Alloy Models
Executable blocks test selected examples; Alloy proves properties for all cases within scope. Both live in the same document.
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.
Only the first fragment may contain a module declaration.
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
}The 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 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.
Build spec with but-scope check and run Alloy verification
mkdir -p .tmp-test
printf '# T\n\n- [But](but-scope.spec.md)\n' > .tmp-test/index.spec.md
printf '{"entry":"index.spec.md","adapters":[],"models":{"builtin":"alloy"},"reporters":[{"builtin":"json","outFile":"but-report.json"}]}' > .tmp-test/but-cfg.json
printf '%s\n' '# But Scope Test' '' '```alloy:model(scoring)' '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' '```' > .tmp-test/but-scope.spec.md
specdown run -config .tmp-test/but-cfg.json 2>&1 || truegrep -q '"status": "passed"' .tmp-test/but-report.jsonCombination 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.
Reject duplicate module declaration in same model
mkdir -p .tmp-test
printf '# Bad\n\n```alloy:model(dm)\nmodule dm\nsig A {}\n```\n\n```alloy:model(dm)\nmodule dm\nsig B {}\n```\n' > .tmp-test/dup-module.spec.md
printf '# T\n\n- [Dup](dup-module.spec.md)\n' > .tmp-test/index.spec.md
printf '{"entry":"index.spec.md","adapters":[],"models":{"builtin":"alloy"}}' > .tmp-test/dup-module-cfg.json
! specdown run -config .tmp-test/dup-module-cfg.json 2>/dev/nullModel 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.
A dry-run must recognize alloy:ref directives as alloy checks.
Create spec with alloy:ref and verify dry-run detects it
mkdir -p .tmp-test
printf '# Ref Test\n\n## Model\n\n```alloy:model(rm)\nmodule rm\nsig A {}\nassert noOrphan { all a: A | a in A }\ncheck noOrphan for 3\n```\n\n## Reference\n\n> alloy:ref(rm#noOrphan, scope=5)\n' > .tmp-test/ref-test.spec.md
printf '# T\n\n- [Ref](ref-test.spec.md)\n' > .tmp-test/index.spec.md
printf '{"entry":"index.spec.md","adapters":[],"models":{"builtin":"alloy"}}' > .tmp-test/ref-test-cfg.json... (1 line)
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 5Verify the traffic light model passes
mkdir -p .tmp-test
printf '%s\n' '# Traffic' '' '```alloy:model(traffic)' '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' '```' > .tmp-test/traffic.spec.md
printf '# T\n\n- [Traffic](traffic.spec.md)\n' > .tmp-test/index.spec.md
printf '{"entry":"index.spec.md","adapters":[],"models":{"builtin":"alloy"}}' > .tmp-test/traffic-cfg.json
specdown run -config .tmp-test/traffic-cfg.json 2>&1 | grep -q 'PASS'Counterexample Artifacts
When an Alloy check fails, specdown writes a counterexample artifact to
.artifacts/specdown/counterexamples/. The AlloyCheckResult in the
JSON report includes a counterexamplePath field pointing to this file.
On failure, the message field includes a counterexample summary
extracted from the Alloy solver output.
Run a deliberately failing Alloy check
mkdir -p .tmp-test
printf '%s\n' '# Counterexample Test' '' '```alloy:model(cx)' 'module cx' 'sig Node { next: lone Node }' 'assert allDisconnected { all n: Node | no n.next }' 'check allDisconnected for 3' '```' > .tmp-test/cx-test.spec.md
printf '# T\n\n- [CX](cx-test.spec.md)\n' > .tmp-test/index.spec.md
printf '{"entry":"index.spec.md","adapters":[],"models":{"builtin":"alloy"},"reporters":[{"builtin":"json","outFile":"cx-report.json"}]}' > .tmp-test/cx-cfg.json
specdown run -config .tmp-test/cx-cfg.json 2>&1 || trueThe JSON report includes a counterexamplePath for the failing check.
The counterexample artifact file exists on disk.
Verify the counterexample file was written
path=$(grep 'counterexamplePath' .tmp-test/cx-report.json | sed 's/.*"counterexamplePath"[^"]*"\([^"]*\)".*/\1/')
test -n "$path" && test -f "$path"