Skip to content

Commit 6ab1548

Browse files
authored
feat(ruby): add static type checking with Sorbet (#585)
Ruby, like most scripting languages, is not statically typed. While great for initial productivity, it can make changes challenging since errors are only caught at runtime, and only if that path is exercised. There are two type checking solutions for Ruby: [Sorbet](https://sorbet.org/docs/overview) and [RBS/Steep](https://github.com/soutaro/steep). I tried both with the UDB code base. Based on that evaluation, I determined that Sorbet was higher quality, had much better documentation, and more mature. It's also in commercial use so is likely to be supported long-term. Sorbet uses _gradual type checking_: you can add type signatures to subsets of the code, and Sorbet only checks what is known. Thus, we can start using it without annotating the entire code base up front. This PR highlights the utility of the tool: it found several bugs already. There is a *cost* to using Sorbet, mostly in terms of added development time * The signature syntax needs to be understood by developers. * Some extra [assertions]https://sorbet.org/docs/type-assertions are needed in code aside from the function signatures * Though I found the tool is surprisingly helpful in telling you exactly what assertion is needed (even with an autocorrect option)   Let's discuss more here, but based on this experiment my recommendation is that we merge this PR.
1 parent ef12b37 commit 6ab1548

26 files changed

+1157
-986
lines changed

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"zhwu95.riscv",
3232
"mathematic.vscode-pdf",
3333
"CraigMaslowski.erb",
34-
"HowerLimited.udb-extension-pack-vscode"
34+
"HowerLimited.udb-extension-pack-vscode",
35+
"sorbet.sorbet-vscode-extension"
3536
]
3637
}
3738
},

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ __pycache__/
2121
.pytest_cache/
2222
*.bak
2323
*.log
24+
sorbet

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@
1414
[submodule "ext/riscv-tests"]
1515
path = ext/riscv-tests
1616
url = https://github.com/riscv-software-src/riscv-tests.git
17+
[submodule "ext/rbi-central"]
18+
path = ext/rbi-central
19+
url = https://github.com/Shopify/rbi-central

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ repos:
119119
rev: v5.0.2
120120
hooks:
121121
- id: reuse-lint-file
122-
exclude: COMMIT_EDITMSG
122+
exclude: COMMIT_EDITMSG|MERGE_MSG
123123

124124
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
125125
rev: v9.22.0

.sorbet-config

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
--dir
2+
lib
3+
--dir
4+
sorbet/rbi
5+
--file
6+
Rakefile
7+
--file
8+
backends/ext_pdf_doc/idl_lexer.rb
9+
--file
10+
backends/cpp_hart_gen/lib/template_helpers.rb

.tapioca-config.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
gem:
2+
outdir: .home/.sorbet/rbi/gems
3+
file_header: true
4+
exclude:
5+
[
6+
rdoc,
7+
asciidoctor-pdf,
8+
solargraph,
9+
webrick,
10+
ttfunk,
11+
tapioca,
12+
rubocop,
13+
rubocop-minitest,
14+
spoom,
15+
rdbg,
16+
parser,
17+
]
18+
dsl:
19+
outdir: .home/.sorbet/rbi/dsl
20+
# Add your `dsl` command parameters here:
21+
#
22+
# exclude:
23+
# - SomeGeneratorName
24+
# workers: 5
25+
annotations:
26+
sources:
27+
- ext/rbi-central

Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ gem "pygments.rb"
1616
gem "rake", "~> 13.0"
1717
gem "rouge"
1818
gem "ruby-progressbar", "~> 1.13"
19+
gem "sorbet-runtime"
1920
gem "treetop", "1.6.12"
2021
gem "ttfunk", "1.7" # needed to avoid having asciidoctor-pdf dependencies pulling in a buggy version of ttunk (1.8)
2122
gem "webrick"
@@ -31,4 +32,7 @@ group :development do
3132
gem "ruby-prof"
3233
gem "ruby-prof-flamegraph", git: "https://github.com/oozou/ruby-prof-flamegraph.git", ref: "fc3c437", require: false
3334
gem "solargraph"
35+
gem "sorbet"
36+
gem "spoom"
37+
gem "tapioca", require: false
3438
end

Gemfile.lock

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ GEM
6565
reline (>= 0.3.8)
6666
diff-lcs (1.6.0)
6767
drb (2.2.1)
68+
erubi (1.13.1)
6869
hana (1.3.7)
6970
hashery (2.1.2)
7071
i18n (1.14.7)
@@ -89,6 +90,7 @@ GEM
8990
logger (1.6.6)
9091
matrix (0.4.2)
9192
minitest (5.25.5)
93+
netrc (0.11.0)
9294
nkf (0.2.0)
9395
nokogiri (1.18.6-aarch64-linux-gnu)
9496
racc (~> 1.4)
@@ -126,6 +128,7 @@ GEM
126128
pdf-reader (~> 2.0)
127129
prawn (~> 2.2)
128130
prettyprint (0.2.0)
131+
prism (1.4.0)
129132
psych (5.2.3)
130133
date
131134
stringio
@@ -134,6 +137,10 @@ GEM
134137
racc (1.8.1)
135138
rainbow (3.1.1)
136139
rake (13.2.1)
140+
rbi (0.3.1)
141+
prism (~> 1.0)
142+
rbs (>= 3.4.4)
143+
sorbet-runtime (>= 0.5.9204)
137144
rbs (3.9.1)
138145
logger
139146
rdbg (0.1.0)
@@ -189,7 +196,31 @@ GEM
189196
tilt (~> 2.0)
190197
yard (~> 0.9, >= 0.9.24)
191198
yard-solargraph (~> 0.1)
199+
sorbet (0.5.11966)
200+
sorbet-static (= 0.5.11966)
201+
sorbet-runtime (0.5.11966)
202+
sorbet-static (0.5.11966-aarch64-linux)
203+
sorbet-static (0.5.11966-x86_64-linux)
204+
sorbet-static-and-runtime (0.5.11966)
205+
sorbet (= 0.5.11966)
206+
sorbet-runtime (= 0.5.11966)
207+
spoom (1.6.1)
208+
erubi (>= 1.10.0)
209+
prism (>= 0.28.0)
210+
rbi (>= 0.2.3)
211+
sorbet-static-and-runtime (>= 0.5.10187)
212+
thor (>= 0.19.2)
192213
stringio (3.1.5)
214+
tapioca (0.16.11)
215+
benchmark
216+
bundler (>= 2.2.25)
217+
netrc (>= 0.11.0)
218+
parallel (>= 1.21.0)
219+
rbi (~> 0.2)
220+
sorbet-static-and-runtime (>= 0.5.11087)
221+
spoom (>= 1.2.0)
222+
thor (>= 1.2.0)
223+
yard-sorbet
193224
thor (1.3.2)
194225
tilt (2.6.0)
195226
treetop (1.6.12)
@@ -208,6 +239,9 @@ GEM
208239
yard (0.9.37)
209240
yard-solargraph (0.1.0)
210241
yard (~> 0.9)
242+
yard-sorbet (0.9.0)
243+
sorbet-runtime
244+
yard
211245

212246
PLATFORMS
213247
aarch64-linux-gnu
@@ -234,6 +268,10 @@ DEPENDENCIES
234268
ruby-prof-flamegraph!
235269
ruby-progressbar (~> 1.13)
236270
solargraph
271+
sorbet
272+
sorbet-runtime
273+
spoom
274+
tapioca
237275
treetop (= 1.6.12)
238276
ttfunk (= 1.7)
239277
webrick

Rakefile

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# frozen_string_literal: true
2+
# typed: true
3+
4+
require "sorbet-runtime"
5+
T.bind(self, T.all(Rake::DSL, Object))
6+
extend T::Sig
27

38
Encoding.default_external = "UTF-8"
49

@@ -37,8 +42,8 @@ directory "#{$root}/.stamps"
3742

3843
# @param config_locator [String or Pathname]
3944
# @return [ConfiguredArchitecture]
45+
sig { params(config_locator: T.any(String, Pathname)).returns(ConfiguredArchitecture) }
4046
def cfg_arch_for(config_locator)
41-
raise ArgumentError, "expecting String or Pathname" unless config_locator.is_a?(String) || config_locator.is_a?(Pathname)
4247
config_locator = config_locator.to_s
4348

4449
$cfg_archs ||= {}
@@ -163,6 +168,11 @@ namespace :serve do
163168
end
164169
end
165170

171+
sig { params(test_files: T::Array[String]).returns(String) }
172+
def make_test_cmd(test_files)
173+
"-Ilib:test -w -e 'require \"minitest/autorun\"; #{test_files.map{ |f| "require \"#{f}\""}.join("; ")}' --"
174+
end
175+
166176
namespace :test do
167177

168178
# "Run the cross-validation against LLVM"
@@ -175,18 +185,20 @@ namespace :test do
175185
end
176186
# "Run the IDL compiler test suite"
177187
task :idl_compiler do
178-
t = Minitest::TestTask.new(:lib_test)
179-
t.test_globs = ["#{$root}/lib/idl/tests/test_*.rb"]
180-
t.process_env
181-
ruby t.make_test_cmd
188+
test_files = Dir["#{$root}/lib/idl/tests/test_*.rb"]
189+
ruby make_test_cmd(test_files)
182190
end
183191

184192
# "Run the Ruby library test suite"
185193
task :lib do
186-
t = Minitest::TestTask.new(:lib_test)
187-
t.test_globs = ["#{$root}/lib/test/test_*.rb"]
188-
t.process_env
189-
ruby t.make_test_cmd
194+
test_files = Dir["#{$root}/lib/test/test_*.rb"]
195+
196+
ruby make_test_cmd(test_files)
197+
end
198+
199+
desc "Type-check the Ruby library"
200+
task :sorbet do
201+
sh "srb tc @.sorbet-config"
190202
end
191203
end
192204

@@ -208,13 +220,13 @@ namespace :test do
208220

209221
cfg_arch = cfg_arch_for("_")
210222
insts = cfg_arch.instructions
211-
failed = false
223+
failed = T.let(false, T::Boolean)
212224
insts.each_with_index do |inst, idx|
213225
[32, 64].each do |xlen|
214226
next unless inst.defined_in_base?(xlen)
215227

216228
(idx...insts.size).each do |other_idx|
217-
other_inst = insts[other_idx]
229+
other_inst = T.must(insts[other_idx])
218230
next unless other_inst.defined_in_base?(xlen)
219231
next if other_inst == inst
220232

@@ -236,13 +248,13 @@ namespace :test do
236248

237249
cfg_arch = cfg_arch_for("_")
238250
csrs = cfg_arch.csrs
239-
failed = false
251+
failed = T.let(false, T::Boolean)
240252
csrs.each_with_index do |csr, idx|
241253
[32, 64].each do |xlen|
242254
next unless csr.defined_in_base?(xlen)
243255

244256
(idx...csrs.size).each do |other_idx|
245-
other_csr = csrs[other_idx]
257+
other_csr = T.must(csrs[other_idx])
246258
next unless other_csr.defined_in_base?(xlen)
247259
next if other_csr == csr
248260

@@ -284,7 +296,6 @@ def insert_warning(str, from)
284296
first_line = lines.shift
285297
lines.unshift(first_line, "\n# WARNING: This file is auto-generated from #{Pathname.new(from).relative_path_from($root)}").join("")
286298
end
287-
private :insert_warning
288299

289300
(3..31).each do |hpm_num|
290301
file "#{$root}/arch/csr/Zihpm/mhpmcounter#{hpm_num}.yaml" => [
@@ -437,6 +448,8 @@ namespace :test do
437448
Rake::Task["test:idl_compiler"].invoke
438449
$logger.info "Running test:lib"
439450
Rake::Task["test:lib"].invoke
451+
$logger.info "UPDATE: Running test:sorbet"
452+
Rake::Task["test:sorbet"].invoke
440453
$logger.info "Running test:schema"
441454
Rake::Task["test:schema"].invoke
442455
$logger.info "UPDATE: Running test:idl for rv32"

backends/cpp_hart_gen/templates/csrs_impl.hxx.erb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,7 @@ void <%= name_of(:csr_field, cfg_arch, csr.name, field.name) %><SocType>::reset(
5656
<%- end -%>
5757
<%- else -%>
5858
auto val_fn = [this]() ->
59-
<%- if field.could_be_undefined? -%>
6059
PossiblyUnknownBits<<%= max_width + 1 %>>
61-
<%- else -%>
62-
Bits<<%= max_width + 1 %>>
63-
<%- end -%>
6460
{
6561
<%= ast.gen_cpp(cfg_arch.symtab, 8) %>
6662
};

0 commit comments

Comments
 (0)