asmdiff 0.4.0__tar.gz → 0.4.1__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.
@@ -1,3 +1,21 @@
1
+ Metadata-Version: 2.5
2
+ Name: asmdiff
3
+ Version: 0.4.1
4
+ Summary: Per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
5
+ Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
6
+ Project-URL: Repository, https://github.com/rt-rtos/asmdiff
7
+ Author: Rasmus Tikkanen
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: arm,assembly,cfg,clang,cli,codegen,compiler,devtools,diff,disassembly,dsp,embedded,firmware,gcc,loop-analysis,objdump,riscv,static-analysis,xtensa
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Compilers
15
+ Classifier: Topic :: Software Development :: Debuggers
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+
1
19
  # [asmdiff](https://pypi.org/project/asmdiff/)
2
20
  ## per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
3
21
 
@@ -321,11 +339,11 @@ everywhere else; two things are ELF-specific:
321
339
  targets, not labels, so in-function targets are rewritten to
322
340
  `.L<hex-offset>` labels (the offset from the function start - stable
323
341
  across rebuilds of an unchanged function, unlike link addresses). An
324
- Xtensa zero-overhead loop's end address becomes `.L<off>_LEND`, so a
325
- hardware loop is distinguishable from a branch loop in the `loop
326
- spans` column at a glance: `render_lut` shipped its 32-instruction
327
- body as a ZOL, `render_lut_cub` fell back to a 74-instruction
328
- branch loop.
342
+ Xtensa zero-overhead loop's end address (a pi32v2 `rep` block's too)
343
+ becomes `.L<off>_LEND`, so a hardware loop is distinguishable from a
344
+ branch loop in the `loop spans` column at a glance: `render_lut`
345
+ shipped its 32-instruction body as a ZOL, `render_lut_cub` fell back
346
+ to a 74-instruction branch loop.
329
347
  - **`-mlongcalls` calls are resolved.** An out-of-range call survives
330
348
  linking as `l32r a8, <lit>` + `callx8 a8`, which has no callee name
331
349
  of its own. When the disassembly annotates the literal with its
@@ -397,6 +415,22 @@ cue to pass a matching target.
397
415
  A whole firmware has over a thousand functions, so a bare ELF with no
398
416
  function names and no `--filter` is an error, not a thousand-row table.
399
417
 
418
+ **Other toolchains.** Besides GNU objdump output, the JieLi pi32v2
419
+ objdump's dialect is read (the AC79 family, e.g. the M-VAVE FM-1 and
420
+ its open firmwares): bare `name:` headers, the space-padded byte column,
421
+ `<sym+0x96 : addr >` targets and the decimal displacement before
422
+ `call`/`goto` are normalised into the GNU shape, `.rodata` dumps are
423
+ skipped, and the algebraic instruction form (`r5_r4 = r4 * r14 (s)`,
424
+ `[r6+0] += r4`) is bucketed by shape for `--span-stats`. The
425
+ toolchain's objdump sits beside its clang, so the bundled `pi32v2`
426
+ profile (or any `--cc` naming that clang by path) derives it; otherwise
427
+ pass `--objdump`. Compile mode works with
428
+ `--cc '$JIELI_TOOLCHAIN/pi32v2/bin/clang -target pi32v2 -Os'`. The
429
+ compiler does not emit the `rep` hardware loop from C, but vendor inline
430
+ asm uses it (`rep n rN { ... }`); a `rep` block gets a span over its
431
+ body, named `.L<off>_LEND` in ELF input like an Xtensa zero-overhead
432
+ loop.
433
+
400
434
  If the question behind the sweep is "did my hot loops ship as
401
435
  zero-overhead loops", the empirical rules (verified on esp-gcc 15.2,
402
436
  `-O2`) are worth knowing before blaming the compiler:
@@ -1400,6 +1434,9 @@ names its *end* label, forward:
1400
1434
  retw.n
1401
1435
  ```
1402
1436
 
1437
+ pi32v2's `rep` hardware loop (`rep .Ltmp8 r7` in `-S` output, `rep 6 r7 {`
1438
+ in objdump) is the same case, with its body ending at the named label.
1439
+
1403
1440
  That is the entire mechanism. There is no CFG, no trip count, and no
1404
1441
  notion of "the" loop: a backward `goto` produces a span exactly like a
1405
1442
  `for` loop, and an unrolled loop's span is the unrolled body. The column
@@ -1,21 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: asmdiff
3
- Version: 0.4.0
4
- Summary: Per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
5
- Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
6
- Project-URL: Repository, https://github.com/rt-rtos/asmdiff
7
- Author: Rasmus Tikkanen
8
- License-Expression: MIT
9
- License-File: LICENSE
10
- Keywords: arm,assembly,cfg,clang,cli,codegen,compiler,devtools,diff,disassembly,dsp,embedded,firmware,gcc,loop-analysis,objdump,riscv,static-analysis,xtensa
11
- Classifier: Environment :: Console
12
- Classifier: Intended Audience :: Developers
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Topic :: Software Development :: Compilers
15
- Classifier: Topic :: Software Development :: Debuggers
16
- Requires-Python: >=3.8
17
- Description-Content-Type: text/markdown
18
-
19
1
  # [asmdiff](https://pypi.org/project/asmdiff/)
20
2
  ## per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
21
3
 
@@ -339,11 +321,11 @@ everywhere else; two things are ELF-specific:
339
321
  targets, not labels, so in-function targets are rewritten to
340
322
  `.L<hex-offset>` labels (the offset from the function start - stable
341
323
  across rebuilds of an unchanged function, unlike link addresses). An
342
- Xtensa zero-overhead loop's end address becomes `.L<off>_LEND`, so a
343
- hardware loop is distinguishable from a branch loop in the `loop
344
- spans` column at a glance: `render_lut` shipped its 32-instruction
345
- body as a ZOL, `render_lut_cub` fell back to a 74-instruction
346
- branch loop.
324
+ Xtensa zero-overhead loop's end address (a pi32v2 `rep` block's too)
325
+ becomes `.L<off>_LEND`, so a hardware loop is distinguishable from a
326
+ branch loop in the `loop spans` column at a glance: `render_lut`
327
+ shipped its 32-instruction body as a ZOL, `render_lut_cub` fell back
328
+ to a 74-instruction branch loop.
347
329
  - **`-mlongcalls` calls are resolved.** An out-of-range call survives
348
330
  linking as `l32r a8, <lit>` + `callx8 a8`, which has no callee name
349
331
  of its own. When the disassembly annotates the literal with its
@@ -415,6 +397,22 @@ cue to pass a matching target.
415
397
  A whole firmware has over a thousand functions, so a bare ELF with no
416
398
  function names and no `--filter` is an error, not a thousand-row table.
417
399
 
400
+ **Other toolchains.** Besides GNU objdump output, the JieLi pi32v2
401
+ objdump's dialect is read (the AC79 family, e.g. the M-VAVE FM-1 and
402
+ its open firmwares): bare `name:` headers, the space-padded byte column,
403
+ `<sym+0x96 : addr >` targets and the decimal displacement before
404
+ `call`/`goto` are normalised into the GNU shape, `.rodata` dumps are
405
+ skipped, and the algebraic instruction form (`r5_r4 = r4 * r14 (s)`,
406
+ `[r6+0] += r4`) is bucketed by shape for `--span-stats`. The
407
+ toolchain's objdump sits beside its clang, so the bundled `pi32v2`
408
+ profile (or any `--cc` naming that clang by path) derives it; otherwise
409
+ pass `--objdump`. Compile mode works with
410
+ `--cc '$JIELI_TOOLCHAIN/pi32v2/bin/clang -target pi32v2 -Os'`. The
411
+ compiler does not emit the `rep` hardware loop from C, but vendor inline
412
+ asm uses it (`rep n rN { ... }`); a `rep` block gets a span over its
413
+ body, named `.L<off>_LEND` in ELF input like an Xtensa zero-overhead
414
+ loop.
415
+
418
416
  If the question behind the sweep is "did my hot loops ship as
419
417
  zero-overhead loops", the empirical rules (verified on esp-gcc 15.2,
420
418
  `-O2`) are worth knowing before blaming the compiler:
@@ -1418,6 +1416,9 @@ names its *end* label, forward:
1418
1416
  retw.n
1419
1417
  ```
1420
1418
 
1419
+ pi32v2's `rep` hardware loop (`rep .Ltmp8 r7` in `-S` output, `rep 6 r7 {`
1420
+ in objdump) is the same case, with its body ending at the named label.
1421
+
1421
1422
  That is the entire mechanism. There is no CFG, no trip count, and no
1422
1423
  notion of "the" loop: a backward `goto` produces a span exactly like a
1423
1424
  `for` loop, and an unrolled loop's span is the unrolled body. The column
@@ -143,3 +143,12 @@ flags = ["-O2", "-mcpu=cortex-m4", "-mthumb"]
143
143
  [rp2350]
144
144
  cc = "riscv64-unknown-elf-gcc"
145
145
  flags = ["-O2", "-march=rv32imac_zicsr_zifencei", "-mabi=ilp32"]
146
+
147
+ # JieLi pi32v2 (the AC79 family: M-VAVE FM-1 and other JieLi-based
148
+ # devices). The vendor's clang 4.0.1 fork comes as one tarball from
149
+ # pkgman.jieliapp.com/s/linux-toolchain; unpack it anywhere and point
150
+ # JIELI_TOOLCHAIN at the directory holding pi32v2/ and common/. Its
151
+ # objdump sits beside clang, so ELF input needs no --objdump.
152
+ [pi32v2]
153
+ cc = "$JIELI_TOOLCHAIN/pi32v2/bin/clang"
154
+ flags = ["-target", "pi32v2", "-Os"]
@@ -46,8 +46,10 @@ Four modes, plus the no-flag whole-file summary:
46
46
  listed in full; --filter matches are summarized
47
47
  only, unless -l list asks for their listings too.
48
48
  objdump comes from
49
- --objdump PATH or the first gcc in the matrix
50
- (trailing gcc swapped for objdump); Xtensa
49
+ --objdump PATH, the first gcc in the matrix
50
+ (trailing gcc swapped for objdump), or an objdump
51
+ beside a clang given by path. GNU and JieLi
52
+ pi32v2 objdump output are both read; Xtensa
51
53
  -mlongcalls call sequences are resolved to their
52
54
  real callees when the disassembly names them,
53
55
  and report as indirect(<reg>) when it does not.
@@ -173,7 +175,7 @@ except ModuleNotFoundError:
173
175
 
174
176
  # Single source of truth for the package version: pyproject.toml reads
175
177
  # it from here (hatchling dynamic version).
176
- __version__ = "0.4.0"
178
+ __version__ = "0.4.1"
177
179
 
178
180
  DEFAULT_COMPILERS = ["gcc", "clang"]
179
181
  FALLBACK_FLAGS = "-O3"
@@ -330,6 +332,15 @@ flags = ["-O2", "-mcpu=cortex-m4", "-mthumb"]
330
332
  [rp2350]
331
333
  cc = "riscv64-unknown-elf-gcc"
332
334
  flags = ["-O2", "-march=rv32imac_zicsr_zifencei", "-mabi=ilp32"]
335
+
336
+ # JieLi pi32v2 (the AC79 family: M-VAVE FM-1 and other JieLi-based
337
+ # devices). The vendor's clang 4.0.1 fork comes as one tarball from
338
+ # pkgman.jieliapp.com/s/linux-toolchain; unpack it anywhere and point
339
+ # JIELI_TOOLCHAIN at the directory holding pi32v2/ and common/. Its
340
+ # objdump sits beside clang, so ELF input needs no --objdump.
341
+ [pi32v2]
342
+ cc = "$JIELI_TOOLCHAIN/pi32v2/bin/clang"
343
+ flags = ["-target", "pi32v2", "-Os"]
333
344
  """
334
345
 
335
346
  # Preprocessor flags lifted from a compile_commands.json entry so a project
@@ -669,6 +680,10 @@ NOISE = re.compile(
669
680
  )
670
681
  # Compiler-generated bracketing labels that add nothing (.LFB0:, .Lfunc_end0:).
671
682
  NOISE_LABEL = re.compile(r"^\.(LFB|LFE|Lfunc_begin|Lfunc_end)\d*:")
683
+ # clang annotates local labels in the target's comment syntax
684
+ # (".LBB2_1: ; =>This Inner Loop Header: Depth=1" - '#' on x86, '@' on
685
+ # ARM, ';' on pi32v2); the loop-span scan keys on lines ending in ':'.
686
+ LABEL_COMMENT = re.compile(r"^(\.?[\w$.]+:)\s*(?:;|#|@|//).*$")
672
687
  # Data emitted *inside* a function body: switch jump tables (.long/.word
673
688
  # entries), inline constants, strings. These are not instructions, so they
674
689
  # must not be counted; and a self-relative table entry (".long .L5-.L4")
@@ -714,29 +729,59 @@ def extract_functions(asm_text):
714
729
  current = None
715
730
  continue
716
731
  line = raw.strip()
717
- if not line or line.startswith(("#", "//")):
732
+ if not line or line.startswith(("#", "//", ";", "@")):
718
733
  continue
719
734
  if NOISE.match(line) or NOISE_LABEL.match(line) or DATA.match(line):
720
735
  continue
736
+ line = LABEL_COMMENT.sub(r"\1", line)
737
+ if line == "}": # pi32v2 predicated-block close
738
+ continue
739
+ if line == "dummy": # pi32v2 rep-end filler, no bytes
740
+ continue
721
741
  funcs[current].append(line)
722
742
  return funcs
723
743
 
724
744
 
725
745
  # `objdump -d` function header: "40370400 <render_lut>:".
726
746
  OBJDUMP_HEADER = re.compile(r"^([0-9a-f]+) <([^>]+)>:\s*$")
747
+ # JieLi's pi32v2 objdump writes the header as a bare label ("rev_hall:");
748
+ # the function's address is its first instruction's.
749
+ OBJDUMP_BARE_HEADER = re.compile(r"^([A-Za-z_][\w$.]*):\s*$")
727
750
  # `objdump -d` instruction line: address, one byte-dump field (hex digits
728
- # and spaces; "004136" is the encoding, not the mnemonic), then the
729
- # mnemonic and operands.
751
+ # and spaces; "004136" is the encoding, not the mnemonic), a tab, then
752
+ # the mnemonic and operands. GNU separates the fields with tabs; JieLi's
753
+ # pads the byte field with spaces before the tab. A data line (JieLi's
754
+ # objdump dumps .rodata too) ends in an ASCII column with no tab after
755
+ # the bytes, so it does not match.
730
756
  OBJDUMP_INSN = re.compile(
731
- r"^\s*([0-9a-f]+):\t[0-9a-f ]+\t\s*(\S+)[ \t]*(.*?)\s*$")
757
+ r"^\s*([0-9a-f]+):\s*[0-9a-f][0-9a-f ]*\t\s*(\S+)[ \t]*(.*?)\s*$")
758
+ # Section banner; data sections are skipped (GNU -d never prints them,
759
+ # JieLi's does, as bare-label symbols over byte dumps).
760
+ OBJDUMP_SECTION = re.compile(r"^Disassembly of section (\S+):")
732
761
  # An "addr <annotation>" operand: branch, loop or call target.
733
762
  OBJDUMP_TARGET = re.compile(r"\b([0-9a-f]+) <([^>]+)>")
763
+ # pi32v2 hardware loop as objdump prints it: "rep 6 r7 {" or
764
+ # "rep 20 16 {" - the body length in bytes (decimal), then the repeat
765
+ # count (register or immediate). The body starts at the next
766
+ # instruction; the closing "}" line has no address and is ignored.
767
+ OBJDUMP_REP = re.compile(r"^(\d+)\s+(\S+)\s*\{?$")
768
+ # JieLi's objdump annotates a target as "<sym+0x96 : 2039318 >" (GNU:
769
+ # "2039318 <sym+0x96>") and puts the decimal displacement before it
770
+ # ("call 762 <fm1_fatal_common : 2000458 >", "goto -26 <...>"). Both are
771
+ # rewritten to the GNU shape so the relabel and call detection apply.
772
+ JIELI_TARGET = re.compile(r"<\s*([^<>:]+?)\s*:\s*([0-9a-f]+)\s*>")
773
+ JIELI_DISP = re.compile(r"(^|goto |call )(-?\d+)\s+(?=[0-9a-f]+ <)")
774
+
775
+
776
+ def _normalize_targets(ops):
777
+ ops = JIELI_TARGET.sub(r"\2 <\1>", ops)
778
+ return JIELI_DISP.sub(r"\1", ops)
734
779
  # A nearest-symbol offset annotation ("<_etext+0x100>"): objdump's name
735
780
  # for an address that is not a symbol start. For addresses outside the
736
781
  # current function the base symbol is usually unrelated (a literal-pool
737
782
  # word or data landing after some other object), so the annotation is
738
783
  # misleading, not informative.
739
- OFFSET_ANNOT = re.compile(r"[+-]0x[0-9a-f]+$")
784
+ OFFSET_ANNOT = re.compile(r"[+-]0x[0-9a-f]+$", re.IGNORECASE)
740
785
 
741
786
 
742
787
  def extract_functions_objdump(dump_text):
@@ -749,8 +794,11 @@ def extract_functions_objdump(dump_text):
749
794
  synthetic local labels (.L<hex-offset>) inserted at the target
750
795
  instruction; an Xtensa zero-overhead loop's end address becomes
751
796
  .L<off>_LEND, keeping the ZOL-vs-branch naming convention -S output
752
- has. Targets outside the function keep a bare-symbol annotation,
753
- so calls still name their callee; offset-form annotations of other
797
+ has. A pi32v2 "rep <bytes> <count> {" block is rewritten to the
798
+ shape its -S output has ("rep .L<off>_LEND <count>"), with the
799
+ label at the address the body ends on. Targets outside the
800
+ function keep a bare-symbol annotation, so calls still name their
801
+ callee; offset-form annotations of other
754
802
  symbols are nearest-symbol noise and become raw hex addresses (see
755
803
  _relabel_objdump). Offset-form headers
756
804
  (``<sym+0x..>``/``<sym-0x..>``: literal pools and other symbol-less
@@ -758,23 +806,43 @@ def extract_functions_objdump(dump_text):
758
806
  """
759
807
  funcs = {}
760
808
  current = None
809
+ in_code = True
761
810
  for raw in dump_text.splitlines():
811
+ s = OBJDUMP_SECTION.match(raw)
812
+ if s:
813
+ in_code = not s.group(1).startswith((".rodata", ".data", ".bss"))
814
+ current = None
815
+ continue
816
+ if not in_code:
817
+ continue
762
818
  h = OBJDUMP_HEADER.match(raw)
763
819
  if h:
764
- name = h.group(2)
820
+ name, start = h.group(2), int(h.group(1), 16)
821
+ else:
822
+ h = OBJDUMP_BARE_HEADER.match(raw)
823
+ if h:
824
+ name, start = h.group(1), None
825
+ if h:
765
826
  if OFFSET_ANNOT.search(name):
766
827
  current = None # symbol-less gap, not a function
767
828
  else:
768
829
  current = []
769
- funcs[name] = (int(h.group(1), 16), current)
830
+ funcs[name] = (start, current)
770
831
  continue
771
832
  if current is None:
772
833
  continue
773
834
  m = OBJDUMP_INSN.match(raw)
774
835
  if m:
775
- current.append((int(m.group(1), 16), m.group(2), m.group(3)))
776
- return {name: _relabel_objdump(name, start, _resolve_longcalls(insns))
777
- for name, (start, insns) in funcs.items()}
836
+ current.append((int(m.group(1), 16), m.group(2),
837
+ _normalize_targets(m.group(3))))
838
+ out = {}
839
+ for name, (start, insns) in funcs.items():
840
+ if start is None:
841
+ if not insns:
842
+ continue # bare-label data symbol in a code section
843
+ start = insns[0][0]
844
+ out[name] = _relabel_objdump(name, start, _resolve_longcalls(insns))
845
+ return out
778
846
 
779
847
 
780
848
  # The value annotation objdump appends to an l32r whose literal resolves
@@ -833,7 +901,14 @@ def _relabel_objdump(name, start, insns):
833
901
  rendered as the raw hex address instead."""
834
902
  addrs = {addr for addr, _, _ in insns}
835
903
  labels = {}
836
- for _, mnem, ops in insns:
904
+ reps = {} # rep addr -> (end addr, count)
905
+ for k, (addr, mnem, ops) in enumerate(insns):
906
+ r = OBJDUMP_REP.match(ops) if mnem == "rep" else None
907
+ if r and k + 1 < len(insns):
908
+ end = insns[k + 1][0] + int(r.group(1))
909
+ if end in addrs:
910
+ reps[addr] = (end, r.group(2))
911
+ labels[end] = ".L%x_LEND" % (end - start)
837
912
  for m in OBJDUMP_TARGET.finditer(ops):
838
913
  taddr = int(m.group(1), 16)
839
914
  if taddr not in addrs:
@@ -856,7 +931,10 @@ def _relabel_objdump(name, start, insns):
856
931
  for addr, mnem, ops in insns:
857
932
  if addr in labels:
858
933
  lines.append(labels[addr] + ":")
859
- ops = OBJDUMP_TARGET.sub(target_name, ops)
934
+ if addr in reps:
935
+ ops = "%s %s" % (labels[reps[addr][0]], reps[addr][1])
936
+ else:
937
+ ops = OBJDUMP_TARGET.sub(target_name, ops)
860
938
  lines.append(mnem + "\t" + ops if ops else mnem)
861
939
  return lines
862
940
 
@@ -944,8 +1022,9 @@ def loop_span_ranges(lines):
944
1022
  A span is the run of instructions from a local label to the last
945
1023
  instruction that references it from below — a backward branch, which
946
1024
  is what a compiled loop looks like on every target the tool parses.
947
- Xtensa zero-overhead loops (loop/loopnez/loopgt) reference their END
948
- label instead; there the span is the instructions the loop encloses.
1025
+ Xtensa zero-overhead loops (loop/loopnez/loopgt) and pi32v2's rep
1026
+ reference their END label instead; there the span is the
1027
+ instructions the loop encloses.
949
1028
  Spans are reported in order of appearance, one per label; nested
950
1029
  labels yield nested spans.
951
1030
  """
@@ -962,8 +1041,8 @@ def loop_span_ranges(lines):
962
1041
  j = label_at[ref]
963
1042
  if j < i: # label above: backward branch
964
1043
  lo, hi = j, i
965
- elif mnem.startswith("loop"): # Xtensa: end label below
966
- lo, hi = i + 1, j - 1
1044
+ elif mnem.startswith("loop") or mnem == "rep":
1045
+ lo, hi = i + 1, j - 1 # Xtensa/pi32v2: end label below
967
1046
  else:
968
1047
  continue
969
1048
  if ref in spans: # several edges to one label
@@ -1051,6 +1130,33 @@ _BRANCH_MNEMONICS = frozenset("""
1051
1130
  """.split())
1052
1131
 
1053
1132
 
1133
+ # pi32v2 (JieLi) assembly is algebraic: "r4 = h[r3++=2] (s)", "[r6+0] += r4",
1134
+ # "r5_r4 = r4 * r14 (s)", "if (--r2 != 0) goto .L1". No mnemonic table
1135
+ # fits, so the shape of the line picks the bucket: memory on the left of
1136
+ # the assignment is a store (push and read-modify-write included), on
1137
+ # the right a load (pop included), a product or quotient on the right is
1138
+ # mul or div, and the if/goto/return forms are branches.
1139
+ _ALGEBRAIC = re.compile(
1140
+ r"^(?:[hbwd]?\[|\{|(?:r\d+(?:_r\d+)?(?:\.[lh])?|sp|psr|icfg)\s*[-+*/|&^<>]*="
1141
+ r"|(?:ifs?|goto|rts|ret[iex]|rets|rep)\b)")
1142
+ _ALGEBRAIC_BRANCH = frozenset("if ifs goto rts reti rete retx rets rep".split())
1143
+
1144
+
1145
+ def _classify_algebraic(line, mnem):
1146
+ if mnem in _ALGEBRAIC_BRANCH:
1147
+ return "branch"
1148
+ lhs, _, rhs = line.partition("=")
1149
+ if "[" in lhs:
1150
+ return "store"
1151
+ if "[" in rhs:
1152
+ return "load"
1153
+ if "*" in rhs:
1154
+ return "mul"
1155
+ if "/" in rhs:
1156
+ return "div"
1157
+ return "other"
1158
+
1159
+
1054
1160
  def classify_insn(line):
1055
1161
  """Bucket one cleaned instruction: load/store/mul/div/branch/other.
1056
1162
 
@@ -1059,11 +1165,14 @@ def classify_insn(line):
1059
1165
  mnemonics no table knows, an AT&T-style memory operand decides
1060
1166
  load (source side) vs store (destination, the last operand);
1061
1167
  lea and alignment nops are excluded from that heuristic first.
1168
+ pi32v2's algebraic lines are bucketed by shape (_classify_algebraic).
1062
1169
  """
1063
1170
  parts = line.split(None, 1)
1064
1171
  mnem = parts[0].lower()
1065
1172
  rest = parts[1] if len(parts) > 1 else ""
1066
1173
  base = mnem.split(".", 1)[0]
1174
+ if _ALGEBRAIC.match(line):
1175
+ return _classify_algebraic(line, mnem)
1067
1176
  if mnem in _LOAD_MNEMONICS or base in _LOAD_MNEMONICS:
1068
1177
  return "load"
1069
1178
  if mnem in _STORE_MNEMONICS or base in _STORE_MNEMONICS:
@@ -2945,12 +3054,18 @@ def run_inspect(source, matrix, fn_names, layout, extra_flags, tmp,
2945
3054
  def derive_objdump(matrix):
2946
3055
  """Objdump belonging to the first gcc in the matrix: the toolchain
2947
3056
  prefix stays, the tool name swaps (xtensa-esp32s3-elf-gcc ->
2948
- xtensa-esp32s3-elf-objdump). None when no entry is gcc-based;
2949
- clang has no paired cross-objdump to guess at."""
3057
+ xtensa-esp32s3-elf-objdump). A clang given by path counts when an
3058
+ objdump sits beside it in the same bin directory (JieLi's pi32v2
3059
+ toolchain ships one there). None otherwise: a bare clang has no
3060
+ paired cross-objdump to guess at."""
2950
3061
  for cc_cmd in matrix:
2951
3062
  cc = shlex.split(cc_cmd)[0]
2952
3063
  if cc == "gcc" or cc.endswith(("-gcc", "/gcc")):
2953
3064
  return cc[:-3] + "objdump"
3065
+ if cc.endswith("/clang"):
3066
+ sibling = cc[:-5] + "objdump"
3067
+ if os.path.exists(sibling):
3068
+ return sibling
2954
3069
  return None
2955
3070
 
2956
3071
 
@@ -3481,7 +3596,8 @@ def main(argv=None):
3481
3596
  parser.add_argument("--objdump", metavar="PATH",
3482
3597
  help="disassembler for ELF input; default: derived "
3483
3598
  "from the first gcc in the matrix by swapping "
3484
- "the trailing gcc for objdump")
3599
+ "the trailing gcc for objdump, or an objdump "
3600
+ "beside a clang given by path")
3485
3601
  parser.add_argument("-t", "--target", action="append", default=[],
3486
3602
  metavar="NAME",
3487
3603
  help="named [table], [groups] name, comma-list, or "
@@ -1006,6 +1006,10 @@ class TestTargetGroups(unittest.TestCase):
1006
1006
  def test_expansion_errors(self):
1007
1007
  for case, config, tokens, present, absent in self.EXPANSION_ERRORS:
1008
1008
  with self.subTest(case=case):
1009
+ # The example-config pointer needs tomllib to read the
1010
+ # example; without it the pointer is omitted (_example_names).
1011
+ if case.startswith("example ") and asmdiff.tomllib is None:
1012
+ self.skipTest("tomllib requires Python >= 3.11")
1009
1013
  with self.assertRaises(SystemExit) as ctx:
1010
1014
  self.expand(*tokens, config=config)
1011
1015
  msg = str(ctx.exception)
@@ -3780,5 +3784,390 @@ class TestGolden(unittest.TestCase):
3780
3784
  + diff)
3781
3785
 
3782
3786
 
3787
+
3788
+ # JieLi pi32v2 clang -S output: algebraic assembly, ';' comments, labels
3789
+ # annotated with a trailing comment, a predicated block closed by '}'.
3790
+ PI32V2_S = """\
3791
+ \t.section\t.text.dot16,"ax",@progbits
3792
+ \t.globl\tdot16
3793
+ \t.type\tdot16,@function
3794
+ dot16: ; @dot16
3795
+ ; BB#0:
3796
+ \t[--sp] = {rets, r5, r4}
3797
+ \tr3 = r0
3798
+ \tr0 = 0
3799
+ \tifs (r2 < 1) goto .LBB2_2
3800
+ .LBB2_1: ; =>This Inner Loop Header: Depth=1
3801
+ \tr4 = h[r3++=2] (s)
3802
+ \tr5 = h[r1++=2] (s)
3803
+ \tr4 *= r5
3804
+ \tr0 += r4
3805
+ \tif (--r2 != 0) goto .LBB2_1
3806
+ .LBB2_2:
3807
+ \t{pc, r5, r4} = [sp++]
3808
+ .Lfunc_end2:
3809
+ \t.size\tdot16, .Lfunc_end2-dot16
3810
+ \t.section\t.text.add16_sat,"ax",@progbits
3811
+ \t.globl\tadd16_sat
3812
+ \t.type\tadd16_sat,@function
3813
+ add16_sat: ; @add16_sat
3814
+ ; BB#0:
3815
+ \tr3 = -32768
3816
+ .LBB0_2: ; =>This Inner Loop Header: Depth=1
3817
+ \tr5 += r4
3818
+ \tr4 = 32767
3819
+ \tifs (r5 < 32768) {
3820
+ \t\tr4 = smax(r5, r3)
3821
+ \t}
3822
+ \th[r0++=2] = r4
3823
+ \tif (--r2 != 0) goto .LBB0_2
3824
+ \t{pc, r5, r4} = [sp++]
3825
+ .Lfunc_end0:
3826
+ \t.size\tadd16_sat, .Lfunc_end0-add16_sat
3827
+ """
3828
+
3829
+ # JieLi pi32v2 `objdump -d`: bare-label headers, space-padded byte column,
3830
+ # "<sym : addr >" targets with a decimal displacement before them, and a
3831
+ # .rodata section dumped as a symbol over byte lines.
3832
+ PI32V2_OBJDUMP = """\
3833
+
3834
+ build/felucca.elf:\tfile format ELF32-pi32v2
3835
+
3836
+ Disassembly of section .text:
3837
+ _start:
3838
+ 2000120: 04 81 \tgoto 2 <_start+0x4 : 2000124 >
3839
+ 2000122: 80 00 \trts
3840
+ 2000124: 81 ea 12 77 \tcall 192036 <fm1_cstart : 202ef7a >
3841
+ 2000128: 49 e0 fe 07 \tr9 = 2046 <build/felucca_size.ll+0x7FE : 7fe >
3842
+ 200012c: 80 00 \trts
3843
+ fm6_op_run:
3844
+ 203916e: 05 90 \tr5 = [r1+r7<<2]
3845
+ 2039170: 44 d1 0d 00 \tr4 = r12 >> 13
3846
+ 2039174: 45 1c \tr5_r4 = r4 * r14 (s)
3847
+ 2039176: b0 20 \t[r6+0] += r4
3848
+ 2039178: 87 f8 e5 1f \tif (r7 != 32) goto -10 <fm6_op_run : 203916e >
3849
+ 203917c: 7f 05 \t{pc, r14-r4} = [sp++]
3850
+ Disassembly of section .rodata:
3851
+ SINE:
3852
+ 203b5f4:\t 00 00 24 03 48 06 6c 09 ..$.H.l.
3853
+ 203b5fc:\t 90 0c b3 0f d6 12 f8 15 ........
3854
+ """
3855
+
3856
+
3857
+ class TestPi32v2(unittest.TestCase):
3858
+ """JieLi pi32v2 dialect: clang -S with ';' comments and annotated
3859
+ labels, the vendor objdump's header, byte-column and target syntax,
3860
+ and shape-based span-stats buckets for algebraic assembly."""
3861
+
3862
+ def test_s_label_comment_stripped_so_span_is_found(self):
3863
+ funcs = asmdiff.extract_functions(PI32V2_S)
3864
+ self.assertEqual(sorted(funcs), ["add16_sat", "dot16"])
3865
+ self.assertIn(".LBB2_1:", funcs["dot16"])
3866
+ self.assertEqual(asmdiff.loop_spans(funcs["dot16"]), [(".LBB2_1", 5)])
3867
+
3868
+ def test_s_comment_lines_and_block_close_not_counted(self):
3869
+ body = asmdiff.extract_functions(PI32V2_S)["add16_sat"]
3870
+ self.assertNotIn("}", body)
3871
+ self.assertFalse(any(ln.startswith(";") for ln in body))
3872
+ self.assertEqual(asmdiff.loop_spans(body), [(".LBB0_2", 6)])
3873
+
3874
+ def test_objdump_bare_headers_and_data_section(self):
3875
+ funcs = asmdiff.extract_functions_objdump(PI32V2_OBJDUMP)
3876
+ self.assertEqual(sorted(funcs), ["_start", "fm6_op_run"])
3877
+
3878
+ def test_objdump_targets_normalized(self):
3879
+ funcs = asmdiff.extract_functions_objdump(PI32V2_OBJDUMP)
3880
+ self.assertIn("goto\t.L4", funcs["_start"])
3881
+ self.assertIn("call\tfm1_cstart", funcs["_start"])
3882
+ # uppercase nearest-symbol offset: noise, rendered as the raw address
3883
+ self.assertIn("r9\t= 2046 0x7fe", funcs["_start"])
3884
+ self.assertEqual(asmdiff.call_symbol("call\tfm1_cstart"), "fm1_cstart")
3885
+ self.assertEqual(asmdiff.loop_spans(funcs["fm6_op_run"]), [(".L0", 5)])
3886
+
3887
+ def test_objdump_insn_lines_cleaned(self):
3888
+ body = asmdiff.extract_functions_objdump(PI32V2_OBJDUMP)["fm6_op_run"]
3889
+ self.assertEqual(body[1], "r5\t= [r1+r7<<2]")
3890
+ for ln in body:
3891
+ self.assertNotRegex(ln, r"^[0-9a-f]+\s")
3892
+
3893
+ def test_algebraic_buckets(self):
3894
+ cases = {"r5\t= [r1+r7<<2]": "load",
3895
+ "{pc,\tr14-r4} = [sp++]": "load",
3896
+ "[r6+0]\t+= r4": "store",
3897
+ "[--sp]\t= {rets, r5, r4}": "store",
3898
+ "h[r0++=2]\t= r4": "store",
3899
+ "r5_r4\t= r4 * r14 (s)": "mul",
3900
+ "r1_r0\t+= r6 * r5 (s)": "mul",
3901
+ "r2\t= r2 / r3 (s)": "div",
3902
+ "if\t(r7 != 32) goto .L0": "branch",
3903
+ "ifs\t(r5 < 32768) {": "branch",
3904
+ "goto\t.L4": "branch",
3905
+ "rts": "branch",
3906
+ "call\t__mulsf3": "branch",
3907
+ "r4\t= r12 >> 13": "other",
3908
+ "r4\t= smax(r5, r3)": "other",
3909
+ "r12\t= r7 #": "other"}
3910
+ for line, want in cases.items():
3911
+ self.assertEqual(asmdiff.classify_insn(line), want, line)
3912
+
3913
+ def test_gnu_buckets_untouched_by_algebraic_rule(self):
3914
+ self.assertEqual(asmdiff.classify_insn("l32i\ta0, a2, 0"), "load")
3915
+ self.assertEqual(asmdiff.classify_insn("movl\t%eax, (%rdi)"), "store")
3916
+
3917
+ def test_objdump_beside_clang_is_derived(self):
3918
+ with tempfile.TemporaryDirectory() as d:
3919
+ binary = Path(d) / "pi32v2" / "bin"
3920
+ binary.mkdir(parents=True)
3921
+ (binary / "clang").write_text("")
3922
+ self.assertIsNone(asmdiff.derive_objdump([str(binary / "clang")]))
3923
+ (binary / "objdump").write_text("")
3924
+ self.assertEqual(asmdiff.derive_objdump([str(binary / "clang") + " -target pi32v2"]),
3925
+ str(binary / "objdump"))
3926
+
3927
+
3928
+ # JieLi pi32v2 `rep` hardware loops (clang 4.0.1 fork, -Os), assembled from
3929
+ # inline asm after fast_i2f in JieLi's jl_fft.c: "rep %1 { ... }" with a
3930
+ # register count. rep_two holds two blocks after a plain C loop,
3931
+ # rep_nested one inside a C loop. In -S the block's "}" is an end label
3932
+ # followed by a "dummy" placeholder; objdump prints "rep <body bytes> rN {".
3933
+ PI32V2_REP_S = """\
3934
+ \t.globl\trep_two
3935
+ \t.p2align\t1
3936
+ \t.type\trep_two,@function
3937
+ rep_two: ; @rep_two
3938
+ ; BB#0:
3939
+ \t[--sp] = {rets, r8-r4}
3940
+ \tr8 = [sp+24]
3941
+ \tifs (r3 < 1) goto .LBB0_3
3942
+ ; BB#1:
3943
+ \tr5 = r3
3944
+ \tr6 = r2
3945
+ \tr7 = r0
3946
+ .LBB0_2: ; =>This Inner Loop Header: Depth=1
3947
+ \tr4 = [r7++=4]
3948
+ \tr4 += 1
3949
+ \t[r6++=4] = r4
3950
+ \tif (--r5 != 0) goto .LBB0_2
3951
+ .LBB0_3:
3952
+ \tr5 = r1
3953
+ \tr6 = r8
3954
+ \tr7 = r3
3955
+ \t;APP
3956
+ .Ltmp7:
3957
+ \trep .Ltmp8 r7
3958
+
3959
+ \tr4 = [r0++=4]
3960
+
3961
+ \tr4 = r4 + r6
3962
+
3963
+ \t[r5++=4] = r4
3964
+
3965
+ .Ltmp8:
3966
+ \tdummy
3967
+
3968
+ \tif (r7 > 0) goto .Ltmp7
3969
+
3970
+
3971
+ \t;NO_APP
3972
+ \t;APP
3973
+ .Ltmp9:
3974
+ \trep .Ltmp10 r3
3975
+
3976
+ \tr0 = [r1++=4]
3977
+
3978
+ \tr0 += r8
3979
+
3980
+ \tr0 = r0 << 1
3981
+
3982
+ \t[r2++=4] = r0
3983
+
3984
+ .Ltmp10:
3985
+ \tdummy
3986
+
3987
+ \tif (r3 > 0) goto .Ltmp9
3988
+
3989
+
3990
+ \t;NO_APP
3991
+ \t{pc, r8-r4} = [sp++]
3992
+ .Lfunc_end0:
3993
+ \t.size\trep_two, .Lfunc_end0-rep_two
3994
+
3995
+ \t.globl\trep_nested
3996
+ \t.p2align\t1
3997
+ \t.type\trep_nested,@function
3998
+ rep_nested: ; @rep_nested
3999
+ ; BB#0:
4000
+ \t[--sp] = {rets, r10-r4}
4001
+ \tifs (r3 < 1) goto .LBB1_3
4002
+ ; BB#1:
4003
+ \tr8 = [sp+32]
4004
+ \tr9 = r2 << 2
4005
+ .LBB1_2: ; =>This Inner Loop Header: Depth=1
4006
+ \tr10 = r0
4007
+ \tr7 = r8
4008
+ \tr4 = r2
4009
+ \tr5 = r1
4010
+ \t;APP
4011
+ .Ltmp20:
4012
+ \trep .Ltmp21 r4
4013
+
4014
+ \tr6 = [r10++=4]
4015
+
4016
+ \tr6 = r6 + r7
4017
+
4018
+ \t[r5++=4] = r6
4019
+
4020
+ .Ltmp21:
4021
+ \tdummy
4022
+
4023
+ \tif (r4 > 0) goto .Ltmp20
4024
+
4025
+
4026
+ \t;NO_APP
4027
+ \tr1 += r9
4028
+ \tif (--r3 != 0) goto .LBB1_2
4029
+ .LBB1_3:
4030
+ \t{pc, r10-r4} = [sp++]
4031
+ .Lfunc_end1:
4032
+ \t.size\trep_nested, .Lfunc_end1-rep_nested
4033
+
4034
+
4035
+ """
4036
+
4037
+ PI32V2_REP_OBJDUMP = """\
4038
+
4039
+ rep_demo.o:\tfile format ELF32-pi32v2
4040
+
4041
+ Disassembly of section .text:
4042
+ rep_two:
4043
+ 0: 78 04 \t[--sp] = {rets, r8-r4}
4044
+ 2: d4 e9 18 80 \tr8 = [sp+24]
4045
+ 6: 83 fd 08 02 \tifs (r3 < 1) goto 16 <rep_two+0x1A : 1a >
4046
+ a: 35 16 \tr5 = r3
4047
+ c: 26 16 \tr6 = r2
4048
+ e: 07 16 \tr7 = r0
4049
+ 10: 74 05 \tr4 = [r7++=4]
4050
+ 12: c4 21 \tr4 += 1
4051
+ 14: e4 05 \t[r6++=4] = r4
4052
+ 16: 05 ea fb ff \tif (--r5 != 0) goto -10 <rep_two+0x10 : 10 >
4053
+ 1a: 15 16 \tr5 = r1
4054
+ 1c: 86 16 \tr6 = r8
4055
+ 1e: 37 16 \tr7 = r3
4056
+ 20: 27 03 \trep 6 r7 {
4057
+ 22: 04 05 \t\tr4 = [r0++=4]
4058
+ 24: c4 1d \t\tr4 = r4 + r6
4059
+ 26: d4 05 \t\t[r5++=4] = r4
4060
+ \t}
4061
+ 28: 07 fc fa 01 \tif (r7 > 0) goto -12 <rep_two+0x20 : 20 >
4062
+ 2c: 33 03 \trep 8 r3 {
4063
+ 2e: 10 05 \t\tr0 = [r1++=4]
4064
+ 30: 80 18 \t\tr0 += r8
4065
+ 32: 00 a1 \t\tr0 = r0 << 1
4066
+ 34: a0 05 \t\t[r2++=4] = r0
4067
+ \t}
4068
+ 36: 03 fc f9 01 \tif (r3 > 0) goto -14 <rep_two+0x2C : 2c >
4069
+ 3a: 58 04 \t{pc, r8-r4} = [sp++]
4070
+
4071
+
4072
+ rep_nested:
4073
+ 3c: 7a 04 \t[--sp] = {rets, r10-r4}
4074
+ 3e: 83 fd 12 02 \tifs (r3 < 1) goto 36 <rep_nested+0x2A : 66 >
4075
+ 42: d4 e9 20 80 \tr8 = [sp+32]
4076
+ 46: c0 e1 22 90 \tr9 = r2 << 2
4077
+ 4a: 0a 16 \tr10 = r0
4078
+ 4c: 87 16 \tr7 = r8
4079
+ 4e: 24 16 \tr4 = r2
4080
+ 50: 15 16 \tr5 = r1
4081
+ 52: 34 03 \trep 8 r4 {
4082
+ 54: d8 ec a4 60 \t\tr6 = [r10++=4]
4083
+ 58: ee 1d \t\tr6 = r6 + r7
4084
+ 5a: d6 05 \t\t[r5++=4] = r6
4085
+ \t}
4086
+ 5c: 04 fc f9 01 \tif (r4 > 0) goto -14 <rep_nested+0x16 : 52 >
4087
+ 60: 91 18 \tr1 += r9
4088
+ 62: 03 ea f2 ff \tif (--r3 != 0) goto -28 <rep_nested+0xE : 4a >
4089
+ 66: 5a 04 \t{pc, r10-r4} = [sp++]
4090
+
4091
+ """
4092
+
4093
+ # An immediate count ("rep 20 16 {"), from a second probe object.
4094
+ PI32V2_REP_IMM_OBJDUMP = """\
4095
+
4096
+ imm.o:\tfile format ELF32-pi32v2
4097
+
4098
+ Disassembly of section .text:
4099
+ rep_imm:
4100
+ 0: 90 8f \trep 20 16 {
4101
+ 2: 02 05 \t\tr2 = [r0++=4]
4102
+ 4: 2a 1c \t\tr2 = r2 + r1
4103
+ 6: 22 a1 \t\tr2 = r2 << 1
4104
+ 8: 2a 1c \t\tr2 = r2 + r1
4105
+ a: 2a 1c \t\tr2 = r2 + r1
4106
+ c: 2a 1c \t\tr2 = r2 + r1
4107
+ e: 2a 1c \t\tr2 = r2 + r1
4108
+ 10: 2a 1c \t\tr2 = r2 + r1
4109
+ 12: 2a 1c \t\tr2 = r2 + r1
4110
+ 14: 82 05 \t\t[r0++=4] = r2
4111
+ \t}
4112
+ 16: 80 00 \trts
4113
+
4114
+ """
4115
+
4116
+
4117
+ class TestPi32v2Rep(unittest.TestCase):
4118
+ """pi32v2 `rep` hardware loops: the span covers the repeated body,
4119
+ in -S output (rep names its end label) and in objdump output (the
4120
+ body length in bytes becomes a .L<off>_LEND label, as for Xtensa)."""
4121
+
4122
+ spans = staticmethod(asmdiff.loop_spans)
4123
+
4124
+ def test_s_rep_span_is_the_body(self):
4125
+ # Same counts as the ELF of this object: the zero-byte "dummy"
4126
+ # clang puts after a rep end label is not an instruction.
4127
+ body = asmdiff.extract_functions(PI32V2_REP_S)["rep_two"]
4128
+ self.assertNotIn("dummy", body)
4129
+ self.assertEqual(self.spans(body),
4130
+ [(".LBB0_2", 4), (".Ltmp7", 5), (".Ltmp8", 3),
4131
+ (".Ltmp9", 6), (".Ltmp10", 4)])
4132
+
4133
+ def test_s_rep_inside_c_loop_nests(self):
4134
+ body = asmdiff.extract_functions(PI32V2_REP_S)["rep_nested"]
4135
+ ranges = asmdiff.loop_span_ranges(body)
4136
+ self.assertEqual(self.spans(body),
4137
+ [(".LBB1_2", 11), (".Ltmp20", 5), (".Ltmp21", 3)])
4138
+ self.assertEqual(asmdiff.span_depths(ranges),
4139
+ {".LBB1_2": 0, ".Ltmp20": 1, ".Ltmp21": 2})
4140
+
4141
+ def test_objdump_rep_gets_end_label(self):
4142
+ body = asmdiff.extract_functions_objdump(PI32V2_REP_OBJDUMP)["rep_two"]
4143
+ self.assertIn("rep\t.L28_LEND r7", body)
4144
+ self.assertIn("rep\t.L36_LEND r3", body)
4145
+ self.assertNotIn("}", body)
4146
+ self.assertEqual(body[body.index(".L28_LEND:") + 1],
4147
+ "if\t(r7 > 0) goto .L20")
4148
+
4149
+ def test_objdump_rep_spans(self):
4150
+ funcs = asmdiff.extract_functions_objdump(PI32V2_REP_OBJDUMP)
4151
+ self.assertEqual(self.spans(funcs["rep_two"]),
4152
+ [(".L10", 4), (".L20", 5), (".L28_LEND", 3),
4153
+ (".L2c", 6), (".L36_LEND", 4)])
4154
+ self.assertEqual(self.spans(funcs["rep_nested"]),
4155
+ [(".Le", 11), (".L16", 5), (".L20_LEND", 3)])
4156
+
4157
+ def test_objdump_rep_immediate_count(self):
4158
+ body = asmdiff.extract_functions_objdump(
4159
+ PI32V2_REP_IMM_OBJDUMP)["rep_imm"]
4160
+ self.assertEqual(body[0], "rep\t.L16_LEND 16")
4161
+ self.assertEqual(self.spans(body), [(".L16_LEND", 10)])
4162
+
4163
+ def test_rep_stays_a_branch(self):
4164
+ self.assertEqual(asmdiff.classify_insn("rep\t.L28_LEND r7"), "branch")
4165
+ self.assertEqual(asmdiff.classify_insn("rep\t.Ltmp8 r7"), "branch")
4166
+
4167
+ def test_x86_rep_prefix_makes_no_span(self):
4168
+ self.assertEqual(asmdiff.loop_span_ranges(
4169
+ ["rep movsb", ".L2:", "addl\t$1, %eax"]), [])
4170
+
4171
+
3783
4172
  if __name__ == "__main__":
3784
4173
  unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes