Alloy Models
Alloy is a lightweight formal modeling language. Its solver exhaustively explores every combination of objects within a bounded scope, finding counterexamples that example-based testing misses.
Specdown embeds Alloy models directly in spec files so that prose, implementation checks, and formal proofs live side by side. Executable blocks test selected examples; Alloy proves properties for all cases within scope.
Alloy fragments are embedded using alloy:model(name)
code blocksdepends and verified through the
Alloy runnerdepends.
See Best Practices for patterns
on combining Alloy models with implementation checks.
For full Alloy language syntax and semantics, see docs/alloy-reference.md
in the repository root.
Embedding Rules
alloy:model(name) is a fragment belonging to the logical model name.
Fragments with the same model name are combined in document order
into a single logical model. This lets a model grow incrementally
alongside the prose that motivates each piece, rather than appearing
as one monolithic block detached from context.
Only the first fragment may contain a module declaration — Alloy
requires exactly one module per compilation unit, and the first
fragment anchors it.
When a model block contains a check statement for an assertion, the
reference is implicit — no separate directive is needed. The check result
is automatically displayed as a status badge in the HTML report.
Formal Properties
specdown's own document model provides a worked example. Every executable unit belongs to exactly one heading scope, and no two heading scopes can share an executable unit.
module docmodel
sig Heading {}
sig Block {
scope: one Heading
}
sig TableRow {
scope: one Heading
}
pred sanityCheck {}
run sanityCheck {} for 5The one multiplicity on scope already guarantees single ownership,
so the following assertions are provably true by construction.
In a real project you would model properties that are not obvious
from the signature alone — this example is deliberately simple to
introduce the syntax.
assert blockBelongsToOneScope {
all b: Block | one b.scope
}
check blockBelongsToOneScope for 5assert rowBelongsToOneScope {
all r: TableRow | one r.scope
}
check rowBelongsToOneScope for 5Scoped Checks with but Clauses
Alloy's check command supports type-specific scope overrides via
but clauses — for example, check foo for 5 but 6 Int sets the
default scope to 5 but widens the integer bitwidth to 6 bits.
This is essential when a model uses integer values outside the default 4-bit range (-8 to 7).
module scoring
sig Player {
score: one Int
}
fact validScores {
all p: Player | p.score >= 0 and p.score <= 21
}
assert scoreBound {
all p: Player | p.score >= 0 implies p.score <= 21
}
check scoreBound for 3 but 6 Int
pred sanityCheck {}
run sanityCheck {} for 3 but 6 IntWithout but 6 Int, Alloy's default 4-bit integers only cover -8 to 7,
which cannot represent values like 10 or 21. The wider bitwidth lets the
solver explore the full range declared in the fact.
Combination Rules
Fragments with the same model name are merged into a single virtual .als file.
Source mapping comments are inserted into the generated model.
A module declaration in a subsequent fragment is a compile-time error.
Model Reference
An explicit alloy:ref directive links a section to a check result
defined in a different section. This is useful for cross-section references.
> alloy:ref(access#privateIsolation, scope=5)
The directive displays as a badge in the HTML report and links to a counterexample artifact on failure.
State Machine Models
Alloy is especially useful for modeling state machines where exhaustive path coverage by example is impractical.
This example models a traffic light with valid transitions only. Alloy proves that an invalid transition (Green → Red) cannot happen.
module traffic
abstract sig Color {}
one sig Red, Yellow, Green extends Color {}
sig Light {
color: one Color
}
pred validTransition[from, to: Color] {
from = Red implies to = Green
from = Green implies to = Yellow
from = Yellow implies to = Red
}
assert noGreenToRed {
all from, to: Color |
validTransition[from, to] implies not (from = Green and to = Red)
}
check noGreenToRed for 5
pred sanityCheck {}
run sanityCheck {} for 5Counterexample Artifacts
When an Alloy check fails, specdown writes a counterexample artifact to
.artifacts/specdown/counterexamples/. The alloy result detail in the
JSON report includes a counterexamplePath field (at .alloy.counterexamplePath
within a case result) pointing to this file.
On failure, the message field includes a counterexample summary
extracted from the Alloy solver output, and the artifact file is
written to disk.