Skip to content

Commit 3eaed3a

Browse files
abs0lutylpil
authored andcommitted
fix(type-checker): fix handling of unlabelled fields in const record updates
1 parent 77ef77d commit 3eaed3a

3 files changed

Lines changed: 82 additions & 33 deletions

File tree

compiler-core/src/type_/expression.rs

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4034,11 +4034,16 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
40344034
return self.new_invalid_constant(location);
40354035
}
40364036

4037-
let mut remaining_fields = field_map.fields.clone();
4038-
let explicit_arguments_count = arguments.len();
4037+
// Emit warning if no fields are being overridden
4038+
if arguments.is_empty() {
4039+
self.problems
4040+
.warning(Warning::NoFieldsRecordUpdate { location });
4041+
}
4042+
4043+
let mut implicit_labelled_arguments = field_map.fields.clone();
4044+
let mut update_argument_indices = HashSet::new();
40394045

4040-
// Type-check explicit override arguments
4041-
let mut typed_overrides = Vec::new();
4046+
let mut final_arguments = base_arguments;
40424047
for argument in arguments {
40434048
if argument.uses_label_shorthand() {
40444049
self.track_feature_usage(
@@ -4050,7 +4055,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
40504055
let label = &argument.label;
40514056
let typed_value = self.infer_const(&None, argument.value);
40524057

4053-
let Some(index) = remaining_fields.remove(label) else {
4058+
let Some(index) = implicit_labelled_arguments.remove(label) else {
40544059
if field_map.fields.contains_key(label) {
40554060
self.problems.error(Error::DuplicateArgument {
40564061
location: argument.location,
@@ -4069,7 +4074,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
40694074
return self.new_invalid_constant(location);
40704075
};
40714076

4072-
// Type check: the override value must match the field type
4077+
// Record update argument value must match the field type
40734078
if let Some(expected_type) = field_types.get(index as usize)
40744079
&& let Err(error) = unify(expected_type.clone(), typed_value.type_())
40754080
{
@@ -4078,49 +4083,57 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
40784083
return self.new_invalid_constant(location);
40794084
}
40804085

4081-
typed_overrides.push(CallArg {
4086+
let _ = update_argument_indices.insert(index as usize);
4087+
4088+
*final_arguments
4089+
.get_mut(index as usize)
4090+
.expect("Index out of bounds") = CallArg {
40824091
label: Some(label.clone()),
40834092
value: typed_value,
40844093
location: argument.location,
40854094
implicit: None,
4086-
});
4087-
}
4088-
4089-
// Emit warning if no fields are being overridden
4090-
if explicit_arguments_count == 0 {
4091-
self.problems
4092-
.warning(Warning::NoFieldsRecordUpdate { location });
4095+
};
40934096
}
40944097

40954098
// Emit warning if all fields are being overriden
4096-
if remaining_fields.is_empty() {
4099+
if implicit_labelled_arguments.is_empty() {
40974100
self.problems
40984101
.warning(Warning::AllFieldsRecordUpdate { location });
40994102
}
41004103

4101-
// Merge: start with base record arguments, override with explicit
4102-
// update arguments
4103-
let mut final_arguments = base_arguments;
4104-
for override_argument in typed_overrides {
4105-
if let Some(label) = &override_argument.label
4106-
&& let Some(&index) = field_map.fields.get(label)
4107-
&& (index as usize) < final_arguments.len()
4108-
{
4109-
*final_arguments
4110-
.get_mut(index as usize)
4111-
.expect("Index out of bounds") = override_argument;
4104+
// Check that fields implicitly overridden (including unlabelled ones) have compatible types.
4105+
for (index, field_arg) in final_arguments.iter().enumerate() {
4106+
// Skip fields that were record update arguments, as they've already been type-checked above
4107+
if update_argument_indices.contains(&index) {
4108+
continue;
41124109
}
4113-
}
41144110

4115-
// Check that implicit fields (fields copied from base) have compatible types
4116-
for (_label, index) in remaining_fields.iter() {
4117-
if let Some(field_arg) = final_arguments.get(*index as usize)
4118-
&& let Some(expected_field_type) = field_types.get(*index as usize)
4111+
if let Some(expected_field_type) = field_types.get(index)
41194112
&& let Err(unify_error) =
41204113
unify(expected_field_type.clone(), field_arg.value.type_())
41214114
{
4122-
self.problems
4123-
.error(convert_unify_error(unify_error, location));
4115+
let field = field_map
4116+
.fields
4117+
.iter()
4118+
.find(|(_, i)| **i == index as u32)
4119+
.map(|(name, _)| RecordField::Labelled(name.clone()))
4120+
.unwrap_or_else(|| RecordField::Unlabelled(index as u32));
4121+
4122+
self.problems.error(match unify_error {
4123+
UnifyError::CouldNotUnify {
4124+
expected, given, ..
4125+
} => Error::UnsafeRecordUpdate {
4126+
location: record.location,
4127+
reason: UnsafeRecordUpdateReason::IncompatibleFieldTypes {
4128+
constructed_variant: expected_type.clone(),
4129+
record_variant: typed_record_type.clone(),
4130+
expected_field_type: expected,
4131+
record_field_type: given,
4132+
field,
4133+
},
4134+
},
4135+
_ => convert_unify_error(unify_error, location),
4136+
});
41244137
return self.new_invalid_constant(location);
41254138
}
41264139
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
source: compiler-core/src/type_/tests.rs
3+
expression: "pub type Wibble(a) {\n Wibble(a, b: Int, c: a)\n }\n\n const w = Wibble(1, 2, 3)\n const w2 = Wibble(..w, c: False)"
4+
---
5+
----- SOURCE CODE
6+
pub type Wibble(a) {
7+
Wibble(a, b: Int, c: a)
8+
}
9+
10+
const w = Wibble(1, 2, 3)
11+
const w2 = Wibble(..w, c: False)
12+
13+
----- ERROR
14+
error: Incomplete record update
15+
┌─ /src/one/two.gleam:6:29
16+
17+
6const w2 = Wibble(..w, c: False)
18+
^ This is a `Wibble(Int)`
19+
20+
The 1st field of this value is a `Int`, but the arguments given to the
21+
record update indicate that it should be a `Bool`.
22+
23+
Note: Unlabelled fields cannot be updated in a record update, so either add
24+
a label or use a record constructor.

compiler-core/src/type_/tests.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2480,6 +2480,18 @@ fn const_record_update_generic_respecialization() {
24802480
);
24812481
}
24822482

2483+
#[test]
2484+
fn generic_unlabelled_field_in_updated_const_record_wrong_type() {
2485+
assert_module_error!(
2486+
"pub type Wibble(a) {
2487+
Wibble(a, b: Int, c: a)
2488+
}
2489+
2490+
const w = Wibble(1, 2, 3)
2491+
const w2 = Wibble(..w, c: False)"
2492+
);
2493+
}
2494+
24832495
#[test]
24842496
fn module_constant_functions() {
24852497
assert_module_infer!(

0 commit comments

Comments
 (0)