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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +11 -0
  6. data/Gemfile.lock +49 -0
  7. data/README.md +99 -0
  8. data/bin/houndstooth.rb +183 -0
  9. data/fuzz/cases/x.rb +8 -0
  10. data/fuzz/cases/y.rb +8 -0
  11. data/fuzz/cases/z.rb +22 -0
  12. data/fuzz/ruby.dict +64 -0
  13. data/fuzz/run +21 -0
  14. data/lib/houndstooth/environment/builder.rb +260 -0
  15. data/lib/houndstooth/environment/type_parser.rb +149 -0
  16. data/lib/houndstooth/environment/types/basic/type.rb +85 -0
  17. data/lib/houndstooth/environment/types/basic/type_instance.rb +54 -0
  18. data/lib/houndstooth/environment/types/compound/union_type.rb +72 -0
  19. data/lib/houndstooth/environment/types/defined/base_defined_type.rb +23 -0
  20. data/lib/houndstooth/environment/types/defined/defined_type.rb +137 -0
  21. data/lib/houndstooth/environment/types/defined/pending_defined_type.rb +14 -0
  22. data/lib/houndstooth/environment/types/method/method.rb +79 -0
  23. data/lib/houndstooth/environment/types/method/method_type.rb +144 -0
  24. data/lib/houndstooth/environment/types/method/parameters.rb +53 -0
  25. data/lib/houndstooth/environment/types/method/special_constructor_method.rb +15 -0
  26. data/lib/houndstooth/environment/types/special/instance_type.rb +9 -0
  27. data/lib/houndstooth/environment/types/special/self_type.rb +9 -0
  28. data/lib/houndstooth/environment/types/special/type_parameter_placeholder.rb +38 -0
  29. data/lib/houndstooth/environment/types/special/untyped_type.rb +11 -0
  30. data/lib/houndstooth/environment/types/special/void_type.rb +12 -0
  31. data/lib/houndstooth/environment/types.rb +3 -0
  32. data/lib/houndstooth/environment.rb +74 -0
  33. data/lib/houndstooth/errors.rb +53 -0
  34. data/lib/houndstooth/instructions.rb +698 -0
  35. data/lib/houndstooth/interpreter/const_internal.rb +148 -0
  36. data/lib/houndstooth/interpreter/objects.rb +142 -0
  37. data/lib/houndstooth/interpreter/runtime.rb +309 -0
  38. data/lib/houndstooth/interpreter.rb +7 -0
  39. data/lib/houndstooth/semantic_node/control_flow.rb +218 -0
  40. data/lib/houndstooth/semantic_node/definitions.rb +253 -0
  41. data/lib/houndstooth/semantic_node/identifiers.rb +308 -0
  42. data/lib/houndstooth/semantic_node/keywords.rb +45 -0
  43. data/lib/houndstooth/semantic_node/literals.rb +226 -0
  44. data/lib/houndstooth/semantic_node/operators.rb +126 -0
  45. data/lib/houndstooth/semantic_node/parameters.rb +108 -0
  46. data/lib/houndstooth/semantic_node/send.rb +349 -0
  47. data/lib/houndstooth/semantic_node/super.rb +12 -0
  48. data/lib/houndstooth/semantic_node.rb +119 -0
  49. data/lib/houndstooth/stdlib.rb +6 -0
  50. data/lib/houndstooth/type_checker.rb +462 -0
  51. data/lib/houndstooth.rb +53 -0
  52. data/spec/ast_to_node_spec.rb +889 -0
  53. data/spec/environment_spec.rb +323 -0
  54. data/spec/instructions_spec.rb +291 -0
  55. data/spec/integration_spec.rb +785 -0
  56. data/spec/interpreter_spec.rb +170 -0
  57. data/spec/self_spec.rb +7 -0
  58. data/spec/spec_helper.rb +50 -0
  59. data/test/ruby_interpreter_test.rb +162 -0
  60. data/types/stdlib.htt +170 -0
  61. 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
@@ -0,0 +1,5 @@
1
+ playground
2
+ fuzz/out
3
+ coverage
4
+ .DS_Store
5
+ fuzz-out
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.0.3
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'parser'
4
+ gem 'rspec'
5
+ gem 'simplecov'
6
+ gem 'rbs'
7
+ gem 'optimist'
8
+
9
+ group :development do
10
+ gem 'afl', git: 'https://github.com/richo/afl-ruby.git'
11
+ end
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
+ ```
@@ -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
@@ -0,0 +1,8 @@
1
+ class X
2
+ #: () -> String
3
+ def name
4
+ "Aaron"
5
+ end
6
+ end
7
+
8
+ X.new.name
data/fuzz/cases/y.rb ADDED
@@ -0,0 +1,8 @@
1
+ class Y
2
+ #: () -> Integer
3
+ def y
4
+ 3
5
+ end
6
+ end
7
+
8
+ Y.new.y
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 @@