-
Notifications
You must be signed in to change notification settings - Fork 98
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 7 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,59 @@ | ||
use std::fmt::Display; | ||
|
||
use heraclitus_compiler::prelude::*; | ||
use itertools::Itertools; | ||
use crate::utils::ParserMetadata; | ||
|
||
#[derive(Debug, Clone, PartialEq, Eq, Default)] | ||
#[derive(Debug, Clone, Eq, Default)] | ||
pub enum Type { | ||
#[default] Null, | ||
Text, | ||
Bool, | ||
Num, | ||
Union(Vec<Type>), | ||
Array(Box<Type>), | ||
Failable(Box<Type>), | ||
Generic | ||
} | ||
|
||
impl Type { | ||
fn eq_union_normal(one: &[Type], other: &Type) -> bool { | ||
one.iter().any(|x| (*x).to_string() == other.to_string()) | ||
} | ||
|
||
fn eq_unions(one: &[Type], other: &[Type]) -> bool { | ||
one.iter().any(|x| { | ||
Self::eq_union_normal(other, x) | ||
}) | ||
} | ||
} | ||
|
||
impl PartialEq for Type { | ||
fn eq(&self, other: &Self) -> bool { | ||
if let Type::Union(union) = self { | ||
if let Type::Union(other) = other { | ||
return Type::eq_unions(union, other); | ||
} else { | ||
return Type::eq_union_normal(union, other); | ||
} | ||
} | ||
|
||
if let Type::Union(other) = other { | ||
Type::eq_union_normal(other, self) | ||
} else { | ||
self.to_string() == other.to_string() | ||
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. While a good workaround, I don't think it is optimal performance-wise to compare types by stringifying them. FYI, the optimal yet broken way would be to use the std::mem::discriminant function to compare enum variants. The issue is that it doesn't care about the nested data, see #300 (comment). |
||
} | ||
} | ||
} | ||
|
||
impl Display for Type { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
match self { | ||
Type::Text => write!(f, "Text"), | ||
Type::Bool => write!(f, "Bool"), | ||
Type::Num => write!(f, "Num"), | ||
Type::Null => write!(f, "Null"), | ||
Type::Union(types) => write!(f, "{}", types.iter().map(|x| format!("{x}")).join(" | ")), | ||
Type::Array(t) => write!(f, "[{}]", t), | ||
Type::Failable(t) => write!(f, "{}?", t), | ||
Type::Generic => write!(f, "Generic") | ||
|
@@ -39,10 +72,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" => { | ||
|
@@ -99,10 +130,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)) | ||
} | ||
|
||
res | ||
|
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,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.
The function
try_parse_type
could be simply extended with a recursive approach. Here is a code block that illustrates how can it be achieved without introducing new functions:Here is the full function for a reference:
Full implementation of
try_parse_type