Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,8 @@
- Fixed a bug where useless comparison warnings for floats compared literal
strings, claiming for example that `1.0 == 1.` was always false.
([fruno](https://github.com/fruno-bulax/))

- Fixed a bug where pattern variables in case clause guards would incorrectly
shadow outer scope variables in other branches when compiling to JavaScript.
([Elias Haider](https://github.com/EliasDerHai))

14 changes: 9 additions & 5 deletions compiler-core/src/javascript/decision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,20 +584,24 @@ impl<'a> CasePrinter<'_, '_, 'a, '_> {
.iter()
.partition(|(variable, _)| guard_variables.contains(variable));

let check_bindings = self.variables.bindings_ref_doc(&check_bindings);
let check = self.variables.expression_generator.guard(guard);
let if_true = self.inside_new_scope(|this| {
let (check_bindings, check, if_true) = self.inside_new_scope(|this| {
// check_bindings and if_true generation have to be in this scope so that pattern-bound
// variables used in guards don't leak into other case branches (if_false).
let check_bindings = this.variables.bindings_ref_doc(&check_bindings);
let check = this.variables.expression_generator.guard(guard);
// All the other bindings that are not needed by the guard check will
// end up directly in the body of the if clause.
let if_true_bindings = this.variables.bindings_ref_doc(&if_true_bindings);
let if_true_body = this.body_expression(if_true.clause_index);
match if_true_body {
let if_true = match if_true_body {
BodyExpression::Variable(variable) => variable,
BodyExpression::Expressions(if_true_body) => {
join_with_line(if_true_bindings, if_true_body)
}
}
};
(check_bindings, check, if_true)
});

let if_false_body = self.inside_new_scope(|this| this.decision(if_false));

// We can now piece everything together into a case body!
Expand Down
27 changes: 27 additions & 0 deletions compiler-core/src/javascript/tests/case_clause_guards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,3 +576,30 @@ pub fn main() {
"#,
);
}

// https://github.com/gleam-lang/gleam/issues/5094
#[test]
fn guard_pattern_does_not_shadow_outer_scope() {
assert_js!(
r#"
pub type Option(a) {
Some(a)
None
}

pub type Container {
Container(x: Option(Int))
}

pub fn main() {
let x: Option(Int) = Some(42)
case Some(1) {
Some(x) if x < 0 -> Container(None)
_ -> {
Container(x:)
}
}
}
"#,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export function main() {
if (i === 1) {
return true;
} else {
let i$1 = $;
if (i$1 < 2) {
let i = $;
if (i < 2) {
return true;
} else {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export function go(x) {
if (first < 10) {
return first * 2;
} else {
let first$1 = x.head;
return first$1;
let first = x.head;
return first;
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this an error in JavaScript? There is already a first defined in this function

Copy link
Contributor Author

@EliasDerHai EliasDerHai Nov 2, 2025

Choose a reason for hiding this comment

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

No it's not an error in js since let and const are block-scoped.

Shadowing doesn't pollute the outer scope - in contrary to var which is bad practice exactly for that reason.

I ran a quick test on how my change behaves for "double-guards" - eg. :

pub fn main() {
  let x = 42
  case x {
    x if x < 0 -> x
    x if x > 40 -> x * 2
    _ -> 0
  }
}

This previously compiled to:

export function main() {
  let x = 42;
  let x$1 = x;
  if (x$1 < 0) {
    return x;
  } else {
    let x$2 = x;
    if (x$2 > 40) {
      return x$2 * 2;
    } else {
      return 0;
    }
  }
}

with my change compiles to:

export function main() {
  let x = 42;
  let x$1 = x;
  if (x$1 < 0) {
    return x;
  } else {
    let x$1 = x;
    if (x$1 > 40) {
      return x$1 * 2;
    } else {
      return 0;
    }
  }
}

Again we see that x$1 is repeatedly defined (in different scopes). - Fine by js but maybe odd for humans to read. It could very well be though that what we declare as x in gleam are different types. Back-tracing declarations from upper scopes for the purpose of potential reuse doesn't make much sense and is probably quite error-prone though.

Each guard produces a declaration as far as I can see. Since expressions within if-conditions aren't possible we're forced to declare in upper scope. if we shadow vars or 'soft-shadow' with counter increment doesn't matter much or?

Do you suggest putting more effort to avoid variable shadowing in js?
Do you see a way how the change could cause a bug?

Copy link
Member

Choose a reason for hiding this comment

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

Oh yes! Thank you for your thorough explanation. Very silly of me there

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
source: compiler-core/src/javascript/tests/case_clause_guards.rs
expression: "\npub type Option(a) {\n Some(a)\n None\n}\n\npub type Container {\n Container(x: Option(Int))\n}\n\npub fn main() {\n let x: Option(Int) = Some(42)\n case Some(1) {\n Some(x) if x < 0 -> Container(None)\n _ -> {\n Container(x:)\n }\n }\n}\n"
---
----- SOURCE CODE

pub type Option(a) {
Some(a)
None
}

pub type Container {
Container(x: Option(Int))
}

pub fn main() {
let x: Option(Int) = Some(42)
case Some(1) {
Some(x) if x < 0 -> Container(None)
_ -> {
Container(x:)
}
}
}


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

export class Some extends $CustomType {
constructor($0) {
super();
this[0] = $0;
}
}
export const Option$Some = ($0) => new Some($0);
export const Option$isSome = (value) => value instanceof Some;
export const Option$Some$0 = (value) => value[0];

export class None extends $CustomType {}
export const Option$None = () => new None();
export const Option$isNone = (value) => value instanceof None;

export class Container extends $CustomType {
constructor(x) {
super();
this.x = x;
}
}
export const Container$Container = (x) => new Container(x);
export const Container$isContainer = (value) => value instanceof Container;
export const Container$Container$x = (value) => value.x;
export const Container$Container$0 = (value) => value.x;

export function main() {
let x = new Some(42);
let $ = new Some(1);
let x$1 = $[0];
if (x$1 < 0) {
return new Container(new None());
} else {
return new Container(x);
}
}
Loading