-
Notifications
You must be signed in to change notification settings - Fork 94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: union types #466
base: staging
Are you sure you want to change the base?
feat: union types #466
Changes from all commits
4f9f00f
a742b63
888ea7a
5a67306
2e459d1
fa78fa5
62bcb5b
3adcf2a
f189e32
65e1e4a
50d2120
87dfe4f
0e42410
6bf7b37
d77b30e
103c2ef
b000459
a8fc55b
cc6d0ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function if token(meta, "|").is_ok() {
// We're parsing this function recursively
match (res, try_parse_type(meta)) {
// And we flatten the result into a single union
(Ok(lhs), Ok(rhs)) => return Ok(Type::Union([&[lhs], &rhs[..]].concat()))
(Err(_), _) => error!(meta, tok, "Expected type before '|'.")
(_, Err(_)) => error!(meta, tok, "Expected type after '|'.")
}
} Here is the full function for a reference: Full implementation of
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -1,14 +1,19 @@ | ||||
use std::fmt::Display; | ||||
|
||||
use heraclitus_compiler::prelude::*; | ||||
use itertools::Itertools; | ||||
use union::UnionType; | ||||
use crate::utils::ParserMetadata; | ||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)] | ||||
mod union; | ||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)] | ||||
pub enum Type { | ||||
#[default] Null, | ||||
Text, | ||||
Bool, | ||||
Num, | ||||
Union(UnionType), | ||||
Array(Box<Type>), | ||||
Failable(Box<Type>), | ||||
Generic | ||||
|
@@ -54,7 +59,12 @@ impl Display for Type { | |||
write!(f, "[{}]", t) | ||||
}, | ||||
Type::Failable(t) => write!(f, "{}?", t), | ||||
Type::Generic => write!(f, "Generic") | ||||
Type::Generic => write!(f, "Generic"), | ||||
|
||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
Type::Union(types) => { | ||||
let types: &Vec<Type> = types.into(); | ||||
write!(f, "{}", types.iter().map(|x| format!("{x}")).join(" | ")) | ||||
} | ||||
} | ||||
} | ||||
} | ||||
|
@@ -70,10 +80,8 @@ pub fn parse_type(meta: &mut ParserMetadata) -> Result<Type, Failure> { | |||
.map_err(|_| Failure::Loud(Message::new_err_at_token(meta, tok).message("Expected a data type"))) | ||||
} | ||||
|
||||
// Tries to parse the type - if it fails, it fails quietly | ||||
pub fn try_parse_type(meta: &mut ParserMetadata) -> Result<Type, Failure> { | ||||
let tok = meta.get_current_token(); | ||||
let res = match tok.clone() { | ||||
fn parse_type_tok(meta: &mut ParserMetadata, tok: Option<Token>) -> Result<Type, Failure> { | ||||
match tok.clone() { | ||||
Some(matched_token) => { | ||||
match matched_token.word.as_ref() { | ||||
"Text" => { | ||||
|
@@ -134,10 +142,35 @@ pub fn try_parse_type(meta: &mut ParserMetadata) -> Result<Type, Failure> { | |||
None => { | ||||
Err(Failure::Quiet(PositionInfo::at_eof(meta))) | ||||
} | ||||
}; | ||||
} | ||||
} | ||||
|
||||
fn parse_one_type(meta: &mut ParserMetadata, tok: Option<Token>) -> Result<Type, Failure> { | ||||
let res = parse_type_tok(meta, tok)?; | ||||
if token(meta, "?").is_ok() { | ||||
return res.map(|t| Type::Failable(Box::new(t))) | ||||
return Ok(Type::Failable(Box::new(res))) | ||||
} | ||||
Ok(res) | ||||
} | ||||
|
||||
// Tries to parse the type - if it fails, it fails quietly | ||||
pub fn try_parse_type(meta: &mut ParserMetadata) -> Result<Type, Failure> { | ||||
let tok = meta.get_current_token(); | ||||
let res = parse_one_type(meta, tok); | ||||
|
||||
if token(meta, "|").is_ok() { | ||||
// is union type | ||||
let mut unioned = vec![ res? ]; | ||||
loop { | ||||
match parse_one_type(meta, meta.get_current_token()) { | ||||
Err(err) => return Err(err), | ||||
Ok(t) => unioned.push(t) | ||||
}; | ||||
if token(meta, "|").is_err() { | ||||
break; | ||||
} | ||||
} | ||||
return Ok(Type::Union(unioned.into())) | ||||
} | ||||
|
||||
res | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
use super::Type; | ||
|
||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)] | ||
pub struct UnionType(pub Vec<Type>); | ||
|
||
impl UnionType { | ||
pub fn has(&self, other: &Type) -> bool { | ||
self.0.iter().find(|x| **x == *other).is_some() | ||
} | ||
} | ||
|
||
impl Into<Vec<Type>> for UnionType { | ||
fn into(self) -> Vec<Type> { | ||
self.0 | ||
} | ||
} | ||
|
||
impl <'a> Into<&'a Vec<Type>> for &'a UnionType { | ||
fn into(self) -> &'a Vec<Type> { | ||
&self.0 | ||
} | ||
} | ||
|
||
impl From<Vec<Type>> for UnionType { | ||
fn from(value: Vec<Type>) -> Self { | ||
let mut value = value; | ||
value.sort(); | ||
if value.len() < 2 { | ||
unreachable!("A union type must have at least two elements") | ||
} | ||
for typ in &value { | ||
if let Type::Union(_) = typ { | ||
unreachable!("Union types cannot be nested") | ||
} | ||
} | ||
|
||
Self(value) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
use crate::tests::test_amber; | ||
|
||
#[test] | ||
#[should_panic(expected = "ERROR: 1st argument 'param' of function 'abc' expects type 'Text | Null', but 'Num' was given")] | ||
fn invalid_union_type_eq_normal_type() { | ||
let code = r#" | ||
fun abc(param: Text | Null) {} | ||
abc("") | ||
abc(123) | ||
"#; | ||
test_amber(code, ""); | ||
} | ||
|
||
#[test] | ||
#[should_panic(expected = "ERROR: 1st argument 'param' of function 'abc' expects type 'Text | Null', but 'Num | [Text]' was given")] | ||
fn invalid_two_unions() { | ||
let code = r#" | ||
fun abc(param: Text | Null) {} | ||
abc(123 as Num | [Text]) | ||
"#; | ||
test_amber(code, ""); | ||
} | ||
|
||
#[test] | ||
#[should_panic(expected = "ERROR: 1st argument 'param' of function 'abc' expects type 'Text | Num | Text? | Num? | [Null]', but 'Null' was given")] | ||
fn big_union() { | ||
let code = r#" | ||
fun abc(param: Text | Num | Text? | Num? | [Null]) {} | ||
abc(null) | ||
"#; | ||
test_amber(code, ""); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
use crate::modules::types::Type; | ||
|
||
#[test] | ||
fn normal_types_eq() { | ||
let types = vec![Type::Null, Type::Text, Type::Bool, Type::Num, Type::Generic]; | ||
for typ in types { | ||
assert_eq!(typ, typ, "{typ} and {typ} must be equal!"); | ||
} | ||
} | ||
|
||
#[test] | ||
fn two_different_normal_types() { | ||
assert_ne!(Type::Null, Type::Bool); | ||
} | ||
|
||
#[test] | ||
fn normal_and_failable_type() { | ||
assert_ne!(Type::Failable(Box::new(Type::Text)), Type::Text, "Text? and Text must not be equal!") | ||
} | ||
|
||
#[test] | ||
fn array_and_normal_type() { | ||
assert_ne!(Type::Array(Box::new(Type::Bool)), Type::Bool); | ||
} | ||
|
||
#[test] | ||
fn array_and_array_of_failables() { | ||
assert_ne!(Type::Array(Box::new(Type::Bool)), Type::Array(Box::new(Type::Failable(Box::new(Type::Bool))))); | ||
} | ||
|
||
#[test] | ||
fn nested_array_normal_array_with_failable() { | ||
assert_ne!(Type::Array(Box::new(Type::Array(Box::new(Type::Bool)))), Type::Failable(Box::new(Type::Bool))); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pub mod union; | ||
pub mod eq; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
use crate::modules::types::Type; | ||
|
||
#[test] | ||
fn partially_overlapping_types() { | ||
let one = Type::Union(vec![Type::Text, Type::Num]); | ||
let two = Type::Union(vec![Type::Num, Type::Null]); | ||
|
||
assert_ne!(one, two, "Text | Num must not be equal to Num | Null!") | ||
} | ||
|
||
#[test] | ||
fn overlapping_types() { | ||
let one = Type::Union(vec![Type::Text, Type::Num]); | ||
let two = Type::Union(vec![Type::Text, Type::Num, Type::Null]); | ||
|
||
assert_eq!(one, two, "Text | Num must be equal to Text | Num | Null!") | ||
} | ||
|
||
#[test] | ||
fn same_union() { | ||
let one = Type::Union(vec![Type::Text, Type::Num]); | ||
let two = Type::Union(vec![Type::Text, Type::Num]); | ||
|
||
assert_eq!(one, two, "Text | Num must be equal to Text | Num!") | ||
} | ||
|
||
#[test] | ||
fn empty_union() { | ||
let one = Type::Union(vec![]); | ||
let two = Type::Union(vec![]); | ||
|
||
assert_eq!(one, two, "If one of unions is empty, it must always be equal to another") | ||
} | ||
|
||
#[test] | ||
fn empty_and_normal_union() { | ||
let one = Type::Union(vec![Type::Text, Type::Num]); | ||
let two = Type::Union(vec![]); | ||
|
||
assert_eq!(one, two, "If one of unions is empty, it must always be equal to another") | ||
} | ||
|
||
#[test] | ||
fn empty_union_and_normal_type() { | ||
let one = Type::Union(vec![]); | ||
let two = Type::Text; | ||
|
||
assert_ne!(one, two, "An empty union and one type are not equal") | ||
} | ||
|
||
#[test] | ||
fn big_union() { | ||
let one = Type::Union(vec![Type::Text, Type::Text, Type::Text, Type::Text, Type::Text, Type::Text, Type::Text, Type::Num]); | ||
let two = Type::Union(vec![Type::Text, Type::Num]); | ||
|
||
assert_eq!(one, two, "Text | Text | ... | Text | Num and Text | Num must be equal!") | ||
} | ||
|
||
#[test] | ||
fn normal_and_union() { | ||
let one = Type::Text; | ||
let two = Type::Union(vec![Type::Text, Type::Null]); | ||
|
||
assert_eq!(one, two, "Text and Text | Null must be equal!"); | ||
} | ||
|
||
#[test] | ||
fn normal_not_in_union() { | ||
let one = Type::Text; | ||
let two = Type::Union(vec![Type::Num, Type::Null]); | ||
|
||
assert_ne!(one, two, "Text and Num | Null must not be equal!"); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
// Output | ||
// abc | ||
// 123 | ||
|
||
fun check(thing: Text | Num): Null { | ||
echo thing | ||
} | ||
|
||
check("abc") | ||
check(123) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// Output | ||
// 123 | ||
|
||
let thingy = "abc" as Text | Num; | ||
thingy = 123; | ||
|
||
echo thingy; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Output | ||
// is text | ||
// abc | ||
// is num | ||
// 123 | ||
|
||
fun check(thing: Text | Num): Null { | ||
if thing is Text { | ||
echo "is text" | ||
echo thing | ||
} | ||
if thing is Num { | ||
echo "is num" | ||
echo thing | ||
} | ||
} | ||
|
||
check("abc") | ||
check(123) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't
Union
be also handled inType::is_allowed_in
?