t-ruby 0.0.39 → 0.0.41

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64bc7ea0ab44e83d6580d07227fee508ae3b5e53bc0e6b19b8164857eaf31376
4
- data.tar.gz: 01da8585df97030974563634d8212058a3d3325ca9bc05188ae12ccf1cdc6770
3
+ metadata.gz: 6a7c7a93e1498a150b541ea7f7838c08435a33ead72774b7e4c0b8ead951e9d3
4
+ data.tar.gz: 1fbfb9ea6b276e130ef04891f39d691e362d5352dc4f0209b00009105f13c3a0
5
5
  SHA512:
6
- metadata.gz: b0bf9cd9518368e5ef256dd1851285fbbb78404b16c837346ecb286bfde37d2b8f8f5738782629efb5507fb7419eaea197726ae85f1ccb9481c09abc26d80a7f
7
- data.tar.gz: 67de07897cec9ef86c5df0798c4f2d44e1d54da0d8bfe799386eb544c28a4c0d5595f17dcc0c0bbc4236a44c17e76f1283fc75a9cd4452cee40f34daa86ef609
6
+ metadata.gz: 82cbc96204194f51b65f484aeda25383e285ce902c325e269766e77871e9dccacc548111e864ba2bfb27ca48fc69c7e22c5a0b1eee8e7c25e4763417ffe2c8fe
7
+ data.tar.gz: ed5f8ad72344e624e8a95d1e9a48078e0f6679d914f978974e0117b75bbc11ce2fbe08ece39360b20ce777ef5c5c333f37376f216272c1e6aa983eb18585fc92
@@ -7,16 +7,17 @@ module TRuby
7
7
  # \p{L} matches any Unicode letter, \p{N} matches any Unicode number
8
8
  IDENTIFIER_CHAR = '[\p{L}\p{N}_]'
9
9
  METHOD_NAME_PATTERN = "#{IDENTIFIER_CHAR}+[?!]?".freeze
10
+ # Visibility modifiers for method definitions
11
+ VISIBILITY_PATTERN = '(?:(?:private|protected|public)\s+)?'
10
12
 
11
13
  class Compiler
12
- attr_reader :declaration_loader, :use_ir, :optimizer
14
+ attr_reader :declaration_loader, :optimizer
13
15
 
14
- def initialize(config = nil, use_ir: true, optimize: true)
16
+ def initialize(config = nil, optimize: true)
15
17
  @config = config || Config.new
16
- @use_ir = use_ir
17
18
  @optimize = optimize
18
19
  @declaration_loader = DeclarationLoader.new
19
- @optimizer = IR::Optimizer.new if use_ir && optimize
20
+ @optimizer = IR::Optimizer.new if optimize
20
21
  @type_inferrer = ASTTypeInferrer.new if type_check?
21
22
  setup_declaration_paths if @config
22
23
  end
@@ -42,16 +43,16 @@ module TRuby
42
43
  source = File.read(input_path)
43
44
 
44
45
  # Parse with IR support
45
- parser = Parser.new(source, use_combinator: @use_ir)
46
- parse_result = parser.parse
46
+ parser = Parser.new(source)
47
+ parser.parse
47
48
 
48
49
  # Run type checking if enabled
49
- if type_check? && @use_ir && parser.ir_program
50
+ if type_check? && parser.ir_program
50
51
  check_types(parser.ir_program, input_path)
51
52
  end
52
53
 
53
54
  # Transform source to Ruby code
54
- output = @use_ir ? transform_with_ir(source, parser) : transform_legacy(source, parse_result)
55
+ output = transform_with_ir(source, parser)
55
56
 
56
57
  # Compute output path (respects preserve_structure setting)
57
58
  output_path = compute_output_path(input_path, @config.ruby_dir, ".rb")
@@ -63,11 +64,7 @@ module TRuby
63
64
  if @config.compiler["generate_rbs"]
64
65
  rbs_path = compute_output_path(input_path, @config.rbs_dir, ".rbs")
65
66
  FileUtils.mkdir_p(File.dirname(rbs_path))
66
- if @use_ir && parser.ir_program
67
- generate_rbs_from_ir_to_path(rbs_path, parser.ir_program)
68
- else
69
- generate_rbs_file_to_path(rbs_path, parse_result)
70
- end
67
+ generate_rbs_from_ir_to_path(rbs_path, parser.ir_program)
71
68
  end
72
69
 
73
70
  # Generate .d.trb file if enabled in config (legacy support)
@@ -87,25 +84,17 @@ module TRuby
87
84
  def compile_string(source, options = {})
88
85
  generate_rbs = options.fetch(:rbs, true)
89
86
 
90
- parser = Parser.new(source, use_combinator: @use_ir)
91
- parse_result = parser.parse
87
+ parser = Parser.new(source)
88
+ parser.parse
92
89
 
93
90
  # Transform source to Ruby code
94
- ruby_output = @use_ir ? transform_with_ir(source, parser) : transform_legacy(source, parse_result)
91
+ ruby_output = transform_with_ir(source, parser)
95
92
 
96
93
  # Generate RBS if requested
97
94
  rbs_output = ""
98
- if generate_rbs
99
- if @use_ir && parser.ir_program
100
- generator = IR::RBSGenerator.new
101
- rbs_output = generator.generate(parser.ir_program)
102
- else
103
- generator = RBSGenerator.new
104
- rbs_output = generator.generate(
105
- parse_result[:functions] || [],
106
- parse_result[:type_aliases] || []
107
- )
108
- end
95
+ if generate_rbs && parser.ir_program
96
+ generator = IR::RBSGenerator.new
97
+ rbs_output = generator.generate(parser.ir_program)
109
98
  end
110
99
 
111
100
  {
@@ -134,7 +123,7 @@ module TRuby
134
123
  end
135
124
 
136
125
  source = File.read(input_path)
137
- parser = Parser.new(source, use_combinator: true)
126
+ parser = Parser.new(source)
138
127
  parser.parse
139
128
  parser.ir_program
140
129
  end
@@ -380,10 +369,10 @@ module TRuby
380
369
  @declaration_loader.add_search_path("./lib/types")
381
370
  end
382
371
 
383
- # Transform using IR system (new approach)
372
+ # Transform using IR system
384
373
  def transform_with_ir(source, parser)
385
374
  ir_program = parser.ir_program
386
- return transform_legacy(source, parser.parse) unless ir_program
375
+ return source unless ir_program
387
376
 
388
377
  # Run optimization passes if enabled
389
378
  if @optimize && @optimizer
@@ -396,33 +385,15 @@ module TRuby
396
385
  generator.generate_with_source(ir_program, source)
397
386
  end
398
387
 
399
- # Legacy transformation using TypeErasure (backward compatible)
400
- def transform_legacy(source, parse_result)
401
- if parse_result[:type] == :success
402
- eraser = TypeErasure.new(source)
403
- eraser.erase
404
- else
405
- source
406
- end
407
- end
408
-
409
388
  # Generate RBS from IR to a specific path
410
389
  def generate_rbs_from_ir_to_path(rbs_path, ir_program)
390
+ return unless ir_program
391
+
411
392
  generator = IR::RBSGenerator.new
412
393
  rbs_content = generator.generate(ir_program)
413
394
  File.write(rbs_path, rbs_content) unless rbs_content.strip.empty?
414
395
  end
415
396
 
416
- # Legacy RBS generation to a specific path
417
- def generate_rbs_file_to_path(rbs_path, parse_result)
418
- generator = RBSGenerator.new
419
- rbs_content = generator.generate(
420
- parse_result[:functions] || [],
421
- parse_result[:type_aliases] || []
422
- )
423
- File.write(rbs_path, rbs_content) unless rbs_content.empty?
424
- end
425
-
426
397
  def generate_dtrb_file(input_path, out_dir)
427
398
  dtrb_path = compute_output_path(input_path, out_dir, DeclarationGenerator::DECLARATION_EXTENSION)
428
399
  FileUtils.mkdir_p(File.dirname(dtrb_path))
@@ -511,7 +482,8 @@ module TRuby
511
482
  result = source.dup
512
483
 
513
484
  # Match function definitions and remove type annotations from parameters
514
- result.gsub!(/^(\s*def\s+#{TRuby::METHOD_NAME_PATTERN}\s*\()([^)]+)(\)\s*)(?::\s*[^\n]+)?(\s*$)/) do |_match|
485
+ # Also supports visibility modifiers: private def, protected def, public def
486
+ result.gsub!(/^(\s*#{TRuby::VISIBILITY_PATTERN}def\s+#{TRuby::METHOD_NAME_PATTERN}\s*\()([^)]+)(\)\s*)(?::\s*[^\n]+)?(\s*$)/) do |_match|
515
487
  indent = ::Regexp.last_match(1)
516
488
  params = ::Regexp.last_match(2)
517
489
  close_paren = ::Regexp.last_match(3)
@@ -533,6 +505,7 @@ module TRuby
533
505
  params = []
534
506
  current = ""
535
507
  depth = 0
508
+ brace_depth = 0
536
509
 
537
510
  params_str.each_char do |char|
538
511
  case char
@@ -542,9 +515,16 @@ module TRuby
542
515
  when ">", "]", ")"
543
516
  depth -= 1
544
517
  current += char
518
+ when "{"
519
+ brace_depth += 1
520
+ current += char
521
+ when "}"
522
+ brace_depth -= 1
523
+ current += char
545
524
  when ","
546
- if depth.zero?
547
- params << clean_param(current.strip)
525
+ if depth.zero? && brace_depth.zero?
526
+ cleaned = clean_param(current.strip)
527
+ params.concat(Array(cleaned)) if cleaned
548
528
  current = ""
549
529
  else
550
530
  current += char
@@ -554,37 +534,128 @@ module TRuby
554
534
  end
555
535
  end
556
536
 
557
- params << clean_param(current.strip) unless current.empty?
537
+ cleaned = clean_param(current.strip) unless current.empty?
538
+ params.concat(Array(cleaned)) if cleaned
558
539
  params.join(", ")
559
540
  end
560
541
 
561
- # Clean a single parameter (remove type annotation)
542
+ # Clean a single parameter (remove type annotation, preserve default value)
543
+ # Returns String or Array of Strings (for keyword args group)
562
544
  def clean_param(param)
563
- # Match: name: Type or name (supports Unicode identifiers)
564
- if (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:/))
545
+ param = param.strip
546
+ return nil if param.empty?
547
+
548
+ # 1. 더블 스플랫: **name: Type -> **name
549
+ if param.start_with?("**")
550
+ match = param.match(/^\*\*(\w+)(?::\s*.+)?$/)
551
+ return "**#{match[1]}" if match
552
+
553
+ return param
554
+ end
555
+
556
+ # 2. 키워드 인자 그룹: { ... } 또는 { ... }: InterfaceName
557
+ if param.start_with?("{")
558
+ return clean_keyword_args_group(param)
559
+ end
560
+
561
+ # 3. Hash 리터럴: name: { ... } -> name
562
+ if param.match?(/^\w+:\s*\{/)
563
+ match = param.match(/^(\w+):\s*\{.+\}(?::\s*\w+)?$/)
564
+ return match[1] if match
565
+
566
+ return param
567
+ end
568
+
569
+ # 4. 일반 파라미터: name: Type = value -> name = value 또는 name: Type -> name
570
+ # Match: name: Type = value (with default value)
571
+ if (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:\s*.+?\s*(=\s*.+)$/))
572
+ "#{match[1]} #{match[2]}"
573
+ # Match: name: Type (without default value)
574
+ elsif (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:/))
565
575
  match[1]
566
576
  else
567
577
  param
568
578
  end
569
579
  end
570
580
 
581
+ # 키워드 인자 그룹을 Ruby 키워드 인자로 변환
582
+ # { name: String, age: Integer = 0 } -> name:, age: 0
583
+ # { name:, age: 0 }: UserParams -> name:, age: 0
584
+ def clean_keyword_args_group(param)
585
+ # { ... }: InterfaceName 또는 { ... } 형태 파싱
586
+ interface_match = param.match(/^\{(.+)\}\s*:\s*\w+\s*$/)
587
+ inline_match = param.match(/^\{(.+)\}\s*$/) unless interface_match
588
+
589
+ inner_content = if interface_match
590
+ interface_match[1]
591
+ elsif inline_match
592
+ inline_match[1]
593
+ else
594
+ return param
595
+ end
596
+
597
+ # 내부 파라미터 분리
598
+ parts = split_nested_content(inner_content)
599
+ keyword_params = []
600
+
601
+ parts.each do |part|
602
+ part = part.strip
603
+ next if part.empty?
604
+
605
+ if interface_match
606
+ # interface 참조: name: default_value 또는 name:
607
+ if (match = part.match(/^(\w+):\s*(.*)$/))
608
+ name = match[1]
609
+ default_value = match[2].strip
610
+ keyword_params << if default_value.empty?
611
+ "#{name}:"
612
+ else
613
+ "#{name}: #{default_value}"
614
+ end
615
+ end
616
+ elsif (match = part.match(/^(\w+):\s*(.+)$/))
617
+ # 인라인 타입: name: Type = default 또는 name: Type
618
+ name = match[1]
619
+ type_and_default = match[2].strip
620
+
621
+ # Type = default 분리
622
+ default_value = extract_default_value(type_and_default)
623
+ keyword_params << if default_value
624
+ "#{name}: #{default_value}"
625
+ else
626
+ "#{name}:"
627
+ end
628
+ end
629
+ end
630
+
631
+ keyword_params
632
+ end
633
+
634
+ # 중첩된 내용을 콤마로 분리
635
+ def split_nested_content(content)
636
+ StringUtils.split_by_comma(content)
637
+ end
638
+
639
+ # 타입과 기본값에서 기본값만 추출
640
+ def extract_default_value(type_and_default)
641
+ StringUtils.extract_default_value(type_and_default)
642
+ end
643
+
571
644
  # Erase return type annotations
572
645
  def erase_return_types(source)
573
646
  result = source.dup
574
647
 
575
- # Remove return type: ): Type or ): Type<Foo> etc.
648
+ # Remove return type after parentheses: ): Type or ): Type<Foo> etc.
576
649
  result.gsub!(/\)\s*:\s*[^\n]+?(?=\s*$)/m) do |_match|
577
650
  ")"
578
651
  end
579
652
 
580
- result
581
- end
582
- end
653
+ # Remove return type for methods without parentheses: def method_name: Type
654
+ result.gsub!(/^(\s*#{TRuby::VISIBILITY_PATTERN}def\s+#{TRuby::METHOD_NAME_PATTERN})\s*:\s*[^\n]+?(?=\s*$)/m) do |_match|
655
+ ::Regexp.last_match(1)
656
+ end
583
657
 
584
- # Legacy Compiler for backward compatibility (no IR)
585
- class LegacyCompiler < Compiler
586
- def initialize(config)
587
- super(config, use_ir: false, optimize: false)
658
+ result
588
659
  end
589
660
  end
590
661
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ # Detects regions that should be skipped during parsing:
5
+ # - Heredoc content
6
+ # - Block comments (=begin/=end)
7
+ class HeredocDetector
8
+ # Heredoc start patterns:
9
+ # <<IDENTIFIER, <<-IDENTIFIER, <<~IDENTIFIER
10
+ # <<'IDENTIFIER', <<"IDENTIFIER"
11
+ HEREDOC_START_PATTERN = /<<([~-])?(['"]?)(\w+)\2/
12
+
13
+ # Detect all skippable ranges in lines (heredocs and block comments)
14
+ # @param lines [Array<String>] source lines
15
+ # @return [Array<Range>] content ranges to skip (0-indexed)
16
+ def self.detect(lines)
17
+ ranges = []
18
+ i = 0
19
+
20
+ while i < lines.length
21
+ line = lines[i]
22
+
23
+ # Check for =begin block comment
24
+ if line.strip == "=begin"
25
+ start_line = i
26
+ i += 1
27
+
28
+ # Find =end
29
+ while i < lines.length
30
+ break if lines[i].strip == "=end"
31
+
32
+ i += 1
33
+ end
34
+
35
+ # Range covers from =begin to =end (inclusive)
36
+ ranges << (start_line..i) if i < lines.length
37
+ # Check for heredoc
38
+ elsif (match = line.match(HEREDOC_START_PATTERN))
39
+ delimiter = match[3]
40
+ squiggly = match[1] == "~"
41
+ start_line = i
42
+ i += 1
43
+
44
+ # Find closing delimiter
45
+ while i < lines.length
46
+ # For squiggly heredoc or dash heredoc, delimiter can be indented
47
+ # For regular heredoc, delimiter must be at line start
48
+ if squiggly || match[1] == "-"
49
+ break if lines[i].strip == delimiter
50
+ elsif lines[i].chomp == delimiter
51
+ break
52
+ end
53
+ i += 1
54
+ end
55
+
56
+ # Range covers content lines (after start, up to and including end delimiter)
57
+ ranges << ((start_line + 1)..i) if i < lines.length
58
+ end
59
+
60
+ i += 1
61
+ end
62
+
63
+ ranges
64
+ end
65
+
66
+ # Check if a line index is inside any skippable region
67
+ # @param line_index [Integer] line index to check
68
+ # @param heredoc_ranges [Array<Range>] ranges from detect()
69
+ # @return [Boolean]
70
+ def self.inside_heredoc?(line_index, heredoc_ranges)
71
+ heredoc_ranges.any? { |range| range.include?(line_index) }
72
+ end
73
+ end
74
+ end
data/lib/t_ruby/ir.rb CHANGED
@@ -147,15 +147,19 @@ module TRuby
147
147
 
148
148
  # Method parameter
149
149
  class Parameter < Node
150
- attr_accessor :name, :type_annotation, :default_value, :kind
150
+ attr_accessor :name, :type_annotation, :default_value, :kind, :interface_ref
151
151
 
152
- # kind: :required, :optional, :rest, :keyrest, :block
153
- def initialize(name:, type_annotation: nil, default_value: nil, kind: :required, **opts)
152
+ # kind: :required, :optional, :rest, :keyrest, :block, :keyword
153
+ # :keyword - 키워드 인자 (구조분해): { name: String } → def foo(name:)
154
+ # :keyrest - 더블 스플랫: **opts: Type → def foo(**opts)
155
+ # interface_ref - interface 참조 타입 (예: }: UserParams 부분)
156
+ def initialize(name:, type_annotation: nil, default_value: nil, kind: :required, interface_ref: nil, **opts)
154
157
  super(**opts)
155
158
  @name = name
156
159
  @type_annotation = type_annotation
157
160
  @default_value = default_value
158
161
  @kind = kind
162
+ @interface_ref = interface_ref
159
163
  end
160
164
  end
161
165
 
@@ -778,7 +782,8 @@ module TRuby
778
782
  name: info[:name],
779
783
  params: params,
780
784
  return_type: info[:return_type] ? parse_type(info[:return_type]) : nil,
781
- body: body
785
+ body: body,
786
+ visibility: info[:visibility] || :public
782
787
  )
783
788
  end
784
789
 
@@ -1051,7 +1056,8 @@ module TRuby
1051
1056
  end
1052
1057
 
1053
1058
  return_type ||= "untyped"
1054
- emit("def #{node.name}: (#{params}) -> #{return_type}")
1059
+ visibility_prefix = format_visibility(node.visibility)
1060
+ emit("#{visibility_prefix}def #{node.name}: (#{params}) -> #{return_type}")
1055
1061
  end
1056
1062
 
1057
1063
  def visit_class_decl(node)
@@ -1092,6 +1098,17 @@ module TRuby
1092
1098
  def emit(text)
1093
1099
  @output << ((" " * @indent) + text)
1094
1100
  end
1101
+
1102
+ def format_visibility(visibility)
1103
+ # RBS only supports private visibility, not protected
1104
+ # See: https://github.com/ruby/rbs/issues/579
1105
+ case visibility
1106
+ when :private
1107
+ "private "
1108
+ else
1109
+ ""
1110
+ end
1111
+ end
1095
1112
  end
1096
1113
 
1097
1114
  #==========================================================================
@@ -787,7 +787,7 @@ module TRuby
787
787
  lines = text.split("\n")
788
788
 
789
789
  # Parse the document to get IR
790
- parser = Parser.new(text, use_combinator: true)
790
+ parser = Parser.new(text)
791
791
  parse_result = parser.parse
792
792
  parser.ir_program
793
793
 
data/lib/t_ruby/parser.rb CHANGED
@@ -12,15 +12,16 @@ module TRuby
12
12
  IDENTIFIER_CHAR = '[\p{L}\p{N}_]'
13
13
  # Method names can end with ? or !
14
14
  METHOD_NAME_PATTERN = "#{IDENTIFIER_CHAR}+[?!]?".freeze
15
+ # Visibility modifiers for method definitions
16
+ VISIBILITY_PATTERN = '(?:(?:private|protected|public)\s+)?'
15
17
 
16
- attr_reader :source, :ir_program, :use_combinator
18
+ attr_reader :source, :ir_program
17
19
 
18
- def initialize(source, use_combinator: true, parse_body: true)
20
+ def initialize(source, parse_body: true)
19
21
  @source = source
20
22
  @lines = source.split("\n")
21
- @use_combinator = use_combinator
22
23
  @parse_body = parse_body
23
- @type_parser = ParserCombinator::TypeParser.new if use_combinator
24
+ @type_parser = ParserCombinator::TypeParser.new
24
25
  @body_parser = BodyParser.new if parse_body
25
26
  @ir_program = nil
26
27
  end
@@ -32,7 +33,16 @@ module TRuby
32
33
  classes = []
33
34
  i = 0
34
35
 
36
+ # Pre-detect heredoc regions to skip
37
+ heredoc_ranges = HeredocDetector.detect(@lines)
38
+
35
39
  while i < @lines.length
40
+ # Skip lines inside heredoc content
41
+ if HeredocDetector.inside_heredoc?(i, heredoc_ranges)
42
+ i += 1
43
+ next
44
+ end
45
+
36
46
  line = @lines[i]
37
47
 
38
48
  # Match type alias definitions
@@ -62,7 +72,7 @@ module TRuby
62
72
  end
63
73
 
64
74
  # Match function definitions (top-level only, not inside class)
65
- if line.match?(/^\s*def\s+#{IDENTIFIER_CHAR}+/)
75
+ if line.match?(/^\s*#{VISIBILITY_PATTERN}def\s+#{IDENTIFIER_CHAR}+/)
66
76
  func_info, next_i = parse_function_with_body(i)
67
77
  if func_info
68
78
  functions << func_info
@@ -82,11 +92,9 @@ module TRuby
82
92
  classes: classes,
83
93
  }
84
94
 
85
- # Build IR if combinator is enabled
86
- if @use_combinator
87
- builder = IR::Builder.new
88
- @ir_program = builder.build(result, source: @source)
89
- end
95
+ # Build IR
96
+ builder = IR::Builder.new
97
+ @ir_program = builder.build(result, source: @source)
90
98
 
91
99
  result
92
100
  end
@@ -97,10 +105,8 @@ module TRuby
97
105
  @ir_program
98
106
  end
99
107
 
100
- # Parse a type expression using combinator (new API)
108
+ # Parse a type expression using combinator
101
109
  def parse_type(type_string)
102
- return nil unless @use_combinator
103
-
104
110
  result = @type_parser.parse(type_string)
105
111
  result[:success] ? result[:type] : nil
106
112
  end
@@ -149,16 +155,14 @@ module TRuby
149
155
  alias_name = match[1]
150
156
  definition = match[2].strip
151
157
 
152
- # Use combinator for complex type parsing if available
153
- if @use_combinator
154
- type_result = @type_parser.parse(definition)
155
- if type_result[:success]
156
- return {
157
- name: alias_name,
158
- definition: definition,
159
- ir_type: type_result[:type],
160
- }
161
- end
158
+ # Use combinator for complex type parsing
159
+ type_result = @type_parser.parse(definition)
160
+ if type_result[:success]
161
+ return {
162
+ name: alias_name,
163
+ definition: definition,
164
+ ir_type: type_result[:type],
165
+ }
162
166
  end
163
167
 
164
168
  {
@@ -173,12 +177,14 @@ module TRuby
173
177
  # def foo(): Type - no params but with return type
174
178
  # def foo(params) - with params, no return type
175
179
  # def foo - no params, no return type
176
- match = line.match(/^\s*def\s+(#{METHOD_NAME_PATTERN})\s*(?:\((.*?)\))?\s*(?::\s*(.+?))?\s*$/)
180
+ # Also supports visibility modifiers: private def, protected def, public def
181
+ match = line.match(/^\s*(?:(private|protected|public)\s+)?def\s+(#{METHOD_NAME_PATTERN})\s*(?:\((.*?)\))?\s*(?::\s*(.+?))?\s*$/)
177
182
  return nil unless match
178
183
 
179
- function_name = match[1]
180
- params_str = match[2] || ""
181
- return_type_str = match[3]&.strip
184
+ visibility = match[1] ? match[1].to_sym : :public
185
+ function_name = match[2]
186
+ params_str = match[3] || ""
187
+ return_type_str = match[4]&.strip
182
188
 
183
189
  # Validate return type if present
184
190
  if return_type_str
@@ -191,10 +197,11 @@ module TRuby
191
197
  name: function_name,
192
198
  params: params,
193
199
  return_type: return_type_str,
200
+ visibility: visibility,
194
201
  }
195
202
 
196
- # Parse return type with combinator if available
197
- if @use_combinator && return_type_str
203
+ # Parse return type with combinator
204
+ if return_type_str
198
205
  type_result = @type_parser.parse(return_type_str)
199
206
  result[:ir_return_type] = type_result[:type] if type_result[:success]
200
207
  end
@@ -237,18 +244,36 @@ module TRuby
237
244
  param_list = split_params(params_str)
238
245
 
239
246
  param_list.each do |param|
240
- param_info = parse_single_parameter(param)
241
- parameters << param_info if param_info
247
+ param = param.strip
248
+
249
+ # 1. 더블 스플랫: **name: Type
250
+ if param.start_with?("**")
251
+ param_info = parse_double_splat_parameter(param)
252
+ parameters << param_info if param_info
253
+ # 2. 키워드 인자 그룹: { ... } 또는 { ... }: InterfaceName
254
+ elsif param.start_with?("{")
255
+ keyword_params = parse_keyword_args_group(param)
256
+ parameters.concat(keyword_params) if keyword_params
257
+ # 3. Hash 리터럴: name: { ... }
258
+ elsif param.match?(/^\w+:\s*\{/)
259
+ param_info = parse_hash_literal_parameter(param)
260
+ parameters << param_info if param_info
261
+ # 4. 일반 위치 인자: name: Type 또는 name: Type = default
262
+ else
263
+ param_info = parse_single_parameter(param)
264
+ parameters << param_info if param_info
265
+ end
242
266
  end
243
267
 
244
268
  parameters
245
269
  end
246
270
 
247
271
  def split_params(params_str)
248
- # Handle nested generics like Array<Map<String, Int>>
272
+ # Handle nested generics, braces, brackets
249
273
  result = []
250
274
  current = ""
251
275
  depth = 0
276
+ brace_depth = 0
252
277
 
253
278
  params_str.each_char do |char|
254
279
  case char
@@ -258,8 +283,14 @@ module TRuby
258
283
  when ">", "]", ")"
259
284
  depth -= 1
260
285
  current += char
286
+ when "{"
287
+ brace_depth += 1
288
+ current += char
289
+ when "}"
290
+ brace_depth -= 1
291
+ current += char
261
292
  when ","
262
- if depth.zero?
293
+ if depth.zero? && brace_depth.zero?
263
294
  result << current.strip
264
295
  current = ""
265
296
  else
@@ -274,8 +305,10 @@ module TRuby
274
305
  result
275
306
  end
276
307
 
277
- def parse_single_parameter(param)
278
- match = param.match(/^(\w+)(?::\s*(.+?))?$/)
308
+ # 더블 스플랫 파라미터 파싱: **opts: Type
309
+ def parse_double_splat_parameter(param)
310
+ # **name: Type
311
+ match = param.match(/^\*\*(\w+)(?::\s*(.+?))?$/)
279
312
  return nil unless match
280
313
 
281
314
  param_name = match[1]
@@ -284,10 +317,159 @@ module TRuby
284
317
  result = {
285
318
  name: param_name,
286
319
  type: type_str,
320
+ kind: :keyrest,
321
+ }
322
+
323
+ if type_str
324
+ type_result = @type_parser.parse(type_str)
325
+ result[:ir_type] = type_result[:type] if type_result[:success]
326
+ end
327
+
328
+ result
329
+ end
330
+
331
+ # 키워드 인자 그룹 파싱: { name: String, age: Integer = 0 } 또는 { name:, age: 0 }: InterfaceName
332
+ def parse_keyword_args_group(param)
333
+ # { ... }: InterfaceName 형태 확인
334
+ # 또는 { ... } 만 있는 형태 (인라인 타입)
335
+ interface_match = param.match(/^\{(.+)\}\s*:\s*(\w+)\s*$/)
336
+ inline_match = param.match(/^\{(.+)\}\s*$/) unless interface_match
337
+
338
+ if interface_match
339
+ inner_content = interface_match[1]
340
+ interface_name = interface_match[2]
341
+ parse_keyword_args_with_interface(inner_content, interface_name)
342
+ elsif inline_match
343
+ inner_content = inline_match[1]
344
+ parse_keyword_args_inline(inner_content)
345
+ end
346
+ end
347
+
348
+ # interface 참조 키워드 인자 파싱: { name:, age: 0 }: UserParams
349
+ def parse_keyword_args_with_interface(inner_content, interface_name)
350
+ parameters = []
351
+ parts = split_keyword_args(inner_content)
352
+
353
+ parts.each do |part|
354
+ part = part.strip
355
+ next if part.empty?
356
+
357
+ # name: default_value 또는 name: 형태
358
+ next unless part.match?(/^(\w+):\s*(.*)$/)
359
+
360
+ match = part.match(/^(\w+):\s*(.*)$/)
361
+ param_name = match[1]
362
+ default_value = match[2].strip
363
+ default_value = nil if default_value.empty?
364
+
365
+ parameters << {
366
+ name: param_name,
367
+ type: nil, # interface에서 타입을 가져옴
368
+ default_value: default_value,
369
+ kind: :keyword,
370
+ interface_ref: interface_name,
371
+ }
372
+ end
373
+
374
+ parameters
375
+ end
376
+
377
+ # 인라인 타입 키워드 인자 파싱: { name: String, age: Integer = 0 }
378
+ def parse_keyword_args_inline(inner_content)
379
+ parameters = []
380
+ parts = split_keyword_args(inner_content)
381
+
382
+ parts.each do |part|
383
+ part = part.strip
384
+ next if part.empty?
385
+
386
+ # name: Type = default 또는 name: Type 형태
387
+ next unless part.match?(/^(\w+):\s*(.+)$/)
388
+
389
+ match = part.match(/^(\w+):\s*(.+)$/)
390
+ param_name = match[1]
391
+ type_and_default = match[2].strip
392
+
393
+ # Type = default 분리
394
+ type_str, default_value = split_type_and_default(type_and_default)
395
+
396
+ result = {
397
+ name: param_name,
398
+ type: type_str,
399
+ default_value: default_value,
400
+ kind: :keyword,
401
+ }
402
+
403
+ if type_str
404
+ type_result = @type_parser.parse(type_str)
405
+ result[:ir_type] = type_result[:type] if type_result[:success]
406
+ end
407
+
408
+ parameters << result
409
+ end
410
+
411
+ parameters
412
+ end
413
+
414
+ # 키워드 인자 내부를 콤마로 분리 (중첩된 제네릭/배열/해시 고려)
415
+ def split_keyword_args(content)
416
+ StringUtils.split_by_comma(content)
417
+ end
418
+
419
+ # 타입과 기본값 분리: "String = 0" -> ["String", "0"]
420
+ def split_type_and_default(type_and_default)
421
+ StringUtils.split_type_and_default(type_and_default)
422
+ end
423
+
424
+ # Hash 리터럴 파라미터 파싱: config: { host: String, port: Integer }
425
+ def parse_hash_literal_parameter(param)
426
+ # name: { ... } 또는 name: { ... }: InterfaceName
427
+ match = param.match(/^(\w+):\s*(\{.+\})(?::\s*(\w+))?$/)
428
+ return nil unless match
429
+
430
+ param_name = match[1]
431
+ hash_type = match[2]
432
+ interface_name = match[3]
433
+
434
+ result = {
435
+ name: param_name,
436
+ type: interface_name || hash_type,
437
+ kind: :required,
438
+ hash_type_def: hash_type, # 원본 해시 타입 정의 저장
439
+ }
440
+
441
+ result[:interface_ref] = interface_name if interface_name
442
+
443
+ result
444
+ end
445
+
446
+ def parse_single_parameter(param)
447
+ # name: Type = default 또는 name: Type 또는 name
448
+ # 기본값이 있는 경우 먼저 처리
449
+ type_str = nil
450
+ default_value = nil
451
+
452
+ if param.include?(":")
453
+ match = param.match(/^(\w+):\s*(.+)$/)
454
+ return nil unless match
455
+
456
+ param_name = match[1]
457
+ type_and_default = match[2].strip
458
+ type_str, default_value = split_type_and_default(type_and_default)
459
+ else
460
+ # 타입 없이 이름만 있는 경우
461
+ param_name = param.strip
462
+ end
463
+
464
+ result = {
465
+ name: param_name,
466
+ type: type_str,
467
+ default_value: default_value,
468
+ kind: default_value ? :optional : :required,
287
469
  }
288
470
 
289
- # Parse type with combinator if available
290
- if @use_combinator && type_str
471
+ # Parse type with combinator
472
+ if type_str
291
473
  type_result = @type_parser.parse(type_str)
292
474
  result[:ir_type] = type_result[:type] if type_result[:success]
293
475
  end
@@ -326,7 +508,7 @@ module TRuby
326
508
  current_line = @lines[i]
327
509
 
328
510
  # Match method definitions inside class
329
- if current_line.match?(/^\s*def\s+#{IDENTIFIER_CHAR}+/)
511
+ if current_line.match?(/^\s*#{VISIBILITY_PATTERN}def\s+#{IDENTIFIER_CHAR}+/)
330
512
  method_info, next_i = parse_method_in_class(i, class_end)
331
513
  if method_info
332
514
  methods << method_info
@@ -438,10 +620,8 @@ module TRuby
438
620
  }
439
621
 
440
622
  # Parse member type with combinator
441
- if @use_combinator
442
- type_result = @type_parser.parse(member[:type])
443
- member[:ir_type] = type_result[:type] if type_result[:success]
444
- end
623
+ type_result = @type_parser.parse(member[:type])
624
+ member[:ir_type] = type_result[:type] if type_result[:success]
445
625
 
446
626
  members << member
447
627
  end
@@ -453,11 +633,4 @@ module TRuby
453
633
  [{ name: interface_name, members: members }, i]
454
634
  end
455
635
  end
456
-
457
- # Legacy Parser for backward compatibility (regex-only)
458
- class LegacyParser < Parser
459
- def initialize(source)
460
- super(source, use_combinator: false)
461
- end
462
- end
463
636
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ # 문자열 파싱을 위한 공통 유틸리티 모듈
5
+ # 파서와 컴파일러에서 공유하는 중첩 괄호 처리 로직
6
+ module StringUtils
7
+ module_function
8
+
9
+ # 중첩된 괄호를 고려하여 콤마로 문자열 분리
10
+ # @param content [String] 분리할 문자열
11
+ # @return [Array<String>] 분리된 문자열 배열
12
+ def split_by_comma(content)
13
+ result = []
14
+ current = ""
15
+ depth = 0
16
+
17
+ content.each_char do |char|
18
+ case char
19
+ when "<", "[", "(", "{"
20
+ depth += 1
21
+ current += char
22
+ when ">", "]", ")", "}"
23
+ depth -= 1
24
+ current += char
25
+ when ","
26
+ if depth.zero?
27
+ result << current.strip
28
+ current = ""
29
+ else
30
+ current += char
31
+ end
32
+ else
33
+ current += char
34
+ end
35
+ end
36
+
37
+ result << current.strip unless current.empty?
38
+ result
39
+ end
40
+
41
+ # 타입과 기본값 분리: "String = 0" -> ["String", "0"]
42
+ # 중첩된 괄호 내부의 = 는 무시
43
+ # @param type_and_default [String] "Type = default" 형태의 문자열
44
+ # @return [Array] [type_str, default_value] 또는 [type_str, nil]
45
+ def split_type_and_default(type_and_default)
46
+ depth = 0
47
+ equals_pos = nil
48
+
49
+ type_and_default.each_char.with_index do |char, i|
50
+ case char
51
+ when "<", "[", "(", "{"
52
+ depth += 1
53
+ when ">", "]", ")", "}"
54
+ depth -= 1
55
+ when "="
56
+ if depth.zero?
57
+ equals_pos = i
58
+ break
59
+ end
60
+ end
61
+ end
62
+
63
+ if equals_pos
64
+ type_str = type_and_default[0...equals_pos].strip
65
+ default_value = type_and_default[(equals_pos + 1)..].strip
66
+ [type_str, default_value]
67
+ else
68
+ [type_and_default, nil]
69
+ end
70
+ end
71
+
72
+ # 기본값만 추출 (타입은 버림)
73
+ # @param type_and_default [String] "Type = default" 형태의 문자열
74
+ # @return [String, nil] 기본값 또는 nil
75
+ def extract_default_value(type_and_default)
76
+ _, default_value = split_type_and_default(type_and_default)
77
+ default_value
78
+ end
79
+ end
80
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRuby
4
- VERSION = "0.0.39"
4
+ VERSION = "0.0.41"
5
5
  end
data/lib/t_ruby.rb CHANGED
@@ -5,12 +5,14 @@ require_relative "t_ruby/version_checker"
5
5
  require_relative "t_ruby/config"
6
6
 
7
7
  # Core infrastructure (must be loaded first)
8
+ require_relative "t_ruby/string_utils"
8
9
  require_relative "t_ruby/ir"
9
10
  require_relative "t_ruby/parser_combinator"
10
11
  require_relative "t_ruby/smt_solver"
11
12
 
12
13
  # Basic components
13
14
  require_relative "t_ruby/type_alias_registry"
15
+ require_relative "t_ruby/heredoc_detector"
14
16
  require_relative "t_ruby/body_parser"
15
17
  require_relative "t_ruby/parser"
16
18
  require_relative "t_ruby/union_type_parser"
@@ -18,7 +20,6 @@ require_relative "t_ruby/generic_type_parser"
18
20
  require_relative "t_ruby/intersection_type_parser"
19
21
  require_relative "t_ruby/type_erasure"
20
22
  require_relative "t_ruby/error_handler"
21
- require_relative "t_ruby/rbs_generator"
22
23
  require_relative "t_ruby/declaration_generator"
23
24
  require_relative "t_ruby/compiler"
24
25
  require_relative "t_ruby/lsp_server"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: t-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.39
4
+ version: 0.0.41
5
5
  platform: ruby
6
6
  authors:
7
7
  - Y. Fred Kim
@@ -52,15 +52,16 @@ files:
52
52
  - lib/t_ruby/docs_example_verifier.rb
53
53
  - lib/t_ruby/error_handler.rb
54
54
  - lib/t_ruby/generic_type_parser.rb
55
+ - lib/t_ruby/heredoc_detector.rb
55
56
  - lib/t_ruby/intersection_type_parser.rb
56
57
  - lib/t_ruby/ir.rb
57
58
  - lib/t_ruby/lsp_server.rb
58
59
  - lib/t_ruby/package_manager.rb
59
60
  - lib/t_ruby/parser.rb
60
61
  - lib/t_ruby/parser_combinator.rb
61
- - lib/t_ruby/rbs_generator.rb
62
62
  - lib/t_ruby/runtime_validator.rb
63
63
  - lib/t_ruby/smt_solver.rb
64
+ - lib/t_ruby/string_utils.rb
64
65
  - lib/t_ruby/type_alias_registry.rb
65
66
  - lib/t_ruby/type_checker.rb
66
67
  - lib/t_ruby/type_env.rb
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TRuby
4
- class RBSGenerator
5
- def initialize
6
- # RBS generation configuration
7
- end
8
-
9
- def generate(functions, type_aliases)
10
- # Add type aliases
11
- lines = type_aliases.map do |type_alias|
12
- generate_type_alias(type_alias)
13
- end
14
-
15
- lines << "" if type_aliases.any? && functions.any?
16
-
17
- # Add function signatures
18
- functions.each do |func|
19
- lines << generate_function_signature(func)
20
- end
21
-
22
- lines.compact.join("\n")
23
- end
24
-
25
- def generate_type_aliases(aliases)
26
- aliases.map { |alias_def| generate_type_alias(alias_def) }.join("\n")
27
- end
28
-
29
- def generate_type_alias(type_alias)
30
- name = type_alias[:name]
31
- definition = type_alias[:definition]
32
-
33
- "type #{name} = #{definition}"
34
- end
35
-
36
- def generate_function_signature(func)
37
- name = func[:name]
38
- params = func[:params] || []
39
- return_type = func[:return_type]
40
-
41
- param_str = format_parameters(params)
42
- return_str = format_return_type(return_type)
43
-
44
- "def #{name}: (#{param_str}) -> #{return_str}"
45
- end
46
-
47
- private
48
-
49
- def format_parameters(params)
50
- return if params.empty?
51
-
52
- param_strs = params.map do |param|
53
- param_name = param[:name]
54
- param_type = param[:type] || "Object"
55
-
56
- "#{param_name}: #{param_type}"
57
- end
58
-
59
- param_strs.join(", ")
60
- end
61
-
62
- def format_return_type(return_type)
63
- return "void" if return_type == "void"
64
- return "nil" if return_type.nil?
65
-
66
- return_type
67
- end
68
- end
69
- end