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

Handling of record field names is inconsistent: different for record creation and record update #2968

Closed
lukaszcz opened this issue Aug 22, 2024 · 6 comments · Fixed by #3138
Assignees
Milestone

Comments

@lukaszcz
Copy link
Collaborator

lukaszcz commented Aug 22, 2024

In record creation, e.g.,

let r1 := A in
mkR@{r1 := r1}

the r1 on the right side refers recursively to the field being defined.

In record update, e.g.,

let r1 := A in
r@R{r1 := r1}

the r1 on the right side refers to the r1 defined by the let.

Complete example:

module rec;

type T := A | B;

type R := mkR { rt : T };

r : R := mkR@{ rt := A };

g : T -> T
  | A := B
  | B := A;

f : R -> R
  | r@mkR@{rt} := (r@R{rt := g rt});

main : T := R.rt (f r);

This type-checks and prints B.

We should somehow unify the behaviour.

Possible solutions

  1. Make references in record update recursive by default. I don't like this one, because it's making life harder for the users.
  2. Make zero-argument functions in record creation/update always non-recursive and "proper" functions with multiple args recursive. This seems to match most closely with actual use-cases. In virtually all cases when using a zero-argument name in record creation you don't want it to be recursive. In most cases when you define a proper function you want it to be recursive by default.
  3. Have a different way of indicating that a field defined in record creation/update is recursive: some rec keyword or something like this.

Option 1 seems to make the language most "unified" (the syntax for definitions is everywhere the same), but also most annoying to use the record update/creation.

Options 2, 3 make the syntax less consistent than 1, but seem to align more closely with actual use-cases.

Any more options?

@lukaszcz lukaszcz added this to the 0.6.6 milestone Aug 22, 2024
@lukaszcz
Copy link
Collaborator Author

lukaszcz commented Aug 22, 2024

In general, maybe we should re-think how we handle local recursive definitions and invent some rules that align with actual use-cases but are still not too complex to be confusing.

Because when someone writes e.g.,

let x := x + 1 in ..

they don't want x to be defined recursively by this equation.

I think it's quite annoying and confusing that we treat zero-argument function definitions as recursive by default. Maybe a general exception is reasonable here?

But then we need some way of indicating that we want a recursive definition. In an eager language this is extremely rare for zero-argument definitions, but one could want something like:

let rec x := c (\{z := if | z == 0 := d | else := x})

I think all zero-argument recursive definitions need the terminating annotation anyway. There are no arguments that could decrease, so they always fail the termination checker.

@lukaszcz
Copy link
Collaborator Author

lukaszcz commented Aug 22, 2024

I propose the following general rule. A definition is treated as recursive only if one of the following holds:

  • it has at least one argument on the left, e.g., f (a : Nat) : Nat := ...,
  • it has a clause with at least one pattern, e.g.,
f : Nat -> Nat
  | a := ...
  • it has no arguments and only one zero-pattern clause, but the body is a lambda-abstraction, e.g.,
f : Nat -> Nat := \{a := ...}

Otherwise, the definition is not recursive.

We treat all definitions this way (top-level, let, record creation/update). I think this aligns quite closely with actual use-cases and is not too complex to be obscure.

In a let, we could allow a rec keyword to override this behaviour and indicate that a definitions should be recursive. This will be used extremely rarely and is only for completeness. We don't need to reserve rec as a general keyword - check for it only in let.

Perhaps we might even do without the rec keyword and in the very rare cases when one needs a recursive zero-argument definition simulate it with a dummy argument:

let x' (dummy : Unit) : C := c (\{z := if | z == 0 := d | else := x' dummy});
    x : C := x' unit

@lukaszcz
Copy link
Collaborator Author

This is super-confusing: the inconsistency is also with record puns.

The expression

let r2 := 3 in
mkR@{r1 := r2; r2}

is equivalent to

mkR 3 3

The expression

let r2 := 3 in
mkR@{r1 := r2; r2 := 4}

is equivalent to

mkR 4 4

In the first expression, the r2 on the right is bound by the let, in the second it's bound in the mutually-recursive definition of record fields.

@janmasrovira
Copy link
Collaborator

Summary of what we briefly discussed online

  1. When defining a term we should only bind the name being defined in the body if the definition looks like a function. This needs to be consistent across top definition, record creation/update, let definition. Looking like a function means that either there is at least an argument on the left of the :, or there is at least one guard. This is what @lukaszcz proposed in the first two points. We don't need rec for now and most likely we won't need it in the future.
  2. For record updates, we would access the old value by prefixing the reference by @. I.e.
    let x := 3 in
    r@R{ x := @x + 1; -- `@x` refers to the value of the field `x` in `r` before the update. 
         y := x}      -- `x` refers to the `x` defined in the outside let. 
    
  3. Record puns do not need to change for record creation. They need to be supported for record updates (Allow punning in record updates #2967).

@lukaszcz
Copy link
Collaborator Author

Idea: should we use .x instead of @x to refer to the old value in the record update? I think we're overusing @ and .x seems intuitive: you're qualifying a variable with an implicit scope of the record update.

@lukaszcz
Copy link
Collaborator Author

lukaszcz commented Nov 4, 2024

We agreed to not have a special syntax to refer to the old value, for now.

janmasrovira pushed a commit that referenced this issue Nov 4, 2024
* Closes #2968 
* Implements detection of function-like definitions, which either:
  - have some arguments on the left of `:`, or
  - have at least one clause.
* Only function-like definitions are recursive.
* Non-recursive definitions are not mutually recursive either, and can
be used only after their definition. This necessitates rearranging some
definitions in existing Juvix code.
* Changes the scoping of identifiers in record updates. Now field names
on the right side don't refer to the old values of the record fields but
to identifiers in scope defined outside the record update. To refer to
old values, one needs to explicitly use record projections, e.g.
```
r@Rec{x := Rec.x r}
```
janmasrovira pushed a commit that referenced this issue Nov 4, 2024
* Closes #2968 
* Implements detection of function-like definitions, which either:
  - have some arguments on the left of `:`, or
  - have at least one clause.
* Only function-like definitions are recursive.
* Non-recursive definitions are not mutually recursive either, and can
be used only after their definition. This necessitates rearranging some
definitions in existing Juvix code.
* Changes the scoping of identifiers in record updates. Now field names
on the right side don't refer to the old values of the record fields but
to identifiers in scope defined outside the record update. To refer to
old values, one needs to explicitly use record projections, e.g.
```
r@Rec{x := Rec.x r}
```
janmasrovira pushed a commit that referenced this issue Nov 4, 2024
* Closes #2968 
* Implements detection of function-like definitions, which either:
  - have some arguments on the left of `:`, or
  - have at least one clause.
* Only function-like definitions are recursive.
* Non-recursive definitions are not mutually recursive either, and can
be used only after their definition. This necessitates rearranging some
definitions in existing Juvix code.
* Changes the scoping of identifiers in record updates. Now field names
on the right side don't refer to the old values of the record fields but
to identifiers in scope defined outside the record update. To refer to
old values, one needs to explicitly use record projections, e.g.
```
r@Rec{x := Rec.x r}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants