×ばつ callback/generator). Useful for pattern matching with extraction, constraint search, type inference, planners, expert systems, and policy checks. ESM, single runtime dependency (deep6). - compile overview · uhop/yopl Wiki"> ×ばつ callback/generator). Useful for pattern matching with..." /> ×ばつ callback/generator). Useful for pattern matching with..." />×ばつ callback/generator). Useful for pattern matching with..." />
Skip to content

Navigation Menu

Sign in
Sign up

compile overview

Eugene Lazutkin edited this page May 9, 2026 · 4 revisions

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 a per-clause tagged-template DSL for writing rules declaratively.

front-end IR lower.js src/solve.js
───────────────── ───────────── ──────────────────── ─────────────
clause`...` Rule { function (...vars) { rules[name] =
 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.

Quick start

import solve from 'yopl';
import {variable as v} from 'deep6/unify.js';
import assemble from 'deep6/traverse/assemble.js';
import {rule, clause} from 'yopl/compile/clause/index.js';
import {lowerRules} from 'yopl/compile/lower.js';
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', [list, X], env => console.log(assemble(X, env)));
// 1, 2, 3

Compare to the hand-written encoding in Writing-rules — the compiler emits the right call(...) wrapping automatically and the validator catches arity drift at compile time.

The clause DSL

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

Inside a clause

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 auto-wrap

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')).

Caveat — strings can't span interpolation slots

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.

The Lit-walker

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/index.js';
import {Lit, Var} from 'yopl/compile/ir.js';
// 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.

IR constructors

Imported from yopl/compile/ir.js.

Terms

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).

Goals

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.

Structure

Constructor Returns
Clause(head, body?, vars?) {head, body, vars?}
Rule(name, arity, clauses) {name, arity, clauses}

Re-exports from deep6

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.

The Js factory shape

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 by cut(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)}`;

Lowering

Imported from yopl/compile/lower.js.

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.

Validation

Imported from yopl/compile/validate.js.

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.

Per-clause unification options

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.

See also

  • Home — module overview.
  • rules-system — generic logic-programming predicates (unifyOpts, type tests, control flow, higher-order).
  • rules-native — JS-native bridge predicates (Array / Map / Set / Date).
  • Writing-rules — older hand-written rule encoding (still supported).
  • dev-docs/compiler-ir.md — internal design rationale, decisions, practical patterns.

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /