Compare commits

..

8 Commits

Author SHA1 Message Date
ringabout
2c9be48edf fix(types): refine alias skipping logic in type comparison functions 2026-05-23 20:28:25 +08:00
ringabout
c31121ec3b refactor: simplify type comparison logic by removing unnecessary type skips 2026-05-23 18:43:26 +08:00
ringabout
49d35670b1 Merge branch 'devel' into pr_orc 2026-05-21 20:07:26 +08:00
ringabout
43ac102ca8 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`.
2026-05-21 13:42:38 +02:00
Rybnikov Alex
393d27b57d fix(stdlib): use first-element flag in $ for collections (#18583) (#25832)
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 <noreply@anthropic.com>
2026-05-21 13:40:35 +02:00
Andreas Rumpf
9f5c193c1d fixes #25814 (#25816) 2026-05-19 23:28:13 +02:00
Pedro Batista
4f6b727d9e pegs: accept UTF-8 bytes in bare identifier terminals (#25829)
## 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.
2026-05-19 23:27:48 +02:00
ringabout
065f46afa8 fixes #23765; Method calling proc with return value overlapping declared generic type generates invalid C 2026-05-06 21:33:46 +08:00
24 changed files with 213 additions and 109 deletions

View File

@@ -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

View File

@@ -974,9 +974,7 @@ proc cow(p: BProc; n: PNode) {.inline.} =
if n.kind == nkHiddenAddr: cowBracket(p, n[0])
template ignoreConv(e: PNode): bool =
let destType = e.typ.skipTypes({tyVar, tyLent, tyGenericInst, tyAlias, tySink})
let srcType = e[1].typ.skipTypes({tyVar, tyLent, tyGenericInst, tyAlias, tySink})
sameBackendTypePickyAliases(destType, srcType)
sameBackendTypePickyAliases(e.typ, e[1].typ)
proc genAddr(p: BProc, e: PNode, d: var TLoc) =
# careful 'addr(myptrToArray)' needs to get the ampersand:
@@ -2816,9 +2814,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 +2836,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)

View File

@@ -578,7 +578,14 @@ proc isOpImpl(c: PContext, n: PNode, flags: TExprFlags): PNode =
if efExplain in flags:
m.diagnostics = @[]
m.diagnosticsEnabled = true
res = typeRel(m, t2, t1) >= isSubtype # isNone
let rel = typeRel(m, t2, t1)
res = rel >= isSubtype # isNone
if res and rel == isEqual and
not compareTypes(t1, t2,
flags = {ExactTypeDescValues,
PickyCAliases,
PickyBackendAliases}):
res = false
# `res = sameType(t1, t2)` would be wrong, e.g. for `int is (int|float)`
result = newIntNode(nkIntLit, ord(res))

View File

@@ -81,7 +81,8 @@ proc sameInstantiation(a, b: TInstantiation): bool =
if not compareTypes(a.concreteTypes[i], b.concreteTypes[i],
flags = {ExactTypeDescValues,
ExactGcSafety,
PickyCAliases}): return
PickyCAliases,
PickyBackendAliases}): return
result = true
else:
result = false

View File

@@ -52,7 +52,8 @@ proc searchInstTypes*(g: ModuleGraph; key: PType): PType =
for j in FirstGenericParamAt..<key.kidsLen:
# XXX sameType is not really correct for nested generics?
if not compareTypes(inst[j], key[j],
flags = {ExactGenericParams, PickyCAliases}):
flags = {ExactGenericParams, PickyCAliases,
PickyBackendAliases}):
break matchType
return inst

View File

@@ -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))

View File

@@ -90,20 +90,11 @@ 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)
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
@@ -194,39 +185,11 @@ proc transformSym(c: PTransf, n: PNode): PNode =
result = transformSymAux(c, n)
proc freshVar(c: PTransf; v: PSym): PNode =
result = freshOwnedSym(c, v, getCurrOwner(c))
proc introduceNewRoutineHeaderSyms(c: PTransf; n: PNode; oldOwner, newOwner: PSym) =
# We need to introduce new symbols for the parameters and result of a routine when
# we copy it for inlining or closure generation.
# Otherwise, we would have multiple nodes referring to the same parameter symbols which
# can lead to problems when we need to change the owner of these symbols.
case n.kind
of nkSym:
if n.sym.owner == oldOwner:
c.transCon.mapping[n.sym.itemId] = freshOwnedSym(c, n.sym, newOwner)
of nkEmpty..pred(nkSym), succ(nkSym)..nkNilLit:
discard
else:
for i in 0..<n.len:
introduceNewRoutineHeaderSyms(c, n[i], oldOwner, newOwner)
proc copyRoutineTypeHeader(c: PTransf; oldProc, newProc: PSym) =
# We need to copy the routine type header to ensure that
# modifications to the newProc do not affect the oldProc.
if oldProc.typ != nil and oldProc.typ.kind == tyProc and oldProc.typ.n != nil:
newProc.typ = copyType(oldProc.typ, c.idgen, newProc)
newProc.typ.n = newNodeI(oldProc.typ.n.kind, oldProc.typ.n.info)
if oldProc.typ.n.len > 0:
newProc.typ.n.add copyNode(oldProc.typ.n[0])
for i in 1..<oldProc.typ.n.len:
let oldParam = oldProc.typ.n[i].sym
var newParam = getOrDefault(c.transCon.mapping, oldParam.itemId)
if newParam == nil:
newParam = freshOwnedSym(c, oldParam, newProc)
c.transCon.mapping[oldParam.itemId] = newParam
doAssert newParam.kind == nkSym
newProc.typ.addParam newParam.sym
let owner = getCurrOwner(c)
var newVar = copySym(v, c.idgen)
incl(newVar.flagsImpl, sfFromGeneric)
setOwner(newVar, owner)
result = newSymNode(newVar)
proc transformVarSection(c: PTransf, v: PNode): PNode =
result = newTransNode(v)
@@ -375,13 +338,8 @@ proc introduceNewLocalVars(c: PTransf, n: PNode): PNode =
return n
of nkLambdaKinds, nkProcDef, nkFuncDef, nkMethodDef, nkConverterDef: # todo optimize nosideeffects?
result = newTransNode(n)
let oldProc = n[namePos].sym
let x = freshOwnedSym(c, oldProc, oldProc.owner)
c.transCon.mapping[oldProc.itemId] = x
introduceNewRoutineHeaderSyms(c, n[paramsPos], oldProc, x.sym)
if resultPos < n.len and n[resultPos] != nil:
introduceNewRoutineHeaderSyms(c, n[resultPos], oldProc, x.sym)
copyRoutineTypeHeader(c, oldProc, x.sym)
let x = newSymNode(copySym(n[namePos].sym, c.idgen))
c.transCon.mapping[n[namePos].sym.itemId] = x
result[namePos] = x # we have to copy proc definitions for iters
for i in 1..<n.len:
result[i] = introduceNewLocalVars(c, n[i])

View File

@@ -897,7 +897,11 @@ proc sameTypeAux(x, y: PType, c: var TSameTypeClosure): bool =
c.flags = oldFlags
if x == y: return true
let aliasSkipSet = maybeSkipRange({tyAlias, tyInferred})
let aliasSkipSet = maybeSkipRange(
if PickyBackendAliases in c.flags:
{tyInferred}
else:
{tyAlias, tyInferred})
var a = skipTypes(x, aliasSkipSet)
while a.kind == tyUserTypeClass and tfResolved in a.flags:
a = skipTypes(a.last, aliasSkipSet)
@@ -1070,6 +1074,8 @@ proc sameBackendTypeIgnoreRange*(x, y: PType): bool =
result = sameTypeAux(x, y, c)
proc sameBackendTypePickyAliases*(x, y: PType): bool =
let x = x.skipTypes({tyVar, tyLent, tySink, tyOwned})
let y = y.skipTypes({tyVar, tyLent, tySink, tyOwned})
var c = initSameTypeClosure()
c.flags.incl {IgnoreTupleFields, IgnoreRangeShallow, PickyCAliases, PickyBackendAliases}
c.cmp = dcEqIgnoreDistinct

View File

@@ -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)

View File

@@ -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("]")

View File

@@ -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("]")

View File

@@ -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("]")

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

7
tests/ccgbugs2/m25800.h Normal file
View File

@@ -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; }
};

23
tests/ccgbugs2/t25800.nim Normal file
View File

@@ -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()

View File

@@ -75,3 +75,19 @@ block: # importc type inheritance
doAssert(cast[cint](b) == 123)
var c = foo(b)
doAssert(cast[cint](c) == 123)
# bug #23765
type
X11[T, E] = object
m: T
B = X11[culonglong, cstring]
S = ref object of RootObj
proc j[T, E](m: X11[T, E]): T = discard
proc n(T: typedesc[SomeUnsignedInt]): X11[T, cstring] = discard
method call(client: S): uint64 {.base.} =
discard j(n(uint64))
var s = S()
discard s.call()

View File

@@ -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()

View File

@@ -465,24 +465,4 @@ block: # bug #25724
else: yield 1
for w in c():
let n = w
(proc() = discard n)()
block:
iterator c(): int =
yield 1
yield 1
for w in c():
proc p(s: int) =
let sap = s
p(0)
block: # bug #25725
iterator c(): int =
when nimvm: yield 0
else: yield 1
for w in c():
let n = w
proc p(s: int) =
let s = s; discard n
p(0)
(proc() = discard n)()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")