|
| 1 | +#!/bin/sh |
| 2 | +# guest-sparc-gates.sh — ENGINE VERDICT for the sparc rung (RUNBOOK-sparc.md |
| 3 | +# gates S0 + S3). Runs INSIDE the NetBSD/sparc 10.1 sun4m guest (SS-20, |
| 4 | +# -m 512M, snapshot=on). Fetches payload from the host at 10.0.2.2:8180, |
| 5 | +# builds bare qjs/qjsc from quickjs-ng v0.15.1 WITHOUT cmake (host-derived |
| 6 | +# recipe, see below), runs the S3 big-endian correctness micro-probes FIRST |
| 7 | +# (fail-fast: a broken engine invalidates the expensive S0 rows), then the |
| 8 | +# S0 RAM-fit measurements (/usr/bin/time -l; NetBSD reports KB). |
| 9 | +# |
| 10 | +# No-cmake recipe (derived + verified on darwin host 2026-07-09; mirrors |
| 11 | +# CMakeLists.txt: qjs lib = dtoa.c libregexp.c libunicode.c quickjs.c, |
| 12 | +# qjs-libc = quickjs-libc.c, qjs_exe = qjs.c gen/repl.c gen/standalone.c, |
| 13 | +# qjsc = qjsc.c; defines _GNU_SOURCE QUICKJS_NG_BUILD; project-wide |
| 14 | +# -funsigned-char; C11 gnu; Release => -DNDEBUG; libs m + pthread). |
| 15 | +# |
| 16 | +# Markers: s1-*-exit=N (build), s3-<n>-*-exit=N (probes), |
| 17 | +# s0-*-exit=N (memory rows), '=== GUEST-DONE ===' at the end. |
| 18 | +# Build failures are findings — keep going and report. |
| 19 | +set -ux |
| 20 | +H=http://10.0.2.2:8180 |
| 21 | +W=/root/sparcwork |
| 22 | +mkdir -p "$W"; cd "$W" || exit 1 |
| 23 | + |
| 24 | +echo "=== HOSTINFO ===" |
| 25 | +date |
| 26 | +uname -a |
| 27 | +cc --version 2>&1 | head -2 |
| 28 | +sysctl hw.model machdep.cpu_arch 2>/dev/null |
| 29 | +ulimit -a |
| 30 | +# raise the data-size soft limit to its hard limit (32-bit NetBSD defaults |
| 31 | +# can be far below physmem; a low limit would masquerade as engine OOM) |
| 32 | +ulimit -d unlimited 2>/dev/null || ulimit -d "$(ulimit -H -d)" 2>/dev/null |
| 33 | +echo "datasize-now=$(ulimit -d)" |
| 34 | + |
| 35 | +echo "=== RAMPROBE ===" |
| 36 | +# what does NetBSD actually map at -m 512M on qemu SS-20? (S0 first question) |
| 37 | +dmesg | grep -i 'memory' |
| 38 | +sysctl hw.physmem hw.usermem 2>/dev/null |
| 39 | +swapctl -l; echo "ramprobe-swap-exit=$?" |
| 40 | +df -m /tmp /root |
| 41 | + |
| 42 | +echo "=== FETCH ===" |
| 43 | +f1() { # $1=out $2=url ; 3 tries |
| 44 | + n=0 |
| 45 | + while [ "$n" -lt 3 ]; do |
| 46 | + ftp -o "$1" "$2" && return 0 |
| 47 | + n=$((n+1)); sleep 10 |
| 48 | + done |
| 49 | + echo "FETCH-FAILED $2"; return 1 |
| 50 | +} |
| 51 | +f1 qjs.tgz "$H/vendor/dist/quickjs-ng-v0.15.1.tar.gz"; echo "fetch-qjs-src-exit=$?" |
| 52 | +f1 exepath.patch "$H/patches/quickjs-ng-js_exepath-netbsd.patch"; echo "fetch-patch-exit=$?" |
| 53 | +# run-3 root cause (gdb: SIGBUS `ldd` at quickjs.c:38005, %g1 4-mod-8): |
| 54 | +# JSFunctionBytecode's trailing JSValue cpool array sits at sizeof(*b), |
| 55 | +# which is 4-mod-8 on sparc32 — 8-byte nan-boxed loads fault. Patch rounds |
| 56 | +# cpool_offset to 8 at both layout sites (writer + reader); host-validated. |
| 57 | +f1 cpool-align.patch "$H/vendor/dist/quickjs-ng-cpool-align.diff"; echo "fetch-patch2-exit=$?" |
| 58 | +f1 probe.js "$H/probe.js"; echo "fetch-probe-exit=$?" |
| 59 | +f1 cli.cjs "$H/vendor/dist/cli.cjs"; echo "fetch-cli-exit=$?" |
| 60 | +wc -c qjs.tgz exepath.patch cpool-align.patch probe.js cli.cjs |
| 61 | + |
| 62 | +echo "=== BUILD-QJS ===" |
| 63 | +date |
| 64 | +tar xzf qjs.tgz && mv quickjs-0.15.1 qjs-src |
| 65 | +cd "$W/qjs-src" || exit 1 |
| 66 | +patch -p1 < ../exepath.patch; echo "s1-patch-exit=$?" |
| 67 | +patch -p1 < ../cpool-align.patch; echo "s1-patch2-exit=$?" |
| 68 | + |
| 69 | +# ccg: compile one file with an address-space cap so gcc FAILS FAST instead |
| 70 | +# of swap-thrashing for hours under TCG (512M guest; clang needed 336MB RSS |
| 71 | +# for quickjs.c -O2 on the host). Fallback ladder -O2 -> -O1 -> -O0; the |
| 72 | +# chosen level is a marker, not a failure. |
| 73 | +CFBASE="-std=gnu11 -funsigned-char -D_GNU_SOURCE -DQUICKJS_NG_BUILD -DNDEBUG -I." |
| 74 | +ccg() { # $1 = src (no .c), out $1.o |
| 75 | + for opt in -O2 -O1 -O0; do |
| 76 | + if (ulimit -v 409600; /usr/bin/time cc $opt $CFBASE -c "$1.c" -o "$1.o"); then |
| 77 | + echo "s1-cc-$1-opt=$opt" |
| 78 | + return 0 |
| 79 | + fi |
| 80 | + echo "s1-cc-$1-$opt-failed (retrying lower)" |
| 81 | + done |
| 82 | + echo "s1-cc-$1-FAILED-ALL" |
| 83 | + return 1 |
| 84 | +} |
| 85 | +FAIL=0 |
| 86 | +for f in quickjs libregexp libunicode dtoa quickjs-libc qjs qjsc; do |
| 87 | + ccg "$f" || FAIL=1 |
| 88 | +done |
| 89 | +LIBOBJS="quickjs.o libregexp.o libunicode.o dtoa.o quickjs-libc.o" |
| 90 | + |
| 91 | +# RUN-1 FINDING (2026-07-09): the tarball's gen/repl.c + gen/standalone.c |
| 92 | +# are qjsc bytecode PRE-GENERATED on upstream's little-endian 64-bit host; |
| 93 | +# JS_ReadObjectAtoms' host-order checksum can never match on BE, so |
| 94 | +# `qjs -c` (which JS_ReadObject's qjsc_standalone first) dies with |
| 95 | +# "SyntaxError: checksum error" (risk #3 biting the build inputs, not the |
| 96 | +# engine). Fix: link qjsc FIRST (qjsc.c has no gen/ dependency), |
| 97 | +# REGENERATE the gen files natively (= big-endian), then compile them and |
| 98 | +# link qjs. Regen commands are upstream's own (Makefile lines 77-78); |
| 99 | +# flow validated on the darwin host with the no-cmake objects. |
| 100 | +# |
| 101 | +# Link ladder (run-1: plain and -latomic both failed — NetBSD/sparc base |
| 102 | +# has no libatomic; shim WORKED): sparc32 (v8) __atomic_* are not |
| 103 | +# lock-free -> gcc emits libatomic calls. pthread-mutex shim preserves |
| 104 | +# semantics for these single-threaded gates. |
| 105 | +cat > atomic-shim.c <<'EOF' |
| 106 | +#include <pthread.h> |
| 107 | +#include <stdint.h> |
| 108 | +#include <stddef.h> |
| 109 | +static pthread_mutex_t L = PTHREAD_MUTEX_INITIALIZER; |
| 110 | +#define OPS(n, t) \ |
| 111 | +t __atomic_load_##n(const volatile void *p, int mo){ t v; pthread_mutex_lock(&L); v = *(const volatile t*)p; pthread_mutex_unlock(&L); return v; } \ |
| 112 | +void __atomic_store_##n(volatile void *p, t v, int mo){ pthread_mutex_lock(&L); *(volatile t*)p = v; pthread_mutex_unlock(&L); } \ |
| 113 | +t __atomic_exchange_##n(volatile void *p, t v, int mo){ t o; pthread_mutex_lock(&L); o = *(volatile t*)p; *(volatile t*)p = v; pthread_mutex_unlock(&L); return o; } \ |
| 114 | +_Bool __atomic_compare_exchange_##n(volatile void *p, void *e, t d, _Bool w, int s, int f){ _Bool r; pthread_mutex_lock(&L); t o = *(volatile t*)p; if (o == *(t*)e) { *(volatile t*)p = d; r = 1; } else { *(t*)e = o; r = 0; } pthread_mutex_unlock(&L); return r; } \ |
| 115 | +t __atomic_fetch_add_##n(volatile void *p, t v, int mo){ pthread_mutex_lock(&L); t o = *(volatile t*)p; *(volatile t*)p = o + v; pthread_mutex_unlock(&L); return o; } \ |
| 116 | +t __atomic_fetch_sub_##n(volatile void *p, t v, int mo){ pthread_mutex_lock(&L); t o = *(volatile t*)p; *(volatile t*)p = o - v; pthread_mutex_unlock(&L); return o; } \ |
| 117 | +t __atomic_fetch_and_##n(volatile void *p, t v, int mo){ pthread_mutex_lock(&L); t o = *(volatile t*)p; *(volatile t*)p = o & v; pthread_mutex_unlock(&L); return o; } \ |
| 118 | +t __atomic_fetch_or_##n(volatile void *p, t v, int mo){ pthread_mutex_lock(&L); t o = *(volatile t*)p; *(volatile t*)p = o | v; pthread_mutex_unlock(&L); return o; } \ |
| 119 | +t __atomic_fetch_xor_##n(volatile void *p, t v, int mo){ pthread_mutex_lock(&L); t o = *(volatile t*)p; *(volatile t*)p = o ^ v; pthread_mutex_unlock(&L); return o; } |
| 120 | +OPS(1, uint8_t) OPS(2, uint16_t) OPS(4, uint32_t) OPS(8, uint64_t) |
| 121 | +_Bool __atomic_is_lock_free(size_t sz, const volatile void *p){ (void)sz; (void)p; return 0; } |
| 122 | +EOF |
| 123 | +cc -O2 -c atomic-shim.c -o atomic-shim.o; echo "s1-shim-cc-exit=$?" |
| 124 | + |
| 125 | +linkbin() { # $1 = output, $2... = objects; ladder: plain -> -latomic -> shim |
| 126 | + out="$1"; shift |
| 127 | + if cc -o "$out" "$@" -lm -lpthread 2> "link-$out-1.err"; then |
| 128 | + echo "s1-link-$out-variant=plain" |
| 129 | + elif cc -o "$out" "$@" -lm -lpthread -latomic 2> "link-$out-2.err"; then |
| 130 | + echo "s1-link-$out-variant=latomic" |
| 131 | + elif cc -o "$out" "$@" atomic-shim.o -lm -lpthread 2> "link-$out-3.err"; then |
| 132 | + echo "s1-link-$out-variant=shim" |
| 133 | + else |
| 134 | + sed -n 1,10p "link-$out-1.err" "link-$out-3.err" |
| 135 | + echo "s1-link-$out-variant=none" |
| 136 | + return 1 |
| 137 | + fi |
| 138 | +} |
| 139 | + |
| 140 | +# 1. qjsc (no gen/ dependency) |
| 141 | +linkbin qjsc qjsc.o $LIBOBJS || FAIL=1 |
| 142 | +# 2. regenerate the gen bytecode natively (big-endian) |
| 143 | +./qjsc -ss -o gen/repl.c -m repl.js; echo "s1-regen-repl-exit=$?" |
| 144 | +./qjsc -ss -o gen/standalone.c -m standalone.js; echo "s1-regen-standalone-exit=$?" |
| 145 | +head -5 gen/standalone.c | tail -2 |
| 146 | +# 3. compile regenerated gen files, link qjs |
| 147 | +ccg gen/repl || FAIL=1 |
| 148 | +ccg gen/standalone || FAIL=1 |
| 149 | +QJSOBJS="qjs.o gen/repl.o gen/standalone.o" |
| 150 | +linkbin qjs $QJSOBJS $LIBOBJS || FAIL=1 |
| 151 | +[ -x ./qjs ] || FAIL=1 |
| 152 | +ls -l qjs qjsc |
| 153 | +echo "s1-build-qjs-exit=$FAIL" |
| 154 | +date |
| 155 | +Q="$W/qjs-src/qjs" |
| 156 | +cd "$W" || exit 1 |
| 157 | + |
| 158 | +echo "=== S3 (BE correctness micro-probes, bare qjs) ===" |
| 159 | +# 3.1 minimal life sign — if 32-bit-BE quickjs-ng is broken, fail here first |
| 160 | +(ulimit -t 900; "$Q" --eval 'print(1+1)' < /dev/null); echo "s3-1-print-exit=$?" |
| 161 | +# 3.2 endianness self-check: host order must be BIG; DataView explicit-endian |
| 162 | +(ulimit -t 900; "$Q" --eval 'const u8=new Uint8Array(new Uint16Array([1]).buffer);const b=new ArrayBuffer(4);const dv=new DataView(b);dv.setUint32(0,0x11223344,false);const v8=new Uint8Array(b);print("host-BE="+(u8[0]===0)+" dv-be-byte0=0x"+v8[0].toString(16)+" dv-le-read=0x"+dv.getUint32(0,true).toString(16));if(u8[0]!==0)throw new Error("host order not big-endian");if(v8[0]!==0x11)throw new Error("DataView BE write broken");if(dv.getUint32(0,true)!==0x44332211)throw new Error("DataView LE read broken")' < /dev/null); echo "s3-2-endian-exit=$?" |
| 163 | +# 3.3 regexp char classes + unicode (historically BE-buggy lre paths) |
| 164 | +(ulimit -t 900; "$Q" --eval 'if(!/[a-z]+/u.test("abc"))throw new Error("u-flag charclass");if(!/\p{L}/u.test("\u00e9"))throw new Error("p{L} u-flag");if("a1b2".replace(/[0-9]/g,"")!=="ab")throw new Error("global charclass");print("regexp-ok");try{print("vflag-informational="+/[\p{L}]/v.test("x"))}catch(e){print("vflag-informational-ERR "+e)}' < /dev/null); echo "s3-3-regexp-exit=$?" |
| 165 | +# 3.4 bytecode round-trip IN-GUEST (BE-produced, BE-consumed; js_exepath patch |
| 166 | +# is what makes the standalone actually run its payload on NetBSD) |
| 167 | +printf 'print("bc-ok",6*7);\n' > hello.js |
| 168 | +(ulimit -t 1800; "$Q" -c hello.js -o hello-exe && ./hello-exe < /dev/null); echo "s3-4-bytecode-exit=$?" |
| 169 | +# 3.5 dtoa / Date / JSON spot checks |
| 170 | +(ulimit -t 900; "$Q" --eval 'const s=(0.1+0.2).toString();print("sum="+s);if(s!=="0.30000000000000004")throw new Error("dtoa");const n=Date.now();print("now="+n);if(!(n>1600000000000&&n<9999999999999))throw new Error("Date.now");const j=JSON.parse("{\"a\":1e300}");print("j.a="+j.a);if(j.a!==1e300)throw new Error("json 1e300")' < /dev/null); echo "s3-5-dtoa-date-json-exit=$?" |
| 171 | +# 3.6 full capability probe (diff against gate2 darwin control) |
| 172 | +(ulimit -t 1800; "$Q" probe.js < /dev/null); echo "s3-6-probe-exit=$?" |
| 173 | +echo "s3-7-inventory=SKIPPED (inventory.cjs is node/tjs-only CommonJS; S2-prime scope)" |
| 174 | + |
| 175 | +echo "=== S0 (RAM-fit measurements; /usr/bin/time -l, NetBSD units = KB) ===" |
| 176 | +date |
| 177 | +# control: interpreter floor |
| 178 | +(ulimit -t 900; /usr/bin/time -l "$Q" --eval '1+1' < /dev/null > /dev/null); echo "s0-control-exit=$?" |
| 179 | +# ladder rung (a): run-from-source peak on the 19MB bundle. Crash at first |
| 180 | +# missing Node API is EXPECTED evidence; OOM/timeout is a DATA POINT. |
| 181 | +(ulimit -t 5400; /usr/bin/time -l "$Q" cli.cjs < /dev/null > /dev/null); echo "s0-run-from-source-exit=$?" |
| 182 | +# ladder rung (b/c) producer: compile peak (bytecode-embedding standalone) |
| 183 | +(ulimit -t 5400; /usr/bin/time -l "$Q" -c cli.cjs -o /tmp/bundle-exe < /dev/null); echo "s0-compile-exit=$?" |
| 184 | +ls -l /tmp/bundle-exe 2>/dev/null |
| 185 | +# ladder rung (b/c) consumer: run-from-bytecode peak |
| 186 | +if [ -x /tmp/bundle-exe ]; then |
| 187 | + (ulimit -t 1800; /usr/bin/time -l /tmp/bundle-exe < /dev/null > /dev/null); echo "s0-run-from-bytecode-exit=$?" |
| 188 | +else |
| 189 | + echo "s0-run-from-bytecode-exit=SKIP (no /tmp/bundle-exe)" |
| 190 | +fi |
| 191 | +date |
| 192 | +echo "=== GUEST-DONE ===" |
0 commit comments