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.
- checksums.yaml +4 -4
- data/LICENSE +21 -17
- data/README.md +3 -4
- data/lib/t_ruby/ast_type_inferrer.rb +511 -0
- data/lib/t_ruby/body_parser.rb +561 -0
- data/lib/t_ruby/cli.rb +3 -0
- data/lib/t_ruby/compiler.rb +152 -3
- data/lib/t_ruby/config.rb +7 -0
- data/lib/t_ruby/ir.rb +90 -6
- data/lib/t_ruby/parser.rb +192 -7
- data/lib/t_ruby/type_checker.rb +17 -14
- data/lib/t_ruby/type_env.rb +127 -0
- data/lib/t_ruby/version.rb +1 -1
- data/lib/t_ruby/watcher.rb +26 -21
- data/lib/t_ruby.rb +3 -0
- metadata +5 -2
data/lib/t_ruby/compiler.rb
CHANGED
|
@@ -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
|
|
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(/^(
|
|
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
|
-
"#{
|
|
1037
|
+
"#{param.name}: #{type}"
|
|
992
1038
|
end.join(", ")
|
|
993
1039
|
|
|
994
|
-
|
|
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
|
|
46
|
-
if line.match?(/^\s*
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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:]+)/)
|
data/lib/t_ruby/type_checker.rb
CHANGED
|
@@ -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 :
|
|
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
|
-
@
|
|
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
|
-
|
|
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: @
|
|
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
|