From 947fe90f62b6ac324a9e975da8441288ad0a9b0c Mon Sep 17 00:00:00 2001 From: jun0 Date: Fri, 25 Sep 2026 08:36:49 +0900 Subject: [PATCH] =?UTF-8?q?[rustjava-checker-offspring-and-orphan-artefact?= =?UTF-8?q?s-cleanup]=20chore:=2009-18=20=EC=B1=84=ED=83=9D=EB=B6=84?= =?UTF-8?q?=EC=9D=98=20=ED=98=B8=EC=B6=9C=EC=9E=90=20=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EC=82=B0=EC=B6=9C=EB=AC=BC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/ldc-tag-survey-targets/ 삭제 + survey 스크립트의 해당 블록을 worklog 포인터 3줄로 - docs/test-data-target-policy.md 삭제 — 결정·재개 조건을 class-file-versions.txt 머리로 접음 - audit-fixture-single-defect.py 유지 — AGENTS.md 픽스처 재생성 절차에 수동 경로 1줄 등재 - check-merge-dropped-symbols.py docstring 84→38줄(검사 내용·trailer·종료 코드) · 측정 이력은 worklog 로 이동 (로직 무접촉 — 두 범위에서 전/후 출력 바이트 동일) Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 1 + docs/test-data-target-policy.md | 125 ------------------ .../2026-09-25-checker-offspring-cleanup.md | 95 +++++++++++++ scripts/check-merge-dropped-symbols.py | 92 ++++--------- scripts/ldc-tag-survey-targets/Targets.kt | 44 ------ scripts/ldc-tag-survey-targets/Targets.scala | 41 ------ scripts/survey-ldc-constant-tags.py | 39 +----- test-data/class-file-versions.txt | 10 ++ 8 files changed, 133 insertions(+), 314 deletions(-) delete mode 100644 docs/test-data-target-policy.md create mode 100644 docs/worklog/2026-09-25-checker-offspring-cleanup.md delete mode 100644 scripts/ldc-tag-survey-targets/Targets.kt delete mode 100644 scripts/ldc-tag-survey-targets/Targets.scala diff --git a/AGENTS.md b/AGENTS.md index 80b192b5..8428dfbe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -163,6 +163,7 @@ done | /usr/bin/grep -c . - Test JVM and interpreter semantics, including class initialization, bytecode execution, and monitor behavior, with compiled Java fixtures under `test-data/src` and expected output under `test-data`, executed by `tests/test_class.rs`. - Do not place JVM core behavior tests in the `rustjava-runtime` standard library test tree. - **Adding or recompiling a fixture means editing `test-data/class-file-versions.txt` in the same commit.** Every committed `.class` has its version frozen there, and a fixture that is not listed fails the pin — deliberately, since a pin that only covers what it already knows about stops covering anything added later. Run `python3 test-data/src/record-class-file-versions.py` and commit the row next to the fixture; a moved version means look at why before re-recording. +- **Regenerating a hand-assembled fixture** (`test-data/src/{cp,indy,ldc}/make_*_fixtures.py`) — also run `python3 test-data/src/audit-fixture-single-defect.py` (no JDK, under a second; rc=0 = each fixture is broken in exactly its named way). It is manual like `test-data/src/verify-javac-fixtures.sh`: CI does not run it. ## Compatibility Sources - Implement Java compatibility from public specifications, Javadocs, and observable behavior tests. Do not consult or reproduce OpenJDK or other Java runtime implementation source code; keep the implementation independent to avoid licensing and provenance concerns. diff --git a/docs/test-data-target-policy.md b/docs/test-data-target-policy.md deleted file mode 100644 index 670aa56d..00000000 --- a/docs/test-data-target-policy.md +++ /dev/null @@ -1,125 +0,0 @@ -# Should the root fixtures be recompiled onto one target? - -**Decision: no. `test-data/` keeps its five class file versions.** -The spread is not neglect — for a JVM implementation it is coverage, and this document is the -measurement that establishes which parts of it are coverage and which parts are merely history. - -Adopted proposal: `2026-09-17-test-data-version-freeze#p0`. Measured 2026-09-18 against -`origin/main` @ `8c7b473f` with javac 26.0.2.1. - -## What is actually there - -Read from the fixtures themselves (`od -An -tu1 -j6 -N2` on bytes 6-7), not from the freeze file: - -| major | Java | root fixtures | -|---|---|---| -| 52 | 8 | 40 | -| 65 | 21 | 62 | -| 66 | 22 | 8 | -| 68 | 24 | 1 | -| 70 | 26 | 3 | -| | | **114** | - -## Where the version *is* the thing under test - -Two lowering axes change with the target, and in both cases the older shape is the one a JVM has -to keep supporting. Both were measured by recompiling the committed source at `--release 21` and -comparing, not by reading the spec. - -**1. String concatenation.** `javac` lowers `+` to `StringBuilder` through Java 8 and to -`invokedynamic StringConcatFactory` from 9 on. - -- Three major-52 fixtures carry `StringBuilder`: `FormatterIntegration`, - `FormatterIntegration$FailingAppendable`, `NullSpecGuards`. -- Recompiled at 21 all three gain `BootstrapMethods`. Two of them — `FormatterIntegration` and - `NullSpecGuards` — lose `StringBuilder` entirely. The third does not: `FailingAppendable` - declares `private final StringBuilder output` at `test-data/src/FormatterIntegration.java:36`, - so that use is the program asking for the class by name rather than the compiler reaching for it, - and no target changes it. What moves is the concatenation, not every mention. -- `StringConcat.class` (major 65) is the **only** root fixture that has `invokedynamic` at all. - -So the root tree holds exactly one fixture per lowering strategy. Retargeting the 52 cohort would -delete the pre-indy side of that pair and leave two fixtures asserting the same path. - -**2. Nestmate access.** Before JEP 181 (Java 11), a nested class reaching a private member went -through a synthetic `access$NNN` bridge; from 11 on, `NestHost`/`NestMembers` permit it directly. - -| fixture | committed (52) | recompiled (21) | -|---|---|---| -| `ThreadInterruption` | `access$` ×10, no Nest attrs | `access$` ×0, `NestMembers` | -| `MonitorSemantics` | `access$` ×4, no Nest attrs | `access$` ×0, `NestMembers` | - -This repo added `NestHost`/`NestMembers` handling only recently, which makes both sides live test -surface rather than legacy. - -**`NativeMethod` is the same second axis wearing a different face.** It changes opcode counts at -21 (`invokespecial` 3→2, `invokevirtual` 1→2) with no nest attributes on either side, which this -document first recorded as unexplained. The gate-2 review isolated it: the whole `javap -c -p` diff -is one line, `invokespecial` becoming `invokevirtual` on a call to `private native void missing()` -made from `static main` in the same class. After JEP 181 a member of a nest reaches its own private -methods directly, so there is no `access$` bridge to delete here — the class is its own nest host — -and only the opcode moves. So all three of the differing fixtures are accounted for by the two axes, -with nothing left over. - -**This coverage exists nowhere else.** Across the 64 generator-built fixtures in `test-data/cp`, -`indy`, `ldc` and `attr`, the counts of `StringBuilder` and of `access$` bridges are both **zero**. -The root major-52 cohort is the only place either path is exercised. - -## Where the version is irrelevant - -Of the 40 major-52 fixtures, 20 have a rebuildable source and carry neither `StringBuilder` nor -`BootstrapMethods`. Recompiling those at `--release 21` and comparing the full opcode sequence: - -- **16 produce an identical instruction sequence.** For these the target is genuinely arbitrary — - `ArrayEdgeCases`, `BooleanTest`, `CheckCast` and the rest lower the same way at 8 and at 21. -- 3 differ, and they are the two nestmate cases plus `NativeMethod` above. -- 1 could not be rebuilt alone (it references a sibling; see `verify-javac-fixtures.sh`). - -So the honest split is: **of the 20 examined the target matters for 3, one cannot be rebuilt -alone, and for the remaining 16 it buys nothing** — which is also why uniformity is not worth its -cost. The three are the two nestmate cases and `NativeMethod`. - -A first draft of this document said "5 of the 20". That was wrong by its own arithmetic — 5 + 16 is -21 — and wrong in the direction that flatters the conclusion, because it reached the 5 by adding -the two string-concatenation fixtures, which the selection had *already excluded* for carrying -`StringBuilder`. The conclusion does not rest on this ratio (it rests on the zero-coverage -measurement below), but a policy document's headline number should be countable from its own -bullets, so it is stated here with the set it counts: the 20 are the major-52 fixtures that are -top-level and free of `StringBuilder`. - -## Why not unify anyway - -The proposal's stated benefit is that a fixture's shapes would be "predictable from one number". -The measurement inverts that argument. Today major 52 reliably *means* "pre-indy, pre-nestmate -shapes", and that is a signal a reader can use. Flattening everything to 65 does not make the -number more informative; it removes the distinction the number currently carries, and it does so -by deleting the only coverage of two paths the runtime still has to implement. - -Against that, the cost is real: recompiling changes bytes, every root fixture's version is pinned -in `test-data/class-file-versions.txt` and asserted by `tests/test_fixture_pins.rs`, and 66 root -fixtures have their stdout compared against a committed `.txt` by `tests/test_class.rs`. A bulk -retarget is therefore a per-fixture judgement, and for 16 of 20 the judgement returns "no change". - -## What this does not decide - -Nothing here says which target a *new* fixture should be built for. This round looked only at the -fixtures that already exist and decided not to move them; the rule for new ones is a separate -question and was deliberately left open. Until it is answered, a new fixture's target is whatever -its author chose, and the freeze records that rather than judging it. - -## Reopen this decision if - -- **The runtime drops support for the old shapes.** If RustJava stops accepting major ≤ 52, or - stops needing the `StringBuilder` and `access$` paths, the 52 cohort stops being coverage. -- **That coverage turns up somewhere else.** The claim above is a measurement with a date: zero - `StringBuilder` and zero `access$` in the 64 generator fixtures. If a later round adds fixtures - that cover those paths deliberately, the root cohort becomes redundant and this should be re-run. -- **The freeze goes away.** This decision assumes `class-file-versions.txt` plus - `test_fixture_pins.rs` keep the versions from moving on their own. Without that guarantee the - question is no longer "should they be uniform" but "why did they change". -- **A third version axis turns up.** `NativeMethod` was the open candidate and is now closed as - the nestmate axis again, so the current set is two. A genuinely new one would widen the "version - is the subject" set and strengthen, rather than weaken, this decision. - -Re-measure rather than re-argue. The commands are in the round's worklog: -`docs/worklog/2026-09-18-root-fixture-target-decision.md`. diff --git a/docs/worklog/2026-09-25-checker-offspring-cleanup.md b/docs/worklog/2026-09-25-checker-offspring-cleanup.md new file mode 100644 index 00000000..fb76a871 --- /dev/null +++ b/docs/worklog/2026-09-25-checker-offspring-cleanup.md @@ -0,0 +1,95 @@ +## [2026-09-25] 09-18 채택분이 남긴 호출자 없는 산출물 정리 (rustjava-checker-offspring-and-orphan-artefacts-cleanup) +- 무엇을: `scripts/ldc-tag-survey-targets/`(Kotlin·Scala 85줄)와 그 조사 블록을 지웠다. `docs/test-data-target-policy.md` 의 결정 논거는 `test-data/class-file-versions.txt` 머리로 접고 문서를 지웠다. `audit-fixture-single-defect.py` 를 `AGENTS.md` 의 픽스처 재생성 절차에 1줄로 올렸다. `check-merge-dropped-symbols.py` docstring 을 검사 내용·trailer 규칙·종료 코드만 남겨 84줄에서 38줄로 줄였고, 측정 이력은 아래로 옮겼다. +- 왜: 셋 다 CI·테스트·DoD 어디에서도 부르지 않았다. 정책 문서는 들어오는 참조가 0이었고 같은 논거가 두 곳에 있었다. 검사기 docstring 은 측정 서술이라 09-18 반려 4회 중 3회가 그 수치 정정이었다. +- 사용자 영향: 없음. 검사 로직·CI 잡은 건드리지 않았다. 감사 스크립트는 JDK 없이 0.8초에 rc=0(검사 18 · 결함 없음 5)이라 지우지 않고 수동 경로에 올렸다. + +열린 카드 2장(`2026-09-18-root-fixture-target-decision#p0`·`#p1`)의 `target` 이 지운 `docs/test-data-target-policy.md` 를 가리킨다. 그 결정·재개 조건은 이제 `test-data/class-file-versions.txt` 머리에 있다. 카드 처분은 이 회차 범위가 아니라 그대로 둔다. + +### `check-merge-dropped-symbols.py` 에서 옮긴 측정 이력 (원문 그대로) + +```text +Report definitions that were in the branch a merge pulled from, and are not in its result. + +What this answers: *did a conflict resolution silently drop one side's work*. That failure is +invisible to everything else we have, which is why it exists. Measured 2026-09-18 by reconstructing +the loss on `origin/main` and running every axis against it: the generator ran (rc=0), the +single-defect auditor passed (23 checks, rc=0), `test_class_format` passed (21), `test_fixture_pins` +passed (3), `check-dod-ci-parity` passed, `git status` was empty and there was no conflict marker. +Nothing went red. The only signal was counting names by hand, which is what this does instead. + +The real incident: two merges on one PR branch (e53b2142, 514d5b08) each dropped four definitions +that came from the branch being merged in -- `Pool.fieldref`, `MAKECONCAT_DESCRIPTOR`, +`make_concat_call_site` and the `LINKED` table. The suite stayed green because the fixtures those +build are committed bytes, so when the generator lost the code the expected output did not move. + +Why it is silent, precisely: the loss is invisible exactly when the dropped names are *consistent +with each other*. Dropping a slice that something else still calls raises AttributeError and gets +noticed; dropping a whole feature -- which is what taking one side of a conflict does -- leaves a +file that parses, runs and agrees with every committed artefact. Both were measured. + +Scope: a definition is flagged when it exists in the merge's second parent and not in the merge +result, looked for in every file that either the merged-in branch or the merge itself changed. +That second half costs something. Measured by running both filters over one window of 83 merges +(38b0df38..8c7b473f, the last 200 commits of origin/main at the time): the narrow filter reports +8 merges / 19 definitions, this one reports 10 / 26. The seven extra definitions come from two +merges -- 514d5b08 (six) and 37ea5a13 (one) -- and they are not all one kind. Four are the second +of the two real incidents, restored later and present in the tree today. The other three are the +class described next. On time: the measurement noise is larger than the difference between the two +versions -- three independent runs of the narrow filter over this window span 229s to 585s, a factor +of 2.6, while the gap between the versions in one back-to-back run was 70s over 83 merges, about +0.8s per merge. So there is no significant increase to report, which is a weaker claim than "no +slower" and the one the numbers actually support. A pull request carries 0-2 internal merges. The +price of widening is reading, not waiting. + +A branch that deletes or renames a definition main still has goes red on its next base pull, and an +ordinary refactor then has to carry a trailer to say so. 37ea5a13 is exactly that: it pulled +origin/main into a branch that had already generalised `at_most_one_bootstrap_methods_attribute` +into `at_most_one_of_each_single_class_attribute`, and the pull reported the old name as dropped. +Three of the seven extra definitions are this class, so it is a class and not a corner. "CI only +looks at origin/main..HEAD" is not a reason to discount it -- that range is precisely where this +lands, on the next base pull of an open branch. + +That deliberately also flags *our* intentional deletions, because from the outside the two +look identical -- which is the whole difficulty. Saying which is which is a judgement, so it is +recorded as one, on the merge commit: + + Dropped-from-theirs: method fieldref -- superseded by the new pool builder, see + +One trailer per name, a reason after `--`, and both are required. THE NAME MUST BE COPIED FROM THIS +CHECK'S OWN OUTPUT, character for character -- it is matched literally, so `method fieldref` works +and `Pool.fieldref` does not, even though the second reads better. Run the check, copy the name it +prints after the colon, paste it. (An earlier version of this docstring used the prettier form as its +only worked example, which meant anyone who followed it got no exemption and lost a round.) + +The list lives in the merge commit message rather than in a file on purpose: a file accumulates +entries that outlive the merge they excused and quietly turns the check off, while a trailer can +only ever excuse the one commit it is written on. + +Usage: + check-merge-dropped-symbols.py [] # default: origin/main..HEAD + +Exit: 0 nothing dropped, or everything dropped is accounted for by a trailer + 1 something was dropped and not accounted for + 2 could not measure -- a git call failed, or the clone is shallow + +★ 2 is not a softer 0. Every git call here raises rather than returning nothing, because the one +failure this check must never produce is a green one: in a shallow clone the second parent's objects +are absent, every read of it comes back empty, and the old code printed `✓ (0 file(s) examined)` and +exited 0. Measured on this repository at depth 10: merges that examine 20 files in a full clone +examined 0 and the whole run was green. A check for a silent loss had a silent pass in it. + +`-s ours` merges -- "record these upstream cuts as ancestors" -- drop the whole other side on +purpose, and one trailer per name would mean hundreds. They use the wildcard form instead: + + Dropped-from-theirs: * -- recorded as a merged ancestor only, our tree is authoritative + +There is deliberately no structural exemption for them. The obvious one, "skip a merge whose tree +equals its first parent's", was written and then removed: it is true of `-s ours` *and* of a +resolution that took our side wholesale, and the second of those is the exact failure this exists +to catch. Measured -- with that exemption in place, a synthetic merge that discarded the other +side's new function passed. So the wholesale case has to be declared rather than inferred. + +A squash landing has one parent, so there is nothing for this to compare and it reports nothing. +That is not a gap: the merges this catches are the base-pulls *inside* a branch, and those are +checked while the pull request is open, whichever way the branch is eventually landed. +``` diff --git a/scripts/check-merge-dropped-symbols.py b/scripts/check-merge-dropped-symbols.py index e0d81acf..91d05e3e 100755 --- a/scripts/check-merge-dropped-symbols.py +++ b/scripts/check-merge-dropped-symbols.py @@ -1,60 +1,30 @@ #!/usr/bin/env python3 """Report definitions that were in the branch a merge pulled from, and are not in its result. -What this answers: *did a conflict resolution silently drop one side's work*. That failure is -invisible to everything else we have, which is why it exists. Measured 2026-09-18 by reconstructing -the loss on `origin/main` and running every axis against it: the generator ran (rc=0), the -single-defect auditor passed (23 checks, rc=0), `test_class_format` passed (21), `test_fixture_pins` -passed (3), `check-dod-ci-parity` passed, `git status` was empty and there was no conflict marker. -Nothing went red. The only signal was counting names by hand, which is what this does instead. - -The real incident: two merges on one PR branch (e53b2142, 514d5b08) each dropped four definitions -that came from the branch being merged in -- `Pool.fieldref`, `MAKECONCAT_DESCRIPTOR`, -`make_concat_call_site` and the `LINKED` table. The suite stayed green because the fixtures those -build are committed bytes, so when the generator lost the code the expected output did not move. - -Why it is silent, precisely: the loss is invisible exactly when the dropped names are *consistent -with each other*. Dropping a slice that something else still calls raises AttributeError and gets -noticed; dropping a whole feature -- which is what taking one side of a conflict does -- leaves a -file that parses, runs and agrees with every committed artefact. Both were measured. - -Scope: a definition is flagged when it exists in the merge's second parent and not in the merge -result, looked for in every file that either the merged-in branch or the merge itself changed. -That second half costs something. Measured by running both filters over one window of 83 merges -(38b0df38..8c7b473f, the last 200 commits of origin/main at the time): the narrow filter reports -8 merges / 19 definitions, this one reports 10 / 26. The seven extra definitions come from two -merges -- 514d5b08 (six) and 37ea5a13 (one) -- and they are not all one kind. Four are the second -of the two real incidents, restored later and present in the tree today. The other three are the -class described next. On time: the measurement noise is larger than the difference between the two -versions -- three independent runs of the narrow filter over this window span 229s to 585s, a factor -of 2.6, while the gap between the versions in one back-to-back run was 70s over 83 merges, about -0.8s per merge. So there is no significant increase to report, which is a weaker claim than "no -slower" and the one the numbers actually support. A pull request carries 0-2 internal merges. The -price of widening is reading, not waiting. - -A branch that deletes or renames a definition main still has goes red on its next base pull, and an -ordinary refactor then has to carry a trailer to say so. 37ea5a13 is exactly that: it pulled -origin/main into a branch that had already generalised `at_most_one_bootstrap_methods_attribute` -into `at_most_one_of_each_single_class_attribute`, and the pull reported the old name as dropped. -Three of the seven extra definitions are this class, so it is a class and not a corner. "CI only -looks at origin/main..HEAD" is not a reason to discount it -- that range is precisely where this -lands, on the next base pull of an open branch. - -That deliberately also flags *our* intentional deletions, because from the outside the two -look identical -- which is the whole difficulty. Saying which is which is a judgement, so it is -recorded as one, on the merge commit: +What it checks: whether a conflict resolution silently dropped one side's work. A definition is +flagged when it exists in the merge's second parent and not in the merge result, looked for in every +file that either the merged-in branch or the merge itself changed. Nothing else catches this: a +dropped *whole feature* still parses, runs and matches every committed artefact. + +The wider scope means a branch that deletes or renames a definition main still has goes red on its +next base pull, and an intentional deletion looks exactly like a lost one from outside. Which is +which is a judgement, recorded as a trailer on the merge commit: Dropped-from-theirs: method fieldref -- superseded by the new pool builder, see -One trailer per name, a reason after `--`, and both are required. THE NAME MUST BE COPIED FROM THIS -CHECK'S OWN OUTPUT, character for character -- it is matched literally, so `method fieldref` works -and `Pool.fieldref` does not, even though the second reads better. Run the check, copy the name it -prints after the colon, paste it. (An earlier version of this docstring used the prettier form as its -only worked example, which meant anyone who followed it got no exemption and lost a round.) +One trailer per name, and a reason after `--` is required. THE NAME IS MATCHED LITERALLY -- copy it +from this check's own output, after the colon (`method fieldref` works, `Pool.fieldref` does not). +The list lives on the merge commit, not in a file, so it can only ever excuse that one commit. + +`-s ours` merges drop the whole other side on purpose and use the wildcard instead: + + Dropped-from-theirs: * -- recorded as a merged ancestor only, our tree is authoritative + +There is no structural exemption for them: "tree equals first parent" is also true of a resolution +that took our side wholesale, which is the exact failure this catches, so it must be declared. -The list lives in the merge commit message rather than in a file on purpose: a file accumulates -entries that outlive the merge they excused and quietly turns the check off, while a trailer can -only ever excuse the one commit it is written on. +A squash landing has one parent and reports nothing; the merges this catches are the base pulls +inside a branch, checked while its pull request is open. Usage: check-merge-dropped-symbols.py [] # default: origin/main..HEAD @@ -63,26 +33,10 @@ class described next. On time: the measurement noise is larger than the differen 1 something was dropped and not accounted for 2 could not measure -- a git call failed, or the clone is shallow -★ 2 is not a softer 0. Every git call here raises rather than returning nothing, because the one -failure this check must never produce is a green one: in a shallow clone the second parent's objects -are absent, every read of it comes back empty, and the old code printed `✓ (0 file(s) examined)` and -exited 0. Measured on this repository at depth 10: merges that examine 20 files in a full clone -examined 0 and the whole run was green. A check for a silent loss had a silent pass in it. - -`-s ours` merges -- "record these upstream cuts as ancestors" -- drop the whole other side on -purpose, and one trailer per name would mean hundreds. They use the wildcard form instead: - - Dropped-from-theirs: * -- recorded as a merged ancestor only, our tree is authoritative - -There is deliberately no structural exemption for them. The obvious one, "skip a merge whose tree -equals its first parent's", was written and then removed: it is true of `-s ours` *and* of a -resolution that took our side wholesale, and the second of those is the exact failure this exists -to catch. Measured -- with that exemption in place, a synthetic merge that discarded the other -side's new function passed. So the wholesale case has to be declared rather than inferred. +2 is not a softer 0: every git call raises rather than returning nothing, because in a shallow clone +the second parent's objects are absent and an empty read would otherwise pass green. -A squash landing has one parent, so there is nothing for this to compare and it reports nothing. -That is not a gap: the merges this catches are the base-pulls *inside* a branch, and those are -checked while the pull request is open, whichever way the branch is eventually landed. +Measurement history (the incident, scope and timing numbers): docs/worklog/2026-09-25-checker-offspring-cleanup.md. """ import re diff --git a/scripts/ldc-tag-survey-targets/Targets.kt b/scripts/ldc-tag-survey-targets/Targets.kt deleted file mode 100644 index 7956fb39..00000000 --- a/scripts/ldc-tag-survey-targets/Targets.kt +++ /dev/null @@ -1,44 +0,0 @@ -// Kotlin shapes most likely to put CONSTANT_MethodHandle (15), MethodType (16) or Dynamic (17) -// at an `ldc` site. Compiled with every flag that routes a feature through invokedynamic, because -// that is where those three tags live in compiler output at all. -// -// kotlinc -jvm-target 21 -Xlambdas=indy -Xsam-conversions=indy \ -// -Xstring-concat=indy-with-constants -d Targets.kt -import java.util.function.Function -import java.util.function.Supplier - -fun interface Op { - fun apply(x: Int): Int -} - -enum class Color { RED, GREEN, BLUE } - -class Holder(val n: Int) { - fun twice(): Int = n * 2 -} - -fun topLevel(x: Int): Int = x + 1 - -val lazyValue: String by lazy { "computed" } - -inline fun typeName(): String = T::class.java.name - -fun main() { - val h = Holder(21) - val lambda: (Int) -> Int = { it * 3 } // lambda - val unbound: (Int) -> Int = ::topLevel // unbound callable reference - val bound: () -> Int = h::twice // bound callable reference - val sam: Op = Op { it - 1 } // SAM conversion, Kotlin fun interface - val jfun: Function = Function { it.length } // SAM conversion, Java interface - val sup: Supplier = Supplier { "s" } - val c = Color.GREEN - val w = when (c) { // enum subject - Color.RED -> "r" - Color.GREEN -> "g" - Color.BLUE -> "b" - } - // string templates: the one construct javac routes through invokedynamic by default - val s = "a=${lambda(1)} b=${unbound(2)} c=${bound()} d=${sam.apply(5)} " + - "e=${jfun.apply("ab")} f=${sup.get()} g=$w h=${typeName()} i=$lazyValue" - println(s + Holder::class.java.simpleName) -} diff --git a/scripts/ldc-tag-survey-targets/Targets.scala b/scripts/ldc-tag-survey-targets/Targets.scala deleted file mode 100644 index 65dda962..00000000 --- a/scripts/ldc-tag-survey-targets/Targets.scala +++ /dev/null @@ -1,41 +0,0 @@ -// Scala 3 shapes most likely to put CONSTANT_MethodHandle (15), MethodType (16) or Dynamic (17) -// at an `ldc` site. Scala routes lambdas, SAM conversion and structural calls through -// invokedynamic, which is where those tags appear in compiler output at all. -// -// scalac -release 21 -d Targets.scala -import java.util.function.{Function => JFunction, Supplier} -import scala.reflect.Selectable.reflectiveSelectable - -trait Op: - def apply(x: Int): Int - -enum Color: - case Red, Green, Blue - -class Holder(val n: Int): - def twice: Int = n * 2 - -object Targets: - def topLevel(x: Int): Int = x + 1 - inline def inlined(x: Int): Int = x * 2 - lazy val lazyValue: String = "computed" - - type Named = { def twice: Int } - - def main(args: Array[String]): Unit = - val h = Holder(21) - val lambda: Int => Int = _ * 3 // lambda - val eta: Int => Int = topLevel // eta-expansion - val bound: () => Int = () => h.twice - val sam: Op = (x: Int) => x - 1 // SAM conversion, Scala trait - val jfun: JFunction[String, Integer] = (s: String) => s.length // SAM conversion, Java interface - val sup: Supplier[String] = () => "s" - val c: Color = Color.Green - val w = c match // enum subject - case Color.Red => "r" - case Color.Green => "g" - case Color.Blue => "b" - val struct: Named = h // structural type -> reflective call site - val s = s"a=${lambda(1)} b=${eta(2)} c=${bound()} d=${sam.apply(5)} " + - s"e=${jfun.apply("ab")} f=${sup.get()} g=$w h=${struct.twice} i=${inlined(4)} j=$lazyValue" - println(s) diff --git a/scripts/survey-ldc-constant-tags.py b/scripts/survey-ldc-constant-tags.py index e4b01a54..26dce3a3 100755 --- a/scripts/survey-ldc-constant-tags.py +++ b/scripts/survey-ldc-constant-tags.py @@ -14,45 +14,14 @@ alignment lands on nonsense, so a non-zero share there is a reason to distrust the rest. It is *detectable* misdecodes only: a misdecode landing on a plausible tag stays invisible. -## Targeted shapes, not just corpora (2026-09-17) - -The first survey measured what kotlinc and scalac had already produced — their own stdlibs — and -said outright that it had not asked either compiler to compile the features most likely to emit -these constants. That gap is closed: `ldc-tag-survey-targets/` holds one file per language -exercising lambdas, unbound and bound method references, SAM conversion to both a native and a -Java interface, an enum subject, string concatenation, a lazy value, and (Scala) eta-expansion, -an inline def and a structural type. - - kotlinc -jvm-target 21 -Xlambdas=indy -Xsam-conversions=indy \ - -Xstring-concat=indy-with-constants -d scripts/ldc-tag-survey-targets/Targets.kt - scalac -release 21 -d scripts/ldc-tag-survey-targets/Targets.scala - python3 scripts/survey-ldc-constant-tags.py - -Measured with kotlinc-jvm 2.4.20 and Scala 3.9.0 (both on JRE 26): - - Kotlin 7 classes · 23 ldc sites · tags 15/16/17 as an ldc operand: 0 - same tags in the pool: MethodHandle 7, MethodType 6 - Scala 7 classes · 18 ldc sites · tags 15/16/17 as an ldc operand: 0 - same tags in the pool: MethodHandle 11, MethodType 6 - -The pool counts are what make that 0 worth reading: they are non-zero, so the invokedynamic paths -really were exercised and the constants really were built — they just never reach an `ldc` -operand. Tag 17 (Dynamic) is absent from the pool in both, so in these shapes neither compiler emits -condy at all — the stronger statement of the two, and bounded exactly like every other number here: -one program per compiler (see "Not covered", below). - -The flags are deliberately the ones that route *more* through invokedynamic. A run without them -measures a compiler configured away from the shapes in question, which is the mistake the corpus -axis could not avoid: a stdlib is compiled for compatibility, not to exercise the backend. - -Positive control, so the 0 is not "the scanner cannot see it": +Positive control, so a 0 is not "the scanner cannot see it": python3 scripts/survey-ldc-constant-tags.py test-data/ldc -> tags 15/16/17 as an ldc operand: {MethodHandle 1, MethodType 2, Dynamic 7} -Not covered, said plainly: this is one program per compiler, so it bounds the *features listed -above* and not the languages. And it needs kotlinc and scalac installed — CI has neither, so like -`test-data/src/verify-javac-fixtures.sh` this is a check a person runs, not one that runs itself. +Targeted Kotlin/Scala shapes were measured once (tags 15/16/17 as an ldc operand: 0) and their +sources dropped; commands and numbers are in docs/worklog/2026-09-17-ldc-targeted-shapes-kotlin-scala.md. +CI runs none of this — like `test-data/src/verify-javac-fixtures.sh`, it is a check a person runs. """ import sys diff --git a/test-data/class-file-versions.txt b/test-data/class-file-versions.txt index 89419e13..18817c1f 100644 --- a/test-data/class-file-versions.txt +++ b/test-data/class-file-versions.txt @@ -5,6 +5,16 @@ # 70 x3 in the root alone. Unifying them would mean recompiling, and recompiling changes bytes, # which can change what a fixture exercises. So the rule here is only "it must not move on its own". # +# Decided 2026-09-18 not to unify: the version is coverage, not history. Recompiled at --release 21, +# the major-52 cohort loses the two pre-Java-9/11 lowerings a JVM must still run — `StringBuilder` +# concatenation (FormatterIntegration, NullSpecGuards) and `access$` nestmate bridges +# (ThreadInterruption, MonitorSemantics; NativeMethod's invokespecial->invokevirtual is the same +# axis). The 64 generator fixtures in cp/indy/ldc/attr carry zero of either, so this cohort is the +# only coverage. Of the other 20 top-level 52 fixtures, 16 rebuild to an identical opcode sequence, +# so unifying buys nothing. Reopen if the runtime drops major <= 52, if those paths gain coverage +# elsewhere, or if a third version axis turns up. The rule for *new* fixtures is left open. +# Re-measure, don't re-argue: docs/worklog/2026-09-18-root-fixture-target-decision.md. +# # What that catches: regenerating a fixture with a different JDK. `javac Switch.java` with no # --release stamps the running JDK's version, and a two-byte change inside a committed binary is # not something anyone notices in review. The majors 66, 68 and 70 above are that accident already