Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion compiler-core/src/analyse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ use vec1::Vec1;

use self::imports::Importer;

#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Inferred<T> {
Known(T),
#[default]
Unknown,
}

Expand Down
6 changes: 0 additions & 6 deletions compiler-core/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2473,12 +2473,6 @@ impl<T> BitArraySize<T> {
}
}

impl Default for Inferred<()> {
fn default() -> Self {
Self::Unknown
}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AssignName {
Variable(EcoString),
Expand Down
2 changes: 1 addition & 1 deletion compiler-core/src/erlang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ fn string_inner(value: &str) -> Document<'_> {
.replace_all(value, |caps: &Captures<'_>| {
let slashes = caps.get(1).map_or("", |m| m.as_str());

if slashes.len() % 2 == 0 {
if slashes.len().is_multiple_of(2) {
format!("{slashes}u")
} else {
format!("{slashes}x")
Expand Down
116 changes: 116 additions & 0 deletions compiler-core/src/javascript/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1536,14 +1536,80 @@ impl<'module, 'a> Generator<'module, 'a> {
return docvec![left_doc, operator, right_doc];
}

// For comparison with singleton custom types, ie, one with no fields.
// If you have some code like this
// ```gleam
// pub type Wibble {
// Wibble
// Wobble
// }

// pub fn is_wibble(w: Wibble) -> Bool {
// w == Wibble
// }
// ```
// Instead of `isEqual(w, new Wibble())`, generate `w instanceof Wibble`
// because the first approach needs to construct a new Wibble, and then call the isEqual function,
// 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.

// RHS: singleton record constructor (arity 0)
TypedExpr::Var {
constructor:
ValueConstructor {
variant: ValueConstructorVariant::Record { arity: 0, name, .. },
..
},
..
},
) = (left, right)
{
return self.singleton_equal_helper(left, name, should_be_equal);
}

if let (
// LHS: singleton record constructor (arity 0)
TypedExpr::Var {
constructor:
ValueConstructor {
variant: ValueConstructorVariant::Record { arity: 0, name, .. },
..
},
..
},
_,
) = (left, right)
{
return self.singleton_equal_helper(right, name, should_be_equal);
}
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


// Other types must be compared using structural equality
let left =
self.not_in_tail_position(Some(Ordering::Strict), |this| this.wrap_expression(left));
let right =
self.not_in_tail_position(Some(Ordering::Strict), |this| this.wrap_expression(right));

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

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?

&mut self,
expr: &'a TypedExpr,
name: &EcoString,
should_be_equal: bool,
) -> Document<'a> {
let expr_doc =
self.not_in_tail_position(Some(Ordering::Strict), |this| this.wrap_expression(expr));
let constructor_name = name.to_doc();

if should_be_equal {
docvec![expr_doc, " instanceof ", constructor_name]
} else {
docvec!["!(", expr_doc, " instanceof ", constructor_name, ")"]
}
}

fn equal_with_doc_operands(
&mut self,
left: Document<'a>,
Expand Down Expand Up @@ -1967,12 +2033,62 @@ impl<'module, 'a> Generator<'module, 'a> {
}

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.

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.

) = (&**left, &**right)
&& arguments.is_empty()
{
let left_doc = self.guard(left);
return docvec![left_doc, " instanceof ", name.to_doc()];
}

if let (
ClauseGuard::Constant(Constant::Record {
arguments, name, ..
}),
_,
// Check if it's a singleton (no arguments)
) = (&**left, &**right)
&& arguments.is_empty()
{
let right_doc = self.guard(right);
return docvec![right_doc, " instanceof ", name.to_doc()];
}

let left = self.guard(left);
let right = self.guard(right);
self.prelude_equal_call(true, left, right)
}

ClauseGuard::NotEquals { left, right, .. } => {
if let (
_,
ClauseGuard::Constant(Constant::Record {
arguments, name, ..
}),
) = (&**left, &**right)
&& arguments.is_empty()
{
let left_doc = self.guard(left);
return docvec!["!(", left_doc, " instanceof ", name.to_doc(), ")"];
}

if let (
ClauseGuard::Constant(Constant::Record {
arguments, name, ..
}),
_,
) = (&**left, &**right)
&& arguments.is_empty()
{
let right_doc = self.guard(right);
return docvec!["!(", right_doc, " instanceof ", name.to_doc(), ")"];
}

let left = self.guard(left);
let right = self.guard(right);
self.prelude_equal_call(false, left, right)
Expand Down
119 changes: 119 additions & 0 deletions compiler-core/src/javascript/tests/custom_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -687,3 +687,122 @@ pub type Wibble {
"#
);
}

#[test]
fn singleton_record_equality() {
assert_js!(
r#"
pub type Wibble {
Wibble
Wobble
}

pub fn is_wibble(w: Wibble) -> Bool {
w == Wibble
}
"#,
);
}

#[test]
fn singleton_record_inequality() {
assert_js!(
r#"
pub type Wibble {
Wibble
Wobble
}

pub fn is_not_wibble(w: Wibble) -> Bool {
w != Wibble
}
"#,
);
}

#[test]
fn singleton_record_reverse_order() {
assert_js!(
r#"
pub type Wibble {
Wibble
Wobble
}

pub fn is_wibble_reverse(w: Wibble) -> Bool {
Wibble == w
}
"#,
);
}

#[test]
fn non_singleton_record_equality() {
assert_js!(
r#"
pub type Person {
Person(name: String, age: Int)
}

pub fn same_person(p1: Person, p2: Person) -> Bool {
p1 == p2
}
"#,
);
}

#[test]
fn multiple_singleton_constructors() {
assert_js!(
r#"
pub type Status {
Loading
Success
Error
}

pub fn is_loading(s: Status) -> Bool {
s == Loading
}

pub fn is_success(s: Status) -> Bool {
s == Success
}
"#,
);
}

#[test]
fn mixed_singleton_and_non_singleton() {
assert_js!(
r#"
pub type Result {
Ok(value: Int)
Error
}

pub fn is_error(r: Result) -> Bool {
r == Error
}
"#,
);
}

#[test]
fn singleton_in_case_guard() {
assert_js!(
r#"
pub type State {
Active
Inactive
}

pub fn process(s: State) -> String {
case s {
state if state == Active -> "active"
_ -> "inactive"
}
}
"#,
);
}
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.

Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
---
source: compiler-core/src/javascript/tests/case_clause_guards.rs
assertion_line: 474
expression: "import other_module.{A as B}\npub fn func() {\n case B {\n x if x == B -> True\n _ -> False\n }\n}\n"
snapshot_kind: text
---
----- SOURCE CODE
import other_module.{A as B}
Expand All @@ -17,12 +15,11 @@ pub fn func() {
----- COMPILED JAVASCRIPT
import * as $other_module from "../../package/other_module.mjs";
import { A as B } from "../../package/other_module.mjs";
import { isEqual } from "../gleam.mjs";

export function func() {
let $ = new B();
let x = $;
if (isEqual(x, new B())) {
if (x instanceof B) {
return true;
} else {
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
---
source: compiler-core/src/javascript/tests/case_clause_guards.rs
assertion_line: 489
expression: "import gleam.{Ok as Y}\npub type X {\n Ok\n}\npub fn func() {\n case Y {\n y if y == Y -> True\n _ -> False\n }\n}\n"
snapshot_kind: text
---
----- SOURCE CODE
import gleam.{Ok as Y}
Expand All @@ -19,14 +17,14 @@ pub fn func() {

----- COMPILED JAVASCRIPT
import * as $gleam from "../gleam.mjs";
import { Ok as Y, CustomType as $CustomType, isEqual } from "../gleam.mjs";
import { Ok as Y, CustomType as $CustomType } from "../gleam.mjs";

export class Ok extends $CustomType {}

export function func() {
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.

return true;
} else {
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
source: compiler-core/src/javascript/tests/custom_types.rs
expression: "\npub type Result {\n Ok(value: Int)\n Error\n}\n\npub fn is_error(r: Result) -> Bool {\n r == Error\n}\n"
---
----- SOURCE CODE

pub type Result {
Ok(value: Int)
Error
}

pub fn is_error(r: Result) -> Bool {
r == Error
}


----- COMPILED JAVASCRIPT
import { CustomType as $CustomType } from "../gleam.mjs";

export class Ok extends $CustomType {
constructor(value) {
super();
this.value = value;
}
}

export class Error extends $CustomType {}

export function is_error(r) {
return r instanceof Error;
}
Loading
Loading