diff --git a/lib/t_ruby.rb b/lib/t_ruby.rb index 4f8351d..b78a555 100644 --- a/lib/t_ruby.rb +++ b/lib/t_ruby.rb @@ -9,6 +9,7 @@ # Core infrastructure (must be loaded first) require_relative "t_ruby/string_utils" require_relative "t_ruby/ir" +require_relative "t_ruby/errors/type_slot_error" require_relative "t_ruby/parser_combinator" require_relative "t_ruby/scanner" require_relative "t_ruby/smt_solver" @@ -32,6 +33,9 @@ require_relative "t_ruby/runner" require_relative "t_ruby/cli" +# Type Resolution +require_relative "t_ruby/type_resolution/slot_resolver" + # Milestone 4: Advanced Features require_relative "t_ruby/constraint_checker" require_relative "t_ruby/type_inferencer" diff --git a/lib/t_ruby/errors/type_slot_error.rb b/lib/t_ruby/errors/type_slot_error.rb new file mode 100644 index 0000000..e55cac4 --- /dev/null +++ b/lib/t_ruby/errors/type_slot_error.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +module TRuby + module Errors + # TypeSlotError - Error with context-aware messaging based on TypeSlot + # + # Provides rich error messages with location info, context, and suggestions. + # Supports LSP diagnostic format for IDE integration. + class TypeSlotError < StandardError + attr_reader :type_slot, :original_message + attr_accessor :suggestion + + def initialize(message:, type_slot: nil) + @type_slot = type_slot + @original_message = message + @suggestion = nil + super(message) + end + + # Line number from type_slot location (1-indexed) + def line + type_slot&.location&.[](:line) + end + + # Column number from type_slot location + def column + type_slot&.location&.[](:column) + end + + # Kind of type slot (parameter, return, variable, etc.) + def kind + type_slot&.kind + end + + # Format error message with location and context + def formatted_message + parts = [] + + # Location header + if line && column + parts << "Line #{line}, Column #{column}:" + elsif line + parts << "Line #{line}:" + end + + # Context description + parts << context_description if type_slot + + # Main error message (use original_message to avoid recursion) + parts << @original_message + + # Suggestion if provided + parts << " Suggestion: #{suggestion}" if suggestion + + parts.join("\n") + end + + # Convert to LSP diagnostic format + # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic + def to_lsp_diagnostic + start_line = line ? line - 1 : 0 # LSP uses 0-indexed lines + start_char = column || 0 + + { + range: { + start: { line: start_line, character: start_char }, + end: { line: start_line, character: start_char + 1 }, + }, + message: message, + severity: 1, # 1 = Error + source: "t-ruby", + } + end + + def to_s + formatted_message + end + + private + + def context_description + return nil unless type_slot + + ctx = type_slot.context || {} + + case kind + when :parameter + param_name = ctx[:param_name] || "unknown" + method_name = ctx[:method_name] || "unknown" + "in parameter '#{param_name}' of method '#{method_name}'" + when :return + method_name = ctx[:method_name] || "unknown" + "in return type of method '#{method_name}'" + when :variable + var_name = ctx[:var_name] || "unknown" + "in variable '#{var_name}'" + when :instance_var + var_name = ctx[:var_name] || "unknown" + "in instance variable '#{var_name}'" + when :generic_arg + type_name = ctx[:type_name] || "unknown" + "in generic argument of '#{type_name}'" + end + end + end + end +end diff --git a/lib/t_ruby/ir.rb b/lib/t_ruby/ir.rb index a700225..3093462 100644 --- a/lib/t_ruby/ir.rb +++ b/lib/t_ruby/ir.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "ir/type_slot" + module TRuby module IR # Base class for all IR nodes @@ -128,9 +130,9 @@ def children # Method definition class MethodDef < Node - attr_accessor :name, :params, :return_type, :body, :visibility, :type_params + attr_accessor :name, :params, :return_type, :body, :visibility, :type_params, :return_type_slot - def initialize(name:, params: [], return_type: nil, body: nil, visibility: :public, type_params: [], **opts) + def initialize(name:, params: [], return_type: nil, body: nil, visibility: :public, type_params: [], return_type_slot: nil, **opts) super(**opts) @name = name @params = params @@ -138,6 +140,7 @@ def initialize(name:, params: [], return_type: nil, body: nil, visibility: :publ @body = body @visibility = visibility @type_params = type_params + @return_type_slot = return_type_slot end def children @@ -147,19 +150,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, :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 부분) - def initialize(name:, type_annotation: nil, default_value: nil, kind: :required, interface_ref: nil, **opts) + # 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) super(**opts) @name = name @type_annotation = type_annotation @default_value = default_value @kind = kind @interface_ref = interface_ref + @type_slot = type_slot end end diff --git a/lib/t_ruby/ir/type_slot.rb b/lib/t_ruby/ir/type_slot.rb new file mode 100644 index 0000000..e5d9b54 --- /dev/null +++ b/lib/t_ruby/ir/type_slot.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module TRuby + module IR + # TypeSlot represents a position where a type annotation is expected. + # It tracks explicit, inferred, and resolved types for that position. + # + # @example Parameter type slot + # slot = TypeSlot.new( + # kind: :parameter, + # location: { line: 5, column: 10 }, + # context: { method_name: "greet", param_name: "name" } + # ) + # slot.explicit_type = SimpleType.new(name: "String") + # + class TypeSlot + KINDS = %i[parameter return variable instance_var generic_arg].freeze + + attr_reader :kind, :location, :context + attr_accessor :explicit_type, :inferred_type, :resolved_type + + # @param kind [Symbol] One of KINDS - the type of slot + # @param location [Hash] Location information (line, column) + # @param context [Hash] Additional context for error messages + def initialize(kind:, location:, context: {}) + @kind = kind + @location = location + @context = context + @explicit_type = nil + @inferred_type = nil + @resolved_type = nil + end + + # @return [Boolean] true if this slot needs type inference + def needs_inference? + @explicit_type.nil? + end + + # @return [Hash] Context information for error messages + def error_context + { + kind: @kind, + location: @location, + context: @context, + } + end + + # Returns the best available type, falling back to untyped + # Priority: resolved_type > explicit_type > inferred_type > untyped + # + # @return [TypeNode] The resolved type or untyped + def resolved_type_or_untyped + @resolved_type || @explicit_type || @inferred_type || SimpleType.new(name: "untyped") + end + end + end +end diff --git a/lib/t_ruby/parser.rb b/lib/t_ruby/parser.rb index 3b021bf..76d9aef 100644 --- a/lib/t_ruby/parser.rb +++ b/lib/t_ruby/parser.rb @@ -3,6 +3,13 @@ module TRuby # Enhanced Parser using Parser Combinator for complex type expressions # Maintains backward compatibility with original Parser interface + # + # This class serves as a facade that can delegate to either: + # 1. Legacy regex-based parsing (default) + # 2. New TokenDeclarationParser with TypeSlot support (opt-in) + # + # To use the new parser, set use_token_parser: true or + # set TRUBY_NEW_PARSER=1 environment variable. class Parser # Type names that are recognized as valid VALID_TYPES = %w[String Integer Boolean Array Hash Symbol void nil].freeze @@ -15,21 +22,144 @@ class Parser # Visibility modifiers for method definitions VISIBILITY_PATTERN = '(?:(?:private|protected|public)\s+)?' - # TODO: Replace regex-based parsing with TokenDeclarationParser + # @deprecated The regex-based parsing will be replaced by TokenDeclarationParser. # See: lib/t_ruby/parser_combinator/token/token_declaration_parser.rb attr_reader :source, :ir_program - def initialize(source, parse_body: true) + def initialize(source, parse_body: true, use_token_parser: nil) @source = source @lines = source.split("\n") @parse_body = parse_body + @use_token_parser = use_token_parser @type_parser = ParserCombinator::TypeParser.new @body_parser = ParserCombinator::TokenBodyParser.new if parse_body @ir_program = nil end def parse + if use_token_parser? + parse_with_token_parser + else + parse_with_legacy_parser + end + rescue Scanner::ScanError => e + raise ParseError.new(e.message, line: e.line, column: e.column) + end + + private + + # Check if token parser should be used + def use_token_parser? + return @use_token_parser unless @use_token_parser.nil? + + ENV["TRUBY_NEW_PARSER"] == "1" + end + + # Parse using the new TokenDeclarationParser with TypeSlot support + def parse_with_token_parser + scanner = Scanner.new(@source) + tokens = scanner.scan_all + token_parser = ParserCombinator::TokenDeclarationParser.new + + program_result = token_parser.parse_program(tokens) + + if program_result.success? + @ir_program = program_result.value + convert_ir_to_legacy_format(@ir_program) + else + raise ParseError.new( + program_result.error, + line: token_parser.errors.first&.line, + column: token_parser.errors.first&.column + ) + end + end + + # Convert IR::Program to legacy hash format for backward compatibility + def convert_ir_to_legacy_format(program) + functions = [] + type_aliases = [] + interfaces = [] + classes = [] + + program.declarations.each do |decl| + case decl + when IR::MethodDef + functions << convert_method_to_legacy(decl) + when IR::TypeAlias + type_aliases << convert_type_alias_to_legacy(decl) + when IR::Interface + interfaces << convert_interface_to_legacy(decl) + when IR::ClassDecl + classes << convert_class_to_legacy(decl) + end + end + + { + type: :success, + functions: functions, + type_aliases: type_aliases, + interfaces: interfaces, + classes: classes, + } + end + + def convert_method_to_legacy(method_def) + params = method_def.params.map do |param| + { + name: param.name, + type: param.type_annotation&.to_s, + ir_type: param.type_annotation, + kind: param.kind || :required, + } + end + + { + name: method_def.name, + params: params, + return_type: method_def.return_type&.to_s, + ir_return_type: method_def.return_type, + visibility: method_def.visibility || :public, + body_ir: method_def.body, + } + end + + def convert_type_alias_to_legacy(type_alias) + { + name: type_alias.name, + definition: type_alias.definition&.to_s, + ir_type: type_alias.definition, + } + end + + def convert_interface_to_legacy(interface_def) + members = interface_def.members.map do |member| + { + name: member[:name] || member.name, + type: member[:type]&.to_s || member.type&.to_s, + ir_type: member[:type] || member.type, + } + end + + { name: interface_def.name, members: members } + end + + def convert_class_to_legacy(class_def) + methods = (class_def.body || []).select { |d| d.is_a?(IR::MethodDef) }.map do |m| + convert_method_to_legacy(m) + end + + { + name: class_def.name, + superclass: class_def.superclass, + methods: methods, + instance_vars: [], + } + end + + # @deprecated Legacy regex-based parser. Will be removed in future version. + def parse_with_legacy_parser functions = [] type_aliases = [] interfaces = [] @@ -100,10 +230,10 @@ def parse @ir_program = builder.build(result, source: @source) result - rescue Scanner::ScanError => e - raise ParseError.new(e.message, line: e.line, column: e.column) end + public + # Parse to IR directly (new API) def parse_to_ir parse unless @ir_program diff --git a/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb b/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb index 5d3676a..f061c04 100644 --- a/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb +++ b/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb @@ -193,7 +193,7 @@ def parse_method_def(tokens, position, visibility: :public) # Parse parameter list unless tokens[position].type == :rparen loop do - param_result = parse_parameter(tokens, position) + param_result = parse_parameter(tokens, position, method_name: method_name) return param_result if param_result.failure? # Handle keyword args group which returns an array @@ -215,10 +215,14 @@ def parse_method_def(tokens, position, visibility: :public) position += 1 end + # Track return type location for TypeSlot + return_type_location = { line: def_line, column: def_column } + # Parse return type return_type = nil if position < tokens.length && tokens[position].type == :colon colon_token = tokens[position] + return_type_location = { line: colon_token.line, column: colon_token.column } # Check: no space allowed before colon (method name or ) must be adjacent to :) prev_token = tokens[position - 1] @@ -288,13 +292,22 @@ def parse_method_def(tokens, position, visibility: :public) position += 1 end + # Create return_type_slot for TypeSlot infrastructure + return_type_slot = IR::TypeSlot.new( + kind: :return, + location: return_type_location, + context: { method_name: method_name } + ) + return_type_slot.explicit_type = return_type if return_type + node = IR::MethodDef.new( name: method_name, params: params, return_type: return_type, body: body_result.value, visibility: visibility, - location: "#{def_line}:#{def_column}" + location: "#{def_line}:#{def_column}", + return_type_slot: return_type_slot ) TokenParseResult.success(node, tokens, position) end @@ -310,21 +323,27 @@ def parse_visibility_method(tokens, position) end end - def parse_parameter(tokens, position) + def parse_parameter(tokens, position, method_name: nil) return TokenParseResult.failure("Expected parameter", tokens, position) if position >= tokens.length + # Capture location for TypeSlot + param_token = tokens[position] + param_location = { line: param_token.line, column: param_token.column } + # Check for different parameter types case tokens[position].type when :lbrace # Keyword args group: { name: Type, age: Type = default } - return parse_keyword_args_group(tokens, position) + return parse_keyword_args_group(tokens, position, method_name: method_name) when :star # Splat parameter *args position += 1 return TokenParseResult.failure("Expected parameter name after *", tokens, position) if position >= tokens.length - name = tokens[position].value + name_token = tokens[position] + name = name_token.value + param_location = { line: name_token.line, column: name_token.column } position += 1 # Check for type annotation: *args: Type @@ -338,7 +357,8 @@ def parse_parameter(tokens, position) position = type_result.position end - param = IR::Parameter.new(name: name, kind: :rest, type_annotation: type_annotation) + type_slot = create_param_type_slot(name, type_annotation, param_location, method_name) + param = IR::Parameter.new(name: name, kind: :rest, type_annotation: type_annotation, type_slot: type_slot) return TokenParseResult.success(param, tokens, position) when :star_star @@ -346,7 +366,9 @@ def parse_parameter(tokens, position) position += 1 return TokenParseResult.failure("Expected parameter name after **", tokens, position) if position >= tokens.length - name = tokens[position].value + name_token = tokens[position] + name = name_token.value + param_location = { line: name_token.line, column: name_token.column } position += 1 # Check for type annotation: **opts: Type @@ -360,7 +382,8 @@ def parse_parameter(tokens, position) position = type_result.position end - param = IR::Parameter.new(name: name, kind: :keyrest, type_annotation: type_annotation) + type_slot = create_param_type_slot(name, type_annotation, param_location, method_name) + param = IR::Parameter.new(name: name, kind: :keyrest, type_annotation: type_annotation, type_slot: type_slot) return TokenParseResult.success(param, tokens, position) when :amp @@ -368,7 +391,9 @@ def parse_parameter(tokens, position) position += 1 return TokenParseResult.failure("Expected parameter name after &", tokens, position) if position >= tokens.length - name = tokens[position].value + name_token = tokens[position] + name = name_token.value + param_location = { line: name_token.line, column: name_token.column } position += 1 # Check for type annotation: &block: Type @@ -382,7 +407,8 @@ def parse_parameter(tokens, position) position = type_result.position end - param = IR::Parameter.new(name: name, kind: :block, type_annotation: type_annotation) + type_slot = create_param_type_slot(name, type_annotation, param_location, method_name) + param = IR::Parameter.new(name: name, kind: :block, type_annotation: type_annotation, type_slot: type_slot) return TokenParseResult.success(param, tokens, position) end @@ -415,12 +441,24 @@ def parse_parameter(tokens, position) end kind = default_value ? :optional : :required - param = IR::Parameter.new(name: name, type_annotation: type_annotation, default_value: default_value, kind: kind) + type_slot = create_param_type_slot(name, type_annotation, param_location, method_name) + param = IR::Parameter.new(name: name, type_annotation: type_annotation, default_value: default_value, kind: kind, type_slot: type_slot) TokenParseResult.success(param, tokens, position) end + # Helper to create TypeSlot for parameter + def create_param_type_slot(param_name, type_annotation, location, method_name) + type_slot = IR::TypeSlot.new( + kind: :parameter, + location: location, + context: { method_name: method_name, param_name: param_name } + ) + type_slot.explicit_type = type_annotation if type_annotation + type_slot + end + # Parse keyword args group: { name: Type, age: Type = default } or { name:, age: default }: InterfaceName - def parse_keyword_args_group(tokens, position) + def parse_keyword_args_group(tokens, position, method_name: nil) position += 1 # consume '{' params = [] @@ -432,7 +470,9 @@ def parse_keyword_args_group(tokens, position) # Parse each keyword arg: name: Type or name: Type = default or name: or name: default return TokenParseResult.failure("Expected parameter name", tokens, position) unless tokens[position].type == :identifier - name = tokens[position].value + name_token = tokens[position] + name = name_token.value + param_location = { line: name_token.line, column: name_token.column } position += 1 type_annotation = nil @@ -469,7 +509,12 @@ def parse_keyword_args_group(tokens, position) default_value = true end - params << IR::Parameter.new(name: name, type_annotation: type_annotation, default_value: default_value, kind: :keyword) + type_slot = create_param_type_slot(name, type_annotation, param_location, method_name) + param = IR::Parameter.new( + name: name, type_annotation: type_annotation, default_value: default_value, + kind: :keyword, type_slot: type_slot + ) + params << param # Skip comma if position < tokens.length && tokens[position].type == :comma diff --git a/lib/t_ruby/type_resolution/slot_resolver.rb b/lib/t_ruby/type_resolution/slot_resolver.rb new file mode 100644 index 0000000..109e5f4 --- /dev/null +++ b/lib/t_ruby/type_resolution/slot_resolver.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module TRuby + module TypeResolution + # SlotResolver - Identifies and resolves TypeSlots that need type inference + # + # This class is responsible for: + # 1. Collecting all TypeSlots from an IR program + # 2. Identifying which slots need type inference (no explicit type) + # 3. Resolving slots to appropriate types (untyped for parameters, inferred for others) + class SlotResolver + # Collect all TypeSlots that need inference from a program + # @param program [IR::Program] the parsed program + # @return [Array] slots needing inference + def collect_unresolved_slots(program) + slots = [] + + program.declarations.each do |decl| + collect_from_declaration(decl, slots) + end + + slots.select(&:needs_inference?) + end + + # Resolve a single slot to untyped + # Used for parameters where we can't infer the type + # @param slot [IR::TypeSlot] the slot to resolve + def resolve_to_untyped(slot) + slot.resolved_type = IR::SimpleType.new(name: "untyped") + end + + # Resolve all unresolved slots in a program to untyped + # This is a fallback strategy for gradual typing + # @param program [IR::Program] the parsed program + def resolve_all_untyped(program) + collect_unresolved_slots(program).each do |slot| + resolve_to_untyped(slot) + end + end + + # Get summary statistics about slots in a program + # @param program [IR::Program] the parsed program + # @return [Hash] summary with :total, :explicit, :needs_inference counts + def slot_summary(program) + all_slots = [] + + program.declarations.each do |decl| + collect_from_declaration(decl, all_slots) + end + + { + total: all_slots.size, + explicit: all_slots.count { |s| !s.needs_inference? }, + needs_inference: all_slots.count(&:needs_inference?), + } + end + + private + + # Collect TypeSlots from a single declaration + def collect_from_declaration(decl, slots) + case decl + when IR::MethodDef + collect_from_method(decl, slots) + when IR::ClassDef + decl.body&.each { |d| collect_from_declaration(d, slots) } + when IR::ModuleDef + decl.body&.each { |d| collect_from_declaration(d, slots) } + end + end + + # Collect TypeSlots from a method definition + def collect_from_method(method_def, slots) + # Collect parameter slots + method_def.params.each do |param| + slots << param.type_slot if param.type_slot + end + + # Collect return type slot + slots << method_def.return_type_slot if method_def.return_type_slot + end + end + end +end diff --git a/spec/t_ruby/errors/type_slot_error_spec.rb b/spec/t_ruby/errors/type_slot_error_spec.rb new file mode 100644 index 0000000..44e4cb0 --- /dev/null +++ b/spec/t_ruby/errors/type_slot_error_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe TRuby::Errors::TypeSlotError do + let(:type_slot) do + TRuby::IR::TypeSlot.new( + kind: :parameter, + location: { line: 10, column: 5 }, + context: { method_name: "greet", param_name: "name" } + ) + end + + describe "initialization" do + it "creates error with message and type_slot" do + error = described_class.new( + message: "Expected type annotation", + type_slot: type_slot + ) + + expect(error.message).to include("Expected type annotation") + expect(error.type_slot).to eq(type_slot) + end + + it "creates error without type_slot" do + error = described_class.new(message: "Generic error") + + expect(error.message).to include("Generic error") + expect(error.type_slot).to be_nil + end + end + + describe "#line" do + it "returns line from type_slot location" do + error = described_class.new(message: "Error", type_slot: type_slot) + + expect(error.line).to eq(10) + end + + it "returns nil when no type_slot" do + error = described_class.new(message: "Error") + + expect(error.line).to be_nil + end + end + + describe "#column" do + it "returns column from type_slot location" do + error = described_class.new(message: "Error", type_slot: type_slot) + + expect(error.column).to eq(5) + end + end + + describe "#kind" do + it "returns kind from type_slot" do + error = described_class.new(message: "Error", type_slot: type_slot) + + expect(error.kind).to eq(:parameter) + end + end + + describe "#formatted_message" do + it "includes location information" do + error = described_class.new( + message: "Expected type annotation", + type_slot: type_slot + ) + + formatted = error.formatted_message + + expect(formatted).to include("Line 10") + expect(formatted).to include("Column 5") + end + + it "includes context information for parameter" do + error = described_class.new( + message: "Expected type annotation", + type_slot: type_slot + ) + + formatted = error.formatted_message + + expect(formatted).to include("parameter") + expect(formatted).to include("name") + expect(formatted).to include("greet") + end + + it "includes context information for return type" do + return_slot = TRuby::IR::TypeSlot.new( + kind: :return, + location: { line: 5, column: 20 }, + context: { method_name: "calculate" } + ) + error = described_class.new( + message: "Missing return type", + type_slot: return_slot + ) + + formatted = error.formatted_message + + expect(formatted).to include("return") + expect(formatted).to include("calculate") + end + end + + describe "#suggestion" do + it "allows setting suggestion" do + error = described_class.new(message: "Error", type_slot: type_slot) + error.suggestion = "Add type annotation like 'name: String'" + + expect(error.suggestion).to eq("Add type annotation like 'name: String'") + end + + it "includes suggestion in formatted_message" do + error = described_class.new(message: "Error", type_slot: type_slot) + error.suggestion = "Add type annotation" + + formatted = error.formatted_message + + expect(formatted).to include("Suggestion:") + expect(formatted).to include("Add type annotation") + end + end + + describe "#to_lsp_diagnostic" do + it "returns LSP-compatible diagnostic hash" do + error = described_class.new( + message: "Type mismatch", + type_slot: type_slot + ) + + diagnostic = error.to_lsp_diagnostic + + expect(diagnostic).to be_a(Hash) + expect(diagnostic[:range][:start][:line]).to eq(9) # 0-indexed + expect(diagnostic[:range][:start][:character]).to eq(5) + expect(diagnostic[:message]).to include("Type mismatch") + expect(diagnostic[:severity]).to eq(1) # Error + end + + it "sets source to 't-ruby'" do + error = described_class.new(message: "Error", type_slot: type_slot) + + diagnostic = error.to_lsp_diagnostic + + expect(diagnostic[:source]).to eq("t-ruby") + end + end + + describe "#to_s" do + it "returns formatted message" do + error = described_class.new( + message: "Expected type annotation", + type_slot: type_slot + ) + + expect(error.to_s).to eq(error.formatted_message) + end + end +end diff --git a/spec/t_ruby/ir/type_slot_spec.rb b/spec/t_ruby/ir/type_slot_spec.rb new file mode 100644 index 0000000..626817c --- /dev/null +++ b/spec/t_ruby/ir/type_slot_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe TRuby::IR::TypeSlot do + describe "initialization" do + it "creates a type slot with kind and location" do + slot = described_class.new( + kind: :parameter, + location: { line: 5, column: 10 } + ) + + expect(slot.kind).to eq(:parameter) + expect(slot.location).to eq({ line: 5, column: 10 }) + end + + it "accepts context information" do + slot = described_class.new( + kind: :parameter, + location: { line: 1, column: 0 }, + context: { method_name: "greet", param_name: "name" } + ) + + expect(slot.context[:method_name]).to eq("greet") + expect(slot.context[:param_name]).to eq("name") + end + + it "validates kind is one of KINDS" do + expect(TRuby::IR::TypeSlot::KINDS).to include(:parameter) + expect(TRuby::IR::TypeSlot::KINDS).to include(:return) + expect(TRuby::IR::TypeSlot::KINDS).to include(:variable) + expect(TRuby::IR::TypeSlot::KINDS).to include(:instance_var) + expect(TRuby::IR::TypeSlot::KINDS).to include(:generic_arg) + end + end + + describe "#explicit_type" do + it "stores explicit type annotation" do + slot = described_class.new(kind: :parameter, location: {}) + type = TRuby::IR::SimpleType.new(name: "String") + + slot.explicit_type = type + + expect(slot.explicit_type).to eq(type) + end + end + + describe "#inferred_type" do + it "stores inferred type" do + slot = described_class.new(kind: :variable, location: {}) + type = TRuby::IR::SimpleType.new(name: "Integer") + + slot.inferred_type = type + + expect(slot.inferred_type).to eq(type) + end + end + + describe "#resolved_type" do + it "stores final resolved type" do + slot = described_class.new(kind: :return, location: {}) + type = TRuby::IR::SimpleType.new(name: "Boolean") + + slot.resolved_type = type + + expect(slot.resolved_type).to eq(type) + end + end + + describe "#needs_inference?" do + it "returns true when explicit_type is nil" do + slot = described_class.new(kind: :parameter, location: {}) + + expect(slot.needs_inference?).to be true + end + + it "returns false when explicit_type is set" do + slot = described_class.new(kind: :parameter, location: {}) + slot.explicit_type = TRuby::IR::SimpleType.new(name: "String") + + expect(slot.needs_inference?).to be false + end + end + + describe "#error_context" do + it "returns context information for error messages" do + slot = described_class.new( + kind: :parameter, + location: { line: 10, column: 5 }, + context: { method_name: "process", param_name: "data" } + ) + + error_ctx = slot.error_context + + expect(error_ctx[:kind]).to eq(:parameter) + expect(error_ctx[:location]).to eq({ line: 10, column: 5 }) + expect(error_ctx[:context][:method_name]).to eq("process") + end + end + + describe "#resolved_type_or_untyped" do + it "returns resolved_type when set" do + slot = described_class.new(kind: :parameter, location: {}) + type = TRuby::IR::SimpleType.new(name: "String") + slot.resolved_type = type + + expect(slot.resolved_type_or_untyped).to eq(type) + end + + it "returns explicit_type when resolved_type is nil" do + slot = described_class.new(kind: :parameter, location: {}) + type = TRuby::IR::SimpleType.new(name: "Integer") + slot.explicit_type = type + + expect(slot.resolved_type_or_untyped).to eq(type) + end + + it "returns inferred_type when explicit and resolved are nil" do + slot = described_class.new(kind: :variable, location: {}) + type = TRuby::IR::SimpleType.new(name: "Float") + slot.inferred_type = type + + expect(slot.resolved_type_or_untyped).to eq(type) + end + + it "returns untyped SimpleType when all types are nil" do + slot = described_class.new(kind: :parameter, location: {}) + + result = slot.resolved_type_or_untyped + + expect(result).to be_a(TRuby::IR::SimpleType) + expect(result.name).to eq("untyped") + end + end +end diff --git a/spec/t_ruby/ir_spec.rb b/spec/t_ruby/ir_spec.rb index a060324..3e6ba47 100644 --- a/spec/t_ruby/ir_spec.rb +++ b/spec/t_ruby/ir_spec.rb @@ -575,6 +575,22 @@ def greet(name: String): String method = described_class.new(name: "test", body: nil) expect(method.children).to eq([]) end + + it "has return_type_slot attribute" do + method = described_class.new(name: "test") + slot = TRuby::IR::TypeSlot.new(kind: :return, location: { line: 1, column: 0 }) + method.return_type_slot = slot + expect(method.return_type_slot).to eq(slot) + end + end + + describe TRuby::IR::Parameter do + it "has type_slot attribute" do + param = described_class.new(name: "x") + slot = TRuby::IR::TypeSlot.new(kind: :parameter, location: { line: 1, column: 5 }) + param.type_slot = slot + expect(param.type_slot).to eq(slot) + end end describe TRuby::IR::Block do diff --git a/spec/t_ruby/parser_facade_spec.rb b/spec/t_ruby/parser_facade_spec.rb new file mode 100644 index 0000000..efe713e --- /dev/null +++ b/spec/t_ruby/parser_facade_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "Parser facade for TokenDeclarationParser" do + describe "use_token_parser option" do + let(:source) do + <<~RUBY + def greet(name: String): String + name + end + RUBY + end + + context "when use_token_parser is true" do + it "uses TokenDeclarationParser" do + parser = TRuby::Parser.new(source, use_token_parser: true) + result = parser.parse + + expect(result[:type]).to eq(:success) + expect(result[:functions]).not_to be_empty + end + + it "generates TypeSlots for parameters" do + parser = TRuby::Parser.new(source, use_token_parser: true) + parser.parse + ir = parser.ir_program + + method_def = ir.declarations.first + expect(method_def.params.first.type_slot).to be_a(TRuby::IR::TypeSlot) + end + + it "generates return_type_slot for methods" do + parser = TRuby::Parser.new(source, use_token_parser: true) + parser.parse + ir = parser.ir_program + + method_def = ir.declarations.first + expect(method_def.return_type_slot).to be_a(TRuby::IR::TypeSlot) + end + end + + context "when use_token_parser is false (default)" do + it "uses legacy regex-based parser" do + parser = TRuby::Parser.new(source) + result = parser.parse + + expect(result[:type]).to eq(:success) + expect(result[:functions]).not_to be_empty + end + end + end + + describe "TRUBY_NEW_PARSER environment variable" do + let(:source) do + <<~RUBY + def test: Integer + 42 + end + RUBY + end + + it "respects environment variable when set to '1'" do + allow(ENV).to receive(:[]).with("TRUBY_NEW_PARSER").and_return("1") + + parser = TRuby::Parser.new(source) + # Should use token parser when env var is set + expect(parser.send(:use_token_parser?)).to be true + end + + it "defaults to false when env var not set" do + allow(ENV).to receive(:[]).with("TRUBY_NEW_PARSER").and_return(nil) + + parser = TRuby::Parser.new(source) + expect(parser.send(:use_token_parser?)).to be false + end + end + + describe "backward compatibility" do + let(:source) do + <<~RUBY + class Greeter + def greet(name: String): String + name + end + end + RUBY + end + + it "produces equivalent results from both parsers" do + legacy_parser = TRuby::Parser.new(source, use_token_parser: false) + token_parser = TRuby::Parser.new(source, use_token_parser: true) + + legacy_result = legacy_parser.parse + token_result = token_parser.parse + + expect(legacy_result[:type]).to eq(token_result[:type]) + expect(legacy_result[:classes].length).to eq(token_result[:classes].length) + end + end +end diff --git a/spec/t_ruby/token_declaration_parser_type_slot_spec.rb b/spec/t_ruby/token_declaration_parser_type_slot_spec.rb new file mode 100644 index 0000000..44b5684 --- /dev/null +++ b/spec/t_ruby/token_declaration_parser_type_slot_spec.rb @@ -0,0 +1,277 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "TokenDeclarationParser TypeSlot integration" do + include TRuby::ParserCombinator::TokenDSL + + let(:parser) { TRuby::ParserCombinator::TokenDeclarationParser.new } + let(:scanner) { TRuby::Scanner.new(source) } + let(:tokens) { scanner.scan_all } + + describe "parameter type slots" do + context "with typed parameter" do + let(:source) do + <<~RUBY + def greet(name: String) + name + end + RUBY + end + + it "creates type_slot for parameter" do + result = parser.parse_declaration(tokens, 0) + + expect(result.success?).to be true + param = result.value.params[0] + expect(param.type_slot).to be_a(TRuby::IR::TypeSlot) + end + + it "sets type_slot kind to :parameter" do + result = parser.parse_declaration(tokens, 0) + + param = result.value.params[0] + expect(param.type_slot.kind).to eq(:parameter) + end + + it "sets explicit_type on type_slot when type annotation present" do + result = parser.parse_declaration(tokens, 0) + + param = result.value.params[0] + expect(param.type_slot.explicit_type).to be_a(TRuby::IR::SimpleType) + expect(param.type_slot.explicit_type.name).to eq("String") + end + + it "sets type_slot context with method and param info" do + result = parser.parse_declaration(tokens, 0) + + param = result.value.params[0] + expect(param.type_slot.context[:method_name]).to eq("greet") + expect(param.type_slot.context[:param_name]).to eq("name") + end + + it "sets type_slot location" do + result = parser.parse_declaration(tokens, 0) + + param = result.value.params[0] + expect(param.type_slot.location).to have_key(:line) + expect(param.type_slot.location).to have_key(:column) + end + end + + context "with untyped parameter" do + let(:source) do + <<~RUBY + def greet(name) + name + end + RUBY + end + + it "creates type_slot for untyped parameter" do + result = parser.parse_declaration(tokens, 0) + + param = result.value.params[0] + expect(param.type_slot).to be_a(TRuby::IR::TypeSlot) + end + + it "leaves explicit_type nil for untyped parameter" do + result = parser.parse_declaration(tokens, 0) + + param = result.value.params[0] + expect(param.type_slot.explicit_type).to be_nil + end + + it "needs_inference? returns true for untyped parameter" do + result = parser.parse_declaration(tokens, 0) + + param = result.value.params[0] + expect(param.type_slot.needs_inference?).to be true + end + end + + context "with multiple parameters" do + let(:source) do + <<~RUBY + def greet(name: String, age) + name + end + RUBY + end + + it "creates type_slot for each parameter" do + result = parser.parse_declaration(tokens, 0) + + expect(result.value.params[0].type_slot).to be_a(TRuby::IR::TypeSlot) + expect(result.value.params[1].type_slot).to be_a(TRuby::IR::TypeSlot) + end + + it "typed parameter has explicit_type set" do + result = parser.parse_declaration(tokens, 0) + + expect(result.value.params[0].type_slot.explicit_type).not_to be_nil + end + + it "untyped parameter has explicit_type nil" do + result = parser.parse_declaration(tokens, 0) + + expect(result.value.params[1].type_slot.explicit_type).to be_nil + end + end + + context "with special parameter kinds" do + describe "rest parameter (*args)" do + let(:source) do + <<~RUBY + def collect(*items: Array) + end + RUBY + end + + it "creates type_slot for rest parameter" do + result = parser.parse_declaration(tokens, 0) + + param = result.value.params[0] + expect(param.type_slot).to be_a(TRuby::IR::TypeSlot) + expect(param.type_slot.kind).to eq(:parameter) + end + end + + describe "keyrest parameter (**opts)" do + let(:source) do + <<~RUBY + def configure(**opts) + end + RUBY + end + + it "creates type_slot for keyrest parameter" do + result = parser.parse_declaration(tokens, 0) + + param = result.value.params[0] + expect(param.type_slot).to be_a(TRuby::IR::TypeSlot) + end + end + + describe "block parameter (&block)" do + let(:source) do + <<~RUBY + def execute(&block) + end + RUBY + end + + it "creates type_slot for block parameter" do + result = parser.parse_declaration(tokens, 0) + + param = result.value.params[0] + expect(param.type_slot).to be_a(TRuby::IR::TypeSlot) + end + end + end + end + + describe "return type slots" do + context "with return type annotation" do + let(:source) do + <<~RUBY + def greet(name: String): String + name + end + RUBY + end + + it "creates return_type_slot for method" do + result = parser.parse_declaration(tokens, 0) + + expect(result.value.return_type_slot).to be_a(TRuby::IR::TypeSlot) + end + + it "sets return_type_slot kind to :return" do + result = parser.parse_declaration(tokens, 0) + + expect(result.value.return_type_slot.kind).to eq(:return) + end + + it "sets explicit_type on return_type_slot" do + result = parser.parse_declaration(tokens, 0) + + expect(result.value.return_type_slot.explicit_type).to be_a(TRuby::IR::SimpleType) + expect(result.value.return_type_slot.explicit_type.name).to eq("String") + end + + it "sets return_type_slot context with method info" do + result = parser.parse_declaration(tokens, 0) + + expect(result.value.return_type_slot.context[:method_name]).to eq("greet") + end + end + + context "without return type annotation" do + let(:source) do + <<~RUBY + def greet(name: String) + name + end + RUBY + end + + it "creates return_type_slot even without annotation" do + result = parser.parse_declaration(tokens, 0) + + expect(result.value.return_type_slot).to be_a(TRuby::IR::TypeSlot) + end + + it "leaves explicit_type nil without annotation" do + result = parser.parse_declaration(tokens, 0) + + expect(result.value.return_type_slot.explicit_type).to be_nil + end + + it "needs_inference? returns true without annotation" do + result = parser.parse_declaration(tokens, 0) + + expect(result.value.return_type_slot.needs_inference?).to be true + end + end + + context "with union return type" do + let(:source) do + <<~RUBY + def find(id: Integer): String | nil + "found" + end + RUBY + end + + it "sets union type as explicit_type" do + result = parser.parse_declaration(tokens, 0) + + expect(result.value.return_type_slot.explicit_type).to be_a(TRuby::IR::UnionType) + end + end + end + + describe "backward compatibility" do + let(:source) do + <<~RUBY + def greet(name: String): String + name + end + RUBY + end + + it "still sets type_annotation on parameter" do + result = parser.parse_declaration(tokens, 0) + + param = result.value.params[0] + expect(param.type_annotation).to be_a(TRuby::IR::SimpleType) + end + + it "still sets return_type on method" do + result = parser.parse_declaration(tokens, 0) + + expect(result.value.return_type).to be_a(TRuby::IR::SimpleType) + end + end +end diff --git a/spec/t_ruby/type_resolution/slot_resolver_spec.rb b/spec/t_ruby/type_resolution/slot_resolver_spec.rb new file mode 100644 index 0000000..8799078 --- /dev/null +++ b/spec/t_ruby/type_resolution/slot_resolver_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe TRuby::TypeResolution::SlotResolver do + let(:resolver) { described_class.new } + + describe "#collect_unresolved_slots" do + it "collects parameter type slots needing inference" do + slot1 = TRuby::IR::TypeSlot.new(kind: :parameter, location: { line: 1, column: 1 }) + slot2 = TRuby::IR::TypeSlot.new(kind: :parameter, location: { line: 2, column: 1 }) + slot2.explicit_type = TRuby::IR::SimpleType.new(name: "String") + + param1 = TRuby::IR::Parameter.new(name: "a", type_slot: slot1) + param2 = TRuby::IR::Parameter.new(name: "b", type_slot: slot2) + + method_def = TRuby::IR::MethodDef.new( + name: "test", + params: [param1, param2], + body: TRuby::IR::Block.new(statements: []) + ) + + program = TRuby::IR::Program.new(declarations: [method_def]) + + unresolved = resolver.collect_unresolved_slots(program) + + expect(unresolved).to include(slot1) + expect(unresolved).not_to include(slot2) + end + + it "collects return type slots needing inference" do + return_slot = TRuby::IR::TypeSlot.new(kind: :return, location: { line: 1, column: 1 }) + + method_def = TRuby::IR::MethodDef.new( + name: "test", + params: [], + body: TRuby::IR::Block.new(statements: []), + return_type_slot: return_slot + ) + + program = TRuby::IR::Program.new(declarations: [method_def]) + + unresolved = resolver.collect_unresolved_slots(program) + + expect(unresolved).to include(return_slot) + end + + it "excludes return type slots with explicit types" do + return_slot = TRuby::IR::TypeSlot.new(kind: :return, location: { line: 1, column: 1 }) + return_slot.explicit_type = TRuby::IR::SimpleType.new(name: "Integer") + + method_def = TRuby::IR::MethodDef.new( + name: "test", + params: [], + body: TRuby::IR::Block.new(statements: []), + return_type_slot: return_slot + ) + + program = TRuby::IR::Program.new(declarations: [method_def]) + + unresolved = resolver.collect_unresolved_slots(program) + + expect(unresolved).not_to include(return_slot) + end + end + + describe "#resolve_to_untyped" do + it "sets resolved_type to untyped for parameter slots" do + slot = TRuby::IR::TypeSlot.new( + kind: :parameter, + location: { line: 1, column: 1 }, + context: { param_name: "x", method_name: "test" } + ) + + resolver.resolve_to_untyped(slot) + + expect(slot.resolved_type).to be_a(TRuby::IR::SimpleType) + expect(slot.resolved_type.name).to eq("untyped") + end + end + + describe "#resolve_all_untyped" do + it "resolves all unresolved parameter slots to untyped" do + slot1 = TRuby::IR::TypeSlot.new(kind: :parameter, location: { line: 1, column: 1 }) + slot2 = TRuby::IR::TypeSlot.new(kind: :parameter, location: { line: 2, column: 1 }) + slot2.explicit_type = TRuby::IR::SimpleType.new(name: "String") + + param1 = TRuby::IR::Parameter.new(name: "a", type_slot: slot1) + param2 = TRuby::IR::Parameter.new(name: "b", type_slot: slot2) + + method_def = TRuby::IR::MethodDef.new( + name: "test", + params: [param1, param2], + body: TRuby::IR::Block.new(statements: []) + ) + + program = TRuby::IR::Program.new(declarations: [method_def]) + + resolver.resolve_all_untyped(program) + + expect(slot1.resolved_type.name).to eq("untyped") + expect(slot2.resolved_type).to be_nil # explicit type, no resolution needed + end + end + + describe "#slot_summary" do + it "returns summary of slot states" do + slot1 = TRuby::IR::TypeSlot.new(kind: :parameter, location: { line: 1, column: 1 }) + slot2 = TRuby::IR::TypeSlot.new(kind: :parameter, location: { line: 2, column: 1 }) + slot2.explicit_type = TRuby::IR::SimpleType.new(name: "String") + + return_slot = TRuby::IR::TypeSlot.new(kind: :return, location: { line: 1, column: 1 }) + + param1 = TRuby::IR::Parameter.new(name: "a", type_slot: slot1) + param2 = TRuby::IR::Parameter.new(name: "b", type_slot: slot2) + + method_def = TRuby::IR::MethodDef.new( + name: "test", + params: [param1, param2], + body: TRuby::IR::Block.new(statements: []), + return_type_slot: return_slot + ) + + program = TRuby::IR::Program.new(declarations: [method_def]) + + summary = resolver.slot_summary(program) + + expect(summary[:total]).to eq(3) + expect(summary[:explicit]).to eq(1) + expect(summary[:needs_inference]).to eq(2) + end + end +end