Skip to content

fix(checker): file-qualified diagnostics, autoload span identities and preludes, checker cost - #1921

Merged
nahime0 merged 8 commits into
illegalstudio:mainfrom
JGamboa:fix/checker-driver-cost-and-diagnostics
Oct 24, 2026
Merged

nahime0 merged 8 commits into
illegalstudio:mainfrom
JGamboa:fix/checker-driver-cost-and-diagnostics

Conversation

@JGamboa

@JGamboa JGamboa commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Four checker-driver fixes found while checking a large Composer project (a Laravel 13
application, ~3400 classes). They share the checker driver, pipeline and source-identity infrastructure. The shared
source-identity fix has also landed on main through #2038; this PR preserves it while
extending entry-file diagnostics, prelude discovery and checker optimizations.

Diagnostic format change. Every diagnostic that has a source position now names its
file: error[main.php:3:6]: Undefined class: Missing instead of
error[3:6]: Undefined class: Missing, and the same for warnings. This affects every
checker and later diagnostic, including the ones in the entry file. Parse errors already
printed error[file:line:col]; now all diagnostics do. The entry file is named as it was
given on the command line; included and autoloaded files by the path they were loaded
from. Diagnostics with no position still print error: message. README's "Error
messages" section is updated. Tests that match the message text (error_tests) are
unaffected; anything that parses error[line:col] literally would need updating.

  1. file:line:col diagnostics and distinct span identities for autoloaded files.
    Autoloaded files were lexed with source id 0 (the entry file's), and the resolver of their
    includes rewound the id counter, so spans of different files collided as map keys. Each
    autoloaded file now gets its own source id (resolve_collecting_includes_continuing), and
    an id -> path registry (Span::register_source_path / Span::source_path) lets
    print_error / print_warning name the file when the error itself carries none. Class
    schema errors (circular inheritance, final class, duplicate class, unknown parent) point at
    the class declaration instead of Span::dummy().
    Visible effect besides the format: two autoloaded class files with a retyping assignment at
    the same line and column no longer fail with "Cannot re-bind $x here" (Give Span a file identity so cross-file same-position collisions stop being compile errors #778).

  2. Post-autoload preludes. The pdo, mysqli, tz, image, hash, curl, xml and var_export
    preludes were decided on the entry program before name resolution, but the Composer
    autoload loads vendor classes later: an autoloaded class typed against PDO or
    CurlHandle failed with Unknown type: PDO unless --with-* was passed.
    autoload::prelude_probe resolves names and runs the autoload on a copy of the program,
    runs each prelude's detector over the result, and the pipeline injects the groups it finds
    as if the entry used them. The probe restores thread-local source IDs and anonymous-class numbering. Process-global
    synthetic spans and value-include temporaries stay monotonic, so a discarded probe cannot
    recycle identities allocated by another compiler thread.

  3. Monotonic method variadics. A first-class callable ($r->is(...)) promotes an untyped
    method variadic's collector to array<mixed>; a later direct call with a string narrowed
    it back to array<string>, because wider_type_syntactic lets Str absorb Mixed. The
    callable path then stored mixed values into a string collector, and the program aborted
    with "heap memory exhausted". widen_method_variadic_element only widens.

  4. Quadratic work removed (profiled on the 3400-class program; release user time
    290 s -> 62 s, same result except union member order, which HashMap order already varied):

    • update_method_return_type and update_method_callable_return_metadata walked every
      class for each method body: an index (implementing class, method, static) -> classes that
      dispatch to it;
    • propagate_constructor_arg_type walked every class per argument of each new: an index
      (property, declaring class) -> classes sharing it;
    • loop_storage_types was cleared with a retain over the whole map per method, function
      and closure: a scope -> loop spans index;
    • check_builtin scanned every declared function on each builtin call: the cheap condition
      goes first, and has_function_decl_folded / folded_map_key try the exact key before
      the scan;
    • validate_late_bound_constructor_targets walked the parent chain from every class at each
      new static: a direct-children index (class_and_descendants).

    The indexes are keyed by classes.len(): implementation and property maps are only written
    while classes are registered, which also changes the table size.

Closes #1445
Fixes the autoload part of #778 (autoloaded files no longer share span identities). The rest
of #778 (a retyping file required twice, file identity in Span equality) is not addressed
here, so #778 should stay open.

Left out

  • The descendants index for widen_override_untyped_method_params. This PR is independent of
    the chained-call PR (fix(checker): widen untyped params reached through call-result receivers #1919), which adds that function; once both land, a follow-up switches
    it to class_and_descendants too.
  • The opt-in fixpoint tracer (ELEPHC_CHECKER_TRACE) of the original branch.
  • core and bcmath preludes are not probed through the autoload closure (they postdate the
    original change); an autoloaded file that is the only user of WeakMap or BcMath\Number
    is still not covered.
  • Known remaining gap: two autoloaded files whose retype warnings sit at the same line and
    column still print only one of the two "changes type" warnings (the program compiles and
    runs correctly).

Tests

After merging main 30f1305e23, 14 exact local unit, CLI and codegen tests pass:

  • Four unit checks cover concurrent synthetic-span identity, discarded-probe include
    temporaries, preserved source/anonymous-class numbering, span-to-file registration, and
    the source file taking precedence over an entry-file diagnostic tag.
  • Six CLI checks cover entry/autoload diagnostic paths, missing includes, early rejected
    autoload-closure warnings, PDO/curl prelude detection and same-position retypes.
  • Four codegen checks cover promoted method variadics, included-file lowering refusals,
    anonymous classes in includes and the original readonly-write span collision from Give autoloaded files distinct span identities #1445.

The three new regressions for global counter rollback and early warnings were verified
failing on 4666dae39 before the fixes. All focused commands ran through the shared
build queue, with one test matched per filter. The library, CLI and selected test binaries
compile without warnings; git diff --check passes.

Refs #1886

AI assistance

This change was written with substantial help from Claude Code (Anthropic). I reviewed it,
checked the expected outputs against php 8.5, and remain responsible for it.

🤖 Generated with Claude Code

@github-actions github-actions Bot added area:parser Touches parsing or AST construction. area:resolver Touches include, namespace, name, or autoload resolution. area:types Touches type checking, inference, or compatibility. size:l Large pull request. type:fix Corrects broken or incompatible behavior. labels Oct 19, 2026
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new blocking issue was established.

Summary

The PR extends file-qualified diagnostics, probes autoloaded files for needed preludes, preserves promoted method variadics, and replaces repeated checker scans with indexes.

  • Positioned checker messages name the file they come from.
  • Autoloaded files can bring in the preludes their code uses.
  • Method variadic collectors keep the widest type seen at each call.
  • Large class checks use indexes instead of repeated full-table scans.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Parse entry and resolve includes] --> B[Probe autoloaded files on a copy]
  B --> C[Restore thread-local counters]
  C --> D[Inject detected preludes]
  D --> E[Resolve names and load real files]
  E --> F[Check types using lookup indexes]
Loading

Reviews (6) · Last reviewed commit: "chore: merge main into pr-1921" · Reviewed by Greptile

Comment thread src/autoload/prelude_probe.rs
Comment thread src/pipeline.rs Outdated
…d preludes, checker cost

Diagnostics:
- Each autoloaded file gets its own source id, and the resolver of its
  includes no longer rewinds the counter, so spans of different files no
  longer collide (two autoloaded files with a retype at the same line and
  column no longer fail with "Cannot re-bind $x here").
- Span::register_source_path / source_path map ids to files; print_error and
  print_warning use it when the diagnostic carries no file, so every
  positioned diagnostic is now `kind[file:line:col]`, entry file included.
- Class schema errors (circular inheritance, final class, duplicate class,
  unknown parent) point at the class declaration instead of Span::dummy().

Preludes: autoload::prelude_probe resolves names and runs the autoload on a
copy of the program, so pdo, mysqli, tz, image, hash, curl, xml and
var_export are injected when only an autoloaded file uses them. The probe
rewinds every counter it advanced.

Method variadics: widen_method_variadic_element only widens, so a collector
a first-class callable promoted to array<mixed> is not narrowed back to
array<string> by a direct string call (the callable path then stored mixed
values into a string collector: "heap memory exhausted").

Cost (3400-class program, 290 s -> 62 s user time, same result): indexes for
method-implementation users, property sharers, loop-storage scopes and class
descendants (new static), and exact-key lookups before the case-insensitive
scans in check_builtin, has_function_decl_folded and folded_map_key.
The entry file's source path was registered only after include resolution
succeeded, so a missing require in the entry file still printed the file-less
error[line:col]. Source id 0 is never handed out again by the resolver, so the
path is now registered before it runs.
@JGamboa
JGamboa force-pushed the fix/checker-driver-cost-and-diagnostics branch from 35cec0d to 36f728c Compare October 21, 2026 06:04
Comment thread README.md
… and checker indexes

The compiler guides now cover what the README already showed: the
kind[file:line:col] diagnostic format in docs/compiling, the autoload
prelude probe and per-file source identities in the pipeline guide, and the
span-to-file registry and class-table lookup indexes in the type checker
internals. Stale error[line:col] examples are updated.
The probe description now sits in its own bullet after func-args instead
of extending the resolve/prelude entry, whose closing sentence main has
since rewritten.
nahime0 added a commit to JGamboa/elephc that referenced this pull request Oct 24, 2026
Autoloaded class files were lexed with the entry file's source identity 0, and
the resolver of their includes rewound the identity counter, so a statement at
the same line and column of two files was one key in every span-keyed checker
map. A readonly write guard of one class fired on a plain write of another
(illegalstudio#1445), and same-position retypes in two class files were refused as
ambiguous.

Each autoloaded file now takes a fresh identity, and its includes resolve
through a continuing resolver entry; only the root resolution rewinds the
counter. The approach comes from illegalstudio#1921 by @JGamboa.
@nahime0
nahime0 added this pull request to the merge queue Oct 24, 2026
Merged via the queue into illegalstudio:main with commit 539e2f0 Oct 24, 2026
143 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:parser Touches parsing or AST construction. area:resolver Touches include, namespace, name, or autoload resolution. area:types Touches type checking, inference, or compatibility. size:l Large pull request. type:fix Corrects broken or incompatible behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Give autoloaded files distinct span identities

2 participants