From d552e1ecae526c6e5059b4d04ae0e4d26a181375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6wenfels?= <282+dfl@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:54:43 +0000 Subject: [PATCH 1/4] feat: add yield statement support - Add IR::Yield node class with arguments attribute - Implement parse_yield in StatementParser - Add visit_yield in CodeGenerator for Ruby output - Support yield with/without arguments, modifiers Closes #34 Co-Authored-By: Claude Opus 4.5 --- lib/t_ruby/ir.rb | 23 ++++++ .../token/statement_parser.rb | 41 +++++++++++ spec/t_ruby/compiler_spec.rb | 70 +++++++++++++++++++ spec/t_ruby/statement_parser_spec.rb | 66 +++++++++++++++++ 4 files changed, 200 insertions(+) diff --git a/lib/t_ruby/ir.rb b/lib/t_ruby/ir.rb index a700225..df748cf 100644 --- a/lib/t_ruby/ir.rb +++ b/lib/t_ruby/ir.rb @@ -386,6 +386,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 @@ -916,6 +930,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)}") diff --git a/lib/t_ruby/parser_combinator/token/statement_parser.rb b/lib/t_ruby/parser_combinator/token/statement_parser.rb index fa253a0..51570d6 100644 --- a/lib/t_ruby/parser_combinator/token/statement_parser.rb +++ b/lib/t_ruby/parser_combinator/token/statement_parser.rb @@ -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 @@ -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' diff --git a/spec/t_ruby/compiler_spec.rb b/spec/t_ruby/compiler_spec.rb index 61841b1..7b3a7d7 100644 --- a/spec/t_ruby/compiler_spec.rb +++ b/spec/t_ruby/compiler_spec.rb @@ -700,4 +700,74 @@ def error2():String end end end + + describe "yield compilation" do + it "compiles method with yield without arguments to Ruby" do + Dir.mktmpdir do |tmpdir| + input_file = File.join(tmpdir, "test.trb") + File.write(input_file, <<~TRB) + def each_twice + yield + yield + end + TRB + + allow_any_instance_of(TRuby::Config).to receive(:out_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:ruby_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:source_include).and_return([tmpdir]) + + compiler = TRuby::Compiler.new(config) + output_path = compiler.compile(input_file) + + output_content = File.read(output_path) + expect(output_content).to include("def each_twice") + expect(output_content).to include("yield") + expect(output_content.scan("yield").length).to eq(2) + end + end + + it "compiles yield with single argument" do + Dir.mktmpdir do |tmpdir| + input_file = File.join(tmpdir, "test.trb") + File.write(input_file, <<~TRB) + def map_values + yield(1) + yield(2) + end + TRB + + allow_any_instance_of(TRuby::Config).to receive(:out_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:ruby_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:source_include).and_return([tmpdir]) + + compiler = TRuby::Compiler.new(config) + output_path = compiler.compile(input_file) + + output_content = File.read(output_path) + expect(output_content).to include("yield(1)") + expect(output_content).to include("yield(2)") + end + end + + it "compiles yield with multiple arguments" do + Dir.mktmpdir do |tmpdir| + input_file = File.join(tmpdir, "test.trb") + File.write(input_file, <<~TRB) + def each_with_index + yield(item, 0) + end + TRB + + allow_any_instance_of(TRuby::Config).to receive(:out_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:ruby_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:source_include).and_return([tmpdir]) + + compiler = TRuby::Compiler.new(config) + output_path = compiler.compile(input_file) + + output_content = File.read(output_path) + expect(output_content).to include("yield(item, 0)") + end + end + end end diff --git a/spec/t_ruby/statement_parser_spec.rb b/spec/t_ruby/statement_parser_spec.rb index 9bc979d..1adfc9a 100644 --- a/spec/t_ruby/statement_parser_spec.rb +++ b/spec/t_ruby/statement_parser_spec.rb @@ -346,4 +346,70 @@ end end end + + describe "yield statements" do + describe "yield without arguments" do + let(:source) { "yield" } + + it "parses yield without arguments" do + result = parser.parse_statement(tokens, 0) + + expect(result.success?).to be true + expect(result.value).to be_a(TRuby::IR::Yield) + expect(result.value.arguments).to be_empty + end + end + + describe "yield with single argument" do + let(:source) { "yield 42" } + + it "parses yield with expression" do + result = parser.parse_statement(tokens, 0) + + expect(result.success?).to be true + expect(result.value).to be_a(TRuby::IR::Yield) + expect(result.value.arguments.length).to eq(1) + expect(result.value.arguments.first).to be_a(TRuby::IR::Literal) + expect(result.value.arguments.first.value).to eq(42) + end + end + + describe "yield with multiple arguments" do + let(:source) { "yield a, b, c" } + + it "parses yield with multiple arguments" do + result = parser.parse_statement(tokens, 0) + + expect(result.success?).to be true + expect(result.value).to be_a(TRuby::IR::Yield) + expect(result.value.arguments.length).to eq(3) + end + end + + describe "yield with complex expression" do + let(:source) { "yield x + 1" } + + it "parses yield with binary operation" do + result = parser.parse_statement(tokens, 0) + + expect(result.success?).to be true + expect(result.value).to be_a(TRuby::IR::Yield) + expect(result.value.arguments.first).to be_a(TRuby::IR::BinaryOp) + end + end + + describe "yield with modifier" do + let(:source) { "yield x if condition" } + + it "parses yield with if modifier" do + result = parser.parse_statement(tokens, 0) + + expect(result.success?).to be true + expect(result.value).to be_a(TRuby::IR::Conditional) + expect(result.value.kind).to eq(:if) + expect(result.value.then_branch).to be_a(TRuby::IR::Block) + expect(result.value.then_branch.statements.first).to be_a(TRuby::IR::Yield) + end + end + end end From 180e1511cd89e8d01f82415a9fd43323a770df83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6wenfels?= <282+dfl@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:28:59 +0000 Subject: [PATCH 2/4] feat: support yield as expression for assignment context - Add yield parsing in ExpressionParser for `result = yield(x)` pattern - Support parenthesized arguments in expression context - Add expression parser and E2E tests Co-Authored-By: Claude Opus 4.5 --- .../token/expression_parser.rb | 24 +++++++++++++++ spec/t_ruby/compiler_spec.rb | 22 ++++++++++++++ spec/t_ruby/expression_parser_spec.rb | 30 +++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/lib/t_ruby/parser_combinator/token/expression_parser.rb b/lib/t_ruby/parser_combinator/token/expression_parser.rb index 453c97e..2caf277 100644 --- a/lib/t_ruby/parser_combinator/token/expression_parser.rb +++ b/lib/t_ruby/parser_combinator/token/expression_parser.rb @@ -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 @@ -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 diff --git a/spec/t_ruby/compiler_spec.rb b/spec/t_ruby/compiler_spec.rb index 7b3a7d7..98495f8 100644 --- a/spec/t_ruby/compiler_spec.rb +++ b/spec/t_ruby/compiler_spec.rb @@ -769,5 +769,27 @@ def each_with_index expect(output_content).to include("yield(item, 0)") end end + + it "compiles yield as expression (assignment)" do + Dir.mktmpdir do |tmpdir| + input_file = File.join(tmpdir, "test.trb") + File.write(input_file, <<~TRB) + def transform + result = yield(value) + result + end + TRB + + allow_any_instance_of(TRuby::Config).to receive(:out_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:ruby_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:source_include).and_return([tmpdir]) + + compiler = TRuby::Compiler.new(config) + output_path = compiler.compile(input_file) + + output_content = File.read(output_path) + expect(output_content).to include("result = yield(value)") + end + end end end diff --git a/spec/t_ruby/expression_parser_spec.rb b/spec/t_ruby/expression_parser_spec.rb index 13a6db8..9083274 100644 --- a/spec/t_ruby/expression_parser_spec.rb +++ b/spec/t_ruby/expression_parser_spec.rb @@ -516,4 +516,34 @@ expect(result.value.method_name).to eq("[]") end end + + describe "yield expressions" do + it "parses yield without arguments as expression" do + scanner = TRuby::Scanner.new("yield") + result = parser.parse_expression(scanner.scan_all, 0) + + expect(result.success?).to be true + expect(result.value).to be_a(TRuby::IR::Yield) + expect(result.value.arguments).to be_empty + end + + it "parses yield with parenthesized arguments" do + scanner = TRuby::Scanner.new("yield(1, 2)") + result = parser.parse_expression(scanner.scan_all, 0) + + expect(result.success?).to be true + expect(result.value).to be_a(TRuby::IR::Yield) + expect(result.value.arguments.length).to eq(2) + end + + it "parses yield in assignment context" do + # This tests that yield can appear on RHS of assignment + scanner = TRuby::Scanner.new("yield(x)") + result = parser.parse_expression(scanner.scan_all, 0) + + expect(result.success?).to be true + expect(result.value).to be_a(TRuby::IR::Yield) + expect(result.value.arguments.length).to eq(1) + end + end end From c77f8482ee1234caaaa866dfc9d74fe37703bd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6wenfels?= <282+dfl@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:48:31 +0000 Subject: [PATCH 3/4] feat: add yield argument type checking (not just count) - Extend check_yield_arguments to verify types match block signature - Infer yield argument types using ASTTypeInferrer - Create method type environment with parameter types - Report type mismatch errors with expected vs actual types Example error: def each(&block: Proc(Integer) -> void) yield "hello" # Error: expected 'Integer' but got 'String' Co-Authored-By: Claude Opus 4.5 --- lib/t_ruby/compiler.rb | 107 ++++++++++++++++++++++++++++++++++ spec/t_ruby/compiler_spec.rb | 110 +++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+) diff --git a/lib/t_ruby/compiler.rb b/lib/t_ruby/compiler.rb index 49ad454..cc88c10 100644 --- a/lib/t_ruby/compiler.rb +++ b/lib/t_ruby/compiler.rb @@ -422,6 +422,113 @@ 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] 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 diff --git a/spec/t_ruby/compiler_spec.rb b/spec/t_ruby/compiler_spec.rb index 98495f8..349dc24 100644 --- a/spec/t_ruby/compiler_spec.rb +++ b/spec/t_ruby/compiler_spec.rb @@ -384,6 +384,116 @@ def add(a: Integer, b: Integer): Integer end.to raise_error(TRuby::TypeCheckError) end end + + it "raises TypeCheckError when yield argument count mismatches block signature" do + Dir.mktmpdir do |tmpdir| + input_file = File.join(tmpdir, "yield_mismatch.trb") + File.write(input_file, <<~RUBY) + def each(&block: Proc(Integer, String) -> void): void + yield 1 + end + RUBY + + allow_any_instance_of(TRuby::Config).to receive(:out_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:ruby_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:source_include).and_return([tmpdir]) + allow_any_instance_of(TRuby::Config).to receive(:compiler).and_return({ "generate_rbs" => false }) + allow_any_instance_of(TRuby::Config).to receive(:type_check?).and_return(true) + + compiler = TRuby::Compiler.new(config) + + error = nil + begin + compiler.compile(input_file) + rescue TRuby::TypeCheckError => e + error = e + end + + expect(error).to be_a(TRuby::TypeCheckError) + expect(error.message).to include("yield") + expect(error.message).to include("2 argument") + expect(error.message).to include("1 argument") + end + end + + it "passes when yield argument count matches block signature" do + Dir.mktmpdir do |tmpdir| + input_file = File.join(tmpdir, "yield_match.trb") + File.write(input_file, <<~RUBY) + def each(&block: Proc(Integer) -> void): void + yield 1 + yield 2 + end + RUBY + + allow_any_instance_of(TRuby::Config).to receive(:out_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:ruby_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:source_include).and_return([tmpdir]) + allow_any_instance_of(TRuby::Config).to receive(:compiler).and_return({ "generate_rbs" => false }) + allow_any_instance_of(TRuby::Config).to receive(:type_check?).and_return(true) + + compiler = TRuby::Compiler.new(config) + + expect do + compiler.compile(input_file) + end.not_to raise_error + end + end + + it "raises TypeCheckError when yield argument type mismatches block signature" do + Dir.mktmpdir do |tmpdir| + input_file = File.join(tmpdir, "yield_type_mismatch.trb") + File.write(input_file, <<~RUBY) + def each(&block: Proc(Integer) -> void): void + yield "not an integer" + end + RUBY + + allow_any_instance_of(TRuby::Config).to receive(:out_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:ruby_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:source_include).and_return([tmpdir]) + allow_any_instance_of(TRuby::Config).to receive(:compiler).and_return({ "generate_rbs" => false }) + allow_any_instance_of(TRuby::Config).to receive(:type_check?).and_return(true) + + compiler = TRuby::Compiler.new(config) + + error = nil + begin + compiler.compile(input_file) + rescue TRuby::TypeCheckError => e + error = e + end + + expect(error).to be_a(TRuby::TypeCheckError) + expect(error.message).to include("type mismatch") + expect(error.message).to include("Integer") + expect(error.message).to include("String") + end + end + + it "passes when yield argument types match block signature" do + Dir.mktmpdir do |tmpdir| + input_file = File.join(tmpdir, "yield_type_match.trb") + File.write(input_file, <<~RUBY) + def each(&block: Proc(String) -> void): void + yield "hello" + yield "world" + end + RUBY + + allow_any_instance_of(TRuby::Config).to receive(:out_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:ruby_dir).and_return(tmpdir) + allow_any_instance_of(TRuby::Config).to receive(:source_include).and_return([tmpdir]) + allow_any_instance_of(TRuby::Config).to receive(:compiler).and_return({ "generate_rbs" => false }) + allow_any_instance_of(TRuby::Config).to receive(:type_check?).and_return(true) + + compiler = TRuby::Compiler.new(config) + + expect do + compiler.compile(input_file) + end.not_to raise_error + end + end end context "with directory structure preservation" do From 8e038f6b75c90f6c895b2d08090064bb70ca6341 Mon Sep 17 00:00:00 2001 From: "Yonghyun Kim (Freddy)" Date: Thu, 22 Jan 2026 17:36:05 +0900 Subject: [PATCH 4/4] fix: enable yield argument type checking and add block parameter parsing - Add check_yield_arguments calls in check_types method - Add parse_block_parameter method for &block: Proc(T) -> R syntax - Add optional attribute to IR::Parameter for &block? syntax - Add callable_kind attribute to IR::FunctionType for Proc/Lambda - Add Proc(T) -> R and Lambda(T) -> R type syntax parsing - Fix RuboCop empty line issue --- Gemfile.lock | 11 +------ lib/t_ruby/compiler.rb | 7 +++-- lib/t_ruby/ir.rb | 34 +++++++++++++++++---- lib/t_ruby/parser.rb | 31 ++++++++++++++++++- lib/t_ruby/parser_combinator/type_parser.rb | 14 +++++++++ 5 files changed, 78 insertions(+), 19 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2b35833..5c55d5d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,14 +11,9 @@ GEM benchmark (0.5.0) diff-lcs (1.6.2) docile (1.4.1) - ffi (1.17.2) - ffi (1.17.2-x86_64-linux-gnu) json (2.17.1) language_server-protocol (3.17.0.5) lint_roller (1.1.0) - listen (3.9.0) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) parallel (1.27.0) parser (3.3.10.0) @@ -28,9 +23,6 @@ GEM racc (1.8.1) rainbow (3.1.1) rake (13.3.1) - rb-fsevent (0.11.2) - rb-inotify (0.11.1) - ffi (~> 1.0) rbs (3.10.0) logger regexp_parser (2.11.3) @@ -73,14 +65,13 @@ GEM simplecov_json_formatter (0.1.4) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) - unicode-emoji (4.1.0) + unicode-emoji (4.2.0) PLATFORMS ruby x86_64-linux DEPENDENCIES - listen (~> 3.8) rake (~> 13.0) rbs (~> 3.0) rspec (~> 3.0) diff --git a/lib/t_ruby/compiler.rb b/lib/t_ruby/compiler.rb index cc88c10..28013a0 100644 --- a/lib/t_ruby/compiler.rb +++ b/lib/t_ruby/compiler.rb @@ -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 @@ -528,7 +532,6 @@ def find_yields_in_body(node) yields end - # Create type environment for class context # @param class_def [IR::ClassDecl] class declaration # @return [TypeEnv] type environment with instance variables diff --git a/lib/t_ruby/ir.rb b/lib/t_ruby/ir.rb index df748cf..cf58aac 100644 --- a/lib/t_ruby/ir.rb +++ b/lib/t_ruby/ir.rb @@ -147,19 +147,21 @@ def children # Method parameter class Parameter < Node - attr_accessor :name, :type_annotation, :default_value, :kind, :interface_ref + attr_accessor :name, :type_annotation, :default_value, :kind, :interface_ref, :optional # kind: :required, :optional, :rest, :keyrest, :block, :keyword # :keyword - 키워드 인자 (구조분해): { name: String } → def foo(name:) # :keyrest - 더블 스플랫: **opts: Type → def foo(**opts) # interface_ref - interface 참조 타입 (예: }: UserParams 부분) - def initialize(name:, type_annotation: nil, default_value: nil, kind: :required, interface_ref: nil, **opts) + # optional - for block params: true if block is optional (&block?) + def initialize(name:, type_annotation: nil, default_value: nil, kind: :required, interface_ref: nil, optional: false, **opts) super(**opts) @name = name @type_annotation = type_annotation @default_value = default_value @kind = kind @interface_ref = interface_ref + @optional = optional end end @@ -613,22 +615,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 @@ -825,7 +845,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 diff --git a/lib/t_ruby/parser.rb b/lib/t_ruby/parser.rb index 3b021bf..2dde88a 100644 --- a/lib/t_ruby/parser.rb +++ b/lib/t_ruby/parser.rb @@ -268,7 +268,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 @@ -338,6 +342,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 형태 확인 diff --git a/lib/t_ruby/parser_combinator/type_parser.rb b/lib/t_ruby/parser_combinator/type_parser.rb index 5cce56e..0aaa30c 100644 --- a/lib/t_ruby/parser_combinator/type_parser.rb +++ b/lib/t_ruby/parser_combinator/type_parser.rb @@ -66,6 +66,18 @@ def build_parsers IR::FunctionType.new(param_types: params, return_type: ret) end + # Proc type: Proc(Params) -> ReturnType (Ruby-familiar syntax) + proc_keyword = lexeme(string("Proc")) + proc_type = (proc_keyword >> param_list >> arrow >> type_expr).map do |((_proc, params), _arrow), ret| + IR::FunctionType.new(param_types: params, return_type: ret, callable_kind: :proc) + end + + # Lambda type: Lambda(Params) -> ReturnType + lambda_keyword = lexeme(string("Lambda")) + lambda_type = (lambda_keyword >> param_list >> arrow >> type_expr).map do |((_lambda, params), _arrow), ret| + IR::FunctionType.new(param_types: params, return_type: ret, callable_kind: :lambda) + end + # Tuple type: [Type, Type, ...] tuple_type = ( lexeme(char("[")) >> @@ -75,6 +87,8 @@ def build_parsers # Primary type (before operators) primary_type = choice( + proc_type, + lambda_type, function_type, tuple_type, paren_type,