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
9 changes: 9 additions & 0 deletions .github/workflows/actions.lock
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ workflows:
- 'actions/checkout@v7.0.1'
'.github/workflows/workflow-linter.yml':
- 'actions/checkout@v7.0.1'
'.github/workflows/zig-ffi-ci.yml':
- 'actions/checkout@v7.0.1'
- 'hyperpolymath/cicd-suite@main'
- 'mlugg/setup-zig@v2.2.1'
dependencies:
'actions/attest-build-provenance@v4.2.2':
ref: 'v4.2.2'
Expand Down Expand Up @@ -172,6 +176,11 @@ dependencies:
commit: 'sha1-0f8e8c99d88aeb3fbfd523f1ef2c6f762d10d64d'
owner_id: 75048950
repo_id: 623796603
'hyperpolymath/cicd-suite@main':
ref: 'main'
commit: 'sha1-d9ebcf99ad4593611b4fa0e18f09b03801f200e8'
owner_id: 6759885
repo_id: 1326697643
'mlugg/setup-zig@v2.2.1':
ref: 'v2.2.1'
commit: 'sha1-d1434d08867e3ee9daa34448df10607b98908d29'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/chapel-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ jobs:
version: 0.14.0

- name: Build Zig FFI library (with Chapel stubs)
run: cd src/zig_ffi && zig build -Doptimize=ReleaseSafe
run: cd src/zig_ffi && zig build -Doptimize=ReleaseSafe -Dcpu=baseline # baseline: the artifact runs on another runner (SIGILL otherwise)

- name: Run Zig FFI tests (stub mode)
run: cd src/zig_ffi && zig build test
Expand Down Expand Up @@ -219,7 +219,7 @@ jobs:
- name: Build Zig FFI without stubs (links against real Chapel)
run: |
cd src/zig_ffi
zig build -Doptimize=ReleaseSafe -Dstubs=false
zig build -Doptimize=ReleaseSafe -Dcpu=baseline -Dstubs=false

- name: Build Rust with real Chapel library
run: |
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,16 @@ jobs:
# first push it happens, instead of silently re-resolving (which
# masked the echidna#92 / echidna PR #128 dependabot major-bump
# break for 24h). See standards#295.
run: cargo check --locked --all-targets
run: cargo check --locked --workspace --all-targets

- name: Cargo fmt
run: cargo fmt --all -- --check

- name: Cargo clippy
run: cargo clippy --locked --all-targets -- -D warnings
run: cargo clippy --locked --workspace --all-targets -- -D warnings

test:
timeout-minutes: 20
timeout-minutes: 30
name: rust-ci / Cargo test
runs-on: ubuntu-latest
needs: [detect, check]
Expand Down Expand Up @@ -149,7 +149,9 @@ jobs:

- name: Run tests
# `--locked` — see Cargo check above.
run: cargo test --locked --all-targets
# `--workspace`: without it only the root package is tested, so the
# echidna-core (prove-result contract) and trust-kernel tests never ran.
run: cargo test --locked --workspace --all-targets

- name: Write summary
if: always()
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/zig-ffi-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# This workflow is managed by gh actions-lock.
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# Zig FFI CI for echidna (ffi/zig): format, unit tests, and the estate
# unified-api-adapter purity check (no std.fs / std.io in any .zig file).
#
# Before 2026-10-05 ffi/zig was built by no workflow and did not compile on
# any Zig release (opaque Handle with fields, implicit libc). This job keeps
# it compiling and tested. The library targets that link against the Rust
# libechidna.so (integration tests, benchmarks) are not built here.
#
# Zig is pinned to 0.15.2, the version ffi/zig/build.zig is written for.
name: Zig FFI CI

on:
push:
branches: [main]
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
zig-ffi:
name: zig-ffi / fmt + test (ffi/zig)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
with:
persist-credentials: false

- name: Install Zig
uses: mlugg/setup-zig@v2.2.1
with:
version: 0.15.2

- name: zig fmt --check
run: zig fmt --check ffi/zig/src ffi/zig/test ffi/zig/build.zig

- name: zig build test
working-directory: ffi/zig
run: zig build test --summary all

unified-api-adapter:
name: zig-ffi / unified-api-adapter purity
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
with:
persist-credentials: false

- name: Zig unified-api-adapter check (hyperpolymath/cicd-suite)
uses: hyperpolymath/cicd-suite/actions/zig-unified-api-adapter-check@main
33 changes: 33 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,39 @@ https://semver.org/spec/v2.0.0.html[Semantic Versioning].

=== [Unreleased]

==== Added — integrations 2026-10-05

* *`+echidna prove --output json+`*: prints exactly one
`+echidna.prove.result/1+` object (JCS-canonical I-JSON) on stdout, logs
to stderr. Statuses `+verified+` / `+failed+` / `+error+` / `+timeout+` /
`+unknown+`, mapped from `+ProverOutcome+`; `+trust.confidence+` is
`+null+` unless receipt-backed. Human output stays the default. See
`+docs/PROVE-RESULT-CONTRACT.adoc+`.
* *`+echidna-core+` 0.2.0* is consumable by clients (echidnabot,
proof-burrower): adds `+ProverKind+` (moved from `+echidna::provers+`,
re-exported there, no source change for ECHIDNA), the
`+echidna.prove.result/1+` type with a strict parser, and the trust
kernel as `+echidna_core::trust+`. Git-dependency recipe and stability
policy in `+crates/echidna-core/README.adoc+`.
* *`+ffi/zig+` builds and is tested in CI* (`+zig-ffi-ci.yml+`, Zig
0.15.2): it previously compiled on no Zig release. Pure Zig (page
allocator, no libc); the estate unified-api-adapter purity check
(`+hyperpolymath/cicd-suite+`) runs on every PR.

==== Fixed — integrations 2026-10-05

* ci(chapel): build the Zig FFI with `+-Dcpu=baseline+`; the artifact
runs on a different runner, and native-CPU code crashed there with
SIGILL in "Rust Build with Chapel Feature".
* ci(rust): check, clippy and test now run with `+--workspace+`; before,
only the root package was tested, so `+echidna-core+` and the trust
kernel tests never ran in CI.
* abi: `+EchidnaABI.Gnn+` and `+EchidnaABI.TacticRecord+` now declare
`+%default total+` (both already type-check total).
* docs(creusot): `+CREUSOT-SETUP.adoc+` no longer claims the Creusot job
is a hard gate or that milestones 8c-M1..M3 are done; it records why
Creusot does not run in CI.

==== Changed

* *Licence: `+AGPL-3.0-or-later+` → `+MPL-2.0+` for code and the
Expand Down
6 changes: 5 additions & 1 deletion Cargo.lock

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

76 changes: 42 additions & 34 deletions crates/echidna-core-creusot/CREUSOT-SETUP.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,40 @@ contract.
All obligations in `+impl_invariants+` are designed to discharge quickly
(< 30 s each on a modern workstation).

=== 6 — CI integration

`+.github/workflows/formal-verification.yml+` runs two jobs, both merge
gates:

* `+stable-tests+`: `+cargo test -p echidna-core-creusot+` on stable Rust
(~10 s).
* `+creusot-verify+`: `+cargo +nightly creusot+` with Why3 + Z3
discharge. Requires `+apt-get install why3 z3 alt-ergo+` in the runner
(Ubuntu 22.04+).

Both were promoted to hard gates in Stage 8c-M3 (no
`+continue-on-error+`).
=== 6 — CI integration (what actually runs)

`+.github/workflows/formal-verification.yml+` has two jobs:

* `+stable-tests+`: `+cargo test -p echidna-core-creusot+` on stable Rust.
It runs on pull requests that touch this crate, and the crate's tests
also run in the workspace test job of `+rust-ci.yml+`. It checks the
stable-Rust mirror of each stated obligation. It does not run Creusot.
* `+creusot-verify+`: manual only (`+workflow_dispatch+`). It has never
discharged an obligation.

==== Why Creusot does not run in CI (measured 2026-10-05)

. *The annotations do not compile under Creusot's macros.*
`+cargo check -p echidna-core-creusot --features creusot+` fails with
34 errors: `+creusot_contracts+` is unresolved (it is not a
dependency), so `+ensures+`, `+requires+`, `+invariant+`, `+pure+` and
`+snapshot!+` are unknown. No obligation has ever reached Why3.
. *The toolchain pin is stale.* `+rust-toolchain.toml+` pins
`+nightly-2024-05-01+`, which predates current Creusot releases.
. *The install path in this guide is wrong for current Creusot.* Creusot
is installed with its own `+INSTALL+` script, which provisions Why3 and
`+why3find+` through opam; `+cargo install creusot+` does not do that.
. *Adding `+creusot-contracts+` as an optional dependency would put a
nightly-only crate in `+Cargo.lock+`*, which the stable `+--locked+`
build and `+cargo audit+` also read.

What it would take: pick one Creusot release, pin its nightly here and in
the workflow, add `+creusot-contracts+` behind the `+creusot+` feature,
rewrite the annotations against that release's logic syntax until
`+cargo creusot+` type-checks them, install Creusot with its `+INSTALL+`
script in the job, and only then make `+creusot-verify+` run on pull
requests. Until a dispatch run discharges every obligation, call the
annotations *stated, not proved*.

=== Annotation style

Expand All @@ -124,36 +145,23 @@ no-ops on stable Rust.
|===
|Milestone |Contents |Status
|8c-M1 |`+rust-toolchain.toml+`, `+formal-verification.yml+`,
`+just verify-trust-pipeline+` |*done*
`+just verify-trust-pipeline+` |files exist; toolchain pin stale

|8c-M2 |`+dominates+` marked `+#[pure]+`; `+compute+` ensures with
`+^candidates+`; inner-loop `+#[invariant]+` for `+dominated+` |*done*
`+^candidates+`; inner-loop `+#[invariant]+` for `+dominated+`
|annotations written, never type-checked by Creusot

|8c-M3 |Outer-loop invariant (`+snapshot!+` + prefix classification);
Why3 CI hard gate |*done*
Why3 CI hard gate |annotations written; *no CI gate* (see above)
|===

All three milestones are complete. The `+compute+` function now carries
the full two-level invariant structure:

* *Outer*: `+snapshot!(candidates)+` at entry + `+#[invariant]+`
asserting
[loweralpha]
. objectives unchanged for all k, (b) `+is_pareto_optimal[k]+` correctly
set for k < i.
* *Inner*: `+dominated == (∃ k < j, k ≠ i : dominates(k, i))+`.

The CI workflow (`+formal-verification.yml+`) is a hard gate: both
`+stable-tests+` and `+creusot-verify+` must pass for a merge. If the
nightly pin drifts, bump `+rust-toolchain.toml+` + the workflow’s
`+toolchain:+` field together.

This keeps the crate buildable on stable Rust at all times while still
expressing every proof obligation in machine-verifiable syntax.
An earlier version of this table marked all three milestones done and
described both jobs as hard merge gates. That was not true: no obligation
has been discharged (see "Why Creusot does not run in CI").

=== References

* Creusot repository: https://github.com/creusot-rs/creusot
* Why3 documentation: https://www.why3.org/
* SPARK Adoption Plan: `+docs/design/SPARK_ADOPTION_PLAN.adoc+`
* SPARK Adoption Plan: `+docs/design/SPARK_ADOPTION_PLAN.adoc+` (historical; this crate is Creusot, not SPARK)
* Echidna ROADMAP Stage 8c: `+docs/ROADMAP.adoc+`
14 changes: 12 additions & 2 deletions crates/echidna-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@

[package]
name = "echidna-core"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
authors = ["Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>"]
license = "MPL-2.0"
description = "Canonical Term / Goal / ProofState / Tactic types for ECHIDNA, re-used by vcl-ut and any future proof-exchange client without depending on the full echidna binary."
description = "Canonical ECHIDNA client types: Term / Goal / ProofState / Tactic, ProverKind, the echidna.prove.result/1 contract and the trust kernel, without depending on the full echidna binary."
repository = "https://github.com/hyperpolymath/echidna"

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# ProverKind::from_str keeps its historical anyhow::Error error type.
anyhow = "1"
# ProverKind derives strum::EnumIter.
strum = { version = "0.28.0", features = ["derive"] }
# JCS (RFC 8785) canonical JSON for the echidna.prove.result/1 contract.
serde_json_canonicalizer = "0.3"
# Trust kernel (TrustLevel, compute_trust_level, axiom scanner), re-exported
# as echidna_core::trust. Path dependency inside this repository; a git
# dependency on echidna-core resolves it from the same commit.
echidna-core-creusot = { path = "../echidna-core-creusot", version = "0.1.0" }
59 changes: 59 additions & 0 deletions crates/echidna-core/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
= echidna-core

The types a client of ECHIDNA needs, without the full `echidna` crate:

[cols="1,3",options="header"]
|===
| Module | Contents

| `core`, `types` | `Term`, `Goal`, `ProofState`, `Tactic`, `TypeInfo`.
| `prover_kind` | `ProverKind`, the backend enumeration, with `FromStr` and `Display`.
| `prove_result` | `ProveResult`, the `echidna.prove.result/1` contract printed by `echidna prove --output json` (see `docs/PROVE-RESULT-CONTRACT.adoc`), with a JCS serialiser and a strict parser.
| `trust` | The trust kernel re-exported from `echidna-core-creusot`: `TrustLevel`, `TrustFactors`, `ProverClass`, `compute_trust_level`, `combine_trust`, `axiom_tracker`, `pareto`.
|===

`echidna` re-exports these, so `echidna::ProverKind` and
`echidna::provers::ProverKind` are the same type as
`echidna_core::ProverKind`.

== Using it from another repository

The crate is not published to crates.io. Depend on it from git, pinned to a
full commit SHA so the build is reproducible:

[source,toml]
----
[dependencies]
echidna-core = { git = "https://github.com/hyperpolymath/echidna", rev = "<full 40-character commit SHA on main>" }
----

Cargo finds the crate inside the repository by name. Its path dependency on
`echidna-core-creusot` resolves from the same commit, so nothing else is
needed. Do not use a `path = "../echidna/..."` dependency in a published
repository: it only builds on a machine with that checkout.

Dependencies it brings: `serde`, `serde_json`, `anyhow`, `strum`,
`serde_json_canonicalizer`, and `echidna-core-creusot` (which needs only
`serde`).

== Stability

The crate follows Semantic Versioning, read the Cargo way for 0.x: a change
in the minor number (0.2 to 0.3) may break, a change in the patch number
does not.

* Stable from 0.2.0: the module paths above, the `ProverKind` variant
names (they are its serde form), the `ProveResult` fields and the
`echidna.prove.result/1` wire format, and the `trust` items listed above.
* Adding a `ProverKind` variant is not breaking for ECHIDNA's purposes; keep
a wildcard arm when you match on it.
* The trust kernel's Creusot annotations are stated, not proved (see
`crates/echidna-core-creusot/CREUSOT-SETUP.adoc`). Its behaviour is
covered by stable-Rust tests.

== History

* 0.2.0 (2026-10-05): added `prover_kind`, `prove_result` and `trust`.
`ProverKind` moved here from `echidna::provers`; nothing was removed.
* 0.1.0: `core` and `types`.
Loading
Loading