From 04240f85907b78652e1d53c04807149d79fe9265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 11:31:33 +0200 Subject: [PATCH 1/2] fix(harness): a quoted LLVM label must start a new basic block (#9494) `native-region-proof` failed `packed_f64_loop_versioning` with hot_loops_no_runtime_calls: {"for.packed_f64_fast.body.54.i.epil": ["js_array_alloc"]} on correct codegen. The named block contains no calls at all -- it is a clean scalar epilogue (shl/add/inttoptr/load/fadd/icmp/br). The `js_array_alloc` belongs to the NEXT block, which builds console.log's argument array. The block splitter matched labels with ^([A-Za-z0-9_.$-]+):(?:\s|$) LLVM quotes any identifier outside its bare-name set, and #9337's specialized functions put a `$` in the name, so the following label is emitted as "perry_fn_..._dynamicRhsPackedStore$spec_i32.exit": That line starts with `"`, so it never matched, no new block began, and the quoted block's body was appended to the preceding label -- moving main's `js_array_alloc` inside an unrolled hot-loop epilogue. Accept optionally-quoted labels (and quoted `define` names). Verified against the exact IR CI analyzed (run 33598905771): 510 -> 512 blocks, hot-loop count unchanged at 29, and the subject's hot-loop runtime calls go from {"...epil": ["js_array_alloc"]} to {}. Swept every workload in that artifact: `packed_f64_loop_versioning` is the only verdict that moves; `h1_buffer_alias_negative` and `image_convolution` are unchanged, so no masked failure is exposed. The regression test is sabotage-checked: reverting the pattern fails 2 of its 3 cases. --- scripts/compiler_output_harness/analyzers.py | 20 +++++-- tests/test_compiler_output_regression.py | 61 ++++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/scripts/compiler_output_harness/analyzers.py b/scripts/compiler_output_harness/analyzers.py index 69a6bf72b1..ddc32c4e7a 100644 --- a/scripts/compiler_output_harness/analyzers.py +++ b/scripts/compiler_output_harness/analyzers.py @@ -45,17 +45,25 @@ def parse_target_triple(ir: str) -> str | None: return match.group(1) if match else None +# LLVM quotes any identifier containing characters outside its bare-name set -- +# notably ``$``, which specialized function names use (``…$spec_i32.exit``). A +# quoted label line starts with ``"``, so a bare-name-only pattern never matches +# it, no new block begins, and the quoted block's body is silently appended to +# the PREVIOUS block. That mis-attributes its calls (perry#9494). +_LABEL_PATTERN = r"^(?:\"([^\"]+)\"|([A-Za-z0-9_.$-]+)):(?:\s|$)" + + def extract_blocks(ir: str) -> list[tuple[str, str]]: blocks: list[tuple[str, str]] = [] current_label: str | None = None current_lines: list[str] = [] - label_re = re.compile(r"^([A-Za-z0-9_.$-]+):(?:\s|$)") + label_re = re.compile(_LABEL_PATTERN) for line in ir.splitlines(): match = label_re.match(line) if match: if current_label is not None: blocks.append((current_label, "\n".join(current_lines))) - current_label = match.group(1) + current_label = match.group(1) or match.group(2) current_lines = [line] elif current_label is not None: current_lines.append(line) @@ -69,8 +77,8 @@ def extract_blocks_with_functions(ir: str) -> list[tuple[str, str, str]]: current_function = "" current_label: str | None = None current_lines: list[str] = [] - label_re = re.compile(r"^([A-Za-z0-9_.$-]+):(?:\s|$)") - define_re = re.compile(r"^define\b.*@([A-Za-z0-9_.$-]+)\(") + label_re = re.compile(_LABEL_PATTERN) + define_re = re.compile(r"^define\b.*@(?:\"([^\"]+)\"|([A-Za-z0-9_.$-]+))\(") for line in ir.splitlines(): define_match = define_re.match(line) if define_match: @@ -78,7 +86,7 @@ def extract_blocks_with_functions(ir: str) -> list[tuple[str, str, str]]: blocks.append( (current_function, current_label, "\n".join(current_lines)) ) - current_function = define_match.group(1) + current_function = define_match.group(1) or define_match.group(2) current_label = None current_lines = [] continue @@ -88,7 +96,7 @@ def extract_blocks_with_functions(ir: str) -> list[tuple[str, str, str]]: blocks.append( (current_function, current_label, "\n".join(current_lines)) ) - current_label = match.group(1) + current_label = match.group(1) or match.group(2) current_lines = [line] elif current_label is not None: current_lines.append(line) diff --git a/tests/test_compiler_output_regression.py b/tests/test_compiler_output_regression.py index 4311d4f7da..2309e7f5cc 100644 --- a/tests/test_compiler_output_regression.py +++ b/tests/test_compiler_output_regression.py @@ -3000,5 +3000,66 @@ def test_loop_data_dependent_rejects_conversion_inside_numeric_merge(self): ) +class QuotedLlvmLabelBlockBoundaryTests(unittest.TestCase): + """perry#9494: a quoted LLVM label must start a new basic block. + + LLVM quotes any identifier outside its bare-name set -- notably ``$``, which + specialized function names carry (``…$spec_i32.exit``). When the block + splitter only matched bare names, a quoted label started no block and its + body was appended to the PREVIOUS one, so a ``js_array_alloc`` belonging to + a ``console.log`` epilogue was reported as a runtime call inside an unrolled + hot-loop epilogue. The proof failed on correct codegen. + """ + + IR = "\n".join( + [ + "define i32 @main() {", + "for.body.7.epil:", + " %v = fadd double %a, %b", + " br label %\"fn$spec_i32.exit\"", + "", + '"fn$spec_i32.exit":', + " %r = tail call i64 @js_array_alloc(i32 1)", + " ret i32 0", + "}", + ] + ) + + def test_quoted_label_starts_its_own_block(self): + from compiler_output_harness.analyzers import extract_blocks + + labels = [label for label, _ in extract_blocks(self.IR)] + self.assertIn( + "fn$spec_i32.exit", + labels, + "quoted label must be recognised as its own block", + ) + + def test_hot_loop_does_not_absorb_following_quoted_block(self): + from compiler_output_harness.analyzers import hot_loop_blocks + + bodies = {label: body for label, body in hot_loop_blocks(self.IR)} + self.assertIn("for.body.7.epil", bodies, "fixture must select the hot loop") + self.assertNotIn( + "js_array_alloc", + bodies["for.body.7.epil"], + "call from the quoted block leaked into the hot loop (perry#9494)", + ) + + def test_quoted_function_name_is_attributed(self): + from compiler_output_harness.analyzers import extract_blocks_with_functions + + ir = "\n".join( + [ + 'define i32 @"mod$spec_i32"() {', + "entry:", + " ret i32 0", + "}", + ] + ) + functions = {fn for fn, _, _ in extract_blocks_with_functions(ir)} + self.assertIn("mod$spec_i32", functions) + + if __name__ == "__main__": unittest.main() From 1a9a0f4d07d089c3690eb6b4909f330e1209bbe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 2 Sep 2026 11:32:08 +0200 Subject: [PATCH 2/2] changelog: quoted LLVM label block boundary (#9510) --- .../9510-quoted-llvm-label-block-boundary.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 changelog.d/9510-quoted-llvm-label-block-boundary.md diff --git a/changelog.d/9510-quoted-llvm-label-block-boundary.md b/changelog.d/9510-quoted-llvm-label-block-boundary.md new file mode 100644 index 0000000000..1c1858584c --- /dev/null +++ b/changelog.d/9510-quoted-llvm-label-block-boundary.md @@ -0,0 +1,23 @@ +- **`compiler-output-regression`: a quoted LLVM label now starts a new basic block.** + `native-region-proof` failed `packed_f64_loop_versioning` with + `hot_loops_no_runtime_calls: {"for.packed_f64_fast.body.54.i.epil": ["js_array_alloc"]}` + on correct codegen. The named block holds no calls at all — it is a clean + scalar epilogue; the `js_array_alloc` belongs to the following block, which + builds `console.log`'s argument array. + + The block splitter matched labels with `^([A-Za-z0-9_.$-]+):(?:\s|$)`. LLVM + quotes any identifier outside its bare-name set, and #9337's specialized + functions carry a `$`, so the next label is emitted as + `"perry_fn_…$spec_i32.exit":`. That line starts with `"`, so it never matched: + no new block began and the quoted block's body was appended to the preceding + label, moving a call into an unrolled hot-loop epilogue. The mis-attribution + can only ever move calls *into* the preceding block, which is exactly the + false-positive shape observed. + + `extract_blocks` / `extract_blocks_with_functions` now accept optionally-quoted + labels and quoted `define` names. Verified against the exact IR CI analyzed + (run 33598905771): 510 → 512 blocks, hot-loop count unchanged at 29, subject's + hot-loop runtime calls `{"…epil": ["js_array_alloc"]}` → `{}`. Sweeping every + workload in that artifact, `packed_f64_loop_versioning` is the only verdict + that moves, so no masked failure is exposed. The regression test is + sabotage-checked: reverting the pattern fails 2 of its 3 cases.