-
-
Notifications
You must be signed in to change notification settings - Fork 0
compile overview
The rule compiler ships in src/compile/. It provides a pure-data IR (5 Term kinds + 4 Goal kinds + Clause + Rule), a lowering pass that produces runtime rule functions, and two tagged-template front-ends: a per-clause DSL (clause\...`) and a **strict-Prolog whole-program parser** (prolog`...``). Both front-ends emit identical IR for the shared subset.
front-ends IR lower.js src/solve.js
───────────────── ───────────── ──────────────────── ─────────────
clause`...` Rule { function (...vars) { rules[name] =
prolog`...` name ... [fn1, fn2, ...]
arity }
clauses[] function (...vars) {
} ...
}
→ rules dict
Front-ends emit IR; the runtime never sees IR. Lowering is the only place that knows the runtime rule shape — front-ends and the IR are insulated.
The full design (IR shapes, lowering decisions, the Lit-walker, validation) is in dev-docs/compiler-ir.md. This page is the user-facing reference for the IR + lowering + per-clause DSL; the strict-Prolog form has its own page at compile-prolog.
| Import path | Exports |
|---|---|
yopl/compile |
IR constructors + lowerRule / lowerRules + validate / validateOrThrow + IR symbol + deep6 re-exports. |
yopl/compile/clause.js |
Per-clause tagged-template DSL: rule(name, arity)(clause\...`, ...)`. |
yopl/compile/prolog |
Strict-Prolog tagged-template parsers: prolog\...`, prologClause`...``. |
yopl/compile/prolog/file.js |
Filesystem-backed loaders for .pl files: prologFile, prologFileAsync. (Node / Bun / Deno; needs node:fs.) |
import solve from 'yopl'; import {variable as v} from 'deep6/env.js'; import assemble from 'deep6/traverse/assemble.js'; import {rule, clause} from 'yopl/compile/clause.js'; import {lowerRules} from 'yopl/compile'; const rules = lowerRules([rule('member', 2)(clause`(X, [X | _])`, clause`(X, [_ | T]) :- member(X, T)`)]); const X = v('X'); const list = {value: 1, next: {value: 2, next: {value: 3, next: null}}}; solve(rules, 'member', [X, list], env => console.log(assemble(X, env))); // 1, 2, 3
For program-style source with multi-clause rules in one tag, use the strict-Prolog form:
import {prolog} from 'yopl/compile/prolog'; const rules = prolog` member(X, [X | _]). member(X, [_ | T]) :- member(X, T). `;
See compile-prolog for the strict-Prolog tag's full surface (body operators, op/3 directives, file loading, source maps).
Compare both to the hand-written encoding in Writing-rules — both compiled forms emit the right call(...) wrapping automatically and the validator catches arity drift at compile time.
rule(name, arity)(...clauses) returns an IR Rule. Each clause is built via the tagged-template:
clause`HEAD :- BODY` // body optional; bodyless clause is a fact
| Syntax | Meaning |
|---|---|
X, _user
|
Logic Variable (uppercase or _-prefixed identifier). |
_ |
Wildcard (matches anything, doesn't bind). |
null, true, false
|
JS literals. |
42, -3.14
|
Numeric literals. |
"foo", 'foo'
|
String literals (escapes via \). |
[X, Y, Z] |
Cons-list with null tail. |
[X, Y | T] |
Cons-list with explicit tail (use |, mirroring Prolog). |
compound(args) |
Compound term {name: 'compound', args: [...]}. |
! |
Cut. |
fail |
Fail goal. |
${value} |
Interpolation slot — auto-wrapped per its position. |
Interpolation slots (${...}) are auto-wrapped based on whether they appear in argument or goal position:
Arg position (head args, list elements, compound args):
| Value | Wrapped as |
|---|---|
Primitive (number, string, boolean, null, undefined) |
Lit(value) |
IR node (object with string kind) |
passed through |
| Function |
throws — wrap explicitly with Js(...)
|
Plain object (no kind) |
throws — wrap explicitly with Lit(...) or build IR |
| Array | throws |
Goal position (body):
| Value | Wrapped as |
|---|---|
| Function |
Js(fn) (factory shape) |
| IR node | passed through |
| Primitive | throws |
Plain object (no kind) |
throws |
The strict "throw on plain object" fence catches a class of bugs where the user intended IR but forgot a constructor (e.g., wrote {name: 'foo', args: []} instead of Compound('foo')).
const i = 5; clause`(X, 'item${i}')`; // ERROR — the lexer can't close the string across chunks clause`(X, ${'item' + i})`; // OK — primitive auto-wraps to Lit('item5')
The interpolation must span complete lexemes; tagged-template chunks are lexed independently.
clause\...`always attaches an optionalsource: {line, col}annotation to the resultingClauseIR (each tag is one clause; the cost is negligible). The strict-Prolog tag opts into this via asourceMap: true` configurator option — see compile-prolog § Source maps.
Lit(value) lowers per activation. The walker descends into plain objects and arrays inside the value, and recursively lowers any nested IR node (Var, Wild, Cons, Compound, Lit) with the activation's fresh logic Variables. So an object literal doubles as a pattern matcher and a constructor:
import {rule, clause} from 'yopl/compile/clause.js'; import {Lit, Var} from 'yopl/compile'; // Match any object whose `age` field is X; bind X. clause`(${Lit({age: Var('A')})}, A) :- ...`; // Construct the same shape from a bound A. clause`(${Lit({age: Var('A')})}, A)`;
The walker is gated on a closed set of IR kind discriminators (var | wildcard | literal | cons | compound), so user objects with a domain kind field (e.g. {kind: 'click', x: 10}) aren't misread.
Maps, Sets, Dates, and Wrap-wrapped values from open() / soft() pass through unchanged — to put logic Variables inside a Map value, build the Map inside a Js body goal where you have direct access to activation Variables.
Imported from yopl/compile.
| Constructor | Returns |
|---|---|
Var(name?) |
{kind: 'var', name} — name optional; Var() mints a fresh Symbol. |
Wild() |
{kind: 'wildcard'} |
Lit(value) |
{kind: 'literal', value} — walked per activation if it contains nested IR. |
Cons(head, tail) |
{kind: 'cons', head, tail} — one cons cell. |
Compound(name, args?) |
{kind: 'compound', name, args} — name may be a VarTerm for dynamic dispatch. |
List(items, tail?) |
Build a cons-list from an array; empty defaults to Lit(null). |
| Constructor | Returns |
|---|---|
Call(name, args?) |
{kind: 'call', name, args} — name may be a VarTerm for dynamic dispatch. |
Cut() |
{kind: 'cut'} |
Fail() |
{kind: 'fail'} |
Js(factory) |
{kind: 'js', factory} — factory(vars, sys) invoked per activation. |
| Constructor | Returns |
|---|---|
Clause(head, body?, vars?) |
{head, body, vars?} |
Rule(name, arity, clauses) |
{name, arity, clauses} |
| Re-export | Purpose |
|---|---|
open(o) |
Wrap factory — locks subset matching regardless of env. |
soft(o) |
Wrap factory — extends both sides with each other's keys. |
_ (also any) |
The deep6 match-anything sentinel. |
IR_KINDS |
Closed set of kind discriminators used by the Lit-walker. |
These propagate through Lit(...) so Lit(open({tag: 'a'})) works as expected.
Inline JS goals are built via the Js IR kind (often auto-wrapped from a function in goal position):
type JsFactory = (vars: Record<string, Variable>, sys: ReadonlyArray<Variable>) => GoalFn;
-
vars— the activation's user-Variable bindings, keyed by user-var name. -
sys— the choice-point frame slot used bycut(sys).
clause`(X) :- ${({X}) => env => X.isBound(env) && X.get(env) > 0}`;
For symbol-named anonymous Variables (Var() without a name argument), destructure with a computed property name:
const X = Var(); clause`(${X}) :- ${({[X.name]: x}) => env => x.isBound(env)}`;
Imported from yopl/compile.
| Function | Returns |
|---|---|
lowerRule(rule) |
RuleBody[] — array of runtime clause functions for one Rule. |
lowerRules(rules) |
Rules — runtime dictionary keyed by rule name. |
Each clause is walked per activation with fresh logic Variables — the IR is read-only, the lowered values are fresh. The Lit-walker handles nested IR; Compound and Cons walkers handle the structure.
Imported from yopl/compile.
| Function | Returns |
|---|---|
validate(rules, options?) |
Issue[] — empty when clean. |
validateOrThrow(rules, options?) |
void — throws on first non-empty list. |
Issue kinds: arity-mismatch, call-arity-mismatch, undeclared-var, duplicate-rule, unresolved-rule (opt-in via options.checkRuleReferences).
undeclared-var only fires when Clause.vars is supplied explicitly — front-ends that compute vars from head/body don't trigger it.
The unifyOpts/3 system predicate runs deep6 unification with a per-call options bag, restoring the env's baseline options before the goal returns. From clause source, wrap the bag with Lit({...}):
rule('strictEq', 2)(clause`(X, Y) :- unifyOpts(X, Y, ${Lit({openObjects: false})})`);
Recognized keys: openObjects, openArrays, openMaps, openSets, loose, circular, ignoreFunctions, signedZero, symbols. See rules-system for the full table.
- Home — module overview.
-
compile-prolog — strict-Prolog tagged-template front-end (
prolog\...`,prologClause`...`,prologFile`). -
rules-system — generic logic-programming predicates (
unifyOpts, type tests, control flow, higher-order). -
rules-math — arithmetic predicates including
is/2,=:=,=\=. - rules-native — JS-native bridge predicates (Array / Map / Set / Date).
- Writing-rules — older hand-written rule encoding (still supported).
-
dev-docs/compiler-ir.md— IR design rationale, decisions, practical patterns. -
dev-docs/prolog-parser.md— strict-Prolog parser internals (Pratt, body-expr, goalize, helper rules).