From 7295f578334beaf9d12de24b336041164582b420 Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Fri, 8 May 2026 06:48:48 +0200 Subject: [PATCH 01/12] Write all variables italic in section "About this document" (#25797) Makes more sense. One variable was already written italic. --- doc/manual.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/manual.md b/doc/manual.md index 40897ac108..4b474888d9 100644 --- a/doc/manual.md +++ b/doc/manual.md @@ -34,10 +34,10 @@ To learn how to compile Nim programs and generate documentation see the [Compiler User Guide](nimc.html) and the [DocGen Tools Guide](docgen.html). The language constructs are explained using an extended BNF, in which `(a)*` -means 0 or more `a`'s, `a+` means 1 or more `a`'s, and `(a)?` means an +means 0 or more *a*'s, `a+` means 1 or more *a*'s, and `(a)?` means an optional *a*. Parentheses may be used to group elements. -`&` is the lookahead operator; `&a` means that an `a` is expected but +`&` is the lookahead operator; `&a` means that an *a* is expected but not consumed. It will be consumed in the following rule. The `|`, `/` symbols are used to mark alternatives and have the lowest From 4c8052a45bc12b0dc1114ca606d142d33495d8f2 Mon Sep 17 00:00:00 2001 From: Ryan McConnell Date: Fri, 8 May 2026 00:50:13 -0400 Subject: [PATCH 02/12] fix: implicit imports drop `std/` prefix (#25780) Preserves implicit imports instead of always storing the resolved absolute filename. That lets the later StdPrefix warning check see the original std/objectdollar spelling. This is for situations where in cfg or cli warnings are enabled for the prefix. Essentially a niche combination of compiler switches don't get along e.g. `-d:nimPreviewSlimSystem --warning:StdPrefix:on --warningAsError:StdPrefix:on --import:std/objectdollar` will cause: `Error: objectdollar needs the 'std' prefix [StdPrefix]` --- compiler/commands.nim | 2 +- compiler/importer.nim | 8 ++++---- tests/compiler/tcmdline_import_std_prefix.nim | 10 ++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 tests/compiler/tcmdline_import_std_prefix.nim diff --git a/compiler/commands.nim b/compiler/commands.nim index f7de0978ed..a37cb348ae 100644 --- a/compiler/commands.nim +++ b/compiler/commands.nim @@ -930,7 +930,7 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo; if m.len == 0: localError(conf, info, "Cannot resolve filename: " & arg) else: - conf.implicitImports.add m + conf.implicitImports.add(if arg.startsWith(stdPrefix): arg else: m) of "include": expectArg(conf, switch, arg, pass, info) if pass in {passCmd2, passPP}: diff --git a/compiler/importer.nim b/compiler/importer.nim index a02a5e96a1..ac10720f90 100644 --- a/compiler/importer.nim +++ b/compiler/importer.nim @@ -13,7 +13,7 @@ import ast, msgs, options, idents, lookups, semdata, modulepaths, sigmatch, lineinfos, modulegraphs, wordrecg -from std/strutils import `%`, startsWith +from std/strutils import `%`, startsWith, replace from std/sequtils import addUnique import std/[sets, tables, intsets] @@ -304,9 +304,9 @@ proc myImportModule(c: PContext, n: var PNode, importStmtResult: PNode): PSym = var prefix = "" if realModule.constraint != nil: prefix = realModule.constraint.strVal & "; " message(c.config, n.info, warnDeprecated, prefix & realModule.name.s & " is deprecated") - let moduleName = getModuleName(c.config, n) - if belongsToStdlib(c.graph, result) and not startsWith(moduleName, stdPrefix) and - not startsWith(moduleName, "system/") and not startsWith(moduleName, "packages/"): + let moduleNameNorm = getModuleName(c.config, n).replace("\\", "/") + if belongsToStdlib(c.graph, result) and not startsWith(moduleNameNorm, stdPrefix) and + not startsWith(moduleNameNorm, "system/") and not startsWith(moduleNameNorm, "packages/"): message(c.config, n.info, warnStdPrefix, realModule.name.s) proc suggestMod(n: PNode; s: PSym) = diff --git a/tests/compiler/tcmdline_import_std_prefix.nim b/tests/compiler/tcmdline_import_std_prefix.nim new file mode 100644 index 0000000000..9b9eca776f --- /dev/null +++ b/tests/compiler/tcmdline_import_std_prefix.nim @@ -0,0 +1,10 @@ +discard """ + matrix: "-d:nimPreviewSlimSystem --warning:StdPrefix:on --warningAsError:StdPrefix:on --import:std/objectdollar" + output: "(a: 23, b: 45)" +""" + +type Foo = object + a, b: int + +let x = Foo(a: 23, b: 45) +echo x From f0c60b06e5cff8064bf0a9a32ec2d2bc14a694d9 Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Sat, 9 May 2026 08:55:39 +0200 Subject: [PATCH 03/12] Update outdated string representation in example (#25802) See [here](https://nim-lang.github.io/Nim/tut1.html#internal-type-representation). --- doc/tut1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tut1.md b/doc/tut1.md index 072d8f3ba6..be13f8fbb5 100644 --- a/doc/tut1.md +++ b/doc/tut1.md @@ -1144,7 +1144,7 @@ there is a difference between the `$` and `repr` outputs: echo myCharacter, ":", repr(myCharacter) # --> n:'n' echo myString, ":", repr(myString) - # --> nim:0x10fa8c050"nim" + # --> nim:"nim" echo myInteger, ":", repr(myInteger) # --> 42:42 echo myFloat, ":", repr(myFloat) From 6204e48ba597972569eab9148b630bbf0ed66fce Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 12 May 2026 23:20:10 +0200 Subject: [PATCH 04/12] SSO strings: bugfix (#25810) --- compiler/semdata.nim | 5 +++++ compiler/sempass2.nim | 4 ++++ lib/system.nim | 4 +++- lib/system/indices.nim | 29 ++++++++++++++++++++----- lib/system/strs_v3.nim | 9 ++++---- tests/errmsgs/tsso_string_index_var.nim | 13 +++++++++++ 6 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 tests/errmsgs/tsso_string_index_var.nim diff --git a/compiler/semdata.nim b/compiler/semdata.nim index 15d8b14fe7..32c98cdb31 100644 --- a/compiler/semdata.nim +++ b/compiler/semdata.nim @@ -637,6 +637,11 @@ proc renderNotLValue*(n: PNode): string = elif n.kind in {nkHiddenStdConv, nkHiddenSubConv} and n.len == 2: result = typeToString(n.typ.skipTypes(abstractVar)) & "(" & result & ")" +proc isSsoStringIndex*(conf: ConfigRef; n: PNode): bool = + result = conf.usesSso() and n.kind == nkBracketExpr and n.len >= 1 and + n[0].typ != nil and + n[0].typ.skipTypes(abstractVar + abstractInst - {tyTypeDesc}).kind == tyString + proc isAssignable(c: PContext, n: PNode): TAssignableResult = result = parampatterns.isAssignable(c.p.owner, n) diff --git a/compiler/sempass2.nim b/compiler/sempass2.nim index 35901ed960..9c84b721ad 100644 --- a/compiler/sempass2.nim +++ b/compiler/sempass2.nim @@ -809,6 +809,10 @@ proc trackOperandForIndirectCall(tracked: PEffects, n: PNode, formals: PType; ar markSideEffect(tracked, a, n.info) let paramType = if formals != nil and argIndex < formals.signatureLen: formals[argIndex] else: nil if paramType != nil and paramType.kind in {tyVar}: + let arg = n.skipAddr() + if isSsoStringIndex(tracked.config, arg): + localError(tracked.config, arg.info, + "expression '$1' is immutable, not 'var'" % renderNotLValue(arg)) invalidateFacts(tracked.guards, n) if n.kind == nkSym and isLocalSym(tracked, n.sym): makeVolatile(tracked, n.sym) diff --git a/lib/system.nim b/lib/system.nim index c76d096426..63989b1502 100644 --- a/lib/system.nim +++ b/lib/system.nim @@ -2694,7 +2694,9 @@ when hasAlloc or defined(nimscript): setLen(x, xl+item.len) var j = xl-1 while j >= i: - when defined(gcArc) or defined(gcOrc) or defined(gcYrc) or defined(gcAtomicArc): + when defined(nimsso): + x[j+item.len] = x[j] + elif defined(gcArc) or defined(gcOrc) or defined(gcYrc) or defined(gcAtomicArc): x[j+item.len] = move x[j] else: shallowCopy(x[j+item.len], x[j]) diff --git a/lib/system/indices.nim b/lib/system/indices.nim index 6230b36788..8f20af5ec5 100644 --- a/lib/system/indices.nim +++ b/lib/system/indices.nim @@ -59,16 +59,35 @@ template `[]=`*(s: string; i: int; val: char) = arrPut(s, i, val) template `^^`(s, i: untyped): untyped = (when i is BackwardsIndex: s.len - int(i) else: int(i)) -template spliceImpl(s, a, L, b: typed): untyped = +template spliceStringImpl(s, a, L, b: typed): untyped = # make room for additional elements or cut: var shift = b.len - max(0,L) # ignore negative slice size var newLen = s.len + shift if shift > 0: # enlarge: setLen(s, newLen) - for i in countdown(newLen-1, a+b.len): movingCopy(s[i], s[i-shift]) + for i in countdown(newLen-1, a+b.len): + s[i] = s[i-shift] else: - for i in countup(a+b.len, newLen-1): movingCopy(s[i], s[i-shift]) + for i in countup(a+b.len, newLen-1): + s[i] = s[i-shift] + # cut down: + setLen(s, newLen) + # fill the hole: + for i in 0 ..< b.len: s[a+i] = b[i] + +template spliceSeqImpl(s, a, L, b: typed): untyped = + # make room for additional elements or cut: + var shift = b.len - max(0,L) # ignore negative slice size + var newLen = s.len + shift + if shift > 0: + # enlarge: + setLen(s, newLen) + for i in countdown(newLen-1, a+b.len): + movingCopy(s[i], s[i-shift]) + else: + for i in countup(a+b.len, newLen-1): + movingCopy(s[i], s[i-shift]) # cut down: setLen(s, newLen) # fill the hole: @@ -102,7 +121,7 @@ proc `[]=`*[T, U: Ordinal](s: var string, x: HSlice[T, U], b: string) {.systemRa if L == b.len: for i in 0.. PayloadSize: a.more.fullLen else: aslen let lb = if bslen > PayloadSize: b.more.fullLen else: bslen let minLen = min(la, lb) + let pfxLen = min(minLen, AlwaysAvail) + result = cmpInlineBytes(inlinePtrOf(a), inlinePtrOf(b), pfxLen) + if result != 0: return if minLen <= AlwaysAvail: result = la - lb return diff --git a/tests/errmsgs/tsso_string_index_var.nim b/tests/errmsgs/tsso_string_index_var.nim new file mode 100644 index 0000000000..401491e0a5 --- /dev/null +++ b/tests/errmsgs/tsso_string_index_var.nim @@ -0,0 +1,13 @@ +discard """ + cmd: "nim check --strings:sso --mm:orc --hints:off $file" + action: "reject" + nimout: ''' +tsso_string_index_var.nim(13, 12) Error: expression 's[0]' is immutable, not 'var' +''' +""" + +proc passByVar(c: var char) = + c = 'x' + +var s = "abc" +passByVar(s[0]) From bbc5bbdcc72c5398c9b495826c3c64e7e491ba6a Mon Sep 17 00:00:00 2001 From: oab24413gmai Date: Thu, 14 May 2026 01:02:11 -0500 Subject: [PATCH 05/12] 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 06/12] 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 07/12] 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 08/12] 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 09/12] 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 10/12] 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 11/12] 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 12/12] 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