rdl 2.0.0.rc2 → 2.0.0.rc3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +7 -1
- data/README.md +94 -20
- data/lib/rdl.rb +4 -1
- data/lib/rdl/config.rb +90 -3
- data/lib/rdl/info.rb +16 -0
- data/lib/rdl/typecheck.rb +207 -79
- data/lib/rdl/types/bot.rb +1 -1
- data/lib/rdl/types/dependent_arg.rb +17 -8
- data/lib/rdl/types/{finitehash.rb → finite_hash.rb} +3 -29
- data/lib/rdl/types/generic.rb +1 -37
- data/lib/rdl/types/intersection.rb +1 -0
- data/lib/rdl/types/lexer.rex +2 -1
- data/lib/rdl/types/lexer.rex.rb +4 -1
- data/lib/rdl/types/method.rb +2 -12
- data/lib/rdl/types/nominal.rb +1 -22
- data/lib/rdl/types/non_null.rb +50 -0
- data/lib/rdl/types/parser.racc +3 -1
- data/lib/rdl/types/parser.tab.rb +222 -190
- data/lib/rdl/types/singleton.rb +1 -6
- data/lib/rdl/types/structural.rb +1 -10
- data/lib/rdl/types/top.rb +1 -2
- data/lib/rdl/types/tuple.rb +3 -19
- data/lib/rdl/types/type.rb +223 -0
- data/lib/rdl/types/union.rb +12 -2
- data/lib/rdl/types/var.rb +4 -1
- data/lib/rdl/types/wild_query.rb +1 -0
- data/lib/rdl/wrap.rb +199 -169
- data/lib/rdl_disable.rb +41 -0
- data/lib/rdl_types.rb +1 -4
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/_aliases.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/abbrev.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/array.rb +56 -56
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/base64.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/basic_object.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/benchmark.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/bigdecimal.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/bigmath.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/bignum.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/class.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/complex.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/coverage.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/csv.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/date.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/dir.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/encoding.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/enumerable.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/enumerator.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/exception.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/file.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/fileutils.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/fixnum.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/float.rb +26 -26
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/gem.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/hash.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/integer.rb +8 -8
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/io.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/kernel.rb +12 -11
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/marshal.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/matchdata.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/math.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/module.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/nil.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/numeric.rb +0 -0
- data/lib/types/core-ruby-2.x/object.rb +75 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/pathname.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/process.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/random.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/range.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/rational.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/regexp.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/set.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/string.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/strscan.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/symbol.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/time.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/uri.rb +0 -0
- data/{types/ruby-2.x → lib/types/core-ruby-2.x}/yaml.rb +0 -0
- data/lib/types/core.rb +4 -0
- data/rdl.gemspec +2 -2
- data/test/test_le.rb +77 -35
- data/test/test_parser.rb +75 -59
- data/test/test_rdl.rb +18 -0
- data/test/test_typecheck.rb +73 -4
- metadata +54 -57
- data/lib/rails_types.rb +0 -1
- data/types/rails-4.2.1/fixnum.rb +0 -3
- data/types/rails-4.2.1/string.rb +0 -3
- data/types/rails-tmp/action_dispatch.rb +0 -406
- data/types/rails-tmp/active_record.rb +0 -406
- data/types/rails-tmp/devise_contracts.rb +0 -216
- data/types/ruby-2.x/object.rb +0 -73
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 12683f52b0d365695fbbddde7ab7494ae049202e
|
4
|
+
data.tar.gz: 67d2aaf4bde9eba67027f5bc7662fd7d7e33c746
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f42995d41cd05b3d9e1115c0cf7e20a4873f840659e9fd56e0f476941adac6dfef199dce88bacafdefaacbc3cf410be917fc6213b77ebe0a5428840e75721542
|
7
|
+
data.tar.gz: 0ee8aa838a19d9977acc2556bba2ef295bc8be97a6a6407af4a7882a2ecd62c09ef4d5b6464a61f49137d26751bd43c581df932e883424f96126847edf1606f9
|
data/CHANGES.md
CHANGED
@@ -2,11 +2,17 @@
|
|
2
2
|
|
3
3
|
## [Unreleased]
|
4
4
|
### Added
|
5
|
-
-
|
5
|
+
- `wrap: false` optional argument to `type`, `pre`, and `post`
|
6
|
+
- Non-null type annotation (not checked)
|
7
|
+
- Default argument configuration for `type`, `pre`, and `post`
|
8
|
+
- `attr_*_type` methods
|
9
|
+
- Static type checking!
|
6
10
|
|
7
11
|
### Changed
|
8
12
|
- Modified `self` type to be any instance of the self's class
|
9
13
|
- Library types now use new aliases %integer and %numeric instead of the Integer and Numeric classes.
|
14
|
+
- Fix issue #14 - allow type/pre/post to coexist, improve docs on dependent types
|
15
|
+
- Fix typos in README, pull req #13
|
10
16
|
|
11
17
|
## [1.1.1] - 2016-05-21
|
12
18
|
### Fixed
|
data/README.md
CHANGED
@@ -8,6 +8,8 @@
|
|
8
8
|
* [Supported versions of Ruby](#supported-versions-of-ruby)
|
9
9
|
* [Installing RDL](#installing-rdl)
|
10
10
|
* [Loading RDL](#loading-rdl)
|
11
|
+
* [Disabling RDL](#disabling-rdl)
|
12
|
+
* [Rails](#rails)
|
11
13
|
* [Preconditions and Postconditions](#preconditions-and-postconditions)
|
12
14
|
* [Type Annotations](#type-annotations)
|
13
15
|
* [RDL Types](#rdl-types)
|
@@ -31,12 +33,15 @@
|
|
31
33
|
* [Finite Hash Types](#finite-hash-types)
|
32
34
|
* [Type Casts](#type-casts)
|
33
35
|
* [Bottom Type (%bot)](#bottom-type-bot)
|
36
|
+
* [Non-null Type](#non-null-type)
|
34
37
|
* [Static Type Checking](#static-type-checking)
|
35
38
|
* [Types for Variables](#types-for-variables)
|
36
39
|
* [Tuples, Finite Hashes, and Subtyping](#tuples-finite-hashes-and-subtyping)
|
37
40
|
* [Other Features and Limitations](#other-features-and-limitations)
|
41
|
+
* [Assumptions](#assumptions)
|
38
42
|
* [Other RDL Methods](#other-rdl-methods)
|
39
43
|
* [Queries](#queries)
|
44
|
+
* [Configuration](#configuration)
|
40
45
|
* [Bibliography](#bibliography)
|
41
46
|
* [Copyright](#copyright)
|
42
47
|
* [Contributors](#contributors)
|
@@ -56,7 +61,7 @@ def sqrt(x)
|
|
56
61
|
end
|
57
62
|
```
|
58
63
|
|
59
|
-
Given this program, RDL intercepts the call to `sqrt` and passes its argument to the `pre` block, which checks that the
|
64
|
+
Given this program, RDL intercepts the call to `sqrt` and passes its argument to the `pre` block, which checks that the argument is positive. Then when `sqrt` returns, RDL passes the return value (as `r`) and the initial argument (as `x`) to the `post` block, which checks that the return is positive. (Let's ignore complex numbers to keep things simple...)
|
60
65
|
|
61
66
|
RDL contracts are enforced at method entry and exit. For example, if we call `sqrt(49)`, RDL first checks that `49 > 0`; then it passes `49` to `sqrt`, which (presumably) returns `7`; then RDL checks that `7 > 0`; and finally it returns `7`.
|
62
67
|
|
@@ -111,7 +116,7 @@ See below for more details of the query format. The `rdl_query` method performs
|
|
111
116
|
$ irb
|
112
117
|
> require 'rdl'
|
113
118
|
=> true
|
114
|
-
> require '
|
119
|
+
> require 'types/core'
|
115
120
|
=> true
|
116
121
|
|
117
122
|
> rdl_query '...' # as above
|
@@ -137,17 +142,13 @@ Use `require 'rdl'` to load the RDL library. If you want to use the core and sta
|
|
137
142
|
|
138
143
|
(Currently all these are assumed to have the same library type signatures, which may not be correct.)
|
139
144
|
|
140
|
-
|
145
|
+
## Disabling RDL
|
141
146
|
|
142
|
-
|
143
|
-
require 'rdl'
|
144
|
-
require 'rdl_types'
|
145
|
-
require 'rails_types'
|
146
|
-
```
|
147
|
+
For performance reasons you probably don't want to use RDL in production code. To disable RDL, replace `require 'rdl'` with `require 'rdl_disable'`. This will cause all invocations of RDL methods to either be no-ops or to do the minimum necessary to preserve the program's semantics (e.g., if the RDL method returns `self`, then so does the `rdl_disable` method.)
|
147
148
|
|
148
|
-
|
149
|
+
## Rails
|
149
150
|
|
150
|
-
|
151
|
+
To use RDL with Rails, use the [rdl-rails](https://github.com/plum-umd/rdl-rails) gem.
|
151
152
|
|
152
153
|
## Preconditions and Postconditions
|
153
154
|
|
@@ -300,7 +301,7 @@ Here we've named the first argument of `to_s` as `base` to give some extra hint
|
|
300
301
|
|
301
302
|
## Dependent Types
|
302
303
|
|
303
|
-
RDL allows for refinement predicates to be attached to named arguments.
|
304
|
+
RDL allows for refinement predicates to be attached to named arguments. The predicates on the arguments are checked when the method is called, and the predicates on the return is checked when then method returns. For instance:
|
304
305
|
|
305
306
|
```ruby
|
306
307
|
type '(Float x {{ x>=0 }}) -> Float y {{ y>=0 }}'
|
@@ -318,7 +319,13 @@ type '(Fixnum x {{ x>y }}, Fixnum y) -> Float z {{ z==(x+y) }}'
|
|
318
319
|
def m(x,y) ... end
|
319
320
|
```
|
320
321
|
|
321
|
-
Any arbitrary code can be placed between the double braces of a type refinement, and RDL will
|
322
|
+
Any arbitrary code can be placed between the double braces of a type refinement, and RDL will dynamically check that this predicate evaluates to true, or raise a type error if it evaluates to false.
|
323
|
+
|
324
|
+
Most pre- and postconditions can be translated into a dependent type by attaching the precondition to one of the arguments and the postcondition to the return. Note, however, that dependently typed positions must always have a name, even if the associated refinment doesn't refer to that name:
|
325
|
+
|
326
|
+
```ruby
|
327
|
+
type '(Fixnum x {{ $y > 0 }}) -> nil' # argument name must be present even though refinment doesn't use it.
|
328
|
+
```
|
322
329
|
|
323
330
|
## Higher-order Types
|
324
331
|
|
@@ -352,7 +359,7 @@ type '(Fixnum x, Float y) -> {(Fixnum z {{ z>y }}) -> Fixnum}'
|
|
352
359
|
def m(x,y,&blk) ... end
|
353
360
|
```
|
354
361
|
|
355
|
-
The type contract above states that method `m` returns a `Proc` which takes a `Fixnum z` which must be greater than the argument `Float y`.
|
362
|
+
The type contract above states that method `m` returns a `Proc` which takes a `Fixnum z` which must be greater than the argument `Float y`. Whenever this `Proc` is called, it will be checked that this contract holds.
|
356
363
|
|
357
364
|
## Class/Singleton Method Types
|
358
365
|
|
@@ -485,7 +492,7 @@ To fully enforce generic types, RDL requires that the developer `instantiate!` a
|
|
485
492
|
|
486
493
|
```ruby
|
487
494
|
x = [1,2]
|
488
|
-
x.
|
495
|
+
x.instantiate!('Fixnum')
|
489
496
|
x.push("three") # type error
|
490
497
|
```
|
491
498
|
|
@@ -514,7 +521,7 @@ The rules for variances are standard. Let's assume `A` is a subclass of `B`. Als
|
|
514
521
|
|
515
522
|
## Tuple Types
|
516
523
|
|
517
|
-
A type such as `Array<Fixnum>` is useful for homogeneous arrays, where all elements have the same type. But Ruby programs often use
|
524
|
+
A type such as `Array<Fixnum>` is useful for homogeneous arrays, where all elements have the same type. But Ruby programs often use heterogeneous arrays, e.g., `[1, "two"]`. The best generic type we can give this is `Array<Fixnum or String>`, but that's imprecise.
|
518
525
|
|
519
526
|
RDL includes special *tuple types* to handle this situation. Tuple types are written `[t1, ..., tn]`, denoting an `Array` of `n` elements of types `t1` through `tn`, in that order. For example, `[1, "two"]` has type `[Fixnum, String]`. As another example, here is the type of `Process#getrlimit`, which returns a two-element array of `Fixnums`:
|
520
527
|
|
@@ -524,7 +531,7 @@ type Process, 'self.getrlimit', '(Symbol or String or Fixnum resource) -> [Fixnu
|
|
524
531
|
|
525
532
|
## Finite Hash Types
|
526
533
|
|
527
|
-
Similarly to tuple types, RDL also supports *finite hash types* for
|
534
|
+
Similarly to tuple types, RDL also supports *finite hash types* for heterogeneous hashes. Finite hash types are written `{k1 => v1, ..., kn => vn}` to indicate a `Hash` with `n` mappings of type `ki` maps to `vi`. The `ki` may be strings, integers, floats, or constants denoted with `${.}`. If a key is a symbol, then the mapping should be written `ki: vi`. In the latter case, the `{}`'s can be left off:
|
528
535
|
```ruby
|
529
536
|
type MyClass, :foo, '(a: Fixnum, b: String) { () -> %any } -> %any'
|
530
537
|
```
|
@@ -538,15 +545,42 @@ Sometimes RDL does not have precise information about an object's type (this is
|
|
538
545
|
|
539
546
|
RDL also includes a special *bottom* type `%bot` that is a subtype of any type, including any class and any singleton types. In static type checking, the type `%bot` is given to so-called *void value expressions*, which are `return`, `break`, `next`, `redo`, and `retry` (notice that these expressions perform jumps rather than producing a value, hence they can be treated as having an arbitrary type). No Ruby objects have type `%bot`.
|
540
547
|
|
548
|
+
## Non-null Type
|
549
|
+
|
550
|
+
Types can be prefixed with `!` to indicate the associated value is not `nil`. For example:
|
551
|
+
|
552
|
+
`type :x=, '(!Fixnum) -> !Fixnum' # x's argument must not be nil`
|
553
|
+
|
554
|
+
**Warning:** This is simply *documentation* of non-nullness, and **is not checked** by the static type checker. The contract checker might or might not enforce non-nullness. (For those who are curious: RDL has this annotation because it seems useful for descriptive purposes. However, it's quite challenging to build a practical analysis that enforces non-nilness without reporting too many false positives.)
|
555
|
+
|
541
556
|
# Static Type Checking
|
542
557
|
|
543
558
|
RDL has experimental support (note: this is in beta release) for static type checking. As mentioned in the introduction, calling `type` with `typecheck: :now` statically type checks the body of the annotated method body against the given signature. If the method has already been defined, RDL will try to check the method immediately. Otherwise, RDL will statically type check the method as soon as it is loaded.
|
544
559
|
|
545
560
|
Often method bodies cannot be type checked as soon as they are loaded because they refer to classes, methods, and variables that have not been created yet. To support these cases, some other symbol can be supplied as `typecheck: sym`. Then when `rdl_do_typecheck sym` is called, all methods typechecked at `sym` will be statically checked.
|
546
561
|
|
547
|
-
|
562
|
+
Additionally, `type` can be called with `typecheck: :call`, which will delay checking the method's type until the method is called. Currently these checks are not cached, so expect a big performance hit for using this feature.
|
563
|
+
|
564
|
+
To perform type checking, RDL needs source code, which it gets by parsing the file containing the to-be-typechecked method. Hence, static type checking does not work in `irb` since RDL has no way of getting the source.
|
565
|
+
|
566
|
+
Typechecking does work in `pry` (this feature has only limited testing) as long as typechecking is delayed until after the method is defined:
|
567
|
+
|
568
|
+
```ruby
|
569
|
+
[2] pry(main)> require 'rdl'
|
570
|
+
[3] pry(main)> require 'rdl_types'
|
571
|
+
[4] pry(main)> type '() -> Fixnum', typecheck: :later # note: typecheck: :now doesn't work in pry
|
572
|
+
[5] pry(main)> def f
|
573
|
+
[5] pry(main)* 'haha'
|
574
|
+
[5] pry(main)* end
|
575
|
+
[6] pry(main)> rdl_do_typecheck :later
|
576
|
+
RDL::Typecheck::StaticTypeError:
|
577
|
+
(string):2:3: error: got type `String' where return type `Fixnum' expected
|
578
|
+
(string):2: 'haha'
|
579
|
+
(string):2: ^~~~~~
|
580
|
+
from .../typecheck.rb:158:in `error'
|
581
|
+
```
|
548
582
|
|
549
|
-
|
583
|
+
RDL currently uses the [parser Gem](https://github.com/whitequark/parser) to parse Ruby source code. (And RDL uses the parser gem's amazing diagnostic output facility to print type error messages.)
|
550
584
|
|
551
585
|
Next we discuss some special features of RDL's type system and some of its limitations.
|
552
586
|
|
@@ -575,7 +609,15 @@ x = 3 # okay
|
|
575
609
|
x = "three" # type error
|
576
610
|
```
|
577
611
|
|
578
|
-
The first argument to `var_type` is a symbol with the local variable name, and the second argument is a string containing the variable's type. Note that `var_type` is most useful at the beginning of method or code block. Using it elsewhere may result in surprising error
|
612
|
+
The first argument to `var_type` is a symbol with the local variable name, and the second argument is a string containing the variable's type. Note that `var_type` is most useful at the beginning of method or code block. Using it elsewhere may result in surprising error messages, since RDL requires variables with fixed types to have the same type along all paths. Method parameters are treated as if `var_type` was called on them at the beginning of the method, fixing them to their declared type. This design choice may be revisited in the future.
|
613
|
+
|
614
|
+
There is one subtlety for local variables and code blocks. Consider the following code:
|
615
|
+
```ruby
|
616
|
+
x = 1
|
617
|
+
m() { x = 'bar' }
|
618
|
+
# what is x's type here?
|
619
|
+
```
|
620
|
+
If `m` invokes the code block, `x` will be a `String` after the call. Otherwise `x` will be `1`. Since RDL can't tell whether the code block is ever called, it assigns `x` type `1 or String`. It's actually quite tricky to do very precise reasoning about code blocks. For example, `m` could (pathologically) store its block in a global variable and then only call it the second time `m` is invoked. To keep its reasoning simple, RDL treats any local variables captured (i.e., imported from an outer scope) by a code block flow-insensitively for the lifetime of the method. The type of any such local variables is the union of all types that are ever assigned to it.
|
579
621
|
|
580
622
|
RDL always treats instance, class, and global variables flow-insensitively, hence their types must be defined with `var_type`:
|
581
623
|
|
@@ -636,13 +678,15 @@ RDL uses the same approach for hashes: hash literals are treated as finite hashe
|
|
636
678
|
|
637
679
|
## Other Features and Limitations
|
638
680
|
|
681
|
+
*Displaying types.* As an aid to debugging, the method `rdl_note_type e` will display the type of `e` during type checking. At run time, this method returns its argument. Note that in certain cases RDL may type check the same code repeatedly in which case an expression's type could be printed multiple times.
|
682
|
+
|
639
683
|
* *Conditional guards and singletons.* If an `if` or `unless` guard has a singleton type, RDL will typecheck both branches but not include types from the unrealizable branch in the expression type. For example, `if true then 1 else 'two' end` has type `1`. RDL behaves similarly for `&&` and `||`. However, RDL does not implement this logic for `case`.
|
640
684
|
|
641
685
|
* *Multiple Assignment and nil.* In Ruby, extra left-hand sides of multiple assignments are set to `nil`, e.g., `x, y = [1]` sets `x` to `1` and `y` to `nil`. However, RDL reports an error in this case; this may change in the future.
|
642
686
|
|
643
687
|
* *Block formal arguments.* Similarly, RDL reports an error if a block is called with the wrong number of arguments even though Ruby does not signal an error in this case.
|
644
688
|
|
645
|
-
* *Caching.* If `typecheck:
|
689
|
+
* *Caching.* If `typecheck: :call` is specified on a method, Ruby will type check the method every time it is called. In the future, RDL will cache these checks.
|
646
690
|
|
647
691
|
* *Unsupported Features.* There are several features of Ruby that are currently not handled by RDL. Here is a non-exhaustive list:
|
648
692
|
* `super` is not supported.
|
@@ -651,6 +695,14 @@ RDL uses the same approach for hashes: hash literals are treated as finite hashe
|
|
651
695
|
* Control flow for exceptions is not analyzed fully soundly; some things are not reported as possibly `nil` that could be.
|
652
696
|
* Only simple usage of constants is handled.
|
653
697
|
|
698
|
+
## Assumptions
|
699
|
+
|
700
|
+
RDL makes some assumptions that should hold unless your Ruby code is doing something highly unusual:
|
701
|
+
|
702
|
+
* `Class#===` is not redefined
|
703
|
+
|
704
|
+
(More assumptions will be added here as they are added to RDL...)
|
705
|
+
|
654
706
|
# Other RDL Methods
|
655
707
|
|
656
708
|
RDL also includes a few other useful methods:
|
@@ -659,6 +711,8 @@ RDL also includes a few other useful methods:
|
|
659
711
|
|
660
712
|
* `rdl_nowrap`, if called at the top-level of a class, causes RDL to behave as if `wrap: false` were passed to all `type`, `pre`, and `post` calls in the class. This is mostly used for the core and standard libraries, which have trustworthy behavior hence enforcing their types and contracts is not worth the overhead.
|
661
713
|
|
714
|
+
* `rdl_remove_type klass, meth` removes the type annotation for meth in klass. Fails if meth does not have a type annotation.
|
715
|
+
|
662
716
|
* `rdl_query` prints information about types; see below for details.
|
663
717
|
|
664
718
|
# Queries
|
@@ -711,6 +765,26 @@ $ rdl_query "(.?) -> ." # methods that take one, optional argumen
|
|
711
765
|
|
712
766
|
Note that aside from `.` and `...`, the matching is exact. For example `(Fixnum) -> Fixnum` will not match a method of type `(Fixnum or String) -> Fixnum`.
|
713
767
|
|
768
|
+
# Configuration
|
769
|
+
|
770
|
+
To configure RDL, execute the following shortly after RDL is loaded:
|
771
|
+
|
772
|
+
```ruby
|
773
|
+
RDL.config { |config|
|
774
|
+
# use config to configure RDL here
|
775
|
+
}
|
776
|
+
```
|
777
|
+
|
778
|
+
RDL supports the following configuration options:
|
779
|
+
|
780
|
+
* `config.nowrap` - `Array<Class>` containing all classes whose methods should not be wrapped.
|
781
|
+
* `config.gather_stats` - currently disabled.
|
782
|
+
* `config.report` - if true, then when the program exits, RDL will print out a list of methods that were statically type checked, and methods that were annotated to be statically type checked but weren't.
|
783
|
+
* `config.guess_types` - List of classes (of type `Array<Symbol>`). For every method added to a listed class *after* this configuration option is set, RDL will record the types of its arguments and returns at run-time. Then when the program exits, RDL will print out a skeleton for each class with types for the monitored methods based on what RDL recorded at run-time and based on what Ruby knows about the methods' signatures. This is probably not going to produce the correct method types, but it might be a good starting place.
|
784
|
+
* `config.type_defaults` - Hash containing default options for `type`. Initially `{ wrap: true, typecheck: false }`.
|
785
|
+
* `config.pre_defaults` - Hash containing default options for `pre`. Initially `{ wrap: true }`.
|
786
|
+
* `config.post_defaults` - same as `pre_defaults`, but for `post`.
|
787
|
+
|
714
788
|
# Bibliography
|
715
789
|
|
716
790
|
Here are some research papers we have written exploring contracts, types, and Ruby.
|
data/lib/rdl.rb
CHANGED
@@ -19,6 +19,8 @@ require 'rdl/info.rb'
|
|
19
19
|
# :type to array of types
|
20
20
|
# :source_location to [filename, linenumber] location of most recent definition
|
21
21
|
# :typecheck - boolean that is true if method should be statically type checked
|
22
|
+
# :otype to set of types that were observed at run time, where a type is a finite hash {:args => Array<Class>, :ret => Class, :block => %bool}
|
23
|
+
# :context_types to array of [klass, meth, Type] - method types that exist only within this method. An icky hack to deal with Rails `params`.
|
22
24
|
# For variables
|
23
25
|
# :type to type
|
24
26
|
$__rdl_info = RDL::Info.new
|
@@ -59,13 +61,14 @@ require 'rdl/types/annotated_arg.rb'
|
|
59
61
|
require 'rdl/types/bot.rb'
|
60
62
|
require 'rdl/types/dependent_arg.rb'
|
61
63
|
require 'rdl/types/dots_query.rb'
|
62
|
-
require 'rdl/types/
|
64
|
+
require 'rdl/types/finite_hash.rb'
|
63
65
|
require 'rdl/types/generic.rb'
|
64
66
|
require 'rdl/types/intersection.rb'
|
65
67
|
require 'rdl/types/lexer.rex.rb'
|
66
68
|
require 'rdl/types/method.rb'
|
67
69
|
require 'rdl/types/singleton.rb'
|
68
70
|
require 'rdl/types/nominal.rb'
|
71
|
+
require 'rdl/types/non_null.rb'
|
69
72
|
require 'rdl/types/optional.rb'
|
70
73
|
require 'rdl/types/parser.tab.rb'
|
71
74
|
require 'rdl/types/structural.rb'
|
data/lib/rdl/config.rb
CHANGED
@@ -6,11 +6,17 @@ class RDL::Config
|
|
6
6
|
attr_accessor :nowrap
|
7
7
|
attr_accessor :gather_stats
|
8
8
|
attr_accessor :report
|
9
|
+
attr_accessor :guess_types
|
10
|
+
attr_accessor :type_defaults, :pre_defaults, :post_defaults
|
9
11
|
|
10
12
|
def initialize
|
11
13
|
@nowrap = Set.new
|
12
14
|
@gather_stats = false
|
13
15
|
@report = false
|
16
|
+
@guess_types = []
|
17
|
+
@type_defaults = { wrap: true, typecheck: false }
|
18
|
+
@pre_defaults = { wrap: true }
|
19
|
+
@post_defaults = { wrap: true }
|
14
20
|
end
|
15
21
|
|
16
22
|
def add_nowrap(*klasses)
|
@@ -120,10 +126,10 @@ RDL::Config.instance.profile_stats
|
|
120
126
|
|
121
127
|
Profiler__.start_profile # Restart profiler after setup
|
122
128
|
end
|
123
|
-
end
|
124
129
|
|
125
|
-
|
126
|
-
|
130
|
+
def do_report
|
131
|
+
return unless @report
|
132
|
+
puts "------------------------------"
|
127
133
|
typechecked = []
|
128
134
|
missing = []
|
129
135
|
$__rdl_info.info.each_pair { |klass, meths|
|
@@ -149,4 +155,85 @@ at_exit do
|
|
149
155
|
missing.each { |klass, meth| puts RDL::Util.pp_klass_method(klass, meth) }
|
150
156
|
end
|
151
157
|
end
|
158
|
+
|
159
|
+
def guess_meth(klass, meth, is_sing, the_meth)
|
160
|
+
# first print based on signature according to Ruby
|
161
|
+
first = true
|
162
|
+
block = false
|
163
|
+
print " type #{if is_sing then '\'self.' + meth + '\'' else ':' + meth end}, '("
|
164
|
+
params = the_meth.parameters
|
165
|
+
params.each_with_index { |param, i|
|
166
|
+
kind, name = param
|
167
|
+
print ", " unless first || kind == :block
|
168
|
+
case kind
|
169
|
+
when :req
|
170
|
+
print "XXXX #{name}"
|
171
|
+
when :opt
|
172
|
+
print "?XXXX #{name}"
|
173
|
+
when :rest
|
174
|
+
print "*#XXXX #{name}"
|
175
|
+
when :key
|
176
|
+
print "#{name}: XXXX"
|
177
|
+
when :block
|
178
|
+
block = true
|
179
|
+
else
|
180
|
+
print "???? param of kind #{kind} for #{name}"
|
181
|
+
end
|
182
|
+
first = false
|
183
|
+
}
|
184
|
+
print ")"
|
185
|
+
print " { BLOCK }" if block
|
186
|
+
puts " -> XXXX'"
|
187
|
+
|
188
|
+
# next print based on observed types
|
189
|
+
otypes = $__rdl_info.get(klass, meth, :otype) if $__rdl_info.has?(klass, meth, :otype) # observed types
|
190
|
+
return if otypes.nil?
|
191
|
+
first = true
|
192
|
+
print " type #{if is_sing then '\'self.' + meth + '\'' else ':' + meth end}, '("
|
193
|
+
otargs = []
|
194
|
+
otret = $__rdl_bot_type
|
195
|
+
otblock = false
|
196
|
+
otypes.each { |ot|
|
197
|
+
ot[:args].each_with_index { |t, i|
|
198
|
+
otargs[i] = $__rdl_bot_type if otargs[i].nil?
|
199
|
+
otargs[i] = RDL::Type::UnionType.new(otargs[i], RDL::Type::NominalType.new(t)).canonical
|
200
|
+
}
|
201
|
+
otret = RDL::Type::UnionType.new(otret, RDL::Type::NominalType.new(ot[:ret])).canonical
|
202
|
+
otblock = otblock || ot[:block]
|
203
|
+
}
|
204
|
+
otargs.each { |t|
|
205
|
+
print ", " unless first
|
206
|
+
print t
|
207
|
+
first = false
|
208
|
+
}
|
209
|
+
print ")"
|
210
|
+
print " { BLOCK }" if otblock
|
211
|
+
puts " -> #{otret}'"
|
212
|
+
end
|
213
|
+
|
214
|
+
def do_guess_types
|
215
|
+
return if @guess_types.empty?
|
216
|
+
puts "------------------------------"
|
217
|
+
puts "TYPE GUESSES"
|
218
|
+
RDL::Config.instance.guess_types.each { |klass|
|
219
|
+
puts
|
220
|
+
puts "class #{klass}"
|
221
|
+
the_klass = RDL::Util.to_class(klass)
|
222
|
+
sklass = RDL::Util.add_singleton_marker(klass.to_s)
|
223
|
+
the_klass.singleton_methods(false).each { |meth|
|
224
|
+
next unless meth.to_s =~ /^__rdl_(.*)_old/
|
225
|
+
guess_meth(sklass, $1, true, the_klass.singleton_method(meth))
|
226
|
+
}
|
227
|
+
the_klass.instance_methods(false).each { |meth|
|
228
|
+
next unless meth.to_s =~ /^__rdl_(.*)_old/
|
229
|
+
guess_meth(klass, $1, false, the_klass.instance_method(meth))
|
230
|
+
}
|
231
|
+
puts "end"
|
232
|
+
}
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
at_exit do
|
237
|
+
RDL::Config.instance.do_report
|
238
|
+
RDL::Config.instance.do_guess_types
|
152
239
|
end
|
data/lib/rdl/info.rb
CHANGED
@@ -71,4 +71,20 @@ class RDL::Info
|
|
71
71
|
return t2[kind]
|
72
72
|
# return @info[klass][label][kind]
|
73
73
|
end
|
74
|
+
|
75
|
+
def get_with_aliases(klass, label, kind)
|
76
|
+
while $__rdl_aliases[klass] && $__rdl_aliases[klass][label]
|
77
|
+
label = $__rdl_aliases[klass][label]
|
78
|
+
end
|
79
|
+
get(klass, label, kind)
|
80
|
+
end
|
81
|
+
|
82
|
+
def remove(klass, label, kind)
|
83
|
+
klass = klass.to_s
|
84
|
+
label = label.to_sym
|
85
|
+
return unless @info.has_key? klass
|
86
|
+
return unless @info[klass].has_key? label
|
87
|
+
@info[klass][label].delete kind
|
88
|
+
end
|
89
|
+
|
74
90
|
end
|
data/lib/rdl/typecheck.rb
CHANGED
@@ -65,8 +65,9 @@ module RDL::Typecheck
|
|
65
65
|
return @env[var][:type]
|
66
66
|
end
|
67
67
|
|
68
|
-
|
69
|
-
|
68
|
+
# force should only be used with care! currently only used when type is being refined to a subtype in a lexical scope
|
69
|
+
def bind(var, typ, force: false)
|
70
|
+
raise RuntimeError, "Can't update variable with fixed type" if !force && @env[var] && @env[var][:fixed]
|
70
71
|
result = Env.new
|
71
72
|
result.env = @env.merge(var => {type: typ, fixed: false})
|
72
73
|
return result
|
@@ -132,7 +133,6 @@ module RDL::Typecheck
|
|
132
133
|
end
|
133
134
|
end
|
134
135
|
|
135
|
-
|
136
136
|
# Call block with new Hash that is the same as Hash [+ scope +] except mappings in [+ elts +] have been merged.
|
137
137
|
# When block returns, copy out mappings in the new Hash to [+ scope +] except keys in [+ elts +].
|
138
138
|
def self.scope_merge(scope, **elts)
|
@@ -144,15 +144,40 @@ module RDL::Typecheck
|
|
144
144
|
return r
|
145
145
|
end
|
146
146
|
|
147
|
+
# add x:t to the captured map in scope
|
148
|
+
def self.capture(scope, x, t)
|
149
|
+
if scope[:captured][x]
|
150
|
+
scope[:captured][x] = RDL::Type::UnionType.new(scope[:captured][x], t).canonical unless t <= scope[:captured][x]
|
151
|
+
else
|
152
|
+
scope[:captured][x] = t
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
147
156
|
# report msg at ast's loc
|
148
157
|
def self.error(reason, args, ast)
|
149
158
|
raise StaticTypeError, ("\n" + (Parser::Diagnostic.new :error, reason, args, ast.loc.expression).render.join("\n"))
|
150
159
|
end
|
151
160
|
|
152
|
-
def self.
|
161
|
+
def self.note(reason, args, ast)
|
162
|
+
puts (Parser::Diagnostic.new :note, reason, args, ast.loc.expression).render
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.get_ast(klass, meth)
|
153
166
|
file, line = $__rdl_info.get(klass, meth, :source_location)
|
154
|
-
raise RuntimeError, "static type checking in irb not supported" if file == "(irb)"
|
155
167
|
raise RuntimeError, "No file for #{RDL::Util.pp_klass_method(klass, meth)}" if file.nil?
|
168
|
+
raise RuntimeError, "static type checking in irb not supported" if file == "(irb)"
|
169
|
+
if file == "(pry)"
|
170
|
+
# no caching...
|
171
|
+
if RDL::Wrap.wrapped?(klass, meth)
|
172
|
+
meth_name = RDL::Wrap.wrapped_name(klass, meth)
|
173
|
+
else
|
174
|
+
meth_name = meth
|
175
|
+
end
|
176
|
+
the_meth = RDL::Util.to_class(klass).instance_method(meth_name)
|
177
|
+
code = Pry::Code.from_method the_meth
|
178
|
+
return Parser::CurrentRuby.parse code.to_s
|
179
|
+
end
|
180
|
+
|
156
181
|
digest = Digest::MD5.file file
|
157
182
|
cache_hit = (($__rdl_ruby_parser_cache.has_key? file) &&
|
158
183
|
($__rdl_ruby_parser_cache[file][0] == digest))
|
@@ -165,6 +190,11 @@ module RDL::Typecheck
|
|
165
190
|
end
|
166
191
|
ast = $__rdl_ruby_parser_cache[file][1][:line_defs][line]
|
167
192
|
raise RuntimeError, "Can't find source for class #{RDL::Util.pp_klass_method(klass, meth)}" if ast.nil?
|
193
|
+
return ast
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.typecheck(klass, meth)
|
197
|
+
ast = get_ast(klass, meth)
|
168
198
|
types = $__rdl_info.get(klass, meth, :type)
|
169
199
|
raise RuntimeError, "Can't typecheck method with no types?!" if types.nil? or types == []
|
170
200
|
|
@@ -176,6 +206,7 @@ module RDL::Typecheck
|
|
176
206
|
raise RuntimeError, "Unexpected ast type #{ast.type}"
|
177
207
|
end
|
178
208
|
raise RuntimeError, "Method #{name} defined where method #{meth} expected" if name.to_sym != meth
|
209
|
+
context_types = $__rdl_info.get(klass, meth, :context_types)
|
179
210
|
types.each { |type|
|
180
211
|
# TODO will need fancier logic here for matching up more complex arg lists
|
181
212
|
if RDL::Util.has_singleton_marker(klass)
|
@@ -189,10 +220,17 @@ module RDL::Typecheck
|
|
189
220
|
unless type.args.length == args.children.length
|
190
221
|
error :arg_count_mismatch, ['method', type.args.length, 'method', args.children.length], (if args.children.empty? then ast else args end)
|
191
222
|
end
|
192
|
-
|
193
|
-
|
194
|
-
scope = {tret: type.ret, tblock: type.block }
|
195
|
-
|
223
|
+
targs = args.children.map { |arg| arg.children[0] }.zip(type.args).to_h
|
224
|
+
targs[:self] = self_type
|
225
|
+
scope = { tret: type.ret, tblock: type.block, captured: Hash.new, context_types: context_types }
|
226
|
+
begin
|
227
|
+
old_captured = scope[:captured].dup
|
228
|
+
if body.nil?
|
229
|
+
body_type = $__rdl_nil_type
|
230
|
+
else
|
231
|
+
_, body_type = tc(scope, Env.new(targs.merge(scope[:captured])), body)
|
232
|
+
end
|
233
|
+
end until old_captured == scope[:captured]
|
196
234
|
error :bad_return_type, [body_type.to_s, type.ret.to_s], body unless body.nil? || body_type <= type.ret
|
197
235
|
}
|
198
236
|
$__rdl_info.set(klass, meth, :typechecked, true)
|
@@ -315,7 +353,7 @@ module RDL::Typecheck
|
|
315
353
|
# promote singleton types to nominal types; safe since Ranges are immutable
|
316
354
|
t1 = RDL::Type::NominalType.new(t1.val.class) if t1.is_a? RDL::Type::SingletonType
|
317
355
|
t2 = RDL::Type::NominalType.new(t2.val.class) if t2.is_a? RDL::Type::SingletonType
|
318
|
-
error :nonmatching_range_type, [t1, t2], e
|
356
|
+
error :nonmatching_range_type, [t1, t2], e unless t1 <= t2 || t2 <= t1
|
319
357
|
[env2, RDL::Type::GenericType.new($__rdl_range_type, t1)]
|
320
358
|
when :self
|
321
359
|
[env, env[:self]]
|
@@ -452,8 +490,9 @@ module RDL::Typecheck
|
|
452
490
|
# children[0] = receiver; if nil, receiver is self
|
453
491
|
# children[1] = method name, a symbol
|
454
492
|
# children [2..] = actual args
|
455
|
-
return tc_var_type(scope, env, e) if e.children[1] == :var_type && e.children[0].nil?
|
493
|
+
return tc_var_type(scope, env, e) if e.children[1] == :var_type && e.children[0].nil?
|
456
494
|
return tc_type_cast(scope, env, e) if e.children[1] == :type_cast && scope[:block].nil?
|
495
|
+
return tc_note_type(scope, env, e) if e.children[1] == :rdl_note_type && e.children[0].nil?
|
457
496
|
envi = env
|
458
497
|
tactuals = []
|
459
498
|
block = scope[:block]
|
@@ -464,7 +503,7 @@ module RDL::Typecheck
|
|
464
503
|
if ti.is_a? RDL::Type::TupleType
|
465
504
|
tactuals.concat ti.params
|
466
505
|
elsif ti.is_a?(RDL::Type::GenericType) && ti.base == $__rdl_array_type
|
467
|
-
tactuals << RDL::Type::VarargType.new(ti)
|
506
|
+
tactuals << RDL::Type::VarargType.new(ti.params[0]) # Turn Array<t> into *t
|
468
507
|
else
|
469
508
|
error :cant_splat, [ti], ei.children[0]
|
470
509
|
end
|
@@ -537,12 +576,38 @@ RUBY
|
|
537
576
|
e.children[1..-2].each { |wclause|
|
538
577
|
raise RuntimeError, "Don't know what to do with case clause #{wclause.type}" unless wclause.type == :when
|
539
578
|
envguards = []
|
579
|
+
tguards = []
|
540
580
|
wclause.children[0..-2].each { |guard| # first wclause.length-1 children are the guards
|
541
581
|
envi, tguard = tc(scope, envi, guard) # guard type can be anything
|
582
|
+
tguards << tguard
|
542
583
|
tc_send(scope, envi, tguard, :===, [tcontrol], nil, guard) unless tcontrol.nil?
|
543
584
|
envguards << envi
|
544
585
|
}
|
545
|
-
|
586
|
+
initial_env = Env.join(e, *envguards)
|
587
|
+
if (tguards.all? { |t| t.is_a?(RDL::Type::SingletonType) && t.val.is_a?(Class) }) && (e.children[0].type == :lvar)
|
588
|
+
# Special case! We're branching on the type of the guard, which is a local variable.
|
589
|
+
# So rebind that local variable to have the union of the guard types
|
590
|
+
new_typ = RDL::Type::UnionType.new(*(tguards.map { |t| RDL::Type::NominalType.new(t.val) })).canonical
|
591
|
+
# TODO adjust following for generics!
|
592
|
+
if tcontrol.is_a? RDL::Type::GenericType
|
593
|
+
if new_typ == tcontrol.base
|
594
|
+
# special case: exact match of control type's base and type of guard; can use
|
595
|
+
# geneirc type on this branch
|
596
|
+
initial_env = initial_env.bind(e.children[0].children[0], tcontrol, force: true)
|
597
|
+
elsif !(tcontrol.base <= new_typ) && !(new_typ <= tcontrol.base)
|
598
|
+
next # can't possibly match this branch
|
599
|
+
else
|
600
|
+
error :generic_error, ["general refinement for generics not implemented yet"], wclause
|
601
|
+
end
|
602
|
+
else
|
603
|
+
next unless tcontrol <= new_typ || new_typ <= tcontrol # If control can't possibly match type, skip this branch
|
604
|
+
initial_env = initial_env.bind(e.children[0].children[0], new_typ, force: true)
|
605
|
+
# note force is safe above because the env from this arm will be joined with the other envs
|
606
|
+
# (where the type was not refined like this), so after the case the variable will be back to its
|
607
|
+
# previous, unrefined type
|
608
|
+
end
|
609
|
+
end
|
610
|
+
envbody, tbody = tc(scope, initial_env, wclause.children[-1]) # last wclause child is body
|
546
611
|
tbodies << tbody
|
547
612
|
envbodies << envbody
|
548
613
|
}
|
@@ -600,6 +665,7 @@ RUBY
|
|
600
665
|
[lscope[:break], lscope[:tbreak].canonical]
|
601
666
|
}
|
602
667
|
when :for
|
668
|
+
# (for (lvasgn var) collection body)
|
603
669
|
# break: loop exit, which is same as top of body, arg allowed
|
604
670
|
# next: top of body, arg allowed
|
605
671
|
# retry: not allowed
|
@@ -607,21 +673,18 @@ RUBY
|
|
607
673
|
raise RuntimeError, "Loop variable #{e.children[0]} in for unsupported" unless e.children[0].type == :lvasgn
|
608
674
|
# TODO: mlhs in e.children[0]
|
609
675
|
x = e.children[0].children[0] # loop variable
|
610
|
-
if scope[:outer_env] && (scope[:outer_env].has_key? x) && (not (scope[:outer_env].fixed? x))
|
611
|
-
error :nonlocal_access, [x], e.children[0]
|
612
|
-
end
|
613
676
|
envi, tcollect = tc(scope, env, e.children[1]) # collection to iterate through
|
614
677
|
teaches = nil
|
615
678
|
tcollect = tcollect.canonical
|
616
679
|
case tcollect
|
617
680
|
when RDL::Type::NominalType
|
618
|
-
teaches = lookup(tcollect.name, :each, e.children[1])
|
681
|
+
teaches = lookup(scope, tcollect.name, :each, e.children[1])
|
619
682
|
when RDL::Type::GenericType, RDL::Type::TupleType, RDL::Type::FiniteHashType
|
620
683
|
unless tcollect.is_a? RDL::Type::GenericType
|
621
684
|
error :tuple_finite_hash_promote, (if tcollect.is_a? RDL::Type::TupleType then ['tuple', 'Array'] else ['finite hash', 'Hash'] end), e.children[1] unless tcollect.promote!
|
622
685
|
tcollect = tcollect.canonical
|
623
686
|
end
|
624
|
-
teaches = lookup(tcollect.base.name, :each, e.children[1])
|
687
|
+
teaches = lookup(scope, tcollect.base.name, :each, e.children[1])
|
625
688
|
inst = tcollect.to_inst.merge(self: tcollect)
|
626
689
|
teaches = teaches.map { |t| t.instantiate(inst) }
|
627
690
|
else
|
@@ -639,7 +702,7 @@ RUBY
|
|
639
702
|
break
|
640
703
|
}
|
641
704
|
error :no_each_type, [tcollect.name], e.children[1] if teach.nil?
|
642
|
-
envi = envi
|
705
|
+
envi, _ = tc_vasgn(scope, envi, :lvasgn, x, teach.block.args[0], e.children[0])
|
643
706
|
scope_merge(scope, break: envi, next: envi, redo: envi, tbreak: teach.ret, tnext: envi[x]) { |lscope|
|
644
707
|
# could exit here
|
645
708
|
# if the loop always exits via break, then return type will come only from break, and otherwise the
|
@@ -741,10 +804,15 @@ RUBY
|
|
741
804
|
case kind
|
742
805
|
when :lvar # local variable
|
743
806
|
error :undefined_local_or_method, [name], e unless env.has_key? name
|
744
|
-
if scope[:outer_env] && (scope[:outer_env].has_key? name) && (not (scope[:outer_env].fixed? name))
|
745
|
-
|
807
|
+
capture(scope, name, env[name].canonical) if scope[:outer_env] && (scope[:outer_env].has_key? name) && (not (scope[:outer_env].fixed? name))
|
808
|
+
# if scope[:outer_env] && (scope[:outer_env].has_key? name) && (not (scope[:outer_env].fixed? name))
|
809
|
+
# error :nonlocal_access, [name], e
|
810
|
+
# end
|
811
|
+
if scope[:captured] && scope[:captured].has_key?(name) then
|
812
|
+
[env, scope[:captured][name]]
|
813
|
+
else
|
814
|
+
[env, env[name].canonical]
|
746
815
|
end
|
747
|
-
[env, env[name].canonical]
|
748
816
|
when :ivar, :cvar, :gvar
|
749
817
|
klass = (if kind == :gvar then RDL::Util::GLOBAL_NAME else env[:self] end)
|
750
818
|
unless $__rdl_info.has?(klass, name, :type)
|
@@ -764,10 +832,11 @@ RUBY
|
|
764
832
|
def self.tc_vasgn(scope, env, kind, name, tright, e)
|
765
833
|
case kind
|
766
834
|
when :lvasgn
|
767
|
-
if scope[:
|
768
|
-
|
769
|
-
|
770
|
-
|
835
|
+
if ((scope[:captured] && scope[:captured].has_key?(name)) ||
|
836
|
+
(scope[:outer_env] && (scope[:outer_env].has_key? name) && (not (scope[:outer_env].fixed? name))))
|
837
|
+
capture(scope, name, tright.canonical)
|
838
|
+
[env, scope[:captured][name]]
|
839
|
+
elsif (env.fixed? name)
|
771
840
|
error :vasgn_incompat, [tright, env[name]], e unless tright <= env[name]
|
772
841
|
[env, tright.canonical]
|
773
842
|
else
|
@@ -787,8 +856,17 @@ RUBY
|
|
787
856
|
when :send
|
788
857
|
meth = e.children[1] # note method name include =!
|
789
858
|
envi, trecv = tc(scope, env, e.children[0]) # receiver
|
859
|
+
typs = []
|
860
|
+
if e.children.length > 2
|
861
|
+
# special case of []= when there's a second arg (the index)
|
862
|
+
# this code is a little more general than it has to be unless other similar operators added
|
863
|
+
e.children[2..-1].each { |arg|
|
864
|
+
envi, targ = tc(scope, envi, arg)
|
865
|
+
typs << targ
|
866
|
+
}
|
867
|
+
end
|
790
868
|
# name is not useful here
|
791
|
-
[envi, tc_send(scope, envi, trecv, meth, [tright], nil, e)] # call receiver.meth(tright)
|
869
|
+
[envi, tc_send(scope, envi, trecv, meth, [*typs, tright], nil, e)] # call receiver.meth(other args, tright)
|
792
870
|
else
|
793
871
|
raise RuntimeError, "unknown kind #{kind}"
|
794
872
|
end
|
@@ -796,7 +874,7 @@ RUBY
|
|
796
874
|
|
797
875
|
# [+ e +] is the method call
|
798
876
|
def self.tc_var_type(scope, env, e)
|
799
|
-
error :var_type_format, [], e unless e.children.length == 4
|
877
|
+
error :var_type_format, [], e unless e.children.length == 4 && scope[:block].nil?
|
800
878
|
var = e.children[2].children[0] if e.children[2].type == :sym
|
801
879
|
error :var_type_format, [], e.children[2] if var.nil? || (not (var =~ /^[a-z]/))
|
802
880
|
typ_str = e.children[3].children[0] if (e.children[3].type == :str) || (e.children[3].type == :string)
|
@@ -829,6 +907,13 @@ RUBY
|
|
829
907
|
[env1, typ]
|
830
908
|
end
|
831
909
|
|
910
|
+
def self.tc_note_type(scope, env, e)
|
911
|
+
error :note_type_format, [], e unless e.children.length == 3 && scope[:block].nil?
|
912
|
+
env, typ = tc(scope, env, e.children[2])
|
913
|
+
note :note_type, [typ], e.children[2]
|
914
|
+
[env, typ]
|
915
|
+
end
|
916
|
+
|
832
917
|
# Type check a send
|
833
918
|
# [+ scope +] is the scope; used only for checking block arguments
|
834
919
|
# [+ env +] is the environment; used only for checking block arguments.
|
@@ -860,20 +945,20 @@ RUBY
|
|
860
945
|
when RDL::Type::SingletonType
|
861
946
|
if trecv.val.is_a? Class
|
862
947
|
if meth == :new then name = :initialize else name = meth end
|
863
|
-
ts = lookup(RDL::Util.add_singleton_marker(trecv.val.to_s), name, e)
|
948
|
+
ts = lookup(scope, RDL::Util.add_singleton_marker(trecv.val.to_s), name, e)
|
864
949
|
ts = [RDL::Type::MethodType.new([], nil, RDL::Type::NominalType.new(trecv.val))] if (meth == :new) && (ts.nil?) # there's always a nullary new if initialize is undefined
|
865
950
|
error :no_singleton_method_type, [trecv.val, meth], e unless ts
|
866
951
|
inst = {self: trecv}
|
867
952
|
tmeth_inter = ts.map { |t| t.instantiate(inst) }
|
868
953
|
else
|
869
954
|
klass = trecv.val.class.to_s
|
870
|
-
ts = lookup(klass, meth, e)
|
955
|
+
ts = lookup(scope, klass, meth, e)
|
871
956
|
error :no_instance_method_type, [klass, meth], e unless ts
|
872
957
|
inst = {self: trecv}
|
873
958
|
tmeth_inter = ts.map { |t| t.instantiate(inst) }
|
874
959
|
end
|
875
960
|
when RDL::Type::NominalType
|
876
|
-
ts = lookup(trecv.name, meth, e)
|
961
|
+
ts = lookup(scope, trecv.name, meth, e)
|
877
962
|
error :no_instance_method_type, [trecv.name, meth], e unless ts
|
878
963
|
inst = {self: trecv}
|
879
964
|
tmeth_inter = ts.map { |t| t.instantiate(inst) }
|
@@ -882,20 +967,38 @@ RUBY
|
|
882
967
|
error :tuple_finite_hash_promote, (if trecv.is_a? RDL::Type::TupleType then ['tuple', 'Array'] else ['finite hash', 'Hash'] end), e unless trecv.promote!
|
883
968
|
trecv = trecv.canonical
|
884
969
|
end
|
885
|
-
ts = lookup(trecv.base.name, meth, e)
|
970
|
+
ts = lookup(scope, trecv.base.name, meth, e)
|
886
971
|
error :no_instance_method_type, [trecv.base.name, meth], e unless ts
|
887
972
|
inst = trecv.to_inst.merge(self: trecv)
|
888
973
|
tmeth_inter = ts.map { |t| t.instantiate(inst) }
|
974
|
+
when RDL::Type::VarType
|
975
|
+
error :recv_var_type, [trecv], e
|
889
976
|
else
|
890
|
-
raise RuntimeError, "receiver type #{
|
977
|
+
raise RuntimeError, "receiver type #{trecv} not supported yet"
|
891
978
|
end
|
892
979
|
|
893
980
|
trets = [] # all possible return types
|
894
981
|
# there might be more than one return type because multiple cases of an intersection type might match
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
982
|
+
|
983
|
+
# for ALL of the expanded lists of actuals...
|
984
|
+
RDL::Type.expand_product(tactuals).each { |tactuals_expanded|
|
985
|
+
# AT LEAST ONE of the possible intesection arms must match
|
986
|
+
trets_tmp = []
|
987
|
+
tmeth_inter.each { |tmeth| # MethodType
|
988
|
+
if ((tmeth.block && block) || (tmeth.block.nil? && block.nil?))
|
989
|
+
tmeth_inst = tc_arg_types(tmeth, tactuals_expanded)
|
990
|
+
if tmeth_inst
|
991
|
+
tc_block(scope, env, tmeth.block, block, tmeth_inst) if block
|
992
|
+
trets_tmp << tmeth.ret.instantiate(tmeth_inst) # found a match for this subunion; add its return type to trets_tmp
|
993
|
+
end
|
994
|
+
end
|
995
|
+
}
|
996
|
+
if trets_tmp.empty?
|
997
|
+
# no arm of the intersection matched this expanded actuals lists, so reset trets to signal error and break loop
|
998
|
+
trets = []
|
999
|
+
break
|
1000
|
+
else
|
1001
|
+
trets.concat(trets_tmp)
|
899
1002
|
end
|
900
1003
|
}
|
901
1004
|
if trets.empty? # no possible matching call
|
@@ -910,10 +1013,10 @@ RUBY
|
|
910
1013
|
:initialize
|
911
1014
|
elsif trecv.is_a? RDL::Type::SingletonType
|
912
1015
|
trecv.val.class.to_s
|
913
|
-
elsif trecv.is_a? RDL::Type::NominalType
|
914
|
-
trecv.
|
1016
|
+
elsif (trecv.is_a? RDL::Type::NominalType) || (trecv.is_a? RDL::Type::GenericType)
|
1017
|
+
trecv.to_s
|
915
1018
|
else
|
916
|
-
raise
|
1019
|
+
raise RuntimeError, "impossible to get type #{trecv}"
|
917
1020
|
end
|
918
1021
|
error :arg_type_single_receiver_error, [name, meth, msg], e
|
919
1022
|
end
|
@@ -923,15 +1026,16 @@ RUBY
|
|
923
1026
|
|
924
1027
|
# [+ tmeth +] is MethodType
|
925
1028
|
# [+ actuals +] is Array<Type> containing the actual argument types
|
926
|
-
# return
|
1029
|
+
# return instiation (possibly empty) that makes actuals match method type (if any), nil otherwise
|
927
1030
|
# Very similar to MethodType#pre_cond?
|
928
1031
|
def self.tc_arg_types(tmeth, tactuals)
|
929
|
-
states = [[0, 0]] # position in tmeth, position in tactuals
|
1032
|
+
states = [[0, 0, Hash.new]] # position in tmeth, position in tactuals, inst of free vars in tmeth
|
930
1033
|
tformals = tmeth.args
|
931
1034
|
until states.empty?
|
932
|
-
formal, actual = states.pop
|
1035
|
+
formal, actual, inst = states.pop
|
1036
|
+
inst = inst.dup # avoid aliasing insts in different states since Type.leq mutates inst arg
|
933
1037
|
if formal == tformals.size && actual == tactuals.size # Matched everything
|
934
|
-
return
|
1038
|
+
return inst
|
935
1039
|
end
|
936
1040
|
next if formal >= tformals.size # Too many actuals to match
|
937
1041
|
t = tformals[formal]
|
@@ -940,60 +1044,64 @@ RUBY
|
|
940
1044
|
end
|
941
1045
|
case t
|
942
1046
|
when RDL::Type::OptionalType
|
943
|
-
t = t.type
|
1047
|
+
t = t.type
|
944
1048
|
if actual == tactuals.size
|
945
|
-
states << [formal+1, actual] # skip over optinal formal
|
946
|
-
elsif (not (tactuals[actual].is_a?(RDL::Type::VarargType))) && tactuals[actual]
|
947
|
-
states << [formal+1, actual+1] # match
|
948
|
-
states << [formal+1, actual] # skip
|
1049
|
+
states << [formal+1, actual, inst] # skip over optinal formal
|
1050
|
+
elsif (not (tactuals[actual].is_a?(RDL::Type::VarargType))) && RDL::Type::Type.leq(tactuals[actual], t, inst, false)
|
1051
|
+
states << [formal+1, actual+1, inst] # match
|
1052
|
+
states << [formal+1, actual, inst] # skip
|
949
1053
|
else
|
950
|
-
states << [formal+1, actual] # types don't match; must skip this formal
|
1054
|
+
states << [formal+1, actual, inst] # types don't match; must skip this formal
|
951
1055
|
end
|
952
1056
|
when RDL::Type::VarargType
|
953
|
-
# t = t.type #TODO .instantiate(inst)
|
954
1057
|
if actual == tactuals.size
|
955
|
-
states << [formal+1, actual] # skip to allow empty vararg at end
|
956
|
-
elsif (not (tactuals[actual].is_a?(RDL::Type::VarargType))) && tactuals[actual]
|
957
|
-
states << [formal, actual+1] # match, more varargs coming
|
958
|
-
states << [formal+1, actual+1] # match, no more varargs
|
959
|
-
states << [formal+1, actual] # skip over even though matches
|
960
|
-
elsif tactuals[actual].is_a?(RDL::Type::VarargType) && tactuals[actual]
|
961
|
-
|
1058
|
+
states << [formal+1, actual, inst] # skip to allow empty vararg at end
|
1059
|
+
elsif (not (tactuals[actual].is_a?(RDL::Type::VarargType))) && RDL::Type::Type.leq(tactuals[actual], t.type, inst, false)
|
1060
|
+
states << [formal, actual+1, inst] # match, more varargs coming
|
1061
|
+
states << [formal+1, actual+1, inst] # match, no more varargs
|
1062
|
+
states << [formal+1, actual, inst] # skip over even though matches
|
1063
|
+
elsif tactuals[actual].is_a?(RDL::Type::VarargType) && RDL::Type::Type.leq(tactuals[actual].type, t.type, inst, false) &&
|
1064
|
+
RDL::Type::Type.leq(t.type, tactuals[actual].type, inst, true)
|
1065
|
+
states << [formal+1, actual+1, inst] # match, no more varargs; no other choices!
|
962
1066
|
else
|
963
|
-
states << [formal+1, actual] # doesn't match, must skip
|
1067
|
+
states << [formal+1, actual, inst] # doesn't match, must skip
|
964
1068
|
end
|
965
1069
|
else
|
966
1070
|
if actual == tactuals.size
|
967
1071
|
next unless t.instance_of? RDL::Type::FiniteHashType
|
968
1072
|
if @@empty_hash_type <= t
|
969
|
-
states << [formal+1, actual]
|
1073
|
+
states << [formal+1, actual, inst]
|
970
1074
|
end
|
971
|
-
elsif (not (tactuals[actual].is_a?(RDL::Type::VarargType))) && tactuals[actual]
|
972
|
-
states << [formal+1, actual+1] # match!
|
1075
|
+
elsif (not (tactuals[actual].is_a?(RDL::Type::VarargType))) && RDL::Type::Type.leq(tactuals[actual], t, inst, false)
|
1076
|
+
states << [formal+1, actual+1, inst] # match!
|
973
1077
|
# no else case; if there is no match, this is a dead end
|
974
1078
|
end
|
975
1079
|
end
|
976
1080
|
end
|
977
|
-
|
1081
|
+
return nil
|
978
1082
|
end
|
979
1083
|
|
980
1084
|
# [+ tblock +] is the type of the block (a MethodType)
|
981
1085
|
# [+ block +] is a pair [block-args, block-body] from the block AST node
|
982
1086
|
# returns if the block matches type tblock
|
983
1087
|
# otherwise throws an exception with a type error
|
984
|
-
def self.tc_block(scope, env, tblock, block)
|
1088
|
+
def self.tc_block(scope, env, tblock, block, inst)
|
985
1089
|
# TODO more complex arg lists (same as self.typecheck?); also for
|
986
1090
|
# TODO self is the same *except* instance_exec or instance_eval
|
987
1091
|
raise RuntimeError, "block with block arg?" unless tblock.block.nil?
|
988
1092
|
args, body = block
|
989
|
-
|
1093
|
+
unless tblock.args.length == args.children.length
|
1094
|
+
error :arg_count_mismatch, ['block', tblock.args.length, 'block', args.children.length], (if block[0].children.empty? then block[1] else block[0] end)
|
1095
|
+
end
|
1096
|
+
tblock = tblock.instantiate(inst)
|
990
1097
|
a = args.children.map { |arg| arg.children[0] }.zip(tblock.args).to_h
|
991
1098
|
|
992
1099
|
scope_merge(scope, outer_env: env) { |bscope|
|
993
1100
|
# note: okay if outer_env shadows, since nested scope will include outer scope by next line
|
994
1101
|
env = env.merge(Env.new(a))
|
995
1102
|
_, body_type = if body.nil? then [nil, $__rdl_nil_type] else tc(bscope, env.merge(Env.new(a)), body) end
|
996
|
-
error :bad_return_type, [body_type, tblock.ret], body unless body.nil? || body_type
|
1103
|
+
error :bad_return_type, [body_type, tblock.ret], body unless body.nil? || RDL::Type::Type.leq(body_type, tblock.ret, inst, false)
|
1104
|
+
#
|
997
1105
|
}
|
998
1106
|
end
|
999
1107
|
|
@@ -1001,24 +1109,40 @@ RUBY
|
|
1001
1109
|
# [+ name +] is a symbol naming the thing to look up (either a method or field)
|
1002
1110
|
# returns klass#name's type, walking up the inheritance hierarchy if appropriate
|
1003
1111
|
# returns nil if no type found
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1112
|
+
# types in scope[:context_type] take precedence
|
1113
|
+
|
1114
|
+
# *always* included module's instance methods only
|
1115
|
+
# if included, those methods are added to instance_methods
|
1116
|
+
# if extended, those methods are added to singleton_methods
|
1117
|
+
def self.lookup(scope, klass, name, e)
|
1118
|
+
if scope[:context_types]
|
1119
|
+
# return array of all matching types from context_types, if any
|
1120
|
+
ts = []
|
1121
|
+
scope[:context_types].each { |ctk, ctm, ctt| ts << ctt if ctk.to_s == klass && ctm == name }
|
1122
|
+
return ts unless ts.empty?
|
1123
|
+
end
|
1124
|
+
return scope[:context_types][klass][name] if scope[:context_types] && scope[:context_types][klass] && scope[:context_types][klass][name]
|
1125
|
+
t = $__rdl_info.get_with_aliases(klass, name, :type)
|
1007
1126
|
return t if t # simplest case, no need to walk inheritance hierarchy
|
1008
1127
|
the_klass = RDL::Util.to_class(klass)
|
1128
|
+
is_singleton = RDL::Util.has_singleton_marker(the_klass.to_s)
|
1009
1129
|
included = the_klass.included_modules
|
1010
1130
|
the_klass.ancestors[1..-1].each { |ancestor|
|
1011
1131
|
# assumes ancestors is proper order to walk hierarchy
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
tancestor = $__rdl_info.get(ancestor_name, name, :type)
|
1132
|
+
# included modules' instance methods get added as instance methods, so can't be in singleton class
|
1133
|
+
next if (ancestor.instance_of? Module) && (included.member? ancestor) && is_singleton
|
1134
|
+
# extended (i.e., not included) modules' instance methods get added as singleton methods, so can't be in class
|
1135
|
+
next if (ancestor.instance_of? Module) && (not (included.member? ancestor)) && (not is_singleton)
|
1136
|
+
tancestor = $__rdl_info.get_with_aliases(ancestor.to_s, name, :type)
|
1018
1137
|
return tancestor if tancestor
|
1019
|
-
|
1138
|
+
# special caes: Kernel's singleton methods are *also* added when included?!
|
1139
|
+
if ancestor == Kernel
|
1140
|
+
tancestor = $__rdl_info.get_with_aliases(RDL::Util.add_singleton_marker('Kernel'), name, :type)
|
1141
|
+
return tancestor if tancestor
|
1142
|
+
end
|
1143
|
+
if ancestor.instance_methods(false).member?(name)
|
1020
1144
|
klass = RDL::Util.remove_singleton_marker klass if RDL::Util.has_singleton_marker klass
|
1021
|
-
error :missing_ancestor_type, [
|
1145
|
+
error :missing_ancestor_type, [ancestor, klass, name], e
|
1022
1146
|
end
|
1023
1147
|
}
|
1024
1148
|
return nil
|
@@ -1050,12 +1174,16 @@ type_error_messages = {
|
|
1050
1174
|
block_block: "can't call yield on a block expecting another block argument",
|
1051
1175
|
block_type_error: "argument type error for block\n%s",
|
1052
1176
|
missing_ancestor_type: "ancestor %s of %s has method %s but no type for it",
|
1053
|
-
type_cast_format: "type_cast must be called as type_cast
|
1054
|
-
var_type_format: "var_type must be called as var_type
|
1177
|
+
type_cast_format: "type_cast must be called as `type_cast type-string' or `type_cast type-string, force: expr'",
|
1178
|
+
var_type_format: "var_type must be called as `var_type :var-name, type-string'",
|
1179
|
+
puts_type_format: "puts_type must be called as `puts_type e'",
|
1055
1180
|
generic_error: "%s",
|
1056
1181
|
exn_type: "can't determine exception type",
|
1057
1182
|
cant_splat: "can't type splat with element of type `%s'",
|
1058
1183
|
for_collection: "can't type for with collection of type `%s'",
|
1184
|
+
note_type: "Type is `%s'",
|
1185
|
+
note_message: "%s",
|
1186
|
+
recv_var_type: "Receiver whose type is unconstrained variable `%s' not allowed",
|
1059
1187
|
}
|
1060
1188
|
old_messages = Parser::MESSAGES
|
1061
1189
|
Parser.send(:remove_const, :MESSAGES)
|