Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Runtime ABI

The C-ABI surface between plgc-generated LLVM IR and libplg_runtime.a. Rust side: crates/runtime/src/abi.rs + entry.rs; IR side: the RUNTIME_DECLS block in crates/compiler/src/codegen/program.rs. Those two files are the contract; this document explains it. The build.rs version-sync guarantees an embedded runtime always matches the compiler that emits calls into it.

The uniform function signature

Every compiled predicate, clause, chain (“try next clause”), continuation, and retry function — and every runtime-provided continuation — shares one C-ABI prototype:

i32 (ptr %machine, i64 %env)

Returns 0 = fail/exhausted (the driver loop backtracks), 1 = stop (solution limit reached / enumeration finished early). The uniform prototype is what makes musttail transfers guaranteed-TCO under the C calling convention (same trick protobuf’s parser uses), and it lets Rust functions (the solve driver, the capture continuation, ,/2 at query level) participate at chain boundaries via plain function pointers.

Function pointers cross the boundary as i64 (ptrtoint/inttoptr in IR, transmute in Rust); frames and environments are heap indices, never raw pointers — the heap is a growable Vec<u64> and indices survive reallocation and backtracking truncation.

Control flow contract

  • Forward execution: generated code installs a continuation with plg_rt_set_k and musttails into the callee’s entry function. Solution delivery is a musttail into the installed k.
  • Failure: ret i32 0 unwinds the (constant-depth) tail-call chain to the runtime driver loop (solve.rs), which pops a choice point, rewinds trail+heap to its marks, and calls its retry function. The driver is the trampoline; the C stack never grows with recursion or backtracking depth.
  • Choice points: plg_rt_push_cp(m, retry_fn, env) snapshots trail/heap marks at push time. Predicate entries push one CP per remaining clause group lazily (entry pushes t1; t1 pushes t2; …).
  • Frames (predicate: [args…, k_fn, k_env]; clause body: [k_fn, k_env, vars…]) are allocated with plg_rt_frame_alloc before any CP that must see them — heap rewind frees everything allocated after the CP, so ordering is the correctness rule.

Generated global data

@plg_atom_strs    = [N x ptr]              ; atom names, id order
@plg_registry     = [K x { i32, i32, ptr }] ; functor, arity, entry fn

plg_rt_init re-interns the atom names in order, reproducing the compiler’s exact id space (both sides pre-seed the same well-known atoms); query atoms unseen at compile time get fresh ids ≥ N and correctly unify with nothing. The registry is sorted by (functor, arity) for binary search; :- dynamic predicates with no clauses point at @plg_rt_pred_fail (silent-fail contract).

Term words (tag in low 3 bits)

REF=0 (heap idx, unbound = self-ref) · ATOM=1 (id) · INT=2 (immediate i61; integers beyond the i61 range are boxed as TAG_BIG, below) · STR=3 (idx → [functor:u32|arity:u32][args…]) · LST=4 (idx → [head][tail]) · FLT=5 (idx → f64 bits, to_bits equality) · TAG_BIG=6 (idx → boxed i64, for integers beyond the i61 immediate).

Atoms and immediate integers never cross the ABI as calls: codegen emits their tagged words as compile-time constants ((id<<3)|1, (n<<3)|2 — mirrored in cell.rs and term_emit.rs, covered by tests on both sides).

The plg_rt_* surface

symbolsignature (IR)purpose
plg_rt_initptr (ptr, i32, ptr, i32)build Machine from atom table + registry
plg_rt_maini32 (ptr, i32, ptr)argv parse, query parse, solve, output, exit code
plg_rt_stepi32 (ptr)step counter; 0 ⇒ uncatchable resource_error set
plg_rt_new_vari64 (ptr)fresh unbound REF cell
plg_rt_frame_alloci64 (ptr, i32)raw continuation-frame cells
plg_rt_frame_set/getvoid/i64 (ptr, i64, i32[, i64])frame slot access
plg_rt_areg_get/seti64/void (ptr, i32[, i64])argument registers
plg_rt_breg_setvoid (ptr, i32, i64)build registers for put_struct
plg_rt_put_structi64 (ptr, i32, i32)compound from breg[0..arity]
plg_rt_put_listi64 (ptr, i64, i64)cons cell
plg_rt_put_floati64 (ptr, i64)boxed f64 (bits)
plg_rt_put_bigi64 (ptr, i64)box an i64 beyond the i61 immediate (TAG_BIG)
plg_rt_unifyi32 (ptr, i64, i64)generic trail-recording unify
plg_rt_set_k / plg_rt_k_fn / plg_rt_k_envcontinuation register
plg_rt_push_cpvoid (ptr, i64, i64)choice point (retry fn + env)
plg_rt_cp_topi64 (ptr)choice-point height (cut barriers, commit slots)
plg_rt_cutvoid (ptr, i64)truncate cps to height (stops at CATCH frames — catch is opaque to cut)
plg_rt_derefi64 (ptr, i64)first-arg indexing dispatch
plg_rt_str_keyi64 (ptr, i64)STR → packed functor cell, else u64::MAX
plg_rt_pred_faili32 (ptr, i64)always-fail body (dynamic stubs)
plg_rt_existence_errori32 (ptr, i32, i32)sets v1-format error, returns 0

Inline comparison and arithmetic builtins

symbolsignature (IR)purpose
plg_rt_b_isi32 (ptr, i64, i64)is/2 (v1 eval semantics, exact error strings)
plg_rt_b_arith_cmpi32 (ptr, i32, i64, i64)op 0..5 = < > =< >= =:= =\=
plg_rt_b_neqi32 (ptr, i64, i64)\= (trail-rewinding attempt)
plg_rt_b_term_cmpi32 (ptr, i32, i64, i64)op 0..5 = == \== @< @> @=< @>=
plg_rt_b_comparei32 (ptr, i64, i64, i64)compare/3

Op-code tables live in codegen lower.rs and runtime control.rs and must stay in sync. Control constructs (;, ->, \+, once) have NO runtime symbols for compiled code — they compile to choice points and commit slots (body.rs); the runtime’s control.rs mirrors the same shapes only for goals built at runtime (queries, metacalls), walking goal terms, never clauses.

Errors, metacall, and control builtins

Structured errors. Machine::error carries a relocatable ball (copyterm::TermBuf) plus its pre-rendered v1-format message. Balls survive heap rewinding; solve::drive unwinds them to the nearest catch frame (a CpKind::Catch choice point whose env holds [catcher, recovery, k_fn, k_env]). plg_rt_cut stops at catch frames (catch is opaque to cut). The step-limit ball remains uncatchable.

Control builtins (the installed k is the continuation; the runtime walks goal TERMS only — never clauses):

symbolsignature (IR)purpose
plg_rt_metacalli32 (ptr, i64)dispatch a runtime goal term (var goals, call/N)
plg_rt_b_catch_3i32 (ptr, i64, i64, i64)push catch frame, run goal
plg_rt_b_throw_1i32 (ptr, i64)snapshot ball, raise
plg_rt_b_findall_3i32 (ptr, i64, i64, i64)bounded sub-search via the shared driver
plg_rt_pred_between_3i32 (ptr, i64)nondet builtin, uniform predicate signature (A-registers)

C-stack note: plg_rt_metacall / plg_rt_b_catch_3 / plg_rt_b_findall_3 hold one Rust frame while their goal runs, so nesting depth of those constructs (not Prolog recursion) consumes C stack — bounded by the step limit; full CPS compilation of metacalls is a named escape hatch.

Deterministic builtins plg_rt_b_<name>_<arity> — one symbol per entry in codegen’s DET_BUILTINS table (lower.rs), declarations generated from the same table. The runtime’s query-side dispatch (control.rs det_builtin) mirrors it; the differential corpus guards the pair. Vocabulary = exactly v1’s: type checks, functor/arg/univ/copy_term, atom/number conversions, msort/sort, succ/plus, unify_with_occurs_check, write/writeln/nl.

Stdlib. crates/compiler/stdlib.pl (v1’s file, verbatim) is parsed before user sources in every plgc build — member/append/length/last/ reverse/nth0/nth1 are ordinary compiled predicates in every binary, exactly like v1’s embedding.

Environment

PLG_MAX_STEPS overrides the default 10,000 step ceiling (documented extension over v1, which hardcoded it; the CLI contract is unchanged).