t-ruby 0.0.40 → 0.0.42

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: 54f0b9008d5a42a8cbf851eac8a0eb540670c3c87018dd87182baf6ab30c7d55
4
- data.tar.gz: 42929dd91ae55ea84e75fd6978298deea2fec1d3517a801bde06af6f70865557
3
+ metadata.gz: 75019794a577853cf5a7e704febef3868a1ddd06776e43f50dc684d7dde87e3c
4
+ data.tar.gz: 89a255f7e7b68e2becd6314be09470424d5f418324c40bcc77a7311e6006d9f0
5
5
  SHA512:
6
- metadata.gz: 20916cbdb9a4d62bd9208c8496df8e8f425885c600d5caa9ef0d5a545c4d52a62fa66d05157bfc89a0c57328658f6ecb08889336352e76b4cfb757518365f50b
7
- data.tar.gz: b6125521d066808e825747418d794ca6adb94503e7d89b61fe5a9bb009e2c2fcc49952b0ea3fd12f5c0bb51de9d20de0ff3a7b4f0ab37358d0616010e1f9ca55
6
+ metadata.gz: 4000a38df6e83591a15f453af3b4d5f759b1af8e869478737c1d7711e34c7e1d65c89df34e56a6575d41e2f120e951970fde1ac444bf24d73c1167a24de875c4
7
+ data.tar.gz: 9273b4bf7d8a9af3ccbe0fa0d93914b13c9ce09f464902a4974749780871516c8fbaeb33a60d1dca754914dde09e498148ec080ff31fef3b11a0a88d96642eee
data/lib/t_ruby/cli.rb CHANGED
@@ -135,7 +135,7 @@ module TRuby
135
135
  compiler:
136
136
  strictness: standard # strict | standard | permissive
137
137
  generate_rbs: true
138
- target_ruby: "3.0"
138
+ target_ruby: "#{RubyVersion.current.major}.#{RubyVersion.current.minor}"
139
139
  # experimental: []
140
140
  # checks:
141
141
  # no_implicit_any: false
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ # Version-specific code transformation strategies
5
+ #
6
+ # @example
7
+ # emitter = CodeEmitter.for_version("4.0")
8
+ # result = emitter.transform(source)
9
+ #
10
+ module CodeEmitter
11
+ # Factory method to get appropriate emitter for target Ruby version
12
+ #
13
+ # @param target_ruby [String] target Ruby version (e.g., "3.0", "4.0")
14
+ # @return [Base] appropriate emitter instance
15
+ def self.for_version(target_ruby)
16
+ version = RubyVersion.parse(target_ruby)
17
+
18
+ if version.numbered_parameters_raise_error?
19
+ Ruby40.new(version)
20
+ elsif version.supports_it_parameter?
21
+ Ruby34.new(version)
22
+ elsif version.supports_anonymous_block_forwarding?
23
+ Ruby31.new(version)
24
+ else
25
+ Ruby30.new(version)
26
+ end
27
+ end
28
+
29
+ # Base class for version-specific code emitters
30
+ class Base
31
+ attr_reader :version
32
+
33
+ def initialize(version)
34
+ @version = version
35
+ end
36
+
37
+ # Apply all transformations for this version
38
+ #
39
+ # @param source [String] source code to transform
40
+ # @return [String] transformed source code
41
+ def transform(source)
42
+ result = source.dup
43
+ result = transform_numbered_params(result)
44
+ transform_block_forwarding(result)
45
+ end
46
+
47
+ # Transform numbered block parameters (_1, _2, etc.)
48
+ # Default: no transformation
49
+ #
50
+ # @param source [String] source code
51
+ # @return [String] transformed source code
52
+ def transform_numbered_params(source)
53
+ source
54
+ end
55
+
56
+ # Transform block forwarding syntax
57
+ # Default: no transformation
58
+ #
59
+ # @param source [String] source code
60
+ # @return [String] transformed source code
61
+ def transform_block_forwarding(source)
62
+ source
63
+ end
64
+
65
+ # Check if this version supports the `it` implicit block parameter
66
+ #
67
+ # @return [Boolean]
68
+ def supports_it?
69
+ false
70
+ end
71
+
72
+ # Check if numbered parameters raise NameError in this version
73
+ #
74
+ # @return [Boolean]
75
+ def numbered_params_error?
76
+ false
77
+ end
78
+ end
79
+
80
+ # Ruby 3.0 emitter - baseline, no transformations
81
+ class Ruby30 < Base
82
+ # Ruby 3.0 uses standard syntax, no transformations needed
83
+ end
84
+
85
+ # Ruby 3.1+ emitter - supports anonymous block forwarding
86
+ class Ruby31 < Base
87
+ # Transform `def foo(&block) ... bar(&block)` to `def foo(&) ... bar(&)`
88
+ #
89
+ # Only transforms when the block parameter is ONLY used for forwarding,
90
+ # not when it's called directly (e.g., block.call)
91
+ def transform_block_forwarding(source)
92
+ result = source.dup
93
+
94
+ # Find method definitions with block parameters
95
+ # Pattern: def method_name(&block_name)
96
+ result.gsub!(/def\s+(\w+[?!=]?)\s*\(([^)]*?)&(\w+)\s*\)/) do |_match|
97
+ method_name = ::Regexp.last_match(1)
98
+ other_params = ::Regexp.last_match(2)
99
+ block_name = ::Regexp.last_match(3)
100
+
101
+ # Find the method body to check block usage
102
+ method_start = ::Regexp.last_match.begin(0)
103
+ remaining = result[method_start..]
104
+
105
+ # Check if block is only used for forwarding (not called directly)
106
+ if block_only_forwarded?(remaining, block_name)
107
+ "def #{method_name}(#{other_params}&)"
108
+ else
109
+ "def #{method_name}(#{other_params}&#{block_name})"
110
+ end
111
+ end
112
+
113
+ # Replace block forwarding calls with anonymous forwarding
114
+ # This is a simplified approach - in practice we'd need proper scope tracking
115
+ result.gsub!(/(\w+)\s*\(\s*&(\w+)\s*\)/) do |match|
116
+ call_name = ::Regexp.last_match(1)
117
+ ::Regexp.last_match(2)
118
+
119
+ # Check if this block name was converted to anonymous
120
+ if result.include?("def ") && result.include?("(&)")
121
+ "#{call_name}(&)"
122
+ else
123
+ match
124
+ end
125
+ end
126
+
127
+ result
128
+ end
129
+
130
+ private
131
+
132
+ # Check if a block parameter is only used for forwarding
133
+ def block_only_forwarded?(method_body, block_name)
134
+ # Simple heuristic: if block_name appears with .call or without &, it's not just forwarding
135
+ # Look for patterns like: block_name.call, block_name.(), yield
136
+
137
+ # Extract method body (until next def or end of class)
138
+ lines = method_body.lines
139
+ depth = 0
140
+ body_lines = []
141
+
142
+ lines.each do |line|
143
+ depth += 1 if line.match?(/\b(def|class|module|do|begin|case|if|unless|while|until)\b/)
144
+ depth -= 1 if line.match?(/\bend\b/)
145
+ body_lines << line
146
+ break if depth <= 0 && body_lines.length > 1
147
+ end
148
+
149
+ body = body_lines.join
150
+
151
+ # Check for direct block usage
152
+ return false if body.match?(/\b#{block_name}\s*\./) # block.call, block.(), etc.
153
+ return false if body.match?(/\b#{block_name}\s*\[/) # block[args]
154
+ return false if body.match?(/\byield\b/) # yield instead of forwarding
155
+
156
+ # Only &block_name patterns - this is forwarding
157
+ true
158
+ end
159
+ end
160
+
161
+ # Ruby 3.4+ emitter - supports `it` implicit block parameter
162
+ class Ruby34 < Ruby31
163
+ def supports_it?
164
+ true
165
+ end
166
+
167
+ # Ruby 3.4 still supports _1 syntax, so no transformation needed by default
168
+ # Users can opt-in to using `it` style if they want
169
+ end
170
+
171
+ # Ruby 4.0+ emitter - _1 raises NameError, must use `it`
172
+ class Ruby40 < Ruby34
173
+ def numbered_params_error?
174
+ true
175
+ end
176
+
177
+ # Transform numbered parameters to appropriate syntax
178
+ #
179
+ # - Single _1 → it
180
+ # - Multiple (_1, _2) → explicit |k, v| params
181
+ def transform_numbered_params(source)
182
+ result = source.dup
183
+
184
+ # Simple approach: replace all _1 with it when it's the only numbered param in scope
185
+ # For complex cases with _2+, we'd need proper parsing
186
+ # For now, do a global replacement if _2 etc are not present
187
+ if result.match?(/\b_[2-9]\b/)
188
+ # Has multiple numbered params - need to convert to explicit params
189
+ # This is a complex case that requires proper block parsing
190
+ transform_multi_numbered_params(result)
191
+ else
192
+ # Only _1 is used - simple replacement
193
+ result.gsub(/\b_1\b/, "it")
194
+ end
195
+ end
196
+
197
+ private
198
+
199
+ def transform_multi_numbered_params(source)
200
+ result = source.dup
201
+
202
+ # Find blocks and transform them
203
+ # Use a recursive approach with placeholder replacement
204
+
205
+ # Replace innermost blocks first
206
+ loop do
207
+ changed = false
208
+ result = result.gsub(/\{([^{}]*)\}/) do |block|
209
+ content = ::Regexp.last_match(1)
210
+ max_param = find_max_numbered_param(content)
211
+
212
+ if max_param > 1
213
+ # Multiple params - convert to explicit
214
+ param_names = generate_param_names(max_param)
215
+ new_content = content.dup
216
+ (1..max_param).each do |i|
217
+ new_content.gsub!(/\b_#{i}\b/, param_names[i - 1])
218
+ end
219
+ changed = true
220
+ "{ |#{param_names.join(", ")}| #{new_content.strip} }"
221
+ elsif max_param == 1
222
+ # Single _1 - convert to it
223
+ changed = true
224
+ "{ #{content.gsub(/\b_1\b/, "it").strip} }"
225
+ else
226
+ block
227
+ end
228
+ end
229
+ break unless changed
230
+ end
231
+
232
+ result
233
+ end
234
+
235
+ def find_max_numbered_param(content)
236
+ max = 0
237
+ content.scan(/\b_(\d+)\b/) do |match|
238
+ num = match[0].to_i
239
+ max = num if num > max
240
+ end
241
+ max
242
+ end
243
+
244
+ def generate_param_names(count)
245
+ # Generate simple parameter names: a, b, c, ... or k, v for 2
246
+ if count == 2
247
+ %w[k v]
248
+ else
249
+ ("a".."z").take(count)
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -380,8 +380,8 @@ module TRuby
380
380
  ir_program = result[:program]
381
381
  end
382
382
 
383
- # Generate Ruby code using IR-aware generator
384
- generator = IRCodeGenerator.new
383
+ # Generate Ruby code using IR-aware generator with target Ruby version
384
+ generator = IRCodeGenerator.new(target_ruby: @config.target_ruby)
385
385
  generator.generate_with_source(ir_program, source)
386
386
  end
387
387
 
@@ -434,8 +434,12 @@ module TRuby
434
434
 
435
435
  # IR-aware code generator for source-preserving transformation
436
436
  class IRCodeGenerator
437
- def initialize
437
+ attr_reader :emitter
438
+
439
+ # @param target_ruby [String] target Ruby version (e.g., "3.0", "4.0")
440
+ def initialize(target_ruby: "3.0")
438
441
  @output = []
442
+ @emitter = CodeEmitter.for_version(target_ruby)
439
443
  end
440
444
 
441
445
  # Generate Ruby code from IR program
@@ -471,6 +475,9 @@ module TRuby
471
475
  # Remove return type annotations
472
476
  result = erase_return_types(result)
473
477
 
478
+ # Apply version-specific transformations
479
+ result = @emitter.transform(result)
480
+
474
481
  # Clean up extra blank lines
475
482
  result.gsub(/\n{3,}/, "\n\n")
476
483
  end
@@ -505,6 +512,7 @@ module TRuby
505
512
  params = []
506
513
  current = ""
507
514
  depth = 0
515
+ brace_depth = 0
508
516
 
509
517
  params_str.each_char do |char|
510
518
  case char
@@ -514,9 +522,16 @@ module TRuby
514
522
  when ">", "]", ")"
515
523
  depth -= 1
516
524
  current += char
525
+ when "{"
526
+ brace_depth += 1
527
+ current += char
528
+ when "}"
529
+ brace_depth -= 1
530
+ current += char
517
531
  when ","
518
- if depth.zero?
519
- params << clean_param(current.strip)
532
+ if depth.zero? && brace_depth.zero?
533
+ cleaned = clean_param(current.strip)
534
+ params.concat(Array(cleaned)) if cleaned
520
535
  current = ""
521
536
  else
522
537
  current += char
@@ -526,12 +541,47 @@ module TRuby
526
541
  end
527
542
  end
528
543
 
529
- params << clean_param(current.strip) unless current.empty?
544
+ cleaned = clean_param(current.strip) unless current.empty?
545
+ params.concat(Array(cleaned)) if cleaned
530
546
  params.join(", ")
531
547
  end
532
548
 
533
549
  # Clean a single parameter (remove type annotation, preserve default value)
550
+ # Returns String or Array of Strings (for keyword args group)
534
551
  def clean_param(param)
552
+ param = param.strip
553
+ return nil if param.empty?
554
+
555
+ # 0. 블록 파라미터: &name: Type -> &name
556
+ if param.start_with?("&")
557
+ match = param.match(/^&(\w+)(?::\s*.+)?$/)
558
+ return "&#{match[1]}" if match
559
+
560
+ return param
561
+ end
562
+
563
+ # 1. 더블 스플랫: **name: Type -> **name
564
+ if param.start_with?("**")
565
+ match = param.match(/^\*\*(\w+)(?::\s*.+)?$/)
566
+ return "**#{match[1]}" if match
567
+
568
+ return param
569
+ end
570
+
571
+ # 2. 키워드 인자 그룹: { ... } 또는 { ... }: InterfaceName
572
+ if param.start_with?("{")
573
+ return clean_keyword_args_group(param)
574
+ end
575
+
576
+ # 3. Hash 리터럴: name: { ... } -> name
577
+ if param.match?(/^\w+:\s*\{/)
578
+ match = param.match(/^(\w+):\s*\{.+\}(?::\s*\w+)?$/)
579
+ return match[1] if match
580
+
581
+ return param
582
+ end
583
+
584
+ # 4. 일반 파라미터: name: Type = value -> name = value 또는 name: Type -> name
535
585
  # Match: name: Type = value (with default value)
536
586
  if (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:\s*.+?\s*(=\s*.+)$/))
537
587
  "#{match[1]} #{match[2]}"
@@ -543,15 +593,83 @@ module TRuby
543
593
  end
544
594
  end
545
595
 
596
+ # 키워드 인자 그룹을 Ruby 키워드 인자로 변환
597
+ # { name: String, age: Integer = 0 } -> name:, age: 0
598
+ # { name:, age: 0 }: UserParams -> name:, age: 0
599
+ def clean_keyword_args_group(param)
600
+ # { ... }: InterfaceName 또는 { ... } 형태 파싱
601
+ interface_match = param.match(/^\{(.+)\}\s*:\s*\w+\s*$/)
602
+ inline_match = param.match(/^\{(.+)\}\s*$/) unless interface_match
603
+
604
+ inner_content = if interface_match
605
+ interface_match[1]
606
+ elsif inline_match
607
+ inline_match[1]
608
+ else
609
+ return param
610
+ end
611
+
612
+ # 내부 파라미터 분리
613
+ parts = split_nested_content(inner_content)
614
+ keyword_params = []
615
+
616
+ parts.each do |part|
617
+ part = part.strip
618
+ next if part.empty?
619
+
620
+ if interface_match
621
+ # interface 참조: name: default_value 또는 name:
622
+ if (match = part.match(/^(\w+):\s*(.*)$/))
623
+ name = match[1]
624
+ default_value = match[2].strip
625
+ keyword_params << if default_value.empty?
626
+ "#{name}:"
627
+ else
628
+ "#{name}: #{default_value}"
629
+ end
630
+ end
631
+ elsif (match = part.match(/^(\w+):\s*(.+)$/))
632
+ # 인라인 타입: name: Type = default 또는 name: Type
633
+ name = match[1]
634
+ type_and_default = match[2].strip
635
+
636
+ # Type = default 분리
637
+ default_value = extract_default_value(type_and_default)
638
+ keyword_params << if default_value
639
+ "#{name}: #{default_value}"
640
+ else
641
+ "#{name}:"
642
+ end
643
+ end
644
+ end
645
+
646
+ keyword_params
647
+ end
648
+
649
+ # 중첩된 내용을 콤마로 분리
650
+ def split_nested_content(content)
651
+ StringUtils.split_by_comma(content)
652
+ end
653
+
654
+ # 타입과 기본값에서 기본값만 추출
655
+ def extract_default_value(type_and_default)
656
+ StringUtils.extract_default_value(type_and_default)
657
+ end
658
+
546
659
  # Erase return type annotations
547
660
  def erase_return_types(source)
548
661
  result = source.dup
549
662
 
550
- # Remove return type: ): Type or ): Type<Foo> etc.
663
+ # Remove return type after parentheses: ): Type or ): Type<Foo> etc.
551
664
  result.gsub!(/\)\s*:\s*[^\n]+?(?=\s*$)/m) do |_match|
552
665
  ")"
553
666
  end
554
667
 
668
+ # Remove return type for methods without parentheses: def method_name: Type
669
+ result.gsub!(/^(\s*#{TRuby::VISIBILITY_PATTERN}def\s+#{TRuby::METHOD_NAME_PATTERN})\s*:\s*[^\n]+?(?=\s*$)/m) do |_match|
670
+ ::Regexp.last_match(1)
671
+ end
672
+
555
673
  result
556
674
  end
557
675
  end
data/lib/t_ruby/config.rb CHANGED
@@ -25,7 +25,7 @@ module TRuby
25
25
  "strictness" => "standard",
26
26
  "generate_rbs" => true,
27
27
  "type_check" => true,
28
- "target_ruby" => "3.0",
28
+ "target_ruby" => nil, # Auto-detect from current Ruby version
29
29
  "experimental" => [],
30
30
  "checks" => {
31
31
  "no_implicit_any" => false,
@@ -97,9 +97,24 @@ module TRuby
97
97
  end
98
98
 
99
99
  # Get target Ruby version
100
- # @return [String] target Ruby version (e.g., "3.0", "3.2")
100
+ # If not specified in config, auto-detects from current Ruby environment
101
+ # @return [String] target Ruby version (e.g., "3.0", "3.2", "4.0")
102
+ # @raise [UnsupportedRubyVersionError] if detected version is not supported
101
103
  def target_ruby
102
- (@compiler["target_ruby"] || "3.0").to_s
104
+ configured = @compiler["target_ruby"]
105
+ if configured
106
+ RubyVersion.parse(configured).validate!
107
+ configured.to_s
108
+ else
109
+ version = RubyVersion.current.validate!
110
+ "#{version.major}.#{version.minor}"
111
+ end
112
+ end
113
+
114
+ # Get target Ruby version as RubyVersion object
115
+ # @return [RubyVersion] target Ruby version object
116
+ def target_ruby_version
117
+ RubyVersion.parse(target_ruby)
103
118
  end
104
119
 
105
120
  # Get list of enabled experimental features
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
 
data/lib/t_ruby/parser.rb CHANGED
@@ -244,18 +244,36 @@ module TRuby
244
244
  param_list = split_params(params_str)
245
245
 
246
246
  param_list.each do |param|
247
- param_info = parse_single_parameter(param)
248
- 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
249
266
  end
250
267
 
251
268
  parameters
252
269
  end
253
270
 
254
271
  def split_params(params_str)
255
- # Handle nested generics like Array<Map<String, Int>>
272
+ # Handle nested generics, braces, brackets
256
273
  result = []
257
274
  current = ""
258
275
  depth = 0
276
+ brace_depth = 0
259
277
 
260
278
  params_str.each_char do |char|
261
279
  case char
@@ -265,8 +283,14 @@ module TRuby
265
283
  when ">", "]", ")"
266
284
  depth -= 1
267
285
  current += char
286
+ when "{"
287
+ brace_depth += 1
288
+ current += char
289
+ when "}"
290
+ brace_depth -= 1
291
+ current += char
268
292
  when ","
269
- if depth.zero?
293
+ if depth.zero? && brace_depth.zero?
270
294
  result << current.strip
271
295
  current = ""
272
296
  else
@@ -281,8 +305,10 @@ module TRuby
281
305
  result
282
306
  end
283
307
 
284
- def parse_single_parameter(param)
285
- match = param.match(/^(\w+)(?::\s*(.+?))?$/)
308
+ # 더블 스플랫 파라미터 파싱: **opts: Type
309
+ def parse_double_splat_parameter(param)
310
+ # **name: Type
311
+ match = param.match(/^\*\*(\w+)(?::\s*(.+?))?$/)
286
312
  return nil unless match
287
313
 
288
314
  param_name = match[1]
@@ -291,6 +317,155 @@ module TRuby
291
317
  result = {
292
318
  name: param_name,
293
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,
294
469
  }
295
470
 
296
471
  # Parse type with combinator
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ # Error raised when an unsupported Ruby version is detected
5
+ class UnsupportedRubyVersionError < StandardError; end
6
+
7
+ # Value object representing a Ruby version with comparison and feature detection
8
+ #
9
+ # @example
10
+ # version = RubyVersion.parse("3.4")
11
+ # version.supports_it_parameter? # => true
12
+ # version >= RubyVersion.parse("3.0") # => true
13
+ #
14
+ class RubyVersion
15
+ include Comparable
16
+
17
+ # Supported version range
18
+ MIN_VERSION = [3, 0].freeze
19
+ MAX_MAJOR = 4
20
+
21
+ # Version string pattern: major.minor or major.minor.patch
22
+ VERSION_REGEX = /\A(\d+)\.(\d+)(?:\.(\d+))?\z/
23
+
24
+ attr_reader :major, :minor, :patch
25
+
26
+ # @param major [Integer] major version number
27
+ # @param minor [Integer] minor version number
28
+ # @param patch [Integer] patch version number (default: 0)
29
+ def initialize(major, minor, patch = 0)
30
+ @major = major
31
+ @minor = minor
32
+ @patch = patch
33
+ end
34
+
35
+ # Parse a version string into a RubyVersion object
36
+ #
37
+ # @param version_string [String, Numeric] version string (e.g., "3.4", "3.4.1")
38
+ # @return [RubyVersion] parsed version object
39
+ # @raise [ArgumentError] if version format is invalid
40
+ def self.parse(version_string)
41
+ str = version_string.to_s
42
+ match = VERSION_REGEX.match(str)
43
+
44
+ raise ArgumentError, "Invalid version: #{version_string}" unless match
45
+
46
+ new(match[1].to_i, match[2].to_i, (match[3] || 0).to_i)
47
+ end
48
+
49
+ # Get the current Ruby version from the environment
50
+ #
51
+ # @return [RubyVersion] current Ruby version
52
+ def self.current
53
+ parse(RUBY_VERSION)
54
+ end
55
+
56
+ # Compare two versions
57
+ #
58
+ # @param other [RubyVersion] version to compare with
59
+ # @return [Integer] -1, 0, or 1
60
+ def <=>(other)
61
+ [major, minor, patch] <=> [other.major, other.minor, other.patch]
62
+ end
63
+
64
+ # Convert to string representation
65
+ #
66
+ # @return [String] version string (e.g., "3.4" or "3.4.1")
67
+ def to_s
68
+ patch.zero? ? "#{major}.#{minor}" : "#{major}.#{minor}.#{patch}"
69
+ end
70
+
71
+ # Check if this version is within the supported range (3.0 ~ 4.x)
72
+ #
73
+ # @return [Boolean] true if version is supported
74
+ def supported?
75
+ self >= self.class.parse("#{MIN_VERSION[0]}.#{MIN_VERSION[1]}") && major <= MAX_MAJOR
76
+ end
77
+
78
+ # Validate that this version is supported, raising an error if not
79
+ #
80
+ # @return [RubyVersion] self if valid
81
+ # @raise [UnsupportedRubyVersionError] if version is not supported
82
+ def validate!
83
+ unless supported?
84
+ raise UnsupportedRubyVersionError,
85
+ "Ruby #{self}는 지원되지 않습니다. 지원 범위: #{MIN_VERSION.join(".")} ~ #{MAX_MAJOR}.x"
86
+ end
87
+
88
+ self
89
+ end
90
+
91
+ # Check if this version supports the `it` implicit block parameter (Ruby 3.4+)
92
+ #
93
+ # @return [Boolean] true if `it` parameter is supported
94
+ def supports_it_parameter?
95
+ self >= self.class.parse("3.4")
96
+ end
97
+
98
+ # Check if this version supports anonymous block forwarding `def foo(&) ... end` (Ruby 3.1+)
99
+ #
100
+ # @return [Boolean] true if anonymous block forwarding is supported
101
+ def supports_anonymous_block_forwarding?
102
+ self >= self.class.parse("3.1")
103
+ end
104
+
105
+ # Check if numbered parameters (_1, _2, etc.) raise NameError (Ruby 4.0+)
106
+ #
107
+ # @return [Boolean] true if numbered parameters cause errors
108
+ def numbered_parameters_raise_error?
109
+ self >= self.class.parse("4.0")
110
+ end
111
+ end
112
+ 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.40"
4
+ VERSION = "0.0.42"
5
5
  end
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "listen"
3
+ # listen gem is optional - only required for watch mode
4
+ # This allows T-Ruby core functionality to work on Ruby 4.0+ where listen/ffi may not be available
5
+ begin
6
+ require "listen"
7
+ LISTEN_AVAILABLE = true
8
+ rescue LoadError
9
+ LISTEN_AVAILABLE = false
10
+ end
4
11
 
5
12
  module TRuby
6
13
  class Watcher
@@ -52,6 +59,14 @@ module TRuby
52
59
  end
53
60
 
54
61
  def watch
62
+ unless LISTEN_AVAILABLE
63
+ puts colorize(:red, "Error: Watch mode requires the 'listen' gem.")
64
+ puts colorize(:yellow, "The 'listen' gem is not available (possibly due to Ruby 4.0+ ffi compatibility).")
65
+ puts colorize(:dim, "Install with: gem install listen")
66
+ puts colorize(:dim, "Or run without watch mode: trc")
67
+ exit 1
68
+ end
69
+
55
70
  print_start_message
56
71
 
57
72
  # Initial compilation
data/lib/t_ruby.rb CHANGED
@@ -2,9 +2,12 @@
2
2
 
3
3
  require_relative "t_ruby/version"
4
4
  require_relative "t_ruby/version_checker"
5
+ require_relative "t_ruby/ruby_version"
6
+ require_relative "t_ruby/code_emitter"
5
7
  require_relative "t_ruby/config"
6
8
 
7
9
  # Core infrastructure (must be loaded first)
10
+ require_relative "t_ruby/string_utils"
8
11
  require_relative "t_ruby/ir"
9
12
  require_relative "t_ruby/parser_combinator"
10
13
  require_relative "t_ruby/smt_solver"
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.40
4
+ version: 0.0.42
5
5
  platform: ruby
6
6
  authors:
7
7
  - Y. Fred Kim
@@ -10,19 +10,19 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: listen
13
+ name: benchmark
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '3.8'
18
+ version: '0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '3.8'
25
+ version: '0'
26
26
  description: t-ruby compiles .trb files with type annotations to executable Ruby (.rb)
27
27
  and optional type signature files (.rbs)
28
28
  email:
@@ -42,6 +42,7 @@ files:
42
42
  - lib/t_ruby/bundler_integration.rb
43
43
  - lib/t_ruby/cache.rb
44
44
  - lib/t_ruby/cli.rb
45
+ - lib/t_ruby/code_emitter.rb
45
46
  - lib/t_ruby/compiler.rb
46
47
  - lib/t_ruby/config.rb
47
48
  - lib/t_ruby/constraint_checker.rb
@@ -59,8 +60,10 @@ files:
59
60
  - lib/t_ruby/package_manager.rb
60
61
  - lib/t_ruby/parser.rb
61
62
  - lib/t_ruby/parser_combinator.rb
63
+ - lib/t_ruby/ruby_version.rb
62
64
  - lib/t_ruby/runtime_validator.rb
63
65
  - lib/t_ruby/smt_solver.rb
66
+ - lib/t_ruby/string_utils.rb
64
67
  - lib/t_ruby/type_alias_registry.rb
65
68
  - lib/t_ruby/type_checker.rb
66
69
  - lib/t_ruby/type_env.rb