[ 
https://issues.apache.org/jira/browse/GROOVY-12115?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King updated GROOVY-12115:
-------------------------------
    Description: 
h1. Summary

Introduce a parameter annotation (working name {{{}@ClassTag{}}}) that lets the 
static compiler *auto-supply a reified {{Class<K>}} argument* from a method 
receiver's generic type, so callers can omit type tokens that today exist only 
to work around erasure.

Split off from GROOVY-11807. That issue ships the *runtime* half — explicit 
{{{}Class<K>{}}}-token overloads that enforce types at run time. This issue is 
the *compile-time ergonomics* half: making the token optional under static 
compilation.

h2. Background and origin

On the JVM, a method that needs the runtime type of a generic type parameter 
cannot
recover it (erasure), so the API must take an explicit {{Class<K>}} token. 
Groovy
already does this in several places, e.g. {{asChecked}} (since 5.0.0) and the 
new
key-checked {{withDefault}} overloads from GROOVY-11807:
{code:java}
// DefaultGroovyMethods
public static <T>   Collection<T> asChecked(Collection<T> self, Class<T> type)  
          // -> Collections.checkedCollection
public static <K,V> Map<K,V>      asChecked(Map<K,V> self, Class<K> keyType, 
Class<V> valueType) // -> Collections.checkedMap

// GROOVY-11807 (already on the groovy11807 branch), Class params carry a /* 
@ClassTag GROOVY-12115 */ marker
public static <K,V> Map<K,V> withDefault(Map<K,V> self, Class<K> keyType, 
Class<V> valueType, Closure<V> init)
public static <K,V> Map<K,V> withDefault(Map<K,V> self, Class<K> keyType, 
Closure<V> init)
{code}
In every case the {{Class}} argument is *redundant with a type the compiler 
already knows* (the receiver's type argument). {{@ClassTag}} removes the 
redundancy under static compilation.

h2. The problem

 * Verbose: {{list.asChecked(String)}} / {{map.asChecked(Number, Foo)}} restate 
the
element/key/value types the static type already declares.
 * Not transparent: the safety-improving variant of {{withDefault}} can't be 
selected
unless the caller manually threads the {{{}Class{}}}.
 * The tokens cannot be inferred at run time — only the compiler, with the 
static type
in hand, can supply them.

h2. Proposal: {{@ClassTag}}

A parameter annotation marking a {{Class<X>}} parameter as 
{*}compiler-supplied{*}. Under
{{@CompileStatic}} / {{@TypeChecked}} the type checker may select an overload 
whose only
extra parameter is a {{@ClassTag Class<X>}} and synthesise {{X.class}} from the
receiver's matching type argument, rewriting the call. Dynamic Groovy ignores 
the
annotation and binds the original overload.
{code:java}
@Retention(RetentionPolicy.CLASS)   // visible to the type checker; SOURCE may 
suffice
@Target(ElementType.PARAMETER)
public @interface ClassTag {
    // Optional override naming the type variable to reify, for when the 
parameter
    // type cannot carry it. NOTE: 'for' is a Java reserved word and cannot be 
an
    // annotation member name, so use e.g. value()/from():  @ClassTag(from = 
"K")
    String from() default "";
}
{code}

h3. Annotation shape and binding

 * *Param-level, not method+index.* An earlier sketch put {{@OverrideCall(1)}} 
on the
method with an index. Moving the marker onto the parameter makes the *position*
implicit (where it sits) and lets the *parameter type* {{Class<K>}} encode 
*which*
generic to reify — strictly more self-describing than a bare index (which never 
said
K-vs-V).
 * *Default binding* reads the type variable from {{{}Class<X>{}}}. The type 
checker sees
{{<X>}} pre-erasure (and the Signature attribute retains it), so this is 
reliable.
* *Optional override* {{@ClassTag("K")}} (member name {{value()}}, for edge 
cases the parameter type can't
express: a {{Class<?>}}/raw token, or a method whose own type variables don't 
line up with the
receiver's. The STC *rejects a name that does not resolve to a type variable in 
scope*, so typos
fail at compile time — *but only for methods compiled from source in the 
current unit*. An extension
method from an already-compiled library (e.g. a Java DGM method) is never 
visited as source, so a
typo in its override silently disables injection rather than being reported. 
Library {{@ClassTag}}

h3. Multiple tokens and fill order

Some consumers need more than one token, e.g. asChecked(Map, @ClassTag Class<K>,
@ClassTag Class<V>). Rule: each {{@ClassTag}} parameter is synthesised *at its
declared position*; the caller's supplied arguments map to the remaining 
(non-tag)
parameters left-to-right. For {{asChecked()}} that means "fill slots 1 and 2 
with
{{{}K.class{}}}, {{V.class}} in declaration order." (This generalises the 
single-hole case
the {{@OverrideCall(1)}} index handled.)

h3. Overload selection / preemption

For a call that *already matches* a no-token overload (e.g. withDefault\{...}), 
the
{{@ClassTag}} overload must *preempt* it for a transparent upgrade — the 
compiler
prefers injecting a token over an exact arity match. This is a deliberate 
resolution
rule and means unchanged source can change behaviour under static typing.
 * Use preemption *only when the new behaviour is an unambiguous bug-fix* (the 
old
behaviour has no legitimate users).
 * Otherwise *mint a new name* (e.g. {{{}withCheckedDefault{}}}) so the opt-in 
is by name
and nothing existing changes meaning. {{@ClassTag}} still earns its keep there 
by
letting static callers omit the {{{}Class{}}}.

h2. Examples (before / after, under @CompileStatic)

||API||Today||With @ClassTag||
|asChecked (list)|[].asChecked(String)|[].asChecked()|
|asChecked (map)|[:].asChecked(Number, Foo)|[:].asChecked()|
|withDefault (map)|[:].withDefault(Number)\{...}|[:].withDefault\{...} 
(transparently checked)|
{code:groovy}
// asChecked: the Class was mandatory -> @ClassTag makes it omissible (opt-in, 
additive)
Map<Number,String> m = [:].asChecked()          // compiler injects 
Number.class, Foo.class

// withDefault: a no-Class form already exists -> @ClassTag overload preempts 
it (silent upgrade)
Map<Number,?> m = [:].withDefault{ null }        // same source as today
m['x']                                            // now ClassCastException 
(compiler injected Number.class)
{code}

h2. Boundaries and edges

 * *Static-only.* The injection is an STC rule. In dynamic Groovy:
 ** {{withDefault}} keeps binding the *unchecked* overload (no K to inject) — 
same
source, different behaviour by compilation mode.
 ** {{asChecked()}} (no Class) won't resolve at all — there is no zero-arg 
method and
no injection -> {{{}MissingMethodException{}}}. The shorter spelling is 
{*}static-only syntax{*}.
 * *K must be statically known.* {{Map<Number,?>}} works; {{def}} / raw {{Map}} 
/
wildcard-only don't — no token is synthesised, so {{withDefault}} stays lenient 
and
{{asChecked()}} won't compile.
 * *Fail-soft vs fail-loud.* As sketched, when K can't be reified the compiler 
silently
degrades (no token / {{{}Object.class{}}}). Scala instead makes it a *compile 
error* (you
must propagate the evidence). Degrading soft risks a false sense of safety; 
this needs
a decision (see open questions).
 * *Escape hatch.* The explicit {{Class}} form always works — in every mode, 
and when
the type is unknown.
 * *Fidelity is erased.* {{Class<K>}} reifies only the *erased* class:
{{List<String>}} and {{List<Integer>}} are indistinguishable (see TypeTag 
section).
* *Override validation is source-only.* {{@ClassTag("name")}} typo-checking 
runs when the declaring
method is type-checked from source; compiled extension methods are not 
re-validated by the consumer.

h2. Retrofit candidates (codebase audit)

||Tier||API||Why it fits / note||
|1|{{asChecked(...)}} — 9 overloads, since 5.0.0|Textbook case: explicit 
{{Class}} is purely an erasure-workaround over {{{}Collections.checkedXxx{}}}. 
Also the multi-token (K,V) stress test.|
|1|{{withDefault}} / {{withCheckedDefault}} (GROOVY-11807)|Type-laundering 
insert via the untyped {{{}Map.get(Object){}}}; reified K lets it check. 
Unifies with checked maps: {{withDefault = checkedMap + default}} once K is 
reified.|
|2|{{ListWithDefault}} ({{{}(T) initClosure.call(...){}}})|Default cast to 
element type; weaker — the closure return is already statically checkable.|
|3 (new method)|{{collection.toArray()}} -> {{T[]}}|Reified array creation (the 
Kotlin {{reified}} / Scala {{ClassTag}} array case). New API, not a retrofit of 
{{{}asType{}}}.|
|none|{{{}asType{}}}/{{{}as{}}}, {{{}newInstance(Class){}}}, 
{{{}find{}}}/{{{}grep{}}}-by-Class, {{castToType}}|The {{Class}} is the user's 
{*}intent{*}, not an erasure-workaround — auto-supplying would be wrong.|

Net: the realistic footprint is small and clustered around *checked-view and
default-growing collection APIs*, not a sprawling refactor.

h2. Reification fidelity: ClassTag vs a possible TypeTag tier (weaker)

{{@ClassTag}} supplies an *erased* {{{}Class<K>{}}}. A richer future tier could 
inject a
*full-generic* token (a super-type token {{{}TypeToken<K>{}}}), analogous to 
Scala's
{{TypeTag}} / {{scala.quoted.Type}} versus its {{{}ClassTag{}}}. Kotlin's
{{inline fun <reified T>}} is the JVM precedent for the cheap tier.

*This extension now looks weak given the boundaries above:*
 * The concrete candidates ({{{}asChecked{}}} -> 
{{{}Collections.checkedMap{}}}, {{{}withDefault{}}})
only ever do {{isInstance}} checks, which are erased anyway — the erased class 
suffices.
 * Restricting the source to the *receiver's type argument* and to *static-only*
injection further limits where full-generic fidelity would pay off.

Recommendation: keep {{ClassTag}} (erased) as the design; note 
{{{}TypeTag{}}}/{{{}TypeToken{}}}
as a *possible later tier* only if a real full-generic consumer appears. The 
annotation
*name* could encode the fidelity ({{{}@ClassTag{}}} now, reserving {{@TypeTag}} 
for the full
tier), or favour familiarity ({{{}@Reified{}}}, after Kotlin) — see open 
questions.

h2. Comparison with Scala / Kotlin / Java

||Mechanism||How the type is obtained||Fidelity||Propagation||
|Groovy {{@ClassTag}} (proposed)|compiler synthesises {{Class}} from the 
receiver's type arg|erased class|fail-soft (proposed)|
|Scala {{ClassTag}}|implicit/{{{}using{}}} evidence materialised from the 
static type|erased class|fail-loud (context bound {{{}[T: ClassTag]{}}})|
|Scala {{TypeTag}} / {{quoted.Type}}|implicit/{{{}using{}}} evidence|full 
generic|fail-loud|
|Kotlin {{reified}}|{{inline}} + reify the type parameter|actual type 
(inlined)|enforced by {{inline}}|
|Java baseline|explicit {{Class<T>}} token ({{{}EnumMap{}}}, 
{{{}Collections.checkedMap{}}}, {{{}getAnnotation(Class<T>){}}})|erased 
class|manual|
|Guava/Gson {{TypeToken<T>}}|super-type token|full generic|manual|

In short: {{@ClassTag}} is Scala's {{ClassTag}} idea (compiler-supplied runtime 
type
evidence) delivered via an annotation + STC call-rewrite instead of implicit
parameters; it auto-supplies the *Java {{Class<T>}} token* idiom rather than 
inventing
a new runtime representation.

h2. Proposed scope / deliverables

 # The {{@ClassTag}} annotation (parameter target; CLASS/SOURCE retention; 
optional
{{{}from{}}}/{{{}value{}}} override — *not* {{{}for{}}}, which is reserved).
 # STC support: overload selection that prefers a {{@ClassTag}} overload and 
synthesises
the {{X.class}} argument(s) from the receiver's type arguments; validation of
{{{}from{}}}; well-defined multi-token fill order.
 # First consumers: retrofit {{asChecked}} and the GROOVY-11807 {{withDefault}}
overloads (validating the K,V multi-token case).
 # Decide and document: fail-soft vs fail-loud; preemption-vs-new-name policy.

*Out of scope:* the full-generic {{{}TypeTag{}}}/{{{}TypeToken{}}} tier; any 
change to dynamic
dispatch (dynamic callers keep passing the {{Class}} explicitly).

h2. Open questions

 * *Fail-soft vs fail-loud* when K is not statically reifiable (silent degrade 
vs
compile error, à la Scala context bounds).
 * *Resolution precedence:* exact rule for when a {{@ClassTag}} overload 
preempts a
matching no-token overload, and the guidance for choosing a new name instead.
 * *Annotation name:* {{@ClassTag}} (honest about erased fidelity) vs 
{{@Reified}}
(familiar, Kotlin) vs reserving {{@TypeTag}} for a later full tier.
 * *Partial supply:* may a caller pass some tokens explicitly and have the rest
synthesised (e.g. pass K, infer V)? Natural given fixed positions, but a rule 
to state.
 * *Retention / API status:* public annotation vs internal marker; ship 
{{{}@Incubating{}}}.

h2. Related issues
 * GROOVY-11807 — runtime key-checked {{withDefault}} (the quick-win this 
builds on); its
new {{Class}} parameters already carry {{/* @ClassTag GROOVY-12115 */}} markers 
as the
seam for this work.

  was:
h1. Summary

Introduce a parameter annotation (working name {{{}@ClassTag{}}}) that lets the 
static compiler *auto-supply a reified {{Class<K>}} argument* from a method 
receiver's generic type, so callers can omit type tokens that today exist only 
to work around erasure.

Split off from GROOVY-11807. That issue ships the *runtime* half — explicit 
{{{}Class<K>{}}}-token overloads that enforce types at run time. This issue is 
the *compile-time ergonomics* half: making the token optional under static 
compilation.

h2. Background and origin

On the JVM, a method that needs the runtime type of a generic type parameter 
cannot
recover it (erasure), so the API must take an explicit {{Class<K>}} token. 
Groovy
already does this in several places, e.g. {{asChecked}} (since 5.0.0) and the 
new
key-checked {{withDefault}} overloads from GROOVY-11807:
{code:java}
// DefaultGroovyMethods
public static <T>   Collection<T> asChecked(Collection<T> self, Class<T> type)  
          // -> Collections.checkedCollection
public static <K,V> Map<K,V>      asChecked(Map<K,V> self, Class<K> keyType, 
Class<V> valueType) // -> Collections.checkedMap

// GROOVY-11807 (already on the groovy11807 branch), Class params carry a /* 
@ClassTag GROOVY-12115 */ marker
public static <K,V> Map<K,V> withDefault(Map<K,V> self, Class<K> keyType, 
Class<V> valueType, Closure<V> init)
public static <K,V> Map<K,V> withDefault(Map<K,V> self, Class<K> keyType, 
Closure<V> init)
{code}
In every case the {{Class}} argument is *redundant with a type the compiler 
already
knows* (the receiver's type argument). {{@ClassTag}} removes the redundancy 
under
static compilation.
h2. The problem
 * Verbose: {{list.asChecked(String)}} / {{map.asChecked(Number, Foo)}} restate 
the
element/key/value types the static type already declares.
 * Not transparent: the safety-improving variant of {{withDefault}} can't be 
selected
unless the caller manually threads the {{{}Class{}}}.
 * The tokens cannot be inferred at run time — only the compiler, with the 
static type
in hand, can supply them.

h2. Proposal: {{@ClassTag}}

A parameter annotation marking a {{Class<X>}} parameter as 
{*}compiler-supplied{*}. Under
{{@CompileStatic}} / {{@TypeChecked}} the type checker may select an overload 
whose only
extra parameter is a {{@ClassTag Class<X>}} and synthesise {{X.class}} from the
receiver's matching type argument, rewriting the call. Dynamic Groovy ignores 
the
annotation and binds the original overload.
{code:java}
@Retention(RetentionPolicy.CLASS)   // visible to the type checker; SOURCE may 
suffice
@Target(ElementType.PARAMETER)
public @interface ClassTag {
    // Optional override naming the type variable to reify, for when the 
parameter
    // type cannot carry it. NOTE: 'for' is a Java reserved word and cannot be 
an
    // annotation member name, so use e.g. value()/from():  @ClassTag(from = 
"K")
    String from() default "";
}
{code}
h3. Annotation shape and binding
 * *Param-level, not method+index.* An earlier sketch put {{@OverrideCall(1)}} 
on the
method with an index. Moving the marker onto the parameter makes the *position*
implicit (where it sits) and lets the *parameter type* {{Class<K>}} encode 
*which*
generic to reify — strictly more self-describing than a bare index (which never 
said
K-vs-V).
 * *Default binding* reads the type variable from {{{}Class<X>{}}}. The type 
checker sees
{{<X>}} pre-erasure (and the Signature attribute retains it), so this is 
reliable.
 * *Optional override* {{@ClassTag(from="K")}} for edge cases the parameter 
type can't
express — a {{{}Class<?>{}}}/raw token, or a method that declares its own type 
variables
that don't line up with the receiver's. Because it is a String, the STC must 
*reject a
{{from}} that does not name a resolvable type variable* so typos fail at 
compile time.

h3. Multiple tokens and fill order

Some consumers need more than one token, e.g. asChecked(Map, @ClassTag Class<K>,
@ClassTag Class<V>). Rule: each {{@ClassTag}} parameter is synthesised *at its
declared position*; the caller's supplied arguments map to the remaining 
(non-tag)
parameters left-to-right. For {{asChecked()}} that means "fill slots 1 and 2 
with
{{{}K.class{}}}, {{V.class}} in declaration order." (This generalises the 
single-hole case
the {{@OverrideCall(1)}} index handled.)
h3. Overload selection / preemption

For a call that *already matches* a no-token overload (e.g. withDefault\{...}), 
the
{{@ClassTag}} overload must *preempt* it for a transparent upgrade — the 
compiler
prefers injecting a token over an exact arity match. This is a deliberate 
resolution
rule and means unchanged source can change behaviour under static typing.
 * Use preemption *only when the new behaviour is an unambiguous bug-fix* (the 
old
behaviour has no legitimate users).
 * Otherwise *mint a new name* (e.g. {{{}withCheckedDefault{}}}) so the opt-in 
is by name
and nothing existing changes meaning. {{@ClassTag}} still earns its keep there 
by
letting static callers omit the {{{}Class{}}}.

h2. Examples (before / after, under @CompileStatic)
||API||Today||With @ClassTag||
|asChecked (list)|[].asChecked(String)|[].asChecked()|
|asChecked (map)|[:].asChecked(Number, Foo)|[:].asChecked()|
|withDefault (map)|[:].withDefault(Number)\{...}|[:].withDefault\{...} 
(transparently checked)|
{code:groovy}
// asChecked: the Class was mandatory -> @ClassTag makes it omissible (opt-in, 
additive)
Map<Number,String> m = [:].asChecked()          // compiler injects 
Number.class, Foo.class

// withDefault: a no-Class form already exists -> @ClassTag overload preempts 
it (silent upgrade)
Map<Number,?> m = [:].withDefault{ null }        // same source as today
m['x']                                            // now ClassCastException 
(compiler injected Number.class)
{code}
h2. Boundaries and edges
 * *Static-only.* The injection is an STC rule. In dynamic Groovy:
 ** {{withDefault}} keeps binding the *unchecked* overload (no K to inject) — 
same
source, different behaviour by compilation mode.
 ** {{asChecked()}} (no Class) won't resolve at all — there is no zero-arg 
method and
no injection -> {{{}MissingMethodException{}}}. The shorter spelling is 
{*}static-only syntax{*}.
 * *K must be statically known.* {{Map<Number,?>}} works; {{def}} / raw {{Map}} 
/
wildcard-only don't — no token is synthesised, so {{withDefault}} stays lenient 
and
{{asChecked()}} won't compile.
 * *Fail-soft vs fail-loud.* As sketched, when K can't be reified the compiler 
silently
degrades (no token / {{{}Object.class{}}}). Scala instead makes it a *compile 
error* (you
must propagate the evidence). Degrading soft risks a false sense of safety; 
this needs
a decision (see open questions).
 * *Escape hatch.* The explicit {{Class}} form always works — in every mode, 
and when
the type is unknown.
 * *Fidelity is erased.* {{Class<K>}} reifies only the *erased* class:
{{List<String>}} and {{List<Integer>}} are indistinguishable (see TypeTag 
section).

h2. Retrofit candidates (codebase audit)
||Tier||API||Why it fits / note||
|1|{{asChecked(...)}} — 9 overloads, since 5.0.0|Textbook case: explicit 
{{Class}} is purely an erasure-workaround over {{{}Collections.checkedXxx{}}}. 
Also the multi-token (K,V) stress test.|
|1|{{withDefault}} / {{withCheckedDefault}} (GROOVY-11807)|Type-laundering 
insert via the untyped {{{}Map.get(Object){}}}; reified K lets it check. 
Unifies with checked maps: {{withDefault = checkedMap + default}} once K is 
reified.|
|2|{{ListWithDefault}} ({{{}(T) initClosure.call(...){}}})|Default cast to 
element type; weaker — the closure return is already statically checkable.|
|3 (new method)|{{collection.toArray()}} -> {{T[]}}|Reified array creation (the 
Kotlin {{reified}} / Scala {{ClassTag}} array case). New API, not a retrofit of 
{{{}asType{}}}.|
|none|{{{}asType{}}}/{{{}as{}}}, {{{}newInstance(Class){}}}, 
{{{}find{}}}/{{{}grep{}}}-by-Class, {{castToType}}|The {{Class}} is the user's 
{*}intent{*}, not an erasure-workaround — auto-supplying would be wrong.|

Net: the realistic footprint is small and clustered around *checked-view and
default-growing collection APIs*, not a sprawling refactor.
h2. Reification fidelity: ClassTag vs a possible TypeTag tier (weaker)

{{@ClassTag}} supplies an *erased* {{{}Class<K>{}}}. A richer future tier could 
inject a
*full-generic* token (a super-type token {{{}TypeToken<K>{}}}), analogous to 
Scala's
{{TypeTag}} / {{scala.quoted.Type}} versus its {{{}ClassTag{}}}. Kotlin's
{{inline fun <reified T>}} is the JVM precedent for the cheap tier.

*This extension now looks weak given the boundaries above:*
 * The concrete candidates ({{{}asChecked{}}} -> 
{{{}Collections.checkedMap{}}}, {{{}withDefault{}}})
only ever do {{isInstance}} checks, which are erased anyway — the erased class 
suffices.
 * Restricting the source to the *receiver's type argument* and to *static-only*
injection further limits where full-generic fidelity would pay off.

Recommendation: keep {{ClassTag}} (erased) as the design; note 
{{{}TypeTag{}}}/{{{}TypeToken{}}}
as a *possible later tier* only if a real full-generic consumer appears. The 
annotation
*name* could encode the fidelity ({{{}@ClassTag{}}} now, reserving {{@TypeTag}} 
for the full
tier), or favour familiarity ({{{}@Reified{}}}, after Kotlin) — see open 
questions.
h2. Comparison with Scala / Kotlin / Java
||Mechanism||How the type is obtained||Fidelity||Propagation||
|Groovy {{@ClassTag}} (proposed)|compiler synthesises {{Class}} from the 
receiver's type arg|erased class|fail-soft (proposed)|
|Scala {{ClassTag}}|implicit/{{{}using{}}} evidence materialised from the 
static type|erased class|fail-loud (context bound {{{}[T: ClassTag]{}}})|
|Scala {{TypeTag}} / {{quoted.Type}}|implicit/{{{}using{}}} evidence|full 
generic|fail-loud|
|Kotlin {{reified}}|{{inline}} + reify the type parameter|actual type 
(inlined)|enforced by {{inline}}|
|Java baseline|explicit {{Class<T>}} token ({{{}EnumMap{}}}, 
{{{}Collections.checkedMap{}}}, {{{}getAnnotation(Class<T>){}}})|erased 
class|manual|
|Guava/Gson {{TypeToken<T>}}|super-type token|full generic|manual|

In short: {{@ClassTag}} is Scala's {{ClassTag}} idea (compiler-supplied runtime 
type
evidence) delivered via an annotation + STC call-rewrite instead of implicit
parameters; it auto-supplies the *Java {{Class<T>}} token* idiom rather than 
inventing
a new runtime representation.
h2. Proposed scope / deliverables
 # The {{@ClassTag}} annotation (parameter target; CLASS/SOURCE retention; 
optional
{{{}from{}}}/{{{}value{}}} override — *not* {{{}for{}}}, which is reserved).
 # STC support: overload selection that prefers a {{@ClassTag}} overload and 
synthesises
the {{X.class}} argument(s) from the receiver's type arguments; validation of
{{{}from{}}}; well-defined multi-token fill order.
 # First consumers: retrofit {{asChecked}} and the GROOVY-11807 {{withDefault}}
overloads (validating the K,V multi-token case).
 # Decide and document: fail-soft vs fail-loud; preemption-vs-new-name policy.

*Out of scope:* the full-generic {{{}TypeTag{}}}/{{{}TypeToken{}}} tier; any 
change to dynamic
dispatch (dynamic callers keep passing the {{Class}} explicitly).
h2. Open questions
 * *Fail-soft vs fail-loud* when K is not statically reifiable (silent degrade 
vs
compile error, à la Scala context bounds).
 * *Resolution precedence:* exact rule for when a {{@ClassTag}} overload 
preempts a
matching no-token overload, and the guidance for choosing a new name instead.
 * *Annotation name:* {{@ClassTag}} (honest about erased fidelity) vs 
{{@Reified}}
(familiar, Kotlin) vs reserving {{@TypeTag}} for a later full tier.
 * *Partial supply:* may a caller pass some tokens explicitly and have the rest
synthesised (e.g. pass K, infer V)? Natural given fixed positions, but a rule 
to state.
 * *Retention / API status:* public annotation vs internal marker; ship 
{{{}@Incubating{}}}.

h2. Related issues
 * GROOVY-11807 — runtime key-checked {{withDefault}} (the quick-win this 
builds on); its
new {{Class}} parameters already carry {{/* @ClassTag GROOVY-12115 */}} markers 
as the
seam for this work.


> Reify a receiver's type argument as a Class parameter under @CompileStatic 
> (@ClassTag)
> --------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12115
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12115
>             Project: Groovy
>          Issue Type: New Feature
>          Components: Static compilation, Static Type Checker
>            Reporter: Eric Milles
>            Priority: Major
>              Labels: GEP
>
> h1. Summary
> Introduce a parameter annotation (working name {{{}@ClassTag{}}}) that lets 
> the static compiler *auto-supply a reified {{Class<K>}} argument* from a 
> method receiver's generic type, so callers can omit type tokens that today 
> exist only to work around erasure.
> Split off from GROOVY-11807. That issue ships the *runtime* half — explicit 
> {{{}Class<K>{}}}-token overloads that enforce types at run time. This issue 
> is the *compile-time ergonomics* half: making the token optional under static 
> compilation.
> h2. Background and origin
> On the JVM, a method that needs the runtime type of a generic type parameter 
> cannot
> recover it (erasure), so the API must take an explicit {{Class<K>}} token. 
> Groovy
> already does this in several places, e.g. {{asChecked}} (since 5.0.0) and the 
> new
> key-checked {{withDefault}} overloads from GROOVY-11807:
> {code:java}
> // DefaultGroovyMethods
> public static <T>   Collection<T> asChecked(Collection<T> self, Class<T> 
> type)            // -> Collections.checkedCollection
> public static <K,V> Map<K,V>      asChecked(Map<K,V> self, Class<K> keyType, 
> Class<V> valueType) // -> Collections.checkedMap
> // GROOVY-11807 (already on the groovy11807 branch), Class params carry a /* 
> @ClassTag GROOVY-12115 */ marker
> public static <K,V> Map<K,V> withDefault(Map<K,V> self, Class<K> keyType, 
> Class<V> valueType, Closure<V> init)
> public static <K,V> Map<K,V> withDefault(Map<K,V> self, Class<K> keyType, 
> Closure<V> init)
> {code}
> In every case the {{Class}} argument is *redundant with a type the compiler 
> already knows* (the receiver's type argument). {{@ClassTag}} removes the 
> redundancy under static compilation.
> h2. The problem
>  * Verbose: {{list.asChecked(String)}} / {{map.asChecked(Number, Foo)}} 
> restate the
> element/key/value types the static type already declares.
>  * Not transparent: the safety-improving variant of {{withDefault}} can't be 
> selected
> unless the caller manually threads the {{{}Class{}}}.
>  * The tokens cannot be inferred at run time — only the compiler, with the 
> static type
> in hand, can supply them.
> h2. Proposal: {{@ClassTag}}
> A parameter annotation marking a {{Class<X>}} parameter as 
> {*}compiler-supplied{*}. Under
> {{@CompileStatic}} / {{@TypeChecked}} the type checker may select an overload 
> whose only
> extra parameter is a {{@ClassTag Class<X>}} and synthesise {{X.class}} from 
> the
> receiver's matching type argument, rewriting the call. Dynamic Groovy ignores 
> the
> annotation and binds the original overload.
> {code:java}
> @Retention(RetentionPolicy.CLASS)   // visible to the type checker; SOURCE 
> may suffice
> @Target(ElementType.PARAMETER)
> public @interface ClassTag {
>     // Optional override naming the type variable to reify, for when the 
> parameter
>     // type cannot carry it. NOTE: 'for' is a Java reserved word and cannot 
> be an
>     // annotation member name, so use e.g. value()/from():  @ClassTag(from = 
> "K")
>     String from() default "";
> }
> {code}
> h3. Annotation shape and binding
>  * *Param-level, not method+index.* An earlier sketch put 
> {{@OverrideCall(1)}} on the
> method with an index. Moving the marker onto the parameter makes the 
> *position*
> implicit (where it sits) and lets the *parameter type* {{Class<K>}} encode 
> *which*
> generic to reify — strictly more self-describing than a bare index (which 
> never said
> K-vs-V).
>  * *Default binding* reads the type variable from {{{}Class<X>{}}}. The type 
> checker sees
> {{<X>}} pre-erasure (and the Signature attribute retains it), so this is 
> reliable.
> * *Optional override* {{@ClassTag("K")}} (member name {{value()}}, for edge 
> cases the parameter type can't
> express: a {{Class<?>}}/raw token, or a method whose own type variables don't 
> line up with the
> receiver's. The STC *rejects a name that does not resolve to a type variable 
> in scope*, so typos
> fail at compile time — *but only for methods compiled from source in the 
> current unit*. An extension
> method from an already-compiled library (e.g. a Java DGM method) is never 
> visited as source, so a
> typo in its override silently disables injection rather than being reported. 
> Library {{@ClassTag}}
> h3. Multiple tokens and fill order
> Some consumers need more than one token, e.g. asChecked(Map, @ClassTag 
> Class<K>,
> @ClassTag Class<V>). Rule: each {{@ClassTag}} parameter is synthesised *at its
> declared position*; the caller's supplied arguments map to the remaining 
> (non-tag)
> parameters left-to-right. For {{asChecked()}} that means "fill slots 1 and 2 
> with
> {{{}K.class{}}}, {{V.class}} in declaration order." (This generalises the 
> single-hole case
> the {{@OverrideCall(1)}} index handled.)
> h3. Overload selection / preemption
> For a call that *already matches* a no-token overload (e.g. 
> withDefault\{...}), the
> {{@ClassTag}} overload must *preempt* it for a transparent upgrade — the 
> compiler
> prefers injecting a token over an exact arity match. This is a deliberate 
> resolution
> rule and means unchanged source can change behaviour under static typing.
>  * Use preemption *only when the new behaviour is an unambiguous bug-fix* 
> (the old
> behaviour has no legitimate users).
>  * Otherwise *mint a new name* (e.g. {{{}withCheckedDefault{}}}) so the 
> opt-in is by name
> and nothing existing changes meaning. {{@ClassTag}} still earns its keep 
> there by
> letting static callers omit the {{{}Class{}}}.
> h2. Examples (before / after, under @CompileStatic)
> ||API||Today||With @ClassTag||
> |asChecked (list)|[].asChecked(String)|[].asChecked()|
> |asChecked (map)|[:].asChecked(Number, Foo)|[:].asChecked()|
> |withDefault (map)|[:].withDefault(Number)\{...}|[:].withDefault\{...} 
> (transparently checked)|
> {code:groovy}
> // asChecked: the Class was mandatory -> @ClassTag makes it omissible 
> (opt-in, additive)
> Map<Number,String> m = [:].asChecked()          // compiler injects 
> Number.class, Foo.class
> // withDefault: a no-Class form already exists -> @ClassTag overload preempts 
> it (silent upgrade)
> Map<Number,?> m = [:].withDefault{ null }        // same source as today
> m['x']                                            // now ClassCastException 
> (compiler injected Number.class)
> {code}
> h2. Boundaries and edges
>  * *Static-only.* The injection is an STC rule. In dynamic Groovy:
>  ** {{withDefault}} keeps binding the *unchecked* overload (no K to inject) — 
> same
> source, different behaviour by compilation mode.
>  ** {{asChecked()}} (no Class) won't resolve at all — there is no zero-arg 
> method and
> no injection -> {{{}MissingMethodException{}}}. The shorter spelling is 
> {*}static-only syntax{*}.
>  * *K must be statically known.* {{Map<Number,?>}} works; {{def}} / raw 
> {{Map}} /
> wildcard-only don't — no token is synthesised, so {{withDefault}} stays 
> lenient and
> {{asChecked()}} won't compile.
>  * *Fail-soft vs fail-loud.* As sketched, when K can't be reified the 
> compiler silently
> degrades (no token / {{{}Object.class{}}}). Scala instead makes it a *compile 
> error* (you
> must propagate the evidence). Degrading soft risks a false sense of safety; 
> this needs
> a decision (see open questions).
>  * *Escape hatch.* The explicit {{Class}} form always works — in every mode, 
> and when
> the type is unknown.
>  * *Fidelity is erased.* {{Class<K>}} reifies only the *erased* class:
> {{List<String>}} and {{List<Integer>}} are indistinguishable (see TypeTag 
> section).
> * *Override validation is source-only.* {{@ClassTag("name")}} typo-checking 
> runs when the declaring
> method is type-checked from source; compiled extension methods are not 
> re-validated by the consumer.
> h2. Retrofit candidates (codebase audit)
> ||Tier||API||Why it fits / note||
> |1|{{asChecked(...)}} — 9 overloads, since 5.0.0|Textbook case: explicit 
> {{Class}} is purely an erasure-workaround over 
> {{{}Collections.checkedXxx{}}}. Also the multi-token (K,V) stress test.|
> |1|{{withDefault}} / {{withCheckedDefault}} (GROOVY-11807)|Type-laundering 
> insert via the untyped {{{}Map.get(Object){}}}; reified K lets it check. 
> Unifies with checked maps: {{withDefault = checkedMap + default}} once K is 
> reified.|
> |2|{{ListWithDefault}} ({{{}(T) initClosure.call(...){}}})|Default cast to 
> element type; weaker — the closure return is already statically checkable.|
> |3 (new method)|{{collection.toArray()}} -> {{T[]}}|Reified array creation 
> (the Kotlin {{reified}} / Scala {{ClassTag}} array case). New API, not a 
> retrofit of {{{}asType{}}}.|
> |none|{{{}asType{}}}/{{{}as{}}}, {{{}newInstance(Class){}}}, 
> {{{}find{}}}/{{{}grep{}}}-by-Class, {{castToType}}|The {{Class}} is the 
> user's {*}intent{*}, not an erasure-workaround — auto-supplying would be 
> wrong.|
> Net: the realistic footprint is small and clustered around *checked-view and
> default-growing collection APIs*, not a sprawling refactor.
> h2. Reification fidelity: ClassTag vs a possible TypeTag tier (weaker)
> {{@ClassTag}} supplies an *erased* {{{}Class<K>{}}}. A richer future tier 
> could inject a
> *full-generic* token (a super-type token {{{}TypeToken<K>{}}}), analogous to 
> Scala's
> {{TypeTag}} / {{scala.quoted.Type}} versus its {{{}ClassTag{}}}. Kotlin's
> {{inline fun <reified T>}} is the JVM precedent for the cheap tier.
> *This extension now looks weak given the boundaries above:*
>  * The concrete candidates ({{{}asChecked{}}} -> 
> {{{}Collections.checkedMap{}}}, {{{}withDefault{}}})
> only ever do {{isInstance}} checks, which are erased anyway — the erased 
> class suffices.
>  * Restricting the source to the *receiver's type argument* and to 
> *static-only*
> injection further limits where full-generic fidelity would pay off.
> Recommendation: keep {{ClassTag}} (erased) as the design; note 
> {{{}TypeTag{}}}/{{{}TypeToken{}}}
> as a *possible later tier* only if a real full-generic consumer appears. The 
> annotation
> *name* could encode the fidelity ({{{}@ClassTag{}}} now, reserving 
> {{@TypeTag}} for the full
> tier), or favour familiarity ({{{}@Reified{}}}, after Kotlin) — see open 
> questions.
> h2. Comparison with Scala / Kotlin / Java
> ||Mechanism||How the type is obtained||Fidelity||Propagation||
> |Groovy {{@ClassTag}} (proposed)|compiler synthesises {{Class}} from the 
> receiver's type arg|erased class|fail-soft (proposed)|
> |Scala {{ClassTag}}|implicit/{{{}using{}}} evidence materialised from the 
> static type|erased class|fail-loud (context bound {{{}[T: ClassTag]{}}})|
> |Scala {{TypeTag}} / {{quoted.Type}}|implicit/{{{}using{}}} evidence|full 
> generic|fail-loud|
> |Kotlin {{reified}}|{{inline}} + reify the type parameter|actual type 
> (inlined)|enforced by {{inline}}|
> |Java baseline|explicit {{Class<T>}} token ({{{}EnumMap{}}}, 
> {{{}Collections.checkedMap{}}}, {{{}getAnnotation(Class<T>){}}})|erased 
> class|manual|
> |Guava/Gson {{TypeToken<T>}}|super-type token|full generic|manual|
> In short: {{@ClassTag}} is Scala's {{ClassTag}} idea (compiler-supplied 
> runtime type
> evidence) delivered via an annotation + STC call-rewrite instead of implicit
> parameters; it auto-supplies the *Java {{Class<T>}} token* idiom rather than 
> inventing
> a new runtime representation.
> h2. Proposed scope / deliverables
>  # The {{@ClassTag}} annotation (parameter target; CLASS/SOURCE retention; 
> optional
> {{{}from{}}}/{{{}value{}}} override — *not* {{{}for{}}}, which is reserved).
>  # STC support: overload selection that prefers a {{@ClassTag}} overload and 
> synthesises
> the {{X.class}} argument(s) from the receiver's type arguments; validation of
> {{{}from{}}}; well-defined multi-token fill order.
>  # First consumers: retrofit {{asChecked}} and the GROOVY-11807 
> {{withDefault}}
> overloads (validating the K,V multi-token case).
>  # Decide and document: fail-soft vs fail-loud; preemption-vs-new-name policy.
> *Out of scope:* the full-generic {{{}TypeTag{}}}/{{{}TypeToken{}}} tier; any 
> change to dynamic
> dispatch (dynamic callers keep passing the {{Class}} explicitly).
> h2. Open questions
>  * *Fail-soft vs fail-loud* when K is not statically reifiable (silent 
> degrade vs
> compile error, à la Scala context bounds).
>  * *Resolution precedence:* exact rule for when a {{@ClassTag}} overload 
> preempts a
> matching no-token overload, and the guidance for choosing a new name instead.
>  * *Annotation name:* {{@ClassTag}} (honest about erased fidelity) vs 
> {{@Reified}}
> (familiar, Kotlin) vs reserving {{@TypeTag}} for a later full tier.
>  * *Partial supply:* may a caller pass some tokens explicitly and have the 
> rest
> synthesised (e.g. pass K, infer V)? Natural given fixed positions, but a rule 
> to state.
>  * *Retention / API status:* public annotation vs internal marker; ship 
> {{{}@Incubating{}}}.
> h2. Related issues
>  * GROOVY-11807 — runtime key-checked {{withDefault}} (the quick-win this 
> builds on); its
> new {{Class}} parameters already carry {{/* @ClassTag GROOVY-12115 */}} 
> markers as the
> seam for this work.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to