-
-
Notifications
You must be signed in to change notification settings - Fork 0
compile prolog
The strict-Prolog tagged-template front-end parses standard Prolog
source into the same IR the per-clause compile-overview
DSL produces. Two tags, one whole-program parser, one single-clause
parser; both share the lexer + Pratt machinery in src/compile/parse/
and lower through the same lower.js.
The internal design (Pratt parser, goalize, helper-rule minting for
;/->, polymorphic-tag scaffolding) is in
dev-docs/prolog-parser.md.
This page is the user-facing reference.
import solve from 'yopl'; import {variable as v} from 'deep6/env.js'; import assemble from 'deep6/traverse/assemble.js'; import {prolog} from 'yopl/compile/prolog'; import {rules as systemRules} from 'yopl/rules/system.js'; const rules = { ...systemRules, ...prolog` member(X, [X | _]). member(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
The tag returns the lowered runtime Rules dict ready to spread into
the rule database. The parsed IR is also attached under
Symbol.for('yopl.ir') (importable as IR from yopl/compile) for
inspection / cross-validation / codegen.
Parse a multi-clause program. Returns the lowered Rules dict by
default; pass {lower: false} (via the configurator form below) to
get the IR Rules dict instead.
import {prolog} from 'yopl/compile/prolog'; const rules = prolog` parent(tom, bob). parent(tom, liz). parent(bob, ann). ancestor(X, Y) :- parent(X, Y). ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y). `;
Parse a single clause. Useful for incrementally building rule libraries
or for IR inspection. The trailing . is auto-appended if absent.
Returns {name, head, body, helpers?} — name is the head functor;
helpers is present when the body produced $or_N / $ite_N helper
rules (see Body operators below).
import {prologClause} from 'yopl/compile/prolog'; prologClause`green`; // → {name: 'green', head: [], body: []} prologClause`member(X, [X | _])`; // → {name: 'member', head: [Var('X'), Cons(Var('X'), Wild())], body: []}
Both tags are polymorphic — the same identifier handles tag form,
function form, and configurator form (lifted from
dollar-shell's
bqSpawn).
prolog`source`; // tagged template — parses the literal
Parses a string. Useful for source already in a variable, e.g. file
content. (For loading files from disk, prefer the dedicated
prologFile loader below.)
const src = ` parent(tom, bob). parent(bob, ann). `; const rules = prolog(src); const ir = prolog(src, {lower: false});
Returns a fresh tag closing over merged options. Per-invocation scope — options never leak to the next call.
const irOnly = prolog.with({lower: false}); const ir = irOnly`foo(X) :- bar(X).`; // Equivalent: const ir2 = prolog({lower: false})`foo(X) :- bar(X).`;
Configurator (prolog(opts) / prolog.with(opts)) accepts:
| Option | Type | Default | Effect |
|---|---|---|---|
lower |
boolean | true |
When false, the tag returns the IR Rules dict instead of the lowered runtime fns dict. |
operators |
PrologOpDecl[] |
— | Extra operators added on top of the default body op table (per-invocation scope). |
sourceMap |
boolean | false |
When true, attach source: {file?, line, col} metadata to each parsed Clause IR. See Source maps. |
file |
string | — | Source file path / URL to attach to each clause's source.file. Only takes effect when sourceMap is true. |
Filesystem-backed convenience loaders for .pl files. Live in their
own subpath so browser bundles without filesystem access don't pull
in node:fs.
import {prologFile, prologFileAsync} from 'yopl/compile/prolog/file.js'; const rules = prologFile(new URL('./family.pl', import.meta.url)); // Async — right default for parallel loading via Promise.all: const [a, b] = await Promise.all([prologFileAsync(urlA), prologFileAsync(urlB)]);
Both wrap node:fs readFileSync / fs/promises.readFile and forward
to prolog(source, options). UTF-8 is assumed. Works in Node, Bun,
and Deno (Deno via its Node-compat layer).
When sourceMap: true is passed, file defaults to url.href (or the
string path) so source positions carry the URL through to validator
issues and runtime error reports. Override by setting options.file
explicitly.
Beyond what the per-clause clause\...``
DSL supports, body context recognizes operator-precedence syntax:
| Op | Priority | Type | Meaning |
|---|---|---|---|
, |
1000 | xfy | Conjunction (sequencing). |
; |
1100 | xfy | Disjunction — mints a fresh $or_<N> helper rule per disjunction site. |
-> |
1050 | xfy | If-then — mints $ite_<N> helper. With ; for else: Cond -> Then ; Else. |
\+ |
900 | fy | Negation as failure — desugars to Call('not', [G]) (target alias to not/1). |
Disjunction example:
prolog` greet(X) :- (X = alice ; X = bob), write(X). `; // transparent disjunction; the parser mints a $or_<N> helper rule and // rewrites the body to call it
Cut inside ; / -> branches scopes opaquely to the helper —
transparent cut requires a dedicated Disjunction IR kind and is
deferred until a real use case appears.
Symbolic operators in body position auto-alias to yopl runtime
predicates (applied at goalize time, not in the op table — so
foo(X = Y) in arg position emits Compound('=', ...) while top-level
X = Y in body emits Call('eq', ...)):
| Source op | Body call |
|---|---|
= |
eq |
\= |
notEq |
== |
eq |
\== |
notEq |
< |
lt |
> |
gt |
=< |
le |
>= |
ge |
Other body-position operators (e.g. is, =:=, =\= from
rules-math) are regular rules and resolve through the
normal call mechanism.
Define custom operators per-invocation via op/3 (ISO) or op/4
(yopl-canonical aliasing) directives:
prolog` :- op(700, xfx, =>). % ISO form — emits Compound('=>', [A, B]) :- op(700, xfx, ===, eq). % op/4 form — body-position rewrites to Call('eq', [A, B]) triple(X, Y, Z) :- X => Y, Y === Z. `;
Or pre-load them via the configurator's operators option:
const tag = prolog.with({ operators: [ {priority: 700, type: 'xfx', name: '=>'}, {priority: 700, type: 'xfx', name: '===', target: 'eq'} ] }); const rules = tag` triple(X, Y, Z) :- X => Y, Y === Z. `;
PrologOpDecl shape: {name: string, priority: number, type: 'xfx' | 'xfy' | 'yfx' | 'fx' | 'fy', target?: string}.
Per-invocation scope: directives mutate a clone of the input op table;
the caller's table is never touched, so each prolog\...`` evaluation
starts from a clean slate.
In arg position, bare lowercase identifiers parse as Lit(name)
(string-atom literals) — matches yopl's runtime convention where atoms
are JS strings:
prolog`color(red).`; // → {color: Rule with head [Lit('red')]}
The per-clause clause\...`DSL rejects bare atoms (must quote as"red"or useLit('red')` interpolation); the strict-Prolog parser
accepts them per ISO conventions.
When sourceMap: true is set on the configurator, each parsed
Clause IR carries an optional source: {file?, line, col}
annotation pointing at the first token of the clause head. The
lowered runtime fn carries source as a non-enumerable property;
validator issues for per-clause errors carry it on the Issue object
and tag the human-readable message with [file:line:col].
import {prolog} from 'yopl/compile/prolog'; import {validate} from 'yopl/compile'; const ir = prolog.with({lower: false, sourceMap: true, file: 'rules.pl'})` member(X, [X | _]). member(X, [_ | T]) :- member(X, T). `; ir.member.clauses[0].source; // → {file: 'rules.pl', line: 2, col: 3}
Default false to skip the per-clause memory cost (~50 bytes per
clause) in production. Flip on under a debug gate during development.
The per-clause clause\...`` DSL always carries source (negligible
cost per single-clause tag).
For the shared subset, prolog\...`and the per-clauseclause`...`DSL emit byte-identical IR. Thetests/test-prolog-dogfood.jssuite asserts this across representative patterns (simple facts, multi-clause rules with cut + fail, list patterns, dynamic dispatch, inline JS factories). Thebench/bench-parity.js` workload confirms behavioral parity at the
runtime level.
The prolog form additionally handles body operators, op/3 / op/4
directives, goal aliases, bare lowercase atoms in arg positions, and
file-name source-mapping — features the per-clause DSL doesn't expose
because they only make sense at the program level.
-
compile-overview — IR + lowering + validation;
the per-clause
clause\...`` DSL. -
dev-docs/prolog-parser.md— internal design (Pratt, body-expr, goalize, helper rules, polymorphic-tag scaffolding). -
dev-docs/compiler-ir.md— IR shapes and lowering rationale. -
rules-math —
is/2,=:=,=\=arithmetic predicates that compose with the strict-Prolog body parser.