From 3b04712edbc405ede0c6a9538e117a1632ba0d95 Mon Sep 17 00:00:00 2001 From: jun0 Date: Thu, 17 Sep 2026 06:52:09 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[rustjava-adopt-link-stringconcatfactory-p0?= =?UTF-8?q?]=20feat(jvm-bytecode):=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20makeConcat=20=EB=8F=84=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 제안의 전제는 틀렸다. javac 은 release 9·11·17·21·26 전부 makeConcatWithConstants 를 내고, 상수 텍스트가 없는 a + b 도 그렇다 — 레시피가 자리표시자뿐일 따름이다. makeConcat 은 비기본 내부 플래그에서만 나오므로 「javac 산출물이 더 많이 돈다」는 거짓이다. 그래도 한 이유는 셋이다: 만들 수 있는 형상이고(그 플래그로 직접 만들었다), 문서화된 공개 진입점이며, 실행기에 새 경로가 필요 없다 — makeConcat(n) 은 레시피가 \u0001 n개인 makeConcatWithConstants 와 같아서 콜사이트 인자 수로 합성하면 된다. 이름과 서술자를 쌍으로 매칭해 한쪽 이름에 다른 쪽 서술자를 붙인 것은 링크되지 않게 했다 — 그 덕에 #55 의 name 축 근접 실패 픽스처가 그대로 살아 있다. 개악 4종 중 M3(쌍 검사를 이름만으로)·M4(정적 인자 가드 제거)가 처음엔 살아남았다. 직전 회차가 세운 「죽었다 ≠ 그 가지가 전부 덮였다」가 내 새 코드에 그대로 적용된 것이라, 축마다 근접 실패를 더해 4종 전건 red 로 만들었다. --- REPORT.md | 19 ++++ STATE.md | 29 ++++++ docs/worklog/2026-09-17-link-makeconcat.json | 33 +++++++ docs/worklog/2026-09-17-link-makeconcat.md | 74 ++++++++++++++ jvm-bytecode/src/string_concat.rs | 91 ++++++++++++++---- test-data/indy/MakeConcat.class | Bin 0 -> 584 bytes test-data/indy/MakeConcatWithArgument.class | Bin 0 -> 598 bytes .../indy/MakeConcatWrongDescriptor.class | Bin 0 -> 636 bytes test-data/src/indy/make_indy_fixtures.py | 79 +++++++++++++++ tests/test_class_format.rs | 29 ++++++ 10 files changed, 333 insertions(+), 21 deletions(-) create mode 100644 docs/worklog/2026-09-17-link-makeconcat.json create mode 100644 docs/worklog/2026-09-17-link-makeconcat.md create mode 100644 test-data/indy/MakeConcat.class create mode 100644 test-data/indy/MakeConcatWithArgument.class create mode 100644 test-data/indy/MakeConcatWrongDescriptor.class diff --git a/REPORT.md b/REPORT.md index 5462d274..56de54de 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,4 +1,23 @@ # REPORT +## [2026-09-17] `StringConcatFactory.makeConcat` 도 링크한다 — 단 «이유는 제안이 적은 것이 아니다» (rustjava-adopt-link-stringconcatfactory-p0) +- 무엇을: 레시피 없는 진입점 `makeConcat` 을 링크한다. ★**실행기 무접촉** — 콜사이트 인자 수로 **레시피를 합성**한다. +- 왜: 채택 제안 `2026-09-16-link-stringconcatfactory#p0`. +- 사용자 영향: ★`makeConcat` 에 묶인 콜사이트가 **거부 대신 실행**된다. +- ★★**제안의 전제는 «틀렸다»**(실측 javac 26.0.1 · 같은 소스 `a + b`): `--release` **9·11·17·21·26 전부 + `makeConcatWithConstants`** 를 낸다 — ★**상수 텍스트가 «없어도»** 그렇다(레시피가 자리표시자 둘일 뿐). + `makeConcat` 은 **비기본 내부 플래그 `-XDstringConcat=indy`** 에서만 나온다. + ⇒ ★**「javac 이 상수 텍스트 없이 연결할 때 쓴다」·「javac 산출물이 더 많이 돈다」는 둘 다 거짓**이다(기본 산출물은 **이미 전부 링크된다**). +- ★**그래도 한 이유를 «바꿔서» 적는다**: ⑴**만들 수 있는 형상**이고(그 플래그로 직접 만들었다 — 이 리니지가 `ldc` 지원을 + 정당화한 「ASM 이 실제로 낸다」와 **같은 기준**) ⑵**문서화된 공개 진입점**이며 ⑶★**실행기에 새 경로가 필요 없다** + (`makeConcat(n)` ≡ 레시피 `\u{1}`×n 인 `makeConcatWithConstants`). +- ★**설계**: 상수 튜플 **둘**뿐이고 **레지스트리로 만들지 않았다**(제안 tradeoff 준수) · + ★**이름과 서술자를 «쌍»으로** 맞춰 ★**#55 의 name 축 근접 실패 픽스처가 «살아 있다»**(그 파일은 이름만 `makeConcat`) · + 레시피 합성은 **콜사이트마다**(한 부트스트랩을 서술자가 다른 콜사이트들이 공유할 수 있다). +- ★★**개악 4종 중 «둘»이 처음엔 살아남았다** — M3(쌍 검사를 이름만으로) · M4(정적 인자 가드 제거). + ★**직전 회차가 세운 「죽었다 ≠ 그 가지가 전부 덮였다」가 내 새 코드에 그대로 적용됐다** ⇒ 축마다 근접 실패를 더해 + **4종 전건 KILLED** 로 만들었다(`MakeConcatWrongDescriptor` · `MakeConcatWithArgument`). +- ★**픽스처가 «출력»한다**(`ab`) — ★레시피를 틀린 길이로 합성해도 **링크되고 실행된다**. 값을 버리면 그 오류가 안 보인다(M2 가 그 증거). +- 검증: `cargo test --all` **573 → 574 / 0 failed** · 픽스처 재생성 **멱등**(기존 4개 바이트 동일) · DoD 7명령 rc=0. ## [2026-09-17] 신원 4축을 «각각» 관측 가능하게 했다 — 감사의 「고칠 것이 없다」를 정정한다 (rustjava-adopt-cp-tag-passthrough-detectable-p0-fix) - 무엇을: `string_concat.rs` 의 부트스트랩 신원 **4축**(kind·class·name·descriptor) 중 ★**3축이 «관측되지 않고» 있었다** — 근접 실패 픽스처가 **class 축 하나**뿐이었기 때문이다. 나머지 3축의 픽스처를 만들었다. ★**제품 코드 무접촉.** diff --git a/STATE.md b/STATE.md index dbe798e2..bcdaf502 100644 --- a/STATE.md +++ b/STATE.md @@ -4,6 +4,35 @@ (없음 — 2026-09-16 실측: 착수 시 진행 티켓 0 · 열린 PR 0. ※「열린 PR 0」은 ★**이 회차 PR 착지 시점 기준**이다 — 회신 시점에는 그 PR 자신이 열려 있다) ## 완료 +- [rustjava-adopt-link-stringconcatfactory-p0] ★★**`StringConcatFactory.makeConcat` 도 링크한다 — 단 «이유는 제안이 적은 것이 아니다».** + 채택 제안 `2026-09-16-link-stringconcatfactory#p0`(worklog json `adoptedProposals` 기록). + ★**제품 동작 변경**: `makeConcat` 콜사이트가 **거부 대신 실행**된다. ★**실행기(`concat_with_constants`)는 한 줄도 안 바뀌었다.** + ★★**ⓒ 제안의 전제가 «거짓»이다**(실측 javac 26.0.1 · 같은 소스 `a + b`): + `--release` **9 · 11 · 17 · 21 · 26 전부 `makeConcatWithConstants`** — ★**상수 텍스트가 «없는» 경우에도** 그렇다 + (레시피가 «자리표시자 둘 · 리터럴 0»일 뿐이다). `makeConcat` 은 **비기본 내부 플래그 `-XDstringConcat=indy`** 에서만 나오고, + `-XDstringConcat=inline` 은 `StringBuilder` 를 낸다. + ⇒ ★**「javac 이 상수 텍스트 없이 연결할 때 쓴다」도, 「javac 산출물이 더 많이 돈다」도 거짓**이다 — + ★**기본 javac 산출물은 이미 «전부» 링크되고 있었다.** + ★★**그래도 한 이유를 «바꿔서» 적는다**(제안의 근거를 그대로 베끼지 않았다): + ⑴★**만들 수 있는 형상**이다 — 그 플래그로 **직접 만들었다**. 이 리니지가 `ldc` 태그 지원을 정당화한 기준 + (「ASM 이 실제로 낸다」)과 ★**같은 기준**이다 ⑵**문서화된 공개 진입점**이라 바이트코드 생성기가 택할 수 있다 + ⑶★★**실행기에 «새 경로»가 필요 없다** — `makeConcat(n개 인자)` ≡ 레시피가 `\u{1}` **n개**인 `makeConcatWithConstants` 다 + ⇒ 콜사이트 서술자의 인자 수로 **레시피를 합성**하면 끝. ★제안이 「a recipe-free path through the same executor」로 본 것보다 **더 싸다**. + ★**설계 — 제안의 「짧은 명시 목록을 유지하라」를 지켰다**: 상수 튜플 **둘**뿐 · **레지스트리 아님** · + ★**이름과 서술자를 «쌍»으로 매칭**한다 ⇒ ★**#55 가 넣은 name 축 근접 실패(`NotMakeConcatWithConstants`)가 «살아 있다»** + (그 파일은 이름만 `makeConcat` 이고 서술자는 `makeConcatWithConstants` 의 것이라 여전히 거부된다). + ★**레시피 합성은 «콜사이트마다»** 한다 — 한 부트스트랩 항목을 **서술자가 다른 여러 콜사이트가 공유**할 수 있어 + 해석 단계에서 고정할 수 없다(그래서 `LinkedFactory::{WithConstants, NoRecipe}` 로 «인식»만 한다). + ★★**개악 대조 4종 — 처음엔 «둘»이 살아남았고, 그것이 이 회차의 교훈이다**: + **M1** 튜플 제거 → KILLED · **M2** 레시피 길이 `repeat(1)` → KILLED · ★**M3** 쌍 검사를 «이름만»으로 → **SURVIVED** · + ★**M4** 「정적 인자 없음」 가드 제거 → **SURVIVED**. + ⇒ ★**직전 `…-p0-fix` 가 세운 「죽었다 ≠ 그 가지가 «전부» 덮였다」가 «내 새 코드»에 그대로 적용됐다** — + 축마다 근접 실패를 더해(`MakeConcatWrongDescriptor` = 서술자만 다르고 정적 인자 0 · `MakeConcatWithArgument` = 이름·서술자 맞고 정적 인자 1) + ★**4종 전건 KILLED** 로 만들었다. + ★**픽스처가 «출력»한다**(`ab`) — ★**레시피를 틀린 길이로 합성해도 링크되고 실행된다.** 값을 버리면 그 오류가 «안 보인다» + (M2 가 그 증거다) ⇒ 「링크됐다」가 아니라 **「무엇이 연결됐나」**를 단언한다. + ★**잃는 것**: 링크 수용 범위가 넓어졌다 — 「콜사이트 «하나»를 링크한다」가 이제 **둘**이다(제안이 경고한 그대로 · 명시 목록으로 억제). + ★`cargo test --all` **573 → 574 / 0 failed / 1 ignored** · 픽스처 재생성 **멱등** · DoD **7줄 전건 rc=0**. - [rustjava-adopt-cp-tag-passthrough-detectable-p0-fix] ★★**신원 4축을 «각각» 관측 가능하게 했다 — 감사의 「고칠 것이 없다」를 정정한다.** 게이트② **request-changes** 승계(PR #55 · 핀 `ab13a3c7`). ★**제품 코드 무접촉** — 없던 것은 **픽스처**다. ★★**무엇이 틀렸나**: 직전 감사의 **M7**(「신원 4축 검사 제거」)은 네 비교를 ★**한꺼번에** 지운다 ⇒ 그 red 가 증명하는 것은 diff --git a/docs/worklog/2026-09-17-link-makeconcat.json b/docs/worklog/2026-09-17-link-makeconcat.json new file mode 100644 index 00000000..80ea2afd --- /dev/null +++ b/docs/worklog/2026-09-17-link-makeconcat.json @@ -0,0 +1,33 @@ +{ + "schema": "worklog/v1", + "date": "2026-09-17", + "taskId": "rustjava-adopt-link-stringconcatfactory-p0", + "summary": "Linked StringConcatFactory.makeConcat, the recipe-free entry point, by synthesising the recipe from the call site's arity — the executor needed nothing new. The proposal's stated reason was wrong: javac never emits makeConcat, at any release from 9 to 26.", + "changes": [ + "jvm-bytecode/src/string_concat.rs: a second name/descriptor pair, matched as a pair so one entry point's name with the other's descriptor does not link; resolve_bootstrap_methods returns LinkedFactory::{WithConstants, NoRecipe}, because a recipe-free call site's recipe depends on the call site rather than the bootstrap", + "test-data/src/indy/make_indy_fixtures.py: make_concat_call_site builds a linkable makeConcat class that prints its result, and takes parameters for the two near misses of the new pair", + "test-data/indy/MakeConcat.class, MakeConcatWrongDescriptor.class, MakeConcatWithArgument.class: new fixtures", + "tests/test_class_format.rs: test_the_recipe_free_factory_concatenates_every_argument, plus the two new near misses in the identity-axis test" + ], + "verification": [ + "javac 26.0.1 measured on the same source at --release 9, 11, 17, 21 and 26: all five emit makeConcatWithConstants, including for a + b with no literal text, where the recipe is two placeholders and nothing else; only -XDstringConcat=indy emits makeConcat, and -XDstringConcat=inline emits StringBuilder", + "end to end: the fixture prints 'ab', so the synthesised recipe consumed both arguments in order", + "mutation M1, removing the makeConcat pair: red", + "mutation M2, synthesising a recipe of the wrong length: red — which is why the fixture prints rather than discarding the value", + "mutation M3, loosening the pair check to the name alone: SURVIVED at first, killed after adding MakeConcatWrongDescriptor", + "mutation M4, dropping the no-static-arguments guard: SURVIVED at first, killed after adding MakeConcatWithArgument", + "the near-miss fixtures added by PR #55 still refuse, because name and descriptor are compared as a pair", + "cargo test --all --no-fail-fast: 574 passed / 0 failed / 1 ignored, up from 573", + "fixture regeneration is idempotent: the four pre-existing indy fixtures come out byte-identical", + "DoD 7 commands all rc=0" + ], + "issues": [ + "The linker now accepts two bootstraps rather than one — exactly the widening the proposal warned about. It is kept as a written-out pair list rather than a registry, and the doc comment says why.", + "The benefit is not the one the proposal claimed: default javac output was already fully linked. This helps producers that choose the recipe-free entry point, including javac under -XDstringConcat=indy.", + "M3 and M4 surviving the first pass is the same shape the sibling audit round documented: a coarse mutation proves one axis is observed, not all of them. Each axis of the new pair now has its own near miss." + ], + "adoptedProposals": [ + "2026-09-16-link-stringconcatfactory#p0" + ], + "proposals": [] +} diff --git a/docs/worklog/2026-09-17-link-makeconcat.md b/docs/worklog/2026-09-17-link-makeconcat.md new file mode 100644 index 00000000..37a1c09c --- /dev/null +++ b/docs/worklog/2026-09-17-link-makeconcat.md @@ -0,0 +1,74 @@ +# 2026-09-17 — `StringConcatFactory.makeConcat` 도 링크한다 — 단 «이유는 제안이 적은 것이 아니다» + +티켓 `rustjava-adopt-link-stringconcatfactory-p0` — 채택 제안 `2026-09-16-link-stringconcatfactory#p0`. + +★**제품 동작이 바뀐다**: `makeConcat` 부트스트랩에 묶인 `invokedynamic` 콜사이트가 이제 **실행된다** +(종전 `UnsupportedOperationException: invokedynamic`). + +## ⓒ ★제안의 전제는 «틀렸다» — 그리고 그 사실이 이 회차의 절반이다 + +제안 `plainSummary`: 「the sibling factory with no recipe — **used when javac concatenates without constant text**」 +제안 `userBenefit`: 「**More javac-emitted** string concatenation runs instead of being refused」 + +★**실측(javac 26.0.1 · 같은 소스 `a + b`)**: + +| 대상 | 내는 부트스트랩 | +|---|---| +| `--release 9` · `11` · `17` · `21` · `26` | ★**전부 `makeConcatWithConstants`** | +| `-XDstringConcat=indy`(비기본 내부 플래그) | `makeConcat` | +| `-XDstringConcat=inline` | `StringBuilder` | + +★**상수 텍스트가 «없는» `a + b` 도 `makeConcatWithConstants` 다** — 레시피가 자리표시자 둘(``)일 뿐 리터럴이 0인 것이다. +⇒ ★**「javac 이 상수 텍스트 없이 연결할 때 쓴다」는 거짓**이고, ★**「javac 산출물이 더 많이 돈다」도 거짓**이다 +(기본 javac 산출물은 **이미 전부 링크된다**). + +## 그래도 «왜» 했나 — 근거를 바꿔서 적는다 + +⑴★**만들 수 있는 형상이다** — `-XDstringConcat=indy` 로 **실제로 만들었다**. + 이 리니지가 `ldc` 태그 지원을 정당화한 기준(「ASM 이 실제로 낸다」)과 **같은 기준**이다. +⑵★**`makeConcat` 은 `StringConcatFactory` 의 «문서화된 공개 진입점»** 이고, 바이트코드 생성기는 그것을 택할 수 있다. +⑶★★**실행기에 «새 경로가 필요 없다»** — `makeConcat(n개 인자)` ≡ 레시피가 `\u{1}` **n개**인 `makeConcatWithConstants` 다. + ⇒ 콜사이트 서술자의 인자 수로 **레시피를 합성**하면 끝이고, `concat_with_constants` 는 **한 줄도 안 바뀐다**. + ★제안이 「a recipe-free path through the same executor」라고 본 것보다 **더 싸다**(경로가 아예 안 생긴다). + +## 설계 — 제안의 「짧은 명시 목록을 유지하라」를 지켰다 + +- 상수 튜플 **둘**(이름+서술자 쌍)뿐이고 **레지스트리로 만들지 않았다**(제안 tradeoff 준수). +- ★**이름과 서술자를 «쌍»으로 맞춘다** — `(name, descriptor)` 를 함께 매칭하므로 + ★**한쪽 진입점의 이름에 다른 쪽의 서술자를 붙인 것은 링크되지 않는다.** + ⇒ 이 결정이 ★**#55 가 넣은 name 축 근접 실패 픽스처(`NotMakeConcatWithConstants`)를 «살렸다»** — 그 파일은 + 이름이 `makeConcat` 이지만 서술자가 `makeConcatWithConstants` 의 것이라 **여전히 거부된다**. +- ★**레시피 합성은 «콜사이트마다»** 한다 — 한 부트스트랩 항목을 서술자가 다른 여러 콜사이트가 공유할 수 있어 + 부트스트랩 해석 단계에서는 **고정할 수 없다**. 그래서 해석은 `LinkedFactory::{WithConstants, NoRecipe}` 로 «인식»만 하고, + 합성은 하강 루프에서 한다. + +## 픽스처 — ★**결과를 «출력»하게 만들었다**(이유가 있다) + +`test-data/indy/MakeConcat.class`(생성기 · 합성): `ldc "a"; ldc "b"; invokedynamic concat(String,String)String; println`. +★**「링크됐다」가 아니라 「무엇이 연결됐나」를 단언**한다 — ★**레시피를 «틀린 길이»로 합성해도 링크는 되고 실행도 된다.** +값을 버리면 그 오류가 **안 보인다**(실측: 아래 M2). +★**javac 이 안 내는 형상이라 손으로 조립했다** — 비기본 내부 플래그에 픽스처 재생성을 의존시키는 것이 더 나쁘다. + +## ★★개악 대조 — 4종 중 «둘»이 처음엔 살아남았고, 그래서 픽스처를 더 만들었다 + +| 개악 | 처음 | 픽스처 보강 후 | +|---|---|---| +| **M1** `makeConcat` 튜플 제거(= 제안 이전) | **KILLED** | KILLED | +| **M2** 레시피 길이를 `repeat(1)` 로 | **KILLED**(출력 대조가 잡는다) | KILLED | +| **M3** 쌍 검사를 느슨하게(`(name, _)`) | ★**SURVIVED** | ★**KILLED** | +| **M4** 「정적 인자 없음」 가드 제거 | ★**SURVIVED** | ★**KILLED** | + +★**M3·M4 가 살아남은 이유는 «축마다 픽스처가 없어서»다** — 직전 `…-p0-fix` 회차가 세운 +「**죽었다 ≠ 그 가지가 «전부» 덮였다**」가 ★**내 새 코드에 그대로 적용됐다.** +⇒ 근접 실패 둘을 더했다: `MakeConcatWrongDescriptor`(이름은 맞고 **서술자만** 다르며 **정적 인자 0** ⇒ 서술자 비교만이 거부할 수 있다) · +`MakeConcatWithArgument`(이름·서술자 맞고 **정적 인자 1** ⇒ 인자 수 가드만이 거부할 수 있다). +★**둘 다 «그 축 하나»만 다르다** — 그래야 그 축이 관측된다. + +## 잃는 것 / 안 하면 무엇이 나쁜가 + +⒜**잃는 것**: ⑴★**링크 수용 범위가 넓어진다** — 제안이 경고한 그대로이고, 「이 런타임은 콜사이트 «하나»를 링크한다」가 + 더 이상 참이 아니다(이제 **둘**이다). ⇒ 상수 튜플을 **명시 목록**으로 유지하고 doc 에 그 이유를 적었다. + ⑵픽스처 **3개** · 테스트 1개 · `cargo test --all` **573 → 574**. + ⑶`resolve_bootstrap_methods` 의 반환형이 튜플 → 작은 enum 으로 바뀌었다(호출부 1곳). +⒝**안 하면**: `makeConcat` 에 묶인 콜사이트는 계속 **「이 런타임이 아직 못 한다」**로 거부된다 — + ★**실행기는 그것을 «이미 할 수 있는데»** 그렇다(레시피 합성 한 줄이면 되는 일이었다). diff --git a/jvm-bytecode/src/string_concat.rs b/jvm-bytecode/src/string_concat.rs index f05c644f..c07bde1c 100644 --- a/jvm-bytecode/src/string_concat.rs +++ b/jvm-bytecode/src/string_concat.rs @@ -1,8 +1,8 @@ -//! Resolving the one `invokedynamic` bootstrap this runtime links: -//! `java.lang.invoke.StringConcatFactory.makeConcatWithConstants`. +//! Resolving the `invokedynamic` bootstraps this runtime links — both of +//! `java.lang.invoke.StringConcatFactory`'s entry points, and nothing else. //! //! javac 9+ lowers `+` on strings to an `invokedynamic` call site bound to that factory, so an -//! ordinary one-liner needs it. Everything else — `makeConcat`, `LambdaMetafactory`, +//! ordinary one-liner needs it. Everything else — `LambdaMetafactory`, //! `ConstantBootstraps` — stays unsupported, and that is deliberate: the verifier rejects any //! `Opcode::Invokedynamic` that is still an `Invokedynamic` by the time it runs, so *not* //! recognising a call site here is what keeps the refusal honest. @@ -17,15 +17,32 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference, MethodHandleKind, Opcode, StringConcatCallSite}; +use jvm::JavaType; -/// The bootstrap method this runtime links, spelled out in full. +/// The bootstrap methods this runtime links, spelled out in full. /// /// All four axes are compared. Matching on fewer — say, the method name alone — would link call /// sites bound to some other class's `makeConcatWithConstants`, which is exactly the "linked /// everything" change this is meant not to be. +/// +/// Two entries, and it stays a written-out list rather than growing into a registry: every +/// addition makes "this runtime links string concatenation" less precise, so the list has to be +/// short enough to read. const FACTORY_CLASS: &str = "java/lang/invoke/StringConcatFactory"; const FACTORY_NAME: &str = "makeConcatWithConstants"; const FACTORY_DESCRIPTOR: &str = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;"; +/// The recipe-free sibling: the call site descriptor alone says what to concatenate, so it takes +/// no static arguments and its own descriptor is shorter by exactly those two parameters. +/// +/// Measured, because the reason to support it is not the one that is usually given: javac does +/// *not* emit this. Targets 9, 11, 17, 21 and 26 all emit `makeConcatWithConstants`, even for +/// `a + b` with no literal text — the recipe is then just `\u0001\u0001`. Only the non-default +/// `-XDstringConcat=indy` produces `makeConcat`. It is linked because it is a documented entry +/// point of the same factory that any bytecode generator may target, and because the executor +/// needs nothing new for it (below). +const FACTORY_NAME_NO_RECIPE: &str = "makeConcat"; +const FACTORY_DESCRIPTOR_NO_RECIPE: &str = + "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;"; /// Rewrite every recognised `makeConcatWithConstants` call site in `class` into a resolved opcode. /// @@ -51,22 +68,46 @@ pub(crate) fn lower(class: &mut ClassInfo) { else { continue; }; - let Some((recipe, constants)) = resolved.get(bootstrap_method_attr_index) else { + let Some(linked) = resolved.get(bootstrap_method_attr_index) else { continue; }; + // `makeConcat` is `makeConcatWithConstants` whose recipe is "every argument, in + // order, nothing else" — so it is expressed as that recipe rather than as a second + // path through the executor. The arity comes from this call site, which is why the + // synthesis happens here and not where the bootstrap was recognised. + let (recipe, constants) = match linked { + LinkedFactory::WithConstants { recipe, constants } => (recipe.clone(), constants.clone()), + LinkedFactory::NoRecipe => { + let method_type = JavaType::parse(descriptor); + let (parameters, _) = method_type.as_method(); + (Arc::new("\u{1}".repeat(parameters.len())), Vec::new()) + } + }; + *opcode = Opcode::InvokedynamicStringConcat(StringConcatCallSite { - recipe: recipe.clone(), + recipe, descriptor: descriptor.clone(), - constants: constants.clone(), + constants, }); } } } } -/// Bootstrap index -> (recipe, constants), for the entries that are this factory and nothing else. -fn resolve_bootstrap_methods(class: &ClassInfo) -> BTreeMap, Vec>)> { +/// What a linked bootstrap tells us, which is not the same for the two entry points. +/// +/// `makeConcatWithConstants` carries its recipe as a static argument, so it is known here. +/// `makeConcat` has none: the recipe follows from the *call site's* descriptor, and one bootstrap +/// entry can be shared by call sites with different descriptors — so it cannot be resolved to a +/// recipe at this level, only recognised. +enum LinkedFactory { + WithConstants { recipe: Arc, constants: Vec> }, + NoRecipe, +} + +/// Bootstrap index -> what it links to, for the entries that are this factory and nothing else. +fn resolve_bootstrap_methods(class: &ClassInfo) -> BTreeMap { let Some(bootstrap_methods) = class.attributes.iter().find_map(|attribute| match attribute { AttributeInfo::BootstrapMethods(methods) => Some(methods), _ => None, @@ -78,22 +119,30 @@ fn resolve_bootstrap_methods(class: &ClassInfo) -> BTreeMap, V .iter() .enumerate() .filter_map(|(index, bootstrap)| { - if bootstrap.method.kind != MethodHandleKind::InvokeStatic - || bootstrap.method.member.class.as_str() != FACTORY_CLASS - || bootstrap.method.member.name.as_str() != FACTORY_NAME - || bootstrap.method.member.descriptor.as_str() != FACTORY_DESCRIPTOR - { + if bootstrap.method.kind != MethodHandleKind::InvokeStatic || bootstrap.method.member.class.as_str() != FACTORY_CLASS { return None; } - // The recipe is static argument 0; the rest fill the recipe's `\u{2}` slots. Both must - // be String constants — a recipe that is not a string is not a shape javac emits, and - // guessing at it would be linking something we have not read. - let (recipe, constants) = bootstrap.arguments.split_first()?; - let recipe = string_constant(class, *recipe)?; - let constants = constants.iter().map(|x| string_constant(class, *x)).collect::>>()?; + // Name and descriptor are still compared together, one pair per entry point: matching a + // name against the other entry point's descriptor must not link. + let linked = match (bootstrap.method.member.name.as_str(), bootstrap.method.member.descriptor.as_str()) { + (FACTORY_NAME, FACTORY_DESCRIPTOR) => { + // The recipe is static argument 0; the rest fill the recipe's `\u{2}` slots. Both + // must be String constants — a recipe that is not a string is not a shape javac + // emits, and guessing at it would be linking something we have not read. + let (recipe, constants) = bootstrap.arguments.split_first()?; + let recipe = string_constant(class, *recipe)?; + let constants = constants.iter().map(|x| string_constant(class, *x)).collect::>>()?; + + LinkedFactory::WithConstants { recipe, constants } + } + // Static arguments are not merely unused here, they are a contradiction: this entry + // point is defined as taking none, so anything else is not the shape it claims. + (FACTORY_NAME_NO_RECIPE, FACTORY_DESCRIPTOR_NO_RECIPE) if bootstrap.arguments.is_empty() => LinkedFactory::NoRecipe, + _ => return None, + }; - Some((index as u16, (recipe, constants))) + Some((index as u16, linked)) }) .collect() } diff --git a/test-data/indy/MakeConcat.class b/test-data/indy/MakeConcat.class new file mode 100644 index 0000000000000000000000000000000000000000..77be8772eb87aebe4161abb4de15be1afc584f1c GIT binary patch literal 584 zcmZuu%TB^T6g?wIp;kcqkYxzfNGasxi$rpx`l`Pdx@o2m(hq{khf zp;UWmuJE=RIey=&cL;CDo?xi1K*yiRp)ey()c>B_N{*%)(lpll8_N*ZVqKTQmQb&< z_rU$0E5d41%HcS$VzlM(SY=K4JPm|Z-$WbSb=!^-mH{0*Ix;Y_DeN+&>_r9)w{bI@ zQ+^*EYrQNP$Rd}gT=&;fY)?ZaJPieggd8h|VkC8>`9%3CQVZ^ppCU>b9N>_lu!wP; zpy8N73+4)!exmq~%X-JaDatxdaK^xR8W*&6)2NW4J1JGDMm&gu4N<`slH?X(l)^Sr iv}<%LBS9x1_l~3dW&UagdeaBa-{w?8u4$cf_kbToo12mV literal 0 HcmV?d00001 diff --git a/test-data/indy/MakeConcatWithArgument.class b/test-data/indy/MakeConcatWithArgument.class new file mode 100644 index 0000000000000000000000000000000000000000..432df81fa6d5189896093e9448a05ebbfc6d7004 GIT binary patch literal 598 zcmZuuJx>Bb5Pd7i9e4_Y;1?<=1ynQ^CR9pdEFhX-u&`Q=4IJFvUbuyXpQVKbg+IU_ zWgIw^BUtRt?!0+3Z{L1>&prWMqr{MF@}a0p*W~KOQvKUeZ|n$HX`RWg`qxhg&WY%dd?U-clpW?bRx6!t1gt7)mS9awl>qszFUK>zz}>Db{8B^8;n=H%%9g`JnKk0`)DyMJCR*pV-LjOZ8AxC!fjEq0 z47&_5bKwWWO<2t4l;1~(S}#)ul1Qb=NM?;1oBFBm$8&EA-eIox9u?2TX zPZqg24sghjS=hKBb5Pc)c;du&zpeTNz6jISxn9zwa77$G^T39W|1`m$g%iUtaKk;v9VL{;! z@JAU35A}#PJG(RQ&AfU0{xNw2aDob<*b)QTP@XOHU7)<)xeV>V@wEyJ5FyhSkD}%Z zuUEV3^rfv0BnXQ`;dq3?@qP2R-qwNR_3G7It~XRy5-Pt%$9q%*S&J~yxC>$Pl(B&n z!5sdmAUqw%y3U9#sUKALQg~fghLxsLgOOj4!Dho_nK|Ok*q8sneat38p3tk zj+S)`CRR)=!%8QyN=VvYz7bC2ivLRab|%((ma>pWCd+@ZK4*1%9BMf Date: Thu, 17 Sep 2026 10:41:15 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[rustjava-adopt-link-stringconcatfactory-p0?= =?UTF-8?q?]=20docs(state):=20=EA=B2=8C=EC=9D=B4=ED=8A=B8=E2=91=A2=20?= =?UTF-8?q?=EC=B0=A9=EC=A7=80=20=EA=B8=B0=EB=A1=9D=20=EB=8F=99=EB=B4=89=20?= =?UTF-8?q?(PR=20#59=20=C2=B7=20--merge)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- STATE.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/STATE.md b/STATE.md index bcdaf502..551f882b 100644 --- a/STATE.md +++ b/STATE.md @@ -33,6 +33,14 @@ (M2 가 그 증거다) ⇒ 「링크됐다」가 아니라 **「무엇이 연결됐나」**를 단언한다. ★**잃는 것**: 링크 수용 범위가 넓어졌다 — 「콜사이트 «하나»를 링크한다」가 이제 **둘**이다(제안이 경고한 그대로 · 명시 목록으로 억제). ★`cargo test --all` **573 → 574 / 0 failed / 1 ignored** · 픽스처 재생성 **멱등** · DoD **7줄 전건 rc=0**. + ★★**게이트③ 착지 — PR #59 · `--merge`**(등재 repo · `merge_strategy: merge` 선언분). 게이트② **approve** · + 핀 `3b04712e` **불이동**(착수 실측 2026-09-17T01:40:32Z) · ★**`MERGEABLE/CLEAN` · base 뒤처짐 «0»** ⇒ 충돌 해소·base 당김 **둘 다 불요**. + ★핀에서 `ci-presence` **rc=0 CI_GREEN** · 자식 PR **0건** · 배포 **0**(이 저장소에 배포 워크플로 없음) · 주기 자동 커밋 **0건**. + ★**동봉은 이 기록 한 줄뿐** — 원장(worklog 쌍·`STATE`·`REPORT`)은 구현 회차가 이미 실었다. + ★★**착지 순서 고지 — 이 회차가 «새 `.class` 3개»를 들여온다**(`MakeConcat`·`MakeConcatWithArgument`·`MakeConcatWrongDescriptor`). + 열린 PR **#57**(ci-pending)이 「`.class` 미등재는 핀이 **실패**시킨다」(`test-data/class-file-versions.txt`)를 세우므로, + ★**이 PR 이 먼저 착지하면 #57 의 표에 이 셋이 «없어»** 그 회차가 red 가 된다(해소 = `record-class-file-versions.py` 재생성). + ★**텍스트 충돌 0 이라 `mergeable` 로는 보이지 않는다** — 같은 고지가 #60 회신에도 있다(그쪽은 다른 3개). - [rustjava-adopt-cp-tag-passthrough-detectable-p0-fix] ★★**신원 4축을 «각각» 관측 가능하게 했다 — 감사의 「고칠 것이 없다」를 정정한다.** 게이트② **request-changes** 승계(PR #55 · 핀 `ab13a3c7`). ★**제품 코드 무접촉** — 없던 것은 **픽스처**다. ★★**무엇이 틀렸나**: 직전 감사의 **M7**(「신원 4축 검사 제거」)은 네 비교를 ★**한꺼번에** 지운다 ⇒ 그 red 가 증명하는 것은