-
-
Notifications
You must be signed in to change notification settings - Fork 172
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
Record projection by expression #499
Record projection by expression #499
Conversation
56e6862
to
cd1e4f2
Compare
@Gabriel439 I noticed that #493 is quite relevant to this PR. I am thinking of expanding the scope of this work to address the requirement in that issue, though I am not sure if this is okay. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two other things that need to be added:
- How to α-normalize
t.(s)
- How to encode/decode
t.(s)
(i.e. inbinary.md
)
standard/semantics.md
Outdated
@@ -3894,6 +3906,16 @@ You can only select field(s) from the record if they are present: | |||
Γ ⊢ e.{ x } : { x : T } | |||
|
|||
|
|||
Γ ⊢ e :⇥ { ts… } : Kind Γ ⊢ s :⇥ { ss… } : Kind | |||
──────────────────────────────────────────────────── ; ss ⊆ ts | |||
Γ ⊢ e.(s) : { ss… } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that the type of e.(s)
should be the normal form of s
rather than the type of s
. In other words:
Γ ⊢ e :⇥ { ts… } Γ ⊢ s : T s ⇥ { ss… }
─────────────────────────────────────────── ; ss ⊆ ts
Γ ⊢ e.(s) : { ss… }
I also don't think you need the Kind
/Sort
type annotations. The type of the type of each record would be checked along the way as part of type-checking e
and s
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My idea is to make it explicit that, in order to type check e.(s)
, s
should lie in a universe immediately above the one where e
lives. But I am not sure how to express this constraint.
By the way, what is T
in Γ ⊢ s : T
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think of Γ ⊢ s : T
as pseudo-code for:
T <- inferTypeWithContext Γ s
In other words, Γ
and s
are the two inputs to the inferTypeWithContext
function and T
is the output of the function. In this case T
is the inferred type of s
@dingxiangfei2009: I think #493 should probably be handled as part of a separate pull request so that each change can be understood separately in the history |
I would also request that |
@Gabriel439 Speaking of adding a α-normalisation rule, though it is not immediately related, I find the deduction rules for |
@dingxiangfei2009 I may be wrong, but it sounds to me like you haven't seen this: https://github.com/dhall-lang/dhall-lang/blob/master/standard/shift.md . If you have, would you mind being more precise about what you find lacking ? |
@Nadrieril Ah, I see! It has what I want to read about. |
#509 should make it easier to spot which judgments are defined and where in the future. |
…o selector-extension
…o selector-extension
standard/binary.md
Outdated
|
||
encode(t₀) = t₁ encode(T₀) = T₁ | ||
───────────────────────────────── | ||
encode(t₀.(T₀)) = [ 27, t₁, T₁ ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest using 19 instead of 27, since record projection by type can survive β-normalization. For example, this is a well-typed expression that is in normal form:
λ(e : { x : Bool }) → e.({ x : Bool })
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see.
Is there any rule on how these numbers are assigned, for backward compatibility?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned here, we try to assign labels below 24 for syntactic features that survive normalization. Currently we use 0 to 18, except 13 and 17. 13 was used before so for backward-compatibility we want to reuse it as late as possible (see here). I don't know why 17 was skipped, but I'm assuming it is similar to 13. That means that the next free label is 19.
Why do we need a new label at all? The record projection label exists already and there will be no overlap with projection by name so I think we should just use that.
|
@singpolyma: Oh yeah, good point. We should probably use the same label as normal record projection |
@@ -0,0 +1 @@ | |||
{ a = 10, b = Some 10 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing newline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dhall format
automatically removes the new line when I add it.
@@ -0,0 +1 @@ | |||
{=} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing newline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
@@ -0,0 +1 @@ | |||
{ a = 10, b = Some 10, c = [ "text" ]}.({ a : Natural, b : Optional Natural }) : { a : Natural, b : Optional Natural } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type annotation on the end here seems unneeded?
@@ -0,0 +1 @@ | |||
let e = { a = 10, b = "Text" } let s = { a : Natural } in e.(s) : { a : Natural } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be named A
and have a B
part with it -- the let
seems like a lot of extra noise for a unit test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that we should still have a test like this one, even if not necessarily for a unit test. It's important to test that type inference works when the s
is an abstract type since that's a likely case that an implementation might implement incorrectly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great! I think the only thing left is to see if we can have the binary encoding for e.(s)
reuse the same label as for e.x
like @singpolyma. Other than that I don't see anything missing
@@ -0,0 +1 @@ | |||
let e = { a = 10, b = "Text" } let s = { a : Natural } in e.(s) : { a : Natural } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that we should still have a test like this one, even if not necessarily for a unit test. It's important to test that type inference works when the s
is an abstract type since that's a likely case that an implementation might implement incorrectly.
48597d3
to
6185c96
Compare
@singpolyma: Actually, I think this will lead to decoding ambiguity because now `e.(x)` and `e.{ x }` have the same encoding.
Not anymore. Variables always have their index encoded these days, not encoded as bare strings.
|
@singpolyma: Oh yeah, that's right. Ignore my concern, then. |
I think the only thing missing is to add the expected inferred type for |
Signed-off-by: Ding Xiang Fei <dingxiangfei2009@gmail.com>
…o selector-extension
Sorry for late response. Caught with some other business earlier this week. 😅 |
... as standardized in dhall-lang/dhall-lang#499
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there! 🙂
I just implemented this in dhall-lang/dhall-haskell#958 and noticed a few small things along the way
standard/type-inference.md
Outdated
@@ -501,6 +501,13 @@ You can only select field(s) from the record if they are present: | |||
Γ ⊢ e :⇥ { x : T, ts… } Γ ⊢ { x : T, ts… } :⇥ Sort | |||
──────────────────────────────────────────────────── | |||
Γ ⊢ e.{ x } : { x : T } | |||
|
|||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps add some prose here explaining that the e.(t)
syntax lets the user select record fields by type. Currently there isn't any text in the standard explaining this feature
standard/type-inference.md
Outdated
@@ -501,6 +501,13 @@ You can only select field(s) from the record if they are present: | |||
Γ ⊢ e :⇥ { x : T, ts… } Γ ⊢ { x : T, ts… } :⇥ Sort | |||
──────────────────────────────────────────────────── | |||
Γ ⊢ e.{ x } : { x : T } | |||
|
|||
|
|||
Γ ⊢ e :⇥ { ts… } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This judgment needs to somehow enforce that { ts… }
and { ss… }
are syntactically record types. One way to do that is define the judgment via induction (i.e. define a judgment to type-check the case where the two record types are empty and then define a judgment to type-check the case where the two record types are non-empty), like this:
Γ ⊢ e :⇥ {}
Γ ⊢ s : T
s ⇥ {}
────────────────
Γ ⊢ e.(s) : {}
Γ ⊢ e :⇥ { x : T₀, … }
Γ ⊢ s : T
s ⇥ { x : T₁, xs₁… }
Γ ⊢ e.({ xs₁… }) : T
────────────────────────
Γ ⊢ e.(s) : { x : T₁ ,xs₁… }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Gabriel439 For the base case, we can relax the requirement on e
, since it is still sound, in the same universe, to project empty record from any record.
Also, do you think that the following induction step make sense?
Γ ⊢ e :⇥ { x : T₀, ts… }
Γ ⊢ s : T₀
s ⇥ { x : T₀, ss… }
Γ ⊢ e.({ ss… }) : T₁
───────────────────────────
Γ ⊢ e.(s) : { x : T₀, ss… }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The concern is with the induction step that you suggested earlier, in which Γ ⊢ s : T
and Γ ⊢ e.({ xs₁… }) : T
cannot reconcile.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Nadrieril: Oh yeah, the base case doesn't need to require that e
is the empty record. That's a mistake on my part. Good catch!
Also, you're right that the two occurrences of T
in the induction judgement should be distinct. That's another mistake on my part.
@singpolyma: Actually, I did run across an ambiguous encoding caught by the binary serialization round-trip property test. See: https://hydra.dhall-lang.org/build/13281/nixlog/5 The issue is that a projection of an expression which is a built-in: x.(Bool) ... and an expression projecting a quoted field of the same name: x.{ `Bool` } ... share the same encoding. They are both encoded as: [ 10, [ "x", 0 ], "Bool" ] Technically, it's an ill-typed expression, but it still seems wrong to permit two different expressions to be encoded the same way. It might be worth still using a different label just to avoid this potential ambiguity (and also to make it easier to write decoders). |
Alternatively, we could add a "sub-label" to indicate that it's a record projection by type, perhaps by adding a
|
Hmm, ok. My initial reaction is that projection on a builtin is a crazy thing to do. But I suppose it's not impossible that a builtin that maps to a record type could ever be added to the language. If they can't overlap, then I guess I don't feel strongly about label vs sub-label |
@dingxiangfei2009: How about we do this, then: store the projection by type inside of another list so that it visually resembles the syntax:
|
@Gabriel439 sounds like a better plan! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great, thanks @dingxiangfei2009!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for doing this! 🙂
I'll go ahead and merge this since you have all the approvals. Thanks again! 🙂 |
... as standardized in dhall-lang/dhall-lang#499
Close #183, replacing #494.
This PR extends record project by permitting expression normalising to records as the projection selector.
The highlight is that to project fields prescribed by an expression from a record, a round bracket is necessary, eg.,
r.(e)
.