Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion lib/t_ruby/ir.rb
Original file line number Diff line number Diff line change
Expand Up @@ -633,12 +633,69 @@ def initialize(element_types: [], **opts)
end

def to_rbs
"[#{@element_types.map(&:to_rbs).join(", ")}]"
# Check if tuple has rest element
has_rest = @element_types.any? { |t| t.is_a?(TupleRestElement) }

if has_rest
# Fallback: convert to union array (RBS doesn't support tuple rest)
all_types = @element_types.flat_map do |t|
t.is_a?(TupleRestElement) ? t.element_type : t
end
type_names = all_types.map(&:to_rbs).uniq
"Array[#{type_names.join(" | ")}]"
else
"[#{@element_types.map(&:to_rbs).join(", ")}]"
end
end

def to_trb
"[#{@element_types.map(&:to_trb).join(", ")}]"
end

# Validate tuple structure
def validate!
rest_indices = @element_types.each_with_index
.select { |t, _| t.is_a?(TupleRestElement) }
.map(&:last)

if rest_indices.length > 1
raise TypeError, "Tuple can have at most one rest element"
end

if rest_indices.any? && rest_indices.first != @element_types.length - 1
raise TypeError, "Rest element must be at the end of tuple"
end

self
end
end

# Tuple rest element (*Integer[] - variable length elements)
class TupleRestElement < TypeNode
attr_accessor :inner_type

def initialize(inner_type:, **opts)
super(**opts)
@inner_type = inner_type
end

def to_rbs
# RBS doesn't support tuple rest, fallback to untyped
"*untyped"
end

def to_trb
"*#{@inner_type.to_trb}"
end

# Extract element type from Array type
def element_type
if @inner_type.is_a?(GenericType) && @inner_type.base == "Array"
@inner_type.type_args.first
else
@inner_type
end
end
end

# Nullable type (String?)
Expand Down
37 changes: 36 additions & 1 deletion lib/t_ruby/lsp_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -491,14 +491,35 @@ def handle_completion(params)
end

def type_completions
BUILT_IN_TYPES.map do |type|
completions = BUILT_IN_TYPES.map do |type|
{
"label" => type,
"kind" => CompletionItemKind::CLASS,
"detail" => "Built-in type",
"documentation" => "T-Ruby built-in type: #{type}",
}
end

# Add tuple type completions
completions << {
"label" => "[T, U]",
"kind" => CompletionItemKind::STRUCT,
"detail" => "Tuple type",
"documentation" => "Fixed-length array with typed elements.\n\nExample: `[String, Integer]`",
"insertText" => "[${1:Type}, ${2:Type}]",
"insertTextFormat" => 2, # Snippet format
}

completions << {
"label" => "[T, *U[]]",
"kind" => CompletionItemKind::STRUCT,
"detail" => "Tuple with rest",
"documentation" => "Tuple with variable-length rest elements.\n\nExample: `[Header, *Row[]]`",
"insertText" => "[${1:Type}, *${2:Type}[]]",
"insertTextFormat" => 2,
}

completions
end

def keyword_completions
Expand Down Expand Up @@ -618,6 +639,20 @@ def get_hover_info(word, text)
return "**#{word}** - Built-in T-Ruby type"
end

# Check if it's a tuple type pattern
if word.match?(/^\[.*\]$/)
return "**Tuple Type**\n\nFixed-length array with typed elements.\n\n" \
"Each position can have a different type.\n\n" \
"Example: `[String, Integer, Boolean]`"
end

# Check if it's a rest element pattern
if word.start_with?("*") && (word.include?("[]") || word.include?("<"))
return "**Rest Element**\n\nVariable-length elements at the end of tuple.\n\n" \
"Syntax: `*Type[]` or `*Array<Type>`\n\n" \
"Example: `[Header, *Row[]]`"
end

# Check if it's a type alias
parser = Parser.new(text)
result = parser.parse
Expand Down
20 changes: 17 additions & 3 deletions lib/t_ruby/parser_combinator/type_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,17 @@ def build_parsers
IR::FunctionType.new(param_types: params, return_type: ret)
end

# Tuple type: [Type, Type, ...]
# Tuple type: [Type, Type, ...] or [Type, *Type[]]
# Note: Uses lazy reference to @tuple_element which is defined after base_type
tuple_type = (
lexeme(char("[")) >>
type_expr.sep_by1(lexeme(char(","))) <<
lazy { @tuple_element }.sep_by1(lexeme(char(","))) <<
lexeme(char("]"))
).map { |(_, types)| IR::TupleType.new(element_types: types) }
).map do |(_, types)|
tuple = IR::TupleType.new(element_types: types)
tuple.validate! # Validates rest element position
tuple
end

# Primary type (before operators)
primary_type = choice(
Expand Down Expand Up @@ -101,6 +106,15 @@ def build_parsers
end
end

# Rest element for tuple: *Type[] or *Array<Type>
# Defined after base_type so it can reference it
rest_element = (lexeme(char("*")) >> base_type).map do |(_, inner)|
IR::TupleRestElement.new(inner_type: inner)
end

# Tuple element: Type or *Type (rest element)
@tuple_element = rest_element | type_expr

# Union type: Type | Type | ...
union_op = lexeme(char("|"))
union_type = base_type.sep_by1(union_op).map do |types|
Expand Down
Loading
Loading