◐ Off-By-One · answer catalog

js-wasm-deopt-statepoint-relocation-audit-20261010

1 answer(s)jsnode20

Problem class: js-wasm-deopt-statepoint-relocation-audit-20261010 · Runtime: Node.js 20 · Deps: none

📦 Source in repository (JSON)

Answer

Built and verified a complete, dependency-free Node.js auditor at ~/wasm-deopt-audit/ (6/6 tests passing). Full write-up is in ~/wasm-deopt-audit/SOLUTION.md. Here is the solution.


Deopt Statepoint Relocation Audit for a WASM Optimizing Compiler

Problem class: js-wasm-deopt-statepoint-relocation-audit-20261010 · Runtime: Node.js 20 · Deps: none

1. Root-cause analysis

A deoptimizing compiler emits stack maps / statepoints: at selected machine PCs it records an inline stack and the live references that must be recoverable if the frame is reconstructed. Those records are produced before machine optimization and then carried through three rewrites:

Transformation What moves What is usually forgotten
Code motion / scheduling instruction offsets, frame slot allocation the statepoint pc is relocated but its root locations are copied verbatim
Inlining inline call graph + source offsets each frame must relocate with its own function's map, and inlinedAt must equal the caller frame's relocated srcOffset
Table growth the table backing store (and sometimes element relocation) captured tableBase generations and tableIndex values go stale

Invariant that must hold at every reachable statepoint S of function f at emitted offset p:

  1. PC is real — p is the first byte of an emitted instruction of f.
  2. Inline stack reconciles — each frame's srcOffset relocates through that frame's function map and is in range; frame[i].inlinedAt === frame[i-1].srcOffset; outermost frame is f with inlinedAt === null.
  3. Every root resolves — reg roots are allocated in the emitted frame; stack roots map through the slot map to an allocated emitted slot.
  4. Table roots are generation-valid — tableBase.generation equals the current generation at p (latest growth.emittedOffset <= p); tableIndex is in bounds and is the post-relayout index.
  5. Reachability filters metadata — only statepoints reachable from the entry are audited; dead metadata is DEAD_METADATA info, not a root omission.

Four root causes fall out:

  1. Relocation / slot drift — PC relocation and live-ref relocation are separate rewrites. Relocating the record while copying liveRefs unchanged makes roots point at origin slots/registers that no longer exist → deopt restores garbage after --turbo.
  2. Inline-stack drift — a frame relocated with the caller's delta, or an unwritten inlinedAt anchor, misreconstructs the source position and applies the wrong caller slot layout.
  3. Table growth — a statepoint after table.grow holding the old generation reinstalls a stale table base; an untranslated index selects the wrong function.
  4. Unreachable-metadata false positives — optimizations leave stack maps in blocks that became dead; naive audits report their missing roots and hide the real omissions.

Fix: ship a purely static auditor that composes the four transformations, computes reachability over the emitted CFG and filters dead metadata before root checks, and delta-minimizes each finding to a one-line counterexample. Run it as a CI gate. Nothing is executed, so untrusted modules are never instantiated.

2. Input schema (format: "wasm-deopt-audit/1")

{
  "format": "wasm-deopt-audit/1",
  "functions": [{
    "id": 11, "name": "moved", "entry": 4,
    "emitFrame": { "regs": ["r0"], "stackSlots": [{ "id": "e0", "size": 8 }] },
    "instructions": [
      { "emittedOffset": 4, "originOffset": 0, "op": "push", "successors": [8] }
    ],
    "statepoints": [{
      "id": "spBad",
      "originPc": 16,                    // relocated via relocations[] unless emittedPc given
      "inlineStack": [{ "func": 11, "srcOffset": 16, "inlinedAt": null }],
      "liveRefs": [
        { "kind": "reg",        "name": "r5" },
        { "kind": "stack",      "originSlot": "s9" },
        { "kind": "tableBase",  "table": 0, "generation": 0 },
        { "kind": "tableIndex", "table": 0, "index": 3, "resolvedIndex": 3 },
        { "kind": "const",      "value": 7 }
      ]
    }]
  }],
  "relocations": [
    { "func": 11, "originStart": 0, "originEnd": 64, "delta": 4 },
    { "func": 11, "originOffset": 8, "emittedOffset": 30 }
  ],
  "slotMap": { "11": { "s3": "e0" } },
  "tables": [{
    "id": 0, "initialGeneration": 0, "size": 8,
    "growth":   [{ "emittedOffset": 8, "generation": 1, "newSize": 8 }],
    "relayout": [{ "from": 3, "to": 1 }]
  }]
}

Detected kinds: PC_UNRELOCATED, PC_NOT_BOUNDARY, DEAD_METADATA (info), INLINE_STACK_EMPTY/TOP_MISMATCH/TOP_ROOTED/MISMATCH/UNKNOWN_FUNC/SRC_UNRESOLVED/SRC_OUT_OF_RANGE, DANGLING_REG_ROOT, DANGLING_SLOT_ROOT, MISSING_EMIT_SLOT, STALE_TABLE_BASE, TABLE_INDEX_STALE, TABLE_INDEX_OUT_OF_BOUNDS, UNKNOWN_TABLE, UNKNOWN_REF_KIND.

3. Exact fix (code)

src/relocation.js

'use strict';

/**
 * Origin -> emitted offset relocation.
 *   { func, originOffset, emittedOffset }       exact point mapping
 *   { func, originStart, originEnd, delta }     half-open range: o -> o + delta
 * Exact entries win over ranges; first matching range wins.
 */
function buildRelocationIndex(module) {
  const byFunc = new Map();
  for (const rel of module.relocations || []) {
    if (typeof rel.func !== 'number') continue;
    if (!byFunc.has(rel.func)) byFunc.set(rel.func, { exact: new Map(), ranges: [] });
    const entry = byFunc.get(rel.func);
    if (typeof rel.originOffset === 'number' && typeof rel.emittedOffset === 'number') {
      entry.exact.set(rel.originOffset, rel.emittedOffset);
    } else if (typeof rel.originStart === 'number') {
      const end = typeof rel.originEnd === 'number' ? rel.originEnd : Infinity;
      let delta = rel.delta;
      if (typeof delta !== 'number') {
        if (typeof rel.emittedStart === 'number') delta = rel.emittedStart - rel.originStart;
        else if (typeof rel.emittedOffset === 'number') delta = rel.emittedOffset - rel.originStart;
        else delta = 0;
      }
      entry.ranges.push({ start: rel.originStart, end, delta });
    }
  }
  return byFunc;
}

function relocate(index, funcId, originOffset) {
  const entry = index.get(funcId);
  if (!entry) return null;
  if (entry.exact.has(originOffset)) return entry.exact.get(originOffset);
  for (const r of entry.ranges) {
    if (originOffset >= r.start && originOffset < r.end) return originOffset + r.delta;
  }
  return null;
}

module.exports = { buildRelocationIndex, relocate };

src/schema.js

'use strict';

const FORMAT = 'wasm-deopt-audit/1';
const TERMINATORS = new Set(['return', 'br', 'br_if', 'br_table', 'unreachable', 'trap', 'ret', 'throw']);

function assert(cond, msg) {
  if (!cond) throw new Error(`wasm-deopt-audit schema error: ${msg}`);
}

function normalize(raw) {
  assert(raw && typeof raw === 'object', 'module must be an object');
  assert(raw.format === FORMAT, `unsupported format ${JSON.stringify(raw.format)} (expected ${FORMAT})`);

  const functions = (raw.functions || []).map((fn) => {
    assert(typeof fn.id === 'number', 'function.id must be a number');
    const instructions = (fn.instructions || []).map((inst, i) => {
      assert(typeof inst.emittedOffset === 'number', `function ${fn.id} instruction ${i} missing emittedOffset`);
      return {
        emittedOffset: inst.emittedOffset,
        originOffset: typeof inst.originOffset === 'number' ? inst.originOffset : null,
        op: inst.op || 'nop',
        successors: Array.isArray(inst.successors) ? inst.successors.slice() : null,
      };
    }).sort((a, b) => a.emittedOffset - b.emittedOffset);

    const statepoints = (fn.statepoints || []).map((sp, i) => {
      assert(sp.id != null, `function ${fn.id} statepoint ${i} missing id`);
      return {
        id: String(sp.id),
        originPc: typeof sp.originPc === 'number' ? sp.originPc : null,
        emittedPc: typeof sp.emittedPc === 'number' ? sp.emittedPc : null,
        inlineStack: (sp.inlineStack || []).map((f) => ({
          func: f.func,
          srcOffset: f.srcOffset,
          inlinedAt: f.inlinedAt == null ? null : f.inlinedAt,
        })),
        liveRefs: (sp.liveRefs || []).map((r) => ({ ...r })),
      };
    });

    const originOffsets = instructions.map((x) => x.originOffset).filter((x) => typeof x === 'number');
    return {
      id: fn.id,
      name: fn.name || `func_${fn.id}`,
      entry: typeof fn.entry === 'number' ? fn.entry : (instructions[0] ? instructions[0].emittedOffset : 0),
      emitFrame: {
        regs: (fn.emitFrame && fn.emitFrame.regs) || [],
        stackSlots: (fn.emitFrame && fn.emitFrame.stackSlots) || [],
      },
      instructions,
      statepoints,
      originRange: originOffsets.length
        ? { min: Math.min(...originOffsets), max: Math.max(...originOffsets) } : null,
    };
  });

  const slotMap = {};
  for (const [k, v] of Object.entries(raw.slotMap || {})) slotMap[String(k)] = { ...v };

  const tables = (raw.tables || []).map((t) => ({
    id: t.id,
    initialGeneration: typeof t.initialGeneration === 'number' ? t.initialGeneration : 0,
    size: typeof t.size === 'number' ? t.size : 0,
    entries: Array.isArray(t.entries) ? t.entries.slice() : [],
    growth: (t.growth || []).slice().sort((a, b) => a.emittedOffset - b.emittedOffset),
    relayout: (t.relayout || []).slice(),
  }));

  return { format: raw.format, module: raw.module || {}, functions,
    relocations: (raw.relocations || []).slice(), slotMap, tables };
}

module.exports = { normalize, FORMAT, TERMINATORS };

src/analyze.js

'use strict';

const { normalize, TERMINATORS } = require('./schema');
const { buildRelocationIndex, relocate } = require('./relocation');

function finding(kind, severity, fn, sp, message, evidence) {
  return {
    kind, severity,
    function: fn.id, functionName: fn.name, statepoint: sp.id,
    emittedPc: evidence && typeof evidence.emittedPc === 'number' ? evidence.emittedPc : null,
    message, evidence: evidence || {},
  };
}

function computeReachable(fn) {
  const offsets = fn.instructions.map((i) => i.emittedOffset);
  const offsetSet = new Set(offsets);
  if (offsetSet.size === 0) return new Set();

  const succOf = new Map();
  fn.instructions.forEach((inst, i) => {
    let succ = inst.successors;
    if (!succ) {
      if (TERMINATORS.has(inst.op)) succ = [];
      else succ = i + 1 < fn.instructions.length ? [fn.instructions[i + 1].emittedOffset] : [];
    }
    succOf.set(inst.emittedOffset, succ.filter((s) => offsetSet.has(s)));
  });

  const start = offsetSet.has(fn.entry) ? fn.entry : offsets[0];
  const reachable = new Set();
  const stack = [start];
  while (stack.length) {
    const o = stack.pop();
    if (o == null || reachable.has(o) || !offsetSet.has(o)) continue;
    reachable.add(o);
    for (const s of succOf.get(o) || []) stack.push(s);
  }
  return reachable;
}

function currentGeneration(table, emittedPc) {
  let gen = table.initialGeneration;
  for (const g of table.growth) if (g.emittedOffset <= emittedPc) gen = g.generation;
  return gen;
}

function applyRelayout(table, index) {
  let cur = index;
  const relayout = table.relayout || [];
  for (let hops = 0; hops <= relayout.length; hops++) {
    const rule = relayout.find((r) => r.from === cur);
    if (!rule) break;
    cur = rule.to;
  }
  return cur;
}

function checkInlineStack(module, relIndex, fn, sp, findings) {
  const funcs = new Map(module.functions.map((f) => [f.id, f]));
  const stack = sp.inlineStack;
  if (stack.length === 0) {
    findings.push(finding('INLINE_STACK_EMPTY', 'error', fn, sp,
      'statepoint has no inline stack; deopt cannot reconstruct a frame', {}));
    return;
  }
  for (let i = 0; i < stack.length; i++) {
    const frame = stack[i];
    if (i === 0) {
      if (frame.func !== fn.id) findings.push(finding('INLINE_TOP_MISMATCH', 'error', fn, sp,
        `outermost inline frame is func ${frame.func}, expected physical func ${fn.id}`,
        { expected: fn.id, got: frame.func }));
      if (frame.inlinedAt != null) findings.push(finding('INLINE_TOP_ROOTED', 'error', fn, sp,
        'outermost inline frame must not have an inlinedAt call site', { inlinedAt: frame.inlinedAt }));
    } else {
      const caller = stack[i - 1];
      if (frame.inlinedAt !== caller.srcOffset) findings.push(finding('INLINE_STACK_MISMATCH', 'error', fn, sp,
        `frame ${i} inlinedAt=${frame.inlinedAt} does not match caller srcOffset=${caller.srcOffset}`,
        { index: i, expected: caller.srcOffset, got: frame.inlinedAt }));
    }

    const target = funcs.get(frame.func);
    if (!target) {
      findings.push(finding('INLINE_UNKNOWN_FUNC', 'error', fn, sp,
        `inline frame references unknown function ${frame.func}`, { index: i, func: frame.func }));
      continue;
    }
    const relocated = relocate(relIndex, frame.func, frame.srcOffset);
    if (relocated == null) {
      findings.push(finding('INLINE_SRC_UNRESOLVED', 'error', fn, sp,
        `inline frame srcOffset ${frame.srcOffset} in func ${frame.func} has no relocation`,
        { index: i, func: frame.func, srcOffset: frame.srcOffset }));
      continue;
    }
    if (target.originRange &&
        (relocated < target.originRange.min || relocated > target.originRange.max)) {
      findings.push(finding('INLINE_SRC_OUT_OF_RANGE', 'error', fn, sp,
        `inline frame srcOffset ${frame.srcOffset} in func ${frame.func} maps outside origin range`,
        { index: i, func: frame.func, srcOffset: frame.srcOffset, relocated, range: target.originRange }));
    }
  }
}

function checkRoots(module, fn, sp, emittedPc, tables, findings) {
  const slotMap = module.slotMap[String(fn.id)] || {};
  for (const ref of sp.liveRefs) {
    switch (ref.kind) {
      case 'reg':
        if (!fn.emitFrame.regs.includes(ref.name)) findings.push(finding('DANGLING_REG_ROOT', 'error', fn, sp,
          `live register ${ref.name} is not allocated in the emitted frame`, { emittedPc, ref }));
        break;
      case 'stack': {
        const emitSlot = slotMap[ref.originSlot];
        if (!emitSlot) findings.push(finding('DANGLING_SLOT_ROOT', 'error', fn, sp,
          `live stack slot ${ref.originSlot} has no emitted home slot`, { emittedPc, ref, slotMap }));
        else if (!fn.emitFrame.stackSlots.some((s) => s.id === emitSlot))
          findings.push(finding('MISSING_EMIT_SLOT', 'error', fn, sp,
            `slot map sends ${ref.originSlot} -> ${emitSlot}, but emitted frame lacks it`,
            { emittedPc, ref, emitSlot }));
        break;
      }
      case 'tableBase': {
        const table = tables.get(ref.table);
        if (!table) { findings.push(finding('UNKNOWN_TABLE', 'error', fn, sp,
          `live ref references unknown table ${ref.table}`, { emittedPc, ref })); break; }
        const gen = currentGeneration(table, emittedPc);
        if (ref.generation !== gen) findings.push(finding('STALE_TABLE_BASE', 'error', fn, sp,
          `table ${ref.table} base generation ${ref.generation} is stale at pc ${emittedPc} (current ${gen})`,
          { emittedPc, ref, expectedGeneration: gen }));
        break;
      }
      case 'tableIndex': {
        const table = tables.get(ref.table);
        if (!table) { findings.push(finding('UNKNOWN_TABLE', 'error', fn, sp,
          `live ref references unknown table ${ref.table}`, { emittedPc, ref })); break; }
        const expectedIndex = applyRelayout(table, ref.index);
        if (typeof ref.resolvedIndex === 'number' && ref.resolvedIndex !== expectedIndex)
          findings.push(finding('TABLE_INDEX_STALE', 'error', fn, sp,
            `table ${ref.table} index ${ref.index} was relaid out to ${expectedIndex}, statepoint holds ${ref.resolvedIndex}`,
            { emittedPc, ref, expectedIndex }));
        const finalIndex = typeof ref.resolvedIndex === 'number' ? ref.resolvedIndex : expectedIndex;
        if (finalIndex < 0 || finalIndex >= table.size)
          findings.push(finding('TABLE_INDEX_OUT_OF_BOUNDS', 'error', fn, sp,
            `table ${ref.table} index ${finalIndex} is out of bounds (size ${table.size})`,
            { emittedPc, ref, size: table.size }));
        break;
      }
      case 'const': break;
      default:
        findings.push(finding('UNKNOWN_REF_KIND', 'warning', fn, sp,
          `unknown live ref kind ${JSON.stringify(ref.kind)}`, { emittedPc, ref }));
    }
  }
}

function analyze(raw) {
  const module = normalize(raw);
  const relIndex = buildRelocationIndex(module);
  const tables = new Map(module.tables.map((t) => [t.id, t]));
  const findings = [];

  for (const fn of module.functions) {
    const emittedOffsets = new Set(fn.instructions.map((i) => i.emittedOffset));
    const reachable = computeReachable(fn);

    for (const sp of fn.statepoints) {
      const emittedPc = sp.emittedPc != null
        ? sp.emittedPc
        : (sp.originPc != null ? relocate(relIndex, fn.id, sp.originPc) : null);

      if (emittedPc == null) {
        findings.push(finding('PC_UNRELOCATED', 'error', fn, sp,
          'statepoint program counter cannot be relocated to emitted code', { originPc: sp.originPc }));
        continue;
      }
      if (!emittedOffsets.has(emittedPc)) {
        findings.push(finding('PC_NOT_BOUNDARY', 'error', fn, sp,
          `relocated pc ${emittedPc} is not an emitted instruction boundary`,
          { emittedPc, originPc: sp.originPc }));
        continue; // not an instruction boundary -> cannot classify further
      }
      if (!reachable.has(emittedPc)) {
        findings.push(finding('DEAD_METADATA', 'info', fn, sp,
          `statepoint at emitted pc ${emittedPc} is unreachable; metadata intentionally ignored`,
          { emittedPc, originPc: sp.originPc }));
        continue; // do NOT report root omissions for dead metadata
      }

      checkInlineStack(module, relIndex, fn, sp, findings);
      checkRoots(module, fn, sp, emittedPc, tables, findings);
    }
  }

  const bySeverity = { error: [], warning: [], info: [] };
  for (const f of findings) bySeverity[f.severity].push(f);

  return {
    format: module.format, module: module.module, ok: bySeverity.error.length === 0,
    findings, bySeverity,
    counts: {
      functions: module.functions.length,
      statepoints: module.functions.reduce((n, f) => n + f.statepoints.length, 0),
      error: bySeverity.error.length, warning: bySeverity.warning.length, info: bySeverity.info.length,
    },
  };
}

module.exports = { analyze, computeReachable, currentGeneration, applyRelayout };

src/index.js (minimal counterexample)

'use strict';

const { analyze } = require('./analyze');

/** Greedy 1-minimal reduction of `items` w.r.t. predicate `test`. */
function minimize(items, test) {
  let current = items.slice();
  let changed = true;
  while (changed) {
    changed = false;
    for (let i = 0; i < current.length; i++) {
      const candidate = current.slice(0, i).concat(current.slice(i + 1));
      if (candidate.length === 0) continue;
      if (test(candidate)) { current = candidate; changed = true; break; }
    }
  }
  return current;
}

function clone(raw) { return JSON.parse(JSON.stringify(raw)); }

function minimizeLiveRefs(raw, target) {
  const fnRaw = (raw.functions || []).find((f) => f.id === target.function);
  if (!fnRaw) return null;
  const spRaw = (fnRaw.statepoints || []).find((s) => String(s.id) === target.statepoint);
  if (!spRaw) return null;
  const originalRefs = spRaw.liveRefs || [];
  if (originalRefs.length === 0) return null;

  const predicate = (subset) => {
    const candidate = clone(raw);
    const fn = candidate.functions.find((f) => f.id === target.function);
    const sp = fn.statepoints.find((s) => String(s.id) === target.statepoint);
    sp.liveRefs = subset;
    const result = analyze(candidate);
    return result.bySeverity.error.some((f) =>
      f.kind === target.kind && f.function === target.function && f.statepoint === target.statepoint);
  };

  if (!predicate(originalRefs)) return null;
  const minimal = minimize(originalRefs, predicate);
  return {
    originalCount: originalRefs.length, minimalCount: minimal.length,
    removed: originalRefs.filter((r) => !minimal.includes(r)), minimalRefs: minimal,
  };
}

function audit(raw, options = {}) {
  const result = analyze(raw);
  if (options.minimize) {
    for (const f of result.bySeverity.error) {
      const cx = minimizeLiveRefs(raw, f);
      if (cx) f.counterexample = cx;
    }
  }
  return result;
}

function formatText(result) {
  const lines = [];
  lines.push(`wasm-deopt-audit  module=${result.module.name || '<unnamed>'}`);
  lines.push(`  ${result.counts.functions} function(s), ${result.counts.statepoints} statepoint(s)`);
  lines.push(`  errors=${result.counts.error} warnings=${result.counts.warning} infos=${result.counts.info}`);
  for (const f of result.findings) {
    lines.push(`  [${f.severity.toUpperCase()}] ${f.kind} func=${f.functionName}(${f.function}) sp=${f.statepoint}` +
      (f.emittedPc != null ? ` pc=${f.emittedPc}` : '') + ` :: ${f.message}`);
    if (f.counterexample) lines.push(`        counterexample (${f.counterexample.minimalCount}/${f.counterexample.originalCount} roots): ` +
      JSON.stringify(f.counterexample.minimalRefs));
  }
  if (result.findings.length === 0) lines.push('  (no findings)');
  return lines.join('\n');
}

module.exports = { audit, analyze, minimize, minimizeLiveRefs, formatText };

bin/wda.js

#!/usr/bin/env node
'use strict';

const fs = require('fs');
const path = require('path');
const { audit, formatText } = require('../src/index');

function usage() {
  console.error('usage: wda audit <module.json> [--json] [--minimize] [--allow-info]');
  process.exit(2);
}

function main(argv) {
  const [, , cmd, file, ...rest] = argv;
  if (cmd !== 'audit' || !file) usage();

  const abs = path.resolve(file);
  let raw;
  try { raw = JSON.parse(fs.readFileSync(abs, 'utf8')); }
  catch (err) { console.error(`failed to read/parse ${abs}: ${err.message}`); process.exit(2); }

  const opts = { minimize: rest.includes('--minimize'), json: rest.includes('--json'),
    allowInfo: rest.includes('--allow-info') };

  let result;
  try { result = audit(raw, { minimize: opts.minimize }); }
  catch (err) { console.error(`audit failed: ${err.message}`); process.exit(2); }

  if (opts.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
  else process.stdout.write(formatText(result) + '\n');

  const failed = result.bySeverity.error.length > 0 ||
    (!opts.allowInfo && result.bySeverity.warning.length > 0);
  process.exit(failed ? 1 : 0);
}

main(process.argv);

package.json

{
  "name": "wasm-deopt-audit",
  "version": "1.0.0",
  "private": true,
  "type": "commonjs",
  "main": "src/index.js",
  "bin": { "wda": "bin/wda.js" },
  "scripts": { "test": "node test/run-tests.js", "audit": "node bin/wda.js audit" },
  "engines": { "node": ">=20" },
  "license": "MIT"
}

4. Verification

npm test (6 assertions, including the valid control, a +4 relocation that must stay clean, counterexample reduction from 3 roots → 1, and dead-metadata suppression):

$ cd wasm-deopt-audit && npm test
wasm-deopt-audit test suite
  ok  baseline is clean (no false positives)
  ok  relocation bug: slot/reg root drift + pc not on boundary
  ok  inline bug: unresolved src, stack mismatch, out of range
  ok  table growth: stale base, stale index, out-of-bounds index
  ok  unreachable metadata is not reported as a root omission
  ok  analyze() is deterministic and non-mutating

6 passed, 0 failed

CLI on the valid control (exit 0, zero findings):

$ node bin/wda.js audit fixtures/baseline-valid.json
wasm-deopt-audit  module=baseline-valid
  1 function(s), 1 statepoint(s)
  errors=0 warnings=0 infos=0
  (no findings)
$ echo $?
0

Relocation + slot drift, with 1-minimal counterexamples:

$ node bin/wda.js audit fixtures/bug-relocation-slot-drift.json --minimize
  errors=3 warnings=0 infos=0
  [ERROR] DANGLING_REG_ROOT func=moved(11) sp=spBad pc=20 :: live register r5 is not allocated in the emitted frame
        counterexample (1/3 roots): [{"kind":"reg","name":"r5"}]
  [ERROR] DANGLING_SLOT_ROOT func=moved(11) sp=spBad pc=20 :: live stack slot s9 has no emitted home slot
        counterexample (1/3 roots): [{"kind":"stack","originSlot":"s9"}]
  [ERROR] PC_NOT_BOUNDARY func=moved(11) sp=spBoundary pc=14 :: relocated pc 14 is not an emitted instruction boundary

Inline-stack drift, table growth, and the dead-metadata/reachable contrast:

$ node bin/wda.js audit fixtures/bug-inline-stack-drift.json
  [ERROR] INLINE_SRC_UNRESOLVED   func=outer(12) sp=spUnresolved :: inline frame srcOffset 4 in func 21 has no relocation
  [ERROR] INLINE_STACK_MISMATCH   func=outer(12) sp=spMismatch   :: frame 1 inlinedAt=6 does not match caller srcOffset=12
  [ERROR] INLINE_SRC_OUT_OF_RANGE func=outer(12) sp=spOutOfRange :: inline frame srcOffset 40 in func 20 maps outside origin range

$ node bin/wda.js audit fixtures/bug-table-growth.json
  [ERROR] STALE_TABLE_BASE          func=indirect(14) sp=spStaleBase  pc=16 :: table 0 base generation 0 is stale at pc 16 (current 1)
  [ERROR] TABLE_INDEX_OUT_OF_BOUNDS func=indirect(14) sp=spIndexOOB  pc=20 :: table 0 index 9 is out of bounds (size 8)
  [ERROR] TABLE_INDEX_STALE         func=indirect(14) sp=spIndexStale pc=24 :: table 0 index 3 was relaid out to 1, statepoint holds 3

$ node bin/wda.js audit fixtures/bug-unreachable-metadata.json
  errors=1 warnings=0 infos=1
  [ERROR] DANGLING_SLOT_ROOT func=dead_and_live(15) sp=spLive pc=0 :: live stack slot s3 has no emitted home slot
  [INFO]  DEAD_METADATA      func=dead_and_live(15) sp=spDead pc=4 :: statepoint at emitted pc 4 is unreachable; metadata intentionally ignored

Expected-result matrix:

Fixture errors infos Error kinds
baseline-valid 0 0 –
bug-relocation-slot-drift 3 0 DANGLING_REG_ROOT, DANGLING_SLOT_ROOT, PC_NOT_BOUNDARY
bug-inline-stack-drift 3 0 INLINE_SRC_UNRESOLVED, INLINE_STACK_MISMATCH, INLINE_SRC_OUT_OF_RANGE
bug-table-growth 3 0 STALE_TABLE_BASE, TABLE_INDEX_OUT_OF_BOUNDS, TABLE_INDEX_STALE
bug-unreachable-metadata 1 1 DANGLING_SLOT_ROOT (live) + DEAD_METADATA (dead)

Compiler-side remediation

Run node bin/wda.js audit build/emit/statepoints.json --minimize as a CI gate. At emission time, compose in this order for every statepoint: (1) relocate the PC; (2) remap stack roots through the slot map and validate reg roots (fail emission if a root has no home); (3) relocate each inline frame with its own function map and rewrite inlinedAt to the relocated caller srcOffset; (4) refresh tableBase.generation to the generation active at the relocated PC and translate tableIndex through the relayout map; (5) drop stack maps whose block became unreachable. Any regression fails the build, and the 1-minimal counterexample names the exact root that regressed.


Artifacts: ~/wasm-deopt-audit/ — SOLUTION.md, README.md, package.json, src/{relocation,schema,analyze,index}.js, bin/wda.js, test/run-tests.js, fixtures/*.json. npm test → 6 passed, 0 failed.

Evidence & signatures

# Evidence
- Problem class: js-wasm-deopt-statepoint-relocation-audit-20261010
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-10-11T04:10:37.301Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Build a JavaScript tool that analyzes a WebAssembly optimizing compiler\u2019s emitted function bodies and stack maps to find deoptimization statepoints whose live references become invalid after code motion, inlining, or table growth. It must reconcile source-level inline stacks with relocated instruction offsets, distinguish unreachable metadata from real root omissions, and produce a minimal counterexample trace without executing untrusted modules.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "js-wasm-deopt-statepoint-relocation-audit-20261010", "provider": "openrouter", "solved_at": "2026-10-11T04:10:37.302Z", "version": "20"}
Generated from the verified corpus · MIT licensedBack to the catalog