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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 35 additions & 10 deletions crates/rue-compiler/src/object_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
//! consumes these stable bytes on every link, while unchanged units reuse the
//! retained projection owned by the compiler query graph.

use std::{hash::Hash, sync::Arc};
use std::{
hash::Hash,
sync::{Arc, OnceLock},
};

use rue_query::{
QueryAbort, QueryContext, QueryFamily, QueryKey, QueryOutcome, QueryOutput, QueryTerminalKind,
Expand Down Expand Up @@ -74,25 +77,43 @@ impl QueryKey for ObjectProjectionQueryKey {
}

/// Retained object-container bytes and their stable content identity for one
/// codegen unit. The digest is computed once with the immutable bytes so every
/// downstream program-image assembly can reuse it without a serialized hash
/// pass.
#[derive(Debug, Clone, PartialEq, Eq)]
/// codegen unit. Fresh linking consumes only the immutable bytes, so the
/// durable digest is computed once on demand if a later plan comparison needs
/// it.
#[derive(Debug, Clone)]
pub(crate) struct ObjectProjection {
pub(crate) bytes: Arc<[u8]>,
pub(crate) content_digest: ContentDigest,
content_digest: OnceLock<ContentDigest>,
}

impl ObjectProjection {
pub(crate) fn from_bytes(bytes: Vec<u8>) -> Self {
let content_digest = bytes_digest(OBJECT_DIGEST_DOMAIN, &bytes);
Self {
bytes: bytes.into(),
content_digest,
content_digest: OnceLock::new(),
}
}

pub(crate) fn content_digest(&self) -> ContentDigest {
*self
.content_digest
.get_or_init(|| bytes_digest(OBJECT_DIGEST_DOMAIN, &self.bytes))
}

#[cfg(test)]
pub(crate) fn content_digest_is_initialized(&self) -> bool {
self.content_digest.get().is_some()
}
}

impl PartialEq for ObjectProjection {
fn eq(&self, other: &Self) -> bool {
self.bytes == other.bytes
}
}

impl Eq for ObjectProjection {}

impl RetainedCharge for ObjectProjection {
fn retained_charge(&self) -> u64 {
self.bytes.retained_charge()
Expand Down Expand Up @@ -207,8 +228,12 @@ mod tests {
let same = ObjectProjection::from_bytes(vec![1_u8, 2, 3, 4]);
let changed = ObjectProjection::from_bytes(vec![1_u8, 2, 3, 5]);

assert_eq!(first.content_digest, same.content_digest);
assert_ne!(first.content_digest, changed.content_digest);
assert!(!first.content_digest_is_initialized());
assert_eq!(first, same);
assert!(!first.content_digest_is_initialized());
assert_eq!(first.content_digest(), same.content_digest());
assert_ne!(first.content_digest(), changed.content_digest());
assert!(first.content_digest_is_initialized());
}

#[test]
Expand Down
54 changes: 45 additions & 9 deletions crates/rue-compiler/src/program_image_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,30 @@ use crate::{
use crate::codegen_query::CollectedCodegenUnit;

/// Stable, link-relevant identity for one reached codegen terminal.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone)]
pub(crate) struct ProgramImageUnit {
/// The durable callable identity retained by the terminal. The encoded
/// key below is only its deterministic ordering/map projection.
pub(crate) function: crate::FunctionInstanceKey,
pub(crate) identity: String,
pub(crate) defined_symbol: Arc<str>,
pub(crate) content_digest: ContentDigest,
/// The immutable projection owns its lazily materialized durable digest.
/// Keeping the projection here lets fresh plan construction avoid hashing
/// bytes that only a later plan comparison needs.
pub(crate) object: Arc<crate::object_query::ObjectProjection>,
}

impl PartialEq for ProgramImageUnit {
fn eq(&self, other: &Self) -> bool {
self.function == other.function
&& self.identity == other.identity
&& self.defined_symbol == other.defined_symbol
&& self.object.content_digest() == other.object.content_digest()
}
}

impl Eq for ProgramImageUnit {}

/// The deterministic contribution of a C-ABI export thunk. Thunks are not
/// codegen terminals, but their object bytes are a compiler-owned link input
/// and therefore have a separate, explicit plan entry.
Expand Down Expand Up @@ -319,7 +333,7 @@ impl ProgramImagePlan {
function: collected.function.clone(),
identity,
defined_symbol: collected.unit.defined_symbol.clone(),
content_digest: collected.object.content_digest,
object: Arc::clone(&collected.object),
})
.collect::<Vec<_>>();
plan_units.sort_by(|left, right| left.identity.cmp(&right.identity));
Expand Down Expand Up @@ -360,7 +374,7 @@ impl ProgramImagePlan {
function: collected.function.clone(),
identity: stable_function_identity(&collected.function),
defined_symbol: collected.unit.defined_symbol.clone(),
content_digest: collected.object.content_digest,
object: Arc::clone(&collected.object),
})
.collect::<Vec<_>>();
plan_units.sort_by(|left, right| left.identity.cmp(&right.identity));
Expand Down Expand Up @@ -726,12 +740,32 @@ mod tests {
.unwrap()
}

#[test]
fn fresh_plan_defers_object_digests_until_comparison() {
let (plan, _, _) = image_for("fn main() -> i32 { 0 }", Target::X86_64Linux, 1);
assert!(
plan.units
.iter()
.all(|unit| !unit.object.content_digest_is_initialized())
);

assert_eq!(plan, plan.clone());
assert!(
plan.units
.iter()
.all(|unit| unit.object.content_digest_is_initialized())
);
}

fn unit(identity: &str, digest_byte: u8) -> ProgramImageUnit {
ProgramImageUnit {
function: crate::FunctionInstanceKey::DropGlue(Box::new(crate::TypeInstanceKey::I64)),
identity: identity.to_owned(),
defined_symbol: Arc::from(identity),
content_digest: [digest_byte; 32],
object: Arc::new(crate::object_query::ObjectProjection::from_bytes(vec![
digest_byte;
32
])),
}
}

Expand Down Expand Up @@ -769,11 +803,13 @@ mod tests {
#[test]
fn delta_tracks_serialized_object_bytes_not_only_codegen_metadata() {
let mut before = unit("same", 1);
before.content_digest =
bytes_digest(b"rue.program-image.object\0v1\0", b"first object encoding");
before.object = Arc::new(crate::object_query::ObjectProjection::from_bytes(
b"first object encoding".to_vec(),
));
let mut after = before.clone();
after.content_digest =
bytes_digest(b"rue.program-image.object\0v1\0", b"second object encoding");
after.object = Arc::new(crate::object_query::ObjectProjection::from_bytes(
b"second object encoding".to_vec(),
));
let delta = plan(vec![after.clone()])
.delta_from(&plan(vec![before]))
.unwrap();
Expand Down
16 changes: 16 additions & 0 deletions docs/notes/post-adr-0063-cold-compiler-architecture-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -1786,6 +1786,22 @@ MAD); clock is neutral at -0.1323% amid substantial host noise, cycles at
compiler-work counter, source/output metric, and executable byte remains
identical.

RUE-1465 defers each immutable object projection's durable content digest until
a caller compares program-image plans. Fresh linking consumes the retained
object bytes directly, so eagerly hashing 1,280 objects and 3,011,805 bytes on
cold Lattice prepared identity for only the dormant later-linking seam. The
projection still owns and memoizes the same domain-separated SHA-256 digest;
ordinary exact object equality and fresh plan construction do not force it,
while plan delta comparison does.

Across 16 balanced fixed one-worker cold Lattice pairs, retired instructions
improve by 0.5993% (0.0651% MAD) and cycles by 0.2315% (0.7333% MAD).
End-to-end clock is neutral at -0.1895% (1.4135% MAD), peak RSS at +0.3510%
(0.3469% MAD), and peak footprint at +0.8275% (0.3708% MAD). Four
allocation-accounted pairs are neutral at +101--320 calls (under 0.003%) and
-3.3--118.6 KB requested bytes. Every compiler-work counter, source/output
metric, and executable byte remains identical.

## Next actions and decision boundary

Authorized low-risk work:
Expand Down