diff --git a/.github/actions/setup-just/action.yml b/.github/actions/setup-just/action.yml index ac62725..4f1300c 100644 --- a/.github/actions/setup-just/action.yml +++ b/.github/actions/setup-just/action.yml @@ -1,6 +1,12 @@ name: Set up just description: Resolve the pinned just version from justfile and install it. +inputs: + cache: + description: Restore and save the installed just binary in the Actions cache. + required: false + default: "true" + outputs: version: description: Resolved just version from justfile. @@ -45,7 +51,15 @@ runs: echo "version=$version" >> "$GITHUB_OUTPUT" - - name: Install just + - name: Install just with caching + if: inputs.cache == 'true' uses: taiki-e/cache-cargo-install-action@9ee83daaa7b96a6fab930949ecf1122bba04a389 # v3.0.8 with: tool: just@${{ steps.resolve.outputs.version }} + + - name: Install just without caching + if: inputs.cache != 'true' + shell: bash + env: + JUST_VERSION: ${{ steps.resolve.outputs.version }} + run: cargo install --locked just --version "$JUST_VERSION" diff --git a/.github/workflows/release-benchmarks.yml b/.github/workflows/release-benchmarks.yml index b8422f2..af4847c 100644 --- a/.github/workflows/release-benchmarks.yml +++ b/.github/workflows/release-benchmarks.yml @@ -37,9 +37,12 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 with: cache: false + cache-bin: false - name: Set up just uses: ./.github/actions/setup-just # zizmor: ignore[self-repository] actionlint 1.7.12 does not accept $/... + with: + cache: false - name: Resolve cargo-nextest version id: cargo_nextest_version @@ -56,9 +59,9 @@ jobs: echo "version=$version" >> "$GITHUB_OUTPUT" - name: Install cargo-nextest - uses: taiki-e/cache-cargo-install-action@9ee83daaa7b96a6fab930949ecf1122bba04a389 # v3.0.8 - with: - tool: cargo-nextest@${{ steps.cargo_nextest_version.outputs.version }} + env: + CARGO_NEXTEST_VERSION: ${{ steps.cargo_nextest_version.outputs.version }} + run: cargo install --locked cargo-nextest --version "$CARGO_NEXTEST_VERSION" - name: Validate benchmark inputs run: just test-bench-inputs diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index cd0e087..9054d74 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -32,7 +32,27 @@ jobs: with: persist-credentials: false + - name: Set up just + uses: ./.github/actions/setup-just # zizmor: ignore[self-repository] actionlint 1.7.12 does not accept $/... + + - name: Resolve zizmor version + id: zizmor_version + shell: bash + run: | + set -euo pipefail + + version="$(just --evaluate zizmor_version)" + if [[ -z "$version" ]]; then + echo "::error::Could not resolve zizmor_version from justfile" + exit 1 + fi + + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Run zizmor uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 with: inputs: .github + online-audits: true + persona: regular + version: ${{ steps.zizmor_version.outputs.version }} diff --git a/AGENTS.md b/AGENTS.md index 36d6414..cb0a306 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,532 +1,235 @@ # AGENTS.md -Essential guidance for AI assistants working in this repository. +Essential guidance for AI assistants working in this repository. Keep this file +as the entry point; detailed rules belong in the focused documents below. + +## Contents + +- [Required Reading](#required-reading) +- [Priorities](#priorities) +- [Core Rules](#core-rules) +- [Scientific Invariants](#scientific-invariants) +- [Rust and API Design](#rust-and-api-design) +- [Testing and Performance](#testing-and-performance) +- [Documentation](#documentation) +- [Validation](#validation) +- [Python](#python) +- [Project Context](#project-context) +- [Agent Expectations](#agent-expectations) + +## Required Reading + +Read this file before making changes. Then read the focused guidance matching +the task; those rules remain binding when applicable. Load additional owners +if the scope expands. Unrelated guides need not be loaded for every task. + +| Task | Read | +|------|------| +| GitHub issues, branches, or commit messages | [Git and GitHub guidance](docs/dev/MANAGING_CHANGES.md) | +| Tests, doctests, or benchmark design | [Testing guidance](docs/dev/testing.md) | +| README, rustdoc, citations, or documentation layout | [Documentation guidance](docs/dev/docs.md) | +| Module ownership, features, or file organization | [Code organization](docs/code_organization.md) | +| Setup, commands, or validation selection | [Contributor workflow](CONTRIBUTING.md#validation-workflow), [justfile](justfile), `just --list` | +| Benchmark execution or performance reports | [Benchmarking](docs/BENCHMARKING.md) | +| Python support scripts | [Scripts guide](scripts/README.md) | +| Maintainer-requested release work | [Releasing](docs/RELEASING.md) | ## Priorities -When making changes in this repo, prioritize (in order): +This is a scientific linear-algebra library. Design decisions prioritize: -- Mathematical correctness and invariant preservation -- API stability and composability -- Idiomatic, well-tested Rust -- Performance within the documented scope +1. Mathematical correctness and invariant preservation. +2. API stability. +3. Composability. +4. Idiomatic, well-tested Rust. +5. Performance within the documented scope. -## Design Principles +Favor the invariant over a convenient edit or faster implementation. -This is a scientific linear-algebra library. Design decisions trade off in -roughly this priority: mathematical correctness → API stability → -composability → idiomatic Rust → performance within scope. The sections -below spell out what each means in practice; when in doubt, favour the -invariant over the convenient edit. +## Core Rules -### Mathematical correctness as an invariant +### Git and Editing + +- **Never mutate version-control state.** Do not run `git commit`, `git push`, + `git tag`, or other ref/index-mutating commands. Suggest those commands for + the user to run manually. +- Use `git --no-pager` for read-only Git commands, including status, diff, log, + show, and blame. +- Preserve user changes. The worktree may be dirty; work around overlapping + edits and do not revert unrelated work. +- **Use the structured patch/edit tool for manual edits.** Never use `sed`, + `awk`, Python, or `perl` to write files. Shell text tools may be used for + read-only inspection. Keep patch rejects and backup files outside the + repository and clean them immediately. +- Repository formatters and linters are allowed, including Cargo, Taplo, + Ruff, rumdl, dprint, typos, and actionlint commands. + +### Safety and Releases + +- **Unsafe Rust is forbidden.** Preserve the manifest-level + `unsafe_code = "forbid"` lint and crate/module `#![forbid(unsafe_code)]`. +- **Dead code is forbidden.** Remove unused items; never add + `#[allow(dead_code)]` or `#![allow(dead_code)]`, including in tests and + static-analysis fixtures. +- Version bumps are maintainer-driven release work. Do not update package + versions, lockfile versions, or dependency snippets during ordinary work; + change them only when explicitly asked to do release/version-bump work. +- Never hand-edit `CHANGELOG.md`; use its documented generation workflow. + Release procedures and generated-artifact ownership live in + [Releasing](docs/RELEASING.md). + +## Scientific Invariants - Arbitrary-precision paths (`det_exact`, `solve_exact`) never silently lose - precision. Strict exact-to-`f64` methods (`det_exact_f64`, - `solve_exact_f64`) return [`LaError::Unrepresentable`] rather than rounding: - [`UnrepresentableReason::RequiresRounding`] means a finite `f64` is available - only after rounding, while [`UnrepresentableReason::NotFinite`] means no - finite `f64` can represent the result. The explicit `*_exact_rounded_f64` - methods opt into rounding but still return `NotFinite` when rounding cannot - produce a finite value. -- New or changed f64 operations that can accumulate rounding error document - their absolute bound (`det_errbound`, `ERR_COEFF_*`) or explicitly state + precision. Strict exact-to-`f64` methods return + `LaError::Unrepresentable` instead of rounding: + `UnrepresentableReason::RequiresRounding` means a finite result requires + rounding; `NotFinite` means no finite `f64` can represent the result. + Explicit `*_exact_rounded_f64` methods opt into rounding but still return + `NotFinite` when rounding cannot produce a finite value. +- New or changed `f64` operations that accumulate rounding error document + an absolute bound (`det_errbound`, `ERR_COEFF_*`) or explicitly state that no bound is provided. -- Non-finite matrix/vector inputs and arithmetic intermediates surface as - `LaError::NonFinite` with typed `NonFiniteOrigin` and `NonFiniteLocation` - metadata; do not silently propagate NaN or use `unwrap_or(f64::NAN)`. - Computed failures name their `ArithmeticOperation`, while raw matrix/vector - values use input origins and exact source locations. -- Exact singularity and tolerance-based rejection remain distinguishable through - `SingularityReason`; numerical failures preserve the factorization, observed - pivot magnitude, and tolerance. -- Parse raw tolerances through `Tolerance::try_new`; failures use typed - `InvalidToleranceReason`. Exact-to-f64 output failures use +- Non-finite inputs and intermediates surface as `LaError::NonFinite` + with typed `NonFiniteOrigin` and `NonFiniteLocation` metadata. Computed + failures identify their `ArithmeticOperation`; raw matrix/vector values + retain input origins and exact source locations. Never silently propagate + NaN or use `unwrap_or(f64::NAN)`. +- Keep exact singularity distinct from tolerance-based rejection through + `SingularityReason`. Numerical failures retain the factorization, + observed pivot magnitude, and tolerance. +- Parse raw tolerances through `Tolerance::try_new`; failures retain typed + `InvalidToleranceReason`. Exact-to-`f64` failures remain `LaError::Unrepresentable`. -- Algorithms cite their source (Shewchuk, Bareiss, Goldberg, …) via - `REFERENCES.md` and document their conditioning behaviour. - `Matrix::det()` uses closed forms through D=4. Its D≥5 zero-tolerance LU fallback preserves `LaError::Singular` when elimination cannot produce a - non-zero pivot; floating-point factorization must not relabel that numerical - failure as an exact `0.0`. Use the exact determinant APIs when exact - singularity classification is required. - -### Public-API stability - -- Public error enums and struct-style error variants are `#[non_exhaustive]`; - downstream matches include wildcard arms and `..`. Public wrapper types are - `#[must_use]`. -- New functionality is additive by default: use the prelude for ergonomic - re-exports, and avoid churn for its own sake. -- Pre-1.0 semver: any `0.x.y` release may include breaking API changes when - they materially improve correctness, orthogonality, performance, or - long-term API clarity. Do not keep compatibility aliases that weaken the - public model; document intentional breaks clearly in release notes and commit - messages. -- Do **not** automatically update the library version in `Cargo.toml`, - `Cargo.lock`, README dependency snippets, or related docs during ordinary - feature, fix, review, or hygiene work. Version bumps are maintainer-driven - release work performed manually as part of `docs/RELEASING.md`; only change - them when the user explicitly asks for release/version-bump work. - -### Composability - -- Const-generic `D` for every core type (`Matrix`, `Vector`, - `Lu`, `Ldlt`). No runtime dimension. -- Stack allocation by default; heap only behind a feature flag or where - exact arithmetic inherently requires it (`BigInt` / `BigRational`). -- Feature flags isolate optional dependency weight; default builds stay - dep-minimal. - -### Idiomatic Rust as a proxy for mathematical clarity - -- `const fn` wherever possible — not for micro-optimisation, but because - compile-time evaluation forces a pure function of inputs. -- Use `Result<_, LaError>` for all fallible operations. Public library code - must not panic on user input. -- Panics are reserved for truly unreachable internal invariant violations and - must be documented when callers could observe them. -- Validation belongs to the lowest type or module that owns the invariant. - Higher-level APIs preserve and propagate typed `LaError` values rather than - stringifying them. -- Public APIs that return plain values must be genuinely infallible for all - representable inputs. If callers can observe failure, return `Result` or - `Option` instead of relying on `panic!`, `assert!`, `unwrap`, or `expect`. -- Borrow by default (`&T`, `&[T]`); return borrowed views when possible. -- Type and function names match textbook vocabulary (`Matrix`, `Vector`, - `Lu`, `Ldlt`, `solve`, `det`, `norm_inf`). Avoid Rust-ecosystem - abstractions that obscure the math. - -### Scientific notation in docs - -- Unicode math (×, ≤, ≥, ∈, Σ, ², `2^-50`, …) is welcome in doc - comments — readability trumps ASCII-only preference. -- Reference literature via `REFERENCES.md` numbered citations (e.g. - `\[8\]`, `\[9-10\]`). -- State invariants mathematically where possible - (`|A[i][i]| > Σ_{j≠i} |A[i][j]|`) rather than prose-only. - -### Performance within scope - -- Performance is a design goal, but strictly subordinate to the - principles above. Never trade correctness, stability, or clarity for - speed; if the two conflict, re-scope the problem rather than - compromise the invariant. -- The library earns its speed through *deliberate scope restriction*: - fixed small dimensions via const generics, stack-allocated storage, - and closed-form algorithms where available (D ≤ 4 for `det_direct` / - `det_errbound`). Problems outside this scope — large or dynamic - dimensions, sparse matrices, parallelism — belong to `nalgebra` or - `faer` (see anti-goals in `README.md`). -- Within scope, prefer allocation-free paths, `const fn` wherever the inputs - allow, and FMA where applicable. -- Performance-sensitive changes require comparable before-and-after evidence - from the same representative benchmark command, inputs, features, and - environment. Use `bench-vs-linalg` (vs nalgebra / faer) or `bench-exact` - (exact arithmetic), as appropriate. -- For nanosecond-scale fixed-size kernels, prefer Criterion `bencher.iter` so - the complete public operation is measured symmetrically across implementations. - Use `iter_batched` only when setup is explicitly outside the estimand, the - exclusion is applied comparably to every implementation, and a same-binary - comparison shows that batching does not materially distort the result. -- Preserve benchmark provenance and distinguish descriptive point-estimate - ratios from statistically supported performance claims. Marginal Criterion - interval separation is not a paired confidence interval for the change. -- Measurements from runs that violate documented invariants are invalid - performance evidence. - -### Testing mirrors the principles - -- Unit tests cover known values, error paths, and dimension-generic - correctness across D=2..=5 (see **Dimension Coverage** below). -- Error-path tests match the exact variant, typed reason/origin/location, and - structured fields; do not replace an unexpected error with a numeric sentinel - or assert only `is_err()`. -- Proptests under `tests/proptest_*.rs` cover algebraic invariants - (round-trip, residual, sign agreement) — not just "does it not panic". -- Adversarial inputs (near-singular, large-entry, Hilbert-style - ill-conditioning) accompany well-conditioned inputs in both tests and - benchmarks. -- When a public API has two paths for the same question (fast filter + - exact fallback), a proptest verifies they agree on the domain where - both are defined. - -## Core Rules - -### Git Operations - -- **NEVER** run `git commit`, `git push`, `git tag`, or any git commands that modify version control state -- **ALLOWED**: Run read-only git commands (e.g. `git --no-pager status`, `git --no-pager diff`, - `git --no-pager log`, `git --no-pager show`, `git --no-pager blame`) to inspect changes/history -- **ALWAYS** use `git --no-pager` when reading git output -- Suggest git commands that modify version control state for the user to run manually -- Do not revert user changes. The worktree may be dirty; preserve unrelated - changes and work around overlapping edits. -- When suggesting branch names, prefer `{type}/{issue}-descriptor-or-two`, e.g. `fix/307-topology-validation`, - `perf/315-bench-profile`, or `doc/329-branch-guidance`. If an environment requires an owner/tool prefix, - keep this structure after the prefix, e.g. `codex/fix/307-topology-validation`. + non-zero pivot. Do not relabel that numerical failure as an exact `0.0`; + use exact determinant APIs for exact singularity classification. +- Algorithms cite their sources through `REFERENCES.md` and document + conditioning behavior. Mathematical explanations belong in + [Mathematical basis](docs/mathematical_basis.md). -### Commit Messages +## Rust and API Design -When user requests commit message generation: - -1. Run `git --no-pager diff --cached --stat` -2. Generate conventional commit format: `: ` -3. Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `style`, `ci`, `build` -4. Include body with organized bullet points and test results -5. Present in code block (no language) - user will commit manually - -### Code Quality - -- **Unsafe Rust is forbidden.** Keep the manifest-level `unsafe_code = "forbid"` - lint and crate/module `#![forbid(unsafe_code)]` enforcement intact. -- **Dead code is forbidden.** Remove unused items instead of suppressing the - lint; never add `#[allow(dead_code)]` or `#![allow(dead_code)]`, including in - tests and static-analysis fixtures. -- **ALLOWED**: Run formatters/linters: `cargo fmt`, `cargo clippy`, `cargo doc`, `taplo fmt`, `taplo lint`, - `uv run --locked ruff check --fix`, `uv run --locked ruff format`, `rumdl`, `dprint`, - `typos`, `actionlint` -- **NEVER**: Use `sed`, `awk`, `perl` for code edits -- **ALWAYS**: Use the provided structured patch/edit tool for manual edits. -- **FALLBACK**: Direct patch rejects and backup files outside the repository - and clean them immediately. -- **EXCEPTION**: Shell text tools OK for read-only analysis only - -### Validation - -- Select validators proportionally to the changed surfaces. Use focused recipes - for documentation, configuration, Python, test-only, benchmark-only, or - example-only changes; compose each relevant validator once when a patch spans - multiple surfaces. Core Rust or public-behavior changes require final - `just ci`. -- **JSON**: Validate with `jq empty .json` after editing (or `just validate-json`) -- **TOML**: Lint/format with taplo: `just toml-lint`, `just toml-fmt-check`, `just toml-fmt` -- **GitHub Actions**: Validate workflows with `just action-lint` (uses `actionlint`) -- **Spell check**: Run `just spell-check` after editing; add legitimate technical terms to - `typos.toml` under `[default.extend-words]` -- **Shell scripts**: Run `just shell-fix` and `just shell-check` after editing -- **YAML**: Use `just yaml-lint` and `just yaml-fix` -- **Markdown**: Use `just markdown-check` and `just markdown-fix` - -### Rust - -- The current MSRV and pinned contributor/CI toolchain are Rust 1.98.1. Keep - `Cargo.toml`, `rust-toolchain.toml`, and `clippy.toml` aligned when that - baseline changes deliberately. +- Keep the MSRV and contributor/CI toolchain aligned across `Cargo.toml`, + `rust-toolchain.toml`, and `clippy.toml`; the current baseline is 1.98.1. - Rust's `f64::algebraic_*` operations are forbidden in all repository-owned - Rust code, including tests, examples, and benchmarks. - Their unspecified reassociation, precision, and special-value behavior can - invalidate defined operation order, error bounds, non-finite classification, - exact fallbacks, and reproducibility. Ordinary operators remain allowed. - Deliberate fused multiply-add through `f64::mul_add` is explicitly allowed; - existing numerical kernels and error bounds may rely on its single-rounding - evaluation. Any fast-math design requires a separate issue, opt-in contract, - correctness analysis, and benchmark evidence. -- Prefer borrowed APIs by default: - take references (`&T`, `&mut T`, `&[T]`) as arguments and return borrowed views (`&T`, `&[T]`) when possible. - Only take ownership or return `Vec`/allocated data when required. - -### Documentation - -- `src/lib.rs` includes `README.md` with `#![doc = include_str!("../README.md")]`, so README examples are the - docs.rs landing page examples. -- Keep the README quickstart and brief capability descriptions discoverable; - put fuller worked API examples and caller contracts in the documentation-only - `guide` module in `src/lib.rs`. Preserve useful detail in the linked guide when - shortening README sections, including numerical limitations and error semantics. -- Link API references and worked API guides to docs.rs. Keep repository-owned - mathematical background, benchmark reports, roadmap, contributing, and release - instructions on GitHub, using absolute URLs to the intended repository revision. -- README links and Contents anchors must work both on GitHub and in generated - rustdoc. Repository-relative file links can resolve incorrectly from rustdoc; - verify destinations in both contexts and use intra-doc links within Rust docs - where the referenced item is available under the selected features. -- Choose docs.rs `latest` links for intentionally current guidance and explicit - versions for release-specific contracts. docs.rs builds published crates, so - merging changes does not publish new guide pages. Local rendering checks do - not establish availability on the published site. -- When changing Rust examples in `README.md`, mirror executable versions in the private `readme_doctests` module in - `src/lib.rs`. Keep mirrors hidden/private so they do not duplicate the docs.rs landing page, but make them runnable - by `cargo test --doc`. -- README examples that require optional features may remain `rust,ignore` in README for default-feature doctest - compatibility, but must have a `#[cfg(feature = "...")]` hidden doctest mirror in `src/lib.rs` and be verified with - the matching feature set (for example, `cargo test --features exact --doc`). -- Guide examples run directly as doctests; gate feature-dependent guides with - the matching feature and remove obsolete private mirrors when moving examples - out of README. Run `just doc-check` for changed guide docs and inspect generated - pages and anchors for explicit links, which rustdoc does not validate. Validate - changed executable examples with the default and matching feature doctest recipes. -- When intentionally updating package versions or dependency snippets, keep README `la-stack` dependency examples in - sync with the package `version` in `Cargo.toml`. Do not perform version bumps unless explicitly requested by the - maintainer; see **Public-API stability** above. - -### Dimension Coverage (2D–5D) - -This library uses `const`-generic dimensions. Tests for dimension-generic code -**must cover D=2 through D=5** whenever possible. - -#### Use macros for per-dimension test generation - -Define a macro that accepts a dimension literal and generates the full set -of test functions for that dimension. Invoke it once per dimension: - -```rust -macro_rules! gen_tests { - ($d:literal) => { - paste! { - #[test] - fn []() { - // assertions … - } - } - }; -} - -gen_tests!(2); -gen_tests!(3); -gen_tests!(4); -gen_tests!(5); -``` - -#### Keep core logic in generic helper functions - -The macro body should be thin — primarily calling `const`-generic helpers and -asserting results. This keeps the macro readable and the helpers independently -testable. - -#### Reference examples - -- `src/matrix.rs` — `gen_matrix_tests!` -- `src/lu.rs` — `gen_pivoting_solve_and_det_tests!`, `gen_tridiagonal_smoke_solve_and_det_tests!` -- `src/ldlt.rs` — `gen_ldlt_identity_tests!`, `gen_ldlt_diagonal_tests!` -- `src/exact.rs` — `gen_det_exact_tests!`, `gen_det_exact_f64_tests!`, `gen_solve_exact_tests!`, `gen_solve_exact_f64_tests!` - -#### When single-dimension tests are acceptable - -Some tests are inherently dimension-specific (e.g. known values for a crafted -matrix, error-handling with a specific layout). These do not need -macro-ification. - -### Python - -- Python support tooling targets Python 3.14. -- Use `uv run --locked` for all Python scripts (never `python3` or `python` directly) -- Use pytest for tests (not unittest) -- **Type checking**: `just python-check` includes type checking (blocking - all code must pass type checks) -- Add type hints to new code - -## Common Commands - -```bash -just check # Lint/validators (non-mutating) -just fix # Apply formatters/auto-fixes (mutating) -just ci # Full CI simulation (checks + tests + examples + bench compile) -just test # Lib + doc tests (fast) -just test-all # All tests (Rust, benchmark inputs, and Python) -just examples # Run all examples -just update # Update dependency requirements, locks, and repository-owned tool pins -just update-version vX.Y.Z # Update release metadata without upgrading dependencies -``` - -### Detailed Command Reference - -- All tests (Rust, exact-feature doctests, benchmark-input smoke tests, and Python): `just test-all` -- Benchmark comparison (local report): `just bench-compare [baseline] [suite] [scope]` -- Benchmarks: `cargo bench --locked --features bench` (or `just bench`) -- Benchmarks (exact arithmetic): `just bench-exact` -- Benchmarks (la-stack vs nalgebra/faer): `just bench-vs-linalg [filter]` (full run) or `just bench-vs-linalg-quick [filter]` (reduced) -- Benchmarks (plot exploratory vs_linalg CSV/SVG/JSON provenance): `just plot-vs-linalg [metric] [stat] [sample] [log_y]`; - after `just performance-release`, publish its retained measurements to README - with `just performance-readme [metric] [stat] [sample] [log_y]` -- Benchmarks (save baseline): `just bench-save-baseline v0.4.1` -- Build (debug): `cargo build` (or `just build`) -- Build (release): `cargo build --release` (or `just build-release`) -- Changelog (generate full): `just changelog` (generates, post-processes, archives, and formats changelog files) -- Changelog (prepend unreleased): `just changelog-unreleased v0.4.1` -- Coverage (CI XML): `just coverage-ci` -- Coverage (HTML): `just coverage` -- Create release tag: `just tag v0.4.1` (creates annotated tag from CHANGELOG.md section) / `just tag-force v0.4.1` (recreate if the tag already exists) -- Fast compile check (no binary produced): `cargo check` (or `just check-fast`) -- Fast Rust tests (lib + doc): `just test` -- Format: `cargo fmt` (or `just fmt`) -- Integration tests: `just test-integration` -- Benchmark-input smoke tests: `just test-bench-inputs` -- Lint (Clippy, canonical default and all-feature passes): `just clippy` -- Lint (Clippy, focused exact-feature pass): `just clippy-exact` -- Lint/validate: `just check` -- Cargo manifest/lockfile synchronization: `just cargo-lock-check` -- Unused dependency check: `just unused-deps` (uses `cargo-machete`) -- Update release metadata: `just update-version vX.Y.Z` (infers the previous stable published GitHub release) -- Pre-commit validation / CI simulation: `just ci` (lint + tests + examples + bench compile) -- Python setup from the lockfile: `uv sync --locked --group dev` (or `just python-sync`) -- Python tests: `just test-python` -- Run one runnable test by substring: `cargo nextest run solve_2x2_basic` - - For an exact full-path match, use `cargo nextest run -- --exact lu::tests::solve_2x2_basic`. -- Run exact-feature tests: `cargo nextest run --profile ci --features exact --verbose` - (or `just test-exact`, which also runs exact-feature doctests) -- Run examples: `just examples` (or `cargo run --example det_5x5` / `cargo run --example solve_5x5` / - `cargo run --example ldlt_solve_3x3` / `cargo run --example const_det_4x4` / - `cargo run --features exact --example exact_det_3x3` / - `cargo run --features exact --example exact_sign_3x3` / - `cargo run --features exact --example exact_solve_3x3`) -- Spell check: `just spell-check` (uses `typos.toml` at repo root; add false positives to `[default.extend-words]`) - -### Changelog - -- Never edit `CHANGELOG.md` directly - it's auto-generated from git commits -- Use `just changelog` to regenerate -- Use `just changelog-unreleased ` to prepend unreleased changes - -### GitHub CLI (`gh`) - -When using `gh` to view issues, PRs, or other GitHub objects: - -- **ALWAYS** use `--json` with `| cat` to avoid pager and scope errors: - - ```bash - gh issue view 64 --repo acgetchell/la-stack --json title,body | cat - ``` - -- To extract specific fields cleanly, combine `--json` with `--jq`: - - ```bash - gh issue view 64 --repo acgetchell/la-stack --json title,body --jq '.title + "\n" + .body' | cat - ``` - -- **AVOID** plain `gh issue view N` — it may fail with `read:project` - scope errors or open a pager. - -- For **arbitrary Markdown** (backticks, quotes, special characters) in - comments, prefer `--body-file -` with a heredoc: - - ```bash - gh issue comment 64 --repo acgetchell/la-stack --body-file - <<'EOF' - ## Heading - - Body with `backticks`, **bold**, and apostrophes that's safe. - EOF - ``` - -### GitHub Issues - -Use the `gh` CLI to read, create, and edit issues: - -- **Read**: `gh issue view --json title,body,labels,milestone | cat` -- **List**: - `gh issue list --json number,title,labels --jq '.[] | "#\(.number) \(.title)"' | cat` - (add `--label enhancement`, `--milestone v0.4.1`, etc. to filter) -- **Create**: `gh issue create --title "..." --body "..." --label enhancement --label rust` -- **Edit**: `gh issue edit --add-label "..."`, `--milestone "..."`, `--title "..."` -- **Comment**: `gh issue comment --body "..."` -- **Close**: `gh issue close ` (with optional `--reason completed` or `--reason "not planned"`) - -When creating or updating issues: - -- **Labels**: Use appropriate labels: `enhancement`, `bug`, `performance`, `documentation`, `rust`, `python`, etc. -- **Milestones**: Assign to the appropriate milestone (e.g., `v0.4.1`, `v0.5.0`) -- **Dependencies**: Document relationships in issue body and comments: - - "Depends on: #XXX" - this issue cannot start until #XXX is complete - - "Blocks: #YYY" - #YYY cannot start until this issue is complete - - "Related: #ZZZ" - related work but not blocking -- **Relationships**: GitHub automatically parses blocking keywords in comments to create visual relationships: - - Use `gh issue comment --body "Blocked by #XXX"` to mark an issue as blocked - - Use `gh issue comment --body "Blocks #YYY"` to mark an issue as blocking another - - GitHub will automatically create the relationship graph in the web UI - - Example: `gh issue comment 217 --body "Blocked by #207"` creates a blocking dependency -- **Issue body format**: Include clear sections: Summary, Current State, Proposed Changes, Benefits, Implementation Notes -- **Cross-referencing**: Always reference related issues/PRs using #XXX notation for automatic linking - -## Feature flags - -- `exact` — enables exact arithmetic methods via `BigRational`: - `det_exact()`, `det_exact_f64()`, `det_exact_rounded_f64()`, - `det_sign_exact()`, `solve_exact()`, `solve_exact_f64()`, and - `solve_exact_rounded_f64()`. `det_sign_exact()` is infallible for every - finite-by-construction `Matrix`; the exact-value, conversion, and solve APIs - remain fallible for their genuine scale, representation, and singularity - failures. `ExactF64Conversion` converts an already-computed - exact determinant or solution under the strict or rounded contract without - rerunning exact elimination. Feature-gated re-exports include - `DeterminantSign`, `ExactF64Conversion`, `BigInt`, `BigRational`, and the - commonly needed `num-traits` items (`FromPrimitive`, `ToPrimitive`, and `Signed`). - `UnrepresentableReason` and the other typed `LaError` category enums remain - available without `exact`; callers should not need optional arithmetic - dependencies merely to match errors. - Gates `src/exact.rs`, additional tests, and the exact-arithmetic examples. - Clippy, doc builds, and test commands have dedicated `--features exact` - variants. -- `bench` — cfg-only gate required by the benchmark targets and - `tests/vs_linalg_inputs.rs`. Benchmark libraries remain dev-dependencies. - -## Code structure (big picture) - -- This is a single Rust *library crate* (no `src/main.rs`). The crate root is `src/lib.rs`. -- The linear algebra implementation is split across: - - `src/lib.rs`: crate root, public module wiring, and re-exports - - `src/error.rs`: `LaError` plus typed singularity, non-finite, - positive-semidefinite, tolerance, factorization, arithmetic-operation, and - exact-conversion categories - - `src/tolerance.rs`: validated singular-tolerance policy - - `src/vector.rs`: `Vector` (`[f64; D]`) - - `src/matrix.rs`: `Matrix` (`[[f64; D]; D]`) + helpers (`get`, `try_get`, `set`, `norm_inf`, `det`, `det_direct`) - - `src/lu.rs`: `Lu` factorization with partial pivoting (`solve`, `det`) - - `src/ldlt.rs`: `Ldlt` factorization without pivoting for exactly - symmetric positive-definite matrices (`solve`, `det`) - - `src/exact.rs`: exact arithmetic behind `features = ["exact"]`: - - Determinants: `det_exact()`, strict `det_exact_f64()`, rounded - `det_exact_rounded_f64()`, and `det_sign_exact()` via a scaled `BigInt` - determinant core (`exact_det_int_finite`): direct expansions for D≤4 and - fraction-free Bareiss elimination for D≥5. `det_sign_exact()` infallibly - returns a `DeterminantSign` and adds a Shewchuk-style f64 filter for fast - sign resolution in D≤4 - - Exact-to-`f64` conversion failures retain an `UnrepresentableReason` so - callers can distinguish required rounding from non-finite output - - Linear system solve: `solve_exact()`, strict `solve_exact_f64()`, and - rounded `solve_exact_rounded_f64()` use fraction-free Bareiss forward - elimination in `BigInt` with first-non-zero pivoting, followed by - `BigRational` back-substitution -- Rust unit tests are inline `#[cfg(test)]` modules in each `src/*.rs` file. -- Property-based tests live under `tests/proptest_*.rs` (uses the `proptest` - dev-dependency): `proptest_matrix.rs`, `proptest_vector.rs`, - `proptest_factorizations.rs`, and `proptest_exact.rs` (the last gated on - the `exact` feature). They run as integration tests via - `just test-integration` or `just test-all`. -- Python tests live in `scripts/tests/` and run via `just test-python` (`uv run --locked pytest`). -- The public API re-exports these items from `src/lib.rs`. -- The `justfile` defines all dev workflows (see `just --list`). -- Dev-only benchmarks live in `benches/vs_linalg.rs` (Criterion + nalgebra/faer comparison) - and `benches/exact.rs` (exact arithmetic across D=2–5, plus adversarial-input groups - `exact_near_singular_3x3`, `exact_large_entries_3x3`, `exact_hilbert_4x4`, `exact_hilbert_5x5`). - Exact Criterion helpers accept only `ValidatedExactInput`, so independent - oracle validation is a type-checked prerequisite outside timed closures. -- Key Python scripts under `scripts/`: - - `bench_compare.py`: exact and vs-linalg Criterion comparison reports under - `target/bench-reports/` - - `archive_performance.py`: promote and archive curated release performance reports - - `criterion_dim_plot.py`: benchmark plotting and fail-closed README publication - (CSV + SVG + JSON provenance + README table) - - `tag_release.py`: annotated tag creation from CHANGELOG.md sections - - `archive_changelog.py`: archive completed changelog minor series - - `postprocess_changelog.py`: inject summaries, reflow and normalize Markdown, - and strip trailing blank lines from git-cliff output - - `subprocess_utils.py`: safe subprocess wrappers for git commands -- Release workflow is documented in `docs/RELEASING.md`. + Rust, including tests, examples, and benchmarks, except intentional + Semgrep fixtures under `tests/semgrep/` whose `f64::algebraic_*` usage is + validated by `just semgrep-test`. Their unspecified + reassociation, precision, and special-value behavior can invalidate error + bounds, exact fallbacks, non-finite classification, and reproducibility. + Ordinary operators and deliberate `f64::mul_add` remain allowed. Any + fast-math design requires a separate issue, opt-in contract, correctness + analysis, and benchmark evidence. +- Use const-generic dimensions for core types; do not introduce runtime + dimensions. Prefer stack storage and allocation-free paths. Heap use + belongs behind a feature flag or where exact arithmetic requires it. +- Keep optional dependencies feature-isolated and default builds minimal. +- Prefer `const fn` wherever possible; compile-time evaluation reinforces + pure operations. +- Public error enums and struct-style error variants are + `#[non_exhaustive]`; downstream matches include wildcard arms and `..`. + Public wrapper types are `#[must_use]`. +- New functionality is additive by default, with ergonomic prelude + re-exports. Pre-1.0 breaks are allowed when they materially improve + correctness, orthogonality, performance, or long-term clarity. Do not + retain compatibility aliases that weaken the model; document intentional + breaks in commit messages and release notes. +- Use `Result<_, LaError>` for fallible operations. Public library code + must not panic on user input. Plain-value APIs must be infallible for all + representable inputs; use `Result` or `Option` for observable failure + instead of `panic!`, `assert!`, `unwrap`, or `expect`. +- Panics are reserved for unreachable internal invariant violations and + must be documented when callers could observe them. +- Validate at the lowest owning type/module. Higher layers preserve and + propagate typed errors rather than stringifying them. +- Borrow by default: accept `&T`, `&mut T`, or `&[T]` and return + borrowed views where possible. Take ownership or allocate only when needed. +- Use textbook names such as `Matrix`, `Vector`, `Lu`, `Ldlt`, + `solve`, `det`, and `norm_inf`. + +## Testing and Performance + +- Test known values, typed error paths, and dimension-generic behavior + across D=2 through D=5 wherever possible. +- Match exact error variants, reasons, origins, locations, and structured + fields. Do not substitute numeric sentinels or assert only `is_err()`. +- Property tests verify algebraic invariants; adversarial inputs accompany + well-conditioned cases. Fast-filter/exact-fallback pairs must agree on the + domain where both are defined. +- Performance stays within the crate's small, fixed-dimension scope: + stack storage and closed forms where available. Large/dynamic dimensions, + sparse matrices, and parallelism belong to other libraries. +- Within that scope, prefer allocation-free kernels and deliberate FMA where + appropriate to the numerical contract. +- Performance-sensitive changes require comparable before-and-after + measurements with the same command, inputs, features, and environment. + Preserve provenance and distinguish descriptive ratios from supported + performance claims. Invariant-violating runs are invalid evidence. +- Read [Testing guidance](docs/dev/testing.md) for dimension macros, + Criterion measurement rules, and independent benchmark validation. + +## Documentation + +- Under `docs/`, use uppercase verb/gerund filenames for task guides with + execution instructions (`RELEASING.md`, `BENCHMARKING.md`); prefer gerunds. + Use lowercase descriptive filenames for invariants, principles, reference + material, and reports. Classify by primary purpose; see the + [filename conventions](docs/dev/docs.md#filename-conventions) for details. +- `README.md` owns concise orientation, quickstart, and API navigation; + `REFERENCES.md` owns bibliographic provenance; `docs/mathematical_basis.md` + owns mathematical explanations, assumptions, guarantees, and derivations. + Link between owners instead of duplicating detail. +- Place "Use this crate when" immediately after Introduction. Maintain + Contents navigation and put API selection and geometry scope near the top + of the mathematical basis. +- Sort independent algorithm discussions and unordered capability bullets + lexicographically within coherent groups; preserve prerequisite and + procedural order. Keep the bibliography thematic and citation identifiers + and deep links stable. Link claims to specific references. +- Read [Documentation guidance](docs/dev/docs.md) for README inclusion in + rustdoc, guide placement, feature-gated doctests, link destinations, + scientific notation, and generated-file rules. + +## Validation + +- Select validators proportionally to the changed surfaces. Compose each + relevant focused validator once for mixed changes; core Rust or public + behavior changes require final `just ci`. +- Use [Contributor validation guidance](CONTRIBUTING.md#validation-workflow) + for the surface-to-command mapping. The [justfile](justfile) and + `just --list` own the full command catalog. +- Run `just spell-check` after editing. Add legitimate technical terms to + `typos.toml` under `[default.extend-words]`. +- For Markdown changes, run `just markdown-fix` and `just markdown-ci` + (Markdown and spelling checks). Changed guide docs also require + `just doc-check`; changed executable examples require the default and + matching feature doctests. + +## Python + +- Support tooling targets Python 3.14. Use `uv run --locked` for all + Python scripts; never invoke `python` or `python3` directly. +- Use pytest, not unittest, and add type hints to new code. +- `just python-check` includes blocking type checking; all code must pass. +- Read the [Scripts guide](scripts/README.md) for maintenance rules and + entry-point ownership. + +## Project Context + +This is one Rust library crate, rooted at `src/lib.rs`. Core dimensions are +compile-time constants. The `exact` feature enables arbitrary-precision +arithmetic; `bench` gates benchmark targets while their dependencies remain +dev-only. The [Code organization guide](docs/code_organization.md) maps +modules, features, tests, and tooling. Update that guide when file ownership +or layout changes. ## Agent Expectations - Prefer small, focused patches and the simplest maintainable correct solution. - Search existing documentation and nearby code before inventing conventions. -- Fix small, clearly related issues discovered in a touched area when doing so - improves correctness, clarity, tests, or maintainability. +- Fix small, clearly related issues in touched areas when doing so improves + correctness, clarity, tests, or maintainability. - Avoid broad mechanical churn; separate repository-wide cleanup from focused work. - -## Publishing note - -- If you publish this crate to crates.io, prefer updating documentation - *before* publishing a new version (doc-only changes still require a version bump on crates.io). - -## Editing tools policy - -- Never use `sed`, `awk`, `python`, or `perl` to edit code or write file changes. -- These tools may be used for read-only inspection, parsing, or analysis, but never for writing. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d67146d..e7cce9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,6 +44,15 @@ checks spelling. Python support tooling is locked with `uv` and checked by Ruff, Ty, and Semgrep. GitHub Actions references are SHA-pinned, restricted to an explicit allowlist, and kept with readable version comments for review. +`just zizmor` uses the same pinned scanner and `regular` persona as the SARIF +workflow. It runs online audits using `ZIZMOR_GITHUB_TOKEN`, `GH_TOKEN`, +`GITHUB_TOKEN`, or an authenticated `gh auth token`, in that order, without +printing the token. Without authentication it reports an offline fallback; +SHA/version-comment resolution and other online findings are then unchecked. +Use `ZIZMOR_OFFLINE=true just zizmor` to request offline audits explicitly. +Zizmor owns remote action SHA/tag resolution; Semgrep guards explicit scanner +version configuration and cache isolation in release workflows. + CI runs `just ci` on Ubuntu, macOS, and Windows to keep platform coverage aligned with the local comprehensive validation path. @@ -128,6 +137,23 @@ For final validation of a non-core change, compose each affected surface once: - Benchmark inputs or harnesses: `just test-bench-inputs` or `just bench-compile` - Examples: `just examples` +For individual edited file formats, use these focused checks and formatters: + +| Surface | Commands | +|---------|----------| +| JSON | `jq empty .json` or `just validate-json` | +| TOML | `just toml-lint`, `just toml-fmt-check`; format with `just toml-fmt` | +| GitHub Actions | `just action-lint` | +| Shell scripts | `just shell-fix`, then `just shell-check` | +| YAML | `just yaml-fix`, then `just yaml-lint` | +| Markdown | `just markdown-fix`, then `just markdown-ci` | + +Run `just spell-check` after editing; `markdown-ci` already includes it. Add +legitimate technical terms to `typos.toml` under `[default.extend-words]`. +Detailed test design, dimension coverage, and benchmark measurement rules live +in [Testing guidance](docs/dev/testing.md). README and guide doctest requirements +live in [Documentation guidance](docs/dev/docs.md). + Run `just ci` for core Rust, public behavior, or GitHub-equivalent validation. It composes leaf validators directly and runs `clippy-all-targets` to match the GitHub Clippy SARIF workflow. Unit and integration tests still run together once @@ -147,13 +173,17 @@ Use the existing canonical documents instead of duplicating their guidance: | Topic | Canonical reference | |-------|---------------------| | Agent rules and repository invariants | [`AGENTS.md`](AGENTS.md) | +| GitHub and agent commit-message procedures | [`docs/dev/MANAGING_CHANGES.md`](docs/dev/MANAGING_CHANGES.md) | +| Module, feature, and file ownership | [`docs/code_organization.md`](docs/code_organization.md) | +| Test design and dimension coverage | [`docs/dev/testing.md`](docs/dev/testing.md) | +| Documentation ownership and rustdoc maintenance | [`docs/dev/docs.md`](docs/dev/docs.md) | | User-facing API, examples, and project scope | [`README.md`](README.md) | | Mathematical basis and numerical validity | [`docs/mathematical_basis.md`](docs/mathematical_basis.md) | | Package metadata, features, and dependencies | [`Cargo.toml`](Cargo.toml) | | Commands and validation workflow | [`justfile`](justfile), `just --list` | | Python support tooling | [`scripts/README.md`](scripts/README.md) | | Benchmark methodology and baselines | [`docs/BENCHMARKING.md`](docs/BENCHMARKING.md) | -| Coverage workflow and reports | [`docs/COVERAGE.md`](docs/COVERAGE.md) | +| Coverage workflow and reports | [`docs/MEASURING_COVERAGE.md`](docs/MEASURING_COVERAGE.md) | | Citations and bibliography | [`CITATION.cff`](CITATION.cff), [`REFERENCES.md`](REFERENCES.md) | | Security reporting and support | [`SECURITY.md`](SECURITY.md) | | Releases and changelog generation | [`docs/RELEASING.md`](docs/RELEASING.md), [`CHANGELOG.md`](CHANGELOG.md) | @@ -212,7 +242,7 @@ benefit that justifies it. Core Rust, Cargo, or public-behavior changes must pass `just ci` before a pull request is ready. Documentation, configuration, Python, test-only, benchmark-only, and example-only changes use the matching focused validators -documented in [`AGENTS.md`](AGENTS.md). Pull requests are reviewed for correctness, +listed in [Validation Workflow](#validation-workflow). Pull requests are reviewed for correctness, mathematical accuracy, tests, documentation, style, dependency impact, and performance. Non-substantive whitespace or formatting churn may be declined unless it is part of an intentional tooling cleanup. @@ -231,7 +261,8 @@ canonical rules and invariants for AI coding assistants and autonomous agents working on this codebase. AI tools, including ChatGPT, Claude, CodeRabbit, Codex, KiloCode, and WARP, are -expected to read and follow `AGENTS.md` when proposing or applying changes. +expected to read and follow `AGENTS.md` and its task-relevant linked guidance +when proposing or applying changes. Portions of this library were developed with the assistance of these tools: diff --git a/Cargo.lock b/Cargo.lock index f7cb4ec..22ded94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -577,9 +577,9 @@ checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" [[package]] name = "glam" -version = "0.33.6" +version = "0.33.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21fef0953c54fd3de2f44b743fbf77e044c81a25faee03636dfccc0d35135e23" +checksum = "e762b9b188634fc508d4ec54b62ea3d352c43fd431d18d05d46f559f217f4725" [[package]] name = "half" @@ -703,7 +703,7 @@ dependencies = [ "glam 0.30.10", "glam 0.31.1", "glam 0.32.1", - "glam 0.33.6", + "glam 0.33.7", "matrixmultiply", "num-complex", "num-rational", diff --git a/README.md b/README.md index 1eb1399..46cc1ae 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@ while keeping the API intentionally small and explicit. ## Contents - [Introduction](#-introduction) +- [Use this crate when](#-use-this-crate-when) - [Quickstart](#-quickstart) - [Mathematical basis](#-mathematical-basis) - [Design goals](#-design-goals) - [Anti-goals](#-anti-goals) -- [Use this crate when](#-use-this-crate-when) - [Scalar and bounded-value types](#-scalar-and-bounded-value-types) - [Features](#-features) - [Adaptive determinant filtering (D ≤ 4)](#adaptive-determinant-filtering-d--4) @@ -49,26 +49,40 @@ while keeping the API intentionally small and explicit. `la-stack` provides a handful of const-generic, stack-backed building blocks: -- `Vector` for fixed-length `f64` vectors backed by `[f64; D]` - `gram_matrix(&[Vector; M])` for allocation-free `Matrix` construction from pairwise vector inner products, with bit-for-bit symmetry. Gram matrices encode lengths and angles and support simplex/facet volume calculations; see - [Gram matrices and geometric measures](REFERENCES.md#gram-matrices-and-geometric-measures). + [Gram matrices and geometric measures][refs-gram]. Each independent dot product is checked once; rounding has no certified error bound, and positive definiteness or affine independence must still be established by factorization or the caller. Benchmark square simplex and rectangular facet inputs through dimension 8 with `cargo bench --locked --features bench --bench gram`. -- `Matrix` for fixed-size square `f64` matrices backed by `[[f64; D]; D]` - `Interval` and `IntervalMatrix` for outward-rounded, proof-bearing determinant filters through D=7 -- `ScalarWithErrorBound` for proof-bearing fixed-vector dot products and - affine differences over finite `f64` inputs -- `RationalVector` and `RationalMatrix` for - exact rational inputs behind the optional `"exact"` feature -- `Lu` for LU factorization with partial pivoting (solve + det) - `Ldlt` for no-pivot factorization intended for exactly symmetric positive-definite matrices (solve + det; typed pivot diagnostics) +- `Lu` for LU factorization with partial pivoting (solve + det) +- `Matrix` for fixed-size square `f64` matrices backed by `[[f64; D]; D]` +- `RationalVector` and `RationalMatrix` for + exact rational inputs behind the optional `"exact"` feature +- `ScalarWithErrorBound` for proof-bearing fixed-vector dot products and + affine differences over finite `f64` inputs +- `Vector` for fixed-length `f64` vectors backed by `[f64; D]` + +## ✅ Use this crate when + +- Robust predicates matter for geometry-style workloads near degeneracy +- Stack allocation and `Copy` value semantics fit your data flow +- You need a certified sign or threshold comparison for a fixed-vector dot + product or `axis · (left - right)` expression +- You need a cheap, sound interval filter for determinant expressions assembled + from rounded binary64 operations +- You need exact determinants, exact determinant signs, or exact linear solves + for fixed-size systems +- You prefer a default build with no runtime dependencies +- You want explicit LU / LDLT / determinant APIs rather than a broad algebra toolkit +- Your matrices and vectors have small, fixed dimensions known at compile time ## 🚀 Quickstart @@ -123,29 +137,31 @@ provide a certified solution error bound. ## 🧮 Mathematical basis `la-stack` operates on finite IEEE 754 binary64 values in small, fixed -dimensions. Its floating-point paths use LU with partial pivoting, LDLT without -pivoting for exactly symmetric positive-definite matrices, and closed-form +dimensions. Its floating-point paths use [LU with partial pivoting][refs-lu], +[LDLT without pivoting][refs-ldlt] for exactly symmetric positive-definite matrices, and closed-form determinants through D=4. These results remain subject to conditioning and binary64 rounding; factorization tolerances are rejection thresholds, not accuracy guarantees. For -D≤4, direct determinants can be paired with a conservative absolute roundoff -bound when its range preconditions hold. Fixed-vector dot products and direct -affine differences can likewise return a paired estimate and certified absolute +D≤4, direct determinants can be paired with a +[conservative absolute roundoff bound][refs-det-bound] when its range +preconditions hold. [Fixed-vector dot products and direct affine differences][refs-reductions] +can likewise return a paired estimate and certified absolute roundoff bound without enabling arbitrary-precision dependencies. Derived binary64 expressions can instead be assembled with `Interval` subtraction, addition, multiplication, negation, and square. The resulting -`IntervalMatrix` determinant sign is certified through D=7 when its enclosure +`IntervalMatrix` [determinant sign][refs-interval] is certified through D=7 when its enclosure separates zero; the singleton `[0, 0]` also certifies exact zero. Every other overlap with zero is explicitly inconclusive. This default-feature surface is distinct from arbitrary-precision exact arithmetic. With `features = ["exact"]`, callers can either lift stored binary64 inputs -losslessly or supply already-exact rational inputs for exact determinant signs, -determinant values, and solves. Exactness over binary64 input starts at the +losslessly or supply already-exact rational inputs for +[exact determinant signs][refs-exact-sign], determinant values, and +[solves][refs-exact-solve]. Exactness over binary64 input starts at the stored values and cannot recover information rounded away before construction. See the -[mathematical basis](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/mathematical_basis.md) +[mathematical basis](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md) for the algorithms, validity boundaries, and supporting references. ## 🎯 Design goals @@ -191,20 +207,6 @@ for current release planning. - Broad general-purpose linear algebra: use [`nalgebra`](https://crates.io/crates/nalgebra) - Large matrices/dimensions with parallelism: use [`faer`](https://crates.io/crates/faer) -## ✅ Use this crate when - -- Your matrices and vectors have small, fixed dimensions known at compile time -- Stack allocation and `Copy` value semantics fit your data flow -- You want explicit LU / LDLT / determinant APIs rather than a broad algebra toolkit -- You need exact determinants, exact determinant signs, or exact linear solves - for fixed-size systems -- You need a cheap, sound interval filter for determinant expressions assembled - from rounded binary64 operations -- You need a certified sign or threshold comparison for a fixed-vector dot - product or `axis · (left - right)` expression -- Robust predicates matter for geometry-style workloads near degeneracy -- You prefer a default build with no runtime dependencies - ## 🔢 Scalar and bounded-value types The public point-value scalar model deliberately has two input domains: @@ -359,10 +361,10 @@ for the full contracts. ## 🗺️ Documentation Map - [API guide][api-guide] — worked examples, API selection, storage, and error contracts. -- [Mathematical basis](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/mathematical_basis.md) — algorithms, numerical guarantees, and limitations. +- [Mathematical basis](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md) — algorithms, numerical guarantees, and limitations. - [Benchmarking](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/BENCHMARKING.md) — benchmark suites, comparison workflows, and measurement methodology. -- [Performance reports](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/PERFORMANCE.md) — release-to-release measurement results and provenance. -- [Coverage](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/COVERAGE.md) — local and CI coverage commands and report locations. +- [Performance reports](https://github.com/acgetchell/la-stack/blob/main/docs/performance.md) — release-to-release measurement results and provenance. +- [Coverage](https://github.com/acgetchell/la-stack/blob/main/docs/MEASURING_COVERAGE.md) — local and CI coverage commands and report locations. - [Roadmap](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/roadmap.md) — release planning, future directions, and non-goals. - [Releasing](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/RELEASING.md) — release preparation, validation, and publication. @@ -395,7 +397,7 @@ For the full per-kernel comparison methodology, algorithm citations, input construction, and release-comparison workflow details, see [docs/BENCHMARKING.md](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/BENCHMARKING.md). For the current release-to-release performance snapshot, see -[docs/PERFORMANCE.md](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/PERFORMANCE.md). +[docs/performance.md](https://github.com/acgetchell/la-stack/blob/main/docs/performance.md). The exact release suite includes the already-exact rational-input groups for D=2 through D=8. Those rows report `RationalMatrix::det_sign`, `det`, and `solve` alongside straightforward `BigRational` Gaussian determinant and solve @@ -482,7 +484,7 @@ CI runs `just ci` on Ubuntu, macOS, and Windows to keep platform coverage aligned with the local comprehensive validation path. For coverage commands and report locations, see -[`docs/COVERAGE.md`](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/COVERAGE.md). +[`docs/MEASURING_COVERAGE.md`](https://github.com/acgetchell/la-stack/blob/main/docs/MEASURING_COVERAGE.md). For the full contributor workflow, see [CONTRIBUTING.md](https://github.com/acgetchell/la-stack/blob/v0.4.5/CONTRIBUTING.md). @@ -517,3 +519,11 @@ BSD 3-Clause License. See [LICENSE](https://github.com/acgetchell/la-stack/blob/ [clippy-badge]: https://github.com/acgetchell/la-stack/actions/workflows/rust-clippy.yml/badge.svg [clippy-workflow]: https://github.com/acgetchell/la-stack/actions/workflows/rust-clippy.yml [lu-solve-benchmark]: https://raw.githubusercontent.com/acgetchell/la-stack/v0.4.5/docs/assets/bench/vs_linalg_lu_solve_median.svg +[refs-det-bound]: https://github.com/acgetchell/la-stack/blob/main/REFERENCES.md#absolute-error-bound-for-closed-form-determinants +[refs-exact-sign]: https://github.com/acgetchell/la-stack/blob/main/REFERENCES.md#exact-determinant-sign-adaptive-precision-integer-arithmetic +[refs-exact-solve]: https://github.com/acgetchell/la-stack/blob/main/REFERENCES.md#exact-linear-system-solve-hybrid-bareiss--bigrational +[refs-gram]: https://github.com/acgetchell/la-stack/blob/main/REFERENCES.md#gram-matrices-and-geometric-measures +[refs-interval]: https://github.com/acgetchell/la-stack/blob/main/REFERENCES.md#outward-rounded-interval-determinant-sign +[refs-ldlt]: https://github.com/acgetchell/la-stack/blob/main/REFERENCES.md#ldlᵀ-factorization-exactly-symmetric-positive-definite-inputs +[refs-lu]: https://github.com/acgetchell/la-stack/blob/main/REFERENCES.md#lu-decomposition-gaussian-elimination-with-partial-pivoting +[refs-reductions]: https://github.com/acgetchell/la-stack/blob/main/REFERENCES.md#certified-fixed-vector-reductions diff --git a/REFERENCES.md b/REFERENCES.md index 2368ad5..0f0fe86 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -1,5 +1,25 @@ # References and citations +## Contents + +- [How to cite this library](#how-to-cite-this-library) +- [AI-Assisted Development Tools](#ai-assisted-development-tools) +- [Linear algebra algorithms](#linear-algebra-algorithms) + - [Absolute error bound for closed-form determinants](#absolute-error-bound-for-closed-form-determinants) + - [Certified fixed-vector reductions](#certified-fixed-vector-reductions) + - [Exact determinant sign](#exact-determinant-sign-adaptive-precision-integer-arithmetic) + - [Exact linear system solve](#exact-linear-system-solve-hybrid-bareiss--bigrational) + - [Exact rational inputs](#exact-rational-inputs-and-row-denominator-clearing) + - [Exact-to-binary64 conversion](#exact-to-binary64-conversion) + - [f64 → integer decomposition](#f64--integer-decomposition-decompose_proven_finite_f64) + - [Gram matrices and geometric measures](#gram-matrices-and-geometric-measures) + - [LDLᵀ factorization](#ldlt-factorization) + - [LU decomposition](#lu-decomposition-gaussian-elimination-with-partial-pivoting) + - [Outward-rounded interval determinant sign](#outward-rounded-interval-determinant-sign) + - [Scaled determinant products](#scaled-determinant-products) + - [Scaled Euclidean vector norm](#scaled-euclidean-vector-norm) +- [References](#references) + ## How to cite this library If you use this library in your research or project, please cite it using the information in @@ -23,77 +43,43 @@ No generated content was used without human oversight. ## Linear algebra algorithms -### Gram matrices and geometric measures - -A Gram matrix collects pairwise inner products: `G[i,j] = v_i · v_j`. -Writing the vectors as rows of `V` gives `G = V Vᵀ`. Its diagonal contains -squared lengths; off-diagonal entries describe angles through -`v_i · v_j = ||v_i|| ||v_j|| cos(θ)` for nonzero vectors. - -In exact real arithmetic, `G` is positive semidefinite and is positive definite -exactly when the vectors are linearly independent. For `M ≤ N`, `det(G)` is -the squared M-dimensional volume spanned by the vectors. For simplex edge -vectors from a common vertex, the simplex volume is `sqrt(det(G)) / M!` [16]. -This applies to triangles embedded in 3D and to higher-dimensional facets. +### Absolute error bound for closed-form determinants -`gram_matrix` computes rounded binary64 entries with exact mirrored symmetry; -it does not certify rank, positive definiteness, or volume accuracy. See [9-12] -for floating-point and conditioning background. +`Matrix::det_errbound()` returns a conservative Shewchuk-style absolute error bound \[[8]\] +for `Matrix::det_direct()` in dimensions 2–4 when every rounded intermediate is normal +or an exact structural zero. The returned bound is the rounded product +`fl(ERR_COEFF_D · p_hat)`, where `p_hat` is the computed approximation to +`p(|A|) = perm(|A|)`, the absolute Leibniz sum. The dimension-specific constants +`ERR_COEFF_2`, `ERR_COEFF_3`, and `ERR_COEFF_4` cover rounding in the determinant, +the permanent, and the final multiplication. +The [coefficient derivation](docs/mathematical_basis.md#derivation-of-the-returned-determinant-bound) +counts the longest rounding paths in both dense and sparse evaluation trees and +proves the returned binary64 bound using the arithmetic model in \[[9], [10], [11]\]. +The method returns `None` when gradual underflow could violate the relative-error model. +The same bound is used internally by `det_sign_exact()`'s fast filter, but +`det_errbound()` itself is available without the `exact` feature, so downstream crates +can build custom adaptive-precision logic with pure f64 arithmetic. ### Certified fixed-vector reductions `Vector::dot_with_errbound()` and `Vector::dot_difference_with_errbound()` use deterministic left-to-right binary64 FMA reductions. When their rounded intermediates stay normal or are exact zeros, the standard -`gamma_n = nu / (1 - nu)` model bounds the absolute forward error by -`gamma_n Σ |a_i b_i|` (references 9–11). The magnitude sum and final bound are +`gamma_n = n·u / (1 - n·u)` model, with `u = 2^-53`, `n = D` for the dot +product, and `n = 2D` for the affine difference, bounds the absolute forward error by +`gamma_n Σ |a_i b_i|` \[[9], [10], [11]\]. The magnitude sum and final bound are rounded upward, while `TwoSum` supplies outward endpoints. Gradual underflow or proof-only range exhaustion makes the filter unavailable rather than turning an inconclusive result into equality. The affine form evaluates alternating `axis_i × left_i` and `-axis_i × right_i` FMAs, so its certificate covers the original coordinates rather than an already-rounded difference vector. -### Outward-rounded interval determinant sign - -`Interval` uses IEEE-754 round-to-nearest binary64 operations plus adjacent -representable values to enclose exact-real addition, subtraction, -multiplication, and square results (references 9–11). Addition and subtraction -use an error-free `TwoSum` residual [8]; multiplication independently compares -the exact integer-significand product with the rounded binary64 result, -including gradual underflow to zero. Results whose exact range cannot fit -between finite binary64 endpoints return a typed range failure rather than -storing infinity. For the broader standardized interval arithmetic model, see -[14]; this crate does not claim IEEE 1788 conformance. - -`IntervalMatrix::det()` evaluates the Leibniz expansion with a division-free -column-subset dynamic program. It uses `2^D` inline interval states and -`D × 2^(D-1)` coefficient products through D=7. A determinant interval strictly -separated from zero certifies its sign; `[0, 0]` certifies zero; every other -overlap is explicitly inconclusive. The determinant identity is standard -linear algebra (reference 12); the interval evaluation and subset-DP -organization are implemented specifically for this crate's small -fixed-dimension scope. - -### Absolute error bound for closed-form determinants - -`Matrix::det_errbound()` returns a conservative Shewchuk-style absolute error bound [8] -for `Matrix::det_direct()` in dimensions 2–4 when every rounded intermediate is normal -or an exact structural zero. The bound has the form -`ERR_COEFF_D · p(|A|)`, where `p(|A|) = perm(|A|)` is the absolute Leibniz sum—the -combinatorial permanent of the entrywise-absolute matrix—and -`ERR_COEFF_D ∈ {ERR_COEFF_2, ERR_COEFF_3, ERR_COEFF_4}` is a dimension-specific constant -derived from the rounding-event count of `det_direct`. -The method returns `None` when gradual underflow could violate the relative-error model. -The same bound is used internally by `det_sign_exact()`'s fast filter, but -`det_errbound()` itself is available without the `exact` feature, so downstream crates -can build custom adaptive-precision logic with pure f64 arithmetic. - ### Exact determinant sign (adaptive-precision integer arithmetic) -`det_sign_exact()` uses a Shewchuk-style f64 error-bound filter [8] (the same bound exposed +`det_sign_exact()` uses a Shewchuk-style f64 error-bound filter \[[8]\] (the same bound exposed by `det_errbound()` above) backed by exact `BigInt` arithmetic. Each f64 entry is decomposed into `mantissa × 2^exponent` and scaled to a common integer base. Dimensions 0–4 use direct -integer determinant expansions; D ≥ 5 uses integer-only Bareiss elimination [7]. Neither +integer determinant expansions; D ≥ 5 uses integer-only Bareiss elimination \[[7]\]. Neither path constructs `BigRational` values or performs GCD normalization. See `src/exact.rs` for the full architecture description. @@ -101,42 +87,86 @@ See `src/exact.rs` for the full architecture description. `solve_exact()`, `solve_exact_f64()`, and `solve_exact_rounded_f64()` share the determinant path's exact f64 decomposition and integer scaling. Matrix and RHS entries are decomposed via -IEEE 754 bit extraction [9]. Matrix and RHS scales start from their respective minimum +IEEE 754 bit extraction \[[9]\]. Matrix and RHS scales start from their respective minimum exponents. When `|e_rhs − e_matrix| ≤ 64`, both sides use `min(e_rhs, e_matrix)` as the shared scale; when `|e_rhs − e_matrix| > 64`, they retain independent scales so one side is not inflated excessively. Forward elimination runs in `BigInt` using Bareiss fraction-free updates -[7]—no `BigRational` and no GCD normalisation in the `O(D³)` phase. The upper-triangular +\[[7]\]—no `BigRational` and no GCD normalisation in the `O(D³)` phase. The upper-triangular result is then lifted into `BigRational` for back-substitution, where fractions are inherent and the cost is only `O(D²)`. Row swaps from first-non-zero pivoting are applied to both the matrix and RHS. After back-substitution, multiplying by the exact power-of-two scale ratio `2^(e_rhs − e_matrix)` recovers the solution to the original `A x = b` system. +### Exact rational inputs and row denominator clearing + +`RationalMatrix` and `RationalVector` accept exact rational coefficients and +canonicalize each quotient to lowest terms with a positive denominator. For row +`i`, a positive denominator LCM `s_i` makes `A_int[i, :] = s_i A[i, :]` integral. +The LCM is accumulated with `lcm(a, b) = (a / gcd(a, b)) b`, using Euclidean GCD. +Determinant multilinearity gives `det(A_int) = (Π s_i) det(A)` \[[12]\]: the +positive scales preserve sign, and determinant values divide by their product. +Solves include the RHS denominator in each row's LCM, preserving the solution +set of the augmented system. The resulting integer matrices reuse direct +expansions through D=4 and the Bareiss determinant/solve backend \[[7]\]. +See the [rational-input construction](docs/mathematical_basis.md#exact-arithmetic-over-rational-inputs). + +### Exact-to-binary64 conversion + +Strict conversion accepts only dyadic rationals whose reduced significand and +exponent fit binary64 exactly. Rounded conversion uses round-to-nearest, +ties-to-even, including subnormal values and signed underflow to zero \[[9], [10]\]. +The integer-and-exponent determinant path reads retained, guard, and sticky +bits directly from `BigInt`; rational-value rounding delegates to +`num-rational`'s `ToPrimitive::to_f64`. Both paths reject rounding overflow. +`RequiresRounding` means a strict conversion failed but finite rounded output +is available; `NotFinite` means rounding cannot produce finite output. +See the [representability and rounding criteria](docs/mathematical_basis.md#exact-to-binary64-conversion). + ### f64 → integer decomposition (`decompose_proven_finite_f64`) Both the determinant and solve paths convert their finite-by-construction entries via `decompose_proven_finite_f64`, which extracts the IEEE 754 binary64 sign, unbiased exponent, -and significand [9]. For nonzero `x`, it strips trailing zeros from the +and significand \[[9]\]. For nonzero `x`, it strips trailing zeros from the significand so `|x| = m · 2^e` with `m` odd; signed zeros use a separate zero component. The integer matrix is then assembled by shifting each mantissa left by `exp − e_min`, giving a GCD-free exact-integer starting point. Solves and D ≥ 5 determinants then apply Bareiss elimination; D ≤ 4 determinants use direct expansions. The test-only fallible wrapper `decompose_f64` verifies rejection of non-finite raw scalars, while the test-only `f64_to_big_rational` helper packages the same decomposition into a single -`BigRational`. See Goldberg [10] for background on floating-point representation and +`BigRational`. See Goldberg \[[10]\] for background on floating-point representation and conversion. +### Gram matrices and geometric measures + +A Gram matrix collects pairwise inner products: `G[i,j] = v_i · v_j`. +Writing the vectors as rows of `V` gives `G = V Vᵀ`. Its diagonal contains +squared lengths; off-diagonal entries describe angles through +`v_i · v_j = ||v_i|| ||v_j|| cos(θ)` for nonzero vectors. + +In exact real arithmetic, `G` is positive semidefinite and is positive definite +exactly when the vectors are linearly independent. For `M ≤ N`, `det(G)` is +the squared M-dimensional volume spanned by the vectors. For simplex edge +vectors from a common vertex, the simplex volume is `sqrt(det(G)) / M!` \[[16]\]. +This applies to triangles embedded in 3D and to higher-dimensional facets. + +`gram_matrix` computes rounded binary64 entries with exact mirrored symmetry; +it does not certify rank, positive definiteness, or volume accuracy. See \[[9], [10], [11], [12]\] +for floating-point and conditioning background. + + + ### LDLᵀ factorization (exactly symmetric positive-definite inputs) The no-pivot LDLT implementation targets `A = L D Lᵀ` for exactly symmetric -positive-definite inputs [4-5, 11-12]. Successful construction requires every +positive-definite inputs \[[4], [5], [11], [12]\]. Successful construction requires every computed diagonal pivot to be positive and greater than the caller's tolerance. Computed zero and tolerance-small positive pivots are therefore part of the typed diagnostic domain, not returned in a usable factorization. Because the pivots are computed in binary64, a successful factorization is not an exact certificate that the represented matrix is positive definite. -For pivoted variants used for symmetric *indefinite* matrices, see [6, 11-12]. +For pivoted variants used for symmetric *indefinite* matrices, see \[[6], [11], [12]\]. ### LU decomposition (Gaussian elimination with partial pivoting) @@ -145,80 +175,136 @@ selects the remaining entry of largest magnitude in the active column. Partial pivoting is a practical stability strategy, not an unconditional accuracy guarantee; worst-case growth and average-case behavior are distinct concerns. -See [1-3, 11-12] for stability analysis, finite-precision behavior, and standard +See \[[1], [2], [3], [11], [12]\] for stability analysis, finite-precision behavior, and standard algorithmic background. +### Outward-rounded interval determinant sign + +`Interval` uses IEEE-754 round-to-nearest binary64 operations plus adjacent +representable values to enclose exact-real addition, subtraction, +multiplication, and square results \[[9], [10], [11]\]. Addition and subtraction +use an error-free `TwoSum` residual \[[8]\]; multiplication independently compares +the exact integer-significand product with the rounded binary64 result, +including gradual underflow to zero. Results whose exact range cannot fit +between finite binary64 endpoints return a typed range failure rather than +storing infinity. For the broader standardized interval arithmetic model, see +\[[14]\]; this crate does not claim IEEE 1788 conformance. + +`IntervalMatrix::det()` evaluates the Leibniz expansion with a division-free +column-subset dynamic program. It uses `2^D` inline interval states and +`D × 2^(D-1)` coefficient products through D=7. A determinant interval strictly +separated from zero certifies its sign; `[0, 0]` certifies zero; every other +overlap is explicitly inconclusive. The determinant identity is standard +linear algebra \[[12]\]; the interval evaluation and subset-DP +organization are implemented specifically for this crate's small +fixed-dimension scope. + +### Scaled determinant products + +`Lu::det` and `Ldlt::det` first multiply factor diagonals directly. A zero, +subnormal, or non-finite running product triggers a complete replay with +normalized mantissas and a separate power-of-two exponent. Each nonzero factor +has `1 ≤ mantissa < 2`; normalization therefore keeps intermediate mantissa +products away from overflow and underflow \[[9], [10]\]. + +The last factor is deferred so a subnormal final result needs only one final +rounding in the destination range. Earlier mantissa products still round: +scaling does not make the product exact or supply a certified error bound. +This crate-specific range-management implementation also retains LU permutation +parity. See the [scaled-product description](docs/mathematical_basis.md#scaled-determinant-products). + ### Scaled Euclidean vector norm `Vector::norm` maintains a scale and a sum of squares relative to that scale, avoiding raw coordinate squares that would overflow or underflow. This follows -the scaled safe-norm approach described by Blue [15]. The implementation retains +the scaled safe-norm approach described by Blue \[[15]\]. The implementation retains a deterministic coordinate order and documents its binary64 rounding contract; it does not generally claim a certified error bound or correct rounding. A fixed-size integer sum of exact binary64 squares handles the upper range, using -the representation and nearest-even rounding model in [9-10] to distinguish +the representation and nearest-even rounding model in \[[9], [10]\] to distinguish finite results from overflow. ## References -1. Trefethen, Lloyd N., and Robert S. Schreiber. "Average-case stability of Gaussian elimination." +Reference numbers are stable citation keys used throughout the code and documentation. +Each entry has a permanent anchor such as `#ref-8` for direct links. +The bibliography retains its thematic order; algorithm headings above are +alphabetized for navigation without renumbering citations. + +1. Trefethen, Lloyd N., and Robert S. Schreiber. "Average-case stability of Gaussian elimination." *SIAM Journal on Matrix Analysis and Applications* 11.3 (1990): 335–360. [DOI](https://doi.org/10.1137/0611023) · [PDF](https://people.maths.ox.ac.uk/trefethen/publication/PDF/1990_44.pdf) -2. Businger, P. A. "Monitoring the Numerical Stability of Gaussian Elimination." +2. Businger, P. A. "Monitoring the Numerical Stability of Gaussian Elimination." *Numerische Mathematik* 16.4 (1971): 360–361. [DOI](https://doi.org/10.1007/BF02165006) · [Full text](https://eudml.org/doc/132040) -3. Huang, Han, and K. Tikhomirov. "Average-case analysis of the Gaussian elimination with partial pivoting." +3. Huang, Han, and K. Tikhomirov. "Average-case analysis of the Gaussian elimination with partial pivoting." *Probability Theory and Related Fields* 189 (2024): 501–567. [DOI](https://doi.org/10.1007/s00440-024-01276-2) · [Open-access article](https://link.springer.com/article/10.1007/s00440-024-01276-2) · [arXiv:2206.01726](https://arxiv.org/abs/2206.01726) -4. Cholesky, André-Louis. "Sur la résolution numérique des systèmes d'équations linéaires." +4. Cholesky, André-Louis. "Sur la résolution numérique des systèmes d'équations linéaires." *Bulletin de la Sabix* 39 (2005): 81–95. Manuscript dated 2 December 1910. [DOI](https://doi.org/10.4000/sabix.529) -5. Brezinski, Claude. "La méthode de Cholesky." +5. Brezinski, Claude. "La méthode de Cholesky." *Revue d'histoire des mathématiques* 11.2 (2005): 205–238. [DOI](https://doi.org/10.24033/rhm.30) · [Full text](https://www.numdam.org/articles/10.24033/rhm.30/) -6. Bunch, James R., Linda Kaufman, and Beresford N. Parlett. "Decomposition of a Symmetric Matrix." +6. Bunch, James R., Linda Kaufman, and Beresford N. Parlett. "Decomposition of a Symmetric Matrix." *Numerische Mathematik* 27 (1976): 95–109. [DOI](https://doi.org/10.1007/BF01399088) · [Full text](https://eudml.org/doc/132435) -7. Bareiss, Erwin H. "Sylvester's Identity and Multistep Integer-Preserving Gaussian +7. Bareiss, Erwin H. "Sylvester's Identity and Multistep Integer-Preserving Gaussian Elimination." *Mathematics of Computation* 22.103 (1968): 565–578. [DOI](https://doi.org/10.1090/S0025-5718-1968-0226829-0) · [PDF](https://www.ams.org/journals/mcom/1968-22-103/S0025-5718-1968-0226829-0/S0025-5718-1968-0226829-0.pdf) -8. Shewchuk, Jonathan Richard. "Adaptive Precision Floating-Point Arithmetic and Fast +8. Shewchuk, Jonathan Richard. "Adaptive Precision Floating-Point Arithmetic and Fast Robust Geometric Predicates." *Discrete & Computational Geometry* 18.3 (1997): 305–363. [DOI](https://doi.org/10.1007/PL00009321) · [PDF](https://people.eecs.berkeley.edu/~jrs/papers/robustr.pdf) Also: Technical Report CMU-CS-96-140, Carnegie Mellon University, May 1996. -9. IEEE Computer Society. "IEEE Standard for Floating-Point Arithmetic." *IEEE Std 754-2019* +9. IEEE Computer Society. "IEEE Standard for Floating-Point Arithmetic." *IEEE Std 754-2019* (Revision of IEEE 754-2008), 2019. [DOI](https://doi.org/10.1109/IEEESTD.2019.8766229) Section 3.4 (binary64 format): 1 sign bit, 11 exponent bits (bias 1023), 52 trailing significand bits; subnormals have biased exponent 0 with implicit leading 0. -10. Goldberg, David. "What Every Computer Scientist Should Know About Floating-Point +10. Goldberg, David. "What Every Computer Scientist Should Know About Floating-Point Arithmetic." *ACM Computing Surveys* 23.1 (1991): 5–48. [DOI](https://doi.org/10.1145/103162.103163) · [Authorized HTML reprint](https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html) Comprehensive survey of floating-point representation, rounding, and conversion. -11. Higham, Nicholas J. *Accuracy and Stability of Numerical Algorithms*. 2nd ed. +11. Higham, Nicholas J. *Accuracy and Stability of Numerical Algorithms*. 2nd ed. Society for Industrial and Applied Mathematics, 2002. [DOI](https://doi.org/10.1137/1.9780898718027) -12. Golub, Gene H., and Charles F. Van Loan. *Matrix Computations*. 4th ed. +12. Golub, Gene H., and Charles F. Van Loan. *Matrix Computations*. 4th ed. Johns Hopkins University Press, 2013. [DOI](https://doi.org/10.56021/9781421407944) · [Publisher record](https://www.press.jhu.edu/books/title/10678/matrix-computations) -13. Kalibera, Tomas, and Richard Jones. "Rigorous Benchmarking in Reasonable Time." +13. Kalibera, Tomas, and Richard Jones. "Rigorous Benchmarking in Reasonable Time." *Proceedings of the 2013 International Symposium on Memory Management* (ISMM '13), 2013: 63–74. [DOI](https://doi.org/10.1145/2464157.2464160) -14. IEEE Computer Society. "IEEE Standard for Interval Arithmetic." +14. IEEE Computer Society. "IEEE Standard for Interval Arithmetic." *IEEE Std 1788-2015*, 2015: 1–97. [DOI](https://doi.org/10.1109/IEEESTD.2015.7140721) · [IEEE record](https://standards.ieee.org/ieee/1788/4431/) -15. Blue, James L. "A Portable Fortran Program to Find the Euclidean Norm of a Vector." +15. Blue, James L. "A Portable Fortran Program to Find the Euclidean Norm of a Vector." *ACM Transactions on Mathematical Software* 4.1 (1978): 15–23. [DOI](https://doi.org/10.1145/355769.355771) -16. Kock, Anders. "Square-densities, and volume forms." Notes, December 10, 2020. +16. Kock, Anders. "Square-densities, and volume forms." Notes, December 10, 2020. Introduction and §1.2 (Gram's formula). [Author's PDF](https://math.au.dk/~kock/heron4.pdf) + +[1]: #ref-1 +[2]: #ref-2 +[3]: #ref-3 +[4]: #ref-4 +[5]: #ref-5 +[6]: #ref-6 +[7]: #ref-7 +[8]: #ref-8 +[9]: #ref-9 +[10]: #ref-10 +[11]: #ref-11 +[12]: #ref-12 +[14]: #ref-14 +[15]: #ref-15 +[16]: #ref-16 diff --git a/benches/exact.rs b/benches/exact.rs index d9329a1..d1ecb50 100644 --- a/benches/exact.rs +++ b/benches/exact.rs @@ -17,7 +17,7 @@ //! ill-conditioning (wide range of `(mantissa, exponent)` pairs in //! the `decompose_f64 → BigInt` path). These measure tail behaviour //! that fixed well-conditioned inputs miss and provide stronger -//! empirical evidence for `docs/PERFORMANCE.md`. +//! empirical evidence for `docs/performance.md`. //! 3. **Random corpus benches** (`exact_random_corpus_d{2..5}`) — a //! fixed-seed corpus of diagonally-dominant random matrices per dimension. //! Every measured iteration executes the full corpus in its stable order, diff --git a/docs/BENCHMARKING.md b/docs/BENCHMARKING.md index 3660d3f..aed21d8 100644 --- a/docs/BENCHMARKING.md +++ b/docs/BENCHMARKING.md @@ -301,7 +301,7 @@ is unavailable, but those measurements cannot be promoted as reproducible release evidence. The pair is validated and published before the temporary worktree is removed. -`docs/PERFORMANCE.md` is then rendered from a validated reload of that retained +`docs/performance.md` is then rendered from a validated reload of that retained pair, and the previous committed report is archived under `docs/archive/performance/`. Archive filenames are release-pair names such as `v0.4.2-vs-v0.4.1.md`. Serialization, validation, rendering, coverage, or @@ -317,7 +317,7 @@ just performance-doc This command fails closed on a missing, partial, malformed, mismatched, or unsupported artifact pair. It consumes the default CSV/JSON pair retained by a successful `performance-local` or `performance-release` run, rewrites the -scratch Markdown, promotes it to `docs/PERFORMANCE.md`, and archives the previous +scratch Markdown, promotes it to `docs/performance.md`, and archives the previous committed report when the release pair changes. Promotion requires distinct current and baseline package versions, so a same-version local comparison is retained and reproducible but cannot become release documentation. Use promotion @@ -359,7 +359,7 @@ shared-harness workflow before attributing a difference solely to library code. | `target/bench-reports/github-assets-performance.md` | No | `performance-github-assets` | Local report from published release artifacts. | | `target/bench-reports/github-assets-performance.csv` | No | `performance-github-assets` | Tabular inputs derived from published native archives. | | `target/bench-reports/github-assets-performance.provenance.json` | No | `performance-github-assets` | Provenance for the published-asset report inputs. | -| `docs/PERFORMANCE.md` | Yes | `performance-release`, `performance-doc` | Latest curated release-to-release comparison. | +| `docs/performance.md` | Yes | `performance-release`, `performance-doc` | Latest curated release-to-release comparison. | | `docs/archive/performance/` | Yes | `performance-release`, `performance-doc` | Older curated release-to-release comparisons. | | `docs/assets/bench/` | Yes | `performance-readme` | README benchmark CSV/SVG assets and JSON provenance. | | GitHub Release | Remote | `.github/workflows/release-benchmarks.yml` | Criterion baseline archive. | @@ -683,4 +683,4 @@ just bench-save-last The durable published baseline is the GitHub Release artifact created by `.github/workflows/release-benchmarks.yml`. That workflow runs the benchmark-input correctness gate before timing or packaging the artifact. The committed release -comparison is `docs/PERFORMANCE.md`, created by `just performance-release`. +comparison is `docs/performance.md`, created by `just performance-release`. diff --git a/docs/COVERAGE.md b/docs/MEASURING_COVERAGE.md similarity index 99% rename from docs/COVERAGE.md rename to docs/MEASURING_COVERAGE.md index 66bab83..3328e1d 100644 --- a/docs/COVERAGE.md +++ b/docs/MEASURING_COVERAGE.md @@ -1,4 +1,4 @@ -# Coverage +# Measuring Coverage la-stack uses `cargo-llvm-cov` with `cargo-nextest` for local and CI coverage. Both coverage recipes use Rust's LLVM source-based instrumentation, run the diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 881ea06..d0d22ab 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -104,7 +104,7 @@ just performance-release ``` The no-argument form compares the current package version with the previous -stable published release. Review `docs/PERFORMANCE.md`, any archived comparison +stable published release. Review `docs/performance.md`, any archived comparison under `docs/archive/performance/`, and the retained CSV and provenance JSON under `target/bench-reports/`. @@ -148,7 +148,7 @@ git --no-pager diff ``` Expected release artifacts include package metadata and lockfiles, -`CITATION.cff`, `CHANGELOG.md`, `README.md`, `docs/PERFORMANCE.md`, and generated +`CITATION.cff`, `CHANGELOG.md`, `README.md`, `docs/performance.md`, and generated files under `docs/archive/` and `docs/assets/bench/`. Stage only the reviewed paths that were intentionally changed; do not stage the entire `docs/` tree. Then inspect the staged diff and commit it: @@ -236,6 +236,12 @@ gh release view "$TAG" --json assets \ The command must print `la-stack-$TAG-criterion-baseline.tar.gz`. A short-lived Actions artifact is not a substitute for this release asset. +The benchmark producer has read-only repository permissions and restores no +dependency caches, including tool binaries. It disables Rust toolchain and +`setup-just` caching and installs the pinned just and cargo-nextest versions +with `cargo install --locked`. Only the separate publisher job receives +`contents: write` to attach the packaged baseline to the release. + ### 7. Remove the merged release branch After publication and baseline verification succeed: diff --git a/docs/archive/performance/README.md b/docs/archive/performance/README.md index 286cec9..b66572d 100644 --- a/docs/archive/performance/README.md +++ b/docs/archive/performance/README.md @@ -1,7 +1,7 @@ # Archived Performance Reports Older release-to-release benchmark comparisons are archived here. -`docs/PERFORMANCE.md` contains the latest curated comparison. +`docs/performance.md` contains the latest curated comparison. - [v0.4.1-vs-v0.4.0](v0.4.1-vs-v0.4.0.md) - [v0.4.2-vs-v0.4.1](v0.4.2-vs-v0.4.1.md) diff --git a/docs/code_organization.md b/docs/code_organization.md new file mode 100644 index 0000000..0303099 --- /dev/null +++ b/docs/code_organization.md @@ -0,0 +1,118 @@ +# Code Organization + +Module, feature, and file-placement guidance for the single Rust library crate. +Read [AGENTS.md](../AGENTS.md) for agent rules and use the +[contributor workflow](../CONTRIBUTING.md) for setup and validation commands. + +## Contents + +- [Library modules](#library-modules) +- [Feature boundaries](#feature-boundaries) +- [Tests and examples](#tests-and-examples) +- [Benchmarks and support tooling](#benchmarks-and-support-tooling) +- [Documentation owners](#documentation-owners) + +## Library modules + +`src/lib.rs` wires private implementation modules and public re-exports. It +also owns the documentation-only `guide` module, the private README doctest +mirrors, and the public prelude. There is no `src/main.rs`. + +| Module | Owns | +|--------|------| +| [`src/error.rs`](../src/error.rs) | `LaError` and typed singularity, non-finite, positive-semidefinite, tolerance, factorization, arithmetic-operation, and exact-conversion categories | +| [`src/exact.rs`](../src/exact.rs) | Exact determinants and solves, determinant-sign filtering, and exact-to-`f64` conversion | +| [`src/gram.rs`](../src/gram.rs) | Fixed-size Gram construction from vector dot products | +| [`src/interval.rs`](../src/interval.rs) | Interval scalars, matrices, and determinant-sign certification | +| [`src/ldlt.rs`](../src/ldlt.rs) | Unpivoted `Ldlt` factorization for exactly symmetric positive-definite matrices, solves, and determinants | +| [`src/lu.rs`](../src/lu.rs) | Partially pivoted `Lu` factorization, solves, and determinants | +| [`src/matrix.rs`](../src/matrix.rs) | `Matrix`, finite storage, accessors, matrix operations, norms, and direct determinant APIs | +| [`src/norm.rs`](../src/norm.rs) | Exact range-boundary fallback for the approximate Euclidean norm | +| [`src/rational.rs`](../src/rational.rs) | `RationalMatrix` and `RationalVector`, canonical rational inputs, and denominator clearing for exact arithmetic | +| [`src/rounding.rs`](../src/rounding.rs) | Shared binary64 rounding primitives for certified arithmetic | +| [`src/scaled_product.rs`](../src/scaled_product.rs) | Allocation-free scaled products for floating-point factor diagonals | +| [`src/tolerance.rs`](../src/tolerance.rs) | Validated singular-tolerance policy | +| [`src/vector.rs`](../src/vector.rs) | `Vector`, finite storage, reductions, norms, and certified scalar results | + +Keep shared arithmetic invariants in the lowest owning module and expose public +items through `src/lib.rs`. Mathematical derivations and algorithm references +belong in [Mathematical basis](mathematical_basis.md) and +[REFERENCES.md](../REFERENCES.md). + +## Feature boundaries + +The [Cargo manifest](../Cargo.toml) owns feature and dependency declarations. + +- **`exact`** gates `src/exact.rs`, `src/rational.rs`, their additional tests, + exact guides, and exact-arithmetic examples. It enables `det_exact()`, + strict `det_exact_f64()`, rounded `det_exact_rounded_f64()`, + `det_sign_exact()`, `solve_exact()`, strict `solve_exact_f64()`, and + rounded `solve_exact_rounded_f64()` for finite binary64 inputs, plus the + rational-input types. +- `det_sign_exact()` is infallible for finite-by-construction `Matrix` values. + Exact-value, conversion, and solve APIs retain their genuine scale, + representation, and singularity failures. +- `ExactF64Conversion` converts an already-computed determinant or solution + under the strict or rounded contract without rerunning exact elimination. + Feature-gated re-exports include `DeterminantSign`, `ExactF64Conversion`, + `RationalMatrix`, `RationalVector`, `BigInt`, `BigRational`, `FromPrimitive`, + `ToPrimitive`, and `Signed`. +- Typed error categories, including `UnrepresentableReason`, remain available + without `exact`; matching errors must not require optional arithmetic + dependencies. +- The scaled `BigInt` determinant core uses direct expansions for D≤4 and + fraction-free Bareiss elimination for D≥5. Exact signs add a floating-point + filter for D≤4. Exact solves use fraction-free forward elimination with + first-non-zero pivoting and `BigRational` back-substitution. Rational inputs + clear denominators before reusing the integer backend. +- **`bench`** is a cfg-only gate for benchmark targets and + `tests/vs_linalg_inputs.rs`; benchmark libraries remain dev-dependencies. + +## Tests and examples + +- Rust unit tests live in inline `#[cfg(test)]` modules in `src/*.rs`. +- `tests/proptest_*.rs` covers matrix, vector, factorization, exact, rational, + interval, and Gram properties. Exact and rational suites require `exact`. +- Other `tests/*.rs` suites cover regressions, API contracts, allocation + behavior, conversion boundaries, and benchmark inputs. Shared property-test + configuration lives in `tests/common/`. +- `tests/semgrep/` contains deliberate static-analysis fixtures, not ordinary + runnable API examples. +- Python tests live under `scripts/tests/` and run with pytest through + `just test-python` or the aggregate validation workflows. +- `examples/` contains complete runnable workflows. README mirrors and public + guide doctests live in `src/lib.rs`. + +Use [Testing guidance](dev/testing.md) for dimension coverage and test design, +and [Documentation guidance](dev/docs.md) for executable-example ownership. + +## Benchmarks and support tooling + +`benches/` contains Criterion suites for exact arithmetic, Gram construction, +intervals, linear forms, and nalgebra/faer comparisons. Helpers under +`benches/common/` own fixture and oracle validation. Exact benchmark helpers +accept only `ValidatedExactInput`, after independent validation outside timing. +Adversarial groups include near-singular, large-entry, and Hilbert inputs. + +The [Benchmarking guide](BENCHMARKING.md) owns benchmark commands, methodology, +baselines, output locations, and report promotion. The [Scripts guide](../scripts/README.md) +owns the Python script inventory and entry points for comparisons, plotting, +release metadata, changelog generation/archiving, and tag preparation. +The [justfile](../justfile) owns executable development workflows. + +## Documentation owners + +Use [Documentation guidance](dev/docs.md) for README, references, mathematical +background, rustdoc, and generated-file ownership. Focused operational rules +live in `docs/dev/`; human setup and validation guidance lives in +[CONTRIBUTING.md](../CONTRIBUTING.md). Release procedures belong in +[Releasing](RELEASING.md). + +[Managing changes](dev/MANAGING_CHANGES.md) owns Git and GitHub procedures. +[Measuring coverage](MEASURING_COVERAGE.md) owns local and CI coverage execution. +The generated [performance report](performance.md) records measured results; +[Benchmarking](BENCHMARKING.md) owns the commands that produce and compare them. + +When adding, removing, renaming, or moving files, update the applicable ownership +rows here. Prefer links to the detailed owner over copying its procedure into +this map or `AGENTS.md`. diff --git a/docs/dev/MANAGING_CHANGES.md b/docs/dev/MANAGING_CHANGES.md new file mode 100644 index 0000000..e7718e3 --- /dev/null +++ b/docs/dev/MANAGING_CHANGES.md @@ -0,0 +1,115 @@ +# Managing Git and GitHub Changes + +Operational details for the Git rules in [AGENTS.md](../../AGENTS.md). + +## Contents + +- [Git operations and branch names](#git-operations-and-branch-names) +- [Commit messages](#commit-messages) +- [GitHub CLI](#github-cli) +- [Issue planning](#issue-planning) +- [Issue dependencies](#issue-dependencies) + +## Git operations and branch names + +Agents use read-only Git commands with `git --no-pager`. Never run commits, +pushes, tags, or other commands that mutate refs or the index; suggest those +commands for the maintainer to run manually. Preserve unrelated user changes. + +Prefer branch names of the form `{type}/{issue}-descriptor-or-two`, for example +`fix/307-topology-validation`, `perf/315-bench-profile`, or +`doc/329-branch-guidance`. If an environment requires an owner/tool prefix, +retain that structure after it, for example +`codex/fix/307-topology-validation`. + +## Commit messages + +When asked to generate a commit message: + +1. Run `git --no-pager diff --cached --stat` and inspect the staged diff. +2. Use `: ` with `feat`, `fix`, `refactor`, `perf`, + `docs`, `test`, `chore`, `style`, `ci`, or `build`. +3. Include organized body bullets describing the changes and test results. +4. Present the message in a code block with no language so the user can commit + manually. + +Document intentional API breaks explicitly. See the +[contributor commit-message guide](../../CONTRIBUTING.md#commit-message-format) +for Conventional Commit examples and breaking-change markers. Changelog +generation and release procedures belong in [Releasing](../RELEASING.md). + +## GitHub CLI + +Use structured output and `| cat` when reading GitHub objects to avoid pagers +and scope errors: + +```bash +gh issue view 64 --repo acgetchell/la-stack --json title,body | cat +gh issue view 64 --repo acgetchell/la-stack --json title,body \ + --jq '.title + "\n" + .body' | cat +gh issue list --repo acgetchell/la-stack --json number,title,labels \ + --jq '.[] | "#\(.number) \(.title)"' | cat +``` + +Avoid plain `gh issue view N`, which may open a pager or fail with +`read:project` scope errors. Include `labels` and `milestone` in `--json` +when inspecting issue placement; use `--label` and `--milestone` to filter lists. + +For arbitrary Markdown in issue bodies or comments, use `--body-file` with a +file or a quoted heredoc. For example: + +```bash +gh issue comment 64 --repo acgetchell/la-stack --body-file - <<'EOF' +## Summary + +Body with `backticks`, **bold**, and apostrophes that's safe. +EOF +``` + +Use `gh issue create`, `gh issue edit`, `gh issue comment`, and +`gh issue close` for the requested operation. Creation supports `--title`, +`--body-file`, `--label`, and `--milestone`; edits support those metadata +changes and `--add-label`. Close with the appropriate reason, `completed` or +`not planned`. + +## Issue planning + +- Use appropriate existing labels, such as `enhancement`, `bug`, + `performance`, `documentation`, `rust`, or `python`. +- Assign the appropriate milestone and preserve the maintainer's requested + release placement. +- Structure new issue bodies around Summary, Current State, Proposed Changes, + Benefits, and Implementation Notes. +- Cross-reference related work with `#XXX`, or `owner/repo#XXX` across + repositories. Distinguish actual prerequisites from related work. +- Record dependency intent clearly in prose, such as `Depends on: #XXX`, + `Blocks: #YYY`, or `Related: #ZZZ`, and use native metadata for blocking + relationships. +- Verify issue contents and metadata after creating or updating them. + +## Issue dependencies + +Create blocking relationships through GitHub's native dependency metadata. +Prose cross-references document intent; verify the relationship itself through +the [issue-dependency API](https://docs.github.com/en/rest/issues/issue-dependencies). +The API takes the blocking issue's internal integer ID, not its issue number. + +For example, to make issue 217 blocked by issue 207: + +```bash +# Inspect the blocker and existing relationships first. +gh api repos/acgetchell/la-stack/issues/207 --jq '.id' | cat +gh api repos/acgetchell/la-stack/issues/217/dependencies/blocked_by \ + --jq '[.[].number]' | cat + +# Replace BLOCKING_ISSUE_ID with the returned integer before running. +gh api repos/acgetchell/la-stack/issues/217/dependencies/blocked_by \ + -X POST -F issue_id=BLOCKING_ISSUE_ID | cat + +# Verify the relationship after adding it. +gh api repos/acgetchell/la-stack/issues/217/dependencies/blocked_by \ + --jq '[.[].number]' | cat +``` + +Use `-F` so the resolved numeric ID is encoded as an integer. Keep independent +tasks related without inventing a blocking dependency. diff --git a/docs/dev/docs.md b/docs/dev/docs.md new file mode 100644 index 0000000..ff22738 --- /dev/null +++ b/docs/dev/docs.md @@ -0,0 +1,129 @@ +# Documentation Guidance + +Detailed documentation rules for [AGENTS.md](../../AGENTS.md). + +## Contents + +- [Document ownership and ordering](#document-ownership-and-ordering) +- [Filename conventions](#filename-conventions) +- [README and rustdoc guides](#readme-and-rustdoc-guides) +- [Links and published destinations](#links-and-published-destinations) +- [Executable examples](#executable-examples) +- [Scientific notation and references](#scientific-notation-and-references) +- [Release metadata and generated files](#release-metadata-and-generated-files) + +## Document ownership and ordering + +- `README.md` owns concise orientation, quickstart, capabilities, and API + navigation. Place "Use this crate when" immediately after Introduction. +- `REFERENCES.md` owns bibliographic provenance. Keep the bibliography thematic + and preserve citation identifiers and deep links. +- `docs/mathematical_basis.md` owns mathematical explanations, assumptions, + guarantees, and derivations. Put API selection and geometry scope near the + top, before detailed algorithm discussions. +- Maintain Contents navigation. Sort independent algorithm discussions and + unordered capability bullets lexicographically within coherent groups; + preserve prerequisite and procedural order. +- Link between content owners instead of duplicating detail. Keep useful + explanations and numerical limitations when moving or shortening material. +- `AGENTS.md` is the short entry point for agent rules. Keep detailed Git, + testing, and documentation policy in the matching `docs/dev/` guide and route + tasks there from the Required Reading table. +- [Code organization](../code_organization.md) owns module and file placement. + Update its map when ownership or layout changes; keep command procedures in + their existing contributor, benchmark, script, and release guides. + +## Filename conventions + +Under `docs/`, including its subdirectories, name documents by their primary +purpose: + +| Purpose | Filename convention | Examples | +|---------|---------------------|----------| +| Task guide with commands and instructions for execution | Uppercase verb or verb phrase, preferably a gerund | `RELEASING.md`, `BENCHMARKING.md`, `MEASURING_COVERAGE.md`, `TUNING_PERFORMANCE.md` | +| Discussion of invariants, principles, policy, or architecture | Lowercase descriptive name | `mathematical_basis.md`, `code_organization.md`, `dev/testing.md` | +| Reference material, analysis, or results report | Lowercase descriptive name | `roadmap.md`, `performance.md` | + +A few command examples do not turn a discussion into a task guide. For example, +`dev/testing.md` explains test-design policy; `TESTING.md` would describe how to +execute a testing workflow. Keep conventional directory indexes named +`README.md`. This convention does not change standard filenames at the repository +root, such as `AGENTS.md`, `CONTRIBUTING.md`, and `REFERENCES.md`. + +Apply the convention to new documents and deliberate documentation renames. +`MEASURING_COVERAGE.md` describes coverage execution, and `performance.md` +reports measured results. Git and GitHub procedures live in +`dev/MANAGING_CHANGES.md`. Reserve `TUNING_PERFORMANCE.md` for a guide +to optimizing code; `BENCHMARKING.md` owns measurement and comparison workflows. +When renaming a document, update links, ownership maps, validation, and any +generators or configured output paths together. Preserve historical archive +paths; never rename a generated report without updating its source workflow. + +## README and rustdoc guides + +`src/lib.rs` includes the README with +`#![doc = include_str!("../README.md")]`, so README examples are also the +docs.rs landing-page examples. + +Keep the README quickstart and brief capability descriptions discoverable. Put +fuller worked API examples and caller contracts in the documentation-only +`guide` module in `src/lib.rs`. Preserve numerical limitations and error +semantics in the linked guide when shortening a README section. + +## Links and published destinations + +- Link API references and worked API guides to docs.rs. Keep repository-owned + mathematical background, benchmark reports, roadmap, contributing, and + release instructions on GitHub, using absolute URLs to the intended revision. +- README links and Contents anchors must work on both GitHub and generated + rustdoc. Repository-relative file links may resolve incorrectly from rustdoc; + verify destinations in both contexts. +- Within Rust docs, use intra-doc links where the referenced item is available + under the selected features. +- Choose docs.rs `latest` links for intentionally current guidance and explicit + versions for release-specific contracts. docs.rs builds published crates; + merging changes does not publish new guide pages. Local rendering does not + establish availability on the published site. + +## Executable examples + +- When changing Rust examples in `README.md`, mirror executable versions in + the private `readme_doctests` module in `src/lib.rs`. Keep mirrors hidden so + they do not duplicate the landing page, but runnable by `cargo test --doc`. +- README examples requiring optional features may remain `rust,ignore` for + default-feature doctest compatibility. Give them hidden mirrors gated by the + matching `#[cfg(feature = "...")]`, and verify the matching feature set, + for example `cargo test --features exact --doc`. +- Guide examples run directly as doctests. Gate feature-dependent guides with + the matching feature; remove obsolete private mirrors when examples move out + of README into guides. +- Run `just doc-check` for changed guide docs and inspect generated pages and + anchors for explicit links, which rustdoc does not validate. Test changed + executable examples with the default and matching feature doctest recipes. +- Ordinary Markdown changes use `just markdown-fix` and `just markdown-ci`; + the latter includes Markdown and spelling checks. + +## Scientific notation and references + +Unicode mathematics is welcome when it improves readability, including +`×`, `≤`, `≥`, `∈`, `Σ`, `²`, and `2^-50`. State invariants mathematically +where possible, for example `|A[i][i]| > Σ_{j≠i} |A[i][j]|`. + +Use numbered `REFERENCES.md` citations such as `\[8\]` and `\[9-10\]` in +algorithm documentation. Link scientific claims to specific reference sections +or entries and describe conditioning behavior. Keep source attribution with the +owning explanation when material moves. + +## Release metadata and generated files + +Do not bump package versions during ordinary documentation work. When the +maintainer explicitly requests release/version changes, keep README `la-stack` +dependency snippets synchronized with `Cargo.toml` and follow +[Releasing](../RELEASING.md). Update documentation before publishing a new crate +version: crates.io documentation changes also require a new version. + +Never edit `CHANGELOG.md` directly. `just changelog` generates, post-processes, +archives, and formats the changelog; `just changelog-unreleased ` +prepends unreleased changes. Commit-message guidance lives in +[Git and GitHub guidance](MANAGING_CHANGES.md). Benchmark report and generated asset ownership +belongs in [Benchmarking](../BENCHMARKING.md). diff --git a/docs/dev/testing.md b/docs/dev/testing.md new file mode 100644 index 0000000..a081e8c --- /dev/null +++ b/docs/dev/testing.md @@ -0,0 +1,102 @@ +# Testing Guidance + +Test-design details for [AGENTS.md](../../AGENTS.md). Command selection belongs +in the [contributor validation workflow](../../CONTRIBUTING.md#validation-workflow). + +## Contents + +- [Scientific coverage](#scientific-coverage) +- [Dimension coverage](#dimension-coverage) +- [Focused execution](#focused-execution) +- [Benchmark evidence](#benchmark-evidence) + +## Scientific coverage + +- Unit tests cover known values, error paths, and dimension-generic behavior. +- Match exact error variants, typed reasons, origins, locations, and structured + fields. Never replace an unexpected error with a numeric sentinel or assert + only `is_err()`. +- Property tests under `tests/proptest_*.rs` verify algebraic invariants such + as round trips, residuals, and sign agreement, rather than merely checking + that an operation does not panic. +- Near-singular, large-entry, and Hilbert-style ill-conditioned inputs accompany + well-conditioned cases in both tests and benchmarks. +- When a public API has a fast filter and an exact fallback for the same + question, a property test verifies agreement wherever both are defined. + +## Dimension coverage + +Dimension-generic code must cover D=2 through D=5 whenever possible. Use a macro +that accepts a dimension literal and generates the corresponding tests: + +```rust +macro_rules! gen_tests { + ($d:literal) => { + paste! { + #[test] + fn []() { + // Call a const-generic helper and assert the result. + } + } + }; +} + +gen_tests!(2); +gen_tests!(3); +gen_tests!(4); +gen_tests!(5); +``` + +Keep the macro body thin. Shared setup and assertions belong in readable, +independently testable const-generic helper functions. + +Existing patterns include: + +- [`src/matrix.rs`](../../src/matrix.rs): `gen_matrix_tests!`. +- [`src/lu.rs`](../../src/lu.rs): `gen_pivoting_solve_and_det_tests!` and + `gen_tridiagonal_smoke_solve_and_det_tests!`. +- [`src/ldlt.rs`](../../src/ldlt.rs): `gen_ldlt_identity_tests!` and + `gen_ldlt_diagonal_tests!`. +- [`src/exact.rs`](../../src/exact.rs): `gen_exact_identity_tests!`, + `gen_det_exact_f64_agrees_with_det_direct!`, `gen_solve_exact_tests!`, and + `gen_solve_exact_f64_agrees_with_lu!`. + +Single-dimension tests are appropriate for inherently dimension-specific known +values or errors requiring a particular layout; they need not be macro-generated. + +## Focused execution + +Select tests that exercise the changed behavior, then run the required final +validation for the affected surfaces. Useful targeted commands include: + +```bash +cargo nextest run solve_2x2_basic +cargo nextest run -- --exact lu::tests::solve_2x2_basic +cargo nextest run --test proptest_matrix +``` + +Doctests run separately from nextest. README mirrors, guide examples, and +feature-dependent doctests follow [Documentation guidance](docs.md). + +## Benchmark evidence + +Performance remains subordinate to mathematical correctness, API stability, +composability, and clarity. When those conflict with speed, re-scope the problem +rather than weaken an invariant. + +- Use comparable before-and-after measurements from the same representative + command, inputs, features, and environment. Choose `bench-vs-linalg` for + nalgebra/faer comparisons or `bench-exact` for exact arithmetic as appropriate. +- Prefer Criterion `bencher.iter` for nanosecond-scale fixed-size kernels so + the complete public operation is measured symmetrically. +- Use `iter_batched` only when setup is explicitly outside the estimand, its + exclusion is comparable across implementations, and a same-binary comparison + shows batching does not materially distort the result. +- Preserve provenance. Point-estimate ratios are descriptive; marginal + Criterion interval separation is not a paired confidence interval for a change. +- Runs that violate documented invariants are invalid performance evidence. + Exact Criterion helpers accept only `ValidatedExactInput`; independent oracle + validation occurs outside timed closures before that type is constructed. + +The [Benchmarking guide](../BENCHMARKING.md) owns command matrices, comparison +methodology, input validation, report promotion, and release-artifact provenance. diff --git a/docs/mathematical_basis.md b/docs/mathematical_basis.md index 54d02c8..0cc41f2 100644 --- a/docs/mathematical_basis.md +++ b/docs/mathematical_basis.md @@ -1,5 +1,28 @@ # Mathematical basis +## Contents + +- [Introduction](#introduction) +- [Choosing an API](#choosing-an-api) +- [Geometry relationship and scope](#geometry-relationship-and-scope) +- [Represented values and arithmetic model](#represented-values-and-arithmetic-model) +- [Tolerances and typed errors](#tolerances-and-typed-errors) +- [Linear algebra algorithms](#linear-algebra-algorithms) + - [Certified fixed-vector reductions](#certified-fixed-vector-reductions) + - [Determinants and certified sign filtering](#determinants-and-certified-sign-filtering) + - [Derivation of the returned determinant bound](#derivation-of-the-returned-determinant-bound) + - [Exact arithmetic over binary64 inputs](#exact-arithmetic-over-binary64-inputs) + - [Exact arithmetic over rational inputs](#exact-arithmetic-over-rational-inputs) + - [Exact-to-binary64 conversion](#exact-to-binary64-conversion) + - [Gram matrices and geometric measures](#gram-matrices-and-geometric-measures) + - [LDLT without pivoting](#ldlt-without-pivoting) + - [LU with partial pivoting](#lu-with-partial-pivoting) + - [Outward-rounded interval expressions](#outward-rounded-interval-expressions) + - [Scaled determinant products](#scaled-determinant-products) + - [Scaled Euclidean vector norm](#scaled-euclidean-vector-norm) + +## Introduction + `la-stack` provides fixed-dimension numerical linear algebra over two deliberate point-value input domains plus one bounded layer. `Matrix` and `Vector` store finite IEEE 754 binary64 values; their default algorithms operate in @@ -19,6 +42,36 @@ This document separates three questions that are easy to conflate: Reference numbers point to [REFERENCES.md](../REFERENCES.md). +## Choosing an API + +| Need | API | Important boundary | +|------|-----|--------------------| +| General floating solve | `lu(tol)` then `solve` | Approximate; absolute pivot policy | +| Positive-definite floating solve | `ldlt(tol)` then `solve` | Exact symmetry; computed pivots must exceed tolerance; success is not a certificate | +| Floating determinant, any `D` | `det` | No certified bound; zero is not exact singularity | +| `D ≤ 4` error-bounded determinant/sign test | `det_direct_with_errbound` | Sign is certified when estimate magnitude exceeds bound; otherwise inconclusive | +| Derived-expression determinant sign through `D ≤ 7` | `IntervalMatrix::det_sign` | Outward-rounded proof; overlap with zero is explicitly inconclusive | +| Exact determinant sign | `det_sign_exact` | Exact for stored binary64 entries | +| Exact determinant value or solve | `det_exact`, `solve_exact` | Exact for represented inputs | +| Exact operations over preassembled rationals | `RationalMatrix::det_sign`, `det`, `solve` | No intermediate binary64 reconstruction | +| Binary64 output from an exact result | Strict or rounded conversions | Strict conversion forbids rounding | + +## Geometry relationship and scope + +Orientation, in-sphere, and related geometric predicates can be reduced to +determinant signs, which is why an adaptive exact sign is useful near degeneracy +\[8\]. `la-stack` supplies both point-matrix and bounded-expression determinant +primitives; callers still own problem-specific matrix assembly and semantic +classification. The crate originated to support +[`delaunay`](https://crates.io/crates/delaunay), but its matrix, factorization, +and exact-arithmetic APIs are general numerical infrastructure. + +The deliberate anti-goals are dynamically sized or rectangular matrices, +sparse storage, broad decomposition coverage, alternate floating scalar +families, and GPU-, parallel-, BLAS-, or LAPACK-scale throughput. Those problems +need different storage models and numerical policies rather than extensions to +this small fixed-dimension design. + ## Represented values and arithmetic model Public matrix and vector construction rejects NaN and infinity. Because the @@ -54,85 +107,34 @@ error bounds. This includes plain `Vector::dot`, Euclidean and squared norms, matrix norms, factorizations, and solves. Some kernels use FMA to reduce rounding steps, but that does not make them exact. -## Scaled Euclidean vector norm - -`Vector::norm` computes `sqrt(Σᵢ xᵢ²)` with a left-to-right scaled -sum-of-squares recurrence. Its state represents the accumulated squared norm as -`scale² × scaled_sum`. For each nonzero `|xᵢ|`, either `|xᵢ| / scale` is -squared and accumulated, or a larger `|xᵢ|` becomes the new scale and the old -sum is rescaled. Every squared ratio is therefore at most one, which avoids raw -square overflow and scales all-subnormal vectors into a safe range \[15\]. - -Division, FMA, square root, and final rescaling still round in binary64. The -method is deterministic for a fixed coordinate order but does not generally -claim correct rounding or publish a certified error bound. - -Near the upper range, a fixed-size integer accumulator avoids both false and -hidden overflow from the rounded recurrence. Every coordinate square is an -integer multiple of `2^-2148` and is below `2^2048`, so 4196 bits plus -`usize::BITS` carry bits suffice for every representable vector length. The -fallback sums these integer squares exactly, retaining even the smallest -subnormal square. It compares against squared binary64 rounding midpoints to -return the nearest norm, ties to even, without a floating square root. The -overflow midpoint is `f64::MAX + 2^970`; equality rounds to infinity \[9-10\]. - -The fallback is selected from the largest coordinate magnitude, independently -of the rounded norm. With `b = bit_length(D)`, a largest magnitude at most -`2^(1023-b)` gives the conservative L1 upper bound `D × scale < 2^1023`, so -the ordinary recurrence has ample overflow margin. Larger magnitudes use the -exact boundary calculation. Its fixed storage stays on the stack and requires -no `exact` feature. - -A scalar `LaError::NonFinite` tagged with `ArithmeticOperation::VectorNorm` -therefore means the exact norm rounds to infinity. `Vector::norm_squared` -intentionally remains the direct FMA sum `Σᵢ xᵢ²` and may therefore fail even -when `norm` succeeds. - -## Outward-rounded interval expressions +## Tolerances and typed errors -`Interval` owns the invariant `-∞ < lower ≤ upper < +∞`. Its public constructor -rejects non-finite or inverted bounds, its fields are private, and every -arithmetic operation either returns another valid enclosure or a typed range -failure. Both signed-zero inputs represent exact real zero and are canonicalized -to `+0.0`; finite subnormal endpoints remain valid. +`Tolerance::try_new` accepts finite values greater than or equal to zero. +`DEFAULT_SINGULAR_TOL` is the absolute value `1e-12`. A tolerance is a rejection +policy for numerical pivots or diagnostics, not an error estimate and not a +condition-number threshold. -Point construction introduces no width. Exact-real subtraction and interval -addition use an error-free `TwoSum` residual to determine whether the rounded -result is exact or which adjacent binary64 value is required for the outward -endpoint [8]. Multiplication decomposes each nonzero binary64 operand into its exact -integer significand and power of two, compares the exact 106-bit significand -product with the rounded result, and widens only in the required direction. -This comparison also handles products that underflow to zero: a positive result -is enclosed by `[0, f64::from_bits(1)]`, and a negative result by the mirrored -interval. Squaring uses multiplication bounds but gives every interval spanning -zero the exact lower bound zero. These guarantees rely on IEEE-754 binary64 -round-to-nearest, ties-to-even, and gradual underflow \[9-11\]. IEEE 1788 -provides the broader standardized interval arithmetic model \[14\]; this crate's -deliberately smaller, undecorated surface does not claim conformance. +The error model keeps distinct mathematical conclusions separate: -If the exact result lies outside `[-f64::MAX, f64::MAX]`, no interval with finite -binary64 endpoints can contain it. The operation then returns -`LaError::IntervalRangeExhausted` with the responsible interval -`ArithmeticOperation`; it never stores infinity as an interval bound. +- `LaError::Singular` distinguishes numerical rejection from exact singularity. + Numerical context retains the factorization kind, observed pivot magnitude, + and tolerance. +- `LaError::Asymmetric` reports the mirrored values that violate LDLT's exact + symmetry precondition. +- `LaError::NotPositiveSemidefinite` records a negative pivot or a zero pivot + with remaining coupling. +- `LaError::NonFinite` distinguishes invalid input locations from arithmetic + operations that overflowed during computation. +- `LaError::Unrepresentable` distinguishes required rounding from the absence + of any finite binary64 output. -For D≤7, `IntervalMatrix::det()` evaluates the Leibniz determinant with subset -dynamic programming. A state for each column subset stores the determinant -enclosure for the corresponding leading-row minor, requiring 128 inline states -at D=7 and `D × 2^(D-1)` interval products. The expansion performs no division, -so a pivot interval containing zero cannot make the algorithm unsound. -`det_sign()` classifies a strictly positive or negative enclosure accordingly, -returns `Zero` only for the singleton `[0, 0]`, and returns `Inconclusive` for -every other overlap with zero. Inconclusive evidence is not a singularity -classification. +`Matrix::is_symmetric` and `Matrix::first_asymmetry` are tolerance-based, +scale-aware diagnostics. They do not establish the exact mirrored equality +required by `Matrix::ldlt`. -Lifting a completed `Matrix` creates point intervals for its stored values. It -does not recover rounding from earlier subtraction, dot products, or lifted-norm -construction. Robust callers instead assemble those derived coefficients with -interval operations, use `IntervalMatrix::det_sign()` as a fast proof, and -rebuild the expression in `RationalMatrix` or another exact representation when -the filter is inconclusive or loses range. +## Linear algebra algorithms -## Certified fixed-vector reductions +### Certified fixed-vector reductions `Vector::dot_with_errbound()` binds the ordinary left-to-right FMA estimate to a certified absolute error bound for the exact-real dot product of the stored @@ -153,7 +155,7 @@ without rounding coordinate differences first. For each coordinate it applies `2D`-event tree. Let `u = 2^-53` be binary64 unit roundoff and -`γₙ = nu / (1 - nu)`. When every estimate FMA result is normal or an exact +`γₙ = n·u / (1 - n·u)`. When every estimate FMA result is normal or an exact zero, standard floating-point reduction analysis gives [9-11] ```text @@ -177,59 +179,7 @@ lower/upper endpoints; overlap requires an exact fallback that reconstructs the same expression over the original inputs. The certificate is a roundoff bound for this arithmetic tree, not a numerical tolerance chosen by the caller. -## Floating-point factorizations - -### LU with partial pivoting - -LU factorization targets - -```text -P A = L U, -``` - -where `P` is a row permutation, `L` is unit lower triangular, and `U` is upper -triangular. At column `k`, the implementation selects the largest-magnitude -remaining entry in the active column. It rejects the factorization when that -magnitude is less than or equal to the caller's tolerance. - -The tolerance is an absolute finite, non-negative threshold. It is not divided -by a matrix norm, so rescaling a system can change whether a pivot is accepted. -Partial pivoting is a practical stability strategy, not an unconditional -accuracy guarantee: worst-case element growth and typical behavior are distinct -questions \[1-3, 11-12\]. `Lu::solve` does not estimate conditioning or refine -the result. - -`Lu::det` combines permutation parity with the product of the diagonal of `U`. -Scaled product accumulation avoids some premature overflow and underflow, but -the final binary64 determinant is still rounded. A returned zero or a numerical -`LaError::Singular` is therefore not proof that the represented matrix is -exactly singular. - -### LDLT without pivoting - -LDLT factorization targets - -```text -A = L D Lᵀ, -``` - -with unit lower-triangular `L` and diagonal `D` \[4-5, 11-12\]. The input -must be exactly symmetric under binary64 comparison: every mirrored pair must -satisfy `A[i][j] == A[j][i]`. Signed zeros compare equal and are accepted. - -A successful `Ldlt` requires every computed diagonal pivot to be positive and -greater than the caller's tolerance. An uncoupled computed zero or a positive -pivot at or below tolerance returns `LaError::Singular`; a negative pivot or -zero pivot with remaining coupling returns `LaError::NotPositiveSemidefinite` -with a typed violation. - -This is not a pivoted symmetric-indefinite factorization such as Bunch-Kaufman -\[6, 11-12\]. Pivots are computed in binary64, so a singular represented matrix -can produce a small positive pivot above a low tolerance. Successful -factorization is therefore not an exact positive-definiteness certificate for -the stored matrix, much less for ideal values before binary64 input conversion. - -## Determinants and certified sign filtering +### Determinants and certified sign filtering `det_direct()` evaluates closed forms for `D = 0..=4`, with the empty-product convention `det(Matrix::<0>) = 1`. `Matrix::det()` uses that path through `D = 4` @@ -272,7 +222,77 @@ returns `Ok(None)` for `D ≥ 5` or when gradual underflow could invalidate the relative-error model. A non-finite computed determinant or bound returns `LaError::NonFinite`. -## Exact arithmetic over binary64 inputs +#### Derivation of the returned determinant bound + +The implementation uses a rounded permanent `p_hat`, so its actual returned +bound is `B = fl(c_D × p_hat)`. The following argument includes that rounding. +Assume IEEE 754 round-to-nearest, ties-to-even and gradual underflow, with every +rounded operation finite and normal or an exact zero \[9-11\]. +For this proof use `ε = 2^-52`, twice the unit roundoff `u = 2^-53`. +Then each nonzero operation satisfies `fl(t) = t(1 + δ)`, `|δ| ≤ ε`. +This allowance also covers an exact `t` just below the smallest normal that +rounds up to a normal result: its relative error is at most `u / (1 - u) < ε`. +Exact zeros require no error term. Per-operation tracking conservatively accepts +only structural zeros; the grid shortcut below also proves cancellation zeros +exact. Subnormal rounded results are excluded by the filter. + +Expanding the implemented arithmetic tree expresses each determinant monomial +as its exact signed value times at most `k_D` factors of the form `(1 + δ)`. +FMA contributes one rounding factor, including when its two terms cancel. +The same count applies to the unsigned monomials of the permanent: + +| Dimension | Longest determinant path | Longest permanent path | `k_D` | +|-----------|--------------------------|------------------------|-------| +| 2 | One multiply, one FMA | One multiply, one addition | 2 | +| 3 | A 2×2 minor, then three outer operations | An absolute 2×2 sum, then three outer operations | 5 | +| 4 | A 3×3 cofactor, then four outer operations | An absolute 3×3 sum, then four outer operations | 9 | + +Sharing the six 2×2 minors in the dense D=4 path changes reuse, not the number +of rounding factors per monomial. Skipping zero coefficients in sparse paths +can only remove terms and operations. The standard product-of-errors bound, +`|Π(1 + δ_j) - 1| ≤ γ_k`, with `γ_k = kε / (1 - kε)`, therefore gives \[11\] + +```text +|d_hat - det(ι(A))| ≤ γ_k p +p_hat ≥ (1 - γ_k) p, +``` + +where `p = p(|A|)` is exact. No independence of the rounding errors is assumed. +Accounting also for rounding the final multiplication yields + +```text +B ≥ (1 - ε) c_D p_hat ≥ (1 - ε) c_D (1 - γ_k) p. +``` + +Consequently, a sufficient condition for `B ≥ |d_hat - det(ι(A))|` is + +```text +c_D ≥ γ_k / ((1 - ε)(1 - γ_k)) + = kε / ((1 - ε)(1 - 2kε)). +``` + +All three stored `c_D` values are exactly representable in binary64. For +`ε = 2^-52` and `k ≤ 9`, the denominator `(1 - ε)(1 - 2kε)` is greater than +`3/4`. The required coefficient is thus less than `(4k/3)ε`, which is at most +the respective linear term `3ε`, `8ε`, or `12ε`. The positive quadratic terms +provide additional margin. This proves the returned bound, including downward +rounding of the permanent and bound, for the stated domain. If `p = 0`, all +Leibniz terms vanish and the same inequalities give zero error and bound. + +The fast underflow check also has a simple grid argument. When every nonzero +input has magnitude at least `2^-16`, every entry is an integer multiple of +`2^-68`. The homogeneous degree-`j` arithmetic nodes preserve the grid +`2^(-68j)` under rounding. Through degree four, a nonzero result is therefore +at least `2^-272`; since each coefficient is a multiple of `2^-100`, a nonzero +final bound is at least `2^-372`. These are far above the smallest normal +`2^-1022`. Smaller inputs use per-operation underflow tracking instead. +Overflow remains a typed failure in either path. + +The independent rational Leibniz checks in `tests/proptest_exact.rs` exercise +the error inequality for D=2–4. They support regression detection; the argument +above supplies the bound for every input satisfying the arithmetic assumptions. + +### Exact arithmetic over binary64 inputs The `exact` feature decomposes each stored entry into an integer mantissa and a power of two. The entries are scaled to integer matrices without changing their @@ -301,7 +321,7 @@ finite output exists only after rounding; `NotFinite` means even the rounded result cannot be finite. The explicit rounded conversions use round-to-nearest, ties-to-even \[9-10\]. A nonzero exact value may consequently round to zero. -## Exact arithmetic over rational inputs +### Exact arithmetic over rational inputs `RationalMatrix` and `RationalVector` are a separate input domain for coefficients assembled exactly before linear algebra begins. Their constructors @@ -329,57 +349,216 @@ The rational types are const-generic and shape-safe after construction. The D=8 without unstable generic const expressions. Conversion to binary64 remains a separate strict or explicitly rounded `ExactF64Conversion` operation. -## Tolerances and typed errors +### Exact-to-binary64 conversion -`Tolerance::try_new` accepts finite values greater than or equal to zero. -`DEFAULT_SINGULAR_TOL` is the absolute value `1e-12`. A tolerance is a rejection -policy for numerical pivots or diagnostics, not an error estimate and not a -condition-number threshold. +For strict conversion, a canonical nonzero rational must have a power-of-two +denominator. Removing powers of two from its numerator produces +`x = sign × m × 2^e`, with `m` positive and odd. Let `b` be the bit length of +`m`. Exact finite binary64 representation is possible precisely when \[9-10\] -The error model keeps distinct mathematical conclusions separate: +```text +b ≤ 53, e ≥ -1074, e + b - 1 ≤ 1023. +``` -- `LaError::Singular` distinguishes numerical rejection from exact singularity. - Numerical context retains the factorization kind, observed pivot magnitude, - and tolerance. -- `LaError::Asymmetric` reports the mirrored values that violate LDLT's exact - symmetry precondition. -- `LaError::NotPositiveSemidefinite` records a negative pivot or a zero pivot - with remaining coupling. -- `LaError::NonFinite` distinguishes invalid input locations from arithmetic - operations that overflowed during computation. -- `LaError::Unrepresentable` distinguishes required rounding from the absence - of any finite binary64 output. +These conditions bound significand precision, the least representable bit, +and the highest exponent. Subnormal output stores `m × 2^(e + 1074)` in the +fraction field; normal output aligns the significand to 53 bits and stores the +biased exponent. Zero is handled separately. Raw `BigRational` inputs with +common factors or negative denominators are normalized as needed before this +decision; a zero denominator is rejected. Canonical storage allows +`RationalVector` conversion to skip that repeated normalization. + +The integer-and-exponent determinant path rounds directly from `BigInt` digits. +It retains 53 significand bits for normal results, or the bits on the `2^-1074` +grid for subnormals. A discarded guard bit triggers an increment only if some +lower discarded bit is set (the sticky condition) or the retained integer is +odd. This implements nearest-even ties without constructing a rational +denominator. A carry can promote a subnormal to normal, advance the exponent, +or overflow. At `f64::MAX + 2^970`, the tie rounds to infinity; at magnitude +`2^-1075`, the tie rounds to signed zero \[9-10\]. + +For an already-computed `BigRational` value, rounded conversion delegates to +`num-rational`'s `ToPrimitive::to_f64` and checks for finite output. The strict +path uses that rounded result only when needed to distinguish +`RequiresRounding` from `NotFinite`; it never returns a rounded value as an exact +conversion. Neither conversion reruns determinant or solve elimination. + +### Gram matrices and geometric measures + +For `M` vectors in `N` coordinates, let `V` contain the vectors as rows. Their +Gram matrix is `G = V Vᵀ`, with `G[i,j] = v_i · v_j`. In exact real arithmetic +`G` is positive semidefinite because `zᵀGz = ||Vᵀz||² ≥ 0`; it is positive +definite exactly when the vectors are linearly independent. + +For `M ≤ N`, `det(G)` is the squared M-dimensional volume of the parallelotope +spanned by those vectors. When the vectors are edges from a common simplex +vertex, the simplex volume is `sqrt(det(G)) / M!` \[16\]. These identities +apply to lower-dimensional simplices embedded in a larger coordinate space. + +`gram_matrix` evaluates each upper-triangle dot product once using +`Vector::dot`'s left-to-right FMA reduction and copies it to the other triangle. +The returned binary64 matrix is therefore exactly symmetric, but rounding can +destroy positive semidefiniteness or rank. Construction does not certify linear +independence or volume accuracy and provides no absolute rounding-error bound. +A dot-product overflow remains a typed `LaError::NonFinite` failure. + +For nonempty `V` with full row rank, `κ₂(G) = κ₂(V)²`: forming a Gram matrix +squares the spectral condition number \[11-12\]. Nearly dependent vectors require care +when interpreting a computed determinant or passing the matrix to `ldlt`. +Its exact-symmetry and computed-pivot checks remain in force. The +[Gram references](../REFERENCES.md#gram-matrices-and-geometric-measures) +provide the volume identity and numerical background. + + + -`Matrix::is_symmetric` and `Matrix::first_asymmetry` are tolerance-based, -scale-aware diagnostics. They do not establish the exact mirrored equality -required by `Matrix::ldlt`. +### LDLT without pivoting -## Choosing an API +LDLT factorization targets -| Need | API | Important boundary | -|------|-----|--------------------| -| General floating solve | `lu(tol)` then `solve` | Approximate; absolute pivot policy | -| Positive-definite floating solve | `ldlt(tol)` then `solve` | Exact symmetry; computed pivots must exceed tolerance; success is not a certificate | -| Floating determinant, any `D` | `det` | No certified bound; zero is not exact singularity | -| `D ≤ 4` error-bounded determinant/sign test | `det_direct_with_errbound` | Sign is certified when estimate magnitude exceeds bound; otherwise inconclusive | -| Derived-expression determinant sign through `D ≤ 7` | `IntervalMatrix::det_sign` | Outward-rounded proof; overlap with zero is explicitly inconclusive | -| Exact determinant sign | `det_sign_exact` | Exact for stored binary64 entries | -| Exact determinant value or solve | `det_exact`, `solve_exact` | Exact for represented inputs | -| Exact operations over preassembled rationals | `RationalMatrix::det_sign`, `det`, `solve` | No intermediate binary64 reconstruction | -| Binary64 output from an exact result | Strict or rounded conversions | Strict conversion forbids rounding | +```text +A = L D Lᵀ, +``` -## Geometry relationship and scope +with unit lower-triangular `L` and diagonal `D` \[4-5, 11-12\]. The input +must be exactly symmetric under binary64 comparison: every mirrored pair must +satisfy `A[i][j] == A[j][i]`. Signed zeros compare equal and are accepted. -Orientation, in-sphere, and related geometric predicates can be reduced to -determinant signs, which is why an adaptive exact sign is useful near degeneracy -\[8\]. `la-stack` supplies both point-matrix and bounded-expression determinant -primitives; callers still own problem-specific matrix assembly and semantic -classification. The crate originated to support -[`delaunay`](https://crates.io/crates/delaunay), but its matrix, factorization, -and exact-arithmetic APIs are general numerical infrastructure. +A successful `Ldlt` requires every computed diagonal pivot to be positive and +greater than the caller's tolerance. An uncoupled computed zero or a positive +pivot at or below tolerance returns `LaError::Singular`; a negative pivot or +zero pivot with remaining coupling returns `LaError::NotPositiveSemidefinite` +with a typed violation. -The deliberate anti-goals are dynamically sized or rectangular matrices, -sparse storage, broad decomposition coverage, alternate floating scalar -families, and GPU-, parallel-, BLAS-, or LAPACK-scale throughput. Those problems -need different storage models and numerical policies rather than extensions to -this small fixed-dimension design. +This is not a pivoted symmetric-indefinite factorization such as Bunch-Kaufman +\[6, 11-12\]. Pivots are computed in binary64, so a singular represented matrix +can produce a small positive pivot above a low tolerance. Successful +factorization is therefore not an exact positive-definiteness certificate for +the stored matrix, much less for ideal values before binary64 input conversion. + +### LU with partial pivoting + +LU factorization targets + +```text +P A = L U, +``` + +where `P` is a row permutation, `L` is unit lower triangular, and `U` is upper +triangular. At column `k`, the implementation selects the largest-magnitude +remaining entry in the active column. It rejects the factorization when that +magnitude is less than or equal to the caller's tolerance. + +The tolerance is an absolute finite, non-negative threshold. It is not divided +by a matrix norm, so rescaling a system can change whether a pivot is accepted. +Partial pivoting is a practical stability strategy, not an unconditional +accuracy guarantee: worst-case element growth and typical behavior are distinct +questions \[1-3, 11-12\]. `Lu::solve` does not estimate conditioning or refine +the result. + +`Lu::det` combines permutation parity with the product of the diagonal of `U`. +Scaled product accumulation avoids some premature overflow and underflow, but +the final binary64 determinant is still rounded. A returned zero or a numerical +`LaError::Singular` is therefore not proof that the represented matrix is +exactly singular. + +### Outward-rounded interval expressions + +`Interval` owns the invariant `-∞ < lower ≤ upper < +∞`. Its public constructor +rejects non-finite or inverted bounds, its fields are private, and every +arithmetic operation either returns another valid enclosure or a typed range +failure. Both signed-zero inputs represent exact real zero and are canonicalized +to `+0.0`; finite subnormal endpoints remain valid. + +Point construction introduces no width. Exact-real subtraction and interval +addition use an error-free `TwoSum` residual to determine whether the rounded +result is exact or which adjacent binary64 value is required for the outward +endpoint [8]. Multiplication decomposes each nonzero binary64 operand into its exact +integer significand and power of two, compares the exact 106-bit significand +product with the rounded result, and widens only in the required direction. +This comparison also handles products that underflow to zero: a positive result +is enclosed by `[0, f64::from_bits(1)]`, and a negative result by the mirrored +interval. Squaring uses multiplication bounds but gives every interval spanning +zero the exact lower bound zero. These guarantees rely on IEEE-754 binary64 +round-to-nearest, ties-to-even, and gradual underflow \[9-11\]. IEEE 1788 +provides the broader standardized interval arithmetic model \[14\]; this crate's +deliberately smaller, undecorated surface does not claim conformance. + +If the exact result lies outside `[-f64::MAX, f64::MAX]`, no interval with finite +binary64 endpoints can contain it. The operation then returns +`LaError::IntervalRangeExhausted` with the responsible interval +`ArithmeticOperation`; it never stores infinity as an interval bound. + +For D≤7, `IntervalMatrix::det()` evaluates the Leibniz determinant with subset +dynamic programming. A state for each column subset stores the determinant +enclosure for the corresponding leading-row minor, requiring 128 inline states +at D=7 and `D × 2^(D-1)` interval products. The expansion performs no division, +so a pivot interval containing zero cannot make the algorithm unsound. +`det_sign()` classifies a strictly positive or negative enclosure accordingly, +returns `Zero` only for the singleton `[0, 0]`, and returns `Inconclusive` for +every other overlap with zero. Inconclusive evidence is not a singularity +classification. + +Lifting a completed `Matrix` creates point intervals for its stored values. It +does not recover rounding from earlier subtraction, dot products, or lifted-norm +construction. Robust callers instead assemble those derived coefficients with +interval operations, use `IntervalMatrix::det_sign()` as a fast proof, and +rebuild the expression in `RationalMatrix` or another exact representation when +the filter is inconclusive or loses range. + +### Scaled determinant products + +Both factorizations compute their determinant from the stored diagonal factors, +with the row-permutation sign included for LU. Direct multiplication is retained +while every running product is finite and normal. Otherwise `ScaledProduct` +replays all factors as a sign, a normalized mantissa, and an integer exponent. +IEEE 754 bit decomposition gives each nonzero factor as `m × 2^e`, including +subnormals, with `1 ≤ m < 2` \[9-10\]. Multiplication of two such mantissas +rounds below 4, so at most one exact division by two restores the invariant. +The separate exponent records the corresponding scale without range loss. + +The most recent factor stays pending until finalization. If the final exponent +is in the subnormal range, the two remaining operands are scaled by exact +powers of two before multiplication. This places their product directly on the +destination's subnormal grid, avoiding a normal-mantissa rounding followed by a +second rounding to that coarser grid. The final result can be signed zero; +overflow returns `LaError::NonFinite`. + +This is range management for a product of already-rounded factor diagonals. +Earlier mantissa multiplications still round, and factorization error remains. +Scaling guarantees neither an exact product nor correct rounding of the exact +determinant. No certified absolute error bound is provided. + +### Scaled Euclidean vector norm + +`Vector::norm` computes `sqrt(Σᵢ xᵢ²)` with a left-to-right scaled +sum-of-squares recurrence. Its state represents the accumulated squared norm as +`scale² × scaled_sum`. For each nonzero `|xᵢ|`, either `|xᵢ| / scale` is +squared and accumulated, or a larger `|xᵢ|` becomes the new scale and the old +sum is rescaled. Every squared ratio is therefore at most one, which avoids raw +square overflow and scales all-subnormal vectors into a safe range \[15\]. + +Division, FMA, square root, and final rescaling still round in binary64. The +method is deterministic for a fixed coordinate order but does not generally +claim correct rounding or publish a certified error bound. + +Near the upper range, a fixed-size integer accumulator avoids both false and +hidden overflow from the rounded recurrence. Every coordinate square is an +integer multiple of `2^-2148` and is below `2^2048`, so 4196 bits plus +`usize::BITS` carry bits suffice for every representable vector length. The +fallback sums these integer squares exactly, retaining even the smallest +subnormal square. It compares against squared binary64 rounding midpoints to +return the nearest norm, ties to even, without a floating square root. The +overflow midpoint is `f64::MAX + 2^970`; equality rounds to infinity \[9-10\]. + +The fallback is selected from the largest coordinate magnitude, independently +of the rounded norm. With `b = bit_length(D)`, a largest magnitude at most +`2^(1023-b)` gives the conservative L1 upper bound `D × scale < 2^1023`, so +the ordinary recurrence has ample overflow margin. Larger magnitudes use the +exact boundary calculation. Its fixed storage stays on the stack and requires +no `exact` feature. + +A scalar `LaError::NonFinite` tagged with `ArithmeticOperation::VectorNorm` +therefore means the exact norm rounds to infinity. `Vector::norm_squared` +intentionally remains the direct FMA sum `Σᵢ xᵢ²` and may therefore fail even +when `norm` succeeds. diff --git a/docs/PERFORMANCE.md b/docs/performance.md similarity index 99% rename from docs/PERFORMANCE.md rename to docs/performance.md index 3a6c756..5a55d65 100644 --- a/docs/PERFORMANCE.md +++ b/docs/performance.md @@ -260,7 +260,7 @@ Local performance reports are generated in isolated temporary worktrees: # Local development: compare the current tree with the latest release just performance-local -# Release PR: update docs/PERFORMANCE.md and archive the previous report +# Release PR: update docs/performance.md and archive the previous report just performance-release # Build release docs from retained CSV/JSON inputs (no benchmarks) diff --git a/justfile b/justfile index affc2dc..73cae4a 100644 --- a/justfile +++ b/justfile @@ -26,7 +26,7 @@ clippy_sarif_version := "0.8.0" dprint_version := "0.57.4" git_cliff_version := "2.14.1" just_version := "1.58.0" -rumdl_version := "0.2.67" +rumdl_version := "0.2.68" sarif_fmt_version := "0.8.0" taplo_version := "0.10.0" typos_version := "1.50.1" @@ -1202,4 +1202,4 @@ yaml-lint: _ensure-yamllint # GitHub Actions security analysis zizmor: _ensure-zizmor - zizmor .github + @bash scripts/run_zizmor.sh diff --git a/pyproject.toml b/pyproject.toml index fee3c26..9eb400a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,7 +157,7 @@ dev = [ "ruff==0.16.6", "semgrep==1.176.1", "shellcheck-py==0.11.0.1", - "shfmt-py==4.1.0", + "shfmt-py==4.2.0", "ty==0.0.78", "yamllint==1.38.0", ] diff --git a/scripts/README.md b/scripts/README.md index 0b0b81e..b08cf1d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -64,7 +64,7 @@ recipes require `gh` only when discovering published release tags. # Local development: compare the current tree with the latest release just performance-local -# Release PR: update docs/PERFORMANCE.md and archive the previous report +# Release PR: update docs/performance.md and archive the previous report just performance-release # Build release docs from retained CSV/JSON inputs @@ -92,7 +92,7 @@ coverage. `performance-local` writes Markdown plus schema-versioned does the same measurement and retention work, requires distinct releases, and promotes the validated result. `performance-doc` consumes the retained pair from either workflow without Cargo or temporary worktrees, then promotes the -result into `docs/PERFORMANCE.md` and the archive. Same-version local artifacts +result into `docs/performance.md` and the archive. Same-version local artifacts remain valid comparison evidence but cannot be promoted as a release report. These files are reproducible scratch and may be removed with `target/`; native Criterion release archives remain the durable raw baselines. Direct comparisons diff --git a/scripts/archive_performance.py b/scripts/archive_performance.py index 7bdd8ff..253097f 100644 --- a/scripts/archive_performance.py +++ b/scripts/archive_performance.py @@ -7,11 +7,11 @@ current machine and branch. - ``target/bench-reports/performance.csv`` and the adjacent provenance JSON are validated, reproducible performance-comparison inputs. - - ``docs/PERFORMANCE.md`` is the latest curated release-to-release comparison. + - ``docs/performance.md`` is the latest curated release-to-release comparison. - ``docs/archive/performance/*.md`` stores older curated comparisons. This script renders from a validated artifact reload, copies the result into -``docs/PERFORMANCE.md``, and archives the previous committed report under a +``docs/performance.md``, and archives the previous committed report under a filename derived from the report metadata, such as ``v0.4.2-vs-v0.4.1.md``. """ @@ -66,7 +66,7 @@ _DEFAULT_SOURCE = "target/bench-reports/performance.md" _DEFAULT_ARTIFACT_CSV = "target/bench-reports/performance.csv" _DEFAULT_ARTIFACT_PROVENANCE = "target/bench-reports/performance.provenance.json" -_DEFAULT_CURRENT = "docs/PERFORMANCE.md" +_DEFAULT_CURRENT = "docs/performance.md" _DEFAULT_ARCHIVE_DIR = "docs/archive/performance" _DEFAULT_SUITE = "all" _DEFAULT_SCOPE = "release-signal" @@ -517,7 +517,7 @@ def _archive_readme(archive_dir: Path) -> str: "# Archived Performance Reports", "", "Older release-to-release benchmark comparisons are archived here.", - "`docs/PERFORMANCE.md` contains the latest curated comparison.", + "`docs/performance.md` contains the latest curated comparison.", "", ] if reports: @@ -1937,7 +1937,7 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument( "--output-only", action="store_true", - help="Write the generated report to --output without promoting docs/PERFORMANCE.md.", + help="Write the generated report to --output without promoting docs/performance.md.", ) parser.add_argument( "--local-report", diff --git a/scripts/bench_compare.py b/scripts/bench_compare.py index 94441f3..9183b2e 100644 --- a/scripts/bench_compare.py +++ b/scripts/bench_compare.py @@ -214,7 +214,7 @@ # Local development: compare the current tree with the latest release just performance-local -# Release PR: update docs/PERFORMANCE.md and archive the previous report +# Release PR: update docs/performance.md and archive the previous report just performance-release # Build release docs from retained CSV/JSON inputs (no benchmarks) diff --git a/scripts/run_zizmor.sh b/scripts/run_zizmor.sh new file mode 100644 index 0000000..cbf73c8 --- /dev/null +++ b/scripts/run_zizmor.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Keep authentication out of command arguments and shell traces. +set +x +set -euo pipefail + +if [[ "${ZIZMOR_OFFLINE:-false}" == true || "${ZIZMOR_NO_ONLINE_AUDITS:-false}" == true ]]; then + echo "zizmor: offline audits requested; online audits are disabled." + exec zizmor --offline --persona regular .github +fi + +zizmor_token="${ZIZMOR_GITHUB_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}" +if [[ -z "$zizmor_token" ]] && command -v gh >/dev/null; then + if resolved_token="$(gh auth token 2>/dev/null)"; then + zizmor_token="$resolved_token" + fi +fi + +if [[ -n "$zizmor_token" ]]; then + echo "zizmor: running authenticated online audits (persona: regular)." + # Normalize GH_TOKEN too: zizmor checks it before ZIZMOR_GITHUB_TOKEN. + export GH_TOKEN="$zizmor_token" ZIZMOR_GITHUB_TOKEN="$zizmor_token" + exec zizmor --persona regular .github +fi + +echo "zizmor: no GitHub token available; using offline audits. Online findings are not checked." +exec zizmor --offline --persona regular .github diff --git a/scripts/tests/test_archive_performance.py b/scripts/tests/test_archive_performance.py index b246248..ce74c9e 100644 --- a/scripts/tests/test_archive_performance.py +++ b/scripts/tests/test_archive_performance.py @@ -562,7 +562,7 @@ def fail_request(_options: object) -> Never: message = "publication and rollback failed" raise ExceptionGroup( message, - [OSError("could not publish docs/PERFORMANCE.md"), OSError("could not restore prior report")], + [OSError("could not publish docs/performance.md"), OSError("could not restore prior report")], ) monkeypatch.setattr(archive_performance, "resolve_archive_request", fail_request) @@ -570,7 +570,7 @@ def fail_request(_options: object) -> Never: assert main(["v0.4.3", "v0.4.2"]) == 1 captured = capsys.readouterr() assert "publication and rollback failed (2 sub-exceptions)" in captured.err - assert "could not publish docs/PERFORMANCE.md" in captured.err + assert "could not publish docs/performance.md" in captured.err assert "could not restore prior report" in captured.err @@ -957,7 +957,7 @@ def test_fallback_current_cargo_command_matches_suite( def test_promote_report_archives_previous_and_updates_sorted_index(tmp_path: Path) -> None: source = tmp_path / "target" / "bench-reports" / "performance.md" - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" source.parent.mkdir(parents=True) @@ -981,7 +981,7 @@ def test_promote_report_archives_previous_and_updates_sorted_index(tmp_path: Pat assert (archive_dir / "README.md").read_text(encoding="utf-8") == ( "# Archived Performance Reports\n\n" "Older release-to-release benchmark comparisons are archived here.\n" - "`docs/PERFORMANCE.md` contains the latest curated comparison.\n\n" + "`docs/performance.md` contains the latest curated comparison.\n\n" "- [v0.3.1-vs-v0.3.0](v0.3.1-vs-v0.3.0.md)\n" "- [v0.4.1-vs-v0.4.0](v0.4.1-vs-v0.4.0.md)\n" ) @@ -989,7 +989,7 @@ def test_promote_report_archives_previous_and_updates_sorted_index(tmp_path: Pat def test_promote_report_is_idempotent_for_same_release_pair(tmp_path: Path) -> None: source = tmp_path / "performance-new.md" - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" source.write_text(_report("0.4.2", "v0.4.1"), encoding="utf-8") @@ -1010,7 +1010,7 @@ def test_promote_report_is_idempotent_for_same_release_pair(tmp_path: Path) -> N def test_promote_report_does_not_overwrite_existing_archive(tmp_path: Path) -> None: source = tmp_path / "performance-new.md" - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" archived = archive_dir / "v0.4.1-vs-v0.4.0.md" @@ -1034,7 +1034,7 @@ def test_promote_report_does_not_overwrite_existing_archive(tmp_path: Path) -> N def test_promote_report_rejects_mismatched_existing_archive_without_mutation(tmp_path: Path) -> None: source = tmp_path / "performance-new.md" - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" archived = archive_dir / "v0.4.1-vs-v0.4.0.md" index = archive_dir / "README.md" @@ -1064,7 +1064,7 @@ def test_promote_report_rejects_mismatched_existing_archive_without_mutation(tmp @pytest.mark.parametrize("collision", ["directory", "symlink"]) def test_promote_report_rejects_non_file_archive_collision_without_mutation(tmp_path: Path, collision: str) -> None: source = tmp_path / "performance-new.md" - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" archived = archive_dir / "v0.4.1-vs-v0.4.0.md" original = _report("0.4.1", "v0.4.0") @@ -1095,7 +1095,7 @@ def test_promote_report_rejects_non_file_archive_collision_without_mutation(tmp_ def test_promote_report_rejects_unexpected_release_pair(tmp_path: Path) -> None: source = tmp_path / "performance-new.md" - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" source.write_text(_report("0.4.2", "v0.4.1"), encoding="utf-8") @@ -1111,7 +1111,7 @@ def test_promote_report_rejects_unexpected_release_pair(tmp_path: Path) -> None: def test_promote_report_rewrites_legacy_update_instructions(tmp_path: Path) -> None: source = tmp_path / "performance-new.md" - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" source.write_text(_legacy_report("0.4.3", "v0.4.2"), encoding="utf-8") current.parent.mkdir(parents=True) @@ -1143,7 +1143,7 @@ def test_main_promotes_generated_report_to_docs_performance( monkeypatch: pytest.MonkeyPatch, ) -> None: source = tmp_path / "target" / "bench-reports" / "performance.md" - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" generated = _report("0.4.3", "v0.4.2") @@ -1185,7 +1185,7 @@ def test_main_reports_release_pair_mismatch_to_stderr( monkeypatch: pytest.MonkeyPatch, ) -> None: source = tmp_path / "target" / "bench-reports" / "performance.md" - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" source.parent.mkdir(parents=True) source.write_text(_report("0.4.3", "v0.4.2"), encoding="utf-8") @@ -1383,7 +1383,7 @@ def test_main_promote_artifacts_rejects_output_alias_without_mutation( artifacts.csv.parent.mkdir(parents=True) artifacts.csv.write_bytes(b"original csv\n") artifacts.provenance.write_bytes(b"original provenance\n") - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" current.parent.mkdir(parents=True) current.write_text(_report("0.4.3", "v0.4.2"), encoding="utf-8") archive_dir = tmp_path / "docs" / "archive" / "performance" @@ -1439,7 +1439,7 @@ def fail_unexpected(*, args: object, paths: object, request: object, repo_root: def test_main_generates_report_in_temp_worktree(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" calls: list[RunnerCall] = [] @@ -1523,7 +1523,7 @@ def fake_run_safe(command: str, args: Sequence[str], cwd: Path | None = None, ** def test_temp_worktree_is_removed_when_benchmark_command_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: (tmp_path / "Cargo.toml").write_text('[package]\nversion = "0.4.3"\n', encoding="utf-8") - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" calls: list[RunnerCall] = [] @@ -1584,7 +1584,7 @@ def fake_run_safe(command: str, args: Sequence[str], cwd: Path | None = None, ** def test_generate_report_rejects_unsafe_baseline_archive(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" calls: list[RunnerCall] = [] @@ -1646,7 +1646,7 @@ def test_generate_report_generates_release_baseline_locally( # noqa: PLR0915 monkeypatch.delenv("RUSTUP_TOOLCHAIN", raising=False) (tmp_path / "Cargo.toml").write_text('[package]\nversion = "0.4.3"\n', encoding="utf-8") (tmp_path / "rust-toolchain.toml").write_text('[toolchain]\nchannel = "1.97.0"\n', encoding="utf-8") - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" calls: list[RunnerCall] = [] @@ -1818,7 +1818,7 @@ def fake_run_safe(command: str, args: Sequence[str], cwd: Path | None = None, ** assert not any(kind == "just" and args == ("bench-exact",) for kind, args, _ in calls) assert not any(kind == "just" and args == ("bench-latest",) for kind, args, _ in calls) - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" with pytest.raises(ValueError, match="cannot promote a same-version local performance comparison"): archive_performance.render_and_promote_artifacts( artifacts=ArtifactPaths( @@ -1881,7 +1881,7 @@ def fake_run_safe(command: str, args: Sequence[str], cwd: Path | None = None, ** assert not any(kind == "cargo" for kind, _, _ in calls) assert not any(kind == "just" and args == ("bench-exact",) for kind, args, _ in calls) - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" promoted = archive_performance.render_and_promote_artifacts( artifacts=ArtifactPaths( csv=output.with_suffix(".csv"), @@ -1896,7 +1896,7 @@ def fake_run_safe(command: str, args: Sequence[str], cwd: Path | None = None, ** def test_main_generates_latest_published_report_from_github_releases(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" calls: list[RunnerCall] = [] @@ -1968,7 +1968,7 @@ def fake_run_safe(command: str, args: Sequence[str], cwd: Path | None = None, ** def test_main_normalizes_explicit_bare_tags_before_fetching_and_checkout(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: output = tmp_path / "target" / "bench-reports" / "github-assets-performance.md" - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" calls: list[RunnerCall] = [] @@ -2040,7 +2040,7 @@ def fake_run_safe(command: str, args: Sequence[str], cwd: Path | None = None, ** def test_main_published_latest_fetch_failure_stops_before_worktree(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" calls: list[RunnerCall] = [] @@ -2098,7 +2098,7 @@ def fake_run_safe(command: str, args: Sequence[str], cwd: Path | None = None, ** def test_failed_atomic_replace_preserves_existing_report(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: source = tmp_path / "performance-new.md" - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" original = _report("0.4.2", "v0.4.1") @@ -2122,14 +2122,14 @@ def fail_replace(src: Path, dst: Path) -> None: ) assert current.read_text(encoding="utf-8") == original - assert not list(current.parent.glob(".PERFORMANCE.md.*.tmp")) + assert not list(current.parent.glob(".performance.md.*.tmp")) def test_restore_file_removes_temp_when_fsync_fails( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: - target = tmp_path / "docs" / "PERFORMANCE.md" + target = tmp_path / "docs" / "performance.md" target.parent.mkdir() target.write_text("current\n", encoding="utf-8") @@ -2143,7 +2143,7 @@ def fail_fsync(_descriptor: int) -> None: archive_performance._restore_file(target, b"restored\n") assert target.read_text(encoding="utf-8") == "current\n" - assert not list(target.parent.glob(".PERFORMANCE.md.*.restore")) + assert not list(target.parent.glob(".performance.md.*.restore")) def test_failed_archive_index_update_rolls_back_report_and_new_archive( @@ -2151,7 +2151,7 @@ def test_failed_archive_index_update_rolls_back_report_and_new_archive( monkeypatch: pytest.MonkeyPatch, ) -> None: source = tmp_path / "performance-new.md" - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" original = _normalized_report("0.4.2", "v0.4.1") source.write_text(_report("0.4.3", "v0.4.2"), encoding="utf-8") @@ -2182,7 +2182,7 @@ def test_failed_artifact_promotion_output_write_rolls_back_report_archive_and_in tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" index = archive_dir / "README.md" output = tmp_path / "target" / "bench-reports" / "performance.md" @@ -2223,7 +2223,7 @@ def fail_output_write(path: Path, text: str) -> None: def test_generate_and_promote_rejects_artifact_alias_before_worktree(tmp_path: Path) -> None: - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" current.parent.mkdir(parents=True) current.write_text(_report("0.4.2", "v0.4.1"), encoding="utf-8") @@ -2279,7 +2279,7 @@ def test_generate_and_promote_uses_temp_worktree_and_current_diff( ) -> None: monkeypatch.delenv("RUSTUP_TOOLCHAIN", raising=False) (tmp_path / "rust-toolchain.toml").write_text('[toolchain]\nchannel = "1.97.0"\n', encoding="utf-8") - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" output = tmp_path / "target" / "bench-reports" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" current.parent.mkdir(parents=True) @@ -2345,7 +2345,7 @@ def fake_run_safe(command: str, args: Sequence[str], cwd: Path | None = None, ** def test_generate_and_promote_legacy_published_tag_uses_legacy_commands(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - current = tmp_path / "docs" / "PERFORMANCE.md" + current = tmp_path / "docs" / "performance.md" output = tmp_path / "target" / "bench-reports" / "performance.md" archive_dir = tmp_path / "docs" / "archive" / "performance" calls: list[RunnerCall] = [] diff --git a/scripts/tests/test_bench_compare.py b/scripts/tests/test_bench_compare.py index 7e369da..8324329 100644 --- a/scripts/tests/test_bench_compare.py +++ b/scripts/tests/test_bench_compare.py @@ -1083,7 +1083,7 @@ def test_read_harness_provenance_rejects_malformed_fields( def test_main_snapshot_writes_output(tmp_path: Path) -> None: criterion_dir = tmp_path / "criterion" _build_criterion_tree(criterion_dir) - output = tmp_path / "PERFORMANCE.md" + output = tmp_path / "performance.md" rc = bench_compare.main( [ diff --git a/scripts/tests/test_run_zizmor.py b/scripts/tests/test_run_zizmor.py new file mode 100644 index 0000000..8768500 --- /dev/null +++ b/scripts/tests/test_run_zizmor.py @@ -0,0 +1,134 @@ +"""Exercise authentication selection without network access or real credentials.""" + +import os +import shutil +import stat +import subprocess +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[2] +TOKEN_ENV = ("ZIZMOR_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN") + + +def _shim(path: Path, body: str) -> None: + path.write_text("#!/bin/bash\nset -eu\n" + body, encoding="utf-8", newline="\n") + path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + +def _run( + tmp_path: Path, + env_overrides: dict[str, str], + *, + expected: str = "", + gh_status: int | None = 0, + scanner_status: int = 0, +) -> subprocess.CompletedProcess[str]: + bash = shutil.which("bash") + assert bash is not None, "Bash is required by the repository's Just recipes" + _shim( + tmp_path / "zizmor", + '[[ "${GH_TOKEN:-}" == "$EXPECTED_TOKEN" ]] || exit 91\n' + '[[ "${ZIZMOR_GITHUB_TOKEN:-}" == "$EXPECTED_TOKEN" ]] || exit 92\n' + 'printf "scanner args: %s\\n" "$*"\n' + 'exit "$SCANNER_STATUS"\n', + ) + if gh_status is not None: + _shim( + tmp_path / "gh", + '[[ "$*" == "auth token" ]] || exit 93\n' + 'echo "called" >> "$GH_CALL_LOG"\n' + 'echo "test-gh-secret"\n' + 'echo "test-gh-diagnostic-secret" >&2\n' + 'exit "$GH_STATUS"\n', + ) + env = { + key: value + for key, value in os.environ.items() + if key not in (*TOKEN_ENV, "ZIZMOR_OFFLINE", "ZIZMOR_NO_ONLINE_AUDITS", "BASH_ENV", "SHELLOPTS", "BASHOPTS") + } + env.update( + EXPECTED_TOKEN=expected, + GH_STATUS=str(gh_status), + GH_CALL_LOG=(tmp_path / "gh-calls").as_posix(), + SCANNER_STATUS=str(scanner_status), + ) + env.update(env_overrides) + # Bash's PWD supplies a POSIX path on Windows too, without drive-letter colons + # being interpreted as PATH separators or reintroducing the real gh binary. + result = subprocess.run( # noqa: S603 - resolved Bash, fixed script, test-owned environment. + [ + bash, + "--noprofile", + "--norc", + "-c", + 'cd "$1"; export PATH="$PWD"; cd "$2"; source scripts/run_zizmor.sh', + "test-zizmor", + tmp_path.as_posix(), + REPO_ROOT.as_posix(), + ], + cwd=REPO_ROOT, + env=env, + capture_output=True, + encoding="utf-8", + check=False, + timeout=10, + ) + for secret in (*(env_overrides.get(key, "") for key in TOKEN_ENV), "test-gh-secret", "test-gh-diagnostic-secret"): + if secret: + assert secret not in result.stdout + result.stderr + return result + + +@pytest.mark.parametrize( + ("tokens", "expected"), + [ + ({"ZIZMOR_GITHUB_TOKEN": "test-zizmor-secret", "GH_TOKEN": "test-gh-env-secret", "GITHUB_TOKEN": "test-github-secret"}, "test-zizmor-secret"), + ({"ZIZMOR_GITHUB_TOKEN": "", "GH_TOKEN": "test-gh-env-secret", "GITHUB_TOKEN": "test-github-secret"}, "test-gh-env-secret"), + ({"GITHUB_TOKEN": "test-github-secret"}, "test-github-secret"), + ], +) +def test_environment_precedence_and_no_token_logging(tmp_path: Path, tokens: dict[str, str], expected: str) -> None: + result = _run(tmp_path, tokens, expected=expected) + + assert result.returncode == 0, result.stderr + assert "authenticated online audits" in result.stdout + assert "scanner args: --persona regular .github" in result.stdout + assert not (tmp_path / "gh-calls").exists() + + +def test_authenticated_gh_fallback(tmp_path: Path) -> None: + result = _run(tmp_path, {}, expected="test-gh-secret") + + assert result.returncode == 0, result.stderr + assert "authenticated online audits" in result.stdout + assert "scanner args: --persona regular .github" in result.stdout + assert (tmp_path / "gh-calls").read_text(encoding="utf-8") == "called\n" + + +@pytest.mark.parametrize("gh_status", [1, None]) +def test_offline_fallback_without_authentication(tmp_path: Path, gh_status: int | None) -> None: + result = _run(tmp_path, {}, gh_status=gh_status) + + assert result.returncode == 0, result.stderr + assert "no GitHub token available; using offline audits" in result.stdout + assert "scanner args: --offline --persona regular .github" in result.stdout + + +@pytest.mark.parametrize("offline_env", ["ZIZMOR_OFFLINE", "ZIZMOR_NO_ONLINE_AUDITS"]) +def test_explicit_offline_skips_credential_lookup(tmp_path: Path, offline_env: str) -> None: + result = _run(tmp_path, {offline_env: "true"}) + + assert result.returncode == 0, result.stderr + assert "offline audits requested" in result.stdout + assert "scanner args: --offline --persona regular .github" in result.stdout + assert not (tmp_path / "gh-calls").exists() + + +@pytest.mark.parametrize("gh_status", [0, 1]) +def test_scanner_failure_propagates_without_offline_retry(tmp_path: Path, gh_status: int) -> None: + result = _run(tmp_path, {}, expected="test-gh-secret" if gh_status == 0 else "", gh_status=gh_status, scanner_status=14) + + assert result.returncode == 14 + assert result.stdout.count("scanner args:") == 1 diff --git a/semgrep.yaml b/semgrep.yaml index e0fd19e..a98ed50 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -431,6 +431,141 @@ rules: patterns: - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?@[a-fA-F0-9]{40}\s*$' + - id: la-stack.github-actions.zizmor-tool-version-pinned + languages: + - yaml + severity: ERROR + message: "Set zizmor-action's version from steps.zizmor_version.outputs.version, resolved with just --evaluate zizmor_version." + metadata: + category: security + rationale: >- + Local and SARIF scans must use the same scanner version. Zizmor's online + audits own action SHA/tag resolution; this rule does not map remote tags. + paths: + include: + - "/.github/workflows/**/*.yml" + - "/.github/workflows/**/*.yaml" + - "/tests/semgrep/.github/workflows/**/*.yml" + - "/tests/semgrep/.github/workflows/**/*.yaml" + patterns: + - pattern: | + uses: $ACTION + ... + - metavariable-regex: + metavariable: $ACTION + regex: ^zizmorcore/zizmor-action@ + - pattern-not: | + uses: $ACTION + with: + ... + version: ${{ steps.zizmor_version.outputs.version }} + ... + ... + + - id: la-stack.github-actions.zizmor-version-resolved-from-just + languages: + - yaml + severity: ERROR + message: "Resolve the zizmor_version step's output with the canonical just --evaluate zizmor_version sequence in .github/workflows/zizmor.yml." + metadata: + category: security + rationale: >- + An output expression alone does not prove scanner parity. Require the + resolver to read justfile and publish that value without replacement. + paths: + include: + - "/.github/workflows/**/*.yml" + - "/.github/workflows/**/*.yaml" + - "/tests/semgrep/.github/workflows/**/*.yml" + - "/tests/semgrep/.github/workflows/**/*.yaml" + patterns: + - pattern: | + id: zizmor_version + ... + run: $SCRIPT + - metavariable-pattern: + metavariable: $SCRIPT + language: bash + patterns: + - pattern: ... + - pattern-not-inside: | + set -euo pipefail + version="$(just --evaluate zizmor_version)" + if [[ -z "$version" ]]; then + echo "::error::Could not resolve zizmor_version from justfile" + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + - id: la-stack.github-actions.release-no-dependency-cache + languages: + - yaml + severity: ERROR + message: "Release artifact jobs must avoid cache actions and explicitly disable setup-rust-toolchain, setup-uv, and setup-just caches." + metadata: + category: security + rationale: >- + Durable release artifacts must not consume writable dependency caches, + including installed tool binaries. Read-only cache restoration is still + restoration and is forbidden in these workflows. + paths: + include: + - "/.github/workflows/release-*.yml" + - "/.github/workflows/release-*.yaml" + - "/tests/semgrep/.github/workflows/release-*.yml" + - "/tests/semgrep/.github/workflows/release-*.yaml" + pattern-either: + - patterns: + - pattern: | + uses: $ACTION + ... + - metavariable-regex: + metavariable: $ACTION + regex: "(?i)^(?:actions/cache(?:/[^@]+)?|swatinem/rust-cache|taiki-e/cache-cargo-install-action)@" + - patterns: + - pattern: | + uses: $ACTION + ... + - metavariable-regex: + metavariable: $ACTION + regex: ^actions-rust-lang/setup-rust-toolchain@ + - pattern-not: | + uses: $ACTION + with: + ... + cache: false + cache-bin: false + ... + ... + - patterns: + - pattern: | + uses: $ACTION + ... + - metavariable-regex: + metavariable: $ACTION + regex: ^astral-sh/setup-uv@ + - pattern-not: | + uses: $ACTION + with: + ... + enable-cache: false + ... + ... + - patterns: + - pattern: | + uses: $ACTION + ... + - metavariable-regex: + metavariable: $ACTION + regex: '^(?:\./|\$/)\.github/actions/setup-just/?$' + - pattern-not: | + uses: $ACTION + with: + ... + cache: false + ... + ... + - id: la-stack.docs.check-before-fix-command-order languages: - regex diff --git a/src/exact.rs b/src/exact.rs index 756a266..0bfe65b 100644 --- a/src/exact.rs +++ b/src/exact.rs @@ -76,6 +76,16 @@ //! and conversion. Reference numbers refer to //! `REFERENCES.md`. //! +//! ## Exact-to-binary64 conversion +//! +//! Strict conversion checks dyadic representability, significand width, and +//! exponent range. Integer-and-exponent rounding reads retained, guard, and +//! sticky bits directly; rational-value rounding uses `num-rational`'s +//! `ToPrimitive::to_f64`. Both implement the nearest-even output policy in +//! `REFERENCES.md` \[9-10\]. See the +//! [conversion criteria](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md#exact-to-binary64-conversion) +//! for subnormal, tie, and overflow cases. +//! //! ## Validation //! //! Public `Matrix` / `Vector` values are finite by construction before exact @@ -576,6 +586,10 @@ fn magnitude_has_lower_bits(value: &BigInt, exclusive_end: u64) -> bool { } /// Right-shift a magnitude and round the retained integer to nearest-even. +/// +/// The guard bit rounds upward exactly when lower discarded bits are nonzero +/// or the retained integer is odd. This is the IEEE 754 tie rule from +/// `REFERENCES.md` \[9-10\], applied before binary64 exponent assembly. fn rounded_shifted_magnitude_to_u64(value: &BigInt, shift: u64) -> Option { if shift > value.bits() { return Some(0); @@ -714,6 +728,9 @@ fn big_int_exp_ref_to_rounded_f64( /// successful strict conversion does not clone an already-computed exact /// result. `rounded_reason` is evaluated only when finite output would require /// rounding. +/// After stripping trailing zeros, an odd magnitude with bit length `b` is +/// representable exactly when `b ≤ 53`, `exp ≥ -1074`, and +/// `exp + b - 1 ≤ 1023`; see `REFERENCES.md` \[9-10\]. fn big_int_exp_ref_to_finite_f64( value: &BigInt, exp: i32, diff --git a/src/interval.rs b/src/interval.rs index d80449f..d266e5b 100644 --- a/src/interval.rs +++ b/src/interval.rs @@ -1,6 +1,14 @@ #![forbid(unsafe_code)] //! Outward-rounded intervals and fixed-size interval determinant signs. +//! +//! See `REFERENCES.md` \[8\] for `TwoSum`, \[9-11\] for the binary64 arithmetic +//! model, and \[12\] for the Leibniz determinant identity. The column-subset +//! evaluation is specialized to this crate's small dimensions. Reference +//! \[14\] describes the broader interval standard; this module does not claim +//! IEEE 1788 conformance. The +//! [interval construction](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md#outward-rounded-interval-expressions) +//! explains outward endpoints and determinant enclosures. use crate::rounding::{compare_product_with_rounded, two_sum_error}; use crate::{ArithmeticOperation, IntervalBound, IntervalOperand, LaError, Matrix}; diff --git a/src/lib.rs b/src/lib.rs index 24dd85b..4d40b30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -956,11 +956,13 @@ pub use rational::{RationalMatrix, RationalVector}; // the computed `permanent` value used by the bound may itself be rounded even // though the mathematical quantity above is exact. // -// Each constant has the shape `a · EPS + b · EPS²`: the linear term bounds -// the first-order rounding and the quadratic term absorbs the interaction -// of errors in nested FMAs. The coefficients `a` and `b` are conservative -// over-estimates derived from the longest dependency chain of `det_direct` -// at that dimension. +// The longest rounding paths in the determinant and permanent have lengths +// k = 2, 5, and 9. With EPS = 2^-52, gamma_k = k*EPS / (1 - k*EPS) bounds +// their accumulated relative perturbations. Each exactly representable +// coefficient satisfies c >= gamma_k / ((1 - EPS)*(1 - gamma_k)), including +// possible downward rounding of both the permanent and the final product. +// See docs/mathematical_basis.md, "Derivation of the returned determinant +// bound", for the full argument and the underflow assumptions. // // These constants are NOT feature-gated — they rely only on f64 arithmetic // and are useful for adaptive-precision logic even without the `exact` @@ -990,10 +992,12 @@ const EPS: f64 = f64::EPSILON; // 2^-52 /// ``` /// /// `det_direct` evaluates `a·d - b·c` as one multiply followed by one FMA -/// (2 rounding events); the linear `3·EPS` term bounds those roundings -/// and the quadratic `16·EPS²` term is a conservative cushion for their -/// interaction. Derivation follows Shewchuk's framework; see -/// `REFERENCES.md` \[8\]. +/// (2 rounding events on the longest path). The permanent also has a +/// two-event path. The coefficient covers both trees and the final rounded +/// multiplication; see the +/// [returned-bound derivation](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md#derivation-of-the-returned-determinant-bound). +/// The analysis follows Shewchuk's framework and the binary64 arithmetic +/// model; see `REFERENCES.md` \[8-11\]. /// /// Prefer /// [`Matrix::det_direct_with_errbound`](crate::Matrix::det_direct_with_errbound) @@ -1040,8 +1044,10 @@ pub const ERR_COEFF_2: f64 = 3.0 * EPS + 16.0 * EPS * EPS; /// where `p(|A|)` is the absolute Leibniz sum (the same cofactor /// expansion as `det_direct` but with `|·|` at every leaf). /// `det_direct` for D=3 uses three 2×2 FMA minors combined by a nested -/// FMA, yielding the `8·EPS + 64·EPS²` bound. See `REFERENCES.md` -/// \[8\] for the Shewchuk framework these bounds follow. +/// FMA. The determinant and permanent each have at most five rounding events +/// per monomial; `8·EPS + 64·EPS²` also covers rounding the permanent and final +/// bound product. See [`ERR_COEFF_2`] for the derivation link and +/// `REFERENCES.md` \[8-11\] for the analysis framework. /// /// Prefer /// [`Matrix::det_direct_with_errbound`](crate::Matrix::det_direct_with_errbound) @@ -1065,9 +1071,11 @@ pub const ERR_COEFF_3: f64 = 8.0 * EPS + 64.0 * EPS * EPS; /// where `p(|A|)` is the absolute Leibniz sum. `det_direct` for D=4 /// evaluates four nested 3×3 cofactors, sharing their six 2×2 minors when /// every coefficient in the first two rows is non-zero, and reduces them with -/// an FMA row combination, yielding the -/// `12·EPS + 128·EPS²` bound. See `REFERENCES.md` \[8\] for the -/// Shewchuk framework these bounds follow. +/// an FMA row combination. Dense and sparse trees each have at most nine +/// rounding events per monomial, as does the permanent tree. The coefficient +/// `12·EPS + 128·EPS²` also covers rounding the permanent and final bound +/// product. See [`ERR_COEFF_2`] for the derivation link and `REFERENCES.md` +/// \[8-11\] for the analysis framework. /// /// Prefer /// [`Matrix::det_direct_with_errbound`](crate::Matrix::det_direct_with_errbound) diff --git a/src/rational.rs b/src/rational.rs index 1875594..f94c47d 100644 --- a/src/rational.rs +++ b/src/rational.rs @@ -8,6 +8,10 @@ //! crate's fraction-free [`BigInt`] Bareiss backend. The positive row scales //! preserve determinant sign; determinant values divide by their product; and //! solves apply the same row scale to the matrix and right-hand side. +//! See `REFERENCES.md` \[7\] for Bareiss elimination and \[12\] for determinant +//! multilinearity. The +//! [row-clearing construction](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md#exact-arithmetic-over-rational-inputs) +//! explains how these identities apply to canonical rational inputs. use std::array::from_fn; @@ -349,6 +353,10 @@ fn integer_at_scale(value: &BigRational, scale: &BigInt) -> BigInt { } /// Return the positive least common multiple of two positive integers. +/// +/// `lcm(a, b) = (a / gcd(a, b)) × b`; dividing before multiplying avoids +/// forming the larger intermediate `a × b`. The resulting positive scale +/// clears both denominators without changing determinant sign. fn least_common_multiple(lhs: BigInt, rhs: &BigInt) -> BigInt { let gcd = greatest_common_divisor(lhs.clone(), rhs.clone()); (lhs / gcd) * rhs diff --git a/src/rounding.rs b/src/rounding.rs index a7f47fa..f7b1967 100644 --- a/src/rounding.rs +++ b/src/rounding.rs @@ -1,12 +1,17 @@ #![forbid(unsafe_code)] //! Shared binary64 rounding primitives for certified arithmetic. +//! +//! Representation and rounding use the IEEE 754 model in `REFERENCES.md` +//! \[9-10\]. The interval and reduction certificates share these primitives. /// Return the exact error in a rounded binary64 sum. /// /// This is Knuth's `TwoSum` transform. With IEEE-754 round-to-nearest and /// gradual underflow, `rounded + error` equals the exact-real sum whenever the /// rounded sum is finite. +/// See `REFERENCES.md` \[8\] for the transform and its error-free arithmetic +/// analysis. Callers supply finite operands and their rounded sum. #[inline] pub(crate) const fn two_sum_error(left: f64, right: f64, rounded: f64) -> f64 { let virtual_right = rounded - left; @@ -78,6 +83,10 @@ const fn compare_binary_magnitudes( } /// Compare the exact-real product `left × right` with its rounded result. +/// +/// Nonzero finite operands contribute at most 53 significand bits each, so the +/// exact product fits in `u128`. Integer comparison selects the outward +/// endpoint even when the rounded product is zero; see `REFERENCES.md` \[9-10\]. #[inline] pub(crate) const fn compare_product_with_rounded(left: f64, right: f64, rounded: f64) -> i8 { let negative = left.is_sign_negative() != right.is_sign_negative(); diff --git a/src/scaled_product.rs b/src/scaled_product.rs index 0a2397d..945516b 100644 --- a/src/scaled_product.rs +++ b/src/scaled_product.rs @@ -1,6 +1,12 @@ #![forbid(unsafe_code)] //! Allocation-free scaled products for floating-point factor diagonals. +//! +//! Binary64 decomposition and rounding follow `REFERENCES.md` \[9-10\]. +//! Mantissa normalization and a separate exponent preserve intermediate range; +//! they do not remove rounding in earlier mantissa multiplications. See the +//! [scaled determinant product description](https://github.com/acgetchell/la-stack/blob/main/docs/mathematical_basis.md#scaled-determinant-products) +//! for the replay policy and deferred final-factor rounding. const SIGN_MASK: u64 = 1_u64 << 63; const FRACTION_BITS: u32 = 52; @@ -91,6 +97,9 @@ impl ScaledProduct { } /// Multiply by one factor while retaining a normalized mantissa. + /// + /// A non-finite factor is recorded so [`Self::finish`] returns `None`, + /// even if another factor is zero. #[inline] pub(crate) const fn multiply(&mut self, factor: f64) { let bits = factor.to_bits(); @@ -155,11 +164,14 @@ impl ScaledProduct { } } - /// Round the accumulated product to binary64. + /// Finalize the accumulated mantissa and pending factor as binary64. /// - /// Returns `None` only when the accumulated result rounds outside the - /// finite binary64 range. Magnitudes below that range round to a signed - /// zero or subnormal value with round-to-nearest, ties-to-even semantics. + /// Returns `None` if any factor was non-finite or the accumulated result + /// rounds outside the finite binary64 range. Magnitudes below that range + /// round to a signed zero or subnormal value with round-to-nearest, + /// ties-to-even semantics. + /// Earlier mantissa products have already rounded, so this does not + /// guarantee correct rounding of the exact product of all original factors. #[inline] #[expect( clippy::cast_possible_truncation, diff --git a/tests/semgrep/.github/workflows/release-cache-policy.yml b/tests/semgrep/.github/workflows/release-cache-policy.yml new file mode 100644 index 0000000..f54d269 --- /dev/null +++ b/tests/semgrep/.github/workflows/release-cache-policy.yml @@ -0,0 +1,107 @@ +name: Release cache isolation fixtures +on: + workflow_dispatch: + +jobs: + fixtures: + runs-on: ubuntu-latest + steps: + - name: Cache-free Rust toolchain + # ok: la-stack.github-actions.release-no-dependency-cache + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + with: + cache: false + cache-bin: false + + - name: Restored Rust dependencies + # ruleid: la-stack.github-actions.release-no-dependency-cache + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + with: + cache: true + cache-bin: false + + - name: Implicit Rust binary caching + # ruleid: la-stack.github-actions.release-no-dependency-cache + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + with: + cache: false + + # ok: la-stack.github-actions.release-no-dependency-cache + - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + with: + cache-bin: false + cache: false + + # ruleid: la-stack.github-actions.release-no-dependency-cache + - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + with: + cache-bin: true + cache: false + + - name: Cache-free just + # ok: la-stack.github-actions.release-no-dependency-cache + uses: ./.github/actions/setup-just + with: + cache: false + + - name: Implicit just cache + # ruleid: la-stack.github-actions.release-no-dependency-cache + uses: ./.github/actions/setup-just + + # ok: la-stack.github-actions.release-no-dependency-cache + - uses: ./.github/actions/setup-just + with: + cache: false + + # ruleid: la-stack.github-actions.release-no-dependency-cache + - uses: ./.github/actions/setup-just + with: + cache: true + + - name: Cache-free uv + # ok: la-stack.github-actions.release-no-dependency-cache + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: false + + - name: Implicit uv cache + # ruleid: la-stack.github-actions.release-no-dependency-cache + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + + # ok: la-stack.github-actions.release-no-dependency-cache + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: false + + # ruleid: la-stack.github-actions.release-no-dependency-cache + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: true + + - name: Cache-restoring installer + # ruleid: la-stack.github-actions.release-no-dependency-cache + uses: taiki-e/cache-cargo-install-action@9ee83daaa7b96a6fab930949ecf1122bba04a389 # v3.0.8 + with: + tool: cargo-nextest@0.9.134 + + # ruleid: la-stack.github-actions.release-no-dependency-cache + - uses: taiki-e/cache-cargo-install-action@9ee83daaa7b96a6fab930949ecf1122bba04a389 # v3.0.8 + with: + tool: cargo-nextest@0.9.134 + + - name: Source install without cache action + # ok: la-stack.github-actions.release-no-dependency-cache + run: cargo install --locked cargo-nextest --version "$CARGO_NEXTEST_VERSION" + + - name: Direct cache action + # ruleid: la-stack.github-actions.release-no-dependency-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + + # ruleid: la-stack.github-actions.release-no-dependency-cache + - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + + - name: Read-only Rust cache is still a restored cache + # ruleid: la-stack.github-actions.release-no-dependency-cache + uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + save-if: false diff --git a/tests/semgrep/.github/workflows/zizmor_policy.yml b/tests/semgrep/.github/workflows/zizmor_policy.yml new file mode 100644 index 0000000..90ab293 --- /dev/null +++ b/tests/semgrep/.github/workflows/zizmor_policy.yml @@ -0,0 +1,120 @@ +name: Zizmor scanner version fixtures +on: + workflow_dispatch: + +jobs: + fixtures: + runs-on: ubuntu-latest + steps: + - name: Resolve the canonical scanner version + # ok: la-stack.github-actions.zizmor-version-resolved-from-just + id: zizmor_version + shell: bash + run: | + set -euo pipefail + version="$(just --evaluate zizmor_version)" + if [[ -z "$version" ]]; then + echo "::error::Could not resolve zizmor_version from justfile" + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Hard-coded resolver output + # ruleid: la-stack.github-actions.zizmor-version-resolved-from-just + id: zizmor_version + shell: bash + run: | + set -euo pipefail + version="1.29.0" + if [[ -z "$version" ]]; then + echo "::error::Could not resolve zizmor_version from justfile" + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: A source read must not justify an unrelated output + # ruleid: la-stack.github-actions.zizmor-version-resolved-from-just + id: zizmor_version + shell: bash + run: | + set -euo pipefail + version="$(just --evaluate zizmor_version)" + if [[ -z "$version" ]]; then + echo "::error::Could not resolve zizmor_version from justfile" + exit 1 + fi + echo "version=1.29.0" >> "$GITHUB_OUTPUT" + + # ok: la-stack.github-actions.zizmor-version-resolved-from-just + - id: zizmor_version + run: | + set -euo pipefail + version="$(just --evaluate zizmor_version)" + if [[ -z "$version" ]]; then + echo "::error::Could not resolve zizmor_version from justfile" + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + # ruleid: la-stack.github-actions.zizmor-version-resolved-from-just + - id: zizmor_version + run: | + set -euo pipefail + version="$(just --evaluate cargo_nextest_version)" + if [[ -z "$version" ]]; then + echo "::error::Could not resolve zizmor_version from justfile" + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: A resolved version must not be overwritten + # ruleid: la-stack.github-actions.zizmor-version-resolved-from-just + id: zizmor_version + run: | + set -euo pipefail + version="$(just --evaluate zizmor_version)" + if [[ -z "$version" ]]; then + echo "::error::Could not resolve zizmor_version from justfile" + exit 1 + fi + version="1.29.0" + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Resolved scanner version + # ok: la-stack.github-actions.zizmor-tool-version-pinned + uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 + with: + inputs: .github + version: ${{ steps.zizmor_version.outputs.version }} + + - name: Floating scanner version + # ruleid: la-stack.github-actions.zizmor-tool-version-pinned + uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 + with: + inputs: .github + version: latest + + - name: Missing scanner version + # ruleid: la-stack.github-actions.zizmor-tool-version-pinned + uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 + with: + inputs: .github + + - name: Independent pin can drift from justfile + # ruleid: la-stack.github-actions.zizmor-tool-version-pinned + uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 + with: + version: "1.30.0" + + # ok: la-stack.github-actions.zizmor-tool-version-pinned + - uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 + with: + version: ${{ steps.zizmor_version.outputs.version }} + + # ruleid: la-stack.github-actions.zizmor-tool-version-pinned + - uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 + + # A later step's version must not satisfy the preceding step. + - uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 + with: + version: ${{ steps.zizmor_version.outputs.version }} diff --git a/uv.lock b/uv.lock index b364149..6ba90eb 100644 --- a/uv.lock +++ b/uv.lock @@ -478,7 +478,7 @@ dev = [ { name = "ruff", specifier = "==0.16.6" }, { name = "semgrep", specifier = "==1.176.1" }, { name = "shellcheck-py", specifier = "==0.11.0.1" }, - { name = "shfmt-py", specifier = "==4.1.0" }, + { name = "shfmt-py", specifier = "==4.2.0" }, { name = "ty", specifier = "==0.0.78" }, { name = "yamllint", specifier = "==1.38.0" }, ] @@ -1113,15 +1113,15 @@ wheels = [ [[package]] name = "shfmt-py" -version = "4.1.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/2f/c9616d3716a8e75a4c36427bede49d61b68c2d1595a76b3de2347e040fa7/shfmt_py-4.1.0.tar.gz", hash = "sha256:e863c07885e2e976b1441e38f660b3262a87a52bf84124415b866e48eed7c00b", size = 29913, upload-time = "2026-08-29T11:26:35.27Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/91/0ae8bbc703ac6779427fcdb40da7784b844278032f7896dbe54cb7e11e0e/shfmt_py-4.2.0.tar.gz", hash = "sha256:cf7842d69e9f787ce97503c1280e65417f2c08014d594018dda245fcba96ff51", size = 30005, upload-time = "2026-09-07T15:25:10.507Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/e1/1d0129c8baaf943921a8b1402fc9bb2a1fc9753a35ddc068a639828e91d8/shfmt_py-4.1.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:7186d0cdb21d8a5e6cb647c1b0ae7ee264751be3cac755277d060df90b1cfa8f", size = 1552154, upload-time = "2026-08-29T11:26:28.435Z" }, - { url = "https://files.pythonhosted.org/packages/36/03/e8f10813c498247f6ddff63c82e4bb351fda188ec73e3821433f913cb651/shfmt_py-4.1.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:8e0201449fc35f3cf16abed3189f1de0c5c30d5c4c989b8d367fd5cd1d35e779", size = 1424817, upload-time = "2026-08-29T11:26:29.854Z" }, - { url = "https://files.pythonhosted.org/packages/ff/91/0bc95e1f3ba2b591986701d00c8ee80eef228fb08c338a873d0df3444092/shfmt_py-4.1.0-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:887a7a04a297302f3472edaeabb5f33f146d9a681234743acc5932a2a39bdb22", size = 1398676, upload-time = "2026-08-29T11:26:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/0f/b2/101c166f6f1895a7e4b2062d350bf156097087ffc95f772877e51c04358d/shfmt_py-4.1.0-py2.py3-none-manylinux2014_x86_64.whl", hash = "sha256:4f1573856d84725f148175ef56532365dc4a7e5332e27c31b6c82be544ad2bda", size = 1557748, upload-time = "2026-08-29T11:26:32.471Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e8/be46d2141ec18d30c713258c97c4a8d85f5e39cedf8c9b2ba642c28a2199/shfmt_py-4.1.0-py2.py3-none-win_amd64.whl", hash = "sha256:7e5c55ac4b619fc4d215259459ac80e8513735245ec621ca290574fa7ab2811d", size = 1661642, upload-time = "2026-08-29T11:26:34.018Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/b07a16ffebc4f04ad7b7f1a6928edbe0c1d024be089e8e37bcf9bb963091/shfmt_py-4.2.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:aaf9e195c2e115309c32ef0d8d05b42afc4a23468b61230a09a8a4bdac95d027", size = 1552980, upload-time = "2026-09-07T15:25:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/e3/39/3b5c4061a4fa7dcfab88ca4bd17566132a82225aad78d6d5b42cab4beee7/shfmt_py-4.2.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b7fa81b120fdccebbe60b9c3411ce1b549fa2828f2809e8fa5e231a9bac65dd", size = 1425340, upload-time = "2026-09-07T15:25:04.79Z" }, + { url = "https://files.pythonhosted.org/packages/53/32/9448afcca6739c64810143d4dacae6a817308aab71d083d39e91b13b31ee/shfmt_py-4.2.0-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:e615cfcfe3e184f59e3ac2574895542976995a46f7506b327c8af71ae02de84c", size = 1399402, upload-time = "2026-09-07T15:25:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/8a/03246179f8bafea1c4f0b664ef5fcad1814f2cf55af6acb2f614463eca20/shfmt_py-4.2.0-py2.py3-none-manylinux2014_x86_64.whl", hash = "sha256:867b55792952d4e4aa27a1b2d578e63b745a77e11b7742d2f6258ef123a9c8f2", size = 1558575, upload-time = "2026-09-07T15:25:07.766Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4a/755b7c7cde56dd7306cea2a50e75a8a6a9281df52e124a5bb0b03474c6ae/shfmt_py-4.2.0-py2.py3-none-win_amd64.whl", hash = "sha256:566f46c036cb475ac84ac952d83122b67c9557418c24b04628c5782f9f57eccf", size = 1662632, upload-time = "2026-09-07T15:25:09.141Z" }, ] [[package]]