Skip to content

Commit 66bc49e

Browse files
authored
[rustjava-adopt-link-stringconcatfactory-p0] feat(jvm-bytecode): 레시피 없는 makeConcat 도 링크한다 (#59)
[rustjava-adopt-link-stringconcatfactory-p0] feat(jvm-bytecode): 레시피 없는 makeConcat 도 링크한다
2 parents 4b89f18 + d9f457c commit 66bc49e

10 files changed

Lines changed: 341 additions & 21 deletions

‎REPORT.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,23 @@
11
# REPORT
2+
## [2026-09-17] `StringConcatFactory.makeConcat` 도 링크한다 — 단 «이유는 제안이 적은 것이 아니다» (rustjava-adopt-link-stringconcatfactory-p0)
3+
- 무엇을: 레시피 없는 진입점 `makeConcat` 을 링크한다. ★**실행기 무접촉** — 콜사이트 인자 수로 **레시피를 합성**한다.
4+
- 왜: 채택 제안 `2026-09-16-link-stringconcatfactory#p0`.
5+
- 사용자 영향: ★`makeConcat` 에 묶인 콜사이트가 **거부 대신 실행**된다.
6+
- ★★**제안의 전제는 «틀렸다»**(실측 javac 26.0.1 · 같은 소스 `a + b`): `--release` **9·11·17·21·26 전부
7+
`makeConcatWithConstants`** 를 낸다 — ★**상수 텍스트가 «없어도»** 그렇다(레시피가 자리표시자 둘일 뿐).
8+
`makeConcat` 은 **비기본 내부 플래그 `-XDstringConcat=indy`** 에서만 나온다.
9+
⇒ ★**「javac 이 상수 텍스트 없이 연결할 때 쓴다」·「javac 산출물이 더 많이 돈다」는 둘 다 거짓**이다(기본 산출물은 **이미 전부 링크된다**).
10+
- ★**그래도 한 이유를 «바꿔서» 적는다**: ⑴**만들 수 있는 형상**이고(그 플래그로 직접 만들었다 — 이 리니지가 `ldc` 지원을
11+
정당화한 「ASM 이 실제로 낸다」와 **같은 기준**) ⑵**문서화된 공개 진입점**이며 ⑶★**실행기에 새 경로가 필요 없다**
12+
(`makeConcat(n)` ≡ 레시피 `\u{1}`×n 인 `makeConcatWithConstants`).
13+
- ★**설계**: 상수 튜플 **둘**뿐이고 **레지스트리로 만들지 않았다**(제안 tradeoff 준수) ·
14+
★**이름과 서술자를 «쌍»으로** 맞춰 ★**#55 의 name 축 근접 실패 픽스처가 «살아 있다»**(그 파일은 이름만 `makeConcat`) ·
15+
레시피 합성은 **콜사이트마다**(한 부트스트랩을 서술자가 다른 콜사이트들이 공유할 수 있다).
16+
- ★★**개악 4종 중 «둘»이 처음엔 살아남았다** — M3(쌍 검사를 이름만으로) · M4(정적 인자 가드 제거).
17+
★**직전 회차가 세운 「죽었다 ≠ 그 가지가 전부 덮였다」가 내 새 코드에 그대로 적용됐다** ⇒ 축마다 근접 실패를 더해
18+
**4종 전건 KILLED** 로 만들었다(`MakeConcatWrongDescriptor` · `MakeConcatWithArgument`).
19+
- ★**픽스처가 «출력»한다**(`ab`) — ★레시피를 틀린 길이로 합성해도 **링크되고 실행된다**. 값을 버리면 그 오류가 안 보인다(M2 가 그 증거).
20+
- 검증: `cargo test --all` **573 → 574 / 0 failed** · 픽스처 재생성 **멱등**(기존 4개 바이트 동일) · DoD 7명령 rc=0.
221
## [2026-09-17] 신원 4축을 «각각» 관측 가능하게 했다 — 감사의 「고칠 것이 없다」를 정정한다 (rustjava-adopt-cp-tag-passthrough-detectable-p0-fix)
322
- 무엇을: `string_concat.rs` 의 부트스트랩 신원 **4축**(kind·class·name·descriptor) 중 ★**3축이 «관측되지 않고» 있었다** —
423
근접 실패 픽스처가 **class 축 하나**뿐이었기 때문이다. 나머지 3축의 픽스처를 만들었다. ★**제품 코드 무접촉.**

‎STATE.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,43 @@
44
(없음 — 2026-09-16 실측: 착수 시 진행 티켓 0 · 열린 PR 0. ※「열린 PR 0」은 ★**이 회차 PR 착지 시점 기준**이다 — 회신 시점에는 그 PR 자신이 열려 있다)
55

66
## 완료
7+
- [rustjava-adopt-link-stringconcatfactory-p0] ★★**`StringConcatFactory.makeConcat` 도 링크한다 — 단 «이유는 제안이 적은 것이 아니다».**
8+
채택 제안 `2026-09-16-link-stringconcatfactory#p0`(worklog json `adoptedProposals` 기록).
9+
★**제품 동작 변경**: `makeConcat` 콜사이트가 **거부 대신 실행**된다. ★**실행기(`concat_with_constants`)는 한 줄도 안 바뀌었다.**
10+
★★**ⓒ 제안의 전제가 «거짓»이다**(실측 javac 26.0.1 · 같은 소스 `a + b`):
11+
`--release` **9 · 11 · 17 · 21 · 26 전부 `makeConcatWithConstants`** — ★**상수 텍스트가 «없는» 경우에도** 그렇다
12+
(레시피가 «자리표시자 둘 · 리터럴 0»일 뿐이다). `makeConcat` 은 **비기본 내부 플래그 `-XDstringConcat=indy`** 에서만 나오고,
13+
`-XDstringConcat=inline` 은 `StringBuilder` 를 낸다.
14+
⇒ ★**「javac 이 상수 텍스트 없이 연결할 때 쓴다」도, 「javac 산출물이 더 많이 돈다」도 거짓**이다 —
15+
★**기본 javac 산출물은 이미 «전부» 링크되고 있었다.**
16+
★★**그래도 한 이유를 «바꿔서» 적는다**(제안의 근거를 그대로 베끼지 않았다):
17+
⑴★**만들 수 있는 형상**이다 — 그 플래그로 **직접 만들었다**. 이 리니지가 `ldc` 태그 지원을 정당화한 기준
18+
(「ASM 이 실제로 낸다」)과 ★**같은 기준**이다 ⑵**문서화된 공개 진입점**이라 바이트코드 생성기가 택할 수 있다
19+
⑶★★**실행기에 «새 경로»가 필요 없다** — `makeConcat(n개 인자)` ≡ 레시피가 `\u{1}` **n개**인 `makeConcatWithConstants` 다
20+
⇒ 콜사이트 서술자의 인자 수로 **레시피를 합성**하면 끝. ★제안이 「a recipe-free path through the same executor」로 본 것보다 **더 싸다**.
21+
★**설계 — 제안의 「짧은 명시 목록을 유지하라」를 지켰다**: 상수 튜플 **둘**뿐 · **레지스트리 아님** ·
22+
★**이름과 서술자를 «쌍»으로 매칭**한다 ⇒ ★**#55 가 넣은 name 축 근접 실패(`NotMakeConcatWithConstants`)가 «살아 있다»**
23+
(그 파일은 이름만 `makeConcat` 이고 서술자는 `makeConcatWithConstants` 의 것이라 여전히 거부된다).
24+
★**레시피 합성은 «콜사이트마다»** 한다 — 한 부트스트랩 항목을 **서술자가 다른 여러 콜사이트가 공유**할 수 있어
25+
해석 단계에서 고정할 수 없다(그래서 `LinkedFactory::{WithConstants, NoRecipe}` 로 «인식»만 한다).
26+
★★**개악 대조 4종 — 처음엔 «둘»이 살아남았고, 그것이 이 회차의 교훈이다**:
27+
**M1** 튜플 제거 → KILLED · **M2** 레시피 길이 `repeat(1)` → KILLED · ★**M3** 쌍 검사를 «이름만»으로 → **SURVIVED** ·
28+
★**M4** 「정적 인자 없음」 가드 제거 → **SURVIVED**.
29+
⇒ ★**직전 `…-p0-fix` 가 세운 「죽었다 ≠ 그 가지가 «전부» 덮였다」가 «내 새 코드»에 그대로 적용됐다** —
30+
축마다 근접 실패를 더해(`MakeConcatWrongDescriptor` = 서술자만 다르고 정적 인자 0 · `MakeConcatWithArgument` = 이름·서술자 맞고 정적 인자 1)
31+
★**4종 전건 KILLED** 로 만들었다.
32+
★**픽스처가 «출력»한다**(`ab`) — ★**레시피를 틀린 길이로 합성해도 링크되고 실행된다.** 값을 버리면 그 오류가 «안 보인다»
33+
(M2 가 그 증거다) ⇒ 「링크됐다」가 아니라 **「무엇이 연결됐나」**를 단언한다.
34+
★**잃는 것**: 링크 수용 범위가 넓어졌다 — 「콜사이트 «하나»를 링크한다」가 이제 **둘**이다(제안이 경고한 그대로 · 명시 목록으로 억제).
35+
★`cargo test --all` **573 → 574 / 0 failed / 1 ignored** · 픽스처 재생성 **멱등** · DoD **7줄 전건 rc=0**.
36+
★★**게이트③ 착지 — PR #59 · `--merge`**(등재 repo · `merge_strategy: merge` 선언분). 게이트② **approve** ·
37+
핀 `3b04712e` **불이동**(착수 실측 2026-09-17T01:40:32Z) · ★**`MERGEABLE/CLEAN` · base 뒤처짐 «0»** ⇒ 충돌 해소·base 당김 **둘 다 불요**.
38+
★핀에서 `ci-presence` **rc=0 CI_GREEN** · 자식 PR **0건** · 배포 **0**(이 저장소에 배포 워크플로 없음) · 주기 자동 커밋 **0건**.
39+
★**동봉은 이 기록 한 줄뿐** — 원장(worklog 쌍·`STATE`·`REPORT`)은 구현 회차가 이미 실었다.
40+
★★**착지 순서 고지 — 이 회차가 «새 `.class` 3개»를 들여온다**(`MakeConcat`·`MakeConcatWithArgument`·`MakeConcatWrongDescriptor`).
41+
열린 PR **#57**(ci-pending)이 「`.class` 미등재는 핀이 **실패**시킨다」(`test-data/class-file-versions.txt`)를 세우므로,
42+
★**이 PR 이 먼저 착지하면 #57 의 표에 이 셋이 «없어»** 그 회차가 red 가 된다(해소 = `record-class-file-versions.py` 재생성).
43+
★**텍스트 충돌 0 이라 `mergeable` 로는 보이지 않는다** — 같은 고지가 #60 회신에도 있다(그쪽은 다른 3개).
744
- [rustjava-adopt-cp-tag-passthrough-detectable-p0-fix] ★★**신원 4축을 «각각» 관측 가능하게 했다 — 감사의 「고칠 것이 없다」를 정정한다.**
845
게이트② **request-changes** 승계(PR #55 · 핀 `ab13a3c7`). ★**제품 코드 무접촉** — 없던 것은 **픽스처**다.
946
★★**무엇이 틀렸나**: 직전 감사의 **M7**(「신원 4축 검사 제거」)은 네 비교를 ★**한꺼번에** 지운다 ⇒ 그 red 가 증명하는 것은
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"schema": "worklog/v1",
3+
"date": "2026-09-17",
4+
"taskId": "rustjava-adopt-link-stringconcatfactory-p0",
5+
"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.",
6+
"changes": [
7+
"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",
8+
"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",
9+
"test-data/indy/MakeConcat.class, MakeConcatWrongDescriptor.class, MakeConcatWithArgument.class: new fixtures",
10+
"tests/test_class_format.rs: test_the_recipe_free_factory_concatenates_every_argument, plus the two new near misses in the identity-axis test"
11+
],
12+
"verification": [
13+
"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",
14+
"end to end: the fixture prints 'ab', so the synthesised recipe consumed both arguments in order",
15+
"mutation M1, removing the makeConcat pair: red",
16+
"mutation M2, synthesising a recipe of the wrong length: red — which is why the fixture prints rather than discarding the value",
17+
"mutation M3, loosening the pair check to the name alone: SURVIVED at first, killed after adding MakeConcatWrongDescriptor",
18+
"mutation M4, dropping the no-static-arguments guard: SURVIVED at first, killed after adding MakeConcatWithArgument",
19+
"the near-miss fixtures added by PR #55 still refuse, because name and descriptor are compared as a pair",
20+
"cargo test --all --no-fail-fast: 574 passed / 0 failed / 1 ignored, up from 573",
21+
"fixture regeneration is idempotent: the four pre-existing indy fixtures come out byte-identical",
22+
"DoD 7 commands all rc=0"
23+
],
24+
"issues": [
25+
"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.",
26+
"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.",
27+
"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."
28+
],
29+
"adoptedProposals": [
30+
"2026-09-16-link-stringconcatfactory#p0"
31+
],
32+
"proposals": []
33+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# 2026-09-17 — `StringConcatFactory.makeConcat` 도 링크한다 — 단 «이유는 제안이 적은 것이 아니다»
2+
3+
티켓 `rustjava-adopt-link-stringconcatfactory-p0` — 채택 제안 `2026-09-16-link-stringconcatfactory#p0`.
4+
5+
★**제품 동작이 바뀐다**: `makeConcat` 부트스트랩에 묶인 `invokedynamic` 콜사이트가 이제 **실행된다**
6+
(종전 `UnsupportedOperationException: invokedynamic`).
7+
8+
## ⓒ ★제안의 전제는 «틀렸다» — 그리고 그 사실이 이 회차의 절반이다
9+
10+
제안 `plainSummary`: 「the sibling factory with no recipe — **used when javac concatenates without constant text**」
11+
제안 `userBenefit`: 「**More javac-emitted** string concatenation runs instead of being refused」
12+
13+
★**실측(javac 26.0.1 · 같은 소스 `a + b`)**:
14+
15+
| 대상 | 내는 부트스트랩 |
16+
|---|---|
17+
| `--release 9` · `11` · `17` · `21` · `26` | ★**전부 `makeConcatWithConstants`** |
18+
| `-XDstringConcat=indy`(비기본 내부 플래그) | `makeConcat` |
19+
| `-XDstringConcat=inline` | `StringBuilder` |
20+
21+
★**상수 텍스트가 «없는» `a + b` 도 `makeConcatWithConstants` 다** — 레시피가 자리표시자 둘(``)일 뿐 리터럴이 0인 것이다.
22+
⇒ ★**「javac 이 상수 텍스트 없이 연결할 때 쓴다」는 거짓**이고, ★**「javac 산출물이 더 많이 돈다」도 거짓**이다
23+
(기본 javac 산출물은 **이미 전부 링크된다**).
24+
25+
## 그래도 «왜» 했나 — 근거를 바꿔서 적는다
26+
27+
⑴★**만들 수 있는 형상이다** — `-XDstringConcat=indy` 로 **실제로 만들었다**.
28+
이 리니지가 `ldc` 태그 지원을 정당화한 기준(「ASM 이 실제로 낸다」)과 **같은 기준**이다.
29+
⑵★**`makeConcat` 은 `StringConcatFactory` 의 «문서화된 공개 진입점»** 이고, 바이트코드 생성기는 그것을 택할 수 있다.
30+
⑶★★**실행기에 «새 경로가 필요 없다»** — `makeConcat(n개 인자)` ≡ 레시피가 `\u{1}` **n개**인 `makeConcatWithConstants` 다.
31+
⇒ 콜사이트 서술자의 인자 수로 **레시피를 합성**하면 끝이고, `concat_with_constants` 는 **한 줄도 안 바뀐다**.
32+
★제안이 「a recipe-free path through the same executor」라고 본 것보다 **더 싸다**(경로가 아예 안 생긴다).
33+
34+
## 설계 — 제안의 「짧은 명시 목록을 유지하라」를 지켰다
35+
36+
- 상수 튜플 **둘**(이름+서술자 쌍)뿐이고 **레지스트리로 만들지 않았다**(제안 tradeoff 준수).
37+
- ★**이름과 서술자를 «쌍»으로 맞춘다** — `(name, descriptor)` 를 함께 매칭하므로
38+
★**한쪽 진입점의 이름에 다른 쪽의 서술자를 붙인 것은 링크되지 않는다.**
39+
⇒ 이 결정이 ★**#55 가 넣은 name 축 근접 실패 픽스처(`NotMakeConcatWithConstants`)를 «살렸다»** — 그 파일은
40+
이름이 `makeConcat` 이지만 서술자가 `makeConcatWithConstants` 의 것이라 **여전히 거부된다**.
41+
- ★**레시피 합성은 «콜사이트마다»** 한다 — 한 부트스트랩 항목을 서술자가 다른 여러 콜사이트가 공유할 수 있어
42+
부트스트랩 해석 단계에서는 **고정할 수 없다**. 그래서 해석은 `LinkedFactory::{WithConstants, NoRecipe}` 로 «인식»만 하고,
43+
합성은 하강 루프에서 한다.
44+
45+
## 픽스처 — ★**결과를 «출력»하게 만들었다**(이유가 있다)
46+
47+
`test-data/indy/MakeConcat.class`(생성기 · 합성): `ldc "a"; ldc "b"; invokedynamic concat(String,String)String; println`.
48+
★**「링크됐다」가 아니라 「무엇이 연결됐나」를 단언**한다 — ★**레시피를 «틀린 길이»로 합성해도 링크는 되고 실행도 된다.**
49+
값을 버리면 그 오류가 **안 보인다**(실측: 아래 M2).
50+
★**javac 이 안 내는 형상이라 손으로 조립했다** — 비기본 내부 플래그에 픽스처 재생성을 의존시키는 것이 더 나쁘다.
51+
52+
## ★★개악 대조 — 4종 중 «둘»이 처음엔 살아남았고, 그래서 픽스처를 더 만들었다
53+
54+
| 개악 | 처음 | 픽스처 보강 후 |
55+
|---|---|---|
56+
| **M1** `makeConcat` 튜플 제거(= 제안 이전) | **KILLED** | KILLED |
57+
| **M2** 레시피 길이를 `repeat(1)` 로 | **KILLED**(출력 대조가 잡는다) | KILLED |
58+
| **M3** 쌍 검사를 느슨하게(`(name, _)`) | ★**SURVIVED** | ★**KILLED** |
59+
| **M4** 「정적 인자 없음」 가드 제거 | ★**SURVIVED** | ★**KILLED** |
60+
61+
★**M3·M4 가 살아남은 이유는 «축마다 픽스처가 없어서»다** — 직전 `…-p0-fix` 회차가 세운
62+
「**죽었다 ≠ 그 가지가 «전부» 덮였다**」가 ★**내 새 코드에 그대로 적용됐다.**
63+
⇒ 근접 실패 둘을 더했다: `MakeConcatWrongDescriptor`(이름은 맞고 **서술자만** 다르며 **정적 인자 0** ⇒ 서술자 비교만이 거부할 수 있다) ·
64+
`MakeConcatWithArgument`(이름·서술자 맞고 **정적 인자 1** ⇒ 인자 수 가드만이 거부할 수 있다).
65+
★**둘 다 «그 축 하나»만 다르다** — 그래야 그 축이 관측된다.
66+
67+
## 잃는 것 / 안 하면 무엇이 나쁜가
68+
69+
⒜**잃는 것**: ⑴★**링크 수용 범위가 넓어진다** — 제안이 경고한 그대로이고, 「이 런타임은 콜사이트 «하나»를 링크한다」가
70+
더 이상 참이 아니다(이제 **둘**이다). ⇒ 상수 튜플을 **명시 목록**으로 유지하고 doc 에 그 이유를 적었다.
71+
⑵픽스처 **3개** · 테스트 1개 · `cargo test --all` **573 → 574**.
72+
⑶`resolve_bootstrap_methods` 의 반환형이 튜플 → 작은 enum 으로 바뀌었다(호출부 1곳).
73+
⒝**안 하면**: `makeConcat` 에 묶인 콜사이트는 계속 **「이 런타임이 아직 못 한다」**로 거부된다 —
74+
★**실행기는 그것을 «이미 할 수 있는데»** 그렇다(레시피 합성 한 줄이면 되는 일이었다).

0 commit comments

Comments
 (0)