From 7c5c723b68264614ad985e42cd1e68fcf8cf5866 Mon Sep 17 00:00:00 2001 From: "Yonghyun Kim (Freddy)" Date: Thu, 22 Jan 2026 17:48:52 +0900 Subject: [PATCH 1/5] Add TypeSlot infrastructure for type position tracking Introduce TypeSlot class that represents positions where type annotations are expected. This enables explicit tracking of explicit, inferred, and resolved types for each position. - Add IR::TypeSlot class with kind, location, and context - Add type_slot attribute to IR::Parameter - Add return_type_slot attribute to IR::MethodDef - Maintain backward compatibility with existing code Refs #47 --- lib/t_ruby/ir.rb | 13 ++- lib/t_ruby/ir/type_slot.rb | 57 +++++++++++++ spec/t_ruby/ir/type_slot_spec.rb | 135 +++++++++++++++++++++++++++++++ spec/t_ruby/ir_spec.rb | 16 ++++ 4 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 lib/t_ruby/ir/type_slot.rb create mode 100644 spec/t_ruby/ir/type_slot_spec.rb 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/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 From 139ad4e00e356c449f26e1db597035f0639f2552 Mon Sep 17 00:00:00 2001 From: "Yonghyun Kim (Freddy)" Date: Thu, 22 Jan 2026 17:53:37 +0900 Subject: [PATCH 2/5] Integrate TypeSlot generation in TokenDeclarationParser - Add TypeSlot creation during parameter parsing in parse_parameter - Add return_type_slot creation in parse_method_def - Pass method_name context to parameter parsers for TypeSlot context - Support all parameter types (regular, rest, keyrest, block, keyword) - Add comprehensive tests for TypeSlot integration - Maintain backward compatibility with existing type_annotation/return_type Refs #47 --- .../token/token_declaration_parser.rb | 73 ++++- ...token_declaration_parser_type_slot_spec.rb | 277 ++++++++++++++++++ 2 files changed, 336 insertions(+), 14 deletions(-) create mode 100644 spec/t_ruby/token_declaration_parser_type_slot_spec.rb 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/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 From de65624354d722d062d3060430a380efbce28b6c Mon Sep 17 00:00:00 2001 From: "Yonghyun Kim (Freddy)" Date: Thu, 22 Jan 2026 17:55:34 +0900 Subject: [PATCH 3/5] Add TypeSlotError for context-aware error messages - Add TRuby::Errors::TypeSlotError class for type-related errors - Support location info (line, column) from TypeSlot - Include context description (parameter/return/variable info) - Support suggestion field for helpful error hints - Provide to_lsp_diagnostic for LSP integration - Add comprehensive tests Refs #47 --- lib/t_ruby.rb | 1 + lib/t_ruby/errors/type_slot_error.rb | 107 ++++++++++++++ spec/t_ruby/errors/type_slot_error_spec.rb | 161 +++++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 lib/t_ruby/errors/type_slot_error.rb create mode 100644 spec/t_ruby/errors/type_slot_error_spec.rb diff --git a/lib/t_ruby.rb b/lib/t_ruby.rb index 4f8351d..baddb56 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" 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/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 From 5c6ccb0af2fe1585b354697eade56891deeb8501 Mon Sep 17 00:00:00 2001 From: "Yonghyun Kim (Freddy)" Date: Thu, 22 Jan 2026 17:56:53 +0900 Subject: [PATCH 4/5] Add SlotResolver for TypeSlot resolution - Add TRuby::TypeResolution::SlotResolver class - Collect unresolved TypeSlots from IR program - Support resolve_to_untyped for gradual typing fallback - Provide slot_summary for type coverage statistics - Handle method parameters and return types - Traverse ClassDef and ModuleDef for nested methods Refs #47 --- lib/t_ruby.rb | 3 + lib/t_ruby/type_resolution/slot_resolver.rb | 84 +++++++++++ .../type_resolution/slot_resolver_spec.rb | 133 ++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 lib/t_ruby/type_resolution/slot_resolver.rb create mode 100644 spec/t_ruby/type_resolution/slot_resolver_spec.rb diff --git a/lib/t_ruby.rb b/lib/t_ruby.rb index baddb56..b78a555 100644 --- a/lib/t_ruby.rb +++ b/lib/t_ruby.rb @@ -33,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/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/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 From 23ba5f88a5a9325c5aa4452e5442a28d977e2981 Mon Sep 17 00:00:00 2001 From: "Yonghyun Kim (Freddy)" Date: Thu, 22 Jan 2026 18:03:39 +0900 Subject: [PATCH 5/5] Add facade pattern for parser with token parser option - Add use_token_parser option to Parser.new for opt-in new parser - Support TRUBY_NEW_PARSER=1 environment variable - Implement parse_with_token_parser using TokenDeclarationParser - Add backward-compatible legacy format conversion - Mark legacy regex-based parser as @deprecated - Add comprehensive facade tests This enables gradual migration from regex-based parsing to the new TokenDeclarationParser with TypeSlot support. Refs #47 --- lib/t_ruby/parser.rb | 138 +++++++++++++++++++++++++++++- spec/t_ruby/parser_facade_spec.rb | 101 ++++++++++++++++++++++ 2 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 spec/t_ruby/parser_facade_spec.rb 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/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