Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .semgrepignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Preserve Semgrep 1.180.0 defaults except test/, tests/, and testsuite/ so
# ordinary Rust and Python tests remain in production scans.
# https://github.com/semgrep/semgrep/blob/v1.180.0/src/targeting/default.semgrepignore
.git
.svn
.hg
_darcs
CVS

build/
vendor/
dist/
*.min.js
.env/
.tox/

node_modules/
.npm/
.yarn/
.venv/
_opam/
_build/
_cargo/

*_test.go

# Deliberately invalid rule fixtures are checked separately by just semgrep-test.
/tests/semgrep/
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions REFERENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ original coordinates rather than an already-rounded difference vector.

`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
into `mantissa × 2^exponent`. Each row uses its own minimum nonzero exponent `e_i`
to form an integer row; the determinant is reconstructed as `det_int × 2^(Σ e_i)`.
Zero rows use exponent zero. Dimensions 0–4 use direct 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.

Expand Down Expand Up @@ -136,8 +138,9 @@ Both the determinant and solve paths convert their finite-by-construction entrie
`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
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
component. Shifting each mantissa left by `exp − e_min` gives a GCD-free
exact-integer starting point. Determinants choose `e_min` separately for each row;
solves use the matrix and RHS scales described above. Solves and D ≥ 5 determinants
then apply Bareiss elimination; D ≤ 4 determinants use direct expansions. Tests verify
non-finite input rejection at the `Matrix` and `Vector` constructors. The test-only
`f64_to_big_rational` helper uses `BigRational::from_f64` as an independent oracle for
Expand Down
8 changes: 5 additions & 3 deletions docs/BENCHMARKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -845,16 +845,18 @@ measured 304 comparative benchmarks in about 55m 34s, plus 2m 9s compilation
that run provides no complete exact-suite runtime. The old 60-minute outer
limit therefore could not accommodate even that smaller harness.

Inventory on 2026-09-07 found the following current workloads. The planning
estimate uses 12 seconds per benchmark, allowing analysis/report overhead
The comparative inventory on 2026-09-07 contained 533 cases. The current exact
suite contains 354 cases: 264 original cases plus 90 diagnostic cases retained
by the [row-scaling study](archive/performance/studies/exact-row-scaling.md).
The planning estimate uses 12 seconds per benchmark, allowing analysis/report overhead
above the configured 3-second warmup and 5-second measurement target. This is
a capacity estimate, not a measured runtime or a guaranteed upper bound;
Criterion can extend measurement for expensive iterations.

| Suite | Benchmarks | Nominal warmup + measurement | Planning estimate | Step limit | Headroom above estimate |
| --- | --- | --- | --- | --- | --- |
| `vs_linalg` | 533 | 71.1 min | 106.6 min | 150 min | 43.4 min (41%) |
| `exact` | 264 | 35.2 min | 52.8 min | 90 min | 37.2 min (70%) |
| `exact` | 354 | 47.2 min | 70.8 min | 90 min | 19.2 min (27%) |

Both commands retain Criterion's release defaults: 100 samples, 100,000
resamples, 95% confidence, 3-second warmup, and 5-second measurement target.
Expand Down
7 changes: 4 additions & 3 deletions docs/MEASURING_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ Nextest also writes JUnit test results to:
target/nextest/coverage/test-results/junit.xml
```

The Codecov workflow reads the `cargo-llvm-cov` and `cargo-nextest` versions
from the `justfile`, installs those exact versions plus Rust's
`llvm-tools-preview` component, and runs `just coverage-ci`. It verifies both
The Codecov workflow uses the shared tool setup to install the `cargo-llvm-cov`
and `cargo-nextest` versions declared in `pyproject.toml` under
`[tool.research-repo-tools.toolchain.cargo]`, plus the `llvm-tools-preview`
component declared in `rust-toolchain.toml`, and runs `just coverage-ci`. It verifies both
reports, uploads the Cobertura and JUnit files to Codecov, and archives each
report directory as a workflow artifact. Local setup via `just setup-tools`
installs the same Rust component and cargo subcommands.
Expand Down
4 changes: 4 additions & 0 deletions docs/code_organization.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ dispositions, Python 3.15 review and native validation gates.
`tooling/semgrep/` owns a locked Python 3.14 environment for Semgrep 1.180.0
while its wheels exclude Python 3.15. Local scans, shared fixture validation
and hosted SARIF use it; ordinary support scripts use Python 3.15.
The root `.semgrepignore` keeps ordinary Rust and Python tests in those scans
while excluding deliberate `tests/semgrep/` fixtures, which `just semgrep-test`
validates separately. Git-ignored dependencies, environments, caches, and build
outputs remain outside production scans.

`.github/actions/prepare-release-benchmarks/action.yml` groups tool installation,
input validation, and inventory under the release workflow's shared setup timeout.
Expand Down
46 changes: 21 additions & 25 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ Until v1.0, API breaks are acceptable when they improve correctness,
performance, or orthogonality. The `v0.4.x` line stays on stable Rust and includes
focused robustness APIs, downstream ergonomics, API-contract cleanup,
documentation, validation, invariant audits, benchmark coverage, and
release/tooling hardening. The current release is `v0.4.6`.
The next planned release is `v0.5.0`, consolidating the work previously targeted
at `v0.4.7`. It remains on stable Rust and makes the Rust 1.99.1 minimum an
release/tooling hardening. The current release is
[`v0.5.0`](https://github.com/acgetchell/la-stack/releases/tag/v0.5.0), published
on 2026-10-17, consolidating the work previously targeted at `v0.4.7`.
It remains on stable Rust and makes the Rust 1.99.1 minimum an
explicit downstream upgrade boundary.
Expanded const-generic work is tracked separately without an assigned release;
it depends on the specific language capabilities required by the proposed API.
Expand Down Expand Up @@ -237,13 +238,13 @@ storage remains available through `as_array()` and `into_array()`.
Further stable-Rust maintenance can remain in `v0.4.x`; it does not depend on
expanded const-generic support.

### v0.5.0 Stable Rust APIs and Performance (planned)
### v0.5.0 Stable Rust APIs and Performance (released)

The [v0.5.0 milestone](https://github.com/acgetchell/la-stack/milestone/3)
replaces the planned `v0.4.7` release. It combines the completed Rust 1.99
replaced the planned `v0.4.7` release. It combined the completed Rust 1.99
migration and stable vector-angle API with correctness-preserving performance
improvements and release-tooling adoption. Expanded const-generics stabilization
does not gate this release.
did not gate this release.

Completed work included in this release:

Expand Down Expand Up @@ -279,28 +280,23 @@ API or feature is planned. The
decision, executed checks, and unperformed native Linux/Windows runs; the
[study](archive/performance/studies/algebraic-float.md) retains the raw evidence.

Remaining release sequence:
The release sequence is complete:

1. Review and land the triangular rational-solve optimization
1. The triangular rational-solve optimization
([#246](https://github.com/acgetchell/la-stack/issues/246),
[draft PR #278](https://github.com/acgetchell/la-stack/pull/278)), retaining the
[PR #278](https://github.com/acgetchell/la-stack/pull/278)) landed with the
exact general-system fallback and independent regression evidence.
2. Adopt the published research-repo-tools v0.1.9 capabilities
([#276](https://github.com/acgetchell/la-stack/issues/276)) after the upstream
[publication gate](https://github.com/acgetchell/research-repo-tools/issues/99)
verifies the registry release. Preserve the dimension plot, scientific
eligibility rules, and historical artifacts.
3. Complete the release validation and `v0.4.6` versus `v0.5.0` performance record
through the validated adopted workflow. Configure and verify the crates.io
publishing workflow and publisher settings
([#259](https://github.com/acgetchell/la-stack/issues/259)) before publication.
4. Publish the reviewed `v0.5.0` release and verify crates.io availability and a
clean downstream dependency resolution. Close #259 only after actual
publication succeeds and its evidence is recorded.

Consumers using a `0.4` Cargo requirement must explicitly adopt `0.5` after the
release is available. Delaunay's adoption issues remain scheduled for `v0.8.3`
and retain their independent correctness and performance gates. Package versions
2. The published shared-tooling capabilities were adopted
([#276](https://github.com/acgetchell/la-stack/issues/276)), preserving the
dimension plot, scientific eligibility rules, and historical artifacts.
3. Release validation and the `v0.4.6` versus `v0.5.0` performance record completed
through the adopted workflow. The crates.io publishing workflow and publisher
settings were verified ([#259](https://github.com/acgetchell/la-stack/issues/259)).
4. The reviewed `v0.5.0` release was published and registry availability verified;
#259 closed after publication on 2026-10-17.

Consumers using a `0.4` Cargo requirement must explicitly adopt `0.5`.
Downstream adoption retains independent correctness and performance gates. Package versions
and generated release artifacts are updated during dedicated release preparation.

### v0.5.1 Follow-Up Work (planned)
Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ security-secrets:

# Repository-owned Semgrep rules for project-specific diagnostics.
semgrep: _ensure-uv
uv run --locked --project tooling/semgrep semgrep --metrics off --error --strict --timeout 30 --exclude tests/semgrep/src/project_rules/algebraic_float.rs --config semgrep.yaml .
uv run --locked --project tooling/semgrep semgrep --metrics off --error --strict --timeout 30 --config semgrep.yaml .

# Fixture tests for repository-owned Semgrep rules.
semgrep-test: python-sync
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ line-length = 160

[tool.uv]
package = false
required-version = "==0.13.1"
required-version = "==0.13.2"
# Semgrep has its own locked Python 3.14 environment under tooling/semgrep.

[dependency-groups]
Expand Down
73 changes: 73 additions & 0 deletions scripts/tests/test_maintenance_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,76 @@ def test_semgrep_keeps_its_current_version_outside_the_python_315_environment()
assert scanner["project"]["requires-python"] == ">=3.14,<3.15"
result = run_command("uv", ["run", "--locked", "--project", "tooling/semgrep", "semgrep", "--version"], cwd=REPO_ROOT)
assert result.stdout.strip() == "1.180.0"


@pytest.mark.parametrize("output_args", [pytest.param((), id="local"), pytest.param(("--sarif",), id="sarif")])
def test_semgrep_production_targets_include_tests_and_exclude_rule_fixtures(tmp_path: Path, output_args: tuple[str, ...]) -> None:
"""Exercise both scan formats without Git ignores masking Semgrep exclusions."""
shutil.copyfile(REPO_ROOT / ".semgrepignore", tmp_path / ".semgrepignore")
included = {
"tests/proptest_exact.rs",
"benches/comparison/tests/vs_linalg_inputs.rs",
"scripts/tests/test_release_baseline.py",
".github/workflows/semgrep-sarif.yml",
"test/control.rs",
"testsuite/control.py",
"src/control.go",
"assets/control.js",
}
excluded = {
f"{directory}/ignored.py"
for directory in (
".svn",
".hg",
"_darcs",
"CVS",
"build",
"vendor",
"dist",
".env",
".tox",
"node_modules",
".npm",
".yarn",
".venv",
"_opam",
"_build",
"_cargo",
"tests/semgrep",
"nested/dist",
"nested/.env",
"nested/_build",
)
} | {"assets/bundle.min.js", "src/example_test.go", "nested/example_test.go"}
for name in included | excluded:
path = tmp_path / name
path.parent.mkdir(parents=True, exist_ok=True)
comment = "#" if path.suffix in {".py", ".yml"} else "//"
path.write_text(f"{comment} Target selection fixture.\n", encoding="utf-8")
result = run_command(
"uv",
[
"run",
"--locked",
"--project",
str(REPO_ROOT / "tooling/semgrep"),
"semgrep",
"scan",
"--metrics",
"off",
"--disable-version-check",
"--error",
"--strict",
"--timeout",
"30",
"--config",
str(REPO_ROOT / "semgrep.yaml"),
*output_args,
"--x-ls-long",
".",
],
cwd=tmp_path,
)
selected = {Path(line.removeprefix("selected ")).as_posix() for line in result.stdout.splitlines() if line.startswith("selected ")}
assert included <= selected
assert not (excluded & selected)
6 changes: 4 additions & 2 deletions scripts/tests/test_performance_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ def assert_readme_presentation(readme: str, statistic: str) -> None:
assert f"Statistic: {statistic}. Unit: ns." in table
current, peer, ratio = ("12<br>[10.8, 13.2]", "20<br>[18, 22]", "1.66667") if statistic == "mean" else ("8<br>[7.2, 8.8]", "10<br>[9, 11]", "1.25")
faer, faer_ratio = ("32<br>[28.8, 35.2]", "2.66667") if statistic == "mean" else ("16<br>[14.4, 17.6]", "2")
assert f"| 2 | {current} | {peer} | {faer} | {ratio}\N{MULTIPLICATION SIGN} | {faer_ratio}\N{MULTIPLICATION SIGN} |" in table
multiplication_sign = "\N{MULTIPLICATION SIGN}"
assert f"| 2 | {current} | {peer} | {faer} | {ratio}{multiplication_sign} | {faer_ratio}{multiplication_sign} |" in table
assert "ratios are point estimates, with no ratio intervals or significance claims" in table
for name in ("nalgebra", "faer"):
assert f"Speedup vs {name}: ratio; numerator = {name} (" in table
Expand Down Expand Up @@ -620,7 +621,8 @@ def test_native_command_policy_and_compatibility_adapter() -> None:
def test_native_inventory_and_phase_commands_use_the_measured_checkout(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, baseline_tag: str, scope: str) -> None:
"""Exercise the configured Justfile, stubbing only the expensive Cargo boundary."""
root = tmp_path / "checkout with spaces"
shutil.copytree(ROOT / "tooling", root / "tooling")
# Native benchmark policy needs the root configs, not the scanner environment.
shutil.copytree(ROOT / "tooling", root / "tooling", ignore=shutil.ignore_patterns("semgrep"))
shutil.copyfile(ROOT / "rust-toolchain.toml", root / "rust-toolchain.toml")
binary = tmp_path / "bin"
binary.mkdir()
Expand Down
7 changes: 6 additions & 1 deletion scripts/tests/test_release_baseline.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import textwrap
import tomllib
from pathlib import Path
from urllib.parse import urlsplit

import pytest
from research_repo_tools.criterion_inventory import CriterionInventory, load_discovery, serialize_inventory
Expand Down Expand Up @@ -274,6 +275,7 @@ def test_preflight_rejects_dispatch_outside_release_tag(tmp_path: Path, ref: str

GH_FIXTURE = r"""import hashlib, json, os, sys
from pathlib import Path
from urllib.parse import urlsplit
args = sys.argv[1:]
root = Path(os.environ["RELEASE_FIXTURE"])
tag = os.environ["RELEASE_TAG"]
Expand All @@ -289,7 +291,7 @@ def test_preflight_rejects_dispatch_outside_release_tag(tmp_path: Path, ref: str
value = [[release]]
elif args[0] == "release":
value = {"databaseId": 42, "tagName": tag}
elif any("uploads.github.com" in item for item in args):
elif any(urlsplit(item).hostname == "uploads.github.com" for item in args):
(root / "uploaded").touch()
value = {}
elif "PATCH" in args:
Expand Down Expand Up @@ -351,7 +353,10 @@ def test_installed_cli_binds_trusted_target_and_leaves_draft_for_review(tmp_path
assert json.loads(target)["expected_title"] == BASELINE
stale = run_shell(runner + shell_step("Attach baseline for review"), fixture, {**env, "RELEASE_TARGET": target, "REMOTE_COMMIT": "b" * 40})
assert stale.returncode != 0
assert not (fixture / "uploaded").exists()
assert not (fixture / "published").exists()
stale_calls = [json.loads(line) for line in (fixture / "api-calls").read_text(encoding="utf-8").splitlines()]
assert not any(urlsplit(item).hostname == "uploads.github.com" for call in stale_calls for item in call)
result = run_shell(runner + shell_step("Attach baseline for review"), fixture, {**env, "RELEASE_TARGET": target})
assert result.returncode == 0, result.stderr
assert (fixture / "uploaded").exists()
Expand Down
5 changes: 4 additions & 1 deletion semgrep.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ rules:
- "/examples/**/*.rs"
- "/benches/**/*.rs"
# Includes integration/property tests and the deliberate violation
# fixture. `just semgrep` excludes that fixture, while
# fixture. The root .semgrepignore excludes rule fixtures, while
# `just semgrep-test` scans it directly.
- "/tests/**/*.rs"
pattern-either:
Expand Down Expand Up @@ -376,6 +376,9 @@ rules:
- "/benches/**/*.rs"
- "/examples/**/*.rs"
- "/tests/semgrep/src/project_rules/bench_example_usage.rs"
exclude:
# The comparison crate's integration tests use ordinary test assertions.
- "/benches/comparison/tests/**"
pattern-either:
- pattern: $VALUE.unwrap()
- pattern: $VALUE.expect(...)
Expand Down
18 changes: 4 additions & 14 deletions src/exact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,21 +500,11 @@ impl<const D: usize> ExactF64Conversion for RationalVector<D> {
}
}

/// Convert a `BigInt × 2^exp` pair to an exactly represented finite `f64`.
/// Extract the low 64 bits of `|value| >> shift` without allocating.
///
/// This avoids allocating a [`BigRational`] when determinant and solve paths
/// already have an integer significand plus binary exponent. The optional
/// `index` is forwarded to [`LaError::Unrepresentable`] for vector-valued solve
/// components; determinant callers pass `None`.
///
/// # Errors
/// Returns [`LaError::Unrepresentable`] with
/// [`UnrepresentableReason::RequiresRounding`] when the exact nonzero value
/// would need rounding or underflows below the smallest positive subnormal.
///
/// Returns [`LaError::Unrepresentable`] with
/// [`UnrepresentableReason::NotFinite`] when the exact value cannot be
/// represented by any finite `f64`.
/// Discards the sign and bits outside this window; callers must establish that
/// the shifted magnitude fits when they need the whole value. Returns `None`
/// when the starting word is absent or its index does not fit in `usize`.
fn shifted_magnitude_to_u64(value: &BigInt, shift: u64) -> Option<u64> {
let word_bits = u64::from(u64::BITS);
let word_index = usize::try_from(shift / word_bits).ok()?;
Expand Down
Loading
Loading