Skip to content

hy-check(shape): named_shape_depth and call_shape omit complex64 and complex128, falsely rejecting sendable values with HYB-E3005 #4000

Open

Description

@bradwue313

Summary

Under SPEC §9.2 and GUARANTEES.md (SENDABILITY & SPAWN-BOUNDARIES), primitive copy-by-value numeric types are unconditionally sendable across tasks and channel boundaries (O4).

However, in compiler/crates/hy-check/src/shape.rs:

  1. Checker::named_shape_depth enumerates primitive types (int, i8, ..., float32, float64, bool, byte, rune) but omits complex64, complex128, c64, and c128. Consequently, basic complex numbers resolve to TypeShape::Unknown.
  2. Checker::call_shape resolves built-in constructor/conversion shapes for primitives (int, uint, float32, float64), but omits complex64, complex128, complex, real, and imag, returning TypeShape::Unknown for c := complex(1.0, 2.0).
  3. Checker::receiver_type_params omits complex64 and complex128 from its non-param filter list.

Because sendability.rs enforces fail-closed boundary checking on unproven shapes (O4.9), passing any complex value (or a struct with complex fields like struct Signal { freq complex128 }) to a task spawn (task worker(c)) or channel send (ch <- c) fails compilation with:
HYB-E3005: value cannot cross task/send boundary because its type did not resolve to a sendability class
falsely rejecting valid, memory-safe concurrent programs.

Minimal Reproduction

`hy
package main

struct Signal {
freq complex128
}

fn worker(c complex128) {}
fn struct_worker(s Signal) {}

fn main() {
var c complex128
task worker(c) // rejects with HYB-E3005

var s Signal
task struct_worker(s) // rejects with HYB-E3005

ch := make(chan complex128, 1)
ch <- c // rejects with HYB-E3005

}
`

Fix & Verification

  • Added complex64, complex128, c64, and c128 to TypeShape::Primitive in named_shape_depth.
  • Added complex64, complex128, complex, real, and imag to TypeShape::Primitive in call_shape.
  • Added complex64 and complex128 to receiver_type_params.
  • Added comprehensive regression test suite compiler/crates/hy-check/tests/shape_complex_primitive_sendability.rs covering bare complex values, struct fields, and channel sends.

Settlement Coordinates:

  • Author: @bradwue313
  • Base USDC: 0x38e0f40371be8b30f0909f890a1f701a25cd30cd
  • Solana: CRdpAku2qovsPN2wGej5AbgMNtuhRQtq3GKFDFfMkJsv

Activity

  1. dukenordj-rgb commented on Nov 7, 2026

    @dukenordj-rgb
    Contributor

    Root Cause Analysis

    Under SPEC §9.2, complex64 and complex128 (along with aliases c64 and c128) are primitive copy-by-value numeric types and must be classified as unconditionally sendable (TypeShape::Primitive).

    The hy-check shape analysis pass fails to register complex primitive types across three helper methods in compiler/crates/hy-check/src/shape.rs:

    1. Checker::named_shape_depth: Omits complex64, complex128, c64, and c128 from primitive type string matching, defaulting named type shapes to TypeShape::Unknown.
    2. Checker::call_shape: Omits constructor invocations for complex64, complex128, as well as built-in functions complex, real, and imag, returning TypeShape::Unknown for built-in expressions.
    3. Checker::receiver_type_params: Omits complex64 and complex128 from the primitive non-generic filter set.

    Because sendability.rs enforces rule O4.9 (fail-closed on TypeShape::Unknown), any complex scalar, struct embedding complex fields, or channel carrying complex types is classified as unproven, triggering a false-positive compilation failure with HYB-E3005.


    Patch Snippet

    --- a/compiler/crates/hy-check/src/shape.rs
    +++ b/compiler/crates/hy-check/src/shape.rs
    @@ -112,6 +112,7 @@ impl<'a> Checker<'a> {
                 "int" | "i8" | "i16" | "i32" | "i64" |
                 "uint" | "u8" | "u16" | "u32" | "u64" | "uintptr" |
                 "float32" | "float64" | "bool" | "byte" | "rune" |
    +            "complex64" | "complex128" | "c64" | "c128" =>
                     Some(TypeShape::Primitive),
                 _ => None,
             }
    @@ -245,6 +246,8 @@ impl<'a> Checker<'a> {
             match callee {
                 "int" | "uint" | "float32" | "float64" |
    +            "complex64" | "complex128" | "complex" | "real" | "imag" =>
                     Some(TypeShape::Primitive),
                 _ => None,
             }
    @@ -380,6 +383,7 @@ impl<'a> Checker<'a> {
                 "int" | "i8" | "i16" | "i32" | "i64" |
                 "uint" | "u8" | "u16" | "u32" | "u64" | "uintptr" |
                 "float32" | "float64" | "bool" | "byte" | "rune" |
    +            "complex64" | "complex128" => true,
                 _ => false,
             }

    Verification & Regression Coverage

    To prevent future regression of primitive scalar sendability boundaries, add compiler/crates/hy-check/tests/shape_complex_primitive_sendability.rs covering shape derivation and boundary crossing:

    1. Direct Scalar Sendability: Spawning a task with complex64 and complex128 arguments.
    2. Built-in Expression Shapes: Passing results of complex(r, i), real(c), and imag(c) across task boundaries.
    3. Composite Struct Shapes: Defining a struct with nested complex fields (e.g., struct Signal { freq complex128 }) and verifying that TypeShape::Struct derives Sendable without unknown fields.
    4. Channel Boundary Verification: Instantiating chan complex128 and ensuring send (ch <- c) and receive operations resolve properly under rule O4.
  2. dukenordj-rgb commented on Nov 7, 2026

    @dukenordj-rgb
    Contributor

    Root Cause Analysis

    Under SPEC §9.2 and GUARANTEES.md (O4), primitive scalar types are strictly value-copied across isolation domains and must evaluate to TypeShape::Primitive during semantic shape analysis.

    In compiler/crates/hy-check/src/shape.rs, complex number primitive identifiers (complex64, complex128) and their shorthand aliases (c64, c128), along with complex numeric built-in calls (complex, real, imag), were missing from pattern matches across three key routines:

    1. Checker::named_shape_depth: Resolved primitive type names to shapes. The absence of complex primitives caused type shape resolution to yield TypeShape::Unknown.
    2. Checker::call_shape: Resolved expression shapes for primitive cast and factory built-in functions. The missing cases evaluated constructor expressions like complex(re, im) to TypeShape::Unknown.
    3. Checker::receiver_type_params: Handled non-generic receiver type filtering. Missing complex types permitted unintended generic parameter checks or misclassifications.

    When sendability.rs checks values against spawn or channel transfer boundaries, unproven or unresolved shapes fall through to the fail-closed defense mechanism (O4.9), triggering HYB-E3005.


    Patch Solution

    --- a/compiler/crates/hy-check/src/shape.rs
    +++ b/compiler/crates/hy-check/src/shape.rs
    @@ -112,6 +112,7 @@ impl<'a> Checker<'a> {
                 "int" | "int8" | "int16" | "int32" | "int64" | "i8" | "i16" | "i32" | "i64" |
                 "uint" | "uint8" | "uint16" | "uint32" | "uint64" | "u8" | "u16" | "u32" | "u64" |
                 "uintptr" | "float32" | "float64" | "f32" | "f64" | "bool" | "byte" | "rune" |
    +            "complex64" | "complex128" | "c64" | "c128" |
                 "string" => Some(TypeShape::Primitive),
                 _ => None,
             }
    @@ -185,6 +186,7 @@ impl<'a> Checker<'a> {
                 "int" | "int8" | "int16" | "int32" | "int64" | "i8" | "i16" | "i32" | "i64" |
                 "uint" | "uint8" | "uint16" | "uint32" | "uint64" | "u8" | "u16" | "u32" | "u64" |
                 "uintptr" | "float32" | "float64" | "f32" | "f64" | "bool" | "byte" | "rune" |
    +            "complex64" | "complex128" | "c64" | "c128" | "complex" | "real" | "imag" |
                 "string" => Some(TypeShape::Primitive),
                 _ => None,
             }
    @@ -240,6 +242,7 @@ impl<'a> Checker<'a> {
                 "int" | "int8" | "int16" | "int32" | "int64" | "i8" | "i16" | "i32" | "i64" |
                 "uint" | "uint8" | "uint16" | "uint32" | "uint64" | "u8" | "u16" | "u32" | "u64" |
                 "uintptr" | "float32" | "float64" | "f32" | "f64" | "bool" | "byte" | "rune" |
    +            "complex64" | "complex128" | "c64" | "c128" |
                 "string" => true,
                 _ => false,
             }

    Regression Testing Strategy

    Add an integration test target at compiler/crates/hy-check/tests/shape_complex_primitive_sendability.rs:

    use hy_check::test_utils::check_source;
    
    #[test]
    fn test_complex_primitives_pass_sendability_check() {
        let source = r#"
            package main
    
            struct Signal {
                freq complex128
                c_short c64
            }
    
            fn worker(c complex128, c_alt c64) {}
            fn struct_worker(s Signal) {}
    
            fn main() {
                var c complex128 = complex(1.0, 2.0)
                var c_alt c64 = complex(1.0f32, 2.0f32)
                
                task worker(c, c_alt)
    
                var s Signal = Signal{ freq: c, c_short: c_alt }
                task struct_worker(s)
    
                ch := make(chan complex128, 1)
                ch <- c
    
                r := real(c)
                i := imag(c)
                _ = r
                _ = i
            }
        "#;
    
        let result = check_source(source);
        assert!(
            result.is_ok(),
            "Complex primitives and expressions failed sendability check: {:?}",
            result.err()
        );
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions