Skip to content

Commit

Permalink
Merge pull request #1165 from soutaro/fix-ffi
Browse files Browse the repository at this point in the history
Fix subtyping issues
  • Loading branch information
soutaro committed Jun 12, 2024
2 parents dd75aac + d2cfe1d commit b3e1a34
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 161 deletions.
18 changes: 18 additions & 0 deletions doc/shape.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ Shape (_Foo) {
Note that the `self` type in the example is resolved to `_Foo` during shape calculation.

The shape calculation of an object is straightforward. Calculate a `RBS::Definition` of a class singleton/instance, or an interface, and translate the data structure to a `Shape` object. But there are a few things to consider.

## Tuple, record, and proc types

The shape of tuple, record, or proc types are based on their base types -- Array, Hash, or Proc classes --, but with specialized method types.

```
Expand All @@ -37,16 +39,22 @@ Shape ([Integer, String]) {
```

The specialization is implemented as a part of shape calculation.

## Special methods

Steep recognizes some special methods for type narrowing, including `#is_a?`, `#===`, `#nil?`, ... These methods are defined with normal RBS syntax, but the method types in shapes are transformed to types using logic types.

The shape calculation inserts the specialized methods with these special methods.

## `self` types

There are two cases of `self` types to consider during shape calculation.

1. `self` types included in the shape of a type
2. `self` types included in given types

### 1. `self` types included in the shape of a type

`self` types may be included in a class or interface definition.

```rbs
Expand All @@ -62,7 +70,9 @@ Shape (_Foo) {
itself: () -> _Foo
}
```

### 2. `self` types included in given types

Unlike `self` types included in definitions, `self` types in given types should be preserved.

```rbs
Expand Down Expand Up @@ -100,7 +110,9 @@ end
```

We want the type of `foo.get` to be `self`, not `Foo`, to avoid a type error being detected.

## Shape of `self` types

We also want `self` type if `self` is the type of the shape.

```rb
Expand Down Expand Up @@ -154,21 +166,27 @@ Shape (Foo | Bar) {
So, the resulting type of `self.foo` where the type of `self` is `Foo | Bar`, would be `Integer | Foo | Bar`. But, actually, it won't be `Foo` because the `self` comes from `Bar`.

This is an incorrect result, but Steep is doing this right now.

## `class` and `instance` types

The shape calculation provides limited support for `class` and `instance` types.

1. `class`/`instance` types from the definition are resolved
2. `class`/`instance` types in generics type arguments of interfaces/instances are preserved
3. Shape of `class`/`instance` types are resolved to configuration's `class_type` and `instance_type`, and the translated types are used to calculate the shape

It's different from `self` types except case #2. The relationship between `self`/`class`/`instance` is not trivial in Ruby. All of them might be resolved to any type, which means calculating one from another of them is simply impossible.

## Public methods, private methods

`Shape` objects have a flag of if the shape is for *public* method calls or *private* method calls. Private method call is a form of `foo()` or `self.foo()` -- when the receiver is omitted or `self`. Public method calls are anything else.

The shape calculation starts with *private methods*, and the `Shape#public_shape` method returns another shape that only has *public* methods.

> Note that the private shape calculation is required even on public method calls. This means a possible chance of future optimizations.
## Lazy method type calculation

We rarely need all of the methods available for an object. If we want to type check a method call, we only need the method type of that method. All other methods can be just ignored.

*Lazy method type calculation* is introduced for that case. Instead of calculating the types of all of the methods, it registers a block that computes the method type.
Expand Down
15 changes: 11 additions & 4 deletions lib/steep/interface/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ def initialize(self_type:, class_type: nil, instance_type: nil, variable_bounds:
@class_type = class_type
@instance_type = instance_type
@variable_bounds = variable_bounds

validate
end

def self.empty
Expand All @@ -23,11 +21,16 @@ def subst
end
end

def validate
def validate_self_type
validate_fvs(:self_type, self_type)
end

def validate_instance_type
validate_fvs(:instance_type, instance_type)
end

def validate_class_type
validate_fvs(:class_type, class_type)
self
end

def validate_fvs(name, type)
Expand All @@ -37,6 +40,7 @@ def validate_fvs(name, type)
raise "#{name} cannot include 'self' type: #{type}"
end
if fvs.include?(AST::Types::Instance.instance)
Steep.logger.fatal { "#{name} cannot include 'instance' type: #{type}" }
raise "#{name} cannot include 'instance' type: #{type}"
end
if fvs.include?(AST::Types::Class.instance)
Expand Down Expand Up @@ -87,12 +91,15 @@ def fetch_cache(cache, key)
def raw_shape(type, config)
case type
when AST::Types::Self
config.validate_self_type
self_type = config.self_type or raise
self_shape(self_type, config)
when AST::Types::Instance
config.validate_instance_type
instance_type = config.instance_type or raise
raw_shape(instance_type, config)
when AST::Types::Class
config.validate_class_type
klass_type = config.class_type or raise
raw_shape(klass_type, config)
when AST::Types::Name::Singleton
Expand Down
Loading

0 comments on commit b3e1a34

Please sign in to comment.