Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions docs/WAM_JAVASCRIPT_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ Assoc (`library(assoc)` shape, list-of-pairs not AVL): **`empty_assoc/1`**,
**`assoc_to_list/2`**, **`assoc_to_keys/2`**.

Term: **`functor/3`**, **`arg/3`**, **`=../2`**, **`copy_term/2`**,
**`read_term_from_atom/2` `/3`**, **`atom_to_term/3`**, **`term_to_atom/2`**.
**`read_term_from_atom/2` `/3`**, **`atom_to_term/3`**, **`term_to_atom/2`**,
**`term_variables/2`**, **`numbervars/3`**, **`=@=/2`**, **`\=@=/2`**.

Metacall: **`\+/1`**, **`call/1`** (re-enter the same instruction loop /
builtin dispatch).
Expand Down Expand Up @@ -123,7 +124,9 @@ parser, not the bundled portable `compiled(prolog_term_parser)`.
|---|---|
| `bagof/3` | **Implemented.** ISO witness grouping (one bag per distinct free-var binding, SWI encounter order), `Var^Goal` / nested `V1^V2^Goal` stripped from the witness set, fails when Goal has no solutions. |
| `setof/3` | **Implemented.** `bagof` then per-group standard-order sort + dedup. Order: Var < Number < Atom < String < Compound; compounds by arity, functor **name**, then args L-to-R (matches SWI mixed-type lists). |
| `term_variables/2`, `numbervars/3`, `=@=/2` | Not ported. |
| `term_variables/2` | **Implemented.** Distinct unbound vars, first-occurrence L-to-R depth-first. Cyclic compounds are visited once (same `seen` walk as `copy_term`). |
| `numbervars/3` | **Implemented.** Binds+trails each distinct unbound var to `'$VAR'(N)` from Start; End is Start+count. `write/1` prints `'$VAR'(N)` literally — it does **not** letter-style SWI rendering (`A`, `B`, …). |
| `=@=/2` / `\=@=/2` | **Implemented.** Variant equality: ground as `==`; vars match via a consistent bijection. Cyclic struct pairs are treated as already-equal once seen. |
| `format/2` `/3` | **Implemented** for `~w ~a ~d ~p ~q ~n ~s ~t ~~`. Not ported: `~f`, `~r`, `~D`, positioning (`~N|`, `~+`, `t~`), aliases, and stream sinks other than stdout / `atom(A)` / `string(S)`. |
| `sub_atom/5` | **Implemented** when Atom is ground; enumerates unbound Before/Length/After (and filters a ground SubAtom). |
| `atom_string/2` / `split_string/4` | **Implemented** but the runtime has no distinct string tag — results intern as atoms (write/== match SWI for the probe suite). |
Expand Down Expand Up @@ -153,19 +156,21 @@ CONFORMANCE_TARGETS=javascript swipl -q -g run_tests -t halt \
tests/test_wam_cross_target_conformance.pl
```

Residual ISO corners not covered: `term_variables/2` / `numbervars/3` /
`=@=/2` are still unported; bagof/setof of *unbound* free vars (two
Residual ISO corners not covered: bagof/setof of *unbound* free vars (two
solutions that leave the same witness unbound) is grouped by copied
variable name rather than `@=`; the runtime has no distinct string tag,
so String vs Atom order is unused and `atom_string`/`split_string`
intern results as atoms; `^/2` as a standalone metacall just
runs the RHS; `format` does not implement `~f` / `~r` / column
positioning; assoc is a list-of-pairs, not SWI's AVL tree.
positioning; assoc is a list-of-pairs, not SWI's AVL tree;
`numbervars` does not letter-render `'$VAR'(N)` on `write/1`.

## Document status

Initial JS WAM bring-up + builtin port from Lua, ISO bagof/3 and setof/3,
first-argument indexing, the Tier-2 lowered emitter, ISO/library builtin
breadth (sort, lists, atom/string, format, assoc), the G-W2 runtime term
parser, then G-W4 file-backed fact sources (TSV/CSV/JSONL; LMDB/CSR out
of scope). Source-verified against SWI-Prolog as the oracle (2026-08-30).
parser, G-W4 file-backed fact sources (TSV/CSV/JSONL; LMDB/CSR out of
scope), then the G-W3 term-meta family (`term_variables/2`,
`numbervars/3`, `=@=/2`, `\=@=/2`). Source-verified against SWI-Prolog
as the oracle (2026-08-30).
5 changes: 5 additions & 0 deletions src/unifyweaver/bindings/javascript_wam_bindings.pl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
% ISO bagof/3 / setof/3 witness grouping and ^/2,
% plus the native Pratt parse_term reader
% (read_term_from_atom, atom_to_term, term_to_atom)
% and term-meta (term_variables, numbervars, =@=, \=@=)

:- module(javascript_wam_bindings, [
javascript_wam_builtin/3, % Name, Arity, Status
Expand Down Expand Up @@ -110,6 +111,10 @@
javascript_wam_builtin(read_term_from_atom, 3, implemented).
javascript_wam_builtin(atom_to_term, 3, implemented).
javascript_wam_builtin(term_to_atom, 2, implemented).
javascript_wam_builtin(term_variables, 2, implemented).
javascript_wam_builtin(numbervars, 3, implemented).
javascript_wam_builtin((=@=), 2, implemented).
javascript_wam_builtin((\=@=), 2, implemented).

javascript_wam_builtins(List) :-
findall(Name/Arity, javascript_wam_builtin(Name, Arity, _), List).
93 changes: 93 additions & 0 deletions templates/targets/javascript_wam/runtime.js.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,95 @@ function builtin_copy_term(_program, state) {
return bind_or_compare(state, 2, copied);
}

function collect_term_variables(state, term, acc, seenNames, seenStructs) {
term = Runtime.deref(state, term);
if (typeof term !== "object" || term === null) return;
if (term.tag === "unbound") {
if (!Object.prototype.hasOwnProperty.call(seenNames, term.name)) {
seenNames[term.name] = true;
acc.push(term);
}
return;
}
if (term.tag !== "struct") return;
if (seenStructs.has(term)) return;
seenStructs.add(term);
const args = term.args || [];
for (let i = 0; i < args.length; i++) {
collect_term_variables(state, args[i], acc, seenNames, seenStructs);
}
}

function builtin_term_variables(program, state) {
const vars = [];
collect_term_variables(
state, Runtime.get_reg(state, 1), vars, Object.create(null), new Set()
);
return bind_or_compare(
state, 2, Runtime.list_from_terms(vars, program.intern_table), program
);
}

function builtin_numbervars(program, state) {
const startV = Runtime.deref(state, Runtime.get_reg(state, 2));
if (typeof startV !== "object" || startV === null || startV.tag !== "int") {
return false;
}
const vars = [];
collect_term_variables(
state, Runtime.get_reg(state, 1), vars, Object.create(null), new Set()
);
const fid = Runtime.intern(program.intern_table, "$VAR");
let n = startV.val;
for (let i = 0; i < vars.length; i++) {
if (Runtime.unify(state, vars[i], V.Struct(fid, [V.Int(n)]), program) !== true) {
return false;
}
n += 1;
}
return bind_or_compare(state, 3, V.Int(n), program);
}

function variant_equal(program, state, a, b, leftToRight, rightToLeft, seen) {
a = Runtime.deref(state, a);
b = Runtime.deref(state, b);
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
return a === b;
}
if (a.tag === "unbound" && b.tag === "unbound") {
if (Object.prototype.hasOwnProperty.call(leftToRight, a.name)) {
return leftToRight[a.name] === b.name;
}
if (Object.prototype.hasOwnProperty.call(rightToLeft, b.name)) return false;
leftToRight[a.name] = b.name;
rightToLeft[b.name] = a.name;
return true;
}
if (a.tag === "unbound" || b.tag === "unbound") return false;
if (a.tag !== "struct") return same_atomic(a, b);
if (b.tag !== "struct") return false;
if (seen.has(a)) return seen.get(a) === b;
const aArity = (a.args || []).length;
const bArity = (b.args || []).length;
if (!same_functor(program, a.fid, aArity, b.fid, bArity)) return false;
seen.set(a, b);
for (let i = 0; i < aArity; i++) {
if (!variant_equal(program, state, a.args[i], b.args[i], leftToRight, rightToLeft, seen)) {
return false;
}
}
return true;
}

function builtin_variant(program, state, wantEqual) {
const ok = variant_equal(
program, state,
Runtime.get_reg(state, 1), Runtime.get_reg(state, 2),
Object.create(null), Object.create(null), new Map()
);
return wantEqual ? ok === true : ok !== true;
}

let invoke_goal;
let collect_goal_solutions;
let collect_goal_rows;
Expand Down Expand Up @@ -2376,6 +2465,10 @@ Runtime.builtin_call = function (program, state, inst) {
if (name === "arg") return builtin_arg(program, state);
if (name === "=..") return builtin_univ(program, state);
if (name === "copy_term") return builtin_copy_term(program, state);
if (name === "term_variables") return builtin_term_variables(program, state);
if (name === "numbervars") return builtin_numbervars(program, state);
if (name === "=@=") return builtin_variant(program, state, true);
if (name === "\\=@=") return builtin_variant(program, state, false);
if (name === "between") return builtin_between(program, state);
if (name === "is") {
const n = eval_arith(program, state, Runtime.get_reg(state, 2));
Expand Down
15 changes: 14 additions & 1 deletion tests/test_wam_javascript_builtins.pl
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
:- dynamic user:gt_float/1.
:- dynamic user:qatom/1.
:- dynamic user:probe_parse_atom/0.
:- dynamic user:probe_term_meta/0.

install_probes :-
retractall(user:probe_findall),
Expand Down Expand Up @@ -97,6 +98,7 @@
retractall(user:gt_float/1),
retractall(user:qatom/1),
retractall(user:probe_parse_atom),
retractall(user:probe_term_meta),
assertz((user:probe_findall :-
findall(X, member(X, [1,2,3]), L), write(L), nl, L == [1,2,3])),
assertz((user:probe_functor :-
Expand Down Expand Up @@ -235,6 +237,16 @@
read_term_from_atom('[a,b|c]', PL), PL == [a, b|c],
read_term_from_atom('1+2', SumT), SumT == +(1, 2),
atom_to_term('bar(X)', U, B), U = bar(hello), B == ['X'=hello],
write(ok), nl)),
assertz((user:probe_term_meta :-
term_variables(f(X, Y, X), L), L == [X, Y],
term_variables(foo(a), Empty), Empty == [],
numbervars(g(A, B), 0, E), E == 2,
A == '$VAR'(0), B == '$VAR'(1),
f(P, Q) =@= f(R, S),
f(T, T) \=@= f(U, V),
\+ (f(T, T) =@= f(U, V)),
foo(a) =@= foo(a),
write(ok), nl)).

probe_preds([
Expand Down Expand Up @@ -270,7 +282,8 @@
user:probe_atoms/0,
user:probe_format/0,
user:probe_assoc/0,
user:probe_parse_atom/0
user:probe_parse_atom/0,
user:probe_term_meta/0
]).

:- dynamic user:ctw_js/0.
Expand Down
Loading