From 0f845beb767cc0adb581e9d817ad312925caf338 Mon Sep 17 00:00:00 2001 From: Allison Cretel Date: Tue, 20 May 2025 14:55:17 -0400 Subject: [PATCH] Add type arguments support to singleton types Previously, singleton type arguments could not be supported by RBS. This adds support for them, enabling syntax like `singleton(Array)[String, Integer]`. --- config.yml | 3 + docs/syntax.md | 25 +++--- ext/rbs_extension/ast_translation.c | 4 +- include/rbs/ast.h | 4 +- lib/rbs/types.rb | 114 +++++++++++++++------------ lib/rbs/unit_test/type_assertions.rb | 18 +++-- sig/types.rbs | 13 ++- src/ast.c | 4 +- src/parser.c | 25 +++++- test/rbs/singleton_type_test.rb | 91 +++++++++++++++++++++ test/rbs/type_parsing_test.rb | 13 +++ test/rbs/types_test.rb | 2 + 12 files changed, 230 insertions(+), 86 deletions(-) create mode 100644 test/rbs/singleton_type_test.rb diff --git a/config.yml b/config.yml index d03685def..c9932753c 100644 --- a/config.yml +++ b/config.yml @@ -573,8 +573,11 @@ nodes: fields: - name: name c_type: rbs_type_name + - name: args + c_type: rbs_node_list locations: - required: name + - optional: args - name: RBS::Types::Function rust_name: FunctionTypeNode expose_location: false diff --git a/docs/syntax.md b/docs/syntax.md index b53a0b4ed..f33f60a0d 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -3,17 +3,17 @@ ## Types ```markdown -_type_ ::= _class-name_ _type-arguments_ (Class instance type) - | _interface-name_ _type-arguments_ (Interface type) - | _alias-name_ _type-arguments_ (Alias type) - | `singleton(` _class-name_ `)` (Class singleton type) - | _literal_ (Literal type) - | _type_ `|` _type_ (Union type) - | _type_ `&` _type_ (Intersection type) - | _type_ `?` (Optional type) - | `{` _record-name_ `:` _type_ `,` etc. `}` (Record type) - | `[]` | `[` _type_ `,` etc. `]` (Tuples) - | _type-variable_ (Type variables) +_type_ ::= _class-name_ _type-arguments_ (Class instance type) + | _interface-name_ _type-arguments_ (Interface type) + | _alias-name_ _type-arguments_ (Alias type) + | `singleton(` _class-name_ `)` _type-arguments_ (Class singleton type) + | _literal_ (Literal type) + | _type_ `|` _type_ (Union type) + | _type_ `&` _type_ (Intersection type) + | _type_ `?` (Optional type) + | `{` _record-name_ `:` _type_ `,` etc. `}` (Record type) + | `[]` | `[` _type_ `,` etc. `]` (Tuples) + | _type-variable_ (Type variables) | `self` | `instance` | `class` @@ -85,7 +85,8 @@ Class singleton type denotes _the type of a singleton object of a class_. ```rbs singleton(String) -singleton(::Hash) # Class singleton type cannot be parametrized. +singleton(::Hash) # Class singleton type +singleton(Array)[String] # Class singleton type with type application ``` ### Literal type diff --git a/ext/rbs_extension/ast_translation.c b/ext/rbs_extension/ast_translation.c index 368580d90..04fe05d70 100644 --- a/ext/rbs_extension/ast_translation.c +++ b/ext/rbs_extension/ast_translation.c @@ -1229,10 +1229,12 @@ VALUE rbs_struct_to_ruby_value(rbs_translation_context_t ctx, rbs_node_t *instan VALUE h = rb_hash_new(); VALUE location = rbs_location_range_to_ruby_location(ctx, node->base.location); rbs_loc *loc = rbs_check_location(location); - rbs_loc_legacy_alloc_children(loc, 1); + rbs_loc_legacy_alloc_children(loc, 2); rbs_loc_legacy_add_required_child(loc, rb_intern("name"), (rbs_loc_range) { .start = node->name_range.start_char, .end = node->name_range.end_char }); + rbs_loc_legacy_add_optional_child(loc, rb_intern("args"), (rbs_loc_range) { .start = node->args_range.start_char, .end = node->args_range.end_char }); rb_hash_aset(h, ID2SYM(rb_intern("location")), location); rb_hash_aset(h, ID2SYM(rb_intern("name")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->name)); // rbs_type_name + rb_hash_aset(h, ID2SYM(rb_intern("args")), rbs_node_list_to_ruby_array(ctx, node->args)); return CLASS_NEW_INSTANCE( RBS_Types_ClassSingleton, diff --git a/include/rbs/ast.h b/include/rbs/ast.h index 67483809a..b3f6aa302 100644 --- a/include/rbs/ast.h +++ b/include/rbs/ast.h @@ -772,8 +772,10 @@ typedef struct rbs_types_class_singleton { rbs_node_t base; struct rbs_type_name *name; + struct rbs_node_list *args; rbs_location_range name_range; /* Required */ + rbs_location_range args_range; /* Optional */ } rbs_types_class_singleton_t; typedef struct rbs_types_function { @@ -947,7 +949,7 @@ rbs_types_bases_top_t *rbs_types_bases_top_new(rbs_allocator_t *allocator, rbs_l rbs_types_bases_void_t *rbs_types_bases_void_new(rbs_allocator_t *allocator, rbs_location_range location); rbs_types_block_t *rbs_types_block_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_node_t *type, bool required, rbs_node_t *self_type); rbs_types_class_instance_t *rbs_types_class_instance_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_type_name_t *name, rbs_node_list_t *args, rbs_location_range name_range); -rbs_types_class_singleton_t *rbs_types_class_singleton_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_type_name_t *name, rbs_location_range name_range); +rbs_types_class_singleton_t *rbs_types_class_singleton_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_type_name_t *name, rbs_node_list_t *args, rbs_location_range name_range); rbs_types_function_t *rbs_types_function_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_node_list_t *required_positionals, rbs_node_list_t *optional_positionals, rbs_node_t *rest_positionals, rbs_node_list_t *trailing_positionals, rbs_hash_t *required_keywords, rbs_hash_t *optional_keywords, rbs_node_t *rest_keywords, rbs_node_t *return_type); rbs_types_function_param_t *rbs_types_function_param_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_node_t *type, rbs_ast_symbol_t *name); rbs_types_interface_t *rbs_types_interface_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_type_name_t *name, rbs_node_list_t *args, rbs_location_range name_range); diff --git a/lib/rbs/types.rb b/lib/rbs/types.rb index 52a9139f4..9c95949bd 100644 --- a/lib/rbs/types.rb +++ b/lib/rbs/types.rb @@ -199,58 +199,6 @@ def with_nonreturn_void? end end - class ClassSingleton - attr_reader :name - attr_reader :location - - def initialize(name:, location:) - @name = name - @location = location - end - - def ==(other) - other.is_a?(ClassSingleton) && other.name == name - end - - alias eql? == - - def hash - self.class.hash ^ name.hash - end - - include NoFreeVariables - include NoSubst - - def to_json(state = nil) - { class: :class_singleton, name: name, location: location }.to_json(state) - end - - def to_s(level = 0) - "singleton(#{name})" - end - - include EmptyEachType - - def map_type_name(&) - ClassSingleton.new( - name: yield(name, location, self), - location: location - ) - end - - def has_self_type? - false - end - - def has_classish_type? - false - end - - def with_nonreturn_void? - false - end - end - module Application attr_reader :name attr_reader :args @@ -309,6 +257,68 @@ def with_nonreturn_void? end end + class ClassSingleton + attr_reader :location + + include Application + + def initialize(name:, location:, args: []) + @name = name + @location = location + @args = args + end + + def ==(other) + other.is_a?(ClassSingleton) && other.name == name && other.args == args + end + + alias eql? == + + def hash + self.class.hash ^ name.hash ^ args.hash + end + + def sub(s) + return self if s.empty? + + self.class.new(name: name, + args: args.map {|ty| ty.sub(s) }, + location: location) + end + + def to_json(state = _ = nil) + { class: :class_singleton, name: name, args: args, location: location }.to_json(state) + end + + def to_s(level = 0) + if args.empty? + "singleton(#{name})" + else + "singleton(#{name})[#{args.join(", ")}]" + end + end + + def map_type_name(&block) + ClassSingleton.new( + name: yield(name, location, self), + args: args.map {|type| type.map_type_name(&block) }, + location: location + ) + end + + def map_type(&block) + if block + ClassSingleton.new( + name: name, + args: args.map {|type| yield type }, + location: location + ) + else + enum_for :map_type + end + end + end + class Interface attr_reader :location diff --git a/lib/rbs/unit_test/type_assertions.rb b/lib/rbs/unit_test/type_assertions.rb index 44847ef0c..92eb496c3 100644 --- a/lib/rbs/unit_test/type_assertions.rb +++ b/lib/rbs/unit_test/type_assertions.rb @@ -256,15 +256,17 @@ def method_defs(method) type, definition = target case type - when Types::ClassInstance - subst = RBS::Substitution.build(definition.type_params, type.args) - definition.methods[method].defs.map do |type_def| - type_def.update( - type: type_def.type.sub(subst) - ) + when Types::ClassInstance, Types::ClassSingleton + if type.is_a?(Types::ClassSingleton) && type.args.empty? + definition.methods[method].defs + else + subst = RBS::Substitution.build(definition.type_params, type.args) + definition.methods[method].defs.map do |type_def| + type_def.update( + type: type_def.type.sub(subst) + ) + end end - when Types::ClassSingleton - definition.methods[method].defs else raise end diff --git a/sig/types.rbs b/sig/types.rbs index 38b3f75dc..2c806de7a 100644 --- a/sig/types.rbs +++ b/sig/types.rbs @@ -172,24 +172,23 @@ module RBS class ClassSingleton # singleton(::Foo) # ^^^^^ => name - type loc = Location[:name, bot] - - def initialize: (name: TypeName, location: loc?) -> void + type loc = Location[:name, :args] - attr_reader name: TypeName + def initialize: (name: TypeName, location: loc?, ?args: Array[t]) -> void attr_reader location: loc? include _TypeBase - include NoFreeVariables - include NoSubst - include EmptyEachType + include Application def ==: (untyped other) -> bool alias eql? == def hash: () -> Integer + + def map_type: () { (t) -> t } -> ClassSingleton + | () -> Enumerator[t, ClassSingleton] end module Application diff --git a/src/ast.c b/src/ast.c index d664ab27d..f5305a59b 100644 --- a/src/ast.c +++ b/src/ast.c @@ -1303,7 +1303,7 @@ rbs_types_class_instance_t *rbs_types_class_instance_new(rbs_allocator_t *alloca return instance; } #line 140 "prism/templates/src/ast.c.erb" -rbs_types_class_singleton_t *rbs_types_class_singleton_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_type_name_t *name, rbs_location_range name_range) { +rbs_types_class_singleton_t *rbs_types_class_singleton_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_type_name_t *name, rbs_node_list_t *args, rbs_location_range name_range) { rbs_types_class_singleton_t *instance = rbs_allocator_alloc(allocator, rbs_types_class_singleton_t); *instance = (rbs_types_class_singleton_t) { @@ -1312,7 +1312,9 @@ rbs_types_class_singleton_t *rbs_types_class_singleton_new(rbs_allocator_t *allo .location = location, }, .name = name, + .args = args, .name_range = name_range, + .args_range = RBS_LOCATION_NULL_RANGE, }; return instance; diff --git a/src/parser.c b/src/parser.c index 533dbbb69..428180a9e 100644 --- a/src/parser.c +++ b/src/parser.c @@ -1042,10 +1042,10 @@ static bool parse_instance_type(rbs_parser_t *parser, bool parse_alias, rbs_node } /* - singleton_type ::= {`singleton`} `(` type_name <`)`> + singleton_type ::= {`singleton`} `(` type_name <`)`> type_args? */ NODISCARD -static bool parse_singleton_type(rbs_parser_t *parser, rbs_types_class_singleton_t **singleton) { +static bool parse_singleton_type(rbs_parser_t *parser, rbs_types_class_singleton_t **singleton, bool self_allowed, bool classish_allowed) { ASSERT_TOKEN(parser, kSINGLETON); rbs_range_t type_range; @@ -1058,9 +1058,26 @@ static bool parse_singleton_type(rbs_parser_t *parser, rbs_types_class_singleton CHECK_PARSE(parse_type_name(parser, CLASS_NAME, &name_range, &type_name)); ADVANCE_ASSERT(parser, pRPAREN); + + rbs_node_list_t *types = rbs_node_list_new(ALLOCATOR()); + + rbs_location_range args_range = RBS_LOCATION_NULL_RANGE; + if (parser->next_token.type == pLBRACKET) { + rbs_parser_advance(parser); + args_range.start_byte = parser->current_token.range.start.byte_pos; + args_range.start_char = parser->current_token.range.start.char_pos; + CHECK_PARSE(parse_type_list(parser, pRBRACKET, types, true, self_allowed, classish_allowed)); + ADVANCE_ASSERT(parser, pRBRACKET); + args_range.end_byte = parser->current_token.range.end.byte_pos; + args_range.end_char = parser->current_token.range.end.char_pos; + } + type_range.end = parser->current_token.range.end; + rbs_location_range loc = RBS_RANGE_LEX2AST(type_range); + + *singleton = rbs_types_class_singleton_new(ALLOCATOR(), loc, type_name, types, RBS_RANGE_LEX2AST(name_range)); + (*singleton)->args_range = args_range; - *singleton = rbs_types_class_singleton_new(ALLOCATOR(), RBS_RANGE_LEX2AST(type_range), type_name, RBS_RANGE_LEX2AST(name_range)); return true; } @@ -1242,7 +1259,7 @@ static bool parse_simple(rbs_parser_t *parser, rbs_node_t **type, bool void_allo } case kSINGLETON: { rbs_types_class_singleton_t *singleton = NULL; - CHECK_PARSE(parse_singleton_type(parser, &singleton)); + CHECK_PARSE(parse_singleton_type(parser, &singleton, self_allowed, classish_allowed)); *type = (rbs_node_t *) singleton; return true; } diff --git a/test/rbs/singleton_type_test.rb b/test/rbs/singleton_type_test.rb new file mode 100644 index 000000000..a22be3842 --- /dev/null +++ b/test/rbs/singleton_type_test.rb @@ -0,0 +1,91 @@ +require "test_helper" + +class RBS::SingletonTypeTest < Test::Unit::TestCase + include TestHelper + + Parser = RBS::Parser + Buffer = RBS::Buffer + Types = RBS::Types + TypeName = RBS::TypeName + Namespace = RBS::Namespace + + def test_singleton_type_with_arguments + Parser.parse_type("singleton(Array)[String]").yield_self do |type| + assert_instance_of Types::ClassSingleton, type + assert_equal TypeName.new(namespace: Namespace.empty, name: :Array), type.name + assert_equal 1, type.args.size + assert_instance_of Types::ClassInstance, type.args[0] + assert_equal TypeName.new(namespace: Namespace.empty, name: :String), type.args[0].name + assert_equal "singleton(Array)[String]", type.location.source + assert_equal "[String]", type.location[:args].source + end + + Parser.parse_type("singleton(Hash)[Symbol, Integer]").yield_self do |type| + assert_instance_of Types::ClassSingleton, type + assert_equal TypeName.new(namespace: Namespace.empty, name: :Hash), type.name + assert_equal 2, type.args.size + assert_instance_of Types::ClassInstance, type.args[0] + assert_instance_of Types::ClassInstance, type.args[1] + assert_equal TypeName.new(namespace: Namespace.empty, name: :Symbol), type.args[0].name + assert_equal TypeName.new(namespace: Namespace.empty, name: :Integer), type.args[1].name + assert_equal "singleton(Hash)[Symbol, Integer]", type.location.source + assert_equal "[Symbol, Integer]", type.location[:args].source + end + + Parser.parse_type("singleton(::Foo::Bar)[Baz]").yield_self do |type| + assert_instance_of Types::ClassSingleton, type + assert_equal TypeName.new(namespace: Namespace.parse("::Foo"), name: :Bar), type.name + assert_equal 1, type.args.size + assert_instance_of Types::ClassInstance, type.args[0] + assert_equal TypeName.new(namespace: Namespace.empty, name: :Baz), type.args[0].name + assert_equal "singleton(::Foo::Bar)[Baz]", type.location.source + assert_equal "[Baz]", type.location[:args].source + end + end + + def test_singleton_type_equality + type1 = parse_type("singleton(Array)[String]") + type2 = parse_type("singleton(Array)[String]") + type3 = parse_type("singleton(Array)[Integer]") + type4 = parse_type("singleton(Hash)[String]") + + assert_equal type1, type2 + refute_equal type1, type3 + refute_equal type1, type4 + end + + def test_singleton_type_hash + type1 = parse_type("singleton(Array)[String]") + type2 = parse_type("singleton(Array)[String]") + type3 = parse_type("singleton(Array)[Integer]") + + assert_equal type1.hash, type2.hash + refute_equal type1.hash, type3.hash + end + + def test_singleton_type_sub + type = parse_type("singleton(Array)[T]", variables: [:T]) + subst = RBS::Substitution.build([:T], [parse_type("String")]) + + result = type.sub(subst) + assert_instance_of Types::ClassSingleton, result + assert_equal TypeName.new(namespace: Namespace.empty, name: :Array), result.name + assert_equal 1, result.args.size + assert_instance_of Types::ClassInstance, result.args[0] + assert_equal TypeName.new(namespace: Namespace.empty, name: :String), result.args[0].name + end + + def test_singleton_type_map_type_name + type = parse_type("singleton(Array)[String]") + + mapped = type.map_type_name do |name, _, _| + TypeName.new(namespace: Namespace.empty, name: :List) + end + + assert_instance_of Types::ClassSingleton, mapped + assert_equal TypeName.new(namespace: Namespace.empty, name: :List), mapped.name + assert_equal 1, mapped.args.size + assert_instance_of Types::ClassInstance, mapped.args[0] + assert_equal TypeName.new(namespace: Namespace.empty, name: :List), mapped.args[0].name + end +end diff --git a/test/rbs/type_parsing_test.rb b/test/rbs/type_parsing_test.rb index 07b3c5875..0a9214052 100644 --- a/test/rbs/type_parsing_test.rb +++ b/test/rbs/type_parsing_test.rb @@ -255,6 +255,7 @@ def test_class_singleton assert_instance_of Types::ClassSingleton, type assert_equal TypeName.new(namespace: Namespace.empty, name: :Object), type.name + assert_equal [], type.args assert_equal "singleton(Object)", type.location.source end @@ -263,10 +264,22 @@ def test_class_singleton assert_instance_of Types::ClassSingleton, type assert_equal TypeName.new(namespace: Namespace.root, name: :Object), type.name + assert_equal [], type.args assert_equal "singleton(::Object)", type.location.source end + Parser.parse_type("singleton(Array)[String]").yield_self do |type| + assert_instance_of Types::ClassSingleton, type + + assert_equal TypeName.new(namespace: Namespace.empty, name: :Array), type.name + assert_equal 1, type.args.size + assert_instance_of Types::ClassInstance, type.args[0] + assert_equal TypeName.new(namespace: Namespace.empty, name: :String), type.args[0].name + + assert_equal "singleton(Array)[String]", type.location.source + end + assert_raises RBS::ParsingError do Parser.parse_type("singleton(foo)") end diff --git a/test/rbs/types_test.rb b/test/rbs/types_test.rb index b4ba8e882..3a55e57dc 100644 --- a/test/rbs/types_test.rb +++ b/test/rbs/types_test.rb @@ -32,6 +32,8 @@ def test_to_s assert_equal "^(bool flag, ?untyped, *Symbol, name: String, ?email: nil, **Symbol) -> void", parse_type("^(bool flag, ?untyped, *Symbol, name: String, ?email: nil, **Symbol) -> void").to_s assert_equal "^(untyped untyped, untyped footype) -> void", parse_type("^(untyped `untyped`, untyped footype) -> void").to_s assert_equal "^(`foo`: untyped) -> void", parse_type("^(`foo`: untyped) -> void").to_s + assert_equal "singleton(Array)[String]", parse_type("singleton(Array)[String]").to_s + assert_equal "singleton(Hash)[Symbol, Integer]", parse_type("singleton(Hash)[Symbol, Integer]").to_s end def test_has_self_type?