Blog › ICP guides
Mercury developer on retainer: mode and determinism system, logic programming, backtracking, module system, and Mercury compiler engineering on monthly retainer
October 19, 2026 · ~18 min read
A configuration rule evaluation system built in Mercury had been producing spurious solutions eight times per day. The predicate evaluate_rule/3, responsible for testing whether a configuration entry matched a given policy rule set, was returning multiple distinct bindings for its output argument on inputs that should have had exactly one answer. The Mercury developer on retainer diagnosed the root cause: the predicate had no explicit :- mode evaluate_rule(in, in, out) is det declaration. Without a mode annotation, the Mercury compiler inferred a determinism category from the clause structure and selected semidet — which permits zero or one solution — rather than det, which requires exactly one. The semidet category allowed clauses that produced uninstantiated output arguments to succeed silently, and callers using the predicate in a solutions/2 backtracking context were collecting those partial solutions rather than reporting an error. Adding the explicit :- mode evaluate_rule(in, in, out) is det declaration forced the Mercury determinism checker to analyze all 23 clauses and verify that every execution path either bound the output argument to a ground term or failed. It surfaced 15 previously silent bugs: branches where the output was left uninstantiated, where a sub-predicate was called in a mode that allowed backtracking, and where an if-then-else arm had incompatible determinism categories. Spurious solutions: 8 per day → 0. Determinism errors caught at compile time: 0 → 15 newly diagnosed bugs.
The work log entry read “fixed spurious solution bug in rule evaluator, 11h.” It names the symptom and the duration. It cannot explain to a client why the fix required analyzing 23 clauses rather than patching one, why the Mercury compiler’s mode system was the diagnostic mechanism, why the absence of a mode declaration allowed the bug to exist silently for months, or what the 15 newly surfaced compile-time errors mean for production reliability. The diagnosis required understanding that Mercury’s determinism categories are a lattice: det (exactly one solution), semidet (zero or one), nondet (zero or more, backtrackable), multi (one or more, backtrackable), failure (always fails), erroneous (never returns), cc_nondet (committed-choice nondet — at most one solution delivered to the caller), and cc_multi (committed-choice multi). The compiler checks that every predicate’s body satisfies the determinism category declared in its mode annotation: a sequential conjunction of two det predicates is det; a conjunction of a det and a semidet is semidet; a disjunction of two det branches is det if the compiler can verify that exactly one branch is chosen per input. The 11 hours of mode analysis (tracing the inst flow for each argument through 23 clauses), branch-by-branch determinism diagnosis (identifying which branches produced uninstantiated outputs or allowed sub-predicate backtracking), restructuring (adding explicit if-then-else to replace implicit disjunction in the failing branches), and re-annotation (adding :- mode declarations for all 8 predicates in the affected module) are not visible in the diff beyond the 8 new mode declarations and the 15 corrected clause bodies. The spurious solutions: gone. The determinism guarantee: compiler-verified.
Mercury’s mode and determinism system: inst, mode, and det declarations
Mercury’s mode system is the compile-time mechanism that tracks the instantiation state of every variable at every program point. An inst (instantiation state) describes what is known about a term: free means the variable is unbound; ground means it is fully instantiated to a ground term; any means it may be ground or may contain unbound sub-variables; unique means it is the only reference to the term, permitting destructive update. The standard inst parameters for a :- mode declaration use these states to describe how each argument flows: in is syntactic sugar for ground >> ground (the argument is ground on entry and ground on exit); out is syntactic sugar for free >> ground (the argument is free on entry and ground on exit); in(Inst) specifies a more precise initial inst; out(Inst) specifies a more precise final inst. A predicate can have multiple mode declarations, each defining a distinct calling convention: :- mode lookup(in, in, out) is det for a forward-mode call (input key and table, output value) and :- mode lookup(in, out, in) is semidet for a reverse-mode call (input key and value, output the table entry key if found).
The determinism categories in a :- mode declaration specify how many solutions the predicate produces and whether it can be backtracked into. det: exactly one solution, no backtracking. semidet: zero or one solution, no backtracking. nondet: zero or more solutions, backtrackable. multi: one or more solutions, backtrackable. failure: always fails (zero solutions). erroneous: never returns (throws an exception or loops). cc_nondet (committed-choice nondet) and cc_multi (committed-choice multi): produce at most one or exactly one solution to the caller even though the predicate body may explore multiple alternatives; the caller commits to the first solution found. The Mercury compiler verifies that the declared determinism category is correct by analyzing the clause structure: an if-then-else where both branches are det and the condition is semidet or det yields det; a disjunction between two det clauses is only det if the compiler can verify exhaustive and non-overlapping switch coverage; a call to a nondet predicate inside a det context is a compile-time error. Mode polymorphism allows a single predicate body to be compiled into multiple specialized versions for different mode combinations; the compiler selects the appropriate version at each call site based on the caller’s mode context.
:- pred and :- func declarations are the primary mechanism for documenting types and modes in the module interface. :- pred evaluate_rule(config_entry::in, rule_set::in, evaluation_result::out) is det. is a combined type-and-mode declaration: it specifies the types of all three arguments and declares a single mode for the predicate with det determinism. Multiple :- mode declarations can appear for a predicate that has multiple valid calling conventions. :- func compute_score(float, float) = float. declares a function; :- func compute_score(float::in, float::in) = (float::out) is det. is the combined type-and-mode form. The Mercury compiler requires that every exported predicate and function has a declaration in the module’s interface section; local predicates may omit declarations, but adding them enables the compiler to report more precise error messages and allows the module interface to be used as documentation. Unused argument analysis (:- pragma unused_args) identifies arguments that are never used in any mode, enabling the compiler to omit them from the compiled code. Termination analysis (:- pragma terminates and :- pragma does_not_terminate) allows the compiler to verify that a predicate terminates on all inputs, which is required for some determinism proofs.
Custom :- inst declarations define named instantiation states for use in mode annotations. :- inst nonempty_list(I) == bound([I | list(I)]). defines an inst for non-empty lists whose elements satisfy inst I. :- inst config_status == bound(valid ; invalid ; pending). defines an inst for a discriminated union type restricted to three of its constructors. Using custom inst declarations in mode annotations tightens the information tracked by the mode system: a predicate declared as :- mode process(in(nonempty_list(ground)), out) is det can be called only with a non-empty list argument, and the mode checker verifies this at every call site. :- mode declarations can be used independently of :- pred declarations to add new calling conventions for predicates whose types are already declared. The separation between interface and implementation sections controls what is visible to importing modules: :- type, :- inst, :- mode, :- pred, and :- func declarations in the interface section are exported; those in the implementation section are module-private. A retainer engagement restructuring a Mercury module typically involves moving declarations from the implementation section to the interface section (to enable callers to use additional mode annotations), splitting large modules to reduce recompilation, and auditing :- use_module versus :- import_module usage (the former requires fully qualified references; the latter imports all exported names into the current namespace, which can cause name conflicts in large codebases).
Logic programming, backtracking, and the Mercury standard library
Mercury’s logic programming model inherits from Prolog’s clause-based predicate definitions but adds static type checking, mode checking, and determinism verification. A Mercury predicate is defined by a set of clauses: evaluate_rule(Entry, rule(Tag, Condition), Result) :- condition_matches(Entry, Condition), Result = matched(Tag). defines one clause; a second clause handles the non-matching case. Clause order matters for determinism: if the Mercury compiler can verify that the clauses are mutually exclusive (based on type and mode analysis), it treats a disjunction as a switch (deterministic); if it cannot, the disjunction is backtrackable. The if-then-else construct (Cond -> Then ; Else) is the preferred determinism-preserving control structure: if Cond is semidet and Then and Else are both det, the entire expression is det. Negation-as-failure \+ Goal succeeds if Goal fails and fails if Goal succeeds; not(Goal) is equivalent. Existential quantification in the condition of an if-then-else: (some [X] member(X, List) -> process(X, Result) ; default(Result)) avoids scope leakage of X into the else branch.
The Mercury standard library’s backtracking predicates collect solutions from nondet or multi predicates. solutions(Goal, List) calls Goal, backtracks to find all solutions, and binds List to the sorted list of all output bindings; it requires Goal to have an output argument of a type with an Ord instance, and it materializes the entire solution set in memory. solutions_set(Goal, Set) produces an set rather than a list. aggregate(Goal, Accumulate, Initial, Final) folds over solutions without materializing the full list: Accumulate is called with each solution and the current accumulator, and Final is the result after all solutions are processed. aggregate_solutions(Goal, Pred, Initial, Final) is the version that takes a closure for accumulation. findall(Template, Goal, List) works like Prolog’s findall/3: it collects all instances of Template (which may be a complex term) for each solution of Goal; it succeeds with an empty list if Goal has no solutions; it does not sort the results. promise_pure(Goal) wraps an impure goal in a purity context: it asserts to the Mercury compiler that despite calling impure predicates, the overall behavior of Goal is pure (referentially transparent). promise_semipure(Goal) asserts semi-purity (reads but does not write global state). The Mercury purity system has three levels: pure (no side effects, referentially transparent), semipure (may read but not write global state), and impure (may read and write global state or perform I/O); the compiler enforces that pure predicates call only pure predicates unless wrapped in a promise.
Mercury’s type class system uses :- typeclass and :- instance declarations for ad-hoc polymorphism. :- typeclass comparable(T) where [pred compare_to(T::in, T::in, comparison_result::out) is det]. declares a type class; :- instance comparable(config_entry) where [pred(compare_to/3) is compare_config_entries]. provides an instance. Type class constraints appear in predicate type declarations: :- pred sort_and_deduplicate(list(T)::in, list(T)::out) is det <= (comparable(T), printable(T)). Higher-order programming uses closure types: :- type predicate(T) == (pred(T) is semidet).; a value of this type can be called with call(Pred, Arg). The solutions/2 predicate takes a closure of type (pred(T) is nondet). Lambda expressions: (pred(X::out) is nondet :- member(X, List), X > 0) creates an anonymous predicate. Mercury’s higher-order mode system tracks instantiation and determinism through closure types, so passing a det closure where a nondet is expected is a compile-time error. The standard library’s list module provides list.foldl/3, list.foldl2/5 (two accumulators), list.map/3, list.filter/3, list.filter_map/3, list.sort/2, list.sort_and_remove_dups/2, list.length/2, and list.member/2 (nondet); string provides string.append/3, string.split_at_char/3, string.format/3, and string.to_int/2; io provides the I/O monad with io.write_string/3, io.read_line_as_string/3, and io.format/4.
The Mercury compiler (mmc) builds Mercury programs using a grade system that selects the backend, garbage collector, and runtime options. The primary grades: asm_fast.gc compiles to native machine code via LLDS (low-level data representation system) with the Boehm-Demers-Weiser conservative garbage collector — the default for production Linux builds; asm_fast.gc.debug adds stack tracing and debugging support; hlc.gc generates portable C code via MLDS (medium-level data representation system) — used when the native backend is not available; java compiles to Java source targeting the JVM — enables deployment in Java-managed environments; csharp compiles to C# targeting .NET — enables Windows and Unity deployment. The --make flag uses Mercury’s dependency tracking to rebuild only changed modules; mmc --make myapp builds the executable named myapp. Mercury’s deep profiler is activated with --profile-calls (call count profiling) and --profile-memory (memory allocation profiling); the resulting .prof files are analyzed with mdprof_cgi to generate the deep profile report. The --halt-at-warn flag promotes all warnings to errors, enabling strict builds that catch mode inference ambiguities, unused imports, and partially instantiated outputs before they reach production.
How HourTab tracks Mercury developer retainer hours
Mercury retainer work shares the invisible-work problem with all logic programming retainers, with the additional challenge that Mercury’s most common retainer tasks — mode annotation design, determinism category diagnosis, backtracking pipeline restructuring, module interface reorganization — produce diffs whose surface area is small relative to the analytical work required. Adding eight :- mode declarations to a module is a diff with eight lines added; the value is 15 previously-silent determinism bugs surfaced at compile time and zero spurious solutions in production. Restructuring a multi-clause predicate from an implicit disjunction to explicit if-then-else branches is a diff that rewrites a few dozen lines; the value is a determinism category upgrade from nondet to semidet that eliminates an entire class of runtime failures. Adding promise_pure wrappers to three C-interop predicates is a diff with three lines changed; the value is compiler-verified purity throughout the call chain, enabling the determinism checker to reason about those predicates’ effects. The mode analysis, the determinism lattice reasoning, the backtracking pattern selection, the purity context design — none of these have artifacts proportional to their complexity in the committed diff.
HourTab gives Mercury developers a public retainer-hours URL they send to clients — typically research teams deploying logic programming systems for configuration management, theorem-proving toolchains, or constraint-solving applications — at the start of an engagement. For Mercury retainers, each work log entry should name the mechanism (:- mode annotation addition for :- pred declarations; determinism category diagnosis for spurious solution predicates; solutions/2 backtracking pipeline design; aggregate_solutions/3 fold pipeline restructuring; promise_pure/promise_semipure purity context authorship; :- pragma foreign_proc C FFI declaration; module interface restructuring with :- type/:- inst/:- mode declarations; circular module dependency resolution; compiler grade selection for asm_fast/hlc/java/csharp targets), the specific predicate name and the mode or determinism problem, and the before/after observable metric. Mercury retainers are often compared to Prolog developer retainers for logic programming work, to Haskell developer retainers for the same type-driven correctness guarantees applied in a functional context, and to Erlang developer retainers for high-reliability systems with strong concurrency models. The distinction from Prolog is fundamental: Mercury adds static type checking, mode checking, and determinism verification that Prolog cannot provide — a Mercury program where all predicates are declared det can never produce spurious solutions at runtime, a guarantee that no amount of Prolog discipline can match. HourTab’s work log makes that distinction legible to clients: the entry names the predicate, the mode declaration added, and the class of bug it eliminated, so the client understands why 11 hours on eight mode declarations was the highest-leverage work in the engagement.
Track Mercury developer retainer hours without the status emails
HourTab gives Mercury developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: Mercury developer retainers
What does a Mercury developer on retainer typically do?
A Mercury developer on monthly retainer covers four principal service areas: mode and determinism annotation design (:- mode annotation addition for exported predicates; determinism category diagnosis for det/semidet/nondet/multi/failure/cc_nondet/cc_multi predicates; compiler-verified determinism for production predicates; determinism error resolution via clause restructuring); backtracking pipeline design (solutions/2 and aggregate_solutions/3 pipeline design; findall/3 usage audit; promise_pure/promise_semipure purity context authorship; negation-as-failure \+ usage review; if-then-else determinism analysis; cut-free predicate restructuring); module interface architecture (:- pred and :- func declaration authorship; :- type/:- inst/:- mode declaration placement in interface sections; :- use_module/:- import_module dependency management; module interface minimization; circular dependency resolution); and Mercury compiler configuration (grade selection for asm_fast/hlc/java/csharp targets; --make build dependency configuration; deep profiler analysis; --halt-at-warn strict compilation).
What Mercury work is most commonly underlogged in a retainer?
Mode annotation design and determinism diagnosis (configuration rule predicate returning spurious solutions 8/day — Mercury defaulting to semidet with no mode declaration; adding :- mode evaluate_rule(in, in, out) is det; 15 compile-time bugs surfaced across 23 clauses; spurious solutions: 8/day → 0; 12–24 hrs invisible in mode analysis and clause restructuring), promise_pure/promise_semipure purity context design (3 C-interop predicates needing purity wrappers; compiler purity errors: 14 → 0; 8–16 hrs invisible in FFI purity analysis), and module interface restructuring (2,400-line module split into interface/implementation sections; 38 :- pred declarations moved to interface; 5 circular dependency resolutions; 18–36 hrs invisible in import graph analysis). Each produces a small diff but a large correctness improvement.
What are typical Mercury developer retainer rates?
Entry-level Mercury developers (1–2 years, basic predicate/function declarations, det/semidet determinism, simple :- mode annotations, list.foldl/list.map, mmc --make) bill at $70–$125/hr. Mid-level Mercury engineers (2–4 years, full determinism category design nondet/multi/cc_nondet/cc_multi, solutions/2 and aggregate_solutions/3 pipeline design, :- pragma foreign_proc C interop with purity annotations, module interface restructuring, deep profiler analysis) bill at $115–$210/hr. Senior Mercury architects (4–8 years, full application architecture with Mercury’s module system, custom :- inst declarations, higher-order programming with closure types, Mercury’s type class system, deep profiler-guided optimization) bill at $170–$310/hr. Monthly retainer ranges: $2,500–$5,800/mo advisory (15–25 hrs), $8,000–$21,000/mo for full logic programming platform engagements.
What should a Mercury developer retainer agreement include?
A Mercury developer retainer agreement should specify: mode and determinism scope (:- mode annotation addition; determinism category diagnosis for all 9 determinism categories; determinism error resolution; compiler-verified determinism for production predicates); backtracking scope (solutions/2 and aggregate_solutions/3 pipeline design; findall/3 usage audit; promise_pure/promise_semipure purity context authorship; negation-as-failure audit; if-then-else determinism analysis; cut-free predicate restructuring); module system scope (:- pred/:- func declaration authorship; :- type/:- inst/:- mode declaration placement; :- use_module/:- import_module dependency management; circular dependency detection and resolution); foreign procedure scope (:- pragma foreign_proc declarations with purity annotations; C/C++/Java/C# interop; :- pragma foreign_type declarations); compiler grade scope (grade selection for production targets; --profile-calls/--profile-memory deep profiler; --halt-at-warn strict compilation); and hour logging format (predicate name; mode annotation added; determinism category before/after; spurious solution count before/after; Mercury version and grade).
How should Mercury developer retainer hours be logged?
Log each Mercury retainer session with: advisory category (:- mode annotation addition; determinism category diagnosis; solutions/2 backtracking pipeline design; aggregate_solutions/3 fold restructuring; promise_pure/promise_semipure purity context authorship; :- pragma foreign_proc C FFI declaration; module interface restructuring; circular module dependency resolution; compiler grade selection; deep profiler analysis), the specific predicate name and mode/determinism problem (evaluate_rule/3 returning spurious solutions — semidet inferred with no mode declaration; :- mode evaluate_rule(in, in, out) is det added; 15 compile-time determinism errors surfaced across 23 clauses), and the before/after metric (spurious solutions/day: 8 → 0; compile-time determinism errors surfaced: 0 → 15; compiler purity errors: 14 → 0). Include Mercury version, compiler grade, and changes to :- interface section. For :- pragma foreign_proc work, log each predicate name, foreign language, purity annotation, and Mercury inst types per argument.