asmdiff 0.2.0__py3-none-any.whl → 0.3.3__py3-none-any.whl
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.2.0.dist-info → asmdiff-0.3.3.dist-info}/METADATA +126 -8
- asmdiff-0.3.3.dist-info/RECORD +6 -0
- {asmdiff-0.2.0.dist-info → asmdiff-0.3.3.dist-info}/WHEEL +1 -1
- asmdiff.py +543 -81
- asmdiff-0.2.0.dist-info/RECORD +0 -6
- {asmdiff-0.2.0.dist-info → asmdiff-0.3.3.dist-info}/entry_points.txt +0 -0
- {asmdiff-0.2.0.dist-info → asmdiff-0.3.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: asmdiff
|
|
3
|
-
Version: 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).
|
|
372
|
-
| `--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`. |
|
|
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
|
|
403
|
-
(
|
|
404
|
-
|
|
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
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
asmdiff.py,sha256=epHsHBHi0ApOcrhbAFG1beAU1dBA5Qo9t7ui1gOfKoM,89342
|
|
2
|
+
asmdiff-0.3.3.dist-info/METADATA,sha256=arU-JzHKkh751x0M9NBp2Sc9RueiD_fGDVA-tDoZ-u4,63100
|
|
3
|
+
asmdiff-0.3.3.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
4
|
+
asmdiff-0.3.3.dist-info/entry_points.txt,sha256=bVeIyjLm3KgWALbgRSeQo16pZ89GXZZ4sFTSWJUMdTI,41
|
|
5
|
+
asmdiff-0.3.3.dist-info/licenses/LICENSE,sha256=Y8JW9wzNeVnfyw62X3qC9cuwZCv_U67rNSF9QUE0HJs,1072
|
|
6
|
+
asmdiff-0.3.3.dist-info/RECORD,,
|
asmdiff.py
CHANGED
|
@@ -13,24 +13,34 @@ Usage:
|
|
|
13
13
|
[--pair OLD:NEW]... [--across FUNC]...
|
|
14
14
|
[--cc 'CC FLAGS']... [--target NAME]...
|
|
15
15
|
[--config PATH] [--compile-commands [PATH]]
|
|
16
|
-
[--flags-like PATH]
|
|
17
|
-
[--
|
|
16
|
+
[--flags-like PATH] [--db-includes]
|
|
17
|
+
[--filter REGEX] [--summary-only]
|
|
18
|
+
[--collapse] [--span-stats]
|
|
19
|
+
[--json] [--layout list|side-by-side] [-v]
|
|
18
20
|
[-- EXTRA_FLAGS...]
|
|
19
21
|
tools/asmdiff/asmdiff.py FIRMWARE.elf [FUNC...] [--filter REGEX]
|
|
20
|
-
[--objdump PATH]
|
|
21
|
-
|
|
22
|
+
[--objdump PATH] [--layout list]
|
|
23
|
+
[--summary-only] [--span-stats] [--json]
|
|
24
|
+
tools/asmdiff/asmdiff.py --edit-config | --example-config | --version
|
|
22
25
|
|
|
23
26
|
Five modes:
|
|
24
27
|
SOURCE.c FUNC inspect: print the named function's assembly, no
|
|
25
28
|
comparison. One usable compiler prints a listing
|
|
26
29
|
plus a stats row, exactly two print side by side,
|
|
27
30
|
more print one block per compiler; --layout forces
|
|
28
|
-
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.
|
|
29
36
|
FIRMWARE.elf ELF input (detected by magic bytes): disassemble a
|
|
30
37
|
linked binary through the toolchain's objdump and
|
|
31
38
|
run named and/or --filter REGEX matched functions
|
|
32
39
|
through the same analyzers - post-LTO/link reality
|
|
33
|
-
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
|
|
34
44
|
--objdump PATH or the first gcc in the matrix
|
|
35
45
|
(trailing gcc swapped for objdump); Xtensa
|
|
36
46
|
-mlongcalls call sequences are resolved to their
|
|
@@ -44,6 +54,14 @@ Five modes:
|
|
|
44
54
|
(neither) whole-file summary: per-function counts plus a file
|
|
45
55
|
total, for one file or side by side for two.
|
|
46
56
|
|
|
57
|
+
Four flags shape the output: --summary-only prints only the stats
|
|
58
|
+
tables, --collapse elides identical runs in side-by-side listings
|
|
59
|
+
keeping context around each difference, --span-stats follows each
|
|
60
|
+
stats table with a per-loop-span instruction mix (load/store/mul/
|
|
61
|
+
branch/other counts; branch includes calls), and --json replaces the
|
|
62
|
+
tables with one JSON document of per-function records (implies
|
|
63
|
+
--summary-only) for scripted callers.
|
|
64
|
+
|
|
47
65
|
Compilers come from --cc strings, from named targets in an asmdiff.toml
|
|
48
66
|
config file (--target NAME), from the config's `default` entry, or —
|
|
49
67
|
failing all of those — plain `gcc -O3` and `clang -O3`.
|
|
@@ -62,7 +80,11 @@ the CWD up to the repository root, and its build/ subdirectory; a source
|
|
|
62
80
|
absent from a database found that way is compiled without borrowed flags
|
|
63
81
|
(with a note) rather than being an error. --flags-like PATH lets such a
|
|
64
82
|
source borrow the flags recorded for PATH, so a modified copy compares
|
|
65
|
-
against its original under one header environment.
|
|
83
|
+
against its original under one header environment. --db-includes limits
|
|
84
|
+
any borrow to the header-search paths, re-emitted as -idirafter so they
|
|
85
|
+
cannot shadow the host's own system headers: a host target can then
|
|
86
|
+
compile a cross project's source without inheriting cross-only defines,
|
|
87
|
+
-specs, or a libc-overlay include directory.
|
|
66
88
|
"""
|
|
67
89
|
import argparse
|
|
68
90
|
import difflib
|
|
@@ -84,6 +106,10 @@ try:
|
|
|
84
106
|
except ModuleNotFoundError:
|
|
85
107
|
tomllib = None
|
|
86
108
|
|
|
109
|
+
# Single source of truth for the package version: pyproject.toml reads
|
|
110
|
+
# it from here (hatchling dynamic version).
|
|
111
|
+
__version__ = "0.3.3"
|
|
112
|
+
|
|
87
113
|
DEFAULT_COMPILERS = ["gcc", "clang"]
|
|
88
114
|
FALLBACK_FLAGS = "-O3"
|
|
89
115
|
CONFIG_NAME = "asmdiff.toml"
|
|
@@ -120,6 +146,12 @@ flags = [
|
|
|
120
146
|
cc = "clang"
|
|
121
147
|
flags = ["-O3", "-Wall", "-Wextra"]
|
|
122
148
|
|
|
149
|
+
# The name the docs use for "your native compiler" (--target host): an
|
|
150
|
+
# alias of [gcc] kept as its own table so the examples work verbatim.
|
|
151
|
+
[host]
|
|
152
|
+
cc = "gcc"
|
|
153
|
+
flags = ["-O3", "-Wall", "-Wextra"]
|
|
154
|
+
|
|
123
155
|
# cc values may use ~, $VARS, and glob patterns. A glob that matches
|
|
124
156
|
# several installed toolchains resolves to the highest version-sorted
|
|
125
157
|
# one (announced on stderr); pin the exact esp-NN directory instead if
|
|
@@ -213,6 +245,13 @@ _CC_DB_EQ_FLAGS = ("-specs", "--specs", "--sysroot")
|
|
|
213
245
|
_DB_CACHE = {}
|
|
214
246
|
_MISS_NOTED = set()
|
|
215
247
|
_BORROW_NOTED = set()
|
|
248
|
+
# --db-includes: borrow only the header-search paths from the database.
|
|
249
|
+
# A project's -I paths are valid on any target; its -D defines, forced
|
|
250
|
+
# includes, and -specs/--sysroot are tied to the arch the database was
|
|
251
|
+
# built for — this is how a host target compiles a cross project's
|
|
252
|
+
# source without inheriting cross-only flags.
|
|
253
|
+
DB_INCLUDES = False
|
|
254
|
+
_DB_SEARCH_PATH_FLAGS = ("-I", "-iquote", "-isystem", "-idirafter")
|
|
216
255
|
# --flags-like PATH: a source with no database entry borrows the flags
|
|
217
256
|
# recorded for PATH. This is how a modified copy of a project source gets
|
|
218
257
|
# the same header environment as its original, so an --across between them
|
|
@@ -383,6 +422,32 @@ def load_compile_commands(path):
|
|
|
383
422
|
return data
|
|
384
423
|
|
|
385
424
|
|
|
425
|
+
def _borrowed(flags):
|
|
426
|
+
"""Apply --db-includes to one borrowed flag list: keep the
|
|
427
|
+
header-search pairs (-I/-iquote/-isystem/-idirafter PATH), drop
|
|
428
|
+
defines, forced includes, and driver-level flags. Identity when
|
|
429
|
+
the option is off.
|
|
430
|
+
|
|
431
|
+
Kept paths are re-emitted as -idirafter — searched AFTER the
|
|
432
|
+
compiler's own system directories — because a cross project's
|
|
433
|
+
include set can overlay libc headers (ESP-IDF ships an esp_libc/
|
|
434
|
+
platform_include whose stdio.h would otherwise shadow the host's);
|
|
435
|
+
project-only headers still resolve, the host's libc wins.
|
|
436
|
+
"""
|
|
437
|
+
if not DB_INCLUDES:
|
|
438
|
+
return flags
|
|
439
|
+
kept, i = [], 0
|
|
440
|
+
while i < len(flags):
|
|
441
|
+
f = flags[i]
|
|
442
|
+
if f in _CC_DB_PATH_FLAGS: # split "-I path" pair
|
|
443
|
+
if f in _DB_SEARCH_PATH_FLAGS:
|
|
444
|
+
kept += ["-idirafter", flags[i + 1]]
|
|
445
|
+
i += 2
|
|
446
|
+
else: # -DFOO / -specs=... token
|
|
447
|
+
i += 1
|
|
448
|
+
return kept
|
|
449
|
+
|
|
450
|
+
|
|
386
451
|
def compile_commands_flags(db_path, source, missing_ok=False):
|
|
387
452
|
"""Header/define flags for ``source`` taken from a compile_commands.json.
|
|
388
453
|
|
|
@@ -401,7 +466,7 @@ def compile_commands_flags(db_path, source, missing_ok=False):
|
|
|
401
466
|
want = Path(source).resolve()
|
|
402
467
|
flags, name_seen = _lookup_entry(entries, want)
|
|
403
468
|
if flags is not None:
|
|
404
|
-
return flags
|
|
469
|
+
return _borrowed(flags)
|
|
405
470
|
if FLAGS_LIKE:
|
|
406
471
|
like = Path(FLAGS_LIKE).resolve()
|
|
407
472
|
like_flags, _ = _lookup_entry(entries, like)
|
|
@@ -413,7 +478,7 @@ def compile_commands_flags(db_path, source, missing_ok=False):
|
|
|
413
478
|
_BORROW_NOTED.add(key)
|
|
414
479
|
print(f"note: {source} not in {db_path}; borrowing the flags "
|
|
415
480
|
f"recorded for {FLAGS_LIKE}", file=sys.stderr)
|
|
416
|
-
return like_flags
|
|
481
|
+
return _borrowed(like_flags)
|
|
417
482
|
suggest = ("; --flags-like PATH can borrow another entry's flags"
|
|
418
483
|
if name_seen else "")
|
|
419
484
|
if missing_ok:
|
|
@@ -685,8 +750,8 @@ def analyze(lines):
|
|
|
685
750
|
LABEL_REF = re.compile(r"\.L[\w$.]+")
|
|
686
751
|
|
|
687
752
|
|
|
688
|
-
def
|
|
689
|
-
"""Return [(label,
|
|
753
|
+
def loop_span_ranges(lines):
|
|
754
|
+
"""Return [(label, lo, hi)] line-index ranges for loop spans.
|
|
690
755
|
|
|
691
756
|
A span is the run of instructions from a local label to the last
|
|
692
757
|
instruction that references it from below — a backward branch, which
|
|
@@ -694,9 +759,7 @@ def loop_spans(lines):
|
|
|
694
759
|
Xtensa zero-overhead loops (loop/loopnez/loopgt) reference their END
|
|
695
760
|
label instead; there the span is the instructions the loop encloses.
|
|
696
761
|
Spans are reported in order of appearance, one per label; nested
|
|
697
|
-
labels yield nested spans.
|
|
698
|
-
lie in the span — nothing about trip count or hotness, which the
|
|
699
|
-
reader must judge from the source.
|
|
762
|
+
labels yield nested spans.
|
|
700
763
|
"""
|
|
701
764
|
label_at = {ln[:-1]: i for i, ln in enumerate(lines)
|
|
702
765
|
if ln.endswith(":")}
|
|
@@ -719,13 +782,129 @@ def loop_spans(lines):
|
|
|
719
782
|
lo = min(lo, spans[ref][0])
|
|
720
783
|
hi = max(hi, spans[ref][1])
|
|
721
784
|
spans[ref] = (lo, hi)
|
|
785
|
+
return [(ref, lo, hi) for ref, (lo, hi)
|
|
786
|
+
in sorted(spans.items(), key=lambda kv: kv[1])]
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def loop_spans(lines):
|
|
790
|
+
"""Return [(label, insns)] spans for cleaned asm lines.
|
|
791
|
+
|
|
792
|
+
The count states how many instructions lie in the span — nothing
|
|
793
|
+
about trip count or hotness, which the reader must judge from the
|
|
794
|
+
source.
|
|
795
|
+
"""
|
|
722
796
|
result = []
|
|
723
|
-
for ref,
|
|
797
|
+
for ref, lo, hi in loop_span_ranges(lines):
|
|
724
798
|
insns = sum(1 for ln in lines[lo:hi + 1] if not ln.endswith(":"))
|
|
725
799
|
result.append((ref, insns))
|
|
726
800
|
return result
|
|
727
801
|
|
|
728
802
|
|
|
803
|
+
# --span-stats buckets, by mnemonic table. Covers the targets the tool
|
|
804
|
+
# routinely meets: Xtensa, RISC-V, and ARM by exact (or dot-stripped)
|
|
805
|
+
# mnemonic; x86 AT&T by the memory-operand heuristic in classify_insn,
|
|
806
|
+
# since there loads and stores are mostly spellings of mov.
|
|
807
|
+
_LOAD_MNEMONICS = frozenset("""
|
|
808
|
+
l8ui l16ui l16si l32i l32i.n l32r l32e l32ai lsi lsiu lsx lsxu
|
|
809
|
+
lb lbu lh lhu lw lwu ld flw fld c.lw c.lwsp c.ld c.ldsp c.flw c.fld
|
|
810
|
+
ldr ldrb ldrh ldrsb ldrsh ldrd ldm ldmia vldr vldm
|
|
811
|
+
pop popl popq
|
|
812
|
+
""".split())
|
|
813
|
+
_STORE_MNEMONICS = frozenset("""
|
|
814
|
+
s8i s16i s32i s32i.n s32e s32ri s32c1i ssi ssiu ssx ssxu
|
|
815
|
+
sb sh sw sd fsw fsd c.sw c.swsp c.sd c.sdsp c.fsw c.fsd
|
|
816
|
+
str strb strh strd stm stmia vstr vstm
|
|
817
|
+
push pushl pushq
|
|
818
|
+
""".split())
|
|
819
|
+
# Any multiply, including multiply-accumulate and FP forms (mull,
|
|
820
|
+
# mulsh, mula.dd.*, fmadd.s, vmla.f32, imull, mulss, ...).
|
|
821
|
+
_MUL_PREFIXES = ("mul", "imul", "fmul", "fmadd", "fmsub", "fnmadd",
|
|
822
|
+
"fnmsub", "madd", "msub", "mla", "mls", "smul", "umul",
|
|
823
|
+
"smla", "umla", "vmul", "vmla", "vmls", "vfma", "vfms")
|
|
824
|
+
_BRANCH_MNEMONICS = frozenset("""
|
|
825
|
+
beq bne blt bge bltu bgeu beqz bnez beqi bnei blti bgei bltui bgeui
|
|
826
|
+
bbci bbsi bbc bbs bany bnone ball bnall bt bf
|
|
827
|
+
blez bgez bltz bgtz bgt ble bgtu bleu
|
|
828
|
+
b bl bx blx cbz cbnz bcs bcc bmi bpl bvs bvc bhi bls bal
|
|
829
|
+
jr jal jalr ret retw tail
|
|
830
|
+
c.j c.jr c.jal c.jalr c.beqz c.bnez
|
|
831
|
+
""".split())
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def classify_insn(line):
|
|
835
|
+
"""Bucket one cleaned instruction: load/store/mul/branch/other.
|
|
836
|
+
|
|
837
|
+
Branch means any control transfer, calls included (a span's
|
|
838
|
+
outbound calls are already itemised in the calls column). For
|
|
839
|
+
mnemonics no table knows, an AT&T-style memory operand decides
|
|
840
|
+
load (source side) vs store (destination, the last operand);
|
|
841
|
+
lea and alignment nops are excluded from that heuristic first.
|
|
842
|
+
"""
|
|
843
|
+
parts = line.split(None, 1)
|
|
844
|
+
mnem = parts[0].lower()
|
|
845
|
+
rest = parts[1] if len(parts) > 1 else ""
|
|
846
|
+
base = mnem.split(".", 1)[0]
|
|
847
|
+
if mnem in _LOAD_MNEMONICS or base in _LOAD_MNEMONICS:
|
|
848
|
+
return "load"
|
|
849
|
+
if mnem in _STORE_MNEMONICS or base in _STORE_MNEMONICS:
|
|
850
|
+
return "store"
|
|
851
|
+
if base.startswith(_MUL_PREFIXES):
|
|
852
|
+
return "mul"
|
|
853
|
+
if mnem in _BRANCH_MNEMONICS or base in _BRANCH_MNEMONICS:
|
|
854
|
+
return "branch"
|
|
855
|
+
if base.startswith(("j", "call", "loop")):
|
|
856
|
+
return "branch"
|
|
857
|
+
if base.startswith(("lea", "nop")):
|
|
858
|
+
return "other"
|
|
859
|
+
if "(" in rest:
|
|
860
|
+
depth, cut = 0, -1
|
|
861
|
+
for i, ch in enumerate(rest):
|
|
862
|
+
if ch == "(":
|
|
863
|
+
depth += 1
|
|
864
|
+
elif ch == ")":
|
|
865
|
+
depth -= 1
|
|
866
|
+
elif ch == "," and depth == 0:
|
|
867
|
+
cut = i
|
|
868
|
+
return "store" if "(" in rest[cut + 1:] else "load"
|
|
869
|
+
return "other"
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
_MIX_KEYS = ("load", "store", "mul", "branch", "other")
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def span_mix(lines):
|
|
876
|
+
"""[{label, insns, load, store, mul, branch, other}] per loop span
|
|
877
|
+
of one function — the data behind --span-stats, table and JSON."""
|
|
878
|
+
result = []
|
|
879
|
+
for label, lo, hi in loop_span_ranges(lines):
|
|
880
|
+
body = [ln for ln in lines[lo:hi + 1] if not ln.endswith(":")]
|
|
881
|
+
counts = dict.fromkeys(_MIX_KEYS, 0)
|
|
882
|
+
for ln in body:
|
|
883
|
+
counts[classify_insn(ln)] += 1
|
|
884
|
+
entry = {"label": label, "insns": len(body)}
|
|
885
|
+
entry.update(counts)
|
|
886
|
+
result.append(entry)
|
|
887
|
+
return result
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def span_stats_table(fn_names, funcs, max_width=None):
|
|
891
|
+
"""Per-span instruction mix for the named functions.
|
|
892
|
+
|
|
893
|
+
One row per loop span: how many of its instructions load, store,
|
|
894
|
+
multiply, or branch. This weighs the span the way the doctrine
|
|
895
|
+
asks — the hand-written awk this replaces kept counting past the
|
|
896
|
+
loop end into the epilogue.
|
|
897
|
+
"""
|
|
898
|
+
rows = [("function", "span", "insns") + _MIX_KEYS]
|
|
899
|
+
for name in fn_names:
|
|
900
|
+
for mix in span_mix(funcs[name]):
|
|
901
|
+
rows.append((name, mix["label"], str(mix["insns"]))
|
|
902
|
+
+ tuple(str(mix[k]) for k in _MIX_KEYS))
|
|
903
|
+
if len(rows) == 1:
|
|
904
|
+
return "(no loop spans)"
|
|
905
|
+
return render_table(rows, max_width)
|
|
906
|
+
|
|
907
|
+
|
|
729
908
|
def auto_pairs(names):
|
|
730
909
|
"""Pair old_X with new_X for every X present in both."""
|
|
731
910
|
names = list(names)
|
|
@@ -941,10 +1120,62 @@ def build_matrix(cc_args, target_args, config, config_path, db_arg=None,
|
|
|
941
1120
|
|
|
942
1121
|
# --verbose: print full compiler command lines and untrimmed stderr.
|
|
943
1122
|
VERBOSE = False
|
|
1123
|
+
# --summary-only: print the stats tables and skip every listing. The
|
|
1124
|
+
# tables are the decision input; a listing is pulled on a second run
|
|
1125
|
+
# when a delta needs explaining.
|
|
1126
|
+
SUMMARY_ONLY = False
|
|
1127
|
+
# --span-stats: follow each stats table with the per-loop-span
|
|
1128
|
+
# instruction mix (see span_stats_table).
|
|
1129
|
+
SPAN_STATS = False
|
|
1130
|
+
# --json: collect summary records here instead of printing tables;
|
|
1131
|
+
# None means normal table output. main() dumps the collected list.
|
|
1132
|
+
JSON_OUT = None
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def span_stats_block(fn_names, funcs):
|
|
1136
|
+
"""Print the per-span mix table after a stats table when
|
|
1137
|
+
--span-stats is on; a silent no-op otherwise."""
|
|
1138
|
+
if SPAN_STATS and JSON_OUT is None:
|
|
1139
|
+
print()
|
|
1140
|
+
print(span_stats_table(fn_names, funcs, table_width()))
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def json_record(name, lines, cc=None, tag=None, role=None):
|
|
1144
|
+
"""One --json result: a summary-table row as data. cc is the
|
|
1145
|
+
resolved compiler command, tag the source/side label of a two-file
|
|
1146
|
+
comparison, role baseline/candidate for paired rows; each is
|
|
1147
|
+
omitted where the mode has no such notion."""
|
|
1148
|
+
insns, calls = analyze(lines)
|
|
1149
|
+
rec = {"function": name}
|
|
1150
|
+
if cc is not None:
|
|
1151
|
+
rec["cc"] = str(cc)
|
|
1152
|
+
if tag is not None:
|
|
1153
|
+
rec["tag"] = tag
|
|
1154
|
+
if role is not None:
|
|
1155
|
+
rec["role"] = role
|
|
1156
|
+
rec["insns"] = insns
|
|
1157
|
+
rec["loop_spans"] = [{"label": label, "insns": n}
|
|
1158
|
+
for label, n in loop_spans(lines)]
|
|
1159
|
+
rec["calls"] = calls
|
|
1160
|
+
if SPAN_STATS:
|
|
1161
|
+
rec["span_stats"] = span_mix(lines)
|
|
1162
|
+
return rec
|
|
944
1163
|
# Without --verbose, a failed compile shows this many stderr lines — enough
|
|
945
1164
|
# for the include chain plus the first error, which is the actionable part.
|
|
946
1165
|
MAX_STDERR_LINES = 20
|
|
947
1166
|
|
|
1167
|
+
# Compilers skipped because their binary was missing, in matrix order.
|
|
1168
|
+
# The final no-usable-compiler error repeats them (via _skipped_suffix)
|
|
1169
|
+
# so the fix is visible in the error itself, not only in an earlier
|
|
1170
|
+
# stderr warning that may have scrolled away.
|
|
1171
|
+
_SKIPPED_CCS = []
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def _skipped_suffix():
|
|
1175
|
+
"""" (cc: not found on PATH; ...)" naming every skipped compiler,
|
|
1176
|
+
empty when nothing was skipped."""
|
|
1177
|
+
return f" ({'; '.join(_SKIPPED_CCS)})" if _SKIPPED_CCS else ""
|
|
1178
|
+
|
|
948
1179
|
|
|
949
1180
|
def _compile_failure(cmd, stderr):
|
|
950
1181
|
"""Exit for a failed compile.
|
|
@@ -989,6 +1220,9 @@ def compile_to_asm(cc_cmd, extra_flags, harness, out_dir):
|
|
|
989
1220
|
if shutil.which(argv[0]) is None:
|
|
990
1221
|
print(f"warning: {argv[0]} not found on PATH, skipping",
|
|
991
1222
|
file=sys.stderr)
|
|
1223
|
+
miss = f"{argv[0]}: not found on PATH"
|
|
1224
|
+
if miss not in _SKIPPED_CCS:
|
|
1225
|
+
_SKIPPED_CCS.append(miss)
|
|
992
1226
|
return None
|
|
993
1227
|
db = getattr(cc_cmd, "compile_commands", None)
|
|
994
1228
|
db_flags = (compile_commands_flags(
|
|
@@ -1039,14 +1273,53 @@ def mangled_pair_hint(names):
|
|
|
1039
1273
|
return any(MANGLED_PAIR.match(n) for n in names)
|
|
1040
1274
|
|
|
1041
1275
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1276
|
+
# --collapse keeps this many line pairs of context around each
|
|
1277
|
+
# differing pair; longer identical runs are elided with a count.
|
|
1278
|
+
COLLAPSE_CONTEXT = 3
|
|
1279
|
+
# --collapse: elide identical runs in side-by-side listings.
|
|
1280
|
+
COLLAPSE = False
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def side_by_side(left, right, ltitle, rtitle, width=44, collapse=False):
|
|
1284
|
+
"""Two-column view of a pair's asm lines.
|
|
1285
|
+
|
|
1286
|
+
With collapse, runs of identical line pairs shrink to the
|
|
1287
|
+
COLLAPSE_CONTEXT pairs around each difference plus an elision
|
|
1288
|
+
marker — two ~500-insn functions differing by a handful of
|
|
1289
|
+
instructions render as a few hunks instead of ~1000 lines. The
|
|
1290
|
+
sides are aligned first (difflib), not paired by position: an
|
|
1291
|
+
inserted instruction gets a gap opposite it instead of desyncing
|
|
1292
|
+
every following pair.
|
|
1293
|
+
"""
|
|
1044
1294
|
rows = [f"{ltitle:<{width}} | {rtitle}",
|
|
1045
1295
|
f"{'-' * width}-+-{'-' * width}"]
|
|
1046
|
-
|
|
1296
|
+
if collapse:
|
|
1297
|
+
pairs = []
|
|
1298
|
+
matcher = difflib.SequenceMatcher(a=left, b=right, autojunk=False)
|
|
1299
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
1300
|
+
pairs += zip_longest(left[i1:i2], right[j1:j2], fillvalue="")
|
|
1301
|
+
keep = [False] * len(pairs)
|
|
1302
|
+
for i, (l, r) in enumerate(pairs):
|
|
1303
|
+
if l != r:
|
|
1304
|
+
for j in range(max(0, i - COLLAPSE_CONTEXT),
|
|
1305
|
+
min(len(pairs), i + COLLAPSE_CONTEXT + 1)):
|
|
1306
|
+
keep[j] = True
|
|
1307
|
+
else:
|
|
1308
|
+
pairs = list(zip_longest(left, right, fillvalue=""))
|
|
1309
|
+
keep = [True] * len(pairs)
|
|
1310
|
+
elided = 0
|
|
1311
|
+
for (l, r), kept in zip(pairs, keep):
|
|
1312
|
+
if not kept:
|
|
1313
|
+
elided += 1
|
|
1314
|
+
continue
|
|
1315
|
+
if elided:
|
|
1316
|
+
rows.append(f" ... {elided} identical lines ...")
|
|
1317
|
+
elided = 0
|
|
1047
1318
|
l = l.expandtabs(8)[:width]
|
|
1048
1319
|
r = r.expandtabs(8)[:width]
|
|
1049
1320
|
rows.append(f"{l:<{width}} | {r}")
|
|
1321
|
+
if elided:
|
|
1322
|
+
rows.append(f" ... {elided} identical lines ...")
|
|
1050
1323
|
return "\n".join(rows)
|
|
1051
1324
|
|
|
1052
1325
|
|
|
@@ -1187,7 +1460,8 @@ def file_tags(a, b):
|
|
|
1187
1460
|
return str(a), str(b)
|
|
1188
1461
|
|
|
1189
1462
|
|
|
1190
|
-
def report_across(fn_names, left_funcs, right_funcs, left_tag, right_tag
|
|
1463
|
+
def report_across(fn_names, left_funcs, right_funcs, left_tag, right_tag,
|
|
1464
|
+
ccs=(None, None)):
|
|
1191
1465
|
"""Side-by-side + summary for the same functions from two compilations."""
|
|
1192
1466
|
missing = sorted({f for f in fn_names
|
|
1193
1467
|
if f not in left_funcs or f not in right_funcs})
|
|
@@ -1195,15 +1469,25 @@ def report_across(fn_names, left_funcs, right_funcs, left_tag, right_tag):
|
|
|
1195
1469
|
sys.exit("error: function(s) not in asm: " + ", ".join(missing)
|
|
1196
1470
|
+ f"; {left_tag} has: " + (", ".join(left_funcs) or "none")
|
|
1197
1471
|
+ f"; {right_tag} has: " + (", ".join(right_funcs) or "none"))
|
|
1472
|
+
if JSON_OUT is not None:
|
|
1473
|
+
for f in fn_names:
|
|
1474
|
+
JSON_OUT.append(json_record(f, left_funcs[f], cc=ccs[0],
|
|
1475
|
+
tag=left_tag, role="baseline"))
|
|
1476
|
+
JSON_OUT.append(json_record(f, right_funcs[f], cc=ccs[1],
|
|
1477
|
+
tag=right_tag, role="candidate"))
|
|
1478
|
+
return
|
|
1198
1479
|
decorated, pairs = {}, []
|
|
1199
1480
|
for f in fn_names:
|
|
1200
1481
|
lt, rt = f"{f} [{left_tag}]", f"{f} [{right_tag}]"
|
|
1201
1482
|
decorated[lt], decorated[rt] = left_funcs[f], right_funcs[f]
|
|
1202
1483
|
pairs.append((lt, rt))
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1484
|
+
if not SUMMARY_ONLY:
|
|
1485
|
+
for lt, rt in pairs:
|
|
1486
|
+
print(side_by_side(decorated[lt], decorated[rt], lt, rt,
|
|
1487
|
+
collapse=COLLAPSE))
|
|
1488
|
+
print()
|
|
1206
1489
|
print(summary_table(pairs, decorated, table_width()))
|
|
1490
|
+
span_stats_block([n for p in pairs for n in p], decorated)
|
|
1207
1491
|
|
|
1208
1492
|
|
|
1209
1493
|
def run_across(sources, matrix, fn_names, extra_flags, tmp):
|
|
@@ -1227,10 +1511,13 @@ def run_across(sources, matrix, fn_names, extra_flags, tmp):
|
|
|
1227
1511
|
ran_any = True
|
|
1228
1512
|
tags = mark_db_misses(cc_cmd, sources,
|
|
1229
1513
|
file_tags(sources[0], sources[1]))
|
|
1230
|
-
|
|
1231
|
-
|
|
1514
|
+
if JSON_OUT is None:
|
|
1515
|
+
print(f"\n== {cc_cmd} ==\n")
|
|
1516
|
+
report_across(fn_names, sides[0], sides[1], *tags,
|
|
1517
|
+
ccs=(cc_cmd, cc_cmd))
|
|
1232
1518
|
if not ran_any:
|
|
1233
|
-
sys.exit("error: no usable compiler in the matrix"
|
|
1519
|
+
sys.exit("error: no usable compiler in the matrix"
|
|
1520
|
+
+ _skipped_suffix())
|
|
1234
1521
|
return 0
|
|
1235
1522
|
|
|
1236
1523
|
usable = []
|
|
@@ -1240,43 +1527,78 @@ def run_across(sources, matrix, fn_names, extra_flags, tmp):
|
|
|
1240
1527
|
usable.append((f"cc#{idx}", cc_cmd, extract_functions(asm)))
|
|
1241
1528
|
if len(usable) < 2:
|
|
1242
1529
|
sys.exit("error: --across needs at least two usable compilers "
|
|
1243
|
-
"in the matrix")
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1530
|
+
"in the matrix" + _skipped_suffix())
|
|
1531
|
+
if JSON_OUT is None:
|
|
1532
|
+
print()
|
|
1533
|
+
for tag, cc_cmd, _ in usable:
|
|
1534
|
+
print(f"{tag}: {cc_cmd}")
|
|
1535
|
+
base_tag, base_cc, base_funcs = usable[0]
|
|
1536
|
+
for tag, cc_cmd, funcs in usable[1:]:
|
|
1537
|
+
if JSON_OUT is None:
|
|
1538
|
+
print(f"\n== {base_tag} vs {tag} ==\n")
|
|
1539
|
+
report_across(fn_names, base_funcs, funcs, base_tag, tag,
|
|
1540
|
+
ccs=(base_cc, cc_cmd))
|
|
1251
1541
|
return 0
|
|
1252
1542
|
|
|
1253
1543
|
|
|
1254
|
-
def
|
|
1255
|
-
"""
|
|
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
|
|
1256
1559
|
tags = (file_tags(*sources) if len(sources) == 2
|
|
1257
1560
|
else [Path(sources[0]).name])
|
|
1258
1561
|
ran_any = False
|
|
1562
|
+
matched_any = False
|
|
1259
1563
|
for cc_cmd in matrix:
|
|
1260
1564
|
sections = []
|
|
1261
1565
|
for src in sources:
|
|
1262
1566
|
asm = compile_to_asm(cc_cmd, extra_flags, src, tmp)
|
|
1263
1567
|
if asm is None:
|
|
1264
1568
|
break
|
|
1265
|
-
|
|
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)
|
|
1266
1573
|
if len(sections) < len(sources):
|
|
1267
1574
|
continue
|
|
1268
1575
|
ran_any = True
|
|
1576
|
+
matched_any = matched_any or any(sections)
|
|
1269
1577
|
shown = (mark_db_misses(cc_cmd, sources, tags)
|
|
1270
1578
|
if len(sources) == 2 else tags)
|
|
1271
|
-
|
|
1579
|
+
if JSON_OUT is None:
|
|
1580
|
+
print(f"\n== {cc_cmd} ==")
|
|
1272
1581
|
for tag, funcs in zip(shown, sections):
|
|
1582
|
+
if JSON_OUT is not None:
|
|
1583
|
+
JSON_OUT.extend(
|
|
1584
|
+
json_record(name, lines, cc=cc_cmd,
|
|
1585
|
+
tag=tag if len(sections) > 1 else None)
|
|
1586
|
+
for name, lines in funcs.items())
|
|
1587
|
+
continue
|
|
1273
1588
|
if len(sections) > 1:
|
|
1274
1589
|
print(f"\n-- {tag} --")
|
|
1275
1590
|
print()
|
|
1276
1591
|
print(file_summary_table(funcs, table_width()) if funcs
|
|
1277
|
-
else "(no functions
|
|
1592
|
+
else ("(no functions match --filter)" if pattern is not None
|
|
1593
|
+
else "(no functions found)"))
|
|
1594
|
+
if funcs:
|
|
1595
|
+
span_stats_block(list(funcs), funcs)
|
|
1278
1596
|
if not ran_any:
|
|
1279
|
-
sys.exit("error: no usable compiler in the matrix"
|
|
1597
|
+
sys.exit("error: no usable compiler in the matrix"
|
|
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")
|
|
1280
1602
|
return 0
|
|
1281
1603
|
|
|
1282
1604
|
|
|
@@ -1300,46 +1622,93 @@ def run_pairs(source, matrix, pair_specs, extra_flags, tmp):
|
|
|
1300
1622
|
print('note: C++ mangling defeats old_*/new_* auto-pairing; '
|
|
1301
1623
|
'declare the pairs extern "C" or use --pair with the '
|
|
1302
1624
|
'mangled names', file=sys.stderr)
|
|
1625
|
+
if JSON_OUT is not None:
|
|
1626
|
+
JSON_OUT.extend(json_record(name, lines, cc=cc_cmd)
|
|
1627
|
+
for name, lines in funcs.items())
|
|
1628
|
+
continue
|
|
1303
1629
|
print(f"\n== {cc_cmd} ==\n")
|
|
1304
1630
|
print(file_summary_table(funcs, table_width()) if funcs
|
|
1305
1631
|
else "(no functions found)")
|
|
1632
|
+
if funcs:
|
|
1633
|
+
span_stats_block(list(funcs), funcs)
|
|
1306
1634
|
continue
|
|
1307
1635
|
missing = sorted({n for p in pairs for n in p if n not in funcs})
|
|
1308
1636
|
if missing:
|
|
1309
1637
|
sys.exit("error: function(s) not in asm: "
|
|
1310
1638
|
+ ", ".join(missing)
|
|
1311
1639
|
+ "; functions seen: " + ", ".join(funcs))
|
|
1640
|
+
if JSON_OUT is not None:
|
|
1641
|
+
for old, new in pairs:
|
|
1642
|
+
JSON_OUT.append(json_record(old, funcs[old], cc=cc_cmd,
|
|
1643
|
+
role="baseline"))
|
|
1644
|
+
JSON_OUT.append(json_record(new, funcs[new], cc=cc_cmd,
|
|
1645
|
+
role="candidate"))
|
|
1646
|
+
continue
|
|
1312
1647
|
print(f"\n== {cc_cmd} ==\n")
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1648
|
+
if not SUMMARY_ONLY:
|
|
1649
|
+
for old, new in pairs:
|
|
1650
|
+
print(side_by_side(funcs[old], funcs[new], old, new,
|
|
1651
|
+
collapse=COLLAPSE))
|
|
1652
|
+
print()
|
|
1316
1653
|
print(summary_table(pairs, funcs, table_width()))
|
|
1654
|
+
span_stats_block([n for p in pairs for n in p], funcs)
|
|
1317
1655
|
if not ran_any:
|
|
1318
|
-
sys.exit("error: no usable compiler in the matrix"
|
|
1656
|
+
sys.exit("error: no usable compiler in the matrix"
|
|
1657
|
+
+ _skipped_suffix())
|
|
1319
1658
|
return 0
|
|
1320
1659
|
|
|
1321
1660
|
|
|
1322
|
-
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):
|
|
1323
1663
|
"""Inspect mode: print named functions' assembly, no comparison.
|
|
1324
1664
|
|
|
1325
1665
|
The presentation adapts to how many matrix entries compiled: one
|
|
1326
1666
|
gives a plain listing with a stats table, exactly two give the
|
|
1327
1667
|
--across side-by-side, more give one block per compiler. --layout
|
|
1328
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.
|
|
1329
1677
|
"""
|
|
1678
|
+
pattern = _compile_filter(filter_regex) if filter_regex else None
|
|
1330
1679
|
usable = []
|
|
1331
1680
|
for idx, cc_cmd in enumerate(matrix, start=1):
|
|
1332
1681
|
asm = compile_to_asm(cc_cmd, extra_flags, source, tmp)
|
|
1333
1682
|
if asm is not None:
|
|
1334
1683
|
usable.append((f"cc#{idx}", cc_cmd, extract_functions(asm)))
|
|
1335
1684
|
if not usable:
|
|
1336
|
-
sys.exit("error: no usable compiler in the matrix"
|
|
1685
|
+
sys.exit("error: no usable compiler in the matrix"
|
|
1686
|
+
+ _skipped_suffix())
|
|
1337
1687
|
for _, cc_cmd, funcs in usable:
|
|
1338
1688
|
missing = sorted({f for f in fn_names if f not in funcs})
|
|
1339
1689
|
if missing:
|
|
1340
1690
|
sys.exit("error: function(s) not in asm: " + ", ".join(missing)
|
|
1341
1691
|
+ f" under {cc_cmd}; functions seen: "
|
|
1342
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
|
+
|
|
1707
|
+
if JSON_OUT is not None:
|
|
1708
|
+
for _, cc_cmd, funcs in usable:
|
|
1709
|
+
JSON_OUT.extend(json_record(name, funcs[name], cc=cc_cmd)
|
|
1710
|
+
for name in selected(funcs))
|
|
1711
|
+
return 0
|
|
1343
1712
|
if layout == "side-by-side" and len(usable) < 2:
|
|
1344
1713
|
sys.exit("error: --layout side-by-side needs at least two usable "
|
|
1345
1714
|
"compilers in the matrix")
|
|
@@ -1352,16 +1721,27 @@ def run_inspect(source, matrix, fn_names, layout, extra_flags, tmp):
|
|
|
1352
1721
|
base_tag, _, base_funcs = usable[0]
|
|
1353
1722
|
for tag, _, funcs in usable[1:]:
|
|
1354
1723
|
print(f"\n== {base_tag} vs {tag} ==\n")
|
|
1355
|
-
|
|
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")
|
|
1356
1733
|
return 0
|
|
1357
1734
|
for _, cc_cmd, funcs in usable:
|
|
1735
|
+
sel = selected(funcs)
|
|
1358
1736
|
if len(usable) > 1:
|
|
1359
1737
|
print(f"\n== {cc_cmd} ==")
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1738
|
+
if not SUMMARY_ONLY:
|
|
1739
|
+
for name in sel:
|
|
1740
|
+
print()
|
|
1741
|
+
print(listing(name, funcs[name]))
|
|
1363
1742
|
print()
|
|
1364
|
-
print(inspect_table(
|
|
1743
|
+
print(inspect_table(sel, funcs, table_width()))
|
|
1744
|
+
span_stats_block(sel, funcs)
|
|
1365
1745
|
return 0
|
|
1366
1746
|
|
|
1367
1747
|
|
|
@@ -1392,7 +1772,7 @@ def run_objdump(objdump, elf):
|
|
|
1392
1772
|
return proc.stdout
|
|
1393
1773
|
|
|
1394
1774
|
|
|
1395
|
-
def run_elf(elf, fn_names, filter_regex, objdump):
|
|
1775
|
+
def run_elf(elf, fn_names, filter_regex, objdump, list_matches=False):
|
|
1396
1776
|
"""ELF mode: disassemble one linked binary and run the selected
|
|
1397
1777
|
functions through the same analyzers as -S output.
|
|
1398
1778
|
|
|
@@ -1400,7 +1780,12 @@ def run_elf(elf, fn_names, filter_regex, objdump):
|
|
|
1400
1780
|
of existence, and only the linked ELF shows whether its loops
|
|
1401
1781
|
survived as zero-overhead loops inside the caller. Named functions
|
|
1402
1782
|
are listed in full; --filter matches are summarized in the stats
|
|
1403
|
-
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.
|
|
1404
1789
|
"""
|
|
1405
1790
|
funcs = extract_functions_objdump(run_objdump(objdump, elf))
|
|
1406
1791
|
missing = [f for f in fn_names if f not in funcs]
|
|
@@ -1411,20 +1796,28 @@ def run_elf(elf, fn_names, filter_regex, objdump):
|
|
|
1411
1796
|
+ ("; close matches: " + ", ".join(hints) if hints else ""))
|
|
1412
1797
|
selected = list(fn_names)
|
|
1413
1798
|
if filter_regex:
|
|
1414
|
-
|
|
1415
|
-
pattern = re.compile(filter_regex)
|
|
1416
|
-
except re.error as exc:
|
|
1417
|
-
sys.exit(f"error: bad --filter regex: {exc}")
|
|
1799
|
+
pattern = _compile_filter(filter_regex)
|
|
1418
1800
|
selected += [n for n in funcs
|
|
1419
1801
|
if pattern.search(n) and n not in fn_names]
|
|
1420
1802
|
if not selected:
|
|
1421
1803
|
sys.exit(f"error: --filter {filter_regex!r} matched no function "
|
|
1422
1804
|
f"among the {len(funcs)} in {elf}")
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1805
|
+
if JSON_OUT is not None:
|
|
1806
|
+
JSON_OUT.extend(json_record(name, funcs[name]) for name in selected)
|
|
1807
|
+
return 0
|
|
1808
|
+
listed = selected if list_matches else fn_names
|
|
1809
|
+
if not SUMMARY_ONLY:
|
|
1810
|
+
for name in listed:
|
|
1811
|
+
print()
|
|
1812
|
+
print(listing(name, funcs[name]))
|
|
1426
1813
|
print()
|
|
1427
1814
|
print(inspect_table(selected, funcs, table_width()))
|
|
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")
|
|
1428
1821
|
return 0
|
|
1429
1822
|
|
|
1430
1823
|
|
|
@@ -1456,12 +1849,18 @@ def main(argv=None):
|
|
|
1456
1849
|
parser.add_argument("-l", "--layout", choices=["list", "side-by-side"],
|
|
1457
1850
|
help="force the inspect presentation instead of "
|
|
1458
1851
|
"adapting to the matrix (1 usable compiler "
|
|
1459
|
-
"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")
|
|
1460
1855
|
parser.add_argument("--filter", metavar="REGEX",
|
|
1461
|
-
help="
|
|
1462
|
-
"
|
|
1463
|
-
"
|
|
1464
|
-
"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")
|
|
1465
1864
|
parser.add_argument("--objdump", metavar="PATH",
|
|
1466
1865
|
help="disassembler for ELF input; default: derived "
|
|
1467
1866
|
"from the first gcc in the matrix by swapping "
|
|
@@ -1487,6 +1886,14 @@ def main(argv=None):
|
|
|
1487
1886
|
help=f"config file; default search: {CONFIG_NAME} "
|
|
1488
1887
|
"next to SOURCE.c, in the current directory, "
|
|
1489
1888
|
"then in ~/.config/")
|
|
1889
|
+
parser.add_argument("--db-includes", action="store_true",
|
|
1890
|
+
help="borrow only the header-search paths from the "
|
|
1891
|
+
"database (re-emitted as -idirafter, so they "
|
|
1892
|
+
"never shadow the host's system headers), "
|
|
1893
|
+
"dropping its defines, forced includes, and "
|
|
1894
|
+
"-specs/--sysroot - lets a host/foreign-arch "
|
|
1895
|
+
"target resolve a cross project's headers "
|
|
1896
|
+
"without inheriting cross-only flags")
|
|
1490
1897
|
parser.add_argument("--flags-like", metavar="PATH",
|
|
1491
1898
|
help="when a source has no compile_commands entry, "
|
|
1492
1899
|
"borrow the include/define flags recorded for "
|
|
@@ -1502,14 +1909,45 @@ def main(argv=None):
|
|
|
1502
1909
|
help="print the built-in example config (the "
|
|
1503
1910
|
"repository's asmdiff.example.toml) to stdout, "
|
|
1504
1911
|
"ready to redirect into a config file")
|
|
1912
|
+
parser.add_argument("--span-stats", action="store_true",
|
|
1913
|
+
help="follow each stats table with a per-loop-span "
|
|
1914
|
+
"instruction mix (load/store/mul/branch/other "
|
|
1915
|
+
"counts) - weighs the span instead of the "
|
|
1916
|
+
"whole function")
|
|
1917
|
+
parser.add_argument("--collapse", action="store_true",
|
|
1918
|
+
help="in side-by-side listings, elide runs of "
|
|
1919
|
+
"identical line pairs, keeping "
|
|
1920
|
+
f"{COLLAPSE_CONTEXT} lines of context around "
|
|
1921
|
+
"each difference - near-identical functions "
|
|
1922
|
+
"render as a few hunks")
|
|
1923
|
+
parser.add_argument("--json", action="store_true",
|
|
1924
|
+
help="emit the summary as JSON on stdout instead "
|
|
1925
|
+
"of tables: one record per function per "
|
|
1926
|
+
"compiler (insns, loop spans, calls; span "
|
|
1927
|
+
"mix with --span-stats). Implies "
|
|
1928
|
+
"--summary-only; errors stay plain text on "
|
|
1929
|
+
"stderr")
|
|
1930
|
+
parser.add_argument("-s", "--summary-only", action="store_true",
|
|
1931
|
+
help="print only the summary/stats tables, "
|
|
1932
|
+
"suppressing every assembly listing - the "
|
|
1933
|
+
"scripted-caller view (pull a listing with a "
|
|
1934
|
+
"second run when a delta needs explaining)")
|
|
1935
|
+
parser.add_argument("--version", action="version",
|
|
1936
|
+
version=f"asmdiff {__version__}")
|
|
1505
1937
|
parser.add_argument("-v", "--verbose", action="store_true",
|
|
1506
1938
|
help="on compile failure, print the full compiler "
|
|
1507
1939
|
"command and complete error output instead of "
|
|
1508
1940
|
"the first lines")
|
|
1509
1941
|
args = parser.parse_args(argv)
|
|
1510
|
-
global VERBOSE, FLAGS_LIKE
|
|
1942
|
+
global VERBOSE, FLAGS_LIKE, SUMMARY_ONLY, COLLAPSE, SPAN_STATS
|
|
1943
|
+
global DB_INCLUDES, JSON_OUT
|
|
1511
1944
|
VERBOSE = args.verbose
|
|
1512
1945
|
FLAGS_LIKE = args.flags_like
|
|
1946
|
+
SUMMARY_ONLY = args.summary_only or args.json
|
|
1947
|
+
COLLAPSE = args.collapse
|
|
1948
|
+
SPAN_STATS = args.span_stats
|
|
1949
|
+
DB_INCLUDES = args.db_includes
|
|
1950
|
+
JSON_OUT = [] if args.json else None
|
|
1513
1951
|
|
|
1514
1952
|
if args.example_config:
|
|
1515
1953
|
sys.stdout.write(EXAMPLE_CONFIG)
|
|
@@ -1524,9 +1962,13 @@ def main(argv=None):
|
|
|
1524
1962
|
if len(sources) > 1:
|
|
1525
1963
|
parser.error("ELF input analyzes one binary; a second file "
|
|
1526
1964
|
"cannot be combined with it")
|
|
1527
|
-
if args.pair or args.across
|
|
1528
|
-
parser.error("--pair/--across
|
|
1965
|
+
if args.pair or args.across:
|
|
1966
|
+
parser.error("--pair/--across compare compilations; "
|
|
1529
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")
|
|
1530
1972
|
if not fn_names and not args.filter:
|
|
1531
1973
|
parser.error("a whole ELF has too many functions to table; "
|
|
1532
1974
|
"name functions after the file or select them "
|
|
@@ -1542,9 +1984,18 @@ def main(argv=None):
|
|
|
1542
1984
|
sys.exit("error: no gcc in the matrix to derive an objdump "
|
|
1543
1985
|
"from (" + "; ".join(matrix) + "); pass --objdump "
|
|
1544
1986
|
"PATH or a gcc-based --cc/--target")
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1987
|
+
status = run_elf(sources[0], fn_names, args.filter, objdump,
|
|
1988
|
+
list_matches=args.layout == "list")
|
|
1989
|
+
if JSON_OUT is not None:
|
|
1990
|
+
print(json.dumps({"asmdiff": __version__, "mode": "elf",
|
|
1991
|
+
"elf": sources[0], "results": JSON_OUT},
|
|
1992
|
+
indent=2))
|
|
1993
|
+
return status
|
|
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")
|
|
1548
1999
|
if len(sources) > 2:
|
|
1549
2000
|
parser.error("at most two source files may be given")
|
|
1550
2001
|
if fn_names and len(sources) == 2:
|
|
@@ -1553,9 +2004,9 @@ def main(argv=None):
|
|
|
1553
2004
|
if fn_names and (args.pair or args.across):
|
|
1554
2005
|
parser.error("bare function names (inspect) cannot be combined "
|
|
1555
2006
|
"with --pair or --across")
|
|
1556
|
-
if args.layout and not fn_names:
|
|
2007
|
+
if args.layout and not fn_names and not args.filter:
|
|
1557
2008
|
parser.error("--layout only applies when inspecting functions "
|
|
1558
|
-
"(SOURCE.c FUNC ...)")
|
|
2009
|
+
"(SOURCE.c FUNC ... or --filter REGEX)")
|
|
1559
2010
|
if args.across and args.pair:
|
|
1560
2011
|
parser.error("--across and --pair are mutually exclusive")
|
|
1561
2012
|
if len(sources) == 2 and args.pair:
|
|
@@ -1572,16 +2023,27 @@ def main(argv=None):
|
|
|
1572
2023
|
parser.error(f"--pair expects OLD:NEW, got {spec!r}")
|
|
1573
2024
|
|
|
1574
2025
|
with tempfile.TemporaryDirectory(prefix="asmdiff") as tmp:
|
|
1575
|
-
if fn_names:
|
|
1576
|
-
|
|
2026
|
+
if fn_names or (args.filter and len(sources) == 1):
|
|
2027
|
+
mode = "inspect"
|
|
2028
|
+
status = run_inspect(sources[0], matrix, fn_names, args.layout,
|
|
2029
|
+
extra_flags, tmp,
|
|
2030
|
+
filter_regex=args.filter)
|
|
2031
|
+
elif args.across:
|
|
2032
|
+
mode = "across"
|
|
2033
|
+
status = run_across(sources, matrix, args.across,
|
|
2034
|
+
extra_flags, tmp)
|
|
2035
|
+
elif len(sources) == 2:
|
|
2036
|
+
mode = "summary"
|
|
2037
|
+
status = run_summary(sources, matrix, extra_flags, tmp,
|
|
2038
|
+
filter_regex=args.filter)
|
|
2039
|
+
else:
|
|
2040
|
+
mode = "pairs"
|
|
2041
|
+
status = run_pairs(sources[0], matrix, args.pair,
|
|
1577
2042
|
extra_flags, tmp)
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
return run_summary(sources, matrix, extra_flags, tmp)
|
|
1583
|
-
return run_pairs(sources[0], matrix, args.pair,
|
|
1584
|
-
extra_flags, tmp)
|
|
2043
|
+
if JSON_OUT is not None:
|
|
2044
|
+
print(json.dumps({"asmdiff": __version__, "mode": mode,
|
|
2045
|
+
"results": JSON_OUT}, indent=2))
|
|
2046
|
+
return status
|
|
1585
2047
|
|
|
1586
2048
|
|
|
1587
2049
|
if __name__ == "__main__":
|
asmdiff-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
asmdiff.py,sha256=cJrdwqqlG4ejalcvBHlSSSCylFRVIAKNYmKsiaCIer4,68504
|
|
2
|
-
asmdiff-0.2.0.dist-info/METADATA,sha256=Gxk-QQA7YDZsmB0WPq2lKHJIg1FxZIuWIkz8TyYotKc,56049
|
|
3
|
-
asmdiff-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
4
|
-
asmdiff-0.2.0.dist-info/entry_points.txt,sha256=bVeIyjLm3KgWALbgRSeQo16pZ89GXZZ4sFTSWJUMdTI,41
|
|
5
|
-
asmdiff-0.2.0.dist-info/licenses/LICENSE,sha256=Y8JW9wzNeVnfyw62X3qC9cuwZCv_U67rNSF9QUE0HJs,1072
|
|
6
|
-
asmdiff-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|