-
-
Notifications
You must be signed in to change notification settings - Fork 908
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
Conversation
lpil
left a comment
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!
compiler-core/src/javascript/tests/singleton_comparison_optimisation.rs
Outdated
Show resolved
Hide resolved
|
please review |
lpil
left a comment
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
| 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.
|
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 |
GearsDatapacks
left a comment
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 = Okso 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
Namedtype. This could be checked with a newtype.is_named()method, similar to the existingtype.is_resultmethod.
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 |
GearsDatapacks
left a comment
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.
| 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 |
GearsDatapacks
left a comment
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
| 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 |
lpil
left a comment
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. |
|
can someone please review this :( |
|
do I need to add or remove or change something? |
GearsDatapacks
left a comment
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
|
any updates? |
|
There's still a piece of feedback that you haven't addressed yet |
oh yeah ill rename the function but im kindof confused about the "same logic" thing. i hope you dont mind elaborating. because a simple zero arity check doesnt work for the case of aliases. so it cannot the logic cannot be 1:1 but please suggest something. |
|
So currently you use the following check for guards: if let ClauseGuard::Constant(Constant::Record {
arguments, name, ..
}) = right
&& arguments.is_empty()
&& right.type_().is_named()
&& let ClauseGuard::Var { type_, .. } = left
&& !matches!(&**type_, Type::Fn { .. }) {
...
}I'm suggesting instead to use: if let ClauseGuard::Constant(Constant::Record {
record_constructor: Some(constructor)
}) = right
&& let ValueConstructorVariant::Record { arity: 0, .. } = constructor.vaiant {
...
}Which is the same logic as you use for expressions, and makes the check easier to understand in my opinion |
|
ive done that. thanks a ton for the help. do i need to handle rebasing etc now? |
|
fixed the merge conflicts. |
|
can we merge this if its okay :( |
lpil
left a comment
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.
Thank you!! Excellent work!!
Could you update the changelog please 🙏
sure. |
|
okay! did it |
|
I'm not sure why github closed this branch, sorry about that. I must have made a mistake while I was trying to update your branch. I have rebased your changes into main! Thank you very much!!! |
…also fixed some clippy errors