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
1 change: 1 addition & 0 deletions crates/javac-ast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ pub enum JavaSyntaxKind {
PatternExpr,

LambdaExpr,
LambdaParam,
MethodRefExpr,

Annotation,
Expand Down
186 changes: 181 additions & 5 deletions crates/javac-bytecode/src/class_gen.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use crate::codegen::CodegenCtx;
use crate::codegen::{CodegenCtx, LambdaInfo};
use crate::error::BytecodeError;
use crate::expr_gen;
use crate::local_var::return_opcode;
use javac_call_resolver::ClassCatalog;
use javac_classfile::ClassFileWriter;
use javac_hir::hir::*;
use javac_ty::Ty;
use rust_asm::constants::V21;
use rust_asm::insn::Handle;
use rust_asm::opcodes;
use std::collections::HashMap;

const OBJECT_CLASS: &str = "java/lang/Object";
const INIT_METHOD: &str = "<init>";
Expand Down Expand Up @@ -64,7 +68,28 @@ fn gen_type_decl(writer: &mut ClassFileWriter, type_decl: &TypeDecl, catalog: &C
if needs_default_constructor(type_decl) {
gen_default_constructor(writer, type_decl, &super_name, catalog);
}
gen_methods(writer, type_decl, &super_name, catalog);

let mut counter = 0u32;
for method in &type_decl.methods {
let mut method_lambda_infos: HashMap<ExprId, LambdaInfo> = HashMap::new();
scan_and_gen_lambdas(
writer,
type_decl,
&super_name,
catalog,
method,
&mut method_lambda_infos,
&mut counter,
);
gen_method(
writer,
type_decl,
method,
&super_name,
catalog,
&method_lambda_infos,
);
}
}

fn gen_fields(writer: &mut ClassFileWriter, fields: &[FieldDecl]) {
Expand All @@ -78,14 +103,163 @@ fn gen_fields(writer: &mut ClassFileWriter, fields: &[FieldDecl]) {
}
}

fn gen_methods(
struct SamInfo {
interface: String,
method_name: String,
method_type: String,
return_ty: Ty,
}

fn resolve_sam_interface(expr: &Expr, catalog: &ClassCatalog, param_count: usize) -> SamInfo {
if let Expr::Lambda {
target_ty: Some(Ty::Class(name)),
..
} = expr
{
if let Some(method) = catalog.functional_interface_method(name) {
let (method_type, return_ty) = erased_descriptor_from_method_ref(&method);
return SamInfo {
interface: name.to_string(),
method_name: method.name.clone(),
method_type,
return_ty,
};
}
}
match param_count {
0 => SamInfo {
interface: "java/util/function/Supplier".into(),
method_name: "get".into(),
method_type: "()Ljava/lang/Object;".into(),
return_ty: Ty::object(),
},
1 => SamInfo {
interface: "java/util/function/Function".into(),
method_name: "apply".into(),
method_type: "(Ljava/lang/Object;)Ljava/lang/Object;".into(),
return_ty: Ty::object(),
},
_ => SamInfo {
interface: "java/util/function/BiFunction".into(),
method_name: "apply".into(),
method_type: "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;".into(),
return_ty: Ty::object(),
},
}
}

fn erased_descriptor_from_method_ref(mr: &javac_call_resolver::MethodRef) -> (String, Ty) {
let param_descs: String = mr
.params
.iter()
.map(|_| "Ljava/lang/Object;")
.collect::<Vec<_>>()
.join("");
let (ret, return_ty) = if matches!(mr.return_ty, Ty::Void) {
("V", Ty::Void)
} else {
("Ljava/lang/Object;", Ty::object())
};
(format!("({}){}", param_descs, ret), return_ty)
}

fn scan_and_gen_lambdas(
writer: &mut ClassFileWriter,
type_decl: &TypeDecl,
super_name: &str,
catalog: &ClassCatalog,
method: &MethodDecl,
lambda_infos: &mut HashMap<ExprId, LambdaInfo>,
counter: &mut u32,
) {
for method in &type_decl.methods {
gen_method(writer, type_decl, method, super_name, catalog);
for (expr_id, expr) in method.body.exprs.iter() {
if let Expr::Lambda {
params,
body: lambda_body,
..
} = expr
{
let synthetic_name = format!("lambda${}${}", method.name, counter);
*counter += 1;

let sam_info = resolve_sam_interface(expr, catalog, params.len());

let param_descs: String = params
.iter()
.map(|_| "Ljava/lang/Object;")
.collect::<Vec<_>>()
.join("");
let impl_descriptor = format!("({}){}", param_descs, sam_info.return_ty.descriptor());
let sam_descriptor = format!("()L{};", sam_info.interface);

let impl_method_handle = Handle {
reference_kind: rust_asm::constants::REF_INVOKE_STATIC,
owner: type_decl.name.to_string(),
name: synthetic_name.clone(),
descriptor: impl_descriptor.clone(),
is_interface: false,
};

{
let mut mw = writer.visit_method(
javac_classfile::ACC_PRIVATE
| javac_classfile::ACC_STATIC
| javac_classfile::ACC_SYNTHETIC,
&synthetic_name,
&impl_descriptor,
);
mw.visit_code();

let mut ctx = CodegenCtx::new(writer, type_decl.name, catalog);
ctx.set_super_name(ustr::Ustr::from(super_name));
ctx.set_fields(&type_decl.fields);
ctx.set_methods(&type_decl.methods);

ctx.return_ty = sam_info.return_ty.clone();
ctx.next_local = 0;
ctx.locals.clear();
ctx.local_types.clear();
for (i, param) in params.iter().enumerate() {
let ty = param.ty.clone().unwrap_or(Ty::object());
mw.visit_local_variable(
param.name.as_str(),
&ty.erasure().descriptor(),
i as u16,
);
ctx.locals.insert(param.name, i as u16);
ctx.local_types.insert(param.name, ty);
ctx.next_local = (i as u16) + 1;
}

match lambda_body {
LambdaBody::Expr(body_expr_id) => {
expr_gen::gen_expr(&mut mw, &mut ctx, &method.body, *body_expr_id);
let body_ty = expr_gen::expr_ty(&ctx, &method.body, *body_expr_id);
mw.visit_insn(return_opcode(&body_ty));
Comment on lines +236 to +238
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Lambda synthetic method uses expression type for return opcode instead of declared SAM return type

In scan_and_gen_lambdas, for LambdaBody::Expr, the return opcode is computed from body_ty (the expression's inferred type) rather than ctx.return_ty / sam_info.return_ty (the declared return type of the synthetic method). When the expression type differs from the SAM return type, this produces invalid bytecode. For example, a Consumer<String> lambda whose body is list.add(x) (returns boolean) would emit IRETURN in a method declared as (Ljava/lang/Object;)V, causing a JVM VerifyError. Similarly, a Supplier<Integer> lambda like () -> 42 would emit IRETURN instead of ARETURN, since the expression type is int but the erased impl descriptor returns Ljava/lang/Object;. No coercion between body_ty and the method's return type is applied either.

Prompt for agents
In scan_and_gen_lambdas (class_gen.rs), the LambdaBody::Expr arm at lines 235-239 computes return_opcode from body_ty (the expression type) instead of ctx.return_ty (the SAM return type set at line 218). This causes a type mismatch when the expression type differs from the SAM's declared return type.

For example:
- Consumer lambda with boolean-returning body: emits IRETURN in a void method
- Supplier lambda with int-returning body: emits IRETURN in an Object-returning method

The fix should:
1. Use ctx.return_ty (or sam_info.return_ty) to determine the return opcode
2. If ctx.return_ty is Void but body_ty is not Void, emit a pop instruction (pop_ty) to discard the expression value before RETURN
3. If both are non-void but differ, apply coercion (crate::expr_gen::coerce) from body_ty to ctx.return_ty before the return instruction

The relevant function is crate::expr_gen::coerce for type coercion and crate::expr_gen::pop_ty for discarding values. The return_opcode function is in crate::local_var::return_opcode.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
LambdaBody::Block(block) => {
crate::method_gen::gen_method_body(&mut mw, &mut ctx, &method.body, block);
}
}

mw.visit_maxs(0, 0);
mw.visit_end(writer);
}

lambda_infos.insert(
expr_id,
LambdaInfo {
synthetic_name,
sam_interface: sam_info.interface.clone(),
sam_method_name: sam_info.method_name.clone(),
sam_method_type: sam_info.method_type.clone(),
sam_descriptor: sam_descriptor.to_string(),
impl_descriptor,
params: params.clone(),
impl_method_handle,
},
);
}
}
}

Expand All @@ -95,6 +269,7 @@ fn gen_method(
method: &MethodDecl,
super_name: &str,
catalog: &ClassCatalog,
lambda_infos: &HashMap<ExprId, LambdaInfo>,
) {
let descriptor = method.signature.descriptor();
let mut mw = writer.visit_method(method.access_flags, &method.name, &descriptor);
Expand All @@ -113,6 +288,7 @@ fn gen_method(
ctx.set_super_name(ustr::Ustr::from(super_name));
ctx.set_fields(&type_decl.fields);
ctx.set_methods(&type_decl.methods);
ctx.lambda_info = lambda_infos.clone();
Comment thread
Neamyoo-dev marked this conversation as resolved.
ctx.begin_method(method);
declare_method_locals(&mut mw, type_decl, method);
gen_constructor_prelude(&mut mw, &ctx, method);
Expand Down
17 changes: 16 additions & 1 deletion crates/javac-bytecode/src/codegen.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use javac_call_resolver::ClassCatalog;
use javac_classfile::{ClassFileWriter, Label};
use javac_hir::hir::{Block, FieldDecl, MethodDecl};
use javac_hir::hir::{Block, ExprId, FieldDecl, LambdaParam, MethodDecl};
use javac_ty::{MethodSig, Ty};
use rust_asm::insn::Handle;
use std::collections::HashMap;
use ustr::Ustr;

Expand Down Expand Up @@ -29,6 +30,18 @@ pub struct ControlTarget {
pub cleanup_depth: usize,
}

#[derive(Clone)]
pub struct LambdaInfo {
pub synthetic_name: String,
pub sam_interface: String,
pub sam_method_name: String,
pub sam_method_type: String,
pub sam_descriptor: String,
pub impl_descriptor: String,
pub params: Vec<LambdaParam>,
pub impl_method_handle: Handle,
}

pub struct CodegenCtx<'a> {
pub writer: &'a mut ClassFileWriter,
pub catalog: ClassCatalog,
Expand All @@ -45,6 +58,7 @@ pub struct CodegenCtx<'a> {
pub labeled_break_labels: Vec<(Ustr, ControlTarget)>,
pub labeled_continue_labels: Vec<(Ustr, ControlTarget)>,
pub cleanup_scopes: Vec<CleanupScope>,
pub lambda_info: HashMap<ExprId, LambdaInfo>,
}

impl<'a> CodegenCtx<'a> {
Expand All @@ -65,6 +79,7 @@ impl<'a> CodegenCtx<'a> {
labeled_break_labels: Vec::new(),
labeled_continue_labels: Vec::new(),
cleanup_scopes: Vec::new(),
lambda_info: HashMap::new(),
}
}

Expand Down
31 changes: 31 additions & 0 deletions crates/javac-bytecode/src/expr_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ use crate::codegen::CodegenCtx;
use javac_classfile::MethodWriter;
use javac_hir::hir::*;
use javac_ty::Ty;
use rust_asm::insn::{BootstrapArgument, Handle};
use rust_asm::opcodes;

const LAMBDA_METAFACTORY: &str = "java/lang/invoke/LambdaMetafactory";
const METAFACTORY_NAME: &str = "metafactory";
const METAFACTORY_DESC: &str = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;";

pub(crate) use arrays::array_load_opcode;
pub(crate) use convert::{cast, coerce, pop_ty, push_default_value};
pub(crate) use types::expr_ty;
Expand Down Expand Up @@ -116,6 +121,9 @@ pub fn gen_expr(mw: &mut MethodWriter, ctx: &mut CodegenCtx, body: &Body, expr_i
gen_expr(mw, ctx, body, *expr);
mw.visit_type_insn(opcodes::INSTANCEOF, &ty.internal_name());
}
Expr::Lambda { .. } => {
emit_lambda(mw, ctx, expr_id);
}
_ => push_default_value(mw, &expr_ty(ctx, body, expr_id)),
}
}
Expand Down Expand Up @@ -180,3 +188,26 @@ fn emit_ternary(
coerce(mw, &expr_ty(ctx, body, else_expr), &result_ty);
mw.visit_label(end_label);
}

fn emit_lambda(mw: &mut MethodWriter, ctx: &CodegenCtx, expr_id: ExprId) {
let Some(info) = ctx.lambda_info.get(&expr_id) else {
mw.visit_insn(opcodes::ACONST_NULL);
return;
};

let bsm = Handle {
reference_kind: rust_asm::constants::REF_INVOKE_STATIC,
owner: LAMBDA_METAFACTORY.to_string(),
name: METAFACTORY_NAME.to_string(),
descriptor: METAFACTORY_DESC.to_string(),
is_interface: false,
};

let args = vec![
BootstrapArgument::MethodType(info.sam_method_type.clone()),
BootstrapArgument::Handle(info.impl_method_handle.clone()),
BootstrapArgument::MethodType(info.impl_descriptor.clone()),
];

mw.visit_invoke_dynamic_insn(&info.sam_method_name, &info.sam_descriptor, bsm, &args);
}
20 changes: 16 additions & 4 deletions crates/javac-bytecode/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,10 +333,22 @@ impl Validator {
}
Ok(())
}
Expr::Lambda { body: lambda, .. } => match lambda {
LambdaBody::Expr(expr) => self.validate_expr(body, scope, *expr),
LambdaBody::Block(block) => self.validate_block(body, &mut scope.clone(), block),
},
Expr::Lambda {
params,
body: lambda,
..
} => {
let mut lambda_scope = scope.clone();
for param in params {
lambda_scope
.locals
.insert(param.name, param.ty.clone().unwrap_or(Ty::object()));
}
match lambda {
LambdaBody::Expr(expr) => self.validate_expr(body, &mut lambda_scope, *expr),
LambdaBody::Block(block) => self.validate_block(body, &mut lambda_scope, block),
}
}
Expr::MethodRef { target, .. } => self.validate_expr(body, scope, *target),
Expr::IntLiteral(_)
| Expr::LongLiteral(_)
Expand Down
25 changes: 25 additions & 0 deletions crates/javac-call-resolver/src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,31 @@ impl ClassCatalog {
}
}

pub fn is_interface(&self, internal_name: &str) -> bool {
self.interfaces.contains(internal_name)
}

pub fn functional_interface_method(&self, internal_name: &str) -> Option<MethodRef> {
if !self.interfaces.contains(internal_name) {
return None;
}

let mut sam: Option<MethodRef> = None;
for ((owner, _), methods) in &self.methods {
if owner == internal_name {
for m in methods {
if m.is_interface {
if sam.is_some() {
return None;
}
sam = Some(m.clone());
}
}
}
}
sam
}
Comment on lines +191 to +210
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: functional_interface_method doesn't distinguish abstract from default/static methods

The new functional_interface_method in crates/javac-call-resolver/src/catalog.rs:191-210 filters by m.is_interface to find the SAM method, but is_interface only means the method's owner is an interface — it doesn't distinguish abstract methods from default or static methods. This works today because the platform catalog (crates/javac-call-resolver/src/platform/java_util_function.rs) only registers the abstract SAM methods. If default methods (like Consumer.andThen) or static methods were added to the catalog, functional_interface_method would incorrectly count them and return None (thinking the interface has multiple abstract methods).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


pub fn resolve_static_field(&self, owner: &str, name: &str) -> Option<FieldRef> {
self.lookup_order(owner).into_iter().find_map(|owner| {
self.fields
Expand Down
Loading
Loading