Wheye guide

Version 0.1.21. Run a Prolog subset in the native CLI or browser, get answers, and inspect their independently checkable proofs. The specification defines the language and the C1–C9 contract.

Why a Prolog subset?

A restricted language can be a useful language in its own right. Datalog is a well-established example: restricting logic programs gives recursive queries a precise declarative meaning and makes specialized evaluation possible. Wheye follows that principle, choosing a Prolog subset for recursive reasoning with independently checkable evidence.

The implemented choices work together:

This scope keeps the relationship between a program, its answers, and its evidence small enough to specify and inspect. Features such as cut, mutable predicates, and procedural I/O would need additional semantics and evidence rules before they could fit that contract. A full Prolog system can support proofs too; wheye’s current C1–C9 contract covers the language specified here.

Wheye extends beyond classical function-free Datalog with structural terms, lists, arithmetic, and fresh witnesses. Term construction and arithmetic can create infinite computations, so the subset does not guarantee termination. Certificate acceptance establishes the recorded claims relative to the source assumptions; it does not establish answer completeness or external-world truth.

Run your first program

With Rust and Cargo installed:

cargo build --release --locked
./target/release/wheye examples/socrates.pl
cargo install --path . --locked

A minimal program selects Socrates as an answer:

human(socrates).
mortal(Person) :- human(Person).
?- mortal(Who).
?- why.
?- check.
?- check(mortal(socrates)).

Each request prints its ?- ... . line, followed by its result. An ordinary query prints answers; why prints its proof certificate; bare check verifies that certificate under C1–C9. check(Goal) prints true. or false. without changing which ordinary query why and check refer to. Goal checks never add facts. A false goal check is a result, not a run error.

The playground uses the same source requests. Opening a shared program does not run it automatically; press Run.

Unedited examples use short share links such as Socrates. The playground recognizes saved copies of bundled examples, including older versions, and loads their current source. Custom edits retain their exact text in saved state and full source share links.

Create Gist share uploads the program to an unlisted GitHub Gist and copies a share link. Enter a GitHub token with Gist write permission when prompted; it stays in the current tab’s session. Anyone with the link can read the Gist. Opening a Gist share restores the source without running it. The top status shows Done · exit 0 · … s or Failed · exit 1 · … s. The input and output display their complete contents. Use the page’s vertical scrollbar to move through both panes; neither pane scrolls independently.

Explore the examples

All programs are directly in examples/ and selectable in the playground. The collection includes lists, structural unification, recursive arithmetic, Fibonacci at 10,000, graph algorithms, finite optimization, text processing, scientific calculations, and classification chains. Source header comments explain each example’s assumptions and expected results. Golden transcripts in examples/output/ contain the current output of every program. The example suite compares them exactly and independently checks the certificates under C1–C9.

Example What it shows
Socrates Queries, certificates, goal checks
Lists Tabled list processing
Aggregation Counts, sums, minima, maxima
List collection Lists with repeated projected items
Inventory Arithmetic followed by aggregation
Airport paths Three routes across the full supplied airport graph
Customer vouchers All nine proof conditions, including witnesses
Bézout identity Exact coefficients from the extended Euclidean algorithm
Command grammar List parsing with structured results and unconsumed input
Drought relief Explicit eligibility, recorded statuses, and fresh payment identities
River crossing Safe state transitions and minimum-cost plans
Catalan numbers Tabled convolution with an explicit recursive sum
Pointer analysis A finite fixed point for possible memory targets
Consumable resources Resource lists prevent spending one token twice
Constructive logic Formula terms evaluated in a finite information-growth model

The airport example has 7,698 airports and 37,274 directed flight links. It finds three routes from Ostend to Prague with at most two intermediate stops. Change route_request/3 to ask for other endpoints or limits. The third answer field is the requested limit; route text lists the actual airports. Repeated airports are excluded, and an airport can have a zero-leg route to itself. The example includes goal checks for its supplied answers.

The packaged classification examples cover depths 10, 100, 1,000, 10,000, and 100,000. The first four include why requests and goal checks; the largest selects test(true) and is kept small enough to inspect or check without printing its entire closure. Use files for large proofs:

printf '?- why.\n?- check.\n' > /tmp/taxonomy-requests.pl
./target/release/wheye examples/deep-taxonomy-100000.pl /tmp/taxonomy-requests.pl > /tmp/taxonomy.txt

Tests bound counted dispatch/matching work and proof size, including a 100,000-level chain. node scripts/generate-taxonomy.mjs --check verifies the generated examples. ./scripts/bench-taxonomy measures pipeline phases; run it on a quiet machine without concurrent builds. Time and memory depend on the program, machine, and allocator.

Terms and arithmetic

Terms include distinct atoms, strings, exact integers, finite binary64 reals, compound terms, and proper lists. 1 and 1.0 are different terms, while 1 =:= 1.0 compares them numerically. Arithmetic expressions remain data until is/2, a numeric comparison, or a numeric aggregate evaluates them.

line(book, 12, 2).
total(Item, Amount) :- line(Item, Price, Quantity), Amount is Price * Quantity.
?- total(Item, Amount), Amount >= 20.
?- Result is 2^100.
?- point(X, [a|Rest]) = point(2, [a,b]).

Unification uses an occurs check. Each _ is fresh; other names beginning with _ remain shared but are omitted from displayed bindings. Arithmetic inputs must already be bound by preceding goals or a call. Invalid inputs, division by zero, and non-finite results are errors. Integer arithmetic does not wrap. See the complete builtin tables.

Collect completed answers

purchase(tools, hammer, 5).
purchase(tools, saw, 5).
purchase(tools, saw, 5).
budget(Department, Count, Total) :-
    department(Department),
    aggregate_all(count, purchase(Department, Item, Price), Count),
    aggregate_all(sum(Price), purchase(Department, Item, Price), Total).
department(tools).
?- budget(Department, Count, Total).

The result is two items and a total of 10. Distinct producer tuples contribute once; equal prices from different items contribute separately. Repeating the same fact or proof path adds no contribution. aggregate_all(list(Term), Goal, Items) retains equal projected elements from distinct producer tuples. Its producer can be a parenthesized conjunction. Producer variables are local; only the result binds outside, and earlier bindings constrain the producer. Empty count and sum return zero; empty list collection returns []; empty minimum and maximum fail.

Negation, tabling, and witnesses

eligible(Person) :- registered(Person), \+ blocked(Person).
:- table fibonacci/2.
fibonacci(0, 0).
fibonacci(1, 1).
fibonacci(N, Value) :-
    N > 1, A is N-1, B is N-2,
    fibonacci(A, X), fibonacci(B, Y), Value is X+Y.
?- fibonacci(100, Value).

Negation requires a ground ordinary call and waits for its compatible producers to complete. Negative and aggregate cycles are rejected. Clauses that need caller inputs use demand-driven tables; a table declaration makes all clauses of that predicate demand-driven. Variant calls share work and recursive consumers resume as answers arrive. Tabling does not guarantee termination for arbitrary programs. Limits and the playground’s Stop button bound a run.

fresh(Witness) is an additional explicit predicate with ordinary Prolog call syntax. It can only end a forward rule and must bind a new output variable. It creates an opaque typed witness for that source-rule firing. Source facts cannot supply witness identities. The certificate checker verifies C8 for the voucher example:

./target/release/wheye examples/orders.pl

Keep and independently check evidence

Add ?- why. after a query to print its certificate, and ?- check. to verify that certificate. The orders example includes both:

./target/release/wheye examples/orders.pl > /tmp/orders.txt

There is one proof format, starting with wheye-proof 1. It lists supporting source facts, rule applications, pure decisions, required closed boundaries and demanded-call patterns, and claims for the preceding ordinary query. % Rule comments show the source rules used; verification uses the supplied source program. To save a standalone certificate, retain the section starting at wheye-proof 1 up to the next request. The Rust API accepts saved evidence with Certificate::parse and check; goal-check requests do not become claims. The checker validates the supplied evidence; it does not call the evaluator, activate tables, or repair missing facts. C5 recomputes builtins and aggregates. C6 requires complete finite models and transitively complete demanded calls; omitting a derivable counterexample cannot establish absence or a smaller aggregate. C8 checks witnesses, and C9 keeps queries and goal checks out of inference evidence.

Every report lists C1–C9. Conditions for unused features are not applicable. The voucher example performs nonzero successful checks under all nine. Source facts and rules remain assumptions. A valid certificate establishes support for recorded claims; it does not authenticate facts, establish external-world truth, or prove that every answer was selected. An empty certificate establishes no positive claim.

Use the command line

wheye FILE...
wheye -
wheye rules.pl data.pl
wheye -h
wheye -v

Only -h (help) and -v (version) are options. - reads source from stdin; multiple files form one program in the supplied order. Source requests select the output, with each request echoed before its result. Use ?- why. for proofs and ?- check. for verification. ?- check(Goal). prints a boolean result. Only :- table Name/Arity. directives are supported.

Success returns status 0. Parse errors, incomplete runs, missing preceding queries for why or bare check, and invalid proof reports return status 1 with no successful partial stdout. Source fact, rule, and query IDs are separate ordered lists; keep source file order when checking saved evidence.

Embed the engine

use wheye::{check, parse, reason, Limits};
fn main() -> Result<(), wheye::Error> {
    let program = parse("human(socrates). mortal(X):-human(X). ?- mortal(Who).")?;
    let run = reason(&program, Limits::default())?;
    print!("{}", run.output(&program)?);
    let report = check(&program, &run.certificate(&program))?;
    report.require_valid()?;
    Ok(())
}

Run exposes stored evidence, query answers, counted work, and closed boundaries. Certificate::parse reads saved evidence. execute offers the same string interface as the browser: execute(source, Limits::default()). Both check and check_with_limits return Ok(report) when semantic checking finishes, even for an invalid proof. Inspect report.valid() or call report.require_valid()? before accepting it. report.conditions lists the nine conditions; report.failures gives specific diagnostics. report.condition(wheye::Condition::C9) selects one. Malformed input and resource exhaustion return errors.

Implementation

parse builds the source model. reason validates its modes and dependencies, then evaluates forward rules and demand-driven tables. Run::output follows the source requests.

The checker validates evidence steps, witness origins, claims, closed boundaries, and relevance. It consumes the supplied certificate and returns condition counts and failures; it never invokes reason or fills a missing boundary.

File Responsibility
src/model.rs Structural values, facts, clauses, and source requests
src/parser.rs The .pl program syntax
src/language.rs Safety, built-in modes, strata, and recursion restrictions
src/semantics.rs Unification, built-ins, aggregates, and finite-model matching
src/arithmetic.rs Exact integer operations and finite real arithmetic
src/engine.rs Indexed forward inference, demand-driven tables, and execution limits
src/output.rs Answers, certificates, and reports in source request order
src/proof.rs Certificate selection and serialization; independent C1–C9 verification
src/main.rs Native command line and file I/O
src/wasm.rs Browser bindings
playground*.js UI and cancellable worker
scripts/prepare-pages.mjs, scripts/pages-layout.html Pages staging and documentation layout
.github/workflows/pages.yml Website build and deployment
release, scripts/sync-version.mjs Version bump, browser rebuild, checks, and publication

Develop and verify

Rust, Cargo, Node.js 22 or later, wasm-pack, and the wasm32-unknown-unknown Rust target are required for the complete checks.

./scripts/test-all
cargo clippy --target wasm32-unknown-unknown --lib --locked -- -D warnings
./scripts/build-playground
git diff --check

scripts/test-all describes each check, times every native test and bundled example, and prints overall timing at the end. Terminal output uses cyan headings, green passes, and red failures; redirected logs stay plain. Set NO_COLOR=1 to disable colors or FORCE_COLOR=1 to retain them in captured output. The script continues after failures and returns a nonzero status if any check fails. Every example must have exactly one golden in examples/output/; the suite also checks its saved certificate and its expected goal checks. Graph oracles, forged-certificate regressions, worker, Pages, Gist sharing, and release recovery tests remain covered. Browser and Wasm smoke tests use small examples; the 100,000-level taxonomy and full airport graph run natively to avoid exhausting browser memory.

After an intentional output change, regenerate and review the goldens:

WHEYE_UPDATE_GOLDENS=1 cargo test --release --locked --test examples -- --nocapture
python3 -m http.server 8766 --bind 127.0.0.1
google-chrome --headless --remote-debugging-port=9226 --user-data-dir=/tmp/wheye-browser about:blank
node tests/browser.mjs

For layout-only checks, add --layout-only. This uses a 200-line input and 240-line output at desktop and phone widths, with reasoning disabled.

The browser engine runs in a worker. Programs remain local unless shared in a URL. The committed pkg/ must be rebuilt after Rust changes. Use the same source version for CLI, documentation, playground, and Wasm.

Make a release

From a reviewed checkout on main:

./release                         # patch: 0.1.0 → 0.1.1
./release minor                   # minor: 0.1.0 → 0.2.0
./release major                   # major: 0.1.0 → 1.0.0
./release patch "Improve proof diagnostics"

The first argument defaults to patch. The optional second argument becomes the commit message, annotated tag message, and GitHub release notes; it defaults to Release v<version>.

Install cargo-edit, wasm-pack, and the GitHub CLI, along with Rust, the wasm32-unknown-unknown target, and Node.js 22 or later. Sign in once with gh auth login. The script needs permission to push main and tags and create releases in the origin repository.

The script switches to main, pulls with --ff-only, bumps Cargo.toml with cargo set-version, and synchronizes the README, guide, specification, and playground version labels. It rebuilds WebAssembly, runs ./scripts/test-all and Wasm clippy, then commits all working-tree changes, creates an annotated v<version> tag, pushes the branch and tag together, and creates the GitHub release. Review git status and git diff first. The push also starts CI and the Pages deployment.

A failed build or check stops before committing or publishing, leaving the local version changes available for inspection. Resolve the failure and finish that version rather than bumping again. If a commit and tag were created but publication stopped, retry only the unfinished steps, substituting the actual tag:

git push --atomic origin main v0.1.1
git tag --list v0.1.1 --format='%(contents)' |
  gh release create v0.1.1 --title=v0.1.1 --notes-file -

Skip the push if it already succeeded, and create the GitHub release only if it is still missing. The release contains GitHub’s source archives. A separate tag workflow is not needed.

Publish the website

The Deploy GitHub Pages workflow builds the site on pushes to main and can also be started manually. Set the repository’s Pages source to GitHub Actions. The workflow checks the website and committed WebAssembly package, renders the guide and specification with Jekyll, and deploys the resulting static artifact.

Open the published guide, specification, or playground. Documentation links in this repository point to those rendered pages. The playground.html route remains available too.

GUIDE.md supplies the guide at the site root; SPEC.md supplies the specification. node scripts/prepare-pages.mjs prepares _pages/ for the Jekyll build; _site/ holds the rendered artifact. Both directories are ignored by Git. The Pages action supplies PAGES_BASE_PATH, so local links and assets work beneath /wheye/ or a different configured prefix. The site uses the committed pkg/; changing the package version requires rebuilding it before publishing. The deployment check rejects a browser package whose exported version differs from Cargo.toml.

MIT licensed.