diff --git a/compiler/aliases.nim b/compiler/aliases.nim index 6877028c3a..8775355c92 100644 --- a/compiler/aliases.nim +++ b/compiler/aliases.nim @@ -25,6 +25,11 @@ type pfStructural ## use structural prefix-chain detection and tree-walk pfBidirectional ## also check reverse direction per field in nkObjConstr +proc isCompileTimeOnlyNode(n: PNode): bool {.inline.} = + ## `typeof` and typedesc/static values describe types at compile time; they + ## do not read the runtime location that alias analysis is protecting. + n.kind == nkTypeOfExpr or (n.typ != nil and n.typ.isCompileTimeOnly) + func sameLocation(a, b: PNode): bool = template sameConstIndex(a, b: PNode): bool = a.kind in nkLiterals and b.kind in nkLiterals and a.intVal == b.intVal @@ -157,10 +162,13 @@ proc isPartOf*(a, b: PNode; flags: set[PartFlag] = {}): TAnalysisResult = ## ## x[] ?<| y depending on type ## ``` + if a.isCompileTimeOnlyNode or b.isCompileTimeOnlyNode: + return arNo + if a.kind == b.kind: case a.kind of nkSym: - const varKinds = {skVar, skTemp, skProc, skFunc} + const varKinds = {skVar, skTemp, skResult, skProc, skFunc} # same symbol: aliasing: if a.sym.id == b.sym.id: result = arYes elif a.sym.kind in varKinds or b.sym.kind in varKinds: @@ -271,6 +279,11 @@ proc isPartOf*(a, b: PNode; flags: set[PartFlag] = {}): TAnalysisResult = of nkCallKinds: result = arNo for i in 1.. 0: if optDocRaw in d.conf.globalOptions: diff --git a/compiler/docgen2.nim b/compiler/docgen2.nim index 7fb11a3bd7..1d5434879c 100644 --- a/compiler/docgen2.nim +++ b/compiler/docgen2.nim @@ -29,7 +29,6 @@ proc shouldProcess(g: PGen): bool = template closeImpl(body: untyped) {.dirty.} = var g = PGen(p) let useWarning = sfMainModule notin g.module.flags - let groupedToc = true if shouldProcess(g): finishGenerateDoc(g.doc) body @@ -41,7 +40,7 @@ template closeImpl(body: untyped) {.dirty.} = proc closeDoc*(graph: ModuleGraph; p: PPassContext, n: PNode): PNode = result = nil closeImpl: - writeOutput(g.doc, useWarning, groupedToc) + writeOutput(g.doc, useWarning, true) proc closeJson*(graph: ModuleGraph; p: PPassContext, n: PNode): PNode = result = nil diff --git a/compiler/enumtostr.nim b/compiler/enumtostr.nim index a21d744bea..9210e8db2e 100644 --- a/compiler/enumtostr.nim +++ b/compiler/enumtostr.nim @@ -49,65 +49,3 @@ proc genEnumToStrProc*(t: PType; info: TLineInfo; g: ModuleGraph; idgen: IdGener result.ast = n incl result.flagsImpl, {sfFromGeneric, sfNeverRaises} setHookDisamb(g, result, "$enumtostr", t) - -proc searchObjCaseImpl(obj: PNode; field: PSym): PNode = - case obj.kind - of nkSym: - result = nil - of nkElse, nkOfBranch: - result = searchObjCaseImpl(obj.lastSon, field) - else: - if obj.kind == nkRecCase and obj[0].kind == nkSym and obj[0].sym == field: - result = obj - else: - result = nil - for x in obj: - result = searchObjCaseImpl(x, field) - if result != nil: break - -proc searchObjCase(t: PType; field: PSym): PNode = - result = searchObjCaseImpl(t.n, field) - if result == nil and t.baseClass != nil: - result = searchObjCase(t.baseClass.skipTypes({tyAlias, tyGenericInst, tyRef, tyPtr}), field) - doAssert result != nil - -proc genCaseObjDiscMapping*(t: PType; field: PSym; info: TLineInfo; g: ModuleGraph; idgen: IdGenerator): PSym = - result = newSym(skProc, getIdent(g.cache, "objDiscMapping"), idgen, t.owner, info) - - let dest = newSym(skParam, getIdent(g.cache, "e"), idgen, result, info) - dest.typ = field.typ - - let res = newSym(skResult, getIdent(g.cache, "result"), idgen, result, info) - res.typ = getSysType(g, info, tyUInt8) - - result.typ = newType(tyProc, idgen, t.owner) - result.typ.n = newNodeI(nkFormalParams, info) - rawAddSon(result.typ, res.typ) - result.typ.n.add newNodeI(nkEffectList, info) - - result.typ.addParam dest - - var body = newNodeI(nkStmtList, info) - var caseStmt = newNodeI(nkCaseStmt, info) - caseStmt.add(newSymNode dest) - - let subObj = searchObjCase(t, field) - for i in 1.. 1: initList.add(", ") var it = n[i] diff --git a/compiler/lambdalifting.nim b/compiler/lambdalifting.nim index 21571de254..c1994a962d 100644 --- a/compiler/lambdalifting.nim +++ b/compiler/lambdalifting.nim @@ -126,11 +126,6 @@ const paramName* = ":envP" envName* = ":env" -proc newCall(a: PSym, b: PNode): PNode = - result = newNodeI(nkCall, a.info) - result.add newSymNode(a) - result.add b - proc createClosureIterStateType*(g: ModuleGraph; iter: PSym; idgen: IdGenerator): PType = var n = newNodeI(nkRange, iter.info) n.add newIntNode(nkIntLit, -1) @@ -288,7 +283,6 @@ proc liftIterSym*(g: ModuleGraph; n: PNode; idgen: IdGenerator; owner: PSym): PN addVar(v, env) result.add(v) # add 'new' statement: - #result.add newCall(getSysSym(g, n.info, "internalNew"), env) result.add genCreateEnv(env) createTypeBoundOpsLL(g, env.typ, n.info, idgen, owner) result.add makeClosure(g, idgen, iter, env, n.info) diff --git a/compiler/lexer.nim b/compiler/lexer.nim index bc94542cc2..b05041ef1a 100644 --- a/compiler/lexer.nim +++ b/compiler/lexer.nim @@ -735,17 +735,11 @@ proc getEscapedChar(L: var Lexer, tok: var Token) = else: lexMessage(L, errGenerated, "invalid character constant") proc handleCRLF(L: var Lexer, pos: int): int = - template registerLine = - let col = L.getColNumber(pos) - - case L.buf[pos] - of CR: - registerLine() - result = nimlexbase.handleCR(L, pos) - of LF: - registerLine() - result = nimlexbase.handleLF(L, pos) - else: result = pos + result = + case L.buf[pos] + of CR: nimlexbase.handleCR(L, pos) + of LF: nimlexbase.handleLF(L, pos) + else: pos type StringMode = enum diff --git a/compiler/liftdestructors.nim b/compiler/liftdestructors.nim index 032a4623f2..caba4b2600 100644 --- a/compiler/liftdestructors.nim +++ b/compiler/liftdestructors.nim @@ -596,12 +596,6 @@ proc newSeqCall(c: var TLiftCtx; x, y: PNode): PNode = lenCall.typ = getSysType(c.g, x.info, tyInt) result.add lenCall -proc setLenStrCall(c: var TLiftCtx; x, y: PNode): PNode = - let lenCall = genBuiltin(c, mLengthStr, "len", y) - lenCall.typ = getSysType(c.g, x.info, tyInt) - result = genBuiltin(c, mSetLengthStr, "setLen", x) # genAddr(g, x)) - result.add lenCall - proc setLenSeqCall(c: var TLiftCtx; t: PType; x, y: PNode; noinit = false): PNode = let lenCall = genBuiltin(c, mLengthSeq, "len", y) lenCall.typ = getSysType(c.g, x.info, tyInt) diff --git a/compiler/main.nim b/compiler/main.nim index e27960f589..0365eba486 100644 --- a/compiler/main.nim +++ b/compiler/main.nim @@ -209,22 +209,6 @@ proc commandInteractive(graph: ModuleGraph) = let s = llStreamOpenStdIn(onPrompt = proc() = flushDot(graph.config)) discard processPipelineModule(graph, m, idgen, s) -proc commandScan(cache: IdentCache, config: ConfigRef) = - var f = addFileExt(AbsoluteFile mainCommandArg(config), NimExt) - var stream = llStreamOpen(f, fmRead) - if stream != nil: - var - L: Lexer = default(Lexer) - tok: Token = default(Token) - openLexer(L, f, stream, cache, config) - while true: - rawGetTok(L, tok) - printTok(config, tok) - if tok.tokType == tkEof: break - closeLexer(L) - else: - rawMessage(config, errGenerated, "cannot open file: " & f.string) - const PrintRopeCacheStats = false diff --git a/compiler/modulegraphs.nim b/compiler/modulegraphs.nim index 7b975268cd..6e8715c837 100644 --- a/compiler/modulegraphs.nim +++ b/compiler/modulegraphs.nim @@ -432,10 +432,6 @@ proc addDispatchers*(g: ModuleGraph, value: PSym) = # TODO: add it for packed modules g.dispatchers.add value -iterator resolveLazySymSeq(g: ModuleGraph, list: var seq[PSym]): PSym = - for it in list.mitems: - yield it - proc setMethodsPerType*(g: ModuleGraph; id: ItemId, methods: seq[PSym]) = # TODO: add it for packed modules g.methodsPerType[id] = methods @@ -668,10 +664,6 @@ proc hash*(u: SigHash): Hash = proc hash*(x: FileIndex): Hash {.borrow.} -template getPContext(): untyped = - when c is PContext: c - else: c.c - when defined(nimsuggest): template onUse*(info: TLineInfo; s: PSym; isGenericInstance = false) = discard template onDefResolveForward*(info: TLineInfo; s: PSym) = discard diff --git a/compiler/msgs.nim b/compiler/msgs.nim index 8c3ef55423..cb9195bec8 100644 --- a/compiler/msgs.nim +++ b/compiler/msgs.nim @@ -24,10 +24,6 @@ template instLoc*(): InstantiationInfo = instantiationInfo(-2, fullPaths = true) template toStdOrrKind(stdOrr): untyped = if stdOrr == stdout: stdOrrStdout else: stdOrrStderr -proc toLowerAscii(a: var string) {.inline.} = - for c in mitems(a): - if isUpperAscii(c): c = char(uint8(c) xor 0b0010_0000'u8) - proc flushDot*(conf: ConfigRef) = ## safe to call multiple times let stdOrr = if optStdout in conf.globalOptions: stdout else: stderr @@ -83,7 +79,8 @@ proc canonicalCase(path: var string) {.inline.} = ## the idea is to only use this for checking whether a path is already in ## the table but otherwise keep the original case when FileSystemCaseSensitive: discard - else: toLowerAscii(path) + else: + for c in mitems(path): c = toLowerAscii(c) proc fileInfoKnown*(conf: ConfigRef; filename: AbsoluteFile): bool = var diff --git a/compiler/packages.nim b/compiler/packages.nim index 95c42151b0..ceb3b3ae32 100644 --- a/compiler/packages.nim +++ b/compiler/packages.nim @@ -27,7 +27,6 @@ proc getPackage*(conf: ConfigRef; cache: IdentCache; fileIdx: FileIndex): PSym = ## * `modulegraphs.getPackage` let filename = AbsoluteFile toFullPath(conf, fileIdx) - name = getIdent(cache, splitFile(filename).name) info = newLineInfo(fileIdx, 1, 1) pkgName = getPackageName(conf, filename.string) pkgIdent = getIdent(cache, pkgName) diff --git a/compiler/pipelineutils.nim b/compiler/pipelineutils.nim index b29d513060..eadd48467e 100644 --- a/compiler/pipelineutils.nim +++ b/compiler/pipelineutils.nim @@ -1,4 +1,3 @@ -import std/intsets import ast, options, lineinfos, pathutils, msgs, modulegraphs, packages proc skipCodegen*(config: ConfigRef; n: PNode): bool {.inline.} = diff --git a/compiler/procfind.nim b/compiler/procfind.nim index c2cc6e71fa..eedf1542cd 100644 --- a/compiler/procfind.nim +++ b/compiler/procfind.nim @@ -11,25 +11,10 @@ # This is needed for proper handling of forward declarations. import - ast, astalgo, msgs, semdata, types, trees, lookups + ast, astalgo, msgs, semdata, types, lookups import std/strutils -proc equalGenericParams(procA, procB: PNode): bool = - if procA.len != procB.len: return false - for i in 0..= 0 or (g.tokens.len > 0 and - g.tokens[^1].kind == tkSpaces) - proc putNL(g: var TSrcGen) = putNL(g, g.indent) @@ -646,28 +642,6 @@ proc maxLineLength(s: string): int = inc(lineLen) inc(i) -proc putRawStr(g: var TSrcGen, kind: TokType, s: string) = - var i = 0 - let hi = s.len - 1 - var str = "" - while i <= hi: - case s[i] - of '\r': - put(g, kind, str) - str = "" - inc(i) - if i <= hi and s[i] == '\n': inc(i) - optNL(g, 0) - of '\n': - put(g, kind, str) - str = "" - inc(i) - optNL(g, 0) - else: - str.add(s[i]) - inc(i) - put(g, kind, str) - proc containsNL(s: string): bool = for i in 0..", nodecl, varargs.} - - -when not declared(signbit): - proc c_signbit(x: SomeFloat): cint {.importc: "signbit", header: "".} - proc signbit*(x: SomeFloat): bool {.inline.} = - result = c_signbit(x) != 0 - import std/formatfloat proc toStrMaxPrecision*(f: BiggestFloat | float32): string = diff --git a/compiler/sem.nim b/compiler/sem.nim index a689e2626f..c3c2b82491 100644 --- a/compiler/sem.nim +++ b/compiler/sem.nim @@ -332,7 +332,6 @@ proc typeAllowedCheck(c: PContext; info: TLineInfo; typ: PType; kind: TSymKind; proc paramsTypeCheck(c: PContext, typ: PType) {.inline.} = typeAllowedCheck(c, typ.n.info, typ, skProc) -proc expectMacroOrTemplateCall(c: PContext, n: PNode): PSym proc semDirectOp(c: PContext, n: PNode, flags: TExprFlags; expectedType: PType = nil): PNode proc semWhen(c: PContext, n: PNode, semCheck: bool = true): PNode proc semTemplateExpr(c: PContext, n: PNode, s: PSym, diff --git a/compiler/semcall.nim b/compiler/semcall.nim index 38b88ee7d2..26c459f6d8 100644 --- a/compiler/semcall.nim +++ b/compiler/semcall.nim @@ -689,7 +689,7 @@ proc bracketNotFoundError(c: PContext; n: PNode; flags: TExprFlags) = baseFilter + {skIterator} else: baseFilter # this will add the errors: - var r = resolveOverloads(c, n, n, filter, flags, errors, true) + discard resolveOverloads(c, n, n, filter, flags, errors, true) if errors.len == 0: localError(c.config, n.info, "could not resolve: " & $n) else: @@ -926,15 +926,6 @@ proc semResolvedCall(c: PContext, x: var TCandidate, result.typ = finalCallee.typ.returnType updateDefaultParams(c, result) -proc canDeref(n: PNode): bool {.inline.} = - result = n.len >= 2 and (let t = n[1].typ; - t != nil and t.skipTypes({tyGenericInst, tyAlias, tySink}).kind in {tyPtr, tyRef}) - -proc tryDeref(n: PNode): PNode = - result = newNodeI(nkHiddenDeref, n.info) - result.typ = n.typ.skipTypes(abstractInst)[0] - result.add n - proc semOverloadedCall(c: PContext, n, nOrig: PNode, filter: TSymKinds, flags: TExprFlags; expectedType: PType = nil): PNode = diff --git a/compiler/semdata.nim b/compiler/semdata.nim index 89e8911975..50b33d48a1 100644 --- a/compiler/semdata.nim +++ b/compiler/semdata.nim @@ -186,6 +186,12 @@ type forwardFieldUpdates*: seq[(PType, PNode, PType)] # object/tuple field definitions whose default values mention forward # types and need delayed const checking + forwardFlagUpdates*: seq[(PType, PType)] + # (owner, son) pairs whose `propagateToOwner` ran on a not yet reified + # forward type and has to be redone in the final pass + staleTypeFlags*: IntSet + # ids of the owners in `forwardFlagUpdates`; their flags are provisional + # too, so reading them makes the reader provisional in turn inTypeofContext*: int semAsgnOpr*: proc (c: PContext; n: PNode; k: TNodeKind): PNode {.nimcall.} @@ -369,6 +375,7 @@ proc newContext*(graph: ModuleGraph; module: PSym): PContext = unknownIdents: initIntSet(), shadowDiscardedDefs: initIntSet(), realizedDefs: initIntSet(), + staleTypeFlags: initIntSet(), cache: graph.cache, graph: graph, signatures: initStrTable(), diff --git a/compiler/semexprs.nim b/compiler/semexprs.nim index d4539f7fa2..5361f57724 100644 --- a/compiler/semexprs.nim +++ b/compiler/semexprs.nim @@ -22,7 +22,6 @@ const errNamedExprExpected = "named expression expected" errNamedExprNotAllowed = "named expression not allowed here" errFieldInitTwice = "field initialized twice: '$1'" - errUndeclaredFieldX = "undeclared field: '$1'" proc semTemplateExpr(c: PContext, n: PNode, s: PSym, flags: TExprFlags = {}; expectedType: PType = nil): PNode = @@ -770,17 +769,6 @@ proc changeType(c: PContext; n: PNode, newType: PType, check: bool) = n.typ = newType -proc arrayConstrType(c: PContext, n: PNode): PType = - var typ = newTypeS(tyArray, c) - rawAddSon(typ, nil) # index type - if n.len == 0: - rawAddSon(typ, newTypeS(tyEmpty, c)) # needs an empty basetype! - else: - var t = skipTypes(n[0].typ, {tyGenericInst, tyVar, tyLent, tyOrdinal, tyAlias, tySink}) - addSonSkipIntLit(typ, t, c.idgen) - typ.setIndexType makeRangeType(c, 0, n.len - 1, n.info) - result = typ - proc semArrayConstr(c: PContext, n: PNode, flags: TExprFlags; expectedType: PType = nil): PNode = result = newNodeI(nkBracket, n.info) # nkBracket nodes can also be produced by the VM as seq constant nodes @@ -1339,7 +1327,6 @@ proc lookupInRecordAndBuildCheck(c: PContext, n, r: PNode, field: PIdent, else: illFormedAst(n, c.config) const - tyTypeParamsHolders = {tyGenericInst, tyCompositeTypeClass} tyDotOpTransparent = {tyVar, tyLent, tyPtr, tyRef, tyOwned, tyAlias, tySink} proc readTypeParameter(c: PContext, typ: PType, @@ -2326,24 +2313,6 @@ proc semDeclared(c: PContext, n: PNode, onlyCurrentScope: bool): PNode = result.info = n.info result.typ = getSysType(c.graph, n.info, tyBool) -proc expectMacroOrTemplateCall(c: PContext, n: PNode): PSym = - ## The argument to the proc should be nkCall(...) or similar - ## Returns the macro/template symbol - if isCallExpr(n): - var expandedSym = qualifiedLookUp(c, n[0], {checkUndeclared}) - if expandedSym == nil: - errorUndeclaredIdentifier(c, n.info, n[0].renderTree) - return errorSym(c, n[0]) - - if expandedSym.kind notin {skMacro, skTemplate}: - localError(c.config, n.info, "'$1' is not a macro or template" % expandedSym.name.s) - return errorSym(c, n[0]) - - result = expandedSym - else: - localError(c.config, n.info, "'$1' is not a macro or template" % n.renderTree) - result = errorSym(c, n) - proc expectString(c: PContext, n: PNode): string = var n = semConstExpr(c, n) if n.kind in nkStrKinds: @@ -2358,14 +2327,6 @@ proc newAnonSym(c: PContext; kind: TSymKind, info: TLineInfo): PSym = proc semExpandToAst(c: PContext, n: PNode): PNode = let macroCall = n[1] - when false: - let expandedSym = expectMacroOrTemplateCall(c, macroCall) - if expandedSym.kind == skError: return n - - macroCall[0] = newSymNode(expandedSym, macroCall.info) - markUsed(c, n.info, expandedSym) - onUse(n.info, expandedSym) - if isCallExpr(macroCall): for i in 1.. lastFloat(n.typ): - localError(g.config, n.info, "cannot convert " & $value & - " to " & typeToString(n.typ)) - proc foldConv(n, a: PNode; idgen: IdGenerator; g: ModuleGraph; check = false): PNode = let dstTyp = skipTypes(n.typ, abstractRange - {tyTypeDesc}) let srcTyp = skipTypes(a.typ, abstractRange - {tyTypeDesc}) diff --git a/compiler/semgnrc.nim b/compiler/semgnrc.nim index 91a834078d..da9b1c187c 100644 --- a/compiler/semgnrc.nim +++ b/compiler/semgnrc.nim @@ -233,7 +233,7 @@ proc fuzzyLookup(c: PContext, n: PNode, flags: TSemGenericFlags, if s.kind == skType: # don't put types in sym choice var ambig = false if candidates.len > 1: - let s2 = searchInScopes(c, ident, ambig) + discard searchInScopes(c, ident, ambig) result = newDot(result, semGenericStmtSymbol(c, n, s, ctx, flags, isAmbiguous = ambig, fromDotExpr = true)) else: diff --git a/compiler/semobjconstr.nim b/compiler/semobjconstr.nim index 769f88b6f2..bb6dbe4144 100644 --- a/compiler/semobjconstr.nim +++ b/compiler/semobjconstr.nim @@ -440,7 +440,7 @@ proc initConstrContext(t: PType, initExpr: PNode): ObjConstrContext = proc computeRequiresInit(c: PContext, t: PType): bool = assert t.kind == tyObject var constrCtx = initConstrContext(t, newNode(nkObjConstr)) - let initResult = semConstructTypeAux(c, constrCtx, {efWantNoDefaults}) + discard semConstructTypeAux(c, constrCtx, {efWantNoDefaults}) constrCtx.missingFields.len > 0 proc defaultConstructionError(c: PContext, t: PType, info: TLineInfo) = @@ -450,7 +450,7 @@ proc defaultConstructionError(c: PContext, t: PType, info: TLineInfo) = assert objType != nil if objType.kind == tyObject: var constrCtx = initConstrContext(objType, newNodeI(nkObjConstr, info)) - let initResult = semConstructTypeAux(c, constrCtx, {efIgnoreDefaults}) + discard semConstructTypeAux(c, constrCtx, {efIgnoreDefaults}) if constrCtx.missingFields.len > 0: localError(c.config, info, "The $1 type doesn't have a default value. The following fields must be initialized: $2." % [typeToString(t), listSymbolNames(constrCtx.missingFields)]) diff --git a/compiler/sempass2.nim b/compiler/sempass2.nim index 2d1a42cd86..1bc5bac628 100644 --- a/compiler/sempass2.nim +++ b/compiler/sempass2.nim @@ -1006,7 +1006,6 @@ proc trackIf(tracked: PEffects, n: PNode) = proc trackBlock(tracked: PEffects, n: PNode; typ: PType) = if n.kind in {nkStmtList, nkStmtListExpr}: - let myBlock = tracked.currentBlock var oldState = -1 for i in 0.. 0: + let updates = move c.forwardFlagUpdates + c.staleTypeFlags = initIntSet() + var changed = true + while changed: + changed = false + for (owner, elem) in updates: + let before = owner.flags + propagateToOwner(owner, elem) + if owner.flags != before: changed = true + for i in 0.. 0: c.getCurrOwner else: rectype.sym for i in 0.. rawDealloc -> chunk.owner == addr(a) --------------> This thread owns the chunk ------> The current chunk is active -> Chunk is completely unused -----> Chunk references no foreign cells + dealloc -> rawDealloc -> chunk.owner == regionOwner(a) -------> This thread owns the chunk ------> The current chunk is active -> Chunk is completely unused -----> Chunk references no foreign cells | | (Add cell into the current chunk) | Return the current chunk back to tlsf | | | | v v v v @@ -63,6 +63,11 @@ const # size of chunks in last matrix bin MaxBigChunkSize = int(1'i32 shl MaxFli - 1'i32 shl (MaxFli-MaxLog2Sli-1)) HugeChunkSize = MaxBigChunkSize + 1 + usesRegionHandles = hasThreadSupport and defined(gcDestructors) + # Deliberately *not* `hasThreadLocalAllocator`: this selects the chunk + # layout, which is ABI and must match between a `--useNimRtl` client and + # the RTL it links against. Whether this module owns a thread local region + # is the separate question that `hasThreadLocalAllocator` answers. type PTrunk = ptr Trunk @@ -112,11 +117,15 @@ type PChunk = ptr BaseChunk PBigChunk = ptr BigChunk PSmallChunk = ptr SmallChunk + SharedFreeLists = array[0..max(1, SmallChunkSize div MemAlign-1), ptr FreeCell] BaseChunk {.pure, inheritable.} = object prevSize: int # size of previous chunk; for coalescing # 0th bit == 1 if 'used size: int # if < PageSize it is a small chunk - owner: ptr MemRegion + when usesRegionHandles: + owner: ptr RegionHandle + else: + owner: ptr MemRegion SmallChunk = object of BaseChunk next, prev: PSmallChunk # chunks of the same size @@ -145,14 +154,16 @@ type next: ptr HeapLinks MemRegion = object + when usesRegionHandles: + regionHandle: ptr RegionHandle when not defined(gcDestructors): minLargeObj, maxLargeObj: int freeSmallChunks: array[0..max(1, SmallChunkSize div MemAlign-1), PSmallChunk] # List of available chunks per size class. Only one is expected to be active per class. when defined(gcDestructors): - sharedFreeLists: array[0..max(1, SmallChunkSize div MemAlign-1), ptr FreeCell] - # When a thread frees a pointer it did not create, it must not adjust the counters. - # Instead, the cell is placed here and deferred until the next allocation. + sharedFreeLists: SharedFreeLists + # Used directly without threads. Threaded builds use RegionHandle but + # retain this 2 KiB spacer: removing it regresses 2-4 KiB allocations. flBitmap: uint32 slBitmap: array[RealFli, uint32] matrix: array[RealFli, array[MaxSli, PBigChunk]] @@ -160,7 +171,7 @@ type currMem, maxMem, freeMem, occ: int # memory sizes (allocated from OS) lastSize: int # needed for the case that OS gives us pages linearly when defined(gcDestructors): - sharedFreeListBigChunks: PBigChunk # make no attempt at avoiding false sharing for now for this object field + sharedFreeListBigChunks: PBigChunk # private pending list with threads; shared queue otherwise chunkStarts: IntSet when not defined(gcDestructors): @@ -173,9 +184,24 @@ type when defined(nimTypeNames): allocCounter, deallocCounter: int + RegionHandle = object + # Permanent chunk-owner identity and home of the remote-free queues. + sharedFreeLists: SharedFreeLists + sharedFreeListBigChunks: PBigChunk + # Keep the movable allocator state with its permanent owner while the + # owning thread is retired. + region: MemRegion + next: ptr RegionHandle + template smallChunkOverhead(): untyped = sizeof(SmallChunk) template bigChunkOverhead(): untyped = sizeof(BigChunk) +template regionOwner(a: var MemRegion): untyped = + when usesRegionHandles: + a.regionHandle + else: + addr a + when hasThreadSupport: template loada(x: untyped): untyped = atomicLoadN(unsafeAddr x, ATOMIC_RELAXED) template storea(x, y: untyped) = atomicStoreN(unsafeAddr x, y, ATOMIC_RELAXED) @@ -502,6 +528,56 @@ proc pageAddr(p: pointer): PChunk {.inline.} = result = cast[PChunk](cast[int](p) and not PageMask) #sysAssert(Contains(allocator.chunkStarts, pageIndex(result))) +when hasThreadLocalAllocator: + var + regionPool: ptr RegionHandle + regionPoolLock: SysLock + initSysLock(regionPoolLock) + + proc moveMemRegion(dest, source: ptr MemRegion) {.inline.} = + # MemRegion owns only raw allocator state, so transfer it bitwise and + # clear the source to leave exactly one owner. + copyMem(dest, source, sizeof(MemRegion)) + zeroMem(source, sizeof(MemRegion)) + + proc acquireMemRegion(a: var MemRegion) {.raises: [], gcsafe.} = + if a.regionHandle != nil: + return + + acquireSys(regionPoolLock) + let handle = regionPool + if handle != nil: + regionPool = handle.next + releaseSys(regionPoolLock) + + if handle == nil: + # RegionHandle is larger than llAlloc's one-page metadata slabs and is + # retained independently of any checked-out MemRegion. + let handleSize = roundup(sizeof(RegionHandle), PageSize) + let newHandle = cast[ptr RegionHandle](osAllocPages(handleSize)) + zeroMem(newHandle, sizeof(RegionHandle)) + a.regionHandle = newHandle + else: + moveMemRegion(addr a, addr handle.region) + + proc releaseMemRegion(a: var MemRegion) {.raises: [], gcsafe.} = + # Zeroing `a` also clears `a.regionHandle`, which is what keeps a late + # `dealloc` on this thread correct: the ownership test can no longer match, + # so the cell is routed to its real owner's handle instead of to a region + # that is about to be reused. A late *alloc* on the other hand would mint + # chunks with a nil owner, so nothing may allocate after this point -- + # `afterThreadRuns` has already run by the time `threadProcWrapStackFrame` + # gets here. + if a.regionHandle == nil: + return + let handle = a.regionHandle + moveMemRegion(addr handle.region, addr a) + + acquireSys(regionPoolLock) + handle.next = regionPool + regionPool = handle + releaseSys(regionPoolLock) + when false: proc writeFreeList(a: MemRegion) = var it = a.freeChunksList @@ -618,7 +694,7 @@ proc splitChunk2(a: var MemRegion, c: PBigChunk, size: int): PBigChunk = result.prev = nil # size and not used: result.prevSize = size - result.owner = addr a + result.owner = regionOwner(a) sysAssert((size and 1) == 0, "splitChunk 2") sysAssert((size and PageMask) == 0, "splitChunk: size is not a multiple of the PageSize") @@ -686,7 +762,7 @@ proc getBigChunk(a: var MemRegion, size: int): PBigChunk = # if we over allocated split the chunk: if result.size > size: splitChunk(a, result, size) - result.owner = addr a + result.owner = regionOwner(a) else: removeChunkFromMatrix2(a, result, fl, sl) if result.size >= size + PageSize: @@ -694,7 +770,7 @@ proc getBigChunk(a: var MemRegion, size: int): PBigChunk = # set 'used' to true: result.prevSize = 1 track("setUsedToFalse", addr result.size, sizeof(int)) - sysAssert result.owner == addr a, "getBigChunk: No owner set!" + sysAssert result.owner == regionOwner(a), "getBigChunk: No owner set!" incl(a, a.chunkStarts, pageIndex(result)) dec(a.freeMem, size) @@ -710,7 +786,7 @@ proc getHugeChunk(a: var MemRegion; size: int): PBigChunk = result.size = size # set 'used' to true: result.prevSize = 1 - result.owner = addr a + result.owner = regionOwner(a) incl(a, a.chunkStarts, pageIndex(result)) proc freeHugeChunk(a: var MemRegion; c: PBigChunk) = @@ -791,7 +867,7 @@ proc deallocBigChunk(a: var MemRegion, c: PBigChunk) = when defined(gcDestructors): template atomicPrepend(head, elem: untyped) = # see also https://en.cppreference.com/w/cpp/atomic/atomic_compare_exchange - when hasThreadSupport: + when usesRegionHandles: while true: elem.next.storea head.loada if atomicCompareExchangeN(addr head, addr elem.next, elem, weak = true, ATOMIC_RELEASE, ATOMIC_RELAXED): @@ -800,30 +876,39 @@ when defined(gcDestructors): elem.next.storea head.loada head.storea elem - proc addToSharedFreeListBigChunks(a: var MemRegion; c: PBigChunk) {.inline.} = - sysAssert c.next == nil, "c.next pointer must be nil" - atomicPrepend a.sharedFreeListBigChunks, c + when usesRegionHandles: + proc addToSharedFreeListBigChunks(handle: ptr RegionHandle; + c: PBigChunk) {.inline.} = + sysAssert c.next == nil, "c.next pointer must be nil" + atomicPrepend handle.sharedFreeListBigChunks, c + else: + proc addToSharedFreeListBigChunks(a: var MemRegion; + c: PBigChunk) {.inline.} = + sysAssert c.next == nil, "c.next pointer must be nil" + atomicPrepend a.sharedFreeListBigChunks, c proc takeFromSharedFreeListBigChunks(a: var MemRegion): PBigChunk {.inline.} = - when hasThreadSupport: - while true: - result = atomicLoadN(addr a.sharedFreeListBigChunks, ATOMIC_ACQUIRE) - if result == nil: - break - let next = result.next.loada - var expected = result - if atomicCompareExchangeN(addr a.sharedFreeListBigChunks, addr expected, next, - weak = true, ATOMIC_ACQUIRE, ATOMIC_RELAXED): - result.next.storea nil - break - else: - result = a.sharedFreeListBigChunks - if result != nil: - a.sharedFreeListBigChunks = result.next - result.next = nil + when usesRegionHandles: + if a.sharedFreeListBigChunks == nil: + let sharedHead = addr a.regionHandle.sharedFreeListBigChunks + # Detach a batch from the stable remote inbox. The embedded MemRegion + # field is now a private pending list and moves with the region. + if atomicLoadN(sharedHead, ATOMIC_RELAXED) != nil: + a.sharedFreeListBigChunks = atomicExchangeN(sharedHead, nil, + ATOMIC_ACQUIRE) + result = a.sharedFreeListBigChunks + if result != nil: + a.sharedFreeListBigChunks = result.next + result.next = nil - proc addToSharedFreeList(c: PSmallChunk; f: ptr FreeCell; size: int) {.inline.} = - atomicPrepend c.owner.sharedFreeLists[size], f + when usesRegionHandles: + proc addToSharedFreeList(handle: ptr RegionHandle; f: ptr FreeCell; + size: int) {.inline.} = + atomicPrepend handle.sharedFreeLists[size], f + else: + proc addToSharedFreeList(c: PSmallChunk; f: ptr FreeCell; + size: int) {.inline.} = + atomicPrepend c.owner.sharedFreeLists[size], f const MaxSteps = 20 @@ -846,9 +931,8 @@ when defined(gcDestructors): dec(a.occ, total) proc freeDeferredObjects(a: var MemRegion) = - # Pop only as many nodes as we can process. Detaching the entire list and - # re-enqueuing its unprocessed tail through atomicPrepend would overwrite - # that tail's next pointer and lose the rest of the list. + # Bound the work per allocation. With threads, takeFromSharedFreeListBigChunks + # detaches the shared stack into the region's private pending list first. for _ in 0..MaxSteps: let it = takeFromSharedFreeListBigChunks(a) if it == nil: break @@ -892,17 +976,20 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer if size + alignOff <= SmallChunkSize-smallChunkOverhead(): template fetchSharedCells(tc: PSmallChunk) = - # Consumes cells from (potentially) foreign threads from `a.sharedFreeLists[s]` + # Consume cells freed by potentially foreign threads. when defined(gcDestructors): if tc.freeList == nil: - when hasThreadSupport: - # Steal the entire list from `sharedFreeList`: - tc.freeList = atomicExchangeN(addr a.sharedFreeLists[s], nil, ATOMIC_RELAXED) + when usesRegionHandles: + let sharedHead = addr tc.owner.sharedFreeLists[s] + # The owner is the only consumer, so once it observes a non-empty + # stack no other thread can make it empty before the exchange. + if atomicLoadN(sharedHead, ATOMIC_RELAXED) != nil: + tc.freeList = atomicExchangeN(sharedHead, nil, ATOMIC_ACQUIRE) else: tc.freeList = a.sharedFreeLists[s] a.sharedFreeLists[s] = nil - # if `tc.freeList` isn't nil, `tc` will gain capacity. - # We must calculate how much it gained and how many foreign cells are included. + # If `tc.freeList` isn't nil, `tc` gains capacity. Calculate how + # much it gained and how many foreign cells are included. compensateCounters(a, tc, size) # allocate a small block: for small chunks, we use only its next pointer @@ -921,11 +1008,11 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer c.size = size c.acc = (alignOff + size).uint32 c.free = SmallChunkSize - smallChunkOverhead() - alignOff.int32 - size.int32 - sysAssert c.owner == addr(a), "rawAlloc: No owner set!" + sysAssert c.owner == regionOwner(a), "rawAlloc: No owner set!" c.next = nil c.prev = nil - # Shared cells are fetched here in case `c.size * 2 >= SmallChunkSize - smallChunkOverhead()`. - # For those single cell chunks, we would otherwise have to allocate a new one almost every time. + # Fetch deferred cells here for single-cell chunks; otherwise every + # allocation of that size would tend to allocate a new chunk. fetchSharedCells(c) if c.free >= size: # Because removals from `a.freeSmallChunks[s]` only happen in the other alloc branch and during dealloc, @@ -963,9 +1050,8 @@ proc rawAlloc(a: var MemRegion, requestedSize: int, alignment: int = 0): pointer dec(c.free, size) sysAssert((cast[int](result) and (MemAlign-1)) == 0, "rawAlloc 9") sysAssert(allocInv(a), "rawAlloc: end c != nil") - # We fetch deferred cells *after* advancing `c.freeList`/`acc` to adjust `c.free`. - # If after the adjustment it turns out there's free cells available, - # the chunk stays in `a.freeSmallChunks[s]` and the need for a new chunk is delayed. + # Fetch after advancing `freeList`/`acc` so `c.free` can be adjusted. If + # cells arrived, keep this chunk active instead of allocating another. fetchSharedCells(c) sysAssert(allocInv(a), "rawAlloc: before c.free < size") if c.free < size: @@ -1030,7 +1116,8 @@ proc rawDealloc(a: var MemRegion, p: pointer) = # ^ We might access thread foreign storage here. # The other thread cannot possibly free this block as it's still alive. var f = cast[ptr FreeCell](p) - if c.owner == addr(a): + let owner = c.owner + if owner == regionOwner(a): # We own the block, there is no foreign thread involved. dec a.occ, s untrackSize(s) @@ -1093,7 +1180,10 @@ proc rawDealloc(a: var MemRegion, p: pointer) = when logAlloc: cprintf("dealloc(pointer_%p) # SMALL FROM %p CALLER %p\n", p, c.owner, addr(a)) when defined(gcDestructors): - addToSharedFreeList(c, f, s div MemAlign) + when usesRegionHandles: + addToSharedFreeList(owner, f, s div MemAlign) + else: + addToSharedFreeList(c, f, s div MemAlign) sysAssert(((cast[int](p) and PageMask) - smallChunkOverhead() - c.chunkAlignOff) %% s == 0, "rawDealloc 2") else: @@ -1101,10 +1191,14 @@ proc rawDealloc(a: var MemRegion, p: pointer) = when overwriteFree: nimSetMem(p, -1'i32, c.size -% bigChunkOverhead()) when logAlloc: cprintf("dealloc(pointer_%p) # BIG %p\n", p, c.owner) when defined(gcDestructors): - if c.owner == addr(a): + let owner = c.owner + if owner == regionOwner(a): deallocBigChunk(a, cast[PBigChunk](c)) else: - addToSharedFreeListBigChunks(c.owner[], cast[PBigChunk](c)) + when usesRegionHandles: + addToSharedFreeListBigChunks(owner, cast[PBigChunk](c)) + else: + addToSharedFreeListBigChunks(owner[], cast[PBigChunk](c)) else: deallocBigChunk(a, cast[PBigChunk](c)) @@ -1263,6 +1357,13 @@ when defined(nimTypeNames): template instantiateForRegion(allocator: untyped) {.dirty.} = {.push stackTrace: off.} + when hasThreadLocalAllocator: + proc initThreadAllocator() {.gcsafe, raises: [].} = + acquireMemRegion(allocator) + + proc releaseThreadAllocator() {.gcsafe, raises: [].} = + releaseMemRegion(allocator) + when defined(nimFulldebug): proc interiorAllocatedPtr*(p: pointer): pointer = result = interiorAllocatedPtr(allocator, p) diff --git a/lib/system/arc.nim b/lib/system/arc.nim index cd74e7f4fa..8c6efbd3f6 100644 --- a/lib/system/arc.nim +++ b/lib/system/arc.nim @@ -312,13 +312,17 @@ when not (defined(gcOrc) or defined(gcYrc)): ## Forces a full garbage collection pass. With `--mm:arc` a nop. discard -template setupForeignThreadGc* = - ## With `--mm:arc` a nop. - discard - -template tearDownForeignThreadGc* = - ## With `--mm:arc` a nop. - discard +when not hasThreadSupport: + template setupForeignThreadGc* = discard + template tearDownForeignThreadGc* = discard +elif emulatedThreadVars: + template setupForeignThreadGc* = + {.error: "setupForeignThreadGc is available only when ``--threads:on`` and ``--tlsEmulation:off`` are used".} + template tearDownForeignThreadGc* = + {.error: "tearDownForeignThreadGc is available only when ``--threads:on`` and ``--tlsEmulation:off`` are used".} +elif not hasThreadLocalAllocator: + template setupForeignThreadGc* = discard + template tearDownForeignThreadGc* = discard proc isObjDisplayCheck(source: PNimTypeV2, targetDepth: int16, token: uint32): bool {.compilerRtl, inl.} = result = targetDepth <= source.depth and source.display[targetDepth] == token diff --git a/lib/system/threadimpl.nim b/lib/system/threadimpl.nim index e35378db0e..62d54ef6d8 100644 --- a/lib/system/threadimpl.nim +++ b/lib/system/threadimpl.nim @@ -19,6 +19,13 @@ when not defined(useNimRtl): threadType = ThreadType.NimThread +when hasThreadLocalAllocator and not emulatedThreadVars: + proc setupForeignThreadGc*() {.gcsafe, raises: [].} = + initThreadAllocator() + + proc tearDownForeignThreadGc*() {.gcsafe, raises: [].} = + releaseThreadAllocator() + when defined(gcDestructors): proc deallocThreadStorage(p: pointer) = c_free(p) else: @@ -83,6 +90,8 @@ else: deallocThreadStorage(thrd.rawStack) proc threadProcWrapStackFrame[TArg](thrd: ptr Thread[TArg]) {.raises: [].} = + when hasThreadLocalAllocator: + initThreadAllocator() when defined(boehmgc): boehmGC_call_with_stack_base(threadProcWrapDispatch[TArg], thrd) elif not defined(nogc) and not defined(gogc) and not defined(gcRegions) and not usesDestructors: @@ -97,6 +106,8 @@ proc threadProcWrapStackFrame[TArg](thrd: ptr Thread[TArg]) {.raises: [].} = when declared(deallocOsPages): deallocOsPages() else: threadProcWrapDispatch(thrd) + when hasThreadLocalAllocator: + releaseThreadAllocator() template nimThreadProcWrapperBody*(closure: untyped): untyped = var thrd = cast[ptr Thread[TArg]](closure) diff --git a/testament/important_packages.nim b/testament/important_packages.nim index 27774db70e..d8b7cfc74f 100644 --- a/testament/important_packages.nim +++ b/testament/important_packages.nim @@ -176,6 +176,7 @@ pkg "unittest2" pkg "unpack" when not defined(arm64): pkg "weave", "nimble install -y cligen@#HEAD; nimble test_gc_arc", useHead = true +pkg "web3", "nimble test_slim", useHead = true pkg "websock", "nim c -d:chronicles_log_level=INFO tests/all_tests.nim" pkg "websocket", "nim c websocket.nim" pkg "with" diff --git a/tests/arc/tarcmisc.nim b/tests/arc/tarcmisc.nim index 22a1b69a43..d5921d33ce 100644 --- a/tests/arc/tarcmisc.nim +++ b/tests/arc/tarcmisc.nim @@ -937,3 +937,47 @@ proc mainRegen() = doAssert b.a.c == right mainRegen() + + +from std/typetraits import distinctBase, supportsCopyMem + +block: # bug #26025 + type + M[B] = distinct seq[B] + W = object + g: U # `U` is only declared below, so it used to be a `tyForward` + # here and `W` ended up without `tfHasAsgn` + U = M[uint64] + + doAssert not supportsCopyMem(W) + + var h: M[W] + seq[W](h).add W(g: U(@[1'u64])) + var copied = h + for it in items(distinctBase(copied)): + doAssert seq[uint64](it.g) == @[1'u64] + doAssert seq[uint64](seq[W](h)[0].g) == @[1'u64] + +block: # bug #26025, the propagation has to reach the indirect owners too + type + M2[B] = distinct seq[B] + + ViaArray = object + g: array[2, Late] # the forward type sits inside the field's type + + Outer = object # `Inner` is forward here... + a: Inner + Inner = object + b: Late + Late = M2[uint64] + + Reader = object # ...whereas `Outer` is already reified but its + z: Outer # own flags were still provisional + + AsTuple = tuple[a: Late] + + doAssert not supportsCopyMem(ViaArray) + doAssert not supportsCopyMem(Inner) + doAssert not supportsCopyMem(Outer) + doAssert not supportsCopyMem(Reader) + doAssert not supportsCopyMem(AsTuple) diff --git a/tests/ccgbugs/t26104.nim b/tests/ccgbugs/t26104.nim new file mode 100644 index 0000000000..53a3550b18 --- /dev/null +++ b/tests/ccgbugs/t26104.nim @@ -0,0 +1,16 @@ +discard """ + action: compile + ccodeCheck: "@'((*Result).f);' .*" +""" + +# bug #26104: a compile-time-only `typeof` argument was treated as a runtime +# alias of the result field, forcing an unnecessarily large temporary and copy. +type + B = array[131072, byte] + Y = object + f: B + +proc fill(_: type B): B = discard +proc make(): Y = result.f = fill(typeof(result.f)) + +discard make() diff --git a/tests/ccgbugs/t26112.nim b/tests/ccgbugs/t26112.nim new file mode 100644 index 0000000000..f10250b61f --- /dev/null +++ b/tests/ccgbugs/t26112.nim @@ -0,0 +1,35 @@ +discard """ + matrix: "--mm:refc; --mm:orc" + ccodeCheck: "'result.fromScalar = x_p0;'" + ccodeCheck: "'result.fromObject = x_p0.fromObject;'" + ccodeCheck: "'result.nested.fromNested = x_p0.fromNested;'" +""" + +# bug #26112: unrelated parameters were considered potential aliases of the +# result location when their types could be contained in the returned object. + +type + Inner = object + fromNested: int + P = object + fromScalar: int + fromObject: int + nested: Inner + +func fromScalar(x: int): P = + P(fromScalar: x) + +proc fromObject(x: P): P = + P(fromObject: x.fromObject) + +func fromNested(x: Inner): P = + P(nested: Inner(fromNested: x.fromNested)) + +proc selfAlias(): P = + result.fromScalar = 42 + result = P(fromScalar: result.fromScalar) + +doAssert fromScalar(1).fromScalar == 1 +doAssert fromObject(P(fromObject: 2)).fromObject == 2 +doAssert fromNested(Inner(fromNested: 3)).nested.fromNested == 3 +doAssert selfAlias().fromScalar == 42 diff --git a/tests/gc/tmove_case_object.nim b/tests/gc/tmove_case_object.nim new file mode 100644 index 0000000000..8f4d9ba450 --- /dev/null +++ b/tests/gc/tmove_case_object.nim @@ -0,0 +1,56 @@ +discard """ + matrix: "--mm:refc; --mm:orc" +""" + +type + A = object of RootObj + + V = object + case g: bool + of true: + v: A + of false: + e: string + +var r = V(g: true, v: A()) +discard move r +GC_fullCollect() + +type + Kind = enum nested, other + Nested = object + case kind: Kind + of nested: + case enabled: bool + of true: payload: A + of false: message: string + of other: + discard + +var n = Nested(kind: nested, enabled: true, payload: A()) +discard move n +GC_fullCollect() + +# Moving from the other branch must keep its value alive and leave the source +# in the default state. +var s = V(g: false, e: "hello") +let moved = move s +doAssert moved.e == "hello" +doAssert not s.g +doAssert s.e.len == 0 + +# Reinitializing the zeroed value must also restore embedded object type +# headers. +type W = object + a: A + value: V + text: string + +var w = W(a: A(), value: V(g: true, v: A()), text: "content") +let movedW = move w +doAssert movedW.text == "content" +doAssert cast[ptr pointer](addr w.a)[] != nil +doAssert not w.value.g +doAssert w.value.e.len == 0 +doAssert w.text.len == 0 +GC_fullCollect() diff --git a/tests/threads/tthreadallocatorforeignpool.nim b/tests/threads/tthreadallocatorforeignpool.nim new file mode 100644 index 0000000000..ad46c74fea --- /dev/null +++ b/tests/threads/tthreadallocatorforeignpool.nim @@ -0,0 +1,59 @@ +discard """ + matrix: "--mm:arc --threads:on --tlsEmulation:off; --mm:orc --threads:on --tlsEmulation:off" + disabled: "windows" + output: "ok" + timeout: "30" +""" + +import std/posix + +var + escaped: pointer + reused: pointer + +proc allocateOnForeignThread(_: pointer): pointer {.noconv.} = + setupForeignThreadGc() + escaped = allocShared(96) + cast[ptr int](escaped)[] = 73 + tearDownForeignThreadGc() + result = nil + +proc reuseOnForeignThread(_: pointer): pointer {.noconv.} = + setupForeignThreadGc() + doAssert cast[ptr int](escaped)[] == 73 + deallocShared(escaped) + reused = allocShared(96) + doAssert reused == escaped + deallocShared(reused) + tearDownForeignThreadGc() + result = nil + +proc consumeDeferredFree(_: pointer): pointer {.noconv.} = + setupForeignThreadGc() + let first = allocShared(96) + let second = allocShared(96) + # The first allocation advances the active chunk and collects its deferred + # foreign frees. The next allocation reuses the remotely returned cell. + doAssert second == escaped + deallocShared(first) + deallocShared(second) + tearDownForeignThreadGc() + result = nil + +proc run(worker: proc(_: pointer): pointer {.noconv.}) = + var thread: Pthread + doAssert pthread_create(addr thread, nil, worker, nil) == 0 + doAssert pthread_join(thread, nil) == 0 + +# setup/teardown is the checkout/return boundary. A distinct native thread can +# safely inherit the allocator even while one of its allocations is still live. +run(allocateOnForeignThread) +run(reuseOnForeignThread) + +# A free that arrives while the allocator is idle is queued on its handle and +# consumed after that allocator is handed to another foreign thread. +run(allocateOnForeignThread) +deallocShared(escaped) +run(consumeDeferredFree) + +echo "ok" diff --git a/tests/threads/tthreadallocatorhandoffrace.nim b/tests/threads/tthreadallocatorhandoffrace.nim new file mode 100644 index 0000000000..3a7803e8f7 --- /dev/null +++ b/tests/threads/tthreadallocatorhandoffrace.nim @@ -0,0 +1,64 @@ +discard """ + matrix: "--mm:arc --threads:on; --mm:orc --threads:on" + output: "ok" + timeout: "30" +""" + +import std/[atomics, typedthreads] + +const + pointerCount = 512 + drainCount = 2048 + iterations {.intdefine.} = 200 + sizes = [16, 64, 4000, 4096, 8192] + +var + pointers: array[pointerCount, pointer] + mayExit: Atomic[bool] + +proc owner() {.thread.} = + for i in 0..