diff --git a/.gitignore b/.gitignore index f0d86dc6..43287fee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +.agent/ # Test files test*.ab @@ -16,4 +17,3 @@ flamegraph.svg # Nixos .direnv /result - diff --git a/grammar.ebnf b/grammar.ebnf index 10502584..8dae6487 100644 --- a/grammar.ebnf +++ b/grammar.ebnf @@ -94,7 +94,8 @@ KEYWORD_WHILE = 'while' ; ANY_CHAR = ? any character ? ; LETTER = 'A'..'Z' | 'a'..'z' ; DIGIT = '0'..'9' ; -TYPE = 'Text' | 'Num' | 'Bool' | 'Null'; +TYPE = SIMPLE_TYPE, { '|', SIMPLE_TYPE } ; +SIMPLE_TYPE = 'Text' | 'Num' | 'Bool' | 'Null' | 'Int' | '[', TYPE, ']' ; UNARY_OP = '-' | KEYWORD_NOT ; BINARY_OP = '+' | '-' | '*' | '/' | '%' | KEYWORD_AND | KEYWORD_OR | '==' | '!=' | '<' | '<=' | '>' | '>=' ; VISIBILITY = KEYWORD_PUB ; diff --git a/src/modules/types.rs b/src/modules/types.rs index 58226009..68b974c4 100644 --- a/src/modules/types.rs +++ b/src/modules/types.rs @@ -12,6 +12,7 @@ pub enum Type { Num, Int, Array(Box), + Union(Vec), Generic } @@ -23,13 +24,16 @@ impl Type { pub fn is_subset_of(&self, other: &Type) -> bool { match (self, other) { + (Type::Generic, Type::Generic) => false, (_, Type::Generic) => true, (Type::Int, Type::Num) => true, (Type::Array(current), Type::Array(other)) => match (&**current, &**other) { (current, Type::Generic) if *current != Type::Generic => true, (Type::Int, Type::Num) => true, - _ => false + (a, b) => a.is_subset_of(b), }, + (Type::Union(types), other) => types.iter().all(|t| t.is_allowed_in(other)), + (other, Type::Union(types)) => types.iter().any(|t| other.is_allowed_in(t)), _ => false } } @@ -42,6 +46,8 @@ impl Type { matches!(self, Type::Array(_)) } + + pub fn pretty_join(types: &[Self], op: &str) -> String { let mut all_types = types.iter().map(|kind| kind.to_string()).collect_vec(); let last_item = all_types.pop(); @@ -71,6 +77,7 @@ impl Display for Type { } else { write!(f, "[{t}]") }, + Type::Union(types) => write!(f, "{}", types.iter().map(|t| t.to_string()).join(" | ")), Type::Generic => write!(f, "Generic") } } @@ -89,6 +96,32 @@ 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 mut left = try_parse_simple_type(meta)?; + // Parse union type + while token(meta, "|").is_ok() { + let right = try_parse_simple_type(meta)?; + left = match (left, right) { + (Type::Union(mut left_types), Type::Union(mut right_types)) => { + left_types.append(&mut right_types); + Type::Union(left_types) + }, + (Type::Union(mut left_types), right) => { + left_types.push(right); + Type::Union(left_types) + }, + (left, Type::Union(mut right_types)) => { + let mut left_types = vec![left]; + left_types.append(&mut right_types); + Type::Union(left_types) + }, + (left, right) => Type::Union(vec![left, right]) + } + } + + Ok(left) +} + +fn try_parse_simple_type(meta: &mut ParserMetadata) -> Result { let tok = meta.get_current_token(); let res = match tok.clone() { Some(matched_token) => { diff --git a/src/tests/erroring/union_types_mismatch.ab b/src/tests/erroring/union_types_mismatch.ab new file mode 100644 index 00000000..dca6c41d --- /dev/null +++ b/src/tests/erroring/union_types_mismatch.ab @@ -0,0 +1,8 @@ +// Output +// 1st argument 'x' of function 'foo' expects type 'Int | Bool', but 'Text' was given + +fun foo(x: Int | Bool) { + echo x +} + +foo("hello") diff --git a/src/tests/validity/union_types_valid.ab b/src/tests/validity/union_types_valid.ab new file mode 100644 index 00000000..e5430980 --- /dev/null +++ b/src/tests/validity/union_types_valid.ab @@ -0,0 +1,30 @@ +// Output +// 1 +// 1 +// hello +// 1 2 +// 1 0 +// 1 2 +// 1 0 + +fun foo(x: Int | Bool | Text) { + echo x +} + +foo(1) +foo(true) +foo("hello") + +fun bar(x: [Int] | [Bool]) { + echo x +} + +bar([1, 2]) +bar([true, false]) + +fun baz(x: [Int | Bool]) { + echo x +} + +baz([1, 2]) +baz([true, false])