rules_tla
rules_tla — hermetic Bazel rules for TLA+ model checking (TLC via a pinned tla2tools.jar); tla_library + tla_check as bazel test targets
| Latest | 0.2.0 |
|---|---|
| Versions | 3 |
| Category | Bazel rules |
| Compat level | 1 |
| Maintainers | Matt Marshall |
| Registry | https://registry.tbzl.dev/modules/rules_tla/ |
| Source | github.com/tomato-bazel/rules_tla |
bazel_dep(name = "rules_tla", version = "0.2.0")
View source & releases on GitHub ↗
Hermetic Bazel rules for TLA+ model checking. Specs become first-class
bazel test targets: bazel test //... runs TLC and fails on any invariant,
deadlock, or temporal-property violation — the same way rules_lean makes Lean
proofs Bazel targets.
The TLA+ tools jar (tla2tools.jar — SANY + TLC + PlusCal) is pinned by
version + sha256 and fetched via http_file, so checks are reproducible. TLC runs
on the JDK that Bazel’s built-in java runtime toolchain resolves.
Usage
MODULE.bazel:
bazel_dep(name = "rules_tla", version = "0.2.0")
BUILD.bazel:
load("@rules_tla//tla:defs.bzl", "tla_library", "tla_check")
# Optional: group modules that other specs EXTEND.
tla_library(
name = "lib",
srcs = ["Helpers.tla"],
)
tla_check(
name = "merge_queue_check",
module = "MergeQueue.tla",
config = "MergeQueue.cfg",
deps = [":lib"],
)
Then:
bazel test //path/to:merge_queue_check
A tla_check passes iff TLC reaches “Model checking completed” and reports no
violation. A safety (INVARIANT), deadlock, or temporal (PROPERTY) violation
fails the test, and TLC’s counterexample trace is printed in full.
Rules
tla_library(name, srcs, deps)— a group of.tlamodules plus transitivetla_librarydeps. ProvidesTlaInfo(transitive sources) and its files viaDefaultInfo. Directories of all transitive sources are placed on TLC’s module search path (TLA-Library), so specs mayEXTENDSmodules from deps.tla_check(name, module, config, deps, expect, tlc_args, timeout_seconds)— model-checkmodule(a.tla) againstconfig(a.cfg) with TLC, as abazel test. Implemented as a build action (running TLC) gated by abuild_test, so it needs no runfiles wiring.
Asserting that a design does break
expect names the TLC outcome that makes the target pass: ok (the
default), invariant_violation, deadlock, or temporal_violation. Any other
outcome fails, including a violation of a different kind than the one named.
tla_check(
name = "positive_cycle_never_settles",
module = "MCCycle.tla",
config = "MCCycle.cfg",
expect = "temporal_violation",
)
This exists because a counterexample is often the result you want to keep. A
comment saying “this configuration does not terminate” decays; a target that
goes red the day it starts terminating does not. It is also the only way a green
suite can demonstrate that it is capable of failing — see examples/failure/.
Bounding the run
tlc_args passes flags straight through to tlc2.TLC (-workers,
-difftrace, -coverage, -simulate, …).
timeout_seconds kills TLC after n seconds. ⚠ A check runs in a build
action, and Bazel does not time actions out. size and timeout on the
wrapping build_test govern the trivial test, not the model check, so a spec
with an infinite state space hangs the build rather than failing it. If a spec
can diverge, either give it a finite abstraction or set timeout_seconds.
Scope
- Engine: TLC (explicit-state). Bounded checking at finite
CONSTANTSis the first-line tool. A TLC result is only ever a statement about the state space it enumerated; it does not license an unbounded claim. - Deferred: Apalache (symbolic/SMT) and TLAPS (machine-checked proofs) as
alternate engines; PlusCal translation (
pcal) as apluscal_translaterule; and a separaterules_pfor the P language.
Hermeticity
- Hermetic: the
tla2tools.jar(version + sha256 pinned intla/extensions.bzl), fetched from an immutable GitHub release asset — not from/archive/refs/tags/, whose bytes GitHub does not guarantee. - Toolchain-resolved: the JDK (Bazel’s
@bazel_tools//tools/jdkruntime toolchain). Pinning a specific hermetic JDK is a follow-up.
Two traps this ruleset has already hit
Both are in tla/private/tla_check.bzl in full, with the diagnostics they
produce, because both look like the user’s fault and neither is.
-
The spec argument must be a bare module name.
tlc2.TLC.mainbuilds its file resolver from the directory component of the spec path, and that code path never reads theTLA-Librarysystem property. Any path with a directory in it — which is every path under Bazel — therefore silently discards-DTLA-Library, anddepsmodules become invisible with the messageCannot find source file for module X, which reads as a missingdepsentry. This is whydepsdid not work in 0.1.0 or 0.1.1: the only example in the repo had no deps, so nothing exercised it. -
TLC needs a private
java.io.tmpdir. It extracts the standard modules (Naturals,Sequences, …) to fixed names underjava.io.tmpdirand marks themdeleteOnExit, so concurrent checks delete each other’s copies and the loser fails withsource file 'Sequences.tla' has apparently been deleted— which looks like a broken spec, not a race.
Usage#
Real usage, taken from the module’s examples/.
examples/BUILD.bazel
load("@rules_tla//tla:defs.bzl", "tla_check")
# End-to-end smoke: fetches the pinned tla2tools.jar and model-checks a finite
# spec (safety invariant + liveness property). `bazel test //examples:counter_check`.
tla_check(
name = "counter_check",
config = "Counter.cfg",
module = "Counter.tla",
)
# The same, but EXTENDS a module supplied through `deps` from another package.
# This is the regression test for the TLA-Library bug — see tla/private/tla_check.bzl.
tla_check(
name = "sensor_check",
config = "Sensor.cfg",
module = "Sensor.tla",
deps = ["//examples/lib:bounded"],
)
examples/failure/BUILD.bazel
load("@rules_tla//tla:defs.bzl", "tla_check")
# THE FAILURE PATH, ASSERTED RATHER THAN ASSUMED.
#
# Each of these specs genuinely breaks, and each target passes only when TLC
# reports THAT kind of break. Between them they cover every outcome the README
# claims `tla_check` catches. A rule that stopped noticing violations would
# turn all three red, which is the property `expect = "ok"` alone cannot buy:
# a green suite made of green specs cannot tell you whether it can fail.
tla_check(
name = "violates_invariant_check",
config = "ViolatesInvariant.cfg",
expect = "invariant_violation",
module = "ViolatesInvariant.tla",
)
tla_check(
name = "deadlocks_check",
config = "Deadlocks.cfg",
expect = "deadlock",
module = "Deadlocks.tla",
)
tla_check(
name = "never_settles_check",
config = "NeverSettles.cfg",
expect = "temporal_violation",
module = "NeverSettles.tla",
)
# The one probe that cannot live in a green suite: a violated invariant under
# the DEFAULT expectation, which must turn the build red. CI builds it and
# asserts a non-zero exit; see .github/workflows/ci.yml. Tagged manual so
# `bazel test //...` does not pick it up.
tla_check(
name = "must_fail_probe",
config = "ViolatesInvariant.cfg",
module = "ViolatesInvariant.tla",
tags = ["manual"],
)
examples/lib/BUILD.bazel
load("@rules_tla//tla:defs.bzl", "tla_library")
package(default_visibility = ["//visibility:public"])
tla_library(
name = "bounded",
srcs = ["Bounded.tla"],
)Conformance#
No gate findings. 8 contested atoms. See how gating works or the full report.
Contested atoms
Third-party modules where this module resolves a different version than others do. Not a violation of anything this module did — it is the actionable form of a registry-level convergence finding, and the sentence a maintainer can act on.
| Atom | Resolved here | Elsewhere |
|---|---|---|
apple_support | 1.24.2 | 2.2.0 ×1 |
bazel_skylib | 1.8.2 | 1.9.0 ×2 |
nlohmann_json | 3.6.1 | 3.12.0.bcr.1 ×1 |
protobuf | 33.4 | 34.0.bcr.1 ×2 |
rules_jvm_external | 6.7 | 6.8 ×4 |
rules_python | 1.7.0 | 2.0.1 ×1 |
rules_swift | 3.1.2 | 3.6.1 ×1 |
upb | 0.0.0-20220923-a547704 | 0.0.0-20230516-61a97ef ×1 |
Dependencies#
Depends on
Used by (1 in the registry)
Versions#
3 published versions, newest first. Each resolves to an immutable, integrity-checked archive.
| Version | Integrity (sha256) | Source archive |
|---|---|---|
0.2.0 latest | xBnk4I64uzeb2n93… | tag archive ↗ |
0.1.1 | Nu12UBrtsHzInTq4… | tag archive ↗ |
0.1.0 | uyKQNBBbu3r3wq1d… | tag archive ↗ |
Changelog#
0.2.0
- FIX:
depsnever worked.tla_checkpassed the spec to TLC aspkg/Module.tla, andtlc2.TLC.mainbuilds its resolver from the directory component of that path — a code path that never reads theTLA-Librarysystem property. So-DTLA-Librarywas silently discarded on every invocation Bazel can produce, and any module reached throughdepsfailed to resolve withCannot find source file for module X, which reads as a missingdepsentry rather than as the rule ignoring the one it was given. The action nowcds to the module’s directory and passes a bare module name.examples:sensor_checkis the regression test — it EXTENDS a module from another package, and it is the first thing in this repo ever to exercisedeps. - New:
expect. Names the TLC outcome that makes a target pass —ok(default),invariant_violation,deadlock,temporal_violation. A counterexample you meant to keep can now be a checked property instead of a comment. - New:
tlc_args— flags passed through totlc2.TLC. - New:
timeout_seconds— kills TLC after n seconds. A check runs in a build action and Bazel does not time actions out, so an infinite state space previously hung the build rather than failing it. - Fail closed. A run that reports no error but never reaches “Model checking
completed” (mistyped
-config, empty cfg, usage dump) is now a failure. It used to pass. - CI. There was none.
bazel test //...now runs on ubuntu and macos, plus a step that builds a genuinely-violated spec under the default expectation and demands a non-zero exit, and a no-cache concurrent re-run that would catch a regression of thejava.io.tmpdirrace.
0.1.1
tla_checkgives TLC a privatejava.io.tmpdir. TLC extracts the standard modules to fixed names there and marks themdeleteOnExit, so concurrent checks deleted each other’s copies and the loser failed withsource file 'Sequences.tla' has apparently been deleted— which looks like a broken spec rather than a race.
0.1.0
- Initial release.
tla_library+tla_checkrunning TLC from a pinned, hermetictla2tools.jar(v1.7.4). Checks run as build actions gated bybuild_test, sobazel test //...model-checks specs with no runfiles wiring. - Transitive
tla_librarysource directories are placed on TLC’sTLA-Librarysearch path.