diff --git a/.github/workflows/ci_publish.yml b/.github/workflows/ci_publish.yml index 44cfaf8213..67b78092f8 100644 --- a/.github/workflows/ci_publish.yml +++ b/.github/workflows/ci_publish.yml @@ -60,7 +60,7 @@ jobs: run: nim c -r -d:release ci/action.nim - name: 'Comment' - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); diff --git a/changelog.md b/changelog.md index 53e0c0d476..f87dadae63 100644 --- a/changelog.md +++ b/changelog.md @@ -35,6 +35,10 @@ errors. - Adds a new warning `--warning:ImplicitRangeConversion` that detects downsizing implicit conversions to range types (e.g., `int -> range[0..255]` or `range[1..256] -> range[0..255]`) that could cause runtime panics. Safe conversions like `range[0..255] -> range[0..65535]` and explicit casts do not trigger warnings. `int` to `Natural` and `Positive` conversions do not trigger warnings, which can be enabled with `--warning:systemRangeConversion`. +- Procedure compatibility also checks the backend representation of the +parameter and result types, not just their source-level shape. Use +`--legacy:procParamTypeBackendAliases` to restore the older behavior. + ## Standard library additions and changes [//]: # "Additions:" @@ -66,12 +70,17 @@ errors. Modes include `Nim` (default, fully compatible) and two new experimental modes: `Lax` and `Gnu` for different option parsing behaviors. +- `std/nre2` is added to replace deprecated NRE. + [//]: # "Changes:" - `std/math` The `^` symbol now supports floating-point as exponent in addition to the Natural type. - `min`, `max`, and `sequtils`' `minIndex`, `maxIndex` and `minmax` for `openArray`s now accept a comparison function. - `system.substr` implementation now uses `copymem` (wrapped C `memcpy`) for copying data, if available at compilation. - `system.newStringUninit` is now considered free of side-effects allowing it to be used with `--experimental:strictFuncs`. +- `std/re` and `std/nre` are deprecated as PCRE library is obsolete. + Use https://github.com/nitely/nim-regex or `std/nre2`. + See: https://github.com/nim-lang/Nim/issues/23668. ## Language changes diff --git a/compiler/ccgcalls.nim b/compiler/ccgcalls.nim index b0964f97be..b2521069d4 100644 --- a/compiler/ccgcalls.nim +++ b/compiler/ccgcalls.nim @@ -230,11 +230,11 @@ proc genOpenArraySlice(p: BProc; q: PNode; formalType, destType: PType; prepareF of tyString, tySequence: let atyp = skipTypes(a.t, abstractInst) if formalType.skipTypes(abstractInst).kind in {tyVar} and atyp.kind == tyString and - optSeqDestructors in p.config.globalOptions and not p.config.isDefined("nimsso"): + optSeqDestructors in p.config.globalOptions and not p.config.usesSso(): let bra = byRefLoc(p, a) p.s(cpsStmts).addCallStmt(cgsymValue(p.module, "nimPrepareStrMutationV2"), bra) - if p.config.isDefined("nimsso") and + if p.config.usesSso() and skipTypes(a.t, abstractVar + abstractInst).kind == tyString: let strPtr = if atyp.kind in {tyVar} and not compileToCpp(p.module): ra else: addrLoc(p.config, a) @@ -296,11 +296,11 @@ proc openArrayLoc(p: BProc, formalType: PType, n: PNode; result: var Builder) = of tyString, tySequence: let ntyp = skipTypes(n.typ, abstractInst) if formalType.skipTypes(abstractInst).kind in {tyVar} and ntyp.kind == tyString and - optSeqDestructors in p.config.globalOptions and not p.config.isDefined("nimsso"): + optSeqDestructors in p.config.globalOptions and not p.config.usesSso(): let bra = byRefLoc(p, a) p.s(cpsStmts).addCallStmt(cgsymValue(p.module, "nimPrepareStrMutationV2"), bra) - if p.config.isDefined("nimsso") and + if p.config.usesSso() and skipTypes(n.typ, abstractVar + abstractInst).kind == tyString: if ntyp.kind in {tyVar} and not compileToCpp(p.module): let ra = a.rdLoc @@ -335,7 +335,7 @@ proc openArrayLoc(p: BProc, formalType: PType, n: PNode; result: var Builder) = let ra = a.rdLoc var t = TLoc(snippet: cDeref(ra)) let lt = lenExpr(p, t) - if p.config.isDefined("nimsso"): + if p.config.usesSso(): result.add(cCall(cgsymValue(p.module, "nimStrData"), ra)) result.addArgumentSeparator() result.add(cCall(cgsymValue(p.module, "nimStrLen"), t.snippet)) @@ -370,7 +370,7 @@ proc expressionsNeedsTmp(p: BProc, a: TLoc): TLoc = proc genArgStringToCString(p: BProc, n: PNode; result: var Builder; needsTmp: bool) {.inline.} = var a = initLocExpr(p, n[0]) let tmp = withTmpIfNeeded(p, a, needsTmp) - let ra = if p.config.isDefined("nimsso"): byRefLoc(p, tmp) else: tmp.rdLoc + let ra = if p.config.usesSso(): byRefLoc(p, tmp) else: tmp.rdLoc result.addCall(cgsymValue(p.module, "nimToCStringConv"), ra) proc genArg(p: BProc, n: PNode, param: PSym; call: PNode; result: var Builder; needsTmp = false) = diff --git a/compiler/ccgexprs.nim b/compiler/ccgexprs.nim index 941327341e..2cf187e687 100644 --- a/compiler/ccgexprs.nim +++ b/compiler/ccgexprs.nim @@ -322,7 +322,7 @@ proc genOpenArrayConv(p: BProc; d: TLoc; a: TLoc; flags: TAssignmentFlags) = bra) let rd = d.rdLoc let la = lenExpr(p, a) - if p.config.isDefined("nimsso"): + if p.config.usesSso(): let bra = byRefLoc(p, a) p.s(cpsStmts).addFieldAssignment(rd, "Field0", cCall(cgsymValue(p.module, "nimStrData"), bra)) @@ -963,7 +963,7 @@ proc genDeref(p: BProc, e: PNode, d: var TLoc) = proc cowBracket(p: BProc; n: PNode) = if n.kind == nkBracketExpr and optSeqDestructors in p.config.globalOptions and - not p.config.isDefined("nimsso"): + not p.config.usesSso(): let strCandidate = n[0] if strCandidate.typ.skipTypes(abstractInst).kind == tyString: var a: TLoc = initLocExpr(p, strCandidate) @@ -989,7 +989,7 @@ proc genAddr(p: BProc, e: PNode, d: var TLoc) = # bug #19497 d.lode = e else: - let ssoStrSub = p.config.isDefined("nimsso") and e[0].kind == nkBracketExpr and + let ssoStrSub = p.config.usesSso() and e[0].kind == nkBracketExpr and e[0][0].typ.skipTypes(abstractVar).kind == tyString var a: TLoc = initLocExpr(p, e[0], if ssoStrSub: {lfEnforceDeref, lfPrepareForMutation} else: {}) if e[0].kind in {nkHiddenStdConv, nkHiddenSubConv, nkConv} and not ignoreConv(e[0]): @@ -1318,7 +1318,7 @@ proc genSeqElem(p: BProc, n, x, y: PNode, d: var TLoc) = if skipTypes(a.t, abstractVar).kind in {tyRef, tyPtr}: a.snippet = cDeref(a.snippet) - if p.config.isDefined("nimsso") and ty.kind == tyString: + if p.config.usesSso() and ty.kind == tyString: let bra = byRefLoc(p, a) if lfPrepareForMutation in d.flags: # Use nimStrAtMutV3 to get a mutable reference (char*) to the element. @@ -1889,10 +1889,17 @@ proc genObjConstr(p: BProc, e: PNode, d: var TLoc) = var t = e.typ.skipTypes(abstractInstOwned) let isRef = t.kind == tyRef - # check if we need to construct the object in a temporary + # check if we need to construct the object in a temporary. + # A temp is needed when: + # - the constructor produces a ref (isRef) + # - the destination is not a writable location (d.k == locNone) + # - the constructed type differs from the destination type (subtype + # assignments need the genAssignment path for ObjectAssignmentDefect) + # - the constructor's field values may alias the destination (isPartOf) var useTemp = isRef or - (d.k notin {locTemp,locLocalVar,locGlobalVar,locParam,locField}) or + d.k == locNone or + (d.t != nil and not sameBackendType(t, d.t.skipTypes(abstractInstOwned))) or (isPartOf(d.lode, e) != arNo) var tmp: TLoc = default(TLoc) @@ -2143,7 +2150,7 @@ proc genRepr(p: BProc, e: PNode, d: var TLoc) = putIntoDest(p, b, e, ra & cArgumentSeparator & ra & "Len_0", a.storage) of tyString, tySequence: let la = lenExpr(p, a) - if p.config.isDefined("nimsso") and + if p.config.usesSso() and skipTypes(a.t, abstractVarRange).kind == tyString: let bra = byRefLoc(p, a) putIntoDest(p, b, e, @@ -2736,7 +2743,7 @@ proc genConv(p: BProc, e: PNode, d: var TLoc) = proc convStrToCStr(p: BProc, n: PNode, d: var TLoc) = var a: TLoc = initLocExpr(p, n[0]) - let arg = if p.config.isDefined("nimsso"): byRefLoc(p, a) else: rdLoc(a) + let arg = if p.config.usesSso(): byRefLoc(p, a) else: rdLoc(a) putIntoDest(p, d, n, cgCall(p, "nimToCStringConv", arg), a.storage) @@ -2815,7 +2822,7 @@ proc genMove(p: BProc; n: PNode; d: var TLoc) = var src: TLoc = initLocExpr(p, n[2]) let destVal = rdLoc(a) let srcVal = rdLoc(src) - if p.config.isDefined("nimsso") and + if p.config.usesSso() and n[1].typ.skipTypes(abstractVar).kind == tyString: # SmallString: destroy dst then struct-copy src; no .p field aliasing needed genStmts(p, n[3]) @@ -2864,7 +2871,7 @@ proc genDestroy(p: BProc; n: PNode) = case t.kind of tyString: var a: TLoc = initLocExpr(p, arg) - if p.config.isDefined("nimsso"): + if p.config.usesSso(): # SmallString: delegate to nimDestroyStrV1 (rc-based, handles static strings) p.s(cpsStmts).addCallStmt(cgsymValue(p.module, "nimDestroyStrV1"), rdLoc(a)) else: @@ -4236,7 +4243,7 @@ proc genBracedInit(p: BProc, n: PNode; isConst: bool; optionalType: PType; resul genConstObjConstr(p, n, isConst, result) of tyString, tyCstring: if optSeqDestructors in p.config.globalOptions and n.kind != nkNilLit and ty == tyString: - if p.config.isDefined("nimsso"): + if p.config.usesSso(): genStringLiteralV3Const(p.module, n, isConst, result) else: genStringLiteralV2Const(p.module, n, isConst, result) diff --git a/compiler/ccgliterals.nim b/compiler/ccgliterals.nim index 54823cc592..0a1586ae29 100644 --- a/compiler/ccgliterals.nim +++ b/compiler/ccgliterals.nim @@ -22,7 +22,7 @@ template detectVersion(field, corename) = result = 1 proc detectStrVersion(m: BModule): int = - if m.g.config.isDefined("nimsso") and + if m.g.config.usesSso() and m.g.config.selectedGC in {gcArc, gcOrc, gcYrc, gcAtomicArc, gcHooks}: result = 3 else: diff --git a/compiler/ccgstmts.nim b/compiler/ccgstmts.nim index 3a2042ae19..5ea23d1f80 100644 --- a/compiler/ccgstmts.nim +++ b/compiler/ccgstmts.nim @@ -230,7 +230,7 @@ proc blockLeaveActions(p: BProc, howManyTrys, howManyExcepts: int, isReturnStmt # Called by return and break stmts. # Deals with issues faced when jumping out of try/except/finally stmts. - var stack = newSeq[tuple[fin: PNode, inExcept: bool, label: Natural]](0) + var stack = newSeq[tuple[fin: PNode, inExcept: bool, isHidden: bool, label: Natural]](0) inc p.withinBlockLeaveActions for i in 1..howManyTrys: @@ -836,12 +836,26 @@ proc raiseExitCleanup(p: BProc, destroy: string) = p.s(cpsStmts).addGoto("LA" & $p.nestedTryStmts[^1].label & "_") proc finallyActions(p: BProc) = - if p.config.exc != excGoto and p.nestedTryStmts.len > 0 and p.nestedTryStmts[^1].inExcept: - # if the current try stmt have a finally block, - # we must execute it before reraising - let finallyBlock = p.nestedTryStmts[^1].fin - if finallyBlock != nil: - genSimpleBlock(p, finallyBlock[0]) + if p.config.exc != excGoto: + # Walk past compiler-injected `nkHiddenTryStmt` wrappers (e.g. ARC's + # destructor try/finally that wraps `except T as e:` bodies) to reach + # the user's actual try. We must NOT walk past a real user try whose + # body we are currently in, because a raise from there will be caught + # by that try's own except branches rather than escaping outward. + # + # If after skipping wrappers the next entry is a user try in its + # except branch (inExcept=true), inline its finally body before the + # raise propagates — without this, the C++ sibling-catch rule would + # cause the user's catch(...)/finally pair to be bypassed and the + # finally would be silently dropped. + for i in countdown(p.nestedTryStmts.high, 0): + if p.nestedTryStmts[i].isHidden: + continue + if p.nestedTryStmts[i].inExcept: + let finallyBlock = p.nestedTryStmts[i].fin + if finallyBlock != nil: + genSimpleBlock(p, finallyBlock[0]) + return proc raiseInstr(p: BProc; result: var Builder) = if p.config.exc == excGoto: @@ -1185,7 +1199,7 @@ proc genTryCpp(p: BProc, t: PNode, d: var TLoc) = lineCg(p, cpsLocals, "std::exception_ptr T$1_;$n", [etmp]) let fin = if t[^1].kind == nkFinally: t[^1] else: nil - p.nestedTryStmts.add((fin, false, 0.Natural)) + p.nestedTryStmts.add((fin, false, t.kind == nkHiddenTryStmt, 0.Natural)) if t.kind == nkHiddenTryStmt: lineCg(p, cpsStmts, "try {$n", []) @@ -1371,7 +1385,7 @@ proc genTryCppOld(p: BProc, t: PNode, d: var TLoc) = genLineDir(p, t) cgsym(p.module, "popCurrentExceptionEx") let fin = if t[^1].kind == nkFinally: t[^1] else: nil - p.nestedTryStmts.add((fin, false, 0.Natural)) + p.nestedTryStmts.add((fin, false, t.kind == nkHiddenTryStmt, 0.Natural)) startBlockWith(p): p.s(cpsStmts).add("try {\n") expr(p, t[0], d) @@ -1450,7 +1464,7 @@ proc genTryGoto(p: BProc; t: PNode; d: var TLoc) = let lab = p.labels let hasExcept = t[1].kind == nkExceptBranch if hasExcept: inc p.withinTryWithExcept - p.nestedTryStmts.add((fin, false, Natural lab)) + p.nestedTryStmts.add((fin, false, t.kind == nkHiddenTryStmt, Natural lab)) p.flags.incl nimErrorFlagAccessed @@ -1656,7 +1670,7 @@ proc genTrySetjmp(p: BProc, t: PNode, d: var TLoc) = initElifBranch(p.s(cpsStmts), nonQuirkyIf, removeSinglePar( cOp(Equal, dotField(safePoint, "status"), cIntValue(0)))) let fin = if t[^1].kind == nkFinally: t[^1] else: nil - p.nestedTryStmts.add((fin, quirkyExceptions, 0.Natural)) + p.nestedTryStmts.add((fin, quirkyExceptions, t.kind == nkHiddenTryStmt, 0.Natural)) expr(p, t[0], d) var quirkyIf = default(IfBuilder) var quirkyScope = default(ScopeBuilder) @@ -1940,7 +1954,7 @@ proc genAsgn(p: BProc, e: PNode, fastAsgn: bool) = elif optFieldCheck in p.options and isDiscriminantField(e[0]): genLineDir(p, e) asgnFieldDiscriminant(p, e) - elif p.config.isDefined("nimsso") and e[0].kind == nkBracketExpr and + elif p.config.usesSso() and e[0].kind == nkBracketExpr and e[0][0].typ.skipTypes(abstractVar).kind == tyString: # nimsso: s[i] = c → nimStrPutV3(&s, i, c) (handles COW internally) genLineDir(p, e) diff --git a/compiler/cgen.nim b/compiler/cgen.nim index 537d248103..8d3b486ca3 100644 --- a/compiler/cgen.nim +++ b/compiler/cgen.nim @@ -389,7 +389,7 @@ proc lenField(p: BProc, val: Rope): Rope {.inline.} = proc lenExpr(p: BProc; a: TLoc): Rope = if optSeqDestructors in p.config.globalOptions: - if p.config.isDefined("nimsso") and a.lode != nil and a.t != nil and + if p.config.usesSso() and a.lode != nil and a.t != nil and a.t.skipTypes(abstractInst).kind == tyString: result = cCall(cgsymValue(p.module, "nimStrLen"), rdLoc(a)) else: @@ -534,7 +534,7 @@ proc resetLoc(p: BProc, loc: var TLoc) = let atyp = skipTypes(loc.t, abstractInst) let rl = rdLoc(loc) - if typ.kind == tyString and p.config.isDefined("nimsso"): + if typ.kind == tyString and p.config.usesSso(): # SmallString zero state: bytes=0 (slen=0 in low byte, all inline chars zeroed) if atyp.kind in {tyVar, tyLent}: p.s(cpsStmts).addAssignment(derefField(rl, "bytes"), cIntValue(0)) @@ -592,7 +592,7 @@ proc constructLoc(p: BProc, loc: var TLoc, isTemp = false) = let typ = loc.t if optSeqDestructors in p.config.globalOptions and skipTypes(typ, abstractInst + {tyStatic}).kind in {tyString, tySequence}: let rl = rdLoc(loc) - if skipTypes(typ, abstractInst + {tyStatic}).kind == tyString and p.config.isDefined("nimsso"): + if skipTypes(typ, abstractInst + {tyStatic}).kind == tyString and p.config.usesSso(): # SmallString zero state: bytes=0 (slen=0 in low byte, all inline chars zeroed) p.s(cpsStmts).addFieldAssignment(rl, "bytes", cIntValue(0)) p.s(cpsStmts).addFieldAssignment(rl, "more", NimNil) diff --git a/compiler/cgendata.nim b/compiler/cgendata.nim index 5b5668024a..fb8f2086cb 100644 --- a/compiler/cgendata.nim +++ b/compiler/cgendata.nim @@ -75,10 +75,13 @@ type flags*: set[TCProcFlag] lastLineInfo*: TLineInfo # to avoid generating excessive 'nimln' statements currLineInfo*: TLineInfo # AST codegen will make this superfluous - nestedTryStmts*: seq[tuple[fin: PNode, inExcept: bool, label: Natural]] + nestedTryStmts*: seq[tuple[fin: PNode, inExcept: bool, isHidden: bool, label: Natural]] # in how many nested try statements we are # (the vars must be volatile then) - # bool is true when are in the except part of a try block + # `inExcept` is true when we are in the except part of a try block. + # `isHidden` is true for compiler-injected `nkHiddenTryStmt` wrappers + # (e.g. ARC's destructor try/finally around `except T as e:` bodies); + # finallyActions walks past such wrappers to reach the user's try. finallySafePoints*: seq[Rope] # For correctly cleaning up exceptions when # using return in finally statements labels*: Natural # for generating unique labels in the C proc diff --git a/compiler/commands.nim b/compiler/commands.nim index be5a8abd27..f7de0978ed 100644 --- a/compiler/commands.nim +++ b/compiler/commands.nim @@ -250,6 +250,7 @@ const errGuiConsoleOrLibExpectedButXFound = "'gui', 'console', 'lib' or 'staticlib' expected, but '$1' found" errInvalidExceptionSystem = "'goto', 'setjmp', 'cpp' or 'quirky' expected, but '$1' found" errInvalidFeatureButXFound = Feature.toSeq.map(proc(val:Feature): string = "'$1'" % $val).join(", ") & " expected, but '$1' found" + errDefaultOrSsoExpectedButXFound = "'default' or 'sso' expected, but '$1' found" template warningOptionNoop(switch: string) = warningDeprecated(conf, info, "'$#' is deprecated, now a noop" % switch) @@ -306,6 +307,13 @@ proc testCompileOptionArg*(conf: ConfigRef; switch, arg: string, info: TLineInfo else: result = false localError(conf, info, errInvalidExceptionSystem % arg) + of "strings": + case arg.normalize + of "default": result = conf.selectedStrings == stringDefault + of "sso": result = conf.selectedStrings == stringSso + else: + result = false + localError(conf, info, errDefaultOrSsoExpectedButXFound % arg) of "experimental": try: result = conf.features.contains parseEnum[Feature](arg) @@ -750,6 +758,17 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo; processMemoryManagementOption(switch, arg, pass, info, conf) of "mm": processMemoryManagementOption(switch, arg, pass, info, conf) + of "strings": + expectArg(conf, switch, arg, pass, info) + if pass in {passCmd2, passPP}: + case arg.normalize + of "default": + conf.selectedStrings = stringDefault + of "sso": + conf.selectedStrings = stringSso + defineSymbol(conf.symbols, "nimsso") + else: + localError(conf, info, errDefaultOrSsoExpectedButXFound % arg) of "warnings", "w": if processOnOffSwitchOrList(conf, {optWarns}, arg, pass, info): listWarnings(conf) of "warning": processSpecificNote(arg, wWarning, pass, info, switch, conf) diff --git a/compiler/deps.nim b/compiler/deps.nim index 6b891fac9f..cfe0f0975f 100644 --- a/compiler/deps.nim +++ b/compiler/deps.nim @@ -10,10 +10,10 @@ ## Generate a .build.nif file for nifmake from a Nim project. ## This enables incremental and parallel compilation using the `m` switch. -import std / [os, tables, sets, times, osproc, strutils] +import std / [os, tables, sets, times, osproc] import options, msgs, lineinfos, pathutils -import "../dist/nimony/src/lib" / [nifstreams, nifcursors, bitabs, nifreader, nifbuilder] +import "../dist/nimony/src/lib" / [nifstreams, bitabs, nifreader, nifbuilder] import "../dist/nimony/src/gear2" / modnames type @@ -79,22 +79,19 @@ proc runNifler(c: DepContext; nimFile: string): bool = let exitCode = execShellCmd(cmd) result = exitCode == 0 -proc resolveFile(c: DepContext; origin, toResolve: string): string = - ## Resolve an import path relative to origin file - # Handle std/ prefix - var path = toResolve - if path.startsWith("std/"): - path = path.substr(4) +proc resolveImport(c: DepContext; origin, toResolve: string): string = + ## Resolve an import path using the compiler's normal module lookup rules. + result = findModule(c.config, toResolve, origin).string - # Try relative to origin first +proc resolveInclude(c: DepContext; origin, toResolve: string): string = + ## Resolve an include path relative to the including file or the search paths. let originDir = parentDir(origin) - result = originDir / path.addFileExt("nim") + result = originDir / toResolve.addFileExt("nim") if fileExists(result): return result - # Try search paths for searchPath in c.config.searchPaths: - result = searchPath.string / path.addFileExt("nim") + result = searchPath.string / toResolve.addFileExt("nim") if fileExists(result): return result @@ -103,7 +100,7 @@ proc resolveFile(c: DepContext; origin, toResolve: string): string = proc traverseDeps(c: var DepContext; pair: FilePair; current: Node) proc processInclude(c: var DepContext; includePath: string; current: Node) = - let resolved = resolveFile(c, current.files[current.files.len - 1].nimFile, includePath) + let resolved = resolveInclude(c, current.files[current.files.len - 1].nimFile, includePath) if resolved.len == 0 or not fileExists(resolved): return @@ -118,7 +115,7 @@ proc processInclude(c: var DepContext; includePath: string; current: Node) = discard c.includeStack.pop() proc processImport(c: var DepContext; importPath: string; current: Node) = - let resolved = resolveFile(c, current.files[0].nimFile, importPath) + let resolved = resolveImport(c, current.files[0].nimFile, importPath) if resolved.len == 0 or not fileExists(resolved): return @@ -140,6 +137,171 @@ proc processImport(c: var DepContext; importPath: string; current: Node) = if existingIdx notin current.deps: current.deps.add existingIdx +proc skipSubtree(s: var Stream; first: PackedToken) = + ## Consume tokens until the ParLe at `first` is balanced. Caller has + ## already obtained `first`. + if first.kind != ParLe: return + var depth = 1 + while depth > 0: + let t = next(s) + if t.kind == ParLe: inc depth + elif t.kind == ParRi: dec depth + elif t.kind == EofToken: return + +proc evalCondExpr(c: DepContext; s: var Stream): bool = + ## Read exactly one condition expression from `s` and return its truth + ## value. Consumes tokens whether the expression is recognised or not so + ## the caller stays in sync. Recognises `defined(IDENT)`, the boolean + ## operators `not`/`and`/`or`, and the literals `true`/`false`. Anything + ## else (e.g. a call to an arbitrary proc) is treated as `true` — the + ## conservative direction, since a false negative here drops a real + ## dependency from the build graph. + let t = next(s) + case t.kind + of Ident: + case pool.strings[t.litId] + of "true": result = true + of "false": result = false + else: result = true + of ParLe: + let tag = pool.tags[t.tagId] + case tag + of "call", "cmd", "callstrlit", "infix", "prefix": + # First child is the head (function/operator name). + let head = next(s) + var name = "" + if head.kind == Ident: name = pool.strings[head.litId] + case name + of "defined": + let arg = next(s) + var sym = "" + if arg.kind == Ident: sym = pool.strings[arg.litId] + result = sym.len > 0 and isDefined(c.config, sym) + of "not": + result = not evalCondExpr(c, s) + of "and": + result = evalCondExpr(c, s) + if result: result = evalCondExpr(c, s) + else: skipSubtree(s, next(s)) + of "or": + result = evalCondExpr(c, s) + if not result: result = evalCondExpr(c, s) + else: skipSubtree(s, next(s)) + else: + result = true + # Drain whatever remains until the matching ParRi. + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: return + of "not": + result = not evalCondExpr(c, s) + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: return + of "and": + result = evalCondExpr(c, s) + if result: result = evalCondExpr(c, s) + else: skipSubtree(s, next(s)) + # consume closing ParRi + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: return + of "or": + result = evalCondExpr(c, s) + if not result: result = evalCondExpr(c, s) + else: skipSubtree(s, next(s)) + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: return + else: + skipSubtree(s, t) + result = true + else: + result = true + +proc whenMarkerHolds(c: DepContext; s: var Stream): bool = + ## Caller has just consumed the `(when` ParLe. Read children until the + ## matching `)`, AND-ing each evaluated condition. + result = true + while true: + # peek by reading; if it's ParRi, we're done + let t = next(s) + if t.kind == ParRi: return + if t.kind == EofToken: return + if t.kind == ParLe: + # Re-feed by manually evaluating the subtree starting at `t`. + # evalCondExpr expects to read its own opener, so handle it directly. + let tag = pool.tags[t.tagId] + case tag + of "call", "cmd", "callstrlit", "infix", "prefix": + let head = next(s) + var name = "" + if head.kind == Ident: name = pool.strings[head.litId] + var ok = true + case name + of "defined": + let arg = next(s) + var sym = "" + if arg.kind == Ident: sym = pool.strings[arg.litId] + ok = sym.len > 0 and isDefined(c.config, sym) + of "not": + ok = not evalCondExpr(c, s) + of "and": + ok = evalCondExpr(c, s) + if ok: ok = evalCondExpr(c, s) + of "or": + ok = evalCondExpr(c, s) + if not ok: ok = evalCondExpr(c, s) + else: + ok = true + # finish the subtree + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: return + if not ok: result = false + of "not", "and", "or": + # Re-emit a synthetic dispatch: rewrap by descending. + var ok = true + case tag + of "not": + ok = not evalCondExpr(c, s) + of "and": + ok = evalCondExpr(c, s) + if ok: ok = evalCondExpr(c, s) + of "or": + ok = evalCondExpr(c, s) + if not ok: ok = evalCondExpr(c, s) + else: discard + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: return + if not ok: result = false + else: + # Unknown — treat as true and skip. + skipSubtree(s, t) + elif t.kind == Ident: + let v = pool.strings[t.litId] + if v == "false": result = false + # else (true / unknown ident): keep result + proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) = ## Read a .deps.nif file and process imports/includes let depsPath = c.depsFile(pair) @@ -161,12 +323,27 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) = if t.kind == ParLe: let tag = pool.tags[t.tagId] case tag - of "import", "fromimport": - # Read import path + of "import", "fromimport", "include": + # Read first child. May be a `(when COND...)` marker — parse and + # evaluate; if the condition is statically false, skip the import + # entirely. Otherwise advance past the marker and parse the path. t = next(s) - # Check for "when" marker (conditional import) - if t.kind == Ident and pool.strings[t.litId] == "when": - t = next(s) # skip it, still process the import + var live = true + if t.kind == ParLe and pool.tags[t.tagId] == "when": + # whenMarkerHolds consumes everything up to and including the + # closing `)` of the `(when ...)` subtree. + live = whenMarkerHolds(c, s) + t = next(s) + if not live: + # Drain the rest of this import/include node. + var depth = 1 + while depth > 0: + let n = next(s) + if n.kind == ParLe: inc depth + elif n.kind == ParRi: dec depth + elif n.kind == EofToken: break + t = next(s) + continue # Handle path expression (could be ident, string, or infix like std/foo) var importPath = "" if t.kind == Ident: @@ -184,26 +361,11 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) = if t.kind == Ident: # second part (foo) importPath = importPath & "/" & pool.strings[t.litId] if importPath.len > 0: - processImport(c, importPath, current) - # Skip to end of import node - var depth = 1 - while depth > 0: - t = next(s) - if t.kind == ParLe: inc depth - elif t.kind == ParRi: dec depth - of "include": - # Read include path - t = next(s) - if t.kind == Ident and pool.strings[t.litId] == "when": - t = next(s) # skip conditional marker - var includePath = "" - if t.kind == Ident: - includePath = pool.strings[t.litId] - elif t.kind == StringLit: - includePath = pool.strings[t.litId] - if includePath.len > 0: - processInclude(c, includePath, current) - # Skip to end + if tag == "include": + processInclude(c, importPath, current) + else: + processImport(c, importPath, current) + # Skip to end of node var depth = 1 while depth > 0: t = next(s) @@ -326,10 +488,19 @@ proc generateBuildFile(c: DepContext): string = let exeFile = changeFileExt(c.nodes[0].files[0].nimFile, ExeExt) b.addTree "do" b.addIdent "nim_nifc" - # Input: .nim file (expanded as argument) and .nif file (dependency) + # Input: .nim file (expanded as argument) b.addTree "input" b.addStrLit mainNif b.endTree() + # Also depend on the semmed .nif files of the main module and all its + # dependencies. nifmake's topological sort orders nodes by depth; without + # these inputs the nim_nifc node sits at depth 1 (no recognized inputs) + # alongside the nifler nodes and runs *before* the nim_m steps that + # produce the .nif files it needs to read. + for node in c.nodes: + b.addTree "input" + b.addStrLit c.semmedFile(node.files[0]) + b.endTree() b.addTree "output" b.addStrLit exeFile b.endTree() diff --git a/compiler/jsgen.nim b/compiler/jsgen.nim index 98153490df..fd82a127f7 100644 --- a/compiler/jsgen.nim +++ b/compiler/jsgen.nim @@ -1544,7 +1544,7 @@ proc genSymAddr(p: PProc, n: PNode, typ: PType, r: var TCompRes) = r.res = s.loc.snippet r.address = "" r.typ = etyNone - of skVar, skLet, skResult: + of skVar, skLet, skResult, skTemp, skForVar: r.kind = resExpr let jsType = mapType(p): if typ.isNil: diff --git a/compiler/liftdestructors.nim b/compiler/liftdestructors.nim index 15c60363f8..9c37038fb5 100644 --- a/compiler/liftdestructors.nim +++ b/compiler/liftdestructors.nim @@ -732,7 +732,7 @@ proc fillStrOp(c: var TLiftCtx; t: PType; body, x, y: PNode) = of attachedAsgn, attachedDeepCopy, attachedDup: body.add callCodegenProc(c.g, "nimAsgnStrV2", c.info, genAddr(c, x), y) of attachedSink: - if c.g.config.isDefined("nimsso"): + if c.g.config.usesSso(): # SmallString: destroy old dst, then bit-copy src (no rc increment — this is a move). # No .p aliasing check needed; rc-based destroy handles COW sharing correctly. doAssert t.destructor != nil diff --git a/compiler/nim.cfg b/compiler/nim.cfg index 425f0df324..19c1df344f 100644 --- a/compiler/nim.cfg +++ b/compiler/nim.cfg @@ -12,7 +12,8 @@ define:nimPreviewNonVarDestructor define:nimPreviewCheckedClose define:nimPreviewAsmSemSymbol define:nimPreviewCStringComparisons -define:nimPreviewDuplicateModuleError +#define:nimPreviewDuplicateModuleError +# Incompatible with Nimony's compat2.nim for now threads:off diff --git a/compiler/nim.nim b/compiler/nim.nim index ed6774983c..a60e030118 100644 --- a/compiler/nim.nim +++ b/compiler/nim.nim @@ -121,6 +121,11 @@ proc handleCmdLine(cache: IdentCache; conf: ConfigRef) = conf.cmd in {cmdGendepend, cmdNifC, cmdIc, cmdM}: initOrcDefines(conf) + if conf.selectedStrings == stringSso and + conf.selectedGC notin {gcArc, gcOrc, gcYrc, gcAtomicArc}: + rawMessage(conf, errGenerated, + "--strings:sso requires --mm:arc, --mm:orc, --mm:yrc, or --mm:atomicArc") + mainCommand(graph) if conf.hasHint(hintGCStats): echo(GC_getStatistics()) #echo(GC_getStatistics()) diff --git a/compiler/options.nim b/compiler/options.nim index fc15ee9792..7a28b1dc6f 100644 --- a/compiler/options.nim +++ b/compiler/options.nim @@ -259,6 +259,9 @@ type ## Old transformation for closures in JS backend noPanicOnExcept ## don't panic on bare except + procParamTypeBackendAliases + ## Keep the old proc type compatibility rules that ignore backend + ## c type aliases. SymbolFilesOption* = enum disabledSf, writeOnlySf, readOnlySf, v2Sf, stressTest @@ -267,6 +270,10 @@ type ccNone, ccGcc, ccNintendoSwitch, ccLLVM_Gcc, ccCLang, ccBcc, ccVcc, ccTcc, ccEnv, ccIcl, ccIcc, ccClangCl, ccHipcc, ccNvcc + StringsMode* = enum + stringDefault = "default" + stringSso = "sso" + ExceptionSystem* = enum excNone, # no exception system selected yet excSetjmp, # setjmp based exception handling @@ -366,6 +373,7 @@ type implicitCmd*: bool # whether some flag triggered an implicit `command` selectedGC*: TGCMode # the selected GC (+) exc*: ExceptionSystem + selectedStrings*: StringsMode hintProcessingDots*: bool # true for dots, false for filenames verbosity*: int # how verbose the compiler is numberOfProcessors*: int # number of processors @@ -698,6 +706,7 @@ template quitOrRaise*(conf: ConfigRef, msg = "") = proc importantComments*(conf: ConfigRef): bool {.inline.} = conf.cmd in cmdDocLike + {cmdIdeTools} proc usesWriteBarrier*(conf: ConfigRef): bool {.inline.} = conf.selectedGC >= gcRefc +proc usesSso*(conf: ConfigRef): bool {.inline.} = conf.selectedStrings == stringSso template compilationCachePresent*(conf: ConfigRef): untyped = false diff --git a/compiler/semdata.nim b/compiler/semdata.nim index c561f6690e..15d8b14fe7 100644 --- a/compiler/semdata.nim +++ b/compiler/semdata.nim @@ -180,9 +180,9 @@ type sideEffects*: Table[int, seq[(TLineInfo, PSym)]] # symbol.id index inUncheckedAssignSection*: int importModuleLookup*: Table[int, seq[int]] # (module.ident.id, [module.id]) - forwardTypeUpdates*: seq[(PType, PNode)] - # types that need to be updated in a type section - # due to containing forward types, and their corresponding nodes + forwardTypeUpdates*: seq[(PSym, PType, PNode)] + # top-level owner, type, and type node for delayed retries inside a + # type section due to containing forward types forwardFieldUpdates*: seq[(PType, PNode, PType)] # object/tuple field definitions whose default values mention forward # types and need delayed const checking diff --git a/compiler/semexprs.nim b/compiler/semexprs.nim index 64a8d2a4f9..aa0489cd22 100644 --- a/compiler/semexprs.nim +++ b/compiler/semexprs.nim @@ -652,6 +652,9 @@ proc overloadedCallOpr(c: PContext, n: PNode): PNode = result = semExpr(c, result, flags = {efNoUndeclared}) proc changeType(c: PContext; n: PNode, newType: PType, check: bool) = + template isViewTarget(t: PType): bool = + t.skipTypes({tyGenericInst, tyAlias, tySink}).kind in {tyVar, tyLent} + case n.kind of nkCurly: for i in 0.. 0: + let pending = move c.forwardTypeUpdates + var madeProgress = false + + for (owner, typ, typeNode) in pending: + # types that need to be updated due to containing forward types + # and their corresponding type nodes + # for example generic invocations of forward types end up here + var reified = semTypeNode(c, typeNode, nil) + assert reified != nil + assignType(typ, reified) + typ.itemId = reified.itemId # same id + if containsForwardType(typ): + c.forwardTypeUpdates.add (owner, typ, typeNode) + elif not remainingOwners.missingOrExcl(owner.id): + madeProgress = true + + if not madeProgress: + # can't error here unfortunately + break + for (owner, field, expectedType) in c.forwardFieldUpdates: semDelayedFieldDefault(c, owner, expectedType, field) c.forwardFieldUpdates = @[] diff --git a/compiler/semtypes.nim b/compiler/semtypes.nim index 0b26cb8b8f..8009f7293c 100644 --- a/compiler/semtypes.nim +++ b/compiler/semtypes.nim @@ -223,7 +223,7 @@ proc semSet(c: PContext, n: PNode, prev: PType): PType = if base.kind in {tyGenericInst, tyAlias, tySink}: base = skipModifier(base) if base.kind notin {tyGenericParam, tyGenericInvocation}: if base.kind == tyForward: - c.forwardTypeUpdates.add (base, n[1]) + c.forwardTypeUpdates.add (getCurrOwner(c), result, n) elif not isOrdinalType(base, allowEnumWithHoles = true): localError(c.config, n.info, errOrdinalTypeExpected % typeToString(base, preferDesc)) elif lengthOrd(c.config, base) > MaxSetElements: @@ -370,6 +370,7 @@ proc semFieldDefault(c: PContext; owner, expectedType: PType; field: PNode): PTy propagateToOwner(owner, result) proc semDelayedFieldDefault(c: PContext; owner, expectedType: PType; field: PNode) = + resetSemFlag(field[^1]) fitDefaultNode(c, field[^1], expectedType) propagateToOwner(owner, field[^1].typ.skipIntLit(c.idgen)) @@ -1114,7 +1115,7 @@ proc semObjectNode(c: PContext, n: PNode, prev: PType; flags: TTypeFlags): PType if needsForwardUpdate: # if the inherited object is a forward type, # the entire object needs to be checked again - c.forwardTypeUpdates.add (result, n) # we retry in the final pass + c.forwardTypeUpdates.add (getCurrOwner(c), result, n) # we retry in the final pass rawAddSon(result, realBase) if realBase == nil and tfInheritable in flags: result.incl tfInheritable @@ -1762,7 +1763,7 @@ proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType = for i in 1..`_ that wraps Regex so that you can easily replace NRE. +## PCRE library is now at end of life. +## ## What is NRE? ## ============ ## @@ -84,7 +89,7 @@ type Regex* = ref RegexDesc ## Represents the pattern that things are matched against, constructed with ## `re(string)`. Examples: `re"foo"`, `re(r"(*ANYCRLF)(?x)foo # - ## comment".` + ## comment")` ## ## `pattern: string` ## : the string that was used to create the pattern. For details on how @@ -154,7 +159,7 @@ type ## will need to pass these as separate flags to PCRE. RegexMatch* = object - ## Usually seen as Option[RegexMatch], it represents the result of an + ## Usually seen as `Option[RegexMatch]`, it represents the result of an ## execution. On failure, it is none, on success, it is some. ## ## `pattern: Regex` diff --git a/lib/impure/re.nim b/lib/impure/re.nim index b39135779b..72d01b9527 100644 --- a/lib/impure/re.nim +++ b/lib/impure/re.nim @@ -10,6 +10,10 @@ when defined(js): {.error: "This library needs to be compiled with a c-like backend, and depends on PCRE; See jsre for JS backend.".} +## .. warning:: This module is deprecated. +## Use [Regex](https://github.com/nitely/nim-regex). +## PCRE library is now at end of life. +## ## Regular expression support for Nim. ## ## This module is implemented by providing a wrapper around the diff --git a/lib/pure/asynchttpserver.nim b/lib/pure/asynchttpserver.nim index a88a2d2e43..f757301ed8 100644 --- a/lib/pure/asynchttpserver.nim +++ b/lib/pure/asynchttpserver.nim @@ -153,7 +153,7 @@ proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] = protocol) result.orig = protocol i.inc protocol.parseSaturatedNatural(result.major, i) - i.inc # Skip . + if i < protocol.len: inc i # Skip . i.inc protocol.parseSaturatedNatural(result.minor, i) proc sendStatus(client: AsyncSocket, status: string): Future[void] = diff --git a/lib/pure/cgi.nim b/lib/pure/cgi.nim index 3d5d4d932e..8783b3389b 100644 --- a/lib/pure/cgi.nim +++ b/lib/pure/cgi.nim @@ -128,7 +128,7 @@ proc getContentLength*(): string = proc getContentType*(): string = ## Returns contents of the `CONTENT_TYPE` environment variable. - return getEnv("CONTENT_Type") + return getEnv("CONTENT_TYPE") proc getDocumentRoot*(): string = ## Returns contents of the `DOCUMENT_ROOT` environment variable. diff --git a/lib/pure/ssl_certs.nim b/lib/pure/ssl_certs.nim index d60cd22eb6..62289df158 100644 --- a/lib/pure/ssl_certs.nim +++ b/lib/pure/ssl_certs.nim @@ -36,6 +36,8 @@ elif defined(linux): # Android "/data/data/com.termux/files/usr/etc/tls/cert.pem", "/system/etc/security/cacerts", + # Nix + "/etc/ssl/certs/ca-bundle.crt" ] elif defined(bsd): const certificatePaths = [ diff --git a/lib/pure/streams.nim b/lib/pure/streams.nim index a1fffa5d95..bebd031ab2 100644 --- a/lib/pure/streams.nim +++ b/lib/pure/streams.nim @@ -259,7 +259,7 @@ proc readDataStr*(s: Stream, buffer: var string, slice: Slice[int]): int = result = s.readDataStrImpl(s, buffer, slice) else: # fallback - result = s.readData(beginStore(buffer, slice.b + 1 - slice.a, slice.a), slice.b + 1 - slice.a) + result = s.readData(beginStore(buffer, buffer.len, slice.a), slice.b + 1 - slice.a) endStore(buffer) template jsOrVmBlock(caseJsOrVm, caseElse: untyped): untyped = @@ -1226,7 +1226,7 @@ else: # after 1.3 or JS not defined jsOrVmBlock: buffer[slice.a.. s.data.len: - setLen(s.data, s.pos + bufLen) when defined(js): + if s.pos + bufLen > s.data.len: + setLen(s.data, s.pos + bufLen) try: s.data[s.pos..`_ , +## that is pure Nim regex engine and guarantees linear time matching. +## It supports compiling regex and matching at compile-time and +## works with JS backend. +## +## NRE2 is mostly compatible with NRE and the syntax of regular expression is similar to PCRE. +## But it lacks a few features and how to set options in a pattern is different. +## +## The syntax of regular expression is explained in https://nitely.github.io/nim-regex/regex.html +runnableExamples: + import std/sugar + let vowels = re"[aeoui]" + let bounds = collect: + for match in "moiga".findIter(vowels): match.matchBounds + assert bounds == @[1 .. 1, 2 .. 2, 4 .. 4] + from std/sequtils import toSeq + let s = sequtils.toSeq("moiga".findIter(vowels)) + # fully qualified to avoid confusion with nre.toSeq + assert s.len == 3 + + let firstVowel = "foo".find(vowels) + let hasVowel = firstVowel.isSome() + assert hasVowel + let matchBounds = firstVowel.get().captureBounds[-1] + assert matchBounds.a == 1 + + # as with module `re`, unless specified otherwise, `start` parameter in each + # proc indicates where the scan starts, but outputs are relative to the start + # of the input string, not to `start`: + assert find("uxabc", re"(?<=x|y)ab", start = 1).get.captures[-1] == "ab" + assert find("uxabc", re"ab", start = 3).isNone + +import std/[options, tables] +import regex, regex/nfatype + +export options +export regex.RegexFlags, regex.RegexError + +type + Regex* = regex.Regex2 + ## Represents the pattern that things are matched against, constructed with + ## `re(string)`. Examples: `re"foo"`, `re(r"(?x)foo #comment")` + ## + ## `captureCount: int` + ## : the number of captures that the pattern has. + ## + ## `captureNameId: Table[string, int]` + ## : a table from the capture names to their numeric id. + ## + ## The syntax of regular expression of Regex is explained in https://nitely.github.io/nim-regex/regex.html + + RegexMatch* = object + ## Usually seen as `Option[RegexMatch]`, it represents the result of an + ## execution. On failure, it is none, on success, it is some. + ## + ## `str: string` + ## : the string that was matched against + ## + ## `captures[]: string` + ## : the string value of whatever was captured at that id. If the value + ## is invalid, then behavior is undefined. If the id is `-1`, then + ## the whole match is returned. If the given capture was not matched, + ## `nil` is returned. See examples for `match`. + ## + ## `captureBounds[]: HSlice[int, int]` + ## : gets the bounds of the given capture according to the same rules as + ## the above. If the capture is not filled, then `None` is returned. + ## The bounds are both inclusive. See examples for `match`. + ## + ## `match: string` + ## : the full text of the match. + ## + ## `matchBounds: HSlice[int, int]` + ## : the bounds of the match, as in `captureBounds[]` + ## + ## `(captureBounds|captures).toTable` + ## : returns a table with each named capture as a key. + ## + ## `(captureBounds|captures).toSeq` + ## : returns all the captures by their number. + ## + ## `$: string` + ## : same as `match` + str*: string ## The string that was matched against. + matchImpl: regex.RegexMatch2 + + Captures* {.borrow: `.`.} = distinct RegexMatch + CaptureBounds* {.borrow: `.`.} = distinct RegexMatch + +func captureCount*(pattern: Regex): int {.inline.} = + pattern.toRegex().groupsCount + +func captureNameId*(pattern: Regex): Table[string, int] = + result = initTable[string, int](pattern.toRegex().namedGroups.len) + for k, v in pattern.toRegex().namedGroups: + result[k] = v + +func captureBounds*(match: RegexMatch): CaptureBounds {.inline.} = + CaptureBounds(match) + +func captures*(match: RegexMatch): Captures {.inline.} = + Captures(match) + +func contains*(match: Captures or CaptureBounds, i: int): bool {.inline.} = + i >= -1 and i < match.matchImpl.groupsCount and match.matchImpl.group(i) != reNonCapture + +func len*(match: Captures or CaptureBounds): int {.inline.} = + ## Return the number of capturing groups + match.matchImpl.groupsCount + +func `[]`*(match: CaptureBounds; i: int): HSlice[int, int] {.inline.} = + if i == -1: match.matchImpl.boundaries else: match.matchImpl.group(i) + +func `[]`*(match: CaptureBounds; name: string): HSlice[int, int] {.inline.} = + result = match.matchImpl.group(name) + if result == reNonCapture: + raise newException(KeyError, "Group '" & name & "' was not captured") + +func `[]`*(match: Captures; i: int): string {.inline.} = + match.str[CaptureBounds(match)[i]] + +func `[]`*(match: Captures, name: string): string {.inline.} = + match.str[CaptureBounds(match)[name]] + +func match*(match: RegexMatch): string {.inline.} = + match.str[match.matchImpl.boundaries] + +func matchBounds*(match: RegexMatch): HSlice[int, int] {.inline.} = + match.matchImpl.boundaries + +func contains*(match: CaptureBounds or Captures, name: string): bool {.inline.} = + name in match.matchImpl.namedGroups and + match.matchImpl.group(name) != reNonCapture + +func toTable*(match: Captures): Table[string, string] = + result = initTable[string, string]() + for k, i in match.matchImpl.namedGroups: + let r = match.matchImpl.group(i) + if r != reNonCapture: + result[k] = match.str[r] + +func toTable*(match: CaptureBounds): Table[string, HSlice[int, int]] = + result = initTable[string, HSlice[int, int]]() + for k, i in match.matchImpl.namedGroups: + let r = match.matchImpl.group(i) + if r != reNonCapture: + result[k] = match.matchImpl.group(i) + +iterator items*(match: CaptureBounds; default = none(HSlice[int, int])): Option[HSlice[int, int]] = + for i in 0 ..< match.len: + yield if i in match: some(match[i]) else: default + +iterator items*(match: Captures; default = none(string)): Option[string] = + for i in 0 ..< match.len: + yield if i in match: some(match[i]) else: default + +func toSeq*(match: CaptureBounds; + default = none(HSlice[int, int])): seq[Option[HSlice[int, int]]] = + result = @[] + for it in match.items(default): result.add it + +func toSeq*(match: Captures; + default: Option[string] = none(string)): seq[Option[string]] = + result = @[] + for it in match.items(default): result.add it + +func `$`*(match: RegexMatch): string = + match.match + +func re*(pattern: static string; flags: static RegexFlags = {}): static[Regex2] = + ## Parse and compile a regular expression at compile-time + result = regex.re2(pattern, flags) + +func re*(pattern: string; flags: RegexFlags = {}): Regex = + ## Parse and compile a regular expression at run-time + result = regex.re2(pattern, flags) + +func match*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[RegexMatch] = + ## Like `find(...)<#find,string,Regex,int>`_, but anchored to the start of the + ## string. + runnableExamples: + assert "foo".match(re"f").isSome + assert "foo".match(re"o").isNone + + assert "abc".match(re"(\w)").get.captures[0] == "a" + assert "abc".match(re"(?P\w)").get.captures["letter"] == "a" + assert "abc".match(re"(\w)\w").get.captures[-1] == "ab" + + assert "abc".match(re"(\w)").get.captureBounds[0] == 0 .. 0 + assert 0 in "abc".match(re"(\w)").get.captureBounds + assert "abc".match(re"").get.captureBounds[-1] == 0 .. -1 + assert "abc".match(re"abc").get.captureBounds[-1] == 0 .. 2 + var mat = default(RegexMatch) + let r = regex.startsWith(str.toOpenArray(0, min(str.high, endpos)), pattern, mat.matchImpl, start) + if r: + mat.str = str + some(mat) + else: + none(RegexMatch) + +iterator findIter*(str: string; pattern: Regex; start = 0, endpos = int.high): RegexMatch = + ## Works the same as `find(...)<#find,string,Regex,int>`_, but finds every + ## non-overlapping match: + runnableExamples: + import std/sugar + assert collect(for a in "2222".findIter(re"22"): a.match) == @["22", "22"] + # not @["22", "22", "22"] + ## Arguments are the same as `find(...)<#find,string,Regex,int>`_ + ## + ## Variants: + ## + ## - `proc findAll(...)` returns a `seq[string]` + var mat = RegexMatch(str: str) + # TODO: + # needs following PR to remove `substr` call. + # https://github.com/nitely/nim-regex/pull/162 + for m in regex.findAll(str.substr(start, endpos), pattern): + mat.matchImpl = m + yield mat + +proc find*(str: string; pattern: Regex; start = 0; endpos = int.high): Option[RegexMatch] = + ## Finds the given pattern in the string between the end and start + ## positions. + ## + ## `start` + ## : The start point at which to start matching. `|abc` is `0`; + ## `a|bc` is `1` + ## + ## `endpos` + ## : The maximum index for a match; `int.high` means the end of the + ## string, otherwise it’s an inclusive upper bound. + var mat = default(RegexMatch) + let r = regex.find(str.substr(start, endpos), pattern, mat.matchImpl) + + # remove following code after regex.find get `start`/`last` parameter + for v in mat.matchImpl.captures.mitems: + v.a += start + v.b += start + mat.matchImpl.boundaries.a += start + mat.matchImpl.boundaries.b += start + + if r: + mat.str = str + some(mat) + else: + none(RegexMatch) + +proc findAll*(str: string; pattern: Regex; start = 0; endpos = int.high): seq[string] = + result = @[] + for match in str.findIter(pattern, start, endpos): + result.add(match.match) + +proc contains*(str: string; pattern: Regex; start = 0; endpos = int.high): bool = + ## Determine if the string contains the given pattern between the end and + ## start positions: + ## This function is equivalent to `isSome(str.find(pattern, start, endpos))`. + runnableExamples: + assert "abc".contains(re"bc") + assert not "abc".contains(re"cd") + assert not "abc".contains(re"a", start = 1) + + isSome(str.find(pattern, start, endpos)) + +proc split*(str: string; pattern: Regex; maxSplit = -1; start = 0): seq[string] = + ## Splits the string with the given regex. This works according to the + ## rules that Perl and Javascript use. + ## + ## `start` behaves the same as in `find(...)<#find,string,Regex,int>`_. + ## + runnableExamples: + # - If the match is zero-width, then the string is still split: + assert "123".split(re"") == @["1", "2", "3"] + + # - If the pattern has a capture in it, it is added after the string + # split: + assert "12".split(re"(\d)") == @["", "1", "", "2", ""] + + # - If `maxsplit != -1`, then the string will only be split + # `maxsplit - 1` times. This means that there will be `maxsplit` + # strings in the output seq. + assert "1.2.3".split(re"\.", maxsplit = 2) == @["1", "2.3"] + + result = splitIncl(str, pattern, maxSplit, start) + +proc replace*(str: string; pattern: Regex; + subproc: proc (match: RegexMatch): string): string = + ## Replaces each match of Regex in the string with `subproc`, which should + ## never be or return `nil`. + ## + ## If `subproc` is a `proc (RegexMatch): string`, then it is executed with + ## each match and the return value is the replacement value. + ## + ## If `subproc` is a `proc (string): string`, then it is executed with the + ## full text of the match and the return value is the replacement value. + ## + ## If `subproc` is a string, the syntax is as follows: + ## + ## - `$$` - literal `$` + ## - `$123` - capture number `123` + ## - `$1$#` - first and second captures + ## - `$#` - first capture + ## + ## Following syntax is not supported in NRE2 + ## + ## - `$foo` - named capture `foo` + ## - `${foo}` - same as above + ## - `$0` - full match + ## + ## If a given capture is missing, `ValueError` is thrown. + proc by(m: RegexMatch2, s: string): string = + let mat = RegexMatch(str: s, matchImpl: m) + result = subproc(mat) + + result = regex.replace(str, pattern, by) + +proc replace*(str: string; pattern: Regex; + subproc: proc (match: string): string): string = + proc by(m: RegexMatch2; s: string): string = + result = subproc(s) + + result = regex.replace(str, pattern, by) + +proc replace*(str: string; pattern: Regex; sub: string): string = + result = regex.replace(str, pattern, sub) + +func escapeRe*(str: string): string = + ## Escapes the string so it doesn't match any special characters. + runnableExamples: + assert escapeRe("fly+wind") == "fly\\+wind" + assert escapeRe("nim*") == "nim\\*" + + result = regex.escapeRe(str) diff --git a/lib/std/nre2.nims b/lib/std/nre2.nims new file mode 100644 index 0000000000..1286aaa33a --- /dev/null +++ b/lib/std/nre2.nims @@ -0,0 +1,14 @@ +import std/os + +if getCommand() == "doc": + # std/nre2 requires nim-regex and it requires nim-unicodedb. + # when build documentation on CI, git clone them as nimble is not available + + const PkgDir = "build/deps" + const Pkgs = ["nim-regex", "nim-unicodedb"] + + for n in Pkgs: + if not dirExists(PkgDir / n): + exec("git clone -q https://github.com/nitely/" & n & " " & (PkgDir / n)) + + switch("path", "$nim" / PkgDir / n / "src") diff --git a/lib/std/packedsets.nim b/lib/std/packedsets.nim index 38d73a0d42..93c55498e8 100644 --- a/lib/std/packedsets.nim +++ b/lib/std/packedsets.nim @@ -294,7 +294,11 @@ proc containsOrIncl*[A](s: var PackedSet[A], key: A): bool = for i in 0.. PayloadSize: s.more.fullLen else: slen if newLen == curLen: return - if newLen <= 0: - # Pattern 's.setLen 0' is common for avoiding allocations; do NOT free the buffer. + if newLen < curLen: + # Shrinking: if slen > PayloadSize: if slen == HeapSlen and s.more.rc == 1: - s.more.fullLen = 0 - s.more.data[0] = '\0' + # Unique heap block: keep the buffer allocated to avoid alloc/dealloc + # ping-pong when callers shrink then grow (e.g. setLen(0) + add loops). + s.more.fullLen = newLen + s.more.data[newLen] = '\0' else: - # shared or static block: detach and go back to empty inline - nimDestroyStrV1(s) - s.bytes = 0 # slen=0, all inline chars zeroed + # shared or static block: detach and go back to inline + if newLen <= 0: + nimDestroyStrV1(s) + s.bytes = 0 + else: + let old = s.more + let inl = inlinePtr(s) + copyMem(inl, addr old.data[0], newLen) + inl[newLen] = '\0' + if slen == HeapSlen and atomicSubFetch(old.rc, 1) == 0: + dealloc(old) + if newLen < AlwaysAvail: + when system.cpuEndian == littleEndian: + let keepBits = (newLen + 1) * 8 + let charMask = ((uint(1) shl keepBits) - 1'u) and not 0xFF'u + s.bytes = (s.bytes and charMask) or uint(newLen) + else: + let discardBits = (AlwaysAvail - newLen) * 8 + let slenBit = 8 * (sizeof(uint) - 1) + let charMask = not ((uint(1) shl discardBits) - 1'u) and not (0xFF'u shl slenBit) + s.bytes = (s.bytes and charMask) or (uint(newLen) shl slenBit) + else: + setSSLen(s, newLen) else: - s.bytes = 0 # slen=0, all inline chars zeroed (SWAR safe) + # inline/medium shrink + if newLen <= 0: + s.bytes = 0 + else: + let inl = inlinePtr(s) + inl[newLen] = '\0' + if newLen < AlwaysAvail: + when system.cpuEndian == littleEndian: + let keepBits = (newLen + 1) * 8 + let charMask = ((uint(1) shl keepBits) - 1'u) and not 0xFF'u + s.bytes = (s.bytes and charMask) or uint(newLen) + else: + let discardBits = (AlwaysAvail - newLen) * 8 + let slenBit = 8 * (sizeof(uint) - 1) + let charMask = not ((uint(1) shl discardBits) - 1'u) and not (0xFF'u shl slenBit) + s.bytes = (s.bytes and charMask) or (uint(newLen) shl slenBit) + else: + setSSLen(s, newLen) return if slen <= PayloadSize: if newLen <= PayloadSize: @@ -564,34 +603,11 @@ proc setLengthStr(s: var SmallString; newLen: int; zeroing: bool) = s.more = p setSSLen(s, HeapSlen) else: - # currently long - if newLen <= PayloadSize: - # shrink back to inline/medium - let old = s.more - let inl = inlinePtr(s) - copyMem(inl, addr old.data[0], newLen) - inl[newLen] = '\0' - if slen == HeapSlen and atomicSubFetch(old.rc, 1) == 0: - dealloc(old) - # Zero padding bytes in `bytes` for SWAR invariant - if newLen < AlwaysAvail: - when system.cpuEndian == littleEndian: - let keepBits = (newLen + 1) * 8 - let charMask = ((uint(1) shl keepBits) - 1'u) and not 0xFF'u - s.bytes = (s.bytes and charMask) or uint(newLen) - else: - let discardBits = (AlwaysAvail - newLen) * 8 - let slenBit = 8 * (sizeof(uint) - 1) - let charMask = not ((uint(1) shl discardBits) - 1'u) and not (0xFF'u shl slenBit) - s.bytes = (s.bytes and charMask) or (uint(newLen) shl slenBit) - else: - setSSLen(s, newLen) - else: - # long -> long - ensureUniqueLong(s, curLen, newLen) # sets fullLen = newLen - if newLen > curLen: - zeroMem(addr s.more.data[curLen], newLen - curLen) - s.more.data[newLen] = '\0' + # currently long: grow within the heap buffer (shrinking already returned above) + ensureUniqueLong(s, curLen, newLen) # sets fullLen = newLen + if zeroing and newLen > curLen: + zeroMem(addr s.more.data[curLen], newLen - curLen) + s.more.data[newLen] = '\0' proc setLengthStrV2(s: var SmallString; newLen: int) {.compilerRtl.} = ## Sets the length of `s` to `newLen`, zeroing new bytes on growth. @@ -705,18 +721,37 @@ proc completeStore(s: var SmallString) {.compilerproc, inline.} = proc completeStore*(s: var string) {.inline.} = completeStore(cast[ptr SmallString](addr s)[]) -proc beginStore*(s: var string; ensuredLen: int; start = 0): ptr UncheckedArray[char] {.inline, noSideEffect, raises: [], tags: [].} = - ## Prepares `s` for a bulk write of `ensuredLen` bytes starting at `start`. - ## The caller must ensure `s.len >= start + ensuredLen` (e.g. via `newString` or `setLen`). +proc beginStore*(s: var string; newLen: int; start = 0): ptr UncheckedArray[char] {.inline, noSideEffect, raises: [], tags: [].} = + ## Sets s.len to `newLen` (new bytes are uninitialized), ensures unique + ## ownership, and returns a pointer to s[start] for bulk writing. ## Call `endStore(s)` afterwards to sync the inline cache. + ## To keep the current length, pass `s.len`. {.cast(noSideEffect).}: let ss = cast[ptr SmallString](addr s) let slen = ssLen(ss[]) - if slen > PayloadSize: - ensureUniqueLong(ss[], ss[].more.fullLen, ss[].more.fullLen) + let curLen = if slen > PayloadSize: ss[].more.fullLen else: slen + if newLen <= PayloadSize and slen <= PayloadSize: + # Stay inline/medium. + if newLen != curLen: + setSSLen(ss[], newLen) + result = cast[ptr UncheckedArray[char]](cast[uint](inlinePtr(ss[])) + uint(start)) + elif slen <= PayloadSize: + # Inline/medium → long. + let newCap = resize(newLen) + let p = cast[ptr LongString](alloc(LongStringDataOffset + newCap + 1)) + p.rc = 1 + p.fullLen = newLen + p.capImpl = newCap + copyMem(addr p.data[0], inlinePtr(ss[]), curLen) + p.data[newLen] = '\0' + ss[].more = p + setSSLen(ss[], HeapSlen) result = cast[ptr UncheckedArray[char]](addr ss[].more.data[start]) else: - result = cast[ptr UncheckedArray[char]](cast[uint](inlinePtr(ss[])) + uint(start)) + # Already long: resize within heap (no transition back to inline). + ensureUniqueLong(ss[], curLen, newLen) + ss[].more.data[newLen] = '\0' + result = cast[ptr UncheckedArray[char]](addr ss[].more.data[start]) proc endStore*(s: var string) {.inline, noSideEffect, raises: [], tags: [].} = ## Syncs the inline cache after bulk writes via `beginStore`. No-op for short/medium strings. diff --git a/lib/system/sysstr.nim b/lib/system/sysstr.nim index f7f5c3b08e..c879558dd0 100644 --- a/lib/system/sysstr.nim +++ b/lib/system/sysstr.nim @@ -264,7 +264,7 @@ proc setLengthStrUninit(s: var string, newlen: Natural) {.nodestroy.} = str.data[n] = '\0' str.len = n s = cast[string](str) - elif n < s.len: + elif n != s.len: str.data[n] = '\0' str.len = n else: return @@ -299,12 +299,22 @@ proc incrSeqV3(s: PGenericSeq, typ: PNimType): PGenericSeq {.compilerproc.} = # since we steal the content from 's', it's crucial to set s's len to 0. s.len = 0 +proc newSeqUninitRaw(typ: PNimType; len: int): pointer {.inline.} = + ## Creates a sequence payload with capacity and length `len` without + ## forcing zero-initialization for `ntfNoRefs` element types. + result = nimNewSeqOfCap(typ, len) + cast[PGenericSeq](result).len = len + proc extendCapacityRaw(src: PGenericSeq; typ: PNimType; - elemSize, elemAlign, newLen: int): PGenericSeq {.inline.} = + elemSize, elemAlign, newLen: int; + doInit: static bool): PGenericSeq {.inline.} = ## Reallocs `src` to fit `newLen` elements without any checks. ## Capacity always increases to at least next `resize` step. let newCap = max(resize(src.space), newLen) - result = cast[PGenericSeq](newSeq(typ, newCap)) + when doInit: + result = cast[PGenericSeq](newSeq(typ, newCap)) + else: + result = cast[PGenericSeq](newSeqUninitRaw(typ, newCap)) copyMem(dataPointer(result, elemAlign), dataPointer(src, elemAlign), src.len * elemSize) # since we steal the content from 's', it's crucial to set s's len to 0. src.len = 0 @@ -335,15 +345,19 @@ proc truncateRaw(src: PGenericSeq; baseFlags: set[TNimTypeFlag]; isTrivial: bool ((result.len-%newLen) *% elemSize)) template setLengthSeqImpl(s: PGenericSeq, typ: PNimType, newLen: int; isTrivial: bool; - doInit: static bool) = + doInit: static bool) = if s == nil: if newLen == 0: return s - else: return cast[PGenericSeq](newSeq(typ, newLen)) # newSeq zeroes! + else: + when doInit: + return cast[PGenericSeq](newSeq(typ, newLen)) # newSeq zeroes! + else: + return cast[PGenericSeq](newSeqUninitRaw(typ, newLen)) else: let elemSize = typ.base.size let elemAlign = typ.base.align result = if newLen > s.space: - s.extendCapacityRaw(typ, elemSize, elemAlign, newLen) + s.extendCapacityRaw(typ, elemSize, elemAlign, newLen, doInit) elif newLen < s.len: s.truncateRaw(typ.base.flags, isTrivial, elemSize, elemAlign, newLen) else: diff --git a/readme.md b/readme.md index 22d5294c2f..c0a60483c7 100644 --- a/readme.md +++ b/readme.md @@ -39,7 +39,7 @@ architecture combinations: |--------------------------------|----------------------------------------| | Windows (Windows XP or greater) | x86 and x86_64 | | Linux (most distributions) | x86, x86_64, ppc64, and armv6l | -| Mac OS X (10.04 or greater) | x86, x86_64, ppc64, and Apple Silicon (ARM64) | +| Mac OS X (10.4 or greater) | x86, x86_64, ppc64, and Apple Silicon (ARM64) | More platforms are supported, however, they are not tested regularly and they may not be as stable as the above-listed platforms. diff --git a/tests/concepts/tconcepts_issues.nim b/tests/concepts/tconcepts_issues.nim index c6d0267c5c..02d9d25bf1 100644 --- a/tests/concepts/tconcepts_issues.nim +++ b/tests/concepts/tconcepts_issues.nim @@ -176,6 +176,42 @@ block t6462: var s = SeqGen[int](fil: FilterMixin[int](test: nil, trans: nil)) doAssert s.test() == nil +block concept_with_cint: + # Generic proc matching through concepts with cint should still work + type + FilterMixin[T] = ref object + test: (T) -> bool + trans: (T) -> T + + SeqGen[T] = ref object + fil: FilterMixin[T] + + WithFilter[T] = concept a + a.fil is FilterMixin[T] + + proc test[T](a: WithFilter[T]): (T) -> bool = + a.fil.test + + var s = SeqGen[cint](fil: FilterMixin[cint](test: nil, trans: nil)) + doAssert s.test() == nil + +block concept_with_int: + type + FilterMixin[T] = ref object + test: (T) -> bool + trans: (T) -> T + + SeqGen[T] = ref object + fil: FilterMixin[T] + + WithFilter[T] = concept a + a.fil is FilterMixin[T] + + proc test[T](a: WithFilter[T]): (T) -> bool = + a.fil.test + + var s = SeqGen[int](fil: FilterMixin[int](test: nil, trans: nil)) + doAssert s.test() == nil block t6770: diff --git a/tests/exception/tcpp_handler_raise_finally.nim b/tests/exception/tcpp_handler_raise_finally.nim new file mode 100644 index 0000000000..880903d5a9 --- /dev/null +++ b/tests/exception/tcpp_handler_raise_finally.nim @@ -0,0 +1,61 @@ +discard """ + targets: "cpp" + matrix: "--mm:arc; --mm:orc; --mm:refc" + output: ''' +inner: orig +finally +outer: re:orig +inner-typeless: orig +finally-typeless +outer-typeless: re-tl:orig +no-catch-finally +caught-propagated: prop +''' +""" + +# When an `except` handler raises a new exception, the enclosing `finally` +# block must still run before the new exception propagates to the outer +# try. +# +# The C++ backend previously emitted the finally's `catch (...)` as a +# sibling of the user-written catches. C++ does not allow sibling catches +# to catch each other's throws, so a handler-raised exception bypassed the +# finally entirely. The fix wraps the inner try/catch sequence in an +# outer try, so any escaping exception (whether from the body or from a +# handler) is captured before the finally runs. + +block typed_except: + try: + try: + raise newException(CatchableError, "orig") + except CatchableError as e: + echo "inner: ", e.msg + raise newException(CatchableError, "re:" & e.msg) + finally: + echo "finally" + except CatchableError as outer: + echo "outer: ", outer.msg + +block typeless_except: + try: + try: + raise newException(CatchableError, "orig") + except: + let e = getCurrentException() + echo "inner-typeless: ", e.msg + raise newException(CatchableError, "re-tl:" & e.msg) + finally: + echo "finally-typeless" + except CatchableError as outer: + echo "outer-typeless: ", outer.msg + +# try/finally without an except: the body's exception must still propagate +# after the finally runs. +block no_catch_finally: + try: + try: + raise newException(CatchableError, "prop") + finally: + echo "no-catch-finally" + except CatchableError as e: + echo "caught-propagated: ", e.msg diff --git a/tests/exception/tcpp_imported_exc.nim b/tests/exception/tcpp_imported_exc.nim index 0c7846956b..8ee008bd96 100644 --- a/tests/exception/tcpp_imported_exc.nim +++ b/tests/exception/tcpp_imported_exc.nim @@ -1,5 +1,5 @@ discard """ -matrix: "--mm:refc" +matrix: "--mm:refc; --mm:orc" targets: "cpp" output: ''' caught as std::exception diff --git a/tests/ic/tmiscs.nim b/tests/ic/tmiscs.nim index aabdd92601..e90ad05244 100644 --- a/tests/ic/tmiscs.nim +++ b/tests/ic/tmiscs.nim @@ -10,6 +10,7 @@ discard """ @[1, 2] ''' """ +import std/strbasics # Object variant / case object type diff --git a/tests/js/test2.nim b/tests/js/test2.nim index fa857ccc5c..c4cb2a25d3 100644 --- a/tests/js/test2.nim +++ b/tests/js/test2.nim @@ -4,7 +4,12 @@ js 3.14 7 1 -21550 --21550''' +-21550 +none(TT) +() +destroyed +destroyed +''' """ # This file tests the JavaScript generator @@ -56,3 +61,15 @@ proc foo09() = const y = 86400 echo (x - (y - 1)) div y # Still gives `-21551` foo09() + +import std/options + +type TT = object + +proc `=destroy`(x: TT) = echo "destroyed" + +func test1: Option[TT] = discard +func test2: TT = discard + +echo test1() # Crash in JS backend, not crash in C backend +echo test2() # Not crash diff --git a/tests/lent/tlent_tuple_address.nim b/tests/lent/tlent_tuple_address.nim new file mode 100644 index 0000000000..f9c8d4542d --- /dev/null +++ b/tests/lent/tlent_tuple_address.nim @@ -0,0 +1,12 @@ +discard """ + errormsg: "expression has no address" +""" + +iterator foo(x: int): (lent int, lent int) = + yield (x, x + 1) + + +var x = 12 +for i in foo(x): + echo i[0] + echo i[1] diff --git a/tests/metatype/ttypetraits.nim b/tests/metatype/ttypetraits.nim index 0107f6b049..46601c1070 100644 --- a/tests/metatype/ttypetraits.nim +++ b/tests/metatype/ttypetraits.nim @@ -434,3 +434,32 @@ block: # bug #24378 type Win222[T] = typeof("foobar") doAssert not supportsCopyMem((int, Win222[int])) doAssert not supportsCopyMem(tuple[a: int, b: Win222[int]]) + +block: # bug #25789 + type + L[T; N: static int] = distinct seq[T] + EPF = distinct L[int, 100] + + var e: EPF = EPF(L[int, 100](@[1, 2, 3])) + + template classifyGeneric[T](x: T): bool = + when typeof(x) is L: + true + else: + false + + template classifyConcrete[T](x: T): bool = + when typeof(x) is L[int, 100]: + true + else: + false + + let viaConv = L[int, 100](e) + doAssert $type(viaConv) == "L[system.int, 100]" + doAssert classifyGeneric(viaConv) + doAssert classifyConcrete(viaConv) + + let viaDB = distinctBase(e, recursive = false) + doAssert $type(viaDB) == "L[system.int, 100]" + doAssert classifyGeneric(viaDB) + doAssert classifyConcrete(viaDB) diff --git a/tests/objects/t25627.nim b/tests/objects/t25627.nim index 7ba1e296c9..9c69d42798 100644 --- a/tests/objects/t25627.nim +++ b/tests/objects/t25627.nim @@ -68,3 +68,20 @@ block: let sized = Sized() doAssert sized.files.x == sizeof(Sized) +block: + type + Generic[T] = object + t: T + + WindowObj = object + svgCache: Generic[SVGSVGElement] + + SVGSVGElement = Generic[SVGSVGElementObj] + + SVGSVGElementObj = object + + proc foo() = + let p: pointer = nil + discard cast[ptr WindowObj](p) + + foo() diff --git a/tests/objects/tobject_default_value.nim b/tests/objects/tobject_default_value.nim index 5b0a5cd8e6..69e35bf826 100644 --- a/tests/objects/tobject_default_value.nim +++ b/tests/objects/tobject_default_value.nim @@ -833,4 +833,37 @@ proc overloaded[T: object](x: T) = var v: typeof(val) overloaded(v) -overloaded(Thing()) \ No newline at end of file +overloaded(Thing()) + +block: + type + Foo = object + x = Bar() + + Bar = object + x: int + + var f = Foo() + doassert f.x.x == 0 + +block: + type + Foo = object + x = Bar(x: 55) + + Bar = object + x: int + + var f = Foo() + doassert f.x.x == 55 + +block: + type + Bar = object + x: int + + Foo = object + x = Bar() + + var f = Foo() + doassert f.x.x == 0 diff --git a/tests/pragmas/tcustom_pragma.nim b/tests/pragmas/tcustom_pragma.nim index 3d6032e605..092d59bc74 100644 --- a/tests/pragmas/tcustom_pragma.nim +++ b/tests/pragmas/tcustom_pragma.nim @@ -549,3 +549,20 @@ block: type X {.p.} = object doAssert foo(X()) + +block: # typeof() type alias preserves field pragmas + template myFieldPragma {.pragma.} + type Orig = object + x {.myFieldPragma.}: int + + var orig: Orig + + # Direct typeof alias + type TAlias = typeof(orig) + var a: TAlias + doAssert a.x.hasCustomPragma(myFieldPragma) + + # Indirect alias of typeof alias + type TAlias2 = TAlias + var b: TAlias2 + doAssert b.x.hasCustomPragma(myFieldPragma) diff --git a/tests/proc/tbackendtypealias.nim b/tests/proc/tbackendtypealias.nim new file mode 100644 index 0000000000..eaae0c2917 --- /dev/null +++ b/tests/proc/tbackendtypealias.nim @@ -0,0 +1,44 @@ +# bug #25617 +# Ensure that proc types with backend type alias mismatches +# (e.g. uint vs csize_t) are rejected at the Nim level rather +# than producing invalid C code. + +discard """ + cmd: "nim check --hints:off --warnings:off --errorMax:0 $file" + action: "reject" + nimout: ''' +tbackendtypealias.nim(21, 7) Error: type mismatch: got but expected 'proc (len: uint){.closure.}' +tbackendtypealias.nim(28, 7) Error: type mismatch: got but expected 'proc (len: csize_t){.closure.}' +''' +""" + +block direct_assignment: + # Direct proc variable assignment with backend type alias mismatch + var + a: proc (len: uint) + b: proc (len: csize_t) + c = a + c = b + +block direct_assignment_reverse: + var + a: proc (len: csize_t) + b: proc (len: uint) + c = a + c = b + +block same_backend_type: + # Same backend type should still work + var + a: proc (len: uint) + b: proc (len: uint) + c = a + c = b + +block cint_same_type: + # cint to cint should work + var + a: proc (len: cint) + b: proc (len: cint) + c = a + c = b diff --git a/tests/stdlib/tnre2.nim b/tests/stdlib/tnre2.nim new file mode 100644 index 0000000000..6cea0f8114 --- /dev/null +++ b/tests/stdlib/tnre2.nim @@ -0,0 +1,196 @@ +import std/[assertions, options, sequtils, strutils, tables] +import std/nre2 + +block: + let pattern = "[0-9" + doAssertRaises(RegexError): discard re(pattern) + +block: # captures + block: # capture bounds are correct + let ex1 = re("([0-9])") + doAssert "1 23".find(ex1).get.matchBounds == 0 .. 0 + doAssert "1 23".find(ex1).get.captureBounds[0] == 0 .. 0 + doAssert "1 23".find(ex1, 1).get.matchBounds == 2 .. 2 + doAssert "1 23".find(ex1, 3).get.matchBounds == 3 .. 3 + + let ex2 = re("()()()()()()()()()()([0-9])") + doAssert "824".find(ex2).get.captureBounds[0] == 0 .. -1 + doAssert "824".find(ex2).get.captureBounds[10] == 0 .. 0 + + let ex3 = re("([0-9]+)") + doAssert "824".find(ex3).get.captureBounds[0] == 0 .. 2 + + block: # named captures + let ex1 = "foobar".find(re("(?Pfoo)(?Pbar)")) + doAssert ex1.get.captures["foo"] == "foo" + doAssert ex1.get.captures["bar"] == "bar" + + let ex2 = "foo".find(re("(?Pfoo)(?Pbar)?")) + doAssert "foo" in ex2.get.captureBounds + doAssert ex2.get.captures["foo"] == "foo" + doAssert not ("bar" in ex2.get.captures) + doAssertRaises(KeyError): + discard ex2.get.captures["bar"] + + block: # named capture bounds + let ex1 = "foo".find(re("(?Pfoo)(?Pbar)?")) + doAssert "foo" in ex1.get.captureBounds + doAssert ex1.get.captureBounds["foo"] == 0..2 + doAssert not ("bar" in ex1.get.captures) + doAssertRaises(KeyError): + discard ex1.get.captureBounds["bar"] + + block: # capture count + let ex1 = re("(?Pfoo)(?Pbar)?") + doAssert ex1.captureCount == 2 + doAssert ex1.captureNameId == {"foo" : 0, "bar" : 1}.toTable() + + block: # named capture table + let ex1 = "foo".find(re("(?Pfoo)(?Pbar)?")) + doAssert ex1.get.captures.toTable == {"foo" : "foo"}.toTable() + doAssert ex1.get.captureBounds.toTable == {"foo" : 0..2}.toTable() + + let ex2 = "foobar".find(re("(?Pfoo)(?Pbar)?")) + doAssert ex2.get.captures.toTable == {"foo" : "foo", "bar" : "bar"}.toTable() + + block: # capture sequence + let ex1 = "foo".find(re("(?Pfoo)(?Pbar)?")) + doAssert ex1.get.captures.toSeq == @[some("foo"), none(string)] + doAssert ex1.get.captureBounds.toSeq == @[some(0..2), none(Slice[int])] + doAssert ex1.get.captures.toSeq(some("")) == @[some("foo"), some("")] + + let ex2 = "foobar".find(re("(?Pfoo)(?Pbar)?")) + doAssert ex2.get.captures.toSeq == @[some("foo"), some("bar")] + +block: # match + block: # upper bound must be inclusive + doAssert "abc".match(re"abc", endpos = -1) == none(RegexMatch) + doAssert "abc".match(re"abc", endpos = 1) == none(RegexMatch) + doAssert "abc".match(re"abc", endpos = 2) != none(RegexMatch) + + block: # match examples + doAssert "abc".match(re"(\w)").get.captures[0] == "a" + doAssert "abc".match(re"(?P\w)").get.captures["letter"] == "a" + doAssert "abc".match(re"(\w)\w").get.captures[-1] == "ab" + doAssert "abc".match(re"(\w)").get.captureBounds[0] == 0 .. 0 + doAssert "abc".match(re"").get.captureBounds[-1] == 0 .. -1 + doAssert "abc".match(re"abc").get.captureBounds[-1] == 0 .. 2 + + let cap1 = "abc".match(re"(\w)(\w)+").get.captures + doAssert cap1.len == 2 + doAssert 0 in cap1 + doAssert 1 in cap1 + doAssert cap1[0] == "a" and cap1[1] == "c" + doAssert 0 in "abc".match(re"(\w)+").get.captureBounds + + block: # match test cases + doAssert "123".match(re"").get.matchBounds == 0 .. -1 + let mat1 = "123".match(re"123").get + doAssert mat1.matchBounds == 0 .. 2 + doAssert mat1.match == "123" + +block: # find + block: # find text + doAssert "3213a".find(re"[a-z]").get.match == "a" + doAssert sequtils.toSeq(findIter("1 2 3 4 5 6 7 8 ", re" ")).mapIt( + it.match + ) == @[" ", " ", " ", " ", " ", " ", " ", " "] + + block: # find bounds + doAssert sequtils.toSeq(findIter("1 2 3 4 5 ", re" ")).mapIt( + it.matchBounds + ) == @[1..1, 3..3, 5..5, 7..7, 9..9] + + block: # overlapping find + doAssert "222".findAll(re"22") == @["22"] + doAssert "2222".findAll(re"22") == @["22", "22"] + + block: # len 0 find + doAssert "".findAll(re"\ ") == newSeq[string]() + doAssert "".findAll(re"") == @[""] + doAssert "abc".findAll(re"") == @["", "", "", ""] + doAssert "word word".findAll(re"\b") == @["", "", "", ""] + doAssert "word\r\lword".findAll(re"(?m)$") == @["", ""] + doAssert "слово слово".findAll(re"\b") == @["", "", "", ""] + +block: # contains + doAssert "abc".contains(re"bc") + doAssert not "abc".contains(re"cd") + doAssert not "abc".contains(re"a", start = 1) + +block: # string splitting + block: # splitting strings + doAssert "1 2 3 4 5 6 ".split(re" ") == @["1", "2", "3", "4", "5", "6", ""] + doAssert "1 2 ".split(re(" ")) == @["1", "", "2", "", ""] + doAssert "1 2".split(re(" ")) == @["1", "2"] + doAssert "foo".split(re("foo")) == @["", ""] + doAssert "".split(re"foo") == @[""] + doAssert "9".split(re"\son\s") == @["9"] + + block: # captured patterns + doAssert "12".split(re"(\d)") == @["", "1", "", "2", ""] + + block: # maxsplit + doAssert "123".split(re"", maxsplit = 2) == @["1", "23"] + doAssert "123".split(re"", maxsplit = 1) == @["123"] + doAssert "123".split(re"", maxsplit = -1) == @["1", "2", "3"] + doAssert "1 2 3".split(re" ", maxsplit = 1) == @["1 2 3"] + doAssert "1 2 3".split(re" ", maxsplit = 2) == @["1", "2 3"] + doAssert "1 2 3".split(re"( )", maxsplit = 2) == @["1", " ", "2 3"] + + block: # split with 0-length match + doAssert "12345".split(re("")) == @["1", "2", "3", "4", "5"] + doAssert "".split(re"") == newSeq[string]() + doAssert "word word".split(re"\b") == @["word", " ", "word"] + #doAssert "word\r\lword".split(re"(?m)$") == @["word", "\r\lword"] + doAssert "слово слово".split(re"(\b)") == @["слово", "", " ", "", "слово", ""] + + block: # perl split tests + doAssert "forty-two" .split(re"") .join(",") == "f,o,r,t,y,-,t,w,o" + doAssert "forty-two" .split(re"", 3) .join(",") == "f,o,rty-two" + doAssert "split this string" .split(re" ") .join(",") == "split,this,string" + doAssert "split this string" .split(re" ", 2) .join(",") == "split,this string" + doAssert "try$this$string" .split(re"\$") .join(",") == "try,this,string" + doAssert "try$this$string" .split(re"\$", 2) .join(",") == "try,this$string" + doAssert "comma, separated, values" .split(re", ") .join("|") == "comma|separated|values" + doAssert "comma, separated, values" .split(re", ", 2) .join("|") == "comma|separated, values" + doAssert "Perl6::Camelia::Test" .split(re"::") .join(",") == "Perl6,Camelia,Test" + doAssert "Perl6::Camelia::Test" .split(re"::", 2) .join(",") == "Perl6,Camelia::Test" + doAssert "split,me,please" .split(re",") .join("|") == "split|me|please" + doAssert "split,me,please" .split(re",", 2) .join("|") == "split|me,please" + doAssert "Hello World Goodbye Mars".split(re"\s+") .join(",") == "Hello,World,Goodbye,Mars" + doAssert "Hello World Goodbye Mars".split(re"\s+", 3).join(",") == "Hello,World,Goodbye Mars" + doAssert "Hello test" .split(re"(\s+)") .join(",") == "Hello, ,test" + doAssert "this will be split" .split(re" ") .join(",") == "this,will,be,split" + doAssert "this will be split" .split(re" ", 3) .join(",") == "this,will,be split" + doAssert "a.b" .split(re"\.") .join(",") == "a,b" + doAssert "" .split(re"") .len == 0 + doAssert ":" .split(re"") .len == 1 + + block: # start position + doAssert "abc".split(re"", start = 1) == @["b", "c"] + doAssert "abc".split(re"", start = 2) == @["c"] + doAssert "abc".split(re"", start = 3) == newSeq[string]() + doAssert "abc".split(re"^b", start = 1) == @["bc"] + +block: # replace + block: # replace with 0-length strings + doAssert "".replace(re"1", proc (v: RegexMatch): string = "1") == "" + doAssert " ".replace(re"", proc (v: RegexMatch): string = "1") == "1 1" + doAssert "".replace(re"", proc (v: RegexMatch): string = "1") == "1" + + block: # regular replace + doAssert "123".replace(re"\d", "foo") == "foofoofoo" + doAssert "123".replace(re"(\d)", "$1$1") == "112233" + doAssert "123".replace(re"(\d)(\d)", "$1$2") == "123" + doAssert "123".replace(re"(\d)(\d)", "$#$#") == "123" + doAssert "abcdefghijklm".replace(re"(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)(k)(l)(m)", "$12") == "l" + + block: # replacing missing captures should throw instead of segfaulting + doAssertRaises(ValueError): discard "ab".replace(re"(a)", "$1$2") + +block: # escape strings + block: # escape strings + doAssert "123".escapeRe() == "123" + doAssert "[]".escapeRe() == r"\[\]" + doAssert "()".escapeRe() == r"\(\)" diff --git a/tests/stdlib/tnre2.nims b/tests/stdlib/tnre2.nims new file mode 100644 index 0000000000..ea30b440ce --- /dev/null +++ b/tests/stdlib/tnre2.nims @@ -0,0 +1,3 @@ +# std/nre2 requires nim-regex and it requires nim-unicodedb +exec("nimble --nimbleDir:build/deps install unicodedb@#head") +exec("nimble --nimbleDir:build/deps install regex@#head") diff --git a/tests/stdlib/tstring.nim b/tests/stdlib/tstring.nim index 724eef4314..536b41161b 100644 --- a/tests/stdlib/tstring.nim +++ b/tests/stdlib/tstring.nim @@ -1,5 +1,5 @@ discard """ - matrix: "--backend:c --mm:refc; --backend:c --mm:orc; --backend:c --mm:orc -d:nimsso; --backend:cpp --mm:refc; --backend:cpp --mm:orc; --backend:js --mm:refc; --backend:js --mm:orc" + matrix: "--backend:c --mm:refc; --backend:c --mm:orc; --backend:c --mm:orc --strings:sso; --backend:cpp --mm:refc; --backend:cpp --mm:orc; --backend:js --mm:refc; --backend:js --mm:orc" """ from std/sequtils import toSeq, map @@ -176,6 +176,13 @@ proc main() = result.add char(ord('a') + i mod 26) proc checkSetLenUninit(oldLen, newLen: int; cmpAfter = -1) = + ## Verifies `setLenUninit`: + ## - preserves the existing prefix + ## - updates the string length + ## - keeps internal null termination valid for both shrink and growth + ## + ## `cmpAfter` is used for layouts where trailing zeroed padding affects + ## string comparison semantics after the resize. var s = makeStr(oldLen) let prefixLen = min(oldLen, newLen) let prefix = makeStr(prefixLen) @@ -203,9 +210,18 @@ proc main() = block setLenUninit: # Shared baseline for both SSO and V2: noop, shrink, grow. - checkSetLenUninit(numbers.len, numbers.len) - checkSetLenUninit(numbers.len, 5) - checkSetLenUninit(numbers.len, 11) + checkSetLenUninit(10, 10) + checkSetLenUninit(10, 5) + checkSetLenUninit(10, 11) + + block growingWithinBiggerCapacity: + # Strings can reserve spare capacity even for short strings. + # Growing within that capacity must still update len and the trailing zero. + var s = newStringOfCap(10) + s.add("abc") + s.setLenUninit(6) + s.checkStrInternals(6) + doAssert s[0..2] == "abc" when hasNativeSso: const diff --git a/tests/system/tnimsso.nim b/tests/system/tnimsso.nim index ca9d64faec..c487945012 100644 --- a/tests/system/tnimsso.nim +++ b/tests/system/tnimsso.nim @@ -1,5 +1,5 @@ discard """ - matrix: "-d:nimsso" + matrix: "--strings:sso --mm:orc" targets: "c cpp" """ diff --git a/tests/types/tforwardcycletimeout.nim b/tests/types/tforwardcycletimeout.nim new file mode 100644 index 0000000000..2ced6576dc --- /dev/null +++ b/tests/types/tforwardcycletimeout.nim @@ -0,0 +1,10 @@ +discard """ + timeout: "1.0" +""" + +type + Generic[T] = object + t: T + + A = Generic[B] + B = Generic[A] diff --git a/tests/types/tillegalset.nim b/tests/types/tillegalset.nim new file mode 100644 index 0000000000..e4f60da444 --- /dev/null +++ b/tests/types/tillegalset.nim @@ -0,0 +1,7 @@ +discard """ + errormsg: "set is too large; use `std/sets` for ordinal types with more than 2^16 elements" +""" + +type + Foo = set[Bar] + Bar = int32 diff --git a/tests/types/tillegalset2.nim b/tests/types/tillegalset2.nim new file mode 100644 index 0000000000..737e7a5892 --- /dev/null +++ b/tests/types/tillegalset2.nim @@ -0,0 +1,8 @@ +discard """ + errormsg: "set is too large; use `std/sets` for ordinal types with more than 2^16 elements" +""" + +type + Foo = int32 + Bar = set[Baz] + Baz = Foo