Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ for f in sorted(glob.glob('docs/worklog/*.json')):
print(*[r for r in refs if r not in done],sep='\n')"
```

1. **선행 사슬**: `java.lang.invoke` 패키지(카드 없음 · 근거 = `rustjava-runtime/src/classes/java/lang/invoke` **부재** · ★다음 칸) → `2026-09-17-string-concat-recipe-arity#p1`. 앞 칸 `2026-09-17-link-lambdametafactory#p0`(어댑터)은 2026-09-27 에 `int`↔`Integer` 한 쌍으로 닫혔다(`docs/worklog/2026-09-27-lambda-int-integer-adapter.md`). 다른 쌍은 이 사슬의 칸이 아니다. 필요한 게스트가 생기면 한 쌍씩 새 카드로 연다.
1. **선행 사슬**: 없음. 마지막 칸 `java.lang.invoke` 패키지 → `2026-09-17-string-concat-recipe-arity#p1` 은 2026-09-29 에 닫혔다(`docs/worklog/2026-09-29-string-concat-exception-as-cause.md`). 패키지에는 `StringConcatException` 하나만 있다 — `MethodHandle`·`CallSite` 는 쓰는 게스트가 생길 때 카드로 연다.
2. **카드 밖**: 없음. PR #81 은 2026-09-26 에 닫혔다 — #83(`750d30d4`)과 같은 변경이었다(`docs/worklog/2026-09-26-pr81-closed-duplicate-of-pr83.md`).

지난 판(⓪-사료·①~⑤)은 동결된 `STATE.md` `## 다음` 아래에 그대로 있다.
7 changes: 7 additions & 0 deletions docs/worklog/2026-09-29-string-concat-exception-as-cause.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"date": "2026-09-29",
"taskId": "rustjava-java-lang-invoke-package-and-linkage-error-cause",
"summary": "Mismatched string concat recipes raise BootstrapMethodError caused by java.lang.invoke.StringConcatException, as on OpenJDK 26.",
"proposals": [],
"adoptedProposals": ["2026-09-17-string-concat-recipe-arity#p1"]
}
20 changes: 20 additions & 0 deletions docs/worklog/2026-09-29-string-concat-exception-as-cause.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## [2026-09-29] 문자열 연결 레시피 불일치 — 상세를 cause(StringConcatException)로 싣는다 (rustjava-java-lang-invoke-package-and-linkage-error-cause)
- 무엇을: `java.lang.invoke` 패키지를 만들고 `StringConcatException` 한 클래스만 넣었다(`<init>(String)` 만 — `Jvm::exception` 이 부르는 것). `concat_with_constants`(`jvm-bytecode/src/interpreter.rs`)가 불일치 시 `BootstrapMethodError: bootstrap method initialization exception` 을 던지고 상세는 `initCause` 로 단 `StringConcatException` 에 싣는다. 인자 → 상수 순으로 보고, 문구는 OpenJDK 26 것이다.
- 왜: `2026-09-17-string-concat-recipe-arity#p1` 채택. 선행 회차(`rustjava-linkage-errors-carry-their-cause`, 2026-09-18 blocked)가 막힌 이유였던 «cause 로 지목할 클래스 부재» 를 풀었다. cause 배관(`Throwable.initCause`·`printStackTrace` 의 `Caused by:`)은 이미 있었다.
- 사용자 영향: 오류 출력이 실 JVM 과 같은 두 줄 모양이 된다. 판정 경계(무엇을 거부하나)는 그대로다.

### 실측 (OpenJDK 26.0.2.1 ↔ 이 런타임, `test-data/indy/RecipeWants*.class`)
| 픽스처 | OpenJDK cause | 이 런타임 cause |
|---|---|---|
| `RecipeWantsFewerArguments` | arguments: wants 1, provides 2 | 같음 |
| `RecipeWantsMoreArguments` | arguments: wants **1**, provides 1 | arguments: wants **2**, provides 1 |
| `RecipeWantsAConstant` | constants: wants **0**, only 0 | constants: wants **1**, only 0 |

숫자가 다른 두 줄은 일부러 맞추지 않았다. OpenJDK 는 파서가 멈춘 시점의 계수를 찍어서 파일을 설명하지 못한다. 문구와 cause 타입은 같다.

### 시험
- `test_a_recipe_that_contradicts_its_call_site_is_a_bootstrap_method_error` 가 부분문자열 대신 머리줄 3개(`Java Exception:` · 바깥 오류 · `Caused by:` 줄)를 정확히 단언한다.
- 수정 전 런타임: red(cause 줄 없음, 상세가 바깥 메시지에 있음). 변이 `initCause` 호출 제거: red(cause 줄 없음).

### 옮기지 않은 것
`lambda.rs`·`string_concat.rs` 의 `java/lang/invoke/LambdaMetafactory`·`StringConcatFactory` 는 부트스트랩 이름을 대조하는 상수이고, 클래스로 싣는 곳이 없다. 그래서 이 패키지로 옮길 것이 없다. `MethodHandle`·`CallSite` 는 쓰는 곳이 없어 만들지 않았다.
48 changes: 36 additions & 12 deletions jvm-bytecode/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1018,7 +1018,7 @@ impl Interpreter {
/// Execute a `StringConcatFactory.makeConcatWithConstants` call site.
///
/// A real JVM asks the factory for a `CallSite` holding a `MethodHandle` chain and invokes it.
/// There is no `java.lang.invoke` package here — no `MethodHandle`, no `CallSite` — so the
/// `java.lang.invoke` here holds only `StringConcatException` — no `MethodHandle`, no `CallSite` — so the
/// recipe is walked directly instead. That is the whole reason this is *one* linked bootstrap
/// rather than a linkage mechanism: the factory's contract is a string template, and a string
/// template can be honoured without the machinery that would normally deliver it.
Expand Down Expand Up @@ -1047,17 +1047,41 @@ impl Interpreter {
async fn concat_with_constants(jvm: &Jvm, call_site: &StringConcatCallSite, params: Vec<JavaValue>) -> Result<Box<dyn ClassInstance>> {
let wanted_params = call_site.recipe.chars().filter(|&x| x == '\u{1}').count();
let wanted_constants = call_site.recipe.chars().filter(|&x| x == '\u{2}').count();
if wanted_params != params.len() || wanted_constants != call_site.constants.len() {
return Err(jvm
.exception(
"java/lang/BootstrapMethodError",
&format!(
"string concat recipe wants {wanted_params} arguments and {wanted_constants} constants, but the call site provides {} and the bootstrap {}",
params.len(),
call_site.constants.len()
),
)
.await);
// OpenJDK 26's wording, arguments checked first. Its numbers are the ones this counted; OpenJDK's
// own counts come from a parser that stops at the first surplus, so they can differ.
let detail = if wanted_params != params.len() {
format!(
"Mismatched number of concat arguments: recipe wants {wanted_params} arguments, but signature provides {}",
params.len()
)
} else if wanted_constants != call_site.constants.len() {
format!(
"Mismatched number of concat constants: recipe wants {wanted_constants} constants, but only {} are passed",
call_site.constants.len()
)
} else {
String::new()
};
if !detail.is_empty() {
let cause = match jvm.exception("java/lang/invoke/StringConcatException", &detail).await {
JavaError::JavaException(x) => x,
e => return Err(e),
};
let error = jvm
.exception("java/lang/BootstrapMethodError", "bootstrap method initialization exception")
.await;
if let JavaError::JavaException(instance) = &error {
let _: Box<dyn ClassInstance> = jvm
.invoke_virtual(
instance,
"java/lang/Throwable",
"initCause",
"(Ljava/lang/Throwable;)Ljava/lang/Throwable;",
(cause,),
)
.await?;
}
return Err(error);
}

let mut result = String::new();
Expand Down
2 changes: 2 additions & 0 deletions rustjava-runtime/src/classes/java/lang.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod invoke;

mod abstract_method_error;
mod abstract_string_builder;
mod appendable;
Expand Down
3 changes: 3 additions & 0 deletions rustjava-runtime/src/classes/java/lang/invoke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod string_concat_exception;

pub use self::string_concat_exception::StringConcatException;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use alloc::vec;

use jvm::{ClassInstanceRef, Jvm, Result};
use jvm_class_proto::JavaMethodProto;
use jvm_types::{ClassAccessFlags, MethodAccessFlags};

use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String};

// class java.lang.invoke.StringConcatException
// Only the constructor `Jvm::exception` calls: this class exists to be the cause of the
// BootstrapMethodError a mismatched string concat recipe raises (jvm-bytecode/src/interpreter.rs).
pub struct StringConcatException;

impl StringConcatException {
pub fn as_proto() -> RuntimeClassProto {
RuntimeClassProto {
name: "java/lang/invoke/StringConcatException",
parent_class: Some("java/lang/Exception"),
interfaces: vec![],
methods: vec![JavaMethodProto::new(
"<init>",
"(Ljava/lang/String;)V",
Self::init_with_message,
MethodAccessFlags::PUBLIC,
)],
fields: vec![],
access_flags: ClassAccessFlags::PUBLIC,
}
}

async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>, message: ClassInstanceRef<String>) -> Result<()> {
tracing::debug!("java.lang.invoke.StringConcatException::<init>({this:?}, {message:?})");

jvm.invoke_special(&this, "java/lang/Exception", "<init>", "(Ljava/lang/String;)V", (message,))
.await
}
}
1 change: 1 addition & 0 deletions rustjava-runtime/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option<RuntimeClassProto> {
crate::classes::java::lang::ArrayStoreException::as_proto(),
crate::classes::java::lang::Boolean::as_proto(),
crate::classes::java::lang::BootstrapMethodError::as_proto(),
crate::classes::java::lang::invoke::StringConcatException::as_proto(),
crate::classes::java::lang::Class::as_proto(),
crate::classes::java::lang::ClassCastException::as_proto(),
crate::classes::java::lang::ClassCircularityError::as_proto(),
Expand Down
39 changes: 27 additions & 12 deletions tests/test_class_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,22 +189,37 @@ async fn test_each_axis_of_the_factory_identity_is_observable() {
// and printed a quietly wrong "a" instead of refusing.
#[tokio::test]
async fn test_a_recipe_that_contradicts_its_call_site_is_a_bootstrap_method_error() {
for (name, disagreement) in [
("RecipeWantsMoreArguments", "recipe wants two arguments, the call site provides one"),
("RecipeWantsFewerArguments", "recipe wants one argument, the call site provides two"),
("RecipeWantsAConstant", "recipe wants a constant the bootstrap did not carry"),
// The detail rides on the cause, as on OpenJDK 26. The numbers are this runtime's own counts: for
// RecipeWantsMoreArguments and RecipeWantsAConstant OpenJDK prints the counts its parser had reached
// when it stopped ("wants 1 ..., provides 1" / "wants 0 constants, but only 0"), which describe
// nothing about the file.
for (name, cause) in [
(
"RecipeWantsMoreArguments",
"java.lang.invoke.StringConcatException: Mismatched number of concat arguments: recipe wants 2 arguments, but signature provides 1",
),
(
"RecipeWantsFewerArguments",
"java.lang.invoke.StringConcatException: Mismatched number of concat arguments: recipe wants 1 arguments, but signature provides 2",
),
(
"RecipeWantsAConstant",
"java.lang.invoke.StringConcatException: Mismatched number of concat constants: recipe wants 1 constants, but only 0 are passed",
),
] {
let path = PathBuf::from(format!("test-data/indy/{name}.class"));

let err = run_class(&path, &[Path::new("./test-data/indy/")], &[]).await.unwrap_err().to_string();

assert!(
err.contains("java.lang.BootstrapMethodError"),
"{name} ({disagreement}): expected the linkage diagnosis, got: {err}"
);
assert!(
!err.contains("ClassFormatError") && !err.contains("UnsupportedOperationException"),
"{name}: the file parses and the bootstrap is one we link, got: {err}"
let headers: Vec<&str> = err.lines().filter(|x| !x.starts_with('\t')).collect();

assert_eq!(
headers,
[
"Java Exception:",
"java.lang.BootstrapMethodError: bootstrap method initialization exception",
&format!("Caused by: {cause}"),
],
"{name}"
);
}
}
Expand Down
Loading