From a908bd1470ed6fe8620234024956ecfbed2a535b Mon Sep 17 00:00:00 2001 From: jun0 Date: Sun, 27 Sep 2026 14:06:19 +0900 Subject: [PATCH] =?UTF-8?q?[rustjava-2026-09-17-link-lambdametafactory-box?= =?UTF-8?q?ing-adapter-adopt-p0]=20feat:=20=EB=9E=8C=EB=8B=A4=20=EB=A7=81?= =?UTF-8?q?=EC=BB=A4=EC=97=90=20int=E2=86=94Integer=20=EC=96=B4=EB=8C=91?= =?UTF-8?q?=ED=84=B0=20=E2=80=94=20=ED=95=9C=20=EC=8C=8D=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jvm-bytecode/src/lambda.rs 의 통과 전용 술어를 위치별 계획(plan)으로 바꿨다. 통과는 종전 passes 를 먼저 묻고, 그 밖에는 int↔Integer 한 쌍만 삽입한다 (Integer.valueOf / Integer 검사 후 intValue · null → NPE · 다른 타입 → CCE). 언박싱 판정은 instantiatedMethodType 을 읽는다. LambdaCallSite 에 그 필드를 추가. - LambdaBoxing: 거부 → 3 (OpenJDK 26 과 동일) - LambdaBoxingKinds(신규): 위치 5 + 예외 3, OpenJDK 출력과 줄 단위 일치 - LambdaBoxingLong·LambdaUnboxingShort(신규): 여전히 거부 - 변이 6종 전부 red Co-Authored-By: Claude Opus 5.5 --- classfile/src/opcode.rs | 4 + docs/next.md | 2 +- ...2026-09-27-lambda-int-integer-adapter.json | 7 + .../2026-09-27-lambda-int-integer-adapter.md | 27 +++ jvm-bytecode/src/lambda.rs | 204 +++++++++++++++--- test-data/class-file-versions.txt | 9 + test-data/indy/LambdaBoxing.class | Bin 1059 -> 1059 bytes test-data/indy/LambdaBoxingKinds$Fn.class | Bin 0 -> 355 bytes test-data/indy/LambdaBoxingKinds$IntGen.class | Bin 0 -> 224 bytes .../indy/LambdaBoxingKinds$IntSink.class | Bin 0 -> 230 bytes test-data/indy/LambdaBoxingKinds$Src.class | Bin 0 -> 314 bytes test-data/indy/LambdaBoxingKinds.class | Bin 0 -> 3067 bytes test-data/indy/LambdaBoxingLong$Gen.class | Bin 0 -> 232 bytes test-data/indy/LambdaBoxingLong.class | Bin 0 -> 1085 bytes test-data/indy/LambdaUnboxingShort$Fn.class | Bin 0 -> 361 bytes test-data/indy/LambdaUnboxingShort.class | Bin 0 -> 1219 bytes test-data/src/indy/LambdaBoxing.java | 16 +- test-data/src/indy/LambdaBoxingKinds.java | 89 ++++++++ test-data/src/indy/LambdaBoxingLong.java | 21 ++ test-data/src/indy/LambdaUnboxingShort.java | 23 ++ tests/test_class_format.rs | 74 +++++-- 21 files changed, 414 insertions(+), 62 deletions(-) create mode 100644 docs/worklog/2026-09-27-lambda-int-integer-adapter.json create mode 100644 docs/worklog/2026-09-27-lambda-int-integer-adapter.md create mode 100644 test-data/indy/LambdaBoxingKinds$Fn.class create mode 100644 test-data/indy/LambdaBoxingKinds$IntGen.class create mode 100644 test-data/indy/LambdaBoxingKinds$IntSink.class create mode 100644 test-data/indy/LambdaBoxingKinds$Src.class create mode 100644 test-data/indy/LambdaBoxingKinds.class create mode 100644 test-data/indy/LambdaBoxingLong$Gen.class create mode 100644 test-data/indy/LambdaBoxingLong.class create mode 100644 test-data/indy/LambdaUnboxingShort$Fn.class create mode 100644 test-data/indy/LambdaUnboxingShort.class create mode 100644 test-data/src/indy/LambdaBoxingKinds.java create mode 100644 test-data/src/indy/LambdaBoxingLong.java create mode 100644 test-data/src/indy/LambdaUnboxingShort.java diff --git a/classfile/src/opcode.rs b/classfile/src/opcode.rs index 69ccd4d2..0b1f74b4 100644 --- a/classfile/src/opcode.rs +++ b/classfile/src/opcode.rs @@ -35,6 +35,10 @@ pub struct LambdaCallSite { /// `samMethodType`) — that is the one callers invoke through the interface. pub method_name: Arc, pub method_descriptor: Arc, + /// The same method with its generic type arguments filled in (the bootstrap's + /// `instantiatedMethodType`). Read only where an adapter converts a value: it is what says an + /// erased `Object` parameter is an `Integer` to unbox rather than, say, a `Short`. + pub instantiated_method_descriptor: Arc, /// The method the interface method delegates to (the bootstrap's `implMethod`). pub implementation: MethodHandleRef, } diff --git a/docs/next.md b/docs/next.md index 5d32514d..8f1a3b42 100644 --- a/docs/next.md +++ b/docs/next.md @@ -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. **선행 사슬**: `2026-09-17-link-lambdametafactory#p0`(어댑터 · ★지금 착수 가능 — `#p1` 은 2026-09-26 「현 상태 유지」로 닫혔고 등록 방식이 안 바뀌므로 선행 조건 없음 · `docs/lambda-class-reflection-visibility.md`) → `java.lang.invoke` 패키지(카드 없음 · 근거 = `rustjava-runtime/src/classes/java/lang/invoke` **부재**) → `2026-09-17-string-concat-recipe-arity#p1`. +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`). 다른 쌍은 이 사슬의 칸이 아니다. 필요한 게스트가 생기면 한 쌍씩 새 카드로 연다. 2. **카드 밖**: 없음. PR #81 은 2026-09-26 에 닫혔다 — #83(`750d30d4`)과 같은 변경이었다(`docs/worklog/2026-09-26-pr81-closed-duplicate-of-pr83.md`). 지난 판(⓪-사료·①~⑤)은 동결된 `STATE.md` `## 다음` 아래에 그대로 있다. diff --git a/docs/worklog/2026-09-27-lambda-int-integer-adapter.json b/docs/worklog/2026-09-27-lambda-int-integer-adapter.json new file mode 100644 index 00000000..6367ac65 --- /dev/null +++ b/docs/worklog/2026-09-27-lambda-int-integer-adapter.json @@ -0,0 +1,7 @@ +{ + "date": "2026-09-27", + "taskId": "rustjava-2026-09-17-link-lambdametafactory-boxing-adapter-adopt-p0", + "summary": "LambdaMetafactory linker inserts the int <-> Integer adapter; every other conversion pair stays refused.", + "proposals": [], + "adoptedProposals": ["2026-09-17-link-lambdametafactory#p0"] +} diff --git a/docs/worklog/2026-09-27-lambda-int-integer-adapter.md b/docs/worklog/2026-09-27-lambda-int-integer-adapter.md new file mode 100644 index 00000000..43dc14e0 --- /dev/null +++ b/docs/worklog/2026-09-27-lambda-int-integer-adapter.md @@ -0,0 +1,27 @@ +## [2026-09-27] 람다 링커에 int↔Integer 어댑터 — 한 쌍만 (rustjava-2026-09-17-link-lambdametafactory-boxing-adapter-adopt-p0) +- 무엇을: `jvm-bytecode/src/lambda.rs` 의 통과 전용 술어 `adapts` 를 위치별 계획 `plan` 으로 바꿨다. 통과(pass-through)는 종전 술어 `passes` 를 그대로 먼저 묻고, 그 밖에는 `int`↔`Integer` 한 쌍만 삽입한다 — 나갈 때 `Integer.valueOf`, 들어올 때 `Integer` 검사 후 `intValue`(null → NPE · 다른 타입 → CCE, 실제 팩토리와 같은 예외). 링크 여부와 실행 시 변환이 **같은 함수**에서 나온다. 부트스트랩 셋째 인자 `instantiatedMethodType` 을 `LambdaCallSite` 에 실었다 — 지워진 `Object` 가 `Integer` 인지 `Short` 인지는 그것만 말한다. +- 왜: `2026-09-17-link-lambdametafactory#p0` 채택. `Supplier`·`Function` 모양이 원시값 구현에 닿는 자리가 전부 거부되고 있었다. 한 쌍으로 좁힌 이유 = 카드의 tradeoff(잘못 허용한 쌍은 거부를 틀린 답으로 바꾼다). +- 사용자 영향: 박싱이 필요한 람다·메서드 참조가 로드·실행된다(OpenJDK 26.0.2.1 과 출력 일치). 그 밖의 쌍(`long` 박싱 · 언박싱 후 확장 · `Number` 로 박싱)은 여전히 거부된다. + +### 실측 +| 무엇 | 결과 | +|---|---| +| `LambdaBoxing`(종전 거부 픽스처) | 수정 전 거부 → 수정 후 `3`(OpenJDK 와 동일) | +| `LambdaBoxingKinds`(신규 · 위치 5 + 예외 3) | `4 / 42 / 7 / obj:5 / integer:6 / npe in / cce / npe out` — OpenJDK 26.0.2.1 출력과 줄 단위 일치 | +| `LambdaBoxingLong`·`LambdaUnboxingShort`(신규 · 거부 유지) | 둘 다 `UnsupportedOperationException … invokedynamic` (OpenJDK 는 `5000000000`·`42`) | +| 통과 경우 기존 시험(`Lambda`·`LambdaKinds`·`LambdaCapturingThis`) | 불변 green | +| `verify-javac-fixtures.sh`(javac 26.0.2.1) | 33/33 재현 | + +### 되돌리면 red (변이 6종 · 전부 1건 red) +| 변이 | red 가 된 시험 | +|---|---| +| M1 인자 박싱 제거 | int↔Integer 시험 | +| M1b 반환 박싱 제거 | int↔Integer 시험 (`LambdaBoxing`) | +| M2 언박싱에서 instantiated 가 `Integer` 인지 안 봄 | 거부 시험 (`LambdaUnboxingShort` 가 링크된다) | +| M3 `long` 도 박싱 허용 | 거부 시험 (`LambdaBoxingLong`) | +| M4 언박싱 전 `Integer` 검사 제거 | int↔Integer 시험 (`cce` 줄) | +| M5 반환 언박싱 제거 | int↔Integer 시험 | + +### 범위 밖(카드 없음) +- 다른 쌍은 쌍마다 따로 추가한다. 지금은 필요로 하는 게스트가 없다. wie 게스트 jar 는 major 47 이라 `invokedynamic` 에 닿지 않는다(`2026-09-26-lambda-class-reflection-visibility`). 그래서 제안 카드를 만들지 않았다. +- 캡처 값은 어댑트하지 않는다(통과가 아니면 거부). javac 는 구현 타입 그대로 캡처한다. diff --git a/jvm-bytecode/src/lambda.rs b/jvm-bytecode/src/lambda.rs index 05c5d12a..2997cbdd 100644 --- a/jvm-bytecode/src/lambda.rs +++ b/jvm-bytecode/src/lambda.rs @@ -17,15 +17,25 @@ //! That is the whole of the shortcut: the object is real, the dispatch is real, and the thing that //! does not exist is the handle chain that would normally deliver them. //! -//! ## What is not linked, and why that is a refusal rather than a gap +//! ## What is linked, and why everything else is a refusal rather than a gap //! //! The implementation's signature has to line up with the interface method's, position by -//! position, as a **pass-through**: identical primitives, or a reference either way. Where the real -//! factory would insert an adapter — boxing an `int` into an `Object`, unboxing, widening — the +//! position. Most positions are a **pass-through**: identical primitives, or a reference either +//! way. The one adapter inserted is the `int` ↔ `Integer` pair, the conversion the generic +//! functional interfaces force (`Supplier` reaching a method that returns `int`): +//! `Integer.valueOf` where an `int` meets a reference, `intValue` after a check that the value is +//! an `Integer` where a reference meets an `int`. [`Adapter`] lists it and [`parameter_adapter`] / +//! [`return_adapter`] say where it applies. +//! +//! Every other adapter the real factory would insert (`long`/`double` boxing, unboxing followed by +//! widening such as `Short` into an `int` parameter, boxing into `Number`) is not inserted. The //! call site is left as `Opcode::Invokedynamic`, and the verifier refuses the class as an //! unsupported feature. A refusal is a worse experience than an adapter and a much better one than -//! a wrong answer, and the boundary is checked where it is decidable: at lowering time, from the -//! descriptors alone, so a class either loads or does not. It never fails halfway through a call. +//! a wrong answer, which is why pairs are added one at a time. The boundary is checked where it is +//! decidable: at lowering time, from the descriptors alone, so a class either loads or does not. +//! It never fails halfway through a call on account of a shape. (It can throw inside a call, as +//! the real factory does: `NullPointerException` unboxing `null`, `ClassCastException` unboxing +//! something that is not an `Integer`.) //! //! `altMetafactory` — the entry point for serializable and multi-interface lambdas — is not this //! method and is not linked. @@ -59,12 +69,13 @@ const FACTORY_DESCRIPTOR: &str = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/ /// A bootstrap entry that is the factory, with its static arguments resolved. /// -/// `instantiated` — the third static argument — is read but not used for dispatch: the interface +/// `instantiated_descriptor` — the third static argument — is not used for dispatch: the interface /// method this runtime defines carries the *erased* signature, which is the one callers invoke -/// through the interface. It is resolved anyway because its absence means the entry is not the -/// shape `metafactory` claims to be, and a bootstrap that is not that shape must not be linked. +/// through the interface. It is read where an adapter decides what a value is: an erased `Object` +/// parameter is unboxed only when this says it is an `Integer`. struct LinkedFactory { sam_descriptor: Arc, + instantiated_descriptor: Arc, implementation: MethodHandleRef, } @@ -119,7 +130,7 @@ pub(crate) fn lower(class: &mut ClassInfo) { continue; }; let captures = &captures[..]; - if !adapts(captures, &linked.sam_descriptor, &linked.implementation) { + if plan(captures, &linked.sam_descriptor, &linked.instantiated_descriptor, &linked.implementation).is_none() { continue; } @@ -129,6 +140,7 @@ pub(crate) fn lower(class: &mut ClassInfo) { descriptor: descriptor.clone(), method_name: name.clone(), method_descriptor: linked.sam_descriptor.clone(), + instantiated_method_descriptor: linked.instantiated_descriptor.clone(), implementation: linked.implementation.clone(), }); index += 1; @@ -165,13 +177,14 @@ fn resolve_bootstrap_methods(class: &ClassInfo) -> BTreeMap return None; }; let sam_descriptor = method_type_descriptor(&class.constant_pool, sam)?; - method_type_descriptor(&class.constant_pool, instantiated)?; + let instantiated_descriptor = method_type_descriptor(&class.constant_pool, instantiated)?; let implementation = MethodHandleRef::resolve(&class.constant_pool, implementation)?; Some(( index as u16, LinkedFactory { sam_descriptor, + instantiated_descriptor, implementation, }, )) @@ -179,34 +192,103 @@ fn resolve_bootstrap_methods(class: &ClassInfo) -> BTreeMap .collect() } -/// Whether the implementation can be called by passing the captured values and the interface -/// method's arguments straight through, with no conversion anywhere. +/// What happens to one value on its way between the interface method and the implementation. +#[derive(Clone, Copy, Debug, PartialEq)] +enum Adapter { + /// Nothing: identical primitives, or a reference either way (`Jvm` values carry their own + /// types, so a reference flows into any reference parameter). + Pass, + /// An `int` meeting a reference: `Integer.valueOf`. + BoxInt, + /// A reference meeting an `int`: a check that it is an `Integer`, then `intValue`. + UnboxInt, +} + +const INTEGER: &str = "java/lang/Integer"; + +/// The one adapter plan for a call site: one entry per interface method parameter, and one for +/// the return (`None` when the interface method is `void` and the result is discarded). /// -/// This is the boundary named at the top of the file. It is deliberately an exact shape test -/// rather than an assignability test: `Jvm` values carry their own types, so a reference flows -/// into any reference parameter, but an `int` reaching an `Object` parameter needs a box that -/// nothing here would create. -fn adapts(captures: &[JavaType], sam_descriptor: &str, implementation: &MethodHandleRef) -> bool { - let Some(sam_type) = JavaType::try_parse(sam_descriptor) else { - return false; - }; - let JavaType::Method(sam_parameters, sam_return) = &sam_type else { - return false; +/// Captured values are not adapted: they are passed straight through or the call site is refused. +/// javac captures a value with the implementation's own type, so no call site it emits needs more. +#[derive(Debug, PartialEq)] +struct Plan { + parameters: Vec, + returns: Option, +} + +/// The plan, or `None` where some position needs an adapter this runtime does not insert. +/// +/// This is the boundary named at the top of the file. Lowering asks it whether to link at all, and +/// the synthesised method asks it again for what to do, so the two can never disagree. +fn plan(captures: &[JavaType], sam_descriptor: &str, instantiated_descriptor: &str, implementation: &MethodHandleRef) -> Option { + let JavaType::Method(sam_parameters, sam_return) = JavaType::try_parse(sam_descriptor)? else { + return None; }; - let Some((parameters, returns)) = implementation_signature(implementation) else { - return false; + let JavaType::Method(instantiated_parameters, instantiated_return) = JavaType::try_parse(instantiated_descriptor)? else { + return None; }; + let (parameters, returns) = implementation_signature(implementation)?; - if captures.len() + sam_parameters.len() != parameters.len() { - return false; + if captures.len() + sam_parameters.len() != parameters.len() || instantiated_parameters.len() != sam_parameters.len() { + return None; } - if !captures.iter().chain(sam_parameters).zip(¶meters).all(|(from, to)| passes(from, to)) { - return false; + let (captured, passed) = parameters.split_at(captures.len()); + if !captures.iter().zip(captured).all(|(from, to)| passes(from, to)) { + return None; } + let parameters = sam_parameters + .iter() + .zip(&instantiated_parameters) + .zip(passed) + .map(|((sam, instantiated), implementation)| parameter_adapter(sam, instantiated, implementation)) + .collect::>>()?; // A `void` interface method discards whatever the implementation returned, which is what the // real factory does too — `Runnable r = list::clear` is an ordinary method reference. - **sam_return == JavaType::Void || passes(&returns, sam_return) + let returns = if *sam_return == JavaType::Void { + None + } else { + Some(return_adapter(&returns, &sam_return, &instantiated_return)?) + }; + + Some(Plan { parameters, returns }) +} + +/// A caller's argument, typed `sam` (erased) and `instantiated` (generic), reaching an +/// implementation parameter typed `implementation`. +fn parameter_adapter(sam: &JavaType, instantiated: &JavaType, implementation: &JavaType) -> Option { + if passes(sam, implementation) { + return Some(Adapter::Pass); + } + match (sam, instantiated, implementation) { + (JavaType::Int, JavaType::Int, to) if holds_boxed_int(to) => Some(Adapter::BoxInt), + // The instantiated type is what makes this an `Integer` rather than a `Short` or a + // `Character`: the erased `Object` says nothing, and unboxing a `Short` as an `Integer` + // would throw where the real factory widens. + (from, JavaType::Class(boxed), JavaType::Int) if holds_boxed_int(from) && boxed == INTEGER => Some(Adapter::UnboxInt), + _ => None, + } +} + +/// The implementation's result, typed `implementation`, returned through an interface method typed +/// `sam` (erased) and `instantiated` (generic). +fn return_adapter(implementation: &JavaType, sam: &JavaType, instantiated: &JavaType) -> Option { + if passes(implementation, sam) { + return Some(Adapter::Pass); + } + match (implementation, sam, instantiated) { + (JavaType::Int, to, generic) if holds_boxed_int(to) && holds_boxed_int(generic) => Some(Adapter::BoxInt), + (JavaType::Class(boxed), JavaType::Int, JavaType::Int) if boxed == INTEGER => Some(Adapter::UnboxInt), + _ => None, + } +} + +/// A reference type an `Integer` is known to fit without asking the class hierarchy: `Integer` +/// itself and `Object`. `Number`, `Comparable` and `Serializable` would be right too, and are +/// left out until something measures them. +fn holds_boxed_int(r#type: &JavaType) -> bool { + matches!(r#type, JavaType::Class(name) if name == INTEGER || name == "java/lang/Object") } /// The signature the implementation is *called* with, which is not the descriptor written in the @@ -246,7 +328,20 @@ fn passes(from: &JavaType, to: &JavaType) -> bool { /// else. pub(crate) async fn instantiate(jvm: &Jvm, call_site: &LambdaCallSite, captures: Vec) -> Result> { if !jvm.has_class(&call_site.class_name) { - jvm.register_class(Box::new(synthesise(call_site)), None).await?; + let captured = JavaType::parse(&call_site.descriptor).as_method().0.to_vec(); + let Some(plan) = plan( + &captured, + &call_site.method_descriptor, + &call_site.instantiated_method_descriptor, + &call_site.implementation, + ) else { + // Lowering linked this call site only because the same function said yes, so a `None` + // here is an opcode written by something that did not run that check. + return Err(jvm + .exception("java/lang/InternalError", "lambda call site needs an adapter that is not inserted") + .await); + }; + jvm.register_class(Box::new(synthesise(call_site, plan)), None).await?; } let mut instance = jvm.instantiate_class(&call_site.class_name).await?; @@ -259,7 +354,7 @@ pub(crate) async fn instantiate(jvm: &Jvm, call_site: &LambdaCallSite, captures: /// The class the call site evaluates an instance of: the interface, one field per captured value, /// and one method — the interface's, implemented by [`LambdaBody`]. -fn synthesise(call_site: &LambdaCallSite) -> ClassDefinitionImpl { +fn synthesise(call_site: &LambdaCallSite, plan: Plan) -> ClassDefinitionImpl { let fields = capture_descriptors(call_site) .into_iter() .enumerate() @@ -273,7 +368,7 @@ fn synthesise(call_site: &LambdaCallSite) -> ClassDefinitionImpl { .enumerate() .map(|(index, x)| (capture_name(index), x.descriptor())) .collect(), - returns_void: matches!(JavaType::parse(&call_site.method_descriptor).as_method().1, JavaType::Void), + plan, }; let method = MethodImpl::new( &call_site.method_name, @@ -326,7 +421,7 @@ fn descriptor_of(r#type: &JavaType) -> String { struct LambdaBody { implementation: MethodHandleRef, captures: Vec<(String, String)>, - returns_void: bool, + plan: Plan, } #[async_trait::async_trait] @@ -343,7 +438,9 @@ impl JvmCallback for LambdaBody { for (name, descriptor) in &self.captures { arguments.push(jvm.get_field(&this, name, descriptor).await?); } - arguments.extend(args.into_vec().into_iter().skip(1)); + for (value, adapter) in args.into_vec().into_iter().skip(1).zip(&self.plan.parameters) { + arguments.push(adapt(jvm, *adapter, value).await?); + } let member = &self.implementation.member; let result: JavaValue = match self.implementation.kind { @@ -375,6 +472,43 @@ impl JvmCallback for LambdaBody { _ => return Err(jvm.exception("java/lang/InternalError", "lambda implementation is not a method").await), }; - Ok(if self.returns_void { JavaValue::Void } else { result }) + match self.plan.returns { + None => Ok(JavaValue::Void), + Some(adapter) => adapt(jvm, adapter, result).await, + } + } +} + +/// Apply one [`Adapter`] to a value. The exceptions are the ones the real factory's adapter throws: +/// it casts to `Integer` before unboxing, so a wrong type is a `ClassCastException` and `null` is a +/// `NullPointerException`. +async fn adapt(jvm: &Jvm, adapter: Adapter, value: JavaValue) -> Result { + match (adapter, value) { + (Adapter::Pass, value) => Ok(value), + (Adapter::BoxInt, JavaValue::Int(value)) => { + let boxed: Box = jvm.invoke_static(INTEGER, "valueOf", "(I)Ljava/lang/Integer;", (value,)).await?; + Ok(JavaValue::Object(Some(boxed))) + } + (Adapter::UnboxInt, JavaValue::Object(None)) => Err(jvm + .exception("java/lang/NullPointerException", "Cannot unbox null to int in a lambda adapter") + .await), + (Adapter::UnboxInt, JavaValue::Object(Some(object))) => { + if !jvm.is_instance(&*object, INTEGER) { + let class_name = object.class_definition().name().replace('/', "."); + return Err(jvm + .exception( + "java/lang/ClassCastException", + &format!("class {class_name} cannot be cast to class java.lang.Integer"), + ) + .await); + } + let value: i32 = jvm.invoke_virtual(&object, INTEGER, "intValue", "()I", ()).await?; + Ok(JavaValue::Int(value)) + } + // The plan was made from the same descriptors the values were typed by; a mismatch here + // is this runtime's mistake, not the guest's. + (adapter, value) => Err(jvm + .exception("java/lang/InternalError", &format!("lambda adapter {adapter:?} given {value:?}")) + .await), } } diff --git a/test-data/class-file-versions.txt b/test-data/class-file-versions.txt index 18817c1f..875d4d73 100644 --- a/test-data/class-file-versions.txt +++ b/test-data/class-file-versions.txt @@ -166,6 +166,13 @@ 65.0 indy/Lambda.class 65.0 indy/LambdaBoxing$Gen.class 65.0 indy/LambdaBoxing.class +65.0 indy/LambdaBoxingKinds$Fn.class +65.0 indy/LambdaBoxingKinds$IntGen.class +65.0 indy/LambdaBoxingKinds$IntSink.class +65.0 indy/LambdaBoxingKinds$Src.class +65.0 indy/LambdaBoxingKinds.class +65.0 indy/LambdaBoxingLong$Gen.class +65.0 indy/LambdaBoxingLong.class 52.0 indy/LambdaCapturingThis$Op.class 52.0 indy/LambdaCapturingThis.class 65.0 indy/LambdaKinds$Base.class @@ -182,6 +189,8 @@ 65.0 indy/LambdaKinds$Sink.class 65.0 indy/LambdaKinds$Weighted.class 65.0 indy/LambdaKinds.class +65.0 indy/LambdaUnboxingShort$Fn.class +65.0 indy/LambdaUnboxingShort.class 52.0 indy/MakeConcat.class 52.0 indy/MakeConcatWithArgument.class 52.0 indy/MakeConcatWrongDescriptor.class diff --git a/test-data/indy/LambdaBoxing.class b/test-data/indy/LambdaBoxing.class index ec8a0d032ce313f5480f92f8b2350ee695484ffe..5ebe79f42f402fc0a1bf0785ea0093aaea5bfa93 100644 GIT binary patch delta 33 pcmZ3?v6y2+H8UgE7i0|1Pj2W0>N delta 33 pcmZ3?v6y2+H8UgM7i0|1S=2YCPh diff --git a/test-data/indy/LambdaBoxingKinds$Fn.class b/test-data/indy/LambdaBoxingKinds$Fn.class new file mode 100644 index 0000000000000000000000000000000000000000..915641e37c8fe959cd9215ef883c4d3fb2553175 GIT binary patch literal 355 zcmZut+iJo<6r7W45~Egy(k~Fv7kzI;s6tQ_3z{ETbqSGlBiUH=*L?5;`ccJW=#%Ty zoS8G6%l!Ubt^hvKa$phO7UFxe6XWz0t7smpoo)xpL5a}Ziytvago*}Vo4wrTgt9ms zk~5*xT|74Zo+%M3>o`&(KW38PcEix`g)5&h9EKmJef2~enKpvAPLJ7E264h;-;D?Q z_uF{iQtEt~>fFIAp=Hq~Y=H2>@i1}(W!TIeW)@tO{;LYSLaCxw PU>yxckDY`%nznlb)5lok literal 0 HcmV?d00001 diff --git a/test-data/indy/LambdaBoxingKinds$IntGen.class b/test-data/indy/LambdaBoxingKinds$IntGen.class new file mode 100644 index 0000000000000000000000000000000000000000..a087a36ab94500423e245428b3940c34502a0264 GIT binary patch literal 224 zcmZXO!4APt5Jm44eN`oF#0OZg(QiP4hOqDjukB0Ire2f2M|_)w5Aac9%F^cE%;e78 z^Ljs?0G1dCIE2YTUgA`)s(Y@peXdh8jkMh=Ezl+m3we`aDRmZ};zA{s(8^S!vw1}D z&(*a~)Fv+#Vf^=L(OeU}qcV0^87t5ujQ(PTAktdZ>rxt{48i>oxBwS1D?77vzzd!Q Ts{;?+uloqteNHkC`mXl@FlsX4 literal 0 HcmV?d00001 diff --git a/test-data/indy/LambdaBoxingKinds$IntSink.class b/test-data/indy/LambdaBoxingKinds$IntSink.class new file mode 100644 index 0000000000000000000000000000000000000000..4b16b5309a84637561c95168455bb254f6a9a0ad GIT binary patch literal 230 zcmZXOy9&ZU5Jm6!$j10U8^PW}8~p|q#Gq&qu-|N!urY4LjpDai_yK;DIEtOkoSETr zInUSq0bquXK!q^a$YY$yd2ur~T^pOYVQ9<9*n>cg(9PskPIGC~=`PN+DhZxcN}qVC zjl%Js5Jbi0tn||4n$Z8Nn|vAw{#LtkRk%{1N$CB>2(8dseO~0!IqeAIr{MvFDjPFd Ys1APcEqGSoqw!^c7Bk=^`=IUlZ!0D>V*mgE literal 0 HcmV?d00001 diff --git a/test-data/indy/LambdaBoxingKinds$Src.class b/test-data/indy/LambdaBoxingKinds$Src.class new file mode 100644 index 0000000000000000000000000000000000000000..c3596da03f3142283f96a7ebaf6b6613ee393036 GIT binary patch literal 314 zcmZWk%MQU%5Iy73R=q007g*GWJt9FOgs^CT;HsOZQnyKOiEp#;0X|9$vC_?)IcLsf z<~(2b2Y?w`CJKbkR-6wYkSdCQ1b7B(7o@3l$Fchm#C4LNSzF+irjR zKN}M&UKA>kT~bLXyM5nh&7Jt8ymy+H2)378(m*bwnD@K6w+5e!1ZyXCwn}tnqCsfo zY=qicDVZ)}p|#ZfGRH9hZa6g~2P?y1ZZa#tLh094V1Jb=YG16Q$!N2aP={k!Z@%A3 APyhe` literal 0 HcmV?d00001 diff --git a/test-data/indy/LambdaBoxingKinds.class b/test-data/indy/LambdaBoxingKinds.class new file mode 100644 index 0000000000000000000000000000000000000000..37fb4987a4addf649342a2e3780913640830ad9b GIT binary patch literal 3067 zcmcIm?ORi29KN41cE&iwoQlrG)KD3MgqbGOGzfIc00lv_jI(io1LK^v9T4n2E3Uj&oGBWKvJF2KE1BdB8BIXgZEw1$|PBISZFHEs-Jk5L0kcVAFCz zdS_Cpev2j0GhK7ione`N4AQ%q);Am64+)4l1*geyS*+X^pb%wLONZ_s&cw^vs}^Nb$y-H7>}0FwA7W3u3H&!kW`Z zWhLAV#k9$XB1)KPnU3m&bzY!4%cUTcZ-=V~7gZu;=AG6IHbheaJ$T$fDw8U#Bm%g)*vEgjeG32QX# z#|(K|iOF{12T4@Px{zJF2%@FdhYJCm!&52}XbWO2h=aU)PToDQB8kx;UKD67OV!10 zk$w|j0 z4+R>Q+ruztttq`VpUC`6*sUdPE5q5n>JG2~%=160nWHH^+t6cKQ#0wPE9OE)TsK9E zgY#*<4BTC)#gQ#F(W#|U1BR_fN$F|;ALA1RpQ`u_p9^d)p_H#I_|q#BtIJxaNZQ;4i3Yx{cL2tOssk<1#n-fd#an7Q3j)sVrzl3Ys-i zGl#8Z)!Lw9H>xm&R9;Qo;BRXc4gQ$JHlT;fI~NcU_#OUR*ySC$h21$k0K_S24n|0H z0sB1+>7g7y_t4AhmIXW_cthY(pxqnx`WMjSK@R;qR)#BsKIhyl9M$2QsA>06FEl`6 z&F#LhFLY{IqDaXZN^&?$btO!PH1BBnrR*^$9ThC?B&Kl!7w{4;VHj6&4cB@0I4v-S zFj?(oE9_(G_hU0Z3)*l%eiWb+hxki*7$?zzGdv~GOPkFYGSjElR(jy(Z3B6KC#ApPxR?;TqR2u3vGDbN!m@b>`qE!hZvR CIp!7s literal 0 HcmV?d00001 diff --git a/test-data/indy/LambdaBoxingLong$Gen.class b/test-data/indy/LambdaBoxingLong$Gen.class new file mode 100644 index 0000000000000000000000000000000000000000..0eb44254b81d78cd918e725ad029a4a77dcbfcc8 GIT binary patch literal 232 zcmZ8bI}Ug16ZkvpM+@P2^M_WKyWu<(Rels58$DUPfMDa_nG%* z-k;|SzzQP?Pasa!HM^*_eH2Eg*68Vuj6_#pT&lZDDrIzX%1X)`fsQ7|cs5VlyQM&Q zwzoQ`ZBdcHr2TgByATMD+CD>K$C8%+j3HXCG~<-Ts_{QqphZ$g3OWV4f^f^_ifMAE)#1D9lO?;RlHJlv zA&q&YkZv4Tq)F6&su8bfXdJ%iQ@#B53Wl8%rLmBT*Y^5e5Ii z+rDxKq~nCG-{EeIlSYcKxa`=%E160i^?Szj&O)9ui3fw82*dwBIFdX+H+%uc%yn+t z4a*k>gZ^4d;nr>Ld4jqarHiTIF~i1GXJ&0u!&8Rh`A~W(qmJMj9YuuR2M`4d-W82k z$y%eC(HyUS2m3+`Mo`yi_IU!C#ym>c7$dg{8HnXh$PM!=E^Pil zj^P`YPbJb+3FyrxkfSk=7;caq!A+EDjuBNJ6vGnLHYru=!?ELB9uaCK`NN|0h@G5Bk4x6YiWP24}O3jB|R0N znmo;!Im5Zk@$_>5@Q5217Gbm$AIY28sHFVLRTjOMmAMO*3!5<5icc{sgvw^CWGhoc zXp7yh_$J(sm;al=q%ILUQJyJb_LU@f<2Vi`@j4*P=kY_`zHZPKbsNEt%6*l}FfVxQ zZNm%Gzw3D8tJLPX)W$`hFl;aqx-Uw}>ZuS~OHHss9^w~-1Vc>YbJ!D(h literal 0 HcmV?d00001 diff --git a/test-data/indy/LambdaUnboxingShort.class b/test-data/indy/LambdaUnboxingShort.class new file mode 100644 index 0000000000000000000000000000000000000000..5cab080d963d12907d85dd82d1c3ef8ecae502ab GIT binary patch literal 1219 zcma)6TT|0O6#lj?4WyA)D5YE#L`*=bchTStW*9qDz?ouSd~w?@5J)$jWXs5Z@qsD}A!T3yX@=Y(KjtNu%l*<$ z{ZKR%Lwe1Tj(Wk6G_BeQWepk_g3d6&+imxRVbZiJ72c{h`8!$nK00#0df@qLVN=>e zFmOe~uz@Uc=c-pv0wr2h)aww&Sfz{Wc%`?#BUM%Tg12lPBN)|?H!y|*L-veY7l1)K z=59yq>@nocsuk7fa_`>T85A+0VKU@9#Zc%^l3~$|rABSr@oGJnjv35qxMtuw<`_o% zP$o+4umM+6PyMRaG~A%nK1>)=8(vdTn-xcj?M|yM{9Rsm$&++sgRaf8RVLHo4plFh z?_;D%Fp}N2Y7BbS>-Y_^>4cfZK0qrWZ-&ga2-KDcVFG%Yb;@ z33YfUQ)#F4VO(<1Pc|sB2lYzCt&rSLag9t46EPi#sJG*ve*2-B}e4X+qxFAt?rjRe6Qy;_1M6-a{S6lq!) z=rlu?ksVn22KJdY1W#*t4d@sQ-Y6uW{F?!zX} T2t^gCskQVVp5rC`xF-GruV^&< literal 0 HcmV?d00001 diff --git a/test-data/src/indy/LambdaBoxing.java b/test-data/src/indy/LambdaBoxing.java index 48e4011f..532f8058 100644 --- a/test-data/src/indy/LambdaBoxing.java +++ b/test-data/src/indy/LambdaBoxing.java @@ -1,14 +1,10 @@ -// The boundary: a call site this runtime refuses to link, and why that is a decision rather than -// an oversight. +// The first adapter this runtime inserts, in its simplest place. The interface method returns +// `Object` and the implementation returns `int`, so `LambdaMetafactory` boxes on the way out — +// measured: OpenJDK 26.0.1 runs this and prints 3. // -// `LambdaMetafactory` is allowed to insert adapters. Here the interface method returns `Object` -// and the implementation returns `int`, so the real factory boxes on the way out — measured: -// OpenJDK 26.0.1 runs this and prints 3. This runtime has no boxing to insert at that point, so -// it does not link the call site at all and the class is refused as an unsupported feature. -// -// A refusal is worse than an adapter and much better than a wrong answer, and this fixture is what -// keeps that choice honest: delete the signature check in `jvm-bytecode/src/lambda.rs` and this -// class stops being refused. +// This fixture used to be the boundary: the call site was refused because nothing here could box. +// `jvm-bytecode/src/lambda.rs` now inserts `Integer.valueOf` for exactly this pair, and the pairs +// that are still refused have their own fixtures (LambdaBoxingLong, LambdaUnboxingShort). // // Compiled with: javac --release 21 -d test-data/indy test-data/src/indy/LambdaBoxing.java public class LambdaBoxing { diff --git a/test-data/src/indy/LambdaBoxingKinds.java b/test-data/src/indy/LambdaBoxingKinds.java new file mode 100644 index 00000000..5b90b798 --- /dev/null +++ b/test-data/src/indy/LambdaBoxingKinds.java @@ -0,0 +1,89 @@ +// Every place the int <-> Integer adapter goes, and the exceptions it throws. The expected output +// is OpenJDK 26.0.2.1's, taken by running this fixture there: +// 4 / 42 / 7 / obj:5 / integer:6 / npe in / cce / npe out +// +// Each line is one position: boxing on the way out through a generic interface, unboxing on the +// way in and boxing on the way out, unboxing on the way out, boxing on the way in to `Object` and +// to `Integer`. The last three are what the real adapter throws: `null` unboxed on the way in, +// something that is not an `Integer` passed through a raw type, and `null` unboxed on the way out. +// +// Compiled with: javac --release 21 -d test-data/indy test-data/src/indy/LambdaBoxingKinds.java +public class LambdaBoxingKinds { + interface Src { + T get(); + } + + interface Fn { + R apply(T t); + } + + interface IntGen { + int get(); + } + + interface IntSink { + void accept(int x); + } + + static int size() { + return 3; + } + + static int inc(int x) { + return x + 1; + } + + static Integer boxed() { + return 7; + } + + static Integer none() { + return null; + } + + static void show(Object o) { + System.out.println("obj:" + o); + } + + static void showInteger(Integer o) { + System.out.println("integer:" + o); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static void main(String[] args) { + Src s = LambdaBoxingKinds::size; + System.out.println(s.get() + 1); + + Fn f = LambdaBoxingKinds::inc; + System.out.println(f.apply(41)); + + IntGen g = LambdaBoxingKinds::boxed; + System.out.println(g.get()); + + IntSink k = LambdaBoxingKinds::show; + k.accept(5); + + IntSink ki = LambdaBoxingKinds::showInteger; + ki.accept(6); + + try { + f.apply(null); + } catch (NullPointerException e) { + System.out.println("npe in"); + } + + Fn raw = f; + try { + raw.apply("x"); + } catch (ClassCastException e) { + System.out.println("cce"); + } + + IntGen n = LambdaBoxingKinds::none; + try { + n.get(); + } catch (NullPointerException e) { + System.out.println("npe out"); + } + } +} diff --git a/test-data/src/indy/LambdaBoxingLong.java b/test-data/src/indy/LambdaBoxingLong.java new file mode 100644 index 00000000..5f6ee7e6 --- /dev/null +++ b/test-data/src/indy/LambdaBoxingLong.java @@ -0,0 +1,21 @@ +// A boxing pair this runtime does not insert: `long` into `Object`. OpenJDK 26.0.2.1 runs this and +// prints 5000000000; here the call site is left unlinked and the class is refused. +// +// The adapter in `jvm-bytecode/src/lambda.rs` is the `int` <-> `Integer` pair and nothing wider, so +// this fixture is what notices a change that generalises it to "any primitive meets a reference". +// +// Compiled with: javac --release 21 -d test-data/indy test-data/src/indy/LambdaBoxingLong.java +public class LambdaBoxingLong { + interface Gen { + Object get(); + } + + static long big() { + return 5000000000L; + } + + public static void main(String[] args) { + Gen gen = LambdaBoxingLong::big; + System.out.println(gen.get()); + } +} diff --git a/test-data/src/indy/LambdaUnboxingShort.java b/test-data/src/indy/LambdaUnboxingShort.java new file mode 100644 index 00000000..adbba8fe --- /dev/null +++ b/test-data/src/indy/LambdaUnboxingShort.java @@ -0,0 +1,23 @@ +// Unboxing followed by widening, which this runtime does not insert. The interface method takes an +// erased `Object`; the instantiated type says it is a `Short`, and the implementation takes an +// `int`. OpenJDK 26.0.2.1 runs this and prints 42; here the class is refused. +// +// This is the case that makes the instantiated type matter. Reading only the erased `Object`, an +// `int` <-> `Integer` adapter would unbox this argument as an `Integer` and throw +// ClassCastException where the real factory answers 42 — a refusal turned into a wrong answer. +// +// Compiled with: javac --release 21 -d test-data/indy test-data/src/indy/LambdaUnboxingShort.java +public class LambdaUnboxingShort { + interface Fn { + R apply(T t); + } + + static int inc(int x) { + return x + 1; + } + + public static void main(String[] args) { + Fn f = LambdaUnboxingShort::inc; + System.out.println(f.apply((short) 41)); + } +} diff --git a/tests/test_class_format.rs b/tests/test_class_format.rs index ef6ea332..557bde55 100644 --- a/tests/test_class_format.rs +++ b/tests/test_class_format.rs @@ -565,29 +565,71 @@ async fn test_every_reference_kind_a_lambda_implementation_can_have_runs() { assert_eq!(output, "42\n", "40 captured through `this`, plus the argument"); } -// The other half of the sentence above: what is *not* linked. The interface method returns -// `Object` and the implementation returns `int`, so a real `LambdaMetafactory` boxes — OpenJDK -// 26.0.1 runs this fixture and prints 3. There is no boxing to insert here, so the call site is -// left alone and the class is refused. +// The one adapter this runtime inserts: `int` <-> `Integer`. `LambdaBoxing` is its simplest place — +// the interface method returns `Object`, the implementation returns `int` — and was the refusal +// fixture until the adapter existed. `LambdaBoxingKinds` is every other position the pair reaches, +// plus the three exceptions the real adapter throws. The expected text is OpenJDK 26.0.2.1's, taken +// by running the same fixtures there. // -// This is what makes the choice visible. Remove the signature check in `jvm-bytecode/src/lambda.rs` -// and the class links instead: the refusal is a decision, and deleting it is a change this notices. +// Delete the adapter in `jvm-bytecode/src/lambda.rs` and both classes go back to being refused. #[tokio::test] -async fn test_a_call_site_needing_an_adapter_is_refused_rather_than_guessed_at() { - let path = Path::new("test-data/indy/LambdaBoxing.class"); +async fn test_the_int_integer_adapter_is_inserted_where_the_real_factory_inserts_it() { + let indy = Path::new("./test-data/indy/"); - let err = run_class(path, &[Path::new("./test-data/indy/")], &[]).await.unwrap_err().to_string(); + let output = run_class(Path::new("test-data/indy/LambdaBoxing.class"), &[indy], &[]) + .await + .expect("an int returned through an Object interface method should be boxed"); + assert_eq!(output, "3\n"); - assert!( - err.contains("java.lang.UnsupportedOperationException") && err.contains("invokedynamic"), - "expected the unsupported-feature diagnosis, got: {err}" - ); - assert!( - !err.contains("ClassFormatError"), - "a class OpenJDK runs to completion is not malformed, got: {err}" + let output = run_class(Path::new("test-data/indy/LambdaBoxingKinds.class"), &[indy], &[]) + .await + .expect("every int <-> Integer position should link and run"); + assert_eq!( + output.lines().collect::>(), + vec![ + "4", // boxed on the way out through Src, unboxed again by the caller + "42", // Fn over inc(int): unboxed in, boxed out + "7", // IntGen over a method returning Integer: unboxed on the way out + "obj:5", // IntSink over show(Object): boxed on the way in + "integer:6", // IntSink over showInteger(Integer): boxed on the way in + "npe in", // null unboxed on the way in + "cce", // a String through the raw type: not an Integer, so not unboxed + "npe out", // null unboxed on the way out + ] ); } +// The other half: pairs the adapter does not cover are still refused, not guessed at. Each fixture +// is a class OpenJDK 26.0.2.1 runs to completion (5000000000 and 42), so each refusal is this +// runtime's decision, and each catches a different way of widening the adapter by mistake: +// +// - `LambdaBoxingLong` — `long` into `Object`. Generalising to "any primitive meets a reference" +// would link it. +// - `LambdaUnboxingShort` — the erased parameter is `Object`, the instantiated one is `Short`, the +// implementation takes `int`. An adapter that read only the erased type would unbox it as an +// `Integer` and throw ClassCastException where the real factory answers 42: a wrong answer +// instead of a refusal. +#[tokio::test] +async fn test_a_call_site_needing_an_adapter_that_is_not_inserted_is_refused_rather_than_guessed_at() { + for name in ["LambdaBoxingLong", "LambdaUnboxingShort"] { + let path = format!("test-data/indy/{name}.class"); + + let err = run_class(Path::new(&path), &[Path::new("./test-data/indy/")], &[]) + .await + .unwrap_err() + .to_string(); + + assert!( + err.contains("java.lang.UnsupportedOperationException") && err.contains("invokedynamic"), + "{name}: expected the unsupported-feature diagnosis, got: {err}" + ); + assert!( + !err.contains("ClassFormatError"), + "{name}: a class OpenJDK runs to completion is not malformed, got: {err}" + ); + } +} + // The identity check is four comparisons and the tests above can observe none of them: every // other bootstrap on hand is refused for an unrelated reason first. So each axis gets a fixture // differing in that axis alone, exactly as the string concat factory has (this is the second