Skip to content

Fix unbound variable error when matching on const record subjects#5302

Open
IgorCastejon wants to merge 4 commits intogleam-lang:mainfrom
IgorCastejon:fix/const-record-as-case-subject-bug
Open

Fix unbound variable error when matching on const record subjects#5302
IgorCastejon wants to merge 4 commits intogleam-lang:mainfrom
IgorCastejon:fix/const-record-as-case-subject-bug

Conversation

@IgorCastejon
Copy link
Contributor

Closes #5261.

  • The changes in this PR have been discussed beforehand in an issue
  • The issue for this PR has been linked
  • Tests have been added for new behaviour
  • The changelog has been updated for any user-facing changes

Comment on lines +228 to +235
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);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added this specific block to allow field access of const records (like user.name) to work inside the branch, matching the current behavior for local variables.

However, I noticed that Gleam doesn't do this type refinement for constants globally:
Gleam playground

Since the fix for the runtime crash doesn't strictly require this part (we could just ignore ModuleConstant entirely here), I have two questions:

  1. Should I keep or remove this code?
  2. Should I open a separate issue about global type refinement for constants (or is the current behavior deliberate)?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I'd open a second separate issue for type refinements of constants and not add any code related to refinement in this PR. Makes it easier to review!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This worked in the JavaScript target, but for the Erlang target it would generate an invalid local variable:
Gleam playground

So removing this code now results in a compiler error that would break existing code. Should I still remove it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've created an issue: #5310

Copy link
Member

Choose a reason for hiding this comment

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

Should this be removed now then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've talked with @giacomocavalieri and we agreed the code should be kept! Otherwise, removing it will break variant inference for consts, which is currently possible in the JavaScript target.

Comment on lines +2 to +22
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
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just something of interest I noticed while testing: if a const is defined but unused, it is properly optimized out of the generated JavaScript, but it still seems to have its name reserved in the variable generator.
Gleam playground

@IgorCastejon IgorCastejon force-pushed the fix/const-record-as-case-subject-bug branch from 90fd708 to a071320 Compare January 19, 2026 21:54
Comment on lines +228 to +235
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);
}
Copy link
Member

Choose a reason for hiding this comment

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

Yeah I'd open a second separate issue for type refinements of constants and not add any code related to refinement in this PR. Makes it easier to 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.

Thank you!! I've left some questions inline 🙏

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

Choose a reason for hiding this comment

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

Remove the references to refinement types please

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

Comment on lines +228 to +235
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);
}
Copy link
Member

Choose a reason for hiding this comment

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

Should this be removed now then?

} else {
return wibble.int;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

How come this is generating flow control instead of returning 24 unconditionally like it would with let wibble = Wobble(42)?

Copy link
Contributor Author

@IgorCastejon IgorCastejon Feb 21, 2026

Choose a reason for hiding this comment

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

Sounds like a separate issue, the code was already generated like this before this PR:
Gleam Playground

@lpil lpil marked this pull request as draft February 5, 2026 15:11
@IgorCastejon IgorCastejon force-pushed the fix/const-record-as-case-subject-bug branch from f70a809 to b4a9b70 Compare February 21, 2026 15:39
@IgorCastejon IgorCastejon marked this pull request as ready for review February 21, 2026 16:04
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.

Erlang target: "unbound variable" error when referencing a const record inside a case expression

3 participants