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
@@ -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