| title | Prolog Engine Reference |
|---|
The Prolog inference engine in zpm uses Trealla Prolog via a C99 API, exposing a memory-safe Zig API for querying, asserting facts, and loading programs.
var engine = try Engine.init(.{});
defer engine.deinit();The engine creates its own GeneralPurposeAllocator internally.
Config fields (EngineConfig):
| Field | Type | Default | Purpose |
|---|---|---|---|
timeout_ms |
u64 |
5000 |
Query execution timeout in milliseconds |
max_solutions |
usize |
100 |
Maximum solutions returned per query |
max_recursion_depth |
usize |
1000 |
Maximum recursion depth during query |
max_memory_bytes |
usize |
67108864 |
Max memory for Prolog runtime (64MB) |
Errors:
OutOfMemory— Allocator exhaustedInitFailed— Trealla Prolog runtime initialization failed
engine.deinit();Cleans up Prolog runtime and leaks check. Consumes the engine; further calls will fail.
Execute a Prolog query and retrieve variable bindings.
const result = try engine.query("parent(X, alice)", allocator);
defer result.deinit(allocator);Returns: QueryResult containing matching solutions.
Errors:
QueryFailed— Query execution returned null from FFITimeout— Sandbox check failed (timeout_ms is zero)OutOfMemory— Allocation failureInvalidJson— FFI returned unparseable JSON
QueryResult fields:
pub const QueryResult = struct {
solutions: []Solution,
allocator: std.mem.Allocator,
pub fn deinit(self: *QueryResult) void;
};
pub const Solution = struct {
bindings: std.StringHashMap(Term), // Variable -> Term mapping
allocator: std.mem.Allocator,
pub fn deinit(self: *Solution) void;
};
pub const Term = union(enum) {
atom: []const u8, // Atoms: abc, 'quoted atom'
integer: i64, // Integers
float: f64, // Floating point numbers
variable: []const u8, // Unbound variables
list: []Term, // Lists: [1,2,3]
compound: struct { // Compounds: f(a, b)
functor: []const u8,
args: []Term,
},
};Example:
var result = try engine.query("member(X, [1, 2, 3])");
defer result.deinit();
for (result.solutions) |solution| {
if (solution.bindings.get("X")) |term| {
std.debug.print("X = {}\n", .{term});
}
}Add a fact to the knowledge base.
try engine.assertFact("parent(bob, alice)");Trailing dots are stripped automatically. Also available as engine.assert().
Errors:
AssertFailed— FFI returned non-zero statusOutOfMemory— Allocation failure
Remove a fact from the knowledge base.
try engine.retractFact("parent(bob, alice)");Also available as engine.retract().
Errors:
RetractFailed— FFI returned non-zero statusOutOfMemory— Allocation failure
Load a Prolog source file (.pl) into the engine.
try engine.loadFile("/path/to/program.pl");Errors:
LoadFailed— FFI returned non-zero status (file not found, syntax error, permission denied)OutOfMemory— Allocation failure
Load a Prolog program from a memory buffer.
const program =
\\ parent(tom, bob).
\\ parent(bob, ann).
\\ ancestor(X, Y) :- parent(X, Y).
\\ ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
;
try engine.loadString(program);
var result = try engine.query("ancestor(tom, ann)");
defer result.deinit();Errors:
LoadFailed— FFI returned non-zero status (syntax error)OutOfMemory— Allocation failure
The engine enforces three layers of protection:
Sandbox constraints are checked before each query via checkSandbox():
- If
timeout_msis0, returnsEngineError.Timeout - If
max_memory_bytesis less than 1024, returnsEngineError.OutOfMemory
var engine = try Engine.init(.{ .timeout_ms = 1000 });
defer engine.deinit();
var result = try engine.query("member(X, [a,b,c])");
defer result.deinit();Configured via max_recursion_depth (default: 1000). Passed to the FFI layer for enforcement.
Configured via max_memory_bytes (default: 64MB). Pre-flight check rejects configurations below 1KB.
All engine methods return EngineError unions:
| Error | Cause |
|---|---|
InitFailed |
Prolog runtime failed to initialize |
QueryFailed |
Query execution returned null from FFI |
AssertFailed |
Fact assertion returned non-zero from FFI |
RetractFailed |
Fact retraction returned non-zero from FFI |
LoadFailed |
File or string loading returned non-zero from FFI |
Timeout |
Sandbox check: timeout_ms is zero |
OutOfMemory |
Allocation failure or sandbox check: max_memory_bytes < 1024 |
InvalidJson |
FFI returned unparseable JSON for query results |
var result = engine.query("parent(X, alice)") catch |err| switch (err) {
error.QueryFailed => std.debug.print("Query failed\n", .{}),
error.Timeout => std.debug.print("Timeout\n", .{}),
error.OutOfMemory => std.debug.print("OOM\n", .{}),
else => return err,
};The engine creates and owns its own GeneralPurposeAllocator:
- Ownership: The engine allocates itself via
page_allocator, then uses its internal GPA for all subsequent allocations - Lifetime:
QueryResultandSolutionown their allocations; free via.deinit() - Leaks: GPA reports Zig-side leaks on
engine.deinit()in debug mode - FFI boundary: C-side strings returned by Trealla are freed via
prolog_free_string; each language owns its own memory
var engine = try Engine.init(.{});
defer engine.deinit();
var result = try engine.query("true");
defer result.deinit(); // Must free before engine.deinit()The engine is intentionally independent of MCP:
- Engine methods use plain Zig types (
[]const u8,Term, error unions) - MCP tool handlers (F003) will wrap engine methods and convert results to JSON-RPC responses
- This separation allows testing and reuse without MCP protocol overhead
See ADR-0004 for the current Prolog backend rationale (Trealla via C FFI). The original Rust FFI design is captured in the superseded ADR-0002; F016 migrated to Trealla's C99 API to eliminate the Rust toolchain dependency.
Engine tests are colocated inline in src/prolog/engine.zig. Run via:
make testTests cover:
- Engine lifecycle (init, deinit, leak detection)
- Query execution (success, no-match, syntax error)
- Fact assertion and retraction
- Program loading from file and string
- Sandboxing (timeouts, recursion limits)