Skip to content

Commit d508222

Browse files
authored
Merge pull request #2673 from ruby/module-name-normalization
Backport module name normalization
2 parents 6f8bcc6 + 19cf9ab commit d508222

14 files changed

Lines changed: 609 additions & 266 deletions

Rakefile

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ test_config = lambda do |t|
1717
t.test_files = FileList["test/**/*_test.rb"].reject do |path|
1818
path =~ %r{test/stdlib/}
1919
end
20+
if defined?(RubyMemcheck)
21+
if t.is_a?(RubyMemcheck::TestTask)
22+
t.verbose = true
23+
t.options = '-v'
24+
end
25+
end
2026
end
2127

2228
Rake::TestTask.new(test: :compile, &test_config)
@@ -225,13 +231,15 @@ task :stdlib_test => :compile do
225231
end
226232

227233
task :typecheck_test => :compile do
228-
FileList["test/typecheck/*"].each do |test|
229-
Dir.chdir(test) do
230-
expectations = File.join(test, "steep_expectations.yml")
231-
if File.exist?(expectations)
232-
sh "steep check --with_expectations"
233-
else
234-
sh "steep check"
234+
Bundler.with_unbundled_env do
235+
FileList["test/typecheck/*"].each do |test|
236+
Dir.chdir(test) do
237+
expectations = File.join(test, "steep_expectations.yml")
238+
if File.exist?(expectations)
239+
sh "#{__dir__}/bin/steep check --with_expectations"
240+
else
241+
sh "#{__dir__}/bin/steep check"
242+
end
235243
end
236244
end
237245
end
@@ -535,4 +543,4 @@ task :prepare_profiling do
535543
Rake::Task[:"clobber"].invoke
536544
Rake::Task[:"templates"].invoke
537545
Rake::Task[:"compile"].invoke
538-
end
546+
end

docs/aliases.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Aliases
2+
3+
This document explains module/class aliases and type aliases.
4+
5+
## Module/class alias
6+
7+
Module/class aliases give another name to a module/class.
8+
This is useful for some syntaxes that has lexical constraints.
9+
10+
```rbs
11+
class C
12+
end
13+
14+
class D = C # ::D is an alias for ::C
15+
16+
class E < D # ::E inherits from ::D, which is actually ::C
17+
end
18+
```
19+
20+
Note that module/class aliases cannot be recursive.
21+
22+
So, we can define a *normalization* of aliased module/class names.
23+
Normalization follows the chain of alias definitions and resolves them to the original module/class defined with `module`/`class` syntax.
24+
25+
```rbs
26+
class C
27+
end
28+
29+
class D = C
30+
class E = D
31+
```
32+
33+
`::E` is defined as an alias, and it can be normalized to `::C`.
34+
35+
## Type alias
36+
37+
The biggest difference from module/class alias is that type alias can be recursive.
38+
39+
```rbs
40+
# cons_cell type is defined recursively
41+
type cons_cell = nil
42+
| [Integer, cons_cell]
43+
```
44+
45+
This means type aliases *cannot be* normalized generally.
46+
So, we provide another operation for type alias, `DefinitionBuilder#expand_alias` and its family.
47+
It substitutes with the immediate right hand side of a type alias.
48+
49+
```
50+
cons_cell ===> nil | [Integer, cons_cell] (expand 1 step)
51+
===> nil | [Integer, nil | [Integer, cons_cell]] (expand 2 steps)
52+
===> ... (expand will go infinitely)
53+
```
54+
55+
Note that the namespace of a type alias *can be* normalized, because they are module names.
56+
57+
```rbs
58+
module M
59+
type t = String
60+
end
61+
62+
module N = M
63+
```
64+
65+
With the type definition above, a type `::N::t` can be normalized to `::M::t`.
66+
And then it can be expanded to `::String`.
67+
68+
> [!NOTE]
69+
> This is something like an *unfold* operation in type theory.
70+
71+
## Type name resolution
72+
73+
Type name resolution in RBS usually rewrites *relative* type names to *absolute* type names.
74+
`Environment#resolve_type_names` converts all type names in the RBS type definitions, and returns a new `Environment` object.
75+
76+
It also *normalizes* modules names in type names.
77+
78+
- If the type name can be resolved and normalized successfully, the AST has *absolute* type names.
79+
- If the type name resolution/normalization fails, the AST has *relative* type names.

lib/rbs/cli/validate.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def validate_class_module_definition
131131

132132
self_params =
133133
if self_type.name.class?
134-
@env.normalized_module_entry(self_type.name)&.type_params
134+
@env.module_entry(self_type.name, normalized: true)&.type_params
135135
else
136136
@env.interface_decls[self_type.name]&.decl&.type_params
137137
end
@@ -188,7 +188,7 @@ def validate_class_module_definition
188188
end
189189
params =
190190
if member.name.class?
191-
module_decl = @env.normalized_module_entry(member.name) or raise
191+
module_decl = @env.module_entry(member.name, normalized: true) or raise
192192
module_decl.type_params
193193
else
194194
interface_decl = @env.interface_decls.fetch(member.name)

lib/rbs/definition_builder/ancestor_builder.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def one_instance_ancestors(type_name)
220220
InvalidTypeApplicationError.check2!(type_name: super_class.name, args: super_class.args, env: env, location: super_class.location)
221221
end
222222

223-
super_entry = env.normalized_class_entry(super_name) or raise
223+
super_entry = env.class_entry(super_name, normalized: true) or raise
224224
super_args = AST::TypeParam.normalize_args(super_entry.type_params, super_args)
225225

226226
ancestors = OneAncestors.class_instance(
@@ -248,7 +248,7 @@ def one_instance_ancestors(type_name)
248248

249249
module_name = module_self.name
250250
if module_name.class?
251-
module_entry = env.normalized_module_class_entry(module_name) or raise
251+
module_entry = env.module_class_entry(module_name, normalized: true) or raise
252252
module_name = module_entry.name
253253
self_args = AST::TypeParam.normalize_args(module_entry.type_params, module_self.args)
254254
end
@@ -359,7 +359,7 @@ def mixin_ancestors0(decl, type_name, align_params:, included_modules:, included
359359
MixinClassError.check!(type_name: type_name, env: env, member: member)
360360
NoMixinFoundError.check!(member.name, env: env, member: member)
361361

362-
module_decl = env.normalized_module_entry(module_name) or raise
362+
module_decl = env.module_entry(module_name, normalized: true) or raise
363363
module_args = AST::TypeParam.normalize_args(module_decl.type_params, module_args)
364364

365365
module_name = env.normalize_module_name(module_name)
@@ -378,7 +378,7 @@ def mixin_ancestors0(decl, type_name, align_params:, included_modules:, included
378378
MixinClassError.check!(type_name: type_name, env: env, member: member)
379379
NoMixinFoundError.check!(member.name, env: env, member: member)
380380

381-
module_decl = env.normalized_module_entry(member.name) or raise
381+
module_decl = env.module_entry(member.name, normalized: true) or raise
382382
module_name = module_decl.name
383383

384384
module_args = member.args.map {|type| align_params ? type.sub(align_params) : type }
@@ -396,7 +396,7 @@ def mixin_ancestors0(decl, type_name, align_params:, included_modules:, included
396396
MixinClassError.check!(type_name: type_name, env: env, member: member)
397397
NoMixinFoundError.check!(member.name, env: env, member: member)
398398

399-
module_decl = env.normalized_module_entry(module_name) or raise
399+
module_decl = env.module_entry(module_name, normalized: true) or raise
400400
module_args = AST::TypeParam.normalize_args(module_decl.type_params, module_args)
401401

402402
module_name = env.normalize_module_name(module_name)

lib/rbs/environment.rb

Lines changed: 64 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -223,21 +223,17 @@ def class_alias?(name)
223223
end
224224
end
225225

226-
def class_entry(type_name)
227-
case
228-
when (class_entry = class_decls[type_name]).is_a?(ClassEntry)
229-
class_entry
230-
when (class_alias = class_alias_decls[type_name]).is_a?(ClassAliasEntry)
231-
class_alias
226+
def class_entry(type_name, normalized: false)
227+
case entry = constant_entry(type_name, normalized: normalized || false)
228+
when ClassEntry, ClassAliasEntry
229+
entry
232230
end
233231
end
234232

235-
def module_entry(type_name)
236-
case
237-
when (module_entry = class_decls[type_name]).is_a?(ModuleEntry)
238-
module_entry
239-
when (module_alias = class_alias_decls[type_name]).is_a?(ModuleAliasEntry)
240-
module_alias
233+
def module_entry(type_name, normalized: false)
234+
case entry = constant_entry(type_name, normalized: normalized || false)
235+
when ModuleEntry, ModuleAliasEntry
236+
entry
241237
end
242238
end
243239

@@ -253,26 +249,40 @@ def normalized_class_entry(type_name)
253249
end
254250

255251
def normalized_module_entry(type_name)
256-
if name = normalize_module_name?(type_name)
257-
case entry = module_entry(name)
258-
when ModuleEntry, nil
259-
entry
260-
when ModuleAliasEntry
261-
raise
262-
end
263-
end
252+
module_entry(type_name, normalized: true)
264253
end
265254

266-
def module_class_entry(type_name)
267-
class_entry(type_name) || module_entry(type_name)
255+
def module_class_entry(type_name, normalized: false)
256+
entry = constant_entry(type_name, normalized: normalized || false)
257+
if entry.is_a?(ConstantEntry)
258+
nil
259+
else
260+
entry
261+
end
268262
end
269263

270264
def normalized_module_class_entry(type_name)
271-
normalized_class_entry(type_name) || normalized_module_entry(type_name)
265+
module_class_entry(type_name, normalized: true)
272266
end
273267

274-
def constant_entry(type_name)
275-
class_entry(type_name) || module_entry(type_name) || constant_decls[type_name]
268+
def constant_entry(type_name, normalized: false)
269+
if normalized
270+
if normalized_name = normalize_module_name?(type_name)
271+
class_decls.fetch(normalized_name, nil)
272+
else
273+
# The type_name may be declared with constant declaration
274+
unless type_name.namespace.empty?
275+
parent = type_name.namespace.to_type_name
276+
normalized_parent = normalize_module_name?(parent) or return
277+
constant_name = TypeName.new(name: type_name.name, namespace: normalized_parent.to_namespace)
278+
constant_decls.fetch(constant_name, nil)
279+
end
280+
end
281+
else
282+
class_decls.fetch(type_name, nil) ||
283+
class_alias_decls.fetch(type_name, nil) ||
284+
constant_decls.fetch(type_name, nil)
285+
end
276286
end
277287

278288
def normalize_type_name?(name)
@@ -307,6 +317,10 @@ def normalize_type_name!(name)
307317
end
308318
end
309319

320+
def normalize_type_name(name)
321+
normalize_type_name?(name) || name
322+
end
323+
310324
def normalized_type_name?(type_name)
311325
case
312326
when type_name.interface?
@@ -321,53 +335,44 @@ def normalized_type_name?(type_name)
321335
end
322336

323337
def normalized_type_name!(name)
324-
normalized_type_name?(name) or raise "Normalized type name is expected but given `#{name}`, which is normalized to `#{normalize_type_name?(name)}`"
338+
normalized_type_name?(name) or raise "Normalized type name is expected but given `#{name}`"
325339
name
326340
end
327341

328-
def normalize_type_name(name)
329-
normalize_type_name?(name) || name
330-
end
331-
332-
def normalize_module_name(name)
333-
normalize_module_name?(name) or name
334-
end
335-
336342
def normalize_module_name?(name)
337343
raise "Class/module name is expected: #{name}" unless name.class?
338344
name = name.absolute! unless name.absolute?
339345

340-
if @normalize_module_name_cache.key?(name)
341-
return @normalize_module_name_cache[name]
346+
original_name = name
347+
348+
if @normalize_module_name_cache.key?(original_name)
349+
return @normalize_module_name_cache[original_name]
342350
end
343351

344-
unless name.namespace.empty?
345-
parent = name.namespace.to_type_name
346-
if normalized_parent = normalize_module_name?(parent)
347-
type_name = TypeName.new(namespace: normalized_parent.to_namespace, name: name.name)
348-
else
349-
@normalize_module_name_cache[name] = nil
350-
return
352+
if alias_entry = class_alias_decls.fetch(name, nil)
353+
unless alias_entry.decl.old_name.absolute?
354+
# Having relative old_name means the type name resolution was failed.
355+
# Run TypeNameResolver for failure reason
356+
resolver = Resolver::TypeNameResolver.build(self)
357+
name = resolver.resolve_namespace(name, context: nil)
358+
@normalize_module_name_cache[original_name] = name
359+
return name
351360
end
352-
else
353-
type_name = name
354-
end
355361

356-
@normalize_module_name_cache[name] = false
362+
name = alias_entry.decl.old_name
363+
end
357364

358-
entry = constant_entry(type_name)
365+
if class_decls.key?(name)
366+
@normalize_module_name_cache[original_name] = name
367+
end
368+
end
359369

360-
normalized_type_name =
361-
case entry
362-
when ClassEntry, ModuleEntry
363-
type_name
364-
when ClassAliasEntry, ModuleAliasEntry
365-
normalize_module_name?(entry.decl.old_name)
366-
else
367-
nil
368-
end
370+
def normalize_module_name(name)
371+
normalize_module_name?(name) || name
372+
end
369373

370-
@normalize_module_name_cache[name] = normalized_type_name
374+
def normalize_module_name!(name)
375+
normalize_module_name?(name) or raise "Module name `#{name}` cannot be normalized"
371376
end
372377

373378
def insert_decl(decl, outer:, namespace:)
@@ -509,7 +514,7 @@ def resolve_signature(resolver, table, dirs, decls, only: nil)
509514
end
510515

511516
def resolve_type_names(only: nil)
512-
resolver = Resolver::TypeNameResolver.new(self)
517+
resolver = Resolver::TypeNameResolver.build(self)
513518
env = Environment.new
514519

515520
table = UseMap::Table.new()

lib/rbs/errors.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def self.check2!(env:, type_name:, args:, location:)
9393
params =
9494
case
9595
when type_name.class?
96-
decl = env.normalized_module_class_entry(type_name) or raise
96+
decl = env.module_class_entry(type_name, normalized: true) or raise
9797
decl.type_params
9898
when type_name.interface?
9999
env.interface_decls.fetch(type_name).decl.type_params

lib/rbs/resolver/constant_resolver.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def initialize(environment)
3232
end
3333

3434
environment.class_alias_decls.each do |name, entry|
35-
normalized_entry = environment.normalized_module_class_entry(name) or next
35+
normalized_entry = environment.module_class_entry(name, normalized: true) or next
3636
constant = constant_of_module(name, normalized_entry)
3737

3838
# Insert class/module aliases into `children_table` and `toplevel` table
@@ -176,7 +176,7 @@ def constants_from_context(context, constants:)
176176
end
177177

178178
def constants_from_ancestors(module_name, constants:)
179-
entry = builder.env.normalized_module_class_entry(module_name) or raise
179+
entry = builder.env.module_class_entry(module_name, normalized: true) or raise
180180

181181
if entry.is_a?(Environment::ClassEntry) || entry.is_a?(Environment::ModuleEntry)
182182
constants.merge!(table.children(BuiltinNames::Object.name) || raise)

0 commit comments

Comments
 (0)