From bbc5bbdcc72c5398c9b495826c3c64e7e491ba6a Mon Sep 17 00:00:00 2001 From: oab24413gmai Date: Thu, 14 May 2026 01:02:11 -0500 Subject: [PATCH 01/33] fix: duplicated words in manual.md and gc_common.nim comment (#25812) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two one-line typo fixes for duplicated words: - `doc/manual.md` — "if the the type was marked as `bycopy`" → "if the type was marked as `bycopy`" - `lib/system/gc_common.nim` — "## thread stack is is returned." → "## thread stack is returned." No code/behavior change. Co-authored-by: Mira Sato <275437409+oab24413gmai@users.noreply.github.com> --- doc/manual.md | 2 +- lib/system/gc_common.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/manual.md b/doc/manual.md index 4b474888d9..0970d0f5b8 100644 --- a/doc/manual.md +++ b/doc/manual.md @@ -8874,7 +8874,7 @@ Byref pragma The `byref` pragma can be applied to an object or tuple type or a proc param. When applied to a type it instructs the compiler to pass the type by reference (hidden pointer) to procs. When applied to a param it will take precedence, even -if the the type was marked as `bycopy`. When an `importc` type has a `byref` pragma or +if the type was marked as `bycopy`. When an `importc` type has a `byref` pragma or parameters are marked as `byref` in an `importc` proc, these params translate to pointers. When an `importcpp` type has a `byref` pragma, these params translate to C++ references `&`. diff --git a/lib/system/gc_common.nim b/lib/system/gc_common.nim index 08e8798b08..1569d12e14 100644 --- a/lib/system/gc_common.nim +++ b/lib/system/gc_common.nim @@ -143,7 +143,7 @@ when nimCoroutines: proc find(first: var GcStack, bottom: pointer): ptr GcStack = ## Find stack struct based on bottom pointer. If `bottom` is nil then main - ## thread stack is is returned. + ## thread stack is returned. if bottom == nil: return addr(gch.stack) From 2c946950f40e5512b41667aa41518ac487aa8d71 Mon Sep 17 00:00:00 2001 From: vip892766gma Date: Thu, 14 May 2026 01:02:36 -0500 Subject: [PATCH 02/33] fix: duplicated "to" in alloc.nim comments (#25813) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two one-line typo fixes for duplicated "to" in `lib/system/alloc.nim`: - "# set 'used' to to true:" → "# set 'used' to true:" (occurs twice, lines ~694 and ~711) No code/behavior change. Co-authored-by: Aiden Park <275402320+vip892766gma@users.noreply.github.com> --- lib/system/alloc.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/system/alloc.nim b/lib/system/alloc.nim index 925f20d906..256c8afd80 100644 --- a/lib/system/alloc.nim +++ b/lib/system/alloc.nim @@ -691,7 +691,7 @@ proc getBigChunk(a: var MemRegion, size: int): PBigChunk = removeChunkFromMatrix2(a, result, fl, sl) if result.size >= size + PageSize: splitChunk(a, result, size) - # set 'used' to to true: + # set 'used' to true: result.prevSize = 1 track("setUsedToFalse", addr result.size, sizeof(int)) sysAssert result.owner == addr a, "getBigChunk: No owner set!" @@ -708,7 +708,7 @@ proc getHugeChunk(a: var MemRegion; size: int): PBigChunk = result.next = nil result.prev = nil result.size = size - # set 'used' to to true: + # set 'used' to true: result.prevSize = 1 result.owner = addr a incl(a, a.chunkStarts, pageIndex(result)) From f9647276d8a9279a7d6eb4591a2feb356a9a4ca7 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Mon, 18 May 2026 13:55:33 +0800 Subject: [PATCH 03/33] fixes #25821; unary minus off by one mistake [backport] (#25823) fixes #25821 This pull request includes a minor bug fix in the lexer and adds new test cases for string formatting with binary operators in interpolated expressions. Lexer bug fix: * Fixed an off-by-one error in the unary minus detection logic in the `rawGetTok` procedure in `lexer.nim`, ensuring that the start-of-buffer condition is correctly checked. Testing improvements: * Added tests to `tstrformat.nim` to verify that binary operators (such as subtraction) work correctly inside interpolated string expressions using both `&` and `fmt`. --- compiler/lexer.nim | 2 +- tests/stdlib/tstrformat.nim | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/compiler/lexer.nim b/compiler/lexer.nim index 9ebec89be5..9c0b803602 100644 --- a/compiler/lexer.nim +++ b/compiler/lexer.nim @@ -1349,7 +1349,7 @@ proc rawGetTok*(L: var Lexer, tok: var Token) = lexMessage(L, errGenerated, "invalid token: no whitespace between number and identifier") of '-': if L.buf[L.bufpos+1] in {'0'..'9'} and - (L.bufpos-1 == 0 or L.buf[L.bufpos-1] in UnaryMinusWhitelist): + (L.bufpos == 0 or L.buf[L.bufpos-1] in UnaryMinusWhitelist): # x)-23 # binary minus # ,-23 # unary minus # \n-78 # unary minus? Yes. diff --git a/tests/stdlib/tstrformat.nim b/tests/stdlib/tstrformat.nim index 74f23b953b..258c190a1d 100644 --- a/tests/stdlib/tstrformat.nim +++ b/tests/stdlib/tstrformat.nim @@ -544,6 +544,12 @@ proc main() = var x = 5 doAssert fmt"{(x=7;123.456)=:13e}" == "(x=7;123.456)= 1.234560e+02" doAssert x==7 + + block: # binary operators in interpolated expressions + let n = 1 + doAssert &"{n-1}" == "0" + doAssert fmt"{n-1}" == "0" + block: #curly bracket expressions and tuples proc formatValue(result: var string; value:Table|bool|JsonNode; specifier:string) = result.add $value From 4f6b727d9ed6d6df5ec6a5488f9693e9f0e8744c Mon Sep 17 00:00:00 2001 From: Pedro Batista Date: Tue, 19 May 2026 18:27:48 -0300 Subject: [PATCH 04/33] pegs: accept UTF-8 bytes in bare identifier terminals (#25829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Fixes `std/pegs` lexing for bare UTF-8 terminals such as `\i café`. - The lexer previously stopped at the first non-ASCII byte, so `pkTerminalIgnoreCase` never saw the full term despite its rune-aware `fastRuneAt`/`toLower` matching. - This now keeps non-ASCII bytes in identifier-style terminals while ASCII non-ident characters still terminate the symbol. ## Behavior Before: `match("CAFÉ", peg"\i café")` failed because the terminal was lexed as `caf`. After: `match("CAFÉ", peg"\i café")`, `match("Café", peg"\i café")`, and `findAll` over mixed-case occurrences pass. `std/pegs` documents `useUnicode = true` as proper UTF-8 support, and quoted terminals already preserved the same bytes; this makes bare terminals consistent with that path. I did not find an existing relevant issue or PR in searches for pegs/unicode/utf8/getSymbol/pkTerminalIgnoreCase. --- changelog.md | 3 +++ lib/pure/pegs.nim | 5 ++++- tests/stdlib/tpegs.nim | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index f87dadae63..af91b49773 100644 --- a/changelog.md +++ b/changelog.md @@ -81,6 +81,9 @@ parameter and result types, not just their source-level shape. Use - `std/re` and `std/nre` are deprecated as PCRE library is obsolete. Use https://github.com/nitely/nim-regex or `std/nre2`. See: https://github.com/nim-lang/Nim/issues/23668. +- `std/pegs` now correctly lexes UTF-8 bytes inside bare identifier-style + terminals, so case-insensitive matching of non-ASCII terms (e.g. ``\i café``) + works without single-quoting. ## Language changes diff --git a/lib/pure/pegs.nim b/lib/pure/pegs.nim index 97d586a7c1..eaece6fa12 100644 --- a/lib/pure/pegs.nim +++ b/lib/pure/pegs.nim @@ -1668,7 +1668,10 @@ func getSymbol(c: var PegLexer, tok: var Token) = while pos < c.buf.len: add(tok.literal, c.buf[pos]) inc(pos) - if pos < c.buf.len and c.buf[pos] notin strutils.IdentChars: break + if pos < c.buf.len: + let ch = c.buf[pos] + # Keep non-ASCII bytes so UTF-8 terminals reach the rune-aware matchers. + if ch notin strutils.IdentChars and ord(ch) < 0x80: break c.bufpos = pos tok.kind = tkIdentifier diff --git a/tests/stdlib/tpegs.nim b/tests/stdlib/tpegs.nim index 18753a9bea..04774aa35b 100644 --- a/tests/stdlib/tpegs.nim +++ b/tests/stdlib/tpegs.nim @@ -259,6 +259,11 @@ block: doAssert match("EINE ÜBERSICHT UND AUSSERDEM", peg"(\upper \white*)+") doAssert(not match("456678", peg"(\letter)+")) + block: + doAssert match("CAFÉ", peg"\i café") + doAssert match("Café", peg"\i café") + doAssert "two cafés: Café and CAFÉ".findAll(peg"\i café").len == 3 + doAssert("var1 = key; var2 = key2".replacef( peg"\skip(\s*) {\ident}'='{\ident}", "$1<-$2$2") == "var1<-keykey;var2<-key2key2") From 9f5c193c1d17d767cd0c3fb938b312e2c23a9ebd Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 19 May 2026 23:28:13 +0200 Subject: [PATCH 05/33] fixes #25814 (#25816) --- compiler/ccgstmts.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/ccgstmts.nim b/compiler/ccgstmts.nim index 5ea23d1f80..83f5e90172 100644 --- a/compiler/ccgstmts.nim +++ b/compiler/ccgstmts.nim @@ -341,9 +341,9 @@ proc genCppParamsForCtor(p: BProc; call: PNode; didGenTemp: var bool): Snippet = call[i][0] else: call[i] - if param.kind != nkBracketExpr or param.typ.kind in + if not param.typ.isCompileTimeOnly and (param.kind != nkBracketExpr or param.typ.kind in {tyRef, tyPtr, tyUncheckedArray, tyArray, tyOpenArray, - tyVarargs, tySequence, tyString, tyCstring, tyTuple}: + tyVarargs, tySequence, tyString, tyCstring, tyTuple}): let tempLoc = initLocExprSingleUse(p, param) didGenTemp = didGenTemp or tempLoc.k == locTemp genOtherArg(p, call, i, typ, res, argBuilder) From 393d27b57da69bd864f456b878ff682a49202b86 Mon Sep 17 00:00:00 2001 From: Rybnikov Alex Date: Thu, 21 May 2026 06:40:35 -0500 Subject: [PATCH 06/33] fix(stdlib): use first-element flag in `$` for collections (#18583) (#25832) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #18583. ## Problem Several stdlib collection types compute the separator for `$` using `result.len > 1`, where `result` starts as the opening bracket (`"["` or `"{"`). This breaks when a collection element type has a `$` that returns an empty string: `result.len` stays at 1 after the first item contributes nothing, so the separator is never inserted for subsequent items. ```nim import std/deques type Test = object proc `$`(x: Test): string = "" echo [Test(), Test()].toDeque # prints [] — expected [, ] ``` ## Fix Replace the length check with an explicit `first` flag in all affected modules: `deques`, `heapqueue`, `lists`, `critbits`, and `strtabs`. ## Tests Regression tests added to `tdeques`, `theapqueue`, and `tlists` using a local type whose `$` returns `""`. All three test files pass with `nim c -r`. ## Notes I work with Claude as a co-processor. I'm 56, came to programming late, and this is genuinely how I learn and contribute. I understand what I'm submitting, but I didn't write it alone. If your project prefers human-only contributions, just say so and I'll close without friction. --------- Co-authored-by: Claude Sonnet 4.6 --- lib/pure/collections/critbits.nim | 7 +++++-- lib/pure/collections/deques.nim | 4 +++- lib/pure/collections/heapqueue.nim | 4 +++- lib/pure/collections/lists.nim | 4 +++- lib/pure/strtabs.nim | 4 +++- tests/stdlib/tdeques.nim | 9 +++++++++ tests/stdlib/theapqueue.nim | 12 ++++++++++++ tests/stdlib/tlists.nim | 11 +++++++++++ 8 files changed, 49 insertions(+), 6 deletions(-) diff --git a/lib/pure/collections/critbits.nim b/lib/pure/collections/critbits.nim index 6435abd0d0..c8ce7a4d20 100644 --- a/lib/pure/collections/critbits.nim +++ b/lib/pure/collections/critbits.nim @@ -495,13 +495,16 @@ func `$`*[T](c: CritBitTree[T]): string = const avgItemLen = 16 result = newStringOfCap(c.count * avgItemLen) result.add("{") + var first = true when T is void: for key in keys(c): - if result.len > 1: result.add(", ") + if first: first = false + else: result.add(", ") result.addQuoted(key) else: for key, val in pairs(c): - if result.len > 1: result.add(", ") + if first: first = false + else: result.add(", ") result.addQuoted(key) result.add(": ") result.addQuoted(val) diff --git a/lib/pure/collections/deques.nim b/lib/pure/collections/deques.nim index 5d67b361ea..0f797fad7d 100644 --- a/lib/pure/collections/deques.nim +++ b/lib/pure/collections/deques.nim @@ -454,8 +454,10 @@ proc `$`*[T](deq: Deque[T]): string = assert $a == "[10, 20, 30]" result = "[" + var first = true for x in deq: - if result.len > 1: result.add(", ") + if first: first = false + else: result.add(", ") result.addQuoted(x) result.add("]") diff --git a/lib/pure/collections/heapqueue.nim b/lib/pure/collections/heapqueue.nim index e83e5abbef..e2b6a6b52a 100644 --- a/lib/pure/collections/heapqueue.nim +++ b/lib/pure/collections/heapqueue.nim @@ -260,7 +260,9 @@ proc `$`*[T](heap: HeapQueue[T]): string = assert $heap == "[1, 2]" result = "[" + var first = true for x in heap.data: - if result.len > 1: result.add(", ") + if first: first = false + else: result.add(", ") result.addQuoted(x) result.add("]") diff --git a/lib/pure/collections/lists.nim b/lib/pure/collections/lists.nim index 6e7de204f1..7ca2242e5f 100644 --- a/lib/pure/collections/lists.nim +++ b/lib/pure/collections/lists.nim @@ -304,8 +304,10 @@ proc `$`*[T](L: SomeLinkedCollection[T]): string = assert $a == "[1, 2, 3, 4]" result = "[" + var first = true for x in nodes(L): - if result.len > 1: result.add(", ") + if first: first = false + else: result.add(", ") result.addQuoted(x.value) result.add("]") diff --git a/lib/pure/strtabs.nim b/lib/pure/strtabs.nim index 4b07aca5a3..663d4c832b 100644 --- a/lib/pure/strtabs.nim +++ b/lib/pure/strtabs.nim @@ -380,8 +380,10 @@ proc `$`*(t: StringTableRef): string {.rtlFunc, extern: "nstDollar".} = result = "{:}" else: result = "{" + var first = true for key, val in pairs(t): - if result.len > 1: result.add(", ") + if first: first = false + else: result.add(", ") result.add(key) result.add(": ") result.add(val) diff --git a/tests/stdlib/tdeques.nim b/tests/stdlib/tdeques.nim index 7d379a5975..e1ad50a84c 100644 --- a/tests/stdlib/tdeques.nim +++ b/tests/stdlib/tdeques.nim @@ -241,3 +241,12 @@ proc main() = static: main() main() + +# https://github.com/nim-lang/Nim/issues/18583 +# $ separator must be emitted even when the item's string repr is empty +type EmptyStr18583 = object +proc `$`(x: EmptyStr18583): string = "" + +block: + var d = [EmptyStr18583(), EmptyStr18583()].toDeque + doAssert $d == "[, ]", "got: " & $d diff --git a/tests/stdlib/theapqueue.nim b/tests/stdlib/theapqueue.nim index afb09c7e3f..92964c5641 100644 --- a/tests/stdlib/theapqueue.nim +++ b/tests/stdlib/theapqueue.nim @@ -104,3 +104,15 @@ template main() = static: main() main() + +# https://github.com/nim-lang/Nim/issues/18583 +type EmptyStr18583HeapQ = object +proc `$`(x: EmptyStr18583HeapQ): string = "" +proc `<`(a, b: EmptyStr18583HeapQ): bool = false + +block: + var h = initHeapQueue[EmptyStr18583HeapQ]() + push(h, EmptyStr18583HeapQ()) + push(h, EmptyStr18583HeapQ()) + let s = $h + doAssert s == "[, ]", "got: " & s diff --git a/tests/stdlib/tlists.nim b/tests/stdlib/tlists.nim index 9339a6df05..cffb00d586 100644 --- a/tests/stdlib/tlists.nim +++ b/tests/stdlib/tlists.nim @@ -287,3 +287,14 @@ template main = static: main() main() + +# https://github.com/nim-lang/Nim/issues/18583 +type EmptyStr18583List = object +proc `$`(x: EmptyStr18583List): string = "" + +block: + var L: SinglyLinkedList[EmptyStr18583List] + L.prepend(EmptyStr18583List()) + L.prepend(EmptyStr18583List()) + let s = $L + doAssert s == "[, ]", "got: " & s From 43ac102ca87cacb0858f47388d7ce3af590687b2 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Thu, 21 May 2026 19:42:38 +0800 Subject: [PATCH 07/33] fixes #25800; move now uses its declaration for overridden =wasMoved (#25809) fixes #25800 closes https://github.com/nim-lang/Nim/pull/25807 ref https://github.com/nim-lang/Nim/issues/25800 This pull request improves the handling of move semantics and the `=wasMoved` hook in the Nim compiler, especially for C++ code generation and user-defined types. It refactors the move operation logic to better support custom hooks, adds new tests for edge cases, and ensures that the `move` operation is safer and more predictable. **Move semantics and `=wasMoved` handling:** * Refactored the move operation in `compiler/ccgexprs.nim` by introducing helper procs (`canGenMoveCall`, `genMoveCall`, `genWasMovedCall`, `genMoveWithWasMoved`) to better handle cases with user-defined `=wasMoved` hooks, especially for generics and C++ interop. The logic now distinguishes between simple assignments and when to call custom hooks, improving correctness and maintainability. [[1]](diffhunk://#diff-4509107d295d7d32b1887c8993cd0f56113ae60f36113e7d8778646dabd92ebcL2818-R2851) [[2]](diffhunk://#diff-4509107d295d7d32b1887c8993cd0f56113ae60f36113e7d8778646dabd92ebcL2841-R2882) * Updated the `move` proc in `lib/system.nim` to include the `nodestroy` pragma, preventing double destruction and making move semantics safer. **Testing and validation:** * Added a new test (`tests/ccgbugs2/t25800.nim`) to ensure that user-defined `=wasMoved` hooks with `{.importcpp.}` are correctly generated and invoked in C++ code, addressing a specific bug with invalid preprocessor directives. * Expanded `tests/destructor/twasmoved.nim` with additional test cases for objects with and without custom `=wasMoved` hooks, including multithreaded scenarios using `threadpool`, to verify correct behavior in a variety of contexts. **Minor cleanup:** * Added a blank line for code style consistency in `compiler/semmagic.nim`. --- compiler/ccgexprs.nim | 27 +++++-------------- compiler/spawn.nim | 24 ++++++++++++++--- lib/system.nim | 2 +- tests/ccgbugs2/m25800.h | 7 +++++ tests/ccgbugs2/t25800.nim | 23 ++++++++++++++++ tests/destructor/twasmoved.nim | 48 ++++++++++++++++++++++++++++++++++ 6 files changed, 107 insertions(+), 24 deletions(-) create mode 100644 tests/ccgbugs2/m25800.h create mode 100644 tests/ccgbugs2/t25800.nim diff --git a/compiler/ccgexprs.nim b/compiler/ccgexprs.nim index 2cf187e687..62302146f1 100644 --- a/compiler/ccgexprs.nim +++ b/compiler/ccgexprs.nim @@ -2816,9 +2816,9 @@ proc genWasMoved(p: BProc; n: PNode) = # [addrLoc(p.config, a), getTypeDesc(p.module, a.t)]) proc genMove(p: BProc; n: PNode; d: var TLoc) = - var a: TLoc = initLocExpr(p, n[1].skipAddr, {lfEnforceDeref, lfPrepareForMutation}) if n.len == 4: # generated by liftdestructors: + var a: TLoc = initLocExpr(p, n[1].skipAddr, {lfEnforceDeref, lfPrepareForMutation}) var src: TLoc = initLocExpr(p, n[2]) let destVal = rdLoc(a) let srcVal = rdLoc(src) @@ -2838,29 +2838,16 @@ proc genMove(p: BProc; n: PNode; d: var TLoc) = else: if d.k == locNone: d = getTemp(p, n.typ) if p.config.selectedGC in {gcArc, gcAtomicArc, gcOrc, gcYrc}: - genAssignment(p, d, a, {}) var op = getAttachedOp(p.module.g.graph, n.typ, attachedWasMoved) - if op == nil: + if op == nil or sfOverridden notin op.flags: + var a: TLoc = initLocExpr(p, n[1].skipAddr, {lfEnforceDeref, lfPrepareForMutation}) + genAssignment(p, d, a, {}) resetLoc(p, a) else: - var b = initLocExpr(p, newSymNode(op)) - case skipTypes(a.t, abstractVar+{tyStatic}).kind - of tyOpenArray, tyVarargs: # todo fixme generated `wasMoved` hooks for - # openarrays, but it probably shouldn't? - let ra = rdLoc(a) - var s: string - if reifiedOpenArray(a.lode): - if a.t.kind in {tyVar, tyLent}: - s = derefField(ra, "Field0") & cArgumentSeparator & derefField(ra, "Field1") - else: - s = dotField(ra, "Field0") & cArgumentSeparator & dotField(ra, "Field1") - else: - s = ra & cArgumentSeparator & ra & "Len_0" - p.s(cpsStmts).addCallStmt(rdLoc(b), s) - else: - let val = if p.module.compileToCpp: rdLoc(a) else: byRefLoc(p, a) - p.s(cpsStmts).addCallStmt(rdLoc(b), val) + n[1] = makeAddr(n[1], p.module.idgen) + genCall(p, n, d) else: + var a: TLoc = initLocExpr(p, n[1].skipAddr, {lfEnforceDeref, lfPrepareForMutation}) genAssignment(p, d, a, {}) resetLoc(p, a) diff --git a/compiler/spawn.nim b/compiler/spawn.nim index 1318ad4b76..cbadc807c6 100644 --- a/compiler/spawn.nim +++ b/compiler/spawn.nim @@ -10,7 +10,7 @@ ## This module implements threadpool's ``spawn``. import ast, types, idents, magicsys, msgs, options, modulegraphs, - lowerings, liftdestructors, renderer + lowerings, liftdestructors, renderer, trees from trees import getMagic, getRoot proc callProc(a: PNode): PNode = @@ -53,6 +53,24 @@ proc typeNeedsNoDeepCopy(t: PType): bool = if t.kind in {tyVar, tyLent, tySequence}: t = t.elementType result = not containsGarbageCollectedRef(t) +proc newSpawnMoveStmt(g: ModuleGraph; idgen: IdGenerator; le, ri: PNode): PNode = + let op = getAttachedOp(g, ri.typ.skipTypes({tyGenericInst, tyAlias, tyVar, tySink}), attachedWasMoved) + if op != nil and sfOverridden in op.flags: + result = newNodeI(nkStmtList, le.info) + result.add newFastAsgnStmt(le, ri) + + let wasMovedCall = newNodeI(nkCall, ri.info) + wasMovedCall.add newSymNode(op) + + if op.typ != nil and op.typ.signatureLen > 1 and op.typ.firstParamType.kind != tyVar: + wasMovedCall.add ri.skipAddr + else: + wasMovedCall.add makeAddr(ri.skipAddr, idgen) + + result.add wasMovedCall + else: + result = newFastMoveStmt(g, le, ri) + proc addLocalVar(g: ModuleGraph; varSection, varInit: PNode; idgen: IdGenerator; owner: PSym; typ: PType; v: PNode; useShallowCopy=false): PSym = result = newSym(skTemp, getIdent(g.cache, genPrefix), idgen, owner, varSection.info, @@ -68,10 +86,10 @@ proc addLocalVar(g: ModuleGraph; varSection, varInit: PNode; idgen: IdGenerator; if varInit != nil: if g.config.selectedGC in {gcArc, gcOrc, gcAtomicArc, gcYrc}: # inject destructors pass will do its own analysis - varInit.add newFastMoveStmt(g, newSymNode(result), v) + varInit.add newSpawnMoveStmt(g, idgen, newSymNode(result), v) else: if useShallowCopy and typeNeedsNoDeepCopy(typ) or optTinyRtti in g.config.globalOptions: - varInit.add newFastMoveStmt(g, newSymNode(result), v) + varInit.add newSpawnMoveStmt(g, idgen, newSymNode(result), v) else: let deepCopyCall = newNodeI(nkCall, varInit.info, 3) deepCopyCall[0] = newSymNode(getSysMagic(g, varSection.info, "deepCopy", mDeepCopy)) diff --git a/lib/system.nim b/lib/system.nim index 63989b1502..26232971bd 100644 --- a/lib/system.nim +++ b/lib/system.nim @@ -166,7 +166,7 @@ proc wasMoved*[T](obj: var T) {.magic: "WasMoved", noSideEffect.} ## it was "moved" and to signify its destructor should do nothing and ## ideally be optimized away. -proc move*[T](x: var T): T {.magic: "Move", noSideEffect.} = +proc move*[T](x: var T): T {.magic: "Move", noSideEffect, nodestroy.} = result = x {.cast(raises: []), cast(tags: []).}: `=wasMoved`(x) diff --git a/tests/ccgbugs2/m25800.h b/tests/ccgbugs2/m25800.h new file mode 100644 index 0000000000..7961eceda0 --- /dev/null +++ b/tests/ccgbugs2/m25800.h @@ -0,0 +1,7 @@ +/*TYPESECTION*/ +struct CppRef { + int* data; + CppRef() : data(new int(42)) {} + ~CppRef() { delete data; data = nullptr; } + void reset() { delete data; data = nullptr; } +}; \ No newline at end of file diff --git a/tests/ccgbugs2/t25800.nim b/tests/ccgbugs2/t25800.nim new file mode 100644 index 0000000000..9574c35009 --- /dev/null +++ b/tests/ccgbugs2/t25800.nim @@ -0,0 +1,23 @@ +discard """ + cmd: "nim cpp $file" + action: "compile" +""" + +# Bug Report 1: {.importcpp.} on =wasMoved generates invalid preprocessor directive #. + + +type CppRef* {.importcpp, bycopy, noInit, header: "m25800.h".} = object + +proc `=destroy`(x: var CppRef) {.importcpp: "#.~CppRef()".} +proc `=wasMoved`(x: var CppRef) {.importcpp: "#.reset()".} +proc `=copy`(dest: var CppRef; src: CppRef) {.importcpp: "dest = src".} +proc `=sink`(dest: var CppRef; src: CppRef) {.importcpp: "dest = std::move(src)".} + +# This triggers =wasMoved when passing to sink parameter +proc consume(x: sink CppRef) = discard + +proc test() = + var x: CppRef + consume(move(x)) # =wasMoved MUST be called here after the move + +test() \ No newline at end of file diff --git a/tests/destructor/twasmoved.nim b/tests/destructor/twasmoved.nim index 5663227022..f9a0ad363a 100644 --- a/tests/destructor/twasmoved.nim +++ b/tests/destructor/twasmoved.nim @@ -12,3 +12,51 @@ proc foo = doAssert m.id == 999 foo() + +block: + type Foo = object + a,b,c: int + + var dest: Foo + + # proc `=wasMoved`(x: var Foo) = + # debugEcho "wasMoved called" + + proc main() = + var x = Foo(a:11, b:12, c:13) + dest = move(x) + + main() + +block: + type Foo = object + a,b,c: int + + var dest: Foo + + proc `=wasMoved`(x: var Foo) = + discard "wasMoved called" + + proc main() = + var x = Foo(a:11, b:12, c:13) + dest = move(x) + + main() + + +import std/threadpool + +block: + type Foo = object + data: string + + proc `=wasMoved`(x: var Foo) = + discard + + proc work(x: Foo) = + discard + + var x = Foo(data: "hello") + spawn work(x) + sync() + From 8771451701d1c9081282d6e9d5cb626bb636da19 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Fri, 22 May 2026 10:32:14 +0800 Subject: [PATCH 08/33] closes #25294; adds a test case (#25833) closes #25294 --- tests/ccgbugs2/m25294/c.nim | 13 +++++++++++++ tests/ccgbugs2/m25294/t.nim | 5 +++++ tests/ccgbugs2/t25294.nim | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 tests/ccgbugs2/m25294/c.nim create mode 100644 tests/ccgbugs2/m25294/t.nim create mode 100644 tests/ccgbugs2/t25294.nim diff --git a/tests/ccgbugs2/m25294/c.nim b/tests/ccgbugs2/m25294/c.nim new file mode 100644 index 0000000000..cd83bdd5fa --- /dev/null +++ b/tests/ccgbugs2/m25294/c.nim @@ -0,0 +1,13 @@ +template a(T: type): int = + when T is uint64: 1 else: 2 + +type + M*[T] = object + data*: seq[T] + b: seq[int] + indices*: array[a(T), int64] + U = distinct uint64 + D* = object + c: M[U] + v: array[180000, int64] + g*: M[uint64] \ No newline at end of file diff --git a/tests/ccgbugs2/m25294/t.nim b/tests/ccgbugs2/m25294/t.nim new file mode 100644 index 0000000000..1d3142b854 --- /dev/null +++ b/tests/ccgbugs2/m25294/t.nim @@ -0,0 +1,5 @@ +import ./c + +proc p*(): D = + let c = M[uint64](data: @[0], indices: [1]) + result = D(g: c) \ No newline at end of file diff --git a/tests/ccgbugs2/t25294.nim b/tests/ccgbugs2/t25294.nim new file mode 100644 index 0000000000..4c47868ee1 --- /dev/null +++ b/tests/ccgbugs2/t25294.nim @@ -0,0 +1,18 @@ +discard """ + matrix: "--mm:refc; --mm:orc" +""" + +import ./m25294/[c, t] + +block: + let a = new D + a[] = p() + discard a[] +block: + let a = new D + a[] = p() + discard a[] +block: + let a = new D + a[] = p() + discard a[] \ No newline at end of file From cfa769fefc86c2f51f5647605622e19a349dd7ee Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Thu, 28 May 2026 05:28:27 +0800 Subject: [PATCH 09/33] fixes #22950; Poor error message on cast effect violation (#25839) fixes #22950 This pull request improves the tracking and reporting of effect annotations (such as `raises`, `tags`, and `forbids`) in pragma blocks, particularly when using the `cast` pragma. It ensures that the source of these effect annotations is correctly preserved and referenced, which improves error reporting and effect analysis. Additionally, a new test was added to check for violations when using `cast` with effect annotations. Effect annotation source tracking and propagation: * Added new fields (`excSource`, `tagsSource`, `forbidsSource`) to the `PragmaBlockContext` type to store the original source node for each effect annotation. * Updated `castBlock` to set these new source fields when processing `raises`, `tags`, and `forbids` pragmas, ensuring the source node is preserved for later error reporting. * Modified `unapplyBlockContext` to use the stored source node (if available) when calling `addRaiseEffect`, `addTag`, and `addNotTag`, improving the accuracy of effect tracking and diagnostics. Pragma handling improvements: * Changed the call to `castBlock` in the main pragma processing loop to pass the entire pragma node, enabling access to the original source for effect annotations. Testing: * Added a new test (`tests/effects/tcast_effect_violation.nim`) to verify that using `cast(raises: ValueError)` inside a procedure with `.raises: [].` correctly triggers an error message about an unlisted exception. --- compiler/sempass2.nim | 15 ++++++++++----- tests/effects/tcast_effect_violation.nim | 8 ++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 tests/effects/tcast_effect_violation.nim diff --git a/compiler/sempass2.nim b/compiler/sempass2.nim index 9c84b721ad..f1e2e69cbe 100644 --- a/compiler/sempass2.nim +++ b/compiler/sempass2.nim @@ -1208,6 +1208,7 @@ type enforcedGcSafety, enforceNoSideEffects: bool oldExc, oldTags, oldForbids: int exc, tags, forbids: PNode + excSource, tagsSource, forbidsSource: PNode proc createBlockContext(tracked: PEffects): PragmaBlockContext = var oldForbidsLen = 0 @@ -1230,17 +1231,18 @@ proc unapplyBlockContext(tracked: PEffects; bc: PragmaBlockContext) = # anything about 'raises' in the 'cast' at all. Same applies for 'tags'. setLen(tracked.exc.sons, bc.oldExc) for e in bc.exc: - addRaiseEffect(tracked, e, e) + addRaiseEffect(tracked, e, if bc.excSource != nil: bc.excSource else: e) if bc.tags != nil: setLen(tracked.tags.sons, bc.oldTags) for t in bc.tags: - addTag(tracked, t, t) + addTag(tracked, t, if bc.tagsSource != nil: bc.tagsSource else: t) if bc.forbids != nil: setLen(tracked.forbids.sons, bc.oldForbids) for t in bc.forbids: - addNotTag(tracked, t, t) + addNotTag(tracked, t, if bc.forbidsSource != nil: bc.forbidsSource else: t) -proc castBlock(tracked: PEffects, pragma: PNode, bc: var PragmaBlockContext) = +proc castBlock(tracked: PEffects, castPragma: PNode, bc: var PragmaBlockContext) = + let pragma = castPragma[1] case whichPragma(pragma) of wGcSafe: bc.enforcedGcSafety = true @@ -1253,6 +1255,7 @@ proc castBlock(tracked: PEffects, pragma: PNode, bc: var PragmaBlockContext) = else: bc.tags = newNodeI(nkArgList, pragma.info) bc.tags.add n + bc.tagsSource = castPragma of wForbids: let n = pragma[1] if n.kind in {nkCurly, nkBracket}: @@ -1260,6 +1263,7 @@ proc castBlock(tracked: PEffects, pragma: PNode, bc: var PragmaBlockContext) = else: bc.forbids = newNodeI(nkArgList, pragma.info) bc.forbids.add n + bc.forbidsSource = castPragma of wRaises: let n = pragma[1] if n.kind in {nkCurly, nkBracket}: @@ -1267,6 +1271,7 @@ proc castBlock(tracked: PEffects, pragma: PNode, bc: var PragmaBlockContext) = else: bc.exc = newNodeI(nkArgList, pragma.info) bc.exc.add n + bc.excSource = castPragma of wUncheckedAssign: discard "handled in sempass1" else: @@ -1520,7 +1525,7 @@ proc track(tracked: PEffects, n: PNode) = of wNoSideEffect: bc.enforceNoSideEffects = true of wCast: - castBlock(tracked, pragmaList[i][1], bc) + castBlock(tracked, pragmaList[i], bc) else: discard applyBlockContext(tracked, bc) diff --git a/tests/effects/tcast_effect_violation.nim b/tests/effects/tcast_effect_violation.nim new file mode 100644 index 0000000000..fe7f93cc4a --- /dev/null +++ b/tests/effects/tcast_effect_violation.nim @@ -0,0 +1,8 @@ +discard """ + errormsg: "cast(raises: ValueError) can raise an unlisted exception: ValueError" + line: 7 +""" + +proc fff() {.raises: [].} = + {.cast(raises: ValueError).}: + discard \ No newline at end of file From 3e2cea21ed3f77320a214539deaf02818e0ba21c Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Thu, 28 May 2026 05:29:27 +0800 Subject: [PATCH 10/33] fixes #22791; ProveField warning with nested case object (#25774) fixes #22791 This pull request introduces a minor improvement to the handling of immutable variables in the compiler and adds a new test case for nested case objects. The most important changes are: ### Compiler improvements * Updated the `isLet` guard in `compiler/guards.nim` to recognize `skConst` symbols as immutable variables, ensuring that constants are correctly identified alongside lets and other immutable types. ### Test coverage * Added a new test in `tests/objvariant/tcorrectcheckedfield.nim` for bug #22791, verifying correct pattern matching and field access in nested `case` objects with constants. --- compiler/guards.nim | 2 +- tests/objvariant/tcorrectcheckedfield.nim | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/compiler/guards.nim b/compiler/guards.nim index 553cc744df..fcfafc954a 100644 --- a/compiler/guards.nim +++ b/compiler/guards.nim @@ -46,7 +46,7 @@ proc isLocation(n: PNode): bool = not n.isValue proc isLet(n: PNode): bool = if n.kind == nkSym: - if n.sym.kind in {skLet, skTemp, skForVar}: + if n.sym.kind in {skLet, skConst, skTemp, skForVar}: # guard immutable variables result = true elif n.sym.kind == skParam and skipTypes(n.sym.typ, abstractInst).kind notin {tyVar}: diff --git a/tests/objvariant/tcorrectcheckedfield.nim b/tests/objvariant/tcorrectcheckedfield.nim index e5e67c727d..acb3acda14 100644 --- a/tests/objvariant/tcorrectcheckedfield.nim +++ b/tests/objvariant/tcorrectcheckedfield.nim @@ -20,3 +20,25 @@ block: # issue #24021 discard else: discard foo.z + + +# bug #22791 +type Foo = object + case a: bool + of false: + discard + of true: + case b: bool + of false: + discard + of true: + c: bool + +const f = Foo(a: true, b: true, c: true) +case f.a +of true: + case f.b + of true: + echo f.c + else: discard +else: discard \ No newline at end of file From f4dd00c4ccfc9791ee21a6d93a18846646613ae6 Mon Sep 17 00:00:00 2001 From: Antonis Geralis <43617260+planetis-m@users.noreply.github.com> Date: Thu, 28 May 2026 00:31:39 +0300 Subject: [PATCH 11/33] Scan until next special char (", \, \0, \c, \L) and append that slice once. (#25498) Benchmark comparison (-d:danger --mm:arc --debugger:native -d:useMalloc, OpenAI file benchmark, 5 runs): - Before: 0.196674934, 0.189423191, 0.198763300, 0.197125584, 0.205015032 - After: 0.182827130, 0.183330852, 0.174878542, 0.174360811, 0.181704921 - Median before: 0.197125584s - Median after: 0.181704921s - Improvement: 7.82% faster Callgrind comparison (same build flags): - Total Ir before: 3,219,477,120 - Total Ir after: 2,449,556,167 - Total Ir reduction: 23.91% parseString hotspot: - Before: 1,343,343,723 Ir - After: 573,423,735 Ir - Reduction: 57.31% --- lib/pure/parsejson.nim | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/pure/parsejson.nim b/lib/pure/parsejson.nim index 9292a85964..657f8cc697 100644 --- a/lib/pure/parsejson.nim +++ b/lib/pure/parsejson.nim @@ -175,23 +175,48 @@ proc parseEscapedUTF16*(buf: cstring, pos: var int): int = else: return -1 +proc addSpan(dst: var string; src: string; startPos, endPos: int) {.inline.} = + let n = endPos - startPos + if n <= 0: + return + + let old = dst.len + dst.setLen old + n + + template impl = + for i in 0.. Date: Fri, 29 May 2026 14:53:37 +0900 Subject: [PATCH 12/33] fixes ReraiseDefect after typeless `except:` + `finally:` (cpp backend) (#25777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Bug A bare `except:` followed by a `finally:` block raises a spurious `ReraiseDefect: no exception to reraise` when compiled with `nim cpp`: ```nim proc test() = try: raise newException(CatchableError, "x") except: discard finally: echo "finally" test() echo "after" ``` Expected output: ``` finally after ``` Actual output: ``` finally fatal.nim(53) sysFatal Error: unhandled exception: no exception to reraise [ReraiseDefect] ``` This reproduces on every memory manager (`--mm:arc`, `--mm:orc`, `--mm:refc`). ## Root cause `genTryCpp` emits `try { ... } catch (Exception* T_) { ... }` followed by a finally block that ends with `if (T_) std::rethrow_exception(T_);`. In the *typed* except branches the codegen explicitly sets `T_ = nullptr;` once the exception is handled, so the rethrow check in the finally is a no-op. The typeless `except:` branch (the `if t[i].len == 1` arm) emitted only `popCurrentException()` and forgot to clear `T_`. After the handler body finished, `T_` still pointed at the original exception, so the trailing `if (T_) std::rethrow_exception(T_);` rethrew it. By that point Nim's current-exception stack had already been popped, and the rethrow surfaced as `ReraiseDefect`. ## Fix Emit `T_ = nullptr;` at the start of the typeless `except:` handler body, mirroring what is already done for the typed branches. This is the same one-line treatment that fixed the analogous typed-except case for #5871. ## Tests Adds `tests/exception/treraise_typeless_except_finally.nim`, exercising the bug pattern on `--mm:arc`, `--mm:orc`, and `--mm:refc`. Locally: - `tests/exception/` — 43 PASS, 0 FAIL, 3 SKIP - new test passes on all three memory managers ## Backport Tagged `[backport]` in the commit message — the same bug exists in `version-2-2` and the fix applies cleanly there. ## Related Independent of, but in the same family as, #25775 (also currently open). Both are silent-finally / cpp-backend exception handling fixes; they touch different lines of `genTryCpp` and don't conflict. Co-authored-by: puffball1567 <17452514+puffball1567@users.noreply.github.com> --- compiler/ccgstmts.nim | 1 + .../treraise_typeless_except_finally.nim | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 tests/exception/treraise_typeless_except_finally.nim diff --git a/compiler/ccgstmts.nim b/compiler/ccgstmts.nim index 83f5e90172..bc9c06fa1d 100644 --- a/compiler/ccgstmts.nim +++ b/compiler/ccgstmts.nim @@ -1237,6 +1237,7 @@ proc genTryCpp(p: BProc, t: PNode, d: var TLoc) = else: scope = initScope(p.s(cpsStmts)) # we handled the error: + linefmt(p, cpsStmts, "T$1_ = nullptr;$n", [etmp]) expr(p, t[i][0], d) linefmt(p, cpsStmts, "#popCurrentException();$n", []) endBlockWith(p): diff --git a/tests/exception/treraise_typeless_except_finally.nim b/tests/exception/treraise_typeless_except_finally.nim new file mode 100644 index 0000000000..259863e4d4 --- /dev/null +++ b/tests/exception/treraise_typeless_except_finally.nim @@ -0,0 +1,30 @@ +discard """ + targets: "cpp" + matrix: "--mm:arc; --mm:orc; --mm:refc" + output: ''' +finally +after +''' +""" + +# Regression test: typeless `except:` followed by `finally:` must not +# trigger ReraiseDefect at the end of the proc. +# +# Previously, `genTryCpp` only emitted `T_ = nullptr;` in the *typed* +# except branches, leaving the typeless `except:` path with a still-set +# `T_`. After the handler body and `popCurrentException`, the trailing +# `if (T_) std::rethrow_exception(T_);` in the finally block would still +# fire — but with the Nim exception stack already popped, the rethrow +# bubbled up as a `ReraiseDefect: no exception to reraise`. + +proc test() = + try: + raise newException(CatchableError, "x") + except: + let e = getCurrentException() + discard e + finally: + echo "finally" + +test() +echo "after" From 645e13173942999ed98c1e545ae241af44b1da50 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Fri, 29 May 2026 13:58:23 +0800 Subject: [PATCH 13/33] fixes #25796; fixes procParamTypeRel to ensure backend type consistency (#25798) fixes #25796 This pull request addresses a subtle type-matching issue in the Nim compiler related to backend type compatibility, particularly for procedures returning `lent` types. It also adds new test cases to ensure correct handling of these scenarios. **Compiler type-checking fix:** * Updated `procParamTypeRel` in `compiler/sigmatch.nim` to skip wrappers like `tyVar`, `tyLent`, `tySink`, and `tyOwned` before comparing backend types, ensuring more accurate type equivalence checks for procedure parameters and return types. **Test coverage improvements:** * Added multiple blocks in `tests/proc/tproc.nim` to test procedure types returning `lent` objects, including cases with constants, variables, and union parameter types, verifying that the compiler now correctly handles these cases. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- compiler/sigmatch.nim | 4 +++- compiler/types.nim | 3 ++- tests/proc/tproc.nim | 27 +++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/compiler/sigmatch.nim b/compiler/sigmatch.nim index bf9c2d2050..db06d3e427 100644 --- a/compiler/sigmatch.nim +++ b/compiler/sigmatch.nim @@ -791,8 +791,10 @@ proc procParamTypeRel(c: var TCandidate; f, a: PType): TTypeRelation = # different C types (size_t vs unsigned long long). let fCheck = concreteType(c, f) let aCheck = concreteType(c, a) + # Note that `result` is equal; now check whether they have the same + # backend type. if fCheck != nil and aCheck != nil and - not sameBackendTypePickyAliases(fCheck, aCheck): + not sameBackendTypePickyAliases(fCheck, aCheck, {IgnoreFlags}): result = isNone if result <= isSubrange or inconsistentVarTypes(f, a): diff --git a/compiler/types.nim b/compiler/types.nim index de24471e7d..f4c7e74bc5 100644 --- a/compiler/types.nim +++ b/compiler/types.nim @@ -1069,9 +1069,10 @@ proc sameBackendTypeIgnoreRange*(x, y: PType): bool = c.cmp = dcEqIgnoreDistinct result = sameTypeAux(x, y, c) -proc sameBackendTypePickyAliases*(x, y: PType): bool = +proc sameBackendTypePickyAliases*(x, y: PType, flags: TTypeCmpFlags = {}): bool = var c = initSameTypeClosure() c.flags.incl {IgnoreTupleFields, IgnoreRangeShallow, PickyCAliases, PickyBackendAliases} + c.flags.incl flags c.cmp = dcEqIgnoreDistinct result = sameTypeAux(x, y, c) diff --git a/tests/proc/tproc.nim b/tests/proc/tproc.nim index d7f8619917..564f9be31b 100644 --- a/tests/proc/tproc.nim +++ b/tests/proc/tproc.nim @@ -29,3 +29,30 @@ block tnestprc: result = x + y result = add(x, 3) doAssert Add3(7) == 10 + +block: + type A = object + c: int + type H = proc(): lent A {.nimcall.} + const u = A(c: 0) + proc e(T: typedesc): lent A = u + proc y(T: typedesc): H = + proc(): lent A {.nimcall.} = T.e + discard y(int) + +block: + type A = object + c: int + type H = proc(): lent A {.nimcall.} + let u = A(c: 0) + proc y(_: int | int): H = + proc(): lent A {.nimcall.} = u + discard y(0) + +block: + type A = object + c: int + type H = proc(): lent A {.nimcall.} + let u = A() + let _: H = proc(): lent A {.nimcall.} = u + From 7813bd8b92824cacec9cddb5152f8c9ed645e03c Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 29 May 2026 08:08:42 +0200 Subject: [PATCH 14/33] fixes #25693 (#25842) --- compiler/sigmatch.nim | 4 +++ tests/template/toverload_over_untyped.nim | 32 +++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 tests/template/toverload_over_untyped.nim diff --git a/compiler/sigmatch.nim b/compiler/sigmatch.nim index db06d3e427..dd5944899b 100644 --- a/compiler/sigmatch.nim +++ b/compiler/sigmatch.nim @@ -2473,6 +2473,10 @@ proc paramTypesMatchAux(m: var TCandidate, f, a: PType, return arg elif f.kind == tyStatic and arg.typ.n != nil: return arg.typ.n + elif f.kind == tyUntyped: + # bug #25693: a different overload candidate may have sem-checked the + # operand and left symbols behind; templates expect the pristine AST. + return argOrig else: return argSemantized # argOrig diff --git a/tests/template/toverload_over_untyped.nim b/tests/template/toverload_over_untyped.nim new file mode 100644 index 0000000000..0f734480d6 --- /dev/null +++ b/tests/template/toverload_over_untyped.nim @@ -0,0 +1,32 @@ +discard """ + output: "ok" +""" + +# bug #25693 + +template g(b: untyped) {.dirty.} = + template t: untyped = b + +proc d() = discard @[0] +proc g(_: int) = discard + +proc f(a: var seq[int], _: string) = + let p = @[0] + d() + a = p + +let q = "a" +g: + var a: seq[int] + try: + f(a, q & "1") + except CatchableError: + discard + try: + f(a, q & "1") + except CatchableError: + discard +block: t() +block: t() +echo "ok" + From 88a18de44f78bf0d0401070bb8cfbc90a78877ba Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Mon, 1 Jun 2026 22:21:37 +0800 Subject: [PATCH 15/33] fixes #25851; ensure --panics:on does not skip nimErr_ check after closure calls (#25855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #25851 ## Summary: `--panics:on` drops `nimErr_` check after closure calls (#25851) ### Bug With `--exceptions:goto` and `--panics:on`, the compiler skipped the `nimErr_` check after indirect closure calls whose result flows directly into another call (e.g., `result.add elem(src)`). A raise inside the closure was silently swallowed — the loop continued, and the next `raise` hit the already-set `nimInErrorMode` flag, overflowing its `bool` storage into `OverflowDefect`. ### Root Cause **ast.nim** — `canRaise` checked `fn.typ.n[0].len < effectListLen` first (false after the expansion) and then `exceptionEffects != nil` (also false, nil), so it returned `false` — meaning "cannot raise." The C codegen trusted this and omitted the `nimErr_` check. ### Fix **ast.nim** — `canRaise` now treats `nil` `exceptionEffects` as "unknown → can raise" (`exceptionEffects == nil` as an additional true condition). This is defense-in-depth: even if some other path expands the list but leaves `exceptionEffects` nil (e.g., a type with `{.tags.}` but no `{.raises.}`), the error check is still emitted. ### Test tclosure_err_panic_goto.nim — exercises the double-trigger pattern (`drawBool` sets the error flag → closure call must propagate it) with `matrix: "; --panics:on"` covering both exception modes. --- compiler/ast.nim | 8 ++++-- tests/ccg/tclosure_err_panic_goto.nim | 36 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/ccg/tclosure_err_panic_goto.nim diff --git a/compiler/ast.nim b/compiler/ast.nim index 5e9680a278..b04a102b20 100644 --- a/compiler/ast.nim +++ b/compiler/ast.nim @@ -1647,9 +1647,13 @@ proc canRaise*(fn: PNode): bool = if fn.typ.n[0].kind == nkSym: result = false else: + # A proc-typed value with no explicit raises slot still has + # unspecified effects, which sempass2 treats conservatively. + # Codegen needs to do the same in order to keep goto-exception + # checks after indirect/closure calls. result = ((fn.typ.n[0].len < effectListLen) or - (fn.typ.n[0][exceptionEffects] != nil and - fn.typ.n[0][exceptionEffects].safeLen > 0)) + fn.typ.n[0][exceptionEffects] == nil or + fn.typ.n[0][exceptionEffects].safeLen > 0) else: result = false diff --git a/tests/ccg/tclosure_err_panic_goto.nim b/tests/ccg/tclosure_err_panic_goto.nim new file mode 100644 index 0000000000..f1619ecff4 --- /dev/null +++ b/tests/ccg/tclosure_err_panic_goto.nim @@ -0,0 +1,36 @@ +discard """ + matrix: "; --panics:on" +""" +# issue #25851: --panics:on must not drop the nimErr_ check after a closure +# call whose result is consumed directly (e.g. `result.add elem(src)`). +# Regression from #25295. + +type + Overrun = object of CatchableError + Source = object + data: seq[bool] + cursor: int + ElemFn = proc(src: var Source): bool {.closure.} + +proc drawBool(src: var Source): bool = + if src.cursor >= src.data.len: raise newException(Overrun, "exhausted") + result = src.data[src.cursor]; inc src.cursor + +proc listRun(elem: ElemFn, src: var Source): seq[bool] = + result = @[] + while true: + if not src.drawBool(): break + result.add elem(src) # closure call – the result flows straight + # into `add`, which previously caused the + # compiler to skip the nimErr_ check. + +let elem: ElemFn = proc(src: var Source): bool = src.drawBool() + +# Both --panics:on and --panics:off must propagate the Overrun. +var caught = false +try: + var src = Source(data: @[true]) + discard listRun(elem, src) +except Overrun: + caught = true +doAssert caught, "Overrun exception was swallowed" From 73986c03a10c1bc5db1edb5750f6841c206b89bf Mon Sep 17 00:00:00 2001 From: Corey Leavitt Date: Mon, 1 Jun 2026 23:07:44 -0600 Subject: [PATCH 16/33] fixes #25857; don't treat typeof(result) as a use-before-init of result (#25858) fixes #25857 ## Bug `typeof(result)` inside the expression that builds `result` gets counted as a read of `result` before it's set. On a `{.requiresInit.}` return type that's a hard error ("'result' requires explicit initialization"). `typeof` never evaluates its operand, so it's a false positive. On 2.2.4 it compiles, but the same line still emits a bogus `ProveInit` warning, so no released version gets it right. Regression from #25151. That PR made a used-before-init `requiresInit` result a hard error instead of a warning, which is correct on its own. The side effect was that this old false-positive warning became a build error. ## Root cause `track` in `compiler/sempass2.nim` has no arm for `nkTypeOfExpr`, so it hits the default that recurses into every child, reaches the `result` `nkSym` inside the `typeof`, and calls `useVar`. `sizeof`/`compiles`/`declared` don't hit this because they fold to a constant before `track` runs. A `typeof(result)` typedesc argument survives into `track`. ## Fix Skip `nkTypeOfExpr` in `track`. Its operand is never evaluated, so it isn't a definite-assignment use. After the patch there's no error and no warning here, even with `--warnings:on`. The #25151 check is untouched: a real use of `result` before init is a plain `nkSym`, not inside a `typeof`, so it still reaches `useVar`. ## Test `tests/init/t25857.nim`, a positive test that compiles and prints `1`. ## Checks - Repro compiles and runs on patched 2.2.6 and patched devel. - `tests/errmsgs/t25117.nim` still fails as expected. A real `xxx(result)` before init still errors. - `testament cat init` and `testament cat errmsgs` green on patched devel (55 tests, 0 failures), including the `--warningAsError:ProveInit` tests. - Bisect: parent `1ab68797` good, `576c4018` (#25151) bad. --- compiler/sempass2.nim | 2 ++ tests/init/t25857.nim | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 tests/init/t25857.nim diff --git a/compiler/sempass2.nim b/compiler/sempass2.nim index f1e2e69cbe..7b2be510f9 100644 --- a/compiler/sempass2.nim +++ b/compiler/sempass2.nim @@ -1308,6 +1308,8 @@ proc allowCStringConv(n: PNode): bool = proc track(tracked: PEffects, n: PNode) = case n.kind + of nkTypeOfExpr: + discard "typeof() never evaluates its operand; not a definite-assignment use" of nkSym: useVar(tracked, n) if n.sym.typ != nil and tfHasAsgn in n.sym.typ.flags: diff --git a/tests/init/t25857.nim b/tests/init/t25857.nim new file mode 100644 index 0000000000..5e71481f5e --- /dev/null +++ b/tests/init/t25857.nim @@ -0,0 +1,19 @@ +discard """ + output: "1" +""" + +# Regression for #25857: `typeof(result)` inside `result`'s initializer must not be +# treated as a use-before-initialization of `result`. `typeof` is a type query and +# never evaluates its operand, so this compiles and runs. +# (Before the fix this errored: "'result' requires explicit initialization" on +# {.requiresInit.} return types, breaking the `ok(typeof(result), v)` idiom.) + +type Box[T] {.requiresInit.} = object + v: T + +func make[T](_: typedesc[Box[T]], v: T): Box[T] = Box[T](v: v) + +proc f(): Box[int] = + make(typeof(result), 1) + +echo f().v From c8e805a2fae2291c306943cbe3eb2fa7a0410c7c Mon Sep 17 00:00:00 2001 From: Corey Leavitt Date: Tue, 2 Jun 2026 23:25:33 -0600 Subject: [PATCH 17/33] fixes #25595; cursor inference: a recorded mutation extends the variable's liveness (#25864) fixes #25595 ## Bug A `let` bound to a field of a value-type **case object** with a `ref` field is inferred as a non-owning cursor, but the cursor's source can be mutated through the cursor's own ref during a call, freeing the ref while the borrow still reads it. Use-after-free under arc/orc (refc is unaffected, it has no cursor inference): ```nim var destroyed = false type O = ref object value: int home: H W = object case k: bool of true: r: O of false: discard H = ref object w: W proc `=destroy`(o: var typeof(O()[])) = destroyed = true proc clear(o: O): int = o.home.w = W() # overwrites h.w via the back-reference -> frees the ref doAssert not destroyed # fails: the element was destroyed during the call result = o.value proc go(h: H): int = let c = h.w # inferred cursor (borrow of h.w) result = clear(c.r) proc main = let h = H() let o = O(value: 42) o.home = h h.w = W(k: true, r: o) doAssert go(h) == 42 main() ``` The `not destroyed` assert fails: the element is destroyed during the call, so the following `o.value` read is a use-after-free. The same code with the `=destroy` guard removed (so the freed `o.value` is actually read) is reported as `heap-use-after-free` by ASan under `-d:useMalloc -fsanitize=address`. Longstanding (reproduces back to 2.2.0). `--cursorInference:off` is a workaround. ## Root cause Cursor inference (`varpartitions.computeCursors`) cursors `let c = h.w` unless `dangerousMutation` finds a mutation of `c`'s graph within `c`'s alive range `aliveStart..aliveEnd`. Here the mutation (the `clear(c.r)` call) *is* connected to `c`'s graph and *is* recorded with `isMutated`, but it is recorded at an `abstractTime` just past `c.aliveEnd`, so the range check misses it. The gap is timing. `aliveEnd` is set from the last `nkSym` use of `c`. A call records its argument's mutation *after* traversing the whole argument subtree (`potentialMutationViaArg`). When the argument is `c.r` on a case object it is an `nkCheckedFieldExpr` (the discriminant check), whose extra nodes advance `abstractTime` past `c`'s last `nkSym`. A plain `nkDotExpr` has no such gap, so the bug needs a case object. ## Fix In `potentialMutation`, extend the mutated variable's liveness to the mutation time: ```nim v.s[id].aliveEnd = max(v.s[id].aliveEnd, v.abstractTime) ``` A variable mutated at time T is provably alive at T, so this only completes the liveness computation that `dangerousMutation` relies on. The worst case is an extra copy, never an unsound cursor. ## Note on the locus The fix is conservative by mechanism (it runs at every recorded mutation) but perf-neutral in practice: it only suppresses a cursor where the corrected liveness proves the borrow unsafe (cursor counts are unchanged on the suites). I can scope it to call arguments if you'd prefer it narrower. ## Test `tests/arc/t25595.nim`, matrix `--mm:orc; --mm:arc; --mm:refc`: the repro above as a `doAssert`. Fails (UAF) on arc/orc before the fix and passes after. refc passes throughout. ## Checks - repro passes on orc/arc after the fix. The guard-removed variant (which reads the freed value) is ASan-clean after the fix and was heap-use-after-free before. refc unaffected. - testament `destructor` 90/90, `arc` 120/120. `views` 5/6, same as stock (the one failure is environmental and pre-exists this change). - perf-neutral: inferred-cursor count is identical stock vs fix across the `arc` and `destructor` test files under `--mm:orc` (322 vs 322). --- compiler/varpartitions.nim | 3 +++ tests/arc/t25595.nim | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/arc/t25595.nim diff --git a/compiler/varpartitions.nim b/compiler/varpartitions.nim index ddba3d4bcb..fea6cc540b 100644 --- a/compiler/varpartitions.nim +++ b/compiler/varpartitions.nim @@ -185,6 +185,9 @@ proc root(v: var Partitions; start: int): int = proc potentialMutation(v: var Partitions; s: PSym; level: int; info: TLineInfo) = let id = variableId(v, s) if id >= 0: + # mutated here => alive here: keep aliveEnd in sync so dangerousMutation catches + # mutations recorded after the var's last use (e.g. via a call arg). See #25595. + v.s[id].aliveEnd = max(v.s[id].aliveEnd, v.abstractTime) let r = root(v, id) let flags = if s.kind == skParam: if isConstParam(s): diff --git a/tests/arc/t25595.nim b/tests/arc/t25595.nim new file mode 100644 index 0000000000..6cfaa7673a --- /dev/null +++ b/tests/arc/t25595.nim @@ -0,0 +1,43 @@ +discard """ + matrix: "--mm:orc; --mm:arc; --mm:refc" +""" + +# bug #25595: cursor inference must not borrow a case object whose source can be +# mutated through the cursor's own ref across a call. `let c = h.w` was inferred as a +# non-owning cursor; `clear(c.r)` overwrites `h.w` via the cursor's back-reference, +# freeing the ref while the borrow still uses it -> use-after-free. Detected here +# deterministically: the element's destructor must not run during the call. + +var destroyed = false + +type + O = ref object + value: int + home: H + W = object + case k: bool + of true: r: O + of false: discard + H = ref object + w: W + +proc `=destroy`(o: var typeof(O()[])) = + destroyed = true + +proc clear(o: O): int = + o.home.w = W() + doAssert not destroyed, "use-after-free: element destroyed during the call" + result = o.value + +proc go(h: H): int = + let c = h.w + result = clear(c.r) + +proc main = + let h = H() + let o = O(value: 42) + o.home = h + h.w = W(k: true, r: o) + doAssert go(h) == 42 + +main() From 4b374eb0a615314f7a1ffac51f2483a1fcea1385 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:29:48 +0800 Subject: [PATCH 18/33] stop a temp register from being freed if addressed for `lent` (#25861) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ref https://github.com/nim-lang/Nim/issues/25849 The important part is in compiler/vmgen.nim:1838: when the VM lowers a[i] or a.b as an address-producing operation, it emits opcLdArrAddr / opcLdObjAddr. That returns an alias into the storage owned by the source register. Before the patch, that source register could still betreated as a normal temporary and later reclaimed or reused by the allocator. Once that happened, the address result was still live, but the backing temp was no longer guaranteed to exist, which is what led to the nil/illegal-storage crash. The fix is to pin that source temp by changing its slot kind to slotTempPerm right after emitting the address load. You can see the same lifetime rule already existed for the generic addr(...) path around compiler/vmgen.nim:1551: if the source is a temporary and we take its address, the compiler marks it permanent so freeTemp won’t recycle it. The patch extends that exact rule to array and object address loads: - compiler/vmgen.nim:1843 - compiler/vmgen.nim:1861 slotTempPerm is outside the normal freeTemp range in compiler/vmgen.nim:248, so once a temp is upgraded to permanent, the VM allocator stops treating it as reusable. That is the actual root-cause fix: it preserves the backing storage for the address result until the surrounding evaluation is done. The regression test in tests/vm/t25849.nim:8 forces exactly that path with a local lent iterator over an array and a static VM evaluation. --- compiler/vmgen.nim | 4 ++++ tests/vm/t25849.nim | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 tests/vm/t25849.nim diff --git a/compiler/vmgen.nim b/compiler/vmgen.nim index 898b5b5def..dd8b8365ca 100644 --- a/compiler/vmgen.nim +++ b/compiler/vmgen.nim @@ -1842,6 +1842,8 @@ proc genArrAccessOpcode(c: PCtx; n: PNode; dest: var TDest; opc: TOpcode; if dest < 0: dest = c.getTemp(n.typ) if opc in {opcLdArrAddr, opcLdStrIdxAddr} and gfNodeAddr in flags: c.gABC(n, opc, dest, a, b) + if c.prc.regInfo[a].kind >= slotTempUnknown: + c.prc.regInfo[a].kind = slotTempPerm elif needsRegLoad(): var cc = c.getTemp(n.typ) c.gABC(n, opc, cc, a, b) @@ -1858,6 +1860,8 @@ proc genObjAccessAux(c: PCtx; n: PNode; a, b: int, dest: var TDest; flags: TGenF if dest < 0: dest = c.getTemp(n.typ) if {gfNodeAddr} * flags != {}: c.gABC(n, opcLdObjAddr, dest, a, b) + if a < c.prc.regInfo.len and c.prc.regInfo[a].kind >= slotTempUnknown: + c.prc.regInfo[a].kind = slotTempPerm elif needsRegLoad(): var cc = c.getTemp(n.typ) c.gABC(n, opcLdObj, cc, a, b) diff --git a/tests/vm/t25849.nim b/tests/vm/t25849.nim new file mode 100644 index 0000000000..a503dd0479 --- /dev/null +++ b/tests/vm/t25849.nim @@ -0,0 +1,16 @@ +discard """ + targets: "c cpp js" +""" + +import std/os +from std/sequtils import toSeq + +iterator items(a: array[3, string]): lent string {.inline.} = + for i in 0..2: + yield a[i] + +static: + const key = "NIM_TESTS_TOSENV_KEY" + for val in items(["a", "b", "c"]): + putEnv(key, val) + doAssert (key, val) in toSeq(envPairs()) From 46259cd0b82455c6a97dc7a70198f842c4ef02ff Mon Sep 17 00:00:00 2001 From: Ryan McConnell Date: Fri, 5 Jun 2026 10:37:00 -0400 Subject: [PATCH 19/33] fix sortVTableDispatchers KeyError on re-entrant method registration via when isMainModule (#25856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encountered in realistic scenario. Didn't really look at this one. AI one shot it lol When a module defines method-bearing types and a when isMainModule block imports additional modules that also define methods on the same type hierarchy, sortVTableDispatchers crashes with: Error: unhandled exception: key not found: (module: N, item: M) [KeyError] Root cause: the itemTable built during vtable sorting is populated from g.objectTree[baseType], which only contains types from the current compilation pass. When when isMainModule triggers re-import of method-bearing modules, the method bucket contains types from both passes. Types from the first pass have ItemIds not present in the second pass's object tree, so itemTable[obj.itemId] raises KeyError at line 155. Fix: if obj.itemId is missing from itemTable, create an empty slot array of the correct length. The entry is a local temporary — the second loop in sortVTableDispatchers only calls setMethodsPerType for types in the current object tree, so types from the prior pass retain their already-established dispatch. The entry exists solely to prevent the KeyError during the assignment loop. The methodIndexLen used for the new entry is the bucket's slot count, which is correct for any type in the hierarchy. Added test tests/method/tvtable_reentry.nim that defines methods across three types in two compilation passes and verifies dispatch correctness for all three. --- compiler/vtables.nim | 2 ++ tests/method/mvtables_reentry_a.nim | 5 +++++ tests/method/mvtables_reentry_b.nim | 7 +++++++ tests/method/tvtable_reentry.nim | 19 +++++++++++++++++++ 4 files changed, 33 insertions(+) create mode 100644 tests/method/mvtables_reentry_a.nim create mode 100644 tests/method/mvtables_reentry_b.nim create mode 100644 tests/method/tvtable_reentry.nim diff --git a/compiler/vtables.nim b/compiler/vtables.nim index 9274aa103e..5b05daa868 100644 --- a/compiler/vtables.nim +++ b/compiler/vtables.nim @@ -152,6 +152,8 @@ proc sortVTableDispatchers*(g: ModuleGraph) = rootItemIdCount.inc(baseType.itemId) for idx in 0.. Date: Sat, 6 Jun 2026 13:58:19 +0800 Subject: [PATCH 20/33] fixes #25849; fixes #25872; Iteration on elements of array (#25860) fixes #25849 fixes https://github.com/nim-lang/Nim/issues/25872 --- compiler/transf.nim | 16 +++++++++++--- lib/system/iterators.nim | 4 ++-- tests/lent/titems_array_lent.nim | 38 ++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 tests/lent/titems_array_lent.nim diff --git a/compiler/transf.nim b/compiler/transf.nim index 3059d8fe6b..47be5b9c61 100644 --- a/compiler/transf.nim +++ b/compiler/transf.nim @@ -22,7 +22,7 @@ import std / tables import options, ast, astalgo, trees, msgs, - idents, renderer, types, semfold, magicsys, cgmeth, + idents, renderer, types, semfold, magicsys, cgmeth, parampatterns, lowerings, liftlocals, modulegraphs, lineinfos @@ -675,7 +675,7 @@ type paDirectMapping, paFastAsgn, paFastAsgnTakeTypeFromArg paVarAsgn, paComplexOpenarray, paViaIndirection -proc putArgInto(arg: PNode, formal: PType): TPutArgInto = +proc putArgInto(arg: PNode, formal: PType; borrowedFirstArg = false): TPutArgInto = # This analyses how to treat the mapping "formal <-> arg" in an # inline context. if formal.kind == tyTypeDesc: return paDirectMapping @@ -726,6 +726,13 @@ proc putArgInto(arg: PNode, formal: PType): TPutArgInto = if skipTypes(formal, abstractInst).kind in {tyVar, tyLent}: result = paVarAsgn else: result = paFastAsgn + if borrowedFirstArg and result == paDirectMapping and parampatterns.exprRoot(arg) == nil and + parampatterns.isAssignable(nil, arg) == arNone: + # Inline iterators like `items(array)` borrow from the first argument. + # If that argument is just a transient expression, materialize it so the + # lifted closure keeps the backing storage alive across yields. + result = paFastAsgnTakeTypeFromArg + proc findWrongOwners(c: PTransf, n: PNode) = if n.kind == nkVarSection: let x = n[0][0] @@ -824,13 +831,16 @@ proc transformFor(c: PTransf, n: PNode): PNode = if iter.kind != skIterator: return result # generate access statements for the parameters (unless they are constant) pushTransCon(c, newC) + let borrowedIterResult = + iter.typ != nil and iter.typ.returnType != nil and + skipTypes(iter.typ.returnType, abstractInst).kind in {tyLent, tyVar} for i in 1..= ff.n.len: return result var formal = ff.n[i].sym - let pa = putArgInto(arg, formal.typ) + let pa = putArgInto(arg, formal.typ, borrowedIterResult and i == 1) case pa of paDirectMapping: newC.mapping[formal.itemId] = arg diff --git a/lib/system/iterators.nim b/lib/system/iterators.nim index 125bee98ff..d4485ad3e9 100644 --- a/lib/system/iterators.nim +++ b/lib/system/iterators.nim @@ -3,7 +3,7 @@ when defined(nimPreviewSlimSystem): import std/assertions -when not defined(nimNoLentIterators): +when (not defined(nimNoLentIterators)) and not defined(js) and not defined(nimscript): template lent2(T): untyped = lent T else: template lent2(T): untyped = T @@ -37,7 +37,7 @@ iterator mitems*[T](a: var openArray[T]): var T {.inline.} = yield a[i] unCheckedInc(i) -iterator items*[IX, T](a: array[IX, T]): T {.inline.} = +iterator items*[IX, T](a: array[IX, T]): lent2 T {.inline.} = ## Iterates over each item of `a`. when a.len > 0: var i = low(IX) diff --git a/tests/lent/titems_array_lent.nim b/tests/lent/titems_array_lent.nim new file mode 100644 index 0000000000..f423c17963 --- /dev/null +++ b/tests/lent/titems_array_lent.nim @@ -0,0 +1,38 @@ +discard """ + targets: "c cpp js" +""" + +template sameAddress(a, b): bool = + when defined(js): + a == b + else: + a.unsafeAddr == b.unsafeAddr + +proc main() = + block: + let a = [10, 11, 12] + for ai in items(a): + doAssert sameAddress(ai, a[0]) + break + + block: + let a = [[1, 2], [1, 2], [1, 2]] + for ai in items(a): + doAssert sameAddress(ai[0], a[0][0]) + break + + block: + let s = @[(1, 2), (3, 4), (5, 6)] + doAssert (3, 4) in s + +main() + +static: + main() + +block: # issue #25849 + static: + const key = "NIM_TESTS_TOSENV_KEY" + for val in ["val", "", "\xc3\x86"]: + let s = @[(key, "val"), (key, ""), (key, "\xc3\x86")] + doAssert (key, val) in s \ No newline at end of file From 3c6449dbddb28a50f8fac12c6be20f375e1f21d2 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sun, 7 Jun 2026 19:55:56 +0200 Subject: [PATCH 21/33] fixes #25850 (#25875) --- compiler/injectdestructors.nim | 23 ++++++++++++++--- tests/arc/t25850.nim | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 tests/arc/t25850.nim diff --git a/compiler/injectdestructors.nim b/compiler/injectdestructors.nim index 0b2d085a3f..2b5ae6421e 100644 --- a/compiler/injectdestructors.nim +++ b/compiler/injectdestructors.nim @@ -803,6 +803,23 @@ proc hasCustomDestructor(c: Con, t: PType): bool = obj = skipTypes(obj.baseClass, abstractPtrs) result = result or isCustomDestructor(c, obj) +const + exprBranchKinds = {nkStmtListExpr, nkBlockExpr, nkIfExpr, nkCaseStmt, + nkTryStmt, nkPragmaBlock} + +proc distributeAsgn(asgnKind: TNodeKind; dest, ri: PNode; c: var Con; s: var Scope): PNode = + ## Distributes an assignment ``dest = ri`` into the leaf expressions of + ## ``ri`` when ``ri`` is an expression-based control flow construct. This + ## avoids creating pointless intermediate temporaries (bug #25850). The + ## descent is recursive so that nestings like ``block: ...; if c: a else: b`` + ## assign directly to ``dest`` instead of going through a temp per branch. + if ri.kind in exprBranchKinds: + template process(child, s): untyped = + distributeAsgn(asgnKind, dest, child, c, s) + handleNestedTempl(ri, process, willProduceStmt = true) + else: + result = newTree(asgnKind, dest, p(ri, c, s, consumed)) + proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode; tmpFlags = {sfSingleUsedTemp}; inReturn = false): PNode = if n.kind in {nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr, nkIfStmt, nkIfExpr, nkCaseStmt, nkWhen, nkWhileStmt, nkParForStmt, nkTryStmt, nkPragmaBlock}: @@ -1004,13 +1021,11 @@ proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode; tmpFlags = {sfSing result = moveOrCopy(p(n[0], c, s, mode), n[1], c, s, flags) elif isDiscriminantField(n[0]): result = c.genDiscriminantAsgn(s, n) - elif n[1].kind in {nkStmtListExpr, nkBlockExpr, nkIfExpr, nkCaseStmt, nkTryStmt, nkPragmaBlock}: + elif n[1].kind in exprBranchKinds: # Distribute the assignment into each branch to avoid # creating pointless temporaries for expression-based control flow. let dest = p(n[0], c, s, mode) - template process(child, s): untyped = - newTree(n.kind, dest, p(child, c, s, consumed)) - handleNestedTempl(n[1], process, willProduceStmt = true) + result = distributeAsgn(n.kind, dest, n[1], c, s) else: result = copyNode(n) result.add p(n[0], c, s, mode) diff --git a/tests/arc/t25850.nim b/tests/arc/t25850.nim new file mode 100644 index 0000000000..abdb772d37 --- /dev/null +++ b/tests/arc/t25850.nim @@ -0,0 +1,47 @@ +discard """ + cmd: '''nim c --mm:orc --expandArc:uIf --expandArc:uCase $file''' + nimout: ''' +--expandArc: uIf + +block :tmp: + let s = w() + if true: + r[] = s + else: + r[] = s +-- end of expandArc ------------------------ +--expandArc: uCase + +block :tmp: + let s = w() + case n + of 0: + r[] = s + else: + r[] = w() +-- end of expandArc ------------------------ +''' +""" + +# bug #25850 +# Assigning an expression-based control flow construct (an `if`/`case` nested in +# a `block`) must distribute the assignment directly into the leaf branches +# instead of creating redundant intermediate temporaries per branch. + +proc w(): array[1000, byte] {.noinline.} = discard + +proc uIf(r: ptr array[1000, byte]) = + r[] = (block: + let s = w() + if true: s else: s) + +proc uCase(r: ptr array[1000, byte], n: int) = + r[] = (block: + let s = w() + case n + of 0: s + else: w()) + +var d: array[1000, byte] +uIf(addr d) +uCase(addr d, 0) From f959a02037849b45c8d842a7a95b95ba9ced3a3e Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:53:10 +0800 Subject: [PATCH 22/33] fixes #25725; environment misses: s with iterator (#25828) fixes #25725 This pull request makes significant improvements to symbol handling during transformation passes in the compiler, particularly for routines (procedures, iterators) and their parameters. The changes ensure that when routines are copied (for inlining, closure generation, etc.), all relevant symbols and type headers are also freshly copied and correctly owned, preventing subtle bugs from symbol reuse. Additionally, new regression tests are added to cover previously problematic iterator cases. **Improvements to symbol copying and ownership:** * Introduced `freshOwnedSym` to create a fresh copy of a symbol with a specified owner, ensuring that transformed routines and their parameters do not share symbols with the originals, which prevents accidental aliasing and ownership issues. * Refactored `freshVar` to use `freshOwnedSym`, centralizing fresh symbol creation logic. * Added `introduceNewRoutineHeaderSyms` and `copyRoutineTypeHeader` to ensure that when routines are copied, all parameter/result symbols and their types are also freshly copied and mapped, avoiding shared state between original and transformed routines. * Updated `introduceNewLocalVars` to use `freshOwnedSym` for routine symbols and to invoke the new header/type copying procedures, ensuring correctness in routine transformation. **Testing and regression coverage:** * Added new blocks to `tests/iter/titer_issues.nim` to test iterator transformation edge cases, including scenarios that previously led to symbol reuse bugs (e.g., bugs #25724 and #25725). --- compiler/transf.nim | 61 ++++++++++++++++++++++++++++++++----- tests/iter/titer_issues.nim | 22 ++++++++++++- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/compiler/transf.nim b/compiler/transf.nim index 47be5b9c61..f131c862f0 100644 --- a/compiler/transf.nim +++ b/compiler/transf.nim @@ -90,11 +90,21 @@ proc getCurrOwner(c: PTransf): PSym = if c.transCon != nil: result = c.transCon.owner else: result = c.module +proc freshOwnedSym(c: PTransf; s, owner: PSym): PNode = + # We need to copy the symbol here because we might need to change its owner and + # we don't want to mess with the original symbol which might be used in other places. + # This can happen for example for iterators which are transformed multiple times when + # they are used in different contexts. + var fresh = copySym(s, c.idgen) + if fresh.kind notin routineKinds: + incl(fresh.flagsImpl, sfFromGeneric) + setOwner(fresh, owner) + result = newSymNode(fresh) + proc newTemp(c: PTransf, typ: PType, info: TLineInfo): PNode = let r = newSym(skTemp, getIdent(c.graph.cache, genPrefix), c.idgen, getCurrOwner(c), info) r.typ = typ #skipTypes(typ, {tyGenericInst, tyAlias, tySink}) incl(r.flagsImpl, sfFromGeneric) - let owner = getCurrOwner(c) result = newSymNode(r) proc transform(c: PTransf, n: PNode, noConstFold = false): PNode @@ -185,11 +195,39 @@ proc transformSym(c: PTransf, n: PNode): PNode = result = transformSymAux(c, n) proc freshVar(c: PTransf; v: PSym): PNode = - let owner = getCurrOwner(c) - var newVar = copySym(v, c.idgen) - incl(newVar.flagsImpl, sfFromGeneric) - setOwner(newVar, owner) - result = newSymNode(newVar) + result = freshOwnedSym(c, v, getCurrOwner(c)) + +proc introduceNewRoutineHeaderSyms(c: PTransf; n: PNode; oldOwner, newOwner: PSym) = + # We need to introduce new symbols for the parameters and result of a routine when + # we copy it for inlining or closure generation. + # Otherwise, we would have multiple nodes referring to the same parameter symbols which + # can lead to problems when we need to change the owner of these symbols. + case n.kind + of nkSym: + if n.sym.owner == oldOwner: + c.transCon.mapping[n.sym.itemId] = freshOwnedSym(c, n.sym, newOwner) + of nkEmpty..pred(nkSym), succ(nkSym)..nkNilLit: + discard + else: + for i in 0.. 0: + newProc.typ.n.add copyTree(oldProc.typ.n[0]) + for i in 1.. Date: Mon, 8 Jun 2026 14:54:15 +0800 Subject: [PATCH 23/33] fixes #18238; Nested object construction can zero same memory multiple times for `--mm:refc` (#25834) fixes #18238 This pull request makes a targeted change to the object construction logic in the `genObjConstr` procedure. The main update refines the conditions under which memory zeroing is required during object construction, making the behavior more accurate for different garbage collection and destructor options. Key logic update: - Improved the `needsZeroMem` condition in `genObjConstr` to check for the presence of garbage-collected references and the `optSeqDestructors` option, instead of relying solely on the selected garbage collector and field flags. This ensures memory is zeroed only when necessary, potentially improving performance and correctness. ```c T1_ = NIM_NIL; T1_ = ((tyObject_E__uEKympBdEK4SY9anUbpNaLQ*) newObj((&NTIrefe__bJ9cSuxv8xHYxmdolQqFkUw_), sizeof(tyObject_E__uEKympBdEK4SY9anUbpNaLQ))); nimZeroMem(((void*) ((&(*T1_).z.z.z.z))), sizeof(tyObject_A__G2lWlL9cFqoiWWwZmWqfJ9bA)); (*T1_).z.z.z.z.y = ((NI) 5); asgnRef(((void**) ((&z1__test8_u12))), T1_); asgnRef(((void**) ((&z2__test8_u55))), new__test8_u13()); (*z2__test8_u55).z.z.z.z.y = ((NI) 5); T2_ = NIM_NIL; T2_ = ((tyObject_E__uEKympBdEK4SY9anUbpNaLQ*) newObj((&NTIrefe__bJ9cSuxv8xHYxmdolQqFkUw_), sizeof(tyObject_E__uEKympBdEK4SY9anUbpNaLQ))); asgnRef(((void**) ((&z3__test8_u56))), T2_); (*z3__test8_u56).z.z.z.z.y = ((NI) 5); ``` The original test case has already been fixed for `ORC`, now extends it to `refc`: if a constructor is fully initialized, it does not need a zero-fill step --- compiler/ccgexprs.nim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/ccgexprs.nim b/compiler/ccgexprs.nim index 62302146f1..c6e6057223 100644 --- a/compiler/ccgexprs.nim +++ b/compiler/ccgexprs.nim @@ -1904,7 +1904,9 @@ proc genObjConstr(p: BProc, e: PNode, d: var TLoc) = var tmp: TLoc = default(TLoc) var r: Rope - let needsZeroMem = p.config.selectedGC notin {gcArc, gcAtomicArc, gcOrc, gcYrc} or nfAllFieldsSet notin e.flags + let needsZeroMem = + nfAllFieldsSet notin e.flags or + (optSeqDestructors notin p.config.globalOptions and containsGarbageCollectedRef(t)) if useTemp: tmp = getTemp(p, t) r = rdLoc(tmp) From f5930d0bb36c94980e4c0bc3d455c6f42e740672 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:55:37 +0800 Subject: [PATCH 24/33] fixes #20811; Nested proc with inner being generic cannot access parameters of outer proc (#25837) fixes #20811 This pull request addresses issues with parameter capture in nested generic procedures and templates, ensuring that outer parameters are correctly visible and accessible within nested scopes. The main changes include a fix in the semantic analysis logic and the addition of targeted regression tests. ### Semantic analysis improvements: * Updated `semGenericStmtSymbol` in `compiler/semgnrc.nim` to ensure that parameters from outer scopes are preserved and accessible in nested generic procedures, fixing visibility issues with captured parameters. ### Added regression tests: * Added `tests/generics/t20811.nim` to verify that both generic and plain inner procedures can access parameters from their enclosing procedure. * Extended `tests/template/topensym.nim` with a new block for issue #20811 to test that template-injected parameters are correctly captured and visible in nested generic procedures. --- compiler/semgnrc.nim | 9 ++++++++- tests/generics/t20811.nim | 16 ++++++++++++++++ tests/template/topensym.nim | 13 +++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/generics/t20811.nim diff --git a/compiler/semgnrc.nim b/compiler/semgnrc.nim index 10bb33bcdc..1d12ae970a 100644 --- a/compiler/semgnrc.nim +++ b/compiler/semgnrc.nim @@ -129,7 +129,14 @@ proc semGenericStmtSymbol(c: PContext, n: PNode, s: PSym, result.typ = nil onUse(n.info, s) of skParam: - result = n + if s.owner == c.p.owner: + # Parameters of the routine currently being semchecked stay as local + # identifiers + result = n + else: + # Preserve captured outer parameters so nested generic procs can still + # see them after the generic pre-pass. + result = newSymNode(s, n.info) onUse(n.info, s) of skType: if (s.typ != nil) and diff --git a/tests/generics/t20811.nim b/tests/generics/t20811.nim new file mode 100644 index 0000000000..f165782719 --- /dev/null +++ b/tests/generics/t20811.nim @@ -0,0 +1,16 @@ +discard """ + output: '''42 +42''' +""" + +proc outer(j: int) = + proc genericInner[T](): int = + j + + proc plainInner(): int = + j + + echo genericInner[int]() + echo plainInner() + +outer(42) \ No newline at end of file diff --git a/tests/template/topensym.nim b/tests/template/topensym.nim index 2f930407b9..25127021d9 100644 --- a/tests/template/topensym.nim +++ b/tests/template/topensym.nim @@ -135,6 +135,19 @@ block: # issue #22605 for templates, original complex example doAssert g2(int) == "error" +block: # issue #20811 + template injectError(body: untyped): untyped = + template error: untyped {.used, inject.} = "injected" + body + + proc outerOpen(error: string): string = + injectError: + proc genericInner[T](): string = + error + genericInner[int]() + + doAssert outerOpen("captured") == "injected" + block: # issue #23865 for templates type Xxx = enum error From 9b80b2e868252d439453aedf0790422e6cb607ec Mon Sep 17 00:00:00 2001 From: Tomohiro Date: Mon, 8 Jun 2026 16:00:00 +0900 Subject: [PATCH 25/33] fixes-25655; defining `>=` operator generates compile error (#25787) Fixes https://github.com/nim-lang/Nim/issues/25655 --------- Co-authored-by: Andreas Rumpf --- compiler/lineinfos.nim | 2 ++ compiler/nilcheck.nim | 6 ------ compiler/semstmts.nim | 5 +++++ tests/proc/tinvalid_cmp_op1.nim | 12 ++++++++++++ tests/proc/tinvalid_cmp_op2.nim | 12 ++++++++++++ tests/proc/tinvalid_cmp_op3.nim | 12 ++++++++++++ 6 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 tests/proc/tinvalid_cmp_op1.nim create mode 100644 tests/proc/tinvalid_cmp_op2.nim create mode 100644 tests/proc/tinvalid_cmp_op3.nim diff --git a/compiler/lineinfos.nim b/compiler/lineinfos.nim index bb3f519535..2ac5cec7aa 100644 --- a/compiler/lineinfos.nim +++ b/compiler/lineinfos.nim @@ -100,6 +100,7 @@ type warnGlobalVarConstructorTemporary = "GlobalVarConstructorTemporary", warnImplicitRangeConversion = "ImplicitRangeConversion", warnSystemRangeConversion = "SystemRangeConversion", + warnInvalidCmpOp = "InvalidCmpOp", # hints hintSuccess = "Success", hintSuccessX = "SuccessX", hintCC = "CC", @@ -210,6 +211,7 @@ const warnGlobalVarConstructorTemporary: "global variable '$1' initialization requires a temporary variable", warnImplicitRangeConversion: "implicit range conversion $1", warnSystemRangeConversion: "implicit range conversion $1", + warnInvalidCmpOp: "$1", hintSuccess: "operation successful: $#", # keep in sync with `testament.isSuccess` hintSuccessX: "$build\n$loc lines; ${sec}s; $mem; proj: $project; out: $output", diff --git a/compiler/nilcheck.nim b/compiler/nilcheck.nim index 7e0efc34bb..fad9bd5b8a 100644 --- a/compiler/nilcheck.nim +++ b/compiler/nilcheck.nim @@ -183,12 +183,6 @@ func `<`*(a: ExprIndex, b: ExprIndex): bool = func `<=`*(a: ExprIndex, b: ExprIndex): bool = a.int16 <= b.int16 -func `>`*(a: ExprIndex, b: ExprIndex): bool = - a.int16 > b.int16 - -func `>=`*(a: ExprIndex, b: ExprIndex): bool = - a.int16 >= b.int16 - func `==`*(a: ExprIndex, b: ExprIndex): bool = a.int16 == b.int16 diff --git a/compiler/semstmts.nim b/compiler/semstmts.nim index 465276ffc2..8b9ce24efa 100644 --- a/compiler/semstmts.nim +++ b/compiler/semstmts.nim @@ -2642,6 +2642,11 @@ proc semProcAux(c: PContext, n: PNode, kind: TSymKind, elif s.name.s == "()" and callOperator notin c.features: localError(c.config, n.info, "the overloaded " & s.name.s & " operator has to be enabled with {.experimental: \"callOperator\".}") + elif sfImportc notin s.flags and (s.name.s == ">" or s.name.s == ">=" or s.name.s == "!="): + # ignore imported procs as these operators in backend language might have different semantics + let op1 = if s.name.s == "!=": "==" elif s.name.s == ">": "<" else: "<=" + message(c.config, n.info, warnInvalidCmpOp, "define `" & op1 & "` instead of `" & s.name.s & "` to implement user defined comparison operator. " & + "it allows you to use `" & s.name.s & "` automatically.") if sfBorrow in s.flags and c.config.cmd notin cmdDocLike: result[bodyPos] = c.graph.emptyNode diff --git a/tests/proc/tinvalid_cmp_op1.nim b/tests/proc/tinvalid_cmp_op1.nim new file mode 100644 index 0000000000..3b223c8348 --- /dev/null +++ b/tests/proc/tinvalid_cmp_op1.nim @@ -0,0 +1,12 @@ +discard """ + cmd: "nim check $file" + action: compile + nimout: ''' +tinvalid_cmp_op1.nim(12, 1) Warning: define `<=` instead of `>=` to implement user defined comparison operator. it allows you to use `>=` automatically. [InvalidCmpOp] +''' +""" + +# issue #25655 + +type Foo = distinct int +func `>=`(a, b: Foo): bool = int(a) >= int(b) diff --git a/tests/proc/tinvalid_cmp_op2.nim b/tests/proc/tinvalid_cmp_op2.nim new file mode 100644 index 0000000000..861e6959cf --- /dev/null +++ b/tests/proc/tinvalid_cmp_op2.nim @@ -0,0 +1,12 @@ +discard """ + cmd: "nim check $file" + action: compile + nimout: ''' +tinvalid_cmp_op2.nim(12, 1) Warning: define `<` instead of `>` to implement user defined comparison operator. it allows you to use `>` automatically. [InvalidCmpOp] +''' +""" + +# issue #25655 + +type Foo = distinct int +func `>`(a, b: Foo): bool = int(a) > int(b) diff --git a/tests/proc/tinvalid_cmp_op3.nim b/tests/proc/tinvalid_cmp_op3.nim new file mode 100644 index 0000000000..f543edd3f0 --- /dev/null +++ b/tests/proc/tinvalid_cmp_op3.nim @@ -0,0 +1,12 @@ +discard """ + cmd: "nim check $file" + action: compile + nimout: ''' +tinvalid_cmp_op3.nim(12, 1) Warning: define `==` instead of `!=` to implement user defined comparison operator. it allows you to use `!=` automatically. [InvalidCmpOp] +''' +""" + +# issue #25655 + +type Foo = distinct int +func `!=`(a, b: Foo): bool = int(a) != int(b) From 1d7510dff03f492e5aff1ff0806882ed02b1a5bd Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:12:00 +0800 Subject: [PATCH 26/33] fixes #22936; Generic inheritance matching gives type mismatch when object has members (#25836) fixes #22936 This pull request improves the compiler's handling of generic type constraints, specifically for subtypes of generics, and adds a test to cover this behavior. The main changes are an enhancement to the type relationship logic in the compiler and a new test case for generic subtyping with `Future`. ### Compiler improvements for generic subtyping * Updated `typeRel` in `compiler/sigmatch.nim` to allow generic constraints (like `F: Future`) to accept not just direct instantiations but also descendants of the generic family, ensuring more flexible and correct overload resolution. Inheritance depth is now considered for overload ranking, making deeper descendants slightly less preferred, consistent with other inheritance-based matches. ### New test coverage * Added a test in `tests/typerel/t8905.nim` to verify that generic constraints correctly accept subtypes of `Future`, including a custom `B[T, E] = ref object of Future[T]` type, and that overloads like `take`, `takeMany`, and the macro `checkFutures` work as expected with these types. --- compiler/sigmatch.nim | 15 +++++++++++++++ tests/typerel/t8905.nim | 28 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/compiler/sigmatch.nim b/compiler/sigmatch.nim index dd5944899b..5f53d7ef44 100644 --- a/compiler/sigmatch.nim +++ b/compiler/sigmatch.nim @@ -1759,6 +1759,21 @@ proc typeRel(c: var TCandidate, f, aOrig: PType, let ff = last(f) if ff != nil: result = typeRel(c, ff, a, flags) + if result == isNone and a.kind == tyGenericInst and trBindGenericParam in flags: + var depth = -1 + # Generic-parameter constraints like `F: Future` can miss in `last(f)` + # when the actual type inherits from a concrete generic instantiation. + # Keep this fallback scoped to generic-parameter matching so typedesc + # overloads such as `type Future[T]` still prefer more specific + # descendants like `InternalRaisesFuture[T, E]`. + if isGenericSubtype(c, a, f, depth, f) and depth > 0: + var askip = skippedNone + let aobj = a.skipToObject(askip) + if aobj != nil and tfFinal notin aobj.flags: + # Keep overload ranking consistent with other inheritance-based + # matches: deeper descendants are slightly worse candidates. + inc c.inheritancePenalty, depth + int(c.inheritancePenalty < 0) + result = isGeneric of tyGenericInvocation: var x = a.skipGenericAlias if x.kind == tyGenericParam and x.len > 0: diff --git a/tests/typerel/t8905.nim b/tests/typerel/t8905.nim index 9383962cf6..01ade68d0a 100644 --- a/tests/typerel/t8905.nim +++ b/tests/typerel/t8905.nim @@ -5,3 +5,31 @@ type proc newFoo[T](): Foo[T] = Foo[T](newSeq[T]()) var x = newFoo[Bar[int]]() + +# issue #22936 + +import std/macros + +type + InternalFutureBase = object of RootObj + + FutureBase = ref object of InternalFutureBase + + Future[T] = ref object of FutureBase + internalValue: T + + B[T, E] = ref object of Future[T] + +proc take[F: Future](fut: F) = discard + +proc takeMany[F: Future](futs: seq[F]) = discard + +macro checkFutures[F: Future](futs: seq[F]): untyped = + newEmptyNode() + +var future: B[void, void] +var futures: seq[B[void, void]] + +take(future) +takeMany(futures) +checkFutures(futures) From c84764a097a414eef3cbea03b941160f91245d41 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 8 Jun 2026 09:13:26 +0200 Subject: [PATCH 27/33] emit modern NIF-27 (#25877) --- koch.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/koch.nim b/koch.nim index 0ea083fb26..ae1d6557c0 100644 --- a/koch.nim +++ b/koch.nim @@ -16,11 +16,11 @@ const ChecksumsStableCommit = "0b8e46379c5bc1bf73d8b3011908389c60fb9b98" # 2.0.1 SatStableCommit = "e63eaea8baf00bed8bcd5a29ffd8823abb265b39" - NimonyStableCommit = "750aa47f2139fe5ad69f04b44428b752011fe873" # unversioned \ + NimonyStableCommit = "fca0e938b04695a3aa4e85abcc976571189f2bd2" # unversioned \ # Note that Nimony uses Nim as a git submodule but we don't want to install # Nimony's dependency to Nim as we are Nim. So a `git clone` without --recursive # is **required** here. - # Commit from 2026-05-05 + # Commit from 2026-06-08 # examples of possible values for fusion: #head, #ea82b54, 1.2.3 FusionStableHash = "#562467452b32cb7a97410ea177f083e6d8405734" From d9e28aac8ec35f7b3cd00d42a8515c04e72aafed Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 8 Jun 2026 11:32:04 +0200 Subject: [PATCH 28/33] parser: `concept of` (#25878) Co-authored-by: Gerke Max Preussner --- compiler/parser.nim | 12 ++++++++---- doc/grammar.txt | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/compiler/parser.nim b/compiler/parser.nim index 934db857b2..32bf4b3d52 100644 --- a/compiler/parser.nim +++ b/compiler/parser.nim @@ -2241,14 +2241,17 @@ proc parseTypeClassParam(p: var Parser): PNode = proc parseTypeClass(p: var Parser): PNode = #| conceptParam = ('var' | 'out' | 'ptr' | 'ref' | 'static' | 'type')? symbol - #| conceptDecl = 'concept' conceptParam ^* ',' (pragma)? ('of' typeDesc ^* ',')? + #| conceptDecl = 'concept' (conceptParam ^* ',' (pragma)?)? ('of' typeDesc ^* ',')? #| &IND{>} stmt result = newNodeP(nkTypeClassTy, p) getTok(p) if p.tok.tokType == tkComment: skipComment(p, result) - if p.tok.indent < 0: + if p.tok.tokType == tkOf and p.tok.indent < 0: + # new-styled `concept of A, B` on the same line as `concept` + result.add(p.emptyNode) + elif p.tok.indent < 0: var args = newNodeP(nkArgList, p) result.add(args) args.add(p.parseTypeClassParam) @@ -2274,9 +2277,10 @@ proc parseTypeClass(p: var Parser): PNode = result.add(p.emptyNode) if p.tok.tokType == tkComment: skipComment(p, result) - # an initial IND{>} HAS to follow: + # an initial IND{>} HAS to follow, unless this concept inherits requirements: if not realInd(p): - if result.isNewStyleConcept: + let hasParents = result[2].kind != nkEmpty + if result.isNewStyleConcept and not hasParents: parMessage(p, "routine expected, but found '$1' (empty new-styled concepts are not allowed)", p.tok) result.add(p.emptyNode) else: diff --git a/doc/grammar.txt b/doc/grammar.txt index 7d430019b1..a25ace34d6 100644 --- a/doc/grammar.txt +++ b/doc/grammar.txt @@ -188,7 +188,7 @@ objectPart = IND{>} objectPart^+IND{=} DED / objectWhen / objectCase / 'nil' / 'discard' / declColonEquals objectDecl = 'object' ('of' typeDesc)? COMMENT? objectPart conceptParam = ('var' | 'out' | 'ptr' | 'ref' | 'static' | 'type')? symbol -conceptDecl = 'concept' conceptParam ^* ',' (pragma)? ('of' typeDesc ^* ',')? +conceptDecl = 'concept' (conceptParam ^* ',' (pragma)?)? ('of' typeDesc ^* ',')? &IND{>} stmt typeDef = identVisDot genericParamList? pragma '=' optInd typeDefValue indAndComment? From 2d148edeb863730bd179d032549ea070b25a4150 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:47:42 +0800 Subject: [PATCH 29/33] adds a test case for #25872 (#25880) --- tests/lent/tlents.nim | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/lent/tlents.nim b/tests/lent/tlents.nim index 1b14972239..4be3eb8d27 100644 --- a/tests/lent/tlents.nim +++ b/tests/lent/tlents.nim @@ -45,3 +45,13 @@ block: r: R func f(o: O): int = 42 + +block: + iterator j(x: array[1, int]): lent int = yield x[0] + iterator g(): int {.closure.} = + let a = 1 + for w in j([a]): + yield 0 + doAssert w == 1 + for _ in g(): discard + From 7a5e35c83ebdcc8dfe3a415f7397ead2a3823a7d Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 8 Jun 2026 22:54:03 +0200 Subject: [PATCH 30/33] fixes #25693; continues the bugfix story (#25876) --- compiler/lookups.nim | 9 +++ compiler/options.nim | 5 ++ compiler/sem.nim | 20 ++++++ compiler/semdata.nim | 19 +++++- compiler/sigmatch.nim | 10 ++- tests/template/toverload_over_untyped.nim | 83 +++++++++++++++++------ 6 files changed, 124 insertions(+), 22 deletions(-) diff --git a/compiler/lookups.nim b/compiler/lookups.nim index 645956de57..2354edbc97 100644 --- a/compiler/lookups.nim +++ b/compiler/lookups.nim @@ -459,6 +459,15 @@ proc openShadowScope*(c: PContext) = symbols: initStrTable(), depthLevel: c.scopeDepth) +proc rememberShadowDefs*(c: PContext) = + ## bug #25693: a template/macro operand's local definitions are sem-checked in + ## a shadow scope that is then discarded. Record those definitions so that a + ## later re-emission (e.g. a captured `typed` fragment expanded more than once) + ## can be detected as a redefinition rather than silently miscompiled. + for s in c.currentScope.symbols: + if s.kind in {skVar, skLet, skForVar} and {sfGenSym, sfWasGenSym} * s.flags == {}: + c.shadowDiscardedDefs.incl s.id + proc closeShadowScope*(c: PContext) = ## closes the shadow scope, but doesn't merge any of the symbols ## Does not check for unused symbols or missing forward decls since a macro diff --git a/compiler/options.nim b/compiler/options.nim index 7a28b1dc6f..472ce0b4c2 100644 --- a/compiler/options.nim +++ b/compiler/options.nim @@ -262,6 +262,11 @@ type procParamTypeBackendAliases ## Keep the old proc type compatibility rules that ignore backend ## c type aliases. + injectedSymbolRedefinition + ## Allow a template to inject a symbol *definition* that is then emitted + ## more than once (e.g. a `typed` argument captured by a `{.dirty.}` + ## template and re-emitted). This is a redefinition and rejected by + ## default; enabling this restores the old, unsound behavior. See #25693. SymbolFilesOption* = enum disabledSf, writeOnlySf, readOnlySf, v2Sf, stressTest diff --git a/compiler/sem.nim b/compiler/sem.nim index cdda93223a..ba9190f572 100644 --- a/compiler/sem.nim +++ b/compiler/sem.nim @@ -247,6 +247,26 @@ proc newSymG*(kind: TSymKind, n: PNode, c: PContext): PSym = if result.kind notin {kind, skTemp}: localError(c.config, n.info, "cannot use symbol of kind '$1' as a '$2'" % [result.kind.toHumanStr, kind.toHumanStr]) + # bug #25693: a local declared inside a template/macro operand (recorded in + # `shadowDiscardedDefs`) can be captured by a `{.dirty.}` template and + # re-emitted as a definition more than once. The first emission keeps the + # original symbol (so a leaked dirty-template name still resolves); every + # later emission gets a fresh copy, so distinct emissions don't share one + # symbol - which the destructor/liveness analysis would otherwise miscompile. + # Unlike a plain redefinition check this is control-flow agnostic, so the + # common "emit a `typed` body in several mutually-exclusive branches" pattern + # keeps working. gensym'ed locals (and ones derived from a gensym name) are + # excluded: the gensym machinery already keeps their names unique, and a + # fresh copy would reuse the unique name and clash in the same scope. + if kind in {skVar, skLet, skForVar} and + {sfGenSym, sfWasGenSym} * result.flags == {} and + result.id in c.shadowDiscardedDefs: + if containsOrIncl(c.realizedDefs, result.id): + let fresh = copySym(result, c.idgen) + fresh.ast = result.ast + put(c.p, result, fresh) + c.hasSymRedefs = true + result = fresh when false: if sfGenSym in result.flags and result.kind notin {skTemplate, skMacro, skParam}: # declarative context, so produce a fresh gensym: diff --git a/compiler/semdata.nim b/compiler/semdata.nim index 32c98cdb31..8e3156db97 100644 --- a/compiler/semdata.nim +++ b/compiler/semdata.nim @@ -189,6 +189,18 @@ type inTypeofContext*: int semAsgnOpr*: proc (c: PContext; n: PNode; k: TNodeKind): PNode {.nimcall.} + shadowDiscardedDefs*: IntSet + # ids of local symbols that were declared inside a template/macro operand's + # shadow scope and then discarded; re-emitting such a symbol as a + # definition gives a fresh copy so distinct emissions don't share a symbol. + # See bug #25693 and `rememberShadowDefs`. + realizedDefs*: IntSet + # ids from `shadowDiscardedDefs` already realized once; the first emission + # keeps the original symbol (so leaked dirty-template names still resolve), + # later emissions get a fresh copy. + hasSymRedefs*: bool + # set once a redefinition mapping has been installed; makes `getGenSym` + # consult the proc-con mapping for non-gensym symbols too. TBorrowState* = enum bsNone, bsReturnNotMatch, bsNoDistinct, bsGeneric, bsNotSupported, bsMatch @@ -281,7 +293,10 @@ proc get*(p: PProcCon; key: PSym): PSym = result = p.mapping.getOrDefault(key.itemId) proc getGenSym*(c: PContext; s: PSym): PSym = - if sfGenSym notin s.flags: return s + # `c.hasSymRedefs` additionally routes ordinary (non-gensym) symbols through + # the mapping so a re-emitted definition can redirect them to its fresh copy, + # see bug #25693 and `newSymG`. + if sfGenSym notin s.flags and not c.hasSymRedefs: return s var it = c.p while it != nil: result = get(it, s) @@ -343,6 +358,8 @@ proc newContext*(graph: ModuleGraph; module: PSym): PContext = userPragmas: initStrTable(), generics: @[], unknownIdents: initIntSet(), + shadowDiscardedDefs: initIntSet(), + realizedDefs: initIntSet(), cache: graph.cache, graph: graph, signatures: initStrTable(), diff --git a/compiler/sigmatch.nim b/compiler/sigmatch.nim index 5f53d7ef44..cb79823af5 100644 --- a/compiler/sigmatch.nim +++ b/compiler/sigmatch.nim @@ -2870,6 +2870,7 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int if m.calleeSym != nil and m.calleeSym.kind notin {skTemplate, skMacro}: c.mergeShadowScope else: + c.rememberShadowDefs c.closeShadowScope m.state = csNoMatch m.firstMismatch.arg = a @@ -2926,7 +2927,10 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int setSon(m.call, formal.position + 1, container) else: incrIndexType(container.typ) - container.add n[a] + # bug #25693: like the scalar `tyUntyped` case in `paramTypesMatchAux`, + # a previous overload candidate may have sem-checked the operand in + # place; templates/macros expect the pristine AST, so use `nOrig`. + container.add nOrig[a] elif n[a].kind == nkExprEqExpr: # named param m.firstMismatch.kind = kUnknownNamedParam @@ -3025,7 +3029,8 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int setSon(m.call, formal.position + 1, container) else: incrIndexType(container.typ) - container.add n[a] + # bug #25693: see the leading isVarargsUntyped branch above. + container.add nOrig[a] else: m.baseTypeMatch = false m.typedescMatched = false @@ -3077,6 +3082,7 @@ proc matchesAux(c: PContext, n, nOrig: PNode, m: var TCandidate, marker: var Int if m.state == csMatch and not (m.calleeSym != nil and m.calleeSym.kind in {skTemplate, skMacro}): c.mergeShadowScope else: + c.rememberShadowDefs c.closeShadowScope inc a diff --git a/tests/template/toverload_over_untyped.nim b/tests/template/toverload_over_untyped.nim index 0f734480d6..ff76663915 100644 --- a/tests/template/toverload_over_untyped.nim +++ b/tests/template/toverload_over_untyped.nim @@ -1,32 +1,77 @@ discard """ - output: "ok" + output: '''ok +ok +ok''' """ # bug #25693 -template g(b: untyped) {.dirty.} = - template t: untyped = b - proc d() = discard @[0] -proc g(_: int) = discard proc f(a: var seq[int], _: string) = let p = @[0] d() a = p -let q = "a" -g: - var a: seq[int] - try: - f(a, q & "1") - except CatchableError: - discard - try: - f(a, q & "1") - except CatchableError: - discard -block: t() -block: t() -echo "ok" +block: # scalar `untyped` parameter + template g(b: untyped) {.dirty.} = + template t: untyped = b + proc g(_: int) = discard + + let q = "a" + g: + var a: seq[int] + try: + f(a, q & "1") + except CatchableError: + discard + try: + f(a, q & "1") + except CatchableError: + discard + block: t() + block: t() + echo "ok" + +block: # `typed` parameter captured and re-emitted: each emission gets its own + # symbols, otherwise the destructor/liveness pass miscompiles the shared + # local `a` and the program crashes at runtime + template g(b: typed) {.dirty.} = + template t: untyped = b + + let q = "a" + g: + var a: seq[int] + try: + f(a, q & "1") + except CatchableError: + discard + try: + f(a, q & "1") + except CatchableError: + discard + block: t() + block: t() + echo "ok" + +block: # `varargs[untyped]` parameter takes the same pristine-AST path + template g(b: varargs[untyped]) {.dirty.} = + template t: untyped = b + + proc g(_: int) = discard + + let q = "a" + g: + var a: seq[int] + try: + f(a, q & "1") + except CatchableError: + discard + try: + f(a, q & "1") + except CatchableError: + discard + block: t() + block: t() + echo "ok" From b000d4a32a1e9707644df20b4d7894311a01ab33 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Tue, 9 Jun 2026 04:57:33 +0800 Subject: [PATCH 31/33] uses lent for `sets` (#25882) --- lib/pure/collections/sets.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pure/collections/sets.nim b/lib/pure/collections/sets.nim index 8a69304898..c2c323b927 100644 --- a/lib/pure/collections/sets.nim +++ b/lib/pure/collections/sets.nim @@ -246,7 +246,7 @@ proc toHashSet*[A](keys: openArray[A]): HashSet[A] = result = initHashSet[A](keys.len) for key in items(keys): result.incl(key) -iterator items*[A](s: HashSet[A]): A = +iterator items*[A](s: HashSet[A]): lent A = ## Iterates over elements of the set `s`. ## ## If you need a sequence with the elements you can use `sequtils.toSeq @@ -891,7 +891,7 @@ proc `$`*[A](s: OrderedSet[A]): string = ## ``` dollarImpl() -iterator items*[A](s: OrderedSet[A]): A = +iterator items*[A](s: OrderedSet[A]): lent A = ## Iterates over keys in the ordered set `s` in insertion order. ## ## If you need a sequence with the elements you can use `sequtils.toSeq From b6842c144d28d82d335dab5515a2fe75f969d84d Mon Sep 17 00:00:00 2001 From: Aleksei Rybnikov <14005836+a-rybnikov@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:58:44 -0500 Subject: [PATCH 32/33] fix(uri): `?` operator now appends to existing query string (#25831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #19782. The `?` operator in `std/uri` was silently overwriting any query string already present in the URI. This PR makes it append instead — which matches the docstring ("Concatenates the query parameters") and the natural expectation when chaining operations. **Before:** ```nim let u = parseUri("https://example.com/foo?existing=1") ? {"bar": "qux"} echo $u # https://example.com/foo?bar=qux (existing=1 lost) ``` **After:** ```nim let u = parseUri("https://example.com/foo?existing=1") ? {"bar": "qux"} echo $u # https://example.com/foo?existing=1&bar=qux ``` ## Changes - `lib/pure/uri.nim`: fix `?` to append with `&` when a query string already exists; add example to `runnableExamples` - `tests/stdlib/turi.nim`: two new test cases (append to existing query, empty params preserve existing) - `changelog.md`: entry under Standard library changes ## Notes I work with Claude as a co-processor. I'm 56, came to programming late, and this is genuinely how I learn and contribute. I understand what I'm submitting, but I didn't write it alone. If your project prefers human-only contributions, just say so and I'll close without friction. --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: n0madgang <14005836+n0madgang@users.noreply.github.com> --- changelog.md | 2 ++ lib/pure/uri.nim | 9 ++++++++- tests/stdlib/turi.nim | 9 +++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index af91b49773..ce7596d5b8 100644 --- a/changelog.md +++ b/changelog.md @@ -84,6 +84,8 @@ parameter and result types, not just their source-level shape. Use - `std/pegs` now correctly lexes UTF-8 bytes inside bare identifier-style terminals, so case-insensitive matching of non-ASCII terms (e.g. ``\i café``) works without single-quoting. +- `std/uri`: The `?` operator now appends query parameters to an existing query + string instead of replacing it. Fixes [#19782](https://github.com/nim-lang/Nim/issues/19782). ## Language changes diff --git a/lib/pure/uri.nim b/lib/pure/uri.nim index e57587ba47..c20049de12 100644 --- a/lib/pure/uri.nim +++ b/lib/pure/uri.nim @@ -487,11 +487,18 @@ func `/`*(x: Uri, path: string): Uri = func `?`*(u: Uri, query: openArray[(string, string)]): Uri = ## Concatenates the query parameters to the specified URI object. + ## If the URI already has a query string, the new parameters are appended. runnableExamples: let foo = parseUri("https://example.com") / "foo" ? {"bar": "qux"} assert $foo == "https://example.com/foo?bar=qux" + let bar = parseUri("https://example.com/foo?existing=1") ? {"bar": "qux"} + assert $bar == "https://example.com/foo?existing=1&bar=qux" result = u - result.query = encodeQuery(query) + let newQuery = encodeQuery(query) + if newQuery.len > 0: + if result.query.len > 0: + result.query.add('&') + result.query.add(newQuery) func `$`*(u: Uri): string = ## Returns the string representation of the specified URI object. diff --git a/tests/stdlib/turi.nim b/tests/stdlib/turi.nim index 9c717c5b15..71aea6af14 100644 --- a/tests/stdlib/turi.nim +++ b/tests/stdlib/turi.nim @@ -289,6 +289,15 @@ template main() = var foo = parseUri("http://example.com") / "foo" ? {"do": "do", "bar": ""} var foo1 = parseUri("http://example.com/foo?do=do&bar") doAssert foo == foo1 + block: # issue #19782: appends to existing query string + var foo = parseUri("http://example.com/foo?existing=1") ? {"bar": "qux"} + doAssert $foo == "http://example.com/foo?existing=1&bar=qux" + block: # issue #19782: empty params list preserves existing query + var foo = parseUri("http://example.com/foo?existing=1") ? {:} + doAssert $foo == "http://example.com/foo?existing=1" + block: # issue #19782: empty params on uri without query is a no-op + var foo = parseUri("http://example.com/foo") ? {:} + doAssert $foo == "http://example.com/foo" block: # getDataUri, dataUriBase64 doAssert getDataUri("", "text/plain") == "data:text/plain;charset=utf-8;base64," From e942da94b5b52fa7e09b133d52125037ba69c7b7 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Tue, 9 Jun 2026 04:59:16 +0800 Subject: [PATCH 33/33] fixes #22122; raise effects for complex expressions (#25845) fixes #22122 The root cause is in the effect tracker: raise was recording the whole conditional expression as one exception source, so semantic checking only saw the widened common base type instead of the concrete exception classes from each branch. --- compiler/sempass2.nim | 22 +++++++++- tests/effects/tcase_raises.nim | 74 ++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 tests/effects/tcase_raises.nim diff --git a/compiler/sempass2.nim b/compiler/sempass2.nim index 7b2be510f9..75ad510b1a 100644 --- a/compiler/sempass2.nim +++ b/compiler/sempass2.nim @@ -497,6 +497,26 @@ proc addRaiseEffect(a: PEffects, e, comesFrom: PNode) = if not isDefectException(e.typ): throws(a.exc, e, comesFrom) +proc addRaiseEffectsFromExpr(a: PEffects, e, comesFrom: PNode) = + if e.isNil: + return + let x = skipConvCastAndClosure(e) + case x.kind + of nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr: + if x.len > 0: + addRaiseEffectsFromExpr(a, x.lastSon, comesFrom) + of nkIfExpr, nkIfStmt: + for branch in items(x): + if branch.len > 0: + addRaiseEffectsFromExpr(a, branch.lastSon, comesFrom) + of nkCaseStmt: + for i in 1.. 0: + addRaiseEffectsFromExpr(a, branch.lastSon, comesFrom) + else: + addRaiseEffect(a, x, x) + proc addTag(a: PEffects, e, comesFrom: PNode) = var aa = a.tags for i in 0..