-
-
Notifications
You must be signed in to change notification settings - Fork 0
Release notes
Long-form per-release changelog for yopl.
The repo's README.md carries cliff-notes for users skimming GitHub; this
page is the canonical history with internal-change context, motivation, and
credits.
Strict-Prolog front-end and IR-level performance dogfood. Largest follow-up since the 1.3.0 compiler MVP: a complete Prolog program parser shares the IR with the per-clause DSL; the IR + lowering + validation surface is now exposed via clean public subpaths; per-clause source positions thread through to validator issues and runtime fns; a parity bench surfaces the codegen opportunity.
- **
prolog\...`** (yopl/compile/prolog) — multi-clause program parser. Returns the lowered runtimeRulesdict by default with the parsed IR attached underSymbol.for('yopl.ir'); pass{lower: false}via the configurator to get the IR dict directly. Three call shapes (tag form, function form, configurator) lifted fromdollar-shell'sbqSpawn` polymorphic-tag pattern. - **
prologClause\...`** — single-clause variant; trailing.optional. Returns{name, head, body, helpers?}wherename` is the head functor. -
Body operators.
,(conjunction at 1000),;(disjunction at 1100, mints$or_<N>helper rules),->(if-then at 1050, mints$ite_<N>helpers),\+(negation at 900, desugars toCall('not', [G])). Cut inside;/->branches scopes opaquely to the helper — transparent cut deferred until a real use case appears. -
Goal aliases. Symbolic operators in body position auto-alias to
yopl runtime predicates:
=→eq,\=→notEq,==→eq,\==→notEq,<→lt,>→gt,=<→le,>=→ge. Applied at goalize time, not the op table — sofoo(X = Y)in arg position emitsCompound('=', ...)while top-levelX = Yin body emitsCall('eq', ...). -
op/3andop/4directives. ISO:- op(P, T, N).(3-arg) and yopl-canonical:- op(P, T, N, Pred).(4-arg with target alias). Per-invocation parser scope — directives mutate a clone of the input table, never the caller's. -
Bare lowercase atoms in arg position parse as
Lit(name)(string-atom literals), matching yopl's runtime convention. The per-clauseclause\...`` DSL still rejects bare atoms. -
All five built-in rule modules ported (
system,comp,math,bits,logic) — the existing 535-test suite now exercises both front-ends through the samerulesexports. -
IR-equivalence dogfood (
tests/test-prolog-dogfood.js) — for a representative sample of patterns, parses each rule both ways and deep-equals the IR (Js.factorycompared by===). Makes the "iter-2 IR ≡ iter-1 IR" design promise tactile.
Filesystem-backed convenience loaders at yopl/compile/prolog/file:
import {prologFile, prologFileAsync} from 'yopl/compile/prolog/file'; const rules = prologFile(new URL('./family.pl', import.meta.url)); const [a, b] = await Promise.all([prologFileAsync(urlA), prologFileAsync(urlB)]);
Both wrap node:fs readFileSync / fs/promises.readFile. Lives in
its own subpath so browser bundles without filesystem access don't
pull in node:fs. Works in Node, Bun, and Deno (Deno via its
Node-compat layer; same module loads unmodified in all three).
When sourceMap: true is passed, file defaults to the URL/path.
Three explicit subpath exports replace the older
yopl/compile/<file>.js deep imports (which still resolve via the
wildcard fallback):
| Subpath | Surface |
|---|---|
yopl/compile |
IR constructors + lowerRule / lowerRules + validate / validateOrThrow + the IR symbol + open / soft / _ / any from deep6. |
yopl/compile/clause |
Per-clause DSL: rule, clause. |
yopl/compile/prolog |
Strict-Prolog tags: prolog, prologClause. |
yopl/compile/prolog/file |
prologFile, prologFileAsync. |
Full TypeScript typings for each. Backfilled the IR symbol export
in yopl/compile/ir.d.ts (was emitted at runtime but missing from
the typings).
Clause IR carries an optional source: {file?, line, col} field.
The lexer always tracks positions (transient memory, GC'd after
parse); attaching the metadata to clauses is opt-in via the prolog /
prologClause configurator's sourceMap: true flag — defaults to
false so production deployments skip the per-clause cost
(~50 bytes / clause permanent memory; ~280 KB on a 5000-clause
program).
When on:
- Both front-ends populate
Clause.sourcefrom the lexer's position tracker. - The
fileconfigurator option (silently ignored whensourceMapis off) populatessource.file.prologFile/prologFileAsyncdefaultfileto the URL. -
lowerClauseattachessourceto the runtime fn as a non-enumerable property — error reporters can introspect it without disturbing JSON serialization of the rules dict. -
validateincludessourceon per-clauseIssueobjects and appends a[file:line:col]suffix to the issue'smessage.
The per-clause clause\...`` DSL keeps source always-on (each tag is
one clause; cost is negligible).
-
is/2(math.js).evalExpr(term, env)recursively walks the RHS term tree, dispatching compound nodes toARITH_BINARY/ARITH_UNARYlookup tables. Throws on insufficiently-instantiated subterms (matches ISO Prolog'sinstantiation_error). Op-table additions:is(700 xfx),mod(400 yfx),//(400 yfx). Ops covered: binary+,-,*,/,//,mod,min,max; unary+,-,abs,sqrt,floor,ceiling,round,sign. -
=:=/=\=(math.js). Both sides evaluated as arithmetic expressions, then compared with===. Distinct from unification (=/eq) —X =:= 3 + 4evaluates RHS arithmetically;X = 3 + 4unifiesXwith the compound term3 + 4. Removed the previous'=:='→'eq'and'=\\='→'notEq'GOAL_ALIASES entries (which silently failed for compound expressions).
lower.js now lowers Wild() to variable() per occurrence (a fresh
deep6 Variable each time), matching standard Prolog's
fresh-per-occurrence _ semantics. Bench-neutral on all three
existing benches. The previous "yopl _ ≠ Prolog _" gap is closed —
encodings can use _ as a fresh anonymous variable rather than naming
every slot. See tests/test-zebra.js for the simplification this
unlocked (50+ named workarounds → bare _).
Two-commit fix (fcdd545 v1, 235022c v2 — "bugfix of bugfix"). A
pre-existing bug in prove: goals.terms[goals.index++] permanently
advanced index; on backtracking-then-rematch the outer goals
walked up via .next to a stale (downstream-advanced) index, silently
skipping body goals that should have been re-processed under the new
bindings. Surfaced via the zebra-puzzle dogfood; latent because the
existing 450-test suite didn't exercise the specific pattern.
Fix v2 stamps restoreParent onto each new sub-goals at match time;
the walk-up loop propagates the reset up the .next chain every time
it climbs from child to parent. Regression test
tests/test-solve-backtrack.js covers all four solvers (8
assertions). Tests grew 450 → 461 with the regression test +
test-zebra.js added.
Bumped deep6 ^1.3.0 → ^1.3.1 (1.3.1 fixed instanceof Env
narrowing in assemble / clone / deref that had been silently
dropping EnvMap); swapped new Env() → new EnvMap() in
src/solve.js and the three driver files. Bench results
(paired against the pre-swap baseline): ×ばつ speedup across
bench-proof-loop.js and bench-drivers.js; ×ばつ on
bench-inline-goals.js (after the cycle TDZ fix below).
bench-inline-goals.js failed to load with Cannot access 'lowerRules' before initialization because its entry point pulled
math.js first, triggering the
math.js → lower.js → system.js → lower.js cycle from a position
that left system.js's body needing lowerRules at module-eval
time. Fix: extracted call, cut, fail, halt (the runtime
primitives lower.js actually needs from system.js) into a leaf
module src/rules/system-runtime.{js,d.ts} that depends only on
deep6/env.js. Repointed lower.js's import. system.js re-exports
the four primitives so the public API surface is preserved.
Encodes member/2 and append/3 four ways (hand-written, codegen,
clause, prolog) and runs the same workloads through each via the
same solve driver. Two assertions:
-
_clause≈_prologwithin bench noise — IR equivalence made tactile at the runtime layer (the IR-level dogfood already asserts it at parse time). -
_codegen≈_handfor wildcard-free rules (append10). Direct evidence that an IR→JS codegen lowering would close the ~13–28% compiler-overhead gap on member workloads. The residual on member is the wildcard-allocation difference (hand-written omits thevalueproperty; the currentlower.jsalways emits a freshvariable()) — recoverable via a "smart codegen" that detects value-position wildcards.
The bench's job is to detect new gaps opening up, not to celebrate the existing compiler-overhead floor.
Six new test files spanning the canonical shapes — Hanoi (Lucas 1883, recursion + move list); Wolf/Goat/Cabbage (Alcuin ~800 AD, state-space search); naïve quicksort (Hoare 1961 algorithm); Knight's Tour (al-Adli 9th c. — partial naïve + full Warnsdorff with cut-and-commit, 24 deterministic calls / ~13 ms for full ×ばつ5); N-queens (Bezzel 1848, 4 / 6 / 8 boards); ×ばつ4 mini-Sudoku (unique- and two-solution puzzles). All public-domain provenance documented in test headers; encodings original. Tests grew 461 → 535 (+74 assertions, +7 files).
The Knight's Tour with Warnsdorff demonstrates the heuristic-driven
search pattern: inline-JS goal walks the visited cons-list, computes
onward-degree per candidate, returns a sorted cons-list of pair
compounds; Prolog commits with Sorted = [pair(X2, Y2) | _], !.
Generalizable to any best-first search.
Tests grew 258 → 666 across iter-2 (+408): parse primitives, Pratt, parseClause, parseProgram, polymorphic-tag, body operators, disjunction, if-then-else, IR equivalence, source maps, public-export resolution, prologFile loading, classic-puzzle suite, regression coverage for the solver bug, plus the EnvMap swap stayed green through it all.
Rule compiler and JS-native bridge library. The largest feature release
since the 1.2.0 ESM repackage. Rules can now be written declaratively via a
tagged-template DSL; the IR is reusable across front-ends; a new predicate
library bridges yopl unification to plain JS objects, arrays, Map, Set,
and Date.
-
Rule compiler (
src/compile/). Pure-data IR with 5 Term kinds (var,wildcard,literal,cons,compound) and 4 Goal kinds (call,cut,fail,js), plusClauseandRule. Lowering is the only place that knows the runtime rule shape; front-ends emit IR.-
Per-clause front-end (
compile/clause/). Tagged-template DSL:Uppercase identifiers are logic Variables;rule('member', 2)(clause`(X, [X | _])`, clause`(X, [_ | T]) :- member(X, T)`);
_is the wildcard;[X | T]is cons-list sugar;${...}interpolates IR or auto-wraps primitives / functions. -
Validator (
compile/validate.js) catchesarity-mismatch,call-arity-mismatch,undeclared-var,duplicate-rule, and (opt-in)unresolved-rule. -
All five existing rule modules (
system,comp,math,bits,logic) are dogfooded through the compiler.
-
Per-clause front-end (
-
Lit-walker.Lit(value)lowers per activation, descending into plain objects and arrays and substituting nested IR nodes (Var,Wild,Cons,Compound,Lit) with the activation's fresh logic Variables. SoLit({age: Var('A')})doubles as a pattern matcher and a constructor.Maps,Sets,Dates, andWrap-wrapped values pass through verbatim. IR detection is gated on the closedIR_KINDSset so user objects with a domainkindfield aren't misread. -
unifyOpts/3(insystem.js).unifyOpts(X, Y, Opts)runs deep6's unification with a per-call options bag ({openObjects, openArrays, openMaps, openSets, circular, loose, ignoreFunctions, signedZero, symbols}). The env's baselineoptionsis restored before the goal returns. From clause source, pass options as${Lit({openArrays: true})}. -
yopl/rules/native.js— JS-native bridge predicates split out fromsystem.jsso the latter stays focused on generic logic-programming.-
Type tests —
isArray,isMap,isSet,isDate. (isArraymoved out ofsystem.jsto live with its siblings.) -
Array —
arrayList(A, L)(bidir array ↔ cons list),arrayGet(A, I, X)(forward indexed lookup),arraySet(A, I, X, A2)(immutable single-index override; in-bounds replace or append-at-end),arrayLength(A, N). -
Map —
mapEntries(M, Es)(bidir; both-bound case is order-independent),mapGet(M, K, V),mapHas(M, K). -
Set —
setItems(S, Items)(bidir),setHas(S, X). -
Date —
dateTimestamp(D, Ms)(bidir; TZ-agnostic),dateComponents(D, C)anddateComponentsUTC(D, C)(bidir; component bag{year, month, day, hour, minute, second, ms}, month 0-based; pairs cleanly with theLit-walker for partial-bag pattern matching).
-
Type tests —
-
bitOrreverse-mode identities.bitOr(0, Y, Z)andbitOr(X, 0, Z)now answerZ = Y/Z = Xrespectively via(0, Y, Y)and(X, 0, X)fact clauses, mirroring the existingbitXoridentity-fact pattern.forwardTernarycuts only on the modes it actually resolves (verify-all-3 + forward) so identity facts can fire on under-specified reverse queries without duplicating forward solutions. -
IR re-exports.
yopl/compile/ir.jsre-exportsopen,soft, and_(also asany) fromdeep6for fine-grained per-value match control:Lit(open({tag: 'a'}))locks subset matching regardless of env options;Lit(soft({...}))extends both sides. -
Var()mints anonymous Variables.Var()(no name argument) returns{kind: 'var', name: Symbol()}— useful for IR fragments built in JS code that need a guaranteed-unique identity. Mirrors deep6'svariable()idiom. -
Bench scaffold (
bench/). Top-levelbench-{proof-loop,drivers, inline-goals}.jsscripts usingnano-benchmarkas a dev dep. Run vianpm run bench(one-shot) ornpm run bench:watch. -
Regex-based clause tokenizer modelled on
stream-json's sticky- regex lexing — the V8 RegExp engine eats whole lexemes in native code rather than the JS interpreter walking char-by-char. Functionally equivalent to the previous lexer; faster on the multi-KB inputs the upcoming Prolog front-end will need. -
Design docs. New
dev-docs/compiler-ir.md(IR design, decisions,Lit-walker,unifyOpts/3, practical patterns) anddev-docs/native-objects.md(Array / Map / Set / Date roadmap, what's shipped vs postponed). -
Junk audit. Removed
AUTHORS(single-line duplicate ofpackage.json#author) andCODEBASE.md(redundant withARCHITECTURE.md).
Test count grew from 152 → 258 across the IR / lower / validate / clause
front-end / native bridges / Lit-walker. npm test is green; npm run ts-check clean; npm run lint clean; bench/bench-proof-loop.js runs
in the same ballpark as the pre-compiler hand-written rules.
ESM-only repackage and test/docs overhaul, the largest release since the extraction of deep6 at 1.1.0.
-
CommonJS build removed. yopl ships as pure ESM. CJS consumers can use
Node's built-in dynamic
import(); a smoke-test demonstrating the interop pattern lives attests/test-cjs.cjsand runs vianode tests/test-cjs.cjs. -
TypeScript typings added. Hand-written
.d.tssidecars for every source module.npm run ts-checkrunstsc --noEmitagainst the typings plus a dedicated TypeScript surface test (tests/test-types.ts). -
Tests restructured. Per-driver test files
(
test-{solve,gen,async,asyncGen}.js) split out of a monolithic suite, plus dedicatedtest-system.jsandtest-rules.jsfor the predicate libraries. Run withnpm test. -
Simplified list creation. Helper exports —
head,term,list,listHead,rest— replace the inlined object-literal patterns common in older yopl rule definitions. The wiki's Writing rules page documents the new shapes. - Bug fixes and perf improvements in the proof loop and the rule libraries.
-
Expanded docs.
ARCHITECTURE.md,llms.txt,llms-full.txt,AGENTS.md, plus per-module wiki pages (solve, solvers-gen, solvers-async, solvers-asyncGen, rules-system, rules-comp, rules-math, rules-bits, rules-logic).
Cliff-notes preserved verbatim from README.md for releases that predate
this long-form page:
- 1.1.4 — updated dependencies.
- 1.1.3 — updated dependencies.
- 1.1.2 — updated dependencies.
- 1.1.1 — updated dependencies.
- 1.1.0 — deep6 was extracted from this package and is now a dependency.
-
1.0.1 — added the
exportsstatement. - 1.0.0 — first 1.0 release.