diff --git a/changelog.md b/changelog.md index 304aeb1f7b..fa8f275f5d 100644 --- a/changelog.md +++ b/changelog.md @@ -85,6 +85,11 @@ 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. +- `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/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/compiler/ccgexprs.nim b/compiler/ccgexprs.nim index 2cf187e687..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) @@ -2816,9 +2818,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 +2840,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/ccgstmts.nim b/compiler/ccgstmts.nim index 5ea23d1f80..bc9c06fa1d 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) @@ -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/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/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/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/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/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/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/options.nim b/compiler/options.nim index adc63e5687..b2144c516f 100644 --- a/compiler/options.nim +++ b/compiler/options.nim @@ -265,6 +265,11 @@ type typedescFieldAccess ## Allow typedesc field access on object/tuple types outside of ## typeof context. + 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/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/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/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/compiler/sempass2.nim b/compiler/sempass2.nim index 9c84b721ad..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.." 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/compiler/sigmatch.nim b/compiler/sigmatch.nim index bf9c2d2050..cb79823af5 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): @@ -1757,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: @@ -2471,6 +2488,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 @@ -2849,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 @@ -2905,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 @@ -3004,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 @@ -3056,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/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/compiler/transf.nim b/compiler/transf.nim index 3059d8fe6b..f131c862f0 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 @@ -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.. arg" in an # inline context. if formal.kind == tyTypeDesc: return paDirectMapping @@ -726,6 +771,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 +876,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/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/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/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/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..} 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? 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/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" 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/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 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.. 1: result.add(", ") + if first: first = false + else: result.add(", ") result.add(key) result.add(": ") result.add(val) 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/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/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)) 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) 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/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() 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) 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" 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/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/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 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() + diff --git a/tests/effects/tcase_raises.nim b/tests/effects/tcase_raises.nim new file mode 100644 index 0000000000..166a79d5c2 --- /dev/null +++ b/tests/effects/tcase_raises.nim @@ -0,0 +1,74 @@ +from std/os import osLastError, osErrorMsg, OSErrorCode, raiseOSError, + newOSError, `==` + +{.push raises: [].} + +const + EPERM* = OSErrorCode(1) + ECONNABORTED* = OSErrorCode(53) + ETIMEDOUT* = OSErrorCode(60) + ENOTCONN* = OSErrorCode(107) + EMFILE* = OSErrorCode(24) + ENFILE* = OSErrorCode(23) + ENOBUFS* = OSErrorCode(55) + ENOMEM* = OSErrorCode(12) + +type + AsyncError* = object of CatchableError + TransportErrorBase* = object of AsyncError + TransportOsError* = object of TransportErrorBase + code*: OSErrorCode + TransportTooManyError* = object of TransportErrorBase + TransportAbortedError* = object of TransportErrorBase + +template getConnectionAbortedError*( + code: OSErrorCode + ): ref TransportAbortedError = + let msg = + case code + of OSErrorCode(0), ECONNABORTED: + "[ECONNABORTED] Connection has been aborted before being accepted" + of EPERM: + "[EPERM] Firewall rules forbid connection" + of ETIMEDOUT: + "[ETIMEDOUT] Operation has been timed out" + of ENOTCONN: + "[ENOTCONN] Transport endpoint is not connected" + else: + "[" & $int(code) & "] Connection has been aborted" + newException(TransportAbortedError, msg) + +template getTransportTooManyError*( + code = OSErrorCode(0) + ): ref TransportTooManyError = + let msg = + case code + of OSErrorCode(0): + "Too many open transports" + of EMFILE: + "[EMFILE] Too many open files in the process" + of ENFILE: + "[ENFILE] Too many open files in system" + of ENOBUFS: + "[ENOBUFS] No buffer space available" + of ENOMEM: + "[ENOMEM] Not enough memory availble" + else: + "[" & $int(code) & "] Too many open transports" + newException(TransportTooManyError, msg) + +template getTransportError*(ecode: OSErrorCode): untyped = + case ecode + of ECONNABORTED, EPERM, ETIMEDOUT, ENOTCONN: + getConnectionAbortedError(ecode) + of EMFILE, ENFILE, ENOBUFS, ENOMEM: + getTransportTooManyError(ecode) + else: + (ref TransportOsError)(code: ecode, + msg: "(" & $int(ecode) & ") " & osErrorMsg(ecode)) + +proc raiseTransportError*(err: OSErrorCode) {. + raises: [TransportAbortedError, TransportTooManyError, TransportOsError], + noreturn.} = + ## Raises transport specific OS error. + raise getTransportError(err) \ No newline at end of file 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 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" 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/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 diff --git a/tests/iter/titer_issues.nim b/tests/iter/titer_issues.nim index 468c230997..fdc7ab0599 100644 --- a/tests/iter/titer_issues.nim +++ b/tests/iter/titer_issues.nim @@ -465,4 +465,24 @@ block: # bug #25724 else: yield 1 for w in c(): let n = w - (proc() = discard n)() \ No newline at end of file + (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) 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 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 + diff --git a/tests/method/mvtables_reentry_a.nim b/tests/method/mvtables_reentry_a.nim new file mode 100644 index 0000000000..b9d297c781 --- /dev/null +++ b/tests/method/mvtables_reentry_a.nim @@ -0,0 +1,5 @@ +type + VtableBaseA* = ref object of RootObj + +method say*(a: VtableBaseA): string {.base.} = + "base" diff --git a/tests/method/mvtables_reentry_b.nim b/tests/method/mvtables_reentry_b.nim new file mode 100644 index 0000000000..2a716b2d9c --- /dev/null +++ b/tests/method/mvtables_reentry_b.nim @@ -0,0 +1,7 @@ +import mvtables_reentry_a + +type + VtableDerivedB* = ref object of VtableBaseA + +method say*(d: VtableDerivedB): string = + "derived" diff --git a/tests/method/tvtable_reentry.nim b/tests/method/tvtable_reentry.nim new file mode 100644 index 0000000000..41109ba879 --- /dev/null +++ b/tests/method/tvtable_reentry.nim @@ -0,0 +1,19 @@ +discard """ + targets: "c cpp" +""" + +import mvtables_reentry_a + +type + MainType = ref object of VtableBaseA + +method say*(m: MainType): string = + "main" + +when isMainModule: + import mvtables_reentry_b + + let a: VtableBaseA = VtableDerivedB() + doAssert a.say() == "derived" + let m: VtableBaseA = MainType() + doAssert m.say() == "main" 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 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) 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 + 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 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") 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 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," 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 diff --git a/tests/template/toverload_over_untyped.nim b/tests/template/toverload_over_untyped.nim new file mode 100644 index 0000000000..ff76663915 --- /dev/null +++ b/tests/template/toverload_over_untyped.nim @@ -0,0 +1,77 @@ +discard """ + output: '''ok +ok +ok''' +""" + +# bug #25693 + +proc d() = discard @[0] + +proc f(a: var seq[int], _: string) = + let p = @[0] + d() + a = p + +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" 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) 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())