Skip to content

rules native

Eugene Lazutkin edited this page Jul 13, 2026 · 3 revisions

rules-native

Predicates that bridge yopl unification to JS-native built-in types — Array, Map, Set, Date. Kept separate from rules-system so the latter stays focused on generic logic-programming primitives.

See dev-docs/native-objects.md for the full design and roadmap (what's shipped vs postponed).

Import

import {rules as nativeRules} from 'yopl/rules/native.js';

// Compose with the rest of the library:
import {rules as systemRules} from 'yopl/rules/system.js';
const rules = {...systemRules, ...nativeRules};

Type predicates

Predicate Succeeds when
isArray(X) X is bound and is a JS Array
isMap(X) X is bound and is a Map instance
isSet(X) X is bound and is a Set instance
isDate(X) X is bound and is a Date instance

All four require their argument to be bound; otherwise they fail.

solve(rules, 'isArray', [[1, 2]], () => console.log('array'));
solve(rules, 'isMap', [new Map()], () => console.log('map'));

Array

arrayList(A, L) — bidirectional Array ↔ cons list

Bind whichever side is unbound. Both bound → structural unification. Both unbound → fail.

const L = variable('L');
solve(rules, 'arrayList', [[1, 2, 3], L], env => assemble(L, env));
// → {value: 1, next: {value: 2, next: {value: 3, next: null}}}

const A = variable('A');
solve(rules, 'arrayList', [A, makeList([10, 20])], env => assemble(A, env));
// → [10, 20]

The list-to-array direction fails on improper lists (non-null, non-cons tail) and on lists with unbound-Variable tails (open lists).

arrayGet(A, I, X) — forward indexed lookup

A and I must be bound; I must be a non-negative integer in range. Out-of-bounds fails. With X also bound, behaves as a check.

const X = variable('X');
solve(rules, 'arrayGet', [[10, 20, 30], 1, X], env => assemble(X, env));
// → 20

solve(rules, 'arrayGet', [[10, 20, 30], 0, 10], () => console.log('match'));
// → match

arraySet(A, I, X, A2) — immutable single-index override

A, I, X bound; A2 is a new array [...A] with A2[I] = X. I may be in-bounds (replace) or exactly A.length (append-at-end); larger or negative I is rejected to prevent silent hole creation.

The original array is not mutated.

const Out = variable('Out');
solve(rules, 'arraySet', [[10, 20, 30], 1, 99, Out], env => assemble(Out, env));
// → [10, 99, 30]

For multiple overrides, chain calls or build the new array inside a Js body goal.

arrayLength(A, N) — forward length

A bound → N binds to A.length. Reverse mode (build an array of N holes) is intentionally unsupported.

Map

mapEntries(M, Es) — bidirectional Map ↔ list of [K, V] pairs

Bind whichever side is unbound. When Es is bound, the entries are materialized into a Map and unified against M as Maps (order-independent). When only M is bound, entries emit in M's insertion order.

const Es = variable('Es');
const m = new Map([
  ['a', 1],
  ['b', 2]
]);
solve(rules, 'mapEntries', [m, Es], env => assemble(Es, env));
// → cons-list of [['a', 1], ['b', 2]]

mapGet(M, K, V) — forward lookup

M and K must be bound; V binds to M.get(K) if K ∈ M. Reverse-mode (find K from V) is non-deterministic in JS Maps and not supported.

mapHas(M, K) — membership predicate

Both M and K must be bound.

Set

setItems(S, Items) — bidirectional Set ↔ list

Same shape as mapEntries. When Items is bound, builds a Set and unifies as Sets (order-independent).

setHas(S, X) — membership predicate

Both S and X must be bound.

Date

Component bag C is a JS object with optional fields:

{year?, month?, day?, hour?, minute?, second?, ms?}

Month is 0-based (matching JS Date.prototype.getMonth). When constructing, missing fields default to 1970/0/1/0/0/0/0.

dateTimestamp(D, Ms) — bidirectional D ↔ epoch ms

TZ-agnostic. D bound → Ms = D.getTime(). Ms bound → D = new Date(Ms).

dateComponents(D, C) — bidirectional D ↔ component bag (local time)

D bound → C is filled with the local-time components. C bound → D = new Date(year, month, day, ...) constructed from local-time components.

const Y = variable('Y'),
  M = variable('M');
const d = new Date(2026, 4, 9); // May 9 2026 local
solve(rules, 'dateComponents', [d, {year: Y, month: M}], env => {
  console.log(assemble(Y, env), assemble(M, env)); // 2026 4
});

dateComponentsUTC(D, C) — bidirectional D ↔ component bag (UTC)

Same as dateComponents but uses getUTCFullYear / getUTCMonth / … and Date.UTC(...). Recommended default for logic programs that should produce TZ-independent answers.

Pairs cleanly with the Lit-walker

The component bag is partial — only the fields you care about. Combined with the Lit-walker, partial-bag pattern matching is declarative:

import {rule, clause} from 'yopl/compile/clause.js';
import {Lit, Var, lowerRules} from 'yopl/compile';

const myRules = lowerRules([rule('extractYear', 2)(clause`(D, Y) :- dateComponentsUTC(D, ${Lit({year: Var('Y')})})`)]);

See also

Clone this wiki locally