JavaScriptの上限・限界値

JavaScriptの文字列や配列は最長でどこまで格納できるか、気にしたことはありますか？関数は何個まで引数を取れるのでしょうか？ブロックのネストは何段まで？

この記事では、そんな素朴な疑問に答えてみます。

テストに使った環境は、

* macOS 12.3.1 (Arm64)
* Node.js v17.7.2
* Firefox Nightly 102.0a1 (2022-05-29)

です。当たり前ですが、この記事に載せる数値は環境によって変わる可能性があります。

テストに使ったスクリプト類は

* https://github.com/minoki/javascript-limits

に置いてあります。

# 文字列の長さ

まずは文字列の長さです。

規格には

> The String type is the set of all ordered sequences of zero or more 16-bit unsigned integer values (“elements”) up to a maximum length of 2^53 - 1 elements.
>
> https://262.ecma-international.org/12.0/#sec-ecmascript-language-types-string-type

と書かれていますが、実際の処理系ではどうなのか。

テストに使ったスクリプトは [string.js](https://github.com/minoki/javascript-limits/blob/43d2b2a59d882ad7bd28a89e43e90510f41f648b/string.js) です。

結果：

| V8 (Node.js) | SpiderMonkey (Firefox) | JavaScriptCore (osascript) |
|-|-|-|
| 536870888 (0x1fffffe8) | 1073741822 (0x3ffffffe) | 2147483647 (0x7fffffff) |

扱える文字列長はV8が一番短いという結果になりました。これでも十分長いように見えますが、512MiBのASCII文字列からなるファイルをreadFileSyncで読もうとすると死ぬということなので注意が必要です。特に、**Node.jsもその辺のJavaScriptで書かれたminifierも、512MiB以上あるJavaScriptファイルを読み込めません**。

# TypedArrayの長さ

次はTypedArrayを試してみます。

テストに使ったスクリプトは [uint8array.js](https://github.com/minoki/javascript-limits/blob/43d2b2a59d882ad7bd28a89e43e90510f41f648b/uint8array.js), [float64array.js](https://github.com/minoki/javascript-limits/blob/43d2b2a59d882ad7bd28a89e43e90510f41f648b/float64array.js) です。

結果：

| | V8 (Node.js) | SpiderMonkey (Firefox) | JavaScriptCore (osascript) |
|-|-|-|-|
| `Uint8Array` | 4294967296 (0x100000000) | 8589934592 (0x200000000) | 4294967296 (0x100000000) |
| `Float64Array` | 4294967296 (0x100000000) | 1073741824 (0x40000000) | 536870912 (0x20000000) |

どの処理系もギガバイト単位のTypedArrayを扱えるようです。SpiderMonkeyとJSCはUint8Array（1要素1バイト）とFloat64Array（1要素8バイト）で8倍の差があるので、要素数というよりはバイト数で制限されているようです。

# 配列の長さ

次は通常の配列を試してみます。規格には

> Array(...values)
>
> Let intLen be !ToUint32(len).
> If intLen is not the same value as len, throw a RangeError exception.
>
> https://262.ecma-international.org/12.0/#sec-array

> 23.1.2.1 Array.from(items [, mapfn [, thisArg]])
>
> i. If k ≥ 2^53-1, then
>     1. Let error be ...
>
> https://262.ecma-international.org/12.0/#sec-array.from

と書かれています。`new Array(n)` の形で初期化するときは $2^{32}-1$ が上限、それ以外の場面では $2^{53}-1$ が上限っぽいですね。

ここでは、中身が詰まった配列を `new Array(n).fill(0)` で構築して長さの最大値を測ります。中身の詰まっていない配列に用がある人なんていませんからね。

測定の上では問題があって、実際のスクリプトであまり長い配列を作ると**回復不能なエラー**が出る場合があります。なので、ここでは別に用意したスクリプトでそれぞれの長さについて処理系（node / osascript）を起動するという方式を取ります。

テストに使ったスクリプトは [array-node.lua](https://github.com/minoki/javascript-limits/blob/43d2b2a59d882ad7bd28a89e43e90510f41f648b/array-node.lua), [array-osascript.lua](https://github.com/minoki/javascript-limits/blob/43d2b2a59d882ad7bd28a89e43e90510f41f648b/array-osascript.lua) です。

結果：

| V8 (Node.js) | JavaScriptCore (osascript) |
|-|-|
| 100663296 (0x6000000) | greater than 268435456 |

JSCの上限はよくわかりませんでした。十分な時間をかければわかるのかもしれませんが、人間の寿命は有限なので……。

# ブロックの深さ `{{{}}}`

JavaScriptのパーサーはネストされたブロックにどこまで耐えられるのでしょうか？試してみました！

テストに使ったスクリプトは [depth-braces.js](https://github.com/minoki/javascript-limits/blob/43d2b2a59d882ad7bd28a89e43e90510f41f648b/depth-braces.js) です。

`{{{}}}` がn=3、という風に数えます。

結果：

| V8 (Node.js) | SpiderMonkey (Firefox) | JavaScriptCore (osascript) |
|-|-|-|
| 2884 (0xb44) | 855 (0x357) | 6587 (0x19bb) |

SpiderMonkeyの上限が意外と低いですね。

# カッコの深さ `(((0)))`

ネストされたカッコはどうでしょうか。

テストに使ったスクリプトは [depth-parens.js](https://github.com/minoki/javascript-limits/blob/43d2b2a59d882ad7bd28a89e43e90510f41f648b/depth-parens.js) です。

`(((0)))` がn=3、という風に数えます。

結果：

| V8 (Node.js) | SpiderMonkey (Firefox) | JavaScriptCore (osascript) |
|-|-|-|
| 1657 (0x679) | 1412 (0x584) | 2932 (0xb74) |

# `let` 変数の個数

letで宣言された変数は一つのスコープに何個まで存在できるのでしょうか。

テストに使ったスクリプトは [let.js](https://github.com/minoki/javascript-limits/blob/43d2b2a59d882ad7bd28a89e43e90510f41f648b/let.js) です。

`let a0; let a1; let a2;` という感じの入力で試します。

結果：

| V8 (Node.js) | SpiderMonkey (Firefox) | JavaScriptCore (osascript) |
|-|-|-|
| 8388607 (0x7fffff) | greater than 16777216 | 638656 (0x9bec0) |

SpiderMonkeyのやつはブラウザーが重くなったので最後まで確認していませんが、他の2つよりは上限が大きそうです。

# 関数の仮引数

関数を宣言する時にパラメーターは何個まで受け取れるのでしょうか。

テストに使ったスクリプトは [parameter.js](https://github.com/minoki/javascript-limits/blob/43d2b2a59d882ad7bd28a89e43e90510f41f648b/parameter.js) です。

結果：

| V8 (Node.js) | SpiderMonkey (Firefox) | JavaScriptCore (osascript) |
|-|-|-|
| 65534 (0xfffe) | 65535 (0xffff) | greater than 16777216 |

JavaScriptCoreの上限はよくわかりませんでしたが、他の2つより大きそうです。

# 関数の実引数

関数を呼び出すときに引数は何個まで与えられるのでしょうか。

テストに使ったスクリプトは [call.js](https://github.com/minoki/javascript-limits/blob/43d2b2a59d882ad7bd28a89e43e90510f41f648b/call.js) です。

`Array.of(0,0,0);` という感じの文字列をevalして検証しています。

結果：

| V8 (Node.js) | SpiderMonkey (Firefox) | JavaScriptCore (osascript) |
|-|-|-|
| 54928 (0xd690) | 65535 (0xffff)  | 638624 (0x9bea0) |

# 配列リテラル

配列リテラルでは何個まで要素を与えられるのでしょうか。

テストに使ったスクリプトは [array-literal.js](https://github.com/minoki/javascript-limits/blob/43d2b2a59d882ad7bd28a89e43e90510f41f648b/array-literal.js) です。

`void[0,0,0];` という感じの文字列をevalして検証しています。

結果：

| V8 (Node.js) | SpiderMonkey (Firefox) | JavaScriptCore (osascript) |
|-|-|-|
| greater than 67108864 | 268435453 (0xffffffd) | 178956970 (0xaaaaaaa) |

`Array.of` を使うよりも配列リテラルを使った方が長い配列を初期化できそうですね！

# 余談

そもそもなんでこんなことが気になったのかと言いますと、最近作っているLunarMLというaltJSで巨大なJavaScriptコードを出力したときにエラーが出たからです。

LunarMLではプログラムに対してCPS変換という変換を行っているのですが、その結果巨大な出力ファイルができたり、手書きではあり得ないような深いブロック（関数）のネストが出現してパースエラーが出たのでした。

今後、LunarMLのJavaScriptバックエンドの質を上げていく中で、この記事に書いたような上限を超えないような工夫を実装したいところです。

LunarMLはGitHubで開発しているほか、筆者のブログでも定期的に記事を書いています。よろしければGitHubにスターを頂けると幸いです。

* https://github.com/minoki/LunarML
* [筆者のブログ](https://blog.miz-ar.info/tag/lunarml/)