From 839cbeb371e9a219662925bd0bb923ba7bd66941 Mon Sep 17 00:00:00 2001 From: metagn Date: Fri, 7 Nov 2025 15:19:50 +0300 Subject: [PATCH] js: replace `push.apply` with for loop for string add [backport] (#25267) While `a.push.apply(a, b)` is better for performance than the previous `a = a.concat(b)` due to the fact that it doesn't create a new array, there is a pretty big problem with it: depending on the JS engine, if the second array is too long, it can [cause a crash](https://tanaikech.github.io/2020/04/20/limitation-of-array.prototype.push.apply-under-v8-for-google-apps-script/) due to the function `push` taking too many arguments. This has unfortunately been what the codegen produces since 1.4.0 (commit https://github.com/nim-lang/Nim/commit/707367e1ca231d964ba82a92b642eb5efdc1aa7c). So string addition is now moved to a compilerproc that just uses a `for` loop. From what I can tell this is the most compatible and the fastest. Only potential problem compared to `concat` etc is with aliasing, i.e. adding an array to itself, but I'm guessing it's enough that the length from before the iteration is used, since it can only grow. The test checks for aliased nim strings but I don't know if there's an extra protection for them. --- compiler/jsgen.nim | 4 ++-- lib/system/jssys.nim | 8 ++++++++ tests/system/tconcat.nim | 32 +++++++++++++++++++++++++++----- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/compiler/jsgen.nim b/compiler/jsgen.nim index 8abb830bd2..fd8ef583d0 100644 --- a/compiler/jsgen.nim +++ b/compiler/jsgen.nim @@ -2333,8 +2333,8 @@ proc genMagic(p: PProc, n: PNode, r: var TCompRes) = r.res = "if (null != $1) { if (null == $2) $2 = $3; else $2 += $3; }" % [b, lhs.rdLoc, tmp] else: - let (a, tmp) = maybeMakeTemp(p, n[1], lhs) - r.res = "$1.push.apply($3, $2);" % [a, rhs.rdLoc, tmp] + useMagic(p, "nimAddStrStr") + r.res = "nimAddStrStr($1, $2);" % [lhs.rdLoc, rhs.rdLoc] r.kind = resExpr of mAppendSeqElem: var x, y: TCompRes = default(TCompRes) diff --git a/lib/system/jssys.nim b/lib/system/jssys.nim index 6934e67ee4..b469c4695f 100644 --- a/lib/system/jssys.nim +++ b/lib/system/jssys.nim @@ -687,6 +687,14 @@ proc isObj(obj, subclass: PNimType): bool {.compilerproc.} = proc addChar(x: string, c: char) {.compilerproc, asmNoStackFrame.} = {.emit: "`x`.push(`c`);".} +proc nimAddStrStr(x, y: string) {.compilerproc, asmNoStackFrame.} = + {.emit: """ + var L = `y`.length; + for (var i = 0; i < L; ++i) { + `x`.push(`y`[i]); + } + """.} + {.pop.} proc tenToThePowerOf(b: int): BiggestFloat = diff --git a/tests/system/tconcat.nim b/tests/system/tconcat.nim index fdce3ea00d..8cf995c938 100644 --- a/tests/system/tconcat.nim +++ b/tests/system/tconcat.nim @@ -1,11 +1,33 @@ discard """ - output: "DabcD" + targets: "c cpp js" + output: ''' +DabcD +(8192, 8, 1024) +''' """ -const - x = "abc" +import std/assertions -var v = "D" & x & "D" +block: + const + x = "abc" -echo v + var v = "D" & x & "D" + doAssert v == "DabcD" + echo v + +block: # test large additions + var a = "abcdefgh" + let initialLen = a.len + let times = 10 + for i in 1..times: + let start = a.len + a.add(a) + doAssert a.len == 2 * start + let multiplier = 1 shl times + doAssert a.len == initialLen * multiplier + echo (a.len, initialLen, multiplier) + for i in 1 ..< multiplier: + for j in 0 ..< initialLen: + doAssert a[j] == a[i * initialLen + j]