diff --git a/docs/syntax.md b/docs/syntax.md index 3bed14acd..477675f81 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -45,6 +45,7 @@ _literal_ ::= _string-literal_ | `false` _proc_ ::= `^` _parameters?_ _self-type-binding?_ _block?_ `->` _type_ + | `^` `(` `?` `)` _self-type-binding?_ _block?_ `->` _type_ # Proc type with untyped parameter ``` ### Class instance type @@ -310,6 +311,7 @@ end ```markdown _method-type_ ::= _parameters?_ _block?_ `->` _type_ # Method type + | `(` `?` `)` `->` _type_ # Method type with untyped parameters _parameters?_ ::= (Empty) | _parameters_ (Parameters) @@ -333,9 +335,11 @@ _var-name_ ::= /[a-z]\w*/ _self-type-binding?_ = (Empty) | `[` `self` `:` _type_ `]` (Self type binding) -_block?_ = (No block) +_block?_ = (No block) | `{` _parameters_ _self-type-binding?_ `->` _type_ `}` (Block) + | `{` `(` `?` `)` `->` _type_ `}` (Block with untyped parameters) | `?` `{` _parameters_ _self-type-binding?_ `->` _type_ `}` (Optional block) + | `?` `{` `(` `?` `)` `->` _type_ `}` (Optional block with untyped parameters) ``` ### Parameters diff --git a/ext/rbs_extension/constants.c b/ext/rbs_extension/constants.c index 57ec73ee3..0e21a1860 100644 --- a/ext/rbs_extension/constants.c +++ b/ext/rbs_extension/constants.c @@ -61,6 +61,7 @@ VALUE RBS_Types_ClassInstance; VALUE RBS_Types_ClassSingleton; VALUE RBS_Types_Function_Param; VALUE RBS_Types_Function; +VALUE RBS_Types_UntypedFunction; VALUE RBS_Types_Interface; VALUE RBS_Types_Intersection; VALUE RBS_Types_Literal; @@ -138,6 +139,7 @@ void rbs__init_constants(void) { IMPORT_CONSTANT(RBS_Types_ClassInstance, RBS_Types, "ClassInstance"); IMPORT_CONSTANT(RBS_Types_ClassSingleton, RBS_Types, "ClassSingleton"); IMPORT_CONSTANT(RBS_Types_Function, RBS_Types, "Function"); + IMPORT_CONSTANT(RBS_Types_UntypedFunction, RBS_Types, "UntypedFunction"); IMPORT_CONSTANT(RBS_Types_Function_Param, RBS_Types_Function, "Param"); IMPORT_CONSTANT(RBS_Types_Interface, RBS_Types, "Interface"); IMPORT_CONSTANT(RBS_Types_Intersection, RBS_Types, "Intersection"); diff --git a/ext/rbs_extension/constants.h b/ext/rbs_extension/constants.h index 978bc32d0..2d1ca63bf 100644 --- a/ext/rbs_extension/constants.h +++ b/ext/rbs_extension/constants.h @@ -64,6 +64,7 @@ extern VALUE RBS_Types_ClassInstance; extern VALUE RBS_Types_ClassSingleton; extern VALUE RBS_Types_Function_Param; extern VALUE RBS_Types_Function; +extern VALUE RBS_Types_UntypedFunction; extern VALUE RBS_Types_Interface; extern VALUE RBS_Types_Intersection; extern VALUE RBS_Types_Literal; diff --git a/ext/rbs_extension/parser.c b/ext/rbs_extension/parser.c index f040a1e86..04fc79a80 100644 --- a/ext/rbs_extension/parser.c +++ b/ext/rbs_extension/parser.c @@ -52,6 +52,10 @@ typedef struct { VALUE rest_keywords; } method_params; +static bool rbs_is_untyped_params(method_params *params) { + return NIL_P(params->required_positionals); +} + // /** // * Returns RBS::Location object of `current_token` of a parser state. // * @@ -362,6 +366,7 @@ static bool is_keyword(parserstate *state) { /* params ::= {} `)` + | {} `?` `)` -- Untyped function params (assign params.required = nil) | `)` | `,` `)` @@ -389,6 +394,11 @@ static bool is_keyword(parserstate *state) { | {} `**` */ static void parse_params(parserstate *state, method_params *params) { + if (state->next_token.type == pQUESTION && state->next_token2.type == pRPAREN) { + params->required_positionals = Qnil; + parser_advance(state); + return; + } if (state->next_token.type == pRPAREN) { return; } @@ -613,6 +623,13 @@ static void parse_function(parserstate *state, VALUE *function, VALUE *block, VA parser_advance_assert(state, pRPAREN); } + // Untyped method parameter means it cannot have block + if (rbs_is_untyped_params(¶ms)) { + if (state->next_token.type != pARROW) { + raise_syntax_error(state, state->next_token2, "A method type with untyped method parameter cannot have block"); + } + } + // Passing NULL to function_self_type means the function itself doesn't accept self type binding. (== method type) if (function_self_type) { *function_self_type = parse_self_type_binding(state); @@ -641,8 +658,11 @@ static void parse_function(parserstate *state, VALUE *function, VALUE *block, VA parser_advance_assert(state, pARROW); VALUE block_return_type = parse_optional(state); - *block = rbs_block( - rbs_function( + VALUE block_function = Qnil; + if (rbs_is_untyped_params(&block_params)) { + block_function = rbs_untyped_function(block_return_type); + } else { + block_function = rbs_function( block_params.required_positionals, block_params.optional_positionals, block_params.rest_positionals, @@ -651,10 +671,10 @@ static void parse_function(parserstate *state, VALUE *function, VALUE *block, VA block_params.optional_keywords, block_params.rest_keywords, block_return_type - ), - required, - block_self_type - ); + ); + } + + *block = rbs_block(block_function, required, block_self_type); parser_advance_assert(state, pRBRACE); } @@ -662,16 +682,20 @@ static void parse_function(parserstate *state, VALUE *function, VALUE *block, VA parser_advance_assert(state, pARROW); VALUE type = parse_optional(state); - *function = rbs_function( - params.required_positionals, - params.optional_positionals, - params.rest_positionals, - params.trailing_positionals, - params.required_keywords, - params.optional_keywords, - params.rest_keywords, - type - ); + if (rbs_is_untyped_params(¶ms)) { + *function = rbs_untyped_function(type); + } else { + *function = rbs_function( + params.required_positionals, + params.optional_positionals, + params.rest_positionals, + params.trailing_positionals, + params.required_keywords, + params.optional_keywords, + params.rest_keywords, + type + ); + } } /* diff --git a/ext/rbs_extension/ruby_objs.c b/ext/rbs_extension/ruby_objs.c index 2a92d1c56..0e9760ebb 100644 --- a/ext/rbs_extension/ruby_objs.c +++ b/ext/rbs_extension/ruby_objs.c @@ -170,6 +170,17 @@ VALUE rbs_function_param(VALUE type, VALUE name, VALUE location) { ); } +VALUE rbs_untyped_function(VALUE return_type) { + VALUE args = rb_hash_new(); + rb_hash_aset(args, ID2SYM(rb_intern("return_type")), return_type); + + return CLASS_NEW_INSTANCE( + RBS_Types_UntypedFunction, + 1, + &args + ); +} + VALUE rbs_function( VALUE required_positional_params, VALUE optional_positional_params, diff --git a/ext/rbs_extension/ruby_objs.h b/ext/rbs_extension/ruby_objs.h index d9ee32262..bf5ea0763 100644 --- a/ext/rbs_extension/ruby_objs.h +++ b/ext/rbs_extension/ruby_objs.h @@ -30,6 +30,7 @@ VALUE rbs_class_instance(VALUE typename, VALUE type_args, VALUE location); VALUE rbs_class_singleton(VALUE typename, VALUE location); VALUE rbs_function_param(VALUE type, VALUE name, VALUE location); VALUE rbs_function(VALUE required_positional_params, VALUE optional_positional_params, VALUE rest_positional_params, VALUE trailing_positional_params, VALUE required_keywords, VALUE optional_keywords, VALUE rest_keywords, VALUE return_type); +VALUE rbs_untyped_function(VALUE return_type); VALUE rbs_interface(VALUE typename, VALUE type_args, VALUE location); VALUE rbs_intersection(VALUE types, VALUE location); VALUE rbs_literal(VALUE literal, VALUE location); diff --git a/lib/rbs/prototype/helpers.rb b/lib/rbs/prototype/helpers.rb index 05285926c..f1d116f1f 100644 --- a/lib/rbs/prototype/helpers.rb +++ b/lib/rbs/prototype/helpers.rb @@ -37,13 +37,9 @@ def block_from_body(node) end end - method_block = Types::Block.new( - required: required, - type: Types::Function.empty(untyped), - self_type: nil - ) - if yields + function = Types::Function.empty(untyped) + yields.each do |yield_node| array_content = yield_node.children[0]&.children&.compact || [] @@ -54,9 +50,9 @@ def block_from_body(node) [array_content, nil] end - if (diff = positionals.size - method_block.type.required_positionals.size) > 0 + if (diff = positionals.size - function.required_positionals.size) > 0 diff.times do - method_block.type.required_positionals << Types::Function::Param.new( + function.required_positionals << Types::Function::Param.new( type: untyped, name: nil ) @@ -67,7 +63,7 @@ def block_from_body(node) keywords.children[0].children.each_slice(2) do |key_node, value_node| if key_node key = key_node.children[0] - method_block.type.required_keywords[key] ||= + function.required_keywords[key] ||= Types::Function::Param.new( type: untyped, name: nil @@ -76,10 +72,13 @@ def block_from_body(node) end end end + else + function = Types::UntypedFunction.new(return_type: untyped) end - end - method_block + + Types::Block.new(required: required, type: function, self_type: nil) + end end def each_child(node, &block) diff --git a/lib/rbs/prototype/rbi.rb b/lib/rbs/prototype/rbi.rb index 7c96267d8..e5d07188e 100644 --- a/lib/rbs/prototype/rbi.rb +++ b/lib/rbs/prototype/rbi.rb @@ -317,7 +317,11 @@ def method_type(args_node, type_node, variables:, overloads:) Types::Function::Param.new(name: name, type: type) end - method_type.update(type: method_type.type.update(required_positionals: required_positionals)) + if method_type.type.is_a?(RBS::Types::Function) + method_type.update(type: method_type.type.update(required_positionals: required_positionals)) + else + method_type + end end when :type_parameters type_params = [] @@ -446,18 +450,22 @@ def parse_params(args_node, args, method_type, variables:, overloads:) end end - method_type.update( - type: method_type.type.update( - required_positionals: required_positionals, - optional_positionals: optional_positionals, - rest_positionals: rest_positionals, - trailing_positionals: trailing_positionals, - required_keywords: required_keywords, - optional_keywords: optional_keywords, - rest_keywords: rest_keywords - ), - block: method_block - ) + if method_type.type.is_a?(Types::Function) + method_type.update( + type: method_type.type.update( + required_positionals: required_positionals, + optional_positionals: optional_positionals, + rest_positionals: rest_positionals, + trailing_positionals: trailing_positionals, + required_keywords: required_keywords, + optional_keywords: optional_keywords, + rest_keywords: rest_keywords + ), + block: method_block + ) + else + method_type + end end def type_of(type_node, variables:) diff --git a/lib/rbs/types.rb b/lib/rbs/types.rb index 9cb9a76a6..f8107e367 100644 --- a/lib/rbs/types.rb +++ b/lib/rbs/types.rb @@ -1202,6 +1202,84 @@ def with_nonreturn_void? end end + class UntypedFunction + attr_reader :return_type + + def initialize(return_type:) + @return_type = return_type + end + + def free_variables(acc = Set.new) + return_type.free_variables(acc) + end + + def map_type(&block) + if block + update(return_type: yield(return_type)) + else + enum_for :map_type + end + end + + def each_type(&block) + if block + yield return_type + else + enum_for :each_type + end + end + + def each_param(&block) + if block + # noop + else + enum_for :each_param + end + end + + def to_json(state = _ = nil) + { + return_type: return_type + }.to_json(state) + end + + def sub(subst) + map_type { _1.sub(subst) } + end + + def with_return_type(ty) + update(return_type: ty) + end + + def update(return_type: self.return_type) + UntypedFunction.new(return_type: return_type) + end + + def empty? + true + end + + def has_self_type? + return_type.has_self_type? + end + + def has_classish_type? + return_type.has_classish_type? + end + + def with_nonreturn_void + false + end + + def param_to_s + "?" + end + + def return_to_s + return_type.to_s(1) + end + end + class Block attr_reader :type attr_reader :required diff --git a/sig/method_types.rbs b/sig/method_types.rbs index 1e81746ba..bf410ccdf 100644 --- a/sig/method_types.rbs +++ b/sig/method_types.rbs @@ -14,11 +14,11 @@ module RBS type loc = def_loc | attr_loc attr_reader type_params: Array[AST::TypeParam] - attr_reader type: Types::Function + attr_reader type: Types::function attr_reader block: Types::Block? attr_reader location: loc? - def initialize: (type_params: Array[AST::TypeParam], type: Types::Function, block: Types::Block?, location: loc?) -> void + def initialize: (type_params: Array[AST::TypeParam], type: Types::function, block: Types::Block?, location: loc?) -> void def ==: (untyped other) -> bool @@ -29,7 +29,7 @@ module RBS # def sub: (Substitution) -> MethodType - def update: (?type_params: Array[AST::TypeParam], ?type: Types::Function, ?block: Types::Block?, ?location: loc?) -> MethodType + def update: (?type_params: Array[AST::TypeParam], ?type: Types::function, ?block: Types::Block?, ?location: loc?) -> MethodType def free_variables: (?Set[Symbol] set) -> Set[Symbol] diff --git a/sig/types.rbs b/sig/types.rbs index 5412ebc18..1b1dfa759 100644 --- a/sig/types.rbs +++ b/sig/types.rbs @@ -448,13 +448,58 @@ module RBS def with_nonreturn_void?: () -> bool end + # Function type without type checking arguments + # + class UntypedFunction + attr_reader return_type: t + + def initialize: (return_type: t) -> void + + def free_variables: (?Set[Symbol]) -> Set[Symbol] + + def map_type: { (t) -> t } -> UntypedFunction + | -> Enumerator[t, UntypedFunction] + + def map_type_name: () { (TypeName, Location[untyped, untyped]?, t) -> TypeName } -> UntypedFunction + + def each_type: () { (t) -> void } -> void + | -> Enumerator[t, void] + + def each_param: () { (Function::Param) -> void } -> void + | -> Enumerator[Function::Param, void] + + include _ToJson + + def sub: (Substitution) -> UntypedFunction + + def with_return_type: (t) -> UntypedFunction + + def update: (?return_type: t) -> UntypedFunction + + def empty?: () -> bool + + def has_self_type?: () -> bool + + def has_classish_type?: () -> bool + + def with_nonreturn_void?: () -> bool + + # Returns `?` + def param_to_s: () -> String + + # Returns `return_type.to_s(1)` + def return_to_s: () -> String + end + + type function = Types::Function | Types::UntypedFunction + class Block - attr_reader type: Types::Function + attr_reader type: function attr_reader required: bool attr_reader self_type: t? - def initialize: (type: Types::Function, ?self_type: t?, required: boolish) -> void + def initialize: (type: function, ?self_type: t?, required: boolish) -> void def ==: (untyped other) -> bool @@ -470,14 +515,14 @@ module RBS end class Proc - attr_reader type: Function + attr_reader type: function attr_reader block: Block? attr_reader self_type: t? type loc = Location[bot, bot] - def initialize: (location: loc?, type: Function, ?self_type: t?, block: Block?) -> void + def initialize: (location: loc?, type: function, ?self_type: t?, block: Block?) -> void include _TypeBase diff --git a/sig/variance_calculator.rbs b/sig/variance_calculator.rbs index 3294fd538..c42a5340c 100644 --- a/sig/variance_calculator.rbs +++ b/sig/variance_calculator.rbs @@ -73,14 +73,14 @@ module RBS def in_inherit: (name: TypeName, args: Array[Types::t], variables: Array[Symbol]) -> Result # The type name must be normalized - # + # def in_type_alias: (name: TypeName) -> Result private def type: (Types::t, result: Result, context: variance) -> void - def function: (Types::Function, result: Result, context: variance) -> void + def function: (Types::function, result: Result, context: variance) -> void def negate: (variance) -> variance end diff --git a/test/rbs/parser_test.rb b/test/rbs/parser_test.rb index ecd5b6c01..a29d992ac 100644 --- a/test/rbs/parser_test.rb +++ b/test/rbs/parser_test.rb @@ -751,4 +751,20 @@ def test_parse_require_eof RBS::Parser.parse_method_type("() -> void () -> void", range: 0..., require_eof: true) end end + + def test_proc__untyped_function + RBS::Parser.parse_type("^(?) -> Integer").tap do |type| + assert_instance_of RBS::Types::UntypedFunction, type.type + end + + RBS::Parser.parse_type("^() { (?) -> String } -> Integer").tap do |type| + assert_instance_of RBS::Types::UntypedFunction, type.block.type + end + end + + def test_proc__untyped_function_parse_error + assert_raises(RBS::ParsingError) do + RBS::Parser.parse_type("^(?) { (?) -> void } -> Integer") + end + end end diff --git a/test/rbs/rb_prototype_test.rb b/test/rbs/rb_prototype_test.rb index 9d07df9b2..cd19b2d6d 100644 --- a/test/rbs/rb_prototype_test.rb +++ b/test/rbs/rb_prototype_test.rb @@ -62,7 +62,7 @@ def kw_req(a:) end assert_write parser.decls, <<-EOF class Hello - def hello: (untyped a, ?::Integer b, *untyped c, untyped d, e: untyped, ?f: ::Integer, **untyped g) { () -> untyped } -> nil + def hello: (untyped a, ?::Integer b, *untyped c, untyped d, e: untyped, ?f: ::Integer, **untyped g) { (?) -> untyped } -> nil def self.world: () { (untyped, untyped, untyped, x: untyped, y: untyped) -> untyped } -> untyped @@ -1070,14 +1070,14 @@ def foo(...) end # Ruby <=3.3 generates AST without kwrest args for `...` args assert_write parser.decls, <<~RBS module M - def foo: (*untyped) ?{ () -> untyped } -> nil + def foo: (*untyped) ?{ (?) -> untyped } -> nil end RBS else # Ruby 3.4 generates AST with kwrest args for `...` args assert_write parser.decls, <<~RBS module M - def foo: (*untyped, **untyped) ?{ () -> untyped } -> nil + def foo: (*untyped, **untyped) ?{ (?) -> untyped } -> nil end RBS end