diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..aebbd2f623 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Preserve complete generated Lean snapshots, including literal bytes. Aeneas's +# pretty printer can emit trailing code spaces; the conservative comparison and +# independent proof builds check these files instead of hand-formatting them. +verification/aeneas/golden/*.lean whitespace=-blank-at-eol diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed995dd69b..3e623db968 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1043,6 +1043,71 @@ jobs: name: ${{ env.ZC_CI_IMAGE_ARCHIVE }} path: ${{ runner.temp }}/${{ env.ZC_CI_IMAGE_ARCHIVE }} + aeneas: + name: Aeneas translation and Lean proofs + runs-on: ubuntu-24.04 + timeout-minutes: 90 + env: + # Charon uses its own pinned Rust compiler. Keep the ordinary Rust CI + # flags (including randomized layouts) out of this extraction. + RUSTFLAGS: "" + RUSTDOCFLAGS: "" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install rustup + uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # zizmor: ignore[superfluous-actions] + with: + toolchain: stable + + - name: Install Nix for the pinned Aeneas source build + uses: DeterminateSystems/determinate-nix-action@8d87e8d5e5b8a8309d4281094560f127d9a265f1 # v3.22.5 + + - name: Install the pinned Aeneas, Charon, Rust, and Lean bundle + run: bash verification/aeneas/setup.sh + + - name: Test Rust syntax ownership, discovery, and golden comparison + run: | + source verification/aeneas/toolchain.sh + export CARGO_TARGET_DIR="$PWD/target/aeneas/annotation-tool" + cargo +"$AENEAS_RUST_TOOLCHAIN" test --locked --manifest-path tools/Cargo.toml -p aeneas-inline + cargo +"$AENEAS_RUST_TOOLCHAIN" build --locked --manifest-path tools/Cargo.toml -p aeneas-inline + python3 -B -m unittest discover -s verification/aeneas/tests + + - name: Check nominal type extraction and upstream compatibility + run: bash verification/aeneas/tests/nominal-tuples.sh + + # run.sh checks the same contracts against two definition sets. Keep + # negative controls after this positive baseline: a missing dependency + # or an already broken proof cannot count as rejection of a mutation. + - name: Compare live extraction with goldens and prove both models + run: bash verification/aeneas/run.sh + + - name: Test compiled effective-spec inspection + run: | + source verification/aeneas/toolchain.sh + export PATH="${AENEAS_TOOLCHAIN_DIR:-$PWD/target/aeneas/toolchain}/lean/bin:$PATH" + export AENEAS_INSPECTION_PROJECT="$PWD/target/aeneas/verification" + python3 -B -m unittest discover -s verification/aeneas/tests -p test_inspect.py + + - name: Verify that admissions and incorrect code fail + env: + LEAN_NUM_THREADS: "2" + run: | + # Each suite mutates only its own generated project. Collect both + # statuses so a failure cannot leave the other suite unobserved. + bash verification/aeneas/negative-controls.sh golden-verification > target/aeneas/golden-controls.log 2>&1 & + golden_pid=$! + bash verification/aeneas/negative-controls.sh verification > target/aeneas/live-controls.log 2>&1 & + live_pid=$! + status=0 + wait "$golden_pid" || status=1 + wait "$live_pid" || status=1 + cat target/aeneas/golden-controls.log target/aeneas/live-controls.log + exit "$status" + # Used to signal to branch protections that all other jobs have succeeded. all-jobs-succeed: # WARNING: This name is load-bearing! It's how GitHub's settings UI configures which jobs @@ -1053,7 +1118,7 @@ jobs: # success in branch protection, so this aggregation must fail closed. if: ${{ always() }} runs-on: ubuntu-latest - needs: [build_test, miri, codegen, coverage, kani, check_be_aarch64, check_avr_atmega, check_fmt, check_actions, check_readme, check_versions, check_msrv_is_minimal, check_stale_stderr, check-all-toolchains-tested, check-job-dependencies, check-todo, run-git-hooks, zizmor, build_docker_env] + needs: [aeneas, build_test, miri, codegen, coverage, kani, check_be_aarch64, check_avr_atmega, check_fmt, check_actions, check_readme, check_versions, check_msrv_is_minimal, check_stale_stderr, check-all-toolchains-tested, check-job-dependencies, check-todo, run-git-hooks, zizmor, build_docker_env] steps: - name: Reject workflow cancellation if: ${{ cancelled() }} diff --git a/.gitignore b/.gitignore index 9891c1a7b4..356a34c075 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,28 @@ target lcov.info +# Generated inputs and caches for the native Aeneas proof development project. +verification/aeneas/__pycache__/ +verification/aeneas/lean/.lake/ +verification/aeneas/lean/lakefile.lean +verification/aeneas/lean/lake-manifest.json +verification/aeneas/lean/lean-toolchain +verification/aeneas/lean/Specs.lean +verification/aeneas/lean/ModelShapes.lean +verification/aeneas/lean/Models.lean +verification/aeneas/lean/Required.lean +verification/aeneas/lean/Specs.source-map.json +verification/aeneas/lean/ModelShapes.source-map.json +verification/aeneas/lean/Models.source-map.json +verification/aeneas/lean/Specs.sources.sha256 +verification/aeneas/lean/.inline-projections.json +verification/aeneas/lean/bindings.json +verification/aeneas/lean/proof-dependencies.json +verification/aeneas/lean/Zerocopy/Types.lean +verification/aeneas/lean/Zerocopy/Funs.lean +verification/aeneas/lean/Zerocopy/TypesExternal_Template.lean +verification/aeneas/lean/Zerocopy/FunsExternal_Template.lean + # VSCode workspace files *.code-workspace diff --git a/evals/unsafe-rust/source-catalog.md b/evals/unsafe-rust/source-catalog.md index b111a540d5..685ce9448d 100644 --- a/evals/unsafe-rust/source-catalog.md +++ b/evals/unsafe-rust/source-catalog.md @@ -355,7 +355,7 @@ Use separate fresh agents for these audit units: 7. primitive, atomic, function-pointer, SIMD, float, and validity implementations in `src/impls.rs` and `src/byteorder.rs`; 8. layout/allocation arithmetic, ZSTs, DST metadata, and allocation failure in - `src/layout.rs`, `src/util/mod.rs`, and alloc branches; + `src/layout/mod.rs`, `src/util/mod.rs`, and alloc branches; 9. declarative macros and hidden support in `src/macros.rs` and `src/util/`; 10. the entire proc-macro generator as a theorem over every accepted token stream and interaction with other macros/attributes; diff --git a/skills/zerocopy-review/SKILL.md b/skills/zerocopy-review/SKILL.md index 249d05ad0f..32629fff8b 100644 --- a/skills/zerocopy-review/SKILL.md +++ b/skills/zerocopy-review/SKILL.md @@ -54,6 +54,44 @@ Check the issues that are relevant to the changed behavior, including: Before suggesting a new dependency, check `Cargo.toml` and the existing workspace for an appropriate dependency or utility. +## Specification Meaning + +For every specification under review, reconstruct the guarantee a human likely +intends. Ground that interpretation in the specification, surrounding +documentation, callers, reference implementations, and the bug or change that +motivated it. Compare the intended guarantee with what the specification +actually states. A passing proof establishes the written proposition; it does +not establish that the proposition captures the intended behavior. + +Look for ways the written guarantee could be weaker than that intention: + +- Requirements, ghost parameters, or branch guards exclude intended inputs. + Inconsistent requirements, a ghost parameter with an empty type, or an + admission predicate that always returns `false` can make a claim vacuous: + there are no cases in which the promised behavior must hold. +- Automatic input decoding or type-model invariants restrict the domain beyond + what a reader would reasonably infer from the function's specification. +- Postconditions omit intended observations, or compare projections that + discard the information whose correctness matters. +- An equivalence harness checks only successful results or skips comparisons + on relevant paths, leaving intended failure behavior or other cases unchecked. +- A reference implementation shares the very logic whose correctness the + equivalence check is meant to establish. +- A partial-correctness contract allows divergence where termination is + intended, or a proof covers fewer inputs or configurations than claimed. + +A total `ensures _ => True` can be sufficient for an assertion harness: +successful termination requires its assertions to pass on admitted inputs. +Inspect the assertions and admission conditions rather than rejecting the +postcondition merely because it is `True`. Concrete admission examples can +expose vacuity, but do not establish coverage of every intended input. + +Report substantiated gaps even when the proofs pass. State the intended +guarantee, the weaker written claim, and a distinguishing case when possible. +Keep inferred intention separate from the existing contract. When intention is +unclear, make that uncertainty explicit and seek clarification when needed; +do not silently invent a stronger requirement. + ## Findings Report actionable findings, not private chain-of-thought. For each finding: diff --git a/tools/Cargo.lock b/tools/Cargo.lock index dcf1c43a89..459e8b6ecb 100644 --- a/tools/Cargo.lock +++ b/tools/Cargo.lock @@ -17,6 +17,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aeneas-inline" +version = "0.0.0" +dependencies = [ + "proc-macro2", + "rustc_lexer", + "serde_json", + "syn", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -432,6 +442,15 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[package]] +name = "rustc_lexer" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86aae0c77166108c01305ee1a36a1e77289d7dc6ca0a3cd91ff4992de2d16a5" +dependencies = [ + "unicode-xid", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -729,6 +748,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unit-prefix" version = "0.5.2" diff --git a/tools/Cargo.toml b/tools/Cargo.toml index f7a1919ab9..59cdf9da94 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -8,6 +8,7 @@ [workspace] members = [ + "aeneas-inline", "cargo-zerocopy", "generate-readme", "ui-runner", diff --git a/tools/aeneas-inline/Cargo.toml b/tools/aeneas-inline/Cargo.toml new file mode 100644 index 0000000000..0af4a50c31 --- /dev/null +++ b/tools/aeneas-inline/Cargo.toml @@ -0,0 +1,20 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +[package] +name = "aeneas-inline" +edition.workspace = true +version.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +proc-macro2 = { version = "1", features = ["span-locations"] } +rustc_lexer = "=0.1.0" +serde_json = "1" +syn = { version = "2", features = ["full", "visit"] } diff --git a/tools/aeneas-inline/src/main.rs b/tools/aeneas-inline/src/main.rs new file mode 100644 index 0000000000..7c563ccf7e --- /dev/null +++ b/tools/aeneas-inline/src/main.rs @@ -0,0 +1,654 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +//! Report source ownership without compiling or expanding Rust. +//! +//! `syn` associates literal outer documentation with functions and nominal types. +//! The lexer independently reports every real comment, including comments that +//! have no supported owner. Python compares these reports so an orphaned fence +//! cannot disappear, while a fence inside a Rust string remains ordinary data. +//! All offsets refer to the exact source returned in the same JSON report. We +//! inspect inactive cfg branches too: coverage must not depend on the host cfg. + +use std::{fs, io}; + +use proc_macro2::LineColumn; +use serde_json::{json, Value}; +use syn::{ + spanned::Spanned, + visit::{self, Visit}, +}; + +struct Functions<'a> { + source: &'a str, + modules: Vec, + // Count enclosing contexts that cannot own supported contracts. A counter + // preserves the outer restriction when a nested visitor enters and leaves. + unsupported: usize, + impl_type: Option, + impl_generics: Vec, + inherent: bool, + functions: Vec, + implementations: Vec, + types: Vec, + attributes: Vec, +} + +impl Functions<'_> { + // Span columns count characters; the editing protocol needs byte offsets. + // Keep that conversion here so every reported range has the same meaning. + fn offset(&self, pos: LineColumn) -> usize { + let start = + self.source.split_inclusive('\n').take(pos.line - 1).map(str::len).sum::(); + start + + self.source[start..] + .char_indices() + .nth(pos.column) + .map_or(self.source[start..].len(), |(byte, _)| byte) + } + + fn docs(&self, attrs: &[syn::Attribute]) -> Vec { + // Return decoded literal text and the physical attribute range together. + // Computed docs are audited separately; expanding them would erase their + // source ownership and could depend on cfg or external files. + attrs + .iter() + .filter_map(|attr| { + if !attr.path().is_ident("doc") || !matches!(attr.style, syn::AttrStyle::Outer) { + return None; + } + let syn::Meta::NameValue(meta) = &attr.meta else { + return None; + }; + let syn::Expr::Lit(expr) = &meta.value else { + return None; + }; + let syn::Lit::Str(text) = &expr.lit else { + return None; + }; + Some(json!({"start": self.offset(attr.span().start()), + "end": self.offset(attr.span().end()), "text": text.value(), + "line": attr.span().start().line})) + }) + .collect() + } + + fn nominal( + &mut self, + ident: &syn::Ident, + generics: &syn::Generics, + attrs: &[syn::Attribute], + span: proc_macro2::Span, + kind: &str, + fields: Value, + ) { + // Preserve source parameter order, even for parameters unused by fields. + // Field admission is structural: an authored decoder cannot make a + // borrowed or otherwise unsupported raw carrier acceptable here. + let mut path = self.modules.clone(); + path.push(ident.to_string()); + let supported = self.unsupported == 0 + && generics.const_params().next().is_none() + && generics.type_params().all(|p| p.bounds.is_empty()) + && generics.where_clause.is_none() + && !has_borrowed_fields(&fields); + let (computed_docs, include_str_docs) = doc_flags(attrs); + self.types.push(json!({"path": path.join("::"), "kind": kind, + "start": self.offset(ident.span().start()), "close": self.offset(span.end()), + "supported": supported, "generics": generics.type_params().map(|p| p.ident.to_string()).collect::>(), + "inputs": [], "docs": self.docs(attrs), "computed_docs": computed_docs, + "include_str_docs": include_str_docs, "fields": fields})); + } + + fn function( + &mut self, + sig: &syn::Signature, + block: &syn::Block, + attrs: &[syn::Attribute], + method: bool, + ) { + // The report identifies the original function and original argument + // names. Python later checks this order against extraction before Lean + // can construct the application used by the generated proposition. + let open = self.offset(block.brace_token.span.open().start()); + assert_eq!(&self.source[open..open + 1], "{", "Invalid function brace span"); + let mut path = self.modules.clone(); + path.push(sig.ident.to_string()); + let mut supported = self.unsupported == 0; + let mut inputs = Vec::new(); + for input in &sig.inputs { + match input { + syn::FnArg::Receiver(receiver) => { + inputs.push("self".to_string()); + supported &= receiver.reference.is_none() || receiver.mutability.is_none(); + } + syn::FnArg::Typed(input) => { + supported &= !unsupported_borrow(&input.ty, true); + match input.pat.as_ref() { + syn::Pat::Ident(pattern) + if pattern.by_ref.is_none() && pattern.subpat.is_none() => + { + inputs.push(pattern.ident.to_string()); + } + _ => supported = false, + } + } + } + } + if let syn::ReturnType::Type(_, ty) = &sig.output { + supported &= !unsupported_borrow(ty, false); + } + supported &= sig.generics.where_clause.is_none(); + let mut generics = if method { self.impl_generics.clone() } else { Vec::new() }; + for parameter in &sig.generics.params { + match parameter { + syn::GenericParam::Type(parameter) => { + generics.push(parameter.ident.to_string()); + supported &= parameter.bounds.is_empty(); + } + syn::GenericParam::Lifetime(_) => {} + syn::GenericParam::Const(_) => supported = false, + } + } + let (computed_docs, include_str_docs) = doc_flags(attrs); + self.functions.push(json!({ + "path": path.join("::"), "kind": "function", "open": open, + "start": self.offset(sig.fn_token.span.start()), "docs": self.docs(attrs), + "computed_docs": computed_docs, "include_str_docs": include_str_docs, + "close": self.offset(block.brace_token.span.close().end()), + "supported": supported, "inputs": inputs, "generics": generics, + "method": method, "impl_type": self.impl_type, "inherent": self.inherent, + })); + } +} + +// Literal source docs own annotations. Other computed docs remain ordinary +// Rust documentation unless that owner also carries an annotation. Direct +// include_str! is detected without expanding any documentation macro. +fn doc_flags(attrs: &[syn::Attribute]) -> (bool, bool) { + fn flags(meta: &syn::Meta) -> (bool, bool) { + match meta { + syn::Meta::NameValue(value) if value.path.is_ident("doc") => { + struct Includes(bool); + impl<'ast> Visit<'ast> for Includes { + fn visit_macro(&mut self, mac: &'ast syn::Macro) { + self.0 |= + mac.path.segments.last().is_some_and(|p| p.ident == "include_str") + || tokens_include_str(mac.tokens.clone()); + } + } + let mut includes = Includes(false); + includes.visit_expr(&value.value); + ( + !matches!(&value.value, syn::Expr::Lit(expr) if matches!(expr.lit, syn::Lit::Str(_))), + includes.0, + ) + } + syn::Meta::List(list) if list.path.is_ident("cfg_attr") => { + use syn::parse::Parser as _; + let parser = + syn::punctuated::Punctuated::::parse_terminated; + match parser.parse2(list.tokens.clone()) { + Ok(entries) => entries + .iter() + .skip(1) + .map(flags) + .fold((false, false), |a, b| (a.0 || b.0, a.1 || b.1)), + Err(_) => ( + mentions_doc(list.tokens.clone()), + mentions_doc(list.tokens.clone()) + && tokens_include_str(list.tokens.clone()), + ), + } + } + _ => (false, false), + } + } + attrs.iter().map(|attr| flags(&attr.meta)).fold((false, false), |a, b| (a.0 || b.0, a.1 || b.1)) +} + +fn tokens_include_str(tokens: proc_macro2::TokenStream) -> bool { + // Macros hide syntax from syn's expression visitor, so inspect their token + // groups too. Require an actual invocation, not the word in a literal. + use proc_macro2::TokenTree; + let tokens: Vec<_> = tokens.into_iter().collect(); + tokens.windows(3).any(|window| { + matches!(window, + [TokenTree::Ident(name), TokenTree::Punct(bang), TokenTree::Group(_)] + if name == "include_str" && bang.as_char() == '!') + }) || tokens.iter().any(|token| match token { + TokenTree::Group(group) => tokens_include_str(group.stream()), + _ => false, + }) +} + +fn mentions_doc(tokens: proc_macro2::TokenStream) -> bool { + // This conservative fallback handles conditional docs that cannot be parsed + // as ordinary metadata. Python must still see their literal fragments. + tokens.into_iter().any(|token| match token { + proc_macro2::TokenTree::Ident(ident) => ident == "doc", + proc_macro2::TokenTree::Group(group) => mentions_doc(group.stream()), + _ => false, + }) +} + +fn token_strings(tokens: proc_macro2::TokenStream, output: &mut Vec) { + // Collect literal fragments without evaluating the surrounding macro. This + // supports the reserved-fence audit but never authorizes a computed fence. + for token in tokens { + match token { + proc_macro2::TokenTree::Literal(literal) => { + if let Ok(text) = syn::parse_str::(&literal.to_string()) { + output.push(text.value()); + } + } + proc_macro2::TokenTree::Group(group) => token_strings(group.stream(), output), + _ => {} + } + } +} + +fn doc_fragments(meta: &syn::Meta) -> Vec { + // Token groups and parsed metadata represent different attribute spellings. + // Both contribute strings to the audit, not to the owner-doc payload. + struct Strings(Vec); + impl<'ast> Visit<'ast> for Strings { + fn visit_lit_str(&mut self, text: &'ast syn::LitStr) { + self.0.push(text.value()); + } + fn visit_macro(&mut self, mac: &'ast syn::Macro) { + token_strings(mac.tokens.clone(), &mut self.0); + } + } + let mut strings = Strings(Vec::new()); + if let syn::Meta::List(list) = meta { + token_strings(list.tokens.clone(), &mut strings.0); + } else { + strings.visit_meta(meta); + } + strings.0 +} + +// Shared input references are supported, but mutable inputs need backward borrow +// reconstruction. Outputs and nominal fields cannot retain any reference. One +// traversal enforces both policies, including references nested in containers. +// Function pointers remain unsupported under either policy. +fn unsupported_borrow(ty: &syn::Type, allow_shared: bool) -> bool { + struct Borrow { + found: bool, + allow_shared: bool, + } + impl<'ast> Visit<'ast> for Borrow { + fn visit_type_reference(&mut self, ty: &'ast syn::TypeReference) { + self.found |= !self.allow_shared || ty.mutability.is_some(); + visit::visit_type_reference(self, ty); + } + fn visit_type_bare_fn(&mut self, _: &'ast syn::TypeBareFn) { + self.found = true; + } + } + let mut visitor = Borrow { found: false, allow_shared }; + visitor.visit_type(ty); + visitor.found +} + +fn has_borrowed_fields(fields: &Value) -> bool { + // Struct fields and enum variants share the report's recursive container + // shape. A borrowed field in any variant makes the nominal owner unsupported. + match fields { + Value::Array(values) => values.iter().any(has_borrowed_fields), + Value::Object(values) => { + values.get("borrowed") == Some(&Value::Bool(true)) + || values.values().any(has_borrowed_fields) + } + _ => false, + } +} + +fn fields(fields: &syn::Fields) -> Value { + // Tuple positions are names too; preserve their order rather than inventing + // source identifiers that could disagree with the extracted carrier. + json!(fields + .iter() + .enumerate() + .map(|(index, field)| { + json!({"name": field.ident.as_ref().map_or(index.to_string(), ToString::to_string), + "borrowed": unsupported_borrow(&field.ty, false)}) + }) + .collect::>()) +} + +impl<'ast> Visit<'ast> for Functions<'_> { + fn visit_attribute(&mut self, attr: &'ast syn::Attribute) { + // Include every doc spelling, even forms that cannot be evaluated by syn. + // Python rejects reserved fences in conditional/computed doc forms. + let conditional_doc = match &attr.meta { + syn::Meta::List(list) if attr.path().is_ident("cfg_attr") => { + mentions_doc(list.tokens.clone()) + } + _ => false, + }; + if attr.path().is_ident("doc") || conditional_doc { + let start = self.offset(attr.span().start()); + let end = self.offset(attr.span().end()); + self.attributes.push(json!({"start": start, "end": end, + "outer": matches!(attr.style, syn::AttrStyle::Outer), + "doc": attr.path().is_ident("doc"), "fragments": doc_fragments(&attr.meta)})); + } + visit::visit_attribute(self, attr); + } + + fn visit_item_struct(&mut self, item: &'ast syn::ItemStruct) { + self.nominal( + &item.ident, + &item.generics, + &item.attrs, + item.span(), + "struct", + fields(&item.fields), + ); + visit::visit_item_struct(self, item); + } + + fn visit_item_enum(&mut self, item: &'ast syn::ItemEnum) { + let variants = item.variants.iter().map(|variant| { + json!({"name": variant.ident.to_string(), "fields": fields(&variant.fields)}) + }).collect::>(); + self.nominal( + &item.ident, + &item.generics, + &item.attrs, + item.span(), + "enum", + json!(variants), + ); + visit::visit_item_enum(self, item); + } + + fn visit_item_mod(&mut self, item: &'ast syn::ItemMod) { + self.modules.push(item.ident.to_string()); + visit::visit_item_mod(self, item); + self.modules.pop(); + } + + fn visit_item_fn(&mut self, item: &'ast syn::ItemFn) { + self.function(&item.sig, &item.block, &item.attrs, false); + // Never attribute nested functions to the enclosing function or method. + self.unsupported += 1; + visit::visit_item_fn(self, item); + self.unsupported -= 1; + } + + fn visit_item_impl(&mut self, item: &'ast syn::ItemImpl) { + let previous_type = self.impl_type.clone(); + let previous_inherent = self.inherent; + let previous_generics = self.impl_generics.clone(); + self.impl_generics = item.generics.type_params().map(|p| p.ident.to_string()).collect(); + self.impl_type = match item.self_ty.as_ref() { + syn::Type::Path(path) => path.path.segments.last().map(|s| s.ident.to_string()), + _ => None, + }; + self.inherent = item.trait_.is_none(); + // A local named Self may apply the impl's own type parameters. + // Specializations, qualified Self and trait impls still fail closed. + let ident = match item.self_ty.as_ref() { + syn::Type::Path(path) if path.qself.is_none() && path.path.segments.len() == 1 => { + let segment = &path.path.segments[0]; + let supported = match &segment.arguments { + syn::PathArguments::None => true, + syn::PathArguments::AngleBracketed(args) => args.args.iter().all(|arg| { + let syn::GenericArgument::Type(syn::Type::Path(arg)) = arg else { + return false; + }; + arg.qself.is_none() + && arg.path.get_ident().is_some_and(|ident| { + item.generics.type_params().any(|param| param.ident == *ident) + }) + }), + _ => false, + }; + supported.then_some(&segment.ident) + } + _ => None, + }; + let ident = ident.filter(|_| { + item.trait_.is_none() + && self.unsupported == 0 + && item.generics.const_params().next().is_none() + && item.generics.type_params().all(|p| p.bounds.is_empty()) + && item.generics.where_clause.is_none() + }); + let macros: Vec<_> = item + .items + .iter() + .filter_map(|item| match item { + syn::ImplItem::Macro(item) => Some( + item.mac + .path + .segments + .iter() + .map(|s| s.ident.to_string()) + .collect::>() + .join("::"), + ), + _ => None, + }) + .collect(); + // Record impls and direct macro invocations before cfg filtering. An + // empty or macro-only impl must not disappear from closed coverage. + self.implementations.push(json!({ + "type": self.impl_type, "inherent": self.inherent, + "supported": ident.is_some(), "macros": macros, "modules": self.modules, + })); + if let Some(ident) = ident { + self.modules.push(ident.to_string()); + visit::visit_item_impl(self, item); + self.modules.pop(); + } else { + self.unsupported += 1; + visit::visit_item_impl(self, item); + self.unsupported -= 1; + } + self.impl_type = previous_type; + self.inherent = previous_inherent; + self.impl_generics = previous_generics; + } + + fn visit_impl_item_fn(&mut self, item: &'ast syn::ImplItemFn) { + self.function(&item.sig, &item.block, &item.attrs, true); + self.unsupported += 1; + visit::visit_impl_item_fn(self, item); + self.unsupported -= 1; + } + + fn visit_item_trait(&mut self, item: &'ast syn::ItemTrait) { + self.unsupported += 1; + visit::visit_item_trait(self, item); + self.unsupported -= 1; + } +} + +fn inspect(source: &str) -> Result { + // Keep comment discovery independent of AST ownership. A source string that + // only resembles a comment must never enter this roster, while a real + // orphaned comment must remain visible even when the AST has no valid owner. + let mut offset = 0; + let mut comments = Vec::new(); + for token in rustc_lexer::tokenize(source) { + match token.kind { + rustc_lexer::TokenKind::LineComment | rustc_lexer::TokenKind::BlockComment { .. } => { + comments.push(json!({"start": offset, "end": offset + token.len})); + } + _ => {} + } + offset += token.len; + } + let syntax = syn::parse_file(source)?; + let mut visitor = Functions { + source, + modules: Vec::new(), + unsupported: 0, + impl_type: None, + impl_generics: Vec::new(), + inherent: false, + functions: Vec::new(), + implementations: Vec::new(), + types: Vec::new(), + attributes: Vec::new(), + }; + visitor.visit_file(&syntax); + Ok(json!({"source": source, "comments": comments, "functions": visitor.functions, + "implementations": visitor.implementations, "types": visitor.types, + "attributes": visitor.attributes})) +} + +fn main() -> Result<(), Box> { + let paths: Vec = serde_json::from_reader(io::stdin())?; + let mut result = serde_json::Map::new(); + for path in paths { + let source = fs::read_to_string(&path)?; + let inspected = inspect(&source).map_err(|error| format!("{path}: {error}"))?; + result.insert(path, inspected); + } + serde_json::to_writer(io::stdout(), &result)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn preserves_real_comments_but_not_raw_string_contents() { + let source = + "fn f() { // real\n let x = r###\"// fake /* fake */\"###; /* outer /* inner */ */ }"; + let value = inspect(source).unwrap(); + assert_eq!(value["comments"].as_array().unwrap().len(), 2); + assert_eq!(value["functions"][0]["path"], "f"); + } + + #[test] + fn tracks_unicode_spans_modules_and_nested_functions_before_cfg_filtering() { + let source = "const 雪: u8 = 0; mod m { #[cfg(any())] fn f() { fn inner() {} } }"; + let value = inspect(source).unwrap(); + let functions = value["functions"].as_array().unwrap(); + assert_eq!(functions[0]["path"], "m::f"); + assert_eq!(functions[0]["supported"], true); + assert_eq!(functions[1]["supported"], false); + let open = functions[0]["open"].as_u64().unwrap() as usize; + assert!(source[open..].starts_with("{ fn inner()")); + } + + #[test] + fn resolves_simple_inherent_methods_and_rejects_other_impl_forms() { + for (implementation, supported) in [ + ("impl S", true), + ("impl S", true), + ("impl S", false), + ("impl Trait for S", false), + ("impl m::S", false), + ] { + let source = + format!("mod m {{ {implementation} {{ fn f() {{ fn nested() {{}} }} }} }}"); + let value = inspect(&source).unwrap(); + assert_eq!(value["functions"][0]["supported"], supported); + assert_eq!(value["functions"][1]["supported"], false); + if supported { + assert_eq!(value["functions"][0]["path"], "m::S::f"); + } + } + let value = inspect("impl S { fn f() {} }").unwrap(); + assert_eq!(value["functions"][0]["supported"], true); + } + + #[test] + fn preserves_input_order_and_impl_generics_and_rejects_patterns() { + let value = inspect("impl S { fn f(self, mut x: T, y: E) {} }").unwrap(); + assert_eq!(value["functions"][0]["generics"], json!(["E", "T"])); + assert_eq!(value["functions"][0]["inputs"], json!(["self", "x", "y"])); + assert_eq!(value["functions"][0]["supported"], true); + for source in [ + "fn f((x, y): (u8, u8)) {}", + "fn f(_: u8) {}", + "fn f() {}", + "fn f(x: &mut u8) {}", + "fn f() -> &'static u8 {}", + "impl S { fn f(&mut self) {} }", + ] { + assert_eq!(inspect(source).unwrap()["functions"][0]["supported"], false); + } + } + + #[test] + fn borrow_policy_distinguishes_shared_inputs_from_outputs_and_fields() { + for (ty, input_supported) in [ + ("u8", true), + ("Option<&'static u8>", true), + ("(&'static u8, Option<&'static mut u8>)", false), + ("&'static &'static mut u8", false), + ("Option u8>", false), + ] { + let parsed: syn::Type = syn::parse_str(ty).unwrap(); + assert_eq!(unsupported_borrow(&parsed, true), !input_supported, "{ty}"); + assert_eq!(unsupported_borrow(&parsed, false), ty != "u8", "{ty}"); + } + } + + #[test] + fn records_empty_and_macro_only_impls_before_cfg_filtering() { + let value = inspect( + "impl S {} #[cfg(any())] impl m::S { #[cfg(any())] methods!(); } \ + impl Trait for S { methods!(); } fn f() { unrelated!(); }", + ) + .unwrap(); + let implementations = value["implementations"].as_array().unwrap(); + assert_eq!(implementations.len(), 3); + assert_eq!(implementations[0]["type"], "S"); + assert_eq!(implementations[0]["supported"], true); + assert_eq!(implementations[0]["macros"], json!([])); + assert_eq!(implementations[1]["type"], "S"); + assert_eq!(implementations[1]["inherent"], true); + assert_eq!(implementations[1]["supported"], false); + assert_eq!(implementations[1]["macros"], json!(["methods"])); + assert_eq!(implementations[2]["inherent"], false); + let nested = inspect("mod m { impl S { fn f() {} } }").unwrap(); + assert_eq!(nested["implementations"][0]["modules"], json!(["m"])); + assert_eq!(nested["functions"][0]["path"], "m::S::f"); + } + #[test] + fn owns_outer_literal_doc_attributes_and_preserves_nominal_metadata() { + let source = "/// ```aeneas\n/// spec f_spec\n/// ensures r => True\n/// ```\n#[allow(unused)] fn f(x: T) {}\n#[doc = \"```aeneas\\nmodel SValue := Fields\\ndecode self => self\\n```\"] struct S(T); enum E { A, B(S) }"; + let value = inspect(source).unwrap(); + assert_eq!(value["functions"][0]["docs"].as_array().unwrap().len(), 4); + assert_eq!(value["functions"][0]["docs"][0]["text"], " ```aeneas"); + assert_eq!(value["types"][0]["kind"], "struct"); + assert_eq!(value["types"][0]["generics"], json!(["T"])); + assert_eq!(value["types"][0]["fields"][0]["name"], "0"); + assert_eq!(value["types"][1]["kind"], "enum"); + assert_eq!(value["types"][1]["fields"][1]["name"], "B"); + assert_eq!(value["attributes"].as_array().unwrap().len(), 5); + } + + #[test] + fn distinguishes_computed_docs_from_visible_include_str_invocations() { + let source = "#[doc = condVersionLink!(\"version\", \"link\")] fn f() {} \ + #[cfg_attr(any(), doc = condVersionLink!(\"version\", \"link\"))] struct S; \ + #[doc = concat!(\"ordinary\", std::include_str!(\"contract.md\"))] fn g() {} \ + #[cfg_attr(any(), cfg_attr(all(), doc = wrapper!(::std::include_str!(\"contract.md\"))))] enum E { A }"; + let value = inspect(source).unwrap(); + assert_eq!(value["functions"][0]["computed_docs"], true); + assert_eq!(value["functions"][0]["include_str_docs"], false); + assert_eq!(value["types"][0]["computed_docs"], true); + assert_eq!(value["types"][0]["include_str_docs"], false); + assert_eq!(value["functions"][1]["include_str_docs"], true); + assert_eq!(value["types"][1]["include_str_docs"], true); + let value = inspect("#[doc = \"include_str!(not an invocation)\"] fn f() {}").unwrap(); + assert_eq!(value["functions"][0]["computed_docs"], false); + assert_eq!(value["functions"][0]["include_str_docs"], false); + } +} diff --git a/verification/aeneas/DESIGN.md b/verification/aeneas/DESIGN.md new file mode 100644 index 0000000000..458c5e0753 --- /dev/null +++ b/verification/aeneas/DESIGN.md @@ -0,0 +1,318 @@ + + +# Aeneas integration: design and development plan + +This document records the unified design, the immediate implementation work, +and the decisions deliberately deferred until more use supplies evidence. +`README.md` describes commands and current behavior; `SEMANTICS.md` describes +the independent layout semantics and explicit Rust correspondence premise. +Changes to this plan must preserve the stated claims or identify an +intentional change to their domain or observations. Implementation status +belongs with the relevant item below rather than in an unrelated backlog. + +## Purpose and scope + +The integration proves conditional functional behavior of selected, actual +zerocopy functions using their Aeneas extractions. An inline Rust +specification must have unambiguous ownership, refer to the original extracted +function and arguments, and receive a checked ordinary Lean proof. The same +claims must hold independently for checked-in generated goldens and fresh +extraction. + +The mathematical model is a common language for operation contracts. +Operations give that language its behavioral meaning. Decoding determines +interpretation and admission; it is an implementation detail that proofs may +unfold or describe with reusable lemmas. There is no universal requirement for +an encoder, injectivity, canonical raw representation, round trips, or a +separately authored representation relation. A map model may forget its seed. +A model must retain enough information for the operations and observations we +actually promise. + +These are conditional contracts. They do not establish that every safe caller +meets preconditions, that every constructor or mutation preserves an +invariant, or that pointer operations are free from undefined behavior. +Anneal's stronger claim would need those additional obligations. Numerical +layout proofs use the explicit Rust premise rather than a formal proof of +rustc. + +## Semantic core + +`RustModel Raw` contains only a mathematical `Model` and `decode : Raw -> +Option Model`. Successful decoding defines admission. Mathematical values can +carry proof fields, so their constraints follow from construction. + +The complete decoder recursively decodes every field before applying a local +decoder, including fields whose information the local model discards. Enum +decoding visits the selected payload. An infallible local decoder adds no +restriction beyond child admission; a fallible one defines additional +admission. Unannotated supported types receive structural models. Missing or +unsupported models fail explicitly rather than receiving a universal trivial +fallback. + +Model shapes depend only on mathematical child types. Full providers depend on +the actual child decoders. This separates shapes, ordinary support lemmas, and +decoder construction without requiring another public typeclass. Distinct raw +nominal types remain distinct through the pinned Aeneas tuple-newtype option. + +Function annotations precede their Rust owner and contain exactly one `spec` +or `partial spec`. Original source type parameters and value arguments are +inferred from the verified extraction. Every original type parameter receives +a modeling dictionary, including unused and result-only parameters. Automatic +providers are fixed before decoded witnesses, ghosts, requirements, and +authored instances. + +Plain clauses expose decoded value inputs and outputs. `(raw)` clauses expose +their original representations. Requirements are successive named premises; +postconditions are conjunctive and share one output witness. A successful raw +payload must decode even for raw-only or `True` postconditions. Execution +always uses the original raw arguments. + +Ghosts elaborate in mathematical input scope and may depend on previous +ghosts. They are universally quantified premises fixed for the contract, not +arguments to the extracted function or choices made after execution. Their +types affect coverage: `Fin xs.length` admits no position for an empty list; +`False` admits no instance at all. Such domains are allowed when intended and +must not conceal a broader promised domain. + +Total contracts require successful termination and exclude panic and +divergence. Explicit partial contracts permit divergence, exclude panic, and +check every successful return. Decoder rejection, Rust `Option`/`Result` +payloads, and Aeneas execution failure remain distinct. + +Decoder-scoped completion uses native record `..` and ordinary kernel-checked +proof search for omitted propositions. Missing data requires ordinary +inference or explicit author input. Explicit proofs remain available. Failure, +including inside a fallible decoder, is a compilation error and never becomes +rejection. There is no per-model tactic registry unless concrete repeated +needs justify it. + +## Proofs and independent contract checks + +The canonical theorem targets the transparent generated `Specs` proposition. +Proofs and helper lemmas are ordinary checked-in Lean modules. Native imports, +declaration order, and Aeneas theorem registration provide assembly. The +compiled proof graph reports dependencies; it does not dictate a proof +structure or a Rust call graph. Raw lemmas remain useful for broad domains and +intermediate representations. There is no requirement to decode after every +assignment. + +Handwritten `Obligations` are independently expressed behavioral expectations. +Their role is specification adequacy and regression protection for promised +domains and observations, not a second universal certification of each model. +When a spec already expresses the complete intended claim, duplication +supplies additional checking evidence rather than another kind of correctness +guarantee. + +The independent comparison must abstract an execution outcome. For original +arguments `args` and arbitrary `run : Result RawOutput`, it must establish +`AuthoredAt args run -> RequiredAt args run`. Separately, the canonical proof +establishes `AuthoredAt args (extractedFunction args)`. The outcome precedes +modeling dictionaries, decoded witnesses, ghosts, and requirements so all +instances concern one execution. + +Comparing concrete propositions permits an adapter to ignore the authored spec +and independently reprove the implementation. Concrete equivalence has the +same problem when both propositions are true. An arbitrary outcome exposes +weak postconditions, accidental partial correctness, and unintended domain +narrowing. The required input domain must be independently maintained where +admission is part of the promise; sharing a changed decoder on both sides can +be vacuous. + +The original execution node and argument prefix remain audited. Its abstracted +WP node must use the fresh outcome, and specialization must be exactly the +original spec. A family that ignores its outcome cannot pass merely because +its concrete specialization agrees. The independently authored family remains +ordinary Lean, with a checked connection to the concrete required proposition. + +Existing required guarantees must survive migrations. New requirements or +decoder restrictions that narrow a theorem's domain are semantic changes and +need explicit review. Useful broader raw theorems can be preserved as helpers. +Representation and admission lemmas are ordinary tools for proving operation +contracts or their adequacy; their existence is not a universal type-level +duty. + +## Implementation organization + +Share a rule when one fact determines its result; retain separate observations +when their independence supplies evidence. For example, a nominal Rust owner's +name determines its generated `Fields`, `decode`, and `aeneasModel` names. The +registry stores the owner once and computes those names. Decoder construction +and its decomposition theorem use the same constructor traversal, while their +different field operations remain explicit: one sequences decoding and the +other states existential witnesses and decoder equations. + +The source tools likewise normalize an extracted type's unique source owner +once and append a generated line with its diagnostic location in one operation. +Copy-back planning reuses region parses only after complete baseline validation. +The editor and CI share the generated-module roster, and failure controls share +one roster for fixture preflight and cleanup. These choices remove independent +state and repeated decisions without adding a new public abstraction. + +The Rust parser and comment audit remain separate, as do authored specs and +independent expectations. Merging those checks would remove the evidence that +catches a missed annotation or weakened claim. Comments explain both the shared +mechanisms and these deliberate boundaries, starting with the reader overview +in `README.md` and continuing beside the relevant implementation. + +## Extraction, CI, and development + +Application behavior can be stated in ordinary Rust. An independent reference +calculation and an assertion harness are extracted together with the production +functions they call. A total `ensures _ => True` on the harness requires successful +termination, so its assertions prove the comparison even though the written +postcondition says nothing about the unit result. Guards are part of this visible +claim: returning early makes no comparison for that input. Reviewers must inspect +guards, reference independence, and complete result comparisons when reconstructing +the intended guarantee. A proof does not establish that the reference implements +rustc; the explicit Rust premise and compiler regression tests remain necessary. + +Every present annotation uses the same mandatory checking path. A harness needs +no special root declaration, and a separate coverage roster would add a second +place for the reviewer to reconstruct the checked set. Removing an annotation +and its proof intentionally reduces that set. Independent expected propositions +still challenge contract elaboration and preserve existing helper domains; they +do not supply hidden application claims that a Rust harness needs to express. + +Goldens contain complete generated modules. CI checks conservative fuzzy +textual equivalence and independently compiles proofs, required comparisons, +corollaries, and axiom audits for both golden and live models. Inline +mathematical decoders never replace the live extracted function definitions. +Both builds establish the checked properties, not equivalence of all unclaimed +behaviors. + +Reserved fences, Rust ownership, annotation-derived extraction roots, nominal +type/provider registration, exact function binding, complete LLBC extraction, +inspected source snapshots, configuration, pins, and external primitive +signatures protect the connection between a kernel-checked proposition and the +Rust item. Every present annotation is checked on both golden and live models. +Removing an annotation and its associated proofs intentionally removes that claim +from the checked set, visibly in the source and generated golden changes. + +The trust boundary includes the pinned Rust/Charon/Aeneas translation, builtin +models, explicit primitive correspondences, Lean's kernel and permitted logic +axioms, source association, and specification adequacy. ABI reads are audited +data inputs, not axioms asserting useful properties. Type erasure does not +supply a universal ABI interpretation. `SEMANTICS.md` records the exact +bridge. + +Failure controls challenge behavioral promises and required domains. Faulty +models must elaborate before proof rejection counts as evidence. Controls +should survive reorganizing helper lemmas: a constant rounding decoder fails +constructor and getter promises; a rejecting decoder fails promised output +admission or independently required input coverage. + +Native Lean editing uses protected generated projections and real handwritten +proof paths. Rust annotations are authoritative; saved snapshots and owned +spans authorize owner-scoped copy-back. Source maps provide diagnostics. +Preview, conflict rejection, exact regeneration, and interruption recovery +preserve edits. Compiled inspection reports its actual subject and freshness +without claiming a new verification result. Source clones, credentials, and +irreplaceable editing state remain outside disposable `target` assets. Monitor +disk headroom before expensive extraction and while sustained builds run. + +## Implemented changes in this stack + +The stack implements the following changes in its earliest useful commits. + +1. Replace concrete required-contract conversion with arbitrary-outcome adequacy + and validate every existing contract. Retain source binding, both builds, + independent domains, coverage, and axiom auditing. Include cross-module and + adversarial tests for ignored outcomes, weakened posts, partial correctness, + empty ghosts, and domain narrowing. +2. Normalize explicit decoder reasoning consistently. Reuse decomposition, + `simp`, scalar facts, and `WP.spec_mono`; add a small sequencing tactic only + where it eliminates repeated bridge work. Mathematical carriers alone cannot + identify a decoder because different providers can share the same carrier. +3. Migrate `extend` to mathematical clauses while preserving its existing exact + placement, alignment, flag, and payload guarantees. Keep useful raw helpers; + avoid proof churn that merely changes presentation. +4. Retarget semantic decoder controls to operation promises and coverage rather + than named representation-helper failures. +5. Allow ordinary acyclic model-support dependencies. Independent comparisons + protect required getter domains and output admission; mutation controls + challenge those guarantees without relying on helper names or import bans. +6. Integrate each change in the earliest useful commit of the existing stack. + Preserve coherent review boundaries and validate intermediate states. Worker + boundaries do not determine PR boundaries. Preserve GHerrit identities unless + reordering requires regeneration to avoid the known auto-close bug. +7. Simplify implementation state and repeated traversals while preserving the + checked boundaries above. Add module introductions, local explanations, and + reader guidance for the source tools, Lean core, proof layers, and CI drivers. +8. Add Rust assertion harnesses and independent reference arithmetic in the + earliest commits that can prove them. Retain the existing mathematical helper + guarantees while making the complete numerical comparisons visible in Rust. + Discover the checked set solely from present annotations, without a separate + method roster or special harness classification. + +## Deferred decisions and reopening evidence + +- **Canonical layout abstraction.** Retain structural `DstLayout` and existing + `ModelViews` initially. Padding and trailing-size pilots already use those + views. Use `extend`, sequence queries, and capacity contracts to compare total + specification, decoder, bridge, and consumer burden before adopting + `LayoutMath.LayoutValue` as the canonical model. Preserve physical offset, + stride, flags, exact overflow outcomes, nested padding, and conservative query + behavior. `DstLayout` intentionally admits intermediate values that do not + describe completed Rust types; no global physical-realizability invariant. +- **Remaining raw clauses.** Migrate where mathematical language improves the + whole proof. Raw clauses remain appropriate for representation-sensitive + helpers and explicit ABI read premises. Do not require a zero raw-clause + count. +- **Per-function independent duplication.** Retain current required contracts + during this migration. After outcome adequacy is exercised, evaluate whether + generic expansion tests can replace routine duplicate declarations while + retaining independent semantic regression boundaries for layout operations. + This is a policy choice, not a kernel requirement. +- **Richer automation.** Prefer upstream checked-arithmetic stepping, scalar + tactics, and the existing loop adapter. Introduce a larger lifting DSL, + per-model tactics, or new loop abstraction only after concrete repeated + failures show a net benefit over ordinary lemmas and a thin tactic. +- **Broader carriers and effects.** Recursive nominal groups, indexed carriers, + trait/const generics, and escaping borrow reconstruction remain unsupported + with explicit errors. Add semantics and admission checks before accepting + them. Stateful or nondeterministic APIs may need observable traces and + effects, rather than only result contracts. +- **Anneal's stronger guarantees.** Construction/mutation closure, universal + caller admission, memory validity, provenance, aliasing, and UB-freedom belong + to a separately stated stronger claim. Conditional functional proofs can feed + it without silently being presented as that claim. +- **Compiler correspondence.** Independent recursive semantics plus explicit + Rust premises are the selected approach. Compiler regression tests challenge + those premises for tested configurations. Formal rustc verification, + exhaustive target coverage, and compatibility across compiler intervals are separate + projects, not consequences of these tests. +- **Editing and execution conveniences.** Existing Lake incrementality, + inspection, source maps, and copy-back are the baseline. Add watch mode, + finer proof modules, doctor checks, or owned-cache pruning only for concrete + latency or operational problems. Cache cleanup must preserve source and + recovery data; disk pressure never authorizes deleting unique work. +- **Further implementation simplification.** The shared nominal traversal and + derived registration names are implemented. Apply the same rule to future + repeated state only when it reduces the whole implementation's reading burden. + Preserve independent Rust ownership/count checks and regressions for discarded + invalid fields, enum payloads, generic shapes, provider selection, and exports. +- **Upstreaming nominal-type support.** Keep the tuple-newtype option patch + reproducible and included in the pin identity. Adopt upstream support when + available with fresh extraction, complete golden review, and both proof + builds. + +## Historical choices that should not be reintroduced accidentally + +The integration no longer uses inline Aeneas function bodies or inline proofs, +model replacement slots, proof templates, per-function declaration sorting, +`depends_on` rosters, `refines` clauses, or separate automatically selected +validity predicates. Native modules, transparent specs, direct retained +modeling providers, recursive decoding, and ordinary postconditions replace +them. + +The generic-field staging problem and copy-back gap are resolved. Model proof +completion is scoped to decoder construction rather than defaults attached to +the model declaration. Encoders, losslessness, and representation certificates +remain optional properties justified by particular operation proofs. diff --git a/verification/aeneas/README.md b/verification/aeneas/README.md new file mode 100644 index 0000000000..b04805d2a0 --- /dev/null +++ b/verification/aeneas/README.md @@ -0,0 +1,734 @@ + + +# Aeneas verification in CI + +The `aeneas` job in `.github/workflows/ci.yml` compiles selected zerocopy functions +with Charon and Aeneas, compares complete generated Lean goldens, and proves +the inline Rust specifications against both the checked-in and live models. +The required `All checks succeeded (ci.yml)` job depends on it. The settled design +and deferred work are recorded in [DESIGN.md](DESIGN.md). + +Application claims can use ordinary Rust assertion harnesses with total +`ensures _ => True` specifications. Successful termination requires every +assertion to pass. Review the independent calculation, complete comparisons, +and early returns together: returning early excludes that input from the +comparison. + +These harnesses use the same annotation and proof rules as every other function. +There is no root marker or separately maintained list of required functions. +Every present specification must have its corresponding proof in both CI builds. +Removing a specification deliberately removes that claim; the Rust diff must +make that decision visible. Helper specifications remain useful proof interfaces, +but understanding their Lean proofs is unnecessary to assess what a Rust +assertion harness promises. Reviewing the integration itself still requires +reviewing the extraction, contract elaboration, and audit machinery below. + +## How the implementation fits together + +The integration connects a comment beside a Rust function to a proposition that +Lean has proved about that function. Several tools make that connection, because +each tool answers a different question: + +1. Rust's parser identifies the item that owns the comment. The independent + source audit accounts for every reserved fence, including fences the parser + cannot associate with a supported item. These checks prevent an annotation + that looks meaningful to a reader from being silently ignored. +2. Charon translates the selected Rust implementation to LLBC, an intermediate + representation of its types and control flow. Aeneas translates LLBC to Lean. + This is the **extracted model**: executable Lean definitions representing the + Rust program. Extraction includes called functions, not just the annotated + function's body. +3. Our Lean declarations describe how extracted values are interpreted as + mathematical values. A `RustModel` supplies a mathematical type and a decoder. + Successful decoding defines which representations a contract admits. The + decoder first checks every field, then applies the type's authored decoder. + A parent that ignores a field cannot thereby skip that field's admission. +4. The `spec` command turns the inline requirements and postconditions into a + transparent proposition in `Specs`. A handwritten Lean theorem proves that + proposition. The command supplies input decoding and successful-result + decoding, even when the authored postcondition says only `True`. +5. Separately written `Obligations` describe the behavior and input domain we + intend to retain. `check_contract` proves that the authored spec implies that + expectation for an arbitrary execution outcome. It then applies that + implication to the actual function and its canonical proof. `Check` audits + these compiled declarations, their source bindings, and their assumptions. + +The extracted model and the mathematical model have different purposes. The +first represents the computation we prove properties about; the second supplies +the vocabulary in which we state those properties. For example, Rust stores +rounding alignment and phase in one nonzero word, while its mathematical model +exposes two natural numbers. Plain clauses use mathematical values; `(raw)` +clauses use extracted representations. Both execute the same extracted function +on its original arguments and require the actual returned value to decode. + +Aeneas also distinguishes execution from the value returned by execution. +`Aeneas.Std.Result` represents success, failure such as panic, or divergence. +A successful computation may itself return a Rust `Option` or Rust `Result`. +Thus successful execution returning Rust `None`, a decoder rejecting a value, +and the function panicking are three different events. Total `spec` requires +success; `partial spec` also allows divergence, while still excluding panic. + +The independent comparison checks the specification itself. For an identity +function, both `ensures r => True` and `ensures r => r = x` are provable about +the real implementation. Comparing only those concrete truths would not reveal +that the first specification promises less. Comparing an arbitrary outcome +does: a wrong result satisfies `True` but fails the independently required +identity property. The required domain is independent too, so a decoder that +rejects every input cannot silently remove previously promised coverage. + +`run.sh` builds this whole chain twice. One build uses the checked-in extraction; +the other uses a fresh extraction. Fuzzy comparison permits specified textual +differences between them, but each build must prove all the same contracts and +pass the same audits. Neither build substitutes the inline mathematical decoder +for an extracted function definition. + +For a first review of the verification machinery, follow this order: + +| Start here | Question answered | +| --- | --- | +| A Rust `aeneas` fence | What behavior does the author promise? | +| `RustModel.lean`, `DeriveModels.lean` | How are representations admitted and interpreted? | +| `SpecsSyntax.lean` | What proposition does the fence mean? | +| `Proofs/Util.lean` or a named theorem in `Proofs.lean` | Why does the extracted computation satisfy it? | +| `Obligations.lean`, `RequiredContracts.lean` | Does that promise retain the independently stated behavior and domain? | +| `Check.lean`, `run.sh` | How does CI check that every source annotation has the required proofs? | +| `copy_back.py`, `workspace.py` | How can native Lean editing update the authoritative Rust comments safely? | + +When reviewing specifications, follow the +[agent review guidance](../../skills/zerocopy-review/SKILL.md#specification-meaning): +reconstruct the intended guarantee and check whether the written domain and +postconditions could promise less. Passing proofs establish the written claims; +they do not establish that those claims capture the intended behavior. + +The comments in these modules explain their inputs, outputs, and non-obvious +checks. This integration proves conditional functional +behavior; it does not prove that every caller meets the requirements or that +zerocopy's unsafe pointer operations are sound. + +## Scope and proofs + +Rust review entry points: [arithmetic assertions](../../zerocopy/src/util/checks.rs), [nested reference](../../zerocopy/src/layout/nested_reference.rs). + +Extraction starts from the function and nominal-type owners of every present +`aeneas` fence in `zerocopy/src`, including their dependencies. Every present +fence must have a supported Rust owner, exact extraction binding, and the +corresponding checked Lean declarations. Unannotated functions and impls can be +added without extending the specification set. + +| Rust function | Checked property | +| --- | --- | +| `max` | Returns the mathematical maximum, selects an input, and bounds both inputs from above. | +| `min` | Returns the mathematical minimum, selects an input, and bounds both inputs from below. | +| `padding_needed_for` | For power-of-two alignment, returns padding below it and exactly `(align - len % align) % align`, the least padding making the sum aligned, with zero padding exactly when the input is aligned. | +| `round_down_to_next_multiple_of_alignment` | For power-of-two alignment, returns exactly `n - n % align`, the greatest aligned value at most `n`; the next multiple exceeds `n`. | +| Alignment/phase encoder and decoders | Power-of-two alignment, bounded phase, and exact encoding round-trip. | + +Every registered function uses total `spec`: accepted raw representations, +supplied mathematical ghosts, and explicit requirements imply successful +termination, successful output decoding, and the postconditions. These are +quantified conditional contracts, not finite test inputs. Broader ordinary Raw +lemmas retain useful representation-level domains. + +`Corollaries.lean` composes the arithmetic proofs: extrema preserve a shared +predicate, and round-down is monotone, aligned and idempotent under its stated +conditions. + +`LayoutMath.lean` supplies independent unbounded size and capacity formulas. + +The mathematical layout semantics proves normalization across arbitrary +nesting and metadata values. These algebraic laws do not themselves verify +a Rust layout method. + +The nominal rounding wrapper decodes to `RoundingValue`, a power-of-two +alignment and bounded phase with machine-fit proofs. Ordinary representation +laws establish acceptance of every positive stored word and exact reconstruction +from that pair. Zero is rejected by the native NonZero child decoder. + +Plain arithmetic clauses use mathematical word values carrying machine bounds +and NonZero positivity. Their Nat/Int arithmetic does not wrap; explicit raw +clauses preserve the extracted representation vocabulary while retaining +automatic input and output decoding. + +CI uses the default features, debug assertions and the runner's native target. +Local replay also supports macOS arm64. These conditional contracts do not +certify other extraction configurations, rustc, zerocopy's pointer safety or +whole-crate unsafe behavior. + +## Reproduce + +Install Nix, `rustup`, Python 3.9+, Git, curl, tar, and zstd, then run from the repository +root: + +```bash +bash verification/aeneas/setup.sh +bash verification/aeneas/run.sh +bash verification/aeneas/negative-controls.sh +python3 -B -m unittest discover -s verification/aeneas/tests +``` + +The runner builds `tools/aeneas-inline` with the pinned compiler. To run that +tool's own tests, use: + +```bash +source verification/aeneas/toolchain.sh +CARGO_TARGET_DIR=target/aeneas/annotation-tool \ + cargo +"$AENEAS_RUST_TOOLCHAIN" test --locked \ + --manifest-path tools/Cargo.toml -p aeneas-inline +``` + +The local failure-control command checks both models sequentially. To select +one isolated project, pass `golden-verification` or `verification`. CI runs +these two suites concurrently, waits for both, and reports both logs; either +failure fails the job. + +`setup.sh` downloads SHA-256-checked release archives, builds the pinned +Aeneas source with the checked-in CLI patch using its locked Nix dependencies, +and installs the matching Rust compiler with `rustc-dev` and `rust-src`. It fetches the pinned Lean +backend's Mathlib dependencies using its supplied lockfile. Cache downloads +are limited to imports of the backend and handwritten proof modules, +together with their dependencies. + +The resulting tools live in ignored `target/aeneas/toolchain`. To reuse an +existing installation with the same bundle layout, set `AENEAS_TOOLCHAIN_DIR`. +`run.sh` checks tool versions before extraction. It runs Charon with +`--preset=aeneas --sysroot default`, using Aeneas's builtin models for standard +library operations instead of regenerating a sysroot. Cargo resolves the +library's dependencies from its checked-in vendor configuration, offline and +locked. Charon is a compiler driver and selects its own Rust version; it does +not use zerocopy's ordinary Cargo wrapper or its stable/nightly aliases. + +Every run replaces `target/aeneas/verification` (live extraction) and +`target/aeneas/golden-verification` (checked-in model), including their compiled +Lean artifacts. This prevents stale declarations or the other model's artifacts +from passing after source is removed or renamed. Both retain Lean sources, +handwritten models/proofs, and resolved Lake manifests for inspection; the live +directory also retains LLBC. External templates are compared but never imported +into proofs as axioms. + +## Inline specifications and coverage + +Annotations are outer Rust doc attributes before the decorated item. The Rust +parser owns the association, including attributes between docs and the item. +Every reserved fence is accounted for: malformed or misspelled fences, unsupported +owners, duplicate declarations, and orphaned annotations fail. Literal `#[doc]` +attributes are also supported. An owner with a literal Aeneas annotation cannot +also carry computed doc strings. Direct `include_str!` in owner doc expressions +is rejected, including nested, qualified, and conditional invocations. Visible +reserved literal fragments in computed docs are rejected even when split across +strings. Conditional and inner doc forms with reserved fences also fail. +Unannotated computed documentation otherwise retains its ordinary Rust meaning. +Macro-produced documentation is outside the annotation language: the checker +does not expand macros or claim to inspect arbitrary external text they produce. + +````rust +/// ```aeneas +/// spec identity_spec +/// ensures result => result = x +/// ``` +fn identity(x: T) -> T { x } +```` + +A function fence contains exactly one `spec` or `partial spec` declaration. +Only explicitly typed ghost binders are authored. The original Rust type +parameters, receiver `self`, and arguments come from the exact extracted +signature. Each original type parameter receives one `RustModel T` dictionary, +including unused and result-only parameters. + +Plain clauses expose value arguments as their mathematical models. Raw clauses +expose their original Aeneas representations: + +```text +requires name : proposition +requires(raw) name : proposition +ensures resultPattern => proposition +ensures(raw) resultPattern => proposition +``` + +Every requirement is a named implication premise; later clauses can use its +proof. Requirements precede all postconditions. Every postcondition contributes +a conjunct, and all plain postconditions share one decoded output witness. +Ghost types elaborate in mathematical input scope, so `(position : Fin self.length)` +can depend on a decoded sequence. Ghosts may depend on earlier ghosts. Inputs, +ghosts, and earlier requirement names cannot be shadowed. No ghost is an argument +to the extracted function or is selected after execution. + +The proposition retains the original contiguous raw-input telescope. It then +introduces modeling dictionaries, mathematical inputs, decoding equations, +ghosts, and requirements before executing the original extracted function on +its original raw arguments. A successful payload must decode, even when every +postcondition is raw or is `True`. Total specifications require successful +termination and exclude panic and divergence. Explicit `partial spec` permits +divergence while excluding panic and checking every successful return. + +A type fence defines one mathematical shape and one local decoder: + +````rust +/// ```aeneas +/// model AlignValue where +/// value : Nat +/// power_of_two : value.isPowerOfTwo +/// decode? self => +/// if h : self.value.value.isPowerOfTwo then +/// some { value := self.value.value, power_of_two := h } +/// else none +/// ``` +struct Align { value: NonZeroUsize } +```` + +The alternative `model Name := ExistingType` associates an existing mathematical +type. `decode self => term` is locally infallible; `decode? self => term` may +reject the decoded fields. Both consume the owner's generated `Fields` carrier. +The full decoder first decodes every field of a structure or the active enum +constructor, including fields ignored by the local decoder. Child rejection +rejects the entire value. There is no independent validity dictionary or predicate. +An unannotated nominal owner's model is its own `Fields` carrier. `model Name := Fields` +uses that same representation under an authored name. Different Rust owners +retain distinct default carriers; aliases inherit their carrier's model. + +Native adapters carry exact scalar values and machine bounds, plus positivity +or nonzero proofs for NonZero types. Products, options, Rust results, lists, +slices, and arrays traverse their child providers and preserve their constructors +and lengths. Rust `Option.none` and Rust `Result.Err` are ordinary successfully +decoded cases; Aeneas panic/failure remains a separate execution outcome. +Unsupported opaque leaves, recursive nominal groups, indexed carriers, +trait/const generics, and escaping borrow continuations fail explicitly. +Generic shapes take mathematical type parameters: `Box.Fields TModel` needs +only its child carrier type. Authored generic model declarations bind `TModel` +for each Rust parameter `T`; decoder bodies and function specifications retain +the real dictionaries and `ModelOf T`. A field of type `Box` can therefore +declare its mathematical shape before Child's decoder exists. Slice and array +shapes retain their length bounds without a child decoder. Full decoding still +requires the actual child providers. + +Each generated nominal decoder has a `decode_decompose` theorem. It characterizes +successful decoding by existential decoded child values, their child decoder +equations, and the local `decodeFields` equation. Enum cases follow their active +constructor. These witnesses remain present when a local model discards fields; +no reconstruction or injectivity assumption is required. + +Automatic decoding uses directly named native/nominal providers, fixed container +combinators, and the original generic dictionary variables. Providers are selected +before decoded witnesses and ghosts and retained explicitly, so a later ghost +instance cannot redirect admission or output meaning. Ordinary instances remain +available for authored expressions and helper lemmas. No global instance-selection +comparison is part of the automatic-decoding audit. + +Generated modules follow the proof-construction dependency: + +```text +raw Types + ModelPrelude → ModelShapes → ModelSupport → Models → Specs + Proofs +``` + +`ModelShapes` declares nominal mathematical fields and inline model types in +verified extraction order. Handwritten `ModelSupport` modules can supply ordinary +Lean helpers. Lean enforces an acyclic import graph; there is no additional ban on +harmless dependencies on extracted functions. `Models` elaborates local decoders +and composes full decoders and named providers. The generated shapes, decoders, +and specifications all carry Rust source maps. Every handwritten module is +discovered and audited; generated modules are excluded from that discovery. +Handwritten module path components must be simple identifiers +(`[A-Za-z_][A-Za-z_0-9]*`); literal dots, whitespace, and quoted components in +filenames are rejected so distinct Lean Name segments cannot be conflated by the +file inventory. + +These are conditional contracts over accepted representations, supplied ghosts, +and satisfied requirements. A decoder may intentionally forget observations or +reject raw values. A ghost with type `False`, or a dependent ghost with an empty +domain, makes the relevant contract vacuous. Neither the pipeline nor a decoder +proves that every Rust caller meets the domain. Operation-specific +alignment and fit conditions remain explicit. The independently written operation expectations check both intended input +admission and promised behavior. Representation laws can be useful proof helpers; +they are not an additional universal model-certification requirement. + +The fully qualified model application is generated and cannot be supplied in +Rust. Charon's root identity, exact source body, source path, and signature must +match the annotation, including inherent Self types and inactive `cfg` +alternatives. Discovery retains exact inspected source snapshots; later digest, +binding, and assembly steps reject changes to any inspected input. The source +digest includes unannotated dependency files. Every +contents-bearing Charon-local file must agree with its inspected snapshot, so an +unchanged caller cannot hide a changed dependency. Pinned external `/rustc` files +without source contents remain within the translation premise. +The elaborated call must pass exactly its original Rust input +variables, with no swapping, coercion, or substitution. Introduced signatures are +inspectable in the generated Lean propositions and native editor. + +The source annotations determine the extraction roots. Removing an annotation +and its associated proof intentionally removes that claim from the checked set; +the source and generated golden changes make the reduced coverage reviewable. +There is no separately maintained function or type coverage roster. Nominal +types are still registered and audited independently from their specifications. + +Every present function annotation must match its exact extracted function. +Called functions and constants remain part of the complete dependency model. +Model +names, theorem identities, filenames, and dependency edges are derived rather +than stored as additional synchronized inventory fields. Discovery scans all +repository Rust sources except generated and vendored directories. A Rust +lexer distinguishes comments from strings and `syn` establishes ownership +before conditional compilation. + +No parser can recognize every informal prose claim as a proof. The `aeneas` +fence is the reserved CI-checked convention. Near-miss guard detection and the +independent policy protect that convention without treating all mathematical +prose as annotations. + +## Native Lean proofs and independent checks + +`Specs.lean` is generated from the Rust comments in a fixed context containing +the extracted model and mathematical vocabulary. Each `Zerocopy.Specs.*` is a +transparent proposition. The module is elaborated before proof modules are +imported, so proof declarations cannot change what the comment means. + +Handwritten proofs are ordinary checked-in `.lean` modules. Their theorem types +refer directly to generated propositions rather than restating them: + +```lean +theorem example_spec : Zerocopy.Specs.example_spec := by + intro input h + ... +``` + +Changing the Rust specification changes this theorem's goal. An old proof is +accepted only when Lean still establishes that goal. Normal imports and Lean +declaration order assemble proofs. There are no proof templates, insertion +slots, or hand-maintained dependency lists. + +Each function annotation requires its canonical exported proof and an independent +proposition in `Obligations.lean`. Missing canonical proofs, stale `Specs` +references, duplicate declarations, and unrelated required theorem types fail. +Additional helper theorems are allowed in the configured proof modules. The +required checks prove that the inline contract implies its independently authored +expectation for **every execution outcome**, before specializing that implication +to the proved implementation. The generated `Specs._contract` family +abstracts only the execution node; the audit checks that every original premise, +decoding check, and postcondition remains unchanged. Handwritten +`Obligations._contract` families independently state the expected domain and +result. Their concrete `Obligations.` aliases are checked to specialize the +same families to the original calls. + +This comparison cannot pass merely because a weaker specification and the +expected behavior happen to hold for the current implementation. It also catches +an unexpectedly narrowed domain. The generated checks retain both an +`_adequate` implication and an `_checked` concrete theorem for kernel +and axiom audits. Every canonical theorem must directly name its inline `Specs` +proposition; the audit requires both independent witnesses in the generated checks +module. It derives required theorems from compiled specifications, without a +separate generated roster. The expectations are reviewable sanity checks on +specification meaning, not an additional axiom or proof language. + +The audit also compares the existing verified Rust-to-model mapping in +`bindings.json` with the functions actually called by compiled specifications +declared in `Specs`. Every mapped function must have exactly one such +specification; missing, extra, or repeated functions fail. Each specification +in turn requires its canonical theorem and independent required theorem. +The same versioned table records every extracted nominal owner's source, +raw carrier, Fields carrier, mathematical type, decoder, and provider. The compiled +audit checks those identities and their generated-module ownership in both +directions, independently scans raw nominal types, and rejects missing or extra +provider registrations. This reuses one verified table without another coverage roster. + +`Check.lean` derives dependencies from elaborated theorem types and terms, +following helpers across handwritten modules, including private helpers, and +writes `proof-dependencies.json` in each proof workspace. It imports and audits all +declarations from every handwritten Lean module, including unused auxiliary +modules, together with the model, vocabulary, obligations, and generated +specifications. Only `propext`, `Classical.choice`, and `Quot.sound` are +permitted. +New axioms, `sorryAx`, and native evaluator proof axioms fail. + +Callers reuse ordinary exported theorems, either explicitly with `step with` or +through Aeneas's existing `step` registration. For a theorem whose type is a +transparent `Specs` proposition, `register_spec_step theorem_name` supplies a +kernel-checked alias with the expanded type for that registry. The canonical +theorem keeps its ordinary `Specs` type. Golden caller proofs use golden +callee theorems; live caller proofs use live callee theorems. Both models are +built in separate fresh workspaces from the same handwritten source, never +from shared compiled proofs or substituted model definitions. Handwritten +modules must not create import cycles through generated `Required` or `Check`. + +## Decoder proof completion + +Model fields declare ordinary Lean constraints. A decoder can supply only its +data and use native `..` to omit proof fields: + +```lean +model RoundingValue where + align : Nat + phase : Nat + align_pow2 : align.isPowerOfTwo + phase_lt : phase < align + fits : align + phase ≤ Usize.max +decode self => + let word := self._0.value + let align := 2 ^ Nat.log2 word + { align := align, phase := word - align, .. } +``` + +Both `decode` and `decode?` implicitly use `model_value` completion on the whole +body; outside decoders write `model_value { ... , .. }` explicitly. Native +elaboration creates the omitted-field goals, so no field discovery or defaults +are attached to model declarations. Every remaining goal must be a proposition. +Missing data (including a field of type `Prop`) is an error. Proof search uses +ordinary assumptions, arithmetic, and shared logarithm/word-bound facts with a +maximum of 100000 Lean heartbeats; explicit field proofs and complete `by` +bodies retain their ordinary meaning. + +Proofs are completed in their own local contexts, including `let` bindings and +conditional hypotheses, even in nested records or `some { ... , .. }`. An +unproved constraint fails compilation, including in a fallible decoder; it +never becomes `none` or changes the decoder's domain. Ordinary proof fields +ensure that chosen data satisfy the model constraints. The independent meaning +and admission laws still check what those data describe and which raw values +are accepted. `ModelCompletionTests.lean` covers both behaviors. + +## Development + +Run `bash verification/aeneas/dev.sh` to prepare the stable development project from the +checked-in golden model. Add `--live` to freshly extract the Rust implementation +and use that model instead. This extraction mode prepares editable goals even +when proofs are unfinished; it does not claim that golden comparison or proof +checks have passed. Add `--check` to build the development project and audit its +proofs. + +The project keeps handwritten proof modules at their checked-in paths and +generates the selected extraction, `ModelShapes.lean`, `Models.lean`, and +`Specs.lean` for native Lean editing. +Incremental Lake builds reuse +that project's compiled dependencies. The generated source map connects +specification diagnostics to the Rust comments; proof diagnostics already point +to their checked-in Lean source. + +Development regeneration records a version 2 baseline of the three inline +projections and their owned Rust doc payloads, and refuses to overwrite local +edits before extraction or project writes. Edit only the marked authored +regions: specifications in `Specs`, model shapes in `ModelShapes`, and the +`decode`/`decode?` clause in `Models`. A type's shape and decoder copy back +together. Generated imports, `for` targets, and derive/check commands remain +unchanged. Handwritten proofs continue to be edited at their real paths. + +Preview the exact Rust diff, then apply it for one full Rust owner: + +```sh +bash verification/aeneas/dev.sh --copy-back zerocopy::util::padding_needed_for --dry-run +bash verification/aeneas/dev.sh --copy-back zerocopy::util::padding_needed_for +``` + +Copy-back reads the existing project without regeneration, extraction, or +builds, and cannot be combined with `--live` or `--check`. Preview writes +nothing. Apply updates one annotation in one Rust file, preserving its doc +indentation, newline style, other source bytes, and file permissions. It accepts +only contiguous, uniformly indented `///` fences; block comments and literal +doc attributes remain readable but require manual editing. The saved source +snapshot and owned spans authorize the edit; source maps are diagnostic only. +Edits outside authored regions and changes to the original Rust file fail +without writes. Authored edits must retain the formatting produced by generation; +copy-back rejects formatting that would be discarded on the next regeneration. Other owners' edits stay protected until copied back in turn; +then rerun the development command to regenerate from Rust. + +Rust and its projection baseline are replaced separately. If interruption +occurs after writing Rust, rerun the same copy-back command with unchanged +Lean projections: it recognizes the exact planned Rust result and finishes +updating the baseline. Keep the baseline and edited projections together for +this recovery. There is no force option. + +Older version 1 baselines still protect edited projections, but cannot copy +back automatically. Clean regeneration upgrades them. For unbaselined or +older edited projections, preserve the files and manually move authored +fragments into the Rust doc fences before establishing a clean baseline. +Unannotated structural models have no authored fragment; edit their Rust type +or the generator. + +After a Lake build, inspect an effective compiled specification with: + +```sh +python3 -B verification/aeneas/workspace.py inspect verification/aeneas/lean \ + --root "$PWD" --spec encoding_components_spec +``` + +This reads compiled declarations without running another build. It displays the +actual proposition, original raw call, selected input/result providers and +decoding equations, total/partial WP judgment, all binder types, compiled axiom +dependencies, existing proof graph, and available extraction metadata. Mathematical +witnesses and ghosts are shown as binders; proposition-valued ghosts and named +requirements have the same universal meaning and are displayed together. Rust/ABI +correspondence assumptions are documented in `SEMANTICS.md` when that document is +present; the scope and translation trust boundary are described above. Missing +extraction provenance is reported as unavailable; legacy LLBC metadata is explicitly uncertified against +compiled imports. An edited source may differ from its compiled declaration; +refresh and audit with `dev.sh --check` before relying on inspection. This command +provides inspection, not a new verification result. + +Edit a specification in Rust and run the development command again to refresh +its generated proposition. Edit its proof directly in Lean. No proof copying +back to Rust is required. This development cache is for iteration; CI always +regenerates specifications and builds both models in fresh isolated projects. + +## Mathematical views and indexed loops + +### Arithmetic, mathematical views, and indexed loops + +Checked addition, subtraction, and multiplication already have upstream +`step_pure` specifications. Use `step as ⟨result, facts⟩` and split the `Option` +result to obtain both the exact successful value and the overflow condition. +`SupportTests.lean` exercises all inputs, including overflow. + +A contract may express equality through a pure mathematical view: + +```lean +theorem operation_spec (input : Input) (h : valid input) : + operation input ⦃ result => + view result = mathematicalOperation (view input) ∧ canonical result ⦄ := by + ... +``` + +This ordinary theorem states the existing total WP postcondition directly. +A view-only theorem states just the view equality. Partial correctness uses +`⦄div` in place of `⦄`: it explicitly permits divergence and still excludes +failure. These are Aeneas's existing specification operators. Use `WP.spec_mono` +to adapt an existing result specification to a view, and register useful caller +specifications with `attribute [step] operation_spec`. The existing Aeneas +registry then lets callers use `step` without specifying the theorem manually. + +`AeneasContracts.indexed_loop_spec` specializes Aeneas's `loop.spec_decr_nat` +to a state and `Usize` index. Supply a view, the mathematical value of each +prefix, and a representation invariant. Each continuing body step must advance +the index by exactly one and establish the next prefix; a completed step must +be at the length. The adapter supplies bounds and the decreasing `length - index` +termination measure. `SupportTests.lean` contains an independent example. + +The specification syntax, examples, and required-contract proof terms +participate in the axiom audit. Failure controls challenge an incorrect view, +divergence under a total view contract, a stationary loop index, an incorrect +prefix step, and continuing past the loop bound. + +## Updating models and fuzzy comparison + +`golden/` stores all four complete generated Aeneas modules, including types, +function bodies, trait dictionaries, constants, and external templates. There +are no per-function model slots or separate scaffolding. Handwritten external +models remain under `lean/Zerocopy/` and templates are never imported as axioms. + +Regenerate the checked-in output with: + +```bash +bash verification/aeneas/run.sh --update-goldens +``` + +The command extracts live output and checks its proofs and independent required +propositions before updating the complete generated module set. It then checks +comparison and both model builds. Rust comments, handwritten proofs, and +executable Rust are preserved. Review and commit the changed goldens alongside +the Rust or pin changes that prompted them. Ordinary CI never updates them. + +To add coverage, add the fenced specification, its ordinary Lean theorem, and an +independent required proposition. +Regenerate the complete goldens. Each annotation's model declaration is checked +in the complete generated modules; missing or duplicate declarations fail. + +Fuzzy comparison ignores line and nested block comments, including source +locations, empty nonliteral lines, and trailing horizontal whitespace outside +literals. It preserves code, indentation, nonempty line boundaries, interior +whitespace, and string/character/quoted-identifier contents. Unsupported raw or +interpolated strings and malformed comments/literals fail closed. Missing or +additional generated files, changes to external-template signatures, and code +changes fail with a normalized diff and require regeneration. + +CI compiles the checked-in model and the unmodified live model in separate fresh +Lake workspaces. Both must prove all inline specifications and any checked-in composition +corollaries and pass required checks and the complete axiom audit. The normalized +text is used only for comparison, never compilation. This independently checks +that accepted differences preserve the proved properties; it does not by itself +prove full semantic equivalence beyond those properties. + +## Pins and upgrade procedure + +The selected release is +[`nightly-2026.10.01-ca282ec`](https://github.com/AeneasVerif/aeneas/releases/tag/nightly-2026.10.01-ca282ec). +`toolchain.sh` records its full Aeneas commit, the compatible Charon commit from +[`charon-pin`](https://github.com/AeneasVerif/aeneas/blob/ca282ec2312a96f6a985a4d23a663690816d3f3e/charon-pin), +Rust `nightly-2026-09-17`, Lean `4.31.0`, and platform archive digests. The release +supplies prebuilt Aeneas/Charon binaries and compiled Lean backend artifacts. +The checked-in patch exposes the existing `Config.use_tuple_structs` option as +`-use-tuple-structs false`, which our extraction selects. The upstream default +remains unchanged. Source and patch digests, a distinct tool version, and +`tests/nominal-tuples.sh` keep this change reproducible and check constructors, +projections, patterns, updates, and default-mode compatibility. +Its `lake-manifest.json` pins Mathlib and transitive dependencies; the generated +proof workspace shares those fetched dependencies through path entries instead +of resolving a second copy. + +Upgrade this tuple together. Verify archive digests, resolve the new +`charon-pin`, inspect that Charon revision's Rust toolchain and Aeneas's Lean +version, and run extraction and all proofs from fresh output. Review new +external templates, the generated helper bodies, proof statements, and axiom +results. Remove or adapt the naming workaround only after inspecting the new +output. Do not pair Aeneas with an independently selected Charon nightly. + +## Trust boundary and failure checks + +These are Lean theorems about generated functions. Transferring them to Rust +requires the following external correspondence premises, which this CI job +records but does not formally prove: + +- Rust MIR generation and Charon's LLBC extraction preserve these selected + functions' semantics for the pinned configuration. +- Aeneas's translation and the pinned integer, comparison, wrapping-subtraction, + checked-addition, bitwise, and assertion models represent those Rust + operations faithfully. The translated layout records and variants represent + their stored fields, not physical Rust layout or niche encoding. +- The handwritten `NonZero` wrapper represents its stored value; + `NonZero::get` succeeds with that value and `NonZeroUsizeInner::clone` + succeeds with a copy. These models abstract values, not niche encoding, + layout, validity, or memory operations. The wrapper over-approximates valid + Rust values by admitting zero; the generated conditional contracts assume + successful recursive decoding of inputs and prove it for returned values. + This introduces no axiom asserting that every modeled wrapper is nonzero. +- Lean's kernel, its standard logic axioms, and the imported proof artifacts + check the encoded propositions correctly. Release checksums establish + artifact identity, not a proof of compiler or model correctness. + +Failure controls challenge missing annotations and proofs, orphaned proofs, +incorrect binding and parameter order, malformed specifications, admitted +proofs, unrelated theorem types, changed generated callees, and comments or +whitespace accepted by fuzzy comparison. Each model must pass the same controls. +Control families are selected from available model capabilities. Once selected, +a missing fixture or mutation target is an error. Semantic model mutations must +compile before their proof failures count as evidence; malformed mutants fail +the control run. Valid weaker contracts and weakened outcome predicates must +also be rejected. Controls edit and restore scratch workspaces, then recheck the +original results. + +Extraction rejects warnings, translation errors, missing artifacts, and LLBC +whose `has_errors` is not exactly `false`, even when Charon exits zero. +`prepare.py` repairs one pinned Aeneas naming collision: `ZeroablePrimitive` has +two `Copy` parent dictionaries that Aeneas names `markerCopyInst`. The second +projection and initializer are renamed `innerCopyInst`; dictionary types and +values are preserved and no function body changes. Unexpected output fails. + +## Research used + +The upstream [Aeneas README](https://github.com/AeneasVerif/aeneas) describes the +Charon-to-LLBC-to-Lean pipeline, required compatible pins, external model +workflow, and safe-subset limitations. The repository's `reference` branch +provided useful prior evidence: + +- [Compatible bundle drift](https://github.com/google/zerocopy/blob/ba4556c30835b56d108b0a6854a760bccf8438c5/reports/aeneas-compatible-bundle-source-drift-2026-09-30/REPORT.md) + identifies the newer Lean module output and the need to resolve Charon from + Aeneas's pin rather than release dates. +- [External models](https://github.com/google/zerocopy/blob/ba4556c30835b56d108b0a6854a760bccf8438c5/reports/aeneas-external-models-nightly-2026-06-03/REPORT.md) + explains the generated templates and the semantic boundary of model + registrations. +- [Whole-library Charon extraction](https://github.com/google/zerocopy/blob/ba4556c30835b56d108b0a6854a760bccf8438c5/reports/charon-zerocopy-library-repeat-order-nightly-2026-05-31/REPORT.md) + observed successful process exits with `has_errors: true`, supporting the + explicit LLBC completeness check. + +Those older experiments are not assumed to describe the current release's +runtime. This integration was validated with the pinned October 1 bundle and +fresh translations of the current checkout. diff --git a/verification/aeneas/build-aeneas.nix b/verification/aeneas/build-aeneas.nix new file mode 100644 index 0000000000..a521524afd --- /dev/null +++ b/verification/aeneas/build-aeneas.nix @@ -0,0 +1,34 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +# Use the upstream locked compiler/dependencies, changing only the version and +# dropping the post-install Charon symlink. The existing release supplies Charon. +let + upstream = builtins.getFlake (builtins.getEnv "AENEAS_SOURCE_DIR"); + system = builtins.currentSystem; + pkgs = import upstream.inputs.nixpkgs { inherit system; }; + base = if pkgs.stdenv.isLinux + then upstream.packages.${system}.aeneas-static + else upstream.packages.${system}.aeneas; + aeneas = base.overrideAttrs (_: { + AENEAS_VERSION = builtins.getEnv "AENEAS_VERSION"; + postInstall = ""; + }); +in pkgs.runCommand "aeneas-nominal-bundle" { + nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isDarwin + [ pkgs.macdylibbundler ]; +} '' + mkdir -p $out + cp ${aeneas}/bin/aeneas $out/aeneas + # macOS needs the executable's dynamic libraries alongside it after copying + # out of the Nix store. Linux uses the upstream static package instead. + ${pkgs.lib.optionalString pkgs.stdenv.isDarwin '' + chmod +w $out/aeneas + dylibbundler -od -b -x $out/aeneas -d $out/libs -p @executable_path/libs + ''} +'' diff --git a/verification/aeneas/build-aeneas.sh b/verification/aeneas/build-aeneas.sh new file mode 100755 index 0000000000..71d1f306c0 --- /dev/null +++ b/verification/aeneas/build-aeneas.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. +set -euo pipefail +cd "$(dirname "$0")/../.." +source verification/aeneas/toolchain.sh +[[ $# == 1 && -d "$1" ]] || { echo "Usage: $0 TOOLCHAIN_DIRECTORY" >&2; exit 1; } +tools_dir=$(cd "$1" && pwd) +command -v nix >/dev/null || { echo 'Building the patched Aeneas requires Nix.' >&2; exit 1; } +mkdir -p target/aeneas +staging=$(mktemp -d "$PWD/target/aeneas/build.XXXXXX") +trap 'rm -rf "$staging"' EXIT +# Check both producer inputs before applying the CLI patch. The build record +# below also hashes the resulting executable so inspection can identify it. +check_hash() { + python3 - "$1" "$2" <<'PY' +import hashlib, sys +from pathlib import Path +actual = hashlib.sha256(Path(sys.argv[1]).read_bytes()).hexdigest() +if actual != sys.argv[2]: + raise SystemExit(f"SHA-256 mismatch for {sys.argv[1]}: {actual}") +PY +} +curl --fail --location --silent --show-error --retry 5 \ + "https://codeload.github.com/AeneasVerif/aeneas/tar.gz/$AENEAS_REV" \ + -o "$staging/source.tar.gz" +check_hash "$staging/source.tar.gz" "$AENEAS_SOURCE_SHA256" +check_hash verification/aeneas/patches/use-tuple-structs.patch "$AENEAS_PATCH_SHA256" +tar -xzf "$staging/source.tar.gz" -C "$staging" +export AENEAS_SOURCE_DIR="$staging/aeneas-$AENEAS_REV" +export AENEAS_VERSION +( + cd "$AENEAS_SOURCE_DIR" + patch -F 0 -p1 < "$OLDPWD/verification/aeneas/patches/use-tuple-structs.patch" +) +# No update/override of flake inputs: the source archive includes flake.lock. +# Limit simultaneous derivations and compiler jobs on small developer machines. +artifact=$(nix --extra-experimental-features "nix-command flakes" build --impure --file verification/aeneas/build-aeneas.nix \ + --no-link --print-out-paths --option max-jobs 1 --option cores 2) +[[ $("$artifact/aeneas" -version) == "aeneas $AENEAS_VERSION" ]] +"$artifact/aeneas" -help | python3 -c 'import sys; assert "-use-tuple-structs" in sys.stdin.read()' +[[ $("$tools_dir/aeneas" -version) == "aeneas $AENEAS_RELEASE" ]] +# Preserve the unpatched release executable as the compatibility oracle for +# nominal-tuples.sh; the patched default must still produce its exact output. +cp "$tools_dir/aeneas" "$tools_dir/aeneas-upstream" +cp "$artifact/aeneas" "$tools_dir/aeneas" +if [[ -d "$artifact/libs" ]]; then + mkdir -p "$tools_dir/libs" + cp "$artifact/libs/"* "$tools_dir/libs/" +fi +python3 - "$tools_dir" "$AENEAS_REV" "$AENEAS_SOURCE_SHA256" "$AENEAS_PATCH_SHA256" "$AENEAS_VERSION" <<'PY' +import hashlib, json, sys +from pathlib import Path +root = Path(sys.argv[1]) +provenance = dict(zip(("source_rev", "source_sha256", "patch_sha256", "version"), sys.argv[2:])) +provenance["binary_sha256"] = hashlib.sha256((root / "aeneas").read_bytes()).hexdigest() +(root / "aeneas-build.json").write_text(json.dumps(provenance, indent=2) + "\n") +PY diff --git a/verification/aeneas/copy_back.py b/verification/aeneas/copy_back.py new file mode 100644 index 0000000000..3e6ea44d83 --- /dev/null +++ b/verification/aeneas/copy_back.py @@ -0,0 +1,373 @@ +#!/usr/bin/env python3 +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +"""Copy one marked development projection into its original owned /// fence. + +The persisted generation snapshot is reverse-edit authority. Source maps only +explain Lean diagnostics. Rust and the baseline have separate atomic writes; +an interrupted baseline write is reconciled against the exact planned Rust. +""" + +import copy +import difflib +import hashlib +import json +import os +from pathlib import Path +import re +import stat +import tempfile + +MODULES = ('ModelShapes', 'Models', 'Specs') +STATE = '.inline-projections.json' +OWNER = r'zerocopy(?:::[A-Za-z_][A-Za-z_0-9]*)+' +MARK = re.compile(r'^-- aeneas-copy-(begin|end) (' + OWNER + r') (spec|shape|decoder)\n', re.M) +PART_MODULE = {'spec': 'Specs', 'shape': 'ModelShapes', 'decoder': 'Models'} + + +def digest(text): + """Hash exact UTF-8 projection text, including its line endings and framing.""" + return hashlib.sha256(text.encode('utf-8')).hexdigest() + + +def regions(text): + """Return strict marker framing, rejecting nested, fake and duplicate marks.""" + pieces, values, position, active = [], {}, 0, None + matches = list(MARK.finditer(text)) + # Even a malformed or embedded lookalike must not become authored content. + if text.count('aeneas-copy-') != len(matches): + raise ValueError('Malformed copy-back marker') + for match in matches: + key = (match[2], match[3]) + if match[1] == 'begin': + if active is not None or key in values: + raise ValueError('Nested or duplicate copy-back marker') + pieces.append(text[position:match.end()]) + active = (key, match.end()) + else: + if active is None or active[0] != key: + raise ValueError('Unmatched copy-back marker') + values[key] = text[active[1]:match.start()] + pieces.append(key) + position, active = match.start(), None + if active is not None: + raise ValueError('Unterminated copy-back marker') + pieces.append(text[position:]) + return pieces, values + + +def render(pieces, values): + """Fill checked regions while preserving every generated framing byte.""" + return ''.join(values[piece] if isinstance(piece, tuple) else piece for piece in pieces) + + +def authored(entry, values): + """Recover one payload while preserving generated application and formatting. + + Parsing alone is insufficient: forward assembly must reproduce every edited + byte. Check that round trip before allowing the payload to replace Rust. + """ + import inline + owner = entry['rust'] + if entry['kind'] == 'function': + text = values[(owner, 'spec')] + lines = text.splitlines(keepends=True) + application = entry['for_line'] + '\n' + if lines.count(application) != 1: + raise ValueError('Generated for target must remain exactly unchanged') + application_index = lines.index(application) + lines.remove(application) + payload = ''.join(lines) + _, clause_line = inline.parse_spec(payload, entry['generics'], entry['inputs']) + if application_index != clause_line: + raise ValueError('Generated for target must remain immediately before the first specification clause') + # Match assemble's projection rules before acknowledging edited text. + # A later regeneration must reproduce every selected authored byte. + projected = payload.rstrip('\n').split('\n') + projected.insert(clause_line, entry['for_line']) + if text != '\n'.join(projected) + '\n': + raise ValueError('Specification formatting would change on regeneration; ' + 'remove trailing blank lines from the authored region and retry') + else: + payload = values[(owner, 'shape')].rstrip('\n') + '\n' + values[(owner, 'decoder')] + model = inline.parse_model(payload, entry['generics']) + shape = '\n'.join(model['shape_text'].splitlines()) + '\n' + decoder = (f'{model["decoder_mode"]} {model["value_binder"]} =>\n' + + ''.join(' ' + line + '\n' for line in model['decoder_text'].splitlines())) + if values[(owner, 'shape')] != shape or values[(owner, 'decoder')] != decoder: + raise ValueError('Model formatting would change on regeneration; remove trailing blank lines, ' + 'put the decode/decode? body on separate lines indented by two spaces, and retry') + if not payload.endswith('\n') or '\r' in payload or '```' in payload: + raise ValueError('Authored payload requires LF lines without Rust fence delimiters') + return payload + + +def record(work, annotations): + """Save complete projections and inspected Rust, not just diagnostic maps. + + The hashes detect changes; the saved text and physical spans explain which + bytes an owner may change. Validate the assembled authority before saving it. + """ + projections = {name: (work / f'{name}.lean').read_bytes().decode('utf-8') for name in MODULES} + all_values = {} + for text in projections.values(): + all_values.update(regions(text)[1]) + owners, sources = {}, {} + for rust, annotation in annotations.items(): + entry = {key: annotation[key] for key in ('rust', 'file', 'kind', 'generics', 'inputs', 'copy_span')} + entry['payload'] = annotation['spec_text'] if entry['kind'] == 'function' else annotation['model_text'] + if entry['kind'] == 'function': + candidates = [line for line in all_values[(rust, 'spec')].splitlines() if line.startswith(' for ')] + if len(candidates) != 1: + raise ValueError('Missing generated specification application') + entry['for_line'] = candidates[0] + owners[rust] = entry + sources[entry['file']] = annotations.source_snapshots[entry['file']].decode('utf-8') + state = {'version': 2, 'sha256': {name: digest(text) for name, text in projections.items()}, + 'projections': projections, 'owners': owners, 'sources': sources} + validate(state) + (work / STATE).write_text(json.dumps(state, ensure_ascii=False, indent=2) + '\n') + + +def validate(state): + """Validate saved authority and return its parsed layouts and authored bytes. + + A baseline must agree in both directions: every marker has a saved owner, + and every owner has exactly its expected regions. Physical spans must also + reproduce the saved Rust payload. Callers may reuse the checked parse after + this function succeeds; no portion becomes authority on an early failure. + """ + import inline + try: + if (not isinstance(state, dict) or set(state) != {'version', 'sha256', 'projections', 'owners', 'sources'} + or state['version'] != 2 or set(state['sha256']) != set(MODULES) + or set(state['projections']) != set(MODULES) or not state['owners']): + raise ValueError('Copy-back requires a complete version 2 baseline; regenerate clean projections to upgrade') + values, projections = {}, {} + for module in MODULES: + text = state['projections'][module] + if not isinstance(text, str) or state['sha256'][module] != digest(text): + raise ValueError('Projection baseline digest disagrees with saved text') + layout, parts = regions(text) + if any(PART_MODULE[part] != module for _, part in parts): + raise ValueError('Projection region is in the wrong module') + values.update(parts) + projections[module] = (layout, parts) + expected, spans = set(), {} + if set(state['sources']) != {e['file'] for e in state['owners'].values()}: + raise ValueError('Source snapshot roster disagrees with owners') + for owner, entry in state['owners'].items(): + keys = {'rust', 'file', 'kind', 'generics', 'inputs', 'copy_span', 'payload'} + if entry.get('kind') == 'function': + keys.add('for_line') + if (set(entry) != keys or entry['rust'] != owner or not re.fullmatch(OWNER, owner) + or entry['kind'] not in ('function', 'type') or not inline.valid_file(entry['file']) + or any(not isinstance(entry[k], list) or any(not isinstance(n, str) or + not re.fullmatch(inline.IDENTIFIER, n) for n in entry[k]) for k in ('generics', 'inputs'))): + raise ValueError('Invalid saved annotation ownership') + expected.update((owner, part) for part in (['spec'] if entry['kind'] == 'function' else ['shape', 'decoder'])) + # Decode the original authored projection independently of Rust's doc + # spelling. Inline decoders are canonically expanded onto two lines. + original = authored(entry, values) + if entry['kind'] == 'function': + # Forward assembly strips final empty specification lines; the + # complete source payload remains authoritative for no-op edits. + equivalent = original == entry['payload'].rstrip('\n') + '\n' + else: + before = inline.parse_model(entry['payload'], entry['generics']) + after = inline.parse_model(original, entry['generics']) + equivalent = all(before[k] == after[k] for k in ('shape_text', 'decoder_mode', 'value_binder', 'decoder_text')) + if not equivalent: + raise ValueError('Saved projection disagrees with original annotation') + span = entry['copy_span'] + if span is None: + continue # Existing block/attribute docs remain readable, not writable. + if (not isinstance(span, dict) or set(span) != {'start', 'end', 'prefix', 'newline'} + or type(span['start']) is not int or type(span['end']) is not int + or not 0 <= span['start'] < span['end'] + or not re.fullmatch(r'[ \t]*/// ?', span['prefix']) or span['newline'] not in ('\n', '\r\n')): + raise ValueError('Invalid owned doc payload span') + source = state['sources'][entry['file']].encode('utf-8') + if span['end'] > len(source): + raise ValueError('Owned doc span escapes source snapshot') + text = source[span['start']:span['end']].decode('utf-8') + lines = text.splitlines(keepends=True) + if (not lines or any(not line.startswith(span['prefix']) or not line.endswith(span['newline']) for line in lines) + or ''.join(line[len(span['prefix']):-len(span['newline'])] + '\n' for line in lines) != entry['payload'] + or (span['start'] and source[span['start'] - 1:span['start']] != b'\n') + or not re.fullmatch(rb'[ \t]*/// ```aeneas\r?\n', source[:span['start']].splitlines(keepends=True)[-1]) + or not re.match(rb'[ \t]*/// ```\r?\n', source[span['end']:])): + raise ValueError('Owned doc payload disagrees with original source fence') + spans.setdefault(entry['file'], []).append((span['start'], span['end'])) + if set(values) != expected: + raise ValueError('Copy-back marker roster disagrees with saved ownership') + for file_spans in spans.values(): + ordered = sorted(file_spans) + if any(a[1] > b[0] for a, b in zip(ordered, ordered[1:])): + raise ValueError('Owned annotation payload spans overlap') + return projections, values + except (KeyError, TypeError, AttributeError, UnicodeError, IndexError) as error: + raise ValueError('Malformed projection baseline') from error + + +def checked_file(root, relative): + """Reject escaping and symlinks; remember parent and file identities.""" + path = Path(relative) + if path.is_absolute() or not path.parts or any(p in ('.', '..') for p in path.parts): + raise ValueError('Copy-back path must stay inside the repository') + parents = [] + current = root + for part in (None, *path.parts): + if part is not None: + current = current / part + info = current.lstat() + if stat.S_ISLNK(info.st_mode): + raise ValueError(f'Copy-back refuses symlink traversal: {current}') + parents.append((current, info.st_dev, info.st_ino, info.st_mode)) + if not stat.S_ISREG(info.st_mode): + raise ValueError(f'Copy-back requires a regular file: {current}') + return {'path': current, 'identities': parents, 'bytes': current.read_bytes(), 'mode': stat.S_IMODE(info.st_mode)} + + +def unchanged(record): + """Recheck every traversed identity and the contents before a replacement. + + Equal bytes alone do not authorize replacing a different inode or following + a parent that has become a symlink since the preflight read. + """ + for path, device, inode, mode in record['identities']: + info = path.lstat() + if (info.st_dev, info.st_ino, info.st_mode) != (device, inode, mode): + raise ValueError(f'Copy-back input identity changed: {path}') + if record['path'].read_bytes() != record['bytes']: + raise ValueError(f'Copy-back input changed: {record["path"]}') + + +def replace(record, contents, guard): + """Prepare a durable same-directory file, then guard and replace atomically. + + Preserve permissions and recheck inputs after preparing the temporary file. + Always remove a leftover temporary file if preparation or the guard fails. + """ + path = record['path'] + descriptor, temporary = tempfile.mkstemp(prefix='.aeneas-copy-', dir=path.parent) + try: + with os.fdopen(descriptor, 'wb') as stream: + os.fchmod(stream.fileno(), record['mode']) + stream.write(contents) + stream.flush() + os.fsync(stream.fileno()) + guard() + os.replace(temporary, path) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + + +def copy_back(root, work, owner, apply=False, after_source_write=None): + """Plan one owner's reverse edit before writing either source or baseline. + + Current Rust must equal the saved snapshot or the exact planned replacement. + The second case lets a retry complete the baseline after an interrupted + source write. Other owners' pending projection edits remain pending, and + all generated framing must still match the saved generation. + """ + root, work = Path(root).absolute(), Path(work).absolute() + try: + relative_work = work.relative_to(root) + except ValueError as error: + raise ValueError('Copy-back project must be inside the repository') from error + if not re.fullmatch(OWNER, owner): + raise ValueError('Copy-back requires the full Rust owner identity') + control = checked_file(root, relative_work / STATE) + try: + state = json.loads(control['bytes']) + except (ValueError, UnicodeError) as error: + raise ValueError('Malformed projection baseline') from error + baseline_projections, baseline_values = validate(state) + if owner not in state['owners']: + raise ValueError('Rust owner is absent from the generation baseline') + entry = state['owners'][owner] + if entry['copy_span'] is None: + raise ValueError('Copy-back supports only contiguous uniformly indented /// fences; other doc forms are read-only') + records, current_values, layouts = [control], {}, {} + for module in MODULES: + record_ = checked_file(root, relative_work / f'{module}.lean') + records.append(record_) + current = record_['bytes'].decode('utf-8') + layout, values = regions(current) + original_layout, original_values = baseline_projections[module] + if layout != original_layout or values.keys() != original_values.keys(): + raise ValueError(f'Edits outside authored regions in {module}.lean are unsupported') + layouts[module] = original_layout + current_values.update(values) + payload = authored(entry, current_values) + source = checked_file(root, entry['file']) + records.append(source) + before = state['sources'][entry['file']].encode('utf-8') + span = entry['copy_span'] + # Keep a physically unchanged fence unchanged, including inline decode + # spelling, when the selected authored regions have no edits. + selected = {key for key in baseline_values if key[0] == owner} + edited = any(current_values[key] != baseline_values[key] for key in selected) + if not edited: + payload = entry['payload'] + replacement = ''.join(span['prefix'] + line + span['newline'] for line in payload[:-1].split('\n')).encode('utf-8') + after = before[:span['start']] + replacement + before[span['end']:] + if source['bytes'] not in (before, after): + raise ValueError('Rust source changed since generation; copy-back has no writes') + diff = ''.join(difflib.unified_diff(before.decode('utf-8').splitlines(keepends=True), + after.decode('utf-8').splitlines(keepends=True), + fromfile=entry['file'], tofile=entry['file'])) + print(diff, end='') + if not diff: + print(f'No Rust payload changes for {owner}.') + if not apply: + return diff + updated = copy.deepcopy(state) + for module, layout in layouts.items(): + values = dict(baseline_projections[module][1]) + values.update({key: current_values[key] for key in selected if key in values}) + updated['projections'][module] = render(layout, values) + updated['sha256'][module] = digest(updated['projections'][module]) + delta = len(replacement) - (span['end'] - span['start']) + updated['sources'][entry['file']] = after.decode('utf-8') + updated['owners'][owner]['payload'] = payload + for other, annotation in updated['owners'].items(): + other_span = annotation['copy_span'] + if annotation['file'] != entry['file'] or other_span is None: + continue + if other == owner: + other_span['end'] += delta + elif other_span['start'] >= span['end']: + other_span['start'] += delta + other_span['end'] += delta + validate(updated) + + # Finish every parsing, ownership, formatting, and conflict check before + # either atomic write. Rust and the baseline cannot be replaced together. + # Accepting exactly the planned after-source lets a retry finish a baseline + # write interrupted after Rust was replaced, without accepting other edits. + + def guard(): + for record_ in records: + unchanged(record_) + + guard() + if source['bytes'] != after: + replace(source, after, guard) + records[-1] = checked_file(root, entry['file']) + if records[-1]['bytes'] != after: + raise ValueError('Rust source changed after replacement; retain projections and baseline for recovery') + if after_source_write is not None: + after_source_write() + encoded = (json.dumps(updated, ensure_ascii=False, indent=2) + '\n').encode('utf-8') + if encoded != control['bytes']: + replace(control, encoded, guard) + return diff diff --git a/verification/aeneas/dev.sh b/verification/aeneas/dev.sh new file mode 100644 index 0000000000..786f5c2317 --- /dev/null +++ b/verification/aeneas/dev.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +# Prepare the tracked Lean sources for native editor and incremental Lake use. +set -euo pipefail +check=false +live=false +copy_owner="" +dry_run=false +while (($#)); do + case "$1" in + --check) check=true ;; + --live) live=true ;; + --dry-run) dry_run=true ;; + --copy-back) + if [[ $# -lt 2 || -n "$copy_owner" || "$2" == --* ]]; then + echo "--copy-back requires one full Rust owner" >&2; exit 1 + fi + copy_owner=$2 + shift ;; + *) echo "Usage: $0 [--check] [--live] | --copy-back OWNER [--dry-run]" >&2; exit 1 ;; + esac + shift +done +if [[ -n "$copy_owner" ]]; then + if "$check" || "$live"; then + echo "--copy-back cannot be combined with --check or --live" >&2; exit 1 + fi +elif "$dry_run"; then + echo "--dry-run requires --copy-back" >&2; exit 1 +fi +cd "$(dirname "$0")/../.." +repo=$PWD +if [[ -n "$copy_owner" ]]; then + copy_args=(copy-back "$repo/verification/aeneas/lean" --root "$repo" --owner "$copy_owner") + if ! "$dry_run"; then copy_args+=(--apply); fi + python3 -B verification/aeneas/workspace.py "${copy_args[@]}" + exit +fi +source verification/aeneas/toolchain.sh +tools_dir=${AENEAS_TOOLCHAIN_DIR:-"$repo/target/aeneas/toolchain"} +backend="$tools_dir/backends/lean" +[[ $(cat "$backend/lean-toolchain") == "$AENEAS_LEAN_TOOLCHAIN" ]] +export PATH="$tools_dir/lean/bin:$PATH" +work="$repo/verification/aeneas/lean" +python3 -B verification/aeneas/workspace.py protect "$work" --root "$repo" +if "$live"; then + # Refresh the model even when changed Rust invalidates an existing proof. + bash verification/aeneas/run.sh --extract-only + model="$repo/target/aeneas/verification/Zerocopy" +else + model="$repo/verification/aeneas/golden" +fi +CARGO_TARGET_DIR="$repo/target/aeneas/annotation-tool" \ + cargo +"$AENEAS_RUST_TOOLCHAIN" build --locked --jobs 2 \ + --manifest-path tools/Cargo.toml -p aeneas-inline +# The golden checker owns the generated-file roster. Validate the complete +# producer output before copying those same modules into the editor project; +# handwritten external implementations stay with the authored proof sources. +python3 -B - "$model" "$work/Zerocopy" <<'PY' +from pathlib import Path +import shutil, sys +sys.path.insert(0, 'verification/aeneas') +import golden +source, destination = map(Path, sys.argv[1:]) +generated = golden.files(source, live=True) +destination.mkdir(parents=True, exist_ok=True) +for name in generated: + shutil.copyfile(source / name, destination / name) +PY +cp "$backend/lean-toolchain" "$work/lean-toolchain" +python3 -B verification/aeneas/inline.py dev-assemble --root "$repo" \ + --tool "$repo/target/aeneas/annotation-tool/debug/aeneas-inline" --work "$work" +python3 -B verification/aeneas/prepare.py workspace "$work" --backend "$backend" +echo "Lean development project: $work" +echo "Model: $model; specifications refreshed from current Rust comments." +echo "Development checks do not replace the fresh golden/live CI verification." +if "$check"; then + python3 -B verification/aeneas/workspace.py check "$work" --root "$repo" +fi diff --git a/verification/aeneas/golden.py b/verification/aeneas/golden.py new file mode 100644 index 0000000000..4ab15c4825 --- /dev/null +++ b/verification/aeneas/golden.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +"""Compare complete Aeneas outputs while ignoring only presentation changes. + +The file roster includes external primitive templates because their signatures +belong to the translation boundary. Live projects may also contain handwritten +external implementations; those are proof inputs, not generated snapshots. +This comparison checks text. run.sh separately compiles proofs against each +definition set rather than treating normalized text equality as a proof. +""" + +import argparse +import difflib +import re +from pathlib import Path + +FILES = ("Types.lean", "Funs.lean", "TypesExternal_Template.lean", + "FunsExternal_Template.lean") +HANDWRITTEN = {"TypesExternal.lean", "FunsExternal.lean"} +CHAR = re.compile(r"'(?:\\(?:u\{[0-9a-fA-F]+\}|.)|[^'\\\r\n])'") +HEADER = """/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +""" + + +def normalize(source): + """Remove only comments, empty code lines, and trailing nonliteral spaces. + + Literal contents and indentation survive because either can change meaning. + Unknown literal forms fail explicitly: an extractor upgrade must receive a + reviewed lexer change before its output can pass this comparison. + """ + # Preserve code, indentation, line boundaries, and literal bytes. Comments + # become a space (not concatenation), retaining their newlines. Only empty + # nonliteral lines and nonliteral trailing horizontal spaces are dropped. + lines = [] + line = [] + i = 0 + + def emit(char, literal=False): + if char == "\n": + finish() + else: + line.append((char, literal)) + + def finish(): + while line and line[-1][0] in " \t\r" and not line[-1][1]: + line.pop() + if line: + lines.append("".join(char for char, _ in line)) + line.clear() + + while i < len(source): + if source.startswith("--", i): + end = source.find("\n", i) + end = len(source) if end == -1 else end + emit(" ") + i = end + elif source.startswith("/-", i): + depth = 1 + emit(" ") + i += 2 + while depth: + if i == len(source): + raise ValueError("Unterminated Lean block comment") + if source.startswith("/-", i): + depth += 1 + i += 2 + elif source.startswith("-/", i): + depth -= 1 + i += 2 + else: + if source[i] == "\n": + emit("\n") + i += 1 + elif source[i] == '"' or source[i] == "«": + end_char = '"' if source[i] == '"' else "»" + # Raw and interpolated strings need their own lexer. This pinned + # generator emits neither; fail closed if an upgrade adds them. + if end_char == '"' and (re.search(r"r#*$", source[:i]) or + source[:i].endswith('!')): + raise ValueError("Unsupported raw or interpolated Lean string") + emit(source[i], True) + i += 1 + while True: + if i == len(source): + raise ValueError("Unterminated Lean literal") + char = source[i] + # Preserve empty lines INSIDE literals, unlike empty code lines. + if char == "\n" and not line: + line.append(("", True)) + emit(char, True) + i += 1 + if char == end_char: + break + if char == "\\" and end_char == '"': + if i == len(source): + raise ValueError("Unterminated Lean escape") + emit(source[i], True) + i += 1 + elif source[i] == "'" and (match := CHAR.match(source, i)): + for char in match[0]: + emit(char, True) + i = match.end() + else: + emit(source[i]) + i += 1 + finish() + return lines + + +def files(directory, live=False): + """Read exactly the generated modules, allowing authored externals in live. + + Reject missing or additional files before returning any module. Callers use + this same roster for comparison, golden updates, and editor projections. + """ + actual = {str(p.relative_to(directory)) for p in directory.rglob("*") if p.is_file()} + if live: + actual -= HANDWRITTEN + if actual != set(FILES): + raise ValueError(f"Unexpected generated file set in {directory}: " + f"missing={sorted(set(FILES) - actual)}, " + f"extra={sorted(actual - set(FILES))}") + return {name: (directory / name).read_bytes().decode("utf-8") for name in FILES} + + +def compare(live, golden): + """Report every normalized module difference, including template signatures.""" + generated = files(live, live=True) + checked_in = files(golden) + differences = [] + for name in FILES: + before, after = normalize(checked_in[name]), normalize(generated[name]) + if before != after: + differences.extend(difflib.unified_diff( + before, after, fromfile=f"golden/{name}", + tofile=f"live/{name}", lineterm="")) + return differences + + +def update(live, golden): + """Write a validated extraction; run.sh checks its live proofs before calling.""" + generated = files(live, live=True) + # Validate the complete candidate before changing any checked-in file. + for source in generated.values(): + normalize(source) + if golden.exists(): + actual = {str(p.relative_to(golden)) for p in golden.rglob("*") if p.is_file()} + if actual - set(FILES): + raise ValueError(f"Unexpected checked-in generated files: {sorted(actual - set(FILES))}") + golden.mkdir(parents=True, exist_ok=True) + for name in FILES: + (golden / name).write_text(HEADER + generated[name], encoding="utf-8") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("command", choices=["compare", "update"]) + parser.add_argument("live", type=Path) + parser.add_argument("golden", type=Path) + args = parser.parse_args() + if args.command == "update": + update(args.live, args.golden) + else: + differences = compare(args.live, args.golden) + if differences: + print("\n".join(differences)) + raise SystemExit("Aeneas goldens differ. Review the extraction and run " + "bash verification/aeneas/run.sh --update-goldens") + print("Live Aeneas output matches goldens under the fuzzy comparison") + + +if __name__ == "__main__": + main() diff --git a/verification/aeneas/golden/Funs.lean b/verification/aeneas/golden/Funs.lean new file mode 100644 index 0000000000..1fa172b4b2 --- /dev/null +++ b/verification/aeneas/golden/Funs.lean @@ -0,0 +1,345 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +-- THIS FILE WAS AUTOMATICALLY GENERATED BY AENEAS +-- [zerocopy]: function definitions +module +public import Aeneas +public import Zerocopy.Types +public import Zerocopy.FunsExternal +@[expose] public section +open Aeneas Aeneas.Std Result ControlFlow Error +set_option linter.dupNamespace false +set_option linter.hashCommand false +set_option linter.unusedVariables false +set_option linter.style.whitespace false +set_option linter.style.setOption false +set_option linter.style.longLine false + +/- You can set the `maxHeartbeats` value with the `-max-heartbeats` CLI option -/ +set_option maxHeartbeats 1000000 + +/- You can set the `maxRecDepth` value with the `-max-recdepth` CLI option -/ +set_option maxRecDepth 2048 + +/- You can remove the following line by using the CLI option `-all-computable`: -/ +noncomputable section + +namespace Zerocopy + +/-- Trait implementation: [core::num::niche_types::{impl core::clone::Clone for core::num::niche_types::NonZeroUsizeInner}] + Source: '/rustc/library/core/src/num/niche_types.rs', lines 17:17-17:22 + Name pattern: [core::clone::Clone] -/ +@[reducible, rust_trait_impl + "core::clone::Clone"] +def core.num.niche_types.NonZeroUsizeInner.Insts.CoreCloneClone : + core.clone.Clone core.num.niche_types.NonZeroUsizeInner := { + clone := core.num.niche_types.NonZeroUsizeInner.Insts.CoreCloneClone.clone +} + +/-- Trait implementation: [core::num::niche_types::{impl core::marker::Copy for core::num::niche_types::NonZeroUsizeInner}] + Source: '/rustc/library/core/src/num/niche_types.rs', lines 17:24-17:28 + Name pattern: [core::marker::Copy] -/ +@[reducible, rust_trait_impl + "core::marker::Copy"] +def core.num.niche_types.NonZeroUsizeInner.Insts.CoreMarkerCopy : + core.marker.Copy core.num.niche_types.NonZeroUsizeInner := { + cloneInst := core.num.niche_types.NonZeroUsizeInner.Insts.CoreCloneClone +} + +/-- Trait implementation: [core::num::nonzero::{impl core::num::nonzero::ZeroablePrimitive for usize}] + Source: '/rustc/library/core/src/num/nonzero.rs', lines 47:12-47:56 + Name pattern: [core::num::nonzero::ZeroablePrimitive] -/ +@[reducible, rust_trait_impl + "core::num::nonzero::ZeroablePrimitive"] +def Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner : + core.num.nonzero.ZeroablePrimitive Std.Usize + core.num.niche_types.NonZeroUsizeInner := { + markerCopyInst := core.marker.CopyUsize + innerCopyInst := core.num.niche_types.NonZeroUsizeInner.Insts.CoreMarkerCopy +} + +/-- [zerocopy::layout::POINTER_WIDTH_BITS] + Source: 'src/layout/mod.rs', lines 20:0-20:62 -/ +@[global_simps, irreducible] +def layout.POINTER_WIDTH_BITS : Result Std.Usize := do + let i ← core.mem.size_of Std.Usize + i * 8#usize + +/-- [zerocopy::layout::{zerocopy::layout::RoundingAlignAndPhase}::new]: + Source: 'src/layout/mod.rs', lines 100:4-119:5 -/ +def layout.RoundingAlignAndPhase.new + (align : core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner) (phase : Std.Usize) : + Result layout.RoundingAlignAndPhase + := do + let i ← + core.num.nonzero.NonZero.get + Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner align + let b ← core.num.Usize.is_power_of_two i + massert b + massert (phase < i) + let encoded ← lift (i ||| phase) + let o ← + core.num.nonzero.NonZero.new + Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner encoded + match o with + | none => fail panic + | some encoded1 => ok { _0 := encoded1 } + +/-- [zerocopy::layout::{zerocopy::layout::RoundingAlignAndPhase}::components]: + Source: 'src/layout/mod.rs', lines 133:4-160:5 -/ +def layout.RoundingAlignAndPhase.components + (self : layout.RoundingAlignAndPhase) : + Result ((core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner) × Std.Usize) + := do + let i ← layout.POINTER_WIDTH_BITS + let i1 ← i - 1#usize + let i2 ← + core.num.nonzero.NonZero.get + Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner self._0 + let i3 ← lift (core.num.Usize.leading_zeros i2) + let i4 ← lift (UScalar.cast .Usize i3) + let shift ← i1 - i4 + let align ← 1#usize <<< shift + let o ← + core.num.nonzero.NonZero.new + Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner align + match o with + | none => fail panic + | some align1 => + let i5 ← + core.num.nonzero.NonZero.get + Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner align1 + let phase ← lift (i2 ^^^ i5) + ok (align1, phase) + +/-- [zerocopy::layout::{zerocopy::layout::RoundingAlignAndPhase}::align]: + Source: 'src/layout/mod.rs', lines 173:4-182:5 -/ +def layout.RoundingAlignAndPhase.align + (self : layout.RoundingAlignAndPhase) : + Result (core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner) + := do + let (nz, _) ← layout.RoundingAlignAndPhase.components self + ok nz + +/-- [zerocopy::layout::nested_reference::round_up]: + Source: 'src/layout/nested_reference.rs', lines 50:0-60:1 -/ +def layout.nested_reference.round_up + (size : Std.Usize) (align : Std.Usize) : Result (Option Std.Usize) := do + if align = 0#usize + then ok none + else + let remainder ← size % align + if remainder = 0#usize + then ok (some size) + else let i ← align - remainder + ok (Usize.checked_add size i) + +/-- [zerocopy::layout::nested_reference::apply_layer]: + Source: 'src/layout/nested_reference.rs', lines 62:0-84:1 -/ +def layout.nested_reference.apply_layer + (layer : layout.nested_reference.NestedLayer) (size : Std.Usize) + (alignment : Std.Usize) : + Result (Option (Std.Usize × Std.Usize)) + := do + let b ← core.num.Usize.is_power_of_two layer.packed + if b + then + let b1 ← core.num.Usize.is_power_of_two layer.min_align + if b1 + then + if layer.min_align > layer.packed + then ok none + else + let field_align ← + if alignment < layer.packed + then ok alignment + else ok layer.packed + let o ← + layout.nested_reference.round_up layer.prefix_bytes field_align + match o with + | none => ok none + | some offset => + let alignment1 ← + if layer.min_align < field_align + then ok field_align + else ok layer.min_align + let o1 ← lift (Usize.checked_add offset size) + match o1 with + | none => ok none + | some size1 => + let o2 ← layout.nested_reference.round_up size1 alignment1 + match o2 with + | none => ok none + | some size2 => ok (some (size2, alignment1)) + else ok none + else ok none + +/-- [zerocopy::layout::nested_reference::size_for_metadata]: loop body 0: + Source: 'src/layout/nested_reference.rs', lines 112:4-119:5 -/ +@[rust_loop_body] +def layout.nested_reference.size_for_metadata_loop.body + (leading : Slice layout.nested_reference.NestedLayer) + (state : Option (Std.Usize × Std.Usize)) (i : Std.Usize) : + Result (ControlFlow ((Option (Std.Usize × Std.Usize)) × Std.Usize) (Option + (Std.Usize × Std.Usize))) + := do + if i != 0#usize + then + let i1 ← i - 1#usize + let layer ← Slice.index_usize leading i1 + match state with + | none => ok (cont (none, i1)) + | some p => + let (size, alignment) := p + let state1 ← layout.nested_reference.apply_layer layer size alignment + ok (cont (state1, i1)) + else ok (done state) + +/-- [zerocopy::layout::nested_reference::size_for_metadata]: loop 0: + Source: 'src/layout/nested_reference.rs', lines 112:4-119:5 -/ +@[rust_loop] +def layout.nested_reference.size_for_metadata_loop + (leading : Slice layout.nested_reference.NestedLayer) + (state : Option (Std.Usize × Std.Usize)) (i : Std.Usize) : + Result (Option (Std.Usize × Std.Usize)) + := do + loop + (fun (state1, i1) => layout.nested_reference.size_for_metadata_loop.body + leading state1 i1) + (state, i) + +/-- [zerocopy::layout::nested_reference::size_for_metadata]: + Source: 'src/layout/nested_reference.rs', lines 98:0-124:1 -/ +def layout.nested_reference.size_for_metadata + (leading : Slice layout.nested_reference.NestedLayer) (elem_size : Std.Usize) + (leaf_align : Std.Usize) (elems : Std.Usize) : + Result (Option Std.Usize) + := do + let b ← core.num.Usize.is_power_of_two leaf_align + if b + then + let i ← elem_size % leaf_align + if i != 0#usize + then ok none + else + let o ← lift (Usize.checked_mul elem_size elems) + let state ← + match o with + | none => ok none + | some size => ok (some (size, leaf_align)) + let i1 := Slice.len leading + let state1 ← + layout.nested_reference.size_for_metadata_loop leading state i1 + match state1 with + | none => ok none + | some p => let (size, _) := p + ok (some size) + else ok none + +/-- [zerocopy::util::round_down_to_next_multiple_of_alignment]: + Source: 'src/util/mod.rs', lines 261:0-279:1 -/ +def util.round_down_to_next_multiple_of_alignment + (n : Std.Usize) + (align : core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner) : + Result Std.Usize + := do + let align1 ← + core.num.nonzero.NonZero.get + Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner align + let b ← core.num.Usize.is_power_of_two align1 + massert b + let i ← align1 - 1#usize + let mask ← lift (~~~ i) + ok (n &&& mask) + +/-- [zerocopy::util::padding_needed_for]: + Source: 'src/util/mod.rs', lines 169:0-229:1 -/ +def util.padding_needed_for + (len : Std.Usize) + (align : core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner) : + Result Std.Usize + := do + let i ← + core.num.nonzero.NonZero.get + Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner align + let mask ← i - 1#usize + let i1 ← lift (core.num.Usize.wrapping_sub len 1#usize) + let i2 ← lift (~~~ i1) + ok (i2 &&& mask) + +/-- [zerocopy::util::checks::check_arithmetic]: + Source: 'src/util/checks.rs', lines 38:0-46:1 -/ +def util.checks.check_arithmetic + (len : Std.Usize) + (align : core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner) : + Result Unit + := do + let i ← + core.num.nonzero.NonZero.get + Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner align + let b ← core.num.Usize.is_power_of_two i + if b + then + let remainder ← len % i + let i1 ← i - remainder + let padding ← i1 % i + let i2 ← util.padding_needed_for len align + massert (i2 = padding) + let i3 ← util.round_down_to_next_multiple_of_alignment len align + let i4 ← len - remainder + massert (i3 = i4) + else ok () + +/-- [zerocopy::util::max]: + Source: 'src/util/mod.rs', lines 289:0-295:1 -/ +def util.max + (a : core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner) + (b : core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner) : + Result (core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner) + := do + let i ← + core.num.nonzero.NonZero.get + Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner a + let i1 ← + core.num.nonzero.NonZero.get + Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner b + if i < i1 + then ok b + else ok a + +/-- [zerocopy::util::min]: + Source: 'src/util/mod.rs', lines 305:0-311:1 -/ +def util.min + (a : core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner) + (b : core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner) : + Result (core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner) + := do + let i ← + core.num.nonzero.NonZero.get + Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner a + let i1 ← + core.num.nonzero.NonZero.get + Usize.Insts.CoreNumNonzeroZeroablePrimitiveNonZeroUsizeInner b + if i > i1 + then ok b + else ok a + +end Zerocopy diff --git a/verification/aeneas/golden/FunsExternal_Template.lean b/verification/aeneas/golden/FunsExternal_Template.lean new file mode 100644 index 0000000000..03f8eb3085 --- /dev/null +++ b/verification/aeneas/golden/FunsExternal_Template.lean @@ -0,0 +1,70 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +-- THIS FILE WAS AUTOMATICALLY GENERATED BY AENEAS +-- [zerocopy]: external functions. +-- This is a template file: rename it to "FunsExternal.lean" and fill the holes. +module +public import Aeneas +public import Zerocopy.Types +@[expose] public section +open Aeneas Aeneas.Std Result ControlFlow Error +set_option linter.dupNamespace false +set_option linter.hashCommand false +set_option linter.unusedVariables false +set_option linter.style.whitespace false +set_option linter.style.setOption false +set_option linter.style.longLine false + +/- You can set the `maxHeartbeats` value with the `-max-heartbeats` CLI option -/ +set_option maxHeartbeats 1000000 + +/- You can set the `maxRecDepth` value with the `-max-recdepth` CLI option -/ +set_option maxRecDepth 2048 +open Zerocopy + +/-- [core::mem::size_of]: + Source: '/rustc/library/core/src/mem/mod.rs', lines 375:0-375:34 + Name pattern: [core::mem::size_of] + Visibility: public -/ +@[rust_fun "core::mem::size_of"] +axiom core.mem.size_of (T : Type) : Result Std.Usize + +/-- [core::num::niche_types::{impl core::clone::Clone for core::num::niche_types::NonZeroUsizeInner}::clone]: + Source: '/rustc/library/core/src/num/niche_types.rs', lines 17:17-17:22 + Name pattern: [core::num::niche_types::{core::clone::Clone}::clone] + Visibility: public -/ +@[rust_fun + "core::num::niche_types::{core::clone::Clone}::clone"] +axiom core.num.niche_types.NonZeroUsizeInner.Insts.CoreCloneClone.clone + : + core.num.niche_types.NonZeroUsizeInner → Result + core.num.niche_types.NonZeroUsizeInner + +/-- [core::num::nonzero::{core::num::nonzero::NonZero}::new]: + Source: '/rustc/library/core/src/num/nonzero.rs', lines 388:4-388:42 + Name pattern: [core::num::nonzero::{core::num::nonzero::NonZero<@T, @Clause0_NonZeroInner>}::new] + Visibility: public -/ +@[rust_fun + "core::num::nonzero::{core::num::nonzero::NonZero<@T, @Clause0_NonZeroInner>}::new"] +axiom core.num.nonzero.NonZero.new + {T : Type} {Clause0_NonZeroInner : Type} (ZeroablePrimitiveInst : + core.num.nonzero.ZeroablePrimitive T Clause0_NonZeroInner) : + T → Result (Option (core.num.nonzero.NonZero T Clause0_NonZeroInner)) + +/-- [core::num::nonzero::{core::num::nonzero::NonZero}::get]: + Source: '/rustc/library/core/src/num/nonzero.rs', lines 467:4-467:31 + Name pattern: [core::num::nonzero::{core::num::nonzero::NonZero<@T, @Clause0_NonZeroInner>}::get] + Visibility: public -/ +@[rust_fun + "core::num::nonzero::{core::num::nonzero::NonZero<@T, @Clause0_NonZeroInner>}::get"] +axiom core.num.nonzero.NonZero.get + {T : Type} {Clause0_NonZeroInner : Type} (ZeroablePrimitiveInst : + core.num.nonzero.ZeroablePrimitive T Clause0_NonZeroInner) : + core.num.nonzero.NonZero T Clause0_NonZeroInner → Result T + diff --git a/verification/aeneas/golden/Types.lean b/verification/aeneas/golden/Types.lean new file mode 100644 index 0000000000..2c2b95e9a7 --- /dev/null +++ b/verification/aeneas/golden/Types.lean @@ -0,0 +1,55 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +-- THIS FILE WAS AUTOMATICALLY GENERATED BY AENEAS +-- [zerocopy]: type definitions +module +public import Aeneas +public import Zerocopy.TypesExternal +@[expose] public section +open Aeneas Aeneas.Std Result ControlFlow Error +set_option linter.dupNamespace false +set_option linter.hashCommand false +set_option linter.unusedVariables false +set_option linter.style.whitespace false +set_option linter.style.setOption false +set_option linter.style.longLine false + +/- You can set the `maxHeartbeats` value with the `-max-heartbeats` CLI option -/ +set_option maxHeartbeats 1000000 + +/- You can set the `maxRecDepth` value with the `-max-recdepth` CLI option -/ +set_option maxRecDepth 2048 + +namespace Zerocopy + +/-- Trait declaration: [core::num::nonzero::ZeroablePrimitive] + Source: '/rustc/library/core/src/num/nonzero.rs', lines 34:0-34:59 + Name pattern: [core::num::nonzero::ZeroablePrimitive] + Visibility: public -/ +@[rust_trait "core::num::nonzero::ZeroablePrimitive" + (parentClauses := ["markerCopyInst", "innerCopyInst"])] +structure core.num.nonzero.ZeroablePrimitive (Self : Type) (Self_NonZeroInner : + Type) where + markerCopyInst : core.marker.Copy Self + innerCopyInst : core.marker.Copy Self_NonZeroInner + +/-- [zerocopy::layout::RoundingAlignAndPhase] + Source: 'src/layout/mod.rs', lines 81:0-81:54 -/ +structure layout.RoundingAlignAndPhase where + _0 : core.num.nonzero.NonZero Std.Usize + core.num.niche_types.NonZeroUsizeInner + +/-- [zerocopy::layout::nested_reference::NestedLayer] + Source: 'src/layout/nested_reference.rs', lines 31:0-35:1 -/ +structure layout.nested_reference.NestedLayer where + packed : Std.Usize + min_align : Std.Usize + prefix_bytes : Std.Usize + +end Zerocopy diff --git a/verification/aeneas/golden/TypesExternal_Template.lean b/verification/aeneas/golden/TypesExternal_Template.lean new file mode 100644 index 0000000000..eb785fcd09 --- /dev/null +++ b/verification/aeneas/golden/TypesExternal_Template.lean @@ -0,0 +1,42 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +-- THIS FILE WAS AUTOMATICALLY GENERATED BY AENEAS +-- [zerocopy]: external types. +-- This is a template file: rename it to "TypesExternal.lean" and fill the holes. +module +public import Aeneas +@[expose] public section +open Aeneas Aeneas.Std Result ControlFlow Error +set_option linter.dupNamespace false +set_option linter.hashCommand false +set_option linter.unusedVariables false +set_option linter.style.whitespace false +set_option linter.style.setOption false +set_option linter.style.longLine false + +/- You can set the `maxHeartbeats` value with the `-max-heartbeats` CLI option -/ +set_option maxHeartbeats 1000000 + +/- You can set the `maxRecDepth` value with the `-max-recdepth` CLI option -/ +set_option maxRecDepth 2048 + +/-- [core::num::niche_types::NonZeroUsizeInner] + Source: '/rustc/library/core/src/num/niche_types.rs', lines 20:8-20:55 + Name pattern: [core::num::niche_types::NonZeroUsizeInner] + Visibility: public -/ +@[rust_type "core::num::niche_types::NonZeroUsizeInner"] +axiom core.num.niche_types.NonZeroUsizeInner : Type + +/-- [core::num::nonzero::NonZero] + Source: '/rustc/library/core/src/num/nonzero.rs', lines 112:0-112:40 + Name pattern: [core::num::nonzero::NonZero] + Visibility: public -/ +@[rust_type "core::num::nonzero::NonZero"] +axiom core.num.nonzero.NonZero (T : Type) (Clause0_NonZeroInner : Type) : Type + diff --git a/verification/aeneas/inline.py b/verification/aeneas/inline.py new file mode 100644 index 0000000000..c7e70d50fd --- /dev/null +++ b/verification/aeneas/inline.py @@ -0,0 +1,1059 @@ +#!/usr/bin/env python3 +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +"""Bind inline contracts to inspected Rust and assemble their Lean projections. + +Rust syntax inspection establishes ownership; the independent comment audit +rejects reserved fences that ownership cannot explain. Extraction then binds +those owners to exact source snapshots, parameter order, and generated names. +Assembly consumes that checked binding rather than trusting generated doc names +alone. Development projections use the same authored bytes, with explicit edit +regions whose saved snapshots authorize owner-scoped copy-back. +""" + +import argparse +import difflib +import hashlib +import json +import os +import re +import subprocess +from pathlib import Path + +import golden +import workspace + +MARKER = re.compile(r'`{2,}\s*([^\s`]+)') +IDENTIFIER = r'[A-Za-z_][A-Za-z_0-9]*' + + +class Annotations(dict): + """Keep authored declarations alongside independently inspected source data. + + The dictionary contains annotations. `types` also retains unannotated nominal + owners; extraction selects exact supported owners into `extracted_types`. + `source_snapshots` fixes all inspected Rust, so a later source edit cannot + inherit an earlier discovery result. + """ + + def __init__(self): + super().__init__() + self.types = [] + self.extracted_types = None + self.source_snapshots = {} + + +class GeneratedLines(list): + """Keep generated lines and their optional Rust locations in one append. + + Maps use one-based Lean line numbers and may omit purely generated lines. + Recording a location before appending its line prevents framing commands or + development markers from shifting the mapping away from the intended line. + The list still preserves the exact ordering and blank lines of assembly. + """ + + def __init__(self, lines): + super().__init__(lines) + self.locations = {} + + def append(self, line, location=None): + if location is not None: + self.locations[str(len(self) + 1)] = location + super().append(line) + + +def suspicious(text): + """Flag reserved and near-miss fence labels for later ownership checks. + + This is discovery, not admission: only the exact aeneas fence is accepted. + A lookalike inside a Rust literal is excluded by syntax and lexer inspection. + """ + return any(word.lower().startswith('aeneas') or + difflib.SequenceMatcher(None, word.lower(), 'aeneas').ratio() >= 0.7 + for word in MARKER.findall(text)) + + +def read_source(path): + """Decode source bytes without translating CRLF line endings. + + Physical spelling matters to saved snapshots and reverse-edit spans. + """ + return path.read_bytes().decode('utf-8') + + +def rust_identity(file, syntax): + """Combine the source module path with the syntax owner path. + + lib.rs and mod.rs name their containing module; other filenames add a + segment. Extraction must later confirm this proposed identity. + """ + relative = Path(file).relative_to('zerocopy/src') + modules = list(relative.parent.parts) + if relative.stem not in ('mod', 'lib'): + modules.append(relative.stem) + return '::'.join(['zerocopy', *modules, syntax]) + + +def valid_file(file): + """Restrict model owners to relative Rust paths under zerocopy/src.""" + return (isinstance(file, str) and file.startswith('zerocopy/src/') + and '..' not in Path(file).parts and file.endswith('.rs')) + + +def source_files(root): + """Inspect all crate Rust sources and reserved-fence candidates elsewhere. + + Unannotated dependencies and inactive cfg alternatives still contribute to + the exact source snapshot. Disposable build and dependency trees are skipped. + """ + paths = set() + for directory, dirs, filenames in os.walk(root): + dirs[:] = [d for d in dirs if d not in {'.git', 'target', 'vendor', '.lake'}] + for name in filenames: + if name.endswith('.rs'): + path = Path(directory) / name + relative = str(path.relative_to(root)) + if (relative.startswith('zerocopy/src/') + or suspicious(read_source(path))): + paths.add(relative) + return sorted(paths) + + +def binder_header(text, position): + """Read our deliberately small named-binder subset; Lean checks the types.""" + names = [] + pairs = {'(': ')', '{': '}', '[': ']'} + while True: + while position < len(text) and text[position].isspace(): + position += 1 + if position == len(text) or text[position] not in pairs: + return names, position + start = position + stack = [pairs[text[position]]] + position += 1 + literal = None + while stack: + if position == len(text): + raise ValueError('Unterminated specification binder') + char = text[position] + if literal: + if char == '\\' and literal == '"': + position += 2 + continue + if char == literal: + literal = None + elif char in ('"', '«'): + literal = '"' if char == '"' else '»' + elif char in pairs: + stack.append(pairs[char]) + elif char in pairs.values(): + if char != stack.pop(): + raise ValueError('Mismatched specification binder') + position += 1 + declaration = text[start + 1:position - 1] + before_type, separator, _ = declaration.partition(':') + if not separator or not re.fullmatch(IDENTIFIER + r'(?:\s+' + IDENTIFIER + ')*', before_type.strip()): + raise ValueError('Specification binders must have explicit names and types') + names.extend(before_type.split()) + + +def code_tokens(text): + """Mask Lean literal contents after the shared lexer validates them.""" + chars = list(text) + index = 0 + while index < len(chars): + char = text[index] + literal = golden.CHAR.match(text, index) if char == "'" else None + if literal: + end = literal.end() + elif char in ('"', '«'): + close = '"' if char == '"' else '»' + end = index + 1 + while end < len(chars): + if text[end] == '\\' and close == '"': + end += 2 + elif text[end] == close: + end += 1 + break + else: + end += 1 + else: + index += 1 + continue + for offset in range(index, end): + if chars[offset] != '\n': + chars[offset] = ' ' + index = end + return ''.join(chars) + + +def checked_payload(text): + """Validate comments/literals and keep generated command framing private.""" + normalized = '\n'.join(golden.normalize(text)) + if re.search(r'\baeneas_(?:spec|model|invariant)_(?:begin|end)\b', code_tokens(normalized)): + raise ValueError('Annotation parser framing markers are reserved for generated Lean') + return normalized + + +COMMAND = (r'(?:for|proof:|model:|contract|theorem|def|abbrev|axiom|opaque|' + r'instance|namespace|section|end|import|open|export|attribute|' + r'set_option|syntax|macro|elab|run_elab|initialize|deriving|' + r'spec|partial|refines|invariant|model|decode\??|example|constant|inductive|structure|class|mutual|noncomputable|private|protected|public|meta|unsafe|notation|local|scoped|aeneas_\w+|derive_\w+|check_\w+|register_\w+)') + + +def reject_commands(text): + """Reject declarations that could escape the authored expression scope. + + Literal contents have been masked, so quoted Lean text does not accidentally + become a command. The accepted annotation subset remains deliberately small. + """ + if re.search(r'^\s*(?:' + COMMAND + r'(?=\s|:|$)|#)', code_tokens(text), re.M): + raise ValueError('Aeneas fence contains an unsupported application or declaration') + + +def parse_spec(text, generics, inputs): + """Validate one contract header and its ordered requirements and posts. + + Original Rust parameters are inferred, not authored binders. Ghosts and + named requirements must not shadow them. Lean checks expression types later. + """ + normalized = checked_payload(text) + match = re.match(r'^(?:partial )?spec (' + IDENTIFIER + r')(?=\s|\(|\{|$)', normalized) + if not match: + raise ValueError('Aeneas fence requires exactly one spec declaration') + names, body = binder_header(normalized, match.end()) + if len(set(names)) != len(names): + raise ValueError('Duplicate specification binder') + expected = generics + inputs + if set(names) & set(expected): + raise ValueError('Ghost binders must not shadow original Rust parameters') + if any(line and not line.startswith(' ') for line in text.splitlines()[1:]): + raise ValueError('Specification continuations must be indented by two spaces') + clauses = normalized[body:] + reject_commands(clauses) + if re.search(r'(?m)^\s*(?:requires|ensures)(?!\(raw\)|\s)', clauses): + raise ValueError('Unsupported clause mode') + headers = list(re.finditer(r'(?m)^ (requires|ensures)(\(raw\))?\s+', clauses)) + # binder_header skips leading whitespace; restore the first clause's line. + if re.match(r'(?:requires|ensures)(?:\(raw\))?\s+', clauses): + clauses = ' ' + clauses + headers = list(re.finditer(r'(?m)^ (requires|ensures)(\(raw\))?\s+', clauses)) + if not headers or clauses[:headers[0].start()].strip(): + raise ValueError('Specification requires a postcondition; clauses must start on their own lines') + requirements, seen_post = [], False + for index, header in enumerate(headers): + end = headers[index + 1].start() if index + 1 < len(headers) else len(clauses) + clause = clauses[header.end():end].strip() + if header[1] == 'requires': + if seen_post: + raise ValueError('Requirements must precede postconditions') + requirement = re.fullmatch(r'(' + IDENTIFIER + r')\s*:\s*(.+)', clause, re.S) + if not requirement: + raise ValueError('Requirements must have an explicit name and proposition') + requirements.append(requirement[1]) + else: + seen_post = True + if not re.fullmatch(r'.+?\s*=>\s*.+', clause, re.S): + raise ValueError('Postconditions require a result pattern and proposition') + if not seen_post: + raise ValueError('Specification requires at least one postcondition') + if len(set(requirements)) != len(requirements) or set(requirements) & set(names + expected): + raise ValueError('Requirement names must be unique and must not shadow specification binders') + _, original_body = binder_header(text, re.match(r'^(?:partial )?spec\s+' + IDENTIFIER, text).end()) + clause_line = text[:original_body].count('\n') + if clause_line == 0 or text[:original_body].rsplit('\n', 1)[1].strip(): + raise ValueError('Specification clauses must begin on their own indented lines') + return match[1], clause_line + + +def parse_model(text, generics): + """Separate one mathematical shape from its local decoder expression. + + Shapes use the original ordered type parameters. Both authored bodies are + expressions within generated commands, so additional declarations are rejected. + """ + checked_payload(text) + shape = re.match(r'^model (' + IDENTIFIER + r')\s*(where|:=)(?=\s|$)', text) + if not shape or shape[1] in [*generics, 'Fields']: + raise ValueError('Aeneas type fence requires one model name where fields or model name := type declaration') + decoders = list(re.finditer(r'^decode(\?)? (' + IDENTIFIER + r')\s*=>[ \t]*', text, re.M)) + if len(decoders) != 1: + raise ValueError('Aeneas model requires exactly one decode or decode? clause') + decoder = decoders[0] + if decoder[2] in generics: + raise ValueError('Decoder value must not shadow a Rust type parameter') + shape_text = text[:decoder.start()].rstrip('\n') + shape_body = text[shape.end():decoder.start()] + decoder_start = decoder.end() + (text[decoder.end():].startswith('\n')) + decoder_text = '\n'.join(line[2:] if line.startswith(' ') else line + for line in text[decoder_start:].rstrip('\n').splitlines()) + if not shape_body.strip() or not decoder_text.strip(): + raise ValueError('Model shapes and decoders must have nonempty bodies') + reject_commands(checked_payload(shape_body)) + reject_commands(checked_payload(decoder_text)) + for index, line in enumerate(text.splitlines()): + if index == 0 or index == text[:decoder.start()].count('\n'): + continue + if line and not line.startswith(' '): + raise ValueError('Model continuations must be indented by two spaces') + return {'model_name': shape[1], 'shape_text': shape_text, + 'decoder_mode': 'decode?' if decoder[1] else 'decode', + 'value_binder': decoder[2], 'decoder_text': decoder_text, + 'decoder_line': text[:decoder_start].count('\n'), + 'decoder_clause_line': text[:decoder.start()].count('\n'), + 'model_text': text} + + +def parse_file(source, syntax): + """Read literal owner docs, then account for every reserved source fence. + + Diagnostic lines describe decoded docs, but copy-back needs physical bytes. + Only contiguous, uniformly prefixed /// payloads receive writable spans. + Other accepted literal doc forms remain readable annotations. + """ + if syntax.get('source') != source: + raise ValueError('Rust source changed during annotation inspection; retry from a stable source snapshot') + + def char_offset(byte): + return len(source.encode('utf-8')[:byte].decode('utf-8')) + + found, owned = [], set() + owners = [*syntax['functions'], *syntax.get('types', [])] + for owner in owners: + if owner.get('include_str_docs'): + raise ValueError('Direct include_str! in owner doc expressions is unsupported; use literal outer docs') + docs = owner.get('docs', []) + if owner.get('computed_docs') and any(suspicious(doc['text']) for doc in docs): + raise ValueError('Computed doc strings on function or nominal type owners are unsupported; use literal outer docs') + content, lines = [], [] + for doc in docs: + owned.add((doc['start'], doc['end'])) + # /// desugars to a string beginning with one conventional space. + # Literal #[doc = "..."] uses the same decoded Rust string semantics. + decoded = doc['text'].splitlines() or [''] + raw_doc = source[char_offset(doc['start']):char_offset(doc['end'])] + block_stars = raw_doc.startswith('/**') and all( + not line.strip() or re.match(r'^\s*\*(?: |$)', line) for line in decoded) + for index, line in enumerate(decoded): + if block_stars: + line = re.sub(r'^\s*\*(?: |$)', '', line) + elif (raw_doc.startswith('///') or (raw_doc.startswith('/**') and index == 0)) and line.startswith(' '): + line = line[1:] + content.append(line) + lines.append(doc['line'] + index if source[char_offset(doc['start']):].startswith(('///', '/**')) else doc['line']) + index, count = 0, 0 + while index < len(content): + line = content[index] + if not suspicious(line): + index += 1 + continue + if line.rstrip() != '```aeneas': + raise ValueError('Malformed, unsupported, or misspelled aeneas fence') + if not owner['supported']: + raise ValueError('Aeneas fence has an unsupported Rust owner (including borrowed/backward forms)') + count += 1 + if count != 1: + raise ValueError('Duplicate Aeneas declarations on one Rust owner') + start = index + 1 + index = start + while index < len(content) and content[index].rstrip() != '```': + if suspicious(content[index]) or content[index].startswith('```'): + raise ValueError('Malformed or nested aeneas fence') + index += 1 + if index == len(content): + raise ValueError('Unterminated aeneas fence') + payload = '\n'.join(content[start:index]) + '\n' + kind = 'function' if owner.get('kind', 'function') == 'function' else 'type' + if kind == 'function': + name, clause_line = parse_spec(payload, owner['generics'], owner['inputs']) + extra = {'spec_text': payload, 'clause_line': clause_line, + 'ghosts': binder_header(payload, re.match(r'^(?:partial )?spec\s+' + IDENTIFIER, payload).end())[0]} + else: + extra = parse_model(payload, owner['generics']) + name = extra['model_name'] + extra.update(nominal_kind=owner['kind'], fields=owner['fields']) + close = char_offset(owner['close']) + declaration = char_offset(owner['start']) + # Reverse edits use owned physical doc spans, never diagnostic maps. + physical = source.split('\n') + physical = [line + '\n' for line in physical[:-1]] + ([physical[-1]] if physical[-1] else []) + selected = physical[lines[start] - 1:lines[index - 1]] + prefixes = [re.match(r'^([ \t]*/// ?)', line) for line in selected] + line_docs = {doc['line'] for doc in docs + if source[char_offset(doc['start']):char_offset(doc['end'])].startswith('///')} + copy_span = None + if (selected and len(selected) == index - start and all(prefixes) + and all(number in line_docs for number in lines[start:index]) + and len({m[1] for m in prefixes}) == 1 + and len({'\r\n' if line.endswith('\r\n') else '\n' for line in selected}) == 1): + begin = len(''.join(physical[:lines[start] - 1]).encode('utf-8')) + finish = begin + len(''.join(selected).encode('utf-8')) + copy_span = {'start': begin, 'end': finish, 'prefix': prefixes[0][1], + 'newline': '\r\n' if selected[0].endswith('\r\n') else '\n'} + found.append({'syntax': owner['path'], 'kind': kind, 'theorem': name, + 'copy_span': copy_span, + 'generics': owner['generics'], 'inputs': owner['inputs'], + 'source_lines': lines[start:index], + 'newline': '\r\n' if '\r\n' in source else '\n', + 'open_line': source[:declaration].count('\n') + 1, + 'end_line': source[:close].count('\n') + 1, + 'end_col': close - source.rfind('\n', 0, close) - 1, + **extra}) + index += 1 + # The AST's ownership relation is authoritative. Audit the lexer and every + # doc spelling so an unsupported/orphaned reserved fence cannot disappear. + for attr in syntax.get('attributes', []): + span = (attr['start'], attr['end']) + raw = source[char_offset(span[0]):char_offset(span[1])] + if (suspicious(raw) or suspicious(''.join(attr.get('fragments', [])))) and span not in owned: + raise ValueError('Reserved aeneas fence in unsupported, conditional, computed, inner, or orphaned doc attribute') + for comment in syntax['comments']: + span = (comment['start'], comment['end']) + raw = source[char_offset(span[0]):char_offset(span[1])] + if suspicious(raw) and not any(a <= span[0] and span[1] <= b for a, b in owned): + raise ValueError('Reserved aeneas fence must belong to an outer Rust doc attribute on a supported function or nominal type') + return found + + +def write_scan(annotations, work): + """Select exactly the Rust owners of the present annotations as roots. + + CI checks every present specification so an owned fence cannot escape + extraction, canonical proof checking, or either model's compiled audit. + """ + (work / 'annotations.json').write_text(json.dumps(annotations, indent=2) + '\n') + (work / 'source-types.json').write_text(json.dumps(getattr(annotations, 'types', []), indent=2) + '\n') + (work / 'roots.txt').write_text(','.join(annotations) + '\n') + + +def discover(root, tool): + """Capture one inspected snapshot, including unannotated dependency sources. + + Keep every nominal owner, not just annotated types. Generated structural + models need the same source ownership checks as authored models. Multiple + cfg alternatives can share a name, so extraction must later select the + candidate whose source span actually matches. + """ + paths = source_files(root) + result = subprocess.run([str(tool)], cwd=root, input=json.dumps(paths), text=True, capture_output=True) + if result.returncode: + raise ValueError(f'Rust syntax inspection failed: {result.stderr}') + syntax = json.loads(result.stdout) + annotations = Annotations() + for file in paths: + source = read_source(root / file) + try: + found = parse_file(source, syntax[file]) + except ValueError as error: + raise ValueError(f'{file}: {error}') from error + encoded = source.encode('utf-8') + annotations.source_snapshots[file] = encoded + if valid_file(file): + for owner in syntax[file].get('types', []): + start = len(encoded[:owner['start']].decode('utf-8')) + close = len(encoded[:owner['close']].decode('utf-8')) + annotations.types.append({ + 'rust': rust_identity(file, owner['path']), 'file': file, + 'syntax': owner['path'], 'kind': 'type', 'nominal_kind': owner['kind'], + 'generics': owner['generics'], 'inputs': [], 'supported': owner['supported'], + 'fields': owner['fields'], 'open_line': source[:start].count('\n') + 1, + 'end_line': source[:close].count('\n') + 1, + 'end_col': close - source.rfind('\n', 0, close) - 1}) + for annotation in found: + if not valid_file(file): + raise ValueError(f'Unsupported annotated source location: {file}') + rust = rust_identity(file, annotation['syntax']) + if rust in annotations: + raise ValueError(f'Duplicate annotation: {rust}') + if annotation['kind'] == 'function' and annotation['theorem'] in {a['theorem'] for a in annotations.values() if a['kind'] == 'function'}: + raise ValueError(f'Duplicate specification name: {annotation["theorem"]}') + annotations[rust] = {**annotation, 'rust': rust, 'file': file} + if not annotations: + raise ValueError('No Aeneas specifications discovered') + return annotations + + +def check_goldens(root, allow_missing=False): + """Check the canonical module roster, allowing initial population on request. + + scan-update may start with missing modules, but unexpected files still fail. + Ordinary scan and later comparison require the complete golden roster. + """ + directory = root / 'verification/aeneas/golden' + if allow_missing: + actual = {str(path.relative_to(directory)) for path in directory.rglob('*') if path.is_file()} + if actual <= set(golden.FILES): + return + golden.files(directory) + + +def render(root, destination): + """Copy whole generated goldens while keeping Rust annotations authoritative.""" + generated = golden.files(root / 'verification/aeneas/golden') + destination.mkdir(parents=True, exist_ok=True) + for name, text in generated.items(): + (destination / name).write_text(text) + + +def generated_nominal_types(source): + """Read local nominal declarations in Aeneas's extracted dependency order. + + These generated names propose the correspondence; inspected source and LLBC + checks establish its authority. Aliases reuse their existing carrier and + therefore do not acquire a competing nominal model provider. + """ + starts = list(re.finditer(r'^/-- \[(zerocopy::[^\]\n]+)\][^\n]*\n', source, re.M)) + result, seen = [], set() + for index, match in enumerate(starts): + end = starts[index + 1].start() if index + 1 < len(starts) else source.rfind('\nend Zerocopy') + close = source.find('-/\n', match.start(), end) + if close < 0 or end < 0: + raise ValueError('Missing generated type documentation or namespace end') + declaration = source[close + 3:end] + declaration = re.sub(r'^(?:@\[[^\]\n]*\]\n)+', '', declaration) + header = re.match(r'(structure|inductive) (' + IDENTIFIER + r'(?:\.' + IDENTIFIER + r')*)\b', declaration) + if not header: + # Genuine aliases are normalized to their existing carriers. In + # particular they never receive a competing model provider. + if re.match(r'(?:def|abbrev)\s', declaration): + continue + raise ValueError(f'Unsupported generated type declaration: {match[1]}') + if match[1] in seen: + raise ValueError(f'Duplicate generated Lean type identity: {match[1]}') + seen.add(match[1]) + names, _ = binder_header(declaration, header.end()) + result.append({'rust': match[1], 'model': header[2], 'generics': names, + 'nominal_kind': 'struct' if header[1] == 'structure' else 'enum'}) + return result + + +def generated_bindings(source, annotations, types_source=None): + """Associate root definitions with pinned Aeneas Rust identity metadata. + + Ignore loop helpers and dependencies, then check original parameter order. + Names alone do not establish source ownership: the separate LLBC audit also + binds each annotated root to its inspected body and argument names. + """ + starts = list(re.finditer(r'^/-- [^\n]*\n', source, re.M)) + models = {} + for index, match in enumerate(starts): + display = re.match(r'/-- \[(zerocopy::[^\]\n]+)\]:', match[0]) + if not display: + continue + rust = display[1] + if '{' in rust: + method = re.fullmatch(r'(.+::)\{([^{}]+)::(' + IDENTIFIER + r')(?:<[^{}]+>)?\}::(' + IDENTIFIER + ')', rust) + if not method or method[1] != method[2] + '::': + raise ValueError('Unsupported generated inherent method identity') + rust = method[1] + method[3] + '::' + method[4] + if (rust not in annotations or annotations[rust].get('kind', 'function') != 'function' + or re.search(r': loop(?: body)? \d+:', match[0])): + continue + end = starts[index + 1].start() if index + 1 < len(starts) else source.rfind('\nend Zerocopy') + close = source.find('-/\n', match.start(), end) + if close < 0 or end < 0: + raise ValueError('Missing generated root documentation or namespace end') + declaration = source[close + 3:end] + header = re.match(r'def (' + IDENTIFIER + r'(?:\.' + IDENTIFIER + r')*)\b', declaration) + if not header: + raise ValueError(f'Unsupported generated root declaration for {rust}') + if rust in models: + raise ValueError(f'Duplicate generated Lean root identity: {rust}') + names, _ = binder_header(declaration, header.end()) + expected = annotations[rust]['generics'] + annotations[rust]['inputs'] + if names != expected: + raise ValueError(f'Generated model parameters disagree with Rust parameters for {rust}: {names} != {expected}') + models[rust] = header[1] + type_annotations = {rust: a for rust, a in annotations.items() if a.get('kind') == 'type'} + if type_annotations: + if types_source is None: + raise ValueError('Generated Types.lean is required to bind type models') + for declaration in generated_nominal_types(types_source): + rust = declaration['rust'] + if rust not in type_annotations: + continue + annotation = type_annotations[rust] + if declaration['nominal_kind'] != annotation['nominal_kind']: + raise ValueError(f'Generated type kind disagrees with Rust owner: {rust}') + if declaration['generics'] != annotation['generics']: + raise ValueError(f'Generated model type parameters disagree with Rust parameters for {rust}') + models[rust] = declaration['model'] + if models.keys() != annotations.keys(): + raise ValueError('Generated Lean root declarations do not match discovered specifications') + return models + + +def source_digest(root, annotations): + """Hash fixed inspected inputs, rejecting edits since their discovery.""" + snapshots = getattr(annotations, 'source_snapshots', None) + if not snapshots: + raise ValueError('Fixed Rust source snapshots are required for model bindings') + digest = hashlib.sha256() + for file, snapshot in sorted(snapshots.items()): + try: + current = (root / file).read_bytes() + except OSError as error: + raise ValueError(f'Scanned source is no longer available: {file}') from error + if current != snapshot: + raise ValueError(f'Scanned source changed after annotation discovery: {file}') + digest.update(file.encode()) + digest.update(snapshot) + return digest.hexdigest() + + +def model_digest(work): + """Fingerprint normalized generated function and nominal-type code. + + Comment-only changes may pass golden comparison, but changed Lean code must + invalidate the saved binding before projection assembly. + """ + normalized = '\n'.join(golden.normalize((work / 'Zerocopy/Funs.lean').read_text())) + types = work / 'Zerocopy/Types.lean' + if types.exists(): + normalized += '\n' + '\n'.join(golden.normalize(types.read_text())) + return hashlib.sha256(normalized.encode()).hexdigest() + + +def save_bindings(root, annotations, work, development=False): + """Persist a checked name correspondence for both projection builds. + + Live extraction has already selected one source owner per nominal type. + Development uses the inspected candidates and therefore still requires an + unambiguous owner. Normalize both paths to candidate lists for the ownership + check, then use only its selected owners when constructing the manifest. + """ + source_digest(root, annotations) + types = work / 'Zerocopy/Types.lean' + models = generated_bindings((work / 'Zerocopy/Funs.lean').read_text(), annotations, + types.read_text() if types.exists() else None) + nominal = generated_nominal_types(types.read_text()) if types.exists() else [] + extracted_types = getattr(annotations, 'extracted_types', None) + source_candidates = {} + if extracted_types is None: + for source in getattr(annotations, 'types', []): + source_candidates.setdefault(source['rust'], []).append(source) + else: + source_candidates = {rust: [source] for rust, source in extracted_types.items()} + if {declaration['rust'] for declaration in nominal} != set(extracted_types): + raise ValueError('Generated nominal type declarations do not match extracted Rust nominal types') + source_types = {} + for declaration in nominal: + candidates = source_candidates.get(declaration['rust'], []) + if len(candidates) != 1: + raise ValueError(f'Generated nominal type lacks unambiguous verified source ownership: {declaration["rust"]}') + source = candidates[0] + if (not source['supported'] or declaration['generics'] != source['generics'] + or declaration['nominal_kind'] != source['nominal_kind']): + raise ValueError(f'Generated nominal type disagrees with supported Rust source: {declaration["rust"]}') + source_types[declaration['rust']] = source + bindings = {} + for rust, raw in models.items(): + a = annotations[rust] + if a.get('kind', 'function') == 'function': + bindings[rust] = {'kind': 'function', 'raw': 'Zerocopy.' + raw, + 'generics': a['generics'], 'inputs': a['inputs'], + 'file': a['file'], 'syntax': a['syntax'], 'spec': a['theorem'], + 'open_line': a['open_line'], 'end_line': a['end_line'], 'end_col': a['end_col']} + for declaration in nominal: + rust = declaration['rust'] + source = source_types[rust] + a = annotations.get(rust) + raw = 'Zerocopy.' + declaration['model'] + model_name = a['model_name'] if a and a['kind'] == 'type' else 'Fields' + bindings[rust] = {'kind': 'type', 'raw': raw, 'fields': raw + '.Fields', + 'model': raw + '.' + model_name, 'decoder': raw + '.decode', + 'provider': raw + '.aeneasModel', 'generics': declaration['generics'], + 'nominal_kind': declaration['nominal_kind'], + 'authored': bool(a and a['kind'] == 'type'), + 'file': source['file'], 'syntax': source['syntax'], + 'open_line': source['open_line'], 'end_line': source['end_line'], 'end_col': source['end_col']} + manifest = {'version': 3, 'bindings': bindings, + 'mode': 'development' if development else 'verified-live', + 'sources': source_digest(root, annotations), 'model': model_digest(work)} + (work / 'bindings.json').write_text(json.dumps(manifest, indent=2) + '\n') + + +def assemble(root, annotations, work, development=False): + """Project checked owners into native Lean modules without replacing code. + + The live binding manifest fixes extracted names and source snapshots. Both + live and golden builds consume it; development explicitly uses a weaker + mode. Only authored fragments receive edit markers. Structural derivations, + function applications, and audit commands remain generated framing. + """ + if not (root / 'verification/aeneas/lean/RequiredContracts.lean').is_file(): + raise ValueError('RequiredContracts.lean is required for independent specification checks') + # CI saves this mapping only after exact-body Charon checks and live Aeneas + # generation. The golden build consumes that same mapping, never its docs. + saved = work / 'bindings.json' + if not saved.exists(): + raise ValueError('Verified live model bindings are required before specification generation') + manifest = json.loads(saved.read_text()) + expected_mode = 'development' if development else 'verified-live' + if (manifest.get('version') != 3 or manifest.get('mode') != expected_mode + or manifest.get('sources') != source_digest(root, annotations) + or manifest.get('model') != model_digest(work)): + raise ValueError('Model bindings are stale; regenerate specs from the current sources and model') + if development: + workspace.protect_projections(work) + workspace.retire_generated(work) + bindings = manifest['bindings'] + claimed = {rust for rust, entry in bindings.items() if entry['kind'] == 'function' or entry['authored']} + if claimed != set(annotations): + raise ValueError('Verified live bindings do not cover every specification') + imports = ['module', 'public import Zerocopy.Funs', 'public import SpecPrelude', 'public import SpecsSyntax', 'public import Models'] + if (root / 'verification/aeneas/lean/LayoutModel.lean').exists(): + imports.append('public import LayoutModel') + elif (root / 'verification/aeneas/lean/Arithmetic.lean').exists(): + imports.append('public import Arithmetic') + specs = GeneratedLines([*golden.HEADER.rstrip().splitlines(), '', *imports, '@[expose] public section', '', + 'open Aeneas Aeneas.Std', 'open Zerocopy.Proofs', 'namespace Zerocopy.Specs', '']) + for a in annotations.values(): + if a.get('kind', 'function') != 'function': + continue + model = bindings[a['rust']]['raw'] + if not re.fullmatch(IDENTIFIER + r'(?:\.' + IDENTIFIER + ')*', model): + raise ValueError('Invalid verified generated model name') + lines = a['spec_text'].rstrip('\n').split('\n') + clause = a['clause_line'] + application = f'@{model} with {len(a["generics"])} type parameters' + lines.insert(clause, ' for ' + application) + specs.append(f'check_model_inputs {model} with {len(a["generics"])} type parameters', + {'file': a['file'], 'line': a['source_lines'][0]}) + specs.append('aeneas_spec_begin') + if development: + specs.append(f'-- aeneas-copy-begin {a["rust"]} spec') + for index, line in enumerate(lines): + original = max(0, index - (index >= clause)) + specs.append(line, {'file': a['file'], 'line': a['source_lines'][original]}) + if development: + specs.append(f'-- aeneas-copy-end {a["rust"]} spec') + specs.append('aeneas_spec_end', {'file': a['file'], 'line': a['source_lines'][-1]}) + specs.append(f'check_spec_binding Zerocopy.Specs.{a["theorem"]} ' + f'for {model} with {len(a["generics"]) + len(a["inputs"])}', + {'file': a['file'], 'line': a['source_lines'][0]}) + specs.append('') + specs.extend(['end Zerocopy.Specs', '']) + (work / 'Specs.lean').write_text('\n'.join(specs)) + (work / 'Specs.source-map.json').write_text(json.dumps(specs.locations, indent=2) + '\n') + shape_imports = ['Zerocopy.Types', 'ModelPrelude', 'DeriveModels'] + support = workspace.model_support(root) + model_imports = ['ModelShapes', 'DeriveModels', *support] + shapes = GeneratedLines([*golden.HEADER.rstrip().splitlines(), '', 'module', + *('public import ' + name for name in shape_imports), + '@[expose] public section', '', 'open Aeneas Aeneas.Std AeneasSpecs', '']) + decoders = GeneratedLines([*golden.HEADER.rstrip().splitlines(), '', 'module', + *('public import ' + name for name in model_imports), + '@[expose] public section', '', 'open Aeneas Aeneas.Std AeneasSpecs', '']) + # This is the single source/extraction binding table's verified order. + for rust, entry in bindings.items(): + if entry['kind'] != 'type': + continue + raw, count = entry['raw'], len(entry['generics']) + a = annotations.get(rust) + if entry['authored']: + shapes.append(f'aeneas_model_shape {raw} with {count} type parameters begin', + {'file': a['file'], 'line': a['source_lines'][0]}) + if development: + shapes.append(f'-- aeneas-copy-begin {rust} shape') + for index, line in enumerate(a['shape_text'].splitlines()): + shapes.append(line, {'file': a['file'], 'line': a['source_lines'][index]}) + if development: + shapes.append(f'-- aeneas-copy-end {rust} shape') + location = {'file': a['file'], 'line': a['source_lines'][a['decoder_clause_line']]} + shapes.append('end', location) + if development: + decoders.append(f'derive_rust_model {raw} with {count} type parameters', location) + decoders.append(f'-- aeneas-copy-begin {rust} decoder') + decoders.append(f'{a["decoder_mode"]} {a["value_binder"]} =>', location) + else: + decoders.append(f'derive_rust_model {raw} with {count} type parameters ' + f'{a["decoder_mode"]} {a["value_binder"]} =>', location) + for index, line in enumerate(a['decoder_text'].splitlines()): + decoders.append(' ' + line, {'file': a['file'], + 'line': a['source_lines'][min(a['decoder_line'] + index, len(a['source_lines']) - 1)]}) + if development: + decoders.append(f'-- aeneas-copy-end {rust} decoder') + else: + location = {'file': entry['file'], 'line': entry['open_line']} + shapes.append(f'derive_model_shape {raw} with {count} type parameters', location) + decoders.append(f'derive_rust_model {raw} with {count} type parameters', location) + decoders.append(f'check_model_binding {raw} with {count} type parameters', location) + shapes.append('') + decoders.append('') + for module, lines in [('ModelShapes', shapes), ('Models', decoders)]: + (work / f'{module}.lean').write_text('\n'.join(lines) + '\n') + (work / f'{module}.source-map.json').write_text(json.dumps(lines.locations, indent=2) + '\n') + modules = ['Proofs', *sorted(str(path.relative_to(root / 'verification/aeneas/lean')).removesuffix('.lean').replace('/', '.') + for path in (root / 'verification/aeneas/lean/Proofs').rglob('*.lean'))] + # Import every configured native proof module, even when its declarations + # are unused by the aggregate. Otherwise a private helper could evade the + # axiom audit simply by being omitted from the aggregate's imports. + audit_modules = sorted({'Specs', 'ModelShapes', 'Models', 'Zerocopy.Funs', 'Zerocopy.Types', + 'Proofs', 'Obligations', + *(str(path).removesuffix('.lean').replace('/', '.') + for path in workspace.handwritten(root) + if path != Path('Check.lean'))}) + required = ['import Lean', *('import ' + module for module in audit_modules)] + required += ['open Lean', ''] + if (root / 'verification/aeneas/lean/Arithmetic.lean').exists(): + required += ['attribute [local contract_simps] Zerocopy.Arithmetic.coe_min', ''] + for a in annotations.values(): + if a.get('kind', 'function') != 'function': + continue + name = a['theorem'] + required.append(f'example : Zerocopy.Specs.{name} := @Zerocopy.Proofs.{name}') + required.append(f'check_contract Zerocopy.Obligations.{name} using @Zerocopy.Proofs.{name}') + required.append('def proofModuleNames : Array Name := #[' + ', '.join('`' + module for module in modules) + ']') + required.append('def auditModuleNames : Array Name := #[' + ', '.join('`' + module for module in audit_modules) + ']') + (work / 'Required.lean').write_text('\n'.join(required) + '\n') + if source_digest(root, annotations) != manifest['sources']: + raise ValueError('Source snapshots disagree with verified model bindings') + (work / 'Specs.sources.sha256').write_text(manifest['sources'] + '\n') + if development: + workspace.record_projections(work, annotations) + + +def check_bindings(root, annotations, llbc): + """Audit extraction against the complete inspected source snapshot. + + Names alone cannot distinguish cfg alternatives or macro-generated owners. + Require exact local contents, owner spans, ordered generics, and original + argument names before accepting the function and nominal-type rosters. + """ + source_digest(root, annotations) + translated = json.loads(llbc.read_text())['translated'] + files = {f['id']: f for f in translated['files']} + if len(files) != len(translated['files']): + raise ValueError('Duplicate extracted source file identity') + for file in files.values(): + name = file.get('name') + if (file.get('contents') is None and isinstance(name, dict) + and set(name) == {'Local'} and isinstance(name['Local'], str) + and name['Local'].startswith('/rustc/')): + # These pinned external sources remain a translation premise. + continue + if not isinstance(name, dict) or set(name) != {'Local'}: + raise ValueError('Unsupported extracted source file identity') + relative = name['Local'] + path = 'zerocopy/' + relative if isinstance(relative, str) else '' + if not valid_file(path) or path not in annotations.source_snapshots: + raise ValueError(f'Extracted local dependency file lacks an inspected source snapshot: {relative}') + if not isinstance(file.get('contents'), str): + raise ValueError(f'Extracted local dependency file is missing source contents: {relative}') + if file['contents'].replace('\r\n', '\n') != annotations.source_snapshots[path].decode('utf-8').replace('\r\n', '\n'): + raise ValueError(f'Charon extracted a different source body for local dependency file {relative}') + # At the pinned Charon version named Self types may be deduplicated. Only + # collect the Adt type domain, not unrelated Value/Deduplicated domains. + adts = {} + + def collect_adts(value): + # Charon may store a named Self carrier once and refer to it by index. + # Gather only the Adt domain: other deduplicated values can reuse the + # same integer without identifying a type. + if isinstance(value, dict): + if (set(value) == {'Value'} and isinstance(value['Value'], list) + and len(value['Value']) == 2 and isinstance(value['Value'][0], int)): + index, body = value['Value'] + if (isinstance(body, dict) and set(body) == {'Adt'} + and isinstance(body['Adt'], dict) + and set(body['Adt']) == {'id', 'generics', 'builtin'}): + if index in adts and adts[index] != body: + raise ValueError('Ambiguous deduplicated Charon Self type') + adts[index] = body + for child in value.values(): + collect_adts(child) + elif isinstance(value, list): + for child in value: + collect_adts(child) + + collect_adts(translated) + type_names = {t['def_id']: t['item_meta']['name'] + for t in translated.get('type_decls', []) if t} + + def identifiers(name): + if not all(set(element) == {'Ident'} and element['Ident'][1] == 0 for element in name): + raise ValueError('Unsupported extracted root identity') + return [element['Ident'][0] for element in name] + + def identity(name): + # An inherent method name embeds its Self carrier. Resolve that carrier + # back to the locally defined nominal type instead of accepting a + # display spelling that could hide a trait or qualified impl. + impls = [i for i, element in enumerate(name) if 'Impl' in element] + if not impls: + return '::'.join(identifiers(name)) + if impls != [len(name) - 2]: + raise ValueError('Unsupported extracted inherent method identity') + index = impls[0] + implementation = name[index]['Impl'] + if set(implementation) != {'Ty'}: + raise ValueError('Trait impl roots are unsupported') + implementation = implementation['Ty'] + if implementation['kind'] != 'InherentImplBlock': + raise ValueError('Only inherent impl roots are supported') + wrapped = implementation['skip_binder'] + if set(wrapped) == {'Value'}: + body = wrapped['Value'][1] + elif set(wrapped) == {'Deduplicated'}: + body = adts.get(wrapped['Deduplicated'], {}) + else: + body = {} + if set(body) != {'Adt'}: + raise ValueError('Inherent Self must resolve to a named type') + adt = body['Adt'] + if adt['builtin'] is not None: + raise ValueError('Builtin Self roots are unsupported') + self_name = identifiers(type_names.get(adt['id'], [])) + prefix = identifiers(name[:index]) + if not self_name or self_name[:-1] != prefix: + raise ValueError('Inherent Self must be defined in the annotated module') + return '::'.join([*self_name, *identifiers(name[index + 1:])]) + + roots = {} + for function in translated['fun_decls']: + if not function or function['item_meta'].get('started_from') is not True: + continue + meta = function['item_meta'] + name = identity(meta['name']) + if name in roots: + raise ValueError('Duplicate extracted root identity') + roots[name] = function + function_annotations = {rust: a for rust, a in annotations.items() + if a.get('kind', 'function') == 'function'} + if roots.keys() != function_annotations.keys(): + raise ValueError('Extracted roots do not match the present function annotations') + for declaration in translated.get('type_decls', []): + if not declaration: + continue + meta = declaration['item_meta'] + # Dependency types may use unsupported external identities. Only local + # model roots participate in this exact source correspondence audit. + if meta.get('is_local') is not True: + continue + rust = identity(meta['name']) + if rust not in annotations or annotations[rust].get('kind') != 'type': + continue + if rust in roots: + raise ValueError(f'Duplicate extracted type identity: {rust}') + roots[rust] = declaration + if roots.keys() != annotations.keys(): + raise ValueError('Extracted type roots do not match the present annotations') + def verify_source(rust, annotation, function): + # File equality binds the whole inspected snapshot; the final source + # position distinguishes same-named cfg alternatives within that file. + # Normalizing CRLF matches Charon's text convention without discarding + # physical byte spelling from the editing baseline. + meta = function['item_meta'] + span = meta['span']['Untagged'] + data = span['data'] + file = files[data['file_id']] + source = annotations.source_snapshots[annotation['file']].decode('utf-8').replace('\r\n', '\n') + if (meta.get('is_local') is not True or span['generated_from_span'] is not None + or file['name'] != {'Local': annotation['file'].removeprefix('zerocopy/')} + or file['contents'].replace('\r\n', '\n') != source + or not data['beg']['line'] <= annotation['open_line'] <= data['end']['line'] + or data['end'] != {'line': annotation['end_line'], 'col': annotation['end_col']}): + raise ValueError(f'Charon extracted a different source body for {rust}') + generics = function['generics'] + if (generics['const_generics'] or generics['trait_clauses'] + or [parameter['name'] for parameter in generics['types']] != annotation['generics']): + raise ValueError(f'Charon extracted different or unsupported generic parameters for {rust}') + + for rust, annotation in annotations.items(): + function = roots[rust] + verify_source(rust, annotation, function) + if annotation.get('kind') == 'type': + expected = 'Struct' if annotation['nominal_kind'] == 'struct' else 'Enum' + if not isinstance(function.get('kind'), dict) or set(function['kind']) != {expected}: + raise ValueError(f'Charon extracted a different or unsupported nominal type for {rust}') + continue + body = function.get('body') or {} + locals_ = body.get('Structured', {}).get('locals', {}) + count = locals_.get('arg_count') + arguments = locals_.get('locals', [])[1:1 + count] if isinstance(count, int) else [] + if (count != len(annotation['inputs']) + or len(function['signature']['inputs']) != count + or [argument['name'] for argument in arguments] != annotation['inputs']): + raise ValueError(f'Charon extracted different function arguments for {rust}') + + if isinstance(annotations, Annotations): + extracted_types = {} + for declaration in translated.get('type_decls', []): + if not declaration or declaration['item_meta'].get('is_local') is not True: + continue + meta = declaration['item_meta'] + kind = declaration.get('kind') + if not isinstance(kind, dict) or not (set(kind) <= {'Struct', 'Enum'}): + # Genuine aliases inherit their carrier; unsupported opaque and + # recursive carriers are diagnosed during structural generation. + continue + rust = identity(meta['name']) + candidates = [a for a in annotations.types if a['rust'] == rust] + matching = [] + for candidate in candidates: + try: + verify_source(rust, candidate, declaration) + except ValueError: + continue + matching.append(candidate) + if len(matching) != 1 or not matching[0]['supported']: + raise ValueError(f'Extracted nominal type lacks exact supported Rust source ownership: {rust}') + source = matching[0] + expected = 'Struct' if source['nominal_kind'] == 'struct' else 'Enum' + if set(kind) != {expected} or rust in extracted_types: + raise ValueError(f'Extracted nominal type kind or identity disagrees with Rust source: {rust}') + extracted_types[rust] = source + annotations.extracted_types = extracted_types + + +def update(root, live): + """Update complete goldens from live output without rewriting Rust fences.""" + golden.update(live, root / 'verification/aeneas/golden') + + +def main(): + """Route discovery, extraction checks, and assembly through their boundaries. + + Development assembly is explicitly weaker than fresh live binding; both + modes still preserve projection edits and bind to inspected snapshots. + """ + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('command', choices=['scan', 'scan-update', 'bindings', 'models', 'render', 'assemble', 'dev-assemble', 'update']) + parser.add_argument('--root', type=Path, required=True) + parser.add_argument('--tool', type=Path, required=True) + parser.add_argument('--work', type=Path, required=True) + args = parser.parse_args() + annotations = discover(args.root, args.tool) + if args.command == 'bindings': + check_bindings(args.root, annotations, args.work / 'zerocopy.llbc') + elif args.command == 'models': + # The driver calls this only after bindings, on freshly generated live + # output. Keep the verified mapping separate from mutable spec text. + check_bindings(args.root, annotations, args.work / 'zerocopy.llbc') + save_bindings(args.root, annotations, args.work) + elif args.command == 'dev-assemble': + # Editor-only generation deliberately lacks the fresh extraction/body + # guarantee. CI uses models above and checks both independent builds. + workspace.protect_projections(args.work) + save_bindings(args.root, annotations, args.work, development=True) + assemble(args.root, annotations, args.work, development=True) + elif args.command == 'update': + update(args.root, args.work) + elif args.command == 'assemble': + assemble(args.root, annotations, args.work) + elif args.command == 'render': + render(args.root, args.work) + else: + check_goldens(args.root, allow_missing=args.command == 'scan-update') + write_scan(annotations, args.work) + + +if __name__ == '__main__': + main() diff --git a/verification/aeneas/lean/Arithmetic.lean b/verification/aeneas/lean/Arithmetic.lean new file mode 100644 index 0000000000..64a7f5c38c --- /dev/null +++ b/verification/aeneas/lean/Arithmetic.lean @@ -0,0 +1,189 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Aeneas +import all Init.Data.Nat.Power2.Basic +@[expose] public section + +/-! +Rust implements alignment operations with bounded words and bit masks. The +contracts describe those results with unbounded natural numbers. This module +proves the arithmetic facts needed to connect the two descriptions: why +clearing low bits rounds down, why the padding mask yields the least aligned +increment, and why wrapping at zero does not invalidate that increment. It +contains no claims about the control flow of a zerocopy function; Proofs.Util +applies these facts while stepping through the extracted implementations. +-/ +open Aeneas Aeneas.Std +namespace Zerocopy.Arithmetic + +/-- Scalar minimum agrees with mathematical minimum. -/ +theorem coe_min (a b : Usize) : + ((min a b : Usize) : Nat) = Nat.min (a : Nat) (b : Nat) := by + by_cases h : a ≤ b + · rw [min_eq_left h] + exact (Nat.min_eq_left ((UScalar.le_equiv _ _).mp h)).symm + · have hba : b ≤ a := le_of_not_ge h + rw [min_eq_right hba] + exact (Nat.min_eq_right ((UScalar.le_equiv _ _).mp hba)).symm + + +/-- Scalar maximum agrees with mathematical maximum. -/ +theorem coe_max (a b : Usize) : + ((max a b : Usize) : Nat) = Nat.max (a : Nat) (b : Nat) := by + by_cases h : a ≤ b + · rw [max_eq_right h] + exact (Nat.max_eq_right ((UScalar.le_equiv _ _).mp h)).symm + · have hba : b ≤ a := le_of_not_ge h + rw [max_eq_left hba] + exact (Nat.max_eq_left ((UScalar.le_equiv _ _).mp hba)).symm + +/-- The unique bounded padding that makes the sum divisible by alignment. -/ +theorem padding_unique (len align p : Nat) (hpos : 0 < align) + (hlt : p < align) (haligned : (len + p) % align = 0) : + p = (align - len % align) % align := by + have hr := Nat.mod_lt len hpos + have hsum : (len % align + p) % align = 0 := by + rw [Nat.add_mod, Nat.mod_eq_of_lt hlt] at haligned + exact haligned + by_cases hzero : len % align = 0 + · simpa only [hzero, Nat.zero_add, Nat.mod_eq_of_lt hlt, + Nat.sub_zero, Nat.mod_self] using hsum + · have hge : align ≤ len % align + p := by + by_contra h + rw [Nat.mod_eq_of_lt (by omega)] at hsum + omega + rw [Nat.mod_eq_sub_mod hge, Nat.mod_eq_of_lt (by omega)] at hsum + rw [Nat.mod_eq_of_lt (by omega)] + omega + +/-- Exact padding also gives minimality and the zero-padding criterion. -/ +theorem padding_properties (len align p : Nat) (hpos : 0 < align) + (hlt : p < align) (haligned : (len + p) % align = 0) : + p = (align - len % align) % align ∧ + (∀ q : Nat, (len + q) % align = 0 → p ≤ q) ∧ + (p = 0 ↔ len % align = 0) := by + refine ⟨padding_unique len align p hpos hlt haligned, ?_, ?_⟩ + · intro q hq + have hqm : (len + q % align) % align = 0 := by + rw [Nat.add_mod] at hq + rw [Nat.add_mod, Nat.mod_mod] + exact hq + have heq := padding_unique len align (q % align) hpos + (Nat.mod_lt q hpos) hqm + have hp := padding_unique len align p hpos hlt haligned + have := Nat.mod_le q align + omega + · constructor + · intro hp + simpa only [hp, Nat.add_zero] using haligned + · intro hl + have hp := padding_unique len align p hpos hlt haligned + simpa only [hl, Nat.sub_zero, Nat.mod_self] using hp + +/-- Clearing and retaining a bit mask partition the original word exactly. -/ +theorem mask_partition (n mask : Usize) : + (n &&& ~~~mask).val + (n &&& mask).val = n.val := by + have hdisjoint : (n.bv &&& ~~~mask.bv) &&& (n.bv &&& mask.bv) = 0 := by + ext i + simp only [BitVec.getElem_and, BitVec.getElem_not] + cases n.bv[i] <;> cases mask.bv[i] <;> simp + have hjoin : (n.bv &&& ~~~mask.bv) ||| (n.bv &&& mask.bv) = n.bv := by + ext i + simp only [BitVec.getElem_or, BitVec.getElem_and, BitVec.getElem_not] + cases n.bv[i] <;> cases mask.bv[i] <;> decide + have hsum := BitVec.add_eq_or_of_and_eq_zero _ _ hdisjoint + rw [hjoin] at hsum + have := congrArg BitVec.toNat hsum + rw [BitVec.toNat_add_of_and_eq_zero hdisjoint] at this + exact this + +/-- A power-of-two alignment divides the word modulus. -/ +theorem alignment_dvd_size (align : Usize) (h : align.val.isPowerOfTwo) : + align.val ∣ UScalar.size .Usize := by + obtain ⟨k, hk⟩ := h + have hsize := UScalar.hSize align + rw [UScalar.size, hk] at hsize ⊢ + exact Nat.pow_dvd_pow 2 ((Nat.pow_le_pow_iff_right (a := 2) (by decide)).mp (by omega)) + +/-- Complementing the wrapping predecessor produces modular negation. -/ +theorem complement_predecessor (len : Usize) : + (~~~(core.num.Usize.wrapping_sub len 1#usize)).val = + if len.val = 0 then 0 else UScalar.size .Usize - len.val := by + change (~~~(len.bv - (1#usize).bv)).toNat = _ + rw [BitVec.toNat_not] + have hsize := UScalar.hSize len + have hone : (1#usize).val = 1 := by simp + by_cases hz : len.val = 0 + · simp only [hz, if_true] + rw [BitVec.toNat_sub, UScalar.bv_toNat, UScalar.bv_toNat] + change 2 ^ System.Platform.numBits - 1 - + ((2 ^ System.Platform.numBits - (1#usize).val) + len.val) % 2 ^ System.Platform.numBits = 0 + rw [hz, hone, Nat.add_zero, Nat.mod_eq_of_lt (by have := Nat.two_pow_pos System.Platform.numBits; omega)] + omega + · simp only [hz, if_false] + rw [BitVec.toNat_sub_of_le (by simpa only [BitVec.le_def, UScalar.bv_toNat, hone] using (show 1 ≤ len.val by omega)), UScalar.bv_toNat, UScalar.bv_toNat, hone] + change 2 ^ System.Platform.numBits - 1 - (len.val - 1) = UScalar.size .Usize - len.val + rw [UScalar.size] at hsize ⊢ + change len.val < 2 ^ System.Platform.numBits at hsize + change 2 ^ System.Platform.numBits - 1 - (len.val - 1) = + 2 ^ System.Platform.numBits - len.val + omega + +/-- The padding bit mask aligns the unbounded sum, including wrapping at zero. -/ +theorem padding_mask_aligned (len align mask : Usize) + (h : align.val.isPowerOfTwo) (hm : mask.val = align.val - 1) : + (len.val + (~~~(core.num.Usize.wrapping_sub len 1#usize) &&& mask).val) + % align.val = 0 := by + have hmod := Nat.mod_eq_zero_of_dvd (alignment_dvd_size align h) + have hsum : (len.val + (~~~(core.num.Usize.wrapping_sub len 1#usize)).val) + % align.val = 0 := by + rw [complement_predecessor] + split + · rename_i hz + simp only [hz, Nat.zero_add, Nat.zero_mod] + · have hsize := UScalar.hSize len + rw [Nat.add_sub_of_le (by omega)] + exact hmod + obtain ⟨k, hk⟩ := h + rw [UScalar.val_and, hm, hk, Nat.and_two_pow_sub_one_eq_mod] + rw [hk, Nat.add_mod] at hsum + rw [Nat.add_mod, Nat.mod_mod] + exact hsum + +/-- The exact arithmetic result of clearing the low alignment bits. -/ +theorem round_down_exact (n align mask : Usize) + (h : align.val.isPowerOfTwo) (hm : mask.val = align.val - 1) : + (n &&& ~~~mask).val = n.val - n.val % align.val := by + have hp := mask_partition n mask + have hlow : (n &&& mask).val = n.val % align.val := by + obtain ⟨k, hk⟩ := h + rw [UScalar.val_and, hm, hk, Nat.and_two_pow_sub_one_eq_mod] + rw [hlow] at hp + omega + +/-- An exact round-down result is aligned and the greatest such lower bound. -/ +theorem round_down_properties (n align m : Nat) (hpos : 0 < align) + (hm : m = n - n % align) : + m ≤ n ∧ m % align = 0 ∧ n < m + align ∧ + (∀ q : Nat, q ≤ n → q % align = 0 → q ≤ m) := by + have hr := Nat.mod_lt n hpos + have hrle := Nat.mod_le n align + have hmul : m = (n / align) * align := by + rw [Nat.div_mul_self_eq_mod_sub_self, hm] + refine ⟨by omega, ?_, by omega, ?_⟩ + · rw [hmul, Nat.mul_mod_left] + · intro q hqn hqa + have hdiv := Nat.div_le_div_right (c := align) hqn + have hle := Nat.mul_le_mul_right align hdiv + rw [Nat.div_mul_self_eq_mod_sub_self, Nat.div_mul_self_eq_mod_sub_self, + hqa, Nat.sub_zero, ← hm] at hle + exact hle + +end Zerocopy.Arithmetic diff --git a/verification/aeneas/lean/Check.lean b/verification/aeneas/lean/Check.lean new file mode 100644 index 0000000000..7693b7ea04 --- /dev/null +++ b/verification/aeneas/lean/Check.lean @@ -0,0 +1,329 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +import Required +open Lean Elab Command + +/-! +This is the final compiled audit of the connection from Rust annotations to +Lean proofs. It compares three sources of evidence: the source/extraction +binding manifest, the actual compiled raw types and specifications, and the +configured proof and required-check modules. Each comparison is closed: both +missing and unexpected owners are errors. Deleting an annotation and its proof +together must not erase a promised root from the audit. + +After checking ownership and arbitrary-outcome adequacy, the audit records the +proof dependency graph and checks the axioms used by every audited declaration. +The graph reports what the compiled proofs use; it does not prescribe their +structure. External ABI size and alignment inputs are permitted only with their +data signatures and explicit read interpretations, rather than as axioms about +useful layout properties. +-/ + +run_elab do + let env ← getEnv + let layoutInputs := #[`Zerocopy.RustLayout.size, `Zerocopy.RustLayout.align] + -- Generic alignment reads identify the ABI-capable model. Earlier models + -- may have only the pointer-width size read and require no generic ABI inputs. + let hasLayoutReads := env.contains `core.mem.align_of + let modelInputs := if hasLayoutReads then layoutInputs else #[] + -- ABI inputs are functions returning data, with no proposition asserting + -- alignment, size, or correctness. A changed type could smuggle a theorem + -- into the allowed axiom list, so check their exact compiled signatures. + let expected ← Term.elabType (← `(Type → Aeneas.Std.Usize)) + for input in modelInputs do + let some (.axiomInfo info) := env.find? input + | throwError "Missing external layout input {input}" + unless ← Meta.isDefEq info.type expected do + throwError "External layout input {input} changed its data-only signature" + if hasLayoutReads then + -- Independently state each primitive interpretation. Keeping the data + -- axioms while ignoring them must not silently shrink the modeled domain. + let alignRead ← Term.elabTerm (← `( + fun (T : Type) => Aeneas.Std.Result.ok (Zerocopy.RustLayout.align T))) none + let sizeRead ← Term.elabTerm (← `( + fun (T : Type) => by + classical + exact Aeneas.Std.Result.ok + (if T = Aeneas.Std.Usize then + ({ bv := BitVec.ofNat _ (System.Platform.numBits / 8) } : Aeneas.Std.Usize) + else Zerocopy.RustLayout.size T))) none + Term.synthesizeSyntheticMVarsNoPostponing + let alignRead ← instantiateMVars alignRead + let sizeRead ← instantiateMVars sizeRead + unless ← Meta.isDefEq (mkConst `core.mem.align_of) alignRead do + throwError "External alignment read changed its data-input interpretation" + unless env.contains `core.mem.size_of do + throwError "Missing external size read" + unless ← Meta.isDefEq (mkConst `core.mem.size_of) sizeRead do + throwError "External size read changed its data-input interpretation" + -- Module indices identify declarations by compiled ownership. A matching + -- namespace is insufficient: a generated or unrelated module could otherwise + -- install a theorem under a handwritten proof's expected name. + let mut proofModules : Array ModuleIdx := #[] + for moduleName in proofModuleNames do + let some index := env.getModuleIdx? moduleName + | throwError "Proof module {moduleName} was not imported" + proofModules := proofModules.push index + let mut auditModules : Array ModuleIdx := #[] + for moduleName in auditModuleNames do + let some index := env.getModuleIdx? moduleName + | throwError "Audit module {moduleName} was not imported" + auditModules := auditModules.push index + -- Compiled spec declarations determine coverage; there is no separately + -- generated theorem roster that can omit a specification and its checks. + let some specsModule := env.getModuleIdx? `Specs + | throwError "Compiled inline specifications must be imported" + let mut required : Array Name := #[] + let mut actualModels : Array Name := #[] + let mut actualFamilies : Array Name := #[] + let mut expectedFamilies : Array Name := #[] + for (declName, info) in env.constants.toList do + if env.getModuleIdxFor? declName != some specsModule || + declName.getPrefix != `Zerocopy.Specs then continue + let .defnInfo info := info + | throwError "Unexpected inline specification declaration {declName}" + -- The generated namespace contains specs and their outcome families only. + -- Collect untagged definitions for an exact family comparison below, rather + -- than treating every nearby declaration as an authored contract. + if !AeneasSpecs.specAttribute.hasTag env declName then + actualFamilies := actualFamilies.push declName + continue + unless info.type.consumeMData == mkSort Level.zero do + throwError "Inline specification {declName} must be a proposition" + let (modelName, _) ← AeneasSpecs.specBinding declName + AeneasSpecs.checkSpecContract declName + let familyName := AeneasSpecs.specContractName declName + unless env.getModuleIdxFor? familyName == some specsModule do + throwError "Abstract contract {familyName} must be declared with its inline specification" + expectedFamilies := expectedFamilies.push familyName + -- One Rust function owns one inline specification. Multiple specs could + -- let the canonical theorem prove a weaker sibling of the intended claim. + if actualModels.contains modelName then + throwError "Compiled inline specifications repeat model {modelName}" + actualModels := actualModels.push modelName + required := required.push ((`Zerocopy.Proofs).str declName.getString!) + let missingFamilies := expectedFamilies.filter (!actualFamilies.contains ·) + let extraFamilies := actualFamilies.filter (!expectedFamilies.contains ·) + unless missingFamilies.isEmpty && extraFamilies.isEmpty do + throwError "Compiled abstract contracts disagree with inline specifications: missing {missingFamilies}; unexpected {extraFamilies}" + if required.isEmpty then + throwError "No compiled inline specifications were found" + -- Reuse the model mapping already produced from Rust annotations/extraction. + -- Deriving both Specs and Required from a smaller subset cannot hide a root. + -- The manifest is an audited data input produced from source ownership and + -- extraction. Validate its schema and mode before using any names/counts; + -- a development manifest still must satisfy the same compiled binding checks. + let bindingsText ← IO.FS.readFile "bindings.json" + let .ok bindings := Json.parse bindingsText + | throwError "Model bindings are not valid JSON" + let .ok version := bindings.getObjValAs? Nat "version" + | throwError "Model bindings must declare version 3" + unless version == 3 do + throwError "Model bindings must declare version 3" + let .ok mode := bindings.getObjValAs? String "mode" + | throwError "Model bindings must declare their validation mode" + unless mode == "verified-live" || mode == "development" do + throwError "Model bindings have an unsupported validation mode {mode}" + let .ok entriesValue := bindings.getObjVal? "bindings" + | throwError "Model bindings must contain their source/extraction binding table" + let .ok entries := entriesValue.getObj? + | throwError "Model bindings must contain a binding table object" + let some typesModule := env.getModuleIdx? `Zerocopy.Types + | throwError "Raw extracted types must be imported" + let some shapesModule := env.getModuleIdx? `ModelShapes + | throwError "ModelShapes must be imported from their generated module" + let some modelsModule := env.getModuleIdx? `Models + | throwError "Models must be imported from their generated module" + let mut expectedTypes : Array Name := #[] + let mut expectedModels : Array Name := #[] + for (rustName, entry) in entries.toArray do + let .ok kind := entry.getObjValAs? String "kind" + | throwError "Binding for {rustName} must name its owner kind" + let .ok raw := entry.getObjValAs? String "raw" + | throwError "Binding for {rustName} must name its exact extracted carrier" + let .ok generics := entry.getObjValAs? (Array String) "generics" + | throwError "Binding for {rustName} must name its original type parameters" + let rawName := raw.toName + -- Function entries bind a source spec to the exact extracted constant and + -- its complete type/value prefix. Type entries bind nominal raw ownership + -- to shape and full-provider declarations in their generated modules. + if kind == "function" then + if expectedModels.contains rawName then + throwError "Model bindings repeat model {rawName}" + expectedModels := expectedModels.push rawName + let .ok sourceSpec := entry.getObjValAs? String "spec" + | throwError "Function binding for {rustName} must name its inline specification" + let .ok sourceInputs := entry.getObjValAs? (Array String) "inputs" + | throwError "Function binding for {rustName} must name its original value inputs" + let specName := (`Zerocopy.Specs).str sourceSpec + unless env.getModuleIdxFor? specName == some specsModule do + throwError "Function binding for {rustName} does not name a compiled inline specification" + let (boundModel, boundInputs) ← AeneasSpecs.specBinding specName + -- This also rejects a same-arity call on replaced or reordered inputs: + -- specBinding already checked each argument's original variable identity. + unless boundModel == rawName && boundInputs == generics.size + sourceInputs.size do + throwError "Compiled specification {specName} changed its source model or original input prefix" + else if kind == "type" then + if expectedTypes.contains rawName then + throwError "Type bindings repeat extracted carrier {rawName}" + expectedTypes := expectedTypes.push rawName + let some (.inductInfo rawInfo) := env.find? rawName + | throwError "Type binding is not a raw nominal carrier: {rawName}" + unless env.getModuleIdxFor? rawName == some typesModule && + rawInfo.numParams == generics.size do + throwError "Raw nominal binding changed its ownership or type parameters: {rawName}" + let (fields, mathType, decoder, provider, count) ← AeneasSpecs.modelBinding rawName + unless count == generics.size do + throwError "Mathematical model changed its original generic parameters: {rawName}" + -- Compare independently emitted manifest names with the registry's actual + -- declarations. Derived fixed names still need this external comparison; + -- a correct provider for the wrong Rust owner is not an acceptable proof. + for (key, actual) in #[("fields", fields), ("model", mathType), + ("decoder", decoder), ("provider", provider)] do + let .ok expected := entry.getObjValAs? String key + | throwError "Type binding for {rustName} must name its {key}" + unless actual == expected.toName do + throwError "Compiled {key} {actual} disagrees with its Rust owner binding {expected}" + -- Shapes can depend on mathematical child types before any decoder is + -- built. Their ownership stays separate from full decoder/provider assembly. + unless env.getModuleIdxFor? fields == some shapesModule && + env.getModuleIdxFor? mathType == some shapesModule do + throwError "Mathematical fields and model must be owned by ModelShapes: {rawName}" + unless env.getModuleIdxFor? decoder == some modelsModule && + env.getModuleIdxFor? provider == some modelsModule do + throwError "Decoder and provider must be owned by Models: {rawName}" + let .ok authored := entry.getObjValAs? Bool "authored" + | throwError "Type binding must record its authored model disposition" + -- An unannotated owner must receive its structural carrier, not an + -- arbitrary smaller interpretation introduced by generation. + unless authored || mathType == fields do + throwError "Unannotated model must use its nominal Fields carrier: {rawName}" + else + throwError "Unsupported binding kind {kind} for {rustName}" + -- Independently inspect the complete compiled raw type image. Removing both + -- a derivation and its manifest entry cannot hide an extracted nominal owner. + -- Aeneas also emits trait dictionaries as inductives. Their own extraction + -- metadata identifies them; excluding by a guessed name pattern could hide + -- an ordinary Rust type that still needs a model binding. + let traitCarriers := (Aeneas.Extract.rustTraitDecls.ext.getState env).filterMap + fun (_, _, info) => info.extract.map String.toName + let mut actualTypes : Array Name := #[] + for (constName, constInfo) in env.constants.toList do + if (`Zerocopy).isPrefixOf constName && env.getModuleIdxFor? constName == some typesModule && + !traitCarriers.any (fun carrier => constName == carrier || constName == (`Zerocopy).append carrier) then + if let .inductInfo _ := constInfo then + actualTypes := actualTypes.push constName + let missingTypes := actualTypes.filter (!expectedTypes.contains ·) + let extraTypes := expectedTypes.filter (!actualTypes.contains ·) + unless missingTypes.isEmpty && extraTypes.isEmpty do + throwError "Raw nominal types disagree with source bindings: missing {missingTypes}; unexpected {extraTypes}" + -- Compare registered providers too: a raw type can be present in extraction + -- and the manifest while its full mathematical provider is absent or extra. + let actualOwners := (← AeneasSpecs.modelOwnerNames).filter + (fun owner => env.getModuleIdxFor? (owner ++ `aeneasModel) == some modelsModule) + let missingOwners := expectedTypes.filter (!actualOwners.contains ·) + let extraOwners := actualOwners.filter (!expectedTypes.contains ·) + unless missingOwners.isEmpty && extraOwners.isEmpty do + throwError "Compiled mathematical models disagree with bindings: missing {missingOwners}; unexpected {extraOwners}" + let missing := expectedModels.filter (fun modelName => !actualModels.contains modelName) + let extra := actualModels.filter (fun modelName => !expectedModels.contains modelName) + unless missing.isEmpty && extra.isEmpty do + throwError "Compiled inline specification models disagree with bindings: missing {missing}; unexpected {extra}" + -- Sort only for reproducible diagnostics and dependency output. Coverage was + -- established from complete sets above, independent of declaration order. + required := required.qsort (fun a b => a.toString < b.toString) + let some checksModule := env.getModuleIdx? `Required + | throwError "Required checks must be imported from their own module" + for declName in required do + let some (.thmInfo info) := env.find? declName + | throwError "Missing required theorem {declName}" + let specName := (`Zerocopy.Specs).str declName.getString! + unless env.contains specName do + throwError "Missing inline specification {specName}" + -- Require the canonical theorem's type to name its exact spec. An arbitrary + -- term or independently reproved proposition could bypass the inline claim. + let actualSpec ← AeneasContracts.canonicalSpecName declName + unless actualSpec == specName do + throwError "{declName} must directly name its inline specification {specName}" + unless ← Meta.isDefEq info.type (mkConst specName) do + throwError "{declName} does not prove its inline specification {specName}" + AeneasSpecs.checkSpecContract specName + unless (env.getModuleIdxFor? declName).any proofModules.contains do + throwError "{declName} must be declared in a configured proof module" + let obligationName := (`Zerocopy.Obligations).str declName.getString! + let adequacyName := obligationName.appendAfter "_adequate" + let some (.thmInfo adequacy) := env.find? adequacyName + | throwError "Missing arbitrary-outcome required theorem {adequacyName}" + -- Reconstruct the arbitrary-outcome implication from compiled families. + -- A concrete checked theorem alone would not expose weak posts, accidental + -- divergence allowance, or a decoder/ghost that narrowed the required domain. + let adequacyType ← AeneasContracts.contractAdequacyType specName obligationName + unless ← Meta.isDefEq adequacy.type adequacyType do + throwError "{adequacyName} does not prove its arbitrary-outcome required implication" + unless env.getModuleIdxFor? adequacyName == some checksModule do + throwError "{adequacyName} must be declared in the required checks module" + -- The concrete witness is the separately named required proposition, and + -- must be owned by the check module along with the adequacy theorem. + let witnessName := obligationName.appendAfter "_checked" + let some (.thmInfo witness) := env.find? witnessName + | throwError "Missing independent required theorem {witnessName}" + unless ← Meta.isDefEq witness.type (mkConst obligationName) do + throwError "{witnessName} does not prove its independent required proposition" + unless env.getModuleIdxFor? witnessName == some checksModule do + throwError "{witnessName} must be declared in the required checks module" + let mut graph : Array Json := #[] + for declName in required do + -- Follow helpers in every audited handwritten and model module, including + -- private and generated declarations. Stop at registered callees, whose + -- own edges follow. Canonical proof ownership remains restricted above. + let mut pending := #[declName] + let mut visited : NameHashSet := {} + let mut actual : Array Name := #[] + while !pending.isEmpty do + let current := pending.back! + pending := pending.pop + if visited.contains current then continue + visited := visited.insert current + let some info := env.find? current + | throwError "Missing proof declaration {current}" + let references := info.type.getUsedConstants ++ + ((info.value? (allowOpaque := true)).map (·.getUsedConstants)).getD #[] + for used in references do + if used == declName then continue + if required.contains used then + unless actual.contains used do actual := actual.push used + else if (env.getModuleIdxFor? used).any auditModules.contains then + pending := pending.push used + actual := actual.qsort (fun a b => a.toString < b.toString) + graph := graph.push (Json.mkObj [ + ("theorem", Json.str declName.toString), + ("depends_on", Json.arr (actual.map (Json.str ∘ Name.toString)))]) + logInfo m!"Recorded proof dependencies of {required.size} theorems" + -- Audit generated specs/models and handwritten helpers, not only canonical + -- roots. A helper with an unapproved axiom must fail even if a current proof + -- happens not to use it. privateToUserName keeps private namespace helpers in + -- scope; module ownership catches auxiliaries with compiler-generated names. + let prefixes := #[`Zerocopy, `core.num, `AeneasContracts, `AeneasSpecs, + `ContractTests, `SpecsSyntaxTests, `SupportTests, `ContractAuditTests] + let mut audited := 0 + for (declName, _) in env.constants.toList do + if prefixes.any (·.isPrefixOf (privateToUserName declName)) || + (env.getModuleIdxFor? declName).any auditModules.contains || + env.getModuleIdxFor? declName == some checksModule then + let used ← collectAxioms declName + for ax in used do + -- These are Lean's permitted logical axioms. The only additional + -- allowances are the ABI data inputs whose types/read meanings were + -- checked above; sorryAx and user-defined proof axioms remain errors. + unless #[`propext, `Classical.choice, `Quot.sound].contains ax || + modelInputs.contains ax do + throwError "{declName} depends on unapproved axiom {ax}" + audited := audited + 1 + IO.FS.writeFile "proof-dependencies.json" (Json.arr graph).pretty + logInfo m!"Checked axiom dependencies of {audited} declarations" diff --git a/verification/aeneas/lean/ContractAuditTests.lean b/verification/aeneas/lean/ContractAuditTests.lean new file mode 100644 index 0000000000..cd2659ee31 --- /dev/null +++ b/verification/aeneas/lean/ContractAuditTests.lean @@ -0,0 +1,113 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import SpecsSyntax +public import RequiredContracts +@[expose] public section + +/-! +These adversarial families test the audits themselves. A family can agree with +a concrete specification yet ignore its outcome, add a premise, or hide the +extracted call in a helper's body or type. Each case below isolates one escape +route and checks its rejection. A wrong arbitrary return separately demonstrates +why narrowing the authored domain cannot imply a broader required promise. +-/ + +open Lean Aeneas Aeneas.Std AeneasSpecs +namespace ContractAuditTests + +def identity (n : Nat) : Result Nat := .ok n + +-- Concrete specialization can be correct even when a family ignores run. +@[aeneas_spec] abbrev ignored : Prop := + ∀ n : Nat, WP.spec (identity n) (fun output => output = n) +def ignored_contract (n : Nat) (_run : Result Nat) : Prop := + WP.spec (identity n) (fun output => output = n) +example : ∀ n : Nat, ignored_contract n (identity n) := by + intro n + exact WP.spec.ret rfl +/-- error: Abstract contract ContractAuditTests.ignored_contract does not use its execution outcome -/ +#guard_msgs in +run_elab AeneasSpecs.checkSpecContract `ContractAuditTests.ignored + +-- The family audit retains every premise of the authored contract. +@[aeneas_spec] abbrev narrowed : Prop := + ∀ n : Nat, WP.spec (identity n) (fun output => output = n) +def narrowed_contract (n : Nat) (run : Result Nat) : Prop := + n = 0 → WP.spec run (fun output => output = n) +/-- error: Abstract contract ContractAuditTests.narrowed_contract changed the specification's premises or postcondition -/ +#guard_msgs in +run_elab AeneasSpecs.checkSpecContract `ContractAuditTests.narrowed + +-- A narrowed authored domain does not imply a broader independent guarantee. +@[aeneas_spec] abbrev conditional : Prop := + ∀ n : Nat, n = 0 → WP.spec (identity n) (fun output => output = n) +def conditional_contract (n : Nat) (run : Result Nat) : Prop := + n = 0 → WP.spec run (fun output => output = n) +def broad_contract (n : Nat) (run : Result Nat) : Prop := + WP.spec run (fun output => output = n) +def broad : Prop := ∀ n : Nat, broad_contract n (identity n) +theorem conditional_proved : conditional := by + intro n _ + exact WP.spec.ret rfl +theorem broad_proved : broad := by + intro n + exact WP.spec.ret rfl +run_elab AeneasSpecs.checkSpecContract `ContractAuditTests.conditional +example : (contract_adequacy% conditional implies broad) = + (∀ n run, conditional_contract n run → broad_contract n run) := rfl +-- This is the exact type required by check_contract, with an explicit witness +-- showing why a concrete proof of identity cannot repair the missing domain. +theorem narrowing_fails_required_domain : + ¬ (contract_adequacy% conditional implies broad) := by + intro adequate + have rejected := adequate 1 (.ok 2) (by intro impossible; contradiction) + simp only [broad_contract, WP.spec_ok] at rejected + contradiction + +-- Independent expected families must also retain the abstract outcome. +def ignoredExpected_contract (n : Nat) (_run : Result Nat) : Prop := + WP.spec (identity n) (fun output => output = n) +def ignoredExpected : Prop := + ∀ n : Nat, ignoredExpected_contract n (identity n) +/-- error: Independent contract ContractAuditTests.ignoredExpected_contract must use its execution outcome -/ +#guard_msgs in +run_elab do + let _ ← AeneasContracts.contractAdequacyType + `ContractAuditTests.conditional `ContractAuditTests.ignoredExpected + +-- Outcome use alone does not protect against an embedded implementation proof. +-- Hide the call behind a helper to exercise the transitive dependency audit. +def hiddenCall (n : Nat) : Result Nat := identity n +def embeddedExpected_contract (n : Nat) (run : Result Nat) : Prop := + WP.spec run (fun output => output = n) ∧ + WP.spec (hiddenCall n) (fun output => output = n) +def embeddedExpected : Prop := + ∀ n : Nat, embeddedExpected_contract n (identity n) +/-- error: Independent contract ContractAuditTests.embeddedExpected_contract depends on its extracted operation -/ +#guard_msgs in +run_elab do + let _ ← AeneasContracts.contractAdequacyType + `ContractAuditTests.conditional `ContractAuditTests.embeddedExpected + +-- An opaque helper can retain the operation only in its unreduced type. Its +-- body is Nat.zero, so checking bodies alone misses this dependency. +opaque typeOnlyHelper : (fun _ : Result Nat => Nat) (identity 0) := Nat.zero + +def typeOnlyExpected_contract (n : Nat) (run : Result Nat) : Prop := + WP.spec run (fun output => output = Nat.add n typeOnlyHelper) +def typeOnlyExpected : Prop := + ∀ n : Nat, typeOnlyExpected_contract n (identity n) +/-- error: Independent contract ContractAuditTests.typeOnlyExpected_contract depends on its extracted operation -/ +#guard_msgs in +run_elab do + let _ ← AeneasContracts.contractAdequacyType + `ContractAuditTests.conditional `ContractAuditTests.typeOnlyExpected + +end ContractAuditTests diff --git a/verification/aeneas/lean/ContractSimps.lean b/verification/aeneas/lean/ContractSimps.lean new file mode 100644 index 0000000000..0fd9e459c2 --- /dev/null +++ b/verification/aeneas/lean/ContractSimps.lean @@ -0,0 +1,27 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Aeneas +public section + +/-! +Required-contract comparisons use a dedicated simplification set. Registering +it in this small module lets imported modules add normalizations before the +checker runs. The lemmas remain ordinary checked theorems; this attribute only +controls which rewrites the comparison's proof search attempts. +-/ + + +namespace AeneasContracts + +/-- Normalizations for independently authored required contracts. This +attribute is initialized before the importing checker registers its lemmas. -/ +register_simp_attr' contractSimpExt contractSimprocExt contract_simps + +end AeneasContracts diff --git a/verification/aeneas/lean/ContractTests.lean b/verification/aeneas/lean/ContractTests.lean new file mode 100644 index 0000000000..83fc943d03 --- /dev/null +++ b/verification/aeneas/lean/ContractTests.lean @@ -0,0 +1,111 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import ModelPrelude +@[expose] public section + +/-! +These kernel-checked examples explain Aeneas's native total and partial +contracts independently of our spec macro. They pin successful results, tuple +postconditions, panic rejection, and divergence behavior. The independently +written theorem types prevent a change in syntax from silently changing the +claim. Total contracts require successful execution even for a True +postcondition. Partial contracts may diverge, but still reject panic. +-/ +open Aeneas Aeneas.Std AeneasSpecs +namespace ContractTests + +-- Omitted requirements mean True; a successful result is still required. +theorem identity_spec (x : Nat) : + Result.ok x + ⦃ ret => ret = x ⦄ := by + exact WP.spec.ret rfl + +-- Named requirements are available without introductions in the proof body. +theorem transitive_spec (x y z : Nat) + (hxy : x = y) + (hyz : y = z) : + Result.ok x + ⦃ ret => ret = z ⦄ := by + exact WP.spec.ret (hxy.trans hyz) + +-- Reuse Aeneas's tuple-aware binders for returns and mutable post-states. +theorem tuple_spec (x : Nat) : + Result.ok (x, x + 1) + ⦃ ret x' => ret = x ∧ x' = x + 1 ⦄ := by + simp + +theorem diverging_spec : + (Result.div : Result Nat) + ⦃ _ => False ⦄div := by + exact WP.dspec.div _ + +theorem partial_identity_spec (x : Nat) : + Result.ok x + ⦃ ret => ret = x ⦄div := by + exact WP.dspec.ret rfl + +-- These independently written types pin the exported theorem types and outcomes. +example : ∀ x : Nat, WP.spec (Result.ok x) (fun ret => ret = x) := identity_spec +example : ∀ x y z : Nat, x = y → y = z → + WP.spec (Result.ok x) (fun ret => ret = z) := transitive_spec +example : ∀ x : Nat, WP.spec (Result.ok (x, x + 1)) + (fun (ret, x') => ret = x ∧ x' = x + 1) := tuple_spec +example : WP.dspec (Result.div : Result Nat) (fun _ => False) := diverging_spec +example : ∀ x : Nat, WP.dspec (Result.ok x) (fun ret => ret = x) := partial_identity_spec + +theorem panic_rejected : + ¬ WP.spec (Result.fail Error.panic : Result Nat) (fun _ => True) := by simp + +theorem divergence_rejected : + ¬ WP.spec (Result.div : Result Nat) (fun _ => True) := by simp + +-- Partial correctness must reject panic and incorrect successful returns too. +theorem partial_panic_rejected : + ¬ WP.dspec (Result.fail Error.panic : Result Nat) (fun _ => True) := by simp + +theorem partial_wrong_return_rejected : + ¬ WP.dspec (Result.ok 0) (fun ret : Nat => ret = 1) := by simp + +-- A generic identity shares one explicit dictionary and the same witness on +-- input and output, even when the dictionary rejects some representations. +theorem modeled_identity {Raw : Type} (provider : RustModel Raw) (raw : Raw) + (value : provider.Model) (h : provider.decode raw = some value) : + WP.spec (Result.ok raw) (fun out => + ∃ decoded, provider.decode out = some decoded ∧ decoded = value) := + WP.spec.ret ⟨value, h, rfl⟩ + +-- A dependent ghost retains its exact domain in mathematical input scope. +theorem modeled_position {Raw : Type} (provider : RustModel Raw) + (length : provider.Model → Nat) (raw : Raw) (value : provider.Model) + (h : provider.decode raw = some value) (position : Fin (length value)) : + WP.spec (Result.ok raw) (fun out => + ∃ decoded, provider.decode out = some decoded ∧ position.val < length decoded) := + WP.spec.ret ⟨value, h, position.isLt⟩ + +-- An accepted execution cannot return a decoder-rejected representation, even +-- when its authored postcondition contributes no information. +theorem rejected_output_rejected {Raw : Type} (provider : RustModel Raw) + (raw : Raw) (h : provider.decode raw = none) : + ¬ WP.spec (Result.ok raw) (fun out => + ∃ decoded, provider.decode out = some decoded ∧ True) := by + simp [WP.spec_ok, h] + +theorem partial_rejected_output_rejected {Raw : Type} (provider : RustModel Raw) + (raw : Raw) (h : provider.decode raw = none) : + ¬ WP.dspec (Result.ok raw) (fun out => + ∃ decoded, provider.decode out = some decoded ∧ True) := by + simp [h] + +-- Rust absence and Rust errors remain successful decoded constructor cases. +example : (@modelOption Usize (modelUScalar .Usize)).decode none = some none := rfl +example : (@modelRustResult Usize Unit (modelUScalar .Usize) modelUnit).decode + (.Err ()) = some (.Err ()) := rfl + +end ContractTests diff --git a/verification/aeneas/lean/Corollaries.lean b/verification/aeneas/lean/Corollaries.lean new file mode 100644 index 0000000000..edab313551 --- /dev/null +++ b/verification/aeneas/lean/Corollaries.lean @@ -0,0 +1,85 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Proofs +public import Aeneas +import all Init.Data.Nat.Power2.Basic +@[expose] public section + +/-! +A function contract is most useful when other proofs can consume it. These +corollaries compose the checked arithmetic operation lemmas into further +numeric consequences. They reuse the proved computation and discharge its +stated requirements, rather than proving a different calculation in its +place. + +The results remain conditional functional guarantees. They do not establish +pointer safety or prove that every Rust caller meets the contract's +requirements. +-/ +open Aeneas Aeneas.Std Aeneas.Std.Result +namespace Zerocopy.Corollaries +open Proofs Proofs.Raw + + +theorem max_preserves (a b : NonZeroUsize) (P : NonZeroUsize → Prop) + (ha : P a) + (hb : P b) : + util.max a b + ⦃ r => P r ⦄ := by + step with Proofs.Raw.max_spec a b as ⟨r, _, hchoice, _, _⟩ + rcases hchoice with h | h + · simpa only [h] using ha + · simpa only [h] using hb + +theorem min_preserves (a b : NonZeroUsize) (P : NonZeroUsize → Prop) + (ha : P a) + (hb : P b) : + util.min a b + ⦃ r => P r ⦄ := by + step with Proofs.Raw.min_spec a b as ⟨r, _, hchoice, _, _⟩ + rcases hchoice with h | h + · simpa only [h] using ha + · simpa only [h] using hb + +theorem round_down_aligned (n : Usize) (align : NonZeroUsize) + (hpow : (align.val : Nat).isPowerOfTwo) + (haligned : (n : Nat) % (align.val : Nat) = 0) : + util.round_down_to_next_multiple_of_alignment n align = .ok n := by + obtain ⟨m, hm, _, hexact, _, _, _⟩ := + WP.spec_imp_exists (Proofs.Raw.round_down_spec n align hpow) + have heq : m = n := UScalar.eq_of_val_eq (by + simpa only [haligned, Nat.sub_zero] using hexact) + simpa only [heq] using hm + +theorem round_down_idempotent (n : Usize) (align : NonZeroUsize) + (hpow : (align.val : Nat).isPowerOfTwo) : + (do + let m ← util.round_down_to_next_multiple_of_alignment n align + let m' ← util.round_down_to_next_multiple_of_alignment m align + ok (m, m')) + ⦃ m m' => m' = m ⦄ := by + step with Proofs.Raw.round_down_spec n align hpow as ⟨m, _, _, haligned, _, _⟩ + rw [round_down_aligned m align hpow haligned] + simp only [bind_ok, WP.spec_ok, WP.uncurry'_pair] + +theorem round_down_monotone (a b : Usize) (align : NonZeroUsize) + (hab : a ≤ b) + (hpow : (align.val : Nat).isPowerOfTwo) : + (do + let m ← util.round_down_to_next_multiple_of_alignment a align + let m' ← util.round_down_to_next_multiple_of_alignment b align + ok (m, m')) + ⦃ m m' => m ≤ m' ⦄ := by + step with Proofs.Raw.round_down_spec a align hpow as ⟨m, hbound, _, haligned, _, _⟩ + step with Proofs.Raw.round_down_spec b align hpow as ⟨m', _, _, _, _, hgreatest⟩ + apply (UScalar.le_equiv _ _).mpr + exact hgreatest m.val ((UScalar.le_equiv _ _).mp (le_trans hbound hab)) haligned + +end Zerocopy.Corollaries diff --git a/verification/aeneas/lean/DeriveModels.lean b/verification/aeneas/lean/DeriveModels.lean new file mode 100644 index 0000000000..8a3e51c336 --- /dev/null +++ b/verification/aeneas/lean/DeriveModels.lean @@ -0,0 +1,507 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import ModelPrelude +public import ModelCompletion +public meta import Lean +public section + +/-! +Rust values have an extracted representation and a mathematical interpretation. +This module builds the connection in two stages. A shape declares the decoded +field types using only mathematical child types. A provider later supplies the +actual child decoders and combines their accepted values with the local decoder. +Separating those stages lets ordinary support lemmas mention shapes before full +providers exist. + +The full decoder must visit every field even when the local model discards its +information. For an enum it visits only the selected constructor's payload. The +same constructor traversal produces a kernel-checked decomposition theorem, +which exposes those child witnesses to operation proofs. The registry preserves +nominal ownership across module imports; it supplies explicit providers rather +than allowing later authored instances to select a different interpretation. +-/ + +open Lean Elab Term Meta Aeneas.Std +namespace AeneasSpecs + +/-- The registration records the Rust owner, its authored mathematical type, +and the original generic count. The generated declarations have fixed names +under that owner, so they are derived rather than stored as independent state. -/ +structure ModelBinding where + raw : Name + model : Name + generics : Nat + deriving Inhabited + +meta def ModelBinding.fields (binding : ModelBinding) : Name := binding.raw ++ `Fields +meta def ModelBinding.decoder (binding : ModelBinding) : Name := binding.raw ++ `decode +meta def ModelBinding.provider (binding : ModelBinding) : Name := binding.raw ++ `aeneasModel + +-- A persistent extension carries model registrations into importing modules. +-- Lean merges imported entries before this module's entries, so nominal child +-- shapes and providers can be resolved without relying on declaration text. +private meta initialize modelExtension : + SimplePersistentEnvExtension ModelBinding (NameMap ModelBinding) ← + registerSimplePersistentEnvExtension { + addEntryFn := fun s b => s.insert b.raw b + addImportedFn := fun entries => entries.foldl (fun s es => + es.foldl (fun s b => s.insert b.raw b) s) {} + } + +meta def modelOwnerNames : MetaM (Array Name) := do + return ((modelExtension.getState (← getEnv)).toList.map (·.1)).toArray + +meta def modelBinding (raw : Name) : MetaM (Name × Name × Name × Name × Nat) := do + let some b := (modelExtension.getState (← getEnv)).find? raw + | throwError "Missing mathematical model binding for {raw}" + unless (← getEnv).contains b.provider && (← getEnv).contains b.decoder do + throwError "Missing full decoder or bound provider for {raw}" + return (b.fields, b.model, b.decoder, b.provider, b.generics) + +/-- One dictionary for every original Rust generic, including unused parameters. -/ +meta def withModelDictionaries (params : Array Expr) + (k : Array Expr → TermElabM α) : TermElabM α := do + let rec loop (i : Nat) (dicts : Array Expr) : TermElabM α := do + if h : i < params.size then + let param := params[i] + unless (← whnf (← inferType param)).isSort do + throwError "Only Rust type parameters are supported in model generation" + let classType ← mkAppM ``RustModel #[param] + withLocalDecl (Name.mkSimple s!"_rustModel{i}") .instImplicit classType fun dict => + loop (i + 1) (dicts.push dict) + else k dicts + loop 0 #[] + +/-- These restrictions describe the supported extracted shape. Indexed types +and recursive groups would require additional carrier and decoding semantics; +accepting them through the simple constructor traversal would invent a model. -/ +private meta def nominal (raw : Name) (count : Nat) : TermElabM InductiveVal := do + let info ← getConstInfoInduct raw + unless info.numParams == count && info.numIndices == 0 && + !info.isRec && info.all == [raw] do + throwError "Models require a nonrecursive, nonindexed nominal Rust type: {raw}" + pure info + +/-- Fixed structural selection. No global instance inference selects automatic +model meanings, even when authored ghosts later introduce competing instances. -/ +meta partial def boundModelProvider (raw : Expr) (params dicts : Array Expr) : MetaM Expr := do + rejectEscapingFunctions raw + let raw ← whnf raw + -- A generic's original dictionary fixes its meaning even when no field uses + -- it. Resolve it before structural cases or any authored local instances. + for i in [:params.size] do + if raw == params[i]! then return dicts[i]! + let args := raw.getAppArgs + let head := raw.getAppFn + let app (n : Name) (xs : Array Expr) : MetaM Expr := do + mkAppOptM n (xs.map some) + if head.isConstOf ``UScalar then return ← app ``modelUScalar args + if head.isConstOf ``IScalar then return ← app ``modelIScalar args + if head.isConstOf ``Bool then return mkConst ``modelBool + if head.isConstOf ``Nat then return mkConst ``modelNat + if head.isConstOf ``Int then return mkConst ``modelInt + -- Reduction expands Unit to PUnit.{1}. Compare the carrier itself so that + -- ordinary Rust functions returning () keep their total unit model. + if ← isDefEq raw (mkConst ``Unit) then return mkConst ``modelUnit + if head.isConstOf ``Prod || head.isConstOf ``core.result.Result then + let ma ← boundModelProvider args[0]! params dicts + let mb ← boundModelProvider args[1]! params dicts + return ← app (if head.isConstOf ``Prod then ``modelProd else ``modelRustResult) + (args ++ #[ma, mb]) + if head.isConstOf ``Option || head.isConstOf ``List || head.isConstOf ``Slice then + let ma ← boundModelProvider args[0]! params dicts + let provider := if head.isConstOf ``Option then ``modelOption + else if head.isConstOf ``List then ``modelList else ``modelSlice + return ← app provider (args ++ #[ma]) + if head.isConstOf ``Aeneas.Std.Array then + let ma ← boundModelProvider args[0]! params dicts + return ← app ``modelArray (args ++ #[ma]) + if head.isConstOf ``core.num.nonzero.NonZero then + let word ← whnf args[0]! + if word.getAppFn.isConstOf ``UScalar then + return ← app ``modelNonZeroUScalar #[word.appArg!, args[1]!] + if word.getAppFn.isConstOf ``IScalar then + return ← app ``modelNonZeroIScalar #[word.appArg!, args[1]!] + if let .const n _ := head then + if let some b := (modelExtension.getState (← getEnv)).find? n then + unless (← getEnv).contains b.provider do + throwError "Full model provider is not available for {n}" + let childDicts ← args.mapM fun a => boundModelProvider a params dicts + return ← app b.provider (args ++ childDicts) + -- A universal fallback would admit values without a defined interpretation. + -- Unsupported carriers must fail at model construction, not weaken a proof. + throwError "Unsupported opaque carrier or missing bound model provider: {raw}" + +/-- Reduce associated mathematical types recursively, while leaving values +alone. Nested containers then elaborate against ordinary mathematical carriers, +without asking instance inference to reverse-map a model type to a raw type. -/ +private meta partial def reduceModelTypes (e : Expr) : MetaM Expr := do + let e ← withTransparency .all (whnf e) + if e.isApp then + let mut args := #[] + for arg in e.getAppArgs do + let reducedArg ← if (← isType arg) then reduceModelTypes arg else pure arg + args := args.push reducedArg + return mkAppN e.getAppFn args + return e + +meta def providerModel (raw provider : Expr) : MetaM Expr := do + reduceModelTypes (← mkAppOptM ``RustModel.Model #[some raw, some provider]) + +meta def providerDecode (raw provider value : Expr) : MetaM Expr := + mkAppOptM ``RustModel.decode #[some raw, some provider, some value] + +/-- Shape families depend only on mathematical carrier types. No decoder +provider is required for a nominal child whose shape has already been declared. -/ +private meta partial def shapeType (raw : Expr) (params models : Array Expr) : MetaM Expr := do + rejectEscapingFunctions raw + let raw ← whnf raw + for i in [:params.size] do + if raw == params[i]! then return models[i]! + let args := raw.getAppArgs + let head := raw.getAppFn + if let .const n _ := head then + if let some b := (modelExtension.getState (← getEnv)).find? n then + let children ← args.mapM fun a => shapeType a params models + return ← mkAppOptM b.model (children.map some) + if head.isConstOf ``Prod || head.isConstOf ``core.result.Result then + let children ← args.mapM fun a => shapeType a params models + return mkAppN (← mkConstWithFreshMVarLevels head.constName!) children + if head.isConstOf ``Option || head.isConstOf ``List then + return mkAppN (← mkConstWithFreshMVarLevels head.constName!) + #[← shapeType args[0]! params models] + if head.isConstOf ``Slice then + return ← mkAppM ``SliceValue #[← shapeType args[0]! params models] + if head.isConstOf ``Aeneas.Std.Array then + return ← mkAppM ``ArrayValue #[← shapeType args[0]! params models, args[1]!] + -- Scalar and nonzero carriers have fixed native mathematical types. + providerModel raw (← boundModelProvider raw #[] #[]) + +/-- Shape generation binds one mathematical type per original generic. These +are types rather than RustModel dictionaries: declaring a child shape need not +wait for its decoder or any proofs used to construct that decoder. -/ +private meta def withModelTypes (params : Array Expr) + (k : Array Expr → TermElabM α) : TermElabM α := do + let rec loop (i : Nat) (models : Array Expr) : TermElabM α := do + if h : i < params.size then + let p := params[i] + let pName := (← p.fvarId!.getDecl).userName + let modelName := Name.mkSimple s!"{pName}Model" + withLocalDeclD modelName (← inferType p) fun model => loop (i + 1) (models.push model) + else k models + loop 0 #[] + +private meta def expressionText (e : Expr) : TermElabM String := do + return (← withOptions (fun o => (o.setBool `pp.explicit true).setBool `pp.universes false) + (ppExpr e)).pretty 100000 + +private meta def parameterText (params dicts : Array Expr) : TermElabM String := do + let mut text := "" + for p in params do + text := text ++ s!" ({(← p.fvarId!.getDecl).userName} : {← expressionText (← inferType p)})" + for d in dicts do + text := text ++ s!" [{(← d.fvarId!.getDecl).userName} : {← expressionText (← inferType d)}]" + return text + +private meta def parseCommand (text : String) : Command.CommandElabM Syntax := do + match Parser.runParserCategory (← getEnv) `command text with + | .ok stx => pure stx + | .error error => throwError "Generated mathematical declaration failed to parse: {error}\n{text}" + +/-- Mirror the raw constructor structure with decoded field types. Parsing +ordinary Lean declarations lets Lean enforce the resulting type and constructor +rules rather than installing unchecked expression representations of a shape. -/ +private meta def generateShape (raw : Name) (count : Nat) : Command.CommandElabM Unit := do + let text ← Command.liftTermElabM do + let info ← nominal raw count + forallTelescope info.type fun params _ => + withModelTypes params fun models => do + let binders ← parameterText models #[] + let shapeName := "Fields" + if let some structureInfo := getStructureInfo? (← getEnv) raw then + let ctorTy := mkAppN (mkConst info.ctors.head! (info.levelParams.map Level.param)) params + forallTelescope (← inferType ctorTy) fun fields _ => do + let mut text := s!"structure {shapeName}{binders} where\n" + for i in [:fields.size] do + let ty ← shapeType (← inferType fields[i]!) params models + text := text ++ s!" {structureInfo.fieldNames[i]!.getString!} : {← expressionText ty}\n" + return text + else + let mut text := s!"inductive {shapeName}{binders} where\n" + for ctor in info.ctors do + let ctorTy := mkAppN (mkConst ctor (info.levelParams.map Level.param)) params + let fieldsText ← forallTelescope (← inferType ctorTy) fun fields _ => do + let mut fieldsText := "" + for field in fields do + let ty ← shapeType (← inferType field) params models + fieldsText := fieldsText ++ s!" → {← expressionText ty}" + return fieldsText + -- Print actual user names, never internal free-variable identifiers. + let application ← models.foldlM (fun s e => do + pure (s ++ " " ++ (← e.fvarId!.getDecl).userName.toString)) "" + let fieldPrefix := if fieldsText.isEmpty then "" else fieldsText.drop 3 |>.toString + let arrow := if fieldsText.isEmpty then "" else " → " + text := text ++ s!"| {ctor.getString!} : {fieldPrefix}{arrow}@{shapeName}{application}\n" + return text + Command.withScope (fun scope => {scope with currNamespace := raw}) do + Command.elabCommand (← parseCommand text) + +syntax (name := modelStructure) "model " ident " where" Parser.Command.structFields : command +syntax (name := modelAlias) "model " ident " := " term : command +syntax "aeneas_model_shape " ident " with " num " type " "parameters" " begin " + command " end" : command + +private meta def registerShape (raw mathName : Name) (count : Nat) : Command.CommandElabM Unit := do + -- One raw owner has one registered interpretation. A second shape must not + -- silently replace the one already used by imported contracts or providers. + if (modelExtension.getState (← getEnv)).contains raw then + throwError "Duplicate mathematical model shape for {raw}" + modifyEnv (modelExtension.addEntry · { + raw, «model» := mathName, generics := count }) + +elab "derive_model_shape " rawId:ident " with " count:num " type " "parameters" : command => do + let raw ← Command.liftTermElabM (realizeGlobalConstNoOverloadWithInfo rawId) + generateShape raw count.getNat + registerShape raw (raw ++ `Fields) count.getNat + +elab_rules : command + | `(aeneas_model_shape $rawId:ident with $count:num type parameters begin $declaration:command end) => do + let raw ← Command.liftTermElabM (realizeGlobalConstNoOverloadWithInfo rawId) + generateShape raw count.getNat + let id : TSyntax `ident := ⟨declaration.raw[1]⟩ + let suffix ← if declaration.raw.getKind == ``modelStructure then do + let fileMap ← getFileMap + let column := declaration.raw[3].getPos?.map (fun p => (fileMap.toPosition p).column) |>.getD 2 + pure (" where\n" ++ String.ofList (List.replicate column ' ') ++ declaration.raw[3].reprint.getD "") + else if declaration.raw.getKind == ``modelAlias then + pure (" := " ++ declaration.raw[3].reprint.getD "") + else throwError "A type fence must contain exactly one model shape declaration" + -- Fields belongs to the generated structural carrier. An authored model + -- may alias it, but cannot overwrite the name and change field admission. + if id.getId == `Fields then throwError "Fields is the generated nominal field carrier" + let (binders, application, levelNames) ← Command.liftTermElabM do + let info ← nominal raw count.getNat + forallTelescope info.type fun params _ => withModelTypes params fun models => do + let binders ← parameterText models #[] + let application ← models.foldlM (fun s e => do + pure (s ++ " " ++ (← e.fvarId!.getDecl).userName.toString)) "" + return (binders, application, info.levelParams) + let suffix := if declaration.raw.getKind == ``modelAlias && declaration.raw[3].isIdent && + declaration.raw[3].getId == `Fields then " := @Fields" ++ application else suffix + let authored := raw ++ id.getId + let keyword := if declaration.raw.getKind == ``modelStructure then "@[ext] structure" else "abbrev" + Command.withScope (fun scope => + {scope with + currNamespace := raw + levelNames := levelNames ++ scope.levelNames + opts := scope.opts.setBool `autoImplicit false}) do + Command.elabCommand (← parseCommand s!"{keyword} {id.getId}{binders}{suffix}") + registerShape raw authored count.getNat + +/-- Finish elaboration and install a kernel-checked definition. Its safe status +forbids unsafe dependencies; `forceExpose` lets importing modules reduce it. +Proof terms nested in the value also need exported names, even when the decoder +is noncomputable and used only to state mathematical contracts. -/ +private meta def addDefinition (declName : Name) (levels : List Name) (value : Expr) : TermElabM Unit := do + synthesizeSyntheticMVarsNoPostponing + let value ← instantiateMVars value + -- Match native declaration assembly: nested proof auxiliaries must remain + -- available when downstream modules unfold the exposed decoder. + let value ← withExporting do + withDeclNameForAuxNaming declName do + abstractNestedProofs value + addDecl (.defnDecl { + «name» := declName, levelParams := levels, «type» := (← inferType value), value, + hints := .abbrev, safety := .safe }) (forceExpose := true) + -- Authored decoders may be logical noncomputable terms. The safe declaration + -- is kernel checked and exposed for subsequent associated-type reduction; + -- contracts do not require executable code for mathematical decoding. + modifyEnv (addNoncomputable · declName) + enableRealizationsForConst declName + +/-- Assemble a case expression using the original nominal constructors. +A structure has one case whose fields are projections. An enum has one branch +per constructor, and that branch receives only the selected payload. Both the +full decoder and its decomposition theorem use this traversal so they cannot +silently disagree about field order or constructor correspondence. -/ +private meta def nominalCases (raw fieldsName : Name) (info : InductiveVal) + (params : Array Expr) (rawValue resultType : Expr) + (branch : Array Expr → Name → TermElabM Expr) : TermElabM Expr := do + let levels := info.levelParams.map Level.param + if let some structureInfo := getStructureInfo? (← getEnv) raw then + branch (structureInfo.fieldNames.mapIdx fun i _ => mkProj raw i rawValue) + ((← getConstInfoInduct fieldsName).ctors.head!) + else + let mut branches := #[] + for ctor in info.ctors do + let ctorType ← inferType (mkAppN (mkConst ctor levels) params) + let body ← forallTelescope ctorType fun fields _ => do + mkLambdaFVars fields (← branch fields (fieldsName ++ Name.mkSimple ctor.getString!)) + branches := branches.push body + -- casesOn needs a motive: the result type for each possible raw value. + -- These decoders are not indexed, so the motive is constant in that value. + let motive ← mkLambdaFVars #[rawValue] resultType + pure (mkAppN (mkConst (mkCasesOnName raw) ((← getLevel resultType) :: levels)) + (params ++ #[motive, rawValue] ++ branches)) + +/-- Build the local decoder, wrap it with recursive field admission, prove +that wrapper's decomposition, and finally register the complete RustModel. +An absent local decoder is structural identity. An infallible authored decoder +returns a model; a fallible one returns Option model and can restrict admission. -/ +private meta def deriveProvider (raw : Name) (count : Nat) + (localDecoder : Option (Name × Syntax × Bool)) : TermElabM Unit := do + let info ← nominal raw count + let some b := (modelExtension.getState (← getEnv)).find? raw + | throwError "Missing Fields/model shape for {raw}" + if (← getEnv).contains b.provider then throwError "Duplicate model provider for {raw}" + let levels := info.levelParams.map Level.param + forallTelescope info.type fun params _ => withModelDictionaries params fun dicts => do + let all := params ++ dicts + let rawTy := mkAppN (mkConst raw levels) params + let models ← params.zip dicts |>.mapM fun (p, d) => providerModel p d + let fieldsTy ← mkAppOptM b.fields (models.map some) + let modelTy ← mkAppOptM b.model (models.map some) + let outputTy ← mkAppM ``Option #[modelTy] + let localFn ← withLocalDeclD (localDecoder.map (·.1) |>.getD `self) fieldsTy fun fields => do + let body ← match localDecoder with + | none => mkAppM ``Option.some #[fields] + | some (_, term, fallible) => do + let completed ← withRef term `(model_value $(⟨term⟩)) + let value ← elabTermEnsuringType completed (if fallible then outputTy else modelTy) + if fallible then pure value else mkAppM ``Option.some #[value] + mkLambdaFVars #[fields] body + addDefinition (raw ++ `decodeFields) info.levelParams (← mkLambdaFVars all localFn) + let localConst := mkAppN (mkConst (raw ++ `decodeFields) levels) all + let full ← withLocalDeclD `raw rawTy fun rawValue => do + -- Bind each child decoder before constructing Fields. Option.bind stops + -- on rejection; successful traversal supplies all decoded values in the + -- original constructor order, including values the local decoder forgets. + let decodeBranch (fields : Array Expr) (constructor : Name) : TermElabM Expr := do + let rec loop (i : Nat) (decoded : Array Expr) : TermElabM Expr := do + if h : i < fields.size then + let field := fields[i] + let fieldTy ← inferType field + let provider ← boundModelProvider fieldTy params dicts + let valueTy ← providerModel fieldTy provider + let decodedRaw ← providerDecode fieldTy provider field + withLocalDeclD (Name.mkSimple s!"field{i}") valueTy fun value => do + let rest ← loop (i+1) (decoded.push value) + let continuation ← mkLambdaFVars #[value] rest + mkAppM ``Option.bind #[decodedRaw, continuation] + else pure (mkApp localConst (mkAppN (mkConst constructor levels) (models ++ decoded))) + loop 0 #[] + let body ← nominalCases raw b.fields info params rawValue outputTy decodeBranch + mkLambdaFVars #[rawValue] body + addDefinition b.decoder info.levelParams (← mkLambdaFVars all full) + -- Keep child witnesses even when decodeFields intentionally forgets them. + -- This proposition follows the actual fixed providers and constructor path. + let decomposition ← withLocalDeclD `raw rawTy fun rawValue => + withLocalDeclD `model modelTy fun modelValue => do + let someModel ← mkAppM ``Option.some #[modelValue] + let branch (fields : Array Expr) (constructor : Name) : TermElabM Expr := do + -- This is the proposition corresponding to the Option.bind chain: + -- each child has a witness and its decoder equation, followed by the + -- local decoder equation. Keeping witnesses prevents lossy models + -- from erasing the parent's recursive admission obligations. + let rec decomposeLoop (i : Nat) (decoded : Array Expr) : TermElabM Expr := do + if h : i < fields.size then + let field := fields[i] + let fieldTy ← inferType field + let provider ← boundModelProvider fieldTy params dicts + let valueTy ← providerModel fieldTy provider + let decodedRaw ← providerDecode fieldTy provider field + withLocalDeclD (Name.mkSimple s!"field{i}") valueTy fun value => do + let equation ← mkEq decodedRaw (← mkAppM ``Option.some #[value]) + let rest ← decomposeLoop (i + 1) (decoded.push value) + let body ← mkAppM ``And #[equation, rest] + mkAppM ``Exists #[← mkLambdaFVars #[value] body] + else + let fields ← mkAppOptM constructor ((models ++ decoded).map some) + mkEq (mkApp localConst fields) someModel + decomposeLoop 0 #[] + let rhs ← nominalCases raw b.fields info params rawValue (mkSort .zero) branch + let lhs ← mkEq (mkApp (mkAppN (mkConst b.decoder levels) all) rawValue) someModel + let proposition ← mkAppM ``Iff #[lhs, rhs] + -- The bridge is proved by ordinary case splitting and bind expansion. + -- It is not an axiom asserted merely because generation succeeded. + let proofSyntax ← match Parser.runParserCategory (← getEnv) `term + s!"by cases raw <;> simp only [{b.decoder}, Option.bind_eq_some_iff]" with + | .ok stx => pure stx + | .error error => throwError "Generated decoder decomposition failed to parse: {error}" + let proof ← withOptions (fun o => o.setBool `linter.unusedSimpArgs false) + (elabTermEnsuringType proofSyntax proposition) + mkLambdaFVars (all ++ #[rawValue, modelValue]) proof + synthesizeSyntheticMVarsNoPostponing + let decomposition ← instantiateMVars decomposition + addDecl (.thmDecl { + «name» := raw ++ `decode_decompose, levelParams := info.levelParams, + «type» := (← inferType decomposition), value := decomposition }) (forceExpose := true) + let provider ← mkAppOptM ``RustModel.mk #[some rawTy, some modelTy, + some (mkAppN (mkConst b.decoder levels) all)] + addDefinition b.provider info.levelParams (← mkLambdaFVars all provider) + setReducibilityStatus b.provider .reducible + addInstance b.provider .global 1000 + +elab "derive_rust_model " rawId:ident " with " count:num " type " "parameters" : command => + Command.liftTermElabM do + deriveProvider (← realizeGlobalConstNoOverloadWithInfo rawId) count.getNat none + +elab "derive_rust_model " rawId:ident " with " count:num " type " "parameters" + " decode " self:ident " => " body:term : command => Command.liftTermElabM do + deriveProvider (← realizeGlobalConstNoOverloadWithInfo rawId) count.getNat + (some (self.getId, body, false)) + +elab "derive_rust_model " rawId:ident " with " count:num " type " "parameters" + " decode? " self:ident " => " body:term : command => Command.liftTermElabM do + deriveProvider (← realizeGlobalConstNoOverloadWithInfo rawId) count.getNat + (some (self.getId, body, true)) + +/-- Audit the elaborated provider against its registered raw owner, mathematical +type, full decoder, and generic count. Definitional equality unfolds actual +expressions, so matching names alone cannot attach a provider to a wrong type. -/ +elab "check_model_binding " rawId:ident " with " count:num " type " "parameters" : command => + Command.liftTermElabM do + let raw ← realizeGlobalConstNoOverloadWithInfo rawId + let (fields, mathName, decoder, provider, generics) ← modelBinding raw + unless generics == count.getNat do throwError "Wrong generic count for {raw}" + let info ← nominal raw count.getNat + forallTelescope info.type fun params _ => withModelDictionaries params fun dicts => do + let all := params ++ dicts + let rawTy := mkAppN (mkConst raw (info.levelParams.map Level.param)) params + let providerExpr := mkAppN (mkConst provider (info.levelParams.map Level.param)) all + unless (← isDefEq (← inferType providerExpr) (← mkAppM ``RustModel #[rawTy])) do + throwError "Provider is attached to the wrong raw carrier: {raw}" + unless (← withTransparency .all (isDefEq (← providerModel rawTy providerExpr) + (← mkAppOptM mathName ((← params.zip dicts |>.mapM fun (p, d) => providerModel p d).map some)))) do + throwError "Provider model is not the bound mathematical type: {raw}" + withLocalDeclD `raw rawTy fun value => do + unless (← withTransparency .all (isDefEq (← providerDecode rawTy providerExpr value) + (mkAppN (mkConst decoder (info.levelParams.map Level.param)) (all.push value)))) do + throwError "Provider decoder is not the bound full decoder: {raw}" + unless (← getEnv).contains fields do throwError "Missing Fields carrier: {raw}" + +/-- Check every original input and the successful output before a function's +specification is authored. Unsupported borrowed continuations or missing models +must fail even if a later clause would ignore the affected value. -/ +elab "check_model_inputs " functionId:ident " with " count:num " type " "parameters" : command => + Command.liftTermElabM do + let function ← realizeGlobalConstNoOverloadWithInfo functionId + forallTelescope (← getConstInfo function).type fun inputs result => do + unless count.getNat ≤ inputs.size do throwError "Invalid Rust generic prefix count" + let params := inputs.extract 0 count.getNat + withModelDictionaries params fun dicts => do + for input in inputs.extract count.getNat inputs.size do + let _ ← boundModelProvider (← inferType input) params dicts + let result ← whnf result + unless result.isAppOfArity ``Aeneas.Std.Result 1 do + throwError "Unsupported result or escaping borrow in {function}" + let _ ← boundModelProvider result.appArg! params dicts + +end AeneasSpecs diff --git a/verification/aeneas/lean/InspectSpec.lean b/verification/aeneas/lean/InspectSpec.lean new file mode 100644 index 0000000000..5eca283256 --- /dev/null +++ b/verification/aeneas/lean/InspectSpec.lean @@ -0,0 +1,79 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +import Specs +open Lean Elab Command Meta AeneasSpecs Aeneas.Std + +/-! +`inspect_spec` explains a compiled specification: its original raw call, +selected providers, decoded input premises, result admission, and postconditions. +It reads the elaborated Lean expressions used by the checker. It does not +reconstruct a contract from annotation text or run a new verification build. +The shape checks below keep the report tied to the generated decoded-result +form, so inspection fails if that form no longer supports the reported meaning. +-/ + + +private meta def inspectionExpr (e : Expr) : MetaM Format := + withOptions (fun o => o.setBool `pp.proofs true) (ppExpr e) + +/-- Inspect compiled expressions, keeping the same original-call audit as Check. +No syntax expansion or independent specification interpretation is involved. -/ +elab "inspect_spec " candidate:ident : command => liftTermElabM do + let specName ← Elab.realizeGlobalConstNoOverloadWithInfo candidate + let (modelName, originalCount) ← specBinding specName + let info ← getConstInfo specName + let some proposition := info.value? | throwError "Missing specification value {specName}" + logInfo m!"Elaborated proposition {specName}:\n{← inspectionExpr proposition}" + forallTelescope proposition fun inputs body => do + let args := body.consumeMData.getAppArgs + let call := args[1]! + logInfo m!"Original target: {modelName}\nOriginal raw call: {← withOptions (fun o => (o.setBool `pp.explicit true).setBool `pp.fullNames true) (inspectionExpr call)}" + let mode := if body.consumeMData.getAppFn.isConstOf ``WP.spec then "total (WP.spec)" + else "partial (WP.dspec)" + logInfo m!"Execution: {mode}" + for i in [:inputs.size] do + let input := inputs[i]! + let decl ← input.fvarId!.getDecl + let ty := decl.type.consumeMData + -- Binder position identifies original raw/type arguments. Later shapes + -- are classified for reporting only; this is not a second domain checker. + if i < originalCount then + logInfo m!"Original binder {decl.userName}: {← inspectionExpr ty}" + else if ty.isAppOfArity ``RustModel 1 then + logInfo m!"Provider binder (original dictionary or ghost) {decl.userName}: {← inspectionExpr ty}" + else if ty.isAppOfArity ``Eq 3 && ty.getAppArgs[1]!.isAppOfArity ``RustModel.decode 3 then + let decoding := ty.getAppArgs[1]!.getAppArgs + logInfo m!"Input domain {decl.userName}: {← inspectionExpr ty}\nSelected provider: {← withOptions (fun o => (o.setBool `pp.explicit true).setBool `pp.fullNames true) (inspectionExpr decoding[1]!)}" + else if ← isProp ty then + logInfo m!"Premise or proposition-valued ghost {decl.userName}: {← inspectionExpr ty}" + else + logInfo m!"Mathematical witness or ghost {decl.userName}: {← inspectionExpr ty}" + let post := args[2]! + -- Unpack exactly one raw output, one existential model witness, and its + -- decoder equation. Check variable identities so a report cannot call a + -- different result's decoding the admission condition for this return. + lambdaTelescope post fun resultInputs resultBody => do + unless resultInputs.size == 1 && resultBody.isAppOfArity ``Exists 2 do + throwError "Specification {specName} lacks the expected decoded-result existential" + lambdaTelescope resultBody.getAppArgs[1]! fun witnesses conjunction => do + unless witnesses.size == 1 && conjunction.isAppOfArity ``And 2 do + throwError "Specification {specName} lacks the expected result decoding conjunction" + let equation := conjunction.getAppArgs[0]! + unless equation.isAppOfArity ``Eq 3 && + equation.getAppArgs[1]!.isAppOfArity ``RustModel.decode 3 do + throwError "Specification {specName} lacks an explicit result decoder equation" + let decoding := equation.getAppArgs[1]!.getAppArgs + let accepted := equation.getAppArgs[2]! + unless decoding[2]!.consumeMData == resultInputs[0]! && + accepted.isAppOfArity ``Option.some 2 && + accepted.getAppArgs[1]!.consumeMData == witnesses[0]! do + throwError "Specification {specName} does not require successful decoding of its original result" + logInfo m!"Result domain: {← inspectionExpr (← inferType witnesses[0]!)}\nSelected result provider: {← withOptions (fun o => (o.setBool `pp.explicit true).setBool `pp.fullNames true) (inspectionExpr decoding[1]!)}\nRequired result decoding: {← inspectionExpr equation}\nPostconditions: {← inspectionExpr conjunction.getAppArgs[1]!}" + let axioms ← collectAxioms specName + logInfo m!"Compiled axiom dependencies: {axioms}" diff --git a/verification/aeneas/lean/LayoutMath.lean b/verification/aeneas/lean/LayoutMath.lean new file mode 100644 index 0000000000..bbf2deb872 --- /dev/null +++ b/verification/aeneas/lean/LayoutMath.lean @@ -0,0 +1,711 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Arithmetic +import all Init.Data.Nat.Power2.Basic +@[expose] public section + +/-! +This module states layout mathematics independently of extracted Rust bodies. +Description retains recursive record nesting and each field's complete size. +Formula normalizes that description into a compact size calculation, and the +compile theorems prove that normalization preserves every metadata value. +LayoutValue then describes fragment operations used by the runtime layout +API. + +All sizes here are natural numbers: arithmetic cannot overflow. Explicit fit +predicates identify when a fragment operation can be implemented with a +machine word. A fragment need not describe a completed Rust type. +SEMANTICS.md states the separate premise connecting completed recursive +descriptions to Rust; the algebra in this module alone does not establish +compiler correspondence. +-/ +namespace Zerocopy.LayoutMath + +/-- Mathematical ceiling to an alignment, without machine overflow. -/ +def roundUp (n a : Nat) : Nat := n + (a - n % a) % a + +theorem roundUp_properties (n a : Nat) (ha : 0 < a) : + n ≤ roundUp n a ∧ roundUp n a < n + a ∧ roundUp n a % a = 0 := by + have hr := Nat.mod_lt n ha + have hp := Nat.mod_lt (a - n % a) ha + refine ⟨by unfold roundUp; omega, by unfold roundUp; omega, ?_⟩ + unfold roundUp + by_cases hz : n % a = 0 + · simp [hz] + · have hp' : (a - n % a) % a = a - n % a := + Nat.mod_eq_of_lt (by omega) + rw [Nat.add_mod, Nat.mod_mod, hp'] + rw [show n % a + (a - n % a) = a by omega, Nat.mod_self] + +theorem roundUp_le (n a q : Nat) (ha : 0 < a) + (hn : n ≤ q) (hq : q % a = 0) : roundUp n a ≤ q := by + have hp := (roundUp_properties n a ha).2.2 + have hbound := Nat.mod_lt (a - n % a) ha + have hq' : (n + (q - n)) % a = 0 := by + simpa only [Nat.add_sub_of_le hn] using hq + have h := (Arithmetic.padding_properties n a _ ha hbound hp).2.1 _ hq' + unfold roundUp + omega + +theorem roundUp_eq (n a : Nat) (h : n % a = 0) : roundUp n a = n := by + simp [roundUp, h] + +theorem roundUp_mono {n m a : Nat} (ha : 0 < a) (h : n ≤ m) : + roundUp n a ≤ roundUp m a := by + have hm := roundUp_properties m a ha + exact roundUp_le n a _ ha (by omega) hm.2.2 + +theorem roundUp_shift (n k a : Nat) (hk : k % a = 0) : + roundUp (k + n) a = k + roundUp n a := by + simp only [roundUp, Nat.add_mod, hk, Nat.zero_add, Nat.mod_mod] + omega + +theorem roundUp_compose (n a b : Nat) (ha : 0 < a) (hb : 0 < b) + (hab : a ∣ b) : roundUp (roundUp n a) b = roundUp n b := by + have hn := roundUp_properties n a ha + have hnb := roundUp_properties n b hb + have hba : roundUp n b % a = 0 := + Nat.mod_eq_zero_of_dvd (dvd_trans hab (Nat.dvd_of_mod_eq_zero hnb.2.2)) + have hle := roundUp_le n a _ ha hnb.1 hba + apply Nat.le_antisymm + · exact roundUp_le _ b _ hb hle hnb.2.2 + · exact roundUp_mono hb hn.1 + +/-- Move a smaller inner ceiling across an addition before a larger ceiling. -/ +theorem roundUp_swap (c x a b : Nat) (ha : 0 < a) (hb : 0 < b) + (hab : a ∣ b) : + roundUp (c + roundUp x a) b = roundUp (roundUp c a + x) b := by + have hc := (roundUp_properties c a ha).2.2 + have hx := (roundUp_properties x a ha).2.2 + calc + roundUp (c + roundUp x a) b = roundUp (roundUp (c + roundUp x a) a) b := + (roundUp_compose _ a b ha hb hab).symm + _ = roundUp (roundUp c a + roundUp x a) b := by + rw [Nat.add_comm c, roundUp_shift c _ a hx, Nat.add_comm (roundUp x a)] + _ = roundUp (roundUp (roundUp c a + x) a) b := by + rw [roundUp_shift x _ a hc] + _ = roundUp (roundUp c a + x) b := roundUp_compose _ a b ha hb hab + +theorem roundUp_inner (c x a b : Nat) (ha : 0 < a) (hba : b ∣ a) : + roundUp (c + roundUp x a) b = roundUp c b + roundUp x a := by + have hx : roundUp x a % b = 0 := Nat.mod_eq_zero_of_dvd + (dvd_trans hba (Nat.dvd_of_mod_eq_zero (roundUp_properties x a ha).2.2)) + rw [Nat.add_comm c, roundUp_shift c _ b hx, Nat.add_comm] + +theorem normalize_phase (c x a : Nat) : + (c - c % a) + roundUp (c % a + x) a = roundUp (c + x) a := by + have hm := Nat.mod_le c a + have hk : (c - c % a) % a = 0 := by + rw [← Nat.div_mul_self_eq_mod_sub_self, Nat.mul_mod_left] + rw [← roundUp_shift _ _ a hk] + congr 1 + omega + +theorem power_alignments (i j : Nat) : + if i ≤ j then 2 ^ i ∣ 2 ^ j else 2 ^ j ∣ 2 ^ i := by + split + · exact Nat.pow_dvd_pow 2 ‹i ≤ j› + · exact Nat.pow_dvd_pow 2 (by omega) + +/-- Normal form used only as the target of the refinement proof. -/ +/- A normalized complete-size calculation: base + roundUp (phase + n * elem) +align. Physical offset is retained separately, because it need not equal the +normalized base. Normalization replaces recursive rounding only after proving +equivalence. +-/ +structure Formula where + base : Nat + phase : Nat + align : Nat + elem : Nat + offset : Nat +deriving DecidableEq + +/- Require a positive rounding alignment and a phase below it. This condition +describes the normalized formula; it does not assert physical realizability +of a completed Rust type. +-/ +def Formula.valid (f : Formula) : Prop := 0 < f.align ∧ f.phase < f.align +/- Evaluate complete size for an arbitrary number of trailing bytes, retaining +the inner alignment rounding. Capacity is first computed in this byte domain. +-/ +def Formula.bytes (f : Formula) (b : Nat) := f.base + roundUp (f.phase + b) f.align +/- Convert metadata (an element count) to trailing bytes before evaluating the +complete size. All arithmetic here is unbounded. +-/ +def Formula.size (f : Formula) (n : Nat) := f.bytes (n * f.elem) + +theorem Formula.size_mono (f : Formula) (ha : 0 < f.align) {n m : Nat} (h : n ≤ m) : + f.size n ≤ f.size m := by + unfold Formula.size Formula.bytes + exact Nat.add_le_add_left (roundUp_mono ha + (Nat.add_le_add_left (Nat.mul_le_mul_right f.elem h) f.phase)) f.base + +/- For positive alignments where the smaller divides the larger, normalize an +additional outer rounding step. When the outer alignment is larger, move +completed inner rounding into base and keep the residual phase; otherwise +preserve inner rounding and align base. pad_size proves this conditional +rule. +-/ +def Formula.pad (f : Formula) (outer : Nat) : Formula := + if f.align < outer then + let fixed := roundUp f.base f.align + f.phase + { f with base := fixed - fixed % outer, phase := fixed % outer, align := outer } + else { f with base := roundUp f.base outer } + +theorem pad_size (f : Formula) (n outer : Nat) (hf : 0 < f.align) + (ho : 0 < outer) + (hd : if f.align < outer then f.align ∣ outer else outer ∣ f.align) : + (f.pad outer).size n = roundUp (f.size n) outer := by + unfold Formula.pad Formula.size Formula.bytes + split <;> rename_i h + · simp only [h, if_true] at hd + dsimp only + rw [normalize_phase] + rw [roundUp_swap _ _ _ _ hf ho hd] + congr 1 + omega + · simp only [h, if_false] at hd + dsimp only + exact (roundUp_inner _ _ _ _ hf hd).symm + +theorem pad_valid (f : Formula) (outer : Nat) (hf : f.valid) (ho : 0 < outer) : + (f.pad outer).valid := by + unfold Formula.pad Formula.valid + split + · exact ⟨ho, Nat.mod_lt _ ho⟩ + · exact hf + +theorem pad_completed (f : Formula) (outer : Nat) (ho : 0 < outer) : + outer ≤ (f.pad outer).align ∧ (f.pad outer).base % outer = 0 := by + unfold Formula.pad + split <;> rename_i h + · dsimp only + refine ⟨by omega, ?_⟩ + rw [← Nat.div_mul_self_eq_mod_sub_self, Nat.mul_mod_left] + · dsimp only + exact ⟨by omega, (roundUp_properties _ _ ho).2.2⟩ + +/- Move the logical beginning by b bytes and replace element stride with e. +Carry the aligned portion into base and retain the remainder as phase, so the +complete-size calculation survives the change of coordinates. +-/ +def Formula.advance (f : Formula) (b e : Nat) : Formula := + let shifted := f.phase + b + { f with base := f.base + shifted - shifted % f.align, + phase := shifted % f.align, elem := e } + +theorem advance_size (f : Formula) (b e n : Nat) : + (f.advance b e).size n = f.base + roundUp (f.phase + b + n * e) f.align := by + have hm := Nat.mod_le (f.phase + b) f.align + unfold Formula.advance Formula.size Formula.bytes + dsimp only + rw [show f.base + (f.phase + b) - (f.phase + b) % f.align = + f.base + ((f.phase + b) - (f.phase + b) % f.align) by omega] + rw [Nat.add_assoc, normalize_phase] + +theorem aligned_element_size (f : Formula) (n : Nat) (he : f.elem % f.align = 0) : + f.size n = f.size 0 + n * f.elem := by + have hm : (n * f.elem) % f.align = 0 := by simp [Nat.mul_mod, he] + unfold Formula.size Formula.bytes + simp only [Nat.zero_mul, Nat.add_zero] + rw [Nat.add_comm f.phase, roundUp_shift _ _ _ hm] + omega + +theorem roundUp_le_budget (n a q : Nat) (ha : 0 < a) : + roundUp n a ≤ q ↔ n ≤ q - q % a := by + have hfloor := Arithmetic.round_down_properties q a (q - q % a) ha rfl + have hceil := roundUp_properties n a ha + constructor + · intro h + have := hfloor.2.2.2 _ h hceil.2.2 + omega + · intro h + have := roundUp_le n a (q - q % a) ha h hfloor.2.1 + omega + +/-- Largest fitting byte count, including rounding plateaus. -/ +def Formula.capacity (f : Formula) (available : Nat) : Option Nat := + if f.bytes 0 ≤ available then + some ((available - f.base) - (available - f.base) % f.align - f.phase) + else none + +theorem capacity_spec (f : Formula) (available : Nat) (ha : 0 < f.align) : + match f.capacity available with + | none => ∀ b, available < f.bytes b + | some cap => ∀ b, f.bytes b ≤ available ↔ b ≤ cap := by + unfold Formula.capacity + by_cases h : f.bytes 0 ≤ available + · simp only [h, if_true] + have hbase : f.base ≤ available := by unfold Formula.bytes at h; omega + have hphase : f.phase ≤ (available - f.base) - (available - f.base) % f.align := by + apply (roundUp_le_budget _ _ _ ha).mp + unfold Formula.bytes at h + simp only [Nat.add_zero] at h + omega + intro b + have hb := roundUp_le_budget (f.phase + b) f.align (available - f.base) ha + unfold Formula.bytes + omega + · simp only [h, if_false] + intro b + have hmono := roundUp_mono ha (show f.phase + 0 ≤ f.phase + b by omega) + unfold Formula.bytes at * + omega + +theorem roundUp_phase (p a : Nat) (_ha : 0 < a) (hp : p < a) : + roundUp p a = if p = 0 then 0 else a := by + by_cases h : p = 0 + · simp [roundUp, h] + · unfold roundUp + rw [Nat.mod_eq_of_lt hp, Nat.mod_eq_of_lt (show a - p < a by omega)] + simp only [h, if_false] + omega + +theorem floor_shift (k x a : Nat) (hk : k % a = 0) : + (k + x) - (k + x) % a = k + (x - x % a) := by + have := Nat.mod_le x a + rw [Nat.add_mod, hk, Nat.zero_add, Nat.mod_mod] + omega + +theorem aligned_or (n p a : Nat) (ha : a.isPowerOfTwo) + (hn : n % a = 0) (hp : p < a) : n ||| p = n + p := by + obtain ⟨k, hk⟩ := ha + rw [hk] at hn hp + have h : n = 2 ^ k * (n / 2 ^ k) := by + rw [Nat.mul_comm, Nat.div_mul_self_eq_mod_sub_self, hn, Nat.sub_zero] + rw [h] + exact (Nat.two_pow_add_eq_or_of_lt hp _).symm + +/- A sufficient, deliberately conservative test for equal size sequences. The +alternatives cover linear aligned strides or identical rounding phases. +Failure of this test does not prove that the sequences differ. +-/ +def Formula.sameSequence (f g : Formula) : Prop := + f.elem = g.elem ∧ f.size 0 = g.size 0 ∧ + ((f.elem % f.align = 0 ∧ g.elem % g.align = 0) ∨ + (f.align = g.align ∧ f.phase = g.phase)) + +theorem same_sequence_sound (f g : Formula) (h : f.sameSequence g) (n : Nat) : + f.size n = g.size n := by + obtain ⟨he, hzero, hcases⟩ := h + rcases hcases with ⟨hf, hg⟩ | ⟨ha, hp⟩ + · rw [aligned_element_size f n hf, aligned_element_size g n hg, he, hzero] + · have hbase : f.base = g.base := by + unfold Formula.size Formula.bytes at hzero + simp only [Nat.zero_mul, Nat.add_zero] at hzero + rw [ha, hp] at hzero + omega + simp only [Formula.size, Formula.bytes, hbase, hp, he, ha] + +theorem no_dynamic_padding (f : Formula) (hzero : f.size 0 = f.offset) + (he : f.elem % f.align = 0) (n : Nat) : f.size n = f.offset + n * f.elem := by + rw [aligned_element_size f n he, hzero] + +theorem maximal_metadata (f : Formula) (available cap : Nat) (he : 0 < f.elem) + (h : f.capacity available = some cap) (ha : 0 < f.align) : + ∀ n, f.size n ≤ available ↔ n ≤ cap / f.elem := by + have hc := capacity_spec f available ha + rw [h] at hc + intro n + unfold Formula.size + rw [hc] + exact Nat.le_div_iff_mul_le he |>.symm + +/- Model a checked machine-size result by bounding the unbounded formula. None +means numerical overflow, not execution failure or decoder rejection. +-/ +def Formula.checkedSize (f : Formula) (limit n : Nat) : Option Nat := + if f.size n ≤ limit then some (f.size n) else none + +theorem checked_size_spec (f : Formula) (limit n : Nat) : + (f.checkedSize limit n = some (f.size n) ↔ f.size n ≤ limit) ∧ + (f.checkedSize limit n = none ↔ limit < f.size n) := by + unfold Formula.checkedSize + split <;> simp_all + +/-- Recursive record semantics, retaining each inner field's complete size. -/ +/- A recursive layout description retains every inner field's complete size. +Alignment exponents encode powers of two. A record contributes a fixed +leading region and places its trailing description under optional packing. +-/ +inductive Description where + | slice (elem alignExponent : Nat) + | record (leading minAlignExponent : Nat) (packedExponent : Option Nat) + (tail : Description) +deriving DecidableEq + +/- Compute the record alignment from its declared minimum and packed field +alignment. Packing limits field placement; it does not remove inner padding. +-/ +def Description.alignExponent : Description → Nat + | .slice _ a => a + | .record _ minimum packed tail => max minimum + (match packed with | none => tail.alignExponent | some p => min p tail.alignExponent) + +/- Cap the field placement alignment by the packing bound, when present. +-/ +def Description.fieldAlignExponent (packed : Option Nat) (tail : Description) : Nat := + match packed with | none => tail.alignExponent | some p => min p tail.alignExponent + +/- Place the field, add its full recursive size, then round the containing +record. The order matters: packing the outer record cannot flatten an inner +field's own final rounding. +-/ +def Description.size : Description → Nat → Nat + | .slice elem _, n => n * elem + | d@(.record leading _ packed tail), n => + roundUp (roundUp leading (2 ^ fieldAlignExponent packed tail) + tail.size n) + (2 ^ d.alignExponent) + +/- Track the physical start of the ultimate slice through every containing +record. This observation is independent of normalized formula base and phase. +-/ +def Description.offset : Description → Nat + | .slice _ _ => 0 + | .record leading _ packed tail => + roundUp leading (2 ^ fieldAlignExponent packed tail) + tail.offset + +def Description.elem : Description → Nat + | .slice e _ => e + | .record _ _ _ tail => tail.elem + +/- Require the slice element stride to be divisible by its alignment. Every +record retains that same obligation through its trailing description. +-/ +def Description.valid : Description → Prop + | .slice elem a => elem % 2 ^ a = 0 + | .record _ _ _ tail => tail.valid + +/- Normalize recursively without inspecting generated Rust code. compile_size, +compile_offset, and compile_elem prove that all retained observations agree. +-/ +def Description.compile : Description → Formula + | .slice elem a => ⟨0, 0, 2 ^ a, elem, 0⟩ + | d@(.record leading _ packed tail) => + let placement := roundUp leading (2 ^ fieldAlignExponent packed tail) + let inner := tail.compile + ({ inner with base := placement + inner.base, + offset := placement + inner.offset }).pad (2 ^ d.alignExponent) + +theorem compile_power (d : Description) : ∃ k, d.compile.align = 2 ^ k := by + induction d with + | slice e a => exact ⟨a, rfl⟩ + | record leading minimum packed tail ih => + obtain ⟨k, hk⟩ := ih + unfold Description.compile Formula.pad + dsimp only + split + · exact ⟨_, rfl⟩ + · exact ⟨k, hk⟩ + +theorem compile_size (d : Description) (h : d.valid) (n : Nat) : + d.compile.size n = d.size n := by + induction d with + | slice e a => + simp only [Description.compile, Formula.size, Formula.bytes, + Description.size, Nat.zero_add] + apply roundUp_eq + simp only [Description.valid] at h + simp [Nat.mul_mod, h] + | record leading minimum packed tail ih => + obtain ⟨k, hk⟩ := compile_power tail + let placement := roundUp leading (2 ^ Description.fieldAlignExponent packed tail) + let f : Formula := { tail.compile with + base := placement + tail.compile.base + offset := placement + tail.compile.offset } + change (f.pad (2 ^ (Description.record leading minimum packed tail).alignExponent)).size n = _ + have hf : 0 < f.align := by dsimp [f]; rw [hk]; exact Nat.two_pow_pos k + have hd : if f.align < 2 ^ (Description.record leading minimum packed tail).alignExponent + then f.align ∣ 2 ^ (Description.record leading minimum packed tail).alignExponent + else 2 ^ (Description.record leading minimum packed tail).alignExponent ∣ f.align := by + dsimp [f] + rw [hk] + split <;> rename_i hh + · exact Nat.pow_dvd_pow 2 ((Nat.pow_lt_pow_iff_right (by decide : 1 < 2)).mp hh).le + · exact Nat.pow_dvd_pow 2 ((Nat.pow_le_pow_iff_right (by decide : 1 < 2)).mp (by omega)) + rw [pad_size f n _ hf (Nat.two_pow_pos _) hd] + simp only [Formula.size, Formula.bytes, f, Nat.add_assoc] + change roundUp (placement + tail.compile.size n) _ = _ + rw [ih h] + rfl + +theorem compile_offset (d : Description) : d.compile.offset = d.offset := by + induction d with + | slice e a => rfl + | record leading minimum packed tail ih => + unfold Description.compile Formula.pad + dsimp only + split <;> simp only [Description.offset, ih] + +theorem compile_elem (d : Description) : d.compile.elem = d.elem := by + induction d with + | slice e a => rfl + | record leading minimum packed tail ih => + unfold Description.compile Formula.pad + dsimp only + split <;> simp only [Description.elem, ih] + +theorem compile_valid (d : Description) : d.compile.valid := by + induction d with + | slice e a => exact ⟨Nat.two_pow_pos a, Nat.two_pow_pos a⟩ + | record leading minimum packed tail ih => + exact pad_valid _ _ ih (Nat.two_pow_pos _) + +/-- A complete recursive layout always contains the physical slice bytes. -/ +theorem description_contains_tail (d : Description) (n : Nat) : + d.offset + n * d.elem ≤ d.size n := by + induction d with + | slice e a => simp only [Description.offset, Description.elem, Description.size, Nat.zero_add, Nat.le_refl] + | record leading minimum packed tail ih => + have h := (roundUp_properties + (roundUp leading (2 ^ Description.fieldAlignExponent packed tail) + tail.size n) + (2 ^ (Description.record leading minimum packed tail).alignExponent) + (Nat.two_pow_pos _)).1 + simp only [Description.offset, Description.elem, Description.size] + omega + +theorem compiled_contains_tail (d : Description) (h : d.valid) (n : Nat) : + d.compile.offset + n * d.compile.elem ≤ d.compile.size n := by + rw [compile_offset, compile_elem, compile_size d h] + exact description_contains_tail d n + +/-- The old flattened formula loses padding inside a packed field. -/ +theorem packed_regression : + (Description.record 2 0 (some 1) (.record 5 2 none (.slice 1 0))).size 0 = 10 ∧ + roundUp 7 2 = 8 := by decide + +theorem aligned_below_next (x k a : Nat) (hx : x % a = 0) (hk : k % a = 0) + (h : x < k + a) : x ≤ k := by + by_contra hnot + have heq : x = k + (x - k) := by omega + have hd : (x - k) % a = 0 := by + rw [heq, Nat.add_mod, hk, Nat.zero_add, Nat.mod_mod] at hx + exact hx + rw [Nat.mod_eq_of_lt (by omega)] at hd + omega + +theorem floor_capacity (n cap a : Nat) (ha : 0 < a) : + n - n % a ≤ cap ↔ n ≤ cap - cap % a + (a - 1) := by + have hn := Arithmetic.round_down_properties n a (n - n % a) ha rfl + have hc := Arithmetic.round_down_properties cap a (cap - cap % a) ha rfl + constructor + · intro h + have hbound := hc.2.2.2 _ h hn.2.1 + omega + · intro h + have hbound := aligned_below_next _ _ _ hn.2.1 hc.2.1 (by omega) + omega + +theorem roundUp_from_aligned_budget (budget unused a : Nat) (ha : 0 < a) + (hb : budget % a = 0) (hu : unused ≤ budget) : + roundUp (budget - unused) a = budget - (unused - unused % a) := by + have hm := Nat.mod_le unused a + have hr := Nat.mod_lt unused ha + let floor := unused - unused % a + have hf : floor % a = 0 := by + dsimp only [floor] + rw [← Nat.div_mul_self_eq_mod_sub_self, Nat.mul_mod_left] + have hq : (budget - floor) % a = 0 := by + have hsplit : budget = (budget - floor) + floor := by dsimp only [floor]; omega + rw [hsplit, Nat.add_mod, hf, Nat.add_zero, Nat.mod_mod] at hb + exact hb + have hceil := roundUp_properties (budget - unused) a ha + have hupper := roundUp_le (budget - unused) a (budget - floor) ha (by dsimp only [floor]; omega) hq + have hlower := aligned_below_next (budget - floor) (roundUp (budget - unused) a) a + hq hceil.2.2 (by dsimp only [floor]; omega) + dsimp only [floor] at hupper hlower + omega + +theorem capacity_object_size (f : Formula) (available cap : Nat) (ha : 0 < f.align) + (hcap : f.capacity available = some cap) : + f.size (cap / f.elem) = f.base + + ((available - f.base) - (available - f.base) % f.align) - + (cap % f.elem - cap % f.elem % f.align) := by + have hspec := capacity_spec f available ha + rw [hcap] at hspec + have hzero := (hspec 0).mpr (by omega) + have hbase : f.base ≤ available := by unfold Formula.bytes at hzero; omega + have hphase : f.phase ≤ (available - f.base) - (available - f.base) % f.align := by + apply (roundUp_le_budget f.phase f.align (available - f.base) ha).mp + unfold Formula.bytes at hzero + simp only [Nat.add_zero] at hzero + omega + have hc : cap = (available - f.base) - (available - f.base) % f.align - f.phase := by + unfold Formula.capacity at hcap + simp only [hzero, if_true, Option.some.injEq] at hcap + exact hcap.symm + let budget := (available - f.base) - (available - f.base) % f.align + have hb : budget % f.align = 0 := by + dsimp only [budget] + rw [← Nat.div_mul_self_eq_mod_sub_self, Nat.mul_mod_left] + have hm := Nat.mod_le cap f.elem + have hdiv := Nat.mod_add_div cap f.elem + have hinput : f.phase + (cap / f.elem) * f.elem = budget - cap % f.elem := by + dsimp only [budget] + rw [Nat.mul_comm] at hdiv + omega + unfold Formula.size Formula.bytes + rw [hinput, roundUp_from_aligned_budget budget _ f.align ha hb (by dsimp only [budget]; omega)] + have hfloor := Nat.mod_le (cap % f.elem) f.align + dsimp only [budget] + omega + +/-- An independent description of a layout fragment, with unbounded sizes. -/ +inductive Payload where + | fixed (bytes : Nat) + | trailing (formula : Formula) +deriving DecidableEq + +/- The mathematical state of a layout fragment, including alignment and the +unpadded flag. Fragment operations can produce intermediate states, so this +type intentionally has no blanket completed-Rust-type invariant. +-/ +structure LayoutValue where + align : Nat + payload : Payload + unpadded : Bool +deriving DecidableEq + +/- Observe fixed byte size or evaluate the trailing formula at this metadata. +-/ +def LayoutValue.size (v : LayoutValue) (n : Nat) : Nat := + match v.payload with + | .fixed bytes => bytes + | .trailing f => f.size n + +/- Begin record construction with no fields and the requested minimum alignment. +-/ +def LayoutValue.initial (align : Nat) : LayoutValue := ⟨align, .fixed 0, true⟩ + +/-- Outside the `extendFits` domain the placeholder result has no contract. -/ +def LayoutValue.extend (v field : LayoutValue) (packing : Nat) : LayoutValue := + match v.payload with + | .trailing _ => v + | .fixed bytes => + let fieldAlign := min field.align packing + let offset := roundUp bytes fieldAlign + { align := max v.align fieldAlign, + payload := match field.payload with + | .fixed size => .fixed (offset + size) + | .trailing f => .trailing { f with base := offset + f.base, offset := offset + f.offset }, + unpadded := v.unpadded && field.unpadded && decide (bytes % fieldAlign = 0) } + +/- State the exact machine bounds needed to append a field. A trailing fragment +cannot receive another field; this rules out extend's placeholder case. For a +trailing field, physical offset and normalized base each need their own +bound. +-/ +def LayoutValue.extendFits (v field : LayoutValue) (packing limit : Nat) : Prop := + match v.payload with + | .trailing _ => False + | .fixed bytes => + let offset := roundUp bytes (min field.align packing) + match field.payload with + | .fixed size => offset + size ≤ limit + | .trailing f => offset + f.offset ≤ limit ∧ offset + f.base ≤ limit + +/- Complete a fragment at its own alignment, retaining inner rounding in a +trailing payload. Fixed payload padding also updates the unpadded +observation. +-/ +def LayoutValue.pad (v : LayoutValue) : LayoutValue := + { v with + payload := match v.payload with + | .fixed bytes => .fixed (roundUp bytes v.align) + | .trailing f => .trailing (f.pad v.align) + unpadded := match v.payload with + | .fixed bytes => v.unpadded && decide (bytes % v.align = 0) + | .trailing _ => v.unpadded } + +/- Bound the stored fields changed by padding. This is a machine-representation +condition, not a bound on every possible metadata value of a trailing +formula. +-/ +def LayoutValue.padFits (v : LayoutValue) (limit : Nat) : Prop := + match v.payload with + | .fixed bytes => roundUp bytes v.align ≤ limit + | .trailing f => + if f.align < v.align then roundUp f.base f.align + f.phase ≤ limit + else roundUp f.base v.align ≤ limit + +theorem LayoutValue.extend_size (v field : LayoutValue) (packing bytes n : Nat) + (h : v.payload = .fixed bytes) : + (v.extend field packing).size n = + roundUp bytes (min field.align packing) + field.size n := by + simp only [LayoutValue.extend, h] + cases hf : field.payload <;> simp [LayoutValue.size, hf, Formula.size, Formula.bytes, Nat.add_assoc] + +/- Fold extension over the first i fields. The constructor loop invariant uses +this prefix calculation to identify its current mathematical state. +-/ +def LayoutValue.prefixValue (fields : List LayoutValue) (initial : LayoutValue) + (packing i : Nat) : LayoutValue := + (fields.take i).foldl (fun v field => v.extend field packing) initial + +theorem LayoutValue.prefixValue_step (fields : List LayoutValue) (initial : LayoutValue) + (packing i : Nat) (hi : i < fields.length) : + prefixValue fields initial packing (i + 1) = + (prefixValue fields initial packing i).extend fields[i] packing := by + unfold prefixValue + rw [List.take_succ_eq_append_getElem hi, List.foldl_append] + rfl + +/-- Direct field placement, evaluated separately for each metadata value. +This rule never manipulates the normalization's base, phase, or size alignment. -/ +def recordState (fields : List LayoutValue) (minimum packing metadata count : Nat) : Nat × Nat := + (fields.take count).foldl + (fun state field => + (max state.1 (min field.align packing), + roundUp state.2 (min field.align packing) + field.size metadata)) + (minimum, 0) + +theorem recordState_step (fields : List LayoutValue) (minimum packing metadata i : Nat) + (hi : i < fields.length) : + recordState fields minimum packing metadata (i + 1) = + (max (recordState fields minimum packing metadata i).1 (min fields[i].align packing), + roundUp (recordState fields minimum packing metadata i).2 (min fields[i].align packing) + + fields[i].size metadata) := by + unfold recordState + rw [List.take_succ_eq_append_getElem hi, List.foldl_append] + rfl + +theorem recordState_refinement (fields : List LayoutValue) (minimum packing metadata limit : Nat) + (hfit : ∀ i (hi : i < fields.length), + (LayoutValue.prefixValue fields (LayoutValue.initial minimum) packing i).extendFits + fields[i] packing limit) : + ∀ i ≤ fields.length, recordState fields minimum packing metadata i = + ((LayoutValue.prefixValue fields (LayoutValue.initial minimum) packing i).align, + (LayoutValue.prefixValue fields (LayoutValue.initial minimum) packing i).size metadata) := by + intro i + induction i with + | zero => + intro hi + simp only [recordState, LayoutValue.prefixValue, List.take_zero, List.foldl_nil, + LayoutValue.initial, LayoutValue.size] + | succ i ih => + intro hi + have hil : i < fields.length := by omega + have hstep := hfit i hil + let current := LayoutValue.prefixValue fields (LayoutValue.initial minimum) packing i + cases hc : current.payload with + | trailing f => + dsimp only [current] at hc + simp only [LayoutValue.extendFits, hc] at hstep + | fixed bytes => + dsimp only [current] at hc + rw [recordState_step fields minimum packing metadata i hil, ih (by omega), + LayoutValue.prefixValue_step fields (LayoutValue.initial minimum) packing i hil] + change _ = ((current.extend fields[i] packing).align, + (current.extend fields[i] packing).size metadata) + rw [LayoutValue.extend_size current fields[i] packing bytes metadata hc] + simp only [current, LayoutValue.extend, hc, LayoutValue.size] + +end Zerocopy.LayoutMath diff --git a/verification/aeneas/lean/LayoutModel.lean b/verification/aeneas/lean/LayoutModel.lean new file mode 100644 index 0000000000..adc6a518f7 --- /dev/null +++ b/verification/aeneas/lean/LayoutModel.lean @@ -0,0 +1,211 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Zerocopy.Funs +public import SpecPrelude +public import LayoutMath +@[expose] public section + +/-! +Extracted trailing records contain machine words and compressed rounding +encodings. This module interprets those records as an unbounded complete-size +formula and a separately observed physical slice offset. Its definitions do +not call an extracted operation or assume that an operation is correct. + +Keeping physical offset separate from normalized base and phase is essential: +inner padding can make them differ. Operation proofs use these observations +to connect extracted arithmetic with the independent layout mathematics. +-/ +open Aeneas Aeneas.Std +namespace Zerocopy.Proofs + + +end Zerocopy.Proofs + +namespace Zerocopy.Proofs.NestedReference + +/- The independent recursive rule uses unbounded arithmetic and complete +inner-field sizes. Layers are ordered from outermost to innermost. These +definitions do not call an extracted implementation or manipulate its +normalized base and phase. +-/ +def layerValid (layer : layout.nested_reference.NestedLayer) : Prop := + layer.packed.val.isPowerOfTwo ∧ layer.min_align.val.isPowerOfTwo ∧ + layer.min_align.val ≤ layer.packed.val + +def valid (layers : List layout.nested_reference.NestedLayer) + (elem alignment : Nat) : Prop := + alignment.isPowerOfTwo ∧ elem % alignment = 0 ∧ ∀ layer ∈ layers, layerValid layer + +def step (layer : layout.nested_reference.NestedLayer) (inner : Nat × Nat) : Nat × Nat := + let placementAlign := min inner.2 layer.packed.val + let alignment := max layer.min_align.val placementAlign + (LayoutMath.roundUp (LayoutMath.roundUp layer.prefix_bytes.val placementAlign + inner.1) alignment, + alignment) + +def state (layers : List layout.nested_reference.NestedLayer) + (elem alignment metadata : Nat) : Nat × Nat := + layers.foldr step (metadata * elem, alignment) + +theorem state_cons (layer : layout.nested_reference.NestedLayer) + (layers : List layout.nested_reference.NestedLayer) (elem alignment metadata : Nat) : + state (layer :: layers) elem alignment metadata = + step layer (state layers elem alignment metadata) := rfl + +theorem step_contains_inner (layer : layout.nested_reference.NestedLayer) (inner : Nat × Nat) : + inner.1 ≤ (step layer inner).1 := by + simp only [step, LayoutMath.roundUp] + omega + +theorem step_alignment_power (layer : layout.nested_reference.NestedLayer) + (inner : Nat × Nat) (hl : layerValid layer) (ha : inner.2.isPowerOfTwo) : + (step layer inner).2.isPowerOfTwo := by + simp only [step] + have hm : (min inner.2 layer.packed.val).isPowerOfTwo := by + by_cases h : inner.2 ≤ layer.packed.val + · simpa only [Nat.min_eq_left h] using ha + · simpa only [Nat.min_eq_right (by omega : layer.packed.val ≤ inner.2)] using hl.1 + by_cases h : layer.min_align.val ≤ min inner.2 layer.packed.val + · simpa only [Nat.max_eq_right h] using hm + · simpa only [Nat.max_eq_left (by omega : min inner.2 layer.packed.val ≤ layer.min_align.val)] + using hl.2.1 + +theorem state_alignment_power (layers : List layout.nested_reference.NestedLayer) + (elem alignment metadata : Nat) (h : valid layers elem alignment) : + (state layers elem alignment metadata).2.isPowerOfTwo := by + induction layers with + | nil => exact h.1 + | cons layer layers ih => + simp only [state, List.foldr_cons] + apply step_alignment_power + · exact h.2.2 layer (by simp) + · apply ih + exact ⟨h.1, h.2.1, fun x hx => h.2.2 x (by simp [hx])⟩ + +theorem state_alignment_bound (layers : List layout.nested_reference.NestedLayer) + (elem alignment metadata bound : Nat) (ha : alignment ≤ bound) + (hl : ∀ layer ∈ layers, layer.min_align.val ≤ bound) : + (state layers elem alignment metadata).2 ≤ bound := by + induction layers with + | nil => exact ha + | cons layer layers ih => + rw [state_cons] + simp only [step] + apply max_le + · exact hl layer (by simp) + · apply le_trans (Nat.min_le_left _ _) + exact ih (fun x hx => hl x (by simp [hx])) + +theorem state_contains_suffix (layers : List layout.nested_reference.NestedLayer) + (elem alignment metadata count : Nat) : + (state (layers.drop count) elem alignment metadata).1 ≤ + (state layers elem alignment metadata).1 := by + induction layers generalizing count with + | nil => simp + | cons layer layers ih => + cases count with + | zero => simp + | succ count => + simp only [List.drop_succ_cons, state_cons] + exact le_trans (ih count) (step_contains_inner layer (state layers elem alignment metadata)) + +theorem suffix_zero_size_fits (layers : List layout.nested_reference.NestedLayer) + (elem alignment limit count : Nat) (hfit : (state layers elem alignment 0).1 ≤ limit) : + (state (layers.drop count) elem alignment 0).1 ≤ limit := + le_trans (state_contains_suffix layers elem alignment 0 count) hfit + +theorem step_contains_placed_field (layer : layout.nested_reference.NestedLayer) + (inner : Nat × Nat) : + LayoutMath.roundUp layer.prefix_bytes.val (min inner.2 layer.packed.val) + inner.1 ≤ + (step layer inner).1 := by + simp only [step, LayoutMath.roundUp] + omega + +theorem step_contains_prefix (layer : layout.nested_reference.NestedLayer) + (inner : Nat × Nat) : layer.prefix_bytes.val ≤ (step layer inner).1 := by + simp only [step, LayoutMath.roundUp] + omega + +theorem valid_cons (layer : layout.nested_reference.NestedLayer) + (layers : List layout.nested_reference.NestedLayer) (elem alignment : Nat) : + valid (layer :: layers) elem alignment ↔ layerValid layer ∧ valid layers elem alignment := by + simp only [valid, List.mem_cons, forall_eq_or_imp] + tauto + +noncomputable def checkedState (layers : List layout.nested_reference.NestedLayer) + (elem alignment metadata : Nat) : Option (Nat × Nat) := by + classical + exact if valid layers elem alignment then + if (state layers elem alignment metadata).1 ≤ Usize.max + then some (state layers elem alignment metadata) else none + else none + +noncomputable def checkedLayer (layer : layout.nested_reference.NestedLayer) + (inner : Option (Nat × Nat)) : Option (Nat × Nat) := by + classical + exact if layerValid layer then inner.bind (fun inner => + if (step layer inner).1 ≤ Usize.max then some (step layer inner) else none) else none + +theorem checkedState_cons (layer : layout.nested_reference.NestedLayer) + (layers : List layout.nested_reference.NestedLayer) (elem alignment metadata : Nat) : + checkedState (layer :: layers) elem alignment metadata = + checkedLayer layer (checkedState layers elem alignment metadata) := by + classical + by_cases hl : layerValid layer + · by_cases hv : valid layers elem alignment + · by_cases hf : (state layers elem alignment metadata).1 ≤ Usize.max + · simp only [checkedState, checkedLayer, valid_cons, hl, hv, true_and, if_true, + hf, state_cons, Option.bind_some] + · have ho : ¬(step layer (state layers elem alignment metadata)).1 ≤ Usize.max := by + have := step_contains_inner layer (state layers elem alignment metadata) + omega + simp only [checkedState, checkedLayer, valid_cons, hl, hv, true_and, if_true, + hf, if_false, state_cons, ho, Option.bind_none] + · simp only [checkedState, checkedLayer, valid_cons, hl, hv, true_and, if_false, + if_true, Option.bind_none] + · simp only [checkedState, checkedLayer, valid_cons, hl, false_and, if_false] + +theorem checkedState_some (layers : List layout.nested_reference.NestedLayer) + (elem alignment metadata : Nat) (out : Nat × Nat) + (h : checkedState layers elem alignment metadata = some out) : + valid layers elem alignment ∧ state layers elem alignment metadata = out ∧ out.1 ≤ Usize.max := by + classical + unfold checkedState at h + split at h + · rename_i hv + split at h + · rename_i hf + have heq := Option.some.inj h + exact ⟨hv, heq, heq ▸ hf⟩ + · contradiction + · contradiction + +theorem checkedState_drop (layers : List layout.nested_reference.NestedLayer) + (elem alignment metadata i : Nat) (hi : i < layers.length) : + checkedState (layers.drop i) elem alignment metadata = + checkedLayer layers[i] (checkedState (layers.drop (i + 1)) elem alignment metadata) := by + rw [List.drop_eq_getElem_cons hi, checkedState_cons] + +noncomputable def checkedSize (layers : List layout.nested_reference.NestedLayer) + (elem alignment metadata : Nat) : Option Nat := by + classical + exact if valid layers elem alignment then + if (state layers elem alignment metadata).1 ≤ Usize.max + then some (state layers elem alignment metadata).1 else none + else none + +theorem checkedSize_eq_map (layers : List layout.nested_reference.NestedLayer) + (elem alignment metadata : Nat) : + checkedSize layers elem alignment metadata = + (checkedState layers elem alignment metadata).map Prod.fst := by + classical + unfold checkedSize checkedState + split <;> (try split) <;> simp + +end Zerocopy.Proofs.NestedReference diff --git a/verification/aeneas/lean/Loops.lean b/verification/aeneas/lean/Loops.lean new file mode 100644 index 0000000000..300d1bc5a0 --- /dev/null +++ b/verification/aeneas/lean/Loops.lean @@ -0,0 +1,57 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Aeneas +public section + +/-! +Aeneas already proves loop reasoning rules. This adapter specializes its +decreasing-natural rule to the indexed loops used by layout construction. The +caller supplies a prefix model and proves one body step. The adapter uses +length minus index as the termination measure and carries both the model +relation and the caller's representation invariant across every step. This +avoids reproving termination separately for each indexed operation. +-/ +open Aeneas Aeneas.Std + +namespace AeneasContracts + +/-- An indexed Rust loop refines a mathematical prefix computation. The +caller proves one body step: continue advances the index by one, while done +occurs at the length. This rule supplies the index bounds and termination +measure, and carries an arbitrary representation invariant. -/ +theorem indexed_loop_spec {State Model : Type} + (length : Nat) (view : State → Model) (prefixValue : Nat → Model) + (valid : State → Prop) + (body : State × Usize → Result (ControlFlow (State × Usize) State)) + (hbody : ∀ state i, i.val ≤ length → view state = prefixValue i.val → valid state → + body (state, i) ⦃ r => match r with + | .cont (next, j) => i.val < length ∧ j.val = i.val + 1 ∧ + view next = prefixValue j.val ∧ valid next + | .done out => i.val = length ∧ view out = prefixValue i.val ∧ valid out ⦄) + (state : State) (i : Usize) + (hi : i.val ≤ length) (hv : view state = prefixValue i.val) (hc : valid state) : + loop body (state, i) ⦃ out => view out = prefixValue length ∧ valid out ⦄ := by + apply loop.spec_decr_nat + (fun s : State × Usize => length - s.2.val) + (fun s => s.2.val ≤ length ∧ view s.1 = prefixValue s.2.val ∧ valid s.1) + · rintro ⟨state, i⟩ ⟨hi, hv, hc⟩ + apply WP.spec_mono (hbody state i hi hv hc) + intro r hr + cases r with + | done out => + obtain ⟨hend, hv, hc⟩ := hr + exact ⟨hend ▸ hv, hc⟩ + | cont s => + rcases s with ⟨next, j⟩ + obtain ⟨hlt, hj, hv, hc⟩ := hr + exact ⟨⟨by scalar_tac, hv, hc⟩, by scalar_tac⟩ + · exact ⟨hi, hv, hc⟩ + +end AeneasContracts diff --git a/verification/aeneas/lean/MathViews.lean b/verification/aeneas/lean/MathViews.lean new file mode 100644 index 0000000000..81ad558122 --- /dev/null +++ b/verification/aeneas/lean/MathViews.lean @@ -0,0 +1,118 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Models +public import ModelLemmas +public import SpecPrelude +public import ContractSimps +@[expose] public section + +/-! +Contracts automatically require values to decode. Raw implementation proofs +instead use concrete conditions such as a nonzero alignment word. This module +proves that the actual decoders admit exactly those independently stated raw +domains, and supplies equations for normalizing retained provider +projections. + +The predicates below are observations used by proofs, not competing validity +instances. The selected RustModel decoder remains authoritative for +admission. The different theorem spellings match expressions produced at +different stages: isValid, a provider's decode projection, or the owner's +named decoder. Each is proved from the same decoder; none chooses an +interpretation from a model type. +-/ +open Aeneas Aeneas.Std AeneasSpecs +namespace Zerocopy.Proofs +set_option linter.unusedSimpArgs false + +-- Raw observations remain available independently of function contracts. +-- They record the full positive encoding domain, including arbitrary stored +-- words, and do not register a separately selectable validity interpretation. +def encodingValid (code : layout.RoundingAlignAndPhase) : Prop := + 0 < code._0.val.val + +@[contract_simps] theorem admission_eq {Raw : Type u} (provider : RustModel Raw) + (raw : Raw) : (∃ value, provider.decode raw = some value) = + @isValid Raw provider raw := rfl + +-- Eliminating an unused mathematical witness is ordinary existential logic. +-- These lemmas apply only when the authored predicate does not depend on it. +@[contract_simps] theorem option_decoded_pre_iff {M : Type u} (d : Option M) (P : Prop) : + (∀ m, d = some m → P) ↔ ((∃ m, d = some m) → P) := by + constructor + · rintro h ⟨m, hm⟩; exact h m hm + · intro h m hm; exact h ⟨m, hm⟩ + +@[contract_simps] theorem option_decoded_post_iff {M : Type u} (d : Option M) (P : Prop) : + (∃ m, d = some m ∧ P) ↔ (∃ m, d = some m) ∧ P := by + constructor + · rintro ⟨m, hm, hp⟩; exact ⟨⟨m, hm⟩, hp⟩ + · rintro ⟨⟨m, hm⟩, hp⟩; exact ⟨m, hm, hp⟩ + +@[simp, contract_simps] theorem scalar_valid_iff (x : UScalar ty) : isValid x ↔ True := by + simp [isValid, RustModel.decode, modelUScalar] + +@[simp, contract_simps] theorem nonzero_valid_iff (a : NonZeroUsize) : + isValid a ↔ 0 < a.val.val := by + unfold isValid + constructor + · rintro ⟨m, hm⟩ + have he := (decodeNonZeroUScalar_iff a m).mp hm + rw [he]; exact m.positive + · intro hp + exact ⟨⟨unsignedWord a.val, hp⟩, (decodeNonZeroUScalar_iff a ⟨unsignedWord a.val, hp⟩).mpr rfl⟩ + +@[simp, contract_simps] theorem bool_valid_iff (x : Bool) : isValid x ↔ True := by + simp [isValid, RustModel.decode, modelBool] + +@[simp, contract_simps] theorem encoding_valid_iff (code : layout.RoundingAlignAndPhase) : + isValid code ↔ encodingValid code := by + by_cases hp : 0 < code._0.val.val <;> + simp [isValid, RustModel.decode, layout.RoundingAlignAndPhase.aeneasModel, + layout.RoundingAlignAndPhase.decode, layout.RoundingAlignAndPhase.decodeFields, + modelNonZeroUScalar, hp, encodingValid] + +@[contract_simps] theorem scalar_admitted_iff (x : UScalar ty) : + (∃ value, RustModel.decode x = some value) ↔ True := scalar_valid_iff x + +@[contract_simps] theorem nonzero_admitted_iff (x : NonZeroUsize) : + (∃ value, RustModel.decode x = some value) ↔ 0 < x.val.val := nonzero_valid_iff x + +@[contract_simps] theorem encoding_admitted_iff (x : layout.RoundingAlignAndPhase) : + (∃ value, RustModel.decode x = some value) ↔ encodingValid x := encoding_valid_iff x + +@[contract_simps] theorem bool_admitted_iff (x : Bool) : + (∃ value, RustModel.decode x = some value) ↔ True := bool_valid_iff x + +@[contract_simps] theorem option_admitted_iff {α : Type} [RustModel α] (x : Option α) : + (∃ value : Option (ModelOf α), RustModel.decode x = some value) ↔ + ∀ v ∈ x, isValid v := option_valid_iff x + +@[contract_simps] theorem slice_admitted_iff {α : Type} [RustModel α] (x : Slice α) : + (∃ value, RustModel.decode x = some value) ↔ ∀ v ∈ x.val, isValid v := slice_valid_iff x + +-- A named decoder can also occur after a surrounding structural traversal +-- exposes the retained dictionary. These are the same admission equivalences. +@[contract_simps] theorem encoding_decoder_admitted_iff (raw : layout.RoundingAlignAndPhase) : + (∃ value, layout.RoundingAlignAndPhase.decode raw = some value) ↔ + encodingValid raw := encoding_valid_iff raw + +@[contract_simps] theorem normalized_nat_pos_iff (n : Nat) : + Nat.le (Nat.succ 0) n ↔ 0 < n := Nat.succ_le_iff + +attribute [contract_simps] option_valid_iff prod_valid_iff result_valid_iff + +attribute [contract_simps] and_true true_and and_self true_implies forall_true_iff +attribute [contract_simps] encodingValid + +@[simp, contract_simps] theorem rounding_model_decode_eq + (raw : layout.RoundingAlignAndPhase) : + RustModel.decode raw = layout.RoundingAlignAndPhase.decode raw := rfl + +end Zerocopy.Proofs diff --git a/verification/aeneas/lean/ModelCompletion.lean b/verification/aeneas/lean/ModelCompletion.lean new file mode 100644 index 0000000000..b833266a2d --- /dev/null +++ b/verification/aeneas/lean/ModelCompletion.lean @@ -0,0 +1,86 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import ModelPrelude +public meta import Lean +import all Init.Data.Nat.Power2.Basic +@[expose] public section + +/-! +Mathematical model records may contain proofs, such as a bound on a word or a +power-of-two constraint. `model_value` lets native record syntax omit those +proof fields and runs ordinary Lean proof search at the construction site. +Lean still checks the resulting proof terms. + +Completion cannot choose missing data or turn an unproved constraint into +decoder rejection. In particular, a failed proof inside `decode?` is a compiler +error; it is not another branch returning `none`. The search has a bounded +heartbeat budget so an omitted proof cannot consume unlimited elaboration work. +-/ + +open Lean Elab Tactic Aeneas.Std +namespace AeneasSpecs + +/-- An exposed theorem avoids requiring clients to unfold the predicate's +private defining module while constructing ordinary mathematical values. -/ +theorem model_power_of_two (n : Nat) : (2 ^ n).isPowerOfTwo := ⟨n, rfl⟩ + +/-- Standard logarithm bounds, in a form usable by native arithmetic automation. -/ +theorem model_log2_bounds (n : Nat) (positive : 0 < n) : + 2 ^ Nat.log2 n ≤ n ∧ n < 2 * 2 ^ Nat.log2 n := by + have lower := Nat.log2_self_le (n := n) (by omega) + have upper := Nat.lt_log2_self (n := n) + rw [Nat.pow_succ] at upper + omega + +grind_pattern model_log2_bounds => Nat.log2 n + +/-- Complete omitted proofs at a construction site. Data goals are never solved +by this tactic; ordinary elaboration and unification may already infer data. -/ +elab "complete_model_proofs" : tactic => withCurrHeartbeats do + let options ← getOptions + let configured := maxHeartbeats.get options + -- Lean uses zero for an unlimited budget. Treat it as the local ceiling + -- here; a smaller caller budget still wins, including deterministic tests. + let budget := if configured == 0 then 100000 else min configured 100000 + withOptions (fun o => maxHeartbeats.set o budget) do + -- A field whose type is Prop can be data (for example, `claim : Prop`). + -- isProp checks whether this goal asks for a proof, not whether its target + -- is a sort. Unresolved target types are therefore not guessed into proofs. + for goal in (← getGoals) do + goal.withContext do + unless (← Meta.isProp (← goal.getType)) do + Meta.throwTacticEx `complete_model_proofs goal + "Supply the omitted data value; automatic model completion only proves propositions" + -- Each branch constructs a proof term using assumptions or checked lemmas. + -- first tries the inexpensive cases before arithmetic and bounded grind. + try + evalTactic (← `(tactic| all_goals first + | assumption + | exact model_power_of_two _ + | exact ⟨_, rfl⟩ + | omega + | grind only [NonZeroUnsignedWord.positive, UnsignedWord.bound, + model_log2_bounds, UScalar.max, Usize.max, Usize.numBits, + UScalarTy.Usize_numBits_eq])) + catch error => + -- An automation failure is a compilation failure, including in decode?. + -- Preserve interruption/heartbeat errors and their original diagnostics. + if error.isInterrupt || error.isMaxHeartbeat then throw error + Meta.throwTacticEx `complete_model_proofs (← getMainGoal) + m!"Could not prove the omitted model constraint. Supply an explicit field proof. +{error.toMessageData}" + +/-- The explicit entry point outside a decoder; decoder bodies use the same rule. -/ +macro "model_value " value:term : term => + `(by + refine' $value + complete_model_proofs) + +end AeneasSpecs diff --git a/verification/aeneas/lean/ModelCompletionTests.lean b/verification/aeneas/lean/ModelCompletionTests.lean new file mode 100644 index 0000000000..f9b7a72cb4 --- /dev/null +++ b/verification/aeneas/lean/ModelCompletionTests.lean @@ -0,0 +1,135 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import DeriveModels +@[expose] public section + +/-! +These fixtures exercise native proof-field completion with explicit data, +inferred data, nested records, and fallible decoders. The negative cases check +that missing data, an impossible proof, an unresolved target type, and exhausted +proof search remain compilation failures. In particular, proof-search failure +must never become a convenient `none` that silently narrows decoder admission. +-/ + +open Lean Aeneas Aeneas.Std AeneasSpecs +namespace ModelCompletionTests + +structure Pair where + first : Nat + second : Nat + ordered : first ≤ second + +example (n : Nat) : Pair := model_value { first := n, second := n + 1, .. } +example (n : Nat) : Pair := + model_value (by exact { first := n, second := n, ordered := Nat.le_refl n }) +example (n : Nat) : Pair := + model_value { first := n, second := n, ordered := by omega } +example (n : Nat) : Option Pair := + model_value (if h : n < 4 then some { first := n, second := 4, .. } + else some { first := 4, second := n, .. }) + +structure Nested where + pair : Pair + positive : 0 < pair.second +example (n : Nat) : Nested := + model_value { pair := { first := n, second := n + 1, .. }, .. } + +structure Encoding where + word : core.num.nonzero.NonZero Usize core.num.niche_types.NonZeroUsizeInner + +aeneas_model_shape Encoding with 0 type parameters begin + model Value where + align : Nat + phase : Nat + pow2 : align.isPowerOfTwo + phase_lt : phase < align + fits : align + phase ≤ Usize.max +end +derive_rust_model Encoding with 0 type parameters decode self => + let word := self.word.value + let align := 2 ^ Nat.log2 word + { align := align, phase := word - align, .. } +check_model_binding Encoding with 0 type parameters + +-- Completion does not admit invalid children of an infallible local decoder. +example : Encoding.decode ⟨⟨0#usize⟩⟩ = none := rfl +example : (Encoding.decode ⟨⟨13#usize⟩⟩).map (fun r => (r.align, r.phase)) = + some (8, 5) := by decide + +structure Fallible where + word : core.num.nonzero.NonZero Usize core.num.niche_types.NonZeroUsizeInner + +aeneas_model_shape Fallible with 0 type parameters begin + model Value where + value : Nat + positive : 0 < value +end +derive_rust_model Fallible with 0 type parameters decode? self => + if h : 1 < self.word.value then some { value := self.word.value - 1, .. } + else none +check_model_binding Fallible with 0 type parameters +example : Fallible.decode ⟨⟨1#usize⟩⟩ = none := rfl +example : (Fallible.decode ⟨⟨3#usize⟩⟩).map (·.value) = some 2 := rfl + +structure Impossible where + value : Nat +aeneas_model_shape Impossible with 0 type parameters begin + model Value where + value : Nat + impossible : False +end + +structure PropositionData where + claim : Prop + +syntax (name := discardedCompletionFixture) "discard_completion_fixture " command : command +@[command_elab discardedCompletionFixture] meta def discardCompletionFixture : + Lean.Elab.Command.CommandElab := fun stx => + Lean.withoutModifyingEnv (Lean.Elab.Command.elabCommand stx[1]) + +-- These check the intended failure reason without fixing incidental pretty printing. +/-- Supply the omitted data value; automatic model completion only proves propositions -/ +#guard_msgs (substring := true) in +discard_completion_fixture example : Pair := model_value { second := 1, .. } + +/-- Supply the omitted data value; automatic model completion only proves propositions -/ +#guard_msgs (substring := true) in +discard_completion_fixture example : PropositionData := model_value { .. } + +-- A false constraint never silently narrows decode? by turning failure into none. +/-- Could not prove the omitted model constraint. Supply an explicit field proof. -/ +#guard_msgs (substring := true) in +discard_completion_fixture derive_rust_model Impossible with 0 type parameters decode? self => + some { value := self.value, .. } + +-- An unresolved target type must not be guessed to be a proposition. +elab "unresolved_completion_goal" : tactic => do + let unknownSort ← Lean.Meta.mkFreshLevelMVar + let unknownType ← Lean.Meta.mkFreshExprMVar (some (mkSort unknownSort)) + let unknownGoal ← Lean.Meta.mkFreshExprMVar (some unknownType) + Lean.Elab.Tactic.setGoals [unknownGoal.mvarId!] + +/-- Supply the omitted data value; automatic model completion only proves propositions -/ +#guard_msgs (substring := true) in +discard_completion_fixture example : True := by + unresolved_completion_goal + complete_model_proofs + +-- Exhaustion remains a native compilation failure, including for a valid model. +/-- deterministic timeout -/ +#guard_msgs (substring := true) in +discard_completion_fixture example (word : NonZeroUsizeValue) : Encoding.Value := + model_value (by + let align := 2 ^ Nat.log2 word.value + refine' { align := align, phase := word.value - align, .. } + set_option maxHeartbeats 1 in + complete_model_proofs) + +end ModelCompletionTests diff --git a/verification/aeneas/lean/ModelExportProducer.lean b/verification/aeneas/lean/ModelExportProducer.lean new file mode 100644 index 0000000000..279c2ec91f --- /dev/null +++ b/verification/aeneas/lean/ModelExportProducer.lean @@ -0,0 +1,65 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import DeriveModels +@[expose] public section + +/-! +This module produces decoded models whose records contain automatically proved +constraints. A separate consumer module unfolds these definitions and uses the +generated decomposition theorems. Splitting producer from consumer catches +export bugs that a same-module proof could miss, particularly private proof +auxiliaries embedded in an exposed decoder. +-/ + +open AeneasSpecs Aeneas.Std +namespace ModelExportRegression +structure Raw where + word : Usize +aeneas_model_shape Raw with 0 type parameters begin + model Value where + word : Nat + fits : word ≤ Usize.max +end +derive_rust_model Raw with 0 type parameters decode self => + let word := self.word.value + { word := word, .. } +check_model_binding Raw with 0 type parameters + +structure Encoding where + word : core.num.nonzero.NonZero Usize core.num.niche_types.NonZeroUsizeInner +aeneas_model_shape Encoding with 0 type parameters begin + model Value where + align : Nat + phase : Nat + pow2 : align.isPowerOfTwo + phase_lt : phase < align + fits : align + phase ≤ Usize.max +end +derive_rust_model Encoding with 0 type parameters decode self => + let word := self.word.value + let align := 2 ^ Nat.log2 word + { align := align, phase := word - align, .. } +check_model_binding Encoding with 0 type parameters + +structure Fallible where + word : Usize +aeneas_model_shape Fallible with 0 type parameters begin + model Value where + word : Nat + positive : 0 < word + fits : word ≤ Usize.max +end +derive_rust_model Fallible with 0 type parameters decode? self => + if h : 1 < self.word.value then + some { word := self.word.value - 1, .. } + else none +check_model_binding Fallible with 0 type parameters + +end ModelExportRegression diff --git a/verification/aeneas/lean/ModelExportTests.lean b/verification/aeneas/lean/ModelExportTests.lean new file mode 100644 index 0000000000..7adaf85c5c --- /dev/null +++ b/verification/aeneas/lean/ModelExportTests.lean @@ -0,0 +1,43 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import ModelExportProducer +@[expose] public section + +/-! +These consumer checks import, unfold, and inspect models produced in another +module. Successful and rejected decodings pin both calculation and admission; +the decomposition example exposes a child witness across the import boundary. +The axiom audit checks the exported decoder and bridge declarations themselves, +so successful elaboration cannot conceal an unchecked proof dependency. +-/ + +open AeneasSpecs Aeneas.Std +namespace ModelExportRegression +example (raw : Raw) : ∃ value, Raw.decode raw = some value := by + simp [Raw.decode, Raw.decodeFields] +-- Consumer kernel checks must unfold proof-containing outputs across the import. +example : (Encoding.decode ⟨⟨13#usize⟩⟩).map (fun value => (value.align, value.phase)) = + some (8, 5) := rfl +example : Encoding.decode ⟨⟨0#usize⟩⟩ = none := rfl +example : (Fallible.decode ⟨13#usize⟩).map (·.word) = some 12 := rfl +example : Fallible.decode ⟨0#usize⟩ = none := rfl +example (raw : Encoding) (value : Encoding.Value) + (h : Encoding.decode raw = some value) : + ∃ word, modelNonZeroUsize.decode raw.word = some word ∧ + Encoding.decodeFields ⟨word⟩ = some value := Encoding.decode_decompose raw value |>.mp h + +run_meta do + for owner in #[``Raw, ``Encoding, ``Fallible] do + for suffix in #[`decodeFields, `decode, `aeneasModel, `decode_decompose] do + let declName := owner ++ suffix + for axiomName in ← Lean.collectAxioms declName do + unless #[`propext, `Classical.choice, `Quot.sound].contains axiomName do + throwError "Unexpected decoder axiom {axiomName} in {declName}" +end ModelExportRegression diff --git a/verification/aeneas/lean/ModelLemmas.lean b/verification/aeneas/lean/ModelLemmas.lean new file mode 100644 index 0000000000..617a6e00cb --- /dev/null +++ b/verification/aeneas/lean/ModelLemmas.lean @@ -0,0 +1,169 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import ModelPrelude +@[expose] public section + +/-! +Operation proofs often need to unpack decoder admission or relate an accepted +model to a raw observation. These lemmas expose container admission, element +witnesses, and length-preserving decoding without changing which values decode. +Where a mathematical carrier could have several interpretations, the provider +or decoder stays explicit: the carrier's type alone does not identify its +meaning. +-/ + +open Aeneas Aeneas.Std +namespace AeneasSpecs +set_option linter.unusedSimpArgs false + +/-- An accepting bind has an accepting first value and an accepting continuation. +This equivalence explains the existential/equation form of generated decoder +bridges without relying on how a parent model observes the child. -/ +theorem admitted_bind {α β : Type} (d : Option α) (f : α → Option β) : + (∃ result, d.bind f = some result) ↔ + ∃ value, d = some value ∧ ∃ result, f value = some result := by + cases d <;> simp + +@[simp] theorem admitted_some {α : Type} (value : α) : + (∃ result, some value = some result) ↔ True := by simp + + +theorem admitted_none {α : Type} : (∃ value : α, none = some value) ↔ False := by simp + +theorem admitted_dite {α : Type} (P : Prop) [Decidable P] (value : P → α) : + (∃ result, (if h : P then some (value h) else none) = some result) ↔ P := by + by_cases h : P <;> simp [h] + +-- Admission statements forget which mathematical witness was produced, but +-- retain exactly which children must decode. Option and Rust Result select a +-- payload; products and lists require all their represented children. +@[simp] theorem option_valid_iff {α : Type} [RustModel α] (raw : Option α) : + isValid raw ↔ ∀ x ∈ raw, isValid x := by + cases raw <;> simp [isValid, RustModel.decode, modelOption, Option.map_eq_some_iff] + +@[simp] theorem prod_valid_iff {α β : Type} [RustModel α] [RustModel β] + (raw : α × β) : isValid raw ↔ isValid raw.1 ∧ isValid raw.2 := by + rcases raw with ⟨a, b⟩ + simp only [isValid, RustModel.decode, modelProd] + cases ha : RustModel.decode a <;> cases hb : RustModel.decode b <;> simp [ha, hb] + +@[simp] theorem result_valid_iff {α β : Type} [RustModel α] [RustModel β] + (raw : core.result.Result α β) : + isValid raw ↔ match raw with | .Ok a => isValid a | .Err b => isValid b := by + cases raw <;> simp [isValid, RustModel.decode, modelRustResult, Option.map_eq_some_iff] + +theorem decodeList_admitted {α β : Type} (d : α → Option β) (raw : List α) : + (∃ values, decodeList d raw = some values) ↔ + ∀ x ∈ raw, ∃ value, d x = some value := by + induction raw with + | nil => simp [decodeList] + | cons x xs ih => + cases hx : d x <;> cases ht : decodeList d xs <;> + simp only [hx, ht, Option.some.injEq, reduceCtorEq, exists_eq_left, + not_exists, forall_const, List.mem_cons, forall_eq_or_imp] at ih ⊢ <;> + simp_all [decodeList] + +@[simp] theorem list_valid_iff {α : Type} [m : RustModel α] (raw : List α) : + isValid raw ↔ ∀ x ∈ raw, isValid x := decodeList_admitted m.decode raw + +@[simp] theorem slice_valid_iff {α : Type} [m : RustModel α] (raw : Slice α) : + isValid raw ↔ ∀ x ∈ raw.val, isValid x := by + unfold isValid + dsimp only [RustModel.decode, modelSlice] + split + · rename_i h + simp only [reduceCtorEq] + constructor + · rintro ⟨_, bad⟩; contradiction + intro hall + obtain ⟨values, hv⟩ := (decodeList_admitted m.decode raw.val).mpr hall + rw [h] at hv; contradiction + · rename_i values h + simp only [Option.some.injEq, exists_eq', true_iff] + exact (decodeList_admitted m.decode raw.val).mp ⟨values, h⟩ + +-- Exposing the child carrier and decoder as separate parameters lets ordinary +-- simplification match a normalized nested associated type without trying to +-- infer its raw carrier from its mathematical carrier. +theorem option_admitted_explicit_iff {Raw Math : Type} + (decoder : Raw → Option Math) (raw : Option Raw) : + (∃ value : Option Math, + (@modelOption Raw ⟨Math, decoder⟩).decode raw = some value) ↔ + ∀ x ∈ raw, ∃ value, decoder x = some value := by + cases raw <;> simp [RustModel.decode, modelOption, Option.map_eq_some_iff] + +/-- Structural traversal preserves the accepted mathematical element witnesses. -/ +theorem decodeList_eq_some_iff {α β : Type u} (d : α → Option β) + (raw : List α) (values : List β) : + decodeList d raw = some values ↔ + List.Forall₂ (fun x value => d x = some value) raw values := by + induction raw generalizing values with + | nil => cases values <;> simp [decodeList] + | cons x xs ih => + cases values with + | nil => cases hx : d x <;> cases ht : decodeList d xs <;> simp [decodeList, hx, ht] + | cons value values => + cases hx : d x <;> cases ht : decodeList d xs <;> + simp only [decodeList, hx, ht, Option.bind_some, Option.bind_none, + Option.some.injEq, reduceCtorEq, List.cons.injEq, + List.forall₂_cons] at ih ⊢ <;> simp_all + +/-- Bounded container decomposition exposes the traversed list, while proof +irrelevance identifies the length evidence already stored in its carrier. -/ +theorem decodeSlice_iff {α : Type u} (m : RustModel α) (raw : Slice α) + (value : SliceValue m.Model) : + (@modelSlice α m).decode raw = some value ↔ + decodeList m.decode raw.val = some value.val := by + change (match h : decodeList m.decode raw.val with + | none => none + | some values => some ⟨values, (decodeList_length m.decode h) ▸ raw.property⟩) = some value ↔ _ + split + · rename_i h; simp [h] + · rename_i values h + simp only [Option.some.injEq] + constructor + · intro he; cases he; exact h + · intro he + -- The lists agree by decoder equality. Subtype.ext discards differences + -- between proofs of the same length bound, not differences in elements. + apply Subtype.ext + exact Option.some.inj (h.symm.trans he) + +theorem decodeArray_iff {α : Type u} (m : RustModel α) (n : Usize) + (raw : Aeneas.Std.Array α n) (value : ArrayValue m.Model n) : + (@modelArray α n m).decode raw = some value ↔ + decodeList m.decode raw.val = some value.val := by + change (match h : decodeList m.decode raw.val with + | none => none + | some values => some ⟨values, (decodeList_length m.decode h) ▸ raw.property⟩) = some value ↔ _ + split + · rename_i h; simp [h] + · rename_i values h + simp only [Option.some.injEq] + constructor + · intro he; cases he; exact h + · intro he; apply Subtype.ext; exact Option.some.inj (h.symm.trans he) + +/-- Lift an admitted raw postcondition through an observation proved for the +retained provider. The provider stays explicit: a model carrier alone does not +identify its decoder. -/ +theorem decoded_view_post {Raw : Type u} {View : Type v} + (provider : RustModel Raw) (rawView : Raw → View) + (modelView : provider.Model → View) + (view_decode : ∀ raw value, provider.decode raw = some value → + modelView value = rawView raw) + (post : View → Prop) (raw : Raw) + (admitted : ∃ value, provider.decode raw = some value) + (facts : post (rawView raw)) : + ∃ value, provider.decode raw = some value ∧ post (modelView value) := by + obtain ⟨value, decoded⟩ := admitted + exact ⟨value, decoded, (view_decode raw value decoded).symm ▸ facts⟩ + +end AeneasSpecs diff --git a/verification/aeneas/lean/ModelPrelude.lean b/verification/aeneas/lean/ModelPrelude.lean new file mode 100644 index 0000000000..c3a7f110b9 --- /dev/null +++ b/verification/aeneas/lean/ModelPrelude.lean @@ -0,0 +1,122 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import RustModel +public import Zerocopy.TypesExternal +@[expose] public section + +/-! +Extracted integer words store finite bit patterns. Their mathematical models +expose ordinary `Nat` or `Int` values together with proofs of the corresponding +word bounds. A reader of a decoded word can use those bounds directly, without +reintroducing a separate validity premise. + +Raw nonzero wrappers need an additional admission check: their extracted shape +can contain zero even though the mathematical nonzero model cannot. These +decoders test that condition and preserve its proof in the accepted value. +-/ + +open Aeneas.Std +namespace AeneasSpecs + +-- The bounds are proofs stored beside the value. They contribute constraints +-- to the mathematical type without another runtime field or admission test. +structure UnsignedWord (ty : UScalarTy) where + value : Nat + bound : value ≤ UScalar.max ty + +structure SignedWord (ty : IScalarTy) where + value : Int + lower : IScalar.min ty ≤ value + upper : value ≤ IScalar.max ty + +structure NonZeroUnsignedWord (ty : UScalarTy) extends UnsignedWord ty where + positive : 0 < value + +structure NonZeroSignedWord (ty : IScalarTy) extends SignedWord ty where + nonzero : value ≠ 0 + +instance {ty : UScalarTy} : CoeOut (UnsignedWord ty) Nat := ⟨UnsignedWord.value⟩ +instance {ty : IScalarTy} : CoeOut (SignedWord ty) Int := ⟨SignedWord.value⟩ +instance {ty : UScalarTy} : CoeOut (NonZeroUnsignedWord ty) Nat := ⟨fun x => x.value⟩ +instance {ty : IScalarTy} : CoeOut (NonZeroSignedWord ty) Int := ⟨fun x => x.value⟩ + +-- Proof irrelevance makes equal numeric observations sufficient for record +-- equality: two proofs of the same bound do not create distinct word values. +@[ext] theorem UnsignedWord.ext {x y : UnsignedWord ty} (h : x.value = y.value) : x = y := by + cases x; cases y; simp_all +@[ext] theorem SignedWord.ext {x y : SignedWord ty} (h : x.value = y.value) : x = y := by + cases x; cases y; simp_all +@[ext] theorem NonZeroUnsignedWord.ext {x y : NonZeroUnsignedWord ty} + (h : x.value = y.value) : x = y := by + cases x; cases y; congr 1; exact UnsignedWord.ext h +@[ext] theorem NonZeroSignedWord.ext {x y : NonZeroSignedWord ty} + (h : x.value = y.value) : x = y := by + cases x; cases y; congr 1; exact SignedWord.ext h + +-- Aeneas scalar values already carry their machine bounds. Transport those +-- existing facts when decoding, instead of retesting valid bit patterns. +def unsignedWord (x : UScalar ty) : UnsignedWord ty := + ⟨x.val, by have := x.hBounds; simp only [UScalar.max]; omega⟩ +def signedWord (x : IScalar ty) : SignedWord ty := + ⟨x.val, by simpa only [IScalar.min] using x.hmin, + by have := x.hmax; simp only [IScalar.max]; omega⟩ + +@[reducible] instance modelUScalar (ty : UScalarTy) : RustModel (UScalar ty) := + ⟨UnsignedWord ty, fun x => some (unsignedWord x)⟩ +@[reducible] instance modelIScalar (ty : IScalarTy) : RustModel (IScalar ty) := + ⟨SignedWord ty, fun x => some (signedWord x)⟩ + +@[reducible] instance modelNonZeroUScalar (ty : UScalarTy) (Inner : Type) : + RustModel (core.num.nonzero.NonZero (UScalar ty) Inner) where + Model := NonZeroUnsignedWord ty + decode raw := if h : 0 < raw.val.val then some ⟨unsignedWord raw.val, h⟩ else none + +@[reducible] instance modelNonZeroIScalar (ty : IScalarTy) (Inner : Type) : + RustModel (core.num.nonzero.NonZero (IScalar ty) Inner) where + Model := NonZeroSignedWord ty + decode raw := if h : raw.val.val ≠ 0 then some ⟨signedWord raw.val, h⟩ else none + +abbrev NonZeroUsizeValue := NonZeroUnsignedWord .Usize +abbrev modelNonZeroUsize := modelNonZeroUScalar .Usize core.num.niche_types.NonZeroUsizeInner + +@[simp] theorem unsignedWord_value (x : UScalar ty) : (unsignedWord x).value = x.val := rfl +@[simp] theorem signedWord_value (x : IScalar ty) : (signedWord x).value = x.val := rfl +@[simp] theorem decodeUScalar (x : UScalar ty) : + (modelUScalar ty).decode x = some (unsignedWord x) := rfl +@[simp] theorem decodeIScalar (x : IScalar ty) : + (modelIScalar ty).decode x = some (signedWord x) := rfl + +/-- Reduce an explicit decoder equation to the numeric observation it preserves. +Extensionality handles the bound proofs, so operation proofs need only numbers. -/ +theorem decodeUScalar_iff (raw : UScalar ty) (value : UnsignedWord ty) : + (modelUScalar ty).decode raw = some value ↔ raw.val = value.value := by + change some (unsignedWord raw) = some value ↔ raw.val = value.value + simp only [Option.some.injEq] + exact ⟨fun h => congrArg UnsignedWord.value h, (fun h => UnsignedWord.ext (x := unsignedWord raw) h)⟩ + +theorem decodeNonZeroUScalar_iff (raw : core.num.nonzero.NonZero (UScalar ty) Inner) + (value : NonZeroUnsignedWord ty) : + (modelNonZeroUScalar ty Inner).decode raw = some value ↔ raw.val.val = value.value := by + change (if h : 0 < raw.val.val then some ⟨unsignedWord raw.val, h⟩ else none) = some value ↔ _ + constructor + · intro h + split at h + · cases Option.some.inj h; rfl + · contradiction + · intro h + -- In the reverse direction, the supplied mathematical value already proves + -- positivity. Numeric equality transfers it to the raw wrapper and selects + -- the accepting branch; there is no separate validity assumption. + have hp : 0 < raw.val.val := h ▸ value.positive + rw [dif_pos hp] + congr 1 + exact NonZeroUnsignedWord.ext h + +end AeneasSpecs diff --git a/verification/aeneas/lean/ModelSupport.lean b/verification/aeneas/lean/ModelSupport.lean new file mode 100644 index 0000000000..8359dac865 --- /dev/null +++ b/verification/aeneas/lean/ModelSupport.lean @@ -0,0 +1,56 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import ModelShapes +import all Mathlib.Data.Nat.Log +import all Init.Data.Nat.Power2.Basic +@[expose] public section + +/-! +The inline rounding decoder constructs a constrained alignment/phase pair. +These ordinary arithmetic lemmas explain why a positive word has the required +components, and why those components are unique. This module imports model +shapes rather than full providers, so it can be available while decoders are +being elaborated. It supplies reusable facts, not a separate certification +duty that every mathematical model must satisfy. +-/ +open Aeneas Aeneas.Std AeneasSpecs + +namespace Zerocopy.RoundingFacts + +/-- A positive word has a unique highest power-of-two bit. -/ +theorem bounds (word : Nat) (positive : 0 < word) : + 2 ^ Nat.log2 word ≤ word ∧ word < 2 ^ (Nat.log2 word + 1) := + (Nat.log2_eq_iff (by omega)).mp rfl + +theorem reconstruction (word : Nat) (positive : 0 < word) : + 2 ^ Nat.log2 word + (word - 2 ^ Nat.log2 word) = word := by + have := bounds word positive + omega + +/-- These are the exact components of every positive encoding, not a restricted +constructor-only subset of the representation domain. -/ +theorem components_unique (word align phase : Nat) + (pow2 : align.isPowerOfTwo) (phase_lt : phase < align) + (encoding : word = align + phase) : + align = 2 ^ Nat.log2 word ∧ phase = word - 2 ^ Nat.log2 word := by + obtain ⟨k, rfl⟩ := pow2 + have positive : 0 < word := by have := Nat.two_pow_pos k; omega + have hk : Nat.log2 word = k := + (Nat.log2_eq_iff (by omega)).mpr (by rw [Nat.pow_succ]; omega) + rw [hk] + omega + +/-- Proof irrelevance makes equality depend only on the observed pair. -/ +@[ext] theorem value_ext + {a b : layout.RoundingAlignAndPhase.RoundingValue} + (ha : a.align = b.align) (hp : a.phase = b.phase) : a = b := by + cases a; cases b; simp_all + +end Zerocopy.RoundingFacts diff --git a/verification/aeneas/lean/ModelTests.lean b/verification/aeneas/lean/ModelTests.lean new file mode 100644 index 0000000000..c568be7502 --- /dev/null +++ b/verification/aeneas/lean/ModelTests.lean @@ -0,0 +1,294 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import DeriveModels +@[expose] public section + +/-! +These fixtures challenge structural model generation independently of zerocopy's +layout models. They cover generic shapes and dictionaries, nominal identity, +containers, enum payloads, and lossy local decoders. The rejected-child cases +are essential: an infallible parent that forgets a field must still reject that +field's invalid raw representation. Decomposition checks also pin the accepted +witness order rather than merely checking one decoder result. +-/ + +open Lean Aeneas Aeneas.Std AeneasSpecs +namespace ModelTests +set_option linter.unusedVariables false + +structure Child where + value : Nat + +aeneas_model_shape Child with 0 type parameters begin + model Positive where + value : Nat + positive : 0 < value +end +derive_rust_model Child with 0 type parameters decode? self => + if h : 0 < self.value then some ⟨self.value, h⟩ else none +check_model_binding Child with 0 type parameters + +structure Parent where + child : Child + ignored : Nat +derive_model_shape Parent with 0 type parameters +derive_rust_model Parent with 0 type parameters +check_model_binding Parent with 0 type parameters +example : Parent.decode ⟨⟨0⟩, 1⟩ = none := rfl + +structure Box (α : Type u) where + value : Option (List α) +derive_model_shape Box with 1 type parameters +derive_rust_model Box with 1 type parameters +check_model_binding Box with 1 type parameters +example (α : Type u) [m : RustModel α] : + (@Box.aeneasModel α m).Model = Box.Fields m.Model := rfl +example (α : Type u) [m : RustModel α] : + @Box.decode α m ⟨none⟩ = some ⟨none⟩ := rfl + +inductive Choice (α : Type u) where + | one : α → Choice α + | empty : Choice α +derive_model_shape Choice with 1 type parameters +derive_rust_model Choice with 1 type parameters +check_model_binding Choice with 1 type parameters +example (α : Type u) [m : RustModel α] : + @Choice.decode α m .empty = some .empty := rfl + +structure SameShape where + child : Child + ignored : Nat +derive_model_shape SameShape with 0 type parameters +derive_rust_model SameShape with 0 type parameters +run_meta do + if ← Lean.Meta.isDefEq (Lean.mkConst ``Parent.Fields) (Lean.mkConst ``SameShape.Fields) then + throwError "Different owners lost nominal identity" + +-- Even unused Rust generics retain their dictionary and carrier parameter. +structure Phantom (T : Type u) where + value : Nat +aeneas_model_shape Phantom with 1 type parameters begin + model Value := Unit +end +derive_rust_model Phantom with 1 type parameters decode self => () +check_model_binding Phantom with 1 type parameters +example (T : Type u) [m : RustModel T] : + @Phantom.decode T m ⟨7⟩ = some () := rfl + +-- An authored Fields alias shares the one nominal carrier, including generics. +structure AliasBox (T : Type u) where + value : T +aeneas_model_shape AliasBox with 1 type parameters begin + model Value := Fields +end +derive_rust_model AliasBox with 1 type parameters +example (T : Type u) [m : RustModel T] : AliasBox.Value m.Model = AliasBox.Fields m.Model := rfl + +-- Even an infallible parent that discards all children first decodes them all. +structure Forgetful where + child : Child +aeneas_model_shape Forgetful with 0 type parameters begin + model Value := Unit +end +derive_rust_model Forgetful with 0 type parameters decode self => () +example : Forgetful.decode ⟨⟨0⟩⟩ = none := rfl + +structure ArrayBox (T : Type u) where + values : Aeneas.Std.Array T (2#usize) +derive_model_shape ArrayBox with 1 type parameters +derive_rust_model ArrayBox with 1 type parameters +check_model_binding ArrayBox with 1 type parameters +structure SliceBox (T : Type u) where + values : Slice T +derive_model_shape SliceBox with 1 type parameters +derive_rust_model SliceBox with 1 type parameters +check_model_binding SliceBox with 1 type parameters + +-- Nested nominal generic fields use the same original dictionary. +structure Nested (T : Type u) where + value : Box T +derive_model_shape Nested with 1 type parameters +derive_rust_model Nested with 1 type parameters +check_model_binding Nested with 1 type parameters + +structure Heterogeneous (T : Type u) where + value : T × Nat +derive_model_shape Heterogeneous with 1 type parameters +derive_rust_model Heterogeneous with 1 type parameters + +example : @RustModel.decode (Option Nat) (modelOption) none = some none := rfl +example : @RustModel.decode (core.result.Result Nat Bool) (modelRustResult) (.Err true) = + some (.Err true) := rfl + +abbrev ChildAlias := Child +example (x : ChildAlias) : @RustModel.decode ChildAlias Child.aeneasModel x = Child.decode x := rfl +example (x y : Child.Positive) (h : x.value = y.value) : x = y := Child.Positive.ext h + +syntax (name := discardedModelFixture) "discard_model_fixture " command : command +@[command_elab discardedModelFixture] meta def discardModelFixture : Lean.Elab.Command.CommandElab := + fun stx => Lean.withoutModifyingEnv (Lean.Elab.Command.elabCommand stx[1]) + +structure Opaque where + value : Nat +structure MissingParent where + child : Opaque +/-- error: Unsupported opaque carrier or missing bound model provider: Opaque -/ +#guard_msgs in +discard_model_fixture derive_model_shape MissingParent with 0 type parameters + +inductive Recursive where + | more : Recursive → Recursive +/-- error: Models require a nonrecursive, nonindexed nominal Rust type: ModelTests.Recursive -/ +#guard_msgs in +discard_model_fixture derive_model_shape Recursive with 0 type parameters + +inductive Indexed : Nat → Type where + | zero : Indexed 0 +/-- error: Models require a nonrecursive, nonindexed nominal Rust type: ModelTests.Indexed -/ +#guard_msgs in +discard_model_fixture derive_model_shape Indexed with 0 type parameters + +structure Escaping where + continuation : Nat → Nat +/-- error: Unsupported escaping function or backward reconstruction in model carrier -/ +#guard_msgs in +discard_model_fixture derive_model_shape Escaping with 0 type parameters + +/-- error: A type fence must contain exactly one model shape declaration -/ +#guard_msgs in +discard_model_fixture aeneas_model_shape Opaque with 0 type parameters begin + def injection : Nat := 0 +end + +-- Concrete generic shape arguments require only their mathematical shape. +-- The child decoder deliberately appears after the containing shape. +structure RoundingRaw where + word : core.num.nonzero.NonZero Usize core.num.niche_types.NonZeroUsizeInner +aeneas_model_shape RoundingRaw with 0 type parameters begin + model RoundingValue where + value : Nat + positive : 0 < value +end +structure Wrapper (T : Type u) where + value : T +derive_model_shape Wrapper with 1 type parameters +structure BeforeDecoder where + child : Wrapper RoundingRaw + choices : Option (List (Wrapper RoundingRaw)) + slice : Slice (Wrapper RoundingRaw) + array : Aeneas.Std.Array (Wrapper RoundingRaw) (2#usize) +derive_model_shape BeforeDecoder with 0 type parameters +example : BeforeDecoder.Fields → Wrapper.Fields RoundingRaw.RoundingValue := fun x => x.child +example : BeforeDecoder.Fields → SliceValue (Wrapper.Fields RoundingRaw.RoundingValue) := + fun x => x.slice +example : BeforeDecoder.Fields → ArrayValue (Wrapper.Fields RoundingRaw.RoundingValue) (2#usize) := + fun x => x.array + +derive_rust_model RoundingRaw with 0 type parameters decode self => + ⟨self.word.value, self.word.positive⟩ +derive_rust_model Wrapper with 1 type parameters +derive_rust_model BeforeDecoder with 0 type parameters +check_model_binding BeforeDecoder with 0 type parameters + +-- Later dictionaries retain distinct meanings despite sharing one shape family. +@[reducible] def identityDictionary : RustModel Nat := ⟨Nat, some⟩ +@[reducible] def doubleDictionary : RustModel Nat := ⟨Nat, fun n => some (2 * n)⟩ +example : @Wrapper.decode Nat identityDictionary ⟨13⟩ = some ⟨13⟩ := rfl +example : @Wrapper.decode Nat doubleDictionary ⟨13⟩ = some ⟨26⟩ := rfl +example : (@Wrapper.aeneasModel Nat identityDictionary).Model = + (@Wrapper.aeneasModel Nat doubleDictionary).Model := rfl + +-- Authored generic shapes bind TModel; decoding still has the actual T dictionary. +structure Authored (T : Type u) where + value : T +aeneas_model_shape Authored with 1 type parameters begin + model Value where + value : TModel +end +derive_rust_model Authored with 1 type parameters decode self => + { value := (self.value : ModelOf T) } +check_model_binding Authored with 1 type parameters +example (TModel : Type u) : Authored.Value TModel → TModel := fun x => x.value + +-- An infallible lossy parent still rejects a malformed native nonzero child. +structure ForgetfulWord where + word : core.num.nonzero.NonZero Usize core.num.niche_types.NonZeroUsizeInner +aeneas_model_shape ForgetfulWord with 0 type parameters begin + model Value := Unit +end +derive_rust_model ForgetfulWord with 0 type parameters decode self => () +example : ForgetfulWord.decode ⟨⟨0#usize⟩⟩ = none := rfl +example (raw : ForgetfulWord) : ForgetfulWord.decode raw = some () ↔ + ∃ word, (modelNonZeroUsize).decode raw.word = some word ∧ + ForgetfulWord.decodeFields ⟨word⟩ = some () := ForgetfulWord.decode_decompose raw () + +-- Separate universe parameters and container bounds remain present. +structure Mixed (A : Type u) (B : Type v) where + value : A × Option B +derive_model_shape Mixed with 2 type parameters +derive_rust_model Mixed with 2 type parameters +check_model_binding Mixed with 2 type parameters +example (A : Type u) (B : Type v) : Mixed.Fields A B → A × Option B := fun x => x.value +example (T : Type u) [m : RustModel T] (x : ArrayBox.Fields m.Model) : + x.values.val.length = (2#usize).val := x.values.property +example (T : Type u) [m : RustModel T] (x : SliceBox.Fields m.Model) : + x.values.val.length ≤ Usize.max := x.values.property + +-- Decomposition exposes every child's decoded witness, including forgotten data. +example (raw : Forgetful) : Forgetful.decode raw = some () ↔ + ∃ child, Child.decode raw.child = some child ∧ + Forgetful.decodeFields ⟨child⟩ = some () := Forgetful.decode_decompose raw () +example (raw : Parent) (value : Parent.Fields) : Parent.decode raw = some value ↔ + ∃ child, Child.decode raw.child = some child ∧ + ∃ ignored, (modelNat).decode raw.ignored = some ignored ∧ + Parent.decodeFields ⟨child, ignored⟩ = some value := Parent.decode_decompose raw value +example (T : Type u) [m : RustModel T] (x : T) (value : Choice.Fields m.Model) : + @Choice.decode T m (.one x) = some value ↔ + ∃ child, m.decode x = some child ∧ @Choice.decodeFields T m (.one child) = some value := + Choice.decode_decompose T (.one x) value +example (T : Type u) [m : RustModel T] (value : Choice.Fields m.Model) : + @Choice.decode T m .empty = some value ↔ @Choice.decodeFields T m .empty = some value := + Choice.decode_decompose T .empty value + +-- An enum traverses exactly its selected payload. The local decoder forgets +-- both fields, but the second field's rejection must still reject the pair. +-- Its empty case has no child admission obligation. +inductive ForgetfulChoice where + | pair : Nat → Child → ForgetfulChoice + | empty : ForgetfulChoice +aeneas_model_shape ForgetfulChoice with 0 type parameters begin + model Value := Unit +end +derive_rust_model ForgetfulChoice with 0 type parameters decode self => () +check_model_binding ForgetfulChoice with 0 type parameters +example : ForgetfulChoice.decode (.pair 7 ⟨0⟩) = none := rfl +example : ForgetfulChoice.decode (.pair 7 ⟨1⟩) = some () := rfl +example : ForgetfulChoice.decode .empty = some () := rfl +example (n : Nat) (raw : Child) : ForgetfulChoice.decode (.pair n raw) = some () ↔ + ∃ number, modelNat.decode n = some number ∧ + ∃ child, Child.decode raw = some child ∧ + ForgetfulChoice.decodeFields (.pair number child) = some () := + ForgetfulChoice.decode_decompose (.pair n raw) () +example : ForgetfulChoice.decode .empty = some () ↔ + ForgetfulChoice.decodeFields .empty = some () := + ForgetfulChoice.decode_decompose .empty () + +-- Audit the generated bridge terms as well as their successful elaboration. +run_meta do + for owner in ← modelOwnerNames do + if !(`ModelTests).isPrefixOf owner then continue + let theoremName := owner ++ `decode_decompose + let .thmInfo _ ← getConstInfo theoremName + | throwError "Missing structural decomposition theorem for {owner}" + for axiomName in ← Lean.collectAxioms theoremName do + unless #[`propext, `Classical.choice, `Quot.sound].contains axiomName do + throwError "Unexpected axiom in generated decomposition {theoremName}: {axiomName}" + +end ModelTests diff --git a/verification/aeneas/lean/Obligations.lean b/verification/aeneas/lean/Obligations.lean new file mode 100644 index 0000000000..e2b5a5aa10 --- /dev/null +++ b/verification/aeneas/lean/Obligations.lean @@ -0,0 +1,149 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Zerocopy.Funs +public import SpecPrelude +public import MathViews +public import Arithmetic +public import LayoutModel +@[expose] public section + +/-! +This module independently states the behavior and input domains we want to +retain. Its propositions are written by the integration's authors, rather +than generated from Rust comments. They protect against a comment's macro +expansion or a model migration expressing less than a reviewer intended. + +Each name ending in _contract describes an arbitrary execution outcome for +the original inputs. Its unsuffixed proposition specializes that outcome to +the actual extracted call. RequiredContracts first proves that the authored +spec implies the arbitrary-outcome expectation, then specializes the +implication. These declarations are expectations awaiting proofs, not axioms +or a second proof that the compiler implements Rust correctly. +-/ +open Aeneas Aeneas.Std AeneasSpecs +namespace Zerocopy.Obligations +open Zerocopy.Proofs + +def nested_round_up_spec_contract (size align : Usize) + (run : Result (Option Usize)) : Prop := + run ⦃ rounded => rounded.map UScalar.val = + if align.val = 0 then none + else if LayoutMath.roundUp size.val align.val ≤ Usize.max + then some (LayoutMath.roundUp size.val align.val) else none ⦄ + +def nested_round_up_spec : Prop := + ∀ size align, nested_round_up_spec_contract size align + (layout.nested_reference.round_up size align) + +def nested_reference_size_spec_contract + (leading : Slice layout.nested_reference.NestedLayer) (elem_size leaf_align elems : Usize) + (run : Result (Option Usize)) : Prop := + run ⦃ result => result.map UScalar.val = + NestedReference.checkedSize leading.val elem_size.val leaf_align.val elems.val ⦄ + +def nested_reference_size_spec : Prop := + ∀ leading elem_size leaf_align elems, + nested_reference_size_spec_contract leading elem_size leaf_align elems + (layout.nested_reference.size_for_metadata leading elem_size leaf_align elems) + +abbrev NonZeroUsize := + core.num.nonzero.NonZero Usize core.num.niche_types.NonZeroUsizeInner + +def max_spec_contract (a b : NonZeroUsize) + (run : Result NonZeroUsize) : Prop := + 0 < a.val.val → 0 < b.val.val → ∃ r, run = .ok r ∧ 0 < r.val.val ∧ + r.val.val = Nat.max a.val.val b.val.val ∧ (r = a ∨ r = b) ∧ + a.val.val ≤ r.val.val ∧ b.val.val ≤ r.val.val + +def max_spec : Prop := + ∀ (a b : NonZeroUsize), + max_spec_contract a b (util.max a b) + +def min_spec_contract (a b : NonZeroUsize) + (run : Result NonZeroUsize) : Prop := + 0 < a.val.val → 0 < b.val.val → ∃ r, run = .ok r ∧ 0 < r.val.val ∧ + r.val.val = Nat.min a.val.val b.val.val ∧ (r = a ∨ r = b) ∧ + r.val.val ≤ a.val.val ∧ r.val.val ≤ b.val.val + +def min_spec : Prop := + ∀ (a b : NonZeroUsize), + min_spec_contract a b (util.min a b) + +def padding_lt_alignment_contract (len : Usize) (align : NonZeroUsize) + (run : Result Usize) : Prop := + ∀ (_valid : 0 < align.val.val) (_h : align.val.val.isPowerOfTwo), + run ⦃ p => p.val < align.val.val ∧ + p.val = (align.val.val - len.val % align.val.val) % align.val.val ∧ + (len.val + p.val) % align.val.val = 0 ∧ + (∀ q : Nat, (len.val + q) % align.val.val = 0 → p.val ≤ q) ∧ + (p.val = 0 ↔ len.val % align.val.val = 0) ⦄ + +def padding_lt_alignment : Prop := + ∀ (len : Usize) (align : NonZeroUsize), + padding_lt_alignment_contract len align (util.padding_needed_for len align) + +def round_down_spec_contract (n : Usize) (align : NonZeroUsize) + (run : Result Usize) : Prop := + ∀ (_valid : 0 < align.val.val) (_h : align.val.val.isPowerOfTwo), + run ⦃ m => + m.val ≤ n.val ∧ m.val = n.val - n.val % align.val.val ∧ + m.val % align.val.val = 0 ∧ n.val < m.val + align.val.val ∧ + (∀ q : Nat, q ≤ n.val → q % align.val.val = 0 → q ≤ m.val) ⦄ + +def round_down_spec : Prop := + ∀ (n : Usize) (align : NonZeroUsize), + round_down_spec_contract n align (util.round_down_to_next_multiple_of_alignment n align) + +def encoding_new_spec_contract (a : NonZeroUsize) (p : Usize) + (run : Result layout.RoundingAlignAndPhase) : Prop := + 0 < a.val.val → a.val.val.isPowerOfTwo → p.val < a.val.val → + ∃ code, run = .ok code ∧ encodingValid code ∧ code._0.val.val = a.val.val + p.val + +def encoding_new_spec : Prop := + ∀ (a : NonZeroUsize) (p : Usize), + encoding_new_spec_contract a p (layout.RoundingAlignAndPhase.new a p) + +def encoding_components_spec_contract (code : layout.RoundingAlignAndPhase) + (run : Result (NonZeroUsize × Usize)) : Prop := + 0 < code._0.val.val → + run ⦃ (a, p) => + 0 < a.val.val ∧ a.val.val.isPowerOfTwo ∧ p.val < a.val.val ∧ a.val.val + p.val = code._0.val.val ∧ + a.val.val = 2 ^ code._0.val.val.log2 ∧ p.val = code._0.val.val - 2 ^ code._0.val.val.log2 ⦄ + +def encoding_components_spec : Prop := + ∀ (code : layout.RoundingAlignAndPhase), + encoding_components_spec_contract code (layout.RoundingAlignAndPhase.components code) + +def encoding_align_spec_contract (code : layout.RoundingAlignAndPhase) + (run : Result NonZeroUsize) : Prop := + 0 < code._0.val.val → + ∃ a, run = .ok a ∧ 0 < a.val.val ∧ a.val.val = 2 ^ code._0.val.val.log2 + +def encoding_align_spec : Prop := + ∀ (code : layout.RoundingAlignAndPhase), + encoding_align_spec_contract code (layout.RoundingAlignAndPhase.align code) + + +end Zerocopy.Obligations + +namespace Zerocopy.Obligations + +-- The source harness promises successful execution, including its assertions. +-- The positive premise records the Rust NonZeroUsize input domain independently +-- of the generated specification's decoding expansion. +def arithmetic_checks_spec_contract (_len : Usize) (align : NonZeroUsize) + (run : Result Unit) : Prop := + 0 < align.val.val → run ⦃ _ => True ⦄ + +def arithmetic_checks_spec : Prop := + ∀ (len : Usize) (align : NonZeroUsize), + arithmetic_checks_spec_contract len align (util.checks.check_arithmetic len align) + +end Zerocopy.Obligations diff --git a/verification/aeneas/lean/Proofs.lean b/verification/aeneas/lean/Proofs.lean new file mode 100644 index 0000000000..bee36adb80 --- /dev/null +++ b/verification/aeneas/lean/Proofs.lean @@ -0,0 +1,278 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Specs +public import Proofs.Util +public import Proofs.NestedReference +public import Aeneas +public import Loops +public import RepresentationLaws +public import LayoutMath +import all Mathlib.Data.Nat.Log +import all Init.Data.Nat.Power2.Basic +@[expose] public section + +/-! +This module proves contracts about the actual Aeneas function definitions. +The Raw namespace steps through extracted control flow and machine +arithmetic. Those lemmas deliberately retain useful representation domains, +even when an inline specification admits a smaller domain through automatic +decoding. + +The canonical theorems outside Raw prove the generated Specs propositions. +They use the raw lemmas, decode accepted inputs, and establish that the +actual returned representation decodes and has the requested mathematical +properties. WP.spec_mono transfers a proved computation to a new +postcondition; it cannot replace that computation. Registered specifications +let later proofs reuse a callee's theorem instead of unfolding its +implementation each time. + +Aeneas Result describes execution success, panic, or divergence. Rust Option +and Rust Result values may themselves be successful payloads. The proof must +keep these cases distinct. +-/ +open Aeneas Aeneas.Std +namespace Zerocopy.Proofs.Raw + +set_option linter.unusedVariables false + +/- Read the pinned platform's pointer width through its modeled primitive. This +establishes the runtime constant used by encoding and packing arithmetic. +-/ +theorem pointer_width_spec : + layout.POINTER_WIDTH_BITS + ⦃ w => w.val = System.Platform.numBits ⦄ := by + unfold layout.POINTER_WIDTH_BITS + simp only [core.mem.size_of, ite_true, bind_ok] + have hv : ({ bv := BitVec.ofNat _ (System.Platform.numBits / 8) } : Usize).val = + System.Platform.numBits / 8 := by + change (System.Platform.numBits / 8) % 2 ^ System.Platform.numBits = _ + rcases System.Platform.numBits_eq with h | h <;> simp only [h] <;> decide + step as ⟨w, hw⟩ + · rw [hv] + rcases System.Platform.numBits_eq with h | h <;> + simp only [Usize.max, Usize.numBits, UScalarTy.Usize_numBits_eq, h, + UScalar.val] <;> decide + · rw [hv] at hw + rcases System.Platform.numBits_eq with h | h <;> + simp only [h] at hw ⊢ <;> omega + +/- Connect the constructor's power-of-two and phase checks to exact encoding. +The canonical form also proves decoding of the actual returned word. +-/ +theorem encoding_new_spec : + ∀ (align : NonZeroUsize) (phase : Usize), ∀ (ha : (align.val.val.isPowerOfTwo : Prop)), ∀ (hp : (phase.val < align.val.val : Prop)), + @Zerocopy.layout.RoundingAlignAndPhase.new align phase ⦃ encoded => encoded._0.val.val = align.val.val + phase.val ⦄ := by + intro align phase ha hp + unfold layout.RoundingAlignAndPhase.new + simp only [core.num.nonzero.NonZero.get, bind_ok] + step as ⟨b, hb⟩ + have hbt : b = true := by + have hb' : (b = true) = True := by simpa only [ha] using hb + exact Eq.mpr hb' trivial + simp only [massert, hbt, if_true, bind_ok, UScalar.lt_equiv, hp, lift] + obtain ⟨k, hk⟩ := ha + have hor : (align.val ||| phase).val = align.val.val + phase.val := by + rw [UScalar.val_or, hk] + have h := Nat.two_pow_add_eq_or_of_lt (show phase.val < 2 ^ k by omega) 1 + simpa only [Nat.mul_one] using h.symm + have hnz : align.val ||| phase ≠ 0#usize := by + intro hz + have hz' := congrArg UScalar.val hz + rw [hor] at hz' + have hpos := Nat.two_pow_pos k + change align.val.val + phase.val = 0 at hz' + omega + simp only [core.num.nonzero.NonZero.new, cast_eq, hnz, ↓reduceDIte, ↓reduceIte, + bind_ok, WP.spec_ok] + exact hor + +/- Remove the highest bit from align + phase when phase uses only lower bits. +The decoder implementation uses XOR; the mathematical model uses subtraction. +-/ +theorem highest_bit_xor (k p : Nat) (hp : p < 2 ^ k) : (2 ^ k + p) ^^^ 2 ^ k = p := by + have hjoin : 2 ^ k + p = 2 ^ k ||| p := by + simpa only [Nat.mul_one] using Nat.two_pow_add_eq_or_of_lt hp 1 + rw [hjoin] + apply Nat.eq_of_testBit_eq + intro i + rw [Nat.testBit_xor, Nat.testBit_or] + by_cases hi : i = k + · subst i + rw [Nat.testBit_two_pow_self, Nat.testBit_lt_two_pow hp] + rfl + · rw [Nat.testBit_two_pow_of_ne (Ne.symm hi)] + simp + +/- Prove the implementation's bit scan recovers the alignment and phase of every +positive stored word, not only words built by the constructor. +-/ +theorem encoding_components_spec : + ∀ (self : layout.RoundingAlignAndPhase), ∀ (hn : (0 < self._0.val.val : Prop)), + @Zerocopy.layout.RoundingAlignAndPhase.components self ⦃ (align, phase) => + align.val.val.isPowerOfTwo ∧ phase.val < align.val.val ∧ + align.val.val + phase.val = self._0.val.val ∧ + align.val.val = 2 ^ Nat.log2 self._0.val.val ∧ + phase.val = self._0.val.val - 2 ^ Nat.log2 self._0.val.val ⦄ := by + intro self hn + let k := Nat.log2 self._0.val.val + have hbound := (Nat.log2_eq_iff (show self._0.val.val ≠ 0 by omega)).mp (show Nat.log2 self._0.val.val = k from rfl) + have hsize := UScalar.hSize self._0.val + rw [UScalar.size, UScalarTy.Usize_numBits_eq] at hsize + have hk : k < System.Platform.numBits := by + dsimp [k] + rw [Nat.log2_eq_log_two] + apply Nat.log_lt_of_lt_pow (by omega) + exact hsize + have hbits : 32 ≤ System.Platform.numBits := by + rcases System.Platform.numBits_eq with h | h <;> omega + have hnzbv : self._0.val.bv ≠ 0 := by + intro h + have := congrArg BitVec.toNat h + change self._0.val.val = 0 at this + omega + have hlz : (core.num.Usize.leading_zeros self._0.val).val = System.Platform.numBits - k - 1 := by + change (BitVec.leadingZeros self._0.val.bv) % 2 ^ 32 = _ + unfold BitVec.leadingZeros + rw [if_neg hnzbv] + change (System.Platform.numBits - Nat.log 2 self._0.val.val - 1) % 2 ^ 32 = _ + rw [← Nat.log2_eq_log_two] + apply Nat.mod_eq_of_lt + rcases System.Platform.numBits_eq with h | h <;> rw [h] <;> omega + have hcast : (UScalar.cast .Usize (core.num.Usize.leading_zeros self._0.val)).val = + System.Platform.numBits - k - 1 := by + rw [UScalar.cast_val_eq, hlz, UScalarTy.Usize_numBits_eq] + apply Nat.mod_eq_of_lt + rcases System.Platform.numBits_eq with h | h <;> rw [h] <;> omega + unfold layout.RoundingAlignAndPhase.components + step with pointer_width_spec as ⟨w, hw⟩ + have hone : (1#usize).val = 1 := by simp + have hwpos : (1#usize).val ≤ w.val := by rw [hone, hw]; omega + step with Usize.sub_spec hwpos as ⟨w1, hw1, _⟩ + simp only [core.num.nonzero.NonZero.get, bind_ok, lift] + have hsub : (UScalar.cast .Usize (core.num.Usize.leading_zeros self._0.val)).val ≤ w1.val := by + rw [hcast, hw1, hw] + omega + step with Usize.sub_spec hsub as ⟨shift, hshift, _⟩ + have hshift' : shift.val = k := by rw [hcast, hw1, hw] at hshift; omega + step with Usize.ShiftLeft_spec 1#usize shift (by rw [hshift']; exact hk) as ⟨a, ha, _⟩ + have ha' : a.val = 2 ^ k := by + rw [hshift'] at ha + simp only [Nat.shiftLeft_eq, Nat.one_mul] at ha + have hp : 2 ^ k < Usize.size := by + rw [Usize.size, Usize.numBits, UScalarTy.Usize_numBits_eq] + omega + simpa only [Nat.mod_eq_of_lt hp] using ha + have haz : a ≠ 0#usize := by + intro hz + have hz' := congrArg UScalar.val hz + rw [ha'] at hz' + change 2 ^ k = 0 at hz' + have := Nat.two_pow_pos k + omega + simp only [core.num.nonzero.NonZero.new, cast_eq, haz, ↓reduceDIte, + ↓reduceIte, bind_ok, WP.spec_ok] + have hp : self._0.val.val - 2 ^ k < 2 ^ k := by + rw [Nat.pow_succ] at hbound + omega + have hx : (self._0.val ^^^ a).val = self._0.val.val - 2 ^ k := by + rw [UScalar.val_xor, ha'] + have heq : self._0.val.val = 2 ^ k + (self._0.val.val - 2 ^ k) := by omega + rw [heq] + simpa only [Nat.add_sub_cancel_left] using highest_bit_xor k _ hp + refine ⟨?_, ?_, ?_, ha', ?_⟩ + · exact ⟨k, ha'⟩ + · rw [hx, ha'] + exact hp + · rw [hx, ha'] + omega + · exact hx + +/- Reuse the components theorem for the getter that discards phase. +-/ +theorem encoding_align_spec : + ∀ (self : layout.RoundingAlignAndPhase), ∀ (hn : (0 < self._0.val.val : Prop)), + @Zerocopy.layout.RoundingAlignAndPhase.align self ⦃ align => align.val.val = 2 ^ Nat.log2 self._0.val.val ⦄ := by + intro self hn + unfold layout.RoundingAlignAndPhase.align + step with encoding_components_spec self hn as ⟨a, p, _, _, _, ha, _⟩ + simpa only [WP.spec_ok] using ha + +end Zerocopy.Proofs.Raw + +namespace Zerocopy.Proofs +open AeneasSpecs + +-- Unfold only representation domains. The model calls and authored outcome +-- predicates remain the independently verified raw contracts below. +macro "representation_simps" : tactic => + `(tactic| + (simp only [WP.spec_equiv_exists] + simp (config := { index := false }) only [contract_simps] + simp only [← WP.spec_equiv_exists])) + +/- Connect the constructor's power-of-two and phase checks to exact encoding. +The canonical form also proves decoding of the actual returned word. +-/ +theorem encoding_new_spec : Zerocopy.Specs.encoding_new_spec := by + intro align phase av ha pv hp hpow hphase + have hav := (decodeNonZeroUScalar_iff align av).mp ha + have hpv := (decodeUScalar_iff phase pv).mp hp + apply WP.spec_mono (Raw.encoding_new_spec align phase + (by simpa only [hav] using hpow) (by simpa only [hav, hpv] using hphase)) + intro encoded he + have hfit : av.value + pv.value ≤ Usize.max := by + rw [← hav, ← hpv, ← he] + scalar_tac + let value : layout.RoundingAlignAndPhase.RoundingValue := + ⟨av.value, pv.value, hpow, hphase, hfit⟩ + refine ⟨value, (rounding_decode_iff encoded value).mpr ?_, rfl, rfl⟩ + simpa only [roundingRepresents, value, hav, hpv] using he +register_spec_step encoding_new_spec + +/- Prove the implementation's bit scan recovers the alignment and phase of every +positive stored word, not only words built by the constructor. +-/ +theorem encoding_components_spec : Zerocopy.Specs.encoding_components_spec := by + intro self value decoded + have hn := (encoding_valid_iff self).mp ⟨value, decoded⟩ + have hvalue := rounding_decode_components self value decoded + apply WP.spec_mono (Raw.encoding_components_spec self hn) + rintro ⟨align, phase⟩ facts + let av : NonZeroUsizeValue := + ⟨unsignedWord align.val, Nat.pos_of_isPowerOfTwo facts.1⟩ + have ha : modelNonZeroUsize.decode align = some av := + (decodeNonZeroUScalar_iff align av).mpr rfl + refine ⟨(av, unsignedWord phase), ?_, ?_⟩ + · simp only [RustModel.decode, + dif_pos (Nat.pos_of_isPowerOfTwo facts.1)] + rfl + · exact ⟨facts.2.2.2.1.trans hvalue.1.symm, + facts.2.2.2.2.trans hvalue.2.symm⟩ +register_spec_step encoding_components_spec + +/- Reuse the components theorem for the getter that discards phase. +-/ +theorem encoding_align_spec : Zerocopy.Specs.encoding_align_spec := by + intro self value decoded + have hn := (encoding_valid_iff self).mp ⟨value, decoded⟩ + have hvalue := rounding_decode_components self value decoded + apply WP.spec_mono (Raw.encoding_align_spec self hn) + intro align he + have hp : 0 < align.val.val := by rw [he]; exact Nat.two_pow_pos _ + let av : NonZeroUsizeValue := ⟨unsignedWord align.val, hp⟩ + exact ⟨av, (decodeNonZeroUScalar_iff align av).mpr rfl, he.trans hvalue.1.symm⟩ +register_spec_step encoding_align_spec + +end Zerocopy.Proofs + +namespace Zerocopy.Proofs +open AeneasSpecs + +end Zerocopy.Proofs diff --git a/verification/aeneas/lean/Proofs/ArithmeticChecks.lean b/verification/aeneas/lean/Proofs/ArithmeticChecks.lean new file mode 100644 index 0000000000..596f49d20e --- /dev/null +++ b/verification/aeneas/lean/Proofs/ArithmeticChecks.lean @@ -0,0 +1,54 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Proofs.Util +@[expose] public section + +/-! +The Rust assertions are the application promise. These lemmas connect the +extracted remainder calculations to the already proved bitwise helpers. The +canonical theorem establishes successful execution of the entire harness, +including both assertions, for every accepted input. +-/ +open Aeneas Aeneas.Std +namespace Zerocopy.Proofs.Raw + +theorem arithmetic_checks (len : Usize) (align : NonZeroUsize) : + util.checks.check_arithmetic len align ⦃ _ => True ⦄ := by + unfold util.checks.check_arithmetic + simp only [core.num.nonzero.NonZero.get, bind_ok] + step as ⟨pow2, hp⟩ + split + · rename_i enabled + have power : align.val.val.isPowerOfTwo := by + exact Eq.mp hp enabled + have positive := Nat.pos_of_isPowerOfTwo power + step as ⟨remainder, hr⟩ + step as ⟨gap, hg⟩ + step as ⟨padding, hpadding⟩ + step with padding_lt_alignment len align power as ⟨actual, _, exactPadding, _⟩ + step + step with round_down_spec len align power as ⟨rounded, _, exactRounded, _⟩ + step as ⟨expected, hexpected⟩ + · rw [hr] + exact Nat.mod_le _ _ + · step + · simp only [WP.spec_ok] + +end Zerocopy.Proofs.Raw + +namespace Zerocopy.Proofs + +theorem arithmetic_checks_spec : Specs.arithmetic_checks_spec := by + intro len align _ _ _ _ + apply WP.spec_mono (Raw.arithmetic_checks len align) + intro result _ + exact ⟨(), rfl, trivial⟩ + +end Zerocopy.Proofs diff --git a/verification/aeneas/lean/Proofs/NestedReference.lean b/verification/aeneas/lean/Proofs/NestedReference.lean new file mode 100644 index 0000000000..071f791689 --- /dev/null +++ b/verification/aeneas/lean/Proofs/NestedReference.lean @@ -0,0 +1,419 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Specs +public import Loops +public import Obligations +public import ModelLemmas +import all Init.Data.Nat.Power2.Basic +@[expose] public section + +open Aeneas Aeneas.Std +namespace Zerocopy.Proofs.Raw + +def nestedStateView (state : Usize × Usize) : Nat × Nat := (state.1.val, state.2.val) + +theorem nested_round_up_spec (size align : Usize) : + layout.nested_reference.round_up size align + ⦃ rounded => rounded.map UScalar.val = + if align.val = 0 then none + else if LayoutMath.roundUp size.val align.val ≤ Usize.max + then some (LayoutMath.roundUp size.val align.val) else none ⦄ := by + unfold layout.nested_reference.round_up + by_cases hz : align = 0#usize + · subst align + simp + · simp only [hz, if_false] + have ha : 0 < align.val := by + have hn : align.val ≠ 0 := by + intro h + apply hz + exact UScalar.eq_of_val_eq (by simpa using h) + omega + step with Usize.rem_spec size (y := align) (by omega) as ⟨remainder, hr⟩ + by_cases hrz : remainder = 0#usize + · have hr0 : size.val % align.val = 0 := by + have h := congrArg UScalar.val hrz + simpa only [hr, show (0#usize).val = 0 by simp] using h + simp only [hrz, if_true, WP.spec_ok, Option.map_some, + show ¬align.val = 0 by omega, if_false, LayoutMath.roundUp_eq _ _ hr0, + show size.val ≤ Usize.max by scalar_tac, if_true] + · simp only [hrz, if_false] + have hrem := Nat.mod_lt size.val ha + step with Usize.sub_spec (x := align) (y := remainder) (by rw [hr]; omega) + as ⟨padding, hp, _⟩ + have hrpos : 0 < size.val % align.val := by + have hn : remainder.val ≠ 0 := by + intro h + apply hrz + exact UScalar.eq_of_val_eq (by simpa using h) + omega + have hp' : padding.val = (align.val - size.val % align.val) % align.val := by + have hm : (align.val - size.val % align.val) % align.val = + align.val - size.val % align.val := Nat.mod_eq_of_lt (by omega) + rw [hp, hr, hm] + have hchecked := Usize.checked_add_bv_spec size padding + cases hc : Usize.checked_add size padding <;> + simp only [hc] at hchecked + · have hn : ¬LayoutMath.roundUp size.val align.val ≤ Usize.max := by + unfold LayoutMath.roundUp + omega + simp only [Option.map_none, show ¬align.val = 0 by omega, if_false, hn] + · rename_i rounded + have hs : LayoutMath.roundUp size.val align.val ≤ Usize.max := by + unfold LayoutMath.roundUp + omega + simp only [Option.map_some, show ¬align.val = 0 by omega, if_false, hs, if_true] + congr 1 + unfold LayoutMath.roundUp + omega + +theorem nested_apply_layer_spec (layer : layout.nested_reference.NestedLayer) + (size alignment : Usize) (ha : alignment.val.isPowerOfTwo) : + layout.nested_reference.apply_layer layer size alignment + ⦃ result => result.map nestedStateView = + NestedReference.checkedLayer layer (some (size.val, alignment.val)) ⦄ := by + classical + unfold layout.nested_reference.apply_layer + step as ⟨packedPow, hp⟩ + cases packedPow with + | false => + have hn : ¬layer.packed.val.isPowerOfTwo := by + intro h + have := (Iff.of_eq hp).mpr h + contradiction + have hl : ¬NestedReference.layerValid layer := by + intro h + exact hn h.1 + simp only [Bool.false_eq_true, if_false, WP.spec_ok, Option.map_none, + NestedReference.checkedLayer, hl, if_false] + | true => + have hp' : layer.packed.val.isPowerOfTwo := (Iff.of_eq hp).mp rfl + simp only [if_true] + step as ⟨minimumPow, hm⟩ + cases minimumPow with + | false => + have hn : ¬layer.min_align.val.isPowerOfTwo := by + intro h + have := (Iff.of_eq hm).mpr h + contradiction + have hl : ¬NestedReference.layerValid layer := by + intro h + exact hn h.2.1 + simp only [Bool.false_eq_true, if_false, WP.spec_ok, Option.map_none, + NestedReference.checkedLayer, hl, if_false] + | true => + have hm' : layer.min_align.val.isPowerOfTwo := (Iff.of_eq hm).mp rfl + simp only [if_true, UScalar.lt_equiv] + split + · rename_i hlarge + have hl : ¬NestedReference.layerValid layer := by + intro h + exact not_lt_of_ge h.2.2 hlarge + simp only [WP.spec_ok, Option.map_none, NestedReference.checkedLayer, hl, if_false] + · rename_i hsmall + have hl : NestedReference.layerValid layer := ⟨hp', hm', by omega⟩ + let field := min alignment layer.packed + have hfieldRaw : (if alignment < layer.packed then Result.ok alignment else .ok layer.packed) = + Result.ok field := by + by_cases h : alignment < layer.packed + · simp only [h, if_true, field, min_eq_left (le_of_lt h)] + · simp only [h, if_false, field, min_eq_right (le_of_not_gt h)] + have hfield : (if alignment.val < layer.packed.val then Result.ok alignment else .ok layer.packed) = + Result.ok field := by simpa only [UScalar.lt_equiv] using hfieldRaw + simp only [hfield, bind_ok] + have hf : field.val = min alignment.val layer.packed.val := Arithmetic.coe_min _ _ + have hfp : field.val.isPowerOfTwo := by + rw [hf] + by_cases h : alignment.val ≤ layer.packed.val + · simpa only [Nat.min_eq_left h] using ha + · simpa only [Nat.min_eq_right (by omega : layer.packed.val ≤ alignment.val)] using hp' + have hfpos := Nat.pos_of_isPowerOfTwo hfp + step with nested_round_up_spec layer.prefix_bytes field as ⟨offset, hoffset⟩ + change offset.map UScalar.val = if field.val = 0 then none else + if LayoutMath.roundUp layer.prefix_bytes.val field.val ≤ Usize.max then + some (LayoutMath.roundUp layer.prefix_bytes.val field.val) else none at hoffset + simp only [show ¬field.val = 0 by omega, if_false] at hoffset + let outer := max layer.min_align field + have houtRaw : (if layer.min_align < field then Result.ok field else .ok layer.min_align) = + Result.ok outer := by + by_cases h : layer.min_align < field + · simp only [h, if_true, outer, max_eq_right (le_of_lt h)] + · simp only [h, if_false, outer, max_eq_left (le_of_not_gt h)] + have hout : (if layer.min_align.val < field.val then Result.ok field else .ok layer.min_align) = + Result.ok outer := by simpa only [UScalar.lt_equiv] using houtRaw + have ho : outer.val = max layer.min_align.val field.val := Arithmetic.coe_max _ _ + have hopos : 0 < outer.val := by + rw [ho] + have := Nat.le_max_right layer.min_align.val field.val + omega + let target := NestedReference.step layer (size.val, alignment.val) + have htarget : target = + (LayoutMath.roundUp (LayoutMath.roundUp layer.prefix_bytes.val field.val + size.val) + outer.val, outer.val) := by + simp only [target, NestedReference.step, hf, ho] + have hcontains : LayoutMath.roundUp layer.prefix_bytes.val field.val ≤ target.1 := by + rw [htarget] + simp only [LayoutMath.roundUp] + omega + have hmodel : NestedReference.checkedLayer layer (some (size.val, alignment.val)) = + if target.1 ≤ Usize.max then some target else none := by + simp only [NestedReference.checkedLayer, hl, if_true, Option.bind_some] + rfl + simp only [hmodel] + cases offset with + | none => + have hnot : ¬LayoutMath.roundUp layer.prefix_bytes.val field.val ≤ Usize.max := by + split at hoffset <;> simp_all + have hn : ¬target.1 ≤ Usize.max := by omega + simp only [WP.spec_ok, Option.map_none, hn, if_false] + | some offset => + have hv : offset.val = LayoutMath.roundUp layer.prefix_bytes.val field.val := by + split at hoffset <;> simp_all + simp only [hout, bind_ok] + step as ⟨unpadded, hu⟩ + cases unpadded with + | none => + have hsum : Usize.max < offset.val + size.val := hu + have hn : ¬target.1 ≤ Usize.max := by + rw [htarget] + simp only [LayoutMath.roundUp] at hv ⊢ + omega + simp only [WP.spec_ok, Option.map_none, hn, if_false] + | some unpadded => + have husize : unpadded.val = offset.val + size.val := hu.2.1 + step with nested_round_up_spec unpadded outer as ⟨rounded, hr⟩ + change rounded.map UScalar.val = if outer.val = 0 then none else + if LayoutMath.roundUp unpadded.val outer.val ≤ Usize.max then + some (LayoutMath.roundUp unpadded.val outer.val) else none at hr + simp only [show ¬outer.val = 0 by omega, if_false] at hr + have hround : LayoutMath.roundUp unpadded.val outer.val = target.1 := by + rw [husize, hv, htarget] + rw [hround] at hr + cases rounded with + | none => + have hn : ¬target.1 ≤ Usize.max := by split at hr <;> simp_all + simp only [WP.spec_ok, Option.map_none, hn, if_false] + | some rounded => + have ht : target.1 ≤ Usize.max := by split at hr <;> simp_all + have hsize : rounded.val = target.1 := by split at hr <;> simp_all + have halign : outer.val = target.2 := by rw [htarget] + simp only [WP.spec_ok, Option.map_some, nestedStateView, ht, if_true, hsize, halign, + Prod.mk.eta] + +/-- The reverse loop retains complete recursive size at every layer. Its +termination measure is the remaining layer index; no bound limits nesting. -/ +theorem nested_reference_loop_spec + (leading : Slice layout.nested_reference.NestedLayer) (elem alignment metadata : Nat) + (state : Option (Usize × Usize)) (i : Usize) + (hi : i.val ≤ leading.val.length) + (hv : state.map nestedStateView = + NestedReference.checkedState (leading.val.drop i.val) elem alignment metadata) : + layout.nested_reference.size_for_metadata_loop leading state i + ⦃ out => out.map nestedStateView = + NestedReference.checkedState leading.val elem alignment metadata ⦄ := by + unfold layout.nested_reference.size_for_metadata_loop + apply loop.spec_decr_nat + (fun s : Option (Usize × Usize) × Usize => s.2.val) + (fun s => s.2.val ≤ leading.val.length ∧ s.1.map nestedStateView = + NestedReference.checkedState (leading.val.drop s.2.val) elem alignment metadata) + · rintro ⟨state, idx⟩ ⟨hi, hv⟩ + dsimp only at hi hv ⊢ + unfold layout.nested_reference.size_for_metadata_loop.body + simp only [bne_iff_ne] + split + · rename_i hn + have hpos : 0 < idx.val := by + have hz : idx.val ≠ 0 := by + intro h + apply hn + exact UScalar.eq_of_val_eq (by simpa using h) + omega + step with Usize.sub_spec (x := idx) (y := 1#usize) (by simp; omega) + as ⟨j, hj, _⟩ + have hj' : j.val + 1 = idx.val := by + change j.val = idx.val - 1 at hj + omega + have hjlt : j.val < leading.val.length := by omega + step with Slice.index_usize_spec leading j hjlt as ⟨layer, hlayer⟩ + have hmath : NestedReference.checkedState (leading.val.drop j.val) elem alignment metadata = + NestedReference.checkedLayer layer + (NestedReference.checkedState (leading.val.drop idx.val) elem alignment metadata) := by + rw [NestedReference.checkedState_drop leading.val elem alignment metadata j.val hjlt, + hj', hlayer] + cases state with + | none => + have hnext : (none : Option (Usize × Usize)).map nestedStateView = + NestedReference.checkedState (leading.val.drop j.val) elem alignment metadata := by + rw [hmath, ← hv] + simp [NestedReference.checkedLayer] + apply WP.spec.ret + change j.val ≤ leading.val.length ∧ + (none : Option (Usize × Usize)).map nestedStateView = + NestedReference.checkedState (leading.val.drop j.val) elem alignment metadata ∧ + j.val < idx.val + exact ⟨by omega, hnext, by omega⟩ + | some pair => + have heq : NestedReference.checkedState (leading.val.drop idx.val) elem alignment metadata = + some (nestedStateView pair) := hv.symm + obtain ⟨hvalid, heq, hfit⟩ := NestedReference.checkedState_some + (leading.val.drop idx.val) elem alignment metadata (nestedStateView pair) heq + have hpower : pair.2.val.isPowerOfTwo := by + have hp := NestedReference.state_alignment_power (leading.val.drop idx.val) + elem alignment metadata hvalid + rw [heq] at hp + exact hp + rcases pair with ⟨size, currentAlignment⟩ + step with nested_apply_layer_spec layer size currentAlignment hpower as ⟨next, hnext⟩ + have hview : next.map nestedStateView = + NestedReference.checkedState (leading.val.drop j.val) elem alignment metadata := by + rw [hmath, ← hv] + exact hnext + exact ⟨by omega, hview, by omega⟩ + · rename_i hz + have heq : idx.val = 0 := by + have h : idx = 0#usize := not_ne_iff.mp hz + simpa using congrArg UScalar.val h + simpa only [heq, List.drop_zero, WP.spec_ok] using hv + · exact ⟨hi, hv⟩ + +theorem nested_reference_size_spec + (leading : Slice layout.nested_reference.NestedLayer) (elem_size leaf_align elems : Usize) : + layout.nested_reference.size_for_metadata leading elem_size leaf_align elems + ⦃ result => result.map UScalar.val = + NestedReference.checkedSize leading.val elem_size.val leaf_align.val elems.val ⦄ := by + classical + unfold layout.nested_reference.size_for_metadata + step as ⟨leafPow, hp⟩ + cases leafPow with + | false => + have hn : ¬leaf_align.val.isPowerOfTwo := by + intro h + have := (Iff.of_eq hp).mpr h + contradiction + have hv : ¬NestedReference.valid leading.val elem_size.val leaf_align.val := by + intro h + exact hn h.1 + simp only [Bool.false_eq_true, if_false, WP.spec_ok, Option.map_none, + NestedReference.checkedSize, hv, if_false] + | true => + have hpow : leaf_align.val.isPowerOfTwo := (Iff.of_eq hp).mp rfl + have hpos := Nat.pos_of_isPowerOfTwo hpow + simp only [if_true] + step with Usize.rem_spec elem_size (y := leaf_align) (by omega) as ⟨remainder, hr⟩ + simp only [bne_iff_ne] + split + · rename_i hn + have hv : ¬NestedReference.valid leading.val elem_size.val leaf_align.val := by + intro h + apply hn + exact UScalar.eq_of_val_eq (by simpa only [hr, show (0#usize).val = 0 by simp] using h.2.1) + simp only [WP.spec_ok, Option.map_none, NestedReference.checkedSize, hv, if_false] + · rename_i hz + have hzero : elem_size.val % leaf_align.val = 0 := by + have h : remainder = 0#usize := not_ne_iff.mp hz + simpa only [hr, show (0#usize).val = 0 by simp] using congrArg UScalar.val h + step as ⟨product, hproduct⟩ + let start : Option (Usize × Usize) := match product with + | none => none + | some size => some (size, leaf_align) + have hinit : start.map nestedStateView = + NestedReference.checkedState (leading.val.drop (Slice.len leading).val) + elem_size.val leaf_align.val elems.val := by + have hvalid : NestedReference.valid [] elem_size.val leaf_align.val := + ⟨hpow, hzero, by simp⟩ + simp only [Slice.len_val, Slice.length, List.drop_length, NestedReference.checkedState, + hvalid, if_true, NestedReference.state, List.foldr_nil] + cases product with + | none => + have hn : ¬elems.val * elem_size.val ≤ Usize.max := by + simp only [] at hproduct + rw [Nat.mul_comm] + omega + simp only [start, Option.map_none, hn, if_false] + | some size => + have hv : size.val = elems.val * elem_size.val := by + simp only [] at hproduct + rw [Nat.mul_comm] + exact hproduct.2.1 + have hf : elems.val * elem_size.val ≤ Usize.max := by + simp only [] at hproduct + rw [Nat.mul_comm] + exact hproduct.1 + simp only [start, Option.map_some, nestedStateView, hv, hf, if_true] + have hrun := nested_reference_loop_spec leading elem_size.val leaf_align.val elems.val + start (Slice.len leading) (by simp only [Slice.len_val, Slice.length, le_refl]) hinit + cases product <;> simp only [start] at hrun ⊢ + all_goals + step with hrun as ⟨out, hout⟩ + have hfinal : (match out with | none => none | some pair => some pair.1).map UScalar.val = + NestedReference.checkedSize leading.val elem_size.val leaf_align.val elems.val := by + rw [NestedReference.checkedSize_eq_map, ← hout] + cases out <;> rfl + cases out with + | none => simpa only [WP.spec_ok] using hfinal + | some pair => + rcases pair with ⟨size, alignment⟩ + dsimp only + exact WP.spec.ret hfinal + +end Zerocopy.Proofs.Raw + +namespace Zerocopy.Proofs +open AeneasSpecs + +theorem nested_round_up_spec : Zerocopy.Specs.nested_round_up_spec := by + intro size align sizeValue hsize alignValue halign + apply WP.spec_mono (Raw.nested_round_up_spec size align) + intro rounded hr + refine ⟨rounded.map unsignedWord, ?_, hr⟩ + cases rounded <;> rfl +register_spec_step nested_round_up_spec + +theorem nested_reference_size_spec : Zerocopy.Specs.nested_reference_size_spec := by + intro leading elem_size leaf_align elems layers hl elemValue he alignmentValue ha metadataValue hn + apply WP.spec_mono (Raw.nested_reference_size_spec leading elem_size leaf_align elems) + intro result hr + refine ⟨result.map unsignedWord, ?_, hr⟩ + cases result <;> rfl +register_spec_step nested_reference_size_spec + +/-- No descriptor bit pattern is excluded by a hidden decoder guard. -/ +theorem nested_layer_decoder_total (raw : layout.nested_reference.NestedLayer) : + isValid raw := by + refine ⟨⟨unsignedWord raw.packed, unsignedWord raw.min_align, + unsignedWord raw.prefix_bytes⟩, ?_⟩ + rfl + +theorem nested_layers_decoder_total (raw : Slice layout.nested_reference.NestedLayer) : + isValid raw := + (slice_valid_iff raw).mpr (fun layer _ => nested_layer_decoder_total layer) + +@[contract_simps] theorem nested_reference_size_spec_implies_required + (leading : Slice layout.nested_reference.NestedLayer) (elem_size leaf_align elems : Usize) + (run : Result (Option Usize)) + (provided : Zerocopy.Specs.nested_reference_size_spec_contract leading elem_size leaf_align elems run) : + Zerocopy.Obligations.nested_reference_size_spec_contract leading elem_size leaf_align elems run := by + obtain ⟨layers, hlayers⟩ := nested_layers_decoder_total leading + have execution := provided layers hlayers (unsignedWord elem_size) rfl + (unsignedWord leaf_align) rfl (unsignedWord elems) rfl + apply WP.spec_mono execution + rintro result ⟨decoded, hdecoded, hr⟩ + exact hr + +@[contract_simps] theorem nested_round_up_spec_implies_required + (size align : Usize) (run : Result (Option Usize)) + (provided : Zerocopy.Specs.nested_round_up_spec_contract size align run) : + Zerocopy.Obligations.nested_round_up_spec_contract size align run := by + have execution := provided (unsignedWord size) rfl (unsignedWord align) rfl + apply WP.spec_mono execution + rintro rounded ⟨decoded, hdecoded, hr⟩ + exact hr + +end Zerocopy.Proofs diff --git a/verification/aeneas/lean/Proofs/Util.lean b/verification/aeneas/lean/Proofs/Util.lean new file mode 100644 index 0000000000..b4556233db --- /dev/null +++ b/verification/aeneas/lean/Proofs/Util.lean @@ -0,0 +1,182 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Specs +import all Init.Data.Nat.Power2.Basic +@[expose] public section + +/-! +These four arithmetic helpers show the proof pattern used by the integration. +Raw lemmas unfold the actual extracted computation and prove precise numeric +facts. Canonical theorems then lift those facts to the generated inline +specs, including successful input and result decoding. + +The notation computation ⦃ result => property ⦄ means that execution +terminates successfully and its returned value satisfies property. Aeneas's +step tactic uses a registered callee specification, generating its +preconditions as proof goals. Pure scalar facts belong in Arithmetic; these +proofs connect those facts to the specific zerocopy implementations. +-/ +open Aeneas Aeneas.Std +namespace Zerocopy.Proofs.Raw + +-- Raw contract requirements intentionally retain their descriptive proof names. +set_option linter.unusedVariables false + +theorem padding_lt_alignment : + ∀ (len : Usize) (align : NonZeroUsize), ∀ (h : ((align.val : Nat).isPowerOfTwo : Prop)), + @Zerocopy.util.padding_needed_for len align ⦃ p => p < align.val ∧ + let L : Nat := len + let A : Nat := align.val + let P : Nat := p + P = (A - L % A) % A ∧ (L + P) % A = 0 ∧ + (∀ q : Nat, (L + q) % A = 0 → P ≤ q) ∧ (P = 0 ↔ L % A = 0) ⦄ := by + intro len align h + have hpos := Nat.pos_of_isPowerOfTwo h + unfold util.padding_needed_for + simp only [core.num.nonzero.NonZero.get, bind_ok] + step + simp only [lift, bind_ok, WP.spec_ok] + have hbound : (~~~(core.num.Usize.wrapping_sub len 1#usize) &&& mask).val + < align.val.val := by + have := Nat.and_le_right (n := (~~~(core.num.Usize.wrapping_sub len 1#usize)).val) + (m := mask.val) + rw [UScalar.val_and] + omega + have haligned := Arithmetic.padding_mask_aligned len align.val mask h (by omega) + obtain ⟨hexact, hminimal, hzero⟩ := + Arithmetic.padding_properties _ _ _ hpos hbound haligned + exact ⟨(UScalar.lt_equiv _ _).mpr hbound, hexact, haligned, hminimal, hzero⟩ + +theorem round_down_spec : + ∀ (n : Usize) (align : NonZeroUsize), ∀ (h : ((align.val : Nat).isPowerOfTwo : Prop)), + @Zerocopy.util.round_down_to_next_multiple_of_alignment n align ⦃ m => m ≤ n ∧ + let N : Nat := n + let A : Nat := align.val + let M : Nat := m + M = N - N % A ∧ M % A = 0 ∧ N < M + A ∧ + (∀ q : Nat, q ≤ N → q % A = 0 → q ≤ M) ⦄ := by + intro n align h + have hpos := Nat.pos_of_isPowerOfTwo h + unfold util.round_down_to_next_multiple_of_alignment + simp only [core.num.nonzero.NonZero.get, bind_ok] + step + step + step with UScalar.sub_bv_spec as ⟨mask, hval, hle, hbv⟩ + simp only [lift, bind_ok, WP.spec_ok] + have hexact := Arithmetic.round_down_exact n align.val mask h hval + obtain ⟨hbound, haligned, hnext, hgreatest⟩ := + Arithmetic.round_down_properties _ _ _ hpos hexact + exact ⟨(UScalar.le_equiv _ _).mpr hbound, hexact, haligned, hnext, hgreatest⟩ + +theorem max_spec : + ∀ (a b : NonZeroUsize), + @Zerocopy.util.max a b ⦃ r => r.val = max a.val b.val ∧ (r = a ∨ r = b) ∧ + a.val ≤ r.val ∧ b.val ≤ r.val ⦄ := by + intro a b + simp only [util.max, core.num.nonzero.NonZero.get, bind_ok] + split + · rename_i h + exact WP.spec.ret ⟨(max_eq_right (le_of_lt h)).symm, Or.inr rfl, + le_of_lt h, le_rfl⟩ + · rename_i h + exact WP.spec.ret ⟨(max_eq_left (le_of_not_gt h)).symm, Or.inl rfl, + le_rfl, le_of_not_gt h⟩ + +theorem min_spec : + ∀ (a b : NonZeroUsize), + @Zerocopy.util.min a b ⦃ r => r.val = min a.val b.val ∧ (r = a ∨ r = b) ∧ + r.val ≤ a.val ∧ r.val ≤ b.val ⦄ := by + intro a b + simp only [util.min, core.num.nonzero.NonZero.get, bind_ok] + split + · rename_i h + exact WP.spec.ret ⟨(min_eq_right (le_of_lt h)).symm, Or.inr rfl, + le_of_lt h, le_rfl⟩ + · rename_i h + exact WP.spec.ret ⟨(min_eq_left (le_of_not_gt h)).symm, Or.inl rfl, + le_rfl, le_of_not_gt h⟩ + +end Zerocopy.Proofs.Raw + + +namespace Zerocopy.Proofs +open AeneasSpecs + +theorem padding_lt_alignment : Zerocopy.Specs.padding_lt_alignment := by + intro len align lenValue hlen alignValue halign h + have hl := (decodeUScalar_iff len lenValue).mp hlen + have ha := (decodeNonZeroUScalar_iff align alignValue).mp halign + apply WP.spec_mono (Raw.padding_lt_alignment len align (by simpa only [ha] using h)) + intro p facts + refine ⟨unsignedWord p, rfl, ?_⟩ + dsimp only + rw [← hl, ← ha] + exact ⟨(UScalar.lt_equiv _ _).mp facts.1, facts.2⟩ +register_spec_step padding_lt_alignment + +theorem round_down_spec : Zerocopy.Specs.round_down_spec := by + intro n align nValue hn alignValue halign h + have hnv := (decodeUScalar_iff n nValue).mp hn + have ha := (decodeNonZeroUScalar_iff align alignValue).mp halign + apply WP.spec_mono (Raw.round_down_spec n align (by simpa only [ha] using h)) + intro m facts + refine ⟨unsignedWord m, rfl, ?_⟩ + dsimp only + rw [← hnv, ← ha] + exact ⟨(UScalar.le_equiv _ _).mp facts.1, facts.2⟩ +register_spec_step round_down_spec + +theorem max_spec : Zerocopy.Specs.max_spec := by + intro a b av ha bv hb + have hav := (decodeNonZeroUScalar_iff a av).mp ha + have hbv := (decodeNonZeroUScalar_iff b bv).mp hb + apply WP.spec_mono (Raw.max_spec a b) + intro r facts + rcases facts.2.1 with hr | hr + · subst r + refine ⟨av, ha, ?_⟩ + dsimp only + rw [← hav, ← hbv] + exact ⟨by simpa only [UScalar.coe_max] using congrArg UScalar.val facts.1, + Or.inl rfl, (UScalar.le_equiv _ _).mp facts.2.2.1, + (UScalar.le_equiv _ _).mp facts.2.2.2⟩ + · subst r + refine ⟨bv, hb, ?_⟩ + dsimp only + rw [← hav, ← hbv] + exact ⟨by simpa only [UScalar.coe_max] using congrArg UScalar.val facts.1, + Or.inr rfl, (UScalar.le_equiv _ _).mp facts.2.2.1, + (UScalar.le_equiv _ _).mp facts.2.2.2⟩ +register_spec_step max_spec + +theorem min_spec : Zerocopy.Specs.min_spec := by + intro a b av ha bv hb + have hav := (decodeNonZeroUScalar_iff a av).mp ha + have hbv := (decodeNonZeroUScalar_iff b bv).mp hb + apply WP.spec_mono (Raw.min_spec a b) + intro r facts + rcases facts.2.1 with hr | hr + · subst r + refine ⟨av, ha, ?_⟩ + dsimp only + rw [← hav, ← hbv] + exact ⟨(Nat.min_eq_left ((UScalar.le_equiv _ _).mp facts.2.2.2)).symm, + Or.inl rfl, (UScalar.le_equiv _ _).mp facts.2.2.1, + (UScalar.le_equiv _ _).mp facts.2.2.2⟩ + · subst r + refine ⟨bv, hb, ?_⟩ + dsimp only + rw [← hav, ← hbv] + exact ⟨(Nat.min_eq_right ((UScalar.le_equiv _ _).mp facts.2.2.1)).symm, + Or.inr rfl, (UScalar.le_equiv _ _).mp facts.2.2.1, + (UScalar.le_equiv _ _).mp facts.2.2.2⟩ +register_spec_step min_spec + +end Zerocopy.Proofs diff --git a/verification/aeneas/lean/RepresentationLaws.lean b/verification/aeneas/lean/RepresentationLaws.lean new file mode 100644 index 0000000000..3c110a6e11 --- /dev/null +++ b/verification/aeneas/lean/RepresentationLaws.lean @@ -0,0 +1,71 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import MathViews +public import ModelSupport +@[expose] public section + +/-! +Rounding operation proofs need to relate an observed pair to its stored word. +The lemmas here expose that particular decoder's equations and positive +domain. They are useful because this encoding is reversible; the integration +does not require every model to be reversible or to retain every +representation detail. A coarse model can be appropriate when its operation +contracts need no more. Decoder construction proves the model's constraints, +while these lemmas describe what this decoder actually computes. +-/ +open Aeneas Aeneas.Std AeneasSpecs +namespace Zerocopy.Proofs + +/-- The stored word represents exactly the sum of the observed pair. -/ +def roundingRepresents (raw : layout.RoundingAlignAndPhase) + (value : layout.RoundingAlignAndPhase.RoundingValue) : Prop := + raw._0.val.val = value.align + value.phase + +theorem rounding_decode_positive (raw : layout.RoundingAlignAndPhase) + (positive : 0 < raw._0.val.val) : + ∃ value, layout.RoundingAlignAndPhase.decode raw = some value := + (encoding_valid_iff raw).mpr positive + +theorem rounding_decode_components (raw : layout.RoundingAlignAndPhase) + (value : layout.RoundingAlignAndPhase.RoundingValue) + (h : layout.RoundingAlignAndPhase.decode raw = some value) : + value.align = 2 ^ Nat.log2 raw._0.val.val ∧ + value.phase = raw._0.val.val - 2 ^ Nat.log2 raw._0.val.val := by + have positive : 0 < raw._0.val.val := + (encoding_valid_iff raw).mp ⟨value, h⟩ + simp [layout.RoundingAlignAndPhase.decode, layout.RoundingAlignAndPhase.decodeFields, + RustModel.decode, positive, unsignedWord] at h + cases h + exact ⟨rfl, rfl⟩ + +theorem rounding_decode_iff (raw : layout.RoundingAlignAndPhase) + (value : layout.RoundingAlignAndPhase.RoundingValue) : + layout.RoundingAlignAndPhase.decode raw = some value ↔ roundingRepresents raw value := by + constructor + · intro h + obtain ⟨ha, hp⟩ := rounding_decode_components raw value h + have positive := (encoding_valid_iff raw).mp ⟨value, h⟩ + unfold roundingRepresents + rw [ha, hp] + exact (RoundingFacts.reconstruction _ positive).symm + · intro h + have positive : 0 < raw._0.val.val := by + have := Nat.pos_of_isPowerOfTwo value.align_pow2 + unfold roundingRepresents at h + omega + obtain ⟨actual, decoded⟩ := rounding_decode_positive raw positive + obtain ⟨ha, hp⟩ := rounding_decode_components raw actual decoded + have unique := RoundingFacts.components_unique raw._0.val.val value.align value.phase + value.align_pow2 value.phase_lt h + have equal : actual = value := RoundingFacts.value_ext + (ha.trans unique.1.symm) (hp.trans unique.2.symm) + simpa only [equal] using decoded + +end Zerocopy.Proofs diff --git a/verification/aeneas/lean/RequiredContractTests.lean b/verification/aeneas/lean/RequiredContractTests.lean new file mode 100644 index 0000000000..e995a55bb2 --- /dev/null +++ b/verification/aeneas/lean/RequiredContractTests.lean @@ -0,0 +1,112 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import RequiredContracts +public import MathViews +@[expose] public section + +/-! +Adequacy must hold for arbitrary outcomes, even when both concrete propositions +are true. These tests supply wrong returns and divergence to distinguish weak +postconditions, partial correctness, and empty ghost domains from the promised +total behavior. A final polymorphic example checks that specialization preserves +original implicit type arguments and their model dictionaries. +-/ + +open Aeneas Aeneas.Std AeneasSpecs AeneasContracts +namespace RequiredContractTests +set_option linter.unusedSimpArgs false + +def identity (x : Nat) : Result Nat := .ok x + +-- The independent family uses only the supplied outcome. Its concrete alias +-- specializes to the operation, whose implementation satisfies this promise. +def expected_contract (x : Nat) (run : Result Nat) : Prop := + WP.spec run (fun output => output = x) +def expected : Prop := ∀ x : Nat, expected_contract x (identity x) +theorem identity_correct : expected := by + intro x + exact WP.spec.ret rfl + +spec strict for identity with 0 type parameters + ensures output => output = x +spec weak for identity with 0 type parameters + ensures output => True +theorem weak_correct : weak := by + simp [weak, weak_contract, identity, RustModel.decode, modelNat] + +-- Both concrete propositions are true, but the weak post permits a wrong +-- arbitrary outcome. A second proof of identity cannot supply adequacy. +theorem weak_post_rejected : ¬ (contract_adequacy% weak implies expected) := by + intro adequate + have accepted : weak_contract 0 (.ok 1) := by + simp [weak_contract, RustModel.decode, modelNat] + have wrong := adequate 0 (.ok 1) accepted + simp [expected_contract] at wrong + +partial spec partial_identity for identity with 0 type parameters + ensures output => output = x +theorem partial_total_rejected : + ¬ (contract_adequacy% partial_identity implies expected) := by + intro adequate + have accepted : partial_identity_contract 0 .div := by + intro math decoded + exact WP.dspec.div _ + have wrong := adequate 0 .div accepted + simp [expected_contract] at wrong + +spec false_ghost (impossible : False) for identity with 0 type parameters + ensures output => output = x +theorem false_ghost_rejected : + ¬ (contract_adequacy% false_ghost implies expected) := by + intro adequate + have accepted : false_ghost_contract 0 (.ok 1) := by + intro math decoded impossible + exact impossible.elim + have wrong := adequate 0 (.ok 1) accepted + simp [expected_contract] at wrong + +spec fin_zero_ghost (position : Fin 0) for identity with 0 type parameters + ensures output => output = x +theorem fin_zero_ghost_rejected : + ¬ (contract_adequacy% fin_zero_ghost implies expected) := by + intro adequate + have accepted : fin_zero_ghost_contract 0 (.ok 1) := by + intro math decoded position + exact Fin.elim0 position + have wrong := adequate 0 (.ok 1) accepted + simp [expected_contract] at wrong + +-- A true concrete alias must still specialize its own independent family. +def mismatched_contract (x : Nat) (run : Result Nat) : Prop := + WP.spec run (fun output => output = x + 1) +def mismatched : Prop := ∀ x : Nat, expected_contract x (identity x) +example : mismatched := identity_correct +/-- error: Independent proposition RequiredContractTests.mismatched does not specialize its arbitrary-outcome family -/ +#guard_msgs in +#check (contract_adequacy% strict implies mismatched) + +end RequiredContractTests + +namespace GenericUniverseTest +universe u +-- Preserve both universe polymorphism and implicit original type arguments. +def identity {T : Type u} (x : T) : Result T := .ok x +spec identity_spec for @identity with 1 type parameters + ensures(raw) r => r = x +theorem identity_proof : identity_spec := by + intro T x provider math decoded + exact ⟨math, decoded, rfl⟩ +def identity_required_contract (T : Type u) (x : T) (run : Result T) : Prop := + ∀ [_m : RustModel T], isValid x → + WP.spec run (fun r => isValid r ∧ r = x) +def identity_required : Prop := ∀ (T : Type u) (x : T), + identity_required_contract T x (@identity T x) +check_contract identity_required using identity_proof +end GenericUniverseTest diff --git a/verification/aeneas/lean/RequiredContracts.lean b/verification/aeneas/lean/RequiredContracts.lean new file mode 100644 index 0000000000..b5affbee7a --- /dev/null +++ b/verification/aeneas/lean/RequiredContracts.lean @@ -0,0 +1,198 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import SpecsSyntax +public import ContractSimps +public import Mathlib.Tactic.Convert +public section + +/-! +A proved specification can still state too little. For example, a postcondition +`True` can be proved for identity but also permits identity to return the wrong +value. This module checks adequacy by replacing the actual execution with an +arbitrary `Result` and proving that the authored contract implies a separately +authored expectation for that same outcome. + +Only after that implication is checked do we specialize it to the extracted +call and apply the canonical proof. The audits require the expected family to +use its outcome and forbid dependencies on the extracted operation, including +through helper declarations. Its concrete alias must also specialize that same +family. These checks prevent an adapter from bypassing the authored contract by +independently proving the implementation again. +-/ + +open Lean Lean.Meta Aeneas Aeneas.Std + +namespace AeneasContracts + +attribute [contract_simps] WP.spec_equiv_exists UScalar.eq_equiv + UScalar.coe_max UScalar.lt_equiv UScalar.le_equiv + +/-- A required check starts from a canonical proof of a named specification. +Accepting arbitrary terms here would let a check ignore the inline contract. -/ +meta def canonicalSpecName (candidate : Name) : MetaM Name := do + let .thmInfo proof ← getConstInfo candidate + | throwError "Required contract candidate must be a theorem: {candidate}" + let .const specName _ := proof.type.consumeMData + | throwError "Required contract candidate must directly name a specification: {candidate}" + unless AeneasSpecs.specAttribute.hasTag (← getEnv) specName do + throwError "Required contract candidate must directly name a specification: {candidate}" + pure specName + +/-- An independent family must describe the supplied outcome, rather than +smuggling the concrete call back into the expectation through a helper. -/ +private meta def checkIndependentFamily (familyName modelName : Name) + (count : Nat) : MetaM Unit := do + let info ← getConstInfo familyName + let some value := info.value? | throwError "Independent contract must be transparent: {familyName}" + -- Open the function type to get the actual input/outcome variables, then + -- reduce its body enough to reject a syntactically present but ignored run. + forallTelescope info.type fun inputs _ => do + unless inputs.size == count + 1 do + throwError "Independent contract {familyName} changed its input and outcome prefix" + let body ← whnf (mkAppN value inputs) + unless body.containsFVar inputs[count]!.fvarId! do + throwError "Independent contract {familyName} must use its execution outcome" + -- Walk types as well as bodies. An opaque helper can mention the extracted + -- operation only in its type; inspecting reduced bodies would miss that + -- dependency and admit a family with an implementation-specific premise. + let mut pending := info.type.getUsedConstants ++ value.getUsedConstants + let mut seen : NameHashSet := {} + while !pending.isEmpty do + let current := pending.back! + pending := pending.pop + if current == modelName then + throwError "Independent contract {familyName} depends on its extracted operation" + if seen.contains current then continue + seen := seen.insert current + let dependency ← getConstInfo current + pending := pending ++ dependency.type.getUsedConstants + if let some body := dependency.value? (allowOpaque := true) then + pending := pending ++ body.getUsedConstants + +/-- Compare the contract for every possible execution outcome. The independent +family must also specialize to its independently authored concrete proposition. +No extracted function body is available as a premise of this implication. -/ +meta def contractAdequacyType (specName expectedName : Name) : MetaM Expr := do + AeneasSpecs.checkSpecContract specName + let (modelName, count) ← AeneasSpecs.specBinding specName + checkIndependentFamily (expectedName.appendAfter "_contract") modelName count + let family ← mkConstWithFreshMVarLevels (AeneasSpecs.specContractName specName) + forallTelescope (← inferType family) fun inputs resultType => do + unless inputs.size == count + 1 && resultType.consumeMData == mkSort .zero do + throwError "Required comparison has an unexpected contract family type" + let expectedAt ← mkAppM (expectedName.appendAfter "_contract") inputs + let expectedFamily := expectedAt.getAppFn + unless ← isDefEq (← inferType expectedAt) (mkSort .zero) do + throwError "Independent contract family must have the same input and outcome types" + -- Quantify over arbitrary run, then form AuthoredAt -> RequiredAt. The + -- actual extracted call is introduced only in the specialization check + -- below, so it cannot prove away a weak post or an accidentally empty domain. + let implication ← mkArrow (mkAppN family inputs) expectedAt + let adequacy ← mkForallFVars inputs implication + -- The concrete expectation and its family must be two views of the same + -- promise. Otherwise a true alias could conceal a false expected family. + let .defnInfo source ← getConstInfo specName + | throwError "Expected a transparent specification" + forallTelescope (source.value.instantiateLevelParams source.levelParams family.constLevels!) fun sourceInputs body => do + let call := body.consumeMData.getAppArgs[1]! + let originalInputs := sourceInputs.extract 0 count + let concrete := mkAppN expectedFamily (originalInputs.push call) + let specialized ← mkForallFVars originalInputs concrete + unless ← isDefEq (← mkConstWithFreshMVarLevels expectedName) specialized do + throwError "Independent proposition {expectedName} does not specialize its arbitrary-outcome family" + instantiateMVars adequacy + +elab "contract_adequacy% " source:ident " implies " expected:ident : term => do + let specName ← Elab.realizeGlobalConstNoOverloadWithInfo source + let expectedName ← Elab.realizeGlobalConstNoOverloadWithInfo expected + contractAdequacyType specName expectedName + +-- Only after adequacy is proved may its abstract outcome be specialized to +-- the actual call and supplied canonical proof. +elab "required_contract_instance% " candidate:ident " with " adequate:ident : term => do + let proofName ← Elab.realizeGlobalConstNoOverloadWithInfo candidate + let adequateName ← Elab.realizeGlobalConstNoOverloadWithInfo adequate + let specName ← canonicalSpecName proofName + let (modelName, count) ← AeneasSpecs.specBinding specName + let family ← mkConstWithFreshMVarLevels (AeneasSpecs.specContractName specName) + forallTelescope (← inferType family) fun inputs _ => do + let originals := inputs.extract 0 count + -- The audited prefix includes implicit Rust type arguments. Apply it + -- literally; mkAppM would instead insert them and treat the first supplied + -- type as a value argument for an implicit-generic extracted operation. + let call ← mkAppOptM modelName (originals.map some) + let proof ← mkAppOptM proofName (originals.map some) + let value ← mkAppOptM adequateName ((originals ++ #[call, proof]).map some) + let _ ← inferType value + mkLambdaFVars originals (← instantiateMVars value) + +/-- Generate a kernel-checked implication about arbitrary outcomes, then apply +it to the proved implementation. Normalization can use a domain-specific lemma, +but it cannot substitute a second proof of the concrete implementation. -/ +elab "check_contract " expected:ident " using " candidate:term : command => do + let candidateId ← match candidate with + | `(@$id:ident) => pure id + | `($id:ident) => pure id + | _ => throwErrorAt candidate "Required contract candidate must be a bare canonical theorem" + let (specName, count, dictionaryCount) ← Elab.Command.liftTermElabM do + let proofName ← Elab.realizeGlobalConstNoOverloadWithInfo candidateId + let specName ← canonicalSpecName proofName + let (_, count) ← AeneasSpecs.specBinding specName + let .defnInfo source ← getConstInfo specName + | throwError "Expected a transparent specification" + let dictionaryCount ← forallTelescope source.value fun inputs _ => do + let mut dictionaries := 0 + for input in inputs[:count] do + if (← whnf (← inferType input)).isSort then + dictionaries := dictionaries + 1 + pure dictionaries + pure (specName, count, dictionaryCount) + let source := mkIdentFrom candidateId specName + let family := mkIdentFrom expected (expected.getId.appendAfter "_contract") + let sourceFamily := mkIdentFrom candidateId (AeneasSpecs.specContractName specName) + let adequate := mkIdentFrom expected (expected.getId.appendAfter "_adequate") + let witness := mkIdentFrom expected (expected.getId.appendAfter "_checked") + let arguments := (List.range (count + 1)).toArray.map + (fun i => mkIdent (Name.mkSimple s!"_contractInput{i}")) + let dictionaries := (List.range dictionaryCount).toArray.map + (fun i => mkIdent (Name.mkSimple s!"_contractDictionary{i}")) + -- Source type parameters contribute original model dictionaries after run. + -- Specialize the authored premise with those same dictionaries before proving + -- the independent implication; ghost instances cannot select new meanings. + let introduceDictionaries ← if dictionaries.isEmpty then `(tactic| skip) else + `(tactic| (intro $dictionaries:ident*; specialize @provided $dictionaries:ident*)) + -- These tactics produce an ordinary theorem, not a trusted comparison + -- result. They normalize known decoder/word facts and align forall/exists + -- witnesses. If the implication is false or needs a new lemma, elaboration + -- fails rather than falling back to a proof of the concrete operation. + Elab.Command.elabCommand (← `(theorem $adequate : + contract_adequacy% $source implies $expected := by + intro $arguments:ident* provided + $introduceDictionaries:tactic + first + | exact provided + | (simp (disch := assumption) only [contract_simps]; done) + | try unfold $family + try unfold $sourceFamily at provided + convert provided using 1 <;> + (simp only [WP.spec_equiv_exists] + all_goals simp (config := { index := false, contextual := true }) only [contract_simps]) + all_goals + (congr! 2 + all_goals first | rfl | (split <;> simp_all only [contract_simps]) | skip) + all_goals + (congr! 4 + all_goals first | rfl | (split <;> simp_all only [contract_simps]) | grind) + all_goals rfl)) + if ← MonadLog.hasErrors then return + Elab.Command.elabCommand (← `(theorem $witness : $expected := + required_contract_instance% $candidateId with $adequate)) + +end AeneasContracts diff --git a/verification/aeneas/lean/RequiredModelContracts.lean b/verification/aeneas/lean/RequiredModelContracts.lean new file mode 100644 index 0000000000..a3051dee3c --- /dev/null +++ b/verification/aeneas/lean/RequiredModelContracts.lean @@ -0,0 +1,84 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import RequiredModelContracts.Util +public import RepresentationLaws +@[expose] public section + +/-! +These adapters prove that mathematical inline promises retain independently +stated raw behavior and input coverage. Each receives provided, a proof about +an arbitrary run, and derives the matching Obligations proposition for that +same run. It never executes or independently proves the function to bypass a +weak authored spec. + +Constructing mathematical input witnesses from raw premises checks the +promised domain. Recovering raw facts from the decoded result checks the +promised observations. The contract_simps attribute makes these ordinary +proved implications available to check_contract's bounded normalization. +-/ +open Aeneas Aeneas.Std AeneasSpecs +namespace Zerocopy.Proofs + +attribute [contract_simps] encoding_decoder_admitted_iff + +@[contract_simps] theorem required_encoding_new (align : NonZeroUsize) (phase : Usize) + (run : Result layout.RoundingAlignAndPhase) + (provided : Specs.encoding_new_spec_contract align phase run) : + Obligations.encoding_new_spec_contract align phase run := by + intro positive pow2 below + let av : NonZeroUsizeValue := ⟨unsignedWord align.val, positive⟩ + have admitted := (decodeNonZeroUScalar_iff align av).mpr rfl + have result := provided av admitted (unsignedWord phase) rfl pow2 below + simp only [WP.spec_equiv_exists] at result + obtain ⟨raw, call, value, decoded, ha, hp⟩ := result + refine ⟨raw, call, ?_, ?_⟩ + · exact (encoding_valid_iff raw).mp ⟨value, decoded⟩ + · have represents := (rounding_decode_iff raw value).mp decoded + simpa only [roundingRepresents, ha, hp, av, unsignedWord] using represents + +@[contract_simps] theorem required_encoding_align (raw : layout.RoundingAlignAndPhase) + (run : Result NonZeroUsize) + (provided : Specs.encoding_align_spec_contract raw run) : + Obligations.encoding_align_spec_contract raw run := by + intro positive + obtain ⟨value, decoded⟩ := rounding_decode_positive raw positive + have result := provided value decoded + simp only [WP.spec_equiv_exists] at result + obtain ⟨align, call, av, admitted, facts⟩ := result + have same := (decodeNonZeroUScalar_iff align av).mp admitted + exact ⟨align, call, by rw [same]; exact av.positive, + same.trans (facts.trans (rounding_decode_components raw value decoded).1)⟩ + +@[contract_simps] theorem required_encoding_components (raw : layout.RoundingAlignAndPhase) + (run : Result (NonZeroUsize × Usize)) + (provided : Specs.encoding_components_spec_contract raw run) : + Obligations.encoding_components_spec_contract raw run := by + intro positive + obtain ⟨value, decoded⟩ := rounding_decode_positive raw positive + apply WP.spec_mono (provided value decoded) + rintro ⟨align, phase⟩ ⟨pair, admitted, ha, hp⟩ + have positiveAlign : 0 < align.val.val := + (nonzero_valid_iff align).mp ((prod_valid_iff (align, phase)).mp ⟨pair, admitted⟩).1 + have pairEq : ((⟨unsignedWord align.val, positiveAlign⟩ : NonZeroUsizeValue), + unsignedWord phase) = pair := by + simpa only [RustModel.decode, dif_pos positiveAlign, Option.some.injEq] using admitted + cases pairEq + dsimp only [unsignedWord] at ha hp + have hraw := (rounding_decode_iff raw value).mp decoded + have hcanonical := rounding_decode_components raw value decoded + change 0 < align.val.val ∧ align.val.val.isPowerOfTwo ∧ + phase.val < align.val.val ∧ align.val.val + phase.val = raw._0.val.val ∧ + align.val.val = 2 ^ raw._0.val.val.log2 ∧ + phase.val = raw._0.val.val - 2 ^ raw._0.val.val.log2 + rw [ha, hp] + exact ⟨Nat.pos_of_isPowerOfTwo value.align_pow2, value.align_pow2, value.phase_lt, + hraw.symm, hcanonical.1, hcanonical.2⟩ + +end Zerocopy.Proofs diff --git a/verification/aeneas/lean/RequiredModelContracts/Util.lean b/verification/aeneas/lean/RequiredModelContracts/Util.lean new file mode 100644 index 0000000000..ee8f971efc --- /dev/null +++ b/verification/aeneas/lean/RequiredModelContracts/Util.lean @@ -0,0 +1,117 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Obligations +public import Specs +public import MathViews +@[expose] public section + +/-! +The arithmetic inline specs use bounded mathematical words; the independent +expectations retain raw machine-word observations. These adapters bridge +those descriptions for an arbitrary execution outcome, using only the +supplied spec. They must construct decoded inputs from the independent raw +domain and recover raw output facts from successful decoding. Proving the +implementation again would not establish that the inline promise itself is +strong enough. +-/ +open Aeneas Aeneas.Std AeneasSpecs +namespace Zerocopy.Proofs + +attribute [contract_simps] option_admitted_explicit_iff admitted_some admitted_none + admitted_dite + +-- These implications consume a supplied model contract and recover the +-- separately authored representation observations. They do not prove or +-- select a function contract themselves. +@[contract_simps] theorem required_padding (len : Usize) (align : NonZeroUsize) + (run : Result Usize) (provided : Specs.padding_lt_alignment_contract len align run) : + Obligations.padding_lt_alignment_contract len align run := by + intro positive pow2 + let av : NonZeroUsizeValue := ⟨unsignedWord align.val, positive⟩ + have admitted := (decodeNonZeroUScalar_iff align av).mpr rfl + apply WP.spec_mono (provided (unsignedWord len) rfl av admitted pow2) + rintro p ⟨value, decoded, facts⟩ + have same := (decodeUScalar_iff p value).mp decoded + simpa only [unsignedWord, av, ← same] using facts + +@[contract_simps] theorem required_round_down (n : Usize) (align : NonZeroUsize) + (run : Result Usize) (provided : Specs.round_down_spec_contract n align run) : + Obligations.round_down_spec_contract n align run := by + intro positive pow2 + let av : NonZeroUsizeValue := ⟨unsignedWord align.val, positive⟩ + have admitted := (decodeNonZeroUScalar_iff align av).mpr rfl + apply WP.spec_mono (provided (unsignedWord n) rfl av admitted pow2) + rintro output ⟨value, decoded, facts⟩ + have same := (decodeUScalar_iff output value).mp decoded + simpa only [unsignedWord, av, ← same] using facts + +theorem nonzero_raw_eq_of_value_eq (a b : NonZeroUsize) + (same : a.val.val = b.val.val) : a = b := by + have hs := UScalar.eq_of_val_eq same + cases a; cases b + congr + +@[contract_simps] theorem required_max (a b : NonZeroUsize) (run : Result NonZeroUsize) + (provided : Specs.max_spec_contract a b run) : + Obligations.max_spec_contract a b run := by + intro pa pb + let av : NonZeroUsizeValue := ⟨unsignedWord a.val, pa⟩ + let bv : NonZeroUsizeValue := ⟨unsignedWord b.val, pb⟩ + have da := (decodeNonZeroUScalar_iff a av).mpr rfl + have db := (decodeNonZeroUScalar_iff b bv).mpr rfl + have result := provided av da bv db + simp only [WP.spec_equiv_exists] at result + obtain ⟨r, call, value, decoded, facts⟩ := result + have same := (decodeNonZeroUScalar_iff r value).mp decoded + refine ⟨r, call, ?_, ?_, ?_, ?_, ?_⟩ + · rw [same]; exact value.positive + · simpa only [av, bv, unsignedWord, ← same] using facts.1 + · rcases facts.2.1 with h | h + · left; apply nonzero_raw_eq_of_value_eq; simp only [same, h, av, unsignedWord] + · right; apply nonzero_raw_eq_of_value_eq; simp only [same, h, bv, unsignedWord] + · simpa only [av, unsignedWord, ← same] using facts.2.2.1 + · simpa only [bv, unsignedWord, ← same] using facts.2.2.2 + +@[contract_simps] theorem required_min (a b : NonZeroUsize) (run : Result NonZeroUsize) + (provided : Specs.min_spec_contract a b run) : + Obligations.min_spec_contract a b run := by + intro pa pb + let av : NonZeroUsizeValue := ⟨unsignedWord a.val, pa⟩ + let bv : NonZeroUsizeValue := ⟨unsignedWord b.val, pb⟩ + have da := (decodeNonZeroUScalar_iff a av).mpr rfl + have db := (decodeNonZeroUScalar_iff b bv).mpr rfl + have result := provided av da bv db + simp only [WP.spec_equiv_exists] at result + obtain ⟨r, call, value, decoded, facts⟩ := result + have same := (decodeNonZeroUScalar_iff r value).mp decoded + refine ⟨r, call, ?_, ?_, ?_, ?_, ?_⟩ + · rw [same]; exact value.positive + · simpa only [av, bv, unsignedWord, ← same] using facts.1 + · rcases facts.2.1 with h | h + · left; apply nonzero_raw_eq_of_value_eq; simp only [same, h, av, unsignedWord] + · right; apply nonzero_raw_eq_of_value_eq; simp only [same, h, bv, unsignedWord] + · simpa only [av, unsignedWord, ← same] using facts.2.2.1 + · simpa only [bv, unsignedWord, ← same] using facts.2.2.2 + +end Zerocopy.Proofs + +namespace Zerocopy.Proofs + +@[contract_simps] theorem required_arithmetic_checks (len : Usize) (align : NonZeroUsize) + (run : Result Unit) (provided : Specs.arithmetic_checks_spec_contract len align run) : + Obligations.arithmetic_checks_spec_contract len align run := by + intro positive + let av : NonZeroUsizeValue := ⟨unsignedWord align.val, positive⟩ + have admitted := (decodeNonZeroUScalar_iff align av).mpr rfl + apply WP.spec_mono (provided (unsignedWord len) rfl av admitted) + intro _ _ + trivial + +end Zerocopy.Proofs diff --git a/verification/aeneas/lean/RustModel.lean b/verification/aeneas/lean/RustModel.lean new file mode 100644 index 0000000000..8db51158bd --- /dev/null +++ b/verification/aeneas/lean/RustModel.lean @@ -0,0 +1,131 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Aeneas +public meta import Lean +@[expose] public section + +/-! +A `RustModel` interprets a raw Rust representation by either accepting it with a +mathematical value (`some`) or rejecting it (`none`). That `Option` is decoder +admission, separate from both a Rust `Option` payload and Aeneas execution +failure. There is no requirement to reconstruct a raw value from its model or +to preserve information that the operation contracts do not observe. + +The builtin container models recursively decode their contents. One rejected +child rejects its container. Slices and arrays retain their length constraints +in proof fields; list decoding preserves length, so those constraints can be +transferred from the raw representation without a second runtime check. +-/ + +open Lean Lean.Meta Aeneas.Std +namespace AeneasSpecs + +/-- The provider fixes both the mathematical type and the decoder. Two providers +may share a Model type while assigning different meanings or admission domains +to the same raw value, so contracts retain the actual provider explicitly. -/ +class RustModel (Raw : Type u) where + Model : Type u + decode : Raw → Option Model + +-- The associated type is transparent so ordinary rewriting and coercions can +-- identify a retained provider's Model with its named mathematical carrier. +attribute [reducible] RustModel.Model + +abbrev ModelOf (Raw : Type u) [m : RustModel Raw] : Type u := m.Model + +/-- Admission means that some mathematical witness exists. It does not require +a canonical witness, an encoder, or a separately authored validity predicate. -/ +def isValid {Raw : Type u} [m : RustModel Raw] (raw : Raw) : Prop := + ∃ value, m.decode raw = some value + +/-- Automatic contracts call this with their retained explicit provider. -/ +def decoded {Raw : Type u} (m : RustModel Raw) (raw : Raw) (value : m.Model) : Prop := + m.decode raw = some value + +@[reducible] instance modelBool : RustModel Bool := ⟨Bool, some⟩ +@[reducible] instance modelNat : RustModel Nat := ⟨Nat, some⟩ +@[reducible] instance modelInt : RustModel Int := ⟨Int, some⟩ +@[reducible] instance modelUnit : RustModel Unit := ⟨Unit, some⟩ + +@[reducible] instance modelProd {α : Type u} {β : Type v} [ma : RustModel α] [mb : RustModel β] : + RustModel (α × β) where + Model := ma.Model × mb.Model + decode raw := match ma.decode raw.1, mb.decode raw.2 with + | some a, some b => some (a, b) + | _, _ => none + +-- Raw none is an admitted mathematical none. A raw some needs child decoding; +-- a rejected child returns decoder none, not an admitted Option payload. +@[reducible] instance modelOption {α : Type u} [ma : RustModel α] : RustModel (Option α) where + Model := Option ma.Model + decode | none => some none | some raw => (ma.decode raw).map some + +def decodeList {α β : Type u} (decode : α → Option β) : List α → Option (List β) + | [] => some [] + | raw :: raws => do return (← decode raw) :: (← decodeList decode raws) + +/-- Each successful list step contributes exactly one output element. This +length equality transports the existing slice/array proof to the decoded list. -/ +theorem decodeList_length {α β : Type u} (decode : α → Option β) + {raws : List α} {values : List β} (h : decodeList decode raws = some values) : + values.length = raws.length := by + induction raws generalizing values with + | nil => simp [decodeList] at h; subst values; rfl + | cons raw raws ih => + simp only [decodeList] at h + cases hd : decode raw <;> simp [hd] at h + rename_i value + cases hs : decodeList decode raws <;> simp [hs] at h + rename_i tail + subst values + simp [ih hs] + +@[reducible] instance modelList {α : Type u} [ma : RustModel α] : RustModel (List α) := + ⟨List ma.Model, decodeList ma.decode⟩ + +/-- Mathematical slice shape; its length bound needs no child decoder. -/ +abbrev SliceValue (Element : Type u) := + { values : List Element // values.length ≤ Usize.max } + +/-- Mathematical array shape; its fixed length needs no child decoder. -/ +abbrev ArrayValue (Element : Type u) (n : Usize) := + { values : List Element // values.length = n.val } + +@[reducible] instance modelSlice {α : Type u} [ma : RustModel α] : RustModel (Slice α) where + Model := SliceValue ma.Model + decode raw := match h : decodeList ma.decode raw.val with + | none => none + | some values => some ⟨values, (decodeList_length ma.decode h) ▸ raw.property⟩ + +@[reducible] instance modelArray {α : Type u} (n : Usize) [ma : RustModel α] : + RustModel (Aeneas.Std.Array α n) where + Model := ArrayValue ma.Model n + decode raw := match h : decodeList ma.decode raw.val with + | none => none + | some values => some ⟨values, (decodeList_length ma.decode h) ▸ raw.property⟩ + +-- This is Rust's Ok/Err payload type. Either constructor is admitted exactly +-- when its selected child decodes, independently of Aeneas execution Result. +@[reducible] instance modelRustResult {α : Type u} {β : Type v} [ma : RustModel α] [mb : RustModel β] : + RustModel (core.result.Result α β) where + Model := core.result.Result ma.Model mb.Model + decode | .Ok raw => (ma.decode raw).map .Ok | .Err raw => (mb.decode raw).map .Err + +/-- Reject borrow continuations even when nested inside supported containers. -/ +meta partial def rejectEscapingFunctions (carrierType : Expr) : MetaM Unit := do + let carrierType ← whnf carrierType + if carrierType.isForall then + throwError "Unsupported escaping function or backward reconstruction in model carrier" + -- Only type-valued arguments can hide another carrier. Values such as an + -- array's fixed length are not recursively treated as modelable types. + for arg in carrierType.getAppArgs do + if (← whnf (← inferType arg)).isSort then rejectEscapingFunctions arg + +end AeneasSpecs diff --git a/verification/aeneas/lean/SpecPrelude.lean b/verification/aeneas/lean/SpecPrelude.lean new file mode 100644 index 0000000000..9d3f5c928b --- /dev/null +++ b/verification/aeneas/lean/SpecPrelude.lean @@ -0,0 +1,24 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Zerocopy.Types +@[expose] public section + +/-! +This small prelude gives handwritten proofs the extracted type declarations +and the exact native nonzero-usize carrier. It names that carrier without +introducing a decoder or a claim that every extracted wrapper is nonzero. +-/ + +open Aeneas Aeneas.Std +namespace Zerocopy.Proofs + +abbrev NonZeroUsize := core.num.nonzero.NonZero Usize core.num.niche_types.NonZeroUsizeInner + +end Zerocopy.Proofs diff --git a/verification/aeneas/lean/SpecsSyntax.lean b/verification/aeneas/lean/SpecsSyntax.lean new file mode 100644 index 0000000000..cd1a897abc --- /dev/null +++ b/verification/aeneas/lean/SpecsSyntax.lean @@ -0,0 +1,398 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import DeriveModels +public meta import Lean +public section + +/-! +An inline Rust `spec` becomes an ordinary Lean proposition about the extracted +function. This module elaborates that proposition in three stages: retain the +original raw arguments and their model providers, introduce decoded witnesses +and authored premises, then require a decoded successful result satisfying all +postconditions. A `(raw)` clause changes the names available to its author; it +never changes the arguments passed to the extracted function. + +The generated `_contract` family replaces only the execution outcome. Required +checks can therefore ask whether the authored contract implies an independent +promise for every outcome, including an incorrect return or divergence. The +binding audits below inspect Lean expressions rather than trusting printed +names or a second interpretation of the source syntax. +-/ + +open Lean Lean.Meta Aeneas Aeneas.Std +namespace AeneasSpecs + +meta initialize specAttribute : TagAttribute ← + registerTagAttribute `aeneas_spec "A transparent Aeneas specification" + +declare_syntax_cat specRequirement +syntax "requires " ident " : " term : specRequirement +syntax "requires(raw) " ident " : " term : specRequirement + +declare_syntax_cat specPostcondition +syntax "ensures " term+ " => " term : specPostcondition +syntax "ensures(raw) " term+ " => " term : specPostcondition + +syntax (name := totalSpec) "spec " ident bracketedBinder* " for " term + " with " num " type " "parameters" specRequirement* specPostcondition+ : command +syntax (name := partialSpec) "partial " "spec " ident bracketedBinder* " for " term + " with " num " type " "parameters" specRequirement* specPostcondition+ : command + +syntax (name := specBody) "aeneas_spec_body% " term " with " num " mode% " num + " ghosts% " bracketedBinder* " requires% " specRequirement* + " posts% " specPostcondition+ : term + +/-- Prevent a ghost or requirement from hiding an original input or another +premise. The user-facing name is checked after removing macro hygiene scopes; +two visually identical binders must not denote different contract inputs. -/ +private meta def checkFreshNames (previous : Array Expr) (newInputs : Array Expr) + : Elab.Term.TermElabM Unit := do + let mut names ← previous.mapM fun e => return (← e.fvarId!.getDecl).userName.eraseMacroScopes + for input in newInputs do + let binderName := (← input.fvarId!.getDecl).userName.eraseMacroScopes + if names.contains binderName then + throwError "Specification binder {binderName} shadows an original input or an earlier binder" + names := names.push binderName + +/-- Open the extracted function's forall type one binder at a time. The local +variables are the exact raw/type arguments later passed back to that function; +no decoded witness, ghost, or requirement can enter this argument prefix. -/ +private meta partial def withOriginalInputs (carrier : Expr) (inputs : Array Expr) + (k : Array Expr → Expr → Elab.Term.TermElabM Expr) : Elab.Term.TermElabM Expr := do + match carrier with + | .forallE binderName domain body _ => + Meta.withLocalDeclD binderName.eraseMacroScopes domain fun input => do + checkFreshNames inputs #[input] + withOriginalInputs (body.instantiate1 input) (inputs.push input) k + | _ => k inputs carrier + +/-- Clause scopes change only local declaration names. Authored terms elaborate +against exact free variables; no textual identifier replacement is performed. -/ +private meta def withClauseScope (raws maths : Array Expr) (useRaw : Bool) + (k : Elab.Term.TermElabM α) : Elab.Term.TermElabM α := withFreshMacroScope do + let mut lctx ← getLCtx + -- A free variable's identity stays fixed when its display name changes. + -- Hide the other interpretation with fresh macro scopes so ordinary clause + -- lookup sees only the requested raw or mathematical input names. + for i in [:raws.size] do + let userName := (← raws[i]!.fvarId!.getDecl).userName + let hiddenRaw ← MonadQuotation.addMacroScope (Name.mkSimple s!"_rawInput{i}") + let hiddenMath ← MonadQuotation.addMacroScope (Name.mkSimple s!"_mathInput{i}") + lctx := lctx.setUserName raws[i]!.fvarId! (if useRaw then userName else hiddenRaw) + lctx := lctx.setUserName maths[i]!.fvarId! (if useRaw then hiddenMath else userName) + Meta.withLCtx' lctx k + +/-- Introduce a mathematical witness immediately followed by its decoding +premise for each raw value. These are universally quantified contract inputs: +the operation promise applies whenever that original raw value decodes to the +supplied witness, using the provider selected before authored binders exist. -/ +private meta partial def withDecodedInputs (raws providers : Array Expr) (i : Nat) + (maths equations : Array Expr) + (k : Array Expr → Array Expr → Elab.Term.TermElabM Expr) : Elab.Term.TermElabM Expr := do + if h : i < raws.size then + let raw := raws[i] + let rawTy ← Meta.inferType raw + let provider := providers[i]! + let mathTy ← providerModel rawTy provider + Meta.withLocalDeclD (Name.mkSimple s!"_mathInput{i}") mathTy fun math => do + let equation ← Meta.mkEq (← providerDecode rawTy provider raw) + (← Meta.mkAppM ``Option.some #[math]) + Meta.withLocalDeclD (Name.mkSimple s!"_decodedInput{i}") equation fun hDecoded => + withDecodedInputs raws providers (i+1) (maths.push math) (equations.push hDecoded) k + else k maths equations + +/-- Elaborate requirements in source order. Each named premise is available +to later requirements and posts, while its clause's raw/math scope affects only +original input names. The freshness check preserves all earlier binder names. -/ +private meta partial def withRequirements (requirements : Array Syntax) (i : Nat) + (raws maths previous : Array Expr) + (k : Array Expr → Elab.Term.TermElabM Expr) : Elab.Term.TermElabM Expr := do + if h : i < requirements.size then + let (id, proposition, useRaw) ← match requirements[i] with + | `(specRequirement| requires $id:ident : $pre:term) => pure (id, pre, false) + | `(specRequirement| requires(raw) $id:ident : $pre:term) => pure (id, pre, true) + | _ => throwError "Unexpected requirement: {repr requirements[i]}" + let carrierType ← withClauseScope raws maths useRaw + (Elab.Term.elabTermEnsuringType proposition (mkSort .zero)) + Meta.withLocalDeclD id.getId carrierType fun hypothesis => do + checkFreshNames previous #[hypothesis] + withRequirements requirements (i+1) raws maths (previous.push hypothesis) k + else k previous + +/-- Use upstream result-pattern notation, retaining ordinary Lean ascriptions +and tuple patterns in either raw or mathematical result scope. -/ +private meta def patternPredicate (patterns : Array (TSyntax `term)) + (body : TSyntax `term) : Elab.Term.TermElabM (TSyntax `term) := do + let contract ← match patterns.toList with + | [binder] => `(Result.ok () ⦃ $binder => $body ⦄) + | binder :: rest => do + let tail := rest.toArray + `(Result.ok () ⦃ $binder $tail:term* => $body ⦄) + | [] => throwError "A result pattern is required" + let some expanded ← Elab.liftMacroM (expandMacro? contract.raw) | throwError "Unsupported result pattern" + match expanded with + | `(Aeneas.Std.WP.spec $_call $post) => pure post + | _ => throwError "Unsupported upstream result-pattern expansion" + +/-- All posts share one successful raw result and one decoded witness. The +existential's decoder equation is required even for a raw-only or True post; +otherwise such clauses would accidentally permit rejected successful outputs. -/ +private meta def postcondition (posts : Array Syntax) (raws maths : Array Expr) + (rawType provider : Expr) : Elab.Term.TermElabM Expr := do + Meta.withLocalDeclD `_rawOutput rawType fun raw => do + let mathType ← providerModel rawType provider + Meta.withLocalDeclD `_mathOutput mathType fun math => do + let mut propositions := #[] + for post in posts do + unless post.getNumArgs == 4 && #["ensures", "ensures(raw)"].contains post[0].getAtomVal do + throwError "Malformed specification postcondition" + let patterns : Array (TSyntax `term) := post[1].getArgs.map (fun stx => ⟨stx⟩) + let body : TSyntax `term := ⟨post[3]⟩ + let useRaw := post[0].getAtomVal == "ensures(raw)" + let predicateSyntax ← patternPredicate patterns body + let carrierType ← mkArrow (if useRaw then rawType else mathType) (mkSort .zero) + let predicate ← withClauseScope raws maths useRaw + (Elab.Term.elabTermEnsuringType predicateSyntax carrierType) + propositions := propositions.push (mkApp predicate (if useRaw then raw else math)) + -- Preserve clause order in a right-associated conjunction. The grammar + -- requires at least one post, so back! cannot read an empty array here. + let mut conjunction := propositions.back! + for p in (propositions.extract 0 (propositions.size-1)).reverse do + conjunction ← Meta.mkAppM ``And #[p, conjunction] + let equation ← Meta.mkEq (← providerDecode rawType provider raw) + (← Meta.mkAppM ``Option.some #[math]) + let body ← Meta.mkAppM ``And #[equation, conjunction] + let existsExpr ← Meta.mkAppM ``Exists #[← Meta.mkLambdaFVars #[math] body] + Meta.mkLambdaFVars #[raw] existsExpr + +@[term_elab specBody] meta def elabSpecBody : Elab.Term.TermElab := fun stx _ => do + match stx with + | `(aeneas_spec_body% $function:term with $count:num mode% $mode:num + ghosts% $ghosts:bracketedBinder* requires% $requirements:specRequirement* + posts% $posts:specPostcondition*) => + let id ← match function with + | `(@$id:ident) => pure id + | `($id:ident) => pure id + | _ => throwErrorAt function "A specification must name a bare extracted function constant" + let functionName ← Elab.realizeGlobalConstNoOverloadWithInfo id + let functionExpr ← Meta.mkConstWithFreshMVarLevels functionName + withOriginalInputs (← Meta.inferType functionExpr) #[] fun originals returnType => do + -- The generated count identifies the source generic prefix. Check both + -- sides of the boundary: counted parameters must be Type-valued, and + -- later value arguments must not contain an omitted type parameter. + unless count.getNat ≤ originals.size do throwErrorAt count "Invalid Rust type-parameter prefix" + for input in originals[:count.getNat] do + let carrierType ← Meta.whnf (← Meta.inferType input) + unless carrierType.isSort && carrierType != mkSort .zero do + throwErrorAt count "Rust generic parameters must be the original Type-valued prefix" + for input in originals[count.getNat:] do + if (← Meta.whnf (← Meta.inferType input)).isSort then + throwErrorAt count "Rust type-parameter count omits an original type parameter" + let returnType ← Meta.whnf returnType + -- This Result represents execution (return, panic, divergence), not a + -- Rust Result payload. Its successful payload gets the output decoder. + unless returnType.isAppOfArity ``Result 1 do + throwErrorAt function "Extracted function must return an Aeneas execution Result" + let rawOutputType := returnType.appArg! + let params := originals.extract 0 count.getNat + let raws := originals.extract count.getNat originals.size + let inputNames ← raws.mapM fun raw => return (← raw.fvarId!.getDecl).userName + withModelDictionaries params fun dictionaries => do + -- Fix all automatic providers before introducing decoded witnesses, + -- dependent ghosts, requirements, or their local instances. + let providers ← raws.mapM fun raw => do + boundModelProvider (← Meta.inferType raw) params dictionaries + let outputProvider ← boundModelProvider rawOutputType params dictionaries + withDecodedInputs raws providers 0 #[] #[] fun maths equations => do + let decodedPrefix := (maths.zip equations).foldl + (fun inputs pair => inputs ++ #[pair.1, pair.2]) #[] + let boundPrefix := originals ++ dictionaries ++ decodedPrefix + withClauseScope raws maths false <| Elab.Term.elabBinders ghosts fun ghostInputs => do + -- Restore raw names as the baseline for each clause-specific scope. + let mut lctx ← getLCtx + for i in [:raws.size] do + lctx := lctx.setUserName raws[i]!.fvarId! inputNames[i]! + lctx := lctx.setUserName maths[i]!.fvarId! (Name.mkSimple s!"_mathInput{i}") + Meta.withLCtx' lctx do + checkFreshNames boundPrefix ghostInputs + withRequirements requirements 0 raws maths (boundPrefix ++ ghostInputs) fun inputs => do + let post ← postcondition posts raws maths rawOutputType outputProvider + -- Execute on the untouched raw prefix. Total WP.spec excludes + -- panic and divergence; partial WP.dspec allows divergence but + -- still checks every successful return and excludes panic. + let call := mkAppN functionExpr originals + let wp := if mode.getNat == 0 then ``WP.spec else ``WP.dspec + let body ← Meta.mkAppM wp #[call, post] + Elab.Term.synthesizeSyntheticMVarsNoPostponing + Elab.Term.levelMVarToParam (← Meta.mkForallFVars inputs (← instantiateMVars body)) + | _ => throwError "Unexpected specification syntax: {repr stx}" + +syntax "aeneas_spec_decl " ident " := " term : command + +private meta def expandSpec (id : TSyntax `ident) + (ghosts : Array (TSyntax ``Lean.Parser.Term.bracketedBinder)) + (function : TSyntax `term) (count : TSyntax `num) (isPartial : Bool) + (requirements : Array (TSyntax `specRequirement)) + (posts : Array (TSyntax `specPostcondition)) : MacroM Syntax := do + let mode := Syntax.mkNumLit (if isPartial then "1" else "0") + let body ← `(aeneas_spec_body% $function with $count mode% $mode:num + ghosts% $ghosts:bracketedBinder* requires% $requirements:specRequirement* posts% $posts:specPostcondition*) + `(set_option linter.unusedVariables false in + aeneas_spec_decl $id := $body) + +macro_rules + | `(spec $id:ident $ghosts:bracketedBinder* for $function:term with $count:num type parameters + $requirements:specRequirement* $posts:specPostcondition*) => + expandSpec id ghosts function count false requirements posts + | `(partial spec $id:ident $ghosts:bracketedBinder* for $function:term with $count:num type parameters + $requirements:specRequirement* $posts:specPostcondition*) => + expandSpec id ghosts function count true requirements posts + +-- A fence owns one specification declaration. Restricting it here prevents a +-- source annotation from injecting unrelated declarations into generated Specs. +syntax "aeneas_spec_begin " command " aeneas_spec_end" : command +macro_rules + | `(aeneas_spec_begin $declaration:command aeneas_spec_end) => do + unless #[``totalSpec, ``partialSpec].contains declaration.raw.getKind do + Macro.throwErrorAt declaration "An Aeneas fence must contain exactly one spec declaration" + pure declaration.raw + +/-- Read a specification's actual model call without reducing the application. +Each argument must be its original prefix input variable; ghost inputs and +requirements may follow that prefix. Both the generated command and the final +coverage audit use this same elaborated-expression check. -/ +meta def specBinding (specName : Name) : MetaM (Name × Nat) := do + let info ← getConstInfo specName + let .defnInfo declaration := info + | throwError "Expected a specification declaration, got {specName}" + unless specAttribute.hasTag (← getEnv) specName do + throwError "Expected a specification declaration, got {specName}" + Meta.forallTelescope declaration.value fun inputs body => do + let body := body.consumeMData + unless body.getAppFn.isConstOf ``WP.spec || body.getAppFn.isConstOf ``WP.dspec do + throwError "Specification {specName} does not directly state a total or partial contract" + -- WP's arguments are the payload type, execution, and result predicate. + -- Inspect this application without unfolding the extracted function: its + -- constant and each original free-variable argument must remain visible. + let contractArgs := body.getAppArgs + unless contractArgs.size == 3 do + throwError "Specification {specName} has an unexpected contract application" + let call := contractArgs[1]!.consumeMData + let .const modelName _ := call.getAppFn + | throwError "Specification {specName} does not directly call a model declaration" + let arguments := call.getAppArgs + unless inputs.size ≥ arguments.size do + throwError "Specification {specName} has more model arguments than bound inputs" + -- Exact expression identity rejects reordered, replaced, or reconstructed + -- arguments, even when a replacement might compute the same value. + for index in [:arguments.size] do + unless arguments[index]!.consumeMData == inputs[index]! do + throwError "Specification {specName} model input {index + 1} is not its original bound variable" + pure (modelName, arguments.size) + +/-- The contract family abstracts only the audited execution node. Original +inputs stay first; one outcome then precedes dictionaries and all premises. +It cannot select a different execution for different ghosts. -/ +meta def specContractName (specName : Name) : Name := specName.appendAfter "_contract" + +meta def abstractSpecContract (specName : Name) : MetaM Expr := do + let (_, count) ← specBinding specName + let .defnInfo info ← getConstInfo specName + | throwError "Expected a transparent specification" + forallTelescope info.value fun inputs body => do + let body := body.consumeMData + let args := body.getAppArgs + let call := args[1]! + -- Keep original arguments outside the outcome binder. All dictionaries, + -- witnesses, ghosts, and premises belong to the one outcome inside it. + withLocalDeclD `_executionOutcome (← inferType call) fun outcome => do + let predicate := mkAppN body.getAppFn #[args[0]!, outcome, args[2]!] + let predicate ← mkForallFVars (inputs.extract count inputs.size) predicate + mkLambdaFVars ((inputs.extract 0 count).push outcome) predicate + +/-- Check the abstract family itself as well as its concrete specialization. +A constant family could specialize correctly while ignoring its outcome. -/ +meta def checkSpecContract (specName : Name) : MetaM Unit := do + let (_, count) ← specBinding specName + let familyName := specContractName specName + let .defnInfo info ← getConstInfo familyName + | throwError "Missing abstract specification contract {familyName}" + lambdaTelescope info.value fun inputs body => do + unless inputs.size == count + 1 do + throwError "Abstract contract {familyName} changed its original input prefix" + let outcome := inputs[count]! + forallTelescope body fun _ predicate => do + let predicate := predicate.consumeMData + unless predicate.getAppFn.isConstOf ``WP.spec || predicate.getAppFn.isConstOf ``WP.dspec do + throwError "Abstract contract {familyName} does not directly state a WP contract" + let args := predicate.getAppArgs + unless args.size == 3 && args[1]!.consumeMData == outcome do + throwError "Abstract contract {familyName} does not use its execution outcome" + -- Outcome use alone is insufficient: an added premise could make the family + -- vacuous. Compare the full abstraction to preserve every original domain + -- premise, provider, mode, and postcondition as well. + unless ← isDefEq info.value (← abstractSpecContract specName) do + throwError "Abstract contract {familyName} changed the specification's premises or postcondition" + +elab_rules : command + | `(aeneas_spec_decl $id:ident := $body:term) => do + Elab.Command.elabCommand (← `(@[aeneas_spec] abbrev $id : Prop := $body)) + -- A failed authored body already has its diagnostic. Do not derive a + -- contract from Lean's error-recovery placeholder or add a second error. + if ← MonadLog.hasErrors then return + Elab.Command.liftTermElabM do + let specName ← Elab.realizeGlobalConstNoOverloadWithInfo id + let .defnInfo source ← getConstInfo specName + | throwError "Expected a transparent specification" + let value ← abstractSpecContract specName + let familyName := specContractName specName + addDecl (.defnDecl { + «name» := familyName + levelParams := source.levelParams + «type» := (← inferType value) + value := value + hints := .abbrev + safety := .safe }) (forceExpose := true) + modifyEnv (addNoncomputable · familyName) + enableRealizationsForConst familyName + checkSpecContract specName + +/-- Check the actual call against the independently verified model declaration +and Rust type/value parameter count supplied by the generated command. -/ +elab "check_spec_binding " candidate:ident " for " extractedId:ident " with " count:num : command => + Elab.Command.liftTermElabM do + let specName ← Elab.realizeGlobalConstNoOverloadWithInfo candidate + let modelName ← Elab.realizeGlobalConstNoOverloadWithInfo extractedId + let (actualModel, inputCount) ← withRef candidate (specBinding specName) + unless actualModel == modelName do + throwErrorAt candidate "Specification {specName} does not call the expected model {modelName}" + unless inputCount == count.getNat do + throwErrorAt candidate "Specification {specName} does not bind exactly {count.getNat} model inputs" + checkSpecContract specName + +/-- Register an ordinary theorem whose type names a `spec` proposition with +Aeneas's existing step database. This pinned upstream registry inspects theorem +types syntactically. The generated alias expands only specifications, and Lean +checks its original theorem value against the expanded type. -/ +elab "register_spec_step " candidate:ident : command => Elab.Command.liftCoreM do + let thmName ← Elab.realizeGlobalConstNoOverloadWithInfo candidate + let info ← getConstInfo thmName + let .thmInfo _ := info + | throwError "Expected a theorem, got {thmName}" + let env ← getEnv + let expanded ← Meta.deltaExpand info.type (specAttribute.hasTag env) + let adapterName := thmName.appendAfter "_step" + let declaration : TheoremVal := ⟨⟨adapterName, info.levelParams, expanded⟩, + mkConst thmName (info.levelParams.map Level.param), [adapterName]⟩ + addDecl (.thmDecl declaration) + Elab.addDeclarationRangesFromSyntax adapterName candidate + Attribute.add adapterName `step .missing .global + +end AeneasSpecs diff --git a/verification/aeneas/lean/SpecsSyntaxTests.lean b/verification/aeneas/lean/SpecsSyntaxTests.lean new file mode 100644 index 0000000000..b0ad71d9f4 --- /dev/null +++ b/verification/aeneas/lean/SpecsSyntaxTests.lean @@ -0,0 +1,251 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import ModelTests +public import SpecsSyntax +@[expose] public section + +/-! +These independent expansions pin what the authored clause syntax means after +elaboration: original raw execution, dictionary placement, interleaved decoded +witnesses and equations, clause scopes, shared result admission, and total versus +partial correctness. Several examples use competing ghost instances or raw-only +postconditions to ensure those conveniences cannot change provider selection or +remove the obligation to decode a successful return. +-/ + +open Lean Aeneas Aeneas.Std AeneasSpecs +namespace SpecsSyntaxTests +set_option linter.unusedVariables false +set_option linter.unusedSimpArgs false + +-- Ordinary Rust assertion harnesses return Unit. The provider lookup reduces +-- that abbreviation to PUnit.{1}; this regression checks the actual function +-- signature, macro expansion, and returned-value decoding together. +def unitAssertionRoot (x : Usize) : Result Unit := do + massert (x = x) + .ok () +check_model_inputs unitAssertionRoot with 0 type parameters +spec unit_assertion_root_spec for unitAssertionRoot with 0 type parameters + ensures(raw) _ => True + +example : unit_assertion_root_spec := by + intro x decoded hdecoded + simp [unitAssertionRoot, massert, WP.spec_ok, RustModel.decode, modelUnit] + +example : ¬ (Result.fail .panic : Result Unit) ⦃ _ => True ⦄ := by + simp + +def identity (α : Type u) (x : α) : Result α := .ok x +spec identity_spec for @identity with 1 type parameters + ensures y => y = x + ensures(raw) y => y = x +check_spec_binding identity_spec for identity with 2 +theorem identity_proof : identity_spec := by + intro α x m mx hx + simp only [identity, Result.ok] + exact ⟨mx, hx, rfl, rfl⟩ + +def sequenceIdentity (xs : List Nat) : Result (List Nat) := .ok xs +spec dependent_spec (position : Fin xs.length) for sequenceIdentity with 0 type parameters + requires h : position.val < xs.length + requires(raw) hr : position.val < xs.length + ensures ys => ys = xs ∧ h = h + ensures(raw) ys => ys = xs ∧ hr = hr +check_spec_binding dependent_spec for sequenceIdentity with 1 + +-- The output interpretation is fixed before the competing ghost exists. +def nativeIdentity (x : Nat) : Result Nat := .ok x +spec fixed_provider_spec [evil : RustModel Nat] for nativeIdentity with 0 type parameters + ensures(raw) y => y = x +example : fixed_provider_spec := by + intro x mx hx evil + change some x = some mx at hx + cases hx + exact ⟨x, rfl, rfl⟩ + +-- Independently authored expansions pin the scopes and upstream WP modes. +def wordIdentity (x : Usize) : Result Usize := .ok x +spec word_math for wordIdentity with 0 type parameters + requires h : x.value < 5 + ensures y => y.value = x.value +spec word_raw for wordIdentity with 0 type parameters + requires(raw) h : x.val < 5 + ensures(raw) y => y.val = x.val +partial spec word_partial for wordIdentity with 0 type parameters + ensures y => y.value = x.value +spec word_mixed for wordIdentity with 0 type parameters + requires h : x.value < 5 + requires(raw) hr : x.val < 5 + ensures y => y.value = x.value ∧ h = h + ensures(raw) y => y.val = x.val ∧ hr = hr + +example : word_math = + (∀ raw : Usize, ∀ math : UnsignedWord .Usize, + (modelUScalar .Usize).decode raw = some math → + (h : math.value < 5) → + WP.spec (wordIdentity raw) (fun rawOutput => + ∃ output : UnsignedWord .Usize, + (modelUScalar .Usize).decode rawOutput = some output ∧ output.value = math.value)) := rfl +example : word_raw = + (∀ raw : Usize, ∀ math : UnsignedWord .Usize, + (modelUScalar .Usize).decode raw = some math → + (h : raw.val < 5) → + WP.spec (wordIdentity raw) (fun rawOutput => + ∃ output : UnsignedWord .Usize, + (modelUScalar .Usize).decode rawOutput = some output ∧ rawOutput.val = raw.val)) := rfl +example : word_partial = + (∀ raw : Usize, ∀ math : UnsignedWord .Usize, + (modelUScalar .Usize).decode raw = some math → + WP.dspec (wordIdentity raw) (fun rawOutput => + ∃ output : UnsignedWord .Usize, + (modelUScalar .Usize).decode rawOutput = some output ∧ output.value = math.value)) := rfl +example : word_mixed = + (∀ raw : Usize, ∀ math : UnsignedWord .Usize, + (modelUScalar .Usize).decode raw = some math → + (h : math.value < 5) → (hr : raw.val < 5) → + WP.spec (wordIdentity raw) (fun rawOutput => + ∃ output : UnsignedWord .Usize, + (modelUScalar .Usize).decode rawOutput = some output ∧ + (output.value = math.value ∧ h = h) ∧ (rawOutput.val = raw.val ∧ hr = hr))) := rfl + +-- A generic ghost cannot change the original generated dictionary either. +spec generic_fixed [evil : RustModel α] for @identity with 1 type parameters + ensures(raw) y => y = x +example : generic_fixed := by + intro α x original math decoded evil + exact ⟨math, decoded, rfl⟩ + +-- Every source generic receives a dictionary, even if unused or result-only. +def unused (α : Type) (x : Nat) : Result Nat := .ok x +spec unused_spec for @unused with 1 type parameters + ensures y => y = x +example : unused_spec = + (∀ α : Type, ∀ raw : Nat, ∀ _dictionary : RustModel α, + ∀ math : Nat, some raw = some math → + WP.spec (unused α raw) (fun rawOutput => ∃ output : Nat, + some rawOutput = some output ∧ output = math)) := rfl + +def resultOnly (T : Type u) : Result (Option T) := .ok none +spec result_only for @resultOnly with 1 type parameters + ensures result => result = none +example : result_only := by + intro T m + exact ⟨none, rfl, rfl⟩ + +-- A restrictive supplied interpretation controls both sides of identity. +abbrev positiveOnly : RustModel Nat := ⟨Unit, fun x => if 0 < x then some () else none⟩ +example : positiveOnly.decode 0 = none := rfl +example : positiveOnly.decode 1 = some () := rfl +example : WP.spec (identity Nat 1) (fun raw => ∃ value : Unit, + positiveOnly.decode raw = some value ∧ value = () ∧ raw = 1) := + @identity_proof Nat 1 positiveOnly () rfl + +-- Concrete function instantiations are supported after Models has supplied +-- every nominal provider. This is distinct from applying a generic Fields +-- family to an undecoded concrete child during ModelShapes. +def concreteBoxIdentity (x : ModelTests.Box ModelTests.Child) : + Result (ModelTests.Box ModelTests.Child) := .ok x +check_model_inputs concreteBoxIdentity with 0 type parameters +spec concrete_box_identity for concreteBoxIdentity with 0 type parameters + ensures result => result = x + ensures(raw) result => result = x +example : concrete_box_identity := by + intro raw math decoded + exact ⟨math, decoded, rfl, rfl⟩ + +def childIdentity (x : ModelTests.Child) : Result ModelTests.Child := .ok x +spec model_dependent_ghost (position : Fin x.value) (constraint : x.positive = x.positive) + for childIdentity with 0 type parameters + ensures y => y.value = x.value + +-- Native pair projections retain bounded word carriers and Nat coercions. +def pairWords (x : Usize) : Result (Usize × Usize) := .ok (x, x) +spec pair_words for pairWords with 0 type parameters + ensures (a, b) => (a : Nat) = (x : Nat) ∧ (b : Nat) = (x : Nat) + +-- A function-valued payload is rejected before any ghost or clause elaborates. +def escapingOutput (x : Nat) : Result (Nat → Nat) := .ok (fun y => y) +/-- error: Unsupported escaping function or backward reconstruction in model carrier -/ +#guard_msgs in +discard_model_fixture spec escaping for escapingOutput with 0 type parameters ensures y => True + +/-- error: An Aeneas fence must contain exactly one spec declaration -/ +#guard_msgs in +discard_model_fixture aeneas_spec_begin + def injected : Nat := 0 +aeneas_spec_end + +def rejectedReturn (x : Nat) : Result + (core.num.nonzero.NonZero Usize core.num.niche_types.NonZeroUsizeInner) := .ok ⟨0#usize⟩ +spec rejected_total for rejectedReturn with 0 type parameters ensures result => True +partial spec rejected_partial for rejectedReturn with 0 type parameters ensures result => True +example : ¬rejected_total := by + intro contract + have h := contract 0 0 rfl + simp [rejectedReturn, WP.spec, WP.dspec, RustModel.decode, modelNonZeroUScalar] at h +example : ¬rejected_partial := by + intro contract + have h := contract 0 0 rfl + simp [rejectedReturn, WP.spec, WP.dspec, RustModel.decode, modelNonZeroUScalar] at h + +-- Five generic raw arguments exercise arbitrary-arity witness elimination. +-- Only the original raw telescope is passed to the extracted function. +def fiveInputs (T : Type u) (a b c d e : T) : Result T := .ok a +spec five_inputs_raw for @fiveInputs with 1 type parameters + requires(raw) accepted : True + ensures(raw) result => result = a +check_spec_binding five_inputs_raw for fiveInputs with 6 +example : five_inputs_raw.{u} = + (∀ T : Type u, ∀ a b c d e : T, ∀ [m : RustModel T], + ∀ ma : m.Model, m.decode a = some ma → + ∀ mb : m.Model, m.decode b = some mb → + ∀ mc : m.Model, m.decode c = some mc → + ∀ md : m.Model, m.decode d = some md → + ∀ me : m.Model, m.decode e = some me → + (accepted : True) → + WP.spec (fiveInputs T a b c d e) (fun rawOutput => + ∃ output : m.Model, m.decode rawOutput = some output ∧ rawOutput = a)) := rfl +example : five_inputs_raw.{u} = + (∀ T : Type u, ∀ a b c d e : T, ∀ [m : RustModel T], + isValid a → isValid b → isValid c → isValid d → isValid e → + (accepted : True) → + WP.spec (fiveInputs T a b c d e) (fun rawOutput => isValid rawOutput ∧ rawOutput = a)) := by + apply propext + simp only [five_inputs_raw, isValid, forall_exists_index, WP.spec_equiv_exists, exists_and_left, exists_and_right] + +-- Two words also pin alternating witness/equation order independently of the +-- generic dictionary case above. +def pairFunction (a b : Nat) : Result (Nat × Nat) := .ok (a, b) +spec pair_words_interleaved for pairFunction with 0 type parameters + ensures(raw) result => result = (a, b) +example : pair_words_interleaved = + (∀ a b : Nat, ∀ ma : Nat, some a = some ma → + ∀ mb : Nat, some b = some mb → + WP.spec (pairFunction a b) (fun rawOutput => ∃ output : Nat × Nat, + (modelProd : RustModel (Nat × Nat)).decode rawOutput = some output ∧ rawOutput = (a, b))) := rfl +@[aeneas_spec] abbrev swapped : Prop := ∀ a b : Nat, + WP.spec (pairFunction b a) (fun _ => True) +/-- error: Specification SpecsSyntaxTests.swapped model input 1 is not its original bound variable -/ +#guard_msgs in +check_spec_binding swapped for pairFunction with 2 + +/-- error: Specification binder x shadows an original input or an earlier binder -/ +#guard_msgs in +discard_model_fixture spec shadows (x : Nat) for nativeIdentity with 0 type parameters + ensures y => True + +/-- error: Specification binder h shadows an original input or an earlier binder -/ +#guard_msgs in +discard_model_fixture spec duplicate_requires for nativeIdentity with 0 type parameters + requires h : True + requires(raw) h : True + ensures y => True + +end SpecsSyntaxTests diff --git a/verification/aeneas/lean/SupportTests.lean b/verification/aeneas/lean/SupportTests.lean new file mode 100644 index 0000000000..3d00b1ac35 --- /dev/null +++ b/verification/aeneas/lean/SupportTests.lean @@ -0,0 +1,141 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Aeneas +public import Loops +public import RequiredContracts +@[expose] public section + +/-! +These examples exercise arithmetic, mathematical-view composition, and +indexed-loop reasoning as ordinary consumers of the shared proof support. +They test how the abstractions are used together, rather than merely +repeating their definitions. The independently written expectations keep +termination, returned values, and loop-state preservation observable. +-/ +open Aeneas Aeneas.Std AeneasContracts AeneasSpecs +namespace SupportTests + +-- Use upstream checked-operation registrations, including overflow branches. +theorem add_spec (x y : Usize) : + lift (x.checked_add y) + ⦃ out => out.map UScalar.val = + if x.val + y.val ≤ Usize.max then some (x.val + y.val) else none ⦄ := by + step as ⟨out, hout⟩ + cases out <;> simp_all only [Option.map_none, Option.map_some] + all_goals scalar_tac +split + +theorem sub_spec (x y : Usize) : + lift (x.checked_sub y) + ⦃ out => out.map UScalar.val = + if y ≤ x then some (x.val - y.val) else none ⦄ := by + step as ⟨out, hout⟩ + cases out <;> simp_all only [Option.map_none, Option.map_some] + all_goals scalar_tac +split + +theorem mul_spec (x y : Usize) : + lift (x.checked_mul y) + ⦃ out => out.map UScalar.val = + if x.val * y.val ≤ Usize.max then some (x.val * y.val) else none ⦄ := by + step as ⟨out, hout⟩ + cases out <;> simp_all only [Option.map_none, Option.map_some] + all_goals scalar_tac +split + +def pair (n : Nat) : Result (Nat × Nat) := .ok (n, n + 1) + +theorem pair_view_spec (n : Nat) : + pair n + ⦃ r => Prod.fst r = n ∧ r.2 = n + 1 ⦄ := by + exact WP.spec.ret ⟨rfl, rfl⟩ + +attribute [step] pair_view_spec + +-- A caller uses the view theorem through Aeneas's existing spec registry. +theorem pair_caller_spec (n : Nat) : + (do let r ← pair n; Result.ok r.1) + ⦃ r => id r = n ⦄ := by + step* + simp_all + +theorem diverging_view_spec : + (Result.div : Result Nat) + ⦃ r => Nat.succ r = 0 ∧ r = 9 ⦄div := by + exact WP.dspec.div _ + +theorem partial_view_spec (n : Nat) : + Result.ok n + ⦃ r => Nat.succ r = n + 1 ⦄div := by + exact WP.dspec.ret rfl + +-- The independent family compares every outcome, even though `pair` itself +-- happens to return the expected pair for every input. +spec pair_inline for pair with 0 type parameters + ensures (first, second) => first = n ∧ second = n + 1 + +theorem pair_inline_proof : pair_inline := by + intro n math decoded + change some n = some math at decoded + cases decoded + exact ⟨(n, n + 1), rfl, rfl, rfl⟩ + +def pair_required_contract (n : Nat) (run : Result (Nat × Nat)) : Prop := + ∃ r, run = .ok r ∧ r.1 = n ∧ r.2 = n + 1 + +def pair_required : Prop := ∀ n : Nat, pair_required_contract n (pair n) + +@[contract_simps] theorem pair_required_bridge (n : Nat) (run : Result (Nat × Nat)) + (provided : pair_inline_contract n run) : pair_required_contract n run := by + obtain ⟨out, success, math, decoded, first, second⟩ := + (WP.spec_equiv_exists _ _).mp (provided n rfl) + have same : out = math := by + simpa [RustModel.decode, modelProd] using decoded + cases same + exact ⟨out, success, first, second⟩ + +check_contract pair_required using pair_inline_proof + +-- An alias still names its own independent family and canonical inline proof. +def pair_required_again_contract := pair_required_contract +def pair_required_again : Prop := pair_required +@[contract_simps] theorem pair_required_again_bridge + (n : Nat) (run : Result (Nat × Nat)) (provided : pair_inline_contract n run) : + pair_required_again_contract n run := pair_required_bridge n run provided +check_contract pair_required_again using pair_inline_proof + +example : ∀ n : Nat, WP.spec (do let r ← pair n; Result.ok r.1) + (fun r => r = n) := pair_caller_spec +example : WP.dspec (Result.div : Result Nat) + (fun r => Nat.succ r = 0 ∧ r = 9) := diverging_view_spec +example : ∀ n : Nat, WP.dspec (Result.ok n) + (fun r => Nat.succ r = n + 1) := partial_view_spec + +def counterBody (n : Usize) (s : Nat × Usize) : + Result (ControlFlow (Nat × Usize) Nat) := do + if s.2 < n then + let j ← s.2 + 1#usize + .ok (.cont (s.1 + 1, j)) + else .ok (.done s.1) + +theorem counter_spec (n i : Usize) (hi : i ≤ n) : + loop (counterBody n) (i.val, i) ⦃ out => out = n.val ∧ True ⦄ := by + apply indexed_loop_spec n.val id (fun i => i) (fun _ => True) + · intro state idx hidx hv _ + unfold counterBody + simp only [UScalar.lt_equiv] + split + · rename_i hlt + step as ⟨j, hj⟩ + simp_all [id] + · rename_i hdone + exact WP.spec.ret ⟨by scalar_tac, hv, trivial⟩ + · exact (UScalar.le_equiv _ _).mp hi + · rfl + · trivial + +end SupportTests diff --git a/verification/aeneas/lean/Zerocopy.lean b/verification/aeneas/lean/Zerocopy.lean new file mode 100644 index 0000000000..b1d17e74f3 --- /dev/null +++ b/verification/aeneas/lean/Zerocopy.lean @@ -0,0 +1,19 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Zerocopy.Funs + +/-! +This is the import root for the extracted program and its handwritten +external models. Aeneas supplies generated Types and Funs; the external +modules state the explicitly selected correspondence for operations outside +that translation. Specs and proofs import this root so their computation +names refer to the selected complete extraction, not to inline mathematical +definitions. +-/ diff --git a/verification/aeneas/lean/Zerocopy/FunsExternal.lean b/verification/aeneas/lean/Zerocopy/FunsExternal.lean new file mode 100644 index 0000000000..52612ebaa5 --- /dev/null +++ b/verification/aeneas/lean/Zerocopy/FunsExternal.lean @@ -0,0 +1,41 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Zerocopy.Types +@[expose] public section +open Aeneas Aeneas.Std +open Zerocopy + +-- External semantic models: Clone copies the value; NonZero::get returns its +-- stored integer without failure. Their correspondence to Rust is part of the +-- trusted boundary described in ../../README.md, not a theorem of this package. +def core.num.niche_types.NonZeroUsizeInner.Insts.CoreCloneClone.clone + (x : core.num.niche_types.NonZeroUsizeInner) : + Result core.num.niche_types.NonZeroUsizeInner := .ok x + +@[simp] def core.num.nonzero.NonZero.get + {T Inner : Type} (_inst : core.num.nonzero.ZeroablePrimitive T Inner) + (x : core.num.nonzero.NonZero T Inner) : Result T := .ok x.val + +-- Extraction uses size_of only for usize to calculate its bit width. Other +-- types remain unsupported rather than receiving arbitrary layout values. +@[simp] noncomputable def core.mem.size_of (T : Type) : Result Usize := by + classical + exact if T = Usize then + .ok { bv := BitVec.ofNat _ (System.Platform.numBits / 8) } + else .fail .panic + +-- The extracted call sites instantiate this primitive only at Usize. +@[simp] noncomputable def core.num.nonzero.NonZero.new + {T Inner : Type} (_inst : core.num.nonzero.ZeroablePrimitive T Inner) + (x : T) : Result (Option (core.num.nonzero.NonZero T Inner)) := by + classical + exact if h : T = Usize then + if (cast h x : Usize) = 0#usize then .ok none else .ok (some ⟨x⟩) + else .fail .panic diff --git a/verification/aeneas/lean/Zerocopy/TypesExternal.lean b/verification/aeneas/lean/Zerocopy/TypesExternal.lean new file mode 100644 index 0000000000..10c6e45a5b --- /dev/null +++ b/verification/aeneas/lean/Zerocopy/TypesExternal.lean @@ -0,0 +1,20 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +module +public import Aeneas +@[expose] public section +open Aeneas.Std + +-- These types model stored values, not Rust's niche encoding or memory layout. +-- The generic wrapper admits zero; the alignment theorems explicitly require +-- positivity. This over-approximation includes every valid NonZeroUsize. +abbrev core.num.niche_types.NonZeroUsizeInner := Usize + +structure core.num.nonzero.NonZero (T : Type) (_Inner : Type) where + val : T diff --git a/verification/aeneas/negative-controls.sh b/verification/aeneas/negative-controls.sh new file mode 100755 index 0000000000..f3569660f9 --- /dev/null +++ b/verification/aeneas/negative-controls.sh @@ -0,0 +1,1286 @@ +#!/usr/bin/env bash +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +# Run after run.sh. Mutate generated scratch files, never checked-in sources. +# Capabilities select control families; a missing selected fixture is an error. +set -euo pipefail +cd "$(dirname "$0")/../.." +repo=$PWD +tools_dir=${AENEAS_TOOLCHAIN_DIR:-"$repo/target/aeneas/toolchain"} +export PATH="$tools_dir/lean/bin:$PATH" +# Golden and live projects have separate generated sources, caches, and +# backups. Each subshell restores its own project even when a control fails. +check_workspace() ( +cd "$repo/target/aeneas/$1" +echo "Testing failure controls in $1" +# Every temporary source has one lifecycle: it must be absent before the +# suite starts and is removed on exit. Use the same roster for both decisions +# so cleanup cannot delete a source that belonged to an earlier editor session. +control_files=( + NegativeControl.lean WeakOutcome.lean + RoundingConstructorControl.lean RoundingDomainControl.lean + Zerocopy/ScopeControl.lean Zerocopy/ShapeControl.lean Zerocopy/ModelControl.lean + Proofs/WitnessOwnerControl.lean Proofs/PaddingDependencyControl.lean +) +for fixture in "${control_files[@]}"; do + # A dangling symlink also belongs to somebody else; -e alone misses it. + [[ ! -e "$fixture" && ! -L "$fixture" ]] || { + echo "Negative control fixture already exists: $fixture" >&2; exit 1; + } +done +backup=$(mktemp -d) +python3 - "$backup" <<'PYBACKUP' +from pathlib import Path +import shutil, sys +root = Path(sys.argv[1]) +files = [Path("bindings.json"), Path("Required.lean"), Path("Specs.lean"), Path("ModelShapes.lean"), Path("Models.lean"), + Path("SpecPrelude.lean"), Path("Obligations.lean"), + Path("Zerocopy/Funs.lean"), Path("Zerocopy/FunsExternal.lean"), + *Path(".").glob("Proofs*.lean"), + *Path("Proofs").rglob("*.lean")] +for name in ("ModelSupport", "LayoutModel", "MathViews", "SupportTests", "Corollaries", + "LayoutModelDomainTests", "OutcomeTests"): + if Path(name + ".lean").exists(): + files.append(Path(name + ".lean")) +for source in files: + destination = root / source + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, destination) +PYBACKUP +# Restore baseline source bytes between control stages. Lake then +# rebuilds from those sources; restoring an old .olean could conceal a mutation. +restore_files() { + python3 - "$backup" <<'PYRESTORE' +from pathlib import Path +import shutil, sys +root = Path(sys.argv[1]) +for source in [*root.rglob("*.lean"), root / "bindings.json"]: + shutil.copyfile(source, source.relative_to(root)) +PYRESTORE +} +cleanup() { + restore_files + rm -f "${control_files[@]}" + rm -rf "$backup" +} +trap cleanup EXIT + +# This helper checks command failure, not its semantic cause. Each control +# must also establish its valid baseline and inspect the expected diagnostic. +expect_failure() { + local description=$1 log=$2 + shift 2 + if "$@" > "$backup/$log" 2>&1; then + echo "Negative control accepted $description" >&2; exit 1 + fi + if ! grep -q 'error:' "$backup/$log"; then + cat "$backup/$log" >&2; exit 1 + fi + echo "Confirmed: rejected $description" +} +build_required() { + lake build Required +} +audit() { + lake env lean -DwarningAsError=true Check.lean +} +# The positive module proves TModel authoring and concrete forward shapes first. +# Reject raw-type/dictionary shape dependencies in a separate source environment. +lake build ModelTests > "$backup/type-only-shapes-positive.log" 2>&1 || { + cat "$backup/type-only-shapes-positive.log" >&2; exit 1; +} +shape_fixture="$repo/verification/aeneas/tests/type-only-shapes/reject-raw-parameter.lean" +expect_failure "raw generic parameter in a type-only model shape" type-only-shapes-negative.log \ + lake env lean --root="$(dirname "$shape_fixture")" -DwarningAsError=true "$shape_fixture" +if ! grep -Fq 'Unknown identifier `T`' "$backup/type-only-shapes-negative.log"; then + echo "Type-only shape fixture failed outside its intended authoring boundary" >&2 + cat "$backup/type-only-shapes-negative.log" >&2; exit 1 +fi +# A semantic mutation must remain a well-formed generated program. Only its +# dependent proofs should fail; a parser/type error cannot establish sensitivity. +check_model_mutant() { + local description=$1 target=${2:-Zerocopy.Funs} + lake build "+$target" > "$backup/model-well-formed.log" 2>&1 || { + echo "Malformed model negative control: $description" >&2 + cat "$backup/model-well-formed.log" >&2; exit 1; + } +} +# Reuse the verified root mapping instead of parsing model declaration syntax +# a second time. Missing/invalid manifests are errors, never absent capabilities. +has_binding() { + local present + present=$(python3 - "$@" <<'PYCONTROL' +from pathlib import Path +import json, sys +manifest = json.loads(Path("bindings.json").read_text()) +models = manifest["bindings"] +if (manifest.get("version") != 3 or manifest.get("mode") != "verified-live" + or not isinstance(models, dict) or not all( + isinstance(key, str) and isinstance(value, dict) and value.get("kind") in ("function", "type") and isinstance(value.get("raw"), str) for key, value in models.items())): + raise SystemExit("Invalid verified model bindings for failure controls") +kind = sys.argv[1] +raw = sys.argv[2] if len(sys.argv) == 3 else None +if kind not in ("function", "type") or len(sys.argv) not in (2, 3): + raise SystemExit("Invalid binding capability request") +print("yes" if any(entry["kind"] == kind and (raw is None or entry["raw"] == raw) + for entry in models.values()) else "no") +PYCONTROL +) || { echo "Cannot determine model capability" >&2; exit 1; } + [[ $present == yes ]] +} +has_model() { + has_binding function "Zerocopy.$1" +} +# A constrained mathematical value can still break an operation promise or its +# required raw input domain. Probe the unchanged constructor source fence in an +# isolated module so reorganizing helper proofs does not change these controls. +rounding_control="$repo/verification/aeneas/tests/rounding_model_controls.py" +rounding_capability=$(python3 "$rounding_control" capability) || exit 1 +if [[ $rounding_capability == yes ]]; then + lake build +Models +SpecsSyntax +Zerocopy.Funs > "$backup/rounding-operation-baseline.log" 2>&1 || { + cat "$backup/rounding-operation-baseline.log" >&2; exit 1; + } + python3 "$rounding_control" write-probes + for fixture in RoundingConstructorControl RoundingDomainControl; do + lake env lean -DwarningAsError=true "$fixture.lean" > "$backup/$fixture-baseline.log" 2>&1 || { + echo "Malformed or false rounding behavior baseline: $fixture" >&2 + cat "$backup/$fixture-baseline.log" >&2; exit 1; + } + done + reject_rounding_decoder() { + local mutation=$1 description=$2 target=$3 + python3 "$rounding_control" mutate "$mutation" + # Proof fields must be correct; a malformed decoder cannot establish + # sensitivity of its operation promise or independently fixed domain. + check_model_mutant "$description" Models + if [[ $mutation == constant ]]; then + lake env lean -DwarningAsError=true RoundingDomainControl.lean \ + > "$backup/rounding-constant-domain.log" 2>&1 || { + echo "Constant rounding mutant did not preserve the required input" >&2 + cat "$backup/rounding-constant-domain.log" >&2; exit 1; + } + fi + expect_failure "$description" "rounding-$mutation-behavior.log" \ + lake env lean -DwarningAsError=true "$target.lean" + python3 "$rounding_control" check-failure "$mutation" "$backup/rounding-$mutation-behavior.log" + cp "$backup/Models.lean" Models.lean + lake build +Models > "$backup/rounding-$mutation-restore.log" 2>&1 || { + cat "$backup/rounding-$mutation-restore.log" >&2; exit 1; + } + } + reject_rounding_decoder constant "a valid constant rounding pair breaking new(2,1)'s output promise" RoundingConstructorControl + reject_rounding_decoder none "an always-reject rounding decoder excluding a required positive getter input" RoundingDomainControl +fi +restore_build() { + restore_files + lake build Required > "$backup/restore-build.log" 2>&1 || { + cat "$backup/restore-build.log" >&2; exit 1; + } +} + +# Establish a valid baseline so a missing artifact cannot satisfy a control. +lake build > "$backup/baseline-build.log" 2>&1 || { + cat "$backup/baseline-build.log" >&2; exit 1; +} +audit > "$backup/baseline-audit.log" 2>&1 || { + cat "$backup/baseline-audit.log" >&2; exit 1; +} + +# Harmless extracted-function imports are ordinary acyclic dependencies. +# Their presence cannot replace the independently authored outcome expectation. +if [[ -f ModelSupport.lean ]]; then + for transitive in false true; do + python3 - "$transitive" <<'PYCONTROL' +from pathlib import Path +import sys +support = Path("ModelSupport.lean") +source = support.read_text() +marker = '@[expose] public section' +if source.count(marker) != 1: + raise SystemExit("Compiled support import control requires the exact support header boundary") +if sys.argv[1] == 'true': + helper = Path("SpecPrelude.lean") + text = helper.read_text() + if text.count(marker) != 1: + raise SystemExit("Compiled support import control requires the exact helper header boundary") + helper.write_text(text.replace(marker, 'public import «Zerocopy».Funs\n' + marker)) + imported = '«SpecPrelude»' +else: + imported = '«Zerocopy».Funs' +support.write_text(source.replace(marker, 'public import ' + imported + '\n' + marker)) +PYCONTROL + lake build Required > "$backup/support-import-build.log" 2>&1 || { + echo "Malformed compiled support import control" >&2 + cat "$backup/support-import-build.log" >&2; exit 1; + } + audit > "$backup/support-import-audit.log" 2>&1 || { + echo "Harmless support function import rejected (transitive=$transitive)" >&2 + cat "$backup/support-import-audit.log" >&2; exit 1; + } + echo "Confirmed: harmless support function import accepted (transitive=$transitive)" + restore_build + done +fi + +# Every independent conversion must exist in the generated checking module. +# These are well-formed modules with the canonical proof and Specs check intact. +reject_witness() { + local mutation=$1 description=$2 diagnostic=$3 + python3 - "$mutation" <<'PYCONTROL' +from pathlib import Path +import sys +p = Path("Required.lean") +s = p.read_text() +command = 'check_contract Zerocopy.Obligations.min_spec using @Zerocopy.Proofs.min_spec\n' +if s.count(command) != 1: + raise SystemExit("Independent witness coverage control no longer matches") +mutation = sys.argv[1] +replacement = '' +# Preserve the independently typed adequacy theorem when testing the later +# concrete witness audit, so a missing earlier check cannot mask the failure. +checked_context = ('namespace IndependentWitnessControl\n' + command + + 'end IndependentWitnessControl\n' + 'theorem Zerocopy.Obligations.min_spec_adequate :\n' + ' contract_adequacy% Zerocopy.Specs.min_spec implies Zerocopy.Obligations.min_spec :=\n' + ' IndependentWitnessControl.Zerocopy.Obligations.min_spec_adequate\n') +if mutation == 'wrong-kind': + replacement = (checked_context + + 'def Zerocopy.Obligations.min_spec_checked : Zerocopy.Obligations.min_spec :=\n' + ' IndependentWitnessControl.Zerocopy.Obligations.min_spec_checked\n') +elif mutation == 'wrong-type': + replacement = checked_context + 'theorem Zerocopy.Obligations.min_spec_checked : True := by trivial\n' +elif mutation == 'missing-rows': + example = 'example : Zerocopy.Specs.min_spec := @Zerocopy.Proofs.min_spec\n' + if s.count(example) != 1 or 'def requiredTheorems ' in s: + raise SystemExit("Compiled specification scope control no longer matches") + s = s.replace(example, '') + '\ndef requiredTheorems : Array Name := #[]\n' +elif mutation == 'wrong-owner': + proof = Path("Proofs/WitnessOwnerControl.lean") + imported = 'import Proofs\n' + if proof.exists() or s.count(imported) != 1: + raise SystemExit("Independent witness ownership fixture no longer matches") + # Keep the successful checking context in a separate owning module. + # Importing normalization infrastructure into Proofs would change the + # elaboration context of its existing raw and mathematical proofs. + imports = ''.join(line + '\n' for line in s.splitlines() + if line.startswith('import ')) + normalizers = ''.join(line + '\n' for line in s.splitlines() + if line.startswith('attribute [local contract_simps] ')) + proof.parent.mkdir(exist_ok=True) + proof.write_text(imports + 'open Lean\n\n' + normalizers + command) + s = s.replace(imported, imported + 'import Proofs.WitnessOwnerControl\n') +elif mutation != 'missing': + raise SystemExit("Unknown independent witness coverage mutation") +p.write_text(s.replace(command, replacement)) +PYCONTROL + lake build Required > "$backup/witness-build.log" 2>&1 || { + echo "Malformed independent witness control: $description" >&2 + cat "$backup/witness-build.log" >&2; exit 1; + } + expect_failure "$description" witness-audit.log audit + if ! grep -Fq "$diagnostic" "$backup/witness-audit.log"; then + cat "$backup/witness-audit.log" >&2; exit 1; + fi + rm -f Proofs/WitnessOwnerControl.lean + restore_build +} +reject_witness missing "an omitted independent conversion" \ + "Missing arbitrary-outcome required theorem Zerocopy.Obligations.min_spec_adequate" +reject_witness wrong-kind "a definition replacing the independent theorem" \ + "Missing independent required theorem Zerocopy.Obligations.min_spec_checked" +reject_witness wrong-type "an independent theorem with an unrelated type" \ + "Zerocopy.Obligations.min_spec_checked does not prove its independent required proposition" +reject_witness wrong-owner "an independent witness declared in a proof module" \ + "Zerocopy.Obligations.min_spec_adequate must be declared in the required checks module" +reject_witness missing-rows "omitted check rows hidden by an empty legacy roster" \ + "Missing arbitrary-outcome required theorem Zerocopy.Obligations.min_spec_adequate" + +# Imported lookalikes, nested compiler helpers and mutable tags do not choose +# coverage. Only immediate proposition definitions owned by Specs are roots. +python3 - <<'PYCONTROL' +from pathlib import Path +required = Path("Required.lean") +required.write_text(required.read_text() + ''' +namespace Zerocopy.Specs +def importedScopeLookalike : Prop := True +end Zerocopy.Specs +''') +specs = Path("Specs.lean") +specs.write_text(specs.read_text() + ''' +namespace Zerocopy.Specs.min_spec +def nestedScopeHelper : Prop := True +end Zerocopy.Specs.min_spec +''') +PYCONTROL +lake build Required > "$backup/scope-helper-build.log" 2>&1 || { + cat "$backup/scope-helper-build.log" >&2; exit 1; +} +audit > "$backup/scope-helper-audit.log" 2>&1 || { + cat "$backup/scope-helper-audit.log" >&2; exit 1; +} +echo "Confirmed: scope excludes imported and nested helpers" +restore_build + +# Move valid propositions to an imported model module. Everything still builds, +# but an empty generated Specs module must not manufacture a zero-proof audit. +python3 - <<'PYCONTROL' +from pathlib import Path +other = Path("Zerocopy/ScopeControl.lean") +if other.exists(): + raise SystemExit("Compiled scope control fixture already exists") +specs = Path("Specs.lean") +other.write_bytes(specs.read_bytes()) +specs.write_text('module\npublic import Zerocopy.ScopeControl\n') +PYCONTROL +lake build Required > "$backup/empty-scope-build.log" 2>&1 || { + echo "Malformed empty compiled scope control" >&2 + cat "$backup/empty-scope-build.log" >&2; exit 1; +} +expect_failure "an empty owned specification scope" empty-scope-audit.log audit +if ! grep -Fq 'No compiled inline specifications were found' "$backup/empty-scope-audit.log"; then + cat "$backup/empty-scope-audit.log" >&2; exit 1; +fi +restore_files +rm Zerocopy/ScopeControl.lean +restore_build + +# Every present source specification must equal its owned compiled call in +# both models. Preserve valid canonical proofs/callers so omitted roots reach +# the audit; removing a generated row cannot erase a present source fence. +reject_binding_image() { + local mutation=$1 description=$2 diagnostic=$3 + python3 - "$mutation" <<'PYCONTROL' +from pathlib import Path +import json, re, sys +mutation = sys.argv[1] +manifest_path = Path("bindings.json") +manifest = json.loads(manifest_path.read_text()) +models = manifest["bindings"] +if (manifest.get("version") != 3 or manifest.get("mode") != "verified-live" + or not isinstance(models, dict) or not all( + isinstance(key, str) and isinstance(value, dict) and value.get("kind") in ("function", "type") and isinstance(value.get("raw"), str) for key, value in models.items())): + raise SystemExit("Invalid verified model bindings for binding-image controls") +keys = [key for key, value in models.items() if value["kind"] == "function" and value["raw"] == "Zerocopy.util.min"] +if len(keys) != 1: + raise SystemExit("Binding-image control requires a unique min mapping") +if mutation == "missing-map": + del models[keys[0]] + manifest_path.write_text(json.dumps(manifest, indent=2) + '\n') +elif mutation == "duplicate-map": + if [entry["raw"] for entry in models.values()].count("Zerocopy.util.max") != 1: + raise SystemExit("Binding-image duplicate control requires a unique max mapping") + models[keys[0]]["raw"] = "Zerocopy.util.max" + manifest_path.write_text(json.dumps(manifest, indent=2) + '\n') +elif mutation in ("missing-spec", "duplicate-spec"): + p = Path("Specs.lean") + s = p.read_text() + fence = re.compile(r'(?m)^check_model_inputs Zerocopy\.util\.min with 0 type parameters\n' + r'aeneas_spec_begin\n' + r'spec min_spec\b[^\n]*\n(?:(?!^aeneas_spec_(?:begin|end)\b).)*?' + r'^aeneas_spec_end\n' + r'check_spec_binding Zerocopy\.Specs\.min_spec for Zerocopy\.util\.min with 2\n', + re.S | re.M) + matches = list(fence.finditer(s)) + if len(matches) != 1 or 'for @Zerocopy.util.min ' not in matches[0].group(): + raise SystemExit("Binding-image control requires the unique complete min fence/command") + match = matches[0] + block = match.group() + if mutation == "missing-spec": + other = Path("Zerocopy/ScopeControl.lean") + marker = '@[expose] public section' + start = s.find('aeneas_spec_begin\n') + if (other.exists() or start < 0 or s.count(marker) != 1 + or s[:start].count('namespace Zerocopy.Specs\n') != 1 + or s.count('end Zerocopy.Specs\n') != 1): + raise SystemExit("Binding-image carrier module boundary no longer matches") + # Reuse this stage's module/import/open preamble without importing Specs. + other.write_text(s[:start] + block + '\nend Zerocopy.Specs\n') + s = s[:match.start()] + s[match.end():] + s = s.replace(marker, 'public import Zerocopy.ScopeControl\n' + marker) + required = Path("Required.lean") + rows = required.read_text() + for row in ('example : Zerocopy.Specs.min_spec := @Zerocopy.Proofs.min_spec\n', + 'check_contract Zerocopy.Obligations.min_spec using @Zerocopy.Proofs.min_spec\n'): + if rows.count(row) != 1: + raise SystemExit("Binding-image min checking row no longer matches") + rows = rows.replace(row, '') + required.write_text(rows) + else: + name = 'bindingImageDuplicate' + if name in s or block.count('spec min_spec') != 1: + raise SystemExit("Binding-image duplicate specification fixture already exists") + duplicate = block.replace('spec min_spec', 'spec ' + name).replace( + 'check_spec_binding Zerocopy.Specs.min_spec ', + 'check_spec_binding Zerocopy.Specs.' + name + ' ') + s = s[:match.end()] + '\n' + duplicate + s[match.end():] + p.write_text(s) +else: + raise SystemExit("Unknown binding-image control mutation") +PYCONTROL + lake build Required > "$backup/binding-image-build.log" 2>&1 || { + echo "Malformed binding-image control: $description" >&2 + cat "$backup/binding-image-build.log" >&2; exit 1; + } + expect_failure "$description" binding-image-audit.log audit + if ! grep -Eq "$diagnostic" "$backup/binding-image-audit.log"; then + cat "$backup/binding-image-audit.log" >&2; exit 1; + fi + restore_files + rm -f Zerocopy/ScopeControl.lean + restore_build + audit > "$backup/binding-image-restore-audit.log" 2>&1 || { + cat "$backup/binding-image-restore-audit.log" >&2; exit 1; + } +} +reject_binding_image missing-spec "a Rust root moved out of the owned Specs scope" \ + 'Function binding for zerocopy::util::min does not name a compiled inline specification' +reject_binding_image missing-map "a compiled root missing from the binding image" \ + 'Compiled inline specification models disagree with bindings: missing .*; unexpected .*Zerocopy[.]util[.]min' +reject_binding_image duplicate-map "two Rust bindings assigned the same model" \ + 'Model bindings repeat model Zerocopy[.]util[.]max' +reject_binding_image duplicate-spec "two owned specifications targeting the same model" \ + 'Compiled inline specifications repeat model Zerocopy[.]util[.]min' + +# Type coverage is audited by the production Check, independently of function +# checks. Keep every model and proof well formed before accepting a rejection. +reject_model_image() { + local mutation=$1 description=$2 diagnostic=$3 + python3 - "$mutation" <<'PYCONTROL' +from pathlib import Path +import json, sys +manifest_path = Path("bindings.json") +manifest = json.loads(manifest_path.read_text()) +entries = manifest.get("bindings") +if (manifest.get("version") != 3 or manifest.get("mode") != "verified-live" + or not isinstance(entries, dict)): + raise SystemExit("Invalid verified model binding table for failure controls") +mutation = sys.argv[1] +rounding = "zerocopy::layout::RoundingAlignAndPhase" +error = "zerocopy::layout::MetadataCastError" +if mutation in ("missing-authored-image", "wrong-authored-model"): + entry = entries.get(rounding) + if not entry or entry["kind"] != "type" or not entry["authored"]: + raise SystemExit("Authored model omission fixture no longer matches") + if mutation == "missing-authored-image": + del entries[rounding] + else: + if entry["model"] == entry["fields"]: + raise SystemExit("Authored model control requires a distinct mathematical model") + entry["model"] = entry["fields"] + manifest_path.write_text(json.dumps(manifest, indent=2) + '\n') +elif mutation == "missing-default-image": + entry = entries.get(error) + if not entry or entry["kind"] != "type" or entry["authored"]: + raise SystemExit("Default model omission fixture no longer matches") + del entries[error] + manifest_path.write_text(json.dumps(manifest, indent=2) + '\n') +elif mutation in ("moved-shapes", "moved-models"): + module, target = (("ModelShapes", "ShapeControl") if mutation == "moved-shapes" + else ("Models", "ModelControl")) + path = Path(module + ".lean") + other = Path("Zerocopy/" + target + ".lean") + if other.exists(): + raise SystemExit("Model ownership control fixture already exists") + other.write_bytes(path.read_bytes()) + path.write_text('module\npublic import Zerocopy.' + target + '\n') +else: + raise SystemExit("Unknown model-image control mutation") +PYCONTROL + lake build Required > "$backup/model-image-build.log" 2>&1 || { + echo "Malformed model-image control: $description" >&2 + cat "$backup/model-image-build.log" >&2; exit 1; + } + expect_failure "$description" model-image-audit.log audit + if ! grep -Eq "$diagnostic" "$backup/model-image-audit.log"; then + cat "$backup/model-image-audit.log" >&2; exit 1; + fi + restore_files + rm -f Zerocopy/ShapeControl.lean Zerocopy/ModelControl.lean + restore_build + audit > "$backup/model-image-restore-audit.log" 2>&1 || { + cat "$backup/model-image-restore-audit.log" >&2; exit 1; + } +} +# Capability selection uses the compiled source carrier, independently of the +# manifest rows whose deletion is under test. Each mutant must build first. +if grep -Eq '^(structure|inductive)[[:blank:]]+layout[.]RoundingAlignAndPhase([[:blank:]]|$)' Zerocopy/Types.lean; then + reject_model_image wrong-authored-model "an authored model associated with the wrong shape" \ + 'Compiled model .* disagrees with its Rust owner binding' + reject_model_image missing-authored-image "an authored model omitted from the binding table" \ + 'Raw nominal types disagree with source bindings: missing .*RoundingAlignAndPhase' +fi +if grep -Eq '^(structure|inductive)[[:blank:]]+layout[.]MetadataCastError([[:blank:]]|$)' Zerocopy/Types.lean; then + reject_model_image missing-default-image "an unannotated carrier omitted from the binding table" \ + 'Raw nominal types disagree with source bindings: missing .*MetadataCastError' +fi +# Without extracted nominal types these modules own no declarations, so moving +# their empty contents cannot violate declaration ownership. The baseline audit +# above checks that the binding table covers every extracted nominal type. +if has_binding type; then + reject_model_image moved-shapes "model shapes moved out of their generated module" \ + 'Mathematical fields and model must be owned by ModelShapes' + reject_model_image moved-models "decoders and providers moved out of their generated module" \ + 'Decoder and provider must be owned by Models' +fi + +# Generic alignment reads select the ABI family. No-layout and pointer-only +# models intentionally have no such declaration and retain their earlier scope. +if grep -Fq 'def core.mem.align_of' Zerocopy/FunsExternal.lean; then + # Run the actual ABI-checking source in an isolated environment. Required + # proofs need not compile for a wrong pointer-width model, and removing an + # input must reach the audit rather than merely break one of its consumers. + python3 - <<'PYCONTROL' +from pathlib import Path +s = Path("Check.lean").read_text() +begin = ' let env ← getEnv\n' +end = ' let mut proofModules : Array ModuleIdx := #[]' +if s.count(begin) != 1 or s.count(end) != 1: + raise SystemExit("External-input audit control boundaries no longer match") +body = s[s.index(begin):s.index(end)] +Path("NegativeControl.lean").write_text( + 'import Lean\nimport Zerocopy.FunsExternal\nopen Lean Elab Command\nrun_elab do\n' + body) +PYCONTROL + lake env lean -DwarningAsError=true NegativeControl.lean \ + > "$backup/abi-baseline.log" 2>&1 || { + cat "$backup/abi-baseline.log" >&2; exit 1; + } + reject_abi() { + local description=$1 mutation=$2 diagnostic=$3 + python3 - "$mutation" <<'PYCONTROL' +from pathlib import Path +import sys +p = Path("Zerocopy/FunsExternal.lean") +s = p.read_text() +def replace(old, new): + global s + if s.count(old) != 1: + raise SystemExit("External-input negative control no longer matches: " + old) + s = s.replace(old, new) +size = 'axiom Zerocopy.RustLayout.size (T : Type) : Usize' +align = 'axiom Zerocopy.RustLayout.align (T : Type) : Usize' +mutation = sys.argv[1] +if mutation in ('missing-size', 'missing-align', 'missing-both'): + if mutation != 'missing-align': + replace(size, '') + replace('Zerocopy.RustLayout.size T', '0#usize') + if mutation != 'missing-size': + replace(align, '') + replace('Zerocopy.RustLayout.align T', '1#usize') + replace('core.mem.align_of (T : Type)', 'core.mem.align_of (_T : Type)') +elif mutation == 'wrong-kind': + replace(size, 'def Zerocopy.RustLayout.size (_T : Type) : Usize := 0#usize') +elif mutation == 'wrong-signature': + replace(size, 'axiom Zerocopy.RustLayout.size (T : Type) : Nat') + replace('Zerocopy.RustLayout.size T', '0#usize') +elif mutation == 'ignored-align': + replace('Zerocopy.RustLayout.align T', '1#usize') + replace('core.mem.align_of (T : Type)', 'core.mem.align_of (_T : Type)') +elif mutation == 'ignored-size': + replace('Zerocopy.RustLayout.size T', '0#usize') +elif mutation == 'wrong-pointer-width': + replace('System.Platform.numBits / 8', 'System.Platform.numBits / 8 + 1') +else: + raise SystemExit("Unknown external-input control mutation") +p.write_text(s) +PYCONTROL + check_model_mutant "$description" Zerocopy.FunsExternal + expect_failure "$description" abi-audit.log \ + lake env lean -DwarningAsError=true NegativeControl.lean + if ! grep -Fq "$diagnostic" "$backup/abi-audit.log"; then + cat "$backup/abi-audit.log" >&2; exit 1; + fi + restore_files + } + reject_abi "a missing generic ABI size input" missing-size \ + "Missing external layout input Zerocopy.RustLayout.size" + reject_abi "a missing generic ABI alignment input" missing-align \ + "Missing external layout input Zerocopy.RustLayout.align" + reject_abi "both generic ABI inputs missing" missing-both \ + "Missing external layout input Zerocopy.RustLayout.size" + reject_abi "a definition replacing an arbitrary ABI data input" wrong-kind \ + "Missing external layout input Zerocopy.RustLayout.size" + reject_abi "an ABI data input with the wrong signature" wrong-signature \ + "External layout input Zerocopy.RustLayout.size changed its data-only signature" + reject_abi "an alignment read ignoring its ABI input" ignored-align \ + "External alignment read changed its data-input interpretation" + reject_abi "a generic size read ignoring its ABI input" ignored-size \ + "External size read changed its data-input interpretation" + reject_abi "an incorrect Usize pointer-width read" wrong-pointer-width \ + "External size read changed its data-input interpretation" + rm NegativeControl.lean + restore_build + audit +fi + +if [[ $1 == verification ]]; then + echo '-- Harmless generated-comment difference' >> Zerocopy/Funs.lean + python3 "$repo/verification/aeneas/golden.py" compare \ + "$PWD/Zerocopy" "$repo/verification/aeneas/golden" + lake build Required > "$backup/comment-build.log" 2>&1 || { + cat "$backup/comment-build.log" >&2; exit 1; + } + audit + echo "Confirmed: comment drift passes comparison and live proofs" + restore_build +fi + +# Ordinary theorems must establish the proposition expanded by the spec command. +# Native proofs use the same total/partial semantics as the inline specs. +reject_spec() { + local description=$1 + cat > NegativeControl.lean <<'LEAN' +import SpecsSyntax +import ModelPrelude +open Aeneas Aeneas.Std +LEAN + cat >> NegativeControl.lean + expect_failure "$description" spec-build.log lake env lean -DwarningAsError=true NegativeControl.lean + if ! grep -q 'unsolved goals' "$backup/spec-build.log" || + ! grep -q '⊢ False' "$backup/spec-build.log"; then + cat "$backup/spec-build.log" >&2; exit 1 + fi + rm NegativeControl.lean +} +reject_spec "panic under a total spec" <<'LEAN' +namespace NegativeControl +def panicking : Result Nat := .fail Error.panic +spec panic + for @panicking with 0 type parameters + ensures _ => True +theorem attempted : panic := by simp [panic, panicking] +end NegativeControl +LEAN +reject_spec "divergence under a total spec" <<'LEAN' +namespace NegativeControl +def divergent : Result Nat := .div +spec diverging + for @divergent with 0 type parameters + ensures _ => True +theorem attempted : diverging := by simp [diverging, divergent] +end NegativeControl +LEAN +reject_spec "panic under a partial spec" <<'LEAN' +namespace NegativeControl +def panicking : Result Nat := .fail Error.panic +partial spec panic + for @panicking with 0 type parameters + ensures _ => True +theorem attempted : panic := by simp [panic, panicking] +end NegativeControl +LEAN +reject_spec "an incorrect successful return under a partial spec" <<'LEAN' +namespace NegativeControl +def zero : Result Nat := .ok 0 +partial spec wrong_return + for @zero with 0 type parameters + ensures out => out = 1 +theorem attempted : wrong_return := by simp [wrong_return, zero, AeneasSpecs.RustModel.decode] +end NegativeControl +LEAN +reject_spec "divergence under a total mathematical-view spec" <<'LEAN' +namespace NegativeControl +def divergent : Result Nat := .div +spec diverging + for @divergent with 0 type parameters + ensures out => out = 0 +theorem attempted : diverging := by simp [diverging, divergent] +end NegativeControl +LEAN +reject_spec "an incorrect mathematical view" <<'LEAN' +namespace NegativeControl +def zero : Result Nat := .ok 0 +spec wrong_view + for @zero with 0 type parameters + ensures out => Nat.succ out = 0 + ensures(raw) out => out = 0 +theorem attempted : wrong_view := by simp [wrong_view, zero] +end NegativeControl +LEAN + + +if [[ -f Loops.lean ]]; then + [[ -f SupportTests.lean ]] || { + echo "Indexed-loop capability requires SupportTests.lean" >&2; exit 1; + } + reject_loop() { + local description=$1 + python3 - "$2" "$3" <<'PYCONTROL' +from pathlib import Path +import sys +p = Path("SupportTests.lean") +s = p.read_text() +old, new = sys.argv[1:] +if s.count(old) != 1: + raise SystemExit("Indexed-loop negative control no longer matches") +p.write_text(s.replace(old, new)) +PYCONTROL + expect_failure "$description" loop-build.log lake build SupportTests + cp "$backup/SupportTests.lean" SupportTests.lean + } + reject_loop "a stationary loop index" 's.2 + 1#usize' 's.2 + 0#usize' + reject_loop "an incorrect loop prefix step" 's.1 + 1, j' 's.1 + 2, j' + reject_loop "continuing a loop past its end" 'if s.2 < n then' 'if s.2 ≤ n then' + lake build SupportTests > "$backup/loop-restore-build.log" 2>&1 || { + cat "$backup/loop-restore-build.log" >&2; exit 1; + } +fi + +# Registered proofs are theorems of their corresponding Specs proposition. +min_module=$(python3 - <<'PYCONTROL' +from pathlib import Path +import re +paths = [Path("Proofs.lean"), *Path("Proofs").rglob("*.lean")] +old = 'theorem min_spec : Zerocopy.Specs.min_spec := by' +matched = [p for p in paths if old in p.read_text()] +if len(matched) != 1: + raise SystemExit("Specification type control no longer matches") +p = matched[0] +s = p.read_text() +registration = 'register_spec_step min_spec\n' +if s.count(old) != 1 or s.count(registration) != 1: + raise SystemExit("Specification type control requires the exact unique min theorem/registration") +start = s.index(old) +body = start + len(old) +# Native proof declarations start in column zero. Stop before the next +# declaration or registration, preserving padding/round-down in early stages. +boundary = re.search(r'(?m)^(?:(?:(?:private|noncomputable) +)?' + r'(?:theorem|def|abbrev)|attribute|register_spec_step|end)\b|^@\[', + s[body:]) +if boundary is None: + raise SystemExit("Cannot isolate the min theorem declaration") +end = body + boundary.start() +mutated = s[:start] + 'theorem min_spec : True := by trivial\n\n' + s[end:] +p.write_text(mutated.replace(registration, '')) +module = str(p.with_suffix('')).replace('/', '.') +Path('NegativeControl.lean').write_text('import ' + module + '\nimport Specs\n' + + 'example : Zerocopy.Specs.min_spec := @Zerocopy.Proofs.min_spec\n') +print(module) +PYCONTROL +) +# Compile just its owning module: callers of min may otherwise fail before +# the independent type check is reached. This is the same example as Required. +lake build "+$min_module" > "$backup/min-module-build.log" 2>&1 || { + cat "$backup/min-module-build.log" >&2; exit 1; +} +expect_failure "an unrelated registered theorem in place of the inline min spec" wrong-type-build.log lake env lean NegativeControl.lean +if ! grep -qi 'type mismatch' "$backup/wrong-type-build.log" || + ! grep -q 'min_spec' "$backup/wrong-type-build.log"; then + cat "$backup/wrong-type-build.log" >&2; exit 1 +fi +rm NegativeControl.lean +restore_build + +min_module=$(python3 - <<'PYCONTROL' +from pathlib import Path +paths = [Path("Proofs.lean"), *Path("Proofs").rglob("*.lean")] +old = 'theorem min_spec : Zerocopy.Specs.min_spec := by' +matched = [p for p in paths if old in p.read_text()] +if len(matched) != 1: + raise SystemExit("Proof declaration-kind control no longer matches") +p = matched[0] +s = p.read_text() +registration = 'register_spec_step min_spec\n' +if s.count(old) != 1 or s.count(registration) != 1: + raise SystemExit("Proof declaration-kind control requires the exact unique min theorem/registration") +p.write_text(s.replace(old, 'def min_spec : Zerocopy.Specs.min_spec := by').replace(registration, '')) +print(str(p.with_suffix('')).replace('/', '.')) +PYCONTROL +) +# Keep the definition well formed while testing the earlier theorem-kind guard. +lake build "+$min_module" > "$backup/definition-module-build.log" 2>&1 || { + cat "$backup/definition-module-build.log" >&2; exit 1; +} +expect_failure "a definition in place of a registered theorem" definition-build.log build_required +if ! grep -Fq 'Required contract candidate must be a theorem: Zerocopy.Proofs.min_spec' "$backup/definition-build.log"; then + cat "$backup/definition-build.log" >&2; exit 1 +fi +restore_build + +# Exercise dependency feedback without prescribing production proof structure. +# Select by the arithmetic extraction binding, independently of how caller +# proofs are organized. A selected missing or changed fixture must fail. +if has_model util.checks.check_arithmetic; then + audit > "$backup/dependency-baseline-audit.log" 2>&1 || { + cat "$backup/dependency-baseline-audit.log" >&2; exit 1; + } + cp proof-dependencies.json "$backup/padding-baseline.json" + padding_dependency_fixture() { + python3 - "$1" "$backup" <<'PYCONTROL' +from pathlib import Path +import sys +mode, backup = sys.argv[1], Path(sys.argv[2]) +if mode not in ('direct', 'unavailable', 'auxiliary', 'independent'): + raise SystemExit("Unknown padding dependency fixture") +caller = Path("Proofs/ArithmeticChecks.lean") +callee = Path("Proofs/Util.lean") +helper = Path("Proofs/PaddingDependencyControl.lean") +for fixture in (caller, callee, Path("Required.lean")): + if not (backup / fixture).is_file(): + raise SystemExit("Selected padding dependency fixture is missing: " + str(fixture)) +header = 'theorem arithmetic_checks_spec : Specs.arithmetic_checks_spec := by' +body = """ + intro len align _ _ _ _ + apply WP.spec_mono (Raw.arithmetic_checks len align) + intro result _ + exact ⟨(), rfl, trivial⟩ +""" +source = (backup / caller).read_text() +original = header + body +if source.count(original) != 1 or source.count(header) != 1: + raise SystemExit("Selected arithmetic dependency fixture is missing or changed") +callee_source = (backup / callee).read_text() +callee_header = 'theorem padding_lt_alignment : Zerocopy.Specs.padding_lt_alignment := by' +registration = 'register_spec_step padding_lt_alignment\n' +if callee_source.count(callee_header) != 1 or callee_source.count(registration) != 1: + raise SystemExit("Selected canonical padding fixture is missing or changed") +callee_body, separator, _ = callee_source.partition(callee_header)[2].partition('\n' + registration) +if not separator or callee_body.count('Raw.padding_lt_alignment') != 1: + raise SystemExit("Independent padding transport requires its exact Raw reference") +reference = 'Zerocopy.Proofs.padding_lt_alignment' +required = (backup / 'Required.lean').read_text() +if mode in ('auxiliary', 'independent'): + alias = 'private def padding_alias := Zerocopy.Proofs.padding_lt_alignment\n' + if mode == 'independent': + alias = ('private theorem padding_alias : Zerocopy.Specs.padding_lt_alignment := by' + + callee_body.replace('Raw.padding_lt_alignment', + 'Zerocopy.Proofs.Raw.padding_lt_alignment') + '\n') + helper.write_text("""module +public import Proofs.Util +@[expose] public section +open Aeneas Aeneas.Std AeneasSpecs Zerocopy.Proofs +namespace PaddingDependencyControl +""" + alias + """theorem padding_proxy : Zerocopy.Specs.padding_lt_alignment := by exact padding_alias +end PaddingDependencyControl +""") + import_at = '@[expose] public section' + if source.count(import_at) != 1: + raise SystemExit("Selected arithmetic module framing is missing or changed") + source = source.replace(import_at, 'public import Proofs.PaddingDependencyControl\n' + import_at) + # Check traverses configured native modules. Include this scratch helper + # in that same boundary; its namespace alone must not grant admission. + audit_header = 'def auditModuleNames : Array Name := #[' + if required.count(audit_header) != 1: + raise SystemExit("Selected audit module fixture is missing or changed") + required = required.replace(audit_header, audit_header + '`Proofs.PaddingDependencyControl, ') + reference = 'PaddingDependencyControl.padding_proxy' +wrapped = (header + '\n exact And.left (show Specs.arithmetic_checks_spec ∧ ' + 'Specs.padding_lt_alignment from\n ⟨(by\n' + + ''.join(' ' + line + '\n' for line in body.splitlines()[1:]) + + ' ), ' + reference + '⟩)\n') +caller.write_text(source.replace(original, wrapped)) +Path('Required.lean').write_text(required) +if mode == 'unavailable': + callee_source = callee_source.replace(callee_header, + 'theorem unavailable_padding_lt_alignment : Zerocopy.Specs.padding_lt_alignment := by') + callee_source = callee_source.replace(registration, '') +callee.write_text(callee_source) +PYCONTROL + } + check_padding_dependency() { + python3 - "$1" "$backup/padding-baseline.json" <<'PYCONTROL' +from pathlib import Path +import json, sys +mode = sys.argv[1] +caller = 'Zerocopy.Proofs.arithmetic_checks_spec' +callee = 'Zerocopy.Proofs.padding_lt_alignment' +def dependencies(path): + entries = [entry for entry in json.loads(Path(path).read_text()) if entry['theorem'] == caller] + if len(entries) != 1: + raise SystemExit("Dependency graph lacks its exact arithmetic caller") + return set(entries[0]['depends_on']) +baseline = dependencies(sys.argv[2]) +if callee in baseline: + raise SystemExit("Original arithmetic fixture already references the canonical padding root") +expected = baseline | {callee} if mode == 'present' else baseline +if mode not in ('present', 'absent') or dependencies('proof-dependencies.json') != expected: + raise SystemExit("Dependency graph does not match the controlled canonical padding edge") +PYCONTROL + } + padding_dependency_fixture direct + lake build Required > "$backup/dependency-direct-build.log" 2>&1 || { + cat "$backup/dependency-direct-build.log" >&2; exit 1; + } + audit > "$backup/dependency-direct-audit.log" 2>&1 || { + cat "$backup/dependency-direct-audit.log" >&2; exit 1; + } + check_padding_dependency present + echo "Confirmed: dependency reporting retains the direct canonical callee" + padding_dependency_fixture unavailable + lake build +Proofs.Util > "$backup/callee-owner-build.log" 2>&1 || { + echo "Malformed callee control: renamed owning Util module did not compile" >&2 + cat "$backup/callee-owner-build.log" >&2; exit 1; + } + expect_failure "an unavailable canonical callee theorem" callee-build.log lake build +Proofs.ArithmeticChecks + if ! grep -q 'Unknown identifier.*padding_lt_alignment' "$backup/callee-build.log"; then + cat "$backup/callee-build.log" >&2; exit 1 + fi + restore_build + padding_dependency_fixture auxiliary + lake build Required > "$backup/helper-build.log" 2>&1 || { + cat "$backup/helper-build.log" >&2; exit 1; + } + audit > "$backup/helper-audit.log" 2>&1 || { + cat "$backup/helper-audit.log" >&2; exit 1; + } + check_padding_dependency present + echo "Confirmed: dependency reporting follows a private helper in an auxiliary native module" + padding_dependency_fixture independent + lake build Required > "$backup/independent-helper-build.log" 2>&1 || { + cat "$backup/independent-helper-build.log" >&2; exit 1; + } + audit > "$backup/independent-helper-audit.log" 2>&1 || { + cat "$backup/independent-helper-audit.log" >&2; exit 1; + } + check_padding_dependency absent + echo "Confirmed: dependency reporting omits a callee replaced by an independent auxiliary proof" + rm -f Proofs/PaddingDependencyControl.lean + restore_build + audit > "$backup/dependency-restored-audit.log" 2>&1 || { + cat "$backup/dependency-restored-audit.log" >&2; exit 1; + } + check_padding_dependency absent +fi + +# Data-only Rust layout inputs are the only additional admitted axioms. Even an +# unused private helper in another configured proof module must be audited. +helper=Proofs.lean +if [[ -f Proofs/Util.lean ]]; then helper=Proofs/Util.lean; fi +cat >> "$helper" <<'LEAN' +namespace UnrelatedControl +set_option warningAsError false in +private theorem unused_helper : True := by sorry +end UnrelatedControl +LEAN +lake build Required > "$backup/private-helper-build.log" 2>&1 || { + cat "$backup/private-helper-build.log" >&2; exit 1; +} +expect_failure "an admitted unused private proof helper" private-helper-audit.log audit +if ! grep -q 'unused_helper.*unapproved axiom.*sorryAx' "$backup/private-helper-audit.log"; then + cat "$backup/private-helper-audit.log" >&2; exit 1 +fi +restore_build + +# The native-module roster covers auxiliary files too, even when neither their +# declarations nor their namespace occur in a registered proof. +cat >> SpecPrelude.lean <<'LEAN' +namespace UnrelatedControl +axiom unused_auxiliary_fact : True +end UnrelatedControl +LEAN +lake build Required > "$backup/auxiliary-axiom-build.log" 2>&1 || { + cat "$backup/auxiliary-axiom-build.log" >&2; exit 1; +} +expect_failure "an unused unapproved axiom in a native auxiliary module" auxiliary-axiom-audit.log audit +if ! grep -q 'unapproved axiom.*UnrelatedControl.unused_auxiliary_fact' "$backup/auxiliary-axiom-audit.log"; then + cat "$backup/auxiliary-axiom-audit.log" >&2; exit 1 +fi +restore_build + +cat >> Proofs.lean <<'LEAN' +namespace UnrelatedControl +axiom unused_fact : True +end UnrelatedControl +LEAN +lake build Required > "$backup/unused-axiom-build.log" 2>&1 || { + cat "$backup/unused-axiom-build.log" >&2; exit 1; +} +expect_failure "an unused unapproved axiom outside the proof namespace" unused-axiom-audit.log audit +if ! grep -q 'unapproved axiom.*UnrelatedControl.unused_fact' "$backup/unused-axiom-audit.log"; then + cat "$backup/unused-axiom-audit.log" >&2; exit 1 +fi +restore_build + +cat >> Proofs.lean <<'LEAN' +namespace UnrelatedControl +set_option warningAsError false in +theorem admitted : True := by sorry +end UnrelatedControl +LEAN +lake build Required > "$backup/sorry-build.log" 2>&1 || { + cat "$backup/sorry-build.log" >&2; exit 1; +} +expect_failure "an admitted proof" sorry-audit.log audit +if ! grep -q 'unapproved axiom.*sorryAx' "$backup/sorry-audit.log"; then + cat "$backup/sorry-audit.log" >&2; exit 1 +fi +restore_build + +# Generated conversion witnesses receive the same admission audit. +python3 - <<'PYCONTROL' +from pathlib import Path +p = Path("Required.lean") +s = p.read_text() +old = 'using @Zerocopy.Proofs.min_spec' +if s.count(old) != 1: + raise SystemExit("Required witness admission control no longer matches") +s = s.replace('check_contract Zerocopy.Obligations.min_spec ' + old, + 'set_option warningAsError false in\n' + + 'theorem Zerocopy.Obligations.min_spec_adequate :\n' + + ' contract_adequacy% Zerocopy.Specs.min_spec implies Zerocopy.Obligations.min_spec := by sorry\n' + + 'theorem Zerocopy.Obligations.min_spec_checked : Zerocopy.Obligations.min_spec :=\n' + + ' required_contract_instance% Zerocopy.Proofs.min_spec with Zerocopy.Obligations.min_spec_adequate') +p.write_text(s) +PYCONTROL +lake build Required > "$backup/required-sorry-build.log" 2>&1 || { + cat "$backup/required-sorry-build.log" >&2; exit 1; +} +expect_failure "an admitted required-contract witness" required-sorry-audit.log audit +if ! grep -q 'unapproved axiom.*sorryAx' "$backup/required-sorry-audit.log"; then + cat "$backup/required-sorry-audit.log" >&2; exit 1 +fi +restore_build + +cat > NegativeControl.lean <<'LEAN' +import Proofs +import Obligations +import RequiredContracts +namespace UnrelatedControl +theorem min_spec : True := by trivial +end UnrelatedControl +check_contract Zerocopy.Obligations.min_spec using UnrelatedControl.min_spec +LEAN +expect_failure "an unrelated True theorem as an independent required contract" unrelated-build.log lake env lean NegativeControl.lean +rm NegativeControl.lean + +# Once arithmetic postconditions are strengthened, a valid earlier weak +# round-down contract must fail the independently written required proposition. +if [[ -f Arithmetic.lean ]]; then + cat > NegativeControl.lean <<'LEAN' +import Proofs +import Obligations +import RequiredContracts +open Aeneas Aeneas.Std Zerocopy Zerocopy.Proofs +namespace WeakerControl +theorem round_down_spec (n : Usize) (align : NonZeroUsize) + (h : (align.val : Nat).isPowerOfTwo) : + util.round_down_to_next_multiple_of_alignment n align + ⦃ m => m ≤ n ∧ m.val % align.val.val = 0 ⦄ := by + apply WP.spec_mono (Zerocopy.Proofs.Raw.round_down_spec n align h) + intro m hm + exact ⟨hm.1, hm.2.2.1⟩ +end WeakerControl +LEAN + lake env lean -DwarningAsError=true NegativeControl.lean > "$backup/weaker-build.log" 2>&1 || { + cat "$backup/weaker-build.log" >&2; exit 1; + } + cat >> NegativeControl.lean <<'LEAN' +check_contract Zerocopy.Obligations.round_down_spec using WeakerControl.round_down_spec +LEAN + expect_failure "a valid weaker round-down contract as the independent required contract" weaker-required.log lake env lean NegativeControl.lean + rm NegativeControl.lean +fi + +reject_model() { + local description=$1 old=$2 new=$3 target=${4:-} + python3 - "$old" "$new" "$target" <<'PYCONTROL' +from pathlib import Path +import sys +p = Path("Zerocopy/Funs.lean") +s = p.read_text() +old, new, target = sys.argv[1:] +start, end = 0, len(s) +if target: + start = s.index('def ' + target) + end = s.find('/-- ', start) + if end < 0: + end = len(s) +part = s[start:end] +if part.count(old) != 1: + raise SystemExit("Model negative control no longer matches") +p.write_text(s[:start] + part.replace(old, new) + s[end:]) +PYCONTROL + check_model_mutant "$description" + expect_failure "$description" model-build.log build_required + cp "$backup/Zerocopy/Funs.lean" Zerocopy/Funs.lean +} +if [[ -f Arithmetic.lean ]]; then + reject_model "always-zero padding under the exact arithmetic spec" 'ok (i2 &&& mask)' 'ok 0#usize' + reject_model "always-zero round-down under the exact arithmetic spec" 'ok (n &&& mask)' 'ok 0#usize' +fi +if has_model layout.DstLayout.pad_to_align; then + # Both the earlier sized-only contract and normalized layout contract check + # this flag. Generated scalar variable names are not a capability boundary. + shallow_flag=$(python3 - <<'PYCONTROL' +from pathlib import Path +import re +s = Path("Zerocopy/Funs.lean").read_text() +start = s.index('def layout.DstLayout.pad_to_align') +end = s.find('/-- ', start) +if end < 0: + end = len(s) +matches = re.findall(r'statically_shallow_unpadded := \([A-Za-z_][A-Za-z0-9_]* = 0#usize\)', s[start:end]) +if len(matches) != 1: + raise SystemExit("Shallow-padding flag negative control no longer matches") +print(matches[0]) +PYCONTROL +) + reject_model "an incorrect shallow-padding flag" "$shallow_flag" \ + 'statically_shallow_unpadded := true' 'layout.DstLayout.pad_to_align' +fi +if has_model layout.DstLayout.extend; then + # Field extension identifies full normalized composition. Earlier stages + # contain only a sized-padding contract, without the recursive DST rule. + reject_model "conflating physical offset with the size base" \ + 'Usize.checked_add offset tsl.offset' \ + 'Usize.checked_add offset tsl.size_base' 'layout.DstLayout.extend' + reject_model "dropping inner rounding under outer packing" \ + 'util.padding_needed_for trailing.size_base size_align' \ + 'util.padding_needed_for trailing.size_base self.align' 'layout.DstLayout.pad_to_align' +fi + +restore_build + +# Independent expectations must reject a genuinely proven weaker contract. +reject_weak_outcome() { + local description=$1 + cat > NegativeControl.lean + # Check the candidate proof alone first; malformed Lean is no witness. + sed '/^check_contract /d' NegativeControl.lean > WeakOutcome.lean + lake env lean -DwarningAsError=true WeakOutcome.lean > "$backup/weak-outcome-valid.log" 2>&1 || { + cat "$backup/weak-outcome-valid.log" >&2; exit 1; + } + rm WeakOutcome.lean + expect_failure "$description" weak-outcome-check.log \ + lake env lean -DwarningAsError=true NegativeControl.lean + rm NegativeControl.lean +} +reject_predicate() { + local name=$1 replacement=$2 module=$3 + python3 - "$name" "$replacement" <<'PYCONTROL' +from pathlib import Path +import re, sys +p = Path("LayoutModel.lean") +s = p.read_text() +pattern = r'(?ms)^(def ' + re.escape(sys.argv[1]) + r'\b.*?: Prop :=).*?(?=^def )' +s, count = re.subn(pattern, lambda m: 'set_option linter.unusedVariables false in\n' + m[1] + ' ' + sys.argv[2] + '\n\n', s) +if count != 1: + raise SystemExit("Outcome predicate negative control no longer matches") +p.write_text(s) +PYCONTROL + check_model_mutant "a changed mathematical predicate $name" LayoutModel + expect_failure "a changed mathematical predicate $name" outcome-test.log \ + lake build "+$module" + if ! grep -Fq "error: $module.lean:" "$backup/outcome-test.log"; then + cat "$backup/outcome-test.log" >&2; exit 1; + fi + restore_files +} +if has_model layout.DstLayout.validate_cast_and_convert_metadata; then + [[ -f OutcomeTests.lean ]] || { + echo "Outcome capability requires OutcomeTests.lean" >&2; exit 1; + } + reject_weak_outcome "a termination-only cast contract" <<'LEAN' +import Proofs +import Obligations +import RequiredContracts +open Aeneas Aeneas.Std Zerocopy Zerocopy.Proofs AeneasContracts + +namespace CastWeakContractControls +def terminationOnly : Prop := + ∀ (self : layout.DstLayout) (addr length : Usize) (side : layout.CastType), + 0 < self.align.val.val → addr.val + length.val ≤ Usize.max → + (match self.size_info with + | .Sized _ => True + | .SliceDst t => 0 < t.size_rounding_align_and_phase._0.val.val ∧ 0 < t.elem_size.val) → + ∃ r, layout.DstLayout.validate_cast_and_convert_metadata self addr length side = .ok r + +theorem terminationOnly_proof : terminationOnly := by + intro self addr length side ha hroom ht + obtain ⟨r, hr, _⟩ := WP.spec_imp_exists + (Zerocopy.Proofs.Raw.validate_cast_spec self addr length side ha hroom ht) + exact ⟨r, hr⟩ +end CastWeakContractControls + +-- Expected rejection; delete only this line for the positive proof-validity stage. +check_contract Zerocopy.Obligations.validate_cast_spec using CastWeakContractControls.terminationOnly_proof +LEAN + reject_predicate castSpec True OutcomeTests + restore_build +fi + +if has_model layout.DstLayout.metadata_for_exact_size; then + [[ -f OutcomeTests.lean ]] || { + echo "Outcome capability requires OutcomeTests.lean" >&2; exit 1; + } + reject_weak_outcome "a termination-only metadata contract" <<'LEAN' +import Proofs +import Obligations +import RequiredContracts +open Aeneas Aeneas.Std Zerocopy Zerocopy.Proofs AeneasContracts + +namespace MetadataWeakContractControls +def terminationOnly : Prop := + ∀ (self : layout.DstLayout) (size : Usize), 0 < self.align.val.val → + (match self.size_info with + | .Sized _ => True + | .SliceDst t => t.elem_size.val ≠ 0 → 0 < t.size_rounding_align_and_phase._0.val.val) → + ∃ r, layout.DstLayout.metadata_for_exact_size self size = .ok r + +theorem terminationOnly_proof : terminationOnly := by + intro self size ha ht + obtain ⟨r, hr, _⟩ := WP.spec_imp_exists + (Zerocopy.Proofs.Raw.metadata_exact_spec self size ha ht) + exact ⟨r, hr⟩ +end MetadataWeakContractControls + +-- Expected rejection; delete only this line for the positive proof-validity stage. +check_contract Zerocopy.Obligations.metadata_exact_spec using MetadataWeakContractControls.terminationOnly_proof +LEAN + reject_predicate metadataSpec True OutcomeTests + restore_build +fi + +# Positive examples keep supported constructor preconditions inhabited. +if has_model layout.DstLayout.for_repr_c_struct; then + [[ -f LayoutModelDomainTests.lean ]] || { + echo "Constructor capability requires LayoutModelDomainTests.lean" >&2; exit 1; + } + for predicate in alignmentDomain canonicalLayout constructionDomain; do + reject_predicate "$predicate" False LayoutModelDomainTests + done + restore_build +fi + +python3 - <<'PYCONTROL' +from pathlib import Path +p = Path("Zerocopy/Funs.lean") +s = p.read_text() +if s.count("if i > i1") != 1: + raise SystemExit("Min negative control no longer matches generated output") +p.write_text(s.replace("if i > i1", "if i < i1")) +PYCONTROL +check_model_mutant "a min implementation with its comparison reversed" +if [[ $1 == verification ]]; then + if python3 "$repo/verification/aeneas/golden.py" compare \ + "$PWD/Zerocopy" "$repo/verification/aeneas/golden" \ + > "$backup/mutated-compare.log" 2>&1; then + echo "Fuzzy comparison accepted an incorrect translated min" >&2; exit 1 + fi + if ! grep -q 'Aeneas goldens differ' "$backup/mutated-compare.log"; then + cat "$backup/mutated-compare.log" >&2; exit 1 + fi + echo "Confirmed: fuzzy comparison rejects an incorrect translated min" +fi +expect_failure "a min implementation with its comparison reversed" min-build.log build_required +restore_build +lake build +audit +) +case "${1:-}" in + "") + [[ $# == 0 ]] || { echo "Usage: $0 [golden-verification|verification]" >&2; exit 1; } + check_workspace golden-verification + check_workspace verification + ;; + golden-verification|verification) + [[ $# == 1 ]] || { echo "Usage: $0 [golden-verification|verification]" >&2; exit 1; } + check_workspace "$1" + ;; + *) echo "Usage: $0 [golden-verification|verification]" >&2; exit 1 ;; +esac diff --git a/verification/aeneas/patches/use-tuple-structs.patch b/verification/aeneas/patches/use-tuple-structs.patch new file mode 100644 index 0000000000..7cdd111104 --- /dev/null +++ b/verification/aeneas/patches/use-tuple-structs.patch @@ -0,0 +1,13 @@ +--- a/src/Main.ml ++++ b/src/Main.ml +@@ -105,6 +105,10 @@ + Arg.String set_subdir, + " Extract the files in a sub-folder; this option has an impact on the \ + import paths of the generated files" ); ++ ( "-use-tuple-structs", ++ Arg.Bool (fun b -> use_tuple_structs := b), ++ " Extract structures with unnamed fields as tuples (default: true). \ ++ Pass `-use-tuple-structs false` to preserve their nominal identity." ); + ( "-use-lean-modules", + Arg.Bool (fun b -> use_lean_modules := b), + " Emit Lean files using the module system (default: true). Pass \ diff --git a/verification/aeneas/prepare.py b/verification/aeneas/prepare.py new file mode 100644 index 0000000000..acccde8731 --- /dev/null +++ b/verification/aeneas/prepare.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +"""Prepare the pinned extraction without changing its translated behavior. + +Reject error-bearing LLBC even when Charon exits successfully. Repair only the +known duplicate dictionary name, then point the Lean project at already-fetched +backend dependencies. Native imports remain responsible for module ordering. +""" + +import argparse +import json +import re +from pathlib import Path + + +def check_llbc(path): + """Require Charon's explicit success flag and an actual translated crate.""" + data = json.loads(path.read_text()) + # Charon can exit successfully even when extraction was incomplete. + if data.get("has_errors") is not False: + raise ValueError("Charon LLBC is missing has_errors: false") + if not isinstance(data.get("translated"), dict): + raise ValueError("Charon LLBC is missing the translated crate") + + +def rename_copy_fields(types, funs): + """Apply the exact naming repair only after every expected match is unique.""" + # At this exact pin ZeroablePrimitive inherits Copy for both Self and its + # associated NonZeroInner. Aeneas gives both projections the same name. + # Rename ONLY the second projection and its initializer; retain both types + # and both dictionaries. Never change the translated helper bodies. + contents = {'types': types, 'funs': funs} + edits = [ + ('types', '"markerCopyInst", "markerCopyInst"', + '"markerCopyInst", "innerCopyInst"'), + ('types', "markerCopyInst : core.marker.Copy Self_NonZeroInner", + "innerCopyInst : core.marker.Copy Self_NonZeroInner"), + ('funs', "markerCopyInst := core.num.niche_types.NonZeroUsizeInner.Insts.CoreMarkerCopy", + "innerCopyInst := core.num.niche_types.NonZeroUsizeInner.Insts.CoreMarkerCopy"), + ] + for name, old, _ in edits: + if contents[name].count(old) != 1: + raise ValueError(f"Pinned Aeneas naming workaround no longer matches: {old}") + for name, old, new in edits: + contents[name] = contents[name].replace(old, new) + return contents['types'], contents['funs'] + + +def share_manifest(workspace, backend): + """Reuse pinned packages and discover library roots from assembled modules. + + Each root is a default build target except Check, which the driver also runs + explicitly to refresh audit reports even when Lake has cached its imports. + """ + # Read the upstream lockfile and reuse the dependencies fetched into the + # backend. A separate `lake update` here would clone and cache Mathlib twice. + upstream = json.loads((backend / "lake-manifest.json").read_text()) + packages = [{ + "type": "path", "scope": "", "name": "aeneas", "inherited": False, + "dir": str(backend), "manifestFile": "lake-manifest.json", + "configFile": "lakefile.lean", + }] + for dep in upstream["packages"]: + directory = backend / ".lake/packages" / dep["name"] + if not directory.is_dir(): + raise ValueError(f"Missing pinned Lean dependency: {directory}") + packages.append({ + "type": "path", "scope": dep["scope"], "name": dep["name"], + "inherited": True, "dir": str(directory), + "manifestFile": dep["manifestFile"], "configFile": dep["configFile"], + }) + manifest = { + "version": upstream["version"], "packagesDir": ".lake/packages", + "packages": packages, "name": "zerocopyVerification", "lakeDir": ".lake", + "fixedToolchain": False, + } + (workspace / "lake-manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") + # Native proof modules use Lake's import graph. Discover their library roots + # rather than maintaining a second assembly order in this script. + libraries = {path.stem for path in workspace.glob('*.lean') + if path.name != 'lakefile.lean'} + declarations = ''.join( + ('' if name == 'Check' else '@[default_target] ') + f'lean_lib {name}\n' + for name in sorted(libraries)) + (workspace / "lakefile.lean").write_text( + 'import Lake\nopen Lake DSL\n' + f'require aeneas from {json.dumps(str(backend))}\n' + 'package zerocopyVerification where\n' + ' moreLeanArgs := #["-DwarningAsError=true"]\n' + declarations) + + +def mathlib_imports(backend, proof_sources=None): + """Collect native Mathlib imports while excluding fetched dependency trees. + + Include handwritten proof submodules when supplied. Their imports determine + which cache artifacts the workflow needs; an unrelated .lake checkout must + not enlarge that request. + """ + pattern = re.compile(r"^(?:(?:public|private|protected|meta)\s+)*import\s+(.+)$") + modules = set() + roots = [backend] if proof_sources is None else [backend, proof_sources] + for root in roots: + for path in root.rglob('*.lean'): + if ".lake" in path.relative_to(root).parts: + continue + for line in path.read_text().splitlines(): + match = pattern.match(line.strip()) + if match: + modules.update(m for m in match[1].split() if m.startswith("Mathlib.")) + if not modules: + raise ValueError("No Mathlib imports found in the pinned Aeneas backend") + return sorted(modules) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("command", choices=["llbc", "prepare", "workspace", "mathlib-imports"]) + parser.add_argument("path", type=Path) + parser.add_argument("--backend", type=Path) + parser.add_argument("--proof-sources", type=Path) + args = parser.parse_args() + if args.command == "llbc": + check_llbc(args.path) + elif args.command == "mathlib-imports": + print("\n".join(mathlib_imports(args.path, args.proof_sources))) + elif args.command == "prepare": + types = args.path / "Zerocopy/Types.lean" + funs = args.path / "Zerocopy/Funs.lean" + types_text, funs_text = rename_copy_fields(types.read_text(), funs.read_text()) + types.write_text(types_text) + funs.write_text(funs_text) + if args.command == "workspace": + if args.backend is None: + parser.error(f"{args.command} requires --backend") + share_manifest(args.path, args.backend.resolve()) + + +if __name__ == "__main__": + main() diff --git a/verification/aeneas/run.sh b/verification/aeneas/run.sh new file mode 100755 index 0000000000..f3476c6c54 --- /dev/null +++ b/verification/aeneas/run.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +# Produce a fresh Rust extraction, compare its generated modules, then check +# the authored proofs against golden and live definitions independently. A text +# match does not establish a proof, and a proof does not establish a text match. +set -euo pipefail +update_goldens=false +extract_only=false +case "${1:-}" in + "") [[ $# == 0 ]] ;; + --update-goldens) [[ $# == 1 ]]; update_goldens=true ;; + --extract-only) [[ $# == 1 ]]; extract_only=true ;; + *) echo "Usage: $0 [--update-goldens|--extract-only]" >&2; exit 1 ;; +esac +cd "$(dirname "$0")/../.." +repo=$PWD +source verification/aeneas/toolchain.sh +tools_dir=${AENEAS_TOOLCHAIN_DIR:-"$repo/target/aeneas/toolchain"} +backend="$tools_dir/backends/lean" +[[ $("$tools_dir/aeneas" -version) == "aeneas $AENEAS_VERSION" ]] +[[ $("$tools_dir/charon" toolchain-version) == "$AENEAS_RUST_TOOLCHAIN" ]] +[[ $("$tools_dir/charon" version) == "$CHARON_VERSION ($CHARON_REV)" ]] +[[ $(cat "$backend/lean-toolchain") == "$AENEAS_LEAN_TOOLCHAIN" ]] +export PATH="$tools_dir/lean/bin:$PATH" +export RUSTUP_TOOLCHAIN="$AENEAS_RUST_TOOLCHAIN" + +workspace_cmd=(python3 -B verification/aeneas/workspace.py) +initialize_workspace() { + "${workspace_cmd[@]}" initialize "$1" --root "$repo" --backend "$backend" +} +check_proofs() { + "${workspace_cmd[@]}" check "$1" --root "$repo" +} +work="$repo/target/aeneas/verification" +golden_work="$repo/target/aeneas/golden-verification" +initialize_workspace "$work" +if ! "$extract_only"; then initialize_workspace "$golden_work"; fi + +# Use a Rust parser for body ownership and a lexer for comments/literals. +CARGO_TARGET_DIR="$repo/target/aeneas/annotation-tool" \ + cargo +"$AENEAS_RUST_TOOLCHAIN" build --locked --jobs 2 \ + --manifest-path tools/Cargo.toml -p aeneas-inline +inline_tool="$repo/target/aeneas/annotation-tool/debug/aeneas-inline" +inline_cmd=(python3 -B verification/aeneas/inline.py) +inline_args=(--root "$repo" --tool "$inline_tool") +scan=scan +if "$update_goldens"; then scan=scan-update; fi +"${inline_cmd[@]}" "$scan" "${inline_args[@]}" --work "$work" +roots=$(cat "$work/roots.txt") + +# This compiles the actual zerocopy library with its normal build script and +# default features. Start from these private helpers and their dependencies; +# whole-crate unsafe verification is outside the current Aeneas safe subset. +( + cd zerocopy + CARGO_TARGET_DIR="$repo/target/aeneas/rust" \ + "$tools_dir/charon" cargo --preset=aeneas --sysroot default \ + --start-from "$roots" \ + --dest-file "$work/zerocopy.llbc" --abort-on-error --error-on-warnings \ + -- --lib --locked --offline +) +# Charon produces LLBC; the inline tool binds Rust owners to that inspected +# extraction. Aeneas then produces the function bodies used by the live proofs. +python3 verification/aeneas/prepare.py llbc "$work/zerocopy.llbc" +"${inline_cmd[@]}" bindings "${inline_args[@]}" --work "$work" +"$tools_dir/aeneas" -backend lean -namespace Zerocopy -dest "$work/Zerocopy" \ + -split-files -abort-on-error -warnings-as-errors -no-progress-bar \ + -use-tuple-structs false \ + "$work/zerocopy.llbc" +python3 verification/aeneas/prepare.py prepare "$work" +"${inline_cmd[@]}" models "${inline_args[@]}" --work "$work" +"${inline_cmd[@]}" assemble "${inline_args[@]}" --work "$work" +python3 verification/aeneas/prepare.py workspace "$work" --backend "$backend" +if "$extract_only"; then + echo "Development extraction: $work" + echo "Golden comparison and proof checks have not run." + exit 0 +fi +if "$update_goldens"; then + # Update explicitly, and only from a fresh extraction that passes proofs. + echo "Checking live model before updating goldens" + check_proofs "$work" + "${inline_cmd[@]}" update "${inline_args[@]}" --work "$work/Zerocopy" +fi +python3 verification/aeneas/golden.py compare "$work/Zerocopy" verification/aeneas/golden +"${inline_cmd[@]}" render "${inline_args[@]}" --work "$golden_work/Zerocopy" +# Reuse the audited Rust-to-Lean owner mapping, but keep the golden function +# definitions. This fixes which function each spec targets without substituting +# live bodies into the golden proof build. Fuzzy documentation cannot select a +# different function; both projects must pass their own proofs and axiom audit. +cp "$work/bindings.json" "$golden_work/bindings.json" +"${inline_cmd[@]}" assemble "${inline_args[@]}" --work "$golden_work" +python3 verification/aeneas/prepare.py workspace "$golden_work" --backend "$backend" +echo "Checking proofs and axioms against the checked-in golden model" +check_proofs "$golden_work" +echo "Checking proofs and axioms against the live Aeneas model" +check_proofs "$work" diff --git a/verification/aeneas/setup.sh b/verification/aeneas/setup.sh new file mode 100755 index 0000000000..685a92e648 --- /dev/null +++ b/verification/aeneas/setup.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +set -euo pipefail +cd "$(dirname "$0")/../.." +source verification/aeneas/toolchain.sh +mkdir -p target/aeneas + +# Downloads, patched compilation, version checks, and Mathlib cache fetching +# must all succeed before this candidate replaces the installed bundle. Only +# this invocation's temporary directory is disposable during a failed setup. +staging=$(mktemp -d "$PWD/target/aeneas/setup.XXXXXX") +trap 'rm -rf "$staging"' EXIT + +fetch() { + curl --fail --location --silent --show-error --retry 5 "$1" -o "$2" + python3 - "$2" "$3" <<'PY' +import hashlib +import sys +from pathlib import Path +digest = hashlib.sha256() +with Path(sys.argv[1]).open("rb") as archive: + for block in iter(lambda: archive.read(1024 * 1024), b""): + digest.update(block) +actual = digest.hexdigest() +if actual != sys.argv[2]: + raise SystemExit(f"SHA-256 mismatch for {sys.argv[1]}: {actual}") +PY +} + +fetch "https://github.com/AeneasVerif/aeneas/releases/download/$AENEAS_RELEASE/aeneas-$AENEAS_PLATFORM.tar.gz" \ + "$staging/aeneas.tar.gz" "$AENEAS_SHA256" +mkdir "$staging/toolchain" +tar -xzf "$staging/aeneas.tar.gz" -C "$staging/toolchain" +fetch "https://github.com/leanprover/lean4/releases/download/v$AENEAS_LEAN_VERSION/lean-$AENEAS_LEAN_VERSION-$AENEAS_LEAN_PLATFORM.tar.zst" \ + "$staging/lean.tar.zst" "$AENEAS_LEAN_SHA256" +mkdir "$staging/toolchain/lean" +tar --zstd -xf "$staging/lean.tar.zst" -C "$staging/toolchain/lean" --strip-components=1 + +[[ $("$staging/toolchain/aeneas" -version) == "aeneas $AENEAS_RELEASE" ]] +bash verification/aeneas/build-aeneas.sh "$staging/toolchain" +[[ $("$staging/toolchain/aeneas" -version) == "aeneas $AENEAS_VERSION" ]] +[[ $("$staging/toolchain/charon" toolchain-version) == "$AENEAS_RUST_TOOLCHAIN" ]] +[[ $("$staging/toolchain/charon" version) == "$CHARON_VERSION ($CHARON_REV)" ]] +[[ $(cat "$staging/toolchain/backends/lean/lean-toolchain") == "$AENEAS_LEAN_TOOLCHAIN" ]] +rustup toolchain install "$AENEAS_RUST_TOOLCHAIN" --profile minimal \ + --component rustc-dev --component rust-src + +# Fetch Mathlib imports of the backend and our proof sources, with dependencies. +# The bundle contains a manifest with pinned dependency revisions. Lake loads +# it directly; do not run `lake update` and float the dependencies. +export PATH="$staging/toolchain/lean/bin:$PATH" +python3 verification/aeneas/prepare.py mathlib-imports \ + "$staging/toolchain/backends/lean" --proof-sources verification/aeneas/lean \ + > "$staging/mathlib-imports.txt" +mathlib_modules=() +while IFS= read -r module; do + mathlib_modules+=("$module") +done < "$staging/mathlib-imports.txt" +( + cd "$staging/toolchain/backends/lean" + lake exe cache get "${mathlib_modules[@]}" + # The release includes compiled backend modules, but may omit some compiled + # dependencies. Direct Lean invocations cannot build a missing import. Let + # Lake complete the backend before installing it so extraction fixtures work + # on a fresh CI runner as well as with an existing local build cache. + lake build +Aeneas +) +rm -rf target/aeneas/toolchain +mv "$staging/toolchain" target/aeneas/toolchain diff --git a/verification/aeneas/tests/check_nominal_tuples.py b/verification/aeneas/tests/check_nominal_tuples.py new file mode 100644 index 0000000000..a6d3d3f8d0 --- /dev/null +++ b/verification/aeneas/tests/check_nominal_tuples.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +"""Check declaration identity and unchanged ordinary tuples in fresh extraction.""" +from pathlib import Path +import re +import sys + +work = Path(sys.argv[1]) +nominal = (work / 'nominal/Nominal.lean').read_text() +default = (work / 'default/Nominal.lean').read_text() +# Nominal mode must retain each Rust owner as a structure, even when owners +# share fields. Default mode intentionally retains upstream tuple aliases. +for name in ('First', 'Second', 'Empty', 'OtherEmpty', 'Generic', 'OtherGeneric', 'Pair', 'OtherPair', 'Nested', 'OtherNested'): + assert re.search(rf'^structure {name}(?:\s|$)', nominal, re.M), name + assert re.search(rf'^def {name}(?:\s|$)', default, re.M), name +for name in ('ordinary', 'ordinary_pattern', 'ordinary_unit', 'ordinary_single'): + pattern = rf'^def {name}\b.*?(?=\n/--|\nend NominalTuples)' + actual = re.search(pattern, nominal, re.M | re.S) + expected = re.search(pattern, default, re.M | re.S) + assert actual and expected and actual.group() == expected.group(), name +print('Nominal identities and ordinary tuple compatibility checked') diff --git a/verification/aeneas/tests/nominal-tuples.sh b/verification/aeneas/tests/nominal-tuples.sh new file mode 100755 index 0000000000..a3145c8dfa --- /dev/null +++ b/verification/aeneas/tests/nominal-tuples.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. +set -euo pipefail +cd "$(dirname "$0")/../../.." +source verification/aeneas/toolchain.sh +repo=$PWD +tools_dir=${AENEAS_TOOLCHAIN_DIR:-"$repo/target/aeneas/toolchain"} +upstream=${AENEAS_UPSTREAM_EXE:-"$tools_dir/aeneas-upstream"} +work="$repo/target/aeneas/nominal-tuples" +mkdir -p "$work"/{nominal,default,explicit} +export RUSTUP_TOOLCHAIN="$AENEAS_RUST_TOOLCHAIN" +( + cd verification/aeneas/tests/nominal-tuples + CARGO_TARGET_DIR="$work/rust" "$tools_dir/charon" cargo --preset=aeneas --sysroot default \ + --dest-file "$work/nominal.llbc" --abort-on-error --error-on-warnings \ + -- --lib --locked --offline +) +extract() { + local executable=$1 destination=$2 + shift 2 + "$executable" -backend lean -namespace NominalTuples -dest "$destination" \ + -abort-on-error -warnings-as-errors -no-progress-bar "$@" "$work/nominal.llbc" +} +# Only false changes the representation of tuple structs. Explicit true, +# the patched default, and the unpatched release must agree byte for byte. +# Ordinary Rust tuples must agree across both representation modes as well. +extract "$tools_dir/aeneas" "$work/nominal" -use-tuple-structs false +extract "$tools_dir/aeneas" "$work/default" +extract "$tools_dir/aeneas" "$work/explicit" -use-tuple-structs true +diff -u "$work/default/Nominal.lean" "$work/explicit/Nominal.lean" +[[ -x "$upstream" ]] || { echo "Missing upstream comparison binary: $upstream" >&2; exit 1; } +mkdir -p "$work/upstream" +extract "$upstream" "$work/upstream" +diff -u "$work/upstream/Nominal.lean" "$work/default/Nominal.lean" +python3 verification/aeneas/tests/check_nominal_tuples.py "$work" +# Read only the installed backend's compiled imports; all test artifacts live here. +export LEAN_PATH +LEAN_PATH=$(python3 - "$tools_dir/backends/lean" "$work/nominal" <<'PYLEAN' +from pathlib import Path +import os, sys +backend = Path(sys.argv[1]) +paths = [str(backend / '.lake/build/lib/lean')] +paths += [str(p) for p in sorted((backend / '.lake/packages').glob('*/.lake/build/lib/lean'))] +print(os.pathsep.join([sys.argv[2], *paths])) +PYLEAN +) +# Text checks establish names and unchanged output. These compilations also +# establish that the generated declarations elaborate and support the intended +# distinct nominal providers, including empty and generic tuple structs. +"$tools_dir/lean/bin/lean" -j1 -DwarningAsError=true -o "$work/nominal/Nominal.olean" "$work/nominal/Nominal.lean" +"$tools_dir/lean/bin/lean" -j1 -DwarningAsError=true verification/aeneas/tests/nominal-tuples/Check.lean +LEAN_PATH="$work/default:${LEAN_PATH#*:}" +"$tools_dir/lean/bin/lean" -j1 -DwarningAsError=true -o "$work/default/Nominal.olean" "$work/default/Nominal.lean" +"$tools_dir/lean/bin/lean" -j1 -DwarningAsError=true verification/aeneas/tests/nominal-tuples/Default.lean +echo "Nominal tuple extraction fixtures: $work" diff --git a/verification/aeneas/tests/nominal-tuples/Cargo.lock b/verification/aeneas/tests/nominal-tuples/Cargo.lock new file mode 100644 index 0000000000..c7ba6b168d --- /dev/null +++ b/verification/aeneas/tests/nominal-tuples/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "nominal-tuples" +version = "0.0.0" diff --git a/verification/aeneas/tests/nominal-tuples/Cargo.toml b/verification/aeneas/tests/nominal-tuples/Cargo.toml new file mode 100644 index 0000000000..8cbcd5caf0 --- /dev/null +++ b/verification/aeneas/tests/nominal-tuples/Cargo.toml @@ -0,0 +1,15 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +[package] +name = "nominal-tuples" +version = "0.0.0" +edition = "2021" +publish = false + +[workspace] diff --git a/verification/aeneas/tests/nominal-tuples/Check.lean b/verification/aeneas/tests/nominal-tuples/Check.lean new file mode 100644 index 0000000000..b3155f24ce --- /dev/null +++ b/verification/aeneas/tests/nominal-tuples/Check.lean @@ -0,0 +1,50 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +import Nominal + +open Aeneas Aeneas.Std Result NominalTuples + +example : True := by + fail_if_success have : First = Second := rfl + fail_if_success have : NominalTuples.Empty = OtherEmpty := rfl + fail_if_success have : Generic Nat = OtherGeneric Nat := rfl + fail_if_success have : Pair Nat = OtherPair Nat := rfl + fail_if_success have : Nested Nat = OtherNested Nat := rfl + trivial + +example (x : First) : x = x := by + fail_if_success have : Second := x + fail_if_success have : Std.U32 := x + rfl + +example (x : Std.U32) : construct x = ok (First.mk x, Second.mk x) := rfl +example (x y : Std.U32) : project (First.mk x) (Second.mk y) = ok (x, y) := rfl +example (x y : Std.U32) : pattern (First.mk x) (Second.mk y) = ok (x, y) := rfl +example : empty = ok (Empty.mk, OtherEmpty.mk) := rfl +example : empty_pattern Empty.mk OtherEmpty.mk = ok () := rfl +example {T : Type} (x : T) : generic x = ok (Generic.mk x) := rfl +example {T : Type} (x : T) : other_generic x = ok (OtherGeneric.mk x) := rfl +example {T : Type} (x : T) : generic_pattern (Generic.mk x) = ok x := rfl +example {T : Type} (x : T) (y : Std.U32) : pair x y = ok (Pair.mk x y) := rfl +example {T : Type} (x : T) (y : Std.U32) : pair_pattern (Pair.mk x y) = ok (x, y) := rfl +example {T : Type} (x : T) (y z : Std.U32) : + update (Pair.mk x y) z = ok (Pair.mk x z) := rfl +example {T : Type} (x : T) (y : Std.U32) : + nested x y = ok (Nested.mk (First.mk y) (Second.mk y) (Generic.mk x)) := rfl +example {T : Type} (x : T) (a b : Std.U32) : + nested_pattern (Nested.mk (First.mk a) (Second.mk b) (Generic.mk x)) = ok (a, b, x) := rfl +example (x : Std.U32) (y : Std.U64) : ordinary x y = ok (x, y) := rfl +example (x : Std.U32) (y : Std.U64) : ordinary_pattern (x, y) = ok (y, x) := rfl +example : ordinary_unit () = ok () := rfl +example {T : Type} (x : T) (y : Std.U32) : other_pair x y = ok (OtherPair.mk x y) := rfl +example {T : Type} (x : T) (y : Std.U32) : other_pair_pattern (OtherPair.mk x y) = ok (x, y) := rfl +example {T : Type} (x : T) (y : Std.U32) : + other_nested x y = ok (OtherNested.mk (First.mk y) (Second.mk y) (Generic.mk x)) := rfl +example {T : Type} (x : T) (a b : Std.U32) : + other_nested_pattern (OtherNested.mk (First.mk a) (Second.mk b) (Generic.mk x)) = ok (a, b, x) := rfl diff --git a/verification/aeneas/tests/nominal-tuples/Default.lean b/verification/aeneas/tests/nominal-tuples/Default.lean new file mode 100644 index 0000000000..39984f927c --- /dev/null +++ b/verification/aeneas/tests/nominal-tuples/Default.lean @@ -0,0 +1,19 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +import Nominal + +open Aeneas Aeneas.Std NominalTuples + +example : First = Second := rfl +example : NominalTuples.Empty = OtherEmpty := rfl +example : Generic Nat = OtherGeneric Nat := rfl +example : Pair Nat = OtherPair Nat := rfl +example : Nested Nat = OtherNested Nat := rfl +example (x : First) : Second := x +example (x : Std.U32) : First := x diff --git a/verification/aeneas/tests/nominal-tuples/src/lib.rs b/verification/aeneas/tests/nominal-tuples/src/lib.rs new file mode 100644 index 0000000000..2dc3ef6205 --- /dev/null +++ b/verification/aeneas/tests/nominal-tuples/src/lib.rs @@ -0,0 +1,95 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +#![no_std] + +#[repr(transparent)] +pub struct First(pub u32); +pub struct Second(pub u32); +pub struct Empty(); +pub struct OtherEmpty(); +pub struct Generic(pub T); +pub struct OtherGeneric(pub T); +pub struct Pair(pub T, pub u32); +pub struct OtherPair(pub T, pub u32); +pub struct Nested(pub First, pub Second, pub Generic); +pub struct OtherNested(pub First, pub Second, pub Generic); + +pub fn construct(x: u32) -> (First, Second) { + (First(x), Second(x)) +} +pub fn project(x: First, y: Second) -> (u32, u32) { + (x.0, y.0) +} +pub fn pattern(x: First, y: Second) -> (u32, u32) { + let First(a) = x; + let Second(b) = y; + (a, b) +} +pub fn empty() -> (Empty, OtherEmpty) { + (Empty(), OtherEmpty()) +} +pub fn empty_pattern(x: Empty, y: OtherEmpty) { + let Empty() = x; + let OtherEmpty() = y; +} +pub fn generic(x: T) -> Generic { + Generic(x) +} +pub fn other_generic(x: T) -> OtherGeneric { + OtherGeneric(x) +} +pub fn generic_pattern(x: Generic) -> T { + let Generic(a) = x; + a +} +pub fn pair(x: T, y: u32) -> Pair { + Pair(x, y) +} +pub fn other_pair(x: T, y: u32) -> OtherPair { + OtherPair(x, y) +} +pub fn other_pair_pattern(x: OtherPair) -> (T, u32) { + let OtherPair(a, b) = x; + (a, b) +} +pub fn pair_pattern(x: Pair) -> (T, u32) { + let Pair(a, b) = x; + (a, b) +} +pub fn update(mut x: Pair, y: u32) -> Pair { + x.1 = y; + x +} +pub fn nested(x: T, y: u32) -> Nested { + Nested(First(y), Second(y), Generic(x)) +} +pub fn nested_pattern(x: Nested) -> (u32, u32, T) { + let Nested(First(a), Second(b), Generic(c)) = x; + (a, b, c) +} +pub fn ordinary(x: u32, y: u64) -> (u32, u64) { + (x, y) +} +pub fn ordinary_pattern(x: (u32, u64)) -> (u64, u32) { + let (a, b) = x; + (b, a) +} +pub fn ordinary_unit(x: ()) { + let () = x; +} +pub fn other_nested(x: T, y: u32) -> OtherNested { + OtherNested(First(y), Second(y), Generic(x)) +} +pub fn other_nested_pattern(x: OtherNested) -> (u32, u32, T) { + let OtherNested(First(a), Second(b), Generic(c)) = x; + (a, b, c) +} +pub fn ordinary_single(x: T) -> (T,) { + (x,) +} diff --git a/verification/aeneas/tests/rounding_model_controls.py b/verification/aeneas/tests/rounding_model_controls.py new file mode 100644 index 0000000000..d8718c0a03 --- /dev/null +++ b/verification/aeneas/tests/rounding_model_controls.py @@ -0,0 +1,223 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +"""Challenge rounding operation promises and input coverage with valid decoders. + +The constant decoder still constructs a constrained mathematical value but loses +the constructor's promised pair. The rejecting decoder excludes an independently +fixed valid getter input. Both probes first pass with the production decoder; +negative-controls.sh also compiles each mutated decoder before checking failure. +Only a diagnostic inside the intended theorem counts as semantic rejection. +""" +import argparse +import json +from pathlib import Path +import re + +OWNER = 'Zerocopy.layout.RoundingAlignAndPhase' +PREFIX = f'derive_rust_model {OWNER} with 0 type parameters' +CONSTANT = PREFIX + """ decode self => + { align := 1, phase := 0, align_pow2 := AeneasSpecs.model_power_of_two 0, + phase_lt := by decide, + fits := by + have positive := self._0.positive + have bound : self._0.value ≤ Usize.max := by + simpa only [UScalar.max, Usize.max, Usize.numBits, UScalarTy.Usize_numBits_eq] + using self._0.bound + omega } +""" +REJECT = PREFIX + ' decode? self =>\n none\n' +PROBES = {'constant': ('RoundingConstructorControl', 'constructor_promise'), + 'none': ('RoundingDomainControl', 'positive_input_domain')} + + +def constructor_spec(): + """Reuse the source-owned fence verbatim, without its helper import closure.""" + source = Path('Specs.lean').read_text() + fences = re.findall(r'^aeneas_spec_begin\n(.*?)^aeneas_spec_end$', + source, re.MULTILINE | re.DOTALL) + matches = [body for body in fences if re.match(r'spec encoding_new_spec\b', body)] + if len(matches) != 1 or not re.search( + r'for @?' + re.escape(OWNER) + r'[.]new with 0 type parameters\b', matches[0]): + raise ValueError('Missing unique source-owned rounding constructor promise') + return matches[0] + + +def write_probes(): + """Write isolated Lean consumers of the real constructor fence and decoder. + + The fixed raw input and successful raw result prevent changed admission from + making the promises vacuous. Imports supply ordinary decoder dependencies; + representation helper lemmas cannot replace the operation being challenged. + """ + header = '''module +public import Zerocopy.Funs +public import Models +public import SpecsSyntax +@[expose] public section +open Aeneas Aeneas.Std AeneasSpecs +set_option linter.unusedSimpArgs false +''' + constructor = header + '''namespace RoundingConstructorControl +-- This is the unchanged generated constructor fence, in an isolated namespace. +''' + constructor_spec() + ''' +def align : core.num.nonzero.NonZero Usize core.num.niche_types.NonZeroUsizeInner := ⟨2#usize⟩ +def phase : Usize := 1#usize +def code : Zerocopy.layout.RoundingAlignAndPhase := ⟨⟨3#usize⟩⟩ +-- Pin the actual successful result independently of the decoder. +theorem constructor_result : Zerocopy.layout.RoundingAlignAndPhase.new align phase = .ok code := by + have pow : (2 : Nat).isPowerOfTwo := by simpa using model_power_of_two 1 + rcases System.Platform.numBits_eq with bits | bits <;> + simp [Zerocopy.layout.RoundingAlignAndPhase.new, align, phase, code, + core.num.nonzero.NonZero.get, core.num.nonzero.NonZero.new, + core.num.Usize.is_power_of_two, UScalar.is_power_of_two, + UScalar.lt_equiv, UScalar.eq_equiv, UScalar.val_or, massert, lift, pow, bits] +-- The arbitrary-outcome contract checks both output admission and pair meaning. +theorem constructor_promise : encoding_new_spec_contract align phase (.ok code) := by + simp [encoding_new_spec_contract, WP.spec, WP.dspec, RustModel.decode, + Zerocopy.layout.RoundingAlignAndPhase.aeneasModel, + Zerocopy.layout.RoundingAlignAndPhase.decode, + Zerocopy.layout.RoundingAlignAndPhase.decodeFields, + modelNonZeroUScalar, modelUScalar, unsignedWord, align, phase, code] +end RoundingConstructorControl +''' + domain = header + '''namespace RoundingDomainControl +def code : Zerocopy.layout.RoundingAlignAndPhase := ⟨⟨3#usize⟩⟩ +-- Independently fix a positive input required by the getter domain. A shared +-- rejecting decoder cannot turn this promised raw input into a vacuous premise. +theorem positive_input_domain : isValid code := by + simp [isValid, RustModel.decode, + Zerocopy.layout.RoundingAlignAndPhase.aeneasModel, + Zerocopy.layout.RoundingAlignAndPhase.decode, + Zerocopy.layout.RoundingAlignAndPhase.decodeFields, + modelNonZeroUScalar, modelUScalar, unsignedWord, code] +end RoundingDomainControl +''' + for module, source in [('RoundingConstructorControl', constructor), + ('RoundingDomainControl', domain)]: + path = Path(module + '.lean') + if path.exists(): + raise ValueError(f'Rounding operation control fixture already exists: {module}') + Path('RoundingConstructorControl.lean').write_text(constructor) + Path('RoundingDomainControl.lean').write_text(domain) + + +def capability(): + """Select controls from the audited owner binding, not from helper names. + + Earlier stack stages may lack this model. Once its owner or decoder is + present, missing or changed binding evidence is an error, never a skip. + """ + manifest = json.loads(Path('bindings.json').read_text()) + entries = manifest.get('bindings') + if manifest.get('version') != 3 or manifest.get('mode') != 'verified-live' or not isinstance(entries, dict): + raise ValueError('Invalid verified binding table for rounding model controls') + owner = entries.get('zerocopy::layout::RoundingAlignAndPhase') + if owner is not None and not isinstance(owner, dict): + raise ValueError('Invalid production rounding owner binding') + selected = bool(owner and owner.get('kind') == 'type' and owner.get('raw') == OWNER + and owner.get('model') == OWNER + '.RoundingValue') + if not selected and (owner is not None or (Path('Models.lean').exists() + and PREFIX in Path('Models.lean').read_text())): + raise ValueError('Rounding operation controls lost their production model binding') + if selected: + for name in ('Models', 'ModelShapes', 'Specs', 'SpecsSyntax', 'Zerocopy/Funs'): + if not Path(name + '.lean').is_file(): + raise ValueError(f'Missing selected rounding model control prerequisite: {name}') + return selected + + +def mutate(kind): + """Replace only the unique owner's decoder, preserving sibling providers.""" + if kind not in ('constant', 'none'): + raise ValueError('Unknown rounding decoder mutation') + path = Path('Models.lean') + text = path.read_text() + end_marker = f'check_model_binding {OWNER} with 0 type parameters' + if text.count(PREFIX) != 1 or text.count(end_marker) != 1: + raise ValueError('Rounding decoder control boundaries no longer match') + start = text.index(PREFIX) + end = text.index(end_marker) + if end <= start or re.search(r'^(?:derive_rust_model|check_model_binding|namespace|end|' + r'def|abbrev|instance|theorem|structure|inductive)\b', + text[start + len(PREFIX):end], re.MULTILINE): + raise ValueError('Rounding decoder control boundaries contain another declaration') + # Keep every generated sibling, raw owner, and child provider untouched. + path.write_text(text[:start] + (CONSTANT if kind == 'constant' else REJECT) + text[end:]) + + +def check_failure(kind, log_path): + """Accept only recognized proof failures within the selected probe theorem. + + Inspect all errors, not merely the first expected one. An unrelated import, + parser, or kernel failure invalidates the evidence even when the semantic + probe also fails. Accept both native Lean and Lake diagnostic locations. + """ + if kind not in ('constant', 'none'): + raise ValueError('Unknown rounding decoder mutation') + module, theorem = PROBES[kind] + lines = Path(module + '.lean').read_text().splitlines() + starts = [i + 1 for i, line in enumerate(lines) if re.search(r'\btheorem\s+' + theorem + r'\b', line)] + if len(starts) != 1: + raise ValueError(f'Missing rounding behavior probe: {module}.{theorem}') + start = starts[0] + end = next((i + 1 for i, line in enumerate(lines) if i + 1 > start + and re.match(r'^(?:@\[[^]]*\]\s*)?(?:theorem|def|abbrev)\s', line)), len(lines) + 1) + text = re.sub(r'\x1b\[[0-9;]*m', '', Path(log_path).read_text()) + # A parser, import, missing dependency, or unknown kernel constant is never + # evidence that the independent mathematical proposition rejects a decoder. + invalid = (r'unknown (?:identifier|constant|module)', r'(?:invalid|failed to) import', + r'object file .*does not exist', r'unexpected token', + r'environment already contains', r'failed to synthesize', + r'kernel (?:exception|error)', r'could not resolve import') + if any(re.search(marker, text, re.IGNORECASE) for marker in invalid): + raise ValueError('Rounding mutant failed outside its operation promise or domain') + # Lean's native renderer puts the severity after the location; Lake puts it + # before the location. Parse every error, including unlocated errors, so a + # semantic failure cannot hide a broken import or kernel failure. + severity = r'error(?:\([^)]*\))?' + native = re.compile(r'^(.+[.]lean):(\d+):\d+: ' + severity + r': (.*)$') + lake = re.compile(r'^' + severity + r': (.+[.]lean):(\d+):\d+: (.*)$') + summaries = ('error: Lean exited with code 1', 'error: build failed') + errors = [] + for line in text.splitlines(): + match = native.match(line) or lake.match(line) + if match: + file, location, diagnostic = match.groups() + errors.append((Path(file).name, int(location), diagnostic)) + elif re.search(r'\berror(?:\([^)]*\))?:', line) and line not in summaries: + raise ValueError('Unrecognized error in rounding semantic rejection log') + expected = ('unsolved goals', 'Application type mismatch', 'Type mismatch', 'Tactic') + if not errors or any(file != module + '.lean' or not start <= line < end + or not diagnostic.startswith(expected) + for file, line, diagnostic in errors): + raise ValueError('Rounding mutant failed outside its operation promise or domain') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('operation', choices=('capability', 'write-probes', 'mutate', 'check-failure')) + parser.add_argument('kind', nargs='?', choices=('constant', 'none')) + parser.add_argument('log', nargs='?') + args = parser.parse_args() + if args.operation == 'capability': + print('yes' if capability() else 'no') + elif args.operation == 'write-probes': + write_probes() + elif args.operation == 'mutate': + if args.kind is None: + parser.error('mutate requires a decoder mutation') + mutate(args.kind) + else: + if args.kind is None or args.log is None: + parser.error('check-failure requires a mutation and log') + check_failure(args.kind, args.log) + + +if __name__ == '__main__': + main() diff --git a/verification/aeneas/tests/test_copy_back.py b/verification/aeneas/tests/test_copy_back.py new file mode 100644 index 0000000000..1ff4070329 --- /dev/null +++ b/verification/aeneas/tests/test_copy_back.py @@ -0,0 +1,393 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import contextlib +import copy +import io +import json +import shutil +from pathlib import Path +import stat +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import copy_back +import inline +import workspace +from test_inline import BLOCK, ENTRY, GENERATED, ROOT, RUST, fixture + +TYPE = 'zerocopy::util::Box' +MODEL = '/// ```aeneas\n/// model BoxValue := Fields\n/// decode self => self\n/// ```\n' +TYPES = ('module\nnamespace Zerocopy\n/-- [zerocopy::util::Box]\n Source: x -/\n' + 'structure util.Box (T : Type) where\n val : T\n\nend Zerocopy\n') + + +class CopyBackTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.work = self.root / 'work' + self.source = self.root / ENTRY['file'] + + def generate(self, source=None, second=False, model=False): + if source is None: + source = (MODEL + 'struct Box(T);\n' if model else '') + BLOCK + 'fn f(x: usize) {}\n' + if second: + source += BLOCK.replace('f_spec', 'g_spec') + 'fn g(x: usize) {}\n' + annotations = fixture(self.root, source) + (self.work / 'Zerocopy').mkdir(parents=True, exist_ok=True) + generated = GENERATED + if second: + generated = generated.replace('\nend Zerocopy', '\n' + GENERATED.split('namespace Zerocopy\n')[1].replace( + 'zerocopy::util::f', 'zerocopy::util::g').replace('util.f', 'util.g').split('\nend Zerocopy')[0] + '\nend Zerocopy') + (self.work / 'Zerocopy/Funs.lean').write_text(generated) + if model: + (self.work / 'Zerocopy/Types.lean').write_text(TYPES) + lean = self.root / 'verification/aeneas/lean' + lean.mkdir(parents=True, exist_ok=True) + (lean / 'RequiredContracts.lean').write_text('') + inline.save_bindings(self.root, annotations, self.work, development=True) + inline.assemble(self.root, annotations, self.work, development=True) + return annotations + + def edit(self, module, old, new): + path = self.work / f'{module}.lean' + text = path.read_text() + self.assertIn(old, text) + path.write_text(text.replace(old, new, 1)) + + def files(self): + return {str(p.relative_to(self.root)): p.read_bytes() for p in self.root.rglob('*') if p.is_file()} + + def run_copy(self, owner=RUST, apply=False, **kwargs): + with contextlib.redirect_stdout(io.StringIO()) as output: + diff = copy_back.copy_back(self.root, self.work, owner, apply=apply, **kwargs) + return diff, output.getvalue() + + def fails_without_writes(self, owner=RUST, message=None): + before = self.files() + with self.assertRaisesRegex(ValueError, message or '.'): + self.run_copy(owner, apply=True) + self.assertEqual(before, self.files()) + + def test_spec_line_count_preview_apply_and_native_regeneration_round_trip(self): + self.generate() + self.edit('Specs', ' ensures r => r = x', ' requires positive : x > 0\n ensures r =>\n r = x + 1') + edited = (self.work / 'Specs.lean').read_bytes() + before = self.files() + diff, printed = self.run_copy() + self.assertEqual(diff, printed) + self.assertIn('+/// requires positive : x > 0', diff) + self.assertEqual(before, self.files()) + self.run_copy(apply=True) + self.assertIn(b'/// r = x + 1\n', self.source.read_bytes()) + workspace.protect_projections(self.work) + self.generate(self.source.read_bytes().decode()) + self.assertEqual(edited, (self.work / 'Specs.lean').read_bytes()) + + def test_shape_and_decoder_are_one_annotation_and_leave_other_owner_dirty(self): + self.generate(model=True) + self.edit('ModelShapes', 'model BoxValue := Fields', 'model BoxValue where\n val : T\n extra : Nat') + self.edit('Models', 'decode self =>\n self', 'decode? self =>\n some { val := self.val, extra := 0 }') + self.edit('Specs', 'r = x', 'r ≠ x') + specs = (self.work / 'Specs.lean').read_bytes() + before = self.source.read_bytes() + self.run_copy(TYPE, apply=True) + source = self.source.read_bytes() + self.assertTrue(source.endswith(before[before.index(BLOCK.encode()):])) + self.assertIn(b'/// extra : Nat\n/// decode? self =>', source) + self.assertEqual(specs, (self.work / 'Specs.lean').read_bytes()) + with self.assertRaisesRegex(ValueError, 'Refusing to overwrite'): + workspace.protect_projections(self.work) + self.run_copy(apply=True) + workspace.protect_projections(self.work) + edited = {m: (self.work / f'{m}.lean').read_bytes() for m in workspace.PROJECTIONS} + self.generate(self.source.read_bytes().decode(), model=True) + self.assertEqual(edited, {m: (self.work / f'{m}.lean').read_bytes() for m in workspace.PROJECTIONS}) + + def test_unicode_crlf_indented_docs_and_mode_preserve_every_other_byte(self): + source = '// 雪 documentation\n' + ''.join(' ' + line for line in BLOCK.splitlines(keepends=True)) + ' fn f(x: usize) {}\n' + source = source.replace('\n', '\r\n') + annotations = self.generate(source) + span = annotations[RUST]['copy_span'] + self.source.chmod(0o640) + before = self.source.read_bytes() + self.edit('Specs', 'r = x', 'r ≠ x\n -- 雪') + self.run_copy(apply=True) + after = self.source.read_bytes() + self.assertEqual(after[:span['start']], before[:span['start']]) + self.assertTrue(after.endswith(before[span['end']:])) + self.assertIn(' /// -- 雪\r\n'.encode(), after) + self.assertNotIn(b'\n', after.replace(b'\r\n', b'')) + self.assertEqual(stat.S_IMODE(self.source.stat().st_mode), 0o640) + + def test_two_function_owners_shift_spans_without_adopting_other_edits(self): + self.generate(second=True) + self.edit('Specs', 'r = x', 'r ≠ x\n ∧ True') + self.edit('Specs', 'spec g_spec\n for @Zerocopy.util.g with 0 type parameters\n ensures r => r = x', + 'spec g_spec\n for @Zerocopy.util.g with 0 type parameters\n ensures r => r = x + 2') + lean = (self.work / 'Specs.lean').read_bytes() + self.run_copy(apply=True) + self.assertIn(b'/// ensures r => r = x\n', self.source.read_bytes()) + self.assertEqual(lean, (self.work / 'Specs.lean').read_bytes()) + with self.assertRaisesRegex(ValueError, 'Refusing to overwrite'): + workspace.protect_projections(self.work) + self.run_copy('zerocopy::util::g', apply=True) + workspace.protect_projections(self.work) + self.assertIn(b'r = x + 2', self.source.read_bytes()) + + def test_generated_application_import_check_and_derive_edits_rejected(self): + changes = [('Specs', ' for @Zerocopy.util.f', ' for @Zerocopy.util.g'), + ('Specs', 'public import Models', 'public import Other'), + ('Specs', 'check_model_inputs', 'check_other_inputs'), + ('Specs', 'check_spec_binding', 'check_other_binding'), + ('ModelShapes', 'type parameters begin', 'type parameters begin\n-- scratch'), + ('Models', 'derive_rust_model Zerocopy.util.Box', 'derive_rust_model Zerocopy.util.Other'), + ('Models', 'check_model_binding', 'check_other_binding')] + self.generate(model=True) + baseline = self.files() + for module, old, new in changes: + with self.subTest(module=module, old=old): + self.edit(module, old, new) + self.fails_without_writes(TYPE if module != 'Specs' else RUST) + for relative, contents in baseline.items(): + (self.root / relative).write_bytes(contents) + + def test_moving_generated_for_target_is_rejected_without_writes(self): + self.generate() + application = ' for @Zerocopy.util.f with 0 type parameters' + self.edit('Specs', application + '\n ensures r => r = x', + ' ensures r => r = x\n' + application) + self.fails_without_writes(message='immediately before the first specification clause') + + def test_added_binder_lines_move_canonical_for_target_and_round_trip(self): + self.generate() + application = ' for @Zerocopy.util.f with 0 type parameters' + self.edit('Specs', 'spec f_spec\n' + application, + 'spec f_spec\n (ghost : Nat)\n' + application) + projected = (self.work / 'Specs.lean').read_bytes() + self.run_copy(apply=True) + self.assertIn(b'/// spec f_spec\n/// (ghost : Nat)\n/// ensures', self.source.read_bytes()) + workspace.protect_projections(self.work) + self.generate(self.source.read_bytes().decode()) + self.assertEqual(projected, (self.work / 'Specs.lean').read_bytes()) + + def test_specification_trailing_blank_edit_is_rejected_without_poisoning_regeneration(self): + self.generate() + original = (self.work / 'Specs.lean').read_bytes() + self.edit('Specs', ' ensures r => r = x\n-- aeneas-copy-end', + ' ensures r => r = x\n\n-- aeneas-copy-end') + self.fails_without_writes(message='remove trailing blank lines') + (self.work / 'Specs.lean').write_bytes(original) + self.generate(self.source.read_bytes().decode()) + self.assertEqual(original, (self.work / 'Specs.lean').read_bytes()) + + def test_inline_decoder_edit_is_rejected_with_normalization_advice(self): + self.generate(model=True) + self.edit('Models', 'decode self =>\n self', 'decode self => self') + self.fails_without_writes(TYPE, message='body on separate lines indented by two spaces') + + def test_original_rust_spec_trailing_blanks_remain_readable_and_noop_preserved(self): + source = BLOCK.replace('/// ```\n', '/// \n/// \n/// ```\n') + 'fn f(x: usize) {}\n' + self.generate(source) + before = self.files() + self.run_copy(apply=True) + self.assertEqual(before, self.files()) + self.generate(source) + self.assertEqual(self.source.read_bytes(), source.encode()) + workspace.protect_projections(self.work) + + def test_model_trailing_blank_edits_are_rejected_before_writes(self): + self.generate(model=True) + for module in ('ModelShapes', 'Models'): + with self.subTest(module=module): + original = (self.work / f'{module}.lean').read_bytes() + self.edit(module, '\n-- aeneas-copy-end', '\n\n-- aeneas-copy-end') + self.fails_without_writes(TYPE, message='remove trailing blank lines') + (self.work / f'{module}.lean').write_bytes(original) + + def test_nested_fake_duplicate_and_missing_markers_rejected(self): + self.generate() + original = (self.work / 'Specs.lean').read_bytes() + begin = f'-- aeneas-copy-begin {RUST} spec\n' + end = f'-- aeneas-copy-end {RUST} spec\n' + for change in (begin + begin, '-- aeneas-copy-begin fake spec\n', '-- aeneas-copy-begin ' + RUST + ' shape\n', ''): + with self.subTest(change=change): + (self.work / 'Specs.lean').write_bytes(original.replace(begin.encode(), change.encode())) + self.fails_without_writes() + (self.work / 'Specs.lean').write_bytes(original + begin.encode() + end.encode()) + self.fails_without_writes() + + def test_unsupported_doc_forms_keep_read_support_and_fail_copy(self): + for source in ('/**\n```aeneas\nspec f_spec\n ensures r => r = x\n```\n*/\nfn f(x: usize) {}\n', + '#[doc = "```aeneas\\nspec f_spec\\n ensures r => r = x\\n```"]\nfn f(x: usize) {}\n'): + with self.subTest(source=source): + self.generate(source) + self.fails_without_writes(message='other doc forms are read-only') + + def test_stale_rust_and_malformed_metadata_have_no_writes(self): + self.generate() + self.edit('Specs', 'r = x', 'r ≠ x') + original = self.source.read_bytes() + self.source.write_bytes(original + b'// independent Rust edit\n') + self.fails_without_writes(message='Rust source changed') + self.source.write_bytes(original) + path = self.work / workspace.PROJECTION_STATE + state = json.loads(path.read_bytes()) + variants = [[], {'version': 1}, {**state, 'version': 3}, {**state, 'projections': {}}, {**state, 'sources': {}}] + wrong_span = copy.deepcopy(state) + wrong_span['owners'][RUST]['copy_span']['start'] += 1 + variants.append(wrong_span) + missing_owner = copy.deepcopy(state) + missing_owner['owners'][RUST]['file'] = '../escape.rs' + variants.append(missing_owner) + for variant in variants: + with self.subTest(variant=variant): + path.write_text(json.dumps(variant)) + self.fails_without_writes() + path.write_text('{') + self.fails_without_writes(message='Malformed') + + def test_interruption_after_rust_write_recovers_then_retries_idempotently(self): + self.generate() + self.edit('Specs', 'r = x', 'r ≠ x\n ∧ True') + control = (self.work / workspace.PROJECTION_STATE).read_bytes() + projections = {m: (self.work / f'{m}.lean').read_bytes() for m in workspace.PROJECTIONS} + with self.assertRaisesRegex(RuntimeError, 'interrupted'): + self.run_copy(apply=True, after_source_write=lambda: (_ for _ in ()).throw(RuntimeError('interrupted'))) + self.assertIn('r ≠ x'.encode(), self.source.read_bytes()) + self.assertEqual(control, (self.work / workspace.PROJECTION_STATE).read_bytes()) + self.assertEqual(projections, {m: (self.work / f'{m}.lean').read_bytes() for m in workspace.PROJECTIONS}) + self.run_copy(apply=True) + workspace.protect_projections(self.work) + before = self.files() + self.run_copy(apply=True) + self.assertEqual(before, self.files()) + + def test_symlink_files_and_parent_directories_rejected(self): + self.generate() + self.edit('Specs', 'r = x', 'r ≠ x') + for target in (self.source, self.work / 'Specs.lean', self.work / workspace.PROJECTION_STATE): + with self.subTest(target=target): + moved = target.with_name(target.name + '.real') + target.rename(moved) + target.symlink_to(moved) + self.fails_without_writes(message='symlink') + target.unlink() + moved.rename(target) + parent = self.source.parent + moved = parent.with_name('real-util') + parent.rename(moved) + parent.symlink_to(moved, target_is_directory=True) + self.fails_without_writes(message='symlink') + + def test_source_maps_are_diagnostic_only_and_cannot_redirect_an_edit(self): + self.generate() + self.edit('Specs', 'r = x', 'r ≠ x') + unrelated = self.root / 'zerocopy/src/unrelated.rs' + unrelated.write_text('unique unrelated Rust source\n') + for module in workspace.PROJECTIONS: + (self.work / f'{module}.source-map.json').write_text(json.dumps({ + '1': {'file': 'zerocopy/src/unrelated.rs', 'line': 1}})) + self.run_copy(apply=True) + self.assertIn('r ≠ x'.encode(), self.source.read_bytes()) + self.assertEqual(unrelated.read_text(), 'unique unrelated Rust source\n') + + def test_malformed_authored_payload_is_rejected_before_writes(self): + self.generate(model=True) + originals = {module: (self.work / f'{module}.lean').read_bytes() for module in workspace.PROJECTIONS} + for module, old, new, owner in [ + ('Specs', ' ensures r => r = x', ' theorem bad : True := by trivial', RUST), + ('Specs', ' ensures r => r = x', ' ensures r = x', RUST), + ('Models', 'decode self =>\n self', 'decode self =>\n self\ndecode other => other', TYPE), + ('ModelShapes', 'model BoxValue := Fields', 'model Fields := Fields', TYPE)]: + with self.subTest(module=module, new=new): + self.edit(module, old, new) + self.fails_without_writes(owner) + (self.work / f'{module}.lean').write_bytes(originals[module]) + + def test_noop_has_no_writes_even_for_inline_decoder_spelling(self): + self.generate(model=True) + before = self.files() + self.run_copy(TYPE, apply=True) + self.run_copy(apply=True) + self.assertEqual(before, self.files()) + + def test_input_changes_immediately_before_replace_abort_without_rust_write(self): + self.generate() + self.edit('Specs', 'r = x', 'r ≠ x') + source = self.source.read_bytes() + original_replace = copy_back.replace + + def raced(record, contents, guard): + (self.work / 'Specs.lean').write_text('changed during planning') + return original_replace(record, contents, guard) + + with mock.patch.object(copy_back, 'replace', side_effect=raced), self.assertRaisesRegex(ValueError, 'input changed'): + self.run_copy(apply=True) + self.assertEqual(source, self.source.read_bytes()) + self.assertFalse(list(self.source.parent.glob('.aeneas-copy-*'))) + + def test_v1_protection_and_clean_regeneration_upgrade(self): + self.generate() + workspace.record_projections(self.work) + workspace.protect_projections(self.work) + self.fails_without_writes(message='version 2') + self.generate(self.source.read_bytes().decode()) + self.assertEqual(json.loads((self.work / workspace.PROJECTION_STATE).read_bytes())['version'], 2) + self.run_copy(apply=True) + + def test_real_dev_script_previews_and_applies_with_system_bash(self): + self.generate() + self.edit('Specs', 'r = x', 'r ≠ x') + project = self.root / 'verification/aeneas/lean' + shutil.copytree(self.work, project, dirs_exist_ok=True) + scripts = project.parent + for name in ('dev.sh', 'workspace.py', 'copy_back.py', 'inline.py', 'golden.py'): + shutil.copyfile(ROOT / 'verification/aeneas' / name, scripts / name) + # No toolchain or extraction driver is supplied: both routes must read + # this existing project directly, even with macOS's system Bash 3.2. + command = ['/bin/bash', str(scripts / 'dev.sh'), '--copy-back', RUST] + before = self.files() + preview = subprocess.run([*command, '--dry-run'], capture_output=True, text=True) + self.assertEqual(preview.returncode, 0, preview.stderr) + self.assertIn('+/// ensures r => r ≠ x', preview.stdout) + self.assertEqual(before, self.files()) + applied = subprocess.run(command, capture_output=True, text=True) + self.assertEqual(applied.returncode, 0, applied.stderr) + self.assertEqual(preview.stdout, applied.stdout) + after = self.files() + self.assertEqual({name for name in before if before[name] != after[name]}, + {ENTRY['file'], 'verification/aeneas/lean/' + workspace.PROJECTION_STATE}) + self.assertIn('r ≠ x'.encode(), self.source.read_bytes()) + workspace.protect_projections(project) + + def test_cli_preview_and_incompatible_dev_flags(self): + self.generate() + self.edit('Specs', 'r = x', 'r ≠ x') + before = self.files() + result = subprocess.run([sys.executable, '-B', str(ROOT / 'verification/aeneas/workspace.py'), + 'copy-back', str(self.work), '--root', str(self.root), '--owner', RUST], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('+/// ensures r => r ≠ x', result.stdout) + self.assertEqual(before, self.files()) + for extra in ('--live', '--check'): + result = subprocess.run(['bash', str(ROOT / 'verification/aeneas/dev.sh'), '--copy-back', RUST, extra], + capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('cannot be combined', result.stderr) + + +if __name__ == '__main__': + unittest.main() diff --git a/verification/aeneas/tests/test_golden.py b/verification/aeneas/tests/test_golden.py new file mode 100644 index 0000000000..ef79960b67 --- /dev/null +++ b/verification/aeneas/tests/test_golden.py @@ -0,0 +1,97 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +"""Check the comparator's admission boundary independently of Lean builds. + +These fixtures distinguish permitted presentation changes from code, literal, +and module-roster changes. A passing comparator test cannot replace compilation +of the golden and live proof projects. +""" + +import importlib.util +import tempfile +import unittest +from pathlib import Path + +spec = importlib.util.spec_from_file_location( + "golden", Path(__file__).parents[1] / "golden.py") +golden = importlib.util.module_from_spec(spec) +spec.loader.exec_module(golden) + + +class GoldenTests(unittest.TestCase): + def test_accepts_comments_blank_lines_and_trailing_whitespace(self): + before = 'def min := do\n if x > y\n then ok y\n else ok x\n' + after = ('-- generated source moved\n\n' + '/-- source location /- nested comment -/ -/\n' + 'def min := do \t\n if x > y -- explanation\n\n' + ' then ok y\n else ok x\n') + self.assertEqual(golden.normalize(before), golden.normalize(after)) + self.assertEqual(golden.normalize('x/- short -/y'), + golden.normalize('x/- much longer -/y')) + self.assertNotEqual(golden.normalize('x/- comment -/y'), + golden.normalize('xy')) + + def test_preserves_code_structure_and_literal_contents(self): + before = 'def min := do\n if x > y\n then ok y\n else ok x\n' + for after in [before.replace('>', '<'), before.replace('ok y', 'ok x'), + before.replace(' then', ' then'), + before.replace('if x', 'if x'), + before.replace('then ok y\n else', 'then ok y else')]: + self.assertNotEqual(golden.normalize(before), golden.normalize(after)) + for literal in ['"-- /- spaces -/"', '"a\\\"--b"', "'\"'", + '«-- /- spaces -/»', '"a\n\n \nb"']: + self.assertEqual(golden.normalize('def s := ' + literal), + ['def s := ' + part if i == 0 else part + for i, part in enumerate(literal.split('\n'))]) + self.assertNotEqual(golden.normalize('def s := "a "'), + golden.normalize('def s := "a "')) + self.assertNotEqual(golden.normalize('def s := "a\n\nb"'), + golden.normalize('def s := "a\nb"')) + + def test_rejects_malformed_or_unsupported_literals(self): + for source in ['/- unclosed', '"unclosed', '«unclosed', '"escape\\', + 'r#"raw"#', 's!"interpolated"']: + with self.assertRaises(ValueError): + golden.normalize(source) + + def test_full_file_set_and_template_signatures_are_checked(self): + with tempfile.TemporaryDirectory() as tmp: + live, snapshot = Path(tmp) / 'live', Path(tmp) / 'golden' + live.mkdir() + for name in golden.FILES: + (live / name).write_text('def x := 1\n') + for name in golden.HANDWRITTEN: + (live / name).write_text('def external := 2\n') + golden.update(live, snapshot) + self.assertEqual(golden.compare(live, snapshot), []) + self.assertTrue((snapshot / 'Types.lean').read_text().startswith('/- Copyright')) + original = {name: (snapshot / name).read_bytes() for name in golden.FILES} + (live / 'Funs.lean').write_text('def x := "unclosed\n') + with self.assertRaises(ValueError): + golden.update(live, snapshot) + self.assertEqual(original, + {name: (snapshot / name).read_bytes() for name in golden.FILES}) + (live / 'Funs.lean').write_text('def x := 1\n') + (live / 'TypesExternal_Template.lean').write_text('def x := 2\n') + self.assertTrue(golden.compare(live, snapshot)) + (live / 'Types.lean').unlink() + with self.assertRaises(ValueError): + golden.compare(live, snapshot) + (live / 'Types.lean').write_text('def x := 1\n') + (live / 'New.lean').write_text('def extra := 1\n') + with self.assertRaises(ValueError): + golden.compare(live, snapshot) + (live / 'New.lean').unlink() + (snapshot / 'Unexpected.lean').write_text('def extra := 1\n') + with self.assertRaises(ValueError): + golden.compare(live, snapshot) + + +if __name__ == '__main__': + unittest.main() diff --git a/verification/aeneas/tests/test_import_graph.py b/verification/aeneas/tests/test_import_graph.py new file mode 100644 index 0000000000..bb5e1f5060 --- /dev/null +++ b/verification/aeneas/tests/test_import_graph.py @@ -0,0 +1,60 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +"""Compile harmless direct and transitive imports with the installed Lean. + +Import reachability supplies dependency evidence, not semantic independence. +The arbitrary-outcome contract checks carry the latter obligation separately. +""" + +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[3] +TOOLCHAIN = Path(os.environ.get('AENEAS_TOOLCHAIN_DIR', str(ROOT / 'target/aeneas/toolchain'))) +LEAN = TOOLCHAIN / 'lean/bin/lean' + + +@unittest.skipUnless(LEAN.is_file(), 'requires installed pinned Lean') +class CompiledImportGraphTests(unittest.TestCase): + def test_harmless_function_imports_are_ordinary_acyclic_dependencies(self): + # Importing a function does not establish an operation expectation. The + # arbitrary-outcome adequacy checks provide that semantic boundary. + check = (ROOT / 'verification/aeneas/lean/Check.lean').read_text() + self.assertNotIn('ModelSupport cannot depend', check) + with tempfile.TemporaryDirectory(prefix='aeneas-import-graph-') as directory: + work = Path(directory) + env = dict(os.environ, LEAN_PATH=str(work)) + + def compile_module(name, imports, body=''): + path = work / (name.replace('.', '/') + '.lean') + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text('module\n' + ''.join('public import ' + imported + '\n' for imported in imports) + body) + result = subprocess.run([str(LEAN), '-o', str(path.with_suffix('.olean')), str(path)], + cwd=work, env=env, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + compile_module('ModelShapes', []) + compile_module('Zerocopy.Funs', [], '@[expose] public def harmless (x : Nat) := x\n') + compile_module('Zerocopy.FunsExternal', [], '@[expose] public def externalData := 3\n') + for imported in ('Zerocopy.Funs', '«Zerocopy».Funs', '«Zerocopy».FunsExternal'): + for transitive in (False, True): + with self.subTest(imported=imported, transitive=transitive): + compile_module('SafeMath', [imported] if transitive else ['ModelShapes']) + compile_module('ModelSupport', ['SafeMath'] if transitive else [imported]) + compile_module('ModelSupport.Nested', [imported]) + compile_module('Consumer', ['ModelSupport', 'ModelSupport.Nested'], + 'example : harmless 2 = 2 := rfl\n' if 'FunsExternal' not in imported else + 'example : externalData = 3 := rfl\n') + + +if __name__ == '__main__': + unittest.main() diff --git a/verification/aeneas/tests/test_inline.py b/verification/aeneas/tests/test_inline.py new file mode 100644 index 0000000000..86e1c8bb25 --- /dev/null +++ b/verification/aeneas/tests/test_inline.py @@ -0,0 +1,766 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import copy +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +sys.path.insert(0, str(ROOT / 'verification/aeneas')) +import inline + +TOOL = Path(os.environ.get('AENEAS_INLINE_TOOL', ROOT / 'target/aeneas/annotation-tool/debug/aeneas-inline')) +BLOCK = ('/// ```aeneas\n' + '/// spec f_spec\n' + '/// ensures r => r = x\n' + '/// ```\n') +ENTRY = {'file': 'zerocopy/src/util/mod.rs', 'syntax': 'f'} +RUST = 'zerocopy::util::f' +GENERATED = ('module\nnamespace Zerocopy\n' + '/-- [zerocopy::util::f]:\n Source: location -/\n' + 'def util.f (x : Nat) :=\n x + 1\n\nend Zerocopy\n') + + +def syntax(source): + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / 'source.rs' + path.write_bytes(source.encode()) + result = subprocess.run([str(TOOL)], input=json.dumps([str(path)]), + text=True, capture_output=True) + if result.returncode: + raise ValueError(result.stderr) + return json.loads(result.stdout)[str(path)] + + +def fixture(root, source=None): + source = BLOCK + 'fn f(x: usize) {}\n' if source is None else source + path = root / ENTRY['file'] + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(source) + return inline.discover(root, TOOL) + + +def llbc(annotation, source): + return {'translated': { + 'files': [{'id': 1, 'name': {'Local': 'src/util/mod.rs'}, 'contents': source}], + 'fun_decls': [{'generics': {'types': [], 'const_generics': [], 'trait_clauses': []}, + 'signature': {'inputs': ['type']}, + 'body': {'Structured': {'locals': {'arg_count': 1, 'locals': [ + {'name': None}, {'name': 'x'}]}}}, + 'item_meta': { + 'name': [{'Ident': [part, 0]} for part in RUST.split('::')], + 'started_from': True, 'is_local': True, + 'span': {'Untagged': {'generated_from_span': None, 'data': { + 'file_id': 1, 'beg': {'line': 1, 'col': 0}, + 'end': {'line': annotation['end_line'], 'col': annotation['end_col']}}}}}}]}} + + +class InlineTests(unittest.TestCase): + def parse(self, source): + return inline.parse_file(source, syntax(source)) + + def test_doc_ownership_crlf_unicode_attributes_and_source_map(self): + for eol in ('\n', '\r\n'): + source = ('const 雪: u8 = 0;\n/// Normal documentation.\n' + BLOCK + + '#[cfg(any())]\n#[allow(unused)]\nfn f(x: usize) {}').replace('\n', eol) + found = self.parse(source) + self.assertEqual(len(found), 1) + self.assertEqual(found[0]['syntax'], 'f') + self.assertEqual(found[0]['kind'], 'function') + self.assertEqual(found[0]['spec_text'], 'spec f_spec\n ensures r => r = x\n') + self.assertEqual(found[0]['inputs'], ['x']) + self.assertEqual(found[0]['ghosts'], []) + self.assertEqual(found[0]['source_lines'], [4, 5]) + self.assertEqual(found[0]['newline'], eol) + + def test_source_snapshot_cannot_change_between_inspection_and_parsing(self): + original = BLOCK + 'fn f(x: usize) {}' + inspected = syntax(original) + changed = original.replace('r = x', 'r ≠ x') + with self.assertRaisesRegex(ValueError, 'Rust source changed during annotation inspection'): + inline.parse_file(changed, inspected) + del inspected['source'] + with self.assertRaisesRegex(ValueError, 'Rust source changed during annotation inspection'): + inline.parse_file(original, inspected) + + def test_rejects_body_fences_orphans_unsupported_owners_and_duplicates(self): + for source in ('fn f(x: usize) {\n' + BLOCK.replace('///', '//') + '}', + 'fn outer() { ' + BLOCK + 'fn f(x: usize) {} }', + 'impl Trait for S { ' + BLOCK + 'fn f(x: usize) {} }', + BLOCK + 'const X: usize = 1;', + BLOCK + 'type S = usize;', + BLOCK + 'mod m {}', + BLOCK + BLOCK + 'fn f(x: usize) {}'): + with self.subTest(source=source), self.assertRaises(ValueError): + self.parse(source) + + def test_direct_include_str_is_rejected_in_owner_doc_expressions(self): + for owner in ('fn f(x: usize) {}', 'struct Box(usize);', 'enum Box { Empty }'): + for attr in ('#[doc = include_str!("contract.md")]', + '#[doc = std::include_str!("contract.md")]', + '#[doc = concat!("ordinary ", include_str!("contract.md"))]', + '#[doc = condVersionLink!(std::include_str!("contract.md"))]', + '#[cfg_attr(any(), doc = include_str!("contract.md"))]', + '#[cfg_attr(any(), cfg_attr(all(), doc = wrapper!(::std::include_str!("contract.md"))))]'): + with self.subTest(owner=owner, attr=attr), self.assertRaisesRegex( + ValueError, 'Direct include_str!'): + self.parse(attr + '\n' + owner) + # Visibility and cfg documentation flags do not supply computed strings. + self.assertEqual(self.parse('#[doc(hidden)]\n#[cfg_attr(any(), doc(hidden))]\nfn f() {}'), []) + + def test_unannotated_computed_docs_are_outside_the_annotation_language(self): + for owner in ('fn f(x: usize) {}', 'struct Box(usize);', 'enum Box { Empty }'): + for attr in ('#[doc = concat!("ordinary ", "documentation")]', + '#[doc = condVersionLink!("version", "link")]', + '#[cfg_attr(any(), doc = condVersionLink!("version", "link"))]', + '#[cfg_attr(any(), cfg_attr(all(), doc = other_macro!("ordinary")))]', + '#[doc = codegen_header!("h5", "f")]', + '#[doc = codegen_section!(f, mutable = true)]', + '#[doc = concat!("include_str!(external text)")]'): + with self.subTest(owner=owner, attr=attr): + self.assertEqual(self.parse(attr + '\n' + owner), []) + + def test_computed_docs_cannot_mix_with_literal_function_or_type_annotations(self): + type_block = '/// ```aeneas\n/// model BoxValue := Fields\\ndecode self => self\n/// ```\n' + for owner, block in (('fn f(x: usize) {}', BLOCK), + ('struct Box(usize);', type_block), + ('enum Box { Empty }', type_block)): + for attr in ('#[doc = concat!("ordinary", " documentation")]', + '#[doc = condVersionLink!("version", "link")]', + '#[cfg_attr(any(), doc = condVersionLink!("version", "link"))]'): + for docs in (attr + '\n' + block, block + attr + '\n'): + with self.subTest(owner=owner, docs=docs), self.assertRaisesRegex( + ValueError, 'Computed doc strings'): + self.parse(docs + owner) + # Macro output supplies no source annotation; an empty extraction fails. + with tempfile.TemporaryDirectory() as tmp, self.assertRaisesRegex( + ValueError, 'No Aeneas specifications discovered'): + fixture(Path(tmp), '#[doc = contract_docs!()]\nfn f(x: usize) {}') + + def test_literal_docs_are_owned_and_conditional_computed_and_inner_docs_rejected(self): + source = '#[doc = "```aeneas\\nspec f_spec\\n ensures r => r = x\\n```"]\nfn f(x: usize) {}' + self.assertEqual(self.parse(source)[0]['spec_text'], 'spec f_spec\n ensures r => r = x\n') + for owner in ('struct Box(usize);', 'enum Box { Empty }'): + source = '#[doc = "```aeneas\\nmodel BoxValue := Fields\\ndecode self => self\\n```"]\n' + owner + self.assertEqual(self.parse(source)[0]['theorem'], 'BoxValue') + for attr in ('#[cfg_attr(any(), doc = "```aeneas")]', + '#[doc = concat!("```", "aeneas")]', + '#[doc = concat!("```ae", "neas")]', + '#[cfg_attr(any(), doc = concat!("```ae", "neas"))]', + '#[doc = condVersionLink!("```ae", "neas")]', + '#[cfg_attr(any(), doc = condVersionLink!("```", "ae", "neas"))]', + '#[doc = include_str!("```aeneas")]', + '#![doc = "```aeneas"]'): + for owner in ('fn f(x: usize) {}', 'struct Box(usize);', 'enum Box { Empty }'): + with self.subTest(attr=attr, owner=owner), self.assertRaises(ValueError): + self.parse(attr + '\n' + owner) + + def test_block_doc_fences_and_non_doc_literal_lookalikes(self): + for comment in ('/**\n```aeneas\nspec f_spec\n ensures r => r = x\n```\n*/', + '/**\n * ```aeneas\n * spec f_spec\n * ensures r => r = x\n * ```\n */'): + self.assertEqual(self.parse(comment + '\nfn f(x: usize) {}')[0]['inputs'], ['x']) + self.assertEqual(self.parse('#[cfg_attr(any(), arbitrary = "```aeneas")] fn f() {}'), []) + + def test_inherent_and_generic_ownership_automatically_introduces_rust_names(self): + found = self.parse('mod m { impl S { ' + BLOCK + 'fn f(&self, x: usize) {} } }') + self.assertEqual(found[0]['syntax'], 'm::S::f') + self.assertEqual(found[0]['generics'], ['E']) + for implementation in ('impl S', 'impl Trait for S', 'impl m::S'): + with self.assertRaises(ValueError): + self.parse(implementation + ' { ' + BLOCK + 'fn f(x: usize) {} }') + for signature in ('fn f((x, y): (usize, usize)) {}', 'fn f(x: &mut usize) {}', + "fn f() -> &'static usize {}", 'fn f() {}'): + with self.assertRaises(ValueError): + self.parse(BLOCK + signature) + with self.assertRaises(ValueError): + self.parse('impl S { ' + BLOCK + 'fn f(&mut self, x: usize) {} }') + + def test_ghosts_and_requirements_cannot_shadow_original_parameters(self): + for header in ('(x : Nat)', '(T : Type)', '(ghost ghost : Nat)'): + block = BLOCK.replace('spec f_spec', 'spec f_spec ' + header) + with self.subTest(header=header), self.assertRaises(ValueError): + self.parse(block + 'fn f(x: T) {}') + block = BLOCK.replace('spec f_spec', 'spec f_spec (ghost : Nat)') + found = self.parse(block + 'fn f(x: T) {}')[0] + self.assertEqual(found['generics'], ['T']) + self.assertEqual(found['ghosts'], ['ghost']) + for requirements in ('/// requires x : True\n', '/// requires T : True\n', + '/// requires h : True\n/// requires h : True\n', + '/// aeneas_spec_end\n', '/// aeneas_spec_begin\n'): + block = BLOCK.replace('/// ensures', requirements + '/// ensures') + with self.subTest(requirements=requirements), self.assertRaises(ValueError): + self.parse(block + 'fn f(x: T) {}') + + def test_nominal_type_models_and_wrong_declaration_kinds(self): + block = ('/// ```aeneas\n/// model BoxValue := Fields\n' + '/// decode self => self\n/// ```\n') + for declaration, kind in (('struct Box(T);', 'struct'), ('enum Box { Some(T), None }', 'enum')): + found = self.parse(block + declaration)[0] + self.assertEqual(found['kind'], 'type') + self.assertEqual(found['nominal_kind'], kind) + self.assertEqual(found['generics'], ['T']) + self.assertEqual(found['decoder_text'], 'self') + self.assertEqual(found['value_binder'], 'self') + for source in (BLOCK + 'struct Box(T);', block + 'fn f(x: usize) {}', + block + 'type Box = usize;', block + 'union Box { x: usize }', + block.replace('decode self', 'decode T') + 'struct Box(T);'): + with self.subTest(source=source), self.assertRaises(ValueError): + self.parse(source) + + def test_plain_raw_clauses_multiple_posts_and_dependent_ghosts(self): + text = ('spec f_spec (position : Fin self.length)\n' + ' requires valid : position.val < self.length\n' + ' requires(raw) bounded : self.val < 10\n' + ' ensures out => out = self\n' + ' ensures(raw) out => out.val = self.val\n') + self.assertEqual(inline.parse_spec(text, [], ['self']), ('f_spec', 1)) + for bad in (text.replace('requires valid', 'requires(raw) self'), + text + ' requires late : True\n', + text.replace('ensures(raw)', 'ensures(other)'), + text.replace('requires(raw)', 'requires(other)'), + text.replace(' ensures out => out = self\n', '' ).replace(' ensures(raw) out => out.val = self.val\n', ''), + text + ' refines id to 0\n'): + with self.subTest(text=bad), self.assertRaises(ValueError): + inline.parse_spec(bad, [], ['self']) + + def test_model_shapes_decoders_and_extra_command_rejection(self): + for shape in ('model BoxValue := Fields', + 'model BoxValue where\n value : TModel\n positive : True'): + for mode in ('decode', 'decode?'): + text = shape + '\n' + mode + ' self =>\n self\n' + parsed = inline.parse_model(text, ['T']) + self.assertEqual(parsed['shape_text'], shape) + self.assertEqual(parsed['decoder_mode'], mode) + self.assertEqual(parsed['decoder_text'], 'self') + for command in ('axiom extra : False', 'def extra := 1', 'end', + 'namespace Extra', 'open Classical', 'set_option maxRecDepth 0', + 'model Extra := Nat', 'decode self => self', 'decode? self => none'): + with self.subTest(command=command), self.assertRaises(ValueError): + inline.parse_model(text + ' ' + command + '\n', ['T']) + nested = inline.parse_model('model BoxValue := Nat\ndecode self =>\n let word := self._0\n let align := word + 1\n align\n', []) + self.assertEqual(nested['decoder_text'], 'let word := self._0\nlet align := word + 1\nalign') + self.assertEqual(nested['decoder_line'], 2) + for bad in ('model BoxValue := Fields\n', + 'model BoxValue := Fields\ndecode self =>\n', + 'model BoxValue := Fields\ndecode T => T\n', + 'model T := Fields\ndecode self => self\n', + 'model Fields := Fields\ndecode self => self\n', + 'invariant old for self => True\n', + 'model BoxValue := Fields\ndecode self => self\nextra command\n'): + with self.subTest(text=bad), self.assertRaises(ValueError): + inline.parse_model(bad, ['T']) + + def test_unannotated_functions_impls_and_macros_are_permitted(self): + extras = ('fn missing() {}', '#[cfg(any())] fn inactive() {}', + 'impl S { fn missing() {} methods!(); }', + '#[cfg(any())] impl S { #[cfg(any())] methods!(); }', + 'impl S { fn missing() {} }', 'impl crate::S {}', + 'impl Trait for S { methods!(); }', + '#[cfg(any())] mod m { impl S {} }', 'global_methods!();') + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + for extra in extras: + with self.subTest(extra=extra): + annotations = fixture(root, BLOCK + 'fn f(x: usize) {}\n' + extra) + self.assertEqual(set(annotations), {RUST}) + + def test_discovery_snapshots_unannotated_sources_and_roots_only_annotations(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + annotations = fixture(root, 'struct S; impl S { ' + BLOCK + 'fn f(self) {} }') + other = root / 'zerocopy/src/other.rs' + other.write_text('#[cfg(any())] impl S { fn missed() {} }\nimpl Other { methods!(); }') + ordinary = root / 'ordinary.rs' + ordinary.write_text('fn unannotated() {}') + annotations = inline.discover(root, TOOL) + self.assertEqual(set(annotations), {'zerocopy::util::S::f'}) + self.assertEqual(set(annotations.source_snapshots), {ENTRY['file'], 'zerocopy/src/other.rs'}) + self.assertEqual([a['rust'] for a in annotations.types], ['zerocopy::util::S']) + work = root / 'work' + work.mkdir() + inline.write_scan(annotations, work) + self.assertEqual((work / 'roots.txt').read_text(), 'zerocopy::util::S::f\n') + self.assertEqual(json.loads((work / 'source-types.json').read_text()), annotations.types) + # Literal lookalikes outside the crate are inspected without becoming + # roots; a real reserved fence there has no supported source owner. + ordinary.write_text('const TEXT: &str = "```aeneas";') + annotations = inline.discover(root, TOOL) + self.assertIn('ordinary.rs', annotations.source_snapshots) + ordinary.write_text(BLOCK + 'fn outside(x: usize) {}') + with self.assertRaisesRegex(ValueError, 'Unsupported annotated source location'): + inline.discover(root, TOOL) + + def test_present_nominal_model_adds_only_its_exact_type_root(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + model = '/// ```aeneas\n/// model SValue := Fields\n/// decode self => self\n/// ```\n' + source = model + 'struct S;\n' + BLOCK + 'fn f(x: usize) {}' + annotations = fixture(root, source) + self.assertEqual(set(annotations), {'zerocopy::util::S', RUST}) + work = root / 'work' + work.mkdir() + inline.write_scan(annotations, work) + self.assertEqual((work / 'roots.txt').read_text(), RUST + ',zerocopy::util::S\n') + # The nominal owner remains available to structural model binding + # after its authored model is removed, but is no longer a root. + annotations = fixture(root, source.removeprefix(model)) + self.assertEqual(set(annotations), {RUST}) + self.assertEqual([a['rust'] for a in annotations.types], ['zerocopy::util::S']) + + def test_nested_annotations_select_exact_owner_without_type_wildcards(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = 'mod m { struct S; impl S { ' + BLOCK + 'fn f(self) {} } }' + annotations = fixture(root, source) + self.assertEqual(set(annotations), {'zerocopy::util::m::S::f'}) + work = root / 'work' + work.mkdir() + inline.write_scan(annotations, work) + self.assertEqual((work / 'roots.txt').read_text(), 'zerocopy::util::m::S::f\n') + + def test_removing_an_annotation_visibly_removes_its_root(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + extra = BLOCK.replace('f_spec', 'g_spec') + 'fn g(x: usize) {}\n' + annotations = fixture(root, BLOCK + 'fn f(x: usize) {}\n' + extra) + self.assertEqual(set(annotations), {RUST, 'zerocopy::util::g'}) + annotations = fixture(root, 'fn f(x: usize) {}\n' + extra) + self.assertEqual(set(annotations), {'zerocopy::util::g'}) + work = root / 'work' + work.mkdir() + inline.write_scan(annotations, work) + self.assertEqual((work / 'roots.txt').read_text(), 'zerocopy::util::g\n') + with self.assertRaisesRegex(ValueError, 'No Aeneas specifications discovered'): + fixture(root, 'fn f(x: usize) {}\nfn g(x: usize) {}') + + def test_rejects_near_miss_guards_legacy_sections_and_extra_commands(self): + variants = [BLOCK.replace('```aeneas', guard) for guard in + ('```Aeneas', '```aeneos', '``aeneas', '```aeneas v2')] + variants += [BLOCK.replace('/// ```\n', ''), + BLOCK.replace('/// ```\n', '/// ``` trailing\n'), + BLOCK.replace('/// ensures', '/// ensures'), + BLOCK.replace('/// ensures', '/// for util.other x\n/// ensures'), + BLOCK.replace('/// ensures', '/// proof:\n/// ensures'), + BLOCK.replace('/// spec', '/// model:\n/// spec'), + BLOCK.replace('/// ```\n', '/// def extra := 1\n/// ```\n')] + for block in variants: + with self.subTest(block=block), self.assertRaises(ValueError): + self.parse(block + 'fn f(x: usize) {}') + + def test_fake_fences_in_literals_are_not_annotations(self): + self.assertEqual(self.parse('fn f() { let x = r###"\n' + BLOCK + '"###; }'), []) + with self.assertRaises(ValueError): + self.parse('fn f() { /* ```aeneas\nspec f_spec\n``` */ }') + + def test_discovery_derives_ids_and_rejects_duplicates_or_unsupported_files(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + annotations = fixture(root) + self.assertEqual(set(annotations), {RUST}) + self.assertNotIn('golden', annotations[RUST]) + self.assertNotIn('depends_on', annotations[RUST]) + path = root / ENTRY['file'] + path.write_text('fn f(x: usize) {}') + with self.assertRaisesRegex(ValueError, 'No Aeneas specifications discovered'): + inline.discover(root, TOOL) + fixture(root) + path.write_text(path.read_text() * 2) + with self.assertRaisesRegex(ValueError, 'Duplicate annotation'): + inline.discover(root, TOOL) + fixture(root) + (root / 'unsupported.rs').write_text(BLOCK + 'fn f(x: usize) {}') + with self.assertRaisesRegex(ValueError, 'Unsupported annotated'): + inline.discover(root, TOOL) + + def test_models_derive_root_names_and_verify_parameter_order(self): + annotations = {RUST: {'generics': [], 'inputs': ['x']}} + self.assertEqual(inline.generated_bindings(GENERATED, annotations), {RUST: 'util.f'}) + for bad in (GENERATED.replace('(x : Nat)', '(other : Nat)'), + GENERATED.replace('[zerocopy::util::f]', '[zerocopy::util::other]'), + GENERATED.replace('\nend Zerocopy', GENERATED + '\nend Zerocopy')): + with self.assertRaises(ValueError): + inline.generated_bindings(bad, annotations) + two = GENERATED.replace('(x : Nat)', '(a b : Nat)').replace('x + 1', 'a + b') + original = {RUST: {'generics': [], 'inputs': ['a', 'b']}} + self.assertEqual(inline.generated_bindings(two, original), {RUST: 'util.f'}) + with self.assertRaisesRegex(ValueError, 'parameters disagree'): + inline.generated_bindings(two.replace('(a b : Nat)', '(b a : Nat)'), original) + generic = GENERATED.replace('[zerocopy::util::f]', '[zerocopy::util::{zerocopy::util::S}::f]') + generic = generic.replace('def util.f (x : Nat)', 'def util.S.f {E : Type} (self : S E)') + name = 'zerocopy::util::S::f' + self.assertEqual(inline.generated_bindings(generic, {name: {'generics': ['E'], 'inputs': ['self']}}), {name: 'util.S.f'}) + with self.assertRaises(ValueError): + inline.generated_bindings(generic.replace('{zerocopy::util::S}', '{zerocopy::other::S}'), {name: {'generics': ['E'], 'inputs': ['self']}}) + + def test_loop_helpers_dependencies_and_constants_do_not_replace_roots(self): + helpers = ('/-- [zerocopy::util::f]: loop body 0:\n Source: x -/\n' + '@[rust_loop_body]\ndef util.f_loop.body := 1\n\n' + '/-- [zerocopy::util::dependency]:\n Source: x -/\n' + 'def util.dependency := 4\n\n') + self.assertEqual(inline.generated_bindings(GENERATED.replace('/-- ', helpers + '/-- ', 1), + {RUST: {'generics': [], 'inputs': ['x']}}), {RUST: 'util.f'}) + + def test_assembly_requires_independent_checker_before_generation(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + work = root / 'work' + work.mkdir() + with self.assertRaisesRegex(ValueError, 'RequiredContracts.lean is required'): + inline.assemble(root, {}, work) + self.assertFalse((work / 'Specs.lean').exists()) + self.assertFalse((work / 'Required.lean').exists()) + + def test_development_assembly_preserves_edited_projection_and_all_outputs(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + annotations = fixture(root) + work = root / 'work' + (work / 'Zerocopy').mkdir(parents=True) + (work / 'Zerocopy/Funs.lean').write_text(GENERATED) + lean = root / 'verification/aeneas/lean' + lean.mkdir(parents=True) + (lean / 'RequiredContracts.lean').write_text('') + inline.save_bindings(root, annotations, work, development=True) + inline.assemble(root, annotations, work, development=True) + self.assertTrue((work / inline.workspace.PROJECTION_STATE).exists()) + inline.assemble(root, annotations, work, development=True) + for module in inline.workspace.PROJECTIONS: + path = work / f'{module}.lean' + original = path.read_bytes() + path.write_bytes(original + b'\n-- local scratch edit\n') + before = {str(p.relative_to(work)): p.read_bytes() for p in work.rglob('*') if p.is_file()} + with self.subTest(module=module), self.assertRaisesRegex(ValueError, 'Refusing to overwrite'): + inline.assemble(root, annotations, work, development=True) + self.assertEqual(before, {str(p.relative_to(work)): p.read_bytes() for p in work.rglob('*') if p.is_file()}) + path.write_bytes(original) + + def test_assembly_injects_verified_application_and_maps_lines_and_modules(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + annotations = fixture(root) + work = root / 'work' + (work / 'Zerocopy').mkdir(parents=True) + (work / 'Zerocopy/Funs.lean').write_text(GENERATED) + proofs = root / 'verification/aeneas/lean/Proofs' + proofs.mkdir(parents=True) + (proofs / 'Util.lean').write_text('import Specs\n') + (root / 'verification/aeneas/lean/RequiredContracts.lean').write_text('') + (root / 'verification/aeneas/lean/ExtraFacts.lean').write_text( + 'namespace Other\naxiom unused : True\nend Other\n') + with self.assertRaisesRegex(ValueError, 'Verified live'): + inline.assemble(root, annotations, work) + inline.save_bindings(root, annotations, work) + inline.assemble(root, annotations, work) + specs = (work / 'Specs.lean').read_text() + self.assertIn('aeneas_spec_begin\nspec f_spec', specs) + self.assertIn('ensures r => r = x\naeneas_spec_end', specs) + self.assertIn('check_spec_binding Zerocopy.Specs.f_spec for Zerocopy.util.f with 1', specs) + self.assertIn('spec f_spec\n for @Zerocopy.util.f with 0 type parameters\n ensures', specs) + required = (work / 'Required.lean').read_text() + self.assertIn('example : Zerocopy.Specs.f_spec := @Zerocopy.Proofs.f_spec', required) + self.assertIn('check_contract Zerocopy.Obligations.f_spec using @Zerocopy.Proofs.f_spec', required) + self.assertNotIn('def requiredTheorems ', required) + self.assertLess(required.index('example : Zerocopy.Specs.f_spec'), + required.index('check_contract Zerocopy.Obligations.f_spec')) + self.assertIn('proofModuleNames : Array Name := #[`Proofs, `Proofs.Util]', required) + self.assertIn('import ExtraFacts', required) + self.assertIn('auditModuleNames : Array Name := #[`ExtraFacts', required) + mapping = json.loads((work / 'Specs.source-map.json').read_text()) + for index, line in enumerate(specs.splitlines(), 1): + if line.startswith('spec '): + self.assertEqual(mapping[str(index)], {'file': ENTRY['file'], 'line': 2}) + if line.startswith(' for '): + self.assertEqual(mapping[str(index)], {'file': ENTRY['file'], 'line': 2}) + if line.startswith(' ensures '): + self.assertEqual(mapping[str(index)], {'file': ENTRY['file'], 'line': 3}) + # Comment-only golden variation is allowed, but code and source + # changes invalidate the mapping before any proof is compiled. + (work / 'Zerocopy/Funs.lean').write_text('-- moved\n' + GENERATED) + inline.assemble(root, annotations, work) + (work / 'Zerocopy/Funs.lean').write_text(GENERATED.replace('x + 1', 'x + 2')) + with self.assertRaisesRegex(ValueError, 'stale'): + inline.assemble(root, annotations, work) + (work / 'Zerocopy/Funs.lean').write_text(GENERATED) + (root / ENTRY['file']).write_text((root / ENTRY['file']).read_text() + '// changed\n') + with self.assertRaisesRegex(ValueError, 'changed after annotation discovery'): + inline.assemble(root, annotations, work) + + def test_fixed_source_snapshot_rejects_discovery_digest_binding_and_assembly_edits(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + annotations = fixture(root) + original = (root / ENTRY['file']).read_bytes() + digest = inline.source_digest(root, annotations) + self.assertEqual(annotations.source_snapshots[ENTRY['file']], original) + work = root / 'work' + (work / 'Zerocopy').mkdir(parents=True) + (work / 'Zerocopy/Funs.lean').write_text(GENERATED) + (root / 'verification/aeneas/lean').mkdir(parents=True) + (root / 'verification/aeneas/lean/RequiredContracts.lean').write_text('') + inline.save_bindings(root, annotations, work) + extracted = root / 'crate.llbc' + extracted.write_text(json.dumps(llbc(annotations[RUST], original.decode()))) + changed = original.replace(b'r = x', b'r != x') + (root / ENTRY['file']).write_bytes(changed) + for operation in (lambda: inline.source_digest(root, annotations), + lambda: inline.save_bindings(root, annotations, work), + lambda: inline.assemble(root, annotations, work), + lambda: inline.check_bindings(root, annotations, extracted)): + with self.assertRaisesRegex(ValueError, 'changed after annotation discovery'): + operation() + self.assertEqual(annotations[RUST]['spec_text'], 'spec f_spec\n ensures r => r = x\n') + self.assertEqual(json.loads((work / 'bindings.json').read_text())['sources'], digest) + (root / ENTRY['file']).write_bytes(original) + self.assertEqual(inline.source_digest(root, annotations), digest) + + def test_unannotated_dependency_files_are_frozen_and_checked_against_charon(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + fixture(root) + dependency = root / 'zerocopy/src/util/macros.rs' + dependency.write_text('fn dependency() -> usize { 1 }\n') + annotations = inline.discover(root, TOOL) + digest = inline.source_digest(root, annotations) + source = (root / ENTRY['file']).read_text() + data = llbc(annotations[RUST], source) + data['translated']['files'].extend([ + {'id': 2, 'name': {'Local': 'src/util/macros.rs'}, 'contents': dependency.read_text()}, + {'id': 3, 'name': {'Local': '/rustc/library/core/src/lib.rs'}, 'contents': None}]) + path = root / 'crate.llbc' + path.write_text(json.dumps(data)) + inline.check_bindings(root, annotations, path) + dependency.write_text('fn dependency() -> usize { 2 }\n') + with self.assertRaisesRegex(ValueError, 'changed after annotation discovery'): + inline.source_digest(root, annotations) + with self.assertRaisesRegex(ValueError, 'changed after annotation discovery'): + inline.check_bindings(root, annotations, path) + dependency.write_bytes(annotations.source_snapshots['zerocopy/src/util/macros.rs']) + self.assertEqual(inline.source_digest(root, annotations), digest) + for change, expected in [('stale', 'different source body for local dependency'), + ('missing', 'missing source contents'), + ('unscanned', 'lacks an inspected source snapshot')]: + bad = copy.deepcopy(data) + file = bad['translated']['files'][1] + if change == 'stale': + file['contents'] = file['contents'].replace('{ 1 }', '{ 2 }') + elif change == 'missing': + file['contents'] = None + else: + file['name'] = {'Local': 'src/unscanned.rs'} + path.write_text(json.dumps(bad)) + with self.subTest(change=change), self.assertRaisesRegex(ValueError, expected): + inline.check_bindings(root, annotations, path) + + def test_type_binding_order_structural_coverage_and_model_assembly(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + block = '/// ```aeneas\n/// model BoxValue := Fields\n/// decode self => self\n/// ```\n' + source = block + 'struct Box(T);\nenum E { A }\n' + BLOCK + 'fn f(x: usize) {}\n' + annotations = fixture(root, source) + self.assertEqual(len(annotations.types), 2) + data = llbc(annotations[RUST], source) + for index, owner in enumerate(annotations.types): + data['translated'].setdefault('type_decls', []).append({ + 'def_id': index, 'generics': {'types': [{'name': name} for name in owner['generics']], + 'const_generics': [], 'trait_clauses': []}, + 'kind': {'Struct' if owner['nominal_kind'] == 'struct' else 'Enum': []}, + 'item_meta': {'name': [{'Ident': [part, 0]} for part in owner['rust'].split('::')], + 'is_local': True, 'started_from': owner['rust'] in annotations, + 'span': {'Untagged': {'generated_from_span': None, 'data': { + 'file_id': 1, 'beg': {'line': owner['open_line'], 'col': 0}, + 'end': {'line': owner['end_line'], 'col': owner['end_col']}}}}}}) + path = root / 'crate.llbc' + path.write_text(json.dumps(data)) + inline.check_bindings(root, annotations, path) + work = root / 'work' + (work / 'Zerocopy').mkdir(parents=True) + (work / 'Zerocopy/Funs.lean').write_text(GENERATED) + types = ('module\nnamespace Zerocopy\n' + '/-- [zerocopy::util::E]\n Source: x -/\n@[discriminant isize]\n' + 'inductive util.E where\n| A : util.E\n' + '/-- [zerocopy::util::Box]\n Source: x -/\n' + 'structure util.Box (T : Type) where\n val : T\n\nend Zerocopy\n') + (work / 'Zerocopy/Types.lean').write_text(types) + inline.save_bindings(root, annotations, work) + manifest = json.loads((work / 'bindings.json').read_text()) + self.assertEqual(manifest['version'], 3) + self.assertEqual([rust for rust, entry in manifest['bindings'].items() if entry['kind'] == 'type'], + ['zerocopy::util::E', 'zerocopy::util::Box']) + self.assertEqual(manifest['bindings']['zerocopy::util::Box']['model'], 'Zerocopy.util.Box.BoxValue') + (root / 'verification/aeneas/lean').mkdir(parents=True) + (root / 'verification/aeneas/lean/RequiredContracts.lean').write_text('') + inline.assemble(root, annotations, work) + shapes = (work / 'ModelShapes.lean').read_text() + models = (work / 'Models.lean').read_text() + self.assertIn('aeneas_model_shape Zerocopy.util.Box with 1 type parameters begin\nmodel BoxValue := Fields\nend', shapes) + self.assertIn('derive_rust_model Zerocopy.util.Box with 1 type parameters decode self =>\n self', models) + default = manifest['bindings']['zerocopy::util::E'] + self.assertEqual(default['model'], default['fields']) + self.assertEqual(default['provider'], 'Zerocopy.util.E.aeneasModel') + for module, generated in [('ModelShapes', shapes), ('Models', models)]: + mapping = json.loads((work / f'{module}.source-map.json').read_text()) + for line, location in mapping.items(): + self.assertLessEqual(int(line), len(generated.splitlines())) + self.assertEqual(location['file'], ENTRY['file']) + required = (work / 'Required.lean').read_text() + self.assertNotIn('Zerocopy.Proofs.BoxValue', required) + self.assertIn('import Models', required) + (work / 'Zerocopy/Types.lean').write_text(types.replace('val : T', 'val : Option T')) + with self.assertRaisesRegex(ValueError, 'stale'): + inline.assemble(root, annotations, work) + # An unannotated nominal carrier cannot vanish into an alias. + (work / 'Zerocopy/Types.lean').write_text(types.replace('inductive util.E where\n| A : util.E', 'def util.E := Unit')) + with self.assertRaisesRegex(ValueError, 'nominal type declarations'): + inline.save_bindings(root, annotations, work) + (work / 'Zerocopy/Types.lean').write_text(types) + for change in ('missing', 'kind', 'span', 'generic'): + bad = copy.deepcopy(data) + if change == 'missing': + bad['translated']['type_decls'].pop(0) + elif change == 'kind': + bad['translated']['type_decls'][1]['kind'] = {'Struct': []} + elif change == 'span': + bad['translated']['type_decls'][1]['item_meta']['span']['Untagged']['data']['end']['line'] += 1 + else: + bad['translated']['type_decls'][0]['generics']['types'] = [] + path.write_text(json.dumps(bad)) + with self.subTest(change=change), self.assertRaises(ValueError): + inline.check_bindings(root, annotations, path) + + def test_two_type_owners_can_choose_the_same_short_model_name(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + model = '/// ```aeneas\n/// model SharedName := Fields\n/// decode self => self\n/// ```\n' + source = model + 'struct Left(usize);\n' + model + 'struct Right(usize);\n' + BLOCK + 'fn f(x: usize) {}\n' + annotations = fixture(root, source) + self.assertEqual(annotations['zerocopy::util::Left']['model_name'], 'SharedName') + self.assertEqual(annotations['zerocopy::util::Right']['model_name'], 'SharedName') + self.assertEqual(len(annotations), 3) + + def test_whole_module_goldens_update_without_rewriting_rust(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + fixture(root) + original = (root / ENTRY['file']).read_bytes() + live = root / 'live' + live.mkdir() + for name in inline.golden.FILES: + (live / name).write_text(GENERATED if name == 'Funs.lean' else 'module\n') + inline.update(root, live) + self.assertEqual((root / ENTRY['file']).read_bytes(), original) + inline.check_goldens(root) + rendered = root / 'rendered' + inline.render(root, rendered) + self.assertEqual(inline.golden.compare(live, rendered), []) + (root / 'verification/aeneas/golden/obsolete.lean.in').write_text('placeholder') + with self.assertRaises(ValueError): + inline.check_goldens(root) + + def test_llbc_binding_rejects_cfg_alternatives_stale_sources_and_parameter_changes(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + annotations = fixture(root) + source = (root / ENTRY['file']).read_text() + data = llbc(annotations[RUST], source) + path = root / 'crate.llbc' + path.write_text(json.dumps(data)) + inline.check_bindings(root, annotations, path) + for kind in ('alternative', 'stale', 'missing', 'duplicate', 'wrong_file', 'generic', 'arg_name', 'arg_count'): + bad = copy.deepcopy(data) + translated = bad['translated'] + function = translated['fun_decls'][0] + if kind == 'alternative': + function['item_meta']['span']['Untagged']['data']['end']['line'] += 1 + elif kind == 'stale': + translated['files'][0]['contents'] += '// stale\n' + elif kind == 'missing': + translated['fun_decls'] = [] + elif kind == 'duplicate': + translated['fun_decls'] *= 2 + elif kind == 'wrong_file': + translated['files'][0]['name']['Local'] = 'src/other.rs' + elif kind == 'generic': + function['generics']['types'] = [{'name': 'T'}] + elif kind == 'arg_name': + function['body']['Structured']['locals']['locals'][1]['name'] = 'other' + else: + function['body']['Structured']['locals']['arg_count'] = 2 + path.write_text(json.dumps(bad)) + with self.subTest(kind=kind), self.assertRaises(ValueError): + inline.check_bindings(root, annotations, path) + + def test_extraction_rejects_extra_roots_and_permits_unannotated_dependencies(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + annotations = fixture(root) + source = (root / ENTRY['file']).read_text() + data = llbc(annotations[RUST], source) + initializer = copy.deepcopy(data['translated']['fun_decls'][0]) + initializer['src'] = {'GlobalInitializer': {'id': 0, 'generics': {}}} + initializer['item_meta']['name'][-1]['Ident'][0] = 'CONSTANT' + data['translated']['fun_decls'].append(initializer) + path = root / 'crate.llbc' + path.write_text(json.dumps(data)) + with self.assertRaisesRegex(ValueError, 'Extracted roots do not match'): + inline.check_bindings(root, annotations, path) + initializer['item_meta']['started_from'] = False + path.write_text(json.dumps(data)) + inline.check_bindings(root, annotations, path) + initializer['item_meta']['started_from'] = True + initializer['src'] = 'Normal' + initializer['item_meta']['name'][-1]['Ident'][0] = 'generated_method' + path.write_text(json.dumps(data)) + with self.assertRaisesRegex(ValueError, 'Extracted roots do not match'): + inline.check_bindings(root, annotations, path) + + def test_inherent_method_llbc_identity_resolves_named_self(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + block = BLOCK.replace('(x : Nat)', '(self : S)') + source = 'struct S; impl S { ' + block + 'fn f(self) {} }\n' + annotations = fixture(root, source) + annotation = annotations['zerocopy::util::S::f'] + data = llbc(annotation, source) + translated = data['translated'] + type_name = [{'Ident': [part, 0]} for part in ['zerocopy', 'util', 'S']] + body = {'Adt': {'id': 1, 'builtin': None, + 'generics': {'regions': [], 'types': [], 'const_generics': [], 'trait_refs': []}}} + implementation = {'Ty': {'kind': 'InherentImplBlock', 'params': {'types': [], 'regions': []}, + 'skip_binder': {'Deduplicated': 3}}} + translated['item_names'] = [{'value': {'Value': [3, body]}}] + translated['type_decls'] = [{'def_id': 1, 'item_meta': {'name': type_name}}] + translated['fun_decls'][0]['item_meta']['name'] = [*type_name[:2], {'Impl': implementation}, {'Ident': ['f', 0]}] + translated['fun_decls'][0]['body']['Structured']['locals']['locals'][1]['name'] = 'self' + path = root / 'crate.llbc' + path.write_text(json.dumps(data)) + inline.check_bindings(root, annotations, path) + for kind in ('wrong_self', 'missing_self', 'trait', 'cross_module'): + bad = copy.deepcopy(data) + translated = bad['translated'] + impl = translated['fun_decls'][0]['item_meta']['name'][2]['Impl'] + if kind == 'wrong_self': + translated['type_decls'][0]['item_meta']['name'][-1]['Ident'][0] = 'T' + elif kind == 'missing_self': + impl['Ty']['skip_binder']['Deduplicated'] = 99 + elif kind == 'trait': + impl.clear() + impl['Trait'] = 0 + else: + translated['type_decls'][0]['item_meta']['name'][1]['Ident'][0] = 'other' + path.write_text(json.dumps(bad)) + with self.subTest(kind=kind), self.assertRaises(ValueError): + inline.check_bindings(root, annotations, path) + + +if __name__ == '__main__': + unittest.main() diff --git a/verification/aeneas/tests/test_inspect.py b/verification/aeneas/tests/test_inspect.py new file mode 100644 index 0000000000..c515447a79 --- /dev/null +++ b/verification/aeneas/tests/test_inspect.py @@ -0,0 +1,79 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +"""Read-only inspection regression against an explicitly selected compiled project. + +AENEAS_INSPECTION_PROJECT=/path/to/compiled/project python3 -B -m unittest + discover -s verification/aeneas/tests -p test_inspect.py +""" +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[3] +PROJECT = os.environ.get('AENEAS_INSPECTION_PROJECT') + + +@unittest.skipUnless(PROJECT, 'Set AENEAS_INSPECTION_PROJECT to reuse compiled prerequisites') +class InspectTests(unittest.TestCase): + def test_compiled_provider_mode_ghosts_and_rejection(self): + source = (ROOT / 'verification/aeneas/lean/InspectSpec.lean').read_text() + source = source.replace('import Specs\n', 'import Specs\nimport SpecsSyntaxTests\n') + # Positive prerequisites are imported compiled modules. Guarded failures + # must identify the intended call/decoding audit, rather than imports. + source += ''' +inspect_spec SpecsSyntaxTests.word_partial +/-- error: Specification SpecsSyntaxTests.word_partial does not call the expected model SpecsSyntaxTests.nativeIdentity -/ +#guard_msgs (error, drop info) in +check_spec_binding SpecsSyntaxTests.word_partial for SpecsSyntaxTests.nativeIdentity with 1 +inspect_spec SpecsSyntaxTests.fixed_provider_spec +inspect_spec SpecsSyntaxTests.generic_fixed +inspect_spec SpecsSyntaxTests.model_dependent_ghost +namespace InspectionRegression +open Aeneas.Std AeneasSpecs +def original (x : Nat) : Result Nat := .ok x +@[aeneas_spec] abbrev wrongCall : Prop := + ∀ x : Nat, WP.spec (original (x + 1)) (fun _ => True) +/-- error: Specification InspectionRegression.wrongCall model input 1 is not its original bound variable -/ +#guard_msgs (error, drop info) in +inspect_spec wrongCall +@[aeneas_spec] abbrev missingDecode : Prop := + ∀ x : Nat, WP.spec (original x) (fun _ => True) +/-- error: Specification InspectionRegression.missingDecode lacks the expected decoded-result existential -/ +#guard_msgs (error, drop info) in +inspect_spec missingDecode +@[aeneas_spec] abbrev rejectedDecode : Prop := + ∀ x : Nat, WP.spec (original x) (fun raw => ∃ math : Nat, + @RustModel.decode Nat modelNat raw = none ∧ math = math) +/-- error: Specification InspectionRegression.rejectedDecode does not require successful decoding of its original result -/ +#guard_msgs (error, drop info) in +inspect_spec rejectedDecode +end InspectionRegression +''' + with tempfile.TemporaryDirectory(prefix='aeneas-inspection-test-') as directory: + path = Path(directory) / 'Regression.lean' + path.write_text(source) + result = subprocess.run(['lake', 'env', 'lean', '-DwarningAsError=true', str(path)], + cwd=PROJECT, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn('Execution: partial (WP.dspec)', result.stdout) + self.assertIn('Execution: total (WP.spec)', result.stdout) + self.assertIn('modelNat', result.stdout) + self.assertIn('position', result.stdout) + self.assertIn('constraint', result.stdout) + self.assertIn('Required result decoding:', result.stdout) + self.assertIn('Original raw call:', result.stdout) + # The competing ghost must not be reported as the selected input provider. + self.assertNotIn('Selected provider: evil', result.stdout) + self.assertNotIn('Selected result provider: evil', result.stdout) + + +if __name__ == '__main__': + unittest.main() diff --git a/verification/aeneas/tests/test_prepare.py b/verification/aeneas/tests/test_prepare.py new file mode 100644 index 0000000000..9be43f15d8 --- /dev/null +++ b/verification/aeneas/tests/test_prepare.py @@ -0,0 +1,95 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + +spec = importlib.util.spec_from_file_location("prepare", Path(__file__).parents[1] / "prepare.py") +prepare = importlib.util.module_from_spec(spec) +spec.loader.exec_module(prepare) + + +class PrepareTests(unittest.TestCase): + def test_rejects_error_bearing_or_missing_llbc_status(self): + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "crate.llbc" + for status in [True, None, 0, "false"]: + path.write_text(json.dumps({"has_errors": status, "translated": {}})) + with self.assertRaises(ValueError): + prepare.check_llbc(path) + path.write_text(json.dumps({"has_errors": False, "translated": {}})) + prepare.check_llbc(path) + + def test_renames_only_second_dictionary(self): + types = ('"markerCopyInst", "markerCopyInst"\n' + 'markerCopyInst : core.marker.Copy Self\n' + 'markerCopyInst : core.marker.Copy Self_NonZeroInner\n') + funs = ('markerCopyInst := core.marker.CopyUsize\n' + 'markerCopyInst := core.num.niche_types.NonZeroUsizeInner.Insts.CoreMarkerCopy\n' + 'def util.max := body\n') + new_types, new_funs = prepare.rename_copy_fields(types, funs) + self.assertIn('"markerCopyInst", "innerCopyInst"', new_types) + self.assertIn('markerCopyInst : core.marker.Copy Self\n', new_types) + self.assertIn('innerCopyInst : core.marker.Copy Self_NonZeroInner\n', new_types) + self.assertIn('markerCopyInst := core.marker.CopyUsize\n', new_funs) + self.assertTrue(new_funs.endswith('def util.max := body\n')) + # Upgrades, duplicate matches, and accidental repeated application fail. + for bad_types, bad_funs in [(types + types, funs), (types, ''), (new_types, new_funs)]: + with self.assertRaises(ValueError): + prepare.rename_copy_fields(bad_types, bad_funs) + + def test_imports_include_public_meta_and_exclude_dependency_cache(self): + with tempfile.TemporaryDirectory() as tmp: + backend = Path(tmp) + (backend / 'Aeneas.lean').write_text( + 'public import Mathlib.Data.BitVec\npublic meta import Mathlib.Tactic\n' + 'import Aeneas.Std\n') + (backend / '.lake').mkdir() + (backend / '.lake/Other.lean').write_text('import Mathlib\n') + self.assertEqual(prepare.mathlib_imports(backend), + ['Mathlib.Data.BitVec', 'Mathlib.Tactic']) + + def test_imports_include_native_proof_submodules(self): + with tempfile.TemporaryDirectory() as tmp: + backend, proofs = Path(tmp) / 'backend', Path(tmp) / 'proofs' + backend.mkdir() + proofs.mkdir() + (backend / 'Aeneas.lean').write_text('import Mathlib.Tactic.Basic\n') + (proofs / 'RequiredContracts.lean').write_text( + 'public import Mathlib.Tactic.Convert\n') + (proofs / 'Proofs').mkdir() + (proofs / 'Proofs/Helpers.lean').write_text('import all Mathlib.Data.Nat.Log\n') + self.assertEqual(prepare.mathlib_imports(backend, proofs), + ['Mathlib.Data.Nat.Log', 'Mathlib.Tactic.Basic', + 'Mathlib.Tactic.Convert']) + + def test_workspace_discovers_all_assembled_modules(self): + with tempfile.TemporaryDirectory() as tmp: + backend, work = Path(tmp) / 'backend', Path(tmp) / 'work' + backend.mkdir() + work.mkdir() + (backend / 'lake-manifest.json').write_text(json.dumps({ + 'version': '1.2.0', 'packages': [], + })) + for module in ('Specs', 'Invariants', 'Required', 'Zerocopy', + 'ExtraGenerated', 'Check'): + (work / (module + '.lean')).write_text('') + prepare.share_manifest(work, backend) + config = (work / 'lakefile.lean').read_text() + for module in ('Specs', 'Invariants', 'Required', 'Zerocopy', + 'ExtraGenerated'): + self.assertIn('@[default_target] lean_lib ' + module + '\n', config) + self.assertIn('\nlean_lib Check\n', config) + self.assertNotIn('@[default_target] lean_lib Check', config) + + +if __name__ == '__main__': + unittest.main() diff --git a/verification/aeneas/tests/test_rounding_model_controls.py b/verification/aeneas/tests/test_rounding_model_controls.py new file mode 100644 index 0000000000..d9715a7404 --- /dev/null +++ b/verification/aeneas/tests/test_rounding_model_controls.py @@ -0,0 +1,176 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +"""Bounded fixtures for production rounding control selection and diagnostics.""" +import json +import os +from pathlib import Path +import tempfile +import unittest + +import rounding_model_controls as controls + + +class RoundingControlTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.old_cwd = Path.cwd() + os.chdir(self.temp.name) + self.addCleanup(self.restore_directory) + for module, theorem in controls.PROBES.values(): + Path(module + '.lean').write_text( + 'namespace Zerocopy\n\ntheorem ' + theorem + ' : True := by\n' + ' trivial\n\ntheorem next_law : True := by trivial\n') + Path('ModelShapes.lean').write_text('') + Path('SpecsSyntax.lean').write_text('') + Path('Zerocopy').mkdir() + Path('Zerocopy/Funs.lean').write_text('') + self.spec = ('spec encoding_new_spec\n for @' + controls.OWNER + '.new with 0 type parameters\n' + ' ensures encoded => encoded.align = (align : Nat) ∧ encoded.phase = (phase : Nat)\n') + Path('Specs.lean').write_text('aeneas_spec_begin\n' + self.spec + 'aeneas_spec_end\n') + self.models = ('-- preceding sibling\n' + controls.PREFIX + ' decode self =>\n' + ' { align := 2, phase := 1, .. }\n' + 'check_model_binding ' + controls.OWNER + ' with 0 type parameters\n' + '-- following sibling and local tuple provider\n') + Path('Models.lean').write_text(self.models) + self.manifest = {'version': 3, 'mode': 'verified-live', 'bindings': { + 'zerocopy::layout::RoundingAlignAndPhase': { + 'kind': 'type', 'raw': controls.OWNER, + 'model': controls.OWNER + '.RoundingValue'}}} + self.write_manifest() + + def restore_directory(self): + os.chdir(self.old_cwd) + self.temp.cleanup() + + def write_manifest(self): + Path('bindings.json').write_text(json.dumps(self.manifest)) + + def check_log(self, text, kind='none'): + Path('failure.log').write_text(text) + controls.check_failure(kind, 'failure.log') + + def test_native_renderer_and_lake_renderer(self): + # These are the real renderer prefixes; locations are adjusted to this + # compact fixture's operation-promise or domain span. + fixtures = [ + ('none', '/probe/generated/RoundingDomainControl.lean:3:41: error: unsolved goals\n' + 'case pos\nhp : 0 < code._0.val.val\n⊢ False\n'), + ('none', '✖ [2/2] Building RoundingDomainControl\n' + 'error: generated/RoundingDomainControl.lean:3:41: unsolved goals\n' + 'case pos\nhp : 0 < code._0.val.val\n⊢ False\n' + 'error: Lean exited with code 1\nerror: build failed\n'), + ('constant', '/probe/generated/RoundingConstructorControl.lean:3:14: error: ' + 'Application type mismatch: The argument\n rfl\n'), + ('constant', 'error: generated/RoundingConstructorControl.lean:3:14: ' + 'Application type mismatch: The argument\n rfl\n' + 'error: Lean exited with code 1\nerror: build failed\n'), + ] + for kind, text in fixtures: + with self.subTest(kind=kind, text=text): + self.check_log(text, kind) + + def test_mixed_semantic_and_unrelated_errors_fail_closed(self): + semantic = 'error: RoundingDomainControl.lean:3:41: unsolved goals\n⊢ False\n' + unrelated = [ + "error: RoundingDomainControl.lean:9:1: (kernel) unknown constant '_private.Models.0._proof_3'\n", + "Other.lean:1:1: error: Unknown identifier `missing`\n", + "error: Other.lean:1:1: object file 'Missing.olean' of module Missing does not exist\n", + 'error: cannot locate module Missing\n', + 'wrapper error: unrecognized kernel failure\n', + 'Other.lean:3:1: error: unsolved goals\n', + 'RoundingDomainControl.lean:1:1: error: unsolved goals\n', + ] + for suffix in unrelated: + with self.subTest(suffix=suffix), self.assertRaises(ValueError): + self.check_log(semantic + suffix) + + def test_wrong_probe_or_missing_diagnostic_does_not_count(self): + for text in ['error: Other.lean:3:41: unsolved goals\n', + 'error: RoundingDomainControl.lean:6:41: unsolved goals\n', + 'error: Lean exited with code 1\nerror: build failed\n']: + with self.subTest(text=text), self.assertRaises(ValueError): + self.check_log(text) + + def test_selected_capability_and_lost_owner(self): + self.assertTrue(controls.capability()) + for field, value in [('kind', 'function'), ('model', controls.OWNER + '.Forgetful'), + ('raw', 'OtherRaw')]: + owner = self.manifest['bindings']['zerocopy::layout::RoundingAlignAndPhase'] + old = owner[field] + owner[field] = value + self.write_manifest() + with self.subTest(field=field), self.assertRaisesRegex(ValueError, 'lost'): + controls.capability() + owner[field] = old + self.manifest['bindings'].clear() + self.write_manifest() + with self.assertRaisesRegex(ValueError, 'lost'): + controls.capability() + Path('Models.lean').write_text('') + self.assertFalse(controls.capability()) + + def test_selected_capability_requires_operation_modules(self): + Path('Specs.lean').unlink() + with self.assertRaisesRegex(ValueError, 'prerequisite: Specs'): + controls.capability() + + def test_probes_reuse_constructor_fence_and_independent_input(self): + for module, _ in controls.PROBES.values(): + Path(module + '.lean').unlink() + controls.write_probes() + constructor = Path('RoundingConstructorControl.lean').read_text() + domain = Path('RoundingDomainControl.lean').read_text() + self.assertIn(self.spec, constructor) + self.assertIn('encoding_new_spec_contract align phase (.ok code)', constructor) + self.assertIn('constructor_result', constructor) + self.assertIn('isValid code', domain) + for source in (constructor, domain): + self.assertNotIn('import MathViews', source) + self.assertNotIn('import RepresentationLaws', source) + self.assertNotIn('derive_rust_model', source) + with self.assertRaisesRegex(ValueError, 'already exists'): + controls.write_probes() + + def test_constructor_fence_selection_fails_closed(self): + original = Path('Specs.lean').read_text() + for source in (original + original, original.replace('encoding_new_spec', 'other_spec'), + original.replace(controls.OWNER + '.new', 'Other.new')): + Path('Specs.lean').write_text(source) + with self.subTest(source=source), self.assertRaises(ValueError): + controls.constructor_spec() + + def test_mutations_touch_only_selected_decoder(self): + marker = 'check_model_binding ' + controls.OWNER + prefix, suffix = self.models.split(controls.PREFIX)[0], self.models[self.models.index(marker):] + for kind, body in [('constant', controls.CONSTANT), ('none', controls.REJECT)]: + Path('Models.lean').write_text(self.models) + controls.mutate(kind) + self.assertEqual(Path('Models.lean').read_text(), prefix + body + suffix) + + def test_bad_mutation_boundaries_leave_source_unchanged(self): + marker = 'check_model_binding ' + controls.OWNER + ' with 0 type parameters\n' + fixtures = [self.models + controls.PREFIX, + self.models.replace(marker, ''), + marker + self.models.replace(marker, ''), + self.models.replace(marker, 'derive_rust_model Another with 0 type parameters\n' + marker), + self.models.replace(marker, 'check_model_binding Another with 0 type parameters\n' + marker), + self.models.replace(marker, 'def unrelated := 0\n' + marker)] + for source in fixtures: + Path('Models.lean').write_text(source) + with self.subTest(source=source), self.assertRaises(ValueError): + controls.mutate('none') + self.assertEqual(Path('Models.lean').read_text(), source) + Path('Models.lean').write_text(self.models) + with self.assertRaises(ValueError): + controls.mutate('invalid') + self.assertEqual(Path('Models.lean').read_text(), self.models) + + +if __name__ == '__main__': + unittest.main() diff --git a/verification/aeneas/tests/test_workspace.py b/verification/aeneas/tests/test_workspace.py new file mode 100644 index 0000000000..984b741b3b --- /dev/null +++ b/verification/aeneas/tests/test_workspace.py @@ -0,0 +1,154 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import contextlib +import io +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import workspace + + +class WorkspaceTests(unittest.TestCase): + def test_projection_edits_block_regeneration_with_copy_back_instructions(self): + with tempfile.TemporaryDirectory() as directory: + work = Path(directory) + workspace.protect_projections(work) + for module in workspace.PROJECTIONS: + (work / f'{module}.lean').write_text(f'{module} original') + workspace.record_projections(work) + workspace.protect_projections(work) + for module in workspace.PROJECTIONS: + path = work / f'{module}.lean' + original = path.read_bytes() + path.write_text('manual construction or clause edit') + before = {p.name: p.read_bytes() for p in work.iterdir()} + with self.subTest(module=module), self.assertRaisesRegex(ValueError, 'Copy each authored fragment back') as error: + workspace.protect_projections(work) + self.assertIn(f'{module}.source-map.json', str(error.exception)) + self.assertEqual(before, {p.name: p.read_bytes() for p in work.iterdir()}) + path.write_bytes(original) + # Explicitly preserving and removing an edited projection permits regeneration. + (work / 'Models.lean').unlink() + workspace.protect_projections(work) + + def test_unbaselined_existing_projection_is_never_silently_adopted(self): + with tempfile.TemporaryDirectory() as directory: + work = Path(directory) + (work / 'Specs.lean').write_text('unrecorded local changes') + with self.assertRaisesRegex(ValueError, 'unbaselined'): + workspace.protect_projections(work) + self.assertFalse((work / workspace.PROJECTION_STATE).exists()) + (work / workspace.PROJECTION_STATE).write_text('{"version": 0}') + with self.assertRaisesRegex(ValueError, 'Invalid projection baseline'): + workspace.protect_projections(work) + + def test_inspection_rejects_wrong_binding_and_command_injection_before_running_lean(self): + with tempfile.TemporaryDirectory() as directory, mock.patch.object(workspace, 'run_checked') as run: + work = Path(directory) + (work / 'bindings.json').write_text('{"bindings": {}}') + for candidate in ['missing', 'x\n#eval 1', 'Zerocopy.Proofs.foo']: + with self.subTest(candidate=candidate), self.assertRaises(ValueError): + workspace.inspect_spec(work, work, candidate) + run.assert_not_called() + + def test_fresh_projects_copy_proof_submodules_and_remove_stale_files(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + sources = root / 'verification/aeneas/lean' + for relative in ('Proofs.lean', 'Proofs/Util.lean', 'Specs.lean', + 'Required.lean', 'lakefile.lean', 'Zerocopy/Funs.lean', + '.lake/packages/Dependency.lean'): + path = sources / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(relative) + backend = root / 'backend' + backend.mkdir() + (backend / 'lean-toolchain').write_text('leanprover/lean4:v4.31.0') + work = root / 'target/aeneas/verification' + work.mkdir(parents=True) + (work / 'RemovedProof.lean').write_text('stale') + workspace.initialize(root, work, backend) + self.assertEqual({str(path.relative_to(work)) + for path in work.rglob('*') if path.is_file()}, + {'Proofs.lean', 'Proofs/Util.lean', 'lean-toolchain'}) + with self.assertRaises(ValueError): + workspace.initialize(root, sources, backend) + self.assertTrue((sources / 'Proofs/Util.lean').exists()) + + def test_retires_only_recognized_legacy_generated_artifacts(self): + with tempfile.TemporaryDirectory() as directory: + work = Path(directory) + old = work / 'Invariants.lean' + mapping = work / 'Invariants.source-map.json' + old.write_text(workspace.golden.HEADER + 'module\npublic import DeriveValidity\nnamespace Zerocopy.Invariants\nend Zerocopy.Invariants\n') + mapping.write_text('{"17": {"file": "zerocopy/src/layout/mod.rs", "line": 42}}') + workspace.retire_generated(work) + self.assertFalse(old.exists()) + self.assertFalse(mapping.exists()) + old.write_text('handwritten source') + with self.assertRaisesRegex(ValueError, 'Refusing to retire unrecognized'): + workspace.retire_generated(work) + self.assertEqual(old.read_text(), 'handwritten source') + + def test_model_support_discovers_all_root_and_nested_modules(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + sources = root / 'verification/aeneas/lean' + (sources / 'ModelSupport').mkdir(parents=True) + (sources / 'ModelSupport.lean').write_text('public import ModelSupport.Facts\n') + (sources / 'ModelSupport/Facts.lean').write_text('public import SafeMath\n') + (sources / 'SafeMath.lean').write_text('public import ModelShapes\n') + self.assertEqual(workspace.model_support(root), ['ModelSupport', 'ModelSupport.Facts']) + # Retired and current generated modules never become handwritten. + for generated in ('Invariants.lean', 'ModelShapes.lean', 'Models.lean'): + (sources / generated).write_text('obsolete') + self.assertFalse(set(workspace.handwritten(root)) & {Path('Invariants.lean'), Path('Models.lean'), Path('ModelShapes.lean')}) + + def test_handwritten_module_paths_do_not_conflate_literal_dotted_segments(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + sources = root / 'verification/aeneas/lean' + (sources / 'Helper').mkdir(parents=True) + (sources / 'Helper/Name.lean').write_text('module\n') + self.assertEqual(workspace.handwritten(root), [Path('Helper/Name.lean')]) + for filename in ('Helper.Name.lean', 'Helper Name.lean', '«Helper».lean'): + path = sources / filename + path.write_text('module\n') + with self.subTest(filename=filename), self.assertRaisesRegex(ValueError, 'simple identifier components'): + workspace.handwritten(root) + path.unlink() + + def test_diagnostics_preserve_lean_output_and_add_rust_location_on_failure(self): + for module, kind in [('Specs', 'specification'), ('ModelShapes', 'model'), ('Models', 'model')]: + with self.subTest(module=module), tempfile.TemporaryDirectory() as directory: + root = Path(directory) + work = root / 'project' + work.mkdir() + (work / f'{module}.source-map.json').write_text(json.dumps({ + '17': {'file': 'zerocopy/src/layout/mod.rs', 'line': 42}})) + output = io.StringIO() + message = f'{module}.lean:17:9: error: unknown identifier' + command = [sys.executable, '-c', + f'print({message!r}); raise SystemExit(1)'] + with contextlib.redirect_stdout(output), self.assertRaises( + subprocess.CalledProcessError): + workspace.run_checked(command, root, work) + self.assertIn(message, output.getvalue()) + self.assertIn(f'Inline {kind}: {root}/zerocopy/src/layout/mod.rs:42', + output.getvalue()) + + +if __name__ == '__main__': + unittest.main() diff --git a/verification/aeneas/tests/type-only-shapes/reject-raw-parameter.lean b/verification/aeneas/tests/type-only-shapes/reject-raw-parameter.lean new file mode 100644 index 0000000000..c833883a48 --- /dev/null +++ b/verification/aeneas/tests/type-only-shapes/reject-raw-parameter.lean @@ -0,0 +1,20 @@ +/- Copyright 2026 The Fuchsia Authors + +Licensed under a BSD-style license , Apache License, Version 2.0 +, or the MIT +license , at your option. +This file may not be copied, modified, or distributed except according to +those terms. -/ + +-- The corresponding TModel shape succeeds in ModelTests. This fixture must +-- fail at the authored shape because raw T and its dictionary are unavailable. +import DeriveModels +open AeneasSpecs +namespace TypeOnlyShapeNegative +structure RawDependent (T : Type) where + value : T +aeneas_model_shape RawDependent with 1 type parameters begin + model Value where + value : T +end +end TypeOnlyShapeNegative diff --git a/verification/aeneas/toolchain.sh b/verification/aeneas/toolchain.sh new file mode 100644 index 0000000000..6860b72259 --- /dev/null +++ b/verification/aeneas/toolchain.sh @@ -0,0 +1,40 @@ +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +# One pin identifies the translation chain: release archives, patched Aeneas +# source, Charon, Rust, and Lean. Upgrade them together; a version string alone +# does not identify the checked-in patch or the downloaded archive bytes. +# Charon is selected by Aeneas's charon-pin. +AENEAS_RELEASE=nightly-2026.10.01-ca282ec +AENEAS_REV=ca282ec2312a96f6a985a4d23a663690816d3f3e +# The release supplies Charon and the backend. Aeneas itself is built from +# this exact source plus the checked-in CLI-only patch. +AENEAS_VERSION="$AENEAS_RELEASE+nominal-tuple-structs.1" +AENEAS_SOURCE_SHA256=c909a1ada228d8ea6b995eef5de10dce6af011f28430834d7c27f0ebb46a8189 +AENEAS_PATCH_SHA256=a61422f5483f4f753164e0e9e209cc0c872f8072c3318728746665f878bf6b19 +CHARON_VERSION=0.1.276 +CHARON_REV=cf02e3f228f97ee7be3a415be56989053063326d +AENEAS_RUST_TOOLCHAIN=nightly-2026-09-17 +AENEAS_LEAN_TOOLCHAIN=leanprover/lean4:v4.31.0 +AENEAS_LEAN_VERSION=4.31.0 + +case "$(uname -s)-$(uname -m)" in + Linux-x86_64) + AENEAS_PLATFORM=linux-x86_64 + AENEAS_SHA256=473df2e5bab0439bfd1a68503ae02c7f88a8de4182e98e4e281590285ab01dee + AENEAS_LEAN_PLATFORM=linux + AENEAS_LEAN_SHA256=07a633cc8d9151cbc08825ea4cdda50d4b02a2c9cb852c0131b13046f49cad7f + ;; + Darwin-arm64) + AENEAS_PLATFORM=macos-aarch64 + AENEAS_SHA256=fac88099e9d687eb321499e43aef1418a4d06f7ae72ecba0b5fd7d38cf013ddd + AENEAS_LEAN_PLATFORM=darwin_aarch64 + AENEAS_LEAN_SHA256=264105500c8abdf37b68ffe03390a783ed259807807222698da8dd92d6ce0a27 + ;; + *) echo "Unsupported Aeneas CI toolchain platform" >&2; return 1 ;; +esac diff --git a/verification/aeneas/workspace.py b/verification/aeneas/workspace.py new file mode 100644 index 0000000000..f5edddfb96 --- /dev/null +++ b/verification/aeneas/workspace.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +# Copyright 2026 The Fuchsia Authors +# +# Licensed under a BSD-style license , Apache License, Version 2.0 +# , or the MIT +# license , at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +"""Manage disposable CI projects and protect persistent development edits. + +Native Lean imports determine compilation order. This module copies handwritten +sources into fresh CI projects, preserves development projections unless their +baseline matches, and adds Rust locations to Lean diagnostics. Inspection reads +the existing compiled subject; it never implies that edited sources were rebuilt. +""" + +import argparse +import hashlib +import json +from pathlib import Path +import re +import shutil +import subprocess +import tempfile + +import golden +from copy_back import MODULES as PROJECTIONS, STATE as PROJECTION_STATE + +GENERATED = {'Specs.lean', 'ModelShapes.lean', 'Models.lean', 'Invariants.lean', 'Required.lean', 'lakefile.lean'} | { + 'Zerocopy/' + name for name in golden.FILES +} + + +def protect_projections(work): + """Fail before development regeneration can destroy an edited projection.""" + state_path = work / PROJECTION_STATE + state = json.loads(state_path.read_text()) if state_path.exists() else None + if state is not None and (not isinstance(state, dict) or state.get('version') not in (1, 2) or + set(state.get('sha256', {})) != set(PROJECTIONS)): + raise ValueError(f'Invalid projection baseline: {state_path}; preserve projections before removing it') + if state is not None and state['version'] == 2: + import copy_back + copy_back.validate(state) + changed = [] + for module in PROJECTIONS: + path = work / f'{module}.lean' + if not path.exists(): + continue + digest = hashlib.sha256(path.read_bytes()).hexdigest() + if state is None or state['sha256'][module] != digest: + changed.append(module) + if changed: + details = '\n'.join(f' {work / (name + ".lean")} -> ' + f'{work / (name + ".source-map.json")}' for name in changed) + raise ValueError('Refusing to overwrite edited or unbaselined inline projections:\n' + details + + '\nUse dev.sh --copy-back FULL_RUST_OWNER --dry-run to preview a version 2 baseline, ' + 'then omit --dry-run to apply. Copy each authored fragment back to the Rust aeneas doc fence at its source-map ' + 'file/line (shape fields from ModelShapes, decode/decode? body from Models, ' + 'spec clauses from Specs). Do not copy generated imports, for targets, or derive/check ' + 'commands. Preserve the edited projections and baseline until copying back is complete. ' + 'Unannotated structural projections have no authored fragment; change the Rust type ' + 'or generator instead. Rust comments remain authoritative.') + + +def record_projections(work, annotations=None): + """Record full editing authority when available, or a legacy hash baseline. + + A legacy baseline can protect files from regeneration but cannot authorize + reverse edits, because it contains no owned source spans or saved payloads. + """ + if annotations is not None: + import copy_back + copy_back.record(work, annotations) + return + state = {'version': 1, 'sha256': {module: hashlib.sha256( + (work / f'{module}.lean').read_bytes()).hexdigest() for module in PROJECTIONS}} + (work / PROJECTION_STATE).write_text(json.dumps(state, indent=2) + '\n') + + +def module_name(relative): + """Keep file components identical to their resolved Lean Name segments.""" + components = (*relative.parts[:-1], relative.stem) + if any(not re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*', part) for part in components): + raise ValueError(f'Handwritten Lean module paths require simple identifier components: {relative}') + return '.'.join(components) + + +def handwritten(root): + """Discover native modules while excluding generated files and Lake caches. + + Validate each path before returning it: a filename containing a literal dot + must not silently become the same Lean name as a nested directory path. + """ + sources = root / 'verification/aeneas/lean' + paths = sorted(path.relative_to(sources) for path in sources.rglob('*.lean') + if '.lake' not in path.relative_to(sources).parts + and str(path.relative_to(sources)) not in GENERATED) + for path in paths: + module_name(path) + return paths + + +def retire_generated(work): + """Remove only recognized artifacts of the retired invariant generator.""" + old = work / 'Invariants.lean' + mapping = work / 'Invariants.source-map.json' + if old.exists(): + text = old.read_text() + if not (text.startswith(golden.HEADER) and 'public import DeriveValidity\n' in text + and 'namespace Zerocopy.Invariants\n' in text): + raise ValueError('Refusing to retire unrecognized Invariants.lean; preserve or relocate this file before regeneration') + if mapping.exists(): + locations = json.loads(mapping.read_text()) + if not isinstance(locations, dict) or any( + not isinstance(key, str) or not key.isdecimal() + or not isinstance(value, dict) or set(value) != {'file', 'line'} + or not isinstance(value['file'], str) or not isinstance(value['line'], int) + for key, value in locations.items()): + raise ValueError('Refusing to retire unrecognized Invariants.source-map.json') + if old.exists(): + old.unlink() + if mapping.exists(): + mapping.unlink() + + +def model_support(root): + """Discover support modules; Check audits their compiled import closure.""" + return sorted(name for path in handwritten(root) + if (name := module_name(path)) == 'ModelSupport' or name.startswith('ModelSupport.')) + + +def initialize(root, work, backend): + """Replace a named disposable CI project with fresh handwritten sources. + + Check the destination before deletion. Development sources and their editing + baseline never belong to this replacement path, even if they look generated. + """ + # Only the two disposable CI projects may be replaced. The developer's + # project contains checked-in proof sources and must never be removed. + expected = root / 'target/aeneas' + if work.parent.resolve() != expected.resolve() or work.name not in { + 'verification', 'golden-verification'}: + raise ValueError('Fresh CI workspaces must be the disposable Aeneas projects') + if work.exists(): + shutil.rmtree(work) + work.mkdir(parents=True) + for relative in handwritten(root): + destination = work / relative + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(root / 'verification/aeneas/lean' / relative, destination) + shutil.copyfile(backend / 'lean-toolchain', work / 'lean-toolchain') + + +def run_checked(command, root, work): + """Stream Lean output unchanged and append any mapped Rust diagnostic. + + Source maps explain generated locations; they do not grant editing authority. + Preserve the command's failing status even when a diagnostic was mapped. + """ + locations = {} + for module in ('Specs', 'ModelShapes', 'Models'): + mapping = work / f'{module}.source-map.json' + locations[module] = json.loads(mapping.read_text()) if mapping.exists() else {} + with subprocess.Popen(command, cwd=work, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True) as process: + for line in process.stdout: + print(line, end='', flush=True) + match = re.search(r'(?:^|[ /])(Specs|ModelShapes|Models)\.lean:(\d+):\d+:', line) + if match and (location := locations[match[1]].get(match[2])): + kind = 'specification' if match[1] == 'Specs' else 'model' + print(f'Inline {kind}: {root / location["file"]}:{location["line"]}', + flush=True) + status = process.wait() + if status: + raise subprocess.CalledProcessError(status, command) + + +def check(root, work): + """Build native modules, then refresh the audit even when Lake caches imports.""" + run_checked(['lake', 'build'], root, work) + # Lake checks every native module with package-wide warnings as errors. + # Fresh CI initialization removes local artifacts; development keeps its + # own dependency cache and lets Lake invalidate changed modules. + # The audit also writes the proof graph; run it even when Lake caches imports. + run_checked(['lake', 'env', 'lean', '-DwarningAsError=true', 'Check.lean'], root, work) + + +def inspect_spec(root, work, spec): + """Report one compiled contract and its existing binding without rebuilding. + + Validate command identifiers before constructing Lean source. Metadata and + fingerprints describe the available project, so report their limitations + explicitly rather than treating inspection as a fresh verification result. + """ + name = spec if spec.startswith('Zerocopy.Specs.') else 'Zerocopy.Specs.' + spec + if not re.fullmatch(r'Zerocopy\.Specs\.[A-Za-z_][A-Za-z_0-9]*', name): + raise ValueError('Inspection requires a simple compiled specification name') + bindings = json.loads((work / 'bindings.json').read_text()) + entries = [entry for entry in bindings['bindings'].values() + if entry.get('kind') == 'function' and entry.get('spec') == name.rsplit('.', 1)[1]] + if len(entries) != 1: + raise ValueError('Inspection requires exactly one existing Rust/spec binding') + entry = entries[0] + if not re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*(?:\.[A-Za-z_][A-Za-z_0-9]*)*', entry['raw']): + raise ValueError('Invalid extracted declaration in existing binding') + print(f'Project: {work} ({bindings["mode"]}); Rust owner: {entry["file"]}:{entry["open_line"]}', flush=True) + print(f'Binding source/model fingerprints: {bindings["sources"]} / {bindings["model"]}', flush=True) + print(f'Pinned toolchain/configuration: {root / "verification/aeneas/toolchain.sh"}; ' + f'extraction driver: {root / "verification/aeneas/run.sh"}', flush=True) + toolchain = work / 'lean-toolchain' + print('Project toolchain: ' + (toolchain.read_text().strip() if toolchain.exists() else 'unavailable'), flush=True) + llbc = work / 'zerocopy.llbc' + if llbc.exists(): + print(f'Available extraction file: {llbc}', flush=True) + translated = json.loads(llbc.read_text())['translated'] + targets = [{'target': item['key'], **{key: value for key, value in item['value'].items() + if key != 'primitive_alignments'}} for item in translated['target_information']] + options = {key: value for key, value in translated['options'].items() + if key not in {'start_from', 'dest_file'}} + print('Available extraction target metadata: ' + json.dumps(targets), flush=True) + print('Available extraction options: ' + json.dumps(options), flush=True) + print('LLBC metadata describes this file; legacy bindings do not certify its identity with compiled imports.', flush=True) + else: + print('Extraction target/configuration metadata unavailable in this project; no target is inferred from the host.', flush=True) + premises = root / 'verification/aeneas/SEMANTICS.md' + if premises.is_file(): + print(f'External Rust/ABI premises: {premises} (Explicit Rust premise); ' + 'compiler/target layout correspondence and Rust safety remain assumptions.', flush=True) + else: + print('Detailed Rust/ABI premise document unavailable in this stage. ' + f'Available scope and translation trust boundary: {root / "verification/aeneas/README.md"}', flush=True) + graph = work / 'proof-dependencies.json' + if graph.exists(): + theorem = 'Zerocopy.Proofs.' + name.rsplit('.', 1)[1] + print('Existing audited proof graph: ' + json.dumps([item for item in json.loads(graph.read_text()) + if item['theorem'] == theorem]), flush=True) + # Reuse the native imported .olean files. A temporary command file outside + # the project cannot overwrite its sources or trigger a second Lake build. + source = (root / 'verification/aeneas/lean/InspectSpec.lean').read_text() + with tempfile.TemporaryDirectory(prefix='aeneas-inspect-') as directory: + path = Path(directory) / 'Inspect.lean' + count = len(entry['generics']) + len(entry['inputs']) + path.write_text(source + f'\ncheck_spec_binding {name} for {entry["raw"]} with {count}\n' + + 'inspect_spec ' + name + '\n') + run_checked(['lake', 'env', 'lean', '-DwarningAsError=true', str(path)], root, work) + print('Inspection reads compiled declarations; run dev.sh --check to refresh and audit edited sources.', flush=True) + + +def main(): + """Keep preview/apply editing, regeneration protection, and inspection explicit.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('command', choices=['initialize', 'check', 'protect', 'inspect', 'copy-back']) + parser.add_argument('work', type=Path) + parser.add_argument('--root', required=True, type=Path) + parser.add_argument('--backend', type=Path) + parser.add_argument('--spec', help='Short or qualified compiled specification name') + parser.add_argument('--owner', help='Full Rust annotation owner for copy-back') + parser.add_argument('--apply', action='store_true', help='Apply the previewed copy-back plan') + args = parser.parse_args() + if args.command == 'copy-back': + if not args.owner: + parser.error('copy-back requires --owner') + import copy_back + try: + copy_back.copy_back(args.root, args.work, args.owner, apply=args.apply) + except (ValueError, OSError, UnicodeError) as error: + parser.exit(1, f'Copy-back failed: {error}\n') + return + if args.owner or args.apply: + parser.error('--owner and --apply require copy-back') + root, work = args.root.resolve(), args.work.resolve() + if args.command == 'initialize': + if args.backend is None: + parser.error('initialize requires --backend') + initialize(root, work, args.backend.resolve()) + elif args.command == 'protect': + protect_projections(work) + elif args.command == 'inspect': + if not args.spec: + parser.error('inspect requires --spec') + inspect_spec(root, work, args.spec) + else: + check(root, work) + + +if __name__ == '__main__': + main() diff --git a/zerocopy/AGENTS.md b/zerocopy/AGENTS.md index 2469b82bd6..07090a2e6e 100644 --- a/zerocopy/AGENTS.md +++ b/zerocopy/AGENTS.md @@ -41,6 +41,13 @@ These instructions apply to work in the `zerocopy/` subtree. In Markdown, indent wrapped bullet continuation lines by two spaces and put a blank line after each section heading. +- **Derived traits in tests:** For traits whose documentation requires + derive-only implementation, test fixture types **MUST** use Zerocopy's + derives. Do not bypass a derive rejection with a manual implementation; + instead, test the lower-level internal primitive or extend the derive. + Generated derive-output snapshots and crate-owned built-in implementations + are exempt. + ## Task-Specific Skills Use the applicable skills in addition to these standing rules. Skills compose; diff --git a/zerocopy/benches/ref_from_bytes_dynamic_padding.x86-64 b/zerocopy/benches/ref_from_bytes_dynamic_padding.x86-64 index e844a4608f..7f695f942e 100644 --- a/zerocopy/benches/ref_from_bytes_dynamic_padding.x86-64 +++ b/zerocopy/benches/ref_from_bytes_dynamic_padding.x86-64 @@ -1,22 +1,21 @@ bench_ref_from_bytes_dynamic_padding: - test dil, 3 - jne .LBB5_3 - movabs rax, 9223372036854775804 - and rax, rsi - cmp rax, 9 - jb .LBB5_3 - add rax, -9 + cmp rsi, 12 + setae al + mov ecx, esi + or ecx, edi + test cl, 3 + sete cl + and cl, al + cmp cl, 1 + je .LBB5_2 + xor edi, edi + mov rax, rdi + ret +.LBB5_2: + add rsi, -9 movabs rcx, -6148914691236517205 + mov rax, rsi mul rcx shr rdx - lea rax, [rdx + 2*rdx] - or rax, 3 - add rax, 9 - cmp rsi, rax - je .LBB5_4 -.LBB5_3: - xor edi, edi - mov rdx, rsi -.LBB5_4: mov rax, rdi ret diff --git a/zerocopy/benches/ref_from_bytes_dynamic_padding.x86-64.mca b/zerocopy/benches/ref_from_bytes_dynamic_padding.x86-64.mca index 423ed38ba2..5eb4fd1a11 100644 --- a/zerocopy/benches/ref_from_bytes_dynamic_padding.x86-64.mca +++ b/zerocopy/benches/ref_from_bytes_dynamic_padding.x86-64.mca @@ -1,11 +1,11 @@ Iterations: 100 Instructions: 1900 -Total Cycles: 645 +Total Cycles: 641 Total uOps: 2000 Dispatch Width: 4 -uOps Per Cycle: 3.10 -IPC: 2.95 +uOps Per Cycle: 3.12 +IPC: 2.96 Block RThroughput: 5.0 @@ -18,23 +18,23 @@ Instruction Info: [6]: HasSideEffects (U) [1] [2] [3] [4] [5] [6] Instructions: - 1 1 0.33 test dil, 3 - 1 1 1.00 jne .LBB5_3 - 1 1 0.33 movabs rax, 9223372036854775804 - 1 1 0.33 and rax, rsi - 1 1 0.33 cmp rax, 9 - 1 1 1.00 jb .LBB5_3 - 1 1 0.33 add rax, -9 + 1 1 0.33 cmp rsi, 12 + 1 1 0.50 setae al + 1 1 0.33 mov ecx, esi + 1 1 0.33 or ecx, edi + 1 1 0.33 test cl, 3 + 1 1 0.50 sete cl + 1 1 0.33 and cl, al + 1 1 0.33 cmp cl, 1 + 1 1 1.00 je .LBB5_2 + 1 0 0.25 xor edi, edi + 1 1 0.33 mov rax, rdi + 1 1 1.00 U ret + 1 1 0.33 add rsi, -9 1 1 0.33 movabs rcx, -6148914691236517205 + 1 1 0.33 mov rax, rsi 2 4 1.00 mul rcx 1 1 0.50 shr rdx - 1 1 0.50 lea rax, [rdx + 2*rdx] - 1 1 0.33 or rax, 3 - 1 1 0.33 add rax, 9 - 1 1 0.33 cmp rsi, rax - 1 1 1.00 je .LBB5_4 - 1 0 0.25 xor edi, edi - 1 1 0.33 mov rdx, rsi 1 1 0.33 mov rax, rdi 1 1 1.00 U ret @@ -52,26 +52,26 @@ Resources: Resource pressure per iteration: [0] [1] [2] [3] [4] [5] [6.0] [6.1] - - - 6.32 6.33 - 6.35 - - + - - 6.33 6.33 - 6.34 - - Resource pressure by instruction: [0] [1] [2] [3] [4] [5] [6.0] [6.1] Instructions: - - - 0.64 0.35 - 0.01 - - test dil, 3 - - - - - - 1.00 - - jne .LBB5_3 - - - 0.34 0.65 - 0.01 - - movabs rax, 9223372036854775804 - - - 0.35 0.65 - - - - and rax, rsi - - - 0.33 0.34 - 0.33 - - cmp rax, 9 - - - - - - 1.00 - - jb .LBB5_3 - - - 0.35 - - 0.65 - - add rax, -9 - - - 0.97 0.01 - 0.02 - - movabs rcx, -6148914691236517205 - - - 1.00 1.00 - - - - mul rcx - - - 0.99 - - 0.01 - - shr rdx - - - 0.33 0.67 - - - - lea rax, [rdx + 2*rdx] - - - 0.34 0.66 - - - - or rax, 3 - - - 0.33 0.66 - 0.01 - - add rax, 9 - - - 0.01 0.99 - - - - cmp rsi, rax - - - - - - 1.00 - - je .LBB5_4 + - - 0.99 - - 0.01 - - cmp rsi, 12 + - - 1.00 - - - - - setae al + - - - 0.34 - 0.66 - - mov ecx, esi + - - - 0.99 - 0.01 - - or ecx, edi + - - - 1.00 - - - - test cl, 3 + - - 0.67 - - 0.33 - - sete cl + - - - 1.00 - - - - and cl, al + - - - 1.00 - - - - cmp cl, 1 + - - - - - 1.00 - - je .LBB5_2 - - - - - - - - xor edi, edi - - - 0.32 0.01 - 0.67 - - mov rdx, rsi - - - 0.02 0.34 - 0.64 - - mov rax, rdi + - - 0.66 - - 0.34 - - mov rax, rdi + - - - - - 1.00 - - ret + - - 0.35 0.01 - 0.64 - - add rsi, -9 + - - 0.65 0.33 - 0.02 - - movabs rcx, -6148914691236517205 + - - 0.01 0.33 - 0.66 - - mov rax, rsi + - - 1.00 1.00 - - - - mul rcx + - - 1.00 - - - - - shr rdx + - - - 0.33 - 0.67 - - mov rax, rdi - - - - - 1.00 - - ret diff --git a/zerocopy/benches/ref_from_bytes_dynamic_size.x86-64 b/zerocopy/benches/ref_from_bytes_dynamic_size.x86-64 index cc905b76c0..43cadf3527 100644 --- a/zerocopy/benches/ref_from_bytes_dynamic_size.x86-64 +++ b/zerocopy/benches/ref_from_bytes_dynamic_size.x86-64 @@ -1,20 +1,11 @@ bench_ref_from_bytes_dynamic_size: mov rdx, rsi - cmp rsi, 4 - setb al - or al, dil - test al, 1 - je .LBB5_2 + mov ecx, edx + or ecx, edi xor eax, eax - ret -.LBB5_2: - lea rcx, [rdx - 4] - mov rsi, rcx - and rsi, -2 - add rsi, 4 - shr rcx - xor eax, eax - cmp rdx, rsi - cmove rdx, rcx + sub rdx, 4 + cmovb rdi, rax + shr rdx + test cl, 1 cmove rax, rdi ret diff --git a/zerocopy/benches/ref_from_bytes_dynamic_size.x86-64.mca b/zerocopy/benches/ref_from_bytes_dynamic_size.x86-64.mca index 68aea583e4..444f4fad4f 100644 --- a/zerocopy/benches/ref_from_bytes_dynamic_size.x86-64.mca +++ b/zerocopy/benches/ref_from_bytes_dynamic_size.x86-64.mca @@ -1,12 +1,12 @@ Iterations: 100 -Instructions: 1800 -Total Cycles: 704 -Total uOps: 2000 +Instructions: 1000 +Total Cycles: 373 +Total uOps: 1200 Dispatch Width: 4 -uOps Per Cycle: 2.84 -IPC: 2.56 -Block RThroughput: 5.0 +uOps Per Cycle: 3.22 +IPC: 2.68 +Block RThroughput: 3.0 Instruction Info: @@ -19,21 +19,13 @@ Instruction Info: [1] [2] [3] [4] [5] [6] Instructions: 1 1 0.33 mov rdx, rsi - 1 1 0.33 cmp rsi, 4 - 1 1 0.50 setb al - 1 1 0.33 or al, dil - 1 1 0.33 test al, 1 - 1 1 1.00 je .LBB5_2 + 1 1 0.33 mov ecx, edx + 1 1 0.33 or ecx, edi 1 0 0.25 xor eax, eax - 1 1 1.00 U ret - 1 1 0.50 lea rcx, [rdx - 4] - 1 1 0.33 mov rsi, rcx - 1 1 0.33 and rsi, -2 - 1 1 0.33 add rsi, 4 - 1 1 0.50 shr rcx - 1 0 0.25 xor eax, eax - 1 1 0.33 cmp rdx, rsi - 2 2 0.67 cmove rdx, rcx + 1 1 0.33 sub rdx, 4 + 2 2 0.67 cmovb rdi, rax + 1 1 0.50 shr rdx + 1 1 0.33 test cl, 1 2 2 0.67 cmove rax, rdi 1 1 1.00 U ret @@ -51,25 +43,17 @@ Resources: Resource pressure per iteration: [0] [1] [2] [3] [4] [5] [6.0] [6.1] - - - 5.97 5.98 - 6.05 - - + - - 3.66 3.67 - 3.67 - - Resource pressure by instruction: [0] [1] [2] [3] [4] [5] [6.0] [6.1] Instructions: - - - 0.97 0.01 - 0.02 - - mov rdx, rsi - - - 0.01 0.02 - 0.97 - - cmp rsi, 4 - - - 0.03 - - 0.97 - - setb al - - - 0.01 0.02 - 0.97 - - or al, dil - - - - 0.98 - 0.02 - - test al, 1 - - - - - - 1.00 - - je .LBB5_2 - - - - - - - - - xor eax, eax - - - - - - 1.00 - - ret - - - 0.98 0.02 - - - - lea rcx, [rdx - 4] - - - 0.01 0.99 - - - - mov rsi, rcx - - - - 0.98 - 0.02 - - and rsi, -2 - - - 0.98 0.01 - 0.01 - - add rsi, 4 - - - 0.99 - - 0.01 - - shr rcx + - - 0.32 0.01 - 0.67 - - mov rdx, rsi + - - 0.33 0.35 - 0.32 - - mov ecx, edx + - - 0.01 0.34 - 0.65 - - or ecx, edi - - - - - - - - xor eax, eax - - - 0.02 0.97 - 0.01 - - cmp rdx, rsi - - - 0.99 0.99 - 0.02 - - cmove rdx, rcx - - - 0.98 0.99 - 0.03 - - cmove rax, rdi + - - 0.67 0.33 - - - - sub rdx, 4 + - - 0.99 0.98 - 0.03 - - cmovb rdi, rax + - - 0.03 - - 0.97 - - shr rdx + - - 0.32 0.67 - 0.01 - - test cl, 1 + - - 0.99 0.99 - 0.02 - - cmove rax, rdi - - - - - 1.00 - - ret diff --git a/zerocopy/benches/ref_from_prefix_dynamic_padding.x86-64 b/zerocopy/benches/ref_from_prefix_dynamic_padding.x86-64 index f380fee6c2..e7f7820a69 100644 --- a/zerocopy/benches/ref_from_prefix_dynamic_padding.x86-64 +++ b/zerocopy/benches/ref_from_prefix_dynamic_padding.x86-64 @@ -1,19 +1,19 @@ bench_ref_from_prefix_dynamic_padding: test dil, 3 + sete al + cmp rsi, 12 + setae cl + test al, cl jne .LBB5_2 - movabs rax, 9223372036854775804 - and rsi, rax - cmp rsi, 8 - ja .LBB5_3 -.LBB5_2: xor edx, edx test dil, 3 sete dl xor edi, edi mov rax, rdi ret -.LBB5_3: - add rsi, -9 +.LBB5_2: + or rsi, 3 + add rsi, -12 movabs rcx, -6148914691236517205 mov rax, rsi mul rcx diff --git a/zerocopy/benches/ref_from_prefix_dynamic_padding.x86-64.mca b/zerocopy/benches/ref_from_prefix_dynamic_padding.x86-64.mca index 499c58d94b..a3d68f37a8 100644 --- a/zerocopy/benches/ref_from_prefix_dynamic_padding.x86-64.mca +++ b/zerocopy/benches/ref_from_prefix_dynamic_padding.x86-64.mca @@ -1,12 +1,12 @@ Iterations: 100 -Instructions: 1900 -Total Cycles: 607 -Total uOps: 2000 +Instructions: 2000 +Total Cycles: 640 +Total uOps: 2100 Dispatch Width: 4 -uOps Per Cycle: 3.29 +uOps Per Cycle: 3.28 IPC: 3.13 -Block RThroughput: 5.0 +Block RThroughput: 5.3 Instruction Info: @@ -19,18 +19,19 @@ Instruction Info: [1] [2] [3] [4] [5] [6] Instructions: 1 1 0.33 test dil, 3 + 1 1 0.50 sete al + 1 1 0.33 cmp rsi, 12 + 1 1 0.50 setae cl + 1 1 0.33 test al, cl 1 1 1.00 jne .LBB5_2 - 1 1 0.33 movabs rax, 9223372036854775804 - 1 1 0.33 and rsi, rax - 1 1 0.33 cmp rsi, 8 - 1 1 1.00 ja .LBB5_3 1 0 0.25 xor edx, edx 1 1 0.33 test dil, 3 1 1 0.50 sete dl 1 0 0.25 xor edi, edi 1 1 0.33 mov rax, rdi 1 1 1.00 U ret - 1 1 0.33 add rsi, -9 + 1 1 0.33 or rsi, 3 + 1 1 0.33 add rsi, -12 1 1 0.33 movabs rcx, -6148914691236517205 1 1 0.33 mov rax, rsi 2 4 1.00 mul rcx @@ -52,26 +53,27 @@ Resources: Resource pressure per iteration: [0] [1] [2] [3] [4] [5] [6.0] [6.1] - - - 6.00 5.99 - 6.01 - - + - - 6.33 6.33 - 6.34 - - Resource pressure by instruction: [0] [1] [2] [3] [4] [5] [6.0] [6.1] Instructions: - - - 0.01 0.98 - 0.01 - - test dil, 3 + - - 0.65 0.34 - 0.01 - - test dil, 3 + - - 0.99 - - 0.01 - - sete al + - - 0.01 0.33 - 0.66 - - cmp rsi, 12 + - - 0.34 - - 0.66 - - setae cl + - - 1.00 - - - - - test al, cl - - - - - 1.00 - - jne .LBB5_2 - - - 0.98 0.01 - 0.01 - - movabs rax, 9223372036854775804 - - - 0.01 0.99 - - - - and rsi, rax - - - 0.01 0.99 - - - - cmp rsi, 8 - - - - - - 1.00 - - ja .LBB5_3 - - - - - - - - xor edx, edx - - - 0.99 0.01 - - - - test dil, 3 - - - 0.99 - - 0.01 - - sete dl + - - - 0.99 - 0.01 - - test dil, 3 + - - - - - 1.00 - - sete dl - - - - - - - - xor edi, edi - - - - 0.01 - 0.99 - - mov rax, rdi + - - - 1.00 - - - - mov rax, rdi - - - - - 1.00 - - ret - - - 0.01 0.99 - - - - add rsi, -9 - - - - 1.00 - - - - movabs rcx, -6148914691236517205 - - - 1.00 - - - - - mov rax, rsi + - - 0.01 0.99 - - - - or rsi, 3 + - - 0.33 0.67 - - - - add rsi, -12 + - - 0.66 0.34 - - - - movabs rcx, -6148914691236517205 + - - 0.34 0.33 - 0.33 - - mov rax, rsi - - 1.00 1.00 - - - - mul rcx - - 1.00 - - - - - shr rdx - - - - 0.01 - 0.99 - - mov rax, rdi + - - - 0.34 - 0.66 - - mov rax, rdi - - - - - 1.00 - - ret diff --git a/zerocopy/benches/ref_from_suffix_dynamic_padding.x86-64 b/zerocopy/benches/ref_from_suffix_dynamic_padding.x86-64 index 3e05f6023f..68f21fe801 100644 --- a/zerocopy/benches/ref_from_suffix_dynamic_padding.x86-64 +++ b/zerocopy/benches/ref_from_suffix_dynamic_padding.x86-64 @@ -1,23 +1,22 @@ bench_ref_from_suffix_dynamic_padding: lea eax, [rsi + rdi] test al, 3 - jne .LBB5_1 - movabs rax, 9223372036854775804 - and rax, rsi - cmp rax, 9 - jae .LBB5_3 -.LBB5_1: - xor eax, eax + setne al + cmp rsi, 12 + setb cl + or cl, al + je .LBB5_2 + xor edi, edi + mov rax, rdi ret -.LBB5_3: - add rax, -9 +.LBB5_2: + mov rax, rsi + or rax, 3 + add rax, -12 movabs rcx, -6148914691236517205 mul rcx shr rdx - lea rax, [rdx + 2*rdx] - sub rsi, rax - or rax, -4 - add rsi, rdi - add rax, rsi - add rax, -8 + and esi, 3 + add rdi, rsi + mov rax, rdi ret diff --git a/zerocopy/benches/ref_from_suffix_dynamic_padding.x86-64.mca b/zerocopy/benches/ref_from_suffix_dynamic_padding.x86-64.mca index 73599d5b6a..c31e098aed 100644 --- a/zerocopy/benches/ref_from_suffix_dynamic_padding.x86-64.mca +++ b/zerocopy/benches/ref_from_suffix_dynamic_padding.x86-64.mca @@ -1,11 +1,11 @@ Iterations: 100 Instructions: 2000 -Total Cycles: 682 +Total Cycles: 673 Total uOps: 2100 Dispatch Width: 4 -uOps Per Cycle: 3.08 -IPC: 2.93 +uOps Per Cycle: 3.12 +IPC: 2.97 Block RThroughput: 5.3 @@ -20,23 +20,23 @@ Instruction Info: [1] [2] [3] [4] [5] [6] Instructions: 1 1 0.50 lea eax, [rsi + rdi] 1 1 0.33 test al, 3 - 1 1 1.00 jne .LBB5_1 - 1 1 0.33 movabs rax, 9223372036854775804 - 1 1 0.33 and rax, rsi - 1 1 0.33 cmp rax, 9 - 1 1 1.00 jae .LBB5_3 - 1 0 0.25 xor eax, eax + 1 1 0.50 setne al + 1 1 0.33 cmp rsi, 12 + 1 1 0.50 setb cl + 1 1 0.33 or cl, al + 1 1 1.00 je .LBB5_2 + 1 0 0.25 xor edi, edi + 1 1 0.33 mov rax, rdi 1 1 1.00 U ret - 1 1 0.33 add rax, -9 + 1 1 0.33 mov rax, rsi + 1 1 0.33 or rax, 3 + 1 1 0.33 add rax, -12 1 1 0.33 movabs rcx, -6148914691236517205 2 4 1.00 mul rcx 1 1 0.50 shr rdx - 1 1 0.50 lea rax, [rdx + 2*rdx] - 1 1 0.33 sub rsi, rax - 1 1 0.33 or rax, -4 - 1 1 0.33 add rsi, rdi - 1 1 0.33 add rax, rsi - 1 1 0.33 add rax, -8 + 1 1 0.33 and esi, 3 + 1 1 0.33 add rdi, rsi + 1 1 0.33 mov rax, rdi 1 1 1.00 U ret @@ -53,27 +53,27 @@ Resources: Resource pressure per iteration: [0] [1] [2] [3] [4] [5] [6.0] [6.1] - - - 6.65 6.67 - 6.68 - - + - - 6.67 6.66 - 6.67 - - Resource pressure by instruction: [0] [1] [2] [3] [4] [5] [6.0] [6.1] Instructions: - - - 0.90 0.10 - - - - lea eax, [rsi + rdi] - - - 0.93 - - 0.07 - - test al, 3 - - - - - - 1.00 - - jne .LBB5_1 - - - 0.51 0.47 - 0.02 - - movabs rax, 9223372036854775804 - - - - - - 1.00 - - and rax, rsi - - - - 0.09 - 0.91 - - cmp rax, 9 - - - - - - 1.00 - - jae .LBB5_3 - - - - - - - - - xor eax, eax + - - 0.35 0.65 - - - - lea eax, [rsi + rdi] + - - 0.01 0.99 - - - - test al, 3 + - - 1.00 - - - - - setne al + - - 0.97 - - 0.03 - - cmp rsi, 12 + - - 0.66 - - 0.34 - - setb cl + - - - 1.00 - - - - or cl, al + - - - - - 1.00 - - je .LBB5_2 + - - - - - - - - xor edi, edi + - - 0.01 0.01 - 0.98 - - mov rax, rdi - - - - - 1.00 - - ret - - - 0.43 0.47 - 0.10 - - add rax, -9 - - - 0.42 0.39 - 0.19 - - movabs rcx, -6148914691236517205 + - - 0.01 0.99 - - - - mov rax, rsi + - - - 0.34 - 0.66 - - or rax, 3 + - - - 1.00 - - - - add rax, -12 + - - 0.66 0.31 - 0.03 - - movabs rcx, -6148914691236517205 - - 1.00 1.00 - - - - mul rcx - - - 0.69 - - 0.31 - - shr rdx - - - 0.54 0.46 - - - - lea rax, [rdx + 2*rdx] - - - 0.07 0.91 - 0.02 - - sub rsi, rax - - - 0.91 0.05 - 0.04 - - or rax, -4 - - - 0.08 0.90 - 0.02 - - add rsi, rdi - - - 0.09 0.91 - - - - add rax, rsi - - - 0.08 0.92 - - - - add rax, -8 + - - 1.00 - - - - - shr rdx + - - 0.67 0.01 - 0.32 - - and esi, 3 + - - 0.32 0.01 - 0.67 - - add rdi, rsi + - - 0.01 0.35 - 0.64 - - mov rax, rdi - - - - - 1.00 - - ret diff --git a/zerocopy/benches/split_via_runtime_check_dynamic_padding.x86-64 b/zerocopy/benches/split_via_runtime_check_dynamic_padding.x86-64 index 03684cb09f..0a8a4f501a 100644 --- a/zerocopy/benches/split_via_runtime_check_dynamic_padding.x86-64 +++ b/zerocopy/benches/split_via_runtime_check_dynamic_padding.x86-64 @@ -1,9 +1,8 @@ bench_split_via_runtime_check_dynamic_padding: mov rax, rdi mov rdx, qword ptr [rsi + 16] - mov ecx, edx - and ecx, 3 - cmp ecx, 1 + lea ecx, [rdx - 1] + test cl, 3 jne .LBB5_1 mov rcx, qword ptr [rsi] mov rsi, qword ptr [rsi + 8] diff --git a/zerocopy/benches/split_via_runtime_check_dynamic_padding.x86-64.mca b/zerocopy/benches/split_via_runtime_check_dynamic_padding.x86-64.mca index 5034ab0583..7140b06e88 100644 --- a/zerocopy/benches/split_via_runtime_check_dynamic_padding.x86-64.mca +++ b/zerocopy/benches/split_via_runtime_check_dynamic_padding.x86-64.mca @@ -1,11 +1,11 @@ Iterations: 100 -Instructions: 2000 +Instructions: 1900 Total Cycles: 708 -Total uOps: 2000 +Total uOps: 1900 Dispatch Width: 4 -uOps Per Cycle: 2.82 -IPC: 2.82 +uOps Per Cycle: 2.68 +IPC: 2.68 Block RThroughput: 5.0 @@ -20,9 +20,8 @@ Instruction Info: [1] [2] [3] [4] [5] [6] Instructions: 1 1 0.33 mov rax, rdi 1 5 0.50 * mov rdx, qword ptr [rsi + 16] - 1 1 0.33 mov ecx, edx - 1 1 0.33 and ecx, 3 - 1 1 0.33 cmp ecx, 1 + 1 1 0.50 lea ecx, [rdx - 1] + 1 1 0.33 test cl, 3 1 1 1.00 jne .LBB5_1 1 5 0.50 * mov rcx, qword ptr [rsi] 1 5 0.50 * mov rsi, qword ptr [rsi + 8] @@ -53,22 +52,21 @@ Resources: Resource pressure per iteration: [0] [1] [2] [3] [4] [5] [6.0] [6.1] - - - 3.00 3.02 5.00 4.98 4.00 4.00 + - - 2.99 3.00 5.00 4.01 4.00 4.00 Resource pressure by instruction: [0] [1] [2] [3] [4] [5] [6.0] [6.1] Instructions: - - - - 0.99 - 0.01 - - mov rax, rdi + - - 0.99 - - 0.01 - - mov rax, rdi - - - - - - - 1.00 mov rdx, qword ptr [rsi + 16] - - - 0.99 0.01 - - - - mov ecx, edx - - - 0.99 0.01 - - - - and ecx, 3 - - - 0.97 0.03 - - - - cmp ecx, 1 + - - - 1.00 - - - - lea ecx, [rdx - 1] + - - - 0.98 - 0.02 - - test cl, 3 - - - - - 1.00 - - jne .LBB5_1 - - - - - - 1.00 - mov rcx, qword ptr [rsi] - - - - - - 0.99 0.01 mov rsi, qword ptr [rsi + 8] - - - 0.01 0.99 - - - - lea rdi, [rdx + 2*rdx] - - - - 0.96 - 0.04 - - add rdi, rcx - - - 0.03 - - 0.97 - - add rdi, 9 - - - 0.01 0.03 - 0.96 - - sub rsi, rdx + - - 1.00 - - - - - lea rdi, [rdx + 2*rdx] + - - 0.01 0.01 - 0.98 - - add rdi, rcx + - - - 1.00 - - - - add rdi, 9 + - - 0.99 0.01 - - - - sub rsi, rdx - - - - 1.00 - 1.00 - mov qword ptr [rax + 8], rdx - - - - 1.00 - - 1.00 mov qword ptr [rax + 16], rdi - - - - 1.00 - 0.01 0.99 mov qword ptr [rax + 24], rsi diff --git a/zerocopy/benches/try_ref_from_bytes_dynamic_padding.x86-64 b/zerocopy/benches/try_ref_from_bytes_dynamic_padding.x86-64 index 217c5fc617..5584e90541 100644 --- a/zerocopy/benches/try_ref_from_bytes_dynamic_padding.x86-64 +++ b/zerocopy/benches/try_ref_from_bytes_dynamic_padding.x86-64 @@ -1,24 +1,22 @@ bench_try_ref_from_bytes_dynamic_padding: - test dil, 3 - jne .LBB5_4 - movabs rax, 9223372036854775804 - and rax, rsi - cmp rax, 9 - jb .LBB5_4 - add rax, -9 + cmp rsi, 12 + setae al + mov ecx, esi + or ecx, edi + test cl, 3 + sete cl + and cl, al + cmp cl, 1 + je .LBB5_2 + xor eax, eax + ret +.LBB5_2: + add rsi, -9 movabs rcx, -6148914691236517205 + mov rax, rsi mul rcx shr rdx - lea rax, [rdx + 2*rdx] - or rax, 3 - add rax, 9 - cmp rsi, rax - jne .LBB5_4 + xor eax, eax cmp word ptr [rdi], -16192 - je .LBB5_5 -.LBB5_4: - xor edi, edi - mov rdx, rsi -.LBB5_5: - mov rax, rdi + cmove rax, rdi ret diff --git a/zerocopy/benches/try_ref_from_bytes_dynamic_padding.x86-64.mca b/zerocopy/benches/try_ref_from_bytes_dynamic_padding.x86-64.mca index 95b993c7e0..ccd610c7ef 100644 --- a/zerocopy/benches/try_ref_from_bytes_dynamic_padding.x86-64.mca +++ b/zerocopy/benches/try_ref_from_bytes_dynamic_padding.x86-64.mca @@ -1,10 +1,10 @@ Iterations: 100 -Instructions: 2100 -Total Cycles: 709 +Instructions: 2000 +Total Cycles: 675 Total uOps: 2300 Dispatch Width: 4 -uOps Per Cycle: 3.24 +uOps Per Cycle: 3.41 IPC: 2.96 Block RThroughput: 5.8 @@ -18,26 +18,25 @@ Instruction Info: [6]: HasSideEffects (U) [1] [2] [3] [4] [5] [6] Instructions: - 1 1 0.33 test dil, 3 - 1 1 1.00 jne .LBB5_4 - 1 1 0.33 movabs rax, 9223372036854775804 - 1 1 0.33 and rax, rsi - 1 1 0.33 cmp rax, 9 - 1 1 1.00 jb .LBB5_4 - 1 1 0.33 add rax, -9 + 1 1 0.33 cmp rsi, 12 + 1 1 0.50 setae al + 1 1 0.33 mov ecx, esi + 1 1 0.33 or ecx, edi + 1 1 0.33 test cl, 3 + 1 1 0.50 sete cl + 1 1 0.33 and cl, al + 1 1 0.33 cmp cl, 1 + 1 1 1.00 je .LBB5_2 + 1 0 0.25 xor eax, eax + 1 1 1.00 U ret + 1 1 0.33 add rsi, -9 1 1 0.33 movabs rcx, -6148914691236517205 + 1 1 0.33 mov rax, rsi 2 4 1.00 mul rcx 1 1 0.50 shr rdx - 1 1 0.50 lea rax, [rdx + 2*rdx] - 1 1 0.33 or rax, 3 - 1 1 0.33 add rax, 9 - 1 1 0.33 cmp rsi, rax - 1 1 1.00 jne .LBB5_4 + 1 0 0.25 xor eax, eax 2 6 0.50 * cmp word ptr [rdi], -16192 - 1 1 1.00 je .LBB5_5 - 1 0 0.25 xor edi, edi - 1 1 0.33 mov rdx, rsi - 1 1 0.33 mov rax, rdi + 2 2 0.67 cmove rax, rdi 1 1 1.00 U ret @@ -54,28 +53,27 @@ Resources: Resource pressure per iteration: [0] [1] [2] [3] [4] [5] [6.0] [6.1] - - - 6.98 6.99 - 7.03 0.50 0.50 + - - 6.67 6.66 - 6.67 0.50 0.50 Resource pressure by instruction: [0] [1] [2] [3] [4] [5] [6.0] [6.1] Instructions: - - - 0.48 0.51 - 0.01 - - test dil, 3 - - - - - - 1.00 - - jne .LBB5_4 - - - 0.51 0.49 - - - - movabs rax, 9223372036854775804 - - - 0.01 0.99 - - - - and rax, rsi - - - 0.51 0.49 - - - - cmp rax, 9 - - - - - - 1.00 - - jb .LBB5_4 - - - 0.98 - - 0.02 - - add rax, -9 - - - 0.98 0.02 - - - - movabs rcx, -6148914691236517205 + - - 0.33 0.66 - 0.01 - - cmp rsi, 12 + - - 1.00 - - - - - setae al + - - 0.66 0.34 - - - - mov ecx, esi + - - - 0.99 - 0.01 - - or ecx, edi + - - - 0.67 - 0.33 - - test cl, 3 + - - 0.67 - - 0.33 - - sete cl + - - 0.33 0.34 - 0.33 - - and cl, al + - - 0.33 - - 0.67 - - cmp cl, 1 + - - - - - 1.00 - - je .LBB5_2 + - - - - - - - - xor eax, eax + - - - - - 1.00 - - ret + - - 0.67 0.33 - - - - add rsi, -9 + - - 0.99 0.01 - - - - movabs rcx, -6148914691236517205 + - - - 0.99 - 0.01 - - mov rax, rsi - - 1.00 1.00 - - - - mul rcx - - - 0.99 - - 0.01 - - shr rdx - - - - 1.00 - - - - lea rax, [rdx + 2*rdx] - - - - 0.51 - 0.49 - - or rax, 3 - - - 0.01 0.49 - 0.50 - - add rax, 9 - - - - 0.02 - 0.98 - - cmp rsi, rax - - - - - - 1.00 - - jne .LBB5_4 - - - 0.51 0.49 - - 0.50 0.50 cmp word ptr [rdi], -16192 - - - - - - 1.00 - - je .LBB5_5 - - - - - - - - - xor edi, edi - - - 0.50 0.50 - - - - mov rdx, rsi - - - 0.50 0.48 - 0.02 - - mov rax, rdi + - - 0.01 - - 0.99 - - shr rdx + - - - - - - - - xor eax, eax + - - 0.67 0.33 - - 0.50 0.50 cmp word ptr [rdi], -16192 + - - 0.01 1.00 - 0.99 - - cmove rax, rdi - - - - - 1.00 - - ret diff --git a/zerocopy/benches/try_ref_from_bytes_dynamic_size.x86-64 b/zerocopy/benches/try_ref_from_bytes_dynamic_size.x86-64 index cf67afd31c..39b12e14ee 100644 --- a/zerocopy/benches/try_ref_from_bytes_dynamic_size.x86-64 +++ b/zerocopy/benches/try_ref_from_bytes_dynamic_size.x86-64 @@ -1,22 +1,21 @@ bench_try_ref_from_bytes_dynamic_size: - mov rdx, rsi - mov rax, rdi cmp rsi, 4 - setb cl - or cl, al + setae al + mov ecx, esi + or ecx, edi test cl, 1 - jne .LBB5_4 - lea rcx, [rdx - 4] - mov rsi, rcx - and rsi, -2 - add rsi, 4 - cmp rdx, rsi - jne .LBB5_4 - cmp word ptr [rax], -16192 - jne .LBB5_4 - shr rcx - mov rdx, rcx + sete cl + and cl, al + cmp cl, 1 + je .LBB5_2 + xor eax, eax ret -.LBB5_4: +.LBB5_2: + lea rdx, [rsi - 4] + shr rdx + movzx ecx, word ptr [rdi] xor eax, eax + cmp ecx, 49344 + cmovne rdx, rsi + cmove rax, rdi ret diff --git a/zerocopy/benches/try_ref_from_bytes_dynamic_size.x86-64.mca b/zerocopy/benches/try_ref_from_bytes_dynamic_size.x86-64.mca index ecd7a18f6d..eecf3a1554 100644 --- a/zerocopy/benches/try_ref_from_bytes_dynamic_size.x86-64.mca +++ b/zerocopy/benches/try_ref_from_bytes_dynamic_size.x86-64.mca @@ -1,10 +1,10 @@ Iterations: 100 -Instructions: 2000 -Total Cycles: 639 +Instructions: 1900 +Total Cycles: 607 Total uOps: 2100 Dispatch Width: 4 -uOps Per Cycle: 3.29 +uOps Per Cycle: 3.46 IPC: 3.13 Block RThroughput: 5.3 @@ -18,25 +18,24 @@ Instruction Info: [6]: HasSideEffects (U) [1] [2] [3] [4] [5] [6] Instructions: - 1 1 0.33 mov rdx, rsi - 1 1 0.33 mov rax, rdi 1 1 0.33 cmp rsi, 4 - 1 1 0.50 setb cl - 1 1 0.33 or cl, al + 1 1 0.50 setae al + 1 1 0.33 mov ecx, esi + 1 1 0.33 or ecx, edi 1 1 0.33 test cl, 1 - 1 1 1.00 jne .LBB5_4 - 1 1 0.50 lea rcx, [rdx - 4] - 1 1 0.33 mov rsi, rcx - 1 1 0.33 and rsi, -2 - 1 1 0.33 add rsi, 4 - 1 1 0.33 cmp rdx, rsi - 1 1 1.00 jne .LBB5_4 - 2 6 0.50 * cmp word ptr [rax], -16192 - 1 1 1.00 jne .LBB5_4 - 1 1 0.50 shr rcx - 1 1 0.33 mov rdx, rcx + 1 1 0.50 sete cl + 1 1 0.33 and cl, al + 1 1 0.33 cmp cl, 1 + 1 1 1.00 je .LBB5_2 + 1 0 0.25 xor eax, eax 1 1 1.00 U ret + 1 1 0.50 lea rdx, [rsi - 4] + 1 1 0.50 shr rdx + 1 5 0.50 * movzx ecx, word ptr [rdi] 1 0 0.25 xor eax, eax + 1 1 0.33 cmp ecx, 49344 + 2 2 0.67 cmovne rdx, rsi + 2 2 0.67 cmove rax, rdi 1 1 1.00 U ret @@ -53,27 +52,26 @@ Resources: Resource pressure per iteration: [0] [1] [2] [3] [4] [5] [6.0] [6.1] - - - 6.32 6.32 - 6.36 0.50 0.50 + - - 5.99 5.99 - 6.02 0.50 0.50 Resource pressure by instruction: [0] [1] [2] [3] [4] [5] [6.0] [6.1] Instructions: - - - 0.33 0.66 - 0.01 - - mov rdx, rsi - - - 0.66 0.34 - - - - mov rax, rdi - - - 0.34 0.66 - - - - cmp rsi, 4 - - - 0.99 - - 0.01 - - setb cl - - - 0.01 0.99 - - - - or cl, al - - - - 1.00 - - - - test cl, 1 - - - - - - 1.00 - - jne .LBB5_4 - - - 0.66 0.34 - - - - lea rcx, [rdx - 4] - - - 0.33 0.66 - 0.01 - - mov rsi, rcx - - - 1.00 - - - - - and rsi, -2 - - - 0.66 0.34 - - - - add rsi, 4 - - - - 1.00 - - - - cmp rdx, rsi - - - - - - 1.00 - - jne .LBB5_4 - - - - - - 1.00 0.50 0.50 cmp word ptr [rax], -16192 - - - - - - 1.00 - - jne .LBB5_4 - - - 0.67 - - 0.33 - - shr rcx - - - 0.67 0.33 - - - - mov rdx, rcx + - - 0.02 0.97 - 0.01 - - cmp rsi, 4 + - - 1.00 - - - - - setae al + - - - 1.00 - - - - mov ecx, esi + - - 0.93 0.05 - 0.02 - - or ecx, edi + - - 0.94 0.06 - - - - test cl, 1 + - - 0.99 - - 0.01 - - sete cl + - - 0.98 0.02 - - - - and cl, al + - - 0.98 0.02 - - - - cmp cl, 1 + - - - - - 1.00 - - je .LBB5_2 + - - - - - - - - xor eax, eax - - - - - 1.00 - - ret + - - 0.01 0.99 - - - - lea rdx, [rsi - 4] + - - - - - 1.00 - - shr rdx + - - - - - - 0.50 0.50 movzx ecx, word ptr [rdi] - - - - - - - - xor eax, eax + - - 0.02 0.96 - 0.02 - - cmp ecx, 49344 + - - 0.05 0.97 - 0.98 - - cmovne rdx, rsi + - - 0.07 0.95 - 0.98 - - cmove rax, rdi - - - - - 1.00 - - ret diff --git a/zerocopy/benches/try_ref_from_prefix_dynamic_padding.x86-64 b/zerocopy/benches/try_ref_from_prefix_dynamic_padding.x86-64 index aa4ddaaca7..ff64bb6595 100644 --- a/zerocopy/benches/try_ref_from_prefix_dynamic_padding.x86-64 +++ b/zerocopy/benches/try_ref_from_prefix_dynamic_padding.x86-64 @@ -1,18 +1,18 @@ bench_try_ref_from_prefix_dynamic_padding: test dil, 3 + sete al + cmp rsi, 12 + setae cl + test al, cl jne .LBB5_2 - movabs rax, 9223372036854775804 - and rsi, rax - cmp rsi, 8 - ja .LBB5_3 -.LBB5_2: xor edx, edx test dil, 3 sete dl xor eax, eax ret -.LBB5_3: - add rsi, -9 +.LBB5_2: + or rsi, 3 + add rsi, -12 movabs rcx, -6148914691236517205 mov rax, rsi mul rcx diff --git a/zerocopy/benches/try_ref_from_prefix_dynamic_padding.x86-64.mca b/zerocopy/benches/try_ref_from_prefix_dynamic_padding.x86-64.mca index f595d499cf..29225404a2 100644 --- a/zerocopy/benches/try_ref_from_prefix_dynamic_padding.x86-64.mca +++ b/zerocopy/benches/try_ref_from_prefix_dynamic_padding.x86-64.mca @@ -1,12 +1,12 @@ Iterations: 100 -Instructions: 2500 -Total Cycles: 808 -Total uOps: 2800 +Instructions: 2600 +Total Cycles: 843 +Total uOps: 2900 Dispatch Width: 4 -uOps Per Cycle: 3.47 -IPC: 3.09 -Block RThroughput: 7.0 +uOps Per Cycle: 3.44 +IPC: 3.08 +Block RThroughput: 7.3 Instruction Info: @@ -19,17 +19,18 @@ Instruction Info: [1] [2] [3] [4] [5] [6] Instructions: 1 1 0.33 test dil, 3 + 1 1 0.50 sete al + 1 1 0.33 cmp rsi, 12 + 1 1 0.50 setae cl + 1 1 0.33 test al, cl 1 1 1.00 jne .LBB5_2 - 1 1 0.33 movabs rax, 9223372036854775804 - 1 1 0.33 and rsi, rax - 1 1 0.33 cmp rsi, 8 - 1 1 1.00 ja .LBB5_3 1 0 0.25 xor edx, edx 1 1 0.33 test dil, 3 1 1 0.50 sete dl 1 0 0.25 xor eax, eax 1 1 1.00 U ret - 1 1 0.33 add rsi, -9 + 1 1 0.33 or rsi, 3 + 1 1 0.33 add rsi, -12 1 1 0.33 movabs rcx, -6148914691236517205 1 1 0.33 mov rax, rsi 2 4 1.00 mul rcx @@ -58,32 +59,33 @@ Resources: Resource pressure per iteration: [0] [1] [2] [3] [4] [5] [6.0] [6.1] - - - 8.00 8.00 - 8.00 0.50 0.50 + - - 8.33 8.33 - 8.34 0.50 0.50 Resource pressure by instruction: [0] [1] [2] [3] [4] [5] [6.0] [6.1] Instructions: - - - 0.97 0.02 - 0.01 - - test dil, 3 + - - 0.01 0.98 - 0.01 - - test dil, 3 + - - 0.98 - - 0.02 - - sete al + - - - 1.00 - - - - cmp rsi, 12 + - - - - - 1.00 - - setae cl + - - 0.01 0.03 - 0.96 - - test al, cl - - - - - 1.00 - - jne .LBB5_2 - - - 0.99 0.01 - - - - movabs rax, 9223372036854775804 - - - 0.02 0.98 - - - - and rsi, rax - - - 0.03 0.97 - - - - cmp rsi, 8 - - - - - - 1.00 - - ja .LBB5_3 - - - - - - - - xor edx, edx - - - 0.98 0.02 - - - - test dil, 3 + - - - 1.00 - - - - test dil, 3 - - 0.99 - - 0.01 - - sete dl - - - - - - - - xor eax, eax - - - - - 1.00 - - ret - - - 0.98 0.02 - - - - add rsi, -9 - - - 0.02 0.98 - - - - movabs rcx, -6148914691236517205 - - - 0.98 0.02 - - - - mov rax, rsi + - - - 1.00 - - - - or rsi, 3 + - - 0.01 0.99 - - - - add rsi, -12 + - - 0.01 0.99 - - - - movabs rcx, -6148914691236517205 + - - 0.34 0.66 - - - - mov rax, rsi - - 1.00 1.00 - - - - mul rcx - - - - 0.01 - 0.99 - - mov rax, rdx - - - 0.01 - - 0.99 - - shr rax + - - 0.99 - - 0.01 - - mov rax, rdx + - - 1.00 - - - - - shr rax - - - - - - 0.50 0.50 movzx ecx, word ptr [rdi] - - - - 0.99 - 0.01 - - cmp cx, -16192 - - - 0.99 0.01 - - - - mov edx, 2 - - - 0.01 1.00 - 0.99 - - cmove rdx, rax + - - 0.34 0.32 - 0.34 - - cmp cx, -16192 + - - 0.33 0.01 - 0.66 - - mov edx, 2 + - - 0.99 0.01 - 1.00 - - cmove rdx, rax - - - - - - - - xor eax, eax - - - 0.02 0.98 - - - - cmp ecx, 49344 - - - 0.01 0.99 - 1.00 - - cmove rax, rdi + - - 0.33 0.33 - 0.34 - - cmp ecx, 49344 + - - 1.00 0.01 - 0.99 - - cmove rax, rdi - - - - - 1.00 - - ret diff --git a/zerocopy/benches/try_ref_from_suffix_dynamic_padding.x86-64 b/zerocopy/benches/try_ref_from_suffix_dynamic_padding.x86-64 index b3e9244428..cee9a31790 100644 --- a/zerocopy/benches/try_ref_from_suffix_dynamic_padding.x86-64 +++ b/zerocopy/benches/try_ref_from_suffix_dynamic_padding.x86-64 @@ -1,26 +1,23 @@ bench_try_ref_from_suffix_dynamic_padding: lea eax, [rsi + rdi] test al, 3 - jne .LBB5_1 - movabs rax, 9223372036854775804 - and rax, rsi - cmp rax, 9 - jae .LBB5_3 -.LBB5_1: + setne al + cmp rsi, 12 + setb cl + or cl, al + je .LBB5_2 xor eax, eax ret -.LBB5_3: - add rax, -9 +.LBB5_2: + mov rax, rsi + or rax, 3 + add rax, -12 movabs rcx, -6148914691236517205 mul rcx shr rdx - lea rcx, [rdx + 2*rdx] - sub rsi, rcx - or rcx, -4 - add rsi, rdi - lea rdi, [rcx + rsi] - add rdi, -8 + and esi, 3 + lea rcx, [rdi + rsi] xor eax, eax - cmp word ptr [rcx + rsi - 8], -16192 - cmove rax, rdi + cmp word ptr [rdi + rsi], -16192 + cmove rax, rcx ret diff --git a/zerocopy/benches/try_ref_from_suffix_dynamic_padding.x86-64.mca b/zerocopy/benches/try_ref_from_suffix_dynamic_padding.x86-64.mca index d56ae56d85..92df4484b8 100644 --- a/zerocopy/benches/try_ref_from_suffix_dynamic_padding.x86-64.mca +++ b/zerocopy/benches/try_ref_from_suffix_dynamic_padding.x86-64.mca @@ -1,12 +1,12 @@ Iterations: 100 -Instructions: 2300 -Total Cycles: 791 -Total uOps: 2600 +Instructions: 2100 +Total Cycles: 710 +Total uOps: 2400 Dispatch Width: 4 -uOps Per Cycle: 3.29 -IPC: 2.91 -Block RThroughput: 6.5 +uOps Per Cycle: 3.38 +IPC: 2.96 +Block RThroughput: 6.0 Instruction Info: @@ -20,26 +20,24 @@ Instruction Info: [1] [2] [3] [4] [5] [6] Instructions: 1 1 0.50 lea eax, [rsi + rdi] 1 1 0.33 test al, 3 - 1 1 1.00 jne .LBB5_1 - 1 1 0.33 movabs rax, 9223372036854775804 - 1 1 0.33 and rax, rsi - 1 1 0.33 cmp rax, 9 - 1 1 1.00 jae .LBB5_3 + 1 1 0.50 setne al + 1 1 0.33 cmp rsi, 12 + 1 1 0.50 setb cl + 1 1 0.33 or cl, al + 1 1 1.00 je .LBB5_2 1 0 0.25 xor eax, eax 1 1 1.00 U ret - 1 1 0.33 add rax, -9 + 1 1 0.33 mov rax, rsi + 1 1 0.33 or rax, 3 + 1 1 0.33 add rax, -12 1 1 0.33 movabs rcx, -6148914691236517205 2 4 1.00 mul rcx 1 1 0.50 shr rdx - 1 1 0.50 lea rcx, [rdx + 2*rdx] - 1 1 0.33 sub rsi, rcx - 1 1 0.33 or rcx, -4 - 1 1 0.33 add rsi, rdi - 1 1 0.50 lea rdi, [rcx + rsi] - 1 1 0.33 add rdi, -8 + 1 1 0.33 and esi, 3 + 1 1 0.50 lea rcx, [rdi + rsi] 1 0 0.25 xor eax, eax - 2 6 0.50 * cmp word ptr [rcx + rsi - 8], -16192 - 2 2 0.67 cmove rax, rdi + 2 6 0.50 * cmp word ptr [rdi + rsi], -16192 + 2 2 0.67 cmove rax, rcx 1 1 1.00 U ret @@ -56,30 +54,28 @@ Resources: Resource pressure per iteration: [0] [1] [2] [3] [4] [5] [6.0] [6.1] - - - 7.70 7.58 - 7.72 0.50 0.50 + - - 7.00 7.00 - 7.00 0.50 0.50 Resource pressure by instruction: [0] [1] [2] [3] [4] [5] [6.0] [6.1] Instructions: - - - 0.26 0.74 - - - - lea eax, [rsi + rdi] - - - 0.19 0.28 - 0.53 - - test al, 3 - - - - - - 1.00 - - jne .LBB5_1 - - - 0.93 0.06 - 0.01 - - movabs rax, 9223372036854775804 - - - 0.81 0.14 - 0.05 - - and rax, rsi - - - 0.55 0.43 - 0.02 - - cmp rax, 9 - - - - - - 1.00 - - jae .LBB5_3 + - - 0.99 0.01 - - - - lea eax, [rsi + rdi] + - - 0.01 0.99 - - - - test al, 3 + - - 0.98 - - 0.02 - - setne al + - - 0.99 - - 0.01 - - cmp rsi, 12 + - - 0.02 - - 0.98 - - setb cl + - - - 0.03 - 0.97 - - or cl, al + - - - - - 1.00 - - je .LBB5_2 - - - - - - - - xor eax, eax - - - - - 1.00 - - ret - - - 0.42 0.56 - 0.02 - - add rax, -9 - - - 0.67 0.30 - 0.03 - - movabs rcx, -6148914691236517205 + - - - 1.00 - - - - mov rax, rsi + - - 0.03 0.97 - - - - or rax, 3 + - - - 1.00 - - - - add rax, -12 + - - 0.99 - - 0.01 - - movabs rcx, -6148914691236517205 - - 1.00 1.00 - - - - mul rcx - - - 0.71 - - 0.29 - - shr rdx - - - 0.32 0.68 - - - - lea rcx, [rdx + 2*rdx] - - - 0.57 0.04 - 0.39 - - sub rsi, rcx - - - 0.28 0.67 - 0.05 - - or rcx, -4 - - - 0.29 0.29 - 0.42 - - add rsi, rdi - - - 0.02 0.98 - - - - lea rdi, [rcx + rsi] - - - 0.02 0.41 - 0.57 - - add rdi, -8 + - - 0.01 - - 0.99 - - shr rdx + - - 1.00 - - - - - and esi, 3 + - - 0.97 0.03 - - - - lea rcx, [rdi + rsi] - - - - - - - - xor eax, eax - - - 0.57 0.01 - 0.42 0.50 0.50 cmp word ptr [rcx + rsi - 8], -16192 - - - 0.09 0.99 - 0.92 - - cmove rax, rdi + - - - 0.97 - 0.03 0.50 0.50 cmp word ptr [rdi + rsi], -16192 + - - 0.01 1.00 - 0.99 - - cmove rax, rcx - - - - - 1.00 - - ret diff --git a/zerocopy/src/layout.rs b/zerocopy/src/layout.rs deleted file mode 100644 index d58786d637..0000000000 --- a/zerocopy/src/layout.rs +++ /dev/null @@ -1,2227 +0,0 @@ -// SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT -// -// Copyright 2024 The Fuchsia Authors -// -// Licensed under the 2-Clause BSD License , Apache License, Version 2.0 -// , or the MIT -// license , at your option. -// This file may not be copied, modified, or distributed except according to -// those terms. - -use core::{mem, num::NonZeroUsize}; - -use crate::util; - -/// The target pointer width, counted in bits. -const POINTER_WIDTH_BITS: usize = mem::size_of::() * 8; - -/// The layout of a type which might be dynamically-sized. -/// -/// `DstLayout` describes the layout of sized types, slice types, and "slice -/// DSTs" - ie, those that are known by the type system to have a trailing slice -/// (as distinguished from `dyn Trait` types - such types *might* have a -/// trailing slice type, but the type system isn't aware of it). -/// -/// Note that `DstLayout` does not have any internal invariants, so no guarantee -/// is made that a `DstLayout` conforms to any of Rust's requirements regarding -/// the layout of real Rust types or instances of types. -#[doc(hidden)] -#[allow(missing_debug_implementations, missing_copy_implementations)] -#[cfg_attr(any(kani, test), derive(Debug, PartialEq, Eq))] -#[derive(Copy, Clone)] -pub struct DstLayout { - pub(crate) align: NonZeroUsize, - pub(crate) size_info: SizeInfo, - // Is it guaranteed statically (without knowing a value's runtime metadata) - // that the top-level type contains no padding? This does *not* apply - // recursively - for example, `[(u8, u16)]` has `statically_shallow_unpadded - // = true` even though this type likely has padding inside each `(u8, u16)`. - pub(crate) statically_shallow_unpadded: bool, -} - -#[cfg_attr(any(kani, test), derive(Debug, PartialEq, Eq))] -#[derive(Copy, Clone)] -pub(crate) enum SizeInfo { - Sized { size: usize }, - SliceDst(TrailingSliceLayout), -} - -#[cfg_attr(any(kani, test), derive(Debug, PartialEq, Eq))] -#[derive(Copy, Clone)] -pub(crate) struct TrailingSliceLayout { - // The offset of the first byte of the trailing slice field. Note that this - // is NOT the same as the minimum size of the type. For example, consider - // the following type: - // - // struct Foo { - // a: u16, - // b: u8, - // c: [u8], - // } - // - // In `Foo`, `c` is at byte offset 3. When `c.len() == 0`, `c` is followed - // by a padding byte. - pub(crate) offset: usize, - // The size of the element type of the trailing slice field. - pub(crate) elem_size: E, -} - -impl SizeInfo { - /// Attempts to create a `SizeInfo` from `Self` in which `elem_size` is a - /// `NonZeroUsize`. If `elem_size` is 0, returns `None`. - #[allow(unused)] - #[cfg_attr(not(zerocopy_inline_always), inline)] - #[cfg_attr(zerocopy_inline_always, inline(always))] - const fn try_to_nonzero_elem_size(&self) -> Option> { - Some(match *self { - SizeInfo::Sized { size } => SizeInfo::Sized { size }, - SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size }) => { - if let Some(elem_size) = NonZeroUsize::new(elem_size) { - SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size }) - } else { - return None; - } - } - }) - } -} - -#[doc(hidden)] -#[derive(Copy, Clone)] -#[cfg_attr(test, derive(Debug))] -#[allow(missing_debug_implementations)] -pub enum CastType { - Prefix, - Suffix, -} - -#[cfg_attr(test, derive(Debug))] -pub(crate) enum MetadataCastError { - Alignment, - Size, -} - -impl DstLayout { - /// The minimum possible alignment of a type. - const MIN_ALIGN: NonZeroUsize = match NonZeroUsize::new(1) { - Some(min_align) => min_align, - None => const_unreachable!(), - }; - - /// The maximum theoretic possible alignment of a type. - /// - /// For compatibility with future Rust versions, this is defined as the - /// maximum power-of-two that fits into a `usize`. See also - /// [`DstLayout::CURRENT_MAX_ALIGN`]. - pub(crate) const THEORETICAL_MAX_ALIGN: NonZeroUsize = - match NonZeroUsize::new(1 << (POINTER_WIDTH_BITS - 1)) { - Some(max_align) => max_align, - None => const_unreachable!(), - }; - - /// The current, documented max alignment of a type \[1\]. - /// - /// \[1\] Per : - /// - /// The alignment value must be a power of two from 1 up to - /// 229. - #[cfg(not(kani))] - #[cfg(not(target_pointer_width = "16"))] - pub(crate) const CURRENT_MAX_ALIGN: NonZeroUsize = match NonZeroUsize::new(1 << 28) { - Some(max_align) => max_align, - None => const_unreachable!(), - }; - - #[cfg(not(kani))] - #[cfg(target_pointer_width = "16")] - pub(crate) const CURRENT_MAX_ALIGN: NonZeroUsize = match NonZeroUsize::new(1 << 15) { - Some(max_align) => max_align, - None => const_unreachable!(), - }; - - /// The maximum size of an allocation \[1\]. - /// - /// \[1\] Per : - /// - /// For any allocation with base `address`, `size`, and a set of `addresses`, - /// the following are guaranteed: [..] - /// - /// - `size <= isize::MAX` - /// - #[allow(clippy::as_conversions)] - pub(crate) const MAX_SIZE: usize = isize::MAX as usize; - - /// Assumes that this layout lacks static shallow padding. - /// - /// # Panics - /// - /// This method does not panic. - /// - /// # Safety - /// - /// If `self` describes the size and alignment of type that lacks static - /// shallow padding, unsafe code may assume that the result of this method - /// accurately reflects the size, alignment, and lack of static shallow - /// padding of that type. - const fn assume_shallow_unpadded(self) -> Self { - Self { statically_shallow_unpadded: true, ..self } - } - - /// Constructs a `DstLayout` for a zero-sized type with `repr_align` - /// alignment (or 1). If `repr_align` is provided, then it must be a power - /// of two. - /// - /// # Panics - /// - /// This function panics if the supplied `repr_align` is not a power of two. - /// - /// # Safety - /// - /// Unsafe code may assume that the contract of this function is satisfied. - #[doc(hidden)] - #[must_use] - #[inline] - pub const fn new_zst(repr_align: Option) -> DstLayout { - let align = match repr_align { - Some(align) => align, - None => Self::MIN_ALIGN, - }; - - const_assert!(align.get().is_power_of_two()); - - DstLayout { - align, - size_info: SizeInfo::Sized { size: 0 }, - statically_shallow_unpadded: true, - } - } - - /// Constructs a `DstLayout` which describes `T` and assumes `T` may contain - /// padding. - /// - /// # Safety - /// - /// Unsafe code may assume that `DstLayout` is the correct layout for `T`. - #[doc(hidden)] - #[must_use] - #[inline] - pub const fn for_type() -> DstLayout { - // SAFETY: `align` is correct by construction. `T: Sized`, and so it is - // sound to initialize `size_info` to `SizeInfo::Sized { size }`; the - // `size` field is also correct by construction. `unpadded` can safely - // default to `false`. - DstLayout { - align: match NonZeroUsize::new(mem::align_of::()) { - Some(align) => align, - None => const_unreachable!(), - }, - size_info: SizeInfo::Sized { size: mem::size_of::() }, - statically_shallow_unpadded: false, - } - } - - /// Constructs a `DstLayout` which describes a `T` that does not contain - /// padding. - /// - /// # Safety - /// - /// Unsafe code may assume that `DstLayout` is the correct layout for `T`. - #[doc(hidden)] - #[must_use] - #[inline] - pub const fn for_unpadded_type() -> DstLayout { - Self::for_type::().assume_shallow_unpadded() - } - - /// Constructs a `DstLayout` which describes `[T]`. - /// - /// # Safety - /// - /// Unsafe code may assume that `DstLayout` is the correct layout for `[T]`. - pub(crate) const fn for_slice() -> DstLayout { - // SAFETY: The alignment of a slice is equal to the alignment of its - // element type, and so `align` is initialized correctly. - // - // Since this is just a slice type, there is no offset between the - // beginning of the type and the beginning of the slice, so it is - // correct to set `offset: 0`. The `elem_size` is correct by - // construction. Since `[T]` is a (degenerate case of a) slice DST, it - // is correct to initialize `size_info` to `SizeInfo::SliceDst`. - DstLayout { - align: match NonZeroUsize::new(mem::align_of::()) { - Some(align) => align, - None => const_unreachable!(), - }, - size_info: SizeInfo::SliceDst(TrailingSliceLayout { - offset: 0, - elem_size: mem::size_of::(), - }), - statically_shallow_unpadded: true, - } - } - - /// Constructs a complete `DstLayout` reflecting a `repr(C)` struct with the - /// given alignment modifiers and fields. - /// - /// This method cannot be used to match the layout of a record with the - /// default representation, as that representation is mostly unspecified. - /// - /// # Safety - /// - /// For any definition of a `repr(C)` struct, if this method is invoked with - /// alignment modifiers and fields corresponding to that definition, the - /// resulting `DstLayout` will correctly encode the layout of that struct. - /// - /// We make no guarantees to the behavior of this method when it is invoked - /// with arguments that cannot correspond to a valid `repr(C)` struct. - #[must_use] - #[inline] - pub const fn for_repr_c_struct( - repr_align: Option, - repr_packed: Option, - fields: &[DstLayout], - ) -> DstLayout { - let mut layout = DstLayout::new_zst(repr_align); - - let mut i = 0; - #[allow(clippy::arithmetic_side_effects)] - while i < fields.len() { - #[allow(clippy::indexing_slicing)] - let field = fields[i]; - layout = layout.extend(field, repr_packed); - i += 1; - } - - layout = layout.pad_to_align(); - - // SAFETY: `layout` accurately describes the layout of a `repr(C)` - // struct with `repr_align` or `repr_packed` alignment modifications and - // the given `fields`. The `layout` is constructed using a sequence of - // invocations of `DstLayout::{new_zst,extend,pad_to_align}`. The - // documentation of these items vows that invocations in this manner - // will accurately describe a type, so long as: - // - // - that type is `repr(C)`, - // - its fields are enumerated in the order they appear, - // - the presence of `repr_align` and `repr_packed` are correctly accounted for. - // - // We respect all three of these preconditions above. - layout - } - - /// Like `Layout::extend`, this creates a layout that describes a record - /// whose layout consists of `self` followed by `next` that includes the - /// necessary inter-field padding, but not any trailing padding. - /// - /// In order to match the layout of a `#[repr(C)]` struct, this method - /// should be invoked for each field in declaration order. To add trailing - /// padding, call `DstLayout::pad_to_align` after extending the layout for - /// all fields. If `self` corresponds to a type marked with - /// `repr(packed(N))`, then `repr_packed` should be set to `Some(N)`, - /// otherwise `None`. - /// - /// This method cannot be used to match the layout of a record with the - /// default representation, as that representation is mostly unspecified. - /// - /// # Safety - /// - /// If a (potentially hypothetical) valid `repr(C)` Rust type begins with - /// fields whose layout are `self`, and those fields are immediately - /// followed by a field whose layout is `field`, then unsafe code may rely - /// on `self.extend(field, repr_packed)` producing a layout that correctly - /// encompasses those two components. - /// - /// We make no guarantees to the behavior of this method if these fragments - /// cannot appear in a valid Rust type (e.g., the concatenation of the - /// layouts would lead to a size larger than `isize::MAX`). - #[doc(hidden)] - #[must_use] - #[inline] - pub const fn extend(self, field: DstLayout, repr_packed: Option) -> Self { - use util::{max, min, padding_needed_for}; - - // If `repr_packed` is `None`, there are no alignment constraints, and - // the value can be defaulted to `THEORETICAL_MAX_ALIGN`. - let max_align = match repr_packed { - Some(max_align) => max_align, - None => Self::THEORETICAL_MAX_ALIGN, - }; - - const_assert!(max_align.get().is_power_of_two()); - - // We use Kani to prove that this method is robust to future increases - // in Rust's maximum allowed alignment. However, if such a change ever - // actually occurs, we'd like to be notified via assertion failures. - #[cfg(not(kani))] - { - const_debug_assert!(self.align.get() <= DstLayout::CURRENT_MAX_ALIGN.get()); - const_debug_assert!(field.align.get() <= DstLayout::CURRENT_MAX_ALIGN.get()); - if let Some(repr_packed) = repr_packed { - const_debug_assert!(repr_packed.get() <= DstLayout::CURRENT_MAX_ALIGN.get()); - } - } - - // The field's alignment is clamped by `repr_packed` (i.e., the - // `repr(packed(N))` attribute, if any) [1]. - // - // [1] Per https://doc.rust-lang.org/reference/type-layout.html#the-alignment-modifiers: - // - // The alignments of each field, for the purpose of positioning - // fields, is the smaller of the specified alignment and the alignment - // of the field's type. - let field_align = min(field.align, max_align); - - // The struct's alignment is the maximum of its previous alignment and - // `field_align`. - let align = max(self.align, field_align); - - let (interfield_padding, size_info) = match self.size_info { - // If the layout is already a DST, we panic; DSTs cannot be extended - // with additional fields. - SizeInfo::SliceDst(..) => const_panic!("Cannot extend a DST with additional fields."), - - SizeInfo::Sized { size: preceding_size } => { - // Compute the minimum amount of inter-field padding needed to - // satisfy the field's alignment, and offset of the trailing - // field. [1] - // - // [1] Per https://doc.rust-lang.org/reference/type-layout.html#the-alignment-modifiers: - // - // Inter-field padding is guaranteed to be the minimum - // required in order to satisfy each field's (possibly - // altered) alignment. - let padding = padding_needed_for(preceding_size, field_align); - - // This will not panic (and is proven to not panic, with Kani) - // if the layout components can correspond to a leading layout - // fragment of a valid Rust type, but may panic otherwise (e.g., - // combining or aligning the components would create a size - // exceeding `isize::MAX`). - let offset = match preceding_size.checked_add(padding) { - Some(offset) => offset, - None => const_panic!("Adding padding to `self`'s size overflows `usize`."), - }; - - ( - padding, - match field.size_info { - SizeInfo::Sized { size: field_size } => { - // If the trailing field is sized, the resulting layout - // will be sized. Its size will be the sum of the - // preceding layout, the size of the new field, and the - // size of inter-field padding between the two. - // - // This will not panic (and is proven with Kani to not - // panic) if the layout components can correspond to a - // leading layout fragment of a valid Rust type, but may - // panic otherwise (e.g., combining or aligning the - // components would create a size exceeding - // `usize::MAX`). - let size = match offset.checked_add(field_size) { - Some(size) => size, - None => const_panic!("`field` cannot be appended without the total size overflowing `usize`"), - }; - SizeInfo::Sized { size } - } - SizeInfo::SliceDst(TrailingSliceLayout { - offset: trailing_offset, - elem_size, - }) => { - // If the trailing field is dynamically sized, so too - // will the resulting layout. The offset of the trailing - // slice component is the sum of the offset of the - // trailing field and the trailing slice offset within - // that field. - // - // This will not panic (and is proven with Kani to not - // panic) if the layout components can correspond to a - // leading layout fragment of a valid Rust type, but may - // panic otherwise (e.g., combining or aligning the - // components would create a size exceeding - // `usize::MAX`). - let offset = match offset.checked_add(trailing_offset) { - Some(offset) => offset, - None => const_panic!("`field` cannot be appended without the total size overflowing `usize`"), - }; - SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size }) - } - }, - ) - } - }; - - let statically_shallow_unpadded = self.statically_shallow_unpadded - && field.statically_shallow_unpadded - && interfield_padding == 0; - - DstLayout { align, size_info, statically_shallow_unpadded } - } - - /// Like `Layout::pad_to_align`, this routine rounds the size of this layout - /// up to the nearest multiple of this type's alignment or `repr_packed` - /// (whichever is less). This method leaves DST layouts unchanged, since the - /// trailing padding of DSTs is computed at runtime. - /// - /// The accompanying boolean is `true` if the resulting composition of - /// fields necessitated static (as opposed to dynamic) padding; otherwise - /// `false`. - /// - /// In order to match the layout of a `#[repr(C)]` struct, this method - /// should be invoked after the invocations of [`DstLayout::extend`]. If - /// `self` corresponds to a type marked with `repr(packed(N))`, then - /// `repr_packed` should be set to `Some(N)`, otherwise `None`. - /// - /// This method cannot be used to match the layout of a record with the - /// default representation, as that representation is mostly unspecified. - /// - /// # Safety - /// - /// If a (potentially hypothetical) valid `repr(C)` type begins with fields - /// whose layout are `self` followed only by zero or more bytes of trailing - /// padding (not included in `self`), then unsafe code may rely on - /// `self.pad_to_align(repr_packed)` producing a layout that correctly - /// encapsulates the layout of that type. - /// - /// We make no guarantees to the behavior of this method if `self` cannot - /// appear in a valid Rust type (e.g., because the addition of trailing - /// padding would lead to a size larger than `isize::MAX`). - #[doc(hidden)] - #[must_use] - #[inline] - pub const fn pad_to_align(self) -> Self { - use util::padding_needed_for; - - let (static_padding, size_info) = match self.size_info { - // For sized layouts, we add the minimum amount of trailing padding - // needed to satisfy alignment. - SizeInfo::Sized { size: unpadded_size } => { - let padding = padding_needed_for(unpadded_size, self.align); - let size = match unpadded_size.checked_add(padding) { - Some(size) => size, - None => const_panic!("Adding padding caused size to overflow `usize`."), - }; - (padding, SizeInfo::Sized { size }) - } - // For DST layouts, trailing padding depends on the length of the - // trailing DST and is computed at runtime. This does not alter the - // offset or element size of the layout, so we leave `size_info` - // unchanged. - size_info @ SizeInfo::SliceDst(_) => (0, size_info), - }; - - let statically_shallow_unpadded = self.statically_shallow_unpadded && static_padding == 0; - - DstLayout { align: self.align, size_info, statically_shallow_unpadded } - } - - /// Produces `true` if `self` requires static padding; otherwise `false`. - #[must_use] - #[inline(always)] - pub const fn requires_static_padding(self) -> bool { - !self.statically_shallow_unpadded - } - - /// Produces `true` if there exists any metadata for which a type of layout - /// `self` would require dynamic trailing padding; otherwise `false`. - #[must_use] - #[inline(always)] - pub const fn requires_dynamic_padding(self) -> bool { - // A `% self.align.get()` cannot panic, since `align` is non-zero. - #[allow(clippy::arithmetic_side_effects)] - match self.size_info { - SizeInfo::Sized { .. } => false, - SizeInfo::SliceDst(trailing_slice_layout) => { - // SAFETY: This predicate is formally proved sound by - // `proofs::prove_requires_dynamic_padding`. - trailing_slice_layout.offset % self.align.get() != 0 - || trailing_slice_layout.elem_size % self.align.get() != 0 - } - } - } - - /// Validates that a cast is sound from a layout perspective. - /// - /// Validates that the size and alignment requirements of a type with the - /// layout described in `self` would not be violated by performing a - /// `cast_type` cast from a pointer with address `addr` which refers to a - /// memory region of size `bytes_len`. - /// - /// If the cast is valid, `validate_cast_and_convert_metadata` returns - /// `(elems, split_at)`. If `self` describes a dynamically-sized type, then - /// `elems` is the maximum number of trailing slice elements for which a - /// cast would be valid (for sized types, `elem` is meaningless and should - /// be ignored). `split_at` is the index at which to split the memory region - /// in order for the prefix (suffix) to contain the result of the cast, and - /// in order for the remaining suffix (prefix) to contain the leftover - /// bytes. - /// - /// There are three conditions under which a cast can fail: - /// - The smallest possible value for the type is larger than the provided - /// memory region - /// - A prefix cast is requested, and `addr` does not satisfy `self`'s - /// alignment requirement - /// - A suffix cast is requested, and `addr + bytes_len` does not satisfy - /// `self`'s alignment requirement (as a consequence, since all instances - /// of the type are a multiple of its alignment, no size for the type will - /// result in a starting address which is properly aligned) - /// - /// # Safety - /// - /// The caller may assume that this implementation is correct, and may rely - /// on that assumption for the soundness of their code. In particular, the - /// caller may assume that, if `validate_cast_and_convert_metadata` returns - /// `Some((elems, split_at))`, then: - /// - A pointer to the type (for dynamically sized types, this includes - /// `elems` as its pointer metadata) describes an object of size `size <= - /// bytes_len` - /// - If this is a prefix cast: - /// - `addr` satisfies `self`'s alignment - /// - `size == split_at` - /// - If this is a suffix cast: - /// - `split_at == bytes_len - size` - /// - `addr + split_at` satisfies `self`'s alignment - /// - /// Note that this method does *not* ensure that a pointer constructed from - /// its return values will be a valid pointer. In particular, this method - /// does not reason about `isize` overflow, which is a requirement of many - /// Rust pointer APIs, and may at some point be determined to be a validity - /// invariant of pointer types themselves. This should never be a problem so - /// long as the arguments to this method are derived from a known-valid - /// pointer (e.g., one derived from a safe Rust reference), but it is - /// nonetheless the caller's responsibility to justify that pointer - /// arithmetic will not overflow based on a safety argument *other than* the - /// mere fact that this method returned successfully. - /// - /// # Panics - /// - /// `validate_cast_and_convert_metadata` will panic if `self` describes a - /// DST whose trailing slice element is zero-sized. - /// - /// If `addr + bytes_len` overflows `usize`, - /// `validate_cast_and_convert_metadata` may panic, or it may return - /// incorrect results. No guarantees are made about when - /// `validate_cast_and_convert_metadata` will panic. The caller should not - /// rely on `validate_cast_and_convert_metadata` panicking in any particular - /// condition, even if `debug_assertions` are enabled. - #[allow(unused)] - #[inline(always)] - pub(crate) const fn validate_cast_and_convert_metadata( - &self, - addr: usize, - bytes_len: usize, - cast_type: CastType, - ) -> Result<(usize, usize), MetadataCastError> { - // `debug_assert!`, but with `#[allow(clippy::arithmetic_side_effects)]`. - macro_rules! __const_debug_assert { - ($e:expr $(, $msg:expr)?) => { - const_debug_assert!({ - #[allow(clippy::arithmetic_side_effects)] - let e = $e; - e - } $(, $msg)?); - }; - } - - // Note that, in practice, `self` is always a compile-time constant. We - // do this check earlier than needed to ensure that we always panic as a - // result of bugs in the program (such as calling this function on an - // invalid type) instead of allowing this panic to be hidden if the cast - // would have failed anyway for runtime reasons (such as a too-small - // memory region). - // - // FIXME(#67): Once our MSRV is 1.65, use let-else: - // https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html#let-else-statements - let size_info = match self.size_info.try_to_nonzero_elem_size() { - Some(size_info) => size_info, - None => const_panic!("attempted to cast to slice type with zero-sized element"), - }; - - // Precondition - __const_debug_assert!( - addr.checked_add(bytes_len).is_some(), - "`addr` + `bytes_len` > usize::MAX" - ); - - // Alignment checks go in their own block to avoid introducing variables - // into the top-level scope. - { - // We check alignment for `addr` (for prefix casts) or `addr + - // bytes_len` (for suffix casts). For a prefix cast, the correctness - // of this check is trivial - `addr` is the address the object will - // live at. - // - // For a suffix cast, we know that all valid sizes for the type are - // a multiple of the alignment (and by safety precondition, we know - // `DstLayout` may only describe valid Rust types). Thus, a - // validly-sized instance which lives at a validly-aligned address - // must also end at a validly-aligned address. Thus, if the end - // address for a suffix cast (`addr + bytes_len`) is not aligned, - // then no valid start address will be aligned either. - let offset = match cast_type { - CastType::Prefix => 0, - CastType::Suffix => bytes_len, - }; - - // Addition is guaranteed not to overflow because `offset <= - // bytes_len`, and `addr + bytes_len <= usize::MAX` is a - // precondition of this method. Modulus is guaranteed not to divide - // by 0 because `align` is non-zero. - #[allow(clippy::arithmetic_side_effects)] - if (addr + offset) % self.align.get() != 0 { - return Err(MetadataCastError::Alignment); - } - } - - let (elems, self_bytes) = match size_info { - SizeInfo::Sized { size } => { - if size > bytes_len { - return Err(MetadataCastError::Size); - } - (0, size) - } - SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size }) => { - // Calculate the maximum number of bytes that could be consumed - // - any number of bytes larger than this will either not be a - // multiple of the alignment, or will be larger than - // `bytes_len`. - let max_total_bytes = - util::round_down_to_next_multiple_of_alignment(bytes_len, self.align); - // Calculate the maximum number of bytes that could be consumed - // by the trailing slice. - // - // FIXME(#67): Once our MSRV is 1.65, use let-else: - // https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html#let-else-statements - let max_slice_and_padding_bytes = match max_total_bytes.checked_sub(offset) { - Some(max) => max, - // `bytes_len` too small even for 0 trailing slice elements. - None => return Err(MetadataCastError::Size), - }; - - // Calculate the number of elements that fit in - // `max_slice_and_padding_bytes`; any remaining bytes will be - // considered padding. - // - // Guaranteed not to divide by zero: `elem_size` is non-zero. - #[allow(clippy::arithmetic_side_effects)] - let elems = max_slice_and_padding_bytes / elem_size.get(); - // Guaranteed not to overflow on multiplication: `usize::MAX >= - // max_slice_and_padding_bytes >= (max_slice_and_padding_bytes / - // elem_size) * elem_size`. - // - // Guaranteed not to overflow on addition: - // - max_slice_and_padding_bytes == max_total_bytes - offset - // - elems * elem_size <= max_slice_and_padding_bytes == max_total_bytes - offset - // - elems * elem_size + offset <= max_total_bytes <= usize::MAX - #[allow(clippy::arithmetic_side_effects)] - let without_padding = offset + elems * elem_size.get(); - // `self_bytes` is equal to the offset bytes plus the bytes - // consumed by the trailing slice plus any padding bytes - // required to satisfy the alignment. Note that we have computed - // the maximum number of trailing slice elements that could fit - // in `self_bytes`, so any padding is guaranteed to be less than - // the size of an extra element. - // - // Guaranteed not to overflow: - // - By previous comment: without_padding == elems * elem_size + - // offset <= max_total_bytes - // - By construction, `max_total_bytes` is a multiple of - // `self.align`. - // - At most, adding padding needed to round `without_padding` - // up to the next multiple of the alignment will bring - // `self_bytes` up to `max_total_bytes`. - #[allow(clippy::arithmetic_side_effects)] - let self_bytes = - without_padding + util::padding_needed_for(without_padding, self.align); - (elems, self_bytes) - } - }; - - __const_debug_assert!(self_bytes <= bytes_len); - - let split_at = match cast_type { - CastType::Prefix => self_bytes, - // Guaranteed not to underflow: - // - In the `Sized` branch, only returns `size` if `size <= - // bytes_len`. - // - In the `SliceDst` branch, calculates `self_bytes <= - // max_toatl_bytes`, which is upper-bounded by `bytes_len`. - #[allow(clippy::arithmetic_side_effects)] - CastType::Suffix => bytes_len - self_bytes, - }; - - Ok((elems, split_at)) - } -} - -pub(crate) use cast_from::CastFrom; -mod cast_from { - use crate::*; - - pub(crate) struct CastFrom { - _never: core::convert::Infallible, - _marker: PhantomData, - } - - // SAFETY: The implementation of `Project::project` preserves the address - // of the referent – it only modifies pointer metadata. - unsafe impl crate::pointer::cast::Cast for CastFrom - where - Src: KnownLayout + ?Sized, - Dst: KnownLayout + ?Sized, - { - } - - // SAFETY: The implementation of `Project::project` preserves the size of - // the referent (see inline comments for a more detailed proof of this). - unsafe impl crate::pointer::cast::CastExact for CastFrom - where - Src: KnownLayout + ?Sized, - Dst: KnownLayout + ?Sized, - { - } - - // SAFETY: `project` produces a pointer which refers to the same referent - // bytes as its input, or to a subset of them (see inline comments for a - // more detailed proof of this). It does this using provenance-preserving - // operations. - unsafe impl crate::pointer::cast::Project for CastFrom - where - Src: KnownLayout + ?Sized, - Dst: KnownLayout + ?Sized, - { - /// # PME - /// - /// Generates a post-monomorphization error if it is not possible to - /// implement soundly. - // - // FIXME(#1817): Support Sized->Unsized and Unsized->Sized casts - fn project(src: PtrInner<'_, Src>) -> *mut Dst { - /// The parameters required in order to perform a pointer cast from - /// `Src` to `Dst`. - /// - /// These are a compile-time function of the layouts of `Src` - /// and `Dst`. - /// - /// # Safety - /// - /// `Src`'s alignment must not be smaller than `Dst`'s alignment. - struct CastParams { - inner: CastParamsInner, - _src: PhantomData, - _dst: PhantomData, - } - - #[derive(Copy, Clone)] - enum CastParamsInner { - // At compile time (specifically, post-monomorphization time), - // we need to compute two things: - // - Whether, given *any* `*Src`, it is possible to construct a - // `*Dst` which addresses the same number of bytes (ie, - // whether, for any `Src` pointer metadata, there exists `Dst` - // pointer metadata that addresses the same number of bytes) - // - If this is possible, any information necessary to perform - // the `Src`->`Dst` metadata conversion at runtime. - // - // Assume that `Src` and `Dst` are slice DSTs, and define: - // - `S_OFF = Src::LAYOUT.size_info.offset` - // - `S_ELEM = Src::LAYOUT.size_info.elem_size` - // - `D_OFF = Dst::LAYOUT.size_info.offset` - // - `D_ELEM = Dst::LAYOUT.size_info.elem_size` - // - // We are trying to solve the following equation: - // - // D_OFF + d_meta * D_ELEM = S_OFF + s_meta * S_ELEM - // - // At runtime, we will be attempting to compute `d_meta`, given - // `s_meta` (a runtime value) and all other parameters (which - // are compile-time values). We can solve like so: - // - // D_OFF + d_meta * D_ELEM = S_OFF + s_meta * S_ELEM - // - // d_meta * D_ELEM = S_OFF - D_OFF + s_meta * S_ELEM - // - // d_meta = (S_OFF - D_OFF + s_meta * S_ELEM)/D_ELEM - // - // Since `d_meta` will be a `usize`, we need the right-hand side - // to be an integer, and this needs to hold for *any* value of - // `s_meta` (in order for our conversion to be infallible - ie, - // to not have to reject certain values of `s_meta` at runtime). - // This means that: - // - // - `s_meta * S_ELEM` must be a multiple of `D_ELEM` - // - Since this must hold for any value of `s_meta`, `S_ELEM` - // must be a multiple of `D_ELEM` - // - `S_OFF - D_OFF` must be a multiple of `D_ELEM` - // - // Thus, let `OFFSET_DELTA_ELEMS = (S_OFF - D_OFF)/D_ELEM` and - // `ELEM_MULTIPLE = S_ELEM/D_ELEM`. We can rewrite the above - // expression as: - // - // d_meta = (S_OFF - D_OFF + s_meta * S_ELEM)/D_ELEM - // - // d_meta = OFFSET_DELTA_ELEMS + s_meta * ELEM_MULTIPLE - // - // Thus, we just need to compute the following and confirm that - // they have integer solutions in order to both a) determine - // whether infallible `Src` -> `Dst` casts are possible and, b) - // pre-compute the parameters necessary to perform those casts - // at runtime. These parameters are encapsulated in - // `CastParams`, which acts as a witness that such infallible - // casts are possible. - /// The parameters required in order to perform an - /// unsized-to-unsized pointer cast from `Src` to `Dst` as - /// described above. - /// - /// # Safety - /// - /// `Src` and `Dst` must both be slice DSTs. - /// - /// `offset_delta_elems` and `elem_multiple` must be valid as - /// described above. - UnsizedToUnsized { offset_delta_elems: usize, elem_multiple: usize }, - - /// The metadata of a `Dst` which has the same size as `Src: - /// Sized`. - /// - /// # Safety - /// - /// `Src: Sized` and `Dst` must be a slice DST. - /// - /// A raw `Dst` pointer with metadata `dst_meta` must address - /// `size_of::()` bytes. - SizedToUnsized { dst_meta: usize }, - - /// The metadata of a `Dst` which has the same size as `Src: - /// Sized`. - /// - /// # Safety - /// - /// `Src` and `Dst` must both be `Sized` and `size_of::() - /// == size_of::()`. - SizedToSized, - } - - impl Copy for CastParams {} - impl Clone for CastParams { - fn clone(&self) -> Self { - *self - } - } - - impl CastParams { - const fn try_compute( - src: &DstLayout, - dst: &DstLayout, - ) -> Option> { - if src.align.get() < dst.align.get() { - return None; - } - - let inner = match (src.size_info, dst.size_info) { - ( - SizeInfo::Sized { size: src_size }, - SizeInfo::Sized { size: dst_size }, - ) => { - if src_size != dst_size { - return None; - } - - // SAFETY: We checked above that `src_size == - // dst_size`. - CastParamsInner::SizedToSized - } - (SizeInfo::Sized { size: src_size }, SizeInfo::SliceDst(dst)) => { - let offset_delta = if let Some(od) = src_size.checked_sub(dst.offset) { - od - } else { - return None; - }; - - let dst_elem_size = if let Some(e) = NonZeroUsize::new(dst.elem_size) { - e - } else { - return None; - }; - - // PANICS: `dst_elem_size: NonZeroUsize`, so this won't - // divide by zero. - #[allow(clippy::arithmetic_side_effects)] - let delta_mod_other_elem = offset_delta % dst_elem_size.get(); - - if delta_mod_other_elem != 0 { - return None; - } - - // PANICS: `dst_elem_size: NonZeroUsize`, so this won't - // divide by zero. - #[allow(clippy::arithmetic_side_effects)] - let dst_meta = offset_delta / dst_elem_size.get(); - - // SAFETY: The preceding math ensures that a `Dst` - // with `dst_meta` addresses `src_size` bytes. - CastParamsInner::SizedToUnsized { dst_meta } - } - (SizeInfo::SliceDst(src), SizeInfo::SliceDst(dst)) => { - let offset_delta = if let Some(od) = src.offset.checked_sub(dst.offset) - { - od - } else { - return None; - }; - - let dst_elem_size = if let Some(e) = NonZeroUsize::new(dst.elem_size) { - e - } else { - return None; - }; - - // PANICS: `dst_elem_size: NonZeroUsize`, so this won't - // divide by zero. - #[allow(clippy::arithmetic_side_effects)] - let delta_mod_other_elem = offset_delta % dst_elem_size.get(); - - // PANICS: `dst_elem_size: NonZeroUsize`, so this won't - // divide by zero. - #[allow(clippy::arithmetic_side_effects)] - let elem_remainder = src.elem_size % dst_elem_size.get(); - - if delta_mod_other_elem != 0 - || src.elem_size < dst.elem_size - || elem_remainder != 0 - { - return None; - } - - // PANICS: `dst_elem_size: NonZeroUsize`, so this won't - // divide by zero. - #[allow(clippy::arithmetic_side_effects)] - let offset_delta_elems = offset_delta / dst_elem_size.get(); - - // PANICS: `dst_elem_size: NonZeroUsize`, so this won't - // divide by zero. - #[allow(clippy::arithmetic_side_effects)] - let elem_multiple = src.elem_size / dst_elem_size.get(); - - CastParamsInner::UnsizedToUnsized { - // SAFETY: We checked above that this is an exact ratio. - offset_delta_elems, - // SAFETY: We checked above that this is an exact ratio. - elem_multiple, - } - } - _ => return None, - }; - - // SAFETY: We checked above that `src.align >= dst.align`. - Some(CastParams { inner, _src: PhantomData, _dst: PhantomData }) - } - } - - impl CastParams { - /// # Safety - /// - /// `src_meta` describes a `Src` whose size is no larger than - /// `isize::MAX`. - /// - /// The returned metadata describes a `Dst` of the same size as - /// the original `Src`. - #[inline(always)] - unsafe fn cast_metadata( - self, - src_meta: Src::PointerMetadata, - ) -> Dst::PointerMetadata { - #[allow(unused)] - use crate::util::polyfills::*; - - let dst_meta = match self.inner { - CastParamsInner::UnsizedToUnsized { offset_delta_elems, elem_multiple } => { - let src_meta = src_meta.to_elem_count(); - #[allow( - unstable_name_collisions, - clippy::multiple_unsafe_ops_per_block - )] - // SAFETY: `self` is a witness that the following - // equation holds: - // - // D_OFF + d_meta * D_ELEM = S_OFF + s_meta * S_ELEM - // - // Since the caller promises that `src_meta` is - // valid `Src` metadata, this math will not - // overflow, and the returned value will describe a - // `Dst` of the same size. - unsafe { - offset_delta_elems - .unchecked_add(src_meta.unchecked_mul(elem_multiple)) - } - } - CastParamsInner::SizedToUnsized { dst_meta } => dst_meta, - CastParamsInner::SizedToSized => 0, - }; - Dst::PointerMetadata::from_elem_count(dst_meta) - } - } - - trait Params { - const CAST_PARAMS: CastParams; - } - - impl Params for Dst - where - Src: KnownLayout + ?Sized, - Dst: KnownLayout + ?Sized, - { - const CAST_PARAMS: CastParams = - match CastParams::try_compute(&Src::LAYOUT, &Dst::LAYOUT) { - Some(params) => params, - None => const_panic!( - "cannot `transmute_ref!` or `transmute_mut!` between incompatible types" - ), - }; - } - - let src_meta = ::pointer_to_metadata(src.as_ptr()); - let params = >::CAST_PARAMS; - - // SAFETY: `src: PtrInner` guarantees that `src`'s referent is zero - // bytes or lives in a single allocation, which means that it is no - // larger than `isize::MAX` bytes [1]. - // - // [1] https://doc.rust-lang.org/1.92.0/std/ptr/index.html#allocation - let dst_meta = unsafe { params.cast_metadata(src_meta) }; - - ::raw_from_ptr_len(src.as_non_null().cast(), dst_meta).as_ptr() - } - } -} - -// FIXME(#67): For some reason, on our MSRV toolchain, this `allow` isn't -// enforced despite having `#![allow(unknown_lints)]` at the crate root, but -// putting it here works. Once our MSRV is high enough that this bug has been -// fixed, remove this `allow`. -#[allow(unknown_lints)] -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dst_layout_for_slice() { - let layout = DstLayout::for_slice::(); - match layout.size_info { - SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size }) => { - assert_eq!(offset, 0); - assert_eq!(elem_size, 4); - } - _ => panic!("Expected SliceDst"), - } - assert_eq!(layout.align.get(), 4); - } - - /// Tests of when a sized `DstLayout` is extended with a sized field. - #[allow(clippy::decimal_literal_representation)] - #[test] - fn test_dst_layout_extend_sized_with_sized() { - // This macro constructs a layout corresponding to a `u8` and extends it - // with a zero-sized trailing field of given alignment `n`. The macro - // tests that the resulting layout has both size and alignment `min(n, - // P)` for all valid values of `repr(packed(P))`. - macro_rules! test_align_is_size { - ($n:expr) => { - let base = DstLayout::for_type::(); - let trailing_field = DstLayout::for_type::>(); - - let packs = - core::iter::once(None).chain((0..29).map(|p| NonZeroUsize::new(2usize.pow(p)))); - - for pack in packs { - let composite = base.extend(trailing_field, pack); - let max_align = pack.unwrap_or(DstLayout::CURRENT_MAX_ALIGN); - let align = $n.min(max_align.get()); - assert_eq!( - composite, - DstLayout { - align: NonZeroUsize::new(align).unwrap(), - size_info: SizeInfo::Sized { size: align }, - statically_shallow_unpadded: false, - } - ) - } - }; - } - - test_align_is_size!(1); - test_align_is_size!(2); - test_align_is_size!(4); - test_align_is_size!(8); - test_align_is_size!(16); - test_align_is_size!(32); - test_align_is_size!(64); - test_align_is_size!(128); - test_align_is_size!(256); - test_align_is_size!(512); - test_align_is_size!(1024); - test_align_is_size!(2048); - test_align_is_size!(4096); - test_align_is_size!(8192); - test_align_is_size!(16384); - test_align_is_size!(32768); - test_align_is_size!(65536); - test_align_is_size!(131072); - test_align_is_size!(262144); - test_align_is_size!(524288); - test_align_is_size!(1048576); - test_align_is_size!(2097152); - test_align_is_size!(4194304); - test_align_is_size!(8388608); - test_align_is_size!(16777216); - test_align_is_size!(33554432); - test_align_is_size!(67108864); - test_align_is_size!(33554432); - test_align_is_size!(134217728); - test_align_is_size!(268435456); - } - - /// Tests of when a sized `DstLayout` is extended with a DST field. - #[test] - fn test_dst_layout_extend_sized_with_dst() { - // Test that for all combinations of real-world alignments and - // `repr_packed` values, that the extension of a sized `DstLayout`` with - // a DST field correctly computes the trailing offset in the composite - // layout. - - let aligns = (0..29).map(|p| NonZeroUsize::new(2usize.pow(p)).unwrap()); - let packs = core::iter::once(None).chain(aligns.clone().map(Some)); - - for align in aligns { - for pack in packs.clone() { - let base = DstLayout::for_type::(); - let elem_size = 42; - let trailing_field_offset = 11; - - let trailing_field = DstLayout { - align, - size_info: SizeInfo::SliceDst(TrailingSliceLayout { elem_size, offset: 11 }), - statically_shallow_unpadded: false, - }; - - let composite = base.extend(trailing_field, pack); - - let max_align = pack.unwrap_or(DstLayout::CURRENT_MAX_ALIGN).get(); - - let align = align.get().min(max_align); - - assert_eq!( - composite, - DstLayout { - align: NonZeroUsize::new(align).unwrap(), - size_info: SizeInfo::SliceDst(TrailingSliceLayout { - elem_size, - offset: align + trailing_field_offset, - }), - statically_shallow_unpadded: false, - } - ) - } - } - } - - /// Tests that calling `pad_to_align` on a sized `DstLayout` adds the - /// expected amount of trailing padding. - #[test] - fn test_dst_layout_pad_to_align_with_sized() { - // For all valid alignments `align`, construct a one-byte layout aligned - // to `align`, call `pad_to_align`, and assert that the size of the - // resulting layout is equal to `align`. - for align in (0..29).map(|p| NonZeroUsize::new(2usize.pow(p)).unwrap()) { - let layout = DstLayout { - align, - size_info: SizeInfo::Sized { size: 1 }, - statically_shallow_unpadded: true, - }; - - assert_eq!( - layout.pad_to_align(), - DstLayout { - align, - size_info: SizeInfo::Sized { size: align.get() }, - statically_shallow_unpadded: align.get() == 1 - } - ); - } - - // Test explicitly-provided combinations of unpadded and padded - // counterparts. - - macro_rules! test { - (unpadded { size: $unpadded_size:expr, align: $unpadded_align:expr } - => padded { size: $padded_size:expr, align: $padded_align:expr }) => { - let unpadded = DstLayout { - align: NonZeroUsize::new($unpadded_align).unwrap(), - size_info: SizeInfo::Sized { size: $unpadded_size }, - statically_shallow_unpadded: false, - }; - let padded = unpadded.pad_to_align(); - - assert_eq!( - padded, - DstLayout { - align: NonZeroUsize::new($padded_align).unwrap(), - size_info: SizeInfo::Sized { size: $padded_size }, - statically_shallow_unpadded: false, - } - ); - }; - } - - test!(unpadded { size: 0, align: 4 } => padded { size: 0, align: 4 }); - test!(unpadded { size: 1, align: 4 } => padded { size: 4, align: 4 }); - test!(unpadded { size: 2, align: 4 } => padded { size: 4, align: 4 }); - test!(unpadded { size: 3, align: 4 } => padded { size: 4, align: 4 }); - test!(unpadded { size: 4, align: 4 } => padded { size: 4, align: 4 }); - test!(unpadded { size: 5, align: 4 } => padded { size: 8, align: 4 }); - test!(unpadded { size: 6, align: 4 } => padded { size: 8, align: 4 }); - test!(unpadded { size: 7, align: 4 } => padded { size: 8, align: 4 }); - test!(unpadded { size: 8, align: 4 } => padded { size: 8, align: 4 }); - - let current_max_align = DstLayout::CURRENT_MAX_ALIGN.get(); - - test!(unpadded { size: 1, align: current_max_align } - => padded { size: current_max_align, align: current_max_align }); - - test!(unpadded { size: current_max_align + 1, align: current_max_align } - => padded { size: current_max_align * 2, align: current_max_align }); - } - - /// Tests that calling `pad_to_align` on a DST `DstLayout` is a no-op. - #[test] - fn test_dst_layout_pad_to_align_with_dst() { - for align in (0..29).map(|p| NonZeroUsize::new(2usize.pow(p)).unwrap()) { - for offset in 0..10 { - for elem_size in 0..10 { - let layout = DstLayout { - align, - size_info: SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size }), - statically_shallow_unpadded: false, - }; - assert_eq!(layout.pad_to_align(), layout); - } - } - } - } - - // This test takes a long time when running under Miri, so we skip it in - // that case. This is acceptable because this is a logic test that doesn't - // attempt to expose UB. - #[test] - #[cfg_attr(miri, ignore)] - fn test_validate_cast_and_convert_metadata() { - #[allow(non_local_definitions)] - impl From for SizeInfo { - fn from(size: usize) -> SizeInfo { - SizeInfo::Sized { size } - } - } - - #[allow(non_local_definitions)] - impl From<(usize, usize)> for SizeInfo { - fn from((offset, elem_size): (usize, usize)) -> SizeInfo { - SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size }) - } - } - - fn layout>(s: S, align: usize) -> DstLayout { - DstLayout { - size_info: s.into(), - align: NonZeroUsize::new(align).unwrap(), - statically_shallow_unpadded: false, - } - } - - /// This macro accepts arguments in the form of: - /// - /// layout(_, _).validate(_, _, _), Ok(Some((_, _))) - /// | | | | | | | - /// size ---------+ | | | | | | - /// align -----------+ | | | | | - /// addr ------------------------+ | | | | - /// bytes_len ----------------------+ | | | - /// cast_type -------------------------+ | | - /// elems ------------------------------------------+ | - /// split_at ------------------------------------------+ - /// - /// `.validate` is shorthand for `.validate_cast_and_convert_metadata` - /// for brevity. - /// - /// Each argument can either be an iterator or a wildcard. Each - /// wildcarded variable is implicitly replaced by an iterator over a - /// representative sample of values for that variable. Each `test!` - /// invocation iterates over every combination of values provided by - /// each variable's iterator (ie, the cartesian product) and validates - /// that the results are expected. - /// - /// The final argument uses the same syntax, but it has a different - /// meaning: - /// - If it is `Ok(pat)`, then the pattern `pat` is supplied to - /// a matching assert to validate the computed result for each - /// combination of input values. - /// - If it is `Err(Some(msg) | None)`, then `test!` validates that the - /// call to `validate_cast_and_convert_metadata` panics with the given - /// panic message or, if the current Rust toolchain version is too - /// early to support panicking in `const fn`s, panics with *some* - /// message. In the latter case, the `const_panic!` macro is used, - /// which emits code which causes a non-panicking error at const eval - /// time, but which does panic when invoked at runtime. Thus, it is - /// merely difficult to predict the *value* of this panic. We deem - /// that testing against the real panic strings on stable and nightly - /// toolchains is enough to ensure correctness. - /// - /// Note that the meta-variables that match these variables have the - /// `tt` type, and some valid expressions are not valid `tt`s (such as - /// `a..b`). In this case, wrap the expression in parentheses, and it - /// will become valid `tt`. - macro_rules! test { - ( - layout($size:tt, $align:tt) - .validate($addr:tt, $bytes_len:tt, $cast_type:tt), $expect:pat $(,)? - ) => { - itertools::iproduct!( - test!(@generate_size $size), - test!(@generate_align $align), - test!(@generate_usize $addr), - test!(@generate_usize $bytes_len), - test!(@generate_cast_type $cast_type) - ).for_each(|(size_info, align, addr, bytes_len, cast_type)| { - // Temporarily disable the panic hook installed by the test - // harness. If we don't do this, all panic messages will be - // kept in an internal log. On its own, this isn't a - // problem, but if a non-caught panic ever happens (ie, in - // code later in this test not in this macro), all of the - // previously-buffered messages will be dumped, hiding the - // real culprit. - let previous_hook = std::panic::take_hook(); - // I don't understand why, but this seems to be required in - // addition to the previous line. - std::panic::set_hook(Box::new(|_| {})); - let actual = std::panic::catch_unwind(|| { - layout(size_info, align).validate_cast_and_convert_metadata(addr, bytes_len, cast_type) - }).map_err(|d| { - let msg = d.downcast::<&'static str>().ok().map(|s| *s.as_ref()); - assert!(msg.is_some() || cfg!(no_zerocopy_panic_in_const_and_vec_try_reserve_1_57_0), "non-string panic messages are not permitted when usage of panic in const fn is enabled"); - msg - }); - std::panic::set_hook(previous_hook); - - assert!( - matches!(actual, $expect), - "layout({:?}, {}).validate_cast_and_convert_metadata({}, {}, {:?})" ,size_info, align, addr, bytes_len, cast_type - ); - }); - }; - (@generate_usize _) => { 0..8 }; - // Generate sizes for both Sized and !Sized types. - (@generate_size _) => { - test!(@generate_size (_)).chain(test!(@generate_size (_, _))) - }; - // Generate sizes for both Sized and !Sized types by chaining - // specified iterators for each. - (@generate_size ($sized_sizes:tt | $unsized_sizes:tt)) => { - test!(@generate_size ($sized_sizes)).chain(test!(@generate_size $unsized_sizes)) - }; - // Generate sizes for Sized types. - (@generate_size (_)) => { test!(@generate_size (0..8)) }; - (@generate_size ($sizes:expr)) => { $sizes.into_iter().map(Into::::into) }; - // Generate sizes for !Sized types. - (@generate_size ($min_sizes:tt, $elem_sizes:tt)) => { - itertools::iproduct!( - test!(@generate_min_size $min_sizes), - test!(@generate_elem_size $elem_sizes) - ).map(Into::::into) - }; - (@generate_fixed_size _) => { (0..8).into_iter().map(Into::::into) }; - (@generate_min_size _) => { 0..8 }; - (@generate_elem_size _) => { 1..8 }; - (@generate_align _) => { [1, 2, 4, 8, 16] }; - (@generate_opt_usize _) => { [None].into_iter().chain((0..8).map(Some).into_iter()) }; - (@generate_cast_type _) => { [CastType::Prefix, CastType::Suffix] }; - (@generate_cast_type $variant:ident) => { [CastType::$variant] }; - // Some expressions need to be wrapped in parentheses in order to be - // valid `tt`s (required by the top match pattern). See the comment - // below for more details. This arm removes these parentheses to - // avoid generating an `unused_parens` warning. - (@$_:ident ($vals:expr)) => { $vals }; - (@$_:ident $vals:expr) => { $vals }; - } - - const EVENS: [usize; 8] = [0, 2, 4, 6, 8, 10, 12, 14]; - const ODDS: [usize; 8] = [1, 3, 5, 7, 9, 11, 13, 15]; - - // base_size is too big for the memory region. - test!( - layout(((1..8) | ((1..8), (1..8))), _).validate([0], [0], _), - Ok(Err(MetadataCastError::Size)) - ); - test!( - layout(((2..8) | ((2..8), (2..8))), _).validate([0], [1], Prefix), - Ok(Err(MetadataCastError::Size)) - ); - test!( - layout(((2..8) | ((2..8), (2..8))), _).validate([0x1000_0000 - 1], [1], Suffix), - Ok(Err(MetadataCastError::Size)) - ); - - // addr is unaligned for prefix cast - test!(layout(_, [2]).validate(ODDS, _, Prefix), Ok(Err(MetadataCastError::Alignment))); - test!(layout(_, [2]).validate(ODDS, _, Prefix), Ok(Err(MetadataCastError::Alignment))); - - // addr is aligned, but end of buffer is unaligned for suffix cast - test!(layout(_, [2]).validate(EVENS, ODDS, Suffix), Ok(Err(MetadataCastError::Alignment))); - test!(layout(_, [2]).validate(EVENS, ODDS, Suffix), Ok(Err(MetadataCastError::Alignment))); - - // Unfortunately, these constants cannot easily be used in the - // implementation of `validate_cast_and_convert_metadata`, since - // `panic!` consumes a string literal, not an expression. - // - // It's important that these messages be in a separate module. If they - // were at the function's top level, we'd pass them to `test!` as, e.g., - // `Err(TRAILING)`, which would run into a subtle Rust footgun - the - // `TRAILING` identifier would be treated as a pattern to match rather - // than a value to check for equality. - mod msgs { - pub(super) const TRAILING: &str = - "attempted to cast to slice type with zero-sized element"; - pub(super) const OVERFLOW: &str = "`addr` + `bytes_len` > usize::MAX"; - } - - // casts with ZST trailing element types are unsupported - test!(layout((_, [0]), _).validate(_, _, _), Err(Some(msgs::TRAILING) | None),); - - // addr + bytes_len must not overflow usize - test!(layout(_, _).validate([usize::MAX], (1..100), _), Err(Some(msgs::OVERFLOW) | None)); - test!(layout(_, _).validate((1..100), [usize::MAX], _), Err(Some(msgs::OVERFLOW) | None)); - test!( - layout(_, _).validate( - [usize::MAX / 2 + 1, usize::MAX], - [usize::MAX / 2 + 1, usize::MAX], - _ - ), - Err(Some(msgs::OVERFLOW) | None) - ); - - // Validates that `validate_cast_and_convert_metadata` satisfies its own - // documented safety postconditions, and also a few other properties - // that aren't documented but we want to guarantee anyway. - fn validate_behavior( - (layout, addr, bytes_len, cast_type): (DstLayout, usize, usize, CastType), - ) { - if let Ok((elems, split_at)) = - layout.validate_cast_and_convert_metadata(addr, bytes_len, cast_type) - { - let (size_info, align) = (layout.size_info, layout.align); - let debug_str = format!( - "layout({:?}, {}).validate_cast_and_convert_metadata({}, {}, {:?}) => ({}, {})", - size_info, align, addr, bytes_len, cast_type, elems, split_at - ); - - // If this is a sized type (no trailing slice), then `elems` is - // meaningless, but in practice we set it to 0. Callers are not - // allowed to rely on this, but a lot of math is nicer if - // they're able to, and some callers might accidentally do that. - let sized = matches!(layout.size_info, SizeInfo::Sized { .. }); - assert!(!(sized && elems != 0), "{}", debug_str); - - let resulting_size = match layout.size_info { - SizeInfo::Sized { size } => size, - SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size }) => { - let padded_size = |elems| { - let without_padding = offset + elems * elem_size; - without_padding + util::padding_needed_for(without_padding, align) - }; - - let resulting_size = padded_size(elems); - // Test that `validate_cast_and_convert_metadata` - // computed the largest possible value that fits in the - // given range. - assert!(padded_size(elems + 1) > bytes_len, "{}", debug_str); - resulting_size - } - }; - - // Test safety postconditions guaranteed by - // `validate_cast_and_convert_metadata`. - assert!(resulting_size <= bytes_len, "{}", debug_str); - match cast_type { - CastType::Prefix => { - assert_eq!(addr % align, 0, "{}", debug_str); - assert_eq!(resulting_size, split_at, "{}", debug_str); - } - CastType::Suffix => { - assert_eq!(split_at, bytes_len - resulting_size, "{}", debug_str); - assert_eq!((addr + split_at) % align, 0, "{}", debug_str); - } - } - } else { - let min_size = match layout.size_info { - SizeInfo::Sized { size } => size, - SizeInfo::SliceDst(TrailingSliceLayout { offset, .. }) => { - offset + util::padding_needed_for(offset, layout.align) - } - }; - - // If a cast is invalid, it is either because... - // 1. there are insufficient bytes at the given region for type: - let insufficient_bytes = bytes_len < min_size; - // 2. performing the cast would misalign type: - let base = match cast_type { - CastType::Prefix => 0, - CastType::Suffix => bytes_len, - }; - let misaligned = (base + addr) % layout.align != 0; - - assert!(insufficient_bytes || misaligned); - } - } - - let sizes = 0..8; - let elem_sizes = 1..8; - let size_infos = sizes - .clone() - .map(Into::::into) - .chain(itertools::iproduct!(sizes, elem_sizes).map(Into::::into)); - let layouts = itertools::iproduct!(size_infos, [1, 2, 4, 8, 16, 32]) - .filter(|(size_info, align)| !matches!(size_info, SizeInfo::Sized { size } if size % align != 0)) - .map(|(size_info, align)| layout(size_info, align)); - itertools::iproduct!(layouts, 0..8, 0..8, [CastType::Prefix, CastType::Suffix]) - .for_each(validate_behavior); - } - - #[test] - #[cfg(__ZEROCOPY_INTERNAL_USE_ONLY_NIGHTLY_FEATURES_IN_TESTS)] - fn test_validate_rust_layout() { - use core::{ - convert::TryInto as _, - ptr::{self, NonNull}, - }; - - use crate::util::testutil::*; - - // This test synthesizes pointers with various metadata and uses Rust's - // built-in APIs to confirm that Rust makes decisions about type layout - // which are consistent with what we believe is guaranteed by the - // language. If this test fails, it doesn't just mean our code is wrong - // - it means we're misunderstanding the language's guarantees. - - #[derive(Debug)] - struct MacroArgs { - offset: usize, - align: NonZeroUsize, - elem_size: Option, - } - - /// # Safety - /// - /// `test` promises to only call `addr_of_slice_field` on a `NonNull` - /// which points to a valid `T`. - /// - /// `with_elems` must produce a pointer which points to a valid `T`. - fn test NonNull>( - args: MacroArgs, - with_elems: W, - addr_of_slice_field: Option) -> NonNull>, - ) { - let dst = args.elem_size.is_some(); - let layout = { - let size_info = match args.elem_size { - Some(elem_size) => { - SizeInfo::SliceDst(TrailingSliceLayout { offset: args.offset, elem_size }) - } - None => SizeInfo::Sized { - // Rust only supports types whose sizes are a multiple - // of their alignment. If the macro created a type like - // this: - // - // #[repr(C, align(2))] - // struct Foo([u8; 1]); - // - // ...then Rust will automatically round the type's size - // up to 2. - size: args.offset + util::padding_needed_for(args.offset, args.align), - }, - }; - DstLayout { size_info, align: args.align, statically_shallow_unpadded: false } - }; - - for elems in 0..128 { - let ptr = with_elems(elems); - - if let Some(addr_of_slice_field) = addr_of_slice_field { - let slc_field_ptr = addr_of_slice_field(ptr).as_ptr(); - // SAFETY: Both `slc_field_ptr` and `ptr` are pointers to - // the same valid Rust object. - // Work around https://github.com/rust-lang/rust-clippy/issues/12280 - let offset: usize = - unsafe { slc_field_ptr.byte_offset_from(ptr.as_ptr()).try_into().unwrap() }; - assert_eq!(offset, args.offset); - } - - // SAFETY: `ptr` points to a valid `T`. - #[allow(clippy::multiple_unsafe_ops_per_block)] - let (size, align) = unsafe { - (mem::size_of_val_raw(ptr.as_ptr()), mem::align_of_val_raw(ptr.as_ptr())) - }; - - // Avoid expensive allocation when running under Miri. - let assert_msg = if !cfg!(miri) { - format!("\n{:?}\nsize:{}, align:{}", args, size, align) - } else { - String::new() - }; - - let without_padding = - args.offset + args.elem_size.map(|elem_size| elems * elem_size).unwrap_or(0); - assert!(size >= without_padding, "{}", assert_msg); - assert_eq!(align, args.align.get(), "{}", assert_msg); - - // This encodes the most important part of the test: our - // understanding of how Rust determines the layout of repr(C) - // types. Sized repr(C) types are trivial, but DST types have - // some subtlety. Note that: - // - For sized types, `without_padding` is just the size of the - // type that we constructed for `Foo`. Since we may have - // requested a larger alignment, `Foo` may actually be larger - // than this, hence `padding_needed_for`. - // - For unsized types, `without_padding` is dynamically - // computed from the offset, the element size, and element - // count. We expect that the size of the object should be - // `offset + elem_size * elems` rounded up to the next - // alignment. - let expected_size = - without_padding + util::padding_needed_for(without_padding, args.align); - assert_eq!(expected_size, size, "{}", assert_msg); - - // For zero-sized element types, - // `validate_cast_and_convert_metadata` just panics, so we skip - // testing those types. - if args.elem_size.map(|elem_size| elem_size > 0).unwrap_or(true) { - let addr = ptr.addr().get(); - let (got_elems, got_split_at) = layout - .validate_cast_and_convert_metadata(addr, size, CastType::Prefix) - .unwrap(); - // Avoid expensive allocation when running under Miri. - let assert_msg = if !cfg!(miri) { - format!( - "{}\nvalidate_cast_and_convert_metadata({}, {})", - assert_msg, addr, size, - ) - } else { - String::new() - }; - assert_eq!(got_split_at, size, "{}", assert_msg); - if dst { - assert!(got_elems >= elems, "{}", assert_msg); - if got_elems != elems { - // If `validate_cast_and_convert_metadata` - // returned more elements than `elems`, that - // means that `elems` is not the maximum number - // of elements that can fit in `size` - in other - // words, there is enough padding at the end of - // the value to fit at least one more element. - // If we use this metadata to synthesize a - // pointer, despite having a different element - // count, we still expect it to have the same - // size. - let got_ptr = with_elems(got_elems); - // SAFETY: `got_ptr` is a pointer to a valid `T`. - let size_of_got_ptr = unsafe { mem::size_of_val_raw(got_ptr.as_ptr()) }; - assert_eq!(size_of_got_ptr, size, "{}", assert_msg); - } - } else { - // For sized casts, the returned element value is - // technically meaningless, and we don't guarantee any - // particular value. In practice, it's always zero. - assert_eq!(got_elems, 0, "{}", assert_msg) - } - } - } - } - - macro_rules! validate_against_rust { - ($offset:literal, $align:literal $(, $elem_size:literal)?) => {{ - #[repr(C, align($align))] - struct Foo([u8; $offset]$(, [[u8; $elem_size]])?); - - let args = MacroArgs { - offset: $offset, - align: $align.try_into().unwrap(), - elem_size: { - #[allow(unused)] - let ret = None::; - $(let ret = Some($elem_size);)? - ret - } - }; - - #[repr(C, align($align))] - struct FooAlign; - // Create an aligned buffer to use in order to synthesize - // pointers to `Foo`. We don't ever load values from these - // pointers - we just do arithmetic on them - so having a "real" - // block of memory as opposed to a validly-aligned-but-dangling - // pointer is only necessary to make Miri happy since we run it - // with "strict provenance" checking enabled. - let aligned_buf = Align::<_, FooAlign>::new([0u8; 1024]); - let with_elems = |elems| { - let slc = NonNull::slice_from_raw_parts(NonNull::from(&aligned_buf.t), elems); - #[allow(clippy::as_conversions)] - NonNull::new(slc.as_ptr() as *mut Foo).unwrap() - }; - let addr_of_slice_field = { - #[allow(unused)] - let f = None::) -> NonNull>; - $( - // SAFETY: `test` promises to only call `f` with a `ptr` - // to a valid `Foo`. - let f: Option) -> NonNull> = Some(|ptr: NonNull| unsafe { - NonNull::new(ptr::addr_of_mut!((*ptr.as_ptr()).1)).unwrap().cast::() - }); - let _ = $elem_size; - )? - f - }; - - test::(args, with_elems, addr_of_slice_field); - }}; - } - - // Every permutation of: - // - offset in [0, 4] - // - align in [1, 16] - // - elem_size in [0, 4] (plus no elem_size) - validate_against_rust!(0, 1); - validate_against_rust!(0, 1, 0); - validate_against_rust!(0, 1, 1); - validate_against_rust!(0, 1, 2); - validate_against_rust!(0, 1, 3); - validate_against_rust!(0, 1, 4); - validate_against_rust!(0, 2); - validate_against_rust!(0, 2, 0); - validate_against_rust!(0, 2, 1); - validate_against_rust!(0, 2, 2); - validate_against_rust!(0, 2, 3); - validate_against_rust!(0, 2, 4); - validate_against_rust!(0, 4); - validate_against_rust!(0, 4, 0); - validate_against_rust!(0, 4, 1); - validate_against_rust!(0, 4, 2); - validate_against_rust!(0, 4, 3); - validate_against_rust!(0, 4, 4); - validate_against_rust!(0, 8); - validate_against_rust!(0, 8, 0); - validate_against_rust!(0, 8, 1); - validate_against_rust!(0, 8, 2); - validate_against_rust!(0, 8, 3); - validate_against_rust!(0, 8, 4); - validate_against_rust!(0, 16); - validate_against_rust!(0, 16, 0); - validate_against_rust!(0, 16, 1); - validate_against_rust!(0, 16, 2); - validate_against_rust!(0, 16, 3); - validate_against_rust!(0, 16, 4); - validate_against_rust!(1, 1); - validate_against_rust!(1, 1, 0); - validate_against_rust!(1, 1, 1); - validate_against_rust!(1, 1, 2); - validate_against_rust!(1, 1, 3); - validate_against_rust!(1, 1, 4); - validate_against_rust!(1, 2); - validate_against_rust!(1, 2, 0); - validate_against_rust!(1, 2, 1); - validate_against_rust!(1, 2, 2); - validate_against_rust!(1, 2, 3); - validate_against_rust!(1, 2, 4); - validate_against_rust!(1, 4); - validate_against_rust!(1, 4, 0); - validate_against_rust!(1, 4, 1); - validate_against_rust!(1, 4, 2); - validate_against_rust!(1, 4, 3); - validate_against_rust!(1, 4, 4); - validate_against_rust!(1, 8); - validate_against_rust!(1, 8, 0); - validate_against_rust!(1, 8, 1); - validate_against_rust!(1, 8, 2); - validate_against_rust!(1, 8, 3); - validate_against_rust!(1, 8, 4); - validate_against_rust!(1, 16); - validate_against_rust!(1, 16, 0); - validate_against_rust!(1, 16, 1); - validate_against_rust!(1, 16, 2); - validate_against_rust!(1, 16, 3); - validate_against_rust!(1, 16, 4); - validate_against_rust!(2, 1); - validate_against_rust!(2, 1, 0); - validate_against_rust!(2, 1, 1); - validate_against_rust!(2, 1, 2); - validate_against_rust!(2, 1, 3); - validate_against_rust!(2, 1, 4); - validate_against_rust!(2, 2); - validate_against_rust!(2, 2, 0); - validate_against_rust!(2, 2, 1); - validate_against_rust!(2, 2, 2); - validate_against_rust!(2, 2, 3); - validate_against_rust!(2, 2, 4); - validate_against_rust!(2, 4); - validate_against_rust!(2, 4, 0); - validate_against_rust!(2, 4, 1); - validate_against_rust!(2, 4, 2); - validate_against_rust!(2, 4, 3); - validate_against_rust!(2, 4, 4); - validate_against_rust!(2, 8); - validate_against_rust!(2, 8, 0); - validate_against_rust!(2, 8, 1); - validate_against_rust!(2, 8, 2); - validate_against_rust!(2, 8, 3); - validate_against_rust!(2, 8, 4); - validate_against_rust!(2, 16); - validate_against_rust!(2, 16, 0); - validate_against_rust!(2, 16, 1); - validate_against_rust!(2, 16, 2); - validate_against_rust!(2, 16, 3); - validate_against_rust!(2, 16, 4); - validate_against_rust!(3, 1); - validate_against_rust!(3, 1, 0); - validate_against_rust!(3, 1, 1); - validate_against_rust!(3, 1, 2); - validate_against_rust!(3, 1, 3); - validate_against_rust!(3, 1, 4); - validate_against_rust!(3, 2); - validate_against_rust!(3, 2, 0); - validate_against_rust!(3, 2, 1); - validate_against_rust!(3, 2, 2); - validate_against_rust!(3, 2, 3); - validate_against_rust!(3, 2, 4); - validate_against_rust!(3, 4); - validate_against_rust!(3, 4, 0); - validate_against_rust!(3, 4, 1); - validate_against_rust!(3, 4, 2); - validate_against_rust!(3, 4, 3); - validate_against_rust!(3, 4, 4); - validate_against_rust!(3, 8); - validate_against_rust!(3, 8, 0); - validate_against_rust!(3, 8, 1); - validate_against_rust!(3, 8, 2); - validate_against_rust!(3, 8, 3); - validate_against_rust!(3, 8, 4); - validate_against_rust!(3, 16); - validate_against_rust!(3, 16, 0); - validate_against_rust!(3, 16, 1); - validate_against_rust!(3, 16, 2); - validate_against_rust!(3, 16, 3); - validate_against_rust!(3, 16, 4); - validate_against_rust!(4, 1); - validate_against_rust!(4, 1, 0); - validate_against_rust!(4, 1, 1); - validate_against_rust!(4, 1, 2); - validate_against_rust!(4, 1, 3); - validate_against_rust!(4, 1, 4); - validate_against_rust!(4, 2); - validate_against_rust!(4, 2, 0); - validate_against_rust!(4, 2, 1); - validate_against_rust!(4, 2, 2); - validate_against_rust!(4, 2, 3); - validate_against_rust!(4, 2, 4); - validate_against_rust!(4, 4); - validate_against_rust!(4, 4, 0); - validate_against_rust!(4, 4, 1); - validate_against_rust!(4, 4, 2); - validate_against_rust!(4, 4, 3); - validate_against_rust!(4, 4, 4); - validate_against_rust!(4, 8); - validate_against_rust!(4, 8, 0); - validate_against_rust!(4, 8, 1); - validate_against_rust!(4, 8, 2); - validate_against_rust!(4, 8, 3); - validate_against_rust!(4, 8, 4); - validate_against_rust!(4, 16); - validate_against_rust!(4, 16, 0); - validate_against_rust!(4, 16, 1); - validate_against_rust!(4, 16, 2); - validate_against_rust!(4, 16, 3); - validate_against_rust!(4, 16, 4); - } -} - -#[cfg(kani)] -mod proofs { - use core::alloc::Layout; - - use super::*; - - impl kani::Arbitrary for DstLayout { - fn any() -> Self { - let align: NonZeroUsize = kani::any(); - let size_info: SizeInfo = kani::any(); - - kani::assume(align.is_power_of_two()); - kani::assume(align < DstLayout::THEORETICAL_MAX_ALIGN); - - // For testing purposes, we most care about instantiations of - // `DstLayout` that can correspond to actual Rust types. We use - // `Layout` to verify that our `DstLayout` satisfies the validity - // conditions of Rust layouts. - kani::assume( - match size_info { - SizeInfo::Sized { size } => Layout::from_size_align(size, align.get()), - SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size: _ }) => { - // `SliceDst` cannot encode an exact size, but we know - // it is at least `offset` bytes. - Layout::from_size_align(offset, align.get()) - } - } - .is_ok(), - ); - - Self { align: align, size_info: size_info, statically_shallow_unpadded: kani::any() } - } - } - - impl kani::Arbitrary for SizeInfo { - fn any() -> Self { - let is_sized: bool = kani::any(); - - match is_sized { - true => { - let size: usize = kani::any(); - - kani::assume(size <= DstLayout::MAX_SIZE); - - SizeInfo::Sized { size } - } - false => SizeInfo::SliceDst(kani::any()), - } - } - } - - impl kani::Arbitrary for TrailingSliceLayout { - fn any() -> Self { - let elem_size: usize = kani::any(); - let offset: usize = kani::any(); - - kani::assume(elem_size < DstLayout::MAX_SIZE); - kani::assume(offset < DstLayout::MAX_SIZE); - - TrailingSliceLayout { elem_size, offset } - } - } - - #[kani::proof] - fn prove_requires_dynamic_padding() { - let layout: DstLayout = kani::any(); - - let SizeInfo::SliceDst(size_info) = layout.size_info else { - kani::assume(false); - loop {} - }; - - let meta: usize = kani::any(); - - let Some(trailing_slice_size) = size_info.elem_size.checked_mul(meta) else { - // The `trailing_slice_size` exceeds `usize::MAX`; `meta` is invalid. - kani::assume(false); - loop {} - }; - - let Some(unpadded_size) = size_info.offset.checked_add(trailing_slice_size) else { - // The `unpadded_size` exceeds `usize::MAX`; `meta`` is invalid. - kani::assume(false); - loop {} - }; - - if unpadded_size >= DstLayout::MAX_SIZE { - // The `unpadded_size` exceeds `isize::MAX`; `meta` is invalid. - kani::assume(false); - loop {} - } - - let trailing_padding = util::padding_needed_for(unpadded_size, layout.align); - - if !layout.requires_dynamic_padding() { - assert!(trailing_padding == 0); - } - } - - #[kani::proof] - fn prove_dst_layout_extend() { - use crate::util::{max, min, padding_needed_for}; - - let base: DstLayout = kani::any(); - let field: DstLayout = kani::any(); - let packed: Option = kani::any(); - - if let Some(max_align) = packed { - kani::assume(max_align.is_power_of_two()); - kani::assume(base.align <= max_align); - } - - // The base can only be extended if it's sized. - kani::assume(matches!(base.size_info, SizeInfo::Sized { .. })); - let base_size = if let SizeInfo::Sized { size } = base.size_info { - size - } else { - unreachable!(); - }; - - // Under the above conditions, `DstLayout::extend` will not panic. - let composite = base.extend(field, packed); - - // The field's alignment is clamped by `max_align` (i.e., the - // `packed` attribute, if any) [1]. - // - // [1] Per https://doc.rust-lang.org/reference/type-layout.html#the-alignment-modifiers: - // - // The alignments of each field, for the purpose of positioning - // fields, is the smaller of the specified alignment and the - // alignment of the field's type. - let field_align = min(field.align, packed.unwrap_or(DstLayout::THEORETICAL_MAX_ALIGN)); - - // The struct's alignment is the maximum of its previous alignment and - // `field_align`. - assert_eq!(composite.align, max(base.align, field_align)); - - // Compute the minimum amount of inter-field padding needed to - // satisfy the field's alignment, and offset of the trailing field. - // [1] - // - // [1] Per https://doc.rust-lang.org/reference/type-layout.html#the-alignment-modifiers: - // - // Inter-field padding is guaranteed to be the minimum required in - // order to satisfy each field's (possibly altered) alignment. - let padding = padding_needed_for(base_size, field_align); - let offset = base_size + padding; - - // For testing purposes, we'll also construct `alloc::Layout` - // stand-ins for `DstLayout`, and show that `extend` behaves - // comparably on both types. - let base_analog = Layout::from_size_align(base_size, base.align.get()).unwrap(); - - match field.size_info { - SizeInfo::Sized { size: field_size } => { - if let SizeInfo::Sized { size: composite_size } = composite.size_info { - // If the trailing field is sized, the resulting layout will - // be sized. Its size will be the sum of the preceding - // layout, the size of the new field, and the size of - // inter-field padding between the two. - assert_eq!(composite_size, offset + field_size); - - let field_analog = - Layout::from_size_align(field_size, field_align.get()).unwrap(); - - if let Ok((actual_composite, actual_offset)) = base_analog.extend(field_analog) - { - assert_eq!(actual_offset, offset); - assert_eq!(actual_composite.size(), composite_size); - assert_eq!(actual_composite.align(), composite.align.get()); - } else { - // An error here reflects that composite of `base` - // and `field` cannot correspond to a real Rust type - // fragment, because such a fragment would violate - // the basic invariants of a valid Rust layout. At - // the time of writing, `DstLayout` is a little more - // permissive than `Layout`, so we don't assert - // anything in this branch (e.g., unreachability). - } - } else { - panic!("The composite of two sized layouts must be sized.") - } - } - SizeInfo::SliceDst(TrailingSliceLayout { - offset: field_offset, - elem_size: field_elem_size, - }) => { - if let SizeInfo::SliceDst(TrailingSliceLayout { - offset: composite_offset, - elem_size: composite_elem_size, - }) = composite.size_info - { - // The offset of the trailing slice component is the sum - // of the offset of the trailing field and the trailing - // slice offset within that field. - assert_eq!(composite_offset, offset + field_offset); - // The elem size is unchanged. - assert_eq!(composite_elem_size, field_elem_size); - - let field_analog = - Layout::from_size_align(field_offset, field_align.get()).unwrap(); - - if let Ok((actual_composite, actual_offset)) = base_analog.extend(field_analog) - { - assert_eq!(actual_offset, offset); - assert_eq!(actual_composite.size(), composite_offset); - assert_eq!(actual_composite.align(), composite.align.get()); - } else { - // An error here reflects that composite of `base` - // and `field` cannot correspond to a real Rust type - // fragment, because such a fragment would violate - // the basic invariants of a valid Rust layout. At - // the time of writing, `DstLayout` is a little more - // permissive than `Layout`, so we don't assert - // anything in this branch (e.g., unreachability). - } - } else { - panic!("The extension of a layout with a DST must result in a DST.") - } - } - } - } - - #[kani::proof] - #[kani::should_panic] - fn prove_dst_layout_extend_dst_panics() { - let base: DstLayout = kani::any(); - let field: DstLayout = kani::any(); - let packed: Option = kani::any(); - - if let Some(max_align) = packed { - kani::assume(max_align.is_power_of_two()); - kani::assume(base.align <= max_align); - } - - kani::assume(matches!(base.size_info, SizeInfo::SliceDst(..))); - - let _ = base.extend(field, packed); - } - - #[kani::proof] - fn prove_dst_layout_pad_to_align() { - use crate::util::padding_needed_for; - - let layout: DstLayout = kani::any(); - - let padded = layout.pad_to_align(); - - // Calling `pad_to_align` does not alter the `DstLayout`'s alignment. - assert_eq!(padded.align, layout.align); - - if let SizeInfo::Sized { size: unpadded_size } = layout.size_info { - if let SizeInfo::Sized { size: padded_size } = padded.size_info { - // If the layout is sized, it will remain sized after padding is - // added. Its sum will be its unpadded size and the size of the - // trailing padding needed to satisfy its alignment - // requirements. - let padding = padding_needed_for(unpadded_size, layout.align); - assert_eq!(padded_size, unpadded_size + padding); - - // Prove that calling `DstLayout::pad_to_align` behaves - // identically to `Layout::pad_to_align`. - let layout_analog = - Layout::from_size_align(unpadded_size, layout.align.get()).unwrap(); - let padded_analog = layout_analog.pad_to_align(); - assert_eq!(padded_analog.align(), layout.align.get()); - assert_eq!(padded_analog.size(), padded_size); - } else { - panic!("The padding of a sized layout must result in a sized layout.") - } - } else { - // If the layout is a DST, padding cannot be statically added. - assert_eq!(padded.size_info, layout.size_info); - } - } -} diff --git a/zerocopy/src/layout/cast_from.rs b/zerocopy/src/layout/cast_from.rs new file mode 100644 index 0000000000..eea5a6812c --- /dev/null +++ b/zerocopy/src/layout/cast_from.rs @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT +// +// Copyright 2026 The Fuchsia Authors +// +// Licensed under the 2-Clause BSD License , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +//! Compile-time cast planning and runtime pointer projection. +//! +//! `CastPlan` selects and applies numerical metadata transformations from two +//! layouts. `CastParams` associates a selected plan with a `KnownLayout` type +//! pair and converts their typed metadata to and from element counts. Its +//! associated constant performs selection at compile time; `CastFrom::project` +//! uses that constant to transform metadata and construct the destination +//! pointer at runtime. + +use crate::*; + +/// A numerical metadata transformation selected for a pair of layouts. +/// +/// A raw plan contains no type or layout identity. Its parameters preserve size +/// only in relation to the layouts used by `try_compute`. `CastParams` retains +/// that connection to the `KnownLayout` pair used for pointer projection. +#[derive(Copy, Clone)] +enum CastPlan { + // At compile time (specifically, post-monomorphization time), + // we need to compute two things: + // - Whether, given *any* `*Src`, it is possible to construct a + // `*Dst` which addresses the same number of bytes (ie, + // whether, for any `Src` pointer metadata, there exists `Dst` + // pointer metadata that addresses the same number of bytes) + // - If this is possible, any information necessary to perform + // the `Src`->`Dst` metadata conversion at runtime. + // + // For slice DSTs, destination metadata is an affine function + // of source metadata: + // + // dst_meta = offset_delta_elems + src_meta * elem_multiple + // + // `elem_multiple` scales the destination's trailing element + // size to the source's. `offset_delta_elems` selects a + // destination size equal to the source's zero-element size. + // We then prove that the two complete rounded size formulas + // produce the same sequence for all metadata. This is + // necessary because a packed outer DST can retain a nested + // field's rounding operation; comparing only physical + // trailing-slice offsets is not sufficient. + /// Converts between slice DSTs using a size-preserving affine map. + UnsizedToUnsized { offset_delta_elems: usize, elem_multiple: usize }, + + /// Uses the metadata selected for a sized source and a slice DST. + SizedToUnsized { dst_meta: usize }, + + /// Converts between sized layouts with equal sizes. + SizedToSized, +} + +impl CastPlan { + /// Given a nonzero `dst.elem_size` that exactly divides + /// `src.elem_size`, produces `true` only when the following + /// metadata map preserves the complete object size for every + /// source element count `src_meta`: + /// + /// ```text + /// dst_meta = dst_base + src_meta * (src.elem_size / dst.elem_size) + /// ``` + /// + /// This recognizes sufficient conditions for equality. A + /// `false` result also includes equivalent size sequences + /// that this test cannot recognize, or an adjustment whose + /// normalized components cannot be represented in `usize`. + const fn size_sequences_match( + src: TrailingSliceLayout, + dst: TrailingSliceLayout, + dst_base: usize, + ) -> bool { + let base_bytes = match dst_base.checked_mul(dst.elem_size) { + Some(bytes) => bytes, + None => return false, + }; + let shifted_dst = match dst.advance(base_bytes, src.elem_size) { + Some(layout) => layout, + None => return false, + }; + src.has_same_size_sequence(shifted_dst) + } + + /// Given the complete layouts of `Src` and `Dst`, produces + /// parameters for a cast that preserves object size. + /// + /// A `Some` result establishes that `Src`'s alignment is at + /// least `Dst`'s and that the metadata map preserves size for + /// every valid source metadata value. + /// + /// Supports casts between sized types of equal size, from a + /// sized type to a slice DST, and between slice DSTs. A slice + /// DST destination must have nonzero-sized elements; between + /// slice DSTs, its element size must exactly divide the + /// source's nonzero element size. + /// + /// Produces `None` for unsupported casts or when this method + /// cannot establish a size-preserving metadata map. Rejection + /// does not establish that no such map exists. + const fn try_compute(src_layout: &DstLayout, dst_layout: &DstLayout) -> Option { + if src_layout.align.get() < dst_layout.align.get() { + return None; + } + + let plan = match (src_layout.size_info, dst_layout.size_info) { + (SizeInfo::Sized { size: src_size }, SizeInfo::Sized { size: dst_size }) => { + if src_size != dst_size { + return None; + } + + // SAFETY: We checked above that `src_size == + // dst_size`. + CastPlan::SizedToSized + } + (SizeInfo::Sized { size: src_size }, SizeInfo::SliceDst(_)) => { + let dst_meta = match dst_layout.metadata_for_exact_size(src_size) { + Some(meta) => meta, + None => return None, + }; + + // SAFETY: The preceding math ensures that a `Dst` + // with `dst_meta` addresses `src_size` bytes. + CastPlan::SizedToUnsized { dst_meta } + } + (SizeInfo::SliceDst(src), SizeInfo::SliceDst(dst)) => { + let dst_elem_size = if let Some(e) = NonZeroUsize::new(dst.elem_size) { + e + } else { + return None; + }; + + if src.elem_size < dst.elem_size { + return None; + } + + let (elem_multiple, described_src_elem_size) = + super::max_elems_for_bytes(src.elem_size, dst_elem_size); + if described_src_elem_size != src.elem_size { + return None; + } + + // Prefer an exact destination-element adjustment + // between `src.size_offset()` and + // `dst.size_offset()`. This preserves offset-based + // cast compatibility and metadata selection when + // several lengths have the same rounded size. It + // also avoids choosing a length whose subsequent + // padding sequence differs from the source's. + let exact_offset_delta_elems = + match src.size_offset().checked_sub(dst.size_offset()) { + Some(delta) => { + let (elems, described_delta) = + super::max_elems_for_bytes(delta, dst_elem_size); + if described_delta == delta { + Some(elems) + } else { + None + } + } + None => None, + }; + + let offset_delta_elems = match exact_offset_delta_elems { + Some(elems) if Self::size_sequences_match(src, dst, elems) => elems, + _ => { + let src_zero_size = match src.size_for_elems(0) { + Some(size) => size, + None => return None, + }; + let elems = match dst_layout.metadata_for_exact_size(src_zero_size) { + Some(elems) => elems, + None => return None, + }; + if !Self::size_sequences_match(src, dst, elems) { + return None; + } + elems + } + }; + + CastPlan::UnsizedToUnsized { + // SAFETY: `size_sequences_match` proves that + // this affine metadata map preserves size. + offset_delta_elems, + // SAFETY: We checked above that this is an exact + // ratio of source to destination element size. + elem_multiple, + } + } + _ => return None, + }; + + // SAFETY: We checked above that `src.align >= dst.align`. + Some(plan) + } + + /// Applies the numerical metadata transformation to an element count. + /// + /// # Safety + /// + /// For `UnsizedToUnsized`, neither `src_meta * elem_multiple` nor + /// `offset_delta_elems + src_meta * elem_multiple` may overflow `usize`. + /// The other variants impose no condition on `src_meta` and ignore it. + #[inline(always)] + unsafe fn cast_metadata(self, src_meta: usize) -> usize { + match self { + CastPlan::UnsizedToUnsized { offset_delta_elems, elem_multiple } => { + // SAFETY: The caller provides the helper's complete + // no-overflow precondition. + unsafe { super::add_scaled_metadata(offset_delta_elems, src_meta, elem_multiple) } + } + CastPlan::SizedToUnsized { dst_meta } => dst_meta, + CastPlan::SizedToSized => 0, + } + } +} + +/// A selected plan associated with the layouts of `Src` and `Dst`. +/// +/// The private fields are initialized only by `CAST_PARAMS`, which selects the +/// plan using `Src::LAYOUT` and `Dst::LAYOUT`. The markers retain this type-pair +/// association when the plan is applied to typed pointer metadata at runtime. +/// +/// # Safety +/// +/// `Src`'s alignment must not be smaller than `Dst`'s alignment. +struct CastParams { + plan: CastPlan, + _src: PhantomData, + _dst: PhantomData, +} + +impl Copy for CastParams {} +impl Clone for CastParams { + fn clone(&self) -> Self { + *self + } +} + +impl CastParams { + /// Computes the parameters at compile time, rejecting incompatible layouts + /// with a post-monomorphization error. + const CAST_PARAMS: Self = match CastPlan::try_compute(&Src::LAYOUT, &Dst::LAYOUT) { + // SAFETY: `try_compute` checked that `Src::LAYOUT.align >= + // Dst::LAYOUT.align`. + Some(plan) => Self { plan, _src: PhantomData, _dst: PhantomData }, + None => { + const_panic!("cannot `transmute_ref!` or `transmute_mut!` between incompatible types") + } + }; + + /// # Safety + /// + /// `src_meta` describes a `Src` whose size is no larger than `isize::MAX`. + /// + /// The returned metadata describes a `Dst` of the same size as the original + /// `Src`. + #[inline(always)] + unsafe fn cast_metadata(self, src_meta: Src::PointerMetadata) -> Dst::PointerMetadata { + let src_meta = match self.plan { + CastPlan::UnsizedToUnsized { .. } => src_meta.to_elem_count(), + _ => 0, + }; + + // SAFETY: The sized variants perform no unchecked arithmetic. + // For `UnsizedToUnsized`, `self.plan` was selected for the layouts and + // witnesses that this affine map makes `Src` and `Dst`'s complete + // rounded size formulas equal for every source metadata value. + // Interpret the arithmetic in this proof over the + // mathematical nonnegative integers. Since the + // caller promises that `src_meta` is valid `Src` + // metadata, the source object size `src_size` it + // describes is at most `isize::MAX`. Let + // `src_elem_size` and `dst_elem_size` be the + // respective trailing-slice element sizes. Then: + // + // src_meta * src_elem_size + // <= src_size <= isize::MAX. + // + // Since `elem_multiple` is the exact ratio + // `src_elem_size / dst_elem_size` and + // `dst_elem_size >= 1`, + // + // src_meta * elem_multiple + // <= src_meta * src_elem_size + // <= isize::MAX <= usize::MAX. + // + // Thus `scaled_elems = src_meta * elem_multiple` + // is representable, and its byte contribution is + // `scaled_elems * dst_elem_size = src_meta * + // src_elem_size`. Let `base_bytes = + // offset_delta_elems * dst_elem_size`. + // `size_sequences_match` compares the source + // formula with the destination formula advanced by + // `base_bytes`. The latter's unrounded term + // contains `base_bytes + src_meta * src_elem_size`, + // and its complete size is at least that large + // (formally checked by Kani in + // `prove_size_formula_bounds_size_offset`). + // Sequence equality therefore gives, over the + // mathematical nonnegative integers, + // + // base_bytes + src_meta * src_elem_size + // = (offset_delta_elems + scaled_elems) + // * dst_elem_size + // <= src_size. + // + // Since `dst_elem_size >= 1`, the metadata sum + // `offset_delta_elems + scaled_elems` is also at + // most `src_size`, and thus at most `isize::MAX <= + // usize::MAX`. The sum is therefore representable, + // and the returned metadata describes a `Dst` of + // the same size. + // SAFETY: The bounds above establish the helper's + // complete no-overflow precondition. + let dst_meta = unsafe { self.plan.cast_metadata(src_meta) }; + Dst::PointerMetadata::from_elem_count(dst_meta) + } +} + +pub(crate) struct CastFrom { + _never: core::convert::Infallible, + _marker: PhantomData, +} + +// SAFETY: The implementation of `Project::project` preserves the address +// of the referent – it only modifies pointer metadata. +unsafe impl crate::pointer::cast::Cast for CastFrom +where + Src: KnownLayout + ?Sized, + Dst: KnownLayout + ?Sized, +{ +} + +// SAFETY: The implementation of `Project::project` preserves the size of +// the referent (see inline comments for a more detailed proof of this). +unsafe impl crate::pointer::cast::CastExact for CastFrom +where + Src: KnownLayout + ?Sized, + Dst: KnownLayout + ?Sized, +{ +} + +// SAFETY: `project` produces a pointer which refers to the same referent +// bytes as its input, or to a subset of them (see inline comments for a +// more detailed proof of this). It does this using provenance-preserving +// operations. +unsafe impl crate::pointer::cast::Project for CastFrom +where + Src: KnownLayout + ?Sized, + Dst: KnownLayout + ?Sized, +{ + /// # PME + /// + /// Generates a post-monomorphization error if it is not possible to + /// implement soundly. + // + // FIXME(#1817): Support Sized->Unsized and Unsized->Sized casts + fn project(src: PtrInner<'_, Src>) -> *mut Dst { + let src_meta = ::pointer_to_metadata(src.as_ptr()); + let params = CastParams::::CAST_PARAMS; + + // SAFETY: `src: PtrInner` guarantees that `src`'s referent is zero + // bytes or lives in a single allocation, which means that it is no + // larger than `isize::MAX` bytes [1]. + // + // [1] https://doc.rust-lang.org/1.92.0/std/ptr/index.html#allocation + let dst_meta = unsafe { params.cast_metadata(src_meta) }; + + ::raw_from_ptr_len(src.as_non_null().cast(), dst_meta).as_ptr() + } +} diff --git a/zerocopy/src/layout/mod.rs b/zerocopy/src/layout/mod.rs new file mode 100644 index 0000000000..443de644cc --- /dev/null +++ b/zerocopy/src/layout/mod.rs @@ -0,0 +1,4835 @@ +// SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT +// +// Copyright 2024 The Fuchsia Authors +// +// Licensed under the 2-Clause BSD License , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +#[allow(dead_code)] +mod nested_reference; + +use core::{mem, num::NonZeroUsize}; + +use crate::util; + +/// The target pointer width, counted in bits. +const POINTER_WIDTH_BITS: usize = mem::size_of::() * 8; + +/// The layout of a type which might be dynamically-sized. +/// +/// `DstLayout` describes the layout of sized types, slice types, and "slice +/// DSTs" - ie, those that are known by the type system to have a trailing slice +/// (as distinguished from `dyn Trait` types - such types *might* have a +/// trailing slice type, but the type system isn't aware of it). +/// +/// Note that `DstLayout` does not have any internal invariants, so no guarantee +/// is made that a `DstLayout` conforms to any of Rust's requirements regarding +/// the layout of real Rust types or instances of types. +#[doc(hidden)] +#[allow(missing_debug_implementations, missing_copy_implementations)] +#[cfg_attr(any(kani, test), derive(Debug, PartialEq, Eq))] +#[derive(Copy, Clone)] +#[cfg_attr(kani, derive(kani::Arbitrary))] +pub struct DstLayout { + pub(crate) align: NonZeroUsize, + pub(crate) size_info: SizeInfo, + // Is it guaranteed statically (without knowing a value's runtime metadata) + // that the top-level type contains no padding? This does *not* apply + // recursively - for example, `[(u8, u16)]` has `statically_shallow_unpadded + // = true` even though this type likely has padding inside each `(u8, u16)`. + pub(crate) statically_shallow_unpadded: bool, +} + +#[cfg_attr(any(kani, test), derive(Debug, PartialEq, Eq))] +#[derive(Copy, Clone)] +#[cfg_attr(kani, derive(kani::Arbitrary))] +pub(crate) enum SizeInfo { + Sized { size: usize }, + SliceDst(TrailingSliceLayout), +} + +/// The alignment and phase of a rounded size calculation. +/// +/// If `encoded` is the stored non-zero integer, then its highest set bit is a +/// power-of-two alignment `align`, and its remaining lower bits are a `phase` +/// in `0..align`. Thus every non-zero bit pattern is a valid encoding, and: +/// +/// ```text +/// encoded = align + phase +/// ``` +/// +/// ```aeneas +/// model RoundingValue where +/// align : Nat +/// phase : Nat +/// align_pow2 : align.isPowerOfTwo +/// phase_lt : phase < align +/// fits : align + phase ≤ Usize.max +/// decode self => +/// let word := self._0.value +/// let align := 2 ^ Nat.log2 word +/// { align := align, phase := word - align, .. } +/// ``` +#[cfg_attr(any(kani, test), derive(Debug, PartialEq, Eq))] +#[repr(transparent)] +#[derive(Copy, Clone)] +#[cfg_attr(kani, derive(kani::Arbitrary))] +pub(crate) struct RoundingAlignAndPhase(NonZeroUsize); + +impl RoundingAlignAndPhase { + /// Encodes `align` and `phase`. + /// + /// # Panics + /// + /// Panics if `align` is not a power of two or `phase` is not less than + /// `align`. + #[inline(always)] + #[cfg_attr(kani, kani::requires(align.is_power_of_two() && phase < align.get()))] + #[cfg_attr(kani, kani::ensures(|result| result.0.get() == align.get() + phase))] + /// + /// ```aeneas + /// spec encoding_new_spec + /// requires ha : (align : Nat).isPowerOfTwo + /// requires hp : (phase : Nat) < (align : Nat) + /// ensures encoded => encoded.align = (align : Nat) ∧ encoded.phase = (phase : Nat) + /// ``` + pub(crate) const fn new(align: NonZeroUsize, phase: usize) -> Self { + #[cfg(kani)] + #[kani::proof_for_contract(RoundingAlignAndPhase::new)] + #[kani::solver(kissat)] + fn proof() { + RoundingAlignAndPhase::new(kani::any(), kani::any()); + } + + const_assert!(align.get().is_power_of_two()); + const_assert!(phase < align.get()); + + // Since `align` is a power of two and `phase < align`, their set bits + // are disjoint. The result is therefore non-zero and losslessly stores + // both components. + let encoded = align.get() | phase; + match NonZeroUsize::new(encoded) { + Some(encoded) => Self(encoded), + None => const_unreachable!(), + } + } + + /// Decodes the alignment and phase. + #[inline(always)] + #[cfg_attr(kani, kani::ensures(|&(align, phase)| { + align.is_power_of_two() && phase < align.get() + && align.get().checked_add(phase) == Some(self.0.get()) + }))] + /// + /// ```aeneas + /// spec encoding_components_spec + /// ensures (align, phase) => + /// align.value = self.align ∧ phase.value = self.phase + /// ``` + pub(crate) const fn components(self) -> (NonZeroUsize, usize) { + #[cfg(kani)] + #[kani::proof_for_contract(RoundingAlignAndPhase::components)] + #[kani::solver(kissat)] + fn proof() { + kani::any::().components(); + } + // `leading_zeros <= POINTER_WIDTH_BITS - 1` because the encoded value + // is non-zero. Converting `leading_zeros` to `usize` cannot truncate + // because it is at most the number of bits in a `usize`. + // Keep the primitive usize operation visible to Aeneas. + #[allow( + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::needless_nonzero_get + )] + let shift = (POINTER_WIDTH_BITS - 1) - (self.0.get().leading_zeros() as usize); + #[allow(clippy::arithmetic_side_effects)] + let align = 1usize << shift; + let align = match NonZeroUsize::new(align) { + Some(align) => align, + None => const_unreachable!(), + }; + // `align` is the highest set bit, so XOR removes exactly that bit and + // leaves a value in `0..align`. + let phase = self.0.get() ^ align.get(); + (align, phase) + } + + /// Decodes the alignment, which is the highest set bit of `self`. + #[inline(always)] + #[cfg_attr(kani, kani::ensures(|align| { + align.is_power_of_two() && align.get() <= self.0.get() + && self.0.get() - align.get() < align.get() + }))] + /// + /// ```aeneas + /// spec encoding_align_spec + /// ensures align => (align : Nat) = self.align + /// ``` + pub(crate) const fn align(self) -> NonZeroUsize { + #[cfg(kani)] + #[kani::proof_for_contract(RoundingAlignAndPhase::align)] + #[kani::solver(kissat)] + fn proof() { + kani::any::().align(); + } + + self.components().0 + } +} + +#[cfg_attr(any(kani, test), derive(Debug, PartialEq, Eq))] +#[derive(Copy, Clone)] +#[cfg_attr(kani, derive(kani::Arbitrary))] +pub(crate) struct TrailingSliceLayout { + /// The offset of the first byte of the trailing slice field. Note that this + /// is NOT the same as the minimum size of the type. For example, consider + /// the following type: + /// + /// ```ignore + /// #[repr(C, align(2))] + /// struct Foo { + /// header: u16, + /// tag: u8, + /// tail: [u8], + /// } + /// ``` + /// + /// In `Foo`, `tail` is at byte offset 3. When `tail.len() == 0`, `tail` is + /// followed by a padding byte. + pub(crate) offset: usize, + /// The size of the element type of the trailing slice field. + pub(crate) elem_size: E, + /// The trailing slice's byte offset plus fixed trailing padding, minus the + /// rounding phase. + /// + /// Fixed trailing padding excludes padding added by the final alignment + /// rounding. The rounding alignment and phase are stored in + /// `size_rounding_align_and_phase`. This value is independent of the slice + /// length and can be smaller than, equal to, or larger than `offset`. + /// + /// For a completed layout, this value is a multiple of `DstLayout::align`. + /// The minimum object size equals this value when the phase is zero; + /// otherwise, it exceeds this value by one rounding-alignment unit. + pub(crate) size_base: usize, + /// The alignment and byte phase of the rounding applied to the trailing + /// slice's contribution to object size. + /// + /// The alignment `size_align` is a nonzero power of two. The phase + /// `size_phase` is the fixed byte contribution inside that rounding, in + /// `0..size_align`. Both are independent of the trailing slice's length. + /// For `elems` trailing elements, the enclosing object's size is: + /// + /// ```text + /// size_base + round_up(size_phase + elems * elem_size, size_align) + /// ``` + /// + /// For a completed layout, `size_align` is at least `DstLayout::align`. + /// Packing can reduce the enclosing type's alignment and the alignment + /// used to place its trailing field without removing padding required by + /// a nested DST's own layout. For example, a `repr(packed(2))` wrapper + /// around a DST with alignment 4 can have an object alignment of 2 and a + /// rounding alignment of 4. + /// + /// The rounding parameters account for padding preserved within nested + /// fields. Their rounding origin can differ from the physical start of + /// the trailing slice, so the phase need not equal `offset % size_align`. + pub(crate) size_rounding_align_and_phase: RoundingAlignAndPhase, +} + +impl TrailingSliceLayout { + /// Produces `self.size_base` rounded down to a multiple of `size_align`, + /// plus `size_phase`, where both components come from + /// `self.size_rounding_align_and_phase`. + /// + /// This value need not equal `self.offset`, the trailing slice's byte + /// offset. + #[inline(always)] + #[cfg_attr(kani, kani::ensures(|&offset| { + let (align, phase) = self.size_rounding_align_and_phase.components(); + offset == util::round_down_to_next_multiple_of_alignment(self.size_base, align) + phase + }))] + const fn size_offset(&self) -> usize { + #[cfg(kani)] + #[kani::proof_for_contract(TrailingSliceLayout::::size_offset)] + #[kani::solver(kissat)] + fn proof() { + kani::any::().size_offset(); + } + + let (size_align, size_phase) = self.size_rounding_align_and_phase.components(); + let aligned_base = + util::round_down_to_next_multiple_of_alignment(self.size_base, size_align); + // The aligned base and phase have disjoint set bits. + aligned_base | size_phase + } + + /// Produces the largest trailing-slice byte capacity whose complete, padded + /// object fits in `available_bytes`, or `None` if the object does not fit + /// even with an empty trailing slice. + /// + /// This capacity is independent of `elem_size`: it need not represent a + /// whole number of elements, even for a valid Rust layout. For example, a + /// slice of `[u8; 3]` elements has three-byte elements and alignment 1. + /// With `available_bytes = 4`, this method produces `Some(4)`, although + /// only one whole element, occupying three bytes, fits. + #[inline(always)] + #[cfg_attr(kani, kani::ensures(|&result| { + let (align, phase) = self.size_rounding_align_and_phase.components(); + result == available_bytes.checked_sub(self.size_base) + .map(|bytes| util::round_down_to_next_multiple_of_alignment(bytes, align)) + .and_then(|rounded| rounded.checked_sub(phase)) + }))] + const fn max_trailing_bytes(&self, available_bytes: usize) -> Option { + #[cfg(kani)] + #[kani::proof_for_contract(TrailingSliceLayout::::max_trailing_bytes)] + #[kani::solver(kissat)] + fn proof() { + kani::any::().max_trailing_bytes(kani::any()); + } + + #[cfg(kani)] + #[kani::proof_for_contract(TrailingSliceLayout::::max_trailing_bytes)] + #[kani::solver(kissat)] + fn proof_nonzero() { + kani::any::>().max_trailing_bytes(kani::any()); + } + + let (size_align, size_phase) = self.size_rounding_align_and_phase.components(); + + // Interpret the normalized size formula over the nonnegative integers: + // + // object_size(trailing_bytes) + // = self.size_base + // + round_up(size_phase + trailing_bytes, size_align). + // + // We seek the largest byte count with + // `object_size(trailing_bytes) <= available_bytes`. + // + // At zero trailing bytes, the input to rounding is just `size_phase`. + // Since `0 <= size_phase < size_align`, the least alignment multiple at + // or above it is zero for a zero phase, and `size_align` otherwise. + // Thus `rounded_phase = round_up(size_phase, size_align)` is exactly + // the rounded contribution of an empty tail. + let rounded_phase = if size_phase == 0 { 0 } else { size_align.get() }; + + // Adding the fixed base gives `minimum_size = object_size(0)`. The size + // formula is nondecreasing in the trailing byte count, so this is the + // minimum object size. If this sum exceeds `usize::MAX`, it also + // exceeds `available_bytes`, and no tail can fit. + let minimum_size = match self.size_base.checked_add(rounded_phase) { + Some(size) => size, + None => return None, + }; + + // Subtracting the minimum size gives the budget for increasing the + // rounded contribution. Failure means even the minimum size exceeds the + // available space. Success establishes `minimum_size + extra_bytes = + // available_bytes`. + let extra_bytes = match available_bytes.checked_sub(minimum_size) { + Some(bytes) => bytes, + None => return None, + }; + + // The rounded contribution can increase only by multiples of + // `size_align`. Its largest permitted increase is therefore + // `aligned_bytes = round_down(extra_bytes, size_align)`, with: + // + // aligned_bytes <= extra_bytes < aligned_bytes + size_align. + // + // Hence `rounded_phase + aligned_bytes` is the largest alignment + // multiple that fits after the base: adding the base gives at most + // `minimum_size + extra_bytes = available_bytes`, while the next + // alignment multiple would exceed that budget. + let aligned_bytes = util::round_down_to_next_multiple_of_alignment(extra_bytes, size_align); + + // Rounding an empty tail adds `rounded_phase - size_phase` bytes. This + // is `initial_padding`: the tail can use these bytes without increasing + // the rounded contribution. The two cases for `rounded_phase` give `0 + // <= initial_padding < size_align`, so the subtraction is + // representable. + #[allow(clippy::arithmetic_side_effects)] + let initial_padding = rounded_phase - size_phase; + + // For any nonnegative candidate byte count, the aligned limit above + // gives the following equivalences: + // + // object_size(candidate_bytes) <= available_bytes + // iff round_up(size_phase + candidate_bytes, size_align) + // <= rounded_phase + aligned_bytes + // iff size_phase + candidate_bytes + // <= rounded_phase + aligned_bytes + // iff candidate_bytes <= aligned_bytes + initial_padding. + // + // Removing `round_up` is valid because its upper bound is an alignment + // multiple. Thus `aligned_bytes + initial_padding` is exactly the + // largest tail that fits. Since `aligned_bytes` is a multiple of the + // power-of-two `size_align` and `initial_padding < size_align`, their + // set bits are disjoint. Bitwise OR therefore equals their sum, with no + // carry or overflow. + let trailing_bytes = aligned_bytes | initial_padding; + Some(trailing_bytes) + } +} + +impl TrailingSliceLayout { + /// Returns the difference between the complete object size and the + /// trailing-slice end for `elems` elements, modulo `usize::MAX + 1`. + /// + /// If the object size fits in `usize` and contains the trailing slice, + /// this is the exact trailing padding. Other inputs need not describe a + /// Rust type; their result is still the modular difference. + #[inline(always)] + #[cfg_attr(kani, kani::ensures(|&padding| { + let (align, phase) = self.size_rounding_align_and_phase.components(); + let trailing_bytes = elems.wrapping_mul(self.elem_size); + let mask = align.get() - 1; + let rounded = phase.wrapping_add(trailing_bytes).wrapping_add(mask) & !mask; + let object_size = self.size_base.wrapping_add(rounded); + padding == object_size.wrapping_sub(self.offset.wrapping_add(trailing_bytes)) + }))] + pub(crate) const fn padding_for_elems(self, elems: usize) -> usize { + #[cfg(kani)] + #[kani::proof_for_contract(TrailingSliceLayout::padding_for_elems)] + #[kani::solver(kissat)] + fn proof() { + kani::any::().padding_for_elems(kani::any()); + } + + let (size_align, size_phase) = self.size_rounding_align_and_phase.components(); + + // Since `size_align` is a nonzero power of two, subtracting one gives + // a mask selecting precisely the remainder modulo `size_align`. + #[allow(clippy::arithmetic_side_effects)] + let size_mask = size_align.get() - 1; + + // Removing whole alignment multiples from each element's size + // preserves the trailing-byte count modulo `size_align`. If elements + // occupy whole alignment units, this is zero, making the padding's + // independence from the element count explicit to the optimizer. + let elem_remainder = self.elem_size & size_mask; + + // The product is congruent to `elems * self.elem_size` modulo + // `size_align`. Wrapping preserves that congruence because every + // power-of-two `size_align` divides `usize::MAX + 1`. Masking then + // gives the exact trailing-byte remainder in `0..size_align`. + let trailing_remainder = elems.wrapping_mul(elem_remainder) & size_mask; + + // Adding `size_phase` gives the same remainder as the mathematical + // `size_phase + elems * self.elem_size`. Reducing before adding the + // phase also exposes this periodicity to the optimizer. + let rounding_input = size_phase.wrapping_add(trailing_remainder); + + // `rounding_padding` is the unique value in `0..size_align` that + // rounds `rounding_input` up to an alignment multiple. Since only + // the remainder determines this value, it is also the padding added + // by `round_up(size_phase + elems * self.elem_size, size_align)`. + let rounding_padding = util::padding_needed_for(rounding_input, size_align); + + // Write `trailing_bytes = elems * self.elem_size` over mathematical + // integers. Subtracting the trailing-slice end from the complete + // object size cancels `trailing_bytes`: + // + // size_base + round_up(size_phase + trailing_bytes, size_align) + // - (offset + trailing_bytes) + // = size_base + size_phase - offset + rounding_padding. + // + // Wrapping computes this difference modulo `usize::MAX + 1`, even + // if an intermediate value or the difference is unrepresentable. + // If the object size fits in `usize` and contains the trailing slice, + // the difference is in `0..=usize::MAX`; its residue is then the + // exact number of padding bytes after the trailing slice. + self.size_base + .wrapping_add(size_phase) + .wrapping_sub(self.offset) + .wrapping_add(rounding_padding) + } + + /// Computes the size described by this layout for `elems` trailing slice + /// elements. + /// + /// Returns `None` if the element-byte count or complete rounded size + /// cannot be represented as a `usize`. + #[inline(always)] + #[cfg_attr(kani, kani::ensures(|&size| { + size == elems.checked_mul(self.elem_size) + .and_then(|bytes| proofs::slice_dst_size_for_trailing_bytes(self, bytes)) + }))] + pub(crate) const fn size_for_elems(self, elems: usize) -> Option { + #[cfg(kani)] + #[kani::proof_for_contract(TrailingSliceLayout::size_for_elems)] + #[kani::solver(kissat)] + fn proof() { + kani::any::().size_for_elems(kani::any()); + } + + // Let `size_align` and `size_phase` denote the components stored in + // `size_rounding_align_and_phase`. The normalized layout formula, + // evaluated over the nonnegative integers, is: + // + // object_size(trailing_bytes) + // = self.size_base + // + round_up(size_phase + trailing_bytes, size_align) + // + // Here, `round_up` gives the least multiple of `size_align` greater + // than or equal to its input. Thus `object_size` is nondecreasing. + // By the capacity helper's contract, `max_trailing_bytes` is the + // largest byte count for which `object_size` fits in a `usize`. + // Monotonicity then gives the equivalence: + // + // object_size(trailing_bytes) <= usize::MAX + // iff trailing_bytes <= max_trailing_bytes. + // + // If the helper returns `None`, even `object_size(0)` exceeds the + // limit, so no element count can produce a representable size. + let max_trailing_bytes = match self.max_trailing_bytes(usize::MAX) { + Some(bytes) => bytes, + None => return None, + }; + + // Each of the `elems` elements contributes `self.elem_size` bytes, + // so substitution into the formula requires their product. Checked + // multiplication yields that exact product, including zero for + // zero-sized elements. If it overflows, the complete size cannot fit + // either: `object_size(trailing_bytes) >= trailing_bytes` because + // the base, phase, and rounding increment are all nonnegative. + let trailing_size = match self.elem_size.checked_mul(elems) { + Some(bytes) => bytes, + None => return None, + }; + + // The equivalence above rejects exactly the remaining cases whose + // complete size exceeds `usize::MAX`. After this check we have + // `object_size(trailing_size) <= usize::MAX`. + if trailing_size > max_trailing_bytes { + return None; + } + + // The trailing slice starts at `self.offset` and occupies + // `trailing_size` bytes. Thus `trailing_end` is its end offset modulo + // `usize::MAX + 1`. This sum need not fit: a raw layout need not + // describe a Rust type, so its trailing slice need not lie within + // the complete object size bounded above. + let trailing_end = self.offset.wrapping_add(trailing_size); + + // `padding_for_elems` returns `object_size(trailing_size)` minus the + // trailing-slice end, modulo `usize::MAX + 1`. Adding the end back + // therefore gives `object_size(trailing_size)` modulo the same + // modulus. The bound above establishes that this size fits in + // `usize`, so its residue is the exact size specified by the layout. + let size = trailing_end.wrapping_add(self.padding_for_elems(elems)); + Some(size) + } + + /// Returns `true` only when `self` and `other` describe the same size for + /// every trailing slice length. + /// + /// This recognizes sufficient conditions for equality. A `false` result + /// also includes equivalent size sequences that this test cannot recognize. + #[inline(always)] + #[cfg_attr(kani, kani::ensures(|&same| { + let (self_align, self_phase) = self.size_rounding_align_and_phase.components(); + let (other_align, other_phase) = other.size_rounding_align_and_phase.components(); + let initial_size = proofs::slice_dst_size_for_trailing_bytes(self, 0); + same == (self.elem_size == other.elem_size + && initial_size.is_some() + && initial_size == proofs::slice_dst_size_for_trailing_bytes(other, 0) + && ((self.elem_size % self_align.get() == 0 + && self.elem_size % other_align.get() == 0) + || (self_align == other_align && self_phase == other_phase))) + }))] + pub(crate) const fn has_same_size_sequence(self, other: Self) -> bool { + #[cfg(kani)] + #[kani::proof_for_contract(TrailingSliceLayout::has_same_size_sequence)] + #[kani::solver(kissat)] + fn proof() { + kani::any::().has_same_size_sequence(kani::any()); + } + + // For either layout, interpret its normalized size formula over the + // nonnegative integers: + // + // object_size(elems) + // = size_base + // + round_up(size_phase + elems * elem_size, size_align). + // + // In two cases below, we establish equality of these mathematical sizes + // for every `elems`. Equal sizes also have the same `usize` + // representability bound. + // + // Both cases require a common byte contribution at each element count. + // Equal element sizes give that shared term, `elems * elem_size`. + if self.elem_size != other.elem_size { + return false; + } + + // At `elems == 0`, the element contribution vanishes. Require equal, + // representable initial sizes; each case below then shows that + // increasing `elems` adds the same amount to both initial sizes. + match (self.size_for_elems(0), other.size_for_elems(0)) { + (Some(self_size), Some(other_size)) if self_size == other_size => {} + _ => return false, + } + + let (self_align, self_phase) = self.size_rounding_align_and_phase.components(); + let (other_align, other_phase) = other.size_rounding_align_and_phase.components(); + let self_align = self_align.get(); + let other_align = other_align.get(); + + // Case 1: The element size is a multiple of both alignments. + // + // For two power-of-two alignments, the larger is divisible by the + // smaller. Thus `max_align` is their least common multiple, and a byte + // count is divisible by both alignments exactly when it is divisible by + // `max_align`. + let max_align = if self_align > other_align { self_align } else { other_align }; + + // If the element size is divisible by both alignments, so is `elems * + // elem_size`. Adding an alignment multiple commutes with rounding, + // giving the following identity for each layout: + // + // round_up(size_phase + elems * elem_size, size_align) + // = round_up(size_phase, size_align) + elems * elem_size. + // + // Consequently, `object_size(elems) = object_size(0) + elems * + // elem_size`. The initial sizes and element sizes already agree, so the + // complete size sequences agree. This also covers zero-sized elements, + // whose contribution is zero for every element count. + #[allow(clippy::arithmetic_side_effects)] + if self.elem_size % max_align == 0 { + return true; + } + + // Case 2: The rounding alignments and phases match. + // + // Require a common rounding alignment before comparing phases. If the + // alignments differ, neither case establishes equality. + if self_align != other_align { + return false; + } + + // If the phases also agree, both `round_up` operations have the same + // input and alignment at every element count. In particular, their + // rounded terms agree at zero. Subtracting that common term from the + // equal initial sizes proves `self.size_base == other.size_base`. Both + // the fixed and rounded contributions therefore agree for every + // `elems`, which establishes equality of the complete size formulas. + self_phase == other_phase + } + + /// Advances the size formula by `bytes` while replacing its element size. + /// + /// Let `(size_align, size_phase)` be decoded from + /// `self.size_rounding_align_and_phase`. For a trailing slice length + /// `elems`, the returned layout describes: + /// + /// ```text + /// self.size_base + /// + round_up(size_phase + bytes + elems * elem_size, size_align) + /// ``` + /// + /// Here, `elem_size` is the replacement element size passed to this + /// method. The returned layout preserves the physical slice offset + /// `self.offset` and the rounding alignment; advancing changes the size + /// calculation. + /// + /// Returns `None` if advancing cannot produce representable normalized + /// components. Even when those components fit, evaluating the rounded + /// object size may overflow for some or all trailing slice lengths. + #[inline(always)] + #[cfg_attr(kani, kani::ensures(|result| { + let expected_offset = self.size_offset().checked_add(bytes); + match result { + Some(advanced) => { + let align = self.size_rounding_align_and_phase.align(); + expected_offset == Some(advanced.size_offset()) + && advanced.offset == self.offset + && advanced.elem_size == elem_size + && advanced.size_rounding_align_and_phase.align() == align + && (advanced.size_base & (align.get() - 1)) == (self.size_base & (align.get() - 1)) + } + None => expected_offset.is_none(), + } + }))] + const fn advance(self, bytes: usize, elem_size: usize) -> Option { + #[cfg(kani)] + #[kani::proof_for_contract(TrailingSliceLayout::advance)] + #[kani::solver(kissat)] + fn proof() { + kani::any::().advance(kani::any(), kani::any()); + } + + let (size_align, size_phase) = self.size_rounding_align_and_phase.components(); + + // Advancing replaces the phase inside `round_up` with `size_phase + + // bytes`. To normalize this sum, separate it into whole alignment units + // and a remainder. The whole units can move outside `round_up` into the + // base; the remainder becomes the new phase. + // + // Since `size_align` is a nonzero power of two, `align_mask` selects + // exactly the bits representing a remainder modulo `size_align`. + // Nonzero `size_align` also makes this subtraction representable. + #[allow(clippy::arithmetic_side_effects)] + let align_mask = size_align.get() - 1; + + // The base can increase by at most `usize::MAX - self.size_base`. Since + // only whole alignment units move into the base, the largest + // transferable contribution is: + // + // whole_capacity + // = round_down(usize::MAX - self.size_base, size_align). + // + // A remainder up to `align_mask` stays in the phase and consumes no + // base capacity. Thus the largest advanced phase that can be split into + // representable normalized components is `phase_capacity = + // whole_capacity + align_mask`. Setting the low bits of the remaining + // base capacity computes exactly that sum. Its two terms have disjoint + // set bits, so it cannot overflow. Any larger sum would transfer more + // whole units than the base can hold. + #[allow(clippy::arithmetic_side_effects)] + let phase_capacity = (usize::MAX - self.size_base) | align_mask; + + // The existing phase already occupies `size_phase` bytes of this + // capacity. Subtracting it gives the greatest permitted increment: + // + // bytes <= max_advance + // iff size_phase + bytes <= phase_capacity. + // + // Since `phase_capacity >= align_mask >= size_phase`, the subtraction + // cannot underflow. + #[allow(clippy::arithmetic_side_effects)] + let max_advance = phase_capacity - size_phase; + if bytes > max_advance { + return None; + } + + // This is the fixed term inside rounding after the requested + // advancement, before normalization. The check above proves `size_phase + // + bytes <= phase_capacity <= usize::MAX`, so the addition is + // representable. + #[allow(clippy::arithmetic_side_effects)] + let advanced_phase = size_phase + bytes; + + // Taking the remainder retains precisely the part that cannot move + // outside rounding as whole alignment units. In particular, `0 <= + // normalized_phase < size_align`, as required for the new rounding + // parameters. + let normalized_phase = advanced_phase & align_mask; + + // Rounding down removes precisely the remainder retained in the + // phase, so `whole_bytes = advanced_phase - normalized_phase`. + let whole_bytes = + util::round_down_to_next_multiple_of_alignment(advanced_phase, size_align); + + // Because `advanced_phase <= phase_capacity`, we have + // `whole_bytes <= whole_capacity <= usize::MAX - self.size_base`. + // Adding this transferred contribution to the base therefore fits. + #[allow(clippy::arithmetic_side_effects)] + let size_base = self.size_base + whole_bytes; + + // To verify the returned formula, let `trailing_size = elems * + // elem_size` and evaluate the following over the nonnegative integers. + // Since `whole_bytes` is a multiple of `size_align`, it can move back + // inside `round_up` without changing the result: + // + // size_base + round_up(normalized_phase + trailing_size, size_align) + // = self.size_base + // + round_up(whole_bytes + normalized_phase + trailing_size, size_align) + // = self.size_base + // + round_up(size_phase + bytes + trailing_size, size_align). + // + // This is the requested advanced formula. The replacement element size + // supplies its new coefficient of `elems`; `offset` retains the + // caller's field-placement description. + Some(Self { + offset: self.offset, + elem_size, + size_base, + size_rounding_align_and_phase: RoundingAlignAndPhase::new(size_align, normalized_phase), + }) + } +} + +impl SizeInfo { + /// Attempts to create a `SizeInfo` from `Self` in which `elem_size` is a + /// `NonZeroUsize`. If `elem_size` is 0, returns `None`. + #[allow(unused)] + #[cfg_attr(not(zerocopy_inline_always), inline)] + #[cfg_attr(zerocopy_inline_always, inline(always))] + #[cfg_attr(kani, kani::ensures(|result| match (*self, *result) { + (SizeInfo::Sized { size }, Some(SizeInfo::Sized { size: converted })) => { + size == converted + } + (SizeInfo::SliceDst(original), Some(SizeInfo::SliceDst(converted))) => { + converted.elem_size.get() == original.elem_size + && converted.offset == original.offset + && converted.size_base == original.size_base + && converted.size_rounding_align_and_phase == original.size_rounding_align_and_phase + } + (SizeInfo::SliceDst(original), None) => original.elem_size == 0, + _ => false, + }))] + const fn try_to_nonzero_elem_size(&self) -> Option> { + #[cfg(kani)] + #[kani::proof_for_contract(SizeInfo::try_to_nonzero_elem_size)] + #[kani::solver(kissat)] + fn proof() { + kani::any::().try_to_nonzero_elem_size(); + } + + Some(match *self { + SizeInfo::Sized { size } => SizeInfo::Sized { size }, + SizeInfo::SliceDst(TrailingSliceLayout { + offset, + elem_size, + size_base, + size_rounding_align_and_phase, + }) => { + if let Some(elem_size) = NonZeroUsize::new(elem_size) { + SizeInfo::SliceDst(TrailingSliceLayout { + offset, + elem_size, + size_base, + size_rounding_align_and_phase, + }) + } else { + return None; + } + } + }) + } +} + +/// Returns the largest number of `elem_size`-byte elements which fit in +/// `bytes`, along with the number of bytes those elements occupy. +#[cfg_attr( + kani, + kani::ensures(|&(elems, used)| { + (match elems.checked_mul(elem_size.get()) { + Some(product) => { + product == used && used <= bytes && bytes - used < elem_size.get() + } + None => false, + }) && used + .checked_add(elem_size.get()) + .map(|next_used| next_used > bytes) + .unwrap_or(true) + }) +)] +#[inline(always)] +const fn max_elems_for_bytes(bytes: usize, elem_size: NonZeroUsize) -> (usize, usize) { + #[cfg(kani)] + #[kani::proof_for_contract(max_elems_for_bytes)] + #[kani::solver(kissat)] + fn proof() { + max_elems_for_bytes(kani::any(), kani::any()); + } + + #[allow(clippy::arithmetic_side_effects)] + let elems = bytes / elem_size.get(); + let used = match elems.checked_mul(elem_size.get()) { + Some(used) => used, + None => const_unreachable!(), + }; + (elems, used) +} + +#[doc(hidden)] +#[derive(Copy, Clone)] +#[cfg_attr(test, derive(Debug))] +#[allow(missing_debug_implementations)] +pub enum CastType { + Prefix, + Suffix, +} + +#[cfg_attr(test, derive(Debug))] +#[cfg_attr(kani, derive(kani::Arbitrary))] +pub(crate) enum MetadataCastError { + Alignment, + Size, +} + +impl DstLayout { + /// The minimum possible alignment of a type. + const MIN_ALIGN: NonZeroUsize = match NonZeroUsize::new(1) { + Some(min_align) => min_align, + None => const_unreachable!(), + }; + + /// The maximum theoretic possible alignment of a type. + /// + /// For compatibility with future Rust versions, this is defined as the + /// maximum power-of-two that fits into a `usize`. See also + /// [`DstLayout::CURRENT_MAX_ALIGN`]. + pub(crate) const THEORETICAL_MAX_ALIGN: NonZeroUsize = + match NonZeroUsize::new(1 << (POINTER_WIDTH_BITS - 1)) { + Some(max_align) => max_align, + None => const_unreachable!(), + }; + + /// The current, documented max alignment of a type \[1\]. + /// + /// \[1\] Per : + /// + /// The alignment value must be a power of two from 1 up to + /// 229. + #[cfg(not(kani))] + #[cfg(not(target_pointer_width = "16"))] + pub(crate) const CURRENT_MAX_ALIGN: NonZeroUsize = match NonZeroUsize::new(1 << 29) { + Some(max_align) => max_align, + None => const_unreachable!(), + }; + + #[cfg(not(kani))] + #[cfg(target_pointer_width = "16")] + pub(crate) const CURRENT_MAX_ALIGN: NonZeroUsize = match NonZeroUsize::new(1 << 15) { + Some(max_align) => max_align, + None => const_unreachable!(), + }; + + /// The maximum size of an allocation \[1\]. + /// + /// \[1\] Per : + /// + /// For any allocation with base `address`, `size`, and a set of `addresses`, + /// the following are guaranteed: [..] + /// + /// - `size <= isize::MAX` + /// + #[allow(clippy::as_conversions)] + pub(crate) const MAX_SIZE: usize = isize::MAX as usize; + + /// Assumes that this layout lacks static shallow padding. + /// + /// # Panics + /// + /// This method does not panic. + /// + /// # Safety + /// + /// If `self` describes the size and alignment of type that lacks static + /// shallow padding, unsafe code may assume that the result of this method + /// accurately reflects the size, alignment, and lack of static shallow + /// padding of that type. + #[cfg_attr(kani, kani::ensures(|result| { + result.align == self.align && result.size_info == self.size_info + && result.statically_shallow_unpadded + }))] + const fn assume_shallow_unpadded(self) -> Self { + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::assume_shallow_unpadded)] + #[kani::solver(kissat)] + fn proof() { + kani::any::().assume_shallow_unpadded(); + } + + Self { statically_shallow_unpadded: true, ..self } + } + + /// Constructs a `DstLayout` for a zero-sized type with `repr_align` + /// alignment (or 1). If `repr_align` is provided, then it must be a power + /// of two. + /// + /// # Panics + /// + /// This function panics if the supplied `repr_align` is not a power of two. + /// + /// # Safety + /// + /// Unsafe code may assume that the contract of this function is satisfied. + #[doc(hidden)] + #[must_use] + #[inline] + #[cfg_attr(kani, kani::requires(repr_align.map_or(true, |align| align.is_power_of_two())))] + #[cfg_attr(kani, kani::ensures(|result| { + result.align == repr_align.unwrap_or(Self::MIN_ALIGN) + && result.size_info == SizeInfo::Sized { size: 0 } + && result.statically_shallow_unpadded + }))] + pub const fn new_zst(repr_align: Option) -> DstLayout { + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::new_zst)] + #[kani::solver(kissat)] + fn proof() { + let _ = DstLayout::new_zst(kani::any()); + } + + let align = match repr_align { + Some(align) => align, + None => Self::MIN_ALIGN, + }; + + const_assert!(align.get().is_power_of_two()); + + DstLayout { + align, + size_info: SizeInfo::Sized { size: 0 }, + statically_shallow_unpadded: true, + } + } + + /// Constructs a `DstLayout` which describes `T` and assumes `T` may contain + /// padding. + /// + /// # Safety + /// + /// Unsafe code may assume that `DstLayout` is the correct layout for `T`. + #[doc(hidden)] + #[must_use] + #[inline] + #[cfg_attr(kani, kani::ensures(|result| { + result.align.get() == mem::align_of::() + && result.size_info == SizeInfo::Sized { size: mem::size_of::() } + && result.statically_shallow_unpadded == false + }))] + pub const fn for_type() -> DstLayout { + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::for_type::<()>)] + fn proof_zst() { + let _ = DstLayout::for_type::<()>(); + } + + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::for_type::<(u8, u64)>)] + fn proof_padded() { + let _ = DstLayout::for_type::<(u8, u64)>(); + } + + // SAFETY: `align` is correct by construction. `T: Sized`, and so it is + // sound to initialize `size_info` to `SizeInfo::Sized { size }`; the + // `size` field is also correct by construction. `unpadded` can safely + // default to `false`. + DstLayout { + align: match NonZeroUsize::new(mem::align_of::()) { + Some(align) => align, + None => const_unreachable!(), + }, + size_info: SizeInfo::Sized { size: mem::size_of::() }, + statically_shallow_unpadded: false, + } + } + + /// Produces `T`'s size and alignment with static shallow padding assumed + /// absent. + /// + /// This method records the assumption without checking `T` for padding. + /// It can be used to ignore padding within a field when computing padding + /// introduced by its enclosing struct. + /// + /// # Safety + /// + /// Unsafe code may rely on the result's size and alignment being correct + /// for `T`. Relying on its recorded absence of static shallow padding + /// requires an independent justification that `T` lacks such padding. + #[doc(hidden)] + #[must_use] + #[inline] + #[cfg_attr(kani, kani::ensures(|result| { + result.align.get() == mem::align_of::() + && result.size_info == SizeInfo::Sized { size: mem::size_of::() } + && result.statically_shallow_unpadded == true + }))] + pub const fn for_unpadded_type() -> DstLayout { + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::for_unpadded_type::<()>)] + fn proof_zst() { + let _ = DstLayout::for_unpadded_type::<()>(); + } + + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::for_unpadded_type::<(u8, u64)>)] + fn proof_padded() { + let _ = DstLayout::for_unpadded_type::<(u8, u64)>(); + } + + Self::for_type::().assume_shallow_unpadded() + } + + /// Constructs a `DstLayout` which describes `[T]`. + /// + /// # Safety + /// + /// Unsafe code may assume that `DstLayout` is the correct layout for `[T]`. + #[cfg_attr(kani, kani::ensures(|result| { + result.align.get() == mem::align_of::() && result.statically_shallow_unpadded + && matches!(result.size_info, SizeInfo::SliceDst(trailing) + if trailing.offset == 0 && trailing.size_base == 0 + && trailing.elem_size == mem::size_of::() + && trailing.size_rounding_align_and_phase.components() == (result.align, 0)) + }))] + pub(crate) const fn for_slice() -> DstLayout { + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::for_slice::<()>)] + fn proof_zst() { + let _ = DstLayout::for_slice::<()>(); + } + + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::for_slice::<(u8, u64)>)] + fn proof_padded() { + let _ = DstLayout::for_slice::<(u8, u64)>(); + } + + let align = match NonZeroUsize::new(mem::align_of::()) { + Some(align) => align, + None => const_unreachable!(), + }; + + // SAFETY: The alignment of a slice is equal to the alignment of its + // element type, and so `align` is initialized correctly. + // + // Since this is just a slice type, there is no offset between the + // beginning of the type and the beginning of the slice, so it is + // correct to set `offset: 0`. The `elem_size` is correct by + // construction. Since `[T]` is a (degenerate case of a) slice DST, it + // is correct to initialize `size_info` to `SizeInfo::SliceDst`. + DstLayout { + align, + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + offset: 0, + elem_size: mem::size_of::(), + size_base: 0, + size_rounding_align_and_phase: RoundingAlignAndPhase::new(align, 0), + }), + statically_shallow_unpadded: true, + } + } + + /// Constructs a complete `DstLayout` reflecting a `repr(C)` struct with the + /// given alignment modifiers and fields. + /// + /// This method cannot be used to match the layout of a record with the + /// default representation, as that representation is mostly unspecified. + /// + /// # Safety + /// + /// For any definition of a `repr(C)` struct, if this method is invoked with + /// alignment modifiers and fields corresponding to that definition, the + /// resulting `DstLayout` will correctly encode the layout of that struct. + /// + /// This guarantee uses one explicit compatibility premise for generic + /// indirection: while Rust accepts an instantiation which transitively + /// places a `repr(align)` type inside a `repr(packed)` type, that + /// instantiation follows the otherwise-applicable `repr(C)`, + /// `repr(packed)`, and inner-type layout rules. The Reference prohibits the + /// direct form, and Rust's continued acceptance and layout behavior are + /// outside the algebra proved by this implementation. Re-audit if the + /// compiler rejects such an instantiation or assigns it a different + /// layout. + /// + /// We make no guarantees to the behavior of this method when it is invoked + /// with arguments that cannot correspond to a valid `repr(C)` struct. + #[must_use] + #[inline] + #[cfg_attr(kani, kani::requires(proofs::repr_c_layout(repr_align, repr_packed, fields).is_some()))] + #[cfg_attr(kani, kani::ensures(|&result| { + Some(result) == proofs::repr_c_layout(repr_align, repr_packed, fields) + }))] + pub const fn for_repr_c_struct( + repr_align: Option, + repr_packed: Option, + fields: &[DstLayout], + ) -> DstLayout { + // This harness covers up to three fields, including an optional final + // DST. Arbitrary-length composition is not established by this bound. + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::for_repr_c_struct)] + #[kani::solver(kissat)] + #[kani::unwind(4)] + fn proof() { + let fields: [DstLayout; 3] = kani::any(); + let len: usize = kani::any(); + kani::assume(len <= fields.len()); + let _ = DstLayout::for_repr_c_struct(kani::any(), kani::any(), &fields[..len]); + } + + let mut result = DstLayout::new_zst(repr_align); + + let mut i = 0; + #[allow(clippy::arithmetic_side_effects)] + while i < fields.len() { + #[allow(clippy::indexing_slicing)] + let field = fields[i]; + result = result.extend(field, repr_packed); + i += 1; + } + + result = result.pad_to_align(); + + // SAFETY: `result` accurately describes the result of a `repr(C)` + // struct with `repr_align` or `repr_packed` alignment modifications and + // the given `fields`. The `result` is constructed using a sequence of + // invocations of `DstLayout::{new_zst,extend,pad_to_align}`. The + // documentation of these items vows that invocations in this manner + // will accurately describe a type, so long as: + // + // - that type is `repr(C)`, + // - its fields are enumerated in the order they appear, + // - the presence of `repr_align` and `repr_packed` are correctly accounted for. + // + // We respect all three of these preconditions above. + result + } + + /// Like `Layout::extend`, this creates a layout that describes a record + /// whose layout consists of `self` followed by `next` that includes the + /// necessary inter-field padding, but not any trailing padding. + /// + /// In order to match the layout of a `#[repr(C)]` struct, this method + /// should be invoked for each field in declaration order. To add trailing + /// padding, call `DstLayout::pad_to_align` after extending the layout for + /// all fields. If `self` corresponds to a type marked with + /// `repr(packed(N))`, then `repr_packed` should be set to `Some(N)`, + /// otherwise `None`. + /// + /// This method cannot be used to match the layout of a record with the + /// default representation, as that representation is mostly unspecified. + /// + /// # Safety + /// + /// If a (potentially hypothetical) valid `repr(C)` Rust type begins with + /// fields whose layout are `self`, and those fields are immediately + /// followed by a field whose layout is `field`, then unsafe code may rely + /// on `self.extend(field, repr_packed)` producing a layout that correctly + /// encompasses those two components. + /// + /// We make no guarantees to the behavior of this method if these fragments + /// cannot appear in a valid Rust type (e.g., the concatenation of the + /// layouts would lead to a size larger than `isize::MAX`). + #[doc(hidden)] + #[must_use] + #[inline] + #[cfg_attr(kani, kani::requires(proofs::extended_layout(self, field, repr_packed).is_some()))] + #[cfg_attr(kani, kani::ensures(|&result| { + Some(result) == proofs::extended_layout(self, field, repr_packed) + }))] + pub const fn extend(self, field: DstLayout, repr_packed: Option) -> Self { + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::extend)] + #[kani::solver(kissat)] + fn proof() { + let _ = kani::any::().extend(kani::any(), kani::any()); + } + + use util::{max, min, padding_needed_for}; + + // If `repr_packed` is `None`, there are no alignment constraints, and + // the value can be defaulted to `THEORETICAL_MAX_ALIGN`. + let max_align = match repr_packed { + Some(max_align) => max_align, + None => Self::THEORETICAL_MAX_ALIGN, + }; + + const_assert!(max_align.get().is_power_of_two()); + + // We use Kani to prove that this method is robust to future increases + // in Rust's maximum allowed alignment. However, if such a change ever + // actually occurs, we'd like to be notified via assertion failures. + #[cfg(not(kani))] + { + const_debug_assert!(self.align.get() <= DstLayout::CURRENT_MAX_ALIGN.get()); + const_debug_assert!(field.align.get() <= DstLayout::CURRENT_MAX_ALIGN.get()); + if let Some(repr_packed) = repr_packed { + const_debug_assert!(repr_packed.get() <= DstLayout::CURRENT_MAX_ALIGN.get()); + } + } + + // The field's alignment is clamped by `repr_packed` (i.e., the + // `repr(packed(N))` attribute, if any) [1]. + // + // [1] Per https://doc.rust-lang.org/reference/type-layout.html#the-alignment-modifiers: + // + // The alignments of each field, for the purpose of positioning + // fields, is the smaller of the specified alignment and the alignment + // of the field's type. + let field_align = min(field.align, max_align); + + // The struct's alignment is the maximum of its previous alignment and + // `field_align`. + let align = max(self.align, field_align); + + let (interfield_padding, size_info) = match self.size_info { + // If the layout is already a DST, we panic; DSTs cannot be extended + // with additional fields. + SizeInfo::SliceDst(..) => const_panic!("Cannot extend a DST with additional fields."), + + SizeInfo::Sized { size: preceding_size } => { + // Compute the minimum amount of inter-field padding needed to + // satisfy the field's alignment, and offset of the trailing + // field. [1] + // + // [1] Per https://doc.rust-lang.org/reference/type-layout.html#the-alignment-modifiers: + // + // Inter-field padding is guaranteed to be the minimum + // required in order to satisfy each field's (possibly + // altered) alignment. + let padding = padding_needed_for(preceding_size, field_align); + + // This will not panic (and is proven to not panic, with Kani) + // if the layout components can correspond to a leading layout + // fragment of a valid Rust type, but may panic otherwise (e.g., + // combining or aligning the components would create a size + // exceeding `isize::MAX`). + let offset = match preceding_size.checked_add(padding) { + Some(offset) => offset, + None => const_panic!("Adding padding to `self`'s size overflows `usize`."), + }; + + ( + padding, + match field.size_info { + SizeInfo::Sized { size: field_size } => { + // If the trailing field is sized, the resulting layout + // will be sized. Its size will be the sum of the + // preceding layout, the size of the new field, and the + // size of inter-field padding between the two. + // + // This will not panic (and is proven with Kani to not + // panic) if the layout components can correspond to a + // leading layout fragment of a valid Rust type, but may + // panic otherwise (e.g., combining or aligning the + // components would create a size exceeding + // `usize::MAX`). + let size = match offset.checked_add(field_size) { + Some(size) => size, + None => const_panic!("`field` cannot be appended without the total size overflowing `usize`"), + }; + SizeInfo::Sized { size } + } + SizeInfo::SliceDst(TrailingSliceLayout { + offset: trailing_offset, + elem_size, + size_base, + size_rounding_align_and_phase, + }) => { + // If the trailing field is dynamically sized, so too + // will the resulting layout. The offset of the trailing + // slice component is the sum of the offset of the + // trailing field and the trailing slice offset within + // that field. + // + // This will not panic (and is proven with Kani to not + // panic) if the layout components can correspond to a + // leading layout fragment of a valid Rust type, but may + // panic otherwise (e.g., combining or aligning the + // components would create a size exceeding + // `usize::MAX`). + let trailing_offset = match offset.checked_add(trailing_offset) { + Some(offset) => offset, + None => const_panic!("`field` cannot be appended without the total size overflowing `usize`"), + }; + + // Before normalization, the composite's size is: + // + // offset + size_base + // + round_up(size_phase + elems * elem_size, size_align) + // + // `offset` does not participate in the rounding, + // so it can be added directly to the normalized + // base. + let size_base = match offset.checked_add(size_base) { + Some(base) => base, + None => const_panic!("`field` cannot be appended without the total size overflowing `usize`"), + }; + + SizeInfo::SliceDst(TrailingSliceLayout { + offset: trailing_offset, + elem_size, + size_base, + size_rounding_align_and_phase, + }) + } + }, + ) + } + }; + + let statically_shallow_unpadded = self.statically_shallow_unpadded + && field.statically_shallow_unpadded + && interfield_padding == 0; + + DstLayout { align, size_info, statically_shallow_unpadded } + } + + /// Like `Layout::pad_to_align`, this routine rounds the size of this layout + /// up to the nearest multiple of this type's alignment. For DST layouts, + /// this updates the runtime size formula to account for trailing padding. + /// + /// In order to match the layout of a `#[repr(C)]` struct, this method + /// should be invoked after the invocations of [`DstLayout::extend`]. + /// + /// This method cannot be used to match the layout of a record with the + /// default representation, as that representation is mostly unspecified. + /// + /// # Safety + /// + /// If a (potentially hypothetical) valid `repr(C)` type begins with fields + /// whose layout are `self` followed only by zero or more bytes of trailing + /// padding (not included in `self`), then unsafe code may rely on + /// `self.pad_to_align()` producing a layout that correctly + /// encapsulates the layout of that type. + /// + /// We make no guarantees to the behavior of this method if `self` cannot + /// appear in a valid Rust type (e.g., because the addition of trailing + /// padding would lead to a size larger than `isize::MAX`). + #[doc(hidden)] + #[must_use] + #[inline] + #[cfg_attr(kani, kani::requires(proofs::padded_layout(self).is_some()))] + #[cfg_attr(kani, kani::ensures(|&result| Some(result) == proofs::padded_layout(self)))] + pub const fn pad_to_align(self) -> Self { + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::pad_to_align)] + #[kani::solver(kissat)] + fn proof() { + let _ = kani::any::().pad_to_align(); + } + + use util::padding_needed_for; + + let (static_padding, size_info) = match self.size_info { + // For sized layouts, we add the minimum amount of trailing padding + // needed to satisfy alignment. + SizeInfo::Sized { size: unpadded_size } => { + let padding = padding_needed_for(unpadded_size, self.align); + let size = match unpadded_size.checked_add(padding) { + Some(size) => size, + None => const_panic!("Adding padding caused size to overflow `usize`."), + }; + (padding, SizeInfo::Sized { size }) + } + // For DST layouts, trailing padding depends on the length of the + // trailing DST and is computed at runtime. Normalize the composed + // rounding operation back into the representation documented on + // `TrailingSliceLayout`. + SizeInfo::SliceDst(mut trailing) => { + let (size_align, size_phase) = trailing.size_rounding_align_and_phase.components(); + if size_align.get() < self.align.get() { + // For a trailing-slice byte count `trailing_size`, the + // input layout describes: + // + // trailing.size_base + // + round_up(size_phase + trailing_size, size_align) + // + // Here, `size_align < self.align`. Both alignments are + // powers of two, so rounding the complete input size to + // `self.align` is equivalent to: + // + // rounded_base = round_up(trailing.size_base, size_align) + // fixed_contribution = rounded_base + size_phase + // new_phase = fixed_contribution % self.align + // new_base = fixed_contribution - new_phase + // new_base + round_up(new_phase + trailing_size, self.align) + // + // `prove_dst_layout_pad_to_align_dst_outer_alignment_transition` + // checks the exact field transition and preserved + // invariants. Its `_formula` companion proves this + // identity for every representable trailing byte count. + let base_padding = padding_needed_for(trailing.size_base, size_align); + let rounded_base = match trailing.size_base.checked_add(base_padding) { + Some(base) => base, + None => const_panic!("Adding padding caused size to overflow `usize`."), + }; + let fixed_contribution = match rounded_base.checked_add(size_phase) { + Some(bytes) => bytes, + None => const_panic!("Adding padding caused size to overflow `usize`."), + }; + #[allow(clippy::arithmetic_side_effects)] + let phase = fixed_contribution & (self.align.get() - 1); + // Rounding down removes `phase`, the fixed contribution + // modulo `self.align`, leaving the new base. + let size_base = util::round_down_to_next_multiple_of_alignment( + fixed_contribution, + self.align, + ); + trailing.size_base = size_base; + trailing.size_rounding_align_and_phase = + RoundingAlignAndPhase::new(self.align, phase); + } else { + // `size_align` is a multiple of `self.align`, so + // the inner rounded size is already aligned to + // `self.align`. Only `size_base` needs to be rounded. + // `prove_dst_layout_pad_to_align_dst_inner_rounding_transition` + // checks the exact field transition and preserved + // invariants. Its `_formula` companion proves size + // equivalence for every representable trailing byte count. + let padding = padding_needed_for(trailing.size_base, self.align); + trailing.size_base = match trailing.size_base.checked_add(padding) { + Some(base) => base, + None => const_panic!("Adding padding caused size to overflow `usize`."), + }; + } + + (0, SizeInfo::SliceDst(trailing)) + } + }; + + let statically_shallow_unpadded = self.statically_shallow_unpadded && static_padding == 0; + + DstLayout { align: self.align, size_info, statically_shallow_unpadded } + } + + /// Produces `false` when this layout is marked as having no static shallow + /// padding, and `true` otherwise. + /// + /// A `true` result is conservative: it does not establish that the type + /// contains padding. For example, this method produces `true` for + /// `DstLayout::for_type::()`, since `for_type` does not record whether + /// static shallow padding is absent. + #[must_use] + #[inline(always)] + #[cfg_attr(kani, kani::ensures(|&padding| padding == !self.statically_shallow_unpadded))] + pub const fn requires_static_padding(self) -> bool { + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::requires_static_padding)] + #[kani::solver(kissat)] + fn proof() { + let _ = kani::any::().requires_static_padding(); + } + + !self.statically_shallow_unpadded + } + + /// Produces `false` only if every valid metadata describes an instance + /// which requires no dynamic trailing padding. + /// + /// A `true` result conservatively selects the dynamic-padding path; it does + /// not guarantee that any valid metadata actually requires padding. + #[must_use] + #[inline(always)] + #[cfg_attr(kani, kani::ensures(|&padding| match self.size_info { + SizeInfo::Sized { .. } => !padding, + SizeInfo::SliceDst(trailing) => padding == ( + proofs::slice_dst_size_for_trailing_bytes(trailing, 0) != Some(trailing.offset) + || trailing.elem_size % trailing.size_rounding_align_and_phase.align().get() != 0 + ), + }))] + pub const fn requires_dynamic_padding(self) -> bool { + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::requires_dynamic_padding)] + #[kani::solver(kissat)] + fn proof() { + let _ = kani::any::().requires_dynamic_padding(); + } + + match self.size_info { + SizeInfo::Sized { .. } => false, + SizeInfo::SliceDst(trailing_slice_layout) => { + // SAFETY: `proofs::prove_requires_dynamic_padding` formally + // proves the safety-critical direction: when this predicate + // returns false, no valid metadata requires dynamic padding. + match trailing_slice_layout.size_for_elems(0) { + Some(initial_size) => { + #[allow(clippy::arithmetic_side_effects)] + let has_static_stride = trailing_slice_layout.elem_size + % trailing_slice_layout.size_rounding_align_and_phase.align().get() + == 0; + initial_size != trailing_slice_layout.offset || !has_static_stride + } + None => true, + } + } + } + } + + /// Returns the largest trailing slice length whose object size is exactly + /// `size`. + /// + /// Returns `None` if there is no such length, if this describes a sized + /// type, or if the trailing slice element is zero-sized. + #[inline(always)] + #[cfg_attr(kani, kani::ensures(|&result| { + result == match self.size_info { + SizeInfo::SliceDst(trailing) if trailing.elem_size != 0 => { + proofs::max_trailing_bytes(trailing, size) + .map(|bytes| bytes / trailing.elem_size) + .filter(|&elems| trailing.size_for_elems(elems) == Some(size)) + } + _ => None, + } + }))] + const fn metadata_for_exact_size(&self, size: usize) -> Option { + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::metadata_for_exact_size)] + #[kani::solver(kissat)] + fn proof() { + kani::any::().metadata_for_exact_size(kani::any()); + } + + match self.size_info { + SizeInfo::Sized { .. } + | SizeInfo::SliceDst(TrailingSliceLayout { elem_size: 0, .. }) => None, + SizeInfo::SliceDst(_) => { + // Address zero is aligned to every supported alignment, and + // `0 + size` cannot overflow. The validator returns the + // largest metadata whose object fits in `size`; accepting it + // only when the resulting object consumes all `size` bytes + // turns that bounded query into an exact-size query. + match self.validate_cast_and_convert_metadata(0, size, CastType::Prefix) { + Ok((elems, object_size)) if object_size == size => Some(elems), + _ => None, + } + } + } + } + + /// Given a completed type layout, produces metadata and a split index for + /// a prefix or suffix cast that satisfies the type's size and alignment + /// requirements. + /// + /// `addr` and `bytes_len` describe the address and size of the source + /// memory region, and `cast_type` selects its prefix or suffix. + /// + /// If the cast is valid, `validate_cast_and_convert_metadata` returns + /// `Ok((elems, split_at))`. For a dynamically-sized type, `elems` is the + /// maximum number of trailing slice elements for which the cast is valid. + /// For sized types, `elems` is meaningless and should be ignored. + /// `split_at` is the index at which to split the memory region so that the + /// prefix (suffix) contains the result of the cast and the remaining + /// suffix (prefix) contains the leftover bytes. + /// + /// There are three conditions under which a cast can fail: + /// + /// - The smallest possible value for the type is larger than the provided + /// memory region + /// - A prefix cast is requested, and `addr` does not satisfy `self`'s + /// alignment requirement + /// - A suffix cast is requested, and `addr + bytes_len` does not satisfy + /// `self`'s alignment requirement (as a consequence, since all instances + /// of the type are a multiple of its alignment, no size for the type will + /// result in a starting address which is properly aligned) + /// + /// # Safety + /// + /// When `self` accurately describes a completed type layout, including + /// trailing padding, and `addr + bytes_len` fits in `usize`, callers may + /// rely on the following guarantees for `Ok((elems, split_at))`: + /// + /// - A pointer to the type (for dynamically sized types, this includes + /// `elems` as its pointer metadata) describes an object of size `size <= + /// bytes_len` + /// - If this is a prefix cast: + /// - `addr` satisfies `self`'s alignment + /// - `size == split_at` + /// - If this is a suffix cast: + /// - `split_at == bytes_len - size` + /// - `addr + split_at` satisfies `self`'s alignment + /// + /// These guarantees require the completed-layout premise; `DstLayout` + /// also permits unfinished layout fragments and arbitrary field values. + /// In particular, suffix alignment relies on every object size being a + /// multiple of the type's alignment. + /// + /// Note that this method does *not* ensure that a pointer constructed from + /// its return values will be a valid pointer. In particular, this method + /// does not reason about `isize` overflow, which is a requirement of many + /// Rust pointer APIs, and may at some point be determined to be a validity + /// invariant of pointer types themselves. This should never be a problem so + /// long as the arguments to this method are derived from a known-valid + /// pointer (e.g., one derived from a safe Rust reference), but it is + /// nonetheless the caller's responsibility to justify that pointer + /// arithmetic will not overflow based on a safety argument *other than* the + /// mere fact that this method returned successfully. + /// + /// # Panics + /// + /// `validate_cast_and_convert_metadata` will panic if `self` describes a + /// DST whose trailing slice element is zero-sized. + /// + /// If `addr + bytes_len` overflows `usize`, + /// `validate_cast_and_convert_metadata` may panic, or it may return + /// incorrect results. No guarantees are made about when + /// `validate_cast_and_convert_metadata` will panic. The caller should not + /// rely on `validate_cast_and_convert_metadata` panicking in any particular + /// condition, even if `debug_assertions` are enabled. + #[allow(unused)] + #[inline(always)] + #[cfg_attr(kani, kani::requires(addr.checked_add(bytes_len).is_some()))] + #[cfg_attr(kani, kani::requires(!matches!(self.size_info, + SizeInfo::SliceDst(TrailingSliceLayout { elem_size: 0, .. }))))] + #[cfg_attr(kani, kani::ensures(|result| { + let endpoint = match cast_type { + CastType::Prefix => addr, + CastType::Suffix => addr + bytes_len, + }; + if endpoint % self.align.get() != 0 { + return matches!(result, Err(MetadataCastError::Alignment)); + } + let candidate = match self.size_info { + SizeInfo::Sized { size } => { + if size <= bytes_len { Some((0, size)) } else { None } + } + SizeInfo::SliceDst(trailing) => { + proofs::max_trailing_bytes(trailing, bytes_len).and_then(|bytes| { + let elems = bytes / trailing.elem_size; + trailing.size_for_elems(elems).map(|size| (elems, size)) + }) + } + }; + match (result, candidate) { + (Ok((elems, split_at)), Some((expected_elems, size))) => { + *elems == expected_elems && *split_at == match cast_type { + CastType::Prefix => size, + CastType::Suffix => bytes_len - size, + } + } + (Err(MetadataCastError::Size), None) => true, + _ => false, + } + }))] + pub(crate) const fn validate_cast_and_convert_metadata( + &self, + addr: usize, + bytes_len: usize, + cast_type: CastType, + ) -> Result<(usize, usize), MetadataCastError> { + #[cfg(kani)] + #[kani::proof_for_contract(DstLayout::validate_cast_and_convert_metadata)] + #[kani::solver(kissat)] + fn proof() { + let cast_type = if kani::any() { CastType::Prefix } else { CastType::Suffix }; + let _ = kani::any::().validate_cast_and_convert_metadata( + kani::any(), + kani::any(), + cast_type, + ); + } + + // `debug_assert!`, but with `#[allow(clippy::arithmetic_side_effects)]`. + macro_rules! __const_debug_assert { + ($e:expr $(, $msg:expr)?) => { + const_debug_assert!({ + #[allow(clippy::arithmetic_side_effects)] + let e = $e; + e + } $(, $msg)?); + }; + } + + // Note that, in practice, `self` is always a compile-time constant. We + // do this check earlier than needed to ensure that we always panic as a + // result of bugs in the program (such as calling this function on an + // invalid type) instead of allowing this panic to be hidden if the cast + // would have failed anyway for runtime reasons (such as a too-small + // memory region). + // + // FIXME(#67): Once our MSRV is 1.65, use let-else: + // https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html#let-else-statements + let size_info = match self.size_info.try_to_nonzero_elem_size() { + Some(size_info) => size_info, + None => const_panic!("attempted to cast to slice type with zero-sized element"), + }; + + // Precondition + __const_debug_assert!( + addr.checked_add(bytes_len).is_some(), + "`addr` + `bytes_len` > usize::MAX" + ); + + // Alignment checks go in their own block to avoid introducing variables + // into the top-level scope. + { + // We check alignment for `addr` (for prefix casts) or `addr + + // bytes_len` (for suffix casts). For a prefix cast, the correctness + // of this check is trivial - `addr` is the address the object will + // live at. + // + // Under the documented completed-layout premise, all valid sizes + // for the type are multiples of its alignment. Thus, a + // validly-sized instance which lives at a validly-aligned address + // must also end at a validly-aligned address. Thus, if the end + // address for a suffix cast (`addr + bytes_len`) is not aligned, + // then no valid start address will be aligned either. + let offset = match cast_type { + CastType::Prefix => 0, + CastType::Suffix => bytes_len, + }; + + // Addition is guaranteed not to overflow because `offset <= + // bytes_len`, and `addr + bytes_len <= usize::MAX` is a + // precondition of this method. Modulus is guaranteed not to divide + // by zero because `self.align` is non-zero. + #[allow(clippy::arithmetic_side_effects)] + if (addr + offset) % self.align.get() != 0 { + return Err(MetadataCastError::Alignment); + } + } + + let (elems, self_bytes) = match size_info { + SizeInfo::Sized { size } => { + if size > bytes_len { + return Err(MetadataCastError::Size); + } + (0, size) + } + SizeInfo::SliceDst(trailing) => { + let max_slice_bytes = match trailing.max_trailing_bytes(bytes_len) { + Some(bytes) => bytes, + None => return Err(MetadataCastError::Size), + }; + // Calculate the number of elements that fit in + // `max_slice_bytes`. Any remaining input bytes stay outside + // the selected object unless the object's own rounding + // operation consumes them as padding. + // + // Guaranteed not to divide by zero: `elem_size` is non-zero. + let (elems, _) = max_elems_for_bytes(max_slice_bytes, trailing.elem_size); + // The remainder is `max_slice_bytes - elems * elem_size`. + // Writing it as a remainder exposes its bound to the + // optimizer: when `elem_size <= size_align`, rounding the + // remainder down to `size_align` always gives zero. + let size_align = trailing.size_rounding_align_and_phase.align(); + // `size_phase + max_slice_bytes == max_rounded_bytes`, an + // alignment multiple. The selected tail removes `unused_bytes` + // from this boundary; rounding back up removes only whole + // alignment units. Thus its normalized rounded size is: + // + // round_up(size_phase + elems * elem_size, size_align) + // = max_rounded_bytes - round_down(unused_bytes, size_align) + // + // `prove_slice_dst_validator_selected_size_fits` checks this + // identity and its intermediate bounds, using the relations + // in `prove_max_trailing_bytes_components`. The capacity is + // exact by `prove_max_trailing_bytes`; the element helper's + // contract establishes the largest whole-element count. + #[allow(clippy::arithmetic_side_effects)] + let unused_bytes = max_slice_bytes % trailing.elem_size.get(); + let unused_aligned_bytes = + util::round_down_to_next_multiple_of_alignment(unused_bytes, size_align); + #[allow(clippy::arithmetic_side_effects)] + let max_rounded_bytes = util::round_down_to_next_multiple_of_alignment( + bytes_len - trailing.size_base, + size_align, + ); + // `unused_aligned_bytes <= unused_bytes <= max_slice_bytes + // <= max_rounded_bytes`, and `size_base + max_rounded_bytes + // <= bytes_len`, so neither operation can overflow. + #[allow(clippy::arithmetic_side_effects)] + let self_bytes = trailing.size_base + (max_rounded_bytes - unused_aligned_bytes); + (elems, self_bytes) + } + }; + + __const_debug_assert!(self_bytes <= bytes_len); + + let split_at = match cast_type { + CastType::Prefix => self_bytes, + // Guaranteed not to underflow: + // - In the `Sized` branch, only returns `size` if `size <= + // bytes_len`. + // - In the `SliceDst` branch, calculates `self_bytes <= + // bytes_len`. + #[allow(clippy::arithmetic_side_effects)] + CastType::Suffix => bytes_len - self_bytes, + }; + + Ok((elems, split_at)) + } +} + +/// Computes the affine destination-metadata map. +/// +/// # Safety +/// +/// `base + metadata * multiple`, evaluated over the mathematical nonnegative +/// integers, must not exceed `usize::MAX`. +#[inline(always)] +#[allow(unstable_name_collisions)] +#[cfg_attr(kani, kani::requires(metadata.checked_mul(multiple) + .and_then(|scaled| base.checked_add(scaled)).is_some()))] +#[cfg_attr(kani, kani::ensures(|&result| { + metadata.checked_mul(multiple).and_then(|scaled| base.checked_add(scaled)) == Some(result) +}))] +unsafe fn add_scaled_metadata(base: usize, metadata: usize, multiple: usize) -> usize { + // The call-site safety argument establishes that both operations are + // representable for valid source metadata. This harness verifies the + // production implementation selected by Kani's modern rustc against + // checked arithmetic under precisely that precondition. The distributive + // natural-number argument which establishes the precondition is kept + // beside the unsafe pointer projection where it can be reviewed with the + // layout premises that it consumes. + #[cfg(kani)] + #[kani::proof_for_contract(add_scaled_metadata)] + #[kani::solver(kissat)] + fn proof() { + let base: usize = kani::any(); + let metadata: usize = kani::any(); + let multiple: usize = kani::any(); + let Some(scaled) = metadata.checked_mul(multiple) else { + kani::assume(false); + loop {} + }; + let Some(expected) = base.checked_add(scaled) else { + kani::assume(false); + loop {} + }; + + // SAFETY: The checked operations above returned `Some`, so the + // operation is representable in `usize`. The standard-library + // contracts say that this rules out undefined behavior for both + // unchecked operations [1][2]. + // + // [1] Per https://doc.rust-lang.org/1.93.1/std/primitive.usize.html#method.unchecked_mul: + // + // This results in undefined behavior [..] when `checked_mul` would + // return `None`. + // + // [2] Per https://doc.rust-lang.org/1.93.1/std/primitive.usize.html#method.unchecked_add: + // + // This results in undefined behavior [..] when `checked_add` would + // return `None`. + assert_eq!(unsafe { add_scaled_metadata(base, metadata, multiple) }, expected); + } + + #[allow(unused_imports)] + use crate::util::polyfills::*; + + // SAFETY: The caller promises that `base + metadata * multiple` is + // representable over the nonnegative integers. Thus `metadata * multiple` + // is representable too. On toolchains with inherent unchecked arithmetic, + // this is exactly the condition under which `usize::unchecked_mul` does + // not have undefined behavior [1]. On older supported toolchains, method + // resolution selects `polyfills::NumExt::unchecked_mul`, whose safety + // contract requires the same no-overflow condition. Kani checks the two + // implementations in separate harnesses. + // + // [1] Per https://doc.rust-lang.org/1.93.1/std/primitive.usize.html#method.unchecked_mul: + // + // This results in undefined behavior when `self * rhs > usize::MAX` or + // `self * rhs < usize::MIN`, i.e. when `checked_mul` would return `None`. + // + let scaled = unsafe { metadata.unchecked_mul(multiple) }; + + // SAFETY: The caller promises that `base + metadata * multiple` is + // representable in `usize`, and `scaled == metadata * multiple`. On + // toolchains with inherent unchecked arithmetic, that is exactly the + // condition under which `usize::unchecked_add` does not have undefined + // behavior [1]. On older supported toolchains, method resolution selects + // `polyfills::NumExt::unchecked_add`, whose safety contract requires the + // same no-overflow condition. Kani checks the two implementations in + // separate harnesses. + // + // [1] Per https://doc.rust-lang.org/1.93.1/std/primitive.usize.html#method.unchecked_add: + // + // This results in undefined behavior when `self + rhs > usize::MAX` or + // `self + rhs < usize::MIN`, i.e. when `checked_add` would return `None`. + unsafe { base.unchecked_add(scaled) } +} + +pub(crate) use cast_from::CastFrom; +mod cast_from; + +#[cfg(any(test, kani))] +mod padding_testutil { + use crate::KnownLayout; + + #[derive(KnownLayout)] + #[repr(C, align(4))] + pub(super) struct Aligned { + pub(super) prefix: Prefix, + pub(super) tail: Tail, + } + + /// Compare direct padding with the complete size minus the slice end. + /// Counts whose complete object size exceeds `usize::MAX` are omitted. + pub(super) fn check_padding_for_elems(elems: usize) -> Option + where + Target: ?Sized + KnownLayout, + { + let layout = crate::trailing_slice_layout::(); + let trailing_size = elems.checked_mul(layout.elem_size)?; + + // Evaluate the complete rounded size independently of both helpers: + // `size_for_elems` delegates to the padding method being tested. + let (alignment, phase) = layout.size_rounding_align_and_phase.components(); + #[allow(clippy::arithmetic_side_effects)] + let mask = alignment.get() - 1; + let unrounded = phase.checked_add(trailing_size)?; + let rounded = unrounded.checked_add(mask)? & !mask; + let object_size = layout.size_base.checked_add(rounded)?; + let trailing_end = layout.offset.checked_add(trailing_size).unwrap(); + let expected = object_size.checked_sub(trailing_end).unwrap(); + + let padding = layout.padding_for_elems(elems); + assert_eq!(padding, expected); + Some(padding) + } + + pub(super) fn check_layouts(elems: usize) { + // Slices have no trailing padding. The aligned fixtures cover + // zero and nonzero phases, a three-byte element whose padding + // cycles with the element count, and zero-sized elements. + let _ = check_padding_for_elems::<[u8]>(elems); + let _ = check_padding_for_elems::<[()]>(elems); + let _ = check_padding_for_elems::>(elems); + let _ = check_padding_for_elems::>(elems); + let _ = check_padding_for_elems::>(elems); + let _ = check_padding_for_elems::>(elems); + } +} + +// FIXME(#67): For some reason, on our MSRV toolchain, this `allow` isn't +// enforced despite having `#![allow(unknown_lints)]` at the crate root, but +// putting it here works. Once our MSRV is high enough that this bug has been +// fixed, remove this `allow`. +#[allow(unknown_lints)] +#[cfg(test)] +mod tests { + use super::*; + use crate::PointerMetadata as _; + + /// Models the size of nested `repr(C)` structs ending in a slice. + /// + /// `leading` lists structs from outermost to innermost. Each tuple gives + /// the packing factor, minimum alignment, and prefix size before padding + /// for the final field. `trailing_elem_size` and `trailing_alignment` give + /// the slice element size and alignment. An empty `leading` describes the + /// slice itself. + /// + /// The returned closure maps a slice length to the complete size, including + /// padding at every nesting level. It returns `None` only on `usize` + /// overflow; sizes above `isize::MAX` are permitted by this arithmetic model. + /// Packing affects field placement, preserving each field's internal + /// padding, even for descriptors that Rust would reject as types. + /// + /// # Panics + /// + /// Panics if any alignment or packing factor is not a power of two, a + /// minimum alignment exceeds its packing factor, or the element size is + /// not a multiple of the slice alignment. + fn size_for_metadata_model( + leading: &[(NonZeroUsize, NonZeroUsize, usize)], + trailing_elem_size: usize, + trailing_alignment: NonZeroUsize, + ) -> impl Fn(usize) -> Option + '_ { + // Rust requires power-of-two alignments and sizes divisible by their + // alignment [1]; alignment modifiers also take powers of two [2]. + // + // [1] https://doc.rust-lang.org/1.93.0/reference/type-layout.html#size-and-alignment + // [2] https://doc.rust-lang.org/1.93.0/reference/type-layout.html#the-alignment-modifiers + assert!(trailing_alignment.get().is_power_of_two()); + assert_eq!(trailing_elem_size % trailing_alignment, 0); + for &(packed, align, _) in leading { + assert!(packed.get().is_power_of_two()); + assert!(align.get().is_power_of_two()); + // The descriptor requires its minimum to fit under the packing + // cap. It also models combinations that Rust forbids, including + // simultaneous `align` and `packed` modifiers [2]. + assert!(align <= packed); + } + + move |elems| { + // A slice shares its array section's layout [3]. Array size is + // element size times length, with the element's alignment [4]. + // + // [3] https://doc.rust-lang.org/1.93.0/reference/type-layout.html#slice-layout + // [4] https://doc.rust-lang.org/1.93.0/reference/type-layout.html#array-layout + let mut size = trailing_elem_size.checked_mul(elems)?; + let mut alignment = trailing_alignment; + + // Work outward with each field's complete size, including its + // trailing padding: an enclosing representation "does not change + // the layout of the fields themselves" [5]. Thus packing a nested + // field preserves the padding already included in `size`. + // + // [5] https://doc.rust-lang.org/1.93.0/reference/type-layout.html#representations + for &(packed, align, prefix) in leading.iter().rev() { + // Packing caps the field's alignment for placement [2]. + let field_align = alignment.min(packed); + + // `repr(C)` places the next field at an aligned offset [6]; + // packing requires the least sufficient inter-field padding + // [2]. Here `prefix` is the end of the preceding fields. + // + // [6] https://doc.rust-lang.org/1.93.0/reference/type-layout.html#reprc-structs + let offset = nested_reference::round_up(prefix, field_align.get())?; + + // `repr(C)` uses the greatest field alignment [6], and an + // explicit alignment can only raise it [2]. Here `align` + // summarizes the minimum from the prefix and any explicit + // alignment. Both operands are <= `packed`, so their maximum + // also respects the packing cap. + alignment = align.max(field_align); + + // Advance past the complete field, then round to the struct's + // alignment, following `repr(C)`'s final sizing steps [6]. + size = offset.checked_add(size)?; + size = nested_reference::round_up(size, alignment.get())?; + } + // Each enclosing size is at least its field's size. Thus any + // checked multiplication or addition that overflowed above would + // also make the final size unrepresentable in `usize`. + Some(size) + } + } + + /// Computes [`size_for_metadata_model`]'s size function using `DstLayout`. + /// + /// Layout construction uses [`DstLayout::for_repr_c_struct`], and the + /// returned closure uses [`crate::PointerMetadata::size_for_metadata`]. + /// + /// # Panics + /// + /// Panics for invalid descriptors as in [`size_for_metadata_model`], or if + /// `DstLayout` cannot construct the layout. This includes static size + /// overflow and, in debug builds, alignments or packing factors above + /// [`DstLayout::CURRENT_MAX_ALIGN`]. + fn size_for_metadata_via_dst_layout( + leading: &[(NonZeroUsize, NonZeroUsize, usize)], + trailing_elem_size: usize, + trailing_alignment: NonZeroUsize, + ) -> impl Fn(usize) -> Option + '_ { + assert_eq!(trailing_elem_size % trailing_alignment, 0); + let mut layout = DstLayout { + align: trailing_alignment, + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + offset: 0, + elem_size: trailing_elem_size, + size_base: 0, + size_rounding_align_and_phase: RoundingAlignAndPhase::new(trailing_alignment, 0), + }), + statically_shallow_unpadded: true, + }; + for &(packed, align, prefix) in leading.iter().rev() { + assert!(align <= packed); + let prefix = DstLayout { + align: DstLayout::MIN_ALIGN, + size_info: SizeInfo::Sized { size: prefix }, + statically_shallow_unpadded: true, + }; + layout = DstLayout::for_repr_c_struct(Some(align), Some(packed), &[prefix, layout]); + } + move |elems: usize| elems.size_for_metadata(layout) + } + + #[test] + fn test_model_via_dst_layout() { + let nz = |n| NonZeroUsize::new(n).unwrap(); + let cases: &[&[_]] = &[ + &[], + &[(nz(8), nz(1), 3)], + &[(nz(8), nz(8), 3)], + &[(nz(2), nz(1), 1), (nz(8), nz(8), 3)], + &[(nz(8), nz(8), 1), (nz(2), nz(1), 3), (nz(8), nz(8), 5)], + &[(nz(1), nz(1), 1), (nz(2), nz(2), 3), (nz(8), nz(8), 5)], + ]; + for &leading in cases { + for &(elem_size, alignment) in &[(0, 1), (0, 8), (1, 1), (2, 2), (3, 1), (8, 8)] { + let alignment = nz(alignment); + let expected = size_for_metadata_model(leading, elem_size, alignment); + let actual = size_for_metadata_via_dst_layout(leading, elem_size, alignment); + let max_elems = usize::MAX / elem_size.max(1); + for elems in (0..32).chain([ + max_elems - 1, + max_elems, + max_elems.saturating_add(1), + usize::MAX, + ]) { + assert_eq!( + actual(elems), + expected(elems), + "leading: {:?}, trailing: {:?}, elems: {}", + leading, + (elem_size, alignment), + elems, + ); + } + } + } + + for prefix in [usize::MAX - 1, usize::MAX] { + let leading = [(nz(1), nz(1), prefix)]; + let expected = size_for_metadata_model(&leading, 1, nz(1)); + let actual = size_for_metadata_via_dst_layout(&leading, 1, nz(1)); + for elems in [0, 1, 2] { + assert_eq!(actual(elems), expected(elems)); + } + } + } + + // This exercises arithmetic rather than unsafe operations; keep the + // smaller comparison test enabled under Miri instead of this large search. + #[test] + #[cfg_attr(miri, ignore)] + fn test_model_via_dst_layout_combinations() { + use rand::{rngs::SmallRng, Rng as _, SeedableRng as _}; + + fn check( + leading: &[(NonZeroUsize, NonZeroUsize, usize)], + trailing_elem_size: usize, + trailing_alignment: NonZeroUsize, + ) { + let expected = size_for_metadata_model(leading, trailing_elem_size, trailing_alignment); + assert!( + expected(0).is_some(), + "unrepresentable test layout: {:?}, {:?}", + leading, + (trailing_elem_size, trailing_alignment) + ); + let actual = std::panic::catch_unwind(|| { + size_for_metadata_via_dst_layout(leading, trailing_elem_size, trailing_alignment) + }) + .unwrap_or_else(|_| { + panic!( + "DstLayout rejected leading: {:?}, trailing: {:?}", + leading, + (trailing_elem_size, trailing_alignment) + ) + }); + let compare = |elems| { + let expected = expected(elems); + assert_eq!( + actual(elems), + expected, + "leading: {:?}, trailing: {:?}, elems: {}", + leading, + (trailing_elem_size, trailing_alignment), + elems, + ); + expected + }; + + let first_sixteen = 0..=16; + let pointer_width = (0..POINTER_WIDTH_BITS).step_by(4).flat_map(|shift| { + let elems = 1usize << shift; + [elems - 1, elems, elems + 1] + }); + let last_two = (usize::MAX - 1)..=usize::MAX; + let elemses = first_sixteen.chain(pointer_width).chain(last_two); + for elems in elemses { + let _ = compare(elems); + } + + // The modeled size is nondecreasing. Find the last fitting length, + // comparing every probe, then check both sides of that boundary. + // Zero-sized elements can remain representable at usize::MAX. + let (mut low, mut high) = (0, usize::MAX); + while low < high { + let distance = high - low; + let mid = low + distance / 2 + distance % 2; + if compare(mid).is_some() { + low = mid; + } else { + high = mid - 1; + } + } + assert!(compare(low).is_some()); + if let Some(previous) = low.checked_sub(1) { + assert!(compare(previous).is_some()); + } + if let Some(next) = low.checked_add(1) { + assert_eq!(compare(next), None); + } + } + + let nz = |n| NonZeroUsize::new(n).unwrap(); + let check_small_tails = |leading: &[_]| { + for alignment in [1, 2, 4, 8] { + for multiple in [0, 1, 2, 3, 7] { + check(leading, multiple * alignment, nz(alignment)); + } + } + }; + let mut fragments = Vec::new(); + for packed in [1, 2, 4] { + for align in [1, 2, 4].iter().copied().filter(|&align| align <= packed) { + for prefix in 0..=4 { + fragments.push((nz(packed), nz(align), prefix)); + } + } + } + + // Exhaust every combination in this small domain at depths 0, 1, 2. + check_small_tails(&[]); + for &outer in &fragments { + check_small_tails(&[outer]); + for &inner in &fragments { + check_small_tails(&[outer, inner]); + } + } + + let mut rng = SmallRng::seed_from_u64(0); + for depth in (0usize..=128).chain([256, 512, 1024]) { + // A small prefix adds at most 4 * max_align bytes per layer. + // Reserve half of usize::MAX for a large outermost prefix so + // construction remains representable even at the largest depth. + let align_limit = + DstLayout::CURRENT_MAX_ALIGN.get().min(usize::MAX / (8 * depth.max(1))); + let alignments: Vec<_> = (0..POINTER_WIDTH_BITS) + .map(|shift| 1usize << shift) + .take_while(|&align| align <= align_limit) + .collect(); + let last = alignments.len() - 1; + let max_align = alignments[last]; + for case in 0..40 { + let mut leading = Vec::with_capacity(depth); + for level in 0..depth { + // Mix arbitrary modifiers, increasing and decreasing + // alignments, alternating packing, and fully packed trees. + let (packed, align) = match case / 8 { + 0 => { + let packed = rng.gen_range(0..=last); + (packed, rng.gen_range(0..=packed)) + } + 1 => (last, level % alignments.len()), + 2 => (last, last - level % alignments.len()), + 3 => { + let index = if level % 2 == 0 { 0 } else { last }; + (index, index) + } + _ => (0, 0), + }; + let (packed, align) = (alignments[packed], alignments[align]); + let prefix = if level == 0 && case % 4 == 3 { + usize::MAX / 2 + rng.gen_range(0..=2) - 1 + } else { + match rng.gen_range(0..8) { + 0 => 0, + 1 => 1, + 2 => align - 1, + 3 => align, + 4 => align + 1, + 5 => packed - 1, + 6 => packed + 1, + _ => rng.gen_range(0..=2 * max_align), + } + }; + leading.push((nz(packed), nz(align), prefix)); + } + let alignment = alignments[rng.gen_range(0..=last)]; + let elem_size = match case % 8 { + 0 => 0, + 1 => alignment, + 2 => 2 * alignment, + 3 => 3 * alignment, + 4 => 7 * alignment, + 5 => 15 * alignment, + 6 => (usize::MAX / 2 / alignment) * alignment, + _ => (usize::MAX / alignment) * alignment, + }; + check(&leading, elem_size, nz(alignment)); + } + } + } + + const TEST_SIZE_ROUNDING_ALIGN_AND_PHASE_ALIGN: NonZeroUsize = match NonZeroUsize::new(8) { + Some(align) => align, + None => const_unreachable!(), + }; + const TEST_SIZE_ROUNDING_ALIGN_AND_PHASE: RoundingAlignAndPhase = + RoundingAlignAndPhase::new(TEST_SIZE_ROUNDING_ALIGN_AND_PHASE_ALIGN, 3); + const TEST_SIZE_ROUNDING_ALIGN_AND_PHASE_DECODED_ALIGN: usize = + TEST_SIZE_ROUNDING_ALIGN_AND_PHASE.align().get(); + const TEST_SIZE_ROUNDING_ALIGN_AND_PHASE_DECODED_PHASE: usize = + TEST_SIZE_ROUNDING_ALIGN_AND_PHASE.components().1; + + #[test] + #[allow(clippy::arithmetic_side_effects)] + fn test_size_rounding_align_and_phase_encoding() { + // These constants ensure that encoding and decoding remain evaluable + // on every supported compiler, including the MSRV. + assert_eq!(TEST_SIZE_ROUNDING_ALIGN_AND_PHASE_DECODED_ALIGN, 8); + assert_eq!(TEST_SIZE_ROUNDING_ALIGN_AND_PHASE_DECODED_PHASE, 3); + + for shift in 0..POINTER_WIDTH_BITS { + #[allow(clippy::arithmetic_side_effects)] + let align = NonZeroUsize::new(1usize << shift).unwrap(); + for phase in [0, align.get() / 2, align.get() - 1] { + let rounding = RoundingAlignAndPhase::new(align, phase); + assert_eq!(rounding.align(), align); + assert_eq!(rounding.components().1, phase); + assert_eq!(rounding.0.get(), align.get() | phase); + } + } + + let max_align = DstLayout::THEORETICAL_MAX_ALIGN; + let max_phase = max_align.get() - 1; + let max = RoundingAlignAndPhase::new(max_align, max_phase); + assert_eq!(max.0.get(), usize::MAX); + assert_eq!(max.align(), max_align); + assert_eq!(max.components().1, max_phase); + + assert_eq!(mem::size_of::(), 4 * mem::size_of::()); + } + + #[test] + #[allow(clippy::arithmetic_side_effects)] + fn test_trailing_slice_layout_normal_form() { + // Compare the stored base-and-phase formula with + // `prefix + round_up(offset + elems * elem_size, align)`, where + // `prefix < align`. Move whole alignment-sized chunks of `offset` + // into `size_base` and retain the remainder as the phase. Also verify + // that `size_offset()` recovers the exact `offset` used by casts to + // choose between metadata values with the same rounded object size. + for align in [1, 2, 4, 8, 16] { + let align = NonZeroUsize::new(align).unwrap(); + for prefix in 0..align.get() { + for offset in 0..32 { + let phase = offset % align; + let size_base = (offset - phase) | prefix; + let layout = TrailingSliceLayout { + offset: 0, + elem_size: 3, + size_base, + size_rounding_align_and_phase: RoundingAlignAndPhase::new(align, phase), + }; + assert_eq!(layout.size_offset(), offset); + + for elems in 0..32 { + let unrounded = offset + elems * layout.elem_size; + let old_size = + prefix + unrounded + util::padding_needed_for(unrounded, align); + assert_eq!(layout.size_for_elems(elems), Some(old_size)); + } + } + } + } + } + + #[test] + #[allow(clippy::arithmetic_side_effects)] + fn test_normalized_size_bounds() { + // Use checked evaluation as an independent oracle, including layouts + // whose empty object is already too large to represent. + let checked_size = |layout: TrailingSliceLayout, trailing_bytes: usize| { + let (alignment, phase) = layout.size_rounding_align_and_phase.components(); + let unrounded = phase.checked_add(trailing_bytes)?; + let rounded = unrounded.checked_add(util::padding_needed_for(unrounded, alignment))?; + layout.size_base.checked_add(rounded) + }; + for exponent in 0..POINTER_WIDTH_BITS { + let alignment = NonZeroUsize::new(1usize << exponent).unwrap(); + for phase in [0, alignment.get() / 2, alignment.get() - 1] { + for base in [0, 1, alignment.get() - 1, alignment.get(), usize::MAX - 1, usize::MAX] + { + let mut layout = TrailingSliceLayout { + offset: 0, + elem_size: 1, + size_base: base, + size_rounding_align_and_phase: RoundingAlignAndPhase::new(alignment, phase), + }; + for available in [0, 1, alignment.get() - 1, alignment.get(), usize::MAX] { + match layout.max_trailing_bytes(available) { + Some(max_bytes) => { + assert!(checked_size(layout, max_bytes).unwrap() <= available); + if let Some(next_bytes) = max_bytes.checked_add(1) { + assert!(checked_size(layout, next_bytes) + .map(|size| size > available) + .unwrap_or(true)); + } + } + None => { + assert!(checked_size(layout, 0) + .map(|size| size > available) + .unwrap_or(true)); + } + } + } + + for elem_size in [0, 1, 3, alignment.get(), usize::MAX] { + layout.elem_size = elem_size; + let max_elems = layout + .max_trailing_bytes(usize::MAX) + .and_then(|bytes| bytes.checked_div(elem_size)) + .unwrap_or(usize::MAX); + for elems in [ + 0, + 1, + max_elems.saturating_sub(1), + max_elems, + max_elems.saturating_add(1), + usize::MAX, + ] { + let expected = elems + .checked_mul(elem_size) + .and_then(|bytes| checked_size(layout, bytes)); + // The physical slice offset does not affect + // the size formula. Raw layouts may place the + // slice beyond the object, and adding its byte + // count to the offset may overflow. + for offset in [0, usize::MAX] { + layout.offset = offset; + assert_eq!(layout.size_for_elems(elems), expected); + } + } + } + } + } + } + } + + #[test] + fn test_padding_for_elems_for_rust_layouts() { + use super::padding_testutil::{check_layouts, check_padding_for_elems, Aligned}; + + for elems in 0..256 { + check_layouts(elems); + } + let size_limit = usize::MAX; + for remaining in 0..16 { + check_layouts(size_limit - remaining); + check_layouts(size_limit / 3 - remaining); + } + check_layouts(usize::MAX); + + for (elems, expected) in [(0, 3), (1, 0), (2, 1), (3, 2)] { + assert_eq!( + check_padding_for_elems::>(elems), + Some(expected) + ); + } + // Every count is valid for zero-sized trailing elements. + assert_eq!(check_padding_for_elems::>(usize::MAX), Some(3)); + } + + #[test] + fn test_padding_for_elems() { + for exponent in 0..POINTER_WIDTH_BITS { + let alignment = NonZeroUsize::new(1usize << exponent).unwrap(); + let mask = alignment.get() - 1; + for phase in [0, alignment.get() / 2, mask] { + // Include zero-sized elements, a negative fixed term, fixed + // padding larger than the alignment, and overflowing sums + // and products. Raw layouts need not describe Rust types. + for (offset, elem_size, size_base) in [ + (0, 0, 0), + (0, 1, 1), + (9, 3, 8), + (3, 3, 0), + (9, 3, 12), + (usize::MAX, usize::MAX, usize::MAX), + (usize::MAX, 0, usize::MAX), + ] { + let layout = TrailingSliceLayout { + offset, + elem_size, + size_base, + size_rounding_align_and_phase: RoundingAlignAndPhase::new(alignment, phase), + }; + for elems in [0, 1, 2, 3, alignment.get(), usize::MAX] { + // Independently reconstruct the complete rounded + // size and slice end, then subtract. Round up by + // adding the mask and clearing the low bits. + let trailing_bytes = elems.wrapping_mul(elem_size); + let unrounded = phase.wrapping_add(trailing_bytes); + let rounded = unrounded.wrapping_add(mask) & !mask; + let object_size = size_base.wrapping_add(rounded); + let trailing_end = offset.wrapping_add(trailing_bytes); + assert_eq!( + layout.padding_for_elems(elems), + object_size.wrapping_sub(trailing_end), + ); + } + } + } + } + } + + #[test] + fn test_dst_layout_for_slice() { + let layout = DstLayout::for_slice::(); + match layout.size_info { + SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size, .. }) => { + assert_eq!(offset, 0); + assert_eq!(elem_size, 4); + } + _ => panic!("Expected SliceDst"), + } + assert_eq!(layout.align.get(), 4); + } + + /// Tests of when a sized `DstLayout` is extended with a sized field. + #[allow(clippy::decimal_literal_representation)] + #[test] + fn test_dst_layout_extend_sized_with_sized() { + // This macro constructs a layout corresponding to a `u8` and extends + // it with a zero-sized trailing field of the specified alignment. The + // resulting size and alignment must both be the field's alignment + // capped by the packing limit. Exercise every supported packing limit + // as well as the absence of a packing limit. + macro_rules! test_align_is_size { + ($n:expr) => { + let base = DstLayout::for_type::(); + let trailing_field = DstLayout::for_type::>(); + + let packs = core::iter::once(None) + .chain((0..=29).map(|p| NonZeroUsize::new(2usize.pow(p)))); + + for pack in packs { + let composite = base.extend(trailing_field, pack); + let max_align = pack.unwrap_or(DstLayout::CURRENT_MAX_ALIGN); + let align = $n.min(max_align.get()); + assert_eq!( + composite, + DstLayout { + align: NonZeroUsize::new(align).unwrap(), + size_info: SizeInfo::Sized { size: align }, + statically_shallow_unpadded: false, + } + ) + } + }; + } + + test_align_is_size!(1); + test_align_is_size!(2); + test_align_is_size!(4); + test_align_is_size!(8); + test_align_is_size!(16); + test_align_is_size!(32); + test_align_is_size!(64); + test_align_is_size!(128); + test_align_is_size!(256); + test_align_is_size!(512); + test_align_is_size!(1024); + test_align_is_size!(2048); + test_align_is_size!(4096); + test_align_is_size!(8192); + test_align_is_size!(16384); + test_align_is_size!(32768); + test_align_is_size!(65536); + test_align_is_size!(131072); + test_align_is_size!(262144); + test_align_is_size!(524288); + test_align_is_size!(1048576); + test_align_is_size!(2097152); + test_align_is_size!(4194304); + test_align_is_size!(8388608); + test_align_is_size!(16777216); + test_align_is_size!(33554432); + test_align_is_size!(67108864); + test_align_is_size!(33554432); + test_align_is_size!(134217728); + test_align_is_size!(268435456); + test_align_is_size!(536870912); + } + + /// Tests of when a sized `DstLayout` is extended with a DST field. + #[test] + fn test_dst_layout_extend_sized_with_dst() { + // Test that for all combinations of real-world alignments and + // `repr_packed` values, that the extension of a sized `DstLayout`` with + // a DST field correctly computes the trailing offset in the composite + // layout. + + let aligns = (0..29).map(|p| NonZeroUsize::new(2usize.pow(p)).unwrap()); + let packs = core::iter::once(None).chain(aligns.clone().map(Some)); + + for field_type_align in aligns { + for pack in packs.clone() { + let base = DstLayout::for_type::(); + let elem_size = 42; + let trailing_field_offset = 11; + #[allow(clippy::arithmetic_side_effects)] + let trailing_phase = trailing_field_offset % field_type_align; + let trailing_base = util::round_down_to_next_multiple_of_alignment( + trailing_field_offset, + field_type_align, + ); + + let trailing_field = DstLayout { + align: field_type_align, + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + elem_size, + offset: trailing_field_offset, + size_base: trailing_base, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + field_type_align, + trailing_phase, + ), + }), + statically_shallow_unpadded: false, + }; + + let composite = base.extend(trailing_field, pack); + + let max_align = pack.unwrap_or(DstLayout::CURRENT_MAX_ALIGN).get(); + + let align = field_type_align.get().min(max_align); + let field_offset = align; + let size_base = field_offset + trailing_base; + + assert_eq!( + composite, + DstLayout { + align: NonZeroUsize::new(align).unwrap(), + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + elem_size, + offset: field_offset + trailing_field_offset, + size_base, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + field_type_align, + trailing_phase, + ), + }), + statically_shallow_unpadded: false, + } + ) + } + } + } + + /// Tests that calling `pad_to_align` on a sized `DstLayout` adds the + /// expected amount of trailing padding. + #[test] + fn test_dst_layout_pad_to_align_with_sized() { + // For all valid alignments `align`, construct a one-byte layout aligned + // to `align`, call `pad_to_align`, and assert that the size of the + // resulting layout is equal to `align`. + for align in (0..29).map(|p| NonZeroUsize::new(2usize.pow(p)).unwrap()) { + let layout = DstLayout { + align, + size_info: SizeInfo::Sized { size: 1 }, + statically_shallow_unpadded: true, + }; + + assert_eq!( + layout.pad_to_align(), + DstLayout { + align, + size_info: SizeInfo::Sized { size: align.get() }, + statically_shallow_unpadded: align.get() == 1 + } + ); + } + + // Test explicitly-provided combinations of unpadded and padded + // counterparts. + + macro_rules! test { + (unpadded { size: $unpadded_size:expr, align: $unpadded_align:expr } + => padded { size: $padded_size:expr, align: $padded_align:expr }) => { + let unpadded = DstLayout { + align: NonZeroUsize::new($unpadded_align).unwrap(), + size_info: SizeInfo::Sized { size: $unpadded_size }, + statically_shallow_unpadded: false, + }; + let padded = unpadded.pad_to_align(); + + assert_eq!( + padded, + DstLayout { + align: NonZeroUsize::new($padded_align).unwrap(), + size_info: SizeInfo::Sized { size: $padded_size }, + statically_shallow_unpadded: false, + } + ); + }; + } + + test!(unpadded { size: 0, align: 4 } => padded { size: 0, align: 4 }); + test!(unpadded { size: 1, align: 4 } => padded { size: 4, align: 4 }); + test!(unpadded { size: 2, align: 4 } => padded { size: 4, align: 4 }); + test!(unpadded { size: 3, align: 4 } => padded { size: 4, align: 4 }); + test!(unpadded { size: 4, align: 4 } => padded { size: 4, align: 4 }); + test!(unpadded { size: 5, align: 4 } => padded { size: 8, align: 4 }); + test!(unpadded { size: 6, align: 4 } => padded { size: 8, align: 4 }); + test!(unpadded { size: 7, align: 4 } => padded { size: 8, align: 4 }); + test!(unpadded { size: 8, align: 4 } => padded { size: 8, align: 4 }); + + let current_max_align = DstLayout::CURRENT_MAX_ALIGN.get(); + + test!(unpadded { size: 1, align: current_max_align } + => padded { size: current_max_align, align: current_max_align }); + + test!(unpadded { size: current_max_align + 1, align: current_max_align } + => padded { size: current_max_align * 2, align: current_max_align }); + } + + /// Tests that calling `pad_to_align` on a DST `DstLayout` incorporates the + /// type's trailing-padding operation into its runtime size formula. + #[test] + fn test_dst_layout_pad_to_align_with_dst() { + for align in (0..29).map(|p| NonZeroUsize::new(2usize.pow(p)).unwrap()) { + for offset in 0..10 { + for elem_size in 0..10 { + let layout = DstLayout { + align, + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + offset, + elem_size, + size_base: offset, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + DstLayout::MIN_ALIGN, + 0, + ), + }), + statically_shallow_unpadded: false, + }; + assert_eq!( + layout.pad_to_align(), + DstLayout { + align, + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + offset, + elem_size, + size_base: offset - offset % align, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + align, + offset % align + ), + }), + statically_shallow_unpadded: false, + } + ); + } + } + } + + // Exercise the branch where the existing inner rounding alignment is + // at least the enclosing alignment. The inner rounded term is already + // aligned to two, so padding only advances `size_base` from three to + // four. + let inner_rounding_dominates = DstLayout { + align: NonZeroUsize::new(2).unwrap(), + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + offset: 3, + elem_size: 1, + size_base: 3, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + NonZeroUsize::new(4).unwrap(), + 1, + ), + }), + statically_shallow_unpadded: false, + }; + assert_eq!( + inner_rounding_dominates.pad_to_align(), + DstLayout { + align: NonZeroUsize::new(2).unwrap(), + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + offset: 3, + elem_size: 1, + size_base: 4, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + NonZeroUsize::new(4).unwrap(), + 1 + ), + }), + statically_shallow_unpadded: false, + } + ); + + // Exercise the branch where the enclosing alignment is larger. For + // a trailing-slice byte count `trailing_size`, rounding + // `6 + round_up(1 + trailing_size, 4)` to eight gives the equivalent + // formula `8 + round_up(1 + trailing_size, 8)`. + let outer_rounding_dominates = DstLayout { + align: NonZeroUsize::new(8).unwrap(), + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + offset: 7, + elem_size: 1, + size_base: 6, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + NonZeroUsize::new(4).unwrap(), + 1, + ), + }), + statically_shallow_unpadded: false, + }; + assert_eq!( + outer_rounding_dominates.pad_to_align(), + DstLayout { + align: NonZeroUsize::new(8).unwrap(), + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + offset: 7, + elem_size: 1, + size_base: 8, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + NonZeroUsize::new(8).unwrap(), + 1 + ), + }), + statically_shallow_unpadded: false, + } + ); + } + + #[test] + fn test_trailing_slice_layout_size_sequence_equivalence() { + // These are the two normalized descriptions produced by a + // `repr(C, packed(2))` wrapper around slice DSTs whose element types + // both have size four but alignments four and two respectively. Their + // formulas differ structurally, but both simplify to `2 + 4 * elems`. + let align4 = TrailingSliceLayout { + offset: 2, + elem_size: 4, + size_base: 2, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + NonZeroUsize::new(4).unwrap(), + 0, + ), + }; + let align2 = TrailingSliceLayout { + offset: 2, + elem_size: 4, + size_base: 2, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + NonZeroUsize::new(2).unwrap(), + 0, + ), + }; + assert!(align4.has_same_size_sequence(align2)); + for elems in 0..8 { + assert_eq!(align4.size_for_elems(elems), Some(2 + 4 * elems)); + assert_eq!(align4.size_for_elems(elems), align2.size_for_elems(elems)); + } + + let dynamically_padded = TrailingSliceLayout { elem_size: 1, ..align4 }; + assert!(!dynamically_padded.has_same_size_sequence(align2)); + } + + // This test takes a long time when running under Miri, so we skip it in + // that case. This is acceptable because this is a logic test that doesn't + // attempt to expose UB. + #[test] + #[cfg_attr(miri, ignore)] + fn test_validate_cast_and_convert_metadata() { + #[allow(non_local_definitions)] + impl From for SizeInfo { + fn from(size: usize) -> SizeInfo { + SizeInfo::Sized { size } + } + } + + #[allow(non_local_definitions)] + impl From<(usize, usize)> for SizeInfo { + fn from((offset, elem_size): (usize, usize)) -> SizeInfo { + SizeInfo::SliceDst(TrailingSliceLayout { + offset, + elem_size, + size_base: offset, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + DstLayout::MIN_ALIGN, + 0, + ), + }) + } + } + + fn layout>(s: S, align: usize) -> DstLayout { + let align = NonZeroUsize::new(align).unwrap(); + let size_info = match s.into() { + SizeInfo::SliceDst(mut trailing) => { + let phase = trailing.size_base % align; + trailing.size_base = + util::round_down_to_next_multiple_of_alignment(trailing.size_base, align); + trailing.size_rounding_align_and_phase = + RoundingAlignAndPhase::new(align, phase); + SizeInfo::SliceDst(trailing) + } + size_info => size_info, + }; + DstLayout { size_info, align, statically_shallow_unpadded: false } + } + + /// This macro accepts arguments in the form of: + /// + /// layout(_, _).validate(_, _, _), Ok(Some((_, _))) + /// | | | | | | | + /// size ---------+ | | | | | | + /// align -----------+ | | | | | + /// addr ------------------------+ | | | | + /// bytes_len ----------------------+ | | | + /// cast_type -------------------------+ | | + /// elems ------------------------------------------+ | + /// split_at ------------------------------------------+ + /// + /// `.validate` is shorthand for `.validate_cast_and_convert_metadata` + /// for brevity. + /// + /// Each argument can either be an iterator or a wildcard. Each + /// wildcarded variable is implicitly replaced by an iterator over a + /// representative sample of values for that variable. Each `test!` + /// invocation iterates over every combination of values provided by + /// each variable's iterator (ie, the cartesian product) and validates + /// that the results are expected. + /// + /// The final argument uses the same syntax, but it has a different + /// meaning: + /// - If it is `Ok(pat)`, then the pattern `pat` is supplied to + /// a matching assert to validate the computed result for each + /// combination of input values. + /// - If it is `Err(Some(msg) | None)`, then `test!` validates that the + /// call to `validate_cast_and_convert_metadata` panics with the given + /// panic message or, if the current Rust toolchain version is too + /// early to support panicking in `const fn`s, panics with *some* + /// message. In the latter case, the `const_panic!` macro is used, + /// which emits code which causes a non-panicking error at const eval + /// time, but which does panic when invoked at runtime. Thus, it is + /// merely difficult to predict the *value* of this panic. We deem + /// that testing against the real panic strings on stable and nightly + /// toolchains is enough to ensure correctness. + /// + /// Note that the meta-variables that match these variables have the + /// `tt` type, and some valid expressions are not valid `tt`s (such as + /// `a..b`). In this case, wrap the expression in parentheses, and it + /// will become valid `tt`. + macro_rules! test { + ( + layout($size:tt, $align:tt) + .validate($addr:tt, $bytes_len:tt, $cast_type:tt), $expect:pat $(,)? + ) => { + itertools::iproduct!( + test!(@generate_size $size), + test!(@generate_align $align), + test!(@generate_usize $addr), + test!(@generate_usize $bytes_len), + test!(@generate_cast_type $cast_type) + ).for_each(|(size_info, align, addr, bytes_len, cast_type)| { + // Temporarily disable the panic hook installed by the test + // harness. If we don't do this, all panic messages will be + // kept in an internal log. On its own, this isn't a + // problem, but if a non-caught panic ever happens (ie, in + // code later in this test not in this macro), all of the + // previously-buffered messages will be dumped, hiding the + // real culprit. + let previous_hook = std::panic::take_hook(); + // I don't understand why, but this seems to be required in + // addition to the previous line. + std::panic::set_hook(Box::new(|_| {})); + let actual = std::panic::catch_unwind(|| { + layout(size_info, align).validate_cast_and_convert_metadata(addr, bytes_len, cast_type) + }).map_err(|d| { + let msg = d.downcast::<&'static str>().ok().map(|s| *s.as_ref()); + assert!(msg.is_some() || cfg!(no_zerocopy_panic_in_const_and_vec_try_reserve_1_57_0), "non-string panic messages are not permitted when usage of panic in const fn is enabled"); + msg + }); + std::panic::set_hook(previous_hook); + + assert!( + matches!(actual, $expect), + "layout({:?}, {}).validate_cast_and_convert_metadata({}, {}, {:?})" ,size_info, align, addr, bytes_len, cast_type + ); + }); + }; + (@generate_usize _) => { 0..8 }; + // Generate sizes for both Sized and !Sized types. + (@generate_size _) => { + test!(@generate_size (_)).chain(test!(@generate_size (_, _))) + }; + // Generate sizes for both Sized and !Sized types by chaining + // specified iterators for each. + (@generate_size ($sized_sizes:tt | $unsized_sizes:tt)) => { + test!(@generate_size ($sized_sizes)).chain(test!(@generate_size $unsized_sizes)) + }; + // Generate sizes for Sized types. + (@generate_size (_)) => { test!(@generate_size (0..8)) }; + (@generate_size ($sizes:expr)) => { $sizes.into_iter().map(Into::::into) }; + // Generate sizes for !Sized types. + (@generate_size ($min_sizes:tt, $elem_sizes:tt)) => { + itertools::iproduct!( + test!(@generate_min_size $min_sizes), + test!(@generate_elem_size $elem_sizes) + ).map(Into::::into) + }; + (@generate_fixed_size _) => { (0..8).into_iter().map(Into::::into) }; + (@generate_min_size _) => { 0..8 }; + (@generate_elem_size _) => { 1..8 }; + (@generate_align _) => { [1, 2, 4, 8, 16] }; + (@generate_opt_usize _) => { [None].into_iter().chain((0..8).map(Some).into_iter()) }; + (@generate_cast_type _) => { [CastType::Prefix, CastType::Suffix] }; + (@generate_cast_type $variant:ident) => { [CastType::$variant] }; + // Some expressions need to be wrapped in parentheses in order to be + // valid `tt`s (required by the top match pattern). See the comment + // below for more details. This arm removes these parentheses to + // avoid generating an `unused_parens` warning. + (@$_:ident ($vals:expr)) => { $vals }; + (@$_:ident $vals:expr) => { $vals }; + } + + const EVENS: [usize; 8] = [0, 2, 4, 6, 8, 10, 12, 14]; + const ODDS: [usize; 8] = [1, 3, 5, 7, 9, 11, 13, 15]; + + // base_size is too big for the memory region. + test!( + layout(((1..8) | ((1..8), (1..8))), _).validate([0], [0], _), + Ok(Err(MetadataCastError::Size)) + ); + test!( + layout(((2..8) | ((2..8), (2..8))), _).validate([0], [1], Prefix), + Ok(Err(MetadataCastError::Size)) + ); + test!( + layout(((2..8) | ((2..8), (2..8))), _).validate([0x1000_0000 - 1], [1], Suffix), + Ok(Err(MetadataCastError::Size)) + ); + + // addr is unaligned for prefix cast + test!(layout(_, [2]).validate(ODDS, _, Prefix), Ok(Err(MetadataCastError::Alignment))); + test!(layout(_, [2]).validate(ODDS, _, Prefix), Ok(Err(MetadataCastError::Alignment))); + + // addr is aligned, but end of buffer is unaligned for suffix cast + test!(layout(_, [2]).validate(EVENS, ODDS, Suffix), Ok(Err(MetadataCastError::Alignment))); + test!(layout(_, [2]).validate(EVENS, ODDS, Suffix), Ok(Err(MetadataCastError::Alignment))); + + // Unfortunately, these constants cannot easily be used in the + // implementation of `validate_cast_and_convert_metadata`, since + // `panic!` consumes a string literal, not an expression. + // + // It's important that these messages be in a separate module. If they + // were at the function's top level, we'd pass them to `test!` as, e.g., + // `Err(TRAILING)`, which would run into a subtle Rust footgun - the + // `TRAILING` identifier would be treated as a pattern to match rather + // than a value to check for equality. + mod msgs { + pub(super) const TRAILING: &str = + "attempted to cast to slice type with zero-sized element"; + pub(super) const OVERFLOW: &str = "`addr` + `bytes_len` > usize::MAX"; + } + + // casts with ZST trailing element types are unsupported + test!(layout((_, [0]), _).validate(_, _, _), Err(Some(msgs::TRAILING) | None),); + + // addr + bytes_len must not overflow usize + test!(layout(_, _).validate([usize::MAX], (1..100), _), Err(Some(msgs::OVERFLOW) | None)); + test!(layout(_, _).validate((1..100), [usize::MAX], _), Err(Some(msgs::OVERFLOW) | None)); + test!( + layout(_, _).validate( + [usize::MAX / 2 + 1, usize::MAX], + [usize::MAX / 2 + 1, usize::MAX], + _ + ), + Err(Some(msgs::OVERFLOW) | None) + ); + + // Validates that `validate_cast_and_convert_metadata` satisfies its own + // documented safety postconditions, and also a few other properties + // that aren't documented but we want to guarantee anyway. + fn validate_behavior( + (layout, addr, bytes_len, cast_type): (DstLayout, usize, usize, CastType), + ) { + if let Ok((elems, split_at)) = + layout.validate_cast_and_convert_metadata(addr, bytes_len, cast_type) + { + let (size_info, align) = (layout.size_info, layout.align); + let debug_str = format!( + "layout({:?}, {}).validate_cast_and_convert_metadata({}, {}, {:?}) => ({}, {})", + size_info, align, addr, bytes_len, cast_type, elems, split_at + ); + + // If this is a sized type (no trailing slice), then `elems` is + // meaningless, but in practice we set it to 0. Callers are not + // allowed to rely on this, but a lot of math is nicer if + // they're able to, and some callers might accidentally do that. + let sized = matches!(layout.size_info, SizeInfo::Sized { .. }); + assert!(!(sized && elems != 0), "{}", debug_str); + + let resulting_size = match layout.size_info { + SizeInfo::Sized { size } => size, + SizeInfo::SliceDst(TrailingSliceLayout { + elem_size, + size_base, + size_rounding_align_and_phase, + .. + }) => { + let (size_align, size_phase) = size_rounding_align_and_phase.components(); + let padded_size = |elems| { + let without_padding = size_phase + elems * elem_size; + size_base + + without_padding + + util::padding_needed_for(without_padding, size_align) + }; + + let resulting_size = padded_size(elems); + // Test that `validate_cast_and_convert_metadata` + // computed the largest possible value that fits in the + // given range. + assert!(padded_size(elems + 1) > bytes_len, "{}", debug_str); + resulting_size + } + }; + + // Test safety postconditions guaranteed by + // `validate_cast_and_convert_metadata`. + assert!(resulting_size <= bytes_len, "{}", debug_str); + match cast_type { + CastType::Prefix => { + assert_eq!(addr % align, 0, "{}", debug_str); + assert_eq!(resulting_size, split_at, "{}", debug_str); + } + CastType::Suffix => { + assert_eq!(split_at, bytes_len - resulting_size, "{}", debug_str); + assert_eq!((addr + split_at) % align, 0, "{}", debug_str); + } + } + } else { + let min_size = match layout.size_info { + SizeInfo::Sized { size } => size, + SizeInfo::SliceDst(TrailingSliceLayout { + size_base, + size_rounding_align_and_phase, + .. + }) => { + let (size_align, size_phase) = size_rounding_align_and_phase.components(); + size_base + size_phase + util::padding_needed_for(size_phase, size_align) + } + }; + + // If a cast is invalid, it is either because... + // 1. there are insufficient bytes at the given region for type: + let insufficient_bytes = bytes_len < min_size; + // 2. performing the cast would misalign type: + let base = match cast_type { + CastType::Prefix => 0, + CastType::Suffix => bytes_len, + }; + let misaligned = (base + addr) % layout.align != 0; + + assert!(insufficient_bytes || misaligned); + } + } + + let sizes = 0..8; + let elem_sizes = 1..8; + let size_infos = sizes + .clone() + .map(Into::::into) + .chain(itertools::iproduct!(sizes, elem_sizes).map(Into::::into)); + let layouts = itertools::iproduct!(size_infos, [1, 2, 4, 8, 16, 32]) + .filter(|(size_info, align)| !matches!(size_info, SizeInfo::Sized { size } if size % align != 0)) + .map(|(size_info, align)| layout(size_info, align)); + itertools::iproduct!(layouts, 0..8, 0..8, [CastType::Prefix, CastType::Suffix]) + .for_each(validate_behavior); + + // Exercise a normalized formula which cannot be represented as + // `round_up(offset + elems * elem_size, align)`: the physical trailing + // slice begins at offset 7, while an inner aligned DST contributes the + // size formula `2 + round_up(5 + elems, 4)` to a packed outer type. + let nested_packed = DstLayout { + align: NonZeroUsize::new(2).unwrap(), + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + offset: 7, + elem_size: 1, + size_base: 6, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + NonZeroUsize::new(4).unwrap(), + 1, + ), + }), + statically_shallow_unpadded: false, + }; + itertools::iproduct!([nested_packed], 0..8, 0..24, [CastType::Prefix, CastType::Suffix]) + .for_each(validate_behavior); + + assert!(matches!( + nested_packed.validate_cast_and_convert_metadata(0, 8, CastType::Prefix), + Err(MetadataCastError::Size) + )); + assert!(matches!( + nested_packed.validate_cast_and_convert_metadata(0, 10, CastType::Prefix), + Ok((3, 10)) + )); + } + + #[test] + #[cfg(__ZEROCOPY_INTERNAL_USE_ONLY_NIGHTLY_FEATURES_IN_TESTS)] + fn test_validate_rust_layout() { + use core::{ + convert::TryInto as _, + ptr::{self, NonNull}, + }; + + use crate::util::testutil::*; + + // This test synthesizes pointers with various metadata and uses Rust's + // built-in APIs to confirm that Rust makes decisions about type layout + // which are consistent with what we believe is guaranteed by the + // language. If this test fails, it doesn't just mean our code is wrong + // - it means we're misunderstanding the language's guarantees. + + #[derive(Debug)] + struct MacroArgs { + offset: usize, + align: NonZeroUsize, + elem_size: Option, + } + + /// # Safety + /// + /// `test` promises to only call `addr_of_slice_field` on a `NonNull` + /// which points to a valid `T`. + /// + /// `with_elems` must produce a pointer which points to a valid `T`. + fn test NonNull>( + args: MacroArgs, + with_elems: W, + addr_of_slice_field: Option) -> NonNull>, + ) { + let dst = args.elem_size.is_some(); + let layout = { + let size_info = match args.elem_size { + Some(elem_size) => SizeInfo::SliceDst(TrailingSliceLayout { + offset: args.offset, + elem_size, + size_base: util::round_down_to_next_multiple_of_alignment( + args.offset, + args.align, + ), + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + args.align, + args.offset % args.align, + ), + }), + None => SizeInfo::Sized { + // Rust only supports types whose sizes are a multiple + // of their alignment. If the macro created a type like + // this: + // + // #[repr(C, align(2))] + // struct Foo([u8; 1]); + // + // ...then Rust will automatically round the type's size + // up to 2. + size: args.offset + util::padding_needed_for(args.offset, args.align), + }, + }; + DstLayout { size_info, align: args.align, statically_shallow_unpadded: false } + }; + + for elems in 0..128 { + let ptr = with_elems(elems); + + if let Some(addr_of_slice_field) = addr_of_slice_field { + let slc_field_ptr = addr_of_slice_field(ptr).as_ptr(); + // SAFETY: Both `slc_field_ptr` and `ptr` are pointers to + // the same valid Rust object. + // Work around https://github.com/rust-lang/rust-clippy/issues/12280 + let offset: usize = + unsafe { slc_field_ptr.byte_offset_from(ptr.as_ptr()).try_into().unwrap() }; + assert_eq!(offset, args.offset); + } + + // SAFETY: `ptr` points to a valid `T`. + #[allow(clippy::multiple_unsafe_ops_per_block)] + let (size, align) = unsafe { + (mem::size_of_val_raw(ptr.as_ptr()), mem::align_of_val_raw(ptr.as_ptr())) + }; + + // Avoid expensive allocation when running under Miri. + let assert_msg = if !cfg!(miri) { + format!("\n{:?}\nsize:{}, align:{}", args, size, align) + } else { + String::new() + }; + + let without_padding = + args.offset + args.elem_size.map(|elem_size| elems * elem_size).unwrap_or(0); + assert!(size >= without_padding, "{}", assert_msg); + assert_eq!(align, args.align.get(), "{}", assert_msg); + + // This encodes the most important part of the test: our + // understanding of how Rust determines the layout of repr(C) + // types. Sized repr(C) types are trivial, but DST types have + // some subtlety. Note that: + // - For sized types, `without_padding` is just the size of the + // type that we constructed for `Foo`. Since we may have + // requested a larger alignment, `Foo` may actually be larger + // than this, hence `padding_needed_for`. + // - For unsized types, `without_padding` is dynamically + // computed from the offset, the element size, and element + // count. We expect that the size of the object should be + // `offset + elem_size * elems` rounded up to the next + // alignment. + let expected_size = + without_padding + util::padding_needed_for(without_padding, args.align); + assert_eq!(expected_size, size, "{}", assert_msg); + + // For zero-sized element types, + // `validate_cast_and_convert_metadata` just panics, so we skip + // testing those types. + if args.elem_size.map(|elem_size| elem_size > 0).unwrap_or(true) { + let addr = ptr.addr().get(); + let (got_elems, got_split_at) = layout + .validate_cast_and_convert_metadata(addr, size, CastType::Prefix) + .unwrap(); + // Avoid expensive allocation when running under Miri. + let assert_msg = if !cfg!(miri) { + format!( + "{}\nvalidate_cast_and_convert_metadata({}, {})", + assert_msg, addr, size, + ) + } else { + String::new() + }; + assert_eq!(got_split_at, size, "{}", assert_msg); + if dst { + assert!(got_elems >= elems, "{}", assert_msg); + if got_elems != elems { + // If `validate_cast_and_convert_metadata` + // returned more elements than `elems`, that + // means that `elems` is not the maximum number + // of elements that can fit in `size` - in other + // words, there is enough padding at the end of + // the value to fit at least one more element. + // If we use this metadata to synthesize a + // pointer, despite having a different element + // count, we still expect it to have the same + // size. + let got_ptr = with_elems(got_elems); + // SAFETY: `got_ptr` is a pointer to a valid `T`. + let size_of_got_ptr = unsafe { mem::size_of_val_raw(got_ptr.as_ptr()) }; + assert_eq!(size_of_got_ptr, size, "{}", assert_msg); + } + } else { + // For sized casts, the returned element value is + // technically meaningless, and we don't guarantee any + // particular value. In practice, it's always zero. + assert_eq!(got_elems, 0, "{}", assert_msg) + } + } + } + } + + macro_rules! validate_against_rust { + ($offset:literal, $align:literal $(, $elem_size:literal)?) => {{ + #[repr(C, align($align))] + struct Foo([u8; $offset]$(, [[u8; $elem_size]])?); + + let args = MacroArgs { + offset: $offset, + align: $align.try_into().unwrap(), + elem_size: { + #[allow(unused)] + let ret = None::; + $(let ret = Some($elem_size);)? + ret + } + }; + + #[repr(C, align($align))] + struct FooAlign; + // Create an aligned buffer to use in order to synthesize + // pointers to `Foo`. We don't ever load values from these + // pointers - we just do arithmetic on them - so having a "real" + // block of memory as opposed to a validly-aligned-but-dangling + // pointer is only necessary to make Miri happy since we run it + // with "strict provenance" checking enabled. + let aligned_buf = Align::<_, FooAlign>::new([0u8; 1024]); + let with_elems = |elems| { + let slc = NonNull::slice_from_raw_parts(NonNull::from(&aligned_buf.t), elems); + #[allow(clippy::as_conversions)] + NonNull::new(slc.as_ptr() as *mut Foo).unwrap() + }; + let addr_of_slice_field = { + #[allow(unused)] + let f = None::) -> NonNull>; + $( + // SAFETY: `test` promises to only call `f` with a `ptr` + // to a valid `Foo`. + let f: Option) -> NonNull> = Some(|ptr: NonNull| unsafe { + NonNull::new(ptr::addr_of_mut!((*ptr.as_ptr()).1)).unwrap().cast::() + }); + let _ = $elem_size; + )? + f + }; + + test::(args, with_elems, addr_of_slice_field); + }}; + } + + // Every permutation of: + // - offset in [0, 4] + // - align in [1, 16] + // - elem_size in [0, 4] (plus no elem_size) + validate_against_rust!(0, 1); + validate_against_rust!(0, 1, 0); + validate_against_rust!(0, 1, 1); + validate_against_rust!(0, 1, 2); + validate_against_rust!(0, 1, 3); + validate_against_rust!(0, 1, 4); + validate_against_rust!(0, 2); + validate_against_rust!(0, 2, 0); + validate_against_rust!(0, 2, 1); + validate_against_rust!(0, 2, 2); + validate_against_rust!(0, 2, 3); + validate_against_rust!(0, 2, 4); + validate_against_rust!(0, 4); + validate_against_rust!(0, 4, 0); + validate_against_rust!(0, 4, 1); + validate_against_rust!(0, 4, 2); + validate_against_rust!(0, 4, 3); + validate_against_rust!(0, 4, 4); + validate_against_rust!(0, 8); + validate_against_rust!(0, 8, 0); + validate_against_rust!(0, 8, 1); + validate_against_rust!(0, 8, 2); + validate_against_rust!(0, 8, 3); + validate_against_rust!(0, 8, 4); + validate_against_rust!(0, 16); + validate_against_rust!(0, 16, 0); + validate_against_rust!(0, 16, 1); + validate_against_rust!(0, 16, 2); + validate_against_rust!(0, 16, 3); + validate_against_rust!(0, 16, 4); + validate_against_rust!(1, 1); + validate_against_rust!(1, 1, 0); + validate_against_rust!(1, 1, 1); + validate_against_rust!(1, 1, 2); + validate_against_rust!(1, 1, 3); + validate_against_rust!(1, 1, 4); + validate_against_rust!(1, 2); + validate_against_rust!(1, 2, 0); + validate_against_rust!(1, 2, 1); + validate_against_rust!(1, 2, 2); + validate_against_rust!(1, 2, 3); + validate_against_rust!(1, 2, 4); + validate_against_rust!(1, 4); + validate_against_rust!(1, 4, 0); + validate_against_rust!(1, 4, 1); + validate_against_rust!(1, 4, 2); + validate_against_rust!(1, 4, 3); + validate_against_rust!(1, 4, 4); + validate_against_rust!(1, 8); + validate_against_rust!(1, 8, 0); + validate_against_rust!(1, 8, 1); + validate_against_rust!(1, 8, 2); + validate_against_rust!(1, 8, 3); + validate_against_rust!(1, 8, 4); + validate_against_rust!(1, 16); + validate_against_rust!(1, 16, 0); + validate_against_rust!(1, 16, 1); + validate_against_rust!(1, 16, 2); + validate_against_rust!(1, 16, 3); + validate_against_rust!(1, 16, 4); + validate_against_rust!(2, 1); + validate_against_rust!(2, 1, 0); + validate_against_rust!(2, 1, 1); + validate_against_rust!(2, 1, 2); + validate_against_rust!(2, 1, 3); + validate_against_rust!(2, 1, 4); + validate_against_rust!(2, 2); + validate_against_rust!(2, 2, 0); + validate_against_rust!(2, 2, 1); + validate_against_rust!(2, 2, 2); + validate_against_rust!(2, 2, 3); + validate_against_rust!(2, 2, 4); + validate_against_rust!(2, 4); + validate_against_rust!(2, 4, 0); + validate_against_rust!(2, 4, 1); + validate_against_rust!(2, 4, 2); + validate_against_rust!(2, 4, 3); + validate_against_rust!(2, 4, 4); + validate_against_rust!(2, 8); + validate_against_rust!(2, 8, 0); + validate_against_rust!(2, 8, 1); + validate_against_rust!(2, 8, 2); + validate_against_rust!(2, 8, 3); + validate_against_rust!(2, 8, 4); + validate_against_rust!(2, 16); + validate_against_rust!(2, 16, 0); + validate_against_rust!(2, 16, 1); + validate_against_rust!(2, 16, 2); + validate_against_rust!(2, 16, 3); + validate_against_rust!(2, 16, 4); + validate_against_rust!(3, 1); + validate_against_rust!(3, 1, 0); + validate_against_rust!(3, 1, 1); + validate_against_rust!(3, 1, 2); + validate_against_rust!(3, 1, 3); + validate_against_rust!(3, 1, 4); + validate_against_rust!(3, 2); + validate_against_rust!(3, 2, 0); + validate_against_rust!(3, 2, 1); + validate_against_rust!(3, 2, 2); + validate_against_rust!(3, 2, 3); + validate_against_rust!(3, 2, 4); + validate_against_rust!(3, 4); + validate_against_rust!(3, 4, 0); + validate_against_rust!(3, 4, 1); + validate_against_rust!(3, 4, 2); + validate_against_rust!(3, 4, 3); + validate_against_rust!(3, 4, 4); + validate_against_rust!(3, 8); + validate_against_rust!(3, 8, 0); + validate_against_rust!(3, 8, 1); + validate_against_rust!(3, 8, 2); + validate_against_rust!(3, 8, 3); + validate_against_rust!(3, 8, 4); + validate_against_rust!(3, 16); + validate_against_rust!(3, 16, 0); + validate_against_rust!(3, 16, 1); + validate_against_rust!(3, 16, 2); + validate_against_rust!(3, 16, 3); + validate_against_rust!(3, 16, 4); + validate_against_rust!(4, 1); + validate_against_rust!(4, 1, 0); + validate_against_rust!(4, 1, 1); + validate_against_rust!(4, 1, 2); + validate_against_rust!(4, 1, 3); + validate_against_rust!(4, 1, 4); + validate_against_rust!(4, 2); + validate_against_rust!(4, 2, 0); + validate_against_rust!(4, 2, 1); + validate_against_rust!(4, 2, 2); + validate_against_rust!(4, 2, 3); + validate_against_rust!(4, 2, 4); + validate_against_rust!(4, 4); + validate_against_rust!(4, 4, 0); + validate_against_rust!(4, 4, 1); + validate_against_rust!(4, 4, 2); + validate_against_rust!(4, 4, 3); + validate_against_rust!(4, 4, 4); + validate_against_rust!(4, 8); + validate_against_rust!(4, 8, 0); + validate_against_rust!(4, 8, 1); + validate_against_rust!(4, 8, 2); + validate_against_rust!(4, 8, 3); + validate_against_rust!(4, 8, 4); + validate_against_rust!(4, 16); + validate_against_rust!(4, 16, 0); + validate_against_rust!(4, 16, 1); + validate_against_rust!(4, 16, 2); + validate_against_rust!(4, 16, 3); + validate_against_rust!(4, 16, 4); + } +} + +#[cfg(kani)] +mod proofs { + //! Arithmetic models and input generators for the layout proofs. + //! + //! The contract models specify expected results without calling the + //! operation being checked. The composition models (`extended_layout`, + //! `padded_layout`, and `repr_c_layout`) also define the contracts' input + //! domains: `Some` supplies an expected layout, while `None` excludes an + //! input from the corresponding non-panicking contract. These models check + //! representability in `usize`, without imposing Rust's `isize::MAX` bound. + + use core::alloc::Layout; + + use super::*; + + /// Evaluates the normalized object size for a trailing slice of + /// `trailing_size` bytes: + /// + /// ```text + /// size_base + round_up(size_phase + trailing_size, size_align) + /// ``` + /// + /// Here, `size_align` and `size_phase` come from `layout.size_rounding_align_and_phase`. + /// `round_up` rounds its input up to a multiple of its alignment. Returns + /// `None` if the size is not representable in `usize`. + /// + /// Using byte counts separates rounding from element-count multiplication + /// and permits proofs over byte counts that are not whole elements. + pub(super) fn slice_dst_size_for_trailing_bytes( + layout: TrailingSliceLayout, + trailing_size: usize, + ) -> Option { + let without_padding = + layout.size_rounding_align_and_phase.components().1.checked_add(trailing_size)?; + let padding = + util::padding_needed_for(without_padding, layout.size_rounding_align_and_phase.align()); + let rounded = without_padding.checked_add(padding)?; + layout.size_base.checked_add(rounded) + } + + /// Returns the least multiple of `align` greater than or equal to `bytes`, + /// or `None` if that multiple exceeds `usize::MAX`. + /// + /// Requires a power-of-two `align`. Composition models use this checked + /// operation to identify inputs for which adding padding would overflow. + fn round_up(bytes: usize, align: NonZeroUsize) -> Option { + bytes.checked_add(util::padding_needed_for(bytes, align)) + } + + /// Models the largest trailing-slice byte count whose object size fits in + /// `available` bytes. Returns `None` if even an empty trailing slice cannot + /// fit. The result need not be a multiple of the element size. + /// + /// Subtracting `size_base` leaves the budget for the rounded contribution. + /// Rounding that budget down to `size_align` and subtracting `size_phase` + /// gives the largest byte count whose `slice_dst_size_for_trailing_bytes` + /// result is at most `available`. Metadata and casting contracts use + /// this bound without calling the production capacity calculation. + pub(super) fn max_trailing_bytes( + trailing: TrailingSliceLayout, + available: usize, + ) -> Option { + let (align, phase) = trailing.size_rounding_align_and_phase.components(); + let capacity = available.checked_sub(trailing.size_base)?; + util::round_down_to_next_multiple_of_alignment(capacity, align).checked_sub(phase) + } + + /// Models appending `field` to a sized `prefix` for `DstLayout::extend`. + /// + /// Places the field at the first offset after the prefix that satisfies the + /// field's alignment, capped by `packed` when present. The resulting + /// alignment is the maximum of the prefix's alignment and this effective + /// field alignment. A DST field retains its inner rounding operation, with + /// its slice offset and size base shifted by the field's placement. + /// Trailing padding for the enclosing layout is left to `padded_layout`. + /// + /// Returns `None` for a non-power-of-two alignment, an unsized prefix, or + /// arithmetic overflow. The `statically_shallow_unpadded` flag is true only + /// if both inputs have it set and field placement introduces no gap. + pub(super) fn extended_layout( + prefix: DstLayout, + field: DstLayout, + packed: Option, + ) -> Option { + if !prefix.align.is_power_of_two() + || !field.align.is_power_of_two() + || packed.map_or(false, |align| !align.is_power_of_two()) + { + return None; + } + let SizeInfo::Sized { size: prefix_size } = prefix.size_info else { return None }; + let field_align = field.align.min(packed.unwrap_or(DstLayout::THEORETICAL_MAX_ALIGN)); + let field_offset = round_up(prefix_size, field_align)?; + let size_info = match field.size_info { + SizeInfo::Sized { size } => SizeInfo::Sized { size: field_offset.checked_add(size)? }, + SizeInfo::SliceDst(trailing) => SizeInfo::SliceDst(TrailingSliceLayout { + offset: field_offset.checked_add(trailing.offset)?, + size_base: field_offset.checked_add(trailing.size_base)?, + ..trailing + }), + }; + Some(DstLayout { + align: prefix.align.max(field_align), + size_info, + statically_shallow_unpadded: prefix.statically_shallow_unpadded + && field.statically_shallow_unpadded + && field_offset == prefix_size, + }) + } + + /// Models adding trailing padding for `DstLayout::pad_to_align`. + /// + /// A sized layout rounds its size up to a multiple of `layout.align`. A DST + /// instead composes that rounding with its existing size expression: + /// + /// ```text + /// round_up(size_base + round_up(size_phase + trailing_size, size_align), + /// layout.align) + /// ``` + /// + /// Here, `size_align` and `size_phase` come from the DST's + /// `size_rounding_align_and_phase`. The result encodes this expression with + /// one base, phase, and rounding alignment, preserving the physical slice + /// offset and element size. The `statically_shallow_unpadded` flag is + /// cleared only when a sized layout gains padding; DST padding is accounted + /// for by the size expression. + /// + /// Returns `None` if `layout.align` is not a power of two or a checked + /// intermediate overflows. The contract compares this expected encoding + /// with the production result; the separate padding formula proofs check + /// that the encoding preserves the rounded size. + pub(super) fn padded_layout(layout: DstLayout) -> Option { + if !layout.align.is_power_of_two() { + return None; + } + let (size_info, no_static_padding) = match layout.size_info { + SizeInfo::Sized { size } => { + let rounded = round_up(size, layout.align)?; + (SizeInfo::Sized { size: rounded }, rounded == size) + } + SizeInfo::SliceDst(trailing) => { + let (inner_align, phase) = trailing.size_rounding_align_and_phase.components(); + let (size_base, size_rounding_align_and_phase) = if inner_align >= layout.align { + ( + round_up(trailing.size_base, layout.align)?, + trailing.size_rounding_align_and_phase, + ) + } else { + let fixed = round_up(trailing.size_base, inner_align)?.checked_add(phase)?; + let new_phase = fixed & (layout.align.get() - 1); + (fixed - new_phase, RoundingAlignAndPhase::new(layout.align, new_phase)) + }; + ( + SizeInfo::SliceDst(TrailingSliceLayout { + size_base, + size_rounding_align_and_phase, + ..trailing + }), + true, + ) + } + }; + Some(DstLayout { + size_info, + statically_shallow_unpadded: layout.statically_shallow_unpadded && no_static_padding, + ..layout + }) + } + + /// Models a complete layout for `DstLayout::for_repr_c_struct` by composing + /// the field-placement and trailing-padding specifications. + /// + /// Starts with an empty prefix of alignment `align`, or one if absent. + /// Appends fields in order, with `packed` as an optional cap on field + /// alignment; the final layout is padded to its resulting alignment. Only + /// the last field may be unsized, since field extension requires a sized + /// prefix. + /// + /// Returns `None` if the initial alignment is not a power of two or any + /// field extension or final padding step returns `None`. + pub(super) fn repr_c_layout( + align: Option, + packed: Option, + fields: &[DstLayout], + ) -> Option { + let align = align.unwrap_or(DstLayout::MIN_ALIGN); + if !align.is_power_of_two() { + return None; + } + let mut layout = DstLayout { + align, + size_info: SizeInfo::Sized { size: 0 }, + statically_shallow_unpadded: true, + }; + for &field in fields { + layout = extended_layout(layout, field, packed)?; + } + padded_layout(layout) + } + + // The inline contract harnesses use unrestricted `Arbitrary` values. + // These generators retain the narrower domains of the Rust-layout proofs. + + /// Generates any power-of-two alignment representable in `usize`, including + /// `DstLayout::THEORETICAL_MAX_ALIGN`. This restricts the alignment alone; + /// callers must impose any constraints relating it to a layout's size. + fn any_layout_align() -> NonZeroUsize { + let exponent: u8 = kani::any(); + kani::assume(usize::from(exponent) < POINTER_WIDTH_BITS); + + // `exponent < POINTER_WIDTH_BITS`, so the shift is in range and its + // result is non-zero. This construction is surjective over every + // power-of-two alignment up to and including + // `THEORETICAL_MAX_ALIGN`. `Layout` admits that maximum alignment for + // zero-sized layouts, so the validity assumptions below—not this + // generator—filter combinations that cannot describe Rust types. + NonZeroUsize::new(1usize << exponent).unwrap() + } + + /// Generates layouts satisfying the arithmetic restrictions used by the + /// standalone Rust-layout proofs. + /// + /// The fixed size, or the zero-element DST size, must be accepted by + /// `Layout::from_size_align`. For a DST, `size_base + size_phase` must also + /// be representable and at least the physical slice offset. Component + /// bounds come from `any_bounded_size_info`; `statically_shallow_unpadded` + /// remains arbitrary. These conditions do not establish that a Rust type + /// with this layout exists. + fn any_valid_dst_layout() -> DstLayout { + let align = any_layout_align(); + let size_info = any_bounded_size_info(); + + kani::assume( + match size_info { + SizeInfo::Sized { size } => Layout::from_size_align(size, align.get()), + SizeInfo::SliceDst(trailing) => { + // `SliceDst` cannot encode one exact size. Validate its + // minimum size and the invariants required of the size + // formula for layouts of real Rust types. + let min_size = trailing.size_for_elems(0); + let unrounded_min_size = trailing + .size_base + .checked_add(trailing.size_rounding_align_and_phase.components().1); + + kani::assume(matches!( + unrounded_min_size, + Some(size) if size >= trailing.offset + )); + + match min_size { + Some(min_size) => Layout::from_size_align(min_size, align.get()), + None => Layout::from_size_align(usize::MAX, align.get()), + } + } + } + .is_ok(), + ); + + DstLayout { align: align, size_info: size_info, statically_shallow_unpadded: kani::any() } + } + + /// Generates either a fixed size at most `DstLayout::MAX_SIZE`, or a DST + /// description from `any_bounded_trailing_layout`. + /// + /// Supplies bounded size components to `any_valid_dst_layout`, which adds + /// constraints involving the enclosing layout's alignment. + fn any_bounded_size_info() -> SizeInfo { + let is_sized: bool = kani::any(); + + match is_sized { + true => { + let size: usize = kani::any(); + + kani::assume(size <= DstLayout::MAX_SIZE); + + SizeInfo::Sized { size } + } + false => SizeInfo::SliceDst(any_bounded_trailing_layout()), + } + } + + /// Generates a trailing-slice description whose element size, slice offset, + /// and size base are each at most `DstLayout::MAX_SIZE` (`isize::MAX`). + /// + /// The rounding alignment spans all representable powers of two, and the + /// phase spans all values below that alignment. No further relationships + /// between these components are imposed: their combined size may overflow + /// or fail to contain the slice. Standalone proofs add the relationships + /// they require; contract harnesses use unrestricted `Arbitrary` values + /// instead. + fn any_bounded_trailing_layout() -> TrailingSliceLayout { + let elem_size: usize = kani::any(); + let offset: usize = kani::any(); + let size_base: usize = kani::any(); + let size_align = any_layout_align(); + let raw_size_phase: usize = kani::any(); + // Since `size_align` is a power of two, masking is surjective over + // precisely the values in `0..size_align`. + #[allow(clippy::arithmetic_side_effects)] + let size_phase = raw_size_phase & (size_align.get() - 1); + + kani::assume(elem_size <= DstLayout::MAX_SIZE); + kani::assume(offset <= DstLayout::MAX_SIZE); + kani::assume(size_base <= DstLayout::MAX_SIZE); + + TrailingSliceLayout { + elem_size, + offset, + size_base, + size_rounding_align_and_phase: RoundingAlignAndPhase::new(size_align, size_phase), + } + } + + #[kani::proof] + fn prove_padding_for_elems_for_rust_layouts() { + // For each fixture, check every element count whose complete size + // fits in `usize`, including all counts for zero-sized elements. + padding_testutil::check_layouts(kani::any()); + } + + #[kani::proof] + #[kani::solver(kissat)] + fn prove_padding_for_elems() { + // Check the modular result for every field value and element count, + // including layouts that cannot describe a Rust type. + let layout = TrailingSliceLayout { + offset: kani::any(), + elem_size: kani::any(), + size_base: kani::any(), + size_rounding_align_and_phase: RoundingAlignAndPhase(kani::any()), + }; + let elems: usize = kani::any(); + let (size_align, size_phase) = layout.size_rounding_align_and_phase.components(); + let mask = size_align.get() - 1; + + // Evaluate the complete size and trailing-slice end separately in + // modular arithmetic, without reducing the trailing bytes first or + // canceling them from the difference. + let trailing_bytes = elems.wrapping_mul(layout.elem_size); + let unrounded = size_phase.wrapping_add(trailing_bytes); + let rounded = unrounded.wrapping_add(mask) & !mask; + let object_size = layout.size_base.wrapping_add(rounded); + let trailing_end = layout.offset.wrapping_add(trailing_bytes); + assert_eq!(layout.padding_for_elems(elems), object_size.wrapping_sub(trailing_end)); + } + + #[kani::proof] + fn prove_size_rounding_align_and_phase_encoding() { + let encoded: usize = kani::any(); + kani::assume(encoded != 0); + let encoded = NonZeroUsize::new(encoded).unwrap(); + let rounding = RoundingAlignAndPhase(encoded); + + let align = rounding.align(); + let phase = rounding.components().1; + assert!(align.is_power_of_two()); + assert!(phase < align.get()); + assert_eq!(RoundingAlignAndPhase::new(align, phase), rounding); + } + + #[kani::proof] + fn prove_size_rounding_align_and_phase_decoding() { + let align: NonZeroUsize = kani::any(); + let phase: usize = kani::any(); + kani::assume(align.is_power_of_two()); + kani::assume(phase < align.get()); + + let rounding = RoundingAlignAndPhase::new(align, phase); + assert_eq!(rounding.components(), (align, phase)); + } + + #[kani::proof] + fn prove_trailing_slice_layout_normal_form() { + let rounding_word: usize = kani::any(); + kani::assume(rounding_word != 0); + let align = RoundingAlignAndPhase(NonZeroUsize::new(rounding_word).unwrap()).align(); + + let raw_prefix: usize = kani::any(); + let offset: usize = kani::any(); + let trailing_size: usize = kani::any(); + #[allow(clippy::arithmetic_side_effects)] + let prefix = raw_prefix & (align.get() - 1); + #[allow(clippy::arithmetic_side_effects)] + let phase = offset & (align.get() - 1); + #[allow(clippy::arithmetic_side_effects)] + let aligned_offset = offset - phase; + let layout = TrailingSliceLayout { + offset: 0, + elem_size: 1, + size_base: aligned_offset | prefix, + size_rounding_align_and_phase: RoundingAlignAndPhase::new(align, phase), + }; + + assert_eq!(layout.size_offset(), offset); + + let old_size = offset + .checked_add(trailing_size) + .and_then(|without_padding| { + without_padding.checked_add(util::padding_needed_for(without_padding, align)) + }) + .and_then(|rounded| prefix.checked_add(rounded)); + assert_eq!(layout.size_for_elems(trailing_size), old_size); + assert_eq!(slice_dst_size_for_trailing_bytes(layout, trailing_size), old_size); + } + + #[kani::proof] + #[kani::solver(kissat)] + fn prove_size_for_elems_uses_element_product() { + let layout: TrailingSliceLayout = any_bounded_trailing_layout(); + let elems: usize = kani::any(); + let expected = layout + .elem_size + .checked_mul(elems) + .and_then(|bytes| slice_dst_size_for_trailing_bytes(layout, bytes)); + assert_eq!(layout.size_for_elems(elems), expected); + } + + #[kani::proof] + #[kani::solver(kissat)] + fn prove_element_product_preserves_alignment() { + let elem_size: usize = kani::any(); + let elems: usize = kani::any(); + let align = any_layout_align(); + #[allow(clippy::arithmetic_side_effects)] + let align_mask = align.get() - 1; + + kani::assume(elem_size & align_mask == 0); + let Some(bytes) = elem_size.checked_mul(elems) else { + kani::assume(false); + loop {} + }; + assert_eq!(bytes & align_mask, 0); + } + + #[kani::proof] + fn prove_trailing_slice_layout_advance() { + let layout: TrailingSliceLayout = any_bounded_trailing_layout(); + let bytes: usize = kani::any(); + let elem_size: usize = kani::any(); + let trailing_size: usize = kani::any(); + + let size_offset = layout.size_offset(); + let advanced_offset = size_offset.checked_add(bytes); + let advanced = layout.advance(bytes, elem_size); + assert_eq!(advanced.is_some(), advanced_offset.is_some()); + let Some(advanced_offset) = advanced_offset else { + kani::assume(false); + loop {} + }; + let Some(advanced) = advanced else { unreachable!() }; + + assert_eq!(advanced.offset, layout.offset); + assert_eq!(advanced.size_offset(), advanced_offset); + assert_eq!(advanced.elem_size, elem_size); + assert_eq!( + advanced.size_rounding_align_and_phase.align(), + layout.size_rounding_align_and_phase.align() + ); + #[allow(clippy::arithmetic_side_effects)] + let size_align_mask = advanced.size_rounding_align_and_phase.align().get() - 1; + assert_eq!(advanced.size_base & size_align_mask, layout.size_base & size_align_mask); + + let size_align = layout.size_rounding_align_and_phase.align(); + #[allow(clippy::arithmetic_side_effects)] + let size_prefix = layout.size_base & (size_align.get() - 1); + let expected_size = advanced_offset + .checked_add(trailing_size) + .and_then(|without_padding| { + without_padding.checked_add(util::padding_needed_for(without_padding, size_align)) + }) + .and_then(|rounded| size_prefix.checked_add(rounded)); + assert_eq!(slice_dst_size_for_trailing_bytes(advanced, trailing_size), expected_size); + } + + #[kani::proof] + fn prove_has_same_size_sequence() { + let left: TrailingSliceLayout = any_bounded_trailing_layout(); + let right: TrailingSliceLayout = any_bounded_trailing_layout(); + let trailing_size: usize = kani::any(); + + kani::assume(left.has_same_size_sequence(right)); + let left_align = left.size_rounding_align_and_phase.align().get(); + let right_align = right.size_rounding_align_and_phase.align().get(); + let max_align = if left_align > right_align { left_align } else { right_align }; + #[allow(clippy::arithmetic_side_effects)] + let max_align_mask = max_align - 1; + if left.elem_size & max_align_mask == 0 { + // In this branch `has_same_size_sequence` relies on every actual + // trailing byte count being a multiple of both alignments. Prove + // the formula for that entire byte class, which includes every + // representable `elems * elem_size`, without introducing a + // nonlinear symbolic multiplication into the harness. + kani::assume(trailing_size & max_align_mask == 0); + } + assert_eq!( + slice_dst_size_for_trailing_bytes(left, trailing_size), + slice_dst_size_for_trailing_bytes(right, trailing_size) + ); + } + + #[kani::proof] + #[kani::solver(kissat)] + fn prove_max_trailing_bytes() { + let trailing: TrailingSliceLayout = any_bounded_trailing_layout(); + let available_bytes: usize = kani::any(); + let trailing_bytes: usize = kani::any(); + let fits = slice_dst_size_for_trailing_bytes(trailing, trailing_bytes) + .map(|size| size <= available_bytes) + .unwrap_or(false); + match trailing.max_trailing_bytes(available_bytes) { + Some(max_bytes) => assert_eq!(trailing_bytes <= max_bytes, fits), + None => assert!(!fits), + } + } + + #[kani::proof] + #[kani::solver(kissat)] + fn prove_max_trailing_bytes_components() { + let trailing: TrailingSliceLayout = any_bounded_trailing_layout(); + let bytes_len: usize = kani::any(); + let Some(max_slice_bytes) = trailing.max_trailing_bytes(bytes_len) else { + kani::assume(false); + loop {} + }; + let (size_align, size_phase) = trailing.size_rounding_align_and_phase.components(); + let Some(bytes_after_base) = bytes_len.checked_sub(trailing.size_base) else { + unreachable!() + }; + let max_rounded_bytes = + util::round_down_to_next_multiple_of_alignment(bytes_after_base, size_align); + assert_eq!(size_phase.checked_add(max_slice_bytes), Some(max_rounded_bytes)); + assert!(trailing.size_base.checked_add(max_rounded_bytes).unwrap() <= bytes_len); + } + + #[kani::proof] + #[kani::solver(kissat)] + fn prove_slice_dst_validator_selected_size_fits() { + let size_align = any_layout_align(); + #[allow(clippy::arithmetic_side_effects)] + let align_mask = size_align.get() - 1; + let raw_phase: usize = kani::any(); + let size_phase = raw_phase & align_mask; + let raw_capacity: usize = kani::any(); + let max_rounded_bytes = raw_capacity & !align_mask; + let size_base: usize = kani::any(); + let bytes_len: usize = kani::any(); + let trailing_size: usize = kani::any(); + + // These are exactly the relations established for every successful + // capacity query by `prove_max_trailing_bytes_components`. Isolate + // them from the helper's implementation to keep this arithmetic + // identity independent of how that helper computes its bound. + let Some(max_slice_bytes) = max_rounded_bytes.checked_sub(size_phase) else { + kani::assume(false); + loop {} + }; + let Some(max_object_size) = size_base.checked_add(max_rounded_bytes) else { + kani::assume(false); + loop {} + }; + kani::assume(max_object_size <= bytes_len); + kani::assume(trailing_size <= max_slice_bytes); + + // For a whole-element count selected by the validator, this unused + // byte count is also `max_slice_bytes % elem_size`. The identity here + // holds for every byte count satisfying the capacity bound. + #[allow(clippy::arithmetic_side_effects)] + let unused_bytes = max_slice_bytes - trailing_size; + let unused_aligned_bytes = + util::round_down_to_next_multiple_of_alignment(unused_bytes, size_align); + #[allow(clippy::arithmetic_side_effects)] + let object_size = size_base + (max_rounded_bytes - unused_aligned_bytes); + + let unrounded = size_phase.checked_add(trailing_size).unwrap(); + let rounded = + unrounded.checked_add(util::padding_needed_for(unrounded, size_align)).unwrap(); + assert_eq!(Some(object_size), size_base.checked_add(rounded)); + assert!(object_size <= bytes_len); + } + + #[kani::proof] + #[kani::solver(kissat)] + fn prove_slice_dst_validator_rejects_larger_trailing_size() { + let trailing: TrailingSliceLayout = any_bounded_trailing_layout(); + let bytes_len: usize = kani::any(); + let larger_trailing_size: usize = kani::any(); + let Some(max_slice_bytes) = trailing.max_trailing_bytes(bytes_len) else { + kani::assume(false); + loop {} + }; + kani::assume(larger_trailing_size > max_slice_bytes); + if let Some(object_size) = slice_dst_size_for_trailing_bytes(trailing, larger_trailing_size) + { + assert!(object_size > bytes_len); + } + } + + #[kani::proof] + fn prove_slice_formula_contains_trailing_slice() { + let trailing: TrailingSliceLayout = any_bounded_trailing_layout(); + let trailing_size: usize = kani::any(); + let (_, size_phase) = trailing.size_rounding_align_and_phase.components(); + + let Some(unrounded_min) = trailing.size_base.checked_add(size_phase) else { + kani::assume(false); + loop {} + }; + kani::assume(unrounded_min >= trailing.offset); + let Some(object_size) = slice_dst_size_for_trailing_bytes(trailing, trailing_size) else { + kani::assume(false); + loop {} + }; + + let Some(trailing_end) = trailing.offset.checked_add(trailing_size) else { unreachable!() }; + assert!(trailing_end <= object_size); + } + + #[kani::proof] + fn prove_finalized_slice_formula_is_aligned() { + let trailing: TrailingSliceLayout = any_bounded_trailing_layout(); + let outer_align = any_layout_align(); + let trailing_size: usize = kani::any(); + kani::assume(trailing.size_rounding_align_and_phase.align() >= outer_align); + #[allow(clippy::arithmetic_side_effects)] + let outer_align_mask = outer_align.get() - 1; + kani::assume(trailing.size_base & outer_align_mask == 0); + let Some(object_size) = slice_dst_size_for_trailing_bytes(trailing, trailing_size) else { + kani::assume(false); + loop {} + }; + + assert_eq!(object_size & outer_align_mask, 0); + } + + #[kani::proof] + fn prove_validator_checks_selected_endpoint_alignment() { + let align = any_layout_align(); + let addr: usize = kani::any(); + let bytes_len: usize = kani::any(); + let Some(end) = addr.checked_add(bytes_len) else { + kani::assume(false); + loop {} + }; + let suffix: bool = kani::any(); + let cast_type = if suffix { CastType::Suffix } else { CastType::Prefix }; + let selected_endpoint = if suffix { end } else { addr }; + #[allow(clippy::arithmetic_side_effects)] + let align_mask = align.get() - 1; + + // The endpoint check precedes and is independent of the size-info + // branch. A zero-sized layout makes every aligned case succeed and + // isolates that control flow, including prefix/suffix split selection. + let layout = DstLayout { + align, + size_info: SizeInfo::Sized { size: 0 }, + statically_shallow_unpadded: false, + }; + match layout.validate_cast_and_convert_metadata(addr, bytes_len, cast_type) { + Ok((elems, split_at)) => { + assert_eq!(selected_endpoint & align_mask, 0); + assert_eq!(elems, 0); + if suffix { + assert_eq!(split_at, bytes_len); + } else { + assert_eq!(split_at, 0); + } + } + Err(MetadataCastError::Alignment) => { + assert_ne!(selected_endpoint & align_mask, 0); + } + Err(MetadataCastError::Size) => unreachable!(), + } + } + + #[kani::proof] + fn prove_aligned_suffix_end_and_size_imply_aligned_start() { + let align = any_layout_align(); + #[allow(clippy::arithmetic_side_effects)] + let align_mask = align.get() - 1; + let addr: usize = kani::any(); + let bytes_len: usize = kani::any(); + let object_size: usize = kani::any(); + let Some(end) = addr.checked_add(bytes_len) else { + kani::assume(false); + loop {} + }; + kani::assume(end & align_mask == 0); + kani::assume(object_size & align_mask == 0); + kani::assume(object_size <= bytes_len); + + #[allow(clippy::arithmetic_side_effects)] + let split_at = bytes_len - object_size; + let Some(object_start) = addr.checked_add(split_at) else { unreachable!() }; + assert_eq!(object_start & align_mask, 0); + } + + #[kani::proof] + #[kani::solver(kissat)] + fn prove_size_formula_bounds_size_offset() { + let trailing: TrailingSliceLayout = any_bounded_trailing_layout(); + let elems: usize = kani::any(); + let Some(object_size) = trailing.size_for_elems(elems) else { + kani::assume(false); + loop {} + }; + let Some(trailing_size) = trailing.elem_size.checked_mul(elems) else { unreachable!() }; + let size_offset = trailing.size_offset(); + let Some(without_padding) = size_offset.checked_add(trailing_size) else { unreachable!() }; + + assert!(without_padding <= object_size); + } + + // Older supported compilers resolve the unchecked operation syntax in + // `add_scaled_metadata` to `polyfills::NumExt`; newer compilers prefer the + // inherent methods. Use UFCS to verify the fallback implementation which + // Kani's modern rustc cannot select in the production helper. + #[kani::proof] + #[kani::solver(kissat)] + fn prove_num_ext_affine_fallback_matches_checked_arithmetic() { + let base: usize = kani::any(); + let metadata: usize = kani::any(); + let multiple: usize = kani::any(); + let Some(scaled) = metadata.checked_mul(multiple) else { + kani::assume(false); + loop {} + }; + let Some(expected) = base.checked_add(scaled) else { + kani::assume(false); + loop {} + }; + + // SAFETY: The checked multiplication returned `Some`, satisfying + // `NumExt::unchecked_mul`'s no-overflow contract. + let scaled = + unsafe { ::unchecked_mul(metadata, multiple) }; + // SAFETY: The checked addition returned `Some`, and `scaled` is the + // checked product, satisfying `NumExt::unchecked_add`'s no-overflow + // contract. + let actual = + unsafe { ::unchecked_add(base, scaled) }; + assert_eq!(actual, expected); + } + + #[kani::proof] + fn prove_requires_dynamic_padding() { + let layout: DstLayout = any_valid_dst_layout(); + + let SizeInfo::SliceDst(size_info) = layout.size_info else { + kani::assume(false); + loop {} + }; + + let meta: usize = kani::any(); + + let Some(trailing_slice_size) = size_info.elem_size.checked_mul(meta) else { + // The `trailing_slice_size` exceeds `usize::MAX`; `meta` is invalid. + kani::assume(false); + loop {} + }; + + let Some(trailing_slice_end) = size_info.offset.checked_add(trailing_slice_size) else { + // The trailing slice end exceeds `usize::MAX`; `meta` is invalid. + kani::assume(false); + loop {} + }; + + let Some(size) = slice_dst_size_for_trailing_bytes(size_info, trailing_slice_size) else { + kani::assume(false); + loop {} + }; + + if size > DstLayout::MAX_SIZE || trailing_slice_end > size { + // This metadata cannot describe a valid instance. + kani::assume(false); + loop {} + } + + if !layout.requires_dynamic_padding() { + assert_eq!(trailing_slice_end, size); + } + } + + #[kani::proof] + fn prove_dst_layout_extend() { + use crate::util::{max, min, padding_needed_for}; + + let base: DstLayout = any_valid_dst_layout(); + let field: DstLayout = any_valid_dst_layout(); + let packed: Option = kani::any(); + + if let Some(max_align) = packed { + kani::assume(max_align.is_power_of_two()); + kani::assume(base.align <= max_align); + } + + // The base can only be extended if it's sized. + kani::assume(matches!(base.size_info, SizeInfo::Sized { .. })); + let base_size = if let SizeInfo::Sized { size } = base.size_info { + size + } else { + unreachable!(); + }; + + // The field's alignment is clamped by `max_align` (i.e., the + // `packed` attribute, if any) [1]. + // + // [1] Per https://doc.rust-lang.org/reference/type-layout.html#the-alignment-modifiers: + // + // The alignments of each field, for the purpose of positioning + // fields, is the smaller of the specified alignment and the + // alignment of the field's type. + let field_align = min(field.align, packed.unwrap_or(DstLayout::THEORETICAL_MAX_ALIGN)); + + // Compute the minimum amount of inter-field padding needed to + // satisfy the field's alignment, and offset of the trailing field. + // [1] + // + // [1] Per https://doc.rust-lang.org/reference/type-layout.html#the-alignment-modifiers: + // + // Inter-field padding is guaranteed to be the minimum required in + // order to satisfy each field's (possibly altered) alignment. + let padding = padding_needed_for(base_size, field_align); + let offset = match base_size.checked_add(padding) { + Some(offset) => offset, + None => { + kani::assume(false); + loop {} + } + }; + + // Restrict the proof to extensions whose zero-metadata composite size + // is representable. This semantic condition implies the + // representability of each implementation intermediate below; prove + // those implications rather than assuming the intermediates. + match field.size_info { + SizeInfo::Sized { size } => kani::assume(offset.checked_add(size).is_some()), + SizeInfo::SliceDst(trailing) => { + let Some(field_min) = trailing.size_for_elems(0) else { + kani::assume(false); + loop {} + }; + kani::assume(offset.checked_add(field_min).is_some()); + + assert!(offset.checked_add(trailing.offset).is_some()); + assert!(offset.checked_add(trailing.size_base).is_some()); + } + } + + // Under the above conditions, `DstLayout::extend` will not panic. + let composite = base.extend(field, packed); + + // The struct's alignment is the maximum of its previous alignment and + // `field_align`. + assert_eq!(composite.align, max(base.align, field_align)); + + // For testing purposes, we'll also construct `alloc::Layout` + // stand-ins for `DstLayout`, and show that `extend` behaves + // comparably on both types. + let base_analog = Layout::from_size_align(base_size, base.align.get()).unwrap(); + + match field.size_info { + SizeInfo::Sized { size: field_size } => { + if let SizeInfo::Sized { size: composite_size } = composite.size_info { + // If the trailing field is sized, the resulting layout will + // be sized. Its size will be the sum of the preceding + // layout, the size of the new field, and the size of + // inter-field padding between the two. + assert_eq!(composite_size, offset + field_size); + + let field_analog = + Layout::from_size_align(field_size, field_align.get()).unwrap(); + + if let Ok((actual_composite, actual_offset)) = base_analog.extend(field_analog) + { + assert_eq!(actual_offset, offset); + assert_eq!(actual_composite.size(), composite_size); + assert_eq!(actual_composite.align(), composite.align.get()); + } else { + // An error here reflects that composite of `base` + // and `field` cannot correspond to a real Rust type + // fragment, because such a fragment would violate + // the basic invariants of a valid Rust layout. At + // the time of writing, `DstLayout` is a little more + // permissive than `Layout`, so we don't assert + // anything in this branch (e.g., unreachability). + } + } else { + panic!("The composite of two sized layouts must be sized.") + } + } + SizeInfo::SliceDst(field_trailing) => { + if let SizeInfo::SliceDst(composite_trailing) = composite.size_info { + // The offset of the trailing slice component is the sum + // of the offset of the trailing field and the trailing + // slice offset within that field. + assert_eq!(composite_trailing.offset, offset + field_trailing.offset); + // The elem size is unchanged. + assert_eq!(composite_trailing.elem_size, field_trailing.elem_size); + // Extension preserves the invariant that the physical + // trailing-slice field fits within the unrounded minimum + // size. This is one of the premises consumed by the + // validator soundness proof. + let composite_phase = + composite_trailing.size_rounding_align_and_phase.components().1; + let Some(composite_unrounded_min) = + composite_trailing.size_base.checked_add(composite_phase) + else { + unreachable!() + }; + assert!(composite_unrounded_min >= composite_trailing.offset); + + // The normalized formula must continue to describe the + // size of the field at every trailing byte contribution + // for which the checked size computation succeeds. This + // is stronger than considering only products of a slice + // length and `elem_size`, and avoids a nonlinear + // multiplication in the verifier. + let trailing_size: usize = kani::any(); + let field_size = + match slice_dst_size_for_trailing_bytes(field_trailing, trailing_size) { + Some(size) => size, + None => { + kani::assume(false); + loop {} + } + }; + let expected_size = match offset.checked_add(field_size) { + Some(size) => size, + None => { + kani::assume(false); + loop {} + } + }; + assert_eq!( + slice_dst_size_for_trailing_bytes(composite_trailing, trailing_size), + Some(expected_size) + ); + + // `alloc::Layout` permits sizes which are not a multiple + // of alignment, so this represents the unpadded prefix of + // the DST through the start of its trailing slice. + let field_analog = + Layout::from_size_align(field_trailing.offset, field_align.get()).unwrap(); + + if let Ok((actual_composite, actual_offset)) = base_analog.extend(field_analog) + { + assert_eq!(actual_offset, offset); + assert_eq!(actual_composite.size(), composite_trailing.offset); + assert_eq!(actual_composite.align(), composite.align.get()); + } else { + // An error here reflects that composite of `base` + // and `field` cannot correspond to a real Rust type + // fragment, because such a fragment would violate + // the basic invariants of a valid Rust layout. At + // the time of writing, `DstLayout` is a little more + // permissive than `Layout`, so we don't assert + // anything in this branch (e.g., unreachability). + } + } else { + panic!("The extension of a layout with a DST must result in a DST.") + } + } + } + } + + #[kani::proof] + #[kani::should_panic] + fn prove_dst_layout_extend_dst_panics() { + let base: DstLayout = any_valid_dst_layout(); + let field: DstLayout = any_valid_dst_layout(); + let packed: Option = kani::any(); + + if let Some(max_align) = packed { + kani::assume(max_align.is_power_of_two()); + kani::assume(base.align <= max_align); + } + + kani::assume(matches!(base.size_info, SizeInfo::SliceDst(..))); + + let _ = base.extend(field, packed); + } + + fn prove_dst_layout_pad_to_align_dst_invariants( + layout: DstLayout, + unpadded_trailing: TrailingSliceLayout, + padded: DstLayout, + ) { + // Calling `pad_to_align` does not alter the `DstLayout`'s alignment. + assert_eq!(padded.align, layout.align); + assert_eq!(padded.statically_shallow_unpadded, layout.statically_shallow_unpadded); + + let SizeInfo::SliceDst(padded_trailing) = padded.size_info else { + panic!("The padding of a DST layout must result in a DST layout.") + }; + assert_eq!(padded_trailing.offset, unpadded_trailing.offset); + assert_eq!(padded_trailing.elem_size, unpadded_trailing.elem_size); + + // A finalized DST formula is visibly aligned: the rounding alignment + // is at least the outer alignment, and the fixed base is a multiple of + // the outer alignment. This is the invariant which lets suffix + // validation infer alignment of the object's start from alignment of + // its end. + assert!(padded_trailing.size_rounding_align_and_phase.align() >= padded.align); + #[allow(clippy::arithmetic_side_effects)] + let padded_align_mask = padded.align.get() - 1; + assert_eq!(padded_trailing.size_base & padded_align_mask, 0); + assert_eq!(padded.pad_to_align(), padded); + let padded_phase = padded_trailing.size_rounding_align_and_phase.components().1; + let Some(padded_unrounded_min) = padded_trailing.size_base.checked_add(padded_phase) else { + unreachable!() + }; + assert!(padded_unrounded_min >= padded_trailing.offset); + } + + #[kani::proof] + fn prove_dst_layout_pad_to_align_sized() { + use crate::util::padding_needed_for; + + let layout: DstLayout = any_valid_dst_layout(); + let SizeInfo::Sized { size: unpadded_size } = layout.size_info else { + kani::assume(false); + loop {} + }; + + let padded = layout.pad_to_align(); + assert_eq!(padded.align, layout.align); + let SizeInfo::Sized { size: padded_size } = padded.size_info else { + panic!("The padding of a sized layout must result in a sized layout.") + }; + + // If the layout is sized, it will remain sized after padding is added. + // Its sum will be its unpadded size and the size of the trailing + // padding needed to satisfy its alignment requirements. + let padding = padding_needed_for(unpadded_size, layout.align); + assert_eq!(padded_size, unpadded_size + padding); + assert_eq!( + padded.statically_shallow_unpadded, + layout.statically_shallow_unpadded && padding == 0 + ); + + // Prove that calling `DstLayout::pad_to_align` behaves identically to + // `Layout::pad_to_align`. + let layout_analog = Layout::from_size_align(unpadded_size, layout.align.get()).unwrap(); + let padded_analog = layout_analog.pad_to_align(); + assert_eq!(padded_analog.align(), layout.align.get()); + assert_eq!(padded_analog.size(), padded_size); + } + + #[kani::proof] + fn prove_dst_layout_pad_to_align_dst_inner_rounding_transition() { + use crate::util::padding_needed_for; + + let layout: DstLayout = any_valid_dst_layout(); + let SizeInfo::SliceDst(trailing) = layout.size_info else { + kani::assume(false); + loop {} + }; + kani::assume(trailing.size_rounding_align_and_phase.align() >= layout.align); + + // `any_valid_dst_layout` requires the zero-metadata layout to be valid. + // Prove that this semantic condition bounds both the padded minimum + // and the implementation intermediate; do not assume the intermediate + // merely because the implementation uses it. + let Some(min_size) = trailing.size_for_elems(0) else { unreachable!() }; + let min_padding = padding_needed_for(min_size, layout.align); + let Some(padded_min_size) = min_size.checked_add(min_padding) else { unreachable!() }; + assert!(padded_min_size <= DstLayout::MAX_SIZE); + + let padding = padding_needed_for(trailing.size_base, layout.align); + let Some(expected_base) = trailing.size_base.checked_add(padding) else { unreachable!() }; + + let padded = layout.pad_to_align(); + let SizeInfo::SliceDst(padded_trailing) = padded.size_info else { unreachable!() }; + assert_eq!(padded_trailing.size_base, expected_base); + assert_eq!( + padded_trailing.size_rounding_align_and_phase, + trailing.size_rounding_align_and_phase + ); + prove_dst_layout_pad_to_align_dst_invariants(layout, trailing, padded); + } + + #[kani::proof] + #[kani::solver(kissat)] + fn prove_dst_layout_pad_to_align_dst_inner_rounding_formula() { + use crate::util::padding_needed_for; + + let size_align = any_layout_align(); + let outer_align = any_layout_align(); + kani::assume(size_align >= outer_align); + let size_base: usize = kani::any(); + let raw_size_phase: usize = kani::any(); + #[allow(clippy::arithmetic_side_effects)] + let size_phase = raw_size_phase & (size_align.get() - 1); + let trailing_size: usize = kani::any(); + + // Evaluate `size_base + round_up(size_phase + trailing_size, + // size_align)`, then round that complete size to `outer_align`. + // Compare with the equivalent expression obtained by rounding only + // `size_base` to `outer_align`, since `size_align` is a multiple of + // `outer_align`. Valid Rust objects have representable sizes; discard + // only trailing byte counts whose input or padded object size is not + // representable. + let Some(unrounded_inner) = size_phase.checked_add(trailing_size) else { + kani::assume(false); + loop {} + }; + let Some(rounded_inner) = + unrounded_inner.checked_add(padding_needed_for(unrounded_inner, size_align)) + else { + kani::assume(false); + loop {} + }; + let Some(unpadded_size) = size_base.checked_add(rounded_inner) else { + kani::assume(false); + loop {} + }; + let Some(expected_size) = + unpadded_size.checked_add(padding_needed_for(unpadded_size, outer_align)) + else { + kani::assume(false); + loop {} + }; + + let Some(padded_base) = size_base.checked_add(padding_needed_for(size_base, outer_align)) + else { + unreachable!() + }; + let Some(actual_size) = padded_base.checked_add(rounded_inner) else { unreachable!() }; + assert_eq!(actual_size, expected_size); + } + + #[kani::proof] + fn prove_dst_layout_pad_to_align_dst_outer_alignment_transition() { + use crate::util::padding_needed_for; + + let layout: DstLayout = any_valid_dst_layout(); + let SizeInfo::SliceDst(trailing) = layout.size_info else { + kani::assume(false); + loop {} + }; + let size_align = trailing.size_rounding_align_and_phase.align(); + kani::assume(size_align < layout.align); + + // `any_valid_dst_layout` requires the zero-metadata layout to be valid. + // Prove that this semantic condition bounds both the padded minimum + // and every implementation intermediate; do not assume the + // intermediates merely because the implementation uses them. + let Some(min_size) = trailing.size_for_elems(0) else { unreachable!() }; + let min_padding = padding_needed_for(min_size, layout.align); + let Some(padded_min_size) = min_size.checked_add(min_padding) else { unreachable!() }; + assert!(padded_min_size <= DstLayout::MAX_SIZE); + + let base_padding = padding_needed_for(trailing.size_base, size_align); + let Some(rounded_base) = trailing.size_base.checked_add(base_padding) else { + unreachable!() + }; + let Some(t) = + rounded_base.checked_add(trailing.size_rounding_align_and_phase.components().1) + else { + unreachable!() + }; + #[allow(clippy::arithmetic_side_effects)] + let expected_phase = t & (layout.align.get() - 1); + #[allow(clippy::arithmetic_side_effects)] + let expected_base = t - expected_phase; + + let padded = layout.pad_to_align(); + let SizeInfo::SliceDst(padded_trailing) = padded.size_info else { unreachable!() }; + assert_eq!(padded_trailing.size_base, expected_base); + assert_eq!( + padded_trailing.size_rounding_align_and_phase, + RoundingAlignAndPhase::new(layout.align, expected_phase) + ); + prove_dst_layout_pad_to_align_dst_invariants(layout, trailing, padded); + } + + #[kani::proof] + #[kani::solver(kissat)] + fn prove_dst_layout_pad_to_align_dst_outer_alignment_formula() { + use crate::util::padding_needed_for; + + let size_align = any_layout_align(); + let outer_align = any_layout_align(); + kani::assume(size_align < outer_align); + let size_base: usize = kani::any(); + let raw_size_phase: usize = kani::any(); + #[allow(clippy::arithmetic_side_effects)] + let size_phase = raw_size_phase & (size_align.get() - 1); + let trailing_size: usize = kani::any(); + + // Evaluate `size_base + round_up(size_phase + trailing_size, + // size_align)`, then round that complete size to `outer_align`. + let Some(unrounded_inner) = size_phase.checked_add(trailing_size) else { + kani::assume(false); + loop {} + }; + let Some(rounded_inner) = + unrounded_inner.checked_add(padding_needed_for(unrounded_inner, size_align)) + else { + kani::assume(false); + loop {} + }; + let Some(unpadded_size) = size_base.checked_add(rounded_inner) else { + kani::assume(false); + loop {} + }; + let Some(expected_size) = + unpadded_size.checked_add(padding_needed_for(unpadded_size, outer_align)) + else { + kani::assume(false); + loop {} + }; + + // Since `outer_align` is a larger multiple of `size_align`, absorb + // the inner rounding into the outer rounding and split the fixed + // contribution into a base and phase: + // + // fixed_contribution = round_up(size_base, size_align) + size_phase + // new_phase = fixed_contribution % outer_align + // new_base = fixed_contribution - new_phase + // new_base + round_up(new_phase + trailing_size, outer_align) + let Some(rounded_base) = size_base.checked_add(padding_needed_for(size_base, size_align)) + else { + unreachable!() + }; + let Some(t) = rounded_base.checked_add(size_phase) else { unreachable!() }; + #[allow(clippy::arithmetic_side_effects)] + let new_phase = t & (outer_align.get() - 1); + #[allow(clippy::arithmetic_side_effects)] + let new_base = t - new_phase; + let Some(new_unrounded_inner) = new_phase.checked_add(trailing_size) else { + unreachable!() + }; + let Some(new_rounded_inner) = + new_unrounded_inner.checked_add(padding_needed_for(new_unrounded_inner, outer_align)) + else { + unreachable!() + }; + let Some(actual_size) = new_base.checked_add(new_rounded_inner) else { unreachable!() }; + assert_eq!(actual_size, expected_size); + } +} diff --git a/zerocopy/src/layout/nested_reference.rs b/zerocopy/src/layout/nested_reference.rs new file mode 100644 index 0000000000..f13b4e0e10 --- /dev/null +++ b/zerocopy/src/layout/nested_reference.rs @@ -0,0 +1,120 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +//! Independent arithmetic for nested `repr(C)` slice DSTs. +//! +//! The layers run from outermost to innermost. Packing caps placement +//! alignment, while every containing layer retains its field's complete size. +//! These descriptors also model arithmetic combinations Rust rejects as types, +//! including simultaneous packing and explicit alignment. No pointer or +//! allocation operation consumes these synthetic layouts. + +// The pinned extraction does not model Option's Try trait. Explicit matches +// also leave the overflow paths visible in this independent reference. +#![allow(clippy::question_mark, clippy::manual_map)] + +/// One containing record, before placing its final field. +/// +/// Plain words make the descriptor's mathematical decoding total. The Rust +/// functions below check the domain explicitly, including zero alignments. +#[derive(Copy, Clone)] +#[cfg_attr(test, derive(Debug))] +pub(super) struct NestedLayer { + pub(super) packed: usize, + pub(super) min_align: usize, + pub(super) prefix_bytes: usize, +} + +/// Returns the least multiple of `align` at least `size`, if representable. +/// +/// Zero alignment returns `None`. The remainder rule is independent of the +/// production padding utility and works for every positive alignment. +/// +/// ```aeneas +/// spec nested_round_up_spec +/// ensures(raw) rounded => rounded.map UScalar.val = +/// if align.val = 0 then none +/// else if LayoutMath.roundUp size.val align.val ≤ Usize.max +/// then some (LayoutMath.roundUp size.val align.val) else none +/// ``` +#[allow(clippy::arithmetic_side_effects)] +pub(super) fn round_up(size: usize, align: usize) -> Option { + if align == 0 { + return None; + } + let remainder = size % align; + if remainder == 0 { + Some(size) + } else { + size.checked_add(align - remainder) + } +} + +fn apply_layer(layer: NestedLayer, size: usize, alignment: usize) -> Option<(usize, usize)> { + if !layer.packed.is_power_of_two() + || !layer.min_align.is_power_of_two() + || layer.min_align > layer.packed + { + return None; + } + let field_align = if alignment < layer.packed { alignment } else { layer.packed }; + let offset = match round_up(layer.prefix_bytes, field_align) { + Some(offset) => offset, + None => return None, + }; + let alignment = if layer.min_align < field_align { field_align } else { layer.min_align }; + let unpadded_size = match offset.checked_add(size) { + Some(size) => size, + None => return None, + }; + let size = match round_up(unpadded_size, alignment) { + Some(size) => size, + None => return None, + }; + Some((size, alignment)) +} + +/// Computes complete recursive size at any element count. +/// +/// `None` means an invalid descriptor or `usize` overflow. An element size of +/// zero is valid at every power-of-two alignment. Sizes above `isize::MAX` +/// remain valid arithmetic results; no allocation is performed here. +/// +/// ```aeneas +/// spec nested_reference_size_spec +/// ensures(raw) result => result.map UScalar.val = +/// NestedReference.checkedSize leading.val elem_size.val leaf_align.val elems.val +/// ``` +#[allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)] +pub(super) fn size_for_metadata( + leading: &[NestedLayer], + elem_size: usize, + leaf_align: usize, + elems: usize, +) -> Option { + if !leaf_align.is_power_of_two() || elem_size % leaf_align != 0 { + return None; + } + let mut state = match elem_size.checked_mul(elems) { + Some(size) => Some((size, leaf_align)), + None => None, + }; + let mut i = leading.len(); + while i != 0 { + i -= 1; + let layer = leading[i]; + state = match state { + Some((size, alignment)) => apply_layer(layer, size, alignment), + None => None, + }; + } + match state { + Some((size, _)) => Some(size), + None => None, + } +} diff --git a/zerocopy/src/lib.rs b/zerocopy/src/lib.rs index 63bb4d5d5b..6040d6e0dd 100644 --- a/zerocopy/src/lib.rs +++ b/zerocopy/src/lib.rs @@ -778,10 +778,16 @@ pub unsafe trait KnownLayout { /// where `size == size_of::()` /// - If `Self` is a slice DST, then `LAYOUT.size_info == /// SizeInfo::SliceDst(slice_layout)` where: + /// - `slice_layout.size_rounding_align_and_phase` decodes to an alignment + /// that is a power of two and a phase which is less than that alignment /// - The size, `size`, of an instance of `Self` with `elems` trailing - /// slice elements is equal to `slice_layout.offset + - /// slice_layout.elem_size * elems` rounded up to the nearest multiple - /// of `LAYOUT.align` + /// slice elements is equal to `slice_layout.size_base + + /// round_up(size_phase + slice_layout.elem_size * elems, size_align)`, + /// where `(size_align, size_phase)` is decoded from + /// `slice_layout.size_rounding_align_and_phase` + /// - The trailing slice begins at byte offset `slice_layout.offset` + /// - `slice_layout.offset + slice_layout.elem_size * elems` does not + /// overflow and is no greater than `size` /// - For such an instance, any bytes in the range `[slice_layout.offset + /// slice_layout.elem_size * elems, size)` are padding and must not be /// assumed to be initialized @@ -996,11 +1002,7 @@ impl PointerMetadata for usize { #[inline] fn size_for_metadata(self, layout: DstLayout) -> Option { match layout.size_info { - SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size }) => { - let slice_len = elem_size.checked_mul(self)?; - let without_padding = offset.checked_add(slice_len)?; - without_padding.checked_add(util::padding_needed_for(without_padding, layout.align)) - } + SizeInfo::SliceDst(trailing) => trailing.size_for_elems(self), // NOTE: This branch is unreachable, but we return `None` rather // than `unreachable!()` to avoid generating panic paths. SizeInfo::Sized { .. } => None, @@ -6740,6 +6742,39 @@ mod tests { } } + // These types exercise a nested slice DST whose inner trailing-padding + // operation cannot be flattened into the alignment of the packed outer + // type. For `NestedPackedDstOuter<[u8]>`, the trailing slice begins at + // offset 7, but the size for `elems` elements is + // `2 + round_up(5 + elems, 4)`. + // + // The Reference rejects directly placing an alignment-4 field in a + // `repr(packed(2))` type. Rust currently accepts this construction through + // generic indirection. So long as Rust accepts it, this test deliberately + // treats its layout as following the otherwise-applicable `repr(C)` and + // `repr(packed)` rules. + #[derive(FromBytes, Immutable, KnownLayout)] + #[repr(C, align(4))] + struct NestedPackedDstWord([u8; 4]); + + #[derive(FromBytes, Immutable, KnownLayout)] + #[repr(C)] + struct NestedPackedDstInner { + a: NestedPackedDstWord, + b: u8, + tail: T, + } + + #[derive(FromBytes, Immutable, KnownLayout)] + #[repr(C, packed(2))] + struct NestedPackedDstOuter { + z: u8, + inner: ManuallyDrop>, + } + + #[repr(C, align(2))] + struct Align2(T); + #[test] fn test_known_layout() { // Test that `$ty` and `ManuallyDrop<$ty>` have the expected layout. @@ -6754,17 +6789,25 @@ mod tests { }; } - let layout = - |offset, align, trailing_slice_elem_size, statically_shallow_unpadded| DstLayout { - align: NonZeroUsize::new(align).unwrap(), + let layout = |offset, align, trailing_slice_elem_size, statically_shallow_unpadded| { + let align = NonZeroUsize::new(align).unwrap(); + let size_phase = offset % align; + DstLayout { + align, size_info: match trailing_slice_elem_size { None => SizeInfo::Sized { size: offset }, - Some(elem_size) => { - SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size }) - } + Some(elem_size) => SizeInfo::SliceDst(TrailingSliceLayout { + offset, + elem_size, + size_base: offset - size_phase, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + align, size_phase, + ), + }), }, statically_shallow_unpadded, - }; + } + }; test!((), layout(0, 1, None, false)); test!(u8, layout(1, 1, None, false)); @@ -6832,6 +6875,101 @@ mod tests { } } + #[test] + fn test_nested_packed_dst_layout() { + fn assert_native_size(expected: usize) { + let value = NestedPackedDstOuter { + z: 0, + inner: ManuallyDrop::new(NestedPackedDstInner { + a: NestedPackedDstWord([0; 4]), + b: 0, + tail: [0; N], + }), + }; + let value: &NestedPackedDstOuter<[u8]> = &value; + assert_eq!(mem::size_of_val(value), expected); + assert_eq!(NestedPackedDstOuter::<[u8]>::size_for_metadata(N), Some(expected)); + } + + assert_native_size::<0>(10); + assert_native_size::<1>(10); + assert_native_size::<2>(10); + assert_native_size::<3>(10); + assert_native_size::<4>(14); + assert_native_size::<7>(14); + assert_native_size::<8>(18); + + let layout = NestedPackedDstOuter::<[u8]>::LAYOUT; + assert_eq!(layout.align.get(), 2); + assert!(layout.requires_dynamic_padding()); + assert_eq!( + layout.size_info, + SizeInfo::SliceDst(TrailingSliceLayout { + offset: 7, + elem_size: 1, + size_base: 6, + size_rounding_align_and_phase: RoundingAlignAndPhase::new( + NonZeroUsize::new(4).unwrap(), + 1 + ), + }) + ); + + // Eight bytes used to be incorrectly accepted for a zero-element + // value. The actual Rust layout requires ten bytes. + let short = Align2([0u8; 8]); + assert!(NestedPackedDstOuter::<[u8]>::ref_from_bytes_with_elems(&short.0, 0).is_err()); + assert!(pointer::Ptr::from_ref(&short.0[..]) + .try_cast_into::, pointer::BecauseImmutable>( + CastType::Prefix, + Some(0), + ) + .is_err()); + + let full = Align2([0, 0, 0, 0, 0, 0, 0, 0, b'S', b'!']); + let value = NestedPackedDstOuter::<[u8]>::ref_from_bytes_with_elems(&full.0, 0).unwrap(); + assert_eq!(mem::size_of_val(value), 10); + + // When metadata is inferred, lengths 0 through 3 all produce a + // ten-byte object. Parsing chooses the largest trailing-slice length + // which fits exactly. + let inferred = NestedPackedDstOuter::<[u8]>::ref_from_bytes(&full.0).unwrap(); + assert_eq!(pointer::Ptr::from_ref(inferred).len(), 3); + + // Trailing padding includes the inner type's padding, even when the + // outer type's alignment is reduced by packing. + let trailing_layout = trailing_slice_layout::>(); + for (elems, expected) in [(0, 3), (1, 2), (2, 1), (3, 0), (4, 3)] { + assert_eq!(trailing_layout.padding_for_elems(elems), expected); + } + } + + // Before Rust 1.76, rust-lang/rust#118537 caused `addr_of_mut!` to + // miscompute the offset of an unsized field in a packed struct. The fix is + // rust-lang/rust#118540. Keep the layout and parsing regression above on + // all supported compilers, and gate only the projection regression which + // necessarily exercises that independent compiler bug. + #[rustversion::since(1.76.0)] + #[test] + fn test_nested_packed_dst_projection() { + let full = Align2([0, 0, 0, 0, 0, 0, 0, 0, b'S', b'!']); + + // The projected inner value occupies the final eight bytes of the full + // ten-byte input, including the two sentinel bytes. + let (value, rest) = pointer::Ptr::from_ref(&full.0[..]) + .try_cast_into::, pointer::BecauseImmutable>( + CastType::Prefix, + Some(0), + ) + .unwrap(); + assert!(rest.as_ref().is_empty()); + let inner = value.project::().unwrap(); + assert_eq!( + inner.as_bytes::().as_ref(), + &[0, 0, 0, 0, 0, 0, b'S', b'!'] + ); + } + #[cfg(feature = "derive")] #[test] fn test_known_layout_derive() { @@ -6887,10 +7025,19 @@ mod tests { statically_shallow_unpadded: false, }; - let unsized_layout = |align, elem_size, offset, statically_shallow_unpadded| DstLayout { - align: NonZeroUsize::new(align).unwrap(), - size_info: SizeInfo::SliceDst(TrailingSliceLayout { offset, elem_size }), - statically_shallow_unpadded, + let unsized_layout = |align, elem_size, offset, statically_shallow_unpadded| { + let align = NonZeroUsize::new(align).unwrap(); + let size_phase = offset % align; + DstLayout { + align, + size_info: SizeInfo::SliceDst(TrailingSliceLayout { + offset, + elem_size, + size_base: offset - size_phase, + size_rounding_align_and_phase: RoundingAlignAndPhase::new(align, size_phase), + }), + statically_shallow_unpadded, + } }; // | `repr(C)`? | generic? | `KnownLayout`? | `Sized`? | Type Name | @@ -7907,6 +8054,16 @@ mod tests { assert_eq!(&*s, &[0, 3, 0]); } + #[test] + fn test_new_box_zeroed_with_elems_nested_packed_dst() { + let mut value = NestedPackedDstOuter::<[u8]>::new_box_zeroed_with_elems(0).unwrap(); + assert_eq!(mem::size_of_val(&*value), 10); + + // Exercise the whole-object write that exposed the former + // under-allocation under Miri and AddressSanitizer. + value.zero(); + } + #[test] fn test_new_box_zeroed_with_elems_empty() { let s: Box<[u64]> = <[u64]>::new_box_zeroed_with_elems(0).unwrap(); diff --git a/zerocopy/src/macros.rs b/zerocopy/src/macros.rs index 5f7479aa1e..9e908a0c3a 100644 --- a/zerocopy/src/macros.rs +++ b/zerocopy/src/macros.rs @@ -1411,6 +1411,38 @@ mod tests { let x: &SliceDst = transmute_ref!(slice_dst_big); assert_eq!(x, slice_dst_small); + // A packed outer DST can preserve the trailing field's stronger + // rounding alignment. These two types consequently have different + // normalized layout formulas even though both have size `2 + 4 * n` + // for every trailing slice length. Ensure the semantic size-sequence + // comparison accepts this representation-preserving transmutation. + #[derive(FromBytes, Immutable, IntoBytes, KnownLayout)] + #[repr(C, align(2))] + struct TransmuteAlign2([u8; 4]); + + #[derive(FromBytes, Immutable, IntoBytes, KnownLayout)] + #[repr(C, packed(2))] + struct TransmutePacked4 { + head: u16, + tail: [u32], + } + + #[derive(FromBytes, Immutable, IntoBytes, KnownLayout)] + #[repr(C, packed(2))] + struct TransmutePacked2 { + head: u16, + tail: [TransmuteAlign2], + } + + #[repr(C, align(2))] + struct TransmuteBytes([u8; 10]); + + let bytes = TransmuteBytes([0; 10]); + let src = TransmutePacked4::ref_from_bytes(&bytes.0).unwrap(); + let dst: &TransmutePacked2 = transmute_ref!(src); + assert_eq!(mem::size_of_val(src), 10); + assert_eq!(mem::size_of_val(dst), 10); + // Test that it's legal to transmute a reference while shrinking the // lifetime (note that `X` has the lifetime `'static`). let x: &[u8; 8] = transmute_ref!(X); diff --git a/zerocopy/src/pointer/inner.rs b/zerocopy/src/pointer/inner.rs index 023df07d04..d926e1e520 100644 --- a/zerocopy/src/pointer/inner.rs +++ b/zerocopy/src/pointer/inner.rs @@ -289,10 +289,10 @@ where /// /// Given `let (left, right) = ptr.split_at(l_len)`, it is guaranteed that /// `left` and `right` are contiguous and non-overlapping if - /// `l_len.padding_needed_for() == 0`. This is true for all `[T]`. + /// the left part has no trailing padding. This is true for all `[T]`. /// - /// If `l_len.padding_needed_for() != 0`, then the left pointer will overlap - /// the right pointer to satisfy `T`'s padding requirements. + /// Any trailing padding in the left part begins at the right part's + /// starting address and may overlap its referent. #[inline] #[must_use] pub unsafe fn split_at_unchecked( @@ -313,23 +313,19 @@ where // Trivially, `slf.meta() <= slf.meta()`. let right = unsafe { right.slice_unchecked(l_len..self.meta().get()) }; - // SAFETY: If `l_len.padding_needed_for() == 0`, then `left` and `right` - // are non-overlapping. Proof: `left` is constructed `slf` with `l_len` - // as its (exclusive) upper bound. If `l_len.padding_needed_for() == 0`, - // then `left` requires no trailing padding following its final element. - // Since `right` is constructed from `slf`'s trailing slice with `l_len` - // as its (inclusive) lower bound, no byte is referred to by both - // pointers. + // SAFETY: If the left part has no trailing padding, then `left` and + // `right` are non-overlapping. Proof: `left` has `l_len` trailing + // elements, and `right` starts at element `l_len` of the original + // trailing slice. Without padding after `left`'s final element, no + // byte is referred to by both pointers. // - // Conversely, `l_len.padding_needed_for() == N`, where `N - // > 0`, `left` requires `N` bytes of trailing padding following its - // final element. Since `right` is constructed from the trailing slice - // of `slf` with `l_len` as its (inclusive) lower bound, the first `N` - // bytes of `right` are aliased by `left`. + // Any trailing padding in `left` begins at `right`'s starting + // address. If `right` is nonzero sized, the referents overlap. (left, right) } - /// Produces the trailing slice of `self`. + /// Produces the trailing slice of `self` using `T::Elem` as its element + /// representation. #[inline] #[must_use] pub fn trailing_slice(self) -> PtrInner<'a, [T::Elem]> @@ -368,9 +364,10 @@ where // 1. If `ptr`'s referent is not zero sized, then `ptr` has valid // provenance for `A` because `raw` is derived from the same // allocated object as `self` via provenance-preserving operations. - // 2. If `ptr`'s referent is not zero sized, then `ptr` addresses a byte - // range which is entirely contained in `A`, by previous safety proof - // on `bytes`. + // 2. `T: SplitAt` guarantees that `T::Elem` has the size of the actual + // trailing element. The new slice has the same length and start as + // the original trailing slice, so it covers the same byte range. + // If nonzero sized, that range is entirely contained in `A`. // 3. `ptr` addresses a byte range whose length fits in an `isize`, by // consequence of #2. // 4. `ptr` addresses a byte range which does not wrap around the diff --git a/zerocopy/src/pointer/mod.rs b/zerocopy/src/pointer/mod.rs index 03b17bf078..52ba00fd95 100644 --- a/zerocopy/src/pointer/mod.rs +++ b/zerocopy/src/pointer/mod.rs @@ -47,10 +47,7 @@ where pub mod cast { use core::{marker::PhantomData, mem}; - use crate::{ - layout::{SizeInfo, TrailingSliceLayout}, - HasField, KnownLayout, PtrInner, - }; + use crate::{layout::SizeInfo, HasField, KnownLayout, PtrInner}; /// A pointer cast or projection. /// @@ -182,11 +179,9 @@ pub mod cast { // SAFETY: By the `static_assert!`, `Src` and `Dst` are either: // - Both sized and equal in size - // - Both slice DSTs with the same trailing slice offset and element size - // and with align_of::() == align_of::(). These ensure that any - // given pointer metadata encodes the same size for both `Src` and `Dst` - // (note that the alignment is required as it affects the amount of - // trailing padding). Thus, `project` preserves the set of referent bytes. + // - Both slice DSTs with the same trailing-slice offset, the same object + // size for every metadata value, and with align_of::() == + // align_of::(). Thus, `project` preserves the set of referent bytes. unsafe impl Project for CastUnsized where Src: ?Sized + KnownLayout, @@ -201,10 +196,11 @@ pub mod cast { let dst = ::LAYOUT; match (src.size_info, dst.size_info) { (SizeInfo::Sized { size: src_size }, SizeInfo::Sized { size: dst_size }) => src_size == dst_size, - ( - SizeInfo::SliceDst(TrailingSliceLayout { offset: src_offset, elem_size: src_elem_size }), - SizeInfo::SliceDst(TrailingSliceLayout { offset: dst_offset, elem_size: dst_elem_size }) - ) => src.align.get() == dst.align.get() && src_offset == dst_offset && src_elem_size == dst_elem_size, + (SizeInfo::SliceDst(src_trailing), SizeInfo::SliceDst(dst_trailing)) => { + src.align.get() == dst.align.get() + && src_trailing.offset == dst_trailing.offset + && src_trailing.has_same_size_sequence(dst_trailing) + }, _ => false, } }); @@ -225,10 +221,8 @@ pub mod cast { // SAFETY: By the `static_assert!` in `Project::project`, `Src` and `Dst` // are either: // - Both sized and equal in size - // - Both slice DSTs with the same alignment, trailing slice offset, and - // element size. These ensure that any given pointer metadata encodes the - // same size for both `Src` and `Dst` (note that the alignment is required - // as it affects the amount of trailing padding). + // - Both slice DSTs with the same alignment and trailing-slice offset, and + // with equal sizes for every metadata value. unsafe impl CastExact for CastUnsized where Src: ?Sized + KnownLayout, diff --git a/zerocopy/src/split_at.rs b/zerocopy/src/split_at.rs index 720ba740c8..0a98c130d0 100644 --- a/zerocopy/src/split_at.rs +++ b/zerocopy/src/split_at.rs @@ -17,6 +17,12 @@ use crate::pointer::invariant::{Aligned, Exclusive, Invariants, Safe, Shared}; /// This trait generalizes Rust's existing support for splitting slices to /// support slices and slice-based dynamically-sized types ("slice DSTs"). /// +/// For a `repr(C, packed)` or `repr(C, packed(N))` struct, the derive wraps +/// the trailing field's [`Elem`](Self::Elem) in [`Unalign`]. The returned +/// slice can therefore be accessed even when packing misaligns the original +/// elements. Each packed struct in the trailing-field chain adds one wrapper; +/// unpacked structs and [`ManuallyDrop`] forward their trailing type's `Elem`. +/// /// # Implementation /// /// **Do not implement this trait yourself!** Instead, use @@ -38,6 +44,28 @@ use crate::pointer::invariant::{Aligned, Exclusive, Invariants, Safe, Shared}; /// This derive performs a sophisticated, compile-time safety analysis to /// determine whether a type is `SplitAt`. /// +/// A packed struct exposes its trailing elements through `Unalign`: +/// +/// ``` +/// use zerocopy::{FromBytes, SplitAt, Unalign}; +/// # use zerocopy_derive::*; +/// +/// #[derive(FromBytes, IntoBytes, KnownLayout, SplitAt)] +/// #[repr(C, packed)] +/// struct Packet { +/// tag: u8, +/// words: [u16], +/// } +/// +/// let mut bytes = [0u8; 5]; +/// let packet = Packet::mut_from_bytes(&mut bytes[..]).unwrap(); +/// let (left, right): (&mut Packet, &mut [Unalign]) = +/// packet.split_at_mut(1).unwrap().via_into_bytes(); +/// left.tag = 1; +/// right[0] = Unalign::new(0x0202); +/// assert_eq!(bytes, [1, 0, 0, 2, 2]); +/// ``` +/// /// # Safety /// /// This trait does not convey any safety guarantees to code outside this crate. @@ -58,10 +86,20 @@ use crate::pointer::invariant::{Aligned, Exclusive, Invariants, Safe, Shared}; )] // # Safety // -// The trailing slice is well-aligned for its element type. `Self` is `[T]`, or -// a `repr(C)` or `repr(transparent)` slice DST. +// `Self` is a slice, a `repr(C)` (possibly packed) or `repr(transparent)` slice +// DST, or `ManuallyDrop` for some `T: SplitAt`. +// +// `Self::Elem` has the same size, bit validity, and `UnsafeCell` coverage as +// the actual trailing element type. Access through either representation +// preserves the other's validity, including writes through mutable references +// and interior mutation through shared references. For every aligned `Self`, +// its trailing slice's address is aligned for `Self::Elem`. pub unsafe trait SplitAt: KnownLayout { - /// The element type of the trailing slice. + /// The element type exposed by the split's returned slice. + /// + /// For `[T]`, this is `T`. For a struct, this is the trailing field's `Elem`, + /// wrapped in [`Unalign`] if the struct is packed. Nested packed structs + /// produce nested `Unalign` wrappers. type Elem; #[doc(hidden)] @@ -188,10 +226,7 @@ pub unsafe trait SplitAt: KnownLayout { /// Attempts to split `self` in two. /// /// Returns `None` if `l_len` is greater than the length of `self`'s - /// trailing slice, or if the given `l_len` would result in [the trailing - /// padding](KnownLayout#slice-dst-layout) of the left portion overlapping - /// the right portion. - /// + /// trailing slice. /// /// # Examples /// @@ -249,7 +284,8 @@ pub unsafe trait SplitAt: KnownLayout { } } -// SAFETY: `[T]`'s trailing slice is `[T]`, which is trivially aligned. +// SAFETY: `[T]` exposes its own element type, preserving size, validity, and +// `UnsafeCell` coverage. An aligned slice is aligned for its elements. unsafe impl SplitAt for [T] { type Elem = T; @@ -262,6 +298,29 @@ unsafe impl SplitAt for [T] { } } +// SAFETY: `ManuallyDrop` has the same layout and bit validity as `T` [1]. +// Its `KnownLayout` implementation preserves `T`'s metadata and trailing-slice +// layout. Forwarding `T::Elem` therefore preserves its size, validity, and +// alignment guarantees. Its `UnsafeCell` coverage also matches `T`'s, as +// established for `ManuallyDrop` in `impls.rs`; accesses through the forwarded +// element representation preserve validity in both directions. +// +// [1] Per https://doc.rust-lang.org/1.93.0/std/mem/struct.ManuallyDrop.html: +// +// `ManuallyDrop` is guaranteed to have the same layout and bit validity as +// `T` +unsafe impl SplitAt for ManuallyDrop { + type Elem = T::Elem; + + #[inline] + #[allow(dead_code)] + fn only_derive_is_allowed_to_implement_this_trait() + where + Self: Sized, + { + } +} + /// A `T` that has been split into two possibly-overlapping parts. /// /// For some dynamically sized types, the padding that appears after the @@ -270,10 +329,11 @@ unsafe impl SplitAt for [T] { /// requires trailing padding, the trailing padding of the left part of the /// split `T` will overlap the right part. If `T` is a mutable reference or /// permits interior mutation, you must ensure that the left and right parts do -/// not overlap. You can do this at zero-cost using using -/// [`Self::via_immutable`], [`Self::via_into_bytes`], or -/// [`Self::via_unaligned`], or with a dynamic check by using -/// [`Self::via_runtime_check`]. +/// not overlap. Use [`Self::via_into_bytes`] or +/// [`Self::via_no_dynamic_padding`] to establish this without a runtime check, +/// or [`Self::via_runtime_check`] to check a particular split. Shared references +/// to an [`Immutable`] type may overlap; use [`Self::via_immutable`] in that +/// case. #[derive(Debug)] pub struct Split { /// A pointer to the source slice DST. @@ -423,59 +483,100 @@ where (l.as_ref(), r.as_ref()) } - /// Produces the split parts of `self`, using [`Unaligned`] to ensure that - /// it is sound to have concurrent references to both parts. + /// Produces the split parts of `self` using [`Self::via_no_dynamic_padding`]. + #[deprecated(note = "use `Split::via_no_dynamic_padding` instead")] + #[doc(hidden)] + #[must_use = "has no side effects"] + #[inline(always)] + pub fn via_unaligned(self) -> (&'a T, &'a [T::Elem]) + where + T: Unaligned, + { + self.via_no_dynamic_padding() + } + + /// Produces the split parts of `self` using a compile-time assertion to + /// ensure that it is unconditionally sound to have concurrent references to + /// both parts. + /// + /// When possible, you should prefer using [`Self::via_into_bytes`], which + /// ensures this safety property through regular trait bounds. /// /// # Examples /// + /// In the below example, a single byte of padding exists (on most + /// platforms) between `Packet<[u16]>`'s `length` and `body` fields, + /// precluding the use of [`Self::via_into_bytes`]. + /// /// ``` - /// use zerocopy::{SplitAt, FromBytes}; + /// use zerocopy::SplitAt; /// # use zerocopy_derive::*; /// - /// #[derive(SplitAt, FromBytes, KnownLayout, Immutable, Unaligned)] + /// #[derive(SplitAt, KnownLayout)] /// #[repr(C)] - /// struct Packet { + /// struct Packet { /// length: u8, - /// body: [u8], + /// body: B, /// } /// - /// // These bytes encode a `Packet`. - /// let bytes = &[4, 1, 2, 3, 4, 5, 6, 7, 8, 9][..]; - /// - /// let packet = Packet::ref_from_bytes(bytes).unwrap(); + /// let packet: &Packet<[u16]> = &Packet { length: 4, body: [1u16, 2, 3, 4, 5, 6, 7, 8, 9] }; /// /// assert_eq!(packet.length, 4); /// assert_eq!(packet.body, [1, 2, 3, 4, 5, 6, 7, 8, 9]); /// - /// // Attempt to split `packet` at `length`. - /// let split = packet.split_at(packet.length as usize).unwrap(); + /// if let Some(split) = packet.split_at(packet.length as usize) { + /// // Every body length requires no trailing padding. + /// let (packet, rest) = split.via_no_dynamic_padding(); + /// assert_eq!(packet.length, 4); + /// assert_eq!(packet.body, [1, 2, 3, 4]); + /// assert_eq!(rest, [5, 6, 7, 8, 9]); + /// } else { + /// unreachable!("The packet's length field is within its body"); + /// } + /// ``` /// - /// // Use the `Unaligned` bound on `Packet` to prove that it's okay to - /// // return concurrent references to `packet` and `rest`. - /// let (packet, rest) = split.via_unaligned(); + /// # Compile-Time Assertions /// - /// assert_eq!(packet.length, 4); - /// assert_eq!(packet.body, [1, 2, 3, 4]); - /// assert_eq!(rest, [5, 6, 7, 8, 9]); + /// This method rejects at compile-time any type that admits dynamic + /// trailing padding. In the below example, some `split_at` indices would + /// produce left parts whose trailing padding overlaps with the right part: + /// + /// ```compile_fail,E0080 + /// use zerocopy::SplitAt; + /// # use zerocopy_derive::*; + /// + /// #[derive(SplitAt, KnownLayout)] + /// #[repr(C, align(2))] + /// struct Packet { + /// length: u16, + /// body: B, + /// } + /// + /// let packet: &Packet<[u8]> = &Packet { length: 4, body: [1u8, 2, 3, 4, 5, 6, 7, 8, 9] }; + /// + /// if let Some(split) = packet.split_at(packet.length as usize) { + /// let _ = split.via_no_dynamic_padding(); // ⚠ Compile Error! + /// } else { + /// unreachable!("The packet's length field is within its body"); + /// } /// ``` /// - #[doc = codegen_header!("h5", "split_via_unaligned")] + /// If you need to split such types, use [`Self::via_runtime_check`]. + /// + #[doc = codegen_header!("h5", "split_via_no_dynamic_padding")] /// /// See [`Split::via_immutable`](#method.split_via_immutable.codegen). #[must_use = "has no side effects"] #[inline(always)] - pub fn via_unaligned(self) -> (&'a T, &'a [T::Elem]) - where - T: Unaligned, - { - let (l, r) = self.into_ptr().via_unaligned(); + pub fn via_no_dynamic_padding(self) -> (&'a T, &'a [T::Elem]) { + let (l, r) = self.into_ptr().via_no_dynamic_padding(); (l.as_ref(), r.as_ref()) } /// Produces the split parts of `self`, using a dynamic check to ensure that /// it is sound to have concurrent references to both parts. You should /// prefer using [`Self::via_immutable`], [`Self::via_into_bytes`], or - /// [`Self::via_unaligned`], which have no runtime cost. + /// [`Self::via_no_dynamic_padding`], which perform no runtime check. /// /// Note that this check is overly conservative if `T` is [`Immutable`]; for /// some types, this check will reject some splits which @@ -484,52 +585,40 @@ where /// # Examples /// /// ``` - /// use zerocopy::{SplitAt, FromBytes, IntoBytes, network_endian::U16}; + /// use zerocopy::SplitAt; /// # use zerocopy_derive::*; /// - /// #[derive(SplitAt, FromBytes, KnownLayout, Immutable, Debug)] + /// #[derive(SplitAt, KnownLayout)] /// #[repr(C, align(2))] - /// struct Packet { - /// length: U16, - /// body: [u8], + /// struct Packet { + /// length: u16, + /// body: B, /// } /// - /// // These bytes encode a `Packet`. - /// let bytes = [ - /// 4u16.to_be(), - /// 1u16.to_be(), - /// 2u16.to_be(), - /// 3u16.to_be(), - /// 4u16.to_be() - /// ]; - /// - /// let packet = Packet::ref_from_bytes(bytes.as_bytes()).unwrap(); + /// let packet: &Packet<[u8]> = &Packet { length: 4, body: [1u8, 2, 3, 4, 5, 6, 7, 8, 9] }; /// /// assert_eq!(packet.length, 4); - /// assert_eq!(packet.body, [0, 1, 0, 2, 0, 3, 0, 4]); - /// - /// // Attempt to split `packet` at `length`. - /// let split = packet.split_at(packet.length.into()).unwrap(); + /// assert_eq!(packet.body, [1, 2, 3, 4, 5, 6, 7, 8, 9]); /// - /// // Use a dynamic check to prove that it's okay to return concurrent - /// // references to `packet` and `rest`. - /// let (packet, rest) = split.via_runtime_check().unwrap(); + /// // The two-byte prefix and four-byte body need no trailing padding. + /// if let Some(split) = packet.split_at(packet.length as usize) { + /// if let Ok((packet, rest)) = split.via_runtime_check() { + /// assert_eq!(packet.length, 4); + /// assert_eq!(packet.body, [1, 2, 3, 4]); + /// assert_eq!(rest, [5, 6, 7, 8, 9]); + /// } else { + /// unreachable!("A four-byte body requires no trailing padding"); + /// } + /// } else { + /// unreachable!("The packet's length field is within its body"); + /// } /// - /// assert_eq!(packet.length, 4); - /// assert_eq!(packet.body, [0, 1, 0, 2]); - /// assert_eq!(rest, [0, 3, 0, 4]); - /// - /// // Attempt to split `packet` at `length - 1`. - /// let idx = packet.length.get() - 1; - /// let split = packet.split_at(idx as usize).unwrap(); - /// - /// // Attempt (and fail) to use a dynamic check to prove that it's okay - /// // to return concurrent references to `packet` and `rest`. Note that - /// // this is a case of `via_runtime_check` being overly conservative. - /// // Although the left and right parts indeed overlap, the `Immutable` - /// // bound ensures that concurrently referencing these overlapping - /// // parts is sound. - /// assert!(split.via_runtime_check().is_err()); + /// // A three-byte body needs a padding byte that would overlap `rest`. + /// if let Some(split) = packet.split_at((packet.length - 1) as usize) { + /// assert!(split.via_runtime_check().is_err()); + /// } else { + /// unreachable!("One less than the packet's length is within its body"); + /// } /// ``` /// #[doc = codegen_section!( @@ -665,101 +754,135 @@ where (l.as_mut(), r.as_mut()) } - /// Produces the split parts of `self`, using [`Unaligned`] to ensure that - /// it is sound to have concurrent references to both parts. + /// Produces the split parts of `self` using [`Self::via_no_dynamic_padding`]. + #[deprecated(note = "use `Split::via_no_dynamic_padding` instead")] + #[doc(hidden)] + #[must_use = "has no side effects"] + #[inline(always)] + pub fn via_unaligned(self) -> (&'a mut T, &'a mut [T::Elem]) + where + T: Unaligned, + { + self.via_no_dynamic_padding() + } + + /// Produces the split parts of `self` using a compile-time assertion to + /// ensure that it is unconditionally sound to have concurrent references to + /// both parts. + /// + /// When possible, you should prefer using [`Self::via_into_bytes`], which + /// ensures this safety property through regular trait bounds. /// /// # Examples /// + /// In the below example, a single byte of padding exists (on most + /// platforms) between `Packet<[u16]>`'s `length` and `body` fields, + /// precluding the use of [`Self::via_into_bytes`]. + /// /// ``` - /// use zerocopy::{SplitAt, FromBytes}; + /// use zerocopy::SplitAt; /// # use zerocopy_derive::*; /// - /// #[derive(SplitAt, FromBytes, KnownLayout, IntoBytes, Unaligned)] + /// #[derive(SplitAt, KnownLayout)] /// #[repr(C)] /// struct Packet { /// length: u8, /// body: B, /// } /// - /// // These bytes encode a `Packet`. - /// let mut bytes = &mut [4, 1, 2, 3, 4, 5, 6, 7, 8, 9][..]; - /// - /// let packet = Packet::<[u8]>::mut_from_bytes(bytes).unwrap(); + /// let packet: &mut Packet<[u16]> = &mut Packet { length: 4, body: [1u16, 2, 3, 4, 5, 6, 7, 8, 9] }; /// /// assert_eq!(packet.length, 4); /// assert_eq!(packet.body, [1, 2, 3, 4, 5, 6, 7, 8, 9]); /// - /// { - /// // Attempt to split `packet` at `length`. - /// let split = packet.split_at_mut(packet.length as usize).unwrap(); - /// - /// // Use the `Unaligned` bound on `Packet` to prove that it's okay to - /// // return concurrent references to `packet` and `rest`. - /// let (packet, rest) = split.via_unaligned(); - /// + /// // Every body length requires no trailing padding. + /// if let Some(split) = packet.split_at_mut(packet.length as usize) { + /// let (packet, rest) = split.via_no_dynamic_padding(); /// assert_eq!(packet.length, 4); /// assert_eq!(packet.body, [1, 2, 3, 4]); /// assert_eq!(rest, [5, 6, 7, 8, 9]); - /// /// rest.fill(0); + /// } else { + /// unreachable!("The packet's length field is within its body"); /// } /// /// assert_eq!(packet.length, 4); /// assert_eq!(packet.body, [1, 2, 3, 4, 0, 0, 0, 0, 0]); /// ``` /// + /// # Compile-Time Assertions + /// + /// This method rejects at compile-time any type that admits dynamic + /// trailing padding. In the below example, some `split_at` indices would + /// produce left parts whose trailing padding overlaps with the right part: + /// + /// ```compile_fail,E0080 + /// use zerocopy::SplitAt; + /// # use zerocopy_derive::*; + /// + /// #[derive(SplitAt, KnownLayout)] + /// #[repr(C, align(2))] + /// struct Packet { + /// length: u16, + /// body: B, + /// } + /// + /// let packet: &mut Packet<[u8]> = &mut Packet { length: 4, body: [1u8, 2, 3, 4, 5, 6, 7, 8, 9] }; + /// + /// if let Some(split) = packet.split_at_mut(packet.length as usize) { + /// let _ = split.via_no_dynamic_padding(); // ⚠ Compile Error! + /// } else { + /// unreachable!("The packet's length field is within its body"); + /// } + /// ``` + /// + /// If you need to split such types, use [`Self::via_runtime_check`]. + /// /// # Code Generation /// /// See [`Split::via_immutable`](#method.split_via_immutable.codegen). #[must_use = "has no side effects"] #[inline(always)] - pub fn via_unaligned(self) -> (&'a mut T, &'a mut [T::Elem]) - where - T: Unaligned, - { - let (l, r) = self.into_ptr().via_unaligned(); + pub fn via_no_dynamic_padding(self) -> (&'a mut T, &'a mut [T::Elem]) { + let (l, r) = self.into_ptr().via_no_dynamic_padding(); (l.as_mut(), r.as_mut()) } /// Produces the split parts of `self`, using a dynamic check to ensure that /// it is sound to have concurrent references to both parts. You should - /// prefer using [`Self::via_into_bytes`] or [`Self::via_unaligned`], which - /// have no runtime cost. + /// prefer using [`Self::via_into_bytes`] or + /// [`Self::via_no_dynamic_padding`], which perform no runtime check. /// /// # Examples /// /// ``` - /// use zerocopy::{SplitAt, FromBytes}; + /// use zerocopy::SplitAt; /// # use zerocopy_derive::*; /// - /// #[derive(SplitAt, FromBytes, KnownLayout, IntoBytes, Debug)] - /// #[repr(C)] + /// #[derive(SplitAt, KnownLayout)] + /// #[repr(C, align(2))] /// struct Packet { - /// length: u8, + /// length: u16, /// body: B, /// } /// - /// // These bytes encode a `Packet`. - /// let mut bytes = &mut [4, 1, 2, 3, 4, 5, 6, 7, 8, 9][..]; - /// - /// let packet = Packet::<[u8]>::mut_from_bytes(bytes).unwrap(); + /// let packet: &mut Packet<[u8]> = &mut Packet { length: 4, body: [1u8, 2, 3, 4, 5, 6, 7, 8, 9] }; /// /// assert_eq!(packet.length, 4); /// assert_eq!(packet.body, [1, 2, 3, 4, 5, 6, 7, 8, 9]); /// - /// { - /// // Attempt to split `packet` at `length`. - /// let split = packet.split_at_mut(packet.length as usize).unwrap(); - /// - /// // Use a dynamic check to prove that it's okay to return concurrent - /// // references to `packet` and `rest`. - /// let (packet, rest) = split.via_runtime_check().unwrap(); - /// - /// assert_eq!(packet.length, 4); - /// assert_eq!(packet.body, [1, 2, 3, 4]); - /// assert_eq!(rest, [5, 6, 7, 8, 9]); - /// - /// rest.fill(0); + /// // The two-byte prefix and four-byte body need no trailing padding. + /// if let Some(split) = packet.split_at_mut(packet.length as usize) { + /// if let Ok((packet, rest)) = split.via_runtime_check() { + /// assert_eq!(packet.length, 4); + /// assert_eq!(packet.body, [1, 2, 3, 4]); + /// assert_eq!(rest, [5, 6, 7, 8, 9]); + /// rest.fill(0); + /// } else { + /// unreachable!("A four-byte body requires no trailing padding"); + /// } + /// } else { + /// unreachable!("The packet's length field is within its body"); /// } /// /// assert_eq!(packet.length, 4); @@ -860,37 +983,57 @@ where unsafe { self.via_unchecked() } } - /// Produces the split parts of `self`, using [`Unaligned`] to ensure that - /// it is sound to have concurrent references to both parts. + /// Produces the split parts of `self` after the conservative layout + /// predicate establishes that `T` never requires dynamic trailing padding. #[inline(always)] - fn via_unaligned(self) -> (Ptr<'a, T, I>, Ptr<'a, [T::Elem], I>) - where - T: Unaligned, - { - // SAFETY: By `T: SplitAt + Unaligned`, `T` is either a slice or a - // `repr(C)` or `repr(transparent)` slice DST that is well-aligned at - // any address and length. If `T` is a slice DST with alignment 1, - // `repr(C)` or `repr(transparent)` ensures that no padding is placed - // after the final element of the trailing slice. Consequently, `T` can - // be split into strictly non-overlapping parts any any index. + fn via_no_dynamic_padding(self) -> (Ptr<'a, T, I>, Ptr<'a, [T::Elem], I>) { + static_assert!( + T: ?Sized + KnownLayout => !T::LAYOUT.requires_dynamic_padding(), + "`Split::via_no_dynamic_padding` cannot be used with a type whose layout may require dynamic trailing padding; use `Split::via_runtime_check` instead" + ); + + // SAFETY: The assertion establishes that `requires_dynamic_padding()` + // is false. By that method's guarantee, every valid metadata—including + // `self.l_len()`, which is valid by `Split`'s invariant—describes an + // object with no trailing padding. By the postcondition of + // `PtrInner::split_at_unchecked`, the two returned referents are then + // contiguous and non-overlapping. Since the source already conforms to + // `I::Aliasing`, splitting it into disjoint byte ranges preserves that + // invariant: under `Exclusive`, neither result aliases or accesses the + // other's bytes; under `Shared`, interior mutation through one result + // cannot mutate bytes pointed to by the other. This satisfies Rust's + // reference aliasing rules [1]. + // + // [1] Per https://doc.rust-lang.org/1.93.1/std/ptr/index.html#pointer-to-reference-conversion: + // + // When creating a mutable reference, then while this reference + // exists, the memory it points to must not get accessed (read or + // written) through any other pointer or reference not derived from + // this reference. + // + // When creating a shared reference, then while this reference + // exists, the memory it points to must not get mutated (except inside + // `UnsafeCell`). unsafe { self.via_unchecked() } } /// Produces the split parts of `self`, using a dynamic check to ensure that /// it is sound to have concurrent references to both parts. You should /// prefer using [`Self::via_immutable`], [`Self::via_into_bytes`], or - /// [`Self::via_unaligned`], which have no runtime cost. + /// [`Self::via_no_dynamic_padding`], which perform no runtime check. #[inline(always)] fn via_runtime_check(self) -> Result<(Ptr<'a, T, I>, Ptr<'a, [T::Elem], I>), Self> { let l_len = self.l_len(); + // `l_len` witnesses an object size at most `isize::MAX`, and + // `KnownLayout::LAYOUT` guarantees that it contains the trailing + // slice. Thus `padding_for_elems` returns the exact trailing padding. + let trailing_padding = crate::trailing_slice_layout::().padding_for_elems(l_len.get()); // FIXME(#1290): Once we require `KnownLayout` on all fields, add an // `IS_IMMUTABLE` associated const, and add `T::IS_IMMUTABLE ||` to the // below check. - if l_len.padding_needed_for() == 0 { - // SAFETY: By `T: SplitAt`, `T` is either `[T]`, or a `repr(C)` or - // `repr(transparent)` slice DST, for which the trailing padding - // needed to accommodate `l_len` trailing elements is - // `l_len.padding_needed_for()`. If no trailing padding is required, + if trailing_padding == 0 { + // SAFETY: As established above, `trailing_padding` is the exact + // padding after the left part's trailing slice. If it is zero, // the left and right parts are strictly non-overlapping. Ok(unsafe { self.via_unchecked() }) } else { @@ -903,7 +1046,7 @@ where /// # Safety /// /// The caller promises that if `I::Aliasing` is [`Exclusive`] or `T` - /// permits interior mutation, then `l_len.padding_needed_for() == 0`. + /// permits interior mutation, then the left part has no trailing padding. #[inline(always)] unsafe fn via_unchecked(self) -> (Ptr<'a, T, I>, Ptr<'a, [T::Elem], I>) { let l_len = self.l_len(); @@ -915,11 +1058,11 @@ where // Lemma 0: `left` and `right` conform to the aliasing invariant // `I::Aliasing`. Proof: If `I::Aliasing` is `Exclusive` or `T` permits - // interior mutation, the caller promises that `l_len.padding_needed_for() - // == 0`. Consequently, by post-condition on `PtrInner::split_at_unchecked`, - // there is no trailing padding after `left`'s final element that would - // overlap into `right`. If `I::Aliasing` is shared and `T` forbids interior - // mutation, then overlap between their referents is permissible. + // interior mutation, the caller promises that the left part has no + // trailing padding. Consequently, by the postcondition of + // `PtrInner::split_at_unchecked`, `left` and `right` do not overlap. + // If `I::Aliasing` is shared and `T` forbids interior mutation, then + // overlap between their referents is permissible. // SAFETY: // 0. `left` conforms to the aliasing invariant of `I::Aliasing`, by Lemma 0. @@ -933,17 +1076,19 @@ where // SAFETY: // 0. `right` conforms to the aliasing invariant of `I::Aliasing`, by Lemma // 0. - // 1. `right` conforms to the alignment invariant of `I::Alignment, because - // if `ptr` with `I::Alignment = Aligned`, then by invariant on `T: - // SplitAt`, the trailing slice of `ptr` (from which `right` is derived) - // will also be well-aligned. + // 1. `right` conforms to the alignment invariant of `I::Alignment`: + // if the source is aligned, `T: SplitAt` guarantees that its + // trailing slice's address is aligned for `T::Elem`. Advancing by + // whole elements preserves that alignment. // 2. `right` conforms to the validity invariant of `I::Validity`, - // because `right: [T::Elem]` is derived from the trailing slice of - // `ptr`, which, by contract on `T: SplitAt::Elem`, has type - // `[T::Elem]`. The `left` part cannot be used to invalidate `right`, - // because the caller promises that if `I::Aliasing` is `Exclusive` - // or `T` permits interior mutation, then `l_len.padding_needed_for() - // == 0` and thus the parts will be non-overlapping. + // because `T: SplitAt` guarantees that `T::Elem` has the size and + // bit validity of the original trailing element. It also guarantees + // compatible shared and mutable access, so writes through `right` + // preserve the original elements' validity and interior mutation + // respects the original `UnsafeCell` coverage. The left part + // cannot invalidate `right`: when the references are exclusive or + // permit interior mutation, the caller guarantees no trailing + // padding and hence no overlap. let right = unsafe { Ptr::from_inner(right) }; (left, right) @@ -952,6 +1097,185 @@ where #[cfg(test)] mod tests { + use core::{cell::Cell, mem::ManuallyDrop}; + + use crate::{Immutable, KnownLayout, SplitAt, Unalign, Unaligned}; + + #[derive(KnownLayout, SplitAt, Immutable, Unaligned)] + #[repr(C, packed)] + struct Packed { + prefix: u8, + tail: ManuallyDrop, + } + + #[derive(KnownLayout, SplitAt, Immutable)] + #[repr(C, packed(2))] + struct Packed2 { + prefix: u8, + tail: ManuallyDrop, + } + + #[derive(KnownLayout, SplitAt, Immutable)] + #[repr(C)] + struct Inner { + prefix: u32, + tail: T, + } + + #[test] + fn test_split_at_packed() { + use crate::{FromBytes, IntoBytes}; + + #[derive(FromBytes, IntoBytes, KnownLayout, SplitAt, Immutable, Unaligned)] + #[repr(C, packed)] + struct Packet { + prefix: u8, + tail: [u16], + } + + // The tail starts at an odd address, exercising unaligned reads and + // writes on targets where `u16` has alignment 2. + #[repr(align(8))] + struct Bytes([u8; 9]); + + for i in 0..=4 { + let mut bytes = Bytes([1; 9]); + let packet = Packet::ref_from_bytes(&bytes.0).unwrap(); + let (left, right): (&Packet, &[Unalign]) = + packet.split_at(i).unwrap().via_immutable(); + assert_eq!(core::mem::size_of_val(left), 1 + 2 * i); + assert_eq!(right.len(), 4 - i); + assert!(right.iter().all(|elem| elem.get() == 0x0101)); + assert!(left.split_at(i).is_some()); + assert!(left.split_at(i + 1).is_none()); + assert!(packet.split_at(5).is_none()); + + let (_, right) = packet.split_at(i).unwrap().via_into_bytes(); + assert_eq!(right.len(), 4 - i); + let (_, right) = packet.split_at(i).unwrap().via_no_dynamic_padding(); + assert_eq!(right.len(), 4 - i); + + let packet = Packet::mut_from_bytes(&mut bytes.0).unwrap(); + let (left, right): (&mut Packet, &mut [Unalign]) = + packet.split_at_mut(i).unwrap().via_runtime_check().ok().unwrap(); + left.prefix = 2; + for elem in right { + *elem = Unalign::new(0x0202); + } + assert_eq!(bytes.0[0], 2); + assert!(bytes.0[1..1 + 2 * i].iter().all(|&byte| byte == 1)); + assert!(bytes.0[1 + 2 * i..].iter().all(|&byte| byte == 2)); + } + } + + #[test] + fn test_split_at_nested_packed() { + // A packing factor greater than 1 still exposes `Unalign` elements, + // including when the factor is below the original element alignment. + let mut words = Packed2 { prefix: 0, tail: ManuallyDrop::new([1u32, 2, 3]) }; + let dst: &mut Packed2<[u32]> = &mut words; + let (left, right): (&mut _, &mut [Unalign]) = + dst.split_at_mut(1).unwrap().via_no_dynamic_padding(); + left.prefix = 4; + right[0] = Unalign::new(5); + let dst: &Packed2<[u32]> = &words; + let (left, right) = dst.split_at(0).unwrap().via_immutable(); + assert_eq!(left.prefix, 4); + assert_eq!(right[0].get(), 1); + assert_eq!(right[1].get(), 5); + assert_eq!(right[2].get(), 3); + + let mut packet = Inner { + prefix: 0, + tail: Packed2 { + prefix: 1, + tail: ManuallyDrop::new(Packed { + prefix: 2, + tail: ManuallyDrop::new([3u16, 4, 5]), + }), + }, + }; + let dst: &Inner>> = &packet; + let (left, right): (&_, &[Unalign>]) = + dst.split_at(1).unwrap().via_immutable(); + assert_eq!(left.prefix, 0); + assert_eq!(right.len(), 2); + assert_eq!(right[0].get().get(), 4); + assert_eq!(right[1].get().get(), 5); + + // Split the packed field separately: the unpacked outer struct's + // padding does not constrain this split. + let dst: &mut Packed2> = &mut packet.tail; + let (left, right): (&mut _, &mut [Unalign>]) = + dst.split_at_mut(1).unwrap().via_no_dynamic_padding(); + left.prefix = 6; + right[0] = Unalign::new(Unalign::new(7)); + let dst: &Inner>> = &packet; + let (_, right) = dst.split_at(0).unwrap().via_immutable(); + assert_eq!(right[0].get().get(), 3); + assert_eq!(right[1].get().get(), 7); + assert_eq!(right[2].get().get(), 5); + } + + #[test] + fn test_split_at_packed_preserves_inner_padding() { + for i in 0..=4 { + let mut packet = Packed { + prefix: 1, + tail: ManuallyDrop::new(Inner { prefix: 0, tail: [2u8, 3, 4, 5] }), + }; + let dst: &Packed> = &packet; + let (left, right): (&_, &[Unalign]) = dst.split_at(i).unwrap().via_immutable(); + assert_eq!(right.len(), 4 - i); + assert_eq!(right.first().map(Unalign::get), [2, 3, 4, 5].get(i).copied()); + + // The inner `u32` prefix makes the tail start at byte 5 of the + // packed wrapper. Its size may still include inner padding. + let has_padding = core::mem::size_of_val(left) != 5 + i; + assert_eq!(dst.split_at(i).unwrap().via_runtime_check().is_err(), has_padding); + + let dst: &mut Packed> = &mut packet; + let split = dst.split_at_mut(i).unwrap().via_runtime_check(); + assert_eq!(split.is_err(), has_padding); + if let Ok((left, right)) = split { + left.prefix = 6; + for elem in right { + *elem = Unalign::new(7); + } + let dst: &Packed> = &packet; + let (left, right) = dst.split_at(i).unwrap().via_immutable(); + assert_eq!(left.prefix, 6); + assert!(right.iter().all(|elem| elem.get() == 7)); + } + } + } + + #[test] + fn test_split_at_packed_interior_mutation_and_zsts() { + let packet = Packed { + prefix: 0, + tail: ManuallyDrop::new([Cell::new(1u8), Cell::new(2), Cell::new(3)]), + }; + let dst: &Packed<[Cell]> = &packet; + let (left, right): (&_, &[Unalign>]) = + dst.split_at(1).unwrap().via_no_dynamic_padding(); + left.tail[0].set(4); + right[0].try_deref().unwrap().set(5); + assert_eq!(dst.tail[0].get(), 4); + assert_eq!(dst.tail[1].get(), 5); + assert_eq!(dst.tail[2].get(), 3); + + let mut packet = Packed { prefix: 0, tail: ManuallyDrop::new([(); 3]) }; + for i in 0..=3 { + let dst: &mut Packed<[()]> = &mut packet; + let (left, right): (&mut _, &mut [Unalign<()>]) = + dst.split_at_mut(i).unwrap().via_no_dynamic_padding(); + assert_eq!(left.tail.len(), i); + assert_eq!(right.len(), 3 - i); + assert_eq!(core::mem::size_of_val(left), 1); + } + } + #[cfg(feature = "derive")] #[test] fn test_split_at() { @@ -1011,6 +1335,8 @@ mod tests { trailing: [u8], } + assert!(SliceDst::LAYOUT.requires_dynamic_padding()); + const N: usize = 16; let arr = [1u16; N]; @@ -1070,21 +1396,79 @@ mod tests { assert_eq!(r, &[4]); } #[test] + #[allow(deprecated)] fn test_split_at_via_unaligned() { - use crate::{FromBytes, Immutable, IntoBytes, KnownLayout, SplitAt, Unaligned}; - #[derive(FromBytes, KnownLayout, SplitAt, IntoBytes, Immutable, Unaligned)] + use crate::{Immutable, KnownLayout, Split, SplitAt, Unaligned}; + + fn via_unaligned<'a, T>(split: Split<&'a T>) -> (&'a T, &'a [T::Elem]) + where + T: ?Sized + SplitAt + Unaligned, + { + split.via_unaligned() + } + + fn via_unaligned_mut<'a, T>(split: Split<&'a mut T>) -> (&'a mut T, &'a mut [T::Elem]) + where + T: ?Sized + SplitAt + Unaligned, + { + split.via_unaligned() + } + + #[derive(KnownLayout, SplitAt, Immutable, Unaligned)] #[repr(C)] - struct Packet { - length: u8, - body: [u8], + struct Packet { + prefix: [u8; 2], + body: B, } - let arr = [1, 2, 3, 4]; - let packet = Packet::ref_from_bytes(&arr[..]).unwrap(); + // Exercise generic callers using the original `T: Unaligned` bound. + assert!(!Packet::<[[u8; 2]]>::LAYOUT.requires_dynamic_padding()); + let packet = Packet { prefix: [0, 1], body: [[2, 3], [4, 5], [6, 7]] }; + let packet: &Packet<[[u8; 2]]> = &packet; let split = packet.split_at(2).unwrap(); - let (l, r) = split.via_unaligned(); - assert_eq!(l.length, 1); - assert_eq!(r, &[4]); + let (l, r) = via_unaligned(split); + assert_eq!(l.body, [[2, 3], [4, 5]]); + assert_eq!(r, &[[6, 7]]); + + let mut packet = Packet { prefix: [0, 1], body: [[2, 3], [4, 5], [6, 7]] }; + { + let packet: &mut Packet<[[u8; 2]]> = &mut packet; + let split = packet.split_at_mut(2).unwrap(); + let (l, r) = via_unaligned_mut(split); + l.body[0] = [8, 9]; + r[0] = [10, 11]; + } + assert_eq!(packet.body, [[8, 9], [4, 5], [10, 11]]); + } + + #[test] + fn test_split_at_via_no_dynamic_padding() { + use core::cell::Cell; + + use crate::SplitAt; + + // `u16` does not implement `Unaligned`, but `[u16]` never requires + // dynamic trailing padding. + let words = [1u16, 2, 3, 4]; + let split = SplitAt::split_at(&words[..], 2).unwrap(); + let (left, right) = split.via_no_dynamic_padding(); + assert_eq!(left, [1, 2]); + assert_eq!(right, [3, 4]); + + let mut words = [1u16, 2, 3, 4]; + let split = SplitAt::split_at_mut(&mut words[..], 2).unwrap(); + let (left, right) = split.via_no_dynamic_padding(); + left[0] = 5; + right[0] = 6; + assert_eq!(words, [5, 2, 6, 4]); + + // Exercise the safety-sensitive shared route with interior mutation. + let cells = [Cell::new(1u16), Cell::new(2), Cell::new(3)]; + let split = SplitAt::split_at(&cells[..], 2).unwrap(); + let (left, right) = split.via_no_dynamic_padding(); + left[0].set(4); + right[0].set(5); + assert_eq!([cells[0].get(), cells[1].get(), cells[2].get()], [4, 2, 5]); } } diff --git a/zerocopy/src/util/checks.rs b/zerocopy/src/util/checks.rs new file mode 100644 index 0000000000..38147e3db4 --- /dev/null +++ b/zerocopy/src/util/checks.rs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT +// +// Copyright 2026 The Fuchsia Authors +// +// Licensed under the 2-Clause BSD License , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +//! Rust assertions describe the arithmetic promises checked by Aeneas. +//! +//! A total specification with postcondition `True` still requires successful +//! termination: every assertion must pass. The independent calculations below +//! use division and remainder, rather than the production bitwise algorithms. +//! The module is part of the ordinary library so extraction sees exactly the +//! source reviewed here. Its private functions have no runtime callers outside +//! tests; optimized builds can discard them. + +// Read the integer explicitly so extraction uses the ordinary scalar remainder +// operation. The proof checks that these guarded arithmetic operations are safe. +#![allow(dead_code, clippy::needless_nonzero_get)] + +use core::num::NonZeroUsize; + +/// Checks padding and rounding down for every length and supported alignment. +/// +/// The explicit guard describes the domain: these helpers require a +/// power-of-two alignment. There is no bound on `len` beyond its Rust type. +/// Neither comparison adds padding to `len`, so `usize::MAX` remains covered. +/// +/// ```aeneas +/// spec arithmetic_checks_spec +/// ensures _ => True +/// ``` +#[allow(clippy::arithmetic_side_effects)] +fn check_arithmetic(len: usize, align: NonZeroUsize) { + if !align.get().is_power_of_two() { + return; + } + let remainder = len % align.get(); + let padding = (align.get() - remainder) % align.get(); + assert!(super::padding_needed_for(len, align) == padding); + assert!(super::round_down_to_next_multiple_of_alignment(len, align) == len - remainder); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn arithmetic_boundaries() { + for align in [1, 2, 4, 8, 1 << (usize::BITS - 1), 3] { + let align = NonZeroUsize::new(align).unwrap(); + for len in [0, 1, 7, 8, 9, usize::MAX - 1, usize::MAX] { + check_arithmetic(len, align); + } + } + } +} diff --git a/zerocopy/src/util/mod.rs b/zerocopy/src/util/mod.rs index a8a56a3b0e..ddd876429a 100644 --- a/zerocopy/src/util/mod.rs +++ b/zerocopy/src/util/mod.rs @@ -14,6 +14,8 @@ pub(crate) mod macros; #[doc(hidden)] pub mod macro_util; +mod checks; + use core::{ marker::PhantomData, mem::{self, ManuallyDrop}, @@ -144,14 +146,26 @@ pub(crate) fn validate_aligned_to(t: T) -> Result<(), Alignment /// on the answer it gives if this is not the case. #[cfg_attr( kani, - kani::requires(len <= DstLayout::MAX_SIZE), kani::requires(align.is_power_of_two()), - kani::ensures(|&p| (len + p) % align.get() == 0), + // A power-of-two alignment divides the `usize` modulus, so wrapping + // preserves congruence even when the next aligned value exceeds `usize`. + kani::ensures(|&p| len.wrapping_add(p) % align.get() == 0), // Ensures that we add the minimum required padding. kani::ensures(|&p| p < align.get()), )] #[cfg_attr(not(zerocopy_inline_always), inline)] #[cfg_attr(zerocopy_inline_always, inline(always))] +/// +/// ```aeneas +/// spec padding_lt_alignment +/// requires h : (align : Nat).isPowerOfTwo +/// ensures p => (p : Nat) < (align : Nat) ∧ +/// let L : Nat := len +/// let A : Nat := align +/// let P : Nat := p +/// P = (A - L % A) % A ∧ (L + P) % A = 0 ∧ +/// (∀ q : Nat, (L + q) % A = 0 → P ≤ q) ∧ (P = 0 ↔ L % A = 0) +/// ``` pub(crate) const fn padding_needed_for(len: usize, align: NonZeroUsize) -> usize { #[cfg(kani)] #[kani::proof_for_contract(padding_needed_for)] @@ -233,6 +247,17 @@ pub(crate) const fn padding_needed_for(len: usize, align: NonZeroUsize) -> usize m.checked_add(align.get()).map(|next_mul| next_mul > n).unwrap_or(true) }) )] +/// +/// ```aeneas +/// spec round_down_spec +/// requires h : (align : Nat).isPowerOfTwo +/// ensures m => (m : Nat) ≤ (n : Nat) ∧ +/// let N : Nat := n +/// let A : Nat := align +/// let M : Nat := m +/// M = N - N % A ∧ M % A = 0 ∧ N < M + A ∧ +/// (∀ q : Nat, q ≤ N → q % A = 0 → q ≤ M) +/// ``` pub(crate) const fn round_down_to_next_multiple_of_alignment( n: usize, align: NonZeroUsize, @@ -255,6 +280,12 @@ pub(crate) const fn round_down_to_next_multiple_of_alignment( #[cfg_attr(not(zerocopy_inline_always), inline)] #[cfg_attr(zerocopy_inline_always, inline(always))] +/// +/// ```aeneas +/// spec max_spec +/// ensures r => (r : Nat) = max (a : Nat) (b : Nat) ∧ (r = a ∨ r = b) ∧ +/// (a : Nat) ≤ (r : Nat) ∧ (b : Nat) ≤ (r : Nat) +/// ``` pub(crate) const fn max(a: NonZeroUsize, b: NonZeroUsize) -> NonZeroUsize { if a.get() < b.get() { b @@ -265,6 +296,12 @@ pub(crate) const fn max(a: NonZeroUsize, b: NonZeroUsize) -> NonZeroUsize { #[cfg_attr(not(zerocopy_inline_always), inline)] #[cfg_attr(zerocopy_inline_always, inline(always))] +/// +/// ```aeneas +/// spec min_spec +/// ensures r => (r : Nat) = min (a : Nat) (b : Nat) ∧ (r = a ∨ r = b) ∧ +/// (r : Nat) ≤ (a : Nat) ∧ (r : Nat) ≤ (b : Nat) +/// ``` pub(crate) const fn min(a: NonZeroUsize, b: NonZeroUsize) -> NonZeroUsize { if a.get() > b.get() { b @@ -562,31 +599,6 @@ mod len_of { self.meta } - #[inline] - pub(crate) fn padding_needed_for(&self) -> usize - where - T: KnownLayout, - { - let trailing_slice_layout = crate::trailing_slice_layout::(); - - // FIXME(#67): Remove this allow. See NumExt for more details. - #[allow( - unstable_name_collisions, - clippy::incompatible_msrv, - clippy::multiple_unsafe_ops_per_block - )] - // SAFETY: By invariant on `self`, a `&T` with metadata `self.meta` - // describes an object of size `<= isize::MAX`. This computes the - // size of such a `&T` without any trailing padding, and so neither - // the multiplication nor the addition will overflow. - let unpadded_size = unsafe { - let trailing_size = self.meta.unchecked_mul(trailing_slice_layout.elem_size); - trailing_size.unchecked_add(trailing_slice_layout.offset) - }; - - util::padding_needed_for(unpadded_size, T::LAYOUT.align) - } - #[inline(always)] pub(crate) fn validate_cast_and_convert_metadata( addr: usize, diff --git a/zerocopy/tests/codegen.rs b/zerocopy/tests/codegen.rs index cc3b0eb34e..92dc92846f 100644 --- a/zerocopy/tests/codegen.rs +++ b/zerocopy/tests/codegen.rs @@ -123,10 +123,15 @@ fn run_codegen_test(bench_name: &str, target_cpu: &str, bless: bool) { if bless { std::fs::write(expected_file_path, actual_result.as_bytes()).unwrap(); } else { - let expected_result = std::fs::read(expected_file_path).unwrap_or_default(); + let expected_result = std::fs::read(&expected_file_path).unwrap_or_default(); if actual_result.as_bytes() != expected_result { let expected = String::from_utf8_lossy(&expected_result[..]); - panic!("Bless codegen tests with BLESS=1\nGot unexpected output:\n{}", expected); + panic!( + "Bless codegen tests with BLESS=1\nSnapshot: {}\nExpected:\n{}\nActual:\n{}", + expected_file_path.display(), + expected, + actual_result + ); } } }; @@ -153,7 +158,11 @@ fn codegen() { }) .collect(); - let failed = handles.into_iter().any(|handle| handle.join().unwrap().is_err()); + // Join every worker so an early failure cannot hide later diagnostics. + let mut failed = false; + for handle in handles { + failed |= handle.join().unwrap().is_err(); + } if failed { panic!("One or more codegen tests failed. See thread panics above for details."); diff --git a/zerocopy/zerocopy-derive/src/derive/mod.rs b/zerocopy/zerocopy-derive/src/derive/mod.rs index 174434561e..6823157bc4 100644 --- a/zerocopy/zerocopy-derive/src/derive/mod.rs +++ b/zerocopy/zerocopy-derive/src/derive/mod.rs @@ -103,13 +103,6 @@ pub(crate) fn derive_split_at(ctx: &Ctx, _top_level: Trait) -> Result Result::Elem); + let elem = + if repr.get_packed().is_some() { quote!(#zerocopy_crate::Unalign<#elem>) } else { elem }; + + // SAFETY: The checks above require `repr(C)` or `repr(transparent)`, and + // `FieldBounds::TRAILING_SELF` requires the trailing field to be `SplitAt`. + // Its `Elem` therefore has the size, bit validity, and `UnsafeCell` + // coverage of the actual trailing element. For an unpacked struct, the + // trailing field is aligned, so its `SplitAt` alignment guarantee applies. + // For a packed struct, wrapping its `Elem` in `Unalign` preserves those + // representation properties and gives alignment 1. Thus the trailing + // slice can be accessed as `[#elem]` even if packing misaligns its field. + // Packing does not change the field's internal layout [1], so its element + // size and byte offsets are preserved in either case. + // + // [1] Per https://doc.rust-lang.org/1.93.0/reference/type-layout.html#layout.repr.alignment.intro: + // + // `packed` may also alter the padding between fields (although it will + // not alter the padding inside of any field). Ok(ImplBlockBuilder::new(ctx, &ctx.ast.data, Trait::SplitAt, FieldBounds::TRAILING_SELF) .inner_extras(quote! { - type Elem = <#trailing_field as #zerocopy_crate::SplitAt>::Elem; + type Elem = #elem; }) .build()) } diff --git a/zerocopy/zerocopy-derive/src/output_tests/expected/split_at_repr_c_packed.expected.rs b/zerocopy/zerocopy-derive/src/output_tests/expected/split_at_repr_c_packed.expected.rs new file mode 100644 index 0000000000..e6c330bf5e --- /dev/null +++ b/zerocopy/zerocopy-derive/src/output_tests/expected/split_at_repr_c_packed.expected.rs @@ -0,0 +1,22 @@ +#[allow( + deprecated, + private_bounds, + non_local_definitions, + non_camel_case_types, + non_upper_case_globals, + non_snake_case, + non_ascii_idents, + clippy::missing_inline_in_public_items, +)] +#[deny(ambiguous_associated_items)] +#[automatically_derived] +const _: () = { + unsafe impl ::zerocopy::SplitAt for Foo + where + Self: Copy, + T: ::zerocopy::SplitAt, + { + fn only_derive_is_allowed_to_implement_this_trait() {} + type Elem = ::zerocopy::Unalign<::Elem>; + } +}; diff --git a/zerocopy/zerocopy-derive/src/output_tests/mod.rs b/zerocopy/zerocopy-derive/src/output_tests/mod.rs index ad4eb29e04..c788845d4a 100644 --- a/zerocopy/zerocopy-derive/src/output_tests/mod.rs +++ b/zerocopy/zerocopy-derive/src/output_tests/mod.rs @@ -1004,13 +1004,27 @@ fn test_split_at() { } expands to "expected/split_at_repr_transparent.expected.rs" } + test! { + SplitAt { + #[repr(C, packed)] + struct Foo(T) where Self: Copy; + } expands to "expected/split_at_repr_c_packed.expected.rs" + } + + test! { + SplitAt { + #[repr(C, packed(2))] + struct Foo(T) where Self: Copy; + } expands to "expected/split_at_repr_c_packed.expected.rs" + } + test! { SplitAt { #[repr(packed)] struct Foo(T) where Self: Copy; } expands to { ::core::compile_error! { - "must not have #[repr(packed)] attribute" + "must have #[repr(C)] or #[repr(transparent)] in order to guarantee this type's layout is splitable" } } no_build } @@ -1021,7 +1035,7 @@ fn test_split_at() { struct Foo(T) where Self: Copy; } expands to { ::core::compile_error! { - "must not have #[repr(packed)] attribute" + "must have #[repr(C)] or #[repr(transparent)] in order to guarantee this type's layout is splitable" } } no_build } diff --git a/zerocopy/zerocopy-derive/tests/ui/struct.msrv.stderr b/zerocopy/zerocopy-derive/tests/ui/struct.msrv.stderr index 6358819d63..ad2db50c39 100644 --- a/zerocopy/zerocopy-derive/tests/ui/struct.msrv.stderr +++ b/zerocopy/zerocopy-derive/tests/ui/struct.msrv.stderr @@ -317,9 +317,9 @@ error[E0277]: the trait bound `SplitAtNotKnownLayout: zerocopy_renamed::KnownLay | ^^^^^^^ the trait `zerocopy_renamed::KnownLayout` is not implemented for `SplitAtNotKnownLayout` | note: required by a bound in `SplitAt` - --> $WORKSPACE/src/split_at.rs:63:27 + --> $WORKSPACE/src/split_at.rs:97:27 | -63 | pub unsafe trait SplitAt: KnownLayout { +97 | pub unsafe trait SplitAt: KnownLayout { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `SplitAt` = note: this error originates in the derive macro `SplitAt` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/zerocopy/zerocopy-derive/tests/ui/struct.nightly.stderr b/zerocopy/zerocopy-derive/tests/ui/struct.nightly.stderr index 49b44a751b..07e443a31b 100644 --- a/zerocopy/zerocopy-derive/tests/ui/struct.nightly.stderr +++ b/zerocopy/zerocopy-derive/tests/ui/struct.nightly.stderr @@ -484,7 +484,7 @@ help: the trait `zerocopy_renamed::KnownLayout` is not implemented for `SplitAtN $IMPLEMENTATION_SAMPLES and $OTHER_TYPES others note: required by a bound in `SplitAt` - --> src/split_at.rs:63:0 + --> src/split_at.rs:97:0 = note: this error originates in the derive macro `SplitAt` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `u8: SplitAt` is not satisfied @@ -502,9 +502,12 @@ help: the following other types implement trait `SplitAt` ... 363 | #[derive(SplitAt, KnownLayout)] | ^^^^^^^ `SplitAtSized` - --> src/split_at.rs:253:0 + --> src/split_at.rs:289:0 | = note: `[T]` + ::: src/split_at.rs:312:0 + | + = note: `ManuallyDrop` = note: this error originates in the derive macro `SplitAt` (in Nightly builds, run with -Z macro-backtrace for more info) help: add `#![feature(trivial_bounds)]` to the crate attributes to enable | diff --git a/zerocopy/zerocopy-derive/tests/ui/struct.stable.stderr b/zerocopy/zerocopy-derive/tests/ui/struct.stable.stderr index 27038d9b49..1ae10d808c 100644 --- a/zerocopy/zerocopy-derive/tests/ui/struct.stable.stderr +++ b/zerocopy/zerocopy-derive/tests/ui/struct.stable.stderr @@ -428,7 +428,7 @@ help: the trait `zerocopy_renamed::KnownLayout` is not implemented for `SplitAtN $IMPLEMENTATION_SAMPLES and $OTHER_TYPES others note: required by a bound in `SplitAt` - --> src/split_at.rs:63:0 + --> src/split_at.rs:97:0 = note: this error originates in the derive macro `SplitAt` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `u8: SplitAt` is not satisfied @@ -446,9 +446,12 @@ help: the following other types implement trait `SplitAt` ... 363 | #[derive(SplitAt, KnownLayout)] | ^^^^^^^ `SplitAtSized` - --> src/split_at.rs:253:0 + --> src/split_at.rs:289:0 | = note: `[T]` + ::: src/split_at.rs:312:0 + | + = note: `ManuallyDrop` = note: this error originates in the derive macro `SplitAt` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `AU16: zerocopy_renamed::Unaligned` is not satisfied