Skip to content

Commit 358f031

Browse files
committed
Check bare row arguments through the shared instantiation relation
1 parent b49877f commit 358f031

7 files changed

Lines changed: 363 additions & 68 deletions

File tree

‎crates/psrs-core/src/tests/instantiation.rs‎

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,167 @@ fn row_evidence_retains_residuals_without_an_arena_node() {
234234
assert_eq!(evidence.constructor(variable), None);
235235
assert_eq!(evidence.row(TypeVariableId(41)).map(|row| row.fields), None);
236236
}
237+
238+
fn row(types: &mut Vec<Type>, fields: &[(&str, TypeId)], tail: TypeId) -> TypeId {
239+
fields.iter().rev().fold(tail, |tail, (label, ty)| {
240+
let id = TypeId(types.len() as u32);
241+
types.push(Type::RowExtend {
242+
label: (*label).into(),
243+
ty: *ty,
244+
tail,
245+
});
246+
id
247+
})
248+
}
249+
250+
#[test]
251+
fn nominal_row_arguments_preserve_checked_residuals_and_label_order_independence() {
252+
let variable = TypeVariableId(40);
253+
let mut types = vec![
254+
Type::Constructor(TypeConstructor::Int),
255+
Type::Constructor(TypeConstructor::Boolean),
256+
Type::RowEmpty,
257+
Type::Variable(variable),
258+
];
259+
let generic_row = row(&mut types, &[("y", TypeId(0))], TypeId(3));
260+
let generic = nominal(&mut types, 1, generic_row);
261+
let concrete_row = row(
262+
&mut types,
263+
&[("a", TypeId(0)), ("b", TypeId(1)), ("y", TypeId(0))],
264+
TypeId(2),
265+
);
266+
let concrete = nominal(&mut types, 1, concrete_row);
267+
let reordered_row = row(
268+
&mut types,
269+
&[("y", TypeId(0)), ("b", TypeId(1)), ("a", TypeId(0))],
270+
TypeId(2),
271+
);
272+
let reordered = nominal(&mut types, 1, reordered_row);
273+
let generic_arrow = arrow(&mut types, generic, generic);
274+
let consistent_arrow = arrow(&mut types, concrete, reordered);
275+
let different_row = row(
276+
&mut types,
277+
&[("y", TypeId(0)), ("b", TypeId(0)), ("a", TypeId(0))],
278+
TypeId(2),
279+
);
280+
let different = nominal(&mut types, 1, different_row);
281+
let inconsistent_arrow = arrow(&mut types, concrete, different);
282+
let missing_row = row(&mut types, &[("a", TypeId(0))], TypeId(2));
283+
let missing = nominal(&mut types, 1, missing_row);
284+
let duplicate_row = row(&mut types, &[("y", TypeId(0)), ("y", TypeId(0))], TypeId(2));
285+
let duplicate = nominal(&mut types, 1, duplicate_row);
286+
let module = bare(types);
287+
let evidence = module
288+
.checked_instantiation(generic_arrow, &[variable], consistent_arrow)
289+
.unwrap();
290+
let mut residual = evidence.row(variable).unwrap();
291+
residual.fields.sort_by(|left, right| left.0.cmp(&right.0));
292+
assert_eq!(
293+
residual.fields,
294+
vec![("a".into(), TypeId(0)), ("b".into(), TypeId(1))]
295+
);
296+
assert_eq!(residual.tail, None);
297+
assert!(
298+
module
299+
.checked_instantiation(concrete, &[], reordered)
300+
.is_some()
301+
);
302+
assert!(
303+
module
304+
.checked_instantiation(generic_arrow, &[variable], inconsistent_arrow)
305+
.is_none()
306+
);
307+
assert!(
308+
module
309+
.checked_instantiation(generic, &[], concrete)
310+
.is_none(),
311+
"rigid tails cannot absorb fields"
312+
);
313+
assert!(
314+
module
315+
.checked_instantiation(generic, &[variable], missing)
316+
.is_none()
317+
);
318+
let duplicate_evidence = module
319+
.checked_instantiation(generic, &[variable], duplicate)
320+
.unwrap();
321+
assert_eq!(
322+
duplicate_evidence.row(variable).unwrap().fields,
323+
vec![("y".into(), TypeId(0))]
324+
);
325+
}
326+
327+
#[test]
328+
fn nominal_row_arguments_preserve_duplicate_label_occurrence_order() {
329+
let variable = TypeVariableId(41);
330+
let mut types = vec![
331+
Type::Constructor(TypeConstructor::Int),
332+
Type::Constructor(TypeConstructor::Boolean),
333+
Type::RowEmpty,
334+
Type::Variable(variable),
335+
];
336+
let scheme_row = row(&mut types, &[("a", TypeId(0)), ("a", TypeId(1))], TypeId(3));
337+
let scheme = nominal(&mut types, 1, scheme_row);
338+
let matching_row = row(
339+
&mut types,
340+
&[("z", TypeId(0)), ("a", TypeId(0)), ("a", TypeId(1))],
341+
TypeId(2),
342+
);
343+
let matching = nominal(&mut types, 1, matching_row);
344+
let reversed_row = row(
345+
&mut types,
346+
&[("a", TypeId(1)), ("a", TypeId(0)), ("z", TypeId(0))],
347+
TypeId(2),
348+
);
349+
let reversed = nominal(&mut types, 1, reversed_row);
350+
let module = bare(types);
351+
let evidence = module
352+
.checked_instantiation(scheme, &[variable], matching)
353+
.unwrap();
354+
assert_eq!(
355+
evidence.row(variable).unwrap().fields,
356+
vec![("z".into(), TypeId(0))]
357+
);
358+
assert!(
359+
module
360+
.checked_instantiation(scheme, &[variable], reversed)
361+
.is_none()
362+
);
363+
}
364+
365+
#[test]
366+
fn repeated_residual_fields_use_the_checked_relation_for_nested_row_arguments() {
367+
let variable = TypeVariableId(42);
368+
let mut types = vec![
369+
Type::Constructor(TypeConstructor::Int),
370+
Type::Constructor(TypeConstructor::Boolean),
371+
Type::RowEmpty,
372+
Type::Variable(variable),
373+
];
374+
let nested_left = row(&mut types, &[("a", TypeId(0)), ("b", TypeId(1))], TypeId(2));
375+
let nested_left = nominal(&mut types, 2, nested_left);
376+
let nested_right = row(&mut types, &[("b", TypeId(1)), ("a", TypeId(0))], TypeId(2));
377+
let nested_right = nominal(&mut types, 2, nested_right);
378+
let scheme_row = row(&mut types, &[("y", TypeId(0))], TypeId(3));
379+
let scheme = nominal(&mut types, 1, scheme_row);
380+
let scheme = arrow(&mut types, scheme, scheme);
381+
let domain_row = row(
382+
&mut types,
383+
&[("x", nested_left), ("y", TypeId(0))],
384+
TypeId(2),
385+
);
386+
let domain = nominal(&mut types, 1, domain_row);
387+
let codomain_row = row(
388+
&mut types,
389+
&[("y", TypeId(0)), ("x", nested_right)],
390+
TypeId(2),
391+
);
392+
let codomain = nominal(&mut types, 1, codomain_row);
393+
let instance = arrow(&mut types, domain, codomain);
394+
let module = bare(types);
395+
assert!(
396+
module
397+
.checked_instantiation(scheme, &[variable], instance)
398+
.is_some()
399+
);
400+
}

‎crates/psrs-core/src/verify/types/matching/invariant.rs‎

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -164,22 +164,8 @@ impl TypeMatcher<'_> {
164164
)
165165
}
166166
}
167-
(Type::RowEmpty, Type::RowEmpty) => true,
168-
(
169-
Type::RowExtend {
170-
label: left_label,
171-
ty: left_ty,
172-
tail: left_tail,
173-
},
174-
Type::RowExtend {
175-
label: right_label,
176-
ty: right_ty,
177-
tail: right_tail,
178-
},
179-
) => {
180-
left_label == right_label
181-
&& self.relate(*left_ty, *right_ty, Variance::Invariant, false)
182-
&& self.relate(*left_tail, *right_tail, Variance::Invariant, false)
167+
(Type::RowEmpty | Type::RowExtend { .. }, Type::RowEmpty | Type::RowExtend { .. }) => {
168+
self.relate_rows(source, target, Variance::Invariant)
183169
}
184170
_ => false,
185171
};

‎crates/psrs-core/src/verify/types/matching/mod.rs‎

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -363,22 +363,8 @@ impl TypeMatcher<'_> {
363363
self.relate(actual, expected, Variance::Invariant, false)
364364
}
365365
}
366-
(Type::RowEmpty, Type::RowEmpty) => true,
367-
(
368-
Type::RowExtend {
369-
label: actual_label,
370-
ty: actual_ty,
371-
tail: actual_tail,
372-
},
373-
Type::RowExtend {
374-
label: expected_label,
375-
ty: expected_ty,
376-
tail: expected_tail,
377-
},
378-
) => {
379-
actual_label == expected_label
380-
&& self.relate(*actual_ty, *expected_ty, Variance::Subsumption, true)
381-
&& self.relate(*actual_tail, *expected_tail, Variance::Invariant, false)
366+
(Type::RowEmpty | Type::RowExtend { .. }, Type::RowEmpty | Type::RowExtend { .. }) => {
367+
self.relate_rows(actual, expected, Variance::Invariant)
382368
}
383369
_ => false,
384370
};

‎crates/psrs-core/src/verify/types/matching/rows.rs‎

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use super::super::types_compatible;
21
use super::helpers::collect_free_variables;
32
use super::{TypeMatcher, Variance};
43
use crate::{Type, TypeId, record_row};
@@ -31,30 +30,68 @@ impl TypeMatcher<'_> {
3130
) else {
3231
return false;
3332
};
33+
for row in [actual_row, expected_row] {
34+
let Some((fields, _)) = self.flatten_row(row) else {
35+
return false;
36+
};
37+
let mut labels = HashSet::new();
38+
if fields.iter().any(|(label, _)| !labels.insert(label)) {
39+
return false;
40+
}
41+
}
42+
self.relate_rows(actual_row, expected_row, variance)
43+
}
44+
45+
/// Bare rows in nominal arguments use the same checked residual relation as
46+
/// records. The caller owns variance; nominal row arguments are invariant.
47+
pub(super) fn relate_rows(
48+
&mut self,
49+
actual: TypeId,
50+
expected: TypeId,
51+
variance: Variance,
52+
) -> bool {
3453
let (Some((actual_fields, actual_tail)), Some((expected_fields, expected_tail))) =
35-
(self.flatten_row(actual_row), self.flatten_row(expected_row))
54+
(self.flatten_row(actual), self.flatten_row(expected))
3655
else {
3756
return false;
3857
};
39-
let Some(mut expected_fields) = index_fields(expected_fields) else {
40-
return false;
41-
};
58+
let mut expected_fields = expected_fields;
59+
let mut actual_fields = actual_fields;
60+
// Preserve duplicate-label occurrence order while ignoring the order
61+
// of distinct labels. Bare rows may contain duplicates (e.g. Union).
62+
actual_fields.sort_by(|left, right| left.0.cmp(&right.0));
63+
expected_fields.sort_by(|left, right| left.0.cmp(&right.0));
64+
let (mut actual_index, mut expected_index) = (0, 0);
4265
let mut actual_rest = Vec::new();
43-
let mut seen = HashSet::new();
44-
for (label, actual_ty) in actual_fields {
45-
if !seen.insert(label.clone()) {
46-
return false;
47-
}
48-
if let Some(expected_ty) = expected_fields.remove(&label) {
49-
let instantiate = variance == Variance::Subsumption;
50-
if !self.relate(actual_ty, expected_ty, variance, instantiate) {
51-
return false;
66+
let mut expected_rest = Vec::new();
67+
while actual_index < actual_fields.len() && expected_index < expected_fields.len() {
68+
let actual_field = &actual_fields[actual_index];
69+
let expected_field = &expected_fields[expected_index];
70+
match actual_field.0.cmp(&expected_field.0) {
71+
std::cmp::Ordering::Equal => {
72+
if !self.relate(
73+
actual_field.1,
74+
expected_field.1,
75+
variance,
76+
variance == Variance::Subsumption,
77+
) {
78+
return false;
79+
}
80+
actual_index += 1;
81+
expected_index += 1;
82+
}
83+
std::cmp::Ordering::Less => {
84+
actual_rest.push(actual_field.clone());
85+
actual_index += 1;
86+
}
87+
std::cmp::Ordering::Greater => {
88+
expected_rest.push(expected_field.clone());
89+
expected_index += 1;
5290
}
53-
} else {
54-
actual_rest.push((label, actual_ty));
5591
}
5692
}
57-
let expected_rest = expected_fields.into_iter().collect::<Vec<_>>();
93+
actual_rest.extend(actual_fields[actual_index..].iter().cloned());
94+
expected_rest.extend(expected_fields[expected_index..].iter().cloned());
5895
self.finish_row(actual_rest, actual_tail, expected_rest, expected_tail)
5996
}
6097

@@ -189,15 +226,7 @@ impl TypeMatcher<'_> {
189226
}
190227

191228
fn types_equal(&mut self, left: TypeId, right: TypeId) -> bool {
192-
let mut alpha = self.alpha.clone();
193-
types_compatible(left, right, self.module, &mut HashSet::new(), &mut alpha)
194-
&& types_compatible(
195-
right,
196-
left,
197-
self.module,
198-
&mut HashSet::new(),
199-
&mut self.alpha.clone(),
200-
)
229+
self.relate(left, right, Variance::Invariant, false)
201230
}
202231

203232
fn flatten_row(&self, mut row: TypeId) -> Option<RowShape> {
@@ -257,16 +286,6 @@ impl TypeMatcher<'_> {
257286
}
258287
}
259288

260-
fn index_fields(fields: Vec<(String, TypeId)>) -> Option<HashMap<String, TypeId>> {
261-
let mut indexed = HashMap::new();
262-
for (label, ty) in fields {
263-
if indexed.insert(label, ty).is_some() {
264-
return None;
265-
}
266-
}
267-
Some(indexed)
268-
}
269-
270289
fn same_labels(left: &[(String, TypeId)], right: &[(String, TypeId)]) -> bool {
271290
let mut left = left.to_vec();
272291
let mut right = right.to_vec();

‎docs/design/frontend/type-system/rows-and-records.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,16 @@ Row equality is independent of source label order; matched fields have equal che
103103

104104
P5 receives kind-checked row expressions and resolved record operations from HIR. It supplies type equations and evidence to [type inference](type-inference.md) and emits verified THIR. Row normalization and the primitive row relations are internal to P5 and share the kind and evidence contracts of [kinds](kinds.md) and [classes](classes-and-evidence.md); a later stage receives only checked row types. The backend chooses record and variant representation only after Core lowering.
105105

106+
Core checks bare rows inside nominal type arguments with the same label-based
107+
row relation as record rows. Nominal arguments stay invariant: fields must have
108+
matching types, repeated bare-row labels retain their occurrence order, closed
109+
rows must have the same label occurrences, and rigid tails cannot
110+
absorb extra fields. Only quantified flexible tails receive checked residual
111+
substitutions. Those residuals remain in instantiation evidence even when no
112+
existing type-arena node represents them; later local-row materialization uses
113+
that evidence rather than reconstructing the match.
114+
115+
106116
## Open questions and future work
107117

108118
Use official tests to pin down duplicate-label diagnostics, record-update edge cases, and primitive row improvement order; [primitives](prim.md) owns the rules those tests exercise. [DEC-04](../../../decision/DEC-04-official-test-suite-roadmap.md) tracks implementation coverage.

0 commit comments

Comments
 (0)