t-ruby 0.0.41 → 0.0.43

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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/t_ruby/ast_type_inferrer.rb +2 -0
  3. data/lib/t_ruby/cache.rb +40 -10
  4. data/lib/t_ruby/cli.rb +14 -9
  5. data/lib/t_ruby/code_emitter.rb +254 -0
  6. data/lib/t_ruby/compiler.rb +186 -3
  7. data/lib/t_ruby/config.rb +18 -3
  8. data/lib/t_ruby/diagnostic.rb +115 -0
  9. data/lib/t_ruby/diagnostic_formatter.rb +162 -0
  10. data/lib/t_ruby/error_handler.rb +201 -35
  11. data/lib/t_ruby/error_reporter.rb +57 -0
  12. data/lib/t_ruby/ir.rb +39 -1
  13. data/lib/t_ruby/lsp_server.rb +40 -97
  14. data/lib/t_ruby/parser.rb +18 -4
  15. data/lib/t_ruby/parser_combinator/combinators/alternative.rb +20 -0
  16. data/lib/t_ruby/parser_combinator/combinators/chain_left.rb +34 -0
  17. data/lib/t_ruby/parser_combinator/combinators/choice.rb +29 -0
  18. data/lib/t_ruby/parser_combinator/combinators/flat_map.rb +21 -0
  19. data/lib/t_ruby/parser_combinator/combinators/label.rb +22 -0
  20. data/lib/t_ruby/parser_combinator/combinators/lookahead.rb +21 -0
  21. data/lib/t_ruby/parser_combinator/combinators/many.rb +29 -0
  22. data/lib/t_ruby/parser_combinator/combinators/many1.rb +32 -0
  23. data/lib/t_ruby/parser_combinator/combinators/map.rb +17 -0
  24. data/lib/t_ruby/parser_combinator/combinators/not_followed_by.rb +21 -0
  25. data/lib/t_ruby/parser_combinator/combinators/optional.rb +21 -0
  26. data/lib/t_ruby/parser_combinator/combinators/sep_by.rb +34 -0
  27. data/lib/t_ruby/parser_combinator/combinators/sep_by1.rb +34 -0
  28. data/lib/t_ruby/parser_combinator/combinators/sequence.rb +23 -0
  29. data/lib/t_ruby/parser_combinator/combinators/skip_right.rb +23 -0
  30. data/lib/t_ruby/parser_combinator/declaration_parser.rb +147 -0
  31. data/lib/t_ruby/parser_combinator/dsl.rb +115 -0
  32. data/lib/t_ruby/parser_combinator/parse_error.rb +48 -0
  33. data/lib/t_ruby/parser_combinator/parse_result.rb +46 -0
  34. data/lib/t_ruby/parser_combinator/parser.rb +84 -0
  35. data/lib/t_ruby/parser_combinator/primitives/end_of_input.rb +16 -0
  36. data/lib/t_ruby/parser_combinator/primitives/fail.rb +16 -0
  37. data/lib/t_ruby/parser_combinator/primitives/lazy.rb +18 -0
  38. data/lib/t_ruby/parser_combinator/primitives/literal.rb +21 -0
  39. data/lib/t_ruby/parser_combinator/primitives/pure.rb +16 -0
  40. data/lib/t_ruby/parser_combinator/primitives/regex.rb +25 -0
  41. data/lib/t_ruby/parser_combinator/primitives/satisfy.rb +21 -0
  42. data/lib/t_ruby/parser_combinator/token/expression_parser.rb +541 -0
  43. data/lib/t_ruby/parser_combinator/token/statement_parser.rb +644 -0
  44. data/lib/t_ruby/parser_combinator/token/token_alternative.rb +20 -0
  45. data/lib/t_ruby/parser_combinator/token/token_body_parser.rb +54 -0
  46. data/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb +920 -0
  47. data/lib/t_ruby/parser_combinator/token/token_dsl.rb +16 -0
  48. data/lib/t_ruby/parser_combinator/token/token_label.rb +22 -0
  49. data/lib/t_ruby/parser_combinator/token/token_many.rb +29 -0
  50. data/lib/t_ruby/parser_combinator/token/token_many1.rb +32 -0
  51. data/lib/t_ruby/parser_combinator/token/token_map.rb +17 -0
  52. data/lib/t_ruby/parser_combinator/token/token_matcher.rb +29 -0
  53. data/lib/t_ruby/parser_combinator/token/token_optional.rb +21 -0
  54. data/lib/t_ruby/parser_combinator/token/token_parse_result.rb +40 -0
  55. data/lib/t_ruby/parser_combinator/token/token_parser.rb +62 -0
  56. data/lib/t_ruby/parser_combinator/token/token_sep_by.rb +34 -0
  57. data/lib/t_ruby/parser_combinator/token/token_sep_by1.rb +34 -0
  58. data/lib/t_ruby/parser_combinator/token/token_sequence.rb +23 -0
  59. data/lib/t_ruby/parser_combinator/token/token_skip_right.rb +23 -0
  60. data/lib/t_ruby/parser_combinator/type_parser.rb +103 -0
  61. data/lib/t_ruby/parser_combinator.rb +64 -936
  62. data/lib/t_ruby/ruby_version.rb +112 -0
  63. data/lib/t_ruby/scanner.rb +883 -0
  64. data/lib/t_ruby/version.rb +1 -1
  65. data/lib/t_ruby/watcher.rb +83 -76
  66. data/lib/t_ruby.rb +17 -1
  67. metadata +58 -7
  68. data/lib/t_ruby/body_parser.rb +0 -561
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a7c7a93e1498a150b541ea7f7838c08435a33ead72774b7e4c0b8ead951e9d3
4
- data.tar.gz: 1fbfb9ea6b276e130ef04891f39d691e362d5352dc4f0209b00009105f13c3a0
3
+ metadata.gz: cb406ad473738afc16343a01b9f7e8e0f77a8b0fe700dd3ced2df65197bce626
4
+ data.tar.gz: 95f1ce4ede5378e17393046c223c46b404d95e6506235e210aeba20a9615f8ac
5
5
  SHA512:
6
- metadata.gz: 82cbc96204194f51b65f484aeda25383e285ce902c325e269766e77871e9dccacc548111e864ba2bfb27ca48fc69c7e22c5a0b1eee8e7c25e4763417ffe2c8fe
7
- data.tar.gz: ed5f8ad72344e624e8a95d1e9a48078e0f6679d914f978974e0117b75bbc11ce2fbe08ece39360b20ce777ef5c5c333f37376f216272c1e6aa983eb18585fc92
6
+ metadata.gz: 6003aa038a7b476b8682aed9e32ba19ddbee6c124fe294c79b0371924527002f20c3ccdf45f71e2ee7360bef4e126d1ee0c15d93afd033df1166c20b5719187a
7
+ data.tar.gz: c0c462fada5b5d98f46298dc307d6000b56d537b8d7f80af1faba2206c682cebf02e588277ca5ea2bf6f4e21141928540c733f0f8851adc48b4ca1be8ede6f81
@@ -158,6 +158,8 @@ module TRuby
158
158
  type = case node
159
159
  when IR::Literal
160
160
  infer_literal(node)
161
+ when IR::InterpolatedString
162
+ "String" # Interpolated strings always produce String
161
163
  when IR::VariableRef
162
164
  infer_variable_ref(node, env)
163
165
  when IR::BinaryOp
data/lib/t_ruby/cache.rb CHANGED
@@ -318,7 +318,7 @@ module TRuby
318
318
  def needs_compile?(file_path)
319
319
  return true unless File.exist?(file_path)
320
320
 
321
- current_hash = file_hash(file_path)
321
+ current_hash = compute_file_hash(file_path)
322
322
  stored_hash = @file_hashes[file_path]
323
323
 
324
324
  return true if stored_hash.nil? || stored_hash != current_hash
@@ -333,7 +333,7 @@ module TRuby
333
333
  return @compiled_files[file_path] unless needs_compile?(file_path)
334
334
 
335
335
  result = @compiler.compile(file_path)
336
- @file_hashes[file_path] = file_hash(file_path)
336
+ @file_hashes[file_path] = compute_file_hash(file_path)
337
337
  @compiled_files[file_path] = result
338
338
 
339
339
  result
@@ -365,9 +365,14 @@ module TRuby
365
365
  @cache.stats # Just accessing for potential cleanup
366
366
  end
367
367
 
368
+ # Update file hash after external compile (for watcher integration)
369
+ def update_file_hash(file_path)
370
+ @file_hashes[file_path] = compute_file_hash(file_path)
371
+ end
372
+
368
373
  private
369
374
 
370
- def file_hash(file_path)
375
+ def compute_file_hash(file_path)
371
376
  return nil unless File.exist?(file_path)
372
377
 
373
378
  Digest::SHA256.hexdigest(File.read(file_path))
@@ -683,27 +688,52 @@ module TRuby
683
688
  end
684
689
 
685
690
  # Compile all with cross-file checking
691
+ # Returns diagnostics using unified Diagnostic format
686
692
  def compile_all_with_checking(file_paths)
687
693
  results = {}
688
- errors = []
694
+ all_diagnostics = []
689
695
 
690
696
  # First pass: compile and register all files
691
697
  file_paths.each do |file_path|
692
- results[file_path] = compile_with_ir(file_path)
693
- rescue StandardError => e
694
- errors << { file: file_path, error: e.message }
698
+ source = File.exist?(file_path) ? File.read(file_path) : nil
699
+
700
+ begin
701
+ results[file_path] = compile_with_ir(file_path)
702
+ rescue TypeCheckError => e
703
+ all_diagnostics << Diagnostic.from_type_check_error(e, file: file_path, source: source)
704
+ rescue ParseError => e
705
+ all_diagnostics << Diagnostic.from_parse_error(e, file: file_path, source: source)
706
+ rescue Scanner::ScanError => e
707
+ all_diagnostics << Diagnostic.from_scan_error(e, file: file_path, source: source)
708
+ rescue StandardError => e
709
+ all_diagnostics << Diagnostic.new(
710
+ code: "TR0001",
711
+ message: e.message,
712
+ file: file_path,
713
+ line: 1,
714
+ column: 1
715
+ )
716
+ end
695
717
  end
696
718
 
697
719
  # Second pass: cross-file type checking
698
720
  if @cross_file_checker
699
721
  check_result = @cross_file_checker.check_all
700
- errors.concat(check_result[:errors])
722
+ check_result[:errors].each do |e|
723
+ all_diagnostics << Diagnostic.new(
724
+ code: "TR2002",
725
+ message: e[:message],
726
+ file: e[:file],
727
+ line: 1,
728
+ column: 1
729
+ )
730
+ end
701
731
  end
702
732
 
703
733
  {
704
734
  results: results,
705
- errors: errors,
706
- success: errors.empty?,
735
+ diagnostics: all_diagnostics,
736
+ success: all_diagnostics.empty?,
707
737
  }
708
738
  end
709
739
 
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
@@ -214,14 +214,19 @@ module TRuby
214
214
  config = Config.new(config_path)
215
215
  compiler = Compiler.new(config)
216
216
 
217
- output_path = compiler.compile(input_file)
218
- puts "Compiled: #{input_file} -> #{output_path}"
219
- rescue TypeCheckError => e
220
- puts "Type error: #{e.message}"
221
- exit 1
222
- rescue ArgumentError => e
223
- puts "Error: #{e.message}"
224
- exit 1
217
+ result = compiler.compile_with_diagnostics(input_file)
218
+
219
+ if result[:success]
220
+ puts "Compiled: #{input_file} -> #{result[:output_path]}"
221
+ else
222
+ formatter = DiagnosticFormatter.new(use_colors: $stdout.tty?)
223
+ result[:diagnostics].each do |diagnostic|
224
+ puts formatter.format(diagnostic)
225
+ end
226
+ puts
227
+ puts formatter.send(:format_summary, result[:diagnostics])
228
+ exit 1
229
+ end
225
230
  end
226
231
 
227
232
  # Extract config path from --config or -c flag
@@ -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
@@ -76,6 +76,161 @@ module TRuby
76
76
  output_path
77
77
  end
78
78
 
79
+ # Compile a file and return result with diagnostics
80
+ # This is the unified compilation interface for CLI and Watcher
81
+ # @param input_path [String] Path to the input file
82
+ # @return [Hash] Result with :success, :output_path, :diagnostics keys
83
+ def compile_with_diagnostics(input_path)
84
+ source = File.exist?(input_path) ? File.read(input_path) : nil
85
+ all_diagnostics = []
86
+
87
+ # Run analyze first to get all diagnostics (colon spacing, etc.)
88
+ if source
89
+ all_diagnostics = analyze(source, file: input_path)
90
+ end
91
+
92
+ begin
93
+ output_path = compile(input_path)
94
+ # Compilation succeeded, but we may still have diagnostics from analyze
95
+ {
96
+ success: all_diagnostics.empty?,
97
+ output_path: all_diagnostics.empty? ? output_path : nil,
98
+ diagnostics: all_diagnostics,
99
+ }
100
+ rescue TypeCheckError => e
101
+ # Skip if already reported by analyze (same message and location)
102
+ new_diag = Diagnostic.from_type_check_error(e, file: input_path, source: source)
103
+ unless all_diagnostics.any? { |d| d.message == new_diag.message && d.line == new_diag.line }
104
+ all_diagnostics << new_diag
105
+ end
106
+ {
107
+ success: false,
108
+ output_path: nil,
109
+ diagnostics: all_diagnostics,
110
+ }
111
+ rescue ParseError => e
112
+ new_diag = Diagnostic.from_parse_error(e, file: input_path, source: source)
113
+ unless all_diagnostics.any? { |d| d.message == new_diag.message && d.line == new_diag.line }
114
+ all_diagnostics << new_diag
115
+ end
116
+ {
117
+ success: false,
118
+ output_path: nil,
119
+ diagnostics: all_diagnostics,
120
+ }
121
+ rescue Scanner::ScanError => e
122
+ new_diag = Diagnostic.from_scan_error(e, file: input_path, source: source)
123
+ unless all_diagnostics.any? { |d| d.message == new_diag.message && d.line == new_diag.line }
124
+ all_diagnostics << new_diag
125
+ end
126
+ {
127
+ success: false,
128
+ output_path: nil,
129
+ diagnostics: all_diagnostics,
130
+ }
131
+ rescue ArgumentError => e
132
+ all_diagnostics << Diagnostic.new(
133
+ code: "TR0001",
134
+ message: e.message,
135
+ file: input_path,
136
+ severity: Diagnostic::SEVERITY_ERROR
137
+ )
138
+ {
139
+ success: false,
140
+ output_path: nil,
141
+ diagnostics: all_diagnostics,
142
+ }
143
+ end
144
+ end
145
+
146
+ # Analyze source code without compiling - returns diagnostics only
147
+ # This is the unified analysis interface for LSP and other tools
148
+ # @param source [String] T-Ruby source code
149
+ # @param file [String] File path for error reporting (optional)
150
+ # @return [Array<Diagnostic>] Array of diagnostic objects
151
+ def analyze(source, file: "<source>")
152
+ diagnostics = []
153
+ source_lines = source.split("\n")
154
+
155
+ # Run ErrorHandler checks (syntax validation, duplicate definitions, etc.)
156
+ error_handler = ErrorHandler.new(source)
157
+ errors = error_handler.check
158
+ errors.each do |error|
159
+ # Parse line number from "Line N: message" format
160
+ next unless error =~ /^Line (\d+):\s*(.+)$/
161
+
162
+ line_num = Regexp.last_match(1).to_i
163
+ message = Regexp.last_match(2)
164
+ source_line = source_lines[line_num - 1] if line_num.positive?
165
+ diagnostics << Diagnostic.new(
166
+ code: "TR1002",
167
+ message: message,
168
+ file: file,
169
+ line: line_num,
170
+ column: 1,
171
+ source_line: source_line,
172
+ severity: Diagnostic::SEVERITY_ERROR
173
+ )
174
+ end
175
+
176
+ # Run TokenDeclarationParser for colon spacing and declaration syntax validation
177
+ begin
178
+ scanner = Scanner.new(source)
179
+ tokens = scanner.scan_all
180
+ decl_parser = ParserCombinator::TokenDeclarationParser.new
181
+ decl_parser.parse_program(tokens)
182
+
183
+ if decl_parser.has_errors?
184
+ decl_parser.errors.each do |err|
185
+ source_line = source_lines[err.line - 1] if err.line.positive? && err.line <= source_lines.length
186
+ diagnostics << Diagnostic.new(
187
+ code: "TR1003",
188
+ message: err.message,
189
+ file: file,
190
+ line: err.line,
191
+ column: err.column,
192
+ source_line: source_line,
193
+ severity: Diagnostic::SEVERITY_ERROR
194
+ )
195
+ end
196
+ end
197
+ rescue Scanner::ScanError
198
+ # Scanner errors will be caught below in the main parse section
199
+ rescue StandardError
200
+ # Ignore TokenDeclarationParser errors for now - regex parser is authoritative
201
+ end
202
+
203
+ begin
204
+ # Parse source with regex-based parser for IR generation
205
+ parser = Parser.new(source)
206
+ parser.parse
207
+
208
+ # Run type checking if enabled and IR is available
209
+ if type_check? && parser.ir_program
210
+ begin
211
+ check_types(parser.ir_program, file)
212
+ rescue TypeCheckError => e
213
+ diagnostics << Diagnostic.from_type_check_error(e, file: file, source: source)
214
+ end
215
+ end
216
+ rescue ParseError => e
217
+ diagnostics << Diagnostic.from_parse_error(e, file: file, source: source)
218
+ rescue Scanner::ScanError => e
219
+ diagnostics << Diagnostic.from_scan_error(e, file: file, source: source)
220
+ rescue StandardError => e
221
+ diagnostics << Diagnostic.new(
222
+ code: "TR0001",
223
+ message: e.message,
224
+ file: file,
225
+ line: 1,
226
+ column: 1,
227
+ severity: Diagnostic::SEVERITY_ERROR
228
+ )
229
+ end
230
+
231
+ diagnostics
232
+ end
233
+
79
234
  # Compile T-Ruby source code from a string (useful for WASM/playground)
80
235
  # @param source [String] T-Ruby source code
81
236
  # @param options [Hash] Options for compilation
@@ -319,6 +474,19 @@ module TRuby
319
474
  # Subtype relationships
320
475
  return true if subtype_of?(inferred, declared)
321
476
 
477
+ # Handle generic types (e.g., Array[untyped] is compatible with Array[String])
478
+ if inferred.include?("[") && declared.include?("[")
479
+ inferred_base = inferred.split("[").first
480
+ declared_base = declared.split("[").first
481
+ if inferred_base == declared_base
482
+ # Extract type arguments
483
+ inferred_args = inferred[/\[(.+)\]/, 1]
484
+ declared_args = declared[/\[(.+)\]/, 1]
485
+ # untyped type argument is compatible with any type argument
486
+ return true if inferred_args == "untyped" || declared_args == "untyped"
487
+ end
488
+ end
489
+
322
490
  # Handle union types in declared
323
491
  if declared.include?("|")
324
492
  declared_types = declared.split("|").map(&:strip)
@@ -380,8 +548,8 @@ module TRuby
380
548
  ir_program = result[:program]
381
549
  end
382
550
 
383
- # Generate Ruby code using IR-aware generator
384
- generator = IRCodeGenerator.new
551
+ # Generate Ruby code using IR-aware generator with target Ruby version
552
+ generator = IRCodeGenerator.new(target_ruby: @config.target_ruby)
385
553
  generator.generate_with_source(ir_program, source)
386
554
  end
387
555
 
@@ -434,8 +602,12 @@ module TRuby
434
602
 
435
603
  # IR-aware code generator for source-preserving transformation
436
604
  class IRCodeGenerator
437
- def initialize
605
+ attr_reader :emitter
606
+
607
+ # @param target_ruby [String] target Ruby version (e.g., "3.0", "4.0")
608
+ def initialize(target_ruby: "3.0")
438
609
  @output = []
610
+ @emitter = CodeEmitter.for_version(target_ruby)
439
611
  end
440
612
 
441
613
  # Generate Ruby code from IR program
@@ -471,6 +643,9 @@ module TRuby
471
643
  # Remove return type annotations
472
644
  result = erase_return_types(result)
473
645
 
646
+ # Apply version-specific transformations
647
+ result = @emitter.transform(result)
648
+
474
649
  # Clean up extra blank lines
475
650
  result.gsub(/\n{3,}/, "\n\n")
476
651
  end
@@ -545,6 +720,14 @@ module TRuby
545
720
  param = param.strip
546
721
  return nil if param.empty?
547
722
 
723
+ # 0. 블록 파라미터: &name: Type -> &name
724
+ if param.start_with?("&")
725
+ match = param.match(/^&(\w+)(?::\s*.+)?$/)
726
+ return "&#{match[1]}" if match
727
+
728
+ return param
729
+ end
730
+
548
731
  # 1. 더블 스플랫: **name: Type -> **name
549
732
  if param.start_with?("**")
550
733
  match = param.match(/^\*\*(\w+)(?::\s*.+)?$/)
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