first order logic
logical reasoning
artificial intelligence
computational logic
FOL engine

First Order Logic Engine

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

A first-order logic engine is a program that stores facts and rules, then answers queries by applying logical inference. In practice, such an engine needs three concrete pieces: a representation for terms and predicates, a unification algorithm, and a search strategy that can chain rules together without confusing variable bindings.

Model terms, facts, and rules explicitly

First-order logic is richer than propositional logic because it can talk about objects and relations between them. A small engine usually models:

  • constants such as alice
  • variables such as X
  • predicates such as parent(alice, bob)
  • rules such as grandparent(X, Z) :- parent(X, Y), parent(Y, Z)

A simple Python representation might look like this:

python
1from dataclasses import dataclass
2
3@dataclass(frozen=True)
4class Term:
5    name: str
6    is_var: bool = False
7
8@dataclass(frozen=True)
9class Atom:
10    name: str
11    args: tuple
12
13@dataclass(frozen=True)
14class Rule:
15    head: Atom
16    body: tuple

That is enough structure to build a toy engine. The important part is that variables and constants are distinguishable.

Unification is the core operation

Unification tries to make two atoms match by finding substitutions for variables. For example:

text
parent(X, bob)
parent(alice, bob)

can be unified with the substitution X = alice.

A minimal unifier for terms can look like this:

python
1def unify_term(a, b, subst):
2    if subst is None:
3        return None
4    if a == b:
5        return subst
6    if a.is_var:
7        return bind(a, b, subst)
8    if b.is_var:
9        return bind(b, a, subst)
10    return None
11
12def bind(var, value, subst):
13    if var in subst:
14        return unify_term(subst[var], value, subst)
15    new_subst = dict(subst)
16    new_subst[var] = value
17    return new_subst

Real engines also need an occurs check or a deliberate decision to skip it, but the main idea is unchanged: matching predicates is about substitutions, not string comparison.

Add backward chaining for query answering

Once facts and rules are represented and unification works, the engine can answer queries by backward chaining:

  1. take the goal
  2. find facts or rule heads that unify with it
  3. if a rule matches, turn its body into new subgoals
  4. continue until the goal list is empty or no rule applies

For example, with these facts:

text
parent(alice, bob)
parent(bob, charlie)

and this rule:

text
grandparent(X, Z) :- parent(X, Y), parent(Y, Z)

a query for grandparent(alice, charlie) succeeds by reducing into the two parent goals.

A simplified recursive skeleton is:

python
1def prove(goal, facts, rules, subst=None):
2    subst = subst or {}
3
4    for fact in facts:
5        result = unify_atom(goal, fact, subst)
6        if result is not None:
7            yield result
8
9    for rule in rules:
10        head_match = unify_atom(goal, rule.head, subst)
11        if head_match is None:
12            continue
13        yield from prove_all(rule.body, facts, rules, head_match)

The details get larger quickly, but that is the essence of a logic engine.

Variable renaming matters

One subtle requirement is variable renaming. Every time a rule is reused during search, its variables must be fresh. Otherwise, one proof branch can accidentally pollute another through shared variable names.

That is why a working engine is more than facts plus recursion. It must manage scope and substitutions carefully or the answers become incorrect.

Common Pitfalls

The most common mistake is representing variables and constants the same way. If the engine cannot tell whether X is a variable or a literal symbol, unification will behave incorrectly.

Another mistake is skipping fresh variable renaming when applying rules. Reused rule variables can leak bindings across branches and produce false results.

Developers also underestimate search control. A correct logical rule set can still loop forever if the engine uses naive recursion without any depth control or rule ordering discipline.

Finally, do not confuse parsing with inference. Reading parent(alice, bob) from text is only the front end; the engine still needs unification and a proof procedure behind it.

Summary

  • A first-order logic engine needs structured terms, predicates, rules, and variables.
  • Unification is the core mechanism that lets queries match facts and rule heads.
  • Backward chaining is a common way to answer queries in small logic engines.
  • Fresh variable renaming is essential for correct repeated rule use.
  • Parsing syntax is only the first step; the real engine is substitution plus search.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.