A simple, practical introduction for developers
When we write programs, we usually describe how to compute something:
do this, then do that, handle these cases, return a result.
Logic programming flips that upside down.
Instead of giving a step-by-step procedure, you describe what is true, and a logic programming engine figures out what follows from those truths. This model comes from languages like Prolog and Datalog, and it can be surprisingly useful in modern systems β especially compilers and optimizers.
π What exactly is a logic programming engine?
At its core, a logic programming engine works with two ingredients:
1. Facts
These are unconditional truths.
Example:
parent(alice, bob).
parent(bob, carol).
2. Rules
These describe when new facts can be inferred.
Example:
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
The engine looks at all facts and all rules and automatically derives everything that must be true. It handles:
- pattern matching (unification)
- variable binding
- recursive inference
- transitive closure
- generating all valid solutions
The programmer doesnβt have to manually write loops or conditional logic.
βοΈ Why developers care: less code, more power
Logic engines shine in domains where you want to express relationships and patterns, not algorithms. For example:
- optimization rules in compilers
- rewriting expressions or ASTs
- static analysis
- dependency solving
- constraint systems
- scheduling and planning problems
A rule like:
simplify(add(0, x)) β x
is much easier to write than the equivalent Rust code that manually walks an AST and checks all the cases.
The engine handles the heavy lifting.
A concrete example in Rust:
crepe
Rust has a small but elegant Datalog engine called crepe.
It lets you express facts and rules directly in Rust syntax:
crepe! {
@input
struct Edge(u32, u32);
@output
struct Reachable(u32, u32);
Reachable(a, b) <- Edge(a, b);
Reachable(a, c) <- Edge(a, b), Reachable(b, c);
}