t-ruby 0.0.38 → 0.0.39

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.
@@ -3,6 +3,11 @@
3
3
  require "fileutils"
4
4
 
5
5
  module TRuby
6
+ # Pattern for method names that supports Unicode characters
7
+ # \p{L} matches any Unicode letter, \p{N} matches any Unicode number
8
+ IDENTIFIER_CHAR = '[\p{L}\p{N}_]'
9
+ METHOD_NAME_PATTERN = "#{IDENTIFIER_CHAR}+[?!]?".freeze
10
+
6
11
  class Compiler
7
12
  attr_reader :declaration_loader, :use_ir, :optimizer
8
13
 
@@ -12,9 +17,14 @@ module TRuby
12
17
  @optimize = optimize
13
18
  @declaration_loader = DeclarationLoader.new
14
19
  @optimizer = IR::Optimizer.new if use_ir && optimize
20
+ @type_inferrer = ASTTypeInferrer.new if type_check?
15
21
  setup_declaration_paths if @config
16
22
  end
17
23
 
24
+ def type_check?
25
+ @config.type_check?
26
+ end
27
+
18
28
  def compile(input_path)
19
29
  unless File.exist?(input_path)
20
30
  raise ArgumentError, "File not found: #{input_path}"
@@ -35,6 +45,11 @@ module TRuby
35
45
  parser = Parser.new(source, use_combinator: @use_ir)
36
46
  parse_result = parser.parse
37
47
 
48
+ # Run type checking if enabled
49
+ if type_check? && @use_ir && parser.ir_program
50
+ check_types(parser.ir_program, input_path)
51
+ end
52
+
38
53
  # Transform source to Ruby code
39
54
  output = @use_ir ? transform_with_ir(source, parser) : transform_legacy(source, parse_result)
40
55
 
@@ -215,6 +230,140 @@ module TRuby
215
230
 
216
231
  private
217
232
 
233
+ # Check types in IR program and raise TypeCheckError if mismatches found
234
+ # @param ir_program [IR::Program] IR program to check
235
+ # @param file_path [String] source file path for error messages
236
+ def check_types(ir_program, file_path)
237
+ ir_program.declarations.each do |decl|
238
+ case decl
239
+ when IR::MethodDef
240
+ check_method_return_type(decl, nil, file_path)
241
+ when IR::ClassDecl
242
+ decl.body.each do |member|
243
+ check_method_return_type(member, decl, file_path) if member.is_a?(IR::MethodDef)
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ # Check if method's inferred return type matches declared return type
250
+ # @param method [IR::MethodDef] method to check
251
+ # @param class_def [IR::ClassDef, nil] containing class if any
252
+ # @param file_path [String] source file path for error messages
253
+ def check_method_return_type(method, class_def, file_path)
254
+ # Skip if no explicit return type annotation
255
+ return unless method.return_type
256
+
257
+ declared_type = normalize_type(method.return_type.to_rbs)
258
+
259
+ # Create type environment for the class context
260
+ class_env = create_class_env(class_def) if class_def
261
+
262
+ # Infer actual return type
263
+ inferred_type = @type_inferrer.infer_method_return_type(method, class_env)
264
+ inferred_type = normalize_type(inferred_type || "nil")
265
+
266
+ # Check compatibility
267
+ return if types_compatible?(inferred_type, declared_type)
268
+
269
+ location = method.location ? "#{file_path}:#{method.location}" : file_path
270
+ method_name = class_def ? "#{class_def.name}##{method.name}" : method.name
271
+
272
+ raise TypeCheckError.new(
273
+ message: "Return type mismatch in method '#{method_name}': " \
274
+ "declared '#{declared_type}' but inferred '#{inferred_type}'",
275
+ location: location,
276
+ expected: declared_type,
277
+ actual: inferred_type
278
+ )
279
+ end
280
+
281
+ # Create type environment for class context
282
+ # @param class_def [IR::ClassDecl] class declaration
283
+ # @return [TypeEnv] type environment with instance variables
284
+ def create_class_env(class_def)
285
+ env = TypeEnv.new
286
+
287
+ # Register instance variables from class
288
+ class_def.instance_vars&.each do |ivar|
289
+ type = ivar.type_annotation&.to_rbs || "untyped"
290
+ env.define_instance_var(ivar.name, type)
291
+ end
292
+
293
+ env
294
+ end
295
+
296
+ # Normalize type string for comparison
297
+ # @param type [String] type string
298
+ # @return [String] normalized type string
299
+ def normalize_type(type)
300
+ return "untyped" if type.nil?
301
+
302
+ normalized = type.to_s.strip
303
+
304
+ # Normalize boolean types (bool/Boolean/TrueClass/FalseClass -> bool)
305
+ case normalized
306
+ when "Boolean", "TrueClass", "FalseClass"
307
+ "bool"
308
+ else
309
+ normalized
310
+ end
311
+ end
312
+
313
+ # Check if inferred type is compatible with declared type
314
+ # @param inferred [String] inferred type
315
+ # @param declared [String] declared type
316
+ # @return [Boolean] true if compatible
317
+ def types_compatible?(inferred, declared)
318
+ # Exact match
319
+ return true if inferred == declared
320
+
321
+ # untyped is compatible with anything
322
+ return true if inferred == "untyped" || declared == "untyped"
323
+
324
+ # void is compatible with anything (no return value check)
325
+ return true if declared == "void"
326
+
327
+ # nil is compatible with nullable types
328
+ return true if inferred == "nil" && declared.end_with?("?")
329
+
330
+ # Subtype relationships
331
+ return true if subtype_of?(inferred, declared)
332
+
333
+ # Handle union types in declared
334
+ if declared.include?("|")
335
+ declared_types = declared.split("|").map(&:strip)
336
+ return true if declared_types.include?(inferred)
337
+ return true if declared_types.any? { |t| types_compatible?(inferred, t) }
338
+ end
339
+
340
+ # Handle union types in inferred - all must be compatible
341
+ if inferred.include?("|")
342
+ inferred_types = inferred.split("|").map(&:strip)
343
+ return inferred_types.all? { |t| types_compatible?(t, declared) }
344
+ end
345
+
346
+ false
347
+ end
348
+
349
+ # Check if subtype is a subtype of supertype
350
+ # @param subtype [String] potential subtype
351
+ # @param supertype [String] potential supertype
352
+ # @return [Boolean] true if subtype
353
+ def subtype_of?(subtype, supertype)
354
+ # Handle nullable - X is subtype of X?
355
+ return true if supertype.end_with?("?") && supertype[0..-2] == subtype
356
+
357
+ # Numeric hierarchy
358
+ return true if subtype == "Integer" && supertype == "Numeric"
359
+ return true if subtype == "Float" && supertype == "Numeric"
360
+
361
+ # Object is supertype of everything
362
+ return true if supertype == "Object"
363
+
364
+ false
365
+ end
366
+
218
367
  # Resolve path to absolute path, following symlinks
219
368
  # Falls back to expand_path if realpath fails (e.g., file doesn't exist yet)
220
369
  def resolve_path(path)
@@ -362,7 +511,7 @@ module TRuby
362
511
  result = source.dup
363
512
 
364
513
  # Match function definitions and remove type annotations from parameters
365
- result.gsub!(/^(\s*def\s+\w+\s*\()([^)]+)(\)\s*)(?::\s*[^\n]+)?(\s*$)/) do |_match|
514
+ result.gsub!(/^(\s*def\s+#{TRuby::METHOD_NAME_PATTERN}\s*\()([^)]+)(\)\s*)(?::\s*[^\n]+)?(\s*$)/) do |_match|
366
515
  indent = ::Regexp.last_match(1)
367
516
  params = ::Regexp.last_match(2)
368
517
  close_paren = ::Regexp.last_match(3)
@@ -411,8 +560,8 @@ module TRuby
411
560
 
412
561
  # Clean a single parameter (remove type annotation)
413
562
  def clean_param(param)
414
- # Match: name: Type or name
415
- if (match = param.match(/^(\w+)\s*:/))
563
+ # Match: name: Type or name (supports Unicode identifiers)
564
+ if (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:/))
416
565
  match[1]
417
566
  else
418
567
  param
data/lib/t_ruby/config.rb CHANGED
@@ -24,6 +24,7 @@ module TRuby
24
24
  "compiler" => {
25
25
  "strictness" => "standard",
26
26
  "generate_rbs" => true,
27
+ "type_check" => true,
27
28
  "target_ruby" => "3.0",
28
29
  "experimental" => [],
29
30
  "checks" => {
@@ -89,6 +90,12 @@ module TRuby
89
90
  @compiler["generate_rbs"] != false
90
91
  end
91
92
 
93
+ # Check if type checking is enabled
94
+ # @return [Boolean] true if type checking is enabled (default: true)
95
+ def type_check?
96
+ @compiler["type_check"] != false
97
+ end
98
+
92
99
  # Get target Ruby version
93
100
  # @return [String] target Ruby version (e.g., "3.0", "3.2")
94
101
  def target_ruby
data/lib/t_ruby/ir.rb CHANGED
@@ -83,15 +83,16 @@ module TRuby
83
83
 
84
84
  # Class declaration
85
85
  class ClassDecl < Node
86
- attr_accessor :name, :superclass, :implements, :type_params, :body
86
+ attr_accessor :name, :superclass, :implements, :type_params, :body, :instance_vars
87
87
 
88
- def initialize(name:, superclass: nil, implements: [], type_params: [], body: [], **opts)
88
+ def initialize(name:, superclass: nil, implements: [], type_params: [], body: [], instance_vars: [], **opts)
89
89
  super(**opts)
90
90
  @name = name
91
91
  @superclass = superclass
92
92
  @implements = implements
93
93
  @type_params = type_params
94
94
  @body = body
95
+ @instance_vars = instance_vars
95
96
  end
96
97
 
97
98
  def children
@@ -99,6 +100,17 @@ module TRuby
99
100
  end
100
101
  end
101
102
 
103
+ # Instance variable declaration
104
+ class InstanceVariable < Node
105
+ attr_accessor :name, :type_annotation
106
+
107
+ def initialize(name:, type_annotation: nil, **opts)
108
+ super(**opts)
109
+ @name = name
110
+ @type_annotation = type_annotation
111
+ end
112
+ end
113
+
102
114
  # Module declaration
103
115
  class ModuleDecl < Node
104
116
  attr_accessor :name, :body
@@ -686,6 +698,11 @@ module TRuby
686
698
  declarations << build_interface(interface_info)
687
699
  end
688
700
 
701
+ # Build classes
702
+ (parse_result[:classes] || []).each do |class_info|
703
+ declarations << build_class(class_info)
704
+ end
705
+
689
706
  # Build functions/methods
690
707
  (parse_result[:functions] || []).each do |func_info|
691
708
  declarations << build_method(func_info)
@@ -724,6 +741,28 @@ module TRuby
724
741
  )
725
742
  end
726
743
 
744
+ def build_class(info)
745
+ # Build methods
746
+ methods = (info[:methods] || []).map do |method_info|
747
+ build_method(method_info)
748
+ end
749
+
750
+ # Build instance variables
751
+ instance_vars = (info[:instance_vars] || []).map do |ivar|
752
+ InstanceVariable.new(
753
+ name: ivar[:name],
754
+ type_annotation: ivar[:type] ? parse_type(ivar[:type]) : nil
755
+ )
756
+ end
757
+
758
+ ClassDecl.new(
759
+ name: info[:name],
760
+ superclass: info[:superclass],
761
+ body: methods,
762
+ instance_vars: instance_vars
763
+ )
764
+ end
765
+
727
766
  def build_method(info)
728
767
  params = (info[:params] || []).map do |param|
729
768
  Parameter.new(
@@ -732,10 +771,14 @@ module TRuby
732
771
  )
733
772
  end
734
773
 
774
+ # 본문 IR이 있으면 사용 (BodyParser에서 파싱됨)
775
+ body = info[:body_ir]
776
+
735
777
  MethodDef.new(
736
778
  name: info[:name],
737
779
  params: params,
738
- return_type: info[:return_type] ? parse_type(info[:return_type]) : nil
780
+ return_type: info[:return_type] ? parse_type(info[:return_type]) : nil,
781
+ body: body
739
782
  )
740
783
  end
741
784
 
@@ -947,9 +990,12 @@ module TRuby
947
990
  class RBSGenerator < Visitor
948
991
  attr_reader :output
949
992
 
950
- def initialize
993
+ def initialize(enable_inference: true)
951
994
  @output = []
952
995
  @indent = 0
996
+ @enable_inference = enable_inference
997
+ @inferrer = TRuby::ASTTypeInferrer.new if enable_inference
998
+ @class_env = nil # 현재 클래스의 타입 환경
953
999
  end
954
1000
 
955
1001
  def generate(program)
@@ -988,21 +1034,59 @@ module TRuby
988
1034
  def visit_method_def(node)
989
1035
  params = node.params.map do |param|
990
1036
  type = param.type_annotation&.to_rbs || "untyped"
991
- "#{type} #{param.name}"
1037
+ "#{param.name}: #{type}"
992
1038
  end.join(", ")
993
1039
 
994
- return_type = node.return_type&.to_rbs || "untyped"
1040
+ # 반환 타입: 명시적 타입 > 추론된 타입 > untyped
1041
+ return_type = node.return_type&.to_rbs
1042
+
1043
+ # initialize 메서드는 특별 처리: 명시적 타입이 없으면 void
1044
+ # Ruby에서 initialize는 생성자이며, 실제 인스턴스 생성은 Class.new가 담당
1045
+ if node.name == "initialize" && return_type.nil?
1046
+ return_type = "void"
1047
+ elsif return_type.nil? && @enable_inference && @inferrer && node.body
1048
+ # 명시적 반환 타입이 없으면 추론 시도
1049
+ inferred = @inferrer.infer_method_return_type(node, @class_env)
1050
+ return_type = inferred if inferred && inferred != "untyped"
1051
+ end
1052
+
1053
+ return_type ||= "untyped"
995
1054
  emit("def #{node.name}: (#{params}) -> #{return_type}")
996
1055
  end
997
1056
 
998
1057
  def visit_class_decl(node)
999
1058
  emit("class #{node.name}")
1000
1059
  @indent += 1
1060
+
1061
+ # 클래스 타입 환경 생성
1062
+ @class_env = TRuby::TypeEnv.new if @enable_inference
1063
+
1064
+ # 인스턴스 변수 타입 등록
1065
+ (node.instance_vars || []).each do |ivar|
1066
+ if @class_env && ivar.type_annotation
1067
+ @class_env.define_instance_var("@#{ivar.name}", ivar.type_annotation.to_rbs)
1068
+ end
1069
+
1070
+ # Emit instance variables first
1071
+ visit_instance_variable(ivar)
1072
+ end
1073
+
1074
+ # Add blank line between ivars and methods if both exist
1075
+ @output << "" if node.instance_vars&.any? && node.body&.any?
1076
+
1077
+ # Emit methods
1001
1078
  node.body.each { |member| visit(member) }
1079
+
1080
+ @class_env = nil
1002
1081
  @indent -= 1
1003
1082
  emit("end")
1004
1083
  end
1005
1084
 
1085
+ def visit_instance_variable(node)
1086
+ type = node.type_annotation&.to_rbs || "untyped"
1087
+ emit("@#{node.name}: #{type}")
1088
+ end
1089
+
1006
1090
  private
1007
1091
 
1008
1092
  def emit(text)
data/lib/t_ruby/parser.rb CHANGED
@@ -7,13 +7,21 @@ module TRuby
7
7
  # Type names that are recognized as valid
8
8
  VALID_TYPES = %w[String Integer Boolean Array Hash Symbol void nil].freeze
9
9
 
10
+ # Pattern for method/variable names that supports Unicode characters
11
+ # \p{L} matches any Unicode letter, \p{N} matches any Unicode number
12
+ IDENTIFIER_CHAR = '[\p{L}\p{N}_]'
13
+ # Method names can end with ? or !
14
+ METHOD_NAME_PATTERN = "#{IDENTIFIER_CHAR}+[?!]?".freeze
15
+
10
16
  attr_reader :source, :ir_program, :use_combinator
11
17
 
12
- def initialize(source, use_combinator: true)
18
+ def initialize(source, use_combinator: true, parse_body: true)
13
19
  @source = source
14
20
  @lines = source.split("\n")
15
21
  @use_combinator = use_combinator
22
+ @parse_body = parse_body
16
23
  @type_parser = ParserCombinator::TypeParser.new if use_combinator
24
+ @body_parser = BodyParser.new if parse_body
17
25
  @ir_program = nil
18
26
  end
19
27
 
@@ -21,6 +29,7 @@ module TRuby
21
29
  functions = []
22
30
  type_aliases = []
23
31
  interfaces = []
32
+ classes = []
24
33
  i = 0
25
34
 
26
35
  while i < @lines.length
@@ -42,10 +51,24 @@ module TRuby
42
51
  end
43
52
  end
44
53
 
45
- # Match function definitions
46
- if line.match?(/^\s*def\s+\w+/)
47
- func_info = parse_function_definition(line)
48
- functions << func_info if func_info
54
+ # Match class definitions
55
+ if line.match?(/^\s*class\s+\w+/)
56
+ class_info, next_i = parse_class(i)
57
+ if class_info
58
+ classes << class_info
59
+ i = next_i
60
+ next
61
+ end
62
+ end
63
+
64
+ # Match function definitions (top-level only, not inside class)
65
+ if line.match?(/^\s*def\s+#{IDENTIFIER_CHAR}+/)
66
+ func_info, next_i = parse_function_with_body(i)
67
+ if func_info
68
+ functions << func_info
69
+ i = next_i
70
+ next
71
+ end
49
72
  end
50
73
 
51
74
  i += 1
@@ -56,6 +79,7 @@ module TRuby
56
79
  functions: functions,
57
80
  type_aliases: type_aliases,
58
81
  interfaces: interfaces,
82
+ classes: classes,
59
83
  }
60
84
 
61
85
  # Build IR if combinator is enabled
@@ -83,6 +107,41 @@ module TRuby
83
107
 
84
108
  private
85
109
 
110
+ # 최상위 함수를 본문까지 포함하여 파싱
111
+ def parse_function_with_body(start_index)
112
+ line = @lines[start_index]
113
+ func_info = parse_function_definition(line)
114
+ return [nil, start_index] unless func_info
115
+
116
+ def_indent = line.match(/^(\s*)/)[1].length
117
+ i = start_index + 1
118
+ body_start = i
119
+ body_end = i
120
+
121
+ # end 키워드 찾기
122
+ while i < @lines.length
123
+ current_line = @lines[i]
124
+
125
+ if current_line.match?(/^\s*end\s*$/)
126
+ end_indent = current_line.match(/^(\s*)/)[1].length
127
+ if end_indent <= def_indent
128
+ body_end = i
129
+ break
130
+ end
131
+ end
132
+
133
+ i += 1
134
+ end
135
+
136
+ # 본문 파싱 (parse_body 옵션이 활성화된 경우)
137
+ if @parse_body && @body_parser && body_start < body_end
138
+ func_info[:body_ir] = @body_parser.parse(@lines, body_start, body_end)
139
+ func_info[:body_range] = { start: body_start, end: body_end }
140
+ end
141
+
142
+ [func_info, i]
143
+ end
144
+
86
145
  def parse_type_alias(line)
87
146
  match = line.match(/^\s*type\s+(\w+)\s*=\s*(.+?)\s*$/)
88
147
  return nil unless match
@@ -109,11 +168,16 @@ module TRuby
109
168
  end
110
169
 
111
170
  def parse_function_definition(line)
112
- match = line.match(/^\s*def\s+(\w+)\s*\((.*?)\)\s*(?::\s*(.+?))?\s*$/)
171
+ # Match methods with or without parentheses
172
+ # def foo(params): Type - with params and return type
173
+ # def foo(): Type - no params but with return type
174
+ # def foo(params) - with params, no return type
175
+ # def foo - no params, no return type
176
+ match = line.match(/^\s*def\s+(#{METHOD_NAME_PATTERN})\s*(?:\((.*?)\))?\s*(?::\s*(.+?))?\s*$/)
113
177
  return nil unless match
114
178
 
115
179
  function_name = match[1]
116
- params_str = match[2]
180
+ params_str = match[2] || ""
117
181
  return_type_str = match[3]&.strip
118
182
 
119
183
  # Validate return type if present
@@ -231,6 +295,127 @@ module TRuby
231
295
  result
232
296
  end
233
297
 
298
+ def parse_class(start_index)
299
+ line = @lines[start_index]
300
+ match = line.match(/^\s*class\s+(\w+)(?:\s*<\s*(\w+))?/)
301
+ return [nil, start_index] unless match
302
+
303
+ class_name = match[1]
304
+ superclass = match[2]
305
+ methods = []
306
+ instance_vars = []
307
+ i = start_index + 1
308
+ class_indent = line.match(/^(\s*)/)[1].length
309
+ class_end = i
310
+
311
+ # 먼저 클래스의 끝을 찾음
312
+ temp_i = i
313
+ while temp_i < @lines.length
314
+ current_line = @lines[temp_i]
315
+ if current_line.match?(/^\s*end\s*$/)
316
+ end_indent = current_line.match(/^(\s*)/)[1].length
317
+ if end_indent <= class_indent
318
+ class_end = temp_i
319
+ break
320
+ end
321
+ end
322
+ temp_i += 1
323
+ end
324
+
325
+ while i < class_end
326
+ current_line = @lines[i]
327
+
328
+ # Match method definitions inside class
329
+ if current_line.match?(/^\s*def\s+#{IDENTIFIER_CHAR}+/)
330
+ method_info, next_i = parse_method_in_class(i, class_end)
331
+ if method_info
332
+ methods << method_info
333
+ i = next_i
334
+ next
335
+ end
336
+ end
337
+
338
+ i += 1
339
+ end
340
+
341
+ # 메서드 본문에서 인스턴스 변수 추출
342
+ methods.each do |method_info|
343
+ extract_instance_vars_from_body(method_info[:body_ir], instance_vars)
344
+ end
345
+
346
+ # Try to infer instance variable types from initialize parameters
347
+ init_method = methods.find { |m| m[:name] == "initialize" }
348
+ if init_method
349
+ instance_vars.each do |ivar|
350
+ # Find matching parameter (e.g., @name = name)
351
+ matching_param = init_method[:params]&.find { |p| p[:name] == ivar[:name] }
352
+ ivar[:type] = matching_param[:type] if matching_param && matching_param[:type]
353
+ ivar[:ir_type] = matching_param[:ir_type] if matching_param && matching_param[:ir_type]
354
+ end
355
+ end
356
+
357
+ [{
358
+ name: class_name,
359
+ superclass: superclass,
360
+ methods: methods,
361
+ instance_vars: instance_vars,
362
+ }, class_end,]
363
+ end
364
+
365
+ # 클래스 내부의 메서드를 본문까지 포함하여 파싱
366
+ def parse_method_in_class(start_index, class_end)
367
+ line = @lines[start_index]
368
+ method_info = parse_function_definition(line)
369
+ return [nil, start_index] unless method_info
370
+
371
+ def_indent = line.match(/^(\s*)/)[1].length
372
+ i = start_index + 1
373
+ body_start = i
374
+ body_end = i
375
+
376
+ # 메서드의 end 키워드 찾기
377
+ while i < class_end
378
+ current_line = @lines[i]
379
+
380
+ if current_line.match?(/^\s*end\s*$/)
381
+ end_indent = current_line.match(/^(\s*)/)[1].length
382
+ if end_indent <= def_indent
383
+ body_end = i
384
+ break
385
+ end
386
+ end
387
+
388
+ i += 1
389
+ end
390
+
391
+ # 본문 파싱 (parse_body 옵션이 활성화된 경우)
392
+ if @parse_body && @body_parser && body_start < body_end
393
+ method_info[:body_ir] = @body_parser.parse(@lines, body_start, body_end)
394
+ method_info[:body_range] = { start: body_start, end: body_end }
395
+ end
396
+
397
+ [method_info, i]
398
+ end
399
+
400
+ # 본문 IR에서 인스턴스 변수 추출
401
+ def extract_instance_vars_from_body(body_ir, instance_vars)
402
+ return unless body_ir.is_a?(IR::Block)
403
+
404
+ body_ir.statements.each do |stmt|
405
+ case stmt
406
+ when IR::Assignment
407
+ if stmt.target.start_with?("@") && !stmt.target.start_with?("@@")
408
+ ivar_name = stmt.target[1..] # @ 제거
409
+ unless instance_vars.any? { |iv| iv[:name] == ivar_name }
410
+ instance_vars << { name: ivar_name }
411
+ end
412
+ end
413
+ when IR::Block
414
+ extract_instance_vars_from_body(stmt, instance_vars)
415
+ end
416
+ end
417
+ end
418
+
234
419
  def parse_interface(start_index)
235
420
  line = @lines[start_index]
236
421
  match = line.match(/^\s*interface\s+([\w:]+)/)
@@ -1,38 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRuby
4
- # Represents a type checking error
5
- class TypeCheckError
6
- attr_reader :message, :location, :expected, :actual, :suggestion, :severity
4
+ # Represents a type checking error (can be raised as an exception)
5
+ class TypeCheckError < StandardError
6
+ attr_reader :error_message, :location, :expected, :actual, :suggestion, :severity
7
7
 
8
8
  def initialize(message:, location: nil, expected: nil, actual: nil, suggestion: nil, severity: :error)
9
- @message = message
9
+ @error_message = message
10
10
  @location = location
11
11
  @expected = expected
12
12
  @actual = actual
13
13
  @suggestion = suggestion
14
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")
15
+ super(build_full_message)
24
16
  end
25
17
 
26
18
  def to_diagnostic
27
19
  {
28
20
  severity: @severity,
29
- message: @message,
21
+ message: @error_message,
30
22
  location: @location,
31
23
  expected: @expected,
32
24
  actual: @actual,
33
25
  suggestion: @suggestion,
34
26
  }
35
27
  end
28
+
29
+ private
30
+
31
+ def build_full_message
32
+ parts = [@error_message]
33
+ parts << " Expected: #{@expected}" if @expected
34
+ parts << " Actual: #{@actual}" if @actual
35
+ parts << " Suggestion: #{@suggestion}" if @suggestion
36
+ parts << " at #{@location}" if @location
37
+ parts.join("\n")
38
+ end
36
39
  end
37
40
 
38
41
  # Type hierarchy for subtype checking