feat(typecheck): report a type name that resolves to nothing - #3409
Merged
Conversation
Closes #3408 `struct S { a: NoSuchTypeAnywhere }` typechecked as "Typecheck OK (0 errors, 0 warnings)", exit 0. The typechecker resolved the name to `TypeInfo::Custom(..)` and asked no further question, so the first reader to notice was rustc -- downstream, in another language, and only for the specs whose emitted Rust got far enough to be type-checked at all. Controls, so this is a finding rather than a broken probe: a parse error exits 1, a valid spec exits 0. `check` can say no. It did not say no to this. Two independent readers now disagree informatively. rustc finds 62 distinct `cannot find type` names across 61 of 651 specs; this check finds 84 names across 169. The typechecker sees the whole spec while rustc only ever sees what the emitter managed to emit. Spot-checked in both directions: igla/coder/_tmp_pipeline_import.t27 uses `AgentProfile` and declares it nowhere -- flagged here, missed by rustc -- while igla/coder/eval.t27 declares it and is not flagged. Two false-positive classes were found and closed before shipping: * `pub const PackedTrit = u8; // Type alias` is a type declaration written as a constant, as is `pub const Trit = enum(i8) { .. }`; neither carries a type ANNOTATION, which is what separates them from `pub const ONE : i8 = 1`. Missing the form produced 33 false warnings on specs/base/types.t27 alone. * `resolve_type_str` matches whole strings, so `[]Point` falls through as `Custom("[]Point")` -- the wrapper, not the type. Reading that directly would report every array as unresolved. And a third: the language has TWO type resolvers that disagree. `resolve_type_str` knows 15 spellings; the emitter's `t27_type_to_rust` also knows the language's own keywords -- `int` -> `i32`, `float` -> `f64`, `string` -> `&'static str`. Asking only the first reported all three as unknown types in specs/ar/coa_planning.t27, which the emitter lowers correctly. The check consults both; the disagreement is filed in #3408, not papered over. Reported as warnings, and the non-regression is measured rather than argued: across all 651 specs the exit code of `check` changes for ZERO of them. Making 169 specs fail is a decision about what `check` means and belongs to the owner. Six tests through the shipped binary, mutation-checked twice with one test each: dropping the emitter consult fails only the keyword test, dropping the const-alias case fails only the alias test. FROZEN_HASH updated in the same commit, as M5 requires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gHashTag
enabled auto-merge (squash)
September 7, 2026 20:14
Contributor
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
This was referenced Sep 7, 2026
gHashTag
added a commit
that referenced
this pull request
Sep 7, 2026
…ntract (#3413) Closes #3412 Every `gen-*` path splices imported declarations in before compiling. `typecheck` read the raw source. So a type arriving through an import read as undeclared, and the unknown-type check added one pass ago in #3409 warned about types the spec correctly imports -- my own defect, found by measuring my own output. Measured over all 651 specs: unknown type warnings 1283 -> 1045 all warnings 1775 -> 1570 exit-code changes 0 specs/base/ternary_add.t27 alone goes from 10 warnings to 0: it writes `use base::types;` and every type it names comes from there. Counted a second way before the repair -- asking whether each warned name appears as a declaration in the RESOLVED output -- 41 distinct names across 29 of the 169 flagged files were false, against 228 true. The naive repair is wrong, and the reason is already written down. run_gen carries the contract: Safety contract ... this may only ADD declarations, never break a spec. If the spliced source stops compiling, the original is used and the spec generates exactly what it generated before. Resolving without that fallback took specs/nn/hslm.t27 from exit 0 to `Expected RParen, got Eof at line 652:1`, while all four backends still compiled it -- because they fall back and this did not. The splice can produce source the parser rejects; that is a handled condition, not a verdict about the spec. With the contract mirrored, exactly 1 of 651 specs takes the fallback and says so on stderr rather than silently checking something other than what it claims to. Mutation-checked: disabling the resolve fails the new test with the measured number, "it warned 10 times". compiler.rs is untouched, so FROZEN_HASH is unchanged. Co-authored-by: lab <lab@example.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3408
struct S { a: NoSuchTypeAnywhere }typechecked as "Typecheck OK (0 errors, 0 warnings)", exit 0.Controls first, so this is a finding and not a broken probe
Error: Expected RParen, got EofTypecheck OK (0 errors, 0 warnings)Typecheck OK (0 errors, 0 warnings)checkcan say no. It did not say no to this. The first reader to notice was rustc — downstream, in another language, and only for the specs whose emitted Rust got far enough to be type-checked at all.Two readers, disagreeing informatively
The typechecker sees the whole spec; rustc only ever sees what the emitter managed to emit, and stops early on files carrying a hundred other errors. Spot-checked in both directions:
specs/igla/coder/_tmp_pipeline_import.t27usesAgentProfileand declares it nowhere → flagged here, missed by rustc.specs/igla/coder/eval.t27does declare it → not flagged. The check does not cry wolf.What the names actually are — only a third is a compiler question
Float,Int,Bool— declared by no spec at all. Two specs even writeuse base::types::Float;, importing a name that does not exist.Trit— declared by four specs aspub const Trit = enum(i8) { .. }, and simply not imported by the files that use it.specs/ar/coa_planning.t27uses it with nousestatement at all.So this is largely a spec defect that the compiler declined to report. Reporting it is the compiler’s job either way.
Three defects found while building the check, two of them mine
pub const PackedTrit = u8; // Type aliasis a type declaration written as a constant, as ispub const Trit = enum(i8). Neither carries a type annotation — which is what separates them frompub const ONE : i8 = 1. Missing that form produced 33 false warnings onspecs/base/types.t27alone.resolve_type_strmatches whole strings, so[]Pointfalls through asCustom("[]Point")— the wrapper, not the type. Reading that directly would report every array as unresolved.resolve_type_strknows 15 spellings; the emitter’st27_type_to_rustalso knows the language’s own keywords —int→i32,float→f64,string→&static str. Asking only the first reported all three as unknown incoa_planning.t27, which the emitter lowers correctly. The check now consults both, and the disagreement is filed rather than papered over.A fourth, found in passing and filed: the emitter does not recurse into generic wrappers — bare
stringlowers correctly butOption<string>andList<string>emitstringverbatim.Non-regression, measured rather than argued
Reported as warnings. Across all 651 specs the exit code of
checkchanges for zero of them:Making 169 specs fail is a decision about what
checkmeans, and it is the owner’s. Making the omission visible is not.Tests
Six, through the shipped binary rather than a library call — this crate has no lib target, and a test that reimplemented the check would pass against a compiler that never shipped it. Three of the six exist to prove the check stays quiet: a declared struct, a const type alias, the keyword spellings, and an array of a declared type.
Mutation-checked twice, one test each:
the_language_own_keyword_spellings_are_not_unknownfails;a_type_alias_spelled_as_a_const_is_a_declarationfails.bootstrap/stage0/FROZEN_HASHupdated in the same commit, as M5 requires.