Skip to content

feat(typecheck): report a type name that resolves to nothing - #3409

Merged
gHashTag merged 1 commit into
masterfrom
fix/type-names
Sep 7, 2026
Merged

feat(typecheck): report a type name that resolves to nothing#3409
gHashTag merged 1 commit into
masterfrom
fix/type-names

Conversation

@gHashTag

@gHashTag gHashTag commented Sep 7, 2026

Copy link
Copy Markdown
Owner

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

input exit says
a parse error 1 Error: Expected RParen, got Eof
a valid spec 0 Typecheck OK (0 errors, 0 warnings)
undefined type names 0 Typecheck OK (0 errors, 0 warnings)

check can 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

reader names specs
rustc, over the emitted Rust 62 61 of 651
this check 84 169

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.t27 uses AgentProfile and declares it nowhere → flagged here, missed by rustc.
  • specs/igla/coder/eval.t27 does 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 write use base::types::Float;, importing a name that does not exist.
  • Trit — declared by four specs as pub const Trit = enum(i8) { .. }, and simply not imported by the files that use it. specs/ar/coa_planning.t27 uses it with no use statement at all.
  • The rest are project types that were never written.

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

  1. 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 that form produced 33 false warnings on specs/base/types.t27 alone.
  2. 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.
  3. The language has two type resolvers and they disagree. resolve_type_str knows 15 spellings; the emitter’s t27_type_to_rust also knows the language’s own keywords — inti32, floatf64, string&static str. Asking only the first reported all three as unknown in coa_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 string lowers correctly but Option<string> and List<string> emit string verbatim.

Non-regression, measured rather than argued

Reported as warnings. Across all 651 specs the exit code of check changes for zero of them:

specs=651  exit-code changes=0

Making 169 specs fail is a decision about what check means, 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:

  • dropping the emitter consult → only the_language_own_keyword_spellings_are_not_unknown fails;
  • dropping the const-alias case → only a_type_alias_spelled_as_a_const_is_a_declaration fails.

bootstrap/stage0/FROZEN_HASH updated in the same commit, as M5 requires.

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
gHashTag enabled auto-merge (squash) September 7, 2026 20:14
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-09-07 20:14:44 UTC

Summary

Status Count
Total Open PRs 16
PRs with Failing Checks 13
PRs with All Checks Green 3
READY 2
FAILING 13
PENDING 0
NO CHECKS YET 0

These columns do not partition: 2 + 13 + 0 + 0 = 15, and there are 16 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=e4081dfc5d25 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@gHashTag
gHashTag merged commit 203f5be into master Sep 7, 2026
33 of 36 checks passed
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>
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.

typecheck accepts a type name that resolves to nothing; rustc is the first reader to notice

1 participant