diff --git a/xsc-core/src/static/info/fn_info.rs b/xsc-core/src/static/info/fn_info.rs index a77510f..f56a27e 100644 --- a/xsc-core/src/static/info/fn_info.rs +++ b/xsc-core/src/static/info/fn_info.rs @@ -8,23 +8,36 @@ use crate::r#static::info::src_loc::SrcLoc; #[allow(dead_code)] pub struct FnInfo { pub identifiers: HashMap, + pub active_loop_params: HashMap, pub src_loc: SrcLoc } impl FnInfo { pub fn new(src_loc: SrcLoc) -> Self { - Self { identifiers: HashMap::new(), src_loc } + Self { identifiers: HashMap::new(), active_loop_params: HashMap::new(), src_loc } } pub fn get_mut(&mut self, id: &Identifier) -> Option<&mut IdInfo> { self.identifiers.get_mut(id) } - + pub fn get(&self, id: &Identifier) -> Option<&IdInfo> { self.identifiers.get(id) } - + pub fn set(&mut self, id: Identifier, info: IdInfo) { self.identifiers.insert(id, info); } -} \ No newline at end of file + + pub fn get_active_loop_param(&self, id: &Identifier) -> Option<&SrcLoc> { + self.active_loop_params.get(id) + } + + pub fn set_active_loop_param(&mut self, id: Identifier, src_loc: SrcLoc) { + self.active_loop_params.insert(id, src_loc); + } + + pub fn unset_active_loop_param(&mut self, id: &Identifier) { + self.active_loop_params.remove(id); + } +} diff --git a/xsc-core/src/static/info/type_env.rs b/xsc-core/src/static/info/type_env.rs index 963dcab..2ca1b8b 100644 --- a/xsc-core/src/static/info/type_env.rs +++ b/xsc-core/src/static/info/type_env.rs @@ -7,6 +7,7 @@ use crate::parsing::ast::{Identifier}; use crate::parsing::span::{contains, Span}; use crate::r#static::info::fn_info::FnInfo; use crate::r#static::info::id_info::IdInfo; +use crate::r#static::info::src_loc::SrcLoc; use crate::r#static::info::xs_error::XsError; #[derive(Debug, Clone)] @@ -85,6 +86,12 @@ impl TypeEnv { .or_else(|| self.identifiers.get(id)) .map(|val| val.clone()) } + + pub fn get_active_loop_param(&self, id: &Identifier) -> Option { + self.current_fnv_env.as_ref() + .and_then(|env| env.get_active_loop_param(id)) + .cloned() + } pub fn set(&mut self, id: &Identifier, info: IdInfo) { match &mut self.current_fnv_env { @@ -102,7 +109,7 @@ impl TypeEnv { .and_then(|env| env.get(&Identifier::new("return"))) .map(|val| val.clone()) } - + pub fn add_group(&mut self, group: &String) { self.groups.insert(group.clone()); } @@ -162,6 +169,20 @@ impl TypeEnv { self.current_doc.take() } + pub fn set_active_loop_param(&mut self, id: &Identifier, src_loc: SrcLoc) { + let Some(env) = self.current_fnv_env.as_mut() else { + return; + }; + env.set_active_loop_param(id.clone(), src_loc); + } + + pub fn unset_active_loop_param(&mut self, id: &Identifier) { + let Some(env) = self.current_fnv_env.as_mut() else { + return; + }; + env.unset_active_loop_param(id); + } + pub fn local_ids(&self, path: &PathBuf, span: &Span) -> Option<&HashMap> { self.fn_envs .values() @@ -171,4 +192,4 @@ impl TypeEnv { loc.file_path == *path && contains(&loc.span, span) }).map(|env| &env.identifiers).next() } -} \ No newline at end of file +} diff --git a/xsc-core/src/static/type_check.rs b/xsc-core/src/static/type_check.rs index 47921fc..aa7a77d 100644 --- a/xsc-core/src/static/type_check.rs +++ b/xsc-core/src/static/type_check.rs @@ -2,5 +2,7 @@ mod expression; mod util; mod statement; mod statements; +#[cfg(test)] +mod tests; pub use statements::{xs_tc}; diff --git a/xsc-core/src/static/type_check/statement.rs b/xsc-core/src/static/type_check/statement.rs index c5c0c1a..a779e9b 100644 --- a/xsc-core/src/static/type_check/statement.rs +++ b/xsc-core/src/static/type_check/statement.rs @@ -632,45 +632,108 @@ match stmt { let (AstNode::VarAssign { name: (name, name_span), value }, _span) = var.as_ref() else { unreachable!("XSC Internal Error while type checking For at {}", var.as_ref().1) }; - /* Redefinitions are allowed for for loop variables */ - - // match type_env.get(name) { - // Some(IdInfo { src_loc: og_src_loc, .. }) => { - // type_env.add_err(path, XSError::redefined_name( - // name, - // name_span, - // &og_src_loc, - // None, - // )); - // return Ok(()); - // } - // _ => {} - // }; - - if let Some(value_type) = xs_tc_expr(path, value, type_env) { - type_env.add_errs(path, type_cmp(&Type::Int, &value_type, &value.1, false, false)); - } - - type_env.set(name, IdInfo::new(Type::Int, SrcLoc::from(path, name_span), doc)); - if let Some(type_) = xs_tc_expr(path, condition, type_env) { - if type_ != Type::Bool { - type_env.add_err(path, XsError::type_mismatch( - &type_.to_string(), - "bool", - &condition.1, - None, + let mut can_activate_loop_param = true; + let mut chk_loop_var_usage = true; + let mut loop_var_type = Type::Int; + let is_new_loop_param = match type_env.get(name) { + Some(IdInfo { type_, modifiers, .. }) if modifiers.is_const() => { + type_env.add_err(path, XsError::syntax( + span, + "Cannot re-assign a value to a {0} variable", + vec!["const"], )); + can_activate_loop_param = false; + chk_loop_var_usage = false; + loop_var_type = type_; + false + } + Some(IdInfo { type_, .. }) => { + loop_var_type = type_.clone(); + match type_ { + Type::Int | Type::Float => {} + _ => { + type_env.add_err(path, XsError::type_mismatch( + &type_.to_string(), + "int | float", + name_span, + None, + )); + can_activate_loop_param = false; + chk_loop_var_usage = false; + } + } + false + } + None => { + type_env.set(name, IdInfo::new(Type::Int, SrcLoc::from(path, name_span), doc.clone())); + true + } + }; + + if let Some(og_src_loc) = type_env.get_active_loop_param(name) { + type_env.add_err(path, XsError::redefined_name( + name, + name_span, + &og_src_loc, + Some("Nested loops cannot reuse the same loop parameter"), + )); + can_activate_loop_param = false; + } + + let value_type = xs_tc_expr(path, value, type_env); + if chk_loop_var_usage { + if let Some(value_type) = value_type { + let expected_type = if is_new_loop_param { &Type::Int } else { &loop_var_type }; + type_env.add_errs(path, type_cmp(expected_type, &value_type, &value.1, false, false)); } } - combine_results(body.0.iter() + if chk_loop_var_usage { + if let Some(type_) = xs_tc_expr(path, condition, type_env) { + if type_ != Type::Bool { + type_env.add_err(path, XsError::type_mismatch( + &type_.to_string(), + "bool", + &condition.1, + None, + )); + } + } + } else { + let Some(spanned_expr) = (match &condition.0 { + Expr::Lt(_lhs, rhs) | Expr::Le(_lhs, rhs) | Expr::Gt(_lhs, rhs) | Expr::Ge(_lhs, rhs) => { + Some(rhs.as_ref()) + } + _ => None, + }) else { + unreachable!("XSC Internal Error while type checking For condition at {}", condition.1); + }; + xs_tc_expr(path, spanned_expr, type_env); + } + + if can_activate_loop_param { + let loop_src_loc = if is_new_loop_param { + SrcLoc::from(path, name_span) + } else { + SrcLoc::from(path, span) + }; + type_env.set_active_loop_param(name, loop_src_loc); + } + + let result = combine_results(body.0.iter() .map(|spanned_stmt| { xs_tc_stmt( path, spanned_stmt, type_env, ast_cache, src_cache, comments, comment_pos, false, true, true, ) }) - ) + ); + + if can_activate_loop_param { + type_env.unset_active_loop_param(name); + } + + result }, AstNode::Switch { clause, cases } => { if is_top_level { diff --git a/xsc-core/src/static/type_check/tests.rs b/xsc-core/src/static/type_check/tests.rs new file mode 100644 index 0000000..3b901c2 --- /dev/null +++ b/xsc-core/src/static/type_check/tests.rs @@ -0,0 +1,252 @@ +use std::path::PathBuf; + +use crate::r#static::info::{gen_errs_from_src, AstMap, SrcCache, TypeEnv, XsError}; + +fn chk(src: &str) -> Vec { + let path = PathBuf::from("loop_param_test.xs"); + let mut type_env = TypeEnv::new(vec![]); + let mut ast_cache = AstMap::new(); + let src_cache = SrcCache::new(); + + let result = gen_errs_from_src(&path, src, &mut type_env, &mut ast_cache, &src_cache); + assert!(result.is_ok(), "unexpected parse errors: {:?}", result.err()); + + type_env.errs.remove(&path).unwrap_or_default() +} + +fn assert_const_loop_error(errs: Vec) { + assert_eq!(errs.len(), 1, "unexpected errors: {errs:?}"); + assert!(matches!( + &errs[0], + XsError::Syntax { msg, keywords, .. } + if msg == "Cannot re-assign a value to a {0} variable" + && keywords == &vec!["const".to_string()] + )); +} + +fn assert_non_numeric_loop_error(errs: Vec, actual: &str) { + assert_eq!(errs.len(), 1, "unexpected errors: {errs:?}"); + assert!(matches!( + &errs[0], + XsError::TypeMismatch { actual: err_actual, expected, .. } + if err_actual == actual && expected == "int | float" + )); +} + +fn assert_redefined_name_error(errs: Vec, name: &str, note: Option<&str>) { + assert_eq!(errs.len(), 1, "unexpected errors: {errs:?}"); + assert!(matches!( + &errs[0], + XsError::RedefinedName { name: err_name, note: err_note, .. } + if err_name == name && err_note.as_deref() == note + )); +} + +#[test] +fn allows_reusing_predeclared_loop_param_across_loops() { + let errs = chk(r#" + void main() { + int i = 1; + for (i = 0; < 2) {} + for (i = 0; < 2) {} + } + "#); + + assert!(errs.is_empty(), "unexpected errors: {errs:?}"); +} + +#[test] +fn rejects_nested_reuse_of_the_same_loop_param() { + let errs = chk(r#" + void main() { + for (i = 0; < 2) { + for (i = 0; < 2) {} + } + } + "#); + + assert_redefined_name_error(errs, "i", Some("Nested loops cannot reuse the same loop parameter")); +} + +#[test] +fn rejects_defining_a_loop_param_after_the_loop() { + let errs = chk(r#" + void main() { + for (i = 0; < 2) {} + int i = 1; + } + "#); + + assert_redefined_name_error(errs, "i", None); +} + +#[test] +fn allows_reusing_an_implicit_loop_param_in_later_loops() { + let errs = chk(r#" + void main() { + for (i = 0; < 2) {} + for (i = 0; < 2) {} + } + "#); + + assert!(errs.is_empty(), "unexpected errors: {errs:?}"); +} + +#[test] +fn rejects_nested_reuse_of_the_same_loop_param_through_non_loop_nesting() { + let errs = chk(r#" + void main() { + for (i = 0; < 2) { + if (true) { + while (true) { + for (i = 0; < 2) {} + } + } + } + } + "#); + + assert_redefined_name_error(errs, "i", Some("Nested loops cannot reuse the same loop parameter")); +} + +#[test] +fn rejects_const_local_loop_param() { + let errs = chk(r#" + void main() { + const int i = 0; + for (i = 0; < 2) {} + } + "#); + + assert_const_loop_error(errs); +} + +#[test] +fn allows_global_int_loop_param_inside_function() { + let errs = chk(r#" + int i = 0; + void foo() { + for (i = 0; < 2) {} + } + "#); + + assert!(errs.is_empty(), "unexpected errors: {errs:?}"); +} + +#[test] +fn rejects_global_const_loop_param_inside_function() { + let errs = chk(r#" + const int i = 0; + void foo() { + for (i = 0; < 2) {} + } + "#); + + assert_const_loop_error(errs); +} + +#[test] +fn allows_global_static_loop_param_inside_function() { + let errs = chk(r#" + static int i = 0; + void foo() { + for (i = 0; < 2) {} + } + "#); + + assert!(errs.is_empty(), "unexpected errors: {errs:?}"); +} + +#[test] +fn allows_local_static_loop_param_inside_function() { + let errs = chk(r#" + void foo() { + static int i = 0; + for (i = 0; < 2) {} + } + "#); + + assert!(errs.is_empty(), "unexpected errors: {errs:?}"); +} + +#[test] +fn allows_function_parameter_loop_param() { + let errs = chk(r#" + void foo(int i = 0) { + for (i = 0; < 2) {} + } + "#); + + assert!(errs.is_empty(), "unexpected errors: {errs:?}"); +} + +#[test] +fn allows_float_local_loop_param() { + let errs = chk(r#" + void main() { + float i = 0; + for (i = 0; < 2) {} + } + "#); + + assert!(errs.is_empty(), "unexpected errors: {errs:?}"); +} + +#[test] +fn allows_float_global_loop_param_inside_function() { + let errs = chk(r#" + float i = 0; + void foo() { + for (i = 0; < 2) {} + } + "#); + + assert!(errs.is_empty(), "unexpected errors: {errs:?}"); +} + +#[test] +fn allows_float_function_parameter_loop_param() { + let errs = chk(r#" + void foo(float i = 0) { + for (i = 0; < 2) {} + } + "#); + + assert!(errs.is_empty(), "unexpected errors: {errs:?}"); +} + +#[test] +fn rejects_bool_loop_param() { + let errs = chk(r#" + void main() { + bool i = true; + for (i = 0; < 2) {} + } + "#); + + assert_non_numeric_loop_error(errs, "bool"); +} + +#[test] +fn rejects_string_loop_param() { + let errs = chk(r#" + void main() { + string i = ""; + for (i = 0; < 2) {} + } + "#); + + assert_non_numeric_loop_error(errs, "string"); +} + +#[test] +fn rejects_vector_loop_param() { + let errs = chk(r#" + void main() { + vector i = vector(0, 0, 0); + for (i = 0; < 2) {} + } + "#); + + assert_non_numeric_loop_error(errs, "vector"); +}