Skip to content

Commit aed4c6e

Browse files
authored
Add tuple rest element support (#44) (#50)
Add support for tuple rest elements using the syntax `[Type, *Type[]]`. This allows tuples with variable-length trailing elements. Features: - New `TupleRestElement` IR node for rest elements - Parser support for `*Type[]` and `*Array<Type>` syntax - Validation: rest must be at end, only one rest allowed - RBS fallback: converts to union array (RBS doesn't support tuple rest) - LSP: tuple type completions and hover support Example: ```ruby def get_values(): [String, *Integer[]] ["header", 1, 2, 3] end ```
1 parent b7f7bc5 commit aed4c6e

File tree

7 files changed

+571
-5
lines changed

7 files changed

+571
-5
lines changed

lib/t_ruby/ir.rb

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -633,12 +633,69 @@ def initialize(element_types: [], **opts)
633633
end
634634

635635
def to_rbs
636-
"[#{@element_types.map(&:to_rbs).join(", ")}]"
636+
# Check if tuple has rest element
637+
has_rest = @element_types.any? { |t| t.is_a?(TupleRestElement) }
638+
639+
if has_rest
640+
# Fallback: convert to union array (RBS doesn't support tuple rest)
641+
all_types = @element_types.flat_map do |t|
642+
t.is_a?(TupleRestElement) ? t.element_type : t
643+
end
644+
type_names = all_types.map(&:to_rbs).uniq
645+
"Array[#{type_names.join(" | ")}]"
646+
else
647+
"[#{@element_types.map(&:to_rbs).join(", ")}]"
648+
end
637649
end
638650

639651
def to_trb
640652
"[#{@element_types.map(&:to_trb).join(", ")}]"
641653
end
654+
655+
# Validate tuple structure
656+
def validate!
657+
rest_indices = @element_types.each_with_index
658+
.select { |t, _| t.is_a?(TupleRestElement) }
659+
.map(&:last)
660+
661+
if rest_indices.length > 1
662+
raise TypeError, "Tuple can have at most one rest element"
663+
end
664+
665+
if rest_indices.any? && rest_indices.first != @element_types.length - 1
666+
raise TypeError, "Rest element must be at the end of tuple"
667+
end
668+
669+
self
670+
end
671+
end
672+
673+
# Tuple rest element (*Integer[] - variable length elements)
674+
class TupleRestElement < TypeNode
675+
attr_accessor :inner_type
676+
677+
def initialize(inner_type:, **opts)
678+
super(**opts)
679+
@inner_type = inner_type
680+
end
681+
682+
def to_rbs
683+
# RBS doesn't support tuple rest, fallback to untyped
684+
"*untyped"
685+
end
686+
687+
def to_trb
688+
"*#{@inner_type.to_trb}"
689+
end
690+
691+
# Extract element type from Array type
692+
def element_type
693+
if @inner_type.is_a?(GenericType) && @inner_type.base == "Array"
694+
@inner_type.type_args.first
695+
else
696+
@inner_type
697+
end
698+
end
642699
end
643700

644701
# Nullable type (String?)

lib/t_ruby/lsp_server.rb

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,14 +491,35 @@ def handle_completion(params)
491491
end
492492

493493
def type_completions
494-
BUILT_IN_TYPES.map do |type|
494+
completions = BUILT_IN_TYPES.map do |type|
495495
{
496496
"label" => type,
497497
"kind" => CompletionItemKind::CLASS,
498498
"detail" => "Built-in type",
499499
"documentation" => "T-Ruby built-in type: #{type}",
500500
}
501501
end
502+
503+
# Add tuple type completions
504+
completions << {
505+
"label" => "[T, U]",
506+
"kind" => CompletionItemKind::STRUCT,
507+
"detail" => "Tuple type",
508+
"documentation" => "Fixed-length array with typed elements.\n\nExample: `[String, Integer]`",
509+
"insertText" => "[${1:Type}, ${2:Type}]",
510+
"insertTextFormat" => 2, # Snippet format
511+
}
512+
513+
completions << {
514+
"label" => "[T, *U[]]",
515+
"kind" => CompletionItemKind::STRUCT,
516+
"detail" => "Tuple with rest",
517+
"documentation" => "Tuple with variable-length rest elements.\n\nExample: `[Header, *Row[]]`",
518+
"insertText" => "[${1:Type}, *${2:Type}[]]",
519+
"insertTextFormat" => 2,
520+
}
521+
522+
completions
502523
end
503524

504525
def keyword_completions
@@ -618,6 +639,20 @@ def get_hover_info(word, text)
618639
return "**#{word}** - Built-in T-Ruby type"
619640
end
620641

642+
# Check if it's a tuple type pattern
643+
if word.match?(/^\[.*\]$/)
644+
return "**Tuple Type**\n\nFixed-length array with typed elements.\n\n" \
645+
"Each position can have a different type.\n\n" \
646+
"Example: `[String, Integer, Boolean]`"
647+
end
648+
649+
# Check if it's a rest element pattern
650+
if word.start_with?("*") && (word.include?("[]") || word.include?("<"))
651+
return "**Rest Element**\n\nVariable-length elements at the end of tuple.\n\n" \
652+
"Syntax: `*Type[]` or `*Array<Type>`\n\n" \
653+
"Example: `[Header, *Row[]]`"
654+
end
655+
621656
# Check if it's a type alias
622657
parser = Parser.new(text)
623658
result = parser.parse

lib/t_ruby/parser_combinator/type_parser.rb

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,17 @@ def build_parsers
6666
IR::FunctionType.new(param_types: params, return_type: ret)
6767
end
6868

69-
# Tuple type: [Type, Type, ...]
69+
# Tuple type: [Type, Type, ...] or [Type, *Type[]]
70+
# Note: Uses lazy reference to @tuple_element which is defined after base_type
7071
tuple_type = (
7172
lexeme(char("[")) >>
72-
type_expr.sep_by1(lexeme(char(","))) <<
73+
lazy { @tuple_element }.sep_by1(lexeme(char(","))) <<
7374
lexeme(char("]"))
74-
).map { |(_, types)| IR::TupleType.new(element_types: types) }
75+
).map do |(_, types)|
76+
tuple = IR::TupleType.new(element_types: types)
77+
tuple.validate! # Validates rest element position
78+
tuple
79+
end
7580

7681
# Primary type (before operators)
7782
primary_type = choice(
@@ -101,6 +106,15 @@ def build_parsers
101106
end
102107
end
103108

109+
# Rest element for tuple: *Type[] or *Array<Type>
110+
# Defined after base_type so it can reference it
111+
rest_element = (lexeme(char("*")) >> base_type).map do |(_, inner)|
112+
IR::TupleRestElement.new(inner_type: inner)
113+
end
114+
115+
# Tuple element: Type or *Type (rest element)
116+
@tuple_element = rest_element | type_expr
117+
104118
# Union type: Type | Type | ...
105119
union_op = lexeme(char("|"))
106120
union_type = base_type.sep_by1(union_op).map do |types|

0 commit comments

Comments
 (0)