diff --git a/src/modules/command/expr.rs b/src/modules/command/expr.rs index 28012e11..97eee1b2 100644 --- a/src/modules/command/expr.rs +++ b/src/modules/command/expr.rs @@ -21,7 +21,7 @@ pub struct CommandExpr { impl Typed for CommandExpr { fn get_type(&self) -> Type { - Type::Text + Type::Failable(Box::new()); } } diff --git a/src/modules/expression/unop/cast.rs b/src/modules/expression/unop/cast.rs index 13b2e23e..fc1a6570 100644 --- a/src/modules/expression/unop/cast.rs +++ b/src/modules/expression/unop/cast.rs @@ -36,6 +36,14 @@ impl SyntaxModule for Cast { let message = Message::new_warn_at_token(meta, tok) .message(format!("Casting a value of type '{l_type}' value to a '{r_type}' is not recommended")) .comment(format!("To suppress this warning, use '{flag_name}' compiler flag")); + let (l_type, r_type) = match (l_type, r_type) { + (Type::Failable(l), Type::Failable(r)) => (*l, *r), + (Type::Failable(_), _) | (_, Type::Failable(_)) => { + meta.add_message(message); + return Ok(()); + }, + types => types + }; match (l_type, r_type) { (Type::Array(left), Type::Array(right)) => { if *left != *right && !matches!(*left, Type::Bool | Type::Num) && !matches!(*right, Type::Bool | Type::Num) { diff --git a/src/modules/function/declaration.rs b/src/modules/function/declaration.rs index f7efdb6e..3df06f43 100644 --- a/src/modules/function/declaration.rs +++ b/src/modules/function/declaration.rs @@ -173,6 +173,9 @@ impl SyntaxModule for FunctionDeclaration { self.arg_types.push(Type::Generic); } } + if let Type::Failable(_) = arg_type { + return error!(meta, name_token, "Failable types cannot be used as arguments"); + } match token(meta,"=") { Ok(_) => { if is_ref { @@ -198,14 +201,23 @@ impl SyntaxModule for FunctionDeclaration { Err(_) => token(meta, ",")? }; } + let mut returns_tok = None; // Optionally parse the return type match token(meta, ":") { - Ok(_) => self.returns = parse_type(meta)?, + Ok(_) => { + returns_tok = meta.get_current_token(); + self.returns = parse_type(meta)? + }, Err(_) => self.returns = Type::Generic } // Parse the body token(meta, "{")?; let (index_begin, index_end, is_failable) = skip_function_body(meta); + if is_failable && !matches!(self.returns, Type::Failable(_) | Type::Generic) { + return error!(meta, returns_tok, "Failable functions must return a Failable type"); + } else if !is_failable && matches!(self.returns, Type::Failable(_)) { + return error!(meta, returns_tok, "Non-failable functions cannot return a Failable type"); + } // Create a new context with the function body let expr = meta.context.expr[index_begin..index_end].to_vec(); let ctx = meta.context.clone().function_invocation(expr); diff --git a/src/modules/function/invocation.rs b/src/modules/function/invocation.rs index 4baeb869..5aa7198e 100644 --- a/src/modules/function/invocation.rs +++ b/src/modules/function/invocation.rs @@ -94,10 +94,17 @@ impl SyntaxModule for FunctionInvocation { } } + let types = self.args.iter().map(|e| e.get_type()).collect::>(); + let var_names = self.args.iter().map(|e| e.is_var()).collect::>(); + self.refs.clone_from(&function_unit.arg_refs); + (self.kind, self.variant_id) = handle_function_parameters(meta, self.id, function_unit.clone(), &types, &var_names, tok.clone())?; + self.is_failable = function_unit.is_failable; if self.is_failable { match syntax(meta, &mut self.failed) { - Ok(_) => {}, + Ok(_) => if let Type::Failable(t) = &self.kind { + self.kind = *t.clone(); + }, Err(Failure::Quiet(_)) => return error!(meta, tok => { message: "This function can fail. Please handle the failure", comment: "You can use '?' in the end to propagate the failure" @@ -113,10 +120,7 @@ impl SyntaxModule for FunctionInvocation { meta.add_message(message); } } - let types = self.args.iter().map(|e| e.get_type()).collect::>(); - let var_names = self.args.iter().map(|e| e.is_var()).collect::>(); - self.refs.clone_from(&function_unit.arg_refs); - (self.kind, self.variant_id) = handle_function_parameters(meta, self.id, function_unit, &types, &var_names, tok)?; + Ok(()) }) } diff --git a/src/modules/function/ret.rs b/src/modules/function/ret.rs index 5ed42d8e..419cdcca 100644 --- a/src/modules/function/ret.rs +++ b/src/modules/function/ret.rs @@ -35,15 +35,26 @@ impl SyntaxModule for Return { }); } syntax(meta, &mut self.expr)?; - match meta.context.fun_ret_type.as_ref() { - Some(ret_type) => if ret_type != &self.expr.get_type() { - return error!(meta, tok => { - message: "Return type does not match function return type", - comment: format!("Given type: {}, expected type: {}", self.expr.get_type(), ret_type) - }); + let ret_type = meta.context.fun_ret_type.as_ref(); + let expr_type = &self.expr.get_type(); + // Unpacking Failable types + let (ret_type, expr_type) = match (ret_type, expr_type) { + types @ (Some(Type::Failable(_)), Type::Failable(_)) => types, + (Some(Type::Failable(ret_type)), expr_type) => (Some(ret_type.as_ref()), expr_type), + (Some(ret_type), Type::Failable(expr_type)) => (Some(ret_type), expr_type.as_ref()), + types @ _ => types + }; + match ret_type { + Some(ret_type) => { + if ret_type != expr_type { + return error!(meta, tok => { + message: "Return type does not match function return type", + comment: format!("Given type: {}, expected type: {}", expr_type, ret_type) + }); + } }, None => { - meta.context.fun_ret_type = Some(self.expr.get_type()); + meta.context.fun_ret_type = Some(expr_type.clone()); } } Ok(()) diff --git a/src/modules/types.rs b/src/modules/types.rs index d6ce868e..bc13310d 100644 --- a/src/modules/types.rs +++ b/src/modules/types.rs @@ -10,6 +10,7 @@ pub enum Type { Num, Null, Array(Box), + Failable(Box), Generic } @@ -21,6 +22,7 @@ impl Display for Type { Type::Num => write!(f, "Num"), Type::Null => write!(f, "Null"), Type::Array(t) => write!(f, "[{}]", t), + Type::Failable(t) => write!(f, "{}?", t), Type::Generic => write!(f, "Generic") } } @@ -40,7 +42,7 @@ pub fn parse_type(meta: &mut ParserMetadata) -> Result { // Tries to parse the type - if it fails, it fails quietly pub fn try_parse_type(meta: &mut ParserMetadata) -> Result { let tok = meta.get_current_token(); - match tok.clone() { + let res = match tok.clone() { Some(matched_token) => { match matched_token.word.as_ref() { "Text" => { @@ -97,5 +99,11 @@ pub fn try_parse_type(meta: &mut ParserMetadata) -> Result { None => { Err(Failure::Quiet(PositionInfo::at_eof(meta))) } + }; + + if token(meta, "?").is_ok() { + return res.map(|t| Type::Failable(Box::new(t))) } -} \ No newline at end of file + + res +} diff --git a/src/rules.rs b/src/rules.rs index 05f03d2e..4c2881f7 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -4,7 +4,7 @@ pub fn get_rules() -> Rules { let symbols = vec![ '+', '-', '*', '/', '%', '\n', ';', ':', '(', ')', '[', ']', '{', '}', ',', '.', - '<', '>', '=', '!' + '<', '>', '=', '!', '?' ]; let compounds = vec![ ('<', '='), diff --git a/src/std/env.ab b/src/std/env.ab index a4747b7f..32f5dbf1 100644 --- a/src/std/env.ab +++ b/src/std/env.ab @@ -26,23 +26,23 @@ pub fun shell_isset(name: Text): Bool { return true } -pub fun shell_constant_set(name: Text, val: Text): Null { +pub fun shell_constant_set(name: Text, val: Text): Null? { $readonly \${nameof name}="\${nameof val}" 2> /dev/null$? } -pub fun shell_constant_get(name: Text): Text { +pub fun shell_constant_get(name: Text): Text? { return $echo \$\{!{nameof name}}$? } -pub fun shell_var_set(name: Text, val: Text): Null { +pub fun shell_var_set(name: Text, val: Text): Null? { $export \${nameof name}="\${nameof val}" 2> /dev/null$? } -pub fun shell_var_get(name: Text): Text { +pub fun shell_var_get(name: Text): Text? { return $echo \$\{!{nameof name}}$? } -pub fun shell_unset(name: Text): Null { +pub fun shell_unset(name: Text): Null? { $unset {name}$? } diff --git a/src/std/text.ab b/src/std/text.ab index cb43f312..fc1d4c47 100644 --- a/src/std/text.ab +++ b/src/std/text.ab @@ -1,9 +1,9 @@ pub fun replace_once(source, pattern, replacement) { - return unsafe $echo "\$\{source/{pattern}/{replacement}}"$ + return "\$\{source/{pattern}/{replacement}}" } pub fun replace(source, pattern, replacement) { - return unsafe $echo "\$\{source//{pattern}/{replacement}}"$ + return "\$\{source//{pattern}/{replacement}}" } pub fun replace_regex(source: Text, pattern: Text, replacement: Text): Text { @@ -52,7 +52,7 @@ pub fun upper(text: Text): Text { } #[allow_absurd_cast] -pub fun parse(text: Text): Num { +pub fun parse(text: Text): Num? { $[ -n "{text}" ] && [ "{text}" -eq "{text}" ] 2>/dev/null$? return text as Num } diff --git a/src/tests/errors.rs b/src/tests/errors.rs new file mode 100644 index 00000000..0e4eb268 --- /dev/null +++ b/src/tests/errors.rs @@ -0,0 +1,55 @@ +use crate::compiler::AmberCompiler; +use crate::Cli; +use crate::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!"); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index b475f87e..967368bd 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -3,6 +3,7 @@ use crate::Cli; pub mod cli; pub mod formatter; +pub mod errors; pub mod stdlib; pub mod validity; diff --git a/src/tests/validity/failed_block_unwraps_failable_type.ab b/src/tests/validity/failed_block_unwraps_failable_type.ab new file mode 100644 index 00000000..157dfcf9 --- /dev/null +++ b/src/tests/validity/failed_block_unwraps_failable_type.ab @@ -0,0 +1,11 @@ +fun failable(): Num? { + if 0 > 5 { + fail 1 + } + + return 1 +} + +let a = failable() failed: echo "Failed" + +if a is Num: echo "Succeded" diff --git a/src/tests/validity/failed_unwraps_failable_type.ab b/src/tests/validity/failed_unwraps_failable_type.ab new file mode 100644 index 00000000..157dfcf9 --- /dev/null +++ b/src/tests/validity/failed_unwraps_failable_type.ab @@ -0,0 +1,11 @@ +fun failable(): Num? { + if 0 > 5 { + fail 1 + } + + return 1 +} + +let a = failable() failed: echo "Failed" + +if a is Num: echo "Succeded" diff --git a/src/tests/validity/function_failable.ab b/src/tests/validity/function_failable.ab new file mode 100644 index 00000000..5b9d70eb --- /dev/null +++ b/src/tests/validity/function_failable.ab @@ -0,0 +1,9 @@ +fun test() { + if 0 < 5 { + fail 1 + } + + return 42 +} + +test() failed: echo "Succeded" diff --git a/src/tests/validity/function_failable_with_typed_return.ab b/src/tests/validity/function_failable_with_typed_return.ab new file mode 100644 index 00000000..8b605c0b --- /dev/null +++ b/src/tests/validity/function_failable_with_typed_return.ab @@ -0,0 +1,9 @@ +pub fun test(): Num? { + if 0 < 5 { + fail 1 + } + + return 42 +} + +echo test() failed: echo "Succeded" diff --git a/src/tests/validity/function_with_typed_return.ab b/src/tests/validity/function_with_typed_return.ab new file mode 100644 index 00000000..e32a3834 --- /dev/null +++ b/src/tests/validity/function_with_typed_return.ab @@ -0,0 +1,5 @@ +pub fun test(): Text { + return "Succeded" +} + +echo test() diff --git a/src/tests/validity/unsafe_function_call.ab b/src/tests/validity/unsafe_function_call.ab index b98b9672..a0900259 100644 --- a/src/tests/validity/unsafe_function_call.ab +++ b/src/tests/validity/unsafe_function_call.ab @@ -1,7 +1,7 @@ // Output // 6, 0 -fun safe_division(a: Num, b: Num): Num { +fun safe_division(a: Num, b: Num): Num? { if b == 0: fail 1 return a / b diff --git a/src/tests/validity/unsafe_unwraps_failable_type.ab b/src/tests/validity/unsafe_unwraps_failable_type.ab new file mode 100644 index 00000000..e71acf8a --- /dev/null +++ b/src/tests/validity/unsafe_unwraps_failable_type.ab @@ -0,0 +1,9 @@ +fun test(): Num? { + if 0 < 5 { + fail 1 + } + + return 42 +} + +if unsafe test() is Num: echo "Succeded"