Files
velzieandClaude Opus 5.5 3329b69bf7 lift-x86: the rest of SSE4.1 and SSE4.2, and PCLMULQDQ
phminposuw, dpps, dppd, mpsadbw, crc32 (all widths), the four
pcmpXstrX with every immediate (formats, aggregations, polarities,
index and mask forms, the flags) and pclmulqdq were SIGILL. They are
lifted now, in sse/sse4.rs. dpps and dppd sum in the hardware's order
and keep the NaN an x86 add keeps (the first operand's, quieted),
spelled out so the function tier's C gives the same NaN. pcmpestrX
take their lengths from RAX and RDX under REX.W, EAX and EDX
otherwise.

test/fixtures/x86-programs/sse4.c runs SSSE3, SSE4.1, SSE4.2 and
PCLMULQDQ over pseudo-random inputs and every immediate, and compares
a hash per group with the one a real CPU gave, so the native and page
legs check it with no dependency on the host's CPU (programs.mjs's
SELF). The native legs take GEX_TEST_ONLY as the page legs do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_853378b774d645b4ac78b16d9af91a31
2026-11-12 10:59:11 -05:00

160 lines
8.7 KiB
JavaScript

// x86 guest programs whose behaviour is checked against native execution
// (fixtures/x86-programs): each is built static with zig for x86-64 and
// i386 musl, run natively and under gexc, and its stdout and exit status
// must match. What each program checks is in its source; how it is run is
// here:
// - mem64-*: a 64-bit guest memory (gexc run --mem64), x86-64 only;
// sig-regs-threads x86-64 only (its loops are assembly);
// - SELF's programs check themselves against results a real CPU gave
// (sse4: SSSE3, SSE4.1, SSE4.2 and PCLMULQDQ), so no native run, which
// the host's CPU might not have: their output is NAME ok; x86-64 only,
// under tier-1, the function JIT and the function tier's C;
// - *smc*, copy-loop, null-access and sig-regs-threads under the function
// JIT as well (--fjit, which must leave writable-and-executable code to
// tier-1's guards); fp-modes under the function tier's C too (--cgen);
// - the page tier: the x86-64 build under the browser shim in Node
// (run-node.mjs, the memory64 shared seed) for PAGE's programs;
// spawn-threads and exec-dethread in the page only (natively gexc's
// posix_spawn says ENOENT);
// - mem64-net reaches a server outside the guest (mem64-net-server.mjs,
// started once per test, its port argv[1]); threads-many runs its first
// round only under a task cap below 1024, and once more natively under
// a cap of 160 (the host refusing a thread is EAGAIN to the guest);
// - smc-race waits between decoding a block and fixing its guard
// (GEX_TEST_LIFT_DELAY_US), null-access needs the null guard (a debug
// opt-in, GEX_NULL_GUARD), fp-modes SSE under MXCSR (GEX_MXCSR), mem64-net
// the page's network of real TCP sockets (GEX_NETWORK);
// - in the page, page-cross-entry with GEX_TEST_UNPUBLISHED_RETIRE (a
// retired page numbered with no directory entry), dense-holes with
// GEX_TEST_DENSE_HOLES (its stderr must say a hole reached the miss
// stub), served-entries with GEX_TEST_SERVE_UNMERGED (it and its VAL=2
// build, as ARGV0-b), fork-dirs with no "gex: heal" line.
import { spawn } from 'node:child_process';
import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync } from 'node:fs';
import { join } from 'node:path';
import { guest, ROOT } from '../lib/guest.mjs';
export const FIX = join(ROOT, 'test/fixtures/x86-programs');
export const PROGRAMS = readdirSync(FIX).filter((f) => f.endsWith('.c') && !f.startsWith('glibc-')).map((f) => f.slice(0, -2)).sort();
export const PAGE = new Set(`fork-dirs dense-lazy dense-holes spawn-threads served-entries page-cross-entry tgkill-futex exec-tail mem64-io mem64-net mem64-console mem64-smc
mem64-reserve retire-during-lift close-while-reading thread-signals signal-storm exec-signals clock-monotonic main-thread-exit pi-mutex tcp-loopback pwait-sigmask
sa-restart thread-churn kernel-contend exec-futex sleep-threads sendmsg-rights scm-fork ioctl-threads fdtable-threads mmap-threads codemap-threads proc-threads
fs-threads sig-state-threads lift-threads fdmisc-threads task-ids sock-threads exec-dethread fork-thread-ctx fork-maps-threads fork-twice-thread quiet-threads
fdobj-threads cred-threads kill-threads sigwait-threads poll-timeout tid-threads mount-threads exit-threads many-workers threads-many stale-loop-head file-stale-loop
stale-cross-page xattr mem64-wx mmap-coherent mmap-remap mmap-writeback null-access sig-regs-threads sig-pause fp-modes sse4`.split(/\s+/));
const PAGE_ONLY = new Set(['spawn-threads', 'exec-dethread']);
/** Programs that check themselves (no native run). */
export const SELF = new Set(['sse4']);
/** NAME's targets and native tiers (t1, fjit, cgen). */
export function plan(name) {
const targets = name.startsWith('mem64-') || name === 'sig-regs-threads' || SELF.has(name) ? ['x86_64-linux-musl'] : ['x86_64-linux-musl', 'x86-linux-musl'];
let tiers = ['t1'];
if (/smc/.test(name) || ['copy-loop', 'null-access', 'sig-regs-threads'].includes(name)) tiers = ['t1', 'fjit'];
if (name === 'fp-modes' || SELF.has(name)) tiers = ['t1', 'fjit', 'cgen'];
if (PAGE_ONLY.has(name)) tiers = [];
return { targets, tiers, mem64: name.startsWith('mem64-') };
}
/** The guest environment NAME runs with (`page`: on the page). */
export function env(name, page) {
const e = {};
if (name === 'smc-race') e.GEX_TEST_LIFT_DELAY_US = '300';
if (name === 'null-access') e.GEX_NULL_GUARD = '1';
if (name === 'fp-modes') e.GEX_MXCSR = '1';
if (name === 'mem64-net') e.GEX_NETWORK = '1';
if (page && name === 'page-cross-entry') e.GEX_TEST_UNPUBLISHED_RETIRE = '1';
if (page && name === 'dense-holes') e.GEX_TEST_DENSE_HOLES = '1';
if (page && name === 'served-entries') e.GEX_TEST_SERVE_UNMERGED = '1';
return e;
}
export function x86Programs(t) {
const g = guest(t);
const zig = g.tool('zig');
const dir = join(t.workDir, 'x86-programs', 'bin');
mkdirSync(dir, { recursive: true });
const { NODE_CHANNEL_FD, NODE_CHANNEL_SERIALIZATION_MODE, ...base } = process.env;
// Output is compared with the native run: not even gex's default lines.
const hostEnv = { ...base, GEX_LOG: 'off' };
let server = null;
const builds = {};
const x = {
g,
dir,
hostEnv,
/** NAME built for TARGET (VAL=2 too as -b for served-entries), once per work directory and source. */
build(name, target) {
const src = join(FIX, `${name}.c`);
const key = createHash('sha256').update(readFileSync(src)).digest('hex').slice(0, 12);
const bin = join(dir, `${name}-${target}-${key}`);
const one = async (out, more = []) => {
if (existsSync(out)) return;
const tmp = `${out}.tmp-${process.pid}`;
const r = await x.exec([zig, 'cc', '-target', target, '-O1', '-static', ...(target === 'x86-linux-musl' ? ['-Wl,--image-base=0x1000000'] : []), ...more, '-o', tmp, src], { timeout: 300000 });
if (r.code !== 0) throw new Error(`zig cc ${name} (${target}): ${r.err.trim().split('\n').slice(-3).join(' / ')}`);
renameSync(tmp, out);
};
builds[bin] ??= (async () => {
await one(bin);
if (name === 'served-entries') await one(`${bin}-b`, ['-DVAL=2']);
return bin;
})();
return builds[bin];
},
/** NAME's run on the host (`argv`), or for SELF's what it prints when right. */
native(name, argv) {
return SELF.has(name) ? Promise.resolve({ code: 0, out: `${name} ok`, err: '' }) : x.exec(argv);
},
/** `argv` as a process: `{ code, out, err }` (stdout, stderr), `code` as a shell gives it. */
exec(argv, { env: more = {}, timeout = 120000 } = {}) {
return new Promise((resolve) => {
const p = spawn(argv[0], argv.slice(1), { cwd: ROOT, env: { ...hostEnv, ...more }, stdio: ['ignore', 'pipe', 'pipe'] });
const out = [], err = [];
p.stdout.on('data', (d) => out.push(d));
p.stderr.on('data', (d) => err.push(d));
const timer = setTimeout(() => p.kill('SIGKILL'), timeout);
p.on('close', (code, signal) => {
clearTimeout(timer);
const sig = { SIGABRT: 6, SIGKILL: 9, SIGSEGV: 11, SIGBUS: 7, SIGILL: 4, SIGTERM: 15, SIGFPE: 8 }[signal];
resolve({ code: code ?? 128 + (sig ?? 0), out: Buffer.concat(out).toString().replace(/\n+$/, ''), err: Buffer.concat(err).toString() });
});
});
},
/** NAME's arguments: mem64-net's server port (the server started once), threads-many's round. */
async args(name) {
if (name === 'mem64-net') {
if (!server) {
const p = spawn(process.execPath, [join(FIX, 'mem64-net-server.mjs')], { stdio: ['ignore', 'pipe', 'ignore'] });
server = { p, port: new Promise((resolve) => p.stdout.once('data', (d) => resolve(String(d).split('\n')[0].trim()))) };
}
return [await server.port];
}
if (name === 'threads-many') {
try {
const cg = readFileSync('/proc/self/cgroup', 'utf8').match(/^0::(.*)$/m)?.[1];
const max = readFileSync(`/sys/fs/cgroup${cg}/pids.max`, 'utf8').trim();
if (max !== 'max' && Number(max) < 1024) return ['first'];
} catch {
// no cgroup v2 limit to read: the full run
}
}
return [];
},
/** The server stopped (at the test's end). */
done() {
server?.p.kill();
},
};
return x;
}
/** The first lines where `a` and `b` differ, for a FAIL's detail. */
export function differ(a, b) {
const x = a.split('\n'), y = b.split('\n');
const i = x.findIndex((l, k) => l !== y[k]);
return i < 0 && x.length === y.length ? 'same output' : `line ${(i < 0 ? x.length : i) + 1}: native ${JSON.stringify(x[i] ?? '(end)')}, gex ${JSON.stringify(y[i < 0 ? x.length : i] ?? '(end)')}`;
}