Skip to content

[buildable] hy-check(emit_guard): local binding shadowing package qualifier in qualified type not rejected (HYB-E1002, Row 13) #4004

Open

Description

@bradwue313

Summary

Under GUARANTEES.md Row 13 (BUILDABLE), every source file accepted by hyb check must emit Go code that passes go build.

In compiler/crates/hy-check/src/emit_guard.rs, TypeExpr::Named checks whether a named type is shadowed by a local binding or parameter in the current environment:
ust TypeExpr::Named { name, span } => { if let Some(st) = st { for tok in name.split(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '.') { if !tok.is_empty() && st.env_lookup(tok).is_some() { // emit HYB-E1002 } } } }

Because the split predicate explicitly excludes '.' (c != '.'), any qualified type name such as sync.Mutex, os.File, or net.IP does NOT split at '.'. As a result, tok remains the entire string (e.g. "sync.Mutex"). Because locals and parameters cannot contain '.', st.env_lookup("sync.Mutex") is always None.

Consequently, when a local variable or parameter shadows an imported package name in a qualified type:
`hy
import "sync"

fn main() {
sync := 1
var m sync.Mutex
_ = sync
_ = m
}
hyb check` accepts the file with 0 diagnostics (Verdict::Accept, exit 0).

The emitter reproduces the declaration verbatim into Go:
`go
package main

import "sync"

func main() {
sync := 1
var m sync.Mutex
_ = sync
_ = m
}
Andgo buildfails compilation: ext
sync (variable) is not a package
`
violating Row 13 (BUILDABLE).

Fix & Verification

  • In compiler/crates/hy-check/src/emit_guard.rs:
    • Check whether the qualified head of TypeExpr::Named has a package qualifier (head.split_once('.')).
    • If the package qualifier matches an in-scope local or parameter in st.env_lookup, emit HYB-E1002 explaining that Go rejects {pkg} (variable) is not a package.
    • Check bare type names and generic type arguments inside [...] against st.env_lookup as before.
  • Added regression test suite compiler/crates/hy-check/tests/shadowed_qualified_type_package.rs verifying:
    • Local variables shadowing package names (sync := 1; var m sync.Mutex) reject with HYB-E1002.
    • Function parameters shadowing package names (fn f(os int) { var f os.File }) reject with HYB-E1002.
    • Shadowed generic type arguments reject with HYB-E1002.
    • Locals matching member names without shadowing the package (mutex := 1; var m sync.Mutex) accept cleanly.

Settlement Coordinates:

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

Activity

  1. dukenordj-rgb commented on Nov 7, 2026

    @dukenordj-rgb
    Contributor

    Root Cause Analysis

    The bug stems from how TypeExpr::Named handles qualified type identifiers during environment validation in compiler/crates/hy-check/src/emit_guard.rs.

    When checking named type expressions for local variable or parameter shadowing, the compiler tokenizes the type string using:

    for tok in name.split(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '.') {
        // ...
        st.env_lookup(tok)
    }

    Because the delimiter closure explicitly preserves '.', qualified types such as sync.Mutex are processed as a single monolithic token ("sync.Mutex"). Local variables and parameters cannot contain periods, so st.env_lookup("sync.Mutex") always resolves to None.

    As a result, when a local binding shadows a package name (e.g., sync := 1), hyb check fails to catch the conflict. It emits the code verbatim into the Go output, causing go build to fail with sync (variable) is not a package, violating the BUILDABLE guarantee (Row 13).

    Patch Implementation

    To fix this, the validation logic must inspect the package qualifier (the segment preceding the first '.') independently of the full type string or type parameters.

    // compiler/crates/hy-check/src/emit_guard.rs
    
    TypeExpr::Named { name, span } => {
        if let Some(st) = st {
            // Check if the qualified package prefix is shadowed by a local or parameter
            if let Some((pkg_qualifier, _)) = name.split_once('.') {
                if !pkg_qualifier.is_empty() && st.env_lookup(pkg_qualifier).is_some() {
                    // Emit HYB-E1002: local binding shadows package qualifier in qualified type
                    ctx.emit_error(HYB_E1002, *span, format!("local binding `{}` shadows package name", pkg_qualifier));
                }
            }
    
            // Check remaining identifiers (e.g., generic type arguments or bare names)
            for tok in name.split(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '.') {
                if !tok.is_empty() && st.env_lookup(tok).is_some() {
                    ctx.emit_error(HYB_E1002, *span, format!("local binding `{}` shadows type identifier", tok));
                }
            }
        }
    }

    Regression Test Coverage

    Add compiler/crates/hy-check/tests/shadowed_qualified_type_package.rs to enforce compile-time rejection of shadowed package qualifiers:

    #[test]
    fn test_local_shadows_package_qualified_type() {
        let source = r#"
            import "sync"
    
            fn main() {
                sync := 1
                var m sync.Mutex
                _ = sync
                _ = m
            }
        "#;
        let diagnostics = hy_check::check_str(source);
        assert_eq!(diagnostics.iter().filter(|d| d.code == "HYB-E1002").count(), 1);
    }
    
    #[test]
    fn test_parameter_shadows_package_qualified_type() {
        let source = r#"
            import "os"
    
            fn f(os: int) {
                var file os.File
                _ = os
                _ = file
            }
        "#;
        let diagnostics = hy_check::check_str(source);
        assert_eq!(diagnostics.iter().filter(|d| d.code == "HYB-E1002").count(), 1);
    }
    
    #[test]
    fn test_unshadowed_member_name_accepts() {
        let source = r#"
            import "sync"
    
            fn main() {
                mutex := 1
                var m sync.Mutex
                _ = mutex
                _ = m
            }
        "#;
        let diagnostics = hy_check::check_str(source);
        assert!(diagnostics.is_empty());
    }
  2. dukenordj-rgb commented on Nov 7, 2026

    @dukenordj-rgb
    Contributor

    Root Cause Analysis

    The core soundness violation stems from how compiler/crates/hy-check/src/emit_guard.rs processes TypeExpr::Named against local symbol environments (st).

    When analyzing a named type expression, the emitter splits the type name string on non-alphanumeric, non-underscore characters, explicitly excluding '.' (c != '.'). For qualified type paths like sync.Mutex, the split operation treats the entire string as a single token ("sync.Mutex"). Because local bindings and function parameters cannot contain dots, environment lookups (st.env_lookup("sync.Mutex")) always return None.

    Consequently, when a local variable or parameter shadows an imported package name (e.g., sync := 1), hyb check fails to detect the collision, emits the unconstrained Go code verbatim, and causes downstream compilation failure (sync (variable) is not a package), violating the GUARANTEES.md BUILDABLE invariant (Row 13).

    Code Solution & Patch

    To fix this, we must inspect the package qualifier prefix (the head before the first '.') of qualified type expressions, and check if that head targets a local symbol table entry rather than an imported package namespace.

    // compiler/crates/hy-check/src/emit_guard.rs
    
    TypeExpr::Named { name, span } => {
        if let Some(st) = st {
            // Check for qualified type package shadowing (e.g., sync.Mutex where sync is a local)
            if let Some((pkg_qualifier, _)) = name.split_once('.') {
                if !pkg_qualifier.is_empty() && st.env_lookup(pkg_qualifier).is_some() {
                    // Emit HYB-E1002: local binding shadows package qualifier
                    emit_diagnostic(
                        span,
                        "HYB-E1002",
                        &format!("local binding '{}' shadows package qualifier in type '{}'", pkg_qualifier, name),
                    );
                }
            }
    
            // Existing token-based checks for bare types and generic arguments
            for tok in name.split(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '.') {
                if !tok.is_empty() && st.env_lookup(tok).is_some() {
                    // emit HYB-E1002
                }
            }
        }
    }

    Regression Test Coverage

    Add compiler/crates/hy-check/tests/shadowed_qualified_type_package.rs to validate the diagnostics pipeline:

    #[test]
    fn test_local_shadows_package_qualified_type() {
        let source = r#"
            import "sync"
    
            fn main() {
                sync := 1
                var m sync.Mutex
                _ = sync
                _ = m
            }
        "#;
        let result = hy_check::check(source);
        assert!(result.has_diagnostic("HYB-E1002"));
    }
    
    #[test]
    fn test_parameter_shadows_package_qualified_type() {
        let source = r#"
            import "os"
    
            fn f(os int) {
                var file os.File
                _ = os
                _ = file
            }
        "#;
        let result = hy_check::check(source);
        assert!(result.has_diagnostic("HYB-E1002"));
    }
    
    #[test]
    fn test_non_shadowing_member_access_accepted() {
        let source = r#"
            import "sync"
    
            fn main() {
                mutex := 1
                var m sync.Mutex
                _ = mutex
                _ = m
            }
        "#;
        let result = hy_check::check(source);
        assert_eq!(result.verdict(), Verdict::Accept);
    }
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