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.
- {asmdiff-0.3.0 → asmdiff-0.3.3}/PKG-INFO +47 -10
- {asmdiff-0.3.0 → asmdiff-0.3.3}/README.md +45 -8
- {asmdiff-0.3.0 → asmdiff-0.3.3}/asmdiff.py +125 -38
- {asmdiff-0.3.0 → asmdiff-0.3.3}/pyproject.toml +12 -0
- {asmdiff-0.3.0 → asmdiff-0.3.3}/test_asmdiff.py +106 -2
- asmdiff-0.3.0/.claude/settings.local.json +0 -12
- asmdiff-0.3.0/2026-07-06-inspect-mode.md +0 -754
- asmdiff-0.3.0/2026-07-07-codegen-hotspots-handoff.md:Zone.Identifier +0 -0
- asmdiff-0.3.0/2026-07-07-elf-mode-handoff.md +0 -156
- asmdiff-0.3.0/2026-07-07-table-layout-handoff.md +0 -90
- asmdiff-0.3.0/2026-07-10-reverb-session-feedback-handoff.md +0 -102
- asmdiff-0.3.0/CLAUDE.md +0 -33
- asmdiff-0.3.0/FIELD-TEST-REPORT.md +0 -123
- asmdiff-0.3.0/Screenshot 2026-07-07 225145.jpg +0 -0
- asmdiff-0.3.0/amy-hotspot-analysis.md +0 -214
- asmdiff-0.3.0/demo/delay_macro.c +0 -414
- asmdiff-0.3.0/demo/delay_orig.c +0 -355
- asmdiff-0.3.0/demo/delay_restrict.c +0 -361
- asmdiff-0.3.0/demo/delay_struct.c +0 -415
- asmdiff-0.3.0/demo/esp32-matrix.gif +0 -0
- asmdiff-0.3.0/demo/esp32-matrix.png +0 -0
- asmdiff-0.3.0/demo/quickstart.gif +0 -0
- asmdiff-0.3.0/demo/quickstart.png +0 -0
- asmdiff-0.3.0/dist_1/.gitignore +0 -1
- asmdiff-0.3.0/dist_1/asmdiff-0.1.1-py3-none-any.whl +0 -0
- asmdiff-0.3.0/dist_1/asmdiff-0.1.1.tar.gz +0 -0
- asmdiff-0.3.0/dist_1/asmdiff-0.1.2-py3-none-any.whl +0 -0
- asmdiff-0.3.0/dist_1/asmdiff-0.1.2.tar.gz +0 -0
- asmdiff-0.3.0/dist_1/asmdiff-0.1.3-py3-none-any.whl +0 -0
- asmdiff-0.3.0/dist_1/asmdiff-0.1.3.tar.gz +0 -0
- asmdiff-0.3.0/dist_1/asmdiff-0.1.4-py3-none-any.whl +0 -0
- asmdiff-0.3.0/dist_1/asmdiff-0.1.4.tar.gz +0 -0
- asmdiff-0.3.0/docs/superpowers/plans/2026-07-08-asmdiff-demo-gifs.md +0 -277
- asmdiff-0.3.0/docs/superpowers/specs/2026-07-07-edit-config-design.md +0 -97
- asmdiff-0.3.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-design.md +0 -147
- asmdiff-0.3.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-pipeline.mmd +0 -16
- asmdiff-0.3.0/gif-plan.md +0 -248
- asmdiff-0.3.0/hackaday-tip-draft.md +0 -101
- asmdiff-0.3.0/hn-show-draft.md +0 -57
- asmdiff-0.3.0/how-could-i-create-velvety-hollerith.md:Zone.Identifier +0 -3
- {asmdiff-0.3.0 → asmdiff-0.3.3}/.gitignore +0 -0
- {asmdiff-0.3.0 → asmdiff-0.3.3}/LICENSE +0 -0
- {asmdiff-0.3.0 → asmdiff-0.3.3}/asmdiff.example.toml +0 -0
- {asmdiff-0.3.0 → asmdiff-0.3.3}/asmdiff_example.c +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: asmdiff
|
|
3
|
-
Version: 0.3.
|
|
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).
|
|
374
|
-
| `--filter REGEX` |
|
|
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,
|
|
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*
|
|
448
|
-
|
|
449
|
-
|
|
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).
|
|
356
|
-
| `--filter REGEX` |
|
|
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,
|
|
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*
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
[--
|
|
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] [--
|
|
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.
|
|
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.
|
|
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
|
|
1537
|
-
"""
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1739
|
+
for name in sel:
|
|
1677
1740
|
print()
|
|
1678
1741
|
print(listing(name, funcs[name]))
|
|
1679
1742
|
print()
|
|
1680
|
-
print(inspect_table(
|
|
1681
|
-
span_stats_block(
|
|
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
|
-
|
|
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
|
|
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="
|
|
1784
|
-
"
|
|
1785
|
-
"
|
|
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
|
|
1889
|
-
parser.error("--pair/--across
|
|
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.
|
|
1913
|
-
parser.error("--
|
|
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"]
|