Skip to content

fix(hy-emit): implement N5 zero-value map auto-allocation guards on index writes (fixes #645) - #663

Closed
dezzy-music wants to merge 1 commit into
limitlessprojects:mainfrom
dezzy-music:fix/n5-zero-map-auto-alloc-645
Closed

dezzy-music wants to merge 1 commit into
limitlessprojects:mainfrom
dezzy-music:fix/n5-zero-map-auto-alloc-645

Conversation

@dezzy-music

Copy link
Copy Markdown
Contributor

Resolves #645.

Summary

SPEC-LANGUAGE §3.1 N5 defines map zero-value semantics as:

var m map[k]v; m[k] = v works (first write allocates), where Go panics on a nil map. Why: removes an entire nil-shaped panic class from Hybrid-declared code at the cost of one emitted guard.

Previously, hy-emit did not emit N5 auto-allocation guards on map index writes (m[k] = v, b.counts[k] = v, compound assignments, or increment/decrement). As a result, writes to zero-valued maps or map fields in zero-valued structs panicked at runtime with panic: assignment to entry in nil map.

Changes

  1. Emitter Type Tracking (compiler/crates/hy-emit/src/lib.rs):
    • Added var_map_types to Emitter and ScopeSnapshot to track declared map types for local variables and parameters.
    • Preserved full map type signatures in struct_fields (e.g. counts: map[string]int) rather than discarding them via base_ty_name.
  2. N5 Auto-Allocation Guard Emission (compiler/crates/hy-emit/src/stmt.rs):
    • Added map_type_of_place and struct_type_of_place to inspect index assignment targets (m[k], b.counts[k], nested struct fields).
    • Before emitting map index assignments, compound assignments, or increments/decrements, emitted an N5 auto-allocation guard:
      if base == nil {
          base = make(map_ty)
      }
  3. Tests (compiler/crates/hy-emit/src/tests.rs):

Verification

  • cargo test --manifest-path compiler/Cargo.toml -p hy-emit: 172/172 passed.
  • python tools/check_conflict_markers.py: none.

@limitlessprojects

Copy link
Copy Markdown
Owner

Credit-close: #645 is fixed by landed #662 (Morphexe), merged to main as 74af832 — declaration-site zero-map allocation via the new zeroinit.rs. Thank you, @dezzy-music. This PR's approach was seriously considered as a composition (write-site guards on top of declaration-site allocation) and rejected for a concrete emission defect; details below.

Per-scenario verification on landed main (74af832)

This PR's test pins exactly the issue #645 repro program (var m map[string]int; m["a"] = 1 + var b bag; b.counts["k"] = 2). On the landed tree, without any write-site guard:

Scenario landed tree 74af832
var m map[string]int; m["a"] = 1 emits var m map[string]int = map[string]int{} → runs 1
var b bag; b.counts["k"] = 2 emits var b bag = bag{counts: map[string]int{}} → runs 1
compound (t["z"] += 7), partially-keyed literal, nested struct, aliased map type (tags tally), new(bag) all allocate at declaration; corpus fixture n5-zero-map-first-write.hy pins all six byte-exact (differential 24+57, cargo 1281/0)

Why the guard take is not composed on top

  1. Generic-struct fields emit invalid Go. map_type_of_place resolves a field's map type from the DECLARED field type text. For struct box[t] { vals map[string]t } and b := box[int]{}; b.vals["a"] = 1, the guard would emit if b.vals == nil { b.vals = make(map[string]t) } — t is not a type in scope; go build rejects it. Landed zeroinit.rs deliberately skips generic structs (documented) rather than emitting a wrong type.
  2. Aliased map fields are missed. The Field leg requires fty.starts_with("map["), so a tags tally field (alias for map[string]int) gets no guard — the panic this PR fixes would survive for exactly the shapes fix(hy-emit): N5 zero-value maps allocate at declaration so the first write works (fixes #645) #662 handles (shelf{tags: tally{}}).
  3. Double evaluation. The guard re-emits the base place (emit_place(base) twice): for a call-result base (f()[k] = 1) the call runs twice — the emitter's never-a-wrong-emission contract prefers not to duplicate side effects. zeroinit.rs's header documents this as the reason for declaration-site lowering.
  4. Redundancy. Every site this PR's test pins is already covered at the source by fix(hy-emit): N5 zero-value maps allocate at declaration so the first write works (fixes #645) #662; the remaining guard sites (call-result/index bases) are exactly where map_type_of_place returns None — so the composition adds no coverage while adding (1).

If the write-site-guard direction is wanted later (e.g. for make([]bag, n) element maps, which #662 explicitly does not cover), it needs place-typed resolution with alias resolution and type-argument substitution for generic structs — happy to review a re-worked minimal PR against current main.

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.

[soundness] N5 not implemented: a zero-value map write (var m map[string]int; m["a"] = 1) passes hyb check, builds, and panics at runtime

2 participants