Skip to content

Regression: fix four AOT module-verification defects — phpc build works again (#22638)#22640

Closed
PurHur wants to merge 2 commits into
masterfrom
fix/22638-aot-module-verification
Closed

Regression: fix four AOT module-verification defects — phpc build works again (#22638)#22640
PurHur wants to merge 2 commits into
masterfrom
fix/22638-aot-module-verification

Conversation

@PurHur

@PurHur PurHur commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Closes #22638.

Problem

On pristine master (e1850c9) in php-compiler:22.04-dev, ./phpc build failed module verification for any user script — including the 5-line tier-1 fixture that check-aot-build-smoke.sh (#16010) uses — so ./script/ci-fast.sh was red at its first gate. The same four defects broke the split helper-object cache: 266 of 266 helper units failed to emit, each silently degraded to the "nested-lowering fallback (#15642)".

One Module->verify() reported all four at once:

Call parameter type does not match function signature!
  %4 = call %__string__* @...htmlspecialcharsjithelper__htmlspecialchars(%__value__* %2, i64 %1)
ZExt only operates on integer
  %52 = zext %__value__ %51 to i64
Incorrect number of arguments passed to called function!
  call void @internal_51()
Both operands to ICmp instruction are not of the same type!
  %3 = icmp eq i32 %2, i64 0

Because every JIT context builds the htmlspecialchars helper, and that helper indexes a string, the string-offset defect alone poisoned every module in the tree.

Root cause and fix

# Defect Site Fix
1 zext %__value__ ... to i64 StringOffsetRuntime::normalizeOffset blind-zexts its operands width-aware widening + read the long out of a box; dimFetch coerces through JitLongArg, which applies the int/bool/null/float/numeric-string offset rules
2 by-value param gets a pointer JitNestedHelperCoerce::coerceArgForHelper always returns a pointer when boxing, but isValueBoxType() also matches a by-value __value__ load the slot when the callee's param is by value
3 call void @internal_N() arity JIT::instanceMethodUsesThis falls back to scope->className (#16075), so {main} inherits a leftover class name and gets an %__object__* param script top-level never receives $this
4 icmp i32 vs i64 strcmp is declared i32 in LibcExtern, four sites compare against an i64 zero compare against an i32 zero

Why it went unobserved

Three layers of false green stacked up, all addressed here:

Extending PHP_COMPILER_LLVM_ASSERT to validate iCmp operand types is what located defect 4 from a backtrace in one shot — the same way the existing zExt assert located defect 1. That assert is the highest-leverage part of this PR.

Verification (php-compiler:22.04-dev, LLVM 9)

Check Before After
./phpc build + execute, tier-1 fixture RuntimeException: Module verification failed Hello AOT! / 3 — matches VM
ci-fast AOT build smoke (#16010) FAILED — phpc build exited non-zero (tier 1) OK tier 1 (build + execute + VM parity)
php script/emit-helper-runtime-object.php 0 emitted / 266 failed 219 emitted / 47 failed
helper-unit sweep wall time (266 units, 16 cores) ~45 min serial 4m27s wall / 55m28s CPU

The 47 remaining unit failures are a different, pre-existing class (Call to undefined method HashTable::iteratekeyed() and friends) and are not touched here.

Not fixed here — still blocking a release

Two gates remain red on master, both for the same structural reason and neither caused by this PR:

  • north-star5-verify --fast step 4f-m — committed gen-0 lowering_source_fingerprint is stale.
  • ci-fast check-selfhost-spine-sidecar-sync — committed .m3_compiler_lib_aot_blob stamp is stale (the spine entry file is untouched by this PR).

Both are repaired by make bootstrap-gen0-refresh-sidecar, which cannot complete: the Zend gen-0 compile of compile_driver.php exhausts 6 GiB, 24 GiB and 48 GiB PHP memory caps in turn, growing monotonically with no completion (~90 min to 48 GiB on a 62 GB box). prelinked/bootstrap-gen0/bin-compile-aot last actually changed on 2026-06-15; the 44 "Trust: restamp gen-0 fingerprint" commits since then re-assert freshness without rebuilding the blob. Filed separately.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

🤖 Generated with Claude Code

@PurHur

PurHur commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Follow-up blocker filed as #22642 (gen-0 seed cannot be rebuilt). The two gates listed under "Not fixed here" are tracked there.

anmichels and others added 2 commits July 23, 2026 11:23
`./phpc build` failed module verification for every user script on master,
including the 5-line tier-1 fixture in check-aot-build-smoke.sh (#16010), so
ci-fast was red at its first gate. The same four defects broke the split
helper-object cache: 266/266 units failed to emit, each silently degraded to
the nested-lowering fallback (#15642).

Every JIT context builds the htmlspecialchars helper, and that helper indexes
a string, so the string-offset defect alone poisoned every module in the tree.

- StringOffsetRuntime::normalizeOffset blind-zext'd its operands, emitting
  `zext %__value__ ... to i64` for a box-backed offset. Widen width-aware and
  read the long out of a box; dimFetch now coerces through JitLongArg, which
  applies the int/bool/null/float/numeric-string offset rules.
- JitNestedHelperCoerce::coerceArgForHelper always returned a pointer when
  boxing, but isValueBoxType() also matches a by-value `__value__` parameter,
  so the call passed `%__value__*` to a by-value param.
- JIT::instanceMethodUsesThis fell back to scope->className (#16075), giving
  `{main}` an `%__object__*` parameter whenever a class was compiled first;
  standalone main then emitted `call void @internal_N()` with no arguments.
- strcmp is declared i32 in LibcExtern, but four sites compared its result
  against an i64 zero.

Also extends the PHP_COMPILER_LLVM_ASSERT hook to validate iCmp operand types.
That assert located the strcmp defect from a backtrace in one shot, the same
way the existing zExt assert located the string-offset one.

Verified in php-compiler:22.04-dev (LLVM 9):
- ./phpc build + execute green on the tier-1 fixture, output matches VM
- ci-fast AOT build smoke: "OK tier 1 (build + execute + VM parity)"
- helper-unit emit: 219/266 units, was 1/266

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The helper-unit sweep ran one `--unit=` child at a time with its stderr sent
to /dev/null, so a full sweep cost ~45 min and 266 failures produced no
diagnostics at all. Three layers of false green stacked up: no stderr, a
prelink check that iterates glob(units/*/manifest.json) and so reports clean
when *zero* units emit, and emission failure tolerated as the nested-lowering
fallback (#15642).

- Fan the child lowerings out over a proc_open worker pool. Each unit already
  runs in its own child writing only to its own cache directory, so there is
  no shared mutable state. Default nproc-2 mirrors bin/lint.php's
  PHP_COMPILER_LINT_JOBS; PHP_COMPILER_EMIT_JOBS=1 restores the serial sweep.
- Send child stderr to <unit dir>/emit.log and surface the first error line in
  the sweep output, so a failing unit names its cause.

Measured on a 16-core box, 266 units: 4m27s wall / 55m28s CPU — ~12.5x.
The stderr capture is what turned an opaque "266 failed" into a ranked defect
list, which is how #22638 was found.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@PurHur

PurHur commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Closing as duplicate of merged #22643 (same #22638 fix already on master: 873e280a7).

#22643 also includes the zero-units check-helper-runtime-prelink --strict gate that this PR left open. Parallel emit from this branch can be re-filed separately if still desired.

@PurHur

PurHur commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Honest scope note — this PR makes the compiler build, it does not make it correct

Follow-up verification after opening this PR, on a class + method + string-offset + htmlspecialchars fixture:

class Greeter { /* ... */ public function initial(): string { return $this->who[0]; } }
$g = new Greeter("AOT");
echo $g->greet(), "\n", $g->initial(), "\n", htmlspecialchars("Tom & Jerry"), "\n";
master this PR
build ❌ module verification fails ✅ builds
output — (never runs) Hello AOT! ✅ · (empty) ❌ · (empty)

So master cannot build it; this PR builds it and then produces silently wrong output for two of the three lines. That is a real improvement in the gate, and it is not a correct compiler. Both remaining defects are pre-existing, not introduced here:

A/B against a master baseline (master + this PR's emit-parallel commit only, so the sweep is comparable)

--filter "StringOffset|Htmlspecialchars|ParamSensitive|ReflectionFunction|StringDim"

master:  Tests: 84, Assertions: 148, Errors: 1, Failures: 10
this PR: Tests: 84, Assertions: 183, Errors: 1, Failures: 10

Failing data-set identities diff clean — no new failures; assertions rise 148 → 183 because more builds now get far enough to assert.

Not verified

A full --filter AotTest A/B (1175 cases) was started and stopped: ~2.7 h per side, and a branch-only number is uninterpretable without the matching baseline. The broad AOT surface is therefore not cleared by this PR beyond the targeted suite above and the project's own AOT build smoke gate.

@PurHur

PurHur commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Cleanup hygiene

Deleting remote branch fix/22638-aot-module-verification.

Reason: PR closed unmerged after landing via #22643 + follow-up #22647. Issue #22638 closed; branch tip is obsolete vs master's icmp-assert / emit-jobs evolution.

Open PR #22717 (agent/issue-22642-gen0-refresh-oom) and release/v1.1.0-dev left untouched.

@PurHur
PurHur deleted the fix/22638-aot-module-verification branch July 23, 2026 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants