Architecture Note: Using Prolog CLP(FD) for Deterministic Safety-Critical Configuration Solving
Architecture note for a safety-critical configuration solver using Prolog’s CLP(FD): requirements, minimal pure-module design, trust boundary validation, runtime checks, failure modes, and when the design must change.
30 Nov 2025, 05:56 UTC

Requirements
The configuration subsystem must:
- Represent each configurable parameter as an integer variable with a known finite domain (e.g., 0‑10).
- Express arithmetic and logical constraints between those variables (equality, inequality, ordering).
- Return either a concrete assignment that satisfies all constraints or a clear unsatisfiability signal.
- Guarantee deterministic runtime behavior suitable for certification (no unbounded search, no nondeterministic side‑effects).
- Treat incoming configuration data as untrusted and prevent malicious values from causing domain explosions or infinite loops.
Smallest Suitable Design
A single Prolog module that imports the CLP(FD) library, defines domain variables, posts constraints using CLP(FD) arithmetic operators, and invokes labeling/2 to search for a solution meets the above requirements.
:- use_module(library(clpfd)).
solve_config(Params, Solution) :-
% Params is a list of validated integer values supplied by the caller
% Example: Params = [MaxSum, Switch1Max, Switch2Max, Switch3Max]
[MaxSum, S1Max, S2Max, S3Max] = Params,
% Domain variables for three switches
Switch1 in 0..S1Max,
Switch2 in 0..S2Max,
Switch3 in 0..S3Max,
% Constraint: total activation must not exceed MaxSum
Switch1 + Switch2 + Switch3 #=< MaxSum,
% Additional example constraint: Switch1 must be at least 1 if Switch2 is 2
Switch2 #= 2 #==> Switch1 #>= 1,
% Search for a solution
labeling([ff], [Switch1, Switch2, Switch3]),
% Build result structure
Solution = [switch1=Switch1, switch2=Switch2, switch3=Switch3].
% Entry point for command‑line use
main :-
current_prolog_flag(argv, [File|_]),
file_exists(File) ->
read_terms(File, Terms),
mapcall(validate_and_solve, Terms) ;
format('Usage: swipl -q -s solver.pl -g main -t halt -- \n'),
halt(1).
validate_and_solve(Term) :-
% Expect a term like config(MaxSum,S1Max,S2Max,S3Max)
Term =.. [config|Vals],
% Simple validation: all values must be integers in 0..100
maplist(between(0,100), Vals),
solve_config(Vals, Sol),
format('Solution: ~w~n', [Sol]),
fail; _ -> true. % fail after first solution to demonstrate unsatisfiable handling
read_terms(File, Terms) :-
open(File, read, Stream),
read_terms(Stream, Terms, []),
close(Stream).
mapcall(_, []).
mapcall(Pred, [H|T]) :-
call(Pred, H),
mapcall(Pred, T).
The module contains only pure predicates; no assert/1 or retract/1 appears inside the solving clause, preserving logical purity.
Trust / Data Boundaries
All data that originates outside the trusted core (e.g., configuration files, operator input) is first passed through a validation step:
- Values are checked to be integers.
- Each value is confined to a predefined safe range (here 0‑100).
- Only after validation are the values bound to CLP(FD) domain variables via
in/2or used as bounds for those variables.
This boundary prevents an attacker from supplying a huge domain (e.g., 0‑1_000_000) that could cause propagation to allocate excessive memory or drive the search space to infeasible size.
Operational Checks
At runtime the program performs three observable checks:
- Validation result – if any input term fails the integer/range test, the solver skips that term and logs a validation warning.
- Labeling outcome –
labeling/2either succeeds (returning a concrete assignment) or fails. Success triggers a secondary verification predicate that re‑evaluates each original constraint with the obtained values to ensure no propagation error slipped through. - Timeout guard (optional) – wrapping the call to
labeling/2withcall_with_time_limit/2(e.g., 500 ms) guarantees that a pathological constraint set cannot hang the system; on timeout the solver reports "search exceeded time limit" and treats the configuration as unsatisfiable for safety purposes.
These checks produce deterministic observable outcomes: a solution, a validation warning, a timeout notice, or an explicit unsatisfiability report.
Failure Modes
- Constraint overload / search blow‑up – highly constrained or poorly ordered variables can cause exponential search time. Mitigation: use variable ordering heuristics (
ff– first fail) inlabeling/2, apply iterative deepening, or enforce a timeout as described. - Incomplete propagation for nonlinear constraints – CLP(FD) guarantees domain consistency only for linear constraints. Non‑linear expressions (e.g.,
X*Y #= Z) may leave spurious values in domains, requiring explicit enumeration (labeling/2) to eliminate them. - Mixing impure predicates – inserting
assert/1orretract/1inside the solving clause breaks referential transparency, making it harder to prove that a given input always yields the same output, which is undesirable for certification.
Conditions That Would Change the Design
The current design would be revisited if any of the following arose:
- Non‑integer domains – if configuration parameters needed to be floats or rational numbers, CLP(FD) would be insufficient; a switch to CLP(R) (real numbers) or an external SAT/SMT solver handling floating‑point theory would be required.
- Global optimization objectives – should the system need to minimize or maximize a cost function (e.g., power consumption) rather than merely find any feasible solution, a dedicated optimization library (
labeling/2withmax/2ormin/2) or a mixed‑integer programming solver would be more appropriate. - Higher‑throughput demands – if the configuration service must solve thousands of instances per second with sub‑millisecond latency, the overhead of Prolog’s backtracking might become prohibitive, prompting a move to a compiled constraint‑propagation engine or a SAT solver with incremental solving capabilities.
- Certification constraints requiring proof‑carrying code – some safety standards demand a machine‑checkable proof of correctness. In that case, generating a proof artifact from the CLP(FD) solving process (e.g., via proof‑producing SAT solvers) would be necessary, altering the interface from a simple solution return to a solution plus proof object.
Practical Verification Steps
To confirm that the solver behaves as intended:
- Create a file
sat.plcontaining a known satisfiable configuration, e.g.,config(5,2,2,2). - Run the solver:
swipl -q -s solver.pl -g main -t halt -- sat.pl. Expected output: a line beginning with "Solution:" showing three numbers whose sum ≤ 5 and satisfying the extra implication constraint. - Create an unsatisfiable file
unsat.plwith contradictory constraints, e.g.,config(0,2,2,2).(sum must be ≤ 0 but each switch ≥ 0 and at least one must be ≥ 1 due to the implication when Switch2=2). - Run the same command with
unsat.pl. The program should print no "Solution:" line and exit with code 0 after exhausting the search (or report a timeout if one is configured). - Optionally enable timing:
swipl -q -s solver.pl -g "call_with_time_limit(main, 500)" -t halt -- unsat.pland verify that the solver terminates within the limit.
These steps provide a concrete, repeatable way to validate both the satisfiable and unsatisfiable paths without relying on external benchmarks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.