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