-
-
Notifications
You must be signed in to change notification settings - Fork 873
Optimise comparison with singleton custom types on JavaScript #4903. … #4951
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
base: main
Are you sure you want to change the base?
Conversation
…ang#4903. also fixed some clippy errors
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.
Code is looking good!
I think you've forgotten to commit the updated snapshots!
return docvec![left_doc, operator, right_doc]; | ||
} | ||
|
||
// Optimise comparison with singleton custom types on JavaScript (https://gleam-lang/gleam/issues/4903) |
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.
Could you remove the link to the issue and write a detailed comment explaining what this code is doing please 🙏
Here's an example a good example of such a comment:
gleam/compiler-core/src/javascript/expression.rs
Lines 1045 to 1071 in 1a5ce62
/// In Gleam, the `&&` operator is short-circuiting, meaning that we can't | |
/// pre-evaluate both sides of it, and use them in the exception that is | |
/// thrown. | |
/// Instead, we need to implement this short-circuiting logic ourself. | |
/// | |
/// If we short-circuit, we must leave the second expression unevaluated, | |
/// and signal that using the `unevaluated` variant, as detailed in the | |
/// exception format. For the first expression, we know it must be `false`, | |
/// otherwise we would have continued by evaluating the second expression. | |
/// | |
/// Similarly, if we do evaluate the second expression and fail, we know | |
/// that the first expression must have evaluated to `true`, and the second | |
/// to `false`. This way, we avoid needing to evaluate either expression | |
/// twice. | |
/// | |
/// The generated code then looks something like this: | |
/// ```javascript | |
/// if (expr1) { | |
/// if (!expr2) { | |
/// <throw exception> | |
/// } | |
/// } else { | |
/// <throw exception> | |
/// } | |
/// ``` | |
/// | |
fn assert_and( |
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.
alright!
} else { | ||
return docvec!["!(", right_doc, " instanceof ", constructor_name, ")"]; | ||
} | ||
} |
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.
Could we extract the duplicated bits to helper methods please 🙏
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.
surely
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.
This hasn't been done yet! The patterns are still duplicated. Could we write something like this?:
if let Some(document) = self.zero_arity_variant_equality(left, right, should_be_equal) {
return document;
}
if let Some(document) = self.zero_arity_variant_equality(right, left, should_be_equal) {
return document;
}
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.
This is still outstanding 🙏
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.
This still hasn't been done
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.
Once again, this still needs to be done
compiler-core/src/javascript/tests/singleton_comparison_optimisation.rs
Outdated
Show resolved
Hide resolved
…ts for the new code
please review |
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.
Very nice! Thank you. I've left a few more comments inline.
let $ = (var0) => { return new Y(var0); }; | ||
let y = $; | ||
if (isEqual(y, (var0) => { return new Y(var0); })) { | ||
if (y instanceof Y) { |
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.
There's a bug here! This has gone from being an equality check against the constructor to being a check if y
is any value produced by that constructor.
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.
oh wait. yea. ill fix it.
self.prelude_equal_call(should_be_equal, left, right) | ||
} | ||
|
||
fn singleton_equal_helper( |
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.
Give this a descriptive name please 🙏
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.
Looks like this still needs to be renamed
} else { | ||
return docvec!["!(", right_doc, " instanceof ", constructor_name, ")"]; | ||
} | ||
} |
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.
This hasn't been done yet! The patterns are still duplicated. Could we write something like this?:
if let Some(document) = self.zero_arity_variant_equality(left, right, should_be_equal) {
return document;
}
if let Some(document) = self.zero_arity_variant_equality(right, left, should_be_equal) {
return document;
}
ClauseGuard::Constant(Constant::Record { | ||
arguments, name, .. | ||
}), | ||
// Check if it's a singleton (no arguments) |
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.
No arguments doesn't mean a singleton, it could be use of the constructor as a value.
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.
ohh yes.
|
||
ClauseGuard::Equals { left, right, .. } => { | ||
if let ( | ||
_, |
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.
This value isn't matched on, so no need to put it in a tuple with the other.
// which supports any shape of data, and so does a lot of extra logic which isn't necessary. | ||
|
||
if let ( | ||
_, |
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.
This value isn't matched on, so no need to put it in a tuple with the other.
} | ||
"#, | ||
); | ||
} |
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.
Could we get tests for these please:
- The variant is defined in another module and used in a qualified fashion (
module.Variant
) - The variant is defined in another module and used in an unqualified fashion (
import module.{Variant}
) - The variant is defined in another module and used in an unqualified fashion while aliased (
import module.{Variant as OtherName}
)
Both in expressions and in case clauses please.
so, i havent been able to come up with a way to add the optimization in the guard clauses. im reverting to all guard clauses having |
import other_module.{A as B}
pub fn func() {
case B {
x if x == B -> True
_ -> False
}
} what should the generated code be? |
We want this optimisation to work for all equality checks, so instanceof |
if something is aliased, for instance, Ok being aliased to Y, |
As far as I can tell, it doesn't change the method of detection at all. Y is still a record with no fields |
but since what should this compile to? import gleam.{Ok as Y}
pub type X {
Ok
}
pub fn func() {
case Y {
y if y == Y -> True
_ -> False
}
} this? let $ = (var0) => { return new Y(var0); };
let y = $;
if (y instanceof Y) {
return true;
} else {
return false;
} |
Nope, Y is not a singleton here. |
im really sorry for asking this but what makes something a singleton? having 0 arity? or what else? |
okay ive figured it out. i just have to clean the code now |
Yes, a singleton as I referred to it in the original issue, though perhaps not the correct term, is a variant of a custom type with no fields, aka 0 arity. |
please review |
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.
A couple notes
self.prelude_equal_call(should_be_equal, left, right) | ||
} | ||
|
||
fn singleton_equal_helper( |
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.
Looks like this still needs to be renamed
if is_var(left) | ||
&& !looks_like_constructor_alias(left) | ||
&& let Some(tag) = ctor_tag(right) | ||
{ |
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.
I don't really understand why this uses different logic from the expression comparison. Could you change singleton_equal_helper
to take a Document
instead of a TypedExpr
, and reuse that code here?
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.
if it is a constructor alias, then we get a false positive.
the result causes code which returns true or false for all values, but in reality, it should not.
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.
normal expression comparison does not need alias checking like for guards.
in guards, the variable can be an alias for the constructor value itself.
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.
What's a constructor alias? I haven't heard that term used before
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.
not sure if that's the exact term.
but i'm referring to any variable that's an alias for a constructor.
for example
let pluh = Ok
so pluh is a "constructor alias". makes ample sense imo.
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.
Not sure what a constructor alias is here, but I agree the logic here is a bit hard to understand.
I'd like to see a similar pattern as I suggested here: https://github.com/gleam-lang/gleam/pull/4951/files#r2337440251
The logic I think should be:
- The pattern is for a variant/variant constructor.
- No arguments have been supplied.
- The type of the pattern is a
Named
type. This could be checked with a newtype.is_named()
method, similar to the existingtype.is_result
method.
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.
okay. ill do it.
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.
I still think this code is too complex. Here are my thoughts:
- I'm not sure why the left-hand-side has to be a variable for this check to take place. You haven't imposed this limitation for regular expression comparison, so it seems odd that it would be required in guards
- I also don't know why you have checked that both sides are not constructor values. Again, this was not done for expression comparison so I don't see why it would be different here
- You've explained what a constructor alias is, but not why it needs to be checked for here. Could you give a code example where this check is needed?
- It is also probably a good idea to do what is suggested for the expressions and make one function for checking whether a comparison is eligible for this optimisation, calling it twice with the different orderings
ClauseGuard::Var { type_, .. } => matches!(&**type_, Type::Fn { .. }), // lambda | ||
_ => false, | ||
} | ||
} |
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.
It's not really clear what these functions do. The names don't seem to properly describe their behaviour. Although, if you follow the comment above, these functions might not be necessary.
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.
yeah. it seems like i wont need these functions if i use Document
instead of TypedExpr
.
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.
ill do the needful
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.
I agree, I don't think these functions belong here. I think they would be best being inlined into the places they are used 🙏
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.
okay sure
ive made the necessary changes |
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.
Looks like a few changes still need to be made
self.prelude_equal_call(false, left, right) | ||
let l = self.guard(left); | ||
let r = self.guard(right); | ||
self.prelude_equal_call(want_equal, l, r) |
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.
Please do not rename these variables. l
and r
are less descriptive names than left
and right
and there is no reason to change them.
} else { | ||
return docvec!["!(", right_doc, " instanceof ", constructor_name, ")"]; | ||
} | ||
} |
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.
This still hasn't been done
if is_var(left) | ||
&& !looks_like_constructor_alias(left) | ||
&& let Some(tag) = ctor_tag(right) | ||
{ |
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.
I still think this code is too complex. Here are my thoughts:
- I'm not sure why the left-hand-side has to be a variable for this check to take place. You haven't imposed this limitation for regular expression comparison, so it seems odd that it would be required in guards
- I also don't know why you have checked that both sides are not constructor values. Again, this was not done for expression comparison so I don't see why it would be different here
- You've explained what a constructor alias is, but not why it needs to be checked for here. Could you give a code example where this check is needed?
- It is also probably a good idea to do what is suggested for the expressions and make one function for checking whether a comparison is eligible for this optimisation, calling it twice with the different orderings
For normal expression context, either of the sides can be any expression, so the optimization triggers for both.
The main intent was to avoid redundant optimizations in cases like
“Constructor alias” cases happen when a variable is pattern-bound to the constructor value itself. for example, import gleam.{Ok as Y}
pub type X {
Ok
}
pub fn func() {
case Y {
y if y == Y -> True
_ -> False
}
} so y == Y should not optimize to
okay, we can refactor it i guess. ill do that |
I don't see why |
i've made the necessary changes. please check |
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.
Still some changes that need to be made
} else { | ||
return docvec!["!(", right_doc, " instanceof ", constructor_name, ")"]; | ||
} | ||
} |
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.
Once again, this still needs to be done
fn eligible_singleton_cmp( | ||
lhs: &TypedClauseGuard, | ||
rhs: &TypedClauseGuard, | ||
) -> Option<EcoString> { |
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.
You haven't really simplified this logic, just refactored it so the code looks slightly different. Could you change it to use the same logic as regular expression comparison please
Thank you both! I think if you focus on that early comment on mine that hasn't been done yet then it'll become clearer how to do the guard side too. https://github.com/gleam-lang/gleam/pull/4951/files#r2336546527 |
i extracted the redundant parts into helper methods. please lmk if i need to do anything else |
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.
Hello! Nice, nearly there!
Could you please use the design I requested in my previous comments 🙏 https://github.com/gleam-lang/gleam/pull/4951/files#r2337440251
So instead of having one call to a handle_singleton_equality
that duplicates the code for left and right, instead of having one function and calling it twice in the equal
method, and the same pattern used for guards too.
The tests with Variant Other(...)
look great! Could you also add versions of these tests that check equality against Other
, to ensure that the optimisation isn't applied when it shouldn't be. Thank you
You'll also need to rebase on main to fix the tests
okay! ill do the needful. thanks a lot |
by the way, if i keep the logic the same for the clause guard and normal expressions, then the alias cases get a false positive.
|
You must be doing the check differently, because |
yeah it's an alias. i need to check for that |
im getting some new "helper" functions in the generated code. i did not change any part of the code which should trigger something like that.
|
is it because of this commit? |
Yes, this is a change from |
okay |
i hope everything is okay this time. please check |
These test are missing 🙏 |
OH! il add them rn. |
… trigger where they shouldnt)
can someone please review this :( |
do I need to add or remove or change something? |
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.
Looking good! A couple small notes
} | ||
} | ||
|
||
fn singleton_equal_helper( |
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.
Can you give this a more descriptive name please?
&& arguments.is_empty() | ||
&& right.type_().is_named() | ||
&& let ClauseGuard::Var { type_, .. } = left | ||
&& !matches!(&**type_, Type::Fn { .. }) |
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.
Can we use the same logic as the expression equality here, where we check the ValueContructor
to see if it has zero arity? I think it would make this easier to read and understand
…also fixed some clippy errors