blackdrag commented on PR #2770:
URL: https://github.com/apache/groovy/pull/2770#issuecomment-5222268882
I agree with most what you both said, but since this started to be a tiring
piece of work (5h of finding methods initial commits, compare with tags and so
on) I wanted first to get some feedback before proceeding.
> ## Annotation shape and docs
> ### Retention
[...}
> With `SOURCE` retention the marker never makes it into the published jars,
so japicmp (and anything else that only sees class files) can’t use it. That
might be exactly what you want if this is mainly for people reading the source
— if so, saying that somewhere would help.
>
> If we later want a small check like “don’t drop a compiler-referenced
member by accident,” `CLASS` or `RUNTIME` would make that easier (closer to how
`@Incubating` works).
>
> It might also be worth a short note in `COMPATIBILITY.md` so `@GroovyABI`
sits next to Public API / `@Internal` / the binary-compat check, rather than
living only in the annotation Javadoc.
agreed will do. The primary purpose was information for the developer, not
for a tool, that is why I did not think of that.
> ### `since`
>
> A few small inconsistencies stood out:
>
> * Five places on `InvokerHelper` use `since="?"`.
yeah, I have to investigate further on those.
> * Version strings mix `1.0` / `1.0.0` and `2.0` / `2.0.0`.
the longer version should be always used, will change that.
> * Spacing varies: `since = "1.6.0"` vs `since="4.0.0"`.
ah well... ok
> * The empty default (`default ""`) leaves it unclear when `since` is
required.
I had to first to work a bit with it and then let it settle. I think now
since should be mandatory.
[...]
> ### Policy vs what’s annotated
>
> The Javadoc says `groovy.*` isn’t annotated because it’s already public
API, but we do annotate members on `Closure`, `Reference`, and `Awaitable`.
yeah, those have to go.
[...]
> Would it make sense to default to the type, and only mark members when
`since` differs or when only part of the type is ABI? That would cut a lot of
noise without losing the refinement pattern you already use on
`ScriptBytecodeAdapter`. `Maps` is the clearest case — the per-overload marks
don’t really say more than one class-level annotation would.
if it is every public method in that file, then we can mark it on type level
using the earliest version and if there are later introduction we can use the
later version directly on those. But what if we have mixed in public methods
that are not used from bytecode? An internal marked class with bytecode ABI
markers on some of the public methods is still allowed to change those public
methods.
> ## How complete does the inventory need to be?
>
> A partial list is still useful as a start. The risk is that people read
“no annotation” as “safe to change.” A few busy places show the gap:
> Surface Role Currently
> `ScriptBytecodeAdapter` main classic bridge class-level — good
> `DefaultTypeTransformation` box/unbox **and** casts (`castToType`,
etc.) box/unbox only
> `NumberMath` arithmetic / compare as well as bitwise mostly
bitwise / shift / mod
> `InvokerHelper` property / invoke helpers a subset; several with
`?`
> `CallSiteArray` classic linkage (ctor + `array` / `owner`)
constructor only
I think castToType for example shows the problem. I did not mark it, because
I did not verify its usage. I did not work through InvocationWriter for
example. And while that uses castToType, it is from SBC. I did another search
and could not find a usage by the compiler. So maybe it was in the past?
Tranditionally there is SBC.castToType that uses that now. So most likely the
method was never used directly by compiler produced bytecode. But can I be
sure? No. It means digging deep into the commit history and trying to find the
usage on an checkout of an old version most likely. The probability is high
though it was never used from the compiler directly. Thus the annotations go on
the box/unbox methods only, none on the class level.
[...]
> ### Looking a bit further ahead
>
> Keeping hundreds of hand-placed marks up to date will be hard. A lot of
this surface is already “registered” in the compiler via `MethodCaller` /
multi-adapters, indy bootstraps, and transform-generated calls. Even a simple
test that checks known registration points against `@GroovyABI` would help
later. No need to build that now — just something to keep in mind so the first
cut doesn’t fight that idea.
A once placed marker does not need an update. The
MethodCaller/multi-adapters help, but what if we decide to use a different
method in the future? The old entry point is potentially lost and forgotten,
then maybe removed and we broke older programs. Extracting StringDGM for
example broke older programs because some of the string methods in there where
used directly. That did happen in 3.0.0, and it took till 3.0.24 I think before
we fixed it. transform generated did proof to be more difficult actually. Only
following the callX usages did help, but sometimes the call is abstracted
through several levels and it gets difficult fast. indy bootstraps are another
good example. We broke that before. But it is only the bootstrap method we have
to consider here, not methods we produce method handles for. Unless the handle
is stored in the class bytecode, then we have to deal with handles as well. So
far the bootstrap method works like an isolation layer here. Adding new
bootstrap methods l
ike some of the Groovy 6 methods are a different story. There are also for
example for methods used by runtime generated classes. These are also not to be
considered.
[...]
> Right now a few DGM / `StringGroovyMethods` / `NumberMath` bits sit next
to the core bridges without saying why those and not others. One sentence of
guidance would go a long way so we don’t slowly mark half the GDK.
I tried to give those in the javadoc of the annotation, maybe needs to be
improved.
> ## A few edits that aren’t about `@GroovyABI`
>
> These are easy to miss in the same commit:
> File Change
> `BinaryExpressionHelper` drops an unused assignment after boxing
> `ClosureWriter` unused `callX` import
> `StaticTypesCallSiteWriter` whitespace on `"or"`, and **drops `case
"implies":`**
>
> The first two are harmless. The `implies` case changes STC dispatch for
number×number `implies` (it no longer takes the optimized path). Even if that
path is rare, it might be clearer in its own change — or with a short note and
a test if we keep it here. Splitting pure inventory from compiler behaviour
also makes history and reverts easier.
The implies drop should maybe be extracted. But fact is we do not compile
1==>1 to 1.implies(1). We compile that by converting the numbers to boolean (in
a quite inefficient way btw.) and then handle it as boolean intrinsic. The path
I removed is never visited. In other words the optimization was never in
BinaryExpressionHelper. If I where wrong, there should have been failing tests.
Though I have the feeling we lack tests with implies and static compilation and
with anything but booleans as well.
> ## Optional next steps
>
> Nothing urgent — more a suggested order if we iterate:
>
> 1. Spell out the contract briefly in `COMPATIBILITY.md` (what it
means, class vs member, version format, how it relates to public API).
> 2. Tighten the annotation (retention, `since` rules, Javadoc,
`groovy.*` wording).
> 3. Prefer type-level marks when everything shares the same `since`.
> 4. Clean the inventory: resolve/drop `?`, fix private members, either
finish the core bridges or document an explicit v1 subset.
> 5. Split (or call out) the unrelated compiler cleanups and the
`implies` change.
> 6. Later: a lightweight check against known compiler registration
points.
agreed I guess. Since the idea was well received so far I also have to add a
JIRA issue.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]