-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Factor out AST node for function return types. #912
Conversation
This enables us to stop treating the return type as a Pattern (which is really isn't), treat return types more consistently with other static types in the typechecker, and drop ReturnTypeContext. Additional changes: - Merge TypeCheckFunDef with TypeOfFunDef. - Handle implicit conversions in `return` statements. - Require function type literals to have an explicit `->`. - Move consistency check for omitted returns from TypeChecker to ResolveControlFlow.
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, it may be worth splitting off the ->
requirement for fnty
into its own discussion. I think that's a significant syntax change, and probably indicates we should get a leads decision for a path forward. I've bumped #191 to try to address this.
// Returns true if this represents an omitted return term. | ||
auto is_omitted() const -> bool { return form_ == Form::Omitted; } | ||
|
||
// Returns true if this represents an auto return term. | ||
auto is_auto() const -> bool { return form_ == Form::Auto; } |
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.
Why expose the kind with boolean accessors? Wouldn't it be less code if the enum were exposed more directly? e.g., in TypeCheckFunctionDeclaration, you could write that as a switch
instead of checking is_omitted
and type_expression.has_value
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.
It might be less code in the places that need to handle all three cases, but it would be more code in the places that need to give special treatment to one of the cases. The latter seem to be slightly more common, and they seem to be places where there's more need for brevity. For example, see resolve_control_flow.cpp
lines 51 and 53, where calls to function_return.is_omitted()
are part of larger expressions, and replacing them with
something like function_return.return_kind() == ReturnTerm::ReturnKind::Omitted
would be fairly unwieldy.
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.
FWIW, I'd still argue for just having a single accessor in order to make it clear that there are other cases being ignored, and also to make it clearer at call sites that there is a mutually exclusive state (i.e., that it can't have is_omitted == true and is_auto == true, and that both of those imply no expression). But I'll leave this up to you.
// Aggregate information about a function being analyzed. | ||
struct FunctionData { |
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.
It might be helpful to make it clearer this is only for return statements. This took me a few passes to understand while reading.
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 quite sure what you mean by "only for return statements". It's true that currently the only nontrivial usages of this type are in the Return
case, but that's not a requirement of the type, it's a more-or-less accidental fact about how the type is currently used.
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 think the accidental fact would be useful to note, at least until (or unless) it changes. I view this as a readability issue where reducing the amount of context that needs to be gained is helpful. But you call this accidental -- do you have other situations where you expect usage to expand when resolving control flow?
While it is a function, if Return
is the only control flow that cares about functions, then I think it's easier to understand in that context. e.g., even just annotating "// Aggregate information about a function being analyzed. Currently only used for Return
statement flow." would be an aid. Renaming it to ReturnContext
would be clearer at usage sites that it's not used in other ways, and encourage a rename should it get used in more situations.
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 think the accidental fact would be useful to note, at least until (or unless) it changes. I view this as a readability issue where reducing the amount of context that needs to be gained is helpful.
I don't understand how that would be helpful, so it seems like there's a gap in my mental model of how people read code. Can you help me understand? For example, can you describe a situation where omitting that information would make the code harder for a reader to understand, and what their thought process might be like?
But you call this accidental -- do you have other situations where you expect usage to expand when resolving control flow?
I don't have specific situations in mind, but I don't have a specific reason to rule it out either.
Partly, I see this as a matter of consistency -- this way, each parameter ofResolveControlFlow
(other than statement
itself) corresponds to a particular kind of enclosing scope that statement
might transfer control to. I think it would be more confusing if one parameter was named and documented in those terms, but the other was named and documented in terms of what kind of statement
uses 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.
LGTM, thank you for discussing this last night.
case ReturnKind::Omitted: | ||
return; | ||
case ReturnKind::Auto: | ||
out << "-> auto"; |
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.
Stray thought: as static types are added, should they be part of the output for debugability?
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.
Currently, Print
on AST nodes is basically an "unparse" operation whose output is syntactically equivalent to the original input, in the sense that parsing the output of Print
should give you an identical AST. I think that's a useful property, and I'm reluctant to abandon it.
I do think we'll eventually want to be able to print something more like a direct dump of the AST, and it would make sense to include type information in that.
// Aggregate information about a function being analyzed. | ||
struct FunctionData { |
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 think the accidental fact would be useful to note, at least until (or unless) it changes. I view this as a readability issue where reducing the amount of context that needs to be gained is helpful. But you call this accidental -- do you have other situations where you expect usage to expand when resolving control flow?
While it is a function, if Return
is the only control flow that cares about functions, then I think it's easier to understand in that context. e.g., even just annotating "// Aggregate information about a function being analyzed. Currently only used for Return
statement flow." would be an aid. Renaming it to ReturnContext
would be clearer at usage sites that it's not used in other ways, and encourage a rename should it get used in more situations.
// Returns true if this represents an omitted return term. | ||
auto is_omitted() const -> bool { return form_ == Form::Omitted; } | ||
|
||
// Returns true if this represents an auto return term. | ||
auto is_auto() const -> bool { return form_ == Form::Auto; } |
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.
FWIW, I'd still argue for just having a single accessor in order to make it clear that there are other cases being ignored, and also to make it clearer at call sites that there is a mutually exclusive state (i.e., that it can't have is_omitted == true and is_auto == true, and that both of those imply no expression). But I'll leave this up to you.
This enables us to stop treating the return type as a Pattern (which is really isn't), treat return types more consistently with other static types in the typechecker, and drop ReturnTypeContext. Additional changes: - Merge TypeCheckFunDef with TypeOfFunDef. - Handle implicit conversions in `return` statements. - Require function type literals to have an explicit `->`. - Move consistency check for omitted returns from TypeChecker to ResolveControlFlow.
This enables us to stop treating the return type as a Pattern (which is really isn't), treat return types more consistently with other static types in the typechecker, and drop ReturnTypeContext.
Additional changes:
return
statements.->
.