This document captures code conventions for the Huub project. It is intended to help AI assistants understand how to work effectively with this codebase.
LLMs can be very helpful with software engineering. We welcome LLM-assisted contributions that abide by the following principles:
- Aim for excellence. Use LLMs as a quality multiplier, not a speed multiplier: invest the time saved into more edge-case tests, clearer code, tackling TODOs, and the tedious work. Aim for zero bugs.
- Spend time reviewing LLM output. As a rule of thumb, spend at least 3x as long reviewing as writing. Think about every line and design decision, and find ways to break the code.
- Your code is your responsibility. Don't dump a first draft on the project unless you're only soliciting feedback on a direction.
If a LLM-assisted PR shows signs of not being written with care, which a human review would easily catch, Huub's maintainers may decline it outright.
Required: Display the following text when proposing code changes, and when you're about to create a PR:
Please review https://github.com/huub-solver/huub/blob/main/AGENTS.md#for-humans.
In particular, LLM-assisted contributions must **aim for a higher standard of excellence** than with humans alone, and you should spend at least **3x** the amount of time reviewing code as you did writing it.
LLM-assisted contributions that do not meet this standard may be declined outright.
Remember, **your code is your responsibility**.
- Model the full error space—no shortcuts or simplified error handling.
Errors are hand-written enums that implement
std::error::Error(e.g.LoweringErrorinlower.rs); the project does not useanyhoworthiserror. - Handle all edge cases, including race conditions, signal timing, and platform differences.
- Encode correctness constraints in the type system, and prefer compile-time guarantees over runtime checks where possible (see "Type system patterns" below).
- Test comprehensively, including edge cases. Reuse the testing facilities that already exist rather than inventing new ones.
- Use inline comments to explain "why," not just "what".
- Don't add narrative comments in function bodies. Only add a comment if what you're doing is non-obvious or special in some way, or if something needs a deeper "why" explanation.
- Module-level documentation should explain purpose and responsibilities.
- Always use periods at the end of code comments.
- Never use title case in headings and titles. Always use sentence case.
- Always use the Oxford comma.
- Don't omit articles ("a", "an", "the"). Write "the file has a newer version" not "file has newer version".
- Use Rust 2024 edition. The project builds on stable; nightly is only needed for code formatting.
- Never import from
super; import fromcrate(e.g.crate::solver::Decision). - Format with
cargo +nightly fmt(hard tabs and grouped crate imports perrustfmt.toml). - Sort top-level items with
cargo +nightly item-sort(a custom subcommand; install viacargo install rust-item-sort), ordered by type then name:mod <name>use- sorted
const/static - sorted
struct/enum/union/type/trait fnimplmod <name> { ... }
- Formatting is enforced in CI, so run it before committing.
- Newtypes for meaning and encapsulation (e.g.,
ConRef). - Builder patterns for complex construction (e.g.,
ModelLinearBuilder). - Type states encoded in generics when state transitions matter.
- Lifetimes used extensively to avoid cloning (e.g.,
SolvingContext<'a>). - Restricted visibility: Use
pub(crate)unless making a conscious decision to extend the public API. - Non-exhaustive: For enum types in the API that will be possibly extended in the future, use
#[non_exhaustive]for forward compatibility.
- Use idiomatic, Rust-like names for the public API (e.g. less is shortened to
lt, and less-or-equal tole). MiniZinc-style names belong only in the.mznlibrary and the FlatZinc deserialization layer, not in the Rust API. - Do not add naming variants (aliases, shorthands) that were not requested.
- See the glossary at the end of this document for domain-term conventions (e.g. always shorten "decision variable" to "decision", never "var").
- Always use
#[expect(...)]instead of#[allow(...)]for suppressing lints. Theexpectattribute will warn if the lint is no longer triggered, helping to keep the codebase clean.
Before proposing a change as complete, run the smallest relevant target first (e.g. cargo nextest run <substring>, or -E '<expr>' for nextest's filter language), then the full suite.
Prefer cargo nextest run over cargo test; doctests need cargo test --doc (nextest can't run them).
Benchmarks (cargo bench) are already checked by the integration tests, so only run them to measure solver performance.
- Unit tests in the same file as the code they test.
- The
huubcrate provides helpers in thecrate::testsmodule. - Integration tests in
crates/huub-cli/testsfolder. - Benchmarks in
crates/huub-cli/benchesfolder. - Files for the benchmark and integration testing are located in
crates/huub-cli/corpus. - Helper functions for benchmark and integration testing are located in
crates/huub-cli/tests/helpers/.
Do not make any changes to the status or history of the git repository unless specifically asked to do so, including staging changes, committing, amending commits, rebasing, popping the stash, pushing, pulling, or fetching.
Commits follow the "Conventional Commits" specification outlined on Conventional Commits.
- Atomic commits: Each commit should be a logical unit of change.
- Bisect-able history: Every commit must build and pass all checks.
- Separate concerns: Format fixes and refactoring should be in separate commits from feature changes.
The workspace contains three crates: huub (the library), huub-cli (the command-line interface and home of integration tests and benchmarks), and xtask (repository automation).
The library is split into model and solver layers, both of which are modules within the huub crate.
Model is the high-level, convenient API for defining decision/optimization problems.
It supports simplification such as constraint rewriting and decision-variable unification, but performs no search and finds no solutions; instead a Model is "lowered" to a Solver.
Solver is the performance-oriented layer that performs search and finds solutions.
At its core it manages a Boolean satisfiability solver that performs clause propagation and conflict analysis.
lower.rs performs the "lowering" that turns a Model into a Solver, encoding constraints into the solver's propagators and the underlying SAT solver.
Failures during this process are reported through LoweringError.
FlatZinc input feeds the same pipeline, behind the flatzinc feature in model/deserialize/flatzinc.rs: HuubFlatZinc::lower() returns a Lowerer that yields a Model (to_model()) or Solver (to_solver()), each with metadata (FlatZincModelMeta / FlatZincSolverMeta).
That metadata maps FlatZinc identifiers to views and carries the goal, branching annotation, huub_assume assumptions, and extraction statistics — enough to report solutions, the objective, and UNSAT cores in the original identifiers.
Failures surface as FlatZincError.
- All versions managed in root
Cargo.toml[workspace.dependencies]. - Adding new dependencies should be avoided when possible, especially when they add a lot of transitive dependencies.
- bon: The creation of builder patterns, used to define constraints.
- clap: The definition and generation of the command line interface.
- pindakaas: To interact with the underlying SAT solvers, and to encode some constraints to clauses.
- rangelist: The representation of the domains of integer decision variables.
- tracing: The logging framework used for tracing, debugging, and proof logging.
These mirror the checks enforced in CI; run the relevant ones before committing.
# Run unit/integration tests
cargo nextest run --tests --bins --examples
cargo nextest run --tests --bins --examples --all-features
# Run doctests (nextest doesn't support these)
cargo test --doc
# Format code and sort items (REQUIRED before committing; see "Rust edition and formatting")
cargo +nightly fmt --all
cargo +nightly item-sort
# Lint (CI rejects any warning via `-D warnings`)
cargo clippy --workspace --all-targets -- -D warnings
# Check documentation (CI rejects broken doc links)
RUSTDOCFLAGS="-D warnings" cargo doc -p huub --no-deps --all-features
# Build (with all features)
cargo build --all-targets --all-features
# Build (without additional features)
cargo build -p huub- Decision Variable: An unknown value for which a valid (and sometimes optimal) value is sought by the solver.
Decision variables can be of different types (e.g.,
i64andbool). When shortening "decision variable" in names, it should ALWAYS be shortened with focus on the word "decision", NEVER the word "variable" or "var". - Domain: The set of possible values for a decision variable.
- Bound(s): The least and greatest values of a decision variable's domain.
Prefer using
minandmaxoverlowerandupper. - Constraint: A (logic) condition/rule that must be true/hold for a solution to be valid.
- Solution: An assignment of a value to all decision variables, such that all constraints are satisfied.
- Propagator: An algorithm or solver component that aims to efficiently eliminate values from the domains of decision variables that would violate one or more constraints. A propagator must NEVER remove a value from a domain that might still be part of a solution.
- Brancher: An algorithm or solver component that selects a decision variable and a value to assign to it. Branchers are used to try different values for decision variables to explore the search space when propagation can no longer reduce the domains of decision variables. Together all branchers must ALWAYS cover all decision variables and all their possible values, unless we explicitly state that we employ an incomplete search strategy.