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
24 changes: 16 additions & 8 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ test_config = lambda do |t|
t.test_files = FileList["test/**/*_test.rb"].reject do |path|
path =~ %r{test/stdlib/}
end
if defined?(RubyMemcheck)
if t.is_a?(RubyMemcheck::TestTask)
t.verbose = true
t.options = '-v'
end
end
end

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

task :typecheck_test => :compile do
FileList["test/typecheck/*"].each do |test|
Dir.chdir(test) do
expectations = File.join(test, "steep_expectations.yml")
if File.exist?(expectations)
sh "steep check --with_expectations"
else
sh "steep check"
Bundler.with_unbundled_env do
FileList["test/typecheck/*"].each do |test|
Dir.chdir(test) do
expectations = File.join(test, "steep_expectations.yml")
if File.exist?(expectations)
sh "#{__dir__}/bin/steep check --with_expectations"
else
sh "#{__dir__}/bin/steep check"
end
end
end
end
Expand Down Expand Up @@ -535,4 +543,4 @@ task :prepare_profiling do
Rake::Task[:"clobber"].invoke
Rake::Task[:"templates"].invoke
Rake::Task[:"compile"].invoke
end
end
79 changes: 79 additions & 0 deletions docs/aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Aliases

This document explains module/class aliases and type aliases.

## Module/class alias

Module/class aliases give another name to a module/class.
This is useful for some syntaxes that has lexical constraints.

```rbs
class C
end

class D = C # ::D is an alias for ::C

class E < D # ::E inherits from ::D, which is actually ::C
end
```

Note that module/class aliases cannot be recursive.

So, we can define a *normalization* of aliased module/class names.
Normalization follows the chain of alias definitions and resolves them to the original module/class defined with `module`/`class` syntax.

```rbs
class C
end

class D = C
class E = D
```

`::E` is defined as an alias, and it can be normalized to `::C`.

## Type alias

The biggest difference from module/class alias is that type alias can be recursive.

```rbs
# cons_cell type is defined recursively
type cons_cell = nil
| [Integer, cons_cell]
```

This means type aliases *cannot be* normalized generally.
So, we provide another operation for type alias, `DefinitionBuilder#expand_alias` and its family.
It substitutes with the immediate right hand side of a type alias.

```
cons_cell ===> nil | [Integer, cons_cell] (expand 1 step)
===> nil | [Integer, nil | [Integer, cons_cell]] (expand 2 steps)
===> ... (expand will go infinitely)
```

Note that the namespace of a type alias *can be* normalized, because they are module names.

```rbs
module M
type t = String
end

module N = M
```

With the type definition above, a type `::N::t` can be normalized to `::M::t`.
And then it can be expanded to `::String`.

> [!NOTE]
> This is something like an *unfold* operation in type theory.

## Type name resolution

Type name resolution in RBS usually rewrites *relative* type names to *absolute* type names.
`Environment#resolve_type_names` converts all type names in the RBS type definitions, and returns a new `Environment` object.

It also *normalizes* modules names in type names.

- If the type name can be resolved and normalized successfully, the AST has *absolute* type names.
- If the type name resolution/normalization fails, the AST has *relative* type names.
4 changes: 2 additions & 2 deletions lib/rbs/cli/validate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def validate_class_module_definition

self_params =
if self_type.name.class?
@env.normalized_module_entry(self_type.name)&.type_params
@env.module_entry(self_type.name, normalized: true)&.type_params
else
@env.interface_decls[self_type.name]&.decl&.type_params
end
Expand Down Expand Up @@ -188,7 +188,7 @@ def validate_class_module_definition
end
params =
if member.name.class?
module_decl = @env.normalized_module_entry(member.name) or raise
module_decl = @env.module_entry(member.name, normalized: true) or raise
module_decl.type_params
else
interface_decl = @env.interface_decls.fetch(member.name)
Expand Down
10 changes: 5 additions & 5 deletions lib/rbs/definition_builder/ancestor_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ def one_instance_ancestors(type_name)
InvalidTypeApplicationError.check2!(type_name: super_class.name, args: super_class.args, env: env, location: super_class.location)
end

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

ancestors = OneAncestors.class_instance(
Expand Down Expand Up @@ -248,7 +248,7 @@ def one_instance_ancestors(type_name)

module_name = module_self.name
if module_name.class?
module_entry = env.normalized_module_class_entry(module_name) or raise
module_entry = env.module_class_entry(module_name, normalized: true) or raise
module_name = module_entry.name
self_args = AST::TypeParam.normalize_args(module_entry.type_params, module_self.args)
end
Expand Down Expand Up @@ -359,7 +359,7 @@ def mixin_ancestors0(decl, type_name, align_params:, included_modules:, included
MixinClassError.check!(type_name: type_name, env: env, member: member)
NoMixinFoundError.check!(member.name, env: env, member: member)

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

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

module_decl = env.normalized_module_entry(member.name) or raise
module_decl = env.module_entry(member.name, normalized: true) or raise
module_name = module_decl.name

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

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

module_name = env.normalize_module_name(module_name)
Expand Down
123 changes: 64 additions & 59 deletions lib/rbs/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,21 +223,17 @@ def class_alias?(name)
end
end

def class_entry(type_name)
case
when (class_entry = class_decls[type_name]).is_a?(ClassEntry)
class_entry
when (class_alias = class_alias_decls[type_name]).is_a?(ClassAliasEntry)
class_alias
def class_entry(type_name, normalized: false)
case entry = constant_entry(type_name, normalized: normalized || false)
when ClassEntry, ClassAliasEntry
entry
end
end

def module_entry(type_name)
case
when (module_entry = class_decls[type_name]).is_a?(ModuleEntry)
module_entry
when (module_alias = class_alias_decls[type_name]).is_a?(ModuleAliasEntry)
module_alias
def module_entry(type_name, normalized: false)
case entry = constant_entry(type_name, normalized: normalized || false)
when ModuleEntry, ModuleAliasEntry
entry
end
end

Expand All @@ -253,26 +249,40 @@ def normalized_class_entry(type_name)
end

def normalized_module_entry(type_name)
if name = normalize_module_name?(type_name)
case entry = module_entry(name)
when ModuleEntry, nil
entry
when ModuleAliasEntry
raise
end
end
module_entry(type_name, normalized: true)
end

def module_class_entry(type_name)
class_entry(type_name) || module_entry(type_name)
def module_class_entry(type_name, normalized: false)
entry = constant_entry(type_name, normalized: normalized || false)
if entry.is_a?(ConstantEntry)
nil
else
entry
end
end

def normalized_module_class_entry(type_name)
normalized_class_entry(type_name) || normalized_module_entry(type_name)
module_class_entry(type_name, normalized: true)
end

def constant_entry(type_name)
class_entry(type_name) || module_entry(type_name) || constant_decls[type_name]
def constant_entry(type_name, normalized: false)
if normalized
if normalized_name = normalize_module_name?(type_name)
class_decls.fetch(normalized_name, nil)
else
# The type_name may be declared with constant declaration
unless type_name.namespace.empty?
parent = type_name.namespace.to_type_name
normalized_parent = normalize_module_name?(parent) or return
constant_name = TypeName.new(name: type_name.name, namespace: normalized_parent.to_namespace)
constant_decls.fetch(constant_name, nil)
end
end
else
class_decls.fetch(type_name, nil) ||
class_alias_decls.fetch(type_name, nil) ||
constant_decls.fetch(type_name, nil)
end
end

def normalize_type_name?(name)
Expand Down Expand Up @@ -307,6 +317,10 @@ def normalize_type_name!(name)
end
end

def normalize_type_name(name)
normalize_type_name?(name) || name
end

def normalized_type_name?(type_name)
case
when type_name.interface?
Expand All @@ -321,53 +335,44 @@ def normalized_type_name?(type_name)
end

def normalized_type_name!(name)
normalized_type_name?(name) or raise "Normalized type name is expected but given `#{name}`, which is normalized to `#{normalize_type_name?(name)}`"
normalized_type_name?(name) or raise "Normalized type name is expected but given `#{name}`"
name
end

def normalize_type_name(name)
normalize_type_name?(name) || name
end

def normalize_module_name(name)
normalize_module_name?(name) or name
end

def normalize_module_name?(name)
raise "Class/module name is expected: #{name}" unless name.class?
name = name.absolute! unless name.absolute?

if @normalize_module_name_cache.key?(name)
return @normalize_module_name_cache[name]
original_name = name

if @normalize_module_name_cache.key?(original_name)
return @normalize_module_name_cache[original_name]
end

unless name.namespace.empty?
parent = name.namespace.to_type_name
if normalized_parent = normalize_module_name?(parent)
type_name = TypeName.new(namespace: normalized_parent.to_namespace, name: name.name)
else
@normalize_module_name_cache[name] = nil
return
if alias_entry = class_alias_decls.fetch(name, nil)
unless alias_entry.decl.old_name.absolute?
# Having relative old_name means the type name resolution was failed.
# Run TypeNameResolver for failure reason
resolver = Resolver::TypeNameResolver.build(self)
name = resolver.resolve_namespace(name, context: nil)
@normalize_module_name_cache[original_name] = name
return name
end
else
type_name = name
end

@normalize_module_name_cache[name] = false
name = alias_entry.decl.old_name
end

entry = constant_entry(type_name)
if class_decls.key?(name)
@normalize_module_name_cache[original_name] = name
end
end

normalized_type_name =
case entry
when ClassEntry, ModuleEntry
type_name
when ClassAliasEntry, ModuleAliasEntry
normalize_module_name?(entry.decl.old_name)
else
nil
end
def normalize_module_name(name)
normalize_module_name?(name) || name
end

@normalize_module_name_cache[name] = normalized_type_name
def normalize_module_name!(name)
normalize_module_name?(name) or raise "Module name `#{name}` cannot be normalized"
end

def insert_decl(decl, outer:, namespace:)
Expand Down Expand Up @@ -509,7 +514,7 @@ def resolve_signature(resolver, table, dirs, decls, only: nil)
end

def resolve_type_names(only: nil)
resolver = Resolver::TypeNameResolver.new(self)
resolver = Resolver::TypeNameResolver.build(self)
env = Environment.new

table = UseMap::Table.new()
Expand Down
2 changes: 1 addition & 1 deletion lib/rbs/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def self.check2!(env:, type_name:, args:, location:)
params =
case
when type_name.class?
decl = env.normalized_module_class_entry(type_name) or raise
decl = env.module_class_entry(type_name, normalized: true) or raise
decl.type_params
when type_name.interface?
env.interface_decls.fetch(type_name).decl.type_params
Expand Down
4 changes: 2 additions & 2 deletions lib/rbs/resolver/constant_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def initialize(environment)
end

environment.class_alias_decls.each do |name, entry|
normalized_entry = environment.normalized_module_class_entry(name) or next
normalized_entry = environment.module_class_entry(name, normalized: true) or next
constant = constant_of_module(name, normalized_entry)

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

def constants_from_ancestors(module_name, constants:)
entry = builder.env.normalized_module_class_entry(module_name) or raise
entry = builder.env.module_class_entry(module_name, normalized: true) or raise

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