[
https://issues.apache.org/jira/browse/GROOVY-12643?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King updated GROOVY-12643:
-------------------------------
Description:
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-12644: 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.
was:
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.
> 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
> Priority: Major
>
> 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-12644: 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)