Inline RBS comments for seamless type checking with Sorbet


Summarized using AI

Inline RBS comments for seamless type checking with Sorbet

Alexandre Terrasa • April 18, 2025 • Matsuyama, Ehime, Japan • Talk

In the talk titled "Inline RBS comments for seamless type checking with Sorbet," Alexandre Terrasa discusses integrating RBS (Ruby Signature) comments directly into Ruby code to facilitate efficient type checking with Sorbet. The presentation outlines how this integration allows for a cleaner syntax while enhancing type safety for large codebases, particularly advantageous for projects with extensive Ruby files. The speaker, part of Shopify's Ruby infrastructure team, highlights a series of improvements made to the ruby/rbs parser, allowing it to function independently of the RubyVM, which expands its potential application beyond Sorbet, making it usable by various tools written in C/C++/Rust.

Key points detailed in the talk include:

- Developer Needs Assessment: The Ruby developer experience team at Shopify gathers feedback via annual surveys, revealing a strong desire for more type annotations and a more user-friendly syntax. In their latest survey, 80% of developers expressed the need for more types, reflecting the growing appreciation for static typing since its introduction.

- Syntax Comparison: The conventional syntax using sig methods tends to clutter Ruby code, thus the talk emphasizes the advantages of RBS comments to express method signatures and types without obstructing the Ruby syntax, resulting in a cleaner, more maintainable codebase.

- Integration Process: Terrasa reviews the challenges faced with Ruby's existing RBS parser, which required a RubyVM, and explains how this hurdle was overcome by converting the RBS parser to a pure C implementation. This reformatted parser can be utilized without the overheads of a Ruby runtime, fitting seamlessly into Sorbet’s C++ ecosystem.

- Code Implementation: The presentation includes practical examples illustrating how to write type annotations as comments. This approach simplifies incorporating types within existing Ruby files without requiring intrusive changes that affect runtime.

- Migration Tools: To aid developers in transitioning from earlier Sorbet syntax to the new RBS comment system, tools are provided for automatic conversion, allowing gradual adoption within large codebases.

The talk concludes with an emphasis on the benefits of this approach, detailing how it enhances developer workflow and improves overall code quality without sacrificing performance. The speaker invites further interaction and suggests reviewing the latest Sorbet documentation to leverage these features effectively in ongoing coding projects.

In summary, the integration of RBS comments into Ruby code allows for seamless type checking with Sorbet, promising enhanced type safety, cleaner syntax, and improved developer experience in large-scale Ruby projects.

Inline RBS comments for seamless type checking with Sorbet
Alexandre Terrasa • Matsuyama, Ehime, Japan • Talk

Date: April 18, 2025
Published: May 27, 2025
Announced: unknown

In this talk, we'll explore how integrating the RBS parser into Sorbet allows us to include type information as comments directly into our Ruby code. This integration enables fast type checking with an appealing syntax while enhancing type safety and code navigation for large-scale projects.

We'll discuss the improvements we made to the `ruby/rbs` parser, which can now function without the need for the RubyVM. This enhancement not only makes it compatible with Sorbet but also opens the door for use from other C/C++/Rust tools.

We'll also showcase our tool to automatically convert Sorbet RBI signatures into RBS comments, addressing some of the differences and challenges between the two syntaxes.

Join us to discover how Sorbet and RBS can work together to elevate your Ruby development experience.

https://rubykaigi.org/2025/presentations/Morriar.html

RubyKaigi 2025

00:00:05.120 hi
00:00:12.280 everybody all right welcome to inline
00:00:14.960 ABS comments for seamless type checking
00:00:17.119 with Sor my name is Alexander Terza uh
00:00:20.960 you can reach out to me by email because
00:00:22.800 I don't use Twitter um or on
00:00:25.800 GitHub i'm a senior staff engineer at
00:00:28.320 Shopify i'm working in the Ruby and RS
00:00:30.560 infrastructure team uh more precisely in
00:00:32.320 the Ruby developer experience team you
00:00:34.559 may have heard of us uh for projects
00:00:36.399 such as the Ruby LSP tapia which is a
00:00:39.120 companion tool for so or contribution to
00:00:42.920 so we actually were created like our
00:00:46.719 team was created to introduce so at
00:00:48.719 Shopify in February 2019 where um we
00:00:52.960 were early adopters of so a few months
00:00:55.199 before it was open sourced and we
00:00:58.239 realized the need to have type checking
00:00:59.840 for such large code base a few years
00:01:02.559 after that ABS was created around 2021
00:01:06.479 and for Ruby3 and this is also around
00:01:08.880 this time that steep was open
00:01:11.400 source so as the Ruby developer
00:01:14.080 experience team our goal is to
00:01:16.320 understand what our developers want and
00:01:19.840 what we should be doing for them to do
00:01:22.560 that we run surveys every year and we
00:01:25.520 ask them question about like what are
00:01:27.200 the things that you need so for example
00:01:29.759 since we introduced typing using so at
00:01:32.000 Shopify we asked them like do you want
00:01:34.320 more code to be typed like is type are
00:01:36.320 the types useful for you and do you need
00:01:38.079 more and while at the beginning when we
00:01:40.320 introduced it it was not very clear if
00:01:42.400 they really want it or not if we look at
00:01:44.320 our most recent edition of the survey
00:01:46.560 80% of them are like yes I need more
00:01:48.640 types this is super
00:01:50.200 useful the same when we ask them do you
00:01:52.640 want so to be used in more code bases at
00:01:55.200 Shopify they're like yes 7 70% of them
00:01:58.159 are like Yes this is super useful if we
00:02:00.399 looked at the beginning though that was
00:02:01.920 not that clear but people came to
00:02:04.399 understand and to appreciate the
00:02:06.240 benefits of static typing gradual typing
00:02:10.640 uh when we ask them do you want to have
00:02:12.239 to write less annotations in your code
00:02:14.800 it's not really something that bother
00:02:16.640 them in majority like 55% of them are
00:02:19.280 saying like yes that would be cool but
00:02:20.640 it's not like as obvious as pre previous
00:02:23.720 questions but when we ask them do you
00:02:26.640 want a friendlier syntax to express the
00:02:29.040 types in your code 75% of them are like
00:02:31.599 yes we need something
00:02:33.080 better if you haven't been introduced to
00:02:35.599 so before let me give you an example of
00:02:37.280 what it looks like uh first we're going
00:02:40.319 to express the signatures for methods
00:02:42.800 using the sig calls here directly in the
00:02:45.680 Ruby code saying I'm having a signature
00:02:47.920 for this attribute reader for example or
00:02:49.599 a signature for this initialize the
00:02:51.680 signature express the params that I used
00:02:54.080 in the sign in um that I use by the
00:02:56.400 method and I'm going to say that the
00:02:58.560 written type is
00:03:00.440 whatever because this is actual Ruby
00:03:02.959 code this sig method needs to be defined
00:03:05.760 somewhere and to be able to use that you
00:03:08.000 need to also depend on the sovereign
00:03:09.760 runtime gem that defines this sig method
00:03:12.319 here somewhere and to make it available
00:03:14.720 in your class you need to extend tig
00:03:17.440 directly in your class so this is
00:03:19.280 already a lot of clutter this is not the
00:03:21.280 be the most beautiful Ruby I've seen in
00:03:23.040 my life and if you see this really easy
00:03:25.840 piece of code here simple piece of code
00:03:27.760 almost half of it is already type
00:03:30.519 annotations when we want to express
00:03:32.560 types you we have to go around the Ruby
00:03:34.560 syntax because it's actually Ruby code
00:03:36.080 so we have to find ways with the Ruby
00:03:37.680 syntax to actually express the types
00:03:39.360 that we have in our code so for example
00:03:40.879 here I'm having a nable of a location uh
00:03:43.760 I can say that my my block for my method
00:03:46.560 is optional by having again a tailable
00:03:49.200 call and I'm expressing the block with
00:03:50.879 t.prock this is getting very painful to
00:03:53.440 write when you have to do that on every
00:03:55.360 method that you have in your
00:03:57.799 codebase uh we can also uh um annotate
00:04:01.920 the type of instance variables local
00:04:03.760 variables these kind of things and again
00:04:05.680 we have to go around with the t.let at
00:04:07.519 to express that I want to set the type
00:04:09.519 of this variable and this is going to be
00:04:11.360 an array of node this is again very
00:04:13.400 painful so we need a friendlier syntax
00:04:16.479 for that this is not something we like
00:04:18.959 to use
00:04:20.440 everywhere when Ruby 3 was released
00:04:23.280 there was like a glimmer of hope because
00:04:25.360 uh it was coming with ABS that was a new
00:04:27.759 language to describe types for Ruby
00:04:30.400 programs and we were super excited
00:04:32.160 because we really wanted to jump on this
00:04:33.919 wagon and say like okay we're going to
00:04:35.360 use ABS now you can replace the service
00:04:36.880 so syntax by this uh sadly we did a few
00:04:40.400 tries and the concept of ABS is you have
00:04:43.440 your Ruby file that is proper Ruby code
00:04:46.479 and you have a companion file that is
00:04:48.080 the RBS file in which you're going to
00:04:50.000 express the types that you were going to
00:04:51.520 use in your program so first problem
00:04:53.840 it's a lot of duplication for every file
00:04:56.320 you have this companion file that's
00:04:58.400 coming with it and for us we have
00:05:01.280 something like 75,000 files in our
00:05:03.600 monolith 1.5 million methods that is a
00:05:07.440 lot of duplication we need to to do and
00:05:09.360 imagine opening the pull request to
00:05:10.800 GitHub by adding the by doubling the
00:05:13.120 size of your codebase that will not fly
00:05:16.720 the other issue is that you cannot
00:05:18.720 express types for local variables for
00:05:21.039 example in your code and you cannot do
00:05:22.880 things like casts or um the setting the
00:05:26.880 type of um I already said that local
00:05:29.120 variables so that was also an issue for
00:05:31.600 proper type checking and in-depth type
00:05:34.680 checking so what we actually really
00:05:36.880 wanted was a mix of the two words wanted
00:05:40.080 to be in a Ruby file and be able to
00:05:42.479 express the types directly by using the
00:05:44.800 nice nicer syntax that is but directly
00:05:47.919 in the Ruby file and for example we can
00:05:50.479 do that using comments we can add a
00:05:52.000 comment on top of the attribute reader
00:05:53.840 or on top of the method saying that here
00:05:55.919 is the type that the signature for my uh
00:05:58.560 for my
00:05:59.720 method and because I'm in a comment now
00:06:02.160 I'm not like bound to the Ruby syntax
00:06:04.479 and I can express using the LBS syntax
00:06:07.039 what is the type of my parameter for
00:06:08.880 example I'm having an label of location
00:06:11.360 using this question mark or I can set
00:06:13.759 the type of my proc by my block sorry by
00:06:16.560 using this syntax this is much
00:06:18.919 nicer and if we're using comments we
00:06:21.280 also can put them in different places
00:06:22.960 and for example use them as well to
00:06:24.560 define the type of the instance
00:06:27.720 variables because we use comments we
00:06:30.160 don't have to support this sig method we
00:06:32.319 don't have to have any kind of runtime
00:06:34.160 dependency that may had an overhead for
00:06:36.639 example uh when I'm running my code uh
00:06:38.960 in production so no more runtime
00:06:40.639 dependency nice syntax just comments
00:06:43.680 this is exactly what we want so how do
00:06:46.240 we get
00:06:47.319 there first we have to choose like a
00:06:49.600 type checker to do this with of course
00:06:51.759 we're using a lot of so at Shopify we
00:06:53.520 have more than 600 codebases using so in
00:06:55.840 our company so that was a strong
00:06:57.440 incentive to go with so and continue
00:06:59.039 with it because we don't want to break
00:07:00.800 like the work we already did or monolith
00:07:04.000 also saw like a lot of a very good
00:07:05.919 adoption 99% of the files we have in the
00:07:08.560 monolith are using sorbet and are typed
00:07:10.639 by sorbet uh all the most of the
00:07:13.280 important methods that we use already
00:07:14.880 have a signature and six more than 60%
00:07:16.960 of the calls we do in our monolith are
00:07:19.280 against the method that comes with a
00:07:22.360 signature there is another type checker
00:07:24.479 for for Ruby that is called steep that
00:07:26.240 was introduced by Sutaro uh we did try
00:07:28.800 we tried to use it uh most the main most
00:07:31.759 important problem here is X speed
00:07:33.280 because SIP is written in Ruby it's very
00:07:35.199 hard to compete with a C++
00:07:36.880 implementation that is highly
00:07:38.080 parallelizable
00:07:39.599 um so when we try it on small code bases
00:07:41.759 it does work it scale um we can type
00:07:44.800 check most of what we already had when
00:07:47.120 we run it against the monolith um
00:07:49.440 something that we can type check in
00:07:51.039 under 20 seconds with so takes more than
00:07:54.080 an hour with steep so this is not
00:07:55.919 something we can run on CI for example
00:07:58.080 because we deploy much more often often
00:08:00.479 so that was a problem so we were like
00:08:02.240 facing two pro two choices here or we
00:08:05.160 can make so support ABS syntax or we can
00:08:09.039 invest a lot of time and steep to make
00:08:10.639 it faster and
00:08:13.000 scalable when we looked at the kind of
00:08:15.039 features that were like defined in so
00:08:17.759 defined in so and that we could express
00:08:19.599 with ABS the coverage was really good
00:08:21.840 most of the features we have in so have
00:08:23.520 an equivalent in the LBS syntax so we
00:08:25.599 can do the replacement um just some of
00:08:28.479 the syntax actually existing in ABS does
00:08:31.120 not have an equivalent in sit so that
00:08:32.719 was not a problem we can just like
00:08:33.919 forbid it when you're trying to write it
00:08:36.320 and for most of the features we have an
00:08:38.000 equivalent the only the only problem
00:08:40.080 here was the lower bound on generics
00:08:42.080 that you cannot express that in so but I
00:08:44.080 believe this is something we can
00:08:45.800 change so do a few features that we
00:08:48.560 cannot express um in ABS and it will not
00:08:51.040 make sense to bring to ABS for example
00:08:52.800 the concept of abstract classes or
00:08:54.320 abstract methods uh for those we will
00:08:57.040 have to find a workar around i'm going
00:08:58.480 to show you that later if you want to
00:09:00.880 know more about AirBS and so syntax and
00:09:04.959 the specification be behind them you can
00:09:07.120 take a look at one of my previous talk
00:09:08.959 um that is gradual typing for Ruby
00:09:10.880 comparing AirBS and
00:09:13.640 Airbus Rubik
00:09:17.040 so let's see how we can start using ABS
00:09:19.680 comments for type checking with
00:09:22.360 so the first problem was the Ruby RBS
00:09:25.360 parser the RBS parser was built around
00:09:28.880 the Ruby VM so to pass a piece of RBS
00:09:31.360 code you had to have a Ruby VM running
00:09:34.000 that means that if we wanted to use this
00:09:37.279 RBS parser from the Sway C++
00:09:39.600 implementation we also needed to embed a
00:09:42.000 uh Ruby VM in the C++ implementation
00:09:44.399 that's a problem especially when you do
00:09:46.399 a lot of parallelized uh analysis where
00:09:48.959 we're going to fight with the GVL a lot
00:09:51.680 so we actually fixed this problem and I
00:09:53.839 hope you had a chance to see my
00:09:55.279 colleague Alexander yesterday that was
00:09:57.279 explaining oh we transformed the Ruby
00:09:58.880 Airbs passer to be a pure C
00:10:01.279 implementation so we can include it in
00:10:03.200 the C++ so with absolutely no
00:10:07.000 problem and from there we needed to find
00:10:10.399 a solution to use this RBS parser inside
00:10:12.399 so let me give you an over just a
00:10:15.120 simplified overview of how so is doing
00:10:17.120 the typeing we're having this what we
00:10:18.959 call a typeeing pipeline here where In
00:10:21.440 input I'm going to give Ruby files and
00:10:23.519 in output I'm going to have the type
00:10:25.200 checking errors if any that are going to
00:10:27.360 be displayed on my
00:10:28.839 screen the very first step is to parse
00:10:31.440 the code i'm having those files that I
00:10:33.760 need to transform in something that is
00:10:35.440 more like easier to analyze so we're
00:10:37.920 going to build an abstract syntax tree
00:10:39.760 from this this is just a tree of nodes
00:10:41.680 representing the content of our Ruby
00:10:44.440 file the second step is what is called
00:10:47.040 dshuga we're actually going to rewrite
00:10:49.279 the a we got at the previous phase into
00:10:51.839 another a that is that I'm going to call
00:10:54.240 like simplified we're just going to
00:10:55.680 remove variation in inside this a let me
00:10:58.399 give you an example so in Ruby I can use
00:11:01.040 a lot of syntactic sugar for example I
00:11:03.200 can express the inverse of if using the
00:11:06.160 unless keyword and I can say that unless
00:11:09.200 ag is empty I'm going to call argv.shift
00:11:12.040 shift and I'm going to use another
00:11:14.240 syntactic sugar to do a safe navigation
00:11:16.079 and call to i on it only if it's not
00:11:18.600 nil dugaring the code is removing this
00:11:21.360 syntactic sugar I'm going to just find
00:11:23.920 an equivalent way to express the same
00:11:25.920 thing with less variation so for example
00:11:28.160 a nless can be changed into a if if I
00:11:30.640 negate the the condition and a safe
00:11:33.360 navigation can be translated in just
00:11:35.519 like adding a if gu to say I'm going to
00:11:37.680 call to i only if the result of a dot
00:11:40.079 shift avshift is not new so by doing
00:11:43.519 this dshugaring we just make the work of
00:11:45.600 subsequent phases easier because they
00:11:48.000 don't have to care about like all the
00:11:49.760 variations of the
00:11:51.959 syntax once I'm having this simplified a
00:11:56.079 I can resolve it which basically is
00:11:58.320 creating a knowledge base about what is
00:12:00.079 in the code what are the relationships
00:12:03.040 between a classes for example through
00:12:05.040 inheritance or like with modules with
00:12:06.959 mixins and just keeping track of like
00:12:09.279 what exists in the codebase and all
00:12:10.880 types are
00:12:11.880 related another phase is going to be to
00:12:14.959 to build the control for graph which is
00:12:16.800 a graph of nodes explaining what is the
00:12:18.800 flow of information in blocks of codes
00:12:21.279 so for example I'm having a variable
00:12:22.800 it's going to go in if and in this
00:12:24.399 branch I'm going to declare another
00:12:25.680 variable this kind of things using both
00:12:28.480 this information the control for graph
00:12:30.320 and the global state I can start type
00:12:32.720 checking the the code I can flow my
00:12:34.639 types in a piece of code and get the
00:12:36.959 type and get the type checking errors if
00:12:39.560 any to support RBS what we did was to
00:12:43.200 add a new phase in between the parsing
00:12:45.680 and the
00:12:46.600 disharing this phase is here to rewrite
00:12:50.800 the a we got from the parser into a new
00:12:53.040 a that has type information that we want
00:12:55.920 to feed to the rest of the pipeline by
00:12:58.399 doing so we can just add this step at
00:13:00.079 the beginning and we very we limit the
00:13:03.200 blast radius of our change the rest of
00:13:05.200 the pipeline doesn't have to care about
00:13:06.560 ABS at all let me give you an example of
00:13:08.959 how that works so as an input I'm having
00:13:11.760 this file with the RBS comments please
00:13:14.720 keep in mind I'm in the so process this
00:13:17.040 is static analysis this is not actually
00:13:19.200 running the code Ruby the Ruby code it's
00:13:21.440 just doing analysis over the code not
00:13:23.440 actually executing it so this is what
00:13:25.279 the Ruby static analyzer sees when it's
00:13:28.079 analyzing my code and trying to find
00:13:30.000 type checking errors so it's seeing
00:13:32.560 those comments and what we want to do is
00:13:35.279 to rewrite the code so SB sees it as if
00:13:38.639 it had like actual SIG calls inside it
00:13:42.240 again I'm not executing this this is
00:13:43.839 just what Sorb is saying so I'm going to
00:13:45.839 take this comment saying I am having an
00:13:47.519 array of node and I'm going to to create
00:13:49.519 the equivalent sig returns array of node
00:13:52.800 uh for sor to see it and so the rest of
00:13:54.880 the type checking pipeline is going to
00:13:56.560 see as I was using sig calls in the
00:14:00.040 middle so for signatures I can express
00:14:03.040 them for example for attribute readers
00:14:04.959 and different attribute accessors by the
00:14:06.720 hash colon comment on top of it and for
00:14:10.480 methods I can use the hashtag comment
00:14:12.320 with the parenthesis the arrow to say
00:14:14.320 the return type I can also split it over
00:14:16.560 multiple lines if I have a very long
00:14:18.079 line and I want to please rubocop these
00:14:20.000 kind of things so we're going to take
00:14:21.680 those comments and translate them into
00:14:23.440 the actual six syntax that so knows for
00:14:27.279 it to type check properly
00:14:29.920 how does that work i'm going to take a
00:14:31.440 very simple a simplified example here so
00:14:33.440 my defu method as a comment on top of it
00:14:36.320 that says it returns void and my
00:14:37.920 attribute reader bar is returning a
00:14:40.760 string when we're running the pass phase
00:14:43.199 on this we're getting back the
00:14:45.720 a and the table of comments that exists
00:14:49.120 in the code we just
00:14:51.240 passed then we're going to visit this a
00:14:54.320 and for each node we're going to check
00:14:55.839 is there a comment that is related to
00:14:57.600 this node so I'm going to find my defaf
00:14:59.680 here looking that oh my def is at line
00:15:01.839 two is there a comment in the previous
00:15:04.360 line that has not been consumed by
00:15:06.800 another node yet so I'm going to look in
00:15:08.560 my table here and see oh okay yeah there
00:15:10.560 is a comment line one so I'm going to
00:15:12.800 associate it to my dev then I'm
00:15:15.440 continuing visiting and I'm going to
00:15:17.360 find this attribute reader at five at
00:15:19.360 line five I'm going to look in my table
00:15:21.040 and say oh is there a comment that is
00:15:23.040 not consumed yet that is related that is
00:15:25.839 before line five and I'm going to find
00:15:27.839 this comment at line four okay
00:15:29.959 associated once I am having this node
00:15:32.800 versus comment association I just have
00:15:34.720 to do the rewriting i'm going to change
00:15:37.199 my est to introduce the s calls that so
00:15:40.320 is going to see as the type-checking
00:15:42.480 artifact it
00:15:43.880 needs so I'm going to introduce a se
00:15:46.240 call before def and I'm going to
00:15:48.000 introduce a sig call before attribute
00:15:50.639 reader to say like oh here are the
00:15:52.480 parameters and here's the return type
00:15:54.560 again I'm not executing this this is
00:15:56.320 just what so sees when it's analyzing
00:15:58.399 the code
00:15:59.720 statically and so for it so is actually
00:16:02.320 just going to see s calls in the middle
00:16:04.320 and it's going to type check it
00:16:06.440 properly we are actually going one step
00:16:08.720 further when we created the S calls here
00:16:10.880 we're lying to so and we're telling it
00:16:12.720 the location of this SQL is somewhere in
00:16:14.880 the comment so I'm passing back the
00:16:16.480 location that I I got from my table here
00:16:19.279 to my SQL to stale so like this thing is
00:16:22.639 actually in the comment at the command
00:16:24.240 location that enables in the so LSP when
00:16:27.120 you hover over something in the comment
00:16:28.959 or you come and click on something in
00:16:30.720 the comment all the power of the LSP
00:16:33.040 actually works and I can over type in
00:16:35.759 the comment giving you like oh here here
00:16:38.160 is the definition of a comment and you
00:16:40.320 can see what is like the the comment of
00:16:42.160 this comment or you can do jump to
00:16:44.720 navigation when you come and click for
00:16:48.600 example um on top of signatures we also
00:16:51.360 have inline assertions inline assertions
00:16:53.199 are here to define the type of local
00:16:54.800 variables or instance variables and you
00:16:56.560 can do multiple things with them you can
00:16:58.160 define the type of a variable with the
00:17:00.320 hashtag colon the type of the variable
00:17:02.160 after that in so you will do t.let and
00:17:05.039 then your variable and finally the co
00:17:07.039 the the type for this you can also cast
00:17:10.000 statically cast the type of a variable
00:17:11.919 when you want to change this type so I
00:17:13.520 had something that was here a node and I
00:17:15.760 know it's actually a tree so I'm going
00:17:17.120 to cast it with hashtag colon as the
00:17:19.839 type of my thing in so you will use
00:17:22.959 t.cast for this and there is a specific
00:17:26.319 kind of cast that is casting to the
00:17:28.319 non-neable version of this thing so I
00:17:30.559 just want to remove remove the nable
00:17:32.480 attribute from it i'm having a nable of
00:17:34.559 node and I know it's not neil so I can
00:17:36.559 cast it to as not nil with the syntax
00:17:39.440 which will be the t do me equivalent in
00:17:43.000 so and we use the exact same approach m
00:17:46.320 mapping the comments directly to the
00:17:48.000 nodes when we're going to visit so
00:17:50.000 outside of the so we have this piece of
00:17:51.600 code here we're going to pass it we're
00:17:53.600 getting the comment table and the a and
00:17:55.679 we're going to start visiting it and
00:17:57.360 find okay I'm having an assign I'm going
00:17:59.039 to look at its return at this value the
00:18:01.360 value finishes line one column 8 do I
00:18:03.919 have a comment after this just that
00:18:05.919 exists so I am having one at like the
00:18:07.919 column 9 so I can associate it with this
00:18:10.080 node going to continue visiting I'm
00:18:11.919 finding the call to bar I'm going to
00:18:13.360 visit the arguments I'm finding this
00:18:15.360 local variable here that is finishing
00:18:17.360 column four at line four I'm going to
00:18:19.039 look my table I'm having a comment okay
00:18:21.360 I'm associating it and the same thing
00:18:23.120 for the return value finding some at
00:18:25.760 column 10 associating it here and
00:18:28.160 finally I'm going to rewrite it so I'm
00:18:29.919 having those nodes with the comment
00:18:31.120 association and I'm going to introduce
00:18:33.440 the nodes in the that represent the
00:18:35.200 calls that I will have to t dot let t
00:18:37.039 dot cast t dot must and using t label
00:18:39.600 and all the apparatus of
00:18:42.520 server I can do this common association
00:18:44.960 with also pieces of expression for
00:18:46.799 example when I need to cast or uh when I
00:18:49.520 need to cast for example a musts in the
00:18:51.120 middle of an expression so I can split
00:18:52.960 it into multiple lines sadly Ruby only
00:18:55.120 supports end of the line comments with
00:18:56.720 the hashtag if we add inline comments
00:18:58.960 that will be even better if you saw the
00:19:01.440 bit like the the Ruby against the world
00:19:03.360 Ruby committers against the world this
00:19:04.720 morning maybe one day it will exist so
00:19:07.120 if you want to use those um hashtag
00:19:09.520 comments on expressions you have to
00:19:11.200 split your expression over multiple
00:19:12.640 lines so here I can express that my at
00:19:14.960 nodes the dot first call I'm going to
00:19:17.280 cast the return value to a tree then on
00:19:20.240 this tree I'm going to call nodes again
00:19:21.919 and call do first and I'm going to cast
00:19:24.320 the ver the return of first as non label
00:19:26.400 because I know I have at least one and
00:19:28.400 finally I'm going to call name on it i
00:19:30.799 can do the same thing for arguments and
00:19:32.960 split my call over multiple lines to pre
00:19:36.000 to specify the type of the arguments and
00:19:38.799 here I'm also taking in consideration
00:19:40.320 the comma that may exist and just
00:19:41.760 looking is there a comment after the
00:19:44.520 comma you can use those command those um
00:19:48.000 casts and things directly inside arrays
00:19:51.280 for typing the element of an arrays or
00:19:54.080 typing the values in a hash can also
00:19:56.799 apply it to nodes and all the kind of
00:19:59.679 expression we use in Ruby
00:20:02.960 for the things that do not exist in ABS
00:20:04.880 that we still need with so for example
00:20:06.880 declaring a class as abstract we have to
00:20:08.640 find another way because we cannot use
00:20:10.160 the ABS comment itself to represent this
00:20:13.200 because there is no syntax equivalent
00:20:14.720 for it so we just decided to reuse the
00:20:16.640 same kind of aird do annotations I can
00:20:18.880 use a hashtag at abstract on top of my
00:20:20.880 class and I'm going to call generate the
00:20:23.679 equivalent so DSL for this that is
00:20:26.080 calling the abstract bang method same
00:20:28.640 thing for the interface and I'm going to
00:20:30.480 call the uh so with the at at interface
00:20:33.440 I'm going to call the interface bang
00:20:35.400 method finally so has this feature that
00:20:37.840 is requiring an ancestor that means that
00:20:39.440 I can only include the module indexable
00:20:42.880 in a class that descend from a node so
00:20:45.360 like a subclass of a node and for this
00:20:47.760 you need to call this require ancestor
00:20:50.240 method we're representing this again
00:20:51.679 with the at comment uh just on top of
00:20:54.320 the module
00:20:56.159 we can also use this annotations on top
00:20:58.000 of methods for example when you want to
00:20:59.679 define that the method is abstract uh
00:21:02.080 very useful when you're having a large
00:21:03.440 codebase and you want to establish
00:21:04.720 contracts with other teams that are
00:21:06.240 working on the same codebase than you uh
00:21:08.240 in so you represent this by calling
00:21:09.919 abstract inside the s block for us in
00:21:11.919 ABS we're going to do it with the at
00:21:13.760 abstract on top of the
00:21:15.480 method uh we can do the override when
00:21:18.480 you're defining an an abstract method uh
00:21:20.960 we use override in a sig block in so
00:21:23.360 we're going to do that with at override
00:21:25.600 in a comment this is still very
00:21:26.960 experimental but that works uh finally
00:21:30.080 generics again experimental but like you
00:21:32.400 can make it work we decided to use
00:21:35.039 because there is no real way in Ruby to
00:21:37.120 express what is a generic we decided to
00:21:38.799 use the the equivalent of a signature
00:21:40.559 before a class so on top of your class
00:21:42.400 or your module you could do hash colon
00:21:45.039 with in brackets the types that your
00:21:47.360 generic container is taking so in We
00:21:50.559 will do that by creating a constant E
00:21:52.640 that calls type parameter type member
00:21:55.039 and saying like for example here I'm
00:21:56.640 having a E that is my type member that
00:21:58.240 is having an upper bound to node I can
00:22:00.880 represent this with bracket E and um
00:22:03.840 lesser than nodes in my LBS signature
00:22:06.320 and that's going to generate the same
00:22:07.600 code here again this is just what so is
00:22:09.840 this is not what you actually execute at
00:22:12.760 runtime when we create the when we
00:22:15.919 instantiate the class in so because we
00:22:18.240 have this runtime equivalent here um
00:22:21.280 when we're using sober runtime it's
00:22:23.039 going to actually create the bracket
00:22:24.559 method for us on the generic class and
00:22:26.720 we don't have this anymore we don't want
00:22:28.240 to run this anymore so the equivalent
00:22:30.799 for this will be to let to tlet with the
00:22:33.679 comment here to say my tree new is
00:22:35.919 actually a tree of node and I'm happy
00:22:37.520 with this then I can use this type
00:22:40.320 members directly into my onto my methods
00:22:42.880 so for example my show operator here is
00:22:45.360 going to take something that is typed as
00:22:46.799 a he uh and I and type check when I'm
00:22:49.919 calling tree shovel devot new I can make
00:22:52.559 sure that dev do new is actually a
00:22:54.080 subtype of my
00:22:56.360 e finally ins and so you can also define
00:23:00.799 method generics where you have a type
00:23:02.320 member that only exists in the context
00:23:03.919 of a method and here we're doing this
00:23:05.440 with the
00:23:06.520 n where we're going to define okay my
00:23:09.120 method here is taking a par type
00:23:10.720 parameter that is n and it's going to
00:23:12.880 take the class the singleton class of n
00:23:15.120 and finally return an array of n so all
00:23:18.080 that works is My lookup methods ex
00:23:20.159 expect a type and it's going to look
00:23:22.400 inside its own tree if there is like
00:23:24.159 something that matches this type so for
00:23:25.520 example I want to look up in my tree if
00:23:27.120 they are like defaf uh instances and
00:23:29.360 it's going to return an an array of the
00:23:31.120 instances of defaf in so this is much
00:23:33.600 more verbose this is what we're going
00:23:34.880 the equivalent we're going to generate
00:23:36.480 we're calling to type uh parameters
00:23:38.880 we're creating a symbol n is going to
00:23:41.039 represent my type parameter and when I
00:23:42.960 want to reference this reference it
00:23:44.720 inside my signature I have to call uh
00:23:47.200 t.type type parameter with the symbol n
00:23:51.080 here so that's a lot of changes if you
00:23:54.640 need to migrate your codebase from the
00:23:56.720 old so syntax to the RBS comments that's
00:23:59.120 kind of painful to do manually for this
00:24:00.960 we decided to provide uh automated
00:24:03.440 translation tool uh we added this
00:24:05.600 feature to a tool that we own that is
00:24:07.360 called spoom that is doing a bunch of
00:24:08.640 things around static analysis for code
00:24:10.720 uh and also bunch of different so
00:24:12.720 related tooling so if you run spoom sb6
00:24:16.000 translates in your codebase it's going
00:24:17.360 to translate all your old sob syntax
00:24:20.000 signatures into the syntax comment
00:24:23.120 equivalent and we have the same thing
00:24:24.880 for assertion with spoon srb assession
00:24:29.320 translate uh of course big codebase you
00:24:32.400 don't want to be changing all the
00:24:33.840 signatures at the same time that's going
00:24:35.279 to be a hopeful uh awful PR to pull
00:24:37.919 request to review by your teammates so
00:24:39.600 maybe you want to do this in multiple
00:24:40.960 installment installment so for example
00:24:43.279 you could be just migrating file by file
00:24:45.120 and opening pull request for this so I
00:24:47.440 coined this thing that I like to be the
00:24:49.039 gradual gradual typing where you can
00:24:50.799 still use the old service syntax at the
00:24:52.799 same time that you're doing using the
00:24:54.159 ABS comments and everything works
00:24:57.960 properly you may have noticed that we
00:25:00.240 also have syntax highlighting for those
00:25:02.320 comments where we having like this
00:25:03.840 coloration here the void has a certain
00:25:05.760 type and stuff we already shipped that
00:25:08.000 inside a Ruby LSP and it's already
00:25:09.679 providing um the syntax highlighting for
00:25:12.240 RBS comments if you have an LSP that is
00:25:14.480 up to
00:25:15.320 date and what if you really really
00:25:17.520 really don't want type annotations in
00:25:20.400 your code well we actually also provided
00:25:22.880 an option in the Ruby LSP where you can
00:25:24.559 twe tweak the opacity of signatures and
00:25:27.440 type annotations so you can see you can
00:25:29.679 say I want the opacity of my type
00:25:31.360 annotation to zero and it's actually
00:25:33.279 going to make disappear your type
00:25:34.720 annotation so your colleagues can still
00:25:36.400 be happy and you can be happy and
00:25:38.080 everybody is happy and this
00:25:40.679 perfect of course using comments are
00:25:43.440 some drawbacks let's just cover them
00:25:45.120 quickly uh the first one is the type
00:25:47.440 checking time is longer because we need
00:25:49.919 to now pass the comments and we need to
00:25:51.679 pass the RBS that is inside the comments
00:25:54.080 so with the proto version we were like
00:25:56.159 up to two times slower than the vanilla
00:25:59.120 sorbet uh we actually worked on this a
00:26:02.080 lot my colleague KHN has been doing like
00:26:03.919 tremendous improvement to it and we
00:26:05.600 actually now like very close to having
00:26:07.440 no overhead at all uh it's not released
00:26:09.679 yet but very soon we also have plans to
00:26:12.720 migrate the internal parser of Sorbet to
00:26:14.720 Prism to benefit from Prism speed and
00:26:16.720 also we're thinking about maybe
00:26:18.480 migrating the comment passing and
00:26:19.919 comment association to Prism directly to
00:26:21.760 avoid the overhand in
00:26:23.640 so because it's comments we don't have
00:26:26.000 any kind of rubocop support for it yet
00:26:27.919 so no way to enforce spaces commas
00:26:30.720 before after commas and parentheses and
00:26:32.799 stuff um if you want to provide rubocop
00:26:36.240 support for LBS signatures please do so
00:26:38.559 that will be super
00:26:40.440 helpful and the last problem is the last
00:26:43.360 the loss of runtime type checking so
00:26:45.440 everything I explained right now is in
00:26:47.919 the static process when you're running
00:26:49.679 so on your codebase like running sbtc on
00:26:52.559 your codebase this is what so was doing
00:26:54.320 and was seeing and it was using your sig
00:26:57.200 to do the type checking
00:26:59.080 statically but with so because we're
00:27:02.400 actually having those sig artifact in
00:27:04.720 the code you can also enable what is
00:27:06.480 called runtime type checking where
00:27:07.919 you're going to check to check the type
00:27:09.279 of things at runtime while it's running
00:27:11.600 this may not be something you want to
00:27:13.039 run in production because there is some
00:27:14.880 overhead but when you're in development
00:27:17.039 mode or on CI that will be super useful
00:27:19.279 to know that you're casting something to
00:27:21.039 the proper type and you're not trying to
00:27:22.960 lie to the static type checker because
00:27:25.279 we changed the SI calls into comments
00:27:27.360 there is no more artifacts that the
00:27:28.880 runtime type checker could be using so
00:27:31.120 we're losing this runtime type checking
00:27:32.720 and that may be a problem because it's
00:27:34.240 very easy to lie to the type checker and
00:27:36.000 then you actually having a type error
00:27:37.840 that the type checker didn't find we
00:27:40.480 have a way to reenable it using Ruby
00:27:42.400 next uh require hooks that means that at
00:27:44.960 runtime when you're going to execute
00:27:46.559 your code when you're going to load a
00:27:48.400 file require it at require time we're
00:27:51.039 going to rewrite this file in the same
00:27:52.640 way we did inside so when we're going to
00:27:54.640 do it in Ruby to reingject the SIG in
00:27:57.440 your actual executed code disclaimer do
00:28:00.640 not do that in production this is very
00:28:02.720 very not performant but in development
00:28:05.200 mode that may be super useful and so
00:28:07.360 when you're going to run your code if
00:28:09.200 you need it to enable runtime type
00:28:11.120 checking we can rewrite this file and
00:28:12.640 reinject the sig blocks inside your
00:28:16.039 file everything I explained I showed
00:28:19.039 today is already live you can see it by
00:28:21.600 yourself we already started migrating
00:28:23.360 some of the gems that we own so for
00:28:24.799 example the Ruby LSP Tapioa Airbi
00:28:27.520 already use this new RBS syntax and
00:28:29.600 we're in the process of removing the
00:28:30.720 sober runtime dependency from it so you
00:28:32.960 can already see all we went from
00:28:34.559 removing the signatures the annotations
00:28:36.559 translating the annotations and stuff
00:28:38.480 spoiler alert is mostly running a tool
00:28:40.320 to translate it
00:28:41.640 automatically if you want to learn more
00:28:43.600 about it you can also install the last
00:28:45.279 version of Sorb and take a look at the
00:28:47.200 documentation to see what are the
00:28:50.080 different features and it works and get
00:28:51.919 started in your codebase with this I'm
00:28:54.640 going to wish you a happy typing and if
00:28:57.440 you have any question please find me i'm
00:28:59.039 the guy looking like this in the crowd
00:29:01.279 and if you're interested in working in
00:29:02.559 the toolings anything about developer
00:29:05.120 developer experience Ruby and Rails
00:29:06.880 infrastructure please take a look at the
00:29:08.640 QR code here and apply thank you very
00:29:11.279 much
Explore all talks recorded at RubyKaigi 2025
+66