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
[sorbet-runtime] Add T.must_because type assertion #6395
Conversation
@@ -221,6 +221,34 @@ def self.must(arg) | |||
end | |||
end | |||
|
|||
# A convenience method to `raise` with a provided error reason when the argument |
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.
sorbet won’t automatically figure out that this method defined in sorbet-runtime exists. you’ll need to duplicate the signature to the file in rbi/
where T.must
is defined. Also it’d be great if you added some tests in test/testdata/infer/must.rb
to confirm that the RBI you’ve added works as expected
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 likely also want to add a test in test/testdata/infer/must_untyped.rb
to document that T.must
and T.must_because
have different behavior when called on some T.untyped
value
If you want to go even further, the code which implements that error is in core/types/calls.cc
. You could cargo cult some of that code to make that error apply to T.must_because
as well if you like.
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 bias towards have the same affordances so there's fewer gotchas and more reason to perhaps always use must_because
over must
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 think it should be fine to write the docs and link ahead of time, and fast-follow with a correction to the docs if the first attempt isn't right. |
core/types/calls.cc
Outdated
} | ||
e.addErrorSection(args.args[0]->explainGot(gs, args.originForUninitialized)); | ||
auto replaceLoc = args.callLoc(); | ||
const auto locWithoutTMustBecause = args.callLoc().adjust(gs, 7, -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.
@jez What I'm assuming based on the numbers is:
- T.must(x)
+ x
But with T.must_because(x) {...}
what could I do to properly rewrite this? Or should I axe this auto-correct for ease?
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 no idea why the original code was so complicated, and I probably wrote it.
I'm pretty sure you should be able to do
e.replaceWith("Remove `T.must_because`", replaceLoc, "{}", args.locs[0].source(gs).value());
(you might have to futz with it to get it to typecheck, not sure. but the idea is that args
has something inside it that tells you what the location of the first argument to the method call is.
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 like it might be called args.argLoc(0)
now
core/types/calls.cc
Outdated
@@ -4107,6 +4141,7 @@ class T_Enum_tripleEq : public IntrinsicMethod { | |||
const vector<Intrinsic> intrinsics{ | |||
{Symbols::T(), Intrinsic::Kind::Singleton, Names::untyped(), &T_untyped}, | |||
{Symbols::T(), Intrinsic::Kind::Singleton, Names::must(), &T_must}, | |||
{Symbols::T(), Intrinsic::Kind::Singleton, Names::mustBecause(), &T_must_because}, |
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 can likely get away with modifying T_must
to be aware of whether it's handling T.must
or T.must_because
, and then use
{Symbols::T(), Intrinsic::Kind::Singleton, Names::mustBecause(), &T_must_because}, | |
{Symbols::T(), Intrinsic::Kind::Singleton, Names::mustBecause(), &T_must}, |
which cuts down on some duplication if you're interested.
r? @jez I think I have everything passing now |
# to contain a non-nil value at this point. | ||
# | ||
# `sig {params(arg: T.nilable(A), reason_blk: T.proc.returns(String)).returns(A)}` | ||
def self.must_because(arg, &reason_blk) |
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 remove the &reason_blk
from this, for performance?
# Intended to be used to promise sorbet that a given nilable value happens | ||
# to contain a non-nil value at this point. | ||
# | ||
# `sig {params(arg: T.nilable(A), reason_blk: T.proc.returns(String)).returns(A)}` |
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 you move this comment to the RBI, where it will be shown on hover? (The only people that will see it here are people who are changing sorbet-runtime.)
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 current added comment in the RBI matches the comment structure of T.must
in the RBI, it seems appropriate? I can remove the comment here in the runtime if it's not useful, I had copied the comment structure of the runtime T.must
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.
Changes look good, thanks. Few tweaks requested. I can merge after that.
# argument is ever `nil`. | ||
# | ||
# For more, see https://sorbet.org/docs/type-assertions#tmust_because | ||
sig {params(arg: T.untyped, reason_blk: T.proc.returns(String)).returns(T.untyped)} |
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.
@jez Now that we can implement T.must-likes with generics, should I be typing this signature more precisely: https://github.com/sorbet/sorbet/compare/andrejewski/t-must-because...andrejewski/t-must-because-stronger-rbi?expand=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.
No, it's already computed by the intrinsic in calls.cc, so the signature is redundant.
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.
Technically there are bugs in the signature-based approach, which are almost impossible to reproduce but for something like T.must
I don't want to risk a bug in generics breaking it (the new generics features is nice for other people, but other code is lower stakes because it is going to be used vastly less frequently).
@jez All that's left is I'm not sure how to address this comment: #6395 (comment) |
I posted this on a Slack discussion thread before landing the change, but wanted to preserve the history in a comment, so here it is:
performance. that suggestion uses a keyword arg which is slow in hot paths. (there is a similar performance hit with an optional argument) Tested on Ubuntu 20.04, Ruby 2.7.2 Time to run a no-op Ruby method call:
no optional args:
optional positional arg:
optional keyword arg:
Using
Using explicit
Even an optional positional arg is slower than an optional block. An optional keyword arg is almost 2x the runtime. Also mentioning the block arg explicitly is bad. The Ruby VM is not a compiler. It has to do the work up front, and can’t reorder or defer work until it’s required. That means that the act of putting Most of the time, the Ruby VM doesn’t actually allocate when given a block. I tested this on Ruby 2.7, but it looks like it’s the same on Ruby 3.1. (I thought maybe the keyword arg changes would make the optional keyword arg case better but it doesn’t.)
yes, but not in the short term
chris and I chatted, and we liked the way that |
Adds a new type assertion method to sorbet-runtime,
T.must_because(maybe_nil_value) {"reason_it_is_not_nil"}
.Motivation
The
T.must
error message is "Passednil
into T.must" which could be more descriptive/self-documenting in the cases where developers know (they usually should know) why they are assuring Sorbet that a value is not nil.T.must_because
is the exact same type narrowing but accepts a reason to document itself.Test plan
See included automated tests.