diff --git a/Gemfile.lock b/Gemfile.lock index 9546987..a7d7f54 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/lib/t_ruby/compiler.rb b/lib/t_ruby/compiler.rb index 49ad454..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 @@ -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] 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/lib/t_ruby/ir.rb b/lib/t_ruby/ir.rb index da04691..fe01ed1 100644 --- a/lib/t_ruby/ir.rb +++ b/lib/t_ruby/ir.rb @@ -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 @@ -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 @@ -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 @@ -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 @@ -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)}") diff --git a/lib/t_ruby/parser.rb b/lib/t_ruby/parser.rb index c0383d2..b1aff99 100644 --- a/lib/t_ruby/parser.rb +++ b/lib/t_ruby/parser.rb @@ -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 @@ -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 형태 확인 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/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/lib/t_ruby/parser_combinator/type_parser.rb b/lib/t_ruby/parser_combinator/type_parser.rb index 28c5255..aea9864 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, ...] or [Type, *Type[]] # Note: Uses lazy reference to @tuple_element which is defined after base_type tuple_type = ( @@ -80,6 +92,8 @@ def build_parsers # Primary type (before operators) primary_type = choice( + proc_type, + lambda_type, function_type, tuple_type, paren_type, diff --git a/spec/t_ruby/compiler_spec.rb b/spec/t_ruby/compiler_spec.rb index 61841b1..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 @@ -700,4 +810,96 @@ 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 + + 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 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