Merge branch 'devel' into pr_renege

This commit is contained in:
ringabout
2026-02-24 16:44:55 +08:00
committed by GitHub
47 changed files with 971 additions and 412 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,3 +65,7 @@ define:useStdoutAsStdmsg
@if nimHasVtables:
experimental:vtables
@end
@if nimHasImplicitRangeConversion:
warning[ImplicitRangeConversion]:off
@end

View File

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

View File

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

View File

@@ -1745,6 +1745,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..<m.call.len:
var typ = m.call[i].typ
# is this a 'typedesc' *parameter*? If so, use the typedesc type,
@@ -1761,13 +1762,36 @@ proc semGeneric(c: PContext, n: PNode, s: PSym, prev: PType): PType =
skip = false
addToResult(typ, skip)
if typ.kind == tyForward:
hasForwardTypeParam = true
if isConcrete:
if s.ast == nil and s.typ.kind != tyCompositeTypeClass:
# XXX: What kind of error is this? is it still relevant?
localError(c.config, n.info, errCannotInstantiateX % s.name.s)
result = newOrPrevType(tyError, prev, c)
elif containsGenericInvocationWithForward(n[0]):
elif containsGenericInvocationWithForward(n[0]) or hasForwardTypeParam:
# isConcrete == false means this generic type is not instanciated here because it invoked with generic parameters.
# Even if isConcrete == true, don't instanciate it now if there are any `tyForward` type params.
# Such `tyForward` type params will be semchecked later and we can instanciate this next time.
# Some generic types like std/options.Option[T] needs a type kinds of the given type argument.
# return `tyForward` instead of `tyGenericInvocation` because:
# ```nim
# type Foo = object
# x: Option[Foo]
# ```
# returning `tyGenericInvocation` makes `Option[Foo]` to `tyGenericInvocation` and
# next time `semGeneric` is called with `Option[Foo]`, containsGenericType(typeof(`Foo`)) == true
# and `isConcrete == false`.
if prev == nil:
result = newTypeS(tyForward, c)
result.sym = s
else:
assignType(result, newTypeS(tyForward, c))
result.sym = s
c.forwardTypeUpdates.add (result, n) #fixes 1500
return
else:
result = instGenericContainer(c, n.info, result,
allowMetaTypes = false)
@@ -2055,7 +2079,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)

View File

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

View File

@@ -12,9 +12,9 @@
const
# examples of possible values for repos: Head, ea82b54
NimbleStableCommit = "9207e8b2bbdf66b5a4d1020214cff44d2d30df92" # 0.20.1
AtlasStableCommit = "2aa62121b40d580aa2fb27920a37b938d36c5f57" # 0.9.4
AtlasStableCommit = "ff1f4289482dce94ba9f95b3b0ae16d16e21eb3d" # 0.10.1
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.
@@ -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.}
@@ -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
@@ -1664,7 +1665,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 +1777,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 +1884,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 +1893,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 +1903,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 +1924,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 +2067,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 +2286,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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: [].} =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.} =

View File

@@ -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`.
##

View File

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

View File

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

143
lib/system/rwlocks.nim Normal file
View File

@@ -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: "<synchapi.h>", final, pure, byref.} = object
p: pointer
proc initializeSRWLock(L: var RwLock) {.importc: "InitializeSRWLock",
header: "<synchapi.h>".}
proc acquireSRWLockShared(L: var RwLock) {.importc: "AcquireSRWLockShared",
header: "<synchapi.h>".}
proc releaseSRWLockShared(L: var RwLock) {.importc: "ReleaseSRWLockShared",
header: "<synchapi.h>".}
proc acquireSRWLockExclusive(L: var RwLock) {.importc: "AcquireSRWLockExclusive",
header: "<synchapi.h>".}
proc releaseSRWLockExclusive(L: var RwLock) {.importc: "ReleaseSRWLockExclusive",
header: "<synchapi.h>".}
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 <sys/types.h>
#include <pthread.h>""", 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: "<pthread.h>", noSideEffect.}
proc pthread_rwlock_destroy(rwlock: var SysRwLockObj): cint {.
importc: "pthread_rwlock_destroy", header: "<pthread.h>", noSideEffect.}
proc pthread_rwlock_rdlock(rwlock: var SysRwLockObj): cint {.
importc: "pthread_rwlock_rdlock", header: "<pthread.h>", noSideEffect.}
proc pthread_rwlock_wrlock(rwlock: var SysRwLockObj): cint {.
importc: "pthread_rwlock_wrlock", header: "<pthread.h>", noSideEffect.}
proc pthread_rwlock_unlock(rwlock: var SysRwLockObj): cint {.
importc: "pthread_rwlock_unlock", header: "<pthread.h>", 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: "<pthread.h>".} = object
const PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP = cint(3)
proc pthread_rwlockattr_init(attr: ptr SysRwLockAttr): cint {.
importc: "pthread_rwlockattr_init", header: "<pthread.h>".}
proc pthread_rwlockattr_destroy(attr: ptr SysRwLockAttr): cint {.
importc: "pthread_rwlockattr_destroy", header: "<pthread.h>".}
proc pthread_rwlockattr_setkind_np(attr: ptr SysRwLockAttr; pref: cint): cint {.
importc: "pthread_rwlockattr_setkind_np", header: "<pthread.h>".}
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: "<stdlib.h>".}
proc c_free(p: pointer) {.importc: "free", header: "<stdlib.h>".}
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: "<stdlib.h>".}
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.}

View File

@@ -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..<NumLockStripes:
acquireWrite(gYrcLocks[yrcI].lock)
lockState = HasCollectorLock
try:
body
finally:
if hadToAcquire:
for yrcI in 0..<NumLockStripes:
releaseWrite(gYrcLocks[yrcI].lock)
lockState = prevState
else:
template yrcMutatorLock*(t: typedesc; body: untyped) =
body
template yrcMutatorLockUntyped(body: untyped) =
body
# Some optimizations here may be not to empty-seq-initialize some symbols, then StrictNotNil complains.
{.push warning[StrictNotNil]: off.} # See https://github.com/nim-lang/Nim/issues/21401
@@ -116,33 +200,35 @@ proc prepareSeqAddUninit(len: int; p: pointer; addlen, elemSize, elemAlign: int)
q.cap = newCap
result = q
proc shrink*[T](x: var seq[T]; newLen: Natural) {.tags: [], raises: [].} =
proc shrink*[T](x: var seq[T]; newLen: Natural) {.tags: [], raises: [], noSideEffect.} =
when nimvm:
{.cast(tags: []).}:
setLen(x, newLen)
else:
#sysAssert newLen <= x.len, "invalid newLen parameter for 'shrink'"
when not supportsCopyMem(T):
for i in countdown(x.len - 1, newLen):
reset x[i]
# XXX This is wrong for const seqs that were moved into 'x'!
{.noSideEffect.}:
cast[ptr NimSeqV2[T]](addr x).len = newLen
yrcMutatorLock(T):
when not supportsCopyMem(T):
for i in countdown(x.len - 1, newLen):
reset x[i]
# XXX This is wrong for const seqs that were moved into 'x'!
{.noSideEffect.}:
cast[ptr NimSeqV2[T]](addr x).len = newLen
proc grow*[T](x: var seq[T]; newLen: Natural; value: T) {.nodestroy.} =
let oldLen = x.len
#sysAssert newLen >= 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..<newlen:
xu.p.data[i] = default(T)
yrcMutatorLock(T):
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..<newlen:
xu.p.data[i] = default(T)
proc newSeq[T](s: var seq[T], len: Natural) =
shrink(s, 0)
@@ -214,11 +302,12 @@ func setLenUninit[T](s: var seq[T], newlen: Natural) {.nodestroy.} =
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
yrcMutatorLock(T):
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
{.pop.} # See https://github.com/nim-lang/Nim/issues/21401

View File

@@ -18,7 +18,8 @@ type
template frees(s: NimSeqV2Reimpl) =
if s.p != nil and (s.p.cap and strlitFlag) != strlitFlag:
when compileOption("threads"):
deallocShared(s.p)
else:
dealloc(s.p)
yrcMutatorLockUntyped:
when compileOption("threads"):
deallocShared(s.p)
else:
dealloc(s.p)

View File

@@ -1,30 +1,29 @@
#
# YRC: Thread-safe ORC (concurrent cycle collector).
# Same API as orc.nim but with striped queues and global lock for merge/collect.
# Same API as orc.nim but with the global mutator/collector RWLock for safety.
# Destructors for refs run at collection time, not immediately on last decRef.
# See yrc_proof.lean for a Lean 4 proof of safety and deadlock freedom.
#
# ## Key Invariant: Topology vs. Reference Counts
# ## Locking Protocol
#
# Only `obj.field = x` can change the topology of the heap graph (heap-to-heap
# edges). Local variable assignments (`var local = someRef`) affect reference
# counts but never create heap-to-heap edges and thus cannot create cycles.
# ALL topology-changing operations — heap-field writes (`nimAsgnYrc`,
# `nimSinkYrc`) and seq mutations that resize internal buffers — hold the
# global mutator read lock (`gYrcGlobalLock` via `acquireMutatorLock`).
# Multiple mutators may hold this read lock simultaneously.
#
# The actual pointer write in `obj.field = x` happens immediately and lock-free —
# the graph topology is always up-to-date in memory. Only the RC adjustments are
# deferred: increments and decrements are buffered into per-stripe queues
# (`toInc`, `toDec`) protected by fine-grained per-stripe locks.
# The cycle collector acquires the exclusive write lock for the entire
# mark/scan/collect phase. This means the heap topology is *completely
# frozen* during collection: no `nimAsgnYrc` or seq operation can mutate
# any pointer field while the three passes run. This gives the Bacon
# algorithm the stable subgraph it requires without full write barriers.
#
# When `collectCycles` runs it takes the global lock, drains all stripe buffers
# via `mergePendingRoots`, and then traces the physical pointer graph (via
# `traceImpl`) to detect cycles. This is sound because `trace` follows the actual
# pointer values in memory — which are always current — and uses the reconciled
# RCs only to identify candidate roots and confirm garbage.
#
# In summary: the physical pointer graph is always consistent (writes are
# immediate); only the reference counts are eventually consistent (writes are
# buffered). The per-stripe locks are cheap; the expensive global lock is only
# needed when interpreting the RCs during collection.
# Consequence for incRef in `nimAsgnYrc`:
# Because the collector is blocked, the incRef can be a direct atomic
# increment on the RefHeader (`increment head(src)`) rather than going
# through the `toInc` stripe queue. The collector will see the updated
# RC immediately when it next acquires the write lock. Only decrements
# (`yrcDec`) still use the `toDec` stripe queue so that objects whose RC
# might reach zero are handled by the collector's cycle-detection logic.
#
# ## Why No Write Barrier Is Needed
#
@@ -35,40 +34,19 @@
# while A still points to it. Traditional concurrent collectors need write
# barriers to prevent this.
#
# This problem structurally cannot arise in YRC because 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, which means A has an external reference
# (from the stack) that is not a heap-to-heap edge. During trial deletion
# (`markGray`) only internal edges are subtracted from RCs, so A's external
# reference survives, `scan` finds A's RC >= 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: [].}
@@ -90,15 +68,52 @@ 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) =
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):
@@ -131,13 +144,12 @@ 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.
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..<NumStripes:
let len = atomicExchangeN(addr stripes[i].toIncLen, 0, ATOMIC_ACQUIRE)
@@ -206,7 +220,7 @@ proc nimIncRefCyclic(p: pointer; cyclic: bool) {.compilerRtl, inl.} =
else:
overflow = true
if overflow:
withLock gYrcGlobalLock:
yrcCollectorLock:
for i in 0..<NumStripes:
withLock stripes[i].lockInc:
for j in 0..<stripes[i].toIncLen:
@@ -221,23 +235,25 @@ proc mergePendingRoots() =
# we don't need to set color to black on incRef because collection runs
# under the global lock, so no concurrent mutations happen during collection.
for i in 0..<NumStripes:
when defined(yrcAtomics):
let incLen = atomicExchangeN(addr stripes[i].toIncLen, 0, ATOMIC_ACQUIRE)
for j in 0..<min(incLen, QueueSize):
let x = atomicLoadN(addr stripes[i].toInc[j], ATOMIC_ACQUIRE)
x.rc = x.rc +% rcIncrement
else:
withLock stripes[i].lockInc:
for j in 0..<stripes[i].toIncLen:
let x = stripes[i].toInc[j]
when not defined(nimYrcAtomicIncs):
# Inc buffers only exist when increfs are buffered (not atomic)
when defined(yrcAtomics):
let incLen = atomicExchangeN(addr stripes[i].toIncLen, 0, ATOMIC_ACQUIRE)
for j in 0..<min(incLen, QueueSize):
let x = atomicLoadN(addr stripes[i].toInc[j], ATOMIC_ACQUIRE)
x.rc = x.rc +% rcIncrement
stripes[i].toIncLen = 0
else:
withLock stripes[i].lockInc:
for j in 0..<stripes[i].toIncLen:
let x = stripes[i].toInc[j]
x.rc = x.rc +% rcIncrement
stripes[i].toIncLen = 0
withLock stripes[i].lockDec:
for j in 0..<stripes[i].toDecLen:
let (c, desc) = stripes[i].toDec[j]
c.rc = c.rc -% rcIncrement
if (c.rc and inRootsFlag) == 0:
c.rc = c.rc or inRootsFlag
trialDec(c)
if (loadRc(c) and inRootsFlag) == 0:
rcSetFlag(c, inRootsFlag)
if roots.d == nil: init(roots)
add(roots, c, desc)
stripes[i].toDecLen = 0
@@ -257,8 +273,8 @@ when logOrc or orcLeakDetector:
proc free(s: Cell; desc: PNimTypeV2) {.inline.} =
when traceCollector:
cprintf("[From ] %p rc %ld color %ld\n", s, s.rc shr rcShift, s.color)
if (s.rc and inRootsFlag) == 0:
cprintf("[From ] %p rc %ld color %ld\n", s, loadRc(s) shr rcShift, s.color)
if (loadRc(s) and inRootsFlag) == 0:
let p = s +! sizeof(RefHeader)
when logOrc: writeCell("free", s, desc)
if desc.destructor != nil:
@@ -291,7 +307,7 @@ proc scanBlack(s: Cell; desc: PNimTypeV2; j: var GcEnv) =
while j.traceStack.len > 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..<NumLockStripes:
initRwLock(gYrcLocks[i].lock)
for i in 0..<NumStripes:
when not defined(yrcAtomics):
when not defined(yrcAtomics) and not defined(nimYrcAtomicIncs):
initLock(stripes[i].lockInc)
initLock(stripes[i].lockDec)

View File

@@ -42,6 +42,32 @@
\* - Mutator must hold stack ref to modify object (external ref)
\* - scanBlack follows current physical edges (rescues newly written objects)
\* - Only objects unreachable from any stack root are freed
\*
\* ## Seq Payload Race and RWLock Fix
\*
\* Value types like seq[T] (where T can form cycles) have internal heap
\* allocations (data arrays / "payloads") that are freed by value-type
\* hooks (=sink, =destroy), NOT by the cycle collector. This creates a race:
\*
\* 1. Object O has a seq field with payload P containing refs
\* 2. Collector starts tracing O -- reads payload pointer P
\* 3. Mutator does O.seq = newSeq -- frees P (value-type destructor)
\* 4. Collector dereferences P -- use-after-free!
\*
\* Fix: Change the global YRC lock to a read-write lock (RWLock).
\* - Collector acquires the WRITE lock (exclusive access during tracing)
\* - Seq mutations (assign, setLen, add, etc.) acquire the READ lock
\* - Multiple seq mutations can proceed concurrently (read lock is shared)
\* - But seq mutations block while the collector traces (write lock is exclusive)
\*
\* This prevents the race: the mutator cannot free a payload while the
\* collector is tracing it, because acquiring the read lock requires
\* the write lock to be unheld.
\*
\* Deadlock avoidance: If a seq operation triggers collectCycles() via stripe
\* overflow while already holding the read lock, it must NOT attempt to
\* acquire the write lock. Instead, it should drain the overflow buffers
\* without running the full collection cycle.
EXTENDS Naturals, Integers, Sequences, FiniteSets, TLC
@@ -53,10 +79,14 @@ ASSUME IsFiniteSet(Objects)
ASSUME IsFiniteSet(Threads)
ASSUME IsFiniteSet(ObjTypes)
\* Seq payload identifiers (models heap-allocated data arrays of seq[T])
CONSTANTS SeqPayloads
ASSUME IsFiniteSet(SeqPayloads)
\* NULL constant (represents "no thread" for locks)
\* We use a sentinel value that's guaranteed not to be in Threads or Objects
NULL == "NULL" \* String literal that won't conflict with Threads/Objects
ASSUME NULL \notin Threads /\ NULL \notin Objects
ASSUME NULL \notin Threads /\ NULL \notin Objects /\ NULL \notin SeqPayloads
\* Helper functions
\* Note: GetStripeIdx is not used, GetStripe is used instead
@@ -90,15 +120,29 @@ VARIABLES
\* Per-stripe locks
lockInc, \* lockInc[stripe] = thread holding increment lock (or NULL)
lockDec, \* lockDec[stripe] = thread holding decrement lock (or NULL)
\* Global lock
globalLock, \* thread holding global lock (or NULL)
\* Global lock (now the WRITE side of the RWLock)
globalLock, \* thread holding write lock (or NULL)
\* Merged roots array (used during collection)
mergedRoots, \* sequence of (object, type) pairs
\* Collection state
collecting, \* TRUE if collection is in progress
gcEnv, \* GC environment: {touched, edges, rcSum, toFree, ...}
\* Pending operations (for modeling atomicity)
pendingWrites \* set of pending write barrier operations
pendingWrites, \* set of pending write barrier operations
\* --- Seq payload race modeling ---
\* Seq payloads: models the heap-allocated data arrays of seq[T] fields
seqData, \* [Objects -> 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 == <<seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* 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 <<roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites>>
/\ UNCHANGED <<roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* 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 <<edges, roots, rc, color, inRoots, mergedRoots, lockInc, lockDec, globalLock, collecting, gcEnv, pendingWrites>>
/\ UNCHANGED <<edges, roots, rc, color, inRoots, mergedRoots, lockInc, lockDec, globalLock, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* 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 <<edges, roots, color, inRoots, toDecLen, toDec, lockInc, lockDec, mergedRoots, collecting, gcEnv, pendingWrites>>
/\ UNCHANGED <<edges, roots, color, inRoots, toDecLen, toDec, lockInc, lockDec, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* 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 <<edges, roots, color, lockInc, lockDec, collecting, gcEnv, pendingWrites>>
/\ UNCHANGED <<edges, roots, color, lockInc, lockDec, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* 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 <<edges, roots, color, lockInc, lockDec, globalLock, collecting, gcEnv, pendingWrites>>
/\ UNCHANGED <<edges, roots, color, lockInc, lockDec, globalLock, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* 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 <<edges, roots, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites>>
/\ UNCHANGED <<edges, roots, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* Scan Phase
@@ -422,7 +471,7 @@ Scan(obj, desc) ==
ELSE \* Mark white (part of closed cycle)
/\ color' = [color EXCEPT ![obj] = colWhite]
/\ UNCHANGED <<rc>>
/\ UNCHANGED <<edges, roots, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites>>
/\ UNCHANGED <<edges, roots, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* 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 <<roots, rc, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites>>
/\ UNCHANGED <<roots, rc, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* Collection Cycle: collectCyclesBacon
@@ -454,7 +503,7 @@ StartCollection ==
/\ Len(mergedRoots) >= RootsThreshold
/\ collecting' = TRUE
/\ gcEnv' = [touched |-> 0, edges |-> 0, rcSum |-> 0, toFree |-> {}]
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, pendingWrites>>
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
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 <<edges, roots, rc, color, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, gcEnv, pendingWrites>>
/\ UNCHANGED <<edges, roots, rc, color, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* 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 <<roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites>>
/\ UNCHANGED <<roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
ELSE \* No overflow: buffer normally
/\ WriteBarrier(thread, destObj, destField, oldVal, newVal, desc)
/\ UNCHANGED <<roots, collecting, pendingWrites>>
@@ -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 <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, mergedRoots, collecting, gcEnv, pendingWrites>>
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
CollectorMerge ==
/\ globalLock # NULL
@@ -571,7 +623,93 @@ CollectorEnd ==
CollectorReleaseLock(thread) ==
/\ globalLock = thread
/\ globalLock' = NULL
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, mergedRoots, collecting, gcEnv, pendingWrites>>
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
\* ============================================================================
\* 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 <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, collectorPayload>>
\* Mutator releases read lock after seq mutation completes.
MutatorReleaseSeqLock(thread) ==
/\ thread \in rwLockReaders
/\ rwLockReaders' = rwLockReaders \ {thread}
/\ UNCHANGED <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, collectorPayload>>
\* 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 <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, rwLockReaders, collectorPayload>>
\* 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 <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders>>
\* 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 <<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders>>
\* ============================================================================
\* 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]_<<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites>>
Spec == Init /\ [][Next]_<<edges, roots, rc, color, inRoots, toIncLen, toInc, toDecLen, toDec, lockInc, lockDec, globalLock, mergedRoots, collecting, gcEnv, pendingWrites, seqData, payloadAlive, rwLockReaders, collectorPayload>>
THEOREM Spec => []Safety
THEOREM Spec => []RCInvariant
THEOREM Spec => []CycleInvariant
THEOREM Spec => []SeqPayloadSafety
THEOREM Spec => []RWLockInvariant
====

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
# issue 16754
type
Opt[T] = object
when T is ref:
val: T
x: int
else:
val: T
x: string
type
Foo = ref object
x: Opt[Foo]
Bar = object
x: ref Opt[Bar]
var f = Foo()
assert f.x.x is int
var b = Bar()
assert b.x.x is string
type
BazG[T] = object
x: int
BazGRef[T] = ref object
x: T
Baz = object
x: Opt[BazG[Baz]]
y: Opt[BazGRef[Baz]]
var z = Baz()
assert z.x.x is string
assert z.y.x is int
import options
type
Person = ref object
parent: Option[Person]
proc newPerson(parent: Option[Person]): Person =
Person(parent: parent)
var person = newPerson(none(Person))

View File

@@ -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
wf = smallFloatRange # OK - SmallFloat range fits in WideFloatRange
proc foo(x: Natural) =
discard
foo(12)

View File

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