feat(lint): E003 undefined-name pass — #404 increment one#457
Merged
Conversation
A name that is READ somewhere but BOUND nowhere in the file — not by any assignment in any scope, not a param/binder, not a builtin, not a top-level name of a literally-load_file'd file — cannot resolve on any execution path. The runtime raises 'undefined variable' only when that path runs; E003 surfaces it statically, including on cold branches (the classic dynamic-language typo tax the 2026-07 survey ranked highest correctness yield per week). The model (docs/DIAGNOSTICS.md 'Name resolution'): - Binding set is a whole-file OVER-approximation — silent on outward-is, local shadowing, sibling-branch first assignment. Over-collecting can only miss a bug, never invent one; every uncertain path fails open (a false positive breaks consumer lint gates). - Builtins come from register_builtins() itself on a scratch Env — the old hand-copied BUILTINS list had drifted ~120 names and carried a phantom 'error' entry (the E003 fixtures caught it referencing a builtin that does not exist). - NEW src/ext_names.h: X-macro name lists for the conditionally-compiled extensions (gfx/audio, http, db, model). The registrars now expand the same lists the linter binds, so registration and name-resolution structurally cannot drift, and extension names resolve regardless of the linting binary's build flags (the lint describes the language surface, not one build). - Literal load_file of "path" resolves via the runtime's own resolve_eigenscript_file_from chain (anchored at the linted file's dir) and pulls the loaded file's binders transitively — entry points that assemble a program from parts lint clean (dmg.eigs, liferaft.eigs, tidepool.eigs verified). - Dynamic escape: eval anywhere, or a computed load_file path, disables the pass for the file. report_value (compiler special form) and the observed-loop __loop_exit__/__loop_iterations__ bindings are known. - E003 is error severity: fails --lint even at --lint-level error; the LSP now maps error-severity diagnostics to red squiggles (severity 1) and anchors E003's load_file resolution at the document's path. NEW '# lint: allow-file <CODE>' file-wide suppression (both --lint and LSP) — the documented escape for module fragments that a loader load_files into scope it provides; applied to the 12 lib/ui fragment files. lib/invariant.eigs instead became self-contained (loads lib/observer.eigs for cs_signature — pure defines, idempotent). The five deliberately-undefined names in tests/ carry per-line allows. lint_collect gained the source path (LSP passes the doc URI's path); resolve_eigenscript_file_from was already exported. Calibration: zero E003 across all 301 lib/+examples/+tests/ files and across DMG, EigenRegex, liferaft, Tidepool (entry files; Tidepool's six src/ fragments pre-armored with allow-file headers in that repo, since its make lint greps per-file). Gates: release suite 2565/2565; ASan+UBSan detect_leaks=1 2565/2565, leak tally 0; tests/test_lint.sh 57/57 (12 new E003/allow-file cases); test_lsp.py 67/67 incl. 3 new E003 severity/suppression cases; test_lsp_asan.sh clean; make http / make gfx / freestanding-check green. Closes nothing — #404's later increments (path-precise analysis, #407 column spans) stay open. Co-Authored-By: Claude Fable 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.
Increment one of #404 (scope-aware name-resolution lint — ROADMAP's sanctioned alternative to a type system, ranked "highest correctness yield per week" by the 2026-07 survey).
What
E003 — undefined name: no binding on any path. A name read somewhere but bound nowhere in the file. The runtime raises
undefined variableonly when that path executes; E003 surfaces it statically — including on cold branches, the classic dynamic-language typo tax:Error severity: fails
--linteven at--lint-level error; red squiggles in the LSP (which now maps error-severity diagnostics to severity 1 and anchors resolution at the doc path).The model (zero-false-positive discipline; full spec in docs/DIAGNOSTICS.md)
is,localshadowing, sibling-branch first assignment, module-qualified names. Over-collecting can only miss a bug, never invent one; every uncertain path fails open.register_builtins()on a scratch env). The old hand-copied list had drifted ~120 names and carried a phantomerrorentry — the fixtures caught that phantom on first run.src/ext_names.h: X-macro lists the gfx/http/db/model registrars now expand and the linter binds — registration and name-resolution structurally cannot drift, and extension names resolve regardless of the linting binary's build flags.load_filefollowed transitively via the runtime's own resolution chain — entry-point files that assemble a program from parts lint clean (dmg.eigs, liferaft.eigs, tidepool.eigs all verified).evalanywhere or a computedload_filepath disables the pass for the file.# lint: allow-file <CODE>file-wide suppression (CLI + LSP) — the documented escape for module fragments (applied to the 12lib/ui_*fragment files).lib/invariant.eigsbecame self-contained instead (loadslib/observer.eigs; pure defines, idempotent).Calibration (the acceptance gate)
Zero E003 across all 301
lib/+examples/+tests/files, and across DMG, EigenRegex, liferaft, and Tidepool. Tidepool is the only consumer with a per-file CI lint gate; its sixsrc/fragments were pre-armored with allow-file headers in that repo (inert under the v0.26.0 pin).Gates
detect_leaks=12565/2565, leak tally 0tests/test_lint.sh57/57 (12 new E003/allow-file fixtures, firing + non-firing)tests/test_lsp.py67/67 (3 new: E003 squiggle, severity 1, allow-file suppression);test_lsp_asan.shcleanmake http,make gfx,make freestanding-checkgreen (ext_dbuntestable locally — no libpq on this box; its registrar change is mechanically identical to the three that compile, CI's db path will confirm)#404 stays open — path-precise analysis and #407 column spans are later increments.
🤖 Generated with Claude Code