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
@@ -0,0 +1,137 @@
1
+ class Houndstooth::Environment
2
+ class DefinedType < Type
3
+ def initialize(path: nil, node: nil, superclass: nil, instance_methods: nil, eigen: :generate, type_parameters: nil)
4
+ @path = path.to_s
5
+ @node = node
6
+ @superclass = superclass
7
+ @instance_methods = instance_methods || []
8
+ @type_parameters = type_parameters || []
9
+ @type_instance_variables = {}
10
+
11
+ if eigen == :generate
12
+ @eigen = DefinedType.new(
13
+ path: "<Eigen:#{path}>",
14
+ superclass:
15
+ if superclass.is_a?(PendingDefinedType)
16
+ PendingDefinedType.new("<Eigen:#{superclass.path}>")
17
+ else
18
+ superclass&.eigen
19
+ end,
20
+ eigen: nil,
21
+ )
22
+ else
23
+ @eigen = eigen
24
+ end
25
+ end
26
+
27
+ # @return [String]
28
+ attr_reader :path
29
+
30
+ # @return [String]
31
+ def name
32
+ path.split("::").last
33
+ end
34
+
35
+ # @return [SemanticNode]
36
+ attr_reader :node
37
+
38
+ # @return [Type, nil]
39
+ attr_accessor :superclass
40
+
41
+ # @return [Type]
42
+ attr_accessor :eigen
43
+
44
+ # @return [<Method>]
45
+ attr_reader :instance_methods
46
+
47
+ # @return [<String>]
48
+ attr_reader :type_parameters
49
+
50
+ # @return [{String => Type}]
51
+ attr_reader :type_instance_variables
52
+
53
+ def resolve_instance_method(method_name, env, instance: nil, top_level: true)
54
+ # Is it available on this type?
55
+ # If not, check the superclass
56
+ # If there's no superclass, then there is no method to be found, so return nil
57
+ instance_method = instance_methods.find { _1.name == method_name }
58
+
59
+ found = if instance_method
60
+ instance_method
61
+ else
62
+ superclass&.resolve_instance_method(method_name, env, instance: instance, top_level: false)
63
+ end
64
+
65
+ # If the upper chain returned a special constructor method, we need to convert this by
66
+ # grabbing our instance's `initialize` type
67
+ if top_level && found && found.is_a?(SpecialConstructorMethod)
68
+ initialize_sig = env.resolve_type(uneigen).resolve_instance_method(:initialize, env, instance: instance)
69
+ @new_sig ||= Method.new(
70
+ :new,
71
+ initialize_sig.signatures.map do |sig|
72
+ # Same parameters, but returns `instance`
73
+ new_sig = sig.clone
74
+ new_sig.return_type = InstanceType.new
75
+ new_sig
76
+ end,
77
+ const: initialize_sig.const,
78
+ )
79
+ @new_sig
80
+ else
81
+ found
82
+ end
83
+ end
84
+
85
+ def resolve_instance_variable(name)
86
+ var_here = type_instance_variables[name]
87
+ return var_here if var_here
88
+
89
+ superclass&.resolve_instance_variable(name)
90
+ end
91
+
92
+ # A path to this type, but with one layer of "eigen-ness" removed from the final element.
93
+ # A bit cursed, but used for constant resolution.
94
+ # @return [String]
95
+ def uneigen
96
+ path_parts = path.split("::")
97
+ *rest, name = path_parts
98
+
99
+ raise "internal error: can't uneigen a non-eigen type" unless /^<Eigen:(.+)>$/ === name
100
+ uneigened_name = $1
101
+
102
+ [*rest, uneigened_name].join("::")
103
+ end
104
+
105
+ def resolve_all_pending_types(environment, context: nil)
106
+ @superclass = resolve_type_if_pending(superclass, self, environment)
107
+ @eigen = resolve_type_if_pending(eigen, self, environment)
108
+
109
+ instance_methods.map do |method|
110
+ method.resolve_all_pending_types(environment, context: self)
111
+ end
112
+
113
+ type_instance_variables.keys.each do |k|
114
+ type_instance_variables[k] = resolve_type_if_pending(type_instance_variables[k], self, environment)
115
+ end
116
+ end
117
+
118
+ def accepts?(other)
119
+ return false unless other.is_a?(DefinedType)
120
+
121
+ distance = 0
122
+ current = other
123
+ until current.nil?
124
+ return distance if current == self
125
+
126
+ current = current&.superclass
127
+ distance += 1
128
+ end
129
+
130
+ false
131
+ end
132
+
133
+ def rbs
134
+ path
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,14 @@
1
+ class Houndstooth::Environment
2
+ class PendingDefinedType < Type
3
+ def initialize(path)
4
+ @path = path
5
+ end
6
+
7
+ # @return [String]
8
+ attr_reader :path
9
+
10
+ def rbs
11
+ "#{path} (unresolved)"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,79 @@
1
+ class Houndstooth::Environment
2
+ class Method
3
+ # @return [String]
4
+ attr_reader :name
5
+
6
+ # @return [<MethodType>]
7
+ attr_reader :signatures
8
+
9
+ # :public, :protected or :private
10
+ # @return [Symbol]
11
+ attr_reader :visibility
12
+
13
+ # If a symbol, the kind of constness this method has:
14
+ # - :normal, defined as user-specified source, can be used anywhere
15
+ # - :internal, defined in Houndstooth, can be used anywhere
16
+ # - :required, defined as user-specified source, can only be used from a const context
17
+ # - :required_internal, defined in Houndstooth, can only be used from a const context
18
+ # If nil, this method is not const.
19
+ # @return [Symbol, nil]
20
+ attr_reader :const
21
+
22
+ def const?; !const.nil?; end
23
+ def const_internal?; const == :internal || const == :required_internal; end
24
+ def const_required?; const == :required || const == :required_internal; end
25
+
26
+ # The instruction block which implements this method.
27
+ # @return [InstructionBlock]
28
+ attr_accessor :instruction_block
29
+
30
+ def initialize(name, signatures = nil, visibility: :public, const: false)
31
+ @name = name
32
+ @signatures = signatures || []
33
+ @visibility = visibility
34
+ @const = const
35
+ end
36
+
37
+ def resolve_all_pending_types(environment, context:)
38
+ signatures.map do |sig|
39
+ sig.resolve_all_pending_types(environment, context: context)
40
+ end
41
+ end
42
+
43
+ def substitute_type_parameters(instance, call_type_args)
44
+ raise 'internal error: tried to substitute parameters on a Method; too high in the hierarchy for this to be sensible'
45
+ end
46
+
47
+ # Given a set of arguments and their types, resolves and returns the best matching signature
48
+ # of this method.
49
+ #
50
+ # If multiple signatures match, the "best" is chosen according to the distance rules used
51
+ # by `Type#accepts?` - the type with the lowest distance over all arguments is returned.
52
+ # If no signatures match, returns nil.
53
+ #
54
+ # @param [TypeInstance] instance
55
+ # @param [<(Instructions::Argument, Type)>] arguments
56
+ # @param [<Type>] type_arguments
57
+ # @return [MethodType, nil]
58
+ def resolve_matching_signature(instance, arguments, type_arguments = nil)
59
+ type_arguments ||= []
60
+
61
+ sigs_with_scores = signatures
62
+ .map do |sig|
63
+ # Create {name => type} type argument mapping, or if the numbers mismatch
64
+ # return false, as this signature cannot match
65
+ next [nil, false] if type_arguments.length != sig.type_parameters.length
66
+
67
+ call_type_args = sig.type_parameters.zip(type_arguments).to_h
68
+ [sig, sig.substitute_type_parameters(instance, call_type_args).accepts_arguments?(arguments)]
69
+ end
70
+ .reject { |_, r| r == false }
71
+
72
+ if sigs_with_scores.any?
73
+ sigs_with_scores.min_by { |sig, score| score }[0]
74
+ else
75
+ nil
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,144 @@
1
+ class Houndstooth::Environment
2
+ class MethodType < Type
3
+ # @return [<PositionalParameter>]
4
+ attr_accessor :positional_parameters
5
+
6
+ # @return [<KeywordParameter>]
7
+ attr_accessor :keyword_parameters
8
+
9
+ # @return [PositionalParameter, nil]
10
+ attr_accessor :rest_positional_parameter
11
+
12
+ # @return [KeywordParameter, nil]
13
+ attr_accessor :rest_keyword_parameter
14
+
15
+ # @return [BlockParameter, nil]
16
+ attr_accessor :block_parameter
17
+
18
+ # @return [Type]
19
+ attr_accessor :return_type
20
+
21
+ # @return [<String>]
22
+ attr_accessor :type_parameters
23
+
24
+ def initialize(positional: [], keyword: [], rest_positional: nil, rest_keyword: nil, block: nil, return_type: nil, type_parameters: nil)
25
+ super()
26
+
27
+ @positional_parameters = positional
28
+ @keyword_parameters = keyword
29
+ @rest_positional_parameter = rest_positional
30
+ @rest_keyword_parameter = rest_keyword
31
+ @block_parameter = block
32
+ @return_type = return_type || VoidType.new
33
+ @type_parameters = type_parameters || []
34
+ end
35
+
36
+ def resolve_all_pending_types(environment, context:)
37
+ @return_type = resolve_type_if_pending(return_type, context, environment)
38
+
39
+ positional_parameters.map do |param|
40
+ param.resolve_all_pending_types(environment, context: context)
41
+ end
42
+
43
+ keyword_parameters.map do |param|
44
+ param.resolve_all_pending_types(environment, context: context)
45
+ end
46
+
47
+ rest_positional_parameter&.resolve_all_pending_types(environment, context: context)
48
+ rest_keyword_parameter&.resolve_all_pending_types(environment, context: context)
49
+ block_parameter&.resolve_all_pending_types(environment, context: context)
50
+ end
51
+
52
+ # Determines whether this method can be called with the given arguments and their types.
53
+ # Follows the same return-value rules as `accepts?`.
54
+ #
55
+ # @param [<(Instructions::Argument, Type)>] arguments
56
+ # @return [Integer, Boolean]
57
+ def accepts_arguments?(arguments)
58
+ distance_total = 0
59
+ args_index = 0
60
+
61
+ # Check the positional parameters first
62
+ positional_parameters.each do |param|
63
+ # Is there also a positional argument in this index slot?
64
+ this_arg, this_type = arguments[args_index]
65
+ if this_arg.is_a?(Houndstooth::Instructions::PositionalArgument)
66
+ # Yes, so this argument was definitely passed to this parameter
67
+ # Are the types compatible?
68
+ dist = param.type.accepts?(this_type)
69
+ if dist
70
+ # Yep! All is well. Add to total distance
71
+ distance_total += dist
72
+ args_index += 1
73
+ else
74
+ # Nope, this isn't valid. Bail
75
+ return false
76
+ end
77
+ else
78
+ # No positional argument - but that's OK if this parameter is optional
79
+ if !param.optional?
80
+ # Missing argument not allowed
81
+ return false
82
+ end
83
+ end
84
+ end
85
+
86
+ # Are there any positional arguments left over?
87
+ while arguments[args_index] && arguments[args_index][0].is_a?(Houndstooth::Instructions::PositionalArgument)
88
+ this_arg, this_type = arguments[args_index]
89
+
90
+ # Is there a rest-parameter to take these?
91
+ if !rest_positional_parameter.nil?
92
+ # Yep, but does this argument match the type of the rest positional?
93
+ dist = param.type.accepts?(this_type)
94
+ if dist
95
+ # Correct - this is passed into the splat!
96
+ distance_total += dist
97
+ args_index += 1
98
+ else
99
+ # Not the right type for the splat, invalid
100
+ return false
101
+ end
102
+ else
103
+ # No, error - too many arguments
104
+ return false
105
+ end
106
+ end
107
+
108
+ # TODO: keyword arguments
109
+ raise "keyword argument checking not implemeneted" \
110
+ if arguments.find { |x, _| x.is_a?(Houndstooth::Instructions::KeywordArgument) }
111
+
112
+ distance_total
113
+ end
114
+
115
+ def substitute_type_parameters(instance, call_type_args)
116
+ clone.tap do |t|
117
+ t.positional_parameters = t.positional_parameters.map { |p| p.substitute_type_parameters(instance, call_type_args) }
118
+ t.keyword_parameters = t.keyword_parameters.map { |p| p.substitute_type_parameters(instance, call_type_args) }
119
+
120
+ t.rest_positional_parameter = t.rest_positional_parameter&.substitute_type_parameters(instance, call_type_args)
121
+ t.rest_keyword_parameter = t.rest_keyword_parameter&.substitute_type_parameters(instance, call_type_args)
122
+ t.block_parameter = t.block_parameter&.substitute_type_parameters(instance, call_type_args)
123
+
124
+ t.return_type = t.return_type.substitute_type_parameters(instance, call_type_args)
125
+ end
126
+ end
127
+
128
+ # TODO: implement accepts?
129
+
130
+ def rbs
131
+ params =
132
+ [positional_parameters.map(&:rbs), keyword_parameters.map(&:rbs)].flatten.join(", ")
133
+
134
+ if type_parameters.any?
135
+ type_params = "[#{type_parameters.join(', ')}] "
136
+ else
137
+ type_params = ''
138
+ end
139
+
140
+ "#{type_params}(#{params}) #{block_parameter ? "#{block_parameter.rbs} " : ''}-> #{return_type.rbs}"
141
+ end
142
+ end
143
+ end
144
+
@@ -0,0 +1,53 @@
1
+ class Houndstooth::Environment
2
+ class Parameter < Type
3
+ # Note: Parameters aren't *really* a type, but we need `resolve_type_if_pending`
4
+
5
+ # @return [Name]
6
+ attr_reader :name
7
+
8
+ # @return [Type]
9
+ attr_accessor :type
10
+
11
+ # @return [Boolean]
12
+ attr_reader :optional
13
+ alias optional? optional
14
+
15
+ def initialize(name, type, optional: false)
16
+ @name = name
17
+ @type = type
18
+ @optional = optional
19
+ end
20
+
21
+ def resolve_all_pending_types(environment, context:)
22
+ @type = resolve_type_if_pending(type, context, environment)
23
+ end
24
+
25
+ def substitute_type_parameters(instance, call_type_args)
26
+ clone.tap do |t|
27
+ t.type = t.type.substitute_type_parameters(instance, call_type_args)
28
+ end
29
+ end
30
+ end
31
+
32
+ class PositionalParameter < Parameter
33
+ def rbs
34
+ if name
35
+ "#{optional? ? '?' : ''}#{type.rbs} #{name}"
36
+ else
37
+ "#{optional? ? '?' : ''}#{type.rbs}"
38
+ end
39
+ end
40
+ end
41
+
42
+ class KeywordParameter < Parameter
43
+ def rbs
44
+ "#{optional? ? '?' : ''}#{name}: #{type.rbs}"
45
+ end
46
+ end
47
+
48
+ class BlockParameter < Parameter
49
+ def rbs
50
+ "#{optional? ? '?' : ''}{ #{type.rbs} }"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,15 @@
1
+ class Houndstooth::Environment
2
+ # A special type which can be used in place of a method, typically only `new`. Specifies
3
+ # that the resolved instance method should actually be taken from an uneigened `initialize`.
4
+ class SpecialConstructorMethod < Type
5
+ def initialize(name)
6
+ @name = name
7
+ end
8
+
9
+ attr_accessor :name
10
+
11
+ def rbs
12
+ "<special constructor '#{name}'>"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ class Houndstooth::Environment
2
+ class InstanceType < Type
3
+ # TODO: implement accepts?
4
+
5
+ def rbs
6
+ "instance"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class Houndstooth::Environment
2
+ class SelfType < Type
3
+ # TODO: implement accepts?
4
+
5
+ def rbs
6
+ "self"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,38 @@
1
+ class Houndstooth::Environment
2
+ # A type which will be replaced by a type argument later.
3
+ class TypeParameterPlaceholder < Type
4
+ def initialize(name)
5
+ @name = name
6
+ end
7
+
8
+ attr_accessor :name
9
+
10
+ def accepts?(other)
11
+ if other.is_a?(TypeParameterPlaceholder) && name == other.name
12
+ 1
13
+ else
14
+ false
15
+ end
16
+ end
17
+
18
+ def rbs
19
+ name
20
+ end
21
+
22
+ def substitute_type_parameters(instance, call_type_args)
23
+ # Call type arguments take priority, check those first
24
+ return call_type_args[name] if call_type_args[name]
25
+
26
+ # Get index of type parameter
27
+ index = instance.type.type_parameters.index { |tp| tp == name } or return self
28
+
29
+ # Replace with type argument, which should be an instance
30
+ instance.type_arguments[index] or self
31
+ end
32
+
33
+ # Yikes!
34
+ # It doesn't ever make sense to instantiate a type parameter, and trying to do so was
35
+ # causing problems when passing type arguments around functions, so just don't allow it
36
+ def instantiate(...) = self
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ class Houndstooth::Environment
2
+ class UntypedType < Type
3
+ def accepts?(other)
4
+ 1
5
+ end
6
+
7
+ def rbs
8
+ "untyped"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ class Houndstooth::Environment
2
+ class VoidType < Type
3
+ def accepts?(other)
4
+ # Only valid as a return type, and you can return anything in a void method
5
+ 1
6
+ end
7
+
8
+ def rbs
9
+ "void"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ Dir[File.join(__dir__, '**', '*.rb')].each do |f|
2
+ require_relative f
3
+ end
@@ -0,0 +1,74 @@
1
+ class Houndstooth::Environment
2
+ def initialize
3
+ @types = {}
4
+ end
5
+
6
+ def add_type(type)
7
+ # Add the type and its entire eigen chain
8
+ @types[type.path] = type
9
+ add_type(type.eigen) if type.respond_to?(:eigen) && type.eigen
10
+ end
11
+
12
+ # @return [{String, DefinedType}]
13
+ attr_reader :types
14
+
15
+ def resolve_all_pending_types
16
+ types.each do |_, type|
17
+ type.resolve_all_pending_types(self)
18
+ end
19
+ end
20
+
21
+ # Resolve a type by path; either an absolute path from the root namespace, or optionally as a
22
+ # relative path from the context of it being used within the given type.
23
+ # If the type does not exist, returns nil.
24
+ #
25
+ # @param [String] path The path to resolve. If `type_context` is nil, this is interpreted as an
26
+ # absolute path regardless of whether it is prefixed with `::`. If `type_context` is given,
27
+ # this is interpreted as a relative path without a `::` prefix, or an absolute path with one.
28
+ # @param [DefinedType] type_context Optional: The context to search from.
29
+ #
30
+ # @return [DefinedType, nil]
31
+ def resolve_type(path, type_context: nil)
32
+ if path.start_with?('::') || type_context.nil?
33
+ # Our `#types` field is indexed by absolute path, let's just look at that!
34
+ # Prune the :: if present
35
+ path = path[2..] if path.start_with?('::')
36
+ return types[path]
37
+ end
38
+
39
+ # This is a relative path - split into parts
40
+ path_parts = path.split('::')
41
+ return nil if path_parts.empty?
42
+ next_part, *rest_parts = *path_parts
43
+
44
+ # Does the current type context contain the next part of the path?
45
+ maybe_inner_type = types[type_context.path + '::' + next_part]
46
+ if maybe_inner_type
47
+ # Yes - either return if there's no more parts, or advance into that type and continue
48
+ # the search
49
+ if rest_parts.empty?
50
+ maybe_inner_type
51
+ else
52
+ resolve_type(rest_parts.join('::'), type_context: maybe_inner_type)
53
+ end
54
+ else
55
+ # No - check the current type context's parent
56
+ # (Or, if there's no parent, we'll try searching for it as absolute as a last-ditch
57
+ # attempt)
58
+ if type_context.path.include?('::')
59
+ resolve_type(path, type_context: types[type_context.path.split('::')[...-1].join('::')])
60
+ else
61
+ resolve_type(path, type_context: nil)
62
+ end
63
+ end
64
+ end
65
+
66
+ def inspect
67
+ "#<Houndstooth::Environment (#{types.length} types)>"
68
+ end
69
+ alias to_s inspect
70
+ end
71
+
72
+ require_relative 'environment/types'
73
+ require_relative 'environment/type_parser'
74
+ require_relative 'environment/builder'
@@ -0,0 +1,53 @@
1
+ module Houndstooth
2
+ module Errors
3
+ class Error
4
+ def initialize(message, tagged_ranges)
5
+ @message = message
6
+ @tagged_ranges = tagged_ranges
7
+ end
8
+
9
+ # @return [String]
10
+ attr_reader :message
11
+
12
+ # @return [(Parser::Source::Range, String)]
13
+ attr_reader :tagged_ranges
14
+
15
+ def format
16
+ # TODO: merge nearby errors
17
+
18
+ (["Error: #{message}"] \
19
+ + tagged_ranges.flat_map do |range, hint|
20
+ # TODO: won't work if the error spans multiple lines
21
+ line_range = range.source_buffer.line_range(range.line)
22
+ begin_pos_on_line = range.begin_pos - line_range.begin_pos
23
+ length = range.end_pos - range.begin_pos
24
+
25
+ [
26
+ "",
27
+ " #{range.source_buffer.name}",
28
+ " #{range.line} | #{range.source_line}",
29
+ " #{' ' * range.line.to_s.length} #{' ' * begin_pos_on_line}#{'^' * length} #{hint}",
30
+ ]
31
+ end).join("\n")
32
+ end
33
+
34
+ def push
35
+ Errors.push(self)
36
+ end
37
+ end
38
+
39
+ @errors = []
40
+
41
+ def self.reset
42
+ @errors = []
43
+ end
44
+
45
+ def self.push(error)
46
+ @errors << error
47
+ end
48
+
49
+ def self.errors
50
+ @errors
51
+ end
52
+ end
53
+ end