From c7ea004ca9b472e7fb2eec88e5e4107638207e15 Mon Sep 17 00:00:00 2001 From: Savant Date: Fri, 3 Jul 2026 03:55:58 -0400 Subject: [PATCH] js: cursor inference to elide nimCopy for safe value aliases (#25948) --- compiler/jsgen.nim | 16 ++++++++++++---- compiler/varpartitions.nim | 21 +++++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/compiler/jsgen.nim b/compiler/jsgen.nim index fd82a127f7..a04c8e8f95 100644 --- a/compiler/jsgen.nim +++ b/compiler/jsgen.nim @@ -34,7 +34,7 @@ import ropes, wordrecg, renderer, cgmeth, lowerings, sighashes, modulegraphs, lineinfos, transf, injectdestructors, sourcemap, astmsgs, pushpoppragmas, - mangleutils + mangleutils, varpartitions import pipelineutils @@ -1298,14 +1298,16 @@ proc genAsgnAux(p: PProc, x, y: PNode, noCopyNeeded: bool) = xtyp = etySeq case xtyp of etySeq: - if x.typ.kind in {tyVar, tyLent} or (needsNoCopy(p, y) and needsNoCopy(p, x)) or noCopyNeeded: + if x.typ.kind in {tyVar, tyLent} or (needsNoCopy(p, y) and needsNoCopy(p, x)) or noCopyNeeded or + (x.kind == nkSym and sfCursor in x.sym.flags): lineF(p, "$1 = $2;$n", [a.rdLoc, b.rdLoc]) else: useMagic(p, "nimCopy") lineF(p, "$1 = nimCopy(null, $2, $3);$n", [a.rdLoc, b.res, genTypeInfo(p, y.typ)]) of etyObject: - if x.typ.kind in {tyVar, tyLent, tyOpenArray, tyVarargs} or (needsNoCopy(p, y) and needsNoCopy(p, x)) or noCopyNeeded: + if x.typ.kind in {tyVar, tyLent, tyOpenArray, tyVarargs} or (needsNoCopy(p, y) and needsNoCopy(p, x)) or noCopyNeeded or + (x.kind == nkSym and sfCursor in x.sym.flags): lineF(p, "$1 = $2;$n", [a.rdLoc, b.rdLoc]) else: useMagic(p, "nimCopy") @@ -2092,7 +2094,8 @@ proc genVarInit(p: PProc, v: PSym, n: PNode) = gen(p, n, a) case mapType(p, v.typ) of etyObject, etySeq: - if v.typ.kind in {tyOpenArray, tyVarargs} or needsNoCopy(p, n): + if v.typ.kind in {tyOpenArray, tyVarargs} or needsNoCopy(p, n) or + sfCursor in v.flags: s = a.res else: useMagic(p, "nimCopy") @@ -2798,6 +2801,11 @@ proc genProc(oldProc: PProc, prc: PSym): Rope = var transformedBody = transformBody(p.module.graph, p.module.idgen, prc, {}) if sfInjectDestructors in prc.flags: transformedBody = injectDestructorCalls(p.module.graph, p.module.idgen, prc, transformedBody) + else: + # JS has a GC, so the destructor pass is off; but the cursor (alias) analysis + # is independent of ownership and always memory-safe on a traced target. + # Running it lets last-use `var b = a` aliases skip the deep `nimCopy`. + computeCursors(prc, transformedBody, p.module.graph) p.nested: genStmt(p, transformedBody) diff --git a/compiler/varpartitions.nim b/compiler/varpartitions.nim index fea6cc540b..3a01eb0bbd 100644 --- a/compiler/varpartitions.nim +++ b/compiler/varpartitions.nim @@ -677,9 +677,13 @@ proc deps(c: var Partitions; dest, src: PNode) = else: let srcid = variableId(c, s) if srcid >= 0: - if s.kind notin {skResult, skParam} and ( - c.s[srcid].aliveEnd < c.s[vid].aliveEnd): - # you cannot borrow from a local that lives shorter than 'vid': + if s.kind notin {skResult, skParam} and + c.s[srcid].aliveEnd < c.s[vid].aliveEnd and + c.g.config.backend != backendJs: + # you cannot borrow from a local that lives shorter than 'vid'. + # On a traced (JS/GC) target the source object stays alive as long + # as the alias references it, so this lifetime rule does not apply; + # value-semantics safety is enforced by `dangerousMutation` instead. when explainCursors: echo "B not a cursor ", d.sym, " ", c.s[srcid].aliveEnd, " ", c.s[vid].aliveEnd c.s[vid].flags.incl preventCursor elif {isReassigned, preventCursor} * c.s[srcid].flags != {}: @@ -1003,13 +1007,22 @@ proc checkBorrowedLocations*(par: var Partitions; body: PNode; config: ConfigRef #if par.s[rid].con.kind == isRootOf and dangerousMutation(par.graphs[par.s[rid].con.graphIndex], par.s[i]): # cannotBorrow(config, s, par.graphs[par.s[rid].con.graphIndex]) +proc jsDeepCopied(t: PType): bool = + ## On the JS backend `nimCopy` deep-copies these type classes on every + ## assignment, so eliding the copy for a safe alias is worthwhile even when + ## the type has no C-style destructor. + t.skipTypes({tyGenericInst, tyAlias, tyDistinct, tyVar, tyLent}).kind in + {tyObject, tyTuple, tyArray, tySequence, tyString} + proc computeCursors*(s: PSym; n: PNode; g: ModuleGraph) = + let jsCursors = g.config.backend == backendJs var par = computeGraphPartitions(s, n, g, {cursorInference}) for i in 0 ..< par.s.len: let v = addr(par.s[i]) if v.flags * {ownsData, preventCursor, isConditionallyReassigned} == {} and v.sym.kind notin {skParam, skResult} and - v.sym.flags * {sfThread, sfGlobal} == {} and hasDestructor(v.sym.typ) and + v.sym.flags * {sfThread, sfGlobal} == {} and + (hasDestructor(v.sym.typ) or (jsCursors and jsDeepCopied(v.sym.typ))) and v.sym.typ.skipTypes({tyGenericInst, tyAlias}).kind != tyOwned and (getAttachedOp(g, v.sym.typ, attachedAsgn) == nil or sfError notin getAttachedOp(g, v.sym.typ, attachedAsgn).flags):