From 15c6249f2c541d3d8fe5dbcd7a655a1b424420fa Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:41:06 +0800 Subject: [PATCH 1/8] replace benign with gcsafe (#25527) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/core/macros.nim | 18 +++++----- lib/pure/random.nim | 12 +++---- lib/pure/times.nim | 44 ++++++++++++------------ lib/std/private/osdirs.nim | 6 ++-- lib/std/syncio.nim | 60 ++++++++++++++++----------------- lib/system.nim | 24 ++++++------- lib/system/alloc.nim | 2 +- lib/system/assign.nim | 12 +++---- lib/system/avltree.nim | 4 +-- lib/system/channels_builtin.nim | 4 +-- lib/system/cyclebreaker.nim | 4 +-- lib/system/deepcopy.nim | 4 +-- lib/system/excpt.nim | 12 +++---- lib/system/gc.nim | 18 +++++----- lib/system/gc_common.nim | 2 +- lib/system/gc_hooks.nim | 2 +- lib/system/gc_interface.nim | 26 +++++++------- lib/system/gc_ms.nim | 12 +++---- lib/system/gc_regions.nim | 4 +-- lib/system/hti.nim | 4 +-- lib/system/jssys.nim | 4 +-- lib/system/memalloc.nim | 38 ++++++++++----------- lib/system/orc.nim | 4 +-- lib/system/repr.nim | 6 ++-- lib/system/yrc.nim | 6 ++-- 25 files changed, 166 insertions(+), 166 deletions(-) diff --git a/lib/core/macros.nim b/lib/core/macros.nim index 605df443ec..793ae75a13 100644 --- a/lib/core/macros.nim +++ b/lib/core/macros.nim @@ -433,15 +433,15 @@ when defined(nimHasNoReturnError): else: {.pragma: errorNoReturn.} -proc error*(msg: string, n: NimNode = nil) {.magic: "NError", benign, errorNoReturn.} +proc error*(msg: string, n: NimNode = nil) {.magic: "NError", gcsafe, errorNoReturn.} ## Writes an error message at compile time. The optional `n: NimNode` ## parameter is used as the source for file and line number information in ## the compilation error message. -proc warning*(msg: string, n: NimNode = nil) {.magic: "NWarning", benign.} +proc warning*(msg: string, n: NimNode = nil) {.magic: "NWarning", gcsafe.} ## Writes a warning message at compile time. -proc hint*(msg: string, n: NimNode = nil) {.magic: "NHint", benign.} +proc hint*(msg: string, n: NimNode = nil) {.magic: "NHint", gcsafe.} ## Writes a hint message at compile time. proc newStrLitNode*(s: string): NimNode {.noSideEffect.} = @@ -511,7 +511,7 @@ proc genSym*(kind: NimSymKind = nskLet; ident = ""): NimNode {. ## Generates a fresh symbol that is guaranteed to be unique. The symbol ## needs to occur in a declaration context. -proc callsite*(): NimNode {.magic: "NCallSite", benign, deprecated: +proc callsite*(): NimNode {.magic: "NCallSite", gcsafe, deprecated: "Deprecated since v0.18.1; use `varargs[untyped]` in the macro prototype instead".} ## Returns the AST of the invocation expression that invoked this macro. # see https://github.com/nim-lang/RFCs/issues/387 as candidate replacement. @@ -933,7 +933,7 @@ proc eqIdent*(a: NimNode; b: NimNode): bool {.magic: "EqIdent", noSideEffect.} const collapseSymChoice = not defined(nimLegacyMacrosCollapseSymChoice) -proc treeTraverse(n: NimNode; res: var string; level = 0; isLisp = false, indented = false) {.benign.} = +proc treeTraverse(n: NimNode; res: var string; level = 0; isLisp = false, indented = false) {.gcsafe.} = if level > 0: if indented: res.add("\n") @@ -982,21 +982,21 @@ proc treeTraverse(n: NimNode; res: var string; level = 0; isLisp = false, indent if isLisp: res.add(")") -proc treeRepr*(n: NimNode): string {.benign.} = +proc treeRepr*(n: NimNode): string {.gcsafe.} = ## Convert the AST `n` to a human-readable tree-like string. ## ## See also `repr`, `lispRepr`_, and `astGenRepr`_. result = "" n.treeTraverse(result, isLisp = false, indented = true) -proc lispRepr*(n: NimNode; indented = false): string {.benign.} = +proc lispRepr*(n: NimNode; indented = false): string {.gcsafe.} = ## Convert the AST `n` to a human-readable lisp-like string. ## ## See also `repr`, `treeRepr`_, and `astGenRepr`_. result = "" n.treeTraverse(result, isLisp = true, indented = indented) -proc astGenRepr*(n: NimNode): string {.benign.} = +proc astGenRepr*(n: NimNode): string {.gcsafe.} = ## Convert the AST `n` to the code required to generate that AST. ## ## See also `repr`_, `treeRepr`_, and `lispRepr`_. @@ -1005,7 +1005,7 @@ proc astGenRepr*(n: NimNode): string {.benign.} = NodeKinds = {nnkEmpty, nnkIdent, nnkSym, nnkNone, nnkCommentStmt} LitKinds = {nnkCharLit..nnkInt64Lit, nnkFloatLit..nnkFloat64Lit, nnkStrLit..nnkTripleStrLit} - proc traverse(res: var string, level: int, n: NimNode) {.benign.} = + proc traverse(res: var string, level: int, n: NimNode) {.gcsafe.} = for i in 0..level-1: res.add " " if n.kind in NodeKinds: res.add("new" & ($n.kind).substr(3) & "Node(") diff --git a/lib/pure/random.nim b/lib/pure/random.nim index 21303fdb64..ec5fef9e4c 100644 --- a/lib/pure/random.nim +++ b/lib/pure/random.nim @@ -243,7 +243,7 @@ proc rand[T: uint | uint64](r: var Rand; max: T): T = else: inc iters -proc rand*(r: var Rand; max: Natural): int {.benign.} = +proc rand*(r: var Rand; max: Natural): int {.gcsafe.} = ## Returns a random integer in the range `0..max` using the given state. ## ## **See also:** @@ -260,7 +260,7 @@ proc rand*(r: var Rand; max: Natural): int {.benign.} = cast[int](rand(r, uint64(max))) # xxx toUnsigned pending https://github.com/nim-lang/Nim/pull/18445 -proc rand*(max: int): int {.benign.} = +proc rand*(max: int): int {.gcsafe.} = ## Returns a random integer in the range `0..max`. ## ## If `randomize <#randomize>`_ has not been called, the sequence of random @@ -281,7 +281,7 @@ proc rand*(max: int): int {.benign.} = rand(state, max) -proc rand*(r: var Rand; max: range[0.0 .. high(float)]): float {.benign.} = +proc rand*(r: var Rand; max: range[0.0 .. high(float)]): float {.gcsafe.} = ## Returns a random floating point number in the range `0.0..max` ## using the given state. ## @@ -308,7 +308,7 @@ proc rand*(r: var Rand; max: range[0.0 .. high(float)]): float {.benign.} = let u = (0x3FFu64 shl 52u64) or (x shr 12u64) result = (cast[float](u) - 1.0) * max -proc rand*(max: float): float {.benign.} = +proc rand*(max: float): float {.gcsafe.} = ## Returns a random floating point number in the range `0.0..max`. ## ## If `randomize <#randomize>`_ has not been called, the sequence of random @@ -612,7 +612,7 @@ proc initRand*(seed: int64): Rand = skipRandomNumbers(result) discard next(result) -proc randomize*(seed: int64) {.benign.} = +proc randomize*(seed: int64) {.gcsafe.} = ## Initializes the default random number generator with the given seed. ## ## Providing a specific seed will produce the same results for that seed each time. @@ -736,7 +736,7 @@ when not defined(standalone): since (1, 5, 1): export initRand - proc randomize*() {.benign.} = + proc randomize*() {.gcsafe.} = ## Initializes the default random number generator with a seed based on ## random number source. ## diff --git a/lib/pure/times.nim b/lib/pure/times.nim index 3cdd3903c9..2951ac6cdb 100644 --- a/lib/pure/times.nim +++ b/lib/pure/times.nim @@ -382,9 +382,9 @@ type ## timezones. The `times` module only supplies implementations for the ## system's local time and UTC. zonedTimeFromTimeImpl: proc (x: Time): ZonedTime - {.tags: [], raises: [], benign.} + {.tags: [], raises: [], gcsafe.} zonedTimeFromAdjTimeImpl: proc (x: Time): ZonedTime - {.tags: [], raises: [], benign.} + {.tags: [], raises: [], gcsafe.} name: string ZonedTime* = object ## Represents a point in time with an associated @@ -432,7 +432,7 @@ else: # Helper procs # -{.pragma: operator, rtl, noSideEffect, benign.} +{.pragma: operator, rtl, noSideEffect, gcsafe.} proc convert*[T: SomeInteger](unitFrom, unitTo: FixedTimeUnit, quantity: T): T {.inline.} = @@ -518,7 +518,7 @@ proc fromEpochDay(epochday: int64): return (d.MonthdayRange, m.Month, (y + ord(m <= 2)).int) proc getDayOfYear*(monthday: MonthdayRange, month: Month, year: int): - YeardayRange {.tags: [], raises: [], benign.} = + YeardayRange {.tags: [], raises: [], gcsafe.} = ## Returns the day of the year. ## Equivalent with `dateTime(year, month, monthday, 0, 0, 0, 0).yearday`. runnableExamples: @@ -538,7 +538,7 @@ proc getDayOfYear*(monthday: MonthdayRange, month: Month, year: int): result = daysUntilMonth[month] + monthday - 1 proc getDayOfWeek*(monthday: MonthdayRange, month: Month, year: int): WeekDay - {.tags: [], raises: [], benign.} = + {.tags: [], raises: [], gcsafe.} = ## Returns the day of the week enum from day, month and year. ## Equivalent with `dateTime(year, month, monthday, 0, 0, 0, 0).weekday`. runnableExamples: @@ -922,21 +922,21 @@ proc nanosecond*(time: Time): NanosecondRange = time.nanosecond proc fromUnix*(unix: int64): Time - {.benign, tags: [], raises: [], noSideEffect.} = + {.gcsafe, tags: [], raises: [], noSideEffect.} = ## Convert a unix timestamp (seconds since `1970-01-01T00:00:00Z`) ## to a `Time`. runnableExamples: doAssert $fromUnix(0).utc == "1970-01-01T00:00:00Z" initTime(unix, 0) -proc toUnix*(t: Time): int64 {.benign, tags: [], raises: [], noSideEffect.} = +proc toUnix*(t: Time): int64 {.gcsafe, tags: [], raises: [], noSideEffect.} = ## Convert `t` to a unix timestamp (seconds since `1970-01-01T00:00:00Z`). ## See also `toUnixFloat` for subsecond resolution. runnableExamples: doAssert fromUnix(0).toUnix() == 0 t.seconds -proc fromUnixFloat(seconds: float): Time {.benign, tags: [], raises: [], noSideEffect.} = +proc fromUnixFloat(seconds: float): Time {.gcsafe, tags: [], raises: [], noSideEffect.} = ## Convert a unix timestamp in seconds to a `Time`; same as `fromUnix` ## but with subsecond resolution. runnableExamples: @@ -946,7 +946,7 @@ proc fromUnixFloat(seconds: float): Time {.benign, tags: [], raises: [], noSideE let nsecs = (seconds - secs) * 1e9 initTime(secs.int64, nsecs.NanosecondRange) -proc toUnixFloat(t: Time): float {.benign, tags: [], raises: [].} = +proc toUnixFloat(t: Time): float {.gcsafe, tags: [], raises: [].} = ## Same as `toUnix` but using subsecond resolution. runnableExamples: let t = getTime() @@ -975,7 +975,7 @@ proc toWinTime*(t: Time): int64 = proc getTimeImpl(typ: typedesc[Time]): Time = raiseAssert "implemented in the vm" -proc getTime*(): Time {.tags: [TimeEffect], benign.} = +proc getTime*(): Time {.tags: [TimeEffect], gcsafe.} = ## Gets the current time as a `Time` with up to nanosecond resolution. when nimvm: result = getTimeImpl(Time) @@ -1154,7 +1154,7 @@ proc isLeapDay*(dt: DateTime): bool {.since: (1, 1).} = assertDateTimeInitialized dt dt.year.isLeapYear and dt.month == mFeb and dt.monthday == 29 -proc toTime*(dt: DateTime): Time {.tags: [], raises: [], benign.} = +proc toTime*(dt: DateTime): Time {.tags: [], raises: [], gcsafe.} = ## Converts a `DateTime` to a `Time` representing the same point in time. assertDateTimeInitialized dt let epochDay = toEpochDay(dt.monthday, dt.month, dt.year) @@ -1197,9 +1197,9 @@ proc initDateTime(zt: ZonedTime, zone: Timezone): DateTime = proc newTimezone*( name: string, zonedTimeFromTimeImpl: proc (time: Time): ZonedTime - {.tags: [], raises: [], benign.}, + {.tags: [], raises: [], gcsafe.}, zonedTimeFromAdjTimeImpl: proc (adjTime: Time): ZonedTime - {.tags: [], raises: [], benign.} + {.tags: [], raises: [], gcsafe.} ): owned Timezone = ## Create a new `Timezone`. ## @@ -1263,12 +1263,12 @@ proc `==`*(zone1, zone2: Timezone): bool = zone1.name == zone2.name proc inZone*(time: Time, zone: Timezone): DateTime - {.tags: [], raises: [], benign.} = + {.tags: [], raises: [], gcsafe.} = ## Convert `time` into a `DateTime` using `zone` as the timezone. result = initDateTime(zone.zonedTimeFromTime(time), zone) proc inZone*(dt: DateTime, zone: Timezone): DateTime - {.tags: [], raises: [], benign.} = + {.tags: [], raises: [], gcsafe.} = ## Returns a `DateTime` representing the same point in time as `dt` but ## using `zone` as the timezone. assertDateTimeInitialized dt @@ -1283,14 +1283,14 @@ proc toAdjTime(dt: DateTime): Time = result = initTime(seconds, dt.nanosecond) when defined(js): - proc localZonedTimeFromTime(time: Time): ZonedTime {.benign.} = + proc localZonedTimeFromTime(time: Time): ZonedTime {.gcsafe.} = let jsDate = newDate(time.seconds * 1000) let offset = jsDate.getTimezoneOffset() * secondsInMin result.time = time result.utcOffset = offset result.isDst = false - proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime {.benign.} = + proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime {.gcsafe.} = let utcDate = newDate(adjTime.seconds * 1000) let localDate = newDate(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate(), utcDate.getUTCHours(), utcDate.getUTCMinutes(), @@ -1337,11 +1337,11 @@ else: return ((a.int64 - tm.toAdjUnix).int, tm.tm_isdst > 0) return (0, false) - proc localZonedTimeFromTime(time: Time): ZonedTime {.benign.} = + proc localZonedTimeFromTime(time: Time): ZonedTime {.gcsafe.} = let (offset, dst) = getLocalOffsetAndDst(time.seconds) result = ZonedTime(time: time, utcOffset: offset, isDst: dst) - proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime {.benign.} = + proc localZonedTimeFromAdjTime(adjTime: Time): ZonedTime {.gcsafe.} = var adjUnix = adjTime.seconds let past = adjUnix - secondsInDay let (pastOffset, _) = getLocalOffsetAndDst(past) @@ -1408,7 +1408,7 @@ proc local*(t: Time): DateTime = ## Shorthand for `t.inZone(local())`. t.inZone(local()) -proc now*(): DateTime {.tags: [TimeEffect], benign.} = +proc now*(): DateTime {.tags: [TimeEffect], gcsafe.} = ## Get the current time as a `DateTime` in the local timezone. ## Shorthand for `getTime().local`. ## @@ -2327,7 +2327,7 @@ proc parseTime*(input: string, f: static[string], zone: Timezone): Time const f2 = initTimeFormat(f) result = input.parse(f2, zone).toTime() -proc `$`*(dt: DateTime): string {.tags: [], raises: [], benign.} = +proc `$`*(dt: DateTime): string {.tags: [], raises: [], gcsafe.} = ## Converts a `DateTime` object to a string representation. ## It uses the format `yyyy-MM-dd'T'HH:mm:sszzz`. runnableExamples: @@ -2339,7 +2339,7 @@ proc `$`*(dt: DateTime): string {.tags: [], raises: [], benign.} = else: result = format(dt, "yyyy-MM-dd'T'HH:mm:sszzz") -proc `$`*(time: Time): string {.tags: [], raises: [], benign.} = +proc `$`*(time: Time): string {.tags: [], raises: [], gcsafe.} = ## Converts a `Time` value to a string representation. It will use the local ## time zone and use the format `yyyy-MM-dd'T'HH:mm:sszzz`. runnableExamples: diff --git a/lib/std/private/osdirs.nim b/lib/std/private/osdirs.nim index 5c6aa3e4d9..5c8ca2f432 100644 --- a/lib/std/private/osdirs.nim +++ b/lib/std/private/osdirs.nim @@ -331,7 +331,7 @@ proc rawRemoveDir(dir: string) {.noWeirdTarget.} = if rmdir(dir) != 0'i32 and errno != ENOENT: raiseOSError(osLastError(), dir) proc removeDir*(dir: string, checkDir = false) {.rtl, extern: "nos$1", tags: [ - WriteDirEffect, ReadDirEffect], benign, noWeirdTarget.} = + WriteDirEffect, ReadDirEffect], gcsafe, noWeirdTarget.} = ## Removes the directory `dir` including all subdirectories and files ## in `dir` (recursively). ## @@ -441,7 +441,7 @@ proc createDir*(dir: string) {.rtl, extern: "nos$1", discard existsOrCreateDir(p) proc copyDir*(source, dest: string, skipSpecial = false) {.rtl, extern: "nos$1", - tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect], benign, noWeirdTarget.} = + tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect], gcsafe, noWeirdTarget.} = ## Copies a directory from `source` to `dest`. ## ## On non-Windows OSes, symlinks are copied as symlinks. On Windows, symlinks @@ -482,7 +482,7 @@ proc copyDirWithPermissions*(source, dest: string, ignorePermissionErrors = true, skipSpecial = false) {.rtl, extern: "nos$1", tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect], - benign, noWeirdTarget.} = + gcsafe, noWeirdTarget.} = ## Copies a directory from `source` to `dest` preserving file permissions. ## ## On non-Windows OSes, symlinks are copied as symlinks. On Windows, symlinks diff --git a/lib/std/syncio.nim b/lib/std/syncio.nim index 2aafb40e93..164b35666a 100644 --- a/lib/std/syncio.nim +++ b/lib/std/syncio.nim @@ -182,7 +182,7 @@ proc checkErr(f: File) = {.push stackTrace: off, profiler: off.} proc readBuffer*(f: File, buffer: pointer, len: Natural): int {. - tags: [ReadIOEffect], benign.} = + tags: [ReadIOEffect], gcsafe.} = ## Reads `len` bytes into the buffer pointed to by `buffer`. Returns ## the actual number of bytes that have been read which may be less than ## `len` (if not as many bytes are remaining), but not greater. @@ -191,20 +191,20 @@ proc readBuffer*(f: File, buffer: pointer, len: Natural): int {. proc readBytes*(f: File, a: var openArray[int8|uint8], start, len: Natural): int {. - tags: [ReadIOEffect], benign.} = + tags: [ReadIOEffect], gcsafe.} = ## Reads `len` bytes into the buffer `a` starting at `a[start]`. Returns ## the actual number of bytes that have been read which may be less than ## `len` (if not as many bytes are remaining), but not greater. result = readBuffer(f, addr(a[start]), len) -proc readChars*(f: File, a: var openArray[char]): int {.tags: [ReadIOEffect], benign.} = +proc readChars*(f: File, a: var openArray[char]): int {.tags: [ReadIOEffect], gcsafe.} = ## Reads up to `a.len` bytes into the buffer `a`. Returns ## the actual number of bytes that have been read which may be less than ## `a.len` (if not as many bytes are remaining), but not greater. result = readBuffer(f, addr(a[0]), a.len) proc readChars*(f: File, a: var openArray[char], start, len: Natural): int {. - tags: [ReadIOEffect], benign, deprecated: + tags: [ReadIOEffect], gcsafe, deprecated: "use other `readChars` overload, possibly via: readChars(toOpenArray(buf, start, len-1))".} = ## Reads `len` bytes into the buffer `a` starting at `a[start]`. Returns ## the actual number of bytes that have been read which may be less than @@ -213,13 +213,13 @@ proc readChars*(f: File, a: var openArray[char], start, len: Natural): int {. raiseEIO("buffer overflow: (start+len) > length of openarray buffer") result = readBuffer(f, addr(a[start]), len) -proc write*(f: File, c: cstring) {.tags: [WriteIOEffect], benign.} = +proc write*(f: File, c: cstring) {.tags: [WriteIOEffect], gcsafe.} = ## Writes a value to the file `f`. May throw an IO exception. discard c_fputs(c, f) checkErr(f) proc writeBuffer*(f: File, buffer: pointer, len: Natural): int {. - tags: [WriteIOEffect], benign.} = + tags: [WriteIOEffect], gcsafe.} = ## Writes the bytes of buffer pointed to by the parameter `buffer` to the ## file `f`. Returns the number of actual written bytes, which may be less ## than `len` in case of an error. @@ -227,7 +227,7 @@ proc writeBuffer*(f: File, buffer: pointer, len: Natural): int {. checkErr(f) proc writeBytes*(f: File, a: openArray[int8|uint8], start, len: Natural): int {. - tags: [WriteIOEffect], benign.} = + tags: [WriteIOEffect], gcsafe.} = ## Writes the bytes of `a[start..start+len-1]` to the file `f`. Returns ## the number of actual written bytes, which may be less than `len` in case ## of an error. @@ -235,7 +235,7 @@ proc writeBytes*(f: File, a: openArray[int8|uint8], start, len: Natural): int {. result = writeBuffer(f, addr(x[int(start)]), len) proc writeChars*(f: File, a: openArray[char], start, len: Natural): int {. - tags: [WriteIOEffect], benign.} = + tags: [WriteIOEffect], gcsafe.} = ## Writes the bytes of `a[start..start+len-1]` to the file `f`. Returns ## the number of actual written bytes, which may be less than `len` in case ## of an error. @@ -264,7 +264,7 @@ when defined(windows): break inc i, w -proc write*(f: File, s: string) {.tags: [WriteIOEffect], benign.} = +proc write*(f: File, s: string) {.tags: [WriteIOEffect], gcsafe.} = when defined(windows): writeWindows(f, s, doRaise = true) else: @@ -393,7 +393,7 @@ when defined(nimdoc) or (defined(posix) and not defined(nimscript)) or defined(w inheritable.WinDWORD) != 0 proc readLine*(f: File, line: var string): bool {.tags: [ReadIOEffect], - benign.} = + gcsafe.} = ## Reads a line of text from the file `f` into `line`. May throw an IO ## exception. ## A line of text may be delimited by `LF` or `CRLF`. The newline @@ -519,43 +519,43 @@ proc readLine*(f: File, line: var string): bool {.tags: [ReadIOEffect], sp = 128 # read in 128 bytes at a time line.setLen(pos+sp) -proc readLine*(f: File): string {.tags: [ReadIOEffect], benign.} = +proc readLine*(f: File): string {.tags: [ReadIOEffect], gcsafe.} = ## Reads a line of text from the file `f`. May throw an IO exception. ## A line of text may be delimited by `LF` or `CRLF`. The newline ## character(s) are not part of the returned string. result = newStringOfCap(80) if not readLine(f, result): raiseEOF() -proc write*(f: File, i: int) {.tags: [WriteIOEffect], benign.} = +proc write*(f: File, i: int) {.tags: [WriteIOEffect], gcsafe.} = when sizeof(int) == 8: if c_fprintf(f, "%lld", i) < 0: checkErr(f) else: if c_fprintf(f, "%ld", i) < 0: checkErr(f) -proc write*(f: File, i: BiggestInt) {.tags: [WriteIOEffect], benign.} = +proc write*(f: File, i: BiggestInt) {.tags: [WriteIOEffect], gcsafe.} = when sizeof(BiggestInt) == 8: if c_fprintf(f, "%lld", i) < 0: checkErr(f) else: if c_fprintf(f, "%ld", i) < 0: checkErr(f) -proc write*(f: File, b: bool) {.tags: [WriteIOEffect], benign.} = +proc write*(f: File, b: bool) {.tags: [WriteIOEffect], gcsafe.} = if b: write(f, "true") else: write(f, "false") -proc write*(f: File, r: float32) {.tags: [WriteIOEffect], benign.} = +proc write*(f: File, r: float32) {.tags: [WriteIOEffect], gcsafe.} = var buffer {.noinit.}: array[65, char] discard writeFloatToBuffer(buffer, r) if c_fprintf(f, "%s", buffer[0].addr) < 0: checkErr(f) -proc write*(f: File, r: BiggestFloat) {.tags: [WriteIOEffect], benign.} = +proc write*(f: File, r: BiggestFloat) {.tags: [WriteIOEffect], gcsafe.} = var buffer {.noinit.}: array[65, char] discard writeFloatToBuffer(buffer, r) if c_fprintf(f, "%s", buffer[0].addr) < 0: checkErr(f) -proc write*(f: File, c: char) {.tags: [WriteIOEffect], benign.} = +proc write*(f: File, c: char) {.tags: [WriteIOEffect], gcsafe.} = discard c_putc(cint(c), f) -proc write*(f: File, a: varargs[string, `$`]) {.tags: [WriteIOEffect], benign.} = +proc write*(f: File, a: varargs[string, `$`]) {.tags: [WriteIOEffect], gcsafe.} = for x in items(a): write(f, x) proc readAllBuffer(file: File): string = @@ -579,7 +579,7 @@ proc rawFileSize(file: File): int64 = result = c_ftell(file) discard c_fseek(file, oldPos, 0) -proc endOfFile*(f: File): bool {.tags: [], benign.} = +proc endOfFile*(f: File): bool {.tags: [], gcsafe.} = ## Returns true if `f` is at the end. var c = c_fgetc(f) discard c_ungetc(c, f) @@ -603,7 +603,7 @@ proc readAllFile(file: File): string = var len = rawFileSize(file) result = readAllFile(file, len) -proc readAll*(file: File): string {.tags: [ReadIOEffect], benign.} = +proc readAll*(file: File): string {.tags: [ReadIOEffect], gcsafe.} = ## Reads all data from the stream `file`. ## ## Raises an IO exception in case of an error. It is an error if the @@ -621,7 +621,7 @@ proc readAll*(file: File): string {.tags: [ReadIOEffect], benign.} = result = readAllBuffer(file) proc writeLine*[Ty](f: File, x: varargs[Ty, `$`]) {.inline, - tags: [WriteIOEffect], benign.} = + tags: [WriteIOEffect], gcsafe.} = ## Writes the values `x` to `f` and then writes "\\n". ## May throw an IO exception. for i in items(x): @@ -713,7 +713,7 @@ when defined(posix) and not defined(nimscript): proc open*(f: var File, filename: string, mode: FileMode = fmRead, - bufSize: int = -1): bool {.tags: [], raises: [], benign.} = + bufSize: int = -1): bool {.tags: [], raises: [], gcsafe.} = ## Opens a file named `filename` with given `mode`. ## ## Default mode is readonly. Returns true if the file could be opened. @@ -747,7 +747,7 @@ proc open*(f: var File, filename: string, result = false proc reopen*(f: File, filename: string, mode: FileMode = fmRead): bool {. - tags: [], benign.} = + tags: [], gcsafe.} = ## Reopens the file `f` with given `filename` and `mode`. This ## is often used to redirect the `stdin`, `stdout` or `stderr` ## file variables. @@ -766,7 +766,7 @@ proc reopen*(f: File, filename: string, mode: FileMode = fmRead): bool {. result = false proc open*(f: var File, filehandle: FileHandle, - mode: FileMode = fmRead): bool {.tags: [], raises: [], benign.} = + mode: FileMode = fmRead): bool {.tags: [], raises: [], gcsafe.} = ## Creates a `File` from a `filehandle` with given `mode`. ## ## Default mode is readonly. Returns true if the file could be opened. @@ -792,26 +792,26 @@ proc open*(filename: string, if not open(result, filename, mode, bufSize): raise newException(IOError, "cannot open: " & filename) -proc setFilePos*(f: File, pos: int64, relativeTo: FileSeekPos = fspSet) {.benign, sideEffect.} = +proc setFilePos*(f: File, pos: int64, relativeTo: FileSeekPos = fspSet) {.gcsafe, sideEffect.} = ## Sets the position of the file pointer that is used for read/write ## operations. The file's first byte has the index zero. if c_fseek(f, pos, cint(relativeTo)) != 0: raiseEIO("cannot set file position") -proc getFilePos*(f: File): int64 {.benign.} = +proc getFilePos*(f: File): int64 {.gcsafe.} = ## Retrieves the current position of the file pointer that is used to ## read from the file `f`. The file's first byte has the index zero. result = c_ftell(f) if result < 0: raiseEIO("cannot retrieve file position") -proc getFileSize*(f: File): int64 {.tags: [ReadIOEffect], benign.} = +proc getFileSize*(f: File): int64 {.tags: [ReadIOEffect], gcsafe.} = ## Retrieves the file size (in bytes) of `f`. let oldPos = getFilePos(f) discard c_fseek(f, 0, 2) # seek the end of the file result = getFilePos(f) setFilePos(f, oldPos) -proc setStdIoUnbuffered*() {.tags: [], benign.} = +proc setStdIoUnbuffered*() {.tags: [], gcsafe.} = ## Configures `stdin`, `stdout` and `stderr` to be unbuffered. when declared(stdout): discard c_setvbuf(stdout, nil, IONBF, 0) @@ -865,7 +865,7 @@ when defined(windows) and appType == "console" and discard setConsoleCP(Utf8codepage) addExitProc(restoreConsoleCP) -proc readFile*(filename: string): string {.tags: [ReadIOEffect], benign.} = +proc readFile*(filename: string): string {.tags: [ReadIOEffect], gcsafe.} = ## Opens a file named `filename` for reading, calls `readAll ## <#readAll,File>`_ and closes the file afterwards. Returns the string. ## Raises an IO exception in case of an error. If you need to call @@ -880,7 +880,7 @@ proc readFile*(filename: string): string {.tags: [ReadIOEffect], benign.} = else: raise newException(IOError, "cannot open: " & filename) -proc writeFile*(filename, content: string) {.tags: [WriteIOEffect], benign.} = +proc writeFile*(filename, content: string) {.tags: [WriteIOEffect], gcsafe.} = ## Opens a file named `filename` for writing. Then writes the ## `content` completely to the file and closes the file afterwards. ## Raises an IO exception in case of an error. diff --git a/lib/system.nim b/lib/system.nim index 6104c1b928..27e7a04320 100644 --- a/lib/system.nim +++ b/lib/system.nim @@ -1147,7 +1147,7 @@ template sysAssert(cond: bool, msg: string) = const hasAlloc = (hostOS != "standalone" or not defined(nogc)) and not defined(nimscript) when notJSnotNims and hasAlloc and not defined(nimSeqsV2): - proc addChar(s: NimString, c: char): NimString {.compilerproc, benign.} + proc addChar(s: NimString, c: char): NimString {.compilerproc, gcsafe.} when defined(nimscript) or not defined(nimSeqsV2): proc add*[T](x: var seq[T], y: sink T) {.magic: "AppendSeqElem", noSideEffect.} @@ -1664,7 +1664,7 @@ when not defined(js) and hasThreadSupport and hostOS != "standalone": when not defined(js) and defined(nimV2): type - DestructorProc = proc (p: pointer) {.nimcall, benign, raises: [].} + DestructorProc = proc (p: pointer) {.nimcall, gcsafe, raises: [].} TNimTypeV2 {.compilerproc.} = object destructor: pointer size: int @@ -1776,7 +1776,7 @@ when not defined(nimscript): when not declared(sysFatal): include "system/fatal" -proc echo*(x: varargs[typed, `$`]) {.magic: "Echo", benign, sideEffect.} +proc echo*(x: varargs[typed, `$`]) {.magic: "Echo", gcsafe, sideEffect.} ## Writes and flushes the parameters to the standard output. ## ## Special built-in that takes a variable number of arguments. Each argument @@ -1883,7 +1883,7 @@ when notJSnotNims: ## lead to the `raise` statement. This only works for debug builds. var - globalRaiseHook*: proc (e: ref Exception): bool {.nimcall, benign.} + globalRaiseHook*: proc (e: ref Exception): bool {.nimcall, gcsafe.} ## With this hook you can influence exception handling on a global level. ## If not nil, every 'raise' statement ends up calling this hook. ## @@ -1892,7 +1892,7 @@ when notJSnotNims: ## If `globalRaiseHook` returns false, the exception is caught and does ## not propagate further through the call stack. - localRaiseHook* {.threadvar.}: proc (e: ref Exception): bool {.nimcall, benign.} + localRaiseHook* {.threadvar.}: proc (e: ref Exception): bool {.nimcall, gcsafe.} ## With this hook you can influence exception handling on a ## thread local level. ## If not nil, every 'raise' statement ends up calling this hook. @@ -1902,7 +1902,7 @@ when notJSnotNims: ## If `localRaiseHook` returns false, the exception ## is caught and does not propagate further through the call stack. - outOfMemHook*: proc () {.nimcall, tags: [], benign, raises: [].} + outOfMemHook*: proc () {.nimcall, tags: [], gcsafe, raises: [].} ## Set this variable to provide a procedure that should be called ## in case of an `out of memory`:idx: event. The standard handler ## writes an error message and terminates the program. @@ -1923,7 +1923,7 @@ when notJSnotNims: ## If the handler does not raise an exception, ordinary control flow ## continues and the program is terminated. - unhandledExceptionHook*: proc (e: ref Exception) {.nimcall, tags: [], benign, raises: [].} + unhandledExceptionHook*: proc (e: ref Exception) {.nimcall, tags: [], gcsafe, raises: [].} ## Set this variable to provide a procedure that should be called ## in case of an `unhandle exception` event. The standard handler ## writes an error message and terminates the program, except when @@ -2066,7 +2066,7 @@ when hostOS == "standalone" and defined(nogc): if s == nil or s.len == 0: result = cstring"" else: result = cast[cstring](addr s.data) -proc getTypeInfo*[T](x: T): pointer {.magic: "GetTypeInfo", benign.} +proc getTypeInfo*[T](x: T): pointer {.magic: "GetTypeInfo", gcsafe.} ## Get type information for `x`. ## ## Ordinary code should not use this, but the `typeinfo module @@ -2285,21 +2285,21 @@ when not defined(js) and declared(alloc0) and declared(dealloc): dealloc(a) when notJSnotNims and hostOS != "standalone": - proc getCurrentException*(): ref Exception {.compilerRtl, inl, benign.} = + proc getCurrentException*(): ref Exception {.compilerRtl, inl, gcsafe.} = ## Retrieves the current exception; if there is none, `nil` is returned. result = currException - proc nimBorrowCurrentException(): ref Exception {.compilerRtl, inl, benign, nodestroy.} = + proc nimBorrowCurrentException(): ref Exception {.compilerRtl, inl, gcsafe, nodestroy.} = # .nodestroy here so that we do not produce a write barrier as the # C codegen only uses it in a borrowed way: result = currException - proc getCurrentExceptionMsg*(): string {.inline, benign.} = + proc getCurrentExceptionMsg*(): string {.inline, gcsafe.} = ## Retrieves the error message that was attached to the current ## exception; if there is none, `""` is returned. return if currException == nil: "" else: currException.msg - proc setCurrentException*(exc: ref Exception) {.inline, benign.} = + proc setCurrentException*(exc: ref Exception) {.inline, gcsafe.} = ## Sets the current exception. ## ## .. warning:: Only use this if you know what you are doing. diff --git a/lib/system/alloc.nim b/lib/system/alloc.nim index a23702dc1f..1c2706120e 100644 --- a/lib/system/alloc.nim +++ b/lib/system/alloc.nim @@ -725,7 +725,7 @@ proc getSmallChunk(a: var MemRegion): PSmallChunk = # ----------------------------------------------------------------------------- when not defined(gcDestructors): - proc isAllocatedPtr(a: MemRegion, p: pointer): bool {.benign.} + proc isAllocatedPtr(a: MemRegion, p: pointer): bool {.gcsafe.} when true: template allocInv(a: MemRegion): bool = true diff --git a/lib/system/assign.nim b/lib/system/assign.nim index 9f4cbc0feb..0955222ec1 100644 --- a/lib/system/assign.nim +++ b/lib/system/assign.nim @@ -9,11 +9,11 @@ include seqs_v2_reimpl -proc genericResetAux(dest: pointer, n: ptr TNimNode) {.benign.} +proc genericResetAux(dest: pointer, n: ptr TNimNode) {.gcsafe.} -proc genericAssignAux(dest, src: pointer, mt: PNimType, shallow: bool) {.benign.} +proc genericAssignAux(dest, src: pointer, mt: PNimType, shallow: bool) {.gcsafe.} proc genericAssignAux(dest, src: pointer, n: ptr TNimNode, - shallow: bool) {.benign.} = + shallow: bool) {.gcsafe.} = var d = cast[int](dest) s = cast[int](src) @@ -187,8 +187,8 @@ proc genericAssignOpenArray(dest, src: pointer, len: int, genericAssign(cast[pointer](d +% i *% mt.base.size), cast[pointer](s +% i *% mt.base.size), mt.base) -proc objectInit(dest: pointer, typ: PNimType) {.compilerproc, benign.} -proc objectInitAux(dest: pointer, n: ptr TNimNode) {.benign.} = +proc objectInit(dest: pointer, typ: PNimType) {.compilerproc, gcsafe.} +proc objectInitAux(dest: pointer, n: ptr TNimNode) {.gcsafe.} = var d = cast[int](dest) case n.kind of nkNone: sysAssert(false, "objectInitAux") @@ -224,7 +224,7 @@ proc objectInit(dest: pointer, typ: PNimType) = # ---------------------- assign zero ----------------------------------------- -proc genericReset(dest: pointer, mt: PNimType) {.compilerproc, benign.} +proc genericReset(dest: pointer, mt: PNimType) {.compilerproc, gcsafe.} proc genericResetAux(dest: pointer, n: ptr TNimNode) = var d = cast[int](dest) case n.kind diff --git a/lib/system/avltree.nim b/lib/system/avltree.nim index 8d4b7e8974..b9020565a5 100644 --- a/lib/system/avltree.nim +++ b/lib/system/avltree.nim @@ -51,7 +51,7 @@ proc split(t: var PAvlNode) = t.link[0] = temp inc t.level -proc add(a: var MemRegion, t: var PAvlNode, key, upperBound: int) {.benign.} = +proc add(a: var MemRegion, t: var PAvlNode, key, upperBound: int) {.gcsafe.} = if t.isBottom: t = allocAvlNode(a, key, upperBound) else: @@ -70,7 +70,7 @@ proc add(a: var MemRegion, t: var PAvlNode, key, upperBound: int) {.benign.} = skew(t) split(t) -proc del(a: var MemRegion, t: var PAvlNode, x: int) {.benign.} = +proc del(a: var MemRegion, t: var PAvlNode, x: int) {.gcsafe.} = if isBottom(t): return a.last = t if x <% t.key: diff --git a/lib/system/channels_builtin.nim b/lib/system/channels_builtin.nim index 2123707301..9534c9b45e 100644 --- a/lib/system/channels_builtin.nim +++ b/lib/system/channels_builtin.nim @@ -181,10 +181,10 @@ proc deinitRawChannel(p: pointer) = when not usesDestructors: proc storeAux(dest, src: pointer, mt: PNimType, t: PRawChannel, - mode: LoadStoreMode) {.benign.} + mode: LoadStoreMode) {.gcsafe.} proc storeAux(dest, src: pointer, n: ptr TNimNode, t: PRawChannel, - mode: LoadStoreMode) {.benign.} = + mode: LoadStoreMode) {.gcsafe.} = var d = cast[int](dest) s = cast[int](src) diff --git a/lib/system/cyclebreaker.nim b/lib/system/cyclebreaker.nim index d611322d96..9ee8a98305 100644 --- a/lib/system/cyclebreaker.nim +++ b/lib/system/cyclebreaker.nim @@ -62,8 +62,8 @@ const colorMask = 0b011 type - TraceProc = proc (p, env: pointer) {.nimcall, benign, raises: [].} - DisposeProc = proc (p: pointer) {.nimcall, benign, raises: [].} + TraceProc = proc (p, env: pointer) {.nimcall, gcsafe, raises: [].} + DisposeProc = proc (p: pointer) {.nimcall, gcsafe, raises: [].} template color(c): untyped = c.rc and colorMask template setColor(c, col) = diff --git a/lib/system/deepcopy.nim b/lib/system/deepcopy.nim index 0f7d0eaae2..fdf1499e5f 100644 --- a/lib/system/deepcopy.nim +++ b/lib/system/deepcopy.nim @@ -58,9 +58,9 @@ proc put(t: var PtrTable; key, val: pointer) = inc t.counter proc genericDeepCopyAux(dest, src: pointer, mt: PNimType; - tab: var PtrTable) {.benign.} + tab: var PtrTable) {.gcsafe.} proc genericDeepCopyAux(dest, src: pointer, n: ptr TNimNode; - tab: var PtrTable) {.benign.} = + tab: var PtrTable) {.gcsafe.} = var d = cast[int](dest) s = cast[int](src) diff --git a/lib/system/excpt.nim b/lib/system/excpt.nim index 12552515cc..0218190607 100644 --- a/lib/system/excpt.nim +++ b/lib/system/excpt.nim @@ -16,7 +16,7 @@ import stacktraces const noStacktraceAvailable = "No stack traceback available\n" var - errorMessageWriter*: (proc(msg: string) {.tags: [WriteIOEffect], benign, + errorMessageWriter*: (proc(msg: string) {.tags: [WriteIOEffect], gcsafe, nimcall, raises: [].}) ## Function that will be called ## instead of `stdmsg.write` when printing stacktrace. @@ -61,10 +61,10 @@ proc showErrorMessage2(data: string) {.inline.} = # TODO showErrorMessage will turn it back to a string when a hook is set (!) showErrorMessage(data.cstring, data.len) -proc chckIndx(i, a, b: int): int {.inline, compilerproc, benign.} -proc chckRange(i, a, b: int): int {.inline, compilerproc, benign.} -proc chckRangeF(x, a, b: float): float {.inline, compilerproc, benign.} -proc chckNil(p: pointer) {.noinline, compilerproc, benign.} +proc chckIndx(i, a, b: int): int {.inline, compilerproc, gcsafe.} +proc chckRange(i, a, b: int): int {.inline, compilerproc, gcsafe.} +proc chckRangeF(x, a, b: float): float {.inline, compilerproc, gcsafe.} +proc chckNil(p: pointer) {.noinline, compilerproc, gcsafe.} type GcFrame = ptr GcFrameHeader @@ -653,7 +653,7 @@ when defined(cpp) and appType != "lib" and not gotoBasedExceptions and rawQuit 1 when not defined(noSignalHandler) and not defined(useNimRtl): - type Sighandler = proc (a: cint) {.noconv, benign.} + type Sighandler = proc (a: cint) {.noconv, gcsafe.} # xxx factor with ansi_c.CSighandlerT, posix.Sighandler proc signalHandler(sign: cint) {.exportc: "signalHandler", noconv, raises: [].} = diff --git a/lib/system/gc.nim b/lib/system/gc.nim index bc199b8351..861e0704f5 100644 --- a/lib/system/gc.nim +++ b/lib/system/gc.nim @@ -76,7 +76,7 @@ const when withRealTime and not declared(getTicks): include "system/timers" when defined(memProfiler): - proc nimProfile(requestedSize: int) {.benign.} + proc nimProfile(requestedSize: int) {.gcsafe.} when hasThreadSupport: import std/sharedlist @@ -97,7 +97,7 @@ type waZctDecRef, waPush #, waDebug - Finalizer {.compilerproc.} = proc (self: pointer) {.nimcall, benign, raises: [], gcsafe.} + Finalizer {.compilerproc.} = proc (self: pointer) {.nimcall, gcsafe, raises: [].} # A ref type can have a finalizer that is called before the object's # storage is freed. @@ -222,11 +222,11 @@ template gcTrace(cell, state: untyped) = when traceGC: traceCell(cell, state) # forward declarations: -proc collectCT(gch: var GcHeap) {.benign, raises: [].} -proc isOnStack(p: pointer): bool {.noinline, benign, raises: [].} -proc forAllChildren(cell: PCell, op: WalkOp) {.benign, raises: [].} -proc doOperation(p: pointer, op: WalkOp) {.benign, raises: [].} -proc forAllChildrenAux(dest: pointer, mt: PNimType, op: WalkOp) {.benign, raises: [].} +proc collectCT(gch: var GcHeap) {.gcsafe, raises: [].} +proc isOnStack(p: pointer): bool {.noinline, gcsafe, raises: [].} +proc forAllChildren(cell: PCell, op: WalkOp) {.gcsafe, raises: [].} +proc doOperation(p: pointer, op: WalkOp) {.gcsafe, raises: [].} +proc forAllChildrenAux(dest: pointer, mt: PNimType, op: WalkOp) {.gcsafe, raises: [].} # we need the prototype here for debugging purposes proc incRef(c: PCell) {.inline.} = @@ -338,7 +338,7 @@ proc cellsetReset(s: var CellSet) = {.push stacktrace:off.} -proc forAllSlotsAux(dest: pointer, n: ptr TNimNode, op: WalkOp) {.benign.} = +proc forAllSlotsAux(dest: pointer, n: ptr TNimNode, op: WalkOp) {.gcsafe.} = var d = cast[int](dest) case n.kind of nkSlot: forAllChildrenAux(cast[pointer](d +% n.offset), n.typ, op) @@ -687,7 +687,7 @@ proc doOperation(p: pointer, op: WalkOp) = proc nimGCvisit(d: pointer, op: int) {.compilerRtl, raises: [].} = doOperation(d, WalkOp(op)) -proc collectZCT(gch: var GcHeap): bool {.benign, raises: [].} +proc collectZCT(gch: var GcHeap): bool {.gcsafe, raises: [].} proc collectCycles(gch: var GcHeap) {.raises: [].} = when hasThreadSupport: diff --git a/lib/system/gc_common.nim b/lib/system/gc_common.nim index eb08845603..08e8798b08 100644 --- a/lib/system/gc_common.nim +++ b/lib/system/gc_common.nim @@ -457,7 +457,7 @@ proc deallocHeap*(runFinalizers = true; allowGcAfterwards = true) = initGC() type - GlobalMarkerProc = proc () {.nimcall, benign, raises: [].} + GlobalMarkerProc = proc () {.nimcall, gcsafe, raises: [].} var globalMarkersLen {.exportc.}: int globalMarkers {.exportc.}: array[0..3499, GlobalMarkerProc] diff --git a/lib/system/gc_hooks.nim b/lib/system/gc_hooks.nim index 936b31b20a..a8ecdd2f51 100644 --- a/lib/system/gc_hooks.nim +++ b/lib/system/gc_hooks.nim @@ -11,7 +11,7 @@ ## collectors etc. type - GlobalMarkerProc = proc () {.nimcall, benign, raises: [], tags: [].} + GlobalMarkerProc = proc () {.nimcall, gcsafe, raises: [], tags: [].} var globalMarkersLen: int globalMarkers: array[0..3499, GlobalMarkerProc] diff --git a/lib/system/gc_interface.nim b/lib/system/gc_interface.nim index b34ce4a566..256efbe547 100644 --- a/lib/system/gc_interface.nim +++ b/lib/system/gc_interface.nim @@ -12,7 +12,7 @@ when hasAlloc: gcOptimizeSpace ## optimize for memory footprint when hasAlloc and not defined(js) and not usesDestructors: - proc GC_disable*() {.rtl, inl, benign, raises: [].} + proc GC_disable*() {.rtl, inl, gcsafe, raises: [].} ## Disables the GC. If called `n` times, `n` calls to `GC_enable` ## are needed to reactivate the GC. ## @@ -20,39 +20,39 @@ when hasAlloc and not defined(js) and not usesDestructors: ## the mark and sweep phase with ## `GC_disableMarkAndSweep <#GC_disableMarkAndSweep>`_. - proc GC_enable*() {.rtl, inl, benign, raises: [].} + proc GC_enable*() {.rtl, inl, gcsafe, raises: [].} ## Enables the GC again. - proc GC_fullCollect*() {.rtl, benign, raises: [].} + proc GC_fullCollect*() {.rtl, gcsafe, raises: [].} ## Forces a full garbage collection pass. ## Ordinary code does not need to call this (and should not). - proc GC_enableMarkAndSweep*() {.rtl, benign, raises: [].} - proc GC_disableMarkAndSweep*() {.rtl, benign, raises: [].} + proc GC_enableMarkAndSweep*() {.rtl, gcsafe, raises: [].} + proc GC_disableMarkAndSweep*() {.rtl, gcsafe, raises: [].} ## The current implementation uses a reference counting garbage collector ## with a seldomly run mark and sweep phase to free cycles. The mark and ## sweep phase may take a long time and is not needed if the application ## does not create cycles. Thus the mark and sweep phase can be deactivated ## and activated separately from the rest of the GC. - proc GC_getStatistics*(): string {.rtl, benign, raises: [].} + proc GC_getStatistics*(): string {.rtl, gcsafe, raises: [].} ## Returns an informative string about the GC's activity. This may be useful ## for tweaking. - proc GC_ref*[T](x: ref T) {.magic: "GCref", benign, raises: [].} - proc GC_ref*[T](x: seq[T]) {.magic: "GCref", benign, raises: [].} - proc GC_ref*(x: string) {.magic: "GCref", benign, raises: [].} + proc GC_ref*[T](x: ref T) {.magic: "GCref", gcsafe, raises: [].} + proc GC_ref*[T](x: seq[T]) {.magic: "GCref", gcsafe, raises: [].} + proc GC_ref*(x: string) {.magic: "GCref", gcsafe, raises: [].} ## Marks the object `x` as referenced, so that it will not be freed until ## it is unmarked via `GC_unref`. ## If called n-times for the same object `x`, ## n calls to `GC_unref` are needed to unmark `x`. - proc GC_unref*[T](x: ref T) {.magic: "GCunref", benign, raises: [].} - proc GC_unref*[T](x: seq[T]) {.magic: "GCunref", benign, raises: [].} - proc GC_unref*(x: string) {.magic: "GCunref", benign, raises: [].} + proc GC_unref*[T](x: ref T) {.magic: "GCunref", gcsafe, raises: [].} + proc GC_unref*[T](x: seq[T]) {.magic: "GCunref", gcsafe, raises: [].} + proc GC_unref*(x: string) {.magic: "GCunref", gcsafe, raises: [].} ## See the documentation of `GC_ref <#GC_ref,string>`_. - proc nimGC_setStackBottom*(theStackBottom: pointer) {.compilerRtl, noinline, benign, raises: [].} + proc nimGC_setStackBottom*(theStackBottom: pointer) {.compilerRtl, noinline, gcsafe, raises: [].} ## Expands operating GC stack range to `theStackBottom`. Does nothing ## if current stack bottom is already lower than `theStackBottom`. diff --git a/lib/system/gc_ms.nim b/lib/system/gc_ms.nim index 9efca9cbae..fcaae690ba 100644 --- a/lib/system/gc_ms.nim +++ b/lib/system/gc_ms.nim @@ -36,7 +36,7 @@ type # local waMarkPrecise # fast precise marking - Finalizer {.compilerproc.} = proc (self: pointer) {.nimcall, benign, raises: [], gcsafe.} + Finalizer {.compilerproc.} = proc (self: pointer) {.nimcall, gcsafe, raises: [].} # A ref type can have a finalizer that is called before the object's # storage is freed. @@ -115,10 +115,10 @@ when BitsPerPage mod (sizeof(int)*8) != 0: {.error: "(BitsPerPage mod BitsPerUnit) should be zero!".} # forward declarations: -proc collectCT(gch: var GcHeap; size: int) {.benign, raises: [].} -proc forAllChildren(cell: PCell, op: WalkOp) {.benign, raises: [].} -proc doOperation(p: pointer, op: WalkOp) {.benign, raises: [].} -proc forAllChildrenAux(dest: pointer, mt: PNimType, op: WalkOp) {.benign, raises: [].} +proc collectCT(gch: var GcHeap; size: int) {.gcsafe, raises: [].} +proc forAllChildren(cell: PCell, op: WalkOp) {.gcsafe, raises: [].} +proc doOperation(p: pointer, op: WalkOp) {.gcsafe, raises: [].} +proc forAllChildrenAux(dest: pointer, mt: PNimType, op: WalkOp) {.gcsafe, raises: [].} # we need the prototype here for debugging purposes when defined(nimGcRefLeak): @@ -216,7 +216,7 @@ proc initGC() = gch.gcThreadId = atomicInc(gHeapidGenerator) - 1 gcAssert(gch.gcThreadId >= 0, "invalid computed thread ID") -proc forAllSlotsAux(dest: pointer, n: ptr TNimNode, op: WalkOp) {.benign.} = +proc forAllSlotsAux(dest: pointer, n: ptr TNimNode, op: WalkOp) {.gcsafe.} = var d = cast[int](dest) case n.kind of nkSlot: forAllChildrenAux(cast[pointer](d +% n.offset), n.typ, op) diff --git a/lib/system/gc_regions.nim b/lib/system/gc_regions.nim index 0385e2963d..c1bf61d283 100644 --- a/lib/system/gc_regions.nim +++ b/lib/system/gc_regions.nim @@ -12,7 +12,7 @@ import std/private/syslocks when defined(memProfiler): - proc nimProfile(requestedSize: int) {.benign.} + proc nimProfile(requestedSize: int) {.gcsafe.} when defined(useMalloc): proc roundup(x, v: int): int {.inline.} = @@ -41,7 +41,7 @@ else: # We also support 'finalizers'. type - Finalizer {.compilerproc.} = proc (self: pointer) {.nimcall, benign, raises: [], gcsafe.} + Finalizer {.compilerproc.} = proc (self: pointer) {.nimcall, gcsafe, raises: [].} # A ref type can have a finalizer that is called before the object's # storage is freed. diff --git a/lib/system/hti.nim b/lib/system/hti.nim index a26aff9822..4ae61753af 100644 --- a/lib/system/hti.nim +++ b/lib/system/hti.nim @@ -96,8 +96,8 @@ type base*: ptr TNimType node: ptr TNimNode # valid for tyRecord, tyObject, tyTuple, tyEnum finalizer*: pointer # the finalizer for the type - marker*: proc (p: pointer, op: int) {.nimcall, benign, tags: [], raises: [].} # marker proc for GC - deepcopy: proc (p: pointer): pointer {.nimcall, benign, tags: [], raises: [].} + marker*: proc (p: pointer, op: int) {.nimcall, gcsafe, tags: [], raises: [].} # marker proc for GC + deepcopy: proc (p: pointer): pointer {.nimcall, gcsafe, tags: [], raises: [].} when defined(nimSeqsV2): typeInfoV2*: pointer when defined(nimTypeNames): diff --git a/lib/system/jssys.nim b/lib/system/jssys.nim index b469c4695f..96f35c3c0c 100644 --- a/lib/system/jssys.nim +++ b/lib/system/jssys.nim @@ -51,7 +51,7 @@ proc nimCharToStr(x: char): string {.compilerproc.} = proc isNimException(): bool {.asmNoStackFrame.} = {.emit: "return `lastJSError` && `lastJSError`.m_type;".} -proc getCurrentException*(): ref Exception {.compilerRtl, benign.} = +proc getCurrentException*(): ref Exception {.compilerRtl, gcsafe.} = if isNimException(): result = cast[ref Exception](lastJSError) proc getCurrentExceptionMsg*(): string = @@ -72,7 +72,7 @@ proc getCurrentExceptionMsg*(): string = proc setCurrentException*(exc: ref Exception) = lastJSError = cast[PJSError](exc) -proc closureIterSetExc(e: ref Exception) {.compilerRtl, benign.} = +proc closureIterSetExc(e: ref Exception) {.compilerRtl, gcsafe.} = setCurrentException(e) proc pushCurrentException(e: sink(ref Exception)) {.compilerRtl, inline.} = diff --git a/lib/system/memalloc.nim b/lib/system/memalloc.nim index b26f3af24d..ed0de06c19 100644 --- a/lib/system/memalloc.nim +++ b/lib/system/memalloc.nim @@ -6,7 +6,7 @@ when notJSnotNims: ## Exactly `size` bytes will be overwritten. Like any procedure ## dealing with raw memory this is **unsafe**. - proc copyMem*(dest, source: pointer, size: Natural) {.inline, benign, + proc copyMem*(dest, source: pointer, size: Natural) {.inline, gcsafe, tags: [], raises: [], enforceNoRaises.} ## Copies the contents from the memory at `source` to the memory ## at `dest`. @@ -14,7 +14,7 @@ when notJSnotNims: ## regions may not overlap. Like any procedure dealing with raw ## memory this is **unsafe**. - proc moveMem*(dest, source: pointer, size: Natural) {.inline, benign, + proc moveMem*(dest, source: pointer, size: Natural) {.inline, gcsafe, tags: [], raises: [], enforceNoRaises.} ## Copies the contents from the memory at `source` to the memory ## at `dest`. @@ -48,17 +48,17 @@ when notJSnotNims: when hasAlloc and not defined(js): - proc allocImpl*(size: Natural): pointer {.noconv, rtl, tags: [], benign, raises: [].} - proc alloc0Impl*(size: Natural): pointer {.noconv, rtl, tags: [], benign, raises: [].} - proc deallocImpl*(p: pointer) {.noconv, rtl, tags: [], benign, raises: [].} - proc reallocImpl*(p: pointer, newSize: Natural): pointer {.noconv, rtl, tags: [], benign, raises: [].} - proc realloc0Impl*(p: pointer, oldSize, newSize: Natural): pointer {.noconv, rtl, tags: [], benign, raises: [].} + proc allocImpl*(size: Natural): pointer {.noconv, rtl, tags: [], gcsafe, raises: [].} + proc alloc0Impl*(size: Natural): pointer {.noconv, rtl, tags: [], gcsafe, raises: [].} + proc deallocImpl*(p: pointer) {.noconv, rtl, tags: [], gcsafe, raises: [].} + proc reallocImpl*(p: pointer, newSize: Natural): pointer {.noconv, rtl, tags: [], gcsafe, raises: [].} + proc realloc0Impl*(p: pointer, oldSize, newSize: Natural): pointer {.noconv, rtl, tags: [], gcsafe, raises: [].} - proc allocSharedImpl*(size: Natural): pointer {.noconv, compilerproc, rtl, benign, raises: [], tags: [].} - proc allocShared0Impl*(size: Natural): pointer {.noconv, rtl, benign, raises: [], tags: [].} - proc deallocSharedImpl*(p: pointer) {.noconv, rtl, benign, raises: [], tags: [].} - proc reallocSharedImpl*(p: pointer, newSize: Natural): pointer {.noconv, rtl, tags: [], benign, raises: [].} - proc reallocShared0Impl*(p: pointer, oldSize, newSize: Natural): pointer {.noconv, rtl, tags: [], benign, raises: [].} + proc allocSharedImpl*(size: Natural): pointer {.noconv, compilerproc, rtl, gcsafe, raises: [], tags: [].} + proc allocShared0Impl*(size: Natural): pointer {.noconv, rtl, gcsafe, raises: [], tags: [].} + proc deallocSharedImpl*(p: pointer) {.noconv, rtl, gcsafe, raises: [], tags: [].} + proc reallocSharedImpl*(p: pointer, newSize: Natural): pointer {.noconv, rtl, tags: [], gcsafe, raises: [].} + proc reallocShared0Impl*(p: pointer, oldSize, newSize: Natural): pointer {.noconv, rtl, tags: [], gcsafe, raises: [].} # Allocator statistics for memory leak tests @@ -103,7 +103,7 @@ when hasAlloc and not defined(js): incStat(allocCount) allocImpl(size) - proc createU*(T: typedesc, size = 1.Positive): ptr T {.inline, benign, raises: [].} = + proc createU*(T: typedesc, size = 1.Positive): ptr T {.inline, gcsafe, raises: [].} = ## Allocates a new memory block with at least `T.sizeof * size` bytes. ## ## The block has to be freed with `resize(block, 0) <#resize,ptr.T,Natural>`_ @@ -131,7 +131,7 @@ when hasAlloc and not defined(js): incStat(allocCount) alloc0Impl(size) - proc create*(T: typedesc, size = 1.Positive): ptr T {.inline, benign, raises: [].} = + proc create*(T: typedesc, size = 1.Positive): ptr T {.inline, gcsafe, raises: [].} = ## Allocates a new memory block with at least `T.sizeof * size` bytes. ## ## The block has to be freed with `resize(block, 0) <#resize,ptr.T,Natural>`_ @@ -174,7 +174,7 @@ when hasAlloc and not defined(js): ## from a shared heap. realloc0Impl(p, oldSize, newSize) - proc resize*[T](p: ptr T, newSize: Natural): ptr T {.inline, benign, raises: [].} = + proc resize*[T](p: ptr T, newSize: Natural): ptr T {.inline, gcsafe, raises: [].} = ## Grows or shrinks a given memory block. ## ## If `p` is **nil** then a new memory block is returned. @@ -187,7 +187,7 @@ when hasAlloc and not defined(js): ## from a shared heap. cast[ptr T](realloc(p, T.sizeof * newSize)) - proc dealloc*(p: pointer) {.noconv, compilerproc, rtl, benign, raises: [], tags: [].} = + proc dealloc*(p: pointer) {.noconv, compilerproc, rtl, gcsafe, raises: [], tags: [].} = ## Frees the memory allocated with `alloc`, `alloc0`, ## `realloc`, `create` or `createU`. ## @@ -218,7 +218,7 @@ when hasAlloc and not defined(js): allocSharedImpl(size) proc createSharedU*(T: typedesc, size = 1.Positive): ptr T {.inline, tags: [], - benign, raises: [].} = + gcsafe, raises: [].} = ## Allocates a new memory block on the shared heap with at ## least `T.sizeof * size` bytes. ## @@ -296,7 +296,7 @@ when hasAlloc and not defined(js): ## `freeShared <#freeShared,ptr.T>`_. cast[ptr T](reallocShared(p, T.sizeof * newSize)) - proc deallocShared*(p: pointer) {.noconv, compilerproc, rtl, benign, raises: [], tags: [].} = + proc deallocShared*(p: pointer) {.noconv, compilerproc, rtl, gcsafe, raises: [], tags: [].} = ## Frees the memory allocated with `allocShared`, `allocShared0` or ## `reallocShared`. ## @@ -307,7 +307,7 @@ when hasAlloc and not defined(js): incStat(deallocCount) deallocSharedImpl(p) - proc freeShared*[T](p: ptr T) {.inline, benign, raises: [].} = + proc freeShared*[T](p: ptr T) {.inline, gcsafe, raises: [].} = ## Frees the memory allocated with `createShared`, `createSharedU` or ## `resizeShared`. ## diff --git a/lib/system/orc.nim b/lib/system/orc.nim index 2b9ce22ec4..5be19fad93 100644 --- a/lib/system/orc.nim +++ b/lib/system/orc.nim @@ -29,8 +29,8 @@ const logOrc = defined(nimArcIds) type - TraceProc = proc (p, env: pointer) {.nimcall, benign, raises: [].} - DisposeProc = proc (p: pointer) {.nimcall, benign, raises: [].} + TraceProc = proc (p, env: pointer) {.nimcall, gcsafe, raises: [].} + DisposeProc = proc (p: pointer) {.nimcall, gcsafe, raises: [].} template color(c): untyped = c.rc and colorMask template setColor(c, col) = diff --git a/lib/system/repr.nim b/lib/system/repr.nim index 13118e40b2..e8ac1e41e5 100644 --- a/lib/system/repr.nim +++ b/lib/system/repr.nim @@ -77,7 +77,7 @@ include system/repr_impl type PByteArray = ptr UncheckedArray[byte] # array[0xffff, byte] -proc addSetElem(result: var string, elem: int, typ: PNimType) {.benign.} = +proc addSetElem(result: var string, elem: int, typ: PNimType) {.gcsafe.} = case typ.kind of tyEnum: add result, reprEnum(elem, typ) of tyBool: add result, reprBool(bool(elem)) @@ -147,7 +147,7 @@ when not defined(useNimRtl): for i in 0..cl.indent-1: add result, ' ' proc reprAux(result: var string, p: pointer, typ: PNimType, - cl: var ReprClosure) {.benign.} + cl: var ReprClosure) {.gcsafe.} proc reprArray(result: var string, p: pointer, typ: PNimType, cl: var ReprClosure) = @@ -188,7 +188,7 @@ when not defined(useNimRtl): add result, "]" proc reprRecordAux(result: var string, p: pointer, n: ptr TNimNode, - cl: var ReprClosure) {.benign.} = + cl: var ReprClosure) {.gcsafe.} = case n.kind of nkNone: sysAssert(false, "reprRecordAux") of nkSlot: diff --git a/lib/system/yrc.nim b/lib/system/yrc.nim index a681917de2..c682c896e1 100644 --- a/lib/system/yrc.nim +++ b/lib/system/yrc.nim @@ -90,8 +90,8 @@ const logOrc = defined(nimArcIds) type - TraceProc = proc (p, env: pointer) {.nimcall, benign, raises: [].} - DisposeProc = proc (p: pointer) {.nimcall, benign, raises: [].} + TraceProc = proc (p, env: pointer) {.nimcall, gcsafe, raises: [].} + DisposeProc = proc (p: pointer) {.nimcall, gcsafe, raises: [].} template color(c): untyped = c.rc and colorMask template setColor(c, col) = @@ -131,7 +131,7 @@ type toDec: array[QueueSize, (Cell, PNimTypeV2)] type - PreventThreadFromCollectProc* = proc(): bool {.nimcall, benign, raises: [].} + PreventThreadFromCollectProc* = proc(): bool {.nimcall, gcsafe, raises: [].} ## Callback run before this thread runs the cycle collector. ## Return `true` to allow collection, `false` to skip (e.g. real-time thread). ## Invoked while holding the global lock; must not call back into YRC. From 44eafa75520bc829d027d43c46cca41bd9390654 Mon Sep 17 00:00:00 2001 From: Miran Date: Sun, 22 Feb 2026 12:54:36 +0100 Subject: [PATCH 2/8] update the shipped tools (#25535) --- koch.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/koch.nim b/koch.nim index 1f193bce40..e59f7d9f12 100644 --- a/koch.nim +++ b/koch.nim @@ -12,9 +12,9 @@ const # examples of possible values for repos: Head, ea82b54 NimbleStableCommit = "9207e8b2bbdf66b5a4d1020214cff44d2d30df92" # 0.20.1 - AtlasStableCommit = "2aa62121b40d580aa2fb27920a37b938d36c5f57" # 0.9.4 + AtlasStableCommit = "092e42cfa3f29cb3258298f238f7a03df205daef" # 0.10.0 ChecksumsStableCommit = "0b8e46379c5bc1bf73d8b3011908389c60fb9b98" # 2.0.1 - SatStableCommit = "faf1617f44d7632ee9601ebc13887644925dcc01" + SatStableCommit = "e63eaea8baf00bed8bcd5a29ffd8823abb265b39" NimonyStableCommit = "deb9b50c573fb55e071825ab55385e293b7216d5" # unversioned \ # Note that Nimony uses Nim as a git submodule but we don't want to install From df42ebc5e6127e9eba1842b1a0a3d9d7403ee63f Mon Sep 17 00:00:00 2001 From: Miran Date: Sun, 22 Feb 2026 23:06:55 +0100 Subject: [PATCH 3/8] bump Atlas' version (#25539) --- koch.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/koch.nim b/koch.nim index e59f7d9f12..6f66a2ffe9 100644 --- a/koch.nim +++ b/koch.nim @@ -12,7 +12,7 @@ const # examples of possible values for repos: Head, ea82b54 NimbleStableCommit = "9207e8b2bbdf66b5a4d1020214cff44d2d30df92" # 0.20.1 - AtlasStableCommit = "092e42cfa3f29cb3258298f238f7a03df205daef" # 0.10.0 + AtlasStableCommit = "ff1f4289482dce94ba9f95b3b0ae16d16e21eb3d" # 0.10.1 ChecksumsStableCommit = "0b8e46379c5bc1bf73d8b3011908389c60fb9b98" # 2.0.1 SatStableCommit = "e63eaea8baf00bed8bcd5a29ffd8823abb265b39" From 6badeb1b4de6339107ccd0159e590349ea32a5ae Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 23 Feb 2026 13:39:55 +0100 Subject: [PATCH 4/8] yrc progress (#25534) --- compiler/liftdestructors.nim | 8 + compiler/semmagic.nim | 2 + lib/pure/typetraits.nim | 3 + lib/system.nim | 3 +- lib/system/rwlocks.nim | 143 ++++++++++++++++ lib/system/seqs_v2.nim | 171 ++++++++++++++----- lib/system/seqs_v2_reimpl.nim | 9 +- lib/system/yrc.nim | 242 ++++++++++++++------------- lib/system/yrc_proof.tla | 230 ++++++++++++++++++++++--- tests/arc/torcbench.nim | 3 +- tests/codegen/titaniummangle_nim.nim | 86 ++++------ 11 files changed, 665 insertions(+), 235 deletions(-) create mode 100644 lib/system/rwlocks.nim diff --git a/compiler/liftdestructors.nim b/compiler/liftdestructors.nim index 6600561c9c..f0a5acc78c 100644 --- a/compiler/liftdestructors.nim +++ b/compiler/liftdestructors.nim @@ -1286,7 +1286,15 @@ proc produceSym(g: ModuleGraph; c: PContext; typ: PType; kind: TTypeAttachedOp; tk = tyNone # no special casing for strings and seqs case tk of tySequence: + let needsYrcLock = g.config.selectedGC == gcYrc and + kind in {attachedDestructor, attachedSink, attachedAsgn, attachedDeepCopy, attachedDup} and + types.canFormAcycle(g, skipped.elementType) + # YRC: topology-changing seq ops must hold the mutator (read) lock + if needsYrcLock: + result.ast[bodyPos].add callCodegenProc(g, "acquireMutatorLock", info) fillSeqOp(a, typ, result.ast[bodyPos], d, src) + if needsYrcLock: + result.ast[bodyPos].add callCodegenProc(g, "releaseMutatorLock", info) of tyString: fillStrOp(a, typ, result.ast[bodyPos], d, src) else: diff --git a/compiler/semmagic.nim b/compiler/semmagic.nim index 8a91d820f0..87e085d4fd 100644 --- a/compiler/semmagic.nim +++ b/compiler/semmagic.nim @@ -236,6 +236,8 @@ proc evalTypeTrait(c: PContext; traitCall: PNode, operand: PType, context: PSym) let complexObj = containsGarbageCollectedRef(t) or hasDestructor(t) result = newIntNodeT(toInt128(ord(not complexObj)), traitCall, c.idgen, c.graph) + of "canFormCycles": + result = newIntNodeT(toInt128(ord(types.canFormAcycle(c.graph, operand))), traitCall, c.idgen, c.graph) of "hasDefaultValue": result = newIntNodeT(toInt128(ord(not operand.requiresInit)), traitCall, c.idgen, c.graph) of "isNamedTuple": diff --git a/lib/pure/typetraits.nim b/lib/pure/typetraits.nim index 508181316e..3043754f03 100644 --- a/lib/pure/typetraits.nim +++ b/lib/pure/typetraits.nim @@ -96,6 +96,9 @@ proc supportsCopyMem*(t: typedesc): bool {.magic: "TypeTrait".} ## ## Other languages name a type like these `blob`:idx:. +proc canFormCycles*(t: typedesc): bool {.magic: "TypeTrait".} + ## Returns true if `t` can form cycles. + proc hasDefaultValue*(t: typedesc): bool {.magic: "TypeTrait".} = ## Returns true if `t` has a valid default value. runnableExamples: diff --git a/lib/system.nim b/lib/system.nim index 27e7a04320..306818ffa0 100644 --- a/lib/system.nim +++ b/lib/system.nim @@ -627,7 +627,7 @@ proc newSeq*[T](s: var seq[T], len: Natural) {.magic: "NewSeq", noSideEffect.} ## #inputStrings[3] = "out of bounds" ## ``` -proc newSeq*[T](len = 0.Natural): seq[T] = +proc newSeq*[T](len = 0.Natural): seq[T] {.noSideEffect.} = ## Creates a new sequence of type `seq[T]` with length `len`. ## ## Note that the sequence will be filled with zeroed entries. @@ -1459,6 +1459,7 @@ proc isNil*[T: proc | iterator {.closure.}](x: T): bool {.noSideEffect, magic: " ## `== nil`. proc supportsCopyMem(t: typedesc): bool {.magic: "TypeTrait".} +proc canFormCycles(t: typedesc): bool {.magic: "TypeTrait".} when defined(nimHasTopDownInference): # magic used for seq type inference diff --git a/lib/system/rwlocks.nim b/lib/system/rwlocks.nim new file mode 100644 index 0000000000..edc8a8f61a --- /dev/null +++ b/lib/system/rwlocks.nim @@ -0,0 +1,143 @@ +# +# +# Nim's Runtime Library +# (c) Copyright 2026 Andreas Rumpf +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +# Read-write lock (RwLock) for lib/system. +# Used by YRC and by traceable containers that perform topology-changing ops. +# POSIX: pthread_rwlock_* ; Windows: SRWLOCK (slim reader/writer). + +{.push stackTrace: off.} + +when defined(windows): + # SRWLOCK is pointer-sized; use single pointer for ABI compatibility + type + RwLock* {.importc: "SRWLOCK", header: "", final, pure, byref.} = object + p: pointer + + proc initializeSRWLock(L: var RwLock) {.importc: "InitializeSRWLock", + header: "".} + proc acquireSRWLockShared(L: var RwLock) {.importc: "AcquireSRWLockShared", + header: "".} + proc releaseSRWLockShared(L: var RwLock) {.importc: "ReleaseSRWLockShared", + header: "".} + proc acquireSRWLockExclusive(L: var RwLock) {.importc: "AcquireSRWLockExclusive", + header: "".} + proc releaseSRWLockExclusive(L: var RwLock) {.importc: "ReleaseSRWLockExclusive", + header: "".} + + proc initRwLock*(L: var RwLock) {.inline.} = + initializeSRWLock(L) + proc deinitRwLock*(L: var RwLock) {.inline.} = + discard + proc acquireRead*(L: var RwLock) {.inline.} = + acquireSRWLockShared(L) + proc releaseRead*(L: var RwLock) {.inline.} = + releaseSRWLockShared(L) + proc acquireWrite*(L: var RwLock) {.inline.} = + acquireSRWLockExclusive(L) + proc releaseWrite*(L: var RwLock) {.inline.} = + releaseSRWLockExclusive(L) + +elif defined(genode): + {.error: "RwLock is not implemented for Genode".} + +else: + # POSIX: pthread_rwlock_* + type + SysRwLockObj {.importc: "pthread_rwlock_t", pure, final, + header: """#include + #include """, byref.} = object + when defined(linux) and defined(amd64): + abi: array[56 div sizeof(clong), clong] + + proc pthread_rwlock_init(rwlock: var SysRwLockObj, attr: pointer): cint {. + importc: "pthread_rwlock_init", header: "", noSideEffect.} + proc pthread_rwlock_destroy(rwlock: var SysRwLockObj): cint {. + importc: "pthread_rwlock_destroy", header: "", noSideEffect.} + proc pthread_rwlock_rdlock(rwlock: var SysRwLockObj): cint {. + importc: "pthread_rwlock_rdlock", header: "", noSideEffect.} + proc pthread_rwlock_wrlock(rwlock: var SysRwLockObj): cint {. + importc: "pthread_rwlock_wrlock", header: "", noSideEffect.} + proc pthread_rwlock_unlock(rwlock: var SysRwLockObj): cint {. + importc: "pthread_rwlock_unlock", header: "", noSideEffect.} + + when defined(linux): + # PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP: once a writer is waiting, + # new readers block. Prevents continuous mutator read-locks from starving + # the collector's write-lock acquisition (glibc default is PREFER_READER). + type + SysRwLockAttr {.importc: "pthread_rwlockattr_t", pure, final, + header: "".} = object + const PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP = cint(3) + proc pthread_rwlockattr_init(attr: ptr SysRwLockAttr): cint {. + importc: "pthread_rwlockattr_init", header: "".} + proc pthread_rwlockattr_destroy(attr: ptr SysRwLockAttr): cint {. + importc: "pthread_rwlockattr_destroy", header: "".} + proc pthread_rwlockattr_setkind_np(attr: ptr SysRwLockAttr; pref: cint): cint {. + importc: "pthread_rwlockattr_setkind_np", header: "".} + + when defined(ios): + type RwLock* = ptr SysRwLockObj + proc initRwLock*(L: var RwLock) = + when not declared(c_malloc): + proc c_malloc(size: csize_t): pointer {.importc: "malloc", header: "".} + proc c_free(p: pointer) {.importc: "free", header: "".} + L = cast[RwLock](c_malloc(csize_t(sizeof(SysRwLockObj)))) + discard pthread_rwlock_init(L[], nil) + proc deinitRwLock*(L: var RwLock) = + if L != nil: + discard pthread_rwlock_destroy(L[]) + when not declared(c_free): + proc c_free(p: pointer) {.importc: "free", header: "".} + c_free(L) + L = nil + proc acquireRead*(L: var RwLock) = + discard pthread_rwlock_rdlock(L[]) + proc releaseRead*(L: var RwLock) = + discard pthread_rwlock_unlock(L[]) + proc acquireWrite*(L: var RwLock) = + discard pthread_rwlock_wrlock(L[]) + proc releaseWrite*(L: var RwLock) = + discard pthread_rwlock_unlock(L[]) + else: + type RwLock* = SysRwLockObj + proc initRwLock*(L: var RwLock) = + when defined(linux): + var attr: SysRwLockAttr + discard pthread_rwlockattr_init(addr attr) + discard pthread_rwlockattr_setkind_np(addr attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP) + discard pthread_rwlock_init(L, addr attr) + discard pthread_rwlockattr_destroy(addr attr) + else: + discard pthread_rwlock_init(L, nil) + proc deinitRwLock*(L: var RwLock) = + discard pthread_rwlock_destroy(L) + proc acquireRead*(L: var RwLock) = + discard pthread_rwlock_rdlock(L) + proc releaseRead*(L: var RwLock) = + discard pthread_rwlock_unlock(L) + proc acquireWrite*(L: var RwLock) = + discard pthread_rwlock_wrlock(L) + proc releaseWrite*(L: var RwLock) = + discard pthread_rwlock_unlock(L) + +template withReadLock*(L: var RwLock, body: untyped) = + acquireRead(L) + try: + body + finally: + releaseRead(L) + +template withWriteLock*(L: var RwLock, body: untyped) = + acquireWrite(L) + try: + body + finally: + releaseWrite(L) + +{.pop.} diff --git a/lib/system/seqs_v2.nim b/lib/system/seqs_v2.nim index f0c880115c..154b443460 100644 --- a/lib/system/seqs_v2.nim +++ b/lib/system/seqs_v2.nim @@ -11,6 +11,90 @@ # import std/typetraits # strs already imported allocateds for us. +when defined(gcYrc): + include rwlocks + include threadids + + const + NumLockStripes = 64 + + type + YrcLockState = enum + HasNoLock + HasMutatorLock + HasCollectorLock + Collecting + + AlignedRwLock = object + ## One RwLock per cache line. {.align: 64.} causes the compiler to round + ## the struct size up to 64 bytes, so consecutive array elements never + ## share a cache line (sizeof(RwLock) = 56 on Linux x86_64 → 8 byte pad). + lock {.align: 64.}: RwLock + + var + gYrcLocks: array[NumLockStripes, AlignedRwLock] + var + lockState {.threadvar.}: YrcLockState + + proc getYrcStripe(): int {.inline.} = + ## Map this thread to one of the NumLockStripes RwLock stripes. + ## getThreadId() is already cached thread-locally in threadids.nim. + getThreadId() and (NumLockStripes - 1) + + proc acquireMutatorLock() {.compilerRtl, inl.} = + if lockState == HasNoLock: + acquireRead gYrcLocks[getYrcStripe()].lock + lockState = HasMutatorLock + + proc releaseMutatorLock() {.compilerRtl, inl.} = + if lockState == HasMutatorLock: + lockState = HasNoLock + releaseRead gYrcLocks[getYrcStripe()].lock + + template yrcMutatorLock*(t: typedesc; body: untyped) = + {.noSideEffect.}: + when canFormCycles(t): + acquireMutatorLock() + try: + body + finally: + {.noSideEffect.}: + when canFormCycles(t): + releaseMutatorLock() + + template yrcMutatorLockUntyped(body: untyped) = + {.noSideEffect.}: + acquireMutatorLock() + try: + body + finally: + {.noSideEffect.}: + releaseMutatorLock() + + template yrcCollectorLock(body: untyped) = + if lockState == HasMutatorLock: releaseMutatorLock() + let prevState = lockState + let hadToAcquire = prevState < HasCollectorLock + if hadToAcquire: + # Acquire all stripes in ascending order — the only thread ever holding + # multiple write locks is the collector, so there is no lock-order cycle. + for yrcI in 0..= x.len, "invalid newLen parameter for 'grow'" if newLen <= oldLen: return - var xu = cast[ptr NimSeqV2[T]](addr x) - if xu.p == nil or (xu.p.cap and not strlitFlag) < newLen: - xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, newLen - oldLen, sizeof(T), alignof(T))) - xu.len = newLen - for i in oldLen .. newLen-1: - when (NimMajor, NimMinor, NimPatch) >= (2, 3, 1): - xu.p.data[i] = `=dup`(value) - else: - wasMoved(xu.p.data[i]) - `=copy`(xu.p.data[i], value) + yrcMutatorLock(T): + var xu = cast[ptr NimSeqV2[T]](addr x) + if xu.p == nil or (xu.p.cap and not strlitFlag) < newLen: + xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, newLen - oldLen, sizeof(T), alignof(T))) + xu.len = newLen + for i in oldLen .. newLen-1: + when (NimMajor, NimMinor, NimPatch) >= (2, 3, 1): + xu.p.data[i] = `=dup`(value) + else: + wasMoved(xu.p.data[i]) + `=copy`(xu.p.data[i], value) proc add*[T](x: var seq[T]; y: sink T) {.magic: "AppendSeqElem", noSideEffect, nodestroy.} = ## Generic proc for adding a data item `y` to a container `x`. @@ -152,30 +238,32 @@ proc add*[T](x: var seq[T]; y: sink T) {.magic: "AppendSeqElem", noSideEffect, n ## Generic code becomes much easier to write if the Nim naming scheme is ## respected. {.cast(noSideEffect).}: - let oldLen = x.len - var xu = cast[ptr NimSeqV2[T]](addr x) - if xu.p == nil or (xu.p.cap and not strlitFlag) < oldLen+1: - xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, 1, sizeof(T), alignof(T))) - xu.len = oldLen+1 - # .nodestroy means `xu.p.data[oldLen] = value` is compiled into a - # copyMem(). This is fine as know by construction that - # in `xu.p.data[oldLen]` there is nothing to destroy. - # We also save the `wasMoved + destroy` pair for the sink parameter. - xu.p.data[oldLen] = y + yrcMutatorLock(T): + let oldLen = x.len + var xu = cast[ptr NimSeqV2[T]](addr x) + if xu.p == nil or (xu.p.cap and not strlitFlag) < oldLen+1: + xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, 1, sizeof(T), alignof(T))) + xu.len = oldLen+1 + # .nodestroy means `xu.p.data[oldLen] = value` is compiled into a + # copyMem(). This is fine as know by construction that + # in `xu.p.data[oldLen]` there is nothing to destroy. + # We also save the `wasMoved + destroy` pair for the sink parameter. + xu.p.data[oldLen] = y proc setLen[T](s: var seq[T], newlen: Natural) {.nodestroy.} = {.noSideEffect.}: if newlen < s.len: shrink(s, newlen) else: - let oldLen = s.len - if newlen <= oldLen: return - var xu = cast[ptr NimSeqV2[T]](addr s) - if xu.p == nil or (xu.p.cap and not strlitFlag) < newlen: - xu.p = cast[typeof(xu.p)](prepareSeqAddUninit(oldLen, xu.p, newlen - oldLen, sizeof(T), alignof(T))) - xu.len = newlen - for i in oldLen..= 0, calls `scanBlack`, and rescues A -# and everything reachable from it — including B. In short: the mutator can only -# modify objects it can reach, but the cycle collector only frees objects nothing -# external can reach. The two conditions are mutually exclusive. +# This problem structurally cannot arise in YRC for two reasons: # -#[ - -The problem described in Bacon01 is: during markGray/scan, a mutator concurrently -does X.field = Z (was X→Y), changing the physical graph while the collector is tracing -it. The collector might see stale or new edges. The reasons this is still safe: - -Stale edges cancel with unbuffered decrements: If the collector sees old edge X→Y -(mutator already wrote X→Z and buffered dec(Y)), the phantom trial deletion and the -unbuffered dec cancel — Y's effective RC is correct. - -scanBlack rescues via current physical edges: If X has external refs (merged RC reflects -the mutator's access), scanBlack(X) re-traces X and follows the current physical edge X→Z, -incrementing Z's RC and marking it black. Z survives. - -rcSum==edges fast path is conservative: Any discrepancy between physical graph and merged -state (stale or new edges) causes rcSum != edges, falling back to the slow path which -rescues anything with RC >= 0. - -Unreachable cycles are truly unreachable: The mutator can only reach objects through chains -rooted in merged references. If a cycle has zero external refs at merge time, no mutator -can reach it. - -]# +# 1. The mutator lock freezes the topology during all three passes, so no +# concurrent field write can race with markGray/scan/collectWhite. +# +# 2. Even without the lock, the cycle collector only frees *closed cycles* — +# subgraphs where every reference to every member comes from within the +# group, with zero external references. To execute `A.field = B` the +# mutator must hold a reference to A (external ref), which `scan` would +# rescue. The two conditions are mutually exclusive. +# +# In practice reason (1) makes reason (2) a belt-and-suspenders safety +# argument rather than the primary mechanism. {.push raises: [].} @@ -93,12 +71,49 @@ type TraceProc = proc (p, env: pointer) {.nimcall, gcsafe, raises: [].} DisposeProc = proc (p: pointer) {.nimcall, gcsafe, raises: [].} -template color(c): untyped = c.rc and colorMask -template setColor(c, col) = - when col == colBlack: - c.rc = c.rc and not colorMask - else: - c.rc = c.rc and not colorMask or col +when defined(nimYrcAtomicIncs): + template color(c): untyped = atomicLoadN(addr c.rc, ATOMIC_ACQUIRE) and colorMask + template setColor(c, col) = + block: + var expected = atomicLoadN(addr c.rc, ATOMIC_RELAXED) + while true: + let desired = (expected and not colorMask) or col + if atomicCompareExchangeN(addr c.rc, addr expected, desired, true, + ATOMIC_ACQ_REL, ATOMIC_RELAXED): + break + template loadRc(c): int = atomicLoadN(addr c.rc, ATOMIC_ACQUIRE) + template trialDec(c) = + discard atomicFetchAdd(addr c.rc, -rcIncrement, ATOMIC_ACQ_REL) + template trialInc(c) = + discard atomicFetchAdd(addr c.rc, rcIncrement, ATOMIC_ACQ_REL) + template rcClearFlag(c, flag) = + block: + var expected = atomicLoadN(addr c.rc, ATOMIC_RELAXED) + while true: + let desired = expected and not flag + if atomicCompareExchangeN(addr c.rc, addr expected, desired, true, + ATOMIC_ACQ_REL, ATOMIC_RELAXED): + break + template rcSetFlag(c, flag) = + block: + var expected = atomicLoadN(addr c.rc, ATOMIC_RELAXED) + while true: + let desired = expected or flag + if atomicCompareExchangeN(addr c.rc, addr expected, desired, true, + ATOMIC_ACQ_REL, ATOMIC_RELAXED): + break +else: + template color(c): untyped = c.rc and colorMask + template setColor(c, col) = + when col == colBlack: + c.rc = c.rc and not colorMask + else: + c.rc = c.rc and not colorMask or col + template loadRc(c): int = c.rc + template trialDec(c) = c.rc = c.rc -% rcIncrement + template trialInc(c) = c.rc = c.rc +% rcIncrement + template rcClearFlag(c, flag) = c.rc = c.rc and not flag + template rcSetFlag(c, flag) = c.rc = c.rc or flag const optimizedOrc = false @@ -118,8 +133,6 @@ proc trace(s: Cell; desc: PNimTypeV2; j: var GcEnv) {.inline.} = var p = s +! sizeof(RefHeader) cast[TraceProc](desc.traceImpl)(p, addr(j)) -include threadids - type Stripe = object when not defined(yrcAtomics): @@ -137,7 +150,6 @@ type ## Invoked while holding the global lock; must not call back into YRC. var - gYrcGlobalLock: Lock roots: CellSeq[Cell] # merged roots, used under global lock stripes: array[NumStripes, Stripe] rootsThreshold: int = 128 @@ -182,13 +194,15 @@ proc nimIncRefCyclic(p: pointer; cyclic: bool) {.compilerRtl, inl.} = let h = head(p) when optimizedOrc: if cyclic: h.rc = h.rc or maybeCycle - when defined(yrcAtomics): + when defined(nimYrcAtomicIncs): + discard atomicFetchAdd(addr h.rc, rcIncrement, ATOMIC_ACQ_REL) + elif defined(yrcAtomics): let s = getStripeIdx() let slot = atomicFetchAdd(addr stripes[s].toIncLen, 1, ATOMIC_ACQ_REL) if slot < QueueSize: atomicStoreN(addr stripes[s].toInc[slot], h, ATOMIC_RELEASE) else: - withLock gYrcGlobalLock: + yrcCollectorLock: h.rc = h.rc +% rcIncrement for i in 0.. until: let (entry, desc) = j.traceStack.pop() let t = head entry[] - t.rc = t.rc +% rcIncrement + trialInc(t) if t.color != colBlack: t.setColor colBlack trace(t, desc, j) @@ -301,23 +317,23 @@ proc markGray(s: Cell; desc: PNimTypeV2; j: var GcEnv) = if s.color != colGray: s.setColor colGray j.touched = j.touched +% 1 - j.rcSum = j.rcSum +% (s.rc shr rcShift) +% 1 + j.rcSum = j.rcSum +% (loadRc(s) shr rcShift) +% 1 orcAssert(j.traceStack.len == 0, "markGray: trace stack not empty") trace(s, desc, j) while j.traceStack.len > 0: let (entry, desc) = j.traceStack.pop() let t = head entry[] - t.rc = t.rc -% rcIncrement + trialDec(t) j.edges = j.edges +% 1 if t.color != colGray: t.setColor colGray j.touched = j.touched +% 1 - j.rcSum = j.rcSum +% (t.rc shr rcShift) +% 2 + j.rcSum = j.rcSum +% (loadRc(t) shr rcShift) +% 2 trace(t, desc, j) proc scan(s: Cell; desc: PNimTypeV2; j: var GcEnv) = if s.color == colGray: - if (s.rc shr rcShift) >= 0: + if (loadRc(s) shr rcShift) >= 0: scanBlack(s, desc, j) else: orcAssert(j.traceStack.len == 0, "scan: trace stack not empty") @@ -327,14 +343,14 @@ proc scan(s: Cell; desc: PNimTypeV2; j: var GcEnv) = let (entry, desc) = j.traceStack.pop() let t = head entry[] if t.color == colGray: - if (t.rc shr rcShift) >= 0: + if (loadRc(t) shr rcShift) >= 0: scanBlack(t, desc, j) else: t.setColor(colWhite) trace(t, desc, j) proc collectColor(s: Cell; desc: PNimTypeV2; col: int; j: var GcEnv) = - if s.color == col and (s.rc and inRootsFlag) == 0: + if s.color == col and (loadRc(s) and inRootsFlag) == 0: orcAssert(j.traceStack.len == 0, "collectWhite: trace stack not empty") s.setColor(colBlack) j.toFree.add(s, desc) @@ -343,7 +359,7 @@ proc collectColor(s: Cell; desc: PNimTypeV2; col: int; j: var GcEnv) = let (entry, desc) = j.traceStack.pop() let t = head entry[] entry[] = nil - if t.color == col and (t.rc and inRootsFlag) == 0: + if t.color == col and (loadRc(t) and inRootsFlag) == 0: j.toFree.add(t, desc) t.setColor(colBlack) trace(t, desc, j) @@ -351,6 +367,9 @@ proc collectColor(s: Cell; desc: PNimTypeV2; col: int; j: var GcEnv) = proc collectCyclesBacon(j: var GcEnv; lowMark: int) = # YRC defers all destruction to collection time - process ALL roots through Bacon's algorithm # This is different from ORC which handles immediate garbage (rc == 0) directly + if lockState == Collecting: + return + lockState = Collecting let last = roots.len -% 1 when logOrc: for i in countdown(last, lowMark): @@ -374,13 +393,10 @@ proc collectCyclesBacon(j: var GcEnv; lowMark: int) = init j.toFree for i in 0 ..< roots.len: let s = roots.d[i][0] - s.rc = s.rc and not inRootsFlag + rcClearFlag(s, inRootsFlag) collectColor(s, roots.d[i][1], colToCollect, j) # Clear roots before freeing to prevent nested collectCycles() from accessing freed cells - when not defined(nimStressOrc): - let oldThreshold = rootsThreshold - rootsThreshold = high(int) roots.len = 0 # Free all collected objects @@ -390,8 +406,6 @@ proc collectCyclesBacon(j: var GcEnv; lowMark: int) = when orcLeakDetector: writeCell("CYCLIC OBJECT FREED", s, j.toFree.d[i][1]) free(s, j.toFree.d[i][1]) - when not defined(nimStressOrc): - rootsThreshold = oldThreshold j.freed = j.freed +% j.toFree.len deinit j.toFree @@ -401,7 +415,7 @@ when defined(nimOrcStats): proc collectCycles() = when logOrc: cfprintf(cstderr, "[collectCycles] begin\n") - withLock gYrcGlobalLock: + yrcCollectorLock: mergePendingRoots() if roots.len >= RootsThreshold and mayRunCycleCollect(): var j: GcEnv @@ -436,9 +450,9 @@ when defined(nimOrcStats): result = OrcStats(freedCyclicObjects: freedCyclicObjects) proc GC_runOrc* = - withLock gYrcGlobalLock: + yrcCollectorLock: mergePendingRoots() - if mayRunCycleCollect(): + if roots.len > 0 and mayRunCycleCollect(): var j: GcEnv init j.traceStack collectCyclesBacon(j, 0) @@ -455,12 +469,12 @@ proc GC_disableOrc*() = rootsThreshold = high(int) proc GC_prepareOrc*(): int {.inline.} = - withLock gYrcGlobalLock: + yrcCollectorLock: mergePendingRoots() result = roots.len proc GC_partialCollect*(limit: int) = - withLock gYrcGlobalLock: + yrcCollectorLock: mergePendingRoots() if roots.len > limit and mayRunCycleCollect(): var j: GcEnv @@ -536,22 +550,24 @@ proc yrcDec(tmp: pointer; desc: PNimTypeV2) {.inline.} = proc nimAsgnYrc(dest: ptr pointer; src: pointer; desc: PNimTypeV2) {.compilerRtl.} = ## YRC write barrier for ref copy assignment. - ## Atomically stores src into dest, then buffers RC adjustments. - ## Freeing is always done by the cycle collector, never inline. + ## Holds the mutator read lock for the entire operation so the collector + ## cannot run between the incRef and decRef, closing the stale-decRef + ## bug. Direct atomic incRef replaces the toInc stripe queue: the + ## collector is blocked, so the RC update is immediately visible and correct. + acquireMutatorLock() + if src != nil: increment head(src) # direct atomic: no toInc queue needed let tmp = dest[] - atomicStoreN(dest, src, ATOMIC_RELEASE) - if src != nil: - nimIncRefCyclic(src, true) - if tmp != nil: - yrcDec(tmp, desc) + dest[] = src + if tmp != nil: yrcDec(tmp, desc) # still deferred via toDec for cycle detection + releaseMutatorLock() proc nimSinkYrc(dest: ptr pointer; src: pointer; desc: PNimTypeV2) {.compilerRtl.} = ## YRC write barrier for ref sink (move). No incRef on source. - ## Freeing is always done by the cycle collector, never inline. + acquireMutatorLock() let tmp = dest[] - atomicStoreN(dest, src, ATOMIC_RELEASE) - if tmp != nil: - yrcDec(tmp, desc) + dest[] = src + if tmp != nil: yrcDec(tmp, desc) + releaseMutatorLock() proc nimMarkCyclic(p: pointer) {.compilerRtl, inl.} = when optimizedOrc: @@ -559,10 +575,12 @@ proc nimMarkCyclic(p: pointer) {.compilerRtl, inl.} = let h = head(p) h.rc = h.rc or maybeCycle -# Initialize locks at module load -initLock(gYrcGlobalLock) +# Initialize locks at module load. +# RwLock stripes live in seqs_v2 (gYrcLocks); NumLockStripes is exported from there. +for i in 0.. SeqPayloads \cup {NULL}] -- current payload for obj's seq + payloadAlive, \* [SeqPayloads -> BOOLEAN] -- is this payload's memory valid? + \* RWLock read side: set of threads holding the read lock. + \* Seq mutations (assign, add, setLen, etc.) acquire the read lock. + \* The collector (write lock holder) gets exclusive access. + rwLockReaders, \* SUBSET Threads -- threads currently holding the read lock + \* Collector's in-progress seq trace: the payload pointer read during tracing. + \* Between reading the pointer and accessing the data, the payload could be freed. + collectorPayload \* SeqPayloads \cup {NULL} -- payload being traced by collector + +\* Convenience tuple for seq-related variables (used in UNCHANGED clauses) +seqVars == <> \* Type invariants TypeOK == @@ -117,6 +161,11 @@ TypeOK == /\ mergedRoots \in Seq([obj: Objects, desc: ObjTypes]) /\ collecting \in BOOLEAN /\ pendingWrites \in SUBSET ([thread: Threads, dest: Objects, old: Objects \cup {NULL}, src: Objects \cup {NULL}, phase: {"store", "inc", "dec"}]) + \* Seq payload types + /\ seqData \in [Objects -> SeqPayloads \cup {NULL}] + /\ payloadAlive \in [SeqPayloads -> BOOLEAN] + /\ rwLockReaders \in SUBSET Threads + /\ collectorPayload \in SeqPayloads \cup {NULL} \* Helper: internal reference count (heap-to-heap edges) InternalRC(obj) == @@ -163,7 +212,7 @@ MutatorWriteAtomicStore(thread, destObj, destField, oldVal, newVal, desc) == IF x = newVal /\ newVal # NULL THEN TRUE ELSE FALSE]] - /\ UNCHANGED <> + /\ UNCHANGED <> \* ============================================================================ \* Phase 2: RC Buffering (if space available) @@ -193,7 +242,7 @@ WriteBarrier(thread, destObj, destField, oldVal, newVal, desc) == /\ toDec' = IF oldVal # NULL /\ toDecLen[stripe] < QueueSize THEN [toDec EXCEPT ![stripe] = Append(toDec[stripe], [obj |-> oldVal, desc |-> desc])] ELSE toDec - /\ UNCHANGED <> + /\ UNCHANGED <> \* ============================================================================ \* Phase 3: Overflow Handling (separate actions that can block) @@ -215,7 +264,7 @@ MutatorWriteMergeInc(thread) == externalRC == Cardinality({t \in Threads : roots[t][x]}) IN internalRC + externalRC] /\ globalLock' = NULL \* Release lock after merge - /\ UNCHANGED <> + /\ UNCHANGED <> \* Handle decrement overflow: merge ALL buffers when lock is available \* This calls collectCycles() which merges both increment and decrement buffers @@ -257,7 +306,7 @@ MutatorWriteMergeDec(thread) == /\ toDecLen' = [s \in 0..(NumStripes-1) |-> 0] /\ toDec' = [s \in 0..(NumStripes-1) |-> <<>>] /\ globalLock' = NULL \* Lock acquired, merge done, lock released (entire withLock block is atomic) - /\ UNCHANGED <> + /\ UNCHANGED <> \* ============================================================================ \* Merge Operation: mergePendingRoots @@ -311,7 +360,7 @@ MergePendingRoots == /\ toInc' = [s \in 0..(NumStripes-1) |-> <<>>] /\ toDecLen' = [s \in 0..(NumStripes-1) |-> 0] /\ toDec' = [s \in 0..(NumStripes-1) |-> <<>>] - /\ UNCHANGED <> + /\ UNCHANGED <> \* ============================================================================ \* Trial Deletion: markGray @@ -365,7 +414,7 @@ MarkGray(obj, desc) == \* For roots, the RC includes external refs which survive trial deletion. rc' = [x \in Objects |-> IF x \in allReachable THEN rc[x] - internalEdgeCount[x] ELSE rc[x]] - /\ UNCHANGED <> + /\ UNCHANGED <> \* ============================================================================ \* Scan Phase @@ -422,7 +471,7 @@ Scan(obj, desc) == ELSE \* Mark white (part of closed cycle) /\ color' = [color EXCEPT ![obj] = colWhite] /\ UNCHANGED <> - /\ UNCHANGED <> + /\ UNCHANGED <> \* ============================================================================ \* Collection Phase: collectColor @@ -442,7 +491,7 @@ CollectColor(obj, desc, targetColor) == edges' = [edges EXCEPT ![obj] = [x \in Objects |-> IF x = obj THEN FALSE ELSE edges[obj][x]]] /\ color' = [color EXCEPT ![obj] = colBlack] \* Mark as freed - /\ UNCHANGED <> + /\ UNCHANGED <> \* ============================================================================ \* Collection Cycle: collectCyclesBacon @@ -454,7 +503,7 @@ StartCollection == /\ Len(mergedRoots) >= RootsThreshold /\ collecting' = TRUE /\ gcEnv' = [touched |-> 0, edges |-> 0, rcSum |-> 0, toFree |-> {}] - /\ UNCHANGED <> + /\ UNCHANGED <> EndCollection == /\ globalLock # NULL @@ -464,7 +513,7 @@ EndCollection == IF x \in {r.obj : r \in mergedRoots} THEN FALSE ELSE inRoots[x]] /\ mergedRoots' = <<>> /\ collecting' = FALSE - /\ UNCHANGED <> + /\ UNCHANGED <> \* ============================================================================ \* Mutator Actions @@ -491,7 +540,7 @@ MutatorWrite(thread, destObj, destField, oldVal, newVal, desc) == IF incOverflow \/ decOverflow THEN \* Overflow: atomic store happened, but buffering is deferred \* Buffers stay full, merge will happen when lock is available (via MutatorWriteMergeInc/Dec) - /\ UNCHANGED <> + /\ UNCHANGED <> ELSE \* No overflow: buffer normally /\ WriteBarrier(thread, destObj, destField, oldVal, newVal, desc) /\ UNCHANGED <> @@ -521,16 +570,19 @@ MutatorRootAssign(thread, obj, val) == /\ collecting' = collecting /\ gcEnv' = gcEnv /\ pendingWrites' = pendingWrites + /\ UNCHANGED seqVars \* ============================================================================ \* Collector Actions \* ============================================================================ -\* Collector acquires global lock for entire collection cycle +\* Collector acquires write lock (global lock) for entire collection cycle. +\* RWLock semantics: writer can only acquire when no readers hold the read lock. CollectorAcquireLock(thread) == /\ globalLock = NULL + /\ rwLockReaders = {} \* RWLock: no readers allowed when acquiring write lock /\ globalLock' = thread - /\ UNCHANGED <> + /\ UNCHANGED <> CollectorMerge == /\ globalLock # NULL @@ -571,7 +623,93 @@ CollectorEnd == CollectorReleaseLock(thread) == /\ globalLock = thread /\ globalLock' = NULL - /\ UNCHANGED <> + /\ UNCHANGED <> + +\* ============================================================================ +\* Seq Payload Actions (RWLock-protected) +\* ============================================================================ +\* These actions model the race between the collector tracing seq payloads +\* and mutators replacing/freeing seq payloads. +\* +\* The collector traces seq payloads in two steps: +\* 1. CollectorStartTraceSeq: reads seqData[obj] (gets payload pointer) +\* 2. CollectorFinishTraceSeq: accesses the payload data +\* Between these steps, a mutator could free the payload (the race). +\* +\* The RWLock prevents this: +\* - Collector holds write lock (globalLock) during tracing +\* - MutatorSeqAssign requires read lock (rwLockReaders) +\* - Read lock requires globalLock = NULL +\* - Therefore MutatorSeqAssign is blocked during collection +\* +\* Note: This models the memory safety aspect of seq tracing. +\* The cycle collection algorithm (MarkGray, Scan, etc.) operates on the +\* logical edge graph. Seq payloads are a physical representation detail +\* that affects memory safety but not GC correctness (which is already +\* covered by the existing Safety property). + +\* Mutator acquires read lock for seq mutation. +\* RWLock semantics: read lock can be acquired when no writer holds the write lock. +\* Multiple readers can hold the read lock simultaneously. +MutatorAcquireSeqLock(thread) == + /\ globalLock = NULL \* RWLock: no writer allowed when acquiring read lock + /\ thread \notin rwLockReaders + /\ rwLockReaders' = rwLockReaders \cup {thread} + /\ UNCHANGED <> + +\* Mutator releases read lock after seq mutation completes. +MutatorReleaseSeqLock(thread) == + /\ thread \in rwLockReaders + /\ rwLockReaders' = rwLockReaders \ {thread} + /\ UNCHANGED <> + +\* Mutator replaces a seq field's payload (e.g., r.list = newSeq). +\* This frees the old payload and installs a new one. +\* Requires the read lock (RWLock protection against concurrent collection). +\* +\* In the real implementation, this is a value-type assignment (=sink/=copy) +\* that frees the old data array and installs a new one. The old array is freed +\* immediately, NOT deferred to the cycle collector. +MutatorSeqAssign(thread, obj, newPayload) == + /\ thread \in rwLockReaders \* Must hold read lock + /\ seqData[obj] # NULL \* Object has an existing seq payload + /\ newPayload \in SeqPayloads + /\ ~payloadAlive[newPayload] \* New payload is freshly allocated (not yet alive) + /\ LET oldPayload == seqData[obj] + IN + /\ seqData' = [seqData EXCEPT ![obj] = newPayload] + /\ payloadAlive' = [payloadAlive EXCEPT ![oldPayload] = FALSE, + ![newPayload] = TRUE] + \* Note: In a complete model, this would also update edges[obj] to reflect + \* the new seq elements and buffer RC changes (inc new elements, dec old elements). + \* We omit this here to focus on the memory safety property (payload lifetime). + /\ UNCHANGED <> + +\* Collector begins tracing an object's seq field. +\* Reads the seqData pointer and stores it in collectorPayload. +\* This is the first step of a two-step trace operation. +\* The collector must hold the write lock (globalLock). +CollectorStartTraceSeq(obj) == + /\ globalLock # NULL \* Collector holds write lock + /\ collecting = TRUE \* In collection phase + /\ seqData[obj] # NULL \* Object has a seq field + /\ collectorPayload = NULL \* Not already mid-trace + /\ collectorPayload' = seqData[obj] + /\ UNCHANGED <> + +\* Collector finishes tracing an object's seq field. +\* Accesses the payload data via collectorPayload. +\* The payload MUST still be alive (this is checked by SeqPayloadSafety). +\* After accessing the payload, clears collectorPayload. +CollectorFinishTraceSeq == + /\ globalLock # NULL \* Collector holds write lock + /\ collecting = TRUE \* In collection phase + /\ collectorPayload # NULL \* Mid-trace on a payload + \* The actual work: read payloadEdges[collectorPayload] to discover children. + \* We don't model the trace results here; the safety property ensures + \* the read is valid (payload is alive). + /\ collectorPayload' = NULL + /\ UNCHANGED <> \* ============================================================================ \* Next State Relation @@ -607,6 +745,16 @@ Next == \/ CollectorEnd \/ \E thread \in Threads: CollectorReleaseLock(thread) + \* --- Seq payload actions --- + \/ \E thread \in Threads: + MutatorAcquireSeqLock(thread) + \/ \E thread \in Threads: + MutatorReleaseSeqLock(thread) + \/ \E thread \in Threads, obj \in Objects, p \in SeqPayloads: + MutatorSeqAssign(thread, obj, p) + \/ \E obj \in Objects: + CollectorStartTraceSeq(obj) + \/ CollectorFinishTraceSeq \* ============================================================================ \* Initial State @@ -642,6 +790,11 @@ Init == /\ collecting = FALSE /\ gcEnv = [touched |-> 0, edges |-> 0, rcSum |-> 0, toFree |-> {}] /\ pendingWrites = {} + \* Seq payload initial state + /\ seqData = [x \in Objects |-> NULL] \* No seq fields initially + /\ payloadAlive = [p \in SeqPayloads |-> FALSE] \* No payloads alive initially + /\ rwLockReaders = {} \* No threads hold read lock + /\ collectorPayload = NULL \* Collector not mid-trace /\ TypeOK \* ============================================================================ @@ -748,14 +901,53 @@ CycleInvariant == THEN ExternalRC(obj) = 0 ELSE TRUE +\* ============================================================================ +\* Seq Payload Safety +\* ============================================================================ +\* Memory safety: The collector never accesses a freed seq payload. +\* +\* collectorPayload holds the payload pointer the collector read during +\* CollectorStartTraceSeq. Between that action and CollectorFinishTraceSeq, +\* the collector will dereference this pointer to read the seq's elements. +\* If the payload has been freed in between, this is a use-after-free. +\* +\* The RWLock prevents this: +\* - collectorPayload is only set when globalLock # NULL (write lock held) +\* - MutatorSeqAssign (which frees payloads) requires rwLockReaders membership +\* - MutatorAcquireSeqLock requires globalLock = NULL (no writer) +\* - Therefore: while collectorPayload # NULL, no MutatorSeqAssign can execute +\* - Therefore: payloadAlive[collectorPayload] remains TRUE +\* +\* Without the RWLock (if MutatorSeqAssign didn't require the read lock), +\* the following interleaving would violate this property: +\* 1. Collector acquires write lock +\* 2. CollectorStartTraceSeq(obj) -- collectorPayload = P +\* 3. MutatorSeqAssign(thread, obj, Q) -- frees P, payloadAlive[P] = FALSE +\* 4. SeqPayloadSafety VIOLATED: collectorPayload = P but payloadAlive[P] = FALSE + +SeqPayloadSafety == + collectorPayload # NULL => payloadAlive[collectorPayload] + +\* ============================================================================ +\* RWLock Invariant +\* ============================================================================ +\* The read-write lock ensures mutual exclusion between the collector (writer) +\* and seq mutations (readers). The writer and readers are never active at +\* the same time. + +RWLockInvariant == + globalLock # NULL => rwLockReaders = {} + \* ============================================================================ \* Specification \* ============================================================================ -Spec == Init /\ [][Next]_<> +Spec == Init /\ [][Next]_<> THEOREM Spec => []Safety THEOREM Spec => []RCInvariant THEOREM Spec => []CycleInvariant +THEOREM Spec => []SeqPayloadSafety +THEOREM Spec => []RWLockInvariant ==== diff --git a/tests/arc/torcbench.nim b/tests/arc/torcbench.nim index 4c9e65feea..e54537883a 100644 --- a/tests/arc/torcbench.nim +++ b/tests/arc/torcbench.nim @@ -35,4 +35,5 @@ proc main() = main() GC_fullCollect() -echo getOccupiedMem() < 10 * 1024 * 1024, " peak memory: ", getMaxMem() < 10 * 1024 * 1024 +when not defined(useMalloc): + echo getOccupiedMem() < 10 * 1024 * 1024, " peak memory: ", getMaxMem() < 10 * 1024 * 1024 diff --git a/tests/codegen/titaniummangle_nim.nim b/tests/codegen/titaniummangle_nim.nim index 204d6ac063..26953166e1 100644 --- a/tests/codegen/titaniummangle_nim.nim +++ b/tests/codegen/titaniummangle_nim.nim @@ -1,35 +1,7 @@ discard """ targets: "c" matrix: "--debugger:native --mangle:nim" - ccodecheck: "'testFunc__titaniummangle95nim_u1316'" - ccodecheck: "'testFunc__titaniummangle95nim_u156'" - ccodecheck: "'testFunc__titaniummangle95nim_u1305'" - ccodecheck: "'testFunc__titaniummangle95nim_u241'" - ccodecheck: "'testFunc__titaniummangle95nim_u1357'" - ccodecheck: "'testFunc__titaniummangle95nim_u292'" - ccodecheck: "'testFunc__titaniummangle95nim_u38'" - ccodecheck: "'testFunc__titaniummangle95nim_u175'" - ccodecheck: "'testFunc__titaniummangle95nim_u1302'" - ccodecheck: "'testFunc__titaniummangle95nim_u1305'" - ccodecheck: "'testFunc__titaniummangle95nim_u535'" - ccodecheck: "'testFunc__titaniummangle95nim_u1294'" - ccodecheck: "'testFunc__titaniummangle95nim_u336'" - ccodecheck: "'testFunc__titaniummangle95nim_u425'" - ccodecheck: "'testFunc__titaniummangle95nim_u308'" - ccodecheck: "'testFunc__titaniummangle95nim_u129'" - ccodecheck: "'testFunc__titaniummangle95nim_u320'" - ccodecheck: "'testFunc__titaniummangle95nim_u223'" - ccodecheck: "'testFunc__titaniummangle95nim_u545'" - ccodecheck: "'testFunc__titaniummangle95nim_u543'" - ccodecheck: "'testFunc__titaniummangle95nim_u895'" - ccodecheck: "'testFunc__titaniummangle95nim_u1104'" - ccodecheck: "'testFunc__titaniummangle95nim_u1155'" - ccodecheck: "'testFunc__titaniummangle95nim_u636'" - ccodecheck: "'testFunc__titaniummangle95nim_u705'" - ccodecheck: "'testFunc__titaniummangle95nim_u800'" - ccodecheck: "'new__titaniummangle95nim_u1320'" - ccodecheck: "'xxx__titaniummangle95nim_u1391'" - ccodecheck: "'xxx__titaniummangle95nim_u1394'" + ccodecheck: "'testFunc__titaniummangle95nim_u'" """ #When debugging this notice that if one check fails, it can be due to any of the above. @@ -48,7 +20,7 @@ type Container[T] = object data: T - + Container2[T, T2] = object data: T data2: T2 @@ -57,7 +29,7 @@ type Coo = Foo - Doo = Boo | Foo + Doo = Boo | Foo TestProc = proc(a:string): string @@ -67,87 +39,87 @@ type EnumSample = enum type EnumAnotherSample = enum a, b, c -proc testFunc(a: set[EnumSample]) = +proc testFunc(a: set[EnumSample]) = echo $a -proc testFunc(a: typedesc) = +proc testFunc(a: typedesc) = echo $a -proc testFunc(a: ptr Foo) = +proc testFunc(a: ptr Foo) = echo repr a -proc testFunc(s: string, a: Coo) = +proc testFunc(s: string, a: Coo) = echo repr a -proc testFunc(s: int, a: Comparable) = +proc testFunc(s: int, a: Comparable) = echo repr a -proc testFunc(a: TestProc) = +proc testFunc(a: TestProc) = let b = "" echo repr a("") -proc testFunc(a: ref Foo) = +proc testFunc(a: ref Foo) = echo repr a -proc testFunc(b: Boo) = +proc testFunc(b: Boo) = echo repr b -proc testFunc(a: ptr UncheckedArray[int]) = +proc testFunc(a: ptr UncheckedArray[int]) = echo repr a -proc testFunc(a: ptr int) = +proc testFunc(a: ptr int) = echo repr a -proc testFunc(a: ptr ptr int) = +proc testFunc(a: ptr ptr int) = echo repr a -proc testFunc(e: FooTuple, str: cstring) = +proc testFunc(e: FooTuple, str: cstring) = echo e -proc testFunc(e: (float, float)) = +proc testFunc(e: (float, float)) = echo e -proc testFunc(e: EnumSample) = +proc testFunc(e: EnumSample) = echo e -proc testFunc(e: var int) = +proc testFunc(e: var int) = echo e -proc testFunc(e: var Foo, a, b: int32, refFoo: ref Foo) = +proc testFunc(e: var Foo, a, b: int32, refFoo: ref Foo) = echo e -proc testFunc(xs: Container[int]) = +proc testFunc(xs: Container[int]) = let a = 2 echo xs -proc testFunc(xs: Container2[int32, int32]) = +proc testFunc(xs: Container2[int32, int32]) = let a = 2 echo xs -proc testFunc(xs: Container[Container2[int32, int32]]) = +proc testFunc(xs: Container[Container2[int32, int32]]) = let a = 2 echo xs -proc testFunc(xs: seq[int]) = +proc testFunc(xs: seq[int]) = let a = 2 echo xs -proc testFunc(xs: openArray[string]) = +proc testFunc(xs: openArray[string]) = let a = 2 echo xs -proc testFunc(xs: array[2, int]) = +proc testFunc(xs: array[2, int]) = let a = 2 echo xs -proc testFunc(e: EnumAnotherSample) = +proc testFunc(e: EnumAnotherSample) = echo e -proc testFunc(a, b: int) = +proc testFunc(a, b: int) = echo "hola" discard -proc testFunc(a: int, xs: varargs[string]) = +proc testFunc(a: int, xs: varargs[string]) = let a = 10 for x in xs: echo x @@ -155,7 +127,7 @@ proc testFunc(a: int, xs: varargs[string]) = proc xxx(v: static int) = echo v -proc testFunc() = +proc testFunc() = var a = 2 var aPtr = a.addr var foo = Foo() From e58acc2e1e98f62637c47769e6a31cea1c392b9d Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:40:31 +0800 Subject: [PATCH 5/8] fixes #25005; new doesn't work with ref object (#25532) fixes #25005 In `semTypeIdent`, when resolving a typedesc parameter inside a generic instantiation, the code took a shortcut: it returned the symbol of the element type (`bound = result.typ.elementType.sym`). However, for generic types like `RpcResponse[T] = ref object`, the instantiated object type (e.g., `RpcResponse:ObjectType[string]`) is a copy with a new type ID but still points to the same symbol as the uninstantiated generic body type. That symbol's .typ refers to the original uninstantiated type, which still contains unresolved generic params `T` --- compiler/semtypes.nim | 4 +++- tests/generics/tgenerics_issues.nim | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/compiler/semtypes.nim b/compiler/semtypes.nim index c6ed5e77dc..1a4bd24965 100644 --- a/compiler/semtypes.nim +++ b/compiler/semtypes.nim @@ -2049,7 +2049,9 @@ proc semTypeIdent(c: PContext, n: PNode): PSym = # proc signature for example if c.inGenericInst > 0: let bound = result.typ.elementType.sym - if bound != nil: return bound + # the symbol may still point to the uninstantiated generic body type + if bound != nil and bound.typ == result.typ.elementType: + return bound return result if result.typ.sym == nil: localError(c.config, n.info, errTypeExpected) diff --git a/tests/generics/tgenerics_issues.nim b/tests/generics/tgenerics_issues.nim index da202874e1..e865c8f7be 100644 --- a/tests/generics/tgenerics_issues.nim +++ b/tests/generics/tgenerics_issues.nim @@ -902,3 +902,15 @@ block: # issue #25494 a, b, c foo[MyEnum]() + +block: # issue #25005 + type + RpcResponse[T] = ref object + result: T + + func testit[T](p: var ref T) = + p = new(T) + + var v: RpcResponse[string] + testit(v) + From 86b9245dd69b1ea7ba9a4f73fd3651808052ca6c Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Tue, 24 Feb 2026 03:37:46 -0500 Subject: [PATCH 6/8] fix: double check inputIndex in base64.decode (#25531) fixes https://github.com/nim-lang/Nim/issues/25530 this double checks the index to make sure whitespace related index increments cannot cause index defect error --- lib/pure/base64.nim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pure/base64.nim b/lib/pure/base64.nim index 3b8fb9b681..f238862508 100644 --- a/lib/pure/base64.nim +++ b/lib/pure/base64.nim @@ -255,6 +255,9 @@ proc decode*(s: string): string = while inputIndex <= inputEnds: while s[inputIndex] in {'\n', '\r', ' '}: inc inputIndex + # double check inputIndex as it can be incremented due to whitespace + if inputIndex > inputEnds: + break inputChar(a) inputChar(b) inputChar(c) From fb80f7707db7ef1c2138230eb74a816aac97d2db Mon Sep 17 00:00:00 2001 From: Tomohiro Date: Tue, 24 Feb 2026 17:39:06 +0900 Subject: [PATCH 7/8] fixes #16754 (#25519) This PR allows passing the defining type to generic types in the right side in a type definition like this: ```nim type Foo = object x: Option[Foo] ``` I think generic types should be instanciated after all given arguments are semchecked, because generic types can access information about them. (for example, `Option[T]` in std/option checks if `T` is a pointer like type) But in this case, need to instanciate `Option[Foo]` before type of `Foo.x` is determined. --- compiler/astdef.nim | 6 ++++- compiler/semtypes.nim | 26 ++++++++++++++++++- tests/generics/tself_type.nim | 48 +++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 tests/generics/tself_type.nim diff --git a/compiler/astdef.nim b/compiler/astdef.nim index b9a8aab3e1..242cdf3ef6 100644 --- a/compiler/astdef.nim +++ b/compiler/astdef.nim @@ -202,7 +202,11 @@ type tySequence, tyProc, tyPointer, tyOpenArray, - tyString, tyCstring, tyForward, + tyString, tyCstring, + tyForward, + # a type not yet semchecked + # When semcheck a type section, all types defined in it are initialized to tyForward + tyInt, tyInt8, tyInt16, tyInt32, tyInt64, # signed integers tyFloat, tyFloat32, tyFloat64, tyFloat128, tyUInt, tyUInt8, tyUInt16, tyUInt32, tyUInt64, diff --git a/compiler/semtypes.nim b/compiler/semtypes.nim index 1a4bd24965..94435886a9 100644 --- a/compiler/semtypes.nim +++ b/compiler/semtypes.nim @@ -1739,6 +1739,7 @@ proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType = var isConcrete = true let rType = m.call[0].typ let mIndex = if rType != nil: rType.len - 1 else: -1 + var hasForwardTypeParam = false for i in 1.. Date: Tue, 24 Feb 2026 16:40:21 +0800 Subject: [PATCH 8/8] enable `--warning:ImplicitRangeConversion` (#25477) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- changelog.md | 2 +- compiler/lineinfos.nim | 4 +++- compiler/nim.cfg | 4 ++++ compiler/sempass2.nim | 18 +++++++++++++++--- doc/manual.md | 2 ++ tests/range/timplicitrangedownsizing.nim | 7 ++++++- tests/range/timplicitrangedownsizing2.nim | 11 +++++++++++ 7 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 tests/range/timplicitrangedownsizing2.nim diff --git a/changelog.md b/changelog.md index 8d59320672..665346ad70 100644 --- a/changelog.md +++ b/changelog.md @@ -33,7 +33,7 @@ errors. - Bitshift operators (`shl`, `shr`, `ashr`) now apply bitmasking to the right operand in the C/C++/VM/JS backends. -- Adds a new warning enabled by `--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 are not warned on. +- 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`. ## Standard library additions and changes diff --git a/compiler/lineinfos.nim b/compiler/lineinfos.nim index d9d44f277d..dc8708c3e4 100644 --- a/compiler/lineinfos.nim +++ b/compiler/lineinfos.nim @@ -99,6 +99,7 @@ type warnUser = "User", warnGlobalVarConstructorTemporary = "GlobalVarConstructorTemporary", warnImplicitRangeConversion = "ImplicitRangeConversion", + warnSystemRangeConversion = "SystemRangeConversion", # hints hintSuccess = "Success", hintSuccessX = "SuccessX", hintCC = "CC", @@ -208,6 +209,7 @@ const warnUser: "$1", warnGlobalVarConstructorTemporary: "global variable '$1' initialization requires a temporary variable", warnImplicitRangeConversion: "implicit range conversion $1", + warnSystemRangeConversion: "implicit range conversion $1", hintSuccess: "operation successful: $#", # keep in sync with `testament.isSuccess` hintSuccessX: "$build\n$loc lines; ${sec}s; $mem; proj: $project; out: $output", @@ -262,7 +264,7 @@ type proc computeNotesVerbosity(): array[0..3, TNoteKinds] = result = default(array[0..3, TNoteKinds]) - result[3] = {low(TNoteKind)..high(TNoteKind)} - {warnObservableStores, warnResultUsed, warnAnyEnumConv, warnBareExcept, warnStdPrefix, warnImplicitRangeConversion} + result[3] = {low(TNoteKind)..high(TNoteKind)} - {warnObservableStores, warnResultUsed, warnAnyEnumConv, warnBareExcept, warnStdPrefix, warnSystemRangeConversion} result[2] = result[3] - {hintStackTrace, hintExtendedContext, hintDeclaredLoc, hintProcessingStmt} result[1] = result[2] - {warnProveField, warnProveIndex, warnGcUnsafe, hintPath, hintDependency, hintCodeBegin, hintCodeEnd, diff --git a/compiler/nim.cfg b/compiler/nim.cfg index 9dab29eeed..425f0df324 100644 --- a/compiler/nim.cfg +++ b/compiler/nim.cfg @@ -65,3 +65,7 @@ define:useStdoutAsStdmsg @if nimHasVtables: experimental:vtables @end + +@if nimHasImplicitRangeConversion: + warning[ImplicitRangeConversion]:off +@end diff --git a/compiler/sempass2.nim b/compiler/sempass2.nim index c206b7f075..c1279f3754 100644 --- a/compiler/sempass2.nim +++ b/compiler/sempass2.nim @@ -168,7 +168,7 @@ proc isRangeSupertype(conf: ConfigRef; wider, narrower: PType): bool = # int -> float ranges; warn result = false -proc shouldWarnRangeConversion(conf: ConfigRef; formalType, argType: PType): bool = +proc shouldWarnRangeConversion(conf: ConfigRef; info: TLineInfo; formalType, argType: PType): bool = ## Determine if an implicit range conversion should warn ## We warn on conversions that are likely to cause panics let f = formalType.skipTypes({tyGenericInst, tyAlias, tySink, tyDistinct}) @@ -176,7 +176,19 @@ proc shouldWarnRangeConversion(conf: ConfigRef; formalType, argType: PType): boo if f.kind == tyRange: # Only warn if formal range doesn't fully contain argument range # Check if the ranges don't perfectly overlap - result = not isRangeSupertype(conf, f, a) + if a.kind == tyInt and f.sym != nil and f.sym.owner != nil and + sfSystemModule in f.sym.owner.flags and + (f.sym.name.s == "Positive" or + f.sym.name.s == "Natural"): + # Positive and Natural are special cases that we do not warn on with + # ImplicitRangeConversion, but may warn on with systemRangeConversion + # if that warning is enabled. + if conf.hasWarn(warnSystemRangeConversion): + message(conf, info, warnSystemRangeConversion, + typeToString(argType) & " -> " & typeToString(formalType)) + result = false + else: + result = not isRangeSupertype(conf, f, a) else: result = false @@ -1538,7 +1550,7 @@ proc track(tracked: PEffects, n: PNode) = # Check for implicit range conversions if n.kind == nkHiddenStdConv and (not tracked.isArrayIndexing) and - shouldWarnRangeConversion(tracked.config, n.typ, n[1].typ): + shouldWarnRangeConversion(tracked.config, n.info, n.typ, n[1].typ): message(tracked.config, n.info, warnImplicitRangeConversion, typeToString(n[1].typ) & " -> " & typeToString(n.typ)) diff --git a/doc/manual.md b/doc/manual.md index f52e0ba38c..b0de4f58bb 100644 --- a/doc/manual.md +++ b/doc/manual.md @@ -1144,6 +1144,8 @@ semantic analysis). Assignments from the base type to one of its subrange types A subrange type has the same size as its base type (`int` in the Subrange example). +Implicit "downsizing" conversions to range types (for example, `int -> range[0..255]` or `range[1..256] -> range[0..255]`) emit the `ImplicitRangeConversion` warning. Conversions that are clearly safe (for example, `range[0..255] -> range[0..65535]`) and any explicit casts do not trigger this warning. Conversions from `int` to common subranges such as `Natural` or `Positive` do not trigger this warning by default, but can be enabled with `--warning:systemRangeConversion`. + Pre-defined floating-point types -------------------------------- diff --git a/tests/range/timplicitrangedownsizing.nim b/tests/range/timplicitrangedownsizing.nim index 1b10f2f322..930d91bb11 100644 --- a/tests/range/timplicitrangedownsizing.nim +++ b/tests/range/timplicitrangedownsizing.nim @@ -70,4 +70,9 @@ var smallFloatRange: SmallFloat = SmallFloat(5.0) acceptWideFloat(smallFloatRange) # OK - SmallFloat (0.0..10.0) fits in WideFloatRange (0.0..100.0) var wf: WideFloatRange -wf = smallFloatRange # OK - SmallFloat range fits in WideFloatRange \ No newline at end of file +wf = smallFloatRange # OK - SmallFloat range fits in WideFloatRange + +proc foo(x: Natural) = + discard + +foo(12) \ No newline at end of file diff --git a/tests/range/timplicitrangedownsizing2.nim b/tests/range/timplicitrangedownsizing2.nim new file mode 100644 index 0000000000..679fad60be --- /dev/null +++ b/tests/range/timplicitrangedownsizing2.nim @@ -0,0 +1,11 @@ +discard """ + matrix: "--warning:systemRangeConversion --warningaserror:systemRangeConversion" + action: "reject" + errormsg: "implicit range conversion int literal(12) -> Natural" +""" + + +proc foo(x: Natural) = + discard + +foo(12) \ No newline at end of file