houndstooth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +49 -0
- data/README.md +99 -0
- data/bin/houndstooth.rb +183 -0
- data/fuzz/cases/x.rb +8 -0
- data/fuzz/cases/y.rb +8 -0
- data/fuzz/cases/z.rb +22 -0
- data/fuzz/ruby.dict +64 -0
- data/fuzz/run +21 -0
- data/lib/houndstooth/environment/builder.rb +260 -0
- data/lib/houndstooth/environment/type_parser.rb +149 -0
- data/lib/houndstooth/environment/types/basic/type.rb +85 -0
- data/lib/houndstooth/environment/types/basic/type_instance.rb +54 -0
- data/lib/houndstooth/environment/types/compound/union_type.rb +72 -0
- data/lib/houndstooth/environment/types/defined/base_defined_type.rb +23 -0
- data/lib/houndstooth/environment/types/defined/defined_type.rb +137 -0
- data/lib/houndstooth/environment/types/defined/pending_defined_type.rb +14 -0
- data/lib/houndstooth/environment/types/method/method.rb +79 -0
- data/lib/houndstooth/environment/types/method/method_type.rb +144 -0
- data/lib/houndstooth/environment/types/method/parameters.rb +53 -0
- data/lib/houndstooth/environment/types/method/special_constructor_method.rb +15 -0
- data/lib/houndstooth/environment/types/special/instance_type.rb +9 -0
- data/lib/houndstooth/environment/types/special/self_type.rb +9 -0
- data/lib/houndstooth/environment/types/special/type_parameter_placeholder.rb +38 -0
- data/lib/houndstooth/environment/types/special/untyped_type.rb +11 -0
- data/lib/houndstooth/environment/types/special/void_type.rb +12 -0
- data/lib/houndstooth/environment/types.rb +3 -0
- data/lib/houndstooth/environment.rb +74 -0
- data/lib/houndstooth/errors.rb +53 -0
- data/lib/houndstooth/instructions.rb +698 -0
- data/lib/houndstooth/interpreter/const_internal.rb +148 -0
- data/lib/houndstooth/interpreter/objects.rb +142 -0
- data/lib/houndstooth/interpreter/runtime.rb +309 -0
- data/lib/houndstooth/interpreter.rb +7 -0
- data/lib/houndstooth/semantic_node/control_flow.rb +218 -0
- data/lib/houndstooth/semantic_node/definitions.rb +253 -0
- data/lib/houndstooth/semantic_node/identifiers.rb +308 -0
- data/lib/houndstooth/semantic_node/keywords.rb +45 -0
- data/lib/houndstooth/semantic_node/literals.rb +226 -0
- data/lib/houndstooth/semantic_node/operators.rb +126 -0
- data/lib/houndstooth/semantic_node/parameters.rb +108 -0
- data/lib/houndstooth/semantic_node/send.rb +349 -0
- data/lib/houndstooth/semantic_node/super.rb +12 -0
- data/lib/houndstooth/semantic_node.rb +119 -0
- data/lib/houndstooth/stdlib.rb +6 -0
- data/lib/houndstooth/type_checker.rb +462 -0
- data/lib/houndstooth.rb +53 -0
- data/spec/ast_to_node_spec.rb +889 -0
- data/spec/environment_spec.rb +323 -0
- data/spec/instructions_spec.rb +291 -0
- data/spec/integration_spec.rb +785 -0
- data/spec/interpreter_spec.rb +170 -0
- data/spec/self_spec.rb +7 -0
- data/spec/spec_helper.rb +50 -0
- data/test/ruby_interpreter_test.rb +162 -0
- data/types/stdlib.htt +170 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0febf254b01185f48cc75c1fc717a524e90dedaebbcbf156b4db2da8f2c6cc4d
|
4
|
+
data.tar.gz: 7d553f1ad85674e0fd5286a9c0414c5c0dfd72ba8e6ae7a648ace0f2fe566222
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bfd945c4d810464e80e389711cc7e8eb367688a7a76c6fd5db1da45e5a509163d3d7dbe1f54d97339913620205134848522304a8c21a19fe1c03dcc41f284cc0
|
7
|
+
data.tar.gz: abfb4d5da0b0232d2279b47d1be19dd4b4935a0f6949679e345f27eb06420eecbb1c26bf1af52ff58c9c8ff8d3254e99544d252ed4febc728d4bf25fc32ffa24
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.0.3
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
GIT
|
2
|
+
remote: https://github.com/richo/afl-ruby.git
|
3
|
+
revision: cbaad7a5fadca7b1617a211d571f7badc80fec82
|
4
|
+
specs:
|
5
|
+
afl (0.0.3)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
ast (2.4.2)
|
11
|
+
diff-lcs (1.5.0)
|
12
|
+
docile (1.4.0)
|
13
|
+
optimist (3.0.1)
|
14
|
+
parser (3.1.0.0)
|
15
|
+
ast (~> 2.4.1)
|
16
|
+
rbs (2.1.0)
|
17
|
+
rspec (3.10.0)
|
18
|
+
rspec-core (~> 3.10.0)
|
19
|
+
rspec-expectations (~> 3.10.0)
|
20
|
+
rspec-mocks (~> 3.10.0)
|
21
|
+
rspec-core (3.10.2)
|
22
|
+
rspec-support (~> 3.10.0)
|
23
|
+
rspec-expectations (3.10.2)
|
24
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
25
|
+
rspec-support (~> 3.10.0)
|
26
|
+
rspec-mocks (3.10.3)
|
27
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
28
|
+
rspec-support (~> 3.10.0)
|
29
|
+
rspec-support (3.10.3)
|
30
|
+
simplecov (0.21.2)
|
31
|
+
docile (~> 1.1)
|
32
|
+
simplecov-html (~> 0.11)
|
33
|
+
simplecov_json_formatter (~> 0.1)
|
34
|
+
simplecov-html (0.12.3)
|
35
|
+
simplecov_json_formatter (0.1.3)
|
36
|
+
|
37
|
+
PLATFORMS
|
38
|
+
arm64-darwin-21
|
39
|
+
|
40
|
+
DEPENDENCIES
|
41
|
+
afl!
|
42
|
+
optimist
|
43
|
+
parser
|
44
|
+
rbs
|
45
|
+
rspec
|
46
|
+
simplecov
|
47
|
+
|
48
|
+
BUNDLED WITH
|
49
|
+
2.2.22
|
data/README.md
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# Houndstooth
|
2
|
+
|
3
|
+
Houndstooth is a **highly-experimental Ruby static type checker**, which is uniquely
|
4
|
+
**metaprogramming-aware**.
|
5
|
+
|
6
|
+
Houndstooth was created for my final-year project at the University of York. It is far from
|
7
|
+
production-ready, and should be treated here as a proof-of-concept!
|
8
|
+
|
9
|
+
Here's an annotated example of what this enables you to do:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
#!var @name String
|
13
|
+
#!var @graduate Boolean
|
14
|
+
class Student
|
15
|
+
#!arg String
|
16
|
+
attr_reader :name
|
17
|
+
|
18
|
+
# Now we'd like to define an accessor for our boolean variable, @graduate. But we usually like
|
19
|
+
# methods returning a boolean to end in ?, so we can't use `attr_accessor`.
|
20
|
+
# Instead, let's define our own helper, `bool_accessor`
|
21
|
+
|
22
|
+
#: (Symbol) -> void
|
23
|
+
#!const required
|
24
|
+
# ^ This special annotation means, "hey, type checker - you need to check out what this does!"
|
25
|
+
def self.bool_accessor(name)
|
26
|
+
# Define our method #<name>?
|
27
|
+
#!arg Boolean
|
28
|
+
attr_reader "#{name}?".to_sym
|
29
|
+
|
30
|
+
# ...and also define a normal writer, #<name>=
|
31
|
+
#!arg Boolean
|
32
|
+
attr_writer name
|
33
|
+
end
|
34
|
+
|
35
|
+
# Now use our neat new helper
|
36
|
+
bool_accessor :graduate
|
37
|
+
|
38
|
+
#: (String) -> void
|
39
|
+
def initialize(name)
|
40
|
+
@name = name
|
41
|
+
@graduate = false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# The type checker sees those `graduate?` and `graduate=` definitions, even though they were
|
46
|
+
# dynamic!
|
47
|
+
s = Student.new("Aaron")
|
48
|
+
s.graduate? # => false
|
49
|
+
s.graduate = true
|
50
|
+
s.graduate? # => true
|
51
|
+
```
|
52
|
+
|
53
|
+
It even understands control flow such as loops:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class Adder
|
57
|
+
1000.times do |i,|
|
58
|
+
#!arg Integer
|
59
|
+
#!arg Integer
|
60
|
+
# ^ These annotations are the parameter type (first one) and return type (second one)
|
61
|
+
define_method :"add_#{i}" do |input,|
|
62
|
+
i + input
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Now we can add to our heart's content
|
68
|
+
a = Adder.new
|
69
|
+
x = a.add_123(a.add_5(3))
|
70
|
+
```
|
71
|
+
|
72
|
+
Houndstooth includes a minimal Ruby interpreter capable of evaluating a pure and deterministic
|
73
|
+
subset of the language. Using this, it executes portions of your codebase to discover methods which
|
74
|
+
will be dynamically defined at runtime.
|
75
|
+
|
76
|
+
All methods can optionally be tagged, either as:
|
77
|
+
|
78
|
+
- _const_, which means they _can_ be executed by the interpreter. Such methods include `Integer#+`,
|
79
|
+
`Array#each`, and `String#length`.
|
80
|
+
- _const-internal_, which means they **must** be executed by the interpreter wherever they appear
|
81
|
+
in your codebase. These are your metaprogramming methods, like `define_method` and `attr_reader`.
|
82
|
+
|
83
|
+
The strict requirements of const-internal mean that Houndstooth's interpreter is guaranteed to
|
84
|
+
discover any invocations of metaprogramming, and therefore knows about the entire environment of
|
85
|
+
your program.
|
86
|
+
|
87
|
+
Thanks to this tagging mechanism, it becomes a type error to write definitions which are not
|
88
|
+
guaranteed to exist at runtime, or depend on non-deterministic data:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class A
|
92
|
+
# This is a type error!
|
93
|
+
# Cannot call non-const method `rand` on `#<interpreter object: <Eigen:Kernel>>` from const context
|
94
|
+
if Kernel.rand > 0.5
|
95
|
+
#: () -> void
|
96
|
+
def x; end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
```
|
data/bin/houndstooth.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'optimist'
|
2
|
+
require 'afl'
|
3
|
+
require_relative '../lib/houndstooth'
|
4
|
+
|
5
|
+
options = Optimist::options do
|
6
|
+
banner "Houndstooth: A Ruby type checker"
|
7
|
+
|
8
|
+
opt :file, "file to type check", type: :string, short: :f
|
9
|
+
opt :code, "code string to type check", type: :string, short: :e
|
10
|
+
|
11
|
+
opt :no_stdlib, "don't load stdlib types (for debugging - almost guaranteed to cause weird problems!)", short: :s
|
12
|
+
opt :fatal_interpreter, "exit on first interpreter error, and print internal backtrace", short: :x
|
13
|
+
|
14
|
+
opt :debug_nodes, "print parsed node tree", short: :none
|
15
|
+
opt :debug_environment, "print known types and methods", short: :none
|
16
|
+
opt :debug_instructions, "print generated instructions", short: :none
|
17
|
+
opt :debug_type_changes, "print instructions after type changes", short: :none
|
18
|
+
opt :verbose_instructions, "show more detail in instruction debug views", short: :none
|
19
|
+
|
20
|
+
opt :instrument, "AFL instrumentation", short: :none
|
21
|
+
end
|
22
|
+
|
23
|
+
def main(options)
|
24
|
+
$cli_options = options
|
25
|
+
# Checks if there are any errors. If so, prints them and aborts.
|
26
|
+
def abort_on_error!
|
27
|
+
if Houndstooth::Errors.errors.any?
|
28
|
+
Houndstooth::Errors.errors.each do |error|
|
29
|
+
puts error.format
|
30
|
+
puts
|
31
|
+
end
|
32
|
+
|
33
|
+
# The fuzzer will view an abort as a crash, so if we're running under instrumentation,
|
34
|
+
# exit gracefully here
|
35
|
+
l = Houndstooth::Errors.errors.length
|
36
|
+
if $cli_options[:instrument]
|
37
|
+
puts "Exiting with #{l} error#{l == 1 ? '' : 's'}."
|
38
|
+
puts "Running with instrumentation, so exit is just a jump."
|
39
|
+
puts "THIS WILL NOT RESULT IN AN ERROR EXIT CODE."
|
40
|
+
throw :afl_exit
|
41
|
+
else
|
42
|
+
abort "Exiting with #{l} error#{l == 1 ? '' : 's'}."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create an environment with stdlib types
|
48
|
+
env = Houndstooth::Environment.new
|
49
|
+
|
50
|
+
# Load and parse code from file
|
51
|
+
if options[:file]
|
52
|
+
unless File.exist?(options[:file])
|
53
|
+
Houndstooth::Errors::Error.new("File '#{options[:file]}' does not exist", []).push
|
54
|
+
abort_on_error!
|
55
|
+
end
|
56
|
+
|
57
|
+
begin
|
58
|
+
code = File.read(options[:file])
|
59
|
+
rescue => e
|
60
|
+
Houndstooth::Errors::Error.new("Error reading file: #{e}", []).push
|
61
|
+
abort_on_error!
|
62
|
+
end
|
63
|
+
elsif options[:code]
|
64
|
+
code = options[:code]
|
65
|
+
else
|
66
|
+
puts "
|
67
|
+
███████▖ ▀██
|
68
|
+
████████▙▖ ▜
|
69
|
+
██████▌▜██▄ HOUNDSTOOTH
|
70
|
+
██████▌ ▝▜██▖ A Ruby type checker
|
71
|
+
▝██▙▖
|
72
|
+
▀██▙ -f/--file: Check file
|
73
|
+
▙ ▝▜█▌ -e/--code: Check string
|
74
|
+
██▄ ▝▌
|
75
|
+
"
|
76
|
+
exit 1
|
77
|
+
end
|
78
|
+
|
79
|
+
if options[:no_stdlib]
|
80
|
+
htt_files = []
|
81
|
+
else
|
82
|
+
htt_files = [["stdlib.htt", File.read(File.join(__dir__, '..', 'types', 'stdlib.htt'))]]
|
83
|
+
end
|
84
|
+
|
85
|
+
# Parse and run builder over all files
|
86
|
+
all_nodes = [[options[:file] || 'inline code', code], *htt_files].map do |name, contents|
|
87
|
+
Houndstooth.process_file(name, contents, env)
|
88
|
+
end
|
89
|
+
node = all_nodes[0]
|
90
|
+
abort_on_error!
|
91
|
+
|
92
|
+
if options[:debug_nodes]
|
93
|
+
puts "------ Nodes ------"
|
94
|
+
pp node
|
95
|
+
puts "-------------------"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Resolve environment
|
99
|
+
env.resolve_all_pending_types
|
100
|
+
abort_on_error!
|
101
|
+
|
102
|
+
if options[:debug_environment]
|
103
|
+
puts "--- Environment ---"
|
104
|
+
env.types.each do |_, t|
|
105
|
+
puts t.path
|
106
|
+
if t.type_instance_variables.any?
|
107
|
+
puts " Vars:"
|
108
|
+
t.type_instance_variables.each do |k, v|
|
109
|
+
puts " #{k}: #{v.rbs}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
t.instance_methods.each do |m|
|
113
|
+
puts " #{m.name}"
|
114
|
+
# Don't try to print special `new`
|
115
|
+
if m.is_a?(Houndstooth::Environment::SpecialConstructorMethod)
|
116
|
+
puts " <special constructor>"
|
117
|
+
else
|
118
|
+
m.signatures.each do |s|
|
119
|
+
puts " #{s.rbs}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
puts
|
124
|
+
end
|
125
|
+
puts "-------------------"
|
126
|
+
end
|
127
|
+
|
128
|
+
# Create a new instruction block and populate it
|
129
|
+
# TODO: this is probably not how it'll be done in the final thing - we need to do this to individual
|
130
|
+
# methods, probably, or just ignore definitions? Don't know!
|
131
|
+
block = Houndstooth::Instructions::InstructionBlock.new(has_scope: true, parent: nil)
|
132
|
+
node.to_instructions(block)
|
133
|
+
env.types["__HoundstoothMain"] = Houndstooth::Environment::DefinedType.new(path: "__HoundstoothMain")
|
134
|
+
abort_on_error!
|
135
|
+
|
136
|
+
if options[:debug_instructions]
|
137
|
+
puts "-- Instructions ---"
|
138
|
+
puts block.to_assembly
|
139
|
+
puts "-------------------"
|
140
|
+
end
|
141
|
+
|
142
|
+
# Run the interpreter
|
143
|
+
runtime = Houndstooth::Interpreter::Runtime.new(env: env)
|
144
|
+
runtime.execute_from_top_level(block)
|
145
|
+
abort_on_error!
|
146
|
+
|
147
|
+
# That could have printed due to const-internal `puts` etc - if it did, print a divider
|
148
|
+
puts "--- end of const stdout ---" if $const_printed
|
149
|
+
|
150
|
+
# Type check the instruction block
|
151
|
+
checker = Houndstooth::TypeChecker.new(env)
|
152
|
+
checker.process_block(
|
153
|
+
block,
|
154
|
+
lexical_context: Houndstooth::Environment::BaseDefinedType.new,
|
155
|
+
self_type: env.types["__HoundstoothMain"],
|
156
|
+
const_context: false,
|
157
|
+
type_parameters: [],
|
158
|
+
)
|
159
|
+
|
160
|
+
if options[:debug_type_changes]
|
161
|
+
puts "--- Inst. Types ---"
|
162
|
+
puts block.to_assembly
|
163
|
+
puts "-------------------"
|
164
|
+
end
|
165
|
+
abort_on_error!
|
166
|
+
|
167
|
+
# Yay!
|
168
|
+
puts "All good!"
|
169
|
+
end
|
170
|
+
|
171
|
+
if options[:instrument]
|
172
|
+
puts "== Instrumentation enabled =="
|
173
|
+
AFL.init
|
174
|
+
AFL.with_logging_to_file("/tmp/houndstooth-afl") do
|
175
|
+
catch :afl_exit do
|
176
|
+
AFL.with_exceptions_as_crashes do
|
177
|
+
main(options)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
else
|
182
|
+
main(options)
|
183
|
+
end
|
data/fuzz/cases/x.rb
ADDED
data/fuzz/cases/y.rb
ADDED
data/fuzz/cases/z.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
class Translator
|
3
|
+
#: (String, String) -> String
|
4
|
+
def translate(text, language)
|
5
|
+
"#{text} in #{language} is..."
|
6
|
+
end
|
7
|
+
|
8
|
+
#!arg String
|
9
|
+
[
|
10
|
+
"english", "french", "german", "japanese",
|
11
|
+
"spanish", "urdu", "korean", "hungarian",
|
12
|
+
].each do |lang,|
|
13
|
+
#!arg String
|
14
|
+
#!arg String
|
15
|
+
define_method(:"to_#{lang}") do |s,|
|
16
|
+
translate(s, lang)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
t = Translator.new
|
22
|
+
x = t.to_german("Hello")
|
data/fuzz/ruby.dict
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# Derived from the Sorbet fuzzer dictionary, with modifications to use Houndstooth type syntax
|
2
|
+
|
3
|
+
# keywords
|
4
|
+
"begin"
|
5
|
+
"end"
|
6
|
+
"def"
|
7
|
+
"class"
|
8
|
+
"module"
|
9
|
+
"do"
|
10
|
+
"if"
|
11
|
+
"else"
|
12
|
+
"elsif"
|
13
|
+
"until"
|
14
|
+
"unless"
|
15
|
+
"for"
|
16
|
+
"while"
|
17
|
+
"rescue"
|
18
|
+
"case"
|
19
|
+
"when"
|
20
|
+
"yield"
|
21
|
+
"next"
|
22
|
+
"break"
|
23
|
+
"return"
|
24
|
+
"super"
|
25
|
+
"ensure"
|
26
|
+
"in"
|
27
|
+
"redo"
|
28
|
+
"retry"
|
29
|
+
|
30
|
+
|
31
|
+
# common values
|
32
|
+
"true"
|
33
|
+
"false"
|
34
|
+
"Integer"
|
35
|
+
"String"
|
36
|
+
"Array"
|
37
|
+
"Numeric"
|
38
|
+
"Object"
|
39
|
+
"BasicObject"
|
40
|
+
"nil"
|
41
|
+
|
42
|
+
|
43
|
+
# types
|
44
|
+
"self"
|
45
|
+
"instance"
|
46
|
+
"void"
|
47
|
+
"untyped"
|
48
|
+
|
49
|
+
|
50
|
+
# interesting stuff
|
51
|
+
"new"
|
52
|
+
"extend"
|
53
|
+
"include"
|
54
|
+
"is_a?"
|
55
|
+
"self"
|
56
|
+
"raise"
|
57
|
+
"initialize"
|
58
|
+
"&&"
|
59
|
+
"||"
|
60
|
+
|
61
|
+
|
62
|
+
# Metaprogramming
|
63
|
+
"define_method"
|
64
|
+
"attr_accessor"
|
data/fuzz/run
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
set -e
|
3
|
+
|
4
|
+
SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
|
5
|
+
if [ -d "$SCRIPTPATH/out/latest" ]
|
6
|
+
then
|
7
|
+
echo "out/latest directory already exists!"
|
8
|
+
echo "Delete it to perform another fuzzer run."
|
9
|
+
exit 1
|
10
|
+
fi
|
11
|
+
|
12
|
+
# gitignored, so a fresh clone won't have this
|
13
|
+
mkdir -p "$SCRIPTPATH/out"
|
14
|
+
|
15
|
+
AFL_NO_FORKSRV=1 AFL_SKIP_BIN_CHECK=1 \
|
16
|
+
bundle exec afl-fuzz \
|
17
|
+
-i "$SCRIPTPATH/cases" \
|
18
|
+
-o "$SCRIPTPATH/out/latest" \
|
19
|
+
-m 5000 \
|
20
|
+
-x "$SCRIPTPATH/ruby.dict" \
|
21
|
+
-- ruby bin/houndstooth.rb --instrument -f @@
|