Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions compiler/crates/hy-check/src/const_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -230,6 +258,7 @@ pub(crate) fn is_conversion_type_name(name: &str) -> bool {
| "string"
| "byte"
| "rune"
| "char"
| "int"
| "int8"
| "int16"
Expand All @@ -243,6 +272,8 @@ pub(crate) fn is_conversion_type_name(name: &str) -> bool {
| "uintptr"
| "float32"
| "float64"
| "f32"
| "f64"
| "i8"
| "i16"
| "i32"
Expand Down
130 changes: 130 additions & 0 deletions compiler/crates/hy-check/tests/const_init_soundness.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
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");
}
Loading