Compare commits

..

46 Commits

Author SHA1 Message Date
ringabout
c38fab3576 test 2026-03-05 16:11:11 +08:00
ringabout
8e2547a5e2 fixes #25566; {.align.} pragma where each 16-byte-aligned (#25570)
fixes #25566
2026-03-04 09:14:13 +01:00
Ryan McConnell
46cddbccd6 fixes #25572 ICE evaluating closure iter with object conversion (#25575) 2026-03-04 05:45:16 +01:00
vercingetorx
9ed4077d9a Fix memory leak in asyncdispatch.withTimeout by clearing losing callbacks (#25567)
withTimeout currently leaves the “losing” callback installed:

  - when fut finishes first, timeout callback remains until timer fires,
- when timeout fires first, fut callback remains on the wrapped future.

Under high-throughput use with large future payloads, this retains
closures/future references longer than needed and causes large transient
RSS growth.
This patch clears the opposite callback immediately once outcome is
decided, reducing retention without changing API behavior.
2026-03-01 22:11:18 +01:00
Kevin Hovsäter
e69d672354 Fix warning admonition in std/streams (#25564)
The rest of the body must be indented in order to fall under the warning
admonition. Right now, only the first part of the warning is inside the
admonition, see [std/streams](https://nim-lang.org/docs/streams.html).
2026-03-01 11:36:31 +08:00
ringabout
bd709f9b4c fixes #25262; proc v[T: typedesc]() = discard / v[0]() compiles even though 0 isn't a typedesc (#25558)
fixes #25262

```nim
if constraint != nil and constraint.kind == tyTypeDesc:
  n[i].typ = e.typ
else:
  n[i].typ = e.typ.skipTypes({tyTypeDesc})
```
at least when `constraint` is a typedesc, it should not skip
`tyTypeDesc`


```nim
if arg.kind != tyTypeDesc:
  arg = makeTypeDesc(m.c, arg)
```
Wrappers literals into typedesc, which can cause problems. Though, it
doesn't seem to be necessary
2026-02-28 23:01:09 +01:00
ringabout
4566ffaca9 fixes #25553; Invalid codegen for accessing tuple in array (#25555)
fixes #25553
2026-02-28 22:51:38 +01:00
Kevin Hovsäter
a2db2af5b6 Fix a few typos (#25563)
While fixing a few things in the tutorial, I found a few other typos
lingering in the `doc/` directory.

---------

Co-authored-by: Andreas Rumpf <araq4k@proton.me>
2026-02-28 22:50:37 +01:00
Kevin Hovsäter
c36617c490 Fix std/pegs sequence example (#25562)
This corrects the example used to describe `std/pegs` sequence notion.
It incorrectly used `Z` whereas `C` was expected.
2026-02-28 17:26:44 +08:00
Raka Hourianto
9b2b286baf nre: fix replacement string parser OOB access, numeric refs, and unterminated named refs (#25560)
1. A trailing `$` at the end of a replacement string could read out of
bounds via `how[i + 1]`; this now raises `ValueError` instead.

2. Numeric capture parsing used `id += (id * 10) + digit` instead of `id
= (id * 10) + digit`, so multi-digit refs were parsed incorrectly (e.g.
`$12` resolved as capture 13 instead of 12).

4. Unterminated named replacement syntax (e.g. `${foo)` is now rejected
with ValueError instead of being accepted and parsed inconsistently.

Found and fixed by GPT 5.3 Codex.
2026-02-28 07:39:16 +01:00
Christian Zietz
49961a54dd Atomics can't cause exceptions with Microsoft Visual C++ (#25559)
The `enforcenoraises` pragma prevents generation of exception checking
code for atomic... functions when compiling with Microsoft Visual C++ as
backend.

Fixes #25445

Without this change, the following test program:
```nim
import std/sysatomics

var x: ptr uint64 = cast[ptr uint64](uint64(0))
var y: ptr uint64 = cast[ptr uint64](uint64(42))
let z = atomicExchangeN(addr x, y, ATOMIC_ACQ_REL)

let a = atomicCompareExchangeN(addr x, addr y, y, true, ATOMIC_ACQ_REL, ATOMIC_ACQ_REL)

var v = 42
atomicStoreN(addr v, 43, ATOMIC_ACQ_REL)
let w = atomicLoadN(addr v, ATOMIC_ACQ_REL)
```
... generates this C code when compiling with `--cc:vcc`:
```c
N_LIB_PRIVATE N_NIMCALL(void, NimMainModule)(void) {
        {
        NU64* T1_;
NIM_BOOL T2_;
NI T3_;
NIM_BOOL* nimErr_;
        nimfr_("testexcept", "/tmp/testexcept.nim");
nimErr_ = nimErrorFlag();
        nimlf_(7, "/tmp/testexcept.nim");T1_ = ((NU64*) 0);
T1_ = atomicExchangeN__testexcept_u4((&x__testexcept_u2), y__testexcept_u3, ((int) 4));
if (NIM_UNLIKELY((*nimErr_))) {
        goto BeforeRet_;
}
z__testexcept_u32 = T1_;
        nimln_(9);T2_ = ((NIM_BOOL) 0);
T2_ = atomicCompareExchangeN__testexcept_u33((&x__testexcept_u2), (&y__testexcept_u3), y__testexcept_u3, NIM_TRUE, ((int) 4), ((int) 4));
if (NIM_UNLIKELY((*nimErr_))) {
        goto BeforeRet_;
}
a__testexcept_u45 = T2_;
        nimln_(12);atomicStoreN__testexcept_u47(((&v__testexcept_u46)), ((NI) 43));
if (NIM_UNLIKELY((*nimErr_))) {
        goto BeforeRet_;
}
        nimln_(13);T3_ = ((NI) 0);
T3_ = atomicLoadN__testexcept_u53(((&v__testexcept_u46)));
if (NIM_UNLIKELY((*nimErr_))) {
        goto BeforeRet_;
}
w__testexcept_u59 = T3_;
BeforeRet_: ;
        nimTestErrorFlag();
                popFrame();
}
}
```

Note the repeated checks for `*nimErr_`.

With this PR applied, the checks vanish:
```c
N_LIB_PRIVATE N_NIMCALL(void, NimMainModule)(void) {
        {
                nimfr_("testexcept", "/tmp/testexcept.nim");
        nimlf_(7, "/tmp/testexcept.nim");z__testexcept_u32 = atomicExchangeN__testexcept_u4((&x__testexcept_u2), y__testexcept_u3, ((int) 4));
        nimln_(9);a__testexcept_u45 = atomicCompareExchangeN__testexcept_u33((&x__testexcept_u2), (&y__testexcept_u3), y__testexcept_u3, NIM_TRUE, ((int) 4), ((int) 4));
        nimln_(12);atomicStoreN__testexcept_u47(((&v__testexcept_u46)), ((NI) 43));
        nimln_(13);w__testexcept_u59 = atomicLoadN__testexcept_u53(((&v__testexcept_u46)));
nimTestErrorFlag();
                popFrame();
}
}
```

For reference, with gcc as backend the generated code looks as follows:
```c
N_LIB_PRIVATE N_NIMCALL(void, NimMainModule)(void) {
        {
                nimfr_("testexcept", "/tmp/testexcept.nim");
        nimlf_(7, "/tmp/testexcept.nim");z__testexcept_u9 = __atomic_exchange_n((&x__testexcept_u2), y__testexcept_u3, __ATOMIC_ACQ_REL);
        nimln_(9);a__testexcept_u18 = __atomic_compare_exchange_n((&x__testexcept_u2), (&y__testexcept_u3), y__testexcept_u3, NIM_TRUE, __ATOMIC_ACQ_REL, __ATOMIC_ACQ_REL);
        nimln_(12);__atomic_store_n(((&v__testexcept_u19)), ((NI) 43), __ATOMIC_ACQ_REL);
        nimln_(13);w__testexcept_u29 = __atomic_load_n(((&v__testexcept_u19)), __ATOMIC_ACQ_REL);
nimTestErrorFlag();
                popFrame();
}
}
```

With this PR the program from #25445 yields the correct output `Error:
unhandled exception: index 4 not in 0 .. 3 [IndexDefect]` instead of
crashing with a SIGSEGV.

PS: Unfortunately, I did not find out how to run the tests with MSVC.
`./koch tests --cc:vcc` doesn't use MSVC.
2026-02-27 19:16:30 +01:00
Kevin Hovsäter
358d9b4497 Fix casing of types in example (#25556)
From the Standard Library Style Guide:

> Type identifiers should be in PascalCase. All other identifiers should
> be in camelCase with the exception of constants which may use
> PascalCase but are not required to.
2026-02-27 09:24:23 +08:00
ringabout
a3157537e1 allows implicitRangeConvs for literals (#25542)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 19:10:00 +01:00
ringabout
74499e4561 fixes #21281; proc f(x: static[auto]) doesn't treat x as static (#25543)
fixes #21281
2026-02-25 19:09:24 +01:00
dependabot[bot]
d0ff0ebb43 Bump actions/setup-node from 4 to 6 (#25545)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4
to 6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-node/releases">actions/setup-node's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<p><strong>Breaking Changes</strong></p>
<ul>
<li>Limit automatic caching to npm, update workflows and documentation
by <a
href="https://github.com/priyagupta108"><code>@​priyagupta108</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1374">actions/setup-node#1374</a></li>
</ul>
<p><strong>Dependency Upgrades</strong></p>
<ul>
<li>Upgrade ts-jest from 29.1.2 to 29.4.1 and document breaking changes
in v5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-node/pull/1336">#1336</a></li>
<li>Upgrade prettier from 2.8.8 to 3.6.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-node/pull/1334">#1334</a></li>
<li>Upgrade actions/publish-action from 0.3.0 to 0.4.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-node/pull/1362">#1362</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v5...v6.0.0">https://github.com/actions/setup-node/compare/v5...v6.0.0</a></p>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<h3>Breaking Changes</h3>
<ul>
<li>Enhance caching in setup-node with automatic package manager
detection by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1348">actions/setup-node#1348</a></li>
</ul>
<p>This update, introduces automatic caching when a valid
<code>packageManager</code> field is present in your
<code>package.json</code>. This aims to improve workflow performance and
make dependency management more seamless.
To disable this automatic caching, set <code>package-manager-cache:
false</code></p>
<pre lang="yaml"><code>steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
  with:
    package-manager-cache: false
</code></pre>
<ul>
<li>Upgrade action to use node24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1325">actions/setup-node#1325</a></li>
</ul>
<p>Make sure your runner is on version v2.327.1 or later to ensure
compatibility with this release. <a
href="https://github.com/actions/runner/releases/tag/v2.327.1">See
Release Notes</a></p>
<h3>Dependency Upgrades</h3>
<ul>
<li>Upgrade <code>@​octokit/request-error</code> and
<code>@​actions/github</code> by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-node/pull/1227">actions/setup-node#1227</a></li>
<li>Upgrade uuid from 9.0.1 to 11.1.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-node/pull/1273">actions/setup-node#1273</a></li>
<li>Upgrade undici from 5.28.5 to 5.29.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-node/pull/1295">actions/setup-node#1295</a></li>
<li>Upgrade form-data to bring in fix for critical vulnerability by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1332">actions/setup-node#1332</a></li>
<li>Upgrade actions/checkout from 4 to 5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-node/pull/1345">actions/setup-node#1345</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1348">actions/setup-node#1348</a></li>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1325">actions/setup-node#1325</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v4...v5.0.0">https://github.com/actions/setup-node/compare/v4...v5.0.0</a></p>
<h2>v4.4.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="6044e13b5d"><code>6044e13</code></a>
Docs: bump actions/checkout from v5 to v6 (<a
href="https://redirect.github.com/actions/setup-node/issues/1468">#1468</a>)</li>
<li><a
href="8e494633d0"><code>8e49463</code></a>
Fix README typo (<a
href="https://redirect.github.com/actions/setup-node/issues/1226">#1226</a>)</li>
<li><a
href="621ac41091"><code>621ac41</code></a>
README.md: bump to latest released checkout version v6 (<a
href="https://redirect.github.com/actions/setup-node/issues/1446">#1446</a>)</li>
<li><a
href="2951748f4c"><code>2951748</code></a>
Bump <code>@​actions/cache</code> to v5.0.1 (<a
href="https://redirect.github.com/actions/setup-node/issues/1449">#1449</a>)</li>
<li><a
href="21ddc7bc1f"><code>21ddc7b</code></a>
Correct mirror option typos (<a
href="https://redirect.github.com/actions/setup-node/issues/1442">#1442</a>)</li>
<li><a
href="65d868f8d4"><code>65d868f</code></a>
Update Documentation for Lockfile (<a
href="https://redirect.github.com/actions/setup-node/issues/1454">#1454</a>)</li>
<li><a
href="395ad32622"><code>395ad32</code></a>
Bump js-yaml from 3.14.1 to 3.14.2 (<a
href="https://redirect.github.com/actions/setup-node/issues/1435">#1435</a>)</li>
<li><a
href="a4d2e2bbca"><code>a4d2e2b</code></a>
Bump actions/checkout from 5 to 6 (<a
href="https://redirect.github.com/actions/setup-node/issues/1439">#1439</a>)</li>
<li><a
href="b9b25d45f7"><code>b9b25d4</code></a>
Remove always-auth configuration handling from action (<a
href="https://redirect.github.com/actions/setup-node/issues/1436">#1436</a>)</li>
<li><a
href="633bb92bc0"><code>633bb92</code></a>
Bump <code>@​actions/cache</code> from 4.0.3 to 4.1.0 (<a
href="https://redirect.github.com/actions/setup-node/issues/1384">#1384</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/setup-node/compare/v4...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=4&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ringabout <43030857+ringabout@users.noreply.github.com>
2026-02-25 20:45:44 +08:00
dependabot[bot]
c292981fd3 Bump actions/checkout from 4 to 6 (#25546)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to
6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>v6-beta by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2298">actions/checkout#2298</a></li>
<li>update readme/changelog for v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2311">actions/checkout#2311</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v5.0.0...v6.0.0">https://github.com/actions/checkout/compare/v5.0.0...v6.0.0</a></p>
<h2>v6-beta</h2>
<h2>What's Changed</h2>
<p>Updated persist-credentials to store the credentials under
<code>$RUNNER_TEMP</code> instead of directly in the local git
config.</p>
<p>This requires a minimum Actions Runner version of <a
href="https://github.com/actions/runner/releases/tag/v2.329.0">v2.329.0</a>
to access the persisted credentials for <a
href="https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action">Docker
container action</a> scenarios.</p>
<h2>v5.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v5...v5.0.1">https://github.com/actions/checkout/compare/v5...v5.0.1</a></p>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
<li>Prepare v5.0.0 release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2238">actions/checkout#2238</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v5.0.0">https://github.com/actions/checkout/compare/v4...v5.0.0</a></p>
<h2>v4.3.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v4.3.1">https://github.com/actions/checkout/compare/v4...v4.3.1</a></p>
<h2>v4.3.0</h2>
<h2>What's Changed</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="de0fac2e45"><code>de0fac2</code></a>
Fix tag handling: preserve annotations and explicit fetch-tags (<a
href="https://redirect.github.com/actions/checkout/issues/2356">#2356</a>)</li>
<li><a
href="064fe7f331"><code>064fe7f</code></a>
Add orchestration_id to git user-agent when ACTIONS_ORCHESTRATION_ID is
set (...</li>
<li><a
href="8e8c483db8"><code>8e8c483</code></a>
Clarify v6 README (<a
href="https://redirect.github.com/actions/checkout/issues/2328">#2328</a>)</li>
<li><a
href="033fa0dc0b"><code>033fa0d</code></a>
Add worktree support for persist-credentials includeIf (<a
href="https://redirect.github.com/actions/checkout/issues/2327">#2327</a>)</li>
<li><a
href="c2d88d3ecc"><code>c2d88d3</code></a>
Update all references from v5 and v4 to v6 (<a
href="https://redirect.github.com/actions/checkout/issues/2314">#2314</a>)</li>
<li><a
href="1af3b93b68"><code>1af3b93</code></a>
update readme/changelog for v6 (<a
href="https://redirect.github.com/actions/checkout/issues/2311">#2311</a>)</li>
<li><a
href="71cf2267d8"><code>71cf226</code></a>
v6-beta (<a
href="https://redirect.github.com/actions/checkout/issues/2298">#2298</a>)</li>
<li><a
href="069c695914"><code>069c695</code></a>
Persist creds to a separate file (<a
href="https://redirect.github.com/actions/checkout/issues/2286">#2286</a>)</li>
<li><a
href="ff7abcd0c3"><code>ff7abcd</code></a>
Update README to include Node.js 24 support details and requirements (<a
href="https://redirect.github.com/actions/checkout/issues/2248">#2248</a>)</li>
<li><a
href="08c6903cd8"><code>08c6903</code></a>
Prepare v5.0.0 release (<a
href="https://redirect.github.com/actions/checkout/issues/2238">#2238</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/checkout/compare/v4...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=4&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 16:53:39 +08:00
dependabot[bot]
29705aab1a Bump actions/stale from 9 to 10 (#25548)
Bumps [actions/stale](https://github.com/actions/stale) from 9 to 10.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/stale/releases">actions/stale's
releases</a>.</em></p>
<blockquote>
<h2>v10.0.0</h2>
<h2>What's Changed</h2>
<h3>Breaking Changes</h3>
<ul>
<li>Upgrade to node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1279">actions/stale#1279</a>
Make sure your runner is on version v2.327.1 or later to ensure
compatibility with this release. <a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></li>
</ul>
<h3>Enhancement</h3>
<ul>
<li>Introducing sort-by option by <a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1254">actions/stale#1254</a></li>
</ul>
<h3>Dependency Upgrades</h3>
<ul>
<li>Upgrade actions/publish-immutable-action from 0.0.3 to 0.0.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/stale/pull/1186">actions/stale#1186</a></li>
<li>Upgrade undici from 5.28.4 to 5.28.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/stale/pull/1201">actions/stale#1201</a></li>
<li>Upgrade <code>@​action/cache</code> from 4.0.0 to 4.0.2 by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1226">actions/stale#1226</a></li>
<li>Upgrade <code>@​action/cache</code> from 4.0.2 to 4.0.3 by <a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1233">actions/stale#1233</a></li>
<li>Upgrade undici from 5.28.5 to 5.29.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/stale/pull/1251">actions/stale#1251</a></li>
<li>Upgrade form-data to bring in fix for critical vulnerability by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/stale/pull/1277">actions/stale#1277</a></li>
</ul>
<h3>Documentation changes</h3>
<ul>
<li>Changelog update for recent releases by <a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1224">actions/stale#1224</a></li>
<li>Permissions update in Readme by <a
href="https://github.com/ghadimir"><code>@​ghadimir</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1248">actions/stale#1248</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1224">actions/stale#1224</a></li>
<li><a href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1248">actions/stale#1248</a></li>
<li><a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1277">actions/stale#1277</a></li>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1279">actions/stale#1279</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/stale/compare/v9...v10.0.0">https://github.com/actions/stale/compare/v9...v10.0.0</a></p>
<h2>v9.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Documentation update by <a
href="https://github.com/Marukome0743"><code>@​Marukome0743</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1116">actions/stale#1116</a></li>
<li>Add workflow file for publishing releases to immutable action
package by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1179">actions/stale#1179</a></li>
<li>Update undici from 5.28.2 to 5.28.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1150">actions/stale#1150</a></li>
<li>Update actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1091">actions/stale#1091</a></li>
<li>Update actions/publish-action from 0.2.2 to 0.3.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1147">actions/stale#1147</a></li>
<li>Update ts-jest from 29.1.1 to 29.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1175">actions/stale#1175</a></li>
<li>Update <code>@​actions/core</code> from 1.10.1 to 1.11.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1191">actions/stale#1191</a></li>
<li>Update <code>@​types/jest</code> from 29.5.11 to 29.5.14 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1193">actions/stale#1193</a></li>
<li>Update <code>@​actions/cache</code> from 3.2.2 to 4.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1194">actions/stale#1194</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/Marukome0743"><code>@​Marukome0743</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1116">actions/stale#1116</a></li>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1179">actions/stale#1179</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/stale/compare/v9...v9.1.0">https://github.com/actions/stale/compare/v9...v9.1.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/stale/blob/main/CHANGELOG.md">actions/stale's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h1>[10.1.0]</h1>
<h2>What's Changed</h2>
<ul>
<li>Add only-issue-types option to filter issues by type by <a
href="https://github.com/Bibo-Joshi"><code>@​Bibo-Joshi</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1255">actions/stale#1255</a></li>
</ul>
<h1>[10.0.0]</h1>
<h2>What's Changed</h2>
<h2>Breaking Changes</h2>
<ul>
<li>Upgrade to node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1279">actions/stale#1279</a>
Make sure your runner is on version v2.327.1 or later to ensure
compatibility with this release. <a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></li>
</ul>
<h2>Enhancement</h2>
<ul>
<li>Introducing sort-by option by <a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1254">actions/stale#1254</a></li>
</ul>
<h2>Dependency Upgrades</h2>
<ul>
<li>Upgrade actions/publish-immutable-action from 0.0.3 to 0.0.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/stale/pull/1186">actions/stale#1186</a></li>
<li>Upgrade undici from 5.28.4 to 5.28.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/stale/pull/1201">actions/stale#1201</a></li>
<li>Upgrade <code>@​action/cache</code> from 4.0.0 to 4.0.2 by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1226">actions/stale#1226</a></li>
<li>Upgrade <code>@​action/cache</code> from 4.0.2 to 4.0.3 by <a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1233">actions/stale#1233</a></li>
<li>Upgrade undici from 5.28.5 to 5.29.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/stale/pull/1251">actions/stale#1251</a></li>
<li>Upgrade form-data to bring in fix for critical vulnerability by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/stale/pull/1277">actions/stale#1277</a></li>
</ul>
<h2>Documentation changes</h2>
<ul>
<li>Changelog update for recent releases by <a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1224">actions/stale#1224</a></li>
<li>Permissions update in Readme by <a
href="https://github.com/ghadimir"><code>@​ghadimir</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1248">actions/stale#1248</a></li>
</ul>
<h1>[9.1.0]</h1>
<h2>What's Changed</h2>
<ul>
<li>Documentation update by <a
href="https://github.com/Marukome0743"><code>@​Marukome0743</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1116">actions/stale#1116</a></li>
<li>Add workflow file for publishing releases to immutable action
package by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1179">actions/stale#1179</a></li>
<li>Update undici from 5.28.2 to 5.28.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1150">actions/stale#1150</a></li>
<li>Update actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1091">actions/stale#1091</a></li>
<li>Update actions/publish-action from 0.2.2 to 0.3.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1147">actions/stale#1147</a></li>
<li>Update ts-jest from 29.1.1 to 29.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1175">actions/stale#1175</a></li>
<li>Update <code>@​actions/core</code> from 1.10.1 to 1.11.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1191">actions/stale#1191</a></li>
<li>Update <code>@​types/jest</code> from 29.5.11 to 29.5.14 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1193">actions/stale#1193</a></li>
<li>Update <code>@​actions/cache</code> from 3.2.2 to 4.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1194">actions/stale#1194</a></li>
</ul>
<h1>[9.0.0]</h1>
<h2>Breaking Changes</h2>
<ol>
<li>Action is now stateful: If the action ends because of <a
href="https://github.com/actions/stale#operations-per-run">operations-per-run</a>
then the next run will start from the first unprocessed issue skipping
the issues processed during the previous run(s). The state is reset when
all the issues are processed. This should be considered for scheduling
workflow runs.</li>
<li>Version 9 of this action updated the runtime to Node.js 20. All
scripts are now run with Node.js 20 instead of Node.js 16 and are
affected by any breaking changes between Node.js 16 and 20.</li>
</ol>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b5d41d4e1d"><code>b5d41d4</code></a>
build(deps-dev): bump lodash from 4.17.21 to 4.17.23 (<a
href="https://redirect.github.com/actions/stale/issues/1313">#1313</a>)</li>
<li><a
href="dcd2b9469d"><code>dcd2b94</code></a>
Fix punycode and url.parse Deprecation Warnings (<a
href="https://redirect.github.com/actions/stale/issues/1312">#1312</a>)</li>
<li><a
href="d6f8a33132"><code>d6f8a33</code></a>
build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (<a
href="https://redirect.github.com/actions/stale/issues/1304">#1304</a>)</li>
<li><a
href="a21a081629"><code>a21a081</code></a>
Fix checking state cache (fix <a
href="https://redirect.github.com/actions/stale/issues/1136">#1136</a>),
also switch to octokit methods (<a
href="https://redirect.github.com/actions/stale/issues/1152">#1152</a>)</li>
<li><a
href="997185467f"><code>9971854</code></a>
build(deps): bump actions/checkout from 4 to 6 (<a
href="https://redirect.github.com/actions/stale/issues/1306">#1306</a>)</li>
<li><a
href="5611b9defa"><code>5611b9d</code></a>
build(deps): bump actions/publish-action from 0.3.0 to 0.4.0 (<a
href="https://redirect.github.com/actions/stale/issues/1291">#1291</a>)</li>
<li><a
href="fad0de84e5"><code>fad0de8</code></a>
Improves error handling when rate limiting is disabled on GHES. (<a
href="https://redirect.github.com/actions/stale/issues/1300">#1300</a>)</li>
<li><a
href="39bea7de61"><code>39bea7d</code></a>
Add Missing Input Reading for <code>only-issue-types</code> (<a
href="https://redirect.github.com/actions/stale/issues/1298">#1298</a>)</li>
<li><a
href="e46bbabb3e"><code>e46bbab</code></a>
build(deps-dev): bump <code>@​types/node</code> from 20.10.3 to 24.2.0
and document breakin...</li>
<li><a
href="65d1d4804d"><code>65d1d48</code></a>
build(deps-dev): bump eslint-config-prettier from 8.10.0 to 10.1.8 (<a
href="https://redirect.github.com/actions/stale/issues/1276">#1276</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/stale/compare/v9...v10">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/stale&package-manager=github_actions&previous-version=9&new-version=10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 15:30:55 +08:00
dependabot[bot]
1ff79079a6 Bump actions/github-script from 7 to 8 (#25547)
Bumps [actions/github-script](https://github.com/actions/github-script)
from 7 to 8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/github-script/releases">actions/github-script's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update Node.js version support to 24.x by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/637">actions/github-script#637</a></li>
<li>README for updating actions/github-script from v7 to v8 by <a
href="https://github.com/sneha-krip"><code>@​sneha-krip</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/653">actions/github-script#653</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/637">actions/github-script#637</a></li>
<li><a
href="https://github.com/sneha-krip"><code>@​sneha-krip</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/653">actions/github-script#653</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v7.1.0...v8.0.0">https://github.com/actions/github-script/compare/v7.1.0...v8.0.0</a></p>
<h2>v7.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Upgrade husky to v9 by <a
href="https://github.com/benelan"><code>@​benelan</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/482">actions/github-script#482</a></li>
<li>Add workflow file for publishing releases to immutable action
package by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/485">actions/github-script#485</a></li>
<li>Upgrade IA Publish by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/486">actions/github-script#486</a></li>
<li>Fix workflow status badges by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/497">actions/github-script#497</a></li>
<li>Update usage of <code>actions/upload-artifact</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/512">actions/github-script#512</a></li>
<li>Clear up package name confusion by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/514">actions/github-script#514</a></li>
<li>Update dependencies with <code>npm audit fix</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/515">actions/github-script#515</a></li>
<li>Specify that the used script is JavaScript by <a
href="https://github.com/timotk"><code>@​timotk</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/478">actions/github-script#478</a></li>
<li>chore: Add Dependabot for NPM and Actions by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/472">actions/github-script#472</a></li>
<li>Define <code>permissions</code> in workflows and update actions by
<a href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in
<a
href="https://redirect.github.com/actions/github-script/pull/531">actions/github-script#531</a></li>
<li>chore: Add Dependabot for .github/actions/install-dependencies by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/532">actions/github-script#532</a></li>
<li>chore: Remove .vscode settings by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/533">actions/github-script#533</a></li>
<li>ci: Use github/setup-licensed by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/473">actions/github-script#473</a></li>
<li>make octokit instance available as octokit on top of github, to make
it easier to seamlessly copy examples from GitHub rest api or octokit
documentations by <a
href="https://github.com/iamstarkov"><code>@​iamstarkov</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/508">actions/github-script#508</a></li>
<li>Remove <code>octokit</code> README updates for v7 by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/557">actions/github-script#557</a></li>
<li>docs: add &quot;exec&quot; usage examples by <a
href="https://github.com/neilime"><code>@​neilime</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/546">actions/github-script#546</a></li>
<li>Bump ruby/setup-ruby from 1.213.0 to 1.222.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/github-script/pull/563">actions/github-script#563</a></li>
<li>Bump ruby/setup-ruby from 1.222.0 to 1.229.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/github-script/pull/575">actions/github-script#575</a></li>
<li>Clearly document passing inputs to the <code>script</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/603">actions/github-script#603</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/610">actions/github-script#610</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/benelan"><code>@​benelan</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/482">actions/github-script#482</a></li>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/485">actions/github-script#485</a></li>
<li><a href="https://github.com/timotk"><code>@​timotk</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/478">actions/github-script#478</a></li>
<li><a
href="https://github.com/iamstarkov"><code>@​iamstarkov</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/508">actions/github-script#508</a></li>
<li><a href="https://github.com/neilime"><code>@​neilime</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/546">actions/github-script#546</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/610">actions/github-script#610</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v7...v7.1.0">https://github.com/actions/github-script/compare/v7...v7.1.0</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="ed597411d8"><code>ed59741</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/653">#653</a>
from actions/sneha-krip/readme-for-v8</li>
<li><a
href="2dc352e4ba"><code>2dc352e</code></a>
Bold minimum Actions Runner version in README</li>
<li><a
href="01e118c8d0"><code>01e118c</code></a>
Update README for Node 24 runtime requirements</li>
<li><a
href="8b222ac82e"><code>8b222ac</code></a>
Apply suggestion from <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a></li>
<li><a
href="adc0eeac99"><code>adc0eea</code></a>
README for updating actions/github-script from v7 to v8</li>
<li><a
href="20fe497b3f"><code>20fe497</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/637">#637</a>
from actions/node24</li>
<li><a
href="e7b7f222b1"><code>e7b7f22</code></a>
update licenses</li>
<li><a
href="2c81ba05f3"><code>2c81ba0</code></a>
Update Node.js version support to 24.x</li>
<li>See full diff in <a
href="https://github.com/actions/github-script/compare/v7...v8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/github-script&package-manager=github_actions&previous-version=7&new-version=8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 15:30:30 +08:00
ringabout
a311ac8d22 Add Dependabot configuration for GitHub Actions (#25544)
Added configuration for Dependabot to manage GitHub Actions updates
weekly.
2026-02-25 15:13:13 +08:00
Andreas Rumpf
f3d07ff114 YRC: fixes typo (#25541)
Unrelated CI failures.
2026-02-24 11:12:28 +01:00
ringabout
b51be75613 fixes #25509; removes void fields from a named tuple type (#25515)
fixes #25509

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-24 10:47:14 +01:00
ringabout
1451651fd9 enable --warning:ImplicitRangeConversion (#25477)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-24 09:40:21 +01:00
Tomohiro
fb80f7707d fixes #16754 (#25519)
This PR allows passing the defining type to generic types in the right
side in a type definition like this:
```nim
type
  Foo = object
    x: Option[Foo]
```
I think generic types should be instanciated after all given arguments
are semchecked,
because generic types can access information about them.
(for example, `Option[T]` in std/option checks if `T` is a pointer like
type)
But in this case, need to instanciate `Option[Foo]` before type of
`Foo.x` is determined.
2026-02-24 09:39:06 +01:00
Miroslav Shubernetskiy
86b9245dd6 fix: double check inputIndex in base64.decode (#25531)
fixes https://github.com/nim-lang/Nim/issues/25530

this double checks the index to make sure whitespace related index
increments cannot cause index defect error
2026-02-24 09:37:46 +01:00
ringabout
e58acc2e1e fixes #25005; new doesn't work with ref object (#25532)
fixes #25005

In `semTypeIdent`, when resolving a typedesc parameter inside a generic
instantiation, the code took a shortcut: it returned the symbol of the
element type (`bound = result.typ.elementType.sym`). However, for
generic types like `RpcResponse[T] = ref object`, the instantiated
object type (e.g., `RpcResponse:ObjectType[string]`) is a copy with a
new type ID but still points to the same symbol as the uninstantiated
generic body type. That symbol's .typ refers to the original
uninstantiated type, which still contains unresolved generic params `T`
2026-02-23 13:40:31 +01:00
Andreas Rumpf
6badeb1b4d yrc progress (#25534) 2026-02-23 13:39:55 +01:00
Miran
df42ebc5e6 bump Atlas' version (#25539) 2026-02-22 23:06:55 +01:00
Miran
44eafa7552 update the shipped tools (#25535) 2026-02-22 12:54:36 +01:00
ringabout
15c6249f2c replace benign with gcsafe (#25527)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-20 16:41:06 +01:00
ringabout
1e3caf457b improve alignment for refc (#25525) 2026-02-17 16:00:11 +01:00
Zoom
72e9bfe0a4 Docs: parseopt fixes, runnable examples (#25526)
Follow-up to #25506.
As I mentioned there, I was in the middle of an edit, so here it is.
Splitting to a separate doc skipped.

A couple of minor mistakes fixed, some things made a bit more concise
and short.
2026-02-16 18:26:08 +01:00
Zoom
7c873ca615 Feat: std: parseopt parser modes (#25506)
Adds configurable parser modes to std/parseopt module. **Take two.**

Initially solved the issue of not being able to pass arguments to short
options as you do with most everyday CLI programs, but reading the tests
made me add more features so that some of the behaviour could be changed
and here we are.

**`std/parseopt` now supports three parser modes** via an optional
`mode` parameter in `initOptParser` and `getopt`.

Three modes are provided:
- `NimMode` (default, fully backward compatible),
- `LaxMode` (POSIX-inspired with relaxed short option handling),
- `GnuMode` (stricter GNU-style conventions).

The new modes are marked as experimental in the documentation.

The parser behaviour is controlled by a new `ParserRules` enum, which
provides granular feature flags that modes are built from. This makes it
possible for users with specific requirements to define custom rule sets
by importing private symbols, this is mentioned but clearly marked as
unsupported.

**Backward compatibility:**

The default mode preserves existing behaviour completely, with a single
exception: `allowWhitespaceAfterColon` is deprecated.

Now, `allowWhitespaceAfterColon` doesn't make much sense as a single
tuning knob. The `ParserRule.prSepAllowDelimAfter` controls this now.
As `allowWhitespaceAfterColon` had a default, most calls never mention
it so they will silently migrate to the new `initOptParser` overload. To
cover cases when the proc param was used at call-site, I added an
overload, which modifies the default parser mode to reflect the required
`allowWhitespaceAfterColon` value. Should be all smooth for most users,
except the deprecation warning.

The only thing I think can be classified as the breaking change is a
surprising **bug** of the old parser:

```nim
let p = initOptParser("-n 10 -m20 -k= 30 -40",  shortNoVal =  {'v'})
#                                     ^-disappears
```

This is with the aforementioned `allowWhitespaceAfterColon` being true
by default, of course. In this case the `30` token is skipped
completely. I don't think that's right, so it's fixed.


Things I still don't like about how the old parser and the new default
mode behave:

1. **Parser behaviour is controlled by an emptiness of two containers**.
This is an interesting approach. It's also made more interesting because
the `shortNoVal`/`longNoVal` control both the namesakes, but *and also
how their opposites (value-taking opts) work*.
---

**Edit:**

2. `shortNoVal` is not mandatory:
    ```nim
	let p = initOptParser(@["-a=foo"], shortNoVal = {'a'})
	# Nim, Lax parses as: (cmdShortOption, "a", "foo") 
	# GnuMode  parses as: (cmdShortOption, "a", "=foo")
	```
In this case, even though the user specified `a` as no no-val, parser
ignores it, relying only on the syntax to decide the kind of the
argument. This is especially problematic with the modes that don't use
the rule `prShortAllowSep` (GnuMode), in this case the provided input is
twice invalid, regardless of the `shortNoVal`.

With the current parser architecture, parsing it this way **is
inevitable**, though. We don't have any way to signal the error state
detected with the input, so the user is expected to validate the input
for mistakes.
Bundling positional arguments is nonsensical and short option can't use
the separator character, so `[cmd "a", arg "=foo"]` and `[cmd "a", cmd
"=", cmd "f"...]` are both out of the question **and** would complicate
validating, requiring keeping track of a previous argument. Hope I'm
clear enough on the issue.
	
**Future work:**

1. Looks like the new modes are already usable, but from the discussions
elsewhere it looks like we might want to support special-casing
multi-digit short options (`-XX..`) to allow numerical options greater
than 9. This complicates bundling, though, so requires a bit of thinking
through.

2. Signaling error state?

---------

Co-authored-by: Andreas Rumpf <araq4k@proton.me>
2026-02-16 16:06:18 +01:00
ringabout
97fed258ed fixes #25475; incompatible types errors for array types with different index types (#25505)
fixes #25475

```nim
var x: array[0..1, int] = [0, 1]
var y: array[4'u..5'u, int] = [0, 3]

echo x == y
```

sigmatch treats array compatibility by element type + length, not by the
index (range) type. Perhaps backend should do the same check
2026-02-13 22:59:21 +01:00
Yuriy Glukhov
937e647f4f Importc codegen fix (#25511)
This fixes two issues with impotc'ed types.
1. Passing an importc'ed inherited object to where superclass is
expected emitted `v.Sup` previously. Now it emits `v`, similar to cpp
codegen.
2. Casting between different nim types that resolve to the same C type
previously was done like `*(T*)&v`, now it is just `v`.
2026-02-13 13:29:01 +01:00
Andreas Rumpf
b41049988f attempt to fix final issue with Nim's multi-threaded allocator (#25513) 2026-02-13 11:53:17 +01:00
Andreas Rumpf
04933b773a YRC: bugfixes (#25512) 2026-02-12 13:29:22 +01:00
Andreas Rumpf
5fa11c5686 YRC: bugfixes (#25504) 2026-02-11 17:45:40 +01:00
ringabout
94008531c1 fixes #25457; make rawAlloc support alignment (#25476)
fixes https://github.com/nim-lang/Nim/issues/25457

Small chunks allocate memory in fixed-size cells. Each cell is
positioned at exact multiples of the cell size from the chunk's data
start, which makes it much harder to support alignment

```nim
sysAssert c.size == size, "rawAlloc 6"
if c.freeList == nil:
  sysAssert(c.acc.int + smallChunkOverhead() + size <= SmallChunkSize,
            "rawAlloc 7")
  result = cast[pointer](cast[int](addr(c.data)) +% c.acc.int)
  inc(c.acc, size)
```

See also https://github.com/nim-lang/Nim/pull/12926 

While using big trunk, each allocation gets its own chunk
2026-02-11 11:33:31 +01:00
ringabout
c346a2b228 fixes #25464; infer =dup for distinct types (#25501)
fixes #25464

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-11 10:46:34 +01:00
Andreas Rumpf
f62669a5d5 Yrc typos and omissions (#25500) 2026-02-10 13:21:35 +01:00
Andreas Rumpf
a690a9ac90 YRC: threadsafe cycle collection for Nim (#25495)
First performance numbers:

time tests/arc/torcbench   -- YRC
true peak memory: true

real    0m0,163s
user    0m0,161s
sys     0m0,002s


time tests/arc/torcbench   -- ORC
true peak memory: true

real    0m0,107s
user    0m0,104s
sys     0m0,003s


So it's 1.6x slower. But it's threadsafe and provably correct. (Lean and
model checking via TLA+ used.)

Of course there is always the chance that the implementation is wrong
and doesn't match the model.
2026-02-10 00:04:11 +01:00
lit
9225d9e9e6 fixes #25490; Remove unused gEnv & env from main func (#25497)
closes #25490
2026-02-09 17:34:44 +01:00
ringabout
ae5f864bff fixes #25494; [regression] Crash on enum ranges as default parameters in generic procs (#25496)
fixes #25494;
2026-02-09 11:50:45 +01:00
ringabout
513c9aa69a fixes #25488; Strings can be compared against nil (#25489)
fixes #25488
ref https://github.com/nim-lang/Nim/pull/20222
2026-02-07 20:46:50 +01:00
ringabout
12a2333817 fixes #25464; gives a deprecated warning when =dup is not provided while there being a custom =copy (#25485)
Gives a deprecated warning to keep backwards compatibility

fixes #25464
2026-02-06 02:19:46 +01:00
Yuriy Glukhov
296b2789b5 Fixes #25340 (#25389) 2026-02-06 00:54:04 +01:00
124 changed files with 4520 additions and 798 deletions

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@@ -15,7 +15,7 @@ jobs:
name: ${{ matrix.platform }}-bisects
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install OpenSSL (Windows)
if: |

View File

@@ -53,7 +53,7 @@ jobs:
steps:
- name: 'Checkout'
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 2

View File

@@ -33,14 +33,14 @@ jobs:
NIM_TESTAMENT_BATCH: ${{ matrix.batch }}
steps:
- name: 'Checkout'
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 2
- name: 'Install node.js 20.x'
uses: actions/setup-node@v4
- name: 'Install node.js'
uses: actions/setup-node@v6
with:
node-version: '20.x'
node-version: 24
- name: 'Install dependencies (Linux amd64)'
if: runner.os == 'Linux' && matrix.cpu == 'amd64'

View File

@@ -17,14 +17,14 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: 'Checkout'
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 2
- name: 'Install node.js'
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ''
node-version: 24
- name: 'Install dependencies (Linux amd64)'
if: runner.os == 'Linux' && matrix.cpu == 'amd64'
@@ -60,7 +60,7 @@ jobs:
run: nim c -r -d:release ci/action.nim
- name: 'Comment'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');

View File

@@ -9,7 +9,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
days-before-pr-stale: 365
days-before-pr-close: 30

View File

@@ -33,7 +33,7 @@ errors.
- Bitshift operators (`shl`, `shr`, `ashr`) now apply bitmasking to the right operand in the C/C++/VM/JS backends.
- Adds a new warning enabled by `--warning:ImplicitRangeConversion` that detects downsizing implicit conversions to range types (e.g., `int -> range[0..255]` or `range[1..256] -> range[0..255]`) that could cause runtime panics. Safe conversions like `range[0..255] -> range[0..65535]` and explicit casts are not warned on.
- Adds a new warning `--warning:ImplicitRangeConversion` that detects downsizing implicit conversions to range types (e.g., `int -> range[0..255]` or `range[1..256] -> range[0..255]`) that could cause runtime panics. Safe conversions like `range[0..255] -> range[0..65535]` and explicit casts do not trigger warnings. `int` to `Natural` and `Positive` conversions do not trigger warnings, which can be enabled with `--warning:systemRangeConversion`.
## Standard library additions and changes
@@ -61,6 +61,10 @@ errors.
- `system.setLenUninit` now supports refc, JS and VM backends.
- `std/parseopt` now supports multiple parser modes via a `CliMode` enum.
Modes include `Nim` (default, fully compatible) and two new experimental modes:
`Lax` and `Gnu` for different option parsing behaviors.
[//]: # "Changes:"
- `std/math` The `^` symbol now supports floating-point as exponent in addition to the Natural type.

View File

@@ -202,7 +202,11 @@ type
tySequence,
tyProc,
tyPointer, tyOpenArray,
tyString, tyCstring, tyForward,
tyString, tyCstring,
tyForward,
# a type not yet semchecked
# When semcheck a type section, all types defined in it are initialized to tyForward
tyInt, tyInt8, tyInt16, tyInt32, tyInt64, # signed integers
tyFloat, tyFloat32, tyFloat64, tyFloat128,
tyUInt, tyUInt8, tyUInt16, tyUInt32, tyUInt64,

View File

@@ -69,7 +69,7 @@ proc copyHalf[Key, Val](h, result: Node[Key, Val]) =
result.links[j] = h.links[Mhalf + j]
else:
for j in 0..<Mhalf:
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc) or defined(gcYrc):
result.vals[j] = move h.vals[Mhalf + j]
else:
shallowCopy(result.vals[j], h.vals[Mhalf + j])
@@ -92,7 +92,7 @@ proc insert[Key, Val](h: Node[Key, Val], key: Key, val: Val): Node[Key, Val] =
if less(key, h.keys[j]): break
inc j
for i in countdown(h.entries, j+1):
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc) or defined(gcYrc):
h.vals[i] = move h.vals[i-1]
else:
shallowCopy(h.vals[i], h.vals[i-1])

View File

@@ -331,7 +331,7 @@ proc withTmpIfNeeded(p: BProc, a: TLoc, needsTmp: bool): TLoc =
# Bug https://github.com/status-im/nimbus-eth2/issues/1549
# Aliasing is preferred over stack overflows.
# Also don't regress for non ARC-builds, too risky.
if needsTmp and a.lode.typ != nil and p.config.selectedGC in {gcArc, gcAtomicArc, gcOrc} and
if needsTmp and a.lode.typ != nil and p.config.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcYrc} and
getSize(p.config, a.lode.typ) < 1024:
result = getTemp(p, a.lode.typ, needsInit=false)
genAssignment(p, result, a, {})

View File

@@ -416,7 +416,7 @@ proc genAssignment(p: BProc, dest, src: TLoc, flags: TAssignmentFlags) =
else:
simpleAsgn(p.s(cpsStmts), dest, src)
of tyArray:
if containsGarbageCollectedRef(dest.t) and p.config.selectedGC notin {gcArc, gcAtomicArc, gcOrc, gcHooks}:
if containsGarbageCollectedRef(dest.t) and p.config.selectedGC notin {gcArc, gcAtomicArc, gcOrc, gcYrc, gcHooks}:
genGenericAsgn(p, dest, src, flags)
else:
let rd = rdLoc(dest)
@@ -1832,7 +1832,7 @@ 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} or nfAllFieldsSet notin e.flags
let needsZeroMem = p.config.selectedGC notin {gcArc, gcAtomicArc, gcOrc, gcYrc} or nfAllFieldsSet notin e.flags
if useTemp:
tmp = getTemp(p, t)
r = rdLoc(tmp)
@@ -2751,7 +2751,7 @@ proc genMove(p: BProc; n: PNode; d: var TLoc) =
p.s(cpsStmts).addFieldAssignment(destVal, "p", dotField(srcVal, "p"))
else:
if d.k == locNone: d = getTemp(p, n.typ)
if p.config.selectedGC in {gcArc, gcAtomicArc, gcOrc}:
if p.config.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcYrc}:
genAssignment(p, d, a, {})
var op = getAttachedOp(p.module.g.graph, n.typ, attachedWasMoved)
if op == nil:
@@ -2835,7 +2835,7 @@ proc genSlice(p: BProc; e: PNode; d: var TLoc) =
let (x, y) = genOpenArraySlice(p, e, e.typ, e.typ.elementType,
prepareForMutation = e[1].kind == nkHiddenDeref and
e[1].typ.skipTypes(abstractInst).kind == tyString and
p.config.selectedGC in {gcArc, gcAtomicArc, gcOrc})
p.config.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcYrc})
if d.k == locNone: d = getTemp(p, e.typ)
let dest = rdLoc(d)
p.s(cpsStmts).addFieldAssignment(dest, "Field0", x)
@@ -3039,7 +3039,7 @@ proc genMagicExpr(p: BProc, e: PNode, d: var TLoc, op: TMagic) =
let n = semparallel.liftParallel(p.module.g.graph, p.module.idgen, p.module.module, e)
expr(p, n, d)
of mDeepCopy:
if p.config.selectedGC in {gcArc, gcAtomicArc, gcOrc} and optEnableDeepCopy notin p.config.globalOptions:
if p.config.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcYrc} and optEnableDeepCopy notin p.config.globalOptions:
localError(p.config, e.info,
"for --mm:arc|atomicArc|orc 'deepcopy' support has to be enabled with --deepcopy:on")
@@ -3271,7 +3271,11 @@ proc upConv(p: BProc, n: PNode, d: var TLoc) =
p.s(cpsStmts).addCallStmt(cgsymValue(p.module, "raiseObjectConversionError"))
raiseInstr(p, p.s(cpsStmts))
if n[0].typ.kind != tyObject:
# skip cast when types map to the same C type
# this avoids invalid C code like `*(T*)&x` for types that can't have their address taken (e.g., WASM __externref_t)
if getTypeDesc(p.module, n.typ) == getTypeDesc(p.module, n[0].typ):
expr(p, n[0], d)
elif n[0].typ.kind != tyObject:
let destTyp = getTypeDesc(p.module, n.typ)
let val = rdLoc(a)
if n.isLValue:
@@ -3317,7 +3321,7 @@ proc downConv(p: BProc, n: PNode, d: var TLoc) =
cCast(ptrType(destType),
wrapPar(cAddr(wrapPar(val))))),
a.storage)
elif p.module.compileToCpp:
elif p.module.compileToCpp or isImportedType(src):
# C++ implicitly downcasts for us
expr(p, arg, d)
else:

View File

@@ -16,7 +16,7 @@
## implementation.
template detectVersion(field, corename) =
if m.g.config.selectedGC in {gcArc, gcOrc, gcAtomicArc, gcHooks}:
if m.g.config.selectedGC in {gcArc, gcOrc, gcYrc, gcAtomicArc, gcHooks}:
result = 2
else:
result = 1

View File

@@ -277,7 +277,7 @@ proc isInvalidReturnType(conf: ConfigRef; typ: PType, isProc = true): bool =
of ctStruct:
let t = skipTypes(rettype, typedescInst)
if rettype.isImportedCppType or t.isImportedCppType or
(typ.callConv == ccCDecl and conf.selectedGC in {gcArc, gcAtomicArc, gcOrc}):
(typ.callConv == ccCDecl and conf.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcYrc}):
# prevents nrvo for cdecl procs; # bug #23401
result = false
else:
@@ -1692,7 +1692,7 @@ proc genHook(m: BModule; t: PType; info: TLineInfo; op: TTypeAttachedOp; result:
echo "ayclic but has this =trace ", t, " ", theProc.ast
else:
when false:
if op == attachedTrace and m.config.selectedGC == gcOrc and
if op == attachedTrace and m.config.selectedGC in {gcOrc, gcYrc} and
containsGarbageCollectedRef(t):
# unfortunately this check is wrong for an object type that only contains
# .cursor fields like 'Node' inside 'cycleleak'.

View File

@@ -1332,7 +1332,7 @@ proc genProcLvl3*(m: BModule, prc: PSym) =
# declare the result symbol:
assignLocalVar(p, resNode)
assert(res.loc.snippet != "")
if p.config.selectedGC in {gcArc, gcAtomicArc, gcOrc} and
if p.config.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcYrc} and
allPathsAsgnResult(p, procBody) == InitSkippable:
# In an ideal world the codegen could rely on injectdestructors doing its job properly
# and then the analysis step would not be required.
@@ -1687,7 +1687,7 @@ proc hcrGetProcLoadCode(builder: var Builder, m: BModule, sym, prefix, handle, g
# prevents inlining of the NimMainInner function and dependent
# functions, which might otherwise merge their stack frames.
proc isInnerMainVolatile(m: BModule): bool =
m.config.selectedGC notin {gcNone, gcArc, gcAtomicArc, gcOrc}
m.config.selectedGC notin {gcNone, gcArc, gcAtomicArc, gcOrc, gcYrc}
proc genPreMain(m: BModule) =
m.s[cfsProcs].addDeclWithVisibility(Private):
@@ -1699,8 +1699,6 @@ proc genPreMain(m: BModule) =
m.s[cfsProcs].addVar(name = "cmdCount", typ = CInt)
m.s[cfsProcs].addDeclWithVisibility(Private):
m.s[cfsProcs].addVar(name = "cmdLine", typ = ptrType(ptrType(CChar)))
m.s[cfsProcs].addDeclWithVisibility(Private):
m.s[cfsProcs].addVar(name = "gEnv", typ = ptrType(ptrType(CChar)))
m.s[cfsProcs].addDeclWithVisibility(Private):
m.s[cfsProcs].addProcHeader(m.config.nimMainPrefix & "PreMain", CVoid, cProcParams())
m.s[cfsProcs].finishProcHeaderWithBody():
@@ -1734,7 +1732,7 @@ proc genNimMainInner(m: BModule) =
m.s[cfsProcs].addNewline()
proc initStackBottom(m: BModule): bool =
not (m.config.target.targetOS == osStandalone or m.config.selectedGC in {gcNone, gcArc, gcAtomicArc, gcOrc})
not (m.config.target.targetOS == osStandalone or m.config.selectedGC in {gcNone, gcArc, gcAtomicArc, gcOrc, gcYrc})
proc genNimMainProc(m: BModule, preMainCode: Snippet) =
m.s[cfsProcs].addProcHeader(ccCDecl, m.config.nimMainPrefix & "NimMain", CVoid, cProcParams())
@@ -1761,12 +1759,10 @@ proc genNimMainBody(m: BModule, preMainCode: Snippet) =
proc genPosixCMain(m: BModule) =
m.s[cfsProcs].addProcHeader("main", CInt, cProcParams(
(name: "argc", typ: CInt),
(name: "args", typ: ptrType(ptrType(CChar))),
(name: "env", typ: ptrType(ptrType(CChar)))))
(name: "args", typ: ptrType(ptrType(CChar)))))
m.s[cfsProcs].finishProcHeaderWithBody():
m.s[cfsProcs].addAssignment("cmdLine", "args")
m.s[cfsProcs].addAssignment("cmdCount", "argc")
m.s[cfsProcs].addAssignment("gEnv", "env")
genMainProcsWithResult(m)
m.s[cfsProcs].addNewline()
@@ -1864,7 +1860,7 @@ proc genMainProc(m: BModule) =
builder.addCallStmt(cgsymValue(m, "nimLoadLibraryError"), strLit)
loadLib(preMainBuilder, "hcr_handle", "hcrGetProc")
if m.config.selectedGC in {gcArc, gcAtomicArc, gcOrc}:
if m.config.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcYrc}:
preMainBuilder.addCallStmt(m.config.nimMainPrefix & "PreMain")
else:
preMainBuilder.addVar(name = "rtl_handle", typ = CPointer)
@@ -2034,7 +2030,7 @@ proc registerModuleToMain(g: BModuleList; m: BModule) =
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}:
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")))
@@ -2603,7 +2599,7 @@ proc finalCodegenActions*(graph: ModuleGraph; m: BModule; n: PNode) =
cgsym(m, "rawWrite")
# raise dependencies on behalf of genMainProc
if m.config.target.targetOS != osStandalone and m.config.selectedGC notin {gcNone, gcArc, gcAtomicArc, gcOrc}:
if m.config.target.targetOS != osStandalone and m.config.selectedGC notin {gcNone, gcArc, gcAtomicArc, gcOrc, gcYrc}:
cgsym(m, "initStackBottomWith")
if emulatedThreadVars(m.config) and m.config.target.targetOS != osStandalone:
cgsym(m, "initThreadVarsEmulation")
@@ -2611,7 +2607,7 @@ 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
m.g.config.selectedGC notin {gcArc, gcOrc, gcAtomicArc} or
m.g.config.selectedGC notin {gcArc, gcOrc, gcAtomicArc, gcYrc} or
vtables notin m.g.config.features:
generateIfMethodDispatchers(graph, m.idgen)

View File

@@ -727,7 +727,7 @@ proc lowerStmtListExprs(ctx: var Ctx, n: PNode, needsSplit: var bool): PNode =
n[0] = ex
result.add(n)
of nkCast, nkHiddenStdConv, nkHiddenSubConv, nkConv, nkObjDownConv,
of nkCast, nkHiddenStdConv, nkHiddenSubConv, nkConv, nkObjDownConv, nkObjUpConv,
nkDerefExpr, nkHiddenDeref:
var ns = false
for i in ord(n.kind == nkCast)..<n.len:

View File

@@ -245,7 +245,7 @@ proc processCompile(conf: ConfigRef; filename: string) =
extccomp.addExternalFileToCompile(conf, found)
const
errNoneBoehmRefcExpectedButXFound = "'arc', 'orc', 'atomicArc', 'markAndSweep', 'boehm', 'go', 'none', 'regions', or 'refc' expected, but '$1' found"
errNoneBoehmRefcExpectedButXFound = "'arc', 'orc', 'yrc', 'atomicArc', 'markAndSweep', 'boehm', 'go', 'none', 'regions', or 'refc' expected, but '$1' found"
errNoneSpeedOrSizeExpectedButXFound = "'none', 'speed' or 'size' expected, but '$1' found"
errGuiConsoleOrLibExpectedButXFound = "'gui', 'console', 'lib' or 'staticlib' expected, but '$1' found"
errInvalidExceptionSystem = "'goto', 'setjmp', 'cpp' or 'quirky' expected, but '$1' found"
@@ -266,6 +266,7 @@ proc testCompileOptionArg*(conf: ConfigRef; switch, arg: string, info: TLineInfo
of "markandsweep": result = conf.selectedGC == gcMarkAndSweep
of "destructors", "arc": result = conf.selectedGC == gcArc
of "orc": result = conf.selectedGC == gcOrc
of "yrc": result = conf.selectedGC == gcYrc
of "hooks": result = conf.selectedGC == gcHooks
of "go": result = conf.selectedGC == gcGo
of "none": result = conf.selectedGC == gcNone
@@ -570,6 +571,7 @@ proc unregisterArcOrc*(conf: ConfigRef) =
undefSymbol(conf.symbols, "gcdestructors")
undefSymbol(conf.symbols, "gcarc")
undefSymbol(conf.symbols, "gcorc")
undefSymbol(conf.symbols, "gcyrc")
undefSymbol(conf.symbols, "gcatomicarc")
undefSymbol(conf.symbols, "nimSeqsV2")
undefSymbol(conf.symbols, "nimV2")
@@ -603,6 +605,10 @@ proc processMemoryManagementOption(switch, arg: string, pass: TCmdLinePass,
conf.selectedGC = gcOrc
defineSymbol(conf.symbols, "gcorc")
registerArcOrc(pass, conf)
of "yrc":
conf.selectedGC = gcYrc
defineSymbol(conf.symbols, "gcyrc")
registerArcOrc(pass, conf)
of "atomicarc":
conf.selectedGC = gcAtomicArc
defineSymbol(conf.symbols, "gcatomicarc")

View File

@@ -483,7 +483,7 @@ proc constructCfg*(s: PSym; body: PNode; root: PSym): ControlFlowGraph =
gen(c, body)
if root.kind == skResult:
genImplicitReturn(c)
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc) or defined(gcYrc):
result = c.code # will move
else:
shallowCopy(result, c.code)

View File

@@ -69,7 +69,7 @@ proc hasDestructor(c: Con; t: PType): bool {.inline.} =
result = ast.hasDestructor(t)
when toDebug.len > 0:
# for more effective debugging
if not result and c.graph.config.selectedGC in {gcArc, gcOrc, gcAtomicArc}:
if not result and c.graph.config.selectedGC in {gcArc, gcOrc, gcYrc, gcAtomicArc}:
assert(not containsGarbageCollectedRef(t))
proc getTemp(c: var Con; s: var Scope; typ: PType; info: TLineInfo): PNode =
@@ -165,7 +165,7 @@ proc isLastReadImpl(n: PNode; c: var Con; scope: var Scope): bool =
template hasDestructorOrAsgn(c: var Con, typ: PType): bool =
# bug #23354; an object type could have a non-trivial assignements when it is passed to a sink parameter
hasDestructor(c, typ) or (c.graph.config.selectedGC in {gcArc, gcOrc, gcAtomicArc} and
hasDestructor(c, typ) or (c.graph.config.selectedGC in {gcArc, gcOrc, gcYrc, gcAtomicArc} and
typ.kind == tyObject and not isTrivial(getAttachedOp(c.graph, typ, attachedAsgn)))
proc isLastRead(n: PNode; c: var Con; s: var Scope): bool =
@@ -329,14 +329,14 @@ proc isCriticalLink(dest: PNode): bool {.inline.} =
result = dest.kind != nkSym
proc finishCopy(c: var Con; result, dest: PNode; flags: set[MoveOrCopyFlag]; isFromSink: bool) =
if c.graph.config.selectedGC == gcOrc and IsExplicitSink notin flags:
if c.graph.config.selectedGC in {gcOrc, gcYrc} and IsExplicitSink notin flags:
# add cyclic flag, but not to sink calls, which IsExplicitSink generates
let t = dest.typ.skipTypes(tyUserTypeClasses + {tyGenericInst, tyAlias, tySink, tyDistinct})
if cyclicType(c.graph, t):
result.add boolLit(c.graph, result.info, isFromSink or isCriticalLink(dest))
proc genMarkCyclic(c: var Con; result, dest: PNode) =
if c.graph.config.selectedGC == gcOrc:
if c.graph.config.selectedGC in {gcOrc, gcYrc}:
let t = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink, tyDistinct})
if cyclicType(c.graph, t):
if t.kind == tyRef:
@@ -457,10 +457,10 @@ proc isCapturedVar(n: PNode): bool =
else: result = false
proc passCopyToSink(n: PNode; c: var Con; s: var Scope): PNode =
result = newNodeIT(nkStmtListExpr, n.info, n.typ)
let nTyp = n.typ.skipTypes(tyUserTypeClasses)
let tmp = c.getTemp(s, nTyp, n.info)
if hasDestructorOrAsgn(c, nTyp):
result = newNodeIT(nkStmtListExpr, n.info, n.typ)
let tmp = c.getTemp(s, nTyp, n.info)
let typ = nTyp.skipTypes({tyGenericInst, tyAlias, tySink})
let op = getAttachedOp(c.graph, typ, attachedDup)
if op != nil and tfHasOwned notin typ.flags:
@@ -494,15 +494,15 @@ proc passCopyToSink(n: PNode; c: var Con; s: var Scope): PNode =
if c.inEnsureMove > 0:
localError(c.graph.config, n.info, errFailedMove,
("cannot move '$1', passing '$1' to a sink parameter introduces an implicit copy") % $n)
# Since we know somebody will take over the produced copy, there is
# no need to destroy it.
result.add tmp
else:
if c.graph.config.selectedGC in {gcArc, gcOrc, gcAtomicArc}:
if c.graph.config.selectedGC in {gcArc, gcOrc, gcYrc, gcAtomicArc}:
assert(not containsManagedMemory(nTyp))
if nTyp.skipTypes(abstractInst).kind in {tyOpenArray, tyVarargs}:
localError(c.graph.config, n.info, "cannot create an implicit openArray copy to be passed to a sink parameter")
result.add newTree(nkAsgn, tmp, p(n, c, s, normal))
# Since we know somebody will take over the produced copy, there is
# no need to destroy it.
result.add tmp
result = p(n, c, s, normal)
proc isDangerousSeq(t: PType): bool {.inline.} =
let t = t.skipTypes(abstractInst)
@@ -926,7 +926,7 @@ proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode; tmpFlags = {sfSing
if n[0].kind == nkSym and n[0].sym.magic in {mNew, mNewFinalize}:
result[0] = copyTree(n[0])
if c.graph.config.selectedGC in {gcHooks, gcArc, gcAtomicArc, gcOrc}:
if c.graph.config.selectedGC in {gcHooks, gcArc, gcAtomicArc, gcOrc, gcYrc}:
let destroyOld = c.genDestroy(result[1])
result = newTree(nkStmtList, destroyOld, result)
else:

View File

@@ -163,7 +163,7 @@ proc fillBodyObj(c: var TLiftCtx; n, body, x, y: PNode; enforceDefaultOp: bool,
if c.filterDiscriminator != nil: return
let f = n.sym
let b = if c.kind == attachedTrace: y else: y.dotField(f)
if (sfCursor in f.flags and c.g.config.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcHooks}) or
if (sfCursor in f.flags and c.g.config.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcYrc, gcHooks}) or
enforceDefaultOp:
defaultOp(c, f.typ, body, x.dotField(f), b)
else:
@@ -558,6 +558,22 @@ proc declareTempOf(c: var TLiftCtx; body: PNode; value: PNode): PNode =
v.addVar(result, value)
body.add v
proc considerInferDupFromCopy(c: var TLiftCtx; t: PType; body, x, y: PNode): bool =
## For `=dup`, if no explicit hook exists, try to infer from `=copy` hook
## to maintain backward compatibility. Returns true if inference was applied.
if c.kind == attachedDup:
var op2 = getAttachedOp(c.g, t, attachedAsgn)
if op2 != nil and sfOverridden in op2.flags:
#markUsed(c.g.config, c.info, op, c.g.usageSym)
onUse(c.info, op2)
body.add genBuiltin(c, mWasMoved, "wasMoved", x)
body.add newHookCall(c, op2, x, y)
result = true
else:
result = false
else:
result = false
proc addIncStmt(c: var TLiftCtx; body, i: PNode) =
let incCall = genBuiltin(c, mInc, "inc", i)
incCall.add lowerings.newIntLit(c.g, c.info, 1)
@@ -721,14 +737,43 @@ proc atomicRefOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
dest[] = source
decRef tmp
For YRC the write barrier is more complicated still and must be:
let tmp = dest
# assignment must come first so that the collector sees the most-recent graph:
atomic: dest[] = source
# Then teach the cycle collector about the changes edge (these use locks, see yrc.nim):
incRef source
decRef tmp
This is implemented as a single runtime call (nimAsgnYrc / nimSinkYrc).
]#
var actions = newNodeI(nkStmtList, c.info)
let elemType = t.elementType
createTypeBoundOps(c.g, c.c, elemType, c.info, c.idgen)
let isCyclic = c.g.config.selectedGC == gcOrc and types.canFormAcycle(c.g, elemType)
let isInheritableAcyclicRef = c.g.config.selectedGC == gcOrc and
# YRC uses dedicated runtime procs for the entire write barrier:
if c.g.config.selectedGC == gcYrc:
let desc =
if isFinal(elemType):
let ti = genBuiltin(c, mGetTypeInfoV2, "getTypeInfoV2", newNodeIT(nkType, x.info, elemType))
ti.typ = getSysType(c.g, c.info, tyPointer)
ti
else:
newNodeIT(nkNilLit, c.info, getSysType(c.g, c.info, tyPointer))
case c.kind
of attachedAsgn, attachedDup:
body.add callCodegenProc(c.g, "nimAsgnYrc", c.info, genAddr(c, x), y, desc)
return
of attachedSink:
body.add callCodegenProc(c.g, "nimSinkYrc", c.info, genAddr(c, x), y, desc)
return
else: discard # fall through for destructor, trace, wasMoved
let isCyclic = c.g.config.selectedGC in {gcOrc, gcYrc} and types.canFormAcycle(c.g, elemType)
let isInheritableAcyclicRef = c.g.config.selectedGC in {gcOrc, gcYrc} and
(not isPureObject(elemType)) and
tfAcyclic in skipTypes(elemType, abstractInst+{tyOwned}-{tyTypeDesc}).flags
# dynamic Acyclic refs need to use dyn decRef
@@ -810,7 +855,26 @@ proc atomicClosureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
let xenv = genBuiltin(c, mAccessEnv, "accessEnv", x)
xenv.typ = getSysType(c.g, c.info, tyPointer)
let isCyclic = c.g.config.selectedGC == gcOrc
# Closures are (fnPtr, env) pairs. nimAsgnYrc/nimSinkYrc handle the env pointer
# (atomic store + buffered inc/dec). We also need newAsgnStmt to copy the fnPtr.
if c.g.config.selectedGC == gcYrc:
let nilDesc = newNodeIT(nkNilLit, c.info, getSysType(c.g, c.info, tyPointer))
let yenv = genBuiltin(c, mAccessEnv, "accessEnv", y)
yenv.typ = getSysType(c.g, c.info, tyPointer)
case c.kind
of attachedAsgn, attachedDup:
# nimAsgnYrc: save old env, atomic store new env, inc new env, dec old env
body.add callCodegenProc(c.g, "nimAsgnYrc", c.info, genAddr(c, xenv), yenv, nilDesc)
# Raw struct copy to also update the function pointer (env write is redundant but benign)
body.add newAsgnStmt(x, y)
return
of attachedSink:
body.add callCodegenProc(c.g, "nimSinkYrc", c.info, genAddr(c, xenv), yenv, nilDesc)
body.add newAsgnStmt(x, y)
return
else: discard # fall through for destructor, trace, wasMoved
let isCyclic = c.g.config.selectedGC in {gcOrc, gcYrc}
let tmp =
if isCyclic and c.kind in {attachedAsgn, attachedSink, attachedDup}:
declareTempOf(c, body, xenv)
@@ -843,7 +907,6 @@ proc atomicClosureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
body.add genIf(c, cond, actions)
else:
body.add genIf(c, yenv, callCodegenProc(c.g, "nimIncRef", c.info, yenv))
body.add genIf(c, cond, actions)
body.add newAsgnStmt(x, y)
of attachedDup:
@@ -928,7 +991,7 @@ proc closureOp(c: var TLiftCtx; t: PType; body, x, y: PNode) =
call[1] = y
body.add newAsgnStmt(x, call)
elif (optOwnedRefs in c.g.config.globalOptions and
optRefCheck in c.g.config.options) or c.g.config.selectedGC in {gcArc, gcAtomicArc, gcOrc}:
optRefCheck in c.g.config.options) or c.g.config.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcYrc}:
let xx = genBuiltin(c, mAccessEnv, "accessEnv", x)
xx.typ = getSysType(c.g, c.info, tyPointer)
case c.kind
@@ -983,7 +1046,7 @@ proc fillBody(c: var TLiftCtx; t: PType; body, x, y: PNode) =
tyPtr, tyUncheckedArray, tyVar, tyLent:
defaultOp(c, t, body, x, y)
of tyRef:
if c.g.config.selectedGC in {gcArc, gcOrc, gcAtomicArc}:
if c.g.config.selectedGC in {gcArc, gcOrc, gcYrc, gcAtomicArc}:
atomicRefOp(c, t, body, x, y)
elif (optOwnedRefs in c.g.config.globalOptions and
optRefCheck in c.g.config.options):
@@ -992,7 +1055,7 @@ proc fillBody(c: var TLiftCtx; t: PType; body, x, y: PNode) =
defaultOp(c, t, body, x, y)
of tyProc:
if t.callConv == ccClosure:
if c.g.config.selectedGC in {gcArc, gcOrc, gcAtomicArc}:
if c.g.config.selectedGC in {gcArc, gcOrc, gcYrc, gcAtomicArc}:
atomicClosureOp(c, t, body, x, y)
else:
closureOp(c, t, body, x, y)
@@ -1053,19 +1116,12 @@ proc fillBody(c: var TLiftCtx; t: PType; body, x, y: PNode) =
elif tfUnion in t.flags: # bug #25236
defaultOp(c, t, body, x, y)
else:
if c.kind == attachedDup:
var op2 = getAttachedOp(c.g, t, attachedAsgn)
if op2 != nil and sfOverridden in op2.flags:
#markUsed(c.g.config, c.info, op, c.g.usageSym)
onUse(c.info, op2)
body.add newHookCall(c, t.assignment, x, y)
else:
fillBodyObjT(c, t, body, x, y)
else:
if not considerInferDupFromCopy(c, t, body, x, y):
fillBodyObjT(c, t, body, x, y)
of tyDistinct:
if not considerUserDefinedOp(c, t, body, x, y):
fillBody(c, t.elementType, body, x, y)
if not considerInferDupFromCopy(c, t, body, x, y):
fillBody(c, t.elementType, body, x, y)
of tyTuple:
fillBodyTup(c, t, body, x, y)
of tyVarargs, tyOpenArray:
@@ -1112,7 +1168,7 @@ proc symDupPrototype(g: ModuleGraph; typ: PType; owner: PSym; kind: TTypeAttache
result.typ.addParam src
if g.config.selectedGC == gcOrc and
if g.config.selectedGC in {gcOrc, gcYrc} and
cyclicType(g, typ.skipTypes(abstractInst)):
let cycleParam = newSym(skParam, getIdent(g.cache, "cyclic"),
idgen, result, info)
@@ -1139,7 +1195,7 @@ proc symPrototype(g: ModuleGraph; typ: PType; owner: PSym; kind: TTypeAttachedOp
let src = newSym(skParam, getIdent(g.cache, if kind == attachedTrace: "env" else: "src"),
idgen, result, info)
if kind == attachedDestructor and g.config.selectedGC in {gcArc, gcOrc, gcAtomicArc} and
if kind == attachedDestructor and g.config.selectedGC in {gcArc, gcOrc, gcYrc, gcAtomicArc} and
((g.config.isDefined("nimPreviewNonVarDestructor") and not isDiscriminant) or (typ.kind in {tyRef, tyString, tySequence})):
dest.typ = typ
else:
@@ -1155,7 +1211,7 @@ proc symPrototype(g: ModuleGraph; typ: PType; owner: PSym; kind: TTypeAttachedOp
if kind notin {attachedDestructor, attachedWasMoved}:
result.typ.addParam src
if kind == attachedAsgn and g.config.selectedGC == gcOrc and
if kind == attachedAsgn and g.config.selectedGC in {gcOrc, gcYrc} and
cyclicType(g, typ.skipTypes(abstractInst)):
let cycleParam = newSym(skParam, getIdent(g.cache, "cyclic"),
idgen, result, info)
@@ -1184,7 +1240,17 @@ proc genTypeFieldCopy(c: var TLiftCtx; t: PType; body, x, y: PNode) =
proc produceSym(g: ModuleGraph; c: PContext; typ: PType; kind: TTypeAttachedOp;
info: TLineInfo; idgen: IdGenerator): PSym =
if typ.kind == tyDistinct:
return produceSymDistinctType(g, c, typ, kind, info, idgen)
# For =dup, if the distinct type has a user-defined =copy, don't delegate
# to the base type. Instead fall through to the normal produceSym logic
# so that fillBody -> considerInferDupFromCopy can synthesize =dup from =copy.
if kind == attachedDup:
let copyOp = getAttachedOp(g, typ, attachedAsgn)
if copyOp != nil and sfOverridden in copyOp.flags:
discard "fall through to normal produceSym logic"
else:
return produceSymDistinctType(g, c, typ, kind, info, idgen)
else:
return produceSymDistinctType(g, c, typ, kind, info, idgen)
result = getAttachedOp(g, typ, kind)
if result == nil:
@@ -1213,14 +1279,22 @@ proc produceSym(g: ModuleGraph; c: PContext; typ: PType; kind: TTypeAttachedOp;
else:
var tk: TTypeKind
var skipped: PType = nil
if g.config.selectedGC in {gcArc, gcOrc, gcHooks, gcAtomicArc}:
if g.config.selectedGC in {gcArc, gcOrc, gcYrc, gcHooks, gcAtomicArc}:
skipped = skipTypes(typ, {tyOrdinal, tyRange, tyInferred, tyGenericInst, tyStatic, tyAlias, tySink})
tk = skipped.kind
else:
tk = tyNone # no special casing for strings and seqs
case tk
of tySequence:
let needsYrcLock = g.config.selectedGC == gcYrc and
kind in {attachedDestructor, attachedSink, attachedAsgn, attachedDeepCopy, attachedDup} and
types.canFormAcycle(g, skipped.elementType)
# YRC: topology-changing seq ops must hold the mutator (read) lock
if needsYrcLock:
result.ast[bodyPos].add callCodegenProc(g, "acquireMutatorLock", info)
fillSeqOp(a, typ, result.ast[bodyPos], d, src)
if needsYrcLock:
result.ast[bodyPos].add callCodegenProc(g, "releaseMutatorLock", info)
of tyString:
fillStrOp(a, typ, result.ast[bodyPos], d, src)
else:
@@ -1335,7 +1409,7 @@ proc createTypeBoundOps(g: ModuleGraph; c: PContext; orig: PType; info: TLineInf
# we do not generate '=trace' procs if we
# have the cycle detection disabled, saves code size.
let lastAttached = if g.config.selectedGC == gcOrc: attachedTrace
let lastAttached = if g.config.selectedGC in {gcOrc, gcYrc}: attachedTrace
else: attachedSink
# bug #15122: We need to produce all prototypes before entering the

View File

@@ -99,6 +99,7 @@ type
warnUser = "User",
warnGlobalVarConstructorTemporary = "GlobalVarConstructorTemporary",
warnImplicitRangeConversion = "ImplicitRangeConversion",
warnSystemRangeConversion = "SystemRangeConversion",
# hints
hintSuccess = "Success", hintSuccessX = "SuccessX",
hintCC = "CC",
@@ -208,6 +209,7 @@ const
warnUser: "$1",
warnGlobalVarConstructorTemporary: "global variable '$1' initialization requires a temporary variable",
warnImplicitRangeConversion: "implicit range conversion $1",
warnSystemRangeConversion: "implicit range conversion $1",
hintSuccess: "operation successful: $#",
# keep in sync with `testament.isSuccess`
hintSuccessX: "$build\n$loc lines; ${sec}s; $mem; proj: $project; out: $output",
@@ -262,7 +264,7 @@ type
proc computeNotesVerbosity(): array[0..3, TNoteKinds] =
result = default(array[0..3, TNoteKinds])
result[3] = {low(TNoteKind)..high(TNoteKind)} - {warnObservableStores, warnResultUsed, warnAnyEnumConv, warnBareExcept, warnStdPrefix, warnImplicitRangeConversion}
result[3] = {low(TNoteKind)..high(TNoteKind)} - {warnObservableStores, warnResultUsed, warnAnyEnumConv, warnBareExcept, warnStdPrefix, warnSystemRangeConversion}
result[2] = result[3] - {hintStackTrace, hintExtendedContext, hintDeclaredLoc, hintProcessingStmt}
result[1] = result[2] - {warnProveField, warnProveIndex,
warnGcUnsafe, hintPath, hintDependency, hintCodeBegin, hintCodeEnd,

View File

@@ -240,7 +240,7 @@ proc setDirtyFile*(conf: ConfigRef; fileIdx: FileIndex; filename: AbsoluteFile)
proc setHash*(conf: ConfigRef; fileIdx: FileIndex; hash: string) =
assert fileIdx.int32 >= 0
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc) or defined(gcYrc):
conf.m.fileInfos[fileIdx.int32].hash = hash
else:
shallowCopy(conf.m.fileInfos[fileIdx.int32].hash, hash)
@@ -248,7 +248,7 @@ proc setHash*(conf: ConfigRef; fileIdx: FileIndex; hash: string) =
proc getHash*(conf: ConfigRef; fileIdx: FileIndex): string =
assert fileIdx.int32 >= 0
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc) or defined(gcYrc):
result = conf.m.fileInfos[fileIdx.int32].hash
else:
shallowCopy(result, conf.m.fileInfos[fileIdx.int32].hash)

View File

@@ -983,7 +983,7 @@ proc genericParamToNif(n: PNode; parent: PNode; c: var TranslationContext) =
toNif n, parent, c
proc addExternName(sym: PSym; c: var TranslationContext) =
if sym.loc.snippet != nil:
if sym.loc.snippet != "":
c.b.addStrLit sym.loc.snippet
else:
c.b.addStrLit sym.name.s

View File

@@ -65,3 +65,7 @@ define:useStdoutAsStdmsg
@if nimHasVtables:
experimental:vtables
@end
@if nimHasImplicitRangeConversion:
warning[ImplicitRangeConversion]:off
@end

View File

@@ -195,6 +195,7 @@ type
gcRegions = "regions"
gcArc = "arc"
gcOrc = "orc"
gcYrc = "yrc" # thread-safe ORC (concurrent cycle collector)
gcAtomicArc = "atomicArc"
gcMarkAndSweep = "markAndSweep"
gcHooks = "hooks"

View File

@@ -567,7 +567,7 @@ proc processCompile(c: PContext, n: PNode) =
n[i] = c.semConstExpr(c, n[i])
case n[i].kind
of nkStrLit, nkRStrLit, nkTripleStrLit:
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc) or defined(gcYrc):
result = n[i].strVal
else:
shallowCopy(result, n[i].strVal)

View File

@@ -231,7 +231,7 @@ proc runNimScript*(cache: IdentCache; scriptName: AbsoluteFile;
if optOwnedRefs in oldGlobalOptions:
conf.globalOptions.incl {optTinyRtti, optOwnedRefs, optSeqDestructors}
defineSymbol(conf.symbols, "nimv2")
if conf.selectedGC in {gcArc, gcOrc, gcAtomicArc}:
if conf.selectedGC in {gcArc, gcOrc, gcYrc, gcAtomicArc}:
conf.globalOptions.incl {optTinyRtti, optSeqDestructors}
defineSymbol(conf.symbols, "nimv2")
defineSymbol(conf.symbols, "gcdestructors")
@@ -241,6 +241,8 @@ proc runNimScript*(cache: IdentCache; scriptName: AbsoluteFile;
defineSymbol(conf.symbols, "gcarc")
of gcOrc:
defineSymbol(conf.symbols, "gcorc")
of gcYrc:
defineSymbol(conf.symbols, "gcyrc")
of gcAtomicArc:
defineSymbol(conf.symbols, "gcatomicarc")
else:

View File

@@ -855,7 +855,7 @@ proc semStmtAndGenerateGenerics(c: PContext, n: PNode): PNode =
appendToModule(c.module, result)
trackStmt(c, c.module, result, isTopLevel = true)
if optMultiMethods notin c.config.globalOptions and
c.config.selectedGC in {gcArc, gcOrc, gcAtomicArc} and
c.config.selectedGC in {gcArc, gcOrc, gcAtomicArc, gcYrc} and
Feature.vtables in c.config.features:
sortVTableDispatchers(c.graph)

View File

@@ -981,7 +981,7 @@ proc setGenericParams(c: PContext, n, expectedParams: PNode) =
if e.typ == nil:
n[i].typ = errorType(c)
else:
n[i].typ = e.typ.skipTypes({tyTypeDesc})
n[i].typ = e.typ
proc explicitGenericInstantiation(c: PContext, n: PNode, s: PSym, doError: bool): PNode =
assert n.kind == nkBracketExpr

View File

@@ -333,7 +333,7 @@ proc isCastable(c: PContext; dst, src: PType, info: TLineInfo): bool =
if skipTypes(dst, abstractInst).kind == tyBuiltInTypeClass:
return false
let conf = c.config
if conf.selectedGC in {gcArc, gcOrc, gcAtomicArc}:
if conf.selectedGC in {gcArc, gcOrc, gcAtomicArc, gcYrc}:
let d = skipTypes(dst, abstractInst)
let s = skipTypes(src, abstractInst)
if d.kind == tyRef and s.kind == tyRef and s[0].isFinal != d[0].isFinal:
@@ -813,7 +813,7 @@ proc semArrayConstr(c: PContext, n: PNode, flags: TExprFlags; expectedType: PTyp
inc(lastIndex)
if isGeneric:
for i in 0..<result.len:
if isIntLit(result[i].typ):
if result[i].typ != nil and isIntLit(result[i].typ):
# generic instantiation strips int lit type which makes conversions fail
result[i].typ = nil
result.typ = nil # current result.typ is invalid, index type is nil
@@ -2800,7 +2800,7 @@ proc semSetConstr(c: PContext, n: PNode, expectedType: PType = nil): PNode =
expectedElementType = typ
if isGeneric:
for i in 0..<n.len:
if isIntLit(n[i].typ):
if n[i].typ != nil and isIntLit(n[i].typ):
# generic instantiation strips int lit type which makes conversions fail
n[i].typ = nil
result.add n[i]
@@ -2913,7 +2913,7 @@ proc semTupleFieldsConstr(c: PContext, n: PNode, flags: TExprFlags; expectedType
result.add n[i]
if isGeneric:
for i in 0..<result.len:
if isIntLit(result[i][1].typ):
if result[i][1].typ != nil and isIntLit(result[i][1].typ):
# generic instantiation strips int lit type which makes conversions fail
result[i][1].typ = nil
result.typ = makeTypeFromExpr(c, result.copyTree)
@@ -2954,7 +2954,7 @@ proc semTuplePositionsConstr(c: PContext, n: PNode, flags: TExprFlags; expectedT
addSonSkipIntLit(typ, n[i].typ.skipTypes({tySink}), c.idgen)
if isGeneric:
for i in 0..<result.len:
if isIntLit(result[i].typ):
if result[i].typ != nil and isIntLit(result[i].typ):
# generic instantiation strips int lit type which makes conversions fail
result[i].typ = nil
result.typ = makeTypeFromExpr(c, result.copyTree)

View File

@@ -236,6 +236,8 @@ proc evalTypeTrait(c: PContext; traitCall: PNode, operand: PType, context: PSym)
let complexObj = containsGarbageCollectedRef(t) or
hasDestructor(t)
result = newIntNodeT(toInt128(ord(not complexObj)), traitCall, c.idgen, c.graph)
of "canFormCycles":
result = newIntNodeT(toInt128(ord(types.canFormAcycle(c.graph, operand))), traitCall, c.idgen, c.graph)
of "hasDefaultValue":
result = newIntNodeT(toInt128(ord(not operand.requiresInit)), traitCall, c.idgen, c.graph)
of "isNamedTuple":

View File

@@ -168,7 +168,7 @@ proc isRangeSupertype(conf: ConfigRef; wider, narrower: PType): bool =
# int -> float ranges; warn
result = false
proc shouldWarnRangeConversion(conf: ConfigRef; formalType, argType: PType): bool =
proc shouldWarnRangeConversion(conf: ConfigRef; info: TLineInfo; formalType, argType: PType): bool =
## Determine if an implicit range conversion should warn
## We warn on conversions that are likely to cause panics
let f = formalType.skipTypes({tyGenericInst, tyAlias, tySink, tyDistinct})
@@ -176,7 +176,19 @@ proc shouldWarnRangeConversion(conf: ConfigRef; formalType, argType: PType): boo
if f.kind == tyRange:
# Only warn if formal range doesn't fully contain argument range
# Check if the ranges don't perfectly overlap
result = not isRangeSupertype(conf, f, a)
if a.kind == tyInt and f.sym != nil and f.sym.owner != nil and
sfSystemModule in f.sym.owner.flags and
(f.sym.name.s == "Positive" or
f.sym.name.s == "Natural"):
# Positive and Natural are special cases that we do not warn on with
# ImplicitRangeConversion, but may warn on with systemRangeConversion
# if that warning is enabled.
if conf.hasWarn(warnSystemRangeConversion):
message(conf, info, warnSystemRangeConversion,
typeToString(argType) & " -> " & typeToString(formalType))
result = false
else:
result = not isRangeSupertype(conf, f, a)
else:
result = false
@@ -1538,7 +1550,8 @@ proc track(tracked: PEffects, n: PNode) =
# Check for implicit range conversions
if n.kind == nkHiddenStdConv and (not tracked.isArrayIndexing) and
shouldWarnRangeConversion(tracked.config, n.typ, n[1].typ):
n[1].kind notin {nkCharLit..nkUInt64Lit, nkFloatLit..nkFloat128Lit} and
shouldWarnRangeConversion(tracked.config, n.info, n.typ, n[1].typ):
message(tracked.config, n.info, warnImplicitRangeConversion,
typeToString(n[1].typ) & " -> " & typeToString(n.typ))
@@ -1756,7 +1769,7 @@ proc trackProc*(c: PContext; s: PSym, body: PNode) =
let param = params[i].sym
let typ = param.typ
if isSinkTypeForParam(typ) or
(t.config.selectedGC in {gcArc, gcOrc, gcAtomicArc} and
(t.config.selectedGC in {gcArc, gcOrc, gcYrc, gcAtomicArc} and
(isClosure(typ.skipTypes(abstractInst)) or param.id in t.escapingParams)):
createTypeBoundOps(t, typ, param.info)
if isOutParam(typ) and param.id notin t.init and s.magic == mNone:

View File

@@ -2175,7 +2175,7 @@ proc bindTypeHook(c: PContext; s: PSym; n: PNode; op: TTypeAttachedOp) =
template notRefc: bool =
# fixes refc with non-var destructor; cancel warnings (#23156)
c.config.backend == backendJs or
c.config.selectedGC in {gcArc, gcAtomicArc, gcOrc}
c.config.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcYrc}
let cond = case op
of attachedWasMoved:
t.len == 2 and t.returnType == nil and t.firstParamType.kind == tyVar

View File

@@ -1014,7 +1014,7 @@ proc skipGenericInvocation(t: PType): PType {.inline.} =
proc tryAddInheritedFields(c: PContext, check: var IntSet, pos: var int,
obj: PType, n: PNode, isPartial = false, innerObj: PType = nil): bool =
if ((not isPartial) and (obj.kind notin {tyObject, tyGenericParam} or tfFinal in obj.flags)) or
(innerObj != nil and obj.sym.id == innerObj.sym.id):
(innerObj != nil and obj.id == innerObj.id):
localError(c.config, n.info, "Cannot inherit from: '" & $obj & "'")
result = false
elif obj.kind == tyObject:
@@ -1149,7 +1149,7 @@ proc semAnyRef(c: PContext; n: PNode; kind: TTypeKind; prev: PType): PType =
result = t
else: discard
if result.kind == tyRef and
c.config.selectedGC in {gcArc, gcOrc, gcAtomicArc} and
c.config.selectedGC in {gcArc, gcOrc, gcAtomicArc, gcYrc} and
tfTriggersCompileTime notin result.flags:
result.incl tfHasAsgn
@@ -1203,7 +1203,15 @@ proc addImplicitGeneric(c: PContext; typeClass: PType, typId: PIdent;
# is this a bindOnce type class already present in the param list?
for i in 0..<genericParams.len:
if genericParams[i].sym.name.id == finalTypId.id:
return genericParams[i].typ
if typeClass.kind == tyStatic and genericParams[i].typ.kind != tyStatic:
# The base type (e.g. from `auto`) was already added as a generic param,
# but `static[auto]` requires upgrading it to a `tyStatic` wrapper so
# it is instantiated as a compile-time value (`skConst`).
genericParams[i].sym.linkTo(typeClass)
typeClass.incl tfImplicitTypeParam
return typeClass
else:
return genericParams[i].typ
let owner = if typeClass.sym != nil: typeClass.sym
else: getCurrOwner(c)
@@ -1739,6 +1747,7 @@ proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType =
var isConcrete = true
let rType = m.call[0].typ
let mIndex = if rType != nil: rType.len - 1 else: -1
var hasForwardTypeParam = false
for i in 1..<m.call.len:
var typ = m.call[i].typ
# is this a 'typedesc' *parameter*? If so, use the typedesc type,
@@ -1755,13 +1764,36 @@ proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType =
skip = false
addToResult(typ, skip)
if typ.kind == tyForward:
hasForwardTypeParam = true
if isConcrete:
if s.ast == nil and s.typ.kind != tyCompositeTypeClass:
# XXX: What kind of error is this? is it still relevant?
localError(c.config, n.info, errCannotInstantiateX % s.name.s)
result = newOrPrevType(tyError, prev, c)
elif containsGenericInvocationWithForward(n[0]):
elif containsGenericInvocationWithForward(n[0]) or hasForwardTypeParam:
# isConcrete == false means this generic type is not instanciated here because it invoked with generic parameters.
# Even if isConcrete == true, don't instanciate it now if there are any `tyForward` type params.
# Such `tyForward` type params will be semchecked later and we can instanciate this next time.
# Some generic types like std/options.Option[T] needs a type kinds of the given type argument.
# return `tyForward` instead of `tyGenericInvocation` because:
# ```nim
# type Foo = object
# x: Option[Foo]
# ```
# returning `tyGenericInvocation` makes `Option[Foo]` to `tyGenericInvocation` and
# next time `semGeneric` is called with `Option[Foo]`, containsGenericType(typeof(`Foo`)) == true
# and `isConcrete == false`.
if prev == nil:
result = newTypeS(tyForward, c)
result.sym = s
else:
assignType(result, newTypeS(tyForward, c))
result.sym = s
c.forwardTypeUpdates.add (result, n) #fixes 1500
return
else:
result = instGenericContainer(c, n.info, result,
allowMetaTypes = false)
@@ -2049,7 +2081,9 @@ proc semTypeIdent(c: PContext, n: PNode): PSym =
# proc signature for example
if c.inGenericInst > 0:
let bound = result.typ.elementType.sym
if bound != nil: return bound
# the symbol may still point to the uninstantiated generic body type
if bound != nil and bound.typ == result.typ.elementType:
return bound
return result
if result.typ.sym == nil:
localError(c.config, n.info, errTypeExpected)
@@ -2390,7 +2424,7 @@ proc semTypeNode(c: PContext, n: PNode, prev: PType): PType =
if n.kind == nkIteratorTy and result.kind == tyProc:
result.incl(tfIterator)
if result.callConv == ccClosure and c.config.selectedGC in {gcArc, gcOrc, gcAtomicArc}:
if result.callConv == ccClosure and c.config.selectedGC in {gcArc, gcOrc, gcAtomicArc, gcYrc}:
result.incl tfHasAsgn
of nkEnumTy: result = semEnum(c, n, prev)
of nkType: result = n.typ

View File

@@ -563,6 +563,26 @@ proc eraseVoidParams*(t: PType) =
setLen t.n.sons, pos
break
proc eraseTupleVoidFields*(t: PType) =
## Remove void fields from a named tuple type, compacting both `t.n`
## (the field symbol nodes) and `t.sonsImpl` (the child types).
if t.n == nil: return # anonymous tuple, nothing to compact
for i in 0..<t.kidsLen:
if t.n[i].kind == nkRecList or t[i].kind == tyVoid:
# found first void field, compact from here
var pos = i
for j in i+1..<t.kidsLen:
if t[j].kind != tyVoid and j < t.n.len and t.n[j].kind != nkRecList:
t.n[pos] = t.n[j]
t[pos] = t[j]
if t.n[pos].kind == nkSym:
t.n[pos].sym.position = pos
inc pos
# else: skip void entries
setLen t.n.sons, pos
t.setSonsLen pos
break
proc skipIntLiteralParams*(t: PType; idgen: IdGenerator) =
for i, p in t.ikids:
if p == nil: continue
@@ -768,6 +788,8 @@ proc replaceTypeVarsTAux(cl: var TReplTypeVars, t: PType, isInstValue = false):
propagateFieldFlags(result, result.n)
if result.kind == tyObject and cl.c.computeRequiresInit(cl.c, result):
result.incl tfRequiresInit
if result.kind == tyTuple:
eraseTupleVoidFields(result)
of tyProc:
eraseVoidParams(result)

View File

@@ -41,6 +41,7 @@ type
CoType
CoOwnerSig
CoIgnoreRange
CoIgnoreRangeInArray
CoConsiderOwned
CoDistinct
CoHashTypeInsideNode
@@ -220,10 +221,17 @@ proc hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]; conf: Confi
else:
for a in t.kids: c.hashType a, flags+{CoIgnoreRange}, conf
of tyRange:
if CoIgnoreRange notin flags:
if {CoIgnoreRange, CoIgnoreRangeInArray} * flags == {}:
c &= char(t.kind)
c.hashTree(t.n, {}, conf)
c.hashType(t.elementType, flags, conf)
c.hashType(t.elementType, flags, conf)
elif CoIgnoreRangeInArray in flags:
# include only the length of the range (not its specific bounds)
c &= char(t.kind)
let l = lengthOrd(conf, t)
lowlevel l
else:
c.hashType(t.elementType, flags, conf)
of tyStatic:
c &= char(t.kind)
c.hashTree(t.n, {}, conf)
@@ -253,7 +261,7 @@ proc hashType(c: var MD5Context, t: PType; flags: set[ConsiderFlag]; conf: Confi
if tfVarargs in t.flags: c &= ".varargs"
of tyArray:
c &= char(t.kind)
c.hashType(t.indexType, flags-{CoIgnoreRange}, conf)
c.hashType(t.indexType, flags-{CoIgnoreRange}+{CoIgnoreRangeInArray}, conf)
c.hashType(t.elementType, flags-{CoIgnoreRange}, conf)
else:
c &= char(t.kind)

View File

@@ -160,8 +160,7 @@ proc matchGenericParam(m: var TCandidate, formal: PType, n: PNode) =
arg = newTypeS(tyStatic, m.c, son = evaluated.typ)
arg.n = evaluated
elif formalBase.kind == tyTypeDesc:
if arg.kind != tyTypeDesc:
arg = makeTypeDesc(m.c, arg)
discard # if arg is not tyTypeDesc, typeRel will report the mismatch
else:
arg = arg.skipTypes({tyTypeDesc})
let tm = typeRel(m, formal, arg)
@@ -1678,7 +1677,6 @@ proc typeRel(c: var TCandidate, f, aOrig: PType,
elif a.kind == tyGenericInst:
if roota.base == rootf.base:
let nextFlags = flags + {trNoCovariance}
var hasCovariance = false
# YYYY
result = isEqual
@@ -1690,7 +1688,7 @@ proc typeRel(c: var TCandidate, f, aOrig: PType,
if res notin {isEqual, isGeneric}:
if trNoCovariance notin flags and ff.kind == aa.kind:
let paramFlags = rootf.base[i-1].flags
hasCovariance =
let hasCovariance =
if tfCovariant in paramFlags:
if tfWeakCovariant in paramFlags:
isCovariantPtr(c, ff, aa)
@@ -1701,35 +1699,36 @@ proc typeRel(c: var TCandidate, f, aOrig: PType,
typeRel(c, aa, ff, flags) == isSubtype
if hasCovariance:
continue
result = isNone
break
return isNone
if prev == nil: put(c, f, a)
else:
let fKind = rootf.last.kind
if fKind in {tyAnd, tyOr}:
result = typeRel(c, last(f), a, flags)
if result != isNone: put(c, f, a)
if result != isNone:
if prev == nil: put(c, f, a)
return
var aAsObject = roota.last
let fKind = rootf.last.kind
if fKind in {tyAnd, tyOr}:
result = typeRel(c, last(f), a, flags)
if result != isNone: put(c, f, a)
return
if fKind in {tyRef, tyPtr}:
if aAsObject.kind == tyObject:
# bug #7600, tyObject cannot be passed
# as argument to tyRef/tyPtr
return isNone
elif aAsObject.kind == fKind:
aAsObject = aAsObject.base
var aAsObject = roota.last
if aAsObject.kind == tyObject and trIsOutParam notin flags:
let baseType = aAsObject.base
if baseType != nil:
if tfFinal notin aAsObject.flags:
inc c.inheritancePenalty, 1 + int(c.inheritancePenalty < 0)
let ret = typeRel(c, f, baseType, flags)
return if ret in {isEqual,isGeneric}: isSubtype else: ret
if fKind in {tyRef, tyPtr}:
if aAsObject.kind == tyObject:
# bug #7600, tyObject cannot be passed
# as argument to tyRef/tyPtr
return isNone
elif aAsObject.kind == fKind:
aAsObject = aAsObject.base
result = isNone
if aAsObject.kind == tyObject and trIsOutParam notin flags:
let baseType = aAsObject.base
if baseType != nil:
if tfFinal notin aAsObject.flags:
inc c.inheritancePenalty, 1 + int(c.inheritancePenalty < 0)
let ret = typeRel(c, f, baseType, flags)
return if ret in {isEqual,isGeneric}: isSubtype else: ret
else:
assert last(origF) != nil
result = typeRel(c, last(origF), a, flags)
@@ -2186,9 +2185,9 @@ proc implicitConv(kind: TNodeKind, f: PType, arg: PNode, m: TCandidate,
result.typ = errorType(c)
else:
result.typ = f.skipTypes({tySink})
# keep varness
# keep varness, but don't wrap lent types with var
if arg.typ != nil and arg.typ.kind == tyVar:
result.typ = toVar(result.typ, tyVar, c.idgen)
result.typ = toVar(result.typ.skipTypes({tyLent}), tyVar, c.idgen)
# copy the tfVarIsPtr flag
result.typ.flags = arg.typ.flags
else:

View File

@@ -37,7 +37,7 @@ proc spawnResult*(t: PType; inParallel: bool): TSpawnResult =
else: srFlowVar
proc flowVarKind(c: ConfigRef, t: PType): TFlowVarKind =
if c.selectedGC in {gcArc, gcOrc, gcAtomicArc}: fvBlob
if c.selectedGC in {gcArc, gcOrc, gcAtomicArc, gcYrc}: fvBlob
elif t.skipTypes(abstractInst).kind in {tyRef, tyString, tySequence}: fvGC
elif containsGarbageCollectedRef(t): fvInvalid
else: fvBlob
@@ -66,7 +66,7 @@ proc addLocalVar(g: ModuleGraph; varSection, varInit: PNode; idgen: IdGenerator;
vpart[2] = if varInit.isNil: v else: vpart[1]
varSection.add vpart
if varInit != nil:
if g.config.selectedGC in {gcArc, gcOrc, gcAtomicArc}:
if g.config.selectedGC in {gcArc, gcOrc, gcAtomicArc, gcYrc}:
# inject destructors pass will do its own analysis
varInit.add newFastMoveStmt(g, newSymNode(result), v)
else:

View File

@@ -120,7 +120,7 @@ template decodeBx(k: untyped) {.dirty.} =
ensureKind(k)
template move(a, b: untyped) {.dirty.} =
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc) or defined(gcYrc):
a = move b
else:
system.shallowCopy(a, b)
@@ -557,7 +557,7 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg =
# Used to keep track of where the execution is resumed.
var savedPC = -1
var savedFrame: PStackFrame = nil
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc) or defined(gcYrc):
template updateRegsAlias = discard
template regs: untyped = tos.slots
else:

View File

@@ -1144,6 +1144,8 @@ semantic analysis). Assignments from the base type to one of its subrange types
A subrange type has the same size as its base type (`int` in the
Subrange example).
Implicit "downsizing" conversions to range types (for example, `int -> range[0..255]` or `range[1..256] -> range[0..255]`) emit the `ImplicitRangeConversion` warning. Conversions that are clearly safe (for example, `range[0..255] -> range[0..65535]`) and any explicit casts do not trigger this warning. Conversions from `int` to common subranges such as `Natural` or `Positive` do not trigger this warning by default, but can be enabled with `--warning:systemRangeConversion`.
Pre-defined floating-point types
--------------------------------
@@ -7904,7 +7906,7 @@ alignment requirement of the type are ignored.
main()
```
This pragma has no effect on the JS backend.
This pragma has no effect on the JavaScript backend and may significantly increase memory usage with the `--mm:refc` option.
Noalias pragma

View File

@@ -2127,7 +2127,7 @@ can be used in an `isolate` context:
`=destroy`(dest.value)
```
The `.sendable` pragma itself is an experimenal, unchecked, unsafe annotation. It is
The `.sendable` pragma itself is an experimental, unchecked, unsafe annotation. It is
currently only used by `Isolated[T]`.
Virtual pragma

View File

@@ -276,9 +276,9 @@ This parser has 2 modes for inline markup:
2) Compatibility mode which is RST rules.
.. Note:: in both modes the parser interpretes text between single
.. Note:: in both modes the parser interprets text between single
backticks (code) identically:
backslash does not escape; the only exception: ``\`` folowed by `
backslash does not escape; the only exception: ``\`` followed by `
does escape so that we can always input a single backtick ` in
inline code. However that makes impossible to input code with
``\`` at the end in *single* backticks, one must use *double*

View File

@@ -52,7 +52,7 @@ Options:
nimgrep --filenames # In current dir
nimgrep --filenames "" DIRECTORY
# Note empty pattern "", lists all files in DIRECTORY
* Interprete patterns:
* Interpret patterns:
--peg PATTERN and PAT are Peg
--re PATTERN and PAT are regular expressions (default)
--rex, -x use the "extended" syntax for the regular expression

View File

@@ -27,7 +27,7 @@ Nim runs on a wide variety of platforms. Support on amd64 and i386 is tested reg
- ppc64el (aka ppc64le)
- riscv64
The following platforms are seldomly tested:
The following platforms are rarely tested:
- alpha
- hppa

View File

@@ -20,8 +20,8 @@ notation meaning
as they succeed. Indicate success if all succeeded.
Otherwise, do not consume any text and indicate failure.
The sequence's precedence is higher than that of ordered
choice: ``A B / C`` means ``(A B) / Z`` and
not ``A (B / Z)``.
choice: ``A B / C`` means ``(A B) / C`` and
not ``A (B / C)``.
``(E)`` Grouping: Parenthesis can be used to change
operator priority.
``{E}`` Capture: Apply expression `E` and store the substring

View File

@@ -1159,8 +1159,8 @@ In Nim new types can be defined within a `type` statement:
```nim test = "nim c $1"
type
biggestInt = int64 # biggest integer type that is available
biggestFloat = float64 # biggest float type that is available
BiggestInt = int64 # biggest integer type that is available
BiggestFloat = float64 # biggest float type that is available
```
Enumeration and object types may only be defined within a

View File

@@ -12,9 +12,9 @@
const
# examples of possible values for repos: Head, ea82b54
NimbleStableCommit = "9207e8b2bbdf66b5a4d1020214cff44d2d30df92" # 0.20.1
AtlasStableCommit = "2aa62121b40d580aa2fb27920a37b938d36c5f57" # 0.9.4
AtlasStableCommit = "ff1f4289482dce94ba9f95b3b0ae16d16e21eb3d" # 0.10.1
ChecksumsStableCommit = "0b8e46379c5bc1bf73d8b3011908389c60fb9b98" # 2.0.1
SatStableCommit = "faf1617f44d7632ee9601ebc13887644925dcc01"
SatStableCommit = "e63eaea8baf00bed8bcd5a29ffd8823abb265b39"
NimonyStableCommit = "deb9b50c573fb55e071825ab55385e293b7216d5" # unversioned \
# Note that Nimony uses Nim as a git submodule but we don't want to install

View File

@@ -433,15 +433,15 @@ when defined(nimHasNoReturnError):
else:
{.pragma: errorNoReturn.}
proc error*(msg: string, n: NimNode = nil) {.magic: "NError", benign, errorNoReturn.}
proc error*(msg: string, n: NimNode = nil) {.magic: "NError", gcsafe, errorNoReturn.}
## Writes an error message at compile time. The optional `n: NimNode`
## parameter is used as the source for file and line number information in
## the compilation error message.
proc warning*(msg: string, n: NimNode = nil) {.magic: "NWarning", benign.}
proc warning*(msg: string, n: NimNode = nil) {.magic: "NWarning", gcsafe.}
## Writes a warning message at compile time.
proc hint*(msg: string, n: NimNode = nil) {.magic: "NHint", benign.}
proc hint*(msg: string, n: NimNode = nil) {.magic: "NHint", gcsafe.}
## Writes a hint message at compile time.
proc newStrLitNode*(s: string): NimNode {.noSideEffect.} =
@@ -511,7 +511,7 @@ proc genSym*(kind: NimSymKind = nskLet; ident = ""): NimNode {.
## Generates a fresh symbol that is guaranteed to be unique. The symbol
## needs to occur in a declaration context.
proc callsite*(): NimNode {.magic: "NCallSite", benign, deprecated:
proc callsite*(): NimNode {.magic: "NCallSite", gcsafe, deprecated:
"Deprecated since v0.18.1; use `varargs[untyped]` in the macro prototype instead".}
## Returns the AST of the invocation expression that invoked this macro.
# see https://github.com/nim-lang/RFCs/issues/387 as candidate replacement.
@@ -933,7 +933,7 @@ proc eqIdent*(a: NimNode; b: NimNode): bool {.magic: "EqIdent", noSideEffect.}
const collapseSymChoice = not defined(nimLegacyMacrosCollapseSymChoice)
proc treeTraverse(n: NimNode; res: var string; level = 0; isLisp = false, indented = false) {.benign.} =
proc treeTraverse(n: NimNode; res: var string; level = 0; isLisp = false, indented = false) {.gcsafe.} =
if level > 0:
if indented:
res.add("\n")
@@ -982,21 +982,21 @@ proc treeTraverse(n: NimNode; res: var string; level = 0; isLisp = false, indent
if isLisp:
res.add(")")
proc treeRepr*(n: NimNode): string {.benign.} =
proc treeRepr*(n: NimNode): string {.gcsafe.} =
## Convert the AST `n` to a human-readable tree-like string.
##
## See also `repr`, `lispRepr`_, and `astGenRepr`_.
result = ""
n.treeTraverse(result, isLisp = false, indented = true)
proc lispRepr*(n: NimNode; indented = false): string {.benign.} =
proc lispRepr*(n: NimNode; indented = false): string {.gcsafe.} =
## Convert the AST `n` to a human-readable lisp-like string.
##
## See also `repr`, `treeRepr`_, and `astGenRepr`_.
result = ""
n.treeTraverse(result, isLisp = true, indented = indented)
proc astGenRepr*(n: NimNode): string {.benign.} =
proc astGenRepr*(n: NimNode): string {.gcsafe.} =
## Convert the AST `n` to the code required to generate that AST.
##
## See also `repr`_, `treeRepr`_, and `lispRepr`_.
@@ -1005,7 +1005,7 @@ proc astGenRepr*(n: NimNode): string {.benign.} =
NodeKinds = {nnkEmpty, nnkIdent, nnkSym, nnkNone, nnkCommentStmt}
LitKinds = {nnkCharLit..nnkInt64Lit, nnkFloatLit..nnkFloat64Lit, nnkStrLit..nnkTripleStrLit}
proc traverse(res: var string, level: int, n: NimNode) {.benign.} =
proc traverse(res: var string, level: int, n: NimNode) {.gcsafe.} =
for i in 0..level-1: res.add " "
if n.kind in NodeKinds:
res.add("new" & ($n.kind).substr(3) & "Node(")

View File

@@ -338,63 +338,3 @@ proc diffText*(textA, textB: string): seq[Item] =
optimize(dataA)
optimize(dataB)
result = createDiffs(dataA, dataB)
proc renderDiff*(a, b: string; res: seq[Item]): string =
## Renders a diff between two strings as a human-readable unified diff format.
##
## `a` the original text
## `b` the modified text
## `res` the sequence of Items from `diffText(a, b)`
##
## Returns a string with the diff output where:
## - Lines prefixed with `-` are deletions from `a`
## - Lines prefixed with `+` are insertions in `b`
## - Lines prefixed with ` ` are context (unchanged)
runnableExamples:
let a = "line1\nline2\nline3"
let b = "line1\nmodified\nline3"
let diff = diffText(a, b)
let rendered = renderDiff(a, b, diff)
assert "-line2" in rendered
assert "+modified" in rendered
let linesA = a.splitLines
let linesB = b.splitLines
var posA = 0
var posB = 0
for item in res:
# Add context lines before this change
while posA < item.startA and posB < item.startB:
result.add ' '
result.add linesA[posA]
result.add '\n'
inc posA
inc posB
# Add deleted lines from A
for i in 0 ..< item.deletedA:
result.add '-'
result.add linesA[item.startA + i]
result.add '\n'
# Add inserted lines from B
for i in 0 ..< item.insertedB:
result.add '+'
result.add linesB[item.startB + i]
result.add '\n'
posA = item.startA + item.deletedA
posB = item.startB + item.insertedB
# Add remaining context lines after the last change
while posA < linesA.len and posB < linesB.len:
result.add ' '
result.add linesA[posA]
result.add '\n'
inc posA
inc posB
proc diffOutput*(a, b: string): string =
renderDiff(a, b, diffText(a, b))

View File

@@ -15,6 +15,9 @@ template formatStr*(howExpr, namegetter, idgetter): untyped =
val.add(how[i])
i += 1
else:
if i + 1 >= how.len:
raise newException(ValueError, "Syntax error in format string at " & $i)
if how[i + 1] == '$':
val.add('$')
i += 2
@@ -27,7 +30,7 @@ template formatStr*(howExpr, namegetter, idgetter): untyped =
i += 1
var id {.inject.} = 0
while i < how.len and how[i] in {'0'..'9'}:
id += (id * 10) + (ord(how[i]) - ord('0'))
id = (id * 10) + (ord(how[i]) - ord('0'))
i += 1
val.add(idgetter)
lastNum = id + 1
@@ -44,6 +47,8 @@ template formatStr*(howExpr, namegetter, idgetter): untyped =
while i < how.len and how[i] != '}':
name.add(how[i])
i += 1
if i >= how.len or how[i] != '}':
raise newException(ValueError, "Syntax error in format string at " & $i)
i += 1
val.add(namegetter)
else:

View File

@@ -1946,9 +1946,14 @@ proc withTimeout*[T](fut: Future[T], timeout: int): owned(Future[bool]) =
retFuture.fail(fut.error)
else:
retFuture.complete(true)
# Timeout side lost; drop its callback to avoid retaining closures/futures.
timeoutFuture.clearCallbacks()
timeoutFuture.callback =
proc () =
if not retFuture.finished: retFuture.complete(false)
if not retFuture.finished:
retFuture.complete(false)
# Wrapped future side lost; drop its callback to avoid retaining closures/futures.
fut.clearCallbacks()
return retFuture
proc accept*(socket: AsyncFD,

View File

@@ -188,7 +188,7 @@ proc processRequest(
# \n
request.headers.clear()
request.body = ""
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc) or defined(gcYrc):
request.hostname = address
else:
request.hostname.shallowCopy(address)

View File

@@ -255,6 +255,9 @@ proc decode*(s: string): string =
while inputIndex <= inputEnds:
while s[inputIndex] in {'\n', '\r', ' '}:
inc inputIndex
# double check inputIndex as it can be incremented due to whitespace
if inputIndex > inputEnds:
break
inputChar(a)
inputChar(b)
inputChar(c)

View File

@@ -36,7 +36,7 @@ when defined(nimPreviewSlimSystem):
import std/assertions
const defaultStackSize = 512 * 1024
const useOrcArc = defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc)
const useOrcArc = defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc) or defined(gcYrc)
when useOrcArc:
proc nimGC_setStackBottom*(theStackBottom: pointer) = discard

View File

@@ -866,7 +866,7 @@ proc parseJson(p: var JsonParser; rawIntegers, rawFloats: bool, depth = 0): Json
case p.tok
of tkString:
# we capture 'p.a' here, so we need to give it a fresh buffer afterwards:
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc) or defined(gcYrc):
result = JsonNode(kind: JString, str: move p.a)
else:
result = JsonNode(kind: JString)

View File

@@ -305,7 +305,7 @@ proc store*[T](s: Stream, data: sink T) =
var stored = initIntSet()
var d: T
when defined(gcArc) or defined(gcOrc)or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc)or defined(gcAtomicArc) or defined(gcYrc):
d = data
else:
shallowCopy(d, data)
@@ -334,7 +334,7 @@ proc `$$`*[T](x: sink T): string =
else:
var stored = initIntSet()
var d: T
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc) or defined(gcYrc):
d = x
else:
shallowCopy(d, x)

View File

@@ -14,149 +14,306 @@
## Supported Syntax
## ================
##
## The following syntax is supported when arguments for the `shortNoVal` and
## `longNoVal` parameters, which are
## `described later<#nimshortnoval-and-nimlongnoval>`_, are not provided:
## The syntax described here applies to the default way the parser works.
## The behavior is configurable, though, and two additional modes
## are supported, see the details: `Parser Modes`_.
##
## 1. Short options: `-abcd`, `-e:5`, `-e=5`
## Parsing also depends on whether the `shortNoVal` and `longNoVal` parameters
## are omitted/empty or provided. The details are described in a
## `later section<#nimshortnoval-and-nimlongnoval>`_.
##
## The following syntax is supported:
##
## 1. Short options: `-a:5`, `-b=5`, `-cde`, `-fgh=5`
## 2. Long options: `--foo:bar`, `--foo=bar`, `--foo`
## 3. Arguments: everything that does not start with a `-`
##
## These three kinds of tokens are enumerated in the
## `CmdLineKind enum<#CmdLineKind>`_.
## Passing values to options **requires** a separator (`:`/`=`), short options
## (flags) can be bundled together and the last one can take a value.
##
## When option values begin with ':' or '=', they need to be doubled up (as in
## `--delim::`) or alternated (as in `--delim=:`).
## Option values can begin with the separator character (`:`/`=`), so all of the
## following is valid:
## - option `foo`, value `:`: `--foo::`, `--foo=:`
## - option `foo`, value `=`: `--foo:=`, `--foo==`
##
## The `--` option, commonly used to denote that every token that follows is
## an argument, is interpreted as a long option, and its name is the empty
## string.
## string. Trailing arguments can be accessed with `remainingArgs<#remainingArgs,OptParser>`_
## or `cmdLineRest<#cmdLineRest,OptParser>`_.
##
## Parsing
## =======
##
## Use an `OptParser<#OptParser>`_ to parse command line options. It can be
## created with `initOptParser<#initOptParser,string,set[char],seq[string]>`_,
## and `next<#next,OptParser>`_ advances the parser by one token.
## To parse command line options, use the `getopt iterator<#getopt.i,OptParser>`_.
## It initializes the `OptParser<#OptParser>`_ object internally and iterates
## through the command line options.
##
## For each token, the parser's `kind`, `key`, and `val` fields give
## information about that token. If the token is a long or short option, `key`
## is the option's name, and `val` is either the option's value, if provided,
## or the empty string. For arguments, the `key` field contains the argument
## itself, and `val` is unused. To check if the end of the command line has
## been reached, check if `kind` is equal to `cmdEnd`.
## For each token, the parser's `kind` (`CmdLineKind enum<#CmdLineKind>`_.),
## `key`, and `val` fields are yielded.
##
## For long and short options, `key` is the option's name, and `val` is either
## the option's value, if given, or an empty string. For arguments, the `key`
## field contains the argument itself, and `val` is unused (empty).
##
## Here is an example:
##
## ```Nim
## import std/parseopt
runnableExamples:
import std/os
let cmds = "-ab -e:5 --foo --bar=20 file.txt".parseCmdLine()
var output: seq[string] = @[]
# If cmds is not supplied, real arguments will be retrieved by the `os` module
for kind, key, val in getopt(cmds):
case kind
of cmdEnd: break
of cmdShortOption, cmdLongOption:
if val == "":
output.add("Option: " & key)
else:
output.add("Option and value: " & key & ", " & val)
of cmdArgument:
output.add("Argument: " & key)
doAssert output == @[
"Option: a",
"Option: b",
"Option and value: e, 5",
"Option: foo",
"Option and value: bar, 20",
"Argument: file.txt"
]
##
## var p = initOptParser("-ab -e:5 --foo --bar=20 file.txt")
## while true:
## p.next()
## case p.kind
## of cmdEnd: break
## of cmdShortOption, cmdLongOption:
## if p.val == "":
## echo "Option: ", p.key
## else:
## echo "Option and value: ", p.key, ", ", p.val
## of cmdArgument:
## echo "Argument: ", p.key
## The `OptParser<#OptParser>`_ can be initialized with
## `initOptParser<#initOptParser,string,set[char],seq[string]>`_.
## The `next<#next,OptParser>`_ proc advances the parser by one token.
##
## # Output:
## # Option: a
## # Option: b
## # Option and value: e, 5
## # Option: foo
## # Option and value: bar, 20
## # Argument: file.txt
## ```
## When iterating the object manually with `next<#next,OptParser>`_, reaching
## the end of the command line is signalled by setting the `kind` field
## to `cmdEnd`.
##
## The `getopt iterator<#getopt.i,OptParser>`_, which is provided for
## convenience, can be used to iterate through all command line options as well.
## To set a default value for an option, assign the default value to a variable
## beforehand, then update it while parsing.
##
## To set a default value for a variable assigned through `getopt` and accept arguments from the cmd line.
## Assign the default value to a variable before parsing.
## Then set the variable to the new value while parsing.
##
## Here is an example:
##
## ```Nim
## import std/parseopt
##
## var varName: string = "defaultValue"
##
## for kind, key, val in getopt():
## case kind
## of cmdArgument:
## discard
## of cmdLongOption, cmdShortOption:
## case key:
## of "varName": # --varName:<value> in the console when executing
## varName = val # do input sanitization in production systems
## of cmdEnd:
## discard
## ```
runnableExamples:
import std/strutils
var varName: string = "defaultValue"
for kind, key, val in getopt(@["--varName:HELLO"]):
case kind
of cmdArgument:
discard
of cmdLongOption, cmdShortOption:
case key
of "varName": # --varName:<value> in the console when executing
varName = val.toLowerAscii() # do input sanitization in production
of cmdEnd:
discard
doAssert varName == "hello"
##
## `shortNoVal` and `longNoVal`
## ============================
##
## The optional `shortNoVal` and `longNoVal` parameters present in
## `initOptParser<#initOptParser,string,set[char],seq[string]>`_ are for
## The optional `shortNoVal` and `longNoVal` parameters in
## `initOptParser<#initOptParser,string,set[char],seq[string]>`_ and
## `getopt iterator<#getopt.i,OptParser>`_ are for
## specifying which short and long options do not accept values.
##
## When `shortNoVal` is non-empty, users are not required to separate short
## options and their values with a ':' or '=' since the parser knows which
## options accept values and which ones do not. This behavior also applies for
## long options if `longNoVal` is non-empty. For short options, `-j4`
## becomes supported syntax, and for long options, `--foo bar` becomes
## supported. This is in addition to the `previously mentioned
## syntax<#supported-syntax>`_. Users can still separate options and their
## values with ':' or '=', but that becomes optional.
## When `shortNoVal` or `longNoVal` is non-empty, using the separators (`:`/`=`)
## becomes non-mandatory and users can separate a value from long
## options (that are not supplied to the corresponding argument) by whitespace
## or, in the case of a short option, by writing the value directly adjacent to
## the option.
##
## For short options, `-j4` becomes supported syntax (parsed as option `j` with
## value `4` instead of two separate options `j` and `4`). For long options,
## `--foo bar` becomes supported syntax in all `modes<Parser Modes>`_.
##
## In `LaxMode` and `GnuMode`, short options can also take values from the next
## argument (`-c val`), but this does **not** work in the default `Nim` mode.
##
## As more options which do not accept values are added to your program,
## remember to amend `shortNoVal` and `longNoVal` accordingly.
##
## The parser does not validate the input for syntax mistakes, thus, options
## can still have values if passed explicitly by the user, even when they are
## marked as `shortNoVal`/`longNoVal`.
##
## This behavior allows associating an option with the mistakenly passed value:
##
runnableExamples:
import std/[sequtils, os]
let cmds = "-n:9 --foo:bar".parseCmdLine()
let parsed = toSeq(cmds.getopt(shortNoVal = {'n'}, longNoVal = @["foo"]))
for (kind, key, val) in parsed:
case kind
of cmdEnd: raise newException(AssertionDefect, "Unreachable")
of cmdShortOption, cmdLongOption:
if key in ["n", "foo"] and val != "":
# Substitute for proper error handling in your code
discard "Option " & key & " can't take values!"
else: discard
of cmdArgument: discard
doAssert parsed == @[
(cmdShortOption, "n", "9"),
(cmdLongOption, "foo", "bar")]
##
## .. Important::
## Next-argument value-taking for short/long options is only enabled when
## `shortNoVal`/`longNoVal` are non-empty. If your program has *no* options
## that take no value, you still must pass a non-empty placeholder (for example,
## `shortNoVal = {'\0'}` and/or `longNoVal = @[""]`) to enable this form.
##
## The following example illustrates the difference between having an empty
## `shortNoVal` and `longNoVal`, which is the default, and providing
## arguments for those two parameters:
##
## ```Nim
## import std/parseopt
runnableExamples:
proc format(kind: CmdLineKind; key, val: string): string =
case kind
of cmdEnd: raise newException(AssertionDefect, "Unreachable")
of cmdShortOption, cmdLongOption:
if val == "": "Option: " & key
else: "Option and value: " & key & ", " & val
of cmdArgument: "Argument: " & key
let cmdLine = "-j4 --first bar"
var output1, output2: seq[string] = @[]
var emptyNoVal = initOptParser(cmdLine)
for kind, key, val in emptyNoVal.getopt():
output1.add format(kind, key, val)
doAssert output1 == @[
"Option: j",
"Option: 4",
"Option: first",
"Argument: bar"
]
var withNoVal = cmdLine.initOptParser(shortNoVal = {'c'},
longNoVal = @["second"])
for kind, key, val in withNoVal.getopt():
output2.add format(kind, key, val)
doAssert output2 == @[
"Option and value: j, 4",
"Option and value: first, bar"
]
##
## proc printToken(kind: CmdLineKind, key: string, val: string) =
## case kind
## of cmdEnd: doAssert(false) # Doesn't happen with getopt()
## of cmdShortOption, cmdLongOption:
## if val == "":
## echo "Option: ", key
## else:
## echo "Option and value: ", key, ", ", val
## of cmdArgument:
## echo "Argument: ", key
## Parser Modes
## ============
##
## let cmdLine = "-j4 --first bar"
## .. Warning:: Modes other than the default (`Nim`) are **experimental** and may
## change in future releases.
##
## var emptyNoVal = initOptParser(cmdLine)
## for kind, key, val in emptyNoVal.getopt():
## printToken(kind, key, val)
## The parser supports several distinct rule sets that change how options are
## interpreted:
##
## # Output:
## # Option: j
## # Option: 4
## # Option: first
## # Argument: bar
## 1. **LaxMode**: Most forgiving mode, combines `Nim` with POSIX-like
## short option handling. Tries to follow the POSIX_ guidelines where possible.
## 2. **NimMode**: Standard Nim parsing rules (default).
## 3. **GnuMode**: GNU-inspired parsing (e.g. `=` as the only delimiter).
## Puts some additional restrictions, following some of the GNU_ conventions.
##
## var withNoVal = initOptParser(cmdLine, shortNoVal = {'c'},
## longNoVal = @["second"])
## for kind, key, val in withNoVal.getopt():
## printToken(kind, key, val)
## Modes are ordered from most relaxed to strictest. The names were
## chosen to set general user expectations and full compliance is neither
## achieved nor planned.
##
## # Output:
## # Option and value: j, 4
## # Option and value: first, bar
## ```
## Mode Differences
## ----------------
##
## **NimMode** (default):
##
## - Short options require adjacent values or explicit delimiters:
## `-cval`, `-c:val`, `-c=val`
## - Short options follow POSIX-style bundling rules
## - Next-argument value taking (`-c val`) is **not** supported by default
## - Supports both `:` and `=` as delimiters
## - Allows whitespace around delimiters
## - Values starting with `-` are interpreted as new options
##
## **LaxMode**:
##
## - Essentially the Nim mode with some relaxations for short options:
## + Allows short options to take values from the next argument: `-c val`
## + Supports bundled short options with trailing value: `-abc val`
## - Values starting with `-` can be consumed as option arguments
##
## **GnuMode**:
##
## - Only `=` is treated as a delimiter (`:` is not a delimiter)
## - No whitespace allowed around `=`
## - Short options can take next-argument values (`-c val`), but only whitespace
## is allowed as a delimiter, separators parse as part of the value
## - Short options follow POSIX-style bundling rules
## - Values starting with `-` can be consumed as option arguments
## - Known discrepancies compared to GNU getopt:
## + No notion of optional/mandatory arguments, colon (`:`) doesn't
## indicate them and overall is not a special character.
##
## Mode-Specific Behavior
## ----------------------
##
## The parser's behavior varies significantly between modes, particularly
## around how options consume their values:
##
## **Short Options**
##
## Consider `-c val`:
##
## - In `Nim` mode: `-c` is parsed as an option without a value, and `val` is
## parsed as a separate argument, regardless of `shortNoVal` being empty or not.
## - In `Lax` and `Gnu` modes:
## + When `shortNoVal` is empty, or not empty and `-c` is in it:
## Same as `Nim`, parsed as option `-c` followed by argument `val`.
## + When `-c` is not in `shortNoVal`:
## parsed as option `-c`, `val` is consumed as its value.
##
## Consider `-c-10`:
##
## - If `shortNoVal` value is empty, all three modes parse three separate short
## options: `c`, `1` and `0`.
## - Otherwise, if `-c` is not in `shortNoVal`:
## + `Nim`: `-c` is an option without an argument. `-10` is interpreted as a
## an option `-1` with the `0` argument.
## + `Lax` and `Gnu` modes: `-10` is consumed as the value of `-c`
## (allowing negative number values).
##
## **Long Options**
##
## Consider `--foo:bar`:
##
## - `Nim`: `:` is a valid delimiter, so `bar` is the value of `--foo`.
## - `LaxMode`: same as `Nim`.
## - `Gnu`: only `=` is a valid delimiter, so this parses as an option named
## `foo:bar` without a value (unless `longNoVal` is non-empty and allows
## next-argument consumption).
##
## Consider `--foo =bar`:
##
## - `Nim`: whitespace around delimiters is allowed, so `=bar` is the
## value of `--foo`.
## - `LaxMode`: same as `Nim`.
## - `Gnu`: whitespace around `=` is not allowed, so `--foo` is an
## option without a value, and `=bar` is parsed as an argument.
##
## Custom Rule Sets
## ================
##
## .. Warning:: Custom rule sets are unsupported and not tested
##
## If you require parsing rules beyond the three provided modes, it's possible
## to define a custom parser behavior by specifying a set of individual parser
## rules.
##
## Due to this feature being unsupported, it requires importing the private
## symbols of the module (with `import std/parseopt {.all.}`) and utilizing
## the unexported `initOptParser` overload, which accepts `set[ParserRules]`
## (see the `ParserRules` enum in the code for details).
##
## See also
## ========
@@ -171,13 +328,42 @@
## parser
## * `parsexml module<parsexml.html>`_ for a XML / HTML parser
## * `other parsers<lib.html#pure-libraries-parsers>`_ for more parsers
## * POSIX_ - The Open Group Base Specifications Issue 8. Utility Conventions
## * GNU_ - GNU C Library reference manual. 26.1.1 Program Argument Syntax Conventions
##
## .. _GNU: https://sourceware.org/glibc/manual/latest/html_node/Argument-Syntax.html
## .. _POSIX: https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap12.html
{.push debugger: off.}
include "system/inclrtl"
import std/strutils
import std/os
when defined(nimscript):
from std/strutils import toLowerAscii, endsWith
type
CliMode* = enum
## Parser behavior profiles used to control parser behavior.
## See `Parser Modes`_ for details.
LaxMode, ## The most forgiving mode
NimMode, ## Nim parsing rules (default)
GnuMode ## GNU-style parsing
type
ParserRules = enum
## Feature flags used to assemble parser behavior for a given mode.
prSepAllowDelimBefore, ## Allow whitespace before an opt-val separator
prSepAllowDelimAfter, ## Allow whitespace after an opt-val separator
prShortAllowSep, ## Allow `-k<separator>val` form
prShortBundle, ## Allow bundling short options behind one '-'
prShortValAllowAdjacent, ## Allow adjacent short option values: `-kval`
prShortValAllowNextArg, ## Allow next-argv short option values: `-k val`
prShortValAllowDashLeading, ## Allow values that start with '-' to be taken
prLongAllowSep, ## Allow `--opt<separator>val` form
prLongValAllowNextArg, ## Allow `--opt val` form, requires non-empty `longNoVal`
prSepAllowColon, ## Allow `:` as an opt-val separator
prSepAllowEq, ## Allow `=` as an opt-val separator
type
CmdLineKind* = enum ## The detected command line token.
@@ -189,21 +375,51 @@ type
## Implementation of the command line parser.
##
## To initialize it, use the
## `initOptParser proc<#initOptParser,string,set[char],seq[string]>`_.
## `initOptParser proc<#initOptParser,string,set[char],seq[string],CliMode>`_.
## `next<#next,OptParser>`_ is used to advance the parser state and move
## through the parsed tokens.
pos: int
inShortState: bool
allowWhitespaceAfterColon: bool
shortNoVal: set[char]
longNoVal: seq[string]
cmds: seq[string]
idx: int
separators: set[char] ## Allowed separators for long/short option values
rules: set[ParserRules]
kind*: CmdLineKind ## The detected command line token
key*, val*: string ## Key and value pair; the key is the option
## or the argument, and the value is not "" if
## the option was given a value
const DelimSet = {'\t', ' '} ## Allowed delimiters between tokens
func toRules(m: CliMode): set[ParserRules] =
## Default rule sets for the given mode `m`
let
Common = {
prSepAllowEq,
prShortValAllowAdjacent,
prShortBundle,
prLongValAllowNextArg,
prLongAllowSep,
}
Lax = {
prSepAllowColon,
prSepAllowDelimBefore,
prSepAllowDelimAfter,
prShortAllowSep,
}
ShortPosix = {
prShortValAllowNextArg,
prShortValAllowDashLeading,
}
case m
of LaxMode: Common + Lax + ShortPosix
of NimMode: Common + Lax
of GnuMode: Common + ShortPosix
proc parseWord(s: string, i: int, w: var string,
delim: set[char] = {'\t', ' '}): int =
delim: set[char] = DelimSet): int =
result = i
if result < s.len and s[result] == '\"':
inc(result)
@@ -218,34 +434,23 @@ proc parseWord(s: string, i: int, w: var string,
add(w, s[result])
inc(result)
proc initOptParser*(cmdline: seq[string], shortNoVal: set[char] = {},
longNoVal: seq[string] = @[];
allowWhitespaceAfterColon = true): OptParser =
## Initializes the command line parser.
##
## If `cmdline.len == 0`, the real command line as provided by the
## `os` module is retrieved instead if it is available. If the
## command line is not available, a `ValueError` will be raised.
## Behavior of the other parameters remains the same as in
## `initOptParser(string, ...)
## <#initOptParser,string,set[char],seq[string]>`_.
##
## See also:
## * `getopt iterator<#getopt.i,seq[string],set[char],seq[string]>`_
runnableExamples:
var p = initOptParser()
p = initOptParser(@["--left", "--debug:3", "-l", "-r:2"])
p = initOptParser(@["--left", "--debug:3", "-l", "-r:2"],
shortNoVal = {'l'}, longNoVal = @["left"])
result = OptParser(pos: 0, idx: 0, inShortState: false,
shortNoVal: shortNoVal, longNoVal: longNoVal,
allowWhitespaceAfterColon: allowWhitespaceAfterColon
proc initOptParser(cmdline: openArray[string];
shortNoVal: set[char];
longNoVal: seq[string];
rules: set[ParserRules]): OptParser =
result = OptParser(pos: 0, idx: 0,
cmds: @cmdline,
inShortState: false,
shortNoVal: shortNoVal,
longNoVal: longNoVal,
separators: {},
rules: rules,
kind: cmdEnd,
key: "", val: "",
)
if cmdline.len != 0:
result.cmds = newSeq[string](cmdline.len)
for i in 0..<cmdline.len:
result.cmds[i] = cmdline[i]
else:
if prSepAllowEq in rules: result.separators.incl('=')
if prSepAllowColon in rules: result.separators.incl(':')
if cmdline.len == 0:
when declared(paramCount):
when defined(nimscript):
var ctr = 0
@@ -254,7 +459,7 @@ proc initOptParser*(cmdline: seq[string], shortNoVal: set[char] = {},
if firstNimsFound:
result.cmds[ctr] = paramStr(i)
inc ctr, 1
if paramStr(i).endsWith(".nims") and not firstNimsFound:
if paramStr(i).toLowerAscii().endsWith(".nims") and not firstNimsFound:
firstNimsFound = true
result.cmds = newSeq[string](paramCount()-i)
else:
@@ -266,25 +471,73 @@ proc initOptParser*(cmdline: seq[string], shortNoVal: set[char] = {},
# access the command line arguments then!
raiseAssert "empty command line given but" &
" real command line is not accessible"
result.kind = cmdEnd
result.key = ""
result.val = ""
proc initOptParser*(cmdline = "", shortNoVal: set[char] = {},
proc initOptParser*(cmdline: seq[string];
shortNoVal: set[char] = {};
longNoVal: seq[string] = @[];
allowWhitespaceAfterColon = true): OptParser =
mode: CliMode = NimMode): OptParser =
## Initializes the command line parser.
##
## If `cmdline == ""`, the real command line as provided by the
## `os` module is retrieved instead if it is available. If the
## command line is not available, a `ValueError` will be raised.
## **Parameters:**
##
## `shortNoVal` and `longNoVal` are used to specify which options
## do not take values. See the `documentation about these
## parameters<#nimshortnoval-and-nimlongnoval>`_ for more information on
## how this affects parsing.
## - `cmdline`: Sequence of command line arguments to parse. If empty, the
## real command line as provided by the `os` module is retrieved instead.
## If the command line is not available, an assertion will be raised.
## - `shortNoVal`: Set of short option characters that do not accept values.
## See `shortNoVal and longNoVal<#nimshortnoval-and-nimlongnoval>`_ for details.
## - `longNoVal`: Sequence of long option names that do not accept values.
## See `shortNoVal and longNoVal<#nimshortnoval-and-nimlongnoval>`_ for details.
## - `mode`: Parser behavior profile (`NimMode`, `LaxMode`, or `GnuMode`).
## See `Parser Modes`_ for details.
##
## This does not provide a way of passing default values to arguments.
## See also:
## * `getopt iterator<#getopt.i,seq[string],set[char],seq[string],CliMode>`_
runnableExamples:
var p = initOptParser()
p = initOptParser(@["--left", "--debug:3", "-l", "-r:2"])
p = initOptParser(@["--left", "--debug:3", "-l", "-r:2"],
shortNoVal = {'l'}, longNoVal = @["left"])
initOptParser(cmdline, shortNoVal, longNoVal, toRules(mode))
proc initOptParser*(cmdline: seq[string],
shortNoVal: set[char] = {},
longNoVal: seq[string] = @[];
allowWhitespaceAfterColon: bool): OptParser {.deprecated:
"`allowWhitespaceAfterColon` is deprecated, use parser modes instead".} =
## This is an overload for continued support of the legacy `allowWhitespaceAfterColon`
## option. It modifies the default parser mode so that the passed value is respected.
##
## Current default parser mode behaves as if `true` was passed (old default)
##
## - `allowWhitespaceAfterColon`: When `true`, allows forms like
## `--option: value` or `--option= value` where the value is in the next
## token after the delimiter. When `false`, the value must be in the same
## token as the delimiter.
var nimrules = toRules(NimMode)
if allowWhitespaceAfterColon == false: nimrules.excl prSepAllowDelimAfter
initOptParser(cmdline, shortNoVal, longNoVal, nimrules)
proc initOptParser*(cmdline = "";
shortNoVal: set[char] = {};
longNoVal: seq[string] = @[];
mode: CliMode = NimMode): OptParser =
## Initializes the command line parser from a command line string.
##
## The `cmdline` string is parsed into tokens using shell-like quoting rules.
##
## **Parameters:**
##
## - `cmdline`: Command line string to parse. If empty, the real command line
## as provided by the `os` module is retrieved instead. If the command line
## is not available, an assertion will be raised.
## - `shortNoVal`: Set of short option characters that do not accept values.
## See `shortNoVal and longNoVal<#nimshortnoval-and-nimlongnoval>`_ for details.
## - `longNoVal`: Sequence of long option names that do not accept values.
## See `shortNoVal and longNoVal<#nimshortnoval-and-nimlongnoval>`_ for details.
## - `mode`: Parser behavior profile (`NimMode`, `LaxMode`, or `GnuMode`).
## See `Parser Modes`_ for details.
##
## **Note:** This does not provide a way of passing default values to arguments.
##
## See also:
## * `getopt iterator<#getopt.i,OptParser>`_
@@ -293,34 +546,81 @@ proc initOptParser*(cmdline = "", shortNoVal: set[char] = {},
p = initOptParser("--left --debug:3 -l -r:2")
p = initOptParser("--left --debug:3 -l -r:2",
shortNoVal = {'l'}, longNoVal = @["left"])
initOptParser(parseCmdLine(cmdline), shortNoVal, longNoVal, toRules(mode))
initOptParser(parseCmdLine(cmdline), shortNoVal, longNoVal, allowWhitespaceAfterColon)
proc initOptParser*(cmdline = "";
shortNoVal: set[char] = {};
longNoVal: seq[string] = @[];
allowWhitespaceAfterColon: bool): OptParser {.deprecated:
"`allowWhitespaceAfterColon` is deprecated, use parser modes instead".} =
## This is an overload for continued support of the legacy `allowWhitespaceAfterColon`
## option. It modifies the default parser mode so that the passed value is respected.
##
## Current default parser mode behaves as if `true` was passed (old default).
##
## - `allowWhitespaceAfterColon`: When `true`, allows forms like
## `--option: value` or `--option= value` where the value is in the next
## token after the delimiter. When `false`, the value must be in the same
## token as the delimiter.
var nimrules = toRules(NimMode)
if allowWhitespaceAfterColon == false: nimrules.excl prSepAllowDelimAfter
initOptParser(parseCmdLine(cmdline), shortNoVal, longNoVal, nimrules)
proc handleShortOption(p: var OptParser; cmd: string) =
var i = p.pos
p.kind = cmdShortOption
if i < cmd.len:
if i < cmd.len: # multidigit short option support goes here
add(p.key, cmd[i])
inc(i)
p.inShortState = true
while i < cmd.len and cmd[i] in {'\t', ' '}:
inc(i)
p.inShortState = false
if i < cmd.len and (cmd[i] in {':', '='} or
card(p.shortNoVal) > 0 and p.key[0] notin p.shortNoVal):
if i < cmd.len and cmd[i] in {':', '='}:
if prSepAllowDelimBefore in p.rules:
while i < cmd.len and cmd[i] in DelimSet:
inc(i)
p.inShortState = false
proc consumeDelims() =
while i < cmd.len and cmd[i] in DelimSet: inc(i)
proc advance(p: var OptParser; n = 1)=
p.inShortState = false
while i < cmd.len and cmd[i] in {'\t', ' '}: inc(i)
p.pos = 0
inc p.idx, n
template next(): untyped = p.cmds[p.idx + 1]
let canTakeVal = card(p.shortNoVal) > 0 and p.key[0] notin p.shortNoVal
if i < cmd.len and cmd[i] in p.separators:
# separator case
if prShortAllowSep in p.rules:
# allow separators: skip the separator and take the value after it
inc(i)
if prSepAllowDelimAfter in p.rules:
consumeDelims()
# prohibit separators: treat separator + remainder as the value
# this represents an error state but produces output that can be validated
p.val = substr(cmd, i)
p.pos = 0
inc p.idx
else:
p.pos = i
p.advance(1)
return
elif canTakeVal and prShortValAllowAdjacent in p.rules and i < cmd.len:
# adjacent value
if prSepAllowDelimBefore in p.rules:
consumeDelims()
p.val = substr(cmd, i)
p.advance(1)
return
elif canTakeVal and
prShortValAllowNextArg in p.rules and
i >= cmd.len and
p.idx + 1 < p.cmds.len and (
prShortValAllowDashLeading in p.rules or
not (next().len > 0 and next()[0] == '-')):
# next-argument value
p.val = next()
p.advance(2)
return
p.pos = i
if i >= cmd.len:
p.inShortState = false
p.pos = 0
inc p.idx
p.advance(1)
proc next*(p: var OptParser) {.rtl, extern: "npo$1".} =
## Parses the next token.
@@ -343,54 +643,71 @@ proc next*(p: var OptParser) {.rtl, extern: "npo$1".} =
return
var i = p.pos
while i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {'\t', ' '}: inc(i)
template cmd(): untyped = p.cmds[p.idx]
template nextArg(): untyped = p.cmds[p.idx + 1]
proc consumeDelims(cmds: openArray[string]; idx: int) =
while i < cmds[idx].len and cmds[idx][i] in DelimSet: inc(i)
proc advance(p: var OptParser; n = 1) =
p.pos = 0
inc p.idx, n
consumeDelims(p.cmds, p.idx)
p.pos = i
setLen(p.key, 0)
setLen(p.val, 0)
if p.inShortState:
p.inShortState = false
if i >= p.cmds[p.idx].len:
inc(p.idx)
p.pos = 0
if i < cmd.len:
handleShortOption(p, p.cmds[p.idx])
return
else:
p.advance(1)
if p.idx >= p.cmds.len:
p.kind = cmdEnd
return
else:
handleShortOption(p, p.cmds[p.idx])
return
if i < p.cmds[p.idx].len and p.cmds[p.idx][i] == '-':
if i < cmd.len and cmd[i] == '-':
inc(i)
if i < p.cmds[p.idx].len and p.cmds[p.idx][i] == '-':
if i < cmd.len and cmd[i] == '-':
p.kind = cmdLongOption
inc(i)
i = parseWord(p.cmds[p.idx], i, p.key, {' ', '\t', ':', '='})
while i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {'\t', ' '}: inc(i)
if i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {':', '='}:
i = parseWord(cmd, i, p.key,
DelimSet + (if prLongAllowSep in p.rules: p.separators else: {}))
if prSepAllowDelimBefore in p.rules:
consumeDelims(p.cmds, p.idx)
if prLongAllowSep in p.rules and i < cmd.len and cmd[i] in p.separators:
inc(i)
while i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {'\t', ' '}: inc(i)
# if we're at the end, use the next command line option:
if i >= p.cmds[p.idx].len and p.idx < p.cmds.len and
p.allowWhitespaceAfterColon:
inc p.idx
i = 0
if p.idx < p.cmds.len:
p.val = p.cmds[p.idx].substr(i)
elif len(p.longNoVal) > 0 and p.key notin p.longNoVal and p.idx+1 < p.cmds.len:
p.val = p.cmds[p.idx+1]
inc p.idx
if prSepAllowDelimAfter in p.rules:
consumeDelims(p.cmds, p.idx)
if i >= cmd.len and p.idx + 1 < p.cmds.len and
prSepAllowDelimAfter in p.rules:
p.val = nextArg()
p.advance(2)
else:
p.val = cmd.substr(i)
p.advance(1)
elif prLongValAllowNextArg in p.rules and
len(p.longNoVal) > 0 and
p.key notin p.longNoVal and
p.idx + 1 < p.cmds.len:
p.val = nextArg()
p.advance(2)
else:
p.val = ""
inc p.idx
p.pos = 0
if i < cmd.len:
# Leave remainder of the current token to be parsed as an argument.
consumeDelims(p.cmds, p.idx)
p.cmds[p.idx] = cmd.substr(i)
else:
p.advance(1)
else:
p.pos = i
handleShortOption(p, p.cmds[p.idx])
handleShortOption(p, cmd)
else:
p.kind = cmdArgument
p.key = p.cmds[p.idx]
inc p.idx
p.pos = 0
p.key = cmd
p.advance(1)
when declared(quoteShellCommand):
proc cmdLineRest*(p: OptParser): string {.rtl, extern: "npo$1".} =
@@ -399,15 +716,13 @@ when declared(quoteShellCommand):
## See also:
## * `remainingArgs proc<#remainingArgs,OptParser>`_
##
## **Examples:**
## ```Nim
## var p = initOptParser("--left -r:2 -- foo.txt bar.txt")
## while true:
## p.next()
## if p.kind == cmdLongOption and p.key == "": # Look for "--"
## break
## doAssert p.cmdLineRest == "foo.txt bar.txt"
## ```
runnableExamples:
var p = initOptParser("--left -r:2 -- foo.txt bar.txt")
while true:
p.next()
if p.kind == cmdLongOption and p.key == "": # Look for "--"
break
doAssert p.cmdLineRest == "foo.txt bar.txt"
result = p.cmds[p.idx .. ^1].quoteShellCommand
proc remainingArgs*(p: OptParser): seq[string] {.rtl, extern: "npo$1".} =
@@ -416,15 +731,13 @@ proc remainingArgs*(p: OptParser): seq[string] {.rtl, extern: "npo$1".} =
## See also:
## * `cmdLineRest proc<#cmdLineRest,OptParser>`_
##
## **Examples:**
## ```Nim
## var p = initOptParser("--left -r:2 -- foo.txt bar.txt")
## while true:
## p.next()
## if p.kind == cmdLongOption and p.key == "": # Look for "--"
## break
## doAssert p.remainingArgs == @["foo.txt", "bar.txt"]
## ```
runnableExamples:
var p = initOptParser("--left -r:2 -- foo.txt bar.txt")
while true:
p.next()
if p.kind == cmdLongOption and p.key == "": # Look for "--"
break
doAssert p.remainingArgs == @["foo.txt", "bar.txt"]
result = @[]
for i in p.idx..<p.cmds.len: result.add p.cmds[i]
@@ -439,29 +752,26 @@ iterator getopt*(p: var OptParser): tuple[kind: CmdLineKind, key,
## See also:
## * `initOptParser proc<#initOptParser,string,set[char],seq[string]>`_
##
## **Examples:**
##
## ```Nim
## # these are placeholders, of course
## proc writeHelp() = discard
## proc writeVersion() = discard
##
## var filename: string
## var p = initOptParser("--left --debug:3 -l -r:2")
##
## for kind, key, val in p.getopt():
## case kind
## of cmdArgument:
## filename = key
## of cmdLongOption, cmdShortOption:
## case key
## of "help", "h": writeHelp()
## of "version", "v": writeVersion()
## of cmdEnd: assert(false) # cannot happen
## if filename == "":
## # no filename has been given, so we show the help
## writeHelp()
## ```
runnableExamples:
# these are placeholders, of course
proc writeHelp() = discard
proc writeVersion() = discard
var filename: string = ""
var p = initOptParser("--left --debug:3 -l -r:2")
for kind, key, val in p.getopt():
case kind
of cmdArgument:
filename = key
of cmdLongOption, cmdShortOption:
case key
of "help", "h": writeHelp()
of "version", "v": writeVersion()
of cmdEnd: assert(false) # cannot happen
if filename == "":
# no filename has been given, so we show the help
writeHelp()
p.pos = 0
p.idx = 0
while true:
@@ -469,8 +779,10 @@ iterator getopt*(p: var OptParser): tuple[kind: CmdLineKind, key,
if p.kind == cmdEnd: break
yield (p.kind, p.key, p.val)
iterator getopt*(cmdline: seq[string] = @[],
shortNoVal: set[char] = {}, longNoVal: seq[string] = @[]):
iterator getopt*(cmdline: seq[string] = @[];
shortNoVal: set[char] = {};
longNoVal: seq[string] = @[];
mode: CliMode = NimMode):
tuple[kind: CmdLineKind, key, val: string] =
## Convenience iterator for iterating over command line arguments.
##
@@ -483,6 +795,9 @@ iterator getopt*(cmdline: seq[string] = @[],
## parameters<#nimshortnoval-and-nimlongnoval>`_ for more information on
## how this affects parsing.
##
## `mode` selects the parser behavior profile (`NimMode`, `LaxMode`,
## or `GnuMode`). See `Parser Modes`_ for details.
##
## There is no need to check for `cmdEnd` while iterating. If using `getopt`
## with case switching, checking for `cmdEnd` is required.
##
@@ -513,7 +828,8 @@ iterator getopt*(cmdline: seq[string] = @[],
## writeHelp()
## ```
var p = initOptParser(cmdline, shortNoVal = shortNoVal,
longNoVal = longNoVal)
longNoVal = longNoVal,
rules = toRules(mode))
while true:
next(p)
if p.kind == cmdEnd: break

View File

@@ -243,7 +243,7 @@ proc rand[T: uint | uint64](r: var Rand; max: T): T =
else:
inc iters
proc rand*(r: var Rand; max: Natural): int {.benign.} =
proc rand*(r: var Rand; max: Natural): int {.gcsafe.} =
## Returns a random integer in the range `0..max` using the given state.
##
## **See also:**
@@ -260,7 +260,7 @@ proc rand*(r: var Rand; max: Natural): int {.benign.} =
cast[int](rand(r, uint64(max)))
# xxx toUnsigned pending https://github.com/nim-lang/Nim/pull/18445
proc rand*(max: int): int {.benign.} =
proc rand*(max: int): int {.gcsafe.} =
## Returns a random integer in the range `0..max`.
##
## If `randomize <#randomize>`_ has not been called, the sequence of random
@@ -281,7 +281,7 @@ proc rand*(max: int): int {.benign.} =
rand(state, max)
proc rand*(r: var Rand; max: range[0.0 .. high(float)]): float {.benign.} =
proc rand*(r: var Rand; max: range[0.0 .. high(float)]): float {.gcsafe.} =
## Returns a random floating point number in the range `0.0..max`
## using the given state.
##
@@ -308,7 +308,7 @@ proc rand*(r: var Rand; max: range[0.0 .. high(float)]): float {.benign.} =
let u = (0x3FFu64 shl 52u64) or (x shr 12u64)
result = (cast[float](u) - 1.0) * max
proc rand*(max: float): float {.benign.} =
proc rand*(max: float): float {.gcsafe.} =
## Returns a random floating point number in the range `0.0..max`.
##
## If `randomize <#randomize>`_ has not been called, the sequence of random
@@ -612,7 +612,7 @@ proc initRand*(seed: int64): Rand =
skipRandomNumbers(result)
discard next(result)
proc randomize*(seed: int64) {.benign.} =
proc randomize*(seed: int64) {.gcsafe.} =
## Initializes the default random number generator with the given seed.
##
## Providing a specific seed will produce the same results for that seed each time.
@@ -736,7 +736,7 @@ when not defined(standalone):
since (1, 5, 1):
export initRand
proc randomize*() {.benign.} =
proc randomize*() {.gcsafe.} =
## Initializes the default random number generator with a seed based on
## random number source.
##

View File

@@ -16,9 +16,9 @@
## stream interface.
##
## .. warning:: Due to the use of `pointer`, the `readData`, `peekData` and
## `writeData` interfaces are not available on the compile-time VM, and must
## be cast from a `ptr string` on the JS backend. However, `readDataStr` is
## available generally in place of `readData`.
## `writeData` interfaces are not available on the compile-time VM, and must
## be cast from a `ptr string` on the JS backend. However, `readDataStr` is
## available generally in place of `readData`.
##
## Basic usage
## ===========

View File

@@ -382,9 +382,9 @@ type
## timezones. The `times` module only supplies implementations for the
## system's local time and UTC.
zonedTimeFromTimeImpl: proc (x: Time): ZonedTime
{.tags: [], raises: [], benign.}
{.tags: [], raises: [], gcsafe.}
zonedTimeFromAdjTimeImpl: proc (x: Time): ZonedTime
{.tags: [], raises: [], benign.}
{.tags: [], raises: [], gcsafe.}
name: string
ZonedTime* = object ## Represents a point in time with an associated
@@ -432,7 +432,7 @@ else:
# Helper procs
#
{.pragma: operator, rtl, noSideEffect, benign.}
{.pragma: operator, rtl, noSideEffect, gcsafe.}
proc convert*[T: SomeInteger](unitFrom, unitTo: FixedTimeUnit, quantity: T): T
{.inline.} =
@@ -518,7 +518,7 @@ proc fromEpochDay(epochday: int64):
return (d.MonthdayRange, m.Month, (y + ord(m <= 2)).int)
proc getDayOfYear*(monthday: MonthdayRange, month: Month, year: int):
YeardayRange {.tags: [], raises: [], benign.} =
YeardayRange {.tags: [], raises: [], gcsafe.} =
## Returns the day of the year.
## Equivalent with `dateTime(year, month, monthday, 0, 0, 0, 0).yearday`.
runnableExamples:
@@ -538,7 +538,7 @@ proc getDayOfYear*(monthday: MonthdayRange, month: Month, year: int):
result = daysUntilMonth[month] + monthday - 1
proc getDayOfWeek*(monthday: MonthdayRange, month: Month, year: int): WeekDay
{.tags: [], raises: [], benign.} =
{.tags: [], raises: [], gcsafe.} =
## Returns the day of the week enum from day, month and year.
## Equivalent with `dateTime(year, month, monthday, 0, 0, 0, 0).weekday`.
runnableExamples:
@@ -922,21 +922,21 @@ proc nanosecond*(time: Time): NanosecondRange =
time.nanosecond
proc fromUnix*(unix: int64): Time
{.benign, tags: [], raises: [], noSideEffect.} =
{.gcsafe, tags: [], raises: [], noSideEffect.} =
## Convert a unix timestamp (seconds since `1970-01-01T00:00:00Z`)
## to a `Time`.
runnableExamples:
doAssert $fromUnix(0).utc == "1970-01-01T00:00:00Z"
initTime(unix, 0)
proc toUnix*(t: Time): int64 {.benign, tags: [], raises: [], noSideEffect.} =
proc toUnix*(t: Time): int64 {.gcsafe, tags: [], raises: [], noSideEffect.} =
## Convert `t` to a unix timestamp (seconds since `1970-01-01T00:00:00Z`).
## See also `toUnixFloat` for subsecond resolution.
runnableExamples:
doAssert fromUnix(0).toUnix() == 0
t.seconds
proc fromUnixFloat(seconds: float): Time {.benign, tags: [], raises: [], noSideEffect.} =
proc fromUnixFloat(seconds: float): Time {.gcsafe, tags: [], raises: [], noSideEffect.} =
## Convert a unix timestamp in seconds to a `Time`; same as `fromUnix`
## but with subsecond resolution.
runnableExamples:
@@ -946,7 +946,7 @@ proc fromUnixFloat(seconds: float): Time {.benign, tags: [], raises: [], noSideE
let nsecs = (seconds - secs) * 1e9
initTime(secs.int64, nsecs.NanosecondRange)
proc toUnixFloat(t: Time): float {.benign, tags: [], raises: [].} =
proc toUnixFloat(t: Time): float {.gcsafe, tags: [], raises: [].} =
## Same as `toUnix` but using subsecond resolution.
runnableExamples:
let t = getTime()
@@ -975,7 +975,7 @@ proc toWinTime*(t: Time): int64 =
proc getTimeImpl(typ: typedesc[Time]): Time =
raiseAssert "implemented in the vm"
proc getTime*(): Time {.tags: [TimeEffect], benign.} =
proc getTime*(): Time {.tags: [TimeEffect], gcsafe.} =
## Gets the current time as a `Time` with up to nanosecond resolution.
when nimvm:
result = getTimeImpl(Time)
@@ -1154,7 +1154,7 @@ proc isLeapDay*(dt: DateTime): bool {.since: (1, 1).} =
assertDateTimeInitialized dt
dt.year.isLeapYear and dt.month == mFeb and dt.monthday == 29
proc toTime*(dt: DateTime): Time {.tags: [], raises: [], benign.} =
proc toTime*(dt: DateTime): Time {.tags: [], raises: [], gcsafe.} =
## Converts a `DateTime` to a `Time` representing the same point in time.
assertDateTimeInitialized dt
let epochDay = toEpochDay(dt.monthday, dt.month, dt.year)
@@ -1197,9 +1197,9 @@ proc initDateTime(zt: ZonedTime, zone: Timezone): DateTime =
proc newTimezone*(
name: string,
zonedTimeFromTimeImpl: proc (time: Time): ZonedTime
{.tags: [], raises: [], benign.},
{.tags: [], raises: [], gcsafe.},
zonedTimeFromAdjTimeImpl: proc (adjTime: Time): ZonedTime
{.tags: [], raises: [], benign.}
{.tags: [], raises: [], gcsafe.}
): owned Timezone =
## Create a new `Timezone`.
##
@@ -1263,12 +1263,12 @@ proc `==`*(zone1, zone2: Timezone): bool =
zone1.name == zone2.name
proc inZone*(time: Time, zone: Timezone): DateTime
{.tags: [], raises: [], benign.} =
{.tags: [], raises: [], gcsafe.} =
## Convert `time` into a `DateTime` using `zone` as the timezone.
result = initDateTime(zone.zonedTimeFromTime(time), zone)
proc inZone*(dt: DateTime, zone: Timezone): DateTime
{.tags: [], raises: [], benign.} =
{.tags: [], raises: [], gcsafe.} =
## Returns a `DateTime` representing the same point in time as `dt` but
## using `zone` as the timezone.
assertDateTimeInitialized dt
@@ -1283,14 +1283,14 @@ proc toAdjTime(dt: DateTime): Time =
result = initTime(seconds, dt.nanosecond)
when defined(js):
proc localZonedTimeFromTime(time: Time): ZonedTime {.benign.} =
proc localZonedTimeFromTime(time: Time): ZonedTime {.gcsafe.} =
let jsDate = newDate(time.seconds * 1000)
let offset = jsDate.getTimezoneOffset() * secondsInMin
result.time = time
result.utcOffset = offset
result.isDst = false
proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime {.benign.} =
proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime {.gcsafe.} =
let utcDate = newDate(adjTime.seconds * 1000)
let localDate = newDate(utcDate.getUTCFullYear(), utcDate.getUTCMonth(),
utcDate.getUTCDate(), utcDate.getUTCHours(), utcDate.getUTCMinutes(),
@@ -1337,11 +1337,11 @@ else:
return ((a.int64 - tm.toAdjUnix).int, tm.tm_isdst > 0)
return (0, false)
proc localZonedTimeFromTime(time: Time): ZonedTime {.benign.} =
proc localZonedTimeFromTime(time: Time): ZonedTime {.gcsafe.} =
let (offset, dst) = getLocalOffsetAndDst(time.seconds)
result = ZonedTime(time: time, utcOffset: offset, isDst: dst)
proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime {.benign.} =
proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime {.gcsafe.} =
var adjUnix = adjTime.seconds
let past = adjUnix - secondsInDay
let (pastOffset, _) = getLocalOffsetAndDst(past)
@@ -1408,7 +1408,7 @@ proc local*(t: Time): DateTime =
## Shorthand for `t.inZone(local())`.
t.inZone(local())
proc now*(): DateTime {.tags: [TimeEffect], benign.} =
proc now*(): DateTime {.tags: [TimeEffect], gcsafe.} =
## Get the current time as a `DateTime` in the local timezone.
## Shorthand for `getTime().local`.
##
@@ -2327,7 +2327,7 @@ proc parseTime*(input: string, f: static[string], zone: Timezone): Time
const f2 = initTimeFormat(f)
result = input.parse(f2, zone).toTime()
proc `$`*(dt: DateTime): string {.tags: [], raises: [], benign.} =
proc `$`*(dt: DateTime): string {.tags: [], raises: [], gcsafe.} =
## Converts a `DateTime` object to a string representation.
## It uses the format `yyyy-MM-dd'T'HH:mm:sszzz`.
runnableExamples:
@@ -2339,7 +2339,7 @@ proc `$`*(dt: DateTime): string {.tags: [], raises: [], benign.} =
else:
result = format(dt, "yyyy-MM-dd'T'HH:mm:sszzz")
proc `$`*(time: Time): string {.tags: [], raises: [], benign.} =
proc `$`*(time: Time): string {.tags: [], raises: [], gcsafe.} =
## Converts a `Time` value to a string representation. It will use the local
## time zone and use the format `yyyy-MM-dd'T'HH:mm:sszzz`.
runnableExamples:

View File

@@ -96,6 +96,9 @@ proc supportsCopyMem*(t: typedesc): bool {.magic: "TypeTrait".}
##
## Other languages name a type like these `blob`:idx:.
proc canFormCycles*(t: typedesc): bool {.magic: "TypeTrait".}
## Returns true if `t` can form cycles.
proc hasDefaultValue*(t: typedesc): bool {.magic: "TypeTrait".} =
## Returns true if `t` has a valid default value.
runnableExamples:

View File

@@ -331,7 +331,7 @@ proc rawRemoveDir(dir: string) {.noWeirdTarget.} =
if rmdir(dir) != 0'i32 and errno != ENOENT: raiseOSError(osLastError(), dir)
proc removeDir*(dir: string, checkDir = false) {.rtl, extern: "nos$1", tags: [
WriteDirEffect, ReadDirEffect], benign, noWeirdTarget.} =
WriteDirEffect, ReadDirEffect], gcsafe, noWeirdTarget.} =
## Removes the directory `dir` including all subdirectories and files
## in `dir` (recursively).
##
@@ -441,7 +441,7 @@ proc createDir*(dir: string) {.rtl, extern: "nos$1",
discard existsOrCreateDir(p)
proc copyDir*(source, dest: string, skipSpecial = false) {.rtl, extern: "nos$1",
tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect], benign, noWeirdTarget.} =
tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect], gcsafe, noWeirdTarget.} =
## Copies a directory from `source` to `dest`.
##
## On non-Windows OSes, symlinks are copied as symlinks. On Windows, symlinks
@@ -482,7 +482,7 @@ proc copyDirWithPermissions*(source, dest: string,
ignorePermissionErrors = true,
skipSpecial = false)
{.rtl, extern: "nos$1", tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect],
benign, noWeirdTarget.} =
gcsafe, noWeirdTarget.} =
## Copies a directory from `source` to `dest` preserving file permissions.
##
## On non-Windows OSes, symlinks are copied as symlinks. On Windows, symlinks

View File

@@ -182,7 +182,7 @@ proc checkErr(f: File) =
{.push stackTrace: off, profiler: off.}
proc readBuffer*(f: File, buffer: pointer, len: Natural): int {.
tags: [ReadIOEffect], benign.} =
tags: [ReadIOEffect], gcsafe.} =
## Reads `len` bytes into the buffer pointed to by `buffer`. Returns
## the actual number of bytes that have been read which may be less than
## `len` (if not as many bytes are remaining), but not greater.
@@ -191,20 +191,20 @@ proc readBuffer*(f: File, buffer: pointer, len: Natural): int {.
proc readBytes*(f: File, a: var openArray[int8|uint8], start,
len: Natural): int {.
tags: [ReadIOEffect], benign.} =
tags: [ReadIOEffect], gcsafe.} =
## Reads `len` bytes into the buffer `a` starting at `a[start]`. Returns
## the actual number of bytes that have been read which may be less than
## `len` (if not as many bytes are remaining), but not greater.
result = readBuffer(f, addr(a[start]), len)
proc readChars*(f: File, a: var openArray[char]): int {.tags: [ReadIOEffect], benign.} =
proc readChars*(f: File, a: var openArray[char]): int {.tags: [ReadIOEffect], gcsafe.} =
## Reads up to `a.len` bytes into the buffer `a`. Returns
## the actual number of bytes that have been read which may be less than
## `a.len` (if not as many bytes are remaining), but not greater.
result = readBuffer(f, addr(a[0]), a.len)
proc readChars*(f: File, a: var openArray[char], start, len: Natural): int {.
tags: [ReadIOEffect], benign, deprecated:
tags: [ReadIOEffect], gcsafe, deprecated:
"use other `readChars` overload, possibly via: readChars(toOpenArray(buf, start, len-1))".} =
## Reads `len` bytes into the buffer `a` starting at `a[start]`. Returns
## the actual number of bytes that have been read which may be less than
@@ -213,13 +213,13 @@ proc readChars*(f: File, a: var openArray[char], start, len: Natural): int {.
raiseEIO("buffer overflow: (start+len) > length of openarray buffer")
result = readBuffer(f, addr(a[start]), len)
proc write*(f: File, c: cstring) {.tags: [WriteIOEffect], benign.} =
proc write*(f: File, c: cstring) {.tags: [WriteIOEffect], gcsafe.} =
## Writes a value to the file `f`. May throw an IO exception.
discard c_fputs(c, f)
checkErr(f)
proc writeBuffer*(f: File, buffer: pointer, len: Natural): int {.
tags: [WriteIOEffect], benign.} =
tags: [WriteIOEffect], gcsafe.} =
## Writes the bytes of buffer pointed to by the parameter `buffer` to the
## file `f`. Returns the number of actual written bytes, which may be less
## than `len` in case of an error.
@@ -227,7 +227,7 @@ proc writeBuffer*(f: File, buffer: pointer, len: Natural): int {.
checkErr(f)
proc writeBytes*(f: File, a: openArray[int8|uint8], start, len: Natural): int {.
tags: [WriteIOEffect], benign.} =
tags: [WriteIOEffect], gcsafe.} =
## Writes the bytes of `a[start..start+len-1]` to the file `f`. Returns
## the number of actual written bytes, which may be less than `len` in case
## of an error.
@@ -235,7 +235,7 @@ proc writeBytes*(f: File, a: openArray[int8|uint8], start, len: Natural): int {.
result = writeBuffer(f, addr(x[int(start)]), len)
proc writeChars*(f: File, a: openArray[char], start, len: Natural): int {.
tags: [WriteIOEffect], benign.} =
tags: [WriteIOEffect], gcsafe.} =
## Writes the bytes of `a[start..start+len-1]` to the file `f`. Returns
## the number of actual written bytes, which may be less than `len` in case
## of an error.
@@ -264,7 +264,7 @@ when defined(windows):
break
inc i, w
proc write*(f: File, s: string) {.tags: [WriteIOEffect], benign.} =
proc write*(f: File, s: string) {.tags: [WriteIOEffect], gcsafe.} =
when defined(windows):
writeWindows(f, s, doRaise = true)
else:
@@ -393,7 +393,7 @@ when defined(nimdoc) or (defined(posix) and not defined(nimscript)) or defined(w
inheritable.WinDWORD) != 0
proc readLine*(f: File, line: var string): bool {.tags: [ReadIOEffect],
benign.} =
gcsafe.} =
## Reads a line of text from the file `f` into `line`. May throw an IO
## exception.
## A line of text may be delimited by `LF` or `CRLF`. The newline
@@ -519,43 +519,43 @@ proc readLine*(f: File, line: var string): bool {.tags: [ReadIOEffect],
sp = 128 # read in 128 bytes at a time
line.setLen(pos+sp)
proc readLine*(f: File): string {.tags: [ReadIOEffect], benign.} =
proc readLine*(f: File): string {.tags: [ReadIOEffect], gcsafe.} =
## Reads a line of text from the file `f`. May throw an IO exception.
## A line of text may be delimited by `LF` or `CRLF`. The newline
## character(s) are not part of the returned string.
result = newStringOfCap(80)
if not readLine(f, result): raiseEOF()
proc write*(f: File, i: int) {.tags: [WriteIOEffect], benign.} =
proc write*(f: File, i: int) {.tags: [WriteIOEffect], gcsafe.} =
when sizeof(int) == 8:
if c_fprintf(f, "%lld", i) < 0: checkErr(f)
else:
if c_fprintf(f, "%ld", i) < 0: checkErr(f)
proc write*(f: File, i: BiggestInt) {.tags: [WriteIOEffect], benign.} =
proc write*(f: File, i: BiggestInt) {.tags: [WriteIOEffect], gcsafe.} =
when sizeof(BiggestInt) == 8:
if c_fprintf(f, "%lld", i) < 0: checkErr(f)
else:
if c_fprintf(f, "%ld", i) < 0: checkErr(f)
proc write*(f: File, b: bool) {.tags: [WriteIOEffect], benign.} =
proc write*(f: File, b: bool) {.tags: [WriteIOEffect], gcsafe.} =
if b: write(f, "true")
else: write(f, "false")
proc write*(f: File, r: float32) {.tags: [WriteIOEffect], benign.} =
proc write*(f: File, r: float32) {.tags: [WriteIOEffect], gcsafe.} =
var buffer {.noinit.}: array[65, char]
discard writeFloatToBuffer(buffer, r)
if c_fprintf(f, "%s", buffer[0].addr) < 0: checkErr(f)
proc write*(f: File, r: BiggestFloat) {.tags: [WriteIOEffect], benign.} =
proc write*(f: File, r: BiggestFloat) {.tags: [WriteIOEffect], gcsafe.} =
var buffer {.noinit.}: array[65, char]
discard writeFloatToBuffer(buffer, r)
if c_fprintf(f, "%s", buffer[0].addr) < 0: checkErr(f)
proc write*(f: File, c: char) {.tags: [WriteIOEffect], benign.} =
proc write*(f: File, c: char) {.tags: [WriteIOEffect], gcsafe.} =
discard c_putc(cint(c), f)
proc write*(f: File, a: varargs[string, `$`]) {.tags: [WriteIOEffect], benign.} =
proc write*(f: File, a: varargs[string, `$`]) {.tags: [WriteIOEffect], gcsafe.} =
for x in items(a): write(f, x)
proc readAllBuffer(file: File): string =
@@ -579,7 +579,7 @@ proc rawFileSize(file: File): int64 =
result = c_ftell(file)
discard c_fseek(file, oldPos, 0)
proc endOfFile*(f: File): bool {.tags: [], benign.} =
proc endOfFile*(f: File): bool {.tags: [], gcsafe.} =
## Returns true if `f` is at the end.
var c = c_fgetc(f)
discard c_ungetc(c, f)
@@ -603,7 +603,7 @@ proc readAllFile(file: File): string =
var len = rawFileSize(file)
result = readAllFile(file, len)
proc readAll*(file: File): string {.tags: [ReadIOEffect], benign.} =
proc readAll*(file: File): string {.tags: [ReadIOEffect], gcsafe.} =
## Reads all data from the stream `file`.
##
## Raises an IO exception in case of an error. It is an error if the
@@ -621,7 +621,7 @@ proc readAll*(file: File): string {.tags: [ReadIOEffect], benign.} =
result = readAllBuffer(file)
proc writeLine*[Ty](f: File, x: varargs[Ty, `$`]) {.inline,
tags: [WriteIOEffect], benign.} =
tags: [WriteIOEffect], gcsafe.} =
## Writes the values `x` to `f` and then writes "\\n".
## May throw an IO exception.
for i in items(x):
@@ -713,7 +713,7 @@ when defined(posix) and not defined(nimscript):
proc open*(f: var File, filename: string,
mode: FileMode = fmRead,
bufSize: int = -1): bool {.tags: [], raises: [], benign.} =
bufSize: int = -1): bool {.tags: [], raises: [], gcsafe.} =
## Opens a file named `filename` with given `mode`.
##
## Default mode is readonly. Returns true if the file could be opened.
@@ -747,7 +747,7 @@ proc open*(f: var File, filename: string,
result = false
proc reopen*(f: File, filename: string, mode: FileMode = fmRead): bool {.
tags: [], benign.} =
tags: [], gcsafe.} =
## Reopens the file `f` with given `filename` and `mode`. This
## is often used to redirect the `stdin`, `stdout` or `stderr`
## file variables.
@@ -766,7 +766,7 @@ proc reopen*(f: File, filename: string, mode: FileMode = fmRead): bool {.
result = false
proc open*(f: var File, filehandle: FileHandle,
mode: FileMode = fmRead): bool {.tags: [], raises: [], benign.} =
mode: FileMode = fmRead): bool {.tags: [], raises: [], gcsafe.} =
## Creates a `File` from a `filehandle` with given `mode`.
##
## Default mode is readonly. Returns true if the file could be opened.
@@ -792,26 +792,26 @@ proc open*(filename: string,
if not open(result, filename, mode, bufSize):
raise newException(IOError, "cannot open: " & filename)
proc setFilePos*(f: File, pos: int64, relativeTo: FileSeekPos = fspSet) {.benign, sideEffect.} =
proc setFilePos*(f: File, pos: int64, relativeTo: FileSeekPos = fspSet) {.gcsafe, sideEffect.} =
## Sets the position of the file pointer that is used for read/write
## operations. The file's first byte has the index zero.
if c_fseek(f, pos, cint(relativeTo)) != 0:
raiseEIO("cannot set file position")
proc getFilePos*(f: File): int64 {.benign.} =
proc getFilePos*(f: File): int64 {.gcsafe.} =
## Retrieves the current position of the file pointer that is used to
## read from the file `f`. The file's first byte has the index zero.
result = c_ftell(f)
if result < 0: raiseEIO("cannot retrieve file position")
proc getFileSize*(f: File): int64 {.tags: [ReadIOEffect], benign.} =
proc getFileSize*(f: File): int64 {.tags: [ReadIOEffect], gcsafe.} =
## Retrieves the file size (in bytes) of `f`.
let oldPos = getFilePos(f)
discard c_fseek(f, 0, 2) # seek the end of the file
result = getFilePos(f)
setFilePos(f, oldPos)
proc setStdIoUnbuffered*() {.tags: [], benign.} =
proc setStdIoUnbuffered*() {.tags: [], gcsafe.} =
## Configures `stdin`, `stdout` and `stderr` to be unbuffered.
when declared(stdout):
discard c_setvbuf(stdout, nil, IONBF, 0)
@@ -865,7 +865,7 @@ when defined(windows) and appType == "console" and
discard setConsoleCP(Utf8codepage)
addExitProc(restoreConsoleCP)
proc readFile*(filename: string): string {.tags: [ReadIOEffect], benign.} =
proc readFile*(filename: string): string {.tags: [ReadIOEffect], gcsafe.} =
## Opens a file named `filename` for reading, calls `readAll
## <#readAll,File>`_ and closes the file afterwards. Returns the string.
## Raises an IO exception in case of an error. If you need to call
@@ -880,7 +880,7 @@ proc readFile*(filename: string): string {.tags: [ReadIOEffect], benign.} =
else:
raise newException(IOError, "cannot open: " & filename)
proc writeFile*(filename, content: string) {.tags: [WriteIOEffect], benign.} =
proc writeFile*(filename, content: string) {.tags: [WriteIOEffect], gcsafe.} =
## Opens a file named `filename` for writing. Then writes the
## `content` completely to the file and closes the file afterwards.
## Raises an IO exception in case of an error.

View File

@@ -219,16 +219,16 @@ elif someVcc:
elif mem == ATOMIC_ACQ_REL: fence()
elif mem == ATOMIC_SEQ_CST: fence()
proc atomicStoreN*[T: AtomType](p: ptr T, val: T, mem: static[AtomMemModel]) =
proc atomicStoreN*[T: AtomType](p: ptr T, val: T, mem: static[AtomMemModel]) {.enforcenoraises.} =
barrier(mem)
p[] = val
proc atomicLoadN*[T: AtomType](p: ptr T, mem: static[AtomMemModel]): T =
proc atomicLoadN*[T: AtomType](p: ptr T, mem: static[AtomMemModel]): T {.enforcenoraises.} =
result = p[]
barrier(mem)
proc atomicCompareExchangeN*[T: ptr](p, expected: ptr T, desired: T,
weak: bool, success_memmodel: AtomMemModel, failure_memmodel: AtomMemModel): bool =
weak: bool, success_memmodel: AtomMemModel, failure_memmodel: AtomMemModel): bool {.enforcenoraises.} =
when sizeof(T) == 8:
interlockedCompareExchange64(p, cast[int64](desired), cast[int64](expected[])) ==
cast[int64](expected[])
@@ -236,7 +236,7 @@ elif someVcc:
interlockedCompareExchange32(p, cast[int32](desired), cast[int32](expected[])) ==
cast[int32](expected[])
proc atomicExchangeN*[T: ptr](p: ptr T, val: T, mem: AtomMemModel): T =
proc atomicExchangeN*[T: ptr](p: ptr T, val: T, mem: AtomMemModel): T {.enforcenoraises.} =
when sizeof(T) == 8:
cast[T](interlockedExchange64(p, cast[int64](val)))
elif sizeof(T) == 4:

View File

@@ -68,7 +68,7 @@ type
proc `=copy`*(x: var Task, y: Task) {.error.}
const arcLike = defined(gcArc) or defined(gcAtomicArc) or defined(gcOrc)
const arcLike = defined(gcArc) or defined(gcAtomicArc) or defined(gcOrc) or defined(gcYrc)
when defined(nimAllowNonVarDestructor) and arcLike:
proc `=destroy`*(t: Task) {.inline, gcsafe.} =
## Frees the resources allocated for a `Task`.

View File

@@ -9,13 +9,13 @@
##[
Thread support for Nim. Threads allow multiple functions to execute concurrently.
In Nim, threads are a low-level construct and using a library like `malebolgia`, `taskpools` or `weave` is recommended.
When creating a thread, you can pass arguments to it. As Nim's garbage collector does not use atomic references, sharing
`ref` and other variables managed by the garbage collector between threads is not supported.
Use global variables to do so, or pointers.
Memory allocated using [`sharedAlloc`](./system.html#allocShared.t%2CNatural) can be used and shared between threads.
To communicate between threads, consider using [channels](./system.html#Channel)
@@ -44,7 +44,7 @@ joinThreads(thr)
deinitLock(L)
```
When using a memory management strategy that supports shared heaps like `arc` or `boehm`,
you can pass pointer to threads and share memory between them, but the memory must outlive the thread.
The default memory management strategy, `orc`, supports this.
@@ -52,14 +52,14 @@ The example below is **not valid** for memory management strategies that use loc
```Nim
import locks
var l: Lock
proc threadFunc(obj: ptr seq[int]) {.thread.} =
withLock l:
for i in 0..<100:
obj[].add(obj[].len * obj[].len)
proc threadHandler() =
var thr: array[0..4, Thread[ptr seq[int]]]
var s = newSeq[int]()
@@ -68,7 +68,7 @@ proc threadHandler() =
createThread(thr[i], threadFunc, s.addr)
joinThreads(thr)
echo s
initLock(l)
threadHandler()
deinitLock(l)
@@ -303,5 +303,5 @@ else:
proc createThread*(t: var Thread[void], tp: proc () {.thread, nimcall.}) =
createThread[void](t, tp)
when not defined(gcOrc):
when not defined(gcOrc) and not defined(gcYrc):
include system/threadids

View File

@@ -25,7 +25,7 @@ when not (defined(cpu16) or defined(cpu8)):
bytes: int
data: WideCString
const arcLike = defined(gcArc) or defined(gcAtomicArc) or defined(gcOrc)
const arcLike = defined(gcArc) or defined(gcAtomicArc) or defined(gcOrc) or defined(gcYrc)
when defined(nimAllowNonVarDestructor) and arcLike:
proc `=destroy`(a: WideCStringObj) =
if a.data != nil:

View File

@@ -125,7 +125,7 @@ proc unsafeAddr*[T](x: T): ptr T {.magic: "Addr", noSideEffect.} =
const ThisIsSystem = true
const arcLikeMem = defined(gcArc) or defined(gcAtomicArc) or defined(gcOrc)
const arcLikeMem = defined(gcArc) or defined(gcAtomicArc) or defined(gcOrc) or defined(gcYrc)
when defined(nimAllowNonVarDestructor) and arcLikeMem:
proc new*[T](a: var ref T, finalizer: proc (x: T) {.nimcall.}) {.
@@ -356,7 +356,7 @@ proc low*(x: string): int {.magic: "Low", noSideEffect.}
## See also:
## * `high(string) <#high,string>`_
when not defined(gcArc) and not defined(gcOrc) and not defined(gcAtomicArc):
when not defined(gcArc) and not defined(gcOrc) and not defined(gcYrc) and not defined(gcAtomicArc):
proc shallowCopy*[T](x: var T, y: T) {.noSideEffect, magic: "ShallowCopy".}
## Use this instead of `=` for a `shallow copy`:idx:.
##
@@ -407,7 +407,7 @@ when defined(nimHasDup):
proc `=sink`*[T](x: var T; y: T) {.inline, nodestroy, magic: "Asgn".} =
## Generic `sink`:idx: implementation that can be overridden.
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcYrc) or defined(gcAtomicArc):
x = y
else:
shallowCopy(x, y)
@@ -627,7 +627,7 @@ proc newSeq*[T](s: var seq[T], len: Natural) {.magic: "NewSeq", noSideEffect.}
## #inputStrings[3] = "out of bounds"
## ```
proc newSeq*[T](len = 0.Natural): seq[T] =
proc newSeq*[T](len = 0.Natural): seq[T] {.noSideEffect.} =
## Creates a new sequence of type `seq[T]` with length `len`.
##
## Note that the sequence will be filled with zeroed entries.
@@ -1147,7 +1147,7 @@ template sysAssert(cond: bool, msg: string) =
const hasAlloc = (hostOS != "standalone" or not defined(nogc)) and not defined(nimscript)
when notJSnotNims and hasAlloc and not defined(nimSeqsV2):
proc addChar(s: NimString, c: char): NimString {.compilerproc, benign.}
proc addChar(s: NimString, c: char): NimString {.compilerproc, gcsafe.}
when defined(nimscript) or not defined(nimSeqsV2):
proc add*[T](x: var seq[T], y: sink T) {.magic: "AppendSeqElem", noSideEffect.}
@@ -1459,6 +1459,7 @@ proc isNil*[T: proc | iterator {.closure.}](x: T): bool {.noSideEffect, magic: "
## `== nil`.
proc supportsCopyMem(t: typedesc): bool {.magic: "TypeTrait".}
proc canFormCycles(t: typedesc): bool {.magic: "TypeTrait".}
when defined(nimHasTopDownInference):
# magic used for seq type inference
@@ -1664,7 +1665,7 @@ when not defined(js) and hasThreadSupport and hostOS != "standalone":
when not defined(js) and defined(nimV2):
type
DestructorProc = proc (p: pointer) {.nimcall, benign, raises: [].}
DestructorProc = proc (p: pointer) {.nimcall, gcsafe, raises: [].}
TNimTypeV2 {.compilerproc.} = object
destructor: pointer
size: int
@@ -1776,7 +1777,7 @@ when not defined(nimscript):
when not declared(sysFatal):
include "system/fatal"
proc echo*(x: varargs[typed, `$`]) {.magic: "Echo", benign, sideEffect.}
proc echo*(x: varargs[typed, `$`]) {.magic: "Echo", gcsafe, sideEffect.}
## Writes and flushes the parameters to the standard output.
##
## Special built-in that takes a variable number of arguments. Each argument
@@ -1883,7 +1884,7 @@ when notJSnotNims:
## lead to the `raise` statement. This only works for debug builds.
var
globalRaiseHook*: proc (e: ref Exception): bool {.nimcall, benign.}
globalRaiseHook*: proc (e: ref Exception): bool {.nimcall, gcsafe.}
## With this hook you can influence exception handling on a global level.
## If not nil, every 'raise' statement ends up calling this hook.
##
@@ -1892,7 +1893,7 @@ when notJSnotNims:
## If `globalRaiseHook` returns false, the exception is caught and does
## not propagate further through the call stack.
localRaiseHook* {.threadvar.}: proc (e: ref Exception): bool {.nimcall, benign.}
localRaiseHook* {.threadvar.}: proc (e: ref Exception): bool {.nimcall, gcsafe.}
## With this hook you can influence exception handling on a
## thread local level.
## If not nil, every 'raise' statement ends up calling this hook.
@@ -1902,7 +1903,7 @@ when notJSnotNims:
## If `localRaiseHook` returns false, the exception
## is caught and does not propagate further through the call stack.
outOfMemHook*: proc () {.nimcall, tags: [], benign, raises: [].}
outOfMemHook*: proc () {.nimcall, tags: [], gcsafe, raises: [].}
## Set this variable to provide a procedure that should be called
## in case of an `out of memory`:idx: event. The standard handler
## writes an error message and terminates the program.
@@ -1923,7 +1924,7 @@ when notJSnotNims:
## If the handler does not raise an exception, ordinary control flow
## continues and the program is terminated.
unhandledExceptionHook*: proc (e: ref Exception) {.nimcall, tags: [], benign, raises: [].}
unhandledExceptionHook*: proc (e: ref Exception) {.nimcall, tags: [], gcsafe, raises: [].}
## Set this variable to provide a procedure that should be called
## in case of an `unhandle exception` event. The standard handler
## writes an error message and terminates the program, except when
@@ -2066,7 +2067,7 @@ when hostOS == "standalone" and defined(nogc):
if s == nil or s.len == 0: result = cstring""
else: result = cast[cstring](addr s.data)
proc getTypeInfo*[T](x: T): pointer {.magic: "GetTypeInfo", benign.}
proc getTypeInfo*[T](x: T): pointer {.magic: "GetTypeInfo", gcsafe.}
## Get type information for `x`.
##
## Ordinary code should not use this, but the `typeinfo module
@@ -2285,21 +2286,21 @@ when not defined(js) and declared(alloc0) and declared(dealloc):
dealloc(a)
when notJSnotNims and hostOS != "standalone":
proc getCurrentException*(): ref Exception {.compilerRtl, inl, benign.} =
proc getCurrentException*(): ref Exception {.compilerRtl, inl, gcsafe.} =
## Retrieves the current exception; if there is none, `nil` is returned.
result = currException
proc nimBorrowCurrentException(): ref Exception {.compilerRtl, inl, benign, nodestroy.} =
proc nimBorrowCurrentException(): ref Exception {.compilerRtl, inl, gcsafe, nodestroy.} =
# .nodestroy here so that we do not produce a write barrier as the
# C codegen only uses it in a borrowed way:
result = currException
proc getCurrentExceptionMsg*(): string {.inline, benign.} =
proc getCurrentExceptionMsg*(): string {.inline, gcsafe.} =
## Retrieves the error message that was attached to the current
## exception; if there is none, `""` is returned.
return if currException == nil: "" else: currException.msg
proc setCurrentException*(exc: ref Exception) {.inline, benign.} =
proc setCurrentException*(exc: ref Exception) {.inline, gcsafe.} =
## Sets the current exception.
##
## .. warning:: Only use this if you know what you are doing.
@@ -2561,7 +2562,7 @@ when compileOption("rangechecks"):
else:
template rangeCheck*(cond) = discard
when not defined(gcArc) and not defined(gcOrc) and not defined(gcAtomicArc):
when not defined(gcArc) and not defined(gcOrc) and not defined(gcYrc) and not defined(gcAtomicArc):
proc shallow*[T](s: var seq[T]) {.noSideEffect, inline.} =
## Marks a sequence `s` as `shallow`:idx:. Subsequent assignments will not
## perform deep copies of `s`.
@@ -2630,7 +2631,7 @@ when hasAlloc or defined(nimscript):
setLen(x, xl+item.len)
var j = xl-1
while j >= i:
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
when defined(gcArc) or defined(gcOrc) or defined(gcYrc) or defined(gcAtomicArc):
x[j+item.len] = move x[j]
else:
shallowCopy(x[j+item.len], x[j])

View File

@@ -104,6 +104,8 @@ type
zeroField: int # 0 means cell is not used (overlaid with typ field)
# 1 means cell is manually managed pointer
# otherwise a PNimType is stored in there
when sizeof(int) == 4: # 32-bit only
headerAlignPad: array[8, byte] # so addr(data) ≡ 8 (mod 16)
else:
alignment: int
@@ -477,7 +479,8 @@ iterator allObjects(m: var MemRegion): pointer {.inline.} =
a = a +% size
else:
let c = cast[PBigChunk](c)
yield addr(c.data)
# prev stores the aligned data pointer set during rawAlloc
yield cast[pointer](c.prev)
m.locked = false
proc iterToProc*(iter: typed, envType: typedesc; procName: untyped) {.
@@ -724,7 +727,7 @@ proc getSmallChunk(a: var MemRegion): PSmallChunk =
# -----------------------------------------------------------------------------
when not defined(gcDestructors):
proc isAllocatedPtr(a: MemRegion, p: pointer): bool {.benign.}
proc isAllocatedPtr(a: MemRegion, p: pointer): bool {.gcsafe.}
when true:
template allocInv(a: MemRegion): bool = true
@@ -777,7 +780,10 @@ proc deallocBigChunk(a: var MemRegion, c: PBigChunk) =
sysAssert a.occ >= 0, "rawDealloc: negative occupied memory (case B)"
when not defined(gcDestructors):
a.deleted = getBottom(a)
del(a, a.root, cast[int](addr(c.data)))
# prev stores the aligned data pointer that was added to the AVL tree during allocation
del(a, a.root, cast[int](c.prev))
# Reset prev before freeing (required by listAdd assertions in freeBigChunk)
c.prev = nil
if c.size >= HugeChunkSize: freeHugeChunk(a, c)
else: freeBigChunk(a, c)
@@ -845,7 +851,14 @@ when defined(heaptrack):
proc heaptrack_malloc(a: pointer, size: int) {.cdecl, importc, dynlib: heaptrackLib.}
proc heaptrack_free(a: pointer) {.cdecl, importc, dynlib: heaptrackLib.}
proc rawAlloc(a: var MemRegion, requestedSize: int): pointer =
proc bigChunkAlignOffset(alignment: int): int {.inline.} =
## Compute the alignment offset for big chunk data.
if alignment == 0:
result = 0
else:
result = align(sizeof(BigChunk) + sizeof(FreeCell), alignment) - sizeof(BigChunk) - sizeof(FreeCell)
proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer =
when defined(nimTypeNames):
inc(a.allocCounter)
sysAssert(allocInv(a), "rawAlloc: begin")
@@ -855,7 +868,9 @@ proc rawAlloc(a: var MemRegion, requestedSize: int): pointer =
sysAssert(size >= requestedSize, "insufficient allocated size!")
#c_fprintf(stdout, "alloc; size: %ld; %ld\n", requestedSize, size)
if size <= SmallChunkSize-smallChunkOverhead():
# For custom alignments > MemAlign, force big chunk allocation
# Small chunks cannot handle arbitrary alignments due to fixed cell boundaries
if size <= SmallChunkSize-smallChunkOverhead() and alignment == 0:
template fetchSharedCells(tc: PSmallChunk) =
# Consumes cells from (potentially) foreign threads from `a.sharedFreeLists[s]`
when defined(gcDestructors):
@@ -950,13 +965,21 @@ proc rawAlloc(a: var MemRegion, requestedSize: int): pointer =
if deferredFrees != nil:
freeDeferredObjects(a, deferredFrees)
size = requestedSize + bigChunkOverhead() # roundup(requestedSize+bigChunkOverhead(), PageSize)
# For big chunks with custom alignment, allocate extra space.
# Since chunks are page-aligned, the needed padding is a compile-time
# deterministic value rather than a worst-case estimate.
let alignPad = bigChunkAlignOffset(alignment)
size = requestedSize + bigChunkOverhead() + alignPad
# allocate a large block
var c = if size >= HugeChunkSize: getHugeChunk(a, size)
else: getBigChunk(a, size)
sysAssert c.prev == nil, "rawAlloc 10"
sysAssert c.next == nil, "rawAlloc 11"
result = addr(c.data)
result = addr(c.data) +! alignPad
# Store the aligned data pointer in prev for deallocation and GC traversal.
# prev is unused while the chunk is allocated (next/prev are free-list links).
c.prev = cast[PBigChunk](result)
sysAssert((cast[int](c) and (MemAlign-1)) == 0, "rawAlloc 13")
sysAssert((cast[int](c) and PageMask) == 0, "rawAlloc: Not aligned on a page boundary")
when not defined(gcDestructors):
@@ -1025,13 +1048,29 @@ proc rawDealloc(a: var MemRegion, p: pointer) =
inc(c.free, s)
else:
inc(c.free, s)
# Free only if the entire chunk is unused and there are no borrowed cells.
# If the chunk were to be freed while it references foreign cells,
# the foreign chunks will leak memory and can never be freed.
if c.free == SmallChunkSize-smallChunkOverhead() and c.foreignCells == 0:
listRemove(a.freeSmallChunks[s div MemAlign], c)
c.size = SmallChunkSize
freeBigChunk(a, cast[PBigChunk](c))
# FIX: Don't free small chunks to avoid race condition with sharedFreeLists.
#
# RACE CONDITION: Between checking foreignCells==0 and calling freeBigChunk,
# another thread may read chunk.owner and decide to add a cell to our
# sharedFreeLists. If we free the chunk, that cell becomes orphaned.
#
# SOLUTION: Never free small chunks. They remain in freeSmallChunks[s] and
# are reused on next allocation. This maintains the invariant that chunks
# in freeSmallChunks[s] have c.free >= s (completely free chunks satisfy this).
# If a chunk becomes exhausted (c.free < s), it's removed by line 949.
#
# TRADEOFF: Memory not returned to OS. Bounded by peak concurrent allocation
# per size class (~4KB per active size class per thread, typically <1MB total).
#
# VERIFIED: TLA+ formal proof shows no race - see VERIFICATION_RESULTS.md
#
# Original code (REMOVED to fix race):
sysAssert(c.free >= s, "Invariant violated: chunk in freeSmallChunks has insufficient space")
when false:
if c.free == SmallChunkSize-smallChunkOverhead() and c.foreignCells == 0:
listRemove(a.freeSmallChunks[s div MemAlign], c)
c.size = SmallChunkSize
freeBigChunk(a, cast[PBigChunk](c))
else:
when logAlloc: cprintf("dealloc(pointer_%p) # SMALL FROM %p CALLER %p\n", p, c.owner, addr(a))
@@ -1067,7 +1106,9 @@ when not defined(gcDestructors):
(cast[ptr FreeCell](p).zeroField >% 1)
else:
var c = cast[PBigChunk](c)
result = p == addr(c.data) and cast[ptr FreeCell](p).zeroField >% 1
# prev stores the aligned data pointer set during rawAlloc
let cellPtr = cast[pointer](c.prev)
result = p == cellPtr and cast[ptr FreeCell](p).zeroField >% 1
proc prepareForInteriorPointerChecking(a: var MemRegion) {.inline.} =
a.minLargeObj = lowGauge(a.root)
@@ -1091,7 +1132,8 @@ when not defined(gcDestructors):
sysAssert isAllocatedPtr(a, result), " result wrong pointer!"
else:
var c = cast[PBigChunk](c)
var d = addr(c.data)
# prev stores the aligned data pointer set during rawAlloc
var d = cast[pointer](c.prev)
if p >= d and cast[ptr FreeCell](d).zeroField >% 1:
result = d
sysAssert isAllocatedPtr(a, result), " result wrong pointer!"
@@ -1104,7 +1146,8 @@ when not defined(gcDestructors):
if avlNode != nil:
var k = cast[pointer](avlNode.key)
var c = cast[PBigChunk](pageAddr(k))
sysAssert(addr(c.data) == k, " k is not the same as addr(c.data)!")
# prev stores the aligned data pointer (the AVL tree key)
sysAssert(cast[pointer](c.prev) == k, " k is not the aligned address!")
if cast[ptr FreeCell](k).zeroField >% 1:
result = k
sysAssert isAllocatedPtr(a, result), " result wrong pointer!"
@@ -1323,4 +1366,4 @@ template instantiateForRegion(allocator: untyped) {.dirty.} =
#sharedMemStatsShared(sharedHeap.currMem - sharedHeap.freeMem)
{.pop.}
{.pop.}
{.pop.}

View File

@@ -16,7 +16,7 @@ runtime type and only contains a reference count.
{.push raises: [], rangeChecks: off.}
when defined(gcOrc):
when defined(gcOrc) or defined(gcYrc):
const
rcIncrement = 0b10000 # so that lowest 4 bits are not touched
rcMask = 0b1111
@@ -36,12 +36,12 @@ type
rc: int # the object header is now a single RC field.
# we could remove it in non-debug builds for the 'owned ref'
# design but this seems unwise.
when defined(gcOrc):
when defined(gcOrc) or defined(gcYrc):
rootIdx: int # thanks to this we can delete potential cycle roots
# in O(1) without doubly linked lists
when defined(nimArcDebug) or defined(nimArcIds):
refId: int
when defined(gcOrc) and orcLeakDetector:
when (defined(gcOrc) or defined(gcYrc)) and orcLeakDetector:
filename: cstring
line: int
@@ -74,7 +74,7 @@ elif defined(nimArcIds):
const traceId = -1
when defined(gcAtomicArc) and hasThreadSupport:
when (defined(gcAtomicArc) or defined(gcYrc)) and hasThreadSupport:
template decrement(cell: Cell): untyped =
discard atomicDec(cell.rc, rcIncrement)
template increment(cell: Cell): untyped =
@@ -119,7 +119,7 @@ proc nimNewObjUninit(size, alignment: int): pointer {.compilerRtl.} =
else:
result = cast[ptr RefHeader](alignedAlloc(s, alignment) +! hdrSize)
head(result).rc = 0
when defined(gcOrc):
when defined(gcOrc) or defined(gcYrc):
head(result).rootIdx = 0
when defined(nimArcDebug):
head(result).refId = gRefId
@@ -157,7 +157,7 @@ proc nimIncRef(p: pointer) {.compilerRtl, inl.} =
when traceCollector:
cprintf("[INCREF] %p\n", head(p))
when not defined(gcOrc) or defined(nimThinout):
when not (defined(gcOrc) or defined(gcYrc)) or defined(nimThinout):
proc unsureAsgnRef(dest: ptr pointer, src: pointer) {.inline.} =
# This is only used by the old RTTI mechanism and we know
# that 'dest[]' is nil and needs no destruction. Which is really handy
@@ -208,7 +208,9 @@ proc nimDestroyAndDispose(p: pointer) {.compilerRtl, quirky, raises: [].} =
cstderr.rawWrite "has destructor!\n"
nimRawDispose(p, rti.align)
when defined(gcOrc):
when defined(gcYrc):
include yrc
elif defined(gcOrc):
when defined(nimThinout):
include cyclebreaker
else:
@@ -225,7 +227,7 @@ proc nimDecRefIsLast(p: pointer): bool {.compilerRtl, inl.} =
writeStackTrace()
cfprintf(cstderr, "[DecRef] %p %ld\n", p, cell.count)
when defined(gcAtomicArc) and hasThreadSupport:
when (defined(gcAtomicArc) or defined(gcYrc)) and hasThreadSupport:
# `atomicDec` returns the new value
if atomicDec(cell.rc, rcIncrement) == -rcIncrement:
result = true
@@ -251,7 +253,7 @@ proc GC_ref*[T](x: ref T) =
## New runtime only supports this operation for 'ref T'.
if x != nil: nimIncRef(cast[pointer](x))
when not defined(gcOrc):
when not (defined(gcOrc) or defined(gcYrc)):
template GC_fullCollect* =
## Forces a full garbage collection pass. With `--mm:arc` a nop.
discard

View File

@@ -9,11 +9,11 @@
include seqs_v2_reimpl
proc genericResetAux(dest: pointer, n: ptr TNimNode) {.benign.}
proc genericResetAux(dest: pointer, n: ptr TNimNode) {.gcsafe.}
proc genericAssignAux(dest, src: pointer, mt: PNimType, shallow: bool) {.benign.}
proc genericAssignAux(dest, src: pointer, mt: PNimType, shallow: bool) {.gcsafe.}
proc genericAssignAux(dest, src: pointer, n: ptr TNimNode,
shallow: bool) {.benign.} =
shallow: bool) {.gcsafe.} =
var
d = cast[int](dest)
s = cast[int](src)
@@ -187,8 +187,8 @@ proc genericAssignOpenArray(dest, src: pointer, len: int,
genericAssign(cast[pointer](d +% i *% mt.base.size),
cast[pointer](s +% i *% mt.base.size), mt.base)
proc objectInit(dest: pointer, typ: PNimType) {.compilerproc, benign.}
proc objectInitAux(dest: pointer, n: ptr TNimNode) {.benign.} =
proc objectInit(dest: pointer, typ: PNimType) {.compilerproc, gcsafe.}
proc objectInitAux(dest: pointer, n: ptr TNimNode) {.gcsafe.} =
var d = cast[int](dest)
case n.kind
of nkNone: sysAssert(false, "objectInitAux")
@@ -224,7 +224,7 @@ proc objectInit(dest: pointer, typ: PNimType) =
# ---------------------- assign zero -----------------------------------------
proc genericReset(dest: pointer, mt: PNimType) {.compilerproc, benign.}
proc genericReset(dest: pointer, mt: PNimType) {.compilerproc, gcsafe.}
proc genericResetAux(dest: pointer, n: ptr TNimNode) =
var d = cast[int](dest)
case n.kind

View File

@@ -51,7 +51,7 @@ proc split(t: var PAvlNode) =
t.link[0] = temp
inc t.level
proc add(a: var MemRegion, t: var PAvlNode, key, upperBound: int) {.benign.} =
proc add(a: var MemRegion, t: var PAvlNode, key, upperBound: int) {.gcsafe.} =
if t.isBottom:
t = allocAvlNode(a, key, upperBound)
else:
@@ -70,7 +70,7 @@ proc add(a: var MemRegion, t: var PAvlNode, key, upperBound: int) {.benign.} =
skew(t)
split(t)
proc del(a: var MemRegion, t: var PAvlNode, x: int) {.benign.} =
proc del(a: var MemRegion, t: var PAvlNode, x: int) {.gcsafe.} =
if isBottom(t): return
a.last = t
if x <% t.key:

View File

@@ -42,13 +42,12 @@ Complete traversal is done in this way::
]#
when defined(gcOrc) or defined(gcArc) or defined(gcAtomicArc):
when defined(gcOrc) or defined(gcArc) or defined(gcAtomicArc) or defined(gcYrc):
type
PCell = Cell
when not declaredInScope(PageShift):
include bitmasks
else:
type
RefCount = int
@@ -56,11 +55,14 @@ else:
Cell {.pure.} = object
refcount: RefCount # the refcount and some flags
typ: PNimType
when trackAllocationSource:
filename: cstring
line: int
when useCellIds:
id: int
when (not trackAllocationSource) and (not useCellIds) and sizeof(int) == 4: # 32-bit only
headerAlignPad: array[8, byte] # so addr(data) ≡ 8 (mod 16)
PCell = ptr Cell
@@ -78,7 +80,7 @@ type
head: PPageDesc
data: PPageDescArray
when defined(gcOrc) or defined(gcArc) or defined(gcAtomicArc):
when defined(gcOrc) or defined(gcArc) or defined(gcAtomicArc) or defined(gcYrc):
discard
else:
include cellseqs_v1

View File

@@ -181,10 +181,10 @@ proc deinitRawChannel(p: pointer) =
when not usesDestructors:
proc storeAux(dest, src: pointer, mt: PNimType, t: PRawChannel,
mode: LoadStoreMode) {.benign.}
mode: LoadStoreMode) {.gcsafe.}
proc storeAux(dest, src: pointer, n: ptr TNimNode, t: PRawChannel,
mode: LoadStoreMode) {.benign.} =
mode: LoadStoreMode) {.gcsafe.} =
var
d = cast[int](dest)
s = cast[int](src)

View File

@@ -38,6 +38,21 @@ proc `==`*[T](x, y: ptr T): bool {.magic: "EqRef", noSideEffect.}
proc `==`*[T: proc | iterator](x, y: T): bool {.magic: "EqProc", noSideEffect.}
## Checks that two `proc` variables refer to the same procedure.
when true:
# guard against string converted to cstring implicitly; see also #bug #25488
proc isNil*(x: string): bool {.noSideEffect, error: "'isNil' is invalid for 'string'".}
# bug #9149; ensure that 'typeof(nil)' does not match *too* well by using 'typeof(nil) | typeof(nil)',
# especially for converters, see tests/overload/tconverter_to_string.nim
# Eventually we will be able to remove this hack completely.
proc `==`*(x: string; y: typeof(nil) | typeof(nil)): bool {.error: "'nil' is invalid for 'string'".} =
discard
proc `==`*(x: typeof(nil) | typeof(nil); y: string): bool {.error: "'nil' is invalid for 'string'".} =
discard
proc `<=`*[Enum: enum](x, y: Enum): bool {.magic: "LeEnum", noSideEffect.}
proc `<=`*(x, y: string): bool {.magic: "LeStr", noSideEffect.} =
## Compares two strings and returns true if `x` is lexicographically

View File

@@ -62,8 +62,8 @@ const
colorMask = 0b011
type
TraceProc = proc (p, env: pointer) {.nimcall, benign, raises: [].}
DisposeProc = proc (p: pointer) {.nimcall, benign, raises: [].}
TraceProc = proc (p, env: pointer) {.nimcall, gcsafe, raises: [].}
DisposeProc = proc (p: pointer) {.nimcall, gcsafe, raises: [].}
template color(c): untyped = c.rc and colorMask
template setColor(c, col) =

View File

@@ -58,9 +58,9 @@ proc put(t: var PtrTable; key, val: pointer) =
inc t.counter
proc genericDeepCopyAux(dest, src: pointer, mt: PNimType;
tab: var PtrTable) {.benign.}
tab: var PtrTable) {.gcsafe.}
proc genericDeepCopyAux(dest, src: pointer, n: ptr TNimNode;
tab: var PtrTable) {.benign.} =
tab: var PtrTable) {.gcsafe.} =
var
d = cast[int](dest)
s = cast[int](src)

View File

@@ -16,7 +16,7 @@ import stacktraces
const noStacktraceAvailable = "No stack traceback available\n"
var
errorMessageWriter*: (proc(msg: string) {.tags: [WriteIOEffect], benign,
errorMessageWriter*: (proc(msg: string) {.tags: [WriteIOEffect], gcsafe,
nimcall, raises: [].})
## Function that will be called
## instead of `stdmsg.write` when printing stacktrace.
@@ -61,10 +61,10 @@ proc showErrorMessage2(data: string) {.inline.} =
# TODO showErrorMessage will turn it back to a string when a hook is set (!)
showErrorMessage(data.cstring, data.len)
proc chckIndx(i, a, b: int): int {.inline, compilerproc, benign.}
proc chckRange(i, a, b: int): int {.inline, compilerproc, benign.}
proc chckRangeF(x, a, b: float): float {.inline, compilerproc, benign.}
proc chckNil(p: pointer) {.noinline, compilerproc, benign.}
proc chckIndx(i, a, b: int): int {.inline, compilerproc, gcsafe.}
proc chckRange(i, a, b: int): int {.inline, compilerproc, gcsafe.}
proc chckRangeF(x, a, b: float): float {.inline, compilerproc, gcsafe.}
proc chckNil(p: pointer) {.noinline, compilerproc, gcsafe.}
type
GcFrame = ptr GcFrameHeader
@@ -653,7 +653,7 @@ when defined(cpp) and appType != "lib" and not gotoBasedExceptions and
rawQuit 1
when not defined(noSignalHandler) and not defined(useNimRtl):
type Sighandler = proc (a: cint) {.noconv, benign.}
type Sighandler = proc (a: cint) {.noconv, gcsafe.}
# xxx factor with ansi_c.CSighandlerT, posix.Sighandler
proc signalHandler(sign: cint) {.exportc: "signalHandler", noconv, raises: [].} =

View File

@@ -76,7 +76,7 @@ const
when withRealTime and not declared(getTicks):
include "system/timers"
when defined(memProfiler):
proc nimProfile(requestedSize: int) {.benign.}
proc nimProfile(requestedSize: int) {.gcsafe.}
when hasThreadSupport:
import std/sharedlist
@@ -97,7 +97,7 @@ type
waZctDecRef, waPush
#, waDebug
Finalizer {.compilerproc.} = proc (self: pointer) {.nimcall, benign, raises: [], gcsafe.}
Finalizer {.compilerproc.} = proc (self: pointer) {.nimcall, gcsafe, raises: [].}
# A ref type can have a finalizer that is called before the object's
# storage is freed.
@@ -222,11 +222,11 @@ template gcTrace(cell, state: untyped) =
when traceGC: traceCell(cell, state)
# forward declarations:
proc collectCT(gch: var GcHeap) {.benign, raises: [].}
proc isOnStack(p: pointer): bool {.noinline, benign, raises: [].}
proc forAllChildren(cell: PCell, op: WalkOp) {.benign, raises: [].}
proc doOperation(p: pointer, op: WalkOp) {.benign, raises: [].}
proc forAllChildrenAux(dest: pointer, mt: PNimType, op: WalkOp) {.benign, raises: [].}
proc collectCT(gch: var GcHeap) {.gcsafe, raises: [].}
proc isOnStack(p: pointer): bool {.noinline, gcsafe, raises: [].}
proc forAllChildren(cell: PCell, op: WalkOp) {.gcsafe, raises: [].}
proc doOperation(p: pointer, op: WalkOp) {.gcsafe, raises: [].}
proc forAllChildrenAux(dest: pointer, mt: PNimType, op: WalkOp) {.gcsafe, raises: [].}
# we need the prototype here for debugging purposes
proc incRef(c: PCell) {.inline.} =
@@ -338,7 +338,7 @@ proc cellsetReset(s: var CellSet) =
{.push stacktrace:off.}
proc forAllSlotsAux(dest: pointer, n: ptr TNimNode, op: WalkOp) {.benign.} =
proc forAllSlotsAux(dest: pointer, n: ptr TNimNode, op: WalkOp) {.gcsafe.} =
var d = cast[int](dest)
case n.kind
of nkSlot: forAllChildrenAux(cast[pointer](d +% n.offset), n.typ, op)
@@ -458,9 +458,16 @@ proc rawNewObj(typ: PNimType, size: int, gch: var GcHeap): pointer =
sysAssert(allocInv(gch.region), "rawNewObj begin")
gcAssert(typ.kind in {tyRef, tyString, tySequence}, "newObj: 1")
collectCT(gch)
var res = cast[PCell](rawAlloc(gch.region, size + sizeof(Cell)))
# Use alignment from typ.base if available, otherwise use MemAlign
let alignment = if typ.kind == tyRef and typ.base != nil and
typ.base.align > 16: typ.base.align else: 0
var res = cast[PCell](rawAlloc(gch.region, size + sizeof(Cell), alignment))
#gcAssert typ.kind in {tyString, tySequence} or size >= typ.base.size, "size too small"
gcAssert((cast[int](res) and (MemAlign-1)) == 0, "newObj: 2")
# Check that the user data (after the Cell header) is properly aligned
if alignment == 0:
gcAssert((cast[int](res) and (MemAlign-1)) == 0, "newObj: 2.1")
else:
gcAssert((cast[int](cellToUsr(res)) and (alignment-1)) == 0, "newObj: 2.2")
# now it is buffered in the ZCT
res.typ = typ
setFrameInfo(res)
@@ -508,9 +515,16 @@ proc newObjRC1(typ: PNimType, size: int): pointer {.compilerRtl, noinline, raise
collectCT(gch)
sysAssert(allocInv(gch.region), "newObjRC1 after collectCT")
var res = cast[PCell](rawAlloc(gch.region, size + sizeof(Cell)))
# Use alignment from typ.base if available, otherwise use MemAlign
let alignment = if typ.kind == tyRef and typ.base != nil and
typ.base.align > 16: typ.base.align else: 0
var res = cast[PCell](rawAlloc(gch.region, size + sizeof(Cell), alignment))
sysAssert(allocInv(gch.region), "newObjRC1 after rawAlloc")
sysAssert((cast[int](res) and (MemAlign-1)) == 0, "newObj: 2")
# Check that the user data (after the Cell header) is properly aligned
if alignment == 0:
sysAssert((cast[int](res) and (MemAlign-1)) == 0, "newObj: 2.1")
else:
sysAssert((cast[int](cellToUsr(res)) and (alignment-1)) == 0, "newObj: 2.2")
# now it is buffered in the ZCT
res.typ = typ
setFrameInfo(res)
@@ -673,7 +687,7 @@ proc doOperation(p: pointer, op: WalkOp) =
proc nimGCvisit(d: pointer, op: int) {.compilerRtl, raises: [].} =
doOperation(d, WalkOp(op))
proc collectZCT(gch: var GcHeap): bool {.benign, raises: [].}
proc collectZCT(gch: var GcHeap): bool {.gcsafe, raises: [].}
proc collectCycles(gch: var GcHeap) {.raises: [].} =
when hasThreadSupport:
@@ -916,4 +930,4 @@ when not defined(useNimRtl):
result.add "[GC] max stack size: " & $gch.stat.maxStackSize & "\n"
{.pop.} # raises: []
{.pop.} # profiler: off, stackTrace: off
{.pop.} # profiler: off, stackTrace: off

View File

@@ -457,7 +457,7 @@ proc deallocHeap*(runFinalizers = true; allowGcAfterwards = true) =
initGC()
type
GlobalMarkerProc = proc () {.nimcall, benign, raises: [].}
GlobalMarkerProc = proc () {.nimcall, gcsafe, raises: [].}
var
globalMarkersLen {.exportc.}: int
globalMarkers {.exportc.}: array[0..3499, GlobalMarkerProc]

View File

@@ -11,7 +11,7 @@
## collectors etc.
type
GlobalMarkerProc = proc () {.nimcall, benign, raises: [], tags: [].}
GlobalMarkerProc = proc () {.nimcall, gcsafe, raises: [], tags: [].}
var
globalMarkersLen: int
globalMarkers: array[0..3499, GlobalMarkerProc]

View File

@@ -12,7 +12,7 @@ when hasAlloc:
gcOptimizeSpace ## optimize for memory footprint
when hasAlloc and not defined(js) and not usesDestructors:
proc GC_disable*() {.rtl, inl, benign, raises: [].}
proc GC_disable*() {.rtl, inl, gcsafe, raises: [].}
## Disables the GC. If called `n` times, `n` calls to `GC_enable`
## are needed to reactivate the GC.
##
@@ -20,39 +20,39 @@ when hasAlloc and not defined(js) and not usesDestructors:
## the mark and sweep phase with
## `GC_disableMarkAndSweep <#GC_disableMarkAndSweep>`_.
proc GC_enable*() {.rtl, inl, benign, raises: [].}
proc GC_enable*() {.rtl, inl, gcsafe, raises: [].}
## Enables the GC again.
proc GC_fullCollect*() {.rtl, benign, raises: [].}
proc GC_fullCollect*() {.rtl, gcsafe, raises: [].}
## Forces a full garbage collection pass.
## Ordinary code does not need to call this (and should not).
proc GC_enableMarkAndSweep*() {.rtl, benign, raises: [].}
proc GC_disableMarkAndSweep*() {.rtl, benign, raises: [].}
proc GC_enableMarkAndSweep*() {.rtl, gcsafe, raises: [].}
proc GC_disableMarkAndSweep*() {.rtl, gcsafe, raises: [].}
## The current implementation uses a reference counting garbage collector
## with a seldomly run mark and sweep phase to free cycles. The mark and
## sweep phase may take a long time and is not needed if the application
## does not create cycles. Thus the mark and sweep phase can be deactivated
## and activated separately from the rest of the GC.
proc GC_getStatistics*(): string {.rtl, benign, raises: [].}
proc GC_getStatistics*(): string {.rtl, gcsafe, raises: [].}
## Returns an informative string about the GC's activity. This may be useful
## for tweaking.
proc GC_ref*[T](x: ref T) {.magic: "GCref", benign, raises: [].}
proc GC_ref*[T](x: seq[T]) {.magic: "GCref", benign, raises: [].}
proc GC_ref*(x: string) {.magic: "GCref", benign, raises: [].}
proc GC_ref*[T](x: ref T) {.magic: "GCref", gcsafe, raises: [].}
proc GC_ref*[T](x: seq[T]) {.magic: "GCref", gcsafe, raises: [].}
proc GC_ref*(x: string) {.magic: "GCref", gcsafe, raises: [].}
## Marks the object `x` as referenced, so that it will not be freed until
## it is unmarked via `GC_unref`.
## If called n-times for the same object `x`,
## n calls to `GC_unref` are needed to unmark `x`.
proc GC_unref*[T](x: ref T) {.magic: "GCunref", benign, raises: [].}
proc GC_unref*[T](x: seq[T]) {.magic: "GCunref", benign, raises: [].}
proc GC_unref*(x: string) {.magic: "GCunref", benign, raises: [].}
proc GC_unref*[T](x: ref T) {.magic: "GCunref", gcsafe, raises: [].}
proc GC_unref*[T](x: seq[T]) {.magic: "GCunref", gcsafe, raises: [].}
proc GC_unref*(x: string) {.magic: "GCunref", gcsafe, raises: [].}
## See the documentation of `GC_ref <#GC_ref,string>`_.
proc nimGC_setStackBottom*(theStackBottom: pointer) {.compilerRtl, noinline, benign, raises: [].}
proc nimGC_setStackBottom*(theStackBottom: pointer) {.compilerRtl, noinline, gcsafe, raises: [].}
## Expands operating GC stack range to `theStackBottom`. Does nothing
## if current stack bottom is already lower than `theStackBottom`.

View File

@@ -36,7 +36,7 @@ type
# local
waMarkPrecise # fast precise marking
Finalizer {.compilerproc.} = proc (self: pointer) {.nimcall, benign, raises: [], gcsafe.}
Finalizer {.compilerproc.} = proc (self: pointer) {.nimcall, gcsafe, raises: [].}
# A ref type can have a finalizer that is called before the object's
# storage is freed.
@@ -115,10 +115,10 @@ when BitsPerPage mod (sizeof(int)*8) != 0:
{.error: "(BitsPerPage mod BitsPerUnit) should be zero!".}
# forward declarations:
proc collectCT(gch: var GcHeap; size: int) {.benign, raises: [].}
proc forAllChildren(cell: PCell, op: WalkOp) {.benign, raises: [].}
proc doOperation(p: pointer, op: WalkOp) {.benign, raises: [].}
proc forAllChildrenAux(dest: pointer, mt: PNimType, op: WalkOp) {.benign, raises: [].}
proc collectCT(gch: var GcHeap; size: int) {.gcsafe, raises: [].}
proc forAllChildren(cell: PCell, op: WalkOp) {.gcsafe, raises: [].}
proc doOperation(p: pointer, op: WalkOp) {.gcsafe, raises: [].}
proc forAllChildrenAux(dest: pointer, mt: PNimType, op: WalkOp) {.gcsafe, raises: [].}
# we need the prototype here for debugging purposes
when defined(nimGcRefLeak):
@@ -216,7 +216,7 @@ proc initGC() =
gch.gcThreadId = atomicInc(gHeapidGenerator) - 1
gcAssert(gch.gcThreadId >= 0, "invalid computed thread ID")
proc forAllSlotsAux(dest: pointer, n: ptr TNimNode, op: WalkOp) {.benign.} =
proc forAllSlotsAux(dest: pointer, n: ptr TNimNode, op: WalkOp) {.gcsafe.} =
var d = cast[int](dest)
case n.kind
of nkSlot: forAllChildrenAux(cast[pointer](d +% n.offset), n.typ, op)

View File

@@ -12,7 +12,7 @@
import std/private/syslocks
when defined(memProfiler):
proc nimProfile(requestedSize: int) {.benign.}
proc nimProfile(requestedSize: int) {.gcsafe.}
when defined(useMalloc):
proc roundup(x, v: int): int {.inline.} =
@@ -41,7 +41,7 @@ else:
# We also support 'finalizers'.
type
Finalizer {.compilerproc.} = proc (self: pointer) {.nimcall, benign, raises: [], gcsafe.}
Finalizer {.compilerproc.} = proc (self: pointer) {.nimcall, gcsafe, raises: [].}
# A ref type can have a finalizer that is called before the object's
# storage is freed.

View File

@@ -96,8 +96,8 @@ type
base*: ptr TNimType
node: ptr TNimNode # valid for tyRecord, tyObject, tyTuple, tyEnum
finalizer*: pointer # the finalizer for the type
marker*: proc (p: pointer, op: int) {.nimcall, benign, tags: [], raises: [].} # marker proc for GC
deepcopy: proc (p: pointer): pointer {.nimcall, benign, tags: [], raises: [].}
marker*: proc (p: pointer, op: int) {.nimcall, gcsafe, tags: [], raises: [].} # marker proc for GC
deepcopy: proc (p: pointer): pointer {.nimcall, gcsafe, tags: [], raises: [].}
when defined(nimSeqsV2):
typeInfoV2*: pointer
when defined(nimTypeNames):

View File

@@ -51,7 +51,7 @@ proc nimCharToStr(x: char): string {.compilerproc.} =
proc isNimException(): bool {.asmNoStackFrame.} =
{.emit: "return `lastJSError` && `lastJSError`.m_type;".}
proc getCurrentException*(): ref Exception {.compilerRtl, benign.} =
proc getCurrentException*(): ref Exception {.compilerRtl, gcsafe.} =
if isNimException(): result = cast[ref Exception](lastJSError)
proc getCurrentExceptionMsg*(): string =
@@ -72,7 +72,7 @@ proc getCurrentExceptionMsg*(): string =
proc setCurrentException*(exc: ref Exception) =
lastJSError = cast[PJSError](exc)
proc closureIterSetExc(e: ref Exception) {.compilerRtl, benign.} =
proc closureIterSetExc(e: ref Exception) {.compilerRtl, gcsafe.} =
setCurrentException(e)
proc pushCurrentException(e: sink(ref Exception)) {.compilerRtl, inline.} =

View File

@@ -6,7 +6,7 @@ when notJSnotNims:
## Exactly `size` bytes will be overwritten. Like any procedure
## dealing with raw memory this is **unsafe**.
proc copyMem*(dest, source: pointer, size: Natural) {.inline, benign,
proc copyMem*(dest, source: pointer, size: Natural) {.inline, gcsafe,
tags: [], raises: [], enforceNoRaises.}
## Copies the contents from the memory at `source` to the memory
## at `dest`.
@@ -14,7 +14,7 @@ when notJSnotNims:
## regions may not overlap. Like any procedure dealing with raw
## memory this is **unsafe**.
proc moveMem*(dest, source: pointer, size: Natural) {.inline, benign,
proc moveMem*(dest, source: pointer, size: Natural) {.inline, gcsafe,
tags: [], raises: [], enforceNoRaises.}
## Copies the contents from the memory at `source` to the memory
## at `dest`.
@@ -48,17 +48,17 @@ when notJSnotNims:
when hasAlloc and not defined(js):
proc allocImpl*(size: Natural): pointer {.noconv, rtl, tags: [], benign, raises: [].}
proc alloc0Impl*(size: Natural): pointer {.noconv, rtl, tags: [], benign, raises: [].}
proc deallocImpl*(p: pointer) {.noconv, rtl, tags: [], benign, raises: [].}
proc reallocImpl*(p: pointer, newSize: Natural): pointer {.noconv, rtl, tags: [], benign, raises: [].}
proc realloc0Impl*(p: pointer, oldSize, newSize: Natural): pointer {.noconv, rtl, tags: [], benign, raises: [].}
proc allocImpl*(size: Natural): pointer {.noconv, rtl, tags: [], gcsafe, raises: [].}
proc alloc0Impl*(size: Natural): pointer {.noconv, rtl, tags: [], gcsafe, raises: [].}
proc deallocImpl*(p: pointer) {.noconv, rtl, tags: [], gcsafe, raises: [].}
proc reallocImpl*(p: pointer, newSize: Natural): pointer {.noconv, rtl, tags: [], gcsafe, raises: [].}
proc realloc0Impl*(p: pointer, oldSize, newSize: Natural): pointer {.noconv, rtl, tags: [], gcsafe, raises: [].}
proc allocSharedImpl*(size: Natural): pointer {.noconv, compilerproc, rtl, benign, raises: [], tags: [].}
proc allocShared0Impl*(size: Natural): pointer {.noconv, rtl, benign, raises: [], tags: [].}
proc deallocSharedImpl*(p: pointer) {.noconv, rtl, benign, raises: [], tags: [].}
proc reallocSharedImpl*(p: pointer, newSize: Natural): pointer {.noconv, rtl, tags: [], benign, raises: [].}
proc reallocShared0Impl*(p: pointer, oldSize, newSize: Natural): pointer {.noconv, rtl, tags: [], benign, raises: [].}
proc allocSharedImpl*(size: Natural): pointer {.noconv, compilerproc, rtl, gcsafe, raises: [], tags: [].}
proc allocShared0Impl*(size: Natural): pointer {.noconv, rtl, gcsafe, raises: [], tags: [].}
proc deallocSharedImpl*(p: pointer) {.noconv, rtl, gcsafe, raises: [], tags: [].}
proc reallocSharedImpl*(p: pointer, newSize: Natural): pointer {.noconv, rtl, tags: [], gcsafe, raises: [].}
proc reallocShared0Impl*(p: pointer, oldSize, newSize: Natural): pointer {.noconv, rtl, tags: [], gcsafe, raises: [].}
# Allocator statistics for memory leak tests
@@ -103,7 +103,7 @@ when hasAlloc and not defined(js):
incStat(allocCount)
allocImpl(size)
proc createU*(T: typedesc, size = 1.Positive): ptr T {.inline, benign, raises: [].} =
proc createU*(T: typedesc, size = 1.Positive): ptr T {.inline, gcsafe, raises: [].} =
## Allocates a new memory block with at least `T.sizeof * size` bytes.
##
## The block has to be freed with `resize(block, 0) <#resize,ptr.T,Natural>`_
@@ -131,7 +131,7 @@ when hasAlloc and not defined(js):
incStat(allocCount)
alloc0Impl(size)
proc create*(T: typedesc, size = 1.Positive): ptr T {.inline, benign, raises: [].} =
proc create*(T: typedesc, size = 1.Positive): ptr T {.inline, gcsafe, raises: [].} =
## Allocates a new memory block with at least `T.sizeof * size` bytes.
##
## The block has to be freed with `resize(block, 0) <#resize,ptr.T,Natural>`_
@@ -174,7 +174,7 @@ when hasAlloc and not defined(js):
## from a shared heap.
realloc0Impl(p, oldSize, newSize)
proc resize*[T](p: ptr T, newSize: Natural): ptr T {.inline, benign, raises: [].} =
proc resize*[T](p: ptr T, newSize: Natural): ptr T {.inline, gcsafe, raises: [].} =
## Grows or shrinks a given memory block.
##
## If `p` is **nil** then a new memory block is returned.
@@ -187,7 +187,7 @@ when hasAlloc and not defined(js):
## from a shared heap.
cast[ptr T](realloc(p, T.sizeof * newSize))
proc dealloc*(p: pointer) {.noconv, compilerproc, rtl, benign, raises: [], tags: [].} =
proc dealloc*(p: pointer) {.noconv, compilerproc, rtl, gcsafe, raises: [], tags: [].} =
## Frees the memory allocated with `alloc`, `alloc0`,
## `realloc`, `create` or `createU`.
##
@@ -218,7 +218,7 @@ when hasAlloc and not defined(js):
allocSharedImpl(size)
proc createSharedU*(T: typedesc, size = 1.Positive): ptr T {.inline, tags: [],
benign, raises: [].} =
gcsafe, raises: [].} =
## Allocates a new memory block on the shared heap with at
## least `T.sizeof * size` bytes.
##
@@ -296,7 +296,7 @@ when hasAlloc and not defined(js):
## `freeShared <#freeShared,ptr.T>`_.
cast[ptr T](reallocShared(p, T.sizeof * newSize))
proc deallocShared*(p: pointer) {.noconv, compilerproc, rtl, benign, raises: [], tags: [].} =
proc deallocShared*(p: pointer) {.noconv, compilerproc, rtl, gcsafe, raises: [], tags: [].} =
## Frees the memory allocated with `allocShared`, `allocShared0` or
## `reallocShared`.
##
@@ -307,7 +307,7 @@ when hasAlloc and not defined(js):
incStat(deallocCount)
deallocSharedImpl(p)
proc freeShared*[T](p: ptr T) {.inline, benign, raises: [].} =
proc freeShared*[T](p: ptr T) {.inline, gcsafe, raises: [].} =
## Frees the memory allocated with `createShared`, `createSharedU` or
## `resizeShared`.
##

View File

@@ -50,7 +50,7 @@ proc deallocSharedImpl(p: pointer) = deallocImpl(p)
proc GC_disable() = discard
proc GC_enable() = discard
when not defined(gcOrc):
when not defined(gcOrc) and not defined(gcYrc):
proc GC_fullCollect() = discard
proc GC_enableMarkAndSweep() = discard
proc GC_disableMarkAndSweep() = discard

View File

@@ -29,8 +29,8 @@ const
logOrc = defined(nimArcIds)
type
TraceProc = proc (p, env: pointer) {.nimcall, benign, raises: [].}
DisposeProc = proc (p: pointer) {.nimcall, benign, raises: [].}
TraceProc = proc (p, env: pointer) {.nimcall, gcsafe, raises: [].}
DisposeProc = proc (p: pointer) {.nimcall, gcsafe, raises: [].}
template color(c): untyped = c.rc and colorMask
template setColor(c, col) =
@@ -433,8 +433,9 @@ proc collectCycles() =
rootsThreshold = (if rootsThreshold <= 0: defaultThreshold else: rootsThreshold)
rootsThreshold = rootsThreshold div 2 +% rootsThreshold
when logOrc:
cfprintf(cstderr, "[collectCycles] end; freed %ld new threshold %ld touched: %ld mem: %ld rcSum: %ld edges: %ld\n", j.freed, rootsThreshold, j.touched,
getOccupiedMem(), j.rcSum, j.edges)
{.cast(raises: []).}:
discard cfprintf(cstderr, "[collectCycles] end; freed %ld new threshold %ld touched: %ld mem: %ld rcSum: %ld edges: %ld\n", j.freed, rootsThreshold, j.touched,
getOccupiedMem(), j.rcSum, j.edges)
when defined(nimOrcStats):
inc freedCyclicObjects, j.freed
@@ -465,13 +466,13 @@ proc GC_runOrc* =
proc GC_enableOrc*() =
## Enables the cycle collector subsystem of `--mm:orc`. This is a `--mm:orc`
## specific API. Check with `when defined(gcOrc)` for its existence.
## specific API. Check with `when defined(gcOrc) or defined(gcYrc)` for its existence.
when not defined(nimStressOrc):
rootsThreshold = 0
proc GC_disableOrc*() =
## Disables the cycle collector subsystem of `--mm:orc`. This is a `--mm:orc`
## specific API. Check with `when defined(gcOrc)` for its existence.
## specific API. Check with `when defined(gcOrc) or defined(gcYrc)` for its existence.
when not defined(nimStressOrc):
rootsThreshold = high(int)

View File

@@ -31,8 +31,8 @@ const doNotUnmap = not (defined(amd64) or defined(i386)) or
when defined(nimAllocPagesViaMalloc):
when not defined(gcArc) and not defined(gcOrc) and not defined(gcAtomicArc):
{.error: "-d:nimAllocPagesViaMalloc is only supported with --mm:arc or --mm:atomicArc or --mm:orc".}
when not defined(gcArc) and not defined(gcOrc) and not defined(gcAtomicArc) and not defined(gcYrc):
{.error: "-d:nimAllocPagesViaMalloc is only supported with --mm:arc or --mm:atomicArc or --mm:orc or --mm:yrc".}
proc osTryAllocPages(size: int): pointer {.inline.} =
let base = c_malloc(csize_t size + PageSize - 1 + sizeof(uint32))

View File

@@ -77,7 +77,7 @@ include system/repr_impl
type
PByteArray = ptr UncheckedArray[byte] # array[0xffff, byte]
proc addSetElem(result: var string, elem: int, typ: PNimType) {.benign.} =
proc addSetElem(result: var string, elem: int, typ: PNimType) {.gcsafe.} =
case typ.kind
of tyEnum: add result, reprEnum(elem, typ)
of tyBool: add result, reprBool(bool(elem))
@@ -147,7 +147,7 @@ when not defined(useNimRtl):
for i in 0..cl.indent-1: add result, ' '
proc reprAux(result: var string, p: pointer, typ: PNimType,
cl: var ReprClosure) {.benign.}
cl: var ReprClosure) {.gcsafe.}
proc reprArray(result: var string, p: pointer, typ: PNimType,
cl: var ReprClosure) =
@@ -188,7 +188,7 @@ when not defined(useNimRtl):
add result, "]"
proc reprRecordAux(result: var string, p: pointer, n: ptr TNimNode,
cl: var ReprClosure) {.benign.} =
cl: var ReprClosure) {.gcsafe.} =
case n.kind
of nkNone: sysAssert(false, "reprRecordAux")
of nkSlot:

143
lib/system/rwlocks.nim Normal file
View File

@@ -0,0 +1,143 @@
#
#
# Nim's Runtime Library
# (c) Copyright 2026 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
# Read-write lock (RwLock) for lib/system.
# Used by YRC and by traceable containers that perform topology-changing ops.
# POSIX: pthread_rwlock_* ; Windows: SRWLOCK (slim reader/writer).
{.push stackTrace: off.}
when defined(windows):
# SRWLOCK is pointer-sized; use single pointer for ABI compatibility
type
RwLock* {.importc: "SRWLOCK", header: "<synchapi.h>", final, pure, byref.} = object
p: pointer
proc initializeSRWLock(L: var RwLock) {.importc: "InitializeSRWLock",
header: "<synchapi.h>".}
proc acquireSRWLockShared(L: var RwLock) {.importc: "AcquireSRWLockShared",
header: "<synchapi.h>".}
proc releaseSRWLockShared(L: var RwLock) {.importc: "ReleaseSRWLockShared",
header: "<synchapi.h>".}
proc acquireSRWLockExclusive(L: var RwLock) {.importc: "AcquireSRWLockExclusive",
header: "<synchapi.h>".}
proc releaseSRWLockExclusive(L: var RwLock) {.importc: "ReleaseSRWLockExclusive",
header: "<synchapi.h>".}
proc initRwLock*(L: var RwLock) {.inline.} =
initializeSRWLock(L)
proc deinitRwLock*(L: var RwLock) {.inline.} =
discard
proc acquireRead*(L: var RwLock) {.inline.} =
acquireSRWLockShared(L)
proc releaseRead*(L: var RwLock) {.inline.} =
releaseSRWLockShared(L)
proc acquireWrite*(L: var RwLock) {.inline.} =
acquireSRWLockExclusive(L)
proc releaseWrite*(L: var RwLock) {.inline.} =
releaseSRWLockExclusive(L)
elif defined(genode):
{.error: "RwLock is not implemented for Genode".}
else:
# POSIX: pthread_rwlock_*
type
SysRwLockObj {.importc: "pthread_rwlock_t", pure, final,
header: """#include <sys/types.h>
#include <pthread.h>""", byref.} = object
when defined(linux) and defined(amd64):
abi: array[56 div sizeof(clong), clong]
proc pthread_rwlock_init(rwlock: var SysRwLockObj, attr: pointer): cint {.
importc: "pthread_rwlock_init", header: "<pthread.h>", noSideEffect.}
proc pthread_rwlock_destroy(rwlock: var SysRwLockObj): cint {.
importc: "pthread_rwlock_destroy", header: "<pthread.h>", noSideEffect.}
proc pthread_rwlock_rdlock(rwlock: var SysRwLockObj): cint {.
importc: "pthread_rwlock_rdlock", header: "<pthread.h>", noSideEffect.}
proc pthread_rwlock_wrlock(rwlock: var SysRwLockObj): cint {.
importc: "pthread_rwlock_wrlock", header: "<pthread.h>", noSideEffect.}
proc pthread_rwlock_unlock(rwlock: var SysRwLockObj): cint {.
importc: "pthread_rwlock_unlock", header: "<pthread.h>", noSideEffect.}
when defined(linux):
# PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP: once a writer is waiting,
# new readers block. Prevents continuous mutator read-locks from starving
# the collector's write-lock acquisition (glibc default is PREFER_READER).
type
SysRwLockAttr {.importc: "pthread_rwlockattr_t", pure, final,
header: "<pthread.h>".} = object
const PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP = cint(3)
proc pthread_rwlockattr_init(attr: ptr SysRwLockAttr): cint {.
importc: "pthread_rwlockattr_init", header: "<pthread.h>".}
proc pthread_rwlockattr_destroy(attr: ptr SysRwLockAttr): cint {.
importc: "pthread_rwlockattr_destroy", header: "<pthread.h>".}
proc pthread_rwlockattr_setkind_np(attr: ptr SysRwLockAttr; pref: cint): cint {.
importc: "pthread_rwlockattr_setkind_np", header: "<pthread.h>".}
when defined(ios):
type RwLock* = ptr SysRwLockObj
proc initRwLock*(L: var RwLock) =
when not declared(c_malloc):
proc c_malloc(size: csize_t): pointer {.importc: "malloc", header: "<stdlib.h>".}
proc c_free(p: pointer) {.importc: "free", header: "<stdlib.h>".}
L = cast[RwLock](c_malloc(csize_t(sizeof(SysRwLockObj))))
discard pthread_rwlock_init(L[], nil)
proc deinitRwLock*(L: var RwLock) =
if L != nil:
discard pthread_rwlock_destroy(L[])
when not declared(c_free):
proc c_free(p: pointer) {.importc: "free", header: "<stdlib.h>".}
c_free(L)
L = nil
proc acquireRead*(L: var RwLock) =
discard pthread_rwlock_rdlock(L[])
proc releaseRead*(L: var RwLock) =
discard pthread_rwlock_unlock(L[])
proc acquireWrite*(L: var RwLock) =
discard pthread_rwlock_wrlock(L[])
proc releaseWrite*(L: var RwLock) =
discard pthread_rwlock_unlock(L[])
else:
type RwLock* = SysRwLockObj
proc initRwLock*(L: var RwLock) =
when defined(linux):
var attr: SysRwLockAttr
discard pthread_rwlockattr_init(addr attr)
discard pthread_rwlockattr_setkind_np(addr attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP)
discard pthread_rwlock_init(L, addr attr)
discard pthread_rwlockattr_destroy(addr attr)
else:
discard pthread_rwlock_init(L, nil)
proc deinitRwLock*(L: var RwLock) =
discard pthread_rwlock_destroy(L)
proc acquireRead*(L: var RwLock) =
discard pthread_rwlock_rdlock(L)
proc releaseRead*(L: var RwLock) =
discard pthread_rwlock_unlock(L)
proc acquireWrite*(L: var RwLock) =
discard pthread_rwlock_wrlock(L)
proc releaseWrite*(L: var RwLock) =
discard pthread_rwlock_unlock(L)
template withReadLock*(L: var RwLock, body: untyped) =
acquireRead(L)
try:
body
finally:
releaseRead(L)
template withWriteLock*(L: var RwLock, body: untyped) =
acquireWrite(L)
try:
body
finally:
releaseWrite(L)
{.pop.}

View File

@@ -11,6 +11,90 @@
# import std/typetraits
# strs already imported allocateds for us.
when defined(gcYrc):
include rwlocks
include threadids
const
NumLockStripes = 64
type
YrcLockState = enum
HasNoLock
HasMutatorLock
HasCollectorLock
Collecting
AlignedRwLock = object
## One RwLock per cache line. {.align: 64.} causes the compiler to round
## the struct size up to 64 bytes, so consecutive array elements never
## share a cache line (sizeof(RwLock) = 56 on Linux x86_64 → 8 byte pad).
lock {.align: 64.}: RwLock
var
gYrcLocks: array[NumLockStripes, AlignedRwLock]
var
lockState {.threadvar.}: YrcLockState
proc getYrcStripe(): int {.inline.} =
## Map this thread to one of the NumLockStripes RwLock stripes.
## getThreadId() is already cached thread-locally in threadids.nim.
getThreadId() and (NumLockStripes - 1)
proc acquireMutatorLock() {.compilerRtl, inl.} =
if lockState == HasNoLock:
acquireRead gYrcLocks[getYrcStripe()].lock
lockState = HasMutatorLock
proc releaseMutatorLock() {.compilerRtl, inl.} =
if lockState == HasMutatorLock:
lockState = HasNoLock
releaseRead gYrcLocks[getYrcStripe()].lock
template yrcMutatorLock*(t: typedesc; body: untyped) =
{.noSideEffect.}:
when canFormCycles(t):
acquireMutatorLock()
try:
body
finally:
{.noSideEffect.}:
when canFormCycles(t):
releaseMutatorLock()
template yrcMutatorLockUntyped(body: untyped) =
{.noSideEffect.}:
acquireMutatorLock()
try:
body
finally:
{.noSideEffect.}:
releaseMutatorLock()
template yrcCollectorLock(body: untyped) =
if lockState == HasMutatorLock: releaseMutatorLock()
let prevState = lockState
let hadToAcquire = prevState < HasCollectorLock
if hadToAcquire:
# Acquire all stripes in ascending order — the only thread ever holding
# multiple write locks is the collector, so there is no lock-order cycle.
for yrcI in 0..<NumLockStripes:
acquireWrite(gYrcLocks[yrcI].lock)
lockState = HasCollectorLock
try:
body
finally:
if hadToAcquire:
for yrcI in 0..<NumLockStripes:
releaseWrite(gYrcLocks[yrcI].lock)
lockState = prevState
else:
template yrcMutatorLock*(t: typedesc; body: untyped) =
body
template yrcMutatorLockUntyped(body: untyped) =
body
# Some optimizations here may be not to empty-seq-initialize some symbols, then StrictNotNil complains.
{.push warning[StrictNotNil]: off.} # See https://github.com/nim-lang/Nim/issues/21401
@@ -116,33 +200,35 @@ proc prepareSeqAddUninit(len: int; p: pointer; addlen, elemSize, elemAlign: int)
q.cap = newCap
result = q
proc shrink*[T](x: var seq[T]; newLen: Natural) {.tags: [], raises: [].} =
proc shrink*[T](x: var seq[T]; newLen: Natural) {.tags: [], raises: [], noSideEffect.} =
when nimvm:
{.cast(tags: []).}:
setLen(x, newLen)
else:
#sysAssert newLen <= x.len, "invalid newLen parameter for 'shrink'"
when not supportsCopyMem(T):
for i in countdown(x.len - 1, newLen):
reset x[i]
# XXX This is wrong for const seqs that were moved into 'x'!
{.noSideEffect.}:
cast[ptr NimSeqV2[T]](addr x).len = newLen
yrcMutatorLock(T):
when not supportsCopyMem(T):
for i in countdown(x.len - 1, newLen):
reset x[i]
# XXX This is wrong for const seqs that were moved into 'x'!
{.noSideEffect.}:
cast[ptr NimSeqV2[T]](addr x).len = newLen
proc grow*[T](x: var seq[T]; newLen: Natural; value: T) {.nodestroy.} =
let oldLen = x.len
#sysAssert newLen >= x.len, "invalid newLen parameter for 'grow'"
if newLen <= oldLen: return
var xu = cast[ptr NimSeqV2[T]](addr x)
if xu.p == nil or (xu.p.cap and not strlitFlag) < newLen:
xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, newLen - oldLen, sizeof(T), alignof(T)))
xu.len = newLen
for i in oldLen .. newLen-1:
when (NimMajor, NimMinor, NimPatch) >= (2, 3, 1):
xu.p.data[i] = `=dup`(value)
else:
wasMoved(xu.p.data[i])
`=copy`(xu.p.data[i], value)
yrcMutatorLock(T):
var xu = cast[ptr NimSeqV2[T]](addr x)
if xu.p == nil or (xu.p.cap and not strlitFlag) < newLen:
xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, newLen - oldLen, sizeof(T), alignof(T)))
xu.len = newLen
for i in oldLen .. newLen-1:
when (NimMajor, NimMinor, NimPatch) >= (2, 3, 1):
xu.p.data[i] = `=dup`(value)
else:
wasMoved(xu.p.data[i])
`=copy`(xu.p.data[i], value)
proc add*[T](x: var seq[T]; y: sink T) {.magic: "AppendSeqElem", noSideEffect, nodestroy.} =
## Generic proc for adding a data item `y` to a container `x`.
@@ -152,30 +238,32 @@ proc add*[T](x: var seq[T]; y: sink T) {.magic: "AppendSeqElem", noSideEffect, n
## Generic code becomes much easier to write if the Nim naming scheme is
## respected.
{.cast(noSideEffect).}:
let oldLen = x.len
var xu = cast[ptr NimSeqV2[T]](addr x)
if xu.p == nil or (xu.p.cap and not strlitFlag) < oldLen+1:
xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, 1, sizeof(T), alignof(T)))
xu.len = oldLen+1
# .nodestroy means `xu.p.data[oldLen] = value` is compiled into a
# copyMem(). This is fine as know by construction that
# in `xu.p.data[oldLen]` there is nothing to destroy.
# We also save the `wasMoved + destroy` pair for the sink parameter.
xu.p.data[oldLen] = y
yrcMutatorLock(T):
let oldLen = x.len
var xu = cast[ptr NimSeqV2[T]](addr x)
if xu.p == nil or (xu.p.cap and not strlitFlag) < oldLen+1:
xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, 1, sizeof(T), alignof(T)))
xu.len = oldLen+1
# .nodestroy means `xu.p.data[oldLen] = value` is compiled into a
# copyMem(). This is fine as know by construction that
# in `xu.p.data[oldLen]` there is nothing to destroy.
# We also save the `wasMoved + destroy` pair for the sink parameter.
xu.p.data[oldLen] = y
proc setLen[T](s: var seq[T], newlen: Natural) {.nodestroy.} =
{.noSideEffect.}:
if newlen < s.len:
shrink(s, newlen)
else:
let oldLen = s.len
if newlen <= oldLen: return
var xu = cast[ptr NimSeqV2[T]](addr s)
if xu.p == nil or (xu.p.cap and not strlitFlag) < newlen:
xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, newlen - oldLen, sizeof(T), alignof(T)))
xu.len = newlen
for i in oldLen..<newlen:
xu.p.data[i] = default(T)
yrcMutatorLock(T):
let oldLen = s.len
if newlen <= oldLen: return
var xu = cast[ptr NimSeqV2[T]](addr s)
if xu.p == nil or (xu.p.cap and not strlitFlag) < newlen:
xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, newlen - oldLen, sizeof(T), alignof(T)))
xu.len = newlen
for i in oldLen..<newlen:
xu.p.data[i] = default(T)
proc newSeq[T](s: var seq[T], len: Natural) =
shrink(s, 0)
@@ -214,11 +302,12 @@ func setLenUninit[T](s: var seq[T], newlen: Natural) {.nodestroy.} =
if newlen < s.len:
shrink(s, newlen)
else:
let oldLen = s.len
if newlen <= oldLen: return
var xu = cast[ptr NimSeqV2[T]](addr s)
if xu.p == nil or (xu.p.cap and not strlitFlag) < newlen:
xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, newlen - oldLen, sizeof(T), alignof(T)))
xu.len = newlen
yrcMutatorLock(T):
let oldLen = s.len
if newlen <= oldLen: return
var xu = cast[ptr NimSeqV2[T]](addr s)
if xu.p == nil or (xu.p.cap and not strlitFlag) < newlen:
xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, newlen - oldLen, sizeof(T), alignof(T)))
xu.len = newlen
{.pop.} # See https://github.com/nim-lang/Nim/issues/21401

View File

@@ -18,7 +18,8 @@ type
template frees(s: NimSeqV2Reimpl) =
if s.p != nil and (s.p.cap and strlitFlag) != strlitFlag:
when compileOption("threads"):
deallocShared(s.p)
else:
dealloc(s.p)
yrcMutatorLockUntyped:
when compileOption("threads"):
deallocShared(s.p)
else:
dealloc(s.p)

591
lib/system/yrc.nim Normal file
View File

@@ -0,0 +1,591 @@
#
# YRC: Thread-safe ORC (concurrent cycle collector).
# Same API as orc.nim but with the global mutator/collector RWLock for safety.
# Destructors for refs run at collection time, not immediately on last decRef.
# See yrc_proof.lean for a Lean 4 proof of safety and deadlock freedom.
#
# ## Locking Protocol
#
# ALL topology-changing operations — heap-field writes (`nimAsgnYrc`,
# `nimSinkYrc`) and seq mutations that resize internal buffers — hold the
# global mutator read lock (`gYrcGlobalLock` via `acquireMutatorLock`).
# Multiple mutators may hold this read lock simultaneously.
#
# The cycle collector acquires the exclusive write lock for the entire
# mark/scan/collect phase. This means the heap topology is *completely
# frozen* during collection: no `nimAsgnYrc` or seq operation can mutate
# any pointer field while the three passes run. This gives the Bacon
# algorithm the stable subgraph it requires without full write barriers.
#
# Consequence for incRef in `nimAsgnYrc`:
# Because the collector is blocked, the incRef can be a direct atomic
# increment on the RefHeader (`increment head(src)`) rather than going
# through the `toInc` stripe queue. The collector will see the updated
# RC immediately when it next acquires the write lock. Only decrements
# (`yrcDec`) still use the `toDec` stripe queue so that objects whose RC
# might reach zero are handled by the collector's cycle-detection logic.
#
# ## Why No Write Barrier Is Needed
#
# The classic concurrent-GC hazard is the "lost object" problem: during
# collection the mutator executes `A.field = B` where A is already scanned
# (black), B is reachable only through an unscanned (gray) object C, and then
# C's reference to B is removed. The collector never discovers B and frees it
# while A still points to it. Traditional concurrent collectors need write
# barriers to prevent this.
#
# This problem structurally cannot arise in YRC for two reasons:
#
# 1. The mutator lock freezes the topology during all three passes, so no
# concurrent field write can race with markGray/scan/collectWhite.
#
# 2. Even without the lock, the cycle collector only frees *closed cycles* —
# subgraphs where every reference to every member comes from within the
# group, with zero external references. To execute `A.field = B` the
# mutator must hold a reference to A (external ref), which `scan` would
# rescue. The two conditions are mutually exclusive.
#
# In practice reason (1) makes reason (2) a belt-and-suspenders safety
# argument rather than the primary mechanism.
{.push raises: [].}
include cellseqs_v2
import std/locks
const
NumStripes = 64
QueueSize = 128
RootsThreshold = 10
colBlack = 0b000
colGray = 0b001
colWhite = 0b010
maybeCycle = 0b100
inRootsFlag = 0b1000
colorMask = 0b011
logOrc = defined(nimArcIds)
type
TraceProc = proc (p, env: pointer) {.nimcall, gcsafe, raises: [].}
DisposeProc = proc (p: pointer) {.nimcall, gcsafe, raises: [].}
when defined(nimYrcAtomicIncs):
template color(c): untyped = atomicLoadN(addr c.rc, ATOMIC_ACQUIRE) and colorMask
template setColor(c, col) =
block:
var expected = atomicLoadN(addr c.rc, ATOMIC_RELAXED)
while true:
let desired = (expected and not colorMask) or col
if atomicCompareExchangeN(addr c.rc, addr expected, desired, true,
ATOMIC_ACQ_REL, ATOMIC_RELAXED):
break
template loadRc(c): int = atomicLoadN(addr c.rc, ATOMIC_ACQUIRE)
template trialDec(c) =
discard atomicFetchAdd(addr c.rc, -rcIncrement, ATOMIC_ACQ_REL)
template trialInc(c) =
discard atomicFetchAdd(addr c.rc, rcIncrement, ATOMIC_ACQ_REL)
template rcClearFlag(c, flag) =
block:
var expected = atomicLoadN(addr c.rc, ATOMIC_RELAXED)
while true:
let desired = expected and not flag
if atomicCompareExchangeN(addr c.rc, addr expected, desired, true,
ATOMIC_ACQ_REL, ATOMIC_RELAXED):
break
template rcSetFlag(c, flag) =
block:
var expected = atomicLoadN(addr c.rc, ATOMIC_RELAXED)
while true:
let desired = expected or flag
if atomicCompareExchangeN(addr c.rc, addr expected, desired, true,
ATOMIC_ACQ_REL, ATOMIC_RELAXED):
break
else:
template color(c): untyped = c.rc and colorMask
template setColor(c, col) =
when col == colBlack:
c.rc = c.rc and not colorMask
else:
c.rc = c.rc and not colorMask or col
template loadRc(c): int = c.rc
template trialDec(c) = c.rc = c.rc -% rcIncrement
template trialInc(c) = c.rc = c.rc +% rcIncrement
template rcClearFlag(c, flag) = c.rc = c.rc and not flag
template rcSetFlag(c, flag) = c.rc = c.rc or flag
const
optimizedOrc = false
useJumpStack = false
type
GcEnv = object
traceStack: CellSeq[ptr pointer]
when useJumpStack:
jumpStack: CellSeq[ptr pointer]
toFree: CellSeq[Cell]
freed, touched, edges, rcSum: int
keepThreshold: bool
proc trace(s: Cell; desc: PNimTypeV2; j: var GcEnv) {.inline.} =
if desc.traceImpl != nil:
var p = s +! sizeof(RefHeader)
cast[TraceProc](desc.traceImpl)(p, addr(j))
type
Stripe = object
when not defined(yrcAtomics):
lockInc: Lock
toIncLen: int
toInc: array[QueueSize, Cell]
lockDec: Lock
toDecLen: int
toDec: array[QueueSize, (Cell, PNimTypeV2)]
type
PreventThreadFromCollectProc* = proc(): bool {.nimcall, gcsafe, raises: [].}
## Callback run before this thread runs the cycle collector.
## Return `true` to allow collection, `false` to skip (e.g. real-time thread).
## Invoked while holding the global lock; must not call back into YRC.
var
roots: CellSeq[Cell] # merged roots, used under global lock
stripes: array[NumStripes, Stripe]
rootsThreshold: int = 128
defaultThreshold = when defined(nimFixedOrc): 10_000 else: 128
gPreventThreadFromCollectProc: PreventThreadFromCollectProc = nil
proc GC_setPreventThreadFromCollectProc*(cb: PreventThreadFromCollectProc) =
##[ Can be used to customize the cycle collector for a thread. For example,
to ensure that a hard realtime thread cannot run the cycle collector use:
```nim
var hardRealTimeThread: int
GC_setPreventThreadFromCollectProc(proc(): bool {.nimcall.} = hardRealTimeThread == getThreadId())
```
To ensure that a hard realtime thread cannot by involved in any cycle collector activity use:
```nim
GC_setPreventThreadFromCollectProc(proc(): bool {.nimcall.} =
if hardRealTimeThread == getThreadId():
writeStackTrace()
echo "Realtime thread involved in unpredictable cycle collector activity!"
result = false
)
```
]##
gPreventThreadFromCollectProc = cb
proc GC_getPreventThreadFromCollectProc*(): PreventThreadFromCollectProc =
## Returns the current "prevent thread from collecting proc".
## Typically `nil` if not set.
result = gPreventThreadFromCollectProc
proc mayRunCycleCollect(): bool {.inline.} =
if gPreventThreadFromCollectProc == nil: true
else: not gPreventThreadFromCollectProc()
proc getStripeIdx(): int {.inline.} =
getThreadId() and (NumStripes - 1)
proc nimIncRefCyclic(p: pointer; cyclic: bool) {.compilerRtl, inl.} =
let h = head(p)
when optimizedOrc:
if cyclic: h.rc = h.rc or maybeCycle
when defined(nimYrcAtomicIncs):
discard atomicFetchAdd(addr h.rc, rcIncrement, ATOMIC_ACQ_REL)
elif defined(yrcAtomics):
let s = getStripeIdx()
let slot = atomicFetchAdd(addr stripes[s].toIncLen, 1, ATOMIC_ACQ_REL)
if slot < QueueSize:
atomicStoreN(addr stripes[s].toInc[slot], h, ATOMIC_RELEASE)
else:
yrcCollectorLock:
h.rc = h.rc +% rcIncrement
for i in 0..<NumStripes:
let len = atomicExchangeN(addr stripes[i].toIncLen, 0, ATOMIC_ACQUIRE)
for j in 0..<min(len, QueueSize):
let x = atomicLoadN(addr stripes[i].toInc[j], ATOMIC_ACQUIRE)
x.rc = x.rc +% rcIncrement
else:
let idx = getStripeIdx()
while true:
var overflow = false
withLock stripes[idx].lockInc:
if stripes[idx].toIncLen < QueueSize:
stripes[idx].toInc[stripes[idx].toIncLen] = h
stripes[idx].toIncLen += 1
else:
overflow = true
if overflow:
yrcCollectorLock:
for i in 0..<NumStripes:
withLock stripes[i].lockInc:
for j in 0..<stripes[i].toIncLen:
let x = stripes[i].toInc[j]
x.rc = x.rc +% rcIncrement
stripes[i].toIncLen = 0
else:
break
proc mergePendingRoots() =
# Merge buffered RC operations. Note: Unlike truly concurrent collectors,
# we don't need to set color to black on incRef because collection runs
# under the global lock, so no concurrent mutations happen during collection.
for i in 0..<NumStripes:
when not defined(nimYrcAtomicIncs):
# Inc buffers only exist when increfs are buffered (not atomic)
when defined(yrcAtomics):
let incLen = atomicExchangeN(addr stripes[i].toIncLen, 0, ATOMIC_ACQUIRE)
for j in 0..<min(incLen, QueueSize):
let x = atomicLoadN(addr stripes[i].toInc[j], ATOMIC_ACQUIRE)
x.rc = x.rc +% rcIncrement
else:
withLock stripes[i].lockInc:
for j in 0..<stripes[i].toIncLen:
let x = stripes[i].toInc[j]
x.rc = x.rc +% rcIncrement
stripes[i].toIncLen = 0
withLock stripes[i].lockDec:
for j in 0..<stripes[i].toDecLen:
let (c, desc) = stripes[i].toDec[j]
trialDec(c)
if (loadRc(c) and inRootsFlag) == 0:
rcSetFlag(c, inRootsFlag)
if roots.d == nil: init(roots)
add(roots, c, desc)
stripes[i].toDecLen = 0
proc collectCycles()
when logOrc or orcLeakDetector:
proc writeCell(msg: cstring; s: Cell; desc: PNimTypeV2) =
when orcLeakDetector:
cfprintf(cstderr, "%s %s file: %s:%ld; color: %ld; thread: %ld\n",
msg, if desc != nil: desc.name else: cstring"(nil)", s.filename, s.line, s.color, getThreadId())
else:
# Guard nil desc/desc.name. Use cell pointer as id to avoid uninitialized s.refId (roots may have refId unset)
let name = if desc != nil and desc.name != nil: desc.name else: cstring"(null)"
cfprintf(cstderr, "%s %s %p isroot: %s; RC: %ld; color: %ld; thread: %ld\n",
msg, name, s, (if (s.rc and inRootsFlag) != 0: "yes" else: "no"), s.rc shr rcShift, s.color, getThreadId())
proc free(s: Cell; desc: PNimTypeV2) {.inline.} =
when traceCollector:
cprintf("[From ] %p rc %ld color %ld\n", s, loadRc(s) shr rcShift, s.color)
if (loadRc(s) and inRootsFlag) == 0:
let p = s +! sizeof(RefHeader)
when logOrc: writeCell("free", s, desc)
if desc.destructor != nil:
cast[DestructorProc](desc.destructor)(p)
nimRawDispose(p, desc.align)
template orcAssert(cond, msg) =
when logOrc:
if not cond:
cfprintf(cstderr, "[Bug!] %s\n", msg)
rawQuit 1
proc nimTraceRef(q: pointer; desc: PNimTypeV2; env: pointer) {.compilerRtl, inl.} =
let p = cast[ptr pointer](q)
if p[] != nil:
var j = cast[ptr GcEnv](env)
j.traceStack.add(p, desc)
proc nimTraceRefDyn(q: pointer; env: pointer) {.compilerRtl, inl.} =
let p = cast[ptr pointer](q)
if p[] != nil:
var j = cast[ptr GcEnv](env)
j.traceStack.add(p, cast[ptr PNimTypeV2](p[])[])
proc scanBlack(s: Cell; desc: PNimTypeV2; j: var GcEnv) =
s.setColor colBlack
let until = j.traceStack.len
trace(s, desc, j)
when logOrc: writeCell("root still alive", s, desc)
while j.traceStack.len > until:
let (entry, desc) = j.traceStack.pop()
let t = head entry[]
trialInc(t)
if t.color != colBlack:
t.setColor colBlack
trace(t, desc, j)
when logOrc: writeCell("child still alive", t, desc)
proc markGray(s: Cell; desc: PNimTypeV2; j: var GcEnv) =
if s.color != colGray:
s.setColor colGray
j.touched = j.touched +% 1
j.rcSum = j.rcSum +% (loadRc(s) shr rcShift) +% 1
orcAssert(j.traceStack.len == 0, "markGray: trace stack not empty")
trace(s, desc, j)
while j.traceStack.len > 0:
let (entry, desc) = j.traceStack.pop()
let t = head entry[]
trialDec(t)
j.edges = j.edges +% 1
if t.color != colGray:
t.setColor colGray
j.touched = j.touched +% 1
j.rcSum = j.rcSum +% (loadRc(t) shr rcShift) +% 2
trace(t, desc, j)
proc scan(s: Cell; desc: PNimTypeV2; j: var GcEnv) =
if s.color == colGray:
if (loadRc(s) shr rcShift) >= 0:
scanBlack(s, desc, j)
else:
orcAssert(j.traceStack.len == 0, "scan: trace stack not empty")
s.setColor(colWhite)
trace(s, desc, j)
while j.traceStack.len > 0:
let (entry, desc) = j.traceStack.pop()
let t = head entry[]
if t.color == colGray:
if (loadRc(t) shr rcShift) >= 0:
scanBlack(t, desc, j)
else:
t.setColor(colWhite)
trace(t, desc, j)
proc collectColor(s: Cell; desc: PNimTypeV2; col: int; j: var GcEnv) =
if s.color == col and (loadRc(s) and inRootsFlag) == 0:
orcAssert(j.traceStack.len == 0, "collectWhite: trace stack not empty")
s.setColor(colBlack)
j.toFree.add(s, desc)
trace(s, desc, j)
while j.traceStack.len > 0:
let (entry, desc) = j.traceStack.pop()
let t = head entry[]
entry[] = nil
if t.color == col and (loadRc(t) and inRootsFlag) == 0:
j.toFree.add(t, desc)
t.setColor(colBlack)
trace(t, desc, j)
proc collectCyclesBacon(j: var GcEnv; lowMark: int) =
# YRC defers all destruction to collection time - process ALL roots through Bacon's algorithm
# This is different from ORC which handles immediate garbage (rc == 0) directly
if lockState == Collecting:
return
lockState = Collecting
let last = roots.len -% 1
when logOrc:
for i in countdown(last, lowMark):
writeCell("root", roots.d[i][0], roots.d[i][1])
# Process all roots through markGray (Bacon's algorithm)
for i in countdown(last, lowMark):
markGray(roots.d[i][0], roots.d[i][1], j)
var colToCollect = colWhite
if j.rcSum == j.edges:
# Short-cut: we know everything is garbage
colToCollect = colGray
j.keepThreshold = true
else:
# Normal scan phase
for i in countdown(last, lowMark):
scan(roots.d[i][0], roots.d[i][1], j)
# Collect phase: free all garbage objects
init j.toFree
for i in 0 ..< roots.len:
let s = roots.d[i][0]
rcClearFlag(s, inRootsFlag)
collectColor(s, roots.d[i][1], colToCollect, j)
# Clear roots before freeing to prevent nested collectCycles() from accessing freed cells
roots.len = 0
# Free all collected objects
# Destructors must not call nimDecRefIsLastCyclicStatic (add to toDec) during this phase
for i in 0 ..< j.toFree.len:
let s = j.toFree.d[i][0]
when orcLeakDetector:
writeCell("CYCLIC OBJECT FREED", s, j.toFree.d[i][1])
free(s, j.toFree.d[i][1])
j.freed = j.freed +% j.toFree.len
deinit j.toFree
when defined(nimOrcStats):
var freedCyclicObjects {.threadvar.}: int
proc collectCycles() =
when logOrc:
cfprintf(cstderr, "[collectCycles] begin\n")
yrcCollectorLock:
mergePendingRoots()
if roots.len >= rootsThreshold and mayRunCycleCollect():
let nRoots = roots.len
var j: GcEnv
init j.traceStack
collectCyclesBacon(j, 0)
if roots.len == 0 and roots.d != nil:
deinit roots
when not defined(nimStressOrc):
if j.keepThreshold:
discard
elif j.freed *% 2 >= j.touched:
when not defined(nimFixedOrc):
rootsThreshold = max(rootsThreshold div 3 *% 2, 16)
else:
rootsThreshold = 0
elif rootsThreshold < high(int) div 4:
rootsThreshold = (if rootsThreshold <= 0: defaultThreshold else: rootsThreshold)
rootsThreshold = rootsThreshold div 2 +% rootsThreshold
# Cost-aware: if this run was expensive (large graph), raise threshold more so we don't run again too soon
if j.touched > nRoots *% 4:
rootsThreshold = rootsThreshold div 2 +% rootsThreshold
rootsThreshold = min(rootsThreshold, defaultThreshold *% 16)
rootsThreshold = min(rootsThreshold, nRoots *% 2)
when logOrc:
cfprintf(cstderr, "[collectCycles] end; freed %ld new threshold %ld\n", j.freed, rootsThreshold)
when defined(nimOrcStats):
inc freedCyclicObjects, j.freed
deinit j.traceStack
when defined(nimOrcStats):
type
OrcStats* = object
freedCyclicObjects*: int
proc GC_orcStats*(): OrcStats =
result = OrcStats(freedCyclicObjects: freedCyclicObjects)
proc GC_runOrc* =
yrcCollectorLock:
mergePendingRoots()
if roots.len > 0 and mayRunCycleCollect():
var j: GcEnv
init j.traceStack
collectCyclesBacon(j, 0)
deinit j.traceStack
roots.len = 0
when logOrc: orcAssert roots.len == 0, "roots not empty!"
proc GC_enableOrc*() =
when not defined(nimStressOrc):
rootsThreshold = 0
proc GC_disableOrc*() =
when not defined(nimStressOrc):
rootsThreshold = high(int)
proc GC_prepareOrc*(): int {.inline.} =
yrcCollectorLock:
mergePendingRoots()
result = roots.len
proc GC_partialCollect*(limit: int) =
yrcCollectorLock:
mergePendingRoots()
if roots.len > limit and mayRunCycleCollect():
var j: GcEnv
init j.traceStack
collectCyclesBacon(j, limit)
deinit j.traceStack
roots.len = limit
proc GC_fullCollect* =
GC_runOrc()
proc GC_enableMarkAndSweep*() = GC_enableOrc()
proc GC_disableMarkAndSweep*() = GC_disableOrc()
const acyclicFlag = 1
when optimizedOrc:
template markedAsCyclic(s: Cell; desc: PNimTypeV2): bool =
(desc.flags and acyclicFlag) == 0 and (s.rc and maybeCycle) != 0
else:
template markedAsCyclic(s: Cell; desc: PNimTypeV2): bool =
(desc.flags and acyclicFlag) == 0
proc nimDecRefIsLastCyclicDyn(p: pointer): bool {.compilerRtl, inl.} =
result = false
if p != nil:
let cell = head(p)
let desc = cast[ptr PNimTypeV2](p)[]
let idx = getStripeIdx()
while true:
var overflow = false
withLock stripes[idx].lockDec:
if stripes[idx].toDecLen < QueueSize:
stripes[idx].toDec[stripes[idx].toDecLen] = (cell, desc)
stripes[idx].toDecLen += 1
else:
overflow = true
if overflow:
collectCycles()
else:
break
proc nimDecRefIsLastDyn(p: pointer): bool {.compilerRtl, inl.} =
nimDecRefIsLastCyclicDyn(p)
proc nimDecRefIsLastCyclicStatic(p: pointer; desc: PNimTypeV2): bool {.compilerRtl, inl.} =
result = false
if p != nil:
let cell = head(p)
let idx = getStripeIdx()
while true:
var overflow = false
withLock stripes[idx].lockDec:
if stripes[idx].toDecLen < QueueSize:
stripes[idx].toDec[stripes[idx].toDecLen] = (cell, desc)
stripes[idx].toDecLen += 1
else:
overflow = true
if overflow:
collectCycles()
else:
break
proc unsureAsgnRef(dest: ptr pointer, src: pointer) {.inline.} =
dest[] = src
if src != nil: nimIncRefCyclic(src, true)
proc yrcDec(tmp: pointer; desc: PNimTypeV2) {.inline.} =
if desc != nil:
discard nimDecRefIsLastCyclicStatic(tmp, desc)
else:
discard nimDecRefIsLastCyclicDyn(tmp)
proc nimAsgnYrc(dest: ptr pointer; src: pointer; desc: PNimTypeV2) {.compilerRtl.} =
## YRC write barrier for ref copy assignment.
## Holds the mutator read lock for the entire operation so the collector
## cannot run between the incRef and decRef, closing the stale-decRef
## bug. Direct atomic incRef replaces the toInc stripe queue: the
## collector is blocked, so the RC update is immediately visible and correct.
acquireMutatorLock()
if src != nil: increment head(src) # direct atomic: no toInc queue needed
let tmp = dest[]
dest[] = src
if tmp != nil: yrcDec(tmp, desc) # still deferred via toDec for cycle detection
releaseMutatorLock()
proc nimSinkYrc(dest: ptr pointer; src: pointer; desc: PNimTypeV2) {.compilerRtl.} =
## YRC write barrier for ref sink (move). No incRef on source.
acquireMutatorLock()
let tmp = dest[]
dest[] = src
if tmp != nil: yrcDec(tmp, desc)
releaseMutatorLock()
proc nimMarkCyclic(p: pointer) {.compilerRtl, inl.} =
when optimizedOrc:
if p != nil:
let h = head(p)
h.rc = h.rc or maybeCycle
# Initialize locks at module load.
# RwLock stripes live in seqs_v2 (gYrcLocks); NumLockStripes is exported from there.
for i in 0..<NumLockStripes:
initRwLock(gYrcLocks[i].lock)
for i in 0..<NumStripes:
when not defined(yrcAtomics) and not defined(nimYrcAtomicIncs):
initLock(stripes[i].lockInc)
initLock(stripes[i].lockDec)
{.pop.}

353
lib/system/yrc_proof.lean Normal file
View File

@@ -0,0 +1,353 @@
/-
YRC Safety Proof (self-contained, no Mathlib)
==============================================
Formal model of YRC's key invariant: the cycle collector never frees
an object that any mutator thread can reach.
## Model overview
We model the heap as a set of objects with directed edges (ref fields).
Each thread owns a set of *stack roots* — objects reachable from local variables.
The write barrier (nimAsgnYrc) does:
1. atomic store dest ← src (graph is immediately current)
2. buffer inc(src) (deferred)
3. buffer dec(old) (deferred)
The collector (under global lock) does:
1. Merge all buffered inc/dec into merged RCs
2. Trial deletion (markGray): subtract internal edges from merged RCs
3. scan: objects with RC ≥ 0 after trial deletion are rescued (scanBlack)
4. Free objects that remain white (closed cycles with zero external refs)
-/
-- Objects and threads are just natural numbers for simplicity.
abbrev Obj := Nat
abbrev Thread := Nat
/-! ### State -/
/-- The state of the heap and collector at a point in time. -/
structure State where
/-- Physical heap edges: `edges x y` means object `x` has a ref field pointing to `y`.
Always up-to-date (atomic stores). -/
edges : Obj Obj Prop
/-- Stack roots per thread. `roots t x` means thread `t` has a local variable pointing to `x`. -/
roots : Thread Obj Prop
/-- Pending buffered increments (not yet merged). -/
pendingInc : Obj Nat
/-- Pending buffered decrements (not yet merged). -/
pendingDec : Obj Nat
/-! ### Reachability -/
/-- An object is *reachable* if some thread can reach it via stack roots + heap edges. -/
inductive Reachable (s : State) : Obj Prop where
| root (t : Thread) (x : Obj) : s.roots t x Reachable s x
| step (x y : Obj) : Reachable s x s.edges x y Reachable s y
/-- Directed reachability between heap objects (following physical edges only). -/
inductive HeapReachable (s : State) : Obj Obj Prop where
| refl (x : Obj) : HeapReachable s x x
| step (x y z : Obj) : HeapReachable s x y s.edges y z HeapReachable s x z
/-- If a root reaches `r` and `r` heap-reaches `x`, then `x` is Reachable. -/
theorem heapReachable_of_reachable (s : State) (r x : Obj)
(hr : Reachable s r) (hp : HeapReachable s r x) :
Reachable s x := by
induction hp with
| refl => exact hr
| step _ _ _ hedge ih => exact Reachable.step _ _ ih hedge
/-! ### What the collector frees -/
/-- An object has an *external reference* if some thread's stack roots point to it. -/
def hasExternalRef (s : State) (x : Obj) : Prop :=
t, s.roots t x
/-- An object is *externally anchored* if it is heap-reachable from some
object that has an external reference. This is what scanBlack computes:
it starts from objects with trialRC ≥ 0 (= has external refs) and traces
the current physical graph. -/
def anchored (s : State) (x : Obj) : Prop :=
r, hasExternalRef s r HeapReachable s r x
/-- The collector frees `x` only if `x` is *not anchored*:
no external ref, and not reachable from any externally-referenced object.
This models: after trial deletion, x remained white, and scanBlack
didn't rescue it. -/
def collectorFrees (s : State) (x : Obj) : Prop :=
¬ anchored s x
/-! ### Main safety theorem -/
/-- **Lemma**: Every reachable object is anchored.
If thread `t` reaches `x`, then there is a chain from a stack root
(which has an external ref) through heap edges to `x`. -/
theorem reachable_is_anchored (s : State) (x : Obj)
(h : Reachable s x) : anchored s x := by
induction h with
| root t x hroot =>
exact x, t, hroot, HeapReachable.refl x
| step a b h_reach_a h_edge ih =>
obtain r, h_ext_r, h_path_r_a := ih
exact r, h_ext_r, HeapReachable.step r a b h_path_r_a h_edge
/-- **Main Safety Theorem**: If the collector frees `x`, then no thread
can reach `x`. Freed objects are unreachable.
This is the contrapositive of `reachable_is_anchored`. -/
theorem yrc_safety (s : State) (x : Obj)
(h_freed : collectorFrees s x) : ¬ Reachable s x := by
intro h_reach
exact h_freed (reachable_is_anchored s x h_reach)
/-! ### The write barrier preserves reachability -/
/-- Model of `nimAsgnYrc(dest_field_of_a, src)`:
Object `a` had a field pointing to `old`, now points to `src`.
Graph update is immediate. The new edge takes priority (handles src = old). -/
def writeBarrier (s : State) (a old src : Obj) : State :=
{ s with
edges := fun x y =>
if x = a y = src then True
else if x = a y = old then False
else s.edges x y
pendingInc := fun x => if x = src then s.pendingInc x + 1 else s.pendingInc x
pendingDec := fun x => if x = old then s.pendingDec x + 1 else s.pendingDec x }
/-- **No Lost Object Theorem**: If thread `t` holds a stack ref to `a` and
executes `a.field = b` (replacing old), then `b` is reachable afterward.
This is why the "lost object" problem from concurrent GC literature
doesn't arise in YRC: the atomic store makes `a→b` visible immediately,
and `a` is anchored (thread `t` holds it), so scanBlack traces `a→b`
and rescues `b`. -/
theorem no_lost_object (s : State) (t : Thread) (a old b : Obj)
(h_root_a : s.roots t a) :
Reachable (writeBarrier s a old b) b := by
apply Reachable.step a b
· exact Reachable.root t a h_root_a
· simp [writeBarrier]
/-! ### Non-atomic write barrier window safety
The write barrier does three steps non-atomically:
1. atomicStore(dest, src) — graph update
2. buffer inc(src) — deferred
3. buffer dec(old) — deferred
If the collector runs between steps 1 and 2 (inc not yet buffered):
- src has a new incoming heap edge not yet reflected in RCs
- But src is reachable from the mutator's stack (mutator held a ref to store it)
- So src has an external ref → trialRC ≥ 1 → scanBlack rescues src ✓
If the collector runs between steps 2 and 3 (dec not yet buffered):
- old's RC is inflated by 1 (the dec hasn't arrived)
- This is conservative: old appears to have more refs than it does
- Trial deletion won't spuriously free it ✓
-/
/-- Model the state between steps 1-2: graph updated, inc not yet buffered.
`src` has new edge but RC doesn't reflect it yet. -/
def stateAfterStore (s : State) (a old src : Obj) : State :=
{ s with
edges := fun x y =>
if x = a y = src then True
else if x = a y = old then False
else s.edges x y }
/-- Even in the window between atomic store and buffered inc,
src is still reachable (from the mutator's stack via a→src). -/
theorem src_reachable_in_window (s : State) (t : Thread) (a old src : Obj)
(h_root_a : s.roots t a) :
Reachable (stateAfterStore s a old src) src := by
apply Reachable.step a src
· exact Reachable.root t a h_root_a
· simp [stateAfterStore]
/-- Therefore src is anchored in the window → collector won't free it. -/
theorem src_safe_in_window (s : State) (t : Thread) (a old src : Obj)
(h_root_a : s.roots t a) :
¬ collectorFrees (stateAfterStore s a old src) src := by
intro h_freed
exact h_freed (reachable_is_anchored _ _ (src_reachable_in_window s t a old src h_root_a))
/-! ### Deadlock freedom
YRC uses three classes of locks:
• gYrcGlobalLock (level 0)
• stripes[i].lockInc (level 2*i + 1, for i in 0..N-1)
• stripes[i].lockDec (level 2*i + 2, for i in 0..N-1)
Total order: global < lockInc[0] < lockDec[0] < lockInc[1] < lockDec[1] < ...
Every code path in yrc.nim acquires locks in strictly ascending level order:
**nimIncRefCyclic** (mutator fast path):
acquire lockInc[myStripe] → release → done.
Holds exactly one lock. ✓
**nimIncRefCyclic** (overflow path):
acquire gYrcGlobalLock (level 0), then for i=0..N-1: acquire lockInc[i] → release.
Ascending: 0 < 1 < 3 < 5 < ... ✓
**nimDecRefIsLastCyclic{Dyn,Static}** (fast path):
acquire lockDec[myStripe] → release → done.
Holds exactly one lock. ✓
**nimDecRefIsLastCyclic{Dyn,Static}** (overflow path):
calls collectCycles → acquire gYrcGlobalLock (level 0),
then mergePendingRoots which for i=0..N-1:
acquire lockInc[i] → release, acquire lockDec[i] → release.
Ascending: 0 < 1 < 2 < 3 < 4 < ... ✓
**collectCycles / GC_runOrc** (collector):
acquire gYrcGlobalLock (level 0),
then mergePendingRoots (same ascending pattern as above). ✓
**nimAsgnYrc / nimSinkYrc** (write barrier):
Calls nimIncRefCyclic then nimDecRefIsLastCyclic*.
Each call acquires and releases its lock independently.
No nesting between the two calls. ✓
Since every path follows the total order, deadlock is impossible.
-/
/-- Lock levels in YRC. Each lock maps to a unique natural number. -/
inductive LockId (n : Nat) where
| global : LockId n
| lockInc (i : Nat) (h : i < n) : LockId n
| lockDec (i : Nat) (h : i < n) : LockId n
/-- The level (priority) of each lock in the total order. -/
def lockLevel {n : Nat} : LockId n Nat
| .global => 0
| .lockInc i _ => 2 * i + 1
| .lockDec i _ => 2 * i + 2
/-- All lock levels are distinct (the level function is injective). -/
theorem lockLevel_injective {n : Nat} (a b : LockId n)
(h : lockLevel a = lockLevel b) : a = b := by
cases a with
| global =>
cases b with
| global => rfl
| lockInc j hj => simp [lockLevel] at h
| lockDec j hj => simp [lockLevel] at h
| lockInc i hi =>
cases b with
| global => simp [lockLevel] at h
| lockInc j hj =>
have : i = j := by simp [lockLevel] at h; omega
subst this; rfl
| lockDec j hj => simp [lockLevel] at h; omega
| lockDec i hi =>
cases b with
| global => simp [lockLevel] at h
| lockInc j hj => simp [lockLevel] at h; omega
| lockDec j hj =>
have : i = j := by simp [lockLevel] at h; omega
subst this; rfl
/-- Helper: stripe lock levels are strictly ascending across stripes. -/
theorem stripe_levels_ascending (i : Nat) :
2 * i + 1 < 2 * i + 2 2 * i + 2 < 2 * (i + 1) + 1 := by
constructor <;> omega
/-- lockInc levels are strictly ascending with index. -/
theorem lockInc_level_strict_mono {n : Nat} (i j : Nat) (hi : i < n) (hj : j < n)
(hij : i < j) : lockLevel (.lockInc i hi : LockId n) < lockLevel (.lockInc j hj) := by
simp [lockLevel]; omega
/-- lockDec levels are strictly ascending with index. -/
theorem lockDec_level_strict_mono {n : Nat} (i j : Nat) (hi : i < n) (hj : j < n)
(hij : i < j) : lockLevel (.lockDec i hi : LockId n) < lockLevel (.lockDec j hj) := by
simp [lockLevel]; omega
/-- Global lock has the lowest level (level 0). -/
theorem global_level_min {n : Nat} (l : LockId n) (h : l .global) :
lockLevel (.global : LockId n) < lockLevel l := by
cases l with
| global => exact absurd rfl h
| lockInc i hi => simp [lockLevel]
| lockDec i hi => simp [lockLevel]
/-- **Deadlock Freedom**: Any sequence of lock acquisitions that follows the
"acquire in ascending level order" discipline cannot deadlock.
This is a standard result: a total order on locks with the invariant that
every thread acquires locks in strictly ascending order prevents cycles
in the wait-for graph, which is necessary and sufficient for deadlock.
We prove the 2-thread case (the general N-thread case follows by the
same transitivity argument on the wait-for cycle). -/
theorem no_deadlock_from_total_order {n : Nat}
-- Two threads each hold a lock and wait for another
(held₁ waited₁ held₂ waited₂ : LockId n)
-- Thread 1 holds held₁ and wants waited₁ (ascending order)
(h1 : lockLevel held₁ < lockLevel waited₁)
-- Thread 2 holds held₂ and wants waited₂ (ascending order)
(h2 : lockLevel held₂ < lockLevel waited₂)
-- Deadlock requires: thread 1 waits for what thread 2 holds,
-- and thread 2 waits for what thread 1 holds
(h_wait1 : waited₁ = held₂)
(h_wait2 : waited₂ = held₁) :
False := by
subst h_wait1; subst h_wait2
omega
/-! ### Summary of verified properties (all QED, no sorry)
1. `reachable_is_anchored`: Every reachable object is anchored
(has a path from an externally-referenced object via heap edges).
2. `yrc_safety`: The collector only frees unanchored objects,
which are unreachable by all threads. **No use-after-free.**
3. `no_lost_object`: After `a.field = b`, `b` is reachable
(atomic store makes the edge visible immediately).
4. `src_safe_in_window`: Even between the atomic store and
the buffered inc, the collector cannot free src.
5. `lockLevel_injective`: All lock levels are distinct (well-defined total order).
6. `global_level_min`: The global lock has the lowest level.
7. `lockInc_level_strict_mono`, `lockDec_level_strict_mono`:
Stripe locks are strictly ordered by index.
8. `no_deadlock_from_total_order`: A 2-thread deadlock cycle is impossible
when both threads acquire locks in ascending level order.
Together these establish that YRC's write barrier protocol
(atomic store → buffer inc → buffer dec) is safe under concurrent
collection, and the locking discipline prevents deadlock.
## What is NOT proved: Completeness (liveness)
This proof covers **safety** (no use-after-free) and **deadlock-freedom**,
but does NOT prove **completeness** — that all garbage cycles are eventually
collected.
Completeness depends on the trial deletion algorithm (Bacon 2001) correctly
identifying closed cycles. Specifically it requires proving:
1. After `mergePendingRoots`, merged RCs equal logical RCs
(buffered inc/dec exactly compensate graph changes since last merge).
2. `markGray` subtracts exactly the internal (heap→heap) edge count from
each node's merged RC, yielding `trialRC(x) = externalRefCount(x)`.
3. `scan` correctly partitions: nodes with `trialRC ≥ 0` are rescued by
`scanBlack`; nodes with `trialRC < 0` remain white.
4. White nodes form closed subgraphs with zero external refs → garbage.
These properties follow from the well-known Bacon trial-deletion algorithm
and are assumed here rather than re-proved. The YRC-specific contribution
(buffered RCs, striped queues, concurrent mutators) is what our safety
proof covers — showing that concurrency does not break the preconditions
that trial deletion relies on (physical graph consistency, eventual RC
consistency after merge).
Reference: D.F. Bacon and V.T. Rajan, "Concurrent Cycle Collection in
Reference Counted Systems", ECOOP 2001.
-/

953
lib/system/yrc_proof.tla Normal file
View File

@@ -0,0 +1,953 @@
---- MODULE yrc_proof ----
\* TLA+ specification of YRC (Thread-safe ORC cycle collector)
\* Models the fine details of barriers, striped queues, and synchronization
\*
\* ## Key Barrier Semantics Modeled
\*
\* ### Write Barrier (nimAsgnYrc)
\* 1. atomicStoreN(dest, src, ATOMIC_RELEASE)
\* - Graph update is immediately visible to all threads (including collector)
\* - ATOMIC_RELEASE ensures all prior writes are visible before this store
\* - No lock required for graph updates (lock-free)
\*
\* 2. nimIncRefCyclic(src, true)
\* - Acquires per-stripe lockInc[stripe] (fine-grained)
\* - Buffers increment in toInc[stripe] queue
\* - On overflow: acquires global lock, merges all stripes, applies increment
\*
\* 3. yrcDec(tmp, desc)
\* - Acquires per-stripe lockDec[stripe] (fine-grained)
\* - Buffers decrement in toDec[stripe] queue
\* - On overflow: acquires global lock, merges all stripes, applies decrement,
\* adds to roots array if not already present
\*
\* ### Merge Operation (mergePendingRoots)
\* - Acquires global lock (exclusive access)
\* - Sequentially acquires each stripe's lockInc and lockDec
\* - Drains all buffers, applies RC adjustments
\* - Adds decremented objects to roots array
\* - After merge: mergedRC = logicalRC (current graph state)
\*
\* ### Collection Cycle (under global lock)
\* 1. mergePendingRoots: reconcile buffered changes
\* 2. markGray: trial deletion (subtract internal edges)
\* 3. scan: rescue objects with RC >= 0 (scanBlack follows current graph)
\* 4. collectColor: free white objects (closed cycles)
\*
\* ## Safety Argument
\*
\* The collector only frees closed cycles (zero external refs). Concurrent writes
\* cannot cause "lost objects" because:
\* - Graph updates are atomic and immediately visible
\* - Mutator must hold stack ref to modify object (external ref)
\* - scanBlack follows current physical edges (rescues newly written objects)
\* - Only objects unreachable from any stack root are freed
\*
\* ## Seq Payload Race and RWLock Fix
\*
\* Value types like seq[T] (where T can form cycles) have internal heap
\* allocations (data arrays / "payloads") that are freed by value-type
\* hooks (=sink, =destroy), NOT by the cycle collector. This creates a race:
\*
\* 1. Object O has a seq field with payload P containing refs
\* 2. Collector starts tracing O -- reads payload pointer P
\* 3. Mutator does O.seq = newSeq -- frees P (value-type destructor)
\* 4. Collector dereferences P -- use-after-free!
\*
\* Fix: Change the global YRC lock to a read-write lock (RWLock).
\* - Collector acquires the WRITE lock (exclusive access during tracing)
\* - Seq mutations (assign, setLen, add, etc.) acquire the READ lock
\* - Multiple seq mutations can proceed concurrently (read lock is shared)
\* - But seq mutations block while the collector traces (write lock is exclusive)
\*
\* This prevents the race: the mutator cannot free a payload while the
\* collector is tracing it, because acquiring the read lock requires
\* the write lock to be unheld.
\*
\* Deadlock avoidance: If a seq operation triggers collectCycles() via stripe
\* overflow while already holding the read lock, it must NOT attempt to
\* acquire the write lock. Instead, it should drain the overflow buffers
\* without running the full collection cycle.
EXTENDS Naturals, Integers, Sequences, FiniteSets, TLC
CONSTANTS NumStripes, QueueSize, RootsThreshold, Objects, Threads, ObjTypes
ASSUME NumStripes \in Nat /\ NumStripes > 0
ASSUME QueueSize \in Nat /\ QueueSize > 0
ASSUME RootsThreshold \in Nat
ASSUME IsFiniteSet(Objects)
ASSUME IsFiniteSet(Threads)
ASSUME IsFiniteSet(ObjTypes)
\* Seq payload identifiers (models heap-allocated data arrays of seq[T])
CONSTANTS SeqPayloads
ASSUME IsFiniteSet(SeqPayloads)
\* NULL constant (represents "no thread" for locks)
\* We use a sentinel value that's guaranteed not to be in Threads or Objects
NULL == "NULL" \* String literal that won't conflict with Threads/Objects
ASSUME NULL \notin Threads /\ NULL \notin Objects /\ NULL \notin SeqPayloads
\* Helper functions
\* Note: GetStripeIdx is not used, GetStripe is used instead
\* Color constants
colBlack == 0
colGray == 1
colWhite == 2
maybeCycle == 4
inRootsFlag == 8
colorMask == 3
\* State variables
VARIABLES
\* Physical heap graph (always up-to-date, atomic stores)
edges, \* edges[obj1][obj2] = TRUE if obj1.field points to obj2
\* Stack roots per thread
roots, \* roots[thread][obj] = TRUE if thread has local var pointing to obj
\* Reference counts (stored in object header)
rc, \* rc[obj] = reference count (logical, after merge)
\* Color markers (stored in object header, bits 0-2)
color, \* color[obj] \in {colBlack, colGray, colWhite}
\* Root tracking flags
inRoots, \* inRoots[obj] = TRUE if obj is in roots array
\* Striped increment queues
toIncLen, \* toIncLen[stripe] = current length of increment queue
toInc, \* toInc[stripe][i] = object to increment
\* Striped decrement queues
toDecLen, \* toDecLen[stripe] = current length of decrement queue
toDec, \* toDec[stripe][i] = (object, type) pair to decrement
\* Per-stripe locks
lockInc, \* lockInc[stripe] = thread holding increment lock (or NULL)
lockDec, \* lockDec[stripe] = thread holding decrement lock (or NULL)
\* Global lock (now the WRITE side of the RWLock)
globalLock, \* thread holding write lock (or NULL)
\* Merged roots array (used during collection)
mergedRoots, \* sequence of (object, type) pairs
\* Collection state
collecting, \* TRUE if collection is in progress
gcEnv, \* GC environment: {touched, edges, rcSum, toFree, ...}
\* Pending operations (for modeling atomicity)
pendingWrites, \* set of pending write barrier operations
\* --- Seq payload race modeling ---
\* Seq payloads: models the heap-allocated data arrays of seq[T] fields
seqData, \* [Objects -> SeqPayloads \cup {NULL}] -- current payload for obj's seq
payloadAlive, \* [SeqPayloads -> BOOLEAN] -- is this payload's memory valid?
\* RWLock read side: set of threads holding the read lock.
\* Seq mutations (assign, add, setLen, etc.) acquire the read lock.
\* The collector (write lock holder) gets exclusive access.
rwLockReaders, \* SUBSET Threads -- threads currently holding the read lock
\* Collector's in-progress seq trace: the payload pointer read during tracing.
\* Between reading the pointer and accessing the data, the payload could be freed.
collectorPayload \* SeqPayloads \cup {NULL} -- payload being traced by collector
\* Convenience tuple for seq-related variables (used in UNCHANGED clauses)
seqVars == <<seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* Type invariants
TypeOK ==
/\ edges \in [Objects -> [Objects -> BOOLEAN]]
/\ roots \in [Threads -> [Objects -> BOOLEAN]]
/\ rc \in [Objects -> Int]
/\ color \in [Objects -> {colBlack, colGray, colWhite}]
/\ inRoots \in [Objects -> BOOLEAN]
/\ toIncLen \in [0..(NumStripes-1) -> 0..QueueSize]
/\ toInc \in [0..(NumStripes-1) -> Seq(Objects)]
/\ toDecLen \in [0..(NumStripes-1) -> 0..QueueSize]
/\ toDec \in [0..(NumStripes-1) -> Seq([obj: Objects, desc: ObjTypes])]
/\ lockInc \in [0..(NumStripes-1) -> Threads \cup {NULL}]
/\ lockDec \in [0..(NumStripes-1) -> Threads \cup {NULL}]
/\ globalLock \in Threads \cup {NULL}
/\ mergedRoots \in Seq([obj: Objects, desc: ObjTypes])
/\ collecting \in BOOLEAN
/\ pendingWrites \in SUBSET ([thread: Threads, dest: Objects, old: Objects \cup {NULL}, src: Objects \cup {NULL}, phase: {"store", "inc", "dec"}])
\* Seq payload types
/\ seqData \in [Objects -> SeqPayloads \cup {NULL}]
/\ payloadAlive \in [SeqPayloads -> BOOLEAN]
/\ rwLockReaders \in SUBSET Threads
/\ collectorPayload \in SeqPayloads \cup {NULL}
\* Helper: internal reference count (heap-to-heap edges)
InternalRC(obj) ==
Cardinality({src \in Objects : edges[src][obj]})
\* Helper: external reference count (stack roots)
ExternalRC(obj) ==
Cardinality({t \in Threads : roots[t][obj]})
\* Helper: logical reference count
LogicalRC(obj) ==
InternalRC(obj) + ExternalRC(obj)
\* Helper: get stripe index for thread
\* Map threads to stripe indices deterministically
\* Since threads are ModelValues, we use a simple deterministic mapping:
\* Assign each thread to stripe 0 (for small models, this is fine)
\* For larger models, TLC will handle the mapping deterministically
GetStripe(thread) == 0
\* ============================================================================
\* Write Barrier: nimAsgnYrc
\* ============================================================================
\* The write barrier does:
\* 1. atomicStoreN(dest, src, ATOMIC_RELEASE) -- graph update is immediate
\* 2. nimIncRefCyclic(src, true) -- buffer inc(src)
\* 3. yrcDec(tmp, desc) -- buffer dec(old)
\*
\* Key barrier semantics:
\* - ATOMIC_RELEASE on store ensures all prior writes are visible before the graph update
\* - The graph update is immediately visible to all threads (including collector)
\* - RC adjustments are buffered and only applied during merge
\* ============================================================================
\* Phase 1: Atomic Store (Topology Update)
\* ============================================================================
\* The atomic store always happens first, updating the graph topology.
\* This is independent of RC operations and never blocks.
MutatorWriteAtomicStore(thread, destObj, destField, oldVal, newVal, desc) ==
\* Atomic store with RELEASE barrier - updates graph topology immediately
\* Clear ALL edges from destObj first (atomic store replaces old value completely),
\* then set the new edge. This ensures destObj.field can only point to one object.
/\ edges' = [edges EXCEPT ![destObj] = [x \in Objects |->
IF x = newVal /\ newVal # NULL
THEN TRUE
ELSE FALSE]]
/\ UNCHANGED <<roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* Phase 2: RC Buffering (if space available)
\* ============================================================================
\* Buffers increment/decrement if there's space. If overflow would happen,
\* this action is disabled (blocked) until merge can happen.
WriteBarrier(thread, destObj, destField, oldVal, newVal, desc) ==
LET stripe == GetStripe(thread)
IN
\* Determine if overflow happens for increment or decrement
/\ LET
incOverflow == (newVal # NULL) /\ (toIncLen[stripe] >= QueueSize)
decOverflow == (oldVal # NULL) /\ (toDecLen[stripe] >= QueueSize)
IN
\* Buffering: only enabled if no overflow (otherwise blocked until merge can happen)
/\ ~incOverflow \* Precondition: increment buffer has space (blocks if full)
/\ ~decOverflow \* Precondition: decrement buffer has space (blocks if full)
/\ toIncLen' = IF newVal # NULL /\ toIncLen[stripe] < QueueSize
THEN [toIncLen EXCEPT ![stripe] = toIncLen[stripe] + 1]
ELSE toIncLen
/\ toInc' = IF newVal # NULL /\ toIncLen[stripe] < QueueSize
THEN [toInc EXCEPT ![stripe] = Append(toInc[stripe], newVal)]
ELSE toInc
/\ toDecLen' = IF oldVal # NULL /\ toDecLen[stripe] < QueueSize
THEN [toDecLen EXCEPT ![stripe] = toDecLen[stripe] + 1]
ELSE toDecLen
/\ toDec' = IF oldVal # NULL /\ toDecLen[stripe] < QueueSize
THEN [toDec EXCEPT ![stripe] = Append(toDec[stripe], [obj |-> oldVal, desc |-> desc])]
ELSE toDec
/\ UNCHANGED <<edges, roots, rc, color, inRoots, mergedRoots, lockInc, lockDec, globalLock, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* Phase 3: Overflow Handling (separate actions that can block)
\* ============================================================================
\* Handle increment overflow: merge increment buffers when lock is available
\* This merges ALL increment buffers (for all stripes), not just the one that overflowed
MutatorWriteMergeInc(thread) ==
LET stripe == GetStripe(thread)
IN
/\ \E s \in 0..(NumStripes-1): toIncLen[s] >= QueueSize \* Some stripe has increment overflow
/\ globalLock = NULL \* Lock must be available (blocks if held)
/\ toIncLen' = [s \in 0..(NumStripes-1) |-> 0]
/\ toInc' = [s \in 0..(NumStripes-1) |-> <<>>]
/\ rc' = \* Compute RC from LogicalRC of current graph (increment buffers merged)
\* The graph is already updated by atomic store, so we compute from current edges
[x \in Objects |->
LET internalRC == Cardinality({src \in Objects : edges[src][x]})
externalRC == Cardinality({t \in Threads : roots[t][x]})
IN internalRC + externalRC]
/\ globalLock' = NULL \* Release lock after merge
/\ UNCHANGED <<edges, roots, color, inRoots, toDecLen, toDec, lockInc, lockDec, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* Handle decrement overflow: merge ALL buffers when lock is available
\* This calls collectCycles() which merges both increment and decrement buffers
\* We inline MergePendingRoots here. The entire withLock block is atomic:
\* lock is acquired, merge happens, lock is released.
MutatorWriteMergeDec(thread) ==
LET stripe == GetStripe(thread)
IN
/\ \E s \in 0..(NumStripes-1): toDecLen[s] >= QueueSize \* Some stripe has decrement overflow
/\ globalLock = NULL \* Lock must be available (blocks if held)
/\ \* Merge all buffers (inlined MergePendingRoots logic)
LET \* Compute new RC by merging all buffered increments and decrements
\* For each object, count buffered increments and decrements
bufferedInc == UNION {{toInc[s][i] : i \in 1..toIncLen[s]} : s \in 0..(NumStripes-1)}
bufferedDec == UNION {{toDec[s][i].obj : i \in 1..toDecLen[s]} : s \in 0..(NumStripes-1)}
\* Compute RC: current graph state (edges) + roots - buffered decrements + buffered increments
\* Actually, we compute from LogicalRC of current graph (buffers are merged)
newRC == [x \in Objects |->
LET internalRC == Cardinality({src \in Objects : edges[src][x]})
externalRC == Cardinality({t \in Threads : roots[t][x]})
IN internalRC + externalRC]
\* Collect objects from decrement buffers for mergedRoots
newRootsSet == UNION {{toDec[s][i].obj : i \in 1..toDecLen[s]} : s \in 0..(NumStripes-1)}
newRootsSeq == IF newRootsSet = {}
THEN <<>>
ELSE LET ordered == CHOOSE f \in [1..Cardinality(newRootsSet) -> newRootsSet] :
\A i, j \in DOMAIN f : i # j => f[i] # f[j]
IN [i \in 1..Cardinality(newRootsSet) |-> ordered[i]]
IN
/\ rc' = newRC
/\ mergedRoots' = mergedRoots \o newRootsSeq
/\ inRoots' = [x \in Objects |->
IF newRootsSet = {}
THEN inRoots[x]
ELSE LET rootObjs == UNION {{mergedRoots'[i].obj : i \in DOMAIN mergedRoots'}}
IN IF x \in rootObjs THEN TRUE ELSE inRoots[x]]
/\ toIncLen' = [s \in 0..(NumStripes-1) |-> 0]
/\ toInc' = [s \in 0..(NumStripes-1) |-> <<>>]
/\ toDecLen' = [s \in 0..(NumStripes-1) |-> 0]
/\ toDec' = [s \in 0..(NumStripes-1) |-> <<>>]
/\ globalLock' = NULL \* Lock acquired, merge done, lock released (entire withLock block is atomic)
/\ UNCHANGED <<edges, roots, color, lockInc, lockDec, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* Merge Operation: mergePendingRoots
\* ============================================================================
\* Drains all stripe buffers under global lock.
\* Sequentially acquires each stripe's lockInc and lockDec to drain buffers.
\* This reconciles buffered RC adjustments with the current graph state.
\*
\* Key invariant: After merge, mergedRC = logicalRC (current graph + buffered changes)
MergePendingRoots ==
/\ globalLock # NULL
/\ LET
\* Count pending increments per object (across all stripes)
pendingInc == [x \in Objects |->
Cardinality(UNION {{i \in DOMAIN toInc[s] : toInc[s][i] = x} :
s \in 0..(NumStripes-1)})]
\* Count pending decrements per object (across all stripes)
pendingDec == [x \in Objects |->
Cardinality(UNION {{i \in DOMAIN toDec[s] : toDec[s][i].obj = x} :
s \in 0..(NumStripes-1)})]
\* After merge, RC should equal LogicalRC (current graph state)
\* The buffered changes compensate for graph changes that already happened,
\* so: mergedRC = currentRC + pendingInc - pendingDec = LogicalRC(current graph)
\* But to ensure correctness, we compute directly from the current graph:
newRC == [x \in Objects |->
LogicalRC(x)] \* RC after merge equals logical RC of current graph
\* Add decremented objects to roots if not already there (check inRootsFlag)
\* Collect all new roots as a set, then convert to sequence
\* Build set by iterating over all (stripe, index) pairs
\* Use UNION with explicit per-stripe sets (avoiding function enumeration issues)
newRootsSet == UNION {UNION {IF inRoots[toDec[s][i].obj] = FALSE
THEN {[obj |-> toDec[s][i].obj, desc |-> toDec[s][i].desc]}
ELSE {} : i \in DOMAIN toDec[s]} : s \in 0..(NumStripes-1)}
newRootsSeq == IF newRootsSet = {}
THEN <<>>
ELSE LET ordered == CHOOSE f \in [1..Cardinality(newRootsSet) -> newRootsSet] :
\A i, j \in DOMAIN f : i # j => f[i] # f[j]
IN [i \in 1..Cardinality(newRootsSet) |-> ordered[i]]
IN
/\ rc' = newRC
/\ mergedRoots' = mergedRoots \o newRootsSeq \* Append new roots to sequence
/\ \* Update inRoots: mark objects in mergedRoots' as being in roots
\* Use explicit iteration to avoid enumeration issues
inRoots' = [x \in Objects |->
IF mergedRoots' = <<>>
THEN inRoots[x]
ELSE LET rootObjs == UNION {{mergedRoots'[i].obj : i \in DOMAIN mergedRoots'}}
IN IF x \in rootObjs THEN TRUE ELSE inRoots[x]]
/\ toIncLen' = [s \in 0..(NumStripes-1) |-> 0]
/\ toInc' = [s \in 0..(NumStripes-1) |-> <<>>]
/\ toDecLen' = [s \in 0..(NumStripes-1) |-> 0]
/\ toDec' = [s \in 0..(NumStripes-1) |-> <<>>]
/\ UNCHANGED <<edges, roots, color, lockInc, lockDec, globalLock, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* Trial Deletion: markGray
\* ============================================================================
\* Subtracts internal (heap-to-heap) edges from reference counts.
\* This isolates external references (stack roots).
\*
\* Algorithm:
\* 1. Mark obj gray
\* 2. Trace obj's fields (via traceImpl)
\* 3. For each child c: decrement c.rc (subtract internal edge)
\* 4. Recursively markGray all children
\*
\* After markGray: trialRC(obj) = mergedRC(obj) - internalRefCount(obj)
\* = externalRefCount(obj) (if merge was correct)
MarkGray(obj, desc) ==
/\ globalLock # NULL
/\ collecting = TRUE
/\ color[obj] # colGray
/\ \* Compute transitive closure of all objects reachable from obj
\* This models the recursive traversal in the actual implementation
LET children == {c \in Objects : edges[obj][c]}
\* Compute all objects reachable from obj via heap edges
\* This is the transitive closure starting from obj's direct children
allReachable == {c \in Objects :
\E path \in Seq(Objects):
Len(path) > 0 /\
path[1] \in children /\
path[Len(path)] = c /\
\A i \in 1..(Len(path)-1):
edges[path[i]][path[i+1]]}
\* All objects to mark gray: obj itself + all reachable descendants
objectsToMarkGray == {obj} \cup allReachable
\* For each reachable object, count internal edges pointing to it
\* from within the subgraph (obj + allReachable)
\* This is the number of times its RC should be decremented
subgraph == {obj} \cup allReachable
internalEdgeCount == [x \in Objects |->
IF x \in allReachable
THEN Cardinality({y \in subgraph : edges[y][x]})
ELSE 0]
IN
/\ \* Mark obj and all reachable objects gray
color' = [x \in Objects |->
IF x \in objectsToMarkGray THEN colGray ELSE color[x]]
/\ \* Subtract internal edges: for each reachable object, decrement its RC
\* by the number of internal edges pointing to it from within the subgraph.
\* This matches the Nim implementation which decrements once per edge traversed.
\* Note: obj's RC is not decremented here (it has no parent in this subgraph).
\* For roots, the RC includes external refs which survive trial deletion.
rc' = [x \in Objects |->
IF x \in allReachable THEN rc[x] - internalEdgeCount[x] ELSE rc[x]]
/\ UNCHANGED <<edges, roots, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* Scan Phase
\* ============================================================================
\* Objects with RC >= 0 after trial deletion are rescued (scanBlack).
\* Objects with RC < 0 remain white (part of closed cycle).
\*
\* Key insight: scanBlack follows the *current* physical edges (which may have
\* changed since merge due to concurrent writes). This ensures objects written
\* during collection are still rescued.
\*
\* Algorithm:
\* IF rc[obj] >= 0:
\* scanBlack(obj): mark black, restore RC, trace and rescue all children
\* ELSE:
\* mark white (closed cycle with zero external refs)
Scan(obj, desc) ==
/\ globalLock # NULL
/\ collecting = TRUE
/\ color[obj] = colGray
/\ IF rc[obj] >= 0
THEN \* scanBlack: rescue obj and all reachable objects
\* This follows the current physical graph (atomic stores are visible)
\* Restore RC for all reachable objects by incrementing by the number of
\* internal edges pointing to each (matching what markGray subtracted)
LET children == {c \in Objects : edges[obj][c]}
allReachable == {c \in Objects :
\E path \in Seq(Objects):
Len(path) > 0 /\
path[1] \in children /\
path[Len(path)] = c /\
\A i \in 1..(Len(path)-1):
edges[path[i]][path[i+1]]}
objectsToMarkBlack == {obj} \cup allReachable
\* For each reachable object, count internal edges pointing to it
\* from within the subgraph (obj + allReachable)
\* This is the number of times its RC should be incremented (restored)
subgraph == {obj} \cup allReachable
internalEdgeCount == [x \in Objects |->
IF x \in allReachable
THEN Cardinality({y \in subgraph : edges[y][x]})
ELSE 0]
IN
/\ \* Restore RC: increment by the number of internal edges pointing to each
\* reachable object. This restores what markGray subtracted.
\* Note: obj's RC is not incremented here (it wasn't decremented in markGray).
\* The root's RC already reflects external refs which survived trial deletion.
rc' = [x \in Objects |->
IF x \in allReachable THEN rc[x] + internalEdgeCount[x] ELSE rc[x]]
/\ \* Mark obj and all reachable objects black in one assignment
color' = [x \in Objects |->
IF x \in objectsToMarkBlack THEN colBlack ELSE color[x]]
ELSE \* Mark white (part of closed cycle)
/\ color' = [color EXCEPT ![obj] = colWhite]
/\ UNCHANGED <<rc>>
/\ UNCHANGED <<edges, roots, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* Collection Phase: collectColor
\* ============================================================================
\* Frees objects of the target color that are not in roots.
\*
\* Safety: Only objects with color = targetColor AND ~inRoots[obj] are freed.
\* These are closed cycles (zero external refs, not reachable from roots).
CollectColor(obj, desc, targetColor) ==
/\ globalLock # NULL
/\ collecting = TRUE
/\ color[obj] = targetColor
/\ ~inRoots[obj]
/\ \* Free obj: nullify all its outgoing edges (prevents use-after-free)
\* In the actual implementation, this happens during trace() when freeing
edges' = [edges EXCEPT ![obj] = [x \in Objects |->
IF x = obj THEN FALSE ELSE edges[obj][x]]]
/\ color' = [color EXCEPT ![obj] = colBlack] \* Mark as freed
/\ UNCHANGED <<roots, rc, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* Collection Cycle: collectCyclesBacon
\* ============================================================================
StartCollection ==
/\ globalLock # NULL
/\ ~collecting
/\ Len(mergedRoots) >= RootsThreshold
/\ collecting' = TRUE
/\ gcEnv' = [touched |-> 0, edges |-> 0, rcSum |-> 0, toFree |-> {}]
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
EndCollection ==
/\ globalLock # NULL
/\ collecting = TRUE
/\ \* Clear root flags
inRoots' = [x \in Objects |->
IF x \in {r.obj : r \in mergedRoots} THEN FALSE ELSE inRoots[x]]
/\ mergedRoots' = <<>>
/\ collecting' = FALSE
/\ UNCHANGED <<edges, roots, rc, color, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* Mutator Actions
\* ============================================================================
\* Mutator can write at any time (graph updates are lock-free)
\* The ATOMIC_RELEASE barrier ensures proper ordering
\* ASSUMPTION: Users synchronize pointer assignments with locks, so oldVal always
\* matches the current graph state (as read before the atomic store).
\* This prevents races at the user level - the GC itself is lock-free.
MutatorWrite(thread, destObj, destField, oldVal, newVal, desc) ==
\* ASSUMPTION: Users synchronize pointer assignments with locks, so oldVal always matches
\* the value read before the atomic store. This prevents races at the user level.
\* The precondition is enforced in the Next relation.
\* Phase 1: Atomic store (topology update) - ALWAYS happens first
/\ MutatorWriteAtomicStore(thread, destObj, destField, oldVal, newVal, desc)
\* Phase 2: RC buffering - happens if no overflow, otherwise overflow is handled separately
\* Note: In reality, if overflow happens, the thread blocks waiting for lock.
\* We model this as: atomic store happens, buffering is deferred (handled by merge actions).
/\ LET stripe == GetStripe(thread)
incOverflow == (newVal # NULL) /\ (toIncLen[stripe] >= QueueSize)
decOverflow == (oldVal # NULL) /\ (toDecLen[stripe] >= QueueSize)
IN
IF incOverflow \/ decOverflow
THEN \* Overflow: atomic store happened, but buffering is deferred
\* Buffers stay full, merge will happen when lock is available (via MutatorWriteMergeInc/Dec)
/\ UNCHANGED <<roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
ELSE \* No overflow: buffer normally
/\ WriteBarrier(thread, destObj, destField, oldVal, newVal, desc)
/\ UNCHANGED <<roots, collecting, pendingWrites>>
\* Stack root assignment: immediate RC increment (not buffered)
\* When assigning val to a root variable named obj, we set roots[thread][val] = TRUE
\* to indicate that thread has a stack reference to val
\* Semantics: obj is root variable name, val is the object being assigned
\* When val=NULL, obj was the old root value, so we decrement rc[obj]
MutatorRootAssign(thread, obj, val) ==
/\ IF val # NULL
THEN /\ roots' = [roots EXCEPT ![thread][val] = TRUE]
/\ rc' = [rc EXCEPT ![val] = IF roots[thread][val] THEN @ ELSE @ + 1] \* Increment only if not already a root
ELSE /\ roots' = [roots EXCEPT ![thread][obj] = FALSE] \* Clear root when assigning NULL
/\ rc' = [rc EXCEPT ![obj] = IF roots[thread][obj] THEN @ - 1 ELSE @] \* Decrement old root value
/\ edges' = edges
/\ color' = color
/\ inRoots' = inRoots
/\ toIncLen' = toIncLen
/\ toInc' = toInc
/\ toDecLen' = toDecLen
/\ toDec' = toDec
/\ lockInc' = lockInc
/\ lockDec' = lockDec
/\ globalLock' = globalLock
/\ mergedRoots' = mergedRoots
/\ collecting' = collecting
/\ gcEnv' = gcEnv
/\ pendingWrites' = pendingWrites
/\ UNCHANGED seqVars
\* ============================================================================
\* Collector Actions
\* ============================================================================
\* Collector acquires write lock (global lock) for entire collection cycle.
\* RWLock semantics: writer can only acquire when no readers hold the read lock.
CollectorAcquireLock(thread) ==
/\ globalLock = NULL
/\ rwLockReaders = {} \* RWLock: no readers allowed when acquiring write lock
/\ globalLock' = thread
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
CollectorMerge ==
/\ globalLock # NULL
/\ MergePendingRoots
CollectorStart ==
/\ globalLock # NULL
/\ StartCollection
\* Mark all roots gray (trial deletion phase)
CollectorMarkGray ==
/\ globalLock # NULL
/\ collecting = TRUE
/\ \E rootIdx \in DOMAIN mergedRoots:
LET root == mergedRoots[rootIdx]
IN MarkGray(root.obj, root.desc)
\* Scan all roots (rescue phase)
CollectorScan ==
/\ globalLock # NULL
/\ collecting = TRUE
/\ \E rootIdx \in DOMAIN mergedRoots:
LET root == mergedRoots[rootIdx]
IN Scan(root.obj, root.desc)
\* Collect white/gray objects (free phase)
CollectorCollect ==
/\ globalLock # NULL
/\ collecting = TRUE
/\ \E rootIdx \in DOMAIN mergedRoots, targetColor \in {colGray, colWhite}:
LET root == mergedRoots[rootIdx]
IN CollectColor(root.obj, root.desc, targetColor)
CollectorEnd ==
/\ globalLock # NULL
/\ EndCollection
CollectorReleaseLock(thread) ==
/\ globalLock = thread
/\ globalLock' = NULL
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* Seq Payload Actions (RWLock-protected)
\* ============================================================================
\* These actions model the race between the collector tracing seq payloads
\* and mutators replacing/freeing seq payloads.
\*
\* The collector traces seq payloads in two steps:
\* 1. CollectorStartTraceSeq: reads seqData[obj] (gets payload pointer)
\* 2. CollectorFinishTraceSeq: accesses the payload data
\* Between these steps, a mutator could free the payload (the race).
\*
\* The RWLock prevents this:
\* - Collector holds write lock (globalLock) during tracing
\* - MutatorSeqAssign requires read lock (rwLockReaders)
\* - Read lock requires globalLock = NULL
\* - Therefore MutatorSeqAssign is blocked during collection
\*
\* Note: This models the memory safety aspect of seq tracing.
\* The cycle collection algorithm (MarkGray, Scan, etc.) operates on the
\* logical edge graph. Seq payloads are a physical representation detail
\* that affects memory safety but not GC correctness (which is already
\* covered by the existing Safety property).
\* Mutator acquires read lock for seq mutation.
\* RWLock semantics: read lock can be acquired when no writer holds the write lock.
\* Multiple readers can hold the read lock simultaneously.
MutatorAcquireSeqLock(thread) ==
/\ globalLock = NULL \* RWLock: no writer allowed when acquiring read lock
/\ thread \notin rwLockReaders
/\ rwLockReaders' = rwLockReaders \cup {thread}
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, collectorPayload>>
\* Mutator releases read lock after seq mutation completes.
MutatorReleaseSeqLock(thread) ==
/\ thread \in rwLockReaders
/\ rwLockReaders' = rwLockReaders \ {thread}
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, collectorPayload>>
\* Mutator replaces a seq field's payload (e.g., r.list = newSeq).
\* This frees the old payload and installs a new one.
\* Requires the read lock (RWLock protection against concurrent collection).
\*
\* In the real implementation, this is a value-type assignment (=sink/=copy)
\* that frees the old data array and installs a new one. The old array is freed
\* immediately, NOT deferred to the cycle collector.
MutatorSeqAssign(thread, obj, newPayload) ==
/\ thread \in rwLockReaders \* Must hold read lock
/\ seqData[obj] # NULL \* Object has an existing seq payload
/\ newPayload \in SeqPayloads
/\ ~payloadAlive[newPayload] \* New payload is freshly allocated (not yet alive)
/\ LET oldPayload == seqData[obj]
IN
/\ seqData' = [seqData EXCEPT ![obj] = newPayload]
/\ payloadAlive' = [payloadAlive EXCEPT ![oldPayload] = FALSE,
![newPayload] = TRUE]
\* Note: In a complete model, this would also update edges[obj] to reflect
\* the new seq elements and buffer RC changes (inc new elements, dec old elements).
\* We omit this here to focus on the memory safety property (payload lifetime).
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, rwLockReaders, collectorPayload>>
\* Collector begins tracing an object's seq field.
\* Reads the seqData pointer and stores it in collectorPayload.
\* This is the first step of a two-step trace operation.
\* The collector must hold the write lock (globalLock).
CollectorStartTraceSeq(obj) ==
/\ globalLock # NULL \* Collector holds write lock
/\ collecting = TRUE \* In collection phase
/\ seqData[obj] # NULL \* Object has a seq field
/\ collectorPayload = NULL \* Not already mid-trace
/\ collectorPayload' = seqData[obj]
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders>>
\* Collector finishes tracing an object's seq field.
\* Accesses the payload data via collectorPayload.
\* The payload MUST still be alive (this is checked by SeqPayloadSafety).
\* After accessing the payload, clears collectorPayload.
CollectorFinishTraceSeq ==
/\ globalLock # NULL \* Collector holds write lock
/\ collecting = TRUE \* In collection phase
/\ collectorPayload # NULL \* Mid-trace on a payload
\* The actual work: read payloadEdges[collectorPayload] to discover children.
\* We don't model the trace results here; the safety property ensures
\* the read is valid (payload is alive).
/\ collectorPayload' = NULL
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders>>
\* ============================================================================
\* Next State Relation
\* ============================================================================
Next ==
\/ \E thread \in Threads:
\E destObj \in Objects, oldVal, newVal \in Objects \cup {NULL}, desc \in ObjTypes:
\* Precondition: oldVal must match current graph state (user-level synchronization)
\* ASSUMPTION: Users synchronize pointer assignments with locks, so oldVal always matches
\* the value read before the atomic store. This prevents races at the user level.
/\ LET oldValMatches == CASE oldVal = NULL -> TRUE
[] oldVal \in Objects -> edges[destObj][oldVal]
[] OTHER -> FALSE
IN oldValMatches
/\ MutatorWrite(thread, destObj, "field", oldVal, newVal, desc)
\/ \E thread \in Threads:
\* Handle increment overflow: merge increment buffers when lock becomes available
MutatorWriteMergeInc(thread)
\/ \E thread \in Threads:
\* Handle decrement overflow: merge all buffers when lock becomes available
MutatorWriteMergeDec(thread)
\/ \E thread \in Threads:
\E obj, val \in Objects \cup {NULL}:
MutatorRootAssign(thread, obj, val)
\/ \E thread \in Threads:
CollectorAcquireLock(thread)
\/ CollectorMerge
\/ CollectorStart
\/ CollectorMarkGray
\/ CollectorScan
\/ CollectorCollect
\/ CollectorEnd
\/ \E thread \in Threads:
CollectorReleaseLock(thread)
\* --- Seq payload actions ---
\/ \E thread \in Threads:
MutatorAcquireSeqLock(thread)
\/ \E thread \in Threads:
MutatorReleaseSeqLock(thread)
\/ \E thread \in Threads, obj \in Objects, p \in SeqPayloads:
MutatorSeqAssign(thread, obj, p)
\/ \E obj \in Objects:
CollectorStartTraceSeq(obj)
\/ CollectorFinishTraceSeq
\* ============================================================================
\* Initial State
\* ============================================================================
Init ==
/\ edges = [x \in Objects |->
[y \in Objects |->
IF x = y THEN FALSE ELSE FALSE]] \* Empty graph initially
/\ roots = [t \in Threads |->
[x \in Objects |->
FALSE]] \* No stack roots initially
/\ rc = [x \in Objects |->
0] \* Zero reference counts
/\ color = [x \in Objects |->
colBlack] \* All objects black initially
/\ inRoots = [x \in Objects |->
FALSE] \* No objects in roots array
/\ toIncLen = [s \in 0..(NumStripes-1) |->
0]
/\ toInc = [s \in 0..(NumStripes-1) |->
<<>>]
/\ toDecLen = [s \in 0..(NumStripes-1) |->
0]
/\ toDec = [s \in 0..(NumStripes-1) |->
<<>>]
/\ lockInc = [s \in 0..(NumStripes-1) |->
NULL]
/\ lockDec = [s \in 0..(NumStripes-1) |->
NULL]
/\ globalLock = NULL
/\ mergedRoots = <<>>
/\ collecting = FALSE
/\ gcEnv = [touched |-> 0, edges |-> 0, rcSum |-> 0, toFree |-> {}]
/\ pendingWrites = {}
\* Seq payload initial state
/\ seqData = [x \in Objects |-> NULL] \* No seq fields initially
/\ payloadAlive = [p \in SeqPayloads |-> FALSE] \* No payloads alive initially
/\ rwLockReaders = {} \* No threads hold read lock
/\ collectorPayload = NULL \* Collector not mid-trace
/\ TypeOK
\* ============================================================================
\* Safety Properties
\* ============================================================================
\* Safety: Objects are only freed if they are unreachable from any thread's stack
\*
\* An object is reachable if:
\* - It is a direct stack root (roots[t][obj] = TRUE), OR
\* - There exists a path from a stack root to obj via heap edges
\*
\* Safety guarantee: If an object is reachable, then:
\* - It is not white (not marked for collection), OR
\* - It is in roots array (protected from collection), OR
\* - It is reachable from an object that will be rescued by scanBlack
\*
\* More precisely: Only closed cycles (zero external refs, unreachable) are freed.
\* Helper: Compute next set of reachable objects (one step of transitive closure)
ReachableStep(current) ==
current \cup UNION {{y \in Objects : edges[x][y]} : x \in current}
\* Compute the set of all reachable objects using bounded iteration
\* Since Objects is finite, we iterate at most Cardinality(Objects) times
\* This computes the transitive closure of edges starting from stack roots
\* We unroll the iteration explicitly to avoid recursion issues with TLC
ReachableSet ==
LET StackRoots == {x \in Objects : \E t \in Threads : roots[t][x]}
Step1 == ReachableStep(StackRoots)
Step2 == ReachableStep(Step1)
Step3 == ReachableStep(Step2)
Step4 == ReachableStep(Step3)
\* Add more steps if needed for larger object sets
\* For small models (2 objects), 4 steps is sufficient
IN Step4
\* Check if an object is reachable
Reachable(obj) == obj \in ReachableSet
\* Helper: Check if there's a path from 'from' to 'to'
\* For small object sets, we check all possible paths by checking
\* all combinations of intermediate objects
\* Path of length 0: from = to
\* Path of length 1: edges[from][to]
\* Path of length 2: \E i1: edges[from][i1] /\ edges[i1][to]
\* Path of length 3: \E i1, i2: edges[from][i1] /\ edges[i1][i2] /\ edges[i2][to]
\* etc. up to Cardinality(Objects)
HasPath(from, to) ==
\/ from = to
\/ edges[from][to]
\/ \E i1 \in Objects:
edges[from][i1] /\ (edges[i1][to] \/ \E i2 \in Objects:
edges[i1][i2] /\ (edges[i2][to] \/ \E i3 \in Objects:
edges[i2][i3] /\ edges[i3][to]))
\* Helper: Compute set of objects reachable from a given starting object
\* Uses the same iterative approach as ReachableSet
ReachableFrom(start) ==
LET Step1 == ReachableStep({start})
Step2 == ReachableStep(Step1)
Step3 == ReachableStep(Step2)
Step4 == ReachableStep(Step3)
IN Step4
\* Safety: Reachable objects are never freed (remain white without being collected)
\* A reachable object is safe if:
\* - It's not white (not marked for collection), OR
\* - It's in roots array (protected from collection), OR
\* - There exists a black object in ReachableSet such that obj is reachable from it
\* (the black object will be rescued by scanBlack, which rescues all white objects
\* reachable from black objects)
Safety ==
\A obj \in Objects:
IF obj \in ReachableSet
THEN \/ color[obj] # colWhite \* Not marked for collection
\/ inRoots[obj] \* Protected in roots array
\/ \E blackObj \in ReachableSet:
/\ color[blackObj] = colBlack \* Black object will be rescued by scanBlack
/\ obj \in ReachableFrom(blackObj) \* obj is reachable from blackObj
ELSE TRUE \* Unreachable objects may be freed (this is safe)
\* Invariant: Reference counts match logical counts after merge
\* (This is maintained by MergePendingRoots)
\* Note: Between merge and collection, RC = logicalRC.
\* During collection (after markGray), RC may be modified by trial deletion.
\* RC may be inconsistent when:
\* - globalLock = NULL (buffered changes pending)
\* - globalLock # NULL but merge hasn't happened yet (buffers still have pending changes)
\* RC must equal LogicalRC when:
\* - After merge (buffers are empty) and before collection starts
RCInvariant ==
IF globalLock = NULL
THEN TRUE \* Not in collection, RC may be inconsistent (buffered changes pending)
ELSE IF collecting = FALSE /\ \A s \in 0..(NumStripes-1): toIncLen[s] = 0 /\ toDecLen[s] = 0
THEN \A obj \in Objects: rc[obj] = LogicalRC(obj) \* After merge, buffers empty, RC = logical RC
ELSE TRUE \* During collection or before merge, RC may differ from logicalRC
\* Invariant: Only closed cycles are collected
\* (Objects with external refs are rescued by scanBlack)
CycleInvariant ==
\A obj \in Objects:
IF color[obj] = colWhite /\ ~inRoots[obj]
THEN ExternalRC(obj) = 0
ELSE TRUE
\* ============================================================================
\* Seq Payload Safety
\* ============================================================================
\* Memory safety: The collector never accesses a freed seq payload.
\*
\* collectorPayload holds the payload pointer the collector read during
\* CollectorStartTraceSeq. Between that action and CollectorFinishTraceSeq,
\* the collector will dereference this pointer to read the seq's elements.
\* If the payload has been freed in between, this is a use-after-free.
\*
\* The RWLock prevents this:
\* - collectorPayload is only set when globalLock # NULL (write lock held)
\* - MutatorSeqAssign (which frees payloads) requires rwLockReaders membership
\* - MutatorAcquireSeqLock requires globalLock = NULL (no writer)
\* - Therefore: while collectorPayload # NULL, no MutatorSeqAssign can execute
\* - Therefore: payloadAlive[collectorPayload] remains TRUE
\*
\* Without the RWLock (if MutatorSeqAssign didn't require the read lock),
\* the following interleaving would violate this property:
\* 1. Collector acquires write lock
\* 2. CollectorStartTraceSeq(obj) -- collectorPayload = P
\* 3. MutatorSeqAssign(thread, obj, Q) -- frees P, payloadAlive[P] = FALSE
\* 4. SeqPayloadSafety VIOLATED: collectorPayload = P but payloadAlive[P] = FALSE
SeqPayloadSafety ==
collectorPayload # NULL => payloadAlive[collectorPayload]
\* ============================================================================
\* RWLock Invariant
\* ============================================================================
\* The read-write lock ensures mutual exclusion between the collector (writer)
\* and seq mutations (readers). The writer and readers are never active at
\* the same time.
RWLockInvariant ==
globalLock # NULL => rwLockReaders = {}
\* ============================================================================
\* Specification
\* ============================================================================
Spec == Init /\ [][Next]_<<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
THEOREM Spec => []Safety
THEOREM Spec => []RCInvariant
THEOREM Spec => []CycleInvariant
THEOREM Spec => []SeqPayloadSafety
THEOREM Spec => []RWLockInvariant
====

View File

@@ -1,5 +1,6 @@
discard """
ccodeCheck: "\\i @'NIM_ALIGN(128) NI mylocal1' .*"
matrix: "--mm:refc -d:useGcAssert -d:useSysAssert; --mm:orc"
targets: "c cpp"
output: "align ok"
"""
@@ -67,3 +68,103 @@ block: # bug #22419
f()()
type Xxx = object
v {.align: 128.}: byte
type Yyy = object
v: byte
v2: Xxx
for i in 0..<3:
let x = new Yyy
# echo "addr v2.v:", cast[uint](addr x.v2.v)
doAssert cast[uint](addr x.v2.v) mod 128 == 0
let m = new Yyy
m.v2.v = 42
doAssert m.v2.v == 42
m.v = 7
doAssert m.v == 7
type
MyType16 = object
a {.align(16).}: int
var x: array[10, ref MyType16]
for q in 0..500:
for i in 0..<x.len:
new x[i]
x[i].a = q
doAssert(cast[int](x[i]) mod alignof(MyType16) == 0)
type
MyType32 = object
a{.align(32).}: int
var y: array[10, ref MyType32]
for q in 0..500:
for i in 0..<y.len:
new y[i]
y[i].a = q
doAssert(cast[int](y[i]) mod alignof(MyType32) == 0)
# Additional tests: allocate custom aligned objects using `new`
type
MyType64 = object
a{.align(64).}: int
var z: array[10, ref MyType64]
for q in 0..500:
for i in 0..<z.len:
new z[i]
z[i].a = q
doAssert(cast[int](z[i]) mod alignof(MyType64) == 0)
type
MyType128 = object
a{.align(128).}: int
var w: array[10, ref MyType128]
for q in 0..500:
for i in 0..<w.len:
new w[i]
w[i].a = q
doAssert(cast[int](w[i]) mod alignof(MyType128) == 0)
# Nested aligned-object tests
type
Inner128 = object
v {.align(128).}: byte
OuterWithInner = object
prefix: int
inner: Inner128
var outerArr: array[8, ref OuterWithInner]
for q in 0..200:
for i in 0..<outerArr.len:
new outerArr[i]
# write to inner to ensure it's allocated
outerArr[i].inner.v = cast[byte](q and 0xFF)
doAssert(cast[uint](addr outerArr[i].inner) mod uint(alignof(Inner128)) == 0)
# Nested two-level alignment
type
DeepInner = object
b {.align(128).}: int
Mid = object
di: DeepInner
Top = object
m: Mid
var topArr: array[4, ref Top]
for q in 0..100:
for i in 0..<topArr.len:
new topArr[i]
topArr[i].m.di.b = q
doAssert(cast[uint](addr topArr[i].m.di) mod uint(alignof(DeepInner)) == 0)

10
tests/align/talign2.nim Normal file
View File

@@ -0,0 +1,10 @@
discard """
matrix: "--mm:refc -d:useGcAssert -d:useSysAssert; --mm:orc"
"""
block:
type U = object
d {.align: 16.}: int8
var e: seq[ref U]
for i in 0 ..< 10000: e.add(new U)
doAssert getTotalMem() <= 1052672 * 2

View File

@@ -35,4 +35,5 @@ proc main() =
main()
GC_fullCollect()
echo getOccupiedMem() < 10 * 1024 * 1024, " peak memory: ", getMaxMem() < 10 * 1024 * 1024
when not defined(useMalloc):
echo getOccupiedMem() < 10 * 1024 * 1024, " peak memory: ", getMaxMem() < 10 * 1024 * 1024

View File

@@ -605,3 +605,17 @@ block t18643:
except IndexDefect:
caught = true
doAssert caught, "IndexDefect not caught!"
# bug #25475
block:
type N = object
b: seq[array[1'u, int]]
doAssert N(b: @[[0]]) == N(b: @[[0]])
block:
var x: array[5..6, int] = [0, 1]
var y: array[1..2, int] = [0, 1]
doAssert x == y # compiles
doAssert @[x] == @[y]

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