-
Notifications
You must be signed in to change notification settings - Fork 846
Allow modules to be used in type applications #14655
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
Conversation
|
Amazed by the quality of your contributions. Learning a lot from it. |
|
Im curious if this would work for |
I don't understand what you mean. The only place where it matters is if you want to refer to the module you're in: module rec A.F
let a = typeof<module A.F> // doesn't work unless A.F is marked as recursive |
I see . You are right , it is not applicable here |
src/Compiler/SyntaxTree/LexFilter.fs
Outdated
|
|
||
| // module ... ~~~> CtxtModuleHead | ||
| | MODULE, _ :: _ -> | ||
| | MODULE, _ :: _ when offsideStack |> List.forall (fun x -> match x with CtxtParen (LESS _, _) -> false | _ -> true) -> |
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 the best way to identify that we're within a type application that I could come up with :/
|
How do I guard this with a lang feature? |
|
@kerams This is how I do it
You can see how I added here https://github.com/dotnet/fsharp/pull/14263/files |
|
Yeah, I know. I am specifically concerned about the parsing change. Since lang features are not available in pars.fsy, I could add it to CheckExpressions.fs, but then I have to supply an appropriate error message, rather than relying on the parser to reject such code. |
When review the static abstract interfaces implementation. I found this check we can use similar in this case ? Line 1784 in 5061f4d
|
|
Thank you, Edgar, it works really well. Not sure how I missed it. |
src/Compiler/pars.fsy
Outdated
| { let mDecl = (rhs parseState 1) | ||
| if not $2 then errorR(Error(FSComp.SR.parsTypeNameCannotBeEmpty(), mDecl)) | ||
| SynMemberDefn.Inherit(SynType.LongIdent(SynLongIdent([], [], [])), None, mDecl) } | ||
| SynMemberDefn.Inherit(SynType.LongIdent(SynLongIdent([], [], []), false), None, mDecl) } |
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 fact that you need to pass false in so many locations is a bit of a code smell to me.
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 outdatedl
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 bad. I'm still quite on the fence about whether SynType.LongIdentModule is the way to go. I think an application would fit the bill better.
The range of SynType.LongIdentModule will currently be incorrect as it will only include the longId range and not start from the module keyword.
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 thing is, you can theoretically do f<module M, int> (), so this isn't a new application type. We're just accepting a new kind of type argument - SynType.
The range of SynType.LongIdentModule will currently be incorrect as it will only include the longId range and not start from the module keyword.
Good catch.
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'm not really following, if you were to write f<module2 M, int> () today (link), you get exactly a SynType.App for module2 M.
And regardless of how your model this in SynType, you will find theoretical scenario's where you can plug in module X wherever a SynType is allowed.
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.
But module A.B.C is in and of itself not an application. module is no type (to be stored in typeName), but a disambiguating keyword. Technically it isn't even needed and LongIdent could have been used with no additions (by virtue of the fact that modules are IL classes just like types).
I think we'd just be abusing SynType.App and making it confusing.
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.
module A.B.C is syntactically an application. This is valid jargon for the untyped tree. Just like int list is an application in the untyped tree. Of course, there is no int function that is being applied by a list, but it is a SynType.App nonetheless.
The evaluation of module A.B.C will later, in the typed tree, of course not be an application, but on the untyped level, I'm calling it as I see it.
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.
Ok, I get it now. However, if the parser can already tell module A.B.C and int list are semantically different, shouldn't they be encoded differently? Wouldn't it be cleaner than seeing whether typeName equals module in CheckExpressions.fs?
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.
Well, that is part of the debate, is this really different enough to have its own DU case?
From an untyped perspective, I'd say no. Most SynType are rather undecided yet on what they actually will represent later in the typed tree.
I think #13700 is a bit of an example. We allowed users to write int array2d instead of int[,]. This could have been a new case in the SynType DU, except it just fits in what is already out there. That feature wasn't new from a syntax point of view.
And just because you are using the module keyword, I'm still convinced this is similar.
You have my two cents, ultimately it is not up to me. The maintainers will tell you what they want eventually. If they approve your current code, I would implore you to include the full range of the type, as that will be problematic sooner or later.
Lastly, I do want to mention that this is a solid PR and you have done amazing work here.
I may seem a tad too critical in reviewing this, but that is only because I have a stake in the SyntaxTree. Every change made to it affects me and I will voice my concerns when I deem it necessary. I expect no different from my peers when they review my PRs.
Keep it up!
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 would implore you to include the full range of the type
Yeah, I have already done this locally.
I may seem a tad too critical in reviewing this
No, no, I love thorough PR scrutiny and discussing the implementation in order to maintain high standards, especially in a project like the F# compiler. So thanks for taking the time to review this.
|
Could some tests be added when using Things like: type X = module Y
let (a: module Y): module X = ()should not become valid AST. |
|
To my surprise this works module rec X
let value: module X = Unchecked.defaultof<module X>Unfortunately And if we were to support modules in type annotations (I don't see why that would be useful though), then the following doesn't work with the parser and lex filter as is |
Hm, interesting, what does the code above result into? I mean what do we codegen. I assume we can still parse those, but later on restrict it to the type parameters only? |
tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj
Outdated
Show resolved
Hide resolved
| (Error 39, Line 13, Col 26, Line 13, Col 39, "The namespace or module 'NonexistentNs' is not defined.") | ||
| (Error 39, Line 14, Col 28, Line 14, Col 39, "The module 'Nonexistent' is not defined.") | ||
| (Error 39, Line 15, Col 30, Line 15, Col 35, "The module 'value' is not defined.") | ||
| (Error 39, Line 16, Col 23, Line 16, Col 24, "The module 'R' is not defined.") |
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.
Any chance for making this better in the case of 'R' being a type?
i.e. turning the diagnostics into a clippy-like suggestion for trying " typeof without the module keyword" instead.
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 will try, it might be tricky. Plus the module keyword might not be present in some context in the future.
tests/FSharp.Compiler.ComponentTests/Language/ModuleAsTypeTests.fs
Outdated
Show resolved
Hide resolved
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.
Left a few minor comments:
- Consider more helpful diagnostics when
typeof<module ..ends up pointing to a type - Check that
typedefofalso works (it should)
|
@kerams would you mind also adding a test in https://github.com/dotnet/fsharp/blob/main/tests/service/SyntaxTreeTests/SynTypeTests.fs for the full range of the SynType? |
auduchinok
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.
@kerams This is an interesting change, and it's really cool that it hasn't required many changes to the parsing!
I'm still very worried about any changes made to how type and module are parsed, as these are very special keywords for LexFilter that affect parser recovery a lot.
It could be much safer to get these changes in, if we had a good test suite that would assert that file trees are not changed for various cases where error recovery is kicking in, but there's no such tests as for now. We would need #13484 and do a very careful testing here. Without that it can break parsing in subtle ways when a user is typing source in an editor and having unfinished code.
We really need to check various cases how unfinished expressions and types interact with the new rule and to have it recorded via tests. Consider cases like this, from the top of my head:
module Module1 =
let x = Unchecked.defaultof<x, // caret here
module Module2 =
()module Module1 =
let x = f<x,
module // caret here
ignore| // module ... ~~~> CtxtModuleHead | ||
| | MODULE, _ :: _ -> | ||
| // module ... ~~~> CtxtModuleHead, unless we're in a type application | ||
| | MODULE, _ :: stack when stack |> List.forall (fun x -> match x with CtxtParen (LESS _, _) -> false | _ -> true) -> |
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.
Does this check indentation somewhere, so we don't consider module a part of a type application while expression isn't finished?
module Module1 =
// typing an expression here, e.g. `x < 0`, in an editor
let f x = x <
module Module2 =
let y = ()I.e. LexFilter should produce additional tokens that would end the first module before returning MODULE to the parser.
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 haven't tried this yet. My knowledge of this area is very limited, and the above was merely the simplest solution.
| /// F# syntax: typeof<module A.B.C> | ||
| | LongIdentModule of longDotId: SynLongIdent * 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.
Please clarity the doc, so it's clear that typeof< and > are not part of this node.
| { [ SynTupleTypeSegment.Type $1 ] } | ||
|
|
||
| appTypeCon: | ||
| | moduleKeyword path %prec prec_atomtyp_path |
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 we get MODULE_COMING_SOON or MODULE_IS_HERE here too, or only MODULE is possible? Perhaps, the changes to LexFilter should not produces the first two, as they are the synchronization tokens that don't seem relevant in the SynType context. We could then only use explicit MODULE token 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.
Agreed, MODULE would be better, since the module context isn't being pushed here.
| [<Fact>] | ||
| let ``Module can be used as generic type argument in lang preview`` () = | ||
| Fsx """ | ||
| let actual = ResizeArray<module LanguagePrimitives>().GetType().FullName |
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 it expected to be correct? What does this code generate?
I think we should only allow it in typeof and typedefof? We could check that during type check and produce an error.
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.
Well, modules are classes, so the IL is valid. While C# doesn't allow static types as type arguments, F# doesn't mind.
Whether anything outside of typeof would be useful is highly questionable, but I'm not sure it's worth banning module type application in general on those grounds. On the other hand, modules in annotations are not allowed... I don't know.
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.
And if we decide typeof is the only valid use, why not just introduce moduleof (though the name is confusing) and avoid mucking around with all these parsing changes, or allow modules in typeof and live with the potential ambiguities. I'd love Don to weigh in here.
Implements fsharp/fslang-suggestions#730.
Includes support for completion filtering.