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