Skip to content

feat(lint): E003 undefined-name pass — #404 increment one#457

Merged
InauguralPhysicist merged 1 commit into
mainfrom
lint-e003-undefined-name-404
Jul 6, 2026
Merged

feat(lint): E003 undefined-name pass — #404 increment one#457
InauguralPhysicist merged 1 commit into
mainfrom
lint-e003-undefined-name-404

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

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 variable only when that path executes; E003 surfaces it statically — including on cold branches, the classic dynamic-language typo tax:

$ eigenscript cold.eigs        # branch never taken → runs "fine"
ok
$ eigenscript --lint cold.eigs
cold.eigs:3: error[E003]: undefined name 'invariant_check' — no binding on any path

Error severity: fails --lint even 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)

  • Whole-file over-approximated binding set — silent on outward-is, local shadowing, sibling-branch first assignment, module-qualified names. Over-collecting can only miss a bug, never invent one; every uncertain path fails open.
  • Builtins from the runtime's own registry (register_builtins() on a scratch env). The old hand-copied list had drifted ~120 names and carried a phantom error entry — the fixtures caught that phantom on first run.
  • New 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.
  • Literal load_file followed 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).
  • Dynamic escape: eval anywhere or a computed load_file path disables the pass for the file.
  • New # lint: allow-file <CODE> file-wide suppression (CLI + LSP) — the documented escape for module fragments (applied to the 12 lib/ui_* fragment files). lib/invariant.eigs became self-contained instead (loads lib/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 six src/ fragments were pre-armored with allow-file headers in that repo (inert under the v0.26.0 pin).

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 fixtures, firing + non-firing)
  • tests/test_lsp.py 67/67 (3 new: E003 squiggle, severity 1, allow-file suppression); test_lsp_asan.sh clean
  • make http, make gfx, make freestanding-check green (ext_db untestable 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

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>
Comment thread src/lint.c Dismissed
@InauguralPhysicist InauguralPhysicist merged commit e0717be into main Jul 6, 2026
17 checks passed
@InauguralPhysicist InauguralPhysicist deleted the lint-e003-undefined-name-404 branch July 6, 2026 19:51
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.

2 participants