-
Notifications
You must be signed in to change notification settings - Fork 834
Spread operator { ...expr }, { ...ty } for records
#18927
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
base: main
Are you sure you want to change the base?
Conversation
❗ Release notes requiredCaution No release notes found for the changed paths (see table below). Please make sure to add an entry with an informative description of the change as well as link to this pull request, issue and language suggestion if applicable. Release notes for this repository are based on Keep A Changelog format. The following format is recommended for this repository:
If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request. You can open this PR in browser to add release notes: open in github.dev
|
|
Amazing work! I was literally asking @edgarfgp a few days ago if we had something like |
|
@brianrourkeboll Great to see work starting in this direction! |
e66b7a6 to
c61dbf4
Compare
6dd4f0b to
d181c97
Compare
d181c97 to
b43fc1d
Compare
b43fc1d to
46998d6
Compare
brianrourkeboll
left a comment
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.
Ha, these can go back.
tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableReferenceTypesTests.fs
Outdated
Show resolved
Hide resolved
tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/Unmanaged.fs
Outdated
Show resolved
Hide resolved
tests/FSharp.Compiler.ComponentTests/Conformance/Constraints/Unmanaged.fs
Outdated
Show resolved
Hide resolved
| // It seems very likely that there's a better/existing way of doing this. | ||
| let spreadSrcTy = | ||
| tryAppTy g spreadSrcTy | ||
| |> ValueOption.map (fun (tcref, tinst) -> | ||
| let tinst = | ||
| tinst | ||
| |> List.map (fun ty -> | ||
| tryDestTyparTy g ty | ||
| |> ValueOption.map (fun typar -> | ||
| let typars, _, _ = FreshenAndFixupTypars g m TyparRigidity.Flexible [] [] [typar] | ||
| mkTyparTy (List.head typars)) | ||
| |> ValueOption.orElseWith (fun () -> | ||
| let tryDestMeasureTy g ty = | ||
| match stripTyEqns g ty with | ||
| | TType_measure m -> ValueSome m | ||
| | _ -> ValueNone | ||
|
|
||
| tryDestMeasureTy g ty | ||
| |> ValueOption.bind (function Measure.Var typar -> ValueSome typar | _ -> ValueNone) | ||
| |> ValueOption.map (fun typar -> | ||
| let typars, _, _ = FreshenAndFixupTypars g m TyparRigidity.Flexible [] [] [typar] | ||
| TType_measure (Measure.Var (List.head typars)))) | ||
| |> ValueOption.defaultValue ty) | ||
|
|
||
| TType_app (tcref, tinst, g.knownWithoutNull)) | ||
| |> ValueOption.defaultValue spreadSrcTy |
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 must be a better way of freshening up the type parameter here.
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.
Cannot this follow what "FreshenMethInfo" does, but on the level of appTy instead?
Doing a match on tryAppTy result, and then FreshenAppTy (a new function added to constraint solver). Working on the original declared typars of the tyconRef (like FreshenTypeInst g range0 (tcref.Typars range0)
I think the measure part is not needed, at least I have not seen it being used elsewhere (the type argument will then be a ttype_var pointing to a measure imo, instead of a direct ttype_measure - so it will get freshened up as well).
After you get freshtypars from FreshenTypeInst .. tcref.Typars ;; SolveTyparsEqualTypes from constraint solver will set the linkage between your new freshened typars and the original tinst.
| | Spread of spread: SynExprSpread * blockSeparator: BlockSeparator option | ||
|
|
||
| [<NoEquality; NoComparison>] | ||
| type SynExprAnonRecordField = SynExprAnonRecordField of fieldName: SynLongIdent * equalsRange: range option * expr: SynExpr * range: range | ||
|
|
||
| [<NoEquality; NoComparison; RequireQualifiedAccess>] | ||
| type SynExprAnonRecordFieldOrSpread = | ||
| | Field of field: SynExprAnonRecordField * blockSeparator: BlockSeparator option |
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.
Could you please unify the placement of "blockSeparator" across record expr and anon record expr?
Right now:
field has it at SynExprRecordField
Spread for records has it at SynExprRecordFieldOrSpread
Anons have it at both cases in SynExprAnonRecordFieldOrSpread
If you do that, it might also end up unifying what SynExprAnonRecordField need to be what SynExprRecordField has.
| range: range * | ||
| trivia: SynMemberDefnAutoPropertyTrivia | ||
|
|
||
| | Spread of spread: SynExprSpread * range: range |
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.
Is this needed?
What use case does it cover now?
(Was it created for experiments with interface spreads etc? )
| || walkExprs ( | ||
| List.choose | ||
| (function | ||
| | SynExprAnonRecordFieldOrSpread.Field(SynExprAnonRecordField(_, _, e, _), _) -> Some e | ||
| | SynExprAnonRecordFieldOrSpread.Spread _ -> None (* TODO. *) ) | ||
| flds | ||
| ) |
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.
| || walkExprs ( | |
| List.choose | |
| (function | |
| | SynExprAnonRecordFieldOrSpread.Field(SynExprAnonRecordField(_, _, e, _), _) -> Some e | |
| | SynExprAnonRecordFieldOrSpread.Spread _ -> None (* TODO. *) ) | |
| flds | |
| ) | |
| || walkExprs ( | |
| List.map | |
| (function | |
| | SynExprAnonRecordFieldOrSpread.Field(SynExprAnonRecordField(_, _, e, _), _) -> e | |
| | SynExprAnonRecordFieldOrSpread.Spread (expr=e) -> e ) | |
| flds | |
| ) |
| SynMatchClause(addOrPat pat, whenExpr, resultExpr, range, debugPoint, trivia) | ||
| :: restClauses | ||
|
|
||
| let (|SynFields|) (synFieldsAndSpreads: SynFieldOrSpread list) = |
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 have seen at a few places within collection builders that this was used and did not trigger a warning.
Maybe it should be a lot more explicit (like "FieldsWithoutSpreads"), especially when used at places without exhaustive matching.
| | SynExprRecordField((synLongIdent, _), mEquals, Some e, _, _) when orig.IsSome -> Some(synLongIdent, mEquals, e) // copy-and-update, long identifier signifies nesting | ||
| | SynExprRecordField((SynLongIdent([ _id ], _, _) as synLongIdent, _), mEquals, Some e, _, _) -> Some(synLongIdent, mEquals, e) // record construction, long identifier not valid | ||
| | SynExprRecordField((synLongIdent, _), mEquals, None, _, _) -> Some(synLongIdent, mEquals, arbExpr ("anonField", synLongIdent.Range)) | ||
| | SynExprRecordFieldOrSpread.Field (SynExprRecordField((synLongIdent, _), mEquals, Some e, m, sep)) when orig.IsSome -> |
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.
Would the suggested changes in SyntaxTree lead to making this block (and the one 20 lines below) unnecessary?
| let tinst = | ||
| tinst | ||
| |> List.map (fun ty -> | ||
| tryDestTyparTy g ty | ||
| |> ValueOption.map (fun typar -> | ||
| let typars, _, _ = FreshenAndFixupTypars g m TyparRigidity.Flexible [] [] [typar] | ||
| mkTyparTy (List.head typars)) | ||
| |> ValueOption.orElseWith (fun () -> | ||
| let tryDestMeasureTy g ty = | ||
| match stripTyEqns g ty with | ||
| | TType_measure m -> ValueSome m | ||
| | _ -> ValueNone | ||
|
|
||
| tryDestMeasureTy g ty | ||
| |> ValueOption.bind (function Measure.Var typar -> ValueSome typar | _ -> ValueNone) | ||
| |> ValueOption.map (fun typar -> | ||
| let typars, _, _ = FreshenAndFixupTypars g m TyparRigidity.Flexible [] [] [typar] | ||
| TType_measure (Measure.Var (List.head typars)))) | ||
| |> ValueOption.defaultValue ty) | ||
|
|
||
| TType_app (tcref, tinst, g.knownWithoutNull)) |
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.
| let tinst = | |
| tinst | |
| |> List.map (fun ty -> | |
| tryDestTyparTy g ty | |
| |> ValueOption.map (fun typar -> | |
| let typars, _, _ = FreshenAndFixupTypars g m TyparRigidity.Flexible [] [] [typar] | |
| mkTyparTy (List.head typars)) | |
| |> ValueOption.orElseWith (fun () -> | |
| let tryDestMeasureTy g ty = | |
| match stripTyEqns g ty with | |
| | TType_measure m -> ValueSome m | |
| | _ -> ValueNone | |
| tryDestMeasureTy g ty | |
| |> ValueOption.bind (function Measure.Var typar -> ValueSome typar | _ -> ValueNone) | |
| |> ValueOption.map (fun typar -> | |
| let typars, _, _ = FreshenAndFixupTypars g m TyparRigidity.Flexible [] [] [typar] | |
| TType_measure (Measure.Var (List.head typars)))) | |
| |> ValueOption.defaultValue ty) | |
| TType_app (tcref, tinst, g.knownWithoutNull)) | |
| let newTints = FreshenTypeInst g range0 (tcref.Typars range0) | |
| SolveTyparsEqualTypes csenv 0 m NoTrace tinst tinst | |
| TType_app (tcref, newTints, g.knownWithoutNull)) |
This should be the main calls, but of course outside of constraint solver this will need some ceremony for the call
| |> List.map (fun fieldPat -> | ||
| let (|Last|) = List.last | ||
| match fieldPat with | ||
| | NamePatPairField (fieldName = SynLongIdent (id = [fieldId])) |
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.
"Last" will cover the single-item list as well.
| | SynTypeDefnSimpleRepr.Union(unionCases = unionCases) -> yield! List.collect visitSynUnionCase unionCases | ||
| | SynTypeDefnSimpleRepr.Enum(cases = cases) -> yield! List.collect visitSynEnumCase cases | ||
| | SynTypeDefnSimpleRepr.Record(recordFields = recordFields) -> yield! List.collect visitSynField recordFields | ||
| | SynTypeDefnSimpleRepr.Record(recordFieldsAndSpreads = SynFields recordFields) -> |
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 skips spreads and only does fields for content processing.
For spreads, it should visitSynType of the spread to build an edge for graph processing (IIRC what this is for)
| | SynTypeDefnSimpleRepr.Union(unionCases = unionCases) -> yield! List.collect visitSynUnionCase unionCases | ||
| | SynTypeDefnSimpleRepr.Enum(cases = cases) -> yield! List.collect visitSynEnumCase cases | ||
| | SynTypeDefnSimpleRepr.Record(recordFields = recordFields) -> yield! List.collect visitSynField recordFields | ||
| | SynTypeDefnSimpleRepr.Record(recordFieldsAndSpreads = SynFields recordFields) -> |
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.
Also here, SynFields AP drops spreads.
This is for sig files (do spreads have any special treatment in sig files?)
| | Some contextLid, SynExprRecordField(fieldName = lid, _) :: _ -> contextLid.Range = lid.Range | ||
| | Some contextLid, SynExprRecordFieldOrSpread.Field(SynExprRecordField(fieldName = lid, _)) :: _ -> | ||
| contextLid.Range = lid.Range | ||
| // TODO: spreads. |
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.
spreads should have "false" for firstField, it is used to affect completion before type checker knows target type of the record creation expression.
Spreads should perhaps have a new RecordContext.Spread completion context ?
Existing instances of records and anons in scope could be offered first.
|
|
||
| /// Helper used to check record expressions and record patterns | ||
| let BuildFieldMap (cenv: cenv) env isPartial ty (flds: ((Ident list * Ident) * 'T) list) m = | ||
| let BuildFieldMap (cenv: cenv) env isPartial ty (flds: ExplicitOrSpread<(Ident list * Ident) * 'Explicit, (Ident list * Ident) * 'Spread> list) m = |
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 file has grown by a lot and pieces of the logic are fairly spread specific.
Can you imagine extracting the biggest blocks into a separate file, with passing in mutually resursive functions as a callback (like TcExprFlex) ?
| let spreadSrcExpr, tpenv = TcExprFlex cenv flex false (NewInferenceType g) env tpenv expr | ||
| let tyOfSpreadSrcExpr = tyOfExpr g spreadSrcExpr | ||
|
|
||
| if isRecdTy g tyOfSpreadSrcExpr || isAnonRecdTy g tyOfSpreadSrcExpr then |
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.
In case of g.checkNullness, it should only do so for nun-nullable record expressions (nullnessOfTy function)
|
|
||
| let flex = false | ||
| let spreadSrcExpr, _ = TcExprFlex cenv flex false (NewInferenceType g) env tpenv expr | ||
| let tyOfSpreadSrcExpr = tyOfExpr g spreadSrcExpr |
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 same here - should reject nullable tyOfSpreadSrcExpr if it is a withNull recdTy
| // ---------------------------------------------------------------------------------- | ||
| // ---------------------------------------------------------------------------------- | ||
| // TODO: Collect spread src exprs so we don't re-typecheck them? | ||
| // TODO: It would be better if we could _try_ to typecheck fields and undo if they |
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.
Is there a scenario, where this causes troubles?
Even in case of duplicates, they get a new inference type variable allocated during type checking, I did not find a source of possible clashes in type inference.
| | Item.RecdField fieldInfo :: fieldsFromSpread -> | ||
| let fieldExpr = mkRecdFieldGetViaExprAddr (spreadSrcAddrExpr, fieldInfo.RecdFieldRef, fieldInfo.TypeInst, m) | ||
| let fieldId = fieldInfo.RecdFieldRef.RecdField.Id | ||
| ignore dedupedSortedFieldTys |
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.
?
| [<NoEquality; NoComparison; DefaultAugmentation(false)>] | ||
| type R3 = { ...R1; ...R2 } | ||
| [<NoEquality; NoComparison; DefaultAugmentation(false)>] | ||
| type R4 = { ...R2; ...R1 } |
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 am not sure if the RFC or discussion covered it, but it just occurred to me that by "flattening" the fields of srcTy into tgTy, this fully bypasses static init.
Both static do / static let at the srcTy, or any module-level logic.
I cannot imagine any normal code in need of this, but it should get a mention in the design and being called out as one of the few symbol usages that do not trigger static init.
| @@ -0,0 +1,71 @@ | |||
| module EmittedIL.NominalRecordExpressionSpreads | |||
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.
Maybe I missed it, but I did not find IL tests with structness, most relevant at expression spreads with anons and regular records.
(also a 1:1 spread for structifying an existing anon).
Can you please add some?
| /// let r = { A = 3; ...b; C = true } | ||
| [<NoEquality; NoComparison; RequireQualifiedAccess>] | ||
| type SynExprRecordFieldOrSpread = | ||
| | Field of field: SynExprRecordField |
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.
Can the field here ever have expr=None?
T-Gro
left a comment
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 is excellent work @brianrourkeboll .
I like the level of detail in the PR's description and the structure of tests reading like a specification, this really helps understanding the topic.
The feature is well designed and covers a lot of edge cases, my reviewer comments are rather minor and I am not strongly attached to any of them.
After thinking about a summary, those points stand out in no particular order:
-
Spreads in signature files, Yes/no - in either case, positive/negative tests should demonstrate what happens.
-
Affect on graph type checking might be missing due to the ActivePattern usage which drops spreads. In general I would be more careful OR a lot more explicit in saying that AP drops info.
-
I believe that SyntaxTree is very close in unifying representations for key data structures around records and anons, and doing that will also simplify mappings done in the parser and then also allow extracting helpers for CheckExpressions. I am not sure if this is @edgarfgp was planning earlier - this can definitely go in separately if Edgar has a plan already
-
I would like to see IL + "run" test for combinations of structness involved.
-
It is clear from the implementation, but maybe not immediately clear from an un-involved user's perspective. Type spreading bypasses static init (e.g.
static do,static let- also in generic contexts) and module-level init of the srcTy. It should deserve a spec update by listing it as a special case at https://github.com/fsharp/fslang-spec/blob/716386d9ab611f631e2cf42ba742de4e444beb11/spec/program-structure-and-execution.md#execution-of-static-initializers -
A lot of code was added to CheckExpressions and much of the logic are clear business rules related to spreads. It would be great if we can think on what would be needed to extract the biggest blocks into a dedicated file.
(especially when foreseeing further growth)
| run | ||
| f | ||
| xs | ||
| (SynExprRecordFieldOrSpread.Field(SynExprRecordField(name, mEquals, value, fieldRange, m)) | ||
| :: acc) |
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.
| run | |
| f | |
| xs | |
| (SynExprRecordFieldOrSpread.Field(SynExprRecordField(name, mEquals, value, fieldRange, m)) | |
| :: acc) | |
| let field = SynExprRecordFieldOrSpread.Field(SynExprRecordField(name, mEquals, value, fieldRange, m) | |
| run f xs (field :: acc) |
This was likely done by Fantomas, but it does not read very well.
Martin521
left a comment
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 is really great to see. Thanks!
But please, please update the RFC before merging this one.
I don't think it needs much text, perhaps even just the test cases from the PR post.
And if possible the related spec changes.
@Martin521 I agree. |
Description
Add support for the spread operator
...in record types and expressions (nominal and anonymous).This PR is meant to begin probing the "spread operator for objects" space — especially the set algebra and associated mechanics — while leaving room for implementing more of the scenarios outlined in fsharp/fslang-suggestions#1253 later. For example, should this prove viable, I would expect one of the next additions to be support for spreading non-records into records, i.e., mapping regular class/struct/interface properties/fields to record fields; this PR explicitly disallows that to ensure that we are free to add it later.
Checklist
withoutin parser? I had this originally but later removed it. I am unsurewithoutis really needed or worth the complexity — but if we want to keep our options open, I will add it back in.Feature overview
I hope that the tests can serve as a reasonable overview of the feature and its behavior. If you see any glaring omissions, or if the tests are unclear or incomplete, please let me know. (I see that I still need to add tests for coercions/conversions…)
Parsing & error recovery
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 46 to 69 in 574e6b1
Record type spreads: set algebra
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 72 to 225 in 574e6b1
Record type spreads: accessibility
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 227 to 244 in 574e6b1
Record type spreads: mutability
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 246 to 261 in 574e6b1
Record type spreads: generic type parameters
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 263 to 435 in 574e6b1
Record type spreads: non-record source (not allowed)
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 437 to 534 in 574e6b1
Record type spreads: recursion (cycles not allowed)
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 536 to 646 in 574e6b1
Anonymous record expression spreads: set algebra
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 649 to 831 in 574e6b1
Anonymous record expression spreads: accessibility
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 833 to 850 in 574e6b1
Anonymous record expression spreads: mutability
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 852 to 874 in 574e6b1
Anonymous record expression spreads: generic type parameters
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 876 to 935 in 574e6b1
Anonymous record expression spreads: non-record-source (not currently allowed)
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 937 to 1101 in 574e6b1
Anonymous record expression spreads: non-field members on record sources are ignored
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 1103 to 1148 in 574e6b1
Anonymous record expression spreads: effects
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 1150 to 1172 in 574e6b1
Nominal record expression spreads: set algebra
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 1231 to 1391 in 574e6b1
Nominal record expression spreads: accessibility
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 1393 to 1411 in 574e6b1
Nominal record expression spreads: non-recourd source (not currently allowed)
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 1413 to 1547 in 574e6b1
Nominal record expression spreads: non-field members on record sources are ignored
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 1549 to 1600 in 574e6b1
Nominal record expression spreads: effects
fsharp/tests/FSharp.Compiler.ComponentTests/Language/SpreadTests.fs
Lines 1602 to 1626 in 574e6b1