diff --git a/docs/WAM_JAVASCRIPT_STATUS.md b/docs/WAM_JAVASCRIPT_STATUS.md index 9ddf91fe9..27e23e052 100644 --- a/docs/WAM_JAVASCRIPT_STATUS.md +++ b/docs/WAM_JAVASCRIPT_STATUS.md @@ -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). @@ -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). | @@ -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). diff --git a/src/unifyweaver/bindings/javascript_wam_bindings.pl b/src/unifyweaver/bindings/javascript_wam_bindings.pl index c744f608a..47ac506e1 100644 --- a/src/unifyweaver/bindings/javascript_wam_bindings.pl +++ b/src/unifyweaver/bindings/javascript_wam_bindings.pl @@ -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 @@ -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). diff --git a/templates/targets/javascript_wam/runtime.js.mustache b/templates/targets/javascript_wam/runtime.js.mustache index cb4c868af..60d1807a9 100644 --- a/templates/targets/javascript_wam/runtime.js.mustache +++ b/templates/targets/javascript_wam/runtime.js.mustache @@ -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; @@ -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)); diff --git a/tests/test_wam_javascript_builtins.pl b/tests/test_wam_javascript_builtins.pl index 09a1cf25f..f1b044dd2 100644 --- a/tests/test_wam_javascript_builtins.pl +++ b/tests/test_wam_javascript_builtins.pl @@ -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), @@ -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 :- @@ -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([ @@ -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.