diff --git a/CHANGELOG.md b/CHANGELOG.md index bb07c3c7dfb..b24ffd449bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,3 +115,7 @@ - Fixed a bug where renaming would not work properly if there was an error in target file. ([Surya Rose](https://github.com/GearsDatapacks)) + +- Fixed a bug where referencing a constant record inside a case branch matching + on that constant would generate invalid code on the Erlang target. + ([Igor Castejón](https://github.com/IgorCastejon)) \ No newline at end of file diff --git a/compiler-core/src/erlang/tests/case.rs b/compiler-core/src/erlang/tests/case.rs index 13a58dc01d9..d9d5380e531 100644 --- a/compiler-core/src/erlang/tests/case.rs +++ b/compiler-core/src/erlang/tests/case.rs @@ -131,3 +131,93 @@ pub fn main(x) { "#, ); } + +#[test] +fn local_variable_as_case_subject_with_type_refinement_allows_field_access_inside_branch() { + assert_erl!( + r#" +pub type User { + User(name: String, age: Int) + Guest +} + +pub fn main() { + let user = User("Gleam", 42) + + case user { + User(..) -> user.name + Guest -> "Guest" + } +} +"# + ); +} + +#[test] +fn local_variable_as_case_subject_shadows_const() { + assert_erl!( + r#" +pub type Wibble { + Wibble + Wobble(int: Int) +} + +const wibble = Wibble + +pub fn main() { + echo wibble + // This 'wibble' shadows the const + let wibble = Wobble(42) + case wibble { + // This matches the local variable, not the const + Wobble(_) -> wibble + } +} +"# + ) +} + +// https://github.com/gleam-lang/gleam/issues/5261 +#[test] +fn case_with_record_const_as_subject_with_record_constructor_clause_and_referencing_same_const_inside_clause_consequence() + { + assert_erl!( + r#" +pub type Wibble { + Wibble + Wobble(int: Int) +} + +const wibble = Wibble + +pub fn main() { + case wibble { + Wobble(_) -> wibble + wobble -> wibble + } +} +"# + ); +} + +// https://github.com/gleam-lang/gleam/issues/5261 +#[test] +fn const_as_case_subject_with_type_refinement_allows_field_access_inside_branch() { + assert_erl!( + r#" +pub type Wibble { + Wibble + Wobble(int: Int) +} + +const wibble = Wobble(42) + +pub fn main() { + case wibble { + Wibble -> 24 + Wobble(_) -> wibble.int + } +} +"# + ) +} diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__case__case_with_record_const_as_subject_with_record_constructor_clause_and_referencing_same_const_inside_clause_consequence.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__case__case_with_record_const_as_subject_with_record_constructor_clause_and_referencing_same_const_inside_clause_consequence.snap new file mode 100644 index 00000000000..f1458492ef6 --- /dev/null +++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__case__case_with_record_const_as_subject_with_record_constructor_clause_and_referencing_same_const_inside_clause_consequence.snap @@ -0,0 +1,40 @@ +--- +source: compiler-core/src/erlang/tests/case.rs +expression: "\npub type Wibble {\n Wibble\n Wobble(int: Int)\n}\n\nconst wibble = Wibble\n\npub fn main() {\n case wibble {\n Wobble(_) -> wibble\n wobble -> wibble\n }\n}\n" +--- +----- SOURCE CODE + +pub type Wibble { + Wibble + Wobble(int: Int) +} + +const wibble = Wibble + +pub fn main() { + case wibble { + Wobble(_) -> wibble + wobble -> wibble + } +} + + +----- COMPILED ERLANG +-module(my@mod). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "project/test/my/mod.gleam"). +-export([main/0]). +-export_type([wibble/0]). + +-type wibble() :: wibble | {wobble, integer()}. + +-file("project/test/my/mod.gleam", 9). +-spec main() -> wibble(). +main() -> + case wibble of + {wobble, _} -> + wibble; + + Wobble -> + wibble + end. diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__case__const_as_case_subject_with_type_refinement_allows_field_access_inside_branch.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__case__const_as_case_subject_with_type_refinement_allows_field_access_inside_branch.snap new file mode 100644 index 00000000000..94cd5a0f42e --- /dev/null +++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__case__const_as_case_subject_with_type_refinement_allows_field_access_inside_branch.snap @@ -0,0 +1,40 @@ +--- +source: compiler-core/src/erlang/tests/case.rs +expression: "\npub type Wibble {\n Wibble\n Wobble(int: Int)\n}\n\nconst wibble = Wobble(42)\n\npub fn main() {\n case wibble {\n Wibble -> 24\n Wobble(_) -> wibble.int\n }\n}\n" +--- +----- SOURCE CODE + +pub type Wibble { + Wibble + Wobble(int: Int) +} + +const wibble = Wobble(42) + +pub fn main() { + case wibble { + Wibble -> 24 + Wobble(_) -> wibble.int + } +} + + +----- COMPILED ERLANG +-module(my@mod). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "project/test/my/mod.gleam"). +-export([main/0]). +-export_type([wibble/0]). + +-type wibble() :: wibble | {wobble, integer()}. + +-file("project/test/my/mod.gleam", 9). +-spec main() -> integer(). +main() -> + case {wobble, 42} of + wibble -> + 24; + + {wobble, _} -> + erlang:element(2, {wobble, 42}) + end. diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__case__local_variable_as_case_subject_shadows_const.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__case__local_variable_as_case_subject_shadows_const.snap new file mode 100644 index 00000000000..b9eb7310c35 --- /dev/null +++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__case__local_variable_as_case_subject_shadows_const.snap @@ -0,0 +1,44 @@ +--- +source: compiler-core/src/erlang/tests/case.rs +expression: "\npub type Wibble {\n Wibble\n Wobble(int: Int)\n}\n\nconst wibble = Wibble\n\npub fn main() {\n echo wibble\n // This 'wibble' shadows the const\n let wibble = Wobble(42)\n case wibble {\n // This matches the local variable, not the const\n Wobble(_) -> wibble\n }\n}\n" +--- +----- SOURCE CODE + +pub type Wibble { + Wibble + Wobble(int: Int) +} + +const wibble = Wibble + +pub fn main() { + echo wibble + // This 'wibble' shadows the const + let wibble = Wobble(42) + case wibble { + // This matches the local variable, not the const + Wobble(_) -> wibble + } +} + + +----- COMPILED ERLANG +-module(my@mod). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "project/test/my/mod.gleam"). +-export([main/0]). +-export_type([wibble/0]). + +-type wibble() :: wibble | {wobble, integer()}. + +-file("project/test/my/mod.gleam", 9). +-spec main() -> wibble(). +main() -> + echo(wibble, nil, 10), + Wibble = {wobble, 42}, + case Wibble of + {wobble, _} -> + Wibble + end. + +% ...omitted code from `templates/echo.erl`... diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__case__local_variable_as_case_subject_with_type_refinement_allows_field_access_inside_branch.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__case__local_variable_as_case_subject_with_type_refinement_allows_field_access_inside_branch.snap new file mode 100644 index 00000000000..ba6ab41c7ba --- /dev/null +++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__case__local_variable_as_case_subject_with_type_refinement_allows_field_access_inside_branch.snap @@ -0,0 +1,41 @@ +--- +source: compiler-core/src/erlang/tests/case.rs +expression: "\npub type User {\n User(name: String, age: Int)\n Guest\n}\n\npub fn main() {\n let user = User(\"Gleam\", 42)\n \n case user {\n User(..) -> user.name\n Guest -> \"Guest\"\n }\n}\n" +--- +----- SOURCE CODE + +pub type User { + User(name: String, age: Int) + Guest +} + +pub fn main() { + let user = User("Gleam", 42) + + case user { + User(..) -> user.name + Guest -> "Guest" + } +} + + +----- COMPILED ERLANG +-module(my@mod). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "project/test/my/mod.gleam"). +-export([main/0]). +-export_type([user/0]). + +-type user() :: {user, binary(), integer()} | guest. + +-file("project/test/my/mod.gleam", 7). +-spec main() -> binary(). +main() -> + User = {user, <<"Gleam"/utf8>>, 42}, + case User of + {user, _, _} -> + erlang:element(2, User); + + guest -> + <<"Guest"/utf8>> + end. diff --git a/compiler-core/src/javascript/tests/case.rs b/compiler-core/src/javascript/tests/case.rs index 1cfdb47b3fd..4ceaa4927f2 100644 --- a/compiler-core/src/javascript/tests/case.rs +++ b/compiler-core/src/javascript/tests/case.rs @@ -887,3 +887,93 @@ pub fn main() { "# ) } + +#[test] +fn local_variable_as_case_subject_with_type_refinement_allows_field_access_inside_branch() { + assert_js!( + r#" +pub type User { + User(name: String, age: Int) + Guest +} + +pub fn main() { + let user = User("Gleam", 42) + + case user { + User(..) -> user.name + Guest -> "Guest" + } +} +"# + ); +} + +#[test] +fn local_variable_as_case_subject_shadows_const() { + assert_js!( + r#" +pub type Wibble { + Wibble + Wobble(int: Int) +} + +const wibble = Wibble + +pub fn main() { + echo wibble + // This 'wibble' shadows the const + let wibble = Wobble(42) + case wibble { + // This matches the local variable, not the const + Wobble(_) -> wibble + } +} +"# + ) +} + +// https://github.com/gleam-lang/gleam/issues/5261 +#[test] +fn case_with_record_const_as_subject_with_record_constructor_clause_and_referencing_same_const_inside_clause_consequence() + { + assert_js!( + r#" +pub type Wibble { + Wibble + Wobble(int: Int) +} + +const wibble = Wibble + +pub fn main() { + case wibble { + Wobble(_) -> wibble + wobble -> wibble + } +} +"# + ); +} + +// https://github.com/gleam-lang/gleam/issues/5261 +#[test] +fn const_as_case_subject_with_type_refinement_allows_field_access_inside_branch() { + assert_js!( + r#" +pub type Wibble { + Wibble + Wobble(int: Int) +} + +const wibble = Wobble(42) + +pub fn main() { + case wibble { + Wibble -> 24 + Wobble(_) -> wibble.int + } +} +"# + ) +} diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__case_with_record_const_as_subject_with_record_constructor_clause_and_referencing_same_const_inside_clause_consequence.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__case_with_record_const_as_subject_with_record_constructor_clause_and_referencing_same_const_inside_clause_consequence.snap new file mode 100644 index 00000000000..7021bc88a3a --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__case_with_record_const_as_subject_with_record_constructor_clause_and_referencing_same_const_inside_clause_consequence.snap @@ -0,0 +1,50 @@ +--- +source: compiler-core/src/javascript/tests/case.rs +expression: "\npub type Wibble {\n Wibble\n Wobble(int: Int)\n}\n\nconst wibble = Wibble\n\npub fn main() {\n case wibble {\n Wobble(_) -> wibble\n wobble -> wibble\n }\n}\n" +--- +----- SOURCE CODE + +pub type Wibble { + Wibble + Wobble(int: Int) +} + +const wibble = Wibble + +pub fn main() { + case wibble { + Wobble(_) -> wibble + wobble -> wibble + } +} + + +----- COMPILED JAVASCRIPT +import { CustomType as $CustomType } from "../gleam.mjs"; + +export class Wibble extends $CustomType {} +export const Wibble$Wibble = () => new Wibble(); +export const Wibble$isWibble = (value) => value instanceof Wibble; + +export class Wobble extends $CustomType { + constructor(int) { + super(); + this.int = int; + } +} +export const Wibble$Wobble = (int) => new Wobble(int); +export const Wibble$isWobble = (value) => value instanceof Wobble; +export const Wibble$Wobble$int = (value) => value.int; +export const Wibble$Wobble$0 = (value) => value.int; + +const wibble = /* @__PURE__ */ new Wibble(); + +export function main() { + let $ = wibble; + if ($ instanceof Wobble) { + return wibble; + } else { + let wobble = $; + return wibble; + } +} diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__const_as_case_subject_with_type_refinement_allows_field_access_inside_branch.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__const_as_case_subject_with_type_refinement_allows_field_access_inside_branch.snap new file mode 100644 index 00000000000..a758444bc61 --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__const_as_case_subject_with_type_refinement_allows_field_access_inside_branch.snap @@ -0,0 +1,49 @@ +--- +source: compiler-core/src/javascript/tests/case.rs +expression: "\npub type Wibble {\n Wibble\n Wobble(int: Int)\n}\n\nconst wibble = Wobble(42)\n\npub fn main() {\n case wibble {\n Wibble -> 24\n Wobble(_) -> wibble.int\n }\n}\n" +--- +----- SOURCE CODE + +pub type Wibble { + Wibble + Wobble(int: Int) +} + +const wibble = Wobble(42) + +pub fn main() { + case wibble { + Wibble -> 24 + Wobble(_) -> wibble.int + } +} + + +----- COMPILED JAVASCRIPT +import { CustomType as $CustomType } from "../gleam.mjs"; + +export class Wibble extends $CustomType {} +export const Wibble$Wibble = () => new Wibble(); +export const Wibble$isWibble = (value) => value instanceof Wibble; + +export class Wobble extends $CustomType { + constructor(int) { + super(); + this.int = int; + } +} +export const Wibble$Wobble = (int) => new Wobble(int); +export const Wibble$isWobble = (value) => value instanceof Wobble; +export const Wibble$Wobble$int = (value) => value.int; +export const Wibble$Wobble$0 = (value) => value.int; + +const wibble = /* @__PURE__ */ new Wobble(42); + +export function main() { + let $ = wibble; + if ($ instanceof Wibble) { + return 24; + } else { + return wibble.int; + } +} diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__local_variable_as_case_subject_shadows_const.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__local_variable_as_case_subject_shadows_const.snap new file mode 100644 index 00000000000..34e994d2cf9 --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__local_variable_as_case_subject_shadows_const.snap @@ -0,0 +1,61 @@ +--- +source: compiler-core/src/javascript/tests/case.rs +expression: "\npub type Wibble {\n Wibble\n Wobble(int: Int)\n}\n\nconst wibble = Wibble\n\npub fn main() {\n echo wibble\n // This 'wibble' shadows the const\n let wibble = Wobble(42)\n case wibble {\n // This matches the local variable, not the const\n Wobble(_) -> wibble\n }\n}\n" +--- +----- SOURCE CODE + +pub type Wibble { + Wibble + Wobble(int: Int) +} + +const wibble = Wibble + +pub fn main() { + echo wibble + // This 'wibble' shadows the const + let wibble = Wobble(42) + case wibble { + // This matches the local variable, not the const + Wobble(_) -> wibble + } +} + + +----- COMPILED JAVASCRIPT +import * as $stdlib$dict from "../../gleam_stdlib/gleam/dict.mjs"; +import { + Empty as $Empty, + NonEmpty as $NonEmpty, + CustomType as $CustomType, + bitArraySlice, + bitArraySliceToInt, + BitArray as $BitArray, + List as $List, + UtfCodepoint as $UtfCodepoint, +} from "../gleam.mjs"; + +export class Wibble extends $CustomType {} +export const Wibble$Wibble = () => new Wibble(); +export const Wibble$isWibble = (value) => value instanceof Wibble; + +export class Wobble extends $CustomType { + constructor(int) { + super(); + this.int = int; + } +} +export const Wibble$Wobble = (int) => new Wobble(int); +export const Wibble$isWobble = (value) => value instanceof Wobble; +export const Wibble$Wobble$int = (value) => value.int; +export const Wibble$Wobble$0 = (value) => value.int; + +const wibble = /* @__PURE__ */ new Wibble(); + +export function main() { + echo(wibble, undefined, "src/module.gleam", 10); + let wibble$1 = new Wobble(42); + return wibble$1; +} + +// ...omitted code from `templates/echo.mjs`... diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__local_variable_as_case_subject_with_type_refinement_allows_field_access_inside_branch.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__local_variable_as_case_subject_with_type_refinement_allows_field_access_inside_branch.snap new file mode 100644 index 00000000000..0fc81971882 --- /dev/null +++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case__local_variable_as_case_subject_with_type_refinement_allows_field_access_inside_branch.snap @@ -0,0 +1,46 @@ +--- +source: compiler-core/src/javascript/tests/case.rs +expression: "\npub type User {\n User(name: String, age: Int)\n Guest\n}\n\npub fn main() {\n let user = User(\"Gleam\", 42)\n \n case user {\n User(..) -> user.name\n Guest -> \"Guest\"\n }\n}\n" +--- +----- SOURCE CODE + +pub type User { + User(name: String, age: Int) + Guest +} + +pub fn main() { + let user = User("Gleam", 42) + + case user { + User(..) -> user.name + Guest -> "Guest" + } +} + + +----- COMPILED JAVASCRIPT +import { CustomType as $CustomType } from "../gleam.mjs"; + +export class User extends $CustomType { + constructor(name, age) { + super(); + this.name = name; + this.age = age; + } +} +export const User$User = (name, age) => new User(name, age); +export const User$isUser = (value) => value instanceof User; +export const User$User$name = (value) => value.name; +export const User$User$0 = (value) => value.name; +export const User$User$age = (value) => value.age; +export const User$User$1 = (value) => value.age; + +export class Guest extends $CustomType {} +export const User$Guest = () => new Guest(); +export const User$isGuest = (value) => value instanceof Guest; + +export function main() { + let user = new User("Gleam", 42); + return user.name; +} diff --git a/compiler-core/src/type_/pattern.rs b/compiler-core/src/type_/pattern.rs index 1647c7e5d6b..800bd993b18 100644 --- a/compiler-core/src/type_/pattern.rs +++ b/compiler-core/src/type_/pattern.rs @@ -215,20 +215,27 @@ impl<'a, 'b> PatternTyper<'a, 'b> { .inferred_variant_variables .insert(name.clone(), variant_index); - let origin = match &variable.variant { - ValueConstructorVariant::LocalVariable { origin, .. } => origin.clone(), - ValueConstructorVariant::ModuleConstant { .. } - | ValueConstructorVariant::ModuleFn { .. } - | ValueConstructorVariant::Record { .. } => VariableOrigin::generated(), - }; - - // This variable is only inferred in this branch of the case expression - self.environment.insert_local_variable( - name.clone(), - variable.definition_location().span, - origin, - type_, - ); + match &variable.variant { + ValueConstructorVariant::LocalVariable { origin, .. } => { + // This variable is only inferred in this branch of the case expression + self.environment.insert_local_variable( + name.clone(), + variable.definition_location().span, + origin.clone(), + type_, + ); + } + ValueConstructorVariant::ModuleConstant { .. } => { + // If the variable is a constant we shadow it with the refined type. + // We cannot convert it to a local variable as that would cause the + // code generator to look for a runtime variable that doesn't exist. + let mut refined_constant = variable.clone(); + refined_constant.type_ = type_; + let _ = self.environment.scope.insert(name, refined_constant); + } + ValueConstructorVariant::ModuleFn { .. } + | ValueConstructorVariant::Record { .. } => {} + } } PatternMode::Alternative(_) => {