Support statically compiled specifications - #2397
Conversation
record() and realizeNas() wrap sub-expressions of rewritten conditions but returned Object, so member access, comparisons, and indexing on a recorded value failed static type checking. Preserving the argument type via a type parameter (same erasure, binary compatible) lets such conditions in @CompileStatic specs compile.
Earlier rewrite steps synthesize method calls inside conditions, e.g. old() becomes oldImpl() and the collection conditions =~ and ==~ become matchCollections*() calls. Wrapping such a synthesized name in record() turns the call into a dynamic method name call, which fails static compilation with "Target method for method call expression hasn't been set". Names without a plausible source position now only allocate their value slot via recordNa(), keeping slot numbering and rendering intact. This makes old() usable in @CompileStatic specs.
Data table cells referencing previous columns are wrapped in a closure that extracts the referenced row values into untyped locals, so such cells failed static type checking even when the feature method declared typed parameters. The extractor locals now use the declared parameter type, coercing the row value like the data processor already does. Without a declared type the generated code is unchanged.
Generated calls like getSpecificationContext() shared the VariableExpression.THIS_EXPRESSION singleton between all use sites. The static type checker stores inferred types as per-node metadata, so a node used both inside and outside of closures accumulated conflicting types, producing contradictory checkcasts and a ClassCastException at runtime. This broke interaction blocks and mock init closures in @CompileStatic specs. Each generated call site now gets its own node.
Conditions record the method name of each call for value tracking, which turns the call into a dynamic method name call. Static type checking rejects those, so any condition containing a method call failed to compile in @CompileStatic or @TypeChecked specs. For methods subject to static type checking, detected via the CompileStatic and TypeChecked annotations on the method or its class, constant method names now stay in place and only their value slot is allocated, and the rewritten call keeps the original implicit-this flag so qualified calls are not dispatched to local closure variables of the same name. Dynamic code is rewritten exactly as before, preserving its dispatch semantics, e.g. calling closures in data variables with method syntax and stubbing getProperty() on Java mocks.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Adds a primer section describing how to use @CompileStatic and @TypeChecked with specs, the typing rules that apply, and the remaining limitations, plus a release notes entry.
Mirrors representative cases from ConditionRendering with typed locals in a @CompileStatic spec, verifying that failed conditions render the same as in dynamic code. One deliberate difference is pinned and documented: the target class of a static method call is not rendered, because statically compiled code never evaluates the receiver of a static method call, so its value is never recorded. Also imports CompileStatic in CompileStaticSupport instead of using the fully qualified annotation in the embedded sources.
1c633eb to
e1bb1c3
Compare
🚨 TestLens detected 1 failed test 🚨Here is what you can do:
Test SummaryVerify Branches and PRs / Build and Verify (2.5, 8, windows-latest) > :spock-specs:test
🏷️ Commit: e1bb1c3 Test FailuresAsyncConditionsSpec > multiple passing evals (:spock-specs:test in Verify Branches and PRs / Build and Verify (2.5, 8, windows-latest))Muted TestsSelect tests to mute in this pull request:
Reuse successful test results:
Click the checkbox to trigger a rerun:
Learn more about TestLens at testlens.app. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2397 +/- ##
============================================
+ Coverage 82.29% 82.31% +0.01%
- Complexity 4882 4902 +20
============================================
Files 474 474
Lines 15258 15292 +34
Branches 1961 1973 +12
============================================
+ Hits 12557 12588 +31
Misses 2002 2002
- Partials 699 702 +3
🚀 New features to boost your workflow:
|
Summary
Specs annotated with
@CompileStaticor@TypeChecked(on the class or on individual feature methods) now compile and run. Conditions containing method calls, property access, comparisons, and indexing,old(), interactions includinginteraction {}blocks and mock init closures, and data tables referencing previous columns all work, with condition rendering fully intact.Code generated for dynamic specs is byte-identical to master except for two snapshot-pinned changes (synthesized method names are no longer record-wrapped), so existing dispatch semantics are preserved, including the behaviors pinned by #396 and #1076.
Changes, one commit each
ValueRecorder.record()/realizeNas()are now generic (<T> T, same erasure), so recorded sub-expressions keep their static type.oldImpl,matchCollections*) are no longer wrapped inrecord(), which turned the call into a dynamic method name call and brokeold()under static compilation. Their value slot is still allocated, keeping rendering intact.VariableExpression.THIS_EXPRESSIONsingleton between call sites. The static type checker stores inferred types as per-node metadata, so the shared node accumulated conflicting types and produced contradictory checkcasts, causing aClassCastExceptionininteraction {}blocks and mock init closures.@CompileDynamic/TypeCheckingMode.SKIPopting out), constant method names in conditions stay in place and the rewritten call keeps its original implicit-this flag. Dynamic code is rewritten exactly as before.CompileStaticSupport(compile, run, and exact failure rendering per fix) andCompileStaticConditionRendering(rendering cases mirrored fromConditionRendering, verifying identical output).Known limitations (documented)
class java.lang.Mathline) is not rendered, because statically compiled code never evaluates the receiver of a static method call, so there is no value to record.Validation
Full
:spock-specs:teston all Groovy variants (2.5, 3.0, 4.0, 5.0) per commit, plus an all-modules test sweep (4028 tests) and the asciidoctor build.🤖 Prepared with Claude Code