rdl 2.0.0.rc4 → 2.0.0.rc5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 036f7f5447f5a5405a0624b7f346101e19012c19
4
- data.tar.gz: 3004e573e29f44e4adea3c2b1cdc314134fa4f4f
3
+ metadata.gz: 37bb60eb1678b69aca3b6967c386f31c2d0c882f
4
+ data.tar.gz: 8824ba76d0daa9d4049df6d4677846386e5c6733
5
5
  SHA512:
6
- metadata.gz: 0355d89a993299da187c096dd85b27209205a0155c80780944c88118a775e2b82f6f9746536465f8f096a8197ebfd1084f64c613f90a1319e2a31b88f5b1645a
7
- data.tar.gz: db84c871660e4e4e6458d31f11bef1ed5a68056d3ac5dd8fd293e85b5b189cc9431c359c28a1db41fc3dd27244835f854aa8aa287a5873786bb0f4b7a72b72b9
6
+ metadata.gz: 593c30a12e453dc79c796b65c8598c345b0bb562978fb4a21fcb903c9a8d0fffcbc0b0534bb0187933ae8399c9dc8022bac689c77601282f442bb687dc79d985
7
+ data.tar.gz: 4daaeafc5cc639db5afe714c9b4c71a8f9455dea7d8151741f3dcd914f384ca728389cc9f1931a255e97513be9b2423c7a0773dc09eba8959905fdfc6a568658
data/README.md CHANGED
@@ -49,23 +49,7 @@
49
49
 
50
50
  # Introduction
51
51
 
52
- RDL is a lightweight system for adding contracts to Ruby. A *contract* decorates a method with assertions describing what the method assumes about its inputs (called a *precondition*) and what the method guarantees about its outputs (called a *postcondition*). For example, using RDL we can write
53
-
54
- ```ruby
55
- require 'rdl'
56
-
57
- pre { |x| x > 0 }
58
- post { |r,x| r > 0 }
59
- def sqrt(x)
60
- # return the square root of x
61
- end
62
- ```
63
-
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...)
65
-
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`.
67
-
68
- In addition to arbitrary pre- and post-conditions, RDL also has extensive support for contracts that are *types*. For example, we can write the following in RDL:
52
+ RDL is a lightweight system for adding types, type checking, and contracts to Ruby. In RDL, *types* can be used to decorate methods:
69
53
 
70
54
  ```ruby
71
55
  require 'rdl'
@@ -74,9 +58,9 @@ type '(Fixnum, Fixnum) -> String'
74
58
  def m(x,y) ... end
75
59
  ```
76
60
 
77
- This indicates that `m` is that method that returns a `String` if given two `Fixnum` arguments. Again this contract is enforced at run-time: When `m` is called, RDL checks that `m` is given exactly two arguments and both are `Fixnums`, and that `m` returns an instance of `String`. RDL supports many more complex type annotations; see below for a complete discussion and examples. We should emphasize here that RDL types are enforced as contracts at method entry and exit. There is no static checking that the method body conforms to the types.
61
+ This indicates that `m` returns a `String` if given two `Fixnum` arguments. When written as above, RDL enforces this type as a *contract* checked at run time: When `m` is called, RDL checks that `m` is given exactly two arguments and both are `Fixnums`, and that `m` returns an instance of `String`.
78
62
 
79
- The beta version of RDL also has an experimental mode in which method bodies can be *statically type checked* against their signatures. (This feature is only in the beta version of the RDL gem.) For example:
63
+ RDL can also *statically type check* method bodies against their signatures. For example:
80
64
 
81
65
  ```ruby
82
66
  file.rb:
@@ -98,7 +82,9 @@ $ ruby file.rb
98
82
 
99
83
  Passing `typecheck: :now` to `type` checks the method body immediately or as soon as it is defined. Passing `typecheck: :call` to `type` statically type checks the method body whenever it is called. Passing `typecheck: sym` for some other symbol statically type checks the method body when `rdl_do_typecheck sym` is called.
100
84
 
101
- RDL contracts and types are stored in memory at run time, so it's also possible for programs to query them. RDL includes lots of contracts and types for the core and standard libraries. Since those methods are generally trustworthy, RDL doesn't actually enforce the contracts (since that would add overhead), but they are available to search and query. RDL includes a small script `rdl_query` to look up type information from the command line. Note you might need to put the argument in quotes depending on your shell.
85
+ RDL supports many more complex type annotations; see below for a complete discussion and examples.
86
+
87
+ RDL types are stored in memory at run time, so it's also possible for programs to query them. RDL includes lots of contracts and types for the core and standard libraries. Since those methods are generally trustworthy, RDL doesn't actually enforce the contracts (since that would add overhead), but they are available to search, query, and use during type checking. RDL includes a small script `rdl_query` to look up type information from the command line. Note you might need to put the argument in quotes depending on your shell.
102
88
 
103
89
  ```shell
104
90
  $ rdl_query String#include? # print type for instance method of another class
@@ -122,7 +108,19 @@ $ irb
122
108
  > rdl_query '...' # as above
123
109
  ```
124
110
 
125
- Currently only type information is returned by `rdl_query` (and not other pre or postconditions).
111
+ RDL also supports more general contracts, though these can only be enforced at run time and are not statically checked. These more general contracts take the form of *preconditions*, describing what a method assumes about its inputs, and *postconditions*, describing what a method guarantees about its outputs. For example:
112
+
113
+ ```ruby
114
+ require 'rdl'
115
+
116
+ pre { |x| x > 0 }
117
+ post { |r,x| r > 0 }
118
+ def sqrt(x)
119
+ # return the square root of x
120
+ end
121
+ ```
122
+
123
+ 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...) 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`. Note that pre- and postconditions can't be searched for using `rdl_query`.
126
124
 
127
125
  # Using RDL
128
126
 
@@ -136,11 +134,11 @@ RDL currently supports Ruby 2.x. It may or may not work with other versions.
136
134
 
137
135
  ## Loading RDL
138
136
 
139
- Use `require 'rdl'` to load the RDL library. If you want to use the core and standard library type signatures that come with RDL, follow it with `require 'rdl_types'`. This will load the types based on the current `RUBY_VERSION`. Currently RDL has types for the following versions of Ruby:
137
+ Use `require 'rdl'` to load the RDL library. If you want to use the core and standard library type signatures that come with RDL, follow it with `require 'types/core'`. This will load the types based on the current `RUBY_VERSION`. Currently RDL has types for the following versions of Ruby:
140
138
 
141
139
  * 2.x
142
140
 
143
- (Currently all these are assumed to have the same library type signatures, which may not be correct.)
141
+ (Currently all 2.x versions are assumed to have the same library type signatures, which may not be correct.)
144
142
 
145
143
  ## Disabling RDL
146
144
 
@@ -164,13 +162,18 @@ to get the head version from github.
164
162
 
165
163
  In development and test mode, you will now have access to `rdl`, `types/core` from RDL, and extra type annotations for Rails and some related gems. In production mode, RDL will be disabled (by loading `rdl_disable`).
166
164
 
167
- Currently, rdl has types for the following versions of Rails:
165
+ **Warning:** Rails support is currently extremely limited, not well tested, and generally needs more work...please send bug reports/pull requests/etc and we will fix things.
166
+
167
+ Currently, RDL has types for the following versions of Rails:
168
168
 
169
169
  * Rails 5.x support - limited to the following:
170
- * Automatically generates
171
- * Models
172
- * Type annotations for model column getters and setters
173
- * `find_by` and `find_by!`
170
+ * Models
171
+ * Generates type annotations for model column getters and setters
172
+ * `find_by` and `find_by!`
173
+ * Generates types annotations for methods added by `belongs_to`, `has_one`, `has_many`, `has_and_belongs_to_many`.
174
+ * Note currently RDL doesn't do very precise type checking of relations
175
+ * Controllers
176
+ * `errors`
174
177
 
175
178
  ## Preconditions and Postconditions
176
179
 
@@ -559,6 +562,12 @@ type MyClass, :foo, '(a: Fixnum, b: String) { () -> %any } -> %any'
559
562
  ```
560
563
  Here `foo`, takes a hash where key `:a` is mapped to a `Fixnum` and key `:b` is mapped to a `String`. Similarly, `{'a'=>Fixnum, 2=>String}` types a hash where keys `'a'` and `2` are mapped to a `Fixnum` and `String`, respectively. Both syntaxes can be used to define hash types.
561
564
 
565
+ RDL also allows a "rest" type in finite hashes (of course, they're not so finite if they use it!):
566
+ ```ruby
567
+ type MyClass, :foo, '(a: Fixnum, b: String, **Float) -> %any'
568
+ ```
569
+ In this method, `a` is a `Fixnum`, `b` is a `String`, and any number (zero or more) remaining keyword arguments can be passed where the values have type `Float`, e.g., a call `foo(a: 3, b: 'b', pi: 3.14)` is allowed.
570
+
562
571
  ## Type Casts
563
572
 
564
573
  Sometimes RDL does not have precise information about an object's type (this is most useful during static type checking). For these cases, RDL supports type casts of the form `o.type_cast(t)`. This call returns a new object that delegates all methods to `o` but that will be treated by RDL as if it had type `t`. If `force: true` is passed to `type_cast`, RDL will perform the cast without checking whether `o` is actually a member of the given type. For example, `x = "a".type_cast('nil', force: true)` will make RDL treat `x` as if it had type `nil`, even though it's a `String`.
@@ -577,23 +586,19 @@ Types can be prefixed with `!` to indicate the associated value is not `nil`. Fo
577
586
 
578
587
  # Static Type Checking
579
588
 
580
- 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.
589
+ 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.
581
590
 
582
591
  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.
583
592
 
584
593
  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.
585
594
 
586
- 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.
587
-
588
- Typechecking does work in `pry` (this feature has only limited testing) as long as typechecking is delayed until after the method is defined:
595
+ 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. Typechecking does work in `pry` (this feature has only limited testing) as long as typechecking is delayed until after the method is defined:
589
596
 
590
597
  ```ruby
591
598
  [2] pry(main)> require 'rdl'
592
- [3] pry(main)> require 'rdl_types'
599
+ [3] pry(main)> require 'types/core'
593
600
  [4] pry(main)> type '() -> Fixnum', typecheck: :later # note: typecheck: :now doesn't work in pry
594
- [5] pry(main)> def f
595
- [5] pry(main)* 'haha'
596
- [5] pry(main)* end
601
+ [5] pry(main)> def f; 'haha'; end
597
602
  [6] pry(main)> rdl_do_typecheck :later
598
603
  RDL::Typecheck::StaticTypeError:
599
604
  (string):2:3: error: got type `String' where return type `Fixnum' expected
@@ -639,7 +644,7 @@ x = 1
639
644
  m() { x = 'bar' }
640
645
  # what is x's type here?
641
646
  ```
642
- 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.
647
+ 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 variable is the union of all types that are ever assigned to it.
643
648
 
644
649
  RDL always treats instance, class, and global variables flow-insensitively, hence their types must be defined with `var_type`:
645
650
 
@@ -656,7 +661,7 @@ end
656
661
 
657
662
  The `var_type` method may also be called as `var_type klass, :name, typ` to assign a type to an instance or class variable of class `klass`.
658
663
 
659
- As a short-hand, RDL defines methods `attr_accessor_type`, `attr_reader_type`, and `attr_writer_type` to behave like their corresponding non-`_type` analogs but attribute types follow the attribute names. For example, `attr_accessor_type :f, 'Fixnum', :g, 'String'` is equivalent to:
664
+ As a short-hand, RDL defines methods `attr_accessor_type`, `attr_reader_type`, and `attr_writer_type` to behave like their corresponding non-`_type` analogs but assign types to the attributes. For example, `attr_accessor_type :f, 'Fixnum', :g, 'String'` is equivalent to:
660
665
 
661
666
  ```ruby
662
667
  var_type :@f, 'Fixnum'
@@ -694,16 +699,18 @@ var_type @g, 'Array<Fixnum or String>'
694
699
  @g = x # uh oh
695
700
  ```
696
701
 
697
- When RDL encounters the assignment to `@g`, it retroactively changes `x` to have type `Array<Fixnum or String>`, which is incompatible with type `[1, String]`, so the last assignment signals an error.
702
+ When RDL encounters the assignment to `@g`, it retroactively changes `x` to have type `Array<Fixnum or String>`, which is incompatible with type `[1, String]` of `@f`, so the assignment to `@g` signals an error.
698
703
 
699
704
  RDL uses the same approach for hashes: hash literals are treated as finite hashes. A finite hash `{k1=>v1, ..., kn=>vn}` can be used where `Hash<k1 or ... or kn, v1 or ... or vn>` is expected. And if a finite hash is used as a `Hash` (including invoking methods on the finite hash; this may change in the future), then it is retroactively converted to a `Hash`.
700
705
 
701
706
  ## Other Features and Limitations
702
707
 
703
- *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.
708
+ *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.
704
709
 
705
710
  * *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`.
706
711
 
712
+ * *Case analysis by class.* If the guard of a `case` statement is a variable, and then `when` branches compare against classes, RDL refines the type of the guard to be those classes within the corresponding `when` branch. For example, in `case x when Fixnum ...(1)... when String ...(2)... end`, RDL will assume `x` is a `Fixnum` within `(1)` and a `String` within `(2)`.
713
+
707
714
  * *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.
708
715
 
709
716
  * *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.
@@ -714,16 +721,17 @@ RDL uses the same approach for hashes: hash literals are treated as finite hashe
714
721
 
715
722
  * *Unsupported Features.* There are several features of Ruby that are currently not handled by RDL. Here is a non-exhaustive list:
716
723
  * `super` is not supported.
717
- * `lambda` has special semantics for `return`; this is not supported.
718
- * Only simple block argument lists and `for` iteration variables are supported.
724
+ * `lambda` has special semantics for `return`; this is not supported. Stabby lambda is also not supported.
725
+ * Only simple `for` iteration variables are supported.
719
726
  * Control flow for exceptions is not analyzed fully soundly; some things are not reported as possibly `nil` that could be.
720
727
  * Only simple usage of constants is handled.
721
728
 
722
729
  ## Assumptions
723
730
 
724
- RDL makes some assumptions that should hold unless your Ruby code is doing something highly unusual:
731
+ RDL's static type checker makes some assumptions that should hold unless your Ruby code is doing something highly unusual. RDL assumes:
725
732
 
726
733
  * `Class#===` is not redefined
734
+ * `Proc#call` is not redefined
727
735
 
728
736
  (More assumptions will be added here as they are added to RDL...)
729
737
 
@@ -739,6 +747,8 @@ RDL also includes a few other useful methods:
739
747
 
740
748
  * `rdl_query` prints information about types; see below for details.
741
749
 
750
+ * `rdl_do_at(sym, &blk)` invokes `blk.call(sym)` when `rdl_do_typecheck(sym)` is called. Useful when type annotations need to be generated at some later time, e.g., because not all classes are loaded.
751
+
742
752
  # Queries
743
753
 
744
754
  As discussed above, RDL includes a small script, `rdl_query`, to look up type information. (Currently it does not support other pre- and postconditions.) The script takes a single argument, which should be a string. Note that when using the shell script, you may need to use quotes depending on your shell. Currently several queries are supported:
@@ -811,7 +821,7 @@ RDL supports the following configuration options:
811
821
 
812
822
  # Bibliography
813
823
 
814
- Here are some research papers we have written exploring contracts, types, and Ruby.
824
+ Here are some research papers we have written exploring types, contracts, and Ruby.
815
825
 
816
826
  * Brianna M. Ren and Jeffrey S. Foster.
817
827
  [Just-in-Time Static Type Checking for Dynamic Languages](http://www.cs.umd.edu/~jfoster/papers/pldi16.pdf).
@@ -863,44 +873,6 @@ Copyright (c) 2014-2016, University of Maryland, College Park. All rights reserv
863
873
  * Alexander T. Yu
864
874
  * Milod Kazerounian
865
875
 
866
- # TODO List
867
-
868
- * How to check whether initialize? is user-defined? method_defined? always
869
- returns true, meaning wrapping isn't fully working with initialize.
870
-
871
- * Currently if a NominalType name is expressed differently, e.g., A
872
- vs. EnclosingClass::A, the types will be different when compared
873
- with ==.
874
-
875
- * Macros, %bool should really be %any
876
-
877
- * Method types that are parametric themselves (not just ones that use
878
- enclosing class parameters)
879
-
880
- * Rails types
881
-
882
- * Deferred contracts on new (watch for class addition)
883
-
884
- * DSL contracts
885
-
886
- * double-splat arguments, which bind to an arbitrary set of keywords
887
-
888
- * included versus extended modules, e.g., Kernel is included in Object, so its
889
- class methods become Object's instance methods
890
-
891
- * Better story for different types for different Ruby versions
892
-
893
- * Better query facility (more kinds of searches). Contract queries?
894
-
895
- * Write documentation on: Raw Contracts and Types, RDL Configuration, Code Overview
896
-
897
- * Structural type queries, allow name to be unknown; same with finite hash keys,
898
- same with generic base types?
899
-
900
- * Allow ... in named args list in queries
901
-
902
- * Queries, include more regexp operators aside from . and ...
903
-
904
- * Queries, allow regexp in class and method names; suggested by Andreas Adamcik, Vienna
876
+ # Discussion group
905
877
 
906
- * Tag for private methods
878
+ [RDL Users](https://groups.google.com/forum/#!forum/rdl-users)
@@ -46,6 +46,9 @@ $__rdl_to_wrap = Set.new
46
46
  $__rdl_to_typecheck = Hash.new
47
47
  $__rdl_to_typecheck[:now] = Set.new
48
48
 
49
+ # Map from symbols to Array<Proc> where the Procs are called when those symbols are rdl_do_typecheck'd
50
+ $__rdl_at = Hash.new
51
+
49
52
  # List of contracts that should be applied to the next method definition
50
53
  $__rdl_deferred = []
51
54
 
@@ -122,6 +125,7 @@ $__rdl_symbol_type = RDL::Type::NominalType.new Symbol
122
125
  $__rdl_range_type = RDL::Type::NominalType.new Range
123
126
  $__rdl_regexp_type = RDL::Type::NominalType.new Regexp
124
127
  $__rdl_standard_error_type = RDL::Type::NominalType.new StandardError
128
+ $__rdl_proc_type = RDL::Type::NominalType.new Proc
125
129
 
126
130
  # Hash from special type names to their values
127
131
  $__rdl_special_types = {'%any' => $__rdl_top_type,
@@ -7,7 +7,9 @@ if Rails.env.development? || Rails.env.test?
7
7
  Dir[File.dirname(__FILE__) + "/../types/rails-#{dir}/**/*.rb"].each { |f| require f }
8
8
  elsif Rails.env.production?
9
9
  require 'rdl_disable'
10
- def (ActionController::Base).params_type(typs); end
10
+ class ActionController::Base
11
+ def self.params_type(typs); end
12
+ end
11
13
  else
12
14
  raise RuntimeError, "Don't know what to do in Rails environment #{Rails.env}"
13
15
  end
@@ -9,8 +9,7 @@ module RDL::Contract
9
9
 
10
10
  def check(slf, *v, &blk)
11
11
  $__rdl_contract_switch.off {
12
- if (@pred &&
13
- ((@pred.arity < 0) ? (@pred.arity.abs - 1) <= v.size : @pred.arity == v.size)) then
12
+ if @pred && v.length >= @pred.arity
14
13
  unless blk ? slf.instance_exec(*v, blk, &@pred) : slf.instance_exec(*v, &@pred) # TODO: Fix blk
15
14
  # unless blk ? pred.call(*v, &blk) : pred.call(*v)
16
15
  raise ContractError,
@@ -2,7 +2,7 @@ module RDL::Typecheck
2
2
 
3
3
  class StaticTypeError < StandardError; end
4
4
 
5
- @@empty_hash_type = RDL::Type::FiniteHashType.new(Hash.new)
5
+ @@empty_hash_type = RDL::Type::FiniteHashType.new(Hash.new, nil)
6
6
  @@asgn_to_var = { lvasgn: :lvar, ivasgn: :ivar, cvasgn: :cvar, gvasgn: :gvar }
7
7
 
8
8
  # Create mapping from file/line numbers to the def that appears at that location
@@ -208,7 +208,6 @@ module RDL::Typecheck
208
208
  raise RuntimeError, "Method #{name} defined where method #{meth} expected" if name.to_sym != meth
209
209
  context_types = $__rdl_info.get(klass, meth, :context_types)
210
210
  types.each { |type|
211
- # TODO will need fancier logic here for matching up more complex arg lists
212
211
  if RDL::Util.has_singleton_marker(klass)
213
212
  # to_class gets the class object itself, so remove singleton marker to get class rather than singleton class
214
213
  self_type = RDL::Type::SingletonType.new(RDL::Util.to_class(RDL::Util.remove_singleton_marker(klass)))
@@ -217,10 +216,7 @@ module RDL::Typecheck
217
216
  end
218
217
  inst = {self: self_type}
219
218
  type = type.instantiate inst
220
- unless type.args.length == args.children.length
221
- error :arg_count_mismatch, ['method', type.args.length, 'method', args.children.length], (if args.children.empty? then ast else args end)
222
- end
223
- targs = args.children.map { |arg| arg.children[0] }.zip(type.args).to_h
219
+ _, targs = args_hash({}, Env.new, type, args, ast, 'method')
224
220
  targs[:self] = self_type
225
221
  scope = { tret: type.ret, tblock: type.block, captured: Hash.new, context_types: context_types }
226
222
  begin
@@ -236,6 +232,79 @@ module RDL::Typecheck
236
232
  $__rdl_info.set(klass, meth, :typechecked, true)
237
233
  end
238
234
 
235
+ # [+ scope +] is used to typecheck default values for optional arguments
236
+ # [+ env +] is used to typecheck default values for optional arguments
237
+ # [+ type +] is a MethodType
238
+ # [+ args +] is an `args` node from the AST
239
+ # [+ ast +] is where to report an error if `args` is empty
240
+ # [+ kind +] is either `'method'` or `'block'`, and is only used for printing error messages
241
+ # Returns a Hash<Symbol, Type> mapping formal argument names to their types
242
+ def self.args_hash(scope, env, type, args, ast, kind)
243
+ targs = Hash.new
244
+ tpos = 0 # position in type.args
245
+ kw_args_matched = []
246
+ kw_rest_matched = false
247
+ args.children.each { |arg|
248
+ error :type_args_fewer, [kind, kind], arg if tpos >= type.args.length && arg.type != :blockarg # blocks could be called with yield
249
+ targ = type.args[tpos]
250
+ if arg.type == :arg
251
+ error :type_arg_kind_mismatch, [kind, 'optional', 'required'], arg if targ.optional?
252
+ error :type_arg_kind_mismatch, [kind, 'vararg', 'required'], arg if targ.vararg?
253
+ targs[arg.children[0]] = targ
254
+ tpos += 1
255
+ elsif arg.type == :optarg
256
+ error :type_arg_kind_mismatch, [kind, 'vararg', 'optional'], arg if targ.vararg?
257
+ error :type_arg_kind_mismatch, [kind, 'required', 'optional'], arg if !targ.optional?
258
+ env, default_type = tc(scope, env, arg.children[1])
259
+ error :optional_default_type, [default_type, targ.type], arg.children[1] unless default_type <= targ.type
260
+ targs[arg.children[0]] = targ.type
261
+ tpos += 1
262
+ elsif arg.type == :restarg
263
+ error :type_arg_kind_mismatch, [kind, 'optional', 'vararg'], arg if targ.optional?
264
+ error :type_arg_kind_mismatch, [kind, 'required', 'vararg'], arg if !targ.vararg?
265
+ targs[arg.children[0]] = RDL::Type::GenericType.new($__rdl_array_type, targ.type)
266
+ tpos += 1
267
+ elsif arg.type == :kwarg
268
+ error :type_args_no_kws, [kind], arg unless targ.is_a?(RDL::Type::FiniteHashType)
269
+ kw = arg.children[0]
270
+ error :type_args_no_kw, [kind, kw], arg unless targ.elts.has_key? kw
271
+ tkw = targ.elts[kw]
272
+ error :type_args_kw_mismatch, [kind, 'optional', kw, 'required'], arg if tkw.is_a? RDL::Type::OptionalType
273
+ kw_args_matched << kw
274
+ targs[kw] = tkw
275
+ elsif arg.type == :kwoptarg
276
+ error :type_args_no_kws, [kind], arg unless targ.is_a?(RDL::Type::FiniteHashType)
277
+ kw = arg.children[0]
278
+ error :type_args_no_kw, [kind, kw], arg unless targ.elts.has_key? kw
279
+ tkw = targ.elts[kw]
280
+ error :type_args_kw_mismatch, [kind, 'required', kw, 'optional'], arg if !tkw.is_a?(RDL::Type::OptionalType)
281
+ env, default_type = tc(scope, env, arg.children[1])
282
+ error :optional_default_kw_type, [kw, default_type, tkw.type], arg.children[1] unless default_type <= tkw.type
283
+ kw_args_matched << kw
284
+ targs[kw] = tkw.type
285
+ elsif arg.type == :kwrestarg
286
+ error :type_args_no_kws, [kind], e unless targ.is_a?(RDL::Type::FiniteHashType)
287
+ error :type_args_no_kw_rest, [kind], arg if targ.rest.nil?
288
+ targs[arg.children[0]] = RDL::Type::GenericType.new($__rdl_hash_type, $__rdl_symbol_type, targ.rest)
289
+ kw_rest_matched = true
290
+ elsif arg.type == :blockarg
291
+ error :type_arg_block, [kind, kind], arg unless type.block
292
+ targs[arg.children[0]] = type.block
293
+ # Note no check that if type.block then method expects block, because blocks can be called with yield
294
+ else
295
+ error :generic_error, ["Don't know what to do with actual argument of type #{arg.type}"], arg
296
+ end
297
+ }
298
+ if (tpos == type.args.length - 1) && type.args[tpos].is_a?(RDL::Type::FiniteHashType)
299
+ rest = type.args[tpos].elts.keys - kw_args_matched
300
+ error :type_args_kw_more, [kind, rest.map { |s| s.to_s }.join(", "), kind], ast unless rest.empty?
301
+ error :type_args_kw_rest, [kind], ast unless kw_rest_matched || type.args[tpos].rest.nil?
302
+ else
303
+ error :type_args_more, [kind, kind], (if args.children.empty? then ast else args end) if type.args.length != tpos
304
+ end
305
+ return [env, targs]
306
+ end
307
+
239
308
  # The actual type checking logic.
240
309
  # [+ scope +] tracks flow-insensitive information about the current scope, excluding local variables
241
310
  # [+ env +] is the (local variable) Env
@@ -339,7 +408,7 @@ module RDL::Typecheck
339
408
  if is_fh
340
409
  # keys are all symbols
341
410
  fh = tlefts.map { |t| t.val }.zip(trights).to_h
342
- [envi, RDL::Type::FiniteHashType.new(fh)]
411
+ [envi, RDL::Type::FiniteHashType.new(fh, nil)]
343
412
  else
344
413
  tleft = RDL::Type::UnionType.new(*tlefts)
345
414
  tright = RDL::Type::UnionType.new(*trights)
@@ -507,6 +576,12 @@ module RDL::Typecheck
507
576
  else
508
577
  error :cant_splat, [ti], ei.children[0]
509
578
  end
579
+ elsif ei.type == :block_pass
580
+ raise RuntimeError, "impossible to pass block arg and literal block" if scope[:block]
581
+ envi, ti = tc(sscope, envi, ei.children[0])
582
+ # convert using to_proc if necessary
583
+ ti = tc_send(sscope, envi, ti, :to_proc, [], nil, ei) unless ti.is_a? RDL::Type::MethodType
584
+ block = [ti, ei]
510
585
  else
511
586
  envi, ti = tc(sscope, envi, ei)
512
587
  tactuals << ti
@@ -918,7 +993,7 @@ RUBY
918
993
  # [+ trecvs +] is the type of the recevier
919
994
  # [+ meth +] is a symbol with the method name
920
995
  # [+ tactuals +] are the actual arguments
921
- # [+ block +] is a pair of expressions [block-args, block-body], from the block AST node
996
+ # [+ block +] is a pair of expressions [block-args, block-body], from the block AST node OR [block-type, block-arg-AST-node]
922
997
  # [+ e +] is the expression at which location to report an error
923
998
  def self.tc_send(scope, env, trecvs, meth, tactuals, block, e)
924
999
  scope[:exn] = Env.join(e, scope[:exn], env) if scope.has_key? :exn # assume this call might raise an exception
@@ -947,6 +1022,16 @@ RUBY
947
1022
  error :no_singleton_method_type, [trecv.val, meth], e unless ts
948
1023
  inst = {self: trecv}
949
1024
  tmeth_inter = ts.map { |t| t.instantiate(inst) }
1025
+ elsif trecv.val.is_a?(Symbol) && meth == :to_proc
1026
+ # Symbol#to_proc on a singleton symbol type produces a Proc for the method of the same name
1027
+ if env[:self].is_a?(RDL::Type::NominalType)
1028
+ klass = env[:self].klass
1029
+ else # SingletonType(class)
1030
+ klass = env[:self].val
1031
+ end
1032
+ ts = lookup(scope, klass.to_s, trecv.val, e)
1033
+ error :no_type_for_symbol, [trecv.val.inspect], e if ts.nil?
1034
+ return ts
950
1035
  else
951
1036
  klass = trecv.val.class.to_s
952
1037
  ts = lookup(scope, klass, meth, e)
@@ -970,6 +1055,14 @@ RUBY
970
1055
  tmeth_inter = ts.map { |t| t.instantiate(inst) }
971
1056
  when RDL::Type::VarType
972
1057
  error :recv_var_type, [trecv], e
1058
+ when RDL::Type::MethodType
1059
+ if meth == :call
1060
+ # Special case - invokes the Proc
1061
+ tmeth_inter = [trecv]
1062
+ else
1063
+ # treat as Proc
1064
+ tc_send_one_recv(scope, env, $__rdl_proc_type, meth, tactuals, block, e)
1065
+ end
973
1066
  else
974
1067
  raise RuntimeError, "receiver type #{trecv} not supported yet"
975
1068
  end
@@ -1006,12 +1099,14 @@ Actual arg type#{tactuals.size > 1 ? "s" : ""}:
1006
1099
  (#{tactuals.map { |ti| ti.to_s }.join(', ')}) #{if block then '{ block }' end}
1007
1100
  RUBY
1008
1101
  msg.chomp! # remove trailing newline
1009
- name = if (trecv.is_a? RDL::Type::SingletonType) && (trecv.val.is_a? Class) && (meth == :new) then
1102
+ name = if trecv.is_a?(RDL::Type::SingletonType) && trecv.val.is_a?(Class) && (meth == :new) then
1010
1103
  :initialize
1011
1104
  elsif trecv.is_a? RDL::Type::SingletonType
1012
1105
  trecv.val.class.to_s
1013
- elsif (trecv.is_a? RDL::Type::NominalType) || (trecv.is_a? RDL::Type::GenericType)
1106
+ elsif trecv.is_a?(RDL::Type::NominalType) || trecv.is_a?(RDL::Type::GenericType)
1014
1107
  trecv.to_s
1108
+ elsif trecv.is_a?(RDL::Type::MethodType)
1109
+ 'Proc'
1015
1110
  else
1016
1111
  raise RuntimeError, "impossible to get type #{trecv}"
1017
1112
  end
@@ -1079,27 +1174,28 @@ RUBY
1079
1174
  end
1080
1175
 
1081
1176
  # [+ tblock +] is the type of the block (a MethodType)
1082
- # [+ block +] is a pair [block-args, block-body] from the block AST node
1177
+ # [+ block +] is a pair [block-args, block-body] from the block AST node OR [block-type, block-arg-AST-node]
1083
1178
  # returns if the block matches type tblock
1084
1179
  # otherwise throws an exception with a type error
1085
1180
  def self.tc_block(scope, env, tblock, block, inst)
1086
- # TODO more complex arg lists (same as self.typecheck?); also for
1087
1181
  # TODO self is the same *except* instance_exec or instance_eval
1088
1182
  raise RuntimeError, "block with block arg?" unless tblock.block.nil?
1089
- args, body = block
1090
- unless tblock.args.length == args.children.length
1091
- error :arg_count_mismatch, ['block', tblock.args.length, 'block', args.children.length], (if block[0].children.empty? then block[1] else block[0] end)
1092
- end
1093
1183
  tblock = tblock.instantiate(inst)
1094
- a = args.children.map { |arg| arg.children[0] }.zip(tblock.args).to_h
1095
-
1096
- scope_merge(scope, outer_env: env) { |bscope|
1097
- # note: okay if outer_env shadows, since nested scope will include outer scope by next line
1098
- env = env.merge(Env.new(a))
1099
- _, body_type = if body.nil? then [nil, $__rdl_nil_type] else tc(bscope, env.merge(Env.new(a)), body) end
1100
- error :bad_return_type, [body_type, tblock.ret], body unless body.nil? || RDL::Type::Type.leq(body_type, tblock.ret, inst, false)
1101
- #
1102
- }
1184
+ if block[0].is_a? RDL::Type::MethodType
1185
+ error :bad_block_arg_type, [block[0], tblock], block[1] unless block[0] <= tblock
1186
+ elsif block[0].is_a?(RDL::Type::NominalType) && block[0].name == 'Proc'
1187
+ error :proc_block_arg_type, [tblock], block[1]
1188
+ else # must be [block-args, block-body]
1189
+ args, body = block
1190
+ env, targs = args_hash(scope, env, tblock, args, block, 'block')
1191
+ scope_merge(scope, outer_env: env) { |bscope|
1192
+ # note: okay if outer_env shadows, since nested scope will include outer scope by next line
1193
+ env = env.merge(Env.new(targs))
1194
+ _, body_type = if body.nil? then [nil, $__rdl_nil_type] else tc(bscope, env.merge(Env.new(targs)), body) end
1195
+ error :bad_return_type, [body_type, tblock.ret], body unless body.nil? || RDL::Type::Type.leq(body_type, tblock.ret, inst, false)
1196
+ #
1197
+ }
1198
+ end
1103
1199
  end
1104
1200
 
1105
1201
  # [+ klass +] is a string containing the class name
@@ -1173,7 +1269,6 @@ type_error_messages = {
1173
1269
  masgn_bad_lhs: "no corresponding right-hand side elemnt for left-hand side assignee",
1174
1270
  kw_not_allowed: "can't use `%s' in current scope",
1175
1271
  kw_arg_not_allowed: "argument to `%s' not allowed in current scope",
1176
- arg_count_mismatch: "`%s' signature expects %d arguments, actual `%s' has %d arguments",
1177
1272
  no_block: "attempt to call yield in method not declared to take a block argument",
1178
1273
  block_block: "can't call yield on a block expecting another block argument",
1179
1274
  block_type_error: "argument type error for block\n%s",
@@ -1187,7 +1282,23 @@ type_error_messages = {
1187
1282
  for_collection: "can't type for with collection of type `%s'",
1188
1283
  note_type: "Type is `%s'",
1189
1284
  note_message: "%s",
1190
- recv_var_type: "Receiver whose type is unconstrained variable `%s' not allowed",
1285
+ recv_var_type: "receiver whose type is unconstrained variable `%s' not allowed",
1286
+ type_args_more: "%s type accepts more arguments than actual %s definition",
1287
+ type_args_fewer: "%s type accepts fewer arguments than actual %s definition",
1288
+ type_arg_kind_mismatch: "%s type has %s argument but actual argument is %s",
1289
+ type_args_no_kws: "%s type does not expect keyword arguments but actual expects keywords",
1290
+ type_args_no_kw: "%s type does not expect keyword argument `%s'",
1291
+ type_args_kw_mismatch: "%s type has %s keyword `%s' but actual argument is %s",
1292
+ type_args_kw_more: "%s type expects keywords `%s' that are not expected by actual %s",
1293
+ type_args_no_kw_rest: "%s type has no rest keyword but actual method accepts rest keywords",
1294
+ type_args_kw_rest: "%s type has rest keyword but actual method does not accept rest keywords",
1295
+ optional_default_type: "default value has type `%s' where type `%s' expected",
1296
+ optional_default_kw_type: "default value for `%s' has type `%s' where type `%s' expected",
1297
+ type_arg_block: "%s type does not expect block but actual %s takes block",
1298
+ bad_block_arg_type: "block argument has type `%s' but expecting type `%s'",
1299
+ non_block_block_arg: "block argument should have a block type but instead has type `%s'",
1300
+ proc_block_arg_type: "block argument is a Proc; can't tell if it matches expected type `%s'",
1301
+ no_type_for_symbol: "can't find type for method corresponding to `%s.to_proc'",
1191
1302
  }
1192
1303
  old_messages = Parser::MESSAGES
1193
1304
  Parser.send(:remove_const, :MESSAGES)