From d6bde575a8928dc4c865c6a88c6db2c9dee44f1a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 30 Aug 2026 20:12:51 +0000 Subject: [PATCH 1/3] Add a distinct string term tag to the JS WAM runtime Introduce V.String and thread it through unify, ==, copy_term, write, compare/sort (Atom < String < Compound). String-producing builtins (atom_string, string_concat, string_chars, string_to_atom, number_string, split_string) now yield strings; compiled Prolog string constants emit as V.String. Co-authored-by: johns243a --- docs/WAM_JAVASCRIPT_STATUS.md | 29 +++-- .../bindings/javascript_wam_bindings.pl | 3 + .../targets/wam_javascript_target.pl | 8 ++ .../javascript_wam/runtime.js.mustache | 121 ++++++++++++++---- tests/test_wam_javascript_builtins.pl | 16 ++- 5 files changed, 142 insertions(+), 35 deletions(-) diff --git a/docs/WAM_JAVASCRIPT_STATUS.md b/docs/WAM_JAVASCRIPT_STATUS.md index bd8d68d84..f31158789 100644 --- a/docs/WAM_JAVASCRIPT_STATUS.md +++ b/docs/WAM_JAVASCRIPT_STATUS.md @@ -63,8 +63,11 @@ Key, `@<`/`@>`/`<`/`>`), **`predsort/3`** (`compare/3` and 3-arg callables), Atom/string: **`atom_concat/3`**, **`string_concat/3`**, **`atom_length/2`**, **`atom_chars/2`**, **`string_chars/2`**, **`atom_codes/2`**, **`char_code/2`**, **`sub_atom/5`** (ground-Atom; enumerates unbound Before/Length/After), -**`atom_string/2`**, **`number_codes/2`**, **`number_string/2`**, -**`split_string/4`**, **`upcase_atom/2`**, **`downcase_atom/2`**. +**`atom_string/2`**, **`string_to_atom/2`**, **`string/1`**, +**`number_codes/2`**, **`number_string/2`**, **`split_string/4`**, +**`upcase_atom/2`**, **`downcase_atom/2`**. Distinct **`string` tag** +(`V.String`): string-producing builtins yield strings; `atom/1` is +false for them. `write/1` prints the text; `format` `~q` quotes with `"`. I/O: `write/1`, `nl/0`, `writeln/1`, **`format/2`**, **`format/3`** (`~w ~a ~d ~p ~q ~n ~s ~t ~~`; `atom(A)` / `string(S)` sinks), **`tab/1`**. @@ -86,8 +89,8 @@ builtin metacall), **`bagof/3`** / **`setof/3`** (ISO free-var grouping, `^/2` existential quantification, empty-goal failure), **`aggregate_all/3`** for `count` / `sum(X)` / `bag(X)` / `set(X)`. -Types: `atom/1`, `integer/1`, `float/1`, `number/1`, `compound/1`, `var/1`, -`nonvar/1`, `is_list/1`, `ground/1`. +Types: `atom/1`, `integer/1`, `float/1`, `number/1`, `string/1`, `compound/1`, +`var/1`, `nonvar/1`, `is_list/1`, `ground/1`. ## Runtime term parser (G-W2) @@ -97,7 +100,8 @@ goes through `parse_cli_atom_or_int`, which now calls the same reader (unreadable text still interned as an atom). **Full:** integers (including a leading `-` after start/`(`/`[`/`,`/`|`), -floats (`3.14`, `-1.5`, `1.0e2`), bare atoms, quoted atoms (`'hi there'` +floats (`3.14`, `-1.5`, `1.0e2`), bare atoms, quoted atoms (`'hi there'`), +double-quoted strings (`"hi"` → string tag), with `\'` / `\\` escapes), variables (`X`, `_`, shared names), `[]`, proper lists `[a,b,c]`, partial lists `[H|T]`, compounds `foo(a, bar(b), 3)`, and parentheses. Cons intern as `[|]/2` + the `[]` @@ -139,7 +143,7 @@ parser, not the bundled portable `compiled(prolog_term_parser)`. | `=@=/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). | +| String term tag | **Implemented.** `V.String` is a distinct tag. Unify/`==` require equal strings (not atoms). Standard order / `compare/3` / `sort`: Var < Number < Atom < **String** < Compound. `atom_string/2`, `string_concat/3`, `string_chars/2` (construct), `string_to_atom/2`, `number_string/2`, `split_string/4` produce strings. `string/1` is true only for the tag. `write/1` prints text; `~q` quotes with `"`. Fact-source JSON/TSV values still intern as atoms. | | `library(assoc)` | **Implemented** as a Prolog `assoc/1` list of Key-Value pairs (not SWI's AVL tree). get/put/list/keys match SWI for unique-key maps. | | First-arg indexing | **Implemented.** `switch_on_constant` / `_fallthrough` / `_a2`, `switch_on_structure` / `_a2`, and `switch_on_term` / `_a2` jump to the matching clause group. Ground first-arg with a unique clause leaves no choice point (`deterministic/0`). Unbound first arg falls through to the try/retry/trust chain (no lost solutions). Exclusive miss fails; fallthrough variants keep the chain for variable-headed clauses. Dedicated `try`/`retry`/`trust` dispatch chains are emitted for multi-clause groups. | | Second-arg / deep indexing | A2 switches are implemented; deep (argument >2) indexing is not. | @@ -169,14 +173,14 @@ CONFORMANCE_TARGETS=javascript swipl -q -g run_tests -t halt \ 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 +variable name rather than `@=`; `^/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; `numbervars` does not letter-render `'$VAR'(N)` on `write/1`; `op/3` is process-global (no module-local ops) and `current_op/3` is -not implemented. +not implemented; `writeq/1` as a standalone builtin is not registered +(quoted string rendering is via `format` `~q`); fact-source cells stay +atoms even when the host file looks like a quoted string. ## Document status @@ -186,5 +190,6 @@ breadth (sort, lists, atom/string, format, assoc), the G-W2 runtime term parser, G-W4 file-backed fact sources (TSV/CSV/JSONL; LMDB/CSR out of scope), the G-W3 term-meta family (`term_variables/2`, `numbervars/3`, `=@=/2`, `\=@=/2`), then G-W2 `op/3` (dynamic Pratt -table: infix + prefix + postfix). Source-verified against SWI-Prolog -as the oracle (2026-08-30). +table: infix + prefix + postfix), then a distinct string term tag +(`V.String`; string-producing builtins + standard order). +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 c6d01c66b..ab53bd9e5 100644 --- a/src/unifyweaver/bindings/javascript_wam_bindings.pl +++ b/src/unifyweaver/bindings/javascript_wam_bindings.pl @@ -13,6 +13,7 @@ % (read_term_from_atom, atom_to_term, term_to_atom) % and term-meta (term_variables, numbervars, =@=, \=@=) % plus op/3 (dynamic Pratt operator table) +% and a distinct string term tag (atom_string, split_string, …) :- module(javascript_wam_bindings, [ javascript_wam_builtin/3, % Name, Arity, Status @@ -92,6 +93,8 @@ javascript_wam_builtin(char_code, 2, implemented). javascript_wam_builtin(sub_atom, 5, implemented). javascript_wam_builtin(atom_string, 2, implemented). +javascript_wam_builtin(string_to_atom, 2, implemented). +javascript_wam_builtin(string, 1, implemented). javascript_wam_builtin(number_codes, 2, implemented). javascript_wam_builtin(number_string, 2, implemented). javascript_wam_builtin(split_string, 4, implemented). diff --git a/src/unifyweaver/targets/wam_javascript_target.pl b/src/unifyweaver/targets/wam_javascript_target.pl index 42c3d203b..6ecd1e706 100644 --- a/src/unifyweaver/targets/wam_javascript_target.pl +++ b/src/unifyweaver/targets/wam_javascript_target.pl @@ -361,6 +361,10 @@ normalize_switch_case_tokens([Token|Rest], [Token|More]) :- normalize_switch_case_tokens(Rest, More). +constant_to_js_term(string(S), Lit) :- + !, + js_string_literal(S, Q), + format(string(Lit), 'V.String(~w)', [Q]). constant_to_js_term(C, Lit) :- wam_classify_constant_token(C, Class), ( Class = integer(N) @@ -603,6 +607,7 @@ CompileOpts = [ite_use_y_level(true), inline_bagof_setof(true)], compile_predicate_to_wam_text(PredIndicator, CompileOpts, WamText). +wam_item_parts(get_constant(C, Ai), ["get_constant", string(C), Ai]) :- string(C), !. wam_item_parts(get_constant(C, Ai), ["get_constant", C, Ai]). wam_item_parts(get_variable(Xn, Ai), ["get_variable", Xn, Ai]). wam_item_parts(get_value(Xn, Ai), ["get_value", Xn, Ai]). @@ -612,14 +617,17 @@ wam_item_parts(get_integer(N, Ai), ["get_integer", N, Ai]). wam_item_parts(unify_variable(Xn), ["unify_variable", Xn]). wam_item_parts(unify_value(Xn), ["unify_value", Xn]). +wam_item_parts(unify_constant(C), ["unify_constant", string(C)]) :- string(C), !. wam_item_parts(unify_constant(C), ["unify_constant", C]). wam_item_parts(put_variable(Xn, Ai), ["put_variable", Xn, Ai]). wam_item_parts(put_value(Xn, Ai), ["put_value", Xn, Ai]). +wam_item_parts(put_constant(C, Ai), ["put_constant", string(C), Ai]) :- string(C), !. wam_item_parts(put_constant(C, Ai), ["put_constant", C, Ai]). wam_item_parts(put_structure(F, Ai), ["put_structure", F, Ai]). wam_item_parts(put_list(Ai), ["put_list", Ai]). wam_item_parts(set_variable(Xn), ["set_variable", Xn]). wam_item_parts(set_value(Xn), ["set_value", Xn]). +wam_item_parts(set_constant(C), ["set_constant", string(C)]) :- string(C), !. wam_item_parts(set_constant(C), ["set_constant", C]). wam_item_parts(call(P, N), ["call", P, N]). wam_item_parts(execute(P), ["execute", P]). diff --git a/templates/targets/javascript_wam/runtime.js.mustache b/templates/targets/javascript_wam/runtime.js.mustache index a32adb2bd..73d14f1b9 100644 --- a/templates/targets/javascript_wam/runtime.js.mustache +++ b/templates/targets/javascript_wam/runtime.js.mustache @@ -18,6 +18,7 @@ const Runtime = {}; V.Atom = function (id) { return { tag: "atom", id: id }; }; V.Int = function (n) { return { tag: "int", val: n | 0 }; }; V.Float = function (n) { return { tag: "float", val: +n }; }; +V.String = function (s) { return { tag: "string", val: String(s) }; }; V.Unbound = function (name) { return { tag: "unbound", name: name }; }; V.Struct = function (fid, args) { return { tag: "struct", fid: fid, args: args || [] }; }; @@ -208,6 +209,7 @@ Runtime.copy_term = function (state, v, seen) { if (v.tag === "atom") return V.Atom(v.id); if (v.tag === "int") return V.Int(v.val); if (v.tag === "float") return V.Float(v.val); + if (v.tag === "string") return V.String(v.val); if (v.tag === "unbound") return V.Unbound(v.name); return v; }; @@ -253,6 +255,7 @@ function same_atomic(a, b) { if (a.tag !== b.tag) return false; if (a.tag === "atom") return a.id === b.id; if (a.tag === "int" || a.tag === "float") return a.val === b.val; + if (a.tag === "string") return a.val === b.val; return false; } @@ -549,6 +552,7 @@ function term_to_string(program, state, term) { if (typeof term !== "object" || term === null) return String(term); if (term.tag === "atom") return Runtime.string_of(program.intern_table, term.id); if (term.tag === "int" || term.tag === "float") return String(term.val); + if (term.tag === "string") return String(term.val); if (term.tag === "unbound") return "_" + String(term.name); if (term.tag === "struct") { const items = list_items(program, state, term); @@ -594,7 +598,8 @@ function builtin_functor(program, state) { name = V.Atom(term.fid); arity = (term.args || []).length; } else if (typeof term === "object" && term !== null - && (term.tag === "atom" || term.tag === "int" || term.tag === "float")) { + && (term.tag === "atom" || term.tag === "int" || term.tag === "float" + || term.tag === "string")) { name = term; arity = 0; } else { @@ -634,7 +639,8 @@ function builtin_univ(program, state) { const items = [V.Atom(term.fid)].concat(term.args || []); out = Runtime.list_from_terms(items, program.intern_table); } else if (typeof term === "object" && term !== null - && (term.tag === "atom" || term.tag === "int" || term.tag === "float")) { + && (term.tag === "atom" || term.tag === "int" || term.tag === "float" + || term.tag === "string")) { out = Runtime.list_from_terms([term], program.intern_table); } else { return false; @@ -664,6 +670,7 @@ function copy_term_fresh(state, term, varMap, seen) { if (term.tag === "atom") return V.Atom(term.id); if (term.tag === "int") return V.Int(term.val); if (term.tag === "float") return V.Float(term.val); + if (term.tag === "string") return V.String(term.val); return term; } @@ -792,6 +799,7 @@ function exact_equal(state, a, b) { if (a.tag === "unbound") return a.name === b.name; if (a.tag === "atom") return a.id === b.id; if (a.tag === "int" || a.tag === "float") return a.val === b.val; + if (a.tag === "string") return a.val === b.val; if (a.tag === "struct") { if (a.fid !== b.fid || (a.args || []).length !== (b.args || []).length) return false; for (let i = 0; i < a.args.length; i++) { @@ -805,7 +813,7 @@ function exact_equal(state, a, b) { function type_builtin(program, state, pred) { const p = strip_trailing_arity(pred); const known = { - atom: true, integer: true, float: true, number: true, + atom: true, integer: true, float: true, number: true, string: true, compound: true, var: true, nonvar: true, is_list: true, ground: true }; if (!known[p]) return null; @@ -813,6 +821,7 @@ function type_builtin(program, state, pred) { if (p === "ground") return is_ground(state, val); if (typeof val !== "object" || val === null) return false; if (p === "atom") return val.tag === "atom"; + if (p === "string") return val.tag === "string"; if (p === "integer") return val.tag === "int"; if (p === "float") return val.tag === "float"; if (p === "number") return val.tag === "int" || val.tag === "float"; @@ -842,6 +851,7 @@ function term_key(state, v) { if (v.tag === "atom") return "a:" + String(v.id); if (v.tag === "int") return "i:" + String(v.val); if (v.tag === "float") return "f:" + String(v.val); + if (v.tag === "string") return "str:" + String(v.val); if (v.tag === "unbound") return "u:" + String(v.name); if (v.tag === "struct") { const parts = ["s:" + String(v.fid) + "/" + String((v.args || []).length)]; @@ -858,11 +868,12 @@ function term_ord(state, v) { if (v.tag === "unbound") return [1, v.name]; if (v.tag === "int" || v.tag === "float") return [2, v.val]; if (v.tag === "atom") return [3, v.id]; + if (v.tag === "string") return [4, v.val]; if (v.tag === "struct") { const args = v.args || []; const inner = []; for (let i = 0; i < args.length; i++) inner.push(term_ord(state, args[i])); - return [4, v.fid, args.length, inner]; + return [5, v.fid, args.length, inner]; } return [9, String(v)]; } @@ -1186,7 +1197,8 @@ function find_matching_end_aggregate(program, startPc) { function is_switch_constant(v) { return typeof v === "object" && v !== null - && (v.tag === "atom" || v.tag === "int" || v.tag === "float"); + && (v.tag === "atom" || v.tag === "int" || v.tag === "float" + || v.tag === "string"); } function switch_values_equal(a, b) { @@ -1199,6 +1211,7 @@ function switch_values_equal(a, b) { } if (a.tag === "atom") return a.id === b.id; if (a.tag === "int" || a.tag === "float") return a.val === b.val; + if (a.tag === "string") return a.val === b.val; return false; } @@ -1842,32 +1855,44 @@ function builtin_include_exclude(program, state, keep) { return bind_list(program, state, 3, out); } -function builtin_atom_concat(program, state) { +function concat_box(program, text, asString) { + return asString ? V.String(text) : intern_atom(program, text); +} + +function builtin_concat_family(program, state, asString) { const a = atomic_text(program, state, Runtime.get_reg(state, 1)); const b = atomic_text(program, state, Runtime.get_reg(state, 2)); const c = atomic_text(program, state, Runtime.get_reg(state, 3)); if (a !== null && b !== null) { - return bind_or_compare(state, 3, intern_atom(program, a + b), program); + return bind_or_compare(state, 3, concat_box(program, a + b, asString), program); } if (c === null) return false; if (a !== null) { if (c.indexOf(a) !== 0) return false; - return bind_or_compare(state, 2, intern_atom(program, c.slice(a.length)), program); + return bind_or_compare(state, 2, concat_box(program, c.slice(a.length), asString), program); } if (b !== null) { if (c.length < b.length || c.slice(c.length - b.length) !== b) return false; - return bind_or_compare(state, 1, intern_atom(program, c.slice(0, c.length - b.length)), program); + return bind_or_compare(state, 1, concat_box(program, c.slice(0, c.length - b.length), asString), program); } const sols = []; for (let i = 0; i <= c.length; i++) { sols.push([ - [1, intern_atom(program, c.slice(0, i))], - [2, intern_atom(program, c.slice(i))] + [1, concat_box(program, c.slice(0, i), asString)], + [2, concat_box(program, c.slice(i), asString)] ]); } return dispatch_enum(program, state, sols); } +function builtin_atom_concat(program, state) { + return builtin_concat_family(program, state, false); +} + +function builtin_string_concat(program, state) { + return builtin_concat_family(program, state, true); +} + function builtin_atom_length(program, state) { const t = atomic_text(program, state, Runtime.get_reg(state, 1)); if (t === null) return false; @@ -1884,6 +1909,16 @@ function builtin_atom_chars(program, state) { return bind_or_compare(state, 1, intern_atom(program, s), program); } +function builtin_string_chars(program, state) { + const t = atomic_text(program, state, Runtime.get_reg(state, 1)); + if (t !== null) return bind_list(program, state, 2, string_to_chars(program, t)); + const items = list_items(program, state, Runtime.get_reg(state, 2)); + if (items === null) return false; + const s = chars_to_string(program, state, items); + if (s === null) return false; + return bind_or_compare(state, 1, V.String(s), program); +} + function builtin_atom_codes(program, state) { const t = atomic_text(program, state, Runtime.get_reg(state, 1)); if (t !== null) { @@ -1949,10 +1984,20 @@ function builtin_sub_atom(program, state) { } function builtin_atom_string(program, state) { - const a = atomic_text(program, state, Runtime.get_reg(state, 1)); - const b = atomic_text(program, state, Runtime.get_reg(state, 2)); - if (a !== null) return bind_or_compare(state, 2, intern_atom(program, a), program); - if (b !== null) return bind_or_compare(state, 1, intern_atom(program, b), program); + const aText = atomic_text(program, state, Runtime.get_reg(state, 1)); + const bText = atomic_text(program, state, Runtime.get_reg(state, 2)); + if (aText !== null && bText !== null) return aText === bText; + if (aText !== null) return bind_or_compare(state, 2, V.String(aText), program); + if (bText !== null) return bind_or_compare(state, 1, intern_atom(program, bText), program); + return false; +} + +function builtin_string_to_atom(program, state) { + const sText = atomic_text(program, state, Runtime.get_reg(state, 1)); + const aText = atomic_text(program, state, Runtime.get_reg(state, 2)); + if (sText !== null && aText !== null) return sText === aText; + if (sText !== null) return bind_or_compare(state, 2, intern_atom(program, sText), program); + if (aText !== null) return bind_or_compare(state, 1, V.String(aText), program); return false; } @@ -1976,7 +2021,7 @@ function builtin_number_codes(program, state) { function builtin_number_string(program, state) { const n = Runtime.deref(state, Runtime.get_reg(state, 1)); if (typeof n === "object" && n !== null && (n.tag === "int" || n.tag === "float")) { - return bind_or_compare(state, 2, intern_atom(program, String(n.val)), program); + return bind_or_compare(state, 2, V.String(String(n.val)), program); } const s = atomic_text(program, state, Runtime.get_reg(state, 2)); if (s === null) return false; @@ -2017,9 +2062,9 @@ function builtin_split_string(program, state) { } parts.push(trimPad(buf)); } - const atoms = []; - for (let i = 0; i < parts.length; i++) atoms.push(intern_atom(program, parts[i])); - return bind_list(program, state, 4, atoms); + const strs = []; + for (let i = 0; i < parts.length; i++) strs.push(V.String(parts[i])); + return bind_list(program, state, 4, strs); } function builtin_case_atom(program, state, up) { @@ -2034,11 +2079,18 @@ function quote_atom_text(s) { return "'" + String(s).replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'"; } +function quote_string_text(s) { + return "\"" + String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\""; +} + function format_write_term(program, state, term, quoted) { term = Runtime.deref(state, term); if (quoted && term && term.tag === "atom") { return quote_atom_text(Runtime.string_of(program.intern_table, term.id)); } + if (quoted && term && term.tag === "string") { + return quote_string_text(term.val); + } return term_to_string(program, state, term); } @@ -2477,13 +2529,16 @@ Runtime.builtin_call = function (program, state, inst) { if (name === "select") return builtin_select(program, state); if (name === "include") return builtin_include_exclude(program, state, true); if (name === "exclude") return builtin_include_exclude(program, state, false); - if (name === "atom_concat" || name === "string_concat") return builtin_atom_concat(program, state); + if (name === "atom_concat") return builtin_atom_concat(program, state); + if (name === "string_concat") return builtin_string_concat(program, state); if (name === "atom_length") return builtin_atom_length(program, state); - if (name === "atom_chars" || name === "string_chars") return builtin_atom_chars(program, state); + if (name === "atom_chars") return builtin_atom_chars(program, state); + if (name === "string_chars") return builtin_string_chars(program, state); if (name === "atom_codes") return builtin_atom_codes(program, state); if (name === "char_code") return builtin_char_code(program, state); if (name === "sub_atom") return builtin_sub_atom(program, state); if (name === "atom_string") return builtin_atom_string(program, state); + if (name === "string_to_atom") return builtin_string_to_atom(program, state); if (name === "number_codes") return builtin_number_codes(program, state); if (name === "number_string") return builtin_number_string(program, state); if (name === "split_string") return builtin_split_string(program, state); @@ -3392,6 +3447,23 @@ Runtime.tokenize_term = function (text) { toks.push({ type: "atom", value: buf }); continue; } + if (c === "\"") { + i += 1; + let buf = ""; + while (i < n && s.charAt(i) !== "\"") { + if (s.charAt(i) === "\\" && i + 1 < n) { + buf += s.charAt(i + 1); + i += 2; + } else { + buf += s.charAt(i); + i += 1; + } + } + if (i >= n) return null; + i += 1; + toks.push({ type: "string", value: buf }); + continue; + } const after_sep = last_type() === null || last_type() === "comma" || last_type() === "semicolon" || last_type() === "lparen" || last_type() === "lbracket" || @@ -3495,6 +3567,10 @@ Runtime.wam_parse_primary = function (p, maxPrec) { if (tok.is_float) return V.Float(num); return V.Int(num); } + if (tok.type === "string") { + p.pos += 1; + return V.String(tok.value); + } if (tok.type === "var") { p.pos += 1; const name = tok.value; @@ -3525,7 +3601,8 @@ Runtime.wam_parse_primary = function (p, maxPrec) { if (op && op.prec <= maxPrec && p.pos < p.tokens.length) { const next = p.tokens[p.pos]; const starts = next.type === "number" || next.type === "var" || - next.type === "atom" || next.type === "lparen" || next.type === "lbracket"; + next.type === "atom" || next.type === "string" || + next.type === "lparen" || next.type === "lbracket"; if (starts) { const rhsMax = op.assoc === "fy" ? op.prec : op.prec - 1; const operand = Runtime.wam_parse_expr(p, rhsMax); diff --git a/tests/test_wam_javascript_builtins.pl b/tests/test_wam_javascript_builtins.pl index b35c71aaa..e7c7c3215 100644 --- a/tests/test_wam_javascript_builtins.pl +++ b/tests/test_wam_javascript_builtins.pl @@ -61,6 +61,7 @@ :- dynamic user:probe_term_meta/0. :- dynamic user:probe_op3/0. :- dynamic user:probe_parse_likes/0. +:- dynamic user:probe_string_tag/0. install_probes :- retractall(user:probe_findall), @@ -103,6 +104,7 @@ retractall(user:probe_term_meta), retractall(user:probe_op3), retractall(user:probe_parse_likes), + retractall(user:probe_string_tag), assertz((user:probe_findall :- findall(X, member(X, [1,2,3]), L), write(L), nl, L == [1,2,3])), assertz((user:probe_functor :- @@ -269,6 +271,17 @@ assertz((user:probe_parse_likes :- read_term_from_atom('alice likes bob', T), T == likes(alice, bob), + write(ok), nl)), + assertz((user:probe_string_tag :- + atom_string(a, S), string(S), \+ atom(S), + string_to_atom(S, A0), A0 == a, + split_string("a,b,c", ",", "", Parts), + Parts = [P1, P2, P3], + string(P1), string(P2), string(P3), + P1 == "a", P2 == "b", P3 == "c", + string_concat("x", "y", Z), string(Z), Z == "xy", + sort([foo, "foo", 1, bar], Ord), + Ord == [1, bar, foo, "foo"], write(ok), nl)). probe_preds([ @@ -306,7 +319,8 @@ user:probe_assoc/0, user:probe_parse_atom/0, user:probe_term_meta/0, - user:probe_op3/0 + user:probe_op3/0, + user:probe_string_tag/0 ]). :- dynamic user:ctw_js/0. From 8081d3f1380f39698e7b0a3f39bfb25f559ed934 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 30 Aug 2026 20:14:51 +0000 Subject: [PATCH 2/3] Keep compiled constants as atoms; construct strings via builtins The shared WAM tokeniser stores every constant as text, so treating string(C) as V.String rewrote atoms and broke unify. Strings come from builtins and the Pratt \"...\" reader. Standard order matches SWI 9.0.4 (String @< Atom). Co-authored-by: johns243a --- docs/WAM_JAVASCRIPT_STATUS.md | 7 ++++--- .../targets/wam_javascript_target.pl | 8 -------- .../javascript_wam/runtime.js.mustache | 19 ++++++++++--------- tests/test_wam_javascript_builtins.pl | 13 ++++++++----- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/docs/WAM_JAVASCRIPT_STATUS.md b/docs/WAM_JAVASCRIPT_STATUS.md index f31158789..70ab2f877 100644 --- a/docs/WAM_JAVASCRIPT_STATUS.md +++ b/docs/WAM_JAVASCRIPT_STATUS.md @@ -143,7 +143,7 @@ parser, not the bundled portable `compiled(prolog_term_parser)`. | `=@=/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). | -| String term tag | **Implemented.** `V.String` is a distinct tag. Unify/`==` require equal strings (not atoms). Standard order / `compare/3` / `sort`: Var < Number < Atom < **String** < Compound. `atom_string/2`, `string_concat/3`, `string_chars/2` (construct), `string_to_atom/2`, `number_string/2`, `split_string/4` produce strings. `string/1` is true only for the tag. `write/1` prints text; `~q` quotes with `"`. Fact-source JSON/TSV values still intern as atoms. | +| String term tag | **Implemented.** `V.String` is a distinct tag. Unify/`==` require equal strings (not atoms). Standard order / `compare/3` / `sort` matches SWI 9.0.4: Var < Number < **String** < Atom < Compound (`"foo" @< foo`). `atom_string/2`, `string_concat/3`, `string_chars/2` (construct), `string_to_atom/2`, `number_string/2`, `split_string/4` produce strings. `string/1` is true only for the tag. `write/1` prints text; `~q` quotes with `"`. The shared WAM tokeniser stores constants as text, so compiled `"foo"` literals collapse to atoms; construct strings via builtins or the Pratt `"..."` reader. Fact-source JSON/TSV values still intern as atoms. | | `library(assoc)` | **Implemented** as a Prolog `assoc/1` list of Key-Value pairs (not SWI's AVL tree). get/put/list/keys match SWI for unique-key maps. | | First-arg indexing | **Implemented.** `switch_on_constant` / `_fallthrough` / `_a2`, `switch_on_structure` / `_a2`, and `switch_on_term` / `_a2` jump to the matching clause group. Ground first-arg with a unique clause leaves no choice point (`deterministic/0`). Unbound first arg falls through to the try/retry/trust chain (no lost solutions). Exclusive miss fails; fallthrough variants keep the chain for variable-headed clauses. Dedicated `try`/`retry`/`trust` dispatch chains are emitted for multi-clause groups. | | Second-arg / deep indexing | A2 switches are implemented; deep (argument >2) indexing is not. | @@ -179,8 +179,9 @@ positioning; assoc is a list-of-pairs, not SWI's AVL tree; `numbervars` does not letter-render `'$VAR'(N)` on `write/1`; `op/3` is process-global (no module-local ops) and `current_op/3` is not implemented; `writeq/1` as a standalone builtin is not registered -(quoted string rendering is via `format` `~q`); fact-source cells stay -atoms even when the host file looks like a quoted string. +(quoted string rendering is via `format` `~q`); compiled `"foo"` +literals become atoms (shared WAM constant tokens); fact-source cells +stay atoms even when the host file looks like a quoted string. ## Document status diff --git a/src/unifyweaver/targets/wam_javascript_target.pl b/src/unifyweaver/targets/wam_javascript_target.pl index 6ecd1e706..42c3d203b 100644 --- a/src/unifyweaver/targets/wam_javascript_target.pl +++ b/src/unifyweaver/targets/wam_javascript_target.pl @@ -361,10 +361,6 @@ normalize_switch_case_tokens([Token|Rest], [Token|More]) :- normalize_switch_case_tokens(Rest, More). -constant_to_js_term(string(S), Lit) :- - !, - js_string_literal(S, Q), - format(string(Lit), 'V.String(~w)', [Q]). constant_to_js_term(C, Lit) :- wam_classify_constant_token(C, Class), ( Class = integer(N) @@ -607,7 +603,6 @@ CompileOpts = [ite_use_y_level(true), inline_bagof_setof(true)], compile_predicate_to_wam_text(PredIndicator, CompileOpts, WamText). -wam_item_parts(get_constant(C, Ai), ["get_constant", string(C), Ai]) :- string(C), !. wam_item_parts(get_constant(C, Ai), ["get_constant", C, Ai]). wam_item_parts(get_variable(Xn, Ai), ["get_variable", Xn, Ai]). wam_item_parts(get_value(Xn, Ai), ["get_value", Xn, Ai]). @@ -617,17 +612,14 @@ wam_item_parts(get_integer(N, Ai), ["get_integer", N, Ai]). wam_item_parts(unify_variable(Xn), ["unify_variable", Xn]). wam_item_parts(unify_value(Xn), ["unify_value", Xn]). -wam_item_parts(unify_constant(C), ["unify_constant", string(C)]) :- string(C), !. wam_item_parts(unify_constant(C), ["unify_constant", C]). wam_item_parts(put_variable(Xn, Ai), ["put_variable", Xn, Ai]). wam_item_parts(put_value(Xn, Ai), ["put_value", Xn, Ai]). -wam_item_parts(put_constant(C, Ai), ["put_constant", string(C), Ai]) :- string(C), !. wam_item_parts(put_constant(C, Ai), ["put_constant", C, Ai]). wam_item_parts(put_structure(F, Ai), ["put_structure", F, Ai]). wam_item_parts(put_list(Ai), ["put_list", Ai]). wam_item_parts(set_variable(Xn), ["set_variable", Xn]). wam_item_parts(set_value(Xn), ["set_value", Xn]). -wam_item_parts(set_constant(C), ["set_constant", string(C)]) :- string(C), !. wam_item_parts(set_constant(C), ["set_constant", C]). wam_item_parts(call(P, N), ["call", P, N]). wam_item_parts(execute(P), ["execute", P]). diff --git a/templates/targets/javascript_wam/runtime.js.mustache b/templates/targets/javascript_wam/runtime.js.mustache index 73d14f1b9..75ab8a7d2 100644 --- a/templates/targets/javascript_wam/runtime.js.mustache +++ b/templates/targets/javascript_wam/runtime.js.mustache @@ -867,8 +867,8 @@ function term_ord(state, v) { if (typeof v !== "object" || v === null) return [0, String(v)]; if (v.tag === "unbound") return [1, v.name]; if (v.tag === "int" || v.tag === "float") return [2, v.val]; - if (v.tag === "atom") return [3, v.id]; - if (v.tag === "string") return [4, v.val]; + if (v.tag === "string") return [3, v.val]; + if (v.tag === "atom") return [4, v.id]; if (v.tag === "struct") { const args = v.args || []; const inner = []; @@ -941,13 +941,14 @@ function term_class_rank(v) { if (typeof v !== "object" || v === null) return 5; if (v.tag === "unbound") return 0; if (v.tag === "int" || v.tag === "float") return 1; - if (v.tag === "atom") return 2; - if (v.tag === "string") return 3; + // SWI 9.0.4: String @< Atom (compare(foo,"foo") = >). Match the oracle. + if (v.tag === "string") return 2; + if (v.tag === "atom") return 3; if (v.tag === "struct") return 4; return 5; } -// ISO §7.2 / SWI standard order: Var < Number < Atom < String < Compound. +// SWI 9.x standard order (oracle): Var < Number < String < Atom < Compound. // Atoms compare by name (not intern id). Compounds compare by arity, // then functor name, then arguments left-to-right. function compare_terms(intern, state, a, b) { @@ -965,15 +966,15 @@ function compare_terms(intern, state, a, b) { return a.val < b.val ? -1 : 1; } if (ca === 2) { + if (a.val === b.val) return 0; + return a.val < b.val ? -1 : 1; + } + if (ca === 3) { const na = Runtime.string_of(intern, a.id); const nb = Runtime.string_of(intern, b.id); if (na === nb) return 0; return na < nb ? -1 : 1; } - if (ca === 3) { - if (a.val === b.val) return 0; - return a.val < b.val ? -1 : 1; - } if (ca === 4) { const aa = (a.args || []).length; const ba = (b.args || []).length; diff --git a/tests/test_wam_javascript_builtins.pl b/tests/test_wam_javascript_builtins.pl index e7c7c3215..b48965e29 100644 --- a/tests/test_wam_javascript_builtins.pl +++ b/tests/test_wam_javascript_builtins.pl @@ -275,13 +275,16 @@ assertz((user:probe_string_tag :- atom_string(a, S), string(S), \+ atom(S), string_to_atom(S, A0), A0 == a, - split_string("a,b,c", ",", "", Parts), + split_string('a,b,c', ',', '', Parts), Parts = [P1, P2, P3], string(P1), string(P2), string(P3), - P1 == "a", P2 == "b", P3 == "c", - string_concat("x", "y", Z), string(Z), Z == "xy", - sort([foo, "foo", 1, bar], Ord), - Ord == [1, bar, foo, "foo"], + atom_string(a, EA), atom_string(b, EB), atom_string(c, EC), + P1 == EA, P2 == EB, P3 == EC, + string_concat(x, y, Z), string(Z), + atom_string(xy, EZ), Z == EZ, + atom_string(foo, SFoo), + sort([foo, SFoo, 1, bar], Ord), + Ord == [1, SFoo, bar, foo], write(ok), nl)). probe_preds([ From f68d2447b8dda82b75abfb06bbb451f5b98dcee9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 30 Aug 2026 20:17:28 +0000 Subject: [PATCH 3/3] Align setof standard-order note with SWI 9.0.4 String sorts before Atom on this SWI (compare(foo,"foo") = >). Co-authored-by: johns243a --- docs/WAM_JAVASCRIPT_STATUS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/WAM_JAVASCRIPT_STATUS.md b/docs/WAM_JAVASCRIPT_STATUS.md index 70ab2f877..171c062da 100644 --- a/docs/WAM_JAVASCRIPT_STATUS.md +++ b/docs/WAM_JAVASCRIPT_STATUS.md @@ -137,7 +137,7 @@ parser, not the bundled portable `compiled(prolog_term_parser)`. | Builtin | Status | |---|---| | `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). | +| `setof/3` | **Implemented.** `bagof` then per-group standard-order sort + dedup. Order: Var < Number < **String** < Atom < Compound (SWI 9.0.4); compounds by arity, functor **name**, then args L-to-R (matches SWI mixed-type lists). | | `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. |