Paul King created GROOVY-12472:
----------------------------------

             Summary: STC: propagate inferred return types of non-overridable 
methods, in the same unit and via @TypeCheckingInfo
                 Key: GROOVY-12472
                 URL: https://issues.apache.org/jira/browse/GROOVY-12472
             Project: Groovy
          Issue Type: Improvement
          Components: Static Type Checker
            Reporter: Paul King


h2. Context

{{@TypeChecked}} infers a return type for every method body it checks, but 
since GROOVY-6401 (2013) that type never leaves the body. 
{{getType(MethodNode)}} still prefers a stored {{INFERRED_RETURN_TYPE}} over 
the declared type, but nothing stores one on a {{MethodNode}} any more, 
{{addTypeCheckingInfoAnnotation}} has no caller, {{silentlyVisitMethodNode}} 
has no caller, and the reader ({{pickInferredTypeFromMethodAnnotation}}) and 
its test ({{PrecompiledClassSTCTest}}) were deleted in d1131470e0. So a call to 
a {{def}} method is typed as {{Object}} at the call site, even within one class:

{code:groovy}
@CompileStatic class A {
    def name() { 'x' }
    String use() { name().toUpperCase() }
    // [Static type checking] - Cannot find matching method 
java.lang.Object#toUpperCase()
}
{code}

GROOVY-6401 removed the propagation because it was unsound for overridable 
methods. The inferred type is a fact about one body; a subclass may override 
the method and return a different subtype of the declared type:

{code:groovy}
@CompileStatic class A {
    Greeter createGreeter() { new HelloGreeter() }          // inferred 
HelloGreeter
    void sayHello() { Greeter g = createGreeter(); g.sayHello() }
}
@CompileStatic class B extends A {
    Greeter createGreeter() { new HelloThereGreeter() }
}
new B().sayHello()  // GroovyCastException: cannot cast HelloThereGreeter to 
HelloGreeter
{code}

Under {{@TypeChecked}} this produced wrong type errors; under 
{{@CompileStatic}} it also emitted a {{checkcast}} to the inferred type, which 
failed at run time.

GROOVY-12446 made the version-1 payload free of checker-only union nodes. 
GROOVY-12445 fixed the unknown-version rule: a {{null}} codec means "no 
inferred signature, use the declared type, do not read the bytes". Both prepare 
the ground for a reader that does not exist yet. This issue adds the missing 
piece.

h2. Proposal

Propagate a method's inferred return type to its call sites only when the 
method cannot be overridden. For those methods the inferred type is a fact 
about the only body that can run, so the GROOVY-6401 counter-example cannot 
arise.

A method is *non-overridable* when any of these holds:
* it is {{private}}
* it is {{static}}
* it is {{final}}
* its declaring class is {{final}} (including records)

Everything else keeps the declared return type. In particular: non-final 
instance methods, interface default methods, non-final enum methods (an enum 
constant body may override them), and trait methods (the helper rewrite changes 
their shape) are *out*.

The rule applies identically to the three places the type can flow:

# *Same compilation unit.* When a call resolves ({{DIRECT_METHOD_CALL_TARGET}}) 
to a non-overridable method in the current source unit that has not been 
visited yet, visit it first ({{silentlyVisitMethodNode}}), store its inferred 
return type on the {{MethodNode}}, and use it at the call site.
# *Writer.* At the end of {{startMethodInference}} for a non-overridable 
method, call {{addTypeCheckingInfoAnnotation}} so the inferred type is recorded 
on the class file as {{@TypeChecked.TypeCheckingInfo(version = 1, inferredType 
= ...)}}. Methods that do not satisfy the rule get no annotation.
# *Reader.* When a call resolves to a precompiled non-overridable method 
carrying {{@TypeCheckingInfo}}, decode it with 
{{SignatureCodecFactory.inferredReturnOrDeclared(version, signature, loader, 
declared)}} and store the result as the method's inferred return type. The 
loader must be the compile classpath loader (as the deleted reader used 
{{getSourceUnit().getClassLoader()}}), not the transform loader, so the encoded 
class names resolve.

h2. Contract

||Method||Same unit||Written to bytecode||Read from bytecode||
|private / static / final / in final class|inferred type|{{@TypeCheckingInfo}} 
added|decoded type|
|any other|declared type|no annotation|declared type, payload unread|
|constructor|n/a|never (existing rule)|n/a|

A reader never fails compilation on the payload. An unknown version falls back 
to the declared type (GROOVY-12445). A corrupt payload of a known version is 
treated the same way: catch the {{GroovyRuntimeException}} from {{decode}}, use 
the declared type, and continue.

The inferred type written and read is the one {{SignatureCodecVersion1}} 
produces after GROOVY-12446: generics preserved, unions widened to a stable 
lowest upper bound or {{Object}}. The type checker treats it exactly as it 
treats a declared return type at that call site; no extra {{checkcast}} beyond 
what the declared/inferred type already implies.

h2. What this gives

* Callers of {{def}}-style methods in a statically compiled library get the 
types the library's own compiler proved, with no API change by the library 
author. Multi-module builds and frameworks consuming Groovy jars benefit most.
* Fewer casts and redundant declared types in consumer code; type errors name 
the real type instead of {{Object}}.
* {{@CompileStatic}} can emit a direct call on the real type rather than going 
through {{Object}} and casting.
* groovy-lsp hover/completion on such calls shows the inferred type rather than 
{{Object}} (it already runs STC).

h2. Touch points

* {{StaticTypeCheckingVisitor}}: a {{isNonOverridable(MethodNode)}} predicate; 
the call-site hook that triggers {{silentlyVisitMethodNode}} / the reader for a 
resolved target; the end of {{startMethodInference}} for the writer. Verify 
which call-site path computes the return type for a given call: 
{{getType(MethodNode)}} already prefers the stored inferred type, but 
{{inferReturnTypeGenerics}} starts from {{method.getReturnType()}} and may need 
to start from the inferred type when one is stored.
* {{SignatureCodecFactory.inferredReturnOrDeclared}} (from GROOVY-12445): gains 
the corrupt-payload fallback, or a wrapper does.
* {{groovy.transform.TypeChecked.TypeCheckingInfo}} Javadoc: currently says the 
annotation "is added by @TypeChecked on methods which have type checking turned 
on"; make it say *non-overridable* methods and name the version rule.
* {{StaticCompilationVisitor}}: confirm the stored inferred type is what the 
static compiler uses for the call's return type and {{checkcast}}.
* Documentation: the "Type checking" section of the language guide gets a 
paragraph on inferred return types and the non-overridable rule.
* {{COMPATIBILITY.md}}: note that {{@TypeCheckingInfo}} is written again (it 
was last written by Groovy 2.1.x) and only for non-overridable methods.

h2. Tests

* Same unit: {{def}} private/static/final methods and methods in a final class 
resolve to their inferred type at call sites, in both {{@TypeChecked}} and 
{{@CompileStatic}}; a non-final instance method still resolves to the declared 
type. The GROOVY-6401 example compiles and runs correctly.
* Writer: a precompiled class carries {{@TypeCheckingInfo}} exactly on its 
non-overridable methods, none on constructors or overridable methods 
({{ConstructorsSTCTest.testTypeCheckingInfoShouldNotBeAddedToConstructor}} 
stays green).
* Reader: reinstate the shape of the deleted {{PrecompiledClassSTCTest}}: a 
precompiled {{static def foo() { new Date() }}} lets {{long t = 
foo().getTime()}} compile; a non-annotated {{bar()}} still fails with {{Cannot 
find matching method java.lang.Object#getTime()}}.
* Reader robustness: an unknown version and a corrupt version-1 payload both 
fall back to the declared type without an error.
* Generics survive the round trip ({{List<String>}}); a union return becomes 
its lowest upper bound (GROOVY-12446).
* Static compilation emits no {{checkcast}} to a type the declared/inferred 
contract does not justify (bytecode check on the GROOVY-6401 example).

h2. Staged delivery

Each stage is independently useful and can land separately:
# Same-unit propagation under the non-overridable rule.
# Writer, under the same rule.
# Reader, with the fallbacks.

h2. Scope

Overridable methods stay on the declared type; a scheme that records a 
covariant contract for overriders is a separate discussion. Traits and 
interface default methods are excluded. Closure return inference is unchanged.



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

Reply via email to