t-ruby 0.0.1
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +221 -0
- data/bin/trc +6 -0
- data/lib/t_ruby/benchmark.rb +592 -0
- data/lib/t_ruby/bundler_integration.rb +569 -0
- data/lib/t_ruby/cache.rb +774 -0
- data/lib/t_ruby/cli.rb +106 -0
- data/lib/t_ruby/compiler.rb +299 -0
- data/lib/t_ruby/config.rb +53 -0
- data/lib/t_ruby/constraint_checker.rb +441 -0
- data/lib/t_ruby/declaration_generator.rb +298 -0
- data/lib/t_ruby/doc_generator.rb +474 -0
- data/lib/t_ruby/error_handler.rb +132 -0
- data/lib/t_ruby/generic_type_parser.rb +68 -0
- data/lib/t_ruby/intersection_type_parser.rb +30 -0
- data/lib/t_ruby/ir.rb +1301 -0
- data/lib/t_ruby/lsp_server.rb +994 -0
- data/lib/t_ruby/package_manager.rb +735 -0
- data/lib/t_ruby/parser.rb +245 -0
- data/lib/t_ruby/parser_combinator.rb +942 -0
- data/lib/t_ruby/rbs_generator.rb +71 -0
- data/lib/t_ruby/runtime_validator.rb +367 -0
- data/lib/t_ruby/smt_solver.rb +1076 -0
- data/lib/t_ruby/type_alias_registry.rb +102 -0
- data/lib/t_ruby/type_checker.rb +770 -0
- data/lib/t_ruby/type_erasure.rb +26 -0
- data/lib/t_ruby/type_inferencer.rb +580 -0
- data/lib/t_ruby/union_type_parser.rb +38 -0
- data/lib/t_ruby/version.rb +5 -0
- data/lib/t_ruby/watcher.rb +320 -0
- data/lib/t_ruby.rb +42 -0
- metadata +87 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
# Represents a type checking error
|
|
5
|
+
class TypeCheckError
|
|
6
|
+
attr_reader :message, :location, :expected, :actual, :suggestion, :severity
|
|
7
|
+
|
|
8
|
+
def initialize(message:, location: nil, expected: nil, actual: nil, suggestion: nil, severity: :error)
|
|
9
|
+
@message = message
|
|
10
|
+
@location = location
|
|
11
|
+
@expected = expected
|
|
12
|
+
@actual = actual
|
|
13
|
+
@suggestion = suggestion
|
|
14
|
+
@severity = severity
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_s
|
|
18
|
+
parts = [@message]
|
|
19
|
+
parts << " Expected: #{@expected}" if @expected
|
|
20
|
+
parts << " Actual: #{@actual}" if @actual
|
|
21
|
+
parts << " Suggestion: #{@suggestion}" if @suggestion
|
|
22
|
+
parts << " at #{@location}" if @location
|
|
23
|
+
parts.join("\n")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_diagnostic
|
|
27
|
+
{
|
|
28
|
+
severity: @severity,
|
|
29
|
+
message: @message,
|
|
30
|
+
location: @location,
|
|
31
|
+
expected: @expected,
|
|
32
|
+
actual: @actual,
|
|
33
|
+
suggestion: @suggestion
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Type hierarchy for subtype checking
|
|
39
|
+
class TypeHierarchy
|
|
40
|
+
NUMERIC_TYPES = %w[Integer Float Numeric].freeze
|
|
41
|
+
COLLECTION_TYPES = %w[Array Hash Set].freeze
|
|
42
|
+
|
|
43
|
+
def initialize
|
|
44
|
+
@subtypes = {
|
|
45
|
+
"Integer" => ["Numeric", "Object"],
|
|
46
|
+
"Float" => ["Numeric", "Object"],
|
|
47
|
+
"Numeric" => ["Object"],
|
|
48
|
+
"String" => ["Object"],
|
|
49
|
+
"Symbol" => ["Object"],
|
|
50
|
+
"Array" => ["Enumerable", "Object"],
|
|
51
|
+
"Hash" => ["Enumerable", "Object"],
|
|
52
|
+
"Set" => ["Enumerable", "Object"],
|
|
53
|
+
"Boolean" => ["Object"],
|
|
54
|
+
"nil" => ["Object"],
|
|
55
|
+
"Object" => []
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def subtype_of?(subtype, supertype)
|
|
60
|
+
return true if subtype == supertype
|
|
61
|
+
return true if supertype == "Object"
|
|
62
|
+
|
|
63
|
+
chain = @subtypes[subtype] || []
|
|
64
|
+
return true if chain.include?(supertype)
|
|
65
|
+
|
|
66
|
+
# Check transitive relationship
|
|
67
|
+
chain.any? { |t| subtype_of?(t, supertype) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def compatible?(type_a, type_b)
|
|
71
|
+
subtype_of?(type_a, type_b) || subtype_of?(type_b, type_a)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def register_subtype(subtype, supertype)
|
|
75
|
+
@subtypes[subtype] ||= []
|
|
76
|
+
@subtypes[subtype] << supertype unless @subtypes[subtype].include?(supertype)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def common_supertype(type_a, type_b)
|
|
80
|
+
return type_a if type_a == type_b
|
|
81
|
+
return type_a if subtype_of?(type_b, type_a)
|
|
82
|
+
return type_b if subtype_of?(type_a, type_b)
|
|
83
|
+
|
|
84
|
+
# Find common ancestor
|
|
85
|
+
chain_a = [type_a] + (@subtypes[type_a] || [])
|
|
86
|
+
chain_b = [type_b] + (@subtypes[type_b] || [])
|
|
87
|
+
|
|
88
|
+
common = chain_a & chain_b
|
|
89
|
+
common.first || "Object"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Scope for tracking variable types in a block
|
|
94
|
+
class TypeScope
|
|
95
|
+
attr_reader :parent, :variables
|
|
96
|
+
|
|
97
|
+
def initialize(parent = nil)
|
|
98
|
+
@parent = parent
|
|
99
|
+
@variables = {}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def define(name, type)
|
|
103
|
+
@variables[name] = type
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def lookup(name)
|
|
107
|
+
@variables[name] || @parent&.lookup(name)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def child_scope
|
|
111
|
+
TypeScope.new(self)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Flow-sensitive type tracking
|
|
116
|
+
class FlowContext
|
|
117
|
+
attr_reader :narrowed_types, :guard_conditions
|
|
118
|
+
|
|
119
|
+
def initialize
|
|
120
|
+
@narrowed_types = {}
|
|
121
|
+
@guard_conditions = []
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def narrow(variable, new_type)
|
|
125
|
+
@narrowed_types[variable] = new_type
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def get_narrowed_type(variable)
|
|
129
|
+
@narrowed_types[variable]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def push_guard(condition)
|
|
133
|
+
@guard_conditions.push(condition)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def pop_guard
|
|
137
|
+
@guard_conditions.pop
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def branch
|
|
141
|
+
new_context = FlowContext.new
|
|
142
|
+
@narrowed_types.each { |k, v| new_context.narrow(k, v) }
|
|
143
|
+
new_context
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def merge(other)
|
|
147
|
+
# Merge two branches - types that are narrowed in both become union
|
|
148
|
+
merged = FlowContext.new
|
|
149
|
+
all_vars = @narrowed_types.keys | other.narrowed_types.keys
|
|
150
|
+
|
|
151
|
+
all_vars.each do |var|
|
|
152
|
+
type_a = @narrowed_types[var]
|
|
153
|
+
type_b = other.narrowed_types[var]
|
|
154
|
+
|
|
155
|
+
if type_a && type_b
|
|
156
|
+
if type_a == type_b
|
|
157
|
+
merged.narrow(var, type_a)
|
|
158
|
+
else
|
|
159
|
+
merged.narrow(var, "#{type_a} | #{type_b}")
|
|
160
|
+
end
|
|
161
|
+
elsif type_a || type_b
|
|
162
|
+
merged.narrow(var, type_a || type_b)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
merged
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Main type checker with SMT solver integration
|
|
171
|
+
class TypeChecker
|
|
172
|
+
attr_reader :errors, :warnings, :hierarchy, :inferencer, :smt_solver, :use_smt
|
|
173
|
+
|
|
174
|
+
def initialize(use_smt: true)
|
|
175
|
+
@errors = []
|
|
176
|
+
@warnings = []
|
|
177
|
+
@hierarchy = TypeHierarchy.new
|
|
178
|
+
@inferencer = TypeInferencer.new
|
|
179
|
+
@function_signatures = {}
|
|
180
|
+
@type_aliases = {}
|
|
181
|
+
@current_scope = TypeScope.new
|
|
182
|
+
@flow_context = FlowContext.new
|
|
183
|
+
@use_smt = use_smt
|
|
184
|
+
@smt_solver = SMT::ConstraintSolver.new if use_smt
|
|
185
|
+
@smt_inference_engine = SMT::TypeInferenceEngine.new if use_smt
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Check an IR program using SMT-based type checking
|
|
189
|
+
def check_program(ir_program)
|
|
190
|
+
return check_program_legacy(ir_program) unless @use_smt
|
|
191
|
+
|
|
192
|
+
@errors = []
|
|
193
|
+
@warnings = []
|
|
194
|
+
|
|
195
|
+
ir_program.declarations.each do |decl|
|
|
196
|
+
case decl
|
|
197
|
+
when IR::TypeAlias
|
|
198
|
+
register_alias(decl.name, decl.definition)
|
|
199
|
+
when IR::Interface
|
|
200
|
+
check_interface(decl)
|
|
201
|
+
when IR::MethodDef
|
|
202
|
+
check_method_with_smt(decl)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
{
|
|
207
|
+
success: @errors.empty?,
|
|
208
|
+
errors: @errors,
|
|
209
|
+
warnings: @warnings
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Check a method definition using SMT solver
|
|
214
|
+
def check_method_with_smt(method_ir)
|
|
215
|
+
result = @smt_inference_engine.infer_method(method_ir)
|
|
216
|
+
|
|
217
|
+
if result[:success]
|
|
218
|
+
# Store inferred types
|
|
219
|
+
@function_signatures[method_ir.name] = {
|
|
220
|
+
params: method_ir.params.map do |p|
|
|
221
|
+
{
|
|
222
|
+
name: p.name,
|
|
223
|
+
type: result[:params][p.name] || infer_param_type(p)
|
|
224
|
+
}
|
|
225
|
+
end,
|
|
226
|
+
return_type: result[:return_type]
|
|
227
|
+
}
|
|
228
|
+
else
|
|
229
|
+
# Add errors from SMT solver
|
|
230
|
+
result[:errors]&.each do |error|
|
|
231
|
+
add_error(
|
|
232
|
+
message: "Type inference error in #{method_ir.name}: #{error}",
|
|
233
|
+
location: method_ir.location
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
result
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Check interface implementation
|
|
242
|
+
def check_interface(interface_ir)
|
|
243
|
+
interface_ir.members.each do |member|
|
|
244
|
+
# Validate member type signature
|
|
245
|
+
if member.type_signature
|
|
246
|
+
validate_type(member.type_signature)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Validate a type node is well-formed
|
|
252
|
+
def validate_type(type_node)
|
|
253
|
+
case type_node
|
|
254
|
+
when IR::SimpleType
|
|
255
|
+
# Check if type exists
|
|
256
|
+
unless known_type?(type_node.name)
|
|
257
|
+
add_warning("Unknown type: #{type_node.name}")
|
|
258
|
+
end
|
|
259
|
+
when IR::GenericType
|
|
260
|
+
# Check base type and type args
|
|
261
|
+
unless known_type?(type_node.base)
|
|
262
|
+
add_warning("Unknown generic type: #{type_node.base}")
|
|
263
|
+
end
|
|
264
|
+
type_node.type_args.each { |t| validate_type(t) }
|
|
265
|
+
when IR::UnionType
|
|
266
|
+
type_node.types.each { |t| validate_type(t) }
|
|
267
|
+
when IR::IntersectionType
|
|
268
|
+
type_node.types.each { |t| validate_type(t) }
|
|
269
|
+
when IR::NullableType
|
|
270
|
+
validate_type(type_node.inner_type)
|
|
271
|
+
when IR::FunctionType
|
|
272
|
+
type_node.param_types.each { |t| validate_type(t) }
|
|
273
|
+
validate_type(type_node.return_type)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# SMT-based subtype checking
|
|
278
|
+
def subtype_with_smt?(subtype, supertype)
|
|
279
|
+
return true if subtype == supertype
|
|
280
|
+
|
|
281
|
+
if @use_smt
|
|
282
|
+
sub = to_smt_type(subtype)
|
|
283
|
+
sup = to_smt_type(supertype)
|
|
284
|
+
@smt_solver.subtype?(sub, sup)
|
|
285
|
+
else
|
|
286
|
+
@hierarchy.subtype_of?(subtype.to_s, supertype.to_s)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Convert to SMT type
|
|
291
|
+
def to_smt_type(type)
|
|
292
|
+
case type
|
|
293
|
+
when String
|
|
294
|
+
SMT::ConcreteType.new(type)
|
|
295
|
+
when IR::SimpleType
|
|
296
|
+
SMT::ConcreteType.new(type.name)
|
|
297
|
+
when SMT::ConcreteType, SMT::TypeVar
|
|
298
|
+
type
|
|
299
|
+
else
|
|
300
|
+
SMT::ConcreteType.new(type.to_s)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Register a function signature
|
|
305
|
+
def register_function(name, params:, return_type:)
|
|
306
|
+
@function_signatures[name] = {
|
|
307
|
+
params: params,
|
|
308
|
+
return_type: return_type
|
|
309
|
+
}
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Register a type alias
|
|
313
|
+
def register_alias(name, definition)
|
|
314
|
+
@type_aliases[name] = definition
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Check a function call
|
|
318
|
+
def check_call(function_name, arguments, location: nil)
|
|
319
|
+
signature = @function_signatures[function_name]
|
|
320
|
+
|
|
321
|
+
unless signature
|
|
322
|
+
add_warning("Unknown function: #{function_name}", location)
|
|
323
|
+
return nil
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
params = signature[:params]
|
|
327
|
+
|
|
328
|
+
# Check argument count
|
|
329
|
+
if arguments.length != params.length
|
|
330
|
+
add_error(
|
|
331
|
+
message: "Wrong number of arguments for #{function_name}",
|
|
332
|
+
expected: "#{params.length} arguments",
|
|
333
|
+
actual: "#{arguments.length} arguments",
|
|
334
|
+
location: location
|
|
335
|
+
)
|
|
336
|
+
return nil
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Check each argument type
|
|
340
|
+
params.each_with_index do |param, idx|
|
|
341
|
+
next unless param[:type]
|
|
342
|
+
|
|
343
|
+
arg = arguments[idx]
|
|
344
|
+
arg_type = infer_type(arg)
|
|
345
|
+
|
|
346
|
+
next unless arg_type
|
|
347
|
+
|
|
348
|
+
unless type_compatible?(arg_type, param[:type])
|
|
349
|
+
add_error(
|
|
350
|
+
message: "Type mismatch in argument '#{param[:name]}' of #{function_name}",
|
|
351
|
+
expected: param[:type],
|
|
352
|
+
actual: arg_type,
|
|
353
|
+
suggestion: suggest_conversion(arg_type, param[:type]),
|
|
354
|
+
location: location
|
|
355
|
+
)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
signature[:return_type]
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Check a return statement
|
|
363
|
+
def check_return(value, expected_type, location: nil)
|
|
364
|
+
return true unless expected_type
|
|
365
|
+
|
|
366
|
+
actual_type = infer_type(value)
|
|
367
|
+
return true unless actual_type
|
|
368
|
+
|
|
369
|
+
unless type_compatible?(actual_type, expected_type)
|
|
370
|
+
add_error(
|
|
371
|
+
message: "Return type mismatch",
|
|
372
|
+
expected: expected_type,
|
|
373
|
+
actual: actual_type,
|
|
374
|
+
suggestion: suggest_conversion(actual_type, expected_type),
|
|
375
|
+
location: location
|
|
376
|
+
)
|
|
377
|
+
return false
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
true
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Check variable assignment
|
|
384
|
+
def check_assignment(variable, value, declared_type: nil, location: nil)
|
|
385
|
+
value_type = infer_type(value)
|
|
386
|
+
|
|
387
|
+
if declared_type
|
|
388
|
+
unless type_compatible?(value_type, declared_type)
|
|
389
|
+
add_error(
|
|
390
|
+
message: "Cannot assign #{value_type} to variable of type #{declared_type}",
|
|
391
|
+
expected: declared_type,
|
|
392
|
+
actual: value_type,
|
|
393
|
+
location: location
|
|
394
|
+
)
|
|
395
|
+
return false
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
@current_scope.define(variable, declared_type || value_type)
|
|
400
|
+
true
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Check property access
|
|
404
|
+
def check_property_access(receiver_type, property_name, location: nil)
|
|
405
|
+
# Known properties for common types
|
|
406
|
+
known_properties = {
|
|
407
|
+
"String" => %w[length size empty? chars bytes],
|
|
408
|
+
"Array" => %w[length size first last empty? count],
|
|
409
|
+
"Hash" => %w[keys values size empty? length],
|
|
410
|
+
"Integer" => %w[abs to_s to_f even? odd? positive? negative?],
|
|
411
|
+
"Float" => %w[abs to_s to_i ceil floor round]
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
properties = known_properties[receiver_type]
|
|
415
|
+
return nil unless properties
|
|
416
|
+
|
|
417
|
+
unless properties.include?(property_name)
|
|
418
|
+
add_warning("Property '#{property_name}' may not exist on type #{receiver_type}", location)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Return expected type for known properties
|
|
422
|
+
infer_property_type(receiver_type, property_name)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Check operator usage
|
|
426
|
+
def check_operator(left_type, operator, right_type, location: nil)
|
|
427
|
+
# Arithmetic operators
|
|
428
|
+
if %w[+ - * / %].include?(operator)
|
|
429
|
+
if left_type == "String" && operator == "+"
|
|
430
|
+
unless right_type == "String"
|
|
431
|
+
add_error(
|
|
432
|
+
message: "Cannot concatenate String with #{right_type}",
|
|
433
|
+
expected: "String",
|
|
434
|
+
actual: right_type,
|
|
435
|
+
suggestion: "Use .to_s to convert to String",
|
|
436
|
+
location: location
|
|
437
|
+
)
|
|
438
|
+
end
|
|
439
|
+
return "String"
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
unless numeric_type?(left_type) && numeric_type?(right_type)
|
|
443
|
+
add_error(
|
|
444
|
+
message: "Operator '#{operator}' requires numeric operands",
|
|
445
|
+
expected: "Numeric",
|
|
446
|
+
actual: "#{left_type} #{operator} #{right_type}",
|
|
447
|
+
location: location
|
|
448
|
+
)
|
|
449
|
+
return nil
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Result type depends on operands
|
|
453
|
+
return "Float" if left_type == "Float" || right_type == "Float"
|
|
454
|
+
return "Integer"
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Comparison operators
|
|
458
|
+
if %w[== != < > <= >=].include?(operator)
|
|
459
|
+
return "Boolean"
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Logical operators
|
|
463
|
+
if %w[&& ||].include?(operator)
|
|
464
|
+
return right_type # Short-circuit returns right operand type
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
nil
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Handle conditional type narrowing
|
|
471
|
+
def narrow_in_conditional(condition, then_scope, else_scope = nil)
|
|
472
|
+
# Parse type guards from condition
|
|
473
|
+
if condition.match?(/(\w+)\.is_a\?\((\w+)\)/)
|
|
474
|
+
match = condition.match(/(\w+)\.is_a\?\((\w+)\)/)
|
|
475
|
+
var = match[1]
|
|
476
|
+
type = match[2]
|
|
477
|
+
|
|
478
|
+
# In then branch, variable is narrowed to the type
|
|
479
|
+
then_scope.narrow(var, type)
|
|
480
|
+
|
|
481
|
+
# In else branch, variable is NOT that type (can't narrow further without more info)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
if condition.match?(/(\w+)\.nil\?/)
|
|
485
|
+
match = condition.match(/(\w+)\.nil\?/)
|
|
486
|
+
var = match[1]
|
|
487
|
+
|
|
488
|
+
then_scope.narrow(var, "nil")
|
|
489
|
+
# In else branch, variable is not nil
|
|
490
|
+
else_scope&.narrow(var, remove_nil_from_type(@current_scope.lookup(var) || "Object"))
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
if condition.match?(/!(\w+)\.nil\?/)
|
|
494
|
+
match = condition.match(/!(\w+)\.nil\?/)
|
|
495
|
+
var = match[1]
|
|
496
|
+
|
|
497
|
+
# Variable is not nil in then branch
|
|
498
|
+
then_scope.narrow(var, remove_nil_from_type(@current_scope.lookup(var) || "Object"))
|
|
499
|
+
else_scope&.narrow(var, "nil")
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Check a complete function
|
|
504
|
+
def check_function(function_info, body_lines)
|
|
505
|
+
@current_scope = TypeScope.new
|
|
506
|
+
|
|
507
|
+
# Register parameters in scope
|
|
508
|
+
function_info[:params].each do |param|
|
|
509
|
+
@current_scope.define(param[:name], param[:type] || "Object")
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Register function signature
|
|
513
|
+
register_function(
|
|
514
|
+
function_info[:name],
|
|
515
|
+
params: function_info[:params],
|
|
516
|
+
return_type: function_info[:return_type]
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Check body (simplified - real implementation would parse AST)
|
|
520
|
+
body_lines.each_with_index do |line, idx|
|
|
521
|
+
check_statement(line, location: "line #{idx + 1}")
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Check a statement
|
|
526
|
+
def check_statement(line, location: nil)
|
|
527
|
+
line = line.strip
|
|
528
|
+
|
|
529
|
+
# Return statement
|
|
530
|
+
if line.match?(/^return\s+(.+)/)
|
|
531
|
+
match = line.match(/^return\s+(.+)/)
|
|
532
|
+
# Would need current function context for proper checking
|
|
533
|
+
return
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Assignment
|
|
537
|
+
if line.match?(/^(\w+)\s*=\s*(.+)/)
|
|
538
|
+
match = line.match(/^(\w+)\s*=\s*(.+)/)
|
|
539
|
+
check_assignment(match[1], match[2], location: location)
|
|
540
|
+
return
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Method call
|
|
544
|
+
if line.match?(/(\w+)\(([^)]*)\)/)
|
|
545
|
+
match = line.match(/(\w+)\(([^)]*)\)/)
|
|
546
|
+
args = match[2].split(",").map(&:strip)
|
|
547
|
+
check_call(match[1], args, location: location)
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Resolve type alias
|
|
552
|
+
def resolve_type(type_name)
|
|
553
|
+
@type_aliases[type_name] || type_name
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Clear all state
|
|
557
|
+
def reset
|
|
558
|
+
@errors.clear
|
|
559
|
+
@warnings.clear
|
|
560
|
+
@function_signatures.clear
|
|
561
|
+
@type_aliases.clear
|
|
562
|
+
@current_scope = TypeScope.new
|
|
563
|
+
@flow_context = FlowContext.new
|
|
564
|
+
@smt_solver = SMT::ConstraintSolver.new if @use_smt
|
|
565
|
+
@smt_inference_engine = SMT::TypeInferenceEngine.new if @use_smt
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# Get all diagnostics
|
|
569
|
+
def diagnostics
|
|
570
|
+
@errors.map { |e| e.respond_to?(:to_diagnostic) ? e.to_diagnostic : { type: :error, message: e.to_s } } +
|
|
571
|
+
@warnings.map { |w| { type: :warning, message: w } }
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# Check if a type name is known
|
|
575
|
+
def known_type?(type_name)
|
|
576
|
+
return true if %w[String Integer Float Boolean Array Hash Symbol void nil Object Numeric Enumerable].include?(type_name)
|
|
577
|
+
return true if @type_aliases.key?(type_name)
|
|
578
|
+
false
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# Infer parameter type from annotation
|
|
582
|
+
def infer_param_type(param)
|
|
583
|
+
if param.type_annotation
|
|
584
|
+
case param.type_annotation
|
|
585
|
+
when IR::SimpleType
|
|
586
|
+
param.type_annotation.name
|
|
587
|
+
else
|
|
588
|
+
param.type_annotation.to_s
|
|
589
|
+
end
|
|
590
|
+
else
|
|
591
|
+
"Object"
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# Legacy program check (without SMT)
|
|
596
|
+
def check_program_legacy(ir_program)
|
|
597
|
+
@errors = []
|
|
598
|
+
@warnings = []
|
|
599
|
+
|
|
600
|
+
ir_program.declarations.each do |decl|
|
|
601
|
+
case decl
|
|
602
|
+
when IR::TypeAlias
|
|
603
|
+
register_alias(decl.name, decl.definition)
|
|
604
|
+
when IR::Interface
|
|
605
|
+
check_interface(decl)
|
|
606
|
+
when IR::MethodDef
|
|
607
|
+
check_function_legacy(decl)
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
{
|
|
612
|
+
success: @errors.empty?,
|
|
613
|
+
errors: @errors,
|
|
614
|
+
warnings: @warnings
|
|
615
|
+
}
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# Check function without SMT (legacy)
|
|
619
|
+
def check_function_legacy(method_ir)
|
|
620
|
+
@current_scope = TypeScope.new
|
|
621
|
+
|
|
622
|
+
# Register parameters
|
|
623
|
+
method_ir.params.each do |param|
|
|
624
|
+
param_type = infer_param_type(param)
|
|
625
|
+
@current_scope.define(param.name, param_type)
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
# Register function signature
|
|
629
|
+
register_function(
|
|
630
|
+
method_ir.name,
|
|
631
|
+
params: method_ir.params.map { |p| { name: p.name, type: infer_param_type(p) } },
|
|
632
|
+
return_type: method_ir.return_type&.to_s || "Object"
|
|
633
|
+
)
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
private
|
|
637
|
+
|
|
638
|
+
def add_error(message:, expected: nil, actual: nil, suggestion: nil, location: nil)
|
|
639
|
+
@errors << TypeCheckError.new(
|
|
640
|
+
message: message,
|
|
641
|
+
expected: expected,
|
|
642
|
+
actual: actual,
|
|
643
|
+
suggestion: suggestion,
|
|
644
|
+
location: location
|
|
645
|
+
)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def add_warning(message, location = nil)
|
|
649
|
+
full_message = location ? "#{message} at #{location}" : message
|
|
650
|
+
@warnings << full_message
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def infer_type(expression)
|
|
654
|
+
result = @inferencer.infer_expression_type(expression)
|
|
655
|
+
result&.type
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
def type_compatible?(actual, expected)
|
|
659
|
+
return true if actual.nil? || expected.nil?
|
|
660
|
+
|
|
661
|
+
actual = resolve_type(actual)
|
|
662
|
+
expected = resolve_type(expected)
|
|
663
|
+
|
|
664
|
+
return true if actual == expected
|
|
665
|
+
|
|
666
|
+
# Handle union types in expected
|
|
667
|
+
if expected.include?("|")
|
|
668
|
+
types = expected.split("|").map(&:strip)
|
|
669
|
+
return types.any? { |t| type_compatible?(actual, t) }
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
# Handle union types in actual
|
|
673
|
+
if actual.include?("|")
|
|
674
|
+
types = actual.split("|").map(&:strip)
|
|
675
|
+
return types.all? { |t| type_compatible?(t, expected) }
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Check hierarchy
|
|
679
|
+
@hierarchy.subtype_of?(actual, expected)
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def numeric_type?(type)
|
|
683
|
+
%w[Integer Float Numeric].include?(type)
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def suggest_conversion(from_type, to_type)
|
|
687
|
+
conversions = {
|
|
688
|
+
["Integer", "String"] => "Use .to_s to convert to String",
|
|
689
|
+
["Float", "String"] => "Use .to_s to convert to String",
|
|
690
|
+
["String", "Integer"] => "Use .to_i to convert to Integer",
|
|
691
|
+
["String", "Float"] => "Use .to_f to convert to Float",
|
|
692
|
+
["Integer", "Float"] => "Use .to_f to convert to Float",
|
|
693
|
+
["Float", "Integer"] => "Use .to_i to convert to Integer (may lose precision)",
|
|
694
|
+
["Symbol", "String"] => "Use .to_s to convert to String",
|
|
695
|
+
["String", "Symbol"] => "Use .to_sym to convert to Symbol"
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
conversions[[from_type, to_type]]
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def infer_property_type(receiver_type, property)
|
|
702
|
+
property_types = {
|
|
703
|
+
["String", "length"] => "Integer",
|
|
704
|
+
["String", "size"] => "Integer",
|
|
705
|
+
["String", "empty?"] => "Boolean",
|
|
706
|
+
["String", "chars"] => "Array<String>",
|
|
707
|
+
["Array", "length"] => "Integer",
|
|
708
|
+
["Array", "size"] => "Integer",
|
|
709
|
+
["Array", "empty?"] => "Boolean",
|
|
710
|
+
["Array", "first"] => nil, # Depends on element type
|
|
711
|
+
["Array", "last"] => nil,
|
|
712
|
+
["Hash", "keys"] => "Array",
|
|
713
|
+
["Hash", "values"] => "Array",
|
|
714
|
+
["Hash", "size"] => "Integer",
|
|
715
|
+
["Integer", "abs"] => "Integer",
|
|
716
|
+
["Integer", "to_s"] => "String",
|
|
717
|
+
["Float", "abs"] => "Float",
|
|
718
|
+
["Float", "to_i"] => "Integer"
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
property_types[[receiver_type, property]]
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def remove_nil_from_type(type)
|
|
725
|
+
return "Object" if type == "nil"
|
|
726
|
+
|
|
727
|
+
if type.include?("|")
|
|
728
|
+
types = type.split("|").map(&:strip).reject { |t| t == "nil" }
|
|
729
|
+
return types.length == 1 ? types.first : types.join(" | ")
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
type
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
# Legacy TypeChecker without SMT (backward compatible)
|
|
737
|
+
class LegacyTypeChecker < TypeChecker
|
|
738
|
+
def initialize
|
|
739
|
+
super(use_smt: false)
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
# SMT-enhanced type checker with constraint solving
|
|
744
|
+
class SMTTypeChecker < TypeChecker
|
|
745
|
+
include SMT::DSL
|
|
746
|
+
|
|
747
|
+
def initialize
|
|
748
|
+
super(use_smt: true)
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
# Add constraint-based type check
|
|
752
|
+
def check_with_constraints(ir_program, &block)
|
|
753
|
+
# Allow users to add custom constraints
|
|
754
|
+
block.call(@smt_solver) if block_given?
|
|
755
|
+
|
|
756
|
+
# Run standard type checking
|
|
757
|
+
check_program(ir_program)
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
# Solve current constraints and return solution
|
|
761
|
+
def solve_constraints
|
|
762
|
+
@smt_solver.solve
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
# Get inferred type for a variable
|
|
766
|
+
def inferred_type(var_name)
|
|
767
|
+
@smt_solver.infer(SMT::TypeVar.new(var_name))
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
end
|