Skip to content

fix(hy-check): reject untyped nil local declarations (var x = nil / x := nil / let x = nil) with HYB-E4003, the N1 untyped leg (fixes #672) - #673

Merged
limitlessprojects merged 1 commit into
limitlessprojects:mainfrom
milkingndb-oss:e4003-untyped-nil-decl
Oct 18, 2026
Merged

limitlessprojects merged 1 commit into
limitlessprojects:mainfrom
milkingndb-oss:e4003-untyped-nil-decl

Conversation

@milkingndb-oss

Copy link
Copy Markdown

Fixes #672

The bug

The N1 nil check at both local-declaration check sites was ascription-gated (if let Some(t) = &l.ty). With no ascription - var x = nil / x := nil / let x = nil - there is no branch to take, shape_of_expr(nil) is TypeShape::Unknown, and the checker accepted the declaration with 0 diagnostics. The emitter then emitted var x = nil / x := nil verbatim, which Go rejects outright: use of untyped nil in variable declaration. Accepted-but-unbuildable - GUARANTEES row 13 (BUILDABLE). Full repro table + controls in #672.

The fix

  • New registry code HYB-E4003 untyped-nil-declaration (rule N, tier0 N1, category nil-safety): nil has exactly two homes (N1) - error and the foreign-Go perimeter (N2) - and both must be visible in a DECLARED type; an untyped nil can never name either. Three fix routes taught: ascribe the nil home (var e error = nil); declare without initializer for the zero value (N3: var ch chan int); option[T] for absence. A new code rather than reusing E4001 because E4001's message and fix-its are built around a named declared type (nil is not valid for {ty}) - here the defect is precisely that no type exists.
  • loans.rs process_local: else-branch on the E4001 ascription gate emits E4003 for bare-nil initializers of var/let/:= kinds (the flat walk covers loop/match-arm/if-block bodies).
  • closures.rs check_stmt_boundaries: the same else-branch for closure bodies (the flat walk skips FnLit bodies; this walk owns them).
  • Local const c = nil is left alone: Go's error there is nil is not constant - the E6003 constant-expression guard's locus (Soundness & buildability gaps in make/new shape resolution, closure builtin validation, bare chan T expressions, and file-level const forward references #640, PR fix(hy-check): reject nil consts and support f32/f64/char and string aliases in const_init.rs #671). The two loci do not double-fire (l.kind != LocalKind::Const in both branches).

Verification

gate result
cargo test --workspace 1150 passed, 0 failed (the one spec_t3_strings failure is a pre-existing CRLF-in-checkout issue - reproduces on clean 0ca08c2, unrelated)
new unit pins hy-check/tests/untyped_nil_decl.rs 13/13: all four spellings (var/:=/let/parallel a, b := nil, 1), closure body, loop body, match arm, blank _; controls stay accepted (ascribed var e error = nil, bare nil as call argument, ch == nil compare, zero-value declarations); local const c = nil does not fire E4003
corpus harness 80/80 verdict agreement, tripwire not fired (new fixture untyped-nil-decl-reject.hy pins REJECT N1 + exact diagnostic HYB-E4003, registered additively in VERDICTS.md)
differential reject-side 57/57
hyb explain HYB-E4003 renders from the registry with the three fix routes

Corpus/examples impact: zero - no existing corpus/example file contains an untyped-nil declaration (grepped); the fix is regression-safe by construction.

Claim check

Searched open issues + PRs for untyped nil, var x = nil, use of untyped nil, E4003: no claim on this locus. #640/#670/#671 are the const-nil family (E6003, expr_is_constant_inner); corpus 28 is composite-literal nested nils; the ascribed E4001 sites are unchanged.

… := nil / let x = nil) with HYB-E4003, the N1 untyped leg (fixes limitlessprojects#672)

The E4001 nil check at both local-declaration sites was ascription-gated
(if let Some(t) = &l.ty): with no ascription, shape_of_expr(nil) is
Unknown, the checker accepted the declaration with 0 diagnostics, and the
emitter emitted �ar x = nil / x := nil — which Go rejects outright
(use of untyped nil in variable declaration). Accepted-but-unbuildable
(GUARANTEES row 13, BUILDABLE).

- new registry code HYB-E4003 untyped-nil-declaration (rule N, tier0 N1):
  nil's two homes (error, foreign perimeter) must be visible in a
  DECLARED type; an untyped nil can never name either. Three fix routes:
  ascribe the nil home; declare without initializer for the zero value
  (N3); option[T] for absence.
- loans.rs process_local: else-branch on the E4001 ascription gate emits
  E4003 for bare-nil initializers of var/let/:= (flat walk covers
  loop/match-arm/if-block bodies).
- closures.rs check_stmt_boundaries: same else-branch for closure bodies.
- local const c = nil is left to the E6003 constant-expression guard
  (issue limitlessprojects#640 / PR limitlessprojects#671) — Go's error there is 'nil is not constant';
  the two loci do not double-fire.
- 13 unit pins (hy-check/tests/untyped_nil_decl.rs): four spellings,
  closure/loop/match-arm/blank; controls that stay accepted (ascribed
  error-nil, bare nil as call argument/compare, zero-value declarations).
- corpus fixture untyped-nil-decl-reject.hy (REJECT N1, exact diagnostic
  HYB-E4003) registered in VERDICTS.md; corpus 80/80, tripwire not fired;
  differential reject-side 57/57; cargo workspace 1150/0.

Co-Authored-By: opencode <noreply@opencode.ai>
@limitlessprojects
limitlessprojects merged commit 7aaf193 into limitlessprojects:main Oct 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants