houndstooth 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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 @@