From e6e00a74a3f1772e056e79564b69c666c0fd810e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:14:36 +0800 Subject: [PATCH 01/23] Bump actions/github-script from 8 to 9 (#25748) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
Release notes

Sourced from actions/github-script's releases.

v9.0.0

New features:

Breaking changes:

What's Changed

New Contributors

Full Changelog: https://github.com/actions/github-script/compare/v8.0.0...v9.0.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/github-script&package-manager=github_actions&previous-version=8&new-version=9)](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) ---
Dependabot commands and options
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 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)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci_publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_publish.yml b/.github/workflows/ci_publish.yml index 44cfaf8213..67b78092f8 100644 --- a/.github/workflows/ci_publish.yml +++ b/.github/workflows/ci_publish.yml @@ -60,7 +60,7 @@ jobs: run: nim c -r -d:release ci/action.nim - name: 'Comment' - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); From f98578ea35fdd8b3887778700c07c903fefee512 Mon Sep 17 00:00:00 2001 From: Ryan McConnell Date: Sat, 18 Apr 2026 02:52:31 -0400 Subject: [PATCH 02/23] fix 25667; Generic forward type confusion (#25737) ref: #25667 drain deferred reification in a loop until there is no more work to do. Could potentially evaluate the same deferred work more than once. --------- Co-authored-by: Andreas Rumpf --- compiler/semdata.nim | 6 ++--- compiler/semstmts.nim | 35 +++++++++++++++++++++------- compiler/semtypes.nim | 8 +++---- tests/objects/t25627.nim | 17 ++++++++++++++ tests/types/tforwardcycletimeout.nim | 10 ++++++++ 5 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 tests/types/tforwardcycletimeout.nim diff --git a/compiler/semdata.nim b/compiler/semdata.nim index c561f6690e..15d8b14fe7 100644 --- a/compiler/semdata.nim +++ b/compiler/semdata.nim @@ -180,9 +180,9 @@ type sideEffects*: Table[int, seq[(TLineInfo, PSym)]] # symbol.id index inUncheckedAssignSection*: int importModuleLookup*: Table[int, seq[int]] # (module.ident.id, [module.id]) - forwardTypeUpdates*: seq[(PType, PNode)] - # types that need to be updated in a type section - # due to containing forward types, and their corresponding nodes + forwardTypeUpdates*: seq[(PSym, PType, PNode)] + # top-level owner, type, and type node for delayed retries inside a + # type section due to containing forward types forwardFieldUpdates*: seq[(PType, PNode, PType)] # object/tuple field definitions whose default values mention forward # types and need delayed const checking diff --git a/compiler/semstmts.nim b/compiler/semstmts.nim index d31bb5a9af..465276ffc2 100644 --- a/compiler/semstmts.nim +++ b/compiler/semstmts.nim @@ -1808,15 +1808,32 @@ proc checkForMetaFields(c: PContext; n: PNode; hasError: var bool) = internalAssert c.config, false proc typeSectionFinalPass(c: PContext, n: PNode) = - for (typ, typeNode) in c.forwardTypeUpdates: - # types that need to be updated due to containing forward types - # and their corresponding type nodes - # for example generic invocations of forward types end up here - var reified = semTypeNode(c, typeNode, nil) - assert reified != nil - assignType(typ, reified) - typ.itemId = reified.itemId # same id - c.forwardTypeUpdates = @[] + # each top level type needs to be processed, each epoch should reify at least one + var remainingOwners = initIntSet() + for (owner, _, _) in c.forwardTypeUpdates: + remainingOwners.incl owner.id + + while c.forwardTypeUpdates.len > 0: + let pending = move c.forwardTypeUpdates + var madeProgress = false + + for (owner, typ, typeNode) in pending: + # types that need to be updated due to containing forward types + # and their corresponding type nodes + # for example generic invocations of forward types end up here + var reified = semTypeNode(c, typeNode, nil) + assert reified != nil + assignType(typ, reified) + typ.itemId = reified.itemId # same id + if containsForwardType(typ): + c.forwardTypeUpdates.add (owner, typ, typeNode) + elif not remainingOwners.missingOrExcl(owner.id): + madeProgress = true + + if not madeProgress: + # can't error here unfortunately + break + for (owner, field, expectedType) in c.forwardFieldUpdates: semDelayedFieldDefault(c, owner, expectedType, field) c.forwardFieldUpdates = @[] diff --git a/compiler/semtypes.nim b/compiler/semtypes.nim index 0b26cb8b8f..e904579283 100644 --- a/compiler/semtypes.nim +++ b/compiler/semtypes.nim @@ -223,7 +223,7 @@ proc semSet(c: PContext, n: PNode, prev: PType): PType = if base.kind in {tyGenericInst, tyAlias, tySink}: base = skipModifier(base) if base.kind notin {tyGenericParam, tyGenericInvocation}: if base.kind == tyForward: - c.forwardTypeUpdates.add (base, n[1]) + c.forwardTypeUpdates.add (getCurrOwner(c), base, n[1]) elif not isOrdinalType(base, allowEnumWithHoles = true): localError(c.config, n.info, errOrdinalTypeExpected % typeToString(base, preferDesc)) elif lengthOrd(c.config, base) > MaxSetElements: @@ -1114,7 +1114,7 @@ proc semObjectNode(c: PContext, n: PNode, prev: PType; flags: TTypeFlags): PType if needsForwardUpdate: # if the inherited object is a forward type, # the entire object needs to be checked again - c.forwardTypeUpdates.add (result, n) # we retry in the final pass + c.forwardTypeUpdates.add (getCurrOwner(c), result, n) # we retry in the final pass rawAddSon(result, realBase) if realBase == nil and tfInheritable in flags: result.incl tfInheritable @@ -1762,7 +1762,7 @@ proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType = for i in 1.. Date: Sat, 18 Apr 2026 15:40:55 +0800 Subject: [PATCH 03/23] fixes #25751; JS backend crashes when returning `Option[T]` with custom `=destroy` (#25752) fixes #25751 This pull request improves the JavaScript backend code generation and expands test coverage, particularly around temporary and loop variables, as well as object destruction behavior. The main changes include updating the code generator to handle more symbol kinds and adding tests to ensure proper destruction and option handling. **JavaScript code generation improvements:** * Updated `genSymAddr` in `compiler/jsgen.nim` to support additional symbol kinds, specifically `skTemp` and `skForVar`, ensuring correct address generation for temporaries and loop variables. **Test suite enhancements:** * Added tests in `tests/js/test2.nim` to verify correct behavior of option types, object destruction (`=destroy`), and to check for backend-specific crashes. This includes printing results of option-returning functions and confirming destruction messages. * Updated expected output in `tests/js/test2.nim` to include results from new tests and destruction messages, ensuring the test suite reflects the latest code behavior. --- compiler/jsgen.nim | 2 +- tests/js/test2.nim | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/compiler/jsgen.nim b/compiler/jsgen.nim index 98153490df..fd82a127f7 100644 --- a/compiler/jsgen.nim +++ b/compiler/jsgen.nim @@ -1544,7 +1544,7 @@ proc genSymAddr(p: PProc, n: PNode, typ: PType, r: var TCompRes) = r.res = s.loc.snippet r.address = "" r.typ = etyNone - of skVar, skLet, skResult: + of skVar, skLet, skResult, skTemp, skForVar: r.kind = resExpr let jsType = mapType(p): if typ.isNil: diff --git a/tests/js/test2.nim b/tests/js/test2.nim index fa857ccc5c..c4cb2a25d3 100644 --- a/tests/js/test2.nim +++ b/tests/js/test2.nim @@ -4,7 +4,12 @@ js 3.14 7 1 -21550 --21550''' +-21550 +none(TT) +() +destroyed +destroyed +''' """ # This file tests the JavaScript generator @@ -56,3 +61,15 @@ proc foo09() = const y = 86400 echo (x - (y - 1)) div y # Still gives `-21551` foo09() + +import std/options + +type TT = object + +proc `=destroy`(x: TT) = echo "destroyed" + +func test1: Option[TT] = discard +func test2: TT = discard + +echo test1() # Crash in JS backend, not crash in C backend +echo test2() # Not crash From 5948dbbeed1a62f66185c09f1ceba2864dea4c4c Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Mon, 20 Apr 2026 02:12:01 +0800 Subject: [PATCH 04/23] fixes #25718; `setLenUnit` slow (#25743) fixes #25718 This pull request optimizes sequence allocation in the Nim standard library by introducing a way to create uninitialized sequence payloads for element types that don't require zero-initialization. The changes allow for more efficient memory allocation when initializing sequences with types that have no references, avoiding unnecessary zeroing of memory. Sequence allocation and initialization improvements: * Added the `newSeqUninitRaw` procedure to create sequence payloads with a specified length without forcing zero-initialization for element types marked as `ntfNoRefs`. (`lib/system/sysstr.nim`, [lib/system/sysstr.nimR277-R292](diffhunk://#diff-bcaa1967f436ad03877f353823c08a8b4a719fe387629d33aab4bddf16534b5eR277-R292)) * Modified the `extendCapacityRaw` procedure and the `setLengthSeqImpl` template to use `newSeqUninitRaw` when zero-initialization is not required, controlled by the `doInit` static parameter. (`lib/system/sysstr.nim`, [[1]](diffhunk://#diff-bcaa1967f436ad03877f353823c08a8b4a719fe387629d33aab4bddf16534b5eR277-R292) [[2]](diffhunk://#diff-bcaa1967f436ad03877f353823c08a8b4a719fe387629d33aab4bddf16534b5eL316-R335) --- lib/system/sysstr.nim | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/system/sysstr.nim b/lib/system/sysstr.nim index f7f5c3b08e..9ecdffb669 100644 --- a/lib/system/sysstr.nim +++ b/lib/system/sysstr.nim @@ -299,12 +299,22 @@ proc incrSeqV3(s: PGenericSeq, typ: PNimType): PGenericSeq {.compilerproc.} = # since we steal the content from 's', it's crucial to set s's len to 0. s.len = 0 +proc newSeqUninitRaw(typ: PNimType; len: int): pointer {.inline.} = + ## Creates a sequence payload with capacity and length `len` without + ## forcing zero-initialization for `ntfNoRefs` element types. + result = nimNewSeqOfCap(typ, len) + cast[PGenericSeq](result).len = len + proc extendCapacityRaw(src: PGenericSeq; typ: PNimType; - elemSize, elemAlign, newLen: int): PGenericSeq {.inline.} = + elemSize, elemAlign, newLen: int; + doInit: static bool): PGenericSeq {.inline.} = ## Reallocs `src` to fit `newLen` elements without any checks. ## Capacity always increases to at least next `resize` step. let newCap = max(resize(src.space), newLen) - result = cast[PGenericSeq](newSeq(typ, newCap)) + when doInit: + result = cast[PGenericSeq](newSeq(typ, newCap)) + else: + result = cast[PGenericSeq](newSeqUninitRaw(typ, newCap)) copyMem(dataPointer(result, elemAlign), dataPointer(src, elemAlign), src.len * elemSize) # since we steal the content from 's', it's crucial to set s's len to 0. src.len = 0 @@ -335,15 +345,19 @@ proc truncateRaw(src: PGenericSeq; baseFlags: set[TNimTypeFlag]; isTrivial: bool ((result.len-%newLen) *% elemSize)) template setLengthSeqImpl(s: PGenericSeq, typ: PNimType, newLen: int; isTrivial: bool; - doInit: static bool) = + doInit: static bool) = if s == nil: if newLen == 0: return s - else: return cast[PGenericSeq](newSeq(typ, newLen)) # newSeq zeroes! + else: + when doInit: + return cast[PGenericSeq](newSeq(typ, newLen)) # newSeq zeroes! + else: + return cast[PGenericSeq](newSeqUninitRaw(typ, newLen)) else: let elemSize = typ.base.size let elemAlign = typ.base.align result = if newLen > s.space: - s.extendCapacityRaw(typ, elemSize, elemAlign, newLen) + s.extendCapacityRaw(typ, elemSize, elemAlign, newLen, doInit) elif newLen < s.len: s.truncateRaw(typ.base.flags, isTrivial, elemSize, elemAlign, newLen) else: From 317bc10824a8d5599b0b11c75d6248138b5dc302 Mon Sep 17 00:00:00 2001 From: Tomohiro Date: Mon, 20 Apr 2026 10:21:46 +0900 Subject: [PATCH 05/23] Makes `containsOrIncl*[A](s: var PackedSet[A], key: A)` proc faster (#25755) This PR makes it faster when a number of elements is less than 34 I used following code to compare the speed of `containsOrIncl` proc. It calls `isRecursiveStructuralType` proc defined in compiler/types.nim that calls `containsOrIncl` with `IntSet`(= `PackedSet[int]`). ```nim import std/[tables, monotimes, times, strformat] import "$nim"/compiler/[astdef, ast, idents, types] var idgen = IdGenerator(module: 0, symId: 0, typeId: 0, disambTable: initCountTable[PIdent]()) proc newType(kind: TTypeKind; son: sink PType = nil): PType = result = newType(kind, idgen, nil, son) proc genNoRecursPType(len: int): PType = assert len > 1 let intTyp = newType(tyInt) result = newType(tyRef, intTyp) for i in 0..<(len - 2): result = newType(tyRef, result) proc test = var noRecursPType = genNoRecursPType(4) assert not isRecursiveStructuralType(noRecursPType) test() template measure(label: string; body: untyped): untyped = let loop = 2000 sampling = 200 block: var r {.inject.} = false var minT = initDuration(hours = 1) for i in 0 ..< sampling: let start = getMonoTime() for j in 0 ..< loop: body let finish = getMonoTime() minT = min(finish - start, minT) echo ($r)[0], ' ', label, minT div loop proc benchNoRecurs(len: int) = echo fmt"No recursive: length: {len}" var noRecursPType = genNoRecursPType(len) measure("IntSet: "): r = isRecursiveStructuralType(noRecursPType) proc bench = benchNoRecurs(30) bench() ``` Output before changing code: ``` f IntSet: 1 microsecond and 262 nanoseconds ``` Output after change: ``` f IntSet: 833 nanoseconds ``` Why this PR make it faster: ```nim proc containsOrIncl*[A](s: var PackedSet[A], key: A): bool = ... if s.elems <= s.a.len: for i in 0.. Date: Mon, 20 Apr 2026 09:17:12 +0200 Subject: [PATCH 06/23] fixes #25695 (#25756) --- compiler/ccgexprs.nim | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/compiler/ccgexprs.nim b/compiler/ccgexprs.nim index 941327341e..b4edfcf6dd 100644 --- a/compiler/ccgexprs.nim +++ b/compiler/ccgexprs.nim @@ -1889,10 +1889,17 @@ proc genObjConstr(p: BProc, e: PNode, d: var TLoc) = var t = e.typ.skipTypes(abstractInstOwned) let isRef = t.kind == tyRef - # check if we need to construct the object in a temporary + # check if we need to construct the object in a temporary. + # A temp is needed when: + # - the constructor produces a ref (isRef) + # - the destination is not a writable location (d.k == locNone) + # - the constructed type differs from the destination type (subtype + # assignments need the genAssignment path for ObjectAssignmentDefect) + # - the constructor's field values may alias the destination (isPartOf) var useTemp = isRef or - (d.k notin {locTemp,locLocalVar,locGlobalVar,locParam,locField}) or + d.k == locNone or + (d.t != nil and not sameBackendType(t, d.t.skipTypes(abstractInstOwned))) or (isPartOf(d.lode, e) != arNo) var tmp: TLoc = default(TLoc) From ba4e12fb65f835d6f657e6d0bd07117e8c3abb42 Mon Sep 17 00:00:00 2001 From: Tomohiro Date: Tue, 21 Apr 2026 03:13:06 +0900 Subject: [PATCH 07/23] fixes #25753 (#25754) --- compiler/semtypes.nim | 2 +- tests/types/tillegalset.nim | 7 +++++++ tests/types/tillegalset2.nim | 8 ++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 tests/types/tillegalset.nim create mode 100644 tests/types/tillegalset2.nim diff --git a/compiler/semtypes.nim b/compiler/semtypes.nim index e904579283..4c2d84c29f 100644 --- a/compiler/semtypes.nim +++ b/compiler/semtypes.nim @@ -223,7 +223,7 @@ proc semSet(c: PContext, n: PNode, prev: PType): PType = if base.kind in {tyGenericInst, tyAlias, tySink}: base = skipModifier(base) if base.kind notin {tyGenericParam, tyGenericInvocation}: if base.kind == tyForward: - c.forwardTypeUpdates.add (getCurrOwner(c), base, n[1]) + c.forwardTypeUpdates.add (getCurrOwner(c), result, n) elif not isOrdinalType(base, allowEnumWithHoles = true): localError(c.config, n.info, errOrdinalTypeExpected % typeToString(base, preferDesc)) elif lengthOrd(c.config, base) > MaxSetElements: diff --git a/tests/types/tillegalset.nim b/tests/types/tillegalset.nim new file mode 100644 index 0000000000..e4f60da444 --- /dev/null +++ b/tests/types/tillegalset.nim @@ -0,0 +1,7 @@ +discard """ + errormsg: "set is too large; use `std/sets` for ordinal types with more than 2^16 elements" +""" + +type + Foo = set[Bar] + Bar = int32 diff --git a/tests/types/tillegalset2.nim b/tests/types/tillegalset2.nim new file mode 100644 index 0000000000..737e7a5892 --- /dev/null +++ b/tests/types/tillegalset2.nim @@ -0,0 +1,8 @@ +discard """ + errormsg: "set is too large; use `std/sets` for ordinal types with more than 2^16 elements" +""" + +type + Foo = int32 + Bar = set[Baz] + Baz = Foo From de3d61f15b6086117aaa95b49867a6f07cd424c6 Mon Sep 17 00:00:00 2001 From: Bojun Chai Date: Tue, 21 Apr 2026 08:50:13 +0800 Subject: [PATCH 08/23] Fix invalid Mac OS X minimum version in README (#25758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Repo:** nim-lang/Nim (⭐ 16000) **Type:** docs **Files changed:** 1 **Lines:** +1/-1 ## What Correct the supported platform table in the top-level README by changing the Mac OS X minimum version from `10.04` to `10.4`. ## Why `10.04` is not a valid Mac OS X release number, so the existing text is misleading for anyone reading the build and platform support guidance. Fixing it keeps the README accurate without changing project behavior or widening scope. ## Testing Verified the README diff locally and confirmed the corrected `Mac OS X (10.4 or greater)` entry appears in `readme.md`. No code or test suite changes were needed for this docs-only patch. ## Risk Low / documentation-only change with no runtime impact. Co-authored-by: Bojun Chai --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 22d5294c2f..c0a60483c7 100644 --- a/readme.md +++ b/readme.md @@ -39,7 +39,7 @@ architecture combinations: |--------------------------------|----------------------------------------| | Windows (Windows XP or greater) | x86 and x86_64 | | Linux (most distributions) | x86, x86_64, ppc64, and armv6l | -| Mac OS X (10.04 or greater) | x86, x86_64, ppc64, and Apple Silicon (ARM64) | +| Mac OS X (10.4 or greater) | x86, x86_64, ppc64, and Apple Silicon (ARM64) | More platforms are supported, however, they are not tested regularly and they may not be as stable as the above-listed platforms. From 60bb9c75ccac37b74fa203e49e043cd19073b1ce Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Tue, 21 Apr 2026 22:38:33 +0800 Subject: [PATCH 09/23] fixes #25650; nim ic import std/strbasics (#25760) fixes #25650 This pull request refactors and improves the dependency resolution logic in the Nim compiler, The most important changes are grouped below: ### Dependency Resolution Refactor * Replaced the `resolveFile` procedure with two more specialized procedures: `resolveImport` (which uses the compiler's module lookup rules for imports) and `resolveInclude` (which resolves includes relative to the including file or search paths). Updated all usages accordingly, improving clarity and correctness of dependency handling. [[1]](diffhunk://#diff-1203947eecb9ef641ce7ee029677f875eb983de050b82c65ca286517feed00e6L82-R94) [[2]](diffhunk://#diff-1203947eecb9ef641ce7ee029677f875eb983de050b82c65ca286517feed00e6L106-R103) [[3]](diffhunk://#diff-1203947eecb9ef641ce7ee029677f875eb983de050b82c65ca286517feed00e6L121-R118) * Removed the unused `strutils` import from `compiler/deps.nim` for cleaner dependencies. ### Testing Improvements * Added `import std/strbasics` to `tests/ic/tmiscs.nim` to ensure required symbols are available for tests. I tried to improve `resolveFile`, which is harder because either we need to add `lib/std` to search path and all of other nested directory to `--path` in `config/nim.cfg`. So I choose toi reuse `findModule` for imports --- compiler/deps.nim | 25 +++++++++++-------------- tests/ic/tmiscs.nim | 1 + 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/compiler/deps.nim b/compiler/deps.nim index 6b891fac9f..18d6608fd0 100644 --- a/compiler/deps.nim +++ b/compiler/deps.nim @@ -10,10 +10,10 @@ ## Generate a .build.nif file for nifmake from a Nim project. ## This enables incremental and parallel compilation using the `m` switch. -import std / [os, tables, sets, times, osproc, strutils] +import std / [os, tables, sets, times, osproc] import options, msgs, lineinfos, pathutils -import "../dist/nimony/src/lib" / [nifstreams, nifcursors, bitabs, nifreader, nifbuilder] +import "../dist/nimony/src/lib" / [nifstreams, bitabs, nifreader, nifbuilder] import "../dist/nimony/src/gear2" / modnames type @@ -79,22 +79,19 @@ proc runNifler(c: DepContext; nimFile: string): bool = let exitCode = execShellCmd(cmd) result = exitCode == 0 -proc resolveFile(c: DepContext; origin, toResolve: string): string = - ## Resolve an import path relative to origin file - # Handle std/ prefix - var path = toResolve - if path.startsWith("std/"): - path = path.substr(4) +proc resolveImport(c: DepContext; origin, toResolve: string): string = + ## Resolve an import path using the compiler's normal module lookup rules. + result = findModule(c.config, toResolve, origin).string - # Try relative to origin first +proc resolveInclude(c: DepContext; origin, toResolve: string): string = + ## Resolve an include path relative to the including file or the search paths. let originDir = parentDir(origin) - result = originDir / path.addFileExt("nim") + result = originDir / toResolve.addFileExt("nim") if fileExists(result): return result - # Try search paths for searchPath in c.config.searchPaths: - result = searchPath.string / path.addFileExt("nim") + result = searchPath.string / toResolve.addFileExt("nim") if fileExists(result): return result @@ -103,7 +100,7 @@ proc resolveFile(c: DepContext; origin, toResolve: string): string = proc traverseDeps(c: var DepContext; pair: FilePair; current: Node) proc processInclude(c: var DepContext; includePath: string; current: Node) = - let resolved = resolveFile(c, current.files[current.files.len - 1].nimFile, includePath) + let resolved = resolveInclude(c, current.files[current.files.len - 1].nimFile, includePath) if resolved.len == 0 or not fileExists(resolved): return @@ -118,7 +115,7 @@ proc processInclude(c: var DepContext; includePath: string; current: Node) = discard c.includeStack.pop() proc processImport(c: var DepContext; importPath: string; current: Node) = - let resolved = resolveFile(c, current.files[0].nimFile, importPath) + let resolved = resolveImport(c, current.files[0].nimFile, importPath) if resolved.len == 0 or not fileExists(resolved): return diff --git a/tests/ic/tmiscs.nim b/tests/ic/tmiscs.nim index aabdd92601..e90ad05244 100644 --- a/tests/ic/tmiscs.nim +++ b/tests/ic/tmiscs.nim @@ -10,6 +10,7 @@ discard """ @[1, 2] ''' """ +import std/strbasics # Object variant / case object type From efacf1f39062e1a7d916cf35a40d9475ceb77c97 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Tue, 21 Apr 2026 22:38:56 +0800 Subject: [PATCH 10/23] Fix typo in getContentType function in cgi.nim (#25757) This pull request fixes a typo in the `getContentType` function in `lib/pure/cgi.nim`, ensuring it retrieves the correct `CONTENT_TYPE` environment variable. > Exact spelling matters: It is CONTENT_TYPE, not CONTENT_Type or Content-Type. Environment variables in CGI are case-sensitive. --- lib/pure/cgi.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pure/cgi.nim b/lib/pure/cgi.nim index 3d5d4d932e..8783b3389b 100644 --- a/lib/pure/cgi.nim +++ b/lib/pure/cgi.nim @@ -128,7 +128,7 @@ proc getContentLength*(): string = proc getContentType*(): string = ## Returns contents of the `CONTENT_TYPE` environment variable. - return getEnv("CONTENT_Type") + return getEnv("CONTENT_TYPE") proc getDocumentRoot*(): string = ## Returns contents of the `DOCUMENT_ROOT` environment variable. From 8b44b9d9ae8b9be9cebd47d7e6dfdd79fe9b9092 Mon Sep 17 00:00:00 2001 From: Tomohiro Date: Wed, 22 Apr 2026 15:06:03 +0900 Subject: [PATCH 11/23] fixes #23668; Create a new std/nre2 module using Nim Regex replaces re and nre (#25696) std/nre2 is implemented using https://github.com/nitely/nim-regex. std/nre2 has almost same features as std/nre but some regular expressions supported by std/nre are not supported. The syntax of regular expressions of Nim Regex is explained in: https://nitely.github.io/nim-regex/regex.html --- changelog.md | 5 + lib/impure/nre.nim | 9 +- lib/impure/re.nim | 4 + lib/std/nre2.nim | 344 ++++++++++++++++++++++++++++++++++++++++ lib/std/nre2.nims | 14 ++ tests/stdlib/tnre2.nim | 196 +++++++++++++++++++++++ tests/stdlib/tnre2.nims | 3 + 7 files changed, 573 insertions(+), 2 deletions(-) create mode 100644 lib/std/nre2.nim create mode 100644 lib/std/nre2.nims create mode 100644 tests/stdlib/tnre2.nim create mode 100644 tests/stdlib/tnre2.nims diff --git a/changelog.md b/changelog.md index 53e0c0d476..aa0485975e 100644 --- a/changelog.md +++ b/changelog.md @@ -66,12 +66,17 @@ errors. Modes include `Nim` (default, fully compatible) and two new experimental modes: `Lax` and `Gnu` for different option parsing behaviors. +- `std/nre2` is added to replace deprecated NRE. + [//]: # "Changes:" - `std/math` The `^` symbol now supports floating-point as exponent in addition to the Natural type. - `min`, `max`, and `sequtils`' `minIndex`, `maxIndex` and `minmax` for `openArray`s now accept a comparison function. - `system.substr` implementation now uses `copymem` (wrapped C `memcpy`) for copying data, if available at compilation. - `system.newStringUninit` is now considered free of side-effects allowing it to be used with `--experimental:strictFuncs`. +- `std/re` and `std/nre` are deprecated as PCRE library is obsolete. + Use https://github.com/nitely/nim-regex or `std/nre2`. + See: https://github.com/nim-lang/Nim/issues/23668. ## Language changes diff --git a/lib/impure/nre.nim b/lib/impure/nre.nim index 8c712c4a6c..adc2ceb22d 100644 --- a/lib/impure/nre.nim +++ b/lib/impure/nre.nim @@ -9,6 +9,11 @@ when defined(js): {.error: "This library needs to be compiled with a c-like backend, and depends on PCRE; See jsre for JS backend.".} +## .. warning:: NRE is deprecated. +## Use [Regex](https://github.com/nitely/nim-regex) or +## `NRE2 `_ that wraps Regex so that you can easily replace NRE. +## PCRE library is now at end of life. +## ## What is NRE? ## ============ ## @@ -84,7 +89,7 @@ type Regex* = ref RegexDesc ## Represents the pattern that things are matched against, constructed with ## `re(string)`. Examples: `re"foo"`, `re(r"(*ANYCRLF)(?x)foo # - ## comment".` + ## comment")` ## ## `pattern: string` ## : the string that was used to create the pattern. For details on how @@ -154,7 +159,7 @@ type ## will need to pass these as separate flags to PCRE. RegexMatch* = object - ## Usually seen as Option[RegexMatch], it represents the result of an + ## Usually seen as `Option[RegexMatch]`, it represents the result of an ## execution. On failure, it is none, on success, it is some. ## ## `pattern: Regex` diff --git a/lib/impure/re.nim b/lib/impure/re.nim index b39135779b..72d01b9527 100644 --- a/lib/impure/re.nim +++ b/lib/impure/re.nim @@ -10,6 +10,10 @@ when defined(js): {.error: "This library needs to be compiled with a c-like backend, and depends on PCRE; See jsre for JS backend.".} +## .. warning:: This module is deprecated. +## Use [Regex](https://github.com/nitely/nim-regex). +## PCRE library is now at end of life. +## ## Regular expression support for Nim. ## ## This module is implemented by providing a wrapper around the diff --git a/lib/std/nre2.nim b/lib/std/nre2.nim new file mode 100644 index 0000000000..60ff977c60 --- /dev/null +++ b/lib/std/nre2.nim @@ -0,0 +1,344 @@ +# +# Nim's Runtime Library +# (c) Copyright 2026 Nim Contributors +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +## What is NRE2? +## ============= +## +## A regular expression library for Nim to replace deprecated NRE. +## It is implemented with `Regex`_ , +## that is pure Nim regex engine and guarantees linear time matching. +## It supports compiling regex and matching at compile-time and +## works with JS backend. +## +## NRE2 is mostly compatible with NRE and the syntax of regular expression is similar to PCRE. +## But it lacks a few features and how to set options in a pattern is different. +## +## The syntax of regular expression is explained in https://nitely.github.io/nim-regex/regex.html +runnableExamples: + import std/sugar + let vowels = re"[aeoui]" + let bounds = collect: + for match in "moiga".findIter(vowels): match.matchBounds + assert bounds == @[1 .. 1, 2 .. 2, 4 .. 4] + from std/sequtils import toSeq + let s = sequtils.toSeq("moiga".findIter(vowels)) + # fully qualified to avoid confusion with nre.toSeq + assert s.len == 3 + + let firstVowel = "foo".find(vowels) + let hasVowel = firstVowel.isSome() + assert hasVowel + let matchBounds = firstVowel.get().captureBounds[-1] + assert matchBounds.a == 1 + + # as with module `re`, unless specified otherwise, `start` parameter in each + # proc indicates where the scan starts, but outputs are relative to the start + # of the input string, not to `start`: + assert find("uxabc", re"(?<=x|y)ab", start = 1).get.captures[-1] == "ab" + assert find("uxabc", re"ab", start = 3).isNone + +import std/[options, tables] +import regex, regex/nfatype + +export options +export regex.RegexFlags, regex.RegexError + +type + Regex* = regex.Regex2 + ## Represents the pattern that things are matched against, constructed with + ## `re(string)`. Examples: `re"foo"`, `re(r"(?x)foo #comment")` + ## + ## `captureCount: int` + ## : the number of captures that the pattern has. + ## + ## `captureNameId: Table[string, int]` + ## : a table from the capture names to their numeric id. + ## + ## The syntax of regular expression of Regex is explained in https://nitely.github.io/nim-regex/regex.html + + RegexMatch* = object + ## Usually seen as `Option[RegexMatch]`, it represents the result of an + ## execution. On failure, it is none, on success, it is some. + ## + ## `str: string` + ## : the string that was matched against + ## + ## `captures[]: string` + ## : the string value of whatever was captured at that id. If the value + ## is invalid, then behavior is undefined. If the id is `-1`, then + ## the whole match is returned. If the given capture was not matched, + ## `nil` is returned. See examples for `match`. + ## + ## `captureBounds[]: HSlice[int, int]` + ## : gets the bounds of the given capture according to the same rules as + ## the above. If the capture is not filled, then `None` is returned. + ## The bounds are both inclusive. See examples for `match`. + ## + ## `match: string` + ## : the full text of the match. + ## + ## `matchBounds: HSlice[int, int]` + ## : the bounds of the match, as in `captureBounds[]` + ## + ## `(captureBounds|captures).toTable` + ## : returns a table with each named capture as a key. + ## + ## `(captureBounds|captures).toSeq` + ## : returns all the captures by their number. + ## + ## `$: string` + ## : same as `match` + str*: string ## The string that was matched against. + matchImpl: regex.RegexMatch2 + + Captures* {.borrow: `.`.} = distinct RegexMatch + CaptureBounds* {.borrow: `.`.} = distinct RegexMatch + +func captureCount*(pattern: Regex): int {.inline.} = + pattern.toRegex().groupsCount + +func captureNameId*(pattern: Regex): Table[string, int] = + result = initTable[string, int](pattern.toRegex().namedGroups.len) + for k, v in pattern.toRegex().namedGroups: + result[k] = v + +func captureBounds*(match: RegexMatch): CaptureBounds {.inline.} = + CaptureBounds(match) + +func captures*(match: RegexMatch): Captures {.inline.} = + Captures(match) + +func contains*(match: Captures or CaptureBounds, i: int): bool {.inline.} = + i >= -1 and i < match.matchImpl.groupsCount and match.matchImpl.group(i) != reNonCapture + +func len*(match: Captures or CaptureBounds): int {.inline.} = + ## Return the number of capturing groups + match.matchImpl.groupsCount + +func `[]`*(match: CaptureBounds; i: int): HSlice[int, int] {.inline.} = + if i == -1: match.matchImpl.boundaries else: match.matchImpl.group(i) + +func `[]`*(match: CaptureBounds; name: string): HSlice[int, int] {.inline.} = + result = match.matchImpl.group(name) + if result == reNonCapture: + raise newException(KeyError, "Group '" & name & "' was not captured") + +func `[]`*(match: Captures; i: int): string {.inline.} = + match.str[CaptureBounds(match)[i]] + +func `[]`*(match: Captures, name: string): string {.inline.} = + match.str[CaptureBounds(match)[name]] + +func match*(match: RegexMatch): string {.inline.} = + match.str[match.matchImpl.boundaries] + +func matchBounds*(match: RegexMatch): HSlice[int, int] {.inline.} = + match.matchImpl.boundaries + +func contains*(match: CaptureBounds or Captures, name: string): bool {.inline.} = + name in match.matchImpl.namedGroups and + match.matchImpl.group(name) != reNonCapture + +func toTable*(match: Captures): Table[string, string] = + result = initTable[string, string]() + for k, i in match.matchImpl.namedGroups: + let r = match.matchImpl.group(i) + if r != reNonCapture: + result[k] = match.str[r] + +func toTable*(match: CaptureBounds): Table[string, HSlice[int, int]] = + result = initTable[string, HSlice[int, int]]() + for k, i in match.matchImpl.namedGroups: + let r = match.matchImpl.group(i) + if r != reNonCapture: + result[k] = match.matchImpl.group(i) + +iterator items*(match: CaptureBounds; default = none(HSlice[int, int])): Option[HSlice[int, int]] = + for i in 0 ..< match.len: + yield if i in match: some(match[i]) else: default + +iterator items*(match: Captures; default = none(string)): Option[string] = + for i in 0 ..< match.len: + yield if i in match: some(match[i]) else: default + +func toSeq*(match: CaptureBounds; + default = none(HSlice[int, int])): seq[Option[HSlice[int, int]]] = + result = @[] + for it in match.items(default): result.add it + +func toSeq*(match: Captures; + default: Option[string] = none(string)): seq[Option[string]] = + result = @[] + for it in match.items(default): result.add it + +func `$`*(match: RegexMatch): string = + match.match + +func re*(pattern: static string; flags: static RegexFlags = {}): static[Regex2] = + ## Parse and compile a regular expression at compile-time + result = regex.re2(pattern, flags) + +func re*(pattern: string; flags: RegexFlags = {}): Regex = + ## Parse and compile a regular expression at run-time + result = regex.re2(pattern, flags) + +func match*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[RegexMatch] = + ## Like `find(...)<#find,string,Regex,int>`_, but anchored to the start of the + ## string. + runnableExamples: + assert "foo".match(re"f").isSome + assert "foo".match(re"o").isNone + + assert "abc".match(re"(\w)").get.captures[0] == "a" + assert "abc".match(re"(?P\w)").get.captures["letter"] == "a" + assert "abc".match(re"(\w)\w").get.captures[-1] == "ab" + + assert "abc".match(re"(\w)").get.captureBounds[0] == 0 .. 0 + assert 0 in "abc".match(re"(\w)").get.captureBounds + assert "abc".match(re"").get.captureBounds[-1] == 0 .. -1 + assert "abc".match(re"abc").get.captureBounds[-1] == 0 .. 2 + var mat = default(RegexMatch) + let r = regex.startsWith(str.toOpenArray(0, min(str.high, endpos)), pattern, mat.matchImpl, start) + if r: + mat.str = str + some(mat) + else: + none(RegexMatch) + +iterator findIter*(str: string; pattern: Regex; start = 0, endpos = int.high): RegexMatch = + ## Works the same as `find(...)<#find,string,Regex,int>`_, but finds every + ## non-overlapping match: + runnableExamples: + import std/sugar + assert collect(for a in "2222".findIter(re"22"): a.match) == @["22", "22"] + # not @["22", "22", "22"] + ## Arguments are the same as `find(...)<#find,string,Regex,int>`_ + ## + ## Variants: + ## + ## - `proc findAll(...)` returns a `seq[string]` + var mat = RegexMatch(str: str) + # TODO: + # needs following PR to remove `substr` call. + # https://github.com/nitely/nim-regex/pull/162 + for m in regex.findAll(str.substr(start, endpos), pattern): + mat.matchImpl = m + yield mat + +proc find*(str: string; pattern: Regex; start = 0; endpos = int.high): Option[RegexMatch] = + ## Finds the given pattern in the string between the end and start + ## positions. + ## + ## `start` + ## : The start point at which to start matching. `|abc` is `0`; + ## `a|bc` is `1` + ## + ## `endpos` + ## : The maximum index for a match; `int.high` means the end of the + ## string, otherwise it’s an inclusive upper bound. + var mat = default(RegexMatch) + let r = regex.find(str.substr(start, endpos), pattern, mat.matchImpl) + + # remove following code after regex.find get `start`/`last` parameter + for v in mat.matchImpl.captures.mitems: + v.a += start + v.b += start + mat.matchImpl.boundaries.a += start + mat.matchImpl.boundaries.b += start + + if r: + mat.str = str + some(mat) + else: + none(RegexMatch) + +proc findAll*(str: string; pattern: Regex; start = 0; endpos = int.high): seq[string] = + result = @[] + for match in str.findIter(pattern, start, endpos): + result.add(match.match) + +proc contains*(str: string; pattern: Regex; start = 0; endpos = int.high): bool = + ## Determine if the string contains the given pattern between the end and + ## start positions: + ## This function is equivalent to `isSome(str.find(pattern, start, endpos))`. + runnableExamples: + assert "abc".contains(re"bc") + assert not "abc".contains(re"cd") + assert not "abc".contains(re"a", start = 1) + + isSome(str.find(pattern, start, endpos)) + +proc split*(str: string; pattern: Regex; maxSplit = -1; start = 0): seq[string] = + ## Splits the string with the given regex. This works according to the + ## rules that Perl and Javascript use. + ## + ## `start` behaves the same as in `find(...)<#find,string,Regex,int>`_. + ## + runnableExamples: + # - If the match is zero-width, then the string is still split: + assert "123".split(re"") == @["1", "2", "3"] + + # - If the pattern has a capture in it, it is added after the string + # split: + assert "12".split(re"(\d)") == @["", "1", "", "2", ""] + + # - If `maxsplit != -1`, then the string will only be split + # `maxsplit - 1` times. This means that there will be `maxsplit` + # strings in the output seq. + assert "1.2.3".split(re"\.", maxsplit = 2) == @["1", "2.3"] + + result = splitIncl(str, pattern, maxSplit, start) + +proc replace*(str: string; pattern: Regex; + subproc: proc (match: RegexMatch): string): string = + ## Replaces each match of Regex in the string with `subproc`, which should + ## never be or return `nil`. + ## + ## If `subproc` is a `proc (RegexMatch): string`, then it is executed with + ## each match and the return value is the replacement value. + ## + ## If `subproc` is a `proc (string): string`, then it is executed with the + ## full text of the match and the return value is the replacement value. + ## + ## If `subproc` is a string, the syntax is as follows: + ## + ## - `$$` - literal `$` + ## - `$123` - capture number `123` + ## - `$1$#` - first and second captures + ## - `$#` - first capture + ## + ## Following syntax is not supported in NRE2 + ## + ## - `$foo` - named capture `foo` + ## - `${foo}` - same as above + ## - `$0` - full match + ## + ## If a given capture is missing, `ValueError` is thrown. + proc by(m: RegexMatch2, s: string): string = + let mat = RegexMatch(str: s, matchImpl: m) + result = subproc(mat) + + result = regex.replace(str, pattern, by) + +proc replace*(str: string; pattern: Regex; + subproc: proc (match: string): string): string = + proc by(m: RegexMatch2; s: string): string = + result = subproc(s) + + result = regex.replace(str, pattern, by) + +proc replace*(str: string; pattern: Regex; sub: string): string = + result = regex.replace(str, pattern, sub) + +func escapeRe*(str: string): string = + ## Escapes the string so it doesn't match any special characters. + runnableExamples: + assert escapeRe("fly+wind") == "fly\\+wind" + assert escapeRe("nim*") == "nim\\*" + + result = regex.escapeRe(str) diff --git a/lib/std/nre2.nims b/lib/std/nre2.nims new file mode 100644 index 0000000000..1286aaa33a --- /dev/null +++ b/lib/std/nre2.nims @@ -0,0 +1,14 @@ +import std/os + +if getCommand() == "doc": + # std/nre2 requires nim-regex and it requires nim-unicodedb. + # when build documentation on CI, git clone them as nimble is not available + + const PkgDir = "build/deps" + const Pkgs = ["nim-regex", "nim-unicodedb"] + + for n in Pkgs: + if not dirExists(PkgDir / n): + exec("git clone -q https://github.com/nitely/" & n & " " & (PkgDir / n)) + + switch("path", "$nim" / PkgDir / n / "src") diff --git a/tests/stdlib/tnre2.nim b/tests/stdlib/tnre2.nim new file mode 100644 index 0000000000..6cea0f8114 --- /dev/null +++ b/tests/stdlib/tnre2.nim @@ -0,0 +1,196 @@ +import std/[assertions, options, sequtils, strutils, tables] +import std/nre2 + +block: + let pattern = "[0-9" + doAssertRaises(RegexError): discard re(pattern) + +block: # captures + block: # capture bounds are correct + let ex1 = re("([0-9])") + doAssert "1 23".find(ex1).get.matchBounds == 0 .. 0 + doAssert "1 23".find(ex1).get.captureBounds[0] == 0 .. 0 + doAssert "1 23".find(ex1, 1).get.matchBounds == 2 .. 2 + doAssert "1 23".find(ex1, 3).get.matchBounds == 3 .. 3 + + let ex2 = re("()()()()()()()()()()([0-9])") + doAssert "824".find(ex2).get.captureBounds[0] == 0 .. -1 + doAssert "824".find(ex2).get.captureBounds[10] == 0 .. 0 + + let ex3 = re("([0-9]+)") + doAssert "824".find(ex3).get.captureBounds[0] == 0 .. 2 + + block: # named captures + let ex1 = "foobar".find(re("(?Pfoo)(?Pbar)")) + doAssert ex1.get.captures["foo"] == "foo" + doAssert ex1.get.captures["bar"] == "bar" + + let ex2 = "foo".find(re("(?Pfoo)(?Pbar)?")) + doAssert "foo" in ex2.get.captureBounds + doAssert ex2.get.captures["foo"] == "foo" + doAssert not ("bar" in ex2.get.captures) + doAssertRaises(KeyError): + discard ex2.get.captures["bar"] + + block: # named capture bounds + let ex1 = "foo".find(re("(?Pfoo)(?Pbar)?")) + doAssert "foo" in ex1.get.captureBounds + doAssert ex1.get.captureBounds["foo"] == 0..2 + doAssert not ("bar" in ex1.get.captures) + doAssertRaises(KeyError): + discard ex1.get.captureBounds["bar"] + + block: # capture count + let ex1 = re("(?Pfoo)(?Pbar)?") + doAssert ex1.captureCount == 2 + doAssert ex1.captureNameId == {"foo" : 0, "bar" : 1}.toTable() + + block: # named capture table + let ex1 = "foo".find(re("(?Pfoo)(?Pbar)?")) + doAssert ex1.get.captures.toTable == {"foo" : "foo"}.toTable() + doAssert ex1.get.captureBounds.toTable == {"foo" : 0..2}.toTable() + + let ex2 = "foobar".find(re("(?Pfoo)(?Pbar)?")) + doAssert ex2.get.captures.toTable == {"foo" : "foo", "bar" : "bar"}.toTable() + + block: # capture sequence + let ex1 = "foo".find(re("(?Pfoo)(?Pbar)?")) + doAssert ex1.get.captures.toSeq == @[some("foo"), none(string)] + doAssert ex1.get.captureBounds.toSeq == @[some(0..2), none(Slice[int])] + doAssert ex1.get.captures.toSeq(some("")) == @[some("foo"), some("")] + + let ex2 = "foobar".find(re("(?Pfoo)(?Pbar)?")) + doAssert ex2.get.captures.toSeq == @[some("foo"), some("bar")] + +block: # match + block: # upper bound must be inclusive + doAssert "abc".match(re"abc", endpos = -1) == none(RegexMatch) + doAssert "abc".match(re"abc", endpos = 1) == none(RegexMatch) + doAssert "abc".match(re"abc", endpos = 2) != none(RegexMatch) + + block: # match examples + doAssert "abc".match(re"(\w)").get.captures[0] == "a" + doAssert "abc".match(re"(?P\w)").get.captures["letter"] == "a" + doAssert "abc".match(re"(\w)\w").get.captures[-1] == "ab" + doAssert "abc".match(re"(\w)").get.captureBounds[0] == 0 .. 0 + doAssert "abc".match(re"").get.captureBounds[-1] == 0 .. -1 + doAssert "abc".match(re"abc").get.captureBounds[-1] == 0 .. 2 + + let cap1 = "abc".match(re"(\w)(\w)+").get.captures + doAssert cap1.len == 2 + doAssert 0 in cap1 + doAssert 1 in cap1 + doAssert cap1[0] == "a" and cap1[1] == "c" + doAssert 0 in "abc".match(re"(\w)+").get.captureBounds + + block: # match test cases + doAssert "123".match(re"").get.matchBounds == 0 .. -1 + let mat1 = "123".match(re"123").get + doAssert mat1.matchBounds == 0 .. 2 + doAssert mat1.match == "123" + +block: # find + block: # find text + doAssert "3213a".find(re"[a-z]").get.match == "a" + doAssert sequtils.toSeq(findIter("1 2 3 4 5 6 7 8 ", re" ")).mapIt( + it.match + ) == @[" ", " ", " ", " ", " ", " ", " ", " "] + + block: # find bounds + doAssert sequtils.toSeq(findIter("1 2 3 4 5 ", re" ")).mapIt( + it.matchBounds + ) == @[1..1, 3..3, 5..5, 7..7, 9..9] + + block: # overlapping find + doAssert "222".findAll(re"22") == @["22"] + doAssert "2222".findAll(re"22") == @["22", "22"] + + block: # len 0 find + doAssert "".findAll(re"\ ") == newSeq[string]() + doAssert "".findAll(re"") == @[""] + doAssert "abc".findAll(re"") == @["", "", "", ""] + doAssert "word word".findAll(re"\b") == @["", "", "", ""] + doAssert "word\r\lword".findAll(re"(?m)$") == @["", ""] + doAssert "слово слово".findAll(re"\b") == @["", "", "", ""] + +block: # contains + doAssert "abc".contains(re"bc") + doAssert not "abc".contains(re"cd") + doAssert not "abc".contains(re"a", start = 1) + +block: # string splitting + block: # splitting strings + doAssert "1 2 3 4 5 6 ".split(re" ") == @["1", "2", "3", "4", "5", "6", ""] + doAssert "1 2 ".split(re(" ")) == @["1", "", "2", "", ""] + doAssert "1 2".split(re(" ")) == @["1", "2"] + doAssert "foo".split(re("foo")) == @["", ""] + doAssert "".split(re"foo") == @[""] + doAssert "9".split(re"\son\s") == @["9"] + + block: # captured patterns + doAssert "12".split(re"(\d)") == @["", "1", "", "2", ""] + + block: # maxsplit + doAssert "123".split(re"", maxsplit = 2) == @["1", "23"] + doAssert "123".split(re"", maxsplit = 1) == @["123"] + doAssert "123".split(re"", maxsplit = -1) == @["1", "2", "3"] + doAssert "1 2 3".split(re" ", maxsplit = 1) == @["1 2 3"] + doAssert "1 2 3".split(re" ", maxsplit = 2) == @["1", "2 3"] + doAssert "1 2 3".split(re"( )", maxsplit = 2) == @["1", " ", "2 3"] + + block: # split with 0-length match + doAssert "12345".split(re("")) == @["1", "2", "3", "4", "5"] + doAssert "".split(re"") == newSeq[string]() + doAssert "word word".split(re"\b") == @["word", " ", "word"] + #doAssert "word\r\lword".split(re"(?m)$") == @["word", "\r\lword"] + doAssert "слово слово".split(re"(\b)") == @["слово", "", " ", "", "слово", ""] + + block: # perl split tests + doAssert "forty-two" .split(re"") .join(",") == "f,o,r,t,y,-,t,w,o" + doAssert "forty-two" .split(re"", 3) .join(",") == "f,o,rty-two" + doAssert "split this string" .split(re" ") .join(",") == "split,this,string" + doAssert "split this string" .split(re" ", 2) .join(",") == "split,this string" + doAssert "try$this$string" .split(re"\$") .join(",") == "try,this,string" + doAssert "try$this$string" .split(re"\$", 2) .join(",") == "try,this$string" + doAssert "comma, separated, values" .split(re", ") .join("|") == "comma|separated|values" + doAssert "comma, separated, values" .split(re", ", 2) .join("|") == "comma|separated, values" + doAssert "Perl6::Camelia::Test" .split(re"::") .join(",") == "Perl6,Camelia,Test" + doAssert "Perl6::Camelia::Test" .split(re"::", 2) .join(",") == "Perl6,Camelia::Test" + doAssert "split,me,please" .split(re",") .join("|") == "split|me|please" + doAssert "split,me,please" .split(re",", 2) .join("|") == "split|me,please" + doAssert "Hello World Goodbye Mars".split(re"\s+") .join(",") == "Hello,World,Goodbye,Mars" + doAssert "Hello World Goodbye Mars".split(re"\s+", 3).join(",") == "Hello,World,Goodbye Mars" + doAssert "Hello test" .split(re"(\s+)") .join(",") == "Hello, ,test" + doAssert "this will be split" .split(re" ") .join(",") == "this,will,be,split" + doAssert "this will be split" .split(re" ", 3) .join(",") == "this,will,be split" + doAssert "a.b" .split(re"\.") .join(",") == "a,b" + doAssert "" .split(re"") .len == 0 + doAssert ":" .split(re"") .len == 1 + + block: # start position + doAssert "abc".split(re"", start = 1) == @["b", "c"] + doAssert "abc".split(re"", start = 2) == @["c"] + doAssert "abc".split(re"", start = 3) == newSeq[string]() + doAssert "abc".split(re"^b", start = 1) == @["bc"] + +block: # replace + block: # replace with 0-length strings + doAssert "".replace(re"1", proc (v: RegexMatch): string = "1") == "" + doAssert " ".replace(re"", proc (v: RegexMatch): string = "1") == "1 1" + doAssert "".replace(re"", proc (v: RegexMatch): string = "1") == "1" + + block: # regular replace + doAssert "123".replace(re"\d", "foo") == "foofoofoo" + doAssert "123".replace(re"(\d)", "$1$1") == "112233" + doAssert "123".replace(re"(\d)(\d)", "$1$2") == "123" + doAssert "123".replace(re"(\d)(\d)", "$#$#") == "123" + doAssert "abcdefghijklm".replace(re"(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)(k)(l)(m)", "$12") == "l" + + block: # replacing missing captures should throw instead of segfaulting + doAssertRaises(ValueError): discard "ab".replace(re"(a)", "$1$2") + +block: # escape strings + block: # escape strings + doAssert "123".escapeRe() == "123" + doAssert "[]".escapeRe() == r"\[\]" + doAssert "()".escapeRe() == r"\(\)" diff --git a/tests/stdlib/tnre2.nims b/tests/stdlib/tnre2.nims new file mode 100644 index 0000000000..ea30b440ce --- /dev/null +++ b/tests/stdlib/tnre2.nims @@ -0,0 +1,3 @@ +# std/nre2 requires nim-regex and it requires nim-unicodedb +exec("nimble --nimbleDir:build/deps install unicodedb@#head") +exec("nimble --nimbleDir:build/deps install regex@#head") From 148e82f41878e5639820739cd39bb390009c0dfe Mon Sep 17 00:00:00 2001 From: Jake Leahy Date: Fri, 24 Apr 2026 22:04:30 +1000 Subject: [PATCH 12/23] Add Nix certificate path to ssl_certs.nim (#25763) This makes it easier to run Nix built containers for Nim programs since by default Nim doesn't search environment variables for SSL certs so its a little annoying having to move around files - https://github.com/NixOS/nixpkgs/blob/10e7ad5bbcb421fe07e3a4ad53a634b0cd57ffac/pkgs/by-name/ca/cacert/package.nix#L85 --- lib/pure/ssl_certs.nim | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pure/ssl_certs.nim b/lib/pure/ssl_certs.nim index d60cd22eb6..62289df158 100644 --- a/lib/pure/ssl_certs.nim +++ b/lib/pure/ssl_certs.nim @@ -36,6 +36,8 @@ elif defined(linux): # Android "/data/data/com.termux/files/usr/etc/tls/cert.pem", "/system/etc/security/cacerts", + # Nix + "/etc/ssl/certs/ca-bundle.crt" ] elif defined(bsd): const certificatePaths = [ From cbe8ce59ed205f6f8018c0dcc2a114d74cb2aff5 Mon Sep 17 00:00:00 2001 From: Zoom Date: Sat, 25 Apr 2026 14:27:13 +0400 Subject: [PATCH 13/23] fix string setLenUninit growth without realloc for refc (#25767) `setLenUninit(string)` was broken on the legacy refc backend when growing within existing spare capacity. `setLengthStrUninit` in `lib/system/sysstr.nim` only updated len when it had to reallocate or when shrinking. If oldLen < newLen <= capacity, it returned early without finalizing: ```nim var s = newStringOfCap(10) s.add("abc") s.setLenUninit(6) doAssert s.len == 6 # used to fail, len stayed 3 ``` This escaped `tests/stdlib/tstring.nim` because the testing routine `checkSetLenUninit` mostly resizes strings created at **exact** length/capacity, so growth usually took the reallocating branch. The new regression test covers the missing edge case. So sorry for catching this only on the day of the stable release! In my defense, the original PR hung in limbo for quite a while and it didn't spend enough time in devel after the merge. --- lib/system/sysstr.nim | 2 +- tests/stdlib/tstring.nim | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/system/sysstr.nim b/lib/system/sysstr.nim index 9ecdffb669..c879558dd0 100644 --- a/lib/system/sysstr.nim +++ b/lib/system/sysstr.nim @@ -264,7 +264,7 @@ proc setLengthStrUninit(s: var string, newlen: Natural) {.nodestroy.} = str.data[n] = '\0' str.len = n s = cast[string](str) - elif n < s.len: + elif n != s.len: str.data[n] = '\0' str.len = n else: return diff --git a/tests/stdlib/tstring.nim b/tests/stdlib/tstring.nim index 724eef4314..fad3865085 100644 --- a/tests/stdlib/tstring.nim +++ b/tests/stdlib/tstring.nim @@ -176,6 +176,13 @@ proc main() = result.add char(ord('a') + i mod 26) proc checkSetLenUninit(oldLen, newLen: int; cmpAfter = -1) = + ## Verifies `setLenUninit`: + ## - preserves the existing prefix + ## - updates the string length + ## - keeps internal null termination valid for both shrink and growth + ## + ## `cmpAfter` is used for layouts where trailing zeroed padding affects + ## string comparison semantics after the resize. var s = makeStr(oldLen) let prefixLen = min(oldLen, newLen) let prefix = makeStr(prefixLen) @@ -203,9 +210,18 @@ proc main() = block setLenUninit: # Shared baseline for both SSO and V2: noop, shrink, grow. - checkSetLenUninit(numbers.len, numbers.len) - checkSetLenUninit(numbers.len, 5) - checkSetLenUninit(numbers.len, 11) + checkSetLenUninit(10, 10) + checkSetLenUninit(10, 5) + checkSetLenUninit(10, 11) + + block growingWithinBiggerCapacity: + # Strings can reserve spare capacity even for short strings. + # Growing within that capacity must still update len and the trailing zero. + var s = newStringOfCap(10) + s.add("abc") + s.setLenUninit(6) + s.checkStrInternals(6) + doAssert s[0..2] == "abc" when hasNativeSso: const From 49b5e66d3a886e30019eafe62f8e8dfbf3f7ce7c Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 27 Apr 2026 18:00:29 +0200 Subject: [PATCH 14/23] SSO: better switch to enable it (#25772) --- compiler/ccgcalls.nim | 12 ++-- compiler/ccgexprs.nim | 18 +++--- compiler/ccgliterals.nim | 2 +- compiler/ccgstmts.nim | 2 +- compiler/cgen.nim | 6 +- compiler/commands.nim | 19 ++++++ compiler/liftdestructors.nim | 2 +- compiler/nim.nim | 5 ++ compiler/options.nim | 6 ++ lib/pure/streams.nim | 12 ++-- lib/std/formatfloat.nim | 6 +- lib/std/strbasics.nim | 2 +- lib/std/syncio.nim | 2 +- lib/system.nim | 5 +- lib/system/strs_v2.nim | 16 +++-- lib/system/strs_v3.nim | 119 ++++++++++++++++++++++------------- tests/stdlib/tstring.nim | 2 +- tests/system/tnimsso.nim | 2 +- 18 files changed, 154 insertions(+), 84 deletions(-) diff --git a/compiler/ccgcalls.nim b/compiler/ccgcalls.nim index b0964f97be..b2521069d4 100644 --- a/compiler/ccgcalls.nim +++ b/compiler/ccgcalls.nim @@ -230,11 +230,11 @@ proc genOpenArraySlice(p: BProc; q: PNode; formalType, destType: PType; prepareF of tyString, tySequence: let atyp = skipTypes(a.t, abstractInst) if formalType.skipTypes(abstractInst).kind in {tyVar} and atyp.kind == tyString and - optSeqDestructors in p.config.globalOptions and not p.config.isDefined("nimsso"): + optSeqDestructors in p.config.globalOptions and not p.config.usesSso(): let bra = byRefLoc(p, a) p.s(cpsStmts).addCallStmt(cgsymValue(p.module, "nimPrepareStrMutationV2"), bra) - if p.config.isDefined("nimsso") and + if p.config.usesSso() and skipTypes(a.t, abstractVar + abstractInst).kind == tyString: let strPtr = if atyp.kind in {tyVar} and not compileToCpp(p.module): ra else: addrLoc(p.config, a) @@ -296,11 +296,11 @@ proc openArrayLoc(p: BProc, formalType: PType, n: PNode; result: var Builder) = of tyString, tySequence: let ntyp = skipTypes(n.typ, abstractInst) if formalType.skipTypes(abstractInst).kind in {tyVar} and ntyp.kind == tyString and - optSeqDestructors in p.config.globalOptions and not p.config.isDefined("nimsso"): + optSeqDestructors in p.config.globalOptions and not p.config.usesSso(): let bra = byRefLoc(p, a) p.s(cpsStmts).addCallStmt(cgsymValue(p.module, "nimPrepareStrMutationV2"), bra) - if p.config.isDefined("nimsso") and + if p.config.usesSso() and skipTypes(n.typ, abstractVar + abstractInst).kind == tyString: if ntyp.kind in {tyVar} and not compileToCpp(p.module): let ra = a.rdLoc @@ -335,7 +335,7 @@ proc openArrayLoc(p: BProc, formalType: PType, n: PNode; result: var Builder) = let ra = a.rdLoc var t = TLoc(snippet: cDeref(ra)) let lt = lenExpr(p, t) - if p.config.isDefined("nimsso"): + if p.config.usesSso(): result.add(cCall(cgsymValue(p.module, "nimStrData"), ra)) result.addArgumentSeparator() result.add(cCall(cgsymValue(p.module, "nimStrLen"), t.snippet)) @@ -370,7 +370,7 @@ proc expressionsNeedsTmp(p: BProc, a: TLoc): TLoc = proc genArgStringToCString(p: BProc, n: PNode; result: var Builder; needsTmp: bool) {.inline.} = var a = initLocExpr(p, n[0]) let tmp = withTmpIfNeeded(p, a, needsTmp) - let ra = if p.config.isDefined("nimsso"): byRefLoc(p, tmp) else: tmp.rdLoc + let ra = if p.config.usesSso(): byRefLoc(p, tmp) else: tmp.rdLoc result.addCall(cgsymValue(p.module, "nimToCStringConv"), ra) proc genArg(p: BProc, n: PNode, param: PSym; call: PNode; result: var Builder; needsTmp = false) = diff --git a/compiler/ccgexprs.nim b/compiler/ccgexprs.nim index b4edfcf6dd..2cf187e687 100644 --- a/compiler/ccgexprs.nim +++ b/compiler/ccgexprs.nim @@ -322,7 +322,7 @@ proc genOpenArrayConv(p: BProc; d: TLoc; a: TLoc; flags: TAssignmentFlags) = bra) let rd = d.rdLoc let la = lenExpr(p, a) - if p.config.isDefined("nimsso"): + if p.config.usesSso(): let bra = byRefLoc(p, a) p.s(cpsStmts).addFieldAssignment(rd, "Field0", cCall(cgsymValue(p.module, "nimStrData"), bra)) @@ -963,7 +963,7 @@ proc genDeref(p: BProc, e: PNode, d: var TLoc) = proc cowBracket(p: BProc; n: PNode) = if n.kind == nkBracketExpr and optSeqDestructors in p.config.globalOptions and - not p.config.isDefined("nimsso"): + not p.config.usesSso(): let strCandidate = n[0] if strCandidate.typ.skipTypes(abstractInst).kind == tyString: var a: TLoc = initLocExpr(p, strCandidate) @@ -989,7 +989,7 @@ proc genAddr(p: BProc, e: PNode, d: var TLoc) = # bug #19497 d.lode = e else: - let ssoStrSub = p.config.isDefined("nimsso") and e[0].kind == nkBracketExpr and + let ssoStrSub = p.config.usesSso() and e[0].kind == nkBracketExpr and e[0][0].typ.skipTypes(abstractVar).kind == tyString var a: TLoc = initLocExpr(p, e[0], if ssoStrSub: {lfEnforceDeref, lfPrepareForMutation} else: {}) if e[0].kind in {nkHiddenStdConv, nkHiddenSubConv, nkConv} and not ignoreConv(e[0]): @@ -1318,7 +1318,7 @@ proc genSeqElem(p: BProc, n, x, y: PNode, d: var TLoc) = if skipTypes(a.t, abstractVar).kind in {tyRef, tyPtr}: a.snippet = cDeref(a.snippet) - if p.config.isDefined("nimsso") and ty.kind == tyString: + if p.config.usesSso() and ty.kind == tyString: let bra = byRefLoc(p, a) if lfPrepareForMutation in d.flags: # Use nimStrAtMutV3 to get a mutable reference (char*) to the element. @@ -2150,7 +2150,7 @@ proc genRepr(p: BProc, e: PNode, d: var TLoc) = putIntoDest(p, b, e, ra & cArgumentSeparator & ra & "Len_0", a.storage) of tyString, tySequence: let la = lenExpr(p, a) - if p.config.isDefined("nimsso") and + if p.config.usesSso() and skipTypes(a.t, abstractVarRange).kind == tyString: let bra = byRefLoc(p, a) putIntoDest(p, b, e, @@ -2743,7 +2743,7 @@ proc genConv(p: BProc, e: PNode, d: var TLoc) = proc convStrToCStr(p: BProc, n: PNode, d: var TLoc) = var a: TLoc = initLocExpr(p, n[0]) - let arg = if p.config.isDefined("nimsso"): byRefLoc(p, a) else: rdLoc(a) + let arg = if p.config.usesSso(): byRefLoc(p, a) else: rdLoc(a) putIntoDest(p, d, n, cgCall(p, "nimToCStringConv", arg), a.storage) @@ -2822,7 +2822,7 @@ proc genMove(p: BProc; n: PNode; d: var TLoc) = var src: TLoc = initLocExpr(p, n[2]) let destVal = rdLoc(a) let srcVal = rdLoc(src) - if p.config.isDefined("nimsso") and + if p.config.usesSso() and n[1].typ.skipTypes(abstractVar).kind == tyString: # SmallString: destroy dst then struct-copy src; no .p field aliasing needed genStmts(p, n[3]) @@ -2871,7 +2871,7 @@ proc genDestroy(p: BProc; n: PNode) = case t.kind of tyString: var a: TLoc = initLocExpr(p, arg) - if p.config.isDefined("nimsso"): + if p.config.usesSso(): # SmallString: delegate to nimDestroyStrV1 (rc-based, handles static strings) p.s(cpsStmts).addCallStmt(cgsymValue(p.module, "nimDestroyStrV1"), rdLoc(a)) else: @@ -4243,7 +4243,7 @@ proc genBracedInit(p: BProc, n: PNode; isConst: bool; optionalType: PType; resul genConstObjConstr(p, n, isConst, result) of tyString, tyCstring: if optSeqDestructors in p.config.globalOptions and n.kind != nkNilLit and ty == tyString: - if p.config.isDefined("nimsso"): + if p.config.usesSso(): genStringLiteralV3Const(p.module, n, isConst, result) else: genStringLiteralV2Const(p.module, n, isConst, result) diff --git a/compiler/ccgliterals.nim b/compiler/ccgliterals.nim index 54823cc592..0a1586ae29 100644 --- a/compiler/ccgliterals.nim +++ b/compiler/ccgliterals.nim @@ -22,7 +22,7 @@ template detectVersion(field, corename) = result = 1 proc detectStrVersion(m: BModule): int = - if m.g.config.isDefined("nimsso") and + if m.g.config.usesSso() and m.g.config.selectedGC in {gcArc, gcOrc, gcYrc, gcAtomicArc, gcHooks}: result = 3 else: diff --git a/compiler/ccgstmts.nim b/compiler/ccgstmts.nim index 3a2042ae19..a80ef37efd 100644 --- a/compiler/ccgstmts.nim +++ b/compiler/ccgstmts.nim @@ -1940,7 +1940,7 @@ proc genAsgn(p: BProc, e: PNode, fastAsgn: bool) = elif optFieldCheck in p.options and isDiscriminantField(e[0]): genLineDir(p, e) asgnFieldDiscriminant(p, e) - elif p.config.isDefined("nimsso") and e[0].kind == nkBracketExpr and + elif p.config.usesSso() and e[0].kind == nkBracketExpr and e[0][0].typ.skipTypes(abstractVar).kind == tyString: # nimsso: s[i] = c → nimStrPutV3(&s, i, c) (handles COW internally) genLineDir(p, e) diff --git a/compiler/cgen.nim b/compiler/cgen.nim index 537d248103..8d3b486ca3 100644 --- a/compiler/cgen.nim +++ b/compiler/cgen.nim @@ -389,7 +389,7 @@ proc lenField(p: BProc, val: Rope): Rope {.inline.} = proc lenExpr(p: BProc; a: TLoc): Rope = if optSeqDestructors in p.config.globalOptions: - if p.config.isDefined("nimsso") and a.lode != nil and a.t != nil and + if p.config.usesSso() and a.lode != nil and a.t != nil and a.t.skipTypes(abstractInst).kind == tyString: result = cCall(cgsymValue(p.module, "nimStrLen"), rdLoc(a)) else: @@ -534,7 +534,7 @@ proc resetLoc(p: BProc, loc: var TLoc) = let atyp = skipTypes(loc.t, abstractInst) let rl = rdLoc(loc) - if typ.kind == tyString and p.config.isDefined("nimsso"): + if typ.kind == tyString and p.config.usesSso(): # SmallString zero state: bytes=0 (slen=0 in low byte, all inline chars zeroed) if atyp.kind in {tyVar, tyLent}: p.s(cpsStmts).addAssignment(derefField(rl, "bytes"), cIntValue(0)) @@ -592,7 +592,7 @@ proc constructLoc(p: BProc, loc: var TLoc, isTemp = false) = let typ = loc.t if optSeqDestructors in p.config.globalOptions and skipTypes(typ, abstractInst + {tyStatic}).kind in {tyString, tySequence}: let rl = rdLoc(loc) - if skipTypes(typ, abstractInst + {tyStatic}).kind == tyString and p.config.isDefined("nimsso"): + if skipTypes(typ, abstractInst + {tyStatic}).kind == tyString and p.config.usesSso(): # SmallString zero state: bytes=0 (slen=0 in low byte, all inline chars zeroed) p.s(cpsStmts).addFieldAssignment(rl, "bytes", cIntValue(0)) p.s(cpsStmts).addFieldAssignment(rl, "more", NimNil) diff --git a/compiler/commands.nim b/compiler/commands.nim index be5a8abd27..f7de0978ed 100644 --- a/compiler/commands.nim +++ b/compiler/commands.nim @@ -250,6 +250,7 @@ const errGuiConsoleOrLibExpectedButXFound = "'gui', 'console', 'lib' or 'staticlib' expected, but '$1' found" errInvalidExceptionSystem = "'goto', 'setjmp', 'cpp' or 'quirky' expected, but '$1' found" errInvalidFeatureButXFound = Feature.toSeq.map(proc(val:Feature): string = "'$1'" % $val).join(", ") & " expected, but '$1' found" + errDefaultOrSsoExpectedButXFound = "'default' or 'sso' expected, but '$1' found" template warningOptionNoop(switch: string) = warningDeprecated(conf, info, "'$#' is deprecated, now a noop" % switch) @@ -306,6 +307,13 @@ proc testCompileOptionArg*(conf: ConfigRef; switch, arg: string, info: TLineInfo else: result = false localError(conf, info, errInvalidExceptionSystem % arg) + of "strings": + case arg.normalize + of "default": result = conf.selectedStrings == stringDefault + of "sso": result = conf.selectedStrings == stringSso + else: + result = false + localError(conf, info, errDefaultOrSsoExpectedButXFound % arg) of "experimental": try: result = conf.features.contains parseEnum[Feature](arg) @@ -750,6 +758,17 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo; processMemoryManagementOption(switch, arg, pass, info, conf) of "mm": processMemoryManagementOption(switch, arg, pass, info, conf) + of "strings": + expectArg(conf, switch, arg, pass, info) + if pass in {passCmd2, passPP}: + case arg.normalize + of "default": + conf.selectedStrings = stringDefault + of "sso": + conf.selectedStrings = stringSso + defineSymbol(conf.symbols, "nimsso") + else: + localError(conf, info, errDefaultOrSsoExpectedButXFound % arg) of "warnings", "w": if processOnOffSwitchOrList(conf, {optWarns}, arg, pass, info): listWarnings(conf) of "warning": processSpecificNote(arg, wWarning, pass, info, switch, conf) diff --git a/compiler/liftdestructors.nim b/compiler/liftdestructors.nim index 15c60363f8..9c37038fb5 100644 --- a/compiler/liftdestructors.nim +++ b/compiler/liftdestructors.nim @@ -732,7 +732,7 @@ proc fillStrOp(c: var TLiftCtx; t: PType; body, x, y: PNode) = of attachedAsgn, attachedDeepCopy, attachedDup: body.add callCodegenProc(c.g, "nimAsgnStrV2", c.info, genAddr(c, x), y) of attachedSink: - if c.g.config.isDefined("nimsso"): + if c.g.config.usesSso(): # SmallString: destroy old dst, then bit-copy src (no rc increment — this is a move). # No .p aliasing check needed; rc-based destroy handles COW sharing correctly. doAssert t.destructor != nil diff --git a/compiler/nim.nim b/compiler/nim.nim index ed6774983c..a60e030118 100644 --- a/compiler/nim.nim +++ b/compiler/nim.nim @@ -121,6 +121,11 @@ proc handleCmdLine(cache: IdentCache; conf: ConfigRef) = conf.cmd in {cmdGendepend, cmdNifC, cmdIc, cmdM}: initOrcDefines(conf) + if conf.selectedStrings == stringSso and + conf.selectedGC notin {gcArc, gcOrc, gcYrc, gcAtomicArc}: + rawMessage(conf, errGenerated, + "--strings:sso requires --mm:arc, --mm:orc, --mm:yrc, or --mm:atomicArc") + mainCommand(graph) if conf.hasHint(hintGCStats): echo(GC_getStatistics()) #echo(GC_getStatistics()) diff --git a/compiler/options.nim b/compiler/options.nim index fc15ee9792..bd84e0ee7b 100644 --- a/compiler/options.nim +++ b/compiler/options.nim @@ -267,6 +267,10 @@ type ccNone, ccGcc, ccNintendoSwitch, ccLLVM_Gcc, ccCLang, ccBcc, ccVcc, ccTcc, ccEnv, ccIcl, ccIcc, ccClangCl, ccHipcc, ccNvcc + StringsMode* = enum + stringDefault = "default" + stringSso = "sso" + ExceptionSystem* = enum excNone, # no exception system selected yet excSetjmp, # setjmp based exception handling @@ -366,6 +370,7 @@ type implicitCmd*: bool # whether some flag triggered an implicit `command` selectedGC*: TGCMode # the selected GC (+) exc*: ExceptionSystem + selectedStrings*: StringsMode hintProcessingDots*: bool # true for dots, false for filenames verbosity*: int # how verbose the compiler is numberOfProcessors*: int # number of processors @@ -698,6 +703,7 @@ template quitOrRaise*(conf: ConfigRef, msg = "") = proc importantComments*(conf: ConfigRef): bool {.inline.} = conf.cmd in cmdDocLike + {cmdIdeTools} proc usesWriteBarrier*(conf: ConfigRef): bool {.inline.} = conf.selectedGC >= gcRefc +proc usesSso*(conf: ConfigRef): bool {.inline.} = conf.selectedStrings == stringSso template compilationCachePresent*(conf: ConfigRef): untyped = false diff --git a/lib/pure/streams.nim b/lib/pure/streams.nim index a1fffa5d95..bebd031ab2 100644 --- a/lib/pure/streams.nim +++ b/lib/pure/streams.nim @@ -259,7 +259,7 @@ proc readDataStr*(s: Stream, buffer: var string, slice: Slice[int]): int = result = s.readDataStrImpl(s, buffer, slice) else: # fallback - result = s.readData(beginStore(buffer, slice.b + 1 - slice.a, slice.a), slice.b + 1 - slice.a) + result = s.readData(beginStore(buffer, buffer.len, slice.a), slice.b + 1 - slice.a) endStore(buffer) template jsOrVmBlock(caseJsOrVm, caseElse: untyped): untyped = @@ -1226,7 +1226,7 @@ else: # after 1.3 or JS not defined jsOrVmBlock: buffer[slice.a.. s.data.len: - setLen(s.data, s.pos + bufLen) when defined(js): + if s.pos + bufLen > s.data.len: + setLen(s.data, s.pos + bufLen) try: s.data[s.pos.. PayloadSize: s.more.fullLen else: slen if newLen == curLen: return - if newLen <= 0: - # Pattern 's.setLen 0' is common for avoiding allocations; do NOT free the buffer. + if newLen < curLen: + # Shrinking: if slen > PayloadSize: if slen == HeapSlen and s.more.rc == 1: - s.more.fullLen = 0 - s.more.data[0] = '\0' + # Unique heap block: keep the buffer allocated to avoid alloc/dealloc + # ping-pong when callers shrink then grow (e.g. setLen(0) + add loops). + s.more.fullLen = newLen + s.more.data[newLen] = '\0' else: - # shared or static block: detach and go back to empty inline - nimDestroyStrV1(s) - s.bytes = 0 # slen=0, all inline chars zeroed + # shared or static block: detach and go back to inline + if newLen <= 0: + nimDestroyStrV1(s) + s.bytes = 0 + else: + let old = s.more + let inl = inlinePtr(s) + copyMem(inl, addr old.data[0], newLen) + inl[newLen] = '\0' + if slen == HeapSlen and atomicSubFetch(old.rc, 1) == 0: + dealloc(old) + if newLen < AlwaysAvail: + when system.cpuEndian == littleEndian: + let keepBits = (newLen + 1) * 8 + let charMask = ((uint(1) shl keepBits) - 1'u) and not 0xFF'u + s.bytes = (s.bytes and charMask) or uint(newLen) + else: + let discardBits = (AlwaysAvail - newLen) * 8 + let slenBit = 8 * (sizeof(uint) - 1) + let charMask = not ((uint(1) shl discardBits) - 1'u) and not (0xFF'u shl slenBit) + s.bytes = (s.bytes and charMask) or (uint(newLen) shl slenBit) + else: + setSSLen(s, newLen) else: - s.bytes = 0 # slen=0, all inline chars zeroed (SWAR safe) + # inline/medium shrink + if newLen <= 0: + s.bytes = 0 + else: + let inl = inlinePtr(s) + inl[newLen] = '\0' + if newLen < AlwaysAvail: + when system.cpuEndian == littleEndian: + let keepBits = (newLen + 1) * 8 + let charMask = ((uint(1) shl keepBits) - 1'u) and not 0xFF'u + s.bytes = (s.bytes and charMask) or uint(newLen) + else: + let discardBits = (AlwaysAvail - newLen) * 8 + let slenBit = 8 * (sizeof(uint) - 1) + let charMask = not ((uint(1) shl discardBits) - 1'u) and not (0xFF'u shl slenBit) + s.bytes = (s.bytes and charMask) or (uint(newLen) shl slenBit) + else: + setSSLen(s, newLen) return if slen <= PayloadSize: if newLen <= PayloadSize: @@ -564,34 +603,11 @@ proc setLengthStr(s: var SmallString; newLen: int; zeroing: bool) = s.more = p setSSLen(s, HeapSlen) else: - # currently long - if newLen <= PayloadSize: - # shrink back to inline/medium - let old = s.more - let inl = inlinePtr(s) - copyMem(inl, addr old.data[0], newLen) - inl[newLen] = '\0' - if slen == HeapSlen and atomicSubFetch(old.rc, 1) == 0: - dealloc(old) - # Zero padding bytes in `bytes` for SWAR invariant - if newLen < AlwaysAvail: - when system.cpuEndian == littleEndian: - let keepBits = (newLen + 1) * 8 - let charMask = ((uint(1) shl keepBits) - 1'u) and not 0xFF'u - s.bytes = (s.bytes and charMask) or uint(newLen) - else: - let discardBits = (AlwaysAvail - newLen) * 8 - let slenBit = 8 * (sizeof(uint) - 1) - let charMask = not ((uint(1) shl discardBits) - 1'u) and not (0xFF'u shl slenBit) - s.bytes = (s.bytes and charMask) or (uint(newLen) shl slenBit) - else: - setSSLen(s, newLen) - else: - # long -> long - ensureUniqueLong(s, curLen, newLen) # sets fullLen = newLen - if newLen > curLen: - zeroMem(addr s.more.data[curLen], newLen - curLen) - s.more.data[newLen] = '\0' + # currently long: grow within the heap buffer (shrinking already returned above) + ensureUniqueLong(s, curLen, newLen) # sets fullLen = newLen + if zeroing and newLen > curLen: + zeroMem(addr s.more.data[curLen], newLen - curLen) + s.more.data[newLen] = '\0' proc setLengthStrV2(s: var SmallString; newLen: int) {.compilerRtl.} = ## Sets the length of `s` to `newLen`, zeroing new bytes on growth. @@ -705,18 +721,37 @@ proc completeStore(s: var SmallString) {.compilerproc, inline.} = proc completeStore*(s: var string) {.inline.} = completeStore(cast[ptr SmallString](addr s)[]) -proc beginStore*(s: var string; ensuredLen: int; start = 0): ptr UncheckedArray[char] {.inline, noSideEffect, raises: [], tags: [].} = - ## Prepares `s` for a bulk write of `ensuredLen` bytes starting at `start`. - ## The caller must ensure `s.len >= start + ensuredLen` (e.g. via `newString` or `setLen`). +proc beginStore*(s: var string; newLen: int; start = 0): ptr UncheckedArray[char] {.inline, noSideEffect, raises: [], tags: [].} = + ## Sets s.len to `newLen` (new bytes are uninitialized), ensures unique + ## ownership, and returns a pointer to s[start] for bulk writing. ## Call `endStore(s)` afterwards to sync the inline cache. + ## To keep the current length, pass `s.len`. {.cast(noSideEffect).}: let ss = cast[ptr SmallString](addr s) let slen = ssLen(ss[]) - if slen > PayloadSize: - ensureUniqueLong(ss[], ss[].more.fullLen, ss[].more.fullLen) + let curLen = if slen > PayloadSize: ss[].more.fullLen else: slen + if newLen <= PayloadSize and slen <= PayloadSize: + # Stay inline/medium. + if newLen != curLen: + setSSLen(ss[], newLen) + result = cast[ptr UncheckedArray[char]](cast[uint](inlinePtr(ss[])) + uint(start)) + elif slen <= PayloadSize: + # Inline/medium → long. + let newCap = resize(newLen) + let p = cast[ptr LongString](alloc(LongStringDataOffset + newCap + 1)) + p.rc = 1 + p.fullLen = newLen + p.capImpl = newCap + copyMem(addr p.data[0], inlinePtr(ss[]), curLen) + p.data[newLen] = '\0' + ss[].more = p + setSSLen(ss[], HeapSlen) result = cast[ptr UncheckedArray[char]](addr ss[].more.data[start]) else: - result = cast[ptr UncheckedArray[char]](cast[uint](inlinePtr(ss[])) + uint(start)) + # Already long: resize within heap (no transition back to inline). + ensureUniqueLong(ss[], curLen, newLen) + ss[].more.data[newLen] = '\0' + result = cast[ptr UncheckedArray[char]](addr ss[].more.data[start]) proc endStore*(s: var string) {.inline, noSideEffect, raises: [], tags: [].} = ## Syncs the inline cache after bulk writes via `beginStore`. No-op for short/medium strings. diff --git a/tests/stdlib/tstring.nim b/tests/stdlib/tstring.nim index fad3865085..536b41161b 100644 --- a/tests/stdlib/tstring.nim +++ b/tests/stdlib/tstring.nim @@ -1,5 +1,5 @@ discard """ - matrix: "--backend:c --mm:refc; --backend:c --mm:orc; --backend:c --mm:orc -d:nimsso; --backend:cpp --mm:refc; --backend:cpp --mm:orc; --backend:js --mm:refc; --backend:js --mm:orc" + matrix: "--backend:c --mm:refc; --backend:c --mm:orc; --backend:c --mm:orc --strings:sso; --backend:cpp --mm:refc; --backend:cpp --mm:orc; --backend:js --mm:refc; --backend:js --mm:orc" """ from std/sequtils import toSeq, map diff --git a/tests/system/tnimsso.nim b/tests/system/tnimsso.nim index ca9d64faec..c487945012 100644 --- a/tests/system/tnimsso.nim +++ b/tests/system/tnimsso.nim @@ -1,5 +1,5 @@ discard """ - matrix: "-d:nimsso" + matrix: "--strings:sso --mm:orc" targets: "c cpp" """ From 92d0c097e56991f193884c1019bdc3843435eac7 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:47:45 +0800 Subject: [PATCH 15/23] fixes #25140; Cannot resolve pragmas when new type is defined from typeof expression (#25764) fixes #25140 --- lib/core/macros.nim | 21 +++++++++++++++++++-- tests/pragmas/tcustom_pragma.nim | 17 +++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/core/macros.nim b/lib/core/macros.nim index 793ae75a13..4a08bf559f 100644 --- a/lib/core/macros.nim +++ b/lib/core/macros.nim @@ -1559,6 +1559,8 @@ macro expandMacros*(body: typed): untyped = echo body.toStrLit result = body +proc getTypeInstSkipAlias(n: NimNode): NimNode {.magic: "NGetType", noSideEffect.} + proc extractTypeImpl(n: NimNode): NimNode = ## attempts to extract the type definition of the given symbol case n.kind @@ -1573,11 +1575,17 @@ proc extractTypeImpl(n: NimNode): NimNode = result = n[0].getImpl() of nnkTypeDef: result = n[2] + if result.kind notin {nnkSym, nnkObjectTy, nnkRefTy, nnkPtrTy, nnkBracketExpr}: + # Handle typeof() and similar unresolvable type expressions + let typSym = if n[0].kind == nnkPragmaExpr: n[0][0] else: n[0] + if typSym.kind == nnkSym: + let resolved = typSym.getTypeInstSkipAlias() + if resolved.kind == nnkSym: + return resolved.getImpl.extractTypeImpl() + error("Invalid node to retrieve type implementation of: " & $result.kind) else: error("Invalid node to retrieve type implementation of: " & $n.kind) -proc getTypeInstSkipAlias(n: NimNode): NimNode {.magic: "NGetType", noSideEffect.} - proc customPragmaNode(n: NimNode): NimNode = result = nil expectKind(n, {nnkSym, nnkDotExpr, nnkBracketExpr, nnkTypeOfExpr, nnkType, nnkCheckedFieldExpr}) @@ -1618,6 +1626,15 @@ proc customPragmaNode(n: NimNode): NimNode = var typDef = getImpl(typInst) while typDef != nil: typDef.expectKind(nnkTypeDef) + # Resolve typeof() and similar unresolvable type expressions + if typDef[2].kind notin {nnkSym, nnkObjectTy, nnkRefTy, nnkPtrTy, nnkBracketExpr}: + let typSym = if typDef[0].kind == nnkPragmaExpr: typDef[0][0] else: typDef[0] + if typSym.kind == nnkSym: + let resolved = typSym.getTypeInstSkipAlias() + if resolved.kind == nnkSym: + typDef = getImpl(resolved) + continue + break let typ = typDef[2].extractTypeImpl() if typ.kind notin {nnkRefTy, nnkPtrTy, nnkObjectTy}: break let isRef = typ.kind in {nnkRefTy, nnkPtrTy} diff --git a/tests/pragmas/tcustom_pragma.nim b/tests/pragmas/tcustom_pragma.nim index 3d6032e605..092d59bc74 100644 --- a/tests/pragmas/tcustom_pragma.nim +++ b/tests/pragmas/tcustom_pragma.nim @@ -549,3 +549,20 @@ block: type X {.p.} = object doAssert foo(X()) + +block: # typeof() type alias preserves field pragmas + template myFieldPragma {.pragma.} + type Orig = object + x {.myFieldPragma.}: int + + var orig: Orig + + # Direct typeof alias + type TAlias = typeof(orig) + var a: TAlias + doAssert a.x.hasCustomPragma(myFieldPragma) + + # Indirect alias of typeof alias + type TAlias2 = TAlias + var b: TAlias2 + doAssert b.x.hasCustomPragma(myFieldPragma) From 4bcb706d496e4dce3f25040e67065950973fbaa2 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 28 Apr 2026 18:48:41 +0200 Subject: [PATCH 16/23] IC: added support for conditional dependencies (#25770) --- compiler/deps.nim | 215 ++++++++++++++++++++++++++++++++++++++++------ compiler/nim.cfg | 3 +- koch.nim | 3 +- 3 files changed, 194 insertions(+), 27 deletions(-) diff --git a/compiler/deps.nim b/compiler/deps.nim index 18d6608fd0..116c0ebdf3 100644 --- a/compiler/deps.nim +++ b/compiler/deps.nim @@ -137,6 +137,171 @@ proc processImport(c: var DepContext; importPath: string; current: Node) = if existingIdx notin current.deps: current.deps.add existingIdx +proc skipSubtree(s: var Stream; first: PackedToken) = + ## Consume tokens until the ParLe at `first` is balanced. Caller has + ## already obtained `first`. + if first.kind != ParLe: return + var depth = 1 + while depth > 0: + let t = next(s) + if t.kind == ParLe: inc depth + elif t.kind == ParRi: dec depth + elif t.kind == EofToken: return + +proc evalCondExpr(c: DepContext; s: var Stream): bool = + ## Read exactly one condition expression from `s` and return its truth + ## value. Consumes tokens whether the expression is recognised or not so + ## the caller stays in sync. Recognises `defined(IDENT)`, the boolean + ## operators `not`/`and`/`or`, and the literals `true`/`false`. Anything + ## else (e.g. a call to an arbitrary proc) is treated as `true` — the + ## conservative direction, since a false negative here drops a real + ## dependency from the build graph. + let t = next(s) + case t.kind + of Ident: + case pool.strings[t.litId] + of "true": result = true + of "false": result = false + else: result = true + of ParLe: + let tag = pool.tags[t.tagId] + case tag + of "call", "cmd", "callstrlit", "infix", "prefix": + # First child is the head (function/operator name). + let head = next(s) + var name = "" + if head.kind == Ident: name = pool.strings[head.litId] + case name + of "defined": + let arg = next(s) + var sym = "" + if arg.kind == Ident: sym = pool.strings[arg.litId] + result = sym.len > 0 and isDefined(c.config, sym) + of "not": + result = not evalCondExpr(c, s) + of "and": + result = evalCondExpr(c, s) + if result: result = evalCondExpr(c, s) + else: skipSubtree(s, next(s)) + of "or": + result = evalCondExpr(c, s) + if not result: result = evalCondExpr(c, s) + else: skipSubtree(s, next(s)) + else: + result = true + # Drain whatever remains until the matching ParRi. + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: return + of "not": + result = not evalCondExpr(c, s) + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: return + of "and": + result = evalCondExpr(c, s) + if result: result = evalCondExpr(c, s) + else: skipSubtree(s, next(s)) + # consume closing ParRi + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: return + of "or": + result = evalCondExpr(c, s) + if not result: result = evalCondExpr(c, s) + else: skipSubtree(s, next(s)) + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: return + else: + skipSubtree(s, t) + result = true + else: + result = true + +proc whenMarkerHolds(c: DepContext; s: var Stream): bool = + ## Caller has just consumed the `(when` ParLe. Read children until the + ## matching `)`, AND-ing each evaluated condition. + result = true + while true: + # peek by reading; if it's ParRi, we're done + let t = next(s) + if t.kind == ParRi: return + if t.kind == EofToken: return + if t.kind == ParLe: + # Re-feed by manually evaluating the subtree starting at `t`. + # evalCondExpr expects to read its own opener, so handle it directly. + let tag = pool.tags[t.tagId] + case tag + of "call", "cmd", "callstrlit", "infix", "prefix": + let head = next(s) + var name = "" + if head.kind == Ident: name = pool.strings[head.litId] + var ok = true + case name + of "defined": + let arg = next(s) + var sym = "" + if arg.kind == Ident: sym = pool.strings[arg.litId] + ok = sym.len > 0 and isDefined(c.config, sym) + of "not": + ok = not evalCondExpr(c, s) + of "and": + ok = evalCondExpr(c, s) + if ok: ok = evalCondExpr(c, s) + of "or": + ok = evalCondExpr(c, s) + if not ok: ok = evalCondExpr(c, s) + else: + ok = true + # finish the subtree + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: return + if not ok: result = false + of "not", "and", "or": + # Re-emit a synthetic dispatch: rewrap by descending. + var ok = true + case tag + of "not": + ok = not evalCondExpr(c, s) + of "and": + ok = evalCondExpr(c, s) + if ok: ok = evalCondExpr(c, s) + of "or": + ok = evalCondExpr(c, s) + if not ok: ok = evalCondExpr(c, s) + else: discard + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: return + if not ok: result = false + else: + # Unknown — treat as true and skip. + skipSubtree(s, t) + elif t.kind == Ident: + let v = pool.strings[t.litId] + if v == "false": result = false + # else (true / unknown ident): keep result + proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) = ## Read a .deps.nif file and process imports/includes let depsPath = c.depsFile(pair) @@ -158,12 +323,27 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) = if t.kind == ParLe: let tag = pool.tags[t.tagId] case tag - of "import", "fromimport": - # Read import path + of "import", "fromimport", "include": + # Read first child. May be a `(when COND...)` marker — parse and + # evaluate; if the condition is statically false, skip the import + # entirely. Otherwise advance past the marker and parse the path. t = next(s) - # Check for "when" marker (conditional import) - if t.kind == Ident and pool.strings[t.litId] == "when": - t = next(s) # skip it, still process the import + var live = true + if t.kind == ParLe and pool.tags[t.tagId] == "when": + # whenMarkerHolds consumes everything up to and including the + # closing `)` of the `(when ...)` subtree. + live = whenMarkerHolds(c, s) + t = next(s) + if not live: + # Drain the rest of this import/include node. + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: break + t = next(s) + continue # Handle path expression (could be ident, string, or infix like std/foo) var importPath = "" if t.kind == Ident: @@ -181,26 +361,11 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) = if t.kind == Ident: # second part (foo) importPath = importPath & "/" & pool.strings[t.litId] if importPath.len > 0: - processImport(c, importPath, current) - # Skip to end of import node - var depth = 1 - while depth > 0: - t = next(s) - if t.kind == ParLe: inc depth - elif t.kind == ParRi: dec depth - of "include": - # Read include path - t = next(s) - if t.kind == Ident and pool.strings[t.litId] == "when": - t = next(s) # skip conditional marker - var includePath = "" - if t.kind == Ident: - includePath = pool.strings[t.litId] - elif t.kind == StringLit: - includePath = pool.strings[t.litId] - if includePath.len > 0: - processInclude(c, includePath, current) - # Skip to end + if tag == "include": + processInclude(c, importPath, current) + else: + processImport(c, importPath, current) + # Skip to end of node var depth = 1 while depth > 0: t = next(s) diff --git a/compiler/nim.cfg b/compiler/nim.cfg index 425f0df324..19c1df344f 100644 --- a/compiler/nim.cfg +++ b/compiler/nim.cfg @@ -12,7 +12,8 @@ define:nimPreviewNonVarDestructor define:nimPreviewCheckedClose define:nimPreviewAsmSemSymbol define:nimPreviewCStringComparisons -define:nimPreviewDuplicateModuleError +#define:nimPreviewDuplicateModuleError +# Incompatible with Nimony's compat2.nim for now threads:off diff --git a/koch.nim b/koch.nim index 0fd0e365c9..c9269303b1 100644 --- a/koch.nim +++ b/koch.nim @@ -16,10 +16,11 @@ const ChecksumsStableCommit = "0b8e46379c5bc1bf73d8b3011908389c60fb9b98" # 2.0.1 SatStableCommit = "e63eaea8baf00bed8bcd5a29ffd8823abb265b39" - NimonyStableCommit = "bbfb21529845567c55b67d176354daef0e7d6c29" # unversioned \ + NimonyStableCommit = "c189ef438598878b2f02f6a2ff91d08febafc04b" # unversioned \ # Note that Nimony uses Nim as a git submodule but we don't want to install # Nimony's dependency to Nim as we are Nim. So a `git clone` without --recursive # is **required** here. + # Commit from 2026-04-27 # examples of possible values for fusion: #head, #ea82b54, 1.2.3 FusionStableHash = "#562467452b32cb7a97410ea177f083e6d8405734" From cbe02aa9de741c0f1fbf6f114b67c86fa3b6f84d Mon Sep 17 00:00:00 2001 From: puffball1567 Date: Tue, 5 May 2026 22:27:33 +0900 Subject: [PATCH 17/23] fixes finally being skipped when `except T as e` re-raises (cpp backend) (#25775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Bug When an `except T as e:` handler in the cpp backend raises a new exception, the enclosing `finally` block is silently dropped under `--mm:arc` and `--mm:orc`: ```nim proc main() = try: try: raise newException(CatchableError, "orig") except CatchableError as e: echo "inner: ", e.msg raise newException(CatchableError, "re:" & e.msg) finally: echo "finally" except CatchableError as outer: echo "outer: ", outer.msg main() ``` Expected output: ``` inner: orig finally outer: re:orig ``` Actual output on `nim cpp --mm:arc` (and `--mm:orc`): ``` inner: orig outer: re:orig ``` The `finally` line is missing. The bug is specific to memory managers that use destructor injection (arc/orc); under `--mm:refc` the original code path works correctly because no destructor wrapper is injected. ## Root cause When the body of `except T as e:` is processed under ARC/ORC, the destructor injection pass injects a compiler-generated `nkHiddenTryStmt` wrapper around the handler body to call `=destroy` on `e` when it goes out of scope. That wrapper sits at the top of `p.nestedTryStmts` with `inExcept = false`. `finallyActions` (which inlines the user-finally body before a raise propagates) only inspected the topmost entry of `nestedTryStmts`. Because the wrapper has `inExcept = false`, the check short-circuited and the user's finally was never inlined. After the raise, C++'s rule that sibling catch clauses do not catch each other's throws means the surrounding `catch(...)/finally` emitted by `genTryCpp` never runs either, so the user's finally is silently dropped. ## Fix - Add an `isHidden` flag to `nestedTryStmts` entries, set to `t.kind == nkHiddenTryStmt` so compiler-injected try wrappers can be distinguished from user-written ones. - In `finallyActions`, walk past `isHidden` wrappers but stop at the first user try. If that user try is in its except branch with a finally, inline the finally body before the raise; otherwise leave the raise untouched (the raise will be caught by that user try's own except branches and the inner finally will run via normal unwinding, which is what already happens correctly under refc). Walking past wrappers fixes the `as e` case under arc/orc. Stopping at user trys preserves the existing correct behaviour for nested try/except/finally constructs (e.g. `tests/exception/tfinally.nim`'s `nested_finally`), which would otherwise see the outer finally inlined too eagerly when an inner raise is processed. ## Tests Adds `tests/exception/tcpp_handler_raise_finally.nim` covering: - `except T as e:` re-raise + outer finally - typeless `except:` re-raise + outer finally - try/finally without except (exception propagation through finally) The test runs on `--mm:arc`, `--mm:orc`, and `--mm:refc`. Locally verified on both `devel` and `version-2-2`: - `tests/exception/` — 42 PASS, 0 FAIL, 3 SKIP - `tests/destructor/` — all PASS - `tests/cpp/` — all PASS (single unrelated failure: `tasync_cpp.nim` needs the `jester` package) - `megatest` — PASS for both `--mm:arc` and `--mm:refc`, including the previously regressing `tfinally.nim`'s `nested_finally` ## Backport Tagged `[backport]` in the commit message for inclusion in `version-2-2`. --------- Co-authored-by: puffball1567 <17452514+puffball1567@users.noreply.github.com> --- compiler/ccgstmts.nim | 36 +++++++---- compiler/cgendata.nim | 7 ++- .../exception/tcpp_handler_raise_finally.nim | 61 +++++++++++++++++++ tests/exception/tcpp_imported_exc.nim | 2 +- 4 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 tests/exception/tcpp_handler_raise_finally.nim diff --git a/compiler/ccgstmts.nim b/compiler/ccgstmts.nim index a80ef37efd..5ea23d1f80 100644 --- a/compiler/ccgstmts.nim +++ b/compiler/ccgstmts.nim @@ -230,7 +230,7 @@ proc blockLeaveActions(p: BProc, howManyTrys, howManyExcepts: int, isReturnStmt # Called by return and break stmts. # Deals with issues faced when jumping out of try/except/finally stmts. - var stack = newSeq[tuple[fin: PNode, inExcept: bool, label: Natural]](0) + var stack = newSeq[tuple[fin: PNode, inExcept: bool, isHidden: bool, label: Natural]](0) inc p.withinBlockLeaveActions for i in 1..howManyTrys: @@ -836,12 +836,26 @@ proc raiseExitCleanup(p: BProc, destroy: string) = p.s(cpsStmts).addGoto("LA" & $p.nestedTryStmts[^1].label & "_") proc finallyActions(p: BProc) = - if p.config.exc != excGoto and p.nestedTryStmts.len > 0 and p.nestedTryStmts[^1].inExcept: - # if the current try stmt have a finally block, - # we must execute it before reraising - let finallyBlock = p.nestedTryStmts[^1].fin - if finallyBlock != nil: - genSimpleBlock(p, finallyBlock[0]) + if p.config.exc != excGoto: + # Walk past compiler-injected `nkHiddenTryStmt` wrappers (e.g. ARC's + # destructor try/finally that wraps `except T as e:` bodies) to reach + # the user's actual try. We must NOT walk past a real user try whose + # body we are currently in, because a raise from there will be caught + # by that try's own except branches rather than escaping outward. + # + # If after skipping wrappers the next entry is a user try in its + # except branch (inExcept=true), inline its finally body before the + # raise propagates — without this, the C++ sibling-catch rule would + # cause the user's catch(...)/finally pair to be bypassed and the + # finally would be silently dropped. + for i in countdown(p.nestedTryStmts.high, 0): + if p.nestedTryStmts[i].isHidden: + continue + if p.nestedTryStmts[i].inExcept: + let finallyBlock = p.nestedTryStmts[i].fin + if finallyBlock != nil: + genSimpleBlock(p, finallyBlock[0]) + return proc raiseInstr(p: BProc; result: var Builder) = if p.config.exc == excGoto: @@ -1185,7 +1199,7 @@ proc genTryCpp(p: BProc, t: PNode, d: var TLoc) = lineCg(p, cpsLocals, "std::exception_ptr T$1_;$n", [etmp]) let fin = if t[^1].kind == nkFinally: t[^1] else: nil - p.nestedTryStmts.add((fin, false, 0.Natural)) + p.nestedTryStmts.add((fin, false, t.kind == nkHiddenTryStmt, 0.Natural)) if t.kind == nkHiddenTryStmt: lineCg(p, cpsStmts, "try {$n", []) @@ -1371,7 +1385,7 @@ proc genTryCppOld(p: BProc, t: PNode, d: var TLoc) = genLineDir(p, t) cgsym(p.module, "popCurrentExceptionEx") let fin = if t[^1].kind == nkFinally: t[^1] else: nil - p.nestedTryStmts.add((fin, false, 0.Natural)) + p.nestedTryStmts.add((fin, false, t.kind == nkHiddenTryStmt, 0.Natural)) startBlockWith(p): p.s(cpsStmts).add("try {\n") expr(p, t[0], d) @@ -1450,7 +1464,7 @@ proc genTryGoto(p: BProc; t: PNode; d: var TLoc) = let lab = p.labels let hasExcept = t[1].kind == nkExceptBranch if hasExcept: inc p.withinTryWithExcept - p.nestedTryStmts.add((fin, false, Natural lab)) + p.nestedTryStmts.add((fin, false, t.kind == nkHiddenTryStmt, Natural lab)) p.flags.incl nimErrorFlagAccessed @@ -1656,7 +1670,7 @@ proc genTrySetjmp(p: BProc, t: PNode, d: var TLoc) = initElifBranch(p.s(cpsStmts), nonQuirkyIf, removeSinglePar( cOp(Equal, dotField(safePoint, "status"), cIntValue(0)))) let fin = if t[^1].kind == nkFinally: t[^1] else: nil - p.nestedTryStmts.add((fin, quirkyExceptions, 0.Natural)) + p.nestedTryStmts.add((fin, quirkyExceptions, t.kind == nkHiddenTryStmt, 0.Natural)) expr(p, t[0], d) var quirkyIf = default(IfBuilder) var quirkyScope = default(ScopeBuilder) diff --git a/compiler/cgendata.nim b/compiler/cgendata.nim index 5b5668024a..fb8f2086cb 100644 --- a/compiler/cgendata.nim +++ b/compiler/cgendata.nim @@ -75,10 +75,13 @@ type flags*: set[TCProcFlag] lastLineInfo*: TLineInfo # to avoid generating excessive 'nimln' statements currLineInfo*: TLineInfo # AST codegen will make this superfluous - nestedTryStmts*: seq[tuple[fin: PNode, inExcept: bool, label: Natural]] + nestedTryStmts*: seq[tuple[fin: PNode, inExcept: bool, isHidden: bool, label: Natural]] # in how many nested try statements we are # (the vars must be volatile then) - # bool is true when are in the except part of a try block + # `inExcept` is true when we are in the except part of a try block. + # `isHidden` is true for compiler-injected `nkHiddenTryStmt` wrappers + # (e.g. ARC's destructor try/finally around `except T as e:` bodies); + # finallyActions walks past such wrappers to reach the user's try. finallySafePoints*: seq[Rope] # For correctly cleaning up exceptions when # using return in finally statements labels*: Natural # for generating unique labels in the C proc diff --git a/tests/exception/tcpp_handler_raise_finally.nim b/tests/exception/tcpp_handler_raise_finally.nim new file mode 100644 index 0000000000..880903d5a9 --- /dev/null +++ b/tests/exception/tcpp_handler_raise_finally.nim @@ -0,0 +1,61 @@ +discard """ + targets: "cpp" + matrix: "--mm:arc; --mm:orc; --mm:refc" + output: ''' +inner: orig +finally +outer: re:orig +inner-typeless: orig +finally-typeless +outer-typeless: re-tl:orig +no-catch-finally +caught-propagated: prop +''' +""" + +# When an `except` handler raises a new exception, the enclosing `finally` +# block must still run before the new exception propagates to the outer +# try. +# +# The C++ backend previously emitted the finally's `catch (...)` as a +# sibling of the user-written catches. C++ does not allow sibling catches +# to catch each other's throws, so a handler-raised exception bypassed the +# finally entirely. The fix wraps the inner try/catch sequence in an +# outer try, so any escaping exception (whether from the body or from a +# handler) is captured before the finally runs. + +block typed_except: + try: + try: + raise newException(CatchableError, "orig") + except CatchableError as e: + echo "inner: ", e.msg + raise newException(CatchableError, "re:" & e.msg) + finally: + echo "finally" + except CatchableError as outer: + echo "outer: ", outer.msg + +block typeless_except: + try: + try: + raise newException(CatchableError, "orig") + except: + let e = getCurrentException() + echo "inner-typeless: ", e.msg + raise newException(CatchableError, "re-tl:" & e.msg) + finally: + echo "finally-typeless" + except CatchableError as outer: + echo "outer-typeless: ", outer.msg + +# try/finally without an except: the body's exception must still propagate +# after the finally runs. +block no_catch_finally: + try: + try: + raise newException(CatchableError, "prop") + finally: + echo "no-catch-finally" + except CatchableError as e: + echo "caught-propagated: ", e.msg diff --git a/tests/exception/tcpp_imported_exc.nim b/tests/exception/tcpp_imported_exc.nim index 0c7846956b..8ee008bd96 100644 --- a/tests/exception/tcpp_imported_exc.nim +++ b/tests/exception/tcpp_imported_exc.nim @@ -1,5 +1,5 @@ discard """ -matrix: "--mm:refc" +matrix: "--mm:refc; --mm:orc" targets: "cpp" output: ''' caught as std::exception From b73908a361b492a7bceafdd08c875bf8624a96ae Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Wed, 6 May 2026 02:21:37 +0800 Subject: [PATCH 18/23] fix #25789; improve handling of distinct types (#25791) fix #25789 This pull request addresses an issue with the `distinctBase` trait in the Nim compiler, ensuring it correctly handles types with generic parameters and static parameters. Additionally, it adds a new test to cover this scenario. The most important changes are: ### Compiler logic improvements * Updated the `evalTypeTrait` implementation for the `distinctBase` trait in `compiler/semmagic.nim` to properly skip all relevant type wrappers, including those with generic and static parameters, when unwrapping distinct types. This fixes incorrect handling of types like `distinct L[int, 100]`. ### Test coverage * Added a new test block for bug #25789 in `tests/metatype/ttypetraits.nim` that defines a distinct type over a generic type with a static parameter, verifies conversions, and checks that the `distinctBase` trait returns the correct type. --- compiler/semmagic.nim | 9 ++++++--- tests/metatype/ttypetraits.nim | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/compiler/semmagic.nim b/compiler/semmagic.nim index 88d7512373..397c08bf67 100644 --- a/compiler/semmagic.nim +++ b/compiler/semmagic.nim @@ -248,10 +248,13 @@ proc evalTypeTrait(c: PContext; traitCall: PNode, operand: PType, context: PSym) assert operand.kind == tyTuple, $operand.kind result = newIntNodeT(toInt128(operand.len), traitCall, c.idgen, c.graph) of "distinctBase": - var arg = operand.skipTypes({tyGenericInst}) + var arg = operand.skipTypes(skippedTypes) let rec = semConstExpr(c, traitCall[2]).intVal != 0 - while arg.kind == tyDistinct: - arg = arg.base.skipTypes(skippedTypes + {tyGenericInst}) + while true: + let distinctArg = arg.skipTypes(skippedTypes + {tyGenericInst}) + if distinctArg.kind != tyDistinct: + break + arg = distinctArg.base.skipTypes(skippedTypes) if not rec: break result = getTypeDescNode(c, arg, operand.owner, traitCall.info) of "rangeBase": diff --git a/tests/metatype/ttypetraits.nim b/tests/metatype/ttypetraits.nim index 0107f6b049..46601c1070 100644 --- a/tests/metatype/ttypetraits.nim +++ b/tests/metatype/ttypetraits.nim @@ -434,3 +434,32 @@ block: # bug #24378 type Win222[T] = typeof("foobar") doAssert not supportsCopyMem((int, Win222[int])) doAssert not supportsCopyMem(tuple[a: int, b: Win222[int]]) + +block: # bug #25789 + type + L[T; N: static int] = distinct seq[T] + EPF = distinct L[int, 100] + + var e: EPF = EPF(L[int, 100](@[1, 2, 3])) + + template classifyGeneric[T](x: T): bool = + when typeof(x) is L: + true + else: + false + + template classifyConcrete[T](x: T): bool = + when typeof(x) is L[int, 100]: + true + else: + false + + let viaConv = L[int, 100](e) + doAssert $type(viaConv) == "L[system.int, 100]" + doAssert classifyGeneric(viaConv) + doAssert classifyConcrete(viaConv) + + let viaDB = distinctBase(e, recursive = false) + doAssert $type(viaDB) == "L[system.int, 100]" + doAssert classifyGeneric(viaDB) + doAssert classifyConcrete(viaDB) From e9a0c9634e15b0bab5356547ff848d73faf11630 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Wed, 6 May 2026 03:05:17 +0800 Subject: [PATCH 19/23] fixes #25784; Object default field initialized with an object constructor (#25785) fixes #25784 This pull request addresses the handling of forward object types during type determination in the Nim compiler and adds new test cases to ensure correct default value initialization for objects with forward references. The main focus is to allow forward object types to remain unresolved during the initial type analysis, deferring their resolution to a later compilation phase. This helps support object constructors with default values involving forward types. **Compiler improvements:** * Updated `semObjConstr` in `compiler/semobjconstr.nim` to allow forward object types (`tyForward`) to remain unresolved during determine-type analysis. This avoids premature errors and ensures that such types are resolved later, supporting delayed field-default resolution. **Testing enhancements:** * Added new test cases in `tests/objects/mobject_default_value.nim` to verify that objects with default fields referencing forward types are correctly initialized, and that their default values are properly set. --------- Co-authored-by: Copilot --- compiler/semobjconstr.nim | 5 ++++ compiler/semtypes.nim | 1 + tests/objects/tobject_default_value.nim | 35 ++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/compiler/semobjconstr.nim b/compiler/semobjconstr.nim index d9317c3320..769f88b6f2 100644 --- a/compiler/semobjconstr.nim +++ b/compiler/semobjconstr.nim @@ -486,6 +486,11 @@ proc semObjConstr(c: PContext, n: PNode, flags: TExprFlags; expectedType: PType # we have to watch out, there are also 'owned proc' types that can be used # multiple times as long as they don't have closures. result.typ.incl tfHasOwned + if t.kind == tyForward and efDetermineType in flags: + # a forward object type does not error during determine-type analysis; + # it now stays unresolved long enough for the existing delayed field-default pass to resolve it after the type section finishes. + result.typ = t + return result if t.kind != tyObject: return localErrorNode(c, result, if t.kind != tyGenericBody: "object constructor needs an object type".dup(addTypeNodeDeclaredLoc(c.config, t)) diff --git a/compiler/semtypes.nim b/compiler/semtypes.nim index 4c2d84c29f..8009f7293c 100644 --- a/compiler/semtypes.nim +++ b/compiler/semtypes.nim @@ -370,6 +370,7 @@ proc semFieldDefault(c: PContext; owner, expectedType: PType; field: PNode): PTy propagateToOwner(owner, result) proc semDelayedFieldDefault(c: PContext; owner, expectedType: PType; field: PNode) = + resetSemFlag(field[^1]) fitDefaultNode(c, field[^1], expectedType) propagateToOwner(owner, field[^1].typ.skipIntLit(c.idgen)) diff --git a/tests/objects/tobject_default_value.nim b/tests/objects/tobject_default_value.nim index 5b0a5cd8e6..69e35bf826 100644 --- a/tests/objects/tobject_default_value.nim +++ b/tests/objects/tobject_default_value.nim @@ -833,4 +833,37 @@ proc overloaded[T: object](x: T) = var v: typeof(val) overloaded(v) -overloaded(Thing()) \ No newline at end of file +overloaded(Thing()) + +block: + type + Foo = object + x = Bar() + + Bar = object + x: int + + var f = Foo() + doassert f.x.x == 0 + +block: + type + Foo = object + x = Bar(x: 55) + + Bar = object + x: int + + var f = Foo() + doassert f.x.x == 55 + +block: + type + Bar = object + x: int + + Foo = object + x = Bar() + + var f = Foo() + doassert f.x.x == 0 From df7a114d7ac3afa5ea7560617539dd9b8211f036 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 6 May 2026 08:41:59 +0200 Subject: [PATCH 20/23] IC: use the newer nif27 format (#25792) --- compiler/deps.nim | 11 ++++++++++- doc/ic.md | 2 +- koch.nim | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/compiler/deps.nim b/compiler/deps.nim index 116c0ebdf3..cfe0f0975f 100644 --- a/compiler/deps.nim +++ b/compiler/deps.nim @@ -488,10 +488,19 @@ proc generateBuildFile(c: DepContext): string = let exeFile = changeFileExt(c.nodes[0].files[0].nimFile, ExeExt) b.addTree "do" b.addIdent "nim_nifc" - # Input: .nim file (expanded as argument) and .nif file (dependency) + # Input: .nim file (expanded as argument) b.addTree "input" b.addStrLit mainNif b.endTree() + # Also depend on the semmed .nif files of the main module and all its + # dependencies. nifmake's topological sort orders nodes by depth; without + # these inputs the nim_nifc node sits at depth 1 (no recognized inputs) + # alongside the nifler nodes and runs *before* the nim_m steps that + # produce the .nif files it needs to read. + for node in c.nodes: + b.addTree "input" + b.addStrLit c.semmedFile(node.files[0]) + b.endTree() b.addTree "output" b.addStrLit exeFile b.endTree() diff --git a/doc/ic.md b/doc/ic.md index 9027f8ba63..9fd7b55d2b 100644 --- a/doc/ic.md +++ b/doc/ic.md @@ -33,7 +33,7 @@ The text representation is particularly valuable for debugging and introspection Each ``.nim`` module produces its own ``.nif`` file during compilation. The NIF format contains: -- **Header** - Version information (e.g., `(.nif26)`) +- **Header** - Version information (e.g., `(.nif27)`) - **Dependencies** - List of source files and dependencies - **Interface** - Exported symbols and their indices - **Body** - The intermediate representation of the module's code in Lisp-like syntax diff --git a/koch.nim b/koch.nim index c9269303b1..0ea083fb26 100644 --- a/koch.nim +++ b/koch.nim @@ -16,11 +16,11 @@ const ChecksumsStableCommit = "0b8e46379c5bc1bf73d8b3011908389c60fb9b98" # 2.0.1 SatStableCommit = "e63eaea8baf00bed8bcd5a29ffd8823abb265b39" - NimonyStableCommit = "c189ef438598878b2f02f6a2ff91d08febafc04b" # unversioned \ + NimonyStableCommit = "750aa47f2139fe5ad69f04b44428b752011fe873" # unversioned \ # Note that Nimony uses Nim as a git submodule but we don't want to install # Nimony's dependency to Nim as we are Nim. So a `git clone` without --recursive # is **required** here. - # Commit from 2026-04-27 + # Commit from 2026-05-05 # examples of possible values for fusion: #head, #ea82b54, 1.2.3 FusionStableHash = "#562467452b32cb7a97410ea177f083e6d8405734" From f2e4ae0016ff0a344e814238ffc26146ab765f21 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Wed, 6 May 2026 14:42:36 +0800 Subject: [PATCH 21/23] fixes lent tuple codegen error (#25782) ref https://github.com/nim-lang/Nim/pull/25783 This pull request addresses an issue with addressability of tuple elements of type `lent` or `var` in Nim, ensuring that expressions involving these types are handled correctly during type changes. The main changes introduce a check to prevent attempting to change the type of tuple elements that are views (`var` or `lent`), and a new test is added to verify the correct error is raised when trying to take the address of such elements. Type system and semantic analysis improvements: * Added the `isViewTarget` template in `semexprs.nim` to check if a type is a view (`var` or `lent`), and updated `changeType` to skip type changes for tuple elements that are views. This prevents invalid addressability operations on these types. [[1]](diffhunk://#diff-539da3a63df08fa987f1b0c67d26cdc690753843d110b6bf0805a685eeaffd40R655-R657) [[2]](diffhunk://#diff-539da3a63df08fa987f1b0c67d26cdc690753843d110b6bf0805a685eeaffd40R686-R693) Testing: * Added a new test `tlent_tuple_address.nim` to verify that attempting to take the address of tuple elements of type `lent` correctly produces an "expression has no address" error. --- compiler/semexprs.nim | 13 ++++++++++--- tests/lent/tlent_tuple_address.nim | 12 ++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 tests/lent/tlent_tuple_address.nim diff --git a/compiler/semexprs.nim b/compiler/semexprs.nim index 64a8d2a4f9..aa0489cd22 100644 --- a/compiler/semexprs.nim +++ b/compiler/semexprs.nim @@ -652,6 +652,9 @@ proc overloadedCallOpr(c: PContext, n: PNode): PNode = result = semExpr(c, result, flags = {efNoUndeclared}) proc changeType(c: PContext; n: PNode, newType: PType, check: bool) = + template isViewTarget(t: PType): bool = + t.skipTypes({tyGenericInst, tyAlias, tySink}).kind in {tyVar, tyLent} + case n.kind of nkCurly: for i in 0.. Date: Wed, 6 May 2026 14:44:09 +0800 Subject: [PATCH 22/23] fixes #25617; handle backend type aliasing in procParamTypeRel (#25692) fixes #25617 This pull request introduces a stricter check for parameter type relations in the `procParamTypeRel` procedure. Specifically, it ensures that two types are not only structurally equal but also have the same backend type, taking type aliases into account. Type relation checks: * [`compiler/sigmatch.nim`](diffhunk://#diff-251afcd01d239369019495096c187998dd6695b6457528953237a7e4a10f7138R787-R789): In `procParamTypeRel`, added a check to ensure that if two types are considered equal (`isEqual`), they must also have the same backend type (using `sameBackendTypePickyAliases`). If not, the result is set to `isNone`, preventing false positives when type aliases differ. --- changelog.md | 4 +++ compiler/options.nim | 3 ++ compiler/sigmatch.nim | 11 ++++++++ compiler/types.nim | 2 +- doc/manual.md | 7 +++++ tests/concepts/tconcepts_issues.nim | 36 +++++++++++++++++++++++ tests/proc/tbackendtypealias.nim | 44 +++++++++++++++++++++++++++++ 7 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 tests/proc/tbackendtypealias.nim diff --git a/changelog.md b/changelog.md index aa0485975e..f87dadae63 100644 --- a/changelog.md +++ b/changelog.md @@ -35,6 +35,10 @@ errors. - 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`. +- Procedure compatibility also checks the backend representation of the +parameter and result types, not just their source-level shape. Use +`--legacy:procParamTypeBackendAliases` to restore the older behavior. + ## Standard library additions and changes [//]: # "Additions:" diff --git a/compiler/options.nim b/compiler/options.nim index bd84e0ee7b..7a28b1dc6f 100644 --- a/compiler/options.nim +++ b/compiler/options.nim @@ -259,6 +259,9 @@ type ## Old transformation for closures in JS backend noPanicOnExcept ## don't panic on bare except + procParamTypeBackendAliases + ## Keep the old proc type compatibility rules that ignore backend + ## c type aliases. SymbolFilesOption* = enum disabledSf, writeOnlySf, readOnlySf, v2Sf, stressTest diff --git a/compiler/sigmatch.nim b/compiler/sigmatch.nim index 55fcc43bd9..bf9c2d2050 100644 --- a/compiler/sigmatch.nim +++ b/compiler/sigmatch.nim @@ -784,6 +784,17 @@ proc procParamTypeRel(c: var TCandidate; f, a: PType): TTypeRelation = # if f is metatype. result = typeRel(c, f, a) + if result == isEqual and + procParamTypeBackendAliases notin c.c.config.legacyFeatures: + # Ensure types that are semantically equal also match at the backend level. + # E.g. reject assigning proc(csize_t) to proc(uint) since these map to + # different C types (size_t vs unsigned long long). + let fCheck = concreteType(c, f) + let aCheck = concreteType(c, a) + if fCheck != nil and aCheck != nil and + not sameBackendTypePickyAliases(fCheck, aCheck): + result = isNone + if result <= isSubrange or inconsistentVarTypes(f, a): result = isNone diff --git a/compiler/types.nim b/compiler/types.nim index 62831624b9..de24471e7d 100644 --- a/compiler/types.nim +++ b/compiler/types.nim @@ -897,7 +897,7 @@ proc sameTypeAux(x, y: PType, c: var TSameTypeClosure): bool = c.flags = oldFlags if x == y: return true - let aliasSkipSet = maybeSkipRange({tyAlias}) + let aliasSkipSet = maybeSkipRange({tyAlias, tyInferred}) var a = skipTypes(x, aliasSkipSet) while a.kind == tyUserTypeClass and tfResolved in a.flags: a = skipTypes(a.last, aliasSkipSet) diff --git a/doc/manual.md b/doc/manual.md index ab06f7aa4b..40897ac108 100644 --- a/doc/manual.md +++ b/doc/manual.md @@ -1024,6 +1024,9 @@ These are the major type classes: * procedural type * generic type +The compiler's internal type zoo is richer than this summary suggests: +some types that are structurally equal still differ in backend representation. + Ordinal types ------------- @@ -2174,6 +2177,10 @@ Procedural type A procedural type is internally a pointer to a procedure. `nil` is an allowed value for a variable of a procedural type. +Procedure compatibility also checks the backend representation of the +parameter and result types, not just their source-level shape. Use +`--legacy:procParamTypeBackendAliases` to restore the older behavior. + Examples: ```nim diff --git a/tests/concepts/tconcepts_issues.nim b/tests/concepts/tconcepts_issues.nim index c6d0267c5c..02d9d25bf1 100644 --- a/tests/concepts/tconcepts_issues.nim +++ b/tests/concepts/tconcepts_issues.nim @@ -176,6 +176,42 @@ block t6462: var s = SeqGen[int](fil: FilterMixin[int](test: nil, trans: nil)) doAssert s.test() == nil +block concept_with_cint: + # Generic proc matching through concepts with cint should still work + type + FilterMixin[T] = ref object + test: (T) -> bool + trans: (T) -> T + + SeqGen[T] = ref object + fil: FilterMixin[T] + + WithFilter[T] = concept a + a.fil is FilterMixin[T] + + proc test[T](a: WithFilter[T]): (T) -> bool = + a.fil.test + + var s = SeqGen[cint](fil: FilterMixin[cint](test: nil, trans: nil)) + doAssert s.test() == nil + +block concept_with_int: + type + FilterMixin[T] = ref object + test: (T) -> bool + trans: (T) -> T + + SeqGen[T] = ref object + fil: FilterMixin[T] + + WithFilter[T] = concept a + a.fil is FilterMixin[T] + + proc test[T](a: WithFilter[T]): (T) -> bool = + a.fil.test + + var s = SeqGen[int](fil: FilterMixin[int](test: nil, trans: nil)) + doAssert s.test() == nil block t6770: diff --git a/tests/proc/tbackendtypealias.nim b/tests/proc/tbackendtypealias.nim new file mode 100644 index 0000000000..eaae0c2917 --- /dev/null +++ b/tests/proc/tbackendtypealias.nim @@ -0,0 +1,44 @@ +# bug #25617 +# Ensure that proc types with backend type alias mismatches +# (e.g. uint vs csize_t) are rejected at the Nim level rather +# than producing invalid C code. + +discard """ + cmd: "nim check --hints:off --warnings:off --errorMax:0 $file" + action: "reject" + nimout: ''' +tbackendtypealias.nim(21, 7) Error: type mismatch: got but expected 'proc (len: uint){.closure.}' +tbackendtypealias.nim(28, 7) Error: type mismatch: got but expected 'proc (len: csize_t){.closure.}' +''' +""" + +block direct_assignment: + # Direct proc variable assignment with backend type alias mismatch + var + a: proc (len: uint) + b: proc (len: csize_t) + c = a + c = b + +block direct_assignment_reverse: + var + a: proc (len: csize_t) + b: proc (len: uint) + c = a + c = b + +block same_backend_type: + # Same backend type should still work + var + a: proc (len: uint) + b: proc (len: uint) + c = a + c = b + +block cint_same_type: + # cint to cint should work + var + a: proc (len: cint) + b: proc (len: cint) + c = a + c = b From f0077a12b20a6cbf3358eaeb09e528ec65e9eca9 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 6 May 2026 13:48:08 +0200 Subject: [PATCH 23/23] fixes DOS via malformed HTTP protocol (#25793) refs https://github.com/nim-lang/Nim/pull/25568 --- lib/pure/asynchttpserver.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pure/asynchttpserver.nim b/lib/pure/asynchttpserver.nim index a88a2d2e43..f757301ed8 100644 --- a/lib/pure/asynchttpserver.nim +++ b/lib/pure/asynchttpserver.nim @@ -153,7 +153,7 @@ proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] = protocol) result.orig = protocol i.inc protocol.parseSaturatedNatural(result.major, i) - i.inc # Skip . + if i < protocol.len: inc i # Skip . i.inc protocol.parseSaturatedNatural(result.minor, i) proc sendStatus(client: AsyncSocket, status: string): Future[void] =