diff --git a/HANDOFF.md b/HANDOFF.md index 25386365..fd99cb6d 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -6,11 +6,16 @@ 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._ The C-B borrow/ownership capability is complete -through L2e. Direct, captured, imported, and function-value returns preserve -exact owner provenance; recursively Move returns carry a path-selected cleanup -bit; and shared/exclusive parameters preserve caller ownership, replacement, -generation invalidation, and whole/per-unit ABI parity. No public `pkg.db` -surface exists yet. +through L2e, F-A native resources is complete through L3, and F-B explicit +region materialization is complete through L4 and L6. Direct, captured, +imported, and function-value returns preserve exact owner provenance; +recursively Move returns carry a path-selected cleanup bit; shared/exclusive +parameters preserve caller ownership, replacement, generation invalidation, +and whole/per-unit ABI parity; package-defined native resources have nominal +identity, checked refs/views, producer-owned cleanup thunks, and exactly-once +Drop; and named regions now support explicit recursive cloning plus chunked +`RegionPlain` array construction without a hidden heap vector. No public +`pkg.db` surface exists yet. The remaining compiler plan uses consumer-complete capability waves rather than one PR per dormant acceptance cell: @@ -18,10 +23,10 @@ than one PR per dormant acceptance cell: ```text C-A canonical callable closure complete through c3 C-B borrow/ownership closure complete through L2e +F-A native resources complete through L3 +F-B region materialization complete through L4 + L6 next independent waves: -F-A native resources L3 -F-B region materialization L4 + L6 F-C static artifacts L5 after F-A/F-B, while also waiting for F-C: diff --git a/crates/align_ast/src/lib.rs b/crates/align_ast/src/lib.rs index 529994b3..847c0549 100644 --- a/crates/align_ast/src/lib.rs +++ b/crates/align_ast/src/lib.rs @@ -408,6 +408,9 @@ pub enum ExprKind { Loop(Block), /// `arena { ... }` — a region whose allocations are freed in bulk at block end. Arena(Block), + /// `arena name { ... }` — the same region with an explicit, scope-limited `region` + /// capability bound as `name` for allocation in ordinary callees. + NamedArena { name: Ident, block: Block }, /// `unsafe { ... }` — a block in which `raw.*` operations (raw allocation, unchecked casts, /// manual free) are permitted. A plain marker block otherwise (no runtime effect); a function /// containing one is inferred impure (so it can never be a `par_map` callee). diff --git a/crates/align_codegen_llvm/src/lib.rs b/crates/align_codegen_llvm/src/lib.rs index 209f8ac1..3c85243b 100644 --- a/crates/align_codegen_llvm/src/lib.rs +++ b/crates/align_codegen_llvm/src/lib.rs @@ -32,7 +32,7 @@ use align_mir::{ ParallelStageId, Program, ProgramCall, RuntimeKey, Rvalue, Slot, Stmt, Term, ValueId, }; use align_sema::{ - DropPlan, ERROR_VARIANT_CODE, EnumDef, FloatTy, IntTy, Layout, Scalar, StructDef, TupleDef, Ty, + ArrayBuilderElem, 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, }; @@ -58,7 +58,10 @@ use inkwell::targets::{ use inkwell::types::{ BasicMetadataTypeEnum, BasicType, BasicTypeEnum, FloatType, FunctionType, IntType, StructType, }; -use inkwell::values::{BasicMetadataValueEnum, BasicValue, BasicValueEnum, FunctionValue, IntValue}; +use inkwell::values::{ + ArrayValue, BasicMetadataValueEnum, BasicValue, BasicValueEnum, FunctionValue, IntValue, + PointerValue, StructValue, +}; pub fn is_available() -> bool { true @@ -2388,8 +2391,39 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { Ty::Box(payload) | Ty::Slice(payload) | Ty::DynArray(payload) - | Ty::ArrayBuilder(payload) | Ty::Task(payload) => self.check_scalar_reference(payload)?, + Ty::ArrayBuilder(payload) => { + self.check_scalar_reference(payload)? + } + ty @ (Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..) + | Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => { + let payload = ty + .array_builder_element() + .and_then(|element| match element { + ArrayBuilderElem::Aggregate(element) => Some(element), + ArrayBuilderElem::Scalar(_) => None, + }) + .or_else(|| ty.dyn_aggregate_array_element()) + .expect("matched aggregate type"); + if !align_sema::region_plain_type_ok( + payload.ty(), + &self.program.structs, + &self.program.enums, + &self.program.tagged_types, + ) { + return Err(Self::invalid(format!( + "aggregate array element {:?} is not RegionPlain", + payload.ty() + ))); + } + work.push(TypeGraphWork::Ty(payload.ty())); + } Ty::DynStructArray(id, _) | Ty::Soa(id) | Ty::JsonScanner(id) => self.require_struct(id)?, @@ -2584,6 +2618,28 @@ fn validate_tagged_program(program: &Program) -> Result<(), CodegenError> { key.push('D'); work.push(SourceAbiKeyWork::Ty(scalar_to_ty(payload))); } + Ty::ArrayBuilder(element) => { + key.push_str("AB_"); + work.push(SourceAbiKeyWork::Ty(scalar_to_ty(element))); + } + ty @ (Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..)) => { + key.push_str("AB_"); + work.push(SourceAbiKeyWork::Ty( + ty.array_builder_element().expect("matched aggregate builder").ty(), + )); + } + ty @ (Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => { + key.push_str("DA_"); + work.push(SourceAbiKeyWork::Ty( + ty.dyn_aggregate_array_element().expect("matched aggregate array").ty(), + )); + } Ty::Array(payload, count) => { key.push_str(&format!("A{count}_")); work.push(SourceAbiKeyWork::Ty(scalar_to_ty(payload))); @@ -4233,7 +4289,9 @@ fn scalar_type<'c>( // array views) lowers to the slice struct. // A `{ptr,len}` payload (an owned `string` in an Option/Result, slice 8a; also str/slice/ // array views) lowers to the slice struct. A `json.doc` is a `{tape,node}` = `{ptr,i64}` too. - Ty::Str | Ty::String | Ty::Slice(_) | Ty::Soa(_) | Ty::JsonDoc | Ty::JsonScanner(_) | Ty::DynArray(_) => slice_struct_type(ctx).into(), + Ty::Str | Ty::String | Ty::Slice(_) | Ty::Soa(_) | Ty::JsonDoc | Ty::JsonScanner(_) | Ty::DynArray(_) + | Ty::DynVecArray(..) | Ty::DynMaskArray(..) | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..) => slice_struct_type(ctx).into(), // An AoS struct array is a `{ptr,len}` view too; an SoA one would be a different // representation (column buffers), so match the layout — `Layout::Soa` (M6) makes this // arm go non-exhaustive (a compile error pointing exactly here). @@ -4365,6 +4423,10 @@ fn abi_type<'c>( | Ty::Reader | Ty::Buffer | Ty::ArrayBuilder(_) + | Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..) | Ty::Regex | Ty::Captures | Ty::TcpConn @@ -4377,7 +4439,9 @@ fn abi_type<'c>( // A function value is a closure `{fn_ptr, env_ptr}` here too — matching `llvm_type`, so an // `Ty::Fn` in an ABI position (later: fn-typed parameters/returns) is not silently `i32`. Ty::Fn(_) => closure_struct_type(ctx).into(), - Ty::Slice(_) | Ty::Soa(_) | Ty::JsonDoc | Ty::JsonScanner(_) | Ty::Str | Ty::String | Ty::DynArray(_) => slice_struct_type(ctx).into(), + Ty::Slice(_) | Ty::Soa(_) | Ty::JsonDoc | Ty::JsonScanner(_) | Ty::Str | Ty::String | Ty::DynArray(_) + | Ty::DynVecArray(..) | Ty::DynMaskArray(..) | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..) => slice_struct_type(ctx).into(), // AoS struct array = `{ptr,len}`; SoA (M6) differs → match the layout (forces revisit). Ty::DynStructArray(_, Layout::Aos) | Ty::DynSliceArray(_) | Ty::DynResponseArray => { slice_struct_type(ctx).into() @@ -5442,6 +5506,12 @@ fn apply_size_attrs<'c>(ctx: &'c Context, module: &Module<'c>, profile: Profile) } } +enum CloneInWork<'c> { + Visit(BasicValueEnum<'c>, Ty), + RebuildStruct { base: StructValue<'c>, fields: Vec }, + RebuildArray { base: ArrayValue<'c>, elements: u32 }, +} + struct FnGen<'c, 'a> { ctx: &'c Context, module: &'a Module<'c>, @@ -5525,7 +5595,7 @@ struct ParMapFunctionSignature { } fn is_builder_header_ty(ty: Ty) -> bool { - matches!(ty, Ty::Builder | Ty::ArrayBuilder(_)) + ty == Ty::Builder || ty.is_array_builder() } #[derive(Default)] @@ -5548,7 +5618,8 @@ fn stack_header_plan(f: &Function) -> StackHeaderPlan { for stmt in &block.stmts { if let Stmt::Let(v, rv) = stmt { match rv { - Rvalue::BuilderNew { .. } | Rvalue::ArrayBuilderNew { .. } => { + Rvalue::BuilderNew { .. } + | Rvalue::ArrayBuilderNew { region: None, .. } => { new_defs.insert(*v, f.value_tys[*v as usize]); } Rvalue::Load(slot) if is_builder_header_ty(f.slots[*slot as usize]) => { @@ -7086,7 +7157,7 @@ impl<'c, 'a> FnGen<'c, 'a> { // an owned payload zeroes the whole aggregate (so its payload reads {null,0}); // the owned `{ptr,len}` collections store `{null, 0}`. let ty = self.f.slots[*slot as usize]; - let z: BasicValueEnum = if matches!(ty, Ty::Builder | Ty::StrFinder | Ty::Writer | Ty::Reader | Ty::Buffer | Ty::ArrayBuilder(_) | Ty::Regex | Ty::Captures | Ty::CliCommand | Ty::CliParsed | Ty::TcpConn | Ty::TcpListener | Ty::UdpSocket | Ty::Child | Ty::File | Ty::HttpRequest | Ty::HttpResponse | Ty::HttpClient | Ty::HttpServer | Ty::HttpRequestCtx | Ty::ResponseBuilder | Ty::HttpStream | Ty::Command | Ty::RunOutput | Ty::Resource(_)) { + let z: BasicValueEnum = if ty.is_array_builder() || matches!(ty, Ty::Builder | Ty::StrFinder | Ty::Writer | Ty::Reader | Ty::Buffer | Ty::Regex | Ty::Captures | Ty::CliCommand | Ty::CliParsed | Ty::TcpConn | Ty::TcpListener | Ty::UdpSocket | Ty::Child | Ty::File | Ty::HttpRequest | Ty::HttpResponse | Ty::HttpClient | Ty::HttpServer | Ty::HttpRequestCtx | Ty::ResponseBuilder | Ty::HttpStream | Ty::Command | Ty::RunOutput | Ty::Resource(_)) { // A builder / writer / reader / buffer / cli / tcp_conn / tcp_listener / udp_socket handle slot holds a bare (nullable) handle pointer. self.ctx.ptr_type(AddressSpace::default()).const_null().into() } else if matches!(ty, Ty::StructArray(..)) { @@ -7200,16 +7271,18 @@ impl<'c, 'a> FnGen<'c, 'a> { self.builder .build_call(self.runtime(RuntimeKey::StrFinderFree), &[p.into()], "") .map_err(|e| self.err(e))?; - } else if let Ty::ArrayBuilder(elem) = ty { + } else if let Some(elem) = ty.array_builder_element() { // An unfrozen `array_builder`: free its storage + header. A `string` element // builder deep-frees each pushed-not-frozen string first (the same // `free_string_array`-class helper); a scalar builder frees the flat storage. // Both are null-safe (a moved-out / never-grown slot drops harmlessly — the // slot was nulled at `build`'s move site). let stack = self.stack_header_slots.contains(slot); - let free_key = if elem == align_sema::Scalar::String && stack { + let string_elem = + elem == ArrayBuilderElem::Scalar(align_sema::Scalar::String); + let free_key = if string_elem && stack { RuntimeKey::ArrayBuilderFreeStringsStack - } else if elem == align_sema::Scalar::String { + } else if string_elem { RuntimeKey::ArrayBuilderFreeStrings } else if stack { RuntimeKey::ArrayBuilderFreeStack @@ -8373,13 +8446,7 @@ impl<'c, 'a> FnGen<'c, 'a> { } Rvalue::Index(slot, idx) => { let ep = self.elem_ptr(*slot, idx)?; - let ty = scalar_type( - self.ctx, - result_ty, - self.struct_types, - self.enum_types, - self.tagged_types, - ); + let ty = self.llvm_type(result_ty); self.builder .build_load(ty, ep, "idx") .map_err(|e| self.err(e))? @@ -9392,6 +9459,12 @@ impl<'c, 'a> FnGen<'c, 'a> { .basic() .expect("str_clone returns a {ptr,len}") } + Rvalue::CloneIn { value, handle } => { + let ty = self.f.operand_ty(value); + let value = self.operand(value)?; + let handle = self.operand(handle)?.into_pointer_value(); + self.clone_in_value(value, ty, handle)? + } Rvalue::StrTrim { kind, recv } => { // Extract the receiver `{ptr,len}` and call the trim; the runtime returns a sub-view // `{ptr,len}` aliasing the same bytes (no allocation). @@ -10962,13 +11035,7 @@ impl<'c, 'a> FnGen<'c, 'a> { .build_extract_value(agg, 0, "ptr") .map_err(|e| self.err(e))? .into_pointer_value(); - let ty = scalar_type( - self.ctx, - result_ty, - self.struct_types, - self.enum_types, - self.tagged_types, - ); + let ty = self.llvm_type(result_ty); let index = self.operand(idx)?.into_int_value(); let ep = unsafe { self.builder @@ -11494,6 +11561,10 @@ impl<'c, 'a> FnGen<'c, 'a> { | Ty::Reader | Ty::Buffer | Ty::ArrayBuilder(_) + | Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..) | Ty::Regex | Ty::Captures | Ty::TcpConn @@ -11516,7 +11587,9 @@ impl<'c, 'a> FnGen<'c, 'a> { .array_type(n) .into(), Ty::StructArray(id, n) => self.struct_types[id as usize].array_type(n).into(), - Ty::Slice(_) | Ty::Soa(_) | Ty::Str | Ty::String | Ty::DynArray(_) => slice_struct_type(self.ctx).into(), + Ty::Slice(_) | Ty::Soa(_) | Ty::Str | Ty::String | Ty::DynArray(_) + | Ty::DynVecArray(..) | Ty::DynMaskArray(..) | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..) => slice_struct_type(self.ctx).into(), // AoS struct array = `{ptr,len}`; SoA (M6) differs → match the layout (forces revisit). Ty::DynStructArray(_, Layout::Aos) | Ty::DynSliceArray(_) | Ty::DynResponseArray => slice_struct_type(self.ctx).into(), Ty::DictEncoded(..) => dictenc_struct_type(self.ctx).into(), @@ -11532,6 +11605,310 @@ impl<'c, 'a> FnGen<'c, 'a> { } } + /// Clone every view-bearing leaf of a checked `RegionPlain` value into `handle`. The explicit + /// worklist is deliberate: nominal graphs and nested tagged values can be deep, and backend + /// construction must not recurse on the host stack. Aggregate tags and non-view fields are + /// preserved byte-for-value; inactive tagged payloads start zeroed and remain semantically + /// unavailable. + fn clone_in_value( + &mut self, + value: BasicValueEnum<'c>, + ty: Ty, + handle: PointerValue<'c>, + ) -> Result, CodegenError> { + let mut work = vec![CloneInWork::Visit(value, ty)]; + let mut values = Vec::new(); + while let Some(item) = work.pop() { + match item { + CloneInWork::Visit(value, ty) => match ty { + Ty::Str + | Ty::Slice(Scalar::Int(IntTy { + bits: 8, + signed: false, + })) => values.push(self.clone_in_view(value, handle)?), + Ty::Struct(id) => { + let BasicValueEnum::StructValue(base) = value else { + return Err(self.err("clone_in RegionPlain struct has a non-struct LLVM value")); + }; + let definition = self + .structs + .get(id as usize) + .ok_or_else(|| self.err(format!("clone_in has unknown struct id {id}")))?; + let permutation = self + .field_perm + .get(id as usize) + .ok_or_else(|| self.err(format!("clone_in has no layout for struct id {id}")))?; + if definition.fields.len() != permutation.len() { + return Err(self.err(format!("clone_in struct {id} layout arity mismatch"))); + } + let fields: Vec<(u32, Ty)> = definition + .fields + .iter() + .zip(permutation) + .map(|(field, physical)| (*physical, field.ty)) + .collect(); + let mut children = Vec::with_capacity(fields.len()); + for (physical, field_ty) in &fields { + let child = self + .builder + .build_extract_value(base, *physical, "clonein.field") + .map_err(|e| self.err(e))?; + children.push((child, *field_ty)); + } + work.push(CloneInWork::RebuildStruct { + base, + fields: fields.iter().map(|(physical, _)| *physical).collect(), + }); + work.extend( + children + .into_iter() + .rev() + .map(|(child, child_ty)| CloneInWork::Visit(child, child_ty)), + ); + } + Ty::Option(payload) => { + let BasicValueEnum::StructValue(base) = value else { + return Err(self.err("clone_in RegionPlain Option has a non-struct LLVM value")); + }; + let child = self + .builder + .build_extract_value(base, 1, "clonein.option") + .map_err(|e| self.err(e))?; + work.push(CloneInWork::RebuildStruct { base, fields: vec![1] }); + work.push(CloneInWork::Visit(child, scalar_to_ty(payload))); + } + Ty::Array(payload, elements) => { + let BasicValueEnum::ArrayValue(base) = value else { + return Err(self.err("clone_in RegionPlain fixed array has a non-array LLVM value")); + }; + let mut children = Vec::with_capacity(elements as usize); + for index in 0..elements { + children.push( + self.builder + .build_extract_value(base, index, "clonein.element") + .map_err(|e| self.err(e))?, + ); + } + work.push(CloneInWork::RebuildArray { base, elements }); + work.extend( + children + .into_iter() + .rev() + .map(|child| CloneInWork::Visit(child, scalar_to_ty(payload))), + ); + } + Ty::StructArray(id, elements) => { + let BasicValueEnum::ArrayValue(base) = value else { + return Err(self.err("clone_in RegionPlain fixed struct array has a non-array LLVM value")); + }; + let mut children = Vec::with_capacity(elements as usize); + for index in 0..elements { + children.push( + self.builder + .build_extract_value(base, index, "clonein.struct.element") + .map_err(|e| self.err(e))?, + ); + } + work.push(CloneInWork::RebuildArray { base, elements }); + work.extend( + children + .into_iter() + .rev() + .map(|child| CloneInWork::Visit(child, Ty::Struct(id))), + ); + } + Ty::Enum(id) => { + let BasicValueEnum::StructValue(base) = value else { + return Err(self.err("clone_in RegionPlain sum has a non-struct LLVM value")); + }; + let definition = self + .enums + .get(id as usize) + .ok_or_else(|| self.err(format!("clone_in has unknown sum id {id}")))?; + let fields: Vec<(u32, Ty)> = definition + .variants + .iter() + .flat_map(|variant| { + variant.payload.iter().enumerate().map(move |(index, payload)| { + (variant.field_base + index as u32, scalar_to_ty(*payload)) + }) + }) + .collect(); + let mut children = Vec::with_capacity(fields.len()); + for (field, field_ty) in &fields { + children.push(( + self.builder + .build_extract_value(base, *field, "clonein.sum.payload") + .map_err(|e| self.err(e))?, + *field_ty, + )); + } + work.push(CloneInWork::RebuildStruct { + base, + fields: fields.iter().map(|(field, _)| *field).collect(), + }); + work.extend( + children + .into_iter() + .rev() + .map(|(child, child_ty)| CloneInWork::Visit(child, child_ty)), + ); + } + Ty::Tagged(id) => { + let BasicValueEnum::StructValue(base) = value else { + return Err(self.err("clone_in RegionPlain tagged value has a non-struct LLVM value")); + }; + let payloads: Vec<(u32, Ty)> = match self.tagged_defs.get(id as usize) { + Some(hir::TaggedType::Option(payload)) => { + vec![(1, scalar_to_ty(*payload))] + } + Some(hir::TaggedType::Result(ok, err)) => vec![ + (1, scalar_to_ty(*ok)), + (2, scalar_to_ty(*err)), + ], + None => { + return Err(self.err(format!("clone_in has unknown tagged id {id}"))); + } + }; + let mut children = Vec::with_capacity(payloads.len()); + for (field, field_ty) in &payloads { + children.push(( + self.builder + .build_extract_value(base, *field, "clonein.tagged.payload") + .map_err(|e| self.err(e))?, + *field_ty, + )); + } + work.push(CloneInWork::RebuildStruct { + base, + fields: payloads.iter().map(|(field, _)| *field).collect(), + }); + work.extend( + children + .into_iter() + .rev() + .map(|(child, child_ty)| CloneInWork::Visit(child, child_ty)), + ); + } + _ => values.push(value), + }, + CloneInWork::RebuildStruct { mut base, fields } => { + let start = values + .len() + .checked_sub(fields.len()) + .ok_or_else(|| self.err("clone_in struct reconstruction underflow"))?; + let children = values.split_off(start); + for (field, child) in fields.into_iter().zip(children) { + base = self + .builder + .build_insert_value(base, child, field, "clonein.rebuild") + .map_err(|e| self.err(e))? + .into_struct_value(); + } + values.push(base.into()); + } + CloneInWork::RebuildArray { mut base, elements } => { + let count = elements as usize; + let start = values + .len() + .checked_sub(count) + .ok_or_else(|| self.err("clone_in array reconstruction underflow"))?; + let children = values.split_off(start); + for (index, child) in children.into_iter().enumerate() { + base = self + .builder + .build_insert_value(base, child, index as u32, "clonein.array.rebuild") + .map_err(|e| self.err(e))? + .into_array_value(); + } + values.push(base.into()); + } + } + } + if values.len() != 1 { + return Err(self.err("clone_in reconstruction did not produce exactly one value")); + } + values + .pop() + .ok_or_else(|| self.err("clone_in reconstruction produced no value")) + } + + fn clone_in_view( + &mut self, + value: BasicValueEnum<'c>, + handle: PointerValue<'c>, + ) -> Result, CodegenError> { + let BasicValueEnum::StructValue(view) = value else { + return Err(self.err("clone_in byte view has a non-view LLVM value")); + }; + let src = self + .builder + .build_extract_value(view, 0, "cloneinsrc") + .map_err(|e| self.err(e))? + .into_pointer_value(); + let len = self + .builder + .build_extract_value(view, 1, "cloneinlen") + .map_err(|e| self.err(e))? + .into_int_value(); + let negative = self + .builder + .build_int_compare( + IntPredicate::SLT, + len, + self.ctx.i64_type().const_zero(), + "clonein.neg", + ) + .map_err(|e| self.err(e))?; + let pointer_bits = self.target_data.get_pointer_byte_size(None) * 8; + let target_isize_max = if pointer_bits >= 64 { + i64::MAX as u64 + } else { + (1u64 << (pointer_bits - 1)) - 1 + }; + let too_large = self + .builder + .build_int_compare( + IntPredicate::UGT, + len, + self.ctx.i64_type().const_int(target_isize_max, false), + "clonein.large", + ) + .map_err(|e| self.err(e))?; + let invalid = self + .builder + .build_or(negative, too_large, "clonein.invalid") + .map_err(|e| self.err(e))?; + self.guard_allocation_size(invalid)?; + let one = self.ctx.i64_type().const_int(1, false); + let dst = self + .builder + .build_call( + self.runtime(RuntimeKey::ArenaAlloc), + &[handle.into(), len.into(), one.into()], + "cloneinbuf", + ) + .map_err(|e| self.err(e))? + .try_as_basic_value() + .basic() + .expect("arena_alloc returns a pointer") + .into_pointer_value(); + self.builder + .build_memcpy(dst, 1, src, 1, len) + .map_err(|e| self.err(e))?; + let out = self + .builder + .build_insert_value(slice_struct_type(self.ctx).get_poison(), dst, 0, "cloneinptr") + .map_err(|e| self.err(e))? + .into_struct_value(); + Ok(self + .builder + .build_insert_value(out, len, 1, "cloneinoutlen") + .map_err(|e| self.err(e))? + .into_struct_value() + .into()) + } + /// `&slot[index]` via an array GEP (indices `[0, index]` into the `[N x T]` alloca). fn elem_ptr(&self, slot: Slot, idx: &Operand) -> Result, CodegenError> { let arr_ty = self.llvm_type(self.f.slots[slot as usize]); @@ -11722,7 +12099,13 @@ impl<'c, 'a> FnGen<'c, 'a> { // one flat free — no per-element deep free — and `array`'s `str` fields are // borrowed views into the input, not freed here. (A Move-struct element is deep-freed by // the arm above.) - Ty::DynArray(_) | Ty::DynStructArray(..) | Ty::DynSliceArray(_) => { + Ty::DynArray(_) + | Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..) + | Ty::DynStructArray(..) + | Ty::DynSliceArray(_) => { let fp = self.builder.build_struct_gep(st, base, pi, "droparr").map_err(|e| self.err(e))?; let agg = self .builder @@ -13291,8 +13674,28 @@ impl<'c, 'a> FnGen<'c, 'a> { ) -> Result>, CodegenError> { match rv { // `array_builder()` — open an empty typed builder sized to the element stride. - Rvalue::ArrayBuilderNew { elem_size } => { - let es = self.ctx.i64_type().const_int(*elem_size as u64, false); + Rvalue::ArrayBuilderNew { elem, region } => { + let element_type = self.llvm_type(*elem); + let es = self.ctx.i64_type().const_int(self.element_allocation_size(element_type), false); + let ea = self + .ctx + .i64_type() + .const_int(self.type_align(*elem) as u64, false); + if let Some(region) = region { + let arena = self.operand(region)?.into(); + let v = self + .builder + .build_call( + self.runtime(RuntimeKey::ArrayBuilderNewIn), + &[arena, es.into(), ea.into()], + "ab.region", + ) + .map_err(|e| self.err(e))? + .try_as_basic_value() + .basic() + .expect("array_builder_new_in returns a pointer"); + return Ok(Some(v)); + } if let Some(slot) = self.stack_header_new_values.get(&result_id).copied() { let header = self.stack_headers[&slot]; let v = self @@ -13321,6 +13724,19 @@ impl<'c, 'a> FnGen<'c, 'a> { Rvalue::ArrayBuilderPush { builder, value, scalar } => { let bp = self.operand(builder)?.into(); let i64t = self.ctx.i64_type(); + if !matches!(scalar, Ty::Int(_) | Ty::Float(_) | Ty::Bool | Ty::Char) { + let value = self.operand(value)?; + let slot = self.alloca_at_entry(self.llvm_type(*scalar), "ab.elem")?; + self.builder.build_store(slot, value).map_err(|e| self.err(e))?; + self.builder + .build_call( + self.runtime(RuntimeKey::ArrayBuilderPushBytes), + &[bp, slot.into()], + "", + ) + .map_err(|e| self.err(e))?; + return Ok(None); + } let bits = if matches!(scalar, Ty::Float(_)) { let fv = self.operand(value)?.into_float_value(); let int_bits = match scalar { Ty::Float(FloatTy { bits: 32 }) => self.ctx.i32_type(), _ => i64t }; @@ -13347,8 +13763,8 @@ impl<'c, 'a> FnGen<'c, 'a> { .map_err(|e| self.err(e))?; Ok(None) } - // `b.append(xs)` — hand the `slice` `{ptr, count}` to the runtime, which bulk-copies - // `count` elements at the builder's stored stride. + // `b.append(xs)` — hand the `slice` `{ptr, count}` to the runtime, which copies + // `count` elements at the builder's stored, target-derived stride. Rvalue::ArrayBuilderAppend { builder, data } => { let bp = self.operand(builder)?.into(); let (ptr, count) = self.split_str(data)?; diff --git a/crates/align_codegen_llvm/src/runtime_abi.rs b/crates/align_codegen_llvm/src/runtime_abi.rs index f83505d5..ade49c6f 100644 --- a/crates/align_codegen_llvm/src/runtime_abi.rs +++ b/crates/align_codegen_llvm/src/runtime_abi.rs @@ -347,11 +347,21 @@ pub(super) fn runtime_abi(key: RuntimeKey) -> RuntimeAbi { symbol: "align_rt_array_builder_new", shape: RuntimeAbiShape::A43, }, + RuntimeKey::ArrayBuilderNewIn => RuntimeAbi { + key, + symbol: "align_rt_array_builder_new_in", + shape: RuntimeAbiShape::A45, + }, RuntimeKey::ArrayBuilderPush => RuntimeAbi { key, symbol: "align_rt_array_builder_push", shape: RuntimeAbiShape::A66, }, + RuntimeKey::ArrayBuilderPushBytes => RuntimeAbi { + key, + symbol: "align_rt_array_builder_push_bytes", + shape: RuntimeAbiShape::A72, + }, RuntimeKey::ArrayBuilderPushStr => RuntimeAbi { key, symbol: "align_rt_array_builder_push_str", @@ -1694,15 +1704,15 @@ pub(super) fn runtime_abis() -> impl Iterator { } pub(super) fn validate_registry() -> Result<(), String> { - if RuntimeKey::ALL.len() != 281 || keyed_runtime_abis().len() != 281 { + if RuntimeKey::ALL.len() != 283 || keyed_runtime_abis().len() != 283 { return Err("runtime ABI registry invariant: key-count".to_string()); } - if runtime_abis().count() != 286 { + if runtime_abis().count() != 288 { return Err("runtime ABI registry invariant: base-count".to_string()); } let mut keys = HashSet::with_capacity(RuntimeKey::ALL.len()); - let mut symbols = HashSet::with_capacity(286); + let mut symbols = HashSet::with_capacity(288); for abi in keyed_runtime_abis() { let key = abi .runtime_key() @@ -2960,17 +2970,17 @@ mod tests { assert_eq!(UNKEYED_RUNTIME_KEYS.map(|key| key as u8), [0, 1, 2, 3, 4]); validate_registry().unwrap(); let rows: Vec<_> = runtime_abis().collect(); - assert_eq!(rows.len(), 286); + assert_eq!(rows.len(), 288); assert_eq!( rows.iter().map(|row| row.key).collect::>().len(), - 286 + 288 ); assert_eq!( rows.iter() .map(|row| row.symbol) .collect::>() .len(), - 286 + 288 ); for (key, row) in RuntimeKey::ALL.into_iter().zip(keyed_runtime_abis()) { assert_eq!(row.key, RuntimeAbiId::Keyed(key)); @@ -3000,7 +3010,7 @@ mod tests { fn runtime_abi_extern_type_matrix_is_exact_for_every_row_and_ordinal() { let ctx = inkwell::context::Context::create(); let rows: Vec<_> = runtime_abis().collect(); - assert_eq!(rows.len(), 286); + assert_eq!(rows.len(), 288); for row in rows { let symbol = row.symbol; diff --git a/crates/align_codegen_llvm/tests/golden/runtime_abi_declarations.txt b/crates/align_codegen_llvm/tests/golden/runtime_abi_declarations.txt index c13e1924..264fd956 100644 --- a/crates/align_codegen_llvm/tests/golden/runtime_abi_declarations.txt +++ b/crates/align_codegen_llvm/tests/golden/runtime_abi_declarations.txt @@ -12,7 +12,9 @@ key|ArrayBuilderFreeStrings|array_builder_free_strings|declare void @align_rt_ar key|ArrayBuilderFreeStringsStack|array_builder_free_strings_stack|declare void @align_rt_array_builder_free_strings_stack(ptr) key|ArrayBuilderInitStack|array_builder_init_stack|declare ptr @align_rt_array_builder_init_stack(ptr, i64) key|ArrayBuilderNew|array_builder_new|declare noalias ptr @align_rt_array_builder_new(i64) #0 +key|ArrayBuilderNewIn|array_builder_new_in|declare noalias ptr @align_rt_array_builder_new_in(ptr, i64, i64) #2 key|ArrayBuilderPush|array_builder_push|declare void @align_rt_array_builder_push(ptr, i64) +key|ArrayBuilderPushBytes|array_builder_push_bytes|declare void @align_rt_array_builder_push_bytes(ptr, ptr) key|ArrayBuilderPushStr|array_builder_push_str|declare void @align_rt_array_builder_push_str(ptr, ptr, i64) key|Base64Decode|base64_decode|declare i32 @align_rt_base64_decode(ptr, i64, ptr) key|Base64Encode|base64_encode|declare { ptr, i64 } @align_rt_base64_encode(ptr, i64) diff --git a/crates/align_driver/tests/fb_region.rs b/crates/align_driver/tests/fb_region.rs new file mode 100644 index 00000000..8244e8d0 --- /dev/null +++ b/crates/align_driver/tests/fb_region.rs @@ -0,0 +1,541 @@ +//! F-B region materialization: named arena capabilities and their first explicit consumers. + +mod common; +use common::*; + +fn code(out: &std::process::Output) -> Option { + out.status.code() +} + +fn assert_no_errors(name: &str, src: &str, context: &str) { + let mut sources = SourceMap::new(); + let checked = check(&mut sources, name, src); + let diags = align_driver::format_diagnostics(&sources, &checked.diags); + assert!(!checked.diags.has_errors(), "{context}:\n{diags}"); +} + +#[test] +fn named_arena_passes_exact_region_capability_to_an_ordinary_function() { + if !backend_available() { + return; + } + let src = "fn use_region(out: region, value: i64) -> i64 = value\nfn main() -> i32 {\n arena out {\n return use_region(out, 37) as i32\n }\n}\n"; + let out = build_and_run("fb-region-param", src); + assert_eq!(code(&out), Some(37), "stderr: {}", String::from_utf8_lossy(&out.stderr)); +} + +#[test] +fn named_region_binding_does_not_escape_its_arena_scope() { + let src = "fn use_region(out: region) -> i64 = 1\nfn main() -> i32 {\n arena out {\n print(use_region(out))\n }\n return use_region(out) as i32\n}\n"; + let diags = check_diagnostics("fb-region-scope", src); + assert!(diags.contains("undefined name: 'out'"), "expected scope diagnostic, got:\n{diags}"); +} + +#[test] +fn region_capability_cannot_cross_a_spawn_boundary() { + let src = "fn main() -> Result<(), Error> {\n arena out {\n task_group {\n task := spawn(fn { \"task\".clone_in(out).len() })\n wait()\n print(task.get())\n }\n }\n return Ok(())\n}\n"; + let diags = check_diagnostics("fb-region-spawn", src); + assert!( + diags.contains("cannot capture a region capability"), + "expected non-Send region diagnostic, got:\n{diags}" + ); +} + +#[test] +fn named_and_anonymous_arenas_keep_the_same_return_and_question_cleanup() { + if !backend_available() { + return; + } + let named = emit_llvm( + "fn ready() -> Result = Ok(5)\nfn early() -> i64 { arena out { return 7 } }\nfn fallible() -> Result { arena out { value := ready()?\n return Ok(value) } }\nfn main() -> i32 = early() as i32\n", + ); + let anonymous = emit_llvm( + "fn ready() -> Result = Ok(5)\nfn early() -> i64 { arena { return 7 } }\nfn fallible() -> Result { arena { value := ready()?\n return Ok(value) } }\nfn main() -> i32 = early() as i32\n", + ); + for symbol in ["align_rt_arena_begin", "align_rt_arena_end"] { + let needle = format!("call {}", if symbol.ends_with("begin") { "ptr" } else { "void" }); + let named_calls = named + .lines() + .filter(|line| line.contains(&needle) && line.contains(&format!("@{symbol}("))) + .count(); + let anonymous_calls = anonymous + .lines() + .filter(|line| line.contains(&needle) && line.contains(&format!("@{symbol}("))) + .count(); + assert_eq!(named_calls, anonymous_calls, "named arena changed {symbol} cleanup shape"); + assert!(named_calls >= 2, "return and ? paths did not both lower {symbol}"); + } +} + +#[test] +fn region_capability_cannot_be_returned() { + let src = "fn leak(out: region) -> region = out\nfn main() -> i32 = 0\n"; + let diags = check_diagnostics("fb-region-return", src); + assert!( + diags.contains("region capability cannot be returned"), + "expected return restriction, got:\n{diags}" + ); +} + +#[test] +fn region_capability_cannot_be_stored_or_mutably_borrowed() { + for (name, src) in [ + ( + "fb-region-local-inferred", + "fn main() -> i32 {\n arena out {\n alias := out\n return 0\n }\n}\n", + ), + ( + "fb-region-local-mutable", + "fn main() -> i32 {\n arena out {\n mut alias: region := out\n return 0\n }\n}\n", + ), + ] { + let diags = check_diagnostics(name, src); + assert!( + diags.contains("cannot be stored in an ordinary local"), + "expected region-storage rejection, got:\n{diags}" + ); + } + + let mutable_parameter = + "fn overwrite(borrow mut out: region) { out = out }\nfn main() -> i32 = 0\n"; + let diags = check_diagnostics("fb-region-param-mutable", mutable_parameter); + assert!( + diags.contains("must be passed by value"), + "expected mutable-region-parameter rejection, got:\n{diags}" + ); +} + +#[test] +fn clone_in_uses_the_region_passed_through_an_ordinary_function() { + if !backend_available() { + return; + } + let src = include_str!("fixtures/fb_region_clone.align"); + let out = build_and_run("fb-clone-in", src); + assert_eq!(code(&out), Some(11), "stderr: {}", String::from_utf8_lossy(&out.stderr)); + assert_eq!(String::from_utf8_lossy(&out.stdout), "region-copy\n"); +} + +#[test] +fn clone_in_llvm_uses_the_explicit_region_and_guards_the_native_length() { + if !backend_available() { + return; + } + let llvm = emit_llvm( + "fn copy_text(out: region, value: str) -> str = value.clone_in(out)\nfn main() -> i32 { arena out { return copy_text(out, \"copy\").len() as i32 } }\n", + ); + assert!(llvm.contains("call ptr @align_rt_arena_alloc("), "clone_in skipped the explicit arena:\n{llvm}"); + assert!(llvm.contains("icmp slt i64"), "clone_in did not reject a negative native length:\n{llvm}"); + assert!(llvm.contains("icmp ugt i64"), "clone_in did not reject a target-oversized native length:\n{llvm}"); + assert!(llvm.contains("call void @align_rt_alloc_size_fail("), "clone_in omitted the allocation-size failure edge:\n{llvm}"); + assert!(!llvm.contains("call { ptr, i64 } @align_rt_str_clone("), "clone_in selected heap clone:\n{llvm}"); + assert!(!llvm.contains("call ptr @align_rt_alloc("), "clone_in called the heap allocator:\n{llvm}"); +} + +#[test] +fn named_arena_statement_body_still_checks_moved_places() { + let src = "fn main() -> i32 {\n mut values: array_builder := array_builder()\n values.push(1)\n mut built := values.build()\n moved := built\n arena out { built[0] = 2 }\n return moved.len() as i32\n}\n"; + let diags = check_diagnostics("fb-region-named-arena-moved-place", src); + assert!( + diags.contains("use of moved value 'built'"), + "named arena skipped MoveCheck for a statement body:\n{diags}" + ); +} + +#[test] +fn region_builder_admits_a_zero_sized_plain_layout() { + let src = "Empty { }\nfn main() -> i32 {\n arena out {\n mut values: array_builder := array_builder(out)\n return 0\n }\n}\n"; + assert_no_errors( + "fb-region-zero-sized-layout", + src, + "a zero-sized RegionPlain builder layout should type-check", + ); +} + +#[test] +fn explicit_region_operands_survive_hir_to_mir() { + let src = "fn copy_text(out: region, value: str) -> str = value.clone_in(out)\nfn main() -> i32 {\n arena out {\n copied := copy_text(out, \"copy\")\n mut values: array_builder := array_builder(out)\n values.push(copied)\n return values.build().len() as i32\n }\n}\n"; + let mut sources = SourceMap::new(); + let checked = check(&mut sources, "fb-region-mir", src); + assert!( + !checked.diags.has_errors(), + "unexpected diagnostics:\n{}", + align_driver::format_diagnostics(&sources, &checked.diags) + ); + let mir = align_mir::print::program_to_string(&lower_to_mir(&checked.hir)); + assert!(mir.contains("clone_in("), "clone_in lost its MIR operation:\n{mir}"); + assert!( + mir.contains("array_builder_new(elem=Str, region=%"), + "region builder lost its exact handle operand:\n{mir}" + ); + assert!( + !mir.contains("array_builder_new(elem=Str, region=heap)"), + "region builder changed allocation mode:\n{mir}" + ); +} + +#[test] +fn clone_in_result_cannot_escape_the_selected_arena() { + let src = "fn copy_text(out: region, value: str) -> str = value.clone_in(out)\nfn leak() -> str {\n arena out {\n return copy_text(out, \"nope\")\n }\n}\nfn main() -> i32 = 0\n"; + let diags = check_diagnostics("fb-clone-in-escape", src); + assert!(diags.contains("arena"), "expected arena escape diagnostic, got:\n{diags}"); +} + +#[test] +fn clone_in_copies_bytes_and_preserves_the_selected_region_identity() { + let bytes = "fn copy_bytes(out: region, value: slice) -> slice = value.clone_in(out)\nfn main() -> i32 {\n arena out {\n copied := copy_bytes(out, \"abc\".bytes())\n return copied[0] as i32\n }\n}\n"; + assert_no_errors("fb-clone-in-bytes", bytes, "unexpected byte clone diagnostics"); + + let wrong_region = "fn copy_text(out: region, value: str) -> str = value.clone_in(out)\nfn main() -> i32 {\n arena outer {\n mut values: array_builder := array_builder(outer)\n arena inner {\n temporary := copy_text(inner, \"temporary\")\n values.push(temporary)\n }\n return 0\n }\n}\n"; + let diags = check_diagnostics("fb-clone-in-wrong-region", wrong_region); + assert!( + diags.contains("shorter-lived view") + || diags.contains("clone_in(outer)") + || diags.contains("outlive"), + "expected exact-region builder-store diagnostic, got:\n{diags}" + ); + + let outer_clone = "fn copy_text(out: region, value: str) -> str = value.clone_in(out)\nfn main() -> i32 {\n arena outer {\n mut values: array_builder := array_builder(outer)\n arena inner {\n retained := copy_text(outer, \"retained\")\n values.push(retained)\n }\n return values.build().len() as i32\n }\n}\n"; + assert_no_errors( + "fb-clone-in-outer-region", + outer_clone, + "outer-region clone should remain valid", + ); +} + +#[test] +fn clone_in_recursively_copies_region_plain_struct_views() { + let src = "Record { id: i64, name: str, alias: Option, data: slice }\nfn copy_record(out: region, value: Record) -> Record = value.clone_in(out)\nfn main() -> i32 {\n arena out {\n copied := copy_record(out, Record { id: 20, name: \"plain\", alias: Some(\"view\"), data: \"bc\".bytes() })\n alias := copied.alias else \"\"\n return (copied.id + copied.name.len() + alias.len() + copied.data[0] as i64) as i32\n }\n}\n"; + assert_no_errors( + "fb-clone-in-plain-struct", + src, + "unexpected plain-struct clone diagnostics", + ); + if !backend_available() { + return; + } + let llvm = emit_llvm(src); + assert_eq!( + llvm.matches("call ptr @align_rt_arena_alloc(").count(), + 3, + "each view-bearing field must receive one explicit region allocation:\n{llvm}", + ); + let out = build_and_run("fb-clone-in-plain-struct", src); + assert_eq!(code(&out), Some(127), "stderr: {}", String::from_utf8_lossy(&out.stderr)); +} + +#[test] +fn clone_in_rejects_structs_with_independently_owned_fields() { + let src = "Owned { name: string }\nfn copy_owned(out: region, value: Owned) -> Owned = value.clone_in(out)\nfn main() -> i32 = 0\n"; + let diags = check_diagnostics("fb-clone-in-owned-struct", src); + assert!( + diags.contains("field 'name' owns independent heap storage"), + "expected recursive clone_in ownership diagnostic, got:\n{diags}", + ); +} + +#[test] +fn captured_region_identity_survives_indirect_closure_calls() { + let valid = "fn main() -> i32 {\n arena out {\n copy := fn value: str { value.clone_in(out) }\n copied := copy(\"captured\")\n return copied.len() as i32\n }\n}\n"; + assert_no_errors( + "fb-region-captured-closure", + valid, + "captured region should remain callable", + ); + + let invalid = "fn main() -> i32 {\n arena outer {\n mut values: array_builder := array_builder(outer)\n arena inner {\n copy := fn value: str { value.clone_in(inner) }\n temporary := copy(\"captured\")\n values.push(temporary)\n }\n return 0\n }\n}\n"; + let diags = check_diagnostics("fb-region-captured-closure-wrong-region", invalid); + assert!( + diags.contains("clone_in(outer)") || diags.contains("shorter-lived"), + "expected captured-region identity rejection, got:\n{diags}" + ); +} + +#[test] +fn region_builder_grows_across_chunks_and_compacts_to_one_array() { + if !backend_available() { + return; + } + let src = include_str!("fixtures/fb_region_builder.align"); + let out = build_and_run("fb-region-builder", src); + // sum(0..17) + len = 136 + 17. + assert_eq!(code(&out), Some(153), "stderr: {}", String::from_utf8_lossy(&out.stderr)); +} + +#[test] +fn heap_builder_rejects_region_only_view_elements() { + let src = "fn main() -> i32 {\n mut values: array_builder := array_builder()\n return 0\n}\n"; + let diags = check_diagnostics("fb-region-builder-heap-view", src); + assert!(diags.contains("use `array_builder(out)`"), "expected allocation-mode diagnostic, got:\n{diags}"); +} + +#[test] +fn region_builder_rejects_independently_owned_fields() { + let src = "Owned { name: string }\nfn main() -> i32 {\n arena out {\n mut values: array_builder := array_builder(out)\n return 0\n }\n}\n"; + let diags = check_diagnostics("fb-region-builder-owned", src); + assert!(diags.contains("field 'name' owns independent heap storage"), "expected RegionPlain field diagnostic, got:\n{diags}"); +} + +#[test] +fn region_plain_bytes_excludes_signed_byte_slices() { + let src = "fn main() -> i32 {\n arena out {\n mut values: array_builder> := array_builder(out)\n return 0\n }\n}\n"; + let diags = check_diagnostics("fb-region-builder-signed-bytes", src); + assert!( + diags.contains("cannot use region storage") || diags.contains("unsupported type"), + "expected RegionPlain byte-view diagnostic, got:\n{diags}" + ); +} + +#[test] +fn region_builder_materializes_option_and_plain_struct_elements() { + if !backend_available() { + return; + } + let src = include_str!("fixtures/fb_region_builder_aggregate.align"); + let out = build_and_run("fb-region-builder-aggregate", src); + assert_eq!(code(&out), Some(47), "stderr: {}", String::from_utf8_lossy(&out.stderr)); +} + +#[test] +fn region_builder_appends_a_fixed_array_without_heap_storage() { + if !backend_available() { + return; + } + let src = "fn main() -> i32 {\n arena out {\n source := [4, 8, 15, 16, 23, 42]\n mut values: array_builder := array_builder(out)\n values.append(source[..])\n built := values.build()\n return (built.sum() + built.len()) as i32\n }\n}\n"; + let out = build_and_run("fb-region-builder-fixed-array", src); + assert_eq!(code(&out), Some(114), "stderr: {}", String::from_utf8_lossy(&out.stderr)); +} + +#[test] +fn borrow_mut_builder_helper_cannot_consume_the_callers_builder() { + let src = "fn finish(borrow mut values: array_builder) -> array = values.build()\nfn main() -> i32 = 0\n"; + let diags = check_diagnostics("fb-region-builder-borrow-build", src); + assert!( + diags.contains("cannot move") || diags.contains("borrow"), + "expected borrowed-builder consumption diagnostic, got:\n{diags}" + ); +} + +#[test] +fn region_builder_cannot_be_returned_from_its_helper() { + let src = "fn leak(out: region) -> array_builder {\n mut values: array_builder := array_builder(out)\n return values\n}\nfn main() -> i32 = 0\n"; + let diags = check_diagnostics("fb-region-builder-return", src); + assert!( + diags.contains("cannot return a region-backed array_builder"), + "expected region-builder return diagnostic, got:\n{diags}" + ); +} + +#[test] +fn region_builder_crosses_calls_only_as_borrow_mut() { + let shared = "fn inspect(borrow values: array_builder) {}\nfn main() -> i32 {\n arena out {\n mut values: array_builder := array_builder(out)\n inspect(values)\n return 0\n }\n}\n"; + let diags = check_diagnostics("fb-region-builder-shared-call", shared); + assert!( + diags.contains("may be passed only as `borrow mut`"), + "expected shared-region-builder rejection, got:\n{diags}" + ); + + let by_value = shared.replace("borrow values", "values"); + let diags = check_diagnostics("fb-region-builder-value-call", &by_value); + assert!( + diags.contains("arena-owned value cannot be moved into a function call"), + "expected by-value region-builder rejection, got:\n{diags}" + ); + + let heap = "fn inspect(borrow values: array_builder) {}\nfn main() -> i32 {\n mut values: array_builder := array_builder()\n inspect(values)\n return 0\n}\n"; + assert_no_errors( + "fb-heap-builder-shared-call", + heap, + "heap builder borrow should remain legal", + ); + + let uncertain = "fn inspect(borrow values: array_builder) {}\nfn relay(borrow mut values: array_builder) { inspect(values) }\nfn main() -> i32 = 0\n"; + let diags = check_diagnostics("fb-incoming-builder-shared-call", uncertain); + assert!( + diags.contains("may be passed only as `borrow mut`"), + "a possibly region-backed incoming builder must fail closed, got:\n{diags}" + ); + + let heap_only = "fn inspect(borrow values: array_builder) {}\nfn relay(borrow mut values: array_builder) { inspect(values) }\nfn main() -> i32 = 0\n"; + assert_no_errors( + "fb-incoming-heap-builder-shared-call", + heap_only, + "a heap-only incoming builder should remain borrowable", + ); +} + +#[test] +fn borrow_mut_builder_helper_preserves_new_view_roots_in_the_caller() { + let valid = "fn push_view(borrow mut values: array_builder, value: str) { values.push(value) }\nfn main() -> i32 {\n arena out {\n mut values: array_builder := array_builder(out)\n push_view(values, \"static\")\n return values.build().len() as i32\n }\n}\n"; + assert_no_errors( + "fb-region-builder-helper-view", + valid, + "view helper should type-check", + ); + + let invalid = "fn copy_text(out: region, value: str) -> str = value.clone_in(out)\nfn push_view(borrow mut values: array_builder, value: str) { values.push(value) }\nfn main() -> i32 {\n arena outer {\n mut values: array_builder := array_builder(outer)\n arena inner {\n temporary := copy_text(inner, \"short\")\n push_view(values, temporary)\n }\n return values.build().len() as i32\n }\n}\n"; + let diags = check_diagnostics("fb-region-builder-helper-wrong-region", invalid); + assert!( + diags.contains("shorter-lived view") + || diags.contains("invalidated") + || diags.contains("dropped") + || diags.contains("outlive"), + "expected cross-call retained-view diagnostic, got:\n{diags}" + ); + + let invalidated_owner = "fn push_view(borrow mut values: array_builder, value: str) { values.push(value) }\nfn main() -> i32 {\n arena out {\n mut owner := \"first\".clone()\n view: str := owner\n mut values: array_builder := array_builder(out)\n push_view(values, view)\n owner = \"second\".clone()\n return values.build().len() as i32\n }\n}\n"; + let diags = check_diagnostics( + "fb-region-builder-helper-owner-invalidation", + invalidated_owner, + ); + assert!( + diags.contains("invalidated") || diags.contains("borrow") || diags.contains("dependent"), + "expected retained owner-generation diagnostic, got:\n{diags}" + ); +} + +#[test] +fn borrowed_region_builders_reject_callee_local_views() { + let incoming = "fn retain_local(borrow mut values: array_builder) {\n arena inner {\n temporary := \"short\".clone_in(inner)\n values.push(temporary)\n }\n}\nfn main() -> i32 {\n arena outer {\n mut values: array_builder := array_builder(outer)\n retain_local(values)\n return 0\n }\n}\n"; + let diags = check_diagnostics("fb-region-builder-callee-local", incoming); + assert!( + diags.contains("shorter-lived view") || diags.contains("outlive"), + "expected callee-local retained-view diagnostic, got:\n{diags}" + ); + + let constructed = "fn materialize(out: region) -> i64 {\n mut values: array_builder := array_builder(out)\n arena inner {\n temporary := \"short\".clone_in(inner)\n values.push(temporary)\n }\n return values.build().len()\n}\nfn main() -> i32 {\n arena out { return materialize(out) as i32 }\n}\n"; + let diags = check_diagnostics("fb-region-builder-param-local", constructed); + assert!( + diags.contains("shorter-lived view") || diags.contains("outlive"), + "expected parameter-region builder diagnostic, got:\n{diags}" + ); +} + +#[test] +fn helper_region_parameter_is_checked_against_the_concrete_builder_region() { + let helper = "fn retain_copy(out: region, borrow mut values: array_builder) {\n values.push(\"copy\".clone_in(out))\n}\n"; + let valid = format!( + "{helper}fn main() -> i32 {{\n arena outer {{\n mut values: array_builder := array_builder(outer)\n retain_copy(outer, values)\n return values.build().len() as i32\n }}\n}}\n" + ); + assert_no_errors( + "fb-region-builder-param-same", + &valid, + "same caller region should remain valid", + ); + + let invalid = format!( + "{helper}fn main() -> i32 {{\n arena outer {{\n mut values: array_builder := array_builder(outer)\n arena inner {{ retain_copy(inner, values) }}\n return 0\n }}\n}}\n" + ); + let diags = check_diagnostics("fb-region-builder-param-different", &invalid); + assert!( + diags.contains("shorter-lived view") || diags.contains("outlive"), + "expected concrete call-site region diagnostic, got:\n{diags}" + ); +} + +#[test] +fn region_builder_llvm_uses_only_explicit_arena_growth_and_generic_copy() { + if !backend_available() { + return; + } + let src = include_str!("fixtures/fb_region_builder.align"); + let llvm = emit_llvm(src); + assert!(llvm.contains("call ptr @align_rt_array_builder_new_in("), "missing region constructor:\n{llvm}"); + assert!(llvm.contains("call { ptr, i64 } @align_rt_array_builder_build("), "missing compacting build:\n{llvm}"); + assert!(!llvm.contains("call ptr @align_rt_array_builder_new(i64"), "region form selected heap constructor:\n{llvm}"); + assert!(!llvm.contains("call ptr @align_rt_array_builder_init_stack("), "region form selected heap stack header:\n{llvm}"); + assert!(!llvm.contains("call ptr @align_rt_alloc("), "region form called the heap allocator:\n{llvm}"); + assert!(!llvm.contains("call ptr @align_rt_realloc("), "region form called the heap reallocator:\n{llvm}"); +} + +#[test] +fn region_plain_aggregate_push_uses_target_layout_copy() { + if !backend_available() { + return; + } + let llvm = emit_llvm(include_str!("fixtures/fb_region_builder_aggregate.align")); + assert!( + llvm.contains("call void @align_rt_array_builder_push_bytes("), + "aggregate push did not use the exact-layout copy seam:\n{llvm}" + ); + assert!(!llvm.contains("call ptr @align_rt_alloc("), "aggregate region builder used heap allocation:\n{llvm}"); + assert!(!llvm.contains("call ptr @align_rt_realloc("), "aggregate region builder used heap reallocation:\n{llvm}"); +} + +#[test] +fn region_builder_preserves_vector_and_mask_element_types_through_indexing() { + let src = "fn main() -> i32 {\n arena out {\n a: vec4 := [1, 2, 3, 4]\n b: vec4 := [0, 3, 2, 5]\n mut vectors: array_builder> := array_builder(out)\n vectors.push(a)\n built_vectors := vectors.build()\n mut masks: array_builder> := array_builder(out)\n masks.push(a > b)\n built_masks := masks.build()\n selected := select(built_masks[0], built_vectors[0], b)\n return (selected[1] as i64 + built_vectors.len() + built_masks.len()) as i32\n }\n}\n"; + assert_no_errors( + "fb-region-vector-mask-builder", + src, + "unexpected vector/mask builder diagnostics", + ); + if !backend_available() { + return; + } + let out = build_and_run("fb-region-vector-mask-builder", src); + assert_eq!( + code(&out), + Some(5), + "stderr: {}", + String::from_utf8_lossy(&out.stderr) + ); +} + +#[test] +fn region_parameters_and_builder_helpers_match_whole_and_per_unit_compilation() { + let lib = "module regionlib\npub fn copy_text(out: region, value: str) -> str = value.clone_in(out)\npub fn push_value(borrow mut values: array_builder, value: i64) { values.push(value) }\npub fn build_value(out: region, value: i64) -> array {\n mut values: array_builder := array_builder(out)\n push_value(values, value)\n return values.build()\n}\n"; + let main = "import regionlib\nfn main() -> i32 {\n arena out {\n copied := regionlib.copy_text(out, \"unit\")\n built := regionlib.build_value(out, 38)\n return (built[0] + copied.len()) as i32\n }\n}\n"; + let files = [("regionlib.align", lib), ("main.align", main)]; + let checked = assert_same_verdict("fb-region-per-unit-check", &files, "main.align"); + assert!(!checked.diags.has_errors(), "unexpected per-unit diagnostics"); + if !backend_available() { + return; + } + let out = build_per_unit_multi("fb-region-per-unit-run", &files, "main.align").link_and_run(); + assert_eq!(code(&out), Some(42), "stderr: {}", String::from_utf8_lossy(&out.stderr)); +} + +#[test] +fn vector_builder_descriptor_matches_whole_and_per_unit_compilation() { + let lib = "module regionvectors\npub fn build_value(out: region, value: vec4) -> array> {\n mut values: array_builder> := array_builder(out)\n values.push(value)\n return values.build()\n}\n"; + let main = "import regionvectors\nfn main() -> i32 {\n arena out {\n value: vec4 := [4, 5, 6, 7]\n built := regionvectors.build_value(out, value)\n return built[0][2]\n }\n}\n"; + let files = [("regionvectors.align", lib), ("main.align", main)]; + let checked = assert_same_verdict("fb-region-vector-per-unit", &files, "main.align"); + assert!( + !checked.diags.has_errors(), + "vector builder descriptor diverged between whole and per-unit compilation" + ); +} + +#[test] +fn imported_view_helper_keeps_the_same_wrong_region_rejection() { + let lib = "module regionviews\npub fn push_view(borrow mut values: array_builder, value: str) { values.push(value) }\n"; + let main = "import regionviews\nfn copy_text(out: region, value: str) -> str = value.clone_in(out)\nfn main() -> i32 {\n arena outer {\n mut values: array_builder := array_builder(outer)\n arena inner {\n temporary := copy_text(inner, \"short\")\n regionviews.push_view(values, temporary)\n }\n return values.build().len() as i32\n }\n}\n"; + let checked = assert_same_verdict( + "fb-region-imported-helper-reject", + &[("regionviews.align", lib), ("main.align", main)], + "main.align", + ); + assert!(checked.diags.has_errors(), "imported helper lost wrong-region rejection"); +} + +#[test] +fn builder_operands_remain_visible_to_effect_inference() { + let src = "fn noisy() -> i64 { print(1)\n return 1 }\nfn materialize(value: i64) -> i64 {\n mut values: array_builder := array_builder()\n values.push(noisy())\n return values.build()[0] + value\n}\nfn main() -> i32 {\n out := [1, 2].par_map(materialize)\n return out[0] as i32\n}\n"; + let diags = check_diagnostics("fb-region-builder-effect", src); + assert!( + diags.contains("Pure") || diags.contains("Impure"), + "expected parallel-effect rejection, got:\n{diags}" + ); +} + +#[test] +fn invalid_clone_in_region_stops_before_typed_hir_construction() { + let src = "fn main() -> i32 {\n copied := \"x\".clone_in(missing)\n return 0\n}\n"; + let diags = check_diagnostics("fb-clone-in-invalid-region", src); + assert!( + diags.contains("undefined name: 'missing'"), + "expected region operand diagnostic, got:\n{diags}" + ); +} diff --git a/crates/align_driver/tests/fixtures/fb_region_builder.align b/crates/align_driver/tests/fixtures/fb_region_builder.align new file mode 100644 index 00000000..62c7a3d8 --- /dev/null +++ b/crates/align_driver/tests/fixtures/fb_region_builder.align @@ -0,0 +1,13 @@ +fn main() -> i32 { + arena out { + mut values: array_builder := array_builder(out) + mut i := 0 + loop { + values.push(i) + i = i + 1 + if i >= 17 { break } + } + built := values.build() + return (built.sum() + built.len()) as i32 + } +} diff --git a/crates/align_driver/tests/fixtures/fb_region_builder_aggregate.align b/crates/align_driver/tests/fixtures/fb_region_builder_aggregate.align new file mode 100644 index 00000000..9f915df7 --- /dev/null +++ b/crates/align_driver/tests/fixtures/fb_region_builder_aggregate.align @@ -0,0 +1,15 @@ +Row { id: i64, name: str } + +fn main() -> i32 { + arena out { + mut opts: array_builder> := array_builder(out) + opts.push(Some(9)) + options := opts.build() + value := options[0] else 0 + + mut rows: array_builder := array_builder(out) + rows.push(Row { id: 33, name: "plain" }) + built := rows.build() + return (value + built[0].id + built[0].name.len()) as i32 + } +} diff --git a/crates/align_driver/tests/fixtures/fb_region_clone.align b/crates/align_driver/tests/fixtures/fb_region_clone.align new file mode 100644 index 00000000..b456798f --- /dev/null +++ b/crates/align_driver/tests/fixtures/fb_region_clone.align @@ -0,0 +1,9 @@ +fn copy_text(out: region, value: str) -> str = value.clone_in(out) + +fn main() -> i32 { + arena out { + copied := copy_text(out, "region-copy") + print(copied) + return copied.len() as i32 + } +} diff --git a/crates/align_fmt/src/lib.rs b/crates/align_fmt/src/lib.rs index 2578dc5b..e74a3f9f 100644 --- a/crates/align_fmt/src/lib.rs +++ b/crates/align_fmt/src/lib.rs @@ -273,7 +273,12 @@ impl Annotations { self.visit_expr(e); } } - ExprKind::Block(b) | ExprKind::Arena(b) | ExprKind::TaskGroup(b) | ExprKind::Unsafe(b) | ExprKind::Loop(b) => self.visit_block(b), + ExprKind::Block(b) + | ExprKind::Arena(b) + | ExprKind::NamedArena { block: b, .. } + | ExprKind::TaskGroup(b) + | ExprKind::Unsafe(b) + | ExprKind::Loop(b) => self.visit_block(b), ExprKind::StructLit { fields, .. } => { for f in fields { self.visit_expr(&f.value); @@ -621,6 +626,15 @@ mod tests { assert_eq!(multi, "fn f() -> i32 {\n return 1\n}\n"); } + #[test] + fn named_arena_round_trips_canonically() { + let source = "fn f()->i64{\n arena out{\n b:array_builder:=array_builder(out)\n return b.build().len()\n }\n}\n"; + let once = fmt(source); + assert!(once.contains("arena out {"), "named arena spacing changed:\n{once}"); + assert!(once.contains("array_builder(out)"), "region argument changed:\n{once}"); + assert_eq!(fmt(&once), once); + } + #[test] fn preserves_comments() { let src = "// header\nfn main() -> i32 {\n return 0 // done\n}\n"; diff --git a/crates/align_interface/src/lib.rs b/crates/align_interface/src/lib.rs index f23a50ca..826b9cde 100644 --- a/crates/align_interface/src/lib.rs +++ b/crates/align_interface/src/lib.rs @@ -312,13 +312,29 @@ fn apply_function_cleanup_metadata( | align_sema::Ty::Box(value) | align_sema::Ty::Slice(value) | align_sema::Ty::DynArray(value) - | align_sema::Ty::ArrayBuilder(value) | align_sema::Ty::Task(value) | align_sema::Ty::Array(value, _) | align_sema::Ty::Vec(value, _) | align_sema::Ty::Mask(value, _) => { vec![align_sema::scalar_to_ty(value)] } + align_sema::Ty::ArrayBuilder(value) => { + vec![align_sema::scalar_to_ty(value)] + } + ty @ (align_sema::Ty::VecArrayBuilder(..) + | align_sema::Ty::MaskArrayBuilder(..) + | align_sema::Ty::FixedArrayBuilder(..) + | align_sema::Ty::FixedStructArrayBuilder(..)) => vec![ty + .array_builder_element() + .expect("matched aggregate builder") + .ty()], + ty @ (align_sema::Ty::DynVecArray(..) + | align_sema::Ty::DynMaskArray(..) + | align_sema::Ty::DynFixedArray(..) + | align_sema::Ty::DynFixedStructArray(..)) => vec![ty + .dyn_aggregate_array_element() + .expect("matched aggregate array") + .ty()], align_sema::Ty::Result(ok, err) => vec![ align_sema::scalar_to_ty(ok), align_sema::scalar_to_ty(err), @@ -1179,7 +1195,7 @@ enum BuiltinCapability { fn builtin_capability(path: &str) -> Option<(usize, BuiltinCapability)> { let result = match path { - "str" | "reader" | "writer" | "http_headers" | "json.doc" => { + "str" | "region" | "reader" | "writer" | "http_headers" | "json.doc" => { (0, BuiltinCapability::BorrowLeaf) } "slice" | "soa" | "json.scanner" | "resource_ref" => { diff --git a/crates/align_mir/src/canonical_graph.rs b/crates/align_mir/src/canonical_graph.rs index 59b68649..6f72643b 100644 --- a/crates/align_mir/src/canonical_graph.rs +++ b/crates/align_mir/src/canonical_graph.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use std::fmt; use align_ast::ParamMode; -use align_sema::{Layout, PrimScalar, Scalar, Ty, hir}; +use align_sema::{AggregateArrayElem, ArrayBuilderElem, Layout, PrimScalar, Scalar, Ty, hir}; use super::source_shape::{SourceShapeNode, SourceShapeView, source_shape_equal}; use super::{Program, function_embedded_types, remap_function_embedded_types}; @@ -622,6 +622,57 @@ impl<'a> GraphValidator<'a> { } } + fn scan_aggregate_array_elem( + &mut self, + value: AggregateArrayElem, + references: &mut Vec, + ) { + self.field_ordinal(); + match value { + AggregateArrayElem::Vec(value, lanes) + | AggregateArrayElem::Mask(value, lanes) => { + let scalar_ordinal = self.next_ordinal; + 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); + } + if !matches!(lanes, 2 | 4 | 8 | 16) { + self.candidate(lanes_ordinal, CanonicalGraphError::InvalidWidth); + } + } + AggregateArrayElem::FixedArray(value, length) => { + self.scan_scalar(value, references, None); + let length_ordinal = self.field_ordinal(); + if length == 0 { + self.candidate(length_ordinal, CanonicalGraphError::InvalidCount); + } + } + AggregateArrayElem::FixedStructArray(id, length) => { + let id_ordinal = self.field_ordinal(); + self.scan_reference(Node::Struct(id), id_ordinal, references); + let length_ordinal = self.field_ordinal(); + if length == 0 { + self.candidate(length_ordinal, CanonicalGraphError::InvalidCount); + } + } + } + } + + fn scan_array_builder_elem( + &mut self, + value: ArrayBuilderElem, + references: &mut Vec, + ) { + self.field_ordinal(); + match value { + ArrayBuilderElem::Scalar(value) => self.scan_scalar(value, references, None), + ArrayBuilderElem::Aggregate(value) => { + self.scan_aggregate_array_elem(value, references) + } + } + } + fn scan_ty( &mut self, value: Ty, @@ -634,8 +685,26 @@ impl<'a> GraphValidator<'a> { Ty::Box(value) | Ty::Slice(value) | Ty::DynArray(value) - | Ty::ArrayBuilder(value) - | Ty::Task(value) => self.scan_scalar(value, references, None), + | Ty::Task(value) => { + self.scan_scalar(value, references, None) + } + Ty::ArrayBuilder(value) => { + self.scan_array_builder_elem(ArrayBuilderElem::Scalar(value), references) + } + value @ (Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..)) => self.scan_array_builder_elem( + value.array_builder_element().expect("matched aggregate builder"), + references, + ), + value @ (Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => self.scan_aggregate_array_elem( + value.dyn_aggregate_array_element().expect("matched aggregate array"), + references, + ), Ty::Result(ok, err) => { self.scan_scalar(ok, references, inline_from); self.scan_scalar(err, references, inline_from); @@ -926,7 +995,7 @@ fn canonical_type_bytes_with_classes( } let mut out = Vec::new(); - out.push(2); + out.push(3); out.extend(checked_count(class_order.len())?.to_le_bytes()); let ordinal = |node: Node| { let class = classes @@ -1161,7 +1230,7 @@ enum DecodedNode { pub(super) fn canonical_type_record_len(bytes: &[u8]) -> Result { let mut cursor = DecodeCursor::new(bytes); - if cursor.byte()? != 2 { + if cursor.byte()? != 3 { return Err(CanonicalCodecError::UnsupportedVersion); } let node_count = cursor.count(1)?; @@ -1463,6 +1532,58 @@ fn decode_scalar(cursor: &mut DecodeCursor<'_>) -> Result, +) -> Result { + let tag = cursor.byte()?; + let elem = match tag { + 0 | 1 => { + 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 == 0 { + AggregateArrayElem::Vec(scalar, lanes) + } else { + AggregateArrayElem::Mask(scalar, lanes) + } + } + 2 => { + let scalar = decode_scalar(cursor)?; + let length = cursor.u32()?; + if length == 0 { + return Err(CanonicalCodecError::InvalidCount); + } + AggregateArrayElem::FixedArray(scalar, length) + } + 3 => { + let id = cursor.u32()?; + let length = cursor.u32()?; + if length == 0 { + return Err(CanonicalCodecError::InvalidCount); + } + AggregateArrayElem::FixedStructArray(id, length) + } + _ => return Err(CanonicalCodecError::UnknownTag), + }; + Ok(elem) +} + +fn decode_array_builder_elem( + cursor: &mut DecodeCursor<'_>, +) -> Result { + match cursor.byte()? { + 0 => Ok(ArrayBuilderElem::Scalar(decode_scalar(cursor)?)), + 1 => Ok(ArrayBuilderElem::Aggregate(decode_aggregate_array_elem( + cursor, + )?)), + _ => Err(CanonicalCodecError::UnknownTag), + } +} + fn decode_ty(cursor: &mut DecodeCursor<'_>) -> Result { let node = |cursor: &mut DecodeCursor<'_>| cursor.u32(); let tag = cursor.byte()?; @@ -1513,7 +1634,7 @@ fn decode_ty(cursor: &mut DecodeCursor<'_>) -> Result { 23 => Ok(Ty::Writer), 24 => Ok(Ty::Reader), 25 => Ok(Ty::Buffer), - 26 => Ok(Ty::ArrayBuilder(decode_scalar(cursor)?)), + 26 => Ok(Ty::array_builder(decode_array_builder_elem(cursor)?)), 27 => Ok(Ty::StrFinder), 28 => Ok(Ty::File), 29 => Ok(Ty::Rng), @@ -1546,6 +1667,7 @@ fn decode_ty(cursor: &mut DecodeCursor<'_>) -> Result { 56 => Ok(Ty::Unit), 57 => Ok(Ty::Resource(node(cursor)?)), 58 => Ok(Ty::ResourceRef(node(cursor)?)), + 59 => Ok(Ty::dyn_aggregate_array(decode_aggregate_array_elem(cursor)?)), _ => Err(CanonicalCodecError::UnknownTag), } } @@ -1629,11 +1751,21 @@ fn remap_decoded_ty(value: &mut Ty, resolved: &[(u8, u32)]) -> Result<(), Canoni | 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::ArrayBuilder(value) => remap_decoded_scalar(value, resolved), + Ty::VecArrayBuilder(value, _) + | Ty::MaskArrayBuilder(value, _) + | Ty::FixedArrayBuilder(value, _) + | Ty::DynVecArray(value, _) + | Ty::DynMaskArray(value, _) + | Ty::DynFixedArray(value, _) => remap_decoded_scalar(value, resolved), + Ty::FixedStructArrayBuilder(id, _) | Ty::DynFixedStructArray(id, _) => { + *id = resolve_decoded_node(*id, 0, resolved)?; + Ok(()) + } Ty::Result(ok, err) => { remap_decoded_scalar(ok, resolved)?; remap_decoded_scalar(err, resolved) @@ -2140,11 +2272,20 @@ fn type_nodes(value: Ty) -> Vec { | 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::ArrayBuilder(value) => scalar_nodes(value), + Ty::VecArrayBuilder(value, _) + | Ty::MaskArrayBuilder(value, _) + | Ty::FixedArrayBuilder(value, _) + | Ty::DynVecArray(value, _) + | Ty::DynMaskArray(value, _) + | Ty::DynFixedArray(value, _) => scalar_nodes(value), + Ty::FixedStructArrayBuilder(id, _) | Ty::DynFixedStructArray(id, _) => { + vec![Node::Struct(id)] + } Ty::Result(ok, err) => { let mut nodes = scalar_nodes(ok); nodes.extend(scalar_nodes(err)); @@ -2326,6 +2467,56 @@ fn scalar( }) } +fn aggregate_array_elem( + out: &mut Vec, + value: AggregateArrayElem, + ordinal: &impl Fn(Node) -> Result, +) -> Result<(), CanonicalGraphError> { + append_transactional(out, |out| match value { + AggregateArrayElem::Vec(elem, lanes) => { + out.push(0); + scalar(out, elem, ordinal)?; + out.extend(lanes.to_le_bytes()); + Ok(()) + } + AggregateArrayElem::Mask(elem, lanes) => { + out.push(1); + scalar(out, elem, ordinal)?; + out.extend(lanes.to_le_bytes()); + Ok(()) + } + AggregateArrayElem::FixedArray(elem, length) => { + out.push(2); + scalar(out, elem, ordinal)?; + out.extend(length.to_le_bytes()); + Ok(()) + } + AggregateArrayElem::FixedStructArray(id, length) => { + out.push(3); + out.extend(ordinal(Node::Struct(id))?.to_le_bytes()); + out.extend(length.to_le_bytes()); + Ok(()) + } + }) +} + +fn array_builder_elem( + out: &mut Vec, + value: ArrayBuilderElem, + ordinal: &impl Fn(Node) -> Result, +) -> Result<(), CanonicalGraphError> { + append_transactional(out, |out| match value { + ArrayBuilderElem::Scalar(value) => { + out.push(0); + scalar(out, value, ordinal) + } + ArrayBuilderElem::Aggregate(value) => { + out.push(1); + aggregate_array_elem(out, value, ordinal) + } + }) +} + #[allow(dead_code)] fn ty( out: &mut Vec, @@ -2425,7 +2616,18 @@ fn ty( Ty::Buffer => leaf!(25), Ty::ArrayBuilder(v) => { out.push(26); - scalar(out, v, ordinal) + array_builder_elem(out, ArrayBuilderElem::Scalar(v), ordinal) + } + value @ (Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..)) => { + out.push(26); + array_builder_elem( + out, + value.array_builder_element().expect("matched aggregate builder"), + ordinal, + ) } Ty::StrFinder => leaf!(27), Ty::File => leaf!(28), @@ -2467,6 +2669,17 @@ fn ty( Ty::Unit => leaf!(56), Ty::Resource(id) => node!(57, Resource, id), Ty::ResourceRef(id) => node!(58, Resource, id), + value @ (Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => { + out.push(59); + aggregate_array_elem( + out, + value.dyn_aggregate_array_element().expect("matched aggregate array"), + ordinal, + ) + } Ty::Param(_) | Ty::IntVar(_) | Ty::FloatVar(_) | Ty::Error => { Err(CanonicalGraphError::InvalidGraph) } @@ -2682,11 +2895,17 @@ fn remap_ty_fn(value: &mut Ty, remap: &[Option]) { | 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::ArrayBuilder(value) => remap_scalar_fn(value, remap), + Ty::VecArrayBuilder(value, _) + | Ty::MaskArrayBuilder(value, _) + | Ty::FixedArrayBuilder(value, _) + | Ty::DynVecArray(value, _) + | Ty::DynMaskArray(value, _) + | Ty::DynFixedArray(value, _) => remap_scalar_fn(value, remap), Ty::Result(ok, err) => { remap_scalar_fn(ok, remap); remap_scalar_fn(err, remap); @@ -3163,14 +3382,14 @@ mod tests { #[test] fn canonical_graph_engine() { let program = baseline_program(); - assert_eq!(canonical(Ty::Unit, &program).unwrap(), [2, 0, 0, 0, 0, 56]); - assert_eq!(canonical(Ty::Bool, &program).unwrap(), [2, 0, 0, 0, 0, 2]); + assert_eq!(canonical(Ty::Unit, &program).unwrap(), [3, 0, 0, 0, 0, 56]); + assert_eq!(canonical(Ty::Bool, &program).unwrap(), [3, 0, 0, 0, 0, 2]); assert_eq!( canonical(Ty::Int(i(64)), &program).unwrap(), - [2, 0, 0, 0, 0, 0, 1, 64] + [3, 0, 0, 0, 0, 0, 1, 64] ); let bytes = canonical(Ty::Struct(0), &program).unwrap(); - assert_eq!(&bytes[..5], [2, 1, 0, 0, 0]); + assert_eq!(&bytes[..5], [3, 1, 0, 0, 0]); assert_eq!(bytes.last(), Some(&0)); } @@ -3178,9 +3397,9 @@ mod tests { fn canonical_type_codec() { let program = mir_program(&baseline_program()); for (root, expected) in [ - (Ty::Unit, vec![2, 0, 0, 0, 0, 56]), - (Ty::Bool, vec![2, 0, 0, 0, 0, 2]), - (Ty::Int(i(64)), vec![2, 0, 0, 0, 0, 0, 1, 64]), + (Ty::Unit, vec![3, 0, 0, 0, 0, 56]), + (Ty::Bool, vec![3, 0, 0, 0, 0, 2]), + (Ty::Int(i(64)), vec![3, 0, 0, 0, 0, 0, 1, 64]), ] { let encoded = CanonicalTy::from_program(root, &program).unwrap(); assert_eq!(encoded.as_bytes(), expected); @@ -3238,6 +3457,16 @@ mod tests { Ty::Reader, Ty::Buffer, Ty::ArrayBuilder(Scalar::Bool), + Ty::array_builder(ArrayBuilderElem::Aggregate(AggregateArrayElem::Vec( + Scalar::Int(i(8)), + 2, + ))), + Ty::array_builder(ArrayBuilderElem::Aggregate(AggregateArrayElem::Mask( + Scalar::Float(f(32)), + 4, + ))), + Ty::dyn_aggregate_array(AggregateArrayElem::FixedArray(Scalar::Bool, 2)), + Ty::dyn_aggregate_array(AggregateArrayElem::FixedStructArray(0, 2)), Ty::StrFinder, Ty::File, Ty::Rng, @@ -3336,7 +3565,7 @@ mod tests { &program, ) .unwrap(); - assert_eq!(abi.as_bytes(), [1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 56, 0, 0, 0]); + assert_eq!(abi.as_bytes(), [1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 56, 0, 0, 0]); assert_eq!(CanonicalFnAbi::decode(abi.as_bytes()).unwrap(), abi); let params = [(ParamMode::ByValue, Ty::Fn(0))]; @@ -3367,33 +3596,47 @@ mod tests { assert_eq!(CanonicalTy::decode(bytes), Err(expected), "{bytes:02x?}"); }; error(&[], CanonicalCodecError::Truncated); - error(&[3], CanonicalCodecError::UnsupportedVersion); - error(&[2, 0, 0, 0, 0, 0xff], CanonicalCodecError::UnknownTag); - error(&[2, 0, 0, 0, 0, 0, 2, 64], CanonicalCodecError::InvalidBool); + error(&[2], CanonicalCodecError::UnsupportedVersion); + error(&[3, 0, 0, 0, 0, 0xff], CanonicalCodecError::UnknownTag); + error(&[3, 0, 0, 0, 0, 26, 2], CanonicalCodecError::UnknownTag); + error(&[3, 0, 0, 0, 0, 26, 1, 4], CanonicalCodecError::UnknownTag); + error( + &[3, 0, 0, 0, 0, 26, 1, 0, 2, 4, 0, 0, 0], + CanonicalCodecError::InvalidWidth, + ); + error( + &[3, 0, 0, 0, 0, 59, 2, 2, 0, 0, 0, 0], + CanonicalCodecError::InvalidCount, + ); + error( + &[3, 0, 0, 0, 0, 59, 3, 0, 0, 0, 0, 0, 0, 0, 0], + CanonicalCodecError::InvalidCount, + ); + error(&[3, 0, 0, 0, 0, 0, 2, 64], CanonicalCodecError::InvalidBool); error( - &[2, 0, 0, 0, 0, 0, 1, 24], + &[3, 0, 0, 0, 0, 0, 1, 24], CanonicalCodecError::InvalidWidth, ); error( - &[2, 0, 0, 0, 0, 50, 0xff, 0xff, 0xff, 0xff], + &[3, 0, 0, 0, 0, 50, 0xff, 0xff, 0xff, 0xff], CanonicalCodecError::MissingReference, ); - let mut trailing = vec![2, 0, 0, 0, 0, 56]; + let mut trailing = vec![3, 0, 0, 0, 0, 56]; trailing.push(0); error(&trailing, CanonicalCodecError::TrailingBytes); let invalid_utf8 = [ - 2, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, + 3, 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 = [ - 2, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, + 3, 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 = [ - 2, 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, + 3, 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); @@ -3417,22 +3660,22 @@ mod tests { error(&recursive, CanonicalCodecError::InvalidGraph); let duplicate_function = [ - 2, 2, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 0, 52, 0, 0, 0, 0, + 3, 2, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 0, 52, 0, 0, 0, 0, ]; error(&duplicate_function, CanonicalCodecError::DuplicateMember); - let unreachable_function = [2, 1, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 0, 56]; + let unreachable_function = [3, 1, 0, 0, 0, 4, 0, 0, 0, 0, 56, 0, 0, 0, 56]; error( &unreachable_function, CanonicalCodecError::NonCanonicalOrder, ); let invalid_summary = [ - 2, 1, 0, 0, 0, 4, 0, 0, 0, 0, 56, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, + 3, 1, 0, 0, 0, 4, 0, 0, 0, 0, 56, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, ]; error(&invalid_summary, CanonicalCodecError::InvalidSummary); - let unit = [2, 0, 0, 0, 0, 56]; + let unit = [3, 0, 0, 0, 0, 56]; let mut invalid_mode = vec![1, 1, 0, 0, 0, 4]; invalid_mode.extend(unit); invalid_mode.extend(unit); @@ -3496,7 +3739,7 @@ mod tests { second.ty = Ty::Tuple(1); program.structs[0].fields.push(second); let bytes = canonical(Ty::Struct(0), &program).unwrap(); - assert_eq!(&bytes[..5], [2, 3, 0, 0, 0]); + assert_eq!(&bytes[..5], [3, 3, 0, 0, 0]); let mut permuted = program.clone(); permuted.tuples.swap(0, 1); @@ -3595,7 +3838,7 @@ mod tests { program.structs.push(definition); } let bytes = canonical(Ty::Struct(0), &program).unwrap(); - assert_eq!(&bytes[..5], [2, 0, 16, 0, 0]); + assert_eq!(&bytes[..5], [3, 0, 16, 0, 0]); } #[test] @@ -3691,6 +3934,8 @@ mod tests { Scalar::HttpServer => [28], Scalar::HttpRequestCtx => [29], Scalar::ResponseBuilder => [30], Scalar::HttpStream => [31], Scalar::RunOutput => [32], Scalar::Fn(1) => [33, 1, 0x50, 0, 0], + Scalar::Resource(1) => [34, 1, 0x60, 0, 0], + Scalar::ResourceRef(1) => [35, 1, 0x60, 0, 0], ); } @@ -3711,7 +3956,12 @@ mod tests { Ty::DynArray(Scalar::Bool) => [16, 2], Ty::DynResponseArray => [17], Ty::Str => [18], Ty::String => [19], Ty::ArenaHandle => [20], Ty::Raw => [21], Ty::Builder => [22], Ty::Writer => [23], Ty::Reader => [24], Ty::Buffer => [25], - Ty::ArrayBuilder(Scalar::Bool) => [26, 2], Ty::StrFinder => [27], + Ty::ArrayBuilder(Scalar::Bool) => [26, 0, 2], + Ty::array_builder(ArrayBuilderElem::Aggregate(AggregateArrayElem::Vec( + Scalar::Int(i(8)), 2, + ))) + => [26, 1, 0, 0, 1, 8, 2, 0, 0, 0], + Ty::StrFinder => [27], Ty::File => [28], Ty::Rng => [29], Ty::Regex => [30], Ty::Captures => [31], Ty::CliCommand => [32], Ty::CliParsed => [33], Ty::TcpConn => [34], Ty::TcpListener => [35], Ty::UdpSocket => [36], Ty::Child => [37], @@ -3724,6 +3974,14 @@ mod tests { Ty::Fn(1) => [52, 1, 0x50, 0, 0], Ty::Enum(1) => [53, 1, 0x20, 0, 0], Ty::Task(Scalar::Bool) => [54, 2], Ty::DictEncoded(1, 2) => [55, 1, 0x10, 0, 0, 2, 0, 0, 0], Ty::Unit => [56], + Ty::Resource(1) => [57, 1, 0x60, 0, 0], + Ty::ResourceRef(1) => [58, 1, 0x60, 0, 0], + Ty::dyn_aggregate_array(AggregateArrayElem::FixedArray(Scalar::Bool, 2)) + => [59, 2, 2, 2, 0, 0, 0], + Ty::dyn_aggregate_array(AggregateArrayElem::Mask(Scalar::Float(f(32)), 4)) + => [59, 1, 1, 32, 4, 0, 0, 0], + Ty::dyn_aggregate_array(AggregateArrayElem::FixedStructArray(1, 2)) + => [59, 3, 1, 0x10, 0, 0, 2, 0, 0, 0], ); } diff --git a/crates/align_mir/src/generated_id.rs b/crates/align_mir/src/generated_id.rs index c9c8851e..455e97bf 100644 --- a/crates/align_mir/src/generated_id.rs +++ b/crates/align_mir/src/generated_id.rs @@ -542,12 +542,12 @@ mod tests { #[test] fn generated_identity_codec() { - let unit = ty("020000000038"); - let bool_ty = ty("020000000002"); - let i64_ty = ty("0200000000000140"); - let slice_i64 = ty("02000000000d000140"); - let empty_abi = abi("0100000000020000000038000000"); - let i64_abi = abi("01010000000002000000000001400200000000000140000000"); + let unit = ty("030000000038"); + let bool_ty = ty("030000000002"); + let i64_ty = ty("0300000000000140"); + let slice_i64 = ty("03000000000d000140"); + let empty_abi = abi("0100000000030000000038000000"); + let i64_abi = abi("01010000000003000000000001400300000000000140000000"); let goldens = [ ( @@ -555,7 +555,7 @@ mod tests { target: call("f"), signature: empty_abi.clone(), }, - "010001000000660100000000020000000038000000", + "010001000000660100000000030000000038000000", ), ( GeneratedId::Closure { @@ -563,21 +563,21 @@ mod tests { explicit_signature: empty_abi.clone(), captures: vec![bool_ty.clone()], }, - "0101010000006c010000000002000000003800000001000000020000000002", + "0101010000006c010000000003000000003800000001000000030000000002", ), ( GeneratedId::Task { fallible: false, result: unit.clone(), }, - "010200020000000038", + "010200030000000038", ), ( GeneratedId::Task { fallible: true, result: i64_ty.clone(), }, - "0102010200000000000140", + "0102010300000000000140", ), ]; for (value, expected) in goldens { @@ -598,7 +598,7 @@ mod tests { work_weight: 1, }); let expected = hex( - "01030002000000000d00014002000000000001400200000000000140010000006601010000000002000000000001400200000000000140000000000000000000000001", + "01030003000000000d00014003000000000001400300000000000140010000006601010000000003000000000001400300000000000140000000000000000000000001", ); assert_eq!(roundtrip(parallel.clone()), expected); assert_eq!(GeneratedId::decode(&expected).unwrap(), parallel); @@ -644,7 +644,7 @@ mod tests { terminal_input: i64_ty.clone(), terminal_output: i64_ty.clone(), terminal: call("terminal"), - terminal_abi: abi("0100000000020000000038000000"), + terminal_abi: abi("0100000000030000000038000000"), terminal_captures: vec![bool_ty.clone()], stages: stages.clone(), work_weight: 4, @@ -685,7 +685,7 @@ mod tests { let valid = GeneratedId::Task { fallible: false, - result: ty("020000000038"), + result: ty("030000000038"), } .to_canonical_bytes() .unwrap(); @@ -698,11 +698,11 @@ mod tests { let invalid_parallel = GeneratedId::Parallel(ParallelGeneratedId { mode: ParallelKernelMode::FilterCount, - source: ty("020000000038"), - terminal_input: ty("020000000038"), - terminal_output: ty("020000000038"), + source: ty("030000000038"), + terminal_input: ty("030000000038"), + terminal_output: ty("030000000038"), terminal: call("f"), - terminal_abi: abi("0100000000020000000038000000"), + terminal_abi: abi("0100000000030000000038000000"), terminal_captures: vec![], stages: vec![], work_weight: 3, @@ -717,8 +717,8 @@ mod tests { fn deep_generated_identity_codec_is_stack_bounded() { let value = GeneratedId::Closure { lifted: call("deep"), - explicit_signature: abi("0100000000020000000038000000"), - captures: vec![ty("020000000038"); 4096], + explicit_signature: abi("0100000000030000000038000000"), + captures: vec![ty("030000000038"); 4096], }; let bytes = value.to_canonical_bytes().unwrap(); assert_eq!(GeneratedId::decode(&bytes).unwrap(), value); diff --git a/crates/align_mir/src/lib.rs b/crates/align_mir/src/lib.rs index c899eac9..75b329ec 100644 --- a/crates/align_mir/src/lib.rs +++ b/crates/align_mir/src/lib.rs @@ -761,6 +761,9 @@ pub enum Rvalue { /// `str.clone()` — deep-copy a `str` operand's bytes into a fresh heap buffer, yielding an /// owned `string` `{ptr,len}`. The buffer is freed by a later [`Stmt::Drop`] of its slot. StrClone(Operand), + /// Copy a `str`/`bytes` view into the exact explicit arena handle, yielding a same-shaped view. + /// Codegen emits the arena allocation and byte copy directly; there is no ambient allocator. + CloneIn { value: Operand, handle: Operand }, /// `s.contains(n)` / `s.starts_with(p)` / `s.ends_with(s)` — a byte-oriented `str` predicate, /// yielding `bool` (`i1`). Both operands are `str` `{ptr,len}` views; backed by a runtime /// `memchr`-class scan. Pure read, no allocation. @@ -959,11 +962,12 @@ pub enum Rvalue { /// `buf.append(data)` — append the raw `slice` operand `data` (copied) to the growable /// `buffer` operand, growing it. BufferAppend { buffer: Operand, data: Operand }, - /// `array_builder()` (M12 A6) — open an empty typed array builder, yielding an opaque handle. - /// `elem_size` is the element stride in bytes (16 for a `string` element). - ArrayBuilderNew { elem_size: i64 }, - /// `b.push(v)` — append one Copy-scalar element (the `value` operand, passed as its raw bits in an - /// `i64`; `elem_size` sets how many low bytes) to the growable `array_builder` operand. + /// Open an empty typed array builder. `region` selects arena-backed chunk storage; `None` + /// preserves the existing individually-owned heap form. Physical element layout is computed by + /// the target backend from `elem`. + ArrayBuilderNew { elem: Ty, region: Option }, + /// `b.push(v)` — append one primitive scalar element (the `value` operand, passed as its raw + /// bits in an `i64`; `elem_size` sets how many low bytes) to the growable builder operand. ArrayBuilderPush { builder: Operand, value: Operand, scalar: Ty }, /// `b.push(s)` — append one moved-in `string` element (the `value` operand, a `{ptr,len}`) to the /// growable `array_builder` operand. The source string is nulled at the move site. @@ -972,8 +976,9 @@ pub enum Rvalue { /// elements) to the growable `array_builder` operand. `data`'s `len` is the element count; the /// element stride is stored in the builder header (set at construction), so no stride here. ArrayBuilderAppend { builder: Operand, data: Operand }, - /// `b.build()` — freeze the `array_builder` operand into an owned `array` `{ptr,len}` (a - /// zero-copy ptr+len retype), consuming the builder (its slot is nulled at the move site). + /// `b.build()` — freeze the builder into an `array` `{ptr,len}`, consuming it (its slot is + /// nulled at the move site). Heap storage transfers zero-copy; region chunks compact once into + /// the same arena. ArrayBuilderBuild { builder: Operand }, /// `fs.write_file(path, data)` — write all of the `str`/`bytes` operand `data` to `path`, then /// close. Yields an `i32` errno-status (0 = ok). @@ -1915,6 +1920,7 @@ pub fn function_embedded_types(f: &Function) -> Vec { | Rvalue::BytesRead { scalar: elem, .. } | Rvalue::BufferPut { scalar: elem, .. } | Rvalue::ArrayBuilderPush { scalar: elem, .. } => types.push(*elem), + Rvalue::ArrayBuilderNew { elem, .. } => types.push(*elem), Rvalue::ParMapParallel { stages, capture_tys, @@ -1986,8 +1992,16 @@ fn canonicalize_tagged_types(program: &mut Program) { | Ty::Box(s) | Ty::Slice(s) | Ty::DynArray(s) - | Ty::ArrayBuilder(s) - | Ty::Task(s) => collect_scalar(s, table, reachable), + | Ty::Task(s) => { + collect_scalar(s, table, reachable) + } + Ty::ArrayBuilder(s) => collect_scalar(s, table, reachable), + Ty::VecArrayBuilder(s, _) + | Ty::MaskArrayBuilder(s, _) + | Ty::FixedArrayBuilder(s, _) + | Ty::DynVecArray(s, _) + | Ty::DynMaskArray(s, _) + | Ty::DynFixedArray(s, _) => collect_scalar(s, table, reachable), Ty::Result(ok, err) => { collect_scalar(ok, table, reachable); collect_scalar(err, table, reachable); @@ -2155,8 +2169,16 @@ fn canonicalize_tagged_types(program: &mut Program) { | Ty::Box(s) | Ty::Slice(s) | Ty::DynArray(s) - | Ty::ArrayBuilder(s) - | Ty::Task(s) => remap_scalar(s, remap), + | Ty::Task(s) => { + remap_scalar(s, remap) + } + Ty::ArrayBuilder(s) => remap_scalar(s, remap), + Ty::VecArrayBuilder(s, _) + | Ty::MaskArrayBuilder(s, _) + | Ty::FixedArrayBuilder(s, _) + | Ty::DynVecArray(s, _) + | Ty::DynMaskArray(s, _) + | Ty::DynFixedArray(s, _) => remap_scalar(s, remap), Ty::Result(ok, err) => { remap_scalar(ok, remap); remap_scalar(err, remap); @@ -2313,6 +2335,7 @@ fn remap_function_embedded_types( | Rvalue::BytesRead { scalar, .. } | Rvalue::BufferPut { scalar, .. } | Rvalue::ArrayBuilderPush { scalar, .. } => remap_ty(scalar, remap), + Rvalue::ArrayBuilderNew { elem, .. } => remap_ty(elem, remap), Rvalue::ParMapParallel { stages, capture_tys, @@ -3245,6 +3268,7 @@ fn null_moved_source(b: &mut Builder, e: &hir::Expr) { } hir::ExprKind::Block(blk) | hir::ExprKind::Arena(blk) + | hir::ExprKind::NamedArena { block: blk, .. } | hir::ExprKind::Unsafe(blk) | hir::ExprKind::TaskGroup(blk) => { if let Some(v) = &blk.value { @@ -3330,6 +3354,7 @@ fn moved_drop_flag(b: &mut Builder, e: &hir::Expr) -> Option { } hir::ExprKind::Block(blk) | hir::ExprKind::Arena(blk) + | hir::ExprKind::NamedArena { block: blk, .. } | hir::ExprKind::Unsafe(blk) | hir::ExprKind::TaskGroup(blk) => { blk.value.as_ref().and_then(|v| moved_drop_flag(b, v)) @@ -3370,7 +3395,10 @@ fn temporary_drop_flag(b: &mut Builder, e: &hir::Expr, operand: &Operand) -> Opt | hir::ExprKind::TupleIndex { .. } | hir::ExprKind::Index { .. } | hir::ExprKind::ElemField { .. } => Some(Operand::Const(Const::Bool(false))), - hir::ExprKind::Block(block) | hir::ExprKind::Unsafe(block) | hir::ExprKind::Arena(block) => { + hir::ExprKind::Block(block) + | hir::ExprKind::Unsafe(block) + | hir::ExprKind::Arena(block) + | hir::ExprKind::NamedArena { block, .. } => { block.value.as_ref().and_then(|value| temporary_drop_flag(b, value, operand)) } // A TaskGroup reaches this arm only when its tail can be fresh: may_need_synthetic_owner @@ -3406,6 +3434,20 @@ fn lower_expr_for_borrow(b: &mut Builder, e: &hir::Expr) -> Operand { tail.unwrap_or(Operand::Const(Const::Unit)) } } + hir::ExprKind::NamedArena { local, block } => { + let handle = b.fresh_value(Ty::ArenaHandle); + b.push(Stmt::Let(handle, Rvalue::ArenaBegin)); + b.push(Stmt::Store(*local, Operand::Value(handle))); + b.arenas.push(handle); + let tail = lower_block_for_borrow(b, block); + b.arenas.pop(); + if !lowering_continues(b) { + Operand::Const(Const::Unit) + } else { + b.push(Stmt::ArenaEnd(Operand::Value(handle))); + tail.unwrap_or(Operand::Const(Const::Unit)) + } + } hir::ExprKind::TaskGroup(block) => { let handle = b.fresh_value(Ty::ArenaHandle); b.push(Stmt::Let(handle, Rvalue::TgBegin)); @@ -4492,6 +4534,7 @@ fn expression_uses_out_of_line_dispatch(e: &hir::Expr) -> bool { | hir::ExprKind::Block(_) | hir::ExprKind::Unsafe(_) | hir::ExprKind::Arena(_) + | hir::ExprKind::NamedArena { .. } | hir::ExprKind::TaskGroup(_) | hir::ExprKind::Template(_) | hir::ExprKind::FileCreateRw { .. } @@ -4613,6 +4656,7 @@ fn lower_out_of_line_expr(b: &mut Builder, e: &hir::Expr) -> Operand { hir::ExprKind::Loop { .. } => lower_loop(b, e), hir::ExprKind::Block(_) | hir::ExprKind::Unsafe(_) => lower_plain_block_spine(b, e), hir::ExprKind::Arena(block) => lower_arena_block(b, block), + hir::ExprKind::NamedArena { local, block } => lower_named_arena_block(b, *local, block), hir::ExprKind::TaskGroup(block) => lower_task_group_block(b, block), hir::ExprKind::Template(_) => lower_template_spine(b, e), hir::ExprKind::FileCreateRw { .. } @@ -4697,6 +4741,22 @@ fn lower_arena_block(b: &mut Builder, block: &hir::Block) -> Operand { } } +#[inline(never)] +fn lower_named_arena_block(b: &mut Builder, local: hir::LocalId, block: &hir::Block) -> Operand { + let handle = b.fresh_value(Ty::ArenaHandle); + b.push(Stmt::Let(handle, Rvalue::ArenaBegin)); + b.push(Stmt::Store(local, Operand::Value(handle))); + b.arenas.push(handle); + let tail = lower_block(b, block); + b.arenas.pop(); + if !lowering_continues(b) { + terminated_operand() + } else { + b.push(Stmt::ArenaEnd(Operand::Value(handle))); + tail.unwrap_or(Operand::Const(Const::Unit)) + } +} + #[inline(never)] fn lower_task_group_block(b: &mut Builder, block: &hir::Block) -> Operand { let handle = b.fresh_value(Ty::ArenaHandle); @@ -6184,6 +6244,20 @@ fn lower_expr_recursive(b: &mut Builder, e: &hir::Expr) -> Operand { tail.unwrap_or(Operand::Const(Const::Unit)) } } + hir::ExprKind::NamedArena { local, block } => { + let handle = b.fresh_value(Ty::ArenaHandle); + b.push(Stmt::Let(handle, Rvalue::ArenaBegin)); + b.push(Stmt::Store(*local, Operand::Value(handle))); + b.arenas.push(handle); + let tail = lower_block(b, block); + b.arenas.pop(); + if !lowering_continues(b) { + Operand::Const(Const::Unit) + } else { + b.push(Stmt::ArenaEnd(Operand::Value(handle))); + tail.unwrap_or(Operand::Const(Const::Unit)) + } + } hir::ExprKind::HeapNew(inner) => { lower_required_binding!( b, @@ -6226,6 +6300,13 @@ fn lower_expr_recursive(b: &mut Builder, e: &hir::Expr) -> Operand { b.push(Stmt::Let(v, Rvalue::StrClone(src))); Operand::Value(v) } + hir::ExprKind::CloneIn { value, region } => { + lower_required_binding!(b, src = lower_expr(b, value), Operand::Const(Const::Unit)); + lower_required_binding!(b, handle = lower_expr(b, region), Operand::Const(Const::Unit)); + let v = b.fresh_value(e.ty); + b.push(Stmt::Let(v, Rvalue::CloneIn { value: src, handle })); + Operand::Value(v) + } hir::ExprKind::StrPredicate { kind, haystack, @@ -7880,32 +7961,23 @@ fn lower_bytes_read(b: &mut Builder, bytes: &hir::Expr, offset: &hir::Expr, be: /// `buf.put__(v)` → append `v`'s bytes to the growable buffer. A unit-valued /// side-effecting rvalue (the runtime grows the buffer); returns `()`. -/// The element stride (bytes) an `array_builder` stores per element — its -/// `align_rt_alloc`/`align_rt_realloc` buffer is `len * elem_size` bytes. A `string` element is a -/// 16-byte `{ptr,len}` (`AlignStr`); a Copy scalar is its machine width. Only the v1 element set -/// (Copy scalar or `string`) reaches here — anything else was rejected at the type. -fn array_builder_elem_size(elem: align_sema::Scalar) -> i64 { - use align_sema::Scalar; - match elem { - Scalar::Int(it) => (it.bits / 8).max(1) as i64, - Scalar::Float(ft) => (ft.bits / 8).max(1) as i64, - Scalar::Bool => 1, - Scalar::Char => 4, - Scalar::String => 16, - _ => 16, - } -} - /// Lower an `array_builder` op (M12 A6): new opens a builder sized to the element stride; /// push/append grow it (`push` of a `string` element moves the value in — null its source slot); -/// build freezes it into an owned `array` (consuming — null the builder slot). Out-of-line -/// (`#[inline(never)]`) so its arm locals stay off the recursive `lower_expr` frame (#296). +/// build freezes it into `array` (consuming — null the builder slot). The backend derives exact +/// target layout for region-plain aggregate copies. Out-of-line (`#[inline(never)]`) so its arm +/// locals stay off the recursive `lower_expr` frame (#296). #[inline(never)] fn lower_array_builder_expr(b: &mut Builder, e: &hir::Expr) -> Operand { match &e.kind { - hir::ExprKind::ArrayBuilderNew { elem } => { + hir::ExprKind::ArrayBuilderNew { elem, region } => { + let region = region.as_deref().map(|region| { + lower_required!(b, lower_expr(b, region), Operand::Const(Const::Unit)) + }); let v = b.fresh_value(e.ty); - b.push(Stmt::Let(v, Rvalue::ArrayBuilderNew { elem_size: array_builder_elem_size(*elem) })); + b.push(Stmt::Let(v, Rvalue::ArrayBuilderNew { + elem: elem.ty(), + region, + })); Operand::Value(v) } hir::ExprKind::ArrayBuilderPush { builder, value, moves_value } => { @@ -8120,7 +8192,15 @@ fn lower_index(b: &mut Builder, recv: &hir::Expr, index: &hir::Expr, elem_ty: Ty // `array` loads a whole struct element; an `array` loads a `response` handle // pointer (the receiver-borrow of `rs[i].status()` etc. — `elem_ty` = `HttpResponse`). All by // `elem_ty` via `SliceIndex`. - Ty::Slice(_) | Ty::DynArray(_) | Ty::DynSliceArray(_) | Ty::DynStructArray(..) | Ty::DynResponseArray => { + Ty::Slice(_) + | Ty::DynArray(_) + | Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..) + | Ty::DynSliceArray(_) + | Ty::DynStructArray(..) + | Ty::DynResponseArray => { let sv = lower_borrowed_owned(b, recv); if !lowering_continues(b) { return Operand::Const(Const::Unit); @@ -14092,7 +14172,10 @@ fn match_scrutinee_transfers_source_to_owner(e: &hir::Expr) -> bool { | hir::ExprKind::EnumValue { .. } | hir::ExprKind::TaskGet(_) | hir::ExprKind::TaskGroup(_) => true, - hir::ExprKind::Block(block) | hir::ExprKind::Unsafe(block) | hir::ExprKind::Arena(block) => { + hir::ExprKind::Block(block) + | hir::ExprKind::Unsafe(block) + | hir::ExprKind::Arena(block) + | hir::ExprKind::NamedArena { block, .. } => { block .value .as_deref() @@ -14642,25 +14725,44 @@ pub fn ty_name(ty: Ty) -> String { Ty::Raw => "raw".to_string(), Ty::Resource(id) => format!("resource#{id}"), Ty::ResourceRef(id) => format!("resource_ref"), - Ty::Array(_, n) | Ty::StructArray(_, n) => format!("array[{n}]"), + Ty::Array(element, n) => { + format!("array<{}>[{n}]", ty_name(align_sema::scalar_to_ty(element))) + } + Ty::StructArray(id, n) => format!("array[{n}]"), Ty::Slice(_) => "slice".to_string(), Ty::Vec(_, n) => format!("vec{n}"), Ty::Mask(_, n) => format!("mask{n}"), Ty::Soa(id) => format!("soa"), // Keep the human-readable MIR type name element-aware. Ty::DynArray(s) => format!("array<{}>", ty_name(align_sema::scalar_to_ty(s))), + ty @ (Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => format!( + "array<{}>", + ty_name(ty.dyn_aggregate_array_element().expect("matched aggregate array").ty()) + ), Ty::DynStructArray(id, _) => format!("array"), Ty::DynSliceArray(_) => "array".to_string(), Ty::DynResponseArray => "array".to_string(), Ty::Str => "str".to_string(), Ty::String => "string".to_string(), - Ty::ArenaHandle => "arena".to_string(), + Ty::ArenaHandle => "region".to_string(), Ty::Builder => "builder".to_string(), Ty::StrFinder => "str_finder".to_string(), Ty::Writer => "writer".to_string(), Ty::Reader => "reader".to_string(), Ty::Buffer => "buffer".to_string(), - Ty::ArrayBuilder(_) => "array_builder".to_string(), + Ty::ArrayBuilder(element) => { + format!("array_builder<{}>", ty_name(align_sema::scalar_to_ty(element))) + } + ty @ (Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..)) => format!( + "array_builder<{}>", + ty_name(ty.array_builder_element().expect("matched aggregate builder").ty()) + ), Ty::File => "file".to_string(), Ty::Rng => "rng".to_string(), Ty::CliCommand => "cli command".to_string(), diff --git a/crates/align_mir/src/print.rs b/crates/align_mir/src/print.rs index 80d6a811..93a9669d 100644 --- a/crates/align_mir/src/print.rs +++ b/crates/align_mir/src/print.rs @@ -488,6 +488,9 @@ fn rvalue_str(rv: &Rvalue) -> String { format!("const_array[{}] : {} = {}", elems.len(), ty_name(*elem), const_elems_str(elems)) } Rvalue::StrClone(op) => format!("str_clone({})", operand_str(op)), + Rvalue::CloneIn { value, handle } => { + format!("clone_in({}, {})", operand_str(value), operand_str(handle)) + } Rvalue::StrPredicate { kind, haystack, needle } => { let name = match kind { align_sema::hir::StrPredKind::Contains => "str_contains", @@ -615,7 +618,10 @@ fn rvalue_str(rv: &Rvalue) -> String { format!("buffer_put{}({}, {})", if *be { "_be" } else { "_le" }, operand_str(buffer), operand_str(value)) } Rvalue::BufferAppend { buffer, data } => format!("buffer_append({}, {})", operand_str(buffer), operand_str(data)), - Rvalue::ArrayBuilderNew { elem_size } => format!("array_builder_new(elem_size={elem_size})"), + Rvalue::ArrayBuilderNew { elem, region } => format!( + "array_builder_new(elem={elem:?}, region={})", + region.as_ref().map_or_else(|| "heap".to_string(), operand_str) + ), Rvalue::ArrayBuilderPush { builder, value, .. } => format!("array_builder_push({}, {})", operand_str(builder), operand_str(value)), Rvalue::ArrayBuilderPushStr { builder, value } => format!("array_builder_push_str({}, {})", operand_str(builder), operand_str(value)), Rvalue::ArrayBuilderAppend { builder, data } => format!("array_builder_append({}, {})", operand_str(builder), operand_str(data)), diff --git a/crates/align_mir/src/runtime_key.rs b/crates/align_mir/src/runtime_key.rs index 24305433..2f411fe8 100644 --- a/crates/align_mir/src/runtime_key.rs +++ b/crates/align_mir/src/runtime_key.rs @@ -11,7 +11,7 @@ macro_rules! runtime_keys { } impl RuntimeKey { - pub const ALL: [Self; 281] = [$(Self::$variant,)*]; + pub const ALL: [Self; 283] = [$(Self::$variant,)*]; pub const fn logical_name(self) -> &'static str { match self { @@ -37,7 +37,9 @@ runtime_keys! { ArrayBuilderFreeStringsStack => "array_builder_free_strings_stack", ArrayBuilderInitStack => "array_builder_init_stack", ArrayBuilderNew => "array_builder_new", + ArrayBuilderNewIn => "array_builder_new_in", ArrayBuilderPush => "array_builder_push", + ArrayBuilderPushBytes => "array_builder_push_bytes", ArrayBuilderPushStr => "array_builder_push_str", Base64Decode => "base64_decode", Base64Encode => "base64_encode", @@ -306,7 +308,7 @@ runtime_keys! { Utf8Valid => "utf8_valid", } -const _: [(); 281] = [(); RuntimeKey::ALL.len()]; +const _: [(); 283] = [(); RuntimeKey::ALL.len()]; #[cfg(test)] mod tests { @@ -315,7 +317,7 @@ mod tests { #[test] fn runtime_keys_are_complete_unique_and_alphabetical() { - assert_eq!(RuntimeKey::ALL.len(), 281); + assert_eq!(RuntimeKey::ALL.len(), 283); let names: Vec<_> = RuntimeKey::ALL .iter() .map(|key| key.logical_name()) diff --git a/crates/align_mir/src/source_shape.rs b/crates/align_mir/src/source_shape.rs index 6b005431..951e1fcd 100644 --- a/crates/align_mir/src/source_shape.rs +++ b/crates/align_mir/src/source_shape.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use align_ast::ParamMode; -use align_sema::{Scalar, Ty, hir}; +use align_sema::{AggregateArrayElem, Scalar, Ty, hir}; use super::canonical_graph::Node; @@ -382,6 +382,25 @@ impl SourceShapeCo } } + fn aggregate_array_elems_equal( + &mut self, + left: AggregateArrayElem, + right: AggregateArrayElem, + ) -> bool { + match (left, right) { + (AggregateArrayElem::Vec(left, a), AggregateArrayElem::Vec(right, b)) + | (AggregateArrayElem::Mask(left, a), AggregateArrayElem::Mask(right, b)) + | (AggregateArrayElem::FixedArray(left, a), AggregateArrayElem::FixedArray(right, b)) => { + a == b && self.scalars_equal(left, right) + } + ( + AggregateArrayElem::FixedStructArray(left, a), + AggregateArrayElem::FixedStructArray(right, b), + ) => a == b && self.queue_equal(Node::Struct(left), Node::Struct(right)), + _ => false, + } + } + fn types_equal(&mut self, left: Ty, right: Ty) -> bool { macro_rules! same { ($pattern:pat => $body:expr) => { @@ -413,9 +432,37 @@ impl SourceShapeCo } Ty::Slice(left) => same!(Ty::Slice(right) => self.scalars_equal(left, right)), Ty::DynArray(left) => same!(Ty::DynArray(right) => self.scalars_equal(left, right)), + left @ (Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => right + .dyn_aggregate_array_element() + .is_some_and(|right| { + self.aggregate_array_elems_equal( + left.dyn_aggregate_array_element().expect("matched aggregate array"), + right, + ) + }), Ty::ArrayBuilder(left) => { same!(Ty::ArrayBuilder(right) => self.scalars_equal(left, right)) } + left @ (Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..)) => right + .array_builder_element() + .and_then(|element| match element { + align_sema::ArrayBuilderElem::Aggregate(element) => Some(element), + align_sema::ArrayBuilderElem::Scalar(_) => None, + }) + .is_some_and(|right| { + let align_sema::ArrayBuilderElem::Aggregate(left) = + left.array_builder_element().expect("matched aggregate builder") + else { + unreachable!() + }; + self.aggregate_array_elems_equal(left, right) + }), Ty::Task(left) => same!(Ty::Task(right) => self.scalars_equal(left, right)), Ty::Tagged(left) => node!(Tagged, Tagged, left), Ty::StructArray(left, a) => { @@ -507,8 +554,15 @@ fn ty_cost(value: Ty) -> (usize, usize) { | Ty::Box(value) | Ty::Slice(value) | Ty::DynArray(value) - | Ty::ArrayBuilder(value) | Ty::Task(value) => scalar_cost(value), + Ty::ArrayBuilder(value) => scalar_cost(value), + Ty::VecArrayBuilder(value, _) + | Ty::MaskArrayBuilder(value, _) + | Ty::FixedArrayBuilder(value, _) + | Ty::DynVecArray(value, _) + | Ty::DynMaskArray(value, _) + | Ty::DynFixedArray(value, _) => scalar_cost(value), + Ty::FixedStructArrayBuilder(..) | Ty::DynFixedStructArray(..) => (1, 1), Ty::Result(left, right) => { let left = scalar_cost(left); let right = scalar_cost(right); @@ -666,6 +720,18 @@ pub(super) mod tests { program.tuples.push(program.tuples[0].clone()); program.tagged_types.push(program.tagged_types[0]); program.fn_types.push(program.fn_types[0].clone()); + let resource = hir::ResourceDef { + name: "pkg$Resource".into(), + source_name: "Resource".into(), + declaring_module: "pkg".into(), + generic_arity: 0, + drop_hook: "pkg$drop_resource".into(), + drop_thunk: "pkg$drop_resource$thunk".into(), + representation_version: 1, + drop_abi_fingerprint: [7; 16], + }; + program.resources.push(resource.clone()); + program.resources.push(resource); program } fn equal(view: &(impl SourceShapeView + ?Sized), left: Node, right: Node) -> bool { diff --git a/crates/align_mir/src/validate_hir.rs b/crates/align_mir/src/validate_hir.rs index 9307bddb..d958212d 100644 --- a/crates/align_mir/src/validate_hir.rs +++ b/crates/align_mir/src/validate_hir.rs @@ -1,6 +1,8 @@ use std::collections::{HashMap, HashSet, VecDeque}; -use align_sema::{Layout, PrimScalar, Scalar, Ty, hir}; +use align_sema::{ + AggregateArrayElem, ArrayBuilderElem, Layout, PrimScalar, Scalar, Ty, hir, +}; use align_span::Span; use super::canonical_graph::Node; @@ -538,7 +540,7 @@ fn mode_is_valid( &program.tagged_types, ) } - align_ast::ParamMode::BorrowMut => true, + align_ast::ParamMode::BorrowMut => ty != Ty::ArenaHandle, } } @@ -1205,7 +1207,7 @@ impl<'a> PlacementValidator<'a> { Ty::Param(_) => allow_param, Ty::Int(integer) => valid_int(integer.bits), Ty::Float(float) => valid_float(float.bits), - Ty::Bool | Ty::Char | Ty::Str | Ty::String | Ty::Unit | Ty::Raw => true, + Ty::Bool | Ty::Char | Ty::Str | Ty::String | Ty::Unit | Ty::Raw | Ty::ArenaHandle => true, Ty::Option(payload) => { self.scalar_ok(payload, ScalarPlacement::Payload { allow_param }) } @@ -1229,10 +1231,21 @@ impl<'a> PlacementValidator<'a> { !matches!(element, Scalar::Struct(_)) && self.scalar_ok(element, ScalarPlacement::Collection) } + ty @ (Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => self.aggregate_array_element_ok( + ty.dyn_aggregate_array_element().expect("matched aggregate array"), + ), Ty::Soa(id) => self.soa_ok(id), - Ty::ArrayBuilder(element) => matches!( - element, - Scalar::Int(_) | Scalar::Float(_) | Scalar::Bool | Scalar::Char | Scalar::String + Ty::ArrayBuilder(element) => { + self.array_builder_element_ok(ArrayBuilderElem::Scalar(element)) + } + ty @ (Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..)) => self.array_builder_element_ok( + ty.array_builder_element().expect("matched aggregate builder"), ), Ty::JsonScanner(id) => self.program.structs.get(id as usize).is_some(), Ty::Struct(id) => self.program.structs.get(id as usize).is_some(), @@ -1276,7 +1289,6 @@ impl<'a> PlacementValidator<'a> { | Ty::DynSliceArray(_) | Ty::DynResponseArray | Ty::Task(_) - | Ty::ArenaHandle | Ty::Builder | Ty::StrFinder | Ty::DictEncoded(..) @@ -1285,10 +1297,60 @@ impl<'a> PlacementValidator<'a> { } } + /// Mirror the exact union admitted by source `array_builder` type formation. Constructor + /// validation separately selects the heap subset or recursively checks concrete RegionPlain; + /// this header gate only proves that the stored scalar graph is a valid source spelling. + fn aggregate_array_element_ok(&self, element: AggregateArrayElem) -> bool { + let shape_ok = match element { + AggregateArrayElem::Vec(scalar, lanes) + | AggregateArrayElem::Mask(scalar, lanes) => { + matches!(lanes, 2 | 4 | 8 | 16) + && matches!(scalar, Scalar::Int(_) | Scalar::Float(_)) + && self.resolve_type_ok(element.ty(), false) + } + AggregateArrayElem::FixedArray(scalar, length) => { + length > 0 + && !matches!(scalar, Scalar::Struct(_)) + && self.scalar_ok(scalar, ScalarPlacement::Collection) + } + AggregateArrayElem::FixedStructArray(id, length) => { + length > 0 && self.program.structs.get(id as usize).is_some() + } + }; + shape_ok + && align_sema::region_plain_type_ok( + element.ty(), + &self.program.structs, + &self.program.enums, + &self.program.tagged_types, + ) + } + + fn array_builder_element_ok(&self, element: ArrayBuilderElem) -> bool { + match element { + ArrayBuilderElem::Scalar(element) => { + matches!( + element, + Scalar::Int(_) + | Scalar::Float(_) + | Scalar::Bool + | Scalar::Char + | Scalar::String + | Scalar::Str + | Scalar::Slice(_) + | Scalar::Struct(_) + | Scalar::Enum(_) + | Scalar::Tagged(_) + ) && self.resolve_type_ok(align_sema::scalar_to_ty(element), false) + } + ArrayBuilderElem::Aggregate(element) => self.aggregate_array_element_ok(element), + } + } + fn source_function_type_ok(&self, ty: Ty, parameter: bool, return_position: bool) -> bool { self.resolve_type_ok(ty, false) && !(parameter && matches!(ty, Ty::Box(_))) - && !(return_position && matches!(ty, Ty::Box(_) | Ty::Fn(_))) + && !(return_position && matches!(ty, Ty::Box(_) | Ty::Fn(_) | Ty::ArenaHandle)) } fn stored_function_parameter_ok(&self, function: &hir::Fn, index: usize, ty: Ty) -> bool { @@ -1633,8 +1695,21 @@ impl<'a> Validator<'a> { Ty::Box(payload) | Ty::Slice(payload) | Ty::DynArray(payload) - | Ty::ArrayBuilder(payload) | Ty::Task(payload) => self.inspect_scalar(payload, Edge::Header, facts), + Ty::ArrayBuilder(payload) => { + self.inspect_scalar(payload, Edge::Header, facts) + } + Ty::VecArrayBuilder(scalar, _) + | Ty::MaskArrayBuilder(scalar, _) + | Ty::FixedArrayBuilder(scalar, _) + | Ty::DynVecArray(scalar, _) + | Ty::DynMaskArray(scalar, _) + | Ty::DynFixedArray(scalar, _) => { + self.inspect_scalar(scalar, Edge::Header, facts) + } + Ty::FixedStructArrayBuilder(id, _) | Ty::DynFixedStructArray(id, _) => { + self.push_ref(Node::Struct(id), 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) @@ -2013,6 +2088,10 @@ enum BodyWork<'a> { enum LocalScopeWork<'a> { EnterBlock(&'a hir::Block), + EnterNamedBlock { + local: hir::LocalId, + block: &'a hir::Block, + }, ExitBlock, EnterStmt(&'a hir::Stmt), Bind { @@ -2099,6 +2178,19 @@ impl<'a> LocalScopeValidator<'a> { work.push(LocalScopeWork::EnterStmt(statement)); } } + LocalScopeWork::EnterNamedBlock { local, block } => { + self.scopes.push(Vec::new()); + if !self.activate_binding(local, true) { + return false; + } + work.push(LocalScopeWork::ExitBlock); + if let Some(value) = block.value.as_deref() { + work.push(LocalScopeWork::EnterExpr(value)); + } + for statement in block.stmts.iter().rev() { + work.push(LocalScopeWork::EnterStmt(statement)); + } + } LocalScopeWork::ExitBlock => { if !self.restore_scope() { return false; @@ -2152,6 +2244,12 @@ impl<'a> LocalScopeValidator<'a> { | hir::ExprKind::Loop { body: block, .. } => { work.push(LocalScopeWork::EnterBlock(block)); } + hir::ExprKind::NamedArena { local, block } => { + work.push(LocalScopeWork::EnterNamedBlock { + local: *local, + block, + }); + } hir::ExprKind::If { cond, then, els } => { work.push(LocalScopeWork::EnterBlock(els)); work.push(LocalScopeWork::EnterBlock(then)); @@ -2355,6 +2453,7 @@ struct BodyValidator<'a> { statements: HashMap, arms: HashMap, binding_counts: HashMap<(usize, hir::LocalId), usize>, + region_bindings: HashSet<(usize, hir::LocalId)>, producer_exprs: HashMap, producer_blocks: HashMap, } @@ -2389,6 +2488,7 @@ impl<'a> BodyValidator<'a> { statements: HashMap::new(), arms: HashMap::new(), binding_counts: HashMap::new(), + region_bindings: HashSet::new(), producer_exprs: HashMap::new(), producer_blocks: HashMap::new(), } @@ -2532,16 +2632,27 @@ impl<'a> BodyValidator<'a> { .copied() .unwrap_or(0); if parameters.contains(&local.id) { + let mode = function + .params + .iter() + .position(|id| *id == local.id) + .and_then(|index| function.param_modes.get(index)); count == 0 + && (local.ty != Ty::ArenaHandle + || (mode == Some(&align_ast::ParamMode::ByValue) && !local.is_mut)) } else if self.allow_implicit_local_params { // Dormant body fixtures predate the am-b4 activation contract and may model an // otherwise unbound local as an implicit parameter. Production validation never // enables this compatibility path. count <= 1 + && (local.ty != Ty::ArenaHandle + || self.region_bindings.contains(&(function_index, local.id))) } else { // Every production nonparameter local is introduced exactly once by Let, // LetTuple, or a match payload. Reject even an unused orphan table record. count == 1 + && (local.ty != Ty::ArenaHandle + || self.region_bindings.contains(&(function_index, local.id))) } }) } @@ -2582,6 +2693,13 @@ impl<'a> BodyValidator<'a> { Ty::DynArray(element) => { !matches!(element, Scalar::Struct(_)) && self.body_scalar_ok(element) } + ty @ (Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => { + let element = ty.dyn_aggregate_array_element().expect("matched aggregate array"); + self.body_ty_ok(element.ty()) && self.region_plain_ty_ok(element.ty()) + } Ty::DynResponseArray => true, Ty::Soa(id) => self.soa_type_ok(id), Ty::Struct(id) => self.program.structs.get(id as usize).is_some(), @@ -2594,6 +2712,13 @@ impl<'a> BodyValidator<'a> { Ty::Task(payload) => primitive_task_scalar(payload) && self.body_scalar_ok(payload), Ty::ArenaHandle | Ty::Builder => true, Ty::ArrayBuilder(element) => self.body_scalar_ok(element), + ty @ (Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..)) => { + let element = ty.array_builder_element().expect("matched aggregate builder"); + self.body_ty_ok(element.ty()) && self.region_plain_ty_ok(element.ty()) + } Ty::JsonScanner(id) => self.program.structs.get(id as usize).is_some(), Ty::DictEncoded(id, field) => self .program @@ -2795,6 +2920,30 @@ impl<'a> BodyValidator<'a> { | (Ty::ArrayBuilder(actual), Ty::ArrayBuilder(expected)) => { work.push(Pending::Scalar(actual, expected)); } + (Ty::VecArrayBuilder(actual, an), Ty::VecArrayBuilder(expected, en)) + | (Ty::MaskArrayBuilder(actual, an), Ty::MaskArrayBuilder(expected, en)) + | (Ty::FixedArrayBuilder(actual, an), Ty::FixedArrayBuilder(expected, en)) + | (Ty::DynVecArray(actual, an), Ty::DynVecArray(expected, en)) + | (Ty::DynMaskArray(actual, an), Ty::DynMaskArray(expected, en)) + | (Ty::DynFixedArray(actual, an), Ty::DynFixedArray(expected, en)) => { + if an != en { + return false; + } + work.push(Pending::Scalar(actual, expected)); + } + ( + Ty::FixedStructArrayBuilder(actual, an), + Ty::FixedStructArrayBuilder(expected, en), + ) + | ( + Ty::DynFixedStructArray(actual, an), + Ty::DynFixedStructArray(expected, en), + ) => { + if an != en { + return false; + } + work.push(Pending::Ty(Ty::Struct(actual), Ty::Struct(expected))); + } (Ty::Result(actual_ok, actual_err), Ty::Result(expected_ok, expected_err)) => { work.push(Pending::Scalar(actual_ok, expected_ok)); work.push(Pending::Scalar(actual_err, expected_err)); @@ -3261,6 +3410,7 @@ impl<'a> BodyValidator<'a> { | hir::ExprKind::ResultErr(_) | hir::ExprKind::Try(_) | hir::ExprKind::Arena(_) + | hir::ExprKind::NamedArena { .. } | hir::ExprKind::Unsafe(_) | hir::ExprKind::RawAlloc(_) | hir::ExprKind::RawFree(_) @@ -3272,6 +3422,7 @@ impl<'a> BodyValidator<'a> { | hir::ExprKind::BoxGet(_) | hir::ExprKind::BoxClone(_) | hir::ExprKind::StrClone(_) + | hir::ExprKind::CloneIn { .. } | hir::ExprKind::StrPredicate { .. } | hir::ExprKind::StrTrim { .. } | hir::ExprKind::StrBorrow(_) @@ -3654,7 +3805,13 @@ impl<'a> BodyValidator<'a> { fn native_expression_envelope_ok(&self, expression: &hir::Expr) -> bool { match &expression.kind { hir::ExprKind::WriterStd { fd, .. } => matches!(*fd, 1 | 2), - hir::ExprKind::ArrayBuilderNew { elem } => self.array_builder_elem_ok(*elem), + hir::ExprKind::ArrayBuilderNew { elem, region } => { + if region.is_some() { + self.array_builder_region_elem_ok(*elem) + } else { + self.array_builder_elem_ok(*elem) + } + } hir::ExprKind::RandShuffle { elem, .. } | hir::ExprKind::RandSample { elem, .. } => { self.rng_elem_ok(*elem) } @@ -3794,11 +3951,54 @@ impl<'a> BodyValidator<'a> { } } - fn array_builder_elem_ok(&self, elem: Scalar) -> bool { + fn array_builder_elem_ok(&self, elem: ArrayBuilderElem) -> bool { + let ArrayBuilderElem::Scalar(elem) = elem else { + return false; + }; align_sema::scalar_to_prim(elem).is_some() && (elem == Scalar::String || self.scalar_copy_ok(elem)) } + fn array_builder_region_elem_ok(&self, elem: ArrayBuilderElem) -> bool { + self.region_plain_ty_ok(elem.ty()) + } + + fn region_plain_ty_ok(&self, ty: Ty) -> bool { + let mut work = vec![ty]; + let mut seen = HashSet::new(); + while let Some(ty) = work.pop() { + let ty = align_sema::expand_tagged_ty(ty, &self.program.tagged_types); + if !seen.insert(ty) { + continue; + } + match ty { + Ty::Int(_) | Ty::Float(_) | Ty::Bool | Ty::Char | Ty::Unit | Ty::Str + | Ty::Vec(..) | Ty::Mask(..) => {} + Ty::Slice(Scalar::Int(align_sema::IntTy { + bits: 8, + signed: false, + })) => {} + Ty::Option(payload) => work.push(align_sema::scalar_to_ty(payload)), + Ty::Struct(id) => { + let Some(definition) = self.program.structs.get(id as usize) else { return false }; + work.extend(definition.fields.iter().rev().map(|field| field.ty)); + } + Ty::Enum(id) => { + let Some(definition) = self.program.enums.get(id as usize) else { return false }; + work.extend( + definition.variants.iter().rev().flat_map(|variant| { + variant.payload.iter().rev().copied().map(align_sema::scalar_to_ty) + }), + ); + } + Ty::Array(payload, _) => work.push(align_sema::scalar_to_ty(payload)), + Ty::StructArray(id, _) => work.push(Ty::Struct(id)), + _ => return false, + } + } + true + } + fn rng_elem_ok(&self, elem: Ty) -> bool { align_sema::ty_to_scalar(elem) .is_some_and(|scalar| align_sema::scalar_to_prim(scalar).is_some() && self.scalar_copy_ok(scalar)) @@ -3924,6 +4124,7 @@ impl<'a> BodyValidator<'a> { match &expression.kind { hir::ExprKind::TaskGroup(block) | hir::ExprKind::Arena(block) + | hir::ExprKind::NamedArena { block, .. } | hir::ExprKind::Unsafe(block) | hir::ExprKind::Block(block) | hir::ExprKind::Loop { body: block, .. } => { @@ -3994,6 +4195,7 @@ impl<'a> BodyValidator<'a> { | hir::ExprKind::Try(recv) => work.push(recv), hir::ExprKind::Block(block) | hir::ExprKind::Arena(block) + | hir::ExprKind::NamedArena { block, .. } | hir::ExprKind::Unsafe(block) => { if let Some(value) = block.value.as_deref() { work.push(value); @@ -4103,6 +4305,7 @@ impl<'a> BodyValidator<'a> { match &expression.kind { hir::ExprKind::TaskGroup(block) | hir::ExprKind::Arena(block) + | hir::ExprKind::NamedArena { block, .. } | hir::ExprKind::Unsafe(block) | hir::ExprKind::Block(block) | hir::ExprKind::Loop { body: block, .. } => { @@ -4135,6 +4338,7 @@ impl<'a> BodyValidator<'a> { hir::ExprKind::ReaderBuffered { .. } => return true, hir::ExprKind::Block(block) | hir::ExprKind::Arena(block) + | hir::ExprKind::NamedArena { block, .. } | hir::ExprKind::Unsafe(block) => { let Some(value) = block.value.as_deref() else { return false; @@ -4614,6 +4818,10 @@ impl<'a> BodyValidator<'a> { | hir::ExprKind::BuilderToString(expr) => { push_expr!(expr, context.clone()); } + hir::ExprKind::CloneIn { value, region } => { + push_expr!(region, context.clone()); + push_expr!(value, context.clone()); + } hir::ExprKind::Binary { lhs, rhs, .. } | hir::ExprKind::IntArith { lhs, rhs, .. } => { push_expr!(rhs, context.clone()); @@ -4690,10 +4898,14 @@ impl<'a> BodyValidator<'a> { hir::ExprKind::TupleIndex { recv, .. } => push_expr!(recv, context.clone()), hir::ExprKind::Block(block) | hir::ExprKind::Arena(block) + | hir::ExprKind::NamedArena { block, .. } | hir::ExprKind::Unsafe(block) => { let mut child = context.clone(); child.pooled_initializer = None; - if matches!(&expression.kind, hir::ExprKind::Arena(_)) { + if matches!( + &expression.kind, + hir::ExprKind::Arena(_) | hir::ExprKind::NamedArena { .. } + ) { child.arena_depth = child.arena_depth.saturating_add(1); } if matches!(&expression.kind, hir::ExprKind::Unsafe(_)) { @@ -5040,6 +5252,25 @@ impl<'a> BodyValidator<'a> { if !self.body_ty_ok(expression.ty) || !stored_type_matches { return false; } + if let hir::ExprKind::NamedArena { local, .. } = &expression.kind { + let Some(function) = self.program.fns.get(context.function) else { + return false; + }; + let Some(binding) = function.locals.get(*local as usize) else { + return false; + }; + if binding.id != *local + || binding.ty != Ty::ArenaHandle + || binding.is_param + || binding.is_mut + || !self.record_binding(context.function, *local) + { + return false; + } + if !self.region_bindings.insert((context.function, *local)) { + return false; + } + } let Some(producer_flow) = self.producer_expression_flow(expression) else { return false; }; @@ -5789,6 +6020,10 @@ impl<'a> BodyValidator<'a> { let flow = self.block_flow(block)?; Some((flow.ty, flow.falls, flow.breaks)) } + hir::ExprKind::NamedArena { block, .. } => { + let flow = self.block_flow(block)?; + Some((flow.ty, flow.falls, flow.breaks)) + } hir::ExprKind::Unsafe(block) => { let flow = self.block_flow(block)?; Some((flow.ty, flow.falls, flow.breaks)) @@ -5973,6 +6208,20 @@ impl<'a> BodyValidator<'a> { let flow = self.expr_flow(value)?; (flow.ty == Ty::Str).then_some((Ty::String, flow.falls, flow.breaks)) } + hir::ExprKind::CloneIn { value, region } => { + let value = self.expr_flow(value)?; + let region = self.expr_flow(region)?; + let value_ty_ok = matches!( + value.ty, + Ty::Str | Ty::Slice(Scalar::Int(align_sema::IntTy { bits: 8, signed: false })) + ) || matches!(value.ty, Ty::Struct(_)) && self.region_plain_ty_ok(value.ty); + if !value_ty_ok || region.ty != Ty::ArenaHandle { + return None; + } + let ty = value.ty; + let (falls, breaks) = strict_flow(&[value, region]); + Some((ty, falls, breaks)) + } hir::ExprKind::StrPredicate { kind, haystack, needle } => { let left = self.expr_flow(haystack)?; let right = self.expr_flow(needle)?; @@ -6103,6 +6352,7 @@ impl<'a> BodyValidator<'a> { }), hir::ExprKind::Block(block) | hir::ExprKind::Arena(block) + | hir::ExprKind::NamedArena { block, .. } | hir::ExprKind::TaskGroup(block) | hir::ExprKind::Unsafe(block) => self.producer_block_flow(block), hir::ExprKind::If { cond, then, els } => { @@ -6339,47 +6589,59 @@ impl<'a> BodyValidator<'a> { } strict(Ty::Unit, &[buffer, data]) } - hir::ExprKind::ArrayBuilderNew { elem } => { - (self.array_builder_elem_ok(*elem) && expression.ty == Ty::ArrayBuilder(*elem)) - .then_some((expression.ty, true, Vec::new())) + hir::ExprKind::ArrayBuilderNew { elem, region } => { + let valid_elem = if region.is_some() { + self.array_builder_region_elem_ok(*elem) + } else { + self.array_builder_elem_ok(*elem) + }; + if !valid_elem || expression.ty != Ty::array_builder(*elem) { + return None; + } + if let Some(region) = region { + if region.ty != Ty::ArenaHandle { + return None; + } + strict(expression.ty, &[region]) + } else { + Some((expression.ty, true, Vec::new())) + } } hir::ExprKind::ArrayBuilderPush { builder, value, moves_value, } => { - let Ty::ArrayBuilder(elem) = self.expr_flow(builder)?.ty else { - return None; - }; - if !self.array_builder_elem_ok(elem) - || !mutable_local(builder, Ty::ArrayBuilder(elem)) - || value.ty != align_sema::scalar_to_ty(elem) - || *moves_value != (elem == Scalar::String) + let elem = self.expr_flow(builder)?.ty.array_builder_element()?; + if !(self.array_builder_elem_ok(elem) || self.array_builder_region_elem_ok(elem)) + || !mutable_local(builder, Ty::array_builder(elem)) + || !self.body_ty_matches(value.ty, elem.ty()) + || *moves_value + != matches!(elem, ArrayBuilderElem::Scalar(Scalar::String)) { return None; } strict(Ty::Unit, &[builder, value]) } hir::ExprKind::ArrayBuilderAppend { builder, data } => { - let Ty::ArrayBuilder(elem) = self.expr_flow(builder)?.ty else { + let elem = self.expr_flow(builder)?.ty.array_builder_element()?; + let ArrayBuilderElem::Scalar(scalar) = elem else { return None; }; - if !self.array_builder_elem_ok(elem) - || elem == Scalar::String - || !self.scalar_copy_ok(elem) - || !mutable_local(builder, Ty::ArrayBuilder(elem)) - || data.ty != Ty::Slice(elem) + if !(self.array_builder_elem_ok(elem) || self.array_builder_region_elem_ok(elem)) + || scalar == Scalar::String + || !self.scalar_copy_ok(scalar) + || !mutable_local(builder, Ty::array_builder(elem)) + || data.ty != Ty::Slice(scalar) { return None; } strict(Ty::Unit, &[builder, data]) } hir::ExprKind::ArrayBuilderBuild(builder) => { - let Ty::ArrayBuilder(elem) = self.expr_flow(builder)?.ty else { - return None; - }; - let primitive = align_sema::scalar_to_prim(elem)?; - strict(Ty::DynArray(align_sema::prim_to_scalar(primitive)), &[builder]) + let elem = self.expr_flow(builder)?.ty.array_builder_element()?; + let result = align_sema::array_builder_result_ty(elem); + strict(result, &[builder]) } hir::ExprKind::FsWriteFile { path, data, builder } => { if path.ty != Ty::Str @@ -6920,6 +7182,10 @@ impl<'a> BodyValidator<'a> { // A scanner is owned by the later JSON slice. Keeping it out here also prevents the // array reducers from taking the Result scanner ABI by accident. Ty::DynStructArray(_, Layout::Soa) + | Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..) | Ty::DynResponseArray | Ty::Unit | Ty::Str @@ -6944,6 +7210,10 @@ impl<'a> BodyValidator<'a> { | Ty::ArenaHandle | Ty::Builder | Ty::ArrayBuilder(_) + | Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..) | Ty::JsonDoc | Ty::DictEncoded(_, _) | Ty::Writer @@ -7587,6 +7857,10 @@ impl<'a> BodyValidator<'a> { | Ty::String | Ty::Slice(_) | Ty::DynArray(_) + | Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..) | Ty::DynStructArray(_, _) | Ty::DynSliceArray(_) | Ty::DynResponseArray @@ -7614,6 +7888,13 @@ impl<'a> BodyValidator<'a> { Ty::Array(scalar, _) | Ty::Slice(scalar) | Ty::DynArray(scalar) => { align_sema::scalar_to_ty(scalar) } + ty @ (Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => ty + .dyn_aggregate_array_element() + .expect("matched aggregate array") + .ty(), Ty::DynSliceArray(primitive) => { Ty::Slice(align_sema::prim_to_scalar(primitive)) } @@ -8934,6 +9215,11 @@ 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::Vec(scalar, lanes) | Ty::Mask(scalar, lanes) => { + valid_vector_lanes(lanes) + && valid_vector_scalar(scalar) + && self.scalar_copy_ok(scalar) + } Ty::Array(scalar, length) => length > 0 && self.scalar_copy_ok(scalar), Ty::StructArray(id, length) => { length > 0 @@ -9068,6 +9354,7 @@ fn context_polymorphic_expression(kind: &hir::ExprKind, falls: bool) -> bool { | hir::ExprKind::Block(_) | hir::ExprKind::Loop { .. } | hir::ExprKind::Arena(_) + | hir::ExprKind::NamedArena { .. } | hir::ExprKind::Unsafe(_) ) } @@ -9505,6 +9792,41 @@ pub(crate) fn body_ty_mangle(ty: Ty, program: &hir::Program) -> String { } }, Ty::Fn(_) => output.push_str("F_cycle"), + Ty::ArrayBuilder(element) => push_sequence( + &mut work, + vec![ + Work::Text("AB_".to_string()), + Work::Type(align_sema::scalar_to_ty(element)), + ], + ), + ty @ (Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..)) => push_sequence( + &mut work, + vec![ + Work::Text("AB_".to_string()), + Work::Type( + ty.array_builder_element() + .expect("matched aggregate builder") + .ty(), + ), + ], + ), + ty @ (Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => push_sequence( + &mut work, + vec![ + Work::Text("DA_".to_string()), + Work::Type( + ty.dyn_aggregate_array_element() + .expect("matched aggregate array") + .ty(), + ), + ], + ), other => output.push_str(&body_simple_ty_name(other)), }, } @@ -9524,15 +9846,37 @@ fn body_simple_ty_name(ty: Ty) -> String { Ty::Str => "str".to_string(), Ty::String => "string".to_string(), Ty::Unit => "()".to_string(), - Ty::ArenaHandle => "arena".to_string(), + Ty::ArenaHandle => "region".to_string(), Ty::Raw => "raw".to_string(), Ty::Builder => "builder".to_string(), Ty::Writer => "writer".to_string(), Ty::Reader => "reader".to_string(), Ty::Buffer => "buffer".to_string(), - Ty::ArrayBuilder(scalar) => format!( + Ty::ArrayBuilder(element) => format!( "array_builder_{}", - body_simple_ty_name(align_sema::scalar_to_ty(scalar)) + body_simple_ty_name(align_sema::scalar_to_ty(element)) + ), + ty @ (Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..)) => format!( + "array_builder_{}", + body_simple_ty_name( + ty.array_builder_element() + .expect("matched aggregate builder") + .ty(), + ) + ), + ty @ (Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => format!( + "array_{}", + body_simple_ty_name( + ty.dyn_aggregate_array_element() + .expect("matched aggregate array") + .ty(), + ) ), Ty::File => "file".to_string(), Ty::Rng => "rng".to_string(), diff --git a/crates/align_mir/src/validate_hir_tests.rs b/crates/align_mir/src/validate_hir_tests.rs index be065d3c..6ad89c15 100644 --- a/crates/align_mir/src/validate_hir_tests.rs +++ b/crates/align_mir/src/validate_hir_tests.rs @@ -1,7 +1,7 @@ use super::*; use crate::validate_hir::{body_core_metadata_is_valid, body_ty_mangle}; use align_sema::{ - FloatTy, FnEffect, IntTy, Layout, PrimScalar, Scalar, Ty, + AggregateArrayElem, ArrayBuilderElem, FloatTy, FnEffect, IntTy, Layout, PrimScalar, Scalar, Ty, hir::{ self, EnumDef, EnumVariant, FieldDef, FnTy, ImportedFn, ReturnBorrowSummary, ReturnRegionSummary, StructDef, TaggedType, TupleDef, @@ -19,6 +19,39 @@ fn direct_program_name(call: &DirectCall) -> Option<&str> { } } +#[test] +fn aggregate_region_builder_source_survives_the_complete_hir_gate() { + for (name, source) in [ + ( + "vector-new", + "fn main() -> i32 { arena out { mut values: array_builder> := array_builder(out)\n return 0 } }\n", + ), + ( + "vector-push", + "fn main() -> i32 { arena out { value: vec4 := [1, 2, 3, 4]\n mut values: array_builder> := array_builder(out)\n values.push(value)\n return 0 } }\n", + ), + ( + "vector-build", + "fn main() -> i32 { arena out { value: vec4 := [1, 2, 3, 4]\n mut values: array_builder> := array_builder(out)\n values.push(value)\n built := values.build()\n return built.len() as i32 } }\n", + ), + ( + "vector-index", + "fn main() -> i32 { arena out { value: vec4 := [1, 2, 3, 4]\n mut values: array_builder> := array_builder(out)\n values.push(value)\n built := values.build()\n return built[0][1] } }\n", + ), + ( + "mask-build", + "fn main() -> i32 { arena out { a: vec4 := [1, 2, 3, 4]\n b: vec4 := [0, 3, 2, 5]\n mut values: array_builder> := array_builder(out)\n values.push(a > b)\n built := values.build()\n selected := select(built[0], a, b)\n return selected[1] } }\n", + ), + ] { + let program = checked_source_program(source); + assert!( + validate_hir::body_only_metadata_is_valid(&program), + "{name}: bodies" + ); + assert_eq!(lower_program(&program).fns.len(), 1, "{name}: lowering"); + } +} + fn declaration_header_program() -> hir::Program { let mut program = baseline_program(); let slice_i32 = Ty::Slice(scalar_int(32)); @@ -398,6 +431,23 @@ fn malformed_hir_declaration_header_metadata_fails_closed() { assert_one_header_mutation("stored-parameter-mode", &base, |program| { program.fns[0].param_modes[0] = align_ast::ParamMode::BorrowMut; }); + + let mut mutable_region = declaration_header_program(); + let function = &mut mutable_region.fns[0]; + function.locals.truncate(1); + function.locals[0].ty = Ty::ArenaHandle; + function.locals[0].is_mut = true; + function.param_modes[0] = align_ast::ParamMode::BorrowMut; + function.ret = Ty::Unit; + function.return_borrow = ReturnBorrowSummary::None; + function.return_region = ReturnRegionSummary::None; + function.body.stmts.clear(); + function.body.value = Some(Box::new(hir::Expr { + kind: hir::ExprKind::Unit, + ty: Ty::Unit, + span: function.span, + })); + assert_header_rejected("mutable-region-parameter", &mutable_region); assert_one_header_mutation("stored-parameter-name", &base, |program| { program.fns[0].locals[0].name = "bad-name".to_string(); }); @@ -3466,8 +3516,8 @@ fn with_array_builder_body_depth(depth: usize) -> hir::Program { "the root Block, array-builder Expr, and value need depth three" ); let span = align_span::Span::new(0, 0, 0); - let elem = scalar_int(64); - let builder_ty = Ty::ArrayBuilder(elem); + let elem = ArrayBuilderElem::Scalar(scalar_int(64)); + let builder_ty = Ty::array_builder(elem); let mut drop_individual_exprs = std::collections::HashMap::new(); drop_individual_exprs.insert(span, true); let expr = hir::Expr { @@ -4698,6 +4748,105 @@ fn malformed_hir_type_placement_fails_closed() { assert_placement_rejected("view extern return", &extern_view_return); } +#[test] +fn region_only_array_builder_headers_are_placement_valid() { + for (label, element) in [ + ("str", ArrayBuilderElem::Scalar(Scalar::Str)), + ( + "bytes", + ArrayBuilderElem::Scalar(Scalar::Slice(PrimScalar::Int(IntTy { + bits: 8, + signed: false, + }))), + ), + ("struct", ArrayBuilderElem::Scalar(Scalar::Struct(0))), + ("sum", ArrayBuilderElem::Scalar(Scalar::Enum(0))), + ("option", ArrayBuilderElem::Scalar(Scalar::Tagged(0))), + ( + "vector", + ArrayBuilderElem::Aggregate(AggregateArrayElem::Vec(scalar_int(32), 4)), + ), + ( + "mask", + ArrayBuilderElem::Aggregate(AggregateArrayElem::Mask(scalar_int(32), 8)), + ), + ( + "fixed_array", + ArrayBuilderElem::Aggregate(AggregateArrayElem::FixedArray(Scalar::Str, 3)), + ), + ( + "fixed_struct_array", + ArrayBuilderElem::Aggregate(AggregateArrayElem::FixedStructArray(0, 2)), + ), + ] { + let mut program = baseline_program(); + program.imported_fns.push(ImportedFn { + name: format!("dep$push_{label}"), + params: vec![Ty::array_builder(element)], + param_modes: vec![align_ast::ParamMode::BorrowMut], + ret: Ty::Unit, + return_provenance_known: true, + return_borrow: ReturnBorrowSummary::None, + return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, + effect: FnEffect::Pure, + }); + assert!( + validate_hir::type_placement_metadata_is_valid(&program), + "region-only array_builder<{label}> header was rejected" + ); + } + + for (label, element) in [ + ( + "non_numeric_vector_lane", + AggregateArrayElem::Vec(Scalar::Bool, 4), + ), + ( + "unsupported_mask_width", + AggregateArrayElem::Mask(scalar_int(32), 3), + ), + ( + "empty_fixed_array", + AggregateArrayElem::FixedArray(Scalar::Bool, 0), + ), + ( + "owned_fixed_array", + AggregateArrayElem::FixedArray(Scalar::String, 2), + ), + ] { + let mut program = baseline_program(); + program.imported_fns.push(ImportedFn { + name: format!("dep$invalid_{label}"), + params: vec![Ty::array_builder(ArrayBuilderElem::Aggregate(element))], + param_modes: vec![align_ast::ParamMode::BorrowMut], + ret: Ty::Unit, + return_provenance_known: true, + return_borrow: ReturnBorrowSummary::None, + return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, + effect: FnEffect::Pure, + }); + assert_placement_rejected(label, &program); + } + + let mut unknown_struct = baseline_program(); + unknown_struct.imported_fns.push(ImportedFn { + name: "dep$invalid_unknown_fixed_struct_array".to_string(), + params: vec![Ty::array_builder(ArrayBuilderElem::Aggregate( + AggregateArrayElem::FixedStructArray(99, 2), + ))], + param_modes: vec![align_ast::ParamMode::BorrowMut], + ret: Ty::Unit, + return_provenance_known: true, + return_borrow: ReturnBorrowSummary::None, + return_region: ReturnRegionSummary::None, + return_cleanup: hir::ReturnCleanupAbi::None, + effect: FnEffect::Pure, + }); + assert_rejected("unknown_fixed_struct_array", &unknown_struct); +} + #[test] fn body_only_header_types_fail_placement_closed() { for (label, ty) in [ @@ -9380,18 +9529,67 @@ fn hir_body_validator_native() { "native_array_builder_new", body_test_expr( hir::ExprKind::ArrayBuilderNew { - elem: Scalar::String, + elem: ArrayBuilderElem::Scalar(Scalar::String), + region: None, }, Ty::ArrayBuilder(Scalar::String), ), Vec::new(), Ty::ArrayBuilder(Scalar::String) ); + program.fns.push(body_test_named_function( + "native_named_region_materialization", + hir::Block { + stmts: Vec::new(), + value: Some(Box::new(body_test_expr( + hir::ExprKind::NamedArena { + local: 0, + block: hir::Block { + stmts: vec![ + hir::Stmt::Expr(body_test_expr( + hir::ExprKind::CloneIn { + value: Box::new(native_str()), + region: Box::new(native_local(0, Ty::ArenaHandle)), + }, + Ty::Str, + )), + hir::Stmt::Expr(body_test_expr( + hir::ExprKind::ArrayBuilderNew { + elem: ArrayBuilderElem::Scalar(scalar_int(64)), + region: Some(Box::new(native_local( + 0, + Ty::ArenaHandle, + ))), + }, + Ty::ArrayBuilder(scalar_int(64)), + )), + ], + value: Some(Box::new(body_test_expr( + hir::ExprKind::Unit, + Ty::Unit, + ))), + }, + }, + Ty::Unit, + ))), + }, + vec![body_test_local( + 0, + "out", + Ty::ArenaHandle, + false, + false, + )], + Ty::Unit, + )); add!( "native_array_builder_push", body_test_expr( hir::ExprKind::ArrayBuilderPush { - builder: Box::new(native_local(0, Ty::ArrayBuilder(Scalar::String))), + builder: Box::new(native_local( + 0, + Ty::ArrayBuilder(Scalar::String), + )), value: Box::new(body_test_expr( hir::ExprKind::StrClone(Box::new(native_str())), Ty::String, @@ -9413,13 +9611,22 @@ fn hir_body_validator_native() { "native_array_builder_append", body_test_expr( hir::ExprKind::ArrayBuilderAppend { - builder: Box::new(native_local(0, Ty::ArrayBuilder(scalar_int(64)))), + builder: Box::new(native_local( + 0, + Ty::ArrayBuilder(scalar_int(64)), + )), data: Box::new(native_local(1, Ty::Slice(scalar_int(64)))), }, Ty::Unit, ), vec![ - body_test_local(0, "builder", Ty::ArrayBuilder(scalar_int(64)), true, false), + body_test_local( + 0, + "builder", + Ty::ArrayBuilder(scalar_int(64)), + true, + false, + ), body_test_local(1, "data", Ty::Slice(scalar_int(64)), false, false), ], Ty::Unit @@ -9442,6 +9649,40 @@ fn hir_body_validator_native() { )], Ty::DynArray(scalar_int(64)) ); + let vector_element = AggregateArrayElem::Vec(scalar_int(32), 4); + let vector_builder = Ty::array_builder(ArrayBuilderElem::Aggregate(vector_element)); + add!( + "native_aggregate_array_builder_push", + body_test_expr( + hir::ExprKind::ArrayBuilderPush { + builder: Box::new(native_local(0, vector_builder)), + value: Box::new(body_test_expr( + hir::ExprKind::VecLit { + elems: (0..4) + .map(|value| { + body_test_expr(hir::ExprKind::Int(value), i32_ty) + }) + .collect(), + elem: scalar_int(32), + }, + vector_element.ty(), + )), + moves_value: false, + }, + Ty::Unit, + ), + vec![body_test_local(0, "builder", vector_builder, true, false)], + Ty::Unit + ); + add!( + "native_aggregate_array_builder_build", + body_test_expr( + hir::ExprKind::ArrayBuilderBuild(Box::new(native_local(0, vector_builder))), + Ty::dyn_aggregate_array(vector_element), + ), + vec![body_test_local(0, "builder", vector_builder, false, false)], + Ty::dyn_aggregate_array(vector_element) + ); add!( "native_fs_write_file", body_test_expr( @@ -10745,6 +10986,90 @@ fn hir_body_validator_native() { ); assert!(body_core_metadata_is_valid(&program), "native body metadata"); + let mut reject = program.clone(); + let function = reject + .fns + .iter_mut() + .find(|function| function.name == "native_named_region_materialization") + .expect("named region fixture is present"); + function.locals.push(body_test_local( + 1, + "alias", + Ty::ArenaHandle, + false, + false, + )); + let expression = function + .body + .value + .as_deref_mut() + .expect("named region fixture has a value"); + let hir::ExprKind::NamedArena { block, .. } = &mut expression.kind else { + panic!("named region fixture lost its arena") + }; + block.stmts.push(hir::Stmt::Let { + local: 1, + init: native_local(0, Ty::ArenaHandle), + }); + assert!( + !body_core_metadata_is_valid(&reject), + "an ordinary local must not store a region capability" + ); + + let mut reject = program.clone(); + let expression = body_value_expression_mut( + &mut reject, + "native_named_region_materialization", + ); + let hir::ExprKind::NamedArena { local, .. } = &mut expression.kind else { + panic!("named region fixture lost its arena") + }; + *local = 99; + assert!(!body_core_metadata_is_valid(&reject)); + + let mut reject = program.clone(); + let expression = body_value_expression_mut( + &mut reject, + "native_named_region_materialization", + ); + let hir::ExprKind::NamedArena { block, .. } = &mut expression.kind else { + panic!("named region fixture lost its arena") + }; + let hir::Stmt::Expr(clone) = &mut block.stmts[0] else { + panic!("named region fixture lost clone_in") + }; + let hir::ExprKind::CloneIn { region, .. } = &mut clone.kind else { + panic!("named region fixture lost clone_in") + }; + region.ty = Ty::Bool; + assert!(!body_core_metadata_is_valid(&reject)); + + let mut reject = program.clone(); + let expression = body_value_expression_mut( + &mut reject, + "native_named_region_materialization", + ); + let hir::ExprKind::NamedArena { block, .. } = &mut expression.kind else { + panic!("named region fixture lost its arena") + }; + let hir::Stmt::Expr(builder) = &mut block.stmts[1] else { + panic!("named region fixture lost its builder") + }; + let hir::ExprKind::ArrayBuilderNew { elem, .. } = &mut builder.kind else { + panic!("named region fixture lost its builder") + }; + *elem = ArrayBuilderElem::Scalar(Scalar::String); + builder.ty = Ty::ArrayBuilder(Scalar::String); + assert!(!body_core_metadata_is_valid(&reject)); + + let mut reject = program.clone(); + let expression = body_statement_expression_mut( + &mut reject, + "native_aggregate_array_builder_build", + ); + expression.ty = Ty::dyn_aggregate_array(AggregateArrayElem::Mask(scalar_int(32), 4)); + assert!(!body_core_metadata_is_valid(&reject)); + let mut reject = program.clone(); reject.fns.push(body_test_named_function( "native_rand_shuffle_readonly_slice", diff --git a/crates/align_parser/src/lib.rs b/crates/align_parser/src/lib.rs index b65049c5..0e928b77 100644 --- a/crates/align_parser/src/lib.rs +++ b/crates/align_parser/src/lib.rs @@ -154,6 +154,7 @@ fn discard_expr_tree(root: DiscardExprTask) { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::Unsafe(block) | ExprKind::TaskGroup(block) | ExprKind::Loop(block) => pending.push(DiscardExprTask::Block(Box::new(block))), @@ -294,6 +295,7 @@ fn cap_expr_depth(e: &mut Expr, depth: u32, diags: &mut Diagnostics) { } ExprKind::Block(b) | ExprKind::Arena(b) + | ExprKind::NamedArena { block: b, .. } | ExprKind::Unsafe(b) | ExprKind::TaskGroup(b) | ExprKind::Loop(b) => cap_block_depth(b, d, diags), @@ -1591,9 +1593,18 @@ impl<'a> Parser<'a> { TokKind::Arena => { let start = self.span(); self.bump(); + let name = if matches!(self.peek(), TokKind::Ident(_)) { + self.parse_ident("arena binding") + } else { + None + }; let block = self.parse_block()?; let span = start.merge(self.prev_span()); - Some(Expr { kind: ExprKind::Arena(block), span }) + let kind = match name { + Some(name) => ExprKind::NamedArena { name, block }, + None => ExprKind::Arena(block), + }; + Some(Expr { kind, span }) } TokKind::Unsafe => { let start = self.span(); @@ -1979,6 +1990,27 @@ mod tests { assert_eq!(fd.name.name, "main"); } + #[test] + fn arena_binding_is_distinct_from_anonymous_arena() { + let (file, errors) = parse( + "fn main() -> i64 {\n a := arena { 1 }\n b := arena out { values: array_builder := array_builder(out)\n values.build().len() }\n return a + b\n}\n", + ); + assert!(!errors); + let Item::Fn(function) = &file.items[0] else { panic!("expected function") }; + let FnBody::Block(body) = &function.body else { panic!("expected block body") }; + let Stmt::Let { init: anonymous, .. } = &body.stmts[0] else { + panic!("expected anonymous arena binding") + }; + assert!(matches!(anonymous.kind, ExprKind::Arena(_))); + let Stmt::Let { init: named, .. } = &body.stmts[1] else { + panic!("expected named arena binding") + }; + assert!(matches!( + &named.kind, + ExprKind::NamedArena { name, .. } if name.name == "out" + )); + } + #[test] fn slice_range_forms_parse() { // `a[i]` stays an Index; the four range shapes parse into SliceRange with the right diff --git a/crates/align_runtime/src/lib.rs b/crates/align_runtime/src/lib.rs index 85ba6999..e3f257c7 100644 --- a/crates/align_runtime/src/lib.rs +++ b/crates/align_runtime/src/lib.rs @@ -12687,12 +12687,11 @@ pub unsafe extern "C" fn align_rt_realloc(ptr: *mut u8, new_size: i64) -> *mut u // ── array_builder (M12 Slice A6) ────────────────────────────────────────────────────────────── // // The typed grow-then-freeze member (`builder`->`string`, `buffer`->bytes, now `array_builder`-> -// `array`). `push`/`append` grow amortized (doubling); `build` hands the raw storage off as an -// owned `array` (a zero-copy ptr+len retype). Storage is [`align_rt_alloc`]/[`align_rt_realloc`] -// memory — the same C allocator that frees `array` — so `build` never copies and the capacity -// slack is freed whole by the size-less C-free. `elem_size` is the element stride in bytes (16 for a -// `string` element: an `AlignStr` `{ptr,len}` moved in per element). The builder holds no views, so a -// realloc can never invalidate a borrow (the soundness rationale for the whole type). +// `array`). The existing heap form grows with [`align_rt_realloc`] and transfers its storage +// zero-copy at `build`. The explicit-region form grows geometric chunks in the selected arena and +// compacts exactly once into a final contiguous arena allocation. `elem_size`/`elem_align` describe +// the target element layout. Region builders may contain views, whose provenance is enforced by +// sema; they never realloc a previously written chunk. /// Test-only live-count of pushed-but-not-yet-freed `string` entries stored in an `array_builder` /// (via [`align_rt_array_builder_push_str`]): incremented there, decremented wherever that entry's @@ -12706,17 +12705,39 @@ pub unsafe extern "C" fn align_rt_realloc(ptr: *mut u8, new_size: i64) -> *mut u #[cfg(test)] static LIVE_ARRAY_BUILDER_STRINGS: core::sync::atomic::AtomicI64 = core::sync::atomic::AtomicI64::new(0); -/// A growable typed array builder (`array_builder`). `data` is `align_rt_alloc`/`align_rt_realloc` -/// storage (null while `cap == 0`); `len`/`cap` count elements; `elem_size` is the byte stride. +/// Test-only count of completed region-builder compaction passes. One `build()` increments once, +/// independent of whether the builder used zero, one, or many growth chunks. +#[cfg(test)] +static REGION_ARRAY_BUILDER_COMPACTIONS: core::sync::atomic::AtomicI64 = + core::sync::atomic::AtomicI64::new(0); + +#[repr(C)] +struct RegionArrayBuilderChunk { + next: *mut RegionArrayBuilderChunk, + data: *mut u8, + len: usize, + cap: usize, +} + +/// A growable typed array builder (`array_builder`). A null `arena` is the existing heap mode, +/// where `data` is realloc-compatible storage. A non-null `arena` selects linked geometric chunks +/// allocated in that arena; `head`/`tail` own no independent heap allocation. #[repr(C)] pub struct ArrayBuilder { data: *mut u8, len: usize, cap: usize, elem_size: usize, + arena: *mut Arena, + head: *mut RegionArrayBuilderChunk, + tail: *mut RegionArrayBuilderChunk, + elem_align: usize, } -const _: () = assert!(core::mem::size_of::() <= 64 && core::mem::align_of::() <= 16); +const _: () = assert!( + core::mem::size_of::() <= 64 + && core::mem::align_of::() <= 16 +); impl ArrayBuilder { /// Ensure room for `additional` more elements, growing by amortized doubling. Aborts on a @@ -12727,7 +12748,23 @@ impl ArrayBuilder { Some(n) => n, None => panic_abort("array_builder capacity overflow"), }; + if self.elem_size == 0 { + self.cap = usize::MAX; + return; + } if needed <= self.cap { + if self.arena.is_null() { + return; + } + let tail_has_room = !self.tail.is_null() + && unsafe { (*self.tail).len.checked_add(additional) } + .is_some_and(|length| length <= unsafe { (*self.tail).cap }); + if tail_has_room { + return; + } + } + if !self.arena.is_null() { + unsafe { self.reserve_region(additional) }; return; } // Amortized doubling with a small floor, so tiny builders don't realloc on every push. @@ -12745,11 +12782,76 @@ impl ArrayBuilder { self.data = unsafe { align_rt_realloc(self.data, bytes as i64) }; self.cap = new_cap; } + + unsafe fn reserve_region(&mut self, additional: usize) { + let previous = if self.tail.is_null() { 0 } else { unsafe { (*self.tail).cap } }; + let mut chunk_cap = previous.max(4); + while chunk_cap < additional { + chunk_cap = chunk_cap.checked_mul(2).unwrap_or(additional); + } + if previous != 0 { + chunk_cap = chunk_cap.checked_mul(2).unwrap_or_else(|| { + panic_abort("array_builder capacity overflow") + }); + } + let bytes = chunk_cap + .checked_mul(self.elem_size) + .filter(|bytes| (*bytes as u64) <= isize::MAX as u64) + .unwrap_or_else(|| panic_abort("array_builder allocation too large")); + let arena = unsafe { &mut *self.arena }; + let chunk_ptr = arena + .alloc_uninit( + core::mem::size_of::(), + core::mem::align_of::(), + ) + .cast::(); + let data = arena.alloc_uninit(bytes, self.elem_align); + unsafe { + chunk_ptr.write(RegionArrayBuilderChunk { + next: core::ptr::null_mut(), + data, + len: 0, + cap: chunk_cap, + }); + if self.tail.is_null() { + self.head = chunk_ptr; + } else { + (*self.tail).next = chunk_ptr; + } + } + self.tail = chunk_ptr; + self.cap = self.cap.checked_add(chunk_cap).unwrap_or_else(|| { + panic_abort("array_builder capacity overflow") + }); + } + + unsafe fn push_destination(&mut self) -> *mut u8 { + unsafe { self.reserve(1) }; + if self.elem_size == 0 { + return self.elem_align as *mut u8; + } + if self.arena.is_null() { + return unsafe { self.data.add(self.len * self.elem_size) }; + } + let tail = unsafe { &mut *self.tail }; + let destination = unsafe { tail.data.add(tail.len * self.elem_size) }; + tail.len += 1; + destination + } } fn array_builder_value(elem_size: i64) -> ArrayBuilder { let es = safe_len(elem_size).unwrap_or(0).max(1); - ArrayBuilder { data: core::ptr::null_mut(), len: 0, cap: 0, elem_size: es } + ArrayBuilder { + data: core::ptr::null_mut(), + len: 0, + cap: 0, + elem_size: es, + arena: core::ptr::null_mut(), + head: core::ptr::null_mut(), + tail: core::ptr::null_mut(), + elem_align: 1, + } } /// `array_builder()` — open an empty builder whose element stride is `elem_size` bytes (`>= 1`; @@ -12759,6 +12861,49 @@ pub extern "C" fn align_rt_array_builder_new(elem_size: i64) -> *mut ArrayBuilde Box::into_raw(Box::new(array_builder_value(elem_size))) } +/// `array_builder(out)` — allocate the builder header in `out`; growth chunks and the final +/// contiguous result are allocated from the same arena. No independently-owned heap vector exists. +/// +/// # Safety +/// `arena` must be null or a live arena handle. `elem_size` must be non-negative and `elem_align` a +/// nonzero power of two. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn align_rt_array_builder_new_in( + arena: *mut Arena, + elem_size: i64, + elem_align: i64, +) -> *mut ArrayBuilder { + let (Ok(elem_size), Some(elem_align)) = ( + safe_len(elem_size), + safe_len(elem_align).ok().filter(|align| align.is_power_of_two()), + ) else { + return core::ptr::null_mut(); + }; + if arena.is_null() { + return core::ptr::null_mut(); + } + let storage = unsafe { + (&mut *arena).alloc_uninit( + core::mem::size_of::(), + core::mem::align_of::(), + ) + } + .cast::(); + unsafe { + storage.write(ArrayBuilder { + data: core::ptr::null_mut(), + len: 0, + cap: 0, + elem_size, + arena, + head: core::ptr::null_mut(), + tail: core::ptr::null_mut(), + elem_align, + }); + } + storage +} + /// Initialize a compiler-provided nonescaping array-builder header. Its realloc-compatible payload /// remains unchanged and can still transfer zero-copy into the built array. /// @@ -12792,11 +12937,10 @@ pub unsafe extern "C" fn align_rt_array_builder_push(b: *mut ArrayBuilder, bits: } let b = unsafe { &mut *b }; debug_assert!(b.elem_size <= 8, "elem_size must be <= 8 for scalar push"); - unsafe { b.reserve(1) }; + let dst = unsafe { b.push_destination() }; let le = bits.to_le_bytes(); let w = b.elem_size.min(8); unsafe { - let dst = b.data.add(b.len * b.elem_size); core::ptr::copy_nonoverlapping(le.as_ptr(), dst, w); } b.len += 1; @@ -12816,23 +12960,43 @@ pub unsafe extern "C" fn align_rt_array_builder_push_str(b: *mut ArrayBuilder, p } let b = unsafe { &mut *b }; debug_assert_eq!(b.elem_size, core::mem::size_of::(), "elem_size must match AlignStr size"); - unsafe { b.reserve(1) }; + let dst = unsafe { b.push_destination() }; let entry = AlignStr { ptr, len }; unsafe { - let dst = b.data.add(b.len * b.elem_size) as *mut AlignStr; - core::ptr::write_unaligned(dst, entry); + core::ptr::write_unaligned(dst.cast::(), entry); } b.len += 1; #[cfg(test)] LIVE_ARRAY_BUILDER_STRINGS.fetch_add(1, core::sync::atomic::Ordering::Relaxed); } -/// `b.append(xs)` — bulk-copy `count` Copy-scalar elements (`count * elem_size` bytes) from `src` -/// onto the builder. A null `src` or non-positive `count` appends nothing. Grows amortized. +/// Append one arbitrary Copy element by copying exactly `elem_size` initialized bytes from `src`. +/// Used for RegionPlain views, Options, sums, and structs whose physical value does not fit in the +/// scalar-bits entry point. +/// +/// # Safety +/// `b` must be a live builder and `src` must address one initialized element of its exact layout. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn align_rt_array_builder_push_bytes( + b: *mut ArrayBuilder, + src: *const u8, +) { + if b.is_null() || src.is_null() { + return; + } + let b = unsafe { &mut *b }; + let dst = unsafe { b.push_destination() }; + unsafe { core::ptr::copy_nonoverlapping(src, dst, b.elem_size) }; + b.len += 1; +} + +/// `b.append(xs)` — bulk-copy `count` Copy elements (`count * elem_size` bytes) from `src` onto the +/// builder. Heap mode remains scalar-only; region mode also accepts validated RegionPlain layouts. +/// A null `src` or non-positive `count` appends nothing. Grows amortized. /// Null-safe. /// /// # Safety -/// `b` must be null or a valid scalar-element [`ArrayBuilder`]; `src`/`count` must describe a +/// `b` must be null or a valid [`ArrayBuilder`]; `src`/`count` must describe a /// readable run of `count` elements of `elem_size` bytes each (or be null / `<= 0`). #[unsafe(no_mangle)] pub unsafe extern "C" fn align_rt_array_builder_append(b: *mut ArrayBuilder, src: *const u8, count: i64) { @@ -12846,35 +13010,79 @@ pub unsafe extern "C" fn align_rt_array_builder_append(b: *mut ArrayBuilder, src if n == 0 || src.is_null() { return; } - debug_assert!(b.elem_size <= 8, "elem_size must be <= 8 for scalar append"); - unsafe { b.reserve(n) }; - let bytes = match n.checked_mul(b.elem_size) { - Some(x) => x, + let bytes = match n + .checked_mul(b.elem_size) + .filter(|bytes| (*bytes as u64) <= isize::MAX as u64) + { + Some(bytes) => bytes, None => return, }; - unsafe { - let dst = b.data.add(b.len * b.elem_size); - core::ptr::copy_nonoverlapping(src, dst, bytes); + debug_assert!( + !b.arena.is_null() || b.elem_size <= 8, + "heap append elements must fit the scalar ABI" + ); + if b.arena.is_null() { + unsafe { b.reserve(n) }; + unsafe { + let dst = b.data.add(b.len * b.elem_size); + core::ptr::copy_nonoverlapping(src, dst, bytes); + } + b.len += n; + } else { + for index in 0..n { + let source = unsafe { src.add(index * b.elem_size) }; + let destination = unsafe { b.push_destination() }; + unsafe { core::ptr::copy_nonoverlapping(source, destination, b.elem_size) }; + b.len += 1; + } } - b.len += n; } -/// `b.build()` — freeze into an owned `array` `{ptr,len}` (a zero-copy ptr+len retype). Hands the -/// raw storage off as the array buffer (the caller's `array` `Drop` frees it — deep-free for a -/// `string` element array via `align_rt_free_string_array`), then frees only the builder header. The -/// capacity slack rides along and is freed whole by the size-less C-free. Null-safe (a moved-out -/// builder yields `{null,0}`). +/// `b.build()` — freeze into `array` `{ptr,len}`. Heap mode hands the raw storage to the array; +/// region mode allocates one exact contiguous buffer in the same arena and copies initialized chunk +/// contents through one pass. Null-safe (a moved-out builder yields `{null,0}`). /// /// # Safety -/// `b` must be null or a valid [`ArrayBuilder`] from [`align_rt_array_builder_new`], not yet frozen. +/// `b` must be null or a valid [`ArrayBuilder`] from [`align_rt_array_builder_new`] or +/// [`align_rt_array_builder_new_in`], not yet frozen. #[unsafe(no_mangle)] pub unsafe extern "C" fn align_rt_array_builder_build(b: *mut ArrayBuilder) -> AlignStr { if b.is_null() { return AlignStr { ptr: core::ptr::null(), len: 0 }; } - // Take the header back; its raw `data` pointer becomes the array buffer (NOT freed here). - let b = *unsafe { Box::from_raw(b) }; - array_builder_build_value(b) + if unsafe { (*b).arena.is_null() } { + // Take the header back; its raw `data` pointer becomes the array buffer (NOT freed here). + let b = *unsafe { Box::from_raw(b) }; + return array_builder_build_value(b); + } + let builder = unsafe { &mut *b }; + #[cfg(test)] + REGION_ARRAY_BUILDER_COMPACTIONS.fetch_add(1, core::sync::atomic::Ordering::Relaxed); + if builder.len == 0 { + return AlignStr { ptr: core::ptr::null(), len: 0 }; + } + let bytes = builder + .len + .checked_mul(builder.elem_size) + .filter(|bytes| (*bytes as u64) <= isize::MAX as u64) + .unwrap_or_else(|| panic_abort("array_builder allocation too large")); + let destination = unsafe { (&mut *builder.arena).alloc_uninit(bytes, builder.elem_align) }; + let mut written = 0usize; + let mut chunk = builder.head; + while !chunk.is_null() { + let current = unsafe { &*chunk }; + let chunk_bytes = current + .len + .checked_mul(builder.elem_size) + .unwrap_or_else(|| panic_abort("array_builder allocation too large")); + unsafe { + core::ptr::copy_nonoverlapping(current.data, destination.add(written), chunk_bytes); + } + written += chunk_bytes; + chunk = current.next; + } + debug_assert_eq!(written, bytes); + AlignStr { ptr: destination, len: builder.len as i64 } } fn array_builder_build_value(b: ArrayBuilder) -> AlignStr { @@ -12908,6 +13116,9 @@ pub unsafe extern "C" fn align_rt_array_builder_free(b: *mut ArrayBuilder) { if b.is_null() { return; } + if !unsafe { (*b).arena.is_null() } { + return; + } let b = *unsafe { Box::from_raw(b) }; unsafe { array_builder_free_value(b) }; } @@ -12949,6 +13160,9 @@ pub unsafe extern "C" fn align_rt_array_builder_free_strings(b: *mut ArrayBuilde if b.is_null() { return; } + if !unsafe { (*b).arena.is_null() } { + return; + } let b = *unsafe { Box::from_raw(b) }; unsafe { array_builder_free_strings_value(b) }; } @@ -18734,8 +18948,8 @@ mod tests { None }) .collect(); - assert_eq!(runtime.len(), 286); - assert_eq!(registry.len(), 286); + assert_eq!(runtime.len(), 288); + assert_eq!(registry.len(), 288); assert_eq!(runtime, registry); } @@ -22054,6 +22268,119 @@ mod tests { } } + // Region-builder compaction is observed through a process-global test counter. Serialize every + // test that freezes a region builder so parallel test execution cannot pollute snapshot deltas. + static REGION_ARRAY_BUILDER_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + #[test] + fn region_array_builder_compacts_empty_single_and_multi_chunk_values_once() { + use core::sync::atomic::Ordering; + + let _serial = REGION_ARRAY_BUILDER_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + for count in [0usize, 3, 17] { + let before = REGION_ARRAY_BUILDER_COMPACTIONS.load(Ordering::Relaxed); + let arena = align_rt_arena_begin(); + let builder = unsafe { align_rt_array_builder_new_in(arena, 8, 8) }; + assert!(!builder.is_null()); + for value in 0..count { + unsafe { align_rt_array_builder_push(builder, value as u64) }; + } + let frozen = unsafe { align_rt_array_builder_build(builder) }; + assert_eq!(frozen.len, count as i64); + if count == 0 { + assert!(frozen.ptr.is_null()); + } else { + let values = unsafe { core::slice::from_raw_parts(frozen.ptr.cast::(), count) }; + assert_eq!(values, (0..count as u64).collect::>()); + } + assert_eq!( + REGION_ARRAY_BUILDER_COMPACTIONS.load(Ordering::Relaxed), + before + 1, + "one build must execute one compaction pass", + ); + // An unfinished-region free owns nothing independently and must not release the header. + unsafe { align_rt_array_builder_free(builder) }; + unsafe { align_rt_arena_end(arena) }; + } + } + + #[test] + fn region_array_builder_push_bytes_preserves_aggregate_layout() { + let _serial = REGION_ARRAY_BUILDER_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + #[repr(C)] + #[derive(Clone, Copy, Debug, PartialEq)] + struct Pair { + left: u64, + right: u64, + } + + let arena = align_rt_arena_begin(); + let builder = unsafe { + align_rt_array_builder_new_in( + arena, + core::mem::size_of::() as i64, + core::mem::align_of::() as i64, + ) + }; + let expected = [Pair { left: 1, right: 2 }, Pair { left: 3, right: 5 }]; + for value in &expected { + unsafe { + align_rt_array_builder_push_bytes( + builder, + (value as *const Pair).cast::(), + ) + }; + } + let frozen = unsafe { align_rt_array_builder_build(builder) }; + let actual = unsafe { + core::slice::from_raw_parts(frozen.ptr.cast::(), frozen.len as usize) + }; + assert_eq!(actual, expected); + unsafe { align_rt_arena_end(arena) }; + } + + #[test] + fn region_array_builder_preserves_zero_sized_element_count() { + let _serial = REGION_ARRAY_BUILDER_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let arena = align_rt_arena_begin(); + let builder = unsafe { align_rt_array_builder_new_in(arena, 0, 8) }; + assert!(!builder.is_null()); + let element = 0u8; + for _ in 0..3 { + unsafe { align_rt_array_builder_push_bytes(builder, &element) }; + } + let frozen = unsafe { align_rt_array_builder_build(builder) }; + assert_eq!(frozen.len, 3); + assert!(!frozen.ptr.is_null()); + unsafe { align_rt_arena_end(arena) }; + } + + #[test] + fn region_array_builder_rejects_invalid_ffi_layout_before_allocation() { + let _serial = REGION_ARRAY_BUILDER_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + assert!(unsafe { + align_rt_array_builder_new_in(core::ptr::null_mut(), 8, 8) + } + .is_null()); + + let arena = align_rt_arena_begin(); + for (size, align) in [(-1, 8), (8, 0), (8, -1), (8, 3)] { + assert!( + unsafe { align_rt_array_builder_new_in(arena, size, align) }.is_null(), + "invalid layout ({size}, {align}) must fail before allocation", + ); + } + let valid = unsafe { align_rt_array_builder_new_in(arena, 8, 8) }; + assert!(!valid.is_null(), "an earlier invalid request must not poison the arena"); + let byte = 0u8; + unsafe { + align_rt_array_builder_append(valid, &byte, i64::MAX); + } + let empty = unsafe { align_rt_array_builder_build(valid) }; + assert_eq!(empty.len, 0, "overflowing append size must be rejected"); + unsafe { align_rt_arena_end(arena) }; + } + #[test] fn integer_parses_edges() { // The hand-rolled single-pass `integer()` must match the old `parse::()`: full range diff --git a/crates/align_sema/src/hir.rs b/crates/align_sema/src/hir.rs index 5dadcf91..e20ecedd 100644 --- a/crates/align_sema/src/hir.rs +++ b/crates/align_sema/src/hir.rs @@ -583,6 +583,9 @@ pub enum ExprKind { Loop { body: Block, diverges: bool, body_locals: std::ops::Range }, /// `arena { ... }` — a region; allocations inside are bulk-freed at block end. Arena(Block), + /// `arena name { ... }` — an arena whose runtime handle is stored in `local` and may be + /// passed as the scope-limited builtin `region` capability. + NamedArena { local: LocalId, block: Block }, /// `unsafe { ... }` — a marker block permitting `raw.*` ops. No runtime effect; lowers to its /// inner block. (Enforcement + impurity are handled in sema.) Unsafe(Block), @@ -639,6 +642,10 @@ pub enum ExprKind { /// slice 7). The result owns its buffer (`Drop`-freed), so it can escape its source's /// region — the explicit escape hatch out of a zero-copy view. StrClone(Box), + /// `value.clone_in(out)` — copy a `str`/`bytes` view or every view-bearing leaf of a + /// RegionPlain struct into the exact explicit `region` capability. The result is tied to `out`, + /// not to `value`'s source storage. + CloneIn { value: Box, region: Box }, /// `s.contains(n)` / `s.starts_with(p)` / `s.ends_with(s)` — a byte-oriented `str` predicate /// (`core.string`), `ty` = `bool`. Both operands are `str` views (an owned `string` operand is /// auto-borrowed via [`ExprKind::StrBorrow`]); the comparison reads bytes only, so neither is @@ -1013,21 +1020,29 @@ pub enum ExprKind { /// the bytes in and growing it. The `ty` is [`crate::Ty::Unit`]. The receiver must be a `mut /// buffer` local; `data` is borrowed (copied, not consumed). Pure (in-memory growth). BufferAppend { buffer: Box, data: Box }, - /// `array_builder()` — open an empty growable typed array builder (M12 A6). The `ty` is - /// [`crate::Ty::ArrayBuilder`] (an owned Move handle, `Drop`-freed); the element type is carried - /// by that `ty`. Pure (allocation only), like `BuilderNew`/`BufferNew`. - ArrayBuilderNew { elem: crate::Scalar }, + /// `array_builder()` / `array_builder(out)` — open an empty growable typed array builder. The + /// `ty` is [`crate::Ty::ArrayBuilder`] and carries the element type. The anonymous form owns + /// heap storage and is `Drop`-freed; the explicit-region form owns no independent allocation. + /// Pure (allocation only), like `BuilderNew`/`BufferNew`. + ArrayBuilderNew { + elem: crate::ArrayBuilderElem, + /// Explicit destination region for `array_builder(out)`. `None` preserves the existing + /// individually-owned heap form. + region: Option>, + }, /// `b.push(v)` — append one element to a growable `array_builder`, growing it (amortized). The /// `ty` is [`crate::Ty::Unit`]. The receiver must be a `mut array_builder` local (mutated in /// place). `moves_value` is set for a `string` element: `v` is **moved** into the builder (its - /// source is nulled), so MoveCheck consumes it; a Copy-scalar element borrows `v`. Pure (growth). + /// source is nulled), so MoveCheck consumes it; a Copy region-plain element is copied while its + /// borrowed storage provenance is retained. Pure (growth). ArrayBuilderPush { builder: Box, value: Box, moves_value: bool }, - /// `b.append(xs)` — bulk-append a `slice` of Copy-scalar elements to a growable - /// `array_builder`, copying them in and growing it. The `ty` is [`crate::Ty::Unit`]. The receiver - /// must be a `mut array_builder` local; `data` is borrowed (copied, not consumed). Pure (growth). + /// `b.append(xs)` — bulk-append a `slice` of Copy elements to a growable `array_builder`, + /// copying them in and growing it. Heap mode remains scalar-only; region mode accepts validated + /// RegionPlain layouts. The receiver is mutable; `data` is borrowed. Pure (growth). ArrayBuilderAppend { builder: Box, data: Box }, - /// `b.build()` — freeze an `array_builder` into an owned `array`, **consuming** (moving) the - /// builder (a zero-copy ptr+len retype). The `ty` is [`crate::Ty::DynArray`] of the element. + /// `b.build()` — freeze an `array_builder` into `array`, **consuming** (moving) the builder. + /// Heap storage transfers zero-copy; region chunks compact once into the same region. The `ty` + /// is [`crate::Ty::DynArray`] (or the AoS struct-array form) of the element. ArrayBuilderBuild(Box), /// `fs.write_file(path, data)` — create/truncate `path` (a `str`) and write all of `data`, then /// close. `data` is a `str`/`bytes` (`slice`) view, or — when `builder` is set — a `builder`'s diff --git a/crates/align_sema/src/hir_depth.rs b/crates/align_sema/src/hir_depth.rs index 1c8c67e7..fb644bf8 100644 --- a/crates/align_sema/src/hir_depth.rs +++ b/crates/align_sema/src/hir_depth.rs @@ -355,7 +355,6 @@ fn walk_body_records<'a>( | ExprKind::ArrayDictEncode { .. } | ExprKind::ReaderStdin | ExprKind::WriterStd { .. } - | ExprKind::ArrayBuilderNew { .. } | ExprKind::TimeNow | ExprKind::TimeInstant | ExprKind::ProcessCpuCount @@ -388,6 +387,10 @@ fn walk_body_records<'a>( | ExprKind::ArrayBuilderBuild(expr) => { work.push((BodyRecord::Expr(expr), child_depth)); } + ExprKind::CloneIn { value, region } => { + work.push((BodyRecord::Expr(value), child_depth)); + work.push((BodyRecord::Expr(region), child_depth)); + } ExprKind::Binary { lhs, rhs, .. } | ExprKind::IntArith { lhs, rhs, .. } | ExprKind::ResultMapErr { @@ -657,6 +660,7 @@ fn walk_body_records<'a>( ExprKind::TaskGroup(block) | ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::Unsafe(block) => { work.push((BodyRecord::Block(block), child_depth)); } @@ -792,6 +796,11 @@ fn walk_body_records<'a>( work.push((BodyRecord::Expr(offset), child_depth)); work.push((BodyRecord::Expr(value), child_depth)); } + ExprKind::ArrayBuilderNew { region, .. } => { + if let Some(region) = region { + work.push((BodyRecord::Expr(region), child_depth)); + } + } ExprKind::BuilderNew { capacity } => { if let Some(value) = capacity.as_deref() { work.push((BodyRecord::Expr(value), child_depth)); @@ -2212,12 +2221,14 @@ mod tests { let mut diagnostics = crate::Diagnostics::new(); let named_return_region = std::collections::HashMap::new(); + let named_param_modes = std::collections::HashMap::new(); { let function = &program.fns[0]; let mut escape = crate::EscapeCheck { f: function, diags: &mut diagnostics, named_return_region: &named_return_region, + named_param_modes: &named_param_modes, fn_types: &program.fn_types, tuples: &program.tuples, structs: &program.structs, @@ -2231,6 +2242,7 @@ mod tests { task_group_regions: Vec::new(), allocation_regions: Vec::new(), allocation_region_by_expr: std::collections::HashMap::new(), + region_capabilities: std::collections::HashMap::new(), flow: crate::EscapeFlowCfg::new(), flow_current: 0, loop_exit_blocks: Vec::new(), diff --git a/crates/align_sema/src/lib.rs b/crates/align_sema/src/lib.rs index 0c99cd55..2414f6e7 100644 --- a/crates/align_sema/src/lib.rs +++ b/crates/align_sema/src/lib.rs @@ -300,6 +300,50 @@ pub enum Layout { Soa, } +/// A concrete non-scalar element of a dynamic array built in an explicit region. Keeping this +/// descriptor non-recursive preserves [`Ty`]'s compact `Copy` representation while carrying the +/// exact LLVM layout that a region builder must copy and expose through indexing. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum AggregateArrayElem { + Vec(Scalar, u32), + Mask(Scalar, u32), + FixedArray(Scalar, u32), + FixedStructArray(u32, u32), +} + +impl AggregateArrayElem { + pub fn ty(self) -> Ty { + match self { + Self::Vec(elem, lanes) => Ty::Vec(elem, lanes), + Self::Mask(elem, lanes) => Ty::Mask(elem, lanes), + Self::FixedArray(elem, length) => Ty::Array(elem, length), + Self::FixedStructArray(id, length) => Ty::StructArray(id, length), + } + } +} + +/// The exact element type retained by `array_builder`. Scalar elements keep the established +/// heap-builder and dynamic-array representation; aggregate elements are admitted only by the +/// explicit-region form and freeze to the corresponding dynamic aggregate-array `Ty` variant. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ArrayBuilderElem { + Scalar(Scalar), + Aggregate(AggregateArrayElem), +} + +impl ArrayBuilderElem { + pub fn ty(self) -> Ty { + match self { + Self::Scalar(elem) => scalar_to_ty(elem), + Self::Aggregate(elem) => elem.ty(), + } + } + + pub fn name(self) -> String { + ty_name(self.ty()) + } +} + /// sema-internal type representation (`03-types.md` §1). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Ty { @@ -370,6 +414,12 @@ pub enum Ty { /// (`{ T* ptr, i64 len }`) but Move and region-tracked. MMv2 slice 3: produced by a /// materializing terminal (`.to_array()`) and (this slice) arena-bump-allocated. DynArray(Scalar), + /// Dynamic region-owned arrays with exact fixed-layout aggregate elements. The four variants + /// keep `Ty` compact by storing the aggregate discriminator in `Ty` itself. + DynVecArray(Scalar, u32), + DynMaskArray(Scalar, u32), + DynFixedArray(Scalar, u32), + DynFixedStructArray(u32, u32), /// `array` — an *owned*, dynamic-length array of opaque `http response` **Move** /// handles, laid out like a slice (`{ response* ptr, i64 len }`), Move but **not** region-tracked /// (freshly owned, like `array` — it borrows nothing). Produced **only** by `cl.get_many` @@ -386,7 +436,8 @@ pub enum Ty { /// heap buffer freed by `Drop` (the same machinery as owned `array`). A `string` is /// readable as a `str` (a borrow of itself). String, - /// An arena handle (internal; produced by `arena {}`, never written by the user). + /// A scope-limited region capability. Produced only by `arena name {}` and accepted as an + /// ordinary function parameter; it cannot be returned, stored in an aggregate, or constructed. ArenaHandle, /// `raw` — an opaque, untyped raw byte pointer, the unsafe escape hatch (`raw.alloc` yields one; /// `raw.free` consumes one). Copy, `Static` region (not arena/region-tracked), never auto-dropped @@ -419,17 +470,22 @@ pub enum Ty { /// `slice` borrow), `.len()` is its byte count; `Drop`-freed. Constructing / reading it is /// pure (no I/O). Buffer, - /// `array_builder` (`core`, M12 A6) — a growable typed array builder, the typed member of the - /// grow-then-freeze family (`builder`->`string`, `buffer`->bytes, this->`array`). An opaque - /// owned **Move** handle to a heap builder object; the payload [`Scalar`] is the element type - /// (a Copy scalar or `string` in v1). `array_builder()` opens it, `b.push(v)` / `b.append(xs)` - /// grow it (amortized doubling, in-place through the handle), `b.build()` **consumes** it into an - /// owned `array` (a zero-copy ptr+len retype — the storage is `align_rt_alloc`-family memory - /// grown via `align_rt_realloc`, freed whole by the array's `Drop`). An unfrozen builder is - /// `Drop`-freed at scope exit (deep-free for a `string` element). Holds **no views**, so a - /// realloc can never invalidate a borrow. Never rides an aggregate (no `Scalar::ArrayBuilder`): - /// bound to one local, like `builder`/`buffer`. + /// `array_builder` (`core`, M12 A6) for scalar elements — a growable typed array builder, the + /// typed member of the grow-then-freeze family (`builder`->`string`, `buffer`->bytes, + /// this->`array`). An opaque + /// owned **Move** handle; [`ArrayBuilderElem`] is the exact element type. `array_builder()` selects + /// individually owned heap storage for the existing primitive/`string` surface. + /// `array_builder(out)` selects region-owned chunks for concrete `RegionPlain` elements and + /// retains view provenance through pushed values. `build()` consumes either form: heap storage + /// transfers zero-copy, while region chunks compact once in `out`. Never rides an aggregate + /// (no `Scalar::ArrayBuilder`): bound to one local, like `builder`/`buffer`. ArrayBuilder(Scalar), + /// The region-only aggregate-element form of `array_builder`. Kept as a separate variant so + /// the non-recursive descriptor does not enlarge `Ty` and every recursive compiler frame. + VecArrayBuilder(Scalar, u32), + MaskArrayBuilder(Scalar, u32), + FixedArrayBuilder(Scalar, u32), + FixedStructArrayBuilder(u32, u32), /// `str_finder` — a **compiler-internal** prepared substring-search plan (doc-13 §6.6 / §11 P3). /// It has **no surface syntax**: it is emitted only by MIR when it recognises the loop-invariant /// repeated-needle where-pipeline (`xs.where(fn(s) = s.contains(NEEDLE)).…`), where MIR builds the @@ -774,6 +830,94 @@ pub fn scalar_to_ty(s: Scalar) -> Ty { } } +/// Convert one resolved concrete type to the exact element descriptor accepted by an +/// `array_builder`. Admission by allocation mode and recursive `RegionPlain` validation remains at +/// the constructor; this conversion only preserves the physical type without truncating it through +/// [`Scalar`]. +pub fn array_builder_elem(ty: Ty) -> Option { + match ty { + Ty::Vec(elem, lanes) => Some(ArrayBuilderElem::Aggregate(AggregateArrayElem::Vec( + elem, lanes, + ))), + Ty::Mask(elem, lanes) => Some(ArrayBuilderElem::Aggregate(AggregateArrayElem::Mask( + elem, lanes, + ))), + Ty::Array(elem, length) => Some(ArrayBuilderElem::Aggregate( + AggregateArrayElem::FixedArray(elem, length), + )), + Ty::StructArray(id, length) => Some(ArrayBuilderElem::Aggregate( + AggregateArrayElem::FixedStructArray(id, length), + )), + other => ty_to_scalar(other).map(ArrayBuilderElem::Scalar), + } +} + +pub fn array_builder_result_ty(elem: ArrayBuilderElem) -> Ty { + match elem { + ArrayBuilderElem::Scalar(Scalar::Struct(id)) => Ty::DynStructArray(id, Layout::Aos), + ArrayBuilderElem::Scalar(elem) => Ty::DynArray(elem), + ArrayBuilderElem::Aggregate(elem) => Ty::dyn_aggregate_array(elem), + } +} + +/// Whether a concrete type can be copied into an explicit region without creating an independent +/// owner. This is the shared source/HIR boundary predicate; diagnostics use a path-aware mirror in +/// [`Checker::region_plain_error`]. +pub fn region_plain_type_ok( + ty: Ty, + structs: &[StructDef], + enums: &[hir::EnumDef], + tagged_types: &[hir::TaggedType], +) -> bool { + let mut work = vec![ty]; + let mut seen = HashSet::new(); + while let Some(ty) = work.pop() { + let ty = expand_tagged_ty(ty, tagged_types); + if !seen.insert(ty) { + continue; + } + match ty { + Ty::Int(_) + | Ty::Float(_) + | Ty::Bool + | Ty::Char + | Ty::Unit + | Ty::Str + | Ty::Vec(..) + | Ty::Mask(..) => {} + Ty::Slice(Scalar::Int(IntTy { + bits: 8, + signed: false, + })) => {} + Ty::Option(payload) => work.push(scalar_to_ty(payload)), + Ty::Struct(id) => { + let Some(definition) = structs.get(id as usize) else { + return false; + }; + work.extend(definition.fields.iter().rev().map(|field| field.ty)); + } + Ty::Enum(id) => { + let Some(definition) = enums.get(id as usize) else { + return false; + }; + work.extend( + definition + .variants + .iter() + .rev() + .flat_map(|variant| variant.payload.iter().rev()) + .copied() + .map(scalar_to_ty), + ); + } + Ty::Array(payload, _) => work.push(scalar_to_ty(payload)), + Ty::StructArray(id, _) => work.push(Ty::Struct(id)), + _ => return false, + } + } + true +} + /// Expand the reversible type view of a nested tagged payload. Other types pass through unchanged. /// Missing ids stay `Ty::Error`, so malformed tables fail closed before a physical classifier can /// silently choose a fallback representation. @@ -1284,6 +1428,10 @@ pub fn drop_plan( Ty::Box(_) | Ty::Task(_) | Ty::DynArray(_) + | Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..) | Ty::DynStructArray(..) | Ty::DynSliceArray(_) | Ty::DynResponseArray @@ -1294,6 +1442,10 @@ pub fn drop_plan( | Ty::Reader | Ty::Buffer | Ty::ArrayBuilder(_) + | Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..) | Ty::Regex | Ty::Captures | Ty::CliCommand @@ -1441,6 +1593,21 @@ fn ty_mentions_slice( } } Ty::Option(payload) => work.push(scalar_to_ty(payload)), + Ty::DynVecArray(element, lanes) | Ty::VecArrayBuilder(element, lanes) => { + work.push(Ty::Vec(element, lanes)) + } + Ty::DynMaskArray(element, lanes) | Ty::MaskArrayBuilder(element, lanes) => { + work.push(Ty::Mask(element, lanes)) + } + Ty::DynFixedArray(element, length) + | Ty::FixedArrayBuilder(element, length) => work.push(Ty::Array(element, length)), + Ty::DynFixedStructArray(id, length) + | Ty::FixedStructArrayBuilder(id, length) => { + work.push(Ty::StructArray(id, length)) + } + Ty::ArrayBuilder(element) => { + work.push(scalar_to_ty(element)) + } Ty::Result(ok, err) => { work.push(scalar_to_ty(err)); work.push(scalar_to_ty(ok)); @@ -1487,6 +1654,21 @@ fn ty_mentions_resource( | Ty::Option(scalar) | Ty::Task(scalar) | Ty::Box(scalar) => work.push(scalar_to_ty(scalar)), + Ty::DynVecArray(element, lanes) | Ty::VecArrayBuilder(element, lanes) => { + work.push(Ty::Vec(element, lanes)) + } + Ty::DynMaskArray(element, lanes) | Ty::MaskArrayBuilder(element, lanes) => { + work.push(Ty::Mask(element, lanes)) + } + Ty::DynFixedArray(element, length) + | Ty::FixedArrayBuilder(element, length) => work.push(Ty::Array(element, length)), + Ty::DynFixedStructArray(id, length) + | Ty::FixedStructArrayBuilder(id, length) => { + work.push(Ty::StructArray(id, length)) + } + Ty::ArrayBuilder(element) => { + work.push(scalar_to_ty(element)) + } Ty::Result(ok, err) => { work.push(scalar_to_ty(err)); work.push(scalar_to_ty(ok)); @@ -1554,6 +1736,7 @@ pub fn ty_may_borrow( match ty { Ty::Str | Ty::Slice(_) + | Ty::ArenaHandle | Ty::Reader | Ty::Writer | Ty::Soa(_) @@ -1583,6 +1766,20 @@ pub fn ty_may_borrow( Ty::Array(s, _) | Ty::DynArray(s) | Ty::Option(s) | Ty::Task(s) => { work.push(scalar_to_ty(s)); } + Ty::DynVecArray(elem, lanes) | Ty::VecArrayBuilder(elem, lanes) => { + work.push(Ty::Vec(elem, lanes)); + } + Ty::DynMaskArray(elem, lanes) | Ty::MaskArrayBuilder(elem, lanes) => { + work.push(Ty::Mask(elem, lanes)); + } + Ty::DynFixedArray(elem, length) | Ty::FixedArrayBuilder(elem, length) => { + work.push(Ty::Array(elem, length)); + } + Ty::DynFixedStructArray(id, length) + | Ty::FixedStructArrayBuilder(id, length) => { + work.push(Ty::StructArray(id, length)); + } + Ty::ArrayBuilder(elem) => work.push(scalar_to_ty(elem)), Ty::Result(ok, err) => { work.push(scalar_to_ty(ok)); work.push(scalar_to_ty(err)); @@ -2170,6 +2367,78 @@ pub fn owns_hidden_string(e: &hir::Expr, in_arena: bool) -> bool { } impl Ty { + pub fn array_builder(element: ArrayBuilderElem) -> Self { + match element { + ArrayBuilderElem::Scalar(element) => Self::ArrayBuilder(element), + ArrayBuilderElem::Aggregate(AggregateArrayElem::Vec(element, lanes)) => { + Self::VecArrayBuilder(element, lanes) + } + ArrayBuilderElem::Aggregate(AggregateArrayElem::Mask(element, lanes)) => { + Self::MaskArrayBuilder(element, lanes) + } + ArrayBuilderElem::Aggregate(AggregateArrayElem::FixedArray(element, length)) => { + Self::FixedArrayBuilder(element, length) + } + ArrayBuilderElem::Aggregate(AggregateArrayElem::FixedStructArray(id, length)) => { + Self::FixedStructArrayBuilder(id, length) + } + } + } + + pub fn array_builder_element(self) -> Option { + match self { + Self::ArrayBuilder(element) => Some(ArrayBuilderElem::Scalar(element)), + Self::VecArrayBuilder(element, lanes) => Some(ArrayBuilderElem::Aggregate( + AggregateArrayElem::Vec(element, lanes), + )), + Self::MaskArrayBuilder(element, lanes) => Some(ArrayBuilderElem::Aggregate( + AggregateArrayElem::Mask(element, lanes), + )), + Self::FixedArrayBuilder(element, length) => Some(ArrayBuilderElem::Aggregate( + AggregateArrayElem::FixedArray(element, length), + )), + Self::FixedStructArrayBuilder(id, length) => Some(ArrayBuilderElem::Aggregate( + AggregateArrayElem::FixedStructArray(id, length), + )), + _ => None, + } + } + + pub fn is_array_builder(self) -> bool { + self.array_builder_element().is_some() + } + + pub fn dyn_aggregate_array(element: AggregateArrayElem) -> Self { + match element { + AggregateArrayElem::Vec(element, lanes) => Self::DynVecArray(element, lanes), + AggregateArrayElem::Mask(element, lanes) => Self::DynMaskArray(element, lanes), + AggregateArrayElem::FixedArray(element, length) => { + Self::DynFixedArray(element, length) + } + AggregateArrayElem::FixedStructArray(id, length) => { + Self::DynFixedStructArray(id, length) + } + } + } + + pub fn dyn_aggregate_array_element(self) -> Option { + match self { + Self::DynVecArray(element, lanes) => Some(AggregateArrayElem::Vec(element, lanes)), + Self::DynMaskArray(element, lanes) => Some(AggregateArrayElem::Mask(element, lanes)), + Self::DynFixedArray(element, length) => { + Some(AggregateArrayElem::FixedArray(element, length)) + } + Self::DynFixedStructArray(id, length) => { + Some(AggregateArrayElem::FixedStructArray(id, length)) + } + _ => None, + } + } + + pub fn is_dyn_aggregate_array(self) -> bool { + self.dyn_aggregate_array_element().is_some() + } + fn is_int_like(self) -> bool { matches!(self, Ty::Int(_) | Ty::IntVar(_)) } @@ -2797,6 +3066,7 @@ impl<'a, 'd> GenericBodyWalker<'a, 'd> { ast::ExprKind::Try(inner) => self.walk_expr(inner), ast::ExprKind::Loop(b) => self.walk_block(b), ast::ExprKind::Arena(b) => self.walk_block(b), + ast::ExprKind::NamedArena { block, .. } => self.walk_block(block), ast::ExprKind::Unsafe(b) => self.walk_block(b), ast::ExprKind::TaskGroup(b) => self.walk_block(b), ast::ExprKind::ArrayLit(elems) => { @@ -4558,6 +4828,12 @@ pub fn check_program_with_all_interface_facts( t.span(), ); } + if r == Ty::ArenaHandle { + diags.error( + "a region capability cannot be returned (it is scoped to its declaring arena)".to_string(), + t.span(), + ); + } // A returned function value would carry a frame-local closure environment out of // the frame (use-after-free); deferred until closures can own a region-backed env. if matches!(r, Ty::Fn(_)) { @@ -5336,6 +5612,7 @@ fn run_body_analysis_passes( f, diags, named_return_region: &named_return_region, + named_param_modes: &named_param_modes, fn_types, tuples, structs, @@ -5349,6 +5626,7 @@ fn run_body_analysis_passes( task_group_regions: Vec::new(), allocation_regions: Vec::new(), allocation_region_by_expr: std::collections::HashMap::new(), + region_capabilities: std::collections::HashMap::new(), flow: EscapeFlowCfg::new(), flow_current: 0, loop_exit_blocks: Vec::new(), @@ -5390,7 +5668,11 @@ fn run_body_analysis_passes( // A chunks header is always malloc-owned; its Region tracks the borrowed source. matches!(ty, Ty::DynSliceArray(_)) || drop_individual.get(id).copied().unwrap_or_else(|| { - !matches!(region.get(id).copied().unwrap_or(Region::Static), Region::Arena(_)) + !region + .get(id) + .copied() + .unwrap_or(Region::Static) + .is_region_owned() }) }) .collect(); @@ -8040,6 +8322,7 @@ impl EffectScan<'_> { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::Unsafe(block) | ExprKind::TaskGroup(block) => { if let Some(value) = block.value.as_deref().filter(|value| { @@ -8371,6 +8654,7 @@ impl EffectScan<'_> { ( ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::Unsafe(block) | ExprKind::TaskGroup(block), _, @@ -8866,11 +9150,22 @@ impl EffectScan<'_> { walk!(buffer); walk!(data); } - // `array_builder` new/push/append/build are all pure in-memory growth. - ExprKind::ArrayBuilderNew { .. } - | ExprKind::ArrayBuilderPush { .. } - | ExprKind::ArrayBuilderAppend { .. } - | ExprKind::ArrayBuilderBuild(_) => {} + // `array_builder` new/push/append/build are pure in-memory growth, but their operands + // may contain calls whose effects still contribute in source order. + ExprKind::ArrayBuilderNew { region, .. } => { + if let Some(region) = region { + walk!(region); + } + } + ExprKind::ArrayBuilderPush { builder, value, .. } => { + walk!(builder); + walk!(value); + } + ExprKind::ArrayBuilderAppend { builder, data } => { + walk!(builder); + walk!(data); + } + ExprKind::ArrayBuilderBuild(builder) => walk!(builder), ExprKind::FsReadFile { path } | ExprKind::ReaderOpen { path } | ExprKind::WriterCreate { path } | ExprKind::FsExists { path } | ExprKind::FsRemove { path } | ExprKind::FsReadDir { path } | ExprKind::FsReadFileView { path } | ExprKind::FsReadBytesView { path } @@ -9474,7 +9769,10 @@ impl EffectScan<'_> { walk!(builder); walk!(arg); } - ExprKind::Block(b) | ExprKind::Arena(b) | ExprKind::TaskGroup(b) => { + ExprKind::Block(b) + | ExprKind::Arena(b) + | ExprKind::NamedArena { block: b, .. } + | ExprKind::TaskGroup(b) => { return !hir_block_diverges(b); } ExprKind::Loop { diverges, .. } => return !*diverges, @@ -9604,6 +9902,10 @@ impl EffectScan<'_> { | ExprKind::HeapNew(i) | ExprKind::BoxGet(i) | ExprKind::BoxClone(i) | ExprKind::StrClone(i) | ExprKind::StrBorrow(i) | ExprKind::BuilderToString(i) | ExprKind::Len(i) | ExprKind::ArrayToSlice(i) => walk!(i), + ExprKind::CloneIn { value, region } => { + walk!(value); + walk!(region); + } ExprKind::StrPredicate { haystack, needle, .. } => { walk!(haystack); walk!(needle); @@ -9653,14 +9955,22 @@ impl EffectScan<'_> { } /// A value's inferred lifetime region (Memory Model v2, `impl/08-memory-model-v2.md`). -/// Total order, longest-lived first: `Static ⊐ Frame ⊐ Arena(1) ⊐ … ⊐ Arena(d)`. Regions are -/// inferred, never written, and live only in this analysis — they are not part of `Ty`. +/// Lifetime order, longest-lived first: `Static ⊐ Caller(_) ⊐ Frame ⊐ Arena(1) ⊐ … ⊐ +/// Arena(d)`. `Caller(i)` retains the identity of a region or borrowed-builder parameter while a +/// function body is checked. Relationships between distinct caller parameters are conditional and +/// are discharged at the concrete call site. Regions are inferred, never written, and live only in +/// this analysis — they are not part of `Ty`. #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Region { /// Process / program lifetime: literals, leaked allocations, owned-from-scalar values. Static, + /// Storage owned by the caller and identified by function-parameter position. It outlives this + /// callee's frame and nested arenas, is returnable through the inferred parameter-root summary, + /// and is never individually freed by the callee. + Caller(u32), /// The current function's frame: a view created in-frame over frame-local storage. Cannot - /// be returned. (A view *parameter* borrows the caller and is `Static` here — returnable.) + /// be returned. View parameters still use the existing root-summary machinery; region and + /// borrowed-builder parameters use `Caller` so their caller-owned storage stays explicit. /// Frame-local slices additionally use `EscapeState::local_backed_slice` for their dedicated /// diagnostic; other frame borrows (for example a `str` view of an owned `string`) use this /// region directly. @@ -9677,8 +9987,9 @@ impl Region { fn ord(self) -> u32 { match self { Region::Static => 0, - Region::Frame => 1, - Region::Arena(k) => 1 + k, + Region::Caller(_) => 1, + Region::Frame => 2, + Region::Arena(k) => 2 + k, } } @@ -9688,6 +9999,18 @@ impl Region { self.ord() <= dst.ord() } + /// Whether storage belongs to an explicit lexical region rather than to an individually freed + /// allocation. A symbolic caller region is arena-owned from this callee's perspective. + fn is_region_owned(self) -> bool { + matches!(self, Region::Caller(_) | Region::Arena(_)) + } + + /// A caller-derived value may cross this function boundary because the inferred return summary + /// maps it back to its concrete argument. Frame and local-arena values cannot. + fn is_returnable(self) -> bool { + matches!(self, Region::Static | Region::Caller(_)) + } + /// The region of a value allocated at arena nesting `depth` (0 = outside any arena, where /// the result is leaked / process-lifetime → `Static`). fn arena(depth: u32) -> Region { @@ -9814,6 +10137,19 @@ enum EscapeFlowOp<'a> { group: Region, depth: u32, }, + /// A region builder stores a copy of `value` until its destination region ends. Any view + /// contained in the pushed value must therefore outlive that destination. + ArrayBuilderStore { + builder: &'a Expr, + value: &'a Expr, + depth: u32, + }, + /// Region-backed builders may cross a call boundary only through `borrow mut`; a shared or out + /// mode cannot implement their one-owner shaping contract. + RegionBuilderNonMutBorrow { + builder: &'a Expr, + depth: u32, + }, /// A by-value call transfers a Move value to the callee. Arena-owned storage cannot cross /// that function boundary because the callee has no caller-region provenance. CallTransfer(&'a Expr, u32), @@ -9930,6 +10266,9 @@ struct EscapeCheck<'a> { diags: &'a mut Diagnostics, /// Settled same-program/imported return-region summaries for direct calls. named_return_region: &'a std::collections::HashMap, + /// Parameter modes for same-program/imported direct calls. Mutable builder parameters may + /// retain view-bearing arguments, so call sites must validate the concrete caller regions. + named_param_modes: &'a std::collections::HashMap>, /// Settled function-value signatures. Parameter roots select call arguments; capture roots are /// resolved through `EscapeState::callable_capture_region`. fn_types: &'a [hir::FnTy], @@ -9968,6 +10307,9 @@ struct EscapeCheck<'a> { /// Actual arena allocation region at each expression, keyed by HIR-node identity before the /// CFG is solved. Absence means the expression allocates free-standing heap/frame storage. allocation_region_by_expr: std::collections::HashMap, + /// Exact semantic region represented by each explicit named-arena capability local. Local ids + /// are function-unique, so entries remain usable by later HIR queries after lexical exit. + region_capabilities: std::collections::HashMap, /// Compact checked-HIR CFG built before solving escape state. flow: EscapeFlowCfg<'a>, /// Block currently receiving lowered escape operations. @@ -10007,15 +10349,46 @@ impl<'a> EscapeCheck<'a> { } fn check(&mut self) { - for ¶m in &self.f.params { - if self.f.locals.get(param as usize).is_some_and(|local| { - is_owned_droppable(local.ty, self.structs, self.enums, self.tagged_types) - || ty_tuple_is_move(local.ty, self.tuples) - }) { - // A by-value Move parameter has already crossed the call boundary and is owned by - // this frame. The caller-side transfer check guarantees it is free-standing. - self.state.individual.insert(param, true); - self.state.individual_may.insert(param, true); + for (position, ¶m) in self.f.params.iter().enumerate() { + let Some(local) = self.f.locals.get(param as usize) else { + continue; + }; + let borrowed_builder = local.ty.is_array_builder() + && matches!( + self.f.param_modes.get(position), + Some(ast::ParamMode::BorrowMut) + ); + if local.ty == Ty::ArenaHandle || borrowed_builder { + self.state + .region + .insert(param, Region::Caller(position as u32)); + } + if is_owned_droppable(local.ty, self.structs, self.enums, self.tagged_types) + || ty_tuple_is_move(local.ty, self.tuples) + { + // A by-value Move parameter has crossed the call boundary and is free-standing. + // A borrowed builder keeps the constructor modes admitted by its element type: + // `string` is heap-only, primitive scalars may use either constructor, and the + // remaining source element forms are region-only. This keeps a nested helper from + // treating a possibly region-backed incoming builder as definitely individual. + let (individual, may_individual) = match (borrowed_builder, local.ty) { + (true, Ty::ArrayBuilder(Scalar::String)) => { + (true, true) + } + ( + true, + Ty::ArrayBuilder( + Scalar::Int(_) + | Scalar::Float(_) + | Scalar::Bool + | Scalar::Char, + ), + ) => (false, true), + (true, ty) if ty.is_array_builder() => (false, false), + _ => (true, true), + }; + self.state.individual.insert(param, individual); + self.state.individual_may.insert(param, may_individual); } } self.walk_block(&self.f.body, 0); @@ -10130,6 +10503,28 @@ impl<'a> EscapeCheck<'a> { group, depth, } => self.check_spawn_capture(closure, group, depth), + EscapeFlowOp::ArrayBuilderStore { builder, value, depth } => { + let destination = self.region_of(builder, depth); + if destination != Region::Static + && self.region_bearing(value.ty) + && !self.region_of(value, depth).outlives(destination) + { + self.diags.error( + "cannot retain a shorter-lived view in this region builder; copy it with `.clone_in(out)` first" + .to_string(), + value.span, + ); + } + } + EscapeFlowOp::RegionBuilderNonMutBorrow { builder, depth } => { + if !self.drop_is_individual(builder, depth) { + self.diags.error( + "a region-backed array_builder may be passed only as `borrow mut`; the caller must retain ownership and perform build()" + .to_string(), + builder.span, + ); + } + } EscapeFlowOp::CallTransfer(value, depth) => { if self.call_transfer_contains_arena_owned(value, depth) { self.diags.error( @@ -10185,10 +10580,7 @@ impl<'a> EscapeCheck<'a> { .get(local) .copied() .unwrap_or_else(|| { - !matches!( - self.region_of(expression, depth), - Region::Arena(_) - ) + !self.region_of(expression, depth).is_region_owned() }); if !individual { return true; @@ -10214,7 +10606,9 @@ impl<'a> EscapeCheck<'a> { work.push((value, depth)); } } - ExprKind::Arena(block) | ExprKind::TaskGroup(block) => { + ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } + | ExprKind::TaskGroup(block) => { if let Some(value) = block.value.as_deref() { work.push((value, depth + 1)); } @@ -10273,7 +10667,9 @@ impl<'a> EscapeCheck<'a> { work.push((value, depth)); } } - ExprKind::Arena(block) | ExprKind::TaskGroup(block) => { + ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } + | ExprKind::TaskGroup(block) => { if let Some(value) = block.value.as_deref() { work.push((value, depth + 1)); } @@ -10328,7 +10724,9 @@ impl<'a> EscapeCheck<'a> { work.push((value, depth)); } } - ExprKind::Arena(block) | ExprKind::TaskGroup(block) => { + ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } + | ExprKind::TaskGroup(block) => { if let Some(value) = block.value.as_deref() { work.push((value, depth + 1)); } @@ -10407,17 +10805,29 @@ impl<'a> EscapeCheck<'a> { } /// Escape check for a returned value `e` (an explicit `return` or a body's trailing value): - /// a region-tracked value must be `Static` (returnable), and a `slice` must not view a local - /// array. The region-tracked diagnostic distinguishes a `Frame` borrow of local storage (use - /// `.clone()`) from an arena allocation. + /// a region-tracked value must be `Static` or caller-derived (returnable through its inferred + /// parameter-root summary), and a `slice` must not view a local array. The region-tracked + /// diagnostic distinguishes a `Frame` borrow of local storage (use `.clone()`) from an arena + /// allocation. fn check_return_escape(&mut self, e: &Expr, depth: u32) { let r = self.region_of(e, depth); + // The shared `array_builder` type also has an individually owned heap constructor, which + // remains returnable. A region constructor does not transfer its arena ownership to the + // callee, so returning that builder would let later mutation/build outlive the caller's + // lexical owner. Path-dependent heap/region builders fail closed here as well. + if e.ty.is_array_builder() && !self.drop_is_individual(e, depth) { + self.diags.error( + "cannot return a region-backed array_builder; pass the caller's builder as `borrow mut` and let the caller build it" + .to_string(), + e.span, + ); + } // A frame-local slice (a `slice` of a local `array`, `buffer.bytes()`, `resp.body()`) keeps // its dedicated message below; the region branch skips it so the diagnostic is unchanged and // not duplicated. An **arena-backed `bytes` view** (`fs.read_bytes_view`) is *not* // local-backed, so it flows to the region branch and gets the "allocated in an arena" message. let local_backed = self.mentions_slice(e.ty) && self.slice_is_local(e); - if self.region_bearing(e.ty) && !r.outlives(Region::Static) && !local_backed { + if self.region_bearing(e.ty) && !r.is_returnable() && !local_backed { let msg = if r == Region::Frame { "cannot return a view that borrows local storage (it is freed when the function returns); use `.clone()` to return an owned value" } else { @@ -10440,13 +10850,13 @@ impl<'a> EscapeCheck<'a> { } /// Escape check for a `break` value: it leaves the loop just as a returned value leaves the - /// function, so it must be `Static` (a Frame/arena view — including a view into a per-iteration - /// owned local dropped at the `break` — would dangle). Same rule as [`check_return_escape`], - /// with a `break`-specific message. + /// function, so it must be `Static` or caller-derived (a Frame/arena view — including a view + /// into a per-iteration owned local dropped at the `break` — would dangle). Same rule as + /// [`check_return_escape`], with a `break`-specific message. fn check_break_escape(&mut self, e: &Expr, depth: u32) { let r = self.region_of(e, depth); let local_backed = self.mentions_slice(e.ty) && self.slice_is_local(e); - if self.region_bearing(e.ty) && !r.outlives(Region::Static) && !local_backed { + if self.region_bearing(e.ty) && !r.is_returnable() && !local_backed { let msg = if r == Region::Frame { "cannot `break` a view that borrows local storage out of the loop (it is dropped at the end of the iteration); use `.clone()` to break an owned value" } else { @@ -10484,7 +10894,13 @@ impl<'a> EscapeCheck<'a> { Ty::Tagged(id), self.tagged_types, )), - Ty::Box(_) | Ty::Str | Ty::String | Ty::Struct(_) | Ty::DynArray(_) | Ty::DynStructArray(..) | Ty::DynSliceArray(_) => return true, + Ty::Box(_) | Ty::Str | Ty::String | Ty::Struct(_) | Ty::DynArray(_) + | Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..) + | Ty::DynStructArray(..) + | Ty::DynSliceArray(_) => return true, // An owned `array` is escape-checked in the same lane as every owned collection. // It borrows nothing, so its `region_of` is explicitly `Static` — the check passes and // the array is freely returnable (like `array` from `fs.read_dir`); tracking keeps @@ -10568,7 +10984,15 @@ impl<'a> EscapeCheck<'a> { // arguments from tainting a direct `fs.open` result while still failing closed for an // unknown borrowing boundary. (A `tcp_conn` itself is always owned, never a borrow, // so it is deliberately NOT here.) - Ty::Reader | Ty::Writer | Ty::Fn(_) => return true, + Ty::Reader + | Ty::Writer + | Ty::Fn(_) + | Ty::ArenaHandle + | Ty::ArrayBuilder(_) + | Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..) => return true, Ty::Resource(_) | Ty::ResourceRef(_) => return true, // Scalar/register values and owned handles carry no inferred borrow region. This list // is exhaustive so every future type must make an explicit escape-analysis choice. @@ -10581,11 +11005,9 @@ impl<'a> EscapeCheck<'a> { | Ty::Char | Ty::Vec(..) | Ty::Mask(..) - | Ty::ArenaHandle | Ty::Raw | Ty::Builder | Ty::Buffer - | Ty::ArrayBuilder(_) // The compiler-internal `str_finder` plan owns a boxed searcher (it copied the needle // bytes) — it borrows nothing, so it carries no inferred region. | Ty::StrFinder @@ -10663,8 +11085,23 @@ impl<'a> EscapeCheck<'a> { continue; } match &expression.kind { + ExprKind::Local(local) => values.push( + self.state + .individual + .get(local) + .copied() + .unwrap_or_else(|| { + !self.region_of(expression, depth).is_region_owned() + }), + ), // A callee cannot return arena-owned storage across its function boundary, // even when `region_of(Call)` is shortened by an arena-borrowing argument. + ExprKind::ArrayBuilderNew { region, .. } => { + values.push(region.is_none()); + } + ExprKind::ArrayBuilderBuild(builder) => { + work.push(Work::Eval(builder, depth)); + } ExprKind::Call { .. } | ExprKind::CallFnValue { .. } => values.push(true), ExprKind::OptionSome(inner) | ExprKind::ResultOk(inner) @@ -10690,7 +11127,9 @@ impl<'a> EscapeCheck<'a> { .unwrap_or_default(), ); } - ExprKind::Arena(block) | ExprKind::TaskGroup(block) => { + ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } + | ExprKind::TaskGroup(block) => { push_all( &mut work, &mut values, @@ -10800,10 +11239,7 @@ impl<'a> EscapeCheck<'a> { { values.push(true); } - _ => values.push(!matches!( - self.region_of(expression, depth), - Region::Arena(_) - )), + _ => values.push(!self.region_of(expression, depth).is_region_owned()), } } Work::All(children, index) => { @@ -10857,12 +11293,15 @@ impl<'a> EscapeCheck<'a> { .get(local) .copied() .unwrap_or_else(|| { - !matches!( - self.region_of(expression, depth), - Region::Arena(_) - ) + !self.region_of(expression, depth).is_region_owned() }), ), + ExprKind::ArrayBuilderNew { region, .. } => { + values.push(region.is_none()); + } + ExprKind::ArrayBuilderBuild(builder) => { + work.push(Work::Eval(builder, depth)); + } ExprKind::Call { .. } | ExprKind::CallFnValue { .. } => values.push(true), ExprKind::OptionSome(inner) | ExprKind::ResultOk(inner) @@ -10892,7 +11331,9 @@ impl<'a> EscapeCheck<'a> { values.push(true); } } - ExprKind::Arena(block) | ExprKind::TaskGroup(block) => { + ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } + | ExprKind::TaskGroup(block) => { if let Some(value) = block.value.as_deref() { work.push(Work::Eval(value, depth + 1)); } else { @@ -10971,10 +11412,7 @@ impl<'a> EscapeCheck<'a> { 0, )); } - _ => values.push(!matches!( - self.region_of(expression, depth), - Region::Arena(_) - )), + _ => values.push(!self.region_of(expression, depth).is_region_owned()), } } Work::All(children, index) => { @@ -11302,6 +11740,7 @@ impl<'a> EscapeCheck<'a> { ExprKind::Block(block) | ExprKind::Unsafe(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) => block.value.as_deref().map_or_else( CallableRegionFact::new, |value| self.callable_region_fact(value, depth), @@ -11943,12 +12382,25 @@ impl<'a> EscapeCheck<'a> { ); } ExprKind::Local(p) => values.push( - self.state - .region + self.region_capabilities .get(p) .copied() + .or_else(|| self.state.region.get(p).copied()) .unwrap_or(Region::Static), ), + // A region builder and the array frozen from it are tied to the explicit destination + // capability. The heap form has no region operand and remains independently owned. + ExprKind::ArrayBuilderNew { region, .. } => { + if let Some(region) = region { + work.push(Work::Eval(region, depth)); + } else { + values.push(Region::Static); + } + } + ExprKind::ArrayBuilderBuild(builder) => work.push(Work::Eval(builder, depth)), + // An explicit copy severs the source-storage borrow and is tied only to the + // destination capability. + ExprKind::CloneIn { region, .. } => work.push(Work::Eval(region, depth)), // A struct's region is the shortest-lived of its fields (a view over it lives only // as long as the shortest source); a scalar/literal-only struct stays `Static`. ExprKind::StructLit { fields, .. } => push_fold( @@ -12015,6 +12467,13 @@ impl<'a> EscapeCheck<'a> { values.push(Region::Static); } } + ExprKind::NamedArena { block, .. } => { + if let Some(value) = block.value.as_deref() { + work.push(Work::Eval(value, depth + 1)); + } else { + values.push(Region::Static); + } + } ExprKind::If { then, els, .. } => { push_fold( &mut work, @@ -12214,10 +12673,8 @@ impl<'a> EscapeCheck<'a> { | ExprKind::BytesRead { .. } | ExprKind::BufferPut { .. } | ExprKind::BufferAppend { .. } - | ExprKind::ArrayBuilderNew { .. } | ExprKind::ArrayBuilderPush { .. } | ExprKind::ArrayBuilderAppend { .. } - | ExprKind::ArrayBuilderBuild(..) | ExprKind::FsWriteFile { .. } | ExprKind::FsExists { .. } | ExprKind::FsRemove { .. } @@ -12430,7 +12887,10 @@ impl<'a> EscapeCheck<'a> { // An `arena` / `unsafe` / `task_group` block yields its block value, which is frame-local // if the inner value is (like the plain `Block` arm above). Without these a local-backed // slice returned through such a block escapes the function undetected (dangling slice). - ExprKind::Arena(b) | ExprKind::Unsafe(b) | ExprKind::TaskGroup(b) => { + ExprKind::Arena(b) + | ExprKind::NamedArena { block: b, .. } + | ExprKind::Unsafe(b) + | ExprKind::TaskGroup(b) => { work.extend(b.value.as_deref()); } // A closure may return a captured local-backed slice. Its callable value carries those @@ -12482,6 +12942,7 @@ impl<'a> EscapeCheck<'a> { | ExprKind::BoxGet(..) | ExprKind::BoxClone(..) | ExprKind::StrClone(..) + | ExprKind::CloneIn { .. } | ExprKind::StrPredicate { .. } | ExprKind::StrTrim { .. } | ExprKind::StrBorrow(..) @@ -12705,6 +13166,19 @@ impl<'a> EscapeCheck<'a> { }); work.push(EscapeWalkItem::Block(block, inner)); } + ExprKind::NamedArena { local, block } => { + let inner = depth + 1; + let region = Region::arena(inner); + self.region_capabilities.insert(*local, region); + self.allocation_regions.push(region); + work.push(EscapeWalkItem::ExprExit(expression, depth)); + work.push(EscapeWalkItem::ArenaDone { + block, + inner, + target: Region::arena(depth), + }); + work.push(EscapeWalkItem::Block(block, inner)); + } ExprKind::Block(block) | ExprKind::Unsafe(block) => { work.push(EscapeWalkItem::ExprExit(expression, depth)); work.push(EscapeWalkItem::Block(block, depth)); @@ -12784,8 +13258,13 @@ impl<'a> EscapeCheck<'a> { work.push(EscapeWalkItem::ExprExit(expression, depth)); let borrows_args = matches!(func.as_str(), "print" | "hash64" | "hash128"); - for argument in args.iter().rev() { - if !borrows_args { + let modes = self.named_param_modes.get(func); + for (index, argument) in args.iter().enumerate().rev() { + let transfers = modes + .and_then(|modes| modes.get(index)) + .is_some_and(|mode| *mode == ast::ParamMode::ByValue) + || (modes.is_none() && !borrows_args); + if transfers { work.push(EscapeWalkItem::Op( EscapeFlowOp::CallTransfer(argument, depth), )); @@ -12795,10 +13274,26 @@ impl<'a> EscapeCheck<'a> { } ExprKind::CallFnValue { callee, args } => { work.push(EscapeWalkItem::ExprExit(expression, depth)); - for argument in args.iter().rev() { - work.push(EscapeWalkItem::Op( - EscapeFlowOp::CallTransfer(argument, depth), - )); + let modes = match callee.ty { + Ty::Fn(id) => self.fn_types.get(id as usize).map(|function| { + function + .params + .iter() + .map(|(mode, _)| *mode) + .collect::>() + }), + _ => None, + }; + for (index, argument) in args.iter().enumerate().rev() { + if modes + .as_ref() + .and_then(|modes| modes.get(index)) + .is_none_or(|mode| *mode == ast::ParamMode::ByValue) + { + work.push(EscapeWalkItem::Op( + EscapeFlowOp::CallTransfer(argument, depth), + )); + } work.push(EscapeWalkItem::Expr(argument, depth)); } work.push(EscapeWalkItem::Expr(callee, depth)); @@ -12838,6 +13333,41 @@ impl<'a> EscapeCheck<'a> { } } EscapeWalkItem::ExprExit(expression, depth) => { + match &expression.kind { + ExprKind::ArrayBuilderPush { builder, value, .. } => { + self.push_flow_op(EscapeFlowOp::ArrayBuilderStore { + builder, + value, + depth, + }); + } + ExprKind::ArrayBuilderAppend { builder, data } => { + self.push_flow_op(EscapeFlowOp::ArrayBuilderStore { + builder, + value: data, + depth, + }); + } + ExprKind::Call { func, args, .. } => { + if let Some(modes) = self.named_param_modes.get(func).cloned() { + self.record_builder_call_modes(args, &modes, depth); + } + } + ExprKind::CallFnValue { callee, args } => { + if let Ty::Fn(id) = callee.ty + && let Some(modes) = self.fn_types.get(id as usize).map(|function| { + function + .params + .iter() + .map(|(mode, _)| *mode) + .collect::>() + }) + { + self.record_builder_call_modes(args, &modes, depth); + } + } + _ => {} + } if needs_drop_flag( expression.ty, self.structs, @@ -13065,6 +13595,47 @@ impl<'a> EscapeCheck<'a> { } } + /// A helper receiving `borrow mut array_builder` may retain any view-bearing argument in + /// that builder. Its own body uses a symbolic [`Region::Caller`] destination to reject local + /// frame/arena values; check relationships among incoming parameters at the caller's concrete + /// regions. MoveCheck separately carries the accepted roots forward so a later owner mutation + /// remains visible. + fn record_builder_call_modes( + &mut self, + args: &'a [Expr], + modes: &[ast::ParamMode], + depth: u32, + ) { + for (index, mode) in modes.iter().copied().enumerate() { + let Some(builder) = args.get(index) else { + continue; + }; + let Some(element) = builder.ty.array_builder_element() else { + continue; + }; + if matches!(mode, ast::ParamMode::Borrow | ast::ParamMode::Out) { + self.push_flow_op(EscapeFlowOp::RegionBuilderNonMutBorrow { + builder, + depth, + }); + continue; + } + if mode != ast::ParamMode::BorrowMut { + continue; + } + if !self.region_bearing(element.ty()) { + continue; + } + for value in args { + self.push_flow_op(EscapeFlowOp::ArrayBuilderStore { + builder, + value, + depth, + }); + } + } + } + fn apply_stmt(&mut self, s: &Stmt, depth: u32) { match s { Stmt::Let { local, init } => { @@ -13291,8 +13862,8 @@ impl<'a> EscapeCheck<'a> { self.replace_callable_region_path(*root, &path, value, depth); } Stmt::Return(Some(e)) => { - // A returned value escapes to the caller (`Static`): only a `Static`-region - // value may be returned (an arena/frame view cannot). + // A returned value escapes to the caller: only a `Static` or caller-derived value + // may be returned (a callee-local arena/frame view cannot). self.check_return_escape(e, depth); } Stmt::Return(None) => {} @@ -13395,6 +13966,14 @@ impl<'a> EscapeCheck<'a> { fn check_spawn_capture(&mut self, closure: &Expr, group: Region, depth: u32) { let check = |this: &mut Self, capture: &Expr| { + if capture.ty == Ty::ArenaHandle { + this.diags.error( + "a spawned task cannot capture a region capability (arena allocation is lexical and non-Send)" + .to_string(), + capture.span, + ); + return; + } if ty_mentions_resource( capture.ty, this.structs, @@ -13453,7 +14032,11 @@ impl<'a> EscapeCheck<'a> { #[inline(never)] fn walk_array_builder(&mut self, kind: &'a ExprKind, depth: u32) { match kind { - ExprKind::ArrayBuilderNew { .. } => {} + ExprKind::ArrayBuilderNew { region, .. } => { + if let Some(region) = region { + self.walk(region, depth); + } + } ExprKind::ArrayBuilderPush { builder, value, .. } => { self.walk(builder, depth); self.walk(value, depth); @@ -13494,6 +14077,7 @@ impl<'a> EscapeCheck<'a> { } ExprKind::TupleIndex { recv, .. } => self.walk(recv, depth), ExprKind::Arena(_) + | ExprKind::NamedArena { .. } | ExprKind::Block(_) | ExprKind::Loop { .. } | ExprKind::Unsafe(_) => { @@ -13556,6 +14140,10 @@ impl<'a> EscapeCheck<'a> { | ExprKind::RawIsNull(i) | ExprKind::BoxGet(i) | ExprKind::BoxClone(i) | ExprKind::StrClone(i) | ExprKind::StrBorrow(i) | ExprKind::StrBytes { inner: i } | ExprKind::BuilderToString(i) | ExprKind::ArrayToSoa { source: i, .. } | ExprKind::ArrayToSlice(i) | ExprKind::Len(i) => self.walk(i, depth), + ExprKind::CloneIn { value, region } => { + self.walk(value, depth); + self.walk(region, depth); + } ExprKind::ResourceFromRaw { raw, parent, .. } => { self.walk(raw, depth); if let Some(parent) = parent { @@ -14293,6 +14881,7 @@ fn hir_diverges(root: HirDivergenceNode<'_>) -> bool { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) => { work.push(HirDivergenceWork::Eval(HirDivergenceNode::Block(block))); @@ -14900,6 +15489,7 @@ fn match_scrutinee_materializes_result(e: &Expr) -> bool { ExprKind::If { .. } | ExprKind::Match { .. } | ExprKind::ElseUnwrap { .. } => true, ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) => block .value @@ -15028,6 +15618,32 @@ impl<'a> MoveCheck<'a> { } } + /// A `borrow mut array_builder` call may retain any view-bearing argument as a new element. + /// The function type already exposes every parameter mode and concrete type, so conservatively + /// join those argument roots into the caller's builder without a body-specific effect summary. + /// Non-borrowing elements add no roots and keep the ordinary exclusive-borrow refresh. + fn refresh_borrow_mut_call(&mut self, argument: &Expr, args: &[Expr]) { + self.refresh_borrow_mut_place(argument); + let Some(element) = argument.ty.array_builder_element() else { + return; + }; + if !ty_may_borrow( + element.ty(), + self.structs, + self.tuples, + self.enums, + self.tagged_types, + ) { + return; + } + let ExprKind::Local(local) = argument.kind else { + return; + }; + for value in args { + self.join_local_borrow_fallback(local, value); + } + } + fn check_call_borrow_aliases( &mut self, display: &str, @@ -15059,6 +15675,14 @@ impl<'a> MoveCheck<'a> { .copied() .unwrap_or(ast::ParamMode::ByValue); let conflicts = match (mode, peer_mode) { + // A region capability is a Copy allocation token, not storage the exclusive + // builder borrow can replace. The shared root is required when a helper grows + // that builder in the exact caller region. + (ast::ParamMode::BorrowMut, ast::ParamMode::ByValue) + if peer.ty == Ty::ArenaHandle => + { + false + } // An exclusive borrow can replace its owner, so every overlapping peer is // invalid even when that peer is a Copy view embedded in a plain aggregate. (ast::ParamMode::BorrowMut, _) => true, @@ -15091,7 +15715,14 @@ impl<'a> MoveCheck<'a> { fn local_storage_roots(&self, id: LocalId) -> BorrowRoots { let mut roots = self.borrows.sources.get(&id).cloned().unwrap_or_default(); - if let Some(position) = self.borrowed_param_position(id) { + let region_param = self + .f + .params + .iter() + .position(|¶m| param == id) + .filter(|_| self.f.locals.get(id as usize).is_some_and(|local| local.ty == Ty::ArenaHandle)) + .map(|position| position as u32); + if let Some(position) = region_param.or_else(|| self.borrowed_param_position(id)) { roots.insert(BorrowRoot::Param(position)); } else if self.local_owns_view_storage(id) || self.local_may_borrow(id) { roots.insert(BorrowRoot::Local(id)); @@ -15101,7 +15732,14 @@ impl<'a> MoveCheck<'a> { fn local_borrow_fact(&self, id: LocalId) -> BorrowFact { let mut fact = self.borrows.facts.get(&id).cloned().unwrap_or_default(); - if let Some(position) = self.borrowed_param_position(id) { + let region_param = self + .f + .params + .iter() + .position(|¶m| param == id) + .filter(|_| self.f.locals.get(id as usize).is_some_and(|local| local.ty == Ty::ArenaHandle)) + .map(|position| position as u32); + if let Some(position) = region_param.or_else(|| self.borrowed_param_position(id)) { fact.direct.insert(BorrowRoot::Param(position)); } else if self.local_owns_view_storage(id) || self.local_may_borrow(id) { fact.direct.insert(BorrowRoot::Local(id)); @@ -15452,6 +16090,7 @@ impl<'a> MoveCheck<'a> { kind, ExprKind::Block(_) | ExprKind::Arena(_) + | ExprKind::NamedArena { .. } | ExprKind::TaskGroup(_) | ExprKind::Unsafe(_) | ExprKind::If { .. } @@ -16058,6 +16697,7 @@ impl<'a> MoveCheck<'a> { ExprKind::FnValue(_) => BorrowFact::default(), ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) => self.block_value_fact(block), ExprKind::If { .. } => self @@ -16155,6 +16795,11 @@ impl<'a> MoveCheck<'a> { }; match &e.kind { ExprKind::Local(id) => self.borrows.sources.get(id).cloned().unwrap_or_default(), + ExprKind::CloneIn { region, .. } => self.storage_roots(region), + ExprKind::ArrayBuilderNew { region: Some(region), .. } => { + self.storage_roots(region) + } + ExprKind::ArrayBuilderBuild(builder) => self.borrow_sources(builder), ExprKind::ResourceBorrow { owner, .. } => self.storage_roots(owner), ExprKind::ResourceFromRaw { parent: Some(parent), @@ -16356,6 +17001,7 @@ impl<'a> MoveCheck<'a> { // an arena: that direction is the unsound one. ExprKind::Block(b) | ExprKind::Arena(b) + | ExprKind::NamedArena { block: b, .. } | ExprKind::TaskGroup(b) | ExprKind::Unsafe(b) => self.block_value_roots(b), ExprKind::If { then, els, .. } => { @@ -16429,8 +17075,8 @@ impl<'a> MoveCheck<'a> { | ExprKind::FileOpenRw { .. } | ExprKind::FilePread { .. } | ExprKind::FilePwrite { .. } | ExprKind::FileLen { .. } | ExprKind::BufferNew { .. } | ExprKind::BufferLen { .. } | ExprKind::BytesRead { .. } | ExprKind::BufferPut { .. } | ExprKind::BufferAppend { .. } - | ExprKind::ArrayBuilderNew { .. } | ExprKind::ArrayBuilderPush { .. } - | ExprKind::ArrayBuilderAppend { .. } | ExprKind::ArrayBuilderBuild(..) | ExprKind::FsWriteFile { .. } + | ExprKind::ArrayBuilderNew { region: None, .. } | ExprKind::ArrayBuilderPush { .. } + | ExprKind::ArrayBuilderAppend { .. } | ExprKind::FsWriteFile { .. } | ExprKind::FsExists { .. } | ExprKind::FsRemove { .. } | ExprKind::FsReadDir { .. } | ExprKind::DnsResolve { .. } | ExprKind::TcpConnect { .. } | ExprKind::TcpListen { .. } | ExprKind::TcpAccept { .. } | ExprKind::UdpBind { .. } | ExprKind::UdpSendTo { .. } @@ -16802,6 +17448,7 @@ impl<'a> MoveCheck<'a> { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) => { roots.extend(self.pipeline_block_source_roots(block)); @@ -17290,14 +17937,24 @@ impl<'a> MoveCheck<'a> { #[inline(never)] fn move_array_builder(&mut self, kind: &'a ExprKind, moved: &mut MovedSet) -> bool { match kind { - ExprKind::ArrayBuilderNew { .. } => {} + ExprKind::ArrayBuilderNew { region, .. } => { + if let Some(region) = region { + move_expr!(self, region, moved, false, false); + } + } ExprKind::ArrayBuilderPush { builder, value, .. } => { move_expr!(self, builder, moved, false, false); move_expr!(self, value, moved, true, true); + if let ExprKind::Local(local) = builder.kind { + self.join_local_borrow_fallback(local, value); + } } ExprKind::ArrayBuilderAppend { builder, data } => { move_expr!(self, builder, moved, false, false); move_expr!(self, data, moved, false, false); + if let ExprKind::Local(local) = builder.kind { + self.join_local_borrow_fallback(local, data); + } } ExprKind::ArrayBuilderBuild(i) => { move_expr!(self, i, moved, true, true); @@ -17372,6 +18029,7 @@ impl<'a> MoveCheck<'a> { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) => { let Some(value) = block.value.as_deref() else { @@ -17489,6 +18147,7 @@ impl<'a> MoveCheck<'a> { expression.kind, ExprKind::Block(_) | ExprKind::Arena(_) + | ExprKind::NamedArena { .. } | ExprKind::TaskGroup(_) | ExprKind::Unsafe(_) | ExprKind::Loop { .. } @@ -17801,6 +18460,7 @@ impl<'a> MoveCheck<'a> { match ¤t.kind { ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) if !block.stmts.is_empty() @@ -17859,7 +18519,10 @@ impl<'a> MoveCheck<'a> { }; ( child, - matches!(¤t.kind, ExprKind::Arena(_)), + matches!( + ¤t.kind, + ExprKind::Arena(_) | ExprKind::NamedArena { .. } + ), child_consuming, child_direct, Post::BlockExprSequence { @@ -17874,6 +18537,7 @@ impl<'a> MoveCheck<'a> { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) if block.value.is_none() @@ -17903,7 +18567,10 @@ impl<'a> MoveCheck<'a> { { ( index, - matches!(¤t.kind, ExprKind::Arena(_)), + matches!( + ¤t.kind, + ExprKind::Arena(_) | ExprKind::NamedArena { .. } + ), false, false, Post::BlockPairAfterIndex { @@ -17917,7 +18584,10 @@ impl<'a> MoveCheck<'a> { } else { ( value, - matches!(¤t.kind, ExprKind::Arena(_)), + matches!( + ¤t.kind, + ExprKind::Arena(_) | ExprKind::NamedArena { .. } + ), false, false, Post::BlockPairAfterValue { @@ -17933,6 +18603,7 @@ impl<'a> MoveCheck<'a> { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) if block.value.is_none() @@ -17971,7 +18642,10 @@ impl<'a> MoveCheck<'a> { { ( index, - matches!(¤t.kind, ExprKind::Arena(_)), + matches!( + ¤t.kind, + ExprKind::Arena(_) | ExprKind::NamedArena { .. } + ), false, false, Post::BlockPairAfterIndex { @@ -17985,7 +18659,10 @@ impl<'a> MoveCheck<'a> { } else { ( value, - matches!(¤t.kind, ExprKind::Arena(_)), + matches!( + ¤t.kind, + ExprKind::Arena(_) | ExprKind::NamedArena { .. } + ), true, true, Post::BlockPairAfterValue { @@ -18001,6 +18678,7 @@ impl<'a> MoveCheck<'a> { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) if block.value.is_none() @@ -18034,7 +18712,10 @@ impl<'a> MoveCheck<'a> { ); ( value, - matches!(¤t.kind, ExprKind::Arena(_)), + matches!( + ¤t.kind, + ExprKind::Arena(_) | ExprKind::NamedArena { .. } + ), !self_assign, !self_assign, Post::BlockAssignField { @@ -18047,6 +18728,7 @@ impl<'a> MoveCheck<'a> { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) if block.value.is_none() @@ -18062,7 +18744,10 @@ impl<'a> MoveCheck<'a> { }; ( value, - matches!(¤t.kind, ExprKind::Arena(_)), + matches!( + ¤t.kind, + ExprKind::Arena(_) | ExprKind::NamedArena { .. } + ), false, false, Post::None, @@ -18070,6 +18755,7 @@ impl<'a> MoveCheck<'a> { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) if block.value.is_none() @@ -18088,7 +18774,10 @@ impl<'a> MoveCheck<'a> { }; ( init, - matches!(¤t.kind, ExprKind::Arena(_)), + matches!( + ¤t.kind, + ExprKind::Arena(_) | ExprKind::NamedArena { .. } + ), true, true, Post::BlockLetTuple { locals, init }, @@ -18096,6 +18785,7 @@ impl<'a> MoveCheck<'a> { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) if block.value.is_none() @@ -18116,7 +18806,10 @@ impl<'a> MoveCheck<'a> { }; ( value, - matches!(¤t.kind, ExprKind::Arena(_)), + matches!( + ¤t.kind, + ExprKind::Arena(_) | ExprKind::NamedArena { .. } + ), true, true, Post::BlockBreak { @@ -18127,6 +18820,7 @@ impl<'a> MoveCheck<'a> { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) if block.value.is_none() @@ -18142,7 +18836,10 @@ impl<'a> MoveCheck<'a> { }; ( init, - matches!(¤t.kind, ExprKind::Arena(_)), + matches!( + ¤t.kind, + ExprKind::Arena(_) | ExprKind::NamedArena { .. } + ), true, true, Post::BlockLet { @@ -18153,6 +18850,7 @@ impl<'a> MoveCheck<'a> { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) if block.value.is_none() @@ -18172,7 +18870,10 @@ impl<'a> MoveCheck<'a> { }; ( value, - matches!(¤t.kind, ExprKind::Arena(_)), + matches!( + ¤t.kind, + ExprKind::Arena(_) | ExprKind::NamedArena { .. } + ), true, true, Post::BlockAssign { @@ -18185,6 +18886,7 @@ impl<'a> MoveCheck<'a> { } ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) if block.value.is_none() @@ -18200,7 +18902,10 @@ impl<'a> MoveCheck<'a> { }; ( value, - matches!(¤t.kind, ExprKind::Arena(_)), + matches!( + ¤t.kind, + ExprKind::Arena(_) | ExprKind::NamedArena { .. } + ), true, true, Post::BlockReturn(value), @@ -18234,7 +18939,7 @@ impl<'a> MoveCheck<'a> { }; (child, false, false, false, Post::None) } - ExprKind::Arena(block) + ExprKind::Arena(block) | ExprKind::NamedArena { block, .. } if block.stmts.is_empty() && block.value.is_some() => { ( @@ -18245,7 +18950,7 @@ impl<'a> MoveCheck<'a> { Post::None, ) } - ExprKind::Arena(block) + ExprKind::Arena(block) | ExprKind::NamedArena { block, .. } if block.value.is_none() && matches!( block.stmts.as_slice(), @@ -19345,6 +20050,7 @@ impl<'a> MoveCheck<'a> { match &wrapper.kind { ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) => match &block.stmts[0] { Stmt::AssignIndex { index, .. } @@ -19750,6 +20456,7 @@ impl<'a> MoveCheck<'a> { | ExprKind::IndexField { .. } => return false, ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::TaskGroup(block) | ExprKind::Unsafe(block) | ExprKind::Loop { body: block, .. } @@ -20018,7 +20725,7 @@ impl<'a> MoveCheck<'a> { argument.span, ); self.borrows.invalidate_roots(&roots, BorrowEnd::Consumed); - self.refresh_borrow_mut_place(argument); + self.refresh_borrow_mut_call(argument, args); } } } @@ -20064,18 +20771,18 @@ impl<'a> MoveCheck<'a> { Ty::Fn(id) => self.fn_types[id as usize].params[index].0, _ => ast::ParamMode::ByValue, }; - if mode == ast::ParamMode::BorrowMut - && let Some(argument) = args.get(index) - { - let roots = self.storage_roots(argument); - self.reject_live_resource_dependents_of_roots( - &roots, - moved, - argument.span, - ); - self.borrows.invalidate_roots(&roots, BorrowEnd::Consumed); - self.refresh_borrow_mut_place(argument); - } + if mode == ast::ParamMode::BorrowMut + && let Some(argument) = args.get(index) + { + let roots = self.storage_roots(argument); + self.reject_live_resource_dependents_of_roots( + &roots, + moved, + argument.span, + ); + self.borrows.invalidate_roots(&roots, BorrowEnd::Consumed); + self.refresh_borrow_mut_call(argument, args); + } } } ExprKind::StructLit { fields, .. } => { @@ -20207,6 +20914,10 @@ impl<'a> MoveCheck<'a> { | ExprKind::Len(i) => { move_expr!(self, i, moved, false, false) } + ExprKind::CloneIn { value, region } => { + move_expr!(self, value, moved, false, false); + move_expr!(self, region, moved, false, false); + } ExprKind::ArraySum { source, stages } | ExprKind::ArrayCount { source, stages } | ExprKind::ArrayAnyAll { source, stages, .. } @@ -20392,6 +21103,14 @@ impl<'a> MoveCheck<'a> { return false; } } + ExprKind::NamedArena { block, .. } => { + self.arena_depth += 1; + let falls_through = self.block(block, moved, consuming, direct); + self.arena_depth -= 1; + if !falls_through { + return false; + } + } // A `loop` runs its body repeatedly, so a value moved out of an *enclosing* (pre-loop) // local by one iteration is already moved at the start of the next — a use there is a // use-after-move on the loop back-edge. Two passes make this sound (moves are monotonic; @@ -21468,6 +22187,24 @@ impl<'a, 't> Checker<'a, 't> { children.push((scalar_to_ty(a), scalar_to_ty(b))); true } + (Ty::ArrayBuilder(a), Ty::ArrayBuilder(b)) => { + children.push((scalar_to_ty(a), scalar_to_ty(b))); + true + } + (Ty::VecArrayBuilder(a, an), Ty::VecArrayBuilder(b, bn)) + | (Ty::MaskArrayBuilder(a, an), Ty::MaskArrayBuilder(b, bn)) + | (Ty::FixedArrayBuilder(a, an), Ty::FixedArrayBuilder(b, bn)) + | (Ty::DynVecArray(a, an), Ty::DynVecArray(b, bn)) + | (Ty::DynMaskArray(a, an), Ty::DynMaskArray(b, bn)) + | (Ty::DynFixedArray(a, an), Ty::DynFixedArray(b, bn)) => { + children.push((scalar_to_ty(a), scalar_to_ty(b))); + an == bn + } + (Ty::FixedStructArrayBuilder(a, an), Ty::FixedStructArrayBuilder(b, bn)) + | (Ty::DynFixedStructArray(a, an), Ty::DynFixedStructArray(b, bn)) => { + children.push((Ty::Struct(a), Ty::Struct(b))); + an == bn + } (Ty::Result(a_ok, a_err), Ty::Result(b_ok, b_err)) => { children.push((scalar_to_ty(a_ok), scalar_to_ty(b_ok))); children.push((scalar_to_ty(a_err), scalar_to_ty(b_err))); @@ -21635,6 +22372,21 @@ impl<'a, 't> Checker<'a, 't> { work.push(Work::Type(scalar_to_ty(payload))); work.push(Work::Text("array<".to_string())); } + Ty::ArrayBuilder(element) => { + work.push(Work::Text(">".to_string())); + work.push(Work::Type(scalar_to_ty(element))); + work.push(Work::Text("array_builder<".to_string())); + } + ty if let Some(element) = ty.array_builder_element() => { + work.push(Work::Text(">".to_string())); + work.push(Work::Type(element.ty())); + work.push(Work::Text("array_builder<".to_string())); + } + ty if let Some(element) = ty.dyn_aggregate_array_element() => { + work.push(Work::Text(">".to_string())); + work.push(Work::Type(element.ty())); + work.push(Work::Text("array<".to_string())); + } Ty::StructArray(id, len) => { work.push(Work::Text(format!(">[{len}]"))); work.push(Work::Type(Ty::Struct(id))); @@ -21923,6 +22675,12 @@ impl<'a, 't> Checker<'a, 't> { let mut params = Vec::new(); for (p, ty) in f.params.iter().zip(param_tys) { + if ty == Ty::ArenaHandle && p.mode != ast::ParamMode::ByValue { + self.diags.error( + "a region capability parameter must be passed by value; it cannot be `out`, `borrow`, or `borrow mut`".to_string(), + p.ty.span(), + ); + } // An `out` parameter is a writable output buffer — only a `slice` (a borrow the // callee writes back through). Mark its local mutable so `dst[i] = v` is allowed. if p.mode.is_out() && !matches!(ty, Ty::Slice(_) | Ty::Error) { @@ -21948,7 +22706,8 @@ impl<'a, 't> Checker<'a, 't> { let id = self.declare( &p.name.name, ty, - p.mode.is_out() || p.mode == ast::ParamMode::BorrowMut, + ty != Ty::ArenaHandle + && (p.mode.is_out() || p.mode == ast::ParamMode::BorrowMut), ); if let Some(spelling) = self.json_scan_row_source_spelling(&p.ty) { self.json_scan_local_spellings.insert(id, spelling); @@ -22051,7 +22810,14 @@ impl<'a, 't> Checker<'a, 't> { }; let initializer_spelling = self.json_scan_source_spelling_of_expr(&init); self.json_scan_source_spelling = saved_json_scan_source_spelling; - let local_ty = ann.unwrap_or(init.ty); + let mut local_ty = ann.unwrap_or(init.ty); + if self.resolve(local_ty) == Ty::ArenaHandle { + self.diags.error( + "a region capability cannot be stored in an ordinary local; pass it as a function parameter or use the binding introduced by `arena name {}` directly".to_string(), + name.span, + ); + local_ty = Ty::Error; + } self.check_shadow(&name.name, name.span, self.scope.len()); let local = self.declare(&name.name, local_ty, *is_mut); if let Some(spelling) = annotation_spelling.or(initializer_spelling) { @@ -22892,6 +23658,7 @@ impl<'a, 't> Checker<'a, 't> { | ExprKind::Match { .. } => {} ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::Unsafe(block) | ExprKind::TaskGroup(block) => { self.reconcile_diverging_completion_block(block, expected); @@ -23023,6 +23790,25 @@ impl<'a, 't> Checker<'a, 't> { }; Expr { kind: ExprKind::Arena(block), ty, span: e.span } } + ast::ExprKind::NamedArena { name, block: b } => { + let syntax_diverges = ast_block_diverges(b); + let scope_mark = self.scope.len(); + self.check_shadow(&name.name, name.span, self.scope.len()); + let local = self.declare(&name.name, Ty::ArenaHandle, false); + self.arena_depth += 1; + let block = self.check_block(b, if syntax_diverges { None } else { expected }); + self.arena_depth -= 1; + self.scope.truncate(scope_mark); + let diverges = hir_block_diverges(&block); + let ty = if diverges { + diverging_block_result_ty(&block, expected) + } else { + let t = block.value.as_ref().map(|v| v.ty).unwrap_or(Ty::Unit); + self.constrain(t, expected, e.span); + t + }; + Expr { kind: ExprKind::NamedArena { local, block }, ty, span: e.span } + } ast::ExprKind::Unsafe(b) => { // A marker block — no region, no runtime effect. It only raises `unsafe_depth` so the // `raw.*` ops inside are permitted, and (via the effect scan) marks the fn impure. The @@ -23506,7 +24292,10 @@ impl<'a, 't> Checker<'a, 't> { } // A borrow / sub-slice of a read-only view is itself read-only. ExprKind::SliceRange { recv, .. } | ExprKind::ArrayToSlice(recv) => self.hir_is_readonly_view(recv), - ExprKind::Block(b) | ExprKind::Arena(b) | ExprKind::Unsafe(b) => { + ExprKind::Block(b) + | ExprKind::Arena(b) + | ExprKind::NamedArena { block: b, .. } + | ExprKind::Unsafe(b) => { b.value.as_ref().is_some_and(|v| self.hir_is_readonly_view(v)) } ExprKind::If { then, els, .. } => { @@ -25922,7 +26711,7 @@ impl<'a, 't> Checker<'a, 't> { // method"); `append` is shared with `buffer`, so dispatch it on the receiver type. if matches!(method, "push" | "build") && let Some((_, rty)) = self.place_local(recv) - && matches!(self.resolve(rty), Ty::ArrayBuilder(_)) + && self.resolve(rty).is_array_builder() { if method == "push" { return self.check_array_builder_push(recv, args, span); @@ -25932,7 +26721,7 @@ impl<'a, 't> Checker<'a, 't> { } if method == "append" { if let Some((_, rty)) = self.place_local(recv) - && matches!(self.resolve(rty), Ty::ArrayBuilder(_)) + && self.resolve(rty).is_array_builder() { return self.check_array_builder_append(recv, args, span); } @@ -26170,6 +26959,7 @@ impl<'a, 't> Checker<'a, 't> { // http-client arm below; `check_box_get` otherwise swallows it with a box-only error). "get" if recv_ty != Ty::HttpClient => self.check_box_get(recv_expr, recv_ty, args, span), "clone" => self.check_box_clone(recv_expr, recv_ty, args, span), + "clone_in" => self.check_clone_in(recv_expr, recv_ty, args, span), "contains" | "starts_with" | "ends_with" | "find" | "rfind" | "eq_ignore_ascii_case" if matches!(recv_ty, Ty::Str | Ty::String) => { @@ -28787,6 +29577,65 @@ impl<'a, 't> Checker<'a, 't> { } } + /// Copy a text/byte view into the caller-selected region. The result borrows only `out`, so it + /// may outlive the source but never the arena that owns the explicit capability. + fn check_clone_in(&mut self, recv: Expr, recv_ty: Ty, args: &[ast::Expr], span: Span) -> Expr { + let err = || Expr { kind: ExprKind::Bool(false), ty: Ty::Error, span }; + if args.len() != 1 { + self.diags.error( + format!("'.clone_in()' takes exactly one region argument, got {}", args.len()), + span, + ); + return err(); + } + let region = self.check_expr(&args[0], Some(Ty::ArenaHandle)); + let region_ty = self.resolve(region.ty); + if region_ty == Ty::Error { + return err(); + } + if region_ty != Ty::ArenaHandle { + self.diags.error( + format!("'.clone_in()' expects a region, got {}", self.ty_display(region.ty)), + args[0].span, + ); + return err(); + } + let (value, ty) = match self.resolve(recv_ty) { + Ty::Str => (recv, Ty::Str), + Ty::String => { + let rspan = recv.span; + ( + Expr { kind: ExprKind::StrBorrow(Box::new(recv)), ty: Ty::Str, span: rspan }, + Ty::Str, + ) + } + Ty::Slice(Scalar::Int(IntTy { bits: 8, signed: false })) => (recv, recv_ty), + ty @ Ty::Struct(_) => { + if let Some(reason) = self.region_plain_error(ty) { + self.diags.error( + format!("cannot clone {} into a region: {reason}", self.ty_display(ty)), + span, + ); + return err(); + } + (recv, ty) + } + Ty::Error => return err(), + other => { + self.diags.error( + format!("'.clone_in()' is available on str, string, bytes, and RegionPlain structs, got {}", self.ty_display(other)), + span, + ); + return err(); + } + }; + Expr { + kind: ExprKind::CloneIn { value: Box::new(value), region: Box::new(region) }, + ty, + span, + } + } + /// `s.contains(n)` / `s.starts_with(p)` / `s.ends_with(s)` / `s.find(n)` — byte-oriented `str` /// scans (`core.string`, draft.md §18). The receiver (`recv`, already a `str`/`string`) and the /// single argument are both treated as `str` views: an owned `string` is auto-borrowed @@ -28902,32 +29751,140 @@ impl<'a, 't> Checker<'a, 't> { Expr { kind: ExprKind::BufferNew { capacity: Box::new(c) }, ty: Ty::Buffer, span } } - /// `array_builder()` (M12 A6) — open an empty growable typed array builder. Takes no value - /// arguments; the element type is inferred from the expected type (a binding/return annotation - /// `array_builder`), mirroring `json.decode`'s context-driven target. Fail-closed: with no - /// inferable element type, a clean error (rather than a silent default). + /// Open an empty growable typed array builder. `array_builder()` preserves the individually + /// owned heap form; `array_builder(out)` selects an explicit region capability. The element + /// type is inferred from the expected `array_builder` annotation. fn check_array_builder_new(&mut self, args: &[ast::Expr], expected: Option, span: Span) -> Expr { let err = Expr { kind: ExprKind::Bool(false), ty: Ty::Error, span }; - if !args.is_empty() { - self.diags.error(format!("'array_builder' takes no arguments (the element type is inferred from the binding), got {}", args.len()), span); + if args.len() > 1 { + self.diags.error(format!("'array_builder' takes zero arguments for heap storage or one `region` argument, got {}", args.len()), span); return err; } - let Some(Ty::ArrayBuilder(elem)) = expected.map(|t| self.resolve(t)) else { + let region = args.first().map(|argument| self.check_expr(argument, Some(Ty::ArenaHandle))); + if let Some(region) = ®ion { + if region.ty == Ty::Error { + return err; + } + if region.ty != Ty::ArenaHandle { + self.diags.error( + format!("region-backed 'array_builder' expects a `region`, got {}", ty_name(region.ty)), + region.span, + ); + return err; + } + } + let Some(elem) = expected + .map(|ty| self.resolve(ty)) + .and_then(Ty::array_builder_element) + else { self.diags.error( "cannot infer the array_builder element type; annotate the binding, e.g. `b: array_builder := array_builder()`".to_string(), span, ); return err; }; - Expr { kind: ExprKind::ArrayBuilderNew { elem }, ty: Ty::ArrayBuilder(elem), span } + if region.is_some() { + if let Some(reason) = self.region_plain_error(elem.ty()) { + self.diags.error( + format!("array_builder<{}> cannot use region storage: {reason}", + elem.name()), + span, + ); + return err; + } + } else if !matches!(elem, + ArrayBuilderElem::Scalar( + Scalar::Int(_) | Scalar::Float(_) | Scalar::Bool | Scalar::Char | Scalar::String) + ) { + self.diags.error( + format!( + "heap array_builder<{}> requires a Copy scalar or `string`; use `array_builder(out)` for RegionPlain values", + elem.name() + ), + span, + ); + return err; + } + Expr { + kind: ExprKind::ArrayBuilderNew { elem, region: region.map(Box::new) }, + ty: Ty::array_builder(elem), + span, + } + } + + /// Return the deterministic first reason `ty` is not recursively RegionPlain. The traversal is + /// source-field/variant order and cycle-safe; unsupported ownership is rejected before MIR. + fn region_plain_error(&self, ty: Ty) -> Option { + let mut work = vec![(ty, String::new())]; + let mut seen = HashSet::new(); + while let Some((ty, path)) = work.pop() { + let ty = expand_tagged_ty(ty, self.tagged_types); + if !seen.insert(ty) { + continue; + } + let at = |what: &str| { + if path.is_empty() { what.to_string() } else { format!("field '{path}' {what}") } + }; + match ty { + Ty::Int(_) | Ty::Float(_) | Ty::Bool | Ty::Char | Ty::Unit | Ty::Str + | Ty::Vec(..) | Ty::Mask(..) => {} + Ty::Slice(Scalar::Int(IntTy { bits: 8, signed: false })) => {} + Ty::Option(payload) => work.push((scalar_to_ty(payload), path)), + Ty::Struct(id) => { + let Some(definition) = self.structs.get(id as usize) else { + return Some(at("has an unknown struct definition")); + }; + for field in definition.fields.iter().rev() { + let child = if path.is_empty() { + field.name.clone() + } else { + format!("{path}.{}", field.name) + }; + work.push((field.ty, child)); + } + } + Ty::Enum(id) => { + let Some(definition) = self.enums.get(id as usize) else { + return Some(at("has an unknown sum definition")); + }; + for variant in definition.variants.iter().rev() { + for (index, payload) in variant.payload.iter().enumerate().rev() { + let leaf = format!("{}[{index}]", variant.name); + let child = if path.is_empty() { leaf } else { format!("{path}.{leaf}") }; + work.push((scalar_to_ty(*payload), child)); + } + } + } + Ty::Array(payload, _) => work.push((scalar_to_ty(payload), path)), + Ty::StructArray(id, _) => work.push((Ty::Struct(id), path)), + Ty::String | Ty::DynArray(_) | Ty::DynStructArray(..) | Ty::DynSliceArray(_) + | Ty::Box(_) => return Some(at("owns independent heap storage")), + Ty::Resource(_) | Ty::ResourceRef(_) => return Some(at("is a native resource")), + Ty::Raw => return Some(at("is `raw`")), + Ty::Fn(_) => return Some(at("is a function value")), + Ty::Builder + | Ty::Buffer + | Ty::ArrayBuilder(_) + | Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..) => return Some(at("is a builder")), + other => return Some(at(&format!("has unsupported type {}", ty_name(other)))), + } + } + None } /// The `mut`-bound-`array_builder`-local check shared by `push`/`append` (they grow the builder in /// place through its handle). Returns the builder's element scalar on success. Mirrors the /// `buffer` `put_*`/`append` receiver rule. - fn array_builder_mut_receiver(&mut self, recv: &ast::Expr, method: &str) -> Option<(Scalar, Expr)> { + fn array_builder_mut_receiver( + &mut self, + recv: &ast::Expr, + method: &str, + ) -> Option<(ArrayBuilderElem, Expr)> { let recv_expr = self.check_expr(recv, None); - let Ty::ArrayBuilder(elem) = self.resolve(recv_expr.ty) else { + let Some(elem) = self.resolve(recv_expr.ty).array_builder_element() else { if recv_expr.ty != Ty::Error { self.diags.error(format!("'.{method}()' grows an `array_builder`, but the receiver is {}", ty_name(recv_expr.ty)), recv.span); } @@ -28953,39 +29910,47 @@ impl<'a, 't> Checker<'a, 't> { /// `b.push(v)` (M12 A6) — append one element to a growable `array_builder`, borrowing the builder /// (mutated through its handle, not consumed). The receiver must be a `mut`-bound `array_builder` - /// local. For a Copy-scalar element `v` is copied in; for a `string` element `v` is **moved** in - /// (its source nulled — the builder then owns the buffer, deep-freed on Drop). Pure (growth). + /// local. A RegionPlain value is copied with its borrow provenance; a heap-form `string` is + /// **moved** in (its source is nulled and the builder deep-frees it on Drop). Calls through a + /// `borrow mut` parameter conservatively retain the roots of all view-bearing arguments in the + /// caller's builder. Pure (growth). fn check_array_builder_push(&mut self, recv: &ast::Expr, args: &[ast::Expr], span: Span) -> Expr { let err = Expr { kind: ExprKind::Bool(false), ty: Ty::Error, span }; - let Some((elem, recv_expr)) = self.array_builder_mut_receiver(recv, "push") else { + let Some((elem, recv_expr)) = + self.array_builder_mut_receiver(recv, "push") + else { return err; }; let [v] = args else { self.diags.error(format!("'.push()' takes 1 argument (the element), got {}", args.len()), span); return err; }; - let elem_ty = scalar_to_ty(elem); + let elem_ty = elem.ty(); let value = self.check_expr(v, Some(elem_ty)); if value.ty == Ty::Error { return err; } - if self.resolve(value.ty) != elem_ty { + if expand_tagged_ty(self.resolve(value.ty), self.tagged_types) + != expand_tagged_ty(elem_ty, self.tagged_types) + { self.diags.error(format!("'.push()' expects a {} element, got {}", ty_name(elem_ty), ty_name(value.ty)), v.span); return err; } - Expr { kind: ExprKind::ArrayBuilderPush { builder: Box::new(recv_expr), value: Box::new(value), moves_value: elem == Scalar::String }, ty: Ty::Unit, span } + Expr { kind: ExprKind::ArrayBuilderPush { builder: Box::new(recv_expr), value: Box::new(value), moves_value: elem == ArrayBuilderElem::Scalar(Scalar::String)}, ty: Ty::Unit, span } } /// `b.append(xs)` (M12 A6) — bulk-append a `slice` of Copy-scalar elements to a growable - /// `array_builder`, borrowing the builder (mutated in place) and copying `xs` in. Only Copy-scalar - /// elements are appendable (a `string` element is added one at a time via `push`, which moves it - /// in — a borrowed `slice` could not be bulk-moved). Pure (growth). + /// `array_builder`, borrowing the builder (mutated in place) and copying `xs` in. Every admitted + /// non-`string` element is Copy; a `string` is added one at a time via `push`, which moves it in. + /// Pure (growth). fn check_array_builder_append(&mut self, recv: &ast::Expr, args: &[ast::Expr], span: Span) -> Expr { let err = Expr { kind: ExprKind::Bool(false), ty: Ty::Error, span }; - let Some((elem, recv_expr)) = self.array_builder_mut_receiver(recv, "append") else { + let Some((elem, recv_expr)) = + self.array_builder_mut_receiver(recv, "append") + else { return err; }; - if elem == Scalar::String { + if elem == ArrayBuilderElem::Scalar(Scalar::String) { self.diags.error( "'.append()' is not available on an array_builder (append bulk-copies a borrowed slice; a `string` element is added with `push`, which moves it in)".to_string(), span, @@ -28996,7 +29961,17 @@ impl<'a, 't> Checker<'a, 't> { self.diags.error(format!("'.append()' takes 1 argument (a slice of elements), got {}", args.len()), span); return err; }; - let want = Ty::Slice(elem); + let ArrayBuilderElem::Scalar(scalar) = elem else { + self.diags.error( + format!( + "'.append()' is not available on an array_builder<{}>; add fixed-layout aggregate elements with `push`", + elem.name() + ), + span, + ); + return err; + }; + let want = Ty::Slice(scalar); let data = self.check_expr(xs, Some(want)); if data.ty == Ty::Error { return err; @@ -29008,12 +29983,12 @@ impl<'a, 't> Checker<'a, 't> { Expr { kind: ExprKind::ArrayBuilderAppend { builder: Box::new(recv_expr), data: Box::new(data) }, ty: Ty::Unit, span } } - /// `b.build()` (M12 A6) — freeze an `array_builder` into an owned `array`, **consuming** - /// (moving) the builder. A zero-copy ptr+len retype: the builder's storage becomes the array - /// buffer. The element `string` freezes into an `array` (deep-drop owned by the array). + /// `b.build()` (M12 A6) — freeze an `array_builder` into `array`, **consuming** (moving) + /// the builder. Heap storage transfers zero-copy; region chunks compact once into the same + /// region. A heap `string` element freezes into an individually owned `array`. fn check_array_builder_build(&mut self, recv_expr: Expr, args: &[ast::Expr], span: Span) -> Expr { let err = Expr { kind: ExprKind::Bool(false), ty: Ty::Error, span }; - let Ty::ArrayBuilder(elem) = self.resolve(recv_expr.ty) else { + let Some(elem) = self.resolve(recv_expr.ty).array_builder_element() else { if recv_expr.ty != Ty::Error { self.diags.error(format!("'.build()' is an array_builder method, got {}", ty_name(recv_expr.ty)), span); } @@ -29023,12 +29998,8 @@ impl<'a, 't> Checker<'a, 't> { self.diags.error(format!("'.build()' takes no arguments, got {}", args.len()), span); return err; } - // Every valid element scalar is a `PrimScalar` (Copy scalar or `string`) — the array element. - let Some(prim) = scalar_to_prim(elem) else { - self.diags.error(format!("array_builder<{}> cannot be built into an array", scalar_name(elem)), span); - return err; - }; - Expr { kind: ExprKind::ArrayBuilderBuild(Box::new(recv_expr)), ty: Ty::DynArray(prim_to_scalar(prim)), span } + let result_ty = array_builder_result_ty(elem); + Expr { kind: ExprKind::ArrayBuilderBuild(Box::new(recv_expr)), ty: result_ty, span } } /// `b.write(s)` / `b.write_int(n)` / `b.write_bool(v)` / `b.write_char(c)` / @@ -30431,7 +31402,10 @@ impl<'a, 't> Checker<'a, 't> { match r.ty { // `str`/`slice`/`soa` carry a runtime length in their `{ ptr, len }` view (a `soa`'s // length is its row count). - Ty::Str | Ty::String | Ty::Slice(_) | Ty::DynArray(_) | Ty::DynStructArray(..) | Ty::DynSliceArray(_) | Ty::DynResponseArray |Ty::Soa(_) => Expr { kind: ExprKind::Len(Box::new(r)), ty: i64_ty, span }, + Ty::Str | Ty::String | Ty::Slice(_) | Ty::DynArray(_) + | Ty::DynVecArray(..) | Ty::DynMaskArray(..) | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..) + | Ty::DynStructArray(..) | Ty::DynSliceArray(_) | Ty::DynResponseArray |Ty::Soa(_) => Expr { kind: ExprKind::Len(Box::new(r)), ty: i64_ty, span }, // A `buffer`'s length is its current byte count (the last read's size). Same v1 // bound-receiver restriction as `.bytes()` (uniform across buffer methods, until Move // temporaries drop): reject `buffer(n).len()` on an unbound temporary. @@ -30508,6 +31482,7 @@ impl<'a, 't> Checker<'a, 't> { } let elem = match r.ty { Ty::Array(s, _) | Ty::Slice(s) | Ty::DynArray(s) => scalar_to_ty(s), + ty if let Some(elem) = ty.dyn_aggregate_array_element() => elem.ty(), // Indexing an `array>` (a `chunks` result) yields one chunk `slice`. Ty::DynSliceArray(p) => Ty::Slice(prim_to_scalar(p)), // Indexing a struct array yields the whole struct by value (a copy). A plain-data struct @@ -30552,6 +31527,10 @@ impl<'a, 't> Checker<'a, 't> { | Ty::Writer | Ty::Buffer | Ty::ArrayBuilder(_) + | Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..) | Ty::TcpConn | Ty::TcpListener | Ty::UdpSocket @@ -30616,6 +31595,10 @@ impl<'a, 't> Checker<'a, 't> { | Ty::Writer | Ty::Buffer | Ty::ArrayBuilder(_) + | Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..) | Ty::TcpConn | Ty::TcpListener | Ty::UdpSocket @@ -34638,7 +35621,11 @@ impl<'a, 't> Checker<'a, 't> { #[inline(never)] fn finalize_array_builder(&mut self, kind: &mut ExprKind) { match kind { - ExprKind::ArrayBuilderNew { .. } => {} + ExprKind::ArrayBuilderNew { region, .. } => { + if let Some(region) = region { + self.finalize_expr(region); + } + } ExprKind::ArrayBuilderPush { builder, value, .. } => { self.finalize_expr(builder); self.finalize_expr(value); @@ -34863,7 +35850,12 @@ impl<'a, 't> Checker<'a, 't> { self.finalize_expr(f); } } - ExprKind::Block(b) | ExprKind::Arena(b) | ExprKind::TaskGroup(b) | ExprKind::Unsafe(b) | ExprKind::Loop { body: b, .. } => self.finalize_block(b), + ExprKind::Block(b) + | ExprKind::Arena(b) + | ExprKind::NamedArena { block: b, .. } + | ExprKind::TaskGroup(b) + | ExprKind::Unsafe(b) + | ExprKind::Loop { body: b, .. } => self.finalize_block(b), ExprKind::RawAlloc(e) | ExprKind::RawFree(e) | ExprKind::RawIsNull(e) => { self.finalize_expr(e) } @@ -34932,6 +35924,10 @@ impl<'a, 't> Checker<'a, 't> { | ExprKind::Len(inner) => { self.finalize_expr(inner) } + ExprKind::CloneIn { value, region } => { + self.finalize_expr(value); + self.finalize_expr(region); + } ExprKind::ArraySum { source, stages } | ExprKind::ArrayCount { source, stages } | ExprKind::ArrayMinMax { source, stages, .. } @@ -35598,7 +36594,11 @@ fn ast_loop_expr_flow( || else_flow.reaches_break, } } - K::Block(block) | K::Arena(block) | K::Unsafe(block) | K::TaskGroup(block) => { + K::Block(block) + | K::Arena(block) + | K::NamedArena { block, .. } + | K::Unsafe(block) + | K::TaskGroup(block) => { ast_loop_block_flow(block, accepted_breaks, loop_fallthrough) } K::StructLit { fields, .. } => { @@ -35896,7 +36896,12 @@ fn walk_expr(e: &ast::Expr, out: &mut std::collections::HashSet) { walk_expr(e, out); } } - K::Block(b) | K::Arena(b) | K::TaskGroup(b) | K::Unsafe(b) | K::Loop(b) => walk_block(b, out), + K::Block(b) + | K::Arena(b) + | K::NamedArena { block: b, .. } + | K::TaskGroup(b) + | K::Unsafe(b) + | K::Loop(b) => walk_block(b, out), K::StructLit { name, fields } => { if let Some(prefix) = path_module_prefix(name) { out.insert(prefix); @@ -36048,7 +37053,10 @@ pub fn print_kind(ty: Ty) -> Option { fn init_is_buffered_reader(e: &hir::Expr) -> bool { match &e.kind { hir::ExprKind::ReaderBuffered { .. } => true, - hir::ExprKind::Block(b) | hir::ExprKind::Arena(b) | hir::ExprKind::Unsafe(b) => { + hir::ExprKind::Block(b) + | hir::ExprKind::Arena(b) + | hir::ExprKind::NamedArena { block: b, .. } + | hir::ExprKind::Unsafe(b) => { b.value.as_ref().is_some_and(|v| init_is_buffered_reader(v)) } _ => false, @@ -36130,10 +37138,17 @@ fn ty_name(ty: Ty) -> String { Ty::Slice(s) => format!("slice<{}>", scalar_name(s)), Ty::Soa(id) => format!("soa"), Ty::DynArray(s) => format!("array<{}>", scalar_name(s)), + ty @ (Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => format!( + "array<{}>", + ty_name(ty.dyn_aggregate_array_element().expect("matched aggregate array").ty()) + ), Ty::DynResponseArray => "array".to_string(), Ty::Str => "str".to_string(), Ty::String => "string".to_string(), - Ty::ArenaHandle => "arena".to_string(), + Ty::ArenaHandle => "region".to_string(), Ty::Raw => "raw".to_string(), Ty::Resource(id) => format!("resource#{id}"), Ty::ResourceRef(id) => format!("resource_ref"), @@ -36144,7 +37159,14 @@ fn ty_name(ty: Ty) -> String { Ty::Writer => "writer".to_string(), Ty::Reader => "reader".to_string(), Ty::Buffer => "buffer".to_string(), - Ty::ArrayBuilder(s) => format!("array_builder<{}>", scalar_name(s)), + Ty::ArrayBuilder(elem) => format!("array_builder<{}>", scalar_name(elem)), + ty @ (Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..)) => format!( + "array_builder<{}>", + ty_name(ty.array_builder_element().expect("matched aggregate builder").ty()) + ), Ty::File => "file".to_string(), Ty::Rng => "rng".to_string(), Ty::Regex => "regex".to_string(), @@ -36445,6 +37467,24 @@ fn resolved_type_source_spelling( next, ) ), + ty @ (Ty::DynVecArray(..) + | Ty::DynMaskArray(..) + | Ty::DynFixedArray(..) + | Ty::DynFixedStructArray(..)) => format!( + "array<{}>", + resolved( + ty.dyn_aggregate_array_element().expect("matched aggregate array").ty(), + type_table, + struct_ids, + enum_ids, + struct_source_spellings, + enum_source_spellings, + tagged_types, + tuples, + fn_types, + next, + ) + ), Ty::DynResponseArray => "array".to_string(), Ty::Box(payload) => format!( "box<{}>", @@ -36506,7 +37546,7 @@ fn resolved_type_source_spelling( next, ) ), - Ty::ArenaHandle => "arena".to_string(), + Ty::ArenaHandle => "region".to_string(), Ty::Raw => "raw".to_string(), Ty::Builder => "builder".to_string(), Ty::Writer => "writer".to_string(), @@ -36514,8 +37554,26 @@ fn resolved_type_source_spelling( Ty::Buffer => "buffer".to_string(), Ty::ArrayBuilder(elem) => format!( "array_builder<{}>", - scalar( - elem, + resolved( + scalar_to_ty(elem), + type_table, + struct_ids, + enum_ids, + struct_source_spellings, + enum_source_spellings, + tagged_types, + tuples, + fn_types, + next, + ) + ), + ty @ (Ty::VecArrayBuilder(..) + | Ty::MaskArrayBuilder(..) + | Ty::FixedArrayBuilder(..) + | Ty::FixedStructArrayBuilder(..)) => format!( + "array_builder<{}>", + resolved( + ty.array_builder_element().expect("matched aggregate builder").ty(), type_table, struct_ids, enum_ids, @@ -37770,6 +38828,13 @@ fn resolve_type( // `raw` — an opaque raw byte pointer (`raw.alloc` yields one). Nameable so it can be a `let` // annotation / function parameter (holding a `raw` is safe; only `raw.*` ops need `unsafe`). "raw" => Ty::Raw, + "region" => { + if !args.is_empty() { + diags.error("region takes no type arguments".to_string(), span); + return Ty::Error; + } + Ty::ArenaHandle + } "resource_ref" => { let [owner] = args else { diags.error("resource_ref takes exactly one resource type argument".to_string(), span); @@ -37813,10 +38878,9 @@ fn resolve_type( } Ty::Buffer } - // `array_builder` (M12 A6) — a growable typed array builder that freezes into `array`. - // Element set v1 = **Copy scalars + `string`** (the settled set). A `str` view, struct, soa, - // or any Move handle is fail-closed rejected here (a clean sema error at the type argument), - // never type-checked then panicking downstream. + // `array_builder` — one owner type for the heap and explicit-region constructors. Type + // formation admits the union of both concrete element sets; the constructor selects and + // validates the allocation mode (`array_builder()` vs `array_builder(out)`). "array_builder" => { let inner = match args { [a] => resolve_type(a, cx, type_params, diags), @@ -37825,17 +38889,51 @@ fn resolve_type( return Ty::Error; } }; - match inner { - Ty::Error => Ty::Error, - Ty::Int(_) | Ty::Float(_) | Ty::Bool | Ty::Char | Ty::String => { - // ty_to_scalar is total on these — an Int/Float/Bool/Char/String all map. - Ty::ArrayBuilder(ty_to_scalar(inner).expect("scalar element")) + let normalized = match inner { + Ty::Error => return Ty::Error, + Ty::Option(payload) => Ty::Tagged(intern_tagged_type( + cx.tagged_types, + hir::TaggedType::Option(payload), + )), + Ty::Result(ok, err) => Ty::Tagged(intern_tagged_type( + cx.tagged_types, + hir::TaggedType::Result(ok, err), + )), + other => other, + }; + let Some(elem) = array_builder_elem(normalized) else { + diags.error( + format!( + "array_builder element must be a heap scalar/string or a concrete RegionPlain scalar, vector, mask, fixed array, view, Option, sum, or struct; got {}", + ty_name(normalized) + ), + span, + ); + return Ty::Error; + }; + match elem { + ArrayBuilderElem::Scalar( + Scalar::Int(_) + | Scalar::Float(_) + | Scalar::Bool + | Scalar::Char + | Scalar::String + | Scalar::Str + | Scalar::Slice(_) + | Scalar::Struct(_) + | Scalar::Enum(_) + | Scalar::Tagged(_), + ) => Ty::array_builder(elem), + ArrayBuilderElem::Aggregate(_) + if region_plain_type_ok(elem.ty(), cx.structs, cx.enums, cx.tagged_types) => + { + Ty::array_builder(elem) } - other => { + _ => { diags.error( format!( - "array_builder element must be a Copy scalar (int/float/bool/char) or `string`, got {} (str views, structs, and owned handles are not supported)", - ty_name(other) + "array_builder element must be a heap scalar/string or a concrete RegionPlain scalar, vector, mask, fixed array, view, Option, sum, or struct; got {}", + elem.name() ), span, ); @@ -38057,8 +39155,8 @@ fn resolve_type( } } } - // `array` — an owned, dynamic-length array (MMv2). Currently usable as a return - // type so a function can hand back a free-standing owned array. + // `array` — a dynamic-length array (MMv2). Scalar and struct elements use the existing + // owned representation; fixed-layout aggregate elements use the region-owned result type. "array" => { let inner = match args { [a] => resolve_type(a, cx, type_params, diags), @@ -38080,6 +39178,42 @@ fn resolve_type( Ty::Error } Ty::Struct(id) => Ty::DynStructArray(id, Layout::Aos), + Ty::Vec(elem, lanes) => { + Ty::dyn_aggregate_array(AggregateArrayElem::Vec(elem, lanes)) + } + Ty::Mask(elem, lanes) => { + Ty::dyn_aggregate_array(AggregateArrayElem::Mask(elem, lanes)) + } + Ty::Array(elem, length) + if region_plain_type_ok( + inner, + cx.structs, + cx.enums, + cx.tagged_types, + ) => + { + Ty::dyn_aggregate_array(AggregateArrayElem::FixedArray(elem, length)) + } + Ty::StructArray(id, length) + if region_plain_type_ok( + inner, + cx.structs, + cx.enums, + cx.tagged_types, + ) => + { + Ty::dyn_aggregate_array(AggregateArrayElem::FixedStructArray(id, length)) + } + Ty::Array(..) | Ty::StructArray(..) => { + diags.error( + format!( + "array<{}> requires a recursively RegionPlain fixed-array element", + ty_name(inner) + ), + span, + ); + Ty::Error + } _ => match collection_scalar_arg( inner, "array element", @@ -38385,6 +39519,7 @@ fn is_field_ok(ty: Ty, tagged_types: &[hir::TaggedType]) -> bool { // `choices: array` shape. The complete struct table and the direct // `array` exception are enforced at declaration (pass 0b-2); here we admit the // array shape, including the recursively droppable `array` form. + ty if ty.is_dyn_aggregate_array() => {} Ty::DynArray(_) | Ty::DynStructArray(..) => {} _ => return false, } @@ -40328,7 +41463,11 @@ fn exit_branch(flag: bool) -> i64 { #[test] fn region_lattice_outlives() { - // Static ⊐ Frame ⊐ Arena(1) ⊐ Arena(2): longer-lived outlives shorter-lived. + // Static ⊐ Caller(_) ⊐ Frame ⊐ Arena(1) ⊐ Arena(2): longer-lived outlives shorter-lived. + assert!(Region::Static.outlives(Region::Caller(0))); + assert!(Region::Caller(0).outlives(Region::Frame)); + assert!(Region::Caller(0).outlives(Region::Arena(1))); + assert!(Region::Caller(0).outlives(Region::Caller(1))); assert!(Region::Static.outlives(Region::Frame)); assert!(Region::Static.outlives(Region::Arena(1))); assert!(Region::Frame.outlives(Region::Arena(1))); @@ -40336,6 +41475,8 @@ fn exit_branch(flag: bool) -> i64 { assert!(Region::Static.outlives(Region::Static)); // …and not the reverse. assert!(!Region::Frame.outlives(Region::Static)); + assert!(!Region::Frame.outlives(Region::Caller(0))); + assert!(!Region::Arena(1).outlives(Region::Caller(0))); assert!(!Region::Arena(1).outlives(Region::Frame)); assert!(!Region::Arena(2).outlives(Region::Arena(1))); // `arena(0)` is the leaked / process-lifetime case → Static; deeper = shorter-lived. @@ -40344,6 +41485,9 @@ fn exit_branch(flag: bool) -> i64 { // `shorter` picks the shorter-lived (the one that bounds a view over both). assert_eq!(Region::Static.shorter(Region::Arena(1)), Region::Arena(1)); assert_eq!(Region::Arena(2).shorter(Region::Frame), Region::Arena(2)); + assert!(Region::Caller(0).is_region_owned()); + assert!(Region::Caller(0).is_returnable()); + assert!(!Region::Frame.is_returnable()); } #[test] diff --git a/crates/align_sema/src/replay_clone.rs b/crates/align_sema/src/replay_clone.rs index 30b82e9c..464dc640 100644 --- a/crates/align_sema/src/replay_clone.rs +++ b/crates/align_sema/src/replay_clone.rs @@ -328,7 +328,6 @@ fn clone_expr_kind(clones: &mut ChildValues, kind: &ExprKind) -> Option Option ExprKind::Arena(clones.block()?), + ExprKind::NamedArena { local, .. } => { + ExprKind::NamedArena { local: *local, block: clones.block()? } + } ExprKind::Unsafe(_) => ExprKind::Unsafe(clones.block()?), ExprKind::RawAlloc(expr) => ExprKind::RawAlloc(boxed!(expr)), ExprKind::RawFree(expr) => ExprKind::RawFree(boxed!(expr)), @@ -485,6 +487,10 @@ fn clone_expr_kind(clones: &mut ChildValues, kind: &ExprKind) -> Option ExprKind::BoxGet(boxed!(expr)), ExprKind::BoxClone(expr) => ExprKind::BoxClone(boxed!(expr)), ExprKind::StrClone(expr) => ExprKind::StrClone(boxed!(expr)), + ExprKind::CloneIn { value, region } => ExprKind::CloneIn { + value: boxed!(value), + region: boxed!(region), + }, ExprKind::StrPredicate { kind, haystack, @@ -499,6 +505,10 @@ fn clone_expr_kind(clones: &mut ChildValues, kind: &ExprKind) -> Option ExprKind::StrBorrow(boxed!(expr)), + ExprKind::ArrayBuilderNew { elem, region } => ExprKind::ArrayBuilderNew { + elem: *elem, + region: take_optional_boxed_expr(clones, region.is_some())?, + }, ExprKind::BuilderNew { capacity } => ExprKind::BuilderNew { capacity: take_optional_boxed_expr(clones, capacity.is_some())?, }, @@ -1720,7 +1730,6 @@ fn drop_expr_kind(kind: ExprKind, work: &mut Vec) { | ExprKind::ArrayDictEncode { .. } | ExprKind::ReaderStdin | ExprKind::WriterStd { .. } - | ExprKind::ArrayBuilderNew { .. } | ExprKind::TimeNow | ExprKind::TimeInstant | ExprKind::ProcessCpuCount @@ -1751,6 +1760,10 @@ fn drop_expr_kind(kind: ExprKind, work: &mut Vec) { | ExprKind::ArrayToSlice(expr) | ExprKind::Len(expr) | ExprKind::ArrayBuilderBuild(expr) => one!(expr), + ExprKind::CloneIn { value, region } => { + one!(value); + one!(region); + } ExprKind::Binary { lhs, rhs, .. } | ExprKind::IntArith { lhs, rhs, .. } | ExprKind::ResultMapErr { @@ -2028,6 +2041,7 @@ fn drop_expr_kind(kind: ExprKind, work: &mut Vec) { ExprKind::TaskGroup(block) | ExprKind::Block(block) | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } | ExprKind::Unsafe(block) => block!(block), ExprKind::Loop { body, .. } => block!(body), ExprKind::Match { scrutinee, arms } => { @@ -2118,6 +2132,7 @@ fn drop_expr_kind(kind: ExprKind, work: &mut Vec) { one!(recv); one!(index); } + ExprKind::ArrayBuilderNew { region, .. } => optional!(region), ExprKind::BuilderNew { capacity } => optional!(capacity), ExprKind::SliceRange { recv, start, end } => { one!(recv); diff --git a/crates/align_sema/src/task_wait.rs b/crates/align_sema/src/task_wait.rs index 68b9d2a2..cd4ced90 100644 --- a/crates/align_sema/src/task_wait.rs +++ b/crates/align_sema/src/task_wait.rs @@ -1180,7 +1180,10 @@ impl<'a> Analyzer<'a> { steps: 0, }); } - ExprKind::Block(block) | ExprKind::Arena(block) | ExprKind::Unsafe(block) => { + ExprKind::Block(block) + | ExprKind::Arena(block) + | ExprKind::NamedArena { block, .. } + | ExprKind::Unsafe(block) => { work.push(ReplayWork::EvalBlock { block, state, diff --git a/docs/impl/17-library-boundary-prerequisites.md b/docs/impl/17-library-boundary-prerequisites.md index b4d81a07..2c63c4b2 100644 --- a/docs/impl/17-library-boundary-prerequisites.md +++ b/docs/impl/17-library-boundary-prerequisites.md @@ -1831,7 +1831,7 @@ The following producer sets are exact: | tuple element | Exactly integer, float, bool, char, `Str`, `String`, `DynArray`, or `DynStructArray`; order is significant and duplicate tuple element lists are one interned identity. A Move tuple Drop recursively dispatches each owned element through its concrete type, including deep `array` and `array` elements. | one positive per kind, deep tuple-drop owner coverage, and all other graph-valid scalar/composite negatives | | `Option`/`Result` payload | `scalar_arg(..., allow_param=true)`: `payload-scalar`, with nested `Option`/`Result` interned as `Tagged`; abstract `Param` is template-only. | every payload kind, nested tagged values, and excluded buffer/builder/header/composite twins | | box type argument | `scalar_arg(..., allow_param=false)`, then reject `Struct`, `Enum`, every `Scalar::is_move`, and `Str`. The admitted type-formation remainder is integer, float, bool, char, unit, primitive `Slice`, SoA, JSON document, and a concrete non-Move `Tagged` value. This is deliberately broader than value construction: `heap.new` additionally rejects `Slice`, whose borrowed view cannot be stored as an owned box payload. | one type-formation positive for every admitted remainder including `Slice`/SoA/JSON/tagged; `heap.new(Slice)` body negative; struct/enum/owned/`Str`/parameter negatives | -| slice/dynamic-array type argument | `collection-scalar`. A dynamic struct array instead records its exact struct id and rejects an over-aligned element. Every owned I/O handle, including `File`, is rejected because the generic array Drop path cannot release one handle per element. SoA separately requires a non-empty struct containing only integer, float, bool, char, or `Str` fields. `ArrayBuilder` accepts only integer, float, bool, char, or `String`. | one positive per type-argument family including `Fn`; every explicitly excluded handle/File/nested/over-aligned/SoA-field/builder negative | +| slice/dynamic-array/builder type argument | `collection-scalar` for slices and established scalar dynamic arrays. A dynamic struct array instead records its exact struct id and rejects an over-aligned element. Every owned I/O handle, including `File`, is rejected because the generic array Drop path cannot release one handle per element. SoA separately requires a non-empty struct containing only integer, float, bool, char, or `Str` fields. `ArrayBuilder` records either an exact scalar descriptor or one of the closed vector, mask, fixed-scalar-array, and fixed-struct-array aggregate descriptors; constructor validation narrows the heap form to primitive Copy scalars/String and the explicit-region form to recursively `RegionPlain` concrete types. | one positive per type-argument family including `Fn`; every explicitly excluded handle/File/nested/over-aligned/SoA-field/builder negative; every builder descriptor positive plus invalid lane, length, scalar, and nominal-id twins | | fixed-array literal element | Body-owned, not am-p-owned. A fixed struct array admits an over-aligned struct and records the padded/aligned slot contract. A scalar literal rejects every owned handle including `File`, every slice-bearing non-struct, and a Move enum; all elements have one checked type, `ArrayLit.elem` matches it, and the length fits the stored type. | over-aligned fixed-struct positive; `File` type-formation-positive/literal-negative twin; handle/slice/Move-enum/type/length/pooled-state matrix in am-b2 | | vector and mask element | Integer or float with exactly 2, 4, 8, or 16 lanes. | every width/lane endpoint and bool/char/aggregate negatives | | annotated `FnTy` type positions | Each parameter is `ty-scalar`. The return is any graph-valid non-`Error` type currently produced by `resolve_type`; the body/call validator separately requires each actual callable origin to satisfy `fn-scalar` parameters and a `fn-scalar`/`Result` return. Mode cardinality/class and summaries belong only to am-h. Imported effect transport belongs to am-h; body-correlated effect cells and parallel eligibility belong only to am-b4. | slice- and buffer-parameter annotation positives, actual fn-value slice negative, Result-return handler, and one type-position mutation per branch | @@ -2267,7 +2267,7 @@ by am-g-t encode. Definition references, field ordinals/bases, vector lanes, fix summary indices, counts, and text lengths are `u32`. No native `usize`, signed integer, enum memory layout, padding, or host endianness enters the bytes. -`CanonicalTy` is `version=1:u8 || node_count:u32 || nodes || root_type`. Nodes are assigned ordinals +`CanonicalTy` is `version=3:u8 || node_count:u32 || nodes || root_type`. Nodes are assigned ordinals by first visit in a depth-first walk from the root; struct fields, enum variants/payloads, tuple elements, tagged payloads, and function parameters are visited in stored declaration order. Repeated and recursive references emit the first assigned `u32` ordinal. The node tags and payloads @@ -2279,7 +2279,8 @@ are: | 1 | enum: source-name, variant count, then each variant name, `field_base:u32`, payload count, and payload scalars | | 2 | tuple: element count and scalar elements | | 3 | tagged: `0 || option scalar` or `1 || ok scalar || err scalar` | -| 4 | function: parameter count, each mode and scalar, return type, borrow summary, region summary; no effect or raw fn-table id | +| 4 | function: parameter count, each mode and scalar, return type, borrow summary, region summary, cleanup ABI; no effect or raw fn-table id | +| 5 | resource: source name, internal name, declaring module, Drop hook, Drop thunk, representation version, 16-byte Drop-ABI fingerprint, and generic arity | Struct/enum nodes include nominal `source_name` and their complete reachable shape. They exclude origin-aware private `name`. The fingerprint is nominal plus structural: different public nominals @@ -2301,7 +2302,7 @@ ordinals on first visit. Decoding rebuilds the same partition, rejects two seria equivalence class as `DuplicateMember`, and rejects any node/ordinal order different from the depth-first first-visit re-encoding as `NonCanonicalOrder`. -The `root_type` tags `0..=56`, in exact order, are: +The current `root_type` tags `0..=59`, in exact order, are: ```text Int Float Bool Char Option Result Tagged Box Array Vec Mask StructArray DynStructArray Slice Soa @@ -2309,23 +2310,30 @@ DynSliceArray DynArray DynResponseArray Str String ArenaHandle Raw Builder Write ArrayBuilder StrFinder File Rng Regex Captures CliCommand CliParsed TcpConn TcpListener UdpSocket Child Command RunOutput HttpRequest HttpResponse HttpClient HttpServer HttpRequestCtx ResponseBuilder HttpStream HttpHeaders JsonDoc JsonScanner Struct Tuple Fn Enum Task DictEncoded Unit +Resource ResourceRef DynAggregateArray ``` `Int` is `signed:bool || bits:u8`; `Float` is `bits:u8`. `Bool`, `Char`, the closed handles, `Str`, `String`, `Raw`, and `Unit` have no payload. `Option`, `Result`, `Box`, `Array`, `Vec`, `Mask`, -`Slice`, `DynArray`, `ArrayBuilder`, and `Task` encode their scalar(s), then any `u32` length/lane. +`Slice`, `DynArray`, and `Task` encode their scalar(s), then any `u32` length/lane. `DynSliceArray` encodes a primitive scalar. `StructArray` encodes a struct-node reference and length; `DynStructArray` encodes a struct-node reference and layout (`0=Aos`, `1=Soa`); `Soa`, `JsonScanner`, and `Struct` encode a struct-node reference. `Tagged`, `Tuple`, `Fn`, and `Enum` encode the matching node reference. `DictEncoded` encodes a struct-node reference then field -ordinal. `DynResponseArray` has no payload. +ordinal. `Resource` and `ResourceRef` encode a resource-node reference. `DynResponseArray` has no +payload. `ArrayBuilder` encodes `0 || scalar` or `1 || aggregate-element`; `DynAggregateArray` +encodes an aggregate element directly. Aggregate-element tags are exactly `0=Vec`, `1=Mask`, +`2=FixedArray`, and `3=FixedStructArray`; vector/mask records encode scalar then lanes, fixed scalar +arrays encode scalar then length, and fixed struct arrays encode a struct-node reference then +length. All lengths and lane counts are `u32`. -Valid scalar tags `0..=33`, in order, are: +Valid scalar tags `0..=35`, in order, are: ```text Int Float Bool Char Unit Struct String DynArray DynStructArray DynResponseArray Str Slice Enum Tagged Soa JsonDoc Reader Writer Buffer Regex Captures CliParsed TcpConn TcpListener UdpSocket -Child File HttpResponse HttpServer HttpRequestCtx ResponseBuilder HttpStream RunOutput Fn +Child File HttpResponse HttpServer HttpRequestCtx ResponseBuilder HttpStream RunOutput Fn Resource +ResourceRef ``` Scalar `Int`/`Float` use the same width payloads; `Struct`/`DynStructArray`/`Soa` use a struct-node @@ -2468,7 +2476,8 @@ The exact `RuntimeKey` set is: alloc alloc_size_fail arena_alloc arena_begin arena_end array_builder_append array_builder_build array_builder_build_stack array_builder_free array_builder_free_stack array_builder_free_strings array_builder_free_strings_stack -array_builder_init_stack array_builder_new array_builder_push array_builder_push_str +array_builder_init_stack array_builder_new array_builder_new_in array_builder_push +array_builder_push_bytes array_builder_push_str base64_decode base64_encode base64url_decode base64url_encode bounds_fail buffer_append buffer_bytes buffer_free buffer_len buffer_new buffer_put builder_finish builder_finish_stack builder_free builder_free_stack builder_init_stack @@ -2526,10 +2535,10 @@ and four distinct `par-map-probe` exports are verification-only runtime-fixture names remain ordinary program/extern/export spellings. `task-group-probe` adds no unmangled export. The four AEAD cross-product symbols are ordinary keys rather than a codegen-side string match. -[`20-runtime-abi-ledger.md`](20-runtime-abi-ledger.md) owns all 281 keyed symbol/type/attribute +[`20-runtime-abi-ledger.md`](20-runtime-abi-ledger.md) owns all 283 keyed symbol/type/attribute records, the five always-built unkeyed records, and the eight verification-only probe records. -The compiler registry is fixed at 286 base records with no feature or ambient input. The eight -probe rows extend only the verification-time maximum runtime-export table to 294; they are never a +The compiler registry is fixed at 288 base records with no feature or ambient input. The eight +probe rows extend only the verification-time maximum runtime-export table to 296; they are never a RuntimeKey, callable declaration, collision reservation, or compatible-extern reuse target. Probe-feature runtime builds never link user artifacts. Runtime feature selection affects only export-set verification and changes no source acceptance or MIR/interface/artifact/cache identity. @@ -3303,9 +3312,9 @@ The am-c author-side construction/consumption inventory is exact for the current | Class | Producers that must change together | Consumers that must change together | |---|---|---| | program | validated stored functions, per-unit imports, extern declarations, HIR `Call`, `FnValue`, lifted `Closure`, every scalar/AoS pipeline stage, `reduce`/`any`/`all`, `scan`, `partition`, `sort_by_key`, and parallel terminal/stage callables | MIR print/debug, work-weight scan, tagged-type remap/embedded-type scan, LLVM definition/import/extern declaration registry, direct-call lowering, extern coercion, function-value and closure thunk discovery/lowering, parallel signature checks, whole/per-unit symbol/linkage, explicit exports, and main wrapping | -| runtime | the 15 compiler-produced direct semantic keys split into eight specialized choices (`Print`, `PrintStr`, `PrintBool`, `PrintChar`, `PrintF32`, `PrintF64`, `Hash64`, `Hash128`) and seven generic legacy-map calls (`ProcessExit`, `ProcessAbort`, `DivFail`, `BoundsFail`, `RangeFail`, `Utf8BoundaryFail`, `LenMismatchFail`); every other dedicated MIR native node remains an exact `RuntimeKey` consumer in LLVM lowering | the fixed 281 keyed declarations and their typed dedicated consumers; the legacy alias seam populated from those declarations for unchanged seven-key generic direct calls and deferred program/generated consumers; two typed unkeyed wrapper handles; contract attributes, ThinLTO guarded rows, runtime export verification, compatible-extern reuse, and allocation/cleanup calls; `AllocSizeFail` is dedicated, while `error(code)` is not a RuntimeKey and lowers to the existing MIR identity value instead of surviving as a call | +| runtime | the 15 compiler-produced direct semantic keys split into eight specialized choices (`Print`, `PrintStr`, `PrintBool`, `PrintChar`, `PrintF32`, `PrintF64`, `Hash64`, `Hash128`) and seven generic legacy-map calls (`ProcessExit`, `ProcessAbort`, `DivFail`, `BoundsFail`, `RangeFail`, `Utf8BoundaryFail`, `LenMismatchFail`); every other dedicated MIR native node remains an exact `RuntimeKey` consumer in LLVM lowering | the fixed 283 keyed declarations and their typed dedicated consumers; the legacy alias seam populated from those declarations for unchanged seven-key generic direct calls and deferred program/generated consumers; two typed unkeyed wrapper handles; contract attributes, ThinLTO guarded rows, runtime export verification, compatible-extern reuse, and allocation/cleanup calls; `AllocSizeFail` is dedicated, while `error(code)` is not a RuntimeKey and lowers to the existing MIR identity value instead of surviving as a call | | generated | every distinct `FnAddr`, capturing `Closure`, `SpawnTask` result/fallibility pair, `ParMapParallel` materialize/filter count/filter scatter request, and `ParMapReduce` request | pre-body collection/validation, canonical byte sorting/deduplication, global-name reservation/probing, helper declaration/body emission, call-site pointer selection, debug names, and malformed-before-publication rejection | -| symbol/cache | stored and imported Align definitions, extern C declarations, explicit exports, direct/wrapped main, 286 fixed native base rows, and generated requests | encoded `align_fn$$` definition/import lookup, exact extern/native reuse, external-identity collision rejection, deterministic generated probing, ThinLTO internalization roots, structural MIR `impl_hash`, compiler-build cache identity, and unchanged interface/source-ABI hashes | +| symbol/cache | stored and imported Align definitions, extern C declarations, explicit exports, direct/wrapped main, 288 fixed native base rows, and generated requests | encoded `align_fn$$` definition/import lookup, exact extern/native reuse, external-identity collision rejection, deterministic generated probing, ThinLTO internalization roots, structural MIR `impl_hash`, compiler-build cache identity, and unchanged interface/source-ABI hashes | The callable applicability matrix is exhaustive; “unavailable” is an invalid hand-built MIR cell, not a missing positive owner: @@ -3335,8 +3344,8 @@ The parallel mode/stage/collection matrix is exact: | contains `Filter`, `FilterStrContains`, or `FilterField`, optionally interleaved with `Map`/`Project` | unavailable | unavailable | valid individual record | valid individual record | exactly one otherwise-identical count/scatter pair after dedupe | | any unknown stage, invalid ordinal/type/signature, invalid work weight, or other mode/stage combination | reject | reject | reject | reject | reject before reservation | -Canonical-type owners cross every root tag `0..=56`, scalar tag `0..=33`, primitive tag `0..=5`, -definition tag `0..=4`, parameter mode, summary state, equivalence/non-equivalence class, repeated and +Canonical-type owners cross every current root tag `0..=59`, scalar tag `0..=35`, primitive tag `0..=5`, +definition tag `0..=5`, parameter mode, summary state, equivalence/non-equivalence class, repeated and recursive reference, shallow/deep graph, and encode/decode direction. Malformed owners mutate one version, tag, boolean, width, count, UTF-8/NUL, reference/order, duplicate member/equivalence class, empty nominal source, member identifier byte, alignment presence/power/range, enum first/later @@ -4257,6 +4266,46 @@ Acceptance: - exactly one compacting element pass occurs; - resources/owned heap fields receive compile diagnostics. +The F-B implementation closure matrix is authoritative while L4 and L6 are built. It implements +the already settled region contract without introducing an ambient allocator or a database-named +compiler path. Symbolic generic `RegionPlain` bounds remain owned by L7; F-B closes the concrete +recursive classifier and every runtime/materialization path that L7 will later select after +monomorphization. + +The concrete builder element record is one non-recursive compiler descriptor, not a widening of +the general `Scalar` payload class. It has exactly these shapes: `Scalar(Scalar)`, +`Vec(Scalar, lanes)`, `Mask(Scalar, lanes)`, `FixedArray(Scalar, length)`, and +`FixedStructArray(struct_id, length)`. The last four shapes freeze to one dedicated dynamic +aggregate-array type carrying the same descriptor; scalar and struct elements retain the existing +`DynArray` and AoS `DynStructArray` result types. Formation converts the resolved concrete `Ty` once, +and push, build, indexing, type display, Drop/region analysis, HIR validation, MIR remapping, +interface reconstruction, and LLVM layout consume that same record. The descriptor preserves +nominal struct/tagged ids for canonical remapping and is rejected before MIR when a lane scalar, +width, length, struct id, or result correlation is malformed. Direct source formation closes the +currently spellable vector/mask shapes; fixed-array descriptors also close monomorphized and +hand-built-HIR consumers without inventing a second fixed-array surface spelling before L7. + +| Closure cell | Required implementation closure | Owner evidence | +|---|---|---| +| syntax, binding, and type formation | Parse both `arena {}` and `arena name {}`; bind `name` as the builtin Copy `region` type only for the block; reject construction, mutation, shadowing, storage, unsupported aggregates, FFI, task transfer, and return | parser/formatter round trips; named/anonymous scoping positives; formation and escape diagnostic matrix | +| exact region identity | Give every named arena and `region` parameter a stable semantic identity; inside a callee keep each caller-owned region or borrowed builder symbolic and distinct from both `Static` and callee-local frame/arena storage, then discharge relationships between distinct symbolic parameters at each concrete call site; preserve returned and captured region ownership through direct/imported/indirect calls, function-value target joins, moved function values, captures, and monomorphization without collapsing distinct caller regions | sema provenance owners for direct, branch, loop, `?`, closure, imported, and function-value return paths; nested-callee-arena rejection for incoming regions/builders; canonical interface and whole/per-unit parity | +| explicit allocation and `clone_in` | Lower every region allocation with the exact capability operand; `clone_in` copies `str`/`bytes` backing storage and recursively copies view-bearing fields of a `RegionPlain` struct into that region, returns a value tied to `out`, validates each view size before allocation, and performs no heap allocation | exact HIR/MIR operand assertions; scalar/bytes/struct runtime content and lifetime positives; wrong-region, owned-field, and post-region escape negatives; LLVM call inspection | +| cleanup and exits | Begin each arena once and end it once on every returning completion path, including normal completion, return, `?`, branch, and loop exit; allocation/overflow hard errors remain process-terminating; named and anonymous cleanup are byte-identical apart from storing the bound handle; a borrowed incoming region is never ended by its callee | named/anonymous LLVM cleanup-shape comparison plus the existing arena completion-path owners; whole/per-unit executable parity | +| concrete `RegionPlain` classification | Recursively accept scalars, `Option`, fixed vectors/masks, fixed arrays, plain structs, and region-valid `str`/`bytes` views; reject resources, refs, raw, functions, builders, independently owned heap fields, and recursive unsupported shapes before execution. Convert every admitted top-level shape to the one exact builder-element descriptor before HIR; never truncate it through `Scalar` | table-driven classifier tests for nested positive/negative shapes and every descriptor discriminator; deterministic first-invalid-field diagnostics; source `vec`/`mask` formation positives; malformed HIR descriptor/result-correlation rejection | +| region builder formation and ownership | `array_builder(out)` records its exact region and concrete element descriptor, is Move and bound to one mutable local, may be passed only as `borrow mut`, and cannot be stored, returned, captured, moved into a task, or built through an alias. A `borrow mut` builder parameter keeps symbolic caller provenance and the heap-only, region-only, or constructor-dependent allocation bound implied by `T`, so callee-local frame/arena views cannot be retained and a nested helper cannot assume heap ownership. Each concrete call over a view-bearing builder conservatively checks and retains every view-bearing argument root in the caller, so direct and imported helpers cannot erase newly stored provenance. Builder-parameter function values remain outside the existing scalar-only first-class signature surface. | constructor/receiver/mode diagnostic matrix across scalar/vector/mask/fixed-array descriptors; direct/imported Pure helper push positives; callee-local nested-arena, nested-helper allocation-mode, and call-site wrong-region negatives; move/alias/capture/store/build negatives | +| chunked growth and push provenance | Builder headers and growth chunks allocate only from the selected arena; scalar/Option/plain-struct pushes copy exactly one initialized element; pushed views retain their source provenance, so a current-row view cannot survive `next`, while `clone_in(out)` can | runtime allocation counters and chunk-boundary data checks; sema current-row/clone provenance tests; exact element-layout MIR/LLVM assertions | +| compacting build | `build` consumes the owned builder, allocates one final contiguous result in the same region, performs exactly one element compaction pass, invalidates the builder, and returns the correctly typed region-tied array | runtime pass counter and 0/1/multi-chunk result tests; move/use-after-build checks; MIR source-nulling and returned-region assertions | +| failure and early cleanup | Invalid native layouts and overflow are rejected before allocation or copy; allocation exhaustion follows the existing hard-error arena contract; early return, `?`, branch, loop, and unfinished-builder exits leave no independently owned storage, never end a borrowed region, and let the enclosing arena reclaim all chunks | invalid-layout/overflow runtime owners plus the existing arena MIR cleanup-path suite; nested named arenas and helper-call coverage | +| interfaces, ABI, and cache identity | Serialize `region` parameters, exact return-region summaries, region-builder forms, and every concrete builder/dynamic-result element discriminator canonically; remap embedded nominal ids once; reject malformed metadata and builder/result mismatches before MIR/codegen; keep whole-program and per-unit ABI byte-equivalent | interface codec/hash goldens and corruption tests for each descriptor; declaration-order determinism; whole/per-unit object/link/run parity | +| end-to-end resource promises | Materialize scalar, Option, vector, mask, and plain-struct arrays through ordinary functions, including recursively plain fields and fixed-array append sources; prove no heap calls in the region form and one compacting element pass without weakening anonymous arena behavior | focused F-B driver suite, applicable runtime/interface/MIR owners, LLVM IR inspection, allocation/pass-count measurement, `scripts/test-pr.sh`, and applicable Clippy | + +F-B is intentionally one consumer-complete capability even when it exceeds roughly 1,000 +hand-written changed lines. Splitting named-region formation from its first allocator consumer would +publish a dormant capability; splitting the builder runtime from provenance would allow accepted +views to dangle; and splitting compacting build from cleanup would leave no safe, usable result. +Intermediate commits therefore remain compiling owner-backed checkpoints on one branch rather than +publishable partial region semantics. + ### L7 — nested generic package APIs and `RegionPlain` bound Scope: diff --git a/docs/impl/19-hir-validation-ledger.md b/docs/impl/19-hir-validation-ledger.md index 79cdc1d1..58932ab7 100644 --- a/docs/impl/19-hir-validation-ledger.md +++ b/docs/impl/19-hir-validation-ledger.md @@ -834,10 +834,10 @@ merely because its `Ty` matches. | `BytesRead` | `env[be]`; `child[bytes,offset]`; `bytes,i64; result exact stored read scalar in {i8/u8/i16/u16/i32/u32/i64/u64/f32/f64}; be must be false for one-byte widths; borrowed bounds-checked read; Pure`. | | `BufferPut` | `env[be]`; `child[buffer,value]`; `SourceMutLocal(Buffer,buffer); value exact supported binary scalar; be false for one-byte widths; result Unit; buffer mutated; Pure`. | | `BufferAppend` | `env[]; child[buffer,data]`; `SourceMutLocal(Buffer,buffer),byte-view; result Unit; data borrowed, buffer mutated; Pure`. | -| `ArrayBuilderNew` | `env[elem]`: exact admitted Copy scalar or String builder element. `child[]; result ArrayBuilder(elem); new owned allocation; Pure`. | -| `ArrayBuilderPush` | `env[moves_value]`; `child[builder,value]`; `SourceMutLocal(ArrayBuilder(elem),builder), value scalar_to_ty(elem); moves_value iff elem==String; result Unit; String consumed, Copy value borrowed, builder mutated; Pure`. | -| `ArrayBuilderAppend` | `env[]; child[builder,data]`; `SourceMutLocal(ArrayBuilder(copy elem),builder), data Slice(elem); result Unit; data borrowed, builder mutated; Pure`. | -| `ArrayBuilderBuild` | `env[]; child[builder]`; `ArrayBuilder(elem), consume-any; result DynArray(elem); transfer the complete producer-valid builder buffer once; Pure`. | +| `ArrayBuilderNew` | `env[elem]`: exact nonrecursive descriptor `Scalar(S)` or `Aggregate(Vec(S,N) | Mask(S,N) | FixedArray(S,N) | FixedStructArray(id,N))`. The heap form admits only primitive Copy scalars or String; the region form requires the descriptor's concrete type to be recursively `RegionPlain`. `child[region?]`; result `ArrayBuilder(elem)`; new owned heap allocation or explicitly region-owned allocation; Pure. | +| `ArrayBuilderPush` | `env[moves_value]`; `child[builder,value]`; `SourceMutLocal(ArrayBuilder(elem),builder), value exact `elem.ty()`; `moves_value` iff `elem == Scalar(String)`; result Unit; String consumed, every RegionPlain value copied with provenance, builder mutated; Pure. | +| `ArrayBuilderAppend` | `env[]; child[builder,data]`; descriptor must be `Scalar(copy elem)`, `SourceMutLocal(ArrayBuilder(elem),builder), data Slice(elem)`; result Unit; data borrowed, builder mutated; Pure. Aggregate descriptors use `push`. | +| `ArrayBuilderBuild` | `env[]; child[builder]`; `ArrayBuilder(elem)`, consume-any; result `DynStructArray(id,Aos)` for `Scalar(Struct(id))`, `DynArray(S)` for every other scalar descriptor, or `DynAggregateArray(elem)` for an aggregate descriptor; transfer the complete producer-valid builder buffer once; Pure. | | `FsWriteFile` | `env[builder]`; `child[path,data]`; `path Str; builder=false requires byte-view, true requires Builder; result ERR(Unit); both borrowed; Impure`. | | `FsExists` | `env[]; child[path]`; `Str; result Bool; borrowed; Impure`. | | `FsRemove` | `env[]; child[path]`; `Str; result ERR(Unit); borrowed; Impure`. | diff --git a/docs/impl/20-runtime-abi-ledger.md b/docs/impl/20-runtime-abi-ledger.md index 73d787d2..c9bbdb3a 100644 --- a/docs/impl/20-runtime-abi-ledger.md +++ b/docs/impl/20-runtime-abi-ledger.md @@ -9,14 +9,16 @@ visible runtime definitions that occupy link identities. The keyed surface is generated from a trivial valid program; the complete base and `alloc-count` surfaces are independently compared with the Rust runtime exports. -Am-c1 has 281 `RuntimeKey` variants and a one-to-one native-symbol record. Four +The F-B region materialization capability has 283 `RuntimeKey` variants and a +one-to-one native-symbol record. Relative to Am-c1, it adds +`ArrayBuilderNewIn` and `ArrayBuilderPushBytes`; the four AEAD symbols that were previously selected from `AeadCipher × AeadDir` become ordinary typed keys; they may no longer bypass the registry. Five always-built runtime records have no `RuntimeKey` and instead use the five-variant `UnkeyedRuntimeKey`: the two main-wrapper callees `align_rt_report_error` and `align_rt_args_build`, plus the runtime-internal `align_rt_arena_reset`, `align_rt_realloc`, and -`align_rt_http_serialize`. The base native registry therefore has 286 records. +`align_rt_http_serialize`. The base native registry therefore has 288 records. The explicit `alloc-count` runtime feature may expose four test/benchmark-only counter definitions. `par-map-probe` may expose four more: `void @align_rt_test_par_map_force_caller(i32)`, @@ -25,10 +27,10 @@ test/benchmark-only counter definitions. `par-map-probe` may expose four more: `i64 @align_rt_test_par_map_workers()`. `task-group-probe` changes internal Rust state only and adds no unmangled native export. -The compiler-visible native registry is always exactly the 286 base records. +The compiler-visible native registry is always exactly the 288 base records. There is no target option, environment variable, Cargo feature, linked-runtime inspection, or other ambient input that changes it. The eight optional probe -records extend only the verification-time maximum runtime-export table to 294. +records extend only the verification-time maximum runtime-export table to 296. They never gain a `RuntimeKey`, callable/declaration policy, collision reservation, or compatible-extern reuse. Their spellings remain ordinary program/extern/export identities in a normal build. Probe-feature runtime @@ -111,7 +113,7 @@ from those bodies. `align_rt_str_cmp` is not guarded and always keeps A01. | A42 | `noalias ptr @SYM() {nofree nounwind}` | `align_rt_arena_begin`, `align_rt_tg_begin` | | A43 | `noalias ptr @SYM(i64) {nofree nounwind}` | `align_rt_alloc`, `align_rt_array_builder_new` | | A44 | `noalias ptr @SYM(ptr, i64) {nofree nounwind}` | `align_rt_str_finder_new`, `align_rt_builder_new` | -| A45 | `noalias ptr @SYM(ptr, i64, i64) {nounwind}` | `align_rt_arena_alloc`, `align_rt_tg_alloc` | +| A45 | `noalias ptr @SYM(ptr, i64, i64) {nounwind}` | `align_rt_arena_alloc`, `align_rt_array_builder_new_in`, `align_rt_tg_alloc` | | A46 | `noalias ptr @SYM(ptr, ptr, i64, i64, i64, i64, ptr)` | `align_rt_par_map` | | A47 | `ptr @SYM()` | `align_rt_io_reader_stdin`, `align_rt_http_client_new` | | A48 | `ptr @SYM(i32, i32)` | `align_rt_io_writer_std` | @@ -138,7 +140,7 @@ from those bodies. `align_rt_str_cmp` is not guarded and always keeps A01. | A69 | `void @SYM(ptr, i64, i64, ptr)` | `align_rt_json_doc_at` | | A70 | `void @SYM(ptr, i64, ptr, i64, ptr)` | `align_rt_json_doc_get`, `align_rt_dict_lookup` | | A71 | `void @SYM(ptr, i64, ptr, ptr)` | `align_rt_json_doc_elems` | -| A72 | `void @SYM(ptr, ptr)` | `align_rt_buffer_bytes` | +| A72 | `void @SYM(ptr, ptr)` | `align_rt_array_builder_push_bytes`, `align_rt_buffer_bytes` | | A73 | `void @SYM(ptr, ptr, i64)` | `align_rt_builder_write`, `align_rt_builder_write_json_str`, `align_rt_command_cwd`, `align_rt_buffer_append`, `align_rt_array_builder_push_str`, `align_rt_array_builder_append`, `align_rt_cli_flag_bool`, `align_rt_http_body`, `align_rt_http_rb_body` | | A74 | `void @SYM(ptr, ptr, i64, i32)` | `align_rt_json_encode_scalar_array` | | A75 | `void @SYM(ptr, ptr, i64, i64)` | `align_rt_rng_shuffle`, `align_rt_cli_flag_i64` | @@ -234,24 +236,24 @@ LLVM construction and receives no runtime-feature input. Tests compare: -- all 281 keys, mapped symbols, LLVM declaration types, and default attributes +- all 283 keys, mapped symbols, LLVM declaration types, and default attributes against this table through the checked-in `crates/align_codegen_llvm/tests/golden/runtime_abi_declarations.txt`; -- the 286 base native symbols against default-feature `align_runtime` exports, +- the 288 base native symbols against default-feature `align_runtime` exports, plus every actual Rust definition's normalized native return and ordered parameter types against the declaration golden, failing on either direction's difference through `scripts/test-runtime-abi-exports.sh`; -- the 290 `alloc-count` and 290 `par-map-probe` native symbols against +- the 292 `alloc-count` and 292 `par-map-probe` native symbols against `align_runtime` built with each feature separately, including the four exact probe signatures above; -- the 294 maximum native symbols against `align_runtime` built with +- the 296 maximum native symbols against `align_runtime` built with `alloc-count,par-map-probe,task-group-probe`, while proving `task-group-probe` adds no unmangled export; - rt-LTO off/on attributes for every guarded symbol, with missing, declaration-only, wrong-type, internal, private, available-externally, and non-C-calling-convention artifact negatives; -- all 286 identities through the one `RuntimeAbiId`-keyed row iterator and all - 286 exact registry function types through the production compatibility +- all 288 identities through the one `RuntimeAbiId`-keyed row iterator and all + 288 exact registry function types through the production compatibility predicate, one return mutation per row, and one mutation of every parameter ordinal; source-valid compatible reuse for a keyed builtin and the four source-reachable unkeyed rows; exact `ArgsBuild` `str` rejection plus the diff --git a/scripts/test-runtime-abi-exports.sh b/scripts/test-runtime-abi-exports.sh index f7e3ee24..7fa98be8 100755 --- a/scripts/test-runtime-abi-exports.sh +++ b/scripts/test-runtime-abi-exports.sh @@ -23,8 +23,8 @@ trap 'rm -rf -- "$work_dir"' EXIT audit_target="$work_dir/audit-target" archive="$audit_target/debug/libalign_runtime.a" sed -nE 's/.*@(align_rt_[A-Za-z0-9_]+)\(.*/\1/p' "$golden" | sort -u > "$work_dir/base" -if [[ "$(wc -l < "$work_dir/base" | tr -d ' ')" != 286 ]]; then - echo "test-runtime-abi-exports: declaration golden does not contain 286 base symbols" >&2 +if [[ "$(wc -l < "$work_dir/base" | tr -d ' ')" != 288 ]]; then + echo "test-runtime-abi-exports: declaration golden does not contain 288 base symbols" >&2 exit 1 fi @@ -73,13 +73,13 @@ perl -ne ' print join("|", $symbol, native_type($ret), @params), "\n"; ' "$runtime_ir" | sort > "$work_dir/runtime-abi" -if [[ "$(wc -l < "$work_dir/golden-abi" | tr -d ' ')" != 286 ]] \ - || [[ "$(wc -l < "$work_dir/runtime-abi" | tr -d ' ')" != 286 ]]; then - echo "test-runtime-abi-exports: normalized base ABI does not contain 286 rows" >&2 +if [[ "$(wc -l < "$work_dir/golden-abi" | tr -d ' ')" != 288 ]] \ + || [[ "$(wc -l < "$work_dir/runtime-abi" | tr -d ' ')" != 288 ]]; then + echo "test-runtime-abi-exports: normalized base ABI does not contain 288 rows" >&2 exit 1 fi diff -u "$work_dir/golden-abi" "$work_dir/runtime-abi" -printf 'runtime-abi-signatures base 286\n' +printf 'runtime-abi-signatures base 288\n' audit_case() { local label=$1 @@ -138,8 +138,8 @@ audit_case() { printf 'runtime-abi-exports %s %s\n' "$label" "$expected_count" } -audit_case base "" 286 -audit_case alloc alloc-count 290 -audit_case par par-map-probe 290 -audit_case task task-group-probe 286 -audit_case all alloc-count,par-map-probe,task-group-probe 294 +audit_case base "" 288 +audit_case alloc alloc-count 292 +audit_case par par-map-probe 292 +audit_case task task-group-probe 288 +audit_case all alloc-count,par-map-probe,task-group-probe 296