-
Notifications
You must be signed in to change notification settings - Fork 6
Add MethodTypeToParserNodePrism for RBS method signature translation #814
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
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
2c0e20a to
5a67fec
Compare
8914ab9 to
77219b7
Compare
77219b7 to
005a338
Compare
5a67fec to
613acea
Compare
005a338 to
4536485
Compare
Co-authored-by: Kaan Ozkan <[email protected]>
4536485 to
388f9ab
Compare
| } | ||
|
|
||
| bool isRaise(pm_node_t *node, const parser::Prism::Parser *prismParser) { | ||
| if (!node) { |
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.
Kinda sus, why/where do callers call with null?
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.
We do check in callsites that the body isn't null but I kept this check from the Whitequark version. It may be helpful in case there are new callers being added.
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 ENFORCE(!node);, and reconsider if/when we hit it.
| // In Prism, method bodies are always wrapped in PM_STATEMENTS_NODE. | ||
| // Unwrap if it contains exactly one statement (just the raise call). | ||
| // Reject if multiple statements (e.g., puts + raise). | ||
| if (PM_NODE_TYPE_P(node, PM_STATEMENTS_NODE)) { |
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.
Huh? This isRaise() method detects def x = raise?
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.
Yes, it's used to ensure that the only thing an abstract method does is call raise. I simplified and moved the comment to make it clearer.
|
|
||
| void ensureAbstractMethodRaises(core::MutableContext ctx, const pm_node_t *node, | ||
| const parser::Prism::Parser *prismParser) { | ||
| if (PM_NODE_TYPE_P(node, PM_DEF_NODE)) { |
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.
| if (PM_NODE_TYPE_P(node, PM_DEF_NODE)) { | |
| if (!PM_NODE_TYPE_P(node, PM_DEF_NODE)) return; |
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.
Still unresolved
| if (PM_NODE_TYPE_P(node, PM_DEF_NODE)) { | ||
| auto *def = down_cast<pm_def_node_t>(const_cast<pm_node_t *>(node)); | ||
| if (def->body && isRaise(def->body, prismParser)) { | ||
| def->body = nullptr; |
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 leaks
| def->body = nullptr; | |
| pm_node_destroy(prismParser, def->body); | |
| def->body = nullptr; |
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.
Still unresolved
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.
Wouldn't the arena free it at the end so that it won't leak?
Also pm_node_destroy takes a pm_parser_t so I'd need to create a new method to access the internal pm_parser_t. Do you still prefer 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.
Prism isn't using an arena (yet). Needs manual cleanup like this.
I'd need to create a new method to access the internal
pm_parser_t
Make sense to me
Co-authored-by: Alexander Momchilov <[email protected]>
9f090e4 to
2db0cdc
Compare
| } | ||
|
|
||
| bool isRaise(pm_node_t *node, const parser::Prism::Parser *prismParser) { | ||
| if (!node) { |
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 ENFORCE(!node);, and reconsider if/when we hit it.
| return false; | ||
| } | ||
|
|
||
| // Check if the method body is only a single `raise` call |
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.
| // Check if the method body is only a single `raise` call | |
| // Detects a single `raise` with no arguments: | |
| // - raise | |
| // - self.raise | |
| // - Kernel.raise | |
| // - ::Kernel.raise |
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 modified this code and updated the comment slightly but we do accept raise with other arguments.
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.
If raise with args is allowed, please add tests to cover 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.
There's good coverage right now so I left it as is https://github.com/sorbet/sorbet/blob/0a871d2922d7f6a8cba6e3512b51e76d149b613a/test/testdata/rbs/signatures_defs.rb#L341-L413
| if (PM_NODE_TYPE_P(node, PM_DEF_NODE)) { | ||
| auto *def = down_cast<pm_def_node_t>(const_cast<pm_node_t *>(node)); | ||
| if (def->body && isRaise(def->body, prismParser)) { | ||
| def->body = nullptr; |
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.
Still unresolved
|
|
||
| void ensureAbstractMethodRaises(core::MutableContext ctx, const pm_node_t *node, | ||
| const parser::Prism::Parser *prismParser) { | ||
| if (PM_NODE_TYPE_P(node, PM_DEF_NODE)) { |
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.
Still unresolved
| {core::AutocorrectSuggestion::Edit{ctx.locAt(editLoc), corrected}}}; | ||
| } | ||
|
|
||
| void ensureAbstractMethodRaises(core::MutableContext ctx, pm_node_t *node, const parser::Prism::Parser *prismParser) { |
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.
| void ensureAbstractMethodRaises(core::MutableContext ctx, pm_node_t *node, const parser::Prism::Parser *prismParser) { | |
| // Abstract methods must always have a single `raise` statement: | |
| // | |
| // def my_abstract_method = raise | |
| void ensureAbstractMethodRaises(core::MutableContext ctx, pm_node_t *node, const parser::Prism::Parser *prismParser) { |
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 moved this comment to isValidAbstractMethod instead. I think the function name ensureAbstractMethodRaises is self documenting.
| sigBuilder = prism.Call0(annotation.typeLoc, sigBuilder, core::Names::overridable().show(ctx.state)); | ||
| } else if (annotation.string == "override") { | ||
| sigBuilder = prism.Call0(annotation.typeLoc, sigBuilder, core::Names::override_().show(ctx.state)); | ||
| } else if (annotation.string == "override(allow_incompatible: 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.
What about override(allow_incompatible: false)
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 just forward the args "as is"?
We can extract the relevant part out of the RBS, and call Prism.parse on it directly, which we can just attach that into the generated sig { override(...)... } call. (low priority)
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 did a quick prototype. We can parse the override args with a new Prism parser instance (AFAIK we can't reuse the existing Prism parser). Then we use the Prism factory to add specific kinds of args to the existing parser. So we still need a case statement that supports specific Prism node types to have good coverage. Not great.
pm_node_t *copyLiteralNode(pm_node_t *node, parser::Prism::Parser &sourceParser,
const parser::Prism::Factory &factory, core::LocOffsets loc) {
switch (PM_NODE_TYPE(node)) {
case PM_TRUE_NODE:
return factory.True(loc);
case PM_FALSE_NODE:
return factory.False(loc);
case PM_SYMBOL_NODE: {
auto *sym = down_cast<pm_symbol_node_t>(node);
return factory.Symbol(loc, string(sourceParser.extractString(&sym->unescaped)));
}
case PM_INTEGER_NODE: {
auto *num = down_cast<pm_integer_node_t>(node);
return factory.Integer(loc, num->value.value);
}
case PM_STRING_NODE: {
auto *str = down_cast<pm_string_node_t>(node);
return factory.String(loc, string(sourceParser.extractString(&str->unescaped)));
}
}
}Or we could go the string manipulation route to extract the arg and call factory methods to insert to the Prism tree, similar to what we do now.
if (annotation.string.compare(0, 28, "override(allow_incompatible:") == 0) {
auto start = annotation.string.find(": ") + 2;
auto valueStr = annotation.string.substr(start, annotation.string.find_last_of(')') - start);
// Trim whitespace
while (!valueStr.empty() && (valueStr[0] == ' ' || valueStr[0] == '\t')) valueStr.remove_prefix(1);
while (!valueStr.empty() && (valueStr.back() == ' ' || valueStr.back() == '\t')) valueStr.remove_suffix(1);
pm_node_t *value = nullptr;
if (valueStr == "true") {
value = prism.True(annotation.typeLoc);
} else if (valueStr == "false") {
value = prism.False(annotation.typeLoc);
} else if (!valueStr.empty() && valueStr[0] == ':') {
value = prism.Symbol(annotation.typeLoc, string(valueStr.substr(1)));
}
// Add more types: integers, strings with parsing, etc.
if (value) { ... }
}I kept it as is right now and added support for false, let me know how you want to proceed.
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 like this approach for now. I'll play around with using the parser
| return; | ||
| } | ||
|
|
||
| for (rbs_hash_node_t *hash_node = field->head; hash_node != nullptr; hash_node = hash_node->next) { |
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.
| for (rbs_hash_node_t *hash_node = field->head; hash_node != nullptr; hash_node = hash_node->next) { | |
| for (auto *hash_node = field->head; hash_node != nullptr; hash_node = hash_node->next) { |
| sigParams.reserve(args.size()); | ||
|
|
||
| // Check if methodParams is empty once before the loop (loop invariant) | ||
| if (methodParams.empty() && !args.empty()) { |
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 wrong kind of check for the kind of message that's emitted
| if (methodParams.empty() && !args.empty()) { | |
| if (methodParams.size() > args.size()) { |
| sigBuilder = prism.Call0(annotation.typeLoc, sigBuilder, core::Names::overridable().show(ctx.state)); | ||
| } else if (annotation.string == "override") { | ||
| sigBuilder = prism.Call0(annotation.typeLoc, sigBuilder, core::Names::override_().show(ctx.state)); | ||
| } else if (annotation.string == "override(allow_incompatible: 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.
Maybe just forward the args "as is"?
We can extract the relevant part out of the RBS, and call Prism.parse on it directly, which we can just attach that into the generated sig { override(...)... } call. (low priority)
amomchilov
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.
One last suggestion, but looks good!
| bool isValidAbstractMethod(pm_node_t *node, const parser::Prism::Parser *prismParser) { | ||
| if (!PM_NODE_TYPE_P(node, PM_DEF_NODE)) { | ||
| return false; | ||
| } | ||
|
|
||
| auto *def = down_cast<pm_def_node_t>(node); | ||
|
|
||
| // Check if body is a single raise call | ||
| if (def->body) { | ||
| pm_node_t *bodyNode = def->body; | ||
|
|
||
| // Unwrap statements node if it contains exactly one statement | ||
| if (PM_NODE_TYPE_P(bodyNode, PM_STATEMENTS_NODE)) { | ||
| auto *stmts = down_cast<pm_statements_node_t>(bodyNode); | ||
| if (stmts->body.size == 1) { | ||
| bodyNode = stmts->body.nodes[0]; | ||
| } else { | ||
| return false; // Multiple statements, not just a raise | ||
| } | ||
| } | ||
|
|
||
| // Check if it's a raise call | ||
| if (bodyNode && PM_NODE_TYPE_P(bodyNode, PM_CALL_NODE)) { | ||
| auto *call = down_cast<pm_call_node_t>(bodyNode); | ||
| auto methodName = prismParser->resolveConstant(call->name); | ||
|
|
||
| if (methodName == "raise" && (call->receiver == nullptr || isSelfOrKernel(call->receiver, prismParser))) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } |
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.
Re-use the same early return style you used in isSelfOrKernel()
| bool isValidAbstractMethod(pm_node_t *node, const parser::Prism::Parser *prismParser) { | |
| if (!PM_NODE_TYPE_P(node, PM_DEF_NODE)) { | |
| return false; | |
| } | |
| auto *def = down_cast<pm_def_node_t>(node); | |
| // Check if body is a single raise call | |
| if (def->body) { | |
| pm_node_t *bodyNode = def->body; | |
| // Unwrap statements node if it contains exactly one statement | |
| if (PM_NODE_TYPE_P(bodyNode, PM_STATEMENTS_NODE)) { | |
| auto *stmts = down_cast<pm_statements_node_t>(bodyNode); | |
| if (stmts->body.size == 1) { | |
| bodyNode = stmts->body.nodes[0]; | |
| } else { | |
| return false; // Multiple statements, not just a raise | |
| } | |
| } | |
| // Check if it's a raise call | |
| if (bodyNode && PM_NODE_TYPE_P(bodyNode, PM_CALL_NODE)) { | |
| auto *call = down_cast<pm_call_node_t>(bodyNode); | |
| auto methodName = prismParser->resolveConstant(call->name); | |
| if (methodName == "raise" && (call->receiver == nullptr || isSelfOrKernel(call->receiver, prismParser))) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| bool isValidAbstractMethod(pm_node_t *node, const parser::Prism::Parser *prismParser) { | |
| if (!PM_NODE_TYPE_P(node, PM_DEF_NODE)) { | |
| return false; | |
| } | |
| auto *def = down_cast<pm_def_node_t>(node); | |
| // Check if body is a single raise call | |
| if (def->body == nullptr) { | |
| return false; | |
| } | |
| pm_node_t *bodyNode = def->body; | |
| ENFORCE(bodyNode != nullptr); | |
| // Unwrap statements node if it contains exactly one statement | |
| if (PM_NODE_TYPE_P(bodyNode, PM_STATEMENTS_NODE)) { | |
| auto *stmts = down_cast<pm_statements_node_t>(bodyNode); | |
| if (stmts->body.size > 1) { // Multiple statements, not just a raise | |
| return false; | |
| } | |
| bodyNode = stmts->body.nodes[0]; | |
| } | |
| if (!PM_NODE_TYPE_P(bodyNode, PM_CALL_NODE)) { | |
| return false; | |
| } | |
| auto *call = down_cast<pm_call_node_t>(bodyNode); | |
| auto methodName = prismParser->resolveConstant(call->name); | |
| return methodName == "raise" && (call->receiver == nullptr || isSelfOrKernel(call->receiver, prismParser)) | |
| } |
| } | ||
|
|
||
| // Check if it's a raise call | ||
| if (bodyNode && PM_NODE_TYPE_P(bodyNode, PM_CALL_NODE)) { |
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 this bodyNode check can't fail. Statements' body nodes can't contain nulls.
| pm_node_t *bodyNode = def->body; | ||
|
|
||
| // Unwrap statements node if it contains exactly one statement | ||
| if (PM_NODE_TYPE_P(bodyNode, PM_STATEMENTS_NODE)) { |
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.
You're relying on bodyNode not being null here, I'd document that with a ENFORCE(bodyNode != nullptr); (which I added in my suggestion in the prev 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.
Updated the function. We now check for null and then assign so ENFORCE shouldn't be necessary.
a8558f5 to
97dfce9
Compare

Motivation
Test plan
See included automated tests.