Skip to content

fix(autoload): give autoloaded files distinct span identities - #2038

Merged
nahime0 merged 3 commits into
mainfrom
fix/autoload-span-identity
Oct 24, 2026
Merged

nahime0 merged 3 commits into
mainfrom
fix/autoload-span-identity

Conversation

@nahime0

@nahime0 nahime0 commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Problem

Autoloaded class files were lexed with the entry file's source identity 0, and the resolver of their own includes rewound the identity counter (Span::reset_source_ids). Two physically different files therefore produced equal spans for statements at the same line and column, and every span-keyed checker result leaked between them: throw_access_sites, builtin_call_types, loop_storage_types, the call-type memo, the local binding decisions.

Concrete miscompile (the Ivory repro, PSR-4 App\ -> src/): src/Fixed.lfc:9:9 initializes a readonly property, src/Counter.lfc:9:9 is a plain $this->total = ... write. Counter::add() got Fixed's readonly guard and the program died with

Fatal error: Uncaught Error: Cannot modify readonly property App\Fixed::$value in .../main.lfc:9

instead of printing 2. Ivory hit this as soon as it had a second class with a readonly property assigned in a constructor (6 unrelated tests broke). The same collision also made two autoloaded files with a retype at the same position fail with Cannot re-bind $x here, and checker errors inside an autoloaded file printed a bare error[7:16] that read as a position in the entry file.

Fix

The span identity approach comes from #1921 by @JGamboa; this PR ports that part as a focused change.

  1. fix(autoload): autoload::load_autoloaded_file lexes each file with Span::fresh_source_id() and resolves its includes through a new resolver::resolve_collecting_includes_continuing, which does not rewind the counter. Span::reset_source_ids moved from the shared resolver body to the root entry resolve_collecting_includes_with_defines, so it runs once per compile. Every file a compile loads now has its own identity, so equal coordinates in two files are distinct map keys.
  2. fix(diagnostics): each loader registers the path behind the identity it hands out (Span::register_source_path, per thread like the counter), and print_error / print_warning name Span::source_path() first. The span wins over a file tag because the pipeline tags every lowering, export and --emit-ir refusal with the entry filename (found by Greptile); the tag is kept only for spans with no registered source, which is every entry-file span. A checker or lowering error inside an included or autoloaded file now prints error[<path>:line:col].

Difference from #1921

#1921 also registers the entry file, so every positioned diagnostic becomes error[file:line:col]. That is a CLI-wide format change beyond #1445, so I left it out: the entry file is never registered and its diagnostics keep error[line:col]. It stays compatible with landing that change later. The other parts of #1921 (autoload prelude probe, monotonic method variadics, checker cost indexes, schema error spans) are untouched here and remain #1921's.

Tests

  • tests/codegen/spl/autoload_span_identity.rs (new):
    • the exact three-file LFC repro through the CLI, expects 2 (fails on main with the readonly error above);
    • a PHP variant through the in-process multi-file harness with the plain write autoloaded first, checking that the readonly class still rejects its own second write;
    • two autoloaded files with retypes at the same position compile and run (0s / 1t; main: Cannot re-bind $x here, the autoload part of Give Span a file identity so cross-file same-position collisions stop being compile errors #778);
    • --check on an autoloaded file with an undefined class names src/Broken.lfc:7:16 (main: error[7:16]).
  • tests/codegen/include_paths.rs: a checker error inside a required file names lib.php:3:12 (main: error[3:12]), and a lowering refusal inside a required file (--emit-ir) names lib.php:6:5 (main: error[main.php:6:5]).
  • Unit tests: span::tests::registered_source_paths_name_the_file_of_their_spans, errors::report::tests::diagnostic_file_falls_back_to_the_span_source.

I reproduced the failing behaviour of the integration fixtures on main with an existing build; the new code itself is validated by CI only (no local cargo runs on this shared machine).

Not addressed

  • The runtime location of a thrown error (Throwable::getFile(), the in <file>:<line> suffix of the uncaught report) is still the per-module entry script path for code that came from an included or autoloaded file (lower_throwable_get_file). With this fix the bogus readonly error no longer happens, but a genuine error raised in a class file still names main.lfc. That needs a per-throwable file slot in the runtime and is left for a follow-up.
  • The Cannot re-bind message and its cli-reference.md section still say a position carries no file; after this PR neither of the causes they list (two files, a file required twice) can trigger it. Left for a follow-up so the message and docs change together.

Fixes #1445. Overlaps #1921.

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
(#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 #1921 by @JGamboa.
…iagnostics

Checker and lowering diagnostics carry only a span, so an error inside an
autoloaded class file printed a bare error[line:col] that read as a position
in the entry file. Each loader now registers the path behind the source
identity it hands out (Span::register_source_path), and the error and warning
printers fall back to that path when the diagnostic names no file. Entry-file
diagnostics keep the error[line:col] form.
@github-actions github-actions Bot added area:resolver Touches include, namespace, name, or autoload resolution. size:m Medium-sized pull request. type:fix Corrects broken or incompatible behavior. labels Oct 24, 2026
@nahime0
nahime0 marked this pull request as ready for review October 24, 2026 07:29
@greptile-apps

greptile-apps Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the previous filename issue is fixed and no new actionable issues were found.

Summary

This PR gives included and autoloaded files distinct span identities and records their paths for diagnostics.

  • Autoloaded class files keep their checker results separate.
  • Diagnostics from loaded files name the file they came from.

Reviews (2) · Last reviewed commit: "fix(diagnostics): let the span's source ..." · Reviewed by Greptile

Comment thread src/errors/report.rs Outdated
The pipeline tags every lowering, export and --emit-ir refusal with the entry
filename, so a refusal inside an included file printed that file's line and
column under main.php. The printer now asks the span's registered source first
and keeps the tag only for spans with no registered source (every entry-file
span), so those diagnostics are unchanged.
@nahime0
nahime0 added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit ae1858e Oct 24, 2026
148 checks passed
@nahime0
nahime0 deleted the fix/autoload-span-identity branch October 24, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:resolver Touches include, namespace, name, or autoload resolution. 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.

Give autoloaded files distinct span identities

1 participant