From fbe56fbde795e8a396b8b41b5080a50bae4e93d3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 30 Aug 2026 17:05:30 +0000 Subject: [PATCH 1/3] Add a Pratt term reader to the JavaScript WAM runtime CLI argv and read_term_from_atom/atom_to_term now parse floats, lists (including [H|T]), and compounds in addition to ints and atoms. Cons intern as [|]/2 plus the [] atom. Register native(parse_term) via INTEGRATION_PATCH.md (shared capability file is not edited). Co-authored-by: johns243a --- INTEGRATION_PATCH.md | 36 +- docs/WAM_JAVASCRIPT_STATUS.md | 41 +- .../bindings/javascript_wam_bindings.pl | 8 +- .../javascript_wam/runtime.js.mustache | 385 +++++++++++++++++- tests/test_wam_javascript_builtins.pl | 46 ++- 5 files changed, 506 insertions(+), 10 deletions(-) diff --git a/INTEGRATION_PATCH.md b/INTEGRATION_PATCH.md index 9b9714087..3b4697bae 100644 --- a/INTEGRATION_PATCH.md +++ b/INTEGRATION_PATCH.md @@ -38,7 +38,7 @@ Add a row for the JS WAM catalogue (see | Target | Bindings | Notes | |---|---|---| -| `wam_javascript` | 41 (`javascript_wam_builtin/3`) | Interpreter-tier Node WAM. findall/functor/arg/=../copy_term/\+/call/aggregate_all/bagof/setof implemented (ISO witness grouping + standard-order setof). | +| `wam_javascript` | catalogue in `javascript_wam_bindings.pl` | Interpreter-tier Node WAM. findall/functor/arg/=../copy_term/\+/call/aggregate_all/bagof/setof + ISO/library breadth + native Pratt `parse_term`. | ## 3. `tests/test_advanced.pl` @@ -165,3 +165,37 @@ green. The dedicated runner that does **not** need this patch: ```bash swipl -q -g run_tests -t halt tests/test_wam_javascript_builtins.pl ``` + +## 7. `src/unifyweaver/targets/wam_runtime_parser_capability.pl` + +The JS WAM ships a hand-written recursive-descent / Pratt reader in +`templates/targets/javascript_wam/runtime.js.mustache` +(`Runtime.parse_term`). That is the same kind of in-runtime host parser +as C++/R (`native(parse_term)`), **not** the bundled portable +`compiled(prolog_term_parser)`. Do **not** also claim `compiled(...)` +until the portable parser is actually prepended. + +Add next to the other `target_runtime_parser_default/2` clauses: + +```prolog +% JavaScript WAM: hand-written Pratt reader in the Node runtime +% (Runtime.parse_term). Powers CLI argv, read_term_from_atom/2,3, +% atom_to_term/3, and term_to_atom/2 reverse mode. ISO default +% operator table is included; user op/3 is not. Same default as +% C++/R because the parser always ships with the generated runtime. +target_runtime_parser_default(wam_javascript, native(parse_term)). +``` + +Add next to the other `target_runtime_parser_mode_/2` clauses: + +```prolog +target_runtime_parser_mode_(wam_javascript, native(parse_term)). +``` + +Add next to the other `normalize_runtime_parser_target/2` clauses +(before the catch-all `normalize_runtime_parser_target(Target, Target)`): + +```prolog +normalize_runtime_parser_target(javascript, wam_javascript) :- !. +normalize_runtime_parser_target(wam_javascript, wam_javascript) :- !. +``` diff --git a/docs/WAM_JAVASCRIPT_STATUS.md b/docs/WAM_JAVASCRIPT_STATUS.md index 7a4ac5a0f..349c1e9e1 100644 --- a/docs/WAM_JAVASCRIPT_STATUS.md +++ b/docs/WAM_JAVASCRIPT_STATUS.md @@ -73,7 +73,8 @@ Assoc (`library(assoc)` shape, list-of-pairs not AVL): **`empty_assoc/1`**, **`list_to_assoc/2`**, **`get_assoc/3`**, **`put_assoc/4`**, **`assoc_to_list/2`**, **`assoc_to_keys/2`**. -Term: **`functor/3`**, **`arg/3`**, **`=../2`**, **`copy_term/2`**. +Term: **`functor/3`**, **`arg/3`**, **`=../2`**, **`copy_term/2`**, +**`read_term_from_atom/2` `/3`**, **`atom_to_term/3`**, **`term_to_atom/2`**. Metacall: **`\+/1`**, **`call/1`** (re-enter the same instruction loop / builtin dispatch). @@ -86,6 +87,36 @@ 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`. +## Runtime term parser (G-W2) + +The Node runtime ships a **hand-written recursive-descent / Pratt reader** +(`Runtime.parse_term` / `tokenize_term` in `runtime.js.mustache`). CLI argv +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'` +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 `[]` +atom (same functor as `put_list`). + +**Partial:** ISO default operator table (`+ - * / //`, comparisons, `,`, +`\+`, prefix `+/-`, …) so `1+2` reads as `+(1,2)`. User `op/3` and +postfix operators are not implemented. Fall back rather than invent a +term when the token stream is leftover or illegal. + +Capability (coordinator applies `INTEGRATION_PATCH.md` §7; the shared +capability file is not edited here): + +```prolog +target_runtime_parser_default(wam_javascript, native(parse_term)). +target_runtime_parser_mode_(wam_javascript, native(parse_term)). +``` + +This matches C++/R (`native(parse_term)`): an in-runtime host-language +parser, not the bundled portable `compiled(prolog_term_parser)`. + ## Remaining / partial | Builtin | Status | @@ -100,6 +131,7 @@ Types: `atom/1`, `integer/1`, `float/1`, `number/1`, `compound/1`, `var/1`, | 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. | | Lowered / functions emit mode | **Implemented.** `javascript_wam_resolve_emit_mode/2` accepts `interpreter` (default), `functions` (lower every eligible predicate), and `mixed([P/A, ...])` (lower only the named ones). Eligible shapes: single-clause deterministic bodies; T4 all-clauses-inline; T5 first-arg constant dispatch; T6 hash dispatch (≥8 atom keys); structured ITE / negation / once. Unsupported ops (`begin_aggregate`, bagof/setof, cuts/jumps the planner rejects) fall back to the interpreter rather than emitting wrong code. Interpreter-mode bytecode and wrappers are unchanged. | +| CLI / runtime term parser | **Implemented.** Pratt reader: int/float/atom (incl. quoted)/var/list/`[H\|T]`/compound. CLI argv + `read_term_from_atom` / `atom_to_term` / `term_to_atom`. Capability `native(parse_term)` via `INTEGRATION_PATCH.md` §7. | | Conformance harness adapter | See `INTEGRATION_PATCH.md` (coordinator applies `conformance_target(javascript)`). | ## How to run @@ -129,6 +161,7 @@ positioning; assoc is a list-of-pairs, not SWI's AVL tree. ## 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, then ISO/library -builtin breadth (sort, lists, atom/string, format, assoc). Source-verified -against SWI-Prolog as the oracle (2026-08-30). +first-argument indexing, the Tier-2 lowered emitter, ISO/library builtin +breadth (sort, lists, atom/string, format, assoc), then the G-W2 runtime +term parser (floats, lists, compounds; `native(parse_term)`). +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 b06679179..c744f608a 100644 --- a/src/unifyweaver/bindings/javascript_wam_bindings.pl +++ b/src/unifyweaver/bindings/javascript_wam_bindings.pl @@ -8,7 +8,9 @@ % dispatches these names from BuiltinCall (and as a Call/Execute fallback % when no user label exists). Status: % implemented - full for the conformance + probe suite, including -% ISO bagof/3 / setof/3 witness grouping and ^/2 +% 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) :- module(javascript_wam_bindings, [ javascript_wam_builtin/3, % Name, Arity, Status @@ -104,6 +106,10 @@ javascript_wam_builtin(put_assoc, 4, implemented). javascript_wam_builtin(assoc_to_list, 2, implemented). javascript_wam_builtin(assoc_to_keys, 2, implemented). +javascript_wam_builtin(read_term_from_atom, 2, implemented). +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_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 b19c6e43a..02ef9d617 100644 --- a/templates/targets/javascript_wam/runtime.js.mustache +++ b/templates/targets/javascript_wam/runtime.js.mustache @@ -2254,6 +2254,51 @@ function builtin_between(program, state) { return bind_or_compare(state, 3, V.Int(lo.val), program); } +function builtin_read_term_from_atom(program, state) { + const text = atomic_text(program, state, Runtime.get_reg(state, 1)); + if (text === null) return false; + const parsed = Runtime.parse_term(program, text, state); + if (parsed === null) return false; + return Runtime.unify(state, Runtime.get_reg(state, 2), parsed, program); +} + +function builtin_atom_to_term(program, state) { + const text = atomic_text(program, state, Runtime.get_reg(state, 1)); + if (text === null) return false; + const parsed = Runtime.parse_term_with_vars(program, text, state); + if (parsed === null) return false; + if (Runtime.unify(state, Runtime.get_reg(state, 2), parsed.term, program) !== true) { + return false; + } + const eqId = Runtime.intern(program.intern_table, "="); + const pairs = []; + const names = Object.keys(parsed.vars); + for (let i = 0; i < names.length; i++) { + const n = names[i]; + pairs.push(V.Struct(eqId, [intern_atom(program, n), parsed.vars[n]])); + } + return Runtime.unify( + state, + Runtime.get_reg(state, 3), + Runtime.list_from_terms(pairs, program.intern_table), + program + ); +} + +function builtin_term_to_atom(program, state) { + const rawT = Runtime.get_reg(state, 1); + const t = Runtime.deref(state, rawT); + if (t && typeof t === "object" && t.tag && t.tag !== "unbound") { + const s = term_to_string(program, state, rawT); + return bind_or_compare(state, 2, intern_atom(program, s), program); + } + const text = atomic_text(program, state, Runtime.get_reg(state, 2)); + if (text === null) return false; + const parsed = Runtime.parse_term(program, text, state); + if (parsed === null) return false; + return Runtime.unify(state, rawT, parsed, program); +} + Runtime.builtin_call = function (program, state, inst) { const p = inst.pred; const name = pred_name(p); @@ -2319,6 +2364,9 @@ Runtime.builtin_call = function (program, state, inst) { if (name === "put_assoc") return builtin_put_assoc(program, state); if (name === "assoc_to_list") return builtin_assoc_to_list(program, state); if (name === "assoc_to_keys") return builtin_assoc_to_keys(program, state); + if (name === "read_term_from_atom") return builtin_read_term_from_atom(program, state); + if (name === "atom_to_term") return builtin_atom_to_term(program, state); + if (name === "term_to_atom") return builtin_term_to_atom(program, state); if (name === "=") return Runtime.unify(state, Runtime.get_reg(state, 1), Runtime.get_reg(state, 2), program); if (name === "==") return exact_equal(state, Runtime.get_reg(state, 1), Runtime.get_reg(state, 2)); if (name === "\\==") return exact_equal(state, Runtime.get_reg(state, 1), Runtime.get_reg(state, 2)) !== true; @@ -2888,10 +2936,341 @@ Runtime.run_predicate = function (program, startPc, args) { return Runtime.run(program, state) === true; }; +// ISO-ish operator tables (same coverage as the R WAM native reader). +// Format: name -> { prec, assoc }. Postfix is empty by default. +Runtime.OP_INFIX = { + ":-": { prec: 1200, assoc: "xfx" }, + "-->": { prec: 1200, assoc: "xfx" }, + ";": { prec: 1100, assoc: "xfy" }, + "->": { prec: 1050, assoc: "xfy" }, + "*->": { prec: 1050, assoc: "xfy" }, + ",": { prec: 1000, assoc: "xfy" }, + "=": { prec: 700, assoc: "xfx" }, + "\\=": { prec: 700, assoc: "xfx" }, + "==": { prec: 700, assoc: "xfx" }, + "\\==": { prec: 700, assoc: "xfx" }, + "=..": { prec: 700, assoc: "xfx" }, + "is": { prec: 700, assoc: "xfx" }, + "=:=": { prec: 700, assoc: "xfx" }, + "=\\=": { prec: 700, assoc: "xfx" }, + "<": { prec: 700, assoc: "xfx" }, + ">": { prec: 700, assoc: "xfx" }, + "=<": { prec: 700, assoc: "xfx" }, + ">=": { prec: 700, assoc: "xfx" }, + "@<": { prec: 700, assoc: "xfx" }, + "@>": { prec: 700, assoc: "xfx" }, + "@=<": { prec: 700, assoc: "xfx" }, + "@>=": { prec: 700, assoc: "xfx" }, + "+": { prec: 500, assoc: "yfx" }, + "-": { prec: 500, assoc: "yfx" }, + "/\\": { prec: 500, assoc: "yfx" }, + "\\/": { prec: 500, assoc: "yfx" }, + "xor": { prec: 500, assoc: "yfx" }, + "*": { prec: 400, assoc: "yfx" }, + "/": { prec: 400, assoc: "yfx" }, + "//": { prec: 400, assoc: "yfx" }, + "mod": { prec: 400, assoc: "yfx" }, + "rem": { prec: 400, assoc: "yfx" }, + "div": { prec: 400, assoc: "yfx" }, + "<<": { prec: 400, assoc: "yfx" }, + ">>": { prec: 400, assoc: "yfx" }, + "**": { prec: 200, assoc: "xfx" }, + "^": { prec: 200, assoc: "xfy" } +}; + +Runtime.OP_PREFIX = { + ":-": { prec: 1200, assoc: "fx" }, + "?-": { prec: 1200, assoc: "fx" }, + "\\+": { prec: 900, assoc: "fy" }, + "-": { prec: 200, assoc: "fy" }, + "+": { prec: 200, assoc: "fy" }, + "\\": { prec: 200, assoc: "fy" } +}; + +Runtime.OP_POSTFIX = {}; + +function term_is_digit(ch) { + return ch >= "0" && ch <= "9"; +} + +function term_is_lower(ch) { + return ch >= "a" && ch <= "z"; +} + +function term_is_var_start(ch) { + return (ch >= "A" && ch <= "Z") || ch === "_"; +} + +function term_is_alnum(ch) { + return term_is_digit(ch) || term_is_lower(ch) || + (ch >= "A" && ch <= "Z") || ch === "_"; +} + +function term_is_sym(ch) { + return "+-*/\\^<>=:@.?~#$&".indexOf(ch) >= 0; +} + +Runtime.tokenize_term = function (text) { + const s = String(text); + const n = s.length; + const toks = []; + let i = 0; + function last_type() { + return toks.length === 0 ? null : toks[toks.length - 1].type; + } + while (i < n) { + const c = s.charAt(i); + if (c === " " || c === "\t" || c === "\n" || c === "\r") { i += 1; continue; } + if (c === "(") { toks.push({ type: "lparen" }); i += 1; continue; } + if (c === ")") { toks.push({ type: "rparen" }); i += 1; continue; } + if (c === "[") { toks.push({ type: "lbracket" }); i += 1; continue; } + if (c === "]") { toks.push({ type: "rbracket" }); i += 1; continue; } + if (c === ",") { toks.push({ type: "comma" }); i += 1; continue; } + if (c === ";") { toks.push({ type: "semicolon" }); i += 1; continue; } + if (c === "|") { toks.push({ type: "pipe" }); i += 1; continue; } + if (c === "!") { + toks.push({ type: "atom", value: "!", is_symbol: true }); + i += 1; + 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: "atom", value: buf }); + continue; + } + const after_sep = last_type() === null || + last_type() === "comma" || last_type() === "semicolon" || + last_type() === "lparen" || last_type() === "lbracket" || + last_type() === "pipe"; + if (term_is_digit(c) || + (c === "-" && i + 1 < n && term_is_digit(s.charAt(i + 1)) && after_sep)) { + let j = c === "-" ? i + 1 : i; + let sawDot = false; + while (j < n && (term_is_digit(s.charAt(j)) || + (!sawDot && s.charAt(j) === "." && j + 1 < n && + term_is_digit(s.charAt(j + 1))))) { + if (s.charAt(j) === ".") sawDot = true; + j += 1; + } + if (j < n && (s.charAt(j) === "e" || s.charAt(j) === "E")) { + let k = j + 1; + if (k < n && (s.charAt(k) === "+" || s.charAt(k) === "-")) k += 1; + if (k < n && term_is_digit(s.charAt(k))) { + sawDot = true; + j = k + 1; + while (j < n && term_is_digit(s.charAt(j))) j += 1; + } + } + toks.push({ type: "number", value: s.slice(i, j), is_float: sawDot }); + i = j; + continue; + } + if (term_is_var_start(c)) { + let j = i + 1; + while (j < n && term_is_alnum(s.charAt(j))) j += 1; + toks.push({ type: "var", value: s.slice(i, j) }); + i = j; + continue; + } + if (term_is_lower(c)) { + let j = i + 1; + while (j < n && term_is_alnum(s.charAt(j))) j += 1; + toks.push({ type: "atom", value: s.slice(i, j) }); + i = j; + continue; + } + if (term_is_sym(c)) { + let j = i + 1; + while (j < n && term_is_sym(s.charAt(j))) j += 1; + toks.push({ type: "atom", value: s.slice(i, j), is_symbol: true }); + i = j; + continue; + } + return null; + } + return toks; +}; + +Runtime.wam_parse_expr = function (p, maxPrec) { + const left0 = Runtime.wam_parse_primary(p, maxPrec); + if (left0 === null) return null; + let left = left0; + let leftPrec = p.last_prec; + while (true) { + if (p.pos >= p.tokens.length) return left; + const tok = p.tokens[p.pos]; + let opName = null; + if (tok.type === "atom") opName = tok.value; + else if (tok.type === "comma") opName = ","; + else if (tok.type === "semicolon") opName = ";"; + if (opName === null) return left; + const iop = Runtime.OP_INFIX[opName]; + if (iop && iop.prec <= maxPrec) { + const lhsOk = iop.assoc === "yfx" ? leftPrec <= iop.prec : leftPrec < iop.prec; + if (lhsOk) { + p.pos += 1; + const rhsMax = iop.assoc === "xfy" ? iop.prec : iop.prec - 1; + const rhs = Runtime.wam_parse_expr(p, rhsMax); + if (rhs === null) return null; + left = V.Struct(Runtime.intern(p.table, opName), [left, rhs]); + leftPrec = iop.prec; + continue; + } + } + const pop = Runtime.OP_POSTFIX[opName]; + if (pop && pop.prec <= maxPrec) { + const lhsOk = pop.assoc === "yf" ? leftPrec <= pop.prec : leftPrec < pop.prec; + if (lhsOk) { + p.pos += 1; + left = V.Struct(Runtime.intern(p.table, opName), [left]); + leftPrec = pop.prec; + continue; + } + } + return left; + } +}; + +Runtime.wam_parse_primary = function (p, maxPrec) { + p.last_prec = 0; + if (p.pos >= p.tokens.length) return null; + const tok = p.tokens[p.pos]; + if (tok.type === "number") { + p.pos += 1; + const num = Number(tok.value); + if (tok.is_float) return V.Float(num); + return V.Int(num); + } + if (tok.type === "var") { + p.pos += 1; + const name = tok.value; + if (name === "_") return Runtime.new_var(p.state); + if (Object.prototype.hasOwnProperty.call(p.vars, name)) return p.vars[name]; + const v = Runtime.new_var(p.state); + p.vars[name] = v; + return v; + } + if (tok.type === "atom") { + p.pos += 1; + if (p.pos < p.tokens.length && p.tokens[p.pos].type === "lparen") { + p.pos += 1; + const args = []; + while (true) { + const a = Runtime.wam_parse_expr(p, 999); + if (a === null) return null; + args.push(a); + if (p.pos >= p.tokens.length) return null; + const sep = p.tokens[p.pos].type; + if (sep === "rparen") { p.pos += 1; break; } + if (sep !== "comma") return null; + p.pos += 1; + } + return V.Struct(Runtime.intern(p.table, tok.value), args); + } + const op = Runtime.OP_PREFIX[tok.value]; + 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"; + if (starts) { + const rhsMax = op.assoc === "fy" ? op.prec : op.prec - 1; + const operand = Runtime.wam_parse_expr(p, rhsMax); + if (operand === null) return null; + p.last_prec = op.prec; + return V.Struct(Runtime.intern(p.table, tok.value), [operand]); + } + } + return V.Atom(Runtime.intern(p.table, tok.value)); + } + if (tok.type === "lparen") { + p.pos += 1; + const inner = Runtime.wam_parse_expr(p, 1200); + if (inner === null) return null; + if (p.pos >= p.tokens.length || p.tokens[p.pos].type !== "rparen") return null; + p.pos += 1; + return inner; + } + if (tok.type === "lbracket") { + p.pos += 1; + if (p.pos < p.tokens.length && p.tokens[p.pos].type === "rbracket") { + p.pos += 1; + return V.Atom(Runtime.intern(p.table, "[]")); + } + const elems = []; + while (true) { + const a = Runtime.wam_parse_expr(p, 999); + if (a === null) return null; + elems.push(a); + if (p.pos >= p.tokens.length) return null; + const sep = p.tokens[p.pos].type; + if (sep === "rbracket") { + p.pos += 1; + return Runtime.list_from_terms(elems, p.table); + } + if (sep === "comma") { p.pos += 1; continue; } + if (sep === "pipe") { + p.pos += 1; + const tail = Runtime.wam_parse_expr(p, 999); + if (tail === null) return null; + if (p.pos >= p.tokens.length || p.tokens[p.pos].type !== "rbracket") return null; + p.pos += 1; + const consId = Runtime.intern(p.table, "[|]"); + let result = tail; + for (let i = elems.length - 1; i >= 0; i--) { + result = V.Struct(consId, [elems[i], result]); + } + return result; + } + return null; + } + } + return null; +}; + +// Recursive-descent / Pratt term reader. Interns cons as [|]/2 + [] atom, +// compounds as put_structure-style structs, floats as V.Float. +Runtime.parse_term_with_vars = function (program, text, state) { + if (state === undefined || state === null) state = Runtime.new_state(); + const toks = Runtime.tokenize_term(text); + if (toks === null || toks.length === 0) return null; + const p = { + tokens: toks, + pos: 0, + table: program.intern_table, + state: state, + vars: {}, + last_prec: 0 + }; + const result = Runtime.wam_parse_expr(p, 1200); + if (result === null) return null; + if (p.pos < toks.length) return null; + return { term: result, vars: p.vars }; +}; + +Runtime.parse_term = function (program, text, state) { + const parsed = Runtime.parse_term_with_vars(program, text, state); + return parsed === null ? null : parsed.term; +}; + +// CLI / argv reader. Full term syntax (int/float/atom/list/compound); +// unreadable text still interned as an atom so existing argv atoms keep working. Runtime.parse_cli_atom_or_int = function (program, text) { - const n = Number(text); - if (text !== "" && Number.isInteger(n) && String(n) === text) return V.Int(n); - return V.Atom(Runtime.intern(program.intern_table, text)); + const state = Runtime.new_state(); + const parsed = Runtime.parse_term(program, String(text), state); + if (parsed !== null) return parsed; + return V.Atom(Runtime.intern(program.intern_table, String(text))); }; Runtime.term_to_string = term_to_string; diff --git a/tests/test_wam_javascript_builtins.pl b/tests/test_wam_javascript_builtins.pl index ba9e93a1b..4175a45ca 100644 --- a/tests/test_wam_javascript_builtins.pl +++ b/tests/test_wam_javascript_builtins.pl @@ -53,6 +53,10 @@ :- dynamic user:probe_atoms/0. :- dynamic user:probe_format/0. :- dynamic user:probe_assoc/0. +:- dynamic user:sum3/2. +:- dynamic user:unwrap/2. +:- dynamic user:gt_float/1. +:- dynamic user:probe_parse_atom/0. install_probes :- retractall(user:probe_findall), @@ -87,6 +91,10 @@ retractall(user:probe_atoms), retractall(user:probe_format), retractall(user:probe_assoc), + retractall(user:sum3/2), + retractall(user:unwrap/2), + retractall(user:gt_float/1), + retractall(user:probe_parse_atom), assertz((user:probe_findall :- findall(X, member(X, [1,2,3]), L), write(L), nl, L == [1,2,3])), assertz((user:probe_functor :- @@ -212,6 +220,19 @@ assoc_to_list(A2, AL), AL == [a-1, b-2], list_to_assoc([a-1, b-2], A3), get_assoc(b, A3, W), W == 2, + write(ok), nl)), + assertz((user:sum3(L, S) :- sum_list(L, S))), + assertz(user:unwrap(foo(X, bar(b), 3), X)), + assertz((user:gt_float(X) :- X > 3.0)), + assertz((user:probe_parse_atom :- + read_term_from_atom('[1,2,3]', L), L == [1,2,3], + read_term_from_atom('foo(a, bar(b), 3)', T), T == foo(a, bar(b), 3), + read_term_from_atom('3.14', F), F > 3.0, + read_term_from_atom('-2', N), N == -2, + read_term_from_atom('[a|Rest]', PL), PL = [a, b], Rest == [b], + read_term_from_atom('\'hi there\'', QA), QA == 'hi there', + 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)). probe_preds([ @@ -246,7 +267,8 @@ user:probe_lists/0, user:probe_atoms/0, user:probe_format/0, - user:probe_assoc/0 + user:probe_assoc/0, + user:probe_parse_atom/0 ]). :- dynamic user:ctw_js/0. @@ -333,6 +355,28 @@ ) )). +test(cli_structured_args, [setup(install_probes)]) :- + % SWI oracle for the same shapes the CLI parser must intern. + assertion((sum_list([1, 2, 3], S0), S0 == 6)), + assertion((foo(a, bar(b), 3) = foo(X0, bar(b), 3), X0 == a)), + assertion(3.14 > 3.0), + Dir = 'output/js_wam_parser_cli', + make_directory_path(Dir), + write_wam_javascript_project( + [user:sum3/2, user:unwrap/2, user:gt_float/1], + [emit_mode(interpreter)], Dir), + run_node_args(Dir, ['sum3/2', '[1,2,3]'], ListExit, ListOut), + assertion(ListExit =:= 0), + assertion(node_succeeded(ListOut)), + assertion(sub_string(ListOut, _, _, _, "A2 = 6")), + run_node_args(Dir, ['unwrap/2', 'foo(a,bar(b),3)'], CompExit, CompOut), + assertion(CompExit =:= 0), + assertion(node_succeeded(CompOut)), + assertion(sub_string(CompOut, _, _, _, "A2 = a")), + run_node_args(Dir, ['gt_float/1', '3.14'], FloatExit, FloatOut), + assertion(FloatExit =:= 0), + assertion(node_succeeded(FloatOut)). + :- end_tests(js_wam_builtins). % --------------------------------------------------------------------------- From 687ec3cf00af6d6531d121201d4d32cc03146c77 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 30 Aug 2026 17:07:56 +0000 Subject: [PATCH 2/3] Pad CLI arity with unbound vars and fix quoted-atom probe Interpreter-mode argv only supplied bound arguments, so unwrap/2 with a compound first arg left A2 undefined and failed. Quoted-atom source with nested quotes also double-escaped in the intern seed; build that source at runtime via atom_concat and cover quoted atoms on the CLI. Co-authored-by: johns243a --- .../targets/javascript_wam/program.js.mustache | 10 +++++----- tests/test_wam_javascript_builtins.pl | 13 ++++++++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/templates/targets/javascript_wam/program.js.mustache b/templates/targets/javascript_wam/program.js.mustache index ae3d85fff..7b8205f3a 100644 --- a/templates/targets/javascript_wam/program.js.mustache +++ b/templates/targets/javascript_wam/program.js.mustache @@ -80,14 +80,14 @@ if (is_main()) { } const state = Runtime.new_state(); for (let i = 0; i < parsed.length; i++) Runtime.put_reg(state, i + 1, parsed[i]); + const slash = String(predArity).lastIndexOf("/"); + const arity = slash >= 0 ? Number(predArity.slice(slash + 1)) : parsed.length; + for (let i = parsed.length; i < arity; i++) { + Runtime.put_reg(state, i + 1, Runtime.new_var(state)); + } state.program = shared_program; let ok; if (typeof loweredFn === "function") { - const slash = String(predArity).lastIndexOf("/"); - const arity = slash >= 0 ? Number(predArity.slice(slash + 1)) : parsed.length; - for (let i = parsed.length; i < arity; i++) { - Runtime.put_reg(state, i + 1, Runtime.new_var(state)); - } ok = loweredFn(shared_program, state) === true; } else { state.pc = startPc; diff --git a/tests/test_wam_javascript_builtins.pl b/tests/test_wam_javascript_builtins.pl index 4175a45ca..1a57073bb 100644 --- a/tests/test_wam_javascript_builtins.pl +++ b/tests/test_wam_javascript_builtins.pl @@ -56,6 +56,7 @@ :- dynamic user:sum3/2. :- dynamic user:unwrap/2. :- dynamic user:gt_float/1. +:- dynamic user:qatom/1. :- dynamic user:probe_parse_atom/0. install_probes :- @@ -94,6 +95,7 @@ retractall(user:sum3/2), retractall(user:unwrap/2), retractall(user:gt_float/1), + retractall(user:qatom/1), retractall(user:probe_parse_atom), assertz((user:probe_findall :- findall(X, member(X, [1,2,3]), L), write(L), nl, L == [1,2,3])), @@ -224,13 +226,15 @@ assertz((user:sum3(L, S) :- sum_list(L, S))), assertz(user:unwrap(foo(X, bar(b), 3), X)), assertz((user:gt_float(X) :- X > 3.0)), + assertz(user:qatom('hello world')), assertz((user:probe_parse_atom :- read_term_from_atom('[1,2,3]', L), L == [1,2,3], read_term_from_atom('foo(a, bar(b), 3)', T), T == foo(a, bar(b), 3), read_term_from_atom('3.14', F), F > 3.0, read_term_from_atom('-2', N), N == -2, read_term_from_atom('[a|Rest]', PL), PL = [a, b], Rest == [b], - read_term_from_atom('\'hi there\'', QA), QA == 'hi there', + atom_concat('\'', 'hi there\'', QSrc), + read_term_from_atom(QSrc, QA), QA == 'hi there', 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)). @@ -363,7 +367,7 @@ Dir = 'output/js_wam_parser_cli', make_directory_path(Dir), write_wam_javascript_project( - [user:sum3/2, user:unwrap/2, user:gt_float/1], + [user:sum3/2, user:unwrap/2, user:gt_float/1, user:qatom/1], [emit_mode(interpreter)], Dir), run_node_args(Dir, ['sum3/2', '[1,2,3]'], ListExit, ListOut), assertion(ListExit =:= 0), @@ -375,7 +379,10 @@ assertion(sub_string(CompOut, _, _, _, "A2 = a")), run_node_args(Dir, ['gt_float/1', '3.14'], FloatExit, FloatOut), assertion(FloatExit =:= 0), - assertion(node_succeeded(FloatOut)). + assertion(node_succeeded(FloatOut)), + run_node_args(Dir, ['qatom/1', '\'hello world\''], QExit, QOut), + assertion(QExit =:= 0), + assertion(node_succeeded(QOut)). :- end_tests(js_wam_builtins). From b7f136416442139cd2f3a672ca55d09065be1044 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 30 Aug 2026 17:08:35 +0000 Subject: [PATCH 3/3] Align parse probe with SWI read_term_from_atom/2 binding rules read_term_from_atom/2 does not bind caller variables from names inside the atom. Test partial lists as [a,b|c] and quoted atoms via CLI argv. Co-authored-by: johns243a --- tests/test_wam_javascript_builtins.pl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_wam_javascript_builtins.pl b/tests/test_wam_javascript_builtins.pl index 1a57073bb..09a1cf25f 100644 --- a/tests/test_wam_javascript_builtins.pl +++ b/tests/test_wam_javascript_builtins.pl @@ -232,9 +232,7 @@ read_term_from_atom('foo(a, bar(b), 3)', T), T == foo(a, bar(b), 3), read_term_from_atom('3.14', F), F > 3.0, read_term_from_atom('-2', N), N == -2, - read_term_from_atom('[a|Rest]', PL), PL = [a, b], Rest == [b], - atom_concat('\'', 'hi there\'', QSrc), - read_term_from_atom(QSrc, QA), QA == 'hi there', + 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)).