asmdiff 0.3.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 (44) hide show
  1. {asmdiff-0.3.0 → asmdiff-0.3.3}/PKG-INFO +47 -10
  2. {asmdiff-0.3.0 → asmdiff-0.3.3}/README.md +45 -8
  3. {asmdiff-0.3.0 → asmdiff-0.3.3}/asmdiff.py +125 -38
  4. {asmdiff-0.3.0 → asmdiff-0.3.3}/pyproject.toml +12 -0
  5. {asmdiff-0.3.0 → asmdiff-0.3.3}/test_asmdiff.py +106 -2
  6. asmdiff-0.3.0/.claude/settings.local.json +0 -12
  7. asmdiff-0.3.0/2026-07-06-inspect-mode.md +0 -754
  8. asmdiff-0.3.0/2026-07-07-codegen-hotspots-handoff.md:Zone.Identifier +0 -0
  9. asmdiff-0.3.0/2026-07-07-elf-mode-handoff.md +0 -156
  10. asmdiff-0.3.0/2026-07-07-table-layout-handoff.md +0 -90
  11. asmdiff-0.3.0/2026-07-10-reverb-session-feedback-handoff.md +0 -102
  12. asmdiff-0.3.0/CLAUDE.md +0 -33
  13. asmdiff-0.3.0/FIELD-TEST-REPORT.md +0 -123
  14. asmdiff-0.3.0/Screenshot 2026-07-07 225145.jpg +0 -0
  15. asmdiff-0.3.0/amy-hotspot-analysis.md +0 -214
  16. asmdiff-0.3.0/demo/delay_macro.c +0 -414
  17. asmdiff-0.3.0/demo/delay_orig.c +0 -355
  18. asmdiff-0.3.0/demo/delay_restrict.c +0 -361
  19. asmdiff-0.3.0/demo/delay_struct.c +0 -415
  20. asmdiff-0.3.0/demo/esp32-matrix.gif +0 -0
  21. asmdiff-0.3.0/demo/esp32-matrix.png +0 -0
  22. asmdiff-0.3.0/demo/quickstart.gif +0 -0
  23. asmdiff-0.3.0/demo/quickstart.png +0 -0
  24. asmdiff-0.3.0/dist_1/.gitignore +0 -1
  25. asmdiff-0.3.0/dist_1/asmdiff-0.1.1-py3-none-any.whl +0 -0
  26. asmdiff-0.3.0/dist_1/asmdiff-0.1.1.tar.gz +0 -0
  27. asmdiff-0.3.0/dist_1/asmdiff-0.1.2-py3-none-any.whl +0 -0
  28. asmdiff-0.3.0/dist_1/asmdiff-0.1.2.tar.gz +0 -0
  29. asmdiff-0.3.0/dist_1/asmdiff-0.1.3-py3-none-any.whl +0 -0
  30. asmdiff-0.3.0/dist_1/asmdiff-0.1.3.tar.gz +0 -0
  31. asmdiff-0.3.0/dist_1/asmdiff-0.1.4-py3-none-any.whl +0 -0
  32. asmdiff-0.3.0/dist_1/asmdiff-0.1.4.tar.gz +0 -0
  33. asmdiff-0.3.0/docs/superpowers/plans/2026-07-08-asmdiff-demo-gifs.md +0 -277
  34. asmdiff-0.3.0/docs/superpowers/specs/2026-07-07-edit-config-design.md +0 -97
  35. asmdiff-0.3.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-design.md +0 -147
  36. asmdiff-0.3.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-pipeline.mmd +0 -16
  37. asmdiff-0.3.0/gif-plan.md +0 -248
  38. asmdiff-0.3.0/hackaday-tip-draft.md +0 -101
  39. asmdiff-0.3.0/hn-show-draft.md +0 -57
  40. asmdiff-0.3.0/how-could-i-create-velvety-hollerith.md:Zone.Identifier +0 -3
  41. {asmdiff-0.3.0 → asmdiff-0.3.3}/.gitignore +0 -0
  42. {asmdiff-0.3.0 → asmdiff-0.3.3}/LICENSE +0 -0
  43. {asmdiff-0.3.0 → asmdiff-0.3.3}/asmdiff.example.toml +0 -0
  44. {asmdiff-0.3.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.3.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
@@ -362,7 +362,7 @@ asmdiff SOURCE.c [SOURCE2.c | FUNC...] [--pair OLD:NEW]... [--across FUNC]...
362
362
  [--summary-only] [--collapse] [--span-stats]
363
363
  [--layout list|side-by-side] [-v] [-- EXTRA_FLAGS...]
364
364
  asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
365
- [--summary-only] [--span-stats]
365
+ [-l list] [--summary-only] [--span-stats]
366
366
  ```
367
367
 
368
368
  | Option | Meaning |
@@ -370,8 +370,8 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
370
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. |
371
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. |
372
372
  | `-a, --across FUNC` | Compare the *same* function across two compilations (see below). Repeatable. Mutually exclusive with `--pair`. |
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). Inspect mode only. |
374
- | `--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`. |
375
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. |
376
376
  | `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
377
377
  | `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
@@ -381,7 +381,7 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
381
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
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
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, elide runs of identical line pairs, keeping 3 lines of context around each difference. |
384
+ | `--collapse` | In side-by-side listings, omit runs of identical line pairs, keeping 3 lines of context around each difference. |
385
385
  | `--span-stats` | Follow each stats table with a per-loop-span instruction mix: load/store/mul/branch/other counts per span. |
386
386
  | `--version` | Print the version and exit. |
387
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. |
@@ -444,9 +444,22 @@ output to purpose:
444
444
  for stores? did the multiplies move?) without hand-counting a listing
445
445
  and accidentally tallying past the loop end into the epilogue.
446
446
  Buckets are by mnemonic (Xtensa, RISC-V, ARM) with an AT&T
447
- memory-operand heuristic for x86; *branch* includes calls (outbound
448
- calls are already itemised in the `calls` column), and anything the
449
- tables don't know lands in *other*.
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.
450
463
 
451
464
  All three combine with every compile mode; `--summary-only` and
452
465
  `--span-stats` also apply to ELF input.
@@ -456,9 +469,33 @@ JSON document on stdout: a flat `results` list holding one record per
456
469
  function per compiler — `cc`, `tag` (source label in two-file runs),
457
470
  `role` (`baseline`/`candidate` in paired runs), `insns`, `loop_spans`,
458
471
  `calls`, and `span_stats` when `--span-stats` is given. Flat records
459
- keep it one `jq` expression away from any question the tables answer:
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:
460
478
 
461
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
462
499
  asmdiff old.c new.c -a stereo_reverb --json --span-stats \
463
500
  | jq '.results[] | {role, insns, spans: .loop_spans}'
464
501
  ```
@@ -344,7 +344,7 @@ asmdiff SOURCE.c [SOURCE2.c | FUNC...] [--pair OLD:NEW]... [--across FUNC]...
344
344
  [--summary-only] [--collapse] [--span-stats]
345
345
  [--layout list|side-by-side] [-v] [-- EXTRA_FLAGS...]
346
346
  asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
347
- [--summary-only] [--span-stats]
347
+ [-l list] [--summary-only] [--span-stats]
348
348
  ```
349
349
 
350
350
  | Option | Meaning |
@@ -352,8 +352,8 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
352
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. |
353
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. |
354
354
  | `-a, --across FUNC` | Compare the *same* function across two compilations (see below). Repeatable. Mutually exclusive with `--pair`. |
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). Inspect mode only. |
356
- | `--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`. |
357
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. |
358
358
  | `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
359
359
  | `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
@@ -363,7 +363,7 @@ asmdiff FIRMWARE.elf [FUNC...] [--filter REGEX] [--objdump PATH]
363
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
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
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, elide runs of identical line pairs, keeping 3 lines of context around each difference. |
366
+ | `--collapse` | In side-by-side listings, omit runs of identical line pairs, keeping 3 lines of context around each difference. |
367
367
  | `--span-stats` | Follow each stats table with a per-loop-span instruction mix: load/store/mul/branch/other counts per span. |
368
368
  | `--version` | Print the version and exit. |
369
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. |
@@ -426,9 +426,22 @@ output to purpose:
426
426
  for stores? did the multiplies move?) without hand-counting a listing
427
427
  and accidentally tallying past the loop end into the epilogue.
428
428
  Buckets are by mnemonic (Xtensa, RISC-V, ARM) with an AT&T
429
- memory-operand heuristic for x86; *branch* includes calls (outbound
430
- calls are already itemised in the `calls` column), and anything the
431
- tables don't know lands in *other*.
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.
432
445
 
433
446
  All three combine with every compile mode; `--summary-only` and
434
447
  `--span-stats` also apply to ELF input.
@@ -438,9 +451,33 @@ JSON document on stdout: a flat `results` list holding one record per
438
451
  function per compiler — `cc`, `tag` (source label in two-file runs),
439
452
  `role` (`baseline`/`candidate` in paired runs), `insns`, `loop_spans`,
440
453
  `calls`, and `span_stats` when `--span-stats` is given. Flat records
441
- keep it one `jq` expression away from any question the tables answer:
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:
442
460
 
443
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
444
481
  asmdiff old.c new.c -a stereo_reverb --json --span-stats \
445
482
  | jq '.results[] | {role, insns, spans: .loop_spans}'
446
483
  ```
@@ -14,12 +14,13 @@ Usage:
14
14
  [--cc 'CC FLAGS']... [--target NAME]...
15
15
  [--config PATH] [--compile-commands [PATH]]
16
16
  [--flags-like PATH] [--db-includes]
17
- [--summary-only] [--collapse] [--span-stats]
17
+ [--filter REGEX] [--summary-only]
18
+ [--collapse] [--span-stats]
18
19
  [--json] [--layout list|side-by-side] [-v]
19
20
  [-- EXTRA_FLAGS...]
20
21
  tools/asmdiff/asmdiff.py FIRMWARE.elf [FUNC...] [--filter REGEX]
21
- [--objdump PATH] [--summary-only]
22
- [--span-stats] [--json]
22
+ [--objdump PATH] [--layout list]
23
+ [--summary-only] [--span-stats] [--json]
23
24
  tools/asmdiff/asmdiff.py --edit-config | --example-config | --version
24
25
 
25
26
  Five modes:
@@ -27,12 +28,19 @@ Five modes:
27
28
  comparison. One usable compiler prints a listing
28
29
  plus a stats row, exactly two print side by side,
29
30
  more print one block per compiler; --layout forces
30
- list or side-by-side.
31
+ list or side-by-side. --filter REGEX selects
32
+ matching functions as full peers of named ones
33
+ (compiler-generated clones included; lenient when a
34
+ match exists under only part of the matrix), and in
35
+ summary mode narrows the table.
31
36
  FIRMWARE.elf ELF input (detected by magic bytes): disassemble a
32
37
  linked binary through the toolchain's objdump and
33
38
  run named and/or --filter REGEX matched functions
34
39
  through the same analyzers - post-LTO/link reality
35
- instead of single-TU codegen. objdump comes from
40
+ instead of single-TU codegen. Named functions are
41
+ listed in full; --filter matches are summarized
42
+ only, unless -l list asks for their listings too.
43
+ objdump comes from
36
44
  --objdump PATH or the first gcc in the matrix
37
45
  (trailing gcc swapped for objdump); Xtensa
38
46
  -mlongcalls call sequences are resolved to their
@@ -100,7 +108,7 @@ except ModuleNotFoundError:
100
108
 
101
109
  # Single source of truth for the package version: pyproject.toml reads
102
110
  # it from here (hatchling dynamic version).
103
- __version__ = "0.3.0"
111
+ __version__ = "0.3.3"
104
112
 
105
113
  DEFAULT_COMPILERS = ["gcc", "clang"]
106
114
  FALLBACK_FLAGS = "-O3"
@@ -1533,21 +1541,39 @@ def run_across(sources, matrix, fn_names, extra_flags, tmp):
1533
1541
  return 0
1534
1542
 
1535
1543
 
1536
- def run_summary(sources, matrix, extra_flags, tmp):
1537
- """No pairs to compare: whole-file summary, one block per file."""
1544
+ def _compile_filter(filter_regex):
1545
+ """Compile a --filter regex, exiting with a clean error on a bad one."""
1546
+ try:
1547
+ return re.compile(filter_regex)
1548
+ except re.error as exc:
1549
+ sys.exit(f"error: bad --filter regex: {exc}")
1550
+
1551
+
1552
+ def run_summary(sources, matrix, extra_flags, tmp, filter_regex=None):
1553
+ """No pairs to compare: whole-file summary, one block per file.
1554
+
1555
+ --filter narrows each file's table to matching functions - the
1556
+ subsystem view of a big TU, same selection idea as ELF mode.
1557
+ """
1558
+ pattern = _compile_filter(filter_regex) if filter_regex else None
1538
1559
  tags = (file_tags(*sources) if len(sources) == 2
1539
1560
  else [Path(sources[0]).name])
1540
1561
  ran_any = False
1562
+ matched_any = False
1541
1563
  for cc_cmd in matrix:
1542
1564
  sections = []
1543
1565
  for src in sources:
1544
1566
  asm = compile_to_asm(cc_cmd, extra_flags, src, tmp)
1545
1567
  if asm is None:
1546
1568
  break
1547
- sections.append(extract_functions(asm))
1569
+ funcs = extract_functions(asm)
1570
+ if pattern is not None:
1571
+ funcs = {n: v for n, v in funcs.items() if pattern.search(n)}
1572
+ sections.append(funcs)
1548
1573
  if len(sections) < len(sources):
1549
1574
  continue
1550
1575
  ran_any = True
1576
+ matched_any = matched_any or any(sections)
1551
1577
  shown = (mark_db_misses(cc_cmd, sources, tags)
1552
1578
  if len(sources) == 2 else tags)
1553
1579
  if JSON_OUT is None:
@@ -1563,12 +1589,16 @@ def run_summary(sources, matrix, extra_flags, tmp):
1563
1589
  print(f"\n-- {tag} --")
1564
1590
  print()
1565
1591
  print(file_summary_table(funcs, table_width()) if funcs
1566
- else "(no functions found)")
1592
+ else ("(no functions match --filter)" if pattern is not None
1593
+ else "(no functions found)"))
1567
1594
  if funcs:
1568
1595
  span_stats_block(list(funcs), funcs)
1569
1596
  if not ran_any:
1570
1597
  sys.exit("error: no usable compiler in the matrix"
1571
1598
  + _skipped_suffix())
1599
+ if pattern is not None and not matched_any:
1600
+ sys.exit(f"error: --filter {filter_regex!r} matched no function "
1601
+ "in any compilation")
1572
1602
  return 0
1573
1603
 
1574
1604
 
@@ -1628,14 +1658,24 @@ def run_pairs(source, matrix, pair_specs, extra_flags, tmp):
1628
1658
  return 0
1629
1659
 
1630
1660
 
1631
- def run_inspect(source, matrix, fn_names, layout, extra_flags, tmp):
1661
+ def run_inspect(source, matrix, fn_names, layout, extra_flags, tmp,
1662
+ filter_regex=None):
1632
1663
  """Inspect mode: print named functions' assembly, no comparison.
1633
1664
 
1634
1665
  The presentation adapts to how many matrix entries compiled: one
1635
1666
  gives a plain listing with a stats table, exactly two give the
1636
1667
  --across side-by-side, more give one block per compiler. --layout
1637
1668
  forces list or side-by-side instead.
1669
+
1670
+ --filter adds every matching function as a full peer of the named
1671
+ ones - the way to reach compiler-generated clones ($constprop$0,
1672
+ .isra.0) whose exact names only the -S output knows. Named
1673
+ functions must exist under every compiler; filter matches are
1674
+ lenient and appear only where that compiler emitted them, since a
1675
+ clone existing under one compiler but not another is a finding,
1676
+ not an error.
1638
1677
  """
1678
+ pattern = _compile_filter(filter_regex) if filter_regex else None
1639
1679
  usable = []
1640
1680
  for idx, cc_cmd in enumerate(matrix, start=1):
1641
1681
  asm = compile_to_asm(cc_cmd, extra_flags, source, tmp)
@@ -1650,10 +1690,24 @@ def run_inspect(source, matrix, fn_names, layout, extra_flags, tmp):
1650
1690
  sys.exit("error: function(s) not in asm: " + ", ".join(missing)
1651
1691
  + f" under {cc_cmd}; functions seen: "
1652
1692
  + ", ".join(funcs))
1693
+ matched = []
1694
+ if pattern is not None:
1695
+ union = {}
1696
+ for _, _, funcs in usable:
1697
+ union.update(dict.fromkeys(funcs))
1698
+ matched = [n for n in union
1699
+ if pattern.search(n) and n not in fn_names]
1700
+ if not matched and not fn_names:
1701
+ sys.exit(f"error: --filter {filter_regex!r} matched no function "
1702
+ f"in {source}")
1703
+
1704
+ def selected(funcs):
1705
+ return list(fn_names) + [m for m in matched if m in funcs]
1706
+
1653
1707
  if JSON_OUT is not None:
1654
1708
  for _, cc_cmd, funcs in usable:
1655
1709
  JSON_OUT.extend(json_record(name, funcs[name], cc=cc_cmd)
1656
- for name in fn_names)
1710
+ for name in selected(funcs))
1657
1711
  return 0
1658
1712
  if layout == "side-by-side" and len(usable) < 2:
1659
1713
  sys.exit("error: --layout side-by-side needs at least two usable "
@@ -1667,18 +1721,27 @@ def run_inspect(source, matrix, fn_names, layout, extra_flags, tmp):
1667
1721
  base_tag, _, base_funcs = usable[0]
1668
1722
  for tag, _, funcs in usable[1:]:
1669
1723
  print(f"\n== {base_tag} vs {tag} ==\n")
1670
- report_across(fn_names, base_funcs, funcs, base_tag, tag)
1724
+ both = list(fn_names) + [m for m in matched
1725
+ if m in base_funcs and m in funcs]
1726
+ report_across(both, base_funcs, funcs, base_tag, tag)
1727
+ one_sided = [m for m in matched
1728
+ if (m in base_funcs) != (m in funcs)]
1729
+ if one_sided:
1730
+ print("\nnote: --filter match(es) present under only one "
1731
+ "compiler, not compared: " + ", ".join(one_sided)
1732
+ + " - inspect with -l list")
1671
1733
  return 0
1672
1734
  for _, cc_cmd, funcs in usable:
1735
+ sel = selected(funcs)
1673
1736
  if len(usable) > 1:
1674
1737
  print(f"\n== {cc_cmd} ==")
1675
1738
  if not SUMMARY_ONLY:
1676
- for name in fn_names:
1739
+ for name in sel:
1677
1740
  print()
1678
1741
  print(listing(name, funcs[name]))
1679
1742
  print()
1680
- print(inspect_table(fn_names, funcs, table_width()))
1681
- span_stats_block(fn_names, funcs)
1743
+ print(inspect_table(sel, funcs, table_width()))
1744
+ span_stats_block(sel, funcs)
1682
1745
  return 0
1683
1746
 
1684
1747
 
@@ -1709,7 +1772,7 @@ def run_objdump(objdump, elf):
1709
1772
  return proc.stdout
1710
1773
 
1711
1774
 
1712
- def run_elf(elf, fn_names, filter_regex, objdump):
1775
+ def run_elf(elf, fn_names, filter_regex, objdump, list_matches=False):
1713
1776
  """ELF mode: disassemble one linked binary and run the selected
1714
1777
  functions through the same analyzers as -S output.
1715
1778
 
@@ -1717,7 +1780,12 @@ def run_elf(elf, fn_names, filter_regex, objdump):
1717
1780
  of existence, and only the linked ELF shows whether its loops
1718
1781
  survived as zero-overhead loops inside the caller. Named functions
1719
1782
  are listed in full; --filter matches are summarized in the stats
1720
- table only, since a match can cover hundreds of functions.
1783
+ table only, since a match can cover hundreds of functions - unless
1784
+ list_matches (-l list) asks for their listings too, the way to see
1785
+ the assembly when only the filter's spelling of a mangled name is
1786
+ known. When filter matches go unlisted, a trailing note says so:
1787
+ a summary that looks complete but silently withholds the listings
1788
+ costs the caller a detour through raw objdump.
1721
1789
  """
1722
1790
  funcs = extract_functions_objdump(run_objdump(objdump, elf))
1723
1791
  missing = [f for f in fn_names if f not in funcs]
@@ -1728,10 +1796,7 @@ def run_elf(elf, fn_names, filter_regex, objdump):
1728
1796
  + ("; close matches: " + ", ".join(hints) if hints else ""))
1729
1797
  selected = list(fn_names)
1730
1798
  if filter_regex:
1731
- try:
1732
- pattern = re.compile(filter_regex)
1733
- except re.error as exc:
1734
- sys.exit(f"error: bad --filter regex: {exc}")
1799
+ pattern = _compile_filter(filter_regex)
1735
1800
  selected += [n for n in funcs
1736
1801
  if pattern.search(n) and n not in fn_names]
1737
1802
  if not selected:
@@ -1740,13 +1805,19 @@ def run_elf(elf, fn_names, filter_regex, objdump):
1740
1805
  if JSON_OUT is not None:
1741
1806
  JSON_OUT.extend(json_record(name, funcs[name]) for name in selected)
1742
1807
  return 0
1808
+ listed = selected if list_matches else fn_names
1743
1809
  if not SUMMARY_ONLY:
1744
- for name in fn_names:
1810
+ for name in listed:
1745
1811
  print()
1746
1812
  print(listing(name, funcs[name]))
1747
1813
  print()
1748
1814
  print(inspect_table(selected, funcs, table_width()))
1749
1815
  span_stats_block(selected, funcs)
1816
+ unlisted = len(selected) - len(listed)
1817
+ if unlisted and not SUMMARY_ONLY:
1818
+ print(f"\nnote: {unlisted} --filter match(es) summarized without "
1819
+ "listings; add -l list for their assembly, or name a "
1820
+ "function after the ELF")
1750
1821
  return 0
1751
1822
 
1752
1823
 
@@ -1778,12 +1849,18 @@ def main(argv=None):
1778
1849
  parser.add_argument("-l", "--layout", choices=["list", "side-by-side"],
1779
1850
  help="force the inspect presentation instead of "
1780
1851
  "adapting to the matrix (1 usable compiler "
1781
- "lists, 2 go side by side, more list)")
1852
+ "lists, 2 go side by side, more list); with "
1853
+ "ELF input, 'list' also prints --filter "
1854
+ "matches' listings")
1782
1855
  parser.add_argument("--filter", metavar="REGEX",
1783
- help="ELF input only: also analyze every function "
1784
- "whose name matches REGEX (re.search) - the "
1785
- "way to sweep a whole subsystem without "
1786
- "naming each function")
1856
+ help="also analyze every function whose name "
1857
+ "matches REGEX (re.search) - sweep a "
1858
+ "subsystem, or reach compiler-generated "
1859
+ "clones, without naming each function. In "
1860
+ "ELF input matches are summarized only (see "
1861
+ "-l list); in compile modes they are full "
1862
+ "peers of named functions. Not with "
1863
+ "--pair/--across")
1787
1864
  parser.add_argument("--objdump", metavar="PATH",
1788
1865
  help="disassembler for ELF input; default: derived "
1789
1866
  "from the first gcc in the matrix by swapping "
@@ -1885,9 +1962,13 @@ def main(argv=None):
1885
1962
  if len(sources) > 1:
1886
1963
  parser.error("ELF input analyzes one binary; a second file "
1887
1964
  "cannot be combined with it")
1888
- if args.pair or args.across or args.layout:
1889
- parser.error("--pair/--across/--layout compare compilations; "
1965
+ if args.pair or args.across:
1966
+ parser.error("--pair/--across compare compilations; "
1890
1967
  "an ELF is disassembled, not compiled")
1968
+ if args.layout == "side-by-side":
1969
+ parser.error("-l side-by-side compares compilations; with ELF "
1970
+ "input use -l list to also print --filter "
1971
+ "matches' listings")
1891
1972
  if not fn_names and not args.filter:
1892
1973
  parser.error("a whole ELF has too many functions to table; "
1893
1974
  "name functions after the file or select them "
@@ -1903,14 +1984,18 @@ def main(argv=None):
1903
1984
  sys.exit("error: no gcc in the matrix to derive an objdump "
1904
1985
  "from (" + "; ".join(matrix) + "); pass --objdump "
1905
1986
  "PATH or a gcc-based --cc/--target")
1906
- status = run_elf(sources[0], fn_names, args.filter, objdump)
1987
+ status = run_elf(sources[0], fn_names, args.filter, objdump,
1988
+ list_matches=args.layout == "list")
1907
1989
  if JSON_OUT is not None:
1908
1990
  print(json.dumps({"asmdiff": __version__, "mode": "elf",
1909
1991
  "elf": sources[0], "results": JSON_OUT},
1910
1992
  indent=2))
1911
1993
  return status
1912
- if args.filter or args.objdump:
1913
- parser.error("--filter/--objdump apply to ELF input only")
1994
+ if args.objdump:
1995
+ parser.error("--objdump applies to ELF input only")
1996
+ if args.filter and (args.pair or args.across):
1997
+ parser.error("--filter selects functions to inspect or summarize; "
1998
+ "--pair/--across name their functions explicitly")
1914
1999
  if len(sources) > 2:
1915
2000
  parser.error("at most two source files may be given")
1916
2001
  if fn_names and len(sources) == 2:
@@ -1919,9 +2004,9 @@ def main(argv=None):
1919
2004
  if fn_names and (args.pair or args.across):
1920
2005
  parser.error("bare function names (inspect) cannot be combined "
1921
2006
  "with --pair or --across")
1922
- if args.layout and not fn_names:
2007
+ if args.layout and not fn_names and not args.filter:
1923
2008
  parser.error("--layout only applies when inspecting functions "
1924
- "(SOURCE.c FUNC ...)")
2009
+ "(SOURCE.c FUNC ... or --filter REGEX)")
1925
2010
  if args.across and args.pair:
1926
2011
  parser.error("--across and --pair are mutually exclusive")
1927
2012
  if len(sources) == 2 and args.pair:
@@ -1938,17 +2023,19 @@ def main(argv=None):
1938
2023
  parser.error(f"--pair expects OLD:NEW, got {spec!r}")
1939
2024
 
1940
2025
  with tempfile.TemporaryDirectory(prefix="asmdiff") as tmp:
1941
- if fn_names:
2026
+ if fn_names or (args.filter and len(sources) == 1):
1942
2027
  mode = "inspect"
1943
2028
  status = run_inspect(sources[0], matrix, fn_names, args.layout,
1944
- extra_flags, tmp)
2029
+ extra_flags, tmp,
2030
+ filter_regex=args.filter)
1945
2031
  elif args.across:
1946
2032
  mode = "across"
1947
2033
  status = run_across(sources, matrix, args.across,
1948
2034
  extra_flags, tmp)
1949
2035
  elif len(sources) == 2:
1950
2036
  mode = "summary"
1951
- status = run_summary(sources, matrix, extra_flags, tmp)
2037
+ status = run_summary(sources, matrix, extra_flags, tmp,
2038
+ filter_regex=args.filter)
1952
2039
  else:
1953
2040
  mode = "pairs"
1954
2041
  status = run_pairs(sources[0], matrix, args.pair,
@@ -36,5 +36,17 @@ asmdiff = "asmdiff:main"
36
36
  [tool.hatch.version]
37
37
  path = "asmdiff.py"
38
38
 
39
+ # Explicit allowlist: hatchling's default sdist selection includes every
40
+ # file that is not VCS-ignored, which sweeps untracked working files
41
+ # (and even old dist/ artifacts) into the published tarball.
42
+ [tool.hatch.build.targets.sdist]
43
+ only-include = [
44
+ "asmdiff.py",
45
+ "test_asmdiff.py",
46
+ "asmdiff_example.c",
47
+ "asmdiff.example.toml",
48
+ "README.md",
49
+ ]
50
+
39
51
  [tool.hatch.build.targets.wheel]
40
52
  include = ["asmdiff.py"]