Skip to content

fix(hy-check): reject untyped nil local declarations (fixes #672) - #691

Closed
dezzy-music wants to merge 1 commit into
limitlessprojects:mainfrom
dezzy-music:fix/untyped-nil-declaration-672
Closed

dezzy-music wants to merge 1 commit into
limitlessprojects:mainfrom
dezzy-music:fix/untyped-nil-declaration-672

Conversation

@dezzy-music

Copy link
Copy Markdown
Contributor

Fixes #672

Problem

Declaring a variable initialized to bare nil without a declared type (var x = nil, x := nil, let x = nil, a, b := nil, 1, var _ = nil, or inside closure/loop/if bodies) passed hyb check with 0 diagnostics and emitted Go that failed go build:

use of untyped nil in variable declaration

This violates the accepted-implies-buildable contract (C5a, rule BUILDABLE / N1).

The gap was that the N1 nil check at both local-declaration check sites (hy-check/src/loans.rs process_local and hy-check/src/closures.rs check_stmt_boundaries) was gated behind if let Some(t) = &l.ty. When l.ty.is_none(), bare nil passed through uninspected.

Solution

  1. Diagnostic Registration (HYB-E4003):
    • Added HYB-E4003 (untyped-nil-declaration) under category nil-safety, severity error, rule N, tier0_rule_id N1 in compiler/crates/hy-diag/data/diagnostics.json.
  2. Checker Enforcement (sendability.rs, loans.rs, closures.rs):
    • Added emit_untyped_nil_error helper in Checker (sendability.rs).
    • In loans.rs (process_local) and closures.rs (check_stmt_boundaries), added an else if l.kind != LocalKind::Const && Checker::is_nil_literal(value) branch when l.ty.is_none().
    • Leaves local const c = nil to the E6003 constant-expression guard (issue Soundness & buildability gaps in make/new shape resolution, closure builtin validation, bare chan T expressions, and file-level const forward references #640) as Go reports nil is not constant there.
    • Controls preserved:
      • var e error = nil (ascribed error) continues to accept.
      • var ch chan int / var s []int (zero-value declarations without initializer) continue to accept.
      • ch == nil / f(nil) (comparisons and call arguments) continue to accept.
      • Ascribed non-nilable (var s []int = nil) continues to reject with HYB-E4001.
  3. Tests & Corpus Pin:
    • Added compiler/crates/hy-check/tests/untyped_nil_decl.rs covering all 14 test cases (all declaration spellings, closure bodies, loop bodies, if-inits, match arms, blank identifiers, and controls).
    • Added tests/checker-corpus/untyped-nil-decl-reject.hy pinning REJECT N1 [HYB-E4003].

Verification

  • cargo test --manifest-path compiler/Cargo.toml --test untyped_nil_decl: 14 passed, 0 failed.
  • cargo test --manifest-path compiler/Cargo.toml -p hy-check: all passed, 0 failed.
  • cargo test --manifest-path compiler/Cargo.toml -p hy-diag: 8 passed, 0 failed.
  • python tools/corpus_harness.py: 80/80 verdict agreement, tripwire not fired.
  • python tools/check_conflict_markers.py: conflict markers: none.

@dezzy-music
dezzy-music force-pushed the fix/untyped-nil-declaration-672 branch from 234c6d9 to 7273bde Compare October 18, 2026 17:02
limitlessprojects added a commit that referenced this pull request Oct 18, 2026
…l declarations (N1 untyped leg)

Merge pr-673 (milkingndb-oss, fixes #672): reject untyped nil local declarations — HYB-E4003, the N1 untyped leg (var x = nil / x := nil / let x = nil / a, b := nil, 1 / var _ = nil, incl. closure/loop/match-arm bodies)

Live on main (probed 0ca08c2, go1.26.9 twin): all ten spellings hyb-check ACCEPT with 0 diagnostics, then go build fails 'use of untyped nil in variable declaration'. The N1 ascribed-nil check was gated on 'if let Some(t) = &l.ty' at both local-decl sites (loans.rs process_local + closures.rs check_stmt_boundaries); untyped decls fell through uninspected.

Fix (PR verbatim): untyped-nil else-leg at both sites emitting registered HYB-E4003 (nil-safety, rule N, tier0 N1); local const c = nil stays E6003's locus (#640), no double-fire.

Verified on merge c5126b0+pr-673: unit suite untyped_nil_decl.rs 13/13 (4 spellings, closure/loop/match-arm bodies, blank ident; controls: ascribed error-nil, bare nil as call arg/compare, zero-value decls, const-nil non-double-fire) — all ten probe spellings REJECT HYB-E4003; controls keep verdicts (var e error = nil ACCEPT, zero-value decls ACCEPT, f(nil)/ch==nil ACCEPT, var s []int = nil still E4001); corpus 80/80 (new fixture untyped-nil-decl-reject.hy, VERDICTS.md entry, parse_all 79->80); differential FULL 23 accept + 57 reject; cargo --workspace 1168/0; #524 Go-twin float matrix re-verified intact (const 1.0/0.0 REJECT E6001 x2, _ = 1.0/0.0 REJECT, runtime x/0.0 ACCEPT x3); CI green on head 17ff816 (quick+compiler).

Credit: milkingndb-oss (this take: corpus fixture + registry date bump + E6003-scope control). Dezzy-music's parallel take #691 (same fix, emit helper emit_untyped_nil_error) — its two extra shapes (if-body and if-init untyped nil) land in the follow-up integration commit; credit to both authors per duplicate-family rule.
@limitlessprojects

Copy link
Copy Markdown
Owner

Landed — thank you @dezzy-music. Your take is correct and its two unique shapes are carried; the duplicate-family rule kept one landing (same defect, same fix, same three check-site hunks as #673).

Landed fix: #673 (milkingndb-oss) merged server-side as 7aaf193 — registered HYB-E4003 untyped-nil-declaration (nil-safety, rule N, tier0 N1) emitted from an untyped else-leg at both local-declaration check sites (loans.rs process_local + closures.rs check_stmt_boundaries), gated off LocalKind::Const so local const c = nil stays E6003's locus (#640). Corpus fixture untyped-nil-decl-reject.hy + VERDICTS.md entry registered (corpus 80/80, parse_all 79→80). Follow-up commit aeac72a carries your take's two extra shapes into the landed suite (if_body_untyped_nil_is_e4003, if_init_untyped_nil_is_e4003) — suite now 15/15, cargo --workspace 1170/0.

Per-take verification on main after the landing (live-bypass probe, was LIVE before — all ten accepted with 0 diagnostics then failed go build 'use of untyped nil in variable declaration', twin-verified with go1.26.9):

#691 claim before after
var x = nil ACCEPT (unbuildable) REJECT HYB-E4003
x := nil ACCEPT REJECT HYB-E4003
let x = nil ACCEPT REJECT HYB-E4003
a, b := nil, 1 ACCEPT REJECT HYB-E4003
var _ = nil ACCEPT REJECT HYB-E4003
closure-body var x = nil ACCEPT REJECT HYB-E4003
loop-body var x = nil ACCEPT REJECT HYB-E4003
if-body var x = nil (your extra) ACCEPT REJECT HYB-E4003 (+ unit pin)
if-init if x := nil; ... (your extra) ACCEPT REJECT HYB-E4003 (+ unit pin)
match-arm var x = nil ACCEPT REJECT HYB-E4003

False-positive guards hold: var e error = nil ACCEPT · zero-value decls (var ch chan int) ACCEPT · f(nil) / ch == nil ACCEPT · var s []int = nil still E4001 · const c = nil still E6003's locus, no double-fire. Battery on the landed tree: cargo 1170/0, corpus 80/80, differential FULL 23+57, #524 Go-twin float matrix intact. CI green on landed heads (17ff816 quick+compiler; aeac72a on main).

Credit: dezzy-music (this take + the two carried shapes) and milkingndb-oss (landed take #673, corpus registration). Closing as duplicate-fixed; issue #672 tracks the defect.

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