From 19c3e35c6836a75e931761386616557e63e44c30 Mon Sep 17 00:00:00 2001 From: Phoenix Himself Date: Mon, 16 Dec 2024 01:09:48 +0100 Subject: [PATCH] Constant variables are usable now (#630) * fix: constant variables are usable now * feat: add function checking for variable existance in previous scope * feat: add tests and resolve issues * feat: test constant erroring * fix: clippy linting * fix: function naming * fix: wrong files * refactor: break down is_shadowing_prev_scope to separate lines * refactor: test_amber --- src/modules/shorthand/add.rs | 5 +- src/modules/shorthand/div.rs | 5 +- src/modules/shorthand/modulo.rs | 5 +- src/modules/shorthand/mul.rs | 5 +- src/modules/shorthand/sub.rs | 5 +- src/modules/statement/stmt.rs | 4 +- src/modules/variable/constinit.rs | 73 ---------------- src/modules/variable/get.rs | 2 +- src/modules/variable/init.rs | 21 +++-- src/modules/variable/mod.rs | 15 +++- src/modules/variable/set.rs | 9 +- src/tests/erroring.rs | 9 ++ src/tests/erroring/array_index_get_by_text.ab | 5 ++ .../erroring/array_index_set_by_range.ab | 5 ++ src/tests/erroring/array_index_set_by_text.ab | 5 ++ ..._failable_with_typed_nonfailable_return.ab | 7 ++ ..._nonfailable_with_typed_failable_return.ab | 7 ++ .../function_with_failable_typed_arg.ab | 7 ++ .../function_with_wrong_typed_return.ab | 7 ++ src/tests/erroring/variable_constant.ab | 5 ++ .../variable_constant_shorthand_add.ab | 5 ++ .../variable_constant_shorthand_div.ab | 5 ++ .../variable_constant_shorthand_mod.ab | 5 ++ .../variable_constant_shorthand_mul.ab | 5 ++ .../variable_constant_shorthand_sub.ab | 5 ++ src/tests/errors.rs | 86 ------------------- src/tests/extra.rs | 3 +- src/tests/mod.rs | 54 ++++++++---- src/tests/stdlib.rs | 3 +- src/tests/validity.rs | 3 +- src/tests/validity/constant.ab | 15 ---- .../validity/import_with_trailing_comma.ab | 4 +- .../validity/variable_constant_condition.ab | 28 ++++++ .../validity/variable_constant_function.ab | 32 +++++++ src/tests/validity/variable_constant_loop.ab | 18 ++++ src/utils/metadata/parser.rs | 14 ++- 36 files changed, 265 insertions(+), 226 deletions(-) delete mode 100644 src/modules/variable/constinit.rs create mode 100644 src/tests/erroring.rs create mode 100644 src/tests/erroring/array_index_get_by_text.ab create mode 100644 src/tests/erroring/array_index_set_by_range.ab create mode 100644 src/tests/erroring/array_index_set_by_text.ab create mode 100644 src/tests/erroring/function_failable_with_typed_nonfailable_return.ab create mode 100644 src/tests/erroring/function_nonfailable_with_typed_failable_return.ab create mode 100644 src/tests/erroring/function_with_failable_typed_arg.ab create mode 100644 src/tests/erroring/function_with_wrong_typed_return.ab create mode 100644 src/tests/erroring/variable_constant.ab create mode 100644 src/tests/erroring/variable_constant_shorthand_add.ab create mode 100644 src/tests/erroring/variable_constant_shorthand_div.ab create mode 100644 src/tests/erroring/variable_constant_shorthand_mod.ab create mode 100644 src/tests/erroring/variable_constant_shorthand_mul.ab create mode 100644 src/tests/erroring/variable_constant_shorthand_sub.ab delete mode 100644 src/tests/errors.rs delete mode 100644 src/tests/validity/constant.ab create mode 100644 src/tests/validity/variable_constant_condition.ab create mode 100644 src/tests/validity/variable_constant_function.ab create mode 100644 src/tests/validity/variable_constant_loop.ab diff --git a/src/modules/shorthand/add.rs b/src/modules/shorthand/add.rs index 0afeb2ea..d886218b 100644 --- a/src/modules/shorthand/add.rs +++ b/src/modules/shorthand/add.rs @@ -2,7 +2,7 @@ use heraclitus_compiler::prelude::*; use crate::docs::module::DocumentationModule; use crate::error_type_match; use crate::modules::expression::expr::Expr; -use crate::modules::variable::{variable_name_extensions, handle_variable_reference}; +use crate::modules::variable::{handle_variable_reference, prevent_constant_mutation, variable_name_extensions}; use crate::translate::compute::translate_computation_eval; use crate::utils::{ParserMetadata, TranslateMetadata}; use crate::translate::{module::TranslateModule, compute::{ArithOp, translate_computation}}; @@ -34,7 +34,8 @@ impl SyntaxModule for ShorthandAdd { let var_tok = meta.get_current_token(); self.var = variable(meta, variable_name_extensions())?; token(meta, "+=")?; - let variable = handle_variable_reference(meta, var_tok, &self.var)?; + let variable = handle_variable_reference(meta, &var_tok, &self.var)?; + prevent_constant_mutation(meta, &var_tok, &self.var, variable.is_const)?; self.kind = variable.kind; self.global_id = variable.global_id; self.is_ref = variable.is_ref; diff --git a/src/modules/shorthand/div.rs b/src/modules/shorthand/div.rs index 79a68d9b..e4f4c0b2 100644 --- a/src/modules/shorthand/div.rs +++ b/src/modules/shorthand/div.rs @@ -2,7 +2,7 @@ use heraclitus_compiler::prelude::*; use crate::docs::module::DocumentationModule; use crate::error_type_match; use crate::modules::expression::expr::Expr; -use crate::modules::variable::{variable_name_extensions, handle_variable_reference}; +use crate::modules::variable::{handle_variable_reference, prevent_constant_mutation, variable_name_extensions}; use crate::translate::compute::translate_computation_eval; use crate::utils::{ParserMetadata, TranslateMetadata}; use crate::translate::{module::TranslateModule, compute::{ArithOp, translate_computation}}; @@ -34,7 +34,8 @@ impl SyntaxModule for ShorthandDiv { let var_tok = meta.get_current_token(); self.var = variable(meta, variable_name_extensions())?; token(meta, "/=")?; - let variable = handle_variable_reference(meta, var_tok, &self.var)?; + let variable = handle_variable_reference(meta, &var_tok, &self.var)?; + prevent_constant_mutation(meta, &var_tok, &self.var, variable.is_const)?; self.kind = variable.kind; self.global_id = variable.global_id; self.is_ref = variable.is_ref; diff --git a/src/modules/shorthand/modulo.rs b/src/modules/shorthand/modulo.rs index f9f92ff9..16bd27d8 100644 --- a/src/modules/shorthand/modulo.rs +++ b/src/modules/shorthand/modulo.rs @@ -2,7 +2,7 @@ use heraclitus_compiler::prelude::*; use crate::docs::module::DocumentationModule; use crate::error_type_match; use crate::modules::expression::expr::Expr; -use crate::modules::variable::{variable_name_extensions, handle_variable_reference}; +use crate::modules::variable::{handle_variable_reference, prevent_constant_mutation, variable_name_extensions}; use crate::translate::compute::translate_computation_eval; use crate::utils::{ParserMetadata, TranslateMetadata}; use crate::translate::{module::TranslateModule, compute::{ArithOp, translate_computation}}; @@ -34,7 +34,8 @@ impl SyntaxModule for ShorthandModulo { let var_tok = meta.get_current_token(); self.var = variable(meta, variable_name_extensions())?; token(meta, "%=")?; - let variable = handle_variable_reference(meta, var_tok, &self.var)?; + let variable = handle_variable_reference(meta, &var_tok, &self.var)?; + prevent_constant_mutation(meta, &var_tok, &self.var, variable.is_const)?; self.kind = variable.kind; self.global_id = variable.global_id; self.is_ref = variable.is_ref; diff --git a/src/modules/shorthand/mul.rs b/src/modules/shorthand/mul.rs index 7d06c87e..9d1ec42a 100644 --- a/src/modules/shorthand/mul.rs +++ b/src/modules/shorthand/mul.rs @@ -2,7 +2,7 @@ use heraclitus_compiler::prelude::*; use crate::docs::module::DocumentationModule; use crate::error_type_match; use crate::modules::expression::expr::Expr; -use crate::modules::variable::{variable_name_extensions, handle_variable_reference}; +use crate::modules::variable::{handle_variable_reference, prevent_constant_mutation, variable_name_extensions}; use crate::translate::compute::translate_computation_eval; use crate::utils::{ParserMetadata, TranslateMetadata}; use crate::translate::{module::TranslateModule, compute::{ArithOp, translate_computation}}; @@ -34,7 +34,8 @@ impl SyntaxModule for ShorthandMul { let var_tok = meta.get_current_token(); self.var = variable(meta, variable_name_extensions())?; token(meta, "*=")?; - let variable = handle_variable_reference(meta, var_tok, &self.var)?; + let variable = handle_variable_reference(meta, &var_tok, &self.var)?; + prevent_constant_mutation(meta, &var_tok, &self.var, variable.is_const)?; self.kind = variable.kind; self.global_id = variable.global_id; self.is_ref = variable.is_ref; diff --git a/src/modules/shorthand/sub.rs b/src/modules/shorthand/sub.rs index 0aed82cc..e40bca18 100644 --- a/src/modules/shorthand/sub.rs +++ b/src/modules/shorthand/sub.rs @@ -2,7 +2,7 @@ use heraclitus_compiler::prelude::*; use crate::docs::module::DocumentationModule; use crate::error_type_match; use crate::modules::expression::expr::Expr; -use crate::modules::variable::{variable_name_extensions, handle_variable_reference}; +use crate::modules::variable::{handle_variable_reference, prevent_constant_mutation, variable_name_extensions}; use crate::translate::compute::translate_computation_eval; use crate::utils::{ParserMetadata, TranslateMetadata}; use crate::translate::{module::TranslateModule, compute::{ArithOp, translate_computation}}; @@ -34,7 +34,8 @@ impl SyntaxModule for ShorthandSub { let var_tok = meta.get_current_token(); self.var = variable(meta, variable_name_extensions())?; token(meta, "-=")?; - let variable = handle_variable_reference(meta, var_tok, &self.var)?; + let variable = handle_variable_reference(meta, &var_tok, &self.var)?; + prevent_constant_mutation(meta, &var_tok, &self.var, variable.is_const)?; self.kind = variable.kind; self.global_id = variable.global_id; self.is_ref = variable.is_ref; diff --git a/src/modules/statement/stmt.rs b/src/modules/statement/stmt.rs index 0962c008..8459561d 100644 --- a/src/modules/statement/stmt.rs +++ b/src/modules/statement/stmt.rs @@ -1,7 +1,6 @@ use heraclitus_compiler::prelude::*; use itertools::Itertools; use crate::docs::module::DocumentationModule; -use crate::modules::variable::constinit::ConstInit; use crate::utils::metadata::{ParserMetadata, TranslateMetadata}; use crate::modules::expression::expr::{Expr, ExprType}; use crate::translate::module::TranslateModule; @@ -72,7 +71,6 @@ pub enum StatementType { CommandModifier(CommandModifier), Comment(Comment), CommentDoc(CommentDoc), - ConstInit(ConstInit), } #[derive(Debug, Clone)] @@ -91,7 +89,7 @@ impl Statement { // Conditions IfChain, IfCondition, // Variables - VariableInit, VariableSet, ConstInit, + VariableInit, VariableSet, // Short hand ShorthandAdd, ShorthandSub, ShorthandMul, ShorthandDiv, diff --git a/src/modules/variable/constinit.rs b/src/modules/variable/constinit.rs deleted file mode 100644 index ebdfc525..00000000 --- a/src/modules/variable/constinit.rs +++ /dev/null @@ -1,73 +0,0 @@ -use heraclitus_compiler::prelude::*; -use crate::docs::module::DocumentationModule; -use crate::modules::types::{Typed, Type}; -use crate::modules::expression::expr::Expr; -use crate::translate::module::TranslateModule; -use crate::utils::metadata::{ParserMetadata, TranslateMetadata}; -use super::{variable_name_extensions, handle_identifier_name}; - -#[derive(Debug, Clone)] -pub struct ConstInit { - name: String, - expr: Box, - global_id: Option, - is_fun_ctx: bool -} - -impl ConstInit { - fn handle_add_const(&mut self, meta: &mut ParserMetadata, name: &str, kind: Type, tok: Option) -> SyntaxResult { - handle_identifier_name(meta, name, tok)?; - self.global_id = meta.add_var(name, kind, true); - Ok(()) - } -} - -impl SyntaxModule for ConstInit { - syntax_name!("Constant Initialize"); - - fn new() -> Self { - ConstInit { - name: String::new(), - expr: Box::new(Expr::new()), - global_id: None, - is_fun_ctx: false - } - } - - fn parse(&mut self, meta: &mut ParserMetadata) -> SyntaxResult { - token(meta, "const")?; - // Get the variable name - let tok = meta.get_current_token(); - self.name = variable(meta, variable_name_extensions())?; - context!({ - token(meta, "=")?; - syntax(meta, &mut *self.expr)?; - // Add a variable to the memory - self.handle_add_const(meta, &self.name.clone(), self.expr.get_type(), tok)?; - self.is_fun_ctx = meta.context.is_fun_ctx; - Ok(()) - }, |position| { - error_pos!(meta, position, format!("Expected '=' after variable name '{}'", self.name)) - }) - } -} - -impl TranslateModule for ConstInit { - fn translate(&self, meta: &mut TranslateMetadata) -> String { - let name = self.name.clone(); - let mut expr = self.expr.translate(meta); - if let Type::Array(_) = self.expr.get_type() { - expr = format!("({expr})"); - } - match self.global_id { - Some(id) => format!("declare -r __{id}_{name}={expr}"), - None => format!("declare -r {name}={expr}") - } - } -} - -impl DocumentationModule for ConstInit { - fn document(&self, _meta: &ParserMetadata) -> String { - "".to_string() - } -} diff --git a/src/modules/variable/get.rs b/src/modules/variable/get.rs index 1e19d195..ce186f2b 100644 --- a/src/modules/variable/get.rs +++ b/src/modules/variable/get.rs @@ -54,7 +54,7 @@ impl SyntaxModule for VariableGet { fn parse(&mut self, meta: &mut ParserMetadata) -> SyntaxResult { let tok = meta.get_current_token(); self.name = variable(meta, variable_name_extensions())?; - let variable = handle_variable_reference(meta, tok.clone(), &self.name)?; + let variable = handle_variable_reference(meta, &tok, &self.name)?; self.global_id = variable.global_id; self.is_ref = variable.is_ref; self.kind = variable.kind.clone(); diff --git a/src/modules/variable/init.rs b/src/modules/variable/init.rs index 94ffc296..bc377f82 100644 --- a/src/modules/variable/init.rs +++ b/src/modules/variable/init.rs @@ -11,13 +11,18 @@ pub struct VariableInit { name: String, expr: Box, global_id: Option, - is_fun_ctx: bool + is_fun_ctx: bool, + is_const: bool, } impl VariableInit { - fn handle_add_variable(&mut self, meta: &mut ParserMetadata, name: &str, kind: Type, tok: Option) -> SyntaxResult { - handle_identifier_name(meta, name, tok)?; - self.global_id = meta.add_var(name, kind, false); + fn handle_add_variable( + &mut self, + meta: &mut ParserMetadata, + tok: Option + ) -> SyntaxResult { + handle_identifier_name(meta, &self.name, tok)?; + self.global_id = meta.add_var(&self.name, self.expr.get_type(), self.is_const); Ok(()) } } @@ -30,12 +35,14 @@ impl SyntaxModule for VariableInit { name: String::new(), expr: Box::new(Expr::new()), global_id: None, - is_fun_ctx: false + is_fun_ctx: false, + is_const: false } } fn parse(&mut self, meta: &mut ParserMetadata) -> SyntaxResult { - token(meta, "let")?; + let keyword = token_by(meta, |word| ["let", "const"].contains(&word.as_str()))?; + self.is_const = keyword == "const"; // Get the variable name let tok = meta.get_current_token(); self.name = variable(meta, variable_name_extensions())?; @@ -43,7 +50,7 @@ impl SyntaxModule for VariableInit { token(meta, "=")?; syntax(meta, &mut *self.expr)?; // Add a variable to the memory - self.handle_add_variable(meta, &self.name.clone(), self.expr.get_type(), tok)?; + self.handle_add_variable(meta, tok)?; self.is_fun_ctx = meta.context.is_fun_ctx; Ok(()) }, |position| { diff --git a/src/modules/variable/mod.rs b/src/modules/variable/mod.rs index a7e2a3fc..85d065be 100644 --- a/src/modules/variable/mod.rs +++ b/src/modules/variable/mod.rs @@ -9,7 +9,6 @@ use similar_string::find_best_similarity; pub mod init; pub mod set; pub mod get; -pub mod constinit; pub fn variable_name_extensions() -> Vec { vec!['_'] @@ -40,7 +39,7 @@ pub fn variable_name_keywords() -> Vec<&'static str> { } -pub fn handle_variable_reference(meta: &mut ParserMetadata, tok: Option, name: &str) -> Result { +pub fn handle_variable_reference(meta: &mut ParserMetadata, tok: &Option, name: &str) -> Result { handle_identifier_name(meta, name, tok.clone())?; match meta.get_var(name) { Some(variable_unit) => Ok(variable_unit.clone()), @@ -48,14 +47,22 @@ pub fn handle_variable_reference(meta: &mut ParserMetadata, tok: Option, let message = format!("Variable '{}' does not exist", name); // Find other similar variable if exists if let Some(comment) = handle_similar_variable(meta, name) { - error!(meta, tok, message, comment) + error!(meta, tok.clone(), message, comment) } else { - error!(meta, tok, message) + error!(meta, tok.clone(), message) } } } } +pub fn prevent_constant_mutation(meta: &mut ParserMetadata, tok: &Option, name: &str, is_const: bool) -> SyntaxResult { + if is_const { + error!(meta, tok.clone(), format!("Cannot reassign constant '{name}'")) + } else { + Ok(()) + } +} + fn handle_similar_variable(meta: &ParserMetadata, name: &str) -> Option { let vars = Vec::from_iter(meta.get_var_names()); find_best_similarity(name, &vars) diff --git a/src/modules/variable/set.rs b/src/modules/variable/set.rs index 0f1c3c64..1e84a037 100644 --- a/src/modules/variable/set.rs +++ b/src/modules/variable/set.rs @@ -2,7 +2,7 @@ use heraclitus_compiler::prelude::*; use crate::docs::module::DocumentationModule; use crate::{modules::expression::expr::Expr, translate::module::TranslateModule}; use crate::utils::{ParserMetadata, TranslateMetadata}; -use super::{variable_name_extensions, handle_variable_reference, handle_index_accessor}; +use super::{handle_index_accessor, handle_variable_reference, prevent_constant_mutation, variable_name_extensions}; use crate::modules::types::{Typed, Type}; #[derive(Debug, Clone)] @@ -43,13 +43,10 @@ impl SyntaxModule for VariableSet { self.index = handle_index_accessor(meta, false)?; token(meta, "=")?; syntax(meta, &mut *self.expr)?; - let variable = handle_variable_reference(meta, tok.clone(), &self.name)?; + let variable = handle_variable_reference(meta, &tok, &self.name)?; self.global_id = variable.global_id; self.is_ref = variable.is_ref; - // Check for constant reassignment - if variable.is_const { - return error!(meta, tok, format!("Cannot reassign constant")) - } + prevent_constant_mutation(meta, &tok, &self.name, variable.is_const)?; // Typecheck the variable let left_type = variable.kind.clone(); let right_type = self.expr.get_type(); diff --git a/src/tests/erroring.rs b/src/tests/erroring.rs new file mode 100644 index 00000000..4c7238d9 --- /dev/null +++ b/src/tests/erroring.rs @@ -0,0 +1,9 @@ +use super::script_test; +use super::TestOutcomeTarget; +use test_generator::test_resources; + +/// Autoload the Amber test files in erroring, match the output in the comment +#[test_resources("src/tests/erroring/*.ab")] +fn test_erroring(input: &str) { + script_test(input, TestOutcomeTarget::Failure); +} diff --git a/src/tests/erroring/array_index_get_by_text.ab b/src/tests/erroring/array_index_get_by_text.ab new file mode 100644 index 00000000..eeb9bf7a --- /dev/null +++ b/src/tests/erroring/array_index_get_by_text.ab @@ -0,0 +1,5 @@ +// Output +// Index accessor must be a number or range for right side of operation + +let array = [0, 1, 2, 3] +let slice = array["foo"] diff --git a/src/tests/erroring/array_index_set_by_range.ab b/src/tests/erroring/array_index_set_by_range.ab new file mode 100644 index 00000000..64fc5227 --- /dev/null +++ b/src/tests/erroring/array_index_set_by_range.ab @@ -0,0 +1,5 @@ +// Output +// Index accessor must be a number (and not a range) for left side of operation + +let array = [0, 1, 2, 3] +array[1..=2] = [11, 22] diff --git a/src/tests/erroring/array_index_set_by_text.ab b/src/tests/erroring/array_index_set_by_text.ab new file mode 100644 index 00000000..c6ca88d4 --- /dev/null +++ b/src/tests/erroring/array_index_set_by_text.ab @@ -0,0 +1,5 @@ +// Output +// Index accessor must be a number (and not a range) for left side of operation + +let array = [0, 1, 2, 3] +array["foo"] = [11, 22] diff --git a/src/tests/erroring/function_failable_with_typed_nonfailable_return.ab b/src/tests/erroring/function_failable_with_typed_nonfailable_return.ab new file mode 100644 index 00000000..4b09882b --- /dev/null +++ b/src/tests/erroring/function_failable_with_typed_nonfailable_return.ab @@ -0,0 +1,7 @@ +// Output +// Failable functions must return a Failable type + +pub fun test(): Null { + fail 1 +} +echo test() failed: echo "Failed" diff --git a/src/tests/erroring/function_nonfailable_with_typed_failable_return.ab b/src/tests/erroring/function_nonfailable_with_typed_failable_return.ab new file mode 100644 index 00000000..862588c9 --- /dev/null +++ b/src/tests/erroring/function_nonfailable_with_typed_failable_return.ab @@ -0,0 +1,7 @@ +// Output +// Non-failable functions cannot return a Failable type + +pub fun test(): Null? { + echo "Hello, World!" +} +echo test() failed: echo "Failed" diff --git a/src/tests/erroring/function_with_failable_typed_arg.ab b/src/tests/erroring/function_with_failable_typed_arg.ab new file mode 100644 index 00000000..2dac9221 --- /dev/null +++ b/src/tests/erroring/function_with_failable_typed_arg.ab @@ -0,0 +1,7 @@ +// Output +// Failable types cannot be used as arguments + +pub fun test(a: Text?) { + echo a +} +test("Hello, World!") diff --git a/src/tests/erroring/function_with_wrong_typed_return.ab b/src/tests/erroring/function_with_wrong_typed_return.ab new file mode 100644 index 00000000..457e3f02 --- /dev/null +++ b/src/tests/erroring/function_with_wrong_typed_return.ab @@ -0,0 +1,7 @@ +// Output +// Return type does not match function return type + +pub fun test(): Num { + return "Hello, World!" +} +echo test() diff --git a/src/tests/erroring/variable_constant.ab b/src/tests/erroring/variable_constant.ab new file mode 100644 index 00000000..958ac6bb --- /dev/null +++ b/src/tests/erroring/variable_constant.ab @@ -0,0 +1,5 @@ +// Output +// Cannot reassign constant 'foo' + +const foo = 12 +foo = 13 diff --git a/src/tests/erroring/variable_constant_shorthand_add.ab b/src/tests/erroring/variable_constant_shorthand_add.ab new file mode 100644 index 00000000..bc9484a3 --- /dev/null +++ b/src/tests/erroring/variable_constant_shorthand_add.ab @@ -0,0 +1,5 @@ +// Output +// Cannot reassign constant 'foo' + +const foo = 12 +foo += 13 diff --git a/src/tests/erroring/variable_constant_shorthand_div.ab b/src/tests/erroring/variable_constant_shorthand_div.ab new file mode 100644 index 00000000..fada8a1b --- /dev/null +++ b/src/tests/erroring/variable_constant_shorthand_div.ab @@ -0,0 +1,5 @@ +// Output +// Cannot reassign constant 'foo' + +const foo = 12 +foo /= 13 diff --git a/src/tests/erroring/variable_constant_shorthand_mod.ab b/src/tests/erroring/variable_constant_shorthand_mod.ab new file mode 100644 index 00000000..ffa004cb --- /dev/null +++ b/src/tests/erroring/variable_constant_shorthand_mod.ab @@ -0,0 +1,5 @@ +// Output +// Cannot reassign constant 'foo' + +const foo = 12 +foo %= 13 diff --git a/src/tests/erroring/variable_constant_shorthand_mul.ab b/src/tests/erroring/variable_constant_shorthand_mul.ab new file mode 100644 index 00000000..ca9cb007 --- /dev/null +++ b/src/tests/erroring/variable_constant_shorthand_mul.ab @@ -0,0 +1,5 @@ +// Output +// Cannot reassign constant 'foo' + +const foo = 12 +foo *= 13 diff --git a/src/tests/erroring/variable_constant_shorthand_sub.ab b/src/tests/erroring/variable_constant_shorthand_sub.ab new file mode 100644 index 00000000..a3de2b60 --- /dev/null +++ b/src/tests/erroring/variable_constant_shorthand_sub.ab @@ -0,0 +1,5 @@ +// Output +// Cannot reassign constant 'foo' + +const foo = 12 +foo -= 13 diff --git a/src/tests/errors.rs b/src/tests/errors.rs deleted file mode 100644 index c8acf8f7..00000000 --- a/src/tests/errors.rs +++ /dev/null @@ -1,86 +0,0 @@ -use super::test_amber; - -#[test] -#[should_panic(expected = "ERROR: Return type does not match function return type")] -fn function_with_wrong_typed_return() { - let code = r#" - pub fun test(): Num { - return "Hello, World!" - } - echo test() - "#; - - test_amber(code, "Hello, World!"); -} - -#[test] -#[should_panic(expected = "ERROR: Failable functions must return a Failable type")] -fn function_failable_with_typed_nonfailable_return() { - let code = r#" - pub fun test(): Null { - fail 1 - } - echo test() failed: echo "Failed" - "#; - - test_amber(code, "Failed"); -} - -#[test] -#[should_panic(expected = "ERROR: Non-failable functions cannot return a Failable type")] -fn function_nonfailable_with_typed_failable_return() { - let code = r#" - pub fun test(): Null? { - echo "Hello, World!" - } - echo test() failed: echo "Failed" - "#; - - test_amber(code, "Hello, World!"); -} - -#[test] -#[should_panic(expected = "ERROR: Failable types cannot be used as arguments")] -fn function_with_failable_typed_arg() { - let code = r#" - pub fun test(a: Text?) { - echo a - } - test("Hello, World!") - "#; - - test_amber(code, "Hello, World!"); -} - -#[test] -#[should_panic(expected = "ERROR: Index accessor must be a number or range for right side of operation")] -fn get_array_index_by_string() { - let code = r#" - let array = [0, 1, 2, 3] - let slice = array["foo"] - "#; - - test_amber(code, ""); -} - -#[test] -#[should_panic(expected = "ERROR: Index accessor must be a number (and not a range) for left side of operation")] -fn set_array_index_by_string() { - let code = r#" - let array = [0, 1, 2, 3] - array["foo"] = [11, 22] - "#; - - test_amber(code, ""); -} - -#[test] -#[should_panic(expected = "ERROR: Index accessor must be a number (and not a range) for left side of operation")] -fn set_array_index_by_range() { - let code = r#" - let array = [0, 1, 2, 3] - array[1..=2] = [11, 22] - "#; - - test_amber(code, ""); -} diff --git a/src/tests/extra.rs b/src/tests/extra.rs index bf53d300..2c2e35a4 100644 --- a/src/tests/extra.rs +++ b/src/tests/extra.rs @@ -5,6 +5,7 @@ use crate::tests::compile_code; use std::fs; use std::process::{Command, Stdio}; use std::time::Duration; +use super::TestOutcomeTarget; fn http_server() { use tiny_http::{Response, Server}; @@ -67,7 +68,7 @@ fn download() { let code = fs::read_to_string("src/tests/stdlib/no_output/download.ab") .expect("Failed to open stdlib/no_output/download.ab test file"); - test_amber(code, "ok"); + test_amber(&code, "ok", TestOutcomeTarget::Success); std::thread::sleep(Duration::from_millis(150)); assert!(server.is_finished(), "Server has not stopped!"); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index cb6eb01e..8c9a5392 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,4 +1,5 @@ extern crate test_generator; +use heraclitus_compiler::prelude::Message; use crate::compiler::{AmberCompiler, CompilerOptions}; use itertools::Itertools; use pretty_assertions::assert_eq; @@ -7,22 +8,46 @@ use std::path::PathBuf; use std::process::{Command, Stdio}; pub mod cli; -pub mod errors; pub mod extra; pub mod postprocessor; mod stdlib; mod validity; +mod erroring; -/// compare the output of the given code with the expected output -pub fn test_amber(code: impl Into, result: impl AsRef) { +pub enum TestOutcomeTarget { + Success, + Failure, +} + +pub fn eval_amber_code(code: &str) -> Result { let options = CompilerOptions::default(); - let mut compiler = AmberCompiler::new(code.into(), None, options); - match compiler.test_eval() { - Ok(eval_result) => assert_eq!( - eval_result.trim_end_matches('\n'), - result.as_ref().trim_end_matches('\n'), - ), - Err(err) => panic!("ERROR: {}", err.message.unwrap()), + let mut compiler = AmberCompiler::new(code.to_string(), None, options); + compiler.test_eval() +} + +/// Tests script output in case of success or failure +pub fn test_amber(code: &str, result: &str, target: TestOutcomeTarget) { + let evaluated = eval_amber_code(code); + match target { + TestOutcomeTarget::Success => match evaluated { + Ok(stdout) => { + assert_eq!( + stdout.trim_end_matches('\n'), + result.trim_end_matches('\n'), + ) + }, + Err(err) => { + panic!("ERROR: {}", err.message.unwrap()) + }, + } + TestOutcomeTarget::Failure => match evaluated { + Ok(stdout) => { + panic!("Expected error, got: {}", stdout) + }, + Err(err) => { + assert_eq!(err.message.expect("Error message expected"), result) + }, + } } } @@ -49,7 +74,7 @@ pub fn eval_bash>(code: T) -> (String, String) { } /// Extracts the output from the comment of amber code -pub fn extract_output(code: impl Into) -> String { +fn extract_output(code: impl Into) -> String { code.into() .lines() .skip_while(|line| !line.starts_with("// Output")) @@ -59,8 +84,8 @@ pub fn extract_output(code: impl Into) -> String { .join("\n") } -/// inner test logic for script tests, used by stdlib tests and validity tests -pub fn script_test(input: &str) { +/// Inner test logic for testing script output in case of success or failure +pub fn script_test(input: &str, target: TestOutcomeTarget) { let code = fs::read_to_string(input).unwrap_or_else(|_| panic!("Failed to open {input} test file")); @@ -76,8 +101,7 @@ pub fn script_test(input: &str) { _ => "Succeeded".to_string(), }; } - - test_amber(code, output); + test_amber(&code, &output, target); } #[cfg(test)] diff --git a/src/tests/stdlib.rs b/src/tests/stdlib.rs index 912f3cee..9168a264 100644 --- a/src/tests/stdlib.rs +++ b/src/tests/stdlib.rs @@ -1,8 +1,9 @@ use super::script_test; +use super::TestOutcomeTarget; use test_generator::test_resources; /// Autoload the Amber test files in stdlib, match the output in the comment #[test_resources("src/tests/stdlib/*.ab")] fn test_stdlib(input: &str) { - script_test(input); + script_test(input, TestOutcomeTarget::Success); } diff --git a/src/tests/validity.rs b/src/tests/validity.rs index 7a7bdb6d..643a6a16 100644 --- a/src/tests/validity.rs +++ b/src/tests/validity.rs @@ -1,8 +1,9 @@ use super::script_test; +use super::TestOutcomeTarget; use test_generator::test_resources; /// Autoload the Amber test files in validity, match the output in the comment #[test_resources("src/tests/validity/*.ab")] fn test_validity(input: &str) { - script_test(input); + script_test(input, TestOutcomeTarget::Success); } diff --git a/src/tests/validity/constant.ab b/src/tests/validity/constant.ab deleted file mode 100644 index e100ca22..00000000 --- a/src/tests/validity/constant.ab +++ /dev/null @@ -1,15 +0,0 @@ -// Output -// 1 -// 42 -// 1 -// 42 - -main { - const x = 42 - unsafe $ {nameof x}=123 $ - echo status // will output 1 if reassignment didnt succeed - echo x - unsafe $ unset {nameof x} $ - echo status // will output 1 if unsetting did not succeed - echo x -} \ No newline at end of file diff --git a/src/tests/validity/import_with_trailing_comma.ab b/src/tests/validity/import_with_trailing_comma.ab index 727f6f5c..0c5625af 100644 --- a/src/tests/validity/import_with_trailing_comma.ab +++ b/src/tests/validity/import_with_trailing_comma.ab @@ -5,8 +5,8 @@ import { import { sum, abs } from "std/math" // Output -// c +// bb // 3 -echo replace_regex(replace("aa", "a", "b"), "b+", "c", true) +echo replace("aa", "a", "b") echo sum([abs(-2), 1]) diff --git a/src/tests/validity/variable_constant_condition.ab b/src/tests/validity/variable_constant_condition.ab new file mode 100644 index 00000000..6878581f --- /dev/null +++ b/src/tests/validity/variable_constant_condition.ab @@ -0,0 +1,28 @@ +// Output +// Outer: 42 +// If: 12 +// Outer: 42 +// Else: 24 +// Outer: 42 + +main { + const x = 42 + echo "Outer: {x}" + + if x > 12 { + const x = 12 + echo "If: {x}" + } + + echo "Outer: {x}" + + if x < 12 { + const x = 12 + echo "If: {x}" + } else { + const x = 24 + echo "Else: {x}" + } + + echo "Outer: {x}" +} diff --git a/src/tests/validity/variable_constant_function.ab b/src/tests/validity/variable_constant_function.ab new file mode 100644 index 00000000..0a3d8f9d --- /dev/null +++ b/src/tests/validity/variable_constant_function.ab @@ -0,0 +1,32 @@ +// Output +// Init: 12 +// Looping: 1 +// Looping: 2 +// Looping: 3 +// Middle: 12 +// If: 24 +// End: 12 +// Outer: 42 + +const x = 42 + +fun foo() { + const x = 12 + echo "Init: {x}" + + for i in [1, 2, 3] { + const x = i + echo "Looping: {x}" + } + echo "Middle: {x}" + + if x >= 12 { + const x = 24 + echo "If: {x}" + } + echo "End: {x}" +} + +foo() + +echo "Outer: {x}" diff --git a/src/tests/validity/variable_constant_loop.ab b/src/tests/validity/variable_constant_loop.ab new file mode 100644 index 00000000..59fb3f0b --- /dev/null +++ b/src/tests/validity/variable_constant_loop.ab @@ -0,0 +1,18 @@ +// Output +// Before: 42 +// Looping: 1 +// Looping: 2 +// Looping: 3 +// After: 42 + +main { + const x = 42 + echo "Before: {x}" + + for i in [1, 2, 3] { + const x = i + echo "Looping: {x}" + } + + echo "After: {x}" +} diff --git a/src/utils/metadata/parser.rs b/src/utils/metadata/parser.rs index ebfd874d..e5665da4 100644 --- a/src/utils/metadata/parser.rs +++ b/src/utils/metadata/parser.rs @@ -69,7 +69,7 @@ impl ParserMetadata { /// Adds a variable to the current scope pub fn add_var(&mut self, name: &str, kind: Type, is_const: bool) -> Option { - let global_id = self.is_global_scope().then(|| self.gen_var_id()); + let global_id = (self.is_global_scope() || self.is_shadowing_prev_scope(name)).then(|| self.gen_var_id()); let scope = self.context.scopes.last_mut().unwrap(); scope.add_var(VariableDecl { name: name.to_string(), @@ -95,6 +95,18 @@ impl ParserMetadata { global_id } + pub fn is_shadowing_prev_scope(&self, name: &str) -> bool { + if self.context.scopes.len() > 1 { + self.context.scopes.iter() + .rev() + .skip(1) + .find_map(|scope| scope.get_var(name)) + .is_some() + } else { + false + } + } + /// Gets a variable from the current scope or any parent scope pub fn get_var(&self, name: &str) -> Option<&VariableDecl> { self.context.scopes.iter().rev().find_map(|scope| scope.get_var(name))