diff --git a/.github/openclaw/qualification/patches/010-arraybuffer-accounting.patch b/.github/openclaw/qualification/patches/010-arraybuffer-accounting.patch index bdbf1f837aefa..791f134348fad 100644 --- a/.github/openclaw/qualification/patches/010-arraybuffer-accounting.patch +++ b/.github/openclaw/qualification/patches/010-arraybuffer-accounting.patch @@ -885,10 +885,10 @@ index f85d1129..43c9a8be 100644 } diff --git a/test/js/node/process/arraybuffer-accounting.fixture.mjs b/test/js/node/process/arraybuffer-accounting.fixture.mjs new file mode 100644 -index 0000000000000000000000000000000000000000..d2bce713c2bd0a7bc66262eb327e1a6cf52676b4 +index 0000000000000000000000000000000000000000..8611deb06e3f23ad5605ac7693efcfb6c833a16d --- /dev/null +++ b/test/js/node/process/arraybuffer-accounting.fixture.mjs -@@ -0,0 +1,361 @@ +@@ -0,0 +1,366 @@ +import assert from "node:assert/strict"; +import { once } from "node:events"; +import { readFileSync } from "node:fs"; @@ -1081,6 +1081,11 @@ index 0000000000000000000000000000000000000000..d2bce713c2bd0a7bc66262eb327e1a6c + return [before, retained, isolated, released]; +} + ++// Keep JIT compilation of the busy loop separate from this large fixture. ++function spinUntilReleased(flag) { ++ while (!Atomics.load(flag, 0)) {} ++} ++ +const kind = isMainThread ? process.argv[2] : typeof workerData === "string" ? workerData : workerData.kind; +if (!isMainThread && ["busy", "wait", "shutdown", "startup"].includes(kind)) { + // Finish startup work before comparing snapshots of a fresh allocation. @@ -1091,7 +1096,7 @@ index 0000000000000000000000000000000000000000..d2bce713c2bd0a7bc66262eb327e1a6c + if (kind === "wait") { + assert.equal(Atomics.wait(flag, 0, 0), "ok"); + } else { -+ while (!Atomics.load(flag, 0)) {} ++ spinUntilReleased(flag); + } + parentPort.postMessage("released"); +} else if (!isMainThread && kind === "transfer") { diff --git a/.github/openclaw/qualify-arm64.sh b/.github/openclaw/qualify-arm64.sh new file mode 100644 index 0000000000000..62d790b490714 --- /dev/null +++ b/.github/openclaw/qualify-arm64.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail +: "${ENGINE_BUILD_DIR:?}" "${QUALIFICATION_DIR:?}" +case "$(uname -m)" in arm64|aarch64) ;; *) echo 'Native ARM64 host required' >&2; exit 1;; esac +mkdir -p "$QUALIFICATION_DIR" +QUALIFICATION_DIR=$(cd "$QUALIFICATION_DIR" && pwd) +jsc="$ENGINE_BUILD_DIR/bin/jsc" +"$ENGINE_BUILD_DIR/bin/testmasm" > "$QUALIFICATION_DIR/testmasm.log" 2>&1 +"$ENGINE_BUILD_DIR/bin/testFFI" > "$QUALIFICATION_DIR/testffi.log" 2>&1 +Tools/Scripts/run-jsc-stress-tests JSTests/stress --jsc "$jsc" --child-processes "${ENGINE_TEST_JOBS:-4}" \ + --filter 'promise|microtask|dynamic-import|bun-async-context|module-loader|top-level-await|async-module|module-namespace|intl-segmenter|callsite-syntax-positions|stack-position-regressions|error-stack' \ + --output-dir "$QUALIFICATION_DIR/jsc-stress" --no-retry > "$QUALIFICATION_DIR/jsc-stress.log" 2>&1 +python3 .github/openclaw/verify-jsc-results.py "$QUALIFICATION_DIR/jsc-stress" --require module-namespace-esmodule-marker.js --require callsite-syntax-positions.js --require stack-position-regressions.js +Tools/Scripts/run-jsc-stress-tests JSTests/modules.yaml --jsc "$jsc" --child-processes "${ENGINE_TEST_JOBS:-4}" \ + --output-dir "$QUALIFICATION_DIR/jsc-modules" --no-retry > "$QUALIFICATION_DIR/jsc-modules.log" 2>&1 +# Accounting requires generational GC; run its established explicit modes below. +for fixture in array-buffer-accounting heap-allocation-sampling; do + for tier in interpreter baseline dfg ftl concurrent; do + args=(--useGenerationalGC=true --forceMiniVMMode=false --useDollarVM=true) + case "$tier" in + interpreter) args+=(--useJIT=false);; + baseline) args+=(--useDFGJIT=false --useFTLJIT=false);; + dfg) args+=(--useFTLJIT=false --useConcurrentJIT=false --thresholdForJITAfterWarmUp=10 --thresholdForOptimizeAfterWarmUp=20);; + ftl) args+=(--useConcurrentJIT=false --thresholdForJITAfterWarmUp=10 --thresholdForOptimizeAfterWarmUp=20 --thresholdForFTLOptimizeAfterWarmUp=20);; + concurrent) args+=(--gcAtEnd=true --useConcurrentGC=true);; + esac + "$jsc" "${args[@]}" "JSTests/stress/$fixture.js" > "$QUALIFICATION_DIR/$fixture-$tier.log" 2>&1 + done +done +python3 - "$QUALIFICATION_DIR" "$(git rev-parse HEAD)" <<'PY' +import json, platform, sys +from pathlib import Path +out = Path(sys.argv[1]) +counts = {} +for suite, minimum in [('jsc-stress', 1400), ('jsc-modules', 1600)]: + lines = (out / suite / 'results').read_text().splitlines() + assert len(lines) >= minimum and all(line.endswith(': PASS') for line in lines), suite + counts[suite] = len(lines) +assert 'testAdd64RegisterAddress()' in (out / 'testmasm.log').read_text() +(out / 'arm64-gate.json').write_text(json.dumps({'passed': True, 'source': sys.argv[2], 'machine': platform.machine(), 'system': platform.system(), 'counts': counts, 'accounting_modes': 5, 'sampling_modes': 5, 'testmasm': True, 'testffi': True}, indent=2) + '\n') +PY diff --git a/.github/workflows/openclaw-arm64.yml b/.github/workflows/openclaw-arm64.yml new file mode 100644 index 0000000000000..b30beb1924467 --- /dev/null +++ b/.github/workflows/openclaw-arm64.yml @@ -0,0 +1,64 @@ +name: OpenClaw ARM64 regressions +on: + pull_request: + branches: [openclaw/main, openclaw/release-5718a6ec] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: openclaw-arm64-${{ github.ref }} + cancel-in-progress: true +jobs: + linux-arm64: + if: github.repository == 'openclaw/WebKit' + runs-on: ubuntu-24.04-arm + timeout-minutes: 150 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + sparse-checkout-cone-mode: false + sparse-checkout: | + /* + !/LayoutTests/ + !/JSTests/* + !/PerformanceTests/ + !/Websites/ + !/ManualTests/ + !/WebDriverTests/ + /JSTests/stress/ + /JSTests/modules/ + /JSTests/*.yaml + /JSTests/BUN-TEST-DIFFERENCES.md + - name: Install native build and regression tools + run: | + curl -fsSL --retry 5 https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null + echo 'deb https://apt.llvm.org/noble/ llvm-toolchain-noble-23 main' | sudo tee /etc/apt/sources.list.d/llvm.list + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends cmake ninja-build clang-23 lld-23 libicu-dev ruby ruby-highline libjson-pp-perl + - name: Build native ARM64 JSC and assembler tests + run: | + test "$(uname -m)" = aarch64 + cmake -S . -B "$RUNNER_TEMP/jsc-build" -G Ninja \ + -DPORT=JSCOnly -DCMAKE_BUILD_TYPE=Release -DDEVELOPER_MODE=ON -DCMAKE_COMPILE_WARNING_AS_ERROR=OFF \ + -DENABLE_STATIC_JSC=ON -DUSE_BUN_JSC_ADDITIONS=ON -DUSE_BUN_EVENT_LOOP=ON \ + -DUSE_MIMALLOC=ON -DUSE_EXTERNAL_MIMALLOC=OFF -DENABLE_FTL_JIT=ON \ + -DENABLE_BUN_SKIP_FAILING_ASSERTIONS=ON -DALLOW_LINE_AND_COLUMN_NUMBER_IN_BUILTINS=ON \ + -DENABLE_REMOTE_INSPECTOR=ON -DCMAKE_C_COMPILER=clang-23 -DCMAKE_CXX_COMPILER=clang++-23 + cmake --build "$RUNNER_TEMP/jsc-build" --target jsc testmasm testFFI -j4 + - name: Qualify assembler and all JIT tiers + env: + ENGINE_BUILD_DIR: ${{ runner.temp }}/jsc-build + QUALIFICATION_DIR: ${{ runner.temp }}/arm64-proof + run: bash .github/openclaw/qualify-arm64.sh + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + if: always() + with: + name: linux-arm64-proof + path: | + ${{ runner.temp }}/arm64-proof/*.json + ${{ runner.temp }}/arm64-proof/*.log + ${{ runner.temp }}/arm64-proof/jsc-*/results + if-no-files-found: warn + retention-days: 7 diff --git a/JSTests/stress/async-stack-trace-promise-all-basic.js b/JSTests/stress/async-stack-trace-promise-all-basic.js index ec393eb69eeae..5e5c2e61e017d 100644 --- a/JSTests/stress/async-stack-trace-promise-all-basic.js +++ b/JSTests/stress/async-stack-trace-promise-all-basic.js @@ -1,5 +1,5 @@ //@ requireOptions("--useAsyncStackTrace=1") - +// Stack columns name syntax tokens, not call parentheses. const source = "async-stack-trace-promise-all-basic.js"; function nop() {} @@ -71,12 +71,12 @@ function shouldThrowAsync(run, errorType, message, stackFunctions) { await run(); }, Error, "error", [ - ["thrower", "65:59"], - ["async run", "66:43"], - ["async test", "71:25"], + ["thrower", "65:50"], + ["async run", "66:26"], + ["async test", "71:16"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "19:20"], - ["global code", "69:24"] + ["shouldThrowAsync", "19:5"], + ["global code", "69:8"] ], ); drainMicrotasks(); @@ -101,12 +101,12 @@ function shouldThrowAsync(run, errorType, message, stackFunctions) { await runner(); }, Error, "task2 error", [ - ["task2", "93:20"], - ["async runner", "96:46"], - ["async test", "101:28"], + ["task2", "93:11"], + ["async runner", "96:29"], + ["async test", "101:16"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "19:20"], - ["global code", "99:24"] + ["shouldThrowAsync", "19:5"], + ["global code", "99:8"] ], ); drainMicrotasks(); @@ -140,15 +140,15 @@ function shouldThrowAsync(run, errorType, message, stackFunctions) { await runner(); }, Error, "task2 error", [ - ["baz", "131:20"], - ["async bar", "127:22"], - ["async foo", "123:14"], - ["async task2", "120:14"], - ["async runner", "135:46"], - ["async test", "140:28"], + ["baz", "131:11"], + ["async bar", "127:5"], + ["async foo", "123:5"], + ["async task2", "120:5"], + ["async runner", "135:29"], + ["async test", "140:16"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "19:20"], - ["global code", "138:24"] + ["shouldThrowAsync", "19:5"], + ["global code", "138:8"] ], ); drainMicrotasks(); diff --git a/JSTests/stress/async-stack-trace-promise-allSettled-basic.js b/JSTests/stress/async-stack-trace-promise-allSettled-basic.js index 356cafea43ed3..0a14f3ee06b44 100644 --- a/JSTests/stress/async-stack-trace-promise-allSettled-basic.js +++ b/JSTests/stress/async-stack-trace-promise-allSettled-basic.js @@ -1,5 +1,5 @@ //@ requireOptions("--useAsyncStackTrace=1") - +// Stack columns name syntax tokens, not call parentheses. const source = "async-stack-trace-promise-allSettled-basic.js"; function nop() {} @@ -60,11 +60,11 @@ function unwrap(promise) { shouldBe(results.length, 1); shouldBe(results[0].status, "rejected"); testStack(results[0].reason, [ - ["thrower", "53:59"], - ["async run", "55:36"], + ["thrower", "53:50"], + ["async run", "55:12"], ["drainMicrotasks", "[native code]"], - ["unwrap", "47:18"], - ["global code", "59:27"] + ["unwrap", "47:3"], + ["global code", "59:21"] ]); } } @@ -98,25 +98,25 @@ function unwrap(promise) { shouldBe(results[3].status, "rejected"); shouldBe(results[4].status, "rejected"); testStack(results[1].reason, [ - ["task1", "78:20"], - ["async run", "89:36"], + ["task1", "78:11"], + ["async run", "89:12"], ["drainMicrotasks", "[native code]"], - ["unwrap", "47:18"], - ["global code", "93:27"] + ["unwrap", "47:3"], + ["global code", "93:21"] ]); testStack(results[3].reason, [ - ["task2", "85:20"], - ["async run", "89:36"], + ["task2", "85:11"], + ["async run", "89:12"], ["drainMicrotasks", "[native code]"], - ["unwrap", "47:18"], - ["global code", "93:27"] + ["unwrap", "47:3"], + ["global code", "93:21"] ]); testStack(results[4].reason, [ - ["task3", "87:52"], - ["async run", "89:36"], + ["task3", "87:43"], + ["async run", "89:12"], ["drainMicrotasks", "[native code]"], - ["unwrap", "47:18"], - ["global code", "93:27"] + ["unwrap", "47:3"], + ["global code", "93:21"] ]); } } diff --git a/JSTests/stress/async-stack-trace-promise-any-basic.js b/JSTests/stress/async-stack-trace-promise-any-basic.js index 0e0f8ff7eeb5c..045388da0f120 100644 --- a/JSTests/stress/async-stack-trace-promise-any-basic.js +++ b/JSTests/stress/async-stack-trace-promise-any-basic.js @@ -1,5 +1,5 @@ //@ requireOptions("--useAsyncStackTrace=1") - +// Stack columns name syntax tokens, not call parentheses. const source = "async-stack-trace-promise-any-basic.js"; function nop() {} @@ -78,12 +78,12 @@ function shouldThrowAsync(run, errorType) { shouldBe(aggregateError.errors.length, 1); testStack(aggregateError.errors[0], [ - ["thrower", "68:59"], - ["async run", "69:43"], - ["async test", "74:18"], + ["thrower", "68:50"], + ["async run", "69:26"], + ["async test", "74:9"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "56:18"], - ["global code", "72:44"] + ["shouldThrowAsync", "56:3"], + ["global code", "72:28"] ] ); } @@ -116,32 +116,32 @@ function shouldThrowAsync(run, errorType) { shouldBe(aggregateError.errors.length, 3); testStack(aggregateError.errors[0], [ - ["task1", "97:20"], - ["async run", "107:43"], - ["async test", "112:18"], + ["task1", "97:11"], + ["async run", "107:26"], + ["async test", "112:9"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "56:18"], - ["global code", "110:44"] + ["shouldThrowAsync", "56:3"], + ["global code", "110:28"] ] ); testStack(aggregateError.errors[1], [ - ["task2", "104:20"], - ["async run", "107:43"], - ["async test", "112:18"], + ["task2", "104:11"], + ["async run", "107:26"], + ["async test", "112:9"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "56:18"], - ["global code", "110:44"] + ["shouldThrowAsync", "56:3"], + ["global code", "110:28"] ] ); testStack(aggregateError.errors[2], [ - ["task3", "106:52"], - ["async run", "107:43"], - ["async test", "112:18"], + ["task3", "106:43"], + ["async run", "107:26"], + ["async test", "112:9"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "56:18"], - ["global code", "110:44"] + ["shouldThrowAsync", "56:3"], + ["global code", "110:28"] ] ); } diff --git a/JSTests/stress/async-stack-trace-promise-race-basic.js b/JSTests/stress/async-stack-trace-promise-race-basic.js index 4773978522d09..9a05b9f023b94 100644 --- a/JSTests/stress/async-stack-trace-promise-race-basic.js +++ b/JSTests/stress/async-stack-trace-promise-race-basic.js @@ -1,5 +1,5 @@ //@ requireOptions("--useAsyncStackTrace=1") - +// Stack columns name syntax tokens, not call parentheses. const source = "async-stack-trace-promise-race-basic.js"; function nop() {} @@ -70,12 +70,12 @@ function shouldThrowAsync(run, errorType, message, stackFunctions) { async function test() { await run(); }, Error, 'error', [ - ["thrower", "65:59"], - ["async run", "66:44"], - ["async test", "71:18"], + ["thrower", "65:50"], + ["async run", "66:26"], + ["async test", "71:9"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "19:20"], - ["global code", "69:21"] + ["shouldThrowAsync", "19:5"], + ["global code", "69:5"] ] ); drainMicrotasks(); @@ -104,12 +104,12 @@ function shouldThrowAsync(run, errorType, message, stackFunctions) { async function test() { await run(); }, Error, 'error from task1' , [ - ["task1", "90:20"], - ["async run", "100:44"], - ["async test", "105:18"], + ["task1", "90:11"], + ["async run", "100:26"], + ["async test", "105:9"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "19:20"], - ["global code", "103:21"] + ["shouldThrowAsync", "19:5"], + ["global code", "103:5"] ] ); drainMicrotasks(); diff --git a/OPENCLAW.md b/OPENCLAW.md index 720377913d125..21e8e9e559a5f 100644 --- a/OPENCLAW.md +++ b/OPENCLAW.md @@ -145,6 +145,8 @@ baseline. Engine and Bun headers and libraries must be rebuilt together. ## Unreleased +- Fix ARM64 MacroAssembler register-to-memory `add64` operand order, restoring DFG typed-array allocation accounting and preserving the source register; cover the operation in testmasm and native ARM64 qualification. + - Cache source-relative stack line and column lookups on demand, including syntax-selected positions. - Build both fork engine artifacts at the exact pull-request head before landing, with native Linux qualification and a Linux-hosted macOS arm64 compilation check. - Keep syntax-selected stack positions separate from exception ranges for calls, constructors, property reads, and async continuations. Preserve the positions across bytecode rewriting, optimization, and cache replay. diff --git a/Source/JavaScriptCore/assembler/MacroAssemblerARM64.h b/Source/JavaScriptCore/assembler/MacroAssemblerARM64.h index 17088a7e5f519..144b6b7b47b55 100644 --- a/Source/JavaScriptCore/assembler/MacroAssemblerARM64.h +++ b/Source/JavaScriptCore/assembler/MacroAssemblerARM64.h @@ -453,7 +453,7 @@ class MacroAssemblerARM64 : public AbstractMacroAssembler { void add64(RegisterID src, Address dest) { load64(dest, getCachedDataTempRegisterIDAndInvalidate()); - m_assembler.add<64>(src, dataTempRegister, dataTempRegister); + m_assembler.add<64>(dataTempRegister, dataTempRegister, src); store64(dataTempRegister, dest); } diff --git a/Source/JavaScriptCore/assembler/testmasm.cpp b/Source/JavaScriptCore/assembler/testmasm.cpp index 3f495628f492d..07351f816cee3 100644 --- a/Source/JavaScriptCore/assembler/testmasm.cpp +++ b/Source/JavaScriptCore/assembler/testmasm.cpp @@ -1028,6 +1028,28 @@ void testAdd64Imm32() } } +void testAdd64RegisterAddress() +{ + auto add = compile([] (CCallHelpers& jit) { + emitFunctionPrologue(jit); + jit.add64(GPRInfo::argumentGPR1, CCallHelpers::Address(GPRInfo::argumentGPR0, sizeof(uint64_t))); + jit.move(GPRInfo::argumentGPR1, GPRInfo::returnValueGPR); + emitFunctionEpilogue(jit); + jit.ret(); + }); + + for (auto initial : int64Operands()) { + for (auto source : int64Operands()) { + uint64_t values[] = { 0x0123456789abcdefULL, static_cast(initial), 0xfedcba9876543210ULL }; + auto result = invoke(add, values, static_cast(source)); + CHECK_EQ(values[1], static_cast(initial) + static_cast(source)); + CHECK_EQ(result, static_cast(source)); + CHECK_EQ(values[0], 0x0123456789abcdefULL); + CHECK_EQ(values[2], 0xfedcba9876543210ULL); + } + } +} + void testAdd64ArgImm32() { for (auto immediate : int32Operands()) { @@ -8534,6 +8556,7 @@ void run(const char* filter) WTF_IGNORES_THREAD_SAFETY_ANALYSIS RUN(testAdd32Imm()); RUN(testAdd32ArgImm()); RUN(testAdd64Imm32()); + RUN(testAdd64RegisterAddress()); RUN(testAdd64ArgImm32()); RUN(testAdd64Imm64()); RUN(testAdd64ArgImm64()); diff --git a/jsc.md b/jsc.md index 5aa7268664e2a..2cf916fb821e9 100644 --- a/jsc.md +++ b/jsc.md @@ -2,6 +2,8 @@ ## Unreleased +- Fix ARM64 MacroAssembler register-to-memory `add64` operand order, restoring DFG typed-array allocation accounting and preserving the source register; cover the operation in testmasm and native ARM64 qualification. + - Fix `Intl.Segmenter` containing lookups around surrogate pairs so segment boundaries and word status agree with iteration. - Expose the require(esm) `__esModule` marker as an own enumerable namespace property while preserving unmarked imports and live bindings. Adapts [oven-sh/WebKit#279](https://github.com/oven-sh/WebKit/pull/279). Thanks @robobun!