You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@groovy.apache.org by "Daniel Sun (Jira)" <ji...@apache.org> on 2026/10/11 11:03:00 UTC

[jira] [Created] (GROOVY-12446) STC: stop writing UnionTypeClassNode into inferred-return signatures

Daniel Sun created GROOVY-12446:
-----------------------------------

             Summary: STC: stop writing UnionTypeClassNode into inferred-return signatures
                 Key: GROOVY-12446
                 URL: https://issues.apache.org/jira/browse/GROOVY-12446
             Project: Groovy
          Issue Type: Improvement
            Reporter: Daniel Sun


h2. Context

{{UnionTypeClassNode}} is a checker-only {{ClassNode}}. It represents a flow type such as {{x instanceof A}} or {{x instanceof B}}. Its Javadoc says it must not leak outside the type checker.

{{SignatureCodecVersion1}} encodes an inferred return type as a Base64 {{DataOutputStream}} payload for {{@TypeChecked.TypeCheckingInfo}}. The first UTF string in that payload is the simple name of the stored {{ClassNode}} subclass.

GROOVY-12445 locked the unknown-version rule and left union payload layout as separate work. The compiler has not written {{@TypeCheckingInfo}} inferred-return annotations since GROOVY-6401. There is no production reader today. The codec still has to define what that payload may contain.

h2. Problem

{{doEncode}} wrote the tag {{UnionTypeClassNode}}, then the arm count, then each arm. {{doDecode}} rebuilt a {{UnionTypeClassNode}}. That round-trips a checker-only node through a class-file annotation.

A union name is synthetic and is not a binary name. A later reader that treated the decoded node as a loadable type would see a name {{Class.forName}} cannot resolve.

h2. Contract

Version {{1}} stays version {{1}}. Newly encoded signatures never start with the {{UnionTypeClassNode}} tag.

On encode, a union is replaced with a type that has a stable binary name, then that type is encoded:

* the lowest upper bound of the arms, unwrapped until a real class or interface remains
* {{Object}} if there is no such type: empty arms, a {{LowestUpperBoundClassNode}} whose superclass is {{Object}}, or a name that is empty or starts with '<'

A {{LowestUpperBoundClassNode}} supplied directly still round-trips, including its interfaces. The primitive encoding tail (array flag, type descriptor, generics) is unchanged.

On decode, a historical payload whose first tag is {{UnionTypeClassNode}} is still consumed. The arms are decoded, then widened by the same rule. An arm count of {{0}} or less decodes as {{Object}}.

||Input||Encode writes||Decode returns||
|disjoint types, e.g. {{String}} and {{List}}|{{Object}}; first tag is not {{UnionTypeClassNode}}|{{Object}}|
|{{RuntimeException}} and {{Exception}}|{{Exception}}|{{Exception}}|
|two types that share one interface|that interface|that interface|
|historical {{UnionTypeClassNode}} tag|(read path only)|same widening|
|direct {{LowestUpperBoundClassNode}}|LUB tag, as today|same LUB node|

Callers of {{SignatureCodecVersion1.decode}} that expected a union node now receive the shared supertype. Method descriptors are unchanged, so {{japicmp}} does not report this.

h2. Touch points

* {{org.codehaus.groovy.transform.stc.SignatureCodecVersion1}} — encode and historical decode
* {{src/test/groovy/org/codehaus/groovy/transform/stc/SignatureCodecTest.groovy}} — stable-bound and historical-payload tests
* {{COMPATIBILITY.md}} — behavioural-change note

h2. Scope

This issue covers union payload layout only. The unknown-version rule is GROOVY-12445. Rewiring {{addTypeCheckingInfoAnnotation}} onto {{startMethodInference}}, and a new compiler diagnostic, remain separate work.




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