asmdiff 0.2.0__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {asmdiff-0.2.0 → asmdiff-0.3.3}/PKG-INFO +126 -8
  2. {asmdiff-0.2.0 → asmdiff-0.3.3}/README.md +124 -6
  3. {asmdiff-0.2.0 → asmdiff-0.3.3}/asmdiff.example.toml +6 -0
  4. {asmdiff-0.2.0 → asmdiff-0.3.3}/asmdiff.py +543 -81
  5. {asmdiff-0.2.0 → asmdiff-0.3.3}/pyproject.toml +16 -1
  6. {asmdiff-0.2.0 → asmdiff-0.3.3}/test_asmdiff.py +596 -2
  7. asmdiff-0.2.0/.claude/settings.local.json +0 -12
  8. asmdiff-0.2.0/2026-07-06-inspect-mode.md +0 -754
  9. asmdiff-0.2.0/2026-07-07-codegen-hotspots-handoff.md:Zone.Identifier +0 -0
  10. asmdiff-0.2.0/2026-07-07-elf-mode-handoff.md +0 -156
  11. asmdiff-0.2.0/2026-07-07-table-layout-handoff.md +0 -90
  12. asmdiff-0.2.0/CLAUDE.md +0 -33
  13. asmdiff-0.2.0/FIELD-TEST-REPORT.md +0 -123
  14. asmdiff-0.2.0/Screenshot 2026-07-07 225145.jpg +0 -0
  15. asmdiff-0.2.0/amy-hotspot-analysis.md +0 -214
  16. asmdiff-0.2.0/demo/esp32-matrix.gif +0 -0
  17. asmdiff-0.2.0/demo/quickstart.gif +0 -0
  18. asmdiff-0.2.0/dist_1/.gitignore +0 -1
  19. asmdiff-0.2.0/dist_1/asmdiff-0.1.1-py3-none-any.whl +0 -0
  20. asmdiff-0.2.0/dist_1/asmdiff-0.1.1.tar.gz +0 -0
  21. asmdiff-0.2.0/dist_1/asmdiff-0.1.2-py3-none-any.whl +0 -0
  22. asmdiff-0.2.0/dist_1/asmdiff-0.1.2.tar.gz +0 -0
  23. asmdiff-0.2.0/dist_1/asmdiff-0.1.3-py3-none-any.whl +0 -0
  24. asmdiff-0.2.0/dist_1/asmdiff-0.1.3.tar.gz +0 -0
  25. asmdiff-0.2.0/dist_1/asmdiff-0.1.4-py3-none-any.whl +0 -0
  26. asmdiff-0.2.0/dist_1/asmdiff-0.1.4.tar.gz +0 -0
  27. asmdiff-0.2.0/docs/superpowers/plans/2026-07-08-asmdiff-demo-gifs.md +0 -277
  28. asmdiff-0.2.0/docs/superpowers/specs/2026-07-07-edit-config-design.md +0 -97
  29. asmdiff-0.2.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-design.md +0 -147
  30. asmdiff-0.2.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-pipeline.mmd +0 -16
  31. asmdiff-0.2.0/gif-plan.md +0 -248
  32. asmdiff-0.2.0/how-could-i-create-velvety-hollerith.md:Zone.Identifier +0 -3
  33. {asmdiff-0.2.0 → asmdiff-0.3.3}/.gitignore +0 -0
  34. {asmdiff-0.2.0 → asmdiff-0.3.3}/LICENSE +0 -0
  35. {asmdiff-0.2.0 → asmdiff-0.3.3}/asmdiff_example.c +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: asmdiff
3
- Version: 0.2.0
3
+ Version: 0.3.3
4
4
  Summary: Per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
5
5
  Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
6
6
  Project-URL: Repository, https://github.com/rt-rtos/asmdiff
@@ -358,9 +358,11 @@ zero-overhead loops", the empirical rules (verified on esp-gcc 15.2,
358
358
  ```
359
359
  asmdiff SOURCE.c [SOURCE2.c | FUNC...] [--pair OLD:NEW]... [--across FUNC]...
360
360
  [--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
361
- [--compile-commands [PATH]] [--flags-like PATH]
361
+ [--compile-commands [PATH]] [--flags-like PATH] [--db-includes]
362
+ [--summary-only] [--collapse] [--span-stats]
362
363
  [--layout list|side-by-side] [-v] [-- EXTRA_FLAGS...]
363
364
  asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
365
+ [-l list] [--summary-only] [--span-stats]
364
366
  ```
365
367
 
366
368
  | Option | Meaning |
@@ -368,14 +370,20 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
368
370
  | `SOURCE.c` | C file to compile — a purpose-built harness or a real project source. A second file may be given: with `--across` to compare a function, without it for a whole-file A/B summary. Bare names after the file are functions to inspect (see [Quick inspect](#quick-inspect-one-function-no-comparison)). An ELF binary (any name — detected by magic bytes) switches to [ELF input](#elf-input-what-actually-shipped-after-lto) instead of compiling. |
369
371
  | `-p, --pair OLD:NEW` | Compare two *different* functions within one compilation. Repeatable. Default: every `old_X` is auto-paired with its `new_X`; with no pairs at all, the whole-file summary is printed instead. |
370
372
  | `-a, --across FUNC` | Compare the *same* function across two compilations (see below). Repeatable. Mutually exclusive with `--pair`. |
371
- | `-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. |
372
- | `--filter REGEX` | ELF input only: also analyze every function whose name matches `REGEX` (`re.search`) — sweep a subsystem without naming each function. Matches appear in the stats table but are not listed in full. |
373
+ | `-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). With ELF input, `list` also prints `--filter` matches' listings. |
374
+ | `--filter REGEX` | Also analyze every function whose name matches `REGEX` (`re.search`) — sweep a subsystem, or reach compiler-generated clones (`$constprop$0`, `.isra.0`), without naming each function. In compile modes matches are full peers of named functions (lenient when a match exists under only part of the matrix) and narrow the whole-file summary; in ELF mode matches appear in the stats table but are not listed in full — add `-l list` to print their listings too (a trailing note reminds you when listings were withheld). Not combinable with `--pair`/`--across`. |
373
375
  | `--objdump PATH` | Disassembler for ELF input. Default: derived from the first gcc in the matrix by swapping the trailing `gcc` for `objdump`, so `--target`/config globs locate it like they locate the compiler. |
374
376
  | `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
375
377
  | `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
376
378
  | `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
377
379
  | `-db, --compile-commands [PATH]` | Borrow each source's include/define flags from a `compile_commands.json`; with no `PATH`, walk up from the CWD checking each directory and its `build/` until the repository root. See [below](#borrowing-includes-from-compile_commandsjson). |
378
380
  | `--flags-like PATH` | A source with no `compile_commands` entry borrows the flags recorded for `PATH` — the way to compare a modified copy of a project source under its original's header environment. |
381
+ | `--db-includes` | Borrow only the header-search paths from the database, dropping its defines, forced includes, and `-specs`/`--sysroot`; kept paths are re-emitted as `-idirafter` so they cannot shadow the host's own system headers. This is how a host target compiles a cross project's source. |
382
+ | `-s`, `--summary-only` | Print only the summary/stats tables, suppressing every assembly listing (see [Shaping the output](#shaping-the-output-for-reading-vs-deciding)). |
383
+ | `--json` | Emit the summary as JSON on stdout instead of tables — one record per function per compiler. Implies `--summary-only`; errors stay plain text on stderr. |
384
+ | `--collapse` | In side-by-side listings, omit runs of identical line pairs, keeping 3 lines of context around each difference. |
385
+ | `--span-stats` | Follow each stats table with a per-loop-span instruction mix: load/store/mul/branch/other counts per span. |
386
+ | `--version` | Print the version and exit. |
379
387
  | `-v`, `--verbose` | On compile failure, print the full compiler command and complete error output. Default shows only the compiler, the source, and the first error lines. |
380
388
  | `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
381
389
 
@@ -399,9 +407,103 @@ asmdiff h.c -- -fno-math-errno
399
407
  ```
400
408
 
401
409
  Compilers missing from `PATH` are skipped with a warning; the run fails only
402
- if none are usable. Exit status is non-zero only for operational failures
403
- (compile error — the compiler's stderr is shown — unknown `--pair` name, no
404
- usable compiler). Differing assembly is the expected result, never an error.
410
+ if none are usable, and that error names each missed binary
411
+ (`no-such-gcc: not found on PATH`) so the fix is in the message, not in a
412
+ warning that scrolled away. Exit status is non-zero only for operational
413
+ failures (compile error — the compiler's stderr is shown — unknown `--pair`
414
+ name, no usable compiler). Differing assembly is the expected result, never
415
+ an error.
416
+
417
+ ## Shaping the output for reading vs deciding
418
+
419
+ A two-file compare over a three-target matrix prints three full
420
+ side-by-side listings with the summary table at the *end* of each — the
421
+ right shape for reading an unfamiliar delta, and roughly 100 KB of the
422
+ wrong shape when the table is all the decision needs. Three flags cut the
423
+ output to purpose:
424
+
425
+ - **`--summary-only`** (`-s`) keeps only the summary/stats tables. This is
426
+ the scripted/agent view: the table is the decision input, and a listing
427
+ is pulled with a second, narrower run only when a delta needs
428
+ explaining.
429
+ - **`--collapse`** elides runs of identical line pairs in every
430
+ side-by-side listing, keeping 3 lines of context around each difference
431
+ plus a `... N identical lines ...` marker. Two ~500-instruction
432
+ functions differing by five instructions render as a few readable hunks
433
+ instead of ~1000 lines.
434
+ - **`--span-stats`** follows each stats table with a per-loop-span
435
+ instruction mix, one row per span:
436
+
437
+ ```
438
+ function span insns load store mul branch other
439
+ stereo_reverb .L108 327 96 31 14 12 174
440
+ ```
441
+
442
+ This weighs the span rather than the whole function — the number that
443
+ actually decides a hot-loop comparison (is the rewrite trading loads
444
+ for stores? did the multiplies move?) without hand-counting a listing
445
+ and accidentally tallying past the loop end into the epilogue.
446
+ Buckets are by mnemonic (Xtensa, RISC-V, ARM) with an AT&T
447
+ memory-operand heuristic for x86; *branch* means any control
448
+ transfer — conditional and unconditional branches, calls, and
449
+ returns (outbound calls are already itemised by name in the `calls`
450
+ column) — and anything the tables don't know lands in *other*.
451
+
452
+ Reading *branch* inside a span: a software loop's own backedge is
453
+ one of them, because the span runs from the label to the last
454
+ backward branch inclusive. A Xtensa zero-overhead loop instead
455
+ encloses only the body between `loop` and its end label, so the loop
456
+ machinery contributes zero and any branch counted there is real
457
+ per-iteration control flow — early exits, per-sample `if`/`else`,
458
+ wrap-around checks. Track it across variants: branches replaced by
459
+ conditional moves (`movnez`, `csel`, `cmov` — counted under *other*)
460
+ show as *branch* falling while *insns* stays roughly flat, and a
461
+ call appearing inside a hot span is the ZOL killer the `calls`
462
+ column already flagged.
463
+
464
+ All three combine with every compile mode; `--summary-only` and
465
+ `--span-stats` also apply to ELF input.
466
+
467
+ For scripted callers, `--json` replaces the tables entirely with one
468
+ JSON document on stdout: a flat `results` list holding one record per
469
+ function per compiler — `cc`, `tag` (source label in two-file runs),
470
+ `role` (`baseline`/`candidate` in paired runs), `insns`, `loop_spans`,
471
+ `calls`, and `span_stats` when `--span-stats` is given. Flat records
472
+ keep it one `jq` expression away from any question the tables answer.
473
+ There is no built-in jq subcommand and there won't be: the tool prints
474
+ JSON on stdout, so a plain `| jq` pipe *is* the integration, and jq
475
+ already does the filtering better than a wrapper flag could.
476
+
477
+ A few recipes that cover the common questions:
478
+
479
+ ```bash
480
+ # Instruction count per function per side, as a table
481
+ asmdiff old.c new.c --json \
482
+ | jq -r '.results[] | "\(.tag)\t\(.function)\t\(.insns)"'
483
+
484
+ # Only the functions that emit libcalls (soft-float, __divdi3, memcpy…)
485
+ asmdiff old.c new.c --json \
486
+ | jq '.results[] | select(.calls | length > 0) | {function, tag, calls}'
487
+
488
+ # Per-function old→new instruction delta (regressions have a positive delta)
489
+ asmdiff old.c new.c --json \
490
+ | jq -r '.results | group_by(.function)[] | select(length == 2)
491
+ | "\(.[0].function): \(.[0].insns) -> \(.[1].insns) (\(.[1].insns - .[0].insns))"'
492
+
493
+ # Fail CI if any function grew, using jq's exit status
494
+ asmdiff old.c new.c --json \
495
+ | jq -e '.results | group_by(.function)
496
+ | all(length < 2 or .[1].insns <= .[0].insns)' > /dev/null
497
+
498
+ # The full record for one hot function, spans and all
499
+ asmdiff old.c new.c -a stereo_reverb --json --span-stats \
500
+ | jq '.results[] | {role, insns, spans: .loop_spans}'
501
+ ```
502
+
503
+ The top level carries `asmdiff` (version) and `mode`
504
+ (`pairs`/`across`/`inspect`/`summary`/`elf`; ELF runs add the binary's
505
+ path as `elf`). Warnings and errors stay plain text on stderr, so a
506
+ failed run never emits half a document.
405
507
 
406
508
  ## Config file: named targets
407
509
 
@@ -573,6 +675,17 @@ Details that make it robust:
573
675
  - **`compile_commands` expands `~` and `$VARS`.** The `==` header always
574
676
  prints the resolved compiler command; run with `-- -v` if you want to see
575
677
  every include path the compiler actually received.
678
+ - **`--db-includes` restricts the borrow to header-search paths**,
679
+ dropping `-D`/`-U`, forced includes, and `-specs`/`--sysroot`. Use it
680
+ when the *target* doesn't match the arch the database was built for —
681
+ host gcc given an ESP-IDF database would otherwise inherit
682
+ `-specs=picolibc.specs` and cross-only defines it cannot compile
683
+ with, yet still needs the project's include paths to find its
684
+ headers. The kept paths are re-emitted as `-idirafter`, searched
685
+ *after* the compiler's own system directories: ESP-IDF's include set
686
+ contains a libc-overlay directory (`esp_libc/platform_include`) whose
687
+ `stdio.h` would shadow the host's under plain `-I`; demoted, the
688
+ project's own headers still resolve and the host's libc wins.
576
689
 
577
690
  ### Auto-discovering the database
578
691
 
@@ -665,6 +778,11 @@ baseline (`../baseline/src/oscillators.c`), which is the same file under a
665
778
  path the database has never heard of. The absent-source error and the
666
779
  soft-miss note both point at this flag when a same-name entry exists.
667
780
 
781
+ To contrast the same pair on a host compiler (does gcc-on-x86 make the
782
+ same choice?), add `--db-includes`: the borrow then carries only the
783
+ project's include paths, which are target-portable, and none of the
784
+ cross-only defines or `-specs` a host gcc would choke on.
785
+
668
786
  ## Whole-file summary
669
787
 
670
788
  With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
@@ -340,9 +340,11 @@ zero-overhead loops", the empirical rules (verified on esp-gcc 15.2,
340
340
  ```
341
341
  asmdiff SOURCE.c [SOURCE2.c | FUNC...] [--pair OLD:NEW]... [--across FUNC]...
342
342
  [--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
343
- [--compile-commands [PATH]] [--flags-like PATH]
343
+ [--compile-commands [PATH]] [--flags-like PATH] [--db-includes]
344
+ [--summary-only] [--collapse] [--span-stats]
344
345
  [--layout list|side-by-side] [-v] [-- EXTRA_FLAGS...]
345
346
  asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
347
+ [-l list] [--summary-only] [--span-stats]
346
348
  ```
347
349
 
348
350
  | Option | Meaning |
@@ -350,14 +352,20 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
350
352
  | `SOURCE.c` | C file to compile — a purpose-built harness or a real project source. A second file may be given: with `--across` to compare a function, without it for a whole-file A/B summary. Bare names after the file are functions to inspect (see [Quick inspect](#quick-inspect-one-function-no-comparison)). An ELF binary (any name — detected by magic bytes) switches to [ELF input](#elf-input-what-actually-shipped-after-lto) instead of compiling. |
351
353
  | `-p, --pair OLD:NEW` | Compare two *different* functions within one compilation. Repeatable. Default: every `old_X` is auto-paired with its `new_X`; with no pairs at all, the whole-file summary is printed instead. |
352
354
  | `-a, --across FUNC` | Compare the *same* function across two compilations (see below). Repeatable. Mutually exclusive with `--pair`. |
353
- | `-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. |
354
- | `--filter REGEX` | ELF input only: also analyze every function whose name matches `REGEX` (`re.search`) — sweep a subsystem without naming each function. Matches appear in the stats table but are not listed in full. |
355
+ | `-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). With ELF input, `list` also prints `--filter` matches' listings. |
356
+ | `--filter REGEX` | Also analyze every function whose name matches `REGEX` (`re.search`) — sweep a subsystem, or reach compiler-generated clones (`$constprop$0`, `.isra.0`), without naming each function. In compile modes matches are full peers of named functions (lenient when a match exists under only part of the matrix) and narrow the whole-file summary; in ELF mode matches appear in the stats table but are not listed in full — add `-l list` to print their listings too (a trailing note reminds you when listings were withheld). Not combinable with `--pair`/`--across`. |
355
357
  | `--objdump PATH` | Disassembler for ELF input. Default: derived from the first gcc in the matrix by swapping the trailing `gcc` for `objdump`, so `--target`/config globs locate it like they locate the compiler. |
356
358
  | `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
357
359
  | `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
358
360
  | `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
359
361
  | `-db, --compile-commands [PATH]` | Borrow each source's include/define flags from a `compile_commands.json`; with no `PATH`, walk up from the CWD checking each directory and its `build/` until the repository root. See [below](#borrowing-includes-from-compile_commandsjson). |
360
362
  | `--flags-like PATH` | A source with no `compile_commands` entry borrows the flags recorded for `PATH` — the way to compare a modified copy of a project source under its original's header environment. |
363
+ | `--db-includes` | Borrow only the header-search paths from the database, dropping its defines, forced includes, and `-specs`/`--sysroot`; kept paths are re-emitted as `-idirafter` so they cannot shadow the host's own system headers. This is how a host target compiles a cross project's source. |
364
+ | `-s`, `--summary-only` | Print only the summary/stats tables, suppressing every assembly listing (see [Shaping the output](#shaping-the-output-for-reading-vs-deciding)). |
365
+ | `--json` | Emit the summary as JSON on stdout instead of tables — one record per function per compiler. Implies `--summary-only`; errors stay plain text on stderr. |
366
+ | `--collapse` | In side-by-side listings, omit runs of identical line pairs, keeping 3 lines of context around each difference. |
367
+ | `--span-stats` | Follow each stats table with a per-loop-span instruction mix: load/store/mul/branch/other counts per span. |
368
+ | `--version` | Print the version and exit. |
361
369
  | `-v`, `--verbose` | On compile failure, print the full compiler command and complete error output. Default shows only the compiler, the source, and the first error lines. |
362
370
  | `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
363
371
 
@@ -381,9 +389,103 @@ asmdiff h.c -- -fno-math-errno
381
389
  ```
382
390
 
383
391
  Compilers missing from `PATH` are skipped with a warning; the run fails only
384
- if none are usable. Exit status is non-zero only for operational failures
385
- (compile error — the compiler's stderr is shown — unknown `--pair` name, no
386
- usable compiler). Differing assembly is the expected result, never an error.
392
+ if none are usable, and that error names each missed binary
393
+ (`no-such-gcc: not found on PATH`) so the fix is in the message, not in a
394
+ warning that scrolled away. Exit status is non-zero only for operational
395
+ failures (compile error — the compiler's stderr is shown — unknown `--pair`
396
+ name, no usable compiler). Differing assembly is the expected result, never
397
+ an error.
398
+
399
+ ## Shaping the output for reading vs deciding
400
+
401
+ A two-file compare over a three-target matrix prints three full
402
+ side-by-side listings with the summary table at the *end* of each — the
403
+ right shape for reading an unfamiliar delta, and roughly 100 KB of the
404
+ wrong shape when the table is all the decision needs. Three flags cut the
405
+ output to purpose:
406
+
407
+ - **`--summary-only`** (`-s`) keeps only the summary/stats tables. This is
408
+ the scripted/agent view: the table is the decision input, and a listing
409
+ is pulled with a second, narrower run only when a delta needs
410
+ explaining.
411
+ - **`--collapse`** elides runs of identical line pairs in every
412
+ side-by-side listing, keeping 3 lines of context around each difference
413
+ plus a `... N identical lines ...` marker. Two ~500-instruction
414
+ functions differing by five instructions render as a few readable hunks
415
+ instead of ~1000 lines.
416
+ - **`--span-stats`** follows each stats table with a per-loop-span
417
+ instruction mix, one row per span:
418
+
419
+ ```
420
+ function span insns load store mul branch other
421
+ stereo_reverb .L108 327 96 31 14 12 174
422
+ ```
423
+
424
+ This weighs the span rather than the whole function — the number that
425
+ actually decides a hot-loop comparison (is the rewrite trading loads
426
+ for stores? did the multiplies move?) without hand-counting a listing
427
+ and accidentally tallying past the loop end into the epilogue.
428
+ Buckets are by mnemonic (Xtensa, RISC-V, ARM) with an AT&T
429
+ memory-operand heuristic for x86; *branch* means any control
430
+ transfer — conditional and unconditional branches, calls, and
431
+ returns (outbound calls are already itemised by name in the `calls`
432
+ column) — and anything the tables don't know lands in *other*.
433
+
434
+ Reading *branch* inside a span: a software loop's own backedge is
435
+ one of them, because the span runs from the label to the last
436
+ backward branch inclusive. A Xtensa zero-overhead loop instead
437
+ encloses only the body between `loop` and its end label, so the loop
438
+ machinery contributes zero and any branch counted there is real
439
+ per-iteration control flow — early exits, per-sample `if`/`else`,
440
+ wrap-around checks. Track it across variants: branches replaced by
441
+ conditional moves (`movnez`, `csel`, `cmov` — counted under *other*)
442
+ show as *branch* falling while *insns* stays roughly flat, and a
443
+ call appearing inside a hot span is the ZOL killer the `calls`
444
+ column already flagged.
445
+
446
+ All three combine with every compile mode; `--summary-only` and
447
+ `--span-stats` also apply to ELF input.
448
+
449
+ For scripted callers, `--json` replaces the tables entirely with one
450
+ JSON document on stdout: a flat `results` list holding one record per
451
+ function per compiler — `cc`, `tag` (source label in two-file runs),
452
+ `role` (`baseline`/`candidate` in paired runs), `insns`, `loop_spans`,
453
+ `calls`, and `span_stats` when `--span-stats` is given. Flat records
454
+ keep it one `jq` expression away from any question the tables answer.
455
+ There is no built-in jq subcommand and there won't be: the tool prints
456
+ JSON on stdout, so a plain `| jq` pipe *is* the integration, and jq
457
+ already does the filtering better than a wrapper flag could.
458
+
459
+ A few recipes that cover the common questions:
460
+
461
+ ```bash
462
+ # Instruction count per function per side, as a table
463
+ asmdiff old.c new.c --json \
464
+ | jq -r '.results[] | "\(.tag)\t\(.function)\t\(.insns)"'
465
+
466
+ # Only the functions that emit libcalls (soft-float, __divdi3, memcpy…)
467
+ asmdiff old.c new.c --json \
468
+ | jq '.results[] | select(.calls | length > 0) | {function, tag, calls}'
469
+
470
+ # Per-function old→new instruction delta (regressions have a positive delta)
471
+ asmdiff old.c new.c --json \
472
+ | jq -r '.results | group_by(.function)[] | select(length == 2)
473
+ | "\(.[0].function): \(.[0].insns) -> \(.[1].insns) (\(.[1].insns - .[0].insns))"'
474
+
475
+ # Fail CI if any function grew, using jq's exit status
476
+ asmdiff old.c new.c --json \
477
+ | jq -e '.results | group_by(.function)
478
+ | all(length < 2 or .[1].insns <= .[0].insns)' > /dev/null
479
+
480
+ # The full record for one hot function, spans and all
481
+ asmdiff old.c new.c -a stereo_reverb --json --span-stats \
482
+ | jq '.results[] | {role, insns, spans: .loop_spans}'
483
+ ```
484
+
485
+ The top level carries `asmdiff` (version) and `mode`
486
+ (`pairs`/`across`/`inspect`/`summary`/`elf`; ELF runs add the binary's
487
+ path as `elf`). Warnings and errors stay plain text on stderr, so a
488
+ failed run never emits half a document.
387
489
 
388
490
  ## Config file: named targets
389
491
 
@@ -555,6 +657,17 @@ Details that make it robust:
555
657
  - **`compile_commands` expands `~` and `$VARS`.** The `==` header always
556
658
  prints the resolved compiler command; run with `-- -v` if you want to see
557
659
  every include path the compiler actually received.
660
+ - **`--db-includes` restricts the borrow to header-search paths**,
661
+ dropping `-D`/`-U`, forced includes, and `-specs`/`--sysroot`. Use it
662
+ when the *target* doesn't match the arch the database was built for —
663
+ host gcc given an ESP-IDF database would otherwise inherit
664
+ `-specs=picolibc.specs` and cross-only defines it cannot compile
665
+ with, yet still needs the project's include paths to find its
666
+ headers. The kept paths are re-emitted as `-idirafter`, searched
667
+ *after* the compiler's own system directories: ESP-IDF's include set
668
+ contains a libc-overlay directory (`esp_libc/platform_include`) whose
669
+ `stdio.h` would shadow the host's under plain `-I`; demoted, the
670
+ project's own headers still resolve and the host's libc wins.
558
671
 
559
672
  ### Auto-discovering the database
560
673
 
@@ -647,6 +760,11 @@ baseline (`../baseline/src/oscillators.c`), which is the same file under a
647
760
  path the database has never heard of. The absent-source error and the
648
761
  soft-miss note both point at this flag when a same-name entry exists.
649
762
 
763
+ To contrast the same pair on a host compiler (does gcc-on-x86 make the
764
+ same choice?), add `--db-includes`: the borrow then carries only the
765
+ project's include paths, which are target-portable, and none of the
766
+ cross-only defines or `-specs` a host gcc would choke on.
767
+
650
768
  ## Whole-file summary
651
769
 
652
770
  With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
@@ -26,6 +26,12 @@ flags = [
26
26
  cc = "clang"
27
27
  flags = ["-O3", "-Wall", "-Wextra"]
28
28
 
29
+ # The name the docs use for "your native compiler" (--target host): an
30
+ # alias of [gcc] kept as its own table so the examples work verbatim.
31
+ [host]
32
+ cc = "gcc"
33
+ flags = ["-O3", "-Wall", "-Wextra"]
34
+
29
35
  # cc values may use ~, $VARS, and glob patterns. A glob that matches
30
36
  # several installed toolchains resolves to the highest version-sorted
31
37
  # one (announced on stderr); pin the exact esp-NN directory instead if