Skip to content

fix(hy-check): prevent option[T] TypeShape erasure on pointers, sends, receives, and range loops (Fixes #753) - #755

Closed
aazminu wants to merge 2 commits into
limitlessprojects:mainfrom
aazminu:fix/option-shape-erasure-753
Closed

aazminu wants to merge 2 commits into
limitlessprojects:mainfrom
aazminu:fix/option-shape-erasure-753

Conversation

@aazminu

@aazminu aazminu commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #753

Summary

In hy-check/src/shape.rs, shape_of_type folds option[T] (TypeExpr::Generic { name: "option", args: [T] }) to shape_of_type(T) so match unwrap and payload classification can inspect the inner shape directly. However, several expression and type shape helpers returned that unwrapped TypeShape while discarding the outer option[...] TypeExpr:

  1. Indexing / Dereferencing / Projection / Calls Returning Type Aliases to option[T]:
    • expr_is_option (shape.rs) did not handle Expr::Index, Expr::Deref / Unary(Deref), Expr::Field, Expr::Paren, or Expr::Move, and checked TypeExpr forms syntactically (is_option_type_expr) without resolving type aliases (type opt_ch = option[chan int]).
    • ty_of_expr did not synthesize TypeExpr for Expr::Index on slices/arrays/maps, Expr::Deref on pointers, Expr::Receive on channels, or Expr::Field on structs.
    • multi_value_shapes and multi_value_tys did not handle Expr::Receive (opt, ok := <-ch) or comma-ok Expr::Index (v, ok := m[k]).
    • SelectRecv (select { opt := <-ch: ... }) and ForKind::Iterate (for _, opt in xs) passed None as ty to define_binding, erasing the outer option[...] wrapper on the bound variable.
    • Reassigning opt = none overwrote a specific b.ty (such as option[option[chan int]]) with option[any], erasing nested option wrappers on subsequent match unwraps.
    • Consequently, xs[0], m["k"], *p, h.opt, get_alias(), <-ch, select { opt := <-ch: ... }, and for _, opt in xs of type option[chan int] or opt_ch were treated as bare chan int (HYB-E3007 and HYB-E2008 bypassed) and as Sendable (HYB-E3001 bypassed on &xs[0], p, &h.opt, &opt).
  2. Containers & Structs Holding option[T]:
    • shape_of_type for Slice, Array, Map, Chan, and Pointer recursively called shape_of_type on their element/pointee types, turning []option[int] into Slice(Primitive) (sendable), map[string]option[atomic.int64] into Map(Atomic), and *option[atomic.int64] into Pointer(Atomic) (sendable!).
    • call_shape for new(opt_alias) resolved opt_alias via named_shape to TypeShape::Atomic, typing new(opt_atomic) as *atomic.int64 (Pointer(Atomic), sendable) instead of Pointer(Struct(None)).
    • struct_is_sendable checked field shapes via shape_of_type(&f.ty) without checking whether f.ty resolves to option[...], allowing a struct with an option[int] or option[chan int] field to be shared across task spawn without move (HYB-E3001 bypassed).

Root Cause & Fix

  1. shape.rs (is_option_type_resolved, element_type_shape, expr_is_option, ty_of_expr, multi_value_tys, iterate_var_tys, struct_is_sendable, call_shape):
    • Added Checker::is_option_type_resolved to resolve type aliases in self.types when checking for option[T].
    • Added Checker::element_type_shape so Slice, Array, Map, Chan, Pointer, make(...), and new(...) represent option[T] element/pointee types as non-sendable TypeShape::Struct(None) instead of collapsing to T's shape.
    • Updated struct_is_sendable to reject fields whose resolved TypeExpr is option[T].
    • Extended ty_of_expr, multi_value_shapes, multi_value_tys, iterate_var_tys, and expr_is_option to preserve and inspect TypeExpr across Expr::Index, Expr::Deref / Unary(Deref), Expr::Receive, Expr::Field, Expr::Paren, Expr::Move, and comma-ok multi-value forms.
  2. lib.rs, closures.rs, emit_guard.rs, loans.rs:
    • Passed element TypeExprs in SelectRecv and ForKind::Iterate bindings across lib.rs, closures.rs, and emit_guard.rs.
    • Preserved existing option[...] TypeExpr when reassigning opt = none in loans::rebind_assign_targets.

Verification

  • Added regression tests in compiler/crates/hy-check/tests/recv_operand_soundness.rs.
  • Full compiler workspace test suite (cargo test -p hy-check -p hy-emit -p hy-syntax -p hy-diag) passes with 0 failures.

@limitlessprojects limitlessprojects added triage:duplicate Overlapping change-set with another open PR triage:new New arrival (<= 3 days old), awaiting recon triage:tests Includes test/corpus coverage labels Oct 19, 2026
@github-actions github-actions Bot added the bot:stale-base Shepherd: behind main beyond threshold label Oct 20, 2026
@github-actions

Copy link
Copy Markdown

Shepherd (Phase A — propose only): this PR's changed-file set overlaps >=0.6 Jaccard with #106, which carries the larger take. Proposing close as superseded.

This is a proposal, not a close — nothing auto-closes while this comment is fresh. If this PR has unique content (a different rule, extra tests, a different fix), say so or push, and the label comes off. Cluster signal is a file-set heuristic; the verdict stays human.

@github-actions github-actions Bot added the bot:dup-of:#106 Shepherd W4: superseded by cluster survivor label Oct 20, 2026
@aazminu
aazminu force-pushed the fix/option-shape-erasure-753 branch from 536d5c5 to 1fa49e8 Compare October 24, 2026 06:34
@limitlessprojects

Copy link
Copy Markdown
Owner

Queue-reset sweep: this repo now runs a rolling conveyor, and open PRs
outside the active review window are being closed as not planned to keep the
queue honest — a stale queue wastes every contributor's time, including yours.

This is not a verdict on the work. If this change still matters:

  1. rebase against current main (main has landed a lot since this PR opened),
  2. confirm the diff still applies and the affected verdicts still hold,
  3. reopen — the triage board picks reopened PRs up as fresh arrivals.

Wildcard holds (verdict window 2026-11-01) and Pareto land candidates are
unaffected. Thanks for the effort — high-volume repos only stay reviewable
with a queue that actually drains.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:dup-of:#106 Shepherd W4: superseded by cluster survivor bot:stale-base Shepherd: behind main beyond threshold triage:duplicate Overlapping change-set with another open PR triage:new New arrival (<= 3 days old), awaiting recon triage:tests Includes test/corpus coverage

Projects

None yet

2 participants