Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/t_ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@

# Basic components
require_relative "t_ruby/type_alias_registry"
require_relative "t_ruby/heredoc_detector"
require_relative "t_ruby/body_parser"
require_relative "t_ruby/parser"
require_relative "t_ruby/union_type_parser"
require_relative "t_ruby/generic_type_parser"
require_relative "t_ruby/intersection_type_parser"
require_relative "t_ruby/type_erasure"
require_relative "t_ruby/error_handler"
require_relative "t_ruby/rbs_generator"
require_relative "t_ruby/declaration_generator"
require_relative "t_ruby/compiler"
require_relative "t_ruby/lsp_server"
Expand Down
90 changes: 29 additions & 61 deletions lib/t_ruby/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ module TRuby
# \p{L} matches any Unicode letter, \p{N} matches any Unicode number
IDENTIFIER_CHAR = '[\p{L}\p{N}_]'
METHOD_NAME_PATTERN = "#{IDENTIFIER_CHAR}+[?!]?".freeze
# Visibility modifiers for method definitions
VISIBILITY_PATTERN = '(?:(?:private|protected|public)\s+)?'

class Compiler
attr_reader :declaration_loader, :use_ir, :optimizer
attr_reader :declaration_loader, :optimizer

def initialize(config = nil, use_ir: true, optimize: true)
def initialize(config = nil, optimize: true)
@config = config || Config.new
@use_ir = use_ir
@optimize = optimize
@declaration_loader = DeclarationLoader.new
@optimizer = IR::Optimizer.new if use_ir && optimize
@optimizer = IR::Optimizer.new if optimize
@type_inferrer = ASTTypeInferrer.new if type_check?
setup_declaration_paths if @config
end
Expand All @@ -42,16 +43,16 @@ def compile(input_path)
source = File.read(input_path)

# Parse with IR support
parser = Parser.new(source, use_combinator: @use_ir)
parse_result = parser.parse
parser = Parser.new(source)
parser.parse

# Run type checking if enabled
if type_check? && @use_ir && parser.ir_program
if type_check? && parser.ir_program
check_types(parser.ir_program, input_path)
end

# Transform source to Ruby code
output = @use_ir ? transform_with_ir(source, parser) : transform_legacy(source, parse_result)
output = transform_with_ir(source, parser)

# Compute output path (respects preserve_structure setting)
output_path = compute_output_path(input_path, @config.ruby_dir, ".rb")
Expand All @@ -63,11 +64,7 @@ def compile(input_path)
if @config.compiler["generate_rbs"]
rbs_path = compute_output_path(input_path, @config.rbs_dir, ".rbs")
FileUtils.mkdir_p(File.dirname(rbs_path))
if @use_ir && parser.ir_program
generate_rbs_from_ir_to_path(rbs_path, parser.ir_program)
else
generate_rbs_file_to_path(rbs_path, parse_result)
end
generate_rbs_from_ir_to_path(rbs_path, parser.ir_program)
end

# Generate .d.trb file if enabled in config (legacy support)
Expand All @@ -87,25 +84,17 @@ def compile(input_path)
def compile_string(source, options = {})
generate_rbs = options.fetch(:rbs, true)

parser = Parser.new(source, use_combinator: @use_ir)
parse_result = parser.parse
parser = Parser.new(source)
parser.parse

# Transform source to Ruby code
ruby_output = @use_ir ? transform_with_ir(source, parser) : transform_legacy(source, parse_result)
ruby_output = transform_with_ir(source, parser)

# Generate RBS if requested
rbs_output = ""
if generate_rbs
if @use_ir && parser.ir_program
generator = IR::RBSGenerator.new
rbs_output = generator.generate(parser.ir_program)
else
generator = RBSGenerator.new
rbs_output = generator.generate(
parse_result[:functions] || [],
parse_result[:type_aliases] || []
)
end
if generate_rbs && parser.ir_program
generator = IR::RBSGenerator.new
rbs_output = generator.generate(parser.ir_program)
end

{
Expand Down Expand Up @@ -134,7 +123,7 @@ def compile_to_ir(input_path)
end

source = File.read(input_path)
parser = Parser.new(source, use_combinator: true)
parser = Parser.new(source)
parser.parse
parser.ir_program
end
Expand Down Expand Up @@ -380,10 +369,10 @@ def setup_declaration_paths
@declaration_loader.add_search_path("./lib/types")
end

# Transform using IR system (new approach)
# Transform using IR system
def transform_with_ir(source, parser)
ir_program = parser.ir_program
return transform_legacy(source, parser.parse) unless ir_program
return source unless ir_program

# Run optimization passes if enabled
if @optimize && @optimizer
Expand All @@ -396,33 +385,15 @@ def transform_with_ir(source, parser)
generator.generate_with_source(ir_program, source)
end

# Legacy transformation using TypeErasure (backward compatible)
def transform_legacy(source, parse_result)
if parse_result[:type] == :success
eraser = TypeErasure.new(source)
eraser.erase
else
source
end
end

# Generate RBS from IR to a specific path
def generate_rbs_from_ir_to_path(rbs_path, ir_program)
return unless ir_program

generator = IR::RBSGenerator.new
rbs_content = generator.generate(ir_program)
File.write(rbs_path, rbs_content) unless rbs_content.strip.empty?
end

# Legacy RBS generation to a specific path
def generate_rbs_file_to_path(rbs_path, parse_result)
generator = RBSGenerator.new
rbs_content = generator.generate(
parse_result[:functions] || [],
parse_result[:type_aliases] || []
)
File.write(rbs_path, rbs_content) unless rbs_content.empty?
end

def generate_dtrb_file(input_path, out_dir)
dtrb_path = compute_output_path(input_path, out_dir, DeclarationGenerator::DECLARATION_EXTENSION)
FileUtils.mkdir_p(File.dirname(dtrb_path))
Expand Down Expand Up @@ -511,7 +482,8 @@ def erase_parameter_types(source)
result = source.dup

# Match function definitions and remove type annotations from parameters
result.gsub!(/^(\s*def\s+#{TRuby::METHOD_NAME_PATTERN}\s*\()([^)]+)(\)\s*)(?::\s*[^\n]+)?(\s*$)/) do |_match|
# Also supports visibility modifiers: private def, protected def, public def
result.gsub!(/^(\s*#{TRuby::VISIBILITY_PATTERN}def\s+#{TRuby::METHOD_NAME_PATTERN}\s*\()([^)]+)(\)\s*)(?::\s*[^\n]+)?(\s*$)/) do |_match|
indent = ::Regexp.last_match(1)
params = ::Regexp.last_match(2)
close_paren = ::Regexp.last_match(3)
Expand Down Expand Up @@ -558,10 +530,13 @@ def remove_param_types(params_str)
params.join(", ")
end

# Clean a single parameter (remove type annotation)
# Clean a single parameter (remove type annotation, preserve default value)
def clean_param(param)
# Match: name: Type or name (supports Unicode identifiers)
if (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:/))
# Match: name: Type = value (with default value)
if (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:\s*.+?\s*(=\s*.+)$/))
"#{match[1]} #{match[2]}"
# Match: name: Type (without default value)
elsif (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:/))
match[1]
else
param
Expand All @@ -580,11 +555,4 @@ def erase_return_types(source)
result
end
end

# Legacy Compiler for backward compatibility (no IR)
class LegacyCompiler < Compiler
def initialize(config)
super(config, use_ir: false, optimize: false)
end
end
end
74 changes: 74 additions & 0 deletions lib/t_ruby/heredoc_detector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

module TRuby
# Detects regions that should be skipped during parsing:
# - Heredoc content
# - Block comments (=begin/=end)
class HeredocDetector
# Heredoc start patterns:
# <<IDENTIFIER, <<-IDENTIFIER, <<~IDENTIFIER
# <<'IDENTIFIER', <<"IDENTIFIER"
HEREDOC_START_PATTERN = /<<([~-])?(['"]?)(\w+)\2/

# Detect all skippable ranges in lines (heredocs and block comments)
# @param lines [Array<String>] source lines
# @return [Array<Range>] content ranges to skip (0-indexed)
def self.detect(lines)
ranges = []
i = 0

while i < lines.length
line = lines[i]

# Check for =begin block comment
if line.strip == "=begin"
start_line = i
i += 1

# Find =end
while i < lines.length
break if lines[i].strip == "=end"

i += 1
end

# Range covers from =begin to =end (inclusive)
ranges << (start_line..i) if i < lines.length
# Check for heredoc
elsif (match = line.match(HEREDOC_START_PATTERN))
delimiter = match[3]
squiggly = match[1] == "~"
start_line = i
i += 1

# Find closing delimiter
while i < lines.length
# For squiggly heredoc or dash heredoc, delimiter can be indented
# For regular heredoc, delimiter must be at line start
if squiggly || match[1] == "-"
break if lines[i].strip == delimiter
elsif lines[i].chomp == delimiter
break
end
i += 1
end

# Range covers content lines (after start, up to and including end delimiter)
ranges << ((start_line + 1)..i) if i < lines.length
end

i += 1
end

ranges
end

# Check if a line index is inside any skippable region
# @param line_index [Integer] line index to check
# @param heredoc_ranges [Array<Range>] ranges from detect()
# @return [Boolean]
def self.inside_heredoc?(line_index, heredoc_ranges)
heredoc_ranges.any? { |range| range.include?(line_index) }
end
end
end
17 changes: 15 additions & 2 deletions lib/t_ruby/ir.rb
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,8 @@ def build_method(info)
name: info[:name],
params: params,
return_type: info[:return_type] ? parse_type(info[:return_type]) : nil,
body: body
body: body,
visibility: info[:visibility] || :public
)
end

Expand Down Expand Up @@ -1051,7 +1052,8 @@ def visit_method_def(node)
end

return_type ||= "untyped"
emit("def #{node.name}: (#{params}) -> #{return_type}")
visibility_prefix = format_visibility(node.visibility)
emit("#{visibility_prefix}def #{node.name}: (#{params}) -> #{return_type}")
end

def visit_class_decl(node)
Expand Down Expand Up @@ -1092,6 +1094,17 @@ def visit_instance_variable(node)
def emit(text)
@output << ((" " * @indent) + text)
end

def format_visibility(visibility)
# RBS only supports private visibility, not protected
# See: https://github.com/ruby/rbs/issues/579
case visibility
when :private
"private "
else
""
end
end
end

#==========================================================================
Expand Down
2 changes: 1 addition & 1 deletion lib/t_ruby/lsp_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,7 @@ def generate_semantic_tokens(text)
lines = text.split("\n")

# Parse the document to get IR
parser = Parser.new(text, use_combinator: true)
parser = Parser.new(text)
parse_result = parser.parse
parser.ir_program

Expand Down
Loading