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.
- checksums.yaml +4 -4
- data/lib/t_ruby/ast_type_inferrer.rb +2 -0
- data/lib/t_ruby/cache.rb +40 -10
- data/lib/t_ruby/cli.rb +14 -9
- data/lib/t_ruby/code_emitter.rb +254 -0
- data/lib/t_ruby/compiler.rb +186 -3
- data/lib/t_ruby/config.rb +18 -3
- data/lib/t_ruby/diagnostic.rb +115 -0
- data/lib/t_ruby/diagnostic_formatter.rb +162 -0
- data/lib/t_ruby/error_handler.rb +201 -35
- data/lib/t_ruby/error_reporter.rb +57 -0
- data/lib/t_ruby/ir.rb +39 -1
- data/lib/t_ruby/lsp_server.rb +40 -97
- data/lib/t_ruby/parser.rb +18 -4
- data/lib/t_ruby/parser_combinator/combinators/alternative.rb +20 -0
- data/lib/t_ruby/parser_combinator/combinators/chain_left.rb +34 -0
- data/lib/t_ruby/parser_combinator/combinators/choice.rb +29 -0
- data/lib/t_ruby/parser_combinator/combinators/flat_map.rb +21 -0
- data/lib/t_ruby/parser_combinator/combinators/label.rb +22 -0
- data/lib/t_ruby/parser_combinator/combinators/lookahead.rb +21 -0
- data/lib/t_ruby/parser_combinator/combinators/many.rb +29 -0
- data/lib/t_ruby/parser_combinator/combinators/many1.rb +32 -0
- data/lib/t_ruby/parser_combinator/combinators/map.rb +17 -0
- data/lib/t_ruby/parser_combinator/combinators/not_followed_by.rb +21 -0
- data/lib/t_ruby/parser_combinator/combinators/optional.rb +21 -0
- data/lib/t_ruby/parser_combinator/combinators/sep_by.rb +34 -0
- data/lib/t_ruby/parser_combinator/combinators/sep_by1.rb +34 -0
- data/lib/t_ruby/parser_combinator/combinators/sequence.rb +23 -0
- data/lib/t_ruby/parser_combinator/combinators/skip_right.rb +23 -0
- data/lib/t_ruby/parser_combinator/declaration_parser.rb +147 -0
- data/lib/t_ruby/parser_combinator/dsl.rb +115 -0
- data/lib/t_ruby/parser_combinator/parse_error.rb +48 -0
- data/lib/t_ruby/parser_combinator/parse_result.rb +46 -0
- data/lib/t_ruby/parser_combinator/parser.rb +84 -0
- data/lib/t_ruby/parser_combinator/primitives/end_of_input.rb +16 -0
- data/lib/t_ruby/parser_combinator/primitives/fail.rb +16 -0
- data/lib/t_ruby/parser_combinator/primitives/lazy.rb +18 -0
- data/lib/t_ruby/parser_combinator/primitives/literal.rb +21 -0
- data/lib/t_ruby/parser_combinator/primitives/pure.rb +16 -0
- data/lib/t_ruby/parser_combinator/primitives/regex.rb +25 -0
- data/lib/t_ruby/parser_combinator/primitives/satisfy.rb +21 -0
- data/lib/t_ruby/parser_combinator/token/expression_parser.rb +541 -0
- data/lib/t_ruby/parser_combinator/token/statement_parser.rb +644 -0
- data/lib/t_ruby/parser_combinator/token/token_alternative.rb +20 -0
- data/lib/t_ruby/parser_combinator/token/token_body_parser.rb +54 -0
- data/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb +920 -0
- data/lib/t_ruby/parser_combinator/token/token_dsl.rb +16 -0
- data/lib/t_ruby/parser_combinator/token/token_label.rb +22 -0
- data/lib/t_ruby/parser_combinator/token/token_many.rb +29 -0
- data/lib/t_ruby/parser_combinator/token/token_many1.rb +32 -0
- data/lib/t_ruby/parser_combinator/token/token_map.rb +17 -0
- data/lib/t_ruby/parser_combinator/token/token_matcher.rb +29 -0
- data/lib/t_ruby/parser_combinator/token/token_optional.rb +21 -0
- data/lib/t_ruby/parser_combinator/token/token_parse_result.rb +40 -0
- data/lib/t_ruby/parser_combinator/token/token_parser.rb +62 -0
- data/lib/t_ruby/parser_combinator/token/token_sep_by.rb +34 -0
- data/lib/t_ruby/parser_combinator/token/token_sep_by1.rb +34 -0
- data/lib/t_ruby/parser_combinator/token/token_sequence.rb +23 -0
- data/lib/t_ruby/parser_combinator/token/token_skip_right.rb +23 -0
- data/lib/t_ruby/parser_combinator/type_parser.rb +103 -0
- data/lib/t_ruby/parser_combinator.rb +64 -936
- data/lib/t_ruby/ruby_version.rb +112 -0
- data/lib/t_ruby/scanner.rb +883 -0
- data/lib/t_ruby/version.rb +1 -1
- data/lib/t_ruby/watcher.rb +83 -76
- data/lib/t_ruby.rb +17 -1
- metadata +58 -7
- data/lib/t_ruby/body_parser.rb +0 -561
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cb406ad473738afc16343a01b9f7e8e0f77a8b0fe700dd3ced2df65197bce626
|
|
4
|
+
data.tar.gz: 95f1ce4ede5378e17393046c223c46b404d95e6506235e210aeba20a9615f8ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6003aa038a7b476b8682aed9e32ba19ddbee6c124fe294c79b0371924527002f20c3ccdf45f71e2ee7360bef4e126d1ee0c15d93afd033df1166c20b5719187a
|
|
7
|
+
data.tar.gz: c0c462fada5b5d98f46298dc307d6000b56d537b8d7f80af1faba2206c682cebf02e588277ca5ea2bf6f4e21141928540c733f0f8851adc48b4ca1be8ede6f81
|
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 =
|
|
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] =
|
|
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
|
|
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
|
-
|
|
694
|
+
all_diagnostics = []
|
|
689
695
|
|
|
690
696
|
# First pass: compile and register all files
|
|
691
697
|
file_paths.each do |file_path|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
|
|
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
|
-
|
|
706
|
-
success:
|
|
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: "
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
data/lib/t_ruby/compiler.rb
CHANGED
|
@@ -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
|
-
|
|
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" =>
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|