Skip to content

fix: narrow $x === false guard (int|false returns as int after a throw-guard)#463

Open
chadmandoo wants to merge 4 commits into
illegalstudio:mainfrom
chadmandoo:upstream-pr/strict-false-narrowing
Open

fix: narrow $x === false guard (int|false returns as int after a throw-guard)#463
chadmandoo wants to merge 4 commits into
illegalstudio:mainfrom
chadmandoo:upstream-pr/strict-false-narrowing

Conversation

@chadmandoo

Copy link
Copy Markdown
Contributor

What

if ($v === false) { throw; } return $v; on an int|false value declared : int was rejected — the return was inferred as Union([Int, Bool]).

Why

The if/else flow-narrowing already keeps the guard complement after a diverging body (throw), but type_guard_narrowing only recognized is_*()/instanceof guards — not the $x === false strict comparison — so the complement never stripped the false-ish Bool member.

Fix

Recognize $var === false / false === $var (BinOp::StrictEq): narrow to Bool in the then-branch, strip it in the else-branch (int|falseint). Byte-parity regression test; builds standalone on main.

chadmandoo and others added 4 commits July 5, 2026 14:08
…throw-guard

`if ($value === false) { throw; } return $value;` (ward-http StreamGuards::requireInt
and the string/stream guards) declared `: int` but was inferred as returning
Union([Int, Bool]) — type_guard_narrowing recognized is_*()/instanceof but not the
`$x === false` strict comparison, so the divergent-guard complement (which the if/else
narrowing already keeps after a diverging body) never stripped the false-ish member.
Recognize `$var === false` / `false === $var` (BinOp::StrictEq) → narrow to Bool in the
then-branch and strip it in the else-branch (int|false → int). Byte-parity regression test.
AIC epic illegalstudio#483 / EC-8 (illegalstudio#491), the return-over-widening sub-part.

(cherry picked from commit 7c8ac75)
`$this->raw instanceof Message ? $this->raw : new Message($this->raw)`
with `private Message|string $rawMessage` failed to compile — ternary
inference applied zero narrowing, and guard narrowing keyed only
variables (ExprKind::Variable), never property receivers.

Add branch_guard_narrowing(): recognizes variable guards (delegating to
type_guard_narrowing) plus `$var->prop instanceof Class` /
`$this->prop instanceof Class`, keying the narrowed type under a
synthetic \x01-sigil env key that infer_property_access_type consults
before falling back to the declared field type. Both ternary inference
paths (pure + assignment-effects) narrow their branch envs with it. A
ternary is a single expression — no intervening writes — so the property
narrowing is write-invalidation-safe here by construction.

Byte-parity vs PHP 8.5 (hi:k) + codegen regression test. AIC EC-8 (illegalstudio#491):
clears the FieldViolation Message|string pattern across the Forms Field
family.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit fcff01d)
…tement-level

Three narrowing gaps behind ~7 AIC Domain classes:
- `if (is_null($p)) { throw; }` did not narrow ?int → int (only
  `=== null` did): add is_null to the predicate map.
- `if (!$this->prop instanceof C) { throw; } return $this->prop;`
  (ward-forms StoreResult::ref) kept the property at ?C: statement-level
  guards now key simple property receivers via the synthetic
  \x01-sigil env key, riding the existing if-chain save/restore and
  divergent-complement machinery.
- The ternary property narrowing from the previous commit now shares the
  same single entry point.

Mechanics: fold type_guard_narrowing + branch_guard_narrowing into
guard_narrowing(); guard_receiver_and_type() extracts (receiver expr,
target) for all guard forms (is_* / is_null / instanceof / === false /
=== null) on variables AND simple property accesses.

Soundness: property narrowings are purged after any call (the callee may
mutate the object), after assignments writing through a property, and at
loop-body entry (a later iteration may observe an earlier write) —
purge_property_narrowings() in the effects walk. A call's own argument
checking still sees the narrowing (purge happens after the call's
inference).

Byte-parity vs PHP 8.5 (5 / x) + 2 codegen regression tests. Full suite
6177 passed / 1 env-only failure (IPv6-DNS network test). AIC EC-8 (illegalstudio#491).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 3c58b4e)
@chadmandoo

Copy link
Copy Markdown
Contributor Author

Pushed two follow-up commits extending the same narrowing seam: (1) property-access flow-narrowing across ternary branches ($this->raw instanceof Message ? $this->raw : new Message($this->raw) with a union-typed property) via synthetic env keys the property-read inference consults; (2) a unified guard_narrowing() entry that adds is_null() to the predicate set and applies the same guards to simple property receivers at statement level (the if (!$this->x instanceof C) { throw; } return $this->x; pattern), with conservative purge-on-call/write/loop-entry soundness. Byte-parity + regression tests per fix.\n\nNote: this PR's tests (pre-existing and new) use declare(strict_types=1) in fixtures, so it depends on #459 landing first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant