t-ruby 0.0.38 → 0.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +21 -17
- data/README.md +3 -4
- data/lib/t_ruby/ast_type_inferrer.rb +511 -0
- data/lib/t_ruby/body_parser.rb +561 -0
- data/lib/t_ruby/cli.rb +3 -0
- data/lib/t_ruby/compiler.rb +177 -60
- data/lib/t_ruby/config.rb +7 -0
- data/lib/t_ruby/heredoc_detector.rb +74 -0
- data/lib/t_ruby/ir.rb +104 -7
- data/lib/t_ruby/lsp_server.rb +1 -1
- data/lib/t_ruby/parser.rb +228 -45
- data/lib/t_ruby/type_checker.rb +17 -14
- data/lib/t_ruby/type_env.rb +127 -0
- data/lib/t_ruby/version.rb +1 -1
- data/lib/t_ruby/watcher.rb +26 -21
- data/lib/t_ruby.rb +4 -1
- metadata +6 -3
- data/lib/t_ruby/rbs_generator.rb +0 -69
data/lib/t_ruby/compiler.rb
CHANGED
|
@@ -3,18 +3,29 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
|
|
5
5
|
module TRuby
|
|
6
|
+
# Pattern for method names that supports Unicode characters
|
|
7
|
+
# \p{L} matches any Unicode letter, \p{N} matches any Unicode number
|
|
8
|
+
IDENTIFIER_CHAR = '[\p{L}\p{N}_]'
|
|
9
|
+
METHOD_NAME_PATTERN = "#{IDENTIFIER_CHAR}+[?!]?".freeze
|
|
10
|
+
# Visibility modifiers for method definitions
|
|
11
|
+
VISIBILITY_PATTERN = '(?:(?:private|protected|public)\s+)?'
|
|
12
|
+
|
|
6
13
|
class Compiler
|
|
7
|
-
attr_reader :declaration_loader, :
|
|
14
|
+
attr_reader :declaration_loader, :optimizer
|
|
8
15
|
|
|
9
|
-
def initialize(config = nil,
|
|
16
|
+
def initialize(config = nil, optimize: true)
|
|
10
17
|
@config = config || Config.new
|
|
11
|
-
@use_ir = use_ir
|
|
12
18
|
@optimize = optimize
|
|
13
19
|
@declaration_loader = DeclarationLoader.new
|
|
14
|
-
@optimizer = IR::Optimizer.new if
|
|
20
|
+
@optimizer = IR::Optimizer.new if optimize
|
|
21
|
+
@type_inferrer = ASTTypeInferrer.new if type_check?
|
|
15
22
|
setup_declaration_paths if @config
|
|
16
23
|
end
|
|
17
24
|
|
|
25
|
+
def type_check?
|
|
26
|
+
@config.type_check?
|
|
27
|
+
end
|
|
28
|
+
|
|
18
29
|
def compile(input_path)
|
|
19
30
|
unless File.exist?(input_path)
|
|
20
31
|
raise ArgumentError, "File not found: #{input_path}"
|
|
@@ -32,11 +43,16 @@ module TRuby
|
|
|
32
43
|
source = File.read(input_path)
|
|
33
44
|
|
|
34
45
|
# Parse with IR support
|
|
35
|
-
parser = Parser.new(source
|
|
36
|
-
|
|
46
|
+
parser = Parser.new(source)
|
|
47
|
+
parser.parse
|
|
48
|
+
|
|
49
|
+
# Run type checking if enabled
|
|
50
|
+
if type_check? && parser.ir_program
|
|
51
|
+
check_types(parser.ir_program, input_path)
|
|
52
|
+
end
|
|
37
53
|
|
|
38
54
|
# Transform source to Ruby code
|
|
39
|
-
output =
|
|
55
|
+
output = transform_with_ir(source, parser)
|
|
40
56
|
|
|
41
57
|
# Compute output path (respects preserve_structure setting)
|
|
42
58
|
output_path = compute_output_path(input_path, @config.ruby_dir, ".rb")
|
|
@@ -48,11 +64,7 @@ module TRuby
|
|
|
48
64
|
if @config.compiler["generate_rbs"]
|
|
49
65
|
rbs_path = compute_output_path(input_path, @config.rbs_dir, ".rbs")
|
|
50
66
|
FileUtils.mkdir_p(File.dirname(rbs_path))
|
|
51
|
-
|
|
52
|
-
generate_rbs_from_ir_to_path(rbs_path, parser.ir_program)
|
|
53
|
-
else
|
|
54
|
-
generate_rbs_file_to_path(rbs_path, parse_result)
|
|
55
|
-
end
|
|
67
|
+
generate_rbs_from_ir_to_path(rbs_path, parser.ir_program)
|
|
56
68
|
end
|
|
57
69
|
|
|
58
70
|
# Generate .d.trb file if enabled in config (legacy support)
|
|
@@ -72,25 +84,17 @@ module TRuby
|
|
|
72
84
|
def compile_string(source, options = {})
|
|
73
85
|
generate_rbs = options.fetch(:rbs, true)
|
|
74
86
|
|
|
75
|
-
parser = Parser.new(source
|
|
76
|
-
|
|
87
|
+
parser = Parser.new(source)
|
|
88
|
+
parser.parse
|
|
77
89
|
|
|
78
90
|
# Transform source to Ruby code
|
|
79
|
-
ruby_output =
|
|
91
|
+
ruby_output = transform_with_ir(source, parser)
|
|
80
92
|
|
|
81
93
|
# Generate RBS if requested
|
|
82
94
|
rbs_output = ""
|
|
83
|
-
if generate_rbs
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
rbs_output = generator.generate(parser.ir_program)
|
|
87
|
-
else
|
|
88
|
-
generator = RBSGenerator.new
|
|
89
|
-
rbs_output = generator.generate(
|
|
90
|
-
parse_result[:functions] || [],
|
|
91
|
-
parse_result[:type_aliases] || []
|
|
92
|
-
)
|
|
93
|
-
end
|
|
95
|
+
if generate_rbs && parser.ir_program
|
|
96
|
+
generator = IR::RBSGenerator.new
|
|
97
|
+
rbs_output = generator.generate(parser.ir_program)
|
|
94
98
|
end
|
|
95
99
|
|
|
96
100
|
{
|
|
@@ -119,7 +123,7 @@ module TRuby
|
|
|
119
123
|
end
|
|
120
124
|
|
|
121
125
|
source = File.read(input_path)
|
|
122
|
-
parser = Parser.new(source
|
|
126
|
+
parser = Parser.new(source)
|
|
123
127
|
parser.parse
|
|
124
128
|
parser.ir_program
|
|
125
129
|
end
|
|
@@ -215,6 +219,140 @@ module TRuby
|
|
|
215
219
|
|
|
216
220
|
private
|
|
217
221
|
|
|
222
|
+
# Check types in IR program and raise TypeCheckError if mismatches found
|
|
223
|
+
# @param ir_program [IR::Program] IR program to check
|
|
224
|
+
# @param file_path [String] source file path for error messages
|
|
225
|
+
def check_types(ir_program, file_path)
|
|
226
|
+
ir_program.declarations.each do |decl|
|
|
227
|
+
case decl
|
|
228
|
+
when IR::MethodDef
|
|
229
|
+
check_method_return_type(decl, nil, file_path)
|
|
230
|
+
when IR::ClassDecl
|
|
231
|
+
decl.body.each do |member|
|
|
232
|
+
check_method_return_type(member, decl, file_path) if member.is_a?(IR::MethodDef)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Check if method's inferred return type matches declared return type
|
|
239
|
+
# @param method [IR::MethodDef] method to check
|
|
240
|
+
# @param class_def [IR::ClassDef, nil] containing class if any
|
|
241
|
+
# @param file_path [String] source file path for error messages
|
|
242
|
+
def check_method_return_type(method, class_def, file_path)
|
|
243
|
+
# Skip if no explicit return type annotation
|
|
244
|
+
return unless method.return_type
|
|
245
|
+
|
|
246
|
+
declared_type = normalize_type(method.return_type.to_rbs)
|
|
247
|
+
|
|
248
|
+
# Create type environment for the class context
|
|
249
|
+
class_env = create_class_env(class_def) if class_def
|
|
250
|
+
|
|
251
|
+
# Infer actual return type
|
|
252
|
+
inferred_type = @type_inferrer.infer_method_return_type(method, class_env)
|
|
253
|
+
inferred_type = normalize_type(inferred_type || "nil")
|
|
254
|
+
|
|
255
|
+
# Check compatibility
|
|
256
|
+
return if types_compatible?(inferred_type, declared_type)
|
|
257
|
+
|
|
258
|
+
location = method.location ? "#{file_path}:#{method.location}" : file_path
|
|
259
|
+
method_name = class_def ? "#{class_def.name}##{method.name}" : method.name
|
|
260
|
+
|
|
261
|
+
raise TypeCheckError.new(
|
|
262
|
+
message: "Return type mismatch in method '#{method_name}': " \
|
|
263
|
+
"declared '#{declared_type}' but inferred '#{inferred_type}'",
|
|
264
|
+
location: location,
|
|
265
|
+
expected: declared_type,
|
|
266
|
+
actual: inferred_type
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Create type environment for class context
|
|
271
|
+
# @param class_def [IR::ClassDecl] class declaration
|
|
272
|
+
# @return [TypeEnv] type environment with instance variables
|
|
273
|
+
def create_class_env(class_def)
|
|
274
|
+
env = TypeEnv.new
|
|
275
|
+
|
|
276
|
+
# Register instance variables from class
|
|
277
|
+
class_def.instance_vars&.each do |ivar|
|
|
278
|
+
type = ivar.type_annotation&.to_rbs || "untyped"
|
|
279
|
+
env.define_instance_var(ivar.name, type)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
env
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Normalize type string for comparison
|
|
286
|
+
# @param type [String] type string
|
|
287
|
+
# @return [String] normalized type string
|
|
288
|
+
def normalize_type(type)
|
|
289
|
+
return "untyped" if type.nil?
|
|
290
|
+
|
|
291
|
+
normalized = type.to_s.strip
|
|
292
|
+
|
|
293
|
+
# Normalize boolean types (bool/Boolean/TrueClass/FalseClass -> bool)
|
|
294
|
+
case normalized
|
|
295
|
+
when "Boolean", "TrueClass", "FalseClass"
|
|
296
|
+
"bool"
|
|
297
|
+
else
|
|
298
|
+
normalized
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Check if inferred type is compatible with declared type
|
|
303
|
+
# @param inferred [String] inferred type
|
|
304
|
+
# @param declared [String] declared type
|
|
305
|
+
# @return [Boolean] true if compatible
|
|
306
|
+
def types_compatible?(inferred, declared)
|
|
307
|
+
# Exact match
|
|
308
|
+
return true if inferred == declared
|
|
309
|
+
|
|
310
|
+
# untyped is compatible with anything
|
|
311
|
+
return true if inferred == "untyped" || declared == "untyped"
|
|
312
|
+
|
|
313
|
+
# void is compatible with anything (no return value check)
|
|
314
|
+
return true if declared == "void"
|
|
315
|
+
|
|
316
|
+
# nil is compatible with nullable types
|
|
317
|
+
return true if inferred == "nil" && declared.end_with?("?")
|
|
318
|
+
|
|
319
|
+
# Subtype relationships
|
|
320
|
+
return true if subtype_of?(inferred, declared)
|
|
321
|
+
|
|
322
|
+
# Handle union types in declared
|
|
323
|
+
if declared.include?("|")
|
|
324
|
+
declared_types = declared.split("|").map(&:strip)
|
|
325
|
+
return true if declared_types.include?(inferred)
|
|
326
|
+
return true if declared_types.any? { |t| types_compatible?(inferred, t) }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Handle union types in inferred - all must be compatible
|
|
330
|
+
if inferred.include?("|")
|
|
331
|
+
inferred_types = inferred.split("|").map(&:strip)
|
|
332
|
+
return inferred_types.all? { |t| types_compatible?(t, declared) }
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
false
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Check if subtype is a subtype of supertype
|
|
339
|
+
# @param subtype [String] potential subtype
|
|
340
|
+
# @param supertype [String] potential supertype
|
|
341
|
+
# @return [Boolean] true if subtype
|
|
342
|
+
def subtype_of?(subtype, supertype)
|
|
343
|
+
# Handle nullable - X is subtype of X?
|
|
344
|
+
return true if supertype.end_with?("?") && supertype[0..-2] == subtype
|
|
345
|
+
|
|
346
|
+
# Numeric hierarchy
|
|
347
|
+
return true if subtype == "Integer" && supertype == "Numeric"
|
|
348
|
+
return true if subtype == "Float" && supertype == "Numeric"
|
|
349
|
+
|
|
350
|
+
# Object is supertype of everything
|
|
351
|
+
return true if supertype == "Object"
|
|
352
|
+
|
|
353
|
+
false
|
|
354
|
+
end
|
|
355
|
+
|
|
218
356
|
# Resolve path to absolute path, following symlinks
|
|
219
357
|
# Falls back to expand_path if realpath fails (e.g., file doesn't exist yet)
|
|
220
358
|
def resolve_path(path)
|
|
@@ -231,10 +369,10 @@ module TRuby
|
|
|
231
369
|
@declaration_loader.add_search_path("./lib/types")
|
|
232
370
|
end
|
|
233
371
|
|
|
234
|
-
# Transform using IR system
|
|
372
|
+
# Transform using IR system
|
|
235
373
|
def transform_with_ir(source, parser)
|
|
236
374
|
ir_program = parser.ir_program
|
|
237
|
-
return
|
|
375
|
+
return source unless ir_program
|
|
238
376
|
|
|
239
377
|
# Run optimization passes if enabled
|
|
240
378
|
if @optimize && @optimizer
|
|
@@ -247,33 +385,15 @@ module TRuby
|
|
|
247
385
|
generator.generate_with_source(ir_program, source)
|
|
248
386
|
end
|
|
249
387
|
|
|
250
|
-
# Legacy transformation using TypeErasure (backward compatible)
|
|
251
|
-
def transform_legacy(source, parse_result)
|
|
252
|
-
if parse_result[:type] == :success
|
|
253
|
-
eraser = TypeErasure.new(source)
|
|
254
|
-
eraser.erase
|
|
255
|
-
else
|
|
256
|
-
source
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
388
|
# Generate RBS from IR to a specific path
|
|
261
389
|
def generate_rbs_from_ir_to_path(rbs_path, ir_program)
|
|
390
|
+
return unless ir_program
|
|
391
|
+
|
|
262
392
|
generator = IR::RBSGenerator.new
|
|
263
393
|
rbs_content = generator.generate(ir_program)
|
|
264
394
|
File.write(rbs_path, rbs_content) unless rbs_content.strip.empty?
|
|
265
395
|
end
|
|
266
396
|
|
|
267
|
-
# Legacy RBS generation to a specific path
|
|
268
|
-
def generate_rbs_file_to_path(rbs_path, parse_result)
|
|
269
|
-
generator = RBSGenerator.new
|
|
270
|
-
rbs_content = generator.generate(
|
|
271
|
-
parse_result[:functions] || [],
|
|
272
|
-
parse_result[:type_aliases] || []
|
|
273
|
-
)
|
|
274
|
-
File.write(rbs_path, rbs_content) unless rbs_content.empty?
|
|
275
|
-
end
|
|
276
|
-
|
|
277
397
|
def generate_dtrb_file(input_path, out_dir)
|
|
278
398
|
dtrb_path = compute_output_path(input_path, out_dir, DeclarationGenerator::DECLARATION_EXTENSION)
|
|
279
399
|
FileUtils.mkdir_p(File.dirname(dtrb_path))
|
|
@@ -362,7 +482,8 @@ module TRuby
|
|
|
362
482
|
result = source.dup
|
|
363
483
|
|
|
364
484
|
# Match function definitions and remove type annotations from parameters
|
|
365
|
-
|
|
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|
|
|
366
487
|
indent = ::Regexp.last_match(1)
|
|
367
488
|
params = ::Regexp.last_match(2)
|
|
368
489
|
close_paren = ::Regexp.last_match(3)
|
|
@@ -409,10 +530,13 @@ module TRuby
|
|
|
409
530
|
params.join(", ")
|
|
410
531
|
end
|
|
411
532
|
|
|
412
|
-
# Clean a single parameter (remove type annotation)
|
|
533
|
+
# Clean a single parameter (remove type annotation, preserve default value)
|
|
413
534
|
def clean_param(param)
|
|
414
|
-
# Match: name: Type
|
|
415
|
-
if (match = param.match(/^(
|
|
535
|
+
# Match: name: Type = value (with default value)
|
|
536
|
+
if (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:\s*.+?\s*(=\s*.+)$/))
|
|
537
|
+
"#{match[1]} #{match[2]}"
|
|
538
|
+
# Match: name: Type (without default value)
|
|
539
|
+
elsif (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:/))
|
|
416
540
|
match[1]
|
|
417
541
|
else
|
|
418
542
|
param
|
|
@@ -431,11 +555,4 @@ module TRuby
|
|
|
431
555
|
result
|
|
432
556
|
end
|
|
433
557
|
end
|
|
434
|
-
|
|
435
|
-
# Legacy Compiler for backward compatibility (no IR)
|
|
436
|
-
class LegacyCompiler < Compiler
|
|
437
|
-
def initialize(config)
|
|
438
|
-
super(config, use_ir: false, optimize: false)
|
|
439
|
-
end
|
|
440
|
-
end
|
|
441
558
|
end
|
data/lib/t_ruby/config.rb
CHANGED
|
@@ -24,6 +24,7 @@ module TRuby
|
|
|
24
24
|
"compiler" => {
|
|
25
25
|
"strictness" => "standard",
|
|
26
26
|
"generate_rbs" => true,
|
|
27
|
+
"type_check" => true,
|
|
27
28
|
"target_ruby" => "3.0",
|
|
28
29
|
"experimental" => [],
|
|
29
30
|
"checks" => {
|
|
@@ -89,6 +90,12 @@ module TRuby
|
|
|
89
90
|
@compiler["generate_rbs"] != false
|
|
90
91
|
end
|
|
91
92
|
|
|
93
|
+
# Check if type checking is enabled
|
|
94
|
+
# @return [Boolean] true if type checking is enabled (default: true)
|
|
95
|
+
def type_check?
|
|
96
|
+
@compiler["type_check"] != false
|
|
97
|
+
end
|
|
98
|
+
|
|
92
99
|
# Get target Ruby version
|
|
93
100
|
# @return [String] target Ruby version (e.g., "3.0", "3.2")
|
|
94
101
|
def target_ruby
|
|
@@ -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
|
@@ -83,15 +83,16 @@ module TRuby
|
|
|
83
83
|
|
|
84
84
|
# Class declaration
|
|
85
85
|
class ClassDecl < Node
|
|
86
|
-
attr_accessor :name, :superclass, :implements, :type_params, :body
|
|
86
|
+
attr_accessor :name, :superclass, :implements, :type_params, :body, :instance_vars
|
|
87
87
|
|
|
88
|
-
def initialize(name:, superclass: nil, implements: [], type_params: [], body: [], **opts)
|
|
88
|
+
def initialize(name:, superclass: nil, implements: [], type_params: [], body: [], instance_vars: [], **opts)
|
|
89
89
|
super(**opts)
|
|
90
90
|
@name = name
|
|
91
91
|
@superclass = superclass
|
|
92
92
|
@implements = implements
|
|
93
93
|
@type_params = type_params
|
|
94
94
|
@body = body
|
|
95
|
+
@instance_vars = instance_vars
|
|
95
96
|
end
|
|
96
97
|
|
|
97
98
|
def children
|
|
@@ -99,6 +100,17 @@ module TRuby
|
|
|
99
100
|
end
|
|
100
101
|
end
|
|
101
102
|
|
|
103
|
+
# Instance variable declaration
|
|
104
|
+
class InstanceVariable < Node
|
|
105
|
+
attr_accessor :name, :type_annotation
|
|
106
|
+
|
|
107
|
+
def initialize(name:, type_annotation: nil, **opts)
|
|
108
|
+
super(**opts)
|
|
109
|
+
@name = name
|
|
110
|
+
@type_annotation = type_annotation
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
102
114
|
# Module declaration
|
|
103
115
|
class ModuleDecl < Node
|
|
104
116
|
attr_accessor :name, :body
|
|
@@ -686,6 +698,11 @@ module TRuby
|
|
|
686
698
|
declarations << build_interface(interface_info)
|
|
687
699
|
end
|
|
688
700
|
|
|
701
|
+
# Build classes
|
|
702
|
+
(parse_result[:classes] || []).each do |class_info|
|
|
703
|
+
declarations << build_class(class_info)
|
|
704
|
+
end
|
|
705
|
+
|
|
689
706
|
# Build functions/methods
|
|
690
707
|
(parse_result[:functions] || []).each do |func_info|
|
|
691
708
|
declarations << build_method(func_info)
|
|
@@ -724,6 +741,28 @@ module TRuby
|
|
|
724
741
|
)
|
|
725
742
|
end
|
|
726
743
|
|
|
744
|
+
def build_class(info)
|
|
745
|
+
# Build methods
|
|
746
|
+
methods = (info[:methods] || []).map do |method_info|
|
|
747
|
+
build_method(method_info)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# Build instance variables
|
|
751
|
+
instance_vars = (info[:instance_vars] || []).map do |ivar|
|
|
752
|
+
InstanceVariable.new(
|
|
753
|
+
name: ivar[:name],
|
|
754
|
+
type_annotation: ivar[:type] ? parse_type(ivar[:type]) : nil
|
|
755
|
+
)
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
ClassDecl.new(
|
|
759
|
+
name: info[:name],
|
|
760
|
+
superclass: info[:superclass],
|
|
761
|
+
body: methods,
|
|
762
|
+
instance_vars: instance_vars
|
|
763
|
+
)
|
|
764
|
+
end
|
|
765
|
+
|
|
727
766
|
def build_method(info)
|
|
728
767
|
params = (info[:params] || []).map do |param|
|
|
729
768
|
Parameter.new(
|
|
@@ -732,10 +771,15 @@ module TRuby
|
|
|
732
771
|
)
|
|
733
772
|
end
|
|
734
773
|
|
|
774
|
+
# 본문 IR이 있으면 사용 (BodyParser에서 파싱됨)
|
|
775
|
+
body = info[:body_ir]
|
|
776
|
+
|
|
735
777
|
MethodDef.new(
|
|
736
778
|
name: info[:name],
|
|
737
779
|
params: params,
|
|
738
|
-
return_type: info[:return_type] ? parse_type(info[:return_type]) : nil
|
|
780
|
+
return_type: info[:return_type] ? parse_type(info[:return_type]) : nil,
|
|
781
|
+
body: body,
|
|
782
|
+
visibility: info[:visibility] || :public
|
|
739
783
|
)
|
|
740
784
|
end
|
|
741
785
|
|
|
@@ -947,9 +991,12 @@ module TRuby
|
|
|
947
991
|
class RBSGenerator < Visitor
|
|
948
992
|
attr_reader :output
|
|
949
993
|
|
|
950
|
-
def initialize
|
|
994
|
+
def initialize(enable_inference: true)
|
|
951
995
|
@output = []
|
|
952
996
|
@indent = 0
|
|
997
|
+
@enable_inference = enable_inference
|
|
998
|
+
@inferrer = TRuby::ASTTypeInferrer.new if enable_inference
|
|
999
|
+
@class_env = nil # 현재 클래스의 타입 환경
|
|
953
1000
|
end
|
|
954
1001
|
|
|
955
1002
|
def generate(program)
|
|
@@ -988,26 +1035,76 @@ module TRuby
|
|
|
988
1035
|
def visit_method_def(node)
|
|
989
1036
|
params = node.params.map do |param|
|
|
990
1037
|
type = param.type_annotation&.to_rbs || "untyped"
|
|
991
|
-
"#{
|
|
1038
|
+
"#{param.name}: #{type}"
|
|
992
1039
|
end.join(", ")
|
|
993
1040
|
|
|
994
|
-
|
|
995
|
-
|
|
1041
|
+
# 반환 타입: 명시적 타입 > 추론된 타입 > untyped
|
|
1042
|
+
return_type = node.return_type&.to_rbs
|
|
1043
|
+
|
|
1044
|
+
# initialize 메서드는 특별 처리: 명시적 타입이 없으면 void
|
|
1045
|
+
# Ruby에서 initialize는 생성자이며, 실제 인스턴스 생성은 Class.new가 담당
|
|
1046
|
+
if node.name == "initialize" && return_type.nil?
|
|
1047
|
+
return_type = "void"
|
|
1048
|
+
elsif return_type.nil? && @enable_inference && @inferrer && node.body
|
|
1049
|
+
# 명시적 반환 타입이 없으면 추론 시도
|
|
1050
|
+
inferred = @inferrer.infer_method_return_type(node, @class_env)
|
|
1051
|
+
return_type = inferred if inferred && inferred != "untyped"
|
|
1052
|
+
end
|
|
1053
|
+
|
|
1054
|
+
return_type ||= "untyped"
|
|
1055
|
+
visibility_prefix = format_visibility(node.visibility)
|
|
1056
|
+
emit("#{visibility_prefix}def #{node.name}: (#{params}) -> #{return_type}")
|
|
996
1057
|
end
|
|
997
1058
|
|
|
998
1059
|
def visit_class_decl(node)
|
|
999
1060
|
emit("class #{node.name}")
|
|
1000
1061
|
@indent += 1
|
|
1062
|
+
|
|
1063
|
+
# 클래스 타입 환경 생성
|
|
1064
|
+
@class_env = TRuby::TypeEnv.new if @enable_inference
|
|
1065
|
+
|
|
1066
|
+
# 인스턴스 변수 타입 등록
|
|
1067
|
+
(node.instance_vars || []).each do |ivar|
|
|
1068
|
+
if @class_env && ivar.type_annotation
|
|
1069
|
+
@class_env.define_instance_var("@#{ivar.name}", ivar.type_annotation.to_rbs)
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
# Emit instance variables first
|
|
1073
|
+
visit_instance_variable(ivar)
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
# Add blank line between ivars and methods if both exist
|
|
1077
|
+
@output << "" if node.instance_vars&.any? && node.body&.any?
|
|
1078
|
+
|
|
1079
|
+
# Emit methods
|
|
1001
1080
|
node.body.each { |member| visit(member) }
|
|
1081
|
+
|
|
1082
|
+
@class_env = nil
|
|
1002
1083
|
@indent -= 1
|
|
1003
1084
|
emit("end")
|
|
1004
1085
|
end
|
|
1005
1086
|
|
|
1087
|
+
def visit_instance_variable(node)
|
|
1088
|
+
type = node.type_annotation&.to_rbs || "untyped"
|
|
1089
|
+
emit("@#{node.name}: #{type}")
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1006
1092
|
private
|
|
1007
1093
|
|
|
1008
1094
|
def emit(text)
|
|
1009
1095
|
@output << ((" " * @indent) + text)
|
|
1010
1096
|
end
|
|
1097
|
+
|
|
1098
|
+
def format_visibility(visibility)
|
|
1099
|
+
# RBS only supports private visibility, not protected
|
|
1100
|
+
# See: https://github.com/ruby/rbs/issues/579
|
|
1101
|
+
case visibility
|
|
1102
|
+
when :private
|
|
1103
|
+
"private "
|
|
1104
|
+
else
|
|
1105
|
+
""
|
|
1106
|
+
end
|
|
1107
|
+
end
|
|
1011
1108
|
end
|
|
1012
1109
|
|
|
1013
1110
|
#==========================================================================
|
data/lib/t_ruby/lsp_server.rb
CHANGED