Skip to content

Conversation

re-masashi
Copy link

…also fixed some clippy errors

Copy link
Member

@lpil lpil left a 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)
Copy link
Member

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:

/// 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(

Copy link
Author

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, ")"];
}
}
Copy link
Member

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 🙏

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

surely

Copy link
Member

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;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still outstanding 🙏

Copy link
Member

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

Copy link
Member

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

@re-masashi
Copy link
Author

please review

Copy link
Member

@lpil lpil left a 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) {
Copy link
Member

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.

Copy link
Author

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(
Copy link
Member

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 🙏

Copy link
Member

@GearsDatapacks GearsDatapacks Sep 15, 2025

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, ")"];
}
}
Copy link
Member

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)
Copy link
Member

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.

Copy link
Author

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 (
_,
Copy link
Member

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 (
_,
Copy link
Member

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.

}
"#,
);
}
Copy link
Member

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.

@re-masashi
Copy link
Author

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 == for now.

@re-masashi
Copy link
Author

re-masashi commented Sep 10, 2025

import other_module.{A as B}
pub fn func() {
  case B {
    x if x == B -> True
    _ -> False
  }
}

what should the generated code be?
if (isEqual(x, new B())) or if (x instanceof B)

@GearsDatapacks
Copy link
Member

We want this optimisation to work for all equality checks, so instanceof

@re-masashi
Copy link
Author

if something is aliased, for instance, Ok being aliased to Y,
then how will i know if Y is a singleton?

@GearsDatapacks
Copy link
Member

As far as I can tell, it doesn't change the method of detection at all. Y is still a record with no fields

@re-masashi
Copy link
Author

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 Ok has a field, shouldnt Y do too?

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;
  }

@GearsDatapacks
Copy link
Member

Nope, Y is not a singleton here.

@re-masashi
Copy link
Author

im really sorry for asking this but what makes something a singleton? having 0 arity? or what else?

@re-masashi
Copy link
Author

okay ive figured it out. i just have to clean the code now

@GearsDatapacks
Copy link
Member

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.

@re-masashi
Copy link
Author

please review

Copy link
Member

@GearsDatapacks GearsDatapacks left a 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(
Copy link
Member

@GearsDatapacks GearsDatapacks Sep 15, 2025

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)
{
Copy link
Member

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?

Copy link
Author

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.

Copy link
Author

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.

Copy link
Member

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

Copy link
Author

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.

Copy link
Member

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:

  1. The pattern is for a variant/variant constructor.
  2. No arguments have been supplied.
  3. The type of the pattern is a Named type. This could be checked with a new type.is_named() method, similar to the existing type.is_result method.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay. ill do it.

Copy link
Member

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,
}
}
Copy link
Member

@GearsDatapacks GearsDatapacks Sep 15, 2025

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.

Copy link
Author

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ill do the needful

Copy link
Member

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 🙏

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay sure

@re-masashi
Copy link
Author

ive made the necessary changes

Copy link
Member

@GearsDatapacks GearsDatapacks left a 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)
Copy link
Member

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, ")"];
}
}
Copy link
Member

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)
{
Copy link
Member

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

@re-masashi
Copy link
Author

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

For normal expression context, either of the sides can be any expression, so the optimization triggers for both.

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

The main intent was to avoid redundant optimizations in cases like Active == Active.
but uhh, if the eligibility function does the right type/arity checks, this explicit double check may be unnecessary, as is the case for expressions. ig removing it makes the logic consistent between guards and expressions.

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?

“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 instanceof; otherwise, every instance would match.

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

okay, we can refactor it i guess. ill do that

@GearsDatapacks
Copy link
Member

I don't see why Wibble == Wibble shouldn't be optimised. There's no point in the code anyway, and new Wibble() instanceof Wibble works fine. It would simplify the code greatly and there are no drawbacks

@re-masashi
Copy link
Author

i've made the necessary changes. please check

Copy link
Member

@GearsDatapacks GearsDatapacks left a 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, ")"];
}
}
Copy link
Member

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> {
Copy link
Member

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

@lpil
Copy link
Member

lpil commented Sep 23, 2025

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

@re-masashi
Copy link
Author

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

Copy link
Member

@lpil lpil left a 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

@re-masashi
Copy link
Author

okay! ill do the needful. thanks a lot

@re-masashi
Copy link
Author

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.

   12    12 │ 
   13    13 │ 
   14    14 │ ----- COMPILED JAVASCRIPT
   15    15 │ import * as $gleam from "../gleam.mjs";
   16       │-import { Ok as Y, CustomType as $CustomType, isEqual } from "../gleam.mjs";
         16 │+import { Ok as Y, CustomType as $CustomType } from "../gleam.mjs";
   17    17 │ 
   18    18 │ export class Ok extends $CustomType {}
   19    19 │ 
   20    20 │ export function func() {
   21    21 │   let $ = (var0) => { return new Y(var0); };
   22    22 │   let y = $;
   23       │-  if (isEqual(y, (var0) => { return new Y(var0); })) {
         23 │+  if (y instanceof Y) {
   24    24 │     return true;
   25    25 │   } else {
   26    26 │     return false;
   27    27 │   }

@GearsDatapacks
Copy link
Member

GearsDatapacks commented Sep 26, 2025

You must be doing the check differently, because Y in that case is not a constructor with 0 arity

@re-masashi
Copy link
Author

You must be doing the check differently, because Y in that case is not a constructor with 0 arity

yeah it's an alias. i need to check for that

@re-masashi
Copy link
Author

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.

   12    12 │ ----- COMPILED JAVASCRIPT
   13    13 │ import { CustomType as $CustomType } from "../gleam.mjs";
   14    14 │ 
   15    15 │ export class Wibble extends $CustomType {}
         16 │+export const Wibble$Wibble = () => new Wibble();
         17 │+export const Wibble$isWibble = (value) => value instanceof Wibble;
   16    18 │ 
   17    19 │ export class Wobble extends $CustomType {}
         20 │+export const Wibble$Wobble = () => new Wobble();
         21 │+export const Wibble$isWobble = (value) => value instanceof Wobble;
   18    22 │ 
   19    23 │ export function is_not_wibble(w) {
   20    24 │   return !(w instanceof Wibble);
   21    25 │ }
   17    17 │     super();
   18    18 │     this.value = value;
   19    19 │   }
   20    20 │ }
         21 │+export const Result$Ok = (value) => new Ok(value);
         22 │+export const Result$isOk = (value) => value instanceof Ok;
         23 │+export const Result$Ok$value = (value) => value.value;
         24 │+export const Result$Ok$0 = (value) => value.value;
   21    25 │ 
   22    26 │ export class Error extends $CustomType {}
         27 │+export const Result$Error = () => new Error();
         28 │+export const Result$isError = (value) => value instanceof Error;
   23    29 │ 
   24    30 │ export function is_error(r) {
   25    31 │   return r instanceof Error;
   26    32 │ }

@re-masashi
Copy link
Author

re-masashi@377f9fa

is it because of this commit?

@GearsDatapacks
Copy link
Member

Yes, this is a change from main which your branch inherited after you rebased. These changes are correct

@re-masashi
Copy link
Author

okay
ill commit the new changes then

@re-masashi
Copy link
Author

i hope everything is okay this time. please check

@lpil
Copy link
Member

lpil commented Sep 27, 2025

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

These test are missing 🙏

@re-masashi
Copy link
Author

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

These test are missing 🙏

OH! il add them rn.
also, i forgot that to format AFTER running clippy. ill fix it rn

@re-masashi
Copy link
Author

can someone please review this :(

@re-masashi
Copy link
Author

do I need to add or remove or change something?

Copy link
Member

@GearsDatapacks GearsDatapacks left a 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(
Copy link
Member

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 { .. })
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants