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)