From 968f508c8b93eeddeb266e95b45065d79cac70cd Mon Sep 17 00:00:00 2001 From: sanohiro Date: Wed, 5 Aug 2026 11:54:44 +0900 Subject: [PATCH 1/7] feat(mir): observe source shape complexity --- crates/align_mir/src/source_shape.rs | 237 ++++++++++++++++++++++++++- 1 file changed, 233 insertions(+), 4 deletions(-) diff --git a/crates/align_mir/src/source_shape.rs b/crates/align_mir/src/source_shape.rs index e7da2c65..cd31f80e 100644 --- a/crates/align_mir/src/source_shape.rs +++ b/crates/align_mir/src/source_shape.rs @@ -32,6 +32,21 @@ pub(super) trait SourceShapeView { fn source_shape_node(&self, node: Node) -> Option>; } +trait SourceShapeObserver { + fn node(&mut self, node: Node, edges: usize); + fn pair(&mut self, pass: usize, left: Node, right: Node); + fn work(&mut self, units: usize); +} + +impl SourceShapeObserver for () { + #[inline] + fn node(&mut self, _node: Node, _edges: usize) {} + #[inline] + fn pair(&mut self, _pass: usize, _left: Node, _right: Node) {} + #[inline] + fn work(&mut self, _units: usize) {} +} + impl SourceShapeView for hir::Program { fn source_shape_node(&self, node: Node) -> Option> { match node { @@ -82,11 +97,23 @@ pub(super) fn source_shape_equal( left: Node, right: Node, known_shapes: &mut HashSet<(Node, Node)>, +) -> bool { + source_shape_equal_observed(view, left, right, known_shapes, &mut ()) +} + +fn source_shape_equal_observed( + view: &V, + left: Node, + right: Node, + known_shapes: &mut HashSet<(Node, Node)>, + observer: &mut O, ) -> bool { let mut comparator = SourceShapeComparator { view, + observer, known_shapes: &*known_shapes, root: (left, right), + pass: 0, cache_enabled: true, pending: VecDeque::from([(left, right)]), seen: HashSet::new(), @@ -102,10 +129,12 @@ pub(super) fn source_shape_equal( valid } -struct SourceShapeComparator<'a, V: ?Sized> { +struct SourceShapeComparator<'a, V: ?Sized, O: ?Sized> { view: &'a V, + observer: &'a mut O, known_shapes: &'a HashSet<(Node, Node)>, root: (Node, Node), + pass: usize, cache_enabled: bool, pending: VecDeque<(Node, Node)>, seen: HashSet<(Node, Node)>, @@ -113,7 +142,7 @@ struct SourceShapeComparator<'a, V: ?Sized> { right_to_left: HashMap, } -impl SourceShapeComparator<'_, V> { +impl SourceShapeComparator<'_, V, O> { fn run(&mut self) -> bool { loop { let mut restart = false; @@ -121,6 +150,7 @@ impl SourceShapeComparator<'_, V> { if !self.map_pair(left, right) { return false; } + self.observer.pair(self.pass, left, right); if self.cache_enabled && self.known_shapes.contains(&(left, right)) { if !self.seen.is_empty() || !self.pending.is_empty() { restart = true; @@ -140,6 +170,7 @@ impl SourceShapeComparator<'_, V> { return true; } self.cache_enabled = false; + self.pass += 1; self.pending.clear(); self.pending.push_back(self.root); self.seen.clear(); @@ -173,6 +204,11 @@ impl SourceShapeComparator<'_, V> { let Some(right) = view.source_shape_node(right_node) else { return false; }; + let (left_edges, work) = shape_cost(&left); + let (right_edges, _) = shape_cost(&right); + self.observer.node(left_node, left_edges); + self.observer.node(right_node, right_edges); + self.observer.work(work); match (left, right) { ( SourceShapeNode::Struct { @@ -435,12 +471,164 @@ impl SourceShapeComparator<'_, V> { } } +#[inline] +fn scalar_cost(value: Scalar) -> (usize, usize) { + match value { + Scalar::Struct(_) + | Scalar::DynStructArray(_) + | Scalar::Soa(_) + | Scalar::Enum(_) + | Scalar::Tagged(_) + | Scalar::Fn(_) => (1, 1), + _ => (0, 1), + } +} + +#[inline] +fn ty_cost(value: Ty) -> (usize, usize) { + let child = match value { + Ty::Option(value) + | Ty::Box(value) + | Ty::Slice(value) + | Ty::DynArray(value) + | Ty::ArrayBuilder(value) + | Ty::Task(value) => scalar_cost(value), + Ty::Result(left, right) => { + let left = scalar_cost(left); + let right = scalar_cost(right); + (left.0 + right.0, left.1 + right.1) + } + Ty::Array(value, _) | Ty::Vec(value, _) | Ty::Mask(value, _) => scalar_cost(value), + Ty::Tagged(_) + | Ty::StructArray(_, _) + | Ty::DynStructArray(_, _) + | Ty::Soa(_) + | Ty::JsonScanner(_) + | Ty::DictEncoded(_, _) + | Ty::Struct(_) + | Ty::Tuple(_) + | Ty::Fn(_) + | Ty::Enum(_) => (1, 1), + _ => (0, 0), + }; + (child.0, child.1 + 1) +} + +#[inline] +fn shape_cost(node: &SourceShapeNode<'_>) -> (usize, usize) { + match node { + SourceShapeNode::Struct { + source_name, + fields, + .. + } => fields + .iter() + .fold((0, 3 + source_name.len()), |(edges, work), field| { + let cost = ty_cost(field.ty); + (edges + cost.0, work + 1 + field.name.len() + cost.1) + }), + SourceShapeNode::Enum { + source_name, + variants, + } => variants.iter().fold( + (0, 2 + source_name.len()), + |(mut edges, mut work), variant| { + work += 2 + variant.name.len(); + for &value in &variant.payload { + let cost = scalar_cost(value); + edges += cost.0; + work += 1 + cost.1; + } + (edges, work) + }, + ), + SourceShapeNode::Tuple { elems } => elems.iter().fold((0, 1), |(edges, work), &value| { + let cost = scalar_cost(value); + (edges + cost.0, work + 1 + cost.1) + }), + SourceShapeNode::Tagged(value) => match value { + hir::TaggedType::Option(value) => { + let cost = scalar_cost(*value); + (cost.0, 2 + cost.1) + } + hir::TaggedType::Result(ok, err) => { + let ok = scalar_cost(*ok); + let err = scalar_cost(*err); + (ok.0 + err.0, 3 + ok.1 + err.1) + } + }, + SourceShapeNode::Function { + params, + ret, + return_borrow, + return_region, + } => { + let mut cost = ty_cost(**ret); + cost.1 += 4 + borrow_summary_work(return_borrow) + region_summary_work(return_region); + for (_, value) in *params { + let value = scalar_cost(*value); + cost.0 += value.0; + cost.1 += 2 + value.1; + } + cost + } + } +} + +#[inline] +fn borrow_summary_work(summary: &hir::ReturnBorrowSummary) -> usize { + match summary { + hir::ReturnBorrowSummary::None => 1, + hir::ReturnBorrowSummary::Roots { params, captures } => 3 + params.len() + captures.len(), + } +} + +#[inline] +fn region_summary_work(summary: &hir::ReturnRegionSummary) -> usize { + match summary { + hir::ReturnRegionSummary::None => 1, + hir::ReturnRegionSummary::Roots { params, captures } => 3 + params.len() + captures.len(), + } +} + #[cfg(test)] pub(super) mod tests { use super::*; use crate::validate_hir_tests::baseline_program; use align_sema::{FloatTy, IntTy, Layout}; use std::collections::HashSet; + + #[derive(Default)] + struct Metrics { + nodes: HashMap, + pairs: HashSet<(usize, Node, Node)>, + work: usize, + } + + impl SourceShapeObserver for Metrics { + fn node(&mut self, node: Node, edges: usize) { + self.nodes.entry(node).or_insert(edges); + } + + fn pair(&mut self, pass: usize, left: Node, right: Node) { + self.pairs.insert((pass, left, right)); + } + + fn work(&mut self, units: usize) { + self.work += units; + } + } + + impl Metrics { + fn counts(&self) -> (usize, usize, usize, usize) { + ( + self.nodes.len(), + self.nodes.values().sum(), + self.pairs.len(), + self.work, + ) + } + } fn i(bits: u8) -> IntTy { IntTy { bits, signed: true } } @@ -617,7 +805,7 @@ pub(super) mod tests { .next() .unwrap(); for (needle, count) in [ - ("HashSet<", 3), + ("HashSet<", 4), ("HashMap<", 2), ("VecDeque<", 1), ("HashSet::new", 1), @@ -626,8 +814,13 @@ pub(super) mod tests { ] { assert_eq!(production.matches(needle).count(), count, "{needle}"); } + assert!( + production + .contains("source_shape_equal_observed(view, left, right, known_shapes, &mut ())") + ); for absent in [ - "Observer", + "dyn SourceShapeObserver", + "static mut", "CanonicalTypeView", "ValidatedGraph", "canonical_type_bytes", @@ -641,4 +834,40 @@ pub(super) mod tests { 1 ); } + + #[test] + fn canonical_source_shape_complexity() { + let program = twin_program(); + let mut known = HashSet::new(); + let mut metrics = Metrics::default(); + let pairs = [ + (Node::Struct(0), Node::Struct(1)), + (Node::Enum(0), Node::Enum(1)), + (Node::Tuple(0), Node::Tuple(1)), + (Node::Tagged(0), Node::Tagged(1)), + (Node::Fn(0), Node::Fn(1)), + ]; + for (left, right) in pairs { + assert!(source_shape_equal_observed( + &program, + left, + right, + &mut known, + &mut metrics, + )); + } + let counts = metrics.counts(); + eprintln!("V/E/P/Q = {counts:?}"); + assert_eq!(counts, (10, 0, 5, 63)); + + let before = metrics.counts(); + assert!(source_shape_equal_observed( + &program, + Node::Struct(0), + Node::Struct(1), + &mut known, + &mut metrics, + )); + assert_eq!(metrics.counts(), before, "a fresh cached root is free"); + } } From c8ebb69ae5f6881d5b2e1c6937c5937dfe471795 Mon Sep 17 00:00:00 2001 From: sanohiro Date: Wed, 5 Aug 2026 12:16:13 +0900 Subject: [PATCH 2/7] feat(mir): canonicalize callable type graphs --- crates/align_mir/src/canonical_graph.rs | 1456 +++++++++++++++++++++++ 1 file changed, 1456 insertions(+) diff --git a/crates/align_mir/src/canonical_graph.rs b/crates/align_mir/src/canonical_graph.rs index 9e089793..68620b2d 100644 --- a/crates/align_mir/src/canonical_graph.rs +++ b/crates/align_mir/src/canonical_graph.rs @@ -1,6 +1,10 @@ +use std::collections::{HashMap, HashSet}; + use align_ast::ParamMode; use align_sema::{hir, Layout, PrimScalar, Scalar, Ty}; +use super::source_shape::{source_shape_equal, SourceShapeNode, SourceShapeView}; + #[derive(Clone, Debug)] pub struct FunctionTypeDef { pub params: Vec<(ParamMode, Scalar)>, @@ -30,6 +34,984 @@ pub(super) enum Node { Fn(u32), } +#[derive(Clone, Copy)] +struct CanonicalTypeView<'a> { + structs: &'a [hir::StructDef], + enums: &'a [hir::EnumDef], + tuples: &'a [hir::TupleDef], + tagged_types: &'a [hir::TaggedType], + fn_types: &'a [FunctionTypeDef], +} + +impl SourceShapeView for CanonicalTypeView<'_> { + fn source_shape_node(&self, node: Node) -> Option> { + match node { + Node::Struct(id) => { + self.structs + .get(id as usize) + .map(|definition| SourceShapeNode::Struct { + source_name: &definition.source_name, + align: &definition.align, + c_repr: &definition.c_repr, + fields: &definition.fields, + }) + } + Node::Enum(id) => self + .enums + .get(id as usize) + .map(|definition| SourceShapeNode::Enum { + source_name: &definition.source_name, + variants: &definition.variants, + }), + Node::Tuple(id) => { + self.tuples + .get(id as usize) + .map(|definition| SourceShapeNode::Tuple { + elems: &definition.elems, + }) + } + Node::Tagged(id) => self + .tagged_types + .get(id as usize) + .map(SourceShapeNode::Tagged), + Node::Fn(id) => { + self.fn_types + .get(id as usize) + .map(|definition| SourceShapeNode::Function { + params: &definition.params, + ret: &definition.ret, + return_borrow: &definition.return_borrow, + return_region: &definition.return_region, + }) + } + } + } +} + +struct ValidatedGraph<'a> { + root: Ty, + view: CanonicalTypeView<'a>, + order: Vec, +} + +impl<'a> ValidatedGraph<'a> { + fn new(root: Ty, view: CanonicalTypeView<'a>) -> Result { + let mut validator = GraphValidator { + view, + pending: Vec::new(), + seen: HashSet::new(), + order: Vec::new(), + candidates: Vec::new(), + next_ordinal: 0, + end_ordinals: HashMap::new(), + }; + let mut roots = Vec::new(); + validator.scan_ty(root, &mut roots); + roots.reverse(); + validator.pending.extend(roots); + while let Some(node) = validator.pending.pop() { + validator.visit_node(node); + } + validator.collect_cross_node_candidates(); + if let Some(candidate) = validator + .candidates + .iter() + .min_by_key(|candidate| (candidate.ordinal, candidate.tie_rank)) + { + return Err(candidate.error); + } + Ok(Self { + root, + view, + order: validator.order, + }) + } +} + +struct GraphValidator<'a> { + view: CanonicalTypeView<'a>, + pending: Vec, + seen: HashSet, + order: Vec, + candidates: Vec, + next_ordinal: u64, + end_ordinals: HashMap, +} + +#[derive(Clone, Copy)] +struct ErrorCandidate { + ordinal: u64, + tie_rank: u8, + error: CanonicalGraphError, +} + +impl<'a> GraphValidator<'a> { + fn visit_node(&mut self, node: Node) { + if !self.seen.insert(node) { + return; + } + let view = self.view; + let Some(shape) = view.source_shape_node(node) else { + let ordinal = self.field_ordinal(); + self.candidate(ordinal, CanonicalGraphError::MissingReference); + return; + }; + self.order.push(node); + let mut references = Vec::new(); + match shape { + SourceShapeNode::Struct { + source_name, + align, + fields, + .. + } => { + let source_ordinal = self.field_ordinal(); + self.validate_source_name(source_name, source_ordinal); + if let Some(align) = *align { + let ordinal = self.field_ordinal(); + if align > (1 << 29) || !align.is_power_of_two() { + self.candidate(ordinal, CanonicalGraphError::InvalidGraph); + } + } else { + self.field_ordinal(); + } + self.field_ordinal(); // c_repr + let count_ordinal = self.field_ordinal(); + self.validate_count(fields.len(), count_ordinal); + let mut names = HashSet::new(); + for field in fields { + let name_ordinal = self.field_ordinal(); + self.validate_identifier(&field.name, name_ordinal); + if !names.insert(field.name.as_str()) { + self.candidate(name_ordinal, CanonicalGraphError::DuplicateMember); + } + self.scan_ty(field.ty, &mut references); + } + } + SourceShapeNode::Enum { + source_name, + variants, + } => { + let source_ordinal = self.field_ordinal(); + self.validate_source_name(source_name, source_ordinal); + let count_ordinal = self.field_ordinal(); + self.validate_count(variants.len(), count_ordinal); + let mut names = HashSet::new(); + let mut expected_base = 1u32; + for variant in variants { + let name_ordinal = self.field_ordinal(); + self.validate_identifier(&variant.name, name_ordinal); + if !names.insert(variant.name.as_str()) { + self.candidate(name_ordinal, CanonicalGraphError::DuplicateMember); + } + let base_ordinal = self.field_ordinal(); + if variant.field_base != expected_base { + self.candidate(base_ordinal, CanonicalGraphError::InvalidGraph); + } + let count_ordinal = self.field_ordinal(); + self.validate_count(variant.payload.len(), count_ordinal); + match u32::try_from(variant.payload.len()) + .ok() + .and_then(|len| expected_base.checked_add(len)) + { + Some(next) => expected_base = next, + None => self.candidate(count_ordinal, CanonicalGraphError::InvalidCount), + } + for &value in &variant.payload { + self.scan_scalar(value, &mut references); + } + } + } + SourceShapeNode::Tuple { elems } => { + let count_ordinal = self.field_ordinal(); + self.validate_count(elems.len(), count_ordinal); + for &value in elems { + self.scan_scalar(value, &mut references); + } + } + SourceShapeNode::Tagged(value) => match value { + hir::TaggedType::Option(value) => { + self.field_ordinal(); + self.scan_scalar(*value, &mut references); + } + hir::TaggedType::Result(ok, err) => { + self.field_ordinal(); + self.scan_scalar(*ok, &mut references); + self.scan_scalar(*err, &mut references); + } + }, + SourceShapeNode::Function { + params, + ret, + return_borrow, + return_region, + } => { + let count_ordinal = self.field_ordinal(); + self.validate_count(params.len(), count_ordinal); + for &(mode, value) in params { + let mode_ordinal = self.field_ordinal(); + if !matches!(mode, ParamMode::ByValue | ParamMode::Out) { + self.candidate(mode_ordinal, CanonicalGraphError::InvalidGraph); + } + self.scan_scalar(value, &mut references); + } + self.scan_ty(*ret, &mut references); + self.scan_borrow_summary(return_borrow, params.len()); + let region_ordinal = self.scan_region_summary(return_region, params.len()); + if !summaries_agree(return_borrow, return_region) { + self.candidate(region_ordinal, CanonicalGraphError::InvalidSummary); + } + } + } + let end_ordinal = self.field_ordinal(); + self.end_ordinals.insert(node, end_ordinal); + references.reverse(); + self.pending.extend(references); + } + + fn collect_cross_node_candidates(&mut self) { + let mut nominal_sources = HashMap::new(); + let mut tuples: HashMap, Node> = HashMap::new(); + let mut known_shapes = HashSet::new(); + let order = self.order.clone(); + let view = self.view; + for node in order { + let Some(&end_ordinal) = self.end_ordinals.get(&node) else { + continue; + }; + match node { + Node::Struct(id) => { + if let Some(definition) = view.structs.get(id as usize) { + if let Some(error) = Self::compare_nominal( + view, + node, + &definition.source_name, + &mut nominal_sources, + &mut known_shapes, + ) { + self.candidate(end_ordinal, error); + } + } + } + Node::Enum(id) => { + if let Some(definition) = view.enums.get(id as usize) { + if let Some(error) = Self::compare_nominal( + view, + node, + &definition.source_name, + &mut nominal_sources, + &mut known_shapes, + ) { + self.candidate(end_ordinal, error); + } + } + } + Node::Tuple(id) => { + if let Some(definition) = view.tuples.get(id as usize) { + if tuples.insert(definition.elems.clone(), node).is_some() { + self.candidate(end_ordinal, CanonicalGraphError::DuplicateMember); + } + } + } + Node::Tagged(_) | Node::Fn(_) => {} + } + } + } + + fn compare_nominal( + view: CanonicalTypeView<'a>, + node: Node, + source_name: &'a str, + nominal_sources: &mut HashMap<&'a str, Node>, + known_shapes: &mut HashSet<(Node, Node)>, + ) -> Option { + if source_name.is_empty() || source_name.as_bytes().contains(&0) { + return None; + } + let Some(&first) = nominal_sources.get(source_name) else { + nominal_sources.insert(source_name, node); + return None; + }; + let same_kind = std::mem::discriminant(&first) == std::mem::discriminant(&node); + let same_shape = same_kind && source_shape_equal(&view, first, node, known_shapes); + Some(if same_shape { + CanonicalGraphError::DuplicateMember + } else { + CanonicalGraphError::InvalidGraph + }) + } + + fn scan_scalar(&mut self, value: Scalar, references: &mut Vec) { + let ordinal = self.field_ordinal(); + match value { + Scalar::Struct(id) | Scalar::DynStructArray(id) | Scalar::Soa(id) => { + self.scan_reference(Node::Struct(id), ordinal, references) + } + Scalar::Enum(id) => self.scan_reference(Node::Enum(id), ordinal, references), + Scalar::Tagged(id) => self.scan_reference(Node::Tagged(id), ordinal, references), + Scalar::Fn(id) => self.scan_reference(Node::Fn(id), ordinal, references), + Scalar::Int(value) if validate_int(value.signed, value.bits).is_err() => { + self.candidate(ordinal, CanonicalGraphError::InvalidWidth) + } + Scalar::Float(value) if validate_float(value.bits).is_err() => { + self.candidate(ordinal, CanonicalGraphError::InvalidWidth) + } + Scalar::DynArray(value) | Scalar::Slice(value) if validate_prim(value).is_err() => { + self.candidate(ordinal, CanonicalGraphError::InvalidWidth) + } + Scalar::Param(_) => self.candidate(ordinal, CanonicalGraphError::InvalidGraph), + _ => {} + } + } + + fn scan_ty(&mut self, value: Ty, references: &mut Vec) { + let ordinal = self.field_ordinal(); + match value { + Ty::Option(value) + | Ty::Box(value) + | Ty::Slice(value) + | Ty::DynArray(value) + | Ty::ArrayBuilder(value) + | Ty::Task(value) => self.scan_scalar(value, references), + Ty::Result(ok, err) => { + self.scan_scalar(ok, references); + self.scan_scalar(err, references); + } + Ty::Array(value, _) => { + self.scan_scalar(value, references); + self.field_ordinal(); + } + Ty::Vec(value, lanes) | Ty::Mask(value, lanes) => { + let scalar_ordinal = self.next_ordinal; + self.scan_scalar(value, references); + let lanes_ordinal = self.field_ordinal(); + if !matches!(value, Scalar::Int(_) | Scalar::Float(_)) { + self.candidate(scalar_ordinal, CanonicalGraphError::InvalidWidth); + } + if !matches!(lanes, 2 | 4 | 8 | 16) { + self.candidate(lanes_ordinal, CanonicalGraphError::InvalidWidth); + } + } + Ty::StructArray(id, _) | Ty::DictEncoded(id, _) => { + self.scan_reference(Node::Struct(id), ordinal, references); + self.field_ordinal(); + } + Ty::DynStructArray(id, _) => { + self.scan_reference(Node::Struct(id), ordinal, references); + self.field_ordinal(); + } + Ty::Tagged(id) => self.scan_reference(Node::Tagged(id), ordinal, references), + Ty::Soa(id) | Ty::JsonScanner(id) | Ty::Struct(id) => { + self.scan_reference(Node::Struct(id), ordinal, references) + } + Ty::Tuple(id) => self.scan_reference(Node::Tuple(id), ordinal, references), + Ty::Fn(id) => self.scan_reference(Node::Fn(id), ordinal, references), + Ty::Enum(id) => self.scan_reference(Node::Enum(id), ordinal, references), + Ty::Int(value) if validate_int(value.signed, value.bits).is_err() => { + self.candidate(ordinal, CanonicalGraphError::InvalidWidth) + } + Ty::Float(value) if validate_float(value.bits).is_err() => { + self.candidate(ordinal, CanonicalGraphError::InvalidWidth) + } + Ty::DynSliceArray(value) if validate_prim(value).is_err() => { + self.candidate(ordinal, CanonicalGraphError::InvalidWidth) + } + Ty::Param(_) | Ty::IntVar(_) | Ty::FloatVar(_) | Ty::Error => { + self.candidate(ordinal, CanonicalGraphError::InvalidGraph); + } + _ => {} + } + } + + fn scan_reference(&mut self, node: Node, ordinal: u64, references: &mut Vec) { + if self.view.source_shape_node(node).is_some() { + references.push(node); + } else { + self.candidate(ordinal, CanonicalGraphError::MissingReference); + } + } + + fn scan_borrow_summary(&mut self, summary: &hir::ReturnBorrowSummary, params: usize) -> u64 { + match summary { + hir::ReturnBorrowSummary::None => self.field_ordinal(), + hir::ReturnBorrowSummary::Roots { + params: roots, + captures, + } => self.scan_roots(roots, captures, params), + } + } + + fn scan_region_summary(&mut self, summary: &hir::ReturnRegionSummary, params: usize) -> u64 { + match summary { + hir::ReturnRegionSummary::None => self.field_ordinal(), + hir::ReturnRegionSummary::Roots { + params: roots, + captures, + } => self.scan_roots(roots, captures, params), + } + } + + fn scan_roots(&mut self, roots: &[u32], captures: &[u32], params: usize) -> u64 { + let summary_ordinal = self.field_ordinal(); + let count_ordinal = self.field_ordinal(); + self.validate_count(roots.len(), count_ordinal); + if roots.is_empty() { + self.candidate(count_ordinal, CanonicalGraphError::InvalidSummary); + } + let mut previous = None; + for &root in roots { + let ordinal = self.field_ordinal(); + if previous.is_some_and(|value| value >= root) || root as usize >= params { + self.candidate(ordinal, CanonicalGraphError::InvalidSummary); + } + previous = Some(root); + } + let captures_count = self.field_ordinal(); + self.validate_count(captures.len(), captures_count); + if !captures.is_empty() { + self.candidate(captures_count, CanonicalGraphError::InvalidSummary); + } + for _ in captures { + self.field_ordinal(); + } + summary_ordinal + } + + fn validate_source_name(&mut self, value: &str, ordinal: u64) { + if value.as_bytes().contains(&0) { + self.candidate(ordinal, CanonicalGraphError::EmbeddedNul); + } + if value.is_empty() { + self.candidate(ordinal, CanonicalGraphError::InvalidGraph); + } + } + + fn validate_identifier(&mut self, value: &str, ordinal: u64) { + if value.as_bytes().contains(&0) { + self.candidate(ordinal, CanonicalGraphError::EmbeddedNul); + } + if !identifier_is_valid(value) { + self.candidate(ordinal, CanonicalGraphError::InvalidGraph); + } + } + + fn validate_count(&mut self, len: usize, ordinal: u64) { + if u32::try_from(len).is_err() { + self.candidate(ordinal, CanonicalGraphError::InvalidCount); + } + } + + fn field_ordinal(&mut self) -> u64 { + let ordinal = self.next_ordinal; + self.next_ordinal = self.next_ordinal.saturating_add(1); + ordinal + } + + fn candidate(&mut self, ordinal: u64, error: CanonicalGraphError) { + self.candidates.push(ErrorCandidate { + ordinal, + tie_rank: error_tie_rank(error), + error, + }); + } +} + +fn identifier_is_valid(value: &str) -> bool { + let mut bytes = value.bytes(); + bytes + .next() + .is_some_and(|byte| byte == b'_' || byte.is_ascii_alphabetic()) + && bytes.all(|byte| byte == b'_' || byte.is_ascii_alphanumeric()) +} + +fn summaries_agree(borrow: &hir::ReturnBorrowSummary, region: &hir::ReturnRegionSummary) -> bool { + match (borrow, region) { + (hir::ReturnBorrowSummary::None, hir::ReturnRegionSummary::None) => true, + ( + hir::ReturnBorrowSummary::Roots { + params: borrow_params, + captures: borrow_captures, + }, + hir::ReturnRegionSummary::Roots { + params: region_params, + captures: region_captures, + }, + ) => borrow_params == region_params && borrow_captures == region_captures, + _ => false, + } +} + +fn error_tie_rank(error: CanonicalGraphError) -> u8 { + match error { + CanonicalGraphError::EmbeddedNul => 0, + CanonicalGraphError::InvalidWidth | CanonicalGraphError::InvalidCount => 1, + CanonicalGraphError::InvalidGraph => 2, + CanonicalGraphError::DuplicateMember => 3, + CanonicalGraphError::MissingReference => 4, + CanonicalGraphError::InvalidSummary => 5, + } +} + +fn validate_int(_signed: bool, bits: u8) -> Result<(), CanonicalGraphError> { + if matches!(bits, 8 | 16 | 32 | 64) { + Ok(()) + } else { + Err(CanonicalGraphError::InvalidWidth) + } +} + +fn validate_float(bits: u8) -> Result<(), CanonicalGraphError> { + if matches!(bits, 32 | 64) { + Ok(()) + } else { + Err(CanonicalGraphError::InvalidWidth) + } +} + +fn validate_prim(value: PrimScalar) -> Result<(), CanonicalGraphError> { + match value { + PrimScalar::Int(value) => validate_int(value.signed, value.bits), + PrimScalar::Float(value) => validate_float(value.bits), + _ => Ok(()), + } +} + +fn canonical_type_bytes(graph: &ValidatedGraph<'_>) -> Result, CanonicalGraphError> { + let classes = stable_classes(graph)?; + let mut representative = HashMap::new(); + for &node in &graph.order { + let class = classes + .get(&node) + .copied() + .ok_or(CanonicalGraphError::MissingReference)?; + representative.entry(class).or_insert(node); + } + + let mut class_order = Vec::new(); + let mut class_ordinals = HashMap::new(); + let mut pending = type_nodes(graph.root); + pending.reverse(); + while let Some(node) = pending.pop() { + let class = classes + .get(&node) + .copied() + .ok_or(CanonicalGraphError::MissingReference)?; + if class_ordinals.contains_key(&class) { + continue; + } + class_ordinals.insert( + class, + u32::try_from(class_order.len()).map_err(|_| CanonicalGraphError::InvalidCount)?, + ); + class_order.push(class); + let node = representative + .get(&class) + .copied() + .ok_or(CanonicalGraphError::MissingReference)?; + let mut children = node_children(graph.view, node)?; + children.reverse(); + pending.extend(children); + } + + let mut out = Vec::new(); + out.push(1); + out.extend(checked_count(class_order.len())?.to_le_bytes()); + let ordinal = |node: Node| { + let class = classes + .get(&node) + .ok_or(CanonicalGraphError::MissingReference)?; + class_ordinals + .get(class) + .copied() + .ok_or(CanonicalGraphError::MissingReference) + }; + for class in class_order { + let node = representative + .get(&class) + .copied() + .ok_or(CanonicalGraphError::MissingReference)?; + encode_node(&mut out, graph.view, node, &ordinal)?; + } + ty(&mut out, graph.root, &ordinal)?; + Ok(out) +} + +fn stable_classes(graph: &ValidatedGraph<'_>) -> Result, CanonicalGraphError> { + stable_classes_and_rounds(graph).map(|(classes, _)| classes) +} + +fn stable_classes_and_rounds( + graph: &ValidatedGraph<'_>, +) -> Result<(HashMap, usize), CanonicalGraphError> { + stable_classes_observed(graph, &mut ()) +} + +fn stable_classes_observed( + graph: &ValidatedGraph<'_>, + observer: &mut O, +) -> Result<(HashMap, usize), CanonicalGraphError> { + let anonymous_nodes = graph + .order + .iter() + .filter(|node| matches!(node, Node::Tuple(_) | Node::Tagged(_) | Node::Fn(_))) + .count(); + let mut classes = assign_classes( + graph, + graph + .order + .iter() + .map(|&node| initial_signature(graph.view, node)) + .collect::, _>>()?, + observer, + )?; + for round in 1..=anonymous_nodes + 1 { + let signatures = graph + .order + .iter() + .map(|&node| { + let mut bytes = Vec::new(); + encode_node(&mut bytes, graph.view, node, &|child| { + classes + .get(&child) + .copied() + .ok_or(CanonicalGraphError::MissingReference) + })?; + Ok(bytes) + }) + .collect::, CanonicalGraphError>>()?; + let next = assign_classes(graph, signatures, observer)?; + if same_partition(&graph.order, &classes, &next) { + return Ok((next, round)); + } + classes = next; + } + Err(CanonicalGraphError::InvalidGraph) +} + +fn assign_classes( + graph: &ValidatedGraph<'_>, + signatures: Vec>, + observer: &mut impl RefinementObserver, +) -> Result, CanonicalGraphError> { + if signatures.len() != graph.order.len() { + return Err(CanonicalGraphError::InvalidGraph); + } + for signature in &signatures { + observer.signature(signature.len()); + } + let mut unique = signatures.clone(); + unique.sort_by(|left, right| observer.compare(left, right)); + unique.dedup(); + let mut class_by_signature = HashMap::new(); + for (class, signature) in unique.into_iter().enumerate() { + class_by_signature.insert(signature, checked_count(class)?); + } + graph + .order + .iter() + .copied() + .zip(signatures) + .map(|(node, signature)| { + class_by_signature + .get(&signature) + .copied() + .map(|class| (node, class)) + .ok_or(CanonicalGraphError::InvalidGraph) + }) + .collect() +} + +trait RefinementObserver { + fn signature(&mut self, _bytes: usize) {} + + fn compare(&mut self, left: &[u8], right: &[u8]) -> std::cmp::Ordering { + left.cmp(right) + } +} + +impl RefinementObserver for () {} + +fn same_partition(order: &[Node], left: &HashMap, right: &HashMap) -> bool { + let mut left_to_right = HashMap::new(); + let mut right_to_left = HashMap::new(); + order.iter().all(|node| { + let (Some(&left), Some(&right)) = (left.get(node), right.get(node)) else { + return false; + }; + if left_to_right + .get(&left) + .is_some_and(|mapped| *mapped != right) + || right_to_left + .get(&right) + .is_some_and(|mapped| *mapped != left) + { + return false; + } + left_to_right.insert(left, right); + right_to_left.insert(right, left); + true + }) +} + +fn initial_signature( + view: CanonicalTypeView<'_>, + node: Node, +) -> Result, CanonicalGraphError> { + let mut out = vec![match node { + Node::Struct(_) => 0, + Node::Enum(_) => 1, + Node::Tuple(_) => 2, + Node::Tagged(_) => 3, + Node::Fn(_) => 4, + }]; + match node { + Node::Struct(id) => text( + &mut out, + &view + .structs + .get(id as usize) + .ok_or(CanonicalGraphError::MissingReference)? + .source_name, + )?, + Node::Enum(id) => text( + &mut out, + &view + .enums + .get(id as usize) + .ok_or(CanonicalGraphError::MissingReference)? + .source_name, + )?, + Node::Tuple(_) | Node::Tagged(_) | Node::Fn(_) => {} + } + Ok(out) +} + +fn encode_node( + out: &mut Vec, + view: CanonicalTypeView<'_>, + node: Node, + ordinal: &impl Fn(Node) -> Result, +) -> Result<(), CanonicalGraphError> { + append_transactional(out, |out| match node { + Node::Struct(id) => { + let definition = view + .structs + .get(id as usize) + .ok_or(CanonicalGraphError::MissingReference)?; + out.push(0); + text(out, &definition.source_name)?; + match definition.align { + None => out.push(0), + Some(align) => { + out.push(1); + out.extend(align.to_le_bytes()); + } + } + out.push(u8::from(definition.c_repr)); + out.extend(checked_count(definition.fields.len())?.to_le_bytes()); + for field in &definition.fields { + text(out, &field.name)?; + ty(out, field.ty, ordinal)?; + } + Ok(()) + } + Node::Enum(id) => { + let definition = view + .enums + .get(id as usize) + .ok_or(CanonicalGraphError::MissingReference)?; + out.push(1); + text(out, &definition.source_name)?; + out.extend(checked_count(definition.variants.len())?.to_le_bytes()); + for variant in &definition.variants { + text(out, &variant.name)?; + out.extend(variant.field_base.to_le_bytes()); + out.extend(checked_count(variant.payload.len())?.to_le_bytes()); + for &value in &variant.payload { + scalar(out, value, ordinal)?; + } + } + Ok(()) + } + Node::Tuple(id) => { + let definition = view + .tuples + .get(id as usize) + .ok_or(CanonicalGraphError::MissingReference)?; + out.push(2); + out.extend(checked_count(definition.elems.len())?.to_le_bytes()); + for &value in &definition.elems { + scalar(out, value, ordinal)?; + } + Ok(()) + } + Node::Tagged(id) => { + let definition = view + .tagged_types + .get(id as usize) + .ok_or(CanonicalGraphError::MissingReference)?; + out.push(3); + match definition { + hir::TaggedType::Option(value) => { + out.push(0); + scalar(out, *value, ordinal)?; + } + hir::TaggedType::Result(ok, err) => { + out.push(1); + scalar(out, *ok, ordinal)?; + scalar(out, *err, ordinal)?; + } + } + Ok(()) + } + Node::Fn(id) => { + let definition = view + .fn_types + .get(id as usize) + .ok_or(CanonicalGraphError::MissingReference)?; + out.push(4); + out.extend(checked_count(definition.params.len())?.to_le_bytes()); + for &(mode, value) in &definition.params { + encode_param_mode(out, mode)?; + scalar(out, value, ordinal)?; + } + ty(out, definition.ret, ordinal)?; + encode_borrow_summary(out, &definition.return_borrow)?; + encode_region_summary(out, &definition.return_region)?; + Ok(()) + } + }) +} + +fn encode_borrow_summary( + out: &mut Vec, + value: &hir::ReturnBorrowSummary, +) -> Result<(), CanonicalGraphError> { + match value { + hir::ReturnBorrowSummary::None => out.push(0), + hir::ReturnBorrowSummary::Roots { params, captures } => { + out.push(1); + encode_roots(out, params, captures)?; + } + } + Ok(()) +} + +fn encode_region_summary( + out: &mut Vec, + value: &hir::ReturnRegionSummary, +) -> Result<(), CanonicalGraphError> { + match value { + hir::ReturnRegionSummary::None => out.push(0), + hir::ReturnRegionSummary::Roots { params, captures } => { + out.push(1); + encode_roots(out, params, captures)?; + } + } + Ok(()) +} + +fn encode_roots( + out: &mut Vec, + params: &[u32], + captures: &[u32], +) -> Result<(), CanonicalGraphError> { + out.extend(checked_count(params.len())?.to_le_bytes()); + for value in params { + out.extend(value.to_le_bytes()); + } + out.extend(checked_count(captures.len())?.to_le_bytes()); + for value in captures { + out.extend(value.to_le_bytes()); + } + Ok(()) +} + +fn node_children( + view: CanonicalTypeView<'_>, + node: Node, +) -> Result, CanonicalGraphError> { + let shape = view + .source_shape_node(node) + .ok_or(CanonicalGraphError::MissingReference)?; + let mut children = Vec::new(); + match shape { + SourceShapeNode::Struct { fields, .. } => { + for field in fields { + children.extend(type_nodes(field.ty)); + } + } + SourceShapeNode::Enum { variants, .. } => { + for variant in variants { + for &value in &variant.payload { + children.extend(scalar_nodes(value)); + } + } + } + SourceShapeNode::Tuple { elems } => { + for &value in elems { + children.extend(scalar_nodes(value)); + } + } + SourceShapeNode::Tagged(value) => match value { + hir::TaggedType::Option(value) => children.extend(scalar_nodes(*value)), + hir::TaggedType::Result(ok, err) => { + children.extend(scalar_nodes(*ok)); + children.extend(scalar_nodes(*err)); + } + }, + SourceShapeNode::Function { params, ret, .. } => { + for &(_, value) in params { + children.extend(scalar_nodes(value)); + } + children.extend(type_nodes(*ret)); + } + } + Ok(children) +} + +fn scalar_nodes(value: Scalar) -> Vec { + match value { + Scalar::Struct(id) | Scalar::DynStructArray(id) | Scalar::Soa(id) => { + vec![Node::Struct(id)] + } + Scalar::Enum(id) => vec![Node::Enum(id)], + Scalar::Tagged(id) => vec![Node::Tagged(id)], + Scalar::Fn(id) => vec![Node::Fn(id)], + _ => Vec::new(), + } +} + +fn type_nodes(value: Ty) -> Vec { + match value { + Ty::Option(value) + | Ty::Box(value) + | Ty::Slice(value) + | Ty::DynArray(value) + | Ty::ArrayBuilder(value) + | Ty::Task(value) + | Ty::Array(value, _) + | Ty::Vec(value, _) + | Ty::Mask(value, _) => scalar_nodes(value), + Ty::Result(ok, err) => { + let mut nodes = scalar_nodes(ok); + nodes.extend(scalar_nodes(err)); + nodes + } + Ty::Tagged(id) => vec![Node::Tagged(id)], + Ty::StructArray(id, _) + | Ty::DynStructArray(id, _) + | Ty::Soa(id) + | Ty::JsonScanner(id) + | Ty::DictEncoded(id, _) + | Ty::Struct(id) => vec![Node::Struct(id)], + Ty::Tuple(id) => vec![Node::Tuple(id)], + Ty::Fn(id) => vec![Node::Fn(id)], + Ty::Enum(id) => vec![Node::Enum(id)], + _ => Vec::new(), + } +} + #[allow(dead_code)] fn checked_count(len: usize) -> Result { u32::try_from(len).map_err(|_| CanonicalGraphError::InvalidCount) @@ -338,9 +1320,101 @@ fn ty( #[cfg(test)] mod tests { + use std::cmp::Ordering; + use align_sema::{FloatTy, IntTy}; use super::*; + use crate::validate_hir_tests::baseline_program; + + fn function_defs(program: &hir::Program) -> Vec { + program + .fn_types + .iter() + .map(|definition| FunctionTypeDef { + params: definition.params.clone(), + ret: definition.ret, + return_borrow: definition.return_borrow.clone(), + return_region: definition.return_region.clone(), + }) + .collect() + } + + fn validate(root: Ty, program: &hir::Program) -> Result, CanonicalGraphError> { + let fn_types = function_defs(program); + let graph = ValidatedGraph::new( + root, + CanonicalTypeView { + structs: &program.structs, + enums: &program.enums, + tuples: &program.tuples, + tagged_types: &program.tagged_types, + fn_types: &fn_types, + }, + )?; + assert_eq!(graph.root, root); + assert_eq!(graph.view.structs.len(), program.structs.len()); + Ok(graph.order) + } + + fn canonical(root: Ty, program: &hir::Program) -> Result, CanonicalGraphError> { + let fn_types = function_defs(program); + let graph = ValidatedGraph::new( + root, + CanonicalTypeView { + structs: &program.structs, + enums: &program.enums, + tuples: &program.tuples, + tagged_types: &program.tagged_types, + fn_types: &fn_types, + }, + )?; + canonical_type_bytes(&graph) + } + + #[derive(Default)] + struct RefinementMetrics { + signature_bytes: usize, + comparisons: usize, + compared_bytes: usize, + } + + impl RefinementObserver for RefinementMetrics { + fn signature(&mut self, bytes: usize) { + self.signature_bytes += bytes; + } + + fn compare(&mut self, left: &[u8], right: &[u8]) -> Ordering { + self.comparisons += 1; + let common = left + .iter() + .zip(right) + .take_while(|(left, right)| left == right) + .count(); + self.compared_bytes += common + usize::from(common < left.len().min(right.len())); + left.cmp(right) + } + } + + fn observed_refinement( + root: Ty, + program: &hir::Program, + ) -> Result<(usize, RefinementMetrics), CanonicalGraphError> { + let fn_types = function_defs(program); + let graph = ValidatedGraph::new( + root, + CanonicalTypeView { + structs: &program.structs, + enums: &program.enums, + tuples: &program.tuples, + tagged_types: &program.tagged_types, + fn_types: &fn_types, + }, + )?; + let mut metrics = RefinementMetrics::default(); + let (_, rounds) = stable_classes_observed(&graph, &mut metrics)?; + Ok((rounds, metrics)) + } fn i(bits: u8) -> IntTy { IntTy { bits, signed: true } @@ -403,6 +1477,388 @@ mod tests { }; } + #[test] + fn canonical_graph_validation() { + let program = baseline_program(); + assert_eq!( + validate(Ty::Struct(0), &program).unwrap(), + [Node::Struct(0)] + ); + assert_eq!( + validate(Ty::Struct(u32::MAX), &program), + Err(CanonicalGraphError::MissingReference) + ); + + let mut invalid = program.clone(); + invalid.structs[0].fields[0].name = "bad-name".into(); + assert_eq!( + validate(Ty::Struct(0), &invalid), + Err(CanonicalGraphError::InvalidGraph) + ); + + let mut duplicate = program.clone(); + let repeated = duplicate.structs[0].fields[0].clone(); + duplicate.structs[0].fields.push(repeated); + assert_eq!( + validate(Ty::Struct(0), &duplicate), + Err(CanonicalGraphError::DuplicateMember) + ); + + let mut unreachable = program.clone(); + let mut bad = unreachable.structs[0].clone(); + bad.source_name.clear(); + unreachable.structs.push(bad); + assert!(validate(Ty::Bool, &unreachable).unwrap().is_empty()); + + let mut all_nodes = program.clone(); + all_nodes.enums[0].variants[0].field_base = 0; + assert_eq!( + validate(Ty::Enum(0), &all_nodes), + Err(CanonicalGraphError::InvalidGraph) + ); + assert_eq!( + validate(Ty::Tuple(u32::MAX), &all_nodes), + Err(CanonicalGraphError::MissingReference) + ); + assert_eq!( + validate(Ty::Tagged(u32::MAX), &all_nodes), + Err(CanonicalGraphError::MissingReference) + ); + assert_eq!( + validate(Ty::Fn(u32::MAX), &all_nodes), + Err(CanonicalGraphError::MissingReference) + ); + } + + #[test] + fn canonical_graph_validation_error_precedence() { + let base = baseline_program(); + let make_struct = |source_name: &str, fields: Vec<(&str, Ty)>| { + let mut definition = base.structs[0].clone(); + definition.source_name = source_name.into(); + definition.fields = fields + .into_iter() + .map(|(name, ty)| { + let mut field = base.structs[0].fields[0].clone(); + field.name = name.into(); + field.ty = ty; + field + }) + .collect(); + definition + }; + + let mut program = base.clone(); + program.structs = vec![make_struct("bad\0source", vec![("value", Ty::Int(i(24)))])]; + assert_eq!( + validate(Ty::Struct(0), &program), + Err(CanonicalGraphError::EmbeddedNul) + ); + + program.structs = vec![ + make_struct( + "Root", + vec![("first", Ty::Int(i(24))), ("child", Ty::Struct(1))], + ), + make_struct("bad\0child", vec![("value", Ty::Bool)]), + ]; + assert_eq!( + validate(Ty::Struct(0), &program), + Err(CanonicalGraphError::InvalidWidth) + ); + + program.structs = vec![make_struct( + "Root", + vec![("bad-name", Ty::Struct(u32::MAX))], + )]; + assert_eq!( + validate(Ty::Struct(0), &program), + Err(CanonicalGraphError::InvalidGraph) + ); + + program.structs = vec![ + make_struct( + "Root", + vec![("first", Ty::Struct(1)), ("second", Ty::Struct(2))], + ), + make_struct("Alias", vec![("value", Ty::Bool)]), + make_struct("Alias", vec![("value", Ty::Struct(u32::MAX))]), + ]; + assert_eq!( + validate(Ty::Struct(0), &program), + Err(CanonicalGraphError::MissingReference) + ); + + program.structs = vec![ + make_struct( + "Root", + vec![ + ("first", Ty::Struct(1)), + ("second", Ty::Struct(2)), + ("later", Ty::Struct(3)), + ], + ), + make_struct("Alias", vec![("value", Ty::Bool)]), + make_struct("Alias", vec![("value", Ty::Char)]), + make_struct("Later", vec![("bad-name", Ty::Bool)]), + ]; + assert_eq!( + validate(Ty::Struct(0), &program), + Err(CanonicalGraphError::InvalidGraph) + ); + + program.structs[2].fields[0].ty = Ty::Struct(3); + program.structs[3].fields[0].name = "bad-name".into(); + assert_eq!( + validate(Ty::Struct(0), &program), + Err(CanonicalGraphError::InvalidGraph) + ); + } + + #[test] + fn canonical_graph_validation_rejects_raw_duplicates() { + let mut nominal = baseline_program(); + let duplicate = nominal.structs[0].clone(); + let mut root = duplicate.clone(); + root.source_name = "Root".into(); + root.fields = vec![root.fields[0].clone()]; + let mut second = root.fields[0].clone(); + root.fields[0].name = "first".into(); + root.fields[0].ty = Ty::Struct(1); + second.name = "second".into(); + second.ty = Ty::Struct(2); + root.fields.push(second); + nominal.structs = vec![root, duplicate.clone(), duplicate]; + assert_eq!( + validate(Ty::Struct(0), &nominal), + Err(CanonicalGraphError::DuplicateMember) + ); + + let mut tuple = baseline_program(); + tuple.tuples.push(tuple.tuples[0].clone()); + tuple.structs[0].fields[0].ty = Ty::Tuple(0); + let mut second = tuple.structs[0].fields[0].clone(); + second.name = "second".into(); + second.ty = Ty::Tuple(1); + tuple.structs[0].fields.push(second); + assert_eq!( + validate(Ty::Struct(0), &tuple), + Err(CanonicalGraphError::DuplicateMember) + ); + } + + #[test] + fn canonical_graph_function_root_validation() { + let mut program = baseline_program(); + program.fn_types[0].params = vec![(ParamMode::ByValue, Scalar::Struct(0))]; + program.fn_types[0].ret = Ty::Option(Scalar::Struct(0)); + assert_eq!( + validate(Ty::Fn(0), &program).unwrap(), + [Node::Fn(0), Node::Struct(0)] + ); + + program.fn_types[0].return_borrow = hir::ReturnBorrowSummary::Roots { + params: vec![], + captures: vec![], + }; + assert_eq!( + validate(Ty::Fn(0), &program), + Err(CanonicalGraphError::InvalidSummary) + ); + } + + #[test] + fn canonical_graph_validation_raw_scan_is_linear() { + let mut program = baseline_program(); + let mut leaf = program.structs[0].clone(); + leaf.source_name = "Leaf".into(); + program.structs.push(leaf); + program.structs[0].fields[0].ty = Ty::Struct(1); + let mut second = program.structs[0].fields[0].clone(); + second.name = "other".into(); + program.structs[0].fields.push(second); + program.structs[1].fields[0].ty = Ty::Struct(1); + let order = validate(Ty::Struct(0), &program).unwrap(); + assert_eq!(order, [Node::Struct(0), Node::Struct(1)]); + let view = CanonicalTypeView { + structs: &program.structs, + enums: &program.enums, + tuples: &program.tuples, + tagged_types: &program.tagged_types, + fn_types: &[], + }; + let edges: usize = order + .iter() + .map(|&node| node_children(view, node).unwrap().len()) + .sum(); + assert_eq!((order.len(), edges), (2, 3)); + } + + #[test] + fn deep_canonical_graph_validation_is_stack_bounded() { + let mut program = baseline_program(); + program.structs.clear(); + for id in 0..4096u32 { + let mut definition = baseline_program().structs[0].clone(); + definition.source_name = format!("S{id}"); + definition.fields[0].ty = if id == 4095 { + Ty::Bool + } else { + Ty::Struct(id + 1) + }; + program.structs.push(definition); + } + let order = validate(Ty::Struct(0), &program).unwrap(); + assert_eq!(order.len(), 4096); + assert_eq!(order.first(), Some(&Node::Struct(0))); + assert_eq!(order.last(), Some(&Node::Struct(4095))); + } + + #[test] + fn canonical_graph_engine() { + let program = baseline_program(); + assert_eq!(canonical(Ty::Unit, &program).unwrap(), [1, 0, 0, 0, 0, 56]); + assert_eq!(canonical(Ty::Bool, &program).unwrap(), [1, 0, 0, 0, 0, 2]); + assert_eq!( + canonical(Ty::Int(i(64)), &program).unwrap(), + [1, 0, 0, 0, 0, 0, 1, 64] + ); + let bytes = canonical(Ty::Struct(0), &program).unwrap(); + assert_eq!(&bytes[..5], [1, 1, 0, 0, 0]); + assert_eq!(bytes.last(), Some(&0)); + } + + #[test] + fn canonical_graph_equivalence() { + let mut program = baseline_program(); + program.fn_types[0].params = vec![(ParamMode::ByValue, Scalar::Bool)]; + program.fn_types[0].ret = Ty::Unit; + program.fn_types.push(program.fn_types[0].clone()); + program.tuples[0].elems = vec![Scalar::Fn(0)]; + let mut equivalent = program.tuples[0].clone(); + equivalent.elems[0] = Scalar::Fn(1); + program.tuples.push(equivalent); + program.structs[0].fields.truncate(1); + program.structs[0].fields[0].ty = Ty::Tuple(0); + let mut second = program.structs[0].fields[0].clone(); + second.name = "other".into(); + second.ty = Ty::Tuple(1); + program.structs[0].fields.push(second); + let bytes = canonical(Ty::Struct(0), &program).unwrap(); + assert_eq!(&bytes[..5], [1, 3, 0, 0, 0]); + + let mut permuted = program.clone(); + permuted.tuples.swap(0, 1); + permuted.structs[0].fields[0].ty = Ty::Tuple(1); + permuted.structs[0].fields[1].ty = Ty::Tuple(0); + assert_eq!(canonical(Ty::Struct(0), &permuted).unwrap(), bytes); + + permuted.fn_types[1].params[0].0 = ParamMode::Out; + assert_ne!(canonical(Ty::Struct(0), &permuted).unwrap(), bytes); + + let mut cycles = baseline_program(); + cycles.fn_types[0].params = vec![(ParamMode::ByValue, Scalar::Fn(0))]; + cycles.fn_types[0].ret = Ty::Unit; + let mut first = cycles.fn_types[0].clone(); + first.params[0].1 = Scalar::Fn(2); + let mut second = cycles.fn_types[0].clone(); + second.params[0].1 = Scalar::Fn(1); + cycles.fn_types.extend([first, second]); + assert_eq!( + canonical(Ty::Fn(0), &cycles).unwrap(), + canonical(Ty::Fn(1), &cycles).unwrap() + ); + cycles.fn_types[2].params[0].0 = ParamMode::Out; + assert_ne!( + canonical(Ty::Fn(0), &cycles).unwrap(), + canonical(Ty::Fn(1), &cycles).unwrap() + ); + } + + #[test] + fn canonical_graph_refinement_round_bound() { + const ANONYMOUS_NODES: usize = 128; + let mut program = baseline_program(); + program.fn_types.clear(); + for id in 0..ANONYMOUS_NODES { + let mut definition = baseline_program().fn_types[0].clone(); + definition.params = vec![( + ParamMode::ByValue, + if id + 1 == ANONYMOUS_NODES { + Scalar::Bool + } else { + Scalar::Fn((id + 1) as u32) + }, + )]; + definition.ret = Ty::Unit; + definition.return_borrow = hir::ReturnBorrowSummary::None; + definition.return_region = hir::ReturnRegionSummary::None; + program.fn_types.push(definition); + } + let (rounds, _) = observed_refinement(Ty::Fn(0), &program).unwrap(); + assert!(rounds > 1); + assert!(rounds <= ANONYMOUS_NODES + 1); + } + + #[test] + fn canonical_graph_signature_sort_bound() { + const FUNCTIONS: usize = 96; + const PARAMS: usize = 192; + let mut program = baseline_program(); + program.fn_types.clear(); + program.tuples[0].elems = (0..FUNCTIONS).map(|id| Scalar::Fn(id as u32)).collect(); + for id in 0..FUNCTIONS { + let mut definition = baseline_program().fn_types[0].clone(); + definition.params = vec![(ParamMode::ByValue, Scalar::Bool); PARAMS]; + definition.params.push(( + ParamMode::ByValue, + if id % 2 == 0 { + Scalar::Char + } else { + Scalar::Unit + }, + )); + definition.ret = Ty::Unit; + definition.return_borrow = hir::ReturnBorrowSummary::None; + definition.return_region = hir::ReturnRegionSummary::None; + program.fn_types.push(definition); + } + let (_, metrics) = observed_refinement(Ty::Tuple(0), &program).unwrap(); + assert!(metrics.signature_bytes > FUNCTIONS * PARAMS); + assert!(metrics.comparisons >= FUNCTIONS); + assert!(metrics.compared_bytes > metrics.comparisons * PARAMS); + } + + #[test] + fn deep_canonical_graph_is_stack_bounded() { + let mut program = baseline_program(); + program.structs.clear(); + for id in 0..4096u32 { + let mut definition = baseline_program().structs[0].clone(); + definition.source_name = format!("S{id}"); + definition.fields[0].ty = if id == 4095 { + Ty::Bool + } else { + Ty::Struct(id + 1) + }; + program.structs.push(definition); + } + let bytes = canonical(Ty::Struct(0), &program).unwrap(); + assert_eq!(&bytes[..5], [1, 0, 16, 0, 0]); + } + + #[test] + fn canonical_graph_function_root() { + let mut program = baseline_program(); + program.fn_types[0].params = vec![(ParamMode::ByValue, Scalar::Bool)]; + program.fn_types[0].ret = Ty::Unit; + let first = canonical(Ty::Fn(0), &program).unwrap(); + program.fn_types.push(program.fn_types[0].clone()); + assert_eq!(canonical(Ty::Fn(1), &program).unwrap(), first); + program.fn_types[1].params[0].0 = ParamMode::Out; + assert_ne!(canonical(Ty::Fn(1), &program).unwrap(), first); + } + #[test] fn canonical_field_codec_covers_every_primitive_and_scalar_tag() { cases!(encoded_prim; From ac1eb89c68402d9c108e176bc4f58affa8275697 Mon Sep 17 00:00:00 2001 From: sanohiro Date: Wed, 5 Aug 2026 12:33:12 +0900 Subject: [PATCH 3/7] feat(mir): retain canonical function types --- crates/align_codegen_llvm/src/lib.rs | 19 ++ crates/align_mir/src/canonical_graph.rs | 324 ++++++++++++++++++++++-- crates/align_mir/src/lib.rs | 43 +++- 3 files changed, 365 insertions(+), 21 deletions(-) diff --git a/crates/align_codegen_llvm/src/lib.rs b/crates/align_codegen_llvm/src/lib.rs index 4b7b3f22..7a7992e5 100644 --- a/crates/align_codegen_llvm/src/lib.rs +++ b/crates/align_codegen_llvm/src/lib.rs @@ -2441,6 +2441,11 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { "nested tagged type table is not compact, unique, and canonical".to_string(), )); } + if !align_mir::function_types_are_canonical(program) { + return Err(CodegenError::Lowering( + "function type table is not compact, unique, and canonical".to_string(), + )); + } Ok(()) } @@ -12352,6 +12357,7 @@ mod tests { structs, enums, tagged_types: vec![], + fn_types: vec![], tuples: vec![], }; emit_llvm_ir(&program, &BuildTarget::Baseline, false, &[], None) @@ -12422,6 +12428,7 @@ mod tests { structs: vec![], enums: vec![], tagged_types: vec![], + fn_types: vec![], tuples: vec![], }; let err = emit_llvm_ir(&program, &BuildTarget::Baseline, false, &[], None) @@ -12473,6 +12480,7 @@ mod tests { structs: vec![], enums: vec![], tagged_types: vec![], + fn_types: vec![], tuples: vec![], }; let err = emit_llvm_ir(&program, &BuildTarget::Baseline, false, &[], None) @@ -12520,6 +12528,7 @@ mod tests { structs: vec![], enums: vec![], tagged_types: vec![], + fn_types: vec![], tuples: vec![], }; let err = emit_llvm_ir(&program, &BuildTarget::Baseline, false, &[], None) @@ -12559,6 +12568,7 @@ mod tests { structs: vec![], enums: vec![], tagged_types: vec![hir::TaggedType::Option(payload)], + fn_types: vec![], tuples: vec![], }; for (payload, expected) in [ @@ -12609,6 +12619,7 @@ mod tests { structs, enums, tagged_types, + fn_types: vec![], tuples: vec![], }; let struct_cycle = program( @@ -12684,6 +12695,7 @@ mod tests { structs: vec![], enums: vec![], tagged_types: vec![], + fn_types: vec![], tuples: vec![], }; @@ -12932,6 +12944,7 @@ mod tests { structs: vec![], enums: vec![], tagged_types: vec![hir::TaggedType::Option(Scalar::String)], + fn_types: vec![], tuples: vec![], }; let ir = emit_llvm_ir(&program, &BuildTarget::Baseline, false, &[], None) @@ -12971,6 +12984,7 @@ mod tests { structs: vec![], enums: vec![], tagged_types: vec![], + fn_types: vec![], tuples: vec![TupleDef { elems: vec![Scalar::DynArray(align_sema::PrimScalar::String)], }], @@ -13017,6 +13031,7 @@ mod tests { }], enums: vec![], tagged_types: vec![], + fn_types: vec![], tuples: vec![TupleDef { elems: vec![Scalar::DynStructArray(0)], }], @@ -13063,6 +13078,7 @@ mod tests { structs: vec![], enums: vec![], tagged_types, + fn_types: vec![], tuples: vec![], }; let cases = [ @@ -13822,6 +13838,7 @@ mod tests { structs, enums: vec![], tagged_types: vec![], + fn_types: vec![], tuples: vec![], }; emit_llvm_ir(&program, &BuildTarget::Baseline, optimized, &[], None).unwrap() @@ -13872,6 +13889,7 @@ mod tests { structs: vec![], enums: vec![], tagged_types: vec![], + fn_types: vec![], tuples: vec![], }; emit_llvm_ir(&program, &BuildTarget::Baseline, false, &[], None).unwrap() @@ -13919,6 +13937,7 @@ mod tests { structs: vec![row], enums: vec![], tagged_types: vec![], + fn_types: vec![], tuples: vec![], }; emit_llvm_ir(&program, &BuildTarget::Baseline, optimized, &[], None).unwrap() diff --git a/crates/align_mir/src/canonical_graph.rs b/crates/align_mir/src/canonical_graph.rs index 68620b2d..666f792f 100644 --- a/crates/align_mir/src/canonical_graph.rs +++ b/crates/align_mir/src/canonical_graph.rs @@ -1,9 +1,10 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; use align_ast::ParamMode; -use align_sema::{hir, Layout, PrimScalar, Scalar, Ty}; +use align_sema::{Layout, PrimScalar, Scalar, Ty, hir}; -use super::source_shape::{source_shape_equal, SourceShapeNode, SourceShapeView}; +use super::source_shape::{SourceShapeNode, SourceShapeView, source_shape_equal}; +use super::{Program, function_embedded_types, remap_function_embedded_types}; #[derive(Clone, Debug)] pub struct FunctionTypeDef { @@ -15,7 +16,7 @@ pub struct FunctionTypeDef { #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[allow(dead_code)] -enum CanonicalGraphError { +pub(super) enum CanonicalGraphError { EmbeddedNul, InvalidWidth, InvalidCount, @@ -96,6 +97,14 @@ struct ValidatedGraph<'a> { impl<'a> ValidatedGraph<'a> { fn new(root: Ty, view: CanonicalTypeView<'a>) -> Result { + Self::new_many(root, std::slice::from_ref(&root), view) + } + + fn new_many( + root: Ty, + roots: &[Ty], + view: CanonicalTypeView<'a>, + ) -> Result { let mut validator = GraphValidator { view, pending: Vec::new(), @@ -105,10 +114,12 @@ impl<'a> ValidatedGraph<'a> { next_ordinal: 0, end_ordinals: HashMap::new(), }; - let mut roots = Vec::new(); - validator.scan_ty(root, &mut roots); - roots.reverse(); - validator.pending.extend(roots); + let mut references = Vec::new(); + for &root in roots { + validator.scan_ty(root, &mut references); + } + references.reverse(); + validator.pending.extend(references); while let Some(node) = validator.pending.pop() { validator.visit_node(node); } @@ -334,11 +345,7 @@ impl<'a> GraphValidator<'a> { }; let same_kind = std::mem::discriminant(&first) == std::mem::discriminant(&node); let same_shape = same_kind && source_shape_equal(&view, first, node, known_shapes); - Some(if same_shape { - CanonicalGraphError::DuplicateMember - } else { - CanonicalGraphError::InvalidGraph - }) + (!same_shape).then_some(CanonicalGraphError::InvalidGraph) } fn scan_scalar(&mut self, value: Scalar, references: &mut Vec) { @@ -578,6 +585,14 @@ fn validate_prim(value: PrimScalar) -> Result<(), CanonicalGraphError> { fn canonical_type_bytes(graph: &ValidatedGraph<'_>) -> Result, CanonicalGraphError> { let classes = stable_classes(graph)?; + canonical_type_bytes_with_classes(graph, graph.root, &classes) +} + +fn canonical_type_bytes_with_classes( + graph: &ValidatedGraph<'_>, + root: Ty, + classes: &HashMap, +) -> Result, CanonicalGraphError> { let mut representative = HashMap::new(); for &node in &graph.order { let class = classes @@ -589,7 +604,7 @@ fn canonical_type_bytes(graph: &ValidatedGraph<'_>) -> Result, Canonical let mut class_order = Vec::new(); let mut class_ordinals = HashMap::new(); - let mut pending = type_nodes(graph.root); + let mut pending = type_nodes(root); pending.reverse(); while let Some(node) = pending.pop() { let class = classes @@ -632,7 +647,7 @@ fn canonical_type_bytes(graph: &ValidatedGraph<'_>) -> Result, Canonical .ok_or(CanonicalGraphError::MissingReference)?; encode_node(&mut out, graph.view, node, &ordinal)?; } - ty(&mut out, graph.root, &ordinal)?; + ty(&mut out, root, &ordinal)?; Ok(out) } @@ -1318,6 +1333,226 @@ fn ty( }) } +pub(super) fn canonicalize_function_types( + program: &mut Program, +) -> Result<(), CanonicalGraphError> { + let roots = program_type_roots(program); + let view = CanonicalTypeView { + structs: &program.structs, + enums: &program.enums, + tuples: &program.tuples, + tagged_types: &program.tagged_types, + fn_types: &program.fn_types, + }; + let graph = ValidatedGraph::new_many(Ty::Unit, &roots, view)?; + let reachable: BTreeSet = graph + .order + .iter() + .filter_map(|node| match node { + Node::Fn(id) => Some(*id), + _ => None, + }) + .collect(); + let classes = stable_classes(&graph)?; + + let mut keyed = Vec::with_capacity(reachable.len()); + for old in reachable { + keyed.push(( + canonical_type_bytes_with_classes(&graph, Ty::Fn(old), &classes)?, + old, + )); + } + keyed.sort(); + + let mut remap = vec![None; program.fn_types.len()]; + let mut representatives = Vec::new(); + let mut previous: Option> = None; + for (bytes, old) in keyed { + let new_class = if previous.as_ref().is_some_and(|value| *value == bytes) { + representatives + .len() + .checked_sub(1) + .ok_or(CanonicalGraphError::InvalidGraph)? + } else { + previous = Some(bytes); + representatives.push(old); + representatives.len() - 1 + }; + let slot = remap + .get_mut(old as usize) + .ok_or(CanonicalGraphError::MissingReference)?; + *slot = Some(checked_count(new_class)?); + } + + let mut canonical = Vec::with_capacity(representatives.len()); + for old in representatives { + let mut definition = program + .fn_types + .get(old as usize) + .cloned() + .ok_or(CanonicalGraphError::MissingReference)?; + remap_ty_fn(&mut definition.ret, &remap); + for (_, scalar) in &mut definition.params { + remap_scalar_fn(scalar, &remap); + } + canonical.push(definition); + } + remap_program_function_types(program, &remap); + program.fn_types = canonical; + Ok(()) +} + +pub fn function_types_are_canonical(program: &Program) -> bool { + let roots = program_type_roots(program); + let definitions = function_type_facts(&program.fn_types); + let mut canonical = program.clone(); + canonicalize_function_types(&mut canonical).is_ok() + && roots == program_type_roots(&canonical) + && definitions == function_type_facts(&canonical.fn_types) +} + +fn function_type_facts( + definitions: &[FunctionTypeDef], +) -> Vec<( + Vec<(ParamMode, Scalar)>, + Ty, + hir::ReturnBorrowSummary, + hir::ReturnRegionSummary, +)> { + definitions + .iter() + .map(|definition| { + ( + definition.params.clone(), + definition.ret, + definition.return_borrow.clone(), + definition.return_region.clone(), + ) + }) + .collect() +} + +fn program_type_roots(program: &Program) -> Vec { + let mut roots = Vec::new(); + for definition in &program.structs { + roots.extend(definition.fields.iter().map(|field| field.ty)); + } + for definition in &program.enums { + for variant in &definition.variants { + roots.extend( + variant + .payload + .iter() + .copied() + .map(align_sema::scalar_to_ty), + ); + } + } + for definition in &program.tuples { + roots.extend( + definition + .elems + .iter() + .copied() + .map(align_sema::scalar_to_ty), + ); + } + for function in &program.fns { + roots.push(function.ret); + roots.extend(function.slots.iter().chain(&function.value_tys).copied()); + roots.extend(function_embedded_types(function)); + } + for function in &program.externs { + roots.push(function.ret); + roots.extend(function.params.iter().copied()); + } + for function in &program.imported_fns { + roots.push(function.ret); + roots.extend(function.params.iter().copied()); + } + roots +} + +fn remap_program_function_types(program: &mut Program, remap: &[Option]) { + for definition in &mut program.structs { + for field in &mut definition.fields { + remap_ty_fn(&mut field.ty, remap); + } + } + for definition in &mut program.enums { + for variant in &mut definition.variants { + for scalar in &mut variant.payload { + remap_scalar_fn(scalar, remap); + } + } + } + for definition in &mut program.tuples { + for scalar in &mut definition.elems { + remap_scalar_fn(scalar, remap); + } + } + for definition in &mut program.tagged_types { + match definition { + hir::TaggedType::Option(scalar) => remap_scalar_fn(scalar, remap), + hir::TaggedType::Result(ok, err) => { + remap_scalar_fn(ok, remap); + remap_scalar_fn(err, remap); + } + } + } + for function in &mut program.fns { + remap_ty_fn(&mut function.ret, remap); + for ty in function.slots.iter_mut().chain(&mut function.value_tys) { + remap_ty_fn(ty, remap); + } + remap_function_embedded_types(function, remap, remap_ty_fn); + } + for function in &mut program.externs { + remap_ty_fn(&mut function.ret, remap); + for ty in &mut function.params { + remap_ty_fn(ty, remap); + } + } + for function in &mut program.imported_fns { + remap_ty_fn(&mut function.ret, remap); + for ty in &mut function.params { + remap_ty_fn(ty, remap); + } + } +} + +fn remap_scalar_fn(value: &mut Scalar, remap: &[Option]) { + if let Scalar::Fn(id) = value + && let Some(Some(new)) = remap.get(*id as usize) + { + *id = *new; + } +} + +fn remap_ty_fn(value: &mut Ty, remap: &[Option]) { + match value { + Ty::Fn(id) => { + if let Some(Some(new)) = remap.get(*id as usize) { + *id = *new; + } + } + Ty::Option(value) + | Ty::Box(value) + | Ty::Slice(value) + | Ty::DynArray(value) + | Ty::ArrayBuilder(value) + | Ty::Task(value) + | Ty::Array(value, _) + | Ty::Vec(value, _) + | Ty::Mask(value, _) => remap_scalar_fn(value, remap), + Ty::Result(ok, err) => { + remap_scalar_fn(ok, remap); + remap_scalar_fn(err, remap); + } + _ => {} + } +} + #[cfg(test)] mod tests { use std::cmp::Ordering; @@ -1630,8 +1865,8 @@ mod tests { root.fields.push(second); nominal.structs = vec![root, duplicate.clone(), duplicate]; assert_eq!( - validate(Ty::Struct(0), &nominal), - Err(CanonicalGraphError::DuplicateMember) + validate(Ty::Struct(0), &nominal).unwrap(), + [Node::Struct(0), Node::Struct(1), Node::Struct(2)] ); let mut tuple = baseline_program(); @@ -1859,6 +2094,61 @@ mod tests { assert_ne!(canonical(Ty::Fn(1), &program).unwrap(), first); } + #[test] + fn canonical_function_type_remap() { + let hir = baseline_program(); + let mut first = function_defs(&hir)[0].clone(); + first.params = vec![(ParamMode::ByValue, Scalar::Bool)]; + let mut second = first.clone(); + second.params[0].1 = Scalar::Char; + let duplicate = first.clone(); + let mut unreachable = first.clone(); + unreachable.params[0].1 = Scalar::Unit; + + let mut structs = hir.structs.clone(); + structs[0].fields[0].ty = Ty::Fn(2); + structs[0].fields[1].ty = Ty::Tagged(0); + let mut enums = hir.enums.clone(); + enums[0].variants[0].payload = vec![Scalar::Fn(1)]; + enums[0].variants[1].field_base = 2; + let mut tuples = hir.tuples.clone(); + tuples[0].elems = vec![Scalar::Fn(2)]; + let mut program = Program { + fns: Vec::new(), + externs: Vec::new(), + imported_fns: Vec::new(), + link_libs: Vec::new(), + structs, + enums, + tagged_types: vec![hir::TaggedType::Result(Scalar::Fn(0), Scalar::Fn(1))], + fn_types: vec![first, second, duplicate, unreachable], + tuples, + }; + + canonicalize_function_types(&mut program).unwrap(); + assert_eq!(program.fn_types.len(), 2); + assert_eq!(program.structs[0].fields[0].ty, Ty::Fn(0)); + let Ty::Tagged(0) = program.structs[0].fields[1].ty else { + panic!("tagged function root must remain reachable"); + }; + let hir::TaggedType::Result(Scalar::Fn(first), Scalar::Fn(second)) = + program.tagged_types[0] + else { + panic!("tagged function references must be remapped"); + }; + assert_ne!(first, second); + assert_eq!(program.tuples[0].elems, [Scalar::Fn(0)]); + assert!(function_types_are_canonical(&program)); + + let mut non_compact = program.clone(); + non_compact.fn_types.push(non_compact.fn_types[0].clone()); + assert!(!function_types_are_canonical(&non_compact)); + + let mut missing = program.clone(); + missing.structs[0].fields[0].ty = Ty::Fn(u32::MAX); + assert!(!function_types_are_canonical(&missing)); + } + #[test] fn canonical_field_codec_covers_every_primitive_and_scalar_tag() { cases!(encoded_prim; diff --git a/crates/align_mir/src/lib.rs b/crates/align_mir/src/lib.rs index ce0a2b6b..c9697352 100644 --- a/crates/align_mir/src/lib.rs +++ b/crates/align_mir/src/lib.rs @@ -25,7 +25,7 @@ mod source_shape; mod validate_hir; mod runtime_key; -pub use canonical_graph::FunctionTypeDef; +pub use canonical_graph::{FunctionTypeDef, function_types_are_canonical}; pub use runtime_key::RuntimeKey; #[cfg(test)] @@ -119,6 +119,10 @@ pub struct Program { /// Concrete nested `Option` / `Result` layouts, indexed by [`Ty::Tagged`] / /// [`Scalar::Tagged`]. Lowering canonicalizes this to the reachable, id-independent closure. pub tagged_types: Vec, + /// Compact effect-free function signatures, indexed by [`Ty::Fn`] / [`Scalar::Fn`]. Lowering + /// removes unreachable/equivalent entries, sorts by canonical graph bytes, and remaps every + /// retained reference before MIR can reach hashing or codegen. + pub fn_types: Vec, /// Tuple layouts, indexed by the id in [`Ty::Tuple`]; codegen builds an anonymous LLVM /// struct type from each element list. pub tuples: Vec, @@ -1600,6 +1604,7 @@ fn empty_program() -> Program { structs: Vec::new(), enums: Vec::new(), tagged_types: Vec::new(), + fn_types: Vec::new(), tuples: Vec::new(), } } @@ -1667,8 +1672,22 @@ fn lower_program_unchecked( structs: program.structs.clone(), enums: program.enums.clone(), tagged_types: program.tagged_types.clone(), + fn_types: program + .fn_types + .iter() + .map(|definition| FunctionTypeDef { + params: definition.params.clone(), + ret: definition.ret, + return_borrow: definition.return_borrow.clone(), + return_region: definition.return_region.clone(), + }) + .collect(), tuples: program.tuples.clone(), }; + // Public lowering validates the complete HIR envelope before this point. The unchecked helper + // is also exercised directly by malformed-continuation owners, where an invalid function id + // must remain fail-closed inside its function rather than erase unrelated lowered bodies. + let _ = canonical_graph::canonicalize_function_types(&mut mir); canonicalize_tagged_types(&mut mir); mir } @@ -1860,6 +1879,12 @@ fn canonicalize_tagged_types(program: &mut Program) { collect_ty(ty, &program.tagged_types, &mut reachable); } } + for definition in &program.fn_types { + collect_ty(definition.ret, &program.tagged_types, &mut reachable); + for &(_, scalar) in &definition.params { + collect_scalar(scalar, &program.tagged_types, &mut reachable); + } + } enum ScalarKeyWork { Scalar(Scalar), @@ -1922,9 +1947,12 @@ fn canonicalize_tagged_types(program: &mut Program) { } } } - // Abstract entries and function-table ids are not a concrete, id-independent - // ABI key. - Scalar::Param(_) | Scalar::Fn(_) => return None, + Scalar::Fn(id) => { + key.push_str("fn:"); + key.push_str(&id.to_string()); + } + // Abstract entries are not a concrete, id-independent ABI key. + Scalar::Param(_) => return None, other => key.push_str(&format!("{other:?}")), }, } @@ -2029,6 +2057,12 @@ fn canonicalize_tagged_types(program: &mut Program) { remap_ty(ty, &remap); } } + for definition in &mut program.fn_types { + remap_ty(&mut definition.ret, &remap); + for (_, scalar) in &mut definition.params { + remap_scalar(scalar, &remap); + } + } program.tagged_types = canonical; } @@ -14988,6 +15022,7 @@ fn main() -> i32 = 0 Scalar::Bool, ), ], + fn_types: vec![], tuples: vec![], }; canonicalize_tagged_types(&mut program); From a2cc99e009cccdc2bd0ac7aa191ae83c2fb68763 Mon Sep 17 00:00:00 2001 From: sanohiro Date: Wed, 5 Aug 2026 12:43:46 +0900 Subject: [PATCH 4/7] feat(mir): expose canonical callable codecs --- crates/align_mir/src/canonical_graph.rs | 1118 ++++++++++++++++++++++- crates/align_mir/src/lib.rs | 5 +- 2 files changed, 1121 insertions(+), 2 deletions(-) diff --git a/crates/align_mir/src/canonical_graph.rs b/crates/align_mir/src/canonical_graph.rs index 666f792f..2571bff1 100644 --- a/crates/align_mir/src/canonical_graph.rs +++ b/crates/align_mir/src/canonical_graph.rs @@ -14,6 +14,77 @@ pub struct FunctionTypeDef { pub return_region: hir::ReturnRegionSummary, } +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct ProgramCall(Box); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProgramCallError { + Empty, + EmbeddedNul, + TooLong, +} + +impl ProgramCall { + pub fn try_from_logical(value: &str) -> Result { + if value.is_empty() { + return Err(ProgramCallError::Empty); + } + if value.as_bytes().contains(&0) { + return Err(ProgramCallError::EmbeddedNul); + } + if u32::try_from(value.len()).is_err() { + return Err(ProgramCallError::TooLong); + } + Ok(Self(value.into())) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct CanonicalTy(Box<[u8]>); + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct CanonicalFnAbi(Box<[u8]>); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CanonicalCodecError { + Truncated, + TrailingBytes, + UnsupportedVersion, + UnknownTag, + InvalidBool, + InvalidUtf8, + EmbeddedNul, + InvalidWidth, + InvalidCount, + MissingReference, + DuplicateMember, + NonCanonicalOrder, + InvalidSummary, + InvalidGraph, +} + +impl From for CanonicalCodecError { + fn from(value: CanonicalGraphError) -> Self { + match value { + CanonicalGraphError::EmbeddedNul => Self::EmbeddedNul, + CanonicalGraphError::InvalidWidth => Self::InvalidWidth, + CanonicalGraphError::InvalidCount => Self::InvalidCount, + CanonicalGraphError::MissingReference => Self::MissingReference, + CanonicalGraphError::DuplicateMember => Self::DuplicateMember, + CanonicalGraphError::InvalidSummary => Self::InvalidSummary, + CanonicalGraphError::InvalidGraph => Self::InvalidGraph, + } + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[allow(dead_code)] pub(super) enum CanonicalGraphError { @@ -399,10 +470,22 @@ impl<'a> GraphValidator<'a> { self.candidate(lanes_ordinal, CanonicalGraphError::InvalidWidth); } } - Ty::StructArray(id, _) | Ty::DictEncoded(id, _) => { + Ty::StructArray(id, _) => { self.scan_reference(Node::Struct(id), ordinal, references); self.field_ordinal(); } + Ty::DictEncoded(id, field) => { + self.scan_reference(Node::Struct(id), ordinal, references); + let field_ordinal = self.field_ordinal(); + if self + .view + .structs + .get(id as usize) + .is_some_and(|definition| field as usize >= definition.fields.len()) + { + self.candidate(field_ordinal, CanonicalGraphError::InvalidGraph); + } + } Ty::DynStructArray(id, _) => { self.scan_reference(Node::Struct(id), ordinal, references); self.field_ordinal(); @@ -651,6 +734,747 @@ fn canonical_type_bytes_with_classes( Ok(out) } +impl CanonicalTy { + pub fn from_program(root: Ty, program: &Program) -> Result { + let graph = ValidatedGraph::new(root, canonical_view(program))?; + Ok(Self(canonical_type_bytes(&graph)?.into_boxed_slice())) + } + + pub fn decode(bytes: &[u8]) -> Result { + let consumed = validate_canonical_type_bytes(bytes)?; + if consumed != bytes.len() { + return Err(CanonicalCodecError::TrailingBytes); + } + Ok(Self(bytes.into())) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl CanonicalFnAbi { + pub fn from_parts( + params: &[(ParamMode, Ty)], + ret: Ty, + borrow: &hir::ReturnBorrowSummary, + region: &hir::ReturnRegionSummary, + program: &Program, + ) -> Result { + let count = checked_count(params.len())?; + validate_function_summaries(borrow, region, params.len())?; + if params + .iter() + .any(|(mode, _)| !matches!(mode, ParamMode::ByValue | ParamMode::Out)) + { + return Err(CanonicalCodecError::InvalidGraph); + } + + let mut canonical_params = Vec::with_capacity(params.len()); + for &(mode, ty) in params { + canonical_params.push((mode, CanonicalTy::from_program(ty, program)?)); + } + let canonical_ret = CanonicalTy::from_program(ret, program)?; + let mut out = Vec::new(); + out.push(1); + out.extend(count.to_le_bytes()); + for (mode, ty) in canonical_params { + encode_param_mode(&mut out, mode)?; + out.extend(ty.as_bytes()); + } + out.extend(canonical_ret.as_bytes()); + encode_borrow_summary(&mut out, borrow)?; + encode_region_summary(&mut out, region)?; + Ok(Self(out.into_boxed_slice())) + } + + pub fn decode(bytes: &[u8]) -> Result { + validate_canonical_fn_abi_bytes(bytes)?; + Ok(Self(bytes.into())) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +fn canonical_view(program: &Program) -> CanonicalTypeView<'_> { + CanonicalTypeView { + structs: &program.structs, + enums: &program.enums, + tuples: &program.tuples, + tagged_types: &program.tagged_types, + fn_types: &program.fn_types, + } +} + +fn validate_function_summaries( + borrow: &hir::ReturnBorrowSummary, + region: &hir::ReturnRegionSummary, + params: usize, +) -> Result<(), CanonicalCodecError> { + fn valid(roots: &[u32], captures: &[u32], params: usize) -> bool { + u32::try_from(roots.len()).is_ok() + && u32::try_from(captures.len()).is_ok() + && !roots.is_empty() + && captures.is_empty() + && roots.windows(2).all(|pair| pair[0] < pair[1]) + && roots.iter().all(|&root| (root as usize) < params) + } + + let borrow_valid = match borrow { + hir::ReturnBorrowSummary::None => true, + hir::ReturnBorrowSummary::Roots { + params: roots, + captures, + } => valid(roots, captures, params), + }; + let region_valid = match region { + hir::ReturnRegionSummary::None => true, + hir::ReturnRegionSummary::Roots { + params: roots, + captures, + } => valid(roots, captures, params), + }; + if borrow_valid && region_valid && summaries_agree(borrow, region) { + Ok(()) + } else { + Err(CanonicalCodecError::InvalidSummary) + } +} + +struct DecodeCursor<'a> { + bytes: &'a [u8], + offset: usize, +} + +impl<'a> DecodeCursor<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, offset: 0 } + } + + fn byte(&mut self) -> Result { + let value = self + .bytes + .get(self.offset) + .copied() + .ok_or(CanonicalCodecError::Truncated)?; + self.offset += 1; + Ok(value) + } + + fn u32(&mut self) -> Result { + let end = self + .offset + .checked_add(4) + .ok_or(CanonicalCodecError::Truncated)?; + let bytes = self + .bytes + .get(self.offset..end) + .ok_or(CanonicalCodecError::Truncated)?; + self.offset = end; + Ok(u32::from_le_bytes( + bytes + .try_into() + .map_err(|_| CanonicalCodecError::Truncated)?, + )) + } + + fn boolean(&mut self) -> Result { + match self.byte()? { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(CanonicalCodecError::InvalidBool), + } + } + + fn count(&mut self, minimum_bytes: usize) -> Result { + let count = self.u32()? as usize; + if minimum_bytes != 0 + && count > self.bytes.len().saturating_sub(self.offset) / minimum_bytes + { + return Err(CanonicalCodecError::Truncated); + } + Ok(count) + } + + fn text(&mut self) -> Result { + let len = self.count(1)?; + let end = self + .offset + .checked_add(len) + .ok_or(CanonicalCodecError::Truncated)?; + let bytes = self + .bytes + .get(self.offset..end) + .ok_or(CanonicalCodecError::Truncated)?; + self.offset = end; + let value = std::str::from_utf8(bytes).map_err(|_| CanonicalCodecError::InvalidUtf8)?; + if value.as_bytes().contains(&0) { + return Err(CanonicalCodecError::EmbeddedNul); + } + Ok(value.to_owned()) + } +} + +enum DecodedNode { + Struct(hir::StructDef), + Enum(hir::EnumDef), + Tuple(hir::TupleDef), + Tagged(hir::TaggedType), + Function(FunctionTypeDef), +} + +fn validate_canonical_type_bytes(bytes: &[u8]) -> Result { + let mut cursor = DecodeCursor::new(bytes); + if cursor.byte()? != 1 { + return Err(CanonicalCodecError::UnsupportedVersion); + } + let node_count = cursor.count(1)?; + let mut nodes = Vec::with_capacity(node_count.min(1024)); + for _ in 0..node_count { + nodes.push(decode_node(&mut cursor)?); + } + let mut root = decode_ty(&mut cursor)?; + let consumed = cursor.offset; + + let mut struct_count = 0usize; + let mut enum_count = 0usize; + let mut tuple_count = 0usize; + let mut tagged_count = 0usize; + let mut function_count = 0usize; + let mut resolved = Vec::with_capacity(nodes.len()); + for node in &nodes { + let (tag, local) = match node { + DecodedNode::Struct(_) => { + let local = struct_count; + struct_count += 1; + (0, local) + } + DecodedNode::Enum(_) => { + let local = enum_count; + enum_count += 1; + (1, local) + } + DecodedNode::Tuple(_) => { + let local = tuple_count; + tuple_count += 1; + (2, local) + } + DecodedNode::Tagged(_) => { + let local = tagged_count; + tagged_count += 1; + (3, local) + } + DecodedNode::Function(_) => { + let local = function_count; + function_count += 1; + (4, local) + } + }; + resolved.push(( + tag, + checked_count(local).map_err(CanonicalCodecError::from)?, + )); + } + + let mut structs = Vec::with_capacity(struct_count); + let mut enums = Vec::with_capacity(enum_count); + let mut tuples = Vec::with_capacity(tuple_count); + let mut tagged_types = Vec::with_capacity(tagged_count); + let mut fn_types = Vec::with_capacity(function_count); + for mut node in nodes { + remap_decoded_node(&mut node, &resolved)?; + match node { + DecodedNode::Struct(value) => structs.push(value), + DecodedNode::Enum(value) => enums.push(value), + DecodedNode::Tuple(value) => tuples.push(value), + DecodedNode::Tagged(value) => tagged_types.push(value), + DecodedNode::Function(value) => fn_types.push(value), + } + } + remap_decoded_ty(&mut root, &resolved)?; + + let view = CanonicalTypeView { + structs: &structs, + enums: &enums, + tuples: &tuples, + tagged_types: &tagged_types, + fn_types: &fn_types, + }; + let mut roots = Vec::with_capacity(resolved.len() + 1); + for &(tag, local) in &resolved { + roots.push(node_root_ty(tag, local)?); + } + roots.push(root); + let graph = ValidatedGraph::new_many(root, &roots, view)?; + let classes = stable_classes(&graph)?; + let unique_classes: HashSet = classes.values().copied().collect(); + if unique_classes.len() != graph.order.len() { + return Err(CanonicalCodecError::DuplicateMember); + } + let canonical = canonical_type_bytes_with_classes(&graph, root, &classes)?; + if canonical.as_slice() != &bytes[..consumed] { + return Err(CanonicalCodecError::NonCanonicalOrder); + } + Ok(consumed) +} + +fn node_root_ty(tag: u8, id: u32) -> Result { + match tag { + 0 => Ok(Ty::Struct(id)), + 1 => Ok(Ty::Enum(id)), + 2 => Ok(Ty::Tuple(id)), + 3 => Ok(Ty::Tagged(id)), + 4 => Ok(Ty::Fn(id)), + _ => Err(CanonicalCodecError::UnknownTag), + } +} + +fn decode_node(cursor: &mut DecodeCursor<'_>) -> Result { + match cursor.byte()? { + 0 => { + let source_name = cursor.text()?; + let align = match cursor.byte()? { + 0 => None, + 1 => Some(cursor.u32()?), + _ => return Err(CanonicalCodecError::UnknownTag), + }; + let c_repr = cursor.boolean()?; + let count = cursor.count(5)?; + let mut fields = Vec::with_capacity(count.min(1024)); + for _ in 0..count { + fields.push(hir::FieldDef { + name: cursor.text()?, + ty: decode_ty(cursor)?, + }); + } + Ok(DecodedNode::Struct(hir::StructDef { + name: source_name.clone(), + source_name, + fields, + align, + c_repr, + })) + } + 1 => { + let source_name = cursor.text()?; + let count = cursor.count(12)?; + let mut variants = Vec::with_capacity(count.min(1024)); + for _ in 0..count { + let name = cursor.text()?; + let field_base = cursor.u32()?; + let payload_count = cursor.count(1)?; + let mut payload = Vec::with_capacity(payload_count.min(1024)); + for _ in 0..payload_count { + payload.push(decode_scalar(cursor)?); + } + variants.push(hir::EnumVariant { + name, + payload, + field_base, + }); + } + Ok(DecodedNode::Enum(hir::EnumDef { + name: source_name.clone(), + source_name, + variants, + })) + } + 2 => { + let count = cursor.count(1)?; + let mut elems = Vec::with_capacity(count.min(1024)); + for _ in 0..count { + elems.push(decode_scalar(cursor)?); + } + Ok(DecodedNode::Tuple(hir::TupleDef { elems })) + } + 3 => match cursor.byte()? { + 0 => Ok(DecodedNode::Tagged(hir::TaggedType::Option(decode_scalar( + cursor, + )?))), + 1 => Ok(DecodedNode::Tagged(hir::TaggedType::Result( + decode_scalar(cursor)?, + decode_scalar(cursor)?, + ))), + _ => Err(CanonicalCodecError::UnknownTag), + }, + 4 => { + let count = cursor.count(2)?; + let mut params = Vec::with_capacity(count.min(1024)); + for _ in 0..count { + params.push((decode_param_mode(cursor)?, decode_scalar(cursor)?)); + } + Ok(DecodedNode::Function(FunctionTypeDef { + params, + ret: decode_ty(cursor)?, + return_borrow: decode_borrow_summary(cursor)?, + return_region: decode_region_summary(cursor)?, + })) + } + _ => Err(CanonicalCodecError::UnknownTag), + } +} + +fn decode_param_mode(cursor: &mut DecodeCursor<'_>) -> Result { + match cursor.byte()? { + 0 => Ok(ParamMode::ByValue), + 1 => Ok(ParamMode::Out), + 2 => Ok(ParamMode::Borrow), + 3 => Ok(ParamMode::BorrowMut), + _ => Err(CanonicalCodecError::UnknownTag), + } +} + +fn decode_int(cursor: &mut DecodeCursor<'_>) -> Result { + let signed = cursor.boolean()?; + let bits = cursor.byte()?; + if !matches!(bits, 8 | 16 | 32 | 64) { + return Err(CanonicalCodecError::InvalidWidth); + } + Ok(align_sema::IntTy { signed, bits }) +} + +fn decode_float(cursor: &mut DecodeCursor<'_>) -> Result { + let bits = cursor.byte()?; + if !matches!(bits, 32 | 64) { + return Err(CanonicalCodecError::InvalidWidth); + } + Ok(align_sema::FloatTy { bits }) +} + +fn decode_prim(cursor: &mut DecodeCursor<'_>) -> Result { + match cursor.byte()? { + 0 => Ok(PrimScalar::Int(decode_int(cursor)?)), + 1 => Ok(PrimScalar::Float(decode_float(cursor)?)), + 2 => Ok(PrimScalar::Bool), + 3 => Ok(PrimScalar::Char), + 4 => Ok(PrimScalar::Str), + 5 => Ok(PrimScalar::String), + _ => Err(CanonicalCodecError::UnknownTag), + } +} + +fn decode_scalar(cursor: &mut DecodeCursor<'_>) -> Result { + let node = |cursor: &mut DecodeCursor<'_>| cursor.u32(); + match cursor.byte()? { + 0 => Ok(Scalar::Int(decode_int(cursor)?)), + 1 => Ok(Scalar::Float(decode_float(cursor)?)), + 2 => Ok(Scalar::Bool), + 3 => Ok(Scalar::Char), + 4 => Ok(Scalar::Unit), + 5 => Ok(Scalar::Struct(node(cursor)?)), + 6 => Ok(Scalar::String), + 7 => Ok(Scalar::DynArray(decode_prim(cursor)?)), + 8 => Ok(Scalar::DynStructArray(node(cursor)?)), + 9 => Ok(Scalar::DynResponseArray), + 10 => Ok(Scalar::Str), + 11 => Ok(Scalar::Slice(decode_prim(cursor)?)), + 12 => Ok(Scalar::Enum(node(cursor)?)), + 13 => Ok(Scalar::Tagged(node(cursor)?)), + 14 => Ok(Scalar::Soa(node(cursor)?)), + 15 => Ok(Scalar::JsonDoc), + 16 => Ok(Scalar::Reader), + 17 => Ok(Scalar::Writer), + 18 => Ok(Scalar::Buffer), + 19 => Ok(Scalar::Regex), + 20 => Ok(Scalar::Captures), + 21 => Ok(Scalar::CliParsed), + 22 => Ok(Scalar::TcpConn), + 23 => Ok(Scalar::TcpListener), + 24 => Ok(Scalar::UdpSocket), + 25 => Ok(Scalar::Child), + 26 => Ok(Scalar::File), + 27 => Ok(Scalar::HttpResponse), + 28 => Ok(Scalar::HttpServer), + 29 => Ok(Scalar::HttpRequestCtx), + 30 => Ok(Scalar::ResponseBuilder), + 31 => Ok(Scalar::HttpStream), + 32 => Ok(Scalar::RunOutput), + 33 => Ok(Scalar::Fn(node(cursor)?)), + _ => Err(CanonicalCodecError::UnknownTag), + } +} + +fn decode_ty(cursor: &mut DecodeCursor<'_>) -> Result { + let node = |cursor: &mut DecodeCursor<'_>| cursor.u32(); + let tag = cursor.byte()?; + match tag { + 0 => Ok(Ty::Int(decode_int(cursor)?)), + 1 => Ok(Ty::Float(decode_float(cursor)?)), + 2 => Ok(Ty::Bool), + 3 => Ok(Ty::Char), + 4 => Ok(Ty::Option(decode_scalar(cursor)?)), + 5 => Ok(Ty::Result(decode_scalar(cursor)?, decode_scalar(cursor)?)), + 6 => Ok(Ty::Tagged(node(cursor)?)), + 7 => Ok(Ty::Box(decode_scalar(cursor)?)), + 8 => Ok(Ty::Array(decode_scalar(cursor)?, cursor.u32()?)), + 9 | 10 => { + let scalar = decode_scalar(cursor)?; + let lanes = cursor.u32()?; + if !matches!(scalar, Scalar::Int(_) | Scalar::Float(_)) + || !matches!(lanes, 2 | 4 | 8 | 16) + { + return Err(CanonicalCodecError::InvalidWidth); + } + if tag == 9 { + Ok(Ty::Vec(scalar, lanes)) + } else { + Ok(Ty::Mask(scalar, lanes)) + } + } + 11 => Ok(Ty::StructArray(node(cursor)?, cursor.u32()?)), + 12 => { + let id = node(cursor)?; + let layout = match cursor.byte()? { + 0 => Layout::Aos, + 1 => Layout::Soa, + _ => return Err(CanonicalCodecError::UnknownTag), + }; + Ok(Ty::DynStructArray(id, layout)) + } + 13 => Ok(Ty::Slice(decode_scalar(cursor)?)), + 14 => Ok(Ty::Soa(node(cursor)?)), + 15 => Ok(Ty::DynSliceArray(decode_prim(cursor)?)), + 16 => Ok(Ty::DynArray(decode_scalar(cursor)?)), + 17 => Ok(Ty::DynResponseArray), + 18 => Ok(Ty::Str), + 19 => Ok(Ty::String), + 20 => Ok(Ty::ArenaHandle), + 21 => Ok(Ty::Raw), + 22 => Ok(Ty::Builder), + 23 => Ok(Ty::Writer), + 24 => Ok(Ty::Reader), + 25 => Ok(Ty::Buffer), + 26 => Ok(Ty::ArrayBuilder(decode_scalar(cursor)?)), + 27 => Ok(Ty::StrFinder), + 28 => Ok(Ty::File), + 29 => Ok(Ty::Rng), + 30 => Ok(Ty::Regex), + 31 => Ok(Ty::Captures), + 32 => Ok(Ty::CliCommand), + 33 => Ok(Ty::CliParsed), + 34 => Ok(Ty::TcpConn), + 35 => Ok(Ty::TcpListener), + 36 => Ok(Ty::UdpSocket), + 37 => Ok(Ty::Child), + 38 => Ok(Ty::Command), + 39 => Ok(Ty::RunOutput), + 40 => Ok(Ty::HttpRequest), + 41 => Ok(Ty::HttpResponse), + 42 => Ok(Ty::HttpClient), + 43 => Ok(Ty::HttpServer), + 44 => Ok(Ty::HttpRequestCtx), + 45 => Ok(Ty::ResponseBuilder), + 46 => Ok(Ty::HttpStream), + 47 => Ok(Ty::HttpHeaders), + 48 => Ok(Ty::JsonDoc), + 49 => Ok(Ty::JsonScanner(node(cursor)?)), + 50 => Ok(Ty::Struct(node(cursor)?)), + 51 => Ok(Ty::Tuple(node(cursor)?)), + 52 => Ok(Ty::Fn(node(cursor)?)), + 53 => Ok(Ty::Enum(node(cursor)?)), + 54 => Ok(Ty::Task(decode_scalar(cursor)?)), + 55 => Ok(Ty::DictEncoded(node(cursor)?, cursor.u32()?)), + 56 => Ok(Ty::Unit), + _ => Err(CanonicalCodecError::UnknownTag), + } +} + +fn decode_borrow_summary( + cursor: &mut DecodeCursor<'_>, +) -> Result { + match cursor.byte()? { + 0 => Ok(hir::ReturnBorrowSummary::None), + 1 => { + let (params, captures) = decode_roots(cursor)?; + Ok(hir::ReturnBorrowSummary::Roots { params, captures }) + } + _ => Err(CanonicalCodecError::UnknownTag), + } +} + +fn decode_region_summary( + cursor: &mut DecodeCursor<'_>, +) -> Result { + match cursor.byte()? { + 0 => Ok(hir::ReturnRegionSummary::None), + 1 => { + let (params, captures) = decode_roots(cursor)?; + Ok(hir::ReturnRegionSummary::Roots { params, captures }) + } + _ => Err(CanonicalCodecError::UnknownTag), + } +} + +fn decode_roots( + cursor: &mut DecodeCursor<'_>, +) -> Result<(Vec, Vec), CanonicalCodecError> { + let param_count = cursor.count(4)?; + let mut params = Vec::with_capacity(param_count.min(1024)); + for _ in 0..param_count { + params.push(cursor.u32()?); + } + let capture_count = cursor.count(4)?; + let mut captures = Vec::with_capacity(capture_count.min(1024)); + for _ in 0..capture_count { + captures.push(cursor.u32()?); + } + Ok((params, captures)) +} + +fn resolve_decoded_node( + global: u32, + expected_tag: u8, + resolved: &[(u8, u32)], +) -> Result { + let &(tag, local) = resolved + .get(global as usize) + .ok_or(CanonicalCodecError::MissingReference)?; + if tag == expected_tag { + Ok(local) + } else { + Err(CanonicalCodecError::MissingReference) + } +} + +fn remap_decoded_scalar( + value: &mut Scalar, + resolved: &[(u8, u32)], +) -> Result<(), CanonicalCodecError> { + let (id, tag) = match value { + Scalar::Struct(id) | Scalar::DynStructArray(id) | Scalar::Soa(id) => (id, 0), + Scalar::Enum(id) => (id, 1), + Scalar::Tagged(id) => (id, 3), + Scalar::Fn(id) => (id, 4), + _ => return Ok(()), + }; + *id = resolve_decoded_node(*id, tag, resolved)?; + Ok(()) +} + +fn remap_decoded_ty(value: &mut Ty, resolved: &[(u8, u32)]) -> Result<(), CanonicalCodecError> { + match value { + Ty::Option(value) + | Ty::Box(value) + | Ty::Slice(value) + | Ty::DynArray(value) + | Ty::ArrayBuilder(value) + | Ty::Task(value) + | Ty::Array(value, _) + | Ty::Vec(value, _) + | Ty::Mask(value, _) => remap_decoded_scalar(value, resolved), + Ty::Result(ok, err) => { + remap_decoded_scalar(ok, resolved)?; + remap_decoded_scalar(err, resolved) + } + Ty::Tagged(id) => { + *id = resolve_decoded_node(*id, 3, resolved)?; + Ok(()) + } + Ty::StructArray(id, _) + | Ty::DynStructArray(id, _) + | Ty::Soa(id) + | Ty::JsonScanner(id) + | Ty::DictEncoded(id, _) + | Ty::Struct(id) => { + *id = resolve_decoded_node(*id, 0, resolved)?; + Ok(()) + } + Ty::Tuple(id) => { + *id = resolve_decoded_node(*id, 2, resolved)?; + Ok(()) + } + Ty::Fn(id) => { + *id = resolve_decoded_node(*id, 4, resolved)?; + Ok(()) + } + Ty::Enum(id) => { + *id = resolve_decoded_node(*id, 1, resolved)?; + Ok(()) + } + _ => Ok(()), + } +} + +fn remap_decoded_node( + node: &mut DecodedNode, + resolved: &[(u8, u32)], +) -> Result<(), CanonicalCodecError> { + match node { + DecodedNode::Struct(definition) => { + for field in &mut definition.fields { + remap_decoded_ty(&mut field.ty, resolved)?; + } + } + DecodedNode::Enum(definition) => { + for variant in &mut definition.variants { + for scalar in &mut variant.payload { + remap_decoded_scalar(scalar, resolved)?; + } + } + } + DecodedNode::Tuple(definition) => { + for scalar in &mut definition.elems { + remap_decoded_scalar(scalar, resolved)?; + } + } + DecodedNode::Tagged(definition) => match definition { + hir::TaggedType::Option(value) => remap_decoded_scalar(value, resolved)?, + hir::TaggedType::Result(ok, err) => { + remap_decoded_scalar(ok, resolved)?; + remap_decoded_scalar(err, resolved)?; + } + }, + DecodedNode::Function(definition) => { + for (_, scalar) in &mut definition.params { + remap_decoded_scalar(scalar, resolved)?; + } + remap_decoded_ty(&mut definition.ret, resolved)?; + } + } + Ok(()) +} + +fn decode_nested_canonical_type(cursor: &mut DecodeCursor<'_>) -> Result<(), CanonicalCodecError> { + let consumed = validate_canonical_type_bytes( + cursor + .bytes + .get(cursor.offset..) + .ok_or(CanonicalCodecError::Truncated)?, + )?; + cursor.offset = cursor + .offset + .checked_add(consumed) + .ok_or(CanonicalCodecError::Truncated)?; + Ok(()) +} + +fn validate_canonical_fn_abi_bytes(bytes: &[u8]) -> Result<(), CanonicalCodecError> { + let mut cursor = DecodeCursor::new(bytes); + if cursor.byte()? != 1 { + return Err(CanonicalCodecError::UnsupportedVersion); + } + let count = cursor.count(7)?; + let mut invalid_mode = false; + for _ in 0..count { + let mode = decode_param_mode(&mut cursor)?; + invalid_mode |= matches!(mode, ParamMode::Borrow | ParamMode::BorrowMut); + decode_nested_canonical_type(&mut cursor)?; + } + decode_nested_canonical_type(&mut cursor)?; + let borrow = decode_borrow_summary(&mut cursor)?; + let region = decode_region_summary(&mut cursor)?; + if invalid_mode { + return Err(CanonicalCodecError::InvalidGraph); + } + validate_function_summaries(&borrow, ®ion, count)?; + if cursor.offset != bytes.len() { + return Err(CanonicalCodecError::TrailingBytes); + } + Ok(()) +} + fn stable_classes(graph: &ValidatedGraph<'_>) -> Result, CanonicalGraphError> { stable_classes_and_rounds(graph).map(|(classes, _)| classes) } @@ -1607,6 +2431,20 @@ mod tests { canonical_type_bytes(&graph) } + fn mir_program(program: &hir::Program) -> Program { + Program { + fns: Vec::new(), + externs: Vec::new(), + imported_fns: Vec::new(), + link_libs: Vec::new(), + structs: program.structs.clone(), + enums: program.enums.clone(), + tagged_types: program.tagged_types.clone(), + fn_types: function_defs(program), + tuples: program.tuples.clone(), + } + } + #[derive(Default)] struct RefinementMetrics { signature_bytes: usize, @@ -1963,6 +2801,284 @@ mod tests { assert_eq!(bytes.last(), Some(&0)); } + #[test] + fn canonical_type_codec() { + let program = mir_program(&baseline_program()); + for (root, expected) in [ + (Ty::Unit, vec![1, 0, 0, 0, 0, 56]), + (Ty::Bool, vec![1, 0, 0, 0, 0, 2]), + (Ty::Int(i(64)), vec![1, 0, 0, 0, 0, 0, 1, 64]), + ] { + let encoded = CanonicalTy::from_program(root, &program).unwrap(); + assert_eq!(encoded.as_bytes(), expected); + assert_eq!(CanonicalTy::decode(&expected).unwrap(), encoded); + } + for root in [ + Ty::Struct(0), + Ty::Enum(0), + Ty::Tuple(0), + Ty::Tagged(0), + Ty::Fn(0), + ] { + let encoded = CanonicalTy::from_program(root, &program).unwrap(); + assert_eq!(CanonicalTy::decode(encoded.as_bytes()).unwrap(), encoded); + } + + assert_eq!( + ProgramCall::try_from_logical("pkg$run").unwrap().as_bytes(), + b"pkg$run" + ); + assert_eq!( + ProgramCall::try_from_logical(""), + Err(ProgramCallError::Empty) + ); + assert_eq!( + ProgramCall::try_from_logical("bad\0name"), + Err(ProgramCallError::EmbeddedNul) + ); + + let roots = [ + Ty::Int(i(8)), + Ty::Float(f(32)), + Ty::Bool, + Ty::Char, + Ty::Option(Scalar::Bool), + Ty::Result(Scalar::Bool, Scalar::Char), + Ty::Tagged(0), + Ty::Box(Scalar::Bool), + Ty::Array(Scalar::Bool, 2), + Ty::Vec(Scalar::Int(i(8)), 2), + Ty::Mask(Scalar::Float(f(32)), 2), + Ty::StructArray(0, 2), + Ty::DynStructArray(0, Layout::Aos), + Ty::Slice(Scalar::Bool), + Ty::Soa(0), + Ty::DynSliceArray(PrimScalar::Bool), + Ty::DynArray(Scalar::Bool), + Ty::DynResponseArray, + Ty::Str, + Ty::String, + Ty::ArenaHandle, + Ty::Raw, + Ty::Builder, + Ty::Writer, + Ty::Reader, + Ty::Buffer, + Ty::ArrayBuilder(Scalar::Bool), + Ty::StrFinder, + Ty::File, + Ty::Rng, + Ty::Regex, + Ty::Captures, + Ty::CliCommand, + Ty::CliParsed, + Ty::TcpConn, + Ty::TcpListener, + Ty::UdpSocket, + Ty::Child, + Ty::Command, + Ty::RunOutput, + Ty::HttpRequest, + Ty::HttpResponse, + Ty::HttpClient, + Ty::HttpServer, + Ty::HttpRequestCtx, + Ty::ResponseBuilder, + Ty::HttpStream, + Ty::HttpHeaders, + Ty::JsonDoc, + Ty::JsonScanner(0), + Ty::Struct(0), + Ty::Tuple(0), + Ty::Fn(0), + Ty::Enum(0), + Ty::Task(Scalar::Bool), + Ty::DictEncoded(0, 0), + Ty::Unit, + ]; + crate::source_shape::tests::assert_ty_matrix(&roots); + for root in roots { + let encoded = CanonicalTy::from_program(root, &program).unwrap(); + assert_eq!(CanonicalTy::decode(encoded.as_bytes()).unwrap(), encoded); + } + + let scalars = [ + Scalar::Int(i(8)), + Scalar::Float(f(32)), + Scalar::Bool, + Scalar::Char, + Scalar::Unit, + Scalar::Struct(0), + Scalar::String, + Scalar::DynArray(PrimScalar::Bool), + Scalar::DynStructArray(0), + Scalar::DynResponseArray, + Scalar::Str, + Scalar::Slice(PrimScalar::Char), + Scalar::Enum(0), + Scalar::Tagged(0), + Scalar::Soa(0), + Scalar::JsonDoc, + Scalar::Reader, + Scalar::Writer, + Scalar::Buffer, + Scalar::Regex, + Scalar::Captures, + Scalar::CliParsed, + Scalar::TcpConn, + Scalar::TcpListener, + Scalar::UdpSocket, + Scalar::Child, + Scalar::File, + Scalar::HttpResponse, + Scalar::HttpServer, + Scalar::HttpRequestCtx, + Scalar::ResponseBuilder, + Scalar::HttpStream, + Scalar::RunOutput, + Scalar::Fn(0), + ]; + crate::source_shape::tests::assert_scalar_matrix(&scalars); + for scalar in scalars { + let encoded = CanonicalTy::from_program(Ty::Option(scalar), &program).unwrap(); + assert_eq!(CanonicalTy::decode(encoded.as_bytes()).unwrap(), encoded); + } + } + + #[test] + fn canonical_type_codec_function_root() { + let mut hir = baseline_program(); + hir.fn_types[0].params = vec![(ParamMode::ByValue, Scalar::Fn(0))]; + hir.fn_types[0].ret = Ty::Unit; + let program = mir_program(&hir); + let ty = CanonicalTy::from_program(Ty::Fn(0), &program).unwrap(); + assert_eq!(CanonicalTy::decode(ty.as_bytes()).unwrap(), ty); + + let abi = CanonicalFnAbi::from_parts( + &[], + Ty::Unit, + &hir::ReturnBorrowSummary::None, + &hir::ReturnRegionSummary::None, + &program, + ) + .unwrap(); + assert_eq!(abi.as_bytes(), [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 56, 0, 0]); + assert_eq!(CanonicalFnAbi::decode(abi.as_bytes()).unwrap(), abi); + + let params = [(ParamMode::ByValue, Ty::Fn(0))]; + let recursive = CanonicalFnAbi::from_parts( + ¶ms, + Ty::Fn(0), + &hir::ReturnBorrowSummary::Roots { + params: vec![0], + captures: vec![], + }, + &hir::ReturnRegionSummary::Roots { + params: vec![0], + captures: vec![], + }, + &program, + ) + .unwrap(); + assert_eq!( + CanonicalFnAbi::decode(recursive.as_bytes()).unwrap(), + recursive + ); + } + + #[test] + fn canonical_codec_error_precedence() { + let error = |bytes: &[u8], expected| { + assert_eq!(CanonicalTy::decode(bytes), Err(expected), "{bytes:02x?}"); + }; + error(&[], CanonicalCodecError::Truncated); + error(&[2], CanonicalCodecError::UnsupportedVersion); + error(&[1, 0, 0, 0, 0, 0xff], CanonicalCodecError::UnknownTag); + error(&[1, 0, 0, 0, 0, 0, 2, 64], CanonicalCodecError::InvalidBool); + error( + &[1, 0, 0, 0, 0, 0, 1, 24], + CanonicalCodecError::InvalidWidth, + ); + error( + &[1, 0, 0, 0, 0, 50, 0xff, 0xff, 0xff, 0xff], + CanonicalCodecError::MissingReference, + ); + + let mut trailing = vec![1, 0, 0, 0, 0, 56]; + trailing.push(0); + error(&trailing, CanonicalCodecError::TrailingBytes); + + let invalid_utf8 = [ + 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, + ]; + error(&invalid_utf8, CanonicalCodecError::InvalidUtf8); + let embedded_nul = [ + 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, + ]; + error(&embedded_nul, CanonicalCodecError::EmbeddedNul); + + let invalid_align = [ + 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, b'S', 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, + ]; + error(&invalid_align, CanonicalCodecError::InvalidGraph); + + let duplicate_function = [ + 1, 2, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 52, 0, 0, 0, 0, + ]; + error(&duplicate_function, CanonicalCodecError::DuplicateMember); + + let unreachable_function = [1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 56]; + error( + &unreachable_function, + CanonicalCodecError::NonCanonicalOrder, + ); + + let invalid_summary = [ + 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 56, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, + ]; + error(&invalid_summary, CanonicalCodecError::InvalidSummary); + + let unit = [1, 0, 0, 0, 0, 56]; + let mut invalid_mode = vec![1, 1, 0, 0, 0, 2]; + invalid_mode.extend(unit); + invalid_mode.extend(unit); + invalid_mode.extend([0, 0]); + assert_eq!( + CanonicalFnAbi::decode(&invalid_mode), + Err(CanonicalCodecError::InvalidGraph) + ); + let mut abi_trailing = vec![1, 0, 0, 0, 0]; + abi_trailing.extend(unit); + abi_trailing.extend([0, 0, 0xff]); + assert_eq!( + CanonicalFnAbi::decode(&abi_trailing), + Err(CanonicalCodecError::TrailingBytes) + ); + } + + #[test] + fn deep_canonical_type_codec_is_stack_bounded() { + let mut hir = baseline_program(); + hir.structs.clear(); + for id in 0..4096u32 { + let mut definition = baseline_program().structs[0].clone(); + definition.source_name = format!("Codec{id}"); + definition.fields[0].ty = if id == 4095 { + Ty::Bool + } else { + Ty::Struct(id + 1) + }; + hir.structs.push(definition); + } + let program = mir_program(&hir); + let encoded = CanonicalTy::from_program(Ty::Struct(0), &program).unwrap(); + assert_eq!(CanonicalTy::decode(encoded.as_bytes()).unwrap(), encoded); + assert_eq!( + CanonicalTy::decode(&encoded.as_bytes()[..encoded.as_bytes().len() - 1]), + Err(CanonicalCodecError::Truncated) + ); + } + #[test] fn canonical_graph_equivalence() { let mut program = baseline_program(); diff --git a/crates/align_mir/src/lib.rs b/crates/align_mir/src/lib.rs index c9697352..ebd60470 100644 --- a/crates/align_mir/src/lib.rs +++ b/crates/align_mir/src/lib.rs @@ -25,7 +25,10 @@ mod source_shape; mod validate_hir; mod runtime_key; -pub use canonical_graph::{FunctionTypeDef, function_types_are_canonical}; +pub use canonical_graph::{ + CanonicalCodecError, CanonicalFnAbi, CanonicalTy, FunctionTypeDef, ProgramCall, + ProgramCallError, function_types_are_canonical, +}; pub use runtime_key::RuntimeKey; #[cfg(test)] From 11802f01cbe6916927bd772aef665ca716aea454 Mon Sep 17 00:00:00 2001 From: sanohiro Date: Wed, 5 Aug 2026 12:49:09 +0900 Subject: [PATCH 5/7] feat(mir): encode generated callable identities --- crates/align_mir/src/canonical_graph.rs | 17 +- crates/align_mir/src/generated_id.rs | 713 ++++++++++++++++++++++++ crates/align_mir/src/lib.rs | 4 + 3 files changed, 725 insertions(+), 9 deletions(-) create mode 100644 crates/align_mir/src/generated_id.rs diff --git a/crates/align_mir/src/canonical_graph.rs b/crates/align_mir/src/canonical_graph.rs index 2571bff1..ac75140c 100644 --- a/crates/align_mir/src/canonical_graph.rs +++ b/crates/align_mir/src/canonical_graph.rs @@ -741,7 +741,7 @@ impl CanonicalTy { } pub fn decode(bytes: &[u8]) -> Result { - let consumed = validate_canonical_type_bytes(bytes)?; + let consumed = canonical_type_record_len(bytes)?; if consumed != bytes.len() { return Err(CanonicalCodecError::TrailingBytes); } @@ -789,7 +789,9 @@ impl CanonicalFnAbi { } pub fn decode(bytes: &[u8]) -> Result { - validate_canonical_fn_abi_bytes(bytes)?; + if canonical_fn_abi_record_len(bytes)? != bytes.len() { + return Err(CanonicalCodecError::TrailingBytes); + } Ok(Self(bytes.into())) } @@ -925,7 +927,7 @@ enum DecodedNode { Function(FunctionTypeDef), } -fn validate_canonical_type_bytes(bytes: &[u8]) -> Result { +pub(super) fn canonical_type_record_len(bytes: &[u8]) -> Result { let mut cursor = DecodeCursor::new(bytes); if cursor.byte()? != 1 { return Err(CanonicalCodecError::UnsupportedVersion); @@ -1437,7 +1439,7 @@ fn remap_decoded_node( } fn decode_nested_canonical_type(cursor: &mut DecodeCursor<'_>) -> Result<(), CanonicalCodecError> { - let consumed = validate_canonical_type_bytes( + let consumed = canonical_type_record_len( cursor .bytes .get(cursor.offset..) @@ -1450,7 +1452,7 @@ fn decode_nested_canonical_type(cursor: &mut DecodeCursor<'_>) -> Result<(), Can Ok(()) } -fn validate_canonical_fn_abi_bytes(bytes: &[u8]) -> Result<(), CanonicalCodecError> { +pub(super) fn canonical_fn_abi_record_len(bytes: &[u8]) -> Result { let mut cursor = DecodeCursor::new(bytes); if cursor.byte()? != 1 { return Err(CanonicalCodecError::UnsupportedVersion); @@ -1469,10 +1471,7 @@ fn validate_canonical_fn_abi_bytes(bytes: &[u8]) -> Result<(), CanonicalCodecErr return Err(CanonicalCodecError::InvalidGraph); } validate_function_summaries(&borrow, ®ion, count)?; - if cursor.offset != bytes.len() { - return Err(CanonicalCodecError::TrailingBytes); - } - Ok(()) + Ok(cursor.offset) } fn stable_classes(graph: &ValidatedGraph<'_>) -> Result, CanonicalGraphError> { diff --git a/crates/align_mir/src/generated_id.rs b/crates/align_mir/src/generated_id.rs new file mode 100644 index 00000000..c7f1fa06 --- /dev/null +++ b/crates/align_mir/src/generated_id.rs @@ -0,0 +1,713 @@ +use crate::{CanonicalCodecError, CanonicalFnAbi, CanonicalTy, ProgramCall}; + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u8)] +pub enum ParallelKernelMode { + Materialize = 0, + Reduce = 1, + FilterCount = 2, + FilterScatter = 3, +} + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum ParallelStageId { + Map { + target: ProgramCall, + abi: CanonicalFnAbi, + input: CanonicalTy, + output: CanonicalTy, + captures: Vec, + }, + Filter { + target: ProgramCall, + abi: CanonicalFnAbi, + input: CanonicalTy, + output: CanonicalTy, + captures: Vec, + }, + FilterStrContains { + input: CanonicalTy, + output: CanonicalTy, + needle: CanonicalTy, + }, + Project { + input: CanonicalTy, + output: CanonicalTy, + field: u32, + }, + FilterField { + input: CanonicalTy, + output: CanonicalTy, + field: u32, + }, +} + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct ParallelGeneratedId { + pub mode: ParallelKernelMode, + pub source: CanonicalTy, + pub terminal_input: CanonicalTy, + pub terminal_output: CanonicalTy, + pub terminal: ProgramCall, + pub terminal_abi: CanonicalFnAbi, + pub terminal_captures: Vec, + pub stages: Vec, + pub work_weight: u8, +} + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum GeneratedId { + FnValue { + target: ProgramCall, + signature: CanonicalFnAbi, + }, + Closure { + lifted: ProgramCall, + explicit_signature: CanonicalFnAbi, + captures: Vec, + }, + Task { + fallible: bool, + result: CanonicalTy, + }, + Parallel(ParallelGeneratedId), +} + +impl GeneratedId { + pub fn to_canonical_bytes(&self) -> Result, CanonicalCodecError> { + validate_generated(self)?; + let mut out = Vec::new(); + out.push(1); + match self { + Self::FnValue { target, signature } => { + out.push(0); + encode_call(&mut out, target)?; + out.extend(signature.as_bytes()); + } + Self::Closure { + lifted, + explicit_signature, + captures, + } => { + out.push(1); + encode_call(&mut out, lifted)?; + out.extend(explicit_signature.as_bytes()); + encode_types(&mut out, captures)?; + } + Self::Task { fallible, result } => { + out.push(2); + out.push(u8::from(*fallible)); + out.extend(result.as_bytes()); + } + Self::Parallel(parallel) => { + out.push(3); + encode_parallel(&mut out, parallel)?; + } + } + Ok(out.into_boxed_slice()) + } + + pub fn decode(bytes: &[u8]) -> Result { + let mut cursor = IdentityCursor::new(bytes); + if cursor.byte()? != 1 { + return Err(CanonicalCodecError::UnsupportedVersion); + } + let value = match cursor.byte()? { + 0 => Self::FnValue { + target: cursor.call()?, + signature: cursor.abi()?, + }, + 1 => Self::Closure { + lifted: cursor.call()?, + explicit_signature: cursor.abi()?, + captures: cursor.types()?, + }, + 2 => Self::Task { + fallible: cursor.boolean()?, + result: cursor.ty()?, + }, + 3 => Self::Parallel(cursor.parallel()?), + _ => return Err(CanonicalCodecError::UnknownTag), + }; + validate_generated(&value)?; + if cursor.offset != bytes.len() { + return Err(CanonicalCodecError::TrailingBytes); + } + Ok(value) + } +} + +fn checked_count(len: usize) -> Result { + u32::try_from(len).map_err(|_| CanonicalCodecError::InvalidCount) +} + +fn encode_call(out: &mut Vec, value: &ProgramCall) -> Result<(), CanonicalCodecError> { + out.extend(checked_count(value.as_bytes().len())?.to_le_bytes()); + out.extend(value.as_bytes()); + Ok(()) +} + +fn encode_types(out: &mut Vec, values: &[CanonicalTy]) -> Result<(), CanonicalCodecError> { + out.extend(checked_count(values.len())?.to_le_bytes()); + for value in values { + out.extend(value.as_bytes()); + } + Ok(()) +} + +fn encode_parallel( + out: &mut Vec, + value: &ParallelGeneratedId, +) -> Result<(), CanonicalCodecError> { + out.push(value.mode as u8); + out.extend(value.source.as_bytes()); + out.extend(value.terminal_input.as_bytes()); + out.extend(value.terminal_output.as_bytes()); + encode_call(out, &value.terminal)?; + out.extend(value.terminal_abi.as_bytes()); + encode_types(out, &value.terminal_captures)?; + out.extend(checked_count(value.stages.len())?.to_le_bytes()); + for stage in &value.stages { + encode_stage(out, stage)?; + } + out.push(value.work_weight); + Ok(()) +} + +fn encode_stage(out: &mut Vec, value: &ParallelStageId) -> Result<(), CanonicalCodecError> { + match value { + ParallelStageId::Map { + target, + abi, + input, + output, + captures, + } + | ParallelStageId::Filter { + target, + abi, + input, + output, + captures, + } => { + out.push(u8::from(matches!(value, ParallelStageId::Filter { .. }))); + encode_call(out, target)?; + out.extend(abi.as_bytes()); + out.extend(input.as_bytes()); + out.extend(output.as_bytes()); + encode_types(out, captures)?; + } + ParallelStageId::FilterStrContains { + input, + output, + needle, + } => { + out.push(2); + out.extend(input.as_bytes()); + out.extend(output.as_bytes()); + out.extend(needle.as_bytes()); + } + ParallelStageId::Project { + input, + output, + field, + } + | ParallelStageId::FilterField { + input, + output, + field, + } => { + out.push(if matches!(value, ParallelStageId::Project { .. }) { + 3 + } else { + 4 + }); + out.extend(input.as_bytes()); + out.extend(output.as_bytes()); + out.extend(field.to_le_bytes()); + } + } + Ok(()) +} + +fn validate_generated(value: &GeneratedId) -> Result<(), CanonicalCodecError> { + match value { + GeneratedId::FnValue { target, .. } => validate_call(target), + GeneratedId::Closure { lifted, .. } => validate_call(lifted), + GeneratedId::Task { .. } => Ok(()), + GeneratedId::Parallel(value) => validate_parallel(value), + } +} + +fn validate_call(value: &ProgramCall) -> Result<(), CanonicalCodecError> { + if value.as_bytes().is_empty() || value.as_bytes().contains(&0) { + Err(CanonicalCodecError::InvalidGraph) + } else if u32::try_from(value.as_bytes().len()).is_err() { + Err(CanonicalCodecError::InvalidCount) + } else { + Ok(()) + } +} + +fn validate_parallel(value: &ParallelGeneratedId) -> Result<(), CanonicalCodecError> { + validate_call(&value.terminal)?; + checked_count(value.terminal_captures.len())?; + checked_count(value.stages.len())?; + let has_filter = value.stages.iter().any(|stage| { + matches!( + stage, + ParallelStageId::Filter { .. } + | ParallelStageId::FilterStrContains { .. } + | ParallelStageId::FilterField { .. } + ) + }); + let mode_valid = match value.mode { + ParallelKernelMode::Materialize => !has_filter, + ParallelKernelMode::Reduce => value.stages.is_empty(), + ParallelKernelMode::FilterCount | ParallelKernelMode::FilterScatter => has_filter, + }; + if !mode_valid || !matches!(value.work_weight, 1 | 2 | 4) { + return Err(CanonicalCodecError::InvalidGraph); + } + for stage in &value.stages { + match stage { + ParallelStageId::Map { + target, captures, .. + } + | ParallelStageId::Filter { + target, captures, .. + } => { + validate_call(target)?; + checked_count(captures.len())?; + } + ParallelStageId::FilterStrContains { .. } + | ParallelStageId::Project { .. } + | ParallelStageId::FilterField { .. } => {} + } + } + Ok(()) +} + +struct IdentityCursor<'a> { + bytes: &'a [u8], + offset: usize, +} + +impl<'a> IdentityCursor<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, offset: 0 } + } + + fn byte(&mut self) -> Result { + let value = self + .bytes + .get(self.offset) + .copied() + .ok_or(CanonicalCodecError::Truncated)?; + self.offset += 1; + Ok(value) + } + + fn boolean(&mut self) -> Result { + match self.byte()? { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(CanonicalCodecError::InvalidBool), + } + } + + fn u32(&mut self) -> Result { + let end = self + .offset + .checked_add(4) + .ok_or(CanonicalCodecError::Truncated)?; + let bytes = self + .bytes + .get(self.offset..end) + .ok_or(CanonicalCodecError::Truncated)?; + self.offset = end; + Ok(u32::from_le_bytes( + bytes + .try_into() + .map_err(|_| CanonicalCodecError::Truncated)?, + )) + } + + fn count(&mut self, minimum_bytes: usize) -> Result { + let count = self.u32()? as usize; + if minimum_bytes != 0 + && count > self.bytes.len().saturating_sub(self.offset) / minimum_bytes + { + return Err(CanonicalCodecError::Truncated); + } + Ok(count) + } + + fn call(&mut self) -> Result { + let len = self.count(1)?; + let end = self + .offset + .checked_add(len) + .ok_or(CanonicalCodecError::Truncated)?; + let bytes = self + .bytes + .get(self.offset..end) + .ok_or(CanonicalCodecError::Truncated)?; + self.offset = end; + let value = std::str::from_utf8(bytes).map_err(|_| CanonicalCodecError::InvalidUtf8)?; + ProgramCall::try_from_logical(value).map_err(|error| match error { + crate::ProgramCallError::Empty => CanonicalCodecError::InvalidGraph, + crate::ProgramCallError::EmbeddedNul => CanonicalCodecError::EmbeddedNul, + crate::ProgramCallError::TooLong => CanonicalCodecError::InvalidCount, + }) + } + + fn ty(&mut self) -> Result { + let bytes = self + .bytes + .get(self.offset..) + .ok_or(CanonicalCodecError::Truncated)?; + let len = crate::canonical_graph::canonical_type_record_len(bytes)?; + let end = self + .offset + .checked_add(len) + .ok_or(CanonicalCodecError::Truncated)?; + let value = CanonicalTy::decode( + self.bytes + .get(self.offset..end) + .ok_or(CanonicalCodecError::Truncated)?, + )?; + self.offset = end; + Ok(value) + } + + fn abi(&mut self) -> Result { + let bytes = self + .bytes + .get(self.offset..) + .ok_or(CanonicalCodecError::Truncated)?; + let len = crate::canonical_graph::canonical_fn_abi_record_len(bytes)?; + let end = self + .offset + .checked_add(len) + .ok_or(CanonicalCodecError::Truncated)?; + let value = CanonicalFnAbi::decode( + self.bytes + .get(self.offset..end) + .ok_or(CanonicalCodecError::Truncated)?, + )?; + self.offset = end; + Ok(value) + } + + fn types(&mut self) -> Result, CanonicalCodecError> { + let count = self.count(6)?; + let mut values = Vec::with_capacity(count.min(1024)); + for _ in 0..count { + values.push(self.ty()?); + } + Ok(values) + } + + fn parallel(&mut self) -> Result { + let mode = match self.byte()? { + 0 => ParallelKernelMode::Materialize, + 1 => ParallelKernelMode::Reduce, + 2 => ParallelKernelMode::FilterCount, + 3 => ParallelKernelMode::FilterScatter, + _ => return Err(CanonicalCodecError::UnknownTag), + }; + let source = self.ty()?; + let terminal_input = self.ty()?; + let terminal_output = self.ty()?; + let terminal = self.call()?; + let terminal_abi = self.abi()?; + let terminal_captures = self.types()?; + let count = self.count(1)?; + let mut stages = Vec::with_capacity(count.min(1024)); + for _ in 0..count { + stages.push(self.stage()?); + } + let work_weight = self.byte()?; + Ok(ParallelGeneratedId { + mode, + source, + terminal_input, + terminal_output, + terminal, + terminal_abi, + terminal_captures, + stages, + work_weight, + }) + } + + fn stage(&mut self) -> Result { + let tag = self.byte()?; + match tag { + 0 | 1 => { + let target = self.call()?; + let abi = self.abi()?; + let input = self.ty()?; + let output = self.ty()?; + let captures = self.types()?; + if tag == 0 { + Ok(ParallelStageId::Map { + target, + abi, + input, + output, + captures, + }) + } else { + Ok(ParallelStageId::Filter { + target, + abi, + input, + output, + captures, + }) + } + } + 2 => Ok(ParallelStageId::FilterStrContains { + input: self.ty()?, + output: self.ty()?, + needle: self.ty()?, + }), + 3 => Ok(ParallelStageId::Project { + input: self.ty()?, + output: self.ty()?, + field: self.u32()?, + }), + 4 => Ok(ParallelStageId::FilterField { + input: self.ty()?, + output: self.ty()?, + field: self.u32()?, + }), + _ => Err(CanonicalCodecError::UnknownTag), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn hex(value: &str) -> Vec { + assert_eq!(value.len() % 2, 0); + value + .as_bytes() + .chunks_exact(2) + .map(|pair| { + let text = std::str::from_utf8(pair).unwrap(); + u8::from_str_radix(text, 16).unwrap() + }) + .collect() + } + + fn ty(value: &str) -> CanonicalTy { + CanonicalTy::decode(&hex(value)).unwrap() + } + + fn abi(value: &str) -> CanonicalFnAbi { + CanonicalFnAbi::decode(&hex(value)).unwrap() + } + + fn call(value: &str) -> ProgramCall { + ProgramCall::try_from_logical(value).unwrap() + } + + fn roundtrip(value: GeneratedId) -> Vec { + let bytes = value.to_canonical_bytes().unwrap(); + assert_eq!(GeneratedId::decode(&bytes).unwrap(), value); + bytes.into() + } + + #[test] + fn generated_identity_codec() { + let unit = ty("010000000038"); + let bool_ty = ty("010000000002"); + let i64_ty = ty("0100000000000140"); + let slice_i64 = ty("01000000000d000140"); + let empty_abi = abi("01000000000100000000380000"); + let i64_abi = abi("010100000000010000000000014001000000000001400000"); + + let goldens = [ + ( + GeneratedId::FnValue { + target: call("f"), + signature: empty_abi.clone(), + }, + "0100010000006601000000000100000000380000", + ), + ( + GeneratedId::Closure { + lifted: call("l"), + explicit_signature: empty_abi.clone(), + captures: vec![bool_ty.clone()], + }, + "0101010000006c0100000000010000000038000001000000010000000002", + ), + ( + GeneratedId::Task { + fallible: false, + result: unit.clone(), + }, + "010200010000000038", + ), + ( + GeneratedId::Task { + fallible: true, + result: i64_ty.clone(), + }, + "0102010100000000000140", + ), + ]; + for (value, expected) in goldens { + let expected = hex(expected); + assert_eq!(roundtrip(value.clone()), expected); + assert_eq!(GeneratedId::decode(&expected).unwrap(), value); + } + + let parallel = GeneratedId::Parallel(ParallelGeneratedId { + mode: ParallelKernelMode::Materialize, + source: slice_i64, + terminal_input: i64_ty.clone(), + terminal_output: i64_ty.clone(), + terminal: call("f"), + terminal_abi: i64_abi, + terminal_captures: vec![], + stages: vec![], + work_weight: 1, + }); + let expected = hex( + "01030001000000000d000140010000000000014001000000000001400100000066010100000000010000000000014001000000000001400000000000000000000001", + ); + assert_eq!(roundtrip(parallel.clone()), expected); + assert_eq!(GeneratedId::decode(&expected).unwrap(), parallel); + + let stages = vec![ + ParallelStageId::Map { + target: call("map"), + abi: empty_abi.clone(), + input: unit.clone(), + output: bool_ty.clone(), + captures: vec![i64_ty.clone()], + }, + ParallelStageId::Filter { + target: call("filter"), + abi: empty_abi, + input: bool_ty.clone(), + output: bool_ty.clone(), + captures: vec![], + }, + ParallelStageId::FilterStrContains { + input: bool_ty.clone(), + output: bool_ty.clone(), + needle: bool_ty.clone(), + }, + ParallelStageId::Project { + input: bool_ty.clone(), + output: i64_ty.clone(), + field: 3, + }, + ParallelStageId::FilterField { + input: i64_ty.clone(), + output: i64_ty.clone(), + field: 4, + }, + ]; + for mode in [ + ParallelKernelMode::FilterCount, + ParallelKernelMode::FilterScatter, + ] { + roundtrip(GeneratedId::Parallel(ParallelGeneratedId { + mode, + source: unit.clone(), + terminal_input: i64_ty.clone(), + terminal_output: i64_ty.clone(), + terminal: call("terminal"), + terminal_abi: abi("01000000000100000000380000"), + terminal_captures: vec![bool_ty.clone()], + stages: stages.clone(), + work_weight: 4, + })); + } + } + + #[test] + fn generated_identity_error_precedence() { + assert_eq!( + GeneratedId::decode(&[]), + Err(CanonicalCodecError::Truncated) + ); + assert_eq!( + GeneratedId::decode(&[2]), + Err(CanonicalCodecError::UnsupportedVersion) + ); + assert_eq!( + GeneratedId::decode(&[1, 0xff]), + Err(CanonicalCodecError::UnknownTag) + ); + assert_eq!( + GeneratedId::decode(&[1, 2, 2]), + Err(CanonicalCodecError::InvalidBool) + ); + assert_eq!( + GeneratedId::decode(&[1, 0, 0, 0, 0, 0]), + Err(CanonicalCodecError::InvalidGraph) + ); + assert_eq!( + GeneratedId::decode(&[1, 0, 1, 0, 0, 0, 0xff]), + Err(CanonicalCodecError::InvalidUtf8) + ); + assert_eq!( + GeneratedId::decode(&[1, 0, 1, 0, 0, 0, 0]), + Err(CanonicalCodecError::EmbeddedNul) + ); + + let valid = GeneratedId::Task { + fallible: false, + result: ty("010000000038"), + } + .to_canonical_bytes() + .unwrap(); + let mut trailing = valid.to_vec(); + trailing.push(0); + assert_eq!( + GeneratedId::decode(&trailing), + Err(CanonicalCodecError::TrailingBytes) + ); + + let invalid_parallel = GeneratedId::Parallel(ParallelGeneratedId { + mode: ParallelKernelMode::FilterCount, + source: ty("010000000038"), + terminal_input: ty("010000000038"), + terminal_output: ty("010000000038"), + terminal: call("f"), + terminal_abi: abi("01000000000100000000380000"), + terminal_captures: vec![], + stages: vec![], + work_weight: 3, + }); + assert_eq!( + invalid_parallel.to_canonical_bytes(), + Err(CanonicalCodecError::InvalidGraph) + ); + } + + #[test] + fn deep_generated_identity_codec_is_stack_bounded() { + let value = GeneratedId::Closure { + lifted: call("deep"), + explicit_signature: abi("01000000000100000000380000"), + captures: vec![ty("010000000038"); 4096], + }; + let bytes = value.to_canonical_bytes().unwrap(); + assert_eq!(GeneratedId::decode(&bytes).unwrap(), value); + assert_eq!( + GeneratedId::decode(&bytes[..bytes.len() - 1]), + Err(CanonicalCodecError::Truncated) + ); + } +} diff --git a/crates/align_mir/src/lib.rs b/crates/align_mir/src/lib.rs index ebd60470..256a8378 100644 --- a/crates/align_mir/src/lib.rs +++ b/crates/align_mir/src/lib.rs @@ -21,6 +21,7 @@ use std::rc::Rc; pub mod print; mod canonical_graph; +mod generated_id; mod source_shape; mod validate_hir; mod runtime_key; @@ -29,6 +30,9 @@ pub use canonical_graph::{ CanonicalCodecError, CanonicalFnAbi, CanonicalTy, FunctionTypeDef, ProgramCall, ProgramCallError, function_types_are_canonical, }; +pub use generated_id::{ + GeneratedId, ParallelGeneratedId, ParallelKernelMode, ParallelStageId, +}; pub use runtime_key::RuntimeKey; #[cfg(test)] From d034eda9984a3eaf08a5027ddb6f70d12c8035c9 Mon Sep 17 00:00:00 2001 From: sanohiro Date: Wed, 5 Aug 2026 14:04:42 +0900 Subject: [PATCH 6/7] feat(codegen): activate canonical callable identities --- HANDOFF.md | 57 +- bench/library_boundary/Cargo.lock | 1 + bench/library_boundary/Cargo.toml | 1 + bench/library_boundary/README.md | 6 + bench/library_boundary/run.sh | 2 +- bench/library_boundary/src/main.rs | 102 + crates/align_codegen_llvm/src/lib.rs | 2233 ++++++++++++----- crates/align_codegen_llvm/src/thinlto.rs | 16 +- crates/align_driver/src/lib.rs | 2 +- crates/align_driver/src/main.rs | 7 +- crates/align_driver/tests/deep_pipeline.rs | 2 +- crates/align_driver/tests/export_roots.rs | 40 +- .../tests/interface_param_modes.rs | 22 +- crates/align_driver/tests/m5.rs | 2 +- crates/align_driver/tests/mir_continuation.rs | 21 +- .../tests/owned_tagged_payloads.rs | 4 +- crates/align_driver/tests/per_unit_codegen.rs | 25 +- .../align_driver/tests/return_provenance.rs | 20 +- crates/align_driver/tests/thin_lto.rs | 25 +- crates/align_interface/src/lib.rs | 4 +- crates/align_mir/src/canonical_graph.rs | 21 + crates/align_mir/src/generated_id.rs | 19 +- crates/align_mir/src/lib.rs | 353 ++- crates/align_mir/src/print.rs | 32 +- crates/align_mir/src/validate_hir_tests.rs | 107 +- 25 files changed, 2309 insertions(+), 815 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index b6aee912..ab3ace99 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -5,16 +5,18 @@ about the present state, the next decision, and operational facts. The former per-PR journal is preserved in [`docs/archive/HANDOFF-2026-07-25.md`](docs/archive/HANDOFF-2026-07-25.md). -_Last updated: 2026-08-05._ Current `main` is `ddc3f393`, including merged C1 -PR #702, C2a1 PR #705, and C2a2a PR #707. C2a2a's typed source-shape comparator, -parity owners, final-SHA preflight, post-open review, and three-platform CI are -complete. No public `pkg.db` surface exists yet. +_Last updated: 2026-08-05._ The C-A canonical callable capability is complete +through am-c3. MIR now separates typed program and runtime direct targets, LLVM +program identities are encoded consistently across whole-program, per-unit, +export, main-wrapper, and ThinLTO paths, and every generated callable family uses +canonical collected identity plus deterministic collision probing. No public +`pkg.db` surface exists yet. The remaining compiler plan uses consumer-complete capability waves rather than one PR per dormant acceptance cell: ```text -C-A canonical callable closure c2a2b through c3 +C-A canonical callable closure complete through c3 C-B borrow/ownership closure af/ar/ap/t/b + L2c/L2d/L2e after C-B, in parallel: @@ -922,32 +924,12 @@ was rerun after #636. #637-#644 passed their focused and PR CI gates. ## Next work -Request 6 implementation and its consolidated review repairs are complete in this worktree but are -not yet merged. Ordinary JSON decode, encode, and scope Drop retain the currently admitted -`Option` shape; only the scanner path is Copy-gated, while partial-error cleanup remains -a separate ownership request. For imported/per-unit consumers, interface/import reconstruction -precedes active `align_mir::hir_program_is_valid`, which precedes MIR/runtime lowering. The source -sema gate uses the canonical recursive `DropPlan`; the active HIR replay walks direct expression -children iteratively and rejects direct, transitive, generic, imported, missing, cyclic, and -malformed-row definitions without activating the dormant body validator. The repair also preserves -outer scanner source spelling through bare generic calls, stops at the first generic argument error, -snapshots all cache-owned files on rejected walks, closes valid-Span precedence pairs, and exercises -the production CodegenKey classifier and non-build-id digest. - -Latest verification on `aa5bb7d`: `scripts/test-pr.sh` passes (workspace build, all bounded library -tests, interface/formatter integration tests, and m0); `cargo +1.96.1 test -p align_sema --lib -function_return_completeness_matrix -- --nocapture` passes; generic m5 owners pass 11/11; imported -module owners pass 4/4; the cache generic no-publication owner passes 1/1; the full m5 aggregate is -193 passed / 5 known pre-existing failures; and `scripts/compare-json-scan-identity.sh HEAD` passes -with the required baseline parity and compiler-isolated identity. `cargo +1.96.1 check -p align_sema --p align_mir -p align_driver --tests --locked`, the matching clippy command, and `git diff --check` -are clean. `cargo fmt --all -- --check` remains N/A because the checkout has pre-existing -workspace-wide rustfmt drift; the changed sema file shares that pre-existing file-wide drift. -Fresh final review and preflight evidence must bind the final pushed head. -Do not update `.align-revision` or the align-llm Request 6 verification status until this -implementation is merged, the sibling release compiler is rebuilt, and the real-client adoption -gate passes. The design PR's `git diff --check`, exact diagnostic consistency, active-gate -references, ordinary Move-option boundary checks, and hosted checks all passed. +C-A closes the canonical callable plan from c2a2b through c3 as one consumer-complete wave. The +next implementation is C-B: direct, captured, and imported return-provenance closure through +L2b-b, followed in the same ownership capability by Move-return cleanup and shared/exclusive +borrow consumers through L2e. Reopen the C-B closure matrix before coding and preserve the exact +control-flow/type-reconciliation matrix in the repository instructions. F-A/F-B/F-C begin only +after C-B. The query-centered `pkg.db` design and its general library-boundary prerequisites are specified in `docs/impl/pkg-design/db.md` and `docs/impl/17-library-boundary-prerequisites.md`; the feasibility @@ -1023,9 +1005,9 @@ outcome-sensitive task-wait dominance (#685), am-v native output-buffer local/mu #688 completes am-u lexical extern invocation, #690 am-p placement, #691 am-n nominal/link, and #692 am-h declarations/headers, #694 am-b1, #695 am-b2a, #696 am-b2b, commit `af5e17a` am-b3, #699 task-wait replay, #700 body-fact replay, merged am-b4 activation PR #706, merged #702 am-c1, -merged #705 am-c2a1, and merged #707 am-c2a2a. The remaining work is grouped by capability: -canonical callable closure through c3, return-provenance closure through b, then cleanup and -borrow closure through L2e. Am-c follows am-b4 because it consumes body-validated callable facts. +merged #705 am-c2a1, and merged #707 am-c2a2a. C-A subsequently closes the canonical callable +vertical through c3. The remaining L2 work is C-B: return-provenance closure through b, then cleanup +and borrow closure through L2e. Am-c follows am-b4 because it consumes body-validated callable facts. The former thirty-two-L2b/thirty-six-L2 PR schedule is retired; the acceptance cells remain. The final author pass found one additional hidden dependency before review convergence: imported effect bits previously arrived only through the sema call's out-of-band map and did not survive in @@ -1296,10 +1278,9 @@ an under-approximating or dangling fact. Its final local provenance benchmark reports 3.147 ms/check, 22,848 interface bytes, and 1.844 ms/import on Apple Silicon. Do not begin a safe SQLite/PostgreSQL driver or add database-named compiler variants before L1a–L7 -are complete. The completed L2 cells run through c2a2a. The remaining cells close in three -capability waves: canonical callable closure through c3, direct/captured return-provenance closure, -and cleanup plus shared/mutable-borrow closure. These are acceptance cells, not a thirty-six-PR -schedule. After L2, L3 resources, L4 regions, and L5 static Query/command artifacts may proceed +are complete. The completed L2 cells run through c3. The remaining cells close in the C-B +direct/captured return-provenance, cleanup, and shared/mutable-borrow capability wave. These are +acceptance cells, not a thirty-six-PR schedule. After L2, L3 resources, L4 regions, and L5 static Query/command artifacts may proceed concurrently; L6 follows L4 and L7 closes the integrated generic surface. No safe driver begins before the complete prerequisite gate. L2 includes contextual parameter parsing, all-peer mutable-borrow alias checking, drop-old replacement, target-relative diff --git a/bench/library_boundary/Cargo.lock b/bench/library_boundary/Cargo.lock index c454ae37..c6747987 100644 --- a/bench/library_boundary/Cargo.lock +++ b/bench/library_boundary/Cargo.lock @@ -8,6 +8,7 @@ version = "0.0.0" dependencies = [ "align_driver", "align_interface", + "align_mir", "align_span", ] diff --git a/bench/library_boundary/Cargo.toml b/bench/library_boundary/Cargo.toml index c6c78868..5eeebc0b 100644 --- a/bench/library_boundary/Cargo.toml +++ b/bench/library_boundary/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dependencies] align_driver = { path = "../../crates/align_driver" } align_interface = { path = "../../crates/align_interface" } +align_mir = { path = "../../crates/align_mir" } align_span = { path = "../../crates/align_span" } [workspace] diff --git a/bench/library_boundary/README.md b/bench/library_boundary/README.md index 89acda0f..dc2e86a3 100644 --- a/bench/library_boundary/README.md +++ b/bench/library_boundary/README.md @@ -29,6 +29,12 @@ a 256-function, high-CFG fixture with three expression-valued branches per funct - `mir-nominal-link-validation`: whole-program MIR-lowering milliseconds per iteration for nominal structs/enums, repeated id-free source-shape twins, and one linked library, isolating the nominal/source-identity, enum-base, alignment, and link-name preflight. +- `canonical-source-shape-comparison`: whole-program MIR-lowering milliseconds per iteration for + the deterministic repeated-source-shape workload used by the canonical comparator observer. +- `canonical-type-graph`: milliseconds to construct canonical semantic records for every function + root in a 128-nominal graph. +- `mir-callable-namespace-validation`: whole-program MIR-lowering milliseconds per iteration for + 512 typed program declarations and 256 direct program targets. - `mir-header-validation`: paired valid and malformed whole-program MIR-lowering milliseconds per iteration for a large function-header/signature fixture, isolating declaration/header validation and its canonical-empty failure path. diff --git a/bench/library_boundary/run.sh b/bench/library_boundary/run.sh index d2d78209..531c7206 100755 --- a/bench/library_boundary/run.sh +++ b/bench/library_boundary/run.sh @@ -2,7 +2,7 @@ set -euo pipefail repo_root="$(cd "$(dirname "$0")/../.." && pwd)" -exec cargo run \ +exec "$repo_root/scripts/cargo.sh" run \ --quiet \ --release \ --manifest-path "$repo_root/bench/library_boundary/Cargo.toml" \ diff --git a/bench/library_boundary/src/main.rs b/bench/library_boundary/src/main.rs index 59adab74..ee681a1b 100644 --- a/bench/library_boundary/src/main.rs +++ b/bench/library_boundary/src/main.rs @@ -136,6 +136,32 @@ fn mir_nominal_link_fixture() -> String { source } +fn canonical_type_graph_fixture() -> String { + let mut source = String::new(); + for index in 0..128 { + source.push_str(&format!("Canonical_{index:04} {{ value: i64 }}\n")); + source.push_str(&format!( + "fn canonical_{index:04}(value: Canonical_{index:04}) -> Canonical_{index:04} = value\n" + )); + } + source.push_str("fn main() -> i32 = 0\n"); + source +} + +fn callable_namespace_fixture() -> String { + let mut source = String::new(); + for index in 0..256 { + source.push_str(&format!( + "fn target_{index:04}(value: i64) -> i64 = value + {index}\n" + )); + source.push_str(&format!( + "fn caller_{index:04}(value: i64) -> i64 = target_{index:04}(value)\n" + )); + } + source.push_str("fn main() -> i32 = caller_0255(1) as i32\n"); + source +} + fn import_validation_fixture() -> InterfaceSummary { let parameter = ITypeParam { name: "T".to_string(), @@ -328,6 +354,82 @@ fn run_provenance() { "mir-nominal-link-validation\t{milliseconds:.3}\tms/lower\t{nominal_definitions}\tdefinitions" ); + let mut iterations = 0_u64; + let start = Instant::now(); + while start.elapsed() < minimum { + let mir = align_driver::lower_to_mir(black_box(&nominal_hir)); + black_box(mir); + iterations += 1; + } + let elapsed = start.elapsed(); + let milliseconds = elapsed.as_secs_f64() * 1_000.0 / iterations as f64; + println!( + "canonical-source-shape-comparison\t{milliseconds:.3}\tms/lower\t{nominal_definitions}\tdefinitions" + ); + + let canonical_source = canonical_type_graph_fixture(); + let mut source_map = align_span::SourceMap::new(); + let canonical_checked = align_driver::check( + &mut source_map, + "canonical-type-graph.align", + &canonical_source, + ); + assert!( + !canonical_checked.diags.has_errors(), + "canonical type-graph fixture must check" + ); + let canonical_mir = align_driver::lower_to_mir(&canonical_checked.hir); + let canonical_roots = canonical_mir.fns.len(); + for function in &canonical_mir.fns { + align_mir::CanonicalTy::from_program(function.ret, &canonical_mir) + .expect("canonical fixture root"); + } + let mut iterations = 0_u64; + let start = Instant::now(); + while start.elapsed() < minimum { + for function in &canonical_mir.fns { + let canonical = align_mir::CanonicalTy::from_program( + black_box(function.ret), + black_box(&canonical_mir), + ) + .expect("benchmark canonical root"); + black_box(canonical); + } + iterations += 1; + } + let elapsed = start.elapsed(); + let milliseconds = elapsed.as_secs_f64() * 1_000.0 / iterations as f64; + println!( + "canonical-type-graph\t{milliseconds:.3}\tms/all-roots\t{canonical_roots}\troots" + ); + + let callable_source = callable_namespace_fixture(); + let mut source_map = align_span::SourceMap::new(); + let callable_checked = align_driver::check( + &mut source_map, + "mir-callable-namespace.align", + &callable_source, + ); + assert!( + !callable_checked.diags.has_errors(), + "callable namespace fixture must check" + ); + let callable_mir = align_driver::lower_to_mir(&callable_checked.hir); + let callable_count = callable_mir.fns.len(); + assert!(callable_count >= 512, "fixture must retain every callable declaration"); + let mut iterations = 0_u64; + let start = Instant::now(); + while start.elapsed() < minimum { + let mir = align_driver::lower_to_mir(black_box(&callable_checked.hir)); + black_box(mir); + iterations += 1; + } + let elapsed = start.elapsed(); + let milliseconds = elapsed.as_secs_f64() * 1_000.0 / iterations as f64; + println!( + "mir-callable-namespace-validation\t{milliseconds:.3}\tms/lower\t{callable_count}\tcallables" + ); + let header_source = provenance_fixture(); let mut source_map = align_span::SourceMap::new(); let checked = align_driver::check( diff --git a/crates/align_codegen_llvm/src/lib.rs b/crates/align_codegen_llvm/src/lib.rs index 7a7992e5..f3c7ae45 100644 --- a/crates/align_codegen_llvm/src/lib.rs +++ b/crates/align_codegen_llvm/src/lib.rs @@ -26,7 +26,11 @@ mod drop_codegen; mod runtime_abi; use align_ast::{BinOp, UnOp}; -use align_mir::{Block, Const, ConstElem, Function, Operand, ParMapStage, ParMapStageKind, Program, RuntimeKey, Rvalue, Slot, Stmt, Term, ValueId}; +use align_mir::{ + Block, CanonicalFnAbi, CanonicalTy, Const, ConstElem, DirectCall, Function, GeneratedId, + Operand, ParMapStage, ParMapStageKind, ParallelGeneratedId, ParallelKernelMode, + ParallelStageId, Program, ProgramCall, RuntimeKey, Rvalue, Slot, Stmt, Term, ValueId, +}; use align_sema::{ DropPlan, ERROR_VARIANT_CODE, EnumDef, FloatTy, IntTy, Layout, Scalar, StructDef, TupleDef, Ty, drop_plan, enum_is_move, hir, scalar_to_ty, struct_is_move, ty_to_scalar, @@ -353,8 +357,8 @@ fn create_target_machine(target: &BuildTarget, opt: OptimizationLevel) -> Result /// Write the program as an object file. /// -/// `exports` names the program functions (matched against `Function::name`, NOT the LLVM-symbol -/// `main`/`align_main` split) that keep `external` linkage instead of the default whole-program +/// `exports` names the program functions (matched against `Function::name`, not their encoded LLVM +/// symbols) that keep `external` linkage instead of the default whole-program /// `internal` (M13 Slice 1) — the explicit export-roots mechanism (`emit-obj --export`, /// `docs/impl/07-roadmap.md` M13 Codex-audit item 1). Empty = every program function stays /// internal, today's default behavior. @@ -674,6 +678,33 @@ struct RuntimeDeclarations { physical_names: HashMap, } +#[derive(Clone, Debug, Eq, PartialEq)] +struct ProgramSignature { + params: Vec, + modes: Vec, + ret: Ty, + borrow: hir::ReturnBorrowSummary, + region: hir::ReturnRegionSummary, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ProgramDeclarationClass { + Stored, + Imported, + Extern, +} + +#[derive(Clone, Debug)] +struct ProgramDeclaration { + signature: ProgramSignature, + classes: Vec, +} + +struct CallablePreflight { + declarations: HashMap, + generated_names: HashMap, +} + fn build_module<'c>( ctx: &'c Context, module: &Module<'c>, @@ -685,6 +716,7 @@ fn build_module<'c>( ) -> Result { runtime_abi::validate_registry().map_err(CodegenError::Lowering)?; validate_tagged_program(program)?; + let callable_declarations = callable_declarations(program)?; // Target layout (for struct field offsets in `json.decode`); also pin the module's data // layout so offsets match the emitted object. let target_data = tm.get_target_data(); @@ -890,7 +922,7 @@ fn build_module<'c>( ))); } } - let extern_abi: HashMap = program + let extern_abi: HashMap = program .externs .iter() .map(|e| { @@ -929,8 +961,10 @@ fn build_module<'c>( let mut extern_fn_types: HashMap> = HashMap::with_capacity(program.externs.len()); for ext in &program.externs { - let abi = &extern_abi[&ext.name]; - check_sysv_struct_args_fit(&ext.name, abi, &ext.params, &program.structs)?; + let abi = extern_abi + .get(&ext.name) + .ok_or_else(|| callable_target_error(&ext.name))?; + check_sysv_struct_args_fit(ext.name.as_str(), abi, &ext.params, &program.structs)?; let mut param_types: Vec = Vec::with_capacity(ext.params.len()); for (pa, &ty) in abi.params.iter().zip(&ext.params) { match pa { @@ -975,32 +1009,34 @@ fn build_module<'c>( ))); } }; - if !runtime_abi::native_extern_abi_matches(&ext.name, fn_ty, ctx) { + if !runtime_abi::native_extern_abi_matches(ext.name.as_str(), fn_ty, ctx) { return Err(CodegenError::Lowering(format!( "native extern ABI mismatch:{}", lowercase_hex(ext.name.as_bytes()), ))); } - extern_fn_types.insert(ext.name.clone(), fn_ty); + extern_fn_types.insert(ext.name.as_str().to_owned(), fn_ty); } + let callable_preflight = callable_preflight(program, exports, callable_declarations)?; - // Pass 1: declare all functions so calls resolve regardless of order. A `Result`- or - // `Unit`-returning `main` is emitted under `align_main`; a C `main` wrapper is - // generated after the bodies (see below). - let mut funcs: HashMap> = HashMap::new(); + // Pass 1: declare all functions so calls resolve regardless of order. A wrapped `main` body + // uses its ordinary encoded Align identity; the external C `main` wrapper is emitted later. + let mut program_funcs: HashMap> = HashMap::new(); + let mut generated_funcs: HashMap> = HashMap::new(); for f in &program.fns { + let symbol = symbol_name(f, exports); let fv = declare_fn( ctx, module, f, - symbol_name(f), + &symbol, &struct_types, &enum_types, &tagged_types, &tuple_types, exports, ); - funcs.insert(f.name.clone(), fv); + program_funcs.insert(f.name.clone(), fv); } // M15 S2 (per-unit): non-generic `pub` functions declared by interface-only dependencies. Each is // an external, bodyless `declare` under the same Align ABI a defining unit emits @@ -1011,7 +1047,7 @@ fn build_module<'c>( for imp in &program.imported_fns { // A name collision with a locally-defined function would be a driver bug (a unit must not both // define and import the same symbol); prefer the local definition and skip the declare. - if funcs.contains_key(&imp.name) { + if program_funcs.contains_key(&imp.name) { continue; } let fv = declare_imported_fn( @@ -1023,12 +1059,12 @@ fn build_module<'c>( &tagged_types, &tuple_types, ); - funcs.insert(imp.name.clone(), fv); + program_funcs.insert(imp.name.clone(), fv); } // Keep the semantic signatures alongside the LLVM declarations. The latter intentionally // erase integer signedness, while a fused range kernel must reject a malformed MIR call whose // physical `i64` happens to be compatible with the wrong declared `Ty`. - let mut fn_sigs: HashMap = program + let mut fn_sigs: HashMap = program .fns .iter() .map(|f| { @@ -1048,20 +1084,22 @@ fn build_module<'c>( // No `mark_nounwind`: unlike an Align function, foreign code is outside our control, so we do not // assert it never unwinds. for ext in &program.externs { - let fn_ty = extern_fn_types[&ext.name]; + let fn_ty = extern_fn_types + .get(ext.name.as_str()) + .copied() + .ok_or_else(|| callable_target_error(&ext.name))?; // Defensive: if the symbol is already in the module (e.g. it coincides with a symbol // declared earlier), reuse that declaration. A fresh `add_function` on a duplicate name // makes LLVM silently rename it (`@abs.1`), which then fails to link against the real // external symbol. let fv = module - .get_function(&ext.name) - .unwrap_or_else(|| module.add_function(&ext.name, fn_ty, None)); - funcs.insert(ext.name.clone(), fv); + .get_function(ext.name.as_str()) + .unwrap_or_else(|| module.add_function(ext.name.as_str(), fn_ty, None)); + program_funcs.insert(ext.name.clone(), fv); } // The fixed native ABI table is the sole declaration/type/attribute authority. Program, // imported, and extern declarations intentionally remain earlier in the module. Keyed native - // rows are then emitted in alphabetical RuntimeKey::ALL order; the legacy mixed alias map is - // populated from the same handles until typed MIR direct calls land in am-c3. + // rows are then emitted in alphabetical RuntimeKey::ALL order into the typed runtime registry. let ptr = ctx.ptr_type(AddressSpace::default()); let mut runtime_funcs: HashMap> = HashMap::with_capacity(RuntimeKey::ALL.len()); @@ -1071,7 +1109,10 @@ fn build_module<'c>( let key = abi .runtime_key() .expect("keyed runtime iterator yielded an unkeyed row"); - let compatible_extern = program.externs.iter().any(|ext| ext.name == abi.symbol); + let compatible_extern = program + .externs + .iter() + .any(|ext| ext.name.as_str() == abi.symbol); let function = if compatible_extern { let existing = module .get_function(abi.symbol) @@ -1089,7 +1130,6 @@ fn build_module<'c>( if !(rt_lto_skip_guarded && abi.is_rt_lto_guarded()) { abi.apply_attributes(ctx, function); } - funcs.insert(key.logical_name().to_string(), function); runtime_funcs.insert(key, function); runtime_physical_names.insert( key, @@ -1101,18 +1141,31 @@ fn build_module<'c>( // value has the env-ABI `fn(env, args)`; a non-capturing / named function is wrapped by // `name$fnval(env, args) = name(args)` so all closure callees share that ABI (the env pointer // is null and ignored). Capturing closures (a later slice) instead point at an env-reading fn. - let mut thunk_names: std::collections::BTreeSet = std::collections::BTreeSet::new(); + let mut thunk_names: std::collections::BTreeSet = + std::collections::BTreeSet::new(); for f in &program.fns { for b in &f.blocks { for s in &b.stmts { - if let Stmt::Let(_, Rvalue::FnAddr { name, .. }) = s { - thunk_names.insert(name.clone()); + if let Stmt::Let(_, Rvalue::FnAddr { target, .. }) = s { + thunk_names.insert(target.clone()); } } } } for name in &thunk_names { - let orig = *funcs + let declaration = callable_preflight + .declarations + .get(name) + .ok_or_else(|| callable_target_error(name))?; + let id = GeneratedId::FnValue { + target: name.clone(), + signature: canonical_signature(&declaration.signature, program)?, + }; + let emitted_name = callable_preflight + .generated_names + .get(&id) + .ok_or_else(|| CodegenError::Lowering("function-value identity was not collected".into()))?; + let orig = *program_funcs .get(name) .ok_or_else(|| CodegenError::Lowering(format!("unknown function {name}")))?; let orig_ty = orig.get_type(); @@ -1122,7 +1175,7 @@ fn build_module<'c>( Some(rt) => rt.fn_type(¶ms, false), None => ctx.void_type().fn_type(¶ms, false), }; - let thunk = module.add_function(&format!("{name}$fnval"), thunk_ty, None); + let thunk = module.add_function(emitted_name, thunk_ty, None); mark_nounwind(ctx, thunk); mark_private_helper(thunk); let bb = ctx.append_basic_block(thunk, "entry"); @@ -1136,13 +1189,14 @@ fn build_module<'c>( None => tb.build_return(None), } .map_err(|e| CodegenError::Lowering(e.to_string()))?; - funcs.insert(format!("{name}$fnval"), thunk); + generated_funcs.insert(id, thunk); } // Pass 1c: a closure thunk per lifted function used as a *capturing* closure. The env-ABI // thunk `lifted$clos(env, explicit…)` loads the captured values out of `env` and forwards them // as the lifted function's trailing capture parameters: `lifted(explicit…, env.0, env.1, …)`. - let mut closure_thunks: std::collections::BTreeMap> = std::collections::BTreeMap::new(); + let mut closure_thunks: std::collections::BTreeMap> = + std::collections::BTreeMap::new(); for f in &program.fns { for b in &f.blocks { for s in &b.stmts { @@ -1153,7 +1207,46 @@ fn build_module<'c>( } } for (lifted, capture_tys) in &closure_thunks { - let orig = *funcs + let declaration = callable_preflight + .declarations + .get(lifted) + .ok_or_else(|| callable_target_error(lifted))?; + let explicit = declaration + .signature + .params + .len() + .checked_sub(capture_tys.len()) + .ok_or_else(|| callable_target_error(lifted))?; + let explicit_params = declaration + .signature + .params + .get(..explicit) + .ok_or_else(|| callable_target_error(lifted))?; + let explicit_modes = declaration + .signature + .modes + .get(..explicit) + .ok_or_else(|| callable_target_error(lifted))?; + let explicit_signature = ProgramSignature { + params: explicit_params.to_vec(), + modes: explicit_modes.to_vec(), + ret: declaration.signature.ret, + borrow: declaration.signature.borrow.clone(), + region: declaration.signature.region.clone(), + }; + let id = GeneratedId::Closure { + lifted: lifted.clone(), + explicit_signature: canonical_signature(&explicit_signature, program)?, + captures: capture_tys + .iter() + .map(|ty| canonical_ty(*ty, program)) + .collect::, _>>()?, + }; + let emitted_name = callable_preflight + .generated_names + .get(&id) + .ok_or_else(|| CodegenError::Lowering("closure identity was not collected".into()))?; + let orig = *program_funcs .get(lifted) .ok_or_else(|| CodegenError::Lowering(format!("unknown lifted function {lifted}")))?; let orig_ty = orig.get_type(); @@ -1171,7 +1264,7 @@ fn build_module<'c>( Some(rt) => rt.fn_type(&tparams, false), None => ctx.void_type().fn_type(&tparams, false), }; - let thunk = module.add_function(&format!("{lifted}$clos"), thunk_ty, None); + let thunk = module.add_function(emitted_name, thunk_ty, None); mark_nounwind(ctx, thunk); mark_private_helper(thunk); let bb = ctx.append_basic_block(thunk, "entry"); @@ -1205,7 +1298,7 @@ fn build_module<'c>( None => tb.build_return(None), } .map_err(|e| CodegenError::Lowering(e.to_string()))?; - funcs.insert(format!("{lifted}$clos"), thunk); + generated_funcs.insert(id, thunk); } // Pass 1d: a `spawn` trampoline per result type `R`. `tramp$R(thunk, env, slot)` runs the @@ -1218,22 +1311,34 @@ fn build_module<'c>( // returns `0`, or returns the `Err` code (which `tg_wait` surfaces to `wait()?`). let lower = |e: inkwell::builder::BuilderError| CodegenError::Lowering(e.to_string()); let i32t = ctx.i32_type(); - let mut tramp_keys: std::collections::BTreeMap = std::collections::BTreeMap::new(); + let mut tramp_keys: std::collections::BTreeMap, (GeneratedId, Ty, bool)> = + std::collections::BTreeMap::new(); for f in &program.fns { for b in &f.blocks { for s in &b.stmts { if let Stmt::Let(_, Rvalue::SpawnTask { r, fallible, .. }) = s { - tramp_keys.insert(spawn_tramp_key(*r, *fallible), (*r, *fallible)); + let id = GeneratedId::Task { + fallible: *fallible, + result: canonical_ty(*r, program)?, + }; + tramp_keys.insert( + canonical_metadata(id.to_canonical_bytes())?, + (id, *r, *fallible), + ); } } } } // The builtin `Error` enum id (always registered), for fallible trampolines. let error_id = program.enums.iter().position(|e| e.name == "Error").map(|i| i as u32); - for (key, (r, fallible)) in &tramp_keys { + for (id, r, fallible) in tramp_keys.values() { // `tramp(thunk, env, slot, err_slot) -> i32` (0 = ok, 1 = errored). let fn_ty = i32t.fn_type(&[ptr.into(), ptr.into(), ptr.into(), ptr.into()], false); - let tramp = module.add_function(&format!("tramp${key}"), fn_ty, None); + let emitted_name = callable_preflight + .generated_names + .get(id) + .ok_or_else(|| CodegenError::Lowering("task identity was not collected".into()))?; + let tramp = module.add_function(emitted_name, fn_ty, None); mark_nounwind(ctx, tramp); mark_private_helper(tramp); let bb = ctx.append_basic_block(tramp, "entry"); @@ -1294,20 +1399,24 @@ fn build_module<'c>( tb.build_store(slot, res).map_err(lower)?; tb.build_return(Some(&i32t.const_zero())).map_err(lower)?; } - funcs.insert(format!("tramp${key}"), tramp); + generated_funcs.insert(id.clone(), tramp); } // Pass 2: define bodies. for f in &program.fns { let builder = ctx.create_builder(); let stack_headers = stack_header_plan(f); + let func = program_funcs + .get(&f.name) + .copied() + .ok_or_else(|| callable_target_error(&f.name))?; // Under debug info, give each function a DISubprogram (anchored to its first source line) // and attach it to the LLVM function, so its instructions can carry DILocations. let fn_line = debug_ctx.as_ref().map_or(0, |_| first_fn_line(f)); let subprogram = debug_ctx.as_ref().map(|dc| { let sp = dc.dib.create_function( dc.scope, - symbol_name(f), + &symbol_name(f, exports), None, dc.file, fn_line, @@ -1318,14 +1427,17 @@ fn build_module<'c>( DIFlags::ZERO, /* is_optimized */ true, ); - funcs[&f.name].set_subprogram(sp); + func.set_subprogram(sp); sp }); FnGen { ctx, module, builder: &builder, - funcs: &funcs, + program_funcs: &program_funcs, + generated_funcs: &generated_funcs, + callable_preflight: &callable_preflight, + program, runtime_funcs: &runtime_funcs, fn_sigs: &fn_sigs, extern_abi: &extern_abi, @@ -1340,7 +1452,7 @@ fn build_module<'c>( tuples: &program.tuples, target_data: &target_data, f, - func: funcs[&f.name], + func, slots: HashMap::new(), values: HashMap::new(), stack_header_slots: stack_headers.slots, @@ -1365,17 +1477,23 @@ fn build_module<'c>( // A `Result`- or `Unit`-returning main needs a C `main` wrapper: `Result` maps Ok/Err to an // exit code (and, when `main(args: array)`, marshals argv into the `array` // argument — the argv form is Result-only, sema-enforced); `Unit` has no error to report, so - // the wrapper just calls `align_main` and always returns a defined `0` (the bug this fixes — + // the wrapper just calls the encoded Align body and always returns a defined `0` (the bug this fixes — // previously a `()`-returning `main` WAS the C entry directly, declared `void`, and `ret void` // left the C ABI's i32 return register undefined; see `docs/open-questions.md` "Unit-returning // `fn main()` yields a nondeterministic exit code"). if let Some(f) = - program.fns.iter().find(|f| f.name == "main" && (matches!(f.ret, Ty::Result(..)) || f.ret == Ty::Unit)) + program.fns.iter().find(|f| { + f.name.as_str() == "main" + && (matches!(f.ret, Ty::Result(..)) || f.ret == Ty::Unit) + }) { emit_main_wrapper( ctx, module, - funcs["main"], + program_funcs + .get(&f.name) + .copied() + .ok_or_else(|| callable_target_error(&f.name))?, f.ret, !f.params.is_empty(), &extern_fn_types, @@ -1589,7 +1707,7 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { param_types: &[Ty], program: &Program, ) -> Result<(), CodegenError> { - if function.name != "main" { + if function.name.as_str() != "main" { return Ok(()); } if function.exportable { @@ -1624,9 +1742,9 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { fn named_signature<'a>( program: &'a Program, - name: &str, + name: &ProgramCall, ) -> Option<(Vec, Ty, &'a [align_ast::ParamMode])> { - if let Some(function) = program.fns.iter().find(|function| function.name == name) { + if let Some(function) = program.fns.iter().find(|function| &function.name == name) { return Some(( function .params @@ -1637,14 +1755,22 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { &function.param_modes, )); } - if let Some(function) = program.imported_fns.iter().find(|function| function.name == name) { + if let Some(function) = program + .imported_fns + .iter() + .find(|function| &function.name == name) + { return Some(( function.params.clone(), function.ret, &function.param_modes, )); } - program.externs.iter().find(|function| function.name == name).map(|function| { + program + .externs + .iter() + .find(|function| &function.name == name) + .map(|function| { ( function.params.clone(), function.ret, @@ -2240,13 +2366,11 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { continue; }; match rvalue { - Rvalue::FnAddr { name, signature } => { + Rvalue::FnAddr { target, signature } => { let Some((param_types, ret, modes)) = - named_signature(program, name) + named_signature(program, target) else { - return Err(CodegenError::Lowering(format!( - "function address refers to unknown target `{name}`" - ))); + return Err(callable_target_error(target)); }; check_signature_facts( SignatureFacts { @@ -2266,9 +2390,7 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { // indirect calls retain the all-compatible-input fallback. L2b-b attaches // target-relative roots and restores exact summary equality here. if signature.param_modes != modes { - return Err(CodegenError::Lowering(format!( - "function address signature facts disagree with target `{name}`" - ))); + return Err(callable_target_error(target)); } } Rvalue::Closure { @@ -2281,67 +2403,54 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { let Some(target) = program.fns.iter().find(|function| function.name == *lifted) else { - return Err(CodegenError::Lowering(format!( - "closure refers to unknown lifted target `{lifted}`" - ))); + return Err(callable_target_error(lifted)); }; let explicit = target.params.len().checked_sub(capture_tys.len()).ok_or_else( - || { - CodegenError::Lowering(format!( - "closure `{lifted}` has more captures than target parameters" - )) - }, + || callable_target_error(lifted), )?; - let target_modes = target.param_modes.get(..explicit).ok_or_else(|| { - CodegenError::Lowering(format!( - "closure `{lifted}` target parameter modes are malformed" - )) - })?; + let target_modes = target + .param_modes + .get(..explicit) + .ok_or_else(|| callable_target_error(lifted))?; let capture_modes = - target.param_modes.get(explicit..).ok_or_else(|| { - CodegenError::Lowering(format!( - "closure `{lifted}` target capture modes are malformed" - )) - })?; + target.param_modes.get(explicit..).ok_or_else(|| callable_target_error(lifted))?; if capture_modes .iter() .any(|mode| *mode != align_ast::ParamMode::ByValue) { - return Err(CodegenError::Lowering(format!( - "closure `{lifted}` target capture parameters must be ByValue" - ))); + return Err(callable_target_error(lifted)); } if captures.len() != capture_tys.len() { - return Err(CodegenError::Lowering(format!( - "closure `{lifted}` has {} capture operands but {} capture types", - captures.len(), - capture_tys.len() - ))); + return Err(callable_target_error(lifted)); } - let target_capture_tys = target.params[explicit..] + let target_capture_tys = target + .params + .get(explicit..) + .ok_or_else(|| callable_target_error(lifted))? .iter() .map(|slot| { - target.slots.get(*slot as usize).copied().ok_or_else(|| { - CodegenError::Lowering(format!( - "closure `{lifted}` target capture slot {slot} is missing" - )) - }) + target + .slots + .get(*slot as usize) + .copied() + .ok_or_else(|| callable_target_error(lifted)) }) .collect::, _>>()?; - let target_explicit_tys = target.params[..explicit] + let target_explicit_tys = target + .params + .get(..explicit) + .ok_or_else(|| callable_target_error(lifted))? .iter() .map(|slot| { - target.slots.get(*slot as usize).copied().ok_or_else(|| { - CodegenError::Lowering(format!( - "closure `{lifted}` target parameter slot {slot} is missing" - )) - }) + target + .slots + .get(*slot as usize) + .copied() + .ok_or_else(|| callable_target_error(lifted)) }) .collect::, _>>()?; if target_capture_tys != *capture_tys { - return Err(CodegenError::Lowering(format!( - "closure `{lifted}` capture types disagree with its target" - ))); + return Err(callable_target_error(lifted)); } check_signature_facts( SignatureFacts { @@ -2361,9 +2470,7 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { || signature.return_borrow != target.return_borrow || signature.return_region != target.return_region { - return Err(CodegenError::Lowering(format!( - "closure signature facts disagree with lifted target `{lifted}`" - ))); + return Err(callable_target_error(lifted)); } } Rvalue::CallIndirect { @@ -2460,29 +2567,957 @@ fn first_fn_line(f: &Function) -> u32 { .unwrap_or(1) } -/// The LLVM symbol for a function: a `Result`- or `Unit`-returning `main` is emitted as -/// `align_main` (the C `main` is a generated wrapper that always returns a defined `i32`); -/// everything else keeps its name. (An `-> i32` `main` needs no wrapper — it already returns -/// the C ABI's type directly — so it keeps the `main` symbol and IS the C entry.) -fn symbol_name(f: &Function) -> &str { - if f.name == "main" && (matches!(f.ret, Ty::Result(..)) || f.ret == Ty::Unit) { - "align_main" +/// Encode one logical Align program identity as a collision-free LLVM symbol. The direct i32 main +/// and explicit exports deliberately use their external identities instead; a wrapped main body +/// uses this encoding while its generated C entry keeps `main`. +fn encoded_program_symbol(name: &ProgramCall) -> String { + let hex = lowercase_hex(name.as_bytes()); + format!("align_fn${}${hex}", name.as_bytes().len()) +} + +fn symbol_name(f: &Function, exports: &[String]) -> String { + let direct_main = f.name.as_str() == "main" + && !matches!(f.ret, Ty::Result(..)) + && f.ret != Ty::Unit; + let explicit_export = f.name.as_str() != "main" + && exports.iter().any(|export| export == f.name.as_str()); + if direct_main || explicit_export { + f.name.as_str().to_owned() + } else { + encoded_program_symbol(&f.name) + } +} + +fn callable_hex(name: &ProgramCall) -> String { + lowercase_hex(name.as_bytes()) +} + +fn callable_target_error(name: &ProgramCall) -> CodegenError { + CodegenError::Lowering(format!("callable target invalid:{}", callable_hex(name))) +} + +fn program_signature(function: &Function) -> Result { + let params = function + .params + .iter() + .map(|slot| { + function + .slots + .get(*slot as usize) + .copied() + .ok_or_else(|| callable_target_error(&function.name)) + }) + .collect::, _>>()?; + Ok(ProgramSignature { + params, + modes: function.param_modes.clone(), + ret: function.ret, + borrow: function.return_borrow.clone(), + region: function.return_region.clone(), + }) +} + +fn register_program_declaration( + declarations: &mut HashMap, + name: &ProgramCall, + class: ProgramDeclarationClass, + signature: ProgramSignature, +) -> Result<(), CodegenError> { + let Some(existing) = declarations.get_mut(name) else { + declarations.insert( + name.clone(), + ProgramDeclaration { + signature, + classes: vec![class], + }, + ); + return Ok(()); + }; + let same_class = existing.classes.contains(&class); + let cross_class_allowed = matches!(class, ProgramDeclarationClass::Stored | ProgramDeclarationClass::Imported) + && existing + .classes + .iter() + .all(|existing| matches!(existing, ProgramDeclarationClass::Stored | ProgramDeclarationClass::Imported)); + let repeated_class_allowed = same_class && class != ProgramDeclarationClass::Stored; + if existing.signature != signature || (!cross_class_allowed && !repeated_class_allowed) { + return Err(CodegenError::Lowering(format!( + "callable declaration conflict:{}", + callable_hex(name) + ))); + } + if !same_class { + existing.classes.push(class); + } + Ok(()) +} + +fn canonical_metadata(result: Result) -> Result { + result.map_err(|error| CodegenError::Lowering(format!("callable metadata invalid:{error:?}"))) +} + +fn canonical_signature( + signature: &ProgramSignature, + program: &Program, +) -> Result { + let params = signature + .modes + .iter() + .copied() + .zip(signature.params.iter().copied()) + .collect::>(); + canonical_metadata(CanonicalFnAbi::from_parts( + ¶ms, + signature.ret, + &signature.borrow, + &signature.region, + program, + )) +} + +fn canonical_ty(ty: Ty, program: &Program) -> Result { + canonical_metadata(CanonicalTy::from_program(ty, program)) +} + +fn callable_metadata_error() -> CodegenError { + CodegenError::Lowering("callable metadata invalid:InvalidGraph".to_owned()) +} + +fn validate_parallel_callable( + declarations: &HashMap, + name: &ProgramCall, + params: &[Ty], + ret: Ty, +) -> Result<(), CodegenError> { + let declaration = declarations + .get(name) + .ok_or_else(|| callable_target_error(name))?; + if declaration.classes.contains(&ProgramDeclarationClass::Extern) + || declaration.signature.params != params + || declaration.signature.ret != ret + { + return Err(callable_target_error(name)); + } + Ok(()) +} + +fn parallel_kernel_ty_is_valid(ty: Ty, program: &Program) -> bool { + matches!(ty, Ty::Int(_) | Ty::Float(_) | Ty::Bool | Ty::Char | Ty::Str) + || matches!(ty, Ty::Struct(id) + if program.structs.get(id as usize).is_some() + && !struct_is_move(id, &program.structs, &program.enums, &program.tagged_types)) +} + +fn parallel_input_ty_is_valid(ty: Ty, program: &Program) -> bool { + matches!(ty, Ty::Slice(_)) || parallel_kernel_ty_is_valid(ty, program) +} + +fn parallel_capture_ty_is_valid(ty: Ty, program: &Program) -> bool { + if align_sema::ty_capture_is_move( + ty, + &program.structs, + &program.tuples, + &program.enums, + &program.tagged_types, + ) { + return false; + } + matches!( + ty, + Ty::Int(_) + | Ty::Float(_) + | Ty::Bool + | Ty::Char + | Ty::Unit + | Ty::Struct(_) + | Ty::Tuple(_) + | Ty::Array(_, _) + | Ty::StructArray(_, _) + | Ty::Slice(_) + | Ty::Soa(_) + | Ty::Str + | Ty::Raw + | Ty::HttpHeaders + | Ty::JsonDoc + | Ty::JsonScanner(_) + | Ty::Option(_) + | Ty::Result(_, _) + | Ty::Tagged(_) + | Ty::Enum(_) + | Ty::Rng + | Ty::Fn(_) + | Ty::Vec(_, _) + | Ty::Mask(_, _) + | Ty::ArenaHandle + ) +} + +#[allow(clippy::too_many_arguments)] +fn validate_parallel_request( + program: &Program, + declarations: &HashMap, + function: &Function, + result: Ty, + src: &Operand, + terminal: &ProgramCall, + stages: &[ParMapStage], + terminal_captures: &[Operand], + terminal_capture_tys: &[Ty], + elem_in: Ty, + elem_out: Ty, + work_weight: u8, + reduce: bool, +) -> Result<(), CodegenError> { + if !matches!(work_weight, 1 | 2 | 4) + || !parallel_input_ty_is_valid(elem_in, program) + || !parallel_kernel_ty_is_valid(elem_out, program) + || terminal_captures.len() != terminal_capture_tys.len() + || terminal_captures + .iter() + .zip(terminal_capture_tys) + .any(|(operand, ty)| { + preflight_operand_ty(function, operand) != Some(*ty) + || !parallel_capture_ty_is_valid(*ty, program) + }) + { + return Err(callable_metadata_error()); + } + let Some(source_ty) = preflight_operand_ty(function, src) else { + return Err(callable_metadata_error()); + }; + let source_elem = match source_ty { + Ty::Slice(scalar) | Ty::DynArray(scalar) => align_sema::scalar_to_ty(scalar), + Ty::DynSliceArray(primitive) => Ty::Slice(align_sema::prim_to_scalar(primitive)), + Ty::DynStructArray(id, Layout::Aos) => Ty::Struct(id), + _ => return Err(callable_metadata_error()), + }; + if source_elem != elem_in { + return Err(callable_metadata_error()); + } + + if reduce { + if !stages.is_empty() || !matches!(elem_out, Ty::Int(_)) || result != elem_out { + return Err(callable_metadata_error()); + } } else { - &f.name + let Some(output) = ty_to_scalar(elem_out) + .filter(|scalar| matches!(scalar, Scalar::Int(_) | Scalar::Float(_) | Scalar::Bool | Scalar::Char)) + else { + return Err(callable_metadata_error()); + }; + if result != Ty::DynArray(output) { + return Err(callable_metadata_error()); + } + } + + let mut stage_input = elem_in; + for stage in stages { + if stage.elem_in != stage_input + || stage.captures.len() != stage.capture_tys.len() + || stage + .captures + .iter() + .zip(&stage.capture_tys) + .any(|(operand, ty)| { + preflight_operand_ty(function, operand) != Some(*ty) + || !parallel_capture_ty_is_valid(*ty, program) + }) + { + return Err(callable_metadata_error()); + } + match stage.kind { + ParMapStageKind::Map | ParMapStageKind::Filter => { + let Some(target) = stage.func.as_ref() else { + return Err(callable_metadata_error()); + }; + if !parallel_input_ty_is_valid(stage.elem_in, program) + || (stage.kind == ParMapStageKind::Map + && !parallel_kernel_ty_is_valid(stage.elem_out, program)) + || (stage.kind == ParMapStageKind::Filter && stage.elem_out != stage.elem_in) + { + return Err(callable_metadata_error()); + } + let mut params = Vec::with_capacity(stage.capture_tys.len() + 1); + params.push(stage.elem_in); + params.extend(stage.capture_tys.iter().copied()); + let ret = if stage.kind == ParMapStageKind::Map { + stage.elem_out + } else { + Ty::Bool + }; + validate_parallel_callable(declarations, target, ¶ms, ret)?; + } + ParMapStageKind::FilterStrContains => { + if stage.func.is_some() + || stage.elem_in != Ty::Str + || stage.elem_out != Ty::Str + || stage.capture_tys.as_slice() != [Ty::Str] + { + return Err(callable_metadata_error()); + } + } + ParMapStageKind::Project { field } | ParMapStageKind::FilterField { field } => { + if stage.func.is_some() || !stage.captures.is_empty() { + return Err(callable_metadata_error()); + } + let Ty::Struct(id) = stage.elem_in else { + return Err(callable_metadata_error()); + }; + let Some(field_ty) = program + .structs + .get(id as usize) + .and_then(|definition| definition.fields.get(field as usize)) + .map(|field| field.ty) + else { + return Err(callable_metadata_error()); + }; + let valid = match stage.kind { + ParMapStageKind::Project { .. } => field_ty == stage.elem_out, + ParMapStageKind::FilterField { .. } => { + field_ty == Ty::Bool && stage.elem_out == stage.elem_in + } + _ => unreachable!(), + }; + if !valid { + return Err(callable_metadata_error()); + } + } + } + stage_input = match stage.kind { + ParMapStageKind::Map | ParMapStageKind::Project { .. } => stage.elem_out, + ParMapStageKind::Filter + | ParMapStageKind::FilterStrContains + | ParMapStageKind::FilterField { .. } => stage.elem_in, + }; + } + + let mut terminal_params = Vec::with_capacity(terminal_capture_tys.len() + 1); + terminal_params.push(stage_input); + terminal_params.extend(terminal_capture_tys.iter().copied()); + validate_parallel_callable(declarations, terminal, &terminal_params, elem_out) +} + +fn parallel_generated_ids( + program: &Program, + declarations: &HashMap, + function: &Function, + src: &Operand, + terminal: &ProgramCall, + stages: &[ParMapStage], + terminal_capture_tys: &[Ty], + elem_in: Ty, + elem_out: Ty, + work_weight: u8, + reduce: bool, +) -> Result, CodegenError> { + let Some(terminal_declaration) = declarations.get(terminal) else { + return Err(callable_target_error(terminal)); + }; + if terminal_declaration + .classes + .contains(&ProgramDeclarationClass::Extern) + { + return Err(callable_target_error(terminal)); + } + let source = preflight_operand_ty(function, src) + .ok_or_else(|| callable_target_error(terminal))?; + let mut stage_ids = Vec::with_capacity(stages.len()); + for stage in stages { + let input = canonical_ty(stage.elem_in, program)?; + let output = canonical_ty(stage.elem_out, program)?; + let captures = stage + .capture_tys + .iter() + .map(|ty| canonical_ty(*ty, program)) + .collect::, _>>()?; + stage_ids.push(match stage.kind { + ParMapStageKind::Map | ParMapStageKind::Filter => { + let target = stage + .func + .as_ref() + .ok_or_else(|| callable_target_error(terminal))?; + let declaration = declarations + .get(target) + .ok_or_else(|| callable_target_error(target))?; + if declaration + .classes + .contains(&ProgramDeclarationClass::Extern) + { + return Err(callable_target_error(target)); + } + let abi = canonical_signature(&declaration.signature, program)?; + if stage.kind == ParMapStageKind::Map { + ParallelStageId::Map { + target: target.clone(), + abi, + input, + output, + captures, + } + } else { + ParallelStageId::Filter { + target: target.clone(), + abi, + input, + output, + captures, + } + } + } + ParMapStageKind::FilterStrContains => ParallelStageId::FilterStrContains { + input, + output, + needle: captures + .into_iter() + .next() + .ok_or_else(|| callable_target_error(terminal))?, + }, + ParMapStageKind::Project { field } => ParallelStageId::Project { + input, + output, + field, + }, + ParMapStageKind::FilterField { field } => ParallelStageId::FilterField { + input, + output, + field, + }, + }); + } + let terminal_input = stages.last().map_or(elem_in, |stage| stage.elem_out); + let terminal_captures = terminal_capture_tys + .iter() + .map(|ty| canonical_ty(*ty, program)) + .collect::, _>>()?; + let base = ParallelGeneratedId { + mode: if reduce { + ParallelKernelMode::Reduce + } else { + ParallelKernelMode::Materialize + }, + source: canonical_ty(source, program)?, + terminal_input: canonical_ty(terminal_input, program)?, + terminal_output: canonical_ty(elem_out, program)?, + terminal: terminal.clone(), + terminal_abi: canonical_signature(&terminal_declaration.signature, program)?, + terminal_captures, + stages: stage_ids, + work_weight, + }; + if reduce { + return Ok(vec![GeneratedId::Parallel(base)]); + } + let has_filter = stages.iter().any(|stage| { + matches!( + stage.kind, + ParMapStageKind::Filter + | ParMapStageKind::FilterStrContains + | ParMapStageKind::FilterField { .. } + ) + }); + if !has_filter { + return Ok(vec![GeneratedId::Parallel(base)]); + } + let mut count = base.clone(); + count.mode = ParallelKernelMode::FilterCount; + let mut scatter = base; + scatter.mode = ParallelKernelMode::FilterScatter; + Ok(vec![GeneratedId::Parallel(count), GeneratedId::Parallel(scatter)]) +} + +fn preflight_operand_ty(function: &Function, operand: &Operand) -> Option { + match operand { + Operand::Const(Const::Int(_, ty)) | Operand::Const(Const::Float(_, ty)) => Some(*ty), + Operand::Const(Const::Char(_)) => Some(Ty::Char), + Operand::Const(Const::Bool(_)) => Some(Ty::Bool), + Operand::Const(Const::Unit) => Some(Ty::Unit), + Operand::Value(value) => function.value_tys.get(*value as usize).copied(), + Operand::Arg(index) => function + .params + .get(*index as usize) + .and_then(|slot| function.slots.get(*slot as usize)) + .copied(), + } +} + +fn direct_runtime_key_is_valid(key: RuntimeKey, args: &[Ty], ret: Ty, program: &Program) -> bool { + let i64_ty = Ty::Int(IntTy { + bits: 64, + signed: true, + }); + match key { + RuntimeKey::Print => args.len() == 1 && matches!(args[0], Ty::Int(_)) && ret == Ty::Unit, + RuntimeKey::PrintStr => args == [Ty::Str] && ret == Ty::Unit, + RuntimeKey::PrintBool => args == [Ty::Bool] && ret == Ty::Unit, + RuntimeKey::PrintChar => args == [Ty::Char] && ret == Ty::Unit, + RuntimeKey::PrintF32 => { + args == [Ty::Float(FloatTy { bits: 32 })] && ret == Ty::Unit + } + RuntimeKey::PrintF64 => { + args == [Ty::Float(FloatTy { bits: 64 })] && ret == Ty::Unit + } + RuntimeKey::Hash64 => { + args.len() == 1 + && matches!(args[0], Ty::Str | Ty::Slice(_)) + && ret + == Ty::Int(IntTy { + bits: 64, + signed: false, + }) + } + RuntimeKey::Hash128 => args.len() == 1 + && matches!(args[0], Ty::Str | Ty::Slice(_)) + && matches!(ret, Ty::Tuple(id) + if program.tuples.get(id as usize).is_some_and(|tuple| { + let u64_scalar = Scalar::Int(IntTy { bits: 64, signed: false }); + tuple.elems.as_slice() == [u64_scalar, u64_scalar] + })), + RuntimeKey::ProcessExit => args == [i64_ty] && ret == Ty::Unit, + RuntimeKey::ProcessAbort | RuntimeKey::DivFail => args.is_empty() && ret == Ty::Unit, + RuntimeKey::BoundsFail | RuntimeKey::Utf8BoundaryFail | RuntimeKey::LenMismatchFail => { + args == [i64_ty, i64_ty] && ret == Ty::Unit + } + RuntimeKey::RangeFail => args == [i64_ty, i64_ty, i64_ty] && ret == Ty::Unit, + _ => false, + } +} + +fn generated_stem(id: &GeneratedId) -> Result { + Ok(match id { + GeneratedId::FnValue { target, .. } => { + format!("align_gen$fnval${}", callable_hex(target)) + } + GeneratedId::Closure { lifted, .. } => { + format!("align_gen$clos${}", callable_hex(lifted)) + } + GeneratedId::Task { fallible, result } => { + let fallible_byte = u8::from(*fallible); + let result_hex = lowercase_hex(result.as_bytes()); + format!("align_gen$tramp${fallible_byte}${result_hex}") + } + GeneratedId::Parallel(parallel) => { + let bytes = canonical_metadata(id.to_canonical_bytes())?; + let mode = parallel.mode as u8; + let bytes_hex = lowercase_hex(&bytes); + format!("align_gen$par${mode}${bytes_hex}") + } + }) +} + +fn reserve_external_identity( + reserved: &mut HashMap, + identity: String, + claimant: String, +) -> Result<(), CodegenError> { + if let Some(existing) = reserved.get(&identity) { + if existing == &claimant { + return Ok(()); + } + return Err(CodegenError::Lowering(format!( + "callable external identity collision:{}", + lowercase_hex(identity.as_bytes()) + ))); + } + reserved.insert(identity, claimant); + Ok(()) +} + +fn validate_generated_pairs(generated: &[GeneratedId]) -> Result<(), CodegenError> { + let set = generated.iter().cloned().collect::>(); + for id in generated { + let GeneratedId::Parallel(parallel) = id else { + continue; + }; + let partner_mode = match parallel.mode { + ParallelKernelMode::FilterCount => ParallelKernelMode::FilterScatter, + ParallelKernelMode::FilterScatter => ParallelKernelMode::FilterCount, + ParallelKernelMode::Materialize | ParallelKernelMode::Reduce => continue, + }; + let mut partner = parallel.clone(); + partner.mode = partner_mode; + if !set.contains(&GeneratedId::Parallel(partner)) { + let bytes = canonical_metadata(id.to_canonical_bytes())?; + return Err(CodegenError::Lowering(format!( + "generated count-scatter pair mismatch:{}", + lowercase_hex(&bytes) + ))); + } + } + Ok(()) +} + +fn probe_generated_name( + stem: &str, + reserved: &mut HashMap, + mut counter: u64, +) -> Result { + loop { + let candidate = format!("{stem}${counter}"); + if !reserved.contains_key(&candidate) { + reserved.insert(candidate.clone(), "generated".to_owned()); + return Ok(candidate); + } + let Some(next) = counter.checked_add(1) else { + return Err(CodegenError::Lowering(format!( + "generated name exhausted:{}", + lowercase_hex(candidate.as_bytes()) + ))); + }; + counter = next; + } +} + +fn callable_declarations( + program: &Program, +) -> Result, CodegenError> { + let mut declarations = HashMap::new(); + for function in &program.fns { + register_program_declaration( + &mut declarations, + &function.name, + ProgramDeclarationClass::Stored, + program_signature(function)?, + )?; + } + for function in &program.imported_fns { + register_program_declaration( + &mut declarations, + &function.name, + ProgramDeclarationClass::Imported, + ProgramSignature { + params: function.params.clone(), + modes: function.param_modes.clone(), + ret: function.ret, + borrow: function.return_borrow.clone(), + region: function.return_region.clone(), + }, + )?; + } + for function in &program.externs { + register_program_declaration( + &mut declarations, + &function.name, + ProgramDeclarationClass::Extern, + ProgramSignature { + params: function.params.clone(), + modes: function.param_modes.clone(), + ret: function.ret, + borrow: function.return_borrow.clone(), + region: function.return_region.clone(), + }, + )?; + } + Ok(declarations) +} + +fn callable_preflight( + program: &Program, + exports: &[String], + declarations: HashMap, +) -> Result { + let mut generated = Vec::new(); + for function in &program.fns { + for block in &function.blocks { + for statement in &block.stmts { + let Stmt::Let(value, rvalue) = statement else { + continue; + }; + match rvalue { + Rvalue::Call(DirectCall::Runtime(key), args) => { + let argument_types = args + .iter() + .map(|operand| preflight_operand_ty(function, operand)) + .collect::>>(); + let result = function.value_tys.get(*value as usize).copied(); + if !argument_types + .as_deref() + .zip(result) + .is_some_and(|(args, ret)| { + direct_runtime_key_is_valid(*key, args, ret, program) + }) + { + return Err(CodegenError::Lowering( + "callable metadata invalid:InvalidGraph".to_owned(), + )); + } + } + Rvalue::Call(DirectCall::Program(target), args) => { + let Some(declaration) = declarations.get(target) else { + return Err(callable_target_error(target)); + }; + let argument_types = args + .iter() + .map(|operand| preflight_operand_ty(function, operand)) + .collect::>>(); + let result = function.value_tys.get(*value as usize).copied(); + if argument_types.as_deref() != Some(declaration.signature.params.as_slice()) + || result != Some(declaration.signature.ret) + { + return Err(callable_target_error(target)); + } + } + Rvalue::FnAddr { target, signature } => { + let Some(declaration) = declarations.get(target) else { + return Err(callable_target_error(target)); + }; + if declaration.classes.contains(&ProgramDeclarationClass::Extern) + || signature.param_modes != declaration.signature.modes + || signature.return_borrow != declaration.signature.borrow + || signature.return_region != declaration.signature.region + { + return Err(callable_target_error(target)); + } + generated.push(GeneratedId::FnValue { + target: target.clone(), + signature: canonical_signature(&declaration.signature, program)?, + }); + } + Rvalue::Closure { + lifted, + captures, + capture_tys, + signature, + .. + } => { + let Some(declaration) = declarations.get(lifted) else { + return Err(callable_target_error(lifted)); + }; + if declaration.classes.as_slice() != [ProgramDeclarationClass::Stored] + || capture_tys.len() > declaration.signature.params.len() + || captures.len() != capture_tys.len() + { + return Err(callable_target_error(lifted)); + } + let explicit = declaration + .signature + .params + .len() + .checked_sub(capture_tys.len()) + .ok_or_else(|| callable_target_error(lifted))?; + let explicit_params = declaration + .signature + .params + .get(..explicit) + .ok_or_else(|| callable_target_error(lifted))?; + let captured_params = declaration + .signature + .params + .get(explicit..) + .ok_or_else(|| callable_target_error(lifted))?; + let explicit_modes = declaration + .signature + .modes + .get(..explicit) + .ok_or_else(|| callable_target_error(lifted))?; + let captured_modes = declaration + .signature + .modes + .get(explicit..) + .ok_or_else(|| callable_target_error(lifted))?; + if captured_params != capture_tys + || captured_modes + .iter() + .any(|mode| *mode != align_ast::ParamMode::ByValue) + || signature.param_modes != explicit_modes + || signature.return_borrow != declaration.signature.borrow + || signature.return_region != declaration.signature.region + || captures + .iter() + .zip(capture_tys) + .any(|(operand, ty)| preflight_operand_ty(function, operand) != Some(*ty)) + { + return Err(callable_target_error(lifted)); + } + let explicit_signature = ProgramSignature { + params: explicit_params.to_vec(), + modes: explicit_modes.to_vec(), + ret: declaration.signature.ret, + borrow: declaration.signature.borrow.clone(), + region: declaration.signature.region.clone(), + }; + generated.push(GeneratedId::Closure { + lifted: lifted.clone(), + explicit_signature: canonical_signature(&explicit_signature, program)?, + captures: capture_tys + .iter() + .map(|ty| canonical_metadata(CanonicalTy::from_program(*ty, program))) + .collect::, _>>()?, + }); + } + Rvalue::SpawnTask { r, fallible, .. } => { + generated.push(GeneratedId::Task { + fallible: *fallible, + result: canonical_metadata(CanonicalTy::from_program(*r, program))?, + }); + } + Rvalue::ParMapParallel { + src, + func, + stages, + captures, + capture_tys, + elem_in, + elem_out, + work_weight, + } => { + let result = function + .value_tys + .get(*value as usize) + .copied() + .ok_or_else(callable_metadata_error)?; + validate_parallel_request( + program, + &declarations, + function, + result, + src, + func, + stages, + captures, + capture_tys, + *elem_in, + *elem_out, + *work_weight, + false, + )?; + generated.extend(parallel_generated_ids( + program, + &declarations, + function, + src, + func, + stages, + capture_tys, + *elem_in, + *elem_out, + *work_weight, + false, + )?); + } + Rvalue::ParMapReduce { + src, + func, + captures, + capture_tys, + elem_in, + elem_out, + work_weight, + } => { + let result = function + .value_tys + .get(*value as usize) + .copied() + .ok_or_else(callable_metadata_error)?; + validate_parallel_request( + program, + &declarations, + function, + result, + src, + func, + &[], + captures, + capture_tys, + *elem_in, + *elem_out, + *work_weight, + true, + )?; + generated.extend(parallel_generated_ids( + program, + &declarations, + function, + src, + func, + &[], + capture_tys, + *elem_in, + *elem_out, + *work_weight, + true, + )?); + } + _ => {} + } + } + } + } + + let mut reserved = HashMap::new(); + for abi in runtime_abi::runtime_abis() { + reserve_external_identity( + &mut reserved, + abi.symbol.to_owned(), + format!("runtime:{:?}", abi.key), + )?; } + for function in &program.fns { + let identity = symbol_name(function, exports); + let claimant = if identity == "main" { + "entry:main".to_owned() + } else { + format!("program:{}", callable_hex(&function.name)) + }; + reserve_external_identity( + &mut reserved, + identity, + claimant, + )?; + } + for function in &program.imported_fns { + reserve_external_identity( + &mut reserved, + encoded_program_symbol(&function.name), + format!("program:{}", callable_hex(&function.name)), + )?; + } + for function in &program.externs { + if runtime_abi::runtime_abi_for_symbol(function.name.as_str()).is_some() { + continue; + } + reserve_external_identity( + &mut reserved, + function.name.as_str().to_owned(), + format!("extern:{}", callable_hex(&function.name)), + )?; + } + if program.fns.iter().any(|function| function.name.as_str() == "main") { + reserve_external_identity(&mut reserved, "main".to_owned(), "entry:main".to_owned())?; + } + + let mut encountered = HashSet::new(); + generated.retain(|id| encountered.insert(id.clone())); + validate_generated_pairs(&generated)?; + let mut unique = HashMap::>::new(); + for id in generated { + let bytes = canonical_metadata(id.to_canonical_bytes())?; + unique.entry(id).or_insert(bytes); + } + let mut ordered = unique.into_iter().collect::>(); + ordered.sort_by(|left, right| left.1.cmp(&right.1)); + let mut generated_names = HashMap::with_capacity(ordered.len()); + for (id, _) in ordered { + let stem = generated_stem(&id)?; + let candidate = probe_generated_name(&stem, &mut reserved, 0)?; + generated_names.insert(id, candidate); + } + Ok(CallablePreflight { + declarations, + generated_names, + }) } -/// Emit the C `main` for a `Result<(), Error>`- or `Unit`-returning Align `main` (renamed -/// `align_main`, see [`symbol_name`]): call it, then materialize a **defined** `i32` exit code — +/// Emit the C `main` for a `Result<(), Error>`- or `Unit`-returning encoded Align main body: call +/// it, then materialize a **defined** `i32` exit code — /// on `Err(code)` report the error and exit with `code`; on `Ok` or a plain `Unit` return, exit 0. -/// A `Unit` `align_main` is `void`, so there is no tag/payload to inspect; the wrapper's only job +/// A `Unit` Align body is `void`, so there is no tag/payload to inspect; the wrapper's only job /// for that case is to turn the void call into `ret i32 0` (never leave the ABI return register /// undefined — the bug this function exists to close for the `Unit` case, `has_args` always /// `false` there since sema restricts the `args: array` form to a `Result`-returning `main`). fn emit_main_wrapper<'c>( ctx: &'c Context, module: &Module<'c>, - align_main: FunctionValue<'c>, + align_body: FunctionValue<'c>, ret: Ty, has_args: bool, extern_fn_types: &HashMap>, @@ -2534,9 +3569,9 @@ fn emit_main_wrapper<'c>( vec![] }; - let call = builder.build_call(align_main, &call_args, "r").map_err(lower)?; + let call = builder.build_call(align_body, &call_args, "r").map_err(lower)?; if ret == Ty::Unit { - // `align_main` is `void(...)` — nothing to inspect, just materialize a defined `0`. + // The encoded Align body is `void(...)` — nothing to inspect, just materialize a defined `0`. builder.build_return(Some(&i32t.const_int(0, false))).map_err(lower)?; return Ok(()); } @@ -2968,24 +4003,6 @@ fn check_sysv_struct_args_fit( Ok(()) } -/// Size/alignment (bytes) of a scalar's in-memory representation. -/// A symbol-safe key for a spawn trampoline, by result type `R` and fallibility (`tramp$`). -fn spawn_tramp_key(ty: Ty, fallible: bool) -> String { - format!("{}{}", task_tramp_key(ty), if fallible { "$f" } else { "" }) -} - -/// A symbol-safe key for a spawn result type `R`, naming its trampoline (`tramp$`). -fn task_tramp_key(ty: Ty) -> String { - match ty { - Ty::Int(it) => format!("{}{}", if it.signed { 'i' } else { 'u' }, it.bits), - Ty::Float(ft) => format!("f{}", ft.bits), - Ty::Bool => "bool".to_string(), - Ty::Char => "char".to_string(), - Ty::Unit => "unit".to_string(), - _ => "x".to_string(), - } -} - /// The natural ABI alignment (in bytes) of a struct field, used only to order fields for padding /// elimination. Sema owns the single iterative layout traversal so deep nominal/tagged graphs cannot /// overflow either compiler layer and field ordering cannot drift from the huge-copy lint's layout. @@ -3324,32 +4341,27 @@ fn declare_fn<'c>( mark_nounwind(ctx, fv); // Every Align program function is module-private (internal) EXCEPT: // - the C entry: an `-> i32` `main` keeps the symbol name `main` and IS the C entry (`crt0` - // resolves it by name), so it must stay external — its LLVM return type is already the C - // ABI's `i32`, so no wrapper is needed. A `Result`- or `Unit`-returning `main` is emitted as - // `align_main` here (internal) instead, and its external C `main` wrapper — which always - // returns a defined `i32` (`0`, an `Err` exit code, or `0` for a plain `Unit` return; see - // `emit_main_wrapper`) — is generated separately. No other function is named `main`. + // resolves it by name), so it must stay external. A `Result`- or `Unit`-returning main body + // keeps its encoded Align identity and stays internal; its external C `main` wrapper always + // returns a defined `i32` and is generated separately. // - an explicit export root (`emit-obj`/`emit-llvm --export `, M13 Codex-audit item // 1). Exporting a function makes it BOTH a linker-visible external symbol AND a DCE root in // the same step (`external` linkage keeps LLVM's `globaldce`/`internalize`-style passes from // ever considering it dead), so linkage and "what stays reachable" always agree. // - // BOTH checks are keyed on `symbol` (the LLVM name), never `f.name` (the source name): for a - // `Result`-returning `main`, `f.name == "main"` but `symbol == "align_main"` — if the export - // check compared `f.name`, `--export main` would match it and skip internalizing `align_main`, - // leaving it wrongly external (a real, one-line-fix regression caught in review). Keying on - // `symbol` makes `--export main` compare against `"align_main"`, which never matches, so - // `align_main` still internalizes and `--export main` stays the harmless no-op the CLI promises - // (the C `main` wrapper was already external via the first check, unconditionally). Every - // *other* function has `symbol == f.name` (only `main` is ever renamed), so this is - // observationally identical to keying on `f.name` for the entire non-`main` case — the export - // roots the driver validates (`align_driver::unknown_exports`, matched against `Function::name`) - // still name exactly what the caller wrote. + // Export matching stays on the logical `f.name`; `main` is excluded explicitly so `--export + // main` remains a harmless no-op and cannot expose the encoded wrapped body. `symbol` already + // contains the exact encoded or explicitly exported external identity selected by preflight. // - a per-unit `pub` export (M15 S2): a non-entry `pub` user function keeps `external` linkage // so a dependent unit's object can resolve the cross-unit call. `f.exportable` is set only by // per-unit lowering; the whole-program path leaves it `false`, so the default object is // byte-identical (every function but `main`/`--export` still internalizes). - if symbol != "main" && !exports.iter().any(|e| e == symbol) && !f.exportable { + let direct_main = f.name.as_str() == "main" + && !matches!(f.ret, Ty::Result(..)) + && f.ret != Ty::Unit; + let explicit_export = f.name.as_str() != "main" + && exports.iter().any(|export| export == f.name.as_str()); + if !direct_main && !explicit_export && !f.exportable { mark_internal(fv); } fv @@ -3381,7 +4393,7 @@ fn declare_imported_fn<'c>( } else { map(imp.ret).fn_type(¶m_types, false) }; - let fv = module.add_function(&imp.name, fn_ty, None); + let fv = module.add_function(&encoded_program_symbol(&imp.name), fn_ty, None); mark_nounwind(ctx, fv); fv } @@ -3686,9 +4698,8 @@ fn link_in_rt_lto<'c>( } // Retarget each incoming definition to the physical declaration selected before lowering. - // A same-spelled program/import claimant precedes native declarations until c3, so LLVM may - // have named the typed native handle `align_rt_*.N`. Linking the bitcode's unsuffixed definition - // unchanged would collide with the program claimant instead of filling that typed handle. + // Retarget explicitly from the captured typed declaration. This remains correct if a future + // declaration source causes LLVM to uniquify the physical runtime symbol. for abi in runtime_abi::keyed_runtime_abis().filter(|abi| abi.is_rt_lto_guarded()) { let Some(incoming) = rt.get_function(abi.symbol) else { continue; @@ -3748,17 +4759,20 @@ struct FnGen<'c, 'a> { ctx: &'c Context, module: &'a Module<'c>, builder: &'a Builder<'c>, - funcs: &'a HashMap>, - /// Typed handles for every fixed keyed native declaration. Dedicated native lowering must use - /// this table; `funcs` remains only for the explicitly deferred mixed program/direct seams. + program_funcs: &'a HashMap>, + generated_funcs: &'a HashMap>, + callable_preflight: &'a CallablePreflight, + program: &'a Program, + /// Typed handles for every fixed keyed native declaration. Runtime calls never share the + /// program-call namespace, even when their logical spellings happen to match. runtime_funcs: &'a HashMap>, /// Semantic signatures for every direct callable. LLVM's physical integer types do not retain /// signedness, so the range-kernel boundary checks these `Ty`s as well as the generated LLVM /// function type before emitting a direct call. - fn_sigs: &'a HashMap, + fn_sigs: &'a HashMap, /// The SysV ABI plan for each `extern "C"` symbol — to coerce call arguments (view→data /// pointer, `layout(C)` struct→register slots) and reconstruct a by-value struct return. - extern_abi: &'a HashMap, + extern_abi: &'a HashMap, structs: &'a [StructDef], struct_types: &'a [StructType<'c>], /// Logical→physical field-index map per struct id (`field_perm[sid][logical] = physical`). @@ -4140,160 +5154,6 @@ impl<'c, 'a> FnGen<'c, 'a> { Ok((physical, field_ty)) } - /// Keep generated staged-kernel names distinct when the same callable chain projects fields - /// from different struct types. The visible chain remains readable; the layout suffix prevents - /// LLVM from reusing a kernel whose input aggregate type or field permutation belongs to another - /// AoS type. - fn par_map_field_layout_suffix(stages: &[ParMapStage]) -> String { - let ids: Vec = stages - .iter() - .filter_map(|stage| match stage.kind { - ParMapStageKind::Project { .. } | ParMapStageKind::FilterField { .. } => Some(match stage.elem_in { - Ty::Struct(id) => id.to_string(), - _ => "invalid".to_string(), - }), - ParMapStageKind::Map | ParMapStageKind::Filter | ParMapStageKind::FilterStrContains => None, - }) - .collect(); - if ids.is_empty() { String::new() } else { format!("$struct{}", ids.join("_")) } - } - - /// Encode the staged portion of a kernel's structural identity with only identifier-safe bytes. - /// The readable chain below intentionally keeps source names, but `$` is a legal part of lifted - /// names, so joining names with `$` alone is not injective (`a$b` versus `a`, `b`). Include the - /// stage kind, length-prefixed callable bytes, element types, field number, and stage capture - /// types so the complete kernel key cannot reuse a kernel built for a different chain. - fn par_map_stage_structural_key(stages: &[ParMapStage]) -> String { - stages - .iter() - .map(|stage| { - let (kind, field) = match stage.kind { - ParMapStageKind::Map => ("m", "-".to_string()), - ParMapStageKind::Filter => ("f", "-".to_string()), - ParMapStageKind::FilterStrContains => ("s", "-".to_string()), - ParMapStageKind::Project { field } => ("p", field.to_string()), - ParMapStageKind::FilterField { field } => ("w", field.to_string()), - }; - let func = stage.func.as_deref().unwrap_or(""); - let captures = stage - .capture_tys - .iter() - .map(|ty| Self::par_map_type_key(*ty)) - .collect::>() - .join(","); - format!( - "{kind}{field}{}{}:{}:{}:{}", - func.len(), - Self::par_map_hex(func), - Self::par_map_type_key(stage.elem_in), - Self::par_map_type_key(stage.elem_out), - captures - ) - }) - .collect::>() - .join(";") - } - - /// Add the parts that are outside the staged chain to a range-kernel identity. Empty - /// materializers and reductions have no stage records at all, and the terminal callable's - /// captures are not represented by a stage, so neither may use a name keyed only by `func`. - /// Include the declared MIR element types, the LLVM element types, mode, terminal captures, and - /// the exact LLVM signatures of every callable. Every free-form component is hex encoded before - /// it enters the generated name, keeping the identifier safe and injective. - #[allow(clippy::too_many_arguments)] // Each argument is an independent cache/ABI identity dimension. - fn par_map_kernel_structural_key( - &self, - func: &str, - in_ty: BasicTypeEnum<'c>, - out_ty: BasicTypeEnum<'c>, - elem_in: Ty, - elem_out: Ty, - capture_tys: &[Ty], - terminal_capture_start: usize, - mode: ParMapKernelMode, - stages: &[ParMapStage], - ) -> Result { - let mode_key = match mode { - ParMapKernelMode::Materialize => "materialize", - ParMapKernelMode::Reduce => "reduce", - ParMapKernelMode::FilterCount => "filter-count", - ParMapKernelMode::FilterScatter => "filter-scatter", - }; - let callable_signature = |name: &str| { - self.funcs - .get(name) - .map(|f| f.get_type().print_to_string().to_string()) - .ok_or_else(|| self.err(format!("par_map function `{name}` is missing from codegen"))) - }; - let stage_signatures = stages - .iter() - .filter_map(|stage| stage.func.as_deref()) - .map(callable_signature) - .collect::, _>>()?; - let terminal_signature = callable_signature(func)?; - let terminal_captures = capture_tys - .get(terminal_capture_start..) - .ok_or_else(|| self.err("par_map terminal capture layout is shorter than its stages"))? - .to_vec(); - Ok(Self::par_map_kernel_identity_key( - func, - &in_ty.print_to_string().to_string(), - &out_ty.print_to_string().to_string(), - elem_in, - elem_out, - &terminal_captures, - mode_key, - stages, - &terminal_signature, - &stage_signatures, - )) - } - - #[allow(clippy::too_many_arguments)] // Keep the testable key function aligned with its ABI dimensions. - fn par_map_kernel_identity_key( - func: &str, - in_llvm: &str, - out_llvm: &str, - elem_in: Ty, - elem_out: Ty, - terminal_captures: &[Ty], - mode: &str, - stages: &[ParMapStage], - terminal_signature: &str, - stage_signatures: &[String], - ) -> String { - let terminal_captures = terminal_captures - .iter() - .map(|ty| Self::par_map_type_key(*ty)) - .collect::>() - .join(","); - format!( - "mode={};elem_in={};elem_out={};in_llvm={};out_llvm={};terminal={}:{};terminal_captures={};stages={};stage_signatures={}", - mode, - Self::par_map_type_key(elem_in), - Self::par_map_type_key(elem_out), - Self::par_map_hex(in_llvm), - Self::par_map_hex(out_llvm), - Self::par_map_hex(func), - Self::par_map_hex(terminal_signature), - terminal_captures, - Self::par_map_stage_structural_key(stages), - stage_signatures - .iter() - .map(|signature| Self::par_map_hex(signature)) - .collect::>() - .join(","), - ) - } - - fn par_map_type_key(ty: Ty) -> String { - Self::par_map_hex(&format!("{ty:?}")) - } - - fn par_map_hex(value: &str) -> String { - value.as_bytes().iter().map(|byte| format!("{byte:02x}")).collect() - } - /// Check every type-table id that the range-kernel path may hand to `llvm_type`. Normal MIR /// has already passed sema, but codegen also accepts compiler-generated MIR in tests and in /// future tooling; a bad id must be a lowering error rather than an index panic. @@ -4401,7 +5261,7 @@ impl<'c, 'a> FnGen<'c, 'a> { /// contract before a cached/generated kernel is reused. fn validate_par_map_callable( &self, - name: &str, + name: &ProgramCall, params: &[Ty], ret: Ty, role: &str, @@ -4417,7 +5277,7 @@ impl<'c, 'a> FnGen<'c, 'a> { ))); } let target = self - .funcs + .program_funcs .get(name) .copied() .ok_or_else(|| self.err(format!("{role} function `{name}` is missing from codegen")))?; @@ -4693,7 +5553,8 @@ impl<'c, 'a> FnGen<'c, 'a> { #[allow(clippy::too_many_arguments)] // The kernel ABI and staged pipeline are separate dimensions. fn par_map_range_kernel( &self, - func: &str, + generated_id: &GeneratedId, + func: &ProgramCall, in_ty: BasicTypeEnum<'c>, out_ty: BasicTypeEnum<'c>, elem_in: Ty, @@ -4750,7 +5611,11 @@ impl<'c, 'a> FnGen<'c, 'a> { if stage.captures.len() != stage.capture_tys.len() { return Err(self.err(format!( "par_map stage `{}` capture operand/type count mismatch: {} operands, {} types", - stage.func.as_deref().unwrap_or(""), + stage + .func + .as_ref() + .map(ProgramCall::as_str) + .unwrap_or(""), stage.captures.len(), stage.capture_tys.len() ))); @@ -4768,7 +5633,7 @@ impl<'c, 'a> FnGen<'c, 'a> { } } ParMapStageKind::Map | ParMapStageKind::Filter => { - let Some(stage_func) = stage.func.as_deref() else { + let Some(stage_func) = stage.func.as_ref() else { return Err(self.err("par_map callable stage is missing its function")); }; let expected_ret = match stage.kind { @@ -4809,57 +5674,14 @@ impl<'c, 'a> FnGen<'c, 'a> { for ty in capture_tys { self.validate_par_map_capture_type(*ty)?; } - let structural_key = self.par_map_kernel_structural_key( - func, - in_ty, - out_ty, - elem_in, - elem_out, - capture_tys, - terminal_capture_start, - mode, - stages, - )?; - let field_layout_suffix = Self::par_map_field_layout_suffix(stages); - let name = match mode { - ParMapKernelMode::Reduce => format!("{func}$parreducekernel$key${structural_key}"), - ParMapKernelMode::Materialize => { - let chain = stages - .iter() - .map(|stage| match stage.kind { - ParMapStageKind::Map => stage.func.clone().unwrap_or_else(|| "map".to_string()), - ParMapStageKind::Filter => stage.func.clone().unwrap_or_else(|| "where".to_string()), - ParMapStageKind::FilterStrContains => "wherecontains".to_string(), - ParMapStageKind::Project { field } => format!("field${field}"), - ParMapStageKind::FilterField { field } => format!("wherefield${field}"), - }) - .collect::>() - .join("$"); - if stages.is_empty() { - format!("{func}$parkernel$key${structural_key}") - } else { - format!("{func}$parmapchain${chain}{field_layout_suffix}$key${structural_key}") - } - } - ParMapKernelMode::FilterCount | ParMapKernelMode::FilterScatter => { - let suffix = if filter_count { "count" } else { "scatter" }; - let chain = stages - .iter() - .map(|stage| match stage.kind { - ParMapStageKind::Map => stage.func.clone().unwrap_or_else(|| "map".to_string()), - ParMapStageKind::Filter => format!("where${}", stage.func.as_deref().unwrap_or("filter")), - ParMapStageKind::FilterStrContains => "wherecontains".to_string(), - ParMapStageKind::Project { field } => format!("field${field}"), - ParMapStageKind::FilterField { field } => format!("wherefield${field}"), - }) - .collect::>() - .join("$"); - format!("{func}$parfilter${suffix}${chain}{field_layout_suffix}$key${structural_key}") - } - }; + let name = self + .callable_preflight + .generated_names + .get(generated_id) + .ok_or_else(|| self.err("parallel generated identity was not collected"))?; // Validate the complete staged shape before consulting the module cache. A malformed MIR // node must not silently reuse an earlier kernel merely because its readable name collides. - if let Some(f) = self.module.get_function(&name) { + if let Some(f) = self.module.get_function(name) { return Ok(f.as_global_value().as_pointer_value()); } let ptr_t = self.ctx.ptr_type(AddressSpace::default()); @@ -4867,7 +5689,7 @@ impl<'c, 'a> FnGen<'c, 'a> { let capture_fields: Vec> = capture_tys.iter().map(|ty| self.llvm_type(*ty)).collect(); let capture_struct = self.ctx.struct_type(&capture_fields, false); let kernel = self.module.add_function( - &name, + name, self.ctx .void_type() .fn_type(&[ptr_t.into(), ptr_t.into(), ptr_t.into(), i64t.into(), i64t.into()], false), @@ -4993,11 +5815,11 @@ impl<'c, 'a> FnGen<'c, 'a> { .ok_or_else(|| self.err("par_map staged capture count overflows"))?; match stage.kind { ParMapStageKind::Map | ParMapStageKind::Filter => { - let Some(stage_func) = stage.func.as_deref() else { + let Some(stage_func) = stage.func.as_ref() else { return Err(self.err("par_map callable stage is missing its function")); }; let target = self - .funcs + .program_funcs .get(stage_func) .copied() .ok_or_else(|| self.err(format!("par_map stage function `{stage_func}` is missing from codegen")))?; @@ -6299,8 +7121,16 @@ impl<'c, 'a> FnGen<'c, 'a> { // A fallible task also gets an `err_slot` (sized for the `Error` enum) the trampoline // writes its `Err` value into; a non-fallible task passes null. let err_slot = if *fallible { - let eid = self.enums.iter().position(|e| e.name == "Error").expect("Error enum registered"); - let ety = self.enum_types[eid]; + let eid = self + .enums + .iter() + .position(|e| e.name == "Error") + .ok_or_else(|| self.err("Error enum not registered"))?; + let ety = self + .enum_types + .get(eid) + .copied() + .ok_or_else(|| self.err("Error enum type not registered"))?; let ebytes = self.target_data.get_store_size(&ety); let ealign = self.target_data.get_abi_alignment(&ety) as u64; self.builder @@ -6311,7 +7141,16 @@ impl<'c, 'a> FnGen<'c, 'a> { self.ctx.ptr_type(AddressSpace::default()).const_null() }; // The per-(R, fallibility) trampoline runs the closure and writes the slot at `wait`. - let tramp = self.funcs[&format!("tramp${}", spawn_tramp_key(*r, *fallible))].as_global_value().as_pointer_value(); + let tramp_id = GeneratedId::Task { + fallible: *fallible, + result: canonical_ty(*r, self.program)?, + }; + let tramp = self + .generated_funcs + .get(&tramp_id) + .ok_or_else(|| self.err("task trampoline identity was not collected"))? + .as_global_value() + .as_pointer_value(); self.builder .build_call(self.runtime(RuntimeKey::TgRegister), &[tgv.into(), tramp.into(), thunk.into(), env.into(), slot.into(), err_slot.into()], "") .map_err(|e| self.err(e))?; @@ -7128,7 +7967,11 @@ impl<'c, 'a> FnGen<'c, 'a> { if stage.captures.len() != stage.capture_tys.len() { return Err(self.err(format!( "par_map stage `{}` capture operand/type count mismatch: {} operands, {} types", - stage.func.as_deref().unwrap_or(""), + stage + .func + .as_ref() + .map(ProgramCall::as_str) + .unwrap_or(""), stage.captures.len(), stage.capture_tys.len() ))); @@ -7177,7 +8020,7 @@ impl<'c, 'a> FnGen<'c, 'a> { let i64t = self.ctx.i64_type(); let in_stride = i64t.const_int(self.element_allocation_size(in_ty), false); let out_stride = i64t.const_int(self.element_allocation_size(out_ty), false); - let work_weight = i64t.const_int(u64::from(*work_weight), false); + let work_weight_value = i64t.const_int(u64::from(*work_weight), false); let context = if all_capture_tys.is_empty() { self.ctx.ptr_type(AddressSpace::default()).const_null() } else { @@ -7197,8 +8040,42 @@ impl<'c, 'a> FnGen<'c, 'a> { .iter() .any(|stage| matches!(stage.kind, ParMapStageKind::Filter | ParMapStageKind::FilterStrContains | ParMapStageKind::FilterField { .. })); let sty = slice_struct_type(self.ctx); + let generated_ids = parallel_generated_ids( + self.program, + &self.callable_preflight.declarations, + self.f, + src, + func, + stages, + capture_tys, + *elem_in, + *elem_out, + *work_weight, + false, + )?; if has_filter { + let count_id = generated_ids + .iter() + .find(|id| { + matches!( + id, + GeneratedId::Parallel(parallel) + if parallel.mode == ParallelKernelMode::FilterCount + ) + }) + .ok_or_else(|| self.err("parallel count identity was not collected"))?; + let scatter_id = generated_ids + .iter() + .find(|id| { + matches!( + id, + GeneratedId::Parallel(parallel) + if parallel.mode == ParallelKernelMode::FilterScatter + ) + }) + .ok_or_else(|| self.err("parallel scatter identity was not collected"))?; let count_kernel = self.par_map_range_kernel( + count_id, func, in_ty, out_ty, @@ -7209,6 +8086,7 @@ impl<'c, 'a> FnGen<'c, 'a> { stages, )?; let scatter_kernel = self.par_map_range_kernel( + scatter_id, func, in_ty, out_ty, @@ -7229,7 +8107,7 @@ impl<'c, 'a> FnGen<'c, 'a> { count.into(), in_stride.into(), out_stride.into(), - work_weight.into(), + work_weight_value.into(), count_kernel.into(), scatter_kernel.into(), ], @@ -7240,7 +8118,11 @@ impl<'c, 'a> FnGen<'c, 'a> { .basic() .expect("align_rt_par_map_filter returns a slice") } else { + let materialize_id = generated_ids + .first() + .ok_or_else(|| self.err("parallel materialize identity was not collected"))?; let kernel = self.par_map_range_kernel( + materialize_id, func, in_ty, out_ty, @@ -7256,7 +8138,7 @@ impl<'c, 'a> FnGen<'c, 'a> { .builder .build_call( self.runtime(RuntimeKey::ParMap), - &[context.into(), in_ptr.into(), count.into(), in_stride.into(), out_stride.into(), work_weight.into(), kernel.into()], + &[context.into(), in_ptr.into(), count.into(), in_stride.into(), out_stride.into(), work_weight_value.into(), kernel.into()], "obuf", ) .map_err(|e| self.err(e))? @@ -7340,7 +8222,7 @@ impl<'c, 'a> FnGen<'c, 'a> { let i64t = self.ctx.i64_type(); let in_stride = i64t.const_int(self.element_allocation_size(in_ty), false); let out_stride = i64t.const_int(self.element_allocation_size(out_ty), false); - let work_weight = i64t.const_int(u64::from(*work_weight), false); + let work_weight_value = i64t.const_int(u64::from(*work_weight), false); let context = if capture_tys.is_empty() { self.ctx.ptr_type(AddressSpace::default()).const_null() } else { @@ -7358,6 +8240,22 @@ impl<'c, 'a> FnGen<'c, 'a> { context }; let kernel = self.par_map_range_kernel( + ¶llel_generated_ids( + self.program, + &self.callable_preflight.declarations, + self.f, + src, + func, + &[], + capture_tys, + *elem_in, + *elem_out, + *work_weight, + true, + )? + .into_iter() + .next() + .ok_or_else(|| self.err("parallel reduce identity was not collected"))?, func, in_ty, out_ty, @@ -7371,7 +8269,7 @@ impl<'c, 'a> FnGen<'c, 'a> { .builder .build_call( self.runtime(RuntimeKey::ParMapReduce), - &[context.into(), in_ptr.into(), count.into(), in_stride.into(), out_stride.into(), work_weight.into(), kernel.into()], + &[context.into(), in_ptr.into(), count.into(), in_stride.into(), out_stride.into(), work_weight_value.into(), kernel.into()], "parsum", ) .map_err(|e| self.err(e))? @@ -9118,15 +10016,42 @@ impl<'c, 'a> FnGen<'c, 'a> { self.builder.build_store(new_ptr, val).map_err(|e| self.err(e))?; new_ptr.into() } - // `error(code)` is identity on the i32 code (the M2 Error repr). - Rvalue::Call(name, args) if name == "error" => self.operand(&args[0])?, - Rvalue::Call(name, args) if name == "print" => return self.gen_print(args), - Rvalue::Call(name, args) if name == "hash64" || name == "hash128" => { - let key = if name == "hash64" { RuntimeKey::Hash64 } else { RuntimeKey::Hash128 }; - return self.gen_hash(key, args); + Rvalue::Call( + DirectCall::Runtime( + RuntimeKey::Print + | RuntimeKey::PrintStr + | RuntimeKey::PrintBool + | RuntimeKey::PrintChar + | RuntimeKey::PrintF32 + | RuntimeKey::PrintF64, + ), + args, + ) => { + return self.gen_print(args); + } + Rvalue::Call(DirectCall::Runtime(RuntimeKey::Hash64), args) => { + return self.gen_hash(RuntimeKey::Hash64, args); + } + Rvalue::Call(DirectCall::Runtime(RuntimeKey::Hash128), args) => { + return self.gen_hash(RuntimeKey::Hash128, args); } - Rvalue::Call(name, args) => { - let callee = self.funcs[name]; + Rvalue::Call(DirectCall::Runtime(key), args) => { + let argv = args + .iter() + .map(|operand| self.operand(operand).map(Into::into)) + .collect::>, _>>()?; + let call = self + .builder + .build_call(self.runtime(*key), &argv, "call") + .map_err(|error| self.err(error))?; + return Ok(call.try_as_basic_value().basic()); + } + Rvalue::Call(DirectCall::Program(name), args) => { + let callee = self + .program_funcs + .get(name) + .copied() + .ok_or_else(|| callable_target_error(name))?; // A foreign call coerces each argument to its SysV form: a `str`/`slice` view → its // data pointer; a `layout(C)` struct → one `i64`/`double` per eightbyte; everything // else passes as its value. A non-extern call passes every argument directly. @@ -9188,12 +10113,21 @@ impl<'c, 'a> FnGen<'c, 'a> { } return Ok(cs.try_as_basic_value().basic()); } - Rvalue::FnAddr { name, .. } => { + Rvalue::FnAddr { target, .. } => { // A non-capturing function value: `{ thunk_ptr, null_env }`. + let declaration = self + .callable_preflight + .declarations + .get(target) + .ok_or_else(|| callable_target_error(target))?; + let id = GeneratedId::FnValue { + target: target.clone(), + signature: canonical_signature(&declaration.signature, self.program)?, + }; let thunk = self - .funcs - .get(&format!("{name}$fnval")) - .ok_or_else(|| self.err(format!("no function-value thunk for {name}")))?; + .generated_funcs + .get(&id) + .ok_or_else(|| self.err(format!("no function-value thunk for {target}")))?; let fn_ptr = thunk.as_global_value().as_pointer_value(); let null_env = self.ctx.ptr_type(AddressSpace::default()).const_null(); let cty = closure_struct_type(self.ctx); @@ -9234,9 +10168,45 @@ impl<'c, 'a> FnGen<'c, 'a> { .map_err(|e| self.err(e))?; self.builder.build_store(fld, v).map_err(|e| self.err(e))?; } + let declaration = self + .callable_preflight + .declarations + .get(lifted) + .ok_or_else(|| callable_target_error(lifted))?; + let explicit = declaration + .signature + .params + .len() + .checked_sub(capture_tys.len()) + .ok_or_else(|| callable_target_error(lifted))?; + let explicit_params = declaration + .signature + .params + .get(..explicit) + .ok_or_else(|| callable_target_error(lifted))?; + let explicit_modes = declaration + .signature + .modes + .get(..explicit) + .ok_or_else(|| callable_target_error(lifted))?; + let explicit_signature = ProgramSignature { + params: explicit_params.to_vec(), + modes: explicit_modes.to_vec(), + ret: declaration.signature.ret, + borrow: declaration.signature.borrow.clone(), + region: declaration.signature.region.clone(), + }; + let id = GeneratedId::Closure { + lifted: lifted.clone(), + explicit_signature: canonical_signature(&explicit_signature, self.program)?, + captures: capture_tys + .iter() + .map(|ty| canonical_ty(*ty, self.program)) + .collect::, _>>()?, + }; let thunk = self - .funcs - .get(&format!("{lifted}$clos")) + .generated_funcs + .get(&id) .ok_or_else(|| self.err(format!("no closure thunk for {lifted}")))?; let fn_ptr = thunk.as_global_value().as_pointer_value(); let cty = closure_struct_type(self.ctx); @@ -11525,6 +12495,21 @@ mod tests { use align_parser::parse_file; use align_sema::check_file; + fn program_call(name: &str) -> ProgramCall { + ProgramCall::try_from_logical(name).expect("valid test program call") + } + + fn direct_program(name: &str) -> DirectCall { + DirectCall::Program(program_call(name)) + } + + fn assert_lowering(error: CodegenError, expected: &str) { + match error { + CodegenError::Lowering(actual) => assert_eq!(actual, expected), + other => panic!("expected a lowering error, got {other}"), + } + } + fn mir(src: &str) -> Program { let mut d = Diagnostics::new(); let toks = tokenize(0, src, &mut d); @@ -11562,7 +12547,7 @@ mod tests { let mut per_unit = mir("fn main() -> i32 = 0\n"); per_unit.imported_fns.push(align_mir::ImportedFn { - name: "dep$identity".to_string(), + name: program_call("dep$identity"), params: vec![Ty::Int(IntTy { bits: 64, signed: true })], param_modes: vec![align_ast::ParamMode::ByValue], ret: Ty::Int(IntTy { bits: 64, signed: true }), @@ -11601,8 +12586,8 @@ mod tests { let i32_ty = Ty::Int(IntTy { bits: 32, signed: true }); let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); - let ext = |name: &str, params: Vec, ret: Ty| hir::ExternFn { - name: name.to_string(), + let ext = |name: &str, params: Vec, ret: Ty| align_mir::ProgramExtern { + name: program_call(name), param_modes: vec![align_ast::ParamMode::ByValue; params.len()], params, ret, @@ -11811,10 +12796,11 @@ mod tests { fn main() -> i32 = 0\n", ); for symbol in symbols { + let encoded = encoded_program_symbol(&program_call(symbol)); assert!( program_ir .lines() - .any(|line| line.starts_with("define ") && line.contains(&format!("@{symbol}("))), + .any(|line| line.starts_with("define ") && line.contains(&format!("@\"{encoded}\"("))), "missing ordinary program definition for {symbol}", ); } @@ -11831,15 +12817,18 @@ mod tests { let tm = create_target_machine(&BuildTarget::Baseline, OptimizationLevel::Default).unwrap(); let runtime = build_module(&ctx, &module, &program, &tm, None, &[], false).unwrap(); let runtime_symbol = &runtime.physical_names[&RuntimeKey::Hash64]; - assert_ne!(runtime_symbol, "align_rt_hash64"); + assert_eq!(runtime_symbol, "align_rt_hash64"); + let program_symbol = encoded_program_symbol(&program_call("align_rt_hash64")); let text = module.print_to_string().to_string(); - assert!(text.lines().any(|line| line.starts_with("define internal i64 @align_rt_hash64("))); + assert!(text.lines().any(|line| { + line.starts_with("define internal i64 ") && line.contains(&program_symbol) + })); assert!(text.contains(&format!("call i64 @{runtime_symbol}("))); let function_loc = inkwell::attributes::AttributeLoc::Function; let memory = enum_kind_id("memory"); let willreturn = enum_kind_id("willreturn"); - let program_function = module.get_function("align_rt_hash64").unwrap(); + let program_function = module.get_function(&program_symbol).unwrap(); let runtime_function = module.get_function(runtime_symbol).unwrap(); assert!(program_function.get_enum_attribute(function_loc, memory).is_none()); assert!(program_function.get_enum_attribute(function_loc, willreturn).is_none()); @@ -11858,7 +12847,8 @@ mod tests { let tm = create_target_machine(&BuildTarget::Baseline, OptimizationLevel::Default).unwrap(); let runtime = build_module(&ctx, &module, &program, &tm, None, &[], true).unwrap(); let physical = runtime.physical_names[&RuntimeKey::StrEq].clone(); - assert_ne!(physical, "align_rt_str_eq"); + assert_eq!(physical, "align_rt_str_eq"); + let program_symbol = encoded_program_symbol(&program_call("align_rt_str_eq")); let rt = ctx.create_module("align_rt_collision_fixture"); let layout = module.get_data_layout(); @@ -11874,7 +12864,7 @@ mod tests { } link_in_rt_lto(&ctx, &module, rt, &runtime).unwrap(); - let program_function = module.get_function("align_rt_str_eq").unwrap(); + let program_function = module.get_function(&program_symbol).unwrap(); let runtime_function = module.get_function(&physical).unwrap(); assert_ne!(program_function.get_type(), runtime_function.get_type()); assert!(program_function.count_basic_blocks() > 0); @@ -12007,9 +12997,9 @@ mod tests { .expect("test module boundary exists") .0; let string_lookups: Vec<_> = implementation.match_indices("self.funcs[").collect(); - assert_eq!(string_lookups.len(), 2, "unexpected dedicated string lookup: {string_lookups:?}"); - assert!(implementation.contains("self.funcs[&format!(\"tramp${}\"")); - assert!(implementation.contains("let callee = self.funcs[name];")); + assert!(string_lookups.is_empty(), "unexpected mixed string lookup: {string_lookups:?}"); + assert!(source.contains(".program_funcs\n .get(name)")); + assert!(source.contains(".generated_funcs\n .get(&tramp_id)")); let compact: String = implementation.split_whitespace().collect(); assert!( !compact.contains("self.funcs.get(\""), @@ -12330,7 +13320,7 @@ mod tests { let n = stmts.len(); let slot_align = vec![None; slots.len()]; let mut fns = vec![Function { - name: "main".to_string(), + name: program_call("main"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -12404,7 +13394,7 @@ mod tests { let i32_ty = Ty::Int(IntTy { bits: 32, signed: true }); let program = Program { fns: vec![Function { - name: "main".to_string(), + name: program_call("main"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -12444,7 +13434,7 @@ mod tests { let i32_ty = Ty::Int(IntTy { bits: 32, signed: true }); let program = Program { fns: vec![Function { - name: "main".to_string(), + name: program_call("main"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -12458,7 +13448,7 @@ mod tests { stmts: vec![Stmt::Let( 0, Rvalue::Closure { - lifted: "unused".to_string(), + lifted: program_call("unused"), captures: vec![], capture_tys: vec![Ty::Tagged(7)], signature: align_mir::FnSignatureFacts { @@ -12497,7 +13487,7 @@ mod tests { let zero = Operand::Const(Const::Int(0, i32_ty)); let program = Program { fns: vec![Function { - name: "main".to_string(), + name: program_call("main"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -12544,7 +13534,7 @@ mod tests { let i32_ty = Ty::Int(IntTy { bits: 32, signed: true }); let program = |payload| Program { fns: vec![Function { - name: "main".to_string(), + name: program_call("main"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -12595,7 +13585,7 @@ mod tests { let i32_ty = Ty::Int(IntTy { bits: 32, signed: true }); let program = |structs, enums, tagged_types| Program { fns: vec![Function { - name: "main".to_string(), + name: program_call("main"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -12671,7 +13661,7 @@ mod tests { let i32_ty = Ty::Int(IntTy { bits: 32, signed: true }); let base = || Program { fns: vec![Function { - name: "main".to_string(), + name: program_call("main"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -12920,7 +13910,7 @@ mod tests { let i32_ty = Ty::Int(IntTy { bits: 32, signed: true }); let program = Program { fns: vec![Function { - name: "main".to_string(), + name: program_call("main"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -12960,7 +13950,7 @@ mod tests { let i32_ty = Ty::Int(IntTy { bits: 32, signed: true }); let program = Program { fns: vec![Function { - name: "main".to_string(), + name: program_call("main"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -12998,7 +13988,7 @@ mod tests { let program = Program { fns: vec![Function { - name: "main".to_string(), + name: program_call("main"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -13054,7 +14044,7 @@ mod tests { let i64_scalar = Scalar::Int(IntTy { bits: 64, signed: true }); let program = |tagged_types: Vec, value_tys: Vec| Program { fns: vec![Function { - name: "main".to_string(), + name: program_call("main"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -13150,7 +14140,7 @@ mod tests { let err = codegen_program( vec![ Stmt::Let(0, Rvalue::Load(0)), - Stmt::Let(1, Rvalue::Call("print".to_string(), vec![Operand::Value(0)])), + Stmt::Let(1, Rvalue::Call(DirectCall::Runtime(RuntimeKey::Print), vec![Operand::Value(0)])), ], vec![*ty, Ty::Unit], vec![*ty], @@ -13160,9 +14150,10 @@ mod tests { ) .expect_err(&format!("print of a `{name}` must not reach the runtime call")); let text = err.to_string(); - assert!( - text.contains("'print' expects an int, float, str, bool, or char"), - "`print({name})` must name the display contract, got: {text}" + assert_eq!( + text, + "lowering failed: callable metadata invalid:InvalidGraph", + "`print({name})` must fail in callable preflight" ); } } @@ -13173,7 +14164,7 @@ mod tests { #[test] fn a_malformed_unit_value_id_is_an_error_not_a_panic() { let unit_fn = Function { - name: "u".to_string(), + name: program_call("u"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -13188,7 +14179,7 @@ mod tests { }; let err = codegen_program( vec![ - Stmt::Let(0, Rvalue::Call("u".to_string(), vec![])), + Stmt::Let(0, Rvalue::Call(direct_program("u"), vec![])), Stmt::Let(1, Rvalue::Use(Operand::Value(0))), ], vec![Ty::Unit, Ty::Unit], @@ -13209,7 +14200,7 @@ mod tests { fn a_malformed_par_map_region_result_is_an_error_not_a_panic() { let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); let return_str = Function { - name: "return_str".to_string(), + name: program_call("return_str"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], return_borrow: hir::ReturnBorrowSummary::None, @@ -13234,7 +14225,7 @@ mod tests { 1, Rvalue::ParMapParallel { src: Operand::Value(0), - func: "return_str".to_string(), + func: program_call("return_str"), stages: vec![], captures: vec![], capture_tys: vec![], @@ -13251,17 +14242,14 @@ mod tests { vec![return_str], ) .expect_err("a range par_map must reject a region-bearing result before LLVM lowering"); - assert!( - err.to_string().contains("non-owning primitive scalar"), - "the codegen guard should name the result contract, got: {err}" - ); + assert_lowering(err, "callable metadata invalid:InvalidGraph"); } #[test] fn malformed_par_map_result_container_is_checked_against_element_output() { let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); let return_i64 = Function { - name: "return_i64".to_string(), + name: program_call("return_i64"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], return_borrow: hir::ReturnBorrowSummary::None, @@ -13289,7 +14277,7 @@ mod tests { 1, Rvalue::ParMapParallel { src: Operand::Value(0), - func: "return_i64".to_string(), + func: program_call("return_i64"), stages: vec![], captures: vec![], capture_tys: vec![], @@ -13306,10 +14294,7 @@ mod tests { vec![return_i64.clone()], ) .expect_err("a range par_map result container must match its primitive element output"); - assert!( - err.to_string().contains("does not match declared output"), - "the codegen guard should name the result/container mismatch, got: {err}" - ); + assert_lowering(err, "callable metadata invalid:InvalidGraph"); } } @@ -13318,7 +14303,7 @@ mod tests { let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); let string_ty = Ty::String; let return_i64 = Function { - name: "return_i64".to_string(), + name: program_call("return_i64"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], return_borrow: hir::ReturnBorrowSummary::None, @@ -13344,7 +14329,7 @@ mod tests { 1, Rvalue::ParMapParallel { src: Operand::Value(0), - func: "return_i64".to_string(), + func: program_call("return_i64"), stages: vec![], captures: vec![], capture_tys: vec![], @@ -13361,10 +14346,7 @@ mod tests { vec![return_i64], ) .expect_err("a range kernel must reject an owning source element before loading it"); - assert!( - err.to_string().contains("primitive, str, or a Copy struct"), - "the codegen ownership guard should reject Move kernel values, got: {err}" - ); + assert_lowering(err, "callable metadata invalid:InvalidGraph"); } #[test] @@ -13372,7 +14354,7 @@ mod tests { let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); let chunk_ty = Ty::DynSliceArray(align_sema::PrimScalar::Int(IntTy { bits: 64, signed: true })); let return_i64 = Function { - name: "return_i64".to_string(), + name: program_call("return_i64"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], return_borrow: hir::ReturnBorrowSummary::None, @@ -13392,7 +14374,7 @@ mod tests { 1, Rvalue::ParMapParallel { src: Operand::Value(0), - func: "return_i64".to_string(), + func: program_call("return_i64"), stages: vec![], captures: vec![], capture_tys: vec![], @@ -13409,7 +14391,7 @@ mod tests { vec![return_i64], ) .expect_err("a chunk par_map with a scalar element declaration must fail before kernel lowering"); - assert!(err.to_string().contains("source element type"), "got: {err}"); + assert_lowering(err, "callable metadata invalid:InvalidGraph"); } #[test] @@ -13417,7 +14399,7 @@ mod tests { let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); let chunk_ty = Ty::DynSliceArray(align_sema::PrimScalar::Int(IntTy { bits: 64, signed: true })); let return_i64 = Function { - name: "return_i64".to_string(), + name: program_call("return_i64"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], return_borrow: hir::ReturnBorrowSummary::None, @@ -13437,7 +14419,7 @@ mod tests { 1, Rvalue::ParMapReduce { src: Operand::Value(0), - func: "return_i64".to_string(), + func: program_call("return_i64"), captures: vec![], capture_tys: vec![], elem_in: i64_ty, @@ -13453,7 +14435,7 @@ mod tests { vec![return_i64], ) .expect_err("a chunk reduction with a scalar element declaration must fail before kernel lowering"); - assert!(err.to_string().contains("reduction source element type"), "got: {err}"); + assert_lowering(err, "callable metadata invalid:InvalidGraph"); } #[test] @@ -13463,7 +14445,7 @@ mod tests { let input_ty = Ty::Slice(Scalar::Int(IntTy { bits: 64, signed: true })); let wrong_output_ty = Ty::Slice(Scalar::Int(IntTy { bits: 32, signed: true })); let keep = Function { - name: "keep".to_string(), + name: program_call("keep"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], return_borrow: hir::ReturnBorrowSummary::None, @@ -13477,7 +14459,7 @@ mod tests { exportable: false, }; let finish = Function { - name: "finish".to_string(), + name: program_call("finish"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], return_borrow: hir::ReturnBorrowSummary::None, @@ -13497,10 +14479,10 @@ mod tests { 1, Rvalue::ParMapParallel { src: Operand::Value(0), - func: "finish".to_string(), + func: program_call("finish"), stages: vec![ParMapStage { kind: ParMapStageKind::Filter, - func: Some("keep".to_string()), + func: Some(program_call("keep")), captures: vec![], capture_tys: vec![], elem_in: input_ty, @@ -13521,7 +14503,7 @@ mod tests { vec![keep, finish], ) .expect_err("a chunk filter that changes its slice type must fail before kernel lowering"); - assert!(err.to_string().contains("filter stage must preserve"), "got: {err}"); + assert_lowering(err, "callable metadata invalid:InvalidGraph"); } #[test] @@ -13530,7 +14512,7 @@ mod tests { let str_ty = Ty::Str; let source_ty = Ty::Slice(Scalar::Str); let finish = Function { - name: "finish".to_string(), + name: program_call("finish"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], return_borrow: hir::ReturnBorrowSummary::None, @@ -13556,10 +14538,10 @@ mod tests { 2, Rvalue::ParMapParallel { src: Operand::Value(0), - func: "finish".to_string(), + func: program_call("finish"), stages: vec![ParMapStage { kind: ParMapStageKind::FilterStrContains, - func: Some("not-compiler-generated".to_string()), + func: Some(program_call("not-compiler-generated")), captures: vec![Operand::Value(1)], capture_tys: vec![str_ty], elem_in: str_ty, @@ -13580,14 +14562,14 @@ mod tests { vec![finish], ) .expect_err("a compiler-generated string filter must not accept a callable name"); - assert!(err.to_string().contains("string filter"), "got: {err}"); + assert_lowering(err, "callable metadata invalid:InvalidGraph"); } #[test] fn malformed_par_map_reduce_source_is_an_error_not_a_panic() { let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); let return_i64 = Function { - name: "return_i64".to_string(), + name: program_call("return_i64"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], return_borrow: hir::ReturnBorrowSummary::None, @@ -13605,7 +14587,7 @@ mod tests { 0, Rvalue::ParMapReduce { src: Operand::Const(Const::Int(0, i64_ty)), - func: "return_i64".to_string(), + func: program_call("return_i64"), captures: vec![], capture_tys: vec![], elem_in: i64_ty, @@ -13620,7 +14602,7 @@ mod tests { vec![return_i64], ) .expect_err("a reduction source that is not a slice must fail before aggregate extraction"); - assert!(err.to_string().contains("reduction source must be a slice"), "got: {err}"); + assert_lowering(err, "callable metadata invalid:InvalidGraph"); } #[test] @@ -13628,7 +14610,7 @@ mod tests { let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); let f64_ty = Ty::Float(FloatTy { bits: 64 }); let return_i64 = Function { - name: "return_i64".to_string(), + name: program_call("return_i64"), params: vec![0, 1], param_modes: vec![align_ast::ParamMode::ByValue, align_ast::ParamMode::ByValue], return_borrow: hir::ReturnBorrowSummary::None, @@ -13649,7 +14631,7 @@ mod tests { 1, Rvalue::ParMapReduce { src: Operand::Value(0), - func: "return_i64".to_string(), + func: program_call("return_i64"), captures: vec![Operand::Const(Const::Float(0.0, f64_ty))], capture_tys: vec![i64_ty], elem_in: i64_ty, @@ -13665,7 +14647,7 @@ mod tests { vec![return_i64], ) .expect_err("a reduction capture operand/type mismatch must fail before context lowering"); - assert!(err.to_string().contains("reduction capture has type"), "got: {err}"); + assert_lowering(err, "callable metadata invalid:InvalidGraph"); } #[test] @@ -13673,7 +14655,7 @@ mod tests { let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); let u64_ty = Ty::Int(IntTy { bits: 64, signed: false }); let return_i64 = Function { - name: "return_u64".to_string(), + name: program_call("return_u64"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], return_borrow: hir::ReturnBorrowSummary::None, @@ -13694,7 +14676,7 @@ mod tests { 1, Rvalue::ParMapReduce { src: Operand::Value(0), - func: "return_u64".to_string(), + func: program_call("return_u64"), captures: vec![], capture_tys: vec![], elem_in: i64_ty, @@ -13710,7 +14692,7 @@ mod tests { vec![return_i64], ) .expect_err("signed and unsigned integer callable types must not share a range kernel"); - assert!(err.to_string().contains("semantic signature"), "got: {err}"); + assert_lowering(err, "callable target invalid:72657475726e5f753634"); } /// The sibling lookup in the same accessor: an argument index past the LLVM parameter list (a @@ -13811,7 +14793,7 @@ mod tests { ) -> String { let program = Program { fns: vec![Function { - name: if count_arg { "allocation_probe" } else { "main" }.to_string(), + name: program_call(if count_arg { "allocation_probe" } else { "main" }), params: if count_arg { vec![0] } else { vec![] }, param_modes: if count_arg { vec![align_ast::ParamMode::ByValue] } else { vec![] }, return_borrow: hir::ReturnBorrowSummary::None, @@ -13845,7 +14827,17 @@ mod tests { } fn function_body<'a>(ir: &'a str, name: &str) -> &'a str { - ir.find(&format!(" @{name}(")) + let encoded = ProgramCall::try_from_logical(name) + .ok() + .map(|name| encoded_program_symbol(&name)); + [Some(name), encoded.as_deref()] + .into_iter() + .flatten() + .find_map(|candidate| { + let unquoted = format!(" @{candidate}("); + let quoted = format!(" @\"{candidate}\"("); + ir.find(&unquoted).or_else(|| ir.find("ed)) + }) .map(|start| &ir[start..]) .and_then(|tail| tail.split_once("{\n").map(|(_, body)| body)) .and_then(|body| body.split("\n}").next()) @@ -13856,7 +14848,7 @@ mod tests { let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); let program = Program { fns: vec![Function { - name: "arena_allocation_probe".to_string(), + name: program_call("arena_allocation_probe"), params: vec![0], param_modes: vec![align_ast::ParamMode::ByValue], return_borrow: hir::ReturnBorrowSummary::None, @@ -13900,7 +14892,7 @@ mod tests { let dynamic = matches!(len, Operand::Arg(0)); let program = Program { fns: vec![Function { - name: if dynamic { "soa_allocation_probe" } else { "main" }.to_string(), + name: program_call(if dynamic { "soa_allocation_probe" } else { "main" }), params: if dynamic { vec![0] } else { vec![] }, param_modes: if dynamic { vec![align_ast::ParamMode::ByValue] } else { vec![] }, return_borrow: hir::ReturnBorrowSummary::None, @@ -13958,7 +14950,8 @@ mod tests { assert!(dynamic.contains("@align_rt_alloc_size_fail"), "missing cold allocation failure:\n{dynamic}"); let arena = arena_allocation_case_ir(); - let arena_body = function_body(&arena, "arena_allocation_probe"); + let arena_name = encoded_program_symbol(&program_call("arena_allocation_probe")); + let arena_body = function_body(&arena, &arena_name); assert!(arena_body.contains("@llvm.umul.with.overflow.i64"), "arena allocation lacks checked multiply:\n{arena_body}"); assert!(arena_body.contains("@align_rt_alloc_size_fail"), "arena allocation lacks overflow failure:\n{arena_body}"); @@ -14376,20 +15369,21 @@ mod tests { assert!(!direct.contains("@align_main")); let unit = ir("fn main() {}\n"); - assert!(unit.contains("define internal void @align_main()")); + let encoded_main = encoded_program_symbol(&program_call("main")); + assert!(unit.contains(&format!("define internal void @\"{encoded_main}\"()"))); assert!(unit.contains("define i32 @main()")); - assert!(unit.contains("call void @align_main()")); + assert!(unit.contains(&format!("call void @\"{encoded_main}\"()"))); assert!(unit.contains("ret i32 0")); let result = ir("fn main() -> Result<(), Error> { return Ok(()) }\n"); assert!(result.contains("define internal")); - assert!(result.contains("@align_main()")); + assert!(result.contains(&format!("@\"{encoded_main}\"()"))); assert!(result.contains("define i32 @main()")); let argv = ir( "fn main(args: array) -> Result<(), Error> { return Ok(()) }\n", ); - assert!(argv.contains("@align_main({ ptr, i64 }")); + assert!(argv.contains(&format!("@\"{encoded_main}\"({{ ptr, i64 }}"))); assert!(argv.contains("define i32 @main(i32")); assert!(argv.contains("ptr %1")); @@ -14581,7 +15575,8 @@ mod tests { let out = ir("fn sq(x: i64) -> i64 = x * x\nfn main() -> i32 = sq(7) as i32\n"); // `sq` is a non-exported program fn → `internal` (M13 Slice 1); `main` is the C entry → // external (no linkage word). Both still carry the `#0` nounwind attribute group. - assert!(out.contains("define internal i64 @sq(i64 %0) #0")); + let sq = encoded_program_symbol(&program_call("sq")); + assert!(out.contains(&format!("define internal i64 @\"{sq}\"(i64 %0) #0"))); assert!(out.contains("define i32 @main() #0")); assert!(out.contains("attributes #0 = { nounwind }")); // ...but the external runtime declarations (ordinary Rust fns) are NOT promised nounwind. @@ -14698,7 +15693,7 @@ mod tests { // audited. This pins the safety property independently of today's surface type limits. let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); let f = Function { - name: "future_wrapper".into(), + name: program_call("future_wrapper"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -15162,103 +16157,218 @@ mod tests { } #[test] - fn staged_kernel_structural_key_disambiguates_callable_boundaries() { - let stage = |func: &str| ParMapStage { - kind: ParMapStageKind::Map, - func: Some(func.to_string()), - captures: Vec::new(), - capture_tys: Vec::new(), - elem_in: Ty::Bool, - elem_out: Ty::Bool, - }; - // The readable `$`-joined names would collide for one callable named `a$b` and two - // callables named `a` then `b`; the structural key must keep those kernels distinct. - let embedded = FnGen::par_map_stage_structural_key(&[stage("a$b")]); - let separated = FnGen::par_map_stage_structural_key(&[stage("a"), stage("b")]); - assert_ne!(embedded, separated); + fn m0_emits_main_returning_i32() { + let text = ir("fn main() -> i32 {\n x := 1\n return x\n}\n"); + assert!(text.contains("define i32 @main()"), "got:\n{text}"); } #[test] - fn range_kernel_identity_includes_mode_types_and_callable_signatures() { + fn callable_namespace_symbols_classes_and_precedence() { + fn identity(name: &str) -> Function { + let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); + Function { + name: program_call(name), + params: vec![0], + param_modes: vec![align_ast::ParamMode::ByValue], + return_borrow: hir::ReturnBorrowSummary::None, + return_region: hir::ReturnRegionSummary::None, + ret: i64_ty, + slots: vec![i64_ty], + slot_align: vec![None], + value_tys: vec![i64_ty], + blocks: vec![Block { + id: 0, + stmts: vec![Stmt::Let(0, Rvalue::Load(0))], + stmt_lines: vec![(0, 0)], + term: Term::Return(Some(Operand::Value(0))), + }], + entry: 0, + exportable: false, + } + } + let i64_ty = Ty::Int(IntTy { bits: 64, signed: true }); - let i32_ty = Ty::Int(IntTy { bits: 32, signed: true }); - let no_stages = Vec::new(); - let no_stage_signatures = Vec::new(); - let base = FnGen::par_map_kernel_identity_key( - "f", - "i64", - "i64", - i64_ty, - i64_ty, - &[i64_ty], - "materialize", - &no_stages, - "i64 (i64)", - &no_stage_signatures, + let text = codegen_program( + vec![ + Stmt::Let( + 0, + Rvalue::Call( + direct_program("print"), + vec![Operand::Const(Const::Int(7, i64_ty))], + ), + ), + Stmt::Let( + 1, + Rvalue::Call( + DirectCall::Runtime(RuntimeKey::Print), + vec![Operand::Value(0)], + ), + ), + ], + vec![i64_ty, Ty::Unit], + vec![], + vec![], + vec![], + vec![identity("print"), identity("pkg$é")], + ) + .expect("typed program/runtime targets with equal logical spelling must coexist"); + let program_print = encoded_program_symbol(&program_call("print")); + assert!(text.contains(&format!("@\"{program_print}\"")), "{text}"); + assert!(text.contains("call void @align_rt_print_i64(i64"), "{text}"); + assert!( + text.contains("@\"align_fn$6$706b6724c3a9\""), + "UTF-8 byte length and raw bytes must determine the encoded symbol:\n{text}" ); - assert!(base.contains("mode=materialize")); - assert!(base.contains("terminal_captures=")); - assert_ne!( - base, - FnGen::par_map_kernel_identity_key( - "f", - "i64", - "i64", - i64_ty, - i64_ty, - &[i32_ty], - "materialize", - &no_stages, - "i64 (i64)", - &no_stage_signatures, - ), - "terminal capture types must be part of the cache identity" + + let missing = codegen_program( + vec![Stmt::Let( + 0, + Rvalue::Call( + direct_program("missing"), + vec![Operand::Const(Const::Int(0, i64_ty))], + ), + )], + vec![i64_ty], + vec![], + vec![], + vec![], + vec![], + ) + .expect_err("an absent typed program target must fail preflight"); + assert_lowering(missing, "callable target invalid:6d697373696e67"); + + let mut conflict = mir("fn dup(value: i64) -> i64 = value\nfn main() -> i32 = 0\n"); + conflict.externs.push(align_mir::ProgramExtern { + name: program_call("dup"), + params: vec![i64_ty], + param_modes: vec![align_ast::ParamMode::ByValue], + ret: i64_ty, + return_borrow: hir::ReturnBorrowSummary::None, + return_region: hir::ReturnRegionSummary::None, + }); + let conflict = emit_llvm_ir(&conflict, &BuildTarget::Baseline, false, &[], None) + .expect_err("stored and extern declarations cannot share one logical target"); + assert_lowering(conflict, "callable declaration conflict:647570"); + + let exported = mir( + "fn align_rt_print_i64(value: i64) -> i64 = value\nfn main() -> i32 = 0\n", ); - assert_ne!( - base, - FnGen::par_map_kernel_identity_key( - "f", - "i64", - "i64", - i32_ty, - i64_ty, - &[], - "materialize", - &no_stages, - "i64 (i32)", - &no_stage_signatures, - ), - "declared element types and callable signatures must be part of the cache identity" + let collision = emit_llvm_ir( + &exported, + &BuildTarget::Baseline, + false, + &["align_rt_print_i64".to_owned()], + None, + ) + .expect_err("an explicit export cannot claim a fixed native identity"); + assert_lowering( + collision, + "callable external identity collision:616c69676e5f72745f7072696e745f693634", ); - assert_ne!( - base, - FnGen::par_map_kernel_identity_key( - "f", - "i64", - "i64", - i64_ty, - i64_ty, - &[i64_ty], - "reduce", - &no_stages, - "i64 (i64)", - &no_stage_signatures, - ), - "materialization and reduction kernels must never share a cache identity" + + let mut precedence = mir( + "extern \"C\" fn align_rt_print_i64(value: i32)\nfn main() -> i32 = 0\n", + ); + precedence.fns[0].blocks[0].stmts.push(Stmt::Let( + 0, + Rvalue::Call(direct_program("missing"), vec![]), + )); + precedence.fns[0].value_tys.push(i64_ty); + precedence.fns[0].blocks[0].stmt_lines.push((0, 0)); + let precedence = emit_llvm_ir(&precedence, &BuildTarget::Baseline, false, &[], None) + .expect_err("extern ABI validation must precede callable target validation"); + assert_lowering( + precedence, + "native extern ABI mismatch:616c69676e5f72745f7072696e745f693634", ); } #[test] - fn m0_emits_main_returning_i32() { - let text = ir("fn main() -> i32 {\n x := 1\n return x\n}\n"); - assert!(text.contains("define i32 @main()"), "got:\n{text}"); + fn generated_identity_collection_families_pairs_and_probes() { + let mut fn_value = mir( + "fn noop() {}\nfn main() -> i32 {\n first := noop\n second := noop\n first()\n second()\n return 0\n}\n", + ); + let occupied = "align_gen$fnval$6e6f6f70$0"; + fn_value.externs.push(align_mir::ProgramExtern { + name: program_call(occupied), + params: vec![], + param_modes: vec![], + ret: Ty::Unit, + return_borrow: hir::ReturnBorrowSummary::None, + return_region: hir::ReturnRegionSummary::None, + }); + let text = emit_llvm_ir(&fn_value, &BuildTarget::Baseline, false, &[], None) + .expect("an occupied generated candidate must probe deterministically"); + assert!(text.contains("align_gen$fnval$6e6f6f70$1"), "{text}"); + assert_eq!( + text.matches("define private void @\"align_gen$fnval$6e6f6f70$1\"") + .count(), + 1, + "equal FnValue requests must deduplicate:\n{text}" + ); + + let closure = ir( + "fn main() -> i32 {\n captured: i64 := 1\n f := fn value: i64 { value + captured }\n return f(1) as i32\n}\n", + ); + assert!(closure.contains("align_gen$clos$"), "{closure}"); + + let task = ir( + "fn main() -> Result<(), Error> {\n task_group {\n task := spawn(fn { 1 })\n wait()\n print(task.get())\n }\n return Ok(())\n}\n", + ); + assert!(task.contains("align_gen$tramp$"), "{task}"); + + let materialize = ir( + "fn twice(value: i64) -> i64 = value * 2\n\ + fn main() -> i32 {\n values := [1, 2].par_map(twice)\n return values[0] as i32\n}\n", + ); + assert!(materialize.contains("align_gen$par$0$"), "{materialize}"); + + let filtered_program = mir( + "fn keep(value: i64) -> bool = value > 0\n\ + fn twice(value: i64) -> i64 = value * 2\n\ + fn main() -> i32 {\n values := [1, 2].where(keep).par_map(twice)\n return values[0] as i32\n}\n", + ); + let declarations = callable_declarations(&filtered_program).unwrap(); + let preflight = callable_preflight(&filtered_program, &[], declarations).unwrap(); + let mut filter_ids = preflight + .generated_names + .keys() + .filter(|id| { + matches!(id, GeneratedId::Parallel(parallel) + if matches!(parallel.mode, ParallelKernelMode::FilterCount | ParallelKernelMode::FilterScatter)) + }) + .cloned() + .collect::>(); + filter_ids.sort(); + assert_eq!(filter_ids.len(), 2, "filter collection must retain one count/scatter pair"); + let missing_partner = vec![filter_ids[0].clone()]; + let missing_bytes = missing_partner[0].to_canonical_bytes().unwrap(); + let error = validate_generated_pairs(&missing_partner) + .expect_err("one retained filter identity cannot omit its partner"); + assert_lowering( + error, + &format!( + "generated count-scatter pair mismatch:{}", + lowercase_hex(&missing_bytes) + ), + ); + + let stem = "align_gen$probe"; + let maximum = format!("{stem}${}", u64::MAX); + let mut reserved = HashMap::from([(maximum.clone(), "extern".to_owned())]); + let error = probe_generated_name(stem, &mut reserved, u64::MAX) + .expect_err("an occupied maximum probe must fail without wrapping"); + assert_lowering( + error, + &format!("generated name exhausted:{}", lowercase_hex(maximum.as_bytes())), + ); } #[test] fn unit_fn_value_uses_void_indirect_call_abi() { let text = ir("fn noop() {}\nfn main() -> i32 {\n f := noop\n f()\n return 0\n}\n"); assert!( - text.contains("define private void @\"noop$fnval\"(ptr"), + text.contains("define private void @\"align_gen$fnval$6e6f6f70$0\"(ptr"), "Unit thunk must return void:\n{text}" ); assert!( @@ -15275,8 +16385,9 @@ mod tests { fn fib_emits_calls_and_branch() { let src = "fn fib(n: i64) -> i64 {\n if n < 2 { return n }\n return fib(n - 1) + fib(n - 2)\n}\n"; let text = ir(src); - assert!(text.contains("define internal i64 @fib(i64"), "got:\n{text}"); - assert!(text.contains("call i64 @fib"), "expected recursive calls:\n{text}"); + let fib = encoded_program_symbol(&program_call("fib")); + assert!(text.contains(&format!("define internal i64 @\"{fib}\"(i64")), "got:\n{text}"); + assert!(text.contains(&format!("call i64 @\"{fib}\"")), "expected recursive calls:\n{text}"); assert!(text.contains("icmp slt"), "expected signed comparison:\n{text}"); } } diff --git a/crates/align_codegen_llvm/src/thinlto.rs b/crates/align_codegen_llvm/src/thinlto.rs index 62fde184..710cec40 100644 --- a/crates/align_codegen_llvm/src/thinlto.rs +++ b/crates/align_codegen_llvm/src/thinlto.rs @@ -172,10 +172,10 @@ pub fn ir_opt_level(profile: Profile) -> c_int { /// ThinLTO preserve set. Mirrors [`crate::declare_fn`]'s linkage decision and /// [`crate::emit_main_wrapper`] exactly: /// * a `main` function yields the external C entry symbol `main` (a plain `-> i32` -/// main, or the generated wrapper for a `Result`/`Unit` main — never the internal -/// `align_main`); +/// main, or the generated wrapper for a `Result`/`Unit` main — never its internal +/// encoded Align body); /// * a `pub` non-entry function (`f.exportable`) keeps external linkage under its -/// mangled `module$name` symbol; +/// encoded Align program symbol; /// * an `--export` root keeps its symbol external (keyed on the LLVM `symbol`, as /// `declare_fn` does — so `--export main` stays the documented no-op). /// @@ -189,13 +189,13 @@ pub fn exported_symbols(program: &Program, exports: &[String]) -> Vec { } }; for f in &program.fns { - let sym = symbol_name(f); - if f.name == "main" { + let sym = symbol_name(f, exports); + if f.name.as_str() == "main" { // The external C entry is always `main` (direct i32 main or the wrapper). push("main", &mut out); } - if f.exportable || exports.iter().any(|e| e == sym) { - push(sym, &mut out); + if f.exportable || exports.iter().any(|e| e == f.name.as_str()) { + push(&sym, &mut out); } } out @@ -204,7 +204,7 @@ pub fn exported_symbols(program: &Program, exports: &[String]) -> Vec { /// Whether a unit defines a `main` (so its object owns the external C `main`) — a /// convenience for the driver's preserve-set assembly and diagnostics. pub fn defines_main(program: &Program) -> bool { - program.fns.iter().any(|f| f.name == "main") + program.fns.iter().any(|f| f.name.as_str() == "main") } // ---- entry-point wrappers ------------------------------------------------ diff --git a/crates/align_driver/src/lib.rs b/crates/align_driver/src/lib.rs index 214149c3..1da76454 100644 --- a/crates/align_driver/src/lib.rs +++ b/crates/align_driver/src/lib.rs @@ -1591,7 +1591,7 @@ pub fn emit_llvm_ir(mir: &align_mir::Program, target: BuildTarget, optimized: bo pub fn unknown_exports<'a>(mir: &align_mir::Program, exports: &'a [String]) -> Vec<&'a str> { exports .iter() - .filter(|name| !mir.fns.iter().any(|f| &f.name == *name)) + .filter(|name| !mir.fns.iter().any(|f| f.name.as_str() == name.as_str())) .map(String::as_str) .collect() } diff --git a/crates/align_driver/src/main.rs b/crates/align_driver/src/main.rs index 16058c51..1fc934fb 100644 --- a/crates/align_driver/src/main.rs +++ b/crates/align_driver/src/main.rs @@ -454,7 +454,12 @@ fn check_exports_entry(walk: &PerUnitWalk, exports: &[String], path: &str) -> Op if let Some(u) = walk .units .iter() - .find(|u| !u.is_entry && u.mir.fns.iter().any(|f| f.name == name || f.name.ends_with(&suffix))) + .find(|u| { + !u.is_entry + && u.mir.fns.iter().any(|f| { + f.name.as_str() == name || f.name.as_str().ends_with(&suffix) + }) + }) { rejected = true; eprintln!( diff --git a/crates/align_driver/tests/deep_pipeline.rs b/crates/align_driver/tests/deep_pipeline.rs index 6219529c..4a53a9e0 100644 --- a/crates/align_driver/tests/deep_pipeline.rs +++ b/crates/align_driver/tests/deep_pipeline.rs @@ -156,7 +156,7 @@ fn depth_sweep_preserves_fusion_inlining_vectorization_and_small_stack_survival( let function = mir .fns .iter() - .find(|f| f.name == *name) + .find(|f| f.name.as_str() == name) .unwrap_or_else(|| panic!("missing MIR function `{name}`")); assert_eq!( cyclic_components(function), diff --git a/crates/align_driver/tests/export_roots.rs b/crates/align_driver/tests/export_roots.rs index 0e1c0032..fd616cf9 100644 --- a/crates/align_driver/tests/export_roots.rs +++ b/crates/align_driver/tests/export_roots.rs @@ -31,6 +31,15 @@ fn define_prefix<'a>(ir: &'a str, sym: &str) -> &'a str { panic!("no `define` for @{sym} found in IR:\n{ir}"); } +fn encoded(sym: &str) -> String { + let hex = sym + .as_bytes() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + format!("align_fn${}${hex}", sym.len()) +} + fn assert_internal(ir: &str, sym: &str) { let pfx = define_prefix(ir, sym); assert!(pfx.contains("internal"), "@{sym} should have `internal` linkage, got `define {pfx}@{sym}(...`"); @@ -62,13 +71,13 @@ fn exported_fn_is_external() { // The named export root is external; everything else keeps the whole-program default // (`internal`) — `--export` is additive, not a switch that turns off internalization. assert_external(&ir, "k1"); - assert_internal(&ir, "k2"); - assert_internal(&ir, "helper"); + assert_internal(&ir, &encoded("k2")); + assert_internal(&ir, &encoded("helper")); // `helper` must still be defined and actually called from `k1` — the export-roots change must // not perturb which functions are lowered or how they call each other, only their linkage. assert!( - ir.contains("call i64 @helper(") || ir.contains("call i64 @\"helper\"("), + ir.contains(&format!("call i64 @\"{}\"(", encoded("helper"))), "k1 must still call helper:\n{ir}" ); } @@ -81,14 +90,13 @@ fn unexported_still_internal() { // Same library, no `--export` at all: every program function stays internal (pins the M13 // Slice 1 default — `pub` alone never exports; `--export` is the only opt-in). let ir = emit_llvm_with_exports(LIB, &[]); - assert_internal(&ir, "k1"); - assert_internal(&ir, "k2"); - assert_internal(&ir, "helper"); + assert_internal(&ir, &encoded("k1")); + assert_internal(&ir, &encoded("k2")); + assert_internal(&ir, &encoded("helper")); } -/// A `Result`-returning `main`: lowers to TWO LLVM definitions — the body under the renamed symbol -/// `align_main` (`Function::name` is still the source name `"main"`), and a separately generated C -/// `main` wrapper (always external, unconditionally, so `crt0` finds it). +/// A `Result`-returning `main`: lowers to TWO LLVM definitions — the body under its encoded Align +/// symbol, and a separately generated C `main` wrapper (always external so `crt0` finds it). const RESULT_MAIN: &str = concat!( "fn helper(x: i64) -> i64 = x + 1\n", "fn main() -> Result<(), Error> {\n", @@ -103,17 +111,13 @@ fn export_main_is_a_harmless_noop_for_result_main() { return; } // `--export main` names the SOURCE function `main` — but for a `Result`-returning `main`, - // `Function::name == "main"` while the LLVM symbol is `align_main`. A regression here (caught in - // PR review): if the internalization guard compared `exports` against `Function::name` instead - // of the LLVM symbol, `--export main` would spuriously match the `align_main` body too (since - // its `Function::name` is also `"main"`) and leave it wrongly external — silently breaking the - // `link_hygiene.rs` invariant that `main` is the only externally-resolved definition. The C - // `main` wrapper is already external unconditionally (first half of the guard), so `--export - // main` must be a genuine no-op: `align_main` stays internal either way. + // `Function::name == "main"` while the LLVM body uses the encoded program identity. The C + // wrapper is already external unconditionally, so `--export main` must remain a genuine no-op: + // the encoded body stays internal either way. let ir = emit_llvm_with_exports(RESULT_MAIN, &["main"]); assert_external(&ir, "main"); - assert_internal(&ir, "align_main"); - assert_internal(&ir, "helper"); + assert_internal(&ir, &encoded("main")); + assert_internal(&ir, &encoded("helper")); } #[test] diff --git a/crates/align_driver/tests/interface_param_modes.rs b/crates/align_driver/tests/interface_param_modes.rs index c68548fe..a89d572b 100644 --- a/crates/align_driver/tests/interface_param_modes.rs +++ b/crates/align_driver/tests/interface_param_modes.rs @@ -46,7 +46,7 @@ fn whole_and_per_unit_interfaces_preserve_modes_and_explicit_none_summaries() { let put = buffer .fns .iter() - .find(|function| function.name == "put") + .find(|function| function.name.as_str() == "put") .expect("put signature"); assert_eq!( put.params @@ -61,7 +61,7 @@ fn whole_and_per_unit_interfaces_preserve_modes_and_explicit_none_summaries() { let named_borrow = buffer .fns .iter() - .find(|function| function.name == "named_borrow") + .find(|function| function.name.as_str() == "named_borrow") .expect("named_borrow signature"); assert_eq!(named_borrow.params[0].mode, ParamMode::ByValue); @@ -118,7 +118,7 @@ fn per_unit_mir_preserves_defining_and_imported_signature_facts() { .mir .fns .iter() - .find(|function| function.name == "buffer$put") + .find(|function| function.name.as_str() == "buffer$put") .expect("put MIR"); assert_eq!(put.param_modes, vec![ParamMode::Out, ParamMode::ByValue]); assert_eq!(put.return_borrow, ReturnBorrowSummary::None); @@ -134,7 +134,7 @@ fn per_unit_mir_preserves_defining_and_imported_signature_facts() { .mir .imported_fns .iter() - .find(|function| function.name == "buffer$put") + .find(|function| function.name.as_str() == "buffer$put") .expect("imported put declaration"); assert_eq!( imported.param_modes, @@ -174,7 +174,7 @@ fn malformed_mir_signature_facts_fail_before_llvm_emission() { let put = wrong_arity .fns .iter_mut() - .find(|function| function.name == "buffer$put") + .find(|function| function.name.as_str() == "buffer$put") .expect("put MIR"); put.param_modes.pop(); let error = emit_llvm_ir(&wrong_arity, BuildTarget::Baseline, false, &[], false) @@ -188,7 +188,7 @@ fn malformed_mir_signature_facts_fail_before_llvm_emission() { let put = disabled_mode .fns .iter_mut() - .find(|function| function.name == "buffer$put") + .find(|function| function.name.as_str() == "buffer$put") .expect("put MIR"); put.param_modes[0] = ParamMode::Borrow; let error = emit_llvm_ir(&disabled_mode, BuildTarget::Baseline, false, &[], false) @@ -202,7 +202,7 @@ fn malformed_mir_signature_facts_fail_before_llvm_emission() { let put = malformed_roots .fns .iter_mut() - .find(|function| function.name == "buffer$put") + .find(|function| function.name.as_str() == "buffer$put") .expect("put MIR"); put.return_borrow = ReturnBorrowSummary::Roots { params: vec![99], @@ -219,7 +219,7 @@ fn malformed_mir_signature_facts_fail_before_llvm_emission() { let put = premature_capture .fns .iter_mut() - .find(|function| function.name == "buffer$put") + .find(|function| function.name.as_str() == "buffer$put") .expect("put MIR"); put.return_region = ReturnRegionSummary::Roots { params: vec![], @@ -236,7 +236,7 @@ fn malformed_mir_signature_facts_fail_before_llvm_emission() { let put = disagreeing_roots .fns .iter_mut() - .find(|function| function.name == "buffer$put") + .find(|function| function.name.as_str() == "buffer$put") .expect("put MIR"); put.return_borrow = ReturnBorrowSummary::Roots { params: vec![0], @@ -260,7 +260,7 @@ fn malformed_mir_signature_facts_fail_before_llvm_emission() { let scalar = scalar_mir .fns .iter_mut() - .find(|function| function.name == "scalar") + .find(|function| function.name.as_str() == "scalar") .expect("scalar MIR"); scalar.return_borrow = ReturnBorrowSummary::Roots { params: vec![0], @@ -292,7 +292,7 @@ fn malformed_mir_signature_facts_fail_before_llvm_emission() { let scalar = cyclic_mir .fns .iter_mut() - .find(|function| function.name == "scalar") + .find(|function| function.name.as_str() == "scalar") .expect("scalar MIR"); scalar.ret = Ty::Struct(cycle_id); scalar.slots[scalar.params[0] as usize] = Ty::Struct(cycle_id); diff --git a/crates/align_driver/tests/m5.rs b/crates/align_driver/tests/m5.rs index 2a12a20d..6ee8fd0c 100644 --- a/crates/align_driver/tests/m5.rs +++ b/crates/align_driver/tests/m5.rs @@ -1900,7 +1900,7 @@ fn main() -> Result<(), Error> { ); let mir = lower_to_mir(&checked.hir); assert!( - mir.fns.iter().any(|function| function.name == "main"), + mir.fns.iter().any(|function| function.name.as_str() == "main"), "composite scanner must pass the pre-MIR validity gate:\n{}", align_mir::print::program_to_string(&mir) ); diff --git a/crates/align_driver/tests/mir_continuation.rs b/crates/align_driver/tests/mir_continuation.rs index 5099eb17..77ccdb51 100644 --- a/crates/align_driver/tests/mir_continuation.rs +++ b/crates/align_driver/tests/mir_continuation.rs @@ -6,7 +6,14 @@ mod common; use common::*; -use align_mir::{Rvalue, Stmt, Term}; +use align_mir::{DirectCall, Rvalue, Stmt, Term}; + +fn direct_program_name(call: &DirectCall) -> Option<&str> { + match call { + DirectCall::Program(target) => Some(target.as_str()), + DirectCall::Runtime(_) => None, + } +} const SOURCE: &str = "\ Holder { callback: T } @@ -119,13 +126,13 @@ fn eager_children_stop_before_parent_actions() { let function = program .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("{name} function")); let emitted_forbidden_action = |statement: &Stmt| match name { "unary" => matches!(statement, Stmt::Let(_, Rvalue::Un(..))), "binary" | "selected" => matches!(statement, Stmt::Let(_, Rvalue::Bin(..))), "arguments" => { - matches!(statement, Stmt::Let(_, Rvalue::Call(callee, _)) if callee == "call") + matches!(statement, Stmt::Let(_, Rvalue::Call(callee, _)) if direct_program_name(callee) == Some("call")) } "fixed_index" => matches!(statement, Stmt::Let(_, Rvalue::Index(..))), "dynamic_index" => matches!(statement, Stmt::Let(_, Rvalue::SliceIndex(..))), @@ -155,7 +162,7 @@ fn eager_children_stop_before_parent_actions() { .all(|statement| { !matches!( statement, - Stmt::Let(_, Rvalue::Call(callee, _)) if callee == "later" + Stmt::Let(_, Rvalue::Call(callee, _)) if direct_program_name(callee) == Some("later") ) && !emitted_forbidden_action(statement) && !matches!( statement, @@ -179,7 +186,7 @@ fn eager_children_stop_before_parent_actions() { let source_compatible = program .fns .iter() - .find(|function| function.name == "source_compatible") + .find(|function| function.name.as_str() == "source_compatible") .expect("source-compatible indirect call"); assert!( source_compatible @@ -194,7 +201,7 @@ fn eager_children_stop_before_parent_actions() { let string_field = program .fns .iter() - .find(|function| function.name == "string_field") + .find(|function| function.name.as_str() == "string_field") .expect("owned-string element field"); assert!( string_field @@ -218,7 +225,7 @@ fn eager_children_stop_before_parent_actions() { let source_compatible_map_err = program .fns .iter() - .find(|function| function.name == "source_compatible_map_err") + .find(|function| function.name.as_str() == "source_compatible_map_err") .expect("source-compatible map_err"); assert!( source_compatible_map_err diff --git a/crates/align_driver/tests/owned_tagged_payloads.rs b/crates/align_driver/tests/owned_tagged_payloads.rs index b97f89c1..c8017c5e 100644 --- a/crates/align_driver/tests/owned_tagged_payloads.rs +++ b/crates/align_driver/tests/owned_tagged_payloads.rs @@ -150,7 +150,7 @@ fn arena_owned_field_replacement_does_not_drop_the_old_leaf_individually() { .hir .fns .iter_mut() - .find(|f| f.name == "main") + .find(|f| f.name.as_str() == "main") .expect("main function"); main.drop_individual_locals.clear(); for individual in main.drop_individual_exprs.values_mut() { @@ -1487,7 +1487,7 @@ fn ignored_inner_tagged_binding_still_runs_recursive_drop() { let main = program .fns .iter() - .find(|function| function.name == "main") + .find(|function| function.name.as_str() == "main") .expect("main MIR"); let inner_slot = main .slots diff --git a/crates/align_driver/tests/per_unit_codegen.rs b/crates/align_driver/tests/per_unit_codegen.rs index 98446381..f928e8ac 100644 --- a/crates/align_driver/tests/per_unit_codegen.rs +++ b/crates/align_driver/tests/per_unit_codegen.rs @@ -31,6 +31,15 @@ fn norm(sym: &str) -> &str { sym.strip_prefix('_').unwrap_or(sym) } +fn encoded(sym: &str) -> String { + let hex = sym + .as_bytes() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + format!("align_fn${}${hex}", sym.len()) +} + // ---- Gate a: N=1 byte-identity ------------------------------------------------------------------ #[test] @@ -151,7 +160,7 @@ fn main() { .mir .fns .iter() - .find(|function| function.name == "continuation$value") + .find(|function| function.name.as_str() == "continuation$value") .expect("exported value function"); assert!( function @@ -253,16 +262,16 @@ fn gate_c_visibility_symbols() { let has_any = |syms: &[(char, String)], want: &str| syms.iter().any(|(_, n)| norm(n) == want); // Non-entry unit `lib`: its `pub` non-generic fns are external-defined... - assert!(is_ext(&lib_syms, "lib$sumv"), "lib$sumv must be external in lib.o:\n{lib_syms:?}"); - assert!(is_ext(&lib_syms, "lib$dbl"), "lib$dbl must be external in lib.o"); + assert!(is_ext(&lib_syms, &encoded("lib$sumv")), "lib$sumv must be external in lib.o:\n{lib_syms:?}"); + assert!(is_ext(&lib_syms, &encoded("lib$dbl")), "lib$dbl must be external in lib.o"); // ...its private fn is NOT external (internal linkage — never a 'T')... assert!( - !is_ext(&lib_syms, "lib$secret"), + !is_ext(&lib_syms, &encoded("lib$secret")), "lib$secret is private; it must not be an external symbol:\n{lib_syms:?}" ); // ...and the generic's monomorph is emitted consumer-side, so no `lib$id...` symbol here. assert!( - !lib_syms.iter().any(|(_, n)| norm(n).starts_with("lib$id")), + !has_any(&lib_syms, &encoded("lib$id$i64")), "a generic pub fn's monomorph must NOT live in the producer object:\n{lib_syms:?}" ); @@ -270,12 +279,14 @@ fn gate_c_visibility_symbols() { assert!(is_ext(&main_syms, "main"), "main must be external in main.o:\n{main_syms:?}"); // ...the cross-unit call is an undefined reference to the mangled extern... assert!( - main_syms.iter().any(|(k, n)| *k == 'U' && norm(n) == "lib$sumv"), + main_syms + .iter() + .any(|(k, n)| *k == 'U' && norm(n) == encoded("lib$sumv")), "main.o must reference lib$sumv as an undefined extern:\n{main_syms:?}" ); // ...and the in-consumer monomorph lives here (as an internal, non-'T' symbol). assert!( - has_any(&main_syms, "lib$id$i64") || main_syms.iter().any(|(_, n)| norm(n).starts_with("lib$id")), + has_any(&main_syms, &encoded("lib$id$i64")), "the lib$id monomorph must be emitted into the consumer object:\n{main_syms:?}" ); } diff --git a/crates/align_driver/tests/return_provenance.rs b/crates/align_driver/tests/return_provenance.rs index 36e1b67c..8e708d7a 100644 --- a/crates/align_driver/tests/return_provenance.rs +++ b/crates/align_driver/tests/return_provenance.rs @@ -73,7 +73,7 @@ pub fn choose(first: str, second: str, take_first: bool) -> str { summary .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("{name} signature")) }; assert_eq!(find("second").return_borrow, roots(&[1], &[])); @@ -313,7 +313,7 @@ pub fn deferred_pipeline_projection(first: str, second: str) -> str { summary .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("{name} signature")) }; for name in [ @@ -769,7 +769,7 @@ pub fn odd(first: str, second: str, depth: i64) -> str { let function = summary .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("{name} signature")); assert_eq!( function.return_borrow, @@ -845,7 +845,7 @@ fn main() -> i32 { .hir .fns .iter() - .find(|function| function.name == "bad") + .find(|function| function.name.as_str() == "bad") .expect("checked bad function"); let loop_value = bad.body.value.as_deref().expect("loop body value"); assert!( @@ -913,7 +913,7 @@ fn main() -> i32 = 0 .hir .fns .iter() - .find(|function| function.name == "bad") + .find(|function| function.name.as_str() == "bad") .expect("checked bad provenance function"); assert_eq!( bad.return_borrow, @@ -1280,7 +1280,7 @@ fn main() -> i32 { let function = dependency .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("{name} signature")); assert_eq!(function.return_borrow, ReturnBorrowSummary::None); assert_eq!(function.return_region, ReturnRegionSummary::None); @@ -1335,7 +1335,7 @@ fn main() -> i32 = 0 let function = dependency .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("{name} signature")); assert_eq!(function.return_borrow, roots(&[0], &[])); assert_eq!( @@ -1386,7 +1386,7 @@ fn main() -> i32 { let function = dependency .fns .iter() - .find(|function| function.name == "lambda0") + .find(|function| function.name.as_str() == "lambda0") .expect("ordinary exported lambda0"); assert_eq!(function.return_borrow, roots(&[0], &[])); assert_eq!( @@ -1751,7 +1751,7 @@ fn main() -> i32 { let second = views .fns .iter() - .find(|function| function.name == "second") + .find(|function| function.name.as_str() == "second") .expect("second signature"); assert_eq!(second.return_borrow, roots(&[1], &[])); } @@ -1846,7 +1846,7 @@ pub fn identity(value: dep.Payload) -> dep.Payload = value let identity = wrapper .fns .iter() - .find(|function| function.name == "identity") + .find(|function| function.name.as_str() == "identity") .expect("identity signature"); assert_eq!(identity.return_borrow, roots(&[0], &[])); } diff --git a/crates/align_driver/tests/thin_lto.rs b/crates/align_driver/tests/thin_lto.rs index ac5b2a6f..cecda626 100644 --- a/crates/align_driver/tests/thin_lto.rs +++ b/crates/align_driver/tests/thin_lto.rs @@ -33,6 +33,15 @@ fn norm(sym: &str) -> &str { sym.strip_prefix('_').unwrap_or(sym) } +fn encoded(sym: &str) -> String { + let hex = sym + .as_bytes() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + format!("align_fn${}${hex}", sym.len()) +} + /// Build a program's per-unit objects the ThinLTO way (all three phases) into `dir/thin.o`. Uses a /// DISABLED cache + `-j 1` so these S1 symbol-shape gates see a fresh, deterministic build. fn thin_objects(per: &PerUnitBuilt) -> Vec { @@ -100,12 +109,14 @@ fn gate_cross_unit_inline_mutation_checked() { // Direction A (flag OFF): the cross-unit call is an undefined reference. assert!( - has_undef(&base_syms, "lib$add1") && has_undef(&base_syms, "lib$mul2"), + has_undef(&base_syms, &encoded("lib$add1")) + && has_undef(&base_syms, &encoded("lib$mul2")), "flag-off entry object must reference lib$add1 / lib$mul2 as undefined externs:\n{base_syms:?}" ); // Direction B (flag ON): the reference is gone (imported + inlined). assert!( - !has_undef(&thin_syms, "lib$add1") && !has_undef(&thin_syms, "lib$mul2"), + !has_undef(&thin_syms, &encoded("lib$add1")) + && !has_undef(&thin_syms, &encoded("lib$mul2")), "under --thin-lto the cross-unit calls must inline (no undefined ref left):\n{thin_syms:?}" ); @@ -153,13 +164,15 @@ fn gate_wide_tuple_sret_vanishes_when_inlined() { // Without the flag: the 4-/8-i64 tuple returns cross the unit boundary — the producer stores // every field into the sret buffer and the consumer loads them, so the entry references the fns. assert!( - has_undef(&base_syms, "lib$quad") && has_undef(&base_syms, "lib$oct"), + has_undef(&base_syms, &encoded("lib$quad")) + && has_undef(&base_syms, &encoded("lib$oct")), "flag-off entry must reference the tuple-returning fns (the retained cross-unit sret ABI):\n{base_syms:?}" ); // Under --thin-lto: the fns are imported + inlined, so the boundary — and the sret store/load // round trip with it — vanishes (no reference remains). assert!( - !has_undef(&thin_syms, "lib$quad") && !has_undef(&thin_syms, "lib$oct"), + !has_undef(&thin_syms, &encoded("lib$quad")) + && !has_undef(&thin_syms, &encoded("lib$oct")), "under --thin-lto the wide-tuple sret round trip must vanish (fns inlined, no undefined ref):\n{thin_syms:?}" ); @@ -330,11 +343,11 @@ fn gate_preserve_main_and_pub_survive() { assert!(has_ext_def(&entry_syms, "main"), "main must be an external define in the entry object:\n{entry_syms:?}"); // Both pub fns stay external defines in lib.o (preserve set — even `unused`, which nothing calls). assert!( - has_ext_def(&lib_syms, "lib$used"), + has_ext_def(&lib_syms, &encoded("lib$used")), "the preserve set must keep lib$used an external define under --thin-lto:\n{lib_syms:?}" ); assert!( - has_ext_def(&lib_syms, "lib$unused"), + has_ext_def(&lib_syms, &encoded("lib$unused")), "the fail-closed preserve set keeps EVERY pub fn (incl. the uncalled lib$unused) external:\n{lib_syms:?}" ); diff --git a/crates/align_interface/src/lib.rs b/crates/align_interface/src/lib.rs index cd7d5cc9..78c6cb92 100644 --- a/crates/align_interface/src/lib.rs +++ b/crates/align_interface/src/lib.rs @@ -505,7 +505,7 @@ fn partition_capabilities( if caps.is_empty() { continue; } - let unit = owning_unit(&f.name).or(entry_unit.as_ref()); + let unit = owning_unit(f.name.as_str()).or(entry_unit.as_ref()); let Some(unit) = unit else { continue }; let bucket = caps_by_unit.entry(unit.clone()).or_default(); for cap in caps { @@ -560,7 +560,7 @@ fn partition_impl_hashes( // Bucket each MIR function's index by owning unit. let mut fns_by_unit: HashMap> = HashMap::new(); for (i, f) in mir.fns.iter().enumerate() { - let Some(unit) = owning_unit(&f.name).or(entry_unit.as_ref()) else { continue }; + let Some(unit) = owning_unit(f.name.as_str()).or(entry_unit.as_ref()) else { continue }; fns_by_unit.entry(unit.clone()).or_default().push(i); } diff --git a/crates/align_mir/src/canonical_graph.rs b/crates/align_mir/src/canonical_graph.rs index ac75140c..915566a0 100644 --- a/crates/align_mir/src/canonical_graph.rs +++ b/crates/align_mir/src/canonical_graph.rs @@ -1,4 +1,5 @@ use std::collections::{BTreeSet, HashMap, HashSet}; +use std::fmt; use align_ast::ParamMode; use align_sema::{Layout, PrimScalar, Scalar, Ty, hir}; @@ -14,6 +15,16 @@ pub struct FunctionTypeDef { pub return_region: hir::ReturnRegionSummary, } +#[derive(Clone, Debug)] +pub struct ProgramExtern { + pub name: ProgramCall, + pub params: Vec, + pub param_modes: Vec, + pub ret: Ty, + pub return_borrow: hir::ReturnBorrowSummary, + pub return_region: hir::ReturnRegionSummary, +} + #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct ProgramCall(Box); @@ -38,6 +49,10 @@ impl ProgramCall { Ok(Self(value.into())) } + pub(super) fn from_validated(value: &str) -> Self { + Self(value.into()) + } + pub fn as_str(&self) -> &str { &self.0 } @@ -47,6 +62,12 @@ impl ProgramCall { } } +impl fmt::Display for ProgramCall { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct CanonicalTy(Box<[u8]>); diff --git a/crates/align_mir/src/generated_id.rs b/crates/align_mir/src/generated_id.rs index c7f1fa06..175f1f11 100644 --- a/crates/align_mir/src/generated_id.rs +++ b/crates/align_mir/src/generated_id.rs @@ -1,4 +1,21 @@ -use crate::{CanonicalCodecError, CanonicalFnAbi, CanonicalTy, ProgramCall}; +use std::fmt; + +use crate::{CanonicalCodecError, CanonicalFnAbi, CanonicalTy, ProgramCall, RuntimeKey}; + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum DirectCall { + Program(ProgramCall), + Runtime(RuntimeKey), +} + +impl fmt::Display for DirectCall { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Program(target) => target.fmt(f), + Self::Runtime(key) => f.write_str(key.logical_name()), + } + } +} #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[repr(u8)] diff --git a/crates/align_mir/src/lib.rs b/crates/align_mir/src/lib.rs index 256a8378..c7afb7ed 100644 --- a/crates/align_mir/src/lib.rs +++ b/crates/align_mir/src/lib.rs @@ -28,10 +28,10 @@ mod runtime_key; pub use canonical_graph::{ CanonicalCodecError, CanonicalFnAbi, CanonicalTy, FunctionTypeDef, ProgramCall, - ProgramCallError, function_types_are_canonical, + ProgramCallError, ProgramExtern, function_types_are_canonical, }; pub use generated_id::{ - GeneratedId, ParallelGeneratedId, ParallelKernelMode, ParallelStageId, + DirectCall, GeneratedId, ParallelGeneratedId, ParallelKernelMode, ParallelStageId, }; pub use runtime_key::RuntimeKey; @@ -95,7 +95,7 @@ pub type BlockId = u32; /// Align ABI, structural MIR bytes, implementation hash, or object-cache input. #[derive(Clone, Debug)] pub struct ImportedFn { - pub name: String, + pub name: ProgramCall, pub params: Vec, pub param_modes: Vec, pub ret: Ty, @@ -108,7 +108,7 @@ pub struct Program { pub fns: Vec, /// Foreign (`extern "C"`) declarations, passed through from HIR unchanged; codegen emits an /// external LLVM declaration for each, keyed by the C symbol so a `Rvalue::Call` resolves. - pub externs: Vec, + pub externs: Vec, /// M15 S2 (per-unit compilation): non-generic `pub` functions declared by interface-only /// dependencies and defined in another unit's object. Codegen emits an external Align-ABI /// `declare` for each so a `Rvalue::Call` keyed by the mangled `module$name` resolves at link @@ -137,7 +137,7 @@ pub struct Program { #[derive(Clone, Debug)] pub struct Function { - pub name: String, + pub name: ProgramCall, /// Slots holding the incoming parameters, in order. pub params: Vec, /// Source-level parameter modes. This is signature identity even while L2a lowers only the @@ -200,7 +200,11 @@ fn par_map_function_work_weight(f: &Function) -> u8 { } } -fn combined_par_map_stage_work_weight(stages: &[ParMapStage], terminal: &str, weights: &std::collections::HashMap) -> u8 { +fn combined_par_map_stage_work_weight( + stages: &[ParMapStage], + terminal: &ProgramCall, + weights: &std::collections::HashMap, +) -> u8 { let stage_weights = stages.iter().map(|stage| match stage.kind { ParMapStageKind::FilterStrContains => PAR_MAP_STRING_CONTAINS_WORK_WEIGHT, _ => stage @@ -220,7 +224,7 @@ fn combined_par_map_stage_work_weight(stages: &[ParMapStage], terminal: &str, we /// post-pass means a staged node can sum the local cost of every source-first stage and terminal, /// while a separate-compilation import with no body remains the fail-closed default. fn annotate_par_map_work(fns: &mut [Function]) { - let weights: std::collections::HashMap = fns + let weights: std::collections::HashMap = fns .iter() .map(|f| (f.name.clone(), par_map_function_work_weight(f))) .collect(); @@ -379,7 +383,7 @@ pub enum ParMapStageKind { pub struct ParMapStage { pub kind: ParMapStageKind, /// A callable name for `Map`/`Filter`; `None` for compiler-generated field and string stages. - pub func: Option, + pub func: Option, pub captures: Vec, pub capture_tys: Vec, pub elem_in: Ty, @@ -415,15 +419,15 @@ pub enum Rvalue { /// A scalar math builtin (`core.math`): `abs` (1 operand) / `min` / `max` (2). `ty` is the /// numeric operand/result type; lowers to the matching LLVM intrinsic (signedness/float from `ty`). MathOp { fn_: align_sema::MathFn, ty: Ty, operands: Vec }, - Call(String, Vec), + Call(DirectCall, Vec), /// The address of a top-level function as a value (`Ty::Fn`) — a function pointer. - FnAddr { name: String, signature: FnSignatureFacts }, + FnAddr { target: ProgramCall, signature: FnSignatureFacts }, /// A capturing closure value: the lifted function `lifted` (which takes the captures as /// trailing parameters) plus the captured values. Codegen copies the captures into a /// frame-local environment and builds `{ thunk_ptr, env_ptr }`, where the thunk unpacks the /// env and forwards to `lifted`. `capture_tys` give the env layout. Closure { - lifted: String, + lifted: ProgramCall, captures: Vec, capture_tys: Vec, signature: FnSignatureFacts, @@ -650,7 +654,7 @@ pub enum Rvalue { /// small post-lowering cost hint (1/2/4) combined with element byte width by the runtime. ParMapParallel { src: Operand, - func: String, + func: ProgramCall, /// Prior admitted length-preserving scalar/AoS stages. Empty for a direct `par_map`. stages: Vec, captures: Vec, @@ -668,7 +672,7 @@ pub enum Rvalue { /// output slot supplied by the runtime. ParMapReduce { src: Operand, - func: String, + func: ProgramCall, captures: Vec, capture_tys: Vec, elem_in: Ty, @@ -1656,7 +1660,18 @@ fn lower_program_unchecked( } let mut mir = Program { fns, - externs: program.externs.clone(), + externs: program + .externs + .iter() + .map(|extern_| ProgramExtern { + name: ProgramCall::from_validated(&extern_.name), + params: extern_.params.clone(), + param_modes: extern_.param_modes.clone(), + ret: extern_.ret, + return_borrow: extern_.return_borrow.clone(), + return_region: extern_.return_region.clone(), + }) + .collect(), // Cross-unit `pub` callee declares are a per-unit-only concern; the whole-program path has // every callee body in `fns`, so its declare list is empty (byte-identity). imported_fns: if per_unit { @@ -1664,7 +1679,7 @@ fn lower_program_unchecked( .imported_fns .iter() .map(|import| ImportedFn { - name: import.name.clone(), + name: ProgramCall::from_validated(&import.name), params: import.params.clone(), param_modes: import.param_modes.clone(), ret: import.ret, @@ -2985,7 +3000,7 @@ fn lower_fn( .collect(); Function { - name: f.name.clone(), + name: ProgramCall::from_validated(&f.name), params, param_modes: f.param_modes.clone(), ret: f.ret, @@ -5060,7 +5075,7 @@ fn lower_expr_recursive(b: &mut Builder, e: &hir::Expr) -> Operand { let v = b.fresh_value(Ty::Unit); b.push(Stmt::Let( v, - Rvalue::Call("process_exit".to_string(), vec![c]), + Rvalue::Call(DirectCall::Runtime(RuntimeKey::ProcessExit), vec![c]), )); b.terminate(Term::Unreachable); Operand::Const(Const::Unit) @@ -5071,7 +5086,7 @@ fn lower_expr_recursive(b: &mut Builder, e: &hir::Expr) -> Operand { let v = b.fresh_value(Ty::Unit); b.push(Stmt::Let( v, - Rvalue::Call("process_abort".to_string(), vec![]), + Rvalue::Call(DirectCall::Runtime(RuntimeKey::ProcessAbort), vec![]), )); b.terminate(Term::Unreachable); Operand::Const(Const::Unit) @@ -6169,7 +6184,7 @@ fn lower_expr_recursive(b: &mut Builder, e: &hir::Expr) -> Operand { stages, *elem, Some(SortKey { - func: key_func.clone(), + func: ProgramCall::from_validated(key_func), captures: captures.clone(), key_ty: *key_ty, }), @@ -6315,12 +6330,12 @@ fn lower_expr_recursive(b: &mut Builder, e: &hir::Expr) -> Operand { let (kind, func, captures) = match &stage.kind { hir::StageKind::Map { func, captures } => ( ParMapStageKind::Map, - Some(func.clone()), + Some(ProgramCall::from_validated(func)), captures.as_slice(), ), hir::StageKind::Where { func, captures } => ( ParMapStageKind::Filter, - Some(func.clone()), + Some(ProgramCall::from_validated(func)), captures.as_slice(), ), hir::StageKind::Project { field } => { @@ -6370,7 +6385,7 @@ fn lower_expr_recursive(b: &mut Builder, e: &hir::Expr) -> Operand { v, Rvalue::ParMapParallel { src: src.clone(), - func: func.clone(), + func: ProgramCall::from_validated(func), stages: stage_records, captures: capture_ops, capture_tys, @@ -6902,7 +6917,7 @@ fn finish_fn_value(b: &mut Builder, name: &str, ty: Ty) -> Operand { b.push(Stmt::Let( value, Rvalue::FnAddr { - name: name.to_string(), + target: ProgramCall::from_validated(name), signature, }, )); @@ -6925,7 +6940,7 @@ fn finish_closure( b.push(Stmt::Let( value, Rvalue::Closure { - lifted: lifted.to_string(), + lifted: ProgramCall::from_validated(lifted), captures, capture_tys, signature, @@ -6950,12 +6965,39 @@ fn fn_signature_facts(b: &Builder, ty: Ty) -> Option { /// value, while its function ABI is LLVM `void`; the `ValueId` remains as statement bookkeeping, /// but every consumer must receive `Const::Unit` instead of referring to that valueless definition. /// Pipeline callables use this helper as well as ordinary call expressions so the rule cannot drift. -fn emit_named_call(b: &mut Builder, func: String, args: Vec, ret_ty: Ty) -> Operand { +fn emit_named_call(b: &mut Builder, func: ProgramCall, args: Vec, ret_ty: Ty) -> Operand { let v = b.fresh_value(ret_ty); - b.push(Stmt::Let(v, Rvalue::Call(func, args))); + b.push(Stmt::Let(v, Rvalue::Call(DirectCall::Program(func), args))); if ret_ty == Ty::Unit { Operand::Const(Const::Unit) } else { Operand::Value(v) } } +fn direct_call_target(func: &str, args: &[hir::Expr]) -> DirectCall { + let runtime = match func { + "print" => Some(match args.first().map(|argument| argument.ty) { + Some(Ty::Str) => RuntimeKey::PrintStr, + Some(Ty::Bool) => RuntimeKey::PrintBool, + Some(Ty::Char) => RuntimeKey::PrintChar, + Some(Ty::Float(FloatTy { bits: 32 })) => RuntimeKey::PrintF32, + Some(Ty::Float(FloatTy { bits: 64 })) => RuntimeKey::PrintF64, + _ => RuntimeKey::Print, + }), + "hash64" => Some(RuntimeKey::Hash64), + "hash128" => Some(RuntimeKey::Hash128), + "process_exit" => Some(RuntimeKey::ProcessExit), + "process_abort" => Some(RuntimeKey::ProcessAbort), + "div_fail" => Some(RuntimeKey::DivFail), + "bounds_fail" => Some(RuntimeKey::BoundsFail), + "range_fail" => Some(RuntimeKey::RangeFail), + "utf8_boundary_fail" => Some(RuntimeKey::Utf8BoundaryFail), + "len_mismatch_fail" => Some(RuntimeKey::LenMismatchFail), + _ => None, + }; + runtime.map_or_else( + || DirectCall::Program(ProgramCall::from_validated(func)), + DirectCall::Runtime, + ) +} + /// Lower an argument whose value will transfer into a call. A fresh Move temporary needs a hidden /// owner while later arguments are evaluated: if one of them returns or diverges, function cleanup /// drops the already-created value. Once every argument succeeds, the caller clears the owner flag @@ -7090,6 +7132,11 @@ fn lower_direct_call(b: &mut Builder, e: &hir::Expr) -> Operand { return Operand::Const(Const::Unit); } } + // `error(code)` is the language-level constructor for the scalar Error representation. It is + // an identity in MIR, not a program or runtime callable, so it never enters either namespace. + if func == "error" { + return ops.into_iter().next().unwrap_or(Operand::Const(Const::Unit)); + } // A by-value owned-array argument is moved into the callee. Borrow-only intrinsics retain the // source, matching their sema contract. if !borrows_args { @@ -7100,7 +7147,16 @@ fn lower_direct_call(b: &mut Builder, e: &hir::Expr) -> Operand { b.set_drop_flag(owner, false); } } - let result = emit_named_call(b, func.clone(), ops.clone(), e.ty); + let v = b.fresh_value(e.ty); + b.push(Stmt::Let( + v, + Rvalue::Call(direct_call_target(func, args), ops.clone()), + )); + let result = if e.ty == Ty::Unit { + Operand::Const(Const::Unit) + } else { + Operand::Value(v) + }; if let Operand::Value(v) = &result { inherit_borrow_owners(b, *v, &ops); if borrows_args @@ -7186,7 +7242,7 @@ fn lower_int_div(b: &mut Builder, op: BinOp, l: Operand, r: Operand, ty: Ty) -> b.terminate(Term::Branch(Operand::Value(is_zero), fail, ok)); b.cur = fail; let t = b.fresh_value(Ty::Unit); - b.push(Stmt::Let(t, Rvalue::Call("div_fail".to_string(), vec![]))); + b.push(Stmt::Let(t, Rvalue::Call(DirectCall::Runtime(RuntimeKey::DivFail), vec![]))); b.terminate(Term::Unreachable); b.cur = ok; @@ -7296,7 +7352,7 @@ fn lower_vec_div(b: &mut Builder, op: BinOp, l: Operand, r: Operand, s: align_se b.terminate(Term::Branch(Operand::Value(any_zero), fail, ok)); b.cur = fail; let t = b.fresh_value(Ty::Unit); - b.push(Stmt::Let(t, Rvalue::Call("div_fail".to_string(), vec![]))); + b.push(Stmt::Let(t, Rvalue::Call(DirectCall::Runtime(RuntimeKey::DivFail), vec![]))); b.terminate(Term::Unreachable); b.cur = ok; @@ -7343,7 +7399,10 @@ fn emit_bounds_check(b: &mut Builder, idx: &Operand, len: Operand) { // fail: report (index, len) and abort. `bounds_fail` is `-> !`, so the block is `Unreachable`. b.cur = fail; let t = b.fresh_value(Ty::Unit); - b.push(Stmt::Let(t, Rvalue::Call("bounds_fail".to_string(), vec![idx.clone(), len]))); + b.push(Stmt::Let( + t, + Rvalue::Call(DirectCall::Runtime(RuntimeKey::BoundsFail), vec![idx.clone(), len]), + )); b.terminate(Term::Unreachable); b.cur = ok; @@ -7689,7 +7748,13 @@ fn emit_range_bounds_check(b: &mut Builder, start: &Operand, end: &Operand, len: b.cur = fail; let t = b.fresh_value(Ty::Unit); - b.push(Stmt::Let(t, Rvalue::Call("range_fail".to_string(), vec![start.clone(), end.clone(), len]))); + b.push(Stmt::Let( + t, + Rvalue::Call( + DirectCall::Runtime(RuntimeKey::RangeFail), + vec![start.clone(), end.clone(), len], + ), + )); b.terminate(Term::Unreachable); b.cur = ok; @@ -7745,7 +7810,10 @@ fn emit_utf8_boundary_check(b: &mut Builder, base: &Operand, index: &Operand, le let t = b.fresh_value(Ty::Unit); b.push(Stmt::Let( t, - Rvalue::Call("utf8_boundary_fail".to_string(), vec![index.clone(), len.clone()]), + Rvalue::Call( + DirectCall::Runtime(RuntimeKey::Utf8BoundaryFail), + vec![index.clone(), len.clone()], + ), )); b.terminate(Term::Unreachable); @@ -8237,9 +8305,9 @@ enum Reducer { Count, /// `reduce(init, f)`: `f(acc, element)`. `captures` are a lifted lambda's captured values, /// passed after the `(acc, element)` arguments. - Fold { func: String, captures: Vec }, + Fold { func: ProgramCall, captures: Vec }, /// `any(p)` / `all(p)`: `acc || p(element)` / `acc && p(element)`. `captures` as `Fold`. - AnyAll { func: String, captures: Vec, all: bool }, + AnyAll { func: ProgramCall, captures: Vec, all: bool }, /// `min` / `max`: keep `element` when it is smaller / larger than `acc`. MinMax { is_max: bool }, } @@ -8272,11 +8340,11 @@ fn prepare_reducer(b: &mut Builder, spec: ReducerSpec<'_>) -> Option { ReducerSpec::Sum => Reducer::Sum, ReducerSpec::Count => Reducer::Count, ReducerSpec::Fold { func, captures } => Reducer::Fold { - func: func.to_string(), + func: ProgramCall::from_validated(func), captures: lower_captures(b, captures)?, }, ReducerSpec::AnyAll { func, captures, all } => Reducer::AnyAll { - func: func.to_string(), + func: ProgramCall::from_validated(func), captures: lower_captures(b, captures)?, all, }, @@ -8742,7 +8810,7 @@ fn lower_array_par_map_reduce( let v = b.fresh_value(elem_out); b.push(Stmt::Let(v, Rvalue::ParMapReduce { src: src.clone(), - func: func.to_string(), + func: ProgramCall::from_validated(func), captures: capture_ops, capture_tys, elem_in, @@ -8888,7 +8956,12 @@ fn lower_array_reduce( } }; let call_args = stage_call_args(arg, &prepared_stages[stage_idx].captures); - cur = Some(emit_named_call(b, func.clone(), call_args, stage.out_ty)); + cur = Some(emit_named_call( + b, + ProgramCall::from_validated(func), + call_args, + stage.out_ty, + )); } hir::StageKind::Where { func, .. } => { // A scalar element is already loaded; a whole struct element (a struct-consuming @@ -8906,7 +8979,13 @@ fn lower_array_reduce( }; let call_args = stage_call_args(arg, &prepared_stages[stage_idx].captures); let pred = b.fresh_value(Ty::Bool); - b.push(Stmt::Let(pred, Rvalue::Call(func.clone(), call_args))); + b.push(Stmt::Let( + pred, + Rvalue::Call( + DirectCall::Program(ProgramCall::from_validated(func)), + call_args, + ), + )); if guard_rejected { let accepted = b.new_block(); b.terminate(Term::Branch(Operand::Value(pred), accepted, cont)); @@ -8999,7 +9078,7 @@ fn lower_array_reduce( let cur = cur.expect("any/all needs a scalar element"); let t = b.fresh_value(Ty::Bool); let args = stage_call_args(cur, captures); - b.push(Stmt::Let(t, Rvalue::Call(func.clone(), args))); + b.push(Stmt::Let(t, Rvalue::Call(DirectCall::Program(func.clone()), args))); let op = if *all { BinOp::And } else { BinOp::Or }; let n = b.fresh_value(Ty::Bool); b.push(Stmt::Let(n, Rvalue::Bin(op, Operand::Value(a), Operand::Value(t)))); @@ -9151,7 +9230,12 @@ fn lower_json_scan_reduce( hir::StageKind::Map { func, .. } => { let arg = cur.take().unwrap_or_else(|| Operand::Value(lower_struct_elem(b, None, &None, row, &index, struct_id))); let call_args = stage_call_args(arg, &prepared_stages[stage_idx].captures); - cur = Some(emit_named_call(b, func.clone(), call_args, stage.out_ty)); + cur = Some(emit_named_call( + b, + ProgramCall::from_validated(func), + call_args, + stage.out_ty, + )); } hir::StageKind::Where { func, .. } => { let arg = match &cur { @@ -9160,7 +9244,13 @@ fn lower_json_scan_reduce( }; let call_args = stage_call_args(arg, &prepared_stages[stage_idx].captures); let pred = b.fresh_value(Ty::Bool); - b.push(Stmt::Let(pred, Rvalue::Call(func.clone(), call_args))); + b.push(Stmt::Let( + pred, + Rvalue::Call( + DirectCall::Program(ProgramCall::from_validated(func)), + call_args, + ), + )); let accepted = b.new_block(); b.terminate(Term::Branch(Operand::Value(pred), accepted, cont)); b.cur = accepted; @@ -9212,7 +9302,7 @@ fn lower_json_scan_reduce( let cur = cur.unwrap_or_else(|| Operand::Value(lower_struct_elem(b, None, &None, row, &index, struct_id))); let t = b.fresh_value(Ty::Bool); let args = stage_call_args(cur, captures); - b.push(Stmt::Let(t, Rvalue::Call(func.clone(), args))); + b.push(Stmt::Let(t, Rvalue::Call(DirectCall::Program(func.clone()), args))); let op = if *all { BinOp::And } else { BinOp::Or }; let n = b.fresh_value(Ty::Bool); b.push(Stmt::Let(n, Rvalue::Bin(op, Operand::Value(a), Operand::Value(t)))); @@ -9278,7 +9368,7 @@ enum CollectKind<'a> { enum PreparedCollectKind { Collect, - Scan { func: String, init: Operand, captures: Vec }, + Scan { func: ProgramCall, init: Operand, captures: Vec }, } /// `source.….to_array()` / `.scan(init, f)` — the fused loop, but each surviving element is @@ -9320,7 +9410,7 @@ fn lower_array_collect( } } PreparedCollectKind::Scan { - func: func.to_string(), + func: ProgramCall::from_validated(func), init, captures: lowered, } @@ -9454,7 +9544,12 @@ fn lower_array_collect( } }; let call_args = stage_call_args(arg, &prepared_stages[stage_idx].captures); - cur = Some(emit_named_call(b, func.clone(), call_args, stage.out_ty)); + cur = Some(emit_named_call( + b, + ProgramCall::from_validated(func), + call_args, + stage.out_ty, + )); } hir::StageKind::Where { func, .. } => { // A scalar element is already loaded; a whole struct element (a struct-consuming @@ -9472,7 +9567,13 @@ fn lower_array_collect( }; let call_args = stage_call_args(arg, &prepared_stages[stage_idx].captures); let pred = b.fresh_value(Ty::Bool); - b.push(Stmt::Let(pred, Rvalue::Call(func.clone(), call_args))); + b.push(Stmt::Let( + pred, + Rvalue::Call( + DirectCall::Program(ProgramCall::from_validated(func)), + call_args, + ), + )); let keep = b.new_block(); b.terminate(Term::Branch(Operand::Value(pred), keep, cont)); b.cur = keep; @@ -9574,7 +9675,10 @@ fn emit_len_eq_check(b: &mut Builder, have: Operand, want: Operand) { b.terminate(Term::Branch(Operand::Value(ne), fail, ok)); b.cur = fail; let t = b.fresh_value(Ty::Unit); - b.push(Stmt::Let(t, Rvalue::Call("len_mismatch_fail".to_string(), vec![have, want]))); + b.push(Stmt::Let( + t, + Rvalue::Call(DirectCall::Runtime(RuntimeKey::LenMismatchFail), vec![have, want]), + )); b.terminate(Term::Unreachable); b.cur = ok; } @@ -9689,7 +9793,12 @@ fn lower_array_map_into(b: &mut Builder, source: &hir::Expr, stages: &[hir::Stag } }; let call_args = stage_call_args(arg, &prepared_stages[stage_idx].captures); - cur = Some(emit_named_call(b, func.clone(), call_args, stage.out_ty)); + cur = Some(emit_named_call( + b, + ProgramCall::from_validated(func), + call_args, + stage.out_ty, + )); } hir::StageKind::Where { .. } | hir::StageKind::WhereField { .. } | hir::StageKind::WhereStrContains { .. } => { unreachable!("map_into rejects filtering `where` stages in sema") @@ -10248,7 +10357,12 @@ fn lower_array_partition( } }; let call_args = stage_call_args(arg, &prepared_stages[stage_idx].captures); - cur = Some(emit_named_call(b, func.clone(), call_args, stage.out_ty)); + cur = Some(emit_named_call( + b, + ProgramCall::from_validated(func), + call_args, + stage.out_ty, + )); } hir::StageKind::Where { func, .. } => { let arg = match &cur { @@ -10263,7 +10377,13 @@ fn lower_array_partition( }; let call_args = stage_call_args(arg, &prepared_stages[stage_idx].captures); let pred = b.fresh_value(Ty::Bool); - b.push(Stmt::Let(pred, Rvalue::Call(func.clone(), call_args))); + b.push(Stmt::Let( + pred, + Rvalue::Call( + DirectCall::Program(ProgramCall::from_validated(func)), + call_args, + ), + )); let keep = b.new_block(); b.terminate(Term::Branch(Operand::Value(pred), keep, cont)); b.cur = keep; @@ -10291,7 +10411,13 @@ fn lower_array_partition( let cur = cur.expect("partition needs a scalar element"); let pred = b.fresh_value(Ty::Bool); let pred_args = stage_call_args(cur.clone(), &prepared_pred_captures); - b.push(Stmt::Let(pred, Rvalue::Call(pred_func.to_string(), pred_args))); + b.push(Stmt::Let( + pred, + Rvalue::Call( + DirectCall::Program(ProgramCall::from_validated(pred_func)), + pred_args, + ), + )); let to_a = b.new_block(); let to_b = b.new_block(); b.terminate(Term::Branch(Operand::Value(pred), to_a, to_b)); @@ -10341,7 +10467,7 @@ fn lower_array_partition( /// compares `key(a)` against `key(b)` instead of `a` against `b` — see [`lower_array_sort`], where /// the keys are precomputed once (decorate) rather than recomputed per comparison. struct SortKey { - func: String, + func: ProgramCall, captures: Vec, key_ty: Ty, } @@ -10577,7 +10703,10 @@ fn lower_array_sort(b: &mut Builder, source: &hir::Expr, stages: &[hir::Stage], let mut args = Vec::with_capacity(1 + lowered_captures.len()); args.push(v); args.extend(lowered_captures.iter().cloned()); - b.push(Stmt::Let(kc, Rvalue::Call(sk.func.clone(), args))); + b.push(Stmt::Let( + kc, + Rvalue::Call(DirectCall::Program(sk.func.clone()), args), + )); Operand::Value(kc) } None => v, @@ -14126,6 +14255,13 @@ mod tests { use align_parser::parse_file; use align_sema::check_file; + fn direct_program_name(call: &DirectCall) -> Option<&str> { + match call { + DirectCall::Program(target) => Some(target.as_str()), + DirectCall::Runtime(_) => None, + } + } + fn lower(src: &str) -> Program { let mut d = Diagnostics::new(); let toks = tokenize(0, src, &mut d); @@ -14298,13 +14434,13 @@ fn main() -> i32 = 0 let function = program .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("{name} function")); let emitted_forbidden_action = |statement: &Stmt| match name { "unary" => matches!(statement, Stmt::Let(_, Rvalue::Un(..))), "binary" | "selected" => matches!(statement, Stmt::Let(_, Rvalue::Bin(..))), "arguments" => { - matches!(statement, Stmt::Let(_, Rvalue::Call(callee, _)) if callee == "call") + matches!(statement, Stmt::Let(_, Rvalue::Call(callee, _)) if direct_program_name(callee) == Some("call")) } "fixed_index" => matches!(statement, Stmt::Let(_, Rvalue::Index(..))), "dynamic_index" => matches!(statement, Stmt::Let(_, Rvalue::SliceIndex(..))), @@ -14334,7 +14470,7 @@ fn main() -> i32 = 0 .all(|statement| { !matches!( statement, - Stmt::Let(_, Rvalue::Call(callee, _)) if callee == "later" + Stmt::Let(_, Rvalue::Call(callee, _)) if direct_program_name(callee) == Some("later") ) && !emitted_forbidden_action(statement) }), "{name} emitted a later sibling or parent action: {function:#?}" @@ -14375,7 +14511,7 @@ fn main() -> i32 = 0 let function = hir .fns .iter_mut() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("{name} function")); let field_expr = match function.body.value.as_mut() { Some(value) => value, @@ -14397,7 +14533,7 @@ fn main() -> i32 = 0 let apply = hir .fns .iter_mut() - .find(|function| function.name == "apply") + .find(|function| function.name.as_str() == "apply") .expect("apply function"); let call = apply.body.value.as_mut().expect("apply expression body"); let hir::ExprKind::CallFnValue { callee, .. } = &mut call.kind else { @@ -14412,7 +14548,7 @@ fn main() -> i32 = 0 let function = program .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("lowered {name}")); assert!( function @@ -14443,7 +14579,7 @@ fn main() -> i32 = 0 let apply = program .fns .iter() - .find(|function| function.name == "apply") + .find(|function| function.name.as_str() == "apply") .expect("lowered apply"); assert!( apply @@ -14486,7 +14622,7 @@ fn main() -> i32 { let main = program .fns .iter() - .find(|function| function.name == "main") + .find(|function| function.name.as_str() == "main") .expect("main function"); let mut captures = Vec::new(); for block in &main.blocks { @@ -14494,13 +14630,16 @@ fn main() -> i32 { let Stmt::Let(_, Rvalue::Call(name, args)) = statement else { continue; }; + let Some(name) = direct_program_name(name) else { + continue; + }; if !name.starts_with("main$lambda") { continue; } let Operand::Value(capture) = args.last().expect("lifted lambda capture") else { panic!("lifted capture must be a preheader SSA value: {statement:?}"); }; - captures.push((name.as_str(), *capture, block.id)); + captures.push((name, *capture, block.id)); } } captures.sort_by_key(|(name, _, _)| *name); @@ -14550,7 +14689,7 @@ fn main() -> i32 { let main = program .fns .iter() - .find(|function| function.name == "main") + .find(|function| function.name.as_str() == "main") .expect("main function"); assert!( main.blocks.iter().all(|block| { @@ -14585,7 +14724,7 @@ fn main() -> i32 { let owned_main = owned_program .fns .iter() - .find(|function| function.name == "main") + .find(|function| function.name.as_str() == "main") .expect("owned-source main"); assert!( owned_main @@ -14631,7 +14770,7 @@ fn main() -> i32 = 0 let function = program .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("{name} function")); let (capture, action_block) = function .blocks @@ -14641,7 +14780,9 @@ fn main() -> i32 = 0 let Stmt::Let(_, Rvalue::Call(callee, args)) = statement else { return None; }; - if !callee.starts_with(&format!("{name}$lambda")) { + if !direct_program_name(callee) + .is_some_and(|callee| callee.starts_with(&format!("{name}$lambda"))) + { return None; } let Operand::Value(capture) = args.last()? else { @@ -14671,7 +14812,7 @@ fn main() -> i32 = 0 let selected = program .fns .iter() - .find(|function| function.name == "selected") + .find(|function| function.name.as_str() == "selected") .expect("selected function"); assert!( selected.blocks.iter().filter(|block| block.id != selected.entry).all(|block| { @@ -14716,7 +14857,7 @@ fn bad() -> i32 { let bad = program .fns .iter() - .find(|function| function.name == "bad") + .find(|function| function.name.as_str() == "bad") .expect("bad function"); let entry = bad .blocks @@ -14742,7 +14883,7 @@ fn bad() -> i32 { let malformed_bad = hir .fns .iter_mut() - .find(|function| function.name == "bad") + .find(|function| function.name.as_str() == "bad") .expect("malformed bad function"); let hir::Stmt::Break { accepted, .. } = &mut malformed_bad.body.stmts[0] else { panic!("bad first statement must remain a checked break"); @@ -14752,7 +14893,7 @@ fn bad() -> i32 { let malformed_bad = malformed .fns .iter() - .find(|function| function.name == "bad") + .find(|function| function.name.as_str() == "bad") .expect("lowered malformed bad function"); assert_eq!( malformed_bad.blocks.len(), @@ -14829,7 +14970,7 @@ fn main() -> i32 = 0 let function = program .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("{name} function")); assert!( function.blocks.iter().flat_map(|block| &block.stmts).all( @@ -14844,7 +14985,7 @@ fn main() -> i32 = 0 let nested = program .fns .iter() - .find(|function| function.name == "nested_break") + .find(|function| function.name.as_str() == "nested_break") .expect("nested_break function"); let str_stores = nested .blocks @@ -14902,7 +15043,7 @@ fn main() -> i32 = 0 let mixed = program .fns .iter() - .find(|function| function.name == "mixed_if") + .find(|function| function.name.as_str() == "mixed_if") .expect("mixed_if function"); let mut str_stores = mixed .slots @@ -14939,7 +15080,7 @@ fn main() -> i32 = 0 let function = program .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("{name} function")); assert!( function @@ -14965,7 +15106,7 @@ fn main() -> i32 = 0 program .fns .iter() - .find(|f| f.name == "output") + .find(|f| f.name.as_str() == "output") .expect("output function") .ret }; @@ -14981,7 +15122,7 @@ fn main() -> i32 = 0 }); let mut program = Program { fns: vec![Function { - name: "main".to_string(), + name: ProgramCall::from_validated("main"), params: vec![], param_modes: vec![], return_borrow: hir::ReturnBorrowSummary::None, @@ -14997,7 +15138,7 @@ fn main() -> i32 = 0 stmts: vec![Stmt::Let( 0, Rvalue::Closure { - lifted: "unused".to_string(), + lifted: ProgramCall::from_validated("unused"), captures: vec![], capture_tys: vec![Ty::Tagged(1)], signature: FnSignatureFacts { @@ -15086,7 +15227,7 @@ fn main() -> i32 = 0 let function = program .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("missing {name}")); assert!( function @@ -15101,7 +15242,7 @@ fn main() -> i32 = 0 let function = program .fns .iter() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("missing {name}")); assert!( function @@ -15136,10 +15277,10 @@ fn main() -> i32 = 0 let p = lower( "fn u() {}\nfn take(x: ()) {}\nfn tail() { return u() }\nfn main() -> i32 {\n a := u()\n f := u\n b := f()\n empty := {}\n looped := loop { break u() }\n take(u())\n take(empty)\n take(looped)\n tail()\n return 0\n}\n", ); - let main = p.fns.iter().find(|f| f.name == "main").expect("main MIR"); + let main = p.fns.iter().find(|f| f.name.as_str() == "main").expect("main MIR"); let stmts: Vec<&Stmt> = main.blocks.iter().flat_map(|b| &b.stmts).collect(); assert!( - stmts.iter().any(|s| matches!(s, Stmt::Let(_, Rvalue::Call(name, _)) if name == "u")), + stmts.iter().any(|s| matches!(s, Stmt::Let(_, Rvalue::Call(name, _)) if direct_program_name(name) == Some("u"))), "the direct Unit call must remain for its effects:\n{}", print::function_to_string(main) ); @@ -15161,12 +15302,12 @@ fn main() -> i32 = 0 } assert!( stmts.iter().any( - |s| matches!(s, Stmt::Let(_, Rvalue::Call(name, args)) if name == "take" && matches!(args.as_slice(), [Operand::Const(Const::Unit)])) + |s| matches!(s, Stmt::Let(_, Rvalue::Call(name, args)) if direct_program_name(name) == Some("take") && matches!(args.as_slice(), [Operand::Const(Const::Unit)])) ), "a Unit call argument must be Const::Unit:\n{}", print::function_to_string(main) ); - let tail = p.fns.iter().find(|f| f.name == "tail").expect("tail MIR"); + let tail = p.fns.iter().find(|f| f.name.as_str() == "tail").expect("tail MIR"); assert!( tail.blocks.iter().any(|b| matches!(b.term, Term::Return(None))), "an explicit Unit return must keep the void ABI:\n{}", @@ -15179,7 +15320,7 @@ fn main() -> i32 = 0 let p = lower( "import std.process\nfn quit() {\n s := \"x\".clone()\n return process.abort()\n}\nfn main() -> i32 = 0\n", ); - let quit = p.fns.iter().find(|f| f.name == "quit").expect("quit MIR"); + let quit = p.fns.iter().find(|f| f.name.as_str() == "quit").expect("quit MIR"); assert!( quit.blocks.iter().any(|b| matches!(b.term, Term::Unreachable)), "process.abort must retain its diverging terminator:\n{}", @@ -15204,7 +15345,7 @@ fn main() -> i32 = 0 "partial_tuple", "partial_struct", ] { - let f = p.fns.iter().find(|f| f.name == name).expect("call test MIR"); + let f = p.fns.iter().find(|f| f.name.as_str() == name).expect("call test MIR"); assert!( f.blocks .iter() @@ -15215,7 +15356,7 @@ fn main() -> i32 = 0 ); assert!( f.blocks.iter().flat_map(|b| &b.stmts).all(|s| { - !matches!(s, Stmt::Let(_, Rvalue::Call(callee, _)) if matches!(callee.as_str(), "take" | "take_tuple" | "take_pair")) + !matches!(s, Stmt::Let(_, Rvalue::Call(callee, _)) if matches!(direct_program_name(callee), Some("take" | "take_tuple" | "take_pair"))) && !matches!(s, Stmt::Let(_, Rvalue::CallIndirect { .. })) }), "{name} must not emit the outer call after the later argument returns:\n{}", @@ -15230,7 +15371,7 @@ fn main() -> i32 = 0 "fn make() -> string {\n return task_group { s := \"hello\".clone(); s }\n}\nfn take(s: string) -> i64 = s.len()\nfn call() -> i64 = take(task_group { s := \"world\".clone(); s })\nfn main() -> i32 = 0\n", ); for name in ["make", "call"] { - let f = p.fns.iter().find(|f| f.name == name).expect("task_group move MIR"); + let f = p.fns.iter().find(|f| f.name.as_str() == name).expect("task_group move MIR"); let cleared_live_flag = f.slots.iter().enumerate().any(|(slot, ty)| { *ty == Ty::Bool && f.blocks @@ -15277,7 +15418,7 @@ fn main() -> i32 = 0 }\n\ fn main() -> i32 = mixed() as i32\n", ); - let f = p.fns.iter().find(|f| f.name == "mixed").expect("mixed match MIR"); + let f = p.fns.iter().find(|f| f.name.as_str() == "mixed").expect("mixed match MIR"); assert!( f.blocks.iter().flat_map(|block| &block.stmts).any( |stmt| matches!( @@ -15300,7 +15441,7 @@ fn main() -> i32 = 0 }\n\ fn main() -> i32 = wildcard() as i32\n", ); - let f = p.fns.iter().find(|f| f.name == "wildcard").expect("wildcard match MIR"); + let f = p.fns.iter().find(|f| f.name.as_str() == "wildcard").expect("wildcard match MIR"); assert!( f.blocks.iter().flat_map(|block| &block.stmts).any( |stmt| matches!( @@ -15322,7 +15463,7 @@ fn main() -> i32 = 0 ("partial", "direct struct let"), ("partial_array", "fixed Move-struct array let"), ] { - let partial = p.fns.iter().find(|f| f.name == name).expect("partial aggregate MIR"); + let partial = p.fns.iter().find(|f| f.name.as_str() == name).expect("partial aggregate MIR"); assert!( partial .blocks @@ -15334,7 +15475,7 @@ fn main() -> i32 = 0 ); } - let joined = p.fns.iter().find(|f| f.name == "joined").expect("joined MIR"); + let joined = p.fns.iter().find(|f| f.name.as_str() == "joined").expect("joined MIR"); let forwards_runtime_flag = joined.blocks.iter().any(|block| { let stores_struct = block.stmts.iter().any( |stmt| matches!(stmt, Stmt::Store(slot, Operand::Value(_)) if matches!(joined.slots[*slot as usize], Ty::Struct(_))), @@ -15356,7 +15497,7 @@ fn main() -> i32 = 0 let p = lower( "Wrap { xs: array }\nfn main() -> i32 {\n return [Wrap { xs: [1, 2].to_array() }].count() as i32\n}\n", ); - let main = p.fns.iter().find(|f| f.name == "main").expect("main MIR"); + let main = p.fns.iter().find(|f| f.name.as_str() == "main").expect("main MIR"); assert!( main.blocks.iter().flat_map(|block| &block.stmts).any( |stmt| matches!( @@ -15377,7 +15518,7 @@ fn main() -> i32 = 0 ); let assert_no_post_clear_reload = |name: &str, marker: &dyn Fn(&Stmt) -> bool| { - let f = p.fns.iter().find(|f| f.name == name).expect("ownership test MIR"); + let f = p.fns.iter().find(|f| f.name.as_str() == name).expect("ownership test MIR"); let block = f .blocks .iter() @@ -15445,7 +15586,7 @@ fn main() -> i32 = 0 matches!(stmt, Stmt::Let(_, Rvalue::MakeTuple { .. })) }); assert_no_post_clear_reload("structured", &|stmt| { - matches!(stmt, Stmt::Let(value, Rvalue::Load(_)) if p.fns.iter().find(|f| f.name == "structured").is_some_and(|f| matches!(f.value_tys[*value as usize], Ty::Struct(_)))) + matches!(stmt, Stmt::Let(value, Rvalue::Load(_)) if p.fns.iter().find(|f| f.name.as_str() == "structured").is_some_and(|f| matches!(f.value_tys[*value as usize], Ty::Struct(_)))) }); assert_no_post_clear_reload("enumed", &|stmt| { matches!(stmt, Stmt::Let(_, Rvalue::MakeEnum { .. })) @@ -15454,7 +15595,7 @@ fn main() -> i32 = 0 matches!(stmt, Stmt::Let(_, Rvalue::ResultIsOk(_))) }); - let arena_exit = p.fns.iter().find(|f| f.name == "arena_exit").expect("arena exit MIR"); + let arena_exit = p.fns.iter().find(|f| f.name.as_str() == "arena_exit").expect("arena exit MIR"); assert!( arena_exit.blocks.iter().any(|block| { block.stmts.windows(2).any(|pair| { @@ -15479,7 +15620,7 @@ fn main() -> i32 = 0 let p = lower( "E { Bad }\nfn make() -> array = [1].to_array()\nfn load() -> Result, E> = Ok(make())\nfn convert(e: E) -> Error = Error.Code(1)\nfn early(c: bool) -> i32 {\n mapped := load().map_err({\n if c { return 0 }\n convert\n })\n return 1\n}\nfn run(c: bool) -> Result {\n arena {\n mut r: Result, E> := Ok(make())\n if c {\n r = Ok([2].to_array())\n }\n mapped := r.map_err(convert)\n xs := mapped?\n return Ok(xs.sum())\n }\n}\nfn main() -> i32 = 0\n", ); - let early = p.fns.iter().find(|f| f.name == "early").expect("early MIR"); + let early = p.fns.iter().find(|f| f.name.as_str() == "early").expect("early MIR"); assert!( early .blocks @@ -15490,7 +15631,7 @@ fn main() -> i32 = 0 print::function_to_string(early) ); - let run = p.fns.iter().find(|f| f.name == "run").expect("run MIR"); + let run = p.fns.iter().find(|f| f.name.as_str() == "run").expect("run MIR"); let stores_dynamic_result_flag = run.blocks.iter().any(|block| { block.stmts.windows(2).any(|pair| { matches!( @@ -15525,8 +15666,8 @@ fn main() -> i32 = 0 vec![Ty::DynArray(scalar_of(i64_ty())), Ty::String], ), ] { - let dynamic = p.fns.iter().find(|f| f.name == name).expect("dynamic aggregate MIR"); - let take = p.fns.iter().find(|f| f.name == callee).expect("aggregate consumer MIR"); + let dynamic = p.fns.iter().find(|f| f.name.as_str() == name).expect("dynamic aggregate MIR"); + let take = p.fns.iter().find(|f| f.name.as_str() == callee).expect("aggregate consumer MIR"); let aggregate_ty = take.slots[take.params[0] as usize]; let has_aggregate_owner = dynamic.blocks.iter().any(|block| { block.stmts.windows(2).any(|pair| { @@ -15562,7 +15703,7 @@ fn main() -> i32 = 0 .blocks .iter() .flat_map(|block| &block.stmts) - .all(|stmt| !matches!(stmt, Stmt::Let(_, Rvalue::Call(called, _)) if called == callee)), + .all(|stmt| !matches!(stmt, Stmt::Let(_, Rvalue::Call(called, _)) if direct_program_name(called) == Some(callee))), "{name}'s outer call must not be emitted after its later argument returns:\n{}", print::function_to_string(dynamic) ); @@ -15574,10 +15715,10 @@ fn main() -> i32 = 0 let p = lower( "fn use(s: str) {}\nfn f() {\n use(\"first\".clone())\n use(\"second\".clone())\n}\nfn main() -> i32 = 0\n", ); - let f = p.fns.iter().find(|f| f.name == "f").expect("borrowed Unit call MIR"); + let f = p.fns.iter().find(|f| f.name.as_str() == "f").expect("borrowed Unit call MIR"); let rendered = print::function_to_string(f); - let first_call = rendered.find("call use").expect("first use call"); - let second_call = rendered.rfind("call use").expect("second use call"); + let first_call = rendered.find("call program use").expect("first use call"); + let second_call = rendered.rfind("call program use").expect("second use call"); let first_drop = rendered[first_call..].find("drop ").map(|offset| first_call + offset).expect("first temporary drop"); assert!( first_call < first_drop && first_drop < second_call, @@ -15590,7 +15731,7 @@ fn main() -> i32 = 0 let p = lower( "fn take() -> string {\n s := \"x\".clone()\n return s\n}\nfn main() -> i32 = take().len() as i32\n", ); - let take = p.fns.iter().find(|f| f.name == "take").expect("take MIR"); + let take = p.fns.iter().find(|f| f.name.as_str() == "take").expect("take MIR"); assert!( take.blocks.iter().flat_map(|b| &b.stmts).all(|stmt| !matches!(stmt, Stmt::Drop(_))), "a definitely moved local must not retain a destructor edge:\n{}", @@ -15603,7 +15744,7 @@ fn main() -> i32 = 0 let p = lower( "fn keep() -> i64 {\n s := \"x\".clone()\n return s.len()\n}\nfn main() -> i32 = keep() as i32\n", ); - let keep = p.fns.iter().find(|f| f.name == "keep").expect("keep MIR"); + let keep = p.fns.iter().find(|f| f.name.as_str() == "keep").expect("keep MIR"); assert_eq!( keep.blocks .iter() diff --git a/crates/align_mir/src/print.rs b/crates/align_mir/src/print.rs index 8dd0202a..b16f2280 100644 --- a/crates/align_mir/src/print.rs +++ b/crates/align_mir/src/print.rs @@ -215,12 +215,19 @@ fn rvalue_str(rv: &Rvalue) -> String { let a: Vec = operands.iter().map(operand_str).collect(); format!("{f}({}) : {}", a.join(", "), ty_name(*ty)) } - Rvalue::Call(name, args) => { + Rvalue::Call(target, args) => { let a: Vec = args.iter().map(operand_str).collect(); - format!("call {name}({})", a.join(", ")) + match target { + crate::DirectCall::Program(name) => { + format!("call program {name}({})", a.join(", ")) + } + crate::DirectCall::Runtime(key) => { + format!("call runtime {}({})", key.logical_name(), a.join(", ")) + } + } } - Rvalue::FnAddr { name, signature } => { - format!("fn_addr {name} signature={signature:?}") + Rvalue::FnAddr { target, signature } => { + format!("fn_addr {target} signature={signature:?}") } Rvalue::Closure { lifted, @@ -397,13 +404,24 @@ fn rvalue_str(rv: &Rvalue) -> String { .collect::>() .join(", "); let chain = if stages.is_empty() { - func.clone() + func.as_str().to_owned() } else { let prefix = stages .iter() .map(|stage| match stage.kind { - ParMapStageKind::Map => stage.func.clone().unwrap_or_else(|| "".to_string()), - ParMapStageKind::Filter => format!("where {}", stage.func.as_deref().unwrap_or("")), + ParMapStageKind::Map => stage + .func + .as_ref() + .map(|target| target.as_str().to_owned()) + .unwrap_or_else(|| "".to_string()), + ParMapStageKind::Filter => format!( + "where {}", + stage + .func + .as_ref() + .map(|target| target.as_str()) + .unwrap_or("") + ), ParMapStageKind::FilterStrContains => "where str.contains".to_string(), ParMapStageKind::Project { field } => format!("field#{field}"), ParMapStageKind::FilterField { field } => format!("where field#{field}"), diff --git a/crates/align_mir/src/validate_hir_tests.rs b/crates/align_mir/src/validate_hir_tests.rs index 027d69d9..a8d57b10 100644 --- a/crates/align_mir/src/validate_hir_tests.rs +++ b/crates/align_mir/src/validate_hir_tests.rs @@ -12,6 +12,13 @@ use align_lexer::tokenize; use align_parser::parse_file; use std::cell::Cell; +fn direct_program_name(call: &DirectCall) -> Option<&str> { + match call { + DirectCall::Program(target) => Some(target.as_str()), + DirectCall::Runtime(_) => None, + } +} + fn declaration_header_program() -> hir::Program { let mut program = baseline_program(); let slice_i32 = Ty::Slice(scalar_int(32)); @@ -532,6 +539,52 @@ fn malformed_hir_declaration_header_metadata_fails_closed() { }); } +#[test] +fn malformed_hir_callable_namespace_fails_closed() { + fn assert_unpublished(label: &str, program: &hir::Program) { + let source_map = SourceMap::new(); + for lowered in [ + lower_program(program), + lower_program_located(program, &source_map), + lower_program_per_unit(program), + lower_program_per_unit_located(program, &source_map), + ] { + assert!(is_empty(&lowered), "{label}: malformed callable namespace published MIR"); + } + } + + let mut stored = checked_source_program( + "fn helper(value: i64) -> i64 = value\n\ + fn main() -> i32 {\n unused := helper(1)\n return 0\n}\n", + ); + stored.fns[0].name.push('\0'); + assert_unpublished("stored-name-nul", &stored); + + let mut direct = checked_source_program( + "fn helper(value: i64) -> i64 = value\n\ + fn main() -> i32 {\n unused := helper(1)\n return 0\n}\n", + ); + let call = direct.fns[1] + .body + .stmts + .iter_mut() + .find_map(|statement| match statement { + hir::Stmt::Let { init, .. } => match &mut init.kind { + hir::ExprKind::Call { func, .. } => Some(func), + _ => None, + }, + _ => None, + }) + .expect("fixture contains a direct call"); + call.clear(); + assert_unpublished("empty-direct-target", &direct); + + let mut declarations = declaration_header_program(); + declarations.imported_fns[0].name.push('\0'); + declarations.externs[0].name.clear(); + assert_unpublished("import-before-extern-name", &declarations); +} + #[test] fn main_header_abi_matrix_is_exhaustive() { let result = Ty::Result(Scalar::Unit, Scalar::Enum(1)); @@ -664,7 +717,7 @@ fn valid_hir_declaration_header_preflight_is_mir_identity() { ); assert_eq!(format!("{located:#?}"), format!("{located_unchecked:#?}")); assert_eq!(checked.imported_fns.len(), 1); - assert_eq!(checked.imported_fns[0].name, "dep$read"); + assert_eq!(checked.imported_fns[0].name.as_str(), "dep$read"); } let mut lifted = declaration_header_program(); @@ -1207,7 +1260,7 @@ fn malformed_hir_visible_local_name_collisions_fail_closed() { let hidden_function = program .fns .iter() - .find(|function| function.name == "hidden_tuple_discards") + .find(|function| function.name.as_str() == "hidden_tuple_discards") .expect("hidden tuple fixture"); let hidden_ids = hidden_function .locals @@ -1237,7 +1290,7 @@ fn malformed_hir_visible_local_name_collisions_fail_closed() { let function = lowered .fns .iter() - .find(|function| function.name == "hidden_tuple_discards") + .find(|function| function.name.as_str() == "hidden_tuple_discards") .expect("hidden tuple MIR function"); for &hidden in &hidden_ids { assert_eq!( @@ -1295,7 +1348,7 @@ fn malformed_hir_visible_local_name_collisions_fail_closed() { accepted .fns .iter_mut() - .find(|function| function.name == function_name) + .find(|function| function.name.as_str() == function_name) .expect("visible-name acceptance function") .locals .iter_mut() @@ -1331,7 +1384,7 @@ fn malformed_hir_visible_local_name_collisions_fail_closed() { let function = malformed .fns .iter_mut() - .find(|function| function.name == "hidden_tuple_discards") + .find(|function| function.name.as_str() == "hidden_tuple_discards") .expect("hidden tuple fixture"); for &hidden in &hidden_ids { function.locals[hidden as usize].name = spelling.to_string(); @@ -1351,7 +1404,7 @@ fn malformed_hir_visible_local_name_collisions_fail_closed() { let function = malformed .fns .iter_mut() - .find(|function| function.name == function_name) + .find(|function| function.name.as_str() == function_name) .expect("visible-name matrix function"); for original in [first, second] { function @@ -1419,7 +1472,7 @@ fn malformed_hir_visible_local_name_collisions_fail_closed() { let function = program .fns .iter_mut() - .find(|function| function.name == "hidden_scope") + .find(|function| function.name.as_str() == "hidden_scope") .expect("hidden scope fixture"); let hir::Stmt::Let { init, .. } = &mut function.body.stmts[0] else { panic!("hidden scope outer binding") @@ -1449,7 +1502,7 @@ fn malformed_hir_visible_local_name_collisions_fail_closed() { let function = after_scope .fns .iter() - .find(|function| function.name == "hidden_scope") + .find(|function| function.name.as_str() == "hidden_scope") .expect("hidden scope fixture"); let hir::Stmt::Let { init, .. } = &function.body.stmts[0] else { panic!("hidden scope outer binding") @@ -1465,7 +1518,7 @@ fn malformed_hir_visible_local_name_collisions_fail_closed() { after_scope .fns .iter_mut() - .find(|function| function.name == "hidden_scope") + .find(|function| function.name.as_str() == "hidden_scope") .expect("hidden scope fixture") .body .stmts @@ -1716,7 +1769,7 @@ fn checked_hir_body_fact_replay_preserves_imported_fact_presence() { let unknown_consumer = unknown .fns .iter() - .find(|function| function.name == "consume") + .find(|function| function.name.as_str() == "consume") .expect("consumer function"); assert_eq!(unknown_consumer.return_borrow, roots); assert_eq!(unknown_consumer.return_region, regions); @@ -1734,7 +1787,7 @@ fn checked_hir_body_fact_replay_preserves_imported_fact_presence() { let consumer = program .fns .iter() - .find(|function| function.name == "consume") + .find(|function| function.name.as_str() == "consume") .expect("consumer function"); if matches!(return_borrow, ReturnBorrowSummary::None) { assert_eq!(consumer.return_borrow, ReturnBorrowSummary::None); @@ -1782,7 +1835,7 @@ fn main() -> i32 { let main_index = malformed_fn_id .fns .iter() - .position(|function| function.name == "main") + .position(|function| function.name.as_str() == "main") .expect("main function"); let local = malformed_fn_id.fns[main_index] .locals @@ -1798,7 +1851,7 @@ fn main() -> i32 { let replace_index = base .fns .iter() - .position(|function| function.name == "replace") + .position(|function| function.name.as_str() == "replace") .expect("replace function"); let assignment_index = base.fns[replace_index] .body @@ -1839,7 +1892,7 @@ fn main() -> i32 { let main = base .fns .iter() - .find(|function| function.name == "main") + .find(|function| function.name.as_str() == "main") .expect("main function"); let function_value_ids: Vec = main .locals @@ -2507,7 +2560,7 @@ fn assert_mir_owner(label: &str, program: &Program, owner: MirOwner, evidence: H values .iter() .copied() - .any(|rv| matches!(rv, Rvalue::Call(name, _) if name == expected)) + .any(|rv| matches!(rv, Rvalue::Call(name, _) if direct_program_name(name) == Some(expected))) }; let owned = match owner { MirOwner::Unary => has(|rv| matches!(rv, Rvalue::Un(..))), @@ -2596,7 +2649,9 @@ fn assert_mir_owner(label: &str, program: &Program, owner: MirOwner, evidence: H .iter() .any(|block| matches!(block.term, Term::Goto(_))) }), - MirOwner::Stage => has(|rv| matches!(rv, Rvalue::Call(name, _) if name == "dep$stage_id")), + MirOwner::Stage => has(|rv| { + matches!(rv, Rvalue::Call(name, _) if direct_program_name(name) == Some("dep$stage_id")) + }), // Transparent blocks and expression statements emit no instruction of their own, so their // fixture ends in a producer-valid imported sentinel. Reaching that call proves the whole // structural spine was traversed rather than merely publishing an empty function. @@ -5194,7 +5249,7 @@ fn body_value_expression_mut<'a>( program .fns .iter_mut() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("missing value fixture {name}")) .body .value @@ -5206,7 +5261,7 @@ fn body_first_statement_mut<'a>(program: &'a mut hir::Program, name: &str) -> &' program .fns .iter_mut() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("missing statement fixture {name}")) .body .stmts @@ -5218,7 +5273,7 @@ fn body_loop_statement_mut<'a>(program: &'a mut hir::Program, name: &str) -> &'a let expression = program .fns .iter_mut() - .find(|function| function.name == name) + .find(|function| function.name.as_str() == name) .unwrap_or_else(|| panic!("missing loop statement fixture {name}")) .body .value @@ -6757,7 +6812,7 @@ fn hir_body_validator_expression_inventory() { let case = malformed .fns .iter_mut() - .find(|function| function.name == "str_predicate_case") + .find(|function| function.name.as_str() == "str_predicate_case") .expect("inventory case"); let hir::Stmt::Expr(expression) = &mut case.body.stmts[0] else { panic!("inventory case lost its expression"); @@ -7044,7 +7099,7 @@ fn hir_body_validator_storage_vector_array() { reject .fns .iter_mut() - .find(|function| function.name == "vec_store_case") + .find(|function| function.name.as_str() == "vec_store_case") .expect("vec-store fixture") .locals[0] .is_mut = false; @@ -7063,7 +7118,7 @@ fn hir_body_validator_storage_vector_array() { reject .fns .iter_mut() - .find(|function| function.name == "body_test") + .find(|function| function.name.as_str() == "body_test") .expect("pooled fixture") .locals[0] .is_mut = true; @@ -7636,7 +7691,7 @@ fn hir_body_validator_pipeline_terminals() { reject .fns .iter_mut() - .find(|function| function.name == "pipeline_map_into") + .find(|function| function.name.as_str() == "pipeline_map_into") .expect("map-into fixture") .locals[1] .is_mut = false; @@ -7690,7 +7745,7 @@ fn hir_body_validator_pipeline_terminals() { impure .imported_fns .iter_mut() - .find(|function| function.name == "dep$terminal_map") + .find(|function| function.name.as_str() == "dep$terminal_map") .expect("par-map callable") .effect = FnEffect::Impure; assert!(body_core_metadata_is_valid(&impure)); @@ -10616,7 +10671,7 @@ fn hir_body_validator_native() { let native = deferred .fns .iter_mut() - .find(|function| function.name == "native_buffer_new") + .find(|function| function.name.as_str() == "native_buffer_new") .expect("native buffer fixture"); native.drop_locals = vec![u32::MAX]; native.drop_individual_locals = vec![u32::MAX]; @@ -10904,7 +10959,7 @@ fn hir_body_validator_generated_callables() { let target = reject .fns .iter_mut() - .find(|function| function.name == "generated_source") + .find(|function| function.name.as_str() == "generated_source") .expect("generated source target"); target.origin = hir::FnOrigin::Monomorph; assert!(!body_core_metadata_is_valid(&reject)); From 781d352871579149531218bd93b1c4d316867ff4 Mon Sep 17 00:00:00 2001 From: sanohiro Date: Wed, 5 Aug 2026 14:28:49 +0900 Subject: [PATCH 7/7] fix(mir): close callable review findings Finding-to-fix ledger: - migrate every remaining backend symbol-shape owner to encoded program and canonical generated identities - reject inline by-value SCCs while preserving header and function-type cycles - report disallowed function ABI modes before later malformed fields - quantize fused parallel work metadata to the canonical 1/2/4 domain - preserve Copy fixed-array captures and current arena Drop ownership checks - keep native byte-pointer conversions lint-clean on signed and unsigned c_char targets --- crates/align_codegen_llvm/src/lib.rs | 14 +- crates/align_codegen_llvm/src/thinlto.rs | 2 +- crates/align_driver/tests/link_hygiene.rs | 101 ++++-- crates/align_driver/tests/main_abi.rs | 20 +- crates/align_driver/tests/par_map.rs | 138 +++++--- crates/align_driver/tests/per_unit_surface.rs | 16 +- .../align_driver/tests/unit_main_exit_code.rs | 6 +- crates/align_mir/src/canonical_graph.rs | 299 +++++++++++++++--- crates/align_mir/src/lib.rs | 6 +- crates/align_mir/src/validate_hir.rs | 47 ++- crates/align_runtime/src/lib.rs | 28 +- 11 files changed, 522 insertions(+), 155 deletions(-) diff --git a/crates/align_codegen_llvm/src/lib.rs b/crates/align_codegen_llvm/src/lib.rs index f3c7ae45..4f75d8bc 100644 --- a/crates/align_codegen_llvm/src/lib.rs +++ b/crates/align_codegen_llvm/src/lib.rs @@ -1138,8 +1138,8 @@ fn build_module<'c>( } // Pass 1b: emit a thunk for each function used as a value (`FnValue`/`FnAddr`). A closure - // value has the env-ABI `fn(env, args)`; a non-capturing / named function is wrapped by - // `name$fnval(env, args) = name(args)` so all closure callees share that ABI (the env pointer + // value has the env-ABI `fn(env, args)`; a non-capturing / named function is wrapped by a + // canonical function-value helper so all closure callees share that ABI (the env pointer // is null and ignored). Capturing closures (a later slice) instead point at an env-reading fn. let mut thunk_names: std::collections::BTreeSet = std::collections::BTreeSet::new(); @@ -1193,7 +1193,7 @@ fn build_module<'c>( } // Pass 1c: a closure thunk per lifted function used as a *capturing* closure. The env-ABI - // thunk `lifted$clos(env, explicit…)` loads the captured values out of `env` and forwards them + // helper loads the captured values out of `env` and forwards them // as the lifted function's trailing capture parameters: `lifted(explicit…, env.0, env.1, …)`. let mut closure_thunks: std::collections::BTreeMap> = std::collections::BTreeMap::new(); @@ -1301,10 +1301,9 @@ fn build_module<'c>( generated_funcs.insert(id, thunk); } - // Pass 1d: a `spawn` trampoline per result type `R`. `tramp$R(thunk, env, slot)` runs the - // spawned closure (`thunk(env) -> R`) and stores the result into `slot` (the typed store is - // why it is generated, not in the runtime). ④b-1 calls it sequentially at `wait`; ④b-2 runs - // it on a worker thread. + // Pass 1d: a canonical `spawn` trampoline per result type `R`. It runs the spawned closure + // (`thunk(env) -> R`) and stores the result into `slot` (the typed store is why it is generated, + // not in the runtime). ④b-1 calls it sequentially at `wait`; ④b-2 runs it on a worker thread. // A trampoline per (result type `R`, fallibility): `tramp(thunk, env, slot) -> i32` runs the // spawned closure and writes its result into `slot`, returning an error code (`0` = ok). A // fallible closure returns `Result`; the trampoline stores the `Ok` payload and @@ -2897,6 +2896,7 @@ fn validate_parallel_request( validate_parallel_callable(declarations, terminal, &terminal_params, elem_out) } +#[allow(clippy::too_many_arguments)] fn parallel_generated_ids( program: &Program, declarations: &HashMap, diff --git a/crates/align_codegen_llvm/src/thinlto.rs b/crates/align_codegen_llvm/src/thinlto.rs index 710cec40..5b71ea11 100644 --- a/crates/align_codegen_llvm/src/thinlto.rs +++ b/crates/align_codegen_llvm/src/thinlto.rs @@ -152,7 +152,7 @@ unsafe fn slice_to_string(ptr: *const c_char, len: usize) -> String { if ptr.is_null() || len == 0 { return String::new(); } - let bytes = unsafe { std::slice::from_raw_parts(ptr as *const u8, len) }; + let bytes = unsafe { std::slice::from_raw_parts(ptr.cast(), len) }; String::from_utf8_lossy(bytes).into_owned() } diff --git a/crates/align_driver/tests/link_hygiene.rs b/crates/align_driver/tests/link_hygiene.rs index 5ca1fca2..a7e91b96 100644 --- a/crates/align_driver/tests/link_hygiene.rs +++ b/crates/align_driver/tests/link_hygiene.rs @@ -8,7 +8,7 @@ //! | symbol class | linkage | //! |-------------------------------------------------|--------------------------| //! | C entry `main` (void/`i32` main, or the wrapper)| external (keep by name) | -//! | `align_main` (a `Result`-returning main's body) | internal | +//! | encoded Align body for a wrapped `main` | internal | //! | every other Align program fn (+ lifted lambdas) | internal | //! | fn-value / closure / spawn / par_map thunks | private | //! | runtime `align_rt_*` + `extern "C"` declares | external (undefined decl)| @@ -56,12 +56,37 @@ fn assert_internal(ir: &str, sym: &str) { ); } -fn assert_private(ir: &str, sym: &str) { - let pfx = define_prefix(ir, sym); - assert!( - pfx.contains("private"), - "@{sym} should have `private` linkage, got `define {pfx}@{sym}(...`" - ); +fn assert_private_prefix(ir: &str, prefix: &str) { + let matches = ir + .lines() + .filter(|line| { + let line = line.trim_start(); + line.starts_with("define ") && line.contains(&format!("@\"{prefix}")) + }) + .collect::>(); + assert!(!matches.is_empty(), "no generated definition with prefix {prefix}:\n{ir}"); + for line in matches { + let before_symbol = line + .split_once('@') + .expect("a define line always names a symbol") + .0; + assert!( + before_symbol.contains("private"), + "generated symbol with prefix {prefix} must be private: {line}" + ); + } +} + +fn lowercase_hex(value: &str) -> String { + value + .as_bytes() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn align_symbol(value: &str) -> String { + format!("align_fn${}${}", value.len(), lowercase_hex(value)) } /// External linkage prints as *no* linkage word — LLVM omits it. So the `define` prefix must carry @@ -84,8 +109,8 @@ fn global_line<'a>(ir: &'a str, sym: &str) -> &'a str { } /// A representative program touching most emitted symbol classes at once: an `extern "C"` decl, a -/// non-exported helper, a `Result`-returning `main` (→ `align_main` + a generated C `main`), lifted -/// pipeline/`par_map`/closure lambdas, a string constant, and thus a `$parkernel` + a `$clos` thunk. +/// non-exported helper, a `Result`-returning `main` (encoded body + generated C `main`), lifted +/// pipeline/`par_map`/closure lambdas, a string constant, and generated parallel/closure helpers. const REPRESENTATIVE: &str = concat!( "extern \"C\" fn cabs(x: i32) -> i32\n", "\n", @@ -115,15 +140,18 @@ fn representative_program_linkage_map() { assert_external(&ir, "main"); // A `Result`-main's body and every other Align program function (incl. lifted lambdas) → internal. - assert_internal(&ir, "align_main"); - assert_internal(&ir, "helper"); - assert_internal(&ir, "main$lambda0"); - assert_internal(&ir, "main$lambda1"); - assert_internal(&ir, "main$lambda2"); + assert_internal(&ir, &align_symbol("main")); + assert_internal(&ir, &align_symbol("helper")); + assert_internal(&ir, &align_symbol("main$lambda0")); + assert_internal(&ir, &align_symbol("main$lambda1")); + assert_internal(&ir, &align_symbol("main$lambda2")); // Compiler-generated helper thunks, reached only via a function pointer → private. - assert_private(&ir, "main$lambda1$parkernel"); - assert_private(&ir, "main$lambda2$clos"); + assert_private_prefix(&ir, "align_gen$par$"); + assert_private_prefix( + &ir, + &format!("align_gen$clos${}$", lowercase_hex("main$lambda2")), + ); // The `extern "C"` symbol is an undefined declaration resolved by the linker → external, and it // must be a `declare` (never `define internal`, which would drop the reference to libc's `abs`). @@ -146,13 +174,16 @@ fn plain_main_stays_external_no_wrapper() { if !backend_available() { return; } - // An `-> i32` `main` IS the C entry directly (no wrapper, no `align_main`) — its LLVM return + // An `-> i32` `main` IS the C entry directly (no wrapper or encoded body) — its LLVM return // type already matches the C ABI's `i32` — so it keeps the symbol name `main` and external // linkage; a sibling helper is still internalized. let ir = emit_llvm("fn helper(x: i64) -> i64 = x + 1\nfn main() -> i32 {\n print(helper(41))\n return 0\n}\n"); assert_external(&ir, "main"); - assert_internal(&ir, "helper"); - assert!(!ir.contains("@align_main"), "an `-> i32` `main` needs no `align_main` body:\n{ir}"); + assert_internal(&ir, &align_symbol("helper")); + assert!( + !ir.contains(&align_symbol("main")), + "an `-> i32` `main` needs no separate Align body:\n{ir}" + ); } #[test] @@ -163,13 +194,17 @@ fn unit_main_gets_the_c_entry_wrapper() { // A `Unit`-returning `main` is NOT the C entry directly (it lowers to `void`, and the C ABI's // `main` must return `i32` — leaving `main` void would leave the return register undefined, // `docs/open-questions.md` "Unit-returning `fn main()` yields a nondeterministic exit code"). - // It is renamed `align_main` (internal) and gets a generated external `main` wrapper that + // Its encoded program identity stays internal and gets a generated external `main` wrapper that // always returns a defined `i32`, same shape as the `Result`-returning case. let ir = emit_llvm("fn helper(x: i64) -> i64 = x + 1\nfn main() {\n print(helper(41))\n}\n"); assert_external(&ir, "main"); - assert_internal(&ir, "align_main"); - assert_internal(&ir, "helper"); - assert!(ir.contains("call void @align_main()"), "wrapper must call align_main:\n{ir}"); + let body = align_symbol("main"); + assert_internal(&ir, &body); + assert_internal(&ir, &align_symbol("helper")); + assert!( + ir.contains(&format!("call void @\"{body}\"()")), + "wrapper must call the encoded Align body:\n{ir}" + ); assert!(ir.contains("ret i32 0"), "wrapper must return a defined 0:\n{ir}"); } @@ -178,13 +213,16 @@ fn fn_value_thunk_is_private() { if !backend_available() { return; } - // Using a function as a first-class value emits a `$fnval` adapter thunk (called through the + // Using a function as a first-class value emits a canonical adapter thunk (called through the // fn-value pointer) → private; the underlying function → internal. let ir = emit_llvm( "fn double(x: i32) -> i32 = x * 2\n\nfn main() -> Result<(), Error> {\n f := double\n print(f(5))\n return Ok(())\n}\n", ); - assert_internal(&ir, "double"); - assert_private(&ir, "double$fnval"); + assert_internal(&ir, &align_symbol("double")); + assert_private_prefix( + &ir, + &format!("align_gen$fnval${}$", lowercase_hex("double")), + ); assert_external(&ir, "main"); } @@ -193,13 +231,16 @@ fn spawn_trampoline_is_private() { if !backend_available() { return; } - // A `spawn`ed closure emits a per-result-type `tramp$R` trampoline (invoked by the task runtime - // through a pointer) → private, plus its capturing `$clos` thunk → private. + // A `spawn`ed closure emits a canonical task trampoline (invoked by the task runtime through a + // pointer) → private, plus its canonical capturing-closure thunk → private. let ir = emit_llvm( "fn main() -> Result<(), Error> {\n k: i64 := 100\n task_group {\n a := spawn(fn { k + 5 })\n wait()\n print(a.get())\n }\n return Ok(())\n}\n", ); - assert_private(&ir, "tramp$i64"); - assert_private(&ir, "main$lambda0$clos"); + assert_private_prefix(&ir, "align_gen$tramp$"); + assert_private_prefix( + &ir, + &format!("align_gen$clos${}$", lowercase_hex("main$lambda0")), + ); assert_external(&ir, "main"); } diff --git a/crates/align_driver/tests/main_abi.rs b/crates/align_driver/tests/main_abi.rs index 438a30de..3bc13093 100644 --- a/crates/align_driver/tests/main_abi.rs +++ b/crates/align_driver/tests/main_abi.rs @@ -22,11 +22,24 @@ fn whole_mir(name: &str, source: &str) -> align_driver::MirProgram { } fn definition_line<'a>(ir: &'a str, symbol: &str) -> &'a str { + let bare = format!("@{symbol}("); + let quoted = format!("@\"{symbol}\"("); ir.lines() - .find(|line| line.starts_with("define ") && line.contains(&format!("@{symbol}("))) + .find(|line| { + line.starts_with("define ") && (line.contains(&bare) || line.contains("ed)) + }) .unwrap_or_else(|| panic!("missing definition for @{symbol}:\n{ir}")) } +fn align_symbol(value: &str) -> String { + let hex = value + .as_bytes() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + format!("align_fn${}${hex}", value.len()) +} + #[test] fn whole_and_per_unit_entries_preserve_exit_behavior() { if !backend_available() { @@ -162,13 +175,14 @@ fn raw_and_optimized_whole_and_per_unit_c_signatures_are_exact() { if !optimized { if wrapped { + let body = align_symbol("main"); assert!( - definition_line(&ir, "align_main").starts_with("define internal "), + definition_line(&ir, &body).starts_with("define internal "), "{path} {name} Align body must be internal" ); } else { assert!( - !ir.contains("@align_main("), + !ir.contains(&align_symbol("main")), "{path} exact i32 is the direct C entry" ); } diff --git a/crates/align_driver/tests/par_map.rs b/crates/align_driver/tests/par_map.rs index 243231fa..e96f8513 100644 --- a/crates/align_driver/tests/par_map.rs +++ b/crates/align_driver/tests/par_map.rs @@ -12,6 +12,24 @@ mod common; use common::*; +fn lowercase_hex(value: &str) -> String { + value + .as_bytes() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn align_symbol(value: &str) -> String { + format!("align_fn${}${}", value.len(), lowercase_hex(value)) +} + +fn parallel_kernel<'a>(ir: &'a str, mode: u8) -> Option<&'a str> { + let prefix = format!("align_gen$par${mode}$"); + ir.split("define ") + .find(|part| part.lines().next().is_some_and(|line| line.contains(&prefix))) +} + #[test] fn par_map_pure_function() { if !backend_available() { @@ -35,9 +53,7 @@ fn par_map_capturing_lambda_uses_parallel_range_kernel() { assert_eq!(String::from_utf8_lossy(&out.stdout), "36\n"); let ir = emit_llvm(src); - let kernel = ir - .split("define ") - .find(|part| part.lines().next().is_some_and(|line| line.contains("$parkernel"))) + let kernel = parallel_kernel(&ir, 0) .unwrap_or_else(|| panic!("no capturing par_map range kernel in IR:\n{ir}")); let kernel = kernel.split_once("\n}\n").map_or(kernel, |(body, _)| body); assert!( @@ -49,7 +65,8 @@ fn par_map_capturing_lambda_uses_parallel_range_kernel() { ); assert!(kernel.contains("load i64"), "the kernel must load the captured value from its context:\n{kernel}"); assert!( - kernel.contains("call i64 @\"main$lambda") && kernel.contains(", i64 %parcapv"), + kernel.contains(&format!("call i64 @\"{}", align_symbol("main$lambda0"))) + && kernel.contains(", i64 %parcapv"), "the direct body call must receive the capture value:\n{kernel}" ); @@ -75,7 +92,11 @@ fn par_map_copy_array_capture_uses_the_context_abi() { let out = build_and_run("pm-copy-array-capture", src); assert_eq!(out.status.code(), Some(0)); assert_eq!(String::from_utf8_lossy(&out.stdout), "18\n"); - assert!(emit_llvm(src).contains("$parkernel"), "a Copy array capture should stay on the range-kernel path"); + let ir = emit_llvm(src); + assert!( + parallel_kernel(&ir, 0).is_some(), + "a Copy array capture should stay on the range-kernel path" + ); } #[test] @@ -144,11 +165,12 @@ fn par_map_over_struct_field() { assert_eq!(String::from_utf8_lossy(&out.stdout), "42\n"); let ir = emit_llvm(src); - let kernel = ir - .split("define ") - .find(|part| part.lines().next().is_some_and(|line| line.contains("$parkernel"))) + let kernel = parallel_kernel(&ir, 0) .unwrap_or_else(|| panic!("an AoS struct source should use the parallel range kernel:\n{ir}")); - assert!(kernel.contains("call i32 @net") || kernel.contains("call i32 @\"net\""), "the kernel must call the struct-consuming body directly:\n{kernel}"); + assert!( + kernel.contains(&format!("call i32 @\"{}\"", align_symbol("net"))), + "the kernel must call the struct-consuming body directly:\n{kernel}" + ); } #[test] @@ -162,7 +184,10 @@ fn par_map_over_dynamic_struct_array_uses_aos_stride_kernel() { assert_eq!(String::from_utf8_lossy(&out.stdout), "2\n15\n27\n"); let ir = emit_llvm(src); - assert!(ir.contains("$parkernel"), "a dynamic AoS struct source should use the parallel range kernel:\n{ir}"); + assert!( + parallel_kernel(&ir, 0).is_some(), + "a dynamic AoS struct source should use the parallel range kernel:\n{ir}" + ); } #[test] @@ -247,7 +272,10 @@ fn par_map_after_struct_map_keeps_struct_abi_in_the_range_kernel() { assert!(text.contains("par_map[net -> twice]"), "a struct map stage should stay in the parallel node:\n{text}"); let ir = emit_llvm(src); - assert!(ir.contains("call i32 @net") || ir.contains("call i32 @\"net\""), "the range kernel must call the aggregate map body directly:\n{ir}"); + assert!( + ir.contains(&format!("call i32 @\"{}\"", align_symbol("net"))), + "the range kernel must call the aggregate map body directly:\n{ir}" + ); } #[test] @@ -266,12 +294,13 @@ fn par_map_after_struct_projection_uses_range_kernel() { assert!(text.contains("par_map[field#1 -> twice]"), "the projection and terminal should share one parallel MIR node:\n{text}"); let ir = emit_llvm(src); - let kernel = ir - .split("define ") - .find(|part| part.lines().next().is_some_and(|line| line.contains("$parmapchain$"))) + let kernel = parallel_kernel(&ir, 0) .unwrap_or_else(|| panic!("no projected par_map range kernel in IR:\n{ir}")); assert!(kernel.contains("extractvalue"), "the projected kernel must extract the AoS field in the range loop:\n{kernel}"); - assert!(kernel.contains("call i32 @twice") || kernel.contains("call i32 @\"twice\""), "the projected kernel must call the terminal body directly:\n{kernel}"); + assert!( + kernel.contains(&format!("call i32 @\"{}\"", align_symbol("twice"))), + "the projected kernel must call the terminal body directly:\n{kernel}" + ); } #[test] @@ -290,8 +319,14 @@ fn par_map_after_struct_field_filter_uses_stable_compaction() { assert!(text.contains("par_map[where field#0 -> field#1 -> twice]"), "field filtering and projection should remain one ordered parallel node:\n{text}"); let ir = emit_llvm(src); - assert!(ir.contains("$parfilter$count$wherefield$0$field$1"), "the field-filter count kernel must be generated:\n{ir}"); - assert!(ir.contains("$parfilter$scatter$wherefield$0$field$1"), "the field-filter scatter kernel must be generated:\n{ir}"); + assert!( + parallel_kernel(&ir, 2).is_some(), + "the field-filter count kernel must be generated:\n{ir}" + ); + assert!( + parallel_kernel(&ir, 3).is_some(), + "the field-filter scatter kernel must be generated:\n{ir}" + ); } #[test] @@ -353,13 +388,14 @@ fn par_map_reduction_range_kernel_writes_partials() { } let src = "fn dbl(x: i64) -> i64 = x * 2\npub fn run(xs: slice) -> i64 = xs.par_map(dbl).sum()\nfn main() -> i32 = 0\n"; let ir = emit_llvm(src); - let kernel = ir - .split("define ") - .find(|part| part.lines().next().is_some_and(|line| line.contains("$parreducekernel"))) + let kernel = parallel_kernel(&ir, 1) .unwrap_or_else(|| panic!("no par_map reduction range kernel in IR:\n{ir}")); let kernel = kernel.split_once("\n}\n").map_or(kernel, |(body, _)| body); assert!(kernel.contains("phi i64"), "the reduction kernel needs a counted loop and an accumulator:\n{kernel}"); - assert!(kernel.contains("call i64 @dbl(i64"), "the reduction kernel must call the body directly:\n{kernel}"); + assert!( + kernel.contains(&format!("call i64 @\"{}\"(i64", align_symbol("dbl"))), + "the reduction kernel must call the body directly:\n{kernel}" + ); assert!(kernel.contains("add i64"), "the reduction kernel must use plain wrapping integer addition:\n{kernel}"); assert!(kernel.contains("store i64"), "the reduction kernel must publish one partial:\n{kernel}"); } @@ -509,7 +545,7 @@ fn chunks_par_map_inside_arena_frees_chunk_buffer() { return; } // Inside an `arena {}`, the `chunks` header buffer is heap-allocated (not arena), so it must - // still be freed (`drop_value`) — the arena's bulk-free doesn't cover it. (1+2)+(3+4) = 10. + // still be dropped before `arena_end` — the arena's bulk-free doesn't cover it. (1+2)+(3+4) = 10. let src = "fn chunk_sum(c: slice) -> i64 = c.sum()\nfn main() -> Result<(), Error> {\n arena {\n total := [1, 2, 3, 4].chunks(2).par_map(chunk_sum).sum()\n print(total)\n }\n return Ok(())\n}\n"; let out = build_and_run("pm-chunks-arena", src); assert_eq!(out.status.code(), Some(0)); @@ -518,7 +554,13 @@ fn chunks_par_map_inside_arena_frees_chunk_buffer() { let mut sm = SourceMap::new(); let mir = lower_to_mir(&check(&mut sm, "m", src).hir); let text = align_mir::print::program_to_string(&mir); - assert!(text.contains("drop_value"), "the chunks buffer must be freed inside the arena:\n{text}"); + let drop = text + .find("drop _1") + .unwrap_or_else(|| panic!("the chunks buffer must be dropped inside the arena:\n{text}")); + let arena_end = text + .find("arena_end") + .unwrap_or_else(|| panic!("the arena must have an explicit end marker:\n{text}")); + assert!(drop < arena_end, "the chunks buffer must be dropped before arena_end:\n{text}"); } #[test] @@ -579,9 +621,7 @@ fn par_map_range_kernel_owns_the_direct_element_loop() { } let src = "fn dbl(x: i64) -> i64 = x * 2\nfn main() -> i32 {\n ys := [1, 2, 3].par_map(dbl)\n return ys[0] as i32\n}\n"; let ir = emit_llvm(src); - let kernel = ir - .split("define ") - .find(|part| part.lines().next().is_some_and(|line| line.contains("$parkernel"))) + let kernel = parallel_kernel(&ir, 0) .unwrap_or_else(|| panic!("no par_map range kernel in IR:\n{ir}")); let kernel = kernel.split_once("\n}\n").map_or(kernel, |(body, _)| body); @@ -595,7 +635,10 @@ fn par_map_range_kernel_owns_the_direct_element_loop() { ); assert!(kernel.contains("phi i64"), "range kernel needs one counted induction variable:\n{kernel}"); assert!(kernel.contains("getelementptr inbounds i64"), "range kernel needs typed element GEPs:\n{kernel}"); - assert!(kernel.contains("call i64 @dbl(i64"), "the element loop must call its known body directly:\n{kernel}"); + assert!( + kernel.contains(&format!("call i64 @\"{}\"(i64", align_symbol("dbl"))), + "the element loop must call its known body directly:\n{kernel}" + ); assert!( !kernel.lines().any(|line| line.trim_start().starts_with("call ") && line.contains(" %")), "the element loop must not retain an indirect per-element callback:\n{kernel}" @@ -609,9 +652,7 @@ fn cheap_par_map_range_kernel_vectorizes_after_specialization() { } let src = "fn dbl(x: i64) -> i64 = x * 2\npub fn run(xs: slice) -> array = xs.par_map(dbl)\nfn main() -> i32 = 0\n"; let ir = emit_llvm_optimized(src, &["run"]); - let kernel = ir - .split("define ") - .find(|part| part.lines().next().is_some_and(|line| line.contains("$parkernel"))) + let kernel = parallel_kernel(&ir, 0) .unwrap_or_else(|| panic!("no optimized par_map range kernel in IR:\n{ir}")); let kernel = kernel.split_once("\n}\n").map_or(kernel, |(body, _)| body); @@ -620,7 +661,10 @@ fn cheap_par_map_range_kernel_vectorizes_after_specialization() { "a cheap arithmetic range kernel should expose a vectorized loop to LLVM:\n{kernel}" ); assert!( - !kernel.contains("call i64 @dbl") && !kernel.lines().any(|line| line.trim_start().starts_with("call ") && line.contains(" %")), + !kernel.contains(&format!("call i64 @\"{}\"", align_symbol("dbl"))) + && !kernel + .lines() + .any(|line| line.trim_start().starts_with("call ") && line.contains(" %")), "the optimized hot loop must inline the body and contain no per-element call:\n{kernel}" ); } @@ -641,13 +685,15 @@ fn par_map_after_length_preserving_maps_uses_one_range_kernel() { assert!(text.contains("par_map[add_one -> triple -> finish]"), "the map chain should be one parallel MIR node:\n{text}"); let ir = emit_llvm(src); - let kernel = ir - .split("define ") - .find(|part| part.lines().next().is_some_and(|line| line.contains("$parmapchain$"))) + let kernel = parallel_kernel(&ir, 0) .unwrap_or_else(|| panic!("no staged par_map range kernel in IR:\n{ir}")); let kernel = kernel.split_once("\n}\n").map_or(kernel, |(body, _)| body); - for func in ["@add_one", "@triple", "@finish"] { - assert!(kernel.contains(&format!("call i64 {func}")), "staged kernel must call {func} directly:\n{kernel}"); + for func in ["add_one", "triple", "finish"] { + let symbol = align_symbol(func); + assert!( + kernel.contains(&format!("call i64 @\"{symbol}\"")), + "staged kernel must call {func} directly:\n{kernel}" + ); } } @@ -678,8 +724,14 @@ fn par_map_after_multiple_filters_uses_one_stable_parallel_node() { assert!(text.contains("par_map[where positive -> where even -> dec]"), "filters should use one parallel MIR node:\n{text}"); let ir = emit_llvm(src); - assert!(ir.contains("$parfilter$count$"), "filter count kernel should be emitted:\n{ir}"); - assert!(ir.contains("$parfilter$scatter$"), "filter scatter kernel should be emitted:\n{ir}"); + assert!( + parallel_kernel(&ir, 2).is_some(), + "filter count kernel should be emitted:\n{ir}" + ); + assert!( + parallel_kernel(&ir, 3).is_some(), + "filter scatter kernel should be emitted:\n{ir}" + ); } #[test] @@ -698,8 +750,14 @@ fn par_map_after_string_contains_filter_uses_stable_compaction() { assert!(text.contains("par_map[where str.contains -> main$lambda"), "string filter should be a generated staged parallel node:\n{text}"); let ir = emit_llvm(src); - assert!(ir.contains("$parfilter$count$wherecontains"), "string filter count kernel should be emitted:\n{ir}"); - assert!(ir.contains("$parfilter$scatter$wherecontains"), "string filter scatter kernel should be emitted:\n{ir}"); + assert!( + parallel_kernel(&ir, 2).is_some(), + "string filter count kernel should be emitted:\n{ir}" + ); + assert!( + parallel_kernel(&ir, 3).is_some(), + "string filter scatter kernel should be emitted:\n{ir}" + ); assert!(ir.contains("call i32 @align_rt_str_contains"), "string filter kernel should use the existing str_contains ABI:\n{ir}"); } diff --git a/crates/align_driver/tests/per_unit_surface.rs b/crates/align_driver/tests/per_unit_surface.rs index 62d144db..59455570 100644 --- a/crates/align_driver/tests/per_unit_surface.rs +++ b/crates/align_driver/tests/per_unit_surface.rs @@ -47,6 +47,15 @@ fn alignc() -> &'static str { env!("CARGO_BIN_EXE_alignc") } +fn align_symbol(value: &str) -> String { + let hex = value + .as_bytes() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + format!("align_fn${}${hex}", value.len()) +} + /// Strip a Mach-O leading underscore so a symbol comparison is object-format-portable. fn norm(sym: &str) -> &str { sym.strip_prefix('_').unwrap_or(sym) @@ -270,8 +279,9 @@ fn emit_obj_multi_file_writes_one_object_per_unit() { // The non-entry `pub` fn is a defined external (`T`) in ITS object (mangled `util.math$cube`). if let Some(dep_syms) = nm_symbols(&proj.dir.join("util.math.o")) { + let cube = align_symbol("util.math$cube"); assert!( - dep_syms.iter().any(|(k, n)| *k == 'T' && norm(n) == "util.math$cube"), + dep_syms.iter().any(|(k, n)| *k == 'T' && norm(n) == cube), "util.math$cube must be external in util.math.o: {dep_syms:?}" ); // The entry object exports `main`. @@ -377,13 +387,13 @@ fn emit_llvm_optimized_leaves_cross_unit_call_opaque() { let dep_ir = emit_llvm_ir(&dep.mir, BuildTarget::Baseline, true, &[], false).expect("dep opt ir"); assert!( - !dep_ir.contains("call") || !dep_ir.contains("$sq"), + !dep_ir.contains("call") || !dep_ir.contains(&align_symbol("util.math$sq")), "the intra-unit private `sq` must inline into `cube` (no surviving call):\n{dep_ir}" ); let entry_ir = emit_llvm_ir(&entry.mir, BuildTarget::Baseline, true, &[], false).expect("entry opt ir"); assert!( - entry_ir.contains("util.math$cube"), + entry_ir.contains(&align_symbol("util.math$cube")), "the cross-unit `pub` call must stay an opaque call to the extern:\n{entry_ir}" ); } diff --git a/crates/align_driver/tests/unit_main_exit_code.rs b/crates/align_driver/tests/unit_main_exit_code.rs index e08f2620..0f77ad75 100644 --- a/crates/align_driver/tests/unit_main_exit_code.rs +++ b/crates/align_driver/tests/unit_main_exit_code.rs @@ -5,9 +5,9 @@ //! yields a nondeterministic exit code"): before the fix, a `()`-returning Align `main` WAS the C //! entry `main` directly, declared `void` in LLVM IR — but the C ABI's `main` must return `i32`, //! so `ret void` left the return register (`eax`/`w0`) undefined and the observed exit code varied -//! run to run (88/216/168/120/104 across five runs of the identical binary). The fix renames a -//! `Unit`-returning `main` to `align_main` (same as the existing `Result`-returning `main` split) -//! and generates a C `main` wrapper that always emits `ret i32 0` after the call. +//! run to run (88/216/168/120/104 across five runs of the identical binary). The fix emits the +//! `Unit`-returning Align body under its encoded program identity and generates a C `main` wrapper +//! that always emits `ret i32 0` after the call. //! //! Both the whole-program and per-unit (`build_per_unit`/M15 S2) codegen paths share this wrapper //! logic, so both are pinned here with a same-binary, run-N-times determinism check. diff --git a/crates/align_mir/src/canonical_graph.rs b/crates/align_mir/src/canonical_graph.rs index 915566a0..0214ddc4 100644 --- a/crates/align_mir/src/canonical_graph.rs +++ b/crates/align_mir/src/canonical_graph.rs @@ -1,3 +1,4 @@ +use std::collections::hash_map::Entry; use std::collections::{BTreeSet, HashMap, HashSet}; use std::fmt; @@ -205,10 +206,11 @@ impl<'a> ValidatedGraph<'a> { candidates: Vec::new(), next_ordinal: 0, end_ordinals: HashMap::new(), + inline_edges: Vec::new(), }; let mut references = Vec::new(); for &root in roots { - validator.scan_ty(root, &mut references); + validator.scan_ty(root, &mut references, None); } references.reverse(); validator.pending.extend(references); @@ -216,6 +218,7 @@ impl<'a> ValidatedGraph<'a> { validator.visit_node(node); } validator.collect_cross_node_candidates(); + validator.collect_inline_cycle_candidates(); if let Some(candidate) = validator .candidates .iter() @@ -239,6 +242,7 @@ struct GraphValidator<'a> { candidates: Vec, next_ordinal: u64, end_ordinals: HashMap, + inline_edges: Vec, } #[derive(Clone, Copy)] @@ -248,6 +252,13 @@ struct ErrorCandidate { error: CanonicalGraphError, } +#[derive(Clone, Copy)] +struct InlineEdge { + from: Node, + to: Node, + ordinal: u64, +} + impl<'a> GraphValidator<'a> { fn visit_node(&mut self, node: Node) { if !self.seen.insert(node) { @@ -288,7 +299,7 @@ impl<'a> GraphValidator<'a> { if !names.insert(field.name.as_str()) { self.candidate(name_ordinal, CanonicalGraphError::DuplicateMember); } - self.scan_ty(field.ty, &mut references); + self.scan_ty(field.ty, &mut references, Some(node)); } } SourceShapeNode::Enum { @@ -321,7 +332,7 @@ impl<'a> GraphValidator<'a> { None => self.candidate(count_ordinal, CanonicalGraphError::InvalidCount), } for &value in &variant.payload { - self.scan_scalar(value, &mut references); + self.scan_scalar(value, &mut references, Some(node)); } } } @@ -329,18 +340,18 @@ impl<'a> GraphValidator<'a> { let count_ordinal = self.field_ordinal(); self.validate_count(elems.len(), count_ordinal); for &value in elems { - self.scan_scalar(value, &mut references); + self.scan_scalar(value, &mut references, Some(node)); } } SourceShapeNode::Tagged(value) => match value { hir::TaggedType::Option(value) => { self.field_ordinal(); - self.scan_scalar(*value, &mut references); + self.scan_scalar(*value, &mut references, Some(node)); } hir::TaggedType::Result(ok, err) => { self.field_ordinal(); - self.scan_scalar(*ok, &mut references); - self.scan_scalar(*err, &mut references); + self.scan_scalar(*ok, &mut references, Some(node)); + self.scan_scalar(*err, &mut references, Some(node)); } }, SourceShapeNode::Function { @@ -356,9 +367,9 @@ impl<'a> GraphValidator<'a> { if !matches!(mode, ParamMode::ByValue | ParamMode::Out) { self.candidate(mode_ordinal, CanonicalGraphError::InvalidGraph); } - self.scan_scalar(value, &mut references); + self.scan_scalar(value, &mut references, None); } - self.scan_ty(*ret, &mut references); + self.scan_ty(*ret, &mut references, None); self.scan_borrow_summary(return_borrow, params.len()); let region_ordinal = self.scan_region_summary(return_region, params.len()); if !summaries_agree(return_borrow, return_region) { @@ -384,36 +395,36 @@ impl<'a> GraphValidator<'a> { }; match node { Node::Struct(id) => { - if let Some(definition) = view.structs.get(id as usize) { - if let Some(error) = Self::compare_nominal( + if let Some(definition) = view.structs.get(id as usize) + && let Some(error) = Self::compare_nominal( view, node, &definition.source_name, &mut nominal_sources, &mut known_shapes, - ) { - self.candidate(end_ordinal, error); - } + ) + { + self.candidate(end_ordinal, error); } } Node::Enum(id) => { - if let Some(definition) = view.enums.get(id as usize) { - if let Some(error) = Self::compare_nominal( + if let Some(definition) = view.enums.get(id as usize) + && let Some(error) = Self::compare_nominal( view, node, &definition.source_name, &mut nominal_sources, &mut known_shapes, - ) { - self.candidate(end_ordinal, error); - } + ) + { + self.candidate(end_ordinal, error); } } Node::Tuple(id) => { - if let Some(definition) = view.tuples.get(id as usize) { - if tuples.insert(definition.elems.clone(), node).is_some() { - self.candidate(end_ordinal, CanonicalGraphError::DuplicateMember); - } + if let Some(definition) = view.tuples.get(id as usize) + && tuples.insert(definition.elems.clone(), node).is_some() + { + self.candidate(end_ordinal, CanonicalGraphError::DuplicateMember); } } Node::Tagged(_) | Node::Fn(_) => {} @@ -421,6 +432,77 @@ impl<'a> GraphValidator<'a> { } } + fn collect_inline_cycle_candidates(&mut self) { + let mut forward = HashMap::>::new(); + let mut reverse = HashMap::>::new(); + for edge in &self.inline_edges { + if self.view.source_shape_node(edge.to).is_none() { + continue; + } + forward.entry(edge.from).or_default().push(edge.to); + reverse.entry(edge.to).or_default().push(edge.from); + } + + let mut seen = HashSet::new(); + let mut finish = Vec::with_capacity(self.order.len()); + for &start in &self.order { + if !seen.insert(start) { + continue; + } + let mut work = vec![(start, false)]; + while let Some((node, exiting)) = work.pop() { + if exiting { + finish.push(node); + continue; + } + work.push((node, true)); + if let Some(children) = forward.get(&node) { + for &child in children.iter().rev() { + if seen.insert(child) { + work.push((child, false)); + } + } + } + } + } + + let mut component_by_node = HashMap::new(); + let mut component_sizes = Vec::new(); + for start in finish.into_iter().rev() { + if component_by_node.contains_key(&start) { + continue; + } + let component = component_sizes.len(); + let mut size = 0usize; + let mut work = vec![start]; + component_by_node.insert(start, component); + while let Some(node) = work.pop() { + size += 1; + if let Some(parents) = reverse.get(&node) { + for &parent in parents { + if let Entry::Vacant(entry) = component_by_node.entry(parent) { + entry.insert(component); + work.push(parent); + } + } + } + } + component_sizes.push(size); + } + + let edges = self.inline_edges.clone(); + for edge in edges { + let Some(&component) = component_by_node.get(&edge.from) else { + continue; + }; + if component_by_node.get(&edge.to) == Some(&component) + && (edge.from == edge.to || component_sizes[component] > 1) + { + self.candidate(edge.ordinal, CanonicalGraphError::InvalidGraph); + } + } + } + fn compare_nominal( view: CanonicalTypeView<'a>, node: Node, @@ -440,14 +522,31 @@ impl<'a> GraphValidator<'a> { (!same_shape).then_some(CanonicalGraphError::InvalidGraph) } - fn scan_scalar(&mut self, value: Scalar, references: &mut Vec) { + fn scan_scalar( + &mut self, + value: Scalar, + references: &mut Vec, + inline_from: Option, + ) { let ordinal = self.field_ordinal(); match value { Scalar::Struct(id) | Scalar::DynStructArray(id) | Scalar::Soa(id) => { - self.scan_reference(Node::Struct(id), ordinal, references) + let node = Node::Struct(id); + self.scan_reference(node, ordinal, references); + if matches!(value, Scalar::Struct(_)) { + self.record_inline_edge(inline_from, node, ordinal); + } + } + Scalar::Enum(id) => { + let node = Node::Enum(id); + self.scan_reference(node, ordinal, references); + self.record_inline_edge(inline_from, node, ordinal); + } + Scalar::Tagged(id) => { + let node = Node::Tagged(id); + self.scan_reference(node, ordinal, references); + self.record_inline_edge(inline_from, node, ordinal); } - Scalar::Enum(id) => self.scan_reference(Node::Enum(id), ordinal, references), - Scalar::Tagged(id) => self.scan_reference(Node::Tagged(id), ordinal, references), Scalar::Fn(id) => self.scan_reference(Node::Fn(id), ordinal, references), Scalar::Int(value) if validate_int(value.signed, value.bits).is_err() => { self.candidate(ordinal, CanonicalGraphError::InvalidWidth) @@ -463,26 +562,31 @@ impl<'a> GraphValidator<'a> { } } - fn scan_ty(&mut self, value: Ty, references: &mut Vec) { + fn scan_ty( + &mut self, + value: Ty, + references: &mut Vec, + inline_from: Option, + ) { let ordinal = self.field_ordinal(); match value { - Ty::Option(value) - | Ty::Box(value) + Ty::Option(value) => self.scan_scalar(value, references, inline_from), + Ty::Box(value) | Ty::Slice(value) | Ty::DynArray(value) | Ty::ArrayBuilder(value) - | Ty::Task(value) => self.scan_scalar(value, references), + | Ty::Task(value) => self.scan_scalar(value, references, None), Ty::Result(ok, err) => { - self.scan_scalar(ok, references); - self.scan_scalar(err, references); + self.scan_scalar(ok, references, inline_from); + self.scan_scalar(err, references, inline_from); } Ty::Array(value, _) => { - self.scan_scalar(value, references); + self.scan_scalar(value, references, inline_from); self.field_ordinal(); } Ty::Vec(value, lanes) | Ty::Mask(value, lanes) => { let scalar_ordinal = self.next_ordinal; - self.scan_scalar(value, references); + self.scan_scalar(value, references, None); let lanes_ordinal = self.field_ordinal(); if !matches!(value, Scalar::Int(_) | Scalar::Float(_)) { self.candidate(scalar_ordinal, CanonicalGraphError::InvalidWidth); @@ -492,7 +596,9 @@ impl<'a> GraphValidator<'a> { } } Ty::StructArray(id, _) => { - self.scan_reference(Node::Struct(id), ordinal, references); + let node = Node::Struct(id); + self.scan_reference(node, ordinal, references); + self.record_inline_edge(inline_from, node, ordinal); self.field_ordinal(); } Ty::DictEncoded(id, field) => { @@ -511,13 +617,29 @@ impl<'a> GraphValidator<'a> { self.scan_reference(Node::Struct(id), ordinal, references); self.field_ordinal(); } - Ty::Tagged(id) => self.scan_reference(Node::Tagged(id), ordinal, references), + Ty::Tagged(id) => { + let node = Node::Tagged(id); + self.scan_reference(node, ordinal, references); + self.record_inline_edge(inline_from, node, ordinal); + } Ty::Soa(id) | Ty::JsonScanner(id) | Ty::Struct(id) => { - self.scan_reference(Node::Struct(id), ordinal, references) + let node = Node::Struct(id); + self.scan_reference(node, ordinal, references); + if matches!(value, Ty::Struct(_)) { + self.record_inline_edge(inline_from, node, ordinal); + } + } + Ty::Tuple(id) => { + let node = Node::Tuple(id); + self.scan_reference(node, ordinal, references); + self.record_inline_edge(inline_from, node, ordinal); } - Ty::Tuple(id) => self.scan_reference(Node::Tuple(id), ordinal, references), Ty::Fn(id) => self.scan_reference(Node::Fn(id), ordinal, references), - Ty::Enum(id) => self.scan_reference(Node::Enum(id), ordinal, references), + Ty::Enum(id) => { + let node = Node::Enum(id); + self.scan_reference(node, ordinal, references); + self.record_inline_edge(inline_from, node, ordinal); + } Ty::Int(value) if validate_int(value.signed, value.bits).is_err() => { self.candidate(ordinal, CanonicalGraphError::InvalidWidth) } @@ -542,6 +664,12 @@ impl<'a> GraphValidator<'a> { } } + fn record_inline_edge(&mut self, from: Option, to: Node, ordinal: u64) { + if let Some(from) = from { + self.inline_edges.push(InlineEdge { from, to, ordinal }); + } + } + fn scan_borrow_summary(&mut self, summary: &hir::ReturnBorrowSummary, params: usize) -> u64 { match summary { hir::ReturnBorrowSummary::None => self.field_ordinal(), @@ -1479,18 +1607,16 @@ pub(super) fn canonical_fn_abi_record_len(bytes: &[u8]) -> Result bool { && definitions == function_type_facts(&canonical.fn_types) } -fn function_type_facts( - definitions: &[FunctionTypeDef], -) -> Vec<( +type FunctionTypeFacts = ( Vec<(ParamMode, Scalar)>, Ty, hir::ReturnBorrowSummary, hir::ReturnRegionSummary, -)> { +); + +fn function_type_facts(definitions: &[FunctionTypeDef]) -> Vec { definitions .iter() .map(|definition| { @@ -2623,6 +2749,54 @@ mod tests { ); } + #[test] + fn canonical_graph_rejects_inline_cycles_but_allows_header_cycles() { + let mut direct = baseline_program(); + direct.structs[0].fields[0].ty = Ty::Struct(0); + assert_eq!( + validate(Ty::Struct(0), &direct), + Err(CanonicalGraphError::InvalidGraph) + ); + + let mut cycle_before_missing = direct.clone(); + let mut later = cycle_before_missing.structs[0].fields[0].clone(); + later.name = "later".into(); + later.ty = Ty::Struct(u32::MAX); + cycle_before_missing.structs[0].fields.push(later); + assert_eq!( + validate(Ty::Struct(0), &cycle_before_missing), + Err(CanonicalGraphError::InvalidGraph) + ); + + let mut missing_before_cycle = direct.clone(); + let mut later = missing_before_cycle.structs[0].fields[0].clone(); + later.name = "later".into(); + missing_before_cycle.structs[0].fields[0].ty = Ty::Struct(u32::MAX); + missing_before_cycle.structs[0].fields.push(later); + assert_eq!( + validate(Ty::Struct(0), &missing_before_cycle), + Err(CanonicalGraphError::MissingReference) + ); + + let mut mutual = baseline_program(); + let mut child = mutual.structs[0].clone(); + child.source_name = "Child".into(); + child.fields[0].ty = Ty::Struct(0); + mutual.structs[0].fields[0].ty = Ty::Struct(1); + mutual.structs.push(child); + assert_eq!( + validate(Ty::Struct(0), &mutual), + Err(CanonicalGraphError::InvalidGraph) + ); + + let mut boxed = baseline_program(); + boxed.structs[0].fields[0].ty = Ty::Box(Scalar::Struct(0)); + assert_eq!( + validate(Ty::Struct(0), &boxed).unwrap(), + [Node::Struct(0)] + ); + } + #[test] fn canonical_graph_validation_error_precedence() { let base = baseline_program(); @@ -2770,7 +2944,7 @@ mod tests { let mut second = program.structs[0].fields[0].clone(); second.name = "other".into(); program.structs[0].fields.push(second); - program.structs[1].fields[0].ty = Ty::Struct(1); + program.structs[1].fields[0].ty = Ty::Box(Scalar::Struct(1)); let order = validate(Ty::Struct(0), &program).unwrap(); assert_eq!(order, [Node::Struct(0), Node::Struct(1)]); let view = CanonicalTypeView { @@ -3042,6 +3216,25 @@ mod tests { ]; error(&invalid_align, CanonicalCodecError::InvalidGraph); + let mut inline_cycle = baseline_program(); + let mut child = inline_cycle.structs[0].clone(); + child.source_name = "Child".into(); + child.fields[0].ty = Ty::Bool; + inline_cycle.structs[0].fields[0].ty = Ty::Struct(1); + inline_cycle.structs.push(child); + let valid = CanonicalTy::from_program( + Ty::Struct(0), + &mir_program(&inline_cycle), + ) + .unwrap(); + let mut recursive = valid.as_bytes().to_vec(); + let reference = recursive + .windows(5) + .position(|window| window == [50, 1, 0, 0, 0]) + .expect("fixture contains the root-to-child inline reference"); + recursive[reference + 1..reference + 5].copy_from_slice(&0u32.to_le_bytes()); + error(&recursive, CanonicalCodecError::InvalidGraph); + let duplicate_function = [ 1, 2, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 52, 0, 0, 0, 0, ]; @@ -3067,6 +3260,12 @@ mod tests { CanonicalFnAbi::decode(&invalid_mode), Err(CanonicalCodecError::InvalidGraph) ); + let mut invalid_mode_then_truncated = vec![1, 1, 0, 0, 0, 2]; + invalid_mode_then_truncated.extend(unit); + assert_eq!( + CanonicalFnAbi::decode(&invalid_mode_then_truncated), + Err(CanonicalCodecError::InvalidGraph) + ); let mut abi_trailing = vec![1, 0, 0, 0, 0]; abi_trailing.extend(unit); abi_trailing.extend([0, 0, 0xff]); diff --git a/crates/align_mir/src/lib.rs b/crates/align_mir/src/lib.rs index c7afb7ed..4870c82a 100644 --- a/crates/align_mir/src/lib.rs +++ b/crates/align_mir/src/lib.rs @@ -217,7 +217,11 @@ fn combined_par_map_stage_work_weight( .chain(std::iter::once(weights.get(terminal).copied().unwrap_or(PAR_MAP_DEFAULT_WORK_WEIGHT))) .map(u16::from) .fold(0u16, u16::saturating_add); - total.min(u16::from(PAR_MAP_MAX_WORK_WEIGHT)) as u8 + match total { + 0..=1 => PAR_MAP_DEFAULT_WORK_WEIGHT, + 2 => 2, + _ => PAR_MAP_MAX_WORK_WEIGHT, + } } /// Fill the runtime scheduling hint after all functions have been lowered. Keeping this as a diff --git a/crates/align_mir/src/validate_hir.rs b/crates/align_mir/src/validate_hir.rs index 64e3a24d..164e0abd 100644 --- a/crates/align_mir/src/validate_hir.rs +++ b/crates/align_mir/src/validate_hir.rs @@ -371,7 +371,11 @@ impl<'a> DeclarationValidator<'a> { parameter_types.len() == function.params.len() && parameter_types .iter() - .all(|&ty| self.placement.source_function_type_ok(ty, true, false)) + .enumerate() + .all(|(index, &ty)| { + self.placement + .stored_function_parameter_ok(function, index, ty) + }) && self .placement .source_function_type_ok(function.ret, false, true) @@ -837,11 +841,13 @@ impl<'a> PlacementValidator<'a> { fn source_functions_valid(&self) -> bool { for function in &self.program.fns { - if !function.params.iter().all(|&local| { + if !function.params.iter().enumerate().all(|(index, &local)| { function .locals .get(local as usize) - .is_some_and(|local| self.source_function_type_ok(local.ty, true, false)) + .is_some_and(|local| { + self.stored_function_parameter_ok(function, index, local.ty) + }) }) || !self.source_function_type_ok(function.ret, false, true) { return false; @@ -1210,6 +1216,30 @@ impl<'a> PlacementValidator<'a> { && !(return_position && matches!(ty, Ty::Box(_) | Ty::Fn(_))) } + fn stored_function_parameter_ok(&self, function: &hir::Fn, index: usize, ty: Ty) -> bool { + let hir::FnOrigin::Lifted { capture_count } = function.origin else { + return self.source_function_type_ok(ty, true, false); + }; + let Some(capture_start) = usize::try_from(capture_count) + .ok() + .and_then(|count| function.params.len().checked_sub(count)) + else { + return self.source_function_type_ok(ty, true, false); + }; + if index < capture_start { + return self.source_function_type_ok(ty, true, false); + } + match ty { + Ty::Array(element, length) => { + length > 0 && self.scalar_ok(element, ScalarPlacement::Collection) + } + Ty::StructArray(id, length) => { + length > 0 && self.program.structs.get(id as usize).is_some() + } + _ => self.source_function_type_ok(ty, true, false), + } + } + fn box_payload_ok(&self, payload: Scalar) -> bool { if !self.scalar_ok(payload, ScalarPlacement::Payload { allow_param: false }) { return false; @@ -8508,6 +8538,17 @@ impl<'a> BodyValidator<'a> { tuple.elems.iter().all(|scalar| self.scalar_copy_ok(*scalar)) }), Ty::Fn(id) => self.program.fn_types.get(id as usize).is_some(), + Ty::Array(scalar, length) => length > 0 && self.scalar_copy_ok(scalar), + Ty::StructArray(id, length) => { + length > 0 + && self.program.structs.get(id as usize).is_some() + && !align_sema::struct_is_move( + id, + &self.program.structs, + &self.program.enums, + &self.program.tagged_types, + ) + } Ty::Box(_) | Ty::String | Ty::DynArray(_) | Ty::DynStructArray(..) => false, other => align_sema::ty_to_scalar(other).is_some_and(|scalar| self.scalar_copy_ok(scalar)), } diff --git a/crates/align_runtime/src/lib.rs b/crates/align_runtime/src/lib.rs index 72dbb5db..85ba6999 100644 --- a/crates/align_runtime/src/lib.rs +++ b/crates/align_runtime/src/lib.rs @@ -538,7 +538,7 @@ pub unsafe extern "C" fn align_rt_dns_resolve(host: *const u8, host_len: i64, ou hints.ai_socktype = SOCK_STREAM; let mut res: *mut AddrInfo = core::ptr::null_mut(); - let rc = unsafe { getaddrinfo(c_host.as_ptr() as *const u8, core::ptr::null(), &hints, &mut res) }; + let rc = unsafe { getaddrinfo(c_host.as_ptr().cast(), core::ptr::null(), &hints, &mut res) }; if rc != 0 { return eai_to_status(rc); } @@ -779,7 +779,7 @@ pub unsafe extern "C" fn align_rt_tcp_connect(host: *const u8, host_len: i64, po hints.ai_socktype = SOCK_STREAM; let mut res: *mut AddrInfo = core::ptr::null_mut(); - let rc = unsafe { getaddrinfo(c_host.as_ptr() as *const u8, c_service.as_ptr() as *const u8, &hints, &mut res) }; + let rc = unsafe { getaddrinfo(c_host.as_ptr().cast(), c_service.as_ptr().cast(), &hints, &mut res) }; if rc != 0 { return eai_to_status(rc); } @@ -1057,9 +1057,9 @@ unsafe fn tcp_listen_impl(host: *const u8, host_len: i64, port: i64, out: *mut * hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; - let node = c_host.as_ref().map_or(core::ptr::null(), |h| h.as_ptr() as *const u8); + let node = c_host.as_ref().map_or(core::ptr::null(), |h| h.as_ptr().cast()); let mut res: *mut AddrInfo = core::ptr::null_mut(); - let rc = unsafe { getaddrinfo(node, c_service.as_ptr() as *const u8, &hints, &mut res) }; + let rc = unsafe { getaddrinfo(node, c_service.as_ptr().cast(), &hints, &mut res) }; if rc != 0 { return eai_to_status(rc); } @@ -1290,9 +1290,9 @@ pub unsafe extern "C" fn align_rt_udp_bind(host: *const u8, host_len: i64, port: hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; - let node = c_host.as_ref().map_or(core::ptr::null(), |h| h.as_ptr() as *const u8); + let node = c_host.as_ref().map_or(core::ptr::null(), |h| h.as_ptr().cast()); let mut res: *mut AddrInfo = core::ptr::null_mut(); - let rc = unsafe { getaddrinfo(node, c_service.as_ptr() as *const u8, &hints, &mut res) }; + let rc = unsafe { getaddrinfo(node, c_service.as_ptr().cast(), &hints, &mut res) }; if rc != 0 { return eai_to_status(rc); } @@ -1386,7 +1386,7 @@ pub unsafe extern "C" fn align_rt_udp_send_to(sock: *mut UdpSocket, data: *const hints.ai_socktype = SOCK_DGRAM; let mut res: *mut AddrInfo = core::ptr::null_mut(); - let rc = unsafe { getaddrinfo(c_host.as_ptr() as *const u8, c_service.as_ptr() as *const u8, &hints, &mut res) }; + let rc = unsafe { getaddrinfo(c_host.as_ptr().cast(), c_service.as_ptr().cast(), &hints, &mut res) }; if rc != 0 { return -(eai_to_status(rc) as i64); } @@ -10963,7 +10963,7 @@ unsafe fn marshal_cmd_argv( argv_owned.push(c); } // The argv pointer vector (borrowing `argv_owned`'s bytes) + a null terminator. - let mut argv_ptrs: Vec<*const u8> = argv_owned.iter().map(|c| c.as_ptr() as *const u8).collect(); + let mut argv_ptrs: Vec<*const u8> = argv_owned.iter().map(|c| c.as_ptr().cast()).collect(); argv_ptrs.push(core::ptr::null()); Ok((cmd_c, argv_owned, argv_ptrs)) } @@ -11021,7 +11021,7 @@ pub unsafe extern "C" fn align_rt_process_spawn( // Child: replace the image. `execvp` returns only on failure — then `_exit(127)` (the shell // "command not found / not executable" convention). No `malloc`/`print` here. unsafe { - execvp(cmd_c.as_ptr() as *const u8, argv_ptrs.as_ptr()); + execvp(cmd_c.as_ptr().cast(), argv_ptrs.as_ptr()); _exit(127) } } @@ -11156,7 +11156,7 @@ pub unsafe extern "C" fn align_rt_process_exec( // NULL-terminated. `execvp` returns ONLY on failure (on success the image is replaced and control // never returns here), so reading `errno` afterwards is always valid. unsafe { - execvp(cmd_c.as_ptr() as *const u8, argv_ptrs.as_ptr()); + execvp(cmd_c.as_ptr().cast(), argv_ptrs.as_ptr()); } io_error_to_status(&std::io::Error::last_os_error()) } @@ -11616,10 +11616,10 @@ pub unsafe extern "C" fn align_rt_command_run(c: *mut Command, out: *mut *mut Ru // Marshal the argv pointer vector in the PARENT (the child does no allocation between fork and // exec — the async-signal-safety discipline shared with `spawn`). `argv_ptrs` borrows `cmd.argv`. - let mut argv_ptrs: Vec<*const u8> = cmd.argv.iter().map(|a| a.as_ptr() as *const u8).collect(); + let mut argv_ptrs: Vec<*const u8> = cmd.argv.iter().map(|a| a.as_ptr().cast()).collect(); argv_ptrs.push(core::ptr::null()); - let cmd_ptr = cmd.cmd.as_ptr() as *const u8; - let cwd_ptr = cmd.cwd.as_ref().map_or(core::ptr::null(), |d| d.as_ptr() as *const u8); + let cmd_ptr: *const u8 = cmd.cmd.as_ptr().cast(); + let cwd_ptr: *const u8 = cmd.cwd.as_ref().map_or(core::ptr::null(), |d| d.as_ptr().cast()); // With a timeout, run the child in its OWN process group so the deadline kill reaps the whole // tree it spawns (e.g. `sh -c "sleep 10"`'s `sleep` grandchild), not just the direct child — // otherwise a surviving grandchild holds the capture pipes open and wedges the drain-to-EOF. @@ -11682,7 +11682,7 @@ pub unsafe extern "C" fn align_rt_command_run(c: *mut Command, out: *mut *mut Ru clearenv_portable(); } for (n, v) in &cmd.env { - setenv(n.as_ptr() as *const u8, v.as_ptr() as *const u8, 1); + setenv(n.as_ptr().cast(), v.as_ptr().cast(), 1); } // Redirect stdout/stderr to the pipe write-ends, then close every pipe fd (the read ends // and the now-duplicated write ends). The dup2'd fds 1/2 are NOT CLOEXEC, so they survive