From 8f840e4ebe2f0c338c9fdaead204a125aa1f3b8b Mon Sep 17 00:00:00 2001 From: Michael Reeves Date: Sun, 6 Sep 2026 15:49:57 +1000 Subject: [PATCH 01/22] add typed SSA and classfile utilities --- compiler-core/src/bitset.rs | 66 +++ compiler-core/src/classfile/bundle.rs | 112 ++++ compiler-core/src/classfile/constant_pool.rs | 213 ++++++++ compiler-core/src/classfile/key.rs | 110 ++++ compiler-core/src/classfile/mod.rs | 40 ++ compiler-core/src/classfile/registry.rs | 83 +++ compiler-core/src/classfile/registry/tests.rs | 117 +++++ compiler-core/src/classfile/summary.rs | 128 +++++ compiler-core/src/debug.rs | 9 + compiler-core/src/ir/body.rs | 492 ++++++++++++++++++ compiler-core/src/ir/builder.rs | 305 +++++++++++ compiler-core/src/ir/debug.rs | 64 +++ compiler-core/src/ir/fold.rs | 144 +++++ compiler-core/src/ir/ids.rs | 54 ++ compiler-core/src/ir/mod.rs | 24 + compiler-core/src/ir/parameters.rs | 144 +++++ compiler-core/src/ir/remap.rs | 118 +++++ compiler-core/src/ir/storage_tests.rs | 75 +++ compiler-core/src/ir/tests.rs | 327 ++++++++++++ compiler-core/src/ir/types.rs | 149 ++++++ compiler-core/src/ir/verify/dominance.rs | 86 +++ compiler-core/src/ir/verify/mod.rs | 429 +++++++++++++++ compiler-core/src/ir/verify/types.rs | 389 ++++++++++++++ compiler-core/src/opt/live.rs | 135 +++++ compiler-core/src/opt/mod.rs | 3 + compiler-core/src/scalar.rs | 297 +++++++++++ compiler-core/src/scalar/bits.rs | 35 ++ compiler-core/src/scalar/cast.rs | 50 ++ compiler-core/src/scalar/fold.rs | 46 ++ compiler-core/src/scalar/tests.rs | 127 +++++ 30 files changed, 4371 insertions(+) create mode 100644 compiler-core/src/bitset.rs create mode 100644 compiler-core/src/classfile/bundle.rs create mode 100644 compiler-core/src/classfile/constant_pool.rs create mode 100644 compiler-core/src/classfile/key.rs create mode 100644 compiler-core/src/classfile/mod.rs create mode 100644 compiler-core/src/classfile/registry.rs create mode 100644 compiler-core/src/classfile/registry/tests.rs create mode 100644 compiler-core/src/classfile/summary.rs create mode 100644 compiler-core/src/debug.rs create mode 100644 compiler-core/src/ir/body.rs create mode 100644 compiler-core/src/ir/builder.rs create mode 100644 compiler-core/src/ir/debug.rs create mode 100644 compiler-core/src/ir/fold.rs create mode 100644 compiler-core/src/ir/ids.rs create mode 100644 compiler-core/src/ir/mod.rs create mode 100644 compiler-core/src/ir/parameters.rs create mode 100644 compiler-core/src/ir/remap.rs create mode 100644 compiler-core/src/ir/storage_tests.rs create mode 100644 compiler-core/src/ir/tests.rs create mode 100644 compiler-core/src/ir/types.rs create mode 100644 compiler-core/src/ir/verify/dominance.rs create mode 100644 compiler-core/src/ir/verify/mod.rs create mode 100644 compiler-core/src/ir/verify/types.rs create mode 100644 compiler-core/src/opt/live.rs create mode 100644 compiler-core/src/opt/mod.rs create mode 100644 compiler-core/src/scalar.rs create mode 100644 compiler-core/src/scalar/bits.rs create mode 100644 compiler-core/src/scalar/cast.rs create mode 100644 compiler-core/src/scalar/fold.rs create mode 100644 compiler-core/src/scalar/tests.rs diff --git a/compiler-core/src/bitset.rs b/compiler-core/src/bitset.rs new file mode 100644 index 0000000..cbfe198 --- /dev/null +++ b/compiler-core/src/bitset.rs @@ -0,0 +1,66 @@ +//! Dense block-boundary facts shared by compiler analyses. +#[derive(Debug)] +pub struct BitMatrix { + pub(crate) words_per_row: usize, + pub(crate) words: Vec, +} + +impl BitMatrix { + pub fn new(rows: usize, local_count: usize) -> Self { + let words_per_row = local_count.div_ceil(u64::BITS as usize); + Self { + words_per_row, + words: vec![ + 0; + rows.checked_mul(words_per_row) + .expect("bit matrix capacity overflow") + ], + } + } + + pub fn row(&self, index: usize) -> &[u64] { + let start = index * self.words_per_row; + &self.words[start..start + self.words_per_row] + } + + pub fn row_mut(&mut self, index: usize) -> &mut [u64] { + let start = index * self.words_per_row; + &mut self.words[start..start + self.words_per_row] + } + + pub fn contains(&self, row: usize, local: usize) -> bool { + self.row(row) + .get(local / u64::BITS as usize) + .is_some_and(|word| word & (1 << (local % u64::BITS as usize)) != 0) + } + + pub fn iter(&self, row: usize) -> BitIter<'_> { + BitIter { + words: self.row(row), + word_index: 0, + remaining: self.row(row).first().copied().unwrap_or(0), + } + } +} + +pub struct BitIter<'a> { + words: &'a [u64], + word_index: usize, + remaining: u64, +} + +impl Iterator for BitIter<'_> { + type Item = usize; + + fn next(&mut self) -> Option { + loop { + if self.remaining != 0 { + let bit = self.remaining.trailing_zeros() as usize; + self.remaining &= self.remaining - 1; + return Some(self.word_index * u64::BITS as usize + bit); + } + self.word_index += 1; + self.remaining = *self.words.get(self.word_index)?; + } + } +} diff --git a/compiler-core/src/classfile/bundle.rs b/compiler-core/src/classfile/bundle.rs new file mode 100644 index 0000000..766061f --- /dev/null +++ b/compiler-core/src/classfile/bundle.rs @@ -0,0 +1,112 @@ +//! Streaming shard bundles. Records remain readable for exact duplicate checks +//! without retaining the class bytes in the compiler heap. +use std::{ + fs::File, + io::{self, BufWriter, Read, Seek, SeekFrom, Write}, + path::{Path, PathBuf}, + sync::Arc, +}; + +pub const MAGIC: &[u8; 8] = b"RCJVMB1\0"; + +pub struct Writer { + output: BufWriter, + path: Arc, + position: u64, + records: usize, +} + +#[derive(Clone)] +pub(super) struct Record { + path: Arc, + offset: u64, +} + +impl Writer { + pub fn create(path: &Path) -> io::Result { + let mut output = BufWriter::new(File::create(path)?); + output.write_all(MAGIC)?; + Ok(Self { + output, + path: Arc::new(path.to_owned()), + position: MAGIC.len() as u64, + records: 0, + }) + } + + pub fn is_empty(&self) -> bool { + self.records == 0 + } + + pub(super) fn append(&mut self, name: &str, bytes: &[u8]) -> io::Result { + let name_len = u32::try_from(name.len()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; + let offset = self.position + 12 + u64::from(name_len); + self.output.write_all(&name_len.to_le_bytes())?; + self.output.write_all(&(bytes.len() as u64).to_le_bytes())?; + self.output.write_all(name.as_bytes())?; + self.output.write_all(bytes)?; + // A published record must be visible to another worker immediately. + self.output.flush()?; + self.position = offset + bytes.len() as u64; + self.records += 1; + Ok(Record { + path: Arc::clone(&self.path), + offset, + }) + } +} + +impl Record { + pub(super) fn equals(&self, bytes: &[u8]) -> io::Result { + // Open only for a candidate comparison: retaining a file descriptor for + // every owner shard would exhaust the host limit on large crates. + let mut reader = File::open(&*self.path)?; + reader.seek(SeekFrom::Start(self.offset))?; + let mut buffer = [0u8; 8192]; + for expected in bytes.chunks(buffer.len()) { + let actual = &mut buffer[..expected.len()]; + reader.read_exact(actual)?; + if actual != expected { + return Ok(false); + } + } + Ok(true) + } +} + +pub fn read_magic(reader: &mut impl Read) -> io::Result<()> { + let mut magic = [0; MAGIC.len()]; + reader.read_exact(&mut magic)?; + if &magic != MAGIC { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid JVM class bundle magic", + )); + } + Ok(()) +} + +/// Read one record, distinguishing a clean end of the bundle from truncation. +pub fn read_record(reader: &mut impl Read) -> io::Result)>> { + let mut header = [0u8; 12]; + loop { + match reader.read(&mut header[..1]) { + Ok(0) => return Ok(None), + Ok(_) => break, + Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + } + } + reader.read_exact(&mut header[1..])?; + let name_len = u32::from_le_bytes(header[..4].try_into().unwrap()) as usize; + let byte_len = usize::try_from(u64::from_le_bytes(header[4..].try_into().unwrap())) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + let mut name = vec![0; name_len]; + reader.read_exact(&mut name)?; + let name = + String::from_utf8(name).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + let mut bytes = vec![0; byte_len]; + reader.read_exact(&mut bytes)?; + Ok(Some((name, bytes))) +} diff --git a/compiler-core/src/classfile/constant_pool.rs b/compiler-core/src/classfile/constant_pool.rs new file mode 100644 index 0000000..6c7a436 --- /dev/null +++ b/compiler-core/src/classfile/constant_pool.rs @@ -0,0 +1,213 @@ +use super::{self as jvm, ClassFile, Constant, ConstantPool, ReferenceKind}; +use rustc_hash::FxHashMap as HashMap; +use std::ops::Deref; + +use super::key::ConstantKey; + +#[derive(Clone, Debug)] +pub struct InternedConstantPool { + pool: ConstantPool<'static>, + constants: HashMap, +} + +impl Default for InternedConstantPool { + fn default() -> Self { + Self { + pool: ConstantPool::default(), + constants: HashMap::default(), + } + } +} + +impl Deref for InternedConstantPool { + type Target = ConstantPool<'static>; + + fn deref(&self) -> &Self::Target { + &self.pool + } +} + +impl InternedConstantPool { + pub fn into_inner(self) -> ConstantPool<'static> { + self.pool + } + + pub fn add(&mut self, constant: Constant<'static>) -> jvm::Result { + let key = ConstantKey::from(&constant); + if let Some(index) = self.constants.get(&key) { + return Ok(*index); + } + let index = self.pool.add(constant)?; + self.constants.insert(key, index); + Ok(index) + } + + pub fn add_utf8>(&mut self, value: S) -> jvm::Result { + self.add(Constant::Utf8(jvm::JavaString::from(value.as_ref()).into())) + } + + pub fn add_integer(&mut self, value: i32) -> jvm::Result { + self.add(Constant::Integer(value)) + } + + pub fn add_float(&mut self, value: f32) -> jvm::Result { + self.add(Constant::Float(value)) + } + + pub fn add_long(&mut self, value: i64) -> jvm::Result { + self.add(Constant::Long(value)) + } + + pub fn add_double(&mut self, value: f64) -> jvm::Result { + self.add(Constant::Double(value)) + } + + pub fn add_class>(&mut self, name: S) -> jvm::Result { + let name_index = self.add_utf8(name)?; + self.add(Constant::Class(name_index)) + } + + pub fn add_string>(&mut self, value: S) -> jvm::Result { + let string_index = self.add_utf8(value)?; + self.add(Constant::String(string_index)) + } + + pub fn add_field_ref, D: AsRef>( + &mut self, + class_index: u16, + name: N, + descriptor: D, + ) -> jvm::Result { + let name_and_type_index = self.add_name_and_type(name, descriptor)?; + self.add(Constant::FieldRef { + class_index, + name_and_type_index, + }) + } + + pub fn add_method_ref, D: AsRef>( + &mut self, + class_index: u16, + name: N, + descriptor: D, + ) -> jvm::Result { + let name_and_type_index = self.add_name_and_type(name, descriptor)?; + self.add(Constant::MethodRef { + class_index, + name_and_type_index, + }) + } + + pub fn add_interface_method_ref, D: AsRef>( + &mut self, + class_index: u16, + name: N, + descriptor: D, + ) -> jvm::Result { + let name_and_type_index = self.add_name_and_type(name, descriptor)?; + self.add(Constant::InterfaceMethodRef { + class_index, + name_and_type_index, + }) + } + + pub fn add_name_and_type, D: AsRef>( + &mut self, + name: N, + descriptor: D, + ) -> jvm::Result { + let name_index = self.add_utf8(name)?; + let descriptor_index = self.add_utf8(descriptor)?; + self.add(Constant::NameAndType { + name_index, + descriptor_index, + }) + } + + pub fn add_method_handle( + &mut self, + reference_kind: ReferenceKind, + reference_index: u16, + ) -> jvm::Result { + self.add(Constant::MethodHandle { + reference_kind, + reference_index, + }) + } + + pub fn add_method_type>(&mut self, descriptor: S) -> jvm::Result { + let descriptor_index = self.add_utf8(descriptor)?; + self.add(Constant::MethodType(descriptor_index)) + } + + #[allow(dead_code)] + pub fn add_dynamic, D: AsRef>( + &mut self, + bootstrap_method_attr_index: u16, + name: N, + descriptor: D, + ) -> jvm::Result { + let name_and_type_index = self.add_name_and_type(name, descriptor)?; + self.add(Constant::Dynamic { + bootstrap_method_attr_index, + name_and_type_index, + }) + } + + pub fn add_invoke_dynamic, D: AsRef>( + &mut self, + bootstrap_method_attr_index: u16, + name: N, + descriptor: D, + ) -> jvm::Result { + let name_and_type_index = self.add_name_and_type(name, descriptor)?; + self.add(Constant::InvokeDynamic { + bootstrap_method_attr_index, + name_and_type_index, + }) + } + + #[allow(dead_code)] + pub fn add_module>(&mut self, name: S) -> jvm::Result { + let name_index = self.add_utf8(name)?; + self.add(Constant::Module(name_index)) + } + + #[allow(dead_code)] + pub fn add_package>(&mut self, name: S) -> jvm::Result { + let name_index = self.add_utf8(name)?; + self.add(Constant::Package(name_index)) + } +} + +// Every generated class obtains its constants through InternedConstantPool, +// which enforces this invariant as entries are added. Keep the full scan in +// development builds as an assertion over that implementation, but do not +// rebuild and hash the entire pool immediately before every production +// serialization. +#[cfg(not(debug_assertions))] +#[inline] +pub fn verify_no_duplicate_constants(_class_file: &ClassFile<'_>) -> jvm::Result<()> { + Ok(()) +} + +#[cfg(debug_assertions)] +pub fn verify_no_duplicate_constants(class_file: &ClassFile<'_>) -> jvm::Result<()> { + let mut seen = HashMap::::default(); + for index in 1..=class_file.constant_pool.len() { + let index = index as u16; + let Ok(constant) = class_file.constant_pool.try_get(index) else { + continue; + }; + let key = ConstantKey::from(constant); + if let Some(first_index) = seen.insert(key, index) { + return Err(jvm::Error::VerificationError { + context: format!("Class constant pool for #{}", class_file.this_class), + message: format!( + "duplicate constant pool entry #{index}; first canonical entry is #{first_index}" + ), + }); + } + } + Ok(()) +} diff --git a/compiler-core/src/classfile/key.rs b/compiler-core/src/classfile/key.rs new file mode 100644 index 0000000..5b60d8f --- /dev/null +++ b/compiler-core/src/classfile/key.rs @@ -0,0 +1,110 @@ +//! Structural constant identity, shared by generation and class merging. +use super::{self as jvm, Constant}; + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum ConstantKey { + Utf8(jvm::JavaString), + Integer(i32), + Float(u32), + Long(i64), + Double(u64), + Class(u16), + String(u16), + FieldRef { + class_index: u16, + name_and_type_index: u16, + }, + MethodRef { + class_index: u16, + name_and_type_index: u16, + }, + InterfaceMethodRef { + class_index: u16, + name_and_type_index: u16, + }, + NameAndType { + name_index: u16, + descriptor_index: u16, + }, + MethodHandle { + reference_kind: u8, + reference_index: u16, + }, + MethodType(u16), + Dynamic { + bootstrap_method_attr_index: u16, + name_and_type_index: u16, + }, + InvokeDynamic { + bootstrap_method_attr_index: u16, + name_and_type_index: u16, + }, + Module(u16), + Package(u16), +} + +impl From<&Constant<'_>> for ConstantKey { + fn from(constant: &Constant<'_>) -> Self { + match constant { + Constant::Utf8(value) => ConstantKey::Utf8(value.as_ref().to_owned()), + Constant::Integer(value) => ConstantKey::Integer(*value), + Constant::Float(value) => ConstantKey::Float(value.to_bits()), + Constant::Long(value) => ConstantKey::Long(*value), + Constant::Double(value) => ConstantKey::Double(value.to_bits()), + Constant::Class(name_index) => ConstantKey::Class(*name_index), + Constant::String(string_index) => ConstantKey::String(*string_index), + Constant::FieldRef { + class_index, + name_and_type_index, + } => ConstantKey::FieldRef { + class_index: *class_index, + name_and_type_index: *name_and_type_index, + }, + Constant::MethodRef { + class_index, + name_and_type_index, + } => ConstantKey::MethodRef { + class_index: *class_index, + name_and_type_index: *name_and_type_index, + }, + Constant::InterfaceMethodRef { + class_index, + name_and_type_index, + } => ConstantKey::InterfaceMethodRef { + class_index: *class_index, + name_and_type_index: *name_and_type_index, + }, + Constant::NameAndType { + name_index, + descriptor_index, + } => ConstantKey::NameAndType { + name_index: *name_index, + descriptor_index: *descriptor_index, + }, + Constant::MethodHandle { + reference_kind, + reference_index, + } => ConstantKey::MethodHandle { + reference_kind: reference_kind.kind(), + reference_index: *reference_index, + }, + Constant::MethodType(descriptor_index) => ConstantKey::MethodType(*descriptor_index), + Constant::Dynamic { + bootstrap_method_attr_index, + name_and_type_index, + } => ConstantKey::Dynamic { + bootstrap_method_attr_index: *bootstrap_method_attr_index, + name_and_type_index: *name_and_type_index, + }, + Constant::InvokeDynamic { + bootstrap_method_attr_index, + name_and_type_index, + } => ConstantKey::InvokeDynamic { + bootstrap_method_attr_index: *bootstrap_method_attr_index, + name_and_type_index: *name_and_type_index, + }, + Constant::Module(name_index) => ConstantKey::Module(*name_index), + Constant::Package(name_index) => ConstantKey::Package(*name_index), + } + } +} diff --git a/compiler-core/src/classfile/mod.rs b/compiler-core/src/classfile/mod.rs new file mode 100644 index 0000000..eaf0d69 --- /dev/null +++ b/compiler-core/src/classfile/mod.rs @@ -0,0 +1,40 @@ +pub mod bundle; +pub mod constant_pool; +pub mod key; +pub mod registry; +pub mod summary; +pub use ristretto_classfile::byte_reader::ByteReader; +pub use ristretto_classfile::*; + +#[derive(Debug)] +pub enum Error { + ClassFile(ristretto_classfile::Error), + VerificationError { context: String, message: String }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ClassFile(error) => error.fmt(formatter), + Self::VerificationError { context, message } => { + write!(formatter, "{context}: {message}") + } + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(error: ristretto_classfile::Error) -> Self { + Self::ClassFile(error) + } +} + +impl From for Error { + fn from(error: std::num::TryFromIntError) -> Self { + Self::ClassFile(error.into()) + } +} + +pub type Result = std::result::Result; diff --git a/compiler-core/src/classfile/registry.rs b/compiler-core/src/classfile/registry.rs new file mode 100644 index 0000000..a83a610 --- /dev/null +++ b/compiler-core/src/classfile/registry.rs @@ -0,0 +1,83 @@ +use super::bundle::{Record, Writer}; +use rustc_hash::{FxHashMap, FxHasher}; +use std::{ + hash::Hasher, + io, + sync::{Arc, Mutex}, +}; + +type Variants = Mutex>; + +/// Class bytes live only in their shard bundle. Hashes filter comparisons; an +/// exact byte comparison still decides whether two contributions are identical. +#[derive(Default)] +pub struct ClassRegistry { + classes: Mutex>>, +} + +struct Variant { + hash: u64, + len: usize, + record: Record, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Emission { + Duplicate, + Written { name_collision: bool }, +} + +impl ClassRegistry { + pub fn emit(&self, output: &mut Writer, name: &str, bytes: &[u8]) -> io::Result { + let mut hasher = FxHasher::default(); + hasher.write(bytes); + self.emit_hashed(output, name, bytes, hasher.finish()) + } + + fn emit_hashed( + &self, + output: &mut Writer, + name: &str, + bytes: &[u8], + hash: u64, + ) -> io::Result { + let variants = { + let mut classes = self + .classes + .lock() + .map_err(|_| io::Error::other("class registry lock poisoned"))?; + if let Some(variants) = classes.get(name) { + Arc::clone(variants) + } else { + let variants = Arc::default(); + classes.insert(name.to_owned(), Arc::clone(&variants)); + variants + } + }; + // Only workers emitting this same class wait during a comparison/write. + // Publishing follows the successful write, so I/O failure cannot leave a + // reservation that another worker waits for forever. + let mut variants = variants + .lock() + .map_err(|_| io::Error::other("class variants lock poisoned"))?; + for variant in variants + .iter() + .filter(|v| v.hash == hash && v.len == bytes.len()) + { + if variant.record.equals(bytes)? { + return Ok(Emission::Duplicate); + } + } + let name_collision = !variants.is_empty(); + let record = output.append(name, bytes)?; + variants.push(Variant { + hash, + len: bytes.len(), + record, + }); + Ok(Emission::Written { name_collision }) + } +} + +#[cfg(test)] +mod tests; diff --git a/compiler-core/src/classfile/registry/tests.rs b/compiler-core/src/classfile/registry/tests.rs new file mode 100644 index 0000000..3e287d1 --- /dev/null +++ b/compiler-core/src/classfile/registry/tests.rs @@ -0,0 +1,117 @@ +use super::*; +use crate::classfile::bundle::{read_magic, read_record}; +use std::{ + fs, + io::Cursor, + sync::atomic::{AtomicUsize, Ordering}, +}; + +static NEXT: AtomicUsize = AtomicUsize::new(0); + +fn directory() -> std::path::PathBuf { + let path = std::env::temp_dir().join(format!( + "jvm-bundle-test-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir(&path).unwrap(); + path +} + +#[test] +fn collisions_compare_exact_bytes_after_writer_is_dropped() { + let directory = directory(); + let registry = ClassRegistry::default(); + let mut first = Writer::create(&directory.join("first")).unwrap(); + let bytes = vec![42; 20_000]; + assert_eq!( + registry.emit_hashed(&mut first, "A", &bytes, 0).unwrap(), + Emission::Written { + name_collision: false + } + ); + drop(first); + let mut second = Writer::create(&directory.join("second")).unwrap(); + assert_eq!( + registry.emit_hashed(&mut second, "A", &bytes, 0).unwrap(), + Emission::Duplicate + ); + assert!(second.is_empty()); + let mut different = bytes; + different[19_999] = 43; + assert_eq!( + registry + .emit_hashed(&mut second, "A", &different, 0) + .unwrap(), + Emission::Written { + name_collision: true + } + ); + assert_eq!( + registry + .emit_hashed(&mut second, "B", &different, 0) + .unwrap(), + Emission::Written { + name_collision: false + } + ); + drop(second); + drop(registry); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn concurrent_writers_publish_each_exact_contribution_once() { + let directory = directory(); + let registry = ClassRegistry::default(); + let barrier = std::sync::Barrier::new(4); + std::thread::scope(|scope| { + for worker in 0..4 { + let registry = ®istry; + let barrier = &barrier; + let directory = &directory; + scope.spawn(move || { + let mut writer = Writer::create(&directory.join(worker.to_string())).unwrap(); + barrier.wait(); + for class in 0..64 { + registry + .emit(&mut writer, &format!("C{class}"), &[class; 32]) + .unwrap(); + } + }); + } + }); + let mut records = std::collections::BTreeMap::new(); + for worker in 0..4 { + let mut reader = fs::File::open(directory.join(worker.to_string())).unwrap(); + read_magic(&mut reader).unwrap(); + while let Some((name, bytes)) = read_record(&mut reader).unwrap() { + assert_eq!(bytes, vec![name[1..].parse::().unwrap(); 32]); + assert!(records.insert(name, bytes).is_none()); + } + } + assert_eq!(records.len(), 64); + drop(registry); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn truncated_records_are_errors_and_empty_tail_is_valid() { + assert!(read_record(&mut Cursor::new([])).unwrap().is_none()); + for length in 1..12 { + assert_eq!( + read_record(&mut Cursor::new(vec![0; length])) + .unwrap_err() + .kind(), + io::ErrorKind::UnexpectedEof + ); + } + let mut bytes = Vec::new(); + bytes.extend_from_slice(&1u32.to_le_bytes()); + bytes.extend_from_slice(&3u64.to_le_bytes()); + bytes.extend_from_slice(b"A12"); + assert_eq!( + read_record(&mut Cursor::new(bytes)).unwrap_err().kind(), + io::ErrorKind::UnexpectedEof + ); +} diff --git a/compiler-core/src/classfile/summary.rs b/compiler-core/src/classfile/summary.rs new file mode 100644 index 0000000..37757bd --- /dev/null +++ b/compiler-core/src/classfile/summary.rs @@ -0,0 +1,128 @@ +//! Read class identity and entry-point metadata without decoding method bodies. +//! Constant strings borrow the input; only the returned class name is allocated. +use super::JavaStr; +use std::io; + +#[derive(Debug, PartialEq, Eq)] +pub struct Summary { + pub name: String, + pub has_main: bool, +} + +#[derive(Clone, Copy)] +enum Constant<'a> { + Other, + Utf8(&'a [u8]), + Class(u16), +} + +struct Reader<'a> { + bytes: &'a [u8], +} + +fn invalid() -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, "invalid JVM class metadata") +} + +impl<'a> Reader<'a> { + fn take(&mut self, count: usize) -> io::Result<&'a [u8]> { + let (head, tail) = self.bytes.split_at_checked(count).ok_or_else(invalid)?; + self.bytes = tail; + Ok(head) + } + fn u8(&mut self) -> io::Result { + Ok(self.take(1)?[0]) + } + fn u16(&mut self) -> io::Result { + Ok(u16::from_be_bytes(self.take(2)?.try_into().unwrap())) + } + fn u32(&mut self) -> io::Result { + Ok(u32::from_be_bytes(self.take(4)?.try_into().unwrap())) + } + fn attributes(&mut self) -> io::Result<()> { + for _ in 0..self.u16()? { + self.u16()?; + let count = usize::try_from(self.u32()?).map_err(|_| invalid())?; + self.take(count)?; + } + Ok(()) + } +} + +pub fn read(bytes: &[u8]) -> io::Result { + let mut r = Reader { bytes }; + if r.take(4)? != b"\xca\xfe\xba\xbe" { + return Err(invalid()); + } + r.take(4)?; // minor/major version + let count = usize::from(r.u16()?); + let mut constants = vec![Constant::Other; count]; + let mut index = 1; + while index < count { + constants[index] = match r.u8()? { + 1 => { + let len = usize::from(r.u16()?); + Constant::Utf8(r.take(len)?) + } + 7 => Constant::Class(r.u16()?), + 3 | 4 => { + r.take(4)?; + Constant::Other + } + 5 | 6 => { + r.take(8)?; + index += 1; + if index >= count { + return Err(invalid()); + } + Constant::Other + } + 8 | 16 | 19 | 20 => { + r.take(2)?; + Constant::Other + } + 9 | 10 | 11 | 12 | 17 | 18 => { + r.take(4)?; + Constant::Other + } + 15 => { + r.take(3)?; + Constant::Other + } + _ => return Err(invalid()), + }; + index += 1; + } + let utf8 = |index: u16| match constants.get(usize::from(index)) { + Some(Constant::Utf8(bytes)) => Ok(*bytes), + _ => Err(invalid()), + }; + r.u16()?; // access flags + let Some(Constant::Class(name_index)) = constants.get(usize::from(r.u16()?)) else { + return Err(invalid()); + }; + let name = JavaStr::from_mutf8(utf8(*name_index)?) + .map_err(|_| invalid())? + .to_rust_string(); + r.u16()?; // superclass + let interface_count = usize::from(r.u16()?); + r.take(interface_count * 2)?; + for _ in 0..r.u16()? { + r.take(6)?; // flags, name, descriptor + r.attributes()?; + } + let mut has_main = false; + for _ in 0..r.u16()? { + let flags = r.u16()?; + let name = utf8(r.u16()?)?; + let descriptor = utf8(r.u16()?)?; + has_main |= + flags & 0x0009 == 0x0009 && name == b"main" && descriptor == b"([Ljava/lang/String;)V"; + r.attributes()?; + } + r.attributes()?; + if !r.bytes.is_empty() { + return Err(invalid()); + } + Ok(Summary { name, has_main }) +} diff --git a/compiler-core/src/debug.rs b/compiler-core/src/debug.rs new file mode 100644 index 0000000..65808f2 --- /dev/null +++ b/compiler-core/src/debug.rs @@ -0,0 +1,9 @@ +/// A Rust source position attached to generated code. +/// +/// JVM line tables only store line numbers; the corresponding file name is +/// stored once on the containing class through its `SourceFile` attribute. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SourceLocation { + pub file_name: String, + pub line: u32, +} diff --git a/compiler-core/src/ir/body.rs b/compiler-core/src/ir/body.rs new file mode 100644 index 0000000..9cfcdac --- /dev/null +++ b/compiler-core/src/ir/body.rs @@ -0,0 +1,492 @@ +use super::*; +use crate::scalar::{BinaryOp, Scalar}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ValueDef { + Inst(InstId), + Param(BlockId), + Alias(ValueId), + Unreachable, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Value { + pub ty: TypeId, + pub def: ValueDef, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum CallKind { + Constructor, + RustStatic, + JvmStatic, + Virtual, + Interface, + Indirect, +} + +/// A body-local call target. Signatures describe value-bearing JVM arguments; +/// source-language unit arguments never enter the operand pool. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct MethodRef { + pub owner: String, + pub name: String, + pub params: Vec, + pub returns: TypeId, + pub interface: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct FieldRef { + pub owner: TypeId, + pub name: String, + pub ty: TypeId, + pub is_static: bool, + /// Generated Rust pointer fields store a base plus two displacement fields. + pub relative_pointer: bool, +} + +/// A field view preserves Rust byte layout and the runtime allocation identity. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PointerProjection { + pub field: MemberId, + pub offset: u64, + pub size: u64, + pub codec: Option, +} + +/// Addressable local storage retains its Rust allocation layout. Ordinary SSA +/// values have no storage record. Codec names use the body's shared vocabulary. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct StorageSlot { + pub ty: TypeId, + pub size: u32, + pub alignment: u32, + pub codec: Option, +} + +impl StorageSlot { + pub fn scalar(ty: TypeId, types: &Types) -> Option { + use crate::scalar::ScalarType::*; + let Type::Scalar(scalar) = types.get(ty)? else { + return None; + }; + let size = match scalar { + Bool | I8 | U8 => 1, + I16 | U16 => 2, + I32 | U32 | F32 => 4, + I64 | U64 | F64 => 8, + _ => return None, + }; + Some(Self { + ty, + size, + alignment: size, + codec: None, + }) + } +} + +/// Operands are SSA handles. Type/member/pointer semantics survive until selection. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Op { + Constant(ConstId), + /// Exception delivered by the current unwind edge. + Exception, + /// Number of elements in the physical JVM carrier (an int). + ArrayLength(ValueId), + Binary { + op: BinaryOp, + left: ValueId, + right: ValueId, + }, + /// Overflow flag for [left, right, wrapped result]. The uncommon payload + /// stays in the operand pool instead of increasing every instruction. + Overflow { + op: BinaryOp, + args: List, + }, + Not(ValueId), + Bit { + op: crate::scalar::BitOp, + value: ValueId, + }, + Opaque(ValueId), + Neg(ValueId), + Cast(ValueId), + /// Representation adaptation at a JVM ABI boundary (boxing, views, casts). + Adapt(ValueId), + /// Same physical JVM carrier with a different semantic type annotation. + Reinterpret(ValueId), + NewArray(ValueId), + FunctionPointer { + signature: MethodId, + target: MethodId, + }, + LoadSlot(SlotId), + StoreSlot { + slot: SlotId, + value: ValueId, + }, + AddressOfSlot(SlotId), + Load(ValueId), + Store { + pointer: ValueId, + value: ValueId, + }, + Project { + base: ValueId, + projection: ProjectionId, + }, + Offset { + pointer: ValueId, + offset: ValueId, + bytes: bool, + wrapping: bool, + }, + Call { + method: MethodId, + kind: CallKind, + args: List, + }, + GetField { + object: ValueId, + field: MemberId, + }, + SetField { + object: ValueId, + field: MemberId, + value: ValueId, + }, + GetStatic(MemberId), + SetStatic { + field: MemberId, + value: ValueId, + }, + ArrayGet { + array: ValueId, + index: ValueId, + }, + ArraySet { + array: ValueId, + index: ValueId, + value: ValueId, + }, + /// Logical Rust length, including zero-sized slices larger than JVM arrays. + Length(ValueId), + /// Immutable fat-pointer carrier; data retains allocation identity and view. + View { + data: ValueId, + length: ValueId, + }, + ViewData { + view: ValueId, + size: u32, + codec: Option, + }, +} + +impl Op { + /// Language-level traps and representation operations need unwind edges; + /// scalar computations and literal loads do not. + pub fn may_throw(self, body: &Body, types: &Types) -> bool { + match self { + Self::Constant(id) => matches!(body.constants[id.index()], Constant::External { .. }), + Self::Exception + | Self::Reinterpret(_) + | Self::Not(_) + | Self::Neg(_) + | Self::Bit { .. } + | Self::Overflow { .. } => false, + Self::Binary { + op: BinaryOp::Div | BinaryOp::Rem, + left, + .. + } => { + matches!(types.get(body.value_type(left)), Some(Type::Scalar(ty)) if ty.integer().is_some()) + } + Self::Binary { .. } => false, + Self::Cast(value) => { + !matches!(types.get(body.value_type(value)), Some(Type::Scalar(_))) + } + _ => true, + } + } + pub fn visit_uses(self, args: &[ValueId], mut visit: impl FnMut(ValueId)) { + match self { + Self::Binary { left, right, .. } => { + visit(left); + visit(right); + } + Self::Not(v) + | Self::Opaque(v) + | Self::Bit { value: v, .. } + | Self::Neg(v) + | Self::Cast(v) + | Self::Adapt(v) + | Self::Reinterpret(v) + | Self::NewArray(v) + | Self::ArrayLength(v) + | Self::Load(v) + | Self::Length(v) => visit(v), + Self::StoreSlot { value, .. } | Self::SetStatic { value, .. } => visit(value), + Self::Store { pointer, value } => { + visit(pointer); + visit(value); + } + Self::Project { base, .. } => visit(base), + Self::ViewData { view, .. } => visit(view), + Self::View { data, length } => { + visit(data); + visit(length); + } + Self::Offset { + pointer, offset, .. + } => { + visit(pointer); + visit(offset); + } + Self::Call { args: list, .. } | Self::Overflow { args: list, .. } => { + for &arg in &args[list.range()] { + visit(arg); + } + } + Self::GetField { object, .. } => visit(object), + Self::SetField { object, value, .. } => { + visit(object); + visit(value); + } + Self::ArrayGet { array, index } => { + visit(array); + visit(index); + } + Self::ArraySet { + array, + index, + value, + } => { + visit(array); + visit(index); + visit(value); + } + Self::Constant(_) + | Self::Exception + | Self::LoadSlot(_) + | Self::AddressOfSlot(_) + | Self::GetStatic(_) + | Self::FunctionPointer { .. } => {} + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Inst { + pub op: Op, + pub result: Option, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Constant { + Scalar(Scalar), + Unit, + Null(TypeId), + /// Initial contents of an uninitialized source local. Valid Rust control + /// flow must overwrite this before observation; edge copies may carry it. + Uninit(TypeId), + /// Handle into the immutable representation-constant pool supplied by the + /// embedding compiler. Its emitter must produce one typed stack value. + External { + index: u32, + ty: TypeId, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Edge { + pub target: BlockId, + pub args: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Terminator { + Jump(EdgeId), + Branch { + condition: ValueId, + yes: EdgeId, + no: EdgeId, + }, + Switch { + value: ValueId, + cases: List, + otherwise: EdgeId, + }, + Return(Option), + Throw { + value: ValueId, + unwind: Option, + }, + Rethrow, + Unreachable, + /// The normal successor is a dedicated single-predecessor continuation. + /// The instruction result is defined there, never on the exceptional edge. + Invoke { + inst: InstId, + normal: EdgeId, + unwind: EdgeId, + }, +} + +impl Terminator { + pub fn visit_uses(self, mut visit: impl FnMut(ValueId)) { + match self { + Self::Branch { + condition: value, .. + } + | Self::Switch { value, .. } + | Self::Return(Some(value)) + | Self::Throw { value, .. } => visit(value), + _ => {} + } + } + pub fn visit_edges(self, cases: &[(Scalar, EdgeId)], mut visit: impl FnMut(EdgeId)) { + match self { + Self::Jump(edge) => visit(edge), + Self::Branch { yes, no, .. } => { + visit(yes); + visit(no); + } + Self::Invoke { normal, unwind, .. } => { + visit(normal); + visit(unwind); + } + Self::Switch { + cases: list, + otherwise, + .. + } => { + for &(_, edge) in &cases[list.range()] { + visit(edge); + } + visit(otherwise); + } + Self::Throw { unwind, .. } => { + if let Some(edge) = unwind { + visit(edge); + } + } + Self::Return(_) | Self::Rethrow | Self::Unreachable => {} + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct Block { + pub params: Vec, + pub instructions: Vec, + pub terminator: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Body { + pub entry: BlockId, + pub return_type: TypeId, + pub blocks: Vec, + pub values: Vec, + pub instructions: Vec, + pub constants: Vec, + pub args: Vec, + pub edges: Vec, + pub cases: Vec<(Scalar, EdgeId)>, + pub slots: Vec, + pub methods: Vec, + pub fields: Vec, + pub projections: Vec, +} + +impl Body { + pub fn new(return_type: TypeId) -> Self { + Self { + entry: BlockId::new(0), + return_type, + blocks: vec![Block::default()], + values: Vec::new(), + instructions: Vec::new(), + constants: Vec::new(), + args: Vec::new(), + edges: Vec::new(), + cases: Vec::new(), + slots: Vec::new(), + methods: Vec::new(), + fields: Vec::new(), + projections: Vec::new(), + } + } + pub fn resolve(&self, mut value: ValueId) -> ValueId { + while let ValueDef::Alias(next) = self.values[value.index()].def { + value = next; + } + value + } + pub(super) fn resolve_mut(&mut self, mut value: ValueId) -> ValueId { + let root = self.resolve(value); + while let ValueDef::Alias(next) = self.values[value.index()].def { + self.values[value.index()].def = ValueDef::Alias(root); + value = next; + } + root + } + pub fn value_type(&self, value: ValueId) -> TypeId { + self.values[value.index()].ty + } + pub fn predecessors(&self) -> Vec> { + let mut result = vec![Vec::new(); self.blocks.len()]; + for (index, block) in self.blocks.iter().enumerate() { + if let Some(term) = block.terminator { + term.visit_edges(&self.cases, |edge| { + result[self.edges[edge.index()].target.index()] + .push((BlockId::new(index), edge)) + }); + } + } + result + } + pub fn reachable(&self) -> Vec { + let mut visited = vec![false; self.blocks.len()]; + let mut pending = vec![self.entry]; + while let Some(block) = pending.pop() { + if std::mem::replace(&mut visited[block.index()], true) { + continue; + } + if let Some(term) = self.blocks[block.index()].terminator { + term.visit_edges(&self.cases, |edge| { + pending.push(self.edges[edge.index()].target) + }); + } + } + visited + } + /// Place normal continuations together and cold unwind paths afterwards. + /// The iterative reverse postorder also handles loops without recursion. + pub fn layout(&self) -> Vec { + let mut seen = vec![false; self.blocks.len()]; + let mut pending = vec![(self.entry, false)]; + let mut order = Vec::new(); + while let Some((block, exiting)) = pending.pop() { + if exiting { + order.push(block); + continue; + } + if std::mem::replace(&mut seen[block.index()], true) { + continue; + } + pending.push((block, true)); + if let Some(term) = self.blocks[block.index()].terminator { + term.visit_edges(&self.cases, |edge| { + pending.push((self.edges[edge.index()].target, false)) + }); + } + } + order.reverse(); + order + } +} diff --git a/compiler-core/src/ir/builder.rs b/compiler-core/src/ir/builder.rs new file mode 100644 index 0000000..7038add --- /dev/null +++ b/compiler-core/src/ir/builder.rs @@ -0,0 +1,305 @@ +use super::*; +use crate::scalar::{Scalar, ScalarType}; +use rustc_hash::FxHashMap; +use std::collections::VecDeque; + +/// Mutable source bindings exist only during construction. Unresolved reads +/// create block parameters; finalization fills their incoming arguments using +/// an iterative queue, including loop backedges and synthetic continuations. +pub struct Builder<'a> { + pub body: Body, + pub(super) types: &'a Types, + current: BlockId, + variables: Vec, + uninitialized: Vec, + bindings: FxHashMap<(BlockId, VariableId), ValueId>, + pending: VecDeque<(BlockId, VariableId, ValueId, usize)>, + constants: FxHashMap<(BlockId, Scalar), ValueId>, +} + +impl<'a> Builder<'a> { + pub fn new(types: &'a Types, return_type: TypeId) -> Self { + let body = Body::new(return_type); + Self { + current: body.entry, + body, + types, + variables: Vec::new(), + uninitialized: Vec::new(), + bindings: FxHashMap::default(), + pending: VecDeque::new(), + constants: FxHashMap::default(), + } + } + pub fn current(&self) -> BlockId { + self.current + } + pub fn create_block(&mut self) -> BlockId { + let block = BlockId::new(self.body.blocks.len()); + self.body.blocks.push(Block::default()); + block + } + pub fn switch_to(&mut self, block: BlockId) { + assert!(block.index() < self.body.blocks.len()); + self.current = block; + } + fn value(&mut self, ty: TypeId, def: ValueDef) -> ValueId { + let value = ValueId::new(self.body.values.len()); + self.body.values.push(Value { ty, def }); + value + } + pub fn parameter(&mut self, block: BlockId, ty: TypeId) -> ValueId { + let value = self.value(ty, ValueDef::Param(block)); + self.body.blocks[block.index()].params.push(value); + value + } + pub fn variable(&mut self, ty: TypeId) -> VariableId { + let var = VariableId::new(self.variables.len()); + self.variables.push(ty); + self.uninitialized.push(false); + var + } + /// A declared source local may be uninitialized on paths excluded by a + /// separate drop flag. Generated temporaries continue to require a definition. + pub fn local(&mut self, ty: TypeId) -> VariableId { + let var = self.variable(ty); + self.uninitialized[var.index()] = true; + var + } + pub fn define(&mut self, var: VariableId, value: ValueId) { + assert_eq!( + self.variables[var.index()], + self.body.value_type(value), + "source variable changes type" + ); + self.bindings.insert((self.current, var), value); + } + /// Source emission may use several semantic types for the same physical + /// binding. Keep the precise value until a join actually needs a common type. + pub fn define_carrier(&mut self, var: VariableId, value: ValueId) { + assert_eq!( + self.types + .get(self.variables[var.index()]) + .unwrap() + .carrier(), + self.types + .get(self.body.value_type(value)) + .unwrap() + .carrier() + ); + self.bindings.insert((self.current, var), value); + } + pub fn read(&mut self, var: VariableId) -> ValueId { + self.read_in(self.current, var) + } + fn read_in(&mut self, block: BlockId, var: VariableId) -> ValueId { + if let Some(&value) = self.bindings.get(&(block, var)) { + return value; + } + if block == self.body.entry && self.uninitialized[var.index()] { + let ty = self.variables[var.index()]; + let constant = ConstId::new(self.body.constants.len()); + self.body.constants.push(Constant::Uninit(ty)); + let inst = InstId::new(self.body.instructions.len()); + let value = self.value(ty, ValueDef::Inst(inst)); + self.body.instructions.push(Inst { + op: Op::Constant(constant), + result: Some(value), + }); + self.body.blocks[block.index()].instructions.push(inst); + self.bindings.insert((block, var), value); + return value; + } + let index = self.body.blocks[block.index()].params.len(); + let value = self.parameter(block, self.variables[var.index()]); + self.bindings.insert((block, var), value); + self.pending.push_back((block, var, value, index)); + value + } + fn instruction(&mut self, op: Op, ty: Option) -> InstId { + assert!( + self.body.blocks[self.current.index()].terminator.is_none(), + "instruction after terminator" + ); + let inst = InstId::new(self.body.instructions.len()); + let result = ty.map(|ty| self.value(ty, ValueDef::Inst(inst))); + self.body.instructions.push(Inst { op, result }); + inst + } + pub fn emit(&mut self, op: Op, ty: Option) -> Option { + assert!( + self.body.blocks[self.current.index()].terminator.is_none(), + "instruction after terminator" + ); + if let Some(value) = ty.and_then(|ty| self.fold(op, ty)) { + return Some(value); + } + let inst = self.instruction(op, ty); + self.body.blocks[self.current.index()] + .instructions + .push(inst); + self.body.instructions[inst.index()].result + } + pub fn constant(&mut self, ty: TypeId, scalar: Scalar) -> ValueId { + assert_eq!(self.types.get(ty), Some(Type::Scalar(scalar.ty()))); + if let Some(&value) = self.constants.get(&(self.current, scalar)) { + return value; + } + let id = ConstId::new(self.body.constants.len()); + self.body.constants.push(Constant::Scalar(scalar)); + let value = self.emit(Op::Constant(id), Some(ty)).unwrap(); + self.constants.insert((self.current, scalar), value); + value + } + pub fn args(&mut self, values: impl IntoIterator) -> List { + List::append(&mut self.body.args, values) + } + pub fn method(&mut self, method: MethodRef) -> MethodId { + if let Some(index) = self.body.methods.iter().position(|m| *m == method) { + return MethodId::new(index); + } + let id = MethodId::new(self.body.methods.len()); + self.body.methods.push(method); + id + } + pub fn field(&mut self, field: FieldRef) -> MemberId { + if let Some(index) = self.body.fields.iter().position(|f| *f == field) { + return MemberId::new(index); + } + let id = MemberId::new(self.body.fields.len()); + self.body.fields.push(field); + id + } + pub fn projection(&mut self, projection: PointerProjection) -> ProjectionId { + if let Some(index) = self.body.projections.iter().position(|p| *p == projection) { + return ProjectionId::new(index); + } + let id = ProjectionId::new(self.body.projections.len()); + self.body.projections.push(projection); + id + } + pub fn edge(&mut self, target: BlockId, args: Vec) -> EdgeId { + let edge = EdgeId::new(self.body.edges.len()); + self.body.edges.push(Edge { target, args }); + edge + } + pub fn terminate(&mut self, terminator: Terminator) { + assert!( + self.body.blocks[self.current.index()] + .terminator + .replace(terminator) + .is_none(), + "two terminators in one block" + ); + } + pub fn jump(&mut self, target: BlockId, args: Vec) { + let edge = self.edge(target, args); + self.terminate(Terminator::Jump(edge)); + } + pub fn branch(&mut self, condition: ValueId, yes: BlockId, no: BlockId) { + assert!(matches!( + self.types.get(self.body.value_type(condition)), + Some(Type::Scalar(ScalarType::Bool)) + )); + if let Some(constant) = self.scalar_value(condition) { + self.jump(if constant.bits() != 0 { yes } else { no }, Vec::new()); + return; + } + let yes = self.edge(yes, Vec::new()); + let no = self.edge(no, Vec::new()); + self.terminate(Terminator::Branch { condition, yes, no }); + } + pub fn switch( + &mut self, + value: ValueId, + targets: impl IntoIterator, + otherwise: BlockId, + ) { + if let Some(constant) = self.scalar_value(value) { + let target = targets + .into_iter() + .find_map(|(key, target)| (key == constant).then_some(target)) + .unwrap_or(otherwise); + self.jump(target, Vec::new()); + return; + } + let start = self.body.cases.len(); + for (key, target) in targets { + let edge = self.edge(target, Vec::new()); + self.body.cases.push((key, edge)); + } + let cases = List { + start: u32::try_from(start).expect("switch pool capacity"), + len: u32::try_from(self.body.cases.len() - start).expect("switch capacity"), + }; + let otherwise = self.edge(otherwise, Vec::new()); + self.terminate(Terminator::Switch { + value, + cases, + otherwise, + }); + } + /// End the protected block and enter a fresh normal continuation. Source + /// assignment happens after this call, so unwind bindings retain old values. + pub fn invoke(&mut self, op: Op, ty: Option, handler: BlockId) -> Option { + if !op.may_throw(&self.body, self.types) { + return self.emit(op, ty); + } + if let Some(value) = ty.and_then(|ty| self.fold(op, ty)) { + return Some(value); + } + let inst = self.instruction(op, ty); + let continuation = self.create_block(); + let normal = self.edge(continuation, Vec::new()); + let unwind = self.edge(handler, Vec::new()); + self.terminate(Terminator::Invoke { + inst, + normal, + unwind, + }); + self.switch_to(continuation); + self.body.instructions[inst.index()].result + } + + pub fn finish(mut self) -> Result { + let predecessors = self.body.predecessors(); + let reachable = self.body.reachable(); + while let Some((block, var, value, index)) = self.pending.pop_front() { + if predecessors[block.index()].is_empty() { + if reachable[block.index()] { + return Err(VerifyError(format!( + "undefined variable {var:?} in {block:?}" + ))); + } + self.body.values[value.index()].def = ValueDef::Unreachable; + continue; + } + for &(source, edge) in &predecessors[block.index()] { + let mut incoming = self.read_in(source, var); + let ty = self.variables[var.index()]; + if self.body.value_type(incoming) != ty { + let inst = InstId::new(self.body.instructions.len()); + let result = self.value(ty, ValueDef::Inst(inst)); + self.body.instructions.push(Inst { + op: Op::Reinterpret(incoming), + result: Some(result), + }); + self.body.blocks[source.index()].instructions.push(inst); + incoming = result; + } + let args = &mut self.body.edges[edge.index()].args; + // Pending parameters are filled in declaration order. Explicit + // parameters must already have arguments on the branch edge. + if args.len() != index { + return Err(VerifyError(format!( + "edge {edge:?} has inconsistent explicit parameters" + ))); + } + args.push(incoming); + } + } + super::parameters::remove_trivial_parameters(&mut self.body, &predecessors); + verify(&self.body, self.types)?; + Ok(self.body) + } +} diff --git a/compiler-core/src/ir/debug.rs b/compiler-core/src/ir/debug.rs new file mode 100644 index 0000000..87ba598 --- /dev/null +++ b/compiler-core/src/ir/debug.rs @@ -0,0 +1,64 @@ +//! Optional source bindings. These are absent from release bodies and never +//! participate in computational identity or change SSA definitions. +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DebugLocal { + Value(TypeId), + /// The debugger observes the stable runtime cell, including alias writes. + Storage(SlotId), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DebugVariable { + pub name: String, + pub local: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DebugChange { + Set { local: u32, value: ValueId }, + Clear(u32), + Scope(u32), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DebugEvent { + pub block: BlockId, + /// Before this instruction in the block; len denotes its terminator. + pub position: u32, + pub change: DebugChange, + pub line: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +pub struct DebugInfo { + pub locals: Vec, + pub variables: Vec, + pub scopes: Vec>, + pub events: Vec, +} + +impl DebugInfo { + pub fn push(&mut self, builder: &Builder<'_>, change: DebugChange) -> &mut DebugEvent { + let block = builder.current(); + self.events.push(DebugEvent { + block, + position: u32::try_from(builder.body.blocks[block.index()].instructions.len()) + .expect("debug position limit"), + change, + line: None, + }); + self.events.last_mut().unwrap() + } + + pub fn roots<'a>(&'a self, body: &'a Body) -> impl Iterator + 'a { + let reachable = body.reachable(); + self.events + .iter() + .filter_map(move |event| match event.change { + DebugChange::Set { value, .. } if reachable[event.block.index()] => Some(value), + _ => None, + }) + } +} diff --git a/compiler-core/src/ir/fold.rs b/compiler-core/src/ir/fold.rs new file mode 100644 index 0000000..9599a89 --- /dev/null +++ b/compiler-core/src/ir/fold.rs @@ -0,0 +1,144 @@ +use super::*; +use crate::scalar::{BinaryFold, Scalar, fold_binary}; + +impl Builder<'_> { + pub(super) fn scalar_value(&self, value: ValueId) -> Option { + let value = self.body.resolve(value); + let ValueDef::Inst(inst) = self.body.values[value.index()].def else { + return None; + }; + let Op::Constant(constant) = self.body.instructions[inst.index()].op else { + return None; + }; + let Constant::Scalar(scalar) = self.body.constants[constant.index()] else { + return None; + }; + Some(scalar) + } + + pub(super) fn fold(&mut self, op: Op, result: TypeId) -> Option { + if let Op::Length(view) = op { + let view = self.body.resolve(view); + if let ValueDef::Inst(inst) = self.body.values[view.index()].def + && let Op::View { length, .. } = self.body.instructions[inst.index()].op + && self.body.value_type(length) == result + { + return Some(length); + } + } + let Some(Type::Scalar(result_ty)) = self.types.get(result) else { + return None; + }; + let constant = match op { + Op::Binary { op, left, right } => { + let Some(Type::Scalar(left_ty)) = self.types.get(self.body.value_type(left)) else { + return None; + }; + let Some(Type::Scalar(right_ty)) = self.types.get(self.body.value_type(right)) + else { + return None; + }; + let result = fold_binary( + op, + left_ty, + right_ty, + self.scalar_value(left), + self.scalar_value(right), + || self.body.resolve(left) == self.body.resolve(right), + )?; + match result { + BinaryFold::Left if result_ty == left_ty => return Some(left), + BinaryFold::Right if result_ty == right_ty => return Some(right), + BinaryFold::Constant(value) => value, + _ => return None, + } + } + Op::Not(value) => self.scalar_value(value)?.not()?, + Op::Bit { op, value } => self.scalar_value(value)?.bit(op)?, + Op::Overflow { op, args } => { + let args = &self.body.args[args.range()]; + let a = self.scalar_value(args[0])?; + let b = self.scalar_value(args[1])?; + Scalar::boolean(a.overflows(op, b)?) + } + Op::Neg(value) => self.scalar_value(value)?.neg()?, + Op::Cast(value) => { + if self.body.value_type(value) == result { + return Some(value); + } + self.scalar_value(value)?.cast(result_ty)? + } + _ => return None, + }; + (constant.ty() == result_ty).then(|| self.constant(result, constant)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scalar::{BinaryOp, ScalarType}; + + #[test] + fn folds_during_construction_without_erasing_traps_or_float_semantics() { + let mut types = Types::default(); + let int = types.scalar(ScalarType::I32); + let float = types.scalar(ScalarType::F32); + let mut b = Builder::new(&types, int); + let x = b.parameter(b.current(), int); + let f = b.parameter(b.current(), float); + let zero = b.constant(int, Scalar::integer(ScalarType::I32, 0).unwrap()); + let one = b.constant(int, Scalar::integer(ScalarType::I32, 1).unwrap()); + let add = b + .emit( + Op::Binary { + op: BinaryOp::Add, + left: x, + right: zero, + }, + Some(int), + ) + .unwrap(); + assert_eq!(add, x); + let cast = b.emit(Op::Cast(x), Some(int)).unwrap(); + assert_eq!(cast, x); + let count = b.body.instructions.len(); + let folded = b + .emit( + Op::Binary { + op: BinaryOp::Add, + left: zero, + right: one, + }, + Some(int), + ) + .unwrap(); + assert_eq!(folded, one); + assert_eq!(b.body.instructions.len(), count); + let trapping = b + .emit( + Op::Binary { + op: BinaryOp::Div, + left: zero, + right: x, + }, + Some(int), + ) + .unwrap(); + assert_ne!(trapping, zero); + let float_zero = b.constant(float, Scalar::f32(0.0)); + let float_add = b + .emit( + Op::Binary { + op: BinaryOp::Add, + left: f, + right: float_zero, + }, + Some(float), + ) + .unwrap(); + assert_ne!(float_add, f); + b.terminate(Terminator::Return(Some(x))); + b.finish().unwrap(); + } +} diff --git a/compiler-core/src/ir/ids.rs b/compiler-core/src/ir/ids.rs new file mode 100644 index 0000000..faeed84 --- /dev/null +++ b/compiler-core/src/ir/ids.rs @@ -0,0 +1,54 @@ +use std::num::NonZeroU32; + +macro_rules! ids { + ($($name:ident),* $(,)?) => {$( + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[repr(transparent)] + pub struct $name(NonZeroU32); + impl $name { + pub fn new(index: usize) -> Self { + let index = u32::try_from(index).ok().and_then(|i| i.checked_add(1)) + .and_then(NonZeroU32::new).expect("IR table exceeds 32-bit identifier capacity"); + Self(index) + } + pub fn index(self) -> usize { (self.0.get() - 1) as usize } + } + )*} +} + +ids!( + ValueId, + InstId, + BlockId, + EdgeId, + VariableId, + TypeId, + SymbolId, + ConstId, + SlotId, + MemberId, + MethodId, + ProjectionId +); + +/// Contiguous storage for uncommon variable-length payloads. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct List { + pub start: u32, + pub len: u32, +} + +impl List { + pub fn append(pool: &mut Vec, values: impl IntoIterator) -> Self { + let start = u32::try_from(pool.len()).expect("IR payload pool exceeds 32-bit capacity"); + pool.extend(values); + let end = u32::try_from(pool.len()).expect("IR payload pool exceeds 32-bit capacity"); + Self { + start, + len: end - start, + } + } + pub fn range(self) -> std::ops::Range { + self.start as usize..self.start as usize + self.len as usize + } +} diff --git a/compiler-core/src/ir/mod.rs b/compiler-core/src/ir/mod.rs new file mode 100644 index 0000000..9194851 --- /dev/null +++ b/compiler-core/src/ir/mod.rs @@ -0,0 +1,24 @@ +//! Typed SSA bodies. Identifiers are local to their owning compilation context. +mod debug; +pub use debug::*; +mod body; +mod builder; +mod fold; +mod ids; +mod parameters; +mod remap; +mod types; +pub use remap::Remap; +mod verify; + +pub use body::*; +pub use builder::Builder; +pub use ids::*; +pub use types::*; +pub use verify::{VerifyError, verify, verify_with_debug}; + +#[cfg(test)] +mod tests; + +#[cfg(test)] +mod storage_tests; diff --git a/compiler-core/src/ir/parameters.rs b/compiler-core/src/ir/parameters.rs new file mode 100644 index 0000000..d6d8cc9 --- /dev/null +++ b/compiler-core/src/ir/parameters.rs @@ -0,0 +1,144 @@ +use super::*; +use std::collections::VecDeque; + +const NONE: u32 = u32::MAX; +struct Parameter { + block: BlockId, + index: usize, + value: ValueId, +} +struct Use { + user: u32, + next: u32, +} + +/// Only parameters can change identity here. Compact linked lists let us move +/// their dependants to the replacement in O(1), without a Vec per SSA value. +pub(super) fn remove_trivial_parameters(body: &mut Body, predecessors: &[Vec<(BlockId, EdgeId)>]) { + let params: Vec<_> = body + .blocks + .iter() + .enumerate() + .filter(|(b, _)| *b != body.entry.index()) + .flat_map(|(b, block)| { + block + .params + .iter() + .enumerate() + .map(move |(index, &value)| Parameter { + block: BlockId::new(b), + index, + value, + }) + }) + .collect(); + if params.is_empty() { + return; + } + let mut numbers = vec![NONE; body.values.len()]; + for (index, param) in params.iter().enumerate() { + numbers[param.value.index()] = index as u32; + } + let mut heads = vec![NONE; params.len()]; + let mut tails = heads.clone(); + let mut uses: Vec = Vec::new(); + for (index, param) in params.iter().enumerate() { + for &(_, edge) in &predecessors[param.block.index()] { + let arg = body.edges[edge.index()].args[param.index]; + let source = numbers[arg.index()]; + if source == NONE || source == index as u32 { + continue; + } + let node = u32::try_from(uses.len()).expect("too many parameter uses"); + assert_ne!(node, NONE); + uses.push(Use { + user: index as u32, + next: NONE, + }); + if heads[source as usize] == NONE { + heads[source as usize] = node; + } else { + uses[tails[source as usize] as usize].next = node; + } + tails[source as usize] = node; + } + } + let mut queue: VecDeque<_> = (0..params.len() as u32).collect(); + let mut queued = vec![true; params.len()]; + while let Some(number) = queue.pop_front() { + let number = number as usize; + queued[number] = false; + let param = ¶ms[number]; + if !matches!(body.values[param.value.index()].def, ValueDef::Param(_)) { + continue; + } + let mut unique = None; + let mut trivial = true; + for &(_, edge) in &predecessors[param.block.index()] { + let arg = body.resolve_mut(body.edges[edge.index()].args[param.index]); + if arg == param.value { + continue; + } + if unique.is_some_and(|previous| previous != arg) { + trivial = false; + break; + } + unique = Some(arg); + } + if !trivial { + continue; + } + let Some(replacement) = unique else { + continue; + }; + body.values[param.value.index()].def = ValueDef::Alias(replacement); + let head = std::mem::replace(&mut heads[number], NONE); + let tail = std::mem::replace(&mut tails[number], NONE); + let mut next = head; + while next != NONE { + let node = &uses[next as usize]; + if !std::mem::replace(&mut queued[node.user as usize], true) { + queue.push_back(node.user); + } + next = node.next; + } + let target = numbers[replacement.index()]; + if head != NONE && target != NONE { + let target = target as usize; + if heads[target] == NONE { + heads[target] = head; + } else { + uses[tails[target] as usize].next = head; + } + tails[target] = tail; + } + } + for index in 0..body.values.len() { + body.resolve_mut(ValueId::new(index)); + } + // Reuse one mask across blocks and preserve each edge's parameter order. + let mut keep = Vec::new(); + for (b, incoming) in predecessors.iter().enumerate() { + keep.clear(); + keep.extend( + body.blocks[b] + .params + .iter() + .map(|p| matches!(body.values[p.index()].def, ValueDef::Param(_))), + ); + for &(_, edge) in incoming { + let mut index = 0; + body.edges[edge.index()].args.retain(|_| { + let yes = keep[index]; + index += 1; + yes + }); + } + let mut index = 0; + body.blocks[b].params.retain(|_| { + let yes = keep[index]; + index += 1; + yes + }); + } +} diff --git a/compiler-core/src/ir/remap.rs b/compiler-core/src/ir/remap.rs new file mode 100644 index 0000000..2bcd6b2 --- /dev/null +++ b/compiler-core/src/ir/remap.rs @@ -0,0 +1,118 @@ +//! Relocate a body region without exposing operand layout to transformations. +use super::*; + +pub trait Remap { + fn value(&mut self, value: ValueId) -> ValueId; + fn args(&mut self, args: List) -> List; + fn constant(&mut self, constant: ConstId) -> ConstId; + fn method(&mut self, method: MethodId) -> MethodId; + fn field(&mut self, field: MemberId) -> MemberId; + fn projection(&mut self, projection: ProjectionId) -> ProjectionId; + fn slot(&mut self, slot: SlotId) -> SlotId; +} +impl Op { + pub fn remap(self, map: &mut impl Remap) -> Self { + use Op::*; + match self { + Constant(c) => Constant(map.constant(c)), + Exception => Exception, + Binary { op, left, right } => Binary { + op, + left: map.value(left), + right: map.value(right), + }, + Overflow { op, args } => Overflow { + op, + args: map.args(args), + }, + Not(v) => Not(map.value(v)), + Neg(v) => Neg(map.value(v)), + Bit { op, value } => Bit { + op, + value: map.value(value), + }, + Opaque(v) => Opaque(map.value(v)), + Cast(v) => Cast(map.value(v)), + Adapt(v) => Adapt(map.value(v)), + Reinterpret(v) => Reinterpret(map.value(v)), + NewArray(v) => NewArray(map.value(v)), + ArrayLength(v) => ArrayLength(map.value(v)), + Length(v) => Length(map.value(v)), + Load(v) => Load(map.value(v)), + FunctionPointer { signature, target } => FunctionPointer { + signature: map.method(signature), + target: map.method(target), + }, + LoadSlot(slot) => LoadSlot(map.slot(slot)), + AddressOfSlot(slot) => AddressOfSlot(map.slot(slot)), + StoreSlot { slot, value } => StoreSlot { + slot: map.slot(slot), + value: map.value(value), + }, + Store { pointer, value } => Store { + pointer: map.value(pointer), + value: map.value(value), + }, + Project { base, projection } => Project { + base: map.value(base), + projection: map.projection(projection), + }, + Offset { + pointer, + offset, + bytes, + wrapping, + } => Offset { + pointer: map.value(pointer), + offset: map.value(offset), + bytes, + wrapping, + }, + Call { method, kind, args } => Call { + method: map.method(method), + kind, + args: map.args(args), + }, + GetField { object, field } => GetField { + object: map.value(object), + field: map.field(field), + }, + SetField { + object, + field, + value, + } => SetField { + object: map.value(object), + field: map.field(field), + value: map.value(value), + }, + GetStatic(field) => GetStatic(map.field(field)), + SetStatic { field, value } => SetStatic { + field: map.field(field), + value: map.value(value), + }, + ArrayGet { array, index } => ArrayGet { + array: map.value(array), + index: map.value(index), + }, + ArraySet { + array, + index, + value, + } => ArraySet { + array: map.value(array), + index: map.value(index), + value: map.value(value), + }, + View { data, length } => View { + data: map.value(data), + length: map.value(length), + }, + ViewData { view, size, codec } => ViewData { + view: map.value(view), + size, + codec, + }, + } + } +} diff --git a/compiler-core/src/ir/storage_tests.rs b/compiler-core/src/ir/storage_tests.rs new file mode 100644 index 0000000..162fdcb --- /dev/null +++ b/compiler-core/src/ir/storage_tests.rs @@ -0,0 +1,75 @@ +use super::*; +use crate::scalar::ScalarType; + +#[test] +fn storage_validates_layout_codec_and_load_store_types() { + let mut types = Types::default(); + let i32 = types.scalar(ScalarType::I32); + let pointer = types.intern(Type::Pointer(i32)); + let address = types.intern(Type::Pointer(pointer)); + let codec = types.symbol("pointer:i32"); + let mut b = Builder::new(&types, pointer); + let value = b.parameter(b.current(), pointer); + let slot = SlotId::new(0); + b.body.slots.push(StorageSlot { + ty: pointer, + size: 8, + alignment: 8, + codec: Some(codec), + }); + b.emit(Op::StoreSlot { slot, value }, None); + let address = b.emit(Op::AddressOfSlot(slot), Some(address)).unwrap(); + let loaded = b.emit(Op::Load(address), Some(pointer)).unwrap(); + b.emit( + Op::StoreSlot { + slot, + value: loaded, + }, + None, + ); + let loaded = b.emit(Op::LoadSlot(slot), Some(pointer)).unwrap(); + b.terminate(Terminator::Return(Some(loaded))); + let body = b.finish().unwrap(); + verify(&body, &types).unwrap(); + for (size, alignment) in [(8, 0), (8, 3), (u32::MAX, 8), (8, 1 << 31)] { + let mut invalid = body.clone(); + invalid.slots[0].size = size; + invalid.slots[0].alignment = alignment; + assert!(verify(&invalid, &types).is_err()); + } + let mut invalid = body.clone(); + invalid.slots[0].codec = Some(SymbolId::new(123)); + assert!(verify(&invalid, &types).is_err()); + let mut invalid = body.clone(); + invalid.slots[0] = StorageSlot::scalar(i32, &types).unwrap(); + assert!(verify(&invalid, &types).is_err()); + let mut invalid = body; + invalid.slots[0].ty = TypeId::new(123); + assert!(verify(&invalid, &types).is_err()); +} + +#[test] +fn symbol_only_extensions_are_retained() { + let base = std::sync::Arc::new(Types::default()); + let mut extended = Types::with_base(base); + assert!(!extended.has_additions()); + let codec = extended.symbol("exact-layout-codec"); + assert!(extended.has_additions()); + assert!(extended.is_empty()); + assert_eq!(extended.symbol_name(codec), Some("exact-layout-codec")); +} + +#[test] +fn opaque_pointees_pass_through_without_becoming_values() { + let mut types = Types::default(); + let opaque = types.intern(Type::Opaque(0)); + let pointer = types.intern(Type::Pointer(opaque)); + let mut b = Builder::new(&types, pointer); + let value = b.parameter(b.current(), pointer); + b.terminate(Terminator::Return(Some(value))); + let mut body = b.finish().unwrap(); + crate::jvm::select::compile(&body, &types, &mut Default::default()).unwrap(); + body.values[value.index()].ty = opaque; + body.return_type = opaque; + assert!(verify(&body, &types).is_err()); +} diff --git a/compiler-core/src/ir/tests.rs b/compiler-core/src/ir/tests.rs new file mode 100644 index 0000000..ed2b53f --- /dev/null +++ b/compiler-core/src/ir/tests.rs @@ -0,0 +1,327 @@ +use super::*; +use crate::scalar::{BinaryOp, Scalar, ScalarType}; + +#[test] +fn construction_prunes_constant_control_flow_without_reading_dead_bindings() { + let mut types = Types::default(); + let int = types.scalar(ScalarType::I32); + let boolean = types.scalar(ScalarType::Bool); + let mut b = Builder::new(&types, int); + let dead = b.create_block(); + let live = b.create_block(); + let condition = b.constant(boolean, Scalar::boolean(false)); + b.branch(condition, dead, live); + b.switch_to(dead); + let missing = b.variable(int); + let value = b.read(missing); + b.terminate(Terminator::Return(Some(value))); + b.switch_to(live); + let value = integer(&mut b, int, 17); + let result = b.create_block(); + b.switch( + value, + [(Scalar::integer(ScalarType::I32, 17).unwrap(), result)], + dead, + ); + b.switch_to(result); + b.terminate(Terminator::Return(Some(value))); + let body = b.finish().unwrap(); + assert!(!body.reachable()[dead.index()]); + assert_eq!(execute(&body, &[]).bits(), 17); +} + +fn integer(builder: &mut Builder<'_>, ty: TypeId, n: i32) -> ValueId { + builder.constant(ty, Scalar::integer(ScalarType::I32, n as u128).unwrap()) +} + +#[test] +fn merges_mutable_source_bindings_with_block_parameters() { + let mut types = Types::default(); + let i32 = types.scalar(ScalarType::I32); + let boolean = types.scalar(ScalarType::Bool); + let mut b = Builder::new(&types, i32); + let condition = b.parameter(b.current(), boolean); + let var = b.variable(i32); + let yes = b.create_block(); + let no = b.create_block(); + let join = b.create_block(); + b.branch(condition, yes, no); + b.switch_to(yes); + let one = integer(&mut b, i32, 1); + b.define(var, one); + b.jump(join, vec![]); + b.switch_to(no); + let two = integer(&mut b, i32, 2); + b.define(var, two); + b.jump(join, vec![]); + b.switch_to(join); + let value = b.read(var); + b.terminate(Terminator::Return(Some(value))); + let body = b.finish().unwrap(); + assert_eq!(body.blocks[join.index()].params, vec![value]); + assert_eq!(execute(&body, &[Scalar::boolean(true)]).bits(), 1); + assert_eq!(execute(&body, &[Scalar::boolean(false)]).bits(), 2); +} + +#[test] +fn loop_backedges_keep_the_induction_value_in_ssa() { + let mut types = Types::default(); + let i32 = types.scalar(ScalarType::I32); + let boolean = types.scalar(ScalarType::Bool); + let mut b = Builder::new(&types, i32); + let count = b.parameter(b.current(), i32); + let var = b.variable(i32); + let zero = integer(&mut b, i32, 0); + b.define(var, zero); + let one = integer(&mut b, i32, 1); + let header = b.create_block(); + let update = b.create_block(); + let exit = b.create_block(); + b.jump(header, vec![]); + b.switch_to(header); + let n = b.read(var); + let cond = b + .emit( + Op::Binary { + op: BinaryOp::Lt, + left: n, + right: count, + }, + Some(boolean), + ) + .unwrap(); + b.branch(cond, update, exit); + b.switch_to(update); + let previous = b.read(var); + let next = b + .emit( + Op::Binary { + op: BinaryOp::Add, + left: previous, + right: one, + }, + Some(i32), + ) + .unwrap(); + b.define(var, next); + b.jump(header, vec![]); + b.switch_to(exit); + let value = b.read(var); + b.terminate(Terminator::Return(Some(value))); + let body = b.finish().unwrap(); + assert_eq!(body.blocks[header.index()].params.len(), 1); + assert!(body.blocks[update.index()].params.is_empty()); + assert!(body.blocks[exit.index()].params.is_empty()); + for n in [0, 1, 20] { + assert_eq!( + execute(&body, &[Scalar::integer(ScalarType::I32, n).unwrap()]).bits(), + n + ); + } +} + +fn throwing_body() -> (Types, Body, ValueId, BlockId) { + let mut types = Types::default(); + let i32 = types.scalar(ScalarType::I32); + let mut b = Builder::new(&types, i32); + let divisor = b.parameter(b.current(), i32); + let source = b.variable(i32); + let seven = integer(&mut b, i32, 7); + b.define(source, seven); + let handler = b.create_block(); + let result = b + .invoke( + Op::Binary { + op: BinaryOp::Div, + left: seven, + right: divisor, + }, + Some(i32), + handler, + ) + .unwrap(); + b.define(source, result); + b.terminate(Terminator::Return(Some(result))); + b.switch_to(handler); + let old = b.read(source); + b.terminate(Terminator::Return(Some(old))); + let body = b.finish().unwrap(); + (types, body, result, handler) +} + +#[test] +fn exceptional_edge_keeps_old_binding_and_normal_edge_gets_result() { + let (_, body, _, _) = throwing_body(); + assert_eq!( + execute(&body, &[Scalar::integer(ScalarType::I32, 0).unwrap()]).bits(), + 7 + ); + assert_eq!( + execute(&body, &[Scalar::integer(ScalarType::I32, 7).unwrap()]).bits(), + 1 + ); +} + +#[test] +fn verifier_rejects_a_call_result_used_on_the_exception_path() { + let (types, mut body, result, handler) = throwing_body(); + body.blocks[handler.index()].terminator = Some(Terminator::Return(Some(result))); + assert!(verify(&body, &types).unwrap_err().0.contains("dominate")); +} + +#[test] +fn undefined_reads_and_alias_cycles_are_errors() { + let mut types = Types::default(); + let i32 = types.scalar(ScalarType::I32); + let mut b = Builder::new(&types, i32); + let variable = b.variable(i32); + let value = b.read(variable); + b.terminate(Terminator::Return(Some(value))); + assert!(b.finish().unwrap_err().0.contains("undefined")); + let mut body = Body::new(i32); + body.values.push(Value { + ty: i32, + def: ValueDef::Alias(ValueId::new(0)), + }); + assert!(verify(&body, &types).unwrap_err().0.contains("cycle")); +} + +#[test] +fn deep_graph_construction_and_verification_do_not_recurse_on_the_host_stack() { + let mut types = Types::default(); + let i32 = types.scalar(ScalarType::I32); + let mut b = Builder::new(&types, i32); + let variable = b.variable(i32); + let one = integer(&mut b, i32, 1); + b.define(variable, one); + for _ in 0..10_000 { + let next = b.create_block(); + b.jump(next, vec![]); + b.switch_to(next); + } + let value = b.read(variable); + b.terminate(Terminator::Return(Some(value))); + let body = b.finish().unwrap(); + assert_eq!(body.resolve(value), one); + assert!(body.blocks.iter().all(|block| block.params.is_empty())); +} + +#[test] +fn identifiers_and_instruction_records_have_bounded_inline_sizes() { + assert_eq!(std::mem::size_of::(), 4); + assert_eq!(std::mem::size_of::>(), 4); + assert_eq!(std::mem::size_of::(), 24); + assert!(std::mem::size_of::() <= 32); + assert!(std::mem::size_of::() <= 16); +} + +/// Small semantic oracle for IR tests. JVM execution separately checks emission. +fn execute(body: &Body, arguments: &[Scalar]) -> Scalar { + let mut values = vec![None; body.values.len()]; + let get = |values: &[Option], v| values[body.resolve(v).index()].unwrap(); + for (¶m, &value) in body.blocks[body.entry.index()].params.iter().zip(arguments) { + values[param.index()] = Some(value); + } + let evaluate = + |inst: InstId, values: &[Option]| match body.instructions[inst.index()].op { + Op::Constant(id) => match body.constants[id.index()] { + Constant::Scalar(v) => Some(v), + _ => panic!("unsupported constant"), + }, + Op::Binary { op, left, right } => get(values, left).binary(op, get(values, right)), + _ => panic!("unsupported test instruction"), + }; + let mut block = body.entry; + for _ in 0..100_000 { + for &inst in &body.blocks[block.index()].instructions { + let value = evaluate(inst, &values).expect("unexpected trap"); + values[body.instructions[inst.index()].result.unwrap().index()] = Some(value); + } + let edge = match body.blocks[block.index()].terminator.unwrap() { + Terminator::Return(Some(v)) => return get(&values, v), + Terminator::Jump(e) => e, + Terminator::Branch { condition, yes, no } => { + if get(&values, condition).bits() != 0 { + yes + } else { + no + } + } + Terminator::Invoke { + inst, + normal, + unwind, + } => match evaluate(inst, &values) { + Some(value) => { + values[body.instructions[inst.index()].result.unwrap().index()] = Some(value); + normal + } + None => unwind, + }, + _ => panic!("unsupported test terminator"), + }; + let edge = &body.edges[edge.index()]; + let args = edge + .args + .iter() + .map(|&v| get(&values, v)) + .collect::>(); + block = edge.target; + for (¶m, value) in body.blocks[block.index()].params.iter().zip(args) { + values[param.index()] = Some(value); + } + } + panic!("test instruction budget exhausted") +} + +#[test] +fn source_local_initial_state_is_explicit_and_temporaries_remain_strict() { + let mut types = Types::default(); + let int = types.scalar(crate::scalar::ScalarType::I32); + let flag = types.scalar(crate::scalar::ScalarType::Bool); + for source_local in [false, true] { + let mut b = Builder::new(&types, int); + let condition = b.parameter(b.current(), flag); + let local = if source_local { + b.local(int) + } else { + b.variable(int) + }; + let assigned = b.create_block(); + let joined = b.create_block(); + let observed = b.create_block(); + let empty = b.create_block(); + b.branch(condition, assigned, joined); + b.switch_to(assigned); + let value = b.constant( + int, + crate::scalar::Scalar::integer(crate::scalar::ScalarType::I32, 7).unwrap(), + ); + b.define(local, value); + b.jump(joined, vec![]); + b.switch_to(joined); + b.branch(condition, observed, empty); + b.switch_to(observed); + let value = b.read(local); + b.terminate(Terminator::Return(Some(value))); + b.switch_to(empty); + let zero = b.constant( + int, + crate::scalar::Scalar::integer(crate::scalar::ScalarType::I32, 0).unwrap(), + ); + b.terminate(Terminator::Return(Some(zero))); + let result = b.finish(); + if source_local { + let body = result.unwrap(); + assert_eq!( + body.constants + .iter() + .filter(|c| matches!(c, Constant::Uninit(_))) + .count(), + 1 + ); + } else { + assert!(result.unwrap_err().0.contains("undefined variable")); + } + } +} diff --git a/compiler-core/src/ir/types.rs b/compiler-core/src/ir/types.rs new file mode 100644 index 0000000..fcf5659 --- /dev/null +++ b/compiler-core/src/ir/types.rs @@ -0,0 +1,149 @@ +use super::{SymbolId, TypeId}; +use crate::scalar::ScalarType; +use rustc_hash::FxHashMap; +use std::sync::Arc; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Type { + Unit, + /// Body-local identity for an uninspected pointee. Never a JVM value. + Opaque(u32), + Scalar(ScalarType), + Class(SymbolId), + Interface(SymbolId), + Pointer(TypeId), + Array(TypeId), + Slice(TypeId), + Str, +} + +impl Type { + /// JVM storage category. Reference identities remain distinct IR types. + pub fn carrier(self) -> u8 { + use ScalarType::*; + match self { + Self::Unit | Self::Opaque(_) => 0, + Self::Scalar(Bool | I8 | U8 | I16 | U16 | I32 | U32 | Char | F16) => 1, + Self::Scalar(I64 | U64) => 2, + Self::Scalar(F32) => 3, + Self::Scalar(F64) => 4, + _ => 5, + } + } +} + +#[derive(Default, Debug, Clone)] +pub struct Types { + base: Option>, + values: Vec, + ids: FxHashMap, + symbols: Vec>, + symbol_ids: FxHashMap, SymbolId>, +} + +impl Types { + /// Extend an immutable common vocabulary without copying its tables. Only + /// one shared layer is allowed, so lookup cost cannot grow across bodies. + pub fn with_base(base: Arc) -> Self { + assert!( + base.base.is_none(), + "type vocabularies have one shared layer" + ); + Self { + base: Some(base), + ..Default::default() + } + } + pub fn len(&self) -> usize { + self.base_types() + self.values.len() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Whether an extension contains no additional types or symbols. + pub fn has_additions(&self) -> bool { + !self.values.is_empty() || !self.symbols.is_empty() + } + fn base_types(&self) -> usize { + self.base.as_ref().map_or(0, |b| b.values.len()) + } + fn base_symbols(&self) -> usize { + self.base.as_ref().map_or(0, |b| b.symbols.len()) + } + pub fn find(&self, ty: Type) -> Option { + self.ids + .get(&ty) + .or_else(|| self.base.as_ref().and_then(|b| b.ids.get(&ty))) + .copied() + } + pub fn intern(&mut self, ty: Type) -> TypeId { + if let Some(id) = self.find(ty) { + return id; + } + let id = TypeId::new(self.len()); + self.values.push(ty); + self.ids.insert(ty, id); + id + } + pub fn scalar(&mut self, ty: ScalarType) -> TypeId { + self.intern(Type::Scalar(ty)) + } + pub fn get(&self, ty: TypeId) -> Option { + let base = self.base_types(); + if ty.index() < base { + self.base.as_ref()?.values.get(ty.index()).copied() + } else { + self.values.get(ty.index() - base).copied() + } + } + pub fn symbol(&mut self, name: &str) -> SymbolId { + if let Some(&id) = self + .symbol_ids + .get(name) + .or_else(|| self.base.as_ref().and_then(|b| b.symbol_ids.get(name))) + { + return id; + } + let id = SymbolId::new(self.base_symbols() + self.symbols.len()); + let name: Arc = name.into(); + self.symbols.push(Arc::clone(&name)); + self.symbol_ids.insert(name, id); + id + } + pub fn symbol_name(&self, symbol: SymbolId) -> Option<&str> { + let base = self.base_symbols(); + if symbol.index() < base { + self.base + .as_ref()? + .symbols + .get(symbol.index()) + .map(AsRef::as_ref) + } else { + self.symbols.get(symbol.index() - base).map(AsRef::as_ref) + } + } +} + +impl PartialEq for Types { + fn eq(&self, other: &Self) -> bool { + self.len() == other.len() + && (0..self.len()).all(|i| self.get(TypeId::new(i)) == other.get(TypeId::new(i))) + && self.base_symbols() + self.symbols.len() + == other.base_symbols() + other.symbols.len() + && (0..self.base_symbols() + self.symbols.len()) + .all(|i| self.symbol_name(SymbolId::new(i)) == other.symbol_name(SymbolId::new(i))) + } +} +impl Eq for Types {} +impl std::hash::Hash for Types { + fn hash(&self, state: &mut H) { + self.len().hash(state); + for i in 0..self.len() { + self.get(TypeId::new(i)).unwrap().hash(state); + } + (self.base_symbols() + self.symbols.len()).hash(state); + for i in 0..self.base_symbols() + self.symbols.len() { + self.symbol_name(SymbolId::new(i)).unwrap().hash(state); + } + } +} diff --git a/compiler-core/src/ir/verify/dominance.rs b/compiler-core/src/ir/verify/dominance.rs new file mode 100644 index 0000000..c6e6dcb --- /dev/null +++ b/compiler-core/src/ir/verify/dominance.rs @@ -0,0 +1,86 @@ +//! Immediate dominators and preorder intervals without recursive graph walks. +use super::*; + +/// Iterative immediate dominators and DFS intervals use O(blocks + edges) memory. +pub(super) fn dominance( + body: &Body, + predecessors: &[Vec], + reachable: &[bool], +) -> (Vec, Vec) { + let mut seen = vec![false; body.blocks.len()]; + let mut stack = vec![(body.entry, false)]; + let mut order = Vec::new(); + while let Some((block, exiting)) = stack.pop() { + if exiting { + order.push(block); + continue; + } + if std::mem::replace(&mut seen[block.index()], true) { + continue; + } + stack.push((block, true)); + body.blocks[block.index()] + .terminator + .unwrap() + .visit_edges(&body.cases, |edge| { + stack.push((body.edges[edge.index()].target, false)) + }); + } + order.reverse(); + let mut ranks = vec![0; body.blocks.len()]; + for (rank, block) in order.iter().enumerate() { + ranks[block.index()] = rank; + } + let mut parent = vec![None; body.blocks.len()]; + parent[body.entry.index()] = Some(body.entry); + loop { + let mut changed = false; + for &block in order.iter().skip(1) { + let mut incoming = predecessors[block.index()] + .iter() + .copied() + .filter(|p| reachable[p.index()] && parent[p.index()].is_some()); + let Some(mut common) = incoming.next() else { + continue; + }; + for mut other in incoming { + while common != other { + while ranks[common.index()] > ranks[other.index()] { + common = parent[common.index()].unwrap(); + } + while ranks[other.index()] > ranks[common.index()] { + other = parent[other.index()].unwrap(); + } + } + } + if parent[block.index()] != Some(common) { + parent[block.index()] = Some(common); + changed = true; + } + } + if !changed { + break; + } + } + let mut children = vec![Vec::new(); body.blocks.len()]; + for &block in order.iter().skip(1) { + children[parent[block.index()].unwrap().index()].push(block); + } + let mut pre = vec![0; body.blocks.len()]; + let mut post = pre.clone(); + let mut tick = 0; + stack.push((body.entry, false)); + while let Some((block, exiting)) = stack.pop() { + if exiting { + post[block.index()] = tick; + continue; + } + pre[block.index()] = tick; + tick += 1; + stack.push((block, true)); + for &child in &children[block.index()] { + stack.push((child, false)); + } + } + (pre, post) +} diff --git a/compiler-core/src/ir/verify/mod.rs b/compiler-core/src/ir/verify/mod.rs new file mode 100644 index 0000000..4ed6ef8 --- /dev/null +++ b/compiler-core/src/ir/verify/mod.rs @@ -0,0 +1,429 @@ +//! Structural and dominance checks for SSA bodies, including throw-point values. +use super::*; +use crate::scalar::{BinaryOp, ScalarType}; +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VerifyError(pub String); +impl fmt::Display for VerifyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} +impl std::error::Error for VerifyError {} + +macro_rules! check { + ($condition:expr, $($message:tt)*) => { + if !$condition { return Err(VerifyError(format!($($message)*))); } + } +} + +mod dominance; +mod types; +use dominance::dominance; +use types::verify_types; + +pub fn verify(body: &Body, types: &Types) -> Result<(), VerifyError> { + verify_with_debug(body, types, None) +} + +pub fn verify_with_debug( + body: &Body, + types: &Types, + debug: Option<&DebugInfo>, +) -> Result<(), VerifyError> { + check!(body.entry.index() < body.blocks.len(), "invalid entry"); + check!( + types + .get(body.return_type) + .is_some_and(|t| !matches!(t, Type::Opaque(_))), + "invalid return type" + ); + for value in &body.values { + check!( + types + .get(value.ty) + .is_some_and(|t| !matches!(t, Type::Opaque(_))), + "invalid value type" + ); + } + for slot in &body.slots { + check!( + slot.size <= i32::MAX as u32 + && slot.alignment <= i32::MAX as u32 + && slot.alignment.is_power_of_two(), + "invalid storage allocation layout" + ); + check!( + slot.codec + .is_none_or(|codec| types.symbol_name(codec).is_some()), + "invalid storage codec" + ); + match types.get(slot.ty) { + Some(Type::Scalar(_)) => check!( + StorageSlot::scalar(slot.ty, types).as_ref() == Some(slot), + "invalid scalar storage layout" + ), + Some(Type::Class(symbol) | Type::Interface(symbol)) => { + check!(types.symbol_name(symbol).is_some(), "invalid storage class") + } + Some(Type::Pointer(_) | Type::Slice(_) | Type::Str) => {} + _ => return Err(VerifyError("unsupported storage type".into())), + } + } + for field in &body.fields { + check!( + !field.relative_pointer + || (!field.is_static && matches!(types.get(field.ty), Some(Type::Pointer(_)))), + "invalid relative pointer field" + ); + check!( + matches!(types.get(field.owner), Some(Type::Class(symbol) | Type::Interface(symbol)) if types.symbol_name(symbol).is_some()), + "invalid field owner" + ); + check!( + types + .get(field.ty) + .is_some_and(|t| !matches!(t, Type::Unit | Type::Opaque(_))), + "invalid field type" + ); + } + let roots = resolve_aliases(body)?; + let mut owners = vec![None; body.instructions.len()]; + let mut edge_owners = vec![None; body.edges.len()]; + let mut predecessors = vec![Vec::new(); body.blocks.len()]; + let mut parameters = vec![false; body.values.len()]; + // Validate every target's parameter table before following any edge. + for (index, block) in body.blocks.iter().enumerate() { + for ¶m in &block.params { + check!( + body.values + .get(param.index()) + .is_some_and(|v| v.def == ValueDef::Param(BlockId::new(index))), + "invalid parameter {param:?}" + ); + check!( + !std::mem::replace(&mut parameters[param.index()], true), + "duplicate parameter {param:?}" + ); + } + } + for (index, block) in body.blocks.iter().enumerate() { + let b = BlockId::new(index); + let term = block + .terminator + .ok_or_else(|| VerifyError(format!("unterminated {b:?}")))?; + if let Terminator::Switch { cases, .. } = term { + check!( + cases.range().end <= body.cases.len(), + "invalid switch cases" + ); + } + let mut edges = Vec::new(); + term.visit_edges(&body.cases, |edge| edges.push(edge)); + for edge in edges { + let e = body + .edges + .get(edge.index()) + .ok_or_else(|| VerifyError("invalid edge".into()))?; + check!( + edge_owners[edge.index()].replace(b).is_none(), + "edge shared between branch sites" + ); + let target = body + .blocks + .get(e.target.index()) + .ok_or_else(|| VerifyError("invalid target".into()))?; + check!( + e.args.len() == target.params.len(), + "edge parameter count mismatch" + ); + for (&arg, ¶m) in e.args.iter().zip(&target.params) { + check!( + body.values + .get(arg.index()) + .is_some_and(|v| v.ty == body.values[param.index()].ty), + "edge argument type mismatch" + ); + } + predecessors[e.target.index()].push(b); + } + for (position, &inst) in block.instructions.iter().enumerate() { + check!(inst.index() < owners.len(), "invalid instruction"); + check!( + owners[inst.index()].replace((b, position + 1)).is_none(), + "instruction appears twice" + ); + } + if let Terminator::Invoke { inst, normal, .. } = term { + check!(inst.index() < owners.len(), "invalid invoke instruction"); + check!( + owners[inst.index()] + .replace((body.edges[normal.index()].target, 0)) + .is_none(), + "invoke instruction appears twice" + ); + } + } + for (b, block) in body.blocks.iter().enumerate() { + if let Some(Terminator::Invoke { normal, .. }) = block.terminator { + let target = body.edges[normal.index()].target; + check!( + target.index() != b + && target != body.entry + && predecessors[target.index()].len() == 1, + "invoke needs a private normal continuation" + ); + } + } + for (index, inst) in body.instructions.iter().enumerate() { + check!(owners[index].is_some(), "orphan instruction"); + if let Some(result) = inst.result { + check!( + body.values + .get(result.index()) + .is_some_and(|v| v.def == ValueDef::Inst(InstId::new(index))), + "invalid instruction result" + ); + } + match inst.op { + Op::Call { method, args, .. } => { + check!(method.index() < body.methods.len(), "invalid call target"); + check!(args.range().end <= body.args.len(), "invalid operand list"); + } + Op::Overflow { args, .. } => { + check!(args.range().end <= body.args.len(), "invalid operand list") + } + Op::Constant(id) => check!(id.index() < body.constants.len(), "invalid constant"), + Op::LoadSlot(id) | Op::AddressOfSlot(id) | Op::StoreSlot { slot: id, .. } => { + check!(id.index() < body.slots.len(), "invalid storage slot") + } + _ => {} + } + let mut bad = false; + inst.op.visit_uses(&body.args, |value| { + bad |= value.index() >= body.values.len() + }); + check!(!bad, "invalid instruction operand"); + verify_types(inst, body, types)?; + } + for (index, value) in body.values.iter().enumerate() { + match value.def { + ValueDef::Inst(inst) => check!( + body.instructions + .get(inst.index()) + .is_some_and(|i| i.result == Some(ValueId::new(index))), + "unowned result value" + ), + ValueDef::Param(_) => check!(parameters[index], "unowned parameter value"), + ValueDef::Alias(_) | ValueDef::Unreachable => {} + } + } + + let reachable = body.reachable(); + let (pre, post) = dominance(body, &predecessors, &reachable); + let use_value = |value: ValueId, + at: BlockId, + position: usize, + invoke_result: Option| + -> Result<(), VerifyError> { + check!(value.index() < roots.len(), "invalid value use"); + let root = roots[value.index()]; + if !reachable[at.index()] { + return Ok(()); + } + let (defined, order) = match body.values[root.index()].def { + ValueDef::Inst(inst) if Some(inst) == invoke_result => return Ok(()), + ValueDef::Inst(inst) => owners[inst.index()].unwrap(), + ValueDef::Param(block) => (block, 0), + ValueDef::Unreachable => { + return Err(VerifyError( + "unreachable value used by reachable code".into(), + )); + } + ValueDef::Alias(_) => unreachable!(), + }; + check!( + reachable[defined.index()] + && pre[defined.index()] <= pre[at.index()] + && pre[at.index()] < post[defined.index()], + "{root:?} does not dominate its use in {at:?}" + ); + check!( + defined != at || order < position, + "value used before definition" + ); + Ok(()) + }; + if let Some(debug) = debug { + for local in &debug.locals { + match *local { + DebugLocal::Value(ty) => check!( + types.get(ty).is_some_and(|t| !matches!(t, Type::Opaque(_))), + "invalid debug local type" + ), + DebugLocal::Storage(slot) => { + check!(slot.index() < body.slots.len(), "invalid debug cell") + } + } + } + for variable in &debug.variables { + check!( + (variable.local as usize) < debug.locals.len(), + "invalid debug variable binding" + ); + } + for scope in &debug.scopes { + check!( + scope.iter().all(|&v| (v as usize) < debug.variables.len()), + "invalid debug scope" + ); + } + for event in &debug.events { + check!( + event.block.index() < body.blocks.len(), + "invalid debug block" + ); + check!( + event.position as usize <= body.blocks[event.block.index()].instructions.len(), + "invalid debug position" + ); + match event.change { + DebugChange::Set { local, value } => { + use_value(value, event.block, event.position as usize + 1, None)?; + check!( + debug.locals.get(local as usize) + == Some(&DebugLocal::Value(body.value_type(value))), + "debug binding type mismatch" + ); + } + DebugChange::Clear(local) => check!( + (local as usize) < debug.locals.len(), + "invalid cleared debug binding" + ), + DebugChange::Scope(scope) => check!( + (scope as usize) < debug.scopes.len(), + "invalid debug scope event" + ), + } + } + } + for (index, block) in body.blocks.iter().enumerate() { + let b = BlockId::new(index); + let term = block.terminator.unwrap(); + let end = block.instructions.len() + 1; + for (position, &inst) in block.instructions.iter().enumerate() { + let mut result = Ok(()); + body.instructions[inst.index()] + .op + .visit_uses(&body.args, |v| { + if result.is_ok() { + result = use_value(v, b, position + 1, None); + } + }); + result?; + } + if let Terminator::Invoke { inst, .. } = term { + let mut result = Ok(()); + body.instructions[inst.index()] + .op + .visit_uses(&body.args, |v| { + if result.is_ok() { + result = use_value(v, b, end, None); + } + }); + result?; + } + let mut result = Ok(()); + term.visit_edges(&body.cases, |edge| { + let allow = match term { + Terminator::Invoke { inst, normal, .. } if edge == normal => Some(inst), + _ => None, + }; + for &arg in &body.edges[edge.index()].args { + if result.is_ok() { + result = use_value(arg, b, end, allow); + } + } + }); + result?; + match term { + Terminator::Branch { condition, .. } => { + use_value(condition, b, end, None)?; + check!( + types.get(body.value_type(condition)) == Some(Type::Scalar(ScalarType::Bool)), + "branch needs boolean" + ); + } + Terminator::Switch { value, cases, .. } => { + use_value(value, b, end, None)?; + let Some(Type::Scalar(scalar)) = types.get(body.value_type(value)) else { + return Err(VerifyError("switch requires scalar".into())); + }; + check!( + scalar.integer().is_some() || scalar == ScalarType::Bool, + "switch requires integer or boolean" + ); + let mut keys = rustc_hash::FxHashSet::default(); + for &(key, _) in &body.cases[cases.range()] { + check!(key.ty() == scalar, "switch key type mismatch"); + check!(keys.insert(key.bits()), "duplicate switch key"); + } + } + Terminator::Throw { value, .. } => { + use_value(value, b, end, None)?; + } + Terminator::Return(Some(value)) => { + use_value(value, b, end, None)?; + check!( + body.value_type(value) == body.return_type, + "return type mismatch" + ); + } + Terminator::Return(None) => check!( + types.get(body.return_type) == Some(Type::Unit), + "missing return value" + ), + _ => {} + } + } + Ok(()) +} + +fn resolve_aliases(body: &Body) -> Result, VerifyError> { + let mut roots = vec![ValueId::new(0); body.values.len()]; + let mut state = vec![0; body.values.len()]; + let mut path = Vec::new(); + for index in 0..body.values.len() { + if state[index] == 2 { + continue; + } + let mut value = ValueId::new(index); + let root = loop { + check!(value.index() < body.values.len(), "invalid alias"); + if state[value.index()] == 2 { + break roots[value.index()]; + } + check!(state[value.index()] != 1, "alias cycle"); + state[value.index()] = 1; + path.push(value); + match body.values[value.index()].def { + ValueDef::Alias(next) => { + check!( + body.values + .get(next.index()) + .is_some_and(|v| v.ty == body.value_type(value)), + "alias type mismatch" + ); + value = next; + } + _ => break value, + } + }; + for value in path.drain(..) { + roots[value.index()] = root; + state[value.index()] = 2; + } + } + Ok(roots) +} diff --git a/compiler-core/src/ir/verify/types.rs b/compiler-core/src/ir/verify/types.rs new file mode 100644 index 0000000..70db6e8 --- /dev/null +++ b/compiler-core/src/ir/verify/types.rs @@ -0,0 +1,389 @@ +//! Typed operation and ABI contracts. +use super::*; + +pub(super) fn verify_types(inst: &Inst, body: &Body, types: &Types) -> Result<(), VerifyError> { + let result = inst.result.map(|v| body.value_type(v)); + let ty = |v| body.value_type(v); + match inst.op { + Op::ArrayLength(value) => check!( + result.and_then(|t| types.get(t)) == Some(Type::Scalar(ScalarType::I32)) + && matches!( + types.get(ty(value)), + Some(Type::Array(_) | Type::Slice(_) | Type::Str) + ), + "array length requires an array or view and int result" + ), + Op::Reinterpret(value) => { + check!( + result.and_then(|r| types.get(r)).map(Type::carrier) + == types.get(ty(value)).map(Type::carrier), + "reinterpretation changes JVM carrier" + ); + } + Op::Exception => check!( + result.is_some_and(|ty| types.get(ty).is_some_and(|ty| ty.carrier() == 5)), + "exception requires reference result" + ), + Op::Adapt(value) => { + check!( + types.get(ty(value)).is_some() && result.and_then(|r| types.get(r)).is_some(), + "invalid ABI adaptation" + ); + } + Op::NewArray(size) => { + check!( + types.get(ty(size)) == Some(Type::Scalar(ScalarType::I32)), + "array size requires JVM int" + ); + check!( + matches!(result.and_then(|r| types.get(r)), Some(Type::Array(_))), + "array allocation requires array result" + ); + } + Op::FunctionPointer { signature, target } => { + let signature = body + .methods + .get(signature.index()) + .ok_or_else(|| VerifyError("invalid callable signature".into()))?; + let target = body + .methods + .get(target.index()) + .ok_or_else(|| VerifyError("invalid callable target".into()))?; + check!( + signature.params == target.params && signature.returns == target.returns, + "callable descriptors differ" + ); + check!( + matches!(result.and_then(|r| types.get(r)), Some(Type::Interface(id) | Type::Class(id)) if types.symbol_name(id) == Some(signature.owner.as_str())), + "callable result type mismatch" + ); + } + Op::Call { method, kind, args } => { + let method = &body.methods[method.index()]; + check!( + types.get(method.returns).is_some(), + "invalid call return type" + ); + let args = &body.args[args.range()]; + let receiver = usize::from(matches!(kind, CallKind::Virtual | CallKind::Interface)); + check!( + kind != CallKind::Interface || method.interface, + "interface call requires interface method reference" + ); + check!( + kind != CallKind::Virtual || !method.interface, + "virtual call requires class method reference" + ); + check!( + kind != CallKind::Indirect, + "indirect calls need an explicit callable signature" + ); + check!( + args.len() == method.params.len() + receiver, + "call argument count mismatch" + ); + if receiver != 0 { + check!( + matches!( + types.get(ty(args[0])), + Some( + Type::Class(_) + | Type::Interface(_) + | Type::Pointer(_) + | Type::Array(_) + | Type::Slice(_) + | Type::Str + ) + ), + "call receiver is not an object" + ); + } + for (&arg, ¶m) in args[receiver..].iter().zip(&method.params) { + check!( + types.get(param).is_some_and(|t| t != Type::Unit) && ty(arg) == param, + "call argument type mismatch" + ); + } + if kind == CallKind::Constructor { + check!( + !method.interface + && method.name == "" + && types.get(method.returns) == Some(Type::Unit), + "invalid constructor signature" + ); + check!( + matches!(result.and_then(|id| types.get(id)), Some(Type::Class(symbol)) if types.symbol_name(symbol) == Some(method.owner.as_str())), + "constructor result owner mismatch" + ); + } else { + check!( + method.name != "" && method.name != "", + "initializer requires construction semantics" + ); + check!( + result + == (types.get(method.returns) != Some(Type::Unit)) + .then_some(method.returns), + "call return type mismatch" + ); + } + } + Op::Constant(id) => { + let actual = match body.constants[id.index()] { + Constant::Scalar(value) => Type::Scalar(value.ty()), + Constant::Unit => Type::Unit, + Constant::External { ty, .. } | Constant::Uninit(ty) => types + .get(ty) + .ok_or_else(|| VerifyError("invalid external constant type".into()))?, + Constant::Null(id) => { + let ty = types + .get(id) + .ok_or_else(|| VerifyError("invalid null type".into()))?; + check!( + !matches!(ty, Type::Scalar(_) | Type::Unit), + "null needs reference type" + ); + ty + } + }; + check!( + result.and_then(|id| types.get(id)) == Some(actual), + "constant type mismatch" + ); + } + Op::Binary { op, left, right } => { + if types.get(ty(left)).is_some_and(|t| t.carrier() == 5) { + check!( + matches!(op, BinaryOp::Eq | BinaryOp::Ne) + && types.get(ty(right)).is_some_and(|t| t.carrier() == 5), + "invalid reference comparison" + ); + check!( + result.and_then(|t| types.get(t)) == Some(Type::Scalar(ScalarType::Bool)), + "reference comparison requires boolean" + ); + return Ok(()); + } + let Some(Type::Scalar(left_ty)) = types.get(ty(left)) else { + return Err(VerifyError("binary operation needs scalar operands".into())); + }; + let integer = left_ty.integer().is_some(); + let float = matches!(left_ty, ScalarType::F16 | ScalarType::F32 | ScalarType::F64); + check!( + match op { + BinaryOp::Eq | BinaryOp::Ne => true, + BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => + integer || float || left_ty == ScalarType::Char, + BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor => + integer || left_ty == ScalarType::Bool, + BinaryOp::Shl | BinaryOp::Shr => integer, + _ => integer || float, + }, + "invalid binary operand category" + ); + if matches!(op, BinaryOp::Shl | BinaryOp::Shr) { + check!( + matches!(types.get(ty(right)), Some(Type::Scalar(t)) if t.integer().is_some()), + "shift count needs integer" + ); + } else { + check!(ty(left) == ty(right), "binary operand type mismatch"); + } + if op.is_comparison() { + check!( + result.and_then(|id| types.get(id)) == Some(Type::Scalar(ScalarType::Bool)), + "comparison result needs boolean" + ); + } else { + check!(result == Some(ty(left)), "binary result type mismatch"); + } + } + Op::Overflow { op, args } => { + check!(args.len == 3, "overflow needs operands and wrapped result"); + let args = &body.args[args.range()]; + check!( + matches!(op, BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul), + "unsupported checked operation" + ); + check!( + matches!(types.get(ty(args[0])), Some(Type::Scalar(t)) if t.integer().is_some()), + "overflow requires integer" + ); + check!( + args.iter().all(|&arg| ty(arg) == ty(args[0])), + "overflow operand type mismatch" + ); + check!( + result.and_then(|t| types.get(t)) == Some(Type::Scalar(ScalarType::Bool)), + "overflow result must be boolean" + ); + } + Op::Not(value) | Op::Neg(value) => { + check!(result == Some(ty(value)), "unary type mismatch"); + let Some(Type::Scalar(t)) = types.get(ty(value)) else { + return Err(VerifyError("unary operation needs scalar".into())); + }; + check!( + t.integer().is_some() + || match inst.op { + Op::Not(_) => t == ScalarType::Bool, + _ => matches!(t, ScalarType::F16 | ScalarType::F32 | ScalarType::F64), + }, + "invalid unary operand category" + ); + } + Op::Bit { op, value } => { + check!( + matches!(types.get(ty(value)), Some(Type::Scalar(t)) if t.integer().is_some()), + "bit operation requires integer" + ); + if op.is_count() { + check!( + result.and_then(|t| types.get(t)) == Some(Type::Scalar(ScalarType::U32)), + "bit count must return u32" + ); + } else { + check!( + result == Some(ty(value)), + "bit operation changes operand type" + ); + } + } + Op::Opaque(value) => check!(result == Some(ty(value)), "opaque value type mismatch"), + Op::Project { base, projection } => { + let projection = body + .projections + .get(projection.index()) + .ok_or_else(|| VerifyError("invalid pointer projection".into()))?; + let field = body + .fields + .get(projection.field.index()) + .ok_or_else(|| VerifyError("invalid projection field".into()))?; + check!( + !field.is_static && types.get(ty(base)) == Some(Type::Pointer(field.owner)), + "projection owner mismatch" + ); + check!( + result.and_then(|id| types.get(id)) == Some(Type::Pointer(field.ty)), + "projection result mismatch" + ); + check!( + projection.offset <= i64::MAX as u64 && projection.size <= i64::MAX as u64, + "projection exceeds runtime address space" + ); + } + Op::Offset { + pointer, offset, .. + } => { + check!( + matches!(types.get(ty(pointer)), Some(Type::Pointer(_))) + && result == Some(ty(pointer)), + "pointer offset type mismatch" + ); + check!( + matches!(types.get(ty(offset)), Some(Type::Scalar(t)) if t.integer().is_some()), + "pointer offset requires integer displacement" + ); + } + Op::Length(view) => { + check!( + matches!(types.get(ty(view)), Some(Type::Slice(_) | Type::Str)), + "length needs a slice or string" + ); + check!( + result.and_then(|id| types.get(id)) == Some(Type::Scalar(ScalarType::U64)), + "length needs usize result" + ); + } + Op::View { data, length } => { + let element = view_element(result.and_then(|id| types.get(id)), types); + check!( + element.is_some() && types.get(ty(data)) == element.map(Type::Pointer), + "view data type mismatch" + ); + check!( + types.get(ty(length)) == Some(Type::Scalar(ScalarType::U64)), + "view length needs usize" + ); + } + Op::ViewData { view, size, codec } => { + let element = view_element(types.get(ty(view)), types); + check!( + element.is_some() + && result.and_then(|id| types.get(id)) == element.map(Type::Pointer), + "view extraction type mismatch" + ); + check!( + size <= i32::MAX as u32 && codec.is_none_or(|id| types.symbol_name(id).is_some()), + "invalid view element layout" + ); + } + Op::Cast(value) => { + check!(result.is_some(), "cast has no result"); + if matches!(types.get(ty(value)), Some(Type::Scalar(_))) { + check!( + matches!(result.and_then(|t| types.get(t)), Some(Type::Scalar(_))), + "scalar cast needs scalar result" + ); + } + } + Op::LoadSlot(slot) => check!( + result == Some(body.slots[slot.index()].ty), + "slot load type mismatch" + ), + Op::AddressOfSlot(slot) => check!( + result.and_then(|ty| types.get(ty)) == Some(Type::Pointer(body.slots[slot.index()].ty)), + "slot address type mismatch" + ), + Op::Load(pointer) => check!( + result.is_some() && types.get(ty(pointer)) == result.map(Type::Pointer), + "pointer load type mismatch" + ), + Op::Store { pointer, value } => check!( + result.is_none() && types.get(ty(pointer)) == Some(Type::Pointer(ty(value))), + "pointer store type mismatch" + ), + Op::StoreSlot { slot, value } => { + check!( + result.is_none() && ty(value) == body.slots[slot.index()].ty, + "slot store type mismatch" + ); + } + Op::GetField { field, .. } + | Op::SetField { field, .. } + | Op::GetStatic(field) + | Op::SetStatic { field, .. } => { + let member = body + .fields + .get(field.index()) + .ok_or_else(|| VerifyError("invalid field reference".into()))?; + let static_access = matches!(inst.op, Op::GetStatic(_) | Op::SetStatic { .. }); + check!( + member.is_static == static_access, + "field access kind mismatch" + ); + if let Op::GetField { object, .. } | Op::SetField { object, .. } = inst.op { + check!(ty(object) == member.owner, "field receiver type mismatch"); + } + if let Op::SetField { value, .. } | Op::SetStatic { value, .. } = inst.op { + check!( + result.is_none() && ty(value) == member.ty, + "field store type mismatch" + ); + } else { + check!(result == Some(member.ty), "field load type mismatch"); + } + } + Op::ArraySet { .. } => check!(result.is_none(), "store produces value"), + _ => {} + } + Ok(()) +} + +fn view_element(ty: Option, types: &Types) -> Option { + match ty { + Some(Type::Slice(element)) => Some(element), + Some(Type::Str) => types.find(Type::Scalar(ScalarType::U8)), + _ => None, + } +} diff --git a/compiler-core/src/opt/live.rs b/compiler-core/src/opt/live.rs new file mode 100644 index 0000000..19c54e3 --- /dev/null +++ b/compiler-core/src/opt/live.rs @@ -0,0 +1,135 @@ +use crate::ir::*; + +pub struct Live { + pub values: Vec, + pub instructions: Vec, + pub blocks: Vec, +} + +/// Mark backwards from control flow and ordered effects. SSA definitions make a +/// separate per-instruction use table unnecessary for this analysis. Unused +/// parameter cycles die together; live parameters follow their incoming edges. +pub fn live(body: &Body, types: &Types) -> Live { + live_with_roots(body, types, std::iter::empty()) +} + +pub fn live_with_roots( + body: &Body, + types: &Types, + roots: impl IntoIterator, +) -> Live { + let mut live = Live { + values: vec![false; body.values.len()], + instructions: vec![false; body.instructions.len()], + blocks: body.reachable(), + }; + let mut pending = roots.into_iter().collect::>(); + let mut positions = vec![0; body.values.len()]; + let predecessors = body.predecessors(); + for (index, block) in body.blocks.iter().enumerate() { + for (position, ¶m) in block.params.iter().enumerate() { + positions[param.index()] = position; + } + if !live.blocks[index] { + continue; + } + for &inst in &block.instructions { + if has_effects(body.instructions[inst.index()].op, body, types) { + live.instructions[inst.index()] = true; + body.instructions[inst.index()] + .op + .visit_uses(&body.args, |v| pending.push(v)); + } + } + let term = block.terminator.unwrap(); + term.visit_uses(|v| pending.push(v)); + if let Terminator::Invoke { inst, .. } = term { + live.instructions[inst.index()] = true; + body.instructions[inst.index()] + .op + .visit_uses(&body.args, |v| pending.push(v)); + } + } + // Signature parameters remain available at their ABI slots even if unused. + pending.extend(&body.blocks[body.entry.index()].params); + while let Some(value) = pending.pop() { + let value = body.resolve(value); + if std::mem::replace(&mut live.values[value.index()], true) { + continue; + } + match body.values[value.index()].def { + ValueDef::Inst(inst) => { + if !std::mem::replace(&mut live.instructions[inst.index()], true) { + body.instructions[inst.index()] + .op + .visit_uses(&body.args, |v| pending.push(v)); + } + } + ValueDef::Param(block) => { + for &(source, edge) in &predecessors[block.index()] { + if live.blocks[source.index()] { + pending.push(body.edges[edge.index()].args[positions[value.index()]]); + } + } + } + ValueDef::Unreachable => {} + ValueDef::Alias(_) => unreachable!(), + } + } + live +} + +fn has_effects(op: Op, body: &Body, types: &Types) -> bool { + // An unused fat-pointer carrier has no observable identity. + !matches!(op, Op::View { .. }) && op.may_throw(body, types) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scalar::{BinaryOp, Scalar, ScalarType}; + #[test] + fn discards_dead_scalar_chains_but_keeps_potentially_throwing_division() { + let mut types = Types::default(); + let int = types.scalar(ScalarType::I32); + let mut b = Builder::new(&types, int); + let divisor = b.parameter(b.current(), int); + let mut previous = b.constant(int, Scalar::integer(ScalarType::I32, 7).unwrap()); + for _ in 0..100_000 { + previous = b + .emit( + Op::Binary { + op: BinaryOp::Add, + left: previous, + right: divisor, + }, + Some(int), + ) + .unwrap(); + } + let seven = b.constant(int, Scalar::integer(ScalarType::I32, 7).unwrap()); + let divided = b + .emit( + Op::Binary { + op: BinaryOp::Div, + left: seven, + right: divisor, + }, + Some(int), + ) + .unwrap(); + b.terminate(Terminator::Return(Some(divisor))); + let body = b.finish().unwrap(); + let live = live(&body, &types); + assert!(!live.values[previous.index()]); + assert!(!live.values[divided.index()]); + assert_eq!(live.instructions.iter().filter(|&&x| x).count(), 2); + let code = crate::jvm::select::compile(&body, &types, &mut Default::default()).unwrap(); + // The literal numerator is rematerialized; only the argument needs a local. + assert_eq!(code.max_locals, 1); + assert!( + code.instructions + .contains(&crate::classfile::attributes::Instruction::Idiv) + ); + } +} diff --git a/compiler-core/src/opt/mod.rs b/compiler-core/src/opt/mod.rs new file mode 100644 index 0000000..54ea28f --- /dev/null +++ b/compiler-core/src/opt/mod.rs @@ -0,0 +1,3 @@ +//! Analyses are built on demand and owned by one body compilation. +mod live; +pub use live::{Live, live, live_with_roots}; diff --git a/compiler-core/src/scalar.rs b/compiler-core/src/scalar.rs new file mode 100644 index 0000000..3657750 --- /dev/null +++ b/compiler-core/src/scalar.rs @@ -0,0 +1,297 @@ +//! Allocation-free scalar semantics. Bit identity is distinct from numeric equality. +mod bits; +mod cast; +pub use bits::BitOp; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum BinaryOp { + Add, + Sub, + Mul, + Div, + Rem, + Eq, + Ne, + Lt, + Le, + Gt, + Ge, + BitAnd, + BitOr, + BitXor, + Shl, + Shr, +} + +impl BinaryOp { + pub fn is_comparison(self) -> bool { + matches!( + self, + Self::Eq | Self::Ne | Self::Lt | Self::Le | Self::Gt | Self::Ge + ) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum ScalarType { + Bool, + Char, + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, + I128, + U128, + F16, + F32, + F64, +} + +impl ScalarType { + pub fn integer(self) -> Option<(u32, bool)> { + use ScalarType::*; + Some(match self { + I8 => (8, true), + U8 => (8, false), + I16 => (16, true), + U16 => (16, false), + I32 => (32, true), + U32 => (32, false), + I64 => (64, true), + U64 => (64, false), + I128 => (128, true), + U128 => (128, false), + _ => return None, + }) + } + + fn mask(self) -> u128 { + let width = self.integer().map_or(128, |(width, _)| width); + u128::MAX >> (128 - width) + } +} + +/// Constants are pool entries, not embedded in every operand. Two words avoid +/// u128's 16-byte alignment inflating this record from 24 to 32 bytes. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Scalar { + words: [u64; 2], + ty: ScalarType, +} + +impl Scalar { + pub fn from_bits(ty: ScalarType, bits: u128) -> Option { + use ScalarType::*; + let valid = match ty { + Bool => bits <= 1, + Char => u32::try_from(bits).ok().and_then(char::from_u32).is_some(), + F16 => bits <= u16::MAX.into(), + F32 => bits <= u32::MAX.into(), + F64 => bits <= u64::MAX.into(), + _ => bits & !ty.mask() == 0, + }; + valid.then_some(Self { + words: [bits as u64, (bits >> 64) as u64], + ty, + }) + } + + pub fn integer(ty: ScalarType, bits: u128) -> Option { + ty.integer()?; + Self::from_bits(ty, bits & ty.mask()) + } + + pub fn boolean(value: bool) -> Self { + Self { + words: [u64::from(value), 0], + ty: ScalarType::Bool, + } + } + + pub fn f32(value: f32) -> Self { + Self { + words: [value.to_bits().into(), 0], + ty: ScalarType::F32, + } + } + + pub fn f64(value: f64) -> Self { + Self { + words: [value.to_bits(), 0], + ty: ScalarType::F64, + } + } + + pub fn ty(self) -> ScalarType { + self.ty + } + pub fn bits(self) -> u128 { + u128::from(self.words[0]) | (u128::from(self.words[1]) << 64) + } + + pub fn signed(self) -> Option { + let (width, signed) = self.ty.integer()?; + signed.then(|| ((self.bits() << (128 - width)) as i128) >> (128 - width)) + } + + pub fn overflows(self, op: BinaryOp, rhs: Self) -> Option { + if self.ty != rhs.ty { + return None; + } + let (width, signed) = self.ty.integer()?; + Some(if signed { + let a = self.signed()?; + let b = rhs.signed()?; + let result = match op { + BinaryOp::Add => a.checked_add(b), + BinaryOp::Sub => a.checked_sub(b), + BinaryOp::Mul => a.checked_mul(b), + _ => return None, + }; + let min = i128::MIN >> (128 - width); + let max = i128::MAX >> (128 - width); + result.is_none_or(|result| result < min || result > max) + } else { + let result = match op { + BinaryOp::Add => self.bits().checked_add(rhs.bits()), + BinaryOp::Sub => self.bits().checked_sub(rhs.bits()), + BinaryOp::Mul => self.bits().checked_mul(rhs.bits()), + _ => return None, + }; + result.is_none_or(|result| result > self.ty.mask()) + }) + } + + pub fn binary(self, op: BinaryOp, rhs: Self) -> Option { + use BinaryOp::*; + if matches!(op, Shl | Shr) { + let (width, signed) = self.ty.integer()?; + rhs.ty.integer()?; + let amount = u32::try_from(rhs.bits()).ok()?; + if amount >= width { + return None; + } + let value = match op { + Shl => self.bits() << amount, + Shr if signed => (self.signed()? >> amount) as u128, + Shr => self.bits() >> amount, + _ => unreachable!(), + }; + return Self::integer(self.ty, value); + } + if self.ty != rhs.ty { + return None; + } + + macro_rules! float { + ($a:expr, $b:expr, $constructor:ident) => {{ + let (a, b) = ($a, $b); + return Some(match op { + Add => Self::$constructor(a + b), + Sub => Self::$constructor(a - b), + Mul => Self::$constructor(a * b), + Div => Self::$constructor(a / b), + Rem => Self::$constructor(a % b), + Eq => Self::boolean(a == b), + Ne => Self::boolean(a != b), + Lt => Self::boolean(a < b), + Le => Self::boolean(a <= b), + Gt => Self::boolean(a > b), + Ge => Self::boolean(a >= b), + _ => return None, + }); + }}; + } + match self.ty { + ScalarType::F32 => float!( + f32::from_bits(self.words[0] as u32), + f32::from_bits(rhs.words[0] as u32), + f32 + ), + ScalarType::F64 => float!( + f64::from_bits(self.words[0]), + f64::from_bits(rhs.words[0]), + f64 + ), + ScalarType::F16 => return None, + _ => {} + } + let (a, b) = (self.bits(), rhs.bits()); + if op.is_comparison() { + let ordering = if let (Some(a), Some(b)) = (self.signed(), rhs.signed()) { + a.cmp(&b) + } else { + a.cmp(&b) + }; + return Some(Self::boolean(match op { + Eq => ordering.is_eq(), + Ne => !ordering.is_eq(), + Lt => ordering.is_lt(), + Le => !ordering.is_gt(), + Gt => ordering.is_gt(), + Ge => !ordering.is_lt(), + _ => unreachable!(), + })); + } + if self.ty == ScalarType::Bool { + return Some(Self::boolean(match op { + BitAnd => a & b != 0, + BitOr => a | b != 0, + BitXor => a ^ b != 0, + _ => return None, + })); + } + let (width, signed) = self.ty.integer()?; + let value = match op { + Add => a.wrapping_add(b), + Sub => a.wrapping_sub(b), + Mul => a.wrapping_mul(b), + BitAnd => a & b, + BitOr => a | b, + BitXor => a ^ b, + Div | Rem if signed => { + let (a, b) = (self.signed()?, rhs.signed()?); + // Preserve the overflow/zero checks even for narrow integers. + let min = i128::MIN >> (128 - width); + if b == 0 || (a == min && b == -1) { + return None; + } + if op == Div { + (a / b) as u128 + } else { + (a % b) as u128 + } + } + Div => a.checked_div(b)?, + Rem => a.checked_rem(b)?, + _ => return None, + }; + Self::integer(self.ty, value) + } + + pub fn not(self) -> Option { + if self.ty == ScalarType::Bool { + Some(Self::boolean(self.bits() == 0)) + } else { + Self::integer(self.ty, !self.bits()) + } + } + + pub fn neg(self) -> Option { + match self.ty { + ScalarType::F32 => Self::from_bits(self.ty, self.bits() ^ (1 << 31)), + ScalarType::F64 => Self::from_bits(self.ty, self.bits() ^ (1 << 63)), + _ => Self::integer(self.ty, self.bits().wrapping_neg()), + } + } +} + +mod fold; +#[cfg(test)] +mod tests; +pub use fold::{BinaryFold, fold_binary}; diff --git a/compiler-core/src/scalar/bits.rs b/compiler-core/src/scalar/bits.rs new file mode 100644 index 0000000..37589e8 --- /dev/null +++ b/compiler-core/src/scalar/bits.rs @@ -0,0 +1,35 @@ +use super::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum BitOp { + Count, + LeadingZeros, + TrailingZeros, + Reverse, + SwapBytes, +} + +impl BitOp { + pub fn is_count(self) -> bool { + matches!(self, Self::Count | Self::LeadingZeros | Self::TrailingZeros) + } +} + +impl Scalar { + pub fn bit(self, op: BitOp) -> Option { + let (width, _) = self.ty.integer()?; + let bits = self.bits(); + Some(match op { + BitOp::Count => Self::integer(ScalarType::U32, bits.count_ones() as u128)?, + BitOp::LeadingZeros => Self::integer( + ScalarType::U32, + (bits.leading_zeros() - (128 - width)) as u128, + )?, + BitOp::TrailingZeros => { + Self::integer(ScalarType::U32, bits.trailing_zeros().min(width) as u128)? + } + BitOp::Reverse => Self::integer(self.ty, bits.reverse_bits() >> (128 - width))?, + BitOp::SwapBytes => Self::integer(self.ty, bits.swap_bytes() >> (128 - width))?, + }) + } +} diff --git a/compiler-core/src/scalar/cast.rs b/compiler-core/src/scalar/cast.rs new file mode 100644 index 0000000..3310bd6 --- /dev/null +++ b/compiler-core/src/scalar/cast.rs @@ -0,0 +1,50 @@ +use super::*; + +impl Scalar { + pub fn cast(self, target: ScalarType) -> Option { + use ScalarType::*; + if self.ty == target { + return Some(self); + } + macro_rules! float { + ($value:expr) => {{ + let value = $value; + return match target { + F32 => Some(Self::f32(value as f32)), + F64 => Some(Self::f64(value as f64)), + I8 => Self::integer(target, (value as i8) as u128), + U8 => Self::integer(target, (value as u8) as u128), + I16 => Self::integer(target, (value as i16) as u128), + U16 => Self::integer(target, (value as u16) as u128), + I32 => Self::integer(target, (value as i32) as u128), + U32 => Self::integer(target, (value as u32) as u128), + I64 => Self::integer(target, (value as i64) as u128), + U64 => Self::integer(target, (value as u64) as u128), + I128 => Self::integer(target, (value as i128) as u128), + U128 => Self::integer(target, value as u128), + _ => None, + }; + }}; + } + match self.ty { + F16 => return None, + F32 => float!(f32::from_bits(self.words[0] as u32)), + F64 => float!(f64::from_bits(self.words[0])), + _ => {} + } + let signed = self.signed(); + let bits = signed.map_or(self.bits(), |value| value as u128); + match target { + F32 => Some(Self::f32( + signed.map_or_else(|| self.bits() as f32, |value| value as f32), + )), + F64 => Some(Self::f64( + signed.map_or_else(|| self.bits() as f64, |value| value as f64), + )), + Bool => Some(Self::boolean(bits != 0)), + Char => Self::from_bits(Char, bits), + F16 => None, + _ => Self::integer(target, bits), + } + } +} diff --git a/compiler-core/src/scalar/fold.rs b/compiler-core/src/scalar/fold.rs new file mode 100644 index 0000000..47e415f --- /dev/null +++ b/compiler-core/src/scalar/fold.rs @@ -0,0 +1,46 @@ +use super::{BinaryOp, Scalar, ScalarType}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BinaryFold { + Left, + Right, + Constant(Scalar), +} + +/// Integer identities and constant evaluation shared by construction and dataflow. +/// Operands have already been evaluated; identities never remove their effects. +pub fn fold_binary( + op: BinaryOp, + left_ty: ScalarType, + right_ty: ScalarType, + left: Option, + right: Option, + same_value: impl FnOnce() -> bool, +) -> Option { + use BinaryFold::*; + use BinaryOp::*; + if let (Some(left), Some(right)) = (left, right) { + return left.binary(op, right).map(Constant); + } + left_ty.integer()?; + if !matches!(op, Shl | Shr) && left_ty != right_ty { + return None; + } + let (a, b) = (left.map(Scalar::bits), right.map(Scalar::bits)); + let same_value = matches!(op, Sub | BitXor | BitAnd | BitOr) && same_value(); + let zero = || Constant(Scalar::integer(left_ty, 0).unwrap()); + Some(match op { + Add | BitOr | BitXor if b == Some(0) => Left, + Add | BitOr | BitXor if a == Some(0) => Right, + Sub | Shl | Shr if b == Some(0) => Left, + Mul | BitAnd if a == Some(0) || b == Some(0) => zero(), + Mul | Div if b == Some(1) => Left, + Mul if a == Some(1) => Right, + Rem if b == Some(1) => zero(), + Sub | BitXor if same_value => zero(), + BitAnd | BitOr if same_value => Left, + // 0/x and x/x retain possible division by zero. Floating-point + // identities would change NaN or signed-zero behavior. + _ => return None, + }) +} diff --git a/compiler-core/src/scalar/tests.rs b/compiler-core/src/scalar/tests.rs new file mode 100644 index 0000000..086cb6c --- /dev/null +++ b/compiler-core/src/scalar/tests.rs @@ -0,0 +1,127 @@ +use super::*; +use std::collections::HashSet; + +#[test] +fn casts_extend_truncate_saturate_and_preserve_float_width() { + let negative = Scalar::integer(ScalarType::I8, 0xff).unwrap(); + assert_eq!( + negative.cast(ScalarType::U64).unwrap().bits(), + u64::MAX.into() + ); + assert_eq!(negative.cast(ScalarType::I128).unwrap().signed(), Some(-1)); + assert_eq!( + Scalar::f64(f64::NAN).cast(ScalarType::I32).unwrap().bits(), + 0 + ); + assert_eq!( + Scalar::f32(f32::INFINITY) + .cast(ScalarType::I8) + .unwrap() + .signed(), + Some(127) + ); + assert_eq!( + Scalar::f64(f64::NEG_INFINITY) + .cast(ScalarType::U128) + .unwrap() + .bits(), + 0 + ); + assert_eq!( + Scalar::f64(0.0).neg().unwrap().cast(ScalarType::F32), + Some(Scalar::f32(-0.0)) + ); + assert!(negative.cast(ScalarType::Char).is_none()); + assert_eq!( + Scalar::integer(ScalarType::U32, 0x1f980) + .unwrap() + .cast(ScalarType::Char) + .unwrap() + .bits(), + 0x1f980 + ); + assert!( + Scalar::integer(ScalarType::U32, 0xd800) + .unwrap() + .cast(ScalarType::Char) + .is_none() + ); +} + +#[test] +fn float_identity_is_bitwise_but_comparisons_are_numeric() { + for zero in [Scalar::f32(0.0), Scalar::f64(0.0)] { + let negative = zero.neg().unwrap(); + assert_ne!(zero, negative); + assert_eq!( + zero.binary(BinaryOp::Eq, negative), + Some(Scalar::boolean(true)) + ); + } + let nan = Scalar::f64(f64::from_bits(0x7ff8_0000_0000_0042)); + assert_eq!(nan, nan); + assert_eq!(HashSet::from([nan, nan]).len(), 1); + assert_eq!(nan.binary(BinaryOp::Eq, nan), Some(Scalar::boolean(false))); + assert_eq!(nan.binary(BinaryOp::Ne, nan), Some(Scalar::boolean(true))); + assert_eq!(nan.binary(BinaryOp::Le, nan), Some(Scalar::boolean(false))); + assert!( + f64::from_bits(Scalar::f64(0.0).binary(BinaryOp::Mul, nan).unwrap().bits() as u64).is_nan() + ); +} + +#[test] +fn integer_widths_overflow_and_traps_are_preserved() { + for ty in [ + ScalarType::I8, + ScalarType::U8, + ScalarType::I16, + ScalarType::U16, + ScalarType::I32, + ScalarType::U32, + ScalarType::I64, + ScalarType::U64, + ScalarType::I128, + ScalarType::U128, + ] { + let (width, signed) = ty.integer().unwrap(); + let one = Scalar::integer(ty, 1).unwrap(); + let zero = Scalar::integer(ty, 0).unwrap(); + let all = Scalar::integer(ty, u128::MAX).unwrap(); + assert_eq!(all.binary(BinaryOp::Add, one), Some(zero)); + assert_eq!(zero.binary(BinaryOp::Div, zero), None); + assert_eq!(one.binary(BinaryOp::Rem, zero), None); + assert_eq!( + one.binary( + BinaryOp::Shl, + Scalar::integer(ScalarType::U32, width.into()).unwrap() + ), + None + ); + if signed { + let min = Scalar::integer(ty, 1 << (width - 1)).unwrap(); + assert_eq!(min.binary(BinaryOp::Div, all), None); + assert_eq!(min.binary(BinaryOp::Rem, all), None); + assert_eq!(all.binary(BinaryOp::Shr, one), Some(all)); + } + } +} + +#[test] +fn exhaustive_byte_arithmetic_matches_rust() { + for a in u8::MIN..=u8::MAX { + for b in u8::MIN..=u8::MAX { + let x = Scalar::integer(ScalarType::U8, a.into()).unwrap(); + let y = Scalar::integer(ScalarType::U8, b.into()).unwrap(); + for (op, expected) in [ + (BinaryOp::Add, a.wrapping_add(b)), + (BinaryOp::Sub, a.wrapping_sub(b)), + (BinaryOp::Mul, a.wrapping_mul(b)), + (BinaryOp::BitAnd, a & b), + (BinaryOp::BitOr, a | b), + (BinaryOp::BitXor, a ^ b), + ] { + assert_eq!(x.binary(op, y).unwrap().bits(), u128::from(expected)); + } + } + } +} From 088a269357e2d5c8ded18844f26037b61a649bc5 Mon Sep 17 00:00:00 2001 From: Michael Reeves Date: Sun, 6 Sep 2026 15:49:57 +1000 Subject: [PATCH 02/22] add SSA JVM codegen --- compiler-core/Cargo.lock | 338 +++++++++ compiler-core/Cargo.toml | 13 + compiler-core/src/jvm/abi.rs | 15 + compiler-core/src/jvm/casts.rs | 199 +++++ compiler-core/src/jvm/constants.rs | 99 +++ compiler-core/src/jvm/encoding.rs | 59 ++ compiler-core/src/jvm/flow.rs | 109 +++ compiler-core/src/jvm/frames/analysis.rs | 289 +++++++ compiler-core/src/jvm/frames/descriptors.rs | 175 +++++ compiler-core/src/jvm/frames/encoding.rs | 196 +++++ compiler-core/src/jvm/frames/flow.rs | 34 + compiler-core/src/jvm/frames/locals.rs | 488 ++++++++++++ compiler-core/src/jvm/frames/mod.rs | 219 ++++++ compiler-core/src/jvm/frames/tests.rs | 91 +++ compiler-core/src/jvm/frames/transfer.rs | 626 ++++++++++++++++ compiler-core/src/jvm/locals.rs | 66 ++ compiler-core/src/jvm/mod.rs | 19 + compiler-core/src/jvm/select/allocate.rs | 277 +++++++ compiler-core/src/jvm/select/arrays.rs | 135 ++++ compiler-core/src/jvm/select/assemble.rs | 104 +++ compiler-core/src/jvm/select/bits.rs | 66 ++ compiler-core/src/jvm/select/calls.rs | 45 ++ compiler-core/src/jvm/select/checked.rs | 98 +++ compiler-core/src/jvm/select/debug.rs | 195 +++++ compiler-core/src/jvm/select/debug_tests.rs | 152 ++++ compiler-core/src/jvm/select/general.rs | 124 ++++ compiler-core/src/jvm/select/memory.rs | 303 ++++++++ compiler-core/src/jvm/select/mod.rs | 501 +++++++++++++ compiler-core/src/jvm/select/object_tests.rs | 279 +++++++ compiler-core/src/jvm/select/objects.rs | 100 +++ .../src/jvm/select/representation.rs | 97 +++ compiler-core/src/jvm/select/scalar.rs | 274 +++++++ compiler-core/src/jvm/select/slots.rs | 42 ++ compiler-core/src/jvm/select/switches.rs | 49 ++ compiler-core/src/jvm/select/tests.rs | 702 ++++++++++++++++++ compiler-core/src/jvm/select/unwind_tests.rs | 108 +++ compiler-core/src/jvm/select/view_tests.rs | 201 +++++ compiler-core/src/jvm/select/views.rs | 56 ++ compiler-core/src/lib.rs | 10 + 39 files changed, 6953 insertions(+) create mode 100644 compiler-core/Cargo.lock create mode 100644 compiler-core/Cargo.toml create mode 100644 compiler-core/src/jvm/abi.rs create mode 100644 compiler-core/src/jvm/casts.rs create mode 100644 compiler-core/src/jvm/constants.rs create mode 100644 compiler-core/src/jvm/encoding.rs create mode 100644 compiler-core/src/jvm/flow.rs create mode 100644 compiler-core/src/jvm/frames/analysis.rs create mode 100644 compiler-core/src/jvm/frames/descriptors.rs create mode 100644 compiler-core/src/jvm/frames/encoding.rs create mode 100644 compiler-core/src/jvm/frames/flow.rs create mode 100644 compiler-core/src/jvm/frames/locals.rs create mode 100644 compiler-core/src/jvm/frames/mod.rs create mode 100644 compiler-core/src/jvm/frames/tests.rs create mode 100644 compiler-core/src/jvm/frames/transfer.rs create mode 100644 compiler-core/src/jvm/locals.rs create mode 100644 compiler-core/src/jvm/mod.rs create mode 100644 compiler-core/src/jvm/select/allocate.rs create mode 100644 compiler-core/src/jvm/select/arrays.rs create mode 100644 compiler-core/src/jvm/select/assemble.rs create mode 100644 compiler-core/src/jvm/select/bits.rs create mode 100644 compiler-core/src/jvm/select/calls.rs create mode 100644 compiler-core/src/jvm/select/checked.rs create mode 100644 compiler-core/src/jvm/select/debug.rs create mode 100644 compiler-core/src/jvm/select/debug_tests.rs create mode 100644 compiler-core/src/jvm/select/general.rs create mode 100644 compiler-core/src/jvm/select/memory.rs create mode 100644 compiler-core/src/jvm/select/mod.rs create mode 100644 compiler-core/src/jvm/select/object_tests.rs create mode 100644 compiler-core/src/jvm/select/objects.rs create mode 100644 compiler-core/src/jvm/select/representation.rs create mode 100644 compiler-core/src/jvm/select/scalar.rs create mode 100644 compiler-core/src/jvm/select/slots.rs create mode 100644 compiler-core/src/jvm/select/switches.rs create mode 100644 compiler-core/src/jvm/select/tests.rs create mode 100644 compiler-core/src/jvm/select/unwind_tests.rs create mode 100644 compiler-core/src/jvm/select/view_tests.rs create mode 100644 compiler-core/src/jvm/select/views.rs create mode 100644 compiler-core/src/lib.rs diff --git a/compiler-core/Cargo.lock b/compiler-core/Cargo.lock new file mode 100644 index 0000000..9dbb6a7 --- /dev/null +++ b/compiler-core/Cargo.lock @@ -0,0 +1,338 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "jvm-compiler-core" +version = "0.1.0" +dependencies = [ + "ristretto_classfile", + "rustc-hash", + "serde", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "ristretto_classfile" +version = "0.31.0" +source = "git+https://github.com/IntegralPilot/ristretto.git?rev=23cd035e386acad45f420f0478415df1d9856b4f#23cd035e386acad45f420f0478415df1d9856b4f" +dependencies = [ + "ahash", + "bitflags", + "byteorder", + "getrandom", + "hashbrown", + "indexmap", + "thiserror", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/compiler-core/Cargo.toml b/compiler-core/Cargo.toml new file mode 100644 index 0000000..6bffe7a --- /dev/null +++ b/compiler-core/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "jvm-compiler-core" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +serde = { version = "1.0.219", features = ["derive"], optional = true } +rustc-hash = "2.1.1" +ristretto_classfile = "0.31.0" + +[patch.crates-io] +ristretto_classfile = { git = "https://github.com/IntegralPilot/ristretto.git", rev = "23cd035e386acad45f420f0478415df1d9856b4f" } diff --git a/compiler-core/src/jvm/abi.rs b/compiler-core/src/jvm/abi.rs new file mode 100644 index 0000000..0acb87e --- /dev/null +++ b/compiler-core/src/jvm/abi.rs @@ -0,0 +1,15 @@ +//! JVM names shared by body selection and generated representation schemas. +pub const SLICE_VIEW_CLASS: &str = "org/rustlang/runtime/SliceView"; +pub const UTF8_VIEW_CLASS: &str = "org/rustlang/runtime/Utf8View"; +pub const POINTER_CLASS: &str = "org/rustlang/runtime/Pointer"; +pub const RELATIVE_POINTER_METHOD_SUFFIX: &str = "$relative"; +pub const RELATIVE_POINTER_ELEMENT_OFFSET_SUFFIX: &str = "$rcj$elementOffset"; +pub const RELATIVE_POINTER_BYTE_OFFSET_SUFFIX: &str = "$rcj$byteOffset"; + +pub fn relative_pointer_element_offset_field(field: &str) -> String { + format!("{field}{RELATIVE_POINTER_ELEMENT_OFFSET_SUFFIX}") +} + +pub fn relative_pointer_byte_offset_field(field: &str) -> String { + format!("{field}{RELATIVE_POINTER_BYTE_OFFSET_SUFFIX}") +} diff --git a/compiler-core/src/jvm/casts.rs b/compiler-core/src/jvm/casts.rs new file mode 100644 index 0000000..96b9a6e --- /dev/null +++ b/compiler-core/src/jvm/casts.rs @@ -0,0 +1,199 @@ +//! Rust primitive casts on JVM scalar carriers, shared by both selectors. +use super::constants::{get_int_const_instr, get_long_const_instr}; +use crate::{ + classfile::{self as jvm, attributes::Instruction, constant_pool::InternedConstantPool}, + scalar::ScalarType, +}; + +/// Semantic Rust primitive casts. The JVM descriptor alone is insufficient here: +/// `u32` is carried in an `int`, `u64` in a `long`, and `f16` in a `short` bit-pattern. +pub fn primitive( + src: &ScalarType, + dest: &ScalarType, + cp: &mut InternedConstantPool, +) -> Result, jvm::Error> { + use Instruction as JI; + + fn int_width(ty: &ScalarType) -> Option { + match ty { + ScalarType::Bool => Some(1), + ScalarType::I8 | ScalarType::U8 => Some(8), + ScalarType::I16 | ScalarType::U16 | ScalarType::Char => Some(16), + ScalarType::I32 | ScalarType::U32 => Some(32), + ScalarType::I64 | ScalarType::U64 => Some(64), + _ => None, + } + } + + fn is_unsigned(ty: &ScalarType) -> bool { + matches!( + ty, + ScalarType::Bool + | ScalarType::U8 + | ScalarType::U16 + | ScalarType::U32 + | ScalarType::U64 + | ScalarType::Char + ) + } + + fn narrow(ty: &ScalarType) -> Option { + match ty { + ScalarType::I8 | ScalarType::U8 => Some(JI::I2b), + ScalarType::I16 => Some(JI::I2s), + ScalarType::U16 | ScalarType::Char => Some(JI::I2c), + _ => None, + } + } + + fn numbers_call( + cp: &mut InternedConstantPool, + name: &str, + descriptor: &str, + ) -> Result { + let class = cp.add_class("org/rustlang/runtime/Numbers")?; + let method = cp.add_method_ref(class, name, descriptor)?; + Ok(JI::Invokestatic(method)) + } + + // binary16 is stored as raw bits. Decode before a cast out, and round once on a cast in. + if src == &ScalarType::F16 { + let mut result = vec![numbers_call(cp, "f16ToF32", "(S)F")?]; + result.extend(primitive(&ScalarType::F32, dest, cp)?); + return Ok(result); + } + if dest == &ScalarType::F16 { + return match src { + ScalarType::F32 => Ok(vec![numbers_call(cp, "f32ToF16", "(F)S")?]), + ScalarType::F64 => Ok(vec![numbers_call(cp, "f64ToF16", "(D)S")?]), + _ if int_width(src).is_some() => { + let mut result = primitive(src, &ScalarType::F64, cp)?; + result.push(numbers_call(cp, "f64ToF16", "(D)S")?); + Ok(result) + } + _ => Err(jvm::Error::VerificationError { + context: "primitive_to_primitive".into(), + message: format!("No path {src:?}→F16"), + }), + }; + } + + if matches!(src, ScalarType::F32 | ScalarType::F64) + && matches!(dest, ScalarType::F32 | ScalarType::F64) + { + return Ok(match (src, dest) { + (ScalarType::F32, ScalarType::F64) => vec![JI::F2d], + (ScalarType::F64, ScalarType::F32) => vec![JI::D2f], + _ => Vec::new(), + }); + } + + if matches!(src, ScalarType::F32 | ScalarType::F64) && int_width(dest).is_some() { + let prefix = if src == &ScalarType::F32 { + "f32" + } else { + "f64" + }; + let source_descriptor = if src == &ScalarType::F32 { "F" } else { "D" }; + let (suffix, return_descriptor, direct) = match dest { + ScalarType::I8 => ("ToI8", "B", None), + ScalarType::I16 => ("ToI16", "S", None), + ScalarType::I32 => ( + "", + "", + Some(if src == &ScalarType::F32 { + JI::F2i + } else { + JI::D2i + }), + ), + ScalarType::I64 => ( + "", + "", + Some(if src == &ScalarType::F32 { + JI::F2l + } else { + JI::D2l + }), + ), + ScalarType::U8 | ScalarType::Bool => ("ToU8", "B", None), + ScalarType::U16 | ScalarType::Char => ("ToU16", "C", None), + ScalarType::U32 => ("ToU32", "I", None), + ScalarType::U64 => ("ToU64", "J", None), + _ => unreachable!(), + }; + if let Some(op) = direct { + return Ok(vec![op]); + } + return Ok(vec![numbers_call( + cp, + &format!("{prefix}{suffix}"), + &format!("({source_descriptor}){return_descriptor}"), + )?]); + } + + if int_width(src).is_some() && matches!(dest, ScalarType::F32 | ScalarType::F64) { + let to_f32 = dest == &ScalarType::F32; + return Ok(match src { + ScalarType::U32 => vec![numbers_call( + cp, + if to_f32 { "u32ToF32" } else { "u32ToF64" }, + if to_f32 { "(I)F" } else { "(I)D" }, + )?], + ScalarType::U64 => vec![numbers_call( + cp, + if to_f32 { "u64ToF32" } else { "u64ToF64" }, + if to_f32 { "(J)F" } else { "(J)D" }, + )?], + ScalarType::I64 => vec![if to_f32 { JI::L2f } else { JI::L2d }], + ScalarType::U8 => vec![ + get_int_const_instr(cp, 0xff), + JI::Iand, + if to_f32 { JI::I2f } else { JI::I2d }, + ], + _ => vec![if to_f32 { JI::I2f } else { JI::I2d }], + }); + } + + if let (Some(src_width), Some(dest_width)) = (int_width(src), int_width(dest)) { + let mut result = Vec::new(); + if dest_width <= 32 { + if src_width == 64 { + result.push(JI::L2i); + } + // The JVM sign-extends a byte local when it is loaded. Preserve the + // Rust u8 value before widening it to any larger integer type. + if src == &ScalarType::U8 && dest_width > 8 { + result.push(get_int_const_instr(cp, 0xff)); + result.push(JI::Iand); + } + if let Some(op) = narrow(dest) { + result.push(op); + } + return Ok(result); + } + + if src_width < 64 { + match src { + ScalarType::U8 => { + result.push(get_int_const_instr(cp, 0xff)); + result.push(JI::Iand); + result.push(JI::I2l); + } + ScalarType::U32 => { + result.push(JI::I2l); + result.push(get_long_const_instr(cp, 0xffff_ffff)); + result.push(JI::Land); + } + _ if is_unsigned(src) => result.push(JI::I2l), + _ => result.push(JI::I2l), + } + } + return Ok(result); + } + + Err(jvm::Error::VerificationError { + context: "primitive_to_primitive".into(), + message: format!("No path {src:?}→{dest:?}"), + }) +} diff --git a/compiler-core/src/jvm/constants.rs b/compiler-core/src/jvm/constants.rs new file mode 100644 index 0000000..25af37a --- /dev/null +++ b/compiler-core/src/jvm/constants.rs @@ -0,0 +1,99 @@ +use crate::classfile::{attributes::Instruction, constant_pool::InternedConstantPool}; + +fn immediate_int_const_instr(val: i32) -> Option { + match val { + -1 => Some(Instruction::Iconst_m1), + 0 => Some(Instruction::Iconst_0), + 1 => Some(Instruction::Iconst_1), + 2 => Some(Instruction::Iconst_2), + 3 => Some(Instruction::Iconst_3), + 4 => Some(Instruction::Iconst_4), + 5 => Some(Instruction::Iconst_5), + v @ -128..=-2 | v @ 6..=127 => Some(Instruction::Bipush(v as i8)), + v @ -32768..=-129 | v @ 128..=32767 => Some(Instruction::Sipush(v as i16)), + _ => None, + } +} + +pub fn get_int_const_instr(cp: &mut InternedConstantPool, val: i32) -> Instruction { + immediate_int_const_instr(val).unwrap_or_else(|| { + let index = cp + .add_integer(val) + .expect("Failed to add integer to constant pool"); + if let Ok(idx8) = u8::try_from(index) { + Instruction::Ldc(idx8) + } else { + Instruction::Ldc_w(index) + } + }) +} + +/// Appends an integer without consuming a constant-pool entry. Generated array +/// indices have high cardinality, so pooled indices can exhaust large classes. +pub fn append_unpooled_int_const(instructions: &mut Vec, val: i32) { + if let Some(instruction) = immediate_int_const_instr(val) { + instructions.push(instruction); + return; + } + + instructions.push(Instruction::Bipush((val >> 24) as i8)); + for shift in [16, 8, 0] { + instructions.push(Instruction::Bipush(8)); + instructions.push(Instruction::Ishl); + let byte = ((val >> shift) & 0xff) as i16; + instructions.push(if byte <= i16::from(i8::MAX) { + Instruction::Bipush(byte as i8) + } else { + Instruction::Sipush(byte) + }); + instructions.push(Instruction::Ior); + } +} + +pub fn get_long_const_instr(cp: &mut InternedConstantPool, val: i64) -> Instruction { + match val { + 0 => Instruction::Lconst_0, + 1 => Instruction::Lconst_1, + _ => { + // Add the long value to the constant pool. + let index = cp + .add_long(val) + .expect("Failed to add long to constant pool"); + // Ldc2_w is used for long/double constants and always takes a u16 index. + Instruction::Ldc2_w(index) + } + } +} + +pub fn get_float_const_instr(cp: &mut InternedConstantPool, val: f32) -> Instruction { + if val.to_bits() == 0.0f32.to_bits() { + Instruction::Fconst_0 + } else if val == 1.0 { + Instruction::Fconst_1 + } else if val == 2.0 { + Instruction::Fconst_2 + } else { + // Add the float value to the constant pool. + let index = cp + .add_float(val) + .expect("Failed to add float to constant pool"); + // Ldc2_w is used for long/double constants and always takes a u16 index. + Instruction::Ldc_w(index) + } +} + +pub fn get_double_const_instr(cp: &mut InternedConstantPool, val: f64) -> Instruction { + // Using bit representation for exact zero comparison is more robust + if val.to_bits() == 0.0f64.to_bits() { + Instruction::Dconst_0 + } else if val == 1.0 { + Instruction::Dconst_1 + } else { + // Add the double value to the constant pool. + let index = cp + .add_double(val) + .expect("Failed to add double to constant pool"); + // Ldc2_w is used for long/double constants and always takes a u16 index. + Instruction::Ldc2_w(index) + } +} diff --git a/compiler-core/src/jvm/encoding.rs b/compiler-core/src/jvm/encoding.rs new file mode 100644 index 0000000..bf6e085 --- /dev/null +++ b/compiler-core/src/jvm/encoding.rs @@ -0,0 +1,59 @@ +//! Exact encoded instruction sizes, including switch alignment. +use crate::classfile::{self as jvm, attributes::Instruction}; +use std::io::Cursor; + +pub fn instruction_byte_offsets(instructions: &[Instruction]) -> Result, jvm::Error> { + let mut offsets = Vec::with_capacity(instructions.len() + 1); + let mut byte_offset = 0usize; + let mut scratch = Cursor::new(Vec::with_capacity(16)); + for instruction in instructions { + offsets.push(byte_offset); + byte_offset += instruction_size_at(instruction, byte_offset, &mut scratch)?; + } + offsets.push(byte_offset); + Ok(offsets) +} + +pub fn instruction_size_at( + instruction: &Instruction, + byte_offset: usize, + scratch: &mut Cursor>, +) -> Result { + match instruction { + Instruction::Ifeq(_) + | Instruction::Ifne(_) + | Instruction::Iflt(_) + | Instruction::Ifge(_) + | Instruction::Ifgt(_) + | Instruction::Ifle(_) + | Instruction::If_icmpeq(_) + | Instruction::If_icmpne(_) + | Instruction::If_icmplt(_) + | Instruction::If_icmpge(_) + | Instruction::If_icmpgt(_) + | Instruction::If_icmple(_) + | Instruction::If_acmpeq(_) + | Instruction::If_acmpne(_) + | Instruction::Goto(_) + | Instruction::Jsr(_) + | Instruction::Ifnull(_) + | Instruction::Ifnonnull(_) => Ok(3), + Instruction::Goto_w(_) | Instruction::Jsr_w(_) => Ok(5), + Instruction::Tableswitch(table_switch) => { + let position_after_opcode = byte_offset + 1; + let padding = (4 - (position_after_opcode % 4)) % 4; + Ok(1 + padding + 12 + table_switch.offsets.len() * 4) + } + Instruction::Lookupswitch(lookup_switch) => { + let position_after_opcode = byte_offset + 1; + let padding = (4 - (position_after_opcode % 4)) % 4; + Ok(1 + padding + 8 + lookup_switch.pairs.len() * 8) + } + _ => { + scratch.get_mut().clear(); + scratch.set_position(0); + instruction.to_bytes(scratch)?; + Ok(scratch.get_ref().len()) + } + } +} diff --git a/compiler-core/src/jvm/flow.rs b/compiler-core/src/jvm/flow.rs new file mode 100644 index 0000000..fd4ce09 --- /dev/null +++ b/compiler-core/src/jvm/flow.rs @@ -0,0 +1,109 @@ +//! Symbolic JVM control flow. Branches use instruction indices; switch offsets are relative. +use crate::classfile::attributes::Instruction; + +macro_rules! conditional_forms { + ($($a:ident, $b:ident);* $(;)?) => { + pub fn conditional_branch_target(instruction: &Instruction) -> Option { + match instruction { $(Instruction::$a(target) | Instruction::$b(target) => Some(*target),)* _ => None } + } + pub fn set_conditional_branch_target(instruction: &Instruction, target: u16) -> Option { + Some(match instruction { $(Instruction::$a(_) => Instruction::$a(target), Instruction::$b(_) => Instruction::$b(target),)* _ => return None }) + } + pub fn invert_conditional_branch(instruction: &Instruction, target: u16) -> Option { + Some(match instruction { $(Instruction::$a(_) => Instruction::$b(target), Instruction::$b(_) => Instruction::$a(target),)* _ => return None }) + } + }; +} +conditional_forms! { + Ifeq, Ifne; Iflt, Ifge; Ifgt, Ifle; + If_icmpeq, If_icmpne; If_icmplt, If_icmpge; If_icmpgt, If_icmple; + If_acmpeq, If_acmpne; Ifnull, Ifnonnull; +} + +pub fn instruction_can_fall_through(instruction: &Instruction) -> bool { + !matches!( + instruction, + Instruction::Goto(_) + | Instruction::Goto_w(_) + | Instruction::Jsr(_) + | Instruction::Jsr_w(_) + | Instruction::Ret(_) + | Instruction::Ret_w(_) + | Instruction::Tableswitch(_) + | Instruction::Lookupswitch(_) + | Instruction::Ireturn + | Instruction::Lreturn + | Instruction::Freturn + | Instruction::Dreturn + | Instruction::Areturn + | Instruction::Return + | Instruction::Athrow + ) +} + +pub fn visit_instruction_successors( + index: usize, + instruction: &Instruction, + instruction_count: usize, + mut visitor: impl FnMut(usize), +) { + visit_branch_targets(index, instruction, |target| { + if let Ok(target) = usize::try_from(target) { + visitor(target); + } + }); + if instruction_can_fall_through(instruction) && index + 1 < instruction_count { + visitor(index + 1); + } +} + +pub fn instruction_successors( + index: usize, + instruction: &Instruction, + instruction_count: usize, +) -> Vec { + let mut result = Vec::new(); + visit_instruction_successors(index, instruction, instruction_count, |target| { + result.push(target) + }); + result +} + +pub fn visit_branch_targets(index: usize, instruction: &Instruction, mut visitor: impl FnMut(i64)) { + use Instruction as I; + + match instruction { + I::Ifeq(target) + | I::Ifne(target) + | I::Iflt(target) + | I::Ifge(target) + | I::Ifgt(target) + | I::Ifle(target) + | I::If_icmpeq(target) + | I::If_icmpne(target) + | I::If_icmplt(target) + | I::If_icmpge(target) + | I::If_icmpgt(target) + | I::If_icmple(target) + | I::If_acmpeq(target) + | I::If_acmpne(target) + | I::Goto(target) + | I::Jsr(target) + | I::Ifnull(target) + | I::Ifnonnull(target) => visitor(i64::from(*target)), + I::Goto_w(target) | I::Jsr_w(target) => visitor(i64::from(*target)), + I::Tableswitch(table_switch) => { + visitor(index as i64 + i64::from(table_switch.default)); + for target in &table_switch.offsets { + visitor(index as i64 + i64::from(*target)); + } + } + I::Lookupswitch(lookup_switch) => { + visitor(index as i64 + i64::from(lookup_switch.default)); + for target in lookup_switch.pairs.values() { + visitor(index as i64 + i64::from(*target)); + } + } + _ => {} + } +} diff --git a/compiler-core/src/jvm/frames/analysis.rs b/compiler-core/src/jvm/frames/analysis.rs new file mode 100644 index 0000000..9368bc1 --- /dev/null +++ b/compiler-core/src/jvm/frames/analysis.rs @@ -0,0 +1,289 @@ +use super::*; + +pub fn analyze( + instructions: &[Instruction], + initial_locals: &[FrameValue], + local_hints: &[FrameValue], + max_locals: usize, + constant_pool: &ConstantPool, + context: &str, + exception_table: &[ExceptionTableEntry], +) -> jvm::Result { + if instructions.is_empty() { + return Ok(FrameAnalysis { + max_stack: 0, + block_starts: Vec::new(), + entry_states: Vec::new(), + }); + } + + let (block_starts, block_ends, block_by_instruction) = + frame_blocks(instructions, exception_table); + let mut handlers_by_block = vec![Vec::new(); block_starts.len()]; + for handler in exception_table { + let start = usize::from(handler.range_pc.start).min(instructions.len()); + let end = usize::from(handler.range_pc.end).min(instructions.len()); + if start >= end { + continue; + } + let first = block_by_instruction[start]; + let last = block_by_instruction[end - 1]; + for handlers in &mut handlers_by_block[first..=last] { + let target = usize::from(handler.handler_pc); + if !handlers.contains(&target) { + handlers.push(target); + } + } + } + + let mut max_stack = 0; + let mut signatures = SignatureCache::default(); + let mut entry_states = vec![None; block_starts.len()]; + entry_states[0] = Some(FrameState::new(initial_locals.to_vec(), max_locals)); + let mut worklist = VecDeque::from([0usize]); + let mut queued = vec![false; block_starts.len()]; + queued[0] = true; + while let Some(block) = worklist.pop_front() { + queued[block] = false; + let Some(mut state) = entry_states[block].clone() else { + continue; + }; + + max_stack = max_stack.max(state.stack_words); + let mut last_handler_locals = HashMap::>>::default(); + for index in block_starts[block]..block_ends[block] { + for &target in &handlers_by_block[block] { + let unchanged = last_handler_locals.get(&target).is_some_and(|locals| { + Arc::ptr_eq(locals, &state.locals) || **locals == *state.locals + }); + if unchanged || target >= instructions.len() { + continue; + } + last_handler_locals.insert(target, Arc::clone(&state.locals)); + let mut handler_state = state.clone(); + handler_state.stack.clear(); + handler_state.stack_words = 0; + handler_state.push(FrameValue::Object("java/lang/Throwable".into())); + merge_block_entry( + target, + handler_state, + &block_starts, + &block_by_instruction, + &mut entry_states, + &mut worklist, + &mut queued, + context, + )?; + } + + transfer_instruction( + index, + &instructions[index], + &mut state, + local_hints, + constant_pool, + context, + &mut signatures, + ) + .map_err(|error| jvm::Error::VerificationError { + context: context.to_string(), + message: format!( + "Stack-map transfer failed at instruction {index} ({}): {error:?}\nInstruction window:\n{}", + describe_instruction(&instructions[index], constant_pool), + instruction_window(instructions, index, constant_pool), + ), + })?; + + max_stack = max_stack.max(state.stack_words); + if index + 1 == block_ends[block] { + for target in assignment_successors(index, &instructions[index], context)? { + if target >= instructions.len() { + continue; + } + merge_block_entry( + target, + state.clone(), + &block_starts, + &block_by_instruction, + &mut entry_states, + &mut worklist, + &mut queued, + context, + )?; + } + } + } + } + + Ok(FrameAnalysis { + max_stack: u16::try_from(max_stack)?, + block_starts, + entry_states, + }) +} + +pub(super) fn frame_blocks( + instructions: &[Instruction], + exception_table: &[ExceptionTableEntry], +) -> (Vec, Vec, Vec) { + let mut starts = BTreeSet::from([0usize]); + for target in branch_targets(instructions) { + if usize::from(target) < instructions.len() { + starts.insert(usize::from(target)); + } + } + for entry in exception_table { + for boundary in [entry.range_pc.start, entry.range_pc.end, entry.handler_pc] { + if usize::from(boundary) < instructions.len() { + starts.insert(usize::from(boundary)); + } + } + } + for (index, instruction) in instructions.iter().enumerate() { + if instruction_ends_block(instruction) && index + 1 < instructions.len() { + starts.insert(index + 1); + } + } + + let block_starts = starts.into_iter().collect::>(); + let block_ends = block_starts + .iter() + .copied() + .skip(1) + .chain(std::iter::once(instructions.len())) + .collect::>(); + let mut block_by_instruction = vec![0usize; instructions.len()]; + for (block, (&start, &end)) in block_starts.iter().zip(&block_ends).enumerate() { + block_by_instruction[start..end].fill(block); + } + (block_starts, block_ends, block_by_instruction) +} + +pub(super) fn instruction_ends_block(instruction: &Instruction) -> bool { + crate::jvm::flow::conditional_branch_target(instruction).is_some() + || !crate::jvm::flow::instruction_can_fall_through(instruction) +} + +pub(super) fn merge_block_entry( + target: usize, + incoming: FrameState, + block_starts: &[usize], + block_by_instruction: &[usize], + entry_states: &mut [Option], + worklist: &mut VecDeque, + queued: &mut [bool], + context: &str, +) -> jvm::Result<()> { + let Some(&block) = block_by_instruction.get(target) else { + return Ok(()); + }; + if block_starts[block] != target { + return Err(jvm::Error::VerificationError { + context: context.to_string(), + message: format!("Control flow targets the middle of bytecode block at {target}"), + }); + } + let changed = match &mut entry_states[block] { + Some(existing) => merge_state(existing, &incoming), + slot @ None => { + *slot = Some(incoming); + true + } + }; + if changed && !queued[block] { + queued[block] = true; + worklist.push_back(block); + } + Ok(()) +} + +pub(super) fn merge_state(existing: &mut FrameState, incoming: &FrameState) -> bool { + let mut changed = false; + + let local_len = existing.locals.len().max(incoming.locals.len()); + let locals = Arc::make_mut(&mut existing.locals); + locals.resize(local_len, FrameValue::Top); + for index in 0..local_len { + let incoming_value = incoming.locals.get(index).unwrap_or(&FrameValue::Top); + let merged = merge_value(&locals[index], incoming_value); + if locals[index] != merged { + locals[index] = merged; + changed = true; + } + } + + if existing.stack.len() != incoming.stack.len() { + let merged_len = existing.stack.len().min(incoming.stack.len()); + existing.stack.truncate(merged_len); + changed = true; + } + for (existing_value, incoming_value) in existing.stack.iter_mut().zip(&incoming.stack) { + let merged = merge_value(existing_value, incoming_value); + if *existing_value != merged { + *existing_value = merged; + changed = true; + } + } + + existing.stack_words = existing + .stack + .iter() + .map(|v| if v.is_category2() { 2 } else { 1 }) + .sum(); + changed +} + +pub(super) fn merge_value(a: &FrameValue, b: &FrameValue) -> FrameValue { + if a == b { + return a.clone(); + } + match (a, b) { + (FrameValue::Top, _) | (_, FrameValue::Top) => FrameValue::Top, + (FrameValue::Null, FrameValue::Object(class_name)) + | (FrameValue::Object(class_name), FrameValue::Null) => { + FrameValue::Object(class_name.clone()) + } + (FrameValue::Null, FrameValue::Null) => FrameValue::Null, + (FrameValue::Object(a_class), FrameValue::Object(b_class)) => { + FrameValue::Object(common_object_class(a_class, b_class).into()) + } + _ => FrameValue::Top, + } +} + +pub(super) fn common_object_class(a: &str, b: &str) -> String { + if a == b { + return a.to_string(); + } + if a == "java/lang/Object" || b == "java/lang/Object" { + return "java/lang/Object".to_string(); + } + if let Some(parent) = nested_parent_class(a) { + if parent == b { + return b.to_string(); + } + } + if let Some(parent) = nested_parent_class(b) { + if parent == a { + return a.to_string(); + } + } + if let (Some(a_parent), Some(b_parent)) = (nested_parent_class(a), nested_parent_class(b)) { + if a_parent == b_parent { + return a_parent.to_string(); + } + } + "java/lang/Object".to_string() +} + +pub(super) fn nested_parent_class(class_name: &str) -> Option<&str> { + if class_name.starts_with('[') { + return None; + } + let (parent, _) = class_name.rsplit_once('$')?; + if parent.is_empty() { + None + } else { + Some(parent) + } +} diff --git a/compiler-core/src/jvm/frames/descriptors.rs b/compiler-core/src/jvm/frames/descriptors.rs new file mode 100644 index 0000000..d90107a --- /dev/null +++ b/compiler-core/src/jvm/frames/descriptors.rs @@ -0,0 +1,175 @@ +use super::*; + +#[derive(Default)] +pub(super) struct SignatureCache { + methods: HashMap, + fields: HashMap, +} + +pub(super) struct MethodTransfer { + pub params: usize, + pub result: Option, + pub constructor: Option>, +} + +impl SignatureCache { + pub fn method(&mut self, cp: &ConstantPool, index: u16) -> jvm::Result<&MethodTransfer> { + use std::collections::hash_map::Entry; + Ok(match self.methods.entry(index) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let method = if matches!(cp.try_get(index)?, Constant::InvokeDynamic { .. }) { + invoke_dynamic_info(cp, index)? + } else { + static_method_ref_info(cp, index)? + }; + let descriptor = jvm::JavaString::from(method.descriptor.as_str()); + let (params, result) = FieldType::parse_method_descriptor(&descriptor)?; + entry.insert(MethodTransfer { + params: params.len(), + result: result.as_ref().map(frame_value_from_field_type), + constructor: (method.method_name == "").then(|| method.class_name.into()), + }) + } + }) + } + + pub fn field(&mut self, cp: &ConstantPool, index: u16) -> jvm::Result { + use std::collections::hash_map::Entry; + Ok(match self.fields.entry(index) { + Entry::Occupied(entry) => entry.get().clone(), + Entry::Vacant(entry) => entry + .insert(frame_value_from_field_type(&field_type_for_ref(cp, index)?)) + .clone(), + }) + } +} + +pub(super) struct MethodRefInfo { + pub(super) class_name: String, + pub(super) method_name: String, + pub(super) descriptor: String, +} + +pub(super) fn frame_value_from_field_type(field_type: &FieldType) -> FrameValue { + match field_type { + FieldType::Base(BaseType::Long) => FrameValue::Long, + FieldType::Base(BaseType::Float) => FrameValue::Float, + FieldType::Base(BaseType::Double) => FrameValue::Double, + FieldType::Base(_) => FrameValue::Integer, + FieldType::Object(class_name) => { + FrameValue::Object(normalize_class_name(&class_name.to_string()).into()) + } + FieldType::Array(_) => FrameValue::Object(field_type.class_name().into()), + } +} + +pub(super) fn field_type_for_ref( + constant_pool: &ConstantPool, + field_ref: u16, +) -> jvm::Result { + let (_, name_and_type_index) = constant_pool.try_get_field_ref(field_ref)?; + let (_, descriptor_index) = constant_pool.try_get_name_and_type(*name_and_type_index)?; + let descriptor = constant_pool.try_get_utf8(*descriptor_index)?; + Ok(FieldType::parse(&descriptor.to_string())?) +} + +pub(super) fn method_ref_info( + constant_pool: &ConstantPool, + method_ref: u16, + is_interface: bool, +) -> jvm::Result { + let (class_index, name_and_type_index) = if is_interface { + constant_pool.try_get_interface_method_ref(method_ref)? + } else { + constant_pool.try_get_method_ref(method_ref)? + }; + let class_name = constant_pool.try_get_class(*class_index)?.to_string(); + let (name_index, descriptor_index) = + constant_pool.try_get_name_and_type(*name_and_type_index)?; + let method_name = constant_pool.try_get_utf8(*name_index)?.to_string(); + let descriptor = constant_pool.try_get_utf8(*descriptor_index)?.to_string(); + Ok(MethodRefInfo { + class_name, + method_name, + descriptor, + }) +} + +pub(super) fn static_method_ref_info( + constant_pool: &ConstantPool, + method_ref: u16, +) -> jvm::Result { + let is_interface = matches!( + constant_pool.try_get(method_ref)?, + Constant::InterfaceMethodRef { .. } + ); + method_ref_info(constant_pool, method_ref, is_interface) +} + +pub(super) fn invoke_dynamic_info( + constant_pool: &ConstantPool, + invoke_dynamic_ref: u16, +) -> jvm::Result { + let Constant::InvokeDynamic { + name_and_type_index, + .. + } = constant_pool.try_get(invoke_dynamic_ref)? + else { + return Err(jvm::Error::VerificationError { + context: "invokedynamic stack-map transfer".to_string(), + message: format!( + "constant-pool entry #{invoke_dynamic_ref} is not an InvokeDynamic constant" + ), + }); + }; + let (name_index, descriptor_index) = + constant_pool.try_get_name_and_type(*name_and_type_index)?; + Ok(MethodRefInfo { + class_name: "java/lang/Object".to_string(), + method_name: constant_pool.try_get_utf8(*name_index)?.to_string(), + descriptor: constant_pool.try_get_utf8(*descriptor_index)?.to_string(), + }) +} + +pub(super) fn describe_instruction( + instruction: &Instruction, + constant_pool: &ConstantPool, +) -> String { + let method = match instruction { + Instruction::Invokevirtual(index) | Instruction::Invokespecial(index) => { + method_ref_info(constant_pool, *index, false).ok() + } + Instruction::Invokestatic(index) => static_method_ref_info(constant_pool, *index).ok(), + Instruction::Invokeinterface(index, _) => method_ref_info(constant_pool, *index, true).ok(), + _ => None, + }; + method.map_or_else( + || format!("{instruction:?}"), + |method| { + format!( + "{instruction:?} => {}.{}{}", + method.class_name, method.method_name, method.descriptor + ) + }, + ) +} + +pub(super) fn instruction_window( + instructions: &[Instruction], + center: usize, + constant_pool: &ConstantPool, +) -> String { + let start = center.saturating_sub(8); + let end = (center + 4).min(instructions.len().saturating_sub(1)); + (start..=end) + .map(|index| { + let marker = if index == center { ">" } else { " " }; + format!( + "{marker} {index}: {}", + describe_instruction(&instructions[index], constant_pool) + ) + }) + .collect::>() + .join("\n") +} diff --git a/compiler-core/src/jvm/frames/encoding.rs b/compiler-core/src/jvm/frames/encoding.rs new file mode 100644 index 0000000..9974e97 --- /dev/null +++ b/compiler-core/src/jvm/frames/encoding.rs @@ -0,0 +1,196 @@ +use super::*; + +pub fn build_stack_map_attributes( + instructions: &[Instruction], + initial_locals: &[FrameValue], + local_hints: &[FrameValue], + max_locals: u16, + constant_pool: &mut InternedConstantPool, + context: &str, + exception_table: &[ExceptionTableEntry], +) -> jvm::Result> { + let analysis = analyze( + instructions, + initial_locals, + local_hints, + max_locals as usize, + constant_pool, + context, + exception_table, + )?; + build_stack_map_attributes_from_analysis( + instructions, + initial_locals, + constant_pool, + exception_table, + &analysis, + ) +} + +pub fn build_stack_map_attributes_from_analysis( + instructions: &[Instruction], + initial_locals: &[FrameValue], + constant_pool: &mut InternedConstantPool, + exception_table: &[ExceptionTableEntry], + analysis: &FrameAnalysis, +) -> jvm::Result> { + let mut target_offsets = branch_targets(instructions); + target_offsets.extend(exception_table.iter().map(|entry| entry.handler_pc)); + if instructions.is_empty() || target_offsets.is_empty() { + return Ok(Vec::new()); + } + + let name_index = constant_pool.add_utf8("StackMapTable")?; + let mut previous_instruction_offset: Option = None; + let mut frames = Vec::new(); + let mut verification_class_cache = HashMap::default(); + let mut previous_locals = + locals_for_stack_map(initial_locals, constant_pool, &mut verification_class_cache)?; + for target in target_offsets { + if target == 0 { + continue; + } + let Some(state) = analysis.state_at(target as usize) else { + continue; + }; + let instruction_delta = match previous_instruction_offset { + Some(previous) => target.saturating_sub(previous).saturating_sub(1), + None => target, + }; + let locals = + locals_for_stack_map(&state.locals, constant_pool, &mut verification_class_cache)?; + let stack = + stack_for_stack_map(&state.stack, constant_pool, &mut verification_class_cache)?; + frames.push(compact_stack_frame( + instruction_delta, + &previous_locals, + &locals, + stack, + )); + previous_locals = locals; + previous_instruction_offset = Some(target); + } + + if frames.is_empty() { + Ok(Vec::new()) + } else { + Ok(vec![Attribute::StackMapTable { name_index, frames }]) + } +} + +pub(super) fn compact_stack_frame( + offset_delta: u16, + previous_locals: &[VerificationType], + locals: &[VerificationType], + stack: Vec, +) -> StackFrame { + if locals == previous_locals { + return match stack.len() { + 0 => StackFrame::SameFrameExtended { + frame_type: 251, + offset_delta, + }, + 1 => StackFrame::SameLocals1StackItemFrameExtended { + frame_type: 247, + offset_delta, + stack, + }, + _ => StackFrame::FullFrame { + frame_type: 255, + offset_delta, + locals: locals.to_vec(), + stack, + }, + }; + } + + if stack.is_empty() && locals.starts_with(previous_locals) { + let appended = &locals[previous_locals.len()..]; + if (1..=3).contains(&appended.len()) { + return StackFrame::AppendFrame { + frame_type: 251 + appended.len() as u8, + offset_delta, + locals: appended.to_vec(), + }; + } + } + if stack.is_empty() && previous_locals.starts_with(locals) { + let removed = previous_locals.len() - locals.len(); + if (1..=3).contains(&removed) { + return StackFrame::ChopFrame { + frame_type: 251 - removed as u8, + offset_delta, + }; + } + } + + StackFrame::FullFrame { + frame_type: 255, + offset_delta, + locals: locals.to_vec(), + stack, + } +} + +pub(super) fn locals_for_stack_map( + locals: &[FrameValue], + constant_pool: &mut InternedConstantPool, + verification_class_cache: &mut HashMap, +) -> jvm::Result> { + let mut end = locals.len(); + while end > 0 && locals[end - 1] == FrameValue::Top { + end -= 1; + } + + let mut result = Vec::new(); + let mut index = 0; + while index < end { + let value = &locals[index]; + result.push(to_verification_type( + value, + constant_pool, + verification_class_cache, + )?); + index += if value.is_category2() { 2 } else { 1 }; + } + Ok(result) +} + +pub(super) fn stack_for_stack_map( + stack: &[FrameValue], + constant_pool: &mut InternedConstantPool, + verification_class_cache: &mut HashMap, +) -> jvm::Result> { + stack + .iter() + .map(|value| to_verification_type(value, constant_pool, verification_class_cache)) + .collect() +} + +pub(super) fn to_verification_type( + value: &FrameValue, + constant_pool: &mut InternedConstantPool, + verification_class_cache: &mut HashMap, +) -> jvm::Result { + Ok(match value { + FrameValue::Top => VerificationType::Top, + FrameValue::Integer => VerificationType::Integer, + FrameValue::Float => VerificationType::Float, + FrameValue::Long => VerificationType::Long, + FrameValue::Double => VerificationType::Double, + FrameValue::Null => VerificationType::Null, + FrameValue::Object(class_name) => { + let cpool_index = match verification_class_cache.get(class_name.as_ref()) { + Some(cpool_index) => *cpool_index, + None => { + let cpool_index = constant_pool.add_class(class_name)?; + verification_class_cache.insert(class_name.to_string(), cpool_index); + cpool_index + } + }; + VerificationType::Object { cpool_index } + } + FrameValue::UninitializedThis => VerificationType::UninitializedThis, + FrameValue::Uninitialized(offset) => VerificationType::Uninitialized { offset: *offset }, + }) +} diff --git a/compiler-core/src/jvm/frames/flow.rs b/compiler-core/src/jvm/frames/flow.rs new file mode 100644 index 0000000..987e179 --- /dev/null +++ b/compiler-core/src/jvm/frames/flow.rs @@ -0,0 +1,34 @@ +use super::*; +use crate::jvm::flow::{visit_branch_targets, visit_instruction_successors}; + +pub(super) fn assignment_successors( + index: usize, + instruction: &Instruction, + context: &str, +) -> jvm::Result> { + let mut invalid = false; + visit_branch_targets(index, instruction, |target| invalid |= target < 0); + if invalid { + return Err(jvm::Error::VerificationError { + context: context.into(), + message: format!("Negative branch target at instruction {index}"), + }); + } + let mut successors = Vec::new(); + visit_instruction_successors(index, instruction, usize::MAX, |target| { + successors.push(target) + }); + Ok(successors) +} + +pub(super) fn branch_targets(instructions: &[Instruction]) -> BTreeSet { + let mut targets = BTreeSet::new(); + for (index, instruction) in instructions.iter().enumerate() { + visit_branch_targets(index, instruction, |target| { + if let Ok(target) = u16::try_from(target) { + targets.insert(target); + } + }); + } + targets +} diff --git a/compiler-core/src/jvm/frames/locals.rs b/compiler-core/src/jvm/frames/locals.rs new file mode 100644 index 0000000..42a8e6c --- /dev/null +++ b/compiler-core/src/jvm/frames/locals.rs @@ -0,0 +1,488 @@ +use super::*; + +pub fn initial_locals_for_descriptor( + descriptor: &str, + is_static: bool, + this_class_name: Option<&str>, + is_constructor: bool, +) -> jvm::Result> { + let mut locals = Vec::new(); + if !is_static { + let this_value = if is_constructor { + FrameValue::UninitializedThis + } else { + FrameValue::Object( + normalize_class_name(this_class_name.unwrap_or("java/lang/Object")).into(), + ) + }; + push_local_value(&mut locals, this_value); + } + + let descriptor = jvm::JavaString::from(descriptor); + let (params, _) = FieldType::parse_method_descriptor(&descriptor)?; + for param in ¶ms { + push_local_value(&mut locals, frame_value_from_field_type(param)); + } + Ok(locals) +} + +/// Give verifier-visible defaults to control-flow-guarded locals, especially +/// drop values. Rust never observes them, but the JVM cannot correlate a drop +/// flag with its guarded load and requires a value on every incoming path. +pub fn initialize_locals_loaded_as_top( + instructions: &mut Vec, + initial_locals: &[FrameValue], + local_hints: &[FrameValue], + max_locals: u16, + constant_pool: &ConstantPool, + context: &str, + exception_table: &mut [ExceptionTableEntry], +) -> jvm::Result<(usize, FrameAnalysis)> { + if instructions.is_empty() { + return Ok(( + 0, + FrameAnalysis { + max_stack: 0, + block_starts: Vec::new(), + entry_states: Vec::new(), + }, + )); + } + + let locals = locals_loaded_before_definite_store( + instructions, + initial_locals, + max_locals as usize, + context, + exception_table, + )?; + if locals.is_empty() { + let analysis = analyze( + instructions, + initial_locals, + local_hints, + max_locals as usize, + constant_pool, + context, + exception_table, + )?; + return Ok((0, analysis)); + } + + let mut prefix = Vec::with_capacity(locals.len() * 2); + for (local, value) in locals { + prefix.extend(default_local_initializer(local, &value)); + } + let prefix_len = u16::try_from(prefix.len()).map_err(|_| jvm::Error::VerificationError { + context: context.to_string(), + message: "Verifier local-initialization prefix exceeds the JVM instruction limit" + .to_string(), + })?; + shift_absolute_branch_targets(instructions, prefix_len, context)?; + instructions.splice(0..0, prefix); + shift_exception_table(exception_table, prefix_len, context)?; + + let remaining = locals_loaded_before_definite_store( + instructions, + initial_locals, + max_locals as usize, + context, + exception_table, + )?; + if !remaining.is_empty() { + return Err(jvm::Error::VerificationError { + context: context.to_string(), + message: format!( + "Local initialization could not resolve verifier Top loads: {remaining:?}" + ), + }); + } + + let analysis = analyze( + instructions, + initial_locals, + local_hints, + max_locals as usize, + constant_pool, + context, + exception_table, + )?; + + Ok((usize::from(prefix_len), analysis)) +} + +pub(super) fn shift_exception_table( + exception_table: &mut [ExceptionTableEntry], + amount: u16, + context: &str, +) -> jvm::Result<()> { + for entry in exception_table { + entry.range_pc.start = entry.range_pc.start.checked_add(amount).ok_or_else(|| { + jvm::Error::VerificationError { + context: context.to_string(), + message: "exception range start overflowed while inserting a prefix".to_string(), + } + })?; + entry.range_pc.end = entry.range_pc.end.checked_add(amount).ok_or_else(|| { + jvm::Error::VerificationError { + context: context.to_string(), + message: "exception range end overflowed while inserting a prefix".to_string(), + } + })?; + entry.handler_pc = + entry + .handler_pc + .checked_add(amount) + .ok_or_else(|| jvm::Error::VerificationError { + context: context.to_string(), + message: "exception handler overflowed while inserting a prefix".to_string(), + })?; + } + Ok(()) +} + +pub(super) fn loaded_local(instruction: &Instruction) -> Option<(u16, FrameValue)> { + use Instruction as I; + + let (local, value) = match instruction { + I::Iload(local) => (u16::from(*local), FrameValue::Integer), + I::Lload(local) => (u16::from(*local), FrameValue::Long), + I::Fload(local) => (u16::from(*local), FrameValue::Float), + I::Dload(local) => (u16::from(*local), FrameValue::Double), + I::Aload(local) => ( + u16::from(*local), + FrameValue::Object("java/lang/Object".into()), + ), + I::Iload_0 => (0, FrameValue::Integer), + I::Iload_1 => (1, FrameValue::Integer), + I::Iload_2 => (2, FrameValue::Integer), + I::Iload_3 => (3, FrameValue::Integer), + I::Lload_0 => (0, FrameValue::Long), + I::Lload_1 => (1, FrameValue::Long), + I::Lload_2 => (2, FrameValue::Long), + I::Lload_3 => (3, FrameValue::Long), + I::Fload_0 => (0, FrameValue::Float), + I::Fload_1 => (1, FrameValue::Float), + I::Fload_2 => (2, FrameValue::Float), + I::Fload_3 => (3, FrameValue::Float), + I::Dload_0 => (0, FrameValue::Double), + I::Dload_1 => (1, FrameValue::Double), + I::Dload_2 => (2, FrameValue::Double), + I::Dload_3 => (3, FrameValue::Double), + I::Aload_0 => (0, FrameValue::Object("java/lang/Object".into())), + I::Aload_1 => (1, FrameValue::Object("java/lang/Object".into())), + I::Aload_2 => (2, FrameValue::Object("java/lang/Object".into())), + I::Aload_3 => (3, FrameValue::Object("java/lang/Object".into())), + I::Iload_w(local) | I::Iinc_w(local, _) => (*local, FrameValue::Integer), + I::Lload_w(local) => (*local, FrameValue::Long), + I::Fload_w(local) => (*local, FrameValue::Float), + I::Dload_w(local) => (*local, FrameValue::Double), + I::Aload_w(local) => (*local, FrameValue::Object("java/lang/Object".into())), + I::Iinc(local, _) => (u16::from(*local), FrameValue::Integer), + _ => return None, + }; + Some((local, value)) +} + +pub(super) fn stored_local(instruction: &Instruction) -> Option { + use Instruction as I; + + Some(match instruction { + I::Istore(local) + | I::Lstore(local) + | I::Fstore(local) + | I::Dstore(local) + | I::Astore(local) => u16::from(*local), + I::Istore_0 | I::Lstore_0 | I::Fstore_0 | I::Dstore_0 | I::Astore_0 => 0, + I::Istore_1 | I::Lstore_1 | I::Fstore_1 | I::Dstore_1 | I::Astore_1 => 1, + I::Istore_2 | I::Lstore_2 | I::Fstore_2 | I::Dstore_2 | I::Astore_2 => 2, + I::Istore_3 | I::Lstore_3 | I::Fstore_3 | I::Dstore_3 | I::Astore_3 => 3, + I::Istore_w(local) + | I::Lstore_w(local) + | I::Fstore_w(local) + | I::Dstore_w(local) + | I::Astore_w(local) + | I::Iinc_w(local, _) => *local, + I::Iinc(local, _) => u16::from(*local), + _ => return None, + }) +} + +/// Find locals whose loads are not preceded by a store on every incoming path. +/// This only needs definite-assignment bits; exact verifier types and operand +/// stacks are left to the single typed analysis performed after prefixing. +pub(super) fn locals_loaded_before_definite_store( + instructions: &[Instruction], + initial_locals: &[FrameValue], + max_locals: usize, + context: &str, + exception_table: &[ExceptionTableEntry], +) -> jvm::Result> { + if instructions.is_empty() { + return Ok(BTreeMap::new()); + } + let (block_starts, block_ends, block_by_instruction) = + frame_blocks(instructions, exception_table); + let word_count = max_locals.div_ceil(u64::BITS as usize); + let mut initial = vec![0u64; word_count]; + for (slot, value) in initial_locals.iter().enumerate().take(max_locals) { + if *value != FrameValue::Top { + assignment_insert(&mut initial, slot); + } + } + + let mut handlers_by_instruction = vec![Vec::new(); instructions.len()]; + for handler in exception_table { + let start = usize::from(handler.range_pc.start).min(instructions.len()); + let end = usize::from(handler.range_pc.end).min(instructions.len()); + for handlers in &mut handlers_by_instruction[start..end] { + let target = usize::from(handler.handler_pc); + if !handlers.contains(&target) { + handlers.push(target); + } + } + } + + let mut entries = vec![None; block_starts.len()]; + let mut block_loads = vec![BTreeMap::new(); block_starts.len()]; + entries[0] = Some(initial); + let mut worklist = VecDeque::from([0usize]); + let mut queued = vec![false; block_starts.len()]; + queued[0] = true; + while let Some(block) = worklist.pop_front() { + queued[block] = false; + let Some(mut assigned) = entries[block].clone() else { + continue; + }; + let mut loads = BTreeMap::new(); + let mut last_handler_assignments = HashMap::>::default(); + for index in block_starts[block]..block_ends[block] { + for &target in &handlers_by_instruction[index] { + if last_handler_assignments.get(&target) == Some(&assigned) { + continue; + } + last_handler_assignments.insert(target, assigned.clone()); + merge_assignment_entry( + target, + &assigned, + &block_starts, + &block_by_instruction, + &mut entries, + &mut worklist, + &mut queued, + context, + )?; + } + if let Some((local, value)) = loaded_local(&instructions[index]) + && !assignment_contains(&assigned, usize::from(local)) + && let Some(existing) = loads.insert(local, value.clone()) + && existing != value + { + return Err(jvm::Error::VerificationError { + context: context.to_string(), + message: format!( + "Local {local} is loaded with incompatible types {existing:?} and {value:?}; latest load is instruction {index}" + ), + }); + } + if let Some(local) = stored_local(&instructions[index]) { + assignment_insert(&mut assigned, usize::from(local)); + } + } + let last = block_ends[block] - 1; + for target in assignment_successors(last, &instructions[last], context)? { + if target < instructions.len() { + merge_assignment_entry( + target, + &assigned, + &block_starts, + &block_by_instruction, + &mut entries, + &mut worklist, + &mut queued, + context, + )?; + } + } + block_loads[block] = loads; + } + + let mut loads = BTreeMap::new(); + for local_loads in block_loads { + for (local, value) in local_loads { + if let Some(existing) = loads.insert(local, value.clone()) + && existing != value + { + return Err(jvm::Error::VerificationError { + context: context.to_string(), + message: format!( + "Local {local} is loaded with incompatible types {existing:?} and {value:?}" + ), + }); + } + } + } + Ok(loads) +} + +pub(super) fn assignment_contains(assignments: &[u64], local: usize) -> bool { + assignments + .get(local / u64::BITS as usize) + .is_some_and(|word| word & (1 << (local % u64::BITS as usize)) != 0) +} + +pub(super) fn assignment_insert(assignments: &mut [u64], local: usize) { + if let Some(word) = assignments.get_mut(local / u64::BITS as usize) { + *word |= 1 << (local % u64::BITS as usize); + } +} + +pub(super) fn merge_assignment_entry( + target: usize, + incoming: &[u64], + block_starts: &[usize], + block_by_instruction: &[usize], + entries: &mut [Option>], + worklist: &mut VecDeque, + queued: &mut [bool], + context: &str, +) -> jvm::Result<()> { + let Some(&block) = block_by_instruction.get(target) else { + return Ok(()); + }; + if block_starts[block] != target { + return Err(jvm::Error::VerificationError { + context: context.to_string(), + message: format!("Control flow targets the middle of bytecode block at {target}"), + }); + } + let changed = match &mut entries[block] { + Some(existing) => { + let previous = existing.clone(); + for (word, incoming) in existing.iter_mut().zip(incoming) { + *word &= incoming; + } + *existing != previous + } + slot @ None => { + *slot = Some(incoming.to_vec()); + true + } + }; + if changed && !queued[block] { + queued[block] = true; + worklist.push_back(block); + } + Ok(()) +} + +pub(super) fn default_local_initializer(local: u16, value: &FrameValue) -> [Instruction; 2] { + use Instruction as I; + + let (constant, store) = match value { + FrameValue::Integer => (I::Iconst_0, local_store(local, I::Istore, I::Istore_w)), + FrameValue::Long => (I::Lconst_0, local_store(local, I::Lstore, I::Lstore_w)), + FrameValue::Float => (I::Fconst_0, local_store(local, I::Fstore, I::Fstore_w)), + FrameValue::Double => (I::Dconst_0, local_store(local, I::Dstore, I::Dstore_w)), + FrameValue::Null + | FrameValue::Object(_) + | FrameValue::UninitializedThis + | FrameValue::Uninitialized(_) => { + (I::Aconst_null, local_store(local, I::Astore, I::Astore_w)) + } + FrameValue::Top => unreachable!("a local load always supplies a concrete JVM type"), + }; + [constant, store] +} + +pub(super) fn local_store( + local: u16, + narrow: impl FnOnce(u8) -> Instruction, + wide: impl FnOnce(u16) -> Instruction, +) -> Instruction { + u8::try_from(local).map_or_else(|_| wide(local), narrow) +} + +pub(super) fn shift_absolute_branch_targets( + instructions: &mut [Instruction], + amount: u16, + context: &str, +) -> jvm::Result<()> { + for instruction in instructions { + match instruction { + Instruction::Ifeq(target) + | Instruction::Ifne(target) + | Instruction::Iflt(target) + | Instruction::Ifge(target) + | Instruction::Ifgt(target) + | Instruction::Ifle(target) + | Instruction::If_icmpeq(target) + | Instruction::If_icmpne(target) + | Instruction::If_icmplt(target) + | Instruction::If_icmpge(target) + | Instruction::If_icmpgt(target) + | Instruction::If_icmple(target) + | Instruction::If_acmpeq(target) + | Instruction::If_acmpne(target) + | Instruction::Goto(target) + | Instruction::Jsr(target) + | Instruction::Ifnull(target) + | Instruction::Ifnonnull(target) => { + *target = + target + .checked_add(amount) + .ok_or_else(|| jvm::Error::VerificationError { + context: context.to_string(), + message: "Branch target overflow while inserting a method-entry prefix" + .to_string(), + })?; + } + Instruction::Goto_w(target) | Instruction::Jsr_w(target) => { + *target = target.checked_add(i32::from(amount)).ok_or_else(|| { + jvm::Error::VerificationError { + context: context.to_string(), + message: + "Wide branch target overflow while inserting a method-entry prefix" + .to_string(), + } + })?; + } + // Switch offsets are relative: both source and target move equally. + _ => {} + } + } + Ok(()) +} + +pub fn move_zero_branch_target( + instructions: &mut Vec, + context: &str, +) -> jvm::Result { + if !branch_targets(instructions).contains(&0) { + return Ok(false); + } + + shift_absolute_branch_targets(instructions, 1, context)?; + instructions.insert(0, Instruction::Nop); + Ok(true) +} + +pub fn push_local_value(locals: &mut Vec, value: FrameValue) { + let is_category2 = value.is_category2(); + locals.push(value); + if is_category2 { + locals.push(FrameValue::Top); + } +} + +pub fn set_slot_value(locals: &mut Vec, local_index: u16, value: FrameValue) { + let local_index = local_index as usize; + let width = if value.is_category2() { 2 } else { 1 }; + if locals.len() < local_index + width { + locals.resize(local_index + width, FrameValue::Top); + } + locals[local_index] = value; + if width == 2 { + locals[local_index + 1] = FrameValue::Top; + } +} diff --git a/compiler-core/src/jvm/frames/mod.rs b/compiler-core/src/jvm/frames/mod.rs new file mode 100644 index 0000000..bc73c9a --- /dev/null +++ b/compiler-core/src/jvm/frames/mod.rs @@ -0,0 +1,219 @@ +mod analysis; +pub use analysis::analyze; +use analysis::*; +mod locals; +pub use locals::{ + initial_locals_for_descriptor, initialize_locals_loaded_as_top, move_zero_branch_target, + push_local_value, set_slot_value, +}; +mod flow; +use flow::*; +mod transfer; +use transfer::*; +mod encoding; +pub use encoding::{build_stack_map_attributes, build_stack_map_attributes_from_analysis}; +mod descriptors; +use crate::classfile::constant_pool::InternedConstantPool; +use crate::classfile::{ + self as jvm, BaseType, Constant, ConstantPool, FieldType, + attributes::{ + ArrayType, Attribute, ExceptionTableEntry, Instruction, StackFrame, VerificationType, + }, +}; +use descriptors::*; +use rustc_hash::FxHashMap as HashMap; +use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::sync::Arc; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FrameValue { + Top, + Integer, + Float, + Long, + Double, + Null, + Object(Arc), + UninitializedThis, + Uninitialized(u16), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct FrameState { + locals: Arc>, + stack: Vec, + stack_words: usize, +} + +pub struct FrameAnalysis { + pub max_stack: u16, + block_starts: Vec, + entry_states: Vec>, +} + +impl FrameAnalysis { + fn state_at(&self, instruction: usize) -> Option<&FrameState> { + let block = self.block_starts.binary_search(&instruction).ok()?; + self.entry_states.get(block)?.as_ref() + } +} + +impl FrameValue { + fn is_category2(&self) -> bool { + matches!(self, FrameValue::Long | FrameValue::Double) + } + + fn is_reference_like(&self) -> bool { + matches!( + self, + FrameValue::Null + | FrameValue::Object(_) + | FrameValue::UninitializedThis + | FrameValue::Uninitialized(_) + ) + } +} + +impl FrameState { + fn new(initial_locals: Vec, max_locals: usize) -> Self { + let mut locals = initial_locals; + locals.resize(max_locals, FrameValue::Top); + Self { + locals: Arc::new(locals), + stack: Vec::new(), + stack_words: 0, + } + } + + fn push(&mut self, value: FrameValue) { + self.stack_words += if value.is_category2() { 2 } else { 1 }; + self.stack.push(value); + } + + fn pop(&mut self, context: &str, instruction_index: usize) -> jvm::Result { + let value = self + .stack + .pop() + .ok_or_else(|| jvm::Error::VerificationError { + context: context.to_string(), + message: format!("Stack underflow at instruction {instruction_index}"), + })?; + self.stack_words -= if value.is_category2() { 2 } else { 1 }; + Ok(value) + } + + fn pop_category1( + &mut self, + context: &str, + instruction_index: usize, + ) -> jvm::Result { + let value = self.pop(context, instruction_index)?; + if value.is_category2() { + return Err(jvm::Error::VerificationError { + context: context.to_string(), + message: format!( + "Expected category-1 value at instruction {instruction_index}, found {value:?}" + ), + }); + } + Ok(value) + } + + fn pop_reference( + &mut self, + context: &str, + instruction_index: usize, + ) -> jvm::Result { + let value = self.pop_category1(context, instruction_index)?; + if !value.is_reference_like() && value != FrameValue::Top { + return Err(jvm::Error::VerificationError { + context: context.to_string(), + message: format!( + "Expected reference value at instruction {instruction_index}, found {value:?}" + ), + }); + } + Ok(value) + } + + fn load_local( + &mut self, + index: u16, + local_hints: &[FrameValue], + load_hint: FrameValue, + context: &str, + instruction_index: usize, + ) -> jvm::Result<()> { + let mut value = self + .locals + .get(index as usize) + .cloned() + .unwrap_or(FrameValue::Top); + if value == FrameValue::Top { + value = local_hints + .get(index as usize) + .cloned() + .unwrap_or(FrameValue::Top); + if value == FrameValue::Top { + value = load_hint; + if value == FrameValue::Top { + return Err(jvm::Error::VerificationError { + context: context.to_string(), + message: format!( + "Loaded uninitialized local {index} at instruction {instruction_index}" + ), + }); + } + } + self.store_local(index, value.clone()); + } + self.push(value); + Ok(()) + } + + fn store_local(&mut self, index: u16, value: FrameValue) { + let index = index as usize; + let width = if value.is_category2() { 2 } else { 1 }; + if self.locals.get(index) == Some(&value) + && (index == 0 || !self.locals[index - 1].is_category2()) + && (width == 1 || self.locals.get(index + 1) == Some(&FrameValue::Top)) + { + return; + } + let locals = Arc::make_mut(&mut self.locals); + if locals.len() < index + width { + locals.resize(index + width, FrameValue::Top); + } + + if index > 0 && locals[index - 1].is_category2() { + locals[index - 1] = FrameValue::Top; + } + locals[index] = value; + if width == 2 { + locals[index + 1] = FrameValue::Top; + } + } + + fn initialize_object(&mut self, uninitialized: &FrameValue, class_name: &str) { + let initialized = FrameValue::Object(normalize_class_name(class_name).into()); + if self.locals.contains(uninitialized) { + for local in Arc::make_mut(&mut self.locals) { + if local == uninitialized { + *local = initialized.clone(); + } + } + } + for stack_value in &mut self.stack { + if stack_value == uninitialized { + *stack_value = initialized.clone(); + } + } + } +} + +pub fn normalize_class_name(class_name: &str) -> String { + class_name.replace('.', "/") +} + +#[cfg(test)] +mod tests; diff --git a/compiler-core/src/jvm/frames/tests.rs b/compiler-core/src/jvm/frames/tests.rs new file mode 100644 index 0000000..c08136c --- /dev/null +++ b/compiler-core/src/jvm/frames/tests.rs @@ -0,0 +1,91 @@ +use super::encoding::compact_stack_frame; +use super::locals::locals_loaded_before_definite_store; +use super::*; + +#[test] +fn compact_frames_reuse_or_extend_previous_locals() { + let previous = vec![VerificationType::Integer]; + assert!(matches!( + compact_stack_frame(4, &previous, &previous, Vec::new()), + StackFrame::SameFrameExtended { + frame_type: 251, + offset_delta: 4 + } + )); + assert!(matches!( + compact_stack_frame( + 7, + &previous, + &[VerificationType::Integer, VerificationType::Float], + Vec::new() + ), + StackFrame::AppendFrame { + frame_type: 252, + offset_delta: 7, + .. + } + )); + assert!(matches!( + compact_stack_frame( + 2, + &[VerificationType::Integer, VerificationType::Float], + &previous, + Vec::new() + ), + StackFrame::ChopFrame { + frame_type: 250, + offset_delta: 2 + } + )); +} + +#[test] +fn compact_frames_fall_back_when_stack_or_locals_require_it() { + let previous = vec![VerificationType::Integer]; + assert!(matches!( + compact_stack_frame(3, &previous, &previous, vec![VerificationType::Integer]), + StackFrame::SameLocals1StackItemFrameExtended { .. } + )); + assert!(matches!( + compact_stack_frame( + 3, + &previous, + &[VerificationType::Float], + vec![VerificationType::Integer, VerificationType::Integer] + ), + StackFrame::FullFrame { .. } + )); +} + +#[test] +fn definite_assignment_finds_control_flow_guarded_loads() { + let instructions = vec![ + Instruction::Iconst_0, + Instruction::Ifeq(4), + Instruction::Iconst_1, + Instruction::Istore_1, + Instruction::Iload_1, + Instruction::Pop, + Instruction::Return, + ]; + let loads = locals_loaded_before_definite_store(&instructions, &[], 2, "test", &[]).unwrap(); + assert_eq!(loads, BTreeMap::from([(1, FrameValue::Integer)])); +} + +#[test] +fn definite_assignment_accepts_a_store_on_every_path() { + let instructions = vec![ + Instruction::Iconst_0, + Instruction::Ifeq(5), + Instruction::Iconst_1, + Instruction::Istore_1, + Instruction::Goto(7), + Instruction::Iconst_0, + Instruction::Istore_1, + Instruction::Iload_1, + Instruction::Pop, + Instruction::Return, + ]; + let loads = locals_loaded_before_definite_store(&instructions, &[], 2, "test", &[]).unwrap(); + assert!(loads.is_empty()); +} diff --git a/compiler-core/src/jvm/frames/transfer.rs b/compiler-core/src/jvm/frames/transfer.rs new file mode 100644 index 0000000..1592aef --- /dev/null +++ b/compiler-core/src/jvm/frames/transfer.rs @@ -0,0 +1,626 @@ +use super::*; + +pub(super) fn transfer_instruction( + instruction_index: usize, + instruction: &Instruction, + state: &mut FrameState, + local_hints: &[FrameValue], + constant_pool: &ConstantPool, + context: &str, + signatures: &mut SignatureCache, +) -> jvm::Result<()> { + use Instruction as I; + + match instruction { + I::Nop => {} + I::Aconst_null => state.push(FrameValue::Null), + I::Iconst_m1 + | I::Iconst_0 + | I::Iconst_1 + | I::Iconst_2 + | I::Iconst_3 + | I::Iconst_4 + | I::Iconst_5 + | I::Bipush(_) + | I::Sipush(_) => state.push(FrameValue::Integer), + I::Lconst_0 | I::Lconst_1 => state.push(FrameValue::Long), + I::Fconst_0 | I::Fconst_1 | I::Fconst_2 => state.push(FrameValue::Float), + I::Dconst_0 | I::Dconst_1 => state.push(FrameValue::Double), + I::Ldc(index) => state.push(frame_value_from_ldc(constant_pool, u16::from(*index))?), + I::Ldc_w(index) => state.push(frame_value_from_ldc(constant_pool, *index)?), + I::Ldc2_w(index) => state.push(frame_value_from_ldc2(constant_pool, *index)?), + + I::Iload(index) => state.load_local( + u16::from(*index), + local_hints, + load_hint_for_instruction(instruction), + context, + instruction_index, + )?, + I::Lload(index) => state.load_local( + u16::from(*index), + local_hints, + load_hint_for_instruction(instruction), + context, + instruction_index, + )?, + I::Fload(index) => state.load_local( + u16::from(*index), + local_hints, + load_hint_for_instruction(instruction), + context, + instruction_index, + )?, + I::Dload(index) => state.load_local( + u16::from(*index), + local_hints, + load_hint_for_instruction(instruction), + context, + instruction_index, + )?, + I::Aload(index) => state.load_local( + u16::from(*index), + local_hints, + load_hint_for_instruction(instruction), + context, + instruction_index, + )?, + I::Iload_0 | I::Lload_0 | I::Fload_0 | I::Dload_0 | I::Aload_0 => state.load_local( + 0, + local_hints, + load_hint_for_instruction(instruction), + context, + instruction_index, + )?, + I::Iload_1 | I::Lload_1 | I::Fload_1 | I::Dload_1 | I::Aload_1 => state.load_local( + 1, + local_hints, + load_hint_for_instruction(instruction), + context, + instruction_index, + )?, + I::Iload_2 | I::Lload_2 | I::Fload_2 | I::Dload_2 | I::Aload_2 => state.load_local( + 2, + local_hints, + load_hint_for_instruction(instruction), + context, + instruction_index, + )?, + I::Iload_3 | I::Lload_3 | I::Fload_3 | I::Dload_3 | I::Aload_3 => state.load_local( + 3, + local_hints, + load_hint_for_instruction(instruction), + context, + instruction_index, + )?, + I::Iload_w(index) + | I::Lload_w(index) + | I::Fload_w(index) + | I::Dload_w(index) + | I::Aload_w(index) => state.load_local( + *index, + local_hints, + load_hint_for_instruction(instruction), + context, + instruction_index, + )?, + + I::Istore(index) => { + state.pop(context, instruction_index)?; + state.store_local(u16::from(*index), FrameValue::Integer); + } + I::Lstore(index) => { + state.pop(context, instruction_index)?; + state.store_local(u16::from(*index), FrameValue::Long); + } + I::Fstore(index) => { + state.pop(context, instruction_index)?; + state.store_local(u16::from(*index), FrameValue::Float); + } + I::Dstore(index) => { + state.pop(context, instruction_index)?; + state.store_local(u16::from(*index), FrameValue::Double); + } + I::Astore(index) => { + let value = state.pop_reference(context, instruction_index)?; + state.store_local(u16::from(*index), value); + } + I::Istore_0 => store_fixed(state, 0, FrameValue::Integer, context, instruction_index)?, + I::Istore_1 => store_fixed(state, 1, FrameValue::Integer, context, instruction_index)?, + I::Istore_2 => store_fixed(state, 2, FrameValue::Integer, context, instruction_index)?, + I::Istore_3 => store_fixed(state, 3, FrameValue::Integer, context, instruction_index)?, + I::Lstore_0 => store_fixed(state, 0, FrameValue::Long, context, instruction_index)?, + I::Lstore_1 => store_fixed(state, 1, FrameValue::Long, context, instruction_index)?, + I::Lstore_2 => store_fixed(state, 2, FrameValue::Long, context, instruction_index)?, + I::Lstore_3 => store_fixed(state, 3, FrameValue::Long, context, instruction_index)?, + I::Fstore_0 => store_fixed(state, 0, FrameValue::Float, context, instruction_index)?, + I::Fstore_1 => store_fixed(state, 1, FrameValue::Float, context, instruction_index)?, + I::Fstore_2 => store_fixed(state, 2, FrameValue::Float, context, instruction_index)?, + I::Fstore_3 => store_fixed(state, 3, FrameValue::Float, context, instruction_index)?, + I::Dstore_0 => store_fixed(state, 0, FrameValue::Double, context, instruction_index)?, + I::Dstore_1 => store_fixed(state, 1, FrameValue::Double, context, instruction_index)?, + I::Dstore_2 => store_fixed(state, 2, FrameValue::Double, context, instruction_index)?, + I::Dstore_3 => store_fixed(state, 3, FrameValue::Double, context, instruction_index)?, + I::Astore_0 => store_reference_fixed(state, 0, context, instruction_index)?, + I::Astore_1 => store_reference_fixed(state, 1, context, instruction_index)?, + I::Astore_2 => store_reference_fixed(state, 2, context, instruction_index)?, + I::Astore_3 => store_reference_fixed(state, 3, context, instruction_index)?, + I::Istore_w(local) => store_fixed( + state, + *local, + FrameValue::Integer, + context, + instruction_index, + )?, + I::Lstore_w(local) => { + store_fixed(state, *local, FrameValue::Long, context, instruction_index)? + } + I::Fstore_w(local) => { + store_fixed(state, *local, FrameValue::Float, context, instruction_index)? + } + I::Dstore_w(local) => store_fixed( + state, + *local, + FrameValue::Double, + context, + instruction_index, + )?, + I::Astore_w(local) => store_reference_fixed(state, *local, context, instruction_index)?, + + I::Iaload | I::Baload | I::Caload | I::Saload => { + state.pop(context, instruction_index)?; + state.pop_reference(context, instruction_index)?; + state.push(FrameValue::Integer); + } + I::Laload => array_load(state, FrameValue::Long, context, instruction_index)?, + I::Faload => array_load(state, FrameValue::Float, context, instruction_index)?, + I::Daload => array_load(state, FrameValue::Double, context, instruction_index)?, + I::Aaload => { + state.pop(context, instruction_index)?; + let array = state.pop_reference(context, instruction_index)?; + state.push(array_component_value(&array)); + } + I::Iastore + | I::Lastore + | I::Fastore + | I::Dastore + | I::Aastore + | I::Bastore + | I::Castore + | I::Sastore => { + state.pop(context, instruction_index)?; + state.pop(context, instruction_index)?; + state.pop_reference(context, instruction_index)?; + } + + I::Pop => { + state.pop(context, instruction_index)?; + } + I::Pop2 => { + let value = state.pop(context, instruction_index)?; + if !value.is_category2() { + state.pop(context, instruction_index)?; + } + } + I::Dup => { + let value = state.pop_category1(context, instruction_index)?; + state.push(value.clone()); + state.push(value); + } + I::Swap => { + let first = state.pop_category1(context, instruction_index)?; + let second = state.pop_category1(context, instruction_index)?; + state.push(first); + state.push(second); + } + I::Dup_x1 => { + let value1 = state.pop_category1(context, instruction_index)?; + let value2 = state.pop_category1(context, instruction_index)?; + state.push(value1.clone()); + state.push(value2); + state.push(value1); + } + I::Dup2 => { + let value1 = state.pop(context, instruction_index)?; + if value1.is_category2() { + state.push(value1.clone()); + state.push(value1); + } else { + let value2 = state.pop_category1(context, instruction_index)?; + state.push(value2.clone()); + state.push(value1.clone()); + state.push(value2); + state.push(value1); + } + } + I::Dup_x2 | I::Dup2_x1 | I::Dup2_x2 => { + return Err(jvm::Error::VerificationError { + context: context.to_string(), + message: format!( + "Stack-map builder does not yet support {:?} at instruction {instruction_index}", + instruction + ), + }); + } + + I::Iadd | I::Isub | I::Imul | I::Idiv | I::Irem | I::Iand | I::Ior | I::Ixor => { + binary(state, FrameValue::Integer, context, instruction_index)? + } + I::Ladd | I::Lsub | I::Lmul | I::Ldiv | I::Lrem | I::Land | I::Lor | I::Lxor => { + binary(state, FrameValue::Long, context, instruction_index)? + } + I::Fadd | I::Fsub | I::Fmul | I::Fdiv | I::Frem => { + binary(state, FrameValue::Float, context, instruction_index)? + } + I::Dadd | I::Dsub | I::Dmul | I::Ddiv | I::Drem => { + binary(state, FrameValue::Double, context, instruction_index)? + } + I::Ineg => unary(state, FrameValue::Integer, context, instruction_index)?, + I::Lneg => unary(state, FrameValue::Long, context, instruction_index)?, + I::Fneg => unary(state, FrameValue::Float, context, instruction_index)?, + I::Dneg => unary(state, FrameValue::Double, context, instruction_index)?, + I::Ishl | I::Ishr | I::Iushr => { + shift(state, FrameValue::Integer, context, instruction_index)? + } + I::Lshl | I::Lshr | I::Lushr => shift(state, FrameValue::Long, context, instruction_index)?, + I::Iinc(local, _) => state.store_local(u16::from(*local), FrameValue::Integer), + I::Iinc_w(local, _) => state.store_local(*local, FrameValue::Integer), + + I::I2l => convert(state, FrameValue::Long, context, instruction_index)?, + I::I2f => convert(state, FrameValue::Float, context, instruction_index)?, + I::I2d => convert(state, FrameValue::Double, context, instruction_index)?, + I::L2i | I::F2i | I::D2i | I::I2b | I::I2c | I::I2s => { + convert(state, FrameValue::Integer, context, instruction_index)? + } + I::L2f | I::D2f => convert(state, FrameValue::Float, context, instruction_index)?, + I::L2d | I::F2d => convert(state, FrameValue::Double, context, instruction_index)?, + I::F2l | I::D2l => convert(state, FrameValue::Long, context, instruction_index)?, + I::Lcmp | I::Fcmpl | I::Fcmpg | I::Dcmpl | I::Dcmpg => { + state.pop(context, instruction_index)?; + state.pop(context, instruction_index)?; + state.push(FrameValue::Integer); + } + + I::Ifeq(_) | I::Ifne(_) | I::Iflt(_) | I::Ifge(_) | I::Ifgt(_) | I::Ifle(_) => { + state.pop(context, instruction_index)?; + } + I::If_icmpeq(_) + | I::If_icmpne(_) + | I::If_icmplt(_) + | I::If_icmpge(_) + | I::If_icmpgt(_) + | I::If_icmple(_) + | I::If_acmpeq(_) + | I::If_acmpne(_) => { + state.pop(context, instruction_index)?; + state.pop(context, instruction_index)?; + } + I::Ifnull(_) | I::Ifnonnull(_) => { + state.pop_reference(context, instruction_index)?; + } + I::Goto(_) | I::Goto_w(_) => {} + I::Tableswitch(_) | I::Lookupswitch(_) => { + state.pop(context, instruction_index)?; + } + I::Jsr(_) | I::Ret(_) | I::Jsr_w(_) | I::Ret_w(_) => { + return Err(jvm::Error::VerificationError { + context: context.to_string(), + message: format!( + "Legacy subroutine instruction unsupported at {instruction_index}" + ), + }); + } + + I::Ireturn | I::Lreturn | I::Freturn | I::Dreturn | I::Areturn => { + state.pop(context, instruction_index)?; + return Ok(()); + } + I::Return => return Ok(()), + + I::Getstatic(field_ref) => { + state.push(signatures.field(constant_pool, *field_ref)?); + } + I::Putstatic(_) => { + state.pop(context, instruction_index)?; + } + I::Getfield(field_ref) => { + state.pop_reference(context, instruction_index)?; + state.push(signatures.field(constant_pool, *field_ref)?); + } + I::Putfield(_) => { + state.pop(context, instruction_index)?; + state.pop_reference(context, instruction_index)?; + } + I::Invokevirtual(method_ref) | I::Invokespecial(method_ref) => { + let method = signatures.method(constant_pool, *method_ref)?; + apply_invoke( + state, + method, + false, + matches!(instruction, I::Invokespecial(_)), + context, + instruction_index, + )?; + } + I::Invokestatic(method_ref) => { + let method = signatures.method(constant_pool, *method_ref)?; + apply_invoke(state, method, true, false, context, instruction_index)?; + } + I::Invokedynamic(invoke_dynamic_ref) => { + let method = signatures.method(constant_pool, *invoke_dynamic_ref)?; + apply_invoke(state, method, true, false, context, instruction_index)?; + } + I::Invokeinterface(method_ref, _) => { + let method = signatures.method(constant_pool, *method_ref)?; + apply_invoke(state, method, false, false, context, instruction_index)?; + } + + I::New(class_index) => { + let _ = constant_pool.try_get_class(*class_index)?; + state.push(FrameValue::Uninitialized(instruction_index as u16)); + } + I::Newarray(array_type) => { + state.pop(context, instruction_index)?; + state.push(FrameValue::Object( + array_descriptor_from_type(array_type).into(), + )); + } + I::Anewarray(class_index) => { + state.pop(context, instruction_index)?; + let class_name = constant_pool.try_get_class(*class_index)?.to_string(); + let array_name = if class_name.starts_with('[') { + format!("[{class_name}") + } else { + format!("[L{};", normalize_class_name(&class_name)) + }; + state.push(FrameValue::Object(array_name.into())); + } + I::Arraylength => { + state.pop_reference(context, instruction_index)?; + state.push(FrameValue::Integer); + } + I::Athrow => { + state.pop_reference(context, instruction_index)?; + return Ok(()); + } + I::Checkcast(class_index) => { + state.pop_reference(context, instruction_index)?; + let class_name = constant_pool.try_get_class(*class_index)?.to_string(); + state.push(FrameValue::Object(normalize_class_name(&class_name).into())); + } + I::Instanceof(_) => { + state.pop_reference(context, instruction_index)?; + state.push(FrameValue::Integer); + } + I::Monitorenter | I::Monitorexit => { + state.pop_reference(context, instruction_index)?; + } + I::Multianewarray(class_index, dimensions) => { + for _ in 0..*dimensions { + state.pop(context, instruction_index)?; + } + let class_name = constant_pool.try_get_class(*class_index)?.to_string(); + state.push(FrameValue::Object(normalize_class_name(&class_name).into())); + } + I::Wide | I::Breakpoint | I::Impdep1 | I::Impdep2 => {} + } + + Ok(()) +} + +pub(super) fn apply_invoke( + state: &mut FrameState, + method: &MethodTransfer, + is_static: bool, + is_special: bool, + context: &str, + instruction_index: usize, +) -> jvm::Result<()> { + for _ in 0..method.params { + state.pop(context, instruction_index)?; + } + + let receiver = if is_static { + None + } else { + Some(state.pop_reference(context, instruction_index)?) + }; + + if is_special && let Some(class_name) = &method.constructor { + if let Some(receiver) = receiver { + match receiver { + FrameValue::Uninitialized(_) | FrameValue::UninitializedThis => { + state.initialize_object(&receiver, class_name); + } + _ => {} + } + } + } + + if let Some(return_type) = &method.result { + state.push(return_type.clone()); + } + Ok(()) +} + +pub(super) fn store_fixed( + state: &mut FrameState, + local: u16, + value: FrameValue, + context: &str, + instruction_index: usize, +) -> jvm::Result<()> { + state.pop(context, instruction_index)?; + state.store_local(local, value); + Ok(()) +} + +pub(super) fn store_reference_fixed( + state: &mut FrameState, + local: u16, + context: &str, + instruction_index: usize, +) -> jvm::Result<()> { + let value = state.pop_reference(context, instruction_index)?; + state.store_local(local, value); + Ok(()) +} + +pub(super) fn array_load( + state: &mut FrameState, + value: FrameValue, + context: &str, + instruction_index: usize, +) -> jvm::Result<()> { + state.pop(context, instruction_index)?; + state.pop_reference(context, instruction_index)?; + state.push(value); + Ok(()) +} + +pub(super) fn unary( + state: &mut FrameState, + value: FrameValue, + context: &str, + instruction_index: usize, +) -> jvm::Result<()> { + state.pop(context, instruction_index)?; + state.push(value); + Ok(()) +} + +pub(super) fn binary( + state: &mut FrameState, + value: FrameValue, + context: &str, + instruction_index: usize, +) -> jvm::Result<()> { + state.pop(context, instruction_index)?; + state.pop(context, instruction_index)?; + state.push(value); + Ok(()) +} + +pub(super) fn shift( + state: &mut FrameState, + value: FrameValue, + context: &str, + instruction_index: usize, +) -> jvm::Result<()> { + state.pop(context, instruction_index)?; + state.pop(context, instruction_index)?; + state.push(value); + Ok(()) +} + +pub(super) fn convert( + state: &mut FrameState, + value: FrameValue, + context: &str, + instruction_index: usize, +) -> jvm::Result<()> { + state.pop(context, instruction_index)?; + state.push(value); + Ok(()) +} + +pub(super) fn load_hint_for_instruction(instruction: &Instruction) -> FrameValue { + match instruction { + Instruction::Iload(_) + | Instruction::Iload_0 + | Instruction::Iload_1 + | Instruction::Iload_2 + | Instruction::Iload_3 + | Instruction::Iload_w(_) => FrameValue::Integer, + Instruction::Lload(_) + | Instruction::Lload_0 + | Instruction::Lload_1 + | Instruction::Lload_2 + | Instruction::Lload_3 + | Instruction::Lload_w(_) => FrameValue::Long, + Instruction::Fload(_) + | Instruction::Fload_0 + | Instruction::Fload_1 + | Instruction::Fload_2 + | Instruction::Fload_3 + | Instruction::Fload_w(_) => FrameValue::Float, + Instruction::Dload(_) + | Instruction::Dload_0 + | Instruction::Dload_1 + | Instruction::Dload_2 + | Instruction::Dload_3 + | Instruction::Dload_w(_) => FrameValue::Double, + Instruction::Aload(_) + | Instruction::Aload_0 + | Instruction::Aload_1 + | Instruction::Aload_2 + | Instruction::Aload_3 + | Instruction::Aload_w(_) => FrameValue::Object("java/lang/Object".into()), + _ => FrameValue::Top, + } +} + +pub(super) fn frame_value_from_ldc( + constant_pool: &ConstantPool, + index: u16, +) -> jvm::Result { + let value = match constant_pool.try_get(index)? { + Constant::Integer(_) => FrameValue::Integer, + Constant::Float(_) => FrameValue::Float, + Constant::String(_) => FrameValue::Object("java/lang/String".into()), + Constant::Class(_) => FrameValue::Object("java/lang/Class".into()), + Constant::MethodType(_) => FrameValue::Object("java/lang/invoke/MethodType".into()), + Constant::MethodHandle { .. } => FrameValue::Object("java/lang/invoke/MethodHandle".into()), + Constant::Dynamic { .. } => FrameValue::Object("java/lang/Object".into()), + _ => FrameValue::Top, + }; + Ok(value) +} + +pub(super) fn frame_value_from_ldc2( + constant_pool: &ConstantPool, + index: u16, +) -> jvm::Result { + let value = match constant_pool.try_get(index)? { + Constant::Long(_) => FrameValue::Long, + Constant::Double(_) => FrameValue::Double, + _ => FrameValue::Top, + }; + Ok(value) +} + +pub(super) fn array_component_value(array: &FrameValue) -> FrameValue { + let FrameValue::Object(class_name) = array else { + return FrameValue::Object("java/lang/Object".into()); + }; + let Some(component_descriptor) = class_name.strip_prefix('[') else { + return FrameValue::Object("java/lang/Object".into()); + }; + if component_descriptor.starts_with('[') { + return FrameValue::Object(component_descriptor.into()); + } + if component_descriptor.starts_with('L') && component_descriptor.ends_with(';') { + return FrameValue::Object(component_descriptor[1..component_descriptor.len() - 1].into()); + } + match component_descriptor.chars().next() { + Some('J') => FrameValue::Long, + Some('F') => FrameValue::Float, + Some('D') => FrameValue::Double, + Some('Z' | 'B' | 'C' | 'S' | 'I') => FrameValue::Integer, + _ => FrameValue::Object("java/lang/Object".into()), + } +} + +pub(super) fn array_descriptor_from_type(array_type: &ArrayType) -> String { + let descriptor = match array_type { + ArrayType::Boolean => "Z", + ArrayType::Char => "C", + ArrayType::Float => "F", + ArrayType::Double => "D", + ArrayType::Byte => "B", + ArrayType::Short => "S", + ArrayType::Int => "I", + ArrayType::Long => "J", + }; + format!("[{descriptor}") +} diff --git a/compiler-core/src/jvm/locals.rs b/compiler-core/src/jvm/locals.rs new file mode 100644 index 0000000..5a6b0f9 --- /dev/null +++ b/compiler-core/src/jvm/locals.rs @@ -0,0 +1,66 @@ +//! JVM local categories and their shortest legal load/store encodings. +use crate::classfile::attributes::Instruction; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LocalKind { + Int, + Long, + Float, + Double, + Reference, +} + +impl LocalKind { + pub fn width(self) -> u16 { + if matches!(self, Self::Long | Self::Double) { + 2 + } else { + 1 + } + } + pub fn return_op(self) -> Instruction { + match self { + Self::Int => Instruction::Ireturn, + Self::Long => Instruction::Lreturn, + Self::Float => Instruction::Freturn, + Self::Double => Instruction::Dreturn, + Self::Reference => Instruction::Areturn, + } + } +} + +// Each row defines both encoding and decoding, including compact and wide forms. +macro_rules! local_forms { + ($method:ident, $decode:ident; $($kind:ident: $short:ident, $wide:ident, $a:ident, $b:ident, $c:ident, $d:ident);* $(;)?) => { + impl LocalKind { + pub fn $method(self, index: u16) -> Instruction { + match self { $(Self::$kind => match index { + 0 => Instruction::$a, 1 => Instruction::$b, 2 => Instruction::$c, 3 => Instruction::$d, + 4..=255 => Instruction::$short(index as u8), _ => Instruction::$wide(index), + }),* } + } + } + pub fn $decode(instruction: &Instruction) -> Option<(LocalKind, u16)> { + Some(match instruction { $( + Instruction::$a => (LocalKind::$kind, 0), Instruction::$b => (LocalKind::$kind, 1), + Instruction::$c => (LocalKind::$kind, 2), Instruction::$d => (LocalKind::$kind, 3), + Instruction::$short(index) => (LocalKind::$kind, u16::from(*index)), + Instruction::$wide(index) => (LocalKind::$kind, *index), + )* _ => return None }) + } + }; +} +local_forms! { load, loaded_local; + Int: Iload, Iload_w, Iload_0, Iload_1, Iload_2, Iload_3; + Long: Lload, Lload_w, Lload_0, Lload_1, Lload_2, Lload_3; + Float: Fload, Fload_w, Fload_0, Fload_1, Fload_2, Fload_3; + Double: Dload, Dload_w, Dload_0, Dload_1, Dload_2, Dload_3; + Reference: Aload, Aload_w, Aload_0, Aload_1, Aload_2, Aload_3; +} +local_forms! { store, stored_local; + Int: Istore, Istore_w, Istore_0, Istore_1, Istore_2, Istore_3; + Long: Lstore, Lstore_w, Lstore_0, Lstore_1, Lstore_2, Lstore_3; + Float: Fstore, Fstore_w, Fstore_0, Fstore_1, Fstore_2, Fstore_3; + Double: Dstore, Dstore_w, Dstore_0, Dstore_1, Dstore_2, Dstore_3; + Reference: Astore, Astore_w, Astore_0, Astore_1, Astore_2, Astore_3; +} diff --git a/compiler-core/src/jvm/mod.rs b/compiler-core/src/jvm/mod.rs new file mode 100644 index 0000000..cbaf553 --- /dev/null +++ b/compiler-core/src/jvm/mod.rs @@ -0,0 +1,19 @@ +pub mod abi; +pub mod casts; +pub mod constants; +pub mod encoding; +pub mod frames; +pub mod select; + +use crate::classfile::attributes::{Attribute, ExceptionTableEntry, Instruction}; + +pub struct MethodCode { + pub instructions: Vec, + pub max_stack: u16, + pub max_locals: u16, + pub attributes: Vec, + pub exceptions: Vec, +} + +pub mod flow; +pub mod locals; diff --git a/compiler-core/src/jvm/select/allocate.rs b/compiler-core/src/jvm/select/allocate.rs new file mode 100644 index 0000000..48d987d --- /dev/null +++ b/compiler-core/src/jvm/select/allocate.rs @@ -0,0 +1,277 @@ +//! Conservative SSA intervals without instruction-by-value liveness matrices. +//! Uses extend backwards through predecessor blocks until their definition; +//! intervals also cover edge-copy destinations, including pre-throw copies. +use super::*; +use std::{cmp::Reverse, collections::BinaryHeap}; + +pub(super) struct Allocation { + pub slots: Vec>, + pub count: u16, +} + +fn value_kind(body: &Body, types: &Types, value: ValueId) -> jvm::Result { + representation::value_kind(types, body.value_type(value)) +} + +pub(super) fn allocate( + body: &Body, + types: &Types, + live: &crate::opt::Live, + relative_pointer_abi: bool, + debug: Option<&DebugInfo>, + order: &[BlockId], +) -> jvm::Result { + let intervals = intervals(body, live, debug, order)?; + let mut result = Allocation { + slots: vec![None; body.values.len()], + count: 0, + }; + let mut active = BinaryHeap::new(); + let mut free: [Vec; 5] = Default::default(); + for ¶m in &body.blocks[body.entry.index()].params { + result.slots[param.index()] = Some(result.count); + result.count = result + .count + .checked_add(value_kind(body, types, param)?.width()) + .ok_or_else(|| error("JVM local limit"))?; + if relative_pointer_abi + && matches!(types.get(body.value_type(param)), Some(Type::Pointer(_))) + { + result.count = result + .count + .checked_add(4) + .ok_or_else(|| error("JVM parameter limit"))?; + } + active.push(Reverse((intervals[param.index()].1, param))); + } + let mut order = (0..body.values.len()) + .filter(|&i| { + live.values[i] + && literal(body, ValueId::new(i)).is_none() + && result.slots[i].is_none() + && matches!(body.values[i].def, ValueDef::Inst(_) | ValueDef::Param(_)) + }) + .collect::>(); + order.sort_unstable_by_key(|&i| (intervals[i].0, i)); + for index in order { + let value = ValueId::new(index); + let (first, last) = intervals[index]; + while active.peek().is_some_and(|Reverse((end, _))| *end < first) { + let Reverse((_, expired)) = active.pop().unwrap(); + free[value_kind(body, types, expired)? as usize] + .push(result.slots[expired.index()].unwrap()); + } + let kind = value_kind(body, types, value)?; + let slot = if let Some(slot) = free[kind as usize].pop() { + slot + } else { + let slot = result.count; + result.count = result + .count + .checked_add(kind.width()) + .ok_or_else(|| error("JVM local limit"))?; + slot + }; + result.slots[index] = Some(slot); + active.push(Reverse((last, value))); + } + Ok(result) +} + +fn intervals( + body: &Body, + live: &crate::opt::Live, + debug: Option<&DebugInfo>, + order: &[BlockId], +) -> jvm::Result> { + let mut ranges = vec![(u32::MAX, 0); body.values.len()]; + let mut definitions = vec![None; body.values.len()]; + let mut starts = vec![0; body.blocks.len()]; + let mut ends = starts.clone(); + let mut uses = Vec::new(); + let mut debug_uses = vec![ + Vec::new(); + if debug.is_some() { + body.blocks.len() + } else { + 0 + } + ]; + if let Some(debug) = debug { + for event in &debug.events { + if let DebugChange::Set { value, .. } = event.change { + debug_uses[event.block.index()].push((event.position as usize, value)); + } + } + for uses in &mut debug_uses { + uses.sort_by_key(|&(position, _)| position); + } + } + let mut position = 0u32; + for &block in order { + starts[block.index()] = position; + let data = &body.blocks[block.index()]; + for ¶m in &data.params { + definitions[param.index()] = Some(block); + extend(&mut ranges[param.index()], position); + } + let mut events = debug_uses + .get(block.index()) + .into_iter() + .flatten() + .peekable(); + for index in 0..=data.instructions.len() { + while events.peek().is_some_and(|&&(point, _)| point == index) { + let &(_, value) = events.next().unwrap(); + use_at(body, value, block, position, &mut ranges, &mut uses); + } + let Some(&inst) = data.instructions.get(index) else { + break; + }; + if !live.instructions[inst.index()] + || body.instructions[inst.index()] + .result + .is_some_and(|v| literal(body, v).is_some()) + { + continue; + } + position = position + .checked_add(2) + .ok_or_else(|| error("SSA position limit"))?; + let inst = body.instructions[inst.index()]; + inst.op.visit_uses(&body.args, |value| { + use_at(body, value, block, position - 1, &mut ranges, &mut uses) + }); + if let Some(result) = inst.result { + definitions[result.index()] = Some(block); + extend(&mut ranges[result.index()], position); + } + } + position = position + .checked_add(3) + .ok_or_else(|| error("SSA position limit"))?; + let term = data.terminator.unwrap(); + term.visit_uses(|value| use_at(body, value, block, position - 2, &mut ranges, &mut uses)); + if let Terminator::Invoke { inst, normal, .. } = term { + let inst = body.instructions[inst.index()]; + inst.op.visit_uses(&body.args, |value| { + use_at(body, value, block, position - 2, &mut ranges, &mut uses) + }); + if let Some(result) = inst.result { + definitions[result.index()] = Some(body.edges[normal.index()].target); + extend(&mut ranges[result.index()], position - 1); + } + } + term.visit_edges(&body.cases, |edge| { + let copy_position = if matches!(term, Terminator::Invoke { normal, .. } if normal == edge) { position } else { position - 2 }; + let edge = &body.edges[edge.index()]; + for (¶m, &arg) in body.blocks[edge.target.index()].params.iter().zip(&edge.args) { + if live.values[param.index()] { + use_at(body, arg, block, copy_position, &mut ranges, &mut uses); + extend(&mut ranges[param.index()], copy_position); + } + } + }); + ends[block.index()] = position; + } + + // Group use blocks by value in one flat table. No per-value heap vectors. + let mut offsets = vec![0; body.values.len() + 1]; + for &(value, _) in &uses { + offsets[value.index() + 1] += 1; + } + for i in 1..offsets.len() { + offsets[i] += offsets[i - 1]; + } + let mut cursor = offsets.clone(); + let mut use_blocks = vec![body.entry; uses.len()]; + for (value, block) in uses { + use_blocks[cursor[value.index()]] = block; + cursor[value.index()] += 1; + } + drop(cursor); + let predecessors = body.predecessors(); + let mut visited = vec![None; body.blocks.len()]; + let mut pending = Vec::new(); + for index in 0..body.values.len() { + if !live.values[index] { + continue; + } + let value = ValueId::new(index); + let definition = definitions[index]; + pending.extend_from_slice(&use_blocks[offsets[index]..offsets[index + 1]]); + while let Some(block) = pending.pop() { + if Some(block) == definition || visited[block.index()] == Some(value) { + continue; + } + visited[block.index()] = Some(value); + extend(&mut ranges[index], starts[block.index()]); + for &(source, _) in &predecessors[block.index()] { + if live.blocks[source.index()] { + extend(&mut ranges[index], ends[source.index()]); + pending.push(source); + } + } + } + } + Ok(ranges) +} + +fn extend(range: &mut (u32, u32), position: u32) { + range.0 = range.0.min(position); + range.1 = range.1.max(position); +} + +fn use_at( + body: &Body, + value: ValueId, + block: BlockId, + position: u32, + ranges: &mut [(u32, u32)], + uses: &mut Vec<(ValueId, BlockId)>, +) { + let value = body.resolve(value); + if literal(body, value).is_some() { + return; + } + extend(&mut ranges[value.index()], position); + uses.push((value, block)); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_hundred_thousand_live_definitions_need_two_slots() { + let mut types = Types::default(); + let int = types.scalar(ScalarType::I32); + let mut b = Builder::new(&types, int); + let parameter = b.parameter(b.current(), int); + let mut result = parameter; + for _ in 0..100_000 { + result = b + .emit( + Op::Binary { + op: BinaryOp::Add, + left: result, + right: parameter, + }, + Some(int), + ) + .unwrap(); + } + b.terminate(Terminator::Return(Some(result))); + let body = b.finish().unwrap(); + let allocation = allocate( + &body, + &types, + &crate::opt::live(&body, &types), + false, + None, + &body.layout(), + ) + .unwrap(); + assert_eq!(allocation.count, 2); + } +} diff --git a/compiler-core/src/jvm/select/arrays.rs b/compiler-core/src/jvm/select/arrays.rs new file mode 100644 index 0000000..409523d --- /dev/null +++ b/compiler-core/src/jvm/select/arrays.rs @@ -0,0 +1,135 @@ +//! Array operations preserve lazy repeat copies and pointer-backed slice views. +use super::*; +impl Selector<'_> { + pub(super) fn array(&mut self, inst: Inst) -> jvm::Result { + if let Op::ArrayLength(array) = inst.op { + self.load(array)?; + let op = if matches!( + self.types.get(self.body.value_type(array)), + Some(Type::Array(_)) + ) { + Instruction::Arraylength + } else { + let owner = self.cp.add_class(representation::SLICE_VIEW_CLASS)?; + Instruction::Getfield(self.cp.add_field_ref(owner, "length", "I")?) + }; + self.assembly.code.push(op); + return Ok(true); + } + let (array, index, value) = match inst.op { + Op::ArrayGet { array, index } => (array, index, None), + Op::ArraySet { + array, + index, + value, + } => (array, index, Some(value)), + _ => return Ok(false), + }; + let representation = self.types.get(self.body.value_type(array)); + let element = match representation { + Some(Type::Array(e) | Type::Slice(e) | Type::Pointer(e)) => e, + Some(Type::Str) => self.types.find(Type::Scalar(ScalarType::U8)).unwrap(), + _ => return Err(error("invalid array/view representation")), + }; + self.load(array)?; + if matches!(representation, Some(Type::Pointer(_))) { + self.load(index)?; + self.assembly.code.push(Instruction::I2l); + let owner = self.cp.add_class(POINTER_CLASS)?; + let offset = self.cp.add_method_ref( + owner, + "offset", + "(Lorg/rustlang/runtime/Pointer;J)Lorg/rustlang/runtime/Pointer;", + )?; + self.assembly.code.push(Instruction::Invokestatic(offset)); + if let Some(value) = value { + self.argument(value)?; + self.write_memory(element)?; + } else { + self.read_memory(element)?; + } + return Ok(true); + } + let view = matches!(representation, Some(Type::Slice(_) | Type::Str)); + if view { + let owner = self.cp.add_class(representation::SLICE_VIEW_CLASS)?; + self.assembly + .code + .push(Instruction::Getfield(self.cp.add_field_ref( + owner, + "array", + "Ljava/lang/Object;", + )?)); + self.load(array)?; + self.assembly.code.push(Instruction::Getfield( + self.cp.add_field_ref(owner, "offset", "I")?, + )); + } + self.load(index)?; + if view { + self.assembly.code.push(Instruction::Iadd); + } + if let Some(value) = value { + self.argument(value)?; + } + use ScalarType::*; + let (suffix, descriptor, read, write) = match self.types.get(element) { + Some(Type::Scalar(Bool)) => ("Boolean", "Z", Instruction::Baload, Instruction::Bastore), + Some(Type::Scalar(I8 | U8)) => ("I8", "B", Instruction::Baload, Instruction::Bastore), + Some(Type::Scalar(I16 | F16)) => { + ("I16", "S", Instruction::Saload, Instruction::Sastore) + } + Some(Type::Scalar(U16 | Char)) => { + ("U16", "C", Instruction::Caload, Instruction::Castore) + } + Some(Type::Scalar(I32 | U32)) => { + ("I32", "I", Instruction::Iaload, Instruction::Iastore) + } + Some(Type::Scalar(I64 | U64)) => { + ("I64", "J", Instruction::Laload, Instruction::Lastore) + } + Some(Type::Scalar(F32)) => ("F32", "F", Instruction::Faload, Instruction::Fastore), + Some(Type::Scalar(F64)) => ("F64", "D", Instruction::Daload, Instruction::Dastore), + _ => ( + "Object", + "Ljava/lang/Object;", + Instruction::Aaload, + Instruction::Aastore, + ), + }; + if view || (suffix == "Object" && value.is_none()) { + let owner = self.cp.add_class(POINTER_CLASS)?; + let name = if view { + format!( + "slice{}{suffix}", + if value.is_some() { "Set" } else { "Get" } + ) + } else { + "arrayGetObject".into() + }; + let signature = if value.is_some() { + format!("(Ljava/lang/Object;I{descriptor})V") + } else { + format!("(Ljava/lang/Object;I){descriptor}") + }; + let method = self.cp.add_method_ref(owner, name, signature)?; + self.assembly.code.push(Instruction::Invokestatic(method)); + if suffix == "Object" && value.is_none() { + let mut name = String::new(); + representation::descriptor(self.types, element, &mut name)?; + let name = name + .strip_prefix('L') + .and_then(|s| s.strip_suffix(';')) + .unwrap_or(&name); + self.assembly + .code + .push(Instruction::Checkcast(self.cp.add_class(name)?)); + } + } else { + self.assembly + .code + .push(if value.is_some() { write } else { read }); + } + Ok(true) + } +} diff --git a/compiler-core/src/jvm/select/assemble.rs b/compiler-core/src/jvm/select/assemble.rs new file mode 100644 index 0000000..1111fdc --- /dev/null +++ b/compiler-core/src/jvm/select/assemble.rs @@ -0,0 +1,104 @@ +use super::*; + +#[derive(Clone, Copy)] +pub(super) struct Label(pub usize); + +#[derive(Default)] +pub(super) struct Assembly { + pub code: Vec, + labels: Vec>, + fixups: Vec<(usize, Label)>, + switches: Vec<(usize, Label, Vec