Skip to content
Open
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
3 changes: 1 addition & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ GEM
benchmark (0.5.0)
diff-lcs (1.6.2)
docile (1.4.1)
ffi (1.17.3)
ffi (1.17.3-x86_64-linux-gnu)
ffi (1.17.2)
json (2.17.1)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
Expand Down
112 changes: 111 additions & 1 deletion lib/t_ruby/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -382,9 +382,13 @@ def check_types(ir_program, file_path)
case decl
when IR::MethodDef
check_method_return_type(decl, nil, file_path)
check_yield_arguments(decl, nil, file_path)
when IR::ClassDecl
decl.body.each do |member|
check_method_return_type(member, decl, file_path) if member.is_a?(IR::MethodDef)
next unless member.is_a?(IR::MethodDef)

check_method_return_type(member, decl, file_path)
check_yield_arguments(member, decl, file_path)
end
end
end
Expand Down Expand Up @@ -422,6 +426,112 @@ def check_method_return_type(method, class_def, file_path)
)
end

# Check yield statements match block parameter signature
# @param method [IR::MethodDef] method to check
# @param class_def [IR::ClassDecl, nil] containing class if any
# @param file_path [String] source file path for error messages
def check_yield_arguments(method, class_def, file_path)
# Find block parameter with FunctionType annotation
block_param = method.params.find { |p| p.kind == :block }
return unless block_param&.type_annotation.is_a?(IR::FunctionType)

block_type = block_param.type_annotation
expected_types = block_type.param_types
expected_arg_count = expected_types.length

# Find all yield statements in method body
yields = find_yields_in_body(method.body)
return if yields.empty?

location = method.location ? "#{file_path}:#{method.location}" : file_path
method_name = class_def ? "#{class_def.name}##{method.name}" : method.name

# Create type environment for inference
type_env = create_method_type_env(method, class_def)

yields.each do |yield_node|
actual_arg_count = yield_node.arguments.length

# Check argument count first
if actual_arg_count != expected_arg_count
raise TypeCheckError.new(
message: "Yield argument count mismatch in '#{method_name}': " \
"block expects #{expected_arg_count} argument(s) but yield passes #{actual_arg_count}",
location: location,
expected: "#{expected_arg_count} argument(s)",
actual: "#{actual_arg_count} argument(s)"
)
end

# Check argument types
yield_node.arguments.each_with_index do |arg, idx|
expected_type = normalize_type(expected_types[idx].to_rbs)
inferred_type = infer_yield_arg_type(arg, type_env)

next if inferred_type.nil? || types_compatible?(inferred_type, expected_type)

raise TypeCheckError.new(
message: "Yield argument type mismatch in '#{method_name}' at position #{idx + 1}: " \
"expected '#{expected_type}' but got '#{inferred_type}'",
location: location,
expected: expected_type,
actual: inferred_type
)
end
end
end

# Create type environment for method context
def create_method_type_env(method, class_def)
env = class_def ? create_class_env(class_def) : TypeEnv.new

# Add method parameters to environment
method.params.each do |param|
next if param.kind == :block

param_type = param.type_annotation&.to_rbs || "untyped"
env.define(param.name, param_type)
end

env
end

# Infer type of a yield argument expression
def infer_yield_arg_type(arg, env)
return nil unless @type_inferrer

inferred = @type_inferrer.infer_expression(arg, env)
normalize_type(inferred)
rescue StandardError
nil # If inference fails, skip type checking for this arg
end

# Find all yield nodes in a method body
# @param node [IR::Node] IR node to search
# @return [Array<IR::Yield>] yield nodes found
def find_yields_in_body(node)
yields = []
return yields unless node

case node
when IR::Yield
yields << node
when IR::Block
node.statements.each { |stmt| yields.concat(find_yields_in_body(stmt)) }
when IR::Conditional
yields.concat(find_yields_in_body(node.then_branch))
yields.concat(find_yields_in_body(node.else_branch))
when IR::Loop
yields.concat(find_yields_in_body(node.body))
when IR::Assignment
yields.concat(find_yields_in_body(node.value))
when IR::Return
yields.concat(find_yields_in_body(node.value))
end

yields
end

# Create type environment for class context
# @param class_def [IR::ClassDecl] class declaration
# @return [TypeEnv] type environment with instance variables
Expand Down
57 changes: 51 additions & 6 deletions lib/t_ruby/ir.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,20 +150,22 @@ def children

# Method parameter
class Parameter < Node
attr_accessor :name, :type_annotation, :default_value, :kind, :interface_ref, :type_slot
attr_accessor :name, :type_annotation, :default_value, :kind, :interface_ref, :optional, :type_slot

# kind: :required, :optional, :rest, :keyrest, :block, :keyword
# :keyword - 키워드 인자 (구조분해): { name: String } → def foo(name:)
# :keyrest - 더블 스플랫: **opts: Type → def foo(**opts)
# interface_ref - interface 참조 타입 (예: }: UserParams 부분)
# optional - for block params: true if block is optional (&block?)
# type_slot - TypeSlot for this parameter's type annotation position
def initialize(name:, type_annotation: nil, default_value: nil, kind: :required, interface_ref: nil, type_slot: nil, **opts)
def initialize(name:, type_annotation: nil, default_value: nil, kind: :required, interface_ref: nil, optional: false, type_slot: nil, **opts)
super(**opts)
@name = name
@type_annotation = type_annotation
@default_value = default_value
@kind = kind
@interface_ref = interface_ref
@optional = optional
@type_slot = type_slot
end
end
Expand Down Expand Up @@ -391,6 +393,20 @@ def children
end
end

# Yield statement
class Yield < Node
attr_accessor :arguments

def initialize(arguments: [], **opts)
super(**opts)
@arguments = arguments
end

def children
@arguments
end
end

# Binary operation
class BinaryOp < Node
attr_accessor :operator, :left, :right
Expand Down Expand Up @@ -604,22 +620,40 @@ def to_trb

# Function/Proc type ((String, Integer) -> Boolean)
class FunctionType < TypeNode
attr_accessor :param_types, :return_type
attr_accessor :param_types, :return_type, :callable_kind

def initialize(return_type:, param_types: [], **opts)
# callable_kind: :proc, :lambda, or nil (generic function type)
def initialize(return_type:, param_types: [], callable_kind: nil, **opts)
super(**opts)
@param_types = param_types
@return_type = return_type
@callable_kind = callable_kind
end

def to_rbs
# RBS doesn't distinguish between Proc and Lambda
params = @param_types.map(&:to_rbs).join(", ")
"^(#{params}) -> #{@return_type.to_rbs}"
end

def to_trb
params = @param_types.map(&:to_trb).join(", ")
"(#{params}) -> #{@return_type.to_trb}"
case @callable_kind
when :proc
"Proc(#{params}) -> #{@return_type.to_trb}"
when :lambda
"Lambda(#{params}) -> #{@return_type.to_trb}"
else
"(#{params}) -> #{@return_type.to_trb}"
end
end

def proc?
@callable_kind == :proc
end

def lambda?
@callable_kind == :lambda
end
end

Expand Down Expand Up @@ -873,7 +907,9 @@ def build_method(info)
params = (info[:params] || []).map do |param|
Parameter.new(
name: param[:name],
type_annotation: param[:type] ? parse_type(param[:type]) : nil
type_annotation: param[:ir_type] || (param[:type] ? parse_type(param[:type]) : nil),
kind: param[:kind] || :required,
optional: param[:optional] || false
)
end

Expand Down Expand Up @@ -978,6 +1014,15 @@ def visit_return(node)
end
end

def visit_yield(node)
if node.arguments.empty?
emit("yield")
else
args = node.arguments.map { |a| generate_expression(a) }.join(", ")
emit("yield(#{args})")
end
end

def visit_conditional(node)
keyword = node.kind == :unless ? "unless" : "if"
emit("#{keyword} #{generate_expression(node.condition)}")
Expand Down
31 changes: 30 additions & 1 deletion lib/t_ruby/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,11 @@ def parse_parameters(params_str)
elsif param.match?(/^\w+:\s*\{/)
param_info = parse_hash_literal_parameter(param)
parameters << param_info if param_info
# 4. 일반 위치 인자: name: Type 또는 name: Type = default
# 4. Block parameter: &block or &block: Type
elsif param.start_with?("&")
param_info = parse_block_parameter(param)
parameters << param_info if param_info
# 5. 일반 위치 인자: name: Type 또는 name: Type = default
else
param_info = parse_single_parameter(param)
parameters << param_info if param_info
Expand Down Expand Up @@ -470,6 +474,31 @@ def parse_double_splat_parameter(param)
result
end

# Block parameter parsing: &block or &block: Proc(T) -> R
def parse_block_parameter(param)
# &name or &name? or &name: Type or &name?: Type
match = param.match(/^&(\w+)(\?)?(?::\s*(.+?))?$/)
return nil unless match

param_name = match[1]
optional = !match[2].nil?
type_str = match[3]&.strip

result = {
name: param_name,
type: type_str,
kind: :block,
optional: optional,
}

if type_str
type_result = @type_parser.parse(type_str)
result[:ir_type] = type_result[:type] if type_result[:success]
end

result
end

# 키워드 인자 그룹 파싱: { name: String, age: Integer = 0 } 또는 { name:, age: 0 }: InterfaceName
def parse_keyword_args_group(param)
# { ... }: InterfaceName 형태 확인
Expand Down
24 changes: 24 additions & 0 deletions lib/t_ruby/parser_combinator/token/expression_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@ def parse_primary(tokens, position)
# Hash literal
parse_hash_literal(tokens, position)

when :yield
# Yield expression: yield or yield(args)
parse_yield_expression(tokens, position)

else
TokenParseResult.failure("Unexpected token: #{token.type}", tokens, position)
end
Expand Down Expand Up @@ -533,6 +537,26 @@ def parse_interpolated_string(tokens, position)
TokenParseResult.success(node, tokens, position)
end

# Parse yield as an expression: yield, yield(args), yield arg
def parse_yield_expression(tokens, position)
position += 1 # consume 'yield'

# Check for parenthesized arguments: yield(arg1, arg2)
if tokens[position]&.type == :lparen
args_result = parse_arguments(tokens, position)
return args_result if args_result.failure?

node = IR::Yield.new(arguments: args_result.value)
return TokenParseResult.success(node, tokens, args_result.position)
end

# No arguments or space-separated single argument
# For expression context, we only support yield() or yield without args
# Space-separated args like "yield x" are handled in statement context
node = IR::Yield.new(arguments: [])
TokenParseResult.success(node, tokens, position)
end

def keywords
@keywords ||= TRuby::Scanner::KEYWORDS
end
Expand Down
41 changes: 41 additions & 0 deletions lib/t_ruby/parser_combinator/token/statement_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def parse_statement(tokens, position = 0)
case token.type
when :return
parse_return(tokens, position)
when :yield
parse_yield(tokens, position)
when :if
parse_if(tokens, position)
when :unless
Expand Down Expand Up @@ -95,6 +97,45 @@ def parse_return(tokens, position)
TokenParseResult.success(node, tokens, expr_result.position)
end

def parse_yield(tokens, position)
position += 1 # consume 'yield'

# Check if there are arguments
position = skip_newlines_if_not_modifier(tokens, position)

if position >= tokens.length ||
tokens[position].type == :eof ||
tokens[position].type == :newline ||
end_of_statement?(tokens, position)
node = IR::Yield.new(arguments: [])
return TokenParseResult.success(node, tokens, position)
end

# Parse arguments (comma-separated expressions)
arguments = []
expr_result = @expression_parser.parse_expression(tokens, position)
return expr_result if expr_result.failure?

arguments << expr_result.value
position = expr_result.position

while tokens[position]&.type == :comma
position += 1
expr_result = @expression_parser.parse_expression(tokens, position)
return expr_result if expr_result.failure?

arguments << expr_result.value
position = expr_result.position
end

# Check for modifier
modifier_result = parse_modifier(tokens, position, IR::Yield.new(arguments: arguments))
return modifier_result if modifier_result.success? && modifier_result.value.is_a?(IR::Conditional)

node = IR::Yield.new(arguments: arguments)
TokenParseResult.success(node, tokens, position)
end

def parse_if(tokens, position)
position += 1 # consume 'if'

Expand Down
Loading