# Quick-Inspect Mode Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** `asmdiff src.c my_func` prints the assembly of one (or more) named functions with an adaptive layout, plus short aliases `-p`/`-a`/`-db`/`-l` for existing and new flags.

**Architecture:** A positional-splitting helper classifies extra positionals as source files or function names; a new `run_inspect` mode compiles the file across the matrix and renders per usable-compiler count (1 = listing + stats, 2 = side-by-side via the existing `report_across`, 3+ = sequential blocks), overridable with `-l/--layout`.

**Tech Stack:** Python stdlib only, single file `asmdiff.py`, `unittest` tests in `test_asmdiff.py`.

Spec: `docs/superpowers/specs/2026-07-06-inspect-mode-design.md`.

## Global Constraints

- Runtime floor is Python 3.8: no `match`, no `X | Y` type syntax, no 3.9+ stdlib calls.
- No third-party dependencies in source or tests.
- All code goes in `asmdiff.py`; all tests in `test_asmdiff.py` (one class per feature area, plain attribute-swap stubs in `try/finally`, no `unittest.mock`).
- Run tests with: `python3 test_asmdiff.py` (add `-v` for detail). Everything is safe: the tool only compiles to `-S` in a temp dir.
- Any user-visible change updates README.md, the module docstring, and `--help` text together (Task 6 covers this).
- Prose and comments use a normal dash `-`, not em dashes.

---

### Task 1: Positional-splitting heuristic

**Files:**
- Modify: `asmdiff.py` (new constant + function near `auto_pairs`, around line 482)
- Test: `test_asmdiff.py` (new class after `TestAutoPairs`, around line 287)

**Interfaces:**
- Consumes: nothing new.
- Produces: `split_positionals(positionals: list[str]) -> (sources: list[str], fn_names: list[str])`, raising `SystemExit` for a path-looking positional that does not exist. `SOURCE_SUFFIXES: set[str]`. Task 4's `main()` wiring calls this.

- [ ] **Step 1: Write the failing tests**

Add to `test_asmdiff.py` after the `TestAutoPairs` class:

```python
class TestSplitPositionals(unittest.TestCase):
    """SOURCE.c FUNC grammar: extra positionals are files when they
    exist, function names when bare, and errors when path-like typos."""

    def test_existing_file_is_a_source(self):
        with tempfile.TemporaryDirectory() as tmp:
            second = Path(tmp) / "b.c"
            second.touch()
            sources, fns = asmdiff.split_positionals(["a.c", str(second)])
        self.assertEqual(sources, ["a.c", str(second)])
        self.assertEqual(fns, [])

    def test_bare_name_is_a_function(self):
        sources, fns = asmdiff.split_positionals(["a.c", "render_lut"])
        self.assertEqual(sources, ["a.c"])
        self.assertEqual(fns, ["render_lut"])

    def test_several_function_names(self):
        sources, fns = asmdiff.split_positionals(["a.c", "f", "g"])
        self.assertEqual(sources, ["a.c"])
        self.assertEqual(fns, ["f", "g"])

    def test_missing_source_suffix_arg_errors(self):
        with self.assertRaises(SystemExit) as ctx:
            asmdiff.split_positionals(["a.c", "typo.c"])
        self.assertIn("no such file", str(ctx.exception))

    def test_missing_path_separator_arg_errors(self):
        with self.assertRaises(SystemExit) as ctx:
            asmdiff.split_positionals(["a.c", "src/render"])
        self.assertIn("no such file", str(ctx.exception))

    def test_uppercase_asm_suffix_is_path_like(self):
        with self.assertRaises(SystemExit):
            asmdiff.split_positionals(["a.c", "startup.S"])

    def test_first_positional_is_always_a_source(self):
        sources, fns = asmdiff.split_positionals(["no_suffix_name"])
        self.assertEqual(sources, ["no_suffix_name"])
        self.assertEqual(fns, [])
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `python3 test_asmdiff.py TestSplitPositionals -v`
Expected: 7 errors, `AttributeError: module 'asmdiff' has no attribute 'split_positionals'`

- [ ] **Step 3: Implement**

Add to `asmdiff.py` immediately after `auto_pairs` (line ~487):

```python
# Suffixes that make a nonexistent positional read as a mistyped source
# file rather than a function name to inspect.
SOURCE_SUFFIXES = {".c", ".h", ".i", ".s", ".cc", ".cpp", ".cxx", ".hpp"}


def split_positionals(positionals):
    """Split positional arguments into source files and function names.

    The first positional is always a source.  A later one that exists
    on disk is a source too; a bare name is a function to inspect; a
    path-looking argument that does not exist (a separator, or a source
    suffix) is a mistyped file - exiting beats searching the assembly
    for a symbol named like a filename.
    """
    sources, fn_names = positionals[:1], []
    for arg in positionals[1:]:
        if Path(arg).exists():
            sources.append(arg)
        elif ("/" in arg or os.sep in arg
                or Path(arg).suffix.lower() in SOURCE_SUFFIXES):
            sys.exit(f"error: no such file: {arg} (a function name to "
                     "inspect must be a bare symbol, not a path)")
        else:
            fn_names.append(arg)
    return sources, fn_names
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `python3 test_asmdiff.py TestSplitPositionals -v`
Expected: 7 passed. Then `python3 test_asmdiff.py` - full suite passes.

- [ ] **Step 5: Commit**

```bash
git add asmdiff.py test_asmdiff.py
git commit -m "Add positional splitter distinguishing files from function names"
```

---

### Task 2: Inspect renderers (listing + stats table)

**Files:**
- Modify: `asmdiff.py` (two functions after `file_summary_table`, around line 785)
- Test: `test_asmdiff.py` (new class after `TestRendering`, around line 976)

**Interfaces:**
- Consumes: `analyze(lines)`, `loop_spans(lines)`, `format_calls(calls)`, `format_spans(spans)`, `render_table(rows)` - all existing.
- Produces: `listing(name: str, lines: list[str]) -> str` and `inspect_table(fn_names: list[str], funcs: dict[str, list[str]]) -> str`. Task 3's `run_inspect` calls both.

- [ ] **Step 1: Write the failing tests**

Add to `test_asmdiff.py` after `TestRendering` (uses the module-level `LOOP_ASM` and `GCC_ASM` fixtures defined at the top of the file):

```python
class TestInspectRendering(unittest.TestCase):
    def test_listing_layout(self):
        funcs = asmdiff.extract_functions(LOOP_ASM)
        lines = asmdiff.listing("looper", funcs["looper"]).splitlines()
        self.assertEqual(lines[0], "looper:")
        self.assertEqual(lines[1], "\txorl\t%eax, %eax")
        self.assertEqual(lines[2], ".L2:")   # local label back at column 0
        self.assertEqual(lines[3], "\taddl\t$1, %eax")

    def test_inspect_table_only_requested_functions(self):
        funcs = asmdiff.extract_functions(GCC_ASM)
        out = asmdiff.inspect_table(["new_const"], funcs)
        lines = out.splitlines()
        self.assertIn("function", lines[0])
        self.assertEqual(len(lines), 2)      # header + the one function
        self.assertRegex(lines[1], r"new_const\s+2\s+ldexpf\s+-")
        self.assertNotIn("TOTAL", out)

    def test_inspect_table_loop_spans(self):
        funcs = asmdiff.extract_functions(LOOP_ASM)
        out = asmdiff.inspect_table(["looper"], funcs)
        self.assertRegex(out.splitlines()[1], r"looper\s+5\s+-\s+\.L2:3")
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `python3 test_asmdiff.py TestInspectRendering -v`
Expected: 3 errors, `AttributeError` for `listing` / `inspect_table`.

- [ ] **Step 3: Implement**

Add to `asmdiff.py` after `file_summary_table` (before `file_tags`):

```python
def listing(name, lines):
    """gcc-style listing of one extracted function: the function label
    at column 0, instructions tabbed, kept local labels back at
    column 0 (extract_functions stores them whitespace-stripped)."""
    body = [line if line.endswith(":") else "\t" + line for line in lines]
    return "\n".join([f"{name}:"] + body)


def inspect_table(fn_names, funcs):
    """Stats rows for the inspected functions only - no pair roles and
    no whole-file total, unlike summary_table/file_summary_table."""
    rows = [("function", "insns", "calls", "loop spans")]
    for name in fn_names:
        insns, calls = analyze(funcs[name])
        rows.append((name, str(insns), format_calls(calls),
                     format_spans(loop_spans(funcs[name]))))
    return render_table(rows)
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `python3 test_asmdiff.py TestInspectRendering -v`
Expected: 3 passed. Then the full suite.

- [ ] **Step 5: Commit**

```bash
git add asmdiff.py test_asmdiff.py
git commit -m "Add single-function listing and stats renderers"
```

---

### Task 3: run_inspect mode

**Files:**
- Modify: `asmdiff.py` (new function after `run_pairs`, around line 928)
- Test: `test_asmdiff.py` (new class after `TestInspectRendering`)

**Interfaces:**
- Consumes: `compile_to_asm(cc_cmd, extra_flags, harness, out_dir)`, `extract_functions(asm_text)`, `report_across(fn_names, left_funcs, right_funcs, left_tag, right_tag)`, plus Task 2's `listing` and `inspect_table`.
- Produces: `run_inspect(source, matrix, fn_names, layout, extra_flags, tmp) -> int` where `layout` is `None`, `"list"`, or `"side-by-side"`. Task 4's `main()` dispatches to it.

- [ ] **Step 1: Write the failing tests**

Add to `test_asmdiff.py` (the stub swaps `asmdiff.compile_to_asm` directly, matching the `asmdiff.FLAGS_LIKE` swap style used in `TestFlagsLike`):

```python
class TestRunInspect(unittest.TestCase):
    """Layout selection in inspect mode, with compile_to_asm stubbed."""

    def _run(self, matrix, fn_names, layout=None):
        real = asmdiff.compile_to_asm
        asmdiff.compile_to_asm = lambda cc, extra, src, tmp: GCC_ASM
        out = io.StringIO()
        try:
            with contextlib.redirect_stdout(out):
                asmdiff.run_inspect("h.c", matrix, fn_names, layout,
                                    [], "/tmp")
        finally:
            asmdiff.compile_to_asm = real
        return out.getvalue()

    def test_one_compiler_plain_listing_and_stats(self):
        out = self._run(["gcc -O2"], ["new_const"])
        self.assertIn("new_const:", out)
        self.assertIn("\tjmp\tldexpf@PLT", out)
        self.assertIn("function", out)           # stats table present
        self.assertNotIn("==", out)              # no per-compiler header
        self.assertNotIn(" | ", out)             # not side-by-side

    def test_two_compilers_side_by_side(self):
        out = self._run(["gcc -O2", "clang -O2"], ["new_const"])
        self.assertIn("cc#1: gcc -O2", out)
        self.assertIn("== cc#1 vs cc#2 ==", out)
        self.assertIn(" | ", out)

    def test_three_compilers_sequential_blocks(self):
        out = self._run(["gcc -O1", "gcc -O2", "gcc -O3"], ["new_const"])
        self.assertEqual(out.count("== gcc -O"), 3)
        self.assertNotIn(" | ", out)

    def test_multiple_functions_listed(self):
        out = self._run(["gcc -O2"], ["old_const", "new_const"])
        self.assertIn("old_const:", out)
        self.assertIn("new_const:", out)

    def test_forced_list_with_two_compilers(self):
        out = self._run(["gcc -O2", "clang -O2"], ["new_const"],
                        layout="list")
        self.assertIn("== gcc -O2 ==", out)
        self.assertNotIn(" | ", out)

    def test_forced_side_by_side_with_three_compilers(self):
        out = self._run(["gcc -O1", "gcc -O2", "gcc -O3"], ["new_const"],
                        layout="side-by-side")
        self.assertIn("== cc#1 vs cc#2 ==", out)
        self.assertIn("== cc#1 vs cc#3 ==", out)

    def test_forced_side_by_side_with_one_compiler_errors(self):
        real = asmdiff.compile_to_asm
        asmdiff.compile_to_asm = lambda cc, extra, src, tmp: GCC_ASM
        try:
            with self.assertRaises(SystemExit) as ctx:
                with contextlib.redirect_stdout(io.StringIO()):
                    asmdiff.run_inspect("h.c", ["gcc -O2"], ["new_const"],
                                        "side-by-side", [], "/tmp")
        finally:
            asmdiff.compile_to_asm = real
        self.assertIn("side-by-side", str(ctx.exception))

    def test_unknown_function_lists_seen(self):
        real = asmdiff.compile_to_asm
        asmdiff.compile_to_asm = lambda cc, extra, src, tmp: GCC_ASM
        try:
            with self.assertRaises(SystemExit) as ctx:
                asmdiff.run_inspect("h.c", ["gcc -O2"], ["nope"],
                                    None, [], "/tmp")
        finally:
            asmdiff.compile_to_asm = real
        msg = str(ctx.exception)
        self.assertIn("not in asm: nope", msg)
        self.assertIn("old_const", msg)          # functions seen listed

    def test_no_usable_compiler_errors(self):
        real = asmdiff.compile_to_asm
        asmdiff.compile_to_asm = lambda cc, extra, src, tmp: None
        try:
            with self.assertRaises(SystemExit) as ctx:
                asmdiff.run_inspect("h.c", ["gcc -O2"], ["f"],
                                    None, [], "/tmp")
        finally:
            asmdiff.compile_to_asm = real
        self.assertIn("no usable compiler", str(ctx.exception))
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `python3 test_asmdiff.py TestRunInspect -v`
Expected: 9 errors, `AttributeError: ... no attribute 'run_inspect'`

- [ ] **Step 3: Implement**

Add to `asmdiff.py` after `run_pairs`, before `main`:

```python
def run_inspect(source, matrix, fn_names, layout, extra_flags, tmp):
    """Inspect mode: print named functions' assembly, no comparison.

    The presentation adapts to how many matrix entries compiled: one
    gives a plain listing with a stats table, exactly two give the
    --across side-by-side, more give one block per compiler.  --layout
    forces list or side-by-side instead.
    """
    usable = []
    for idx, cc_cmd in enumerate(matrix, start=1):
        asm = compile_to_asm(cc_cmd, extra_flags, source, tmp)
        if asm is not None:
            usable.append((f"cc#{idx}", cc_cmd, extract_functions(asm)))
    if not usable:
        sys.exit("error: no usable compiler in the matrix")
    for _, cc_cmd, funcs in usable:
        missing = sorted({f for f in fn_names if f not in funcs})
        if missing:
            sys.exit("error: function(s) not in asm: " + ", ".join(missing)
                     + f" under {cc_cmd}; functions seen: "
                     + ", ".join(funcs))
    if layout == "side-by-side" and len(usable) < 2:
        sys.exit("error: --layout side-by-side needs at least two usable "
                 "compilers in the matrix")
    if layout is None:
        layout = "side-by-side" if len(usable) == 2 else "list"
    if layout == "side-by-side":
        print()
        for tag, cc_cmd, _ in usable:
            print(f"{tag}: {cc_cmd}")
        base_tag, _, base_funcs = usable[0]
        for tag, _, funcs in usable[1:]:
            print(f"\n== {base_tag} vs {tag} ==\n")
            report_across(fn_names, base_funcs, funcs, base_tag, tag)
        return 0
    for _, cc_cmd, funcs in usable:
        if len(usable) > 1:
            print(f"\n== {cc_cmd} ==")
        for name in fn_names:
            print()
            print(listing(name, funcs[name]))
        print()
        print(inspect_table(fn_names, funcs))
    return 0
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `python3 test_asmdiff.py TestRunInspect -v`
Expected: 9 passed. Then the full suite.

- [ ] **Step 5: Commit**

```bash
git add asmdiff.py test_asmdiff.py
git commit -m "Add run_inspect with adaptive listing/side-by-side layout"
```

---

### Task 4: CLI wiring (positional grammar, --layout, guards, dispatch)

**Files:**
- Modify: `asmdiff.py:942-1014` (`main()`)
- Modify: `test_asmdiff.py` (`TestAcrossValidation.test_at_most_two_files`, around line 1011)
- Test: `test_asmdiff.py` (new class after `TestAcrossValidation`)

**Interfaces:**
- Consumes: Task 1's `split_positionals`, Task 3's `run_inspect`.
- Produces: the user-facing grammar `SOURCE.c [FUNC ...]` and `-l/--layout {list,side-by-side}` (argparse dest `layout`, default `None`).

- [ ] **Step 1: Write the failing tests**

Add after `TestAcrossValidation`:

```python
class TestInspectValidation(unittest.TestCase):
    """CLI validation for the SOURCE.c FUNC inspect grammar."""

    def _expect_error(self, argv, fragment):
        err = io.StringIO()
        with contextlib.redirect_stderr(err):
            with self.assertRaises(SystemExit) as ctx:
                asmdiff.main(argv)
        # parser.error writes to stderr; sys.exit carries the message
        self.assertIn(fragment, err.getvalue() + str(ctx.exception))

    def test_function_with_pair_rejected(self):
        self._expect_error(["x.c", "f", "--pair", "a:b"],
                           "cannot be combined")

    def test_function_with_across_rejected(self):
        self._expect_error(["x.c", "f", "--across", "g"],
                           "cannot be combined")

    def test_two_files_plus_function_rejected(self):
        with tempfile.TemporaryDirectory() as tmp:
            a, b = Path(tmp) / "a.c", Path(tmp) / "b.c"
            a.touch()
            b.touch()
            self._expect_error([str(a), str(b), "f"], "--across")

    def test_layout_without_function_rejected(self):
        self._expect_error(["x.c", "--layout", "list"],
                           "--layout only applies")

    def test_layout_value_checked(self):
        self._expect_error(["x.c", "f", "--layout", "diagonal"],
                           "invalid choice")

    def test_typo_filename_is_not_a_function(self):
        self._expect_error(["x.c", "typo.c"], "no such file")
```

Also update two existing `TestAcrossValidation` tests - their extra positionals must now exist on disk to be classified as sources (a nonexistent `b.c` now dies earlier with "no such file"):

```python
    def test_two_files_with_pair_rejected(self):
        with tempfile.TemporaryDirectory() as tmp:
            a, b = Path(tmp) / "a.c", Path(tmp) / "b.c"
            a.touch()
            b.touch()
            self._expect_error([str(a), str(b), "--pair", "x:y"],
                               "--pair compares within one file")

    def test_at_most_two_files(self):
        with tempfile.TemporaryDirectory() as tmp:
            argv = []
            for name in ("a.c", "b.c", "c.c"):
                p = Path(tmp) / name
                p.touch()
                argv.append(str(p))
            self._expect_error(argv + ["--across", "f"], "at most two")
```

(`test_across_and_pair_mutually_exclusive` and
`test_across_one_file_needs_two_cc_entries` pass one positional only and
need no change.)

- [ ] **Step 2: Run tests to verify they fail**

Run: `python3 test_asmdiff.py TestInspectValidation TestAcrossValidation -v`
Expected: the six new tests fail (no `--layout` option, no combined-use guards); `test_at_most_two_files` still passes (guard exists, files now real).

- [ ] **Step 3: Implement**

In `main()`:

(a) Update the `sources` help text (asmdiff.py:942-944):

```python
    parser.add_argument("sources", nargs="+", metavar="SOURCE.c",
                        help="C file to compile; follow it with bare "
                             "function names to inspect their assembly, "
                             "or give two files with --across to compare "
                             "versions of a function")
```

(b) Add `--layout` after the `--across` argument definition:

```python
    parser.add_argument("-l", "--layout", choices=["list", "side-by-side"],
                        help="force the inspect presentation instead of "
                             "adapting to the matrix (1 usable compiler "
                             "lists, 2 go side by side, more list)")
```

(c) Replace the validation block (currently asmdiff.py:990-996) with:

```python
    sources, fn_names = split_positionals(args.sources)
    if len(sources) > 2:
        parser.error("at most two source files may be given")
    if fn_names and len(sources) == 2:
        parser.error("bare function names inspect within one file; "
                     "use --across FUNC for two files")
    if fn_names and (args.pair or args.across):
        parser.error("bare function names (inspect) cannot be combined "
                     "with --pair or --across")
    if args.layout and not fn_names:
        parser.error("--layout only applies when inspecting functions "
                     "(SOURCE.c FUNC ...)")
    if args.across and args.pair:
        parser.error("--across and --pair are mutually exclusive")
    if len(sources) == 2 and args.pair:
        parser.error("--pair compares within one file; "
                     "use --across FUNC for two files")
```

(d) Replace every later `args.sources` use in `main()` with `sources`, and dispatch inspect first (currently asmdiff.py:997-1014):

```python
    config_path = find_config(args.config, sources)
    config = load_config(config_path) if config_path else None
    matrix = build_matrix(args.cc, args.target, config, config_path,
                          args.compile_commands)
    if args.across and len(sources) == 1 and len(matrix) < 2:
        parser.error("--across on one file needs at least two --cc entries")
    for spec in args.pair:
        if ":" not in spec:
            parser.error(f"--pair expects OLD:NEW, got {spec!r}")

    with tempfile.TemporaryDirectory(prefix="asmdiff") as tmp:
        if fn_names:
            return run_inspect(sources[0], matrix, fn_names, args.layout,
                               extra_flags, tmp)
        if args.across:
            return run_across(sources, matrix, args.across,
                              extra_flags, tmp)
        if len(sources) == 2:
            return run_summary(sources, matrix, extra_flags, tmp)
        return run_pairs(sources[0], matrix, args.pair,
                         extra_flags, tmp)
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `python3 test_asmdiff.py` (full suite)
Expected: all pass, including `TestAcrossValidation` and the new `TestInspectValidation`.

- [ ] **Step 5: End-to-end sanity run**

```bash
python3 asmdiff.py asmdiff_example.c old_const
python3 asmdiff.py asmdiff_example.c old_const new_const --layout list
```

Expected: first command shows the gcc-vs-clang side-by-side (the two
default compilers), second shows two sequential `== cc ==` blocks each
listing both functions with a stats table.

- [ ] **Step 6: Commit**

```bash
git add asmdiff.py test_asmdiff.py
git commit -m "Wire inspect mode into the CLI: SOURCE.c FUNC positionals and --layout"
```

---

### Task 5: Short aliases -p, -a, -db

**Files:**
- Modify: `asmdiff.py` (the `--pair`, `--across`, `--compile-commands` `add_argument` calls in `main()`)
- Test: `test_asmdiff.py` (new class after `TestInspectValidation`)

**Interfaces:**
- Consumes: existing argparse dests (`pair`, `across`, `compile_commands`); `-l` already exists from Task 4.
- Produces: alias option strings only; no dest or behavior changes.

- [ ] **Step 1: Write the failing tests**

Aliases are verified through behavior that only triggers if the alias maps to the right dest (the parser is local to `main()`, so there is nothing to introspect):

```python
class TestShortAliases(unittest.TestCase):
    """-p/-a/-db/-l are aliases of --pair/--across/--compile-commands/
    --layout: each hits that option's own validation."""

    def _expect_error(self, argv, fragment):
        err = io.StringIO()
        with contextlib.redirect_stderr(err):
            with self.assertRaises(SystemExit) as ctx:
                asmdiff.main(argv)
        self.assertIn(fragment, err.getvalue() + str(ctx.exception))

    def test_p_is_pair(self):
        self._expect_error(["x.c", "-p", "nocolon"], "expects OLD:NEW")

    def test_a_is_across(self):
        self._expect_error(["x.c", "-a", "f", "--cc", "gcc -O3"],
                           "at least two --cc")

    def test_l_is_layout(self):
        self._expect_error(["x.c", "-l", "list"], "--layout only applies")

    def test_db_is_compile_commands(self):
        # Bare -db in a directory tree with no database is the
        # --compile-commands discovery error.
        with tempfile.TemporaryDirectory() as tmp:
            cwd = Path(tmp) / "a" / "b"
            cwd.mkdir(parents=True)
            (Path(tmp) / ".git").mkdir()     # stop the walk-up inside tmp
            with _inside(cwd):
                self._expect_error(["x.c", "f", "-db"],
                                   "no compile_commands.json")
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `python3 test_asmdiff.py TestShortAliases -v`
Expected: `test_l_is_layout` passes (added in Task 4); the other three fail with argparse "unrecognized arguments" / "expected one argument" noise - the aliases do not exist yet.

- [ ] **Step 3: Implement**

In `main()`, add the alias as the first option string (dest still derives from the long name):

```python
    parser.add_argument("-p", "--pair", action="append", default=[],
```
```python
    parser.add_argument("-a", "--across", action="append", default=[],
```
```python
    parser.add_argument("-db", "--compile-commands", nargs="?", const=True,
```

(Rest of each call unchanged.)

- [ ] **Step 4: Run tests to verify they pass**

Run: `python3 test_asmdiff.py` (full suite)
Expected: all pass.

- [ ] **Step 5: Commit**

```bash
git add asmdiff.py test_asmdiff.py
git commit -m "Add short aliases -p, -a, -db"
```

---

### Task 6: Documentation (docstring, README)

**Files:**
- Modify: `asmdiff.py:11-27` (module docstring Usage block and modes list)
- Modify: `README.md` (new section after Quick start, ends line 73; usage block and option table in Command reference, lines 76-94)

**Interfaces:** none - prose only.

- [ ] **Step 1: Update the module docstring**

Replace the `Usage:` block (asmdiff.py:11-17) with:

```
Usage:
    tools/asmdiff/asmdiff.py SOURCE.c [SOURCE2.c | FUNC...]
                             [--pair OLD:NEW]... [--across FUNC]...
                             [--cc 'CC FLAGS']... [--target NAME]...
                             [--config PATH] [--compile-commands [PATH]]
                             [--flags-like PATH]
                             [--layout list|side-by-side] [-v]
                             [-- EXTRA_FLAGS...]
```

Replace "Three modes:" (asmdiff.py:19-27) with:

```
Four modes:
  SOURCE.c FUNC   inspect: print the named function's assembly, no
                  comparison.  One usable compiler prints a listing
                  plus a stats row, exactly two print side by side,
                  more print one block per compiler; --layout forces
                  list or side-by-side.
  --pair OLD:NEW  compares two different functions within one compilation
                  (with no --pair, old_X/new_X names auto-pair).
  --across FUNC   compares the SAME function across two compilations:
                  either one file under two --cc entries (flag/define
                  variants), or two source files (before/after versions)
                  under each compiler in the matrix.
  (neither)       whole-file summary: per-function counts plus a file
                  total, for one file or side by side for two.
```

- [ ] **Step 2: Add the README quick-inspect section**

Insert between the Quick start section (ends README.md:73) and `## Command reference` (README.md:74):

```markdown
## Quick inspect: one function, no comparison

To just look at what a compiler emits for one function, name it after
the file - no harness, no pairing:

    $ asmdiff.py src/oscillators.c render_lut

With one usable compiler you get the function's listing and a stats
row; with exactly two (the default gcc + clang matrix) the listings
appear side by side; with a bigger matrix each compiler gets its own
block. `-l list` / `-l side-by-side` forces a presentation. Several
function names can be given at once.

A bare name is inspected as a function; an argument that exists on
disk is a second source file, and a path-looking argument that does
not exist is an error - a mistyped filename is never silently searched
for as a symbol.
```

- [ ] **Step 3: Update the Command reference**

Replace the usage block (README.md:77-80) with:

```
asmdiff.py SOURCE.c [SOURCE2.c | FUNC...] [--pair OLD:NEW]... [--across FUNC]...
           [--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
           [--compile-commands [PATH]] [--flags-like PATH]
           [--layout list|side-by-side] [-v] [-- EXTRA_FLAGS...]
```

In the option table (README.md:83-94):

- `SOURCE.c` row: append to the existing text: `Bare names after the file are functions to inspect (see [Quick inspect](#quick-inspect-one-function-no-comparison)).`
- `--pair OLD:NEW` cell -> `-p, --pair OLD:NEW`
- `--across FUNC` cell -> `-a, --across FUNC`
- `--compile-commands [PATH]` cell -> `-db, --compile-commands [PATH]`
- New row after the `--across` row (verbatim):

```
| `-l, --layout list\|side-by-side` | Force the inspect-mode presentation instead of the adaptive default (1 usable compiler lists, 2 go side by side, more list). Inspect mode only. |
```

- [ ] **Step 4: Verify**

```bash
python3 test_asmdiff.py
python3 asmdiff.py --help
python3 -c "import asmdiff; print(asmdiff.__doc__.partition(chr(10))[0])"
```

Expected: suite passes; `--help` shows `-p`, `-a`, `-db`, `-l/--layout`, and the new sources help.

- [ ] **Step 5: Commit**

```bash
git add asmdiff.py README.md
git commit -m "Document quick-inspect mode and short flag aliases"
```
