Skip to content

fix(types): join mismatched array/array branch merges as array<mixed> instead of bare mixed#596

Open
mirchaemanuel wants to merge 5 commits into
illegalstudio:mainfrom
mirchaemanuel:fix/587-checker-merge-array-type
Open

fix(types): join mismatched array/array branch merges as array<mixed> instead of bare mixed#596
mirchaemanuel wants to merge 5 commits into
illegalstudio:mainfrom
mirchaemanuel:fix/587-checker-merge-array-type

Conversation

@mirchaemanuel

@mirchaemanuel mirchaemanuel commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #587

Root cause

The type checker's branch-result join had no array-specific case. A merge such
as match($argc) { 1 => [1, 2], default => ["a", "b"] } therefore collapsed
from array<int> / array<string> to bare mixed, even though the EIR merge
materializes one array container with widened element storage.

That checker/runtime disagreement rejected PHP-valid consumers:

  • by-reference array parameters;
  • array_sum() and in_array();
  • array spread.

?? had a second divergence: it used the general syntactic join, so array
alternatives could keep the left element type instead of widening elementwise.

Fix

  • Keep indexed/indexed and associative/associative branch results array-shaped,
    widening their element or key/value dimensions.
  • Share the element join between the checker and EIR lowering. Empty-array
    Never placeholders defer to populated branches, bool/false normalize to
    bool, and real element-type disagreements use boxed mixed storage.
  • Apply the same array-specific join to ?? after removing null from its value
    side. match, ternary, and ?: already share the branch-result join.
  • Retain the existing fallback for scalar disagreements, object unions, and
    indexed-vs-associative merges.
  • Complete the EIR backend paths exercised by Checker types a heterogeneous array/array merge as mixed, rejecting valid array uses (by-ref, in_array, array_sum, spread) #587:
    • array_sum() over indexed array<mixed> and associative
      array<*, mixed>;
    • loose and strict integer-needle in_array() over indexed
      array<mixed>.

The runtime helpers are implemented for macOS/Linux AArch64 and Linux x86_64,
borrow their Mixed cells without ownership transfer, and keep hash iteration
read-only.

The misleading Undefined variable follow-on no longer fires for this issue
because spread now type-checks. The underlying failed-assignment diagnostic
cascade remains a separate defect, reproducible independently with
<?php $x = 5; $s = [...$x]; echo count($s);.

Integration

The branch now includes current main, including #583's heterogeneous merge
materialization. The PR therefore covers the missing checker contract and the
adjacent builtin backend paths end to end rather than remaining checker-only.

Test plan

  • cargo build
  • cargo test -p elephc --lib merge_array_branch
  • cargo test -p elephc --lib merge_null_coalesce_result_type
  • cargo test --test error_tests heterogeneous_
  • focused macOS AArch64 codegen regressions for match, ??, empty/null
    branches, array_sum(), in_array(), by-reference mutation, and spread
  • Linux x86_64 Docker run: cargo test --test codegen_tests heterogeneous_
    (35 passed)
  • assembly-comment alignment check for every touched emitter
  • git diff --check

https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L

… instead of bare mixed

The type checker collapsed a heterogeneous-element match/ternary/`??` array
merge (e.g. `[1, 2]` vs `["a", "b"]`) to bare `Mixed`, so PHP-valid array uses
of the result were rejected at compile time: passing it to a by-ref `array`
parameter, `in_array()`/`array_sum()`, and spread (`[...$r]`). The spread
rejection additionally emitted a misleading follow-on "Undefined variable"
diagnostic naming the assignment target.

`merge_match_arm_result_type` now mirrors the lowering-side `wider_type_for_merge`
array/array and assoc/assoc rules through a new pure `merge_array_branch_types`
helper: differing element (or key/value) types widen elementwise to `Mixed`, so
the merged value keeps an `array<mixed>` type and array operations type-check
just as they do for homogeneous merges. Non-array merges are untouched — scalar
unions stay `Mixed` and object|null merges stay nullable unions — and an
indexed-vs-associative mix still falls through to `Mixed`, matching lowering.

Runtime correctness of heterogeneous merges is tracked by illegalstudio#549 / PR illegalstudio#583; this
change only aligns the static type. The residual "Undefined variable" cascade
for genuinely-failing spreads (repro: `$x = 5; $s = [...$x]; count($s);`) is a
separate latent defect left for a follow-up.

Claude-Session: https://claude.ai/code/session_01HRvbcjPHa3kAojdCjNZL5L
@mirchaemanuel

Copy link
Copy Markdown
Contributor Author

The misleading Undefined variable follow-on mentioned in the description is a separate error-recovery defect (a failed assignment RHS leaves the target unregistered) — reverified on current main with an independent repro and filed as #597.

@github-actions github-actions Bot added area:types Touches type checking, inference, or compatibility. size:s Small pull request. type:fix Corrects broken or incompatible behavior. labels Jul 22, 2026
@nahime0

nahime0 commented Jul 24, 2026

Copy link
Copy Markdown
Member

Updated this PR in place after merging the latest main.

What changed:

  • completed the checker-side array join for match, ternary, ?:, and ??;
  • shared the element-widening contract with EIR lowering, preserving populated
    element types against empty-array Never placeholders while widening real
    disagreements to boxed mixed;
  • added end-to-end backend support for array_sum() over indexed and
    associative Mixed arrays;
  • added loose/strict integer-needle in_array() support for indexed Mixed
    arrays on AArch64 and x86_64;
  • added focused checker/codegen regressions, an Unreleased changelog entry, and
    updated the PR description to reflect the final non-checker-only scope.

Focused validation:

  • cargo build
  • checker unit tests for branch and null-coalesce joins
  • cargo test --test error_tests heterogeneous_
  • focused macOS AArch64 codegen tests for match, ??, array builtins, spread,
    by-reference mutation, empty arrays, and null elements
  • Linux x86_64 heterogeneous_ codegen filter: 35 passed
  • assembly-comment alignment and git diff --check

The independent failed-assignment Undefined variable diagnostic cascade
remains out of scope.

Published head: a4404f150653390d617f7277f3275e47e3d2417c
(bf84efd50 is the semantic repair commit). The PR is currently mergeable;
fresh exact-head CI is still running/queued, so readiness remains conditional
on that matrix turning green.

@github-actions github-actions Bot added area:eir Touches EIR definitions, lowering, validation, or passes. area:runtime Touches runtime helpers, GC, ownership, or bridge runtimes. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:m Medium-sized pull request. and removed size:s Small pull request. labels Jul 24, 2026
@mirchaemanuel

Copy link
Copy Markdown
Contributor Author

Heads-up on the CI failure on 650e.../a4404f150codegen::io::streams::test_array_literal_of_resources_round_trips (linux-aarch64 6/16): [STDIN, STDOUT, STDERR] now prints the raw fds (0|1|2) instead of Resource id #1|.... Local repro on the branch head confirms; --emit-ir shows the literal now lowers as array<mixed> while each element is still I64 php=resource<stream> — so the elements land in boxed-mixed storage and the element echo takes the mixed path, which renders the raw payload instead of the resource id.

All three constants are the same PhpType::stream_resource() (Resource(Some("stream"))), and widen_array_branch_element keeps equal types, so an equal-element literal shouldn't widen — it looks like the new literal element join drops Resource(Some(_)) equality somewhere on the way (or folds through a repr that maps resources to Mixed) in the bf84efd50 inference changes. Happy to dig further if useful.

@nahime0

nahime0 commented Jul 24, 2026

Copy link
Copy Markdown
Member

CI follow-up for Codegen Tests (linux-aarch64 6/16):

  • Root cause: the shared EIR element join called .codegen_repr() directly,
    which relabeled homogeneous resource<stream> array elements as int.
    [STDIN, STDOUT, STDERR] therefore printed raw payloads 0|1|2.
  • Fix: route the joined type through ir_array_storage_type(...), preserving
    resource metadata while retaining normal codegen normalization for other
    element types.
  • Added the equivalent associative resource-literal regression.

Focused validation:

  • both indexed and associative resource-literal round trips pass;
  • 9 heterogeneous match regressions pass;
  • the empty-array merge regression passes;
  • cargo build and git diff --check pass.

Published head: 5bb580ee530831be0b2e80198fbdcdb3b40a073b.
Fresh exact-head CI has been triggered and is currently queued.

@mirchaemanuel

Copy link
Copy Markdown
Contributor Author

Looking into this. On the current head 5bb580ee5, which carries the fix for the resource-literal regression reported earlier today, the check Eval Codegen Tests (linux-x86_64 6/16) is a failure: failing step Run eval codegen test shard, process exit code 100.

The individual test name is not surfaced in the exposed job log, so we cannot name it yet. The rest of the matrix on that head is green.

This comes from the CI logs; we have not reproduced it locally yet and are not claiming a cause.

@nahime0

nahime0 commented Jul 24, 2026

Copy link
Copy Markdown
Member

Thanks @mirchaemanuel

@mirchaemanuel

Copy link
Copy Markdown
Contributor Author

Followed up on the single red check here, and I don't think it's this PR. Correcting my earlier comment too: I said the test name wasn't recoverable — it wasn't from the job-log API, but it is in the run's full log archive.

The check isn't a failing assertion, it's a timeout:

Summary [1420.440s] 70 tests run: 69 passed, 1 timed out, 6694 skipped
TRY 1 TMT [60.016s] codegen::eval::test_eval_declared_class_rejects_throwable_interfaces
TRY 1 TRMNTG [> 60.000s] (terminating)

Killed at exactly the global 60s slow-timeout. What I measured:

test_eval_declared_class_rejects_throwable_interfaces
main @ 0fd01c162 15.99s
this PR @ 5bb580ee5 15.90s

Same test, same machine (macOS aarch64), back to back. This branch doesn't move it.

Three other things point the same way. The test is pre-existing — added by 16d65e16e in main, and none of this branch's commits touch it or the eval path. Eval Codegen Tests 6/16 is green on macos-aarch64 and on linux-aarch64; only linux-x86_64 timed out, and shard 6/16 contains the same tests on every arch. And that shard averaged ~20s per test over its 1420s, so it was a slow, contended runner to begin with.

So it reads as a runner-side timeout on a test that normally has a ~4x margin, not a regression. Could you re-run that one job? I don't have admin rights on the repo, so I can't trigger it, and it's the fastest way to confirm.

I deliberately did not add a .config/nextest.toml override to force it green. The two existing overrides are documented for tests that legitimately run ~65-70s and ~40s; at 15.9s this one doesn't fit that profile, and an override would mask the symptom rather than explain it. If it reproduces on a re-run, that changes the picture and it's worth a proper look — happy to dig in then.

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

Labels

area:eir Touches EIR definitions, lowering, validation, or passes. area:runtime Touches runtime helpers, GC, ownership, or bridge runtimes. area:types Touches type checking, inference, or compatibility. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:m Medium-sized pull request. type:fix Corrects broken or incompatible behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Checker types a heterogeneous array/array merge as mixed, rejecting valid array uses (by-ref, in_array, array_sum, spread)

2 participants