From 0a99a6e6aaa8224ec8ee91b5f3db98f009d784c6 Mon Sep 17 00:00:00 2001 From: sonson0910 Date: Sun, 18 Oct 2026 22:12:02 +0700 Subject: [PATCH] fix(hy-check): reject nil consts and support f32/f64/char and string aliases in const_init.rs --- compiler/crates/hy-check/src/const_init.rs | 45 +++++- .../hy-check/tests/const_init_soundness.rs | 130 ++++++++++++++++++ 2 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 compiler/crates/hy-check/tests/const_init_soundness.rs diff --git a/compiler/crates/hy-check/src/const_init.rs b/compiler/crates/hy-check/src/const_init.rs index 9c6ca9de..28c97fe8 100644 --- a/compiler/crates/hy-check/src/const_init.rs +++ b/compiler/crates/hy-check/src/const_init.rs @@ -88,13 +88,16 @@ impl<'a> Checker<'a> { if st.is_some_and(|s| s.env_lookup(ty_name).is_some()) { return false; } - if self.fns.contains_key(ty_name) { + if self.fns.contains_key(ty_name) || self.structs.contains_key(ty_name) { return false; } if args.len() != 1 || args[0].spread { return false; } if ty_name == "len" { + if self.type_aliases.contains_key("len") { + return false; + } return self.expr_is_constant_inner(&args[0].value, st, visiting, ref_depth) && self.is_const_string_expr(&args[0].value, st, ref_depth); } @@ -111,16 +114,33 @@ impl<'a> Checker<'a> { } fn is_const_conversion_target(&self, name: &str) -> bool { - if is_conversion_type_name(name) { - return true; - } if let Some(ty) = self.type_aliases.get(name) { let resolved = self.resolve_type_alias(ty); if let hy_syntax::TypeExpr::Named { name: target, .. } = resolved { - return is_conversion_type_name(&target); + return !self.structs.contains_key(target.as_str()) + && is_conversion_type_name(&target); } + return false; } - false + is_conversion_type_name(name) + } + + fn is_const_string_conversion_target(&self, name: &str, st: Option<&FnState>) -> bool { + if st.is_some_and(|s| s.env_lookup(name).is_some()) + || self.fns.contains_key(name) + || self.structs.contains_key(name) + { + return false; + } + if let Some(ty) = self.type_aliases.get(name) { + let resolved = self.resolve_type_alias(ty); + return matches!( + resolved, + hy_syntax::TypeExpr::Named { name: target, .. } + if target == "string" && !self.structs.contains_key("string") + ); + } + name == "string" } fn is_const_string_expr(&self, e: &Expr, st: Option<&FnState>, ref_depth: usize) -> bool { @@ -148,12 +168,20 @@ impl<'a> Checker<'a> { } } if let Some(cdecl) = self.file_const_decl(name) { + if let Some(ty) = &cdecl.ty { + if !matches!(self.shape_of_type(ty), TypeShape::Str) { + return false; + } + } return self.is_const_string_expr(&cdecl.value, None, ref_depth + 1); } false } Expr::Call { callee, args, .. } => { - conversion_callee_name(callee) == Some("string") + let Some(ty_name) = conversion_callee_name(callee) else { + return false; + }; + self.is_const_string_conversion_target(ty_name, st) && args.len() == 1 && !args[0].spread } @@ -230,6 +258,7 @@ pub(crate) fn is_conversion_type_name(name: &str) -> bool { | "string" | "byte" | "rune" + | "char" | "int" | "int8" | "int16" @@ -243,6 +272,8 @@ pub(crate) fn is_conversion_type_name(name: &str) -> bool { | "uintptr" | "float32" | "float64" + | "f32" + | "f64" | "i8" | "i16" | "i32" diff --git a/compiler/crates/hy-check/tests/const_init_soundness.rs b/compiler/crates/hy-check/tests/const_init_soundness.rs new file mode 100644 index 00000000..e39bf719 --- /dev/null +++ b/compiler/crates/hy-check/tests/const_init_soundness.rs @@ -0,0 +1,130 @@ +use hy_check::{check_file, Verdict}; +use hy_diag::Severity; +use hy_syntax::parse_file; + +fn check(src: &str) -> hy_check::CheckResult { + let ast = parse_file("inline.hy", src) + .unwrap_or_else(|diags| panic!("must parse: {:?}", diags)); + check_file(&ast) +} + +fn error_codes(r: &hy_check::CheckResult) -> Vec { + r.diagnostics + .iter() + .filter(|d| d.severity == Severity::Error) + .map(|d| d.code.clone()) + .collect() +} + +fn assert_has(r: &hy_check::CheckResult, code: &str) { + let cs = error_codes(r); + assert!( + cs.iter().any(|c| c == code), + "expected {}, got {:?}", + code, + cs + ); + assert_eq!(r.verdict, Verdict::Reject); +} + +fn assert_accepts(src: &str) { + let r = check(src); + let errs = error_codes(&r); + assert!( + errs.is_empty(), + "expected clean accept, got {:?}\nsrc: {}", + errs, + src + ); + assert_eq!(r.verdict, Verdict::Accept); +} + +#[test] +fn const_nil_error_is_rejected_with_e6003() { + let r_top = check( + r#"package main + +const e error = nil + +fn main() {} +"#, + ); + assert_has(&r_top, "HYB-E6003"); + + let r_local = check( + r#"package main + +fn main() { + const e error = nil + _ = e +} +"#, + ); + assert_has(&r_local, "HYB-E6003"); +} + +#[test] +fn const_spec_float_and_char_conversions_are_accepted() { + assert_accepts( + r#"package main + +type my_char = char + +const a = f64(42) +const b = f32(1.5) +const c = my_char(65) + +fn main() { + _ = a + _ = b + _ = c +} +"#, + ); +} + +#[test] +fn const_len_on_non_string_typed_file_const_is_rejected_with_e6003() { + let r = check( + r#"package main + +const s any = "hello" +const n = len(s) + +fn main() { + _ = n +} +"#, + ); + assert_has(&r, "HYB-E6003"); +} + +#[test] +fn const_len_string_alias_and_shadowed_string_type_handled() { + assert_accepts( + r#"package main + +type my_str = string + +const n = len(my_str("hello")) + +fn main() { + _ = n +} +"#, + ); + + let r_shadow = check( + r#"package main + +type string = int + +const n = len(string(65)) + +fn main() { + _ = n +} +"#, + ); + assert_has(&r_shadow, "HYB-E6003"); +}