Paul King created GROOVY-12643:
----------------------------------

             Summary: STC list/map literals to collection types
                 Key: GROOVY-12643
                 URL: https://issues.apache.org/jira/browse/GROOVY-12643
             Project: Groovy
          Issue Type: Improvement
            Reporter: Paul King


h3. Problem

Under {{@TypeChecked}} and {{@CompileStatic}}, a list or map literal assigned 
to a collection or map type follows rules of its own (the "list and map 
constructors" section of the type checking documentation). They differ from the 
dynamic cast in two ways:
* a literal assigned to a collection or map interface that the literal's own 
class ({{ArrayList}}, {{LinkedHashSet}} or {{LinkedHashMap}}) does not 
implement is a compile error;
* a literal assigned to a concrete collection class is matched against a 
constructor taking the *elements* as arguments, so a collection class is either 
rejected or given the elements as constructor arguments, e.g. a capacity.

GROOVY-12585 gives dynamic code one table of default implementations, so the 
gap between modes grows unless the type checker follows.

h4. 1. Interface targets are rejected

||Declaration||Dynamic (with GROOVY-12585)||{{@TypeChecked}} / 
{{@CompileStatic}}||
|{{SortedSet s = [3, 1, 2]}}|{{TreeSet [1, 2, 3]}}|Cannot assign value of type 
java.util.ArrayList<java.lang.Integer> to variable of type java.util.SortedSet|
|{{Deque d = [3, 1, 2]}}|{{LinkedList [3, 1, 2]}}|same error|
|{{Queue q = []}}|{{LinkedList []}}|same error|
|{{SortedMap m = [b: 2, a: 1]}}|{{TreeMap [a:1, b:2]}}|Cannot find matching 
constructor java.util.SortedMap(java.util.LinkedHashMap<...>)|
|{{ConcurrentMap m = [a: 1]}}|{{ConcurrentHashMap [a:1]}}|same error|

{{Set}}, {{List}}, {{Collection}}, {{Iterable}} and {{Map}} work, because the 
literal's class implements them or because of the {{LinkedHashSet}} exemption 
added in GROOVY-10002.

The interface targets have never worked by assignment: they fail in every mode 
in every release checked (2.4.12, 2.5.23, 3.0.9, 3.0.25, 4.0.5, 4.0.33, 5.0.8, 
5.1.3, 6.0.0). Some releases (2.4.12, 3.0.9, 4.0.5) type check {{SortedMap m = 
[...]}} and then fail at run time with {{GroovyCastException}}; the others 
reject it at compile time.

h4. 2. Concrete collection classes use the element constructor

||Declaration||Dynamic (with 
GROOVY-12585)||{{@TypeChecked}}||{{@CompileStatic}}||
|{{Vector v = [5]}}|{{[5]}}|{{[5]}}|{{[]}} (calls {{Vector(int)}})|
|{{PriorityQueue q = [5]}}|{{[5]}}|{{[5]}}|{{[]}}|
|{{ArrayDeque d = [5]}}|{{[5]}}|{{[5]}}|{{[]}}|
|{{LinkedList l = [3, 1]}}|{{[3, 1]}}|Cannot find matching constructor 
java.util.LinkedList(int, int)|same error|
|{{Stack s = [3, 1, 2]}}|{{[3, 1, 2]}}|Cannot find matching constructor|same 
error|

{{@TypeChecked}} code runs the dynamic cast, so where it compiles it follows 
the dynamic result. {{@CompileStatic}} code calls the matched constructor 
directly.

h3. Cause

In {{StaticTypeCheckingVisitor}}:
* {{addListAssignmentConstructorErrors}} exempts targets that are supertypes of 
{{ArrayList}} or {{LinkedHashSet}}, applies the constructor shorthand to 
concrete classes, and rejects any other interface or abstract class.
* {{addMapAssignmentConstructorErrors}} exempts supertypes of {{LinkedHashMap}} 
and applies the constructor shorthand to everything else, which an interface 
cannot satisfy.
* The inferred type of the declaration ({{getLiteralResultType}}) is 
{{ArrayList}}, {{LinkedHashSet}} or {{LinkedHashMap}} for the exempt targets.

Under {{@CompileStatic}} the literal is built as an {{ArrayList}} or 
{{LinkedHashMap}}, and a target it is not an instance of is reached through the 
indy cast, which calls {{DefaultTypeTransformation.castToType}}. {{Set s = [3, 
1]}} compiles to an {{ArrayList}} followed by {{invokedynamic 
cast:(ArrayList)LinkedHashSet}}. So an interface target needs no new code 
generation once the checker accepts it.

h3. Proposal

*Part 1: interface targets.* Accept a list literal assigned to {{SortedSet}}, 
{{NavigableSet}}, {{Queue}}, {{Deque}} or {{BlockingQueue}}, and a map literal 
assigned to {{SortedMap}}, {{NavigableMap}} or {{ConcurrentMap}}, as {{Set}} is 
accepted today. The inferred type is the default implementation from 
GROOVY-12585 ({{TreeSet}}, {{LinkedList}}, {{LinkedBlockingQueue}}, 
{{TreeMap}}, {{ConcurrentHashMap}}), and the elements are checked against the 
declared type arguments:
{code:groovy}
@CompileStatic
void test() {
    SortedSet<Integer> s = [3, 1, 2]      // TreeSet [1, 2, 3]
    Deque<String> d = []                  // LinkedList
    SortedMap<String, Integer> m = [b: 2, a: 1]  // TreeMap [a:1, b:2]
    SortedSet<String> bad = [3, 1]        // error: Incompatible generic 
argument types
}
{code}
Every program this accepts is a compile error today, so no compiling program 
changes meaning.

*Part 2: concrete collection classes.* For a list literal assigned to a 
concrete collection class that has a constructor taking a collection, or a 
no-arg constructor, copy the elements, as the dynamic cast does, instead of 
matching a constructor against them. The inferred type is the class, and the 
elements are checked against its type arguments:
{code:groovy}
@CompileStatic
void test() {
    Vector<Integer> v = [5]               // [5], was an empty Vector
    LinkedList<Integer> l = [3, 1]        // [3, 1], was a compile error
    Stack<Integer> s = [3, 1, 2]          // was a compile error
}
{code}
Other classes keep the constructor shorthand: {{Point p = [1, 2]}}, {{Dimension 
d = [100, 200]}}, and a collection class whose only constructors take the 
elements.

h3. Behaviour changes

* Part 1 only accepts programs that are rejected today.
* Part 2 changes the result of compiling code. Under {{@CompileStatic}}, 
{{Vector v = [5]}}, {{PriorityQueue q = [5]}} and {{ArrayDeque d = [5]}} hold 
the element instead of being empty with that capacity. With type arguments, 
using the literal as a capacity becomes a compile error, for example 
{{ArrayDeque<String> d = [16]}}. Write {{new ArrayDeque(16)}} for a capacity.
* {{ArraysAndCollectionsSTCTest.testCollectionTypesInitializedByListLiteral2}} 
pins {{ArrayDeque<String> deque = [123] // ArrayDeque(int numElements)}} 
(GROOVY-10002). It would change to {{ArrayDeque<Integer> deque = [123]}} 
holding {{123}}, with {{ArrayDeque<String> deque = [123]}} rejected.
* The {{Deque}}/{{Queue}} cases in 
{{testCollectionTypesInitializedByListLiteral3}} and {{SortedMap m = [:]}} in 
{{ConstructorsSTCTest.testMapLiteral}} pin the current rejections, and would 
assert the new results.

Part 2 is the one to discuss: it changes behaviour that has been consistent 
across modes, if unhelpful, and that one existing test treats as intended. Part 
1 can be done without it.

h3. Out of scope

* GROOVY-YYYYY: a literal returned from a method under static checking is 
rejected unless its own class is assignable to the return type. {{Set m() \{ 
[3, 1] \}}} fails while {{Set s = [3, 1]}} compiles. Returns have never 
followed the declaration rules, including the existing {{Set}} exemption.
* Assigning a non-literal value, such as {{SortedSet s = aList}}, stays an 
error under static checking, as in Java.
* A list or map argument is not converted to a method's parameter type, in any 
mode, by design.

h3. Related

* GROOVY-12585: the runtime default-implementation table this relies on.
* GROOVY-10002: introduced the {{LinkedHashSet}} exemption and the 
capacity-idiom test.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to