diff --git a/bench/library_boundary/README.md b/bench/library_boundary/README.md index a3815fd5..2c101903 100644 --- a/bench/library_boundary/README.md +++ b/bench/library_boundary/README.md @@ -23,6 +23,9 @@ a 256-function, high-CFG fixture with three expression-valued branches per funct interface bytes, so inference cost and summary-size growth are recorded together. - `import-validation`: semantic-import milliseconds per iteration, including the complete type-shape walk, borrow/growth fixed points, dependency-cycle check, and provenance-root validation. +- `mir-global-type-validation`: whole-program MIR-lowering milliseconds per iteration for a trivial + body plus 512 concrete nominal roots, isolating the global type-domain/root/cycle preflight and + its required metadata copies. - `mir-continuation-lowering`: L2b-a2-ac whole-program MIR-lowering milliseconds per iteration and the fixture's total basic-block count, tracking the O(1) required-child continuation protocol. diff --git a/bench/library_boundary/src/main.rs b/bench/library_boundary/src/main.rs index 5bf4cd75..531efffc 100644 --- a/bench/library_boundary/src/main.rs +++ b/bench/library_boundary/src/main.rs @@ -116,6 +116,15 @@ fn mir_continuation_fixture() -> String { source } +fn mir_global_type_fixture() -> String { + let mut source = String::new(); + for index in 0..512 { + source.push_str(&format!("Record_{index:04} {{ value: i64 }}\n")); + } + source.push_str("fn main() -> i32 = 0\n"); + source +} + fn import_validation_fixture() -> InterfaceSummary { let parameter = ITypeParam { name: "T".to_string(), @@ -232,6 +241,41 @@ fn run_provenance() { let milliseconds = elapsed.as_secs_f64() * 1_000.0 / iterations as f64; println!("import-validation\t{milliseconds:.3}\tms/import"); + let global_type_source = mir_global_type_fixture(); + let mut source_map = align_span::SourceMap::new(); + let checked = align_driver::check( + &mut source_map, + "mir-global-type.align", + &global_type_source, + ); + assert!( + !checked.diags.has_errors(), + "MIR global-type fixture must check" + ); + let mir = align_driver::lower_to_mir(&checked.hir); + assert_eq!( + mir.structs + .iter() + .filter(|definition| definition.source_name.starts_with("Record_")) + .count(), + 512, + "fixture must retain every concrete nominal root" + ); + + let mut iterations = 0_u64; + let start = Instant::now(); + while start.elapsed() < minimum { + let mir = align_driver::lower_to_mir(black_box(&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-global-type-validation\t{milliseconds:.3}\tms/lower\t{}\ttypes", + checked.hir.structs.len() + ); + let continuation_source = mir_continuation_fixture(); let mut source_map = align_span::SourceMap::new(); let checked = align_driver::check( diff --git a/crates/align_mir/src/lib.rs b/crates/align_mir/src/lib.rs index 10e773f8..5ccd1b46 100644 --- a/crates/align_mir/src/lib.rs +++ b/crates/align_mir/src/lib.rs @@ -20,6 +20,10 @@ use std::collections::VecDeque; use std::rc::Rc; pub mod print; +mod validate_hir; + +#[cfg(test)] +mod validate_hir_tests; /// A byte-offset → (line, col) index over every source file, built once from the [`SourceMap`] and /// threaded (via [`Rc`]) into lowering so each MIR statement can record the 1-based source @@ -1537,6 +1541,30 @@ pub fn lower_program_per_unit_located(program: &hir::Program, sm: &SourceMap) -> // base of the deep `lower_fn`/`lower_expr` recursion (`expr_depth` stack margin). #[inline] fn lower_program_impl(program: &hir::Program, lines: Option>, per_unit: bool) -> Program { + if !validate_hir::global_type_metadata_is_valid(program) { + return empty_program(); + } + lower_program_unchecked(program, lines, per_unit) +} + +fn empty_program() -> Program { + Program { + fns: Vec::new(), + externs: Vec::new(), + imported_fns: Vec::new(), + link_libs: Vec::new(), + structs: Vec::new(), + enums: Vec::new(), + tagged_types: Vec::new(), + tuples: Vec::new(), + } +} + +fn lower_program_unchecked( + program: &hir::Program, + lines: Option>, + per_unit: bool, +) -> Program { // Function signature facts are immutable during MIR lowering. Materialize the shared table once // so lowering F functions does not deep-clone all T entries F times. let fn_types: Rc<[hir::FnTy]> = program.fn_types.clone().into(); diff --git a/crates/align_mir/src/validate_hir.rs b/crates/align_mir/src/validate_hir.rs new file mode 100644 index 00000000..3c0384af --- /dev/null +++ b/crates/align_mir/src/validate_hir.rs @@ -0,0 +1,391 @@ +use std::collections::{HashMap, HashSet, VecDeque}; + +use align_sema::{PrimScalar, Scalar, Ty, hir}; + +/// Validate the program-global HIR type domain before MIR construction. +pub(crate) fn global_type_metadata_is_valid(program: &hir::Program) -> bool { + Validator::new(program).validate() +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +enum Node { + Struct(u32), + Enum(u32), + Tuple(u32), + Tagged(u32), + Fn(u32), +} + +#[derive(Clone, Copy)] +enum Edge { + Inline, + Header, +} + +#[derive(Default)] +struct NodeFacts { + refs: Vec<(Node, Edge)>, + has_param: bool, +} + +struct Validator<'a> { + program: &'a hir::Program, + nodes: Vec, + facts: HashMap, +} + +impl<'a> Validator<'a> { + fn new(program: &'a hir::Program) -> Self { + let mut nodes = Vec::with_capacity( + program.structs.len() + + program.enums.len() + + program.tuples.len() + + program.tagged_types.len() + + program.fn_types.len(), + ); + nodes.extend((0..program.structs.len()).map(|id| Node::Struct(id as u32))); + nodes.extend((0..program.enums.len()).map(|id| Node::Enum(id as u32))); + nodes.extend((0..program.tuples.len()).map(|id| Node::Tuple(id as u32))); + nodes.extend((0..program.tagged_types.len()).map(|id| Node::Tagged(id as u32))); + nodes.extend((0..program.fn_types.len()).map(|id| Node::Fn(id as u32))); + Self { + program, + nodes, + facts: HashMap::new(), + } + } + + fn validate(mut self) -> bool { + self.collect_node_facts() + && self.root_types_are_concrete() + && self.inline_graph_is_acyclic() + } + + fn collect_node_facts(&mut self) -> bool { + for node in self.nodes.clone() { + let mut facts = NodeFacts::default(); + let valid = match node { + Node::Struct(id) => self.program.structs[id as usize] + .fields + .iter() + .all(|field| self.inspect_ty(field.ty, Edge::Inline, &mut facts)), + Node::Enum(id) => self.program.enums[id as usize] + .variants + .iter() + .flat_map(|variant| &variant.payload) + .all(|&payload| self.inspect_scalar(payload, Edge::Inline, &mut facts)), + Node::Tuple(id) => self.program.tuples[id as usize] + .elems + .iter() + .all(|&element| self.inspect_scalar(element, Edge::Inline, &mut facts)), + Node::Tagged(id) => match self.program.tagged_types[id as usize] { + hir::TaggedType::Option(payload) => { + self.inspect_scalar(payload, Edge::Inline, &mut facts) + } + hir::TaggedType::Result(ok, err) => { + self.inspect_scalar(ok, Edge::Inline, &mut facts) + && self.inspect_scalar(err, Edge::Inline, &mut facts) + } + }, + Node::Fn(id) => { + let function = &self.program.fn_types[id as usize]; + function.params.iter().all(|(_, parameter)| { + self.inspect_scalar(*parameter, Edge::Header, &mut facts) + }) && self.inspect_ty(function.ret, Edge::Header, &mut facts) + } + }; + if !valid { + return false; + } + self.facts.insert(node, facts); + } + true + } + + fn inspect_ty(&self, ty: Ty, edge: Edge, facts: &mut NodeFacts) -> bool { + match ty { + Ty::Int(integer) => valid_int(integer.bits), + Ty::Float(float) => valid_float(float.bits), + Ty::Param(_) => { + facts.has_param = true; + true + } + Ty::IntVar(_) | Ty::FloatVar(_) | Ty::Error | Ty::StrFinder => false, + Ty::Option(payload) | Ty::Array(payload, _) => { + self.inspect_scalar(payload, edge, facts) + } + Ty::Result(ok, err) => { + self.inspect_scalar(ok, edge, facts) && self.inspect_scalar(err, edge, facts) + } + Ty::Vec(element, lanes) | Ty::Mask(element, lanes) => { + matches!(lanes, 2 | 4 | 8 | 16) + && matches!(element, Scalar::Int(_) | Scalar::Float(_)) + && self.inspect_scalar(element, edge, facts) + } + Ty::Tagged(id) => self.push_ref(Node::Tagged(id), edge, facts), + Ty::Box(payload) + | Ty::Slice(payload) + | Ty::DynArray(payload) + | Ty::ArrayBuilder(payload) + | Ty::Task(payload) => self.inspect_scalar(payload, Edge::Header, facts), + Ty::StructArray(id, _) => self.push_ref(Node::Struct(id), edge, facts), + Ty::DynStructArray(id, _) | Ty::Soa(id) | Ty::JsonScanner(id) => { + self.push_ref(Node::Struct(id), Edge::Header, facts) + } + Ty::DynSliceArray(element) => valid_prim(element), + Ty::Struct(id) => self.push_ref(Node::Struct(id), edge, facts), + Ty::Tuple(id) => self.push_ref(Node::Tuple(id), edge, facts), + Ty::Fn(id) => self.push_ref(Node::Fn(id), Edge::Header, facts), + Ty::Enum(id) => self.push_ref(Node::Enum(id), edge, facts), + Ty::DictEncoded(id, field) => { + let Some(definition) = self.program.structs.get(id as usize) else { + return false; + }; + definition + .fields + .get(field as usize) + .is_some_and(|definition| definition.ty == Ty::Str) + && self.push_ref(Node::Struct(id), Edge::Header, facts) + } + Ty::Bool + | Ty::Char + | Ty::DynResponseArray + | Ty::Str + | Ty::String + | Ty::ArenaHandle + | Ty::Raw + | Ty::Builder + | Ty::Writer + | Ty::Reader + | Ty::Buffer + | 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::Unit => true, + } + } + + fn inspect_scalar(&self, scalar: Scalar, edge: Edge, facts: &mut NodeFacts) -> bool { + match scalar { + Scalar::Int(integer) => valid_int(integer.bits), + Scalar::Float(float) => valid_float(float.bits), + Scalar::Param(_) => { + facts.has_param = true; + true + } + Scalar::Struct(id) => self.push_ref(Node::Struct(id), edge, facts), + Scalar::Enum(id) => self.push_ref(Node::Enum(id), edge, facts), + Scalar::Tagged(id) => self.push_ref(Node::Tagged(id), edge, facts), + Scalar::Fn(id) => self.push_ref(Node::Fn(id), Edge::Header, facts), + Scalar::DynStructArray(id) | Scalar::Soa(id) => { + self.push_ref(Node::Struct(id), Edge::Header, facts) + } + Scalar::DynArray(element) | Scalar::Slice(element) => valid_prim(element), + Scalar::Bool + | Scalar::Char + | Scalar::Unit + | Scalar::String + | Scalar::DynResponseArray + | Scalar::Str + | 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 => true, + } + } + + fn push_ref(&self, node: Node, edge: Edge, facts: &mut NodeFacts) -> bool { + if !self.node_exists(node) { + return false; + } + facts.refs.push((node, edge)); + true + } + + fn node_exists(&self, node: Node) -> bool { + match node { + Node::Struct(id) => self.program.structs.get(id as usize).is_some(), + Node::Enum(id) => self.program.enums.get(id as usize).is_some(), + Node::Tuple(id) => self.program.tuples.get(id as usize).is_some(), + Node::Tagged(id) => self.program.tagged_types.get(id as usize).is_some(), + Node::Fn(id) => self.program.fn_types.get(id as usize).is_some(), + } + } + + fn root_types_are_concrete(&self) -> bool { + let mut roots = Vec::new(); + roots.extend((0..self.program.structs.len()).map(|id| Node::Struct(id as u32))); + roots.extend((0..self.program.enums.len()).map(|id| Node::Enum(id as u32))); + roots.extend((0..self.program.tuples.len()).map(|id| Node::Tuple(id as u32))); + + for function in &self.program.fns { + for local in &function.locals { + if !self.inspect_root_ty(local.ty, &mut roots) { + return false; + } + } + if !self.inspect_root_ty(function.ret, &mut roots) { + return false; + } + } + for function in &self.program.externs { + for ¶meter in &function.params { + if !self.inspect_root_ty(parameter, &mut roots) { + return false; + } + } + if !self.inspect_root_ty(function.ret, &mut roots) { + return false; + } + } + for function in &self.program.imported_fns { + for ¶meter in &function.params { + if !self.inspect_root_ty(parameter, &mut roots) { + return false; + } + } + if !self.inspect_root_ty(function.ret, &mut roots) { + return false; + } + } + + let mut reverse = HashMap::>::new(); + let mut templates = VecDeque::new(); + let mut template_nodes = HashSet::new(); + for (&node, facts) in &self.facts { + if facts.has_param && template_nodes.insert(node) { + templates.push_back(node); + } + for &(dependency, _) in &facts.refs { + reverse.entry(dependency).or_default().push(node); + } + } + while let Some(node) = templates.pop_front() { + for &dependent in reverse.get(&node).into_iter().flatten() { + if template_nodes.insert(dependent) { + templates.push_back(dependent); + } + } + } + roots.extend( + self.nodes + .iter() + .copied() + .filter(|node| !template_nodes.contains(node)), + ); + + let mut reachable = HashSet::new(); + let mut queue: VecDeque<_> = roots.into(); + while let Some(node) = queue.pop_front() { + if !reachable.insert(node) { + continue; + } + if self.facts[&node].has_param { + return false; + } + queue.extend( + self.facts[&node] + .refs + .iter() + .map(|&(dependency, _)| dependency), + ); + } + reachable.is_disjoint(&template_nodes) + } + + fn inspect_root_ty(&self, ty: Ty, roots: &mut Vec) -> bool { + let mut facts = NodeFacts::default(); + if !self.inspect_ty(ty, Edge::Inline, &mut facts) || facts.has_param { + return false; + } + roots.extend(facts.refs.into_iter().map(|(node, _)| node)); + true + } + + fn inline_graph_is_acyclic(&self) -> bool { + #[derive(Clone, Copy)] + enum Work { + Enter(Node), + Exit(Node), + } + + let mut complete = HashSet::new(); + let mut active = HashSet::new(); + for &root in &self.nodes { + let mut work = vec![Work::Enter(root)]; + while let Some(item) = work.pop() { + match item { + Work::Enter(node) => { + if complete.contains(&node) { + continue; + } + if !active.insert(node) { + return false; + } + work.push(Work::Exit(node)); + for &(dependency, edge) in self.facts[&node].refs.iter().rev() { + if matches!(edge, Edge::Inline) { + work.push(Work::Enter(dependency)); + } + } + } + Work::Exit(node) => { + if !active.remove(&node) { + return false; + } + complete.insert(node); + } + } + } + } + true + } +} + +fn valid_int(bits: u8) -> bool { + matches!(bits, 8 | 16 | 32 | 64) +} + +fn valid_float(bits: u8) -> bool { + matches!(bits, 32 | 64) +} + +fn valid_prim(primitive: PrimScalar) -> bool { + match primitive { + PrimScalar::Int(integer) => valid_int(integer.bits), + PrimScalar::Float(float) => valid_float(float.bits), + PrimScalar::Bool | PrimScalar::Char | PrimScalar::Str | PrimScalar::String => true, + } +} diff --git a/crates/align_mir/src/validate_hir_tests.rs b/crates/align_mir/src/validate_hir_tests.rs new file mode 100644 index 00000000..71faa438 --- /dev/null +++ b/crates/align_mir/src/validate_hir_tests.rs @@ -0,0 +1,480 @@ +use super::*; +use align_sema::{ + FloatTy, FnEffect, IntTy, Layout, PrimScalar, Scalar, Ty, + hir::{ + self, EnumDef, EnumVariant, FieldDef, FnTy, ImportedFn, ReturnBorrowSummary, + ReturnRegionSummary, StructDef, TaggedType, TupleDef, + }, +}; +use std::cell::Cell; + +fn int(bits: u8) -> Ty { + Ty::Int(IntTy { bits, signed: true }) +} + +fn scalar_int(bits: u8) -> Scalar { + Scalar::Int(IntTy { bits, signed: true }) +} + +fn fn_type(ret: Ty) -> FnTy { + FnTy { + params: Vec::new(), + ret, + return_borrow: ReturnBorrowSummary::None, + return_region: ReturnRegionSummary::None, + effect: Cell::new(FnEffect::Pure), + } +} + +fn baseline_program() -> hir::Program { + hir::Program { + fns: Vec::new(), + externs: Vec::new(), + link_libs: Vec::new(), + structs: vec![StructDef { + name: "Record".to_string(), + source_name: "Record".to_string(), + fields: vec![ + FieldDef { + name: "key".to_string(), + ty: Ty::Str, + }, + FieldDef { + name: "value".to_string(), + ty: int(64), + }, + ], + align: None, + c_repr: false, + }], + enums: vec![EnumDef { + name: "Choice".to_string(), + source_name: "Choice".to_string(), + variants: vec![ + EnumVariant { + name: "Empty".to_string(), + payload: Vec::new(), + field_base: 1, + }, + EnumVariant { + name: "Value".to_string(), + payload: vec![scalar_int(64)], + field_base: 1, + }, + ], + }], + tagged_types: vec![TaggedType::Option(scalar_int(64))], + tuples: vec![TupleDef { + elems: vec![scalar_int(64), Scalar::Bool], + }], + fn_types: vec![fn_type(Ty::Unit)], + imported_fns: Vec::new(), + } +} + +fn with_return(ty: Ty) -> hir::Program { + let mut program = baseline_program(); + program.imported_fns.push(ImportedFn { + name: "dep$value".to_string(), + params: Vec::new(), + param_modes: Vec::new(), + ret: ty, + return_borrow: ReturnBorrowSummary::None, + return_region: ReturnRegionSummary::None, + }); + program +} + +fn is_empty(program: &Program) -> bool { + program.fns.is_empty() + && program.externs.is_empty() + && program.imported_fns.is_empty() + && program.link_libs.is_empty() + && program.structs.is_empty() + && program.enums.is_empty() + && program.tagged_types.is_empty() + && program.tuples.is_empty() +} + +fn assert_rejected(label: &str, program: &hir::Program) { + assert!( + !validate_hir::global_type_metadata_is_valid(program), + "{label}: validator accepted malformed metadata" + ); + 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}: an entrypoint published partial MIR" + ); + } +} + +fn assert_accepted(label: &str, program: &hir::Program) { + assert!( + validate_hir::global_type_metadata_is_valid(program), + "{label}: validator rejected valid metadata" + ); + 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}: valid metadata did not reach an entrypoint" + ); + } +} + +#[test] +fn malformed_hir_global_type_metadata_fails_closed() { + for (label, ty) in [ + ("param", Ty::Param(0)), + ("int-var", Ty::IntVar(0)), + ("float-var", Ty::FloatVar(0)), + ("str-finder", Ty::StrFinder), + ("error", Ty::Error), + ("scalar-param", Ty::Option(Scalar::Param(0))), + ("missing-struct", Ty::Struct(99)), + ("missing-enum", Ty::Enum(99)), + ("missing-tuple", Ty::Tuple(99)), + ("missing-tagged", Ty::Tagged(99)), + ("missing-function-type", Ty::Fn(99)), + ("missing-struct-array", Ty::StructArray(99, 1)), + ( + "missing-dynamic-struct-array", + Ty::DynStructArray(99, Layout::Aos), + ), + ("missing-soa", Ty::Soa(99)), + ("missing-scanner", Ty::JsonScanner(99)), + ("missing-dictionary", Ty::DictEncoded(99, 0)), + ("missing-dictionary-field", Ty::DictEncoded(0, 99)), + ("non-string-dictionary-field", Ty::DictEncoded(0, 1)), + ] { + assert_rejected(label, &with_return(ty)); + } + + for bits in [0, 7, 24, 128] { + assert_rejected("integer-width", &with_return(int(bits))); + assert_rejected( + "scalar-integer-width", + &with_return(Ty::Option(scalar_int(bits))), + ); + assert_rejected( + "primitive-integer-width", + &with_return(Ty::DynSliceArray(PrimScalar::Int(IntTy { + bits, + signed: false, + }))), + ); + } + for bits in [0, 16, 128] { + let float = FloatTy { bits }; + assert_rejected("float-width", &with_return(Ty::Float(float))); + assert_rejected( + "scalar-float-width", + &with_return(Ty::Option(Scalar::Float(float))), + ); + assert_rejected( + "primitive-float-width", + &with_return(Ty::DynSliceArray(PrimScalar::Float(float))), + ); + } + for lanes in [0, 1, 3, 32] { + assert_rejected("vector-lanes", &with_return(Ty::Vec(scalar_int(32), lanes))); + assert_rejected("mask-lanes", &with_return(Ty::Mask(scalar_int(32), lanes))); + } + assert_rejected("vector-element", &with_return(Ty::Vec(Scalar::Bool, 4))); + assert_rejected("mask-element", &with_return(Ty::Mask(Scalar::Str, 4))); + + let mut inline_cycle = baseline_program(); + inline_cycle.structs[0].fields[0].ty = Ty::Struct(0); + assert_rejected("inline-cycle", &inline_cycle); + + let mut concrete_unused_tagged = baseline_program(); + concrete_unused_tagged.tagged_types[0] = TaggedType::Option(Scalar::Struct(99)); + assert_rejected("unused-concrete-tagged", &concrete_unused_tagged); + + let mut concrete_unused_fn = baseline_program(); + concrete_unused_fn.fn_types[0] = fn_type(Ty::Struct(99)); + assert_rejected("unused-concrete-function-type", &concrete_unused_fn); + + let mut broken_template_tagged = baseline_program(); + broken_template_tagged.tagged_types[0] = + TaggedType::Result(Scalar::Param(0), Scalar::Struct(99)); + assert_rejected("template-tagged-bad-reference", &broken_template_tagged); + + let mut reachable_template_tagged = baseline_program(); + reachable_template_tagged.tagged_types[0] = TaggedType::Option(Scalar::Param(0)); + reachable_template_tagged.imported_fns.push(ImportedFn { + ret: Ty::Tagged(0), + ..with_return(Ty::Unit).imported_fns.remove(0) + }); + assert_rejected("reachable-template-tagged", &reachable_template_tagged); + + let mut reachable_template_fn = baseline_program(); + reachable_template_fn.fn_types[0] = fn_type(Ty::Param(0)); + reachable_template_fn.imported_fns.push(ImportedFn { + ret: Ty::Fn(0), + ..with_return(Ty::Unit).imported_fns.remove(0) + }); + assert_rejected("reachable-template-function-type", &reachable_template_fn); + + for invalid_index in 0..3 { + let mut program = baseline_program(); + program.structs = (0..3) + .map(|index| StructDef { + name: format!("Root{index}"), + source_name: format!("Root{index}"), + fields: vec![FieldDef { + name: "value".to_string(), + ty: int(64), + }], + align: None, + c_repr: false, + }) + .collect(); + program.structs[invalid_index].fields[0].ty = int(7); + assert_rejected("first-middle-final-concrete-root", &program); + } +} + +#[test] +fn valid_hir_global_type_preflight_is_mir_identity() { + for bits in [8, 16, 32, 64] { + assert_accepted("integer-width", &with_return(int(bits))); + assert_accepted( + "scalar-integer-width", + &with_return(Ty::Option(scalar_int(bits))), + ); + } + for bits in [32, 64] { + assert_accepted("float-width", &with_return(Ty::Float(FloatTy { bits }))); + } + for lanes in [2, 4, 8, 16] { + assert_accepted("vector-lanes", &with_return(Ty::Vec(scalar_int(32), lanes))); + assert_accepted("mask-lanes", &with_return(Ty::Mask(scalar_int(32), lanes))); + } + + for (label, ty) in [ + ("struct", Ty::Struct(0)), + ("enum", Ty::Enum(0)), + ("tuple", Ty::Tuple(0)), + ("tagged", Ty::Tagged(0)), + ("function-type", Ty::Fn(0)), + ("struct-array", Ty::StructArray(0, 0)), + ( + "dynamic-struct-array-aos", + Ty::DynStructArray(0, Layout::Aos), + ), + ( + "dynamic-struct-array-soa", + Ty::DynStructArray(0, Layout::Soa), + ), + ("soa", Ty::Soa(0)), + ("scanner", Ty::JsonScanner(0)), + ("dictionary", Ty::DictEncoded(0, 0)), + ] { + assert_accepted(label, &with_return(ty)); + } + + let valid_leaf_types = [ + Ty::Bool, + Ty::Char, + Ty::DynResponseArray, + Ty::Str, + Ty::String, + Ty::ArenaHandle, + Ty::Raw, + Ty::Builder, + Ty::Writer, + Ty::Reader, + Ty::Buffer, + 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::Unit, + ]; + for ty in valid_leaf_types { + if ty != Ty::StrFinder { + assert_accepted("leaf-type", &with_return(ty)); + } + } + + let valid_scalars = [ + scalar_int(32), + Scalar::Float(FloatTy { bits: 64 }), + Scalar::Bool, + Scalar::Char, + Scalar::Unit, + Scalar::Struct(0), + Scalar::String, + Scalar::DynArray(PrimScalar::Int(IntTy { + bits: 32, + signed: true, + })), + Scalar::DynStructArray(0), + Scalar::DynResponseArray, + Scalar::Str, + Scalar::Slice(PrimScalar::Str), + 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), + ]; + for scalar in valid_scalars { + assert_accepted("scalar-discriminator", &with_return(Ty::Option(scalar))); + } + + for primitive in [ + PrimScalar::Int(IntTy { + bits: 8, + signed: false, + }), + PrimScalar::Float(FloatTy { bits: 32 }), + PrimScalar::Bool, + PrimScalar::Char, + PrimScalar::Str, + PrimScalar::String, + ] { + assert_accepted( + "primitive-discriminator", + &with_return(Ty::DynSliceArray(primitive)), + ); + } + + for ty in [ + Ty::Option(scalar_int(32)), + Ty::Result(Scalar::Bool, Scalar::String), + Ty::Box(Scalar::Struct(0)), + Ty::Array(Scalar::Bool, 0), + Ty::Slice(Scalar::Str), + Ty::DynArray(Scalar::String), + Ty::ArrayBuilder(Scalar::String), + Ty::Task(Scalar::Struct(0)), + ] { + assert_accepted("wrapper-type", &with_return(ty)); + } + + for (label, ty) in [ + ("box-header-cycle", Ty::Box(Scalar::Struct(0))), + ( + "dynamic-array-header-cycle", + Ty::DynArray(Scalar::Struct(0)), + ), + ("task-header-cycle", Ty::Task(Scalar::Struct(0))), + ] { + let mut wrapper_cycle = baseline_program(); + wrapper_cycle.structs[0].fields[0].ty = ty; + assert_accepted(label, &wrapper_cycle); + } + + let mut function_header_cycle = baseline_program(); + function_header_cycle.structs[0].fields[0].ty = Ty::Fn(0); + function_header_cycle.fn_types[0] = fn_type(Ty::Struct(0)); + assert_accepted("function-header-cycle", &function_header_cycle); + + let mut abstract_tagged = baseline_program(); + abstract_tagged.tagged_types[0] = TaggedType::Option(Scalar::Param(0)); + assert_accepted("unreachable-template-tagged", &abstract_tagged); + let mut abstract_fn = baseline_program(); + abstract_fn.fn_types[0] = fn_type(Ty::Param(0)); + assert_accepted("unreachable-template-function-type", &abstract_fn); + + let mut deep_graph = baseline_program(); + deep_graph.structs = (0..4_096) + .map(|index| StructDef { + name: format!("Deep{index}"), + source_name: format!("Deep{index}"), + fields: vec![FieldDef { + name: "next".to_string(), + ty: if index == 4_095 { + int(64) + } else { + Ty::Struct(index + 1) + }, + }], + align: None, + c_repr: false, + }) + .collect(); + assert_accepted("iterative-deep-type-graph", &deep_graph); + deep_graph.structs[4_095].fields[0].ty = Ty::Struct(0); + assert_rejected("iterative-deep-type-cycle", &deep_graph); + + let program = baseline_program(); + let source_map = SourceMap::new(); + for (checked, unchecked) in [ + ( + lower_program(&program), + lower_program_unchecked(&program, None, false), + ), + ( + lower_program_located(&program, &source_map), + lower_program_unchecked( + &program, + Some(Rc::new(SourceLines::from_map(&source_map))), + false, + ), + ), + ( + lower_program_per_unit(&program), + lower_program_unchecked(&program, None, true), + ), + ( + lower_program_per_unit_located(&program, &source_map), + lower_program_unchecked( + &program, + Some(Rc::new(SourceLines::from_map(&source_map))), + true, + ), + ), + ] { + assert_eq!(format!("{checked:#?}"), format!("{unchecked:#?}")); + } +}