Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a subsumption rule between record types and dictionary types #1977

Merged
merged 5 commits into from
Jul 1, 2024

Conversation

Eckaos
Copy link
Contributor

@Eckaos Eckaos commented Jun 25, 2024

Modified the subsumption function to add the subsumption rule between a record type and a dictionary type.

@Eckaos Eckaos marked this pull request as ready for review June 25, 2024 13:38
Copy link
Member

@yannham yannham left a comment

Choose a reason for hiding this comment

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

It's on the right track! There's one oversight though around the tail of record types. See the comments.

We probably also want to add a bunch of tests.

@@ -2420,7 +2420,29 @@ pub fn subsumption(
checked: UnifType,
) -> Result<(), UnifError> {
let inferred_inst = instantiate_foralls(state, &mut ctxt, inferred, ForallInst::UnifVar);
checked.unify(inferred_inst, state, &ctxt)
match (inferred_inst.clone(), checked.clone()) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
match (inferred_inst.clone(), checked.clone()) {
match (inferred_inst, checked) {

Those early clones are probably not needed. You might need to put a ref in the pattern matching though, as in:

           UnifType::Concrete {
                typ: TypeF::Record(ref rrows),
                ..
            },

typ: TypeF::Dict { type_fields, .. },
..
},
) => rrows.iter().fold(
Copy link
Member

Choose a reason for hiding this comment

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

If you don't build anything valuable in a fold, then you can probably get rid of it and just use a good old for loop:

for row in rrows {
    if  let GenericUnifRecordRowsIteratorItem::Row(a) = row {
        subsumption(state, ctxt.clone(), a.typ.clone(), *type_fields.clone())?;
    }
}

Ok(())

(GenericUnifRecordRowsIteratorItem::Row(a), Ok(_)) => {
subsumption(state, ctxt.clone(), a.typ.clone(), *type_fields.clone())
}
_ => acc,
Copy link
Member

Choose a reason for hiding this comment

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

Now I'm thinking about something that I haven't thought about before. During type inference, we might have open record types, such as {foo : Number, bar : Number; tail} where the tail at the end means that this value can have more fields that we don't know yet.

If we try to subtype that with a dictionary, as in {foo : Number, bar : Number; ?a} <: {_ : Number}, we can't let the ?a be, because it might very much be instantiated with baz : String at some point. So we need to close the tail of the record.

Interestingly, we can put Dyn in the tail currently (instead of ?a) to say that we don't know anything about it, that those are some fields with Dyn type. I wonder if we could extend the type system to add other types, for example {foo : Number, bar : Number; Number} (or {foo : Number, bar : Number; _ : Number}) to say that the tail is unknown but fields must be numbers. That would probably be a better representation.

Anyhow, I'm just thinking out loud. Let's forget that, for now, I don't think we can do better than closing the tail of the record. In practice, this means that we need to also consider the cases:

  • GenericUnifRecordRowsIteratorItem::UnifVar { id, init_level}. In that case we need to unify this var with an empty row. Here is how it's done (note: it's done for enum rows below, but it's exactly the same for record rows; just change any "enum" to "record"):

    // We don't need to perform any variable level checks when unifying a free
    // unification variable with a ground type
    state
    .table
    .assign_erows(id, UnifEnumRows::concrete(EnumRowsF::Empty));

  • GenericUnifRecordRowsIteratorItem::TailConstant(id). In this case we must fail. We can raise a WithConst error - in the future we could have a dedicated error for better messages, but for now, that will do.

@yannham
Copy link
Member

yannham commented Jun 27, 2024

@Eckaos feel free to re-request a review from me when you think it's ready for a new round!

@Eckaos
Copy link
Contributor Author

Eckaos commented Jun 27, 2024

If you think there no need to think about GenericUnifRecordRowsIteratorItem::TailDyn and GenericUnifRecordRowsIteratorItem::TailVar(&'a LocIdent), I think it's ready for a new round ! @yannham

Copy link
Member

@yannham yannham left a comment

Choose a reason for hiding this comment

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

The logic looks good to me.

What's still missing before we can merge:

  • Update the documentation of subsumption, which isn't up to date. It says that we don't perform any form of subtyping, but we do, know. We can also include a bit on why we close the tail when it's a unification variable.
  • Add tests. You can look at core/tests/integration, the README (EDIT: when I say look at the readme, I mean core/tests/integration/README.md, not the root one), and various inputs (in the inputs directory). It's sufficient to just add new test .ncl test files somewhere in core/tests/integration/inputs/* and they'll automatically be picked up in tests.

We should also update the user manual, but that can probably come in a second step (that is, another pull request).

.assign_rrows(id, UnifRecordRows::concrete(RecordRowsF::Empty)),
GenericUnifRecordRowsIteratorItem::TailConstant(id) => {
Err(UnifError::WithConst {
var_kind: VarKindDiscriminant::EnumRows,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
var_kind: VarKindDiscriminant::EnumRows,
var_kind: VarKindDiscriminant::RecordRows,

Copy-paste leftover I guess, but we are unifying record rows, not enum rows 🙂

Comment on lines +2423 to +2433
match (&inferred_inst, &checked) {
(
UnifType::Concrete {
typ: TypeF::Record(rrows),
..
},
UnifType::Concrete {
typ: TypeF::Dict { type_fields, .. },
..
},
) => {
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: in Rust, when you have a match case of the form (that is with only one pattern and a default case)

match foo {
  Pattern => /* .. some code */,
  _ => /* .. default case */,
}

It's more idiomatic to use an if-let. It's exactly the same result but save one level of indentation:

if let Pattern = foo then {
  /* some code */
}
else {
 /* default case */
}

Comment on lines 2439 to 2441
GenericUnifRecordRowsIteratorItem::TailUnifVar { id, .. } => state
.table
.assign_rrows(id, UnifRecordRows::concrete(RecordRowsF::Empty)),
Copy link
Member

Choose a reason for hiding this comment

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

It could be good to also reproduce the comment that was at the other place where this code was taken from, saying that because we are unifying with a ground type, we don't need to care about level and stuff. Usually assign_rrows is a bit low-level and one shouldn't blindly use it directly. Here we can, but for future readers, it's good to say why.

@Eckaos
Copy link
Contributor Author

Eckaos commented Jun 27, 2024

@yannham It must be good !

Copy link
Member

@yannham yannham left a comment

Choose a reason for hiding this comment

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

I think more tests would be welcome, in particular when applying a function such as std.record.insert or std.record.get to a record literal (put in a variable). However, I tested those locally and they work, so let's proceed with this PR and add more compelling tests in a second step.

/// relation).
///
/// The type instantiation corresponds to the zero-ary case of application in the current
/// specification (which is based on [A Quick Look at Impredicativity][quick-look], although we
/// currently don't support impredicative polymorphism).
///
/// In the future, this function might implement a non-trivial subsumption rule.
/// In the future, this function might implement a other non-trivial subsumption rule.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// In the future, this function might implement a other non-trivial subsumption rule.

Comment on lines +2407 to 2408
/// a record/dictionary subtyping is the equality
/// relation).
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// a record/dictionary subtyping is the equality
/// relation).
/// a record/dictionary subtyping is the equality relation).

@yannham yannham enabled auto-merge July 1, 2024 13:24
@yannham yannham added this pull request to the merge queue Jul 1, 2024
Merged via the queue into tweag:master with commit af5d1b5 Jul 1, 2024
4 checks passed
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.

2 participants