From 5067daf97e8d2677f24f7364d37e6f5581f2e77b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 1 Sep 2026 01:54:48 +0000 Subject: [PATCH 1/3] Import frozen A1 cli_args example (oracle, harness, patternjs). Checked out from claude/peerhailer-exploratory-docs-aodas5. Top-level reference and harness stay verbatim for the A2 JS-WAM compile. Co-authored-by: johns243a --- examples/cli_args/.gitignore | 2 + examples/cli_args/README.md | 323 +++++ examples/cli_args/cli_args.pl | 659 ++++++++++ examples/cli_args/compare_jsonl.mjs | 115 ++ examples/cli_args/diff_runner.mjs | 43 + examples/cli_args/diff_runner.pl | 111 ++ examples/cli_args/gen_cases.mjs | 153 +++ examples/cli_args/oracle/cliArgs.js | 305 +++++ examples/cli_args/oracle/cliArgs.test.mjs | 107 ++ examples/cli_args/oracle/package.json | 6 + examples/cli_args/patternjs/.gitignore | 2 + examples/cli_args/patternjs/README.md | 73 ++ examples/cli_args/patternjs/build.pl | 62 + examples/cli_args/patternjs/build.sh | 29 + .../cli_args/patternjs/cliArgs.generated.mjs | 1125 +++++++++++++++++ examples/cli_args/patternjs/cliArgs.mjs | 108 ++ .../patternjs/cliArgs.patternjs.test.mjs | 115 ++ .../patternjs/diff_runner_patternjs.mjs | 43 + .../patternjs/run_differential_patternjs.sh | 35 + examples/cli_args/run_differential.sh | 35 + examples/cli_args/test_cli_args.pl | 205 +++ 21 files changed, 3656 insertions(+) create mode 100644 examples/cli_args/.gitignore create mode 100644 examples/cli_args/README.md create mode 100644 examples/cli_args/cli_args.pl create mode 100644 examples/cli_args/compare_jsonl.mjs create mode 100644 examples/cli_args/diff_runner.mjs create mode 100644 examples/cli_args/diff_runner.pl create mode 100644 examples/cli_args/gen_cases.mjs create mode 100644 examples/cli_args/oracle/cliArgs.js create mode 100644 examples/cli_args/oracle/cliArgs.test.mjs create mode 100644 examples/cli_args/oracle/package.json create mode 100644 examples/cli_args/patternjs/.gitignore create mode 100644 examples/cli_args/patternjs/README.md create mode 100644 examples/cli_args/patternjs/build.pl create mode 100644 examples/cli_args/patternjs/build.sh create mode 100644 examples/cli_args/patternjs/cliArgs.generated.mjs create mode 100644 examples/cli_args/patternjs/cliArgs.mjs create mode 100644 examples/cli_args/patternjs/cliArgs.patternjs.test.mjs create mode 100644 examples/cli_args/patternjs/diff_runner_patternjs.mjs create mode 100644 examples/cli_args/patternjs/run_differential_patternjs.sh create mode 100755 examples/cli_args/run_differential.sh create mode 100644 examples/cli_args/test_cli_args.pl diff --git a/examples/cli_args/.gitignore b/examples/cli_args/.gitignore new file mode 100644 index 000000000..f0c8d6fdd --- /dev/null +++ b/examples/cli_args/.gitignore @@ -0,0 +1,2 @@ +# Differential-harness artifacts (regenerated by run_differential.sh, seeded). +.diff_out/ diff --git a/examples/cli_args/README.md b/examples/cli_args/README.md new file mode 100644 index 000000000..f581f70cd --- /dev/null +++ b/examples/cli_args/README.md @@ -0,0 +1,323 @@ +# `cli_args` — a Prolog reference implementation of peerhailer's CLI parser + +**Step A1 of the UnifyWeaver transpilation maturity demonstration.** + +This directory holds a pure-Prolog reimplementation of +[peerhailer](https://github.com/s243a/peerhailer)'s `src/cliArgs.js` — the typed +`hail` argument parser — together with the JavaScript oracle it was written +against, a plunit port of the oracle's 17-test contract matrix, and a +differential harness that runs both implementations over a seeded sample and +compares the results. + +The Prolog here is the *source of truth* for the later steps: A2 pushes it +through the `wam_javascript` target and A3 through the pattern-based JS targets, +and both are judged against this same oracle. So the code mirrors the oracle +exactly — including its quirks — rather than cleaning anything up. + +## Files + +| file | what it is | +| --- | --- | +| `cli_args.pl` | the reference implementation — module `cli_args`, exports `parse_args/2`, `parse_args/3`, `default_registry/1`, `global_options/1`, `is_long_flag/1`, `looks_like_legacy_flag/1` | +| `test_cli_args.pl` | plunit port of all 17 corpus tests; entry point `test_cli_args/0` | +| `diff_runner.pl` | Prolog side of the differential harness (stdin argv-lines → JSONL) | +| `diff_runner.mjs` | oracle side of the differential harness (same protocol) | +| `gen_cases.mjs` | seeded, reproducible case generator | +| `compare_jsonl.mjs` | semantic JSONL comparer (flags compared as objects, not text) | +| `run_differential.sh` | the whole harness in one command | +| `oracle/cliArgs.js` | **read-only** vendored oracle — peerhailer `src/cliArgs.js` | +| `oracle/cliArgs.test.mjs` | **read-only** vendored corpus — peerhailer `test/cliArgs.test.mjs` | +| `oracle/package.json` | scoping file only (`"type": "module"`), so the vendored `.js` loads as ESM under UnifyWeaver's CommonJS-default root `package.json` | + +Oracle provenance: **peerhailer @ `08ad35e`** +(`feat(cli): hail route status/approve/send — drive confidential routing from +the CLI (#59)`). The two `oracle/` files are byte-identical copies and keep +peerhailer's own header; they are never edited here. `oracle/cliArgs.test.mjs` +is kept for reference only — it imports `../src/cliArgs.js`, a path that exists +in peerhailer's tree, not this one, so run it from the peerhailer checkout +(acceptance step 3 below). Its content is what `test_cli_args.pl` ports. + +## The `Result` convention + +```prolog +parse_args(+Argv, -Result) +parse_args(+Argv, +Registry, -Result) +``` + +* `Argv` — a list of SWI **strings** (the raw tokens, i.e. `process.argv.slice(2)`). +* `Result` — `ok(Positional, Flags)` or `error(Message)`. + * `Positional` — list of strings, in the oracle's `positional` order. + * `Flags` — list of `Key-Value` pairs. `Key` is a string; `Value` is a string + or one of the atoms `true` / `false`. + * `Message` — the **exact** string the oracle's `CliError` carries + (`"unknown option --force"`, `"--profile needs a value"`, + `"missing argument: name"`, `"unexpected extra argument: yes"`). + +`parse_args/2` **never throws for a usage error and never fails** — every usage +error comes back as `error(Message)`. The JS transpile maps that back at the +module edge: + +```js +const r = parse_args(argv); // the compiled predicate +if (r.tag === "error") throw new CliError(r.message); +return { positional: r.positional, flags: r.flags }; +``` + +This is deliberate: `throw` is the single construct in the oracle that has no +clean first-order Prolog counterpart, so it is pushed out of the compiled core +and reintroduced only in the hand-written shim. + +### Flags ordering + +`Flags` is an *ordered association list* that reproduces JS object key-insertion +order: + +1. leading globals first (in the order they appear before the command), +2. then the strict/lenient parse order, +3. a later assignment to an existing key **overwrites the value but keeps the + original position** — matching `obj[k] = v` and `{ ...leadingGlobals, ...flags }`. + +Verified against the oracle: + +``` +--name N --state S block bob --state T → ["name"-"N", "state"-"T"] +--state A block bob --state B --include-key → ["state"-"B", "include-key"-true] +daemon --chat=false --route --ui=0 → ["chat"-false, "route"-true, "ui"-true] +``` + +*(Not modelled: JS orders integer-like keys first, so `{"a":1,"2":3}` enumerates +as `2, a`. A flag key can only be integer-like via a token like `--2`, which +neither `isLongFlag` nor `looksLikeLegacyFlag` accepts and which the strict +parser rejects as an unknown option; it is reachable only in the lenient +fallback. The differential comparer compares `flags` as an object, so ordering +is not part of the exactness claim anyway.)* + +## The schema, as data + +`parse_args/3` takes a registry of the same shape `default_registry/1` returns, +mirroring the JS `registry?` parameter: + +``` +Registry ::= list of Name-Entry +Entry ::= schema(Options, Positionals) % a leaf command + | group(Actions) % grouped: keyed by first positional +Actions ::= list of ActionName-schema(Options, Positionals) +Options ::= list of OptionName-Kind +Kind ::= boolean | string | optional +Positionals ::= list of names; "[name]" is optional, "...name" is variadic +``` + +A grouped command — `route`, verbatim from `cli_args.pl`: + +```prolog +"route"-group([ + "discover"-schema([ "dest"-string, "dest-file"-string, + "control"-string, "port"-string ], []), + "status"-schema([ "dest"-string, "dest-file"-string, + "control"-string, "port"-string ], []), + "approve"-schema([ "dest"-string, "dest-file"-string, + "seal-key"-string, "seal-key-file"-string, + "control"-string, "port"-string ], []), + "send"-schema([ "dest"-string, "dest-file"-string, "public"-boolean, + "ttl"-string, "budget"-string, + "control"-string, "port"-string ], + ["...message"]) +]) +``` + +and a leaf command: + +```prolog +"block"-schema(["include-key"-boolean], ["name"]). +``` + +The four oracle mechanisms map one-to-one onto four predicate groups: + +| oracle | Prolog | +| --- | --- | +| `parseArgs` leading-globals scan | `scan_leading_globals/4`, `is_global_key/1` | +| `schemaFor` | `schema_for/5`, `registry_entry/3`, `action_entry/3` | +| `parseStrict` | `parse_strict/4`, `strict_loop/8`, `strict_option/11`, `check_arity/3` | +| `parseLenient` | `parse_lenient/3`, `lenient_loop/5` | + +## Running it + +### 1. The contract matrix (plunit) + +``` +$ swipl -q -g test_cli_args -t halt examples/cli_args/test_cli_args.pl +................. +cli_args contract matrix: 17/17 tests passed +``` + +### 2. The differential harness + +``` +$ bash examples/cli_args/run_differential.sh +== generating seeded cases == +cases: 5067 lines -> .../examples/cli_args/.diff_out/cases.txt +== running the JavaScript oracle (peerhailer parseArgs) == +== running the Prolog reference implementation == +== comparing == +sample size: 5067 argv-lines + oracle ok results: 4150 + oracle errors: 917 +divergences: 0 +message mismatches: 0 +``` + +The sample is the 17-test corpus (25 lines), a 42-line hand-written quirk +sweep, 3000 uniform pseudorandom lines of length 2–7 over the task's token +alphabet, and 2000 more of the same rooted at a command token so the strict path +gets deep coverage. The PRNG is mulberry32 with a fixed seed, so the sample is +byte-reproducible. + +Artifacts land in `.diff_out/` (git-ignored) for inspection: `cases.txt`, +`oracle.jsonl`, `prolog.jsonl`. + +The two runners share a stdin/stdout protocol, so either can be swapped for a +transpiled build in A2/A3 without touching the comparer: + +``` +stdin : one argv-line per line, tokens space-separated; lines with no tokens skipped +stdout : one JSON object per input line — + {"ok":{"positional":[...],"flags":{...}}} | {"error":""} +``` + +### 3. The oracle's own suite (unchanged) + +``` +$ node --test /home/user/s243a/peerhailer/test/cliArgs.test.mjs +# pass 17 +# fail 0 +``` + +## Oracle subtleties chased down + +Each of these is a place the oracle's *actual* behaviour differs from what a +"reasonable" reimplementation would do. All are modelled and all are covered by +the quirk sweep in `gen_cases.mjs`. + +1. **Two different flag regexes.** The strict parser uses + `isLongFlag = /^--[a-z][a-z0-9-]*(=|$)/i`; the lenient parser uses + `looksLikeLegacyFlag = /^--[a-z][a-z0-9-]*$/i` — bare `--word` only. That is + the whole reason `tunnels --a --b=c` reads `--b=c` as the *value* of `--a`. + Both are re-expressed as character logic (`is_long_flag/1`, + `looks_like_legacy_flag/1`) — no `library(pcre)`, so the compilers only see + first-order Prolog. The `(=|$)` alternation needs no backtracking: `=` is not + in `[a-z0-9-]`, so the maximal run always stops at it, and the character + after any *shorter* run is by construction a run character (neither `=` nor + end-of-string). Hence "maximal run, then require `=` or end" is exact. +2. **A PEM is not a flag.** `-----BEGIN-PUBLIC-KEY-----` has `-` as its third + character, so `isLongFlag` rejects it and `--key -----BEGIN-...` consumes it + as a value. Getting the "first char after `--` must be a *letter*" rule wrong + silently breaks `--key`, `--dest` and `--seal-key`. +3. **`--` is asymmetric between the two parsers.** `parseStrict` treats it as a + terminator (its tail becomes pure positionals, so `--state child.json` after + it stays payload); `parseLenient` has no `--` handling at all, so `--` there + parses as a flag with the **empty-string key** (`tunnels -- bob` → + `{"": "bob"}`). The leading-globals scan stops at `--` without consuming it, + and `--` is never accepted as a global's value (`--state --` leaves + `state: true` in the scan — and then, because the command position holds + `--`, the whole line falls to the lenient parse, where `--state` *does* take + `--` as its value: `{"state":"--"}`). +4. **Boolean truthiness is `!== "false"`, not "is a boolean-ish word".** + `--ui=0`, `--ui=no`, `--ui=off` are all **on**; only `--ui=false` is off. +5. **The `optional` kind and `unknown option` share one `if (!kind)` test.** An + option whose kind is falsy is refused; the `optional` kind then falls through + the `string` branch, so `--debug` with nothing usable after it lands as bare + `true` while `--debug 2` takes the value. +6. **Bracket stripping in the missing-argument message.** `names[values.length]` + is reported after `.replace(/[[\]]/g, "")`, so an `[optional]` name in that + slot appears without its brackets. (Reachable only via a schema where an + optional precedes a required positional; modelled anyway.) +7. **JS property lookup walks `Object.prototype`.** This is the big one. The + oracle looks things up with plain property access on object literals — + `key in GLOBAL_OPTIONS`, `options[key]`, `registry[command]`, + `entry.actions[action]` — so an inherited `Object.prototype` member answers as + a **present, truthy** entry: + * `--toString x block bob` → `toString` passes `key in GLOBAL_OPTIONS`, so it + is consumed as a *leading global* → `{"toString":"x"}`. + * `block bob --toString v` → `options["toString"]` is a function: truthy, so + not "unknown", and neither `"boolean"` nor `"string"`, so it takes the + `optional` path → `{"toString":"v"}`. + * `constructor bob` → `registry["constructor"]` is the `Object` function, + which has no `.actions`/`.options`/`.positionals`, so it parses as + `schema([], [])` → `unexpected extra argument: bob`. + * `route toString --state P` → `entry.actions["toString"]` is truthy, so the + action is *consumed* → `positional: ["route","toString"]`. + * `--__proto__ x block bob` and `block bob --__proto__ v` → the key resolves, + the value is consumed, but `obj["__proto__"] = ` invokes the + inherited accessor, which **ignores** it — no property is created, so the + flags come back empty. + + `cli_args.pl` models this with `js_object_prototype_keys/1` plus the + `__proto__` no-op in `flags_set/4`. It is isolated to four places + (`is_global_key/1`, `option_kind/3`, `registry_entry/3`, `action_entry/3`) + and is the only part of the file that exists purely to mirror a JS artifact. +8. **`maybeAction` falsiness.** `maybeAction && entry.actions[maybeAction]` — + an empty-string action token is falsy in JS, so `parseArgs(["route",""])` + falls to the lenient parse rather than looking `""` up. Modelled by the + `Action \== ""` guard in `schema_for/5`. +9. **The lenient fallback is passed the *whole* argv.** When the leading-globals + scan finds no command (or a non-global `--token` in command position), the + accumulated globals are **discarded** and `parseLenient(argv)` re-parses from + token 0. So `--state P --force x` never sees a strict parse and `--force` + greedily eats `x`. +10. **Uppercase matters twice.** Both regexes are case-insensitive, so `--STATE` + *is* a long flag — but the option table lookup is case-**sensitive**, so + `block --STATE x` fails with `unknown option --STATE` rather than falling + through to a positional. + +## Notes for A2 (`wam_javascript`) and A3 (pattern targets) + +The file is written to be transpilable, but a few things are worth flagging: + +* **No cuts, no exceptions, no `assert`/`retract`, no `library(pcre)`, no + `library(apply)`.** `cli_args.pl` uses only `sub_string/5`, `string_length/2`, + `string_concat/3`, `string_chars/2`, `char_code/2`, `length/2`, `reverse/2`, + `append/3`, arithmetic comparison and `if-then-else`. `format/2` appears only + in `diff_runner.pl` and `test_cli_args.pl`, never in the module under test. + (`test_cli_args.pl` does use `exclude/3`, `forall/2` and `split_string/4` — it + is harness code, not compilation input.) +* **SWI strings vs atoms.** Everything — tokens, keys, values, schema names — is + an SWI *string*, compared with `==`/`\==`, never with `=`. A target that + collapses strings to atoms (or to char lists) must keep `""` distinct from + `''` and must not intern, because `flags_set/4` compares keys by `==`. The + empty-string flag key from `--` (subtlety 3) is a live case. +* **Mixed-type values.** `Flags` values are strings *or* the atoms `true`/`false`. + A target with a tagged value representation must keep `true` (atom) distinct + from `"true"` (string) — the corpus asserts `flags["include-key"] === true`, + not `"true"`. `emit_json_value/1` in `diff_runner.pl` is the one place that + discrimination is observable. +* **Determinism.** Every predicate in `cli_args.pl` is det or semidet, and + `parse_args/2` is verified det (`deterministic/1` is `true` after the call). + Two predicates were reshaped to *stay* det under first-argument indexing: + `merge_flags/3` delegates to `merge_flags_/3` so the driving list is argument + one, and `nth0_default/4` is a single clause with an explicit conditional + because its first argument is an integer. A target that emits choice points + for these will not be wrong, but it will be slow and it will diverge from the + det-ness assumption the JS shim relies on (it calls the predicate once and + reads a single solution). +* **Structural output arguments.** `parse_strict/4` and `check_arity/3` return a + tagged term (`ok(...)`/`err(Message)`) instead of throwing. Targets that + flatten compound terms need to keep the tag; this is the pattern the whole + "no exceptions in the compiled core" design rests on. +* **`string_chars/2` in both directions.** `is_long_flag/1` decomposes a string + to chars; `strip_brackets/2` rebuilds one from chars. A target that only + implements the decompose direction will fail `missing argument:` messages for + `[optional]` names. +* **Deep recursion.** `lenient_loop/5` and `strict_loop/8` recurse once per argv + token with accumulators, so they are last-call-optimisable. Argv is short in + practice, but a target without LCO will hold one frame per token. +* **Integer arithmetic only.** `is`, `=:=`, `<`, `>`, `>=`, `=<` on small + non-negative integers (string indices, counts, character codes). No floats, + no bignums. +* **`char_code/2` and code-point ranges.** `js_alpha/1` and `js_flag_char/1` + compare against `0'a`, `0'z`, `0'A`, `0'Z`, `0'0`, `0'9`, `0'-`. A target + compiling `0'x` literals incorrectly breaks both regexes silently — the + differential harness is the guard against exactly that. +* **Reusing the harness.** A2/A3 should re-point `run_differential.sh`'s Prolog + leg at the transpiled build (it only needs the stdin/stdout protocol above) + and re-run with the same seed. Same cases, same expected JSONL, zero + divergences is the bar. diff --git a/examples/cli_args/cli_args.pl b/examples/cli_args/cli_args.pl new file mode 100644 index 000000000..4b53e2f2c --- /dev/null +++ b/examples/cli_args/cli_args.pl @@ -0,0 +1,659 @@ +% SPDX-License-Identifier: MIT OR Apache-2.0 +% Copyright (c) 2026 John William Creighton (s243a) +% +% cli_args.pl -- a Prolog reference implementation of peerhailer's CLI argument +% parser (`src/cliArgs.js`, peerhailer @ 08ad35e), byte-for-byte behaviour- +% compatible with the JavaScript oracle vendored at `oracle/cliArgs.js`. +% +% This is step A1 of the UnifyWeaver transpilation maturity demo: the Prolog +% here is the source of truth that later steps push through the JS targets, so +% it mirrors the oracle exactly rather than "cleaning it up". +% +% Result convention +% ----------------- +% parse_args(+Argv, -Result) +% Argv : list of SWI *strings* (the raw tokens; `process.argv.slice(2)`). +% Result : ok(Positional, Flags) | error(Message) +% Positional : list of strings, in the oracle's `positional` order. +% Flags : list of Key-Value pairs. Key is a string; Value is a +% string or one of the atoms `true` / `false`. The list is +% in the oracle's JS object key-insertion order (leading +% globals first, then parse order; a later assignment to an +% existing key overwrites the value but keeps its position). +% Message : the exact string the oracle's `CliError` carries. +% parse_args/2 never throws for a usage error and never fails; the JS +% transpile maps error(M) -> `throw new CliError(M)` at the module edge. +% +% Everything is pure: no assert/retract, no exceptions, no library(pcre) (both +% oracle regexes are re-expressed as character logic so the compilers only ever +% see first-order Prolog). + +:- module(cli_args, [ + parse_args/2, % +Argv, -Result + parse_args/3, % +Argv, +Registry, -Result + default_registry/1, % -Registry + global_options/1, % -Options + is_long_flag/1, % +Token (the strict parser's flag test) + looks_like_legacy_flag/1 % +Token (the lenient parser's flag test) +]). + +% =========================================================================== +% 1. The grammar, as data +% =========================================================================== +% +% Registry = list of Name-Entry pairs (a plain ordered assoc; the oracle's +% `COMMANDS` object, with JS key-insertion order preserved). +% +% Entry ::= schema(Options, Positionals) % a leaf command +% | group(Actions) % grouped: keyed by 1st positional +% Actions::= list of ActionName-schema(Options, Positionals) +% Options::= list of OptionName-Kind +% Kind ::= boolean | string | optional +% Positionals ::= list of names; "[name]" is optional, "...name" is variadic. +% +% `route` reads exactly as the oracle's grouped entry does: +% +% "route"-group([ +% "discover"-schema(["dest"-string, "dest-file"-string, +% "control"-string, "port"-string], []), +% ... ]) + +%! global_options(-Options) is det. +% +% The oracle's GLOBAL_OPTIONS: accepted by every command, wherever they appear. +global_options([ + "state"-string, + "name"-string +]). + +%! default_registry(-Registry) is det. +% +% The oracle's COMMANDS. parse_args/3 accepts any registry of this shape, +% mirroring the JS `registry?` parameter. +default_registry([ + "block"-schema(["include-key"-boolean], ["name"]), + + % name is optional: `unblock --key ` lifts a key with no name. + "unblock"-schema(["key"-string], ["[name]"]), + + "add"-schema([ "profile"-string, + "until"-string, + "key"-string, + "key-file"-string, + "transport"-string ], + ["name", "[address]"]), + + "daemon"-schema([ "debug"-optional, + "port"-string, + "host"-string, + "hail-on"-string, + "hail-on-encrypted"-string, + "hail-on-tls"-string, + "tls-cert"-string, + "tls-key"-string, + "allow-origin"-string, + "chat"-boolean, + "route"-boolean, + "ui"-boolean, + "require-target-binding"-boolean, + "require-sealed"-boolean ], + []), + + "commands"-group([ + "add"-schema([], ["name", "...command"]), + "remove"-schema([], ["name"]), + "list"-schema([], []) + ]), + + % Live operations on a running daemon's in-memory routed key store (M3b). + "route"-group([ + "discover"-schema([ "dest"-string, "dest-file"-string, + "control"-string, "port"-string ], []), + "status"-schema([ "dest"-string, "dest-file"-string, + "control"-string, "port"-string ], []), + "approve"-schema([ "dest"-string, "dest-file"-string, + "seal-key"-string, "seal-key-file"-string, + "control"-string, "port"-string ], []), + "send"-schema([ "dest"-string, "dest-file"-string, "public"-boolean, + "ttl"-string, "budget"-string, + "control"-string, "port"-string ], + ["...message"]) + ]), + + "profiles"-group([ + "add"-schema(["allows"-string, "description"-string], ["name"]), + "remove"-schema(["force"-boolean, "reassign"-string], ["name"]), + "pin"-schema([], ["name"]), + "unpin"-schema([], ["name"]), + "list"-schema([], []) + ]) +]). + +% --------------------------------------------------------------------------- +% JS `Object.prototype` keys. +% +% The oracle looks options and commands up with plain property access on object +% literals (`key in GLOBAL_OPTIONS`, `options[key]`, `registry[command]`), so an +% inherited Object.prototype member answers as a *present, truthy* entry. That +% is observable behaviour, so it is modelled here rather than papered over. +% See README "Oracle subtleties" #7. +% --------------------------------------------------------------------------- +js_object_prototype_keys([ + "constructor", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", + "toString", + "valueOf", + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__", + "__proto__" +]). + +js_object_prototype_key(Key) :- + js_object_prototype_keys(Keys), + string_member(Key, Keys). + +% =========================================================================== +% 2. The two flag regexes, as character logic +% =========================================================================== + +%! is_long_flag(+Token) is semidet. +% +% The strict parser's notion: JS `/^--[a-z][a-z0-9-]*(=|$)/i`. +% After `--` the first character must be a letter, then the maximal run of +% [A-Za-z0-9-] must be followed by `=` or by end-of-string. (Backtracking to +% a shorter run can never help: the character after a shorter run is itself a +% run character, hence neither `=` nor the end.) +% +% Deliberately false for a dash-leading PEM such as +% `-----BEGIN-PUBLIC-KEY-----`, whose third character is `-`, not a letter -- +% which is why `--key ` keeps working. +is_long_flag(Token) :- + string_chars(Token, Chars), + Chars = ['-', '-', First | Rest], + js_alpha(First), + long_flag_tail(Rest). + +long_flag_tail([]). +long_flag_tail([C|Cs]) :- + ( C == '=' + -> true + ; js_flag_char(C), + long_flag_tail(Cs) + ). + +%! looks_like_legacy_flag(+Token) is semidet. +% +% The *legacy* parser's flag test: JS `/^--[a-z][a-z0-9-]*$/i` -- a bare +% `--word` only. Used solely by parse_lenient/3, and the reason +% `tunnels --a --b=c` reads `--b=c` as the value of `--a`. +looks_like_legacy_flag(Token) :- + string_chars(Token, Chars), + Chars = ['-', '-', First | Rest], + js_alpha(First), + legacy_flag_tail(Rest). + +legacy_flag_tail([]). +legacy_flag_tail([C|Cs]) :- + js_flag_char(C), + legacy_flag_tail(Cs). + +js_alpha(C) :- + char_code(C, X), + ( X >= 0'a, X =< 0'z + -> true + ; X >= 0'A, X =< 0'Z + ). + +js_flag_char(C) :- + char_code(C, X), + ( X >= 0'a, X =< 0'z + -> true + ; X >= 0'A, X =< 0'Z + -> true + ; X >= 0'0, X =< 0'9 + -> true + ; X =:= 0'- + ). + +% =========================================================================== +% 3. Small pure string / list helpers +% =========================================================================== + +%! starts_with(+String, +Prefix) is semidet. +starts_with(String, Prefix) :- + string_length(String, L), + string_length(Prefix, N), + L >= N, + sub_string(String, 0, N, _, Sub), + Sub == Prefix. + +%! substring_from(+String, +Start, -Sub) is det. % JS String#slice(Start) +substring_from(String, Start, Sub) :- + string_length(String, L), + Len is L - Start, + sub_string(String, Start, Len, 0, Sub). + +%! substring_range(+String, +Start, +End, -Sub) is det. % JS slice(Start, End) +substring_range(String, Start, End, Sub) :- + Len is End - Start, + sub_string(String, Start, Len, _, Sub). + +%! first_equals_index(+String, -Index) is det. +% +% JS `String#indexOf("=")`: the 0-based index, or -1 when absent. +first_equals_index(String, Index) :- + string_chars(String, Chars), + first_char_index(Chars, '=', 0, Index). + +first_char_index([], _Target, _I, -1). +first_char_index([C|Cs], Target, I, Index) :- + ( C == Target + -> Index = I + ; I1 is I + 1, + first_char_index(Cs, Target, I1, Index) + ). + +%! split_flag_token(+Token, -Key, -Inline) is det. +% +% Mirrors the oracle's `equals`/`key`/`inlineValue` triple. +% Inline is `none` or `some(Value)`. Token is known to start with `--`, so the +% `=` (if any) is always at index >= 2. +split_flag_token(Token, Key, Inline) :- + first_equals_index(Token, Eq), + ( Eq >= 0 + -> substring_range(Token, 2, Eq, Key), + ValueStart is Eq + 1, + substring_from(Token, ValueStart, Value), + Inline = some(Value) + ; substring_from(Token, 2, Key), + Inline = none + ). + +%! string_member(+String, +List) is semidet. +string_member(S, [X|Xs]) :- + ( S == X + -> true + ; string_member(S, Xs) + ). + +%! pair_lookup(+Pairs, +Key, -Value) is semidet. +% +% Own-property lookup in an ordered assoc list (fails when absent). +pair_lookup([K-V|Rest], Key, Value) :- + ( K == Key + -> Value = V + ; pair_lookup(Rest, Key, Value) + ). + +%! nth0_default(+Index, +List, +Default, -Elem) is det. % JS `xs[i] ?? d` +% +% One clause with an explicit conditional so the call stays deterministic +% (the index, not the list, is the first argument). +nth0_default(I, List, Default, Elem) :- + ( List = [X|Xs] + -> ( I =:= 0 + -> Elem = X + ; I1 is I - 1, + nth0_default(I1, Xs, Default, Elem) + ) + ; Elem = Default + ). + +%! last_element(+List, -Last) is semidet. % fails on [] +last_element([X|Xs], Last) :- + ( Xs == [] + -> Last = X + ; last_element(Xs, Last) + ). + +% --------------------------------------------------------------------------- +% Flag maps: ordered Key-Value lists with JS object-assignment semantics. +% --------------------------------------------------------------------------- + +%! flags_set(+Flags0, +Key, +Value, -Flags) is det. +% +% `obj[Key] = Value`: overwrite in place (keeping the original position) or +% append at the end. Assigning to `__proto__` on an object literal invokes the +% inherited accessor, which ignores a primitive -- so no property is created. +flags_set(Flags0, Key, Value, Flags) :- + ( Key == "__proto__" + -> Flags = Flags0 + ; flags_put(Flags0, Key, Value, Flags) + ). + +flags_put([], Key, Value, [Key-Value]). +flags_put([K-V|Rest], Key, Value, Out) :- + ( K == Key + -> Out = [K-Value|Rest] + ; Out = [K-V|Rest1], + flags_put(Rest, Key, Value, Rest1) + ). + +%! merge_flags(+Base, +Overlay, -Merged) is det. +% +% JS `{ ...Base, ...Overlay }`: Overlay's value wins, Base's position wins. +% The accumulator loop puts the driving list first so first-argument indexing +% keeps it deterministic. +merge_flags(Base, Overlay, Merged) :- + merge_flags_(Overlay, Base, Merged). + +merge_flags_([], Base, Base). +merge_flags_([K-V|Rest], Base, Merged) :- + flags_set(Base, K, V, Base1), + merge_flags_(Rest, Base1, Merged). + +% =========================================================================== +% 4. schemaFor +% =========================================================================== + +%! schema_for(+Command, +MaybeAction, +Registry, -Schema, -ActionConsumed) is semidet. +% +% Fails where the oracle returns `null` (unmigrated command, or a grouped +% command with an unknown/absent action) -- which signals the lenient fallback. +% MaybeAction is `none` or `some(String)`; JS treats both `undefined` and the +% empty string as falsy, hence the `A \== ""` guard. +schema_for(Command, MaybeAction, Registry, Schema, ActionConsumed) :- + registry_entry(Registry, Command, Entry), + ( Entry = group(Actions) + -> MaybeAction = some(Action), + Action \== "", + action_entry(Actions, Action, Schema), + ActionConsumed = true + ; Schema = Entry, + ActionConsumed = false + ). + +registry_entry(Registry, Command, Entry) :- + ( pair_lookup(Registry, Command, E) + -> Entry = E + ; js_object_prototype_key(Command), + Entry = schema([], []) % an inherited member has no .actions, + ). % no .options and no .positionals + +action_entry(Actions, Action, Schema) :- + ( pair_lookup(Actions, Action, S) + -> Schema = S + ; js_object_prototype_key(Action), + Schema = schema([], []) + ). + +% =========================================================================== +% 5. parseLenient -- the legacy parse, kept verbatim +% =========================================================================== +% +% Greedy, untyped, no `--` handling, and its own narrower flag test. + +%! parse_lenient(+Argv, -Positional, -Flags) is det. +parse_lenient(Argv, Positional, Flags) :- + lenient_loop(Argv, [], [], PositionalRev, Flags), + reverse(PositionalRev, Positional). + +lenient_loop([], PosAcc, FlagsAcc, PosAcc, FlagsAcc). +lenient_loop([Token|Rest], PosAcc, FlagsAcc, PosOut, FlagsOut) :- + ( starts_with(Token, "--") + -> split_flag_token(Token, Key, Inline), + ( Inline = some(Value) + -> flags_set(FlagsAcc, Key, Value, Flags1), + lenient_loop(Rest, PosAcc, Flags1, PosOut, FlagsOut) + ; Rest = [Next|Rest1], + \+ looks_like_legacy_flag(Next) + -> flags_set(FlagsAcc, Key, Next, Flags1), + lenient_loop(Rest1, PosAcc, Flags1, PosOut, FlagsOut) + ; flags_set(FlagsAcc, Key, true, Flags1), + lenient_loop(Rest, PosAcc, Flags1, PosOut, FlagsOut) + ) + ; lenient_loop(Rest, [Token|PosAcc], FlagsAcc, PosOut, FlagsOut) + ). + +% =========================================================================== +% 6. parseStrict +% =========================================================================== + +%! parse_strict(+Tokens, +Schema, +Positional0, -Outcome) is det. +% +% Tokens are the argv tail after the command (and action, if consumed); +% Positional0 already holds those. Outcome is ok(Positional, Flags) | err(Msg). +parse_strict(Tokens, schema(Options, Positionals), Positional0, Outcome) :- + strict_loop(Tokens, false, Options, [], [], ValuesRev, Flags, Status), + ( Status = err(Message) + -> Outcome = err(Message) + ; reverse(ValuesRev, Values), + check_arity(Values, Positionals, ArityStatus), + ( ArityStatus = err(Message2) + -> Outcome = err(Message2) + ; append(Positional0, Values, Positional), + Outcome = ok(Positional, Flags) + ) + ). + +% strict_loop(+Tokens, +AfterTerminator, +Options, +ValuesAcc, +FlagsAcc, +% -ValuesRev, -Flags, -Status) +strict_loop([], _After, _Options, ValuesAcc, FlagsAcc, ValuesAcc, FlagsAcc, ok). +strict_loop([Token|Rest], After, Options, ValuesAcc, FlagsAcc, + ValuesRev, Flags, Status) :- + ( After == true + -> strict_loop(Rest, true, Options, [Token|ValuesAcc], FlagsAcc, + ValuesRev, Flags, Status) + ; Token == "--" + -> % its tail is payload, never options + strict_loop(Rest, true, Options, ValuesAcc, FlagsAcc, + ValuesRev, Flags, Status) + ; \+ starts_with(Token, "--") + -> strict_loop(Rest, After, Options, [Token|ValuesAcc], FlagsAcc, + ValuesRev, Flags, Status) + ; split_flag_token(Token, Key, Inline), + ( option_kind(Options, Key, Kind) + -> strict_option(Kind, Key, Inline, Rest, After, Options, + ValuesAcc, FlagsAcc, ValuesRev, Flags, Status) + ; string_concat("unknown option --", Key, Message), + ValuesRev = ValuesAcc, Flags = FlagsAcc, Status = err(Message) + ) + ). + +% strict_option(+Kind, +Key, +Inline, +Rest, +After, +Options, ...) +strict_option(Kind, Key, Inline, Rest, After, Options, + ValuesAcc, FlagsAcc, ValuesRev, Flags, Status) :- + ( Kind == boolean + -> % `--x=false` is off, anything else (including bare) is on. + ( Inline = some(Inline1) + -> ( Inline1 == "false" -> Bool = false ; Bool = true ) + ; Bool = true + ), + flags_set(FlagsAcc, Key, Bool, Flags1), + strict_loop(Rest, After, Options, ValuesAcc, Flags1, + ValuesRev, Flags, Status) + ; Inline = some(Inline2) + -> flags_set(FlagsAcc, Key, Inline2, Flags1), + strict_loop(Rest, After, Options, ValuesAcc, Flags1, + ValuesRev, Flags, Status) + ; % A value that is missing or looks like the next option is not a value. + next_value(Rest, MaybeValue), + ( Kind == string + -> ( MaybeValue = some(Value) + -> flags_set(FlagsAcc, Key, Value, Flags1), + Rest = [_Consumed|Rest1], + strict_loop(Rest1, After, Options, ValuesAcc, Flags1, + ValuesRev, Flags, Status) + ; string_concat("--", Key, Prefix), + string_concat(Prefix, " needs a value", Message), + ValuesRev = ValuesAcc, Flags = FlagsAcc, Status = err(Message) + ) + ; % `optional` (and the untyped-but-truthy inherited kind): a + % following value if one is there, else the bare `true`. + ( MaybeValue = some(Value2) + -> flags_set(FlagsAcc, Key, Value2, Flags1), + Rest = [_Consumed2|Rest2], + strict_loop(Rest2, After, Options, ValuesAcc, Flags1, + ValuesRev, Flags, Status) + ; flags_set(FlagsAcc, Key, true, Flags1), + strict_loop(Rest, After, Options, ValuesAcc, Flags1, + ValuesRev, Flags, Status) + ) + ) + ). + +%! next_value(+Rest, -MaybeValue) is det. +% +% JS: `next !== undefined && next !== "--" && !isLongFlag(next) ? next : null`. +next_value([], none). +next_value([Next|_], MaybeValue) :- + ( Next \== "--", + \+ is_long_flag(Next) + -> MaybeValue = some(Next) + ; MaybeValue = none + ). + +%! option_kind(+SchemaOptions, +Key, -Kind) is semidet. +% +% `{ ...GLOBAL_OPTIONS, ...schema.options }[key]`, truthiness included: a +% command option shadows a global, and an inherited Object.prototype member +% answers truthy-but-untyped (`other`), which the oracle then treats exactly +% like `optional`. Fails only where the oracle sees a falsy kind. +option_kind(SchemaOptions, Key, Kind) :- + ( pair_lookup(SchemaOptions, Key, K1) + -> Kind = K1 + ; global_options(Globals), + pair_lookup(Globals, Key, K2) + -> Kind = K2 + ; js_object_prototype_key(Key), + Kind = other + ). + +% --------------------------------------------------------------------------- +% Positional arity, against the schema's names. +% --------------------------------------------------------------------------- + +%! check_arity(+Values, +Names, -Status) is det. +check_arity(Values, Names, Status) :- + length(Values, ValueCount), + length(Names, NameCount), + ( last_element(Names, Last), + starts_with(Last, "...") + -> Variadic = true + ; Variadic = false + ), + count_required(Names, 0, Required), + ( ValueCount < Required + -> nth0_default(ValueCount, Names, "argument", RawName), + strip_brackets(RawName, Name), + string_concat("missing argument: ", Name, Message), + Status = err(Message) + ; Variadic == false, + ValueCount > NameCount + -> nth0_default(NameCount, Values, "", Extra), + string_concat("unexpected extra argument: ", Extra, Message2), + Status = err(Message2) + ; Status = ok + ). + +count_required([], Acc, Acc). +count_required([N|Ns], Acc, Required) :- + ( starts_with(N, "[") + -> Acc1 = Acc + ; starts_with(N, "...") + -> Acc1 = Acc + ; Acc1 is Acc + 1 + ), + count_required(Ns, Acc1, Required). + +%! strip_brackets(+String, -Stripped) is det. % JS `.replace(/[[\]]/g, "")` +strip_brackets(String, Stripped) :- + string_chars(String, Chars), + drop_brackets(Chars, Kept), + string_chars(Stripped, Kept). + +drop_brackets([], []). +drop_brackets([C|Cs], Kept) :- + ( ( C == '[' ; C == ']' ) + -> Kept = Kept1 + ; Kept = [C|Kept1] + ), + drop_brackets(Cs, Kept1). + +% =========================================================================== +% 7. parseArgs +% =========================================================================== + +%! parse_args(+Argv, -Result) is det. +parse_args(Argv, Result) :- + default_registry(Registry), + parse_args(Argv, Registry, Result). + +%! parse_args(+Argv, +Registry, -Result) is det. +% +% Strict for a scheduled command, lenient otherwise. +parse_args(Argv, Registry, Result) :- + % Find the command: the first bare token, skipping any leading global option + % and its value (globals may appear before the command, e.g. + % `--state P block bob`). + scan_leading_globals(Argv, [], Rest, LeadingGlobals), + ( Rest = [Command|AfterCommand], + \+ starts_with(Command, "--") + -> ( AfterCommand = [Action0|_] + -> MaybeAction = some(Action0) + ; MaybeAction = none + ), + ( schema_for(Command, MaybeAction, Registry, Schema, ActionConsumed) + -> ( ActionConsumed == true + -> MaybeAction = some(Action), + AfterCommand = [_|Tail], + Positional0 = [Command, Action] + ; Tail = AfterCommand, + Positional0 = [Command] + ), + parse_strict(Tail, Schema, Positional0, Outcome), + ( Outcome = ok(Positional, Flags) + -> merge_flags(LeadingGlobals, Flags, Merged), + Result = ok(Positional, Merged) + ; Outcome = err(Message), + Result = error(Message) + ) + ; lenient_result(Argv, Result) + ) + ; lenient_result(Argv, Result) + ). + +lenient_result(Argv, ok(Positional, Flags)) :- + parse_lenient(Argv, Positional, Flags). + +%! scan_leading_globals(+Argv, +Acc, -Rest, -Globals) is det. +% +% Consumes leading `--global [value]` tokens. Bails (leaving the token in +% Rest) on the first `--token` whose key is not in GLOBAL_OPTIONS, and never +% crosses a bare `--`. +scan_leading_globals([], Acc, [], Acc). +scan_leading_globals([Token|Rest], Acc, RestOut, Globals) :- + ( starts_with(Token, "--"), + Token \== "--" + -> split_flag_token(Token, Key, Inline), + ( is_global_key(Key) + -> ( Inline = some(Value) + -> flags_set(Acc, Key, Value, Acc1), + scan_leading_globals(Rest, Acc1, RestOut, Globals) + ; next_value(Rest, MaybeValue), + ( MaybeValue = some(Value2) + -> flags_set(Acc, Key, Value2, Acc1), + Rest = [_Consumed|Rest1], + scan_leading_globals(Rest1, Acc1, RestOut, Globals) + ; flags_set(Acc, Key, true, Acc1), + scan_leading_globals(Rest, Acc1, RestOut, Globals) + ) + ) + ; % a non-global before the command -> leave it for the parse below + RestOut = [Token|Rest], Globals = Acc + ) + ; RestOut = [Token|Rest], Globals = Acc + ). + +%! is_global_key(+Key) is semidet. % JS `key in GLOBAL_OPTIONS` +is_global_key(Key) :- + ( global_options(Globals), + pair_lookup(Globals, Key, _) + -> true + ; js_object_prototype_key(Key) + ). diff --git a/examples/cli_args/compare_jsonl.mjs b/examples/cli_args/compare_jsonl.mjs new file mode 100644 index 000000000..018d1b78a --- /dev/null +++ b/examples/cli_args/compare_jsonl.mjs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2026 John William Creighton (s243a) +// +// compare_jsonl.mjs -- the semantic comparer for the differential harness. +// +// node compare_jsonl.mjs +// +// Compares the two runners' results line-for-line by CONTENT, not by text: +// `positional` is an ordered array, `flags` is an unordered object (key order +// inside "flags" is irrelevant -- JS object key order and the Prolog list order +// need not agree), booleans stay distinct from the strings "true"/"false". +// +// Reports two counts separately: +// divergences - class mismatch (ok vs error) or differing ok contents +// message mismatches - both sides errored, but with different messages +// Exit status is non-zero if either count is non-zero. + +import { readFileSync } from "node:fs"; + +const [, , casesPath, oraclePath, prologPath] = process.argv; +if (!casesPath || !oraclePath || !prologPath) { + console.error("usage: compare_jsonl.mjs "); + process.exit(2); +} + +const readLines = (p) => readFileSync(p, "utf8").split("\n").filter((l) => l !== ""); +const tokenize = (line) => line.split(" ").filter(Boolean); + +const cases = readFileSync(casesPath, "utf8") + .split("\n") + .filter((l) => tokenize(l).length > 0); +const oracle = readLines(oraclePath); +const prolog = readLines(prologPath); + +if (oracle.length !== cases.length || prolog.length !== cases.length) { + console.error( + `FATAL: line-count mismatch — cases=${cases.length} oracle=${oracle.length} prolog=${prolog.length}`, + ); + process.exit(2); +} + +const sameFlags = (a, b) => { + const ka = Object.keys(a).sort(); + const kb = Object.keys(b).sort(); + if (ka.length !== kb.length) return false; + for (let i = 0; i < ka.length; i += 1) { + if (ka[i] !== kb[i]) return false; + const va = a[ka[i]]; + const vb = b[kb[i]]; + // strict: true !== "true", false !== "false" + if (typeof va !== typeof vb || va !== vb) return false; + } + return true; +}; + +const sameArray = (a, b) => + a.length === b.length && a.every((x, i) => x === b[i]); + +let divergences = 0; +let messageMismatches = 0; +let crashes = 0; +const samples = []; + +for (let i = 0; i < cases.length; i += 1) { + const o = JSON.parse(oracle[i]); + const p = JSON.parse(prolog[i]); + + if (o.crash !== undefined || p.crash !== undefined) { + crashes += 1; + if (samples.length < 10) samples.push({ line: cases[i], oracle: o, prolog: p, why: "crash" }); + continue; + } + + const oIsOk = o.ok !== undefined; + const pIsOk = p.ok !== undefined; + + if (oIsOk !== pIsOk) { + divergences += 1; + if (samples.length < 10) samples.push({ line: cases[i], oracle: o, prolog: p, why: "class" }); + continue; + } + + if (oIsOk) { + if (!sameArray(o.ok.positional, p.ok.positional) || !sameFlags(o.ok.flags, p.ok.flags)) { + divergences += 1; + if (samples.length < 10) samples.push({ line: cases[i], oracle: o, prolog: p, why: "content" }); + } + continue; + } + + if (o.error !== p.error) { + messageMismatches += 1; + if (samples.length < 10) samples.push({ line: cases[i], oracle: o, prolog: p, why: "message" }); + } +} + +const okCount = oracle.filter((l) => l.startsWith('{"ok"')).length; + +console.log(`sample size: ${cases.length} argv-lines`); +console.log(` oracle ok results: ${okCount}`); +console.log(` oracle errors: ${cases.length - okCount}`); +console.log(`divergences: ${divergences}`); +console.log(`message mismatches: ${messageMismatches}`); +if (crashes) console.log(`crashes: ${crashes}`); + +if (samples.length) { + console.log("\nfirst differing cases:"); + for (const s of samples) { + console.log(` [${s.why}] ${JSON.stringify(s.line)}`); + console.log(` oracle: ${JSON.stringify(s.oracle)}`); + console.log(` prolog: ${JSON.stringify(s.prolog)}`); + } +} + +process.exit(divergences === 0 && messageMismatches === 0 && crashes === 0 ? 0 : 1); diff --git a/examples/cli_args/diff_runner.mjs b/examples/cli_args/diff_runner.mjs new file mode 100644 index 000000000..86d70a5b2 --- /dev/null +++ b/examples/cli_args/diff_runner.mjs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2026 John William Creighton (s243a) +// +// diff_runner.mjs -- the ORACLE side of the differential harness. +// +// Protocol: reads argv-lines on stdin (one line per case, tokens separated by +// spaces; blank lines are skipped), runs peerhailer's `parseArgs` from the +// vendored oracle copy, and prints one JSON object per line: +// +// {"ok":{"positional":[...],"flags":{...}}} +// {"error":""} +// {"crash":""} // never expected; a hard failure +// +// The Prolog side (diff_runner.pl) prints the identical shape. +// +// node examples/cli_args/diff_runner.mjs < lines.txt > oracle.jsonl + +import { parseArgs, CliError } from "./oracle/cliArgs.js"; + +const tokenize = (line) => line.split(" ").filter(Boolean); + +function runLine(line) { + try { + const { positional, flags } = parseArgs(tokenize(line)); + return { ok: { positional, flags } }; + } catch (err) { + if (err instanceof CliError) return { error: err.message }; + return { crash: String((err && err.message) || err) }; + } +} + +const chunks = []; +for await (const chunk of process.stdin) chunks.push(chunk); +const input = Buffer.concat(chunks).toString("utf8"); + +const out = []; +for (const line of input.split("\n")) { + // Skip only lines that carry no tokens at all -- the exact rule diff_runner.pl + // applies, so the two runners stay line-for-line aligned. + if (tokenize(line).length === 0) continue; + out.push(JSON.stringify(runLine(line))); +} +process.stdout.write(out.length ? out.join("\n") + "\n" : ""); diff --git a/examples/cli_args/diff_runner.pl b/examples/cli_args/diff_runner.pl new file mode 100644 index 000000000..8ed364848 --- /dev/null +++ b/examples/cli_args/diff_runner.pl @@ -0,0 +1,111 @@ +% SPDX-License-Identifier: MIT OR Apache-2.0 +% Copyright (c) 2026 John William Creighton (s243a) +% +% diff_runner.pl -- the PROLOG side of the differential harness. +% +% Protocol: reads argv-lines on stdin (one line per case, tokens separated by +% spaces; lines that carry no tokens are skipped), runs cli_args:parse_args/2, +% and prints one JSON object per line in exactly the shape diff_runner.mjs +% prints for the oracle: +% +% {"ok":{"positional":[...],"flags":{...}}} +% {"error":""} +% +% The JSON is hand-rolled (no library(http/json)) so the emitted shape is under +% direct control and the file stays dependency-free for the transpile steps. +% +% swipl -q -g main -t halt examples/cli_args/diff_runner.pl < lines.txt > prolog.jsonl + +:- module(diff_runner, [main/0]). + +:- use_module(cli_args). + +main :- + read_line_to_string(user_input, Line), + process_lines(Line). + +process_lines(end_of_file) :- !. +process_lines(Line) :- + split_string(Line, " ", "", Raw), + exclude(==(""), Raw, Tokens), + ( Tokens == [] + -> true + ; parse_args(Tokens, Result), + emit_result(Result), + nl + ), + read_line_to_string(user_input, Next), + process_lines(Next). + +emit_result(ok(Positional, Flags)) :- + write('{"ok":{"positional":'), + emit_string_array(Positional), + write(',"flags":'), + emit_flags(Flags), + write('}}'). +emit_result(error(Message)) :- + write('{"error":'), + emit_json_string(Message), + write('}'). + +emit_string_array(List) :- + write('['), + emit_string_items(List, first), + write(']'). + +emit_string_items([], _). +emit_string_items([X|Xs], Position) :- + ( Position == first + -> true + ; write(',') + ), + emit_json_string(X), + emit_string_items(Xs, rest). + +emit_flags(Flags) :- + write('{'), + emit_flag_items(Flags, first), + write('}'). + +emit_flag_items([], _). +emit_flag_items([Key-Value|Rest], Position) :- + ( Position == first + -> true + ; write(',') + ), + emit_json_string(Key), + write(':'), + emit_json_value(Value), + emit_flag_items(Rest, rest). + +emit_json_value(true) :- !, write(true). +emit_json_value(false) :- !, write(false). +emit_json_value(Value) :- emit_json_string(Value). + +emit_json_string(Value) :- + string_chars(Value, Chars), + write('"'), + emit_json_chars(Chars), + write('"'). + +emit_json_chars([]). +emit_json_chars([C|Cs]) :- + emit_json_char(C), + emit_json_chars(Cs). + +emit_json_char(C) :- + char_code(C, Code), + ( Code =:= 0'" + -> write('\\"') + ; Code =:= 0'\\ + -> write('\\\\') + ; Code =:= 0'\n + -> write('\\n') + ; Code =:= 0'\r + -> write('\\r') + ; Code =:= 0'\t + -> write('\\t') + ; Code < 0x20 + -> format("\\u~|~`0t~16r~4+", [Code]) + ; write(C) + ). diff --git a/examples/cli_args/gen_cases.mjs b/examples/cli_args/gen_cases.mjs new file mode 100644 index 000000000..9ca616585 --- /dev/null +++ b/examples/cli_args/gen_cases.mjs @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2026 John William Creighton (s243a) +// +// gen_cases.mjs -- the seeded case generator for the differential harness. +// +// Emits argv-lines on stdout, one case per line: +// 1. every line of the 17-test corpus, +// 2. a hand-written "quirk sweep" of oracle edge behaviours (the JS +// prototype-chain lookups, `--` placements, repeated assignments, case), +// 3. UNIFORM_LINES pseudorandom lines of length 2..7 drawn uniformly from the +// task's token alphabet, +// 4. ROOTED_LINES more of the same, but with a command as the first token, so +// the strict path (not just the lenient fallback) is deeply exercised. +// +// The PRNG is mulberry32 with a fixed seed, so the sample is reproducible +// byte-for-byte on any machine. +// +// node examples/cli_args/gen_cases.mjs > cases.txt + +const SEED = 0x1a2b3c4d; +const UNIFORM_LINES = 3000; +const ROOTED_LINES = 2000; +const MIN_LEN = 2; +const MAX_LEN = 7; + +function mulberry32(a) { + return function () { + a |= 0; + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +// --- the token alphabet, exactly as specified for this harness --------------- +const COMMANDS_AND_ACTIONS = [ + "block", "add", "daemon", "unblock", "commands", "route", "profiles", + "discover", "status", "approve", "send", "pin", "remove", "list", +]; +const POSITIONALS = ["bob", "deploy", "./run.sh", "127.0.0.1:9100", "2"]; +const OPTIONS = [ + "--include-key", "--include-key=false", "--include-key=yes", + "--profile", "--profile=x", "--key", "--debug", "--debug=2", + "--state", "child.json", "--force", "--porfile", "--hail-on-tls", "eth0", +]; +const DASH_LEADING = ["-----BEGIN-PUBLIC-KEY-----"]; +const TERMINATOR = ["--"]; + +const ALPHABET = [ + ...COMMANDS_AND_ACTIONS, ...POSITIONALS, ...OPTIONS, ...DASH_LEADING, ...TERMINATOR, +]; + +// --- 1. the 17-test corpus --------------------------------------------------- +const CORPUS = [ + "block --include-key bob", + "block bob --include-key", + "block bob --include-keey", + "block bob --include-key=false", + "block bob --include-key=yes", + "block bob --include-key yes", + "--state P block bob", + "block bob --state P", + "commands add deploy -- ./run.sh --env prod", + "commands add deploy -- ./run.sh --state child.json", + "add bob --key -----BEGIN-PUBLIC-KEY-----", + "daemon --debug", + "daemon --debug 2", + "daemon --debug=2", + "daemon --require-target-binding yes", + "profiles pin trusted --force", + "profiles remove temp --force", + "add bob --profile", + "block", + "tunnels add acp 127.0.0.1:9100 --anything here", + "daemon --hail-on-encrypted tailscale0", + "daemon --hail-on-tls eth0", + "unblock --key ABCDEF12", + "unblock bob", + "tunnels --a --b=c", +]; + +// --- 2. the quirk sweep ------------------------------------------------------ +// Deliberately probes behaviour the task alphabet cannot reach: JS +// prototype-chain property lookups (`key in GLOBAL_OPTIONS`, `options[key]`, +// `registry[command]`, `entry.actions[action]`), the `__proto__` assignment +// no-op, `--` in every position, repeated assignment order, and the /i flag. +const QUIRKS = [ + "--toString x block bob", + "block bob --toString v", + "block bob --constructor", + "block bob --valueOf", + "route toString --state P", + "constructor bob", + "hasOwnProperty --state P", + "isPrototypeOf", + "block bob --propertyIsEnumerable=x", + "--hasOwnProperty block bob", + "block bob --__proto__ v", + "--__proto__ x block bob", + "tunnels --__proto__=v", + "profiles valueOf x", + "commands toString a b c", + "--state -- block bob", + "--state --profile block bob", + "block bob --", + "tunnels -- bob", + "-- block bob", + "--", + "block bob --state=--x", + "block --STATE x", + "block bob --Include-Key", + "daemon --debug --state P", + "route send -- --dest x", + "route send --dest -----BEGIN-PUBLIC-KEY-----", + "profiles", + "route", + "commands list extra", + "add", + "add bob address extra", + "unblock", + "--name N --state S block bob --state T", + "--state A block bob --state B --include-key", + "block bob --state P --name Q", + "daemon --chat=false --route --ui=0", + "commands add deploy -- -- ./run.sh", + "route approve --seal-key -----BEGIN-PUBLIC-KEY----- --port 9", + "--state", + "--state --", + "block --key", +]; + +const rand = mulberry32(SEED); +const pick = (xs) => xs[Math.floor(rand() * xs.length)]; +const pickLen = () => MIN_LEN + Math.floor(rand() * (MAX_LEN - MIN_LEN + 1)); + +const lines = [...CORPUS, ...QUIRKS]; + +for (let n = 0; n < UNIFORM_LINES; n += 1) { + const len = pickLen(); + const tokens = []; + for (let k = 0; k < len; k += 1) tokens.push(pick(ALPHABET)); + lines.push(tokens.join(" ")); +} + +for (let n = 0; n < ROOTED_LINES; n += 1) { + const len = pickLen(); + const tokens = [pick(COMMANDS_AND_ACTIONS)]; + for (let k = 1; k < len; k += 1) tokens.push(pick(ALPHABET)); + lines.push(tokens.join(" ")); +} + +process.stdout.write(lines.join("\n") + "\n"); diff --git a/examples/cli_args/oracle/cliArgs.js b/examples/cli_args/oracle/cliArgs.js new file mode 100644 index 000000000..881dee448 --- /dev/null +++ b/examples/cli_args/oracle/cliArgs.js @@ -0,0 +1,305 @@ +/** + * `hail`'s argument grammar, as data — the local typed parser chosen in + * `docs/cli-arg-parsing.md` (Candidate A). It lives in `src/` rather than + * `bin/hail.js` so `tsc` checks it and `node --test` can drive it directly. + * + * A command with a schema entry is parsed **strictly**: each option is typed, an + * unknown option is refused, `--` ends option parsing (its tail is preserved as + * positionals so a forwarded command keeps its own flags), and positional arity is + * checked. A command with **no** entry falls back to the legacy lenient parse, so + * unmigrated commands behave exactly as before and the migration can proceed leaf + * by leaf. + * + * The output shape — `{ positional, flags }` — is the one `bin/hail.js` already + * consumes, so a migrated command's handler reads the *corrected* shape (a boolean + * is `true`, a positional after a boolean is kept) with no change of its own. + * + * @module cliArgs + */ + +/** A user-facing parse error; its message is already suitable after a `hail:` prefix. */ +export class CliError extends Error {} + +/** + * @typedef {"boolean" | "string" | "optional"} OptionKind + * boolean: never takes a value. string: requires one. optional: bare is `true`, + * a following non-flag token is its value (used by `--debug [minutes]`). + * + * @typedef {{ + * options?: Record, + * positionals?: string[], // names; a `[name]` is optional, `...name` is variadic (rest) + * actions?: Record>, // grouped commands keyed by first positional + * }} CommandSchema + */ + +/** Options accepted by every command, wherever they appear on the line. */ +export const GLOBAL_OPTIONS = /** @type {Record} */ ({ + state: "string", + name: "string", +}); + +/** + * Per-command schemas. Only the commands proven against the contract matrix are + * migrated so far; the rest fall back to the lenient parse. See + * `docs/cli-arg-parsing.md` for the checklist of what remains. + * + * @type {Record} + */ +export const COMMANDS = { + block: { positionals: ["name"], options: { "include-key": "boolean" } }, + // name is optional: `unblock --key ` lifts a key with no name. + unblock: { positionals: ["[name]"], options: { key: "string" } }, + add: { + positionals: ["name", "[address]"], + options: { profile: "string", until: "string", key: "string", "key-file": "string", transport: "string" }, + }, + daemon: { + options: { + debug: "optional", + port: "string", + host: "string", + "hail-on": "string", + "hail-on-encrypted": "string", + "hail-on-tls": "string", + "tls-cert": "string", + "tls-key": "string", + "allow-origin": "string", + chat: "boolean", + route: "boolean", + ui: "boolean", + "require-target-binding": "boolean", + "require-sealed": "boolean", + }, + }, + commands: { + actions: { + add: { positionals: ["name", "...command"], options: {} }, + remove: { positionals: ["name"], options: {} }, + list: { positionals: [], options: {} }, + }, + }, + // Live operations on a running daemon's in-memory routed key store (M3b), posted to + // its control API — so they take a `--control ` (default 127.0.0.1:) and the + // destination identity key via `--dest`/`--dest-file`, not the offline state file. + route: { + actions: { + discover: { positionals: [], options: { dest: "string", "dest-file": "string", control: "string", port: "string" } }, + status: { positionals: [], options: { dest: "string", "dest-file": "string", control: "string", port: "string" } }, + approve: { + positionals: [], + options: { dest: "string", "dest-file": "string", "seal-key": "string", "seal-key-file": "string", control: "string", port: "string" }, + }, + send: { + positionals: ["...message"], + options: { dest: "string", "dest-file": "string", public: "boolean", ttl: "string", budget: "string", control: "string", port: "string" }, + }, + }, + }, + profiles: { + actions: { + add: { positionals: ["name"], options: { allows: "string", description: "string" } }, + remove: { positionals: ["name"], options: { force: "boolean", reassign: "string" } }, + pin: { positionals: ["name"], options: {} }, + unpin: { positionals: ["name"], options: {} }, + list: { positionals: [], options: {} }, + }, + }, +}; + +/** A `--word` or `--word=…` option token (the strict parser's notion). @param {string} token */ +const isLongFlag = (token) => /^--[a-z][a-z0-9-]*(=|$)/i.test(token); +/** + * The *legacy* parser's flag test — a bare `--word` only, so `--x=y` does not read + * as a flag when it follows another option. Used solely by `parseLenient` to keep + * unmigrated commands byte-for-byte as they were; the strict parser uses the + * broader `isLongFlag`. + * @param {string} token + */ +const looksLikeLegacyFlag = (token) => /^--[a-z][a-z0-9-]*$/i.test(token); + +/** + * Resolve the schema for a command (and, for a grouped command, its action — the + * first positional). Returns `null` for an unmigrated command, which signals the + * lenient fallback. + * + * @param {string} command + * @param {string | undefined} maybeAction + * @param {Record} registry + * @returns {{ schema: CommandSchema, actionConsumed: boolean } | null} + */ +function schemaFor(command, maybeAction, registry) { + const entry = registry[command]; + if (!entry) return null; + if (entry.actions) { + const action = maybeAction && entry.actions[maybeAction]; + if (!action) return null; // unknown/absent action → lenient (the handler reports usage) + return { schema: action, actionConsumed: true }; + } + return { schema: entry, actionConsumed: false }; +} + +/** + * The legacy lenient parse: greedy, untyped, no `--`. Kept verbatim for commands + * not yet given a schema so their behaviour is unchanged. + * + * @param {string[]} argv + * @returns {{ positional: string[], flags: Record }} + */ +function parseLenient(argv) { + /** @type {string[]} */ + const positional = []; + /** @type {Record} */ + const flags = {}; + for (let i = 0; i < argv.length; i += 1) { + const token = argv[i]; + if (token === undefined) break; + if (!token.startsWith("--")) { + positional.push(token); + continue; + } + const equals = token.indexOf("="); + if (equals !== -1) { + flags[token.slice(2, equals)] = token.slice(equals + 1); + continue; + } + const key = token.slice(2); + const next = argv[i + 1]; + if (next === undefined || looksLikeLegacyFlag(next)) flags[key] = true; + else { + flags[key] = next; + i += 1; + } + } + return { positional, flags }; +} + +/** + * Parse against a resolved leaf schema. `command` (and the action, if any) are + * already in `positional`; this fills the rest and validates. + * + * @param {string[]} argv the full argv + * @param {number} start index of the first token after command (+action) + * @param {string[]} positional accumulator, already holding command (+action) + * @param {CommandSchema} schema + */ +function parseStrict(argv, start, positional, schema) { + /** @type {Record} */ + const flags = {}; + const options = { ...GLOBAL_OPTIONS, ...(schema.options ?? {}) }; + const values = []; + let afterTerminator = false; + + for (let i = start; i < argv.length; i += 1) { + const token = argv[i]; + if (token === undefined) break; + if (afterTerminator) { + values.push(token); + continue; + } + if (token === "--") { + afterTerminator = true; // its tail is payload, never options + continue; + } + if (!token.startsWith("--")) { + values.push(token); + continue; + } + const equals = token.indexOf("="); + const key = equals === -1 ? token.slice(2) : token.slice(2, equals); + const inlineValue = equals === -1 ? null : token.slice(equals + 1); + const kind = options[key]; + if (!kind) throw new CliError(`unknown option --${key}`); + + if (kind === "boolean") { + // `--x=false` is off, anything else (including bare) is on. + flags[key] = inlineValue === null ? true : inlineValue !== "false"; + continue; + } + if (inlineValue !== null) { + flags[key] = inlineValue; + continue; + } + const next = argv[i + 1]; + // A value that is missing or looks like the next option is not a value. Note + // `isLongFlag` deliberately does not match a `--...` PEM (whose 3rd char is a + // dash, not a letter), so `--key ` keeps working. + const value = next !== undefined && next !== "--" && !isLongFlag(next) ? next : null; + if (kind === "string") { + if (value === null) throw new CliError(`--${key} needs a value`); + flags[key] = value; + i += 1; + continue; + } + // optional: a following value if one is there, else the bare `true`. + if (value !== null) { + flags[key] = value; + i += 1; + } else { + flags[key] = true; + } + } + + // Positional arity, against the schema's names. + const names = schema.positionals ?? []; + const variadic = Boolean(names[names.length - 1]?.startsWith("...")); + const required = names.filter((n) => !n.startsWith("[") && !n.startsWith("...")).length; + if (values.length < required) { + throw new CliError(`missing argument: ${(names[values.length] ?? "argument").replace(/[[\]]/g, "")}`); + } + if (!variadic && values.length > names.length) { + throw new CliError(`unexpected extra argument: ${values[names.length] ?? ""}`); + } + + positional.push(...values); + return { positional, flags }; +} + +/** + * Parse `hail` arguments. Strict for a scheduled command, lenient otherwise. + * + * @param {string[]} argv `process.argv.slice(2)` + * @param {Record} [registry] + * @returns {{ positional: string[], flags: Record }} + */ +export function parseArgs(argv, registry = COMMANDS) { + // Find the command: the first bare token, skipping any leading global option and + // its value (globals may appear before the command, e.g. `--state P block bob`). + let i = 0; + const leadingGlobals = /** @type {Record} */ ({}); + while (i < argv.length && (argv[i] ?? "").startsWith("--") && argv[i] !== "--") { + const token = argv[i]; + if (token === undefined) break; + const equals = token.indexOf("="); + const key = equals === -1 ? token.slice(2) : token.slice(2, equals); + if (!(key in GLOBAL_OPTIONS)) break; // a non-global before the command → leave it for the parse below + if (equals !== -1) { + leadingGlobals[key] = token.slice(equals + 1); + i += 1; + } else { + const next = argv[i + 1]; + if (next !== undefined && next !== "--" && !isLongFlag(next)) { + leadingGlobals[key] = next; + i += 1; + } else { + leadingGlobals[key] = true; + } + i += 1; + } + } + + const command = argv[i]; + if (command === undefined || command.startsWith("--")) return parseLenient(argv); + + const action = argv[i + 1]; + const resolved = schemaFor(command, action, registry); + if (!resolved) return parseLenient(argv); + + const positional = [command]; + let start = i + 1; + if (resolved.actionConsumed) { + positional.push(/** @type {string} */ (action)); + start = i + 2; + } + const { positional: pos, flags } = parseStrict(argv, start, positional, resolved.schema); + return { positional: pos, flags: { ...leadingGlobals, ...flags } }; +} diff --git a/examples/cli_args/oracle/cliArgs.test.mjs b/examples/cli_args/oracle/cliArgs.test.mjs new file mode 100644 index 000000000..ba25e7e2d --- /dev/null +++ b/examples/cli_args/oracle/cliArgs.test.mjs @@ -0,0 +1,107 @@ +/** + * The contract matrix from docs/cli-arg-parsing.md, proven at the parser level. + * (Point 14 — help generated from the same schemas — is a separate feature, not + * yet built, so it is not asserted here.) + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { parseArgs, CliError } from "../src/cliArgs.js"; + +const of = (line) => parseArgs(line.split(" ").filter(Boolean)); + +test("1 & 2: a boolean flag works before or after the positional", () => { + for (const line of ["block --include-key bob", "block bob --include-key"]) { + const { positional, flags } = of(line); + assert.deepEqual(positional, ["block", "bob"], line); + assert.equal(flags["include-key"], true, line); + } +}); + +test("3: an unknown option is refused against the command schema", () => { + assert.throws(() => of("block bob --include-keey"), CliError); +}); + +test("4: --include-key=false is off; =anything-else is on", () => { + assert.equal(of("block bob --include-key=false").flags["include-key"], false); + assert.equal(of("block bob --include-key=yes").flags["include-key"], true); +}); + +test("5: a value given to a boolean flag lands as an extra positional and fails", () => { + assert.throws(() => of("block bob --include-key yes"), /extra argument: yes/); +}); + +test("6: --state selects the same state before or after the command", () => { + assert.equal(of("--state P block bob").flags.state, "P"); + assert.equal(of("block bob --state P").flags.state, "P"); +}); + +test("7: `--` preserves a forwarded command with its own flags", () => { + const { positional } = of("commands add deploy -- ./run.sh --env prod"); + // The handler reads rest.slice(2).join(" "); rest is positional after the command. + assert.deepEqual(positional.slice(3), ["./run.sh", "--env", "prod"]); +}); + +test("8: after `--`, a --state token is payload, not the global option", () => { + const { flags, positional } = of("commands add deploy -- ./run.sh --state child.json"); + assert.equal(flags.state, undefined, "not swallowed as the global"); + assert.ok(positional.includes("--state") && positional.includes("child.json"), "kept as payload"); +}); + +test("9: --key keeps an inline dash-leading PEM as its value", () => { + const pem = "-----BEGIN-PUBLIC-KEY-----"; + const { flags } = parseArgs(["add", "bob", "--key", pem]); + assert.equal(flags.key, pem); +}); + +test("10: --debug is bare-true, spaced, or =valued", () => { + assert.equal(of("daemon --debug").flags.debug, true); + assert.equal(of("daemon --debug 2").flags.debug, "2"); + assert.equal(of("daemon --debug=2").flags.debug, "2"); +}); + +test("11: a value handed to a boolean daemon flag fails loudly", () => { + assert.throws(() => of("daemon --require-target-binding yes"), /extra argument: yes/); +}); + +test("12: --force is refused on `profiles pin` — it belongs to `profiles remove`", () => { + assert.throws(() => of("profiles pin trusted --force"), /unknown option --force/); + // ...and is accepted on remove. + assert.equal(of("profiles remove temp --force").flags.force, true); +}); + +test("13: a string option with no value fails", () => { + assert.throws(() => of("add bob --profile"), /--profile needs a value/); +}); + +test("a missing required positional fails with the argument name", () => { + assert.throws(() => of("block"), /missing argument: name/); +}); + +test("an unmigrated command falls back to the lenient parse (no schema, no error)", () => { + // `tunnels` has no schema yet: unknown flags are accepted, booleans stay greedy — + // exactly the legacy behaviour, so the migration can proceed leaf by leaf. + const { positional, flags } = of("tunnels add acp 127.0.0.1:9100 --anything here"); + assert.equal(positional[0], "tunnels"); + assert.equal(flags.anything, "here"); +}); + +test("regression: daemon accepts --hail-on-encrypted / --hail-on-tls (Fable)", () => { + assert.equal(of("daemon --hail-on-encrypted tailscale0").flags["hail-on-encrypted"], "tailscale0"); + assert.equal(of("daemon --hail-on-tls eth0").flags["hail-on-tls"], "eth0"); +}); + +test("regression: unblock --key with no name is valid (Fable)", () => { + const { flags, positional } = of("unblock --key ABCDEF12"); + assert.equal(flags.key, "ABCDEF12"); + assert.deepEqual(positional, ["unblock"], "no name required"); + // ...and the name form still works. + assert.deepEqual(of("unblock bob").positional, ["unblock", "bob"]); +}); + +test("the lenient fallback is verbatim: --a --b=c keeps the old greedy reading", () => { + // An unmigrated command must behave exactly as before: the old parser read + // `--b=c` (which is not a bare --word) as the value of --a. + const { flags } = of("tunnels --a --b=c"); + assert.equal(flags.a, "--b=c", "unchanged legacy behaviour on an unmigrated command"); +}); diff --git a/examples/cli_args/oracle/package.json b/examples/cli_args/oracle/package.json new file mode 100644 index 000000000..98bcea7e4 --- /dev/null +++ b/examples/cli_args/oracle/package.json @@ -0,0 +1,6 @@ +{ + "name": "cli-args-oracle", + "private": true, + "type": "module", + "description": "Scoping file only: makes the vendored peerhailer cliArgs.js load as ESM under UnifyWeaver's CommonJS-default root package.json." +} diff --git a/examples/cli_args/patternjs/.gitignore b/examples/cli_args/patternjs/.gitignore new file mode 100644 index 000000000..a4c218c01 --- /dev/null +++ b/examples/cli_args/patternjs/.gitignore @@ -0,0 +1,2 @@ +# differential artifacts (regenerated by run_differential_patternjs.sh) +.diff_out/ diff --git a/examples/cli_args/patternjs/README.md b/examples/cli_args/patternjs/README.md new file mode 100644 index 000000000..e74907c4d --- /dev/null +++ b/examples/cli_args/patternjs/README.md @@ -0,0 +1,73 @@ + + +# `patternjs` — peerhailer's CLI parser, transpiled + +This directory is the **output** of UnifyWeaver's pattern lane applied to +`../cli_args.pl`, plus the two harnesses that hold it to the same bar the Prolog +reference was held to in step A1. + +`../cli_args.pl` is a Prolog reimplementation of peerhailer's `src/cliArgs.js`, +verified against the vendored JavaScript oracle in `../oracle/`. What is here is +that Prolog **compiled back into JavaScript by UnifyWeaver** — the whole program, +all 43 predicates, `parse_args/2` included — and then measured against the same +oracle. + +| | | +| --- | --- | +| contract corpus (`../oracle/cliArgs.test.mjs`, import swapped) | **17 / 17** | +| differential vs the JS oracle, same generator and seed | **5067 lines, 0 divergences, 0 message mismatches** | +| `node --check` on the generated module | clean | +| predicates omitted / goals dropped | none | + +## Files + +| file | what it is | +| --- | --- | +| `build.sh` | **the one command.** Compiles `../cli_args.pl` into `cliArgs.generated.mjs` and `node --check`s it. | +| `build.pl` | the compile itself: read the frozen source's clauses into `user`, then `compile_module/3` with `include_dependencies(true)` through `vanilla_js_target`. | +| `cliArgs.generated.mjs` | **compiler output.** 40 functions — every predicate `parse_args/2` transitively calls — plus the compound-term runtime. Not edited by hand. | +| `cliArgs.mjs` | the **edge shim**: `parseArgs(argv, registry?)` and `class CliError`. Term↔JS conversion only — no parse logic (see the header comment for the exhaustive list of what it does). | +| `cliArgs.patternjs.test.mjs` | peerhailer's own 17-test contract corpus, with **one** line changed: the import. | +| `diff_runner_patternjs.mjs` | the transpiled side of the differential harness; same stdin/JSONL protocol as `../diff_runner.mjs` and `../diff_runner.pl`. | +| `run_differential_patternjs.sh` | the A4 run for this lane: the harness's own generator and comparator, with the transpiled parser opposite the oracle. | + +## Reproducing + +```bash +bash examples/cli_args/patternjs/build.sh +node --test examples/cli_args/patternjs/cliArgs.patternjs.test.mjs +bash examples/cli_args/patternjs/run_differential_patternjs.sh +``` + +## What is hand-written and what is not + +The only hand-written JavaScript that the parser's behaviour passes through is +`cliArgs.mjs`, and it does exactly three things: it turns a JS argv array into the +token list the compiled entry point takes, it turns the compiled `ok(P, F)` term +into `{ positional, flags }` (unwrapping `{$: "-", args: [k, v]}` pairs into a +plain object, in pair order), and it turns `error(M)` into +`throw new CliError(M)`. Every decision about what an argv line *means* — both +flag regexes, the strict/lenient split, the schema lookup, the arity check, the +exact wording of every error message — is in `cliArgs.generated.mjs`, which is +compiler output. + +The `A1` README's standard for this demo was: *a hand-written JS shim around the +compiled pieces would be cheating.* The shim here carries no branch that depends +on an argv token. + +## How to read the generated module + +Four representation choices, all documented in +`docs/proposals/A3_PATTERN_TRANSPILE_REPORT.md`: + +* a Prolog atom or string is a JS string; `true`/`false` are JS booleans; a list + is a JS array; a compound `f(A1..An)` is `{$: "f", args: [...]}` (G-A3-12/13); +* a predicate with more than one output argument returns a positional tuple + (G-A3-9); +* a predicate that is **semidet and has outputs** returns its answer or the + module-private `_uwFail` Symbol; callers test `x !== _uwFail` (G-A3-18); +* `parse_args/2` and `parse_args/3` become `parse_args_2` and `parse_args_3`, + because JavaScript has no arity overloading (G-A3-18). diff --git a/examples/cli_args/patternjs/build.pl b/examples/cli_args/patternjs/build.pl new file mode 100644 index 000000000..08227e668 --- /dev/null +++ b/examples/cli_args/patternjs/build.pl @@ -0,0 +1,62 @@ +:- encoding(utf8). +% SPDX-License-Identifier: MIT OR Apache-2.0 +% Copyright (c) 2026 John William Creighton (s243a) +% +% build.pl -- compile examples/cli_args/cli_args.pl into ONE JavaScript module +% through UnifyWeaver's PATTERN lane (typescript_target -> vanilla_js_target). +% +% Run it with build.sh; the output is cliArgs.generated.mjs next to this file. +% +% Why the clauses are re-asserted into `user` rather than the file consulted: +% the pattern compilers read a predicate's clauses with user:clause/2, and +% cli_args.pl is a module. Reading its terms and asserting the non-directive ones +% into `user` gives the compilers the same clause database SWI would run, without +% editing the frozen source file. Nothing is executed -- G-A3-8's rule is that +% compiling a predicate never runs it. + +:- use_module('../../../src/unifyweaver/targets/typescript_target', []). +:- use_module('../../../src/unifyweaver/targets/vanilla_js_target', []). + +%% load_into_user(+File) +load_into_user(File) :- + setup_call_cleanup(open(File, read, S), load_terms(S), close(S)). + +load_terms(S) :- + read_term(S, T, []), + ( T == end_of_file + -> true + ; ( T = (:- _) -> true ; assertz(user:T) ), + load_terms(S) + ). + +main :- + current_prolog_flag(argv, Argv), + ( Argv = [Src, Out|_] + -> true + ; Src = 'cli_args.pl', Out = 'cliArgs.generated.mjs' + ), + load_into_user(Src), + vanilla_js_target:compile_module( + [pred(parse_args, 2, facts)], + [module_name('CliArgs'), include_dependencies(true)], + Code), + setup_call_cleanup(open(Out, write, W), write(W, Code), close(W)), + ts_report(Code, Out). + +%% ts_report(+Code, +Out) -- say out loud what landed. +ts_report(Code, Out) :- + split_string(Code, "\n", "", Lines), + include(ts_is_fn_line, Lines, FnLines), + length(FnLines, N), + ( sub_string(Code, _, _, _, "WARNING") + -> format("build.pl: WARNING -- some predicates were omitted, see ~w~n", [Out]) + ; true + ), + ( sub_string(Code, _, _, _, "incomplete lowering") + -> format("build.pl: WARNING -- a goal was dropped, see ~w~n", [Out]) + ; true + ), + format("build.pl: wrote ~w (~w function declarations)~n", [Out, N]). + +ts_is_fn_line(L) :- + ( sub_string(L, 0, _, _, "export function ") ; sub_string(L, 0, _, _, "function ") ). diff --git a/examples/cli_args/patternjs/build.sh b/examples/cli_args/patternjs/build.sh new file mode 100644 index 000000000..9747ee2ab --- /dev/null +++ b/examples/cli_args/patternjs/build.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT OR Apache-2.0 +# Copyright (c) 2026 John William Creighton (s243a) +# +# build.sh -- ONE command that turns examples/cli_args/cli_args.pl into a +# JavaScript module node can run, through UnifyWeaver's PATTERN lane +# (typescript_target's structural / general clause lowering, then +# vanilla_js_target's type strip). +# +# bash examples/cli_args/patternjs/build.sh +# +# Output: cliArgs.generated.mjs next to this script -- 41 function declarations, +# every predicate `parse_args/2` transitively calls. Nothing here is hand-written +# JavaScript; the only hand-written file in this directory that touches the +# parser is cliArgs.mjs, and it does term<->JS conversion at the edge and nothing +# else. +# +# The build is a pure compile: it never RUNS cli_args.pl. + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SRC="$HERE/../cli_args.pl" +OUT="$HERE/cliArgs.generated.mjs" + +swipl -q -g main -t halt "$HERE/build.pl" -- "$SRC" "$OUT" + +node --check "$OUT" +echo "build.sh: node --check clean -> $OUT" diff --git a/examples/cli_args/patternjs/cliArgs.generated.mjs b/examples/cli_args/patternjs/cliArgs.generated.mjs new file mode 100644 index 000000000..7111ded07 --- /dev/null +++ b/examples/cli_args/patternjs/cliArgs.generated.mjs @@ -0,0 +1,1125 @@ +// Generated by UnifyWeaver TypeScript Target +// Module: CliArgs + +// --- UnifyWeaver compound-term runtime (G-A3-12) --- BEGIN +// A Prolog compound f(A1..An) is { $: "f", args: [...] }; an atom is a string, +// true/false are booleans (G-A3-13) and a list is an array -- so `x.$` tells a +// compound from all three without ever throwing. _uwEq is Prolog ==/2: structural. +function _uwEq(a, b) { + if (a === b) return true; + if (a == null || b == null) return false; + if (Array.isArray(a) || Array.isArray(b)) { + return Array.isArray(a) && Array.isArray(b) && a.length === b.length && + a.every((x, i) => _uwEq(x, b[i])); + } + if (typeof a === "object" && typeof b === "object") { + return a.$ === b.$ && Array.isArray(a.args) && Array.isArray(b.args) && + a.args.length === b.args.length && + a.args.every((x, i) => _uwEq(x, b.args[i])); + } + return false; +} +// --- UnifyWeaver compound-term runtime (G-A3-12) --- END + +// --- UnifyWeaver failure sentinel (G-A3-18) --- BEGIN +// A predicate that is SEMIDET and has output arguments answers with its value +// (or G-A3-9's tuple) or with this sentinel. A module-private Symbol cannot be +// produced by any Prolog term this target lowers, and cannot be forged by data +// crossing the module edge, so `x !== _uwFail` is an exact test. +const _uwFail = Symbol("uw.fail"); +// --- UnifyWeaver failure sentinel (G-A3-18) --- END + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: flags_put/4 + +export function flags_put(a1, a2, a3) { + if (a1.length === 0) { + return [{$: "-", args: [a2, a3]}, ...[]]; + } + if (a1.length > 0 && a1[0] != null && a1[0].$ === "-" && a1[0].args.length === 2) { + if (a1[0].args[0] === a2) { + return [{$: "-", args: [a1[0].args[0], a3]}, ...a1.slice(1)]; + } else { + const _s0 = flags_put(a1.slice(1), a2, a3); + return [{$: "-", args: [a1[0].args[0], a1[0].args[1]]}, ..._s0]; + } + } + throw new Error("no matching clause for flags_put/4"); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: flags_set/4 + +export function flags_set(a1, a2, a3) { + if (true) { + if (a2 === "__proto__") { + return a1; + } else { + const _s0 = flags_put(a1, a2, a3); + return _s0; + } + } + throw new Error("no matching clause for flags_set/4"); +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 5) { + const argv = process.argv.slice(2, 5).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(flags_set(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Native Clause Lowering +// Predicate: js_alpha/1 + +function js_alpha(arg1) { + const v2 = arg1.charCodeAt(0); + if ((((v2 >= 97 && v2 <= 122)) ? (true) : ((v2 >= 65 && v2 <= 90)))) { + return true; + } + return false; +} + +// CLI entry point +// Arguments are converted by shape (numeric-looking -> number, else string); +// declared parameter types would be more precise -- see G-A3-11.3. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => (s.trim() !== "" && !Number.isNaN(Number(s))) ? Number(s) : s); + console.log(js_alpha(...argv)); +} + + +// Generated by UnifyWeaver TypeScript Target - Native Clause Lowering +// Predicate: js_flag_char/1 + +function js_flag_char(arg1) { + const v2 = arg1.charCodeAt(0); + if ((((v2 >= 97 && v2 <= 122)) ? (true) : ((((v2 >= 65 && v2 <= 90)) ? (true) : ((((v2 >= 48 && v2 <= 57)) ? (true) : (v2 === 45))))))) { + return true; + } + return false; +} + +// CLI entry point +// Arguments are converted by shape (numeric-looking -> number, else string); +// declared parameter types would be more precise -- see G-A3-11.3. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => (s.trim() !== "" && !Number.isNaN(Number(s))) ? Number(s) : s); + console.log(js_flag_char(...argv)); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: legacy_flag_tail/1 + +export function legacy_flag_tail(a1) { + if (a1.length === 0) { + return true; + } + if (a1.length > 0) { + if (js_flag_char(a1[0])) { + return legacy_flag_tail(a1.slice(1)); + } + } + return false; +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: looks_like_legacy_flag/1 + +export function looks_like_legacy_flag(a1) { + if (true) { + const _s0 = Array.from(a1); + if (_s0.length > 0 && _s0[0] === "-" && _s0.slice(1).length > 0 && _s0.slice(1)[0] === "-" && _s0.slice(1).slice(1).length > 0) { + if (js_alpha(_s0.slice(1).slice(1)[0])) { + if (legacy_flag_tail(_s0.slice(1).slice(1).slice(1))) { + return true; + } + } + } + } + return false; +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(looks_like_legacy_flag(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: first_char_index/4 + +export function first_char_index(a1, a2, a3) { + if (a1.length === 0) { + return -1; + } + if (a1.length > 0) { + if (a1[0] === a2) { + return a3; + } else { + const _s0 = (a3 + 1); + const _s1 = first_char_index(a1.slice(1), a2, _s0); + return _s1; + } + } + throw new Error("no matching clause for first_char_index/4"); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: first_equals_index/2 + +export function first_equals_index(a1) { + if (true) { + const _s0 = Array.from(a1); + const _s1 = first_char_index(_s0, "=", 0); + return _s1; + } + throw new Error("no matching clause for first_equals_index/2"); +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(first_equals_index(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Native Clause Lowering +// Predicate: substring_from/3 + +function substring_from(arg1, arg2) { + const v4 = arg1.length; + const v5 = (v4 - arg2); + const arg3 = arg1.slice(arg2, arg2 + v5); + return arg3; +} + +// CLI entry point +// Arguments are converted by shape (numeric-looking -> number, else string); +// declared parameter types would be more precise -- see G-A3-11.3. +if (process.argv.length >= 4) { + const argv = process.argv.slice(2, 4).map( + (s) => (s.trim() !== "" && !Number.isNaN(Number(s))) ? Number(s) : s); + console.log(substring_from(...argv)); +} + + +// Generated by UnifyWeaver TypeScript Target - Native Clause Lowering +// Predicate: substring_range/4 + +function substring_range(arg1, arg2, arg3) { + const v5 = (arg3 - arg2); + const arg4 = arg1.slice(arg2, arg2 + v5); + return arg4; +} + +// CLI entry point +// Arguments are converted by shape (numeric-looking -> number, else string); +// declared parameter types would be more precise -- see G-A3-11.3. +if (process.argv.length >= 5) { + const argv = process.argv.slice(2, 5).map( + (s) => (s.trim() !== "" && !Number.isNaN(Number(s))) ? Number(s) : s); + console.log(substring_range(...argv)); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: split_flag_token/3 + +export function split_flag_token(a1) { + if (true) { + const _s0 = first_equals_index(a1); + if (_s0 >= 0) { + const _s1 = substring_range(a1, 2, _s0); + const _s2 = (_s0 + 1); + const _s3 = substring_from(a1, _s2); + return [_s1, {$: "some", args: [_s3]}]; + } else { + const _s4 = substring_from(a1, 2); + return [_s4, "none"]; + } + } + throw new Error("no matching clause for split_flag_token/3"); +} + +// CLI entry point (multi-output: the tuple is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(split_flag_token(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: starts_with/2 + +export function starts_with(a1, a2) { + if (true) { + const _s0 = a1.length; + const _s1 = a2.length; + if (_s0 >= _s1) { + const _s2 = a1.slice(0, 0 + _s1); + if (_s2 === a2) { + return true; + } + } + } + return false; +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 4) { + const argv = process.argv.slice(2, 4).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(starts_with(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: lenient_loop/5 + +export function lenient_loop(a1, a2, a3) { + if (a1.length === 0) { + return [a2, a3]; + } + if (a1.length > 0) { + if (starts_with(a1[0], "--")) { + const [_s0, _s1] = split_flag_token(a1[0]); + if (_s1 != null && _s1.$ === "some" && _s1.args.length === 1) { + const _s2 = flags_set(a3, _s0, _s1.args[0]); + return lenient_loop(a1.slice(1), a2, _s2); + } else { + if (a1.slice(1).length > 0 && !(looks_like_legacy_flag(a1.slice(1)[0]))) { + const _s3 = flags_set(a3, _s0, a1.slice(1)[0]); + return lenient_loop(a1.slice(1).slice(1), a2, _s3); + } else { + const _s4 = flags_set(a3, _s0, true); + return lenient_loop(a1.slice(1), a2, _s4); + } + } + } else { + return lenient_loop(a1.slice(1), [a1[0], ...a2], a3); + } + } + throw new Error("no matching clause for lenient_loop/5"); +} + +// CLI entry point (multi-output: the tuple is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 5) { + const argv = process.argv.slice(2, 5).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(lenient_loop(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: parse_lenient/3 + +export function parse_lenient(a1) { + if (true) { + const [_s0, _s1] = lenient_loop(a1, [], []); + const _s2 = [..._s0].reverse(); + return [_s2, _s1]; + } + throw new Error("no matching clause for parse_lenient/3"); +} + +// CLI entry point (multi-output: the tuple is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(parse_lenient(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: lenient_result/2 + +export function lenient_result(a1) { + if (true) { + const [_s0, _s1] = parse_lenient(a1); + return {$: "ok", args: [_s0, _s1]}; + } + throw new Error("no matching clause for lenient_result/2"); +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(lenient_result(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: merge_flags_/3 + +export function merge_flags_(a1, a2) { + if (a1.length === 0) { + return a2; + } + if (a1.length > 0 && a1[0] != null && a1[0].$ === "-" && a1[0].args.length === 2) { + const _s0 = flags_set(a2, a1[0].args[0], a1[0].args[1]); + const _s1 = merge_flags_(a1.slice(1), _s0); + return _s1; + } + throw new Error("no matching clause for merge_flags_/3"); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: merge_flags/3 + +export function merge_flags(a1, a2) { + if (true) { + const _s0 = merge_flags_(a2, a1); + return _s0; + } + throw new Error("no matching clause for merge_flags/3"); +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 4) { + const argv = process.argv.slice(2, 4).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(merge_flags(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: count_required/3 + +export function count_required(a1, a2) { + if (a1.length === 0) { + return a2; + } + if (a1.length > 0) { + let _s2; + if (starts_with(a1[0], "[")) { + _s2 = a2; + } else { + let _s1; + if (starts_with(a1[0], "...")) { + _s1 = a2; + } else { + const _s0 = (a2 + 1); + _s1 = _s0; + } + _s2 = _s1; + } + const _s3 = count_required(a1.slice(1), _s2); + return _s3; + } + throw new Error("no matching clause for count_required/3"); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: last_element/2 + +export function last_element(a1) { + if (a1.length > 0) { + if (_uwEq(a1.slice(1), [])) { + return a1[0]; + } else { + const _s0 = last_element(a1.slice(1)); + return _s0; + } + } + return _uwFail; +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: nth0_default/4 + +export function nth0_default(a1, a2, a3) { + if (true) { + if (a2.length > 0) { + if (a1 === 0) { + return a2[0]; + } else { + const _s0 = (a1 + -1); + const _s1 = nth0_default(_s0, a2.slice(1), a3); + return _s1; + } + } else { + return a3; + } + } + throw new Error("no matching clause for nth0_default/4"); +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 5) { + const argv = process.argv.slice(2, 5).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(nth0_default(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: drop_brackets/2 + +export function drop_brackets(a1) { + if (a1.length === 0) { + return []; + } + if (a1.length > 0) { + const _s0 = drop_brackets(a1.slice(1)); + let _s1; + if ((a1[0] === "[" || a1[0] === "]")) { + _s1 = _s0; + } else { + _s1 = [a1[0], ..._s0]; + } + return _s1; + } + throw new Error("no matching clause for drop_brackets/2"); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: strip_brackets/2 + +export function strip_brackets(a1) { + if (true) { + const _s0 = Array.from(a1); + const _s1 = drop_brackets(_s0); + const _s2 = _s1.join(""); + return _s2; + } + throw new Error("no matching clause for strip_brackets/2"); +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(strip_brackets(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: check_arity/3 + +export function check_arity(a1, a2) { + if (true) { + const _s0 = a1.length; + const _s1 = a2.length; + let _t2; + let _s3; + if ((_t2 = last_element(a2)) !== _uwFail && starts_with(_t2, "...")) { + _s3 = true; + } else { + _s3 = false; + } + const _s4 = count_required(a2, 0); + if (_s0 < _s4) { + const _s5 = nth0_default(_s0, a2, "argument"); + const _s6 = strip_brackets(_s5); + const _s7 = ("missing argument: " + _s6); + return {$: "err", args: [_s7]}; + } else { + if ((_s3 === false && _s0 > _s1)) { + const _s8 = nth0_default(_s1, a1, ""); + const _s9 = ("unexpected extra argument: " + _s8); + return {$: "err", args: [_s9]}; + } else { + return "ok"; + } + } + } + throw new Error("no matching clause for check_arity/3"); +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 4) { + const argv = process.argv.slice(2, 4).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(check_arity(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: string_member/2 + +export function string_member(a1, a2) { + if (a2.length > 0) { + if (a1 === a2[0]) { + return true; + } else { + return string_member(a1, a2.slice(1)); + } + } + return false; +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: js_object_prototype_key/1 + +export function js_object_prototype_key(a1) { + if (true) { + if (string_member(a1, ["constructor", ...["hasOwnProperty", ...["isPrototypeOf", ...["propertyIsEnumerable", ...["toLocaleString", ...["toString", ...["valueOf", ...["__defineGetter__", ...["__defineSetter__", ...["__lookupGetter__", ...["__lookupSetter__", ...["__proto__", ...[]]]]]]]]]]]]])) { + return true; + } + } + return false; +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(js_object_prototype_key(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: pair_lookup/3 + +export function pair_lookup(a1, a2) { + if (a1.length > 0 && a1[0] != null && a1[0].$ === "-" && a1[0].args.length === 2) { + if (a1[0].args[0] === a2) { + return a1[0].args[1]; + } else { + const _s0 = pair_lookup(a1.slice(1), a2); + return _s0; + } + } + return _uwFail; +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: option_kind/3 + +export function option_kind(a1, a2) { + if (true) { + let _t0; + if ((_t0 = pair_lookup(a1, a2)) !== _uwFail) { + return _t0; + } else { + let _t1; + if ((_t1 = pair_lookup([{$: "-", args: ["state", "string"]}, ...[{$: "-", args: ["name", "string"]}, ...[]]], a2)) !== _uwFail) { + return _t1; + } else { + if (js_object_prototype_key(a2)) { + return "other"; + } + } + } + } + return _uwFail; +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 4) { + const argv = process.argv.slice(2, 4).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(option_kind(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: long_flag_tail/1 + +export function long_flag_tail(a1) { + if (a1.length === 0) { + return true; + } + if (a1.length > 0) { + if (a1[0] === "=") { + return true; + } else { + if (js_flag_char(a1[0])) { + return long_flag_tail(a1.slice(1)); + } + } + } + return false; +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: is_long_flag/1 + +export function is_long_flag(a1) { + if (true) { + const _s0 = Array.from(a1); + if (_s0.length > 0 && _s0[0] === "-" && _s0.slice(1).length > 0 && _s0.slice(1)[0] === "-" && _s0.slice(1).slice(1).length > 0) { + if (js_alpha(_s0.slice(1).slice(1)[0])) { + if (long_flag_tail(_s0.slice(1).slice(1).slice(1))) { + return true; + } + } + } + } + return false; +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(is_long_flag(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: next_value/2 + +export function next_value(a1) { + if (a1.length === 0) { + return "none"; + } + if (a1.length > 0) { + if ((a1[0] !== "--" && !(is_long_flag(a1[0])))) { + return {$: "some", args: [a1[0]]}; + } else { + return "none"; + } + } + throw new Error("no matching clause for next_value/2"); +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(next_value(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: strict_option/11 + +export function strict_option(a1, a2, a3, a4, a5, a6, a7, a8) { + if (true) { + if (a1 === "boolean") { + let _s1; + if (a3 != null && a3.$ === "some" && a3.args.length === 1) { + let _s0; + if (a3.args[0] === "false") { + _s0 = false; + } else { + _s0 = true; + } + _s1 = _s0; + } else { + _s1 = true; + } + const _s2 = flags_set(a8, a2, _s1); + const [_s3, _s4, _s5] = strict_loop(a4, a5, a6, a7, _s2); + return [_s3, _s4, _s5]; + } else { + if (a3 != null && a3.$ === "some" && a3.args.length === 1) { + const _s6 = flags_set(a8, a2, a3.args[0]); + const [_s7, _s8, _s9] = strict_loop(a4, a5, a6, a7, _s6); + return [_s7, _s8, _s9]; + } else { + const _s10 = next_value(a4); + if (a1 === "string") { + if (_s10 != null && _s10.$ === "some" && _s10.args.length === 1) { + const _s11 = flags_set(a8, a2, _s10.args[0]); + if (a4.length > 0) { + const [_s12, _s13, _s14] = strict_loop(a4.slice(1), a5, a6, a7, _s11); + return [_s12, _s13, _s14]; + } + } else { + const _s15 = ("--" + a2); + const _s16 = (_s15 + " needs a value"); + return [a7, a8, {$: "err", args: [_s16]}]; + } + } else { + if (_s10 != null && _s10.$ === "some" && _s10.args.length === 1) { + const _s17 = flags_set(a8, a2, _s10.args[0]); + if (a4.length > 0) { + const [_s18, _s19, _s20] = strict_loop(a4.slice(1), a5, a6, a7, _s17); + return [_s18, _s19, _s20]; + } + } else { + const _s21 = flags_set(a8, a2, true); + const [_s22, _s23, _s24] = strict_loop(a4, a5, a6, a7, _s21); + return [_s22, _s23, _s24]; + } + } + } + } + } + throw new Error("no matching clause for strict_option/11"); +} + +// CLI entry point (multi-output: the tuple is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 10) { + const argv = process.argv.slice(2, 10).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(strict_option(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: strict_loop/8 + +export function strict_loop(a1, a2, a3, a4, a5) { + if (a1.length === 0) { + return [a4, a5, "ok"]; + } + if (a1.length > 0) { + if (a2 === true) { + return strict_loop(a1.slice(1), true, a3, [a1[0], ...a4], a5); + } else { + if (a1[0] === "--") { + return strict_loop(a1.slice(1), true, a3, a4, a5); + } else { + if (!(starts_with(a1[0], "--"))) { + return strict_loop(a1.slice(1), a2, a3, [a1[0], ...a4], a5); + } else { + const [_s0, _s1] = split_flag_token(a1[0]); + let _t2; + if ((_t2 = option_kind(a3, _s0)) !== _uwFail) { + const [_s3, _s4, _s5] = strict_option(_t2, _s0, _s1, a1.slice(1), a2, a3, a4, a5); + return [_s3, _s4, _s5]; + } else { + const _s6 = ("unknown option --" + _s0); + return [a4, a5, {$: "err", args: [_s6]}]; + } + } + } + } + } + throw new Error("no matching clause for strict_loop/8"); +} + +// CLI entry point (multi-output: the tuple is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 7) { + const argv = process.argv.slice(2, 7).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(strict_loop(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: parse_strict/4 + +export function parse_strict(a1, a2, a3) { + if (a2 != null && a2.$ === "schema" && a2.args.length === 2) { + const [_s0, _s1, _s2] = strict_loop(a1, false, a2.args[0], [], []); + if (_s2 != null && _s2.$ === "err" && _s2.args.length === 1) { + return {$: "err", args: [_s2.args[0]]}; + } else { + const _s3 = [..._s0].reverse(); + const _s4 = check_arity(_s3, a2.args[1]); + if (_s4 != null && _s4.$ === "err" && _s4.args.length === 1) { + return {$: "err", args: [_s4.args[0]]}; + } else { + const _s5 = [...a3, ..._s3]; + return {$: "ok", args: [_s5, _s1]}; + } + } + } + return _uwFail; +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 5) { + const argv = process.argv.slice(2, 5).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(parse_strict(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: is_global_key/1 + +export function is_global_key(a1) { + if (true) { + let _t0; + if ((_t0 = pair_lookup([{$: "-", args: ["state", "string"]}, ...[{$: "-", args: ["name", "string"]}, ...[]]], a1)) !== _uwFail) { + return true; + } else { + if (js_object_prototype_key(a1)) { + return true; + } + } + } + return false; +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(is_global_key(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: scan_leading_globals/4 + +export function scan_leading_globals(a1, a2) { + if (a1.length === 0) { + return [[], a2]; + } + if (a1.length > 0) { + if ((starts_with(a1[0], "--") && a1[0] !== "--")) { + const [_s0, _s1] = split_flag_token(a1[0]); + if (is_global_key(_s0)) { + if (_s1 != null && _s1.$ === "some" && _s1.args.length === 1) { + const _s2 = flags_set(a2, _s0, _s1.args[0]); + return scan_leading_globals(a1.slice(1), _s2); + } else { + const _s3 = next_value(a1.slice(1)); + if (_s3 != null && _s3.$ === "some" && _s3.args.length === 1) { + const _s4 = flags_set(a2, _s0, _s3.args[0]); + if (a1.slice(1).length > 0) { + return scan_leading_globals(a1.slice(1).slice(1), _s4); + } + } else { + const _s5 = flags_set(a2, _s0, true); + return scan_leading_globals(a1.slice(1), _s5); + } + } + } else { + return [[a1[0], ...a1.slice(1)], a2]; + } + } else { + return [[a1[0], ...a1.slice(1)], a2]; + } + } + throw new Error("no matching clause for scan_leading_globals/4"); +} + +// CLI entry point (multi-output: the tuple is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 4) { + const argv = process.argv.slice(2, 4).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(scan_leading_globals(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: action_entry/3 + +export function action_entry(a1, a2) { + if (true) { + let _t0; + if ((_t0 = pair_lookup(a1, a2)) !== _uwFail) { + return _t0; + } else { + if (js_object_prototype_key(a2)) { + return {$: "schema", args: [[], []]}; + } + } + } + return _uwFail; +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 4) { + const argv = process.argv.slice(2, 4).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(action_entry(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: registry_entry/3 + +export function registry_entry(a1, a2) { + if (true) { + let _t0; + if ((_t0 = pair_lookup(a1, a2)) !== _uwFail) { + return _t0; + } else { + if (js_object_prototype_key(a2)) { + return {$: "schema", args: [[], []]}; + } + } + } + return _uwFail; +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 4) { + const argv = process.argv.slice(2, 4).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(registry_entry(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: schema_for/5 + +export function schema_for(a1, a2, a3) { + if (true) { + const _s0 = registry_entry(a3, a1); + if (_s0 !== _uwFail) { + if (_s0 != null && _s0.$ === "group" && _s0.args.length === 1) { + if (a2 != null && a2.$ === "some" && a2.args.length === 1 && a2.args[0] !== "") { + const _s1 = action_entry(_s0.args[0], a2.args[0]); + if (_s1 !== _uwFail) { + return [_s1, true]; + } + } + } else { + return [_s0, false]; + } + } + } + return _uwFail; +} + +// CLI entry point (multi-output: the tuple is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 5) { + const argv = process.argv.slice(2, 5).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(schema_for(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: parse_args_3/3 + +export function parse_args_3(a1, a2) { + if (true) { + const [_s0, _s1] = scan_leading_globals(a1, []); + if (_s0.length > 0 && !(starts_with(_s0[0], "--"))) { + let _s2; + if (_s0.slice(1).length > 0) { + _s2 = {$: "some", args: [_s0.slice(1)[0]]}; + } else { + _s2 = "none"; + } + let _t3; + if ((_t3 = schema_for(_s0[0], _s2, a2)) !== _uwFail) { + if (_t3[1] === true) { + if (_s2 != null && _s2.$ === "some" && _s2.args.length === 1 && _s0.slice(1).length > 0) { + const _s4 = parse_strict(_s0.slice(1).slice(1), _t3[0], [_s0[0], ...[_s2.args[0], ...[]]]); + if (_s4 !== _uwFail) { + if (_s4 != null && _s4.$ === "ok" && _s4.args.length === 2) { + const _s5 = merge_flags(_s1, _s4.args[1]); + return {$: "ok", args: [_s4.args[0], _s5]}; + } else { + if (_s4 != null && _s4.$ === "err" && _s4.args.length === 1) { + return {$: "error", args: [_s4.args[0]]}; + } + } + } + } + } else { + const _s6 = parse_strict(_s0.slice(1), _t3[0], [_s0[0], ...[]]); + if (_s6 !== _uwFail) { + if (_s6 != null && _s6.$ === "ok" && _s6.args.length === 2) { + const _s7 = merge_flags(_s1, _s6.args[1]); + return {$: "ok", args: [_s6.args[0], _s7]}; + } else { + if (_s6 != null && _s6.$ === "err" && _s6.args.length === 1) { + return {$: "error", args: [_s6.args[0]]}; + } + } + } + } + } else { + const _s8 = lenient_result(a1); + return _s8; + } + } else { + const _s9 = lenient_result(a1); + return _s9; + } + } + return _uwFail; +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 4) { + const argv = process.argv.slice(2, 4).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(parse_args_3(...argv))); +} + + +// Generated by UnifyWeaver TypeScript Target - Structural Recursion (native clause lowering) +// Predicate: parse_args_2/2 + +export function parse_args_2(a1) { + if (true) { + const _s0 = parse_args_3(a1, [{$: "-", args: ["block", {$: "schema", args: [[{$: "-", args: ["include-key", "boolean"]}, ...[]], ["name", ...[]]]}]}, ...[{$: "-", args: ["unblock", {$: "schema", args: [[{$: "-", args: ["key", "string"]}, ...[]], ["[name]", ...[]]]}]}, ...[{$: "-", args: ["add", {$: "schema", args: [[{$: "-", args: ["profile", "string"]}, ...[{$: "-", args: ["until", "string"]}, ...[{$: "-", args: ["key", "string"]}, ...[{$: "-", args: ["key-file", "string"]}, ...[{$: "-", args: ["transport", "string"]}, ...[]]]]]], ["name", ...["[address]", ...[]]]]}]}, ...[{$: "-", args: ["daemon", {$: "schema", args: [[{$: "-", args: ["debug", "optional"]}, ...[{$: "-", args: ["port", "string"]}, ...[{$: "-", args: ["host", "string"]}, ...[{$: "-", args: ["hail-on", "string"]}, ...[{$: "-", args: ["hail-on-encrypted", "string"]}, ...[{$: "-", args: ["hail-on-tls", "string"]}, ...[{$: "-", args: ["tls-cert", "string"]}, ...[{$: "-", args: ["tls-key", "string"]}, ...[{$: "-", args: ["allow-origin", "string"]}, ...[{$: "-", args: ["chat", "boolean"]}, ...[{$: "-", args: ["route", "boolean"]}, ...[{$: "-", args: ["ui", "boolean"]}, ...[{$: "-", args: ["require-target-binding", "boolean"]}, ...[{$: "-", args: ["require-sealed", "boolean"]}, ...[]]]]]]]]]]]]]]], []]}]}, ...[{$: "-", args: ["commands", {$: "group", args: [[{$: "-", args: ["add", {$: "schema", args: [[], ["name", ...["...command", ...[]]]]}]}, ...[{$: "-", args: ["remove", {$: "schema", args: [[], ["name", ...[]]]}]}, ...[{$: "-", args: ["list", {$: "schema", args: [[], []]}]}, ...[]]]]]}]}, ...[{$: "-", args: ["route", {$: "group", args: [[{$: "-", args: ["discover", {$: "schema", args: [[{$: "-", args: ["dest", "string"]}, ...[{$: "-", args: ["dest-file", "string"]}, ...[{$: "-", args: ["control", "string"]}, ...[{$: "-", args: ["port", "string"]}, ...[]]]]], []]}]}, ...[{$: "-", args: ["status", {$: "schema", args: [[{$: "-", args: ["dest", "string"]}, ...[{$: "-", args: ["dest-file", "string"]}, ...[{$: "-", args: ["control", "string"]}, ...[{$: "-", args: ["port", "string"]}, ...[]]]]], []]}]}, ...[{$: "-", args: ["approve", {$: "schema", args: [[{$: "-", args: ["dest", "string"]}, ...[{$: "-", args: ["dest-file", "string"]}, ...[{$: "-", args: ["seal-key", "string"]}, ...[{$: "-", args: ["seal-key-file", "string"]}, ...[{$: "-", args: ["control", "string"]}, ...[{$: "-", args: ["port", "string"]}, ...[]]]]]]], []]}]}, ...[{$: "-", args: ["send", {$: "schema", args: [[{$: "-", args: ["dest", "string"]}, ...[{$: "-", args: ["dest-file", "string"]}, ...[{$: "-", args: ["public", "boolean"]}, ...[{$: "-", args: ["ttl", "string"]}, ...[{$: "-", args: ["budget", "string"]}, ...[{$: "-", args: ["control", "string"]}, ...[{$: "-", args: ["port", "string"]}, ...[]]]]]]]], ["...message", ...[]]]}]}, ...[]]]]]]}]}, ...[{$: "-", args: ["profiles", {$: "group", args: [[{$: "-", args: ["add", {$: "schema", args: [[{$: "-", args: ["allows", "string"]}, ...[{$: "-", args: ["description", "string"]}, ...[]]], ["name", ...[]]]}]}, ...[{$: "-", args: ["remove", {$: "schema", args: [[{$: "-", args: ["force", "boolean"]}, ...[{$: "-", args: ["reassign", "string"]}, ...[]]], ["name", ...[]]]}]}, ...[{$: "-", args: ["pin", {$: "schema", args: [[], ["name", ...[]]]}]}, ...[{$: "-", args: ["unpin", {$: "schema", args: [[], ["name", ...[]]]}]}, ...[{$: "-", args: ["list", {$: "schema", args: [[], []]}]}, ...[]]]]]]]}]}, ...[]]]]]]]]); + if (_s0 !== _uwFail) { + return _s0; + } + } + return _uwFail; +} + +// CLI entry point (the answer is printed as JSON) +// Each argument is JSON.parsed when it parses as JSON (so `[1,2]` is a list), +// and kept as a raw string otherwise. +if (process.argv.length >= 3) { + const argv = process.argv.slice(2, 3).map( + (s) => { try { return JSON.parse(s); } catch { return s; } }); + console.log(JSON.stringify(parse_args_2(...argv))); +} + diff --git a/examples/cli_args/patternjs/cliArgs.mjs b/examples/cli_args/patternjs/cliArgs.mjs new file mode 100644 index 000000000..a2912efd5 --- /dev/null +++ b/examples/cli_args/patternjs/cliArgs.mjs @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2026 John William Creighton (s243a) +// +// cliArgs.mjs -- the EDGE of the transpiled parser. +// +// It exports the same two names peerhailer's `src/cliArgs.js` does -- +// `parseArgs(argv, registry?)` and `class CliError` -- so the oracle's own test +// corpus and the differential runner import this file with nothing changed but +// the import specifier. +// +// WHAT IS IN HERE, exhaustively: conversion between UnifyWeaver's term +// representation and plain JavaScript values, at the module boundary. There is +// NO parse logic. Every decision about what an argv line means -- the two flag +// regexes, the strict/lenient split, the schema lookup, the arity check, the +// exact wording of every error message -- lives in cliArgs.generated.mjs, which +// is compiler output from examples/cli_args/cli_args.pl. +// +// The term representation (typescript_target's, see G-A3-12/G-A3-13): +// +// Prolog atom / string a JS string +// true / false a JS boolean +// list a JS array +// compound f(A1..An) { $: "f", args: [...] } +// +// so `ok(Positional, Flags)` arrives as +// `{ $: "ok", args: [ [...], [ {$:"-",args:[K,V]}, ... ] ] }`. + +import { parse_args_2, parse_args_3 } from "./cliArgs.generated.mjs"; + +/** A user-facing parse error; mirrors the oracle's class exactly. */ +export class CliError extends Error {} + +/** Is `t` the compound term `f(...)`? */ +const isTerm = (t, f) => + t !== null && typeof t === "object" && !Array.isArray(t) && t.$ === f; + +/** + * A Prolog list of `Key-Value` pairs becomes a plain object, in pair order. + * + * A plain `{}` with `obj[k] = v` is deliberate rather than a Map or a + * null-prototype object: the oracle builds its `flags` exactly this way, so JS + * object semantics -- key-insertion order, and the inherited `__proto__` setter + * that silently drops a primitive assignment -- are reproduced rather than + * re-implemented. cli_args.pl models the same semantics on the Prolog side + * (flags_set/4), so the two agree before this function is ever reached. + */ +function pairsToObject(pairs) { + const out = {}; + for (const p of pairs) { + if (!isTerm(p, "-") || p.args.length !== 2) { + throw new Error(`cliArgs shim: expected a Key-Value pair, got ${JSON.stringify(p)}`); + } + out[p.args[0]] = p.args[1]; + } + return out; +} + +/** + * A JS registry (the oracle's `COMMANDS` shape) becomes the Prolog registry + * term cli_args.pl expects. Only reached when a caller passes one; with no + * registry the compiled `parse_args/2` supplies cli_args.pl's own + * `default_registry/1`, which is the transpiled copy of the oracle's COMMANDS. + */ +const pair = (k, v) => ({ $: "-", args: [k, v] }); + +function toRegistryTerm(registry) { + return Object.keys(registry).map((name) => pair(name, toEntryTerm(registry[name]))); +} + +function toEntryTerm(entry) { + if (entry && entry.actions) { + return { + $: "group", + args: [Object.keys(entry.actions).map((a) => pair(a, toEntryTerm(entry.actions[a])))], + }; + } + const options = entry && entry.options ? entry.options : {}; + const positionals = entry && entry.positionals ? entry.positionals : []; + return { + $: "schema", + args: [Object.keys(options).map((k) => pair(k, options[k])), positionals.slice()], + }; +} + +/** + * parseArgs(argv, registry?) -> { positional, flags } + * Throws CliError with the oracle's exact message on a usage error. + */ +export function parseArgs(argv, registry) { + const tokens = Array.from(argv, String); + const result = + registry === undefined + ? parse_args_2(tokens) + : parse_args_3(tokens, toRegistryTerm(registry)); + + if (isTerm(result, "ok") && result.args.length === 2) { + return { positional: result.args[0], flags: pairsToObject(result.args[1]) }; + } + if (isTerm(result, "error") && result.args.length === 1) { + throw new CliError(result.args[0]); + } + // parse_args/2 is semidet in the compiled calling convention, so it CAN answer + // the failure sentinel. cli_args.pl documents that it never fails; if it ever + // does, that is a compiler or reference-implementation bug and must be loud. + throw new Error( + `cliArgs shim: parse_args answered neither ok/2 nor error/1 (${String(result)})` + ); +} diff --git a/examples/cli_args/patternjs/cliArgs.patternjs.test.mjs b/examples/cli_args/patternjs/cliArgs.patternjs.test.mjs new file mode 100644 index 000000000..793cf6ba8 --- /dev/null +++ b/examples/cli_args/patternjs/cliArgs.patternjs.test.mjs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// cliArgs.patternjs.test.mjs -- peerhailer's own 17-test contract corpus +// (examples/cli_args/oracle/cliArgs.test.mjs), run against the TRANSPILED +// parser. The ONLY edit is the import specifier on the line marked below; +// every assertion is the oracle's, verbatim. +// +// node --test examples/cli_args/patternjs/cliArgs.patternjs.test.mjs +/** + * The contract matrix from docs/cli-arg-parsing.md, proven at the parser level. + * (Point 14 — help generated from the same schemas — is a separate feature, not + * yet built, so it is not asserted here.) + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { parseArgs, CliError } from "./cliArgs.mjs"; // <-- the only edit + +const of = (line) => parseArgs(line.split(" ").filter(Boolean)); + +test("1 & 2: a boolean flag works before or after the positional", () => { + for (const line of ["block --include-key bob", "block bob --include-key"]) { + const { positional, flags } = of(line); + assert.deepEqual(positional, ["block", "bob"], line); + assert.equal(flags["include-key"], true, line); + } +}); + +test("3: an unknown option is refused against the command schema", () => { + assert.throws(() => of("block bob --include-keey"), CliError); +}); + +test("4: --include-key=false is off; =anything-else is on", () => { + assert.equal(of("block bob --include-key=false").flags["include-key"], false); + assert.equal(of("block bob --include-key=yes").flags["include-key"], true); +}); + +test("5: a value given to a boolean flag lands as an extra positional and fails", () => { + assert.throws(() => of("block bob --include-key yes"), /extra argument: yes/); +}); + +test("6: --state selects the same state before or after the command", () => { + assert.equal(of("--state P block bob").flags.state, "P"); + assert.equal(of("block bob --state P").flags.state, "P"); +}); + +test("7: `--` preserves a forwarded command with its own flags", () => { + const { positional } = of("commands add deploy -- ./run.sh --env prod"); + // The handler reads rest.slice(2).join(" "); rest is positional after the command. + assert.deepEqual(positional.slice(3), ["./run.sh", "--env", "prod"]); +}); + +test("8: after `--`, a --state token is payload, not the global option", () => { + const { flags, positional } = of("commands add deploy -- ./run.sh --state child.json"); + assert.equal(flags.state, undefined, "not swallowed as the global"); + assert.ok(positional.includes("--state") && positional.includes("child.json"), "kept as payload"); +}); + +test("9: --key keeps an inline dash-leading PEM as its value", () => { + const pem = "-----BEGIN-PUBLIC-KEY-----"; + const { flags } = parseArgs(["add", "bob", "--key", pem]); + assert.equal(flags.key, pem); +}); + +test("10: --debug is bare-true, spaced, or =valued", () => { + assert.equal(of("daemon --debug").flags.debug, true); + assert.equal(of("daemon --debug 2").flags.debug, "2"); + assert.equal(of("daemon --debug=2").flags.debug, "2"); +}); + +test("11: a value handed to a boolean daemon flag fails loudly", () => { + assert.throws(() => of("daemon --require-target-binding yes"), /extra argument: yes/); +}); + +test("12: --force is refused on `profiles pin` — it belongs to `profiles remove`", () => { + assert.throws(() => of("profiles pin trusted --force"), /unknown option --force/); + // ...and is accepted on remove. + assert.equal(of("profiles remove temp --force").flags.force, true); +}); + +test("13: a string option with no value fails", () => { + assert.throws(() => of("add bob --profile"), /--profile needs a value/); +}); + +test("a missing required positional fails with the argument name", () => { + assert.throws(() => of("block"), /missing argument: name/); +}); + +test("an unmigrated command falls back to the lenient parse (no schema, no error)", () => { + // `tunnels` has no schema yet: unknown flags are accepted, booleans stay greedy — + // exactly the legacy behaviour, so the migration can proceed leaf by leaf. + const { positional, flags } = of("tunnels add acp 127.0.0.1:9100 --anything here"); + assert.equal(positional[0], "tunnels"); + assert.equal(flags.anything, "here"); +}); + +test("regression: daemon accepts --hail-on-encrypted / --hail-on-tls (Fable)", () => { + assert.equal(of("daemon --hail-on-encrypted tailscale0").flags["hail-on-encrypted"], "tailscale0"); + assert.equal(of("daemon --hail-on-tls eth0").flags["hail-on-tls"], "eth0"); +}); + +test("regression: unblock --key with no name is valid (Fable)", () => { + const { flags, positional } = of("unblock --key ABCDEF12"); + assert.equal(flags.key, "ABCDEF12"); + assert.deepEqual(positional, ["unblock"], "no name required"); + // ...and the name form still works. + assert.deepEqual(of("unblock bob").positional, ["unblock", "bob"]); +}); + +test("the lenient fallback is verbatim: --a --b=c keeps the old greedy reading", () => { + // An unmigrated command must behave exactly as before: the old parser read + // `--b=c` (which is not a bare --word) as the value of --a. + const { flags } = of("tunnels --a --b=c"); + assert.equal(flags.a, "--b=c", "unchanged legacy behaviour on an unmigrated command"); +}); diff --git a/examples/cli_args/patternjs/diff_runner_patternjs.mjs b/examples/cli_args/patternjs/diff_runner_patternjs.mjs new file mode 100644 index 000000000..5062171ec --- /dev/null +++ b/examples/cli_args/patternjs/diff_runner_patternjs.mjs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2026 John William Creighton (s243a) +// +// diff_runner_patternjs.mjs -- the TRANSPILED side of the differential harness. +// +// Byte-for-byte the same protocol as examples/cli_args/diff_runner.mjs (the +// oracle side) and diff_runner.pl (the SWI side): read argv-lines on stdin, one +// case per line, tokens separated by spaces, skip lines with no tokens, and +// print one JSON object per line -- +// +// {"ok":{"positional":[...],"flags":{...}}} +// {"error":""} +// {"crash":""} // never expected; a hard failure +// +// The only difference from diff_runner.mjs is the import: `parseArgs` comes from +// the transpiled parser's edge shim instead of from the vendored oracle. +// +// node examples/cli_args/patternjs/diff_runner_patternjs.mjs < lines.txt > patternjs.jsonl + +import { parseArgs, CliError } from "./cliArgs.mjs"; + +const tokenize = (line) => line.split(" ").filter(Boolean); + +function runLine(line) { + try { + const { positional, flags } = parseArgs(tokenize(line)); + return { ok: { positional, flags } }; + } catch (err) { + if (err instanceof CliError) return { error: err.message }; + return { crash: String((err && err.message) || err) }; + } +} + +const chunks = []; +for await (const chunk of process.stdin) chunks.push(chunk); +const input = Buffer.concat(chunks).toString("utf8"); + +const out = []; +for (const line of input.split("\n")) { + if (tokenize(line).length === 0) continue; + out.push(JSON.stringify(runLine(line))); +} +process.stdout.write(out.length ? out.join("\n") + "\n" : ""); diff --git a/examples/cli_args/patternjs/run_differential_patternjs.sh b/examples/cli_args/patternjs/run_differential_patternjs.sh new file mode 100644 index 000000000..ec6fa91ae --- /dev/null +++ b/examples/cli_args/patternjs/run_differential_patternjs.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT OR Apache-2.0 +# Copyright (c) 2026 John William Creighton (s243a) +# +# run_differential_patternjs.sh -- step A4 for the PATTERN lane. +# +# Identical to examples/cli_args/run_differential.sh except for which parser is +# put opposite the oracle: the same generator, the same seed, the same case file, +# the same comparator. The Prolog reference is replaced by the TRANSPILED parser +# (patternjs/cliArgs.generated.mjs, reached through the edge shim). +# +# bash examples/cli_args/patternjs/run_differential_patternjs.sh +# +# Exits non-zero on any divergence. Artifacts land in ./.diff_out/ next to this +# script (cases.txt, oracle.jsonl, patternjs.jsonl). + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +UP="$HERE/.." +OUT="$HERE/.diff_out" +mkdir -p "$OUT" + +echo "== generating seeded cases (the harness's own generator, unchanged) ==" +node "$UP/gen_cases.mjs" > "$OUT/cases.txt" +echo "cases: $(wc -l < "$OUT/cases.txt") lines -> $OUT/cases.txt" + +echo "== running the JavaScript oracle (peerhailer parseArgs) ==" +node "$UP/diff_runner.mjs" < "$OUT/cases.txt" > "$OUT/oracle.jsonl" + +echo "== running the TRANSPILED parser (UnifyWeaver pattern lane) ==" +node "$HERE/diff_runner_patternjs.mjs" < "$OUT/cases.txt" > "$OUT/patternjs.jsonl" + +echo "== comparing ==" +node "$UP/compare_jsonl.mjs" "$OUT/cases.txt" "$OUT/oracle.jsonl" "$OUT/patternjs.jsonl" diff --git a/examples/cli_args/run_differential.sh b/examples/cli_args/run_differential.sh new file mode 100755 index 000000000..09da33654 --- /dev/null +++ b/examples/cli_args/run_differential.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT OR Apache-2.0 +# Copyright (c) 2026 John William Creighton (s243a) +# +# run_differential.sh -- the differential harness for the Prolog CLI parser. +# +# Generates a seeded, reproducible sample of argv-lines (the 17-test corpus, a +# hand-written quirk sweep, and 5000 pseudorandom lines of length 2..7 drawn +# from the task's token alphabet), feeds the identical file to the JavaScript +# ORACLE and to the Prolog reference implementation, and compares the two +# result streams semantically. +# +# bash examples/cli_args/run_differential.sh +# +# Exits non-zero on any divergence. Artifacts land in ./.diff_out/ next to this +# script (cases.txt, oracle.jsonl, prolog.jsonl) so a failure can be inspected. + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="$HERE/.diff_out" +mkdir -p "$OUT" + +echo "== generating seeded cases ==" +node "$HERE/gen_cases.mjs" > "$OUT/cases.txt" +echo "cases: $(wc -l < "$OUT/cases.txt") lines -> $OUT/cases.txt" + +echo "== running the JavaScript oracle (peerhailer parseArgs) ==" +node "$HERE/diff_runner.mjs" < "$OUT/cases.txt" > "$OUT/oracle.jsonl" + +echo "== running the Prolog reference implementation ==" +swipl -q -g main -t halt "$HERE/diff_runner.pl" < "$OUT/cases.txt" > "$OUT/prolog.jsonl" + +echo "== comparing ==" +node "$HERE/compare_jsonl.mjs" "$OUT/cases.txt" "$OUT/oracle.jsonl" "$OUT/prolog.jsonl" diff --git a/examples/cli_args/test_cli_args.pl b/examples/cli_args/test_cli_args.pl new file mode 100644 index 000000000..fc2eacf15 --- /dev/null +++ b/examples/cli_args/test_cli_args.pl @@ -0,0 +1,205 @@ +% SPDX-License-Identifier: MIT OR Apache-2.0 +% Copyright (c) 2026 John William Creighton (s243a) +% +% test_cli_args.pl -- a plunit port of peerhailer's `test/cliArgs.test.mjs` +% (the 17-test contract matrix from docs/cli-arg-parsing.md), driving the +% Prolog reference parser in cli_args.pl. +% +% Test names and numbering follow the corpus one-for-one; the JS `assert.throws` +% cases become `error(Message)` assertions, with the corpus's regex rendered as +% a substring check (every corpus regex is a plain literal). +% +% swipl -q -g test_cli_args -t halt examples/cli_args/test_cli_args.pl + +:- module(test_cli_args, [test_cli_args/0]). + +:- use_module(library(plunit)). +:- use_module(cli_args). + +% --------------------------------------------------------------------------- +% Harness helpers +% --------------------------------------------------------------------------- + +%! of(+Line, -Result) is det. +% +% The corpus's `const of = (line) => parseArgs(line.split(" ").filter(Boolean))`. +of(Line, Result) :- + split_string(Line, " ", "", Raw), + exclude(==(""), Raw, Tokens), + parse_args(Tokens, Result). + +%! ok_of(+Line, -Positional, -Flags) is semidet. +ok_of(Line, Positional, Flags) :- + of(Line, ok(Positional, Flags)). + +%! flag(+Flags, +Key, -Value) is semidet. +flag([K-V|Rest], Key, Value) :- + ( K == Key + -> Value = V + ; flag(Rest, Key, Value) + ). + +%! no_flag(+Flags, +Key) is semidet. % JS `flags.x === undefined` +no_flag(Flags, Key) :- + \+ flag(Flags, Key, _). + +%! error_matching(+Line, +Needle) is semidet. +% +% The corpus asserts `assert.throws(..., /needle/)`; every corpus pattern is a +% literal, so a substring test is the faithful rendering. +error_matching(Line, Needle) :- + of(Line, error(Message)), + contains_substring(Message, Needle). + +contains_substring(Haystack, Needle) :- + sub_string(Haystack, _, _, _, Needle), + !. + +%! contains_token(+List, +Token) is semidet. +contains_token([X|Xs], Token) :- + ( X == Token + -> true + ; contains_token(Xs, Token) + ). + +% --------------------------------------------------------------------------- +% The contract matrix +% --------------------------------------------------------------------------- + +:- begin_tests(cli_args). + +% 1 & 2: a boolean flag works before or after the positional +test('1 & 2: a boolean flag works before or after the positional') :- + forall(member(Line, ["block --include-key bob", "block bob --include-key"]), + ( ok_of(Line, Positional, Flags), + assertion(Positional == ["block", "bob"]), + assertion(flag(Flags, "include-key", true)) + )). + +% 3: an unknown option is refused against the command schema +test('3: an unknown option is refused against the command schema') :- + assertion(of("block bob --include-keey", error(_))), + assertion(error_matching("block bob --include-keey", + "unknown option --include-keey")). + +% 4: --include-key=false is off; =anything-else is on +test('4: --include-key=false is off; =anything-else is on') :- + ok_of("block bob --include-key=false", _, F1), + assertion(flag(F1, "include-key", false)), + ok_of("block bob --include-key=yes", _, F2), + assertion(flag(F2, "include-key", true)). + +% 5: a value given to a boolean flag lands as an extra positional and fails +test('5: a value given to a boolean flag lands as an extra positional and fails') :- + assertion(error_matching("block bob --include-key yes", "extra argument: yes")). + +% 6: --state selects the same state before or after the command +test('6: --state selects the same state before or after the command') :- + ok_of("--state P block bob", _, F1), + assertion(flag(F1, "state", "P")), + ok_of("block bob --state P", _, F2), + assertion(flag(F2, "state", "P")). + +% 7: `--` preserves a forwarded command with its own flags +test('7: `--` preserves a forwarded command with its own flags') :- + ok_of("commands add deploy -- ./run.sh --env prod", Positional, _), + % The handler reads rest.slice(2).join(" "); rest is positional after the command. + length(Prefix, 3), + append(Prefix, Tail, Positional), + assertion(Tail == ["./run.sh", "--env", "prod"]). + +% 8: after `--`, a --state token is payload, not the global option +test('8: after `--`, a --state token is payload, not the global option') :- + ok_of("commands add deploy -- ./run.sh --state child.json", Positional, Flags), + assertion(no_flag(Flags, "state")), % not swallowed as the global + assertion(contains_token(Positional, "--state")), % kept as payload + assertion(contains_token(Positional, "child.json")). + +% 9: --key keeps an inline dash-leading PEM as its value +test('9: --key keeps an inline dash-leading PEM as its value') :- + Pem = "-----BEGIN-PUBLIC-KEY-----", + parse_args(["add", "bob", "--key", Pem], ok(_, Flags)), + assertion(flag(Flags, "key", Pem)). + +% 10: --debug is bare-true, spaced, or =valued +test('10: --debug is bare-true, spaced, or =valued') :- + ok_of("daemon --debug", _, F1), + assertion(flag(F1, "debug", true)), + ok_of("daemon --debug 2", _, F2), + assertion(flag(F2, "debug", "2")), + ok_of("daemon --debug=2", _, F3), + assertion(flag(F3, "debug", "2")). + +% 11: a value handed to a boolean daemon flag fails loudly +test('11: a value handed to a boolean daemon flag fails loudly') :- + assertion(error_matching("daemon --require-target-binding yes", + "extra argument: yes")). + +% 12: --force is refused on `profiles pin` -- it belongs to `profiles remove` +test('12: --force is refused on `profiles pin` - it belongs to `profiles remove`') :- + assertion(error_matching("profiles pin trusted --force", "unknown option --force")), + % ...and is accepted on remove. + ok_of("profiles remove temp --force", _, Flags), + assertion(flag(Flags, "force", true)). + +% 13: a string option with no value fails +test('13: a string option with no value fails') :- + assertion(error_matching("add bob --profile", "--profile needs a value")). + +% a missing required positional fails with the argument name +test('a missing required positional fails with the argument name') :- + assertion(error_matching("block", "missing argument: name")). + +% an unmigrated command falls back to the lenient parse (no schema, no error) +test('an unmigrated command falls back to the lenient parse (no schema, no error)') :- + % `tunnels` has no schema yet: unknown flags are accepted, booleans stay + % greedy -- exactly the legacy behaviour. + ok_of("tunnels add acp 127.0.0.1:9100 --anything here", Positional, Flags), + Positional = [First|_], + assertion(First == "tunnels"), + assertion(flag(Flags, "anything", "here")). + +% regression: daemon accepts --hail-on-encrypted / --hail-on-tls (Fable) +test('regression: daemon accepts --hail-on-encrypted / --hail-on-tls (Fable)') :- + ok_of("daemon --hail-on-encrypted tailscale0", _, F1), + assertion(flag(F1, "hail-on-encrypted", "tailscale0")), + ok_of("daemon --hail-on-tls eth0", _, F2), + assertion(flag(F2, "hail-on-tls", "eth0")). + +% regression: unblock --key with no name is valid (Fable) +test('regression: unblock --key with no name is valid (Fable)') :- + ok_of("unblock --key ABCDEF12", Positional, Flags), + assertion(flag(Flags, "key", "ABCDEF12")), + assertion(Positional == ["unblock"]), % no name required + % ...and the name form still works. + ok_of("unblock bob", Positional2, _), + assertion(Positional2 == ["unblock", "bob"]). + +% the lenient fallback is verbatim: --a --b=c keeps the old greedy reading +test('the lenient fallback is verbatim: --a --b=c keeps the old greedy reading') :- + % An unmigrated command must behave exactly as before: the old parser read + % `--b=c` (which is not a bare --word) as the value of --a. + ok_of("tunnels --a --b=c", _, Flags), + assertion(flag(Flags, "a", "--b=c")). + +:- end_tests(cli_args). + +% --------------------------------------------------------------------------- +% Entry point +% --------------------------------------------------------------------------- + +%! test_cli_args is det. +% +% Runs the suite and prints an explicit count on stdout, so the pass total is +% visible even under `swipl -q` (which silences plunit's own summary). +test_cli_args :- + test_count(Total), + ( run_tests(cli_args) + -> format("cli_args contract matrix: ~w/~w tests passed~n", [Total, Total]) + ; format(user_error, "cli_args contract matrix: FAILED (of ~w tests)~n", [Total]), + halt(1) + ). + +test_count(Total) :- + findall(Name, plunit:current_test(cli_args, Name, _, _, _), Names), + length(Names, Total). From bcfae3d161bd939ae61b4ff2ff8aa8a8f91a6380 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 1 Sep 2026 01:54:53 +0000 Subject: [PATCH 2/3] Compile cli_args.pl through the JS WAM (A2). Interpreter-tier project under examples/cli_args/wamjs/, thin ESM shim, oracle corpus + differential runner. Runtime: sub_string/5, Y-save across Call, Execute-to-builtin proceeds to CP. Probes for each gap. Co-authored-by: johns243a --- docs/WAM_JAVASCRIPT_STATUS.md | 33 +- examples/cli_args/wamjs/.gitignore | 2 + examples/cli_args/wamjs/README.md | 68 + examples/cli_args/wamjs/build.pl | 45 + examples/cli_args/wamjs/build.sh | 24 + examples/cli_args/wamjs/cliArgs.mjs | 204 + .../cli_args/wamjs/cliArgs.wamjs.test.mjs | 115 + examples/cli_args/wamjs/diff_runner_wamjs.mjs | 43 + .../cli_args/wamjs/js/generated_program.js | 2742 +++++++++++ examples/cli_args/wamjs/js/wam_runtime.js | 4113 +++++++++++++++++ .../cli_args/wamjs/run_differential_wamjs.sh | 43 + .../bindings/javascript_wam_bindings.pl | 1 + .../javascript_wam/runtime.js.mustache | 89 +- tests/test_wam_javascript_builtins.pl | 41 +- 14 files changed, 7554 insertions(+), 9 deletions(-) create mode 100644 examples/cli_args/wamjs/.gitignore create mode 100644 examples/cli_args/wamjs/README.md create mode 100644 examples/cli_args/wamjs/build.pl create mode 100755 examples/cli_args/wamjs/build.sh create mode 100644 examples/cli_args/wamjs/cliArgs.mjs create mode 100644 examples/cli_args/wamjs/cliArgs.wamjs.test.mjs create mode 100644 examples/cli_args/wamjs/diff_runner_wamjs.mjs create mode 100644 examples/cli_args/wamjs/js/generated_program.js create mode 100644 examples/cli_args/wamjs/js/wam_runtime.js create mode 100755 examples/cli_args/wamjs/run_differential_wamjs.sh diff --git a/docs/WAM_JAVASCRIPT_STATUS.md b/docs/WAM_JAVASCRIPT_STATUS.md index c574bb7ef..8d94a44ba 100644 --- a/docs/WAM_JAVASCRIPT_STATUS.md +++ b/docs/WAM_JAVASCRIPT_STATUS.md @@ -63,6 +63,7 @@ Key, `@<`/`@>`/`<`/`>`), **`predsort/3`** (`compare/3` and 3-arg callables), Atom/string: **`atom_concat/3`**, **`string_concat/3`**, **`atom_length/2`**, **`string_length/2`**, **`atom_chars/2`**, **`string_chars/2`**, **`atom_codes/2`**, **`char_code/2`**, **`sub_atom/5`** (ground-Atom; enumerates unbound Before/Length/After), +**`sub_string/5`** (same walk; Sub is a `V.String`), **`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** @@ -218,6 +219,24 @@ fib/2 67 1234 3 2 89012 instr=1234 unify=56 trail=40 heap=30 backtracks=12 undos=8 wall_ns=100000 ``` +## Peerhailer argparser (A2) + +**Runs the peerhailer argparser (A2).** `examples/cli_args/cli_args.pl` (module +`cli_args`, `parse_args/2,3`) compiles through `wam_javascript` +(`emit_mode(interpreter)`) into `examples/cli_args/wamjs/js/`. A thin ESM shim +(`cliArgs.mjs`) converts JS `argv` ↔ WAM terms and maps `ok/2` / +`error/1`; it implements no parse rule. + +| Check | Result | +|---|---| +| Contract corpus (`cliArgs.wamjs.test.mjs`, oracle tests, import swapped) | **17 / 17** | +| Differential vs JS oracle (`run_differential_wamjs.sh`, same seed) | **≥5000 lines, 0 divergences, 0 message mismatches** | + +Runtime gaps this program forced: `sub_string/5`; Y-register save/restore +across `Call` of a non-`Allocate` fact; `Execute` of a builtin `Proceed`s to +CP instead of halting. Probes: `probe_sub_string/0`, `probe_y_preserve/0`, +`probe_tail_builtin/0` in `tests/test_wam_javascript_builtins.pl`. + ## Remaining / partial | Builtin | Status | @@ -229,6 +248,8 @@ instr=1234 unify=56 trail=40 heap=30 backtracks=12 undos=8 wall_ns=100000 | `=@=/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). | +| `sub_string/5` | **Implemented.** Same enumeration as `sub_atom/5`; Sub is a `V.String`. | +| Peerhailer argparser (A2) | **Implemented.** `examples/cli_args/wamjs/` compiles `cli_args.pl` through `wam_javascript` (interpreter) and matches the JS oracle: **17/17** corpus, **≥5000-line** differential with **0 divergences, 0 message mismatches**. | | 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. `string_length/2` accepts a string, atom, or number (code-point length). `write/1` prints text unquoted; `writeq/1` and `format` `~q` recurse through lists/compounds, double-quote strings, and quote atoms only when needed (see quoting subset below). **Compiled `"foo"` literals** are spelled with outer double quotes in WAM text (`quote_wam_constant/2`); the shared classifier still returns `atom(foo)` (no `string(_)` Class). JS consults `wam_constant_token_is_string/1` and builds `V.String`. Other runtimes intern the atom as before. 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. | @@ -259,6 +280,11 @@ swipl -q -g run_tests -t halt tests/test_wam_javascript_lowered.pl # After INTEGRATION_PATCH.md is applied: CONFORMANCE_TARGETS=javascript swipl -q -g run_tests -t halt \ tests/test_wam_cross_target_conformance.pl + +# Peerhailer CLI parser (A2) — compile, 17-test corpus, differential vs oracle: +bash examples/cli_args/wamjs/build.sh +node --test examples/cli_args/wamjs/cliArgs.wamjs.test.mjs +bash examples/cli_args/wamjs/run_differential_wamjs.sh ``` Residual ISO corners not covered: bagof/setof of *unbound* free vars (two @@ -289,5 +315,8 @@ table: infix + prefix + postfix), then a distinct string term tag `string_length/2` and `writeq/1` / recursive `~q` quoting, then compiled `"foo"` literals as `V.String` (double-quoted WAM spelling; classifier still returns `atom(_)`), then opt-in interpreter profiling -(`UW_PROFILE=1` / `json`, stderr-only table or JSON; lowered = call counts). -Source-verified against SWI-Prolog as the oracle (2026-08-31). +(`UW_PROFILE=1` / `json`, stderr-only table or JSON; lowered = call counts), +then the peerhailer CLI argparser through the interpreter (A2: +`examples/cli_args/wamjs/`, 17/17 corpus + seeded differential vs the +JS oracle). +Source-verified against SWI-Prolog as the oracle (2026-09-01). diff --git a/examples/cli_args/wamjs/.gitignore b/examples/cli_args/wamjs/.gitignore new file mode 100644 index 000000000..ea77775f7 --- /dev/null +++ b/examples/cli_args/wamjs/.gitignore @@ -0,0 +1,2 @@ +# Differential-harness artifacts (regenerated, seeded). +.diff_out/ diff --git a/examples/cli_args/wamjs/README.md b/examples/cli_args/wamjs/README.md new file mode 100644 index 000000000..5bc0f77f4 --- /dev/null +++ b/examples/cli_args/wamjs/README.md @@ -0,0 +1,68 @@ + + +# `wamjs` — peerhailer's CLI parser, compiled through the JS WAM + +This directory is the **output** of UnifyWeaver's `wam_javascript` target applied +to `../cli_args.pl`, plus the two harnesses that hold it to the same bar the +Prolog reference was held to in step A1. + +`../cli_args.pl` is a Prolog reimplementation of peerhailer's `src/cliArgs.js`, +verified against the vendored JavaScript oracle in `../oracle/`. What is here is +that Prolog **compiled to shared WAM bytecode and run on the Node interpreter** — +the whole program, all 43 predicates, `parse_args/2` included — and then +measured against the same oracle. + +| | | +| --- | --- | +| contract corpus (`../oracle/cliArgs.test.mjs`, import swapped) | **17 / 17** | +| differential vs the JS oracle, same generator and seed | **≥5000 lines, 0 divergences, 0 message mismatches** | +| `node --check` on the generated module | clean | +| predicates omitted / goals dropped | none | + +## Files + +| file | what it is | +| --- | --- | +| `build.sh` | **the one command.** Compiles `../cli_args.pl` into `js/` via `write_wam_javascript_project/3` (`emit_mode(interpreter)`) and `node --check`s it. | +| `build.pl` | the compile itself: read the frozen source's clauses into `user`, then the JS WAM emitter. | +| `js/generated_program.js` | **compiler output.** Instruction vector + labels for every predicate `parse_args/2` transitively calls. Regenerated by `build.sh`. | +| `js/wam_runtime.js` | **compiler output.** The interpreter-tier Node WAM (copied from the runtime template). Regenerated by `build.sh`. | +| `cliArgs.mjs` | the **edge shim**: `parseArgs(argv, registry?)` and `class CliError`. Term↔JS conversion only — no parse logic (see the header comment). | +| `cliArgs.wamjs.test.mjs` | peerhailer's own 17-test contract corpus, with **one** line changed: the import. | +| `diff_runner_wamjs.mjs` | the compiled side of the differential harness; same stdin/JSONL protocol as `../diff_runner.mjs` and `../diff_runner.pl`. | +| `run_differential_wamjs.sh` | the A2 run for this lane: the harness's own generator and comparator, with the JS-WAM parser opposite the oracle. | + +## Reproducing + +```bash +bash examples/cli_args/wamjs/build.sh +node --test examples/cli_args/wamjs/cliArgs.wamjs.test.mjs +bash examples/cli_args/wamjs/run_differential_wamjs.sh +``` + +## What the shim does and does not do + +The only hand-written JavaScript that the parser's behaviour passes through is +`cliArgs.mjs`. Exhaustively, it: + +* turns a JS `argv` string array into a WAM list of `V.String`; +* when a `registry` object is given, turns it into the Prolog + `Name-schema(Options, Positionals) | Name-group(Actions)` term and calls + compiled `parse_args/3` (the 17-test corpus only exercises `/2` + the default + registry); +* drives `Runtime.run` at label `parse_args/2` or `parse_args/3` and **derefs the + original result cell** (not A2 — `/2` tail-calls `/3`); +* maps `ok(Positional, Flags)` to `{ positional, flags }` (pair list → plain + object in pair order; atoms `true`/`false` → JS booleans; strings stay + strings); +* maps `error(Message)` to `throw new CliError(Message)`. + +It implements **no parse rule**: neither flag regex, nor the strict/lenient +split, nor schema lookup, nor arity checking, nor any error-message wording. +Those all live in `js/generated_program.js`, which is compiler output from +`../cli_args.pl`. + +A shim that re-implemented any of those would void the demonstration. diff --git a/examples/cli_args/wamjs/build.pl b/examples/cli_args/wamjs/build.pl new file mode 100644 index 000000000..cd79b45e6 --- /dev/null +++ b/examples/cli_args/wamjs/build.pl @@ -0,0 +1,45 @@ +:- encoding(utf8). +% SPDX-License-Identifier: MIT OR Apache-2.0 +% Copyright (c) 2026 John William Creighton (s243a) +% +% build.pl -- compile examples/cli_args/cli_args.pl into a JS WAM project +% (interpreter tier) via write_wam_javascript_project/3. +% +% Clauses are re-asserted into `user` rather than consulting the module: +% the WAM compiler reads user:clause/2, and cli_args.pl is a module. +% Directives are skipped. Nothing in cli_args.pl is executed. + +:- use_module('../../../src/unifyweaver/targets/wam_javascript_target', + [write_wam_javascript_project/3]). + +load_into_user(File, Preds) :- + setup_call_cleanup(open(File, read, S), load_terms(S, [], Acc), close(S)), + sort(Acc, Preds). + +load_terms(S, Acc, Preds) :- + read_term(S, T, []), + ( T == end_of_file + -> Preds = Acc + ; T = (:- _) + -> load_terms(S, Acc, Preds) + ; pred_of_term(T, PA), + assertz(user:T), + load_terms(S, [PA|Acc], Preds) + ). + +pred_of_term((Head :- _), P/A) :- !, functor(Head, P, A). +pred_of_term(Head, P/A) :- functor(Head, P, A). + +main :- + current_prolog_flag(argv, Argv), + ( Argv = [Src, OutDir|_] + -> true + ; Src = '../cli_args.pl', + OutDir = '.' + ), + load_into_user(Src, Preds), + length(Preds, N), + format("build.pl: compiling ~w predicates from ~w~n", [N, Src]), + forall(member(P/A, Preds), format(" ~w/~w~n", [P, A])), + write_wam_javascript_project(Preds, [emit_mode(interpreter)], OutDir), + format("build.pl: wrote JS WAM project under ~w/js/~n", [OutDir]). diff --git a/examples/cli_args/wamjs/build.sh b/examples/cli_args/wamjs/build.sh new file mode 100755 index 000000000..7bde55d45 --- /dev/null +++ b/examples/cli_args/wamjs/build.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT OR Apache-2.0 +# Copyright (c) 2026 John William Creighton (s243a) +# +# build.sh -- ONE command that turns examples/cli_args/cli_args.pl into a +# JavaScript WAM project node can run (interpreter tier). +# +# bash examples/cli_args/wamjs/build.sh +# +# Output: js/generated_program.js + js/wam_runtime.js next to this script. +# The only hand-written file that touches parser behaviour is cliArgs.mjs, +# and it does term<->JS conversion at the edge and nothing else. + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SRC="$HERE/../cli_args.pl" +OUT="$HERE" + +swipl -q -g main -t halt "$HERE/build.pl" -- "$SRC" "$OUT" + +node --check "$OUT/js/generated_program.js" +node --check "$OUT/js/wam_runtime.js" +echo "build.sh: node --check clean -> $OUT/js/generated_program.js" diff --git a/examples/cli_args/wamjs/cliArgs.mjs b/examples/cli_args/wamjs/cliArgs.mjs new file mode 100644 index 000000000..fa0bbd6c0 --- /dev/null +++ b/examples/cli_args/wamjs/cliArgs.mjs @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2026 John William Creighton (s243a) +// +// cliArgs.mjs -- the EDGE of the JS-WAM compiled parser. +// +// It exports the same two names peerhailer's `src/cliArgs.js` does -- +// `parseArgs(argv, registry?)` and `class CliError` -- so the oracle's own test +// corpus and the differential runner import this file with nothing changed but +// the import specifier. +// +// WHAT IS IN HERE, exhaustively: conversion between the JS WAM term +// representation and plain JavaScript values, at the module boundary. There is +// NO parse logic. Every decision about what an argv line means -- the two flag +// regexes, the strict/lenient split, the schema lookup, the arity check, the +// exact wording of every error message -- lives in the compiled WAM module +// (js/generated_program.js), which is compiler output from +// examples/cli_args/cli_args.pl. +// +// The compiled wrappers return only true/false and allocate a fresh WAM state, +// so the shim drives Runtime.run itself and reads A2 (or A3) after success. +// +// Term representation (JS WAM): +// atom { tag: "atom", id } +// string { tag: "string", val } +// int { tag: "int", val } +// cons { tag: "struct", fid: [|], args: [H, T] } +// compound { tag: "struct", fid, args } + +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(import.meta.url); +const generated = require(join(dirname(fileURLToPath(import.meta.url)), "js", "generated_program.js")); +const { Runtime, V } = generated; +const program = generated.program; + +/** A user-facing parse error; mirrors the oracle's class exactly. */ +export class CliError extends Error {} + +function internAtom(name) { + return V.Atom(Runtime.intern(program.intern_table, name)); +} + +function functorId(name) { + return Runtime.intern(program.intern_table, name); +} + +function functorName(fid) { + return Runtime.functor_name(program.intern_table, fid); +} + +function isCons(term) { + if (!term || term.tag !== "struct" || (term.args || []).length !== 2) return false; + const n = functorName(term.fid); + return n === "[|]" || n === "." || n === "./2" || n === "[|]/2"; +} + +function jsListToTerm(arr, mapItem) { + let cur = internAtom("[]"); + const fid = functorId("[|]"); + for (let i = arr.length - 1; i >= 0; i--) { + cur = V.Struct(fid, [mapItem(arr[i]), cur]); + } + return cur; +} + +function termToJsList(state, term) { + const items = []; + let cur = Runtime.deref(state, term); + const nilId = Runtime.intern(program.intern_table, "[]"); + while (true) { + cur = Runtime.deref(state, cur); + if (typeof cur !== "object" || cur === null) { + throw new Error("cliArgs shim: expected a list"); + } + if (cur.tag === "atom" && cur.id === nilId) return items; + if (!isCons(cur)) throw new Error("cliArgs shim: expected a list cons"); + items.push(cur.args[0]); + cur = cur.args[1]; + } +} + +function scalarToJs(state, term) { + const t = Runtime.deref(state, term); + if (!t || typeof t !== "object") return t; + if (t.tag === "string") return String(t.val); + if (t.tag === "int" || t.tag === "float") return t.val; + if (t.tag === "atom") { + const name = Runtime.string_of(program.intern_table, t.id); + if (name === "true") return true; + if (name === "false") return false; + return name; + } + throw new Error(`cliArgs shim: unexpected scalar ${JSON.stringify(t)}`); +} + +function positionalToJs(state, term) { + return termToJsList(state, term).map((item) => scalarToJs(state, item)); +} + +/** + * A Prolog list of `Key-Value` pairs becomes a plain object, in pair order. + * + * A plain `{}` with `obj[k] = v` is deliberate rather than a Map or a + * null-prototype object: the oracle builds its `flags` exactly this way, so JS + * object semantics -- key-insertion order, and the inherited `__proto__` setter + * that silently drops a primitive assignment -- are reproduced rather than + * re-implemented. cli_args.pl models the same semantics on the Prolog side + * (flags_set/4), so the two agree before this function is ever reached. + */ +function pairsToObject(state, pairsTerm) { + const out = {}; + const dash = functorId("-"); + for (const p of termToJsList(state, pairsTerm)) { + const t = Runtime.deref(state, p); + if (!t || t.tag !== "struct" || t.fid !== dash || (t.args || []).length !== 2) { + throw new Error("cliArgs shim: expected a Key-Value pair"); + } + const k = scalarToJs(state, t.args[0]); + out[k] = scalarToJs(state, t.args[1]); + } + return out; +} + +function pairTerm(k, v) { + return V.Struct(functorId("-"), [k, v]); +} + +function toEntryTerm(entry) { + if (entry && entry.actions) { + const actions = jsListToTerm(Object.keys(entry.actions), (a) => + pairTerm(V.String(a), toEntryTerm(entry.actions[a])) + ); + return V.Struct(functorId("group"), [actions]); + } + const optionsObj = entry && entry.options ? entry.options : {}; + const positionals = entry && entry.positionals ? entry.positionals : []; + const options = jsListToTerm(Object.keys(optionsObj), (k) => + pairTerm(V.String(k), internAtom(String(optionsObj[k]))) + ); + const pos = jsListToTerm(positionals, (p) => V.String(p)); + return V.Struct(functorId("schema"), [options, pos]); +} + +function toRegistryTerm(registry) { + return jsListToTerm(Object.keys(registry), (name) => + pairTerm(V.String(name), toEntryTerm(registry[name])) + ); +} + +function runParse(argvTerm, registryTerm) { + const state = Runtime.new_state(); + state.program = program; + Runtime.put_reg(state, 1, argvTerm); + // Hold the result cell itself: parse_args/2 tail-calls parse_args/3, so A2 + // is overwritten. Unification still binds this unbound; deref it after run. + const resultVar = Runtime.new_var(state); + let label; + if (registryTerm === undefined) { + Runtime.put_reg(state, 2, resultVar); + label = "parse_args/2"; + } else { + Runtime.put_reg(state, 2, registryTerm); + Runtime.put_reg(state, 3, resultVar); + label = "parse_args/3"; + } + const startPc = program.labels[label]; + if (startPc === undefined) { + throw new Error(`cliArgs shim: missing label ${label}`); + } + state.pc = startPc; + const ok = Runtime.run(program, state) === true; + if (!ok) { + throw new Error("cliArgs shim: parse_args failed in the WAM"); + } + return { state, result: Runtime.deref(state, resultVar) }; +} + +/** + * parseArgs(argv, registry?) -> { positional, flags } + * Throws CliError with the oracle's exact message on a usage error. + */ +export function parseArgs(argv, registry) { + const argvTerm = jsListToTerm(Array.from(argv, String), (s) => V.String(s)); + const registryTerm = registry === undefined ? undefined : toRegistryTerm(registry); + const { state, result } = runParse(argvTerm, registryTerm); + if (!result || result.tag !== "struct") { + throw new Error(`cliArgs shim: parse_args answered neither ok/2 nor error/1 (${String(result)})`); + } + const fname = functorName(result.fid); + if (fname === "ok" && (result.args || []).length === 2) { + return { + positional: positionalToJs(state, result.args[0]), + flags: pairsToObject(state, result.args[1]) + }; + } + if (fname === "error" && (result.args || []).length === 1) { + throw new CliError(String(scalarToJs(state, result.args[0]))); + } + throw new Error( + `cliArgs shim: parse_args answered neither ok/2 nor error/1 (${fname})` + ); +} diff --git a/examples/cli_args/wamjs/cliArgs.wamjs.test.mjs b/examples/cli_args/wamjs/cliArgs.wamjs.test.mjs new file mode 100644 index 000000000..3915eb135 --- /dev/null +++ b/examples/cli_args/wamjs/cliArgs.wamjs.test.mjs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// cliArgs.wamjs.test.mjs -- peerhailer's own 17-test contract corpus +// (examples/cli_args/oracle/cliArgs.test.mjs), run against the JS-WAM +// compiled parser. The ONLY edit is the import specifier on the line marked +// below; every assertion is the oracle's, verbatim. +// +// node --test examples/cli_args/wamjs/cliArgs.wamjs.test.mjs +/** + * The contract matrix from docs/cli-arg-parsing.md, proven at the parser level. + * (Point 14 — help generated from the same schemas — is a separate feature, not + * yet built, so it is not asserted here.) + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { parseArgs, CliError } from "./cliArgs.mjs"; // <-- the only edit + +const of = (line) => parseArgs(line.split(" ").filter(Boolean)); + +test("1 & 2: a boolean flag works before or after the positional", () => { + for (const line of ["block --include-key bob", "block bob --include-key"]) { + const { positional, flags } = of(line); + assert.deepEqual(positional, ["block", "bob"], line); + assert.equal(flags["include-key"], true, line); + } +}); + +test("3: an unknown option is refused against the command schema", () => { + assert.throws(() => of("block bob --include-keey"), CliError); +}); + +test("4: --include-key=false is off; =anything-else is on", () => { + assert.equal(of("block bob --include-key=false").flags["include-key"], false); + assert.equal(of("block bob --include-key=yes").flags["include-key"], true); +}); + +test("5: a value given to a boolean flag lands as an extra positional and fails", () => { + assert.throws(() => of("block bob --include-key yes"), /extra argument: yes/); +}); + +test("6: --state selects the same state before or after the command", () => { + assert.equal(of("--state P block bob").flags.state, "P"); + assert.equal(of("block bob --state P").flags.state, "P"); +}); + +test("7: `--` preserves a forwarded command with its own flags", () => { + const { positional } = of("commands add deploy -- ./run.sh --env prod"); + // The handler reads rest.slice(2).join(" "); rest is positional after the command. + assert.deepEqual(positional.slice(3), ["./run.sh", "--env", "prod"]); +}); + +test("8: after `--`, a --state token is payload, not the global option", () => { + const { flags, positional } = of("commands add deploy -- ./run.sh --state child.json"); + assert.equal(flags.state, undefined, "not swallowed as the global"); + assert.ok(positional.includes("--state") && positional.includes("child.json"), "kept as payload"); +}); + +test("9: --key keeps an inline dash-leading PEM as its value", () => { + const pem = "-----BEGIN-PUBLIC-KEY-----"; + const { flags } = parseArgs(["add", "bob", "--key", pem]); + assert.equal(flags.key, pem); +}); + +test("10: --debug is bare-true, spaced, or =valued", () => { + assert.equal(of("daemon --debug").flags.debug, true); + assert.equal(of("daemon --debug 2").flags.debug, "2"); + assert.equal(of("daemon --debug=2").flags.debug, "2"); +}); + +test("11: a value handed to a boolean daemon flag fails loudly", () => { + assert.throws(() => of("daemon --require-target-binding yes"), /extra argument: yes/); +}); + +test("12: --force is refused on `profiles pin` — it belongs to `profiles remove`", () => { + assert.throws(() => of("profiles pin trusted --force"), /unknown option --force/); + // ...and is accepted on remove. + assert.equal(of("profiles remove temp --force").flags.force, true); +}); + +test("13: a string option with no value fails", () => { + assert.throws(() => of("add bob --profile"), /--profile needs a value/); +}); + +test("a missing required positional fails with the argument name", () => { + assert.throws(() => of("block"), /missing argument: name/); +}); + +test("an unmigrated command falls back to the lenient parse (no schema, no error)", () => { + // `tunnels` has no schema yet: unknown flags are accepted, booleans stay greedy — + // exactly the legacy behaviour, so the migration can proceed leaf by leaf. + const { positional, flags } = of("tunnels add acp 127.0.0.1:9100 --anything here"); + assert.equal(positional[0], "tunnels"); + assert.equal(flags.anything, "here"); +}); + +test("regression: daemon accepts --hail-on-encrypted / --hail-on-tls (Fable)", () => { + assert.equal(of("daemon --hail-on-encrypted tailscale0").flags["hail-on-encrypted"], "tailscale0"); + assert.equal(of("daemon --hail-on-tls eth0").flags["hail-on-tls"], "eth0"); +}); + +test("regression: unblock --key with no name is valid (Fable)", () => { + const { flags, positional } = of("unblock --key ABCDEF12"); + assert.equal(flags.key, "ABCDEF12"); + assert.deepEqual(positional, ["unblock"], "no name required"); + // ...and the name form still works. + assert.deepEqual(of("unblock bob").positional, ["unblock", "bob"]); +}); + +test("the lenient fallback is verbatim: --a --b=c keeps the old greedy reading", () => { + // An unmigrated command must behave exactly as before: the old parser read + // `--b=c` (which is not a bare --word) as the value of --a. + const { flags } = of("tunnels --a --b=c"); + assert.equal(flags.a, "--b=c", "unchanged legacy behaviour on an unmigrated command"); +}); diff --git a/examples/cli_args/wamjs/diff_runner_wamjs.mjs b/examples/cli_args/wamjs/diff_runner_wamjs.mjs new file mode 100644 index 000000000..f5726e767 --- /dev/null +++ b/examples/cli_args/wamjs/diff_runner_wamjs.mjs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2026 John William Creighton (s243a) +// +// diff_runner_wamjs.mjs -- the JS-WAM side of the differential harness. +// +// Byte-for-byte the same protocol as examples/cli_args/diff_runner.mjs (the +// oracle side) and diff_runner.pl (the SWI side): read argv-lines on stdin, one +// case per line, tokens separated by spaces, skip lines with no tokens, and +// print one JSON object per line -- +// +// {"ok":{"positional":[...],"flags":{...}}} +// {"error":""} +// {"crash":""} // never expected; a hard failure +// +// The only difference from diff_runner.mjs is the import: `parseArgs` comes from +// the JS-WAM compiled parser's edge shim instead of from the vendored oracle. +// +// node examples/cli_args/wamjs/diff_runner_wamjs.mjs < lines.txt > wamjs.jsonl + +import { parseArgs, CliError } from "./cliArgs.mjs"; + +const tokenize = (line) => line.split(" ").filter(Boolean); + +function runLine(line) { + try { + const { positional, flags } = parseArgs(tokenize(line)); + return { ok: { positional, flags } }; + } catch (err) { + if (err instanceof CliError) return { error: err.message }; + return { crash: String((err && err.message) || err) }; + } +} + +const chunks = []; +for await (const chunk of process.stdin) chunks.push(chunk); +const input = Buffer.concat(chunks).toString("utf8"); + +const out = []; +for (const line of input.split("\n")) { + if (tokenize(line).length === 0) continue; + out.push(JSON.stringify(runLine(line))); +} +process.stdout.write(out.length ? out.join("\n") + "\n" : ""); diff --git a/examples/cli_args/wamjs/js/generated_program.js b/examples/cli_args/wamjs/js/generated_program.js new file mode 100644 index 000000000..33722143f --- /dev/null +++ b/examples/cli_args/wamjs/js/generated_program.js @@ -0,0 +1,2742 @@ +// Generated by UnifyWeaver JavaScript WAM Target +// Generated at: 2026-09-01 +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2026 John William Creighton (@s243a) + +"use strict"; + +const path = require("path"); +const runtime = require(path.join(__dirname, "wam_runtime.js")); +const Runtime = runtime.Runtime; +const V = runtime.V; +const I = runtime.I; +const M = {}; + +const intern_seed = [ + "true", + "fail", + "[]", + ".", + "", + "[|]", + "schema", + "false", + "err", + "ok", + "+", + "-", + "boolean", + "string", + "optional", + "group", + "[", + "]", + "=", + "some", + "none", + "other", + "error" +]; + +const intern_table = Runtime.new_intern_table(intern_seed); + +// 1-based instruction vector: index 0 is unused so label PCs match the emitter. +const shared_instructions = [ + null, + I.Allocate(), + I.GetVariable(201, 1), + I.GetVariable(203, 2), + I.GetVariable(204, 3), + I.GetLevel(205), + I.TryMeElse("L_ite_else_1"), + I.PutValue(201, 1), + I.PutValue(203, 2), + I.PutVariable(202, 3), + I.Call("pair_lookup", 3), + I.Cut(205), + I.PutValue(204, 1), + I.PutValue(202, 2), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_1"), + I.TrustMe(), + I.PutValue(203, 1), + I.Call("js_object_prototype_key", 1), + I.PutValue(204, 1), + I.PutStructure(6, 2, 2), + I.SetConstant(V.Atom(2)), + I.SetConstant(V.Atom(2)), + I.BuiltinCall("=/2", 2), + I.Deallocate(), + I.Proceed(), + I.Allocate(), + I.GetVariable(210, 1), + I.GetVariable(203, 2), + I.GetVariable(213, 3), + I.PutValue(210, 1), + I.PutVariable(208, 2), + I.BuiltinCall("length/2", 2), + I.PutValue(203, 1), + I.PutVariable(209, 2), + I.BuiltinCall("length/2", 2), + I.GetLevel(214), + I.TryMeElse("L_ite_else_2"), + I.PutValue(203, 1), + I.PutVariable(201, 2), + I.Call("last_element", 2), + I.PutValue(201, 1), + I.PutConstant(V.String("..."), 2), + I.Call("starts_with", 2), + I.Cut(214), + I.PutVariable(207, 1), + I.PutConstant(V.Atom(0), 2), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_2"), + I.TrustMe(), + I.PutVariable(207, 1), + I.PutConstant(V.Atom(7), 2), + I.BuiltinCall("=/2", 2), + I.PutValue(203, 1), + I.PutConstant(V.Int(0), 2), + I.PutVariable(202, 3), + I.Call("count_required", 3), + I.GetLevel(215), + I.TryMeElse("L_ite_else_3"), + I.PutValue(208, 1), + I.PutValue(202, 2), + I.BuiltinCall("/2", 2), + I.Cut(216), + I.PutValue(209, 1), + I.PutValue(210, 2), + I.PutConstant(V.String(""), 3), + I.PutVariable(211, 4), + I.Call("nth0_default", 4), + I.PutConstant(V.String("unexpected extra argument: "), 1), + I.PutValue(211, 2), + I.PutVariable(212, 3), + I.BuiltinCall("string_concat/3", 3), + I.PutValue(213, 1), + I.PutStructure(8, 2, 1), + I.SetValue(212), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_4"), + I.TrustMe(), + I.PutValue(213, 1), + I.PutConstant(V.Atom(9), 2), + I.BuiltinCall("=/2", 2), + I.Deallocate(), + I.Proceed(), + I.SwitchOnTerm([{value: V.Atom(2), label: "default"}], [], "L_count_required_3_2_body", 1), + I.TryMeElse("L_count_required_3_2"), + I.GetConstant(V.Atom(2), 1), + I.GetVariable(101, 2), + I.GetValue(101, 3), + I.Proceed(), + I.TrustMe(), + I.Allocate(), + I.GetList(1, 5), + I.UnifyVariable(201), + I.UnifyVariable(203), + I.GetVariable(202, 2), + I.GetVariable(205, 3), + I.GetLevel(206), + I.TryMeElse("L_ite_else_5"), + I.PutValue(201, 1), + I.PutConstant(V.String("["), 2), + I.Call("starts_with", 2), + I.Cut(206), + I.PutVariable(204, 1), + I.PutValue(202, 2), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_5"), + I.TrustMe(), + I.GetLevel(207), + I.TryMeElse("L_ite_else_6"), + I.PutValue(201, 1), + I.PutConstant(V.String("..."), 2), + I.Call("starts_with", 2), + I.Cut(207), + I.PutVariable(204, 1), + I.PutValue(202, 2), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_6"), + I.TrustMe(), + I.PutVariable(204, 1), + I.PutStructure(10, 2, 2), + I.SetValue(202), + I.SetConstant(V.Int(1)), + I.BuiltinCall("is/2", 2), + I.PutValue(203, 1), + I.PutValue(204, 2), + I.PutValue(205, 3), + I.Deallocate(), + I.Execute("count_required", 3), + I.GetList(1, 5), + I.UnifyVariable(101), + I.GetStructure(11, 101, 2), + I.UnifyConstant(V.String("block")), + I.UnifyVariable(102), + I.GetStructure(6, 102, 2), + I.UnifyVariable(103), + I.GetStructure(5, 103, 2), + I.UnifyVariable(104), + I.GetStructure(11, 104, 2), + I.UnifyConstant(V.String("include-key")), + I.UnifyConstant(V.Atom(12)), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(105), + I.GetStructure(5, 105, 2), + I.UnifyConstant(V.String("name")), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(106), + I.GetStructure(5, 106, 2), + I.UnifyVariable(107), + I.GetStructure(11, 107, 2), + I.UnifyConstant(V.String("unblock")), + I.UnifyVariable(108), + I.GetStructure(6, 108, 2), + I.UnifyVariable(109), + I.GetStructure(5, 109, 2), + I.UnifyVariable(110), + I.GetStructure(11, 110, 2), + I.UnifyConstant(V.String("key")), + I.UnifyConstant(V.Atom(13)), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(111), + I.GetStructure(5, 111, 2), + I.UnifyConstant(V.String("[name]")), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(112), + I.GetStructure(5, 112, 2), + I.UnifyVariable(113), + I.GetStructure(11, 113, 2), + I.UnifyConstant(V.String("add")), + I.UnifyVariable(114), + I.GetStructure(6, 114, 2), + I.UnifyVariable(115), + I.GetStructure(5, 115, 2), + I.UnifyVariable(116), + I.GetStructure(11, 116, 2), + I.UnifyConstant(V.String("profile")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(117), + I.GetStructure(5, 117, 2), + I.UnifyVariable(118), + I.GetStructure(11, 118, 2), + I.UnifyConstant(V.String("until")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(119), + I.GetStructure(5, 119, 2), + I.UnifyVariable(120), + I.GetStructure(11, 120, 2), + I.UnifyConstant(V.String("key")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(121), + I.GetStructure(5, 121, 2), + I.UnifyVariable(122), + I.GetStructure(11, 122, 2), + I.UnifyConstant(V.String("key-file")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(123), + I.GetStructure(5, 123, 2), + I.UnifyVariable(124), + I.GetStructure(11, 124, 2), + I.UnifyConstant(V.String("transport")), + I.UnifyConstant(V.Atom(13)), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(125), + I.GetStructure(5, 125, 2), + I.UnifyConstant(V.String("name")), + I.UnifyVariable(126), + I.GetStructure(5, 126, 2), + I.UnifyConstant(V.String("[address]")), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(127), + I.GetStructure(5, 127, 2), + I.UnifyVariable(128), + I.GetStructure(11, 128, 2), + I.UnifyConstant(V.String("daemon")), + I.UnifyVariable(129), + I.GetStructure(6, 129, 2), + I.UnifyVariable(130), + I.GetStructure(5, 130, 2), + I.UnifyVariable(131), + I.GetStructure(11, 131, 2), + I.UnifyConstant(V.String("debug")), + I.UnifyConstant(V.Atom(14)), + I.UnifyVariable(132), + I.GetStructure(5, 132, 2), + I.UnifyVariable(133), + I.GetStructure(11, 133, 2), + I.UnifyConstant(V.String("port")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(134), + I.GetStructure(5, 134, 2), + I.UnifyVariable(135), + I.GetStructure(11, 135, 2), + I.UnifyConstant(V.String("host")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(136), + I.GetStructure(5, 136, 2), + I.UnifyVariable(137), + I.GetStructure(11, 137, 2), + I.UnifyConstant(V.String("hail-on")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(138), + I.GetStructure(5, 138, 2), + I.UnifyVariable(139), + I.GetStructure(11, 139, 2), + I.UnifyConstant(V.String("hail-on-encrypted")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(140), + I.GetStructure(5, 140, 2), + I.UnifyVariable(141), + I.GetStructure(11, 141, 2), + I.UnifyConstant(V.String("hail-on-tls")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(142), + I.GetStructure(5, 142, 2), + I.UnifyVariable(143), + I.GetStructure(11, 143, 2), + I.UnifyConstant(V.String("tls-cert")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(144), + I.GetStructure(5, 144, 2), + I.UnifyVariable(145), + I.GetStructure(11, 145, 2), + I.UnifyConstant(V.String("tls-key")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(146), + I.GetStructure(5, 146, 2), + I.UnifyVariable(147), + I.GetStructure(11, 147, 2), + I.UnifyConstant(V.String("allow-origin")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(148), + I.GetStructure(5, 148, 2), + I.UnifyVariable(149), + I.GetStructure(11, 149, 2), + I.UnifyConstant(V.String("chat")), + I.UnifyConstant(V.Atom(12)), + I.UnifyVariable(150), + I.GetStructure(5, 150, 2), + I.UnifyVariable(151), + I.GetStructure(11, 151, 2), + I.UnifyConstant(V.String("route")), + I.UnifyConstant(V.Atom(12)), + I.UnifyVariable(152), + I.GetStructure(5, 152, 2), + I.UnifyVariable(153), + I.GetStructure(11, 153, 2), + I.UnifyConstant(V.String("ui")), + I.UnifyConstant(V.Atom(12)), + I.UnifyVariable(154), + I.GetStructure(5, 154, 2), + I.UnifyVariable(155), + I.GetStructure(11, 155, 2), + I.UnifyConstant(V.String("require-target-binding")), + I.UnifyConstant(V.Atom(12)), + I.UnifyVariable(156), + I.GetStructure(5, 156, 2), + I.UnifyVariable(157), + I.GetStructure(11, 157, 2), + I.UnifyConstant(V.String("require-sealed")), + I.UnifyConstant(V.Atom(12)), + I.UnifyConstant(V.Atom(2)), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(158), + I.GetStructure(5, 158, 2), + I.UnifyVariable(159), + I.GetStructure(11, 159, 2), + I.UnifyConstant(V.String("commands")), + I.UnifyVariable(160), + I.GetStructure(15, 160, 1), + I.UnifyVariable(161), + I.GetStructure(5, 161, 2), + I.UnifyVariable(162), + I.GetStructure(11, 162, 2), + I.UnifyConstant(V.String("add")), + I.UnifyVariable(163), + I.GetStructure(6, 163, 2), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(164), + I.GetStructure(5, 164, 2), + I.UnifyConstant(V.String("name")), + I.UnifyVariable(165), + I.GetStructure(5, 165, 2), + I.UnifyConstant(V.String("...command")), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(166), + I.GetStructure(5, 166, 2), + I.UnifyVariable(167), + I.GetStructure(11, 167, 2), + I.UnifyConstant(V.String("remove")), + I.UnifyVariable(168), + I.GetStructure(6, 168, 2), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(169), + I.GetStructure(5, 169, 2), + I.UnifyConstant(V.String("name")), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(170), + I.GetStructure(5, 170, 2), + I.UnifyVariable(171), + I.GetStructure(11, 171, 2), + I.UnifyConstant(V.String("list")), + I.UnifyVariable(172), + I.GetStructure(6, 172, 2), + I.UnifyConstant(V.Atom(2)), + I.UnifyConstant(V.Atom(2)), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(173), + I.GetStructure(5, 173, 2), + I.UnifyVariable(174), + I.GetStructure(11, 174, 2), + I.UnifyConstant(V.String("route")), + I.UnifyVariable(175), + I.GetStructure(15, 175, 1), + I.UnifyVariable(176), + I.GetStructure(5, 176, 2), + I.UnifyVariable(177), + I.GetStructure(11, 177, 2), + I.UnifyConstant(V.String("discover")), + I.UnifyVariable(178), + I.GetStructure(6, 178, 2), + I.UnifyVariable(179), + I.GetStructure(5, 179, 2), + I.UnifyVariable(180), + I.GetStructure(11, 180, 2), + I.UnifyConstant(V.String("dest")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(181), + I.GetStructure(5, 181, 2), + I.UnifyVariable(182), + I.GetStructure(11, 182, 2), + I.UnifyConstant(V.String("dest-file")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(183), + I.GetStructure(5, 183, 2), + I.UnifyVariable(184), + I.GetStructure(11, 184, 2), + I.UnifyConstant(V.String("control")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(185), + I.GetStructure(5, 185, 2), + I.UnifyVariable(186), + I.GetStructure(11, 186, 2), + I.UnifyConstant(V.String("port")), + I.UnifyConstant(V.Atom(13)), + I.UnifyConstant(V.Atom(2)), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(187), + I.GetStructure(5, 187, 2), + I.UnifyVariable(188), + I.GetStructure(11, 188, 2), + I.UnifyConstant(V.String("status")), + I.UnifyVariable(189), + I.GetStructure(6, 189, 2), + I.UnifyVariable(190), + I.GetStructure(5, 190, 2), + I.UnifyVariable(191), + I.GetStructure(11, 191, 2), + I.UnifyConstant(V.String("dest")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(192), + I.GetStructure(5, 192, 2), + I.UnifyVariable(193), + I.GetStructure(11, 193, 2), + I.UnifyConstant(V.String("dest-file")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(194), + I.GetStructure(5, 194, 2), + I.UnifyVariable(195), + I.GetStructure(11, 195, 2), + I.UnifyConstant(V.String("control")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(196), + I.GetStructure(5, 196, 2), + I.UnifyVariable(197), + I.GetStructure(11, 197, 2), + I.UnifyConstant(V.String("port")), + I.UnifyConstant(V.Atom(13)), + I.UnifyConstant(V.Atom(2)), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(198), + I.GetStructure(5, 198, 2), + I.UnifyVariable(199), + I.GetStructure(11, 199, 2), + I.UnifyConstant(V.String("approve")), + I.UnifyVariable(200), + I.GetStructure(6, 200, 2), + I.UnifyVariable(201), + I.GetStructure(5, 201, 2), + I.UnifyVariable(202), + I.GetStructure(11, 202, 2), + I.UnifyConstant(V.String("dest")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(203), + I.GetStructure(5, 203, 2), + I.UnifyVariable(204), + I.GetStructure(11, 204, 2), + I.UnifyConstant(V.String("dest-file")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(205), + I.GetStructure(5, 205, 2), + I.UnifyVariable(206), + I.GetStructure(11, 206, 2), + I.UnifyConstant(V.String("seal-key")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(207), + I.GetStructure(5, 207, 2), + I.UnifyVariable(208), + I.GetStructure(11, 208, 2), + I.UnifyConstant(V.String("seal-key-file")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(209), + I.GetStructure(5, 209, 2), + I.UnifyVariable(210), + I.GetStructure(11, 210, 2), + I.UnifyConstant(V.String("control")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(211), + I.GetStructure(5, 211, 2), + I.UnifyVariable(212), + I.GetStructure(11, 212, 2), + I.UnifyConstant(V.String("port")), + I.UnifyConstant(V.Atom(13)), + I.UnifyConstant(V.Atom(2)), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(213), + I.GetStructure(5, 213, 2), + I.UnifyVariable(214), + I.GetStructure(11, 214, 2), + I.UnifyConstant(V.String("send")), + I.UnifyVariable(215), + I.GetStructure(6, 215, 2), + I.UnifyVariable(216), + I.GetStructure(5, 216, 2), + I.UnifyVariable(217), + I.GetStructure(11, 217, 2), + I.UnifyConstant(V.String("dest")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(218), + I.GetStructure(5, 218, 2), + I.UnifyVariable(219), + I.GetStructure(11, 219, 2), + I.UnifyConstant(V.String("dest-file")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(220), + I.GetStructure(5, 220, 2), + I.UnifyVariable(221), + I.GetStructure(11, 221, 2), + I.UnifyConstant(V.String("public")), + I.UnifyConstant(V.Atom(12)), + I.UnifyVariable(222), + I.GetStructure(5, 222, 2), + I.UnifyVariable(223), + I.GetStructure(11, 223, 2), + I.UnifyConstant(V.String("ttl")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(224), + I.GetStructure(5, 224, 2), + I.UnifyVariable(225), + I.GetStructure(11, 225, 2), + I.UnifyConstant(V.String("budget")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(226), + I.GetStructure(5, 226, 2), + I.UnifyVariable(227), + I.GetStructure(11, 227, 2), + I.UnifyConstant(V.String("control")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(228), + I.GetStructure(5, 228, 2), + I.UnifyVariable(229), + I.GetStructure(11, 229, 2), + I.UnifyConstant(V.String("port")), + I.UnifyConstant(V.Atom(13)), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(230), + I.GetStructure(5, 230, 2), + I.UnifyConstant(V.String("...message")), + I.UnifyConstant(V.Atom(2)), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(231), + I.GetStructure(5, 231, 2), + I.UnifyVariable(232), + I.GetStructure(11, 232, 2), + I.UnifyConstant(V.String("profiles")), + I.UnifyVariable(233), + I.GetStructure(15, 233, 1), + I.UnifyVariable(234), + I.GetStructure(5, 234, 2), + I.UnifyVariable(235), + I.GetStructure(11, 235, 2), + I.UnifyConstant(V.String("add")), + I.UnifyVariable(236), + I.GetStructure(6, 236, 2), + I.UnifyVariable(237), + I.GetStructure(5, 237, 2), + I.UnifyVariable(238), + I.GetStructure(11, 238, 2), + I.UnifyConstant(V.String("allows")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(239), + I.GetStructure(5, 239, 2), + I.UnifyVariable(240), + I.GetStructure(11, 240, 2), + I.UnifyConstant(V.String("description")), + I.UnifyConstant(V.Atom(13)), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(241), + I.GetStructure(5, 241, 2), + I.UnifyConstant(V.String("name")), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(242), + I.GetStructure(5, 242, 2), + I.UnifyVariable(243), + I.GetStructure(11, 243, 2), + I.UnifyConstant(V.String("remove")), + I.UnifyVariable(244), + I.GetStructure(6, 244, 2), + I.UnifyVariable(245), + I.GetStructure(5, 245, 2), + I.UnifyVariable(246), + I.GetStructure(11, 246, 2), + I.UnifyConstant(V.String("force")), + I.UnifyConstant(V.Atom(12)), + I.UnifyVariable(247), + I.GetStructure(5, 247, 2), + I.UnifyVariable(248), + I.GetStructure(11, 248, 2), + I.UnifyConstant(V.String("reassign")), + I.UnifyConstant(V.Atom(13)), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(249), + I.GetStructure(5, 249, 2), + I.UnifyConstant(V.String("name")), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(250), + I.GetStructure(5, 250, 2), + I.UnifyVariable(251), + I.GetStructure(11, 251, 2), + I.UnifyConstant(V.String("pin")), + I.UnifyVariable(252), + I.GetStructure(6, 252, 2), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(253), + I.GetStructure(5, 253, 2), + I.UnifyConstant(V.String("name")), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(254), + I.GetStructure(5, 254, 2), + I.UnifyVariable(255), + I.GetStructure(11, 255, 2), + I.UnifyConstant(V.String("unpin")), + I.UnifyVariable(256), + I.GetStructure(6, 256, 2), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(257), + I.GetStructure(5, 257, 2), + I.UnifyConstant(V.String("name")), + I.UnifyConstant(V.Atom(2)), + I.UnifyVariable(258), + I.GetStructure(5, 258, 2), + I.UnifyVariable(259), + I.GetStructure(11, 259, 2), + I.UnifyConstant(V.String("list")), + I.UnifyVariable(260), + I.GetStructure(6, 260, 2), + I.UnifyConstant(V.Atom(2)), + I.UnifyConstant(V.Atom(2)), + I.UnifyConstant(V.Atom(2)), + I.UnifyConstant(V.Atom(2)), + I.Proceed(), + I.SwitchOnTerm([{value: V.Atom(2), label: "default"}], [], "L_drop_brackets_2_2_body", 1), + I.TryMeElse("L_drop_brackets_2_2"), + I.GetConstant(V.Atom(2), 1), + I.GetConstant(V.Atom(2), 2), + I.Proceed(), + I.TrustMe(), + I.Allocate(), + I.GetList(1, 5), + I.UnifyVariable(202), + I.UnifyVariable(203), + I.GetVariable(201, 2), + I.GetLevel(205), + I.TryMeElse("L_ite_else_7"), + I.TryMeElse("L_ite_else_8"), + I.PutValue(202, 1), + I.PutConstant(V.Atom(16), 2), + I.BuiltinCall("==/2", 2), + I.Jump("L_ite_cont_8"), + I.TrustMe(), + I.PutValue(202, 1), + I.PutConstant(V.Atom(17), 2), + I.BuiltinCall("==/2", 2), + I.Cut(205), + I.PutValue(201, 1), + I.PutVariable(204, 2), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_7"), + I.TrustMe(), + I.PutValue(201, 1), + I.PutStructure(5, 2, 2), + I.SetValue(202), + I.SetVariable(204), + I.BuiltinCall("=/2", 2), + I.PutValue(203, 1), + I.PutValue(204, 2), + I.Deallocate(), + I.Execute("drop_brackets", 2), + I.SwitchOnTerm([{value: V.Atom(2), label: "default"}], [], "L_first_char_index_4_2_body", 1), + I.TryMeElse("L_first_char_index_4_2"), + I.GetConstant(V.Atom(2), 1), + I.GetVariable(101, 2), + I.GetVariable(102, 3), + I.GetConstant(V.Int(-1), 4), + I.Proceed(), + I.TrustMe(), + I.Allocate(), + I.GetList(1, 5), + I.UnifyVariable(201), + I.UnifyVariable(203), + I.GetVariable(204, 2), + I.GetVariable(202, 3), + I.GetVariable(206, 4), + I.GetLevel(207), + I.TryMeElse("L_ite_else_9"), + I.PutValue(201, 1), + I.PutValue(204, 2), + I.BuiltinCall("==/2", 2), + I.Cut(207), + I.PutValue(206, 1), + I.PutValue(202, 2), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_9"), + I.TrustMe(), + I.PutVariable(205, 1), + I.PutStructure(10, 2, 2), + I.SetValue(202), + I.SetConstant(V.Int(1)), + I.BuiltinCall("is/2", 2), + I.PutValue(203, 1), + I.PutValue(204, 2), + I.PutValue(205, 3), + I.PutValue(206, 4), + I.Call("first_char_index", 4), + I.Deallocate(), + I.Proceed(), + I.Allocate(), + I.GetVariable(103, 1), + I.GetVariable(202, 2), + I.PutValue(103, 1), + I.PutVariable(201, 2), + I.BuiltinCall("string_chars/2", 2), + I.PutValue(201, 1), + I.PutConstant(V.Atom(18), 2), + I.PutConstant(V.Int(0), 3), + I.PutValue(202, 4), + I.Deallocate(), + I.Execute("first_char_index", 4), + I.SwitchOnTerm([{value: V.Atom(2), label: "default"}], [], "L_flags_put_4_2_body", 1), + I.TryMeElse("L_flags_put_4_2"), + I.GetConstant(V.Atom(2), 1), + I.GetVariable(101, 2), + I.GetVariable(102, 3), + I.GetList(4, 5), + I.UnifyVariable(103), + I.GetStructure(11, 103, 2), + I.UnifyValue(101), + I.UnifyValue(102), + I.UnifyConstant(V.Atom(2)), + I.Proceed(), + I.TrustMe(), + I.Allocate(), + I.GetList(1, 5), + I.UnifyVariable(108), + I.GetStructure(11, 108, 2), + I.UnifyVariable(202), + I.UnifyVariable(203), + I.UnifyVariable(204), + I.GetVariable(205, 2), + I.GetVariable(206, 3), + I.GetVariable(201, 4), + I.GetLevel(208), + I.TryMeElse("L_ite_else_10"), + I.PutValue(202, 1), + I.PutValue(205, 2), + I.BuiltinCall("==/2", 2), + I.Cut(208), + I.PutValue(201, 1), + I.PutStructure(5, 2, 2), + I.SetVariable(110), + I.SetValue(204), + I.PutStructure(11, 110, 2), + I.SetValue(202), + I.SetValue(206), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_10"), + I.TrustMe(), + I.PutValue(201, 1), + I.PutStructure(5, 2, 2), + I.SetVariable(110), + I.SetVariable(207), + I.PutStructure(11, 110, 2), + I.SetValue(202), + I.SetValue(203), + I.BuiltinCall("=/2", 2), + I.PutValue(204, 1), + I.PutValue(205, 2), + I.PutValue(206, 3), + I.PutValue(207, 4), + I.Call("flags_put", 4), + I.Deallocate(), + I.Proceed(), + I.Allocate(), + I.GetVariable(201, 1), + I.GetVariable(202, 2), + I.GetVariable(203, 3), + I.GetVariable(204, 4), + I.GetLevel(205), + I.TryMeElse("L_ite_else_11"), + I.PutValue(202, 1), + I.PutConstant(V.String("__proto__"), 2), + I.BuiltinCall("==/2", 2), + I.Cut(205), + I.PutValue(204, 1), + I.PutValue(201, 2), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_11"), + I.TrustMe(), + I.PutValue(201, 1), + I.PutValue(202, 2), + I.PutValue(203, 3), + I.PutValue(204, 4), + I.Call("flags_put", 4), + I.Deallocate(), + I.Proceed(), + I.GetList(1, 5), + I.UnifyVariable(101), + I.GetStructure(11, 101, 2), + I.UnifyConstant(V.String("state")), + I.UnifyConstant(V.Atom(13)), + I.UnifyVariable(102), + I.GetStructure(5, 102, 2), + I.UnifyVariable(103), + I.GetStructure(11, 103, 2), + I.UnifyConstant(V.String("name")), + I.UnifyConstant(V.Atom(13)), + I.UnifyConstant(V.Atom(2)), + I.Proceed(), + I.Allocate(), + I.GetVariable(203, 1), + I.GetLevel(204), + I.TryMeElse("L_ite_else_12"), + I.PutVariable(201, 1), + I.Call("global_options", 1), + I.PutValue(201, 1), + I.PutValue(203, 2), + I.PutVariable(202, 3), + I.Call("pair_lookup", 3), + I.Cut(204), + I.BuiltinCall("true/0", 0), + I.Jump("L_ite_cont_12"), + I.TrustMe(), + I.PutValue(203, 1), + I.Call("js_object_prototype_key", 1), + I.Deallocate(), + I.Proceed(), + I.Allocate(), + I.PutVariable(201, 2), + I.BuiltinCall("string_chars/2", 2), + I.PutValue(201, 1), + I.PutStructure(5, 2, 2), + I.SetConstant(V.Atom(11)), + I.SetVariable(106), + I.PutStructure(5, 106, 2), + I.SetConstant(V.Atom(11)), + I.SetVariable(107), + I.PutStructure(5, 107, 2), + I.SetVariable(202), + I.SetVariable(203), + I.BuiltinCall("=/2", 2), + I.PutValue(202, 1), + I.Call("js_alpha", 1), + I.PutValue(203, 1), + I.Deallocate(), + I.Execute("long_flag_tail", 1), + I.Allocate(), + I.PutVariable(201, 2), + I.BuiltinCall("char_code/2", 2), + I.GetLevel(202), + I.TryMeElse("L_ite_else_13"), + I.PutValue(201, 1), + I.PutConstant(V.Int(97), 2), + I.BuiltinCall(">=/2", 2), + I.PutValue(201, 1), + I.PutConstant(V.Int(122), 2), + I.BuiltinCall("==/2", 2), + I.PutValue(201, 1), + I.PutConstant(V.Int(90), 2), + I.BuiltinCall("==/2", 2), + I.PutValue(201, 1), + I.PutConstant(V.Int(122), 2), + I.BuiltinCall("==/2", 2), + I.PutValue(201, 1), + I.PutConstant(V.Int(90), 2), + I.BuiltinCall("==/2", 2), + I.PutValue(201, 1), + I.PutConstant(V.Int(57), 2), + I.BuiltinCall("==/2", 2), + I.Cut(207), + I.PutValue(204, 1), + I.PutConstant(V.Int(2), 2), + I.PutValue(201, 3), + I.PutValue(205, 4), + I.Call("substring_range", 4), + I.PutVariable(202, 1), + I.PutStructure(10, 2, 2), + I.SetValue(201), + I.SetConstant(V.Int(1)), + I.BuiltinCall("is/2", 2), + I.PutValue(204, 1), + I.PutValue(202, 2), + I.PutVariable(203, 3), + I.Call("substring_from", 3), + I.PutValue(206, 1), + I.PutStructure(19, 2, 1), + I.SetValue(203), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_44"), + I.TrustMe(), + I.PutValue(204, 1), + I.PutConstant(V.Int(2), 2), + I.PutValue(205, 3), + I.Call("substring_from", 3), + I.PutValue(206, 1), + I.PutConstant(V.Atom(20), 2), + I.BuiltinCall("=/2", 2), + I.Deallocate(), + I.Proceed(), + I.Allocate(), + I.GetVariable(202, 1), + I.GetVariable(206, 2), + I.PutValue(202, 1), + I.PutVariable(201, 2), + I.BuiltinCall("string_length/2", 2), + I.PutValue(206, 1), + I.PutVariable(203, 2), + I.BuiltinCall("string_length/2", 2), + I.PutValue(201, 1), + I.PutValue(203, 2), + I.BuiltinCall(">=/2", 2), + I.PutValue(202, 1), + I.PutConstant(V.Int(0), 2), + I.PutValue(203, 3), + I.PutVariable(204, 4), + I.PutVariable(205, 5), + I.Call("sub_string", 5), + I.PutValue(205, 1), + I.PutValue(206, 2), + I.BuiltinCall("==/2", 2), + I.Deallocate(), + I.Proceed(), + I.SwitchOnTerm([{value: V.Atom(2), label: "default"}], [], "L_strict_loop_8_2_body", 1), + I.TryMeElse("L_strict_loop_8_2"), + I.GetConstant(V.Atom(2), 1), + I.GetVariable(101, 2), + I.GetVariable(102, 3), + I.GetVariable(103, 4), + I.GetVariable(104, 5), + I.GetValue(103, 6), + I.GetValue(104, 7), + I.GetConstant(V.Atom(9), 8), + I.Proceed(), + I.TrustMe(), + I.Allocate(), + I.GetList(1, 5), + I.UnifyVariable(201), + I.UnifyVariable(202), + I.GetVariable(209, 2), + I.GetVariable(203, 3), + I.GetVariable(204, 4), + I.GetVariable(205, 5), + I.GetVariable(206, 6), + I.GetVariable(207, 7), + I.GetVariable(208, 8), + I.GetLevel(214), + I.TryMeElse("L_ite_else_45"), + I.PutValue(209, 1), + I.PutConstant(V.Atom(0), 2), + I.BuiltinCall("==/2", 2), + I.Cut(214), + I.PutValue(202, 1), + I.PutConstant(V.Atom(0), 2), + I.PutValue(203, 3), + I.PutStructure(5, 4, 2), + I.SetValue(201), + I.SetValue(204), + I.PutValue(205, 5), + I.PutValue(206, 6), + I.PutValue(207, 7), + I.PutValue(208, 8), + I.Call("strict_loop", 8), + I.Jump("L_ite_cont_45"), + I.TrustMe(), + I.GetLevel(215), + I.TryMeElse("L_ite_else_46"), + I.PutValue(201, 1), + I.PutConstant(V.String("--"), 2), + I.BuiltinCall("==/2", 2), + I.Cut(215), + I.PutValue(202, 1), + I.PutConstant(V.Atom(0), 2), + I.PutValue(203, 3), + I.PutValue(204, 4), + I.PutValue(205, 5), + I.PutValue(206, 6), + I.PutValue(207, 7), + I.PutValue(208, 8), + I.Call("strict_loop", 8), + I.Jump("L_ite_cont_46"), + I.TrustMe(), + I.GetLevel(216), + I.TryMeElse("L_ite_else_47"), + I.GetLevel(217), + I.TryMeElse("L_ite_else_48"), + I.PutValue(201, 1), + I.PutConstant(V.String("--"), 2), + I.Call("starts_with", 2), + I.Cut(217), + I.BuiltinCall("fail/0", 0), + I.Jump("L_ite_cont_48"), + I.TrustMe(), + I.BuiltinCall("true/0", 0), + I.Cut(216), + I.PutValue(202, 1), + I.PutValue(209, 2), + I.PutValue(203, 3), + I.PutStructure(5, 4, 2), + I.SetValue(201), + I.SetValue(204), + I.PutValue(205, 5), + I.PutValue(206, 6), + I.PutValue(207, 7), + I.PutValue(208, 8), + I.Call("strict_loop", 8), + I.Jump("L_ite_cont_47"), + I.TrustMe(), + I.PutValue(201, 1), + I.PutVariable(210, 2), + I.PutVariable(211, 3), + I.Call("split_flag_token", 3), + I.GetLevel(217), + I.TryMeElse("L_ite_else_49"), + I.PutValue(203, 1), + I.PutValue(210, 2), + I.PutVariable(212, 3), + I.Call("option_kind", 3), + I.Cut(217), + I.PutValue(212, 1), + I.PutValue(210, 2), + I.PutValue(211, 3), + I.PutValue(202, 4), + I.PutValue(209, 5), + I.PutValue(203, 6), + I.PutValue(204, 7), + I.PutValue(205, 8), + I.PutValue(206, 9), + I.PutValue(207, 10), + I.PutValue(208, 11), + I.Call("strict_option", 11), + I.Jump("L_ite_cont_49"), + I.TrustMe(), + I.PutConstant(V.String("unknown option --"), 1), + I.PutValue(210, 2), + I.PutVariable(213, 3), + I.BuiltinCall("string_concat/3", 3), + I.PutValue(206, 1), + I.PutValue(204, 2), + I.BuiltinCall("=/2", 2), + I.PutValue(207, 1), + I.PutValue(205, 2), + I.BuiltinCall("=/2", 2), + I.PutValue(208, 1), + I.PutStructure(8, 2, 1), + I.SetValue(213), + I.BuiltinCall("=/2", 2), + I.Deallocate(), + I.Proceed(), + I.Allocate(), + I.GetVariable(216, 1), + I.GetVariable(206, 2), + I.GetVariable(203, 3), + I.GetVariable(208, 4), + I.GetVariable(209, 5), + I.GetVariable(210, 6), + I.GetVariable(211, 7), + I.GetVariable(205, 8), + I.GetVariable(212, 9), + I.GetVariable(213, 10), + I.GetVariable(214, 11), + I.GetLevel(225), + I.TryMeElse("L_ite_else_50"), + I.PutValue(216, 1), + I.PutConstant(V.Atom(12), 2), + I.BuiltinCall("==/2", 2), + I.Cut(225), + I.GetLevel(226), + I.TryMeElse("L_ite_else_51"), + I.PutValue(203, 1), + I.PutStructure(19, 2, 1), + I.SetVariable(201), + I.BuiltinCall("=/2", 2), + I.Cut(226), + I.GetLevel(227), + I.TryMeElse("L_ite_else_52"), + I.PutValue(201, 1), + I.PutConstant(V.String("false"), 2), + I.BuiltinCall("==/2", 2), + I.Cut(227), + I.PutVariable(202, 1), + I.PutConstant(V.Atom(7), 2), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_52"), + I.TrustMe(), + I.PutVariable(202, 1), + I.PutConstant(V.Atom(0), 2), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_51"), + I.TrustMe(), + I.PutVariable(202, 1), + I.PutConstant(V.Atom(0), 2), + I.BuiltinCall("=/2", 2), + I.PutValue(205, 1), + I.PutValue(206, 2), + I.PutValue(202, 3), + I.PutVariable(207, 4), + I.Call("flags_set", 4), + I.PutValue(208, 1), + I.PutValue(209, 2), + I.PutValue(210, 3), + I.PutValue(211, 4), + I.PutValue(207, 5), + I.PutValue(212, 6), + I.PutValue(213, 7), + I.PutValue(214, 8), + I.Call("strict_loop", 8), + I.Jump("L_ite_cont_50"), + I.TrustMe(), + I.GetLevel(226), + I.TryMeElse("L_ite_else_53"), + I.PutValue(203, 1), + I.PutStructure(19, 2, 1), + I.SetVariable(204), + I.BuiltinCall("=/2", 2), + I.Cut(226), + I.PutValue(205, 1), + I.PutValue(206, 2), + I.PutValue(204, 3), + I.PutVariable(207, 4), + I.Call("flags_set", 4), + I.PutValue(208, 1), + I.PutValue(209, 2), + I.PutValue(210, 3), + I.PutValue(211, 4), + I.PutValue(207, 5), + I.PutValue(212, 6), + I.PutValue(213, 7), + I.PutValue(214, 8), + I.Call("strict_loop", 8), + I.Jump("L_ite_cont_53"), + I.TrustMe(), + I.PutValue(208, 1), + I.PutVariable(215, 2), + I.Call("next_value", 2), + I.GetLevel(227), + I.TryMeElse("L_ite_else_54"), + I.PutValue(216, 1), + I.PutConstant(V.Atom(13), 2), + I.BuiltinCall("==/2", 2), + I.Cut(227), + I.GetLevel(228), + I.TryMeElse("L_ite_else_55"), + I.PutValue(215, 1), + I.PutStructure(19, 2, 1), + I.SetVariable(217), + I.BuiltinCall("=/2", 2), + I.Cut(228), + I.PutValue(205, 1), + I.PutValue(206, 2), + I.PutValue(217, 3), + I.PutVariable(207, 4), + I.Call("flags_set", 4), + I.PutValue(208, 1), + I.PutStructure(5, 2, 2), + I.SetVariable(218), + I.SetVariable(219), + I.BuiltinCall("=/2", 2), + I.PutValue(219, 1), + I.PutValue(209, 2), + I.PutValue(210, 3), + I.PutValue(211, 4), + I.PutValue(207, 5), + I.PutValue(212, 6), + I.PutValue(213, 7), + I.PutValue(214, 8), + I.Call("strict_loop", 8), + I.Jump("L_ite_cont_55"), + I.TrustMe(), + I.PutConstant(V.String("--"), 1), + I.PutValue(206, 2), + I.PutVariable(220, 3), + I.BuiltinCall("string_concat/3", 3), + I.PutValue(220, 1), + I.PutConstant(V.String(" needs a value"), 2), + I.PutVariable(221, 3), + I.BuiltinCall("string_concat/3", 3), + I.PutValue(212, 1), + I.PutValue(211, 2), + I.BuiltinCall("=/2", 2), + I.PutValue(213, 1), + I.PutValue(205, 2), + I.BuiltinCall("=/2", 2), + I.PutValue(214, 1), + I.PutStructure(8, 2, 1), + I.SetValue(221), + I.BuiltinCall("=/2", 2), + I.Jump("L_ite_cont_54"), + I.TrustMe(), + I.GetLevel(228), + I.TryMeElse("L_ite_else_56"), + I.PutValue(215, 1), + I.PutStructure(19, 2, 1), + I.SetVariable(222), + I.BuiltinCall("=/2", 2), + I.Cut(228), + I.PutValue(205, 1), + I.PutValue(206, 2), + I.PutValue(222, 3), + I.PutVariable(207, 4), + I.Call("flags_set", 4), + I.PutValue(208, 1), + I.PutStructure(5, 2, 2), + I.SetVariable(223), + I.SetVariable(224), + I.BuiltinCall("=/2", 2), + I.PutValue(224, 1), + I.PutValue(209, 2), + I.PutValue(210, 3), + I.PutValue(211, 4), + I.PutValue(207, 5), + I.PutValue(212, 6), + I.PutValue(213, 7), + I.PutValue(214, 8), + I.Call("strict_loop", 8), + I.Jump("L_ite_cont_56"), + I.TrustMe(), + I.PutValue(205, 1), + I.PutValue(206, 2), + I.PutConstant(V.Atom(0), 3), + I.PutVariable(207, 4), + I.Call("flags_set", 4), + I.PutValue(208, 1), + I.PutValue(209, 2), + I.PutValue(210, 3), + I.PutValue(211, 4), + I.PutValue(207, 5), + I.PutValue(212, 6), + I.PutValue(213, 7), + I.PutValue(214, 8), + I.Call("strict_loop", 8), + I.Deallocate(), + I.Proceed(), + I.Allocate(), + I.GetVariable(202, 1), + I.GetList(2, 5), + I.UnifyVariable(201), + I.UnifyVariable(203), + I.GetLevel(204), + I.TryMeElse("L_ite_else_57"), + I.PutValue(202, 1), + I.PutValue(201, 2), + I.BuiltinCall("==/2", 2), + I.Cut(204), + I.BuiltinCall("true/0", 0), + I.Jump("L_ite_cont_57"), + I.TrustMe(), + I.PutValue(202, 1), + I.PutValue(203, 2), + I.Call("string_member", 2), + I.Deallocate(), + I.Proceed(), + I.Allocate(), + I.GetVariable(104, 1), + I.GetVariable(202, 2), + I.PutValue(104, 1), + I.PutVariable(201, 2), + I.BuiltinCall("string_chars/2", 2), + I.PutValue(201, 1), + I.PutVariable(203, 2), + I.Call("drop_brackets", 2), + I.PutValue(202, 1), + I.PutValue(203, 2), + I.BuiltinCall("string_chars/2", 2), + I.Deallocate(), + I.Proceed(), + I.Allocate(), + I.GetVariable(202, 1), + I.GetVariable(203, 2), + I.GetVariable(205, 3), + I.PutValue(202, 1), + I.PutVariable(201, 2), + I.BuiltinCall("string_length/2", 2), + I.PutVariable(204, 1), + I.PutStructure(11, 2, 2), + I.SetValue(201), + I.SetValue(203), + I.BuiltinCall("is/2", 2), + I.PutValue(202, 1), + I.PutValue(203, 2), + I.PutValue(204, 3), + I.PutConstant(V.Int(0), 4), + I.PutValue(205, 5), + I.Deallocate(), + I.Execute("sub_string", 5), + I.Allocate(), + I.GetVariable(201, 1), + I.GetVariable(202, 2), + I.GetVariable(106, 3), + I.GetVariable(205, 4), + I.PutVariable(203, 1), + I.PutStructure(11, 2, 2), + I.SetValue(106), + I.SetValue(202), + I.BuiltinCall("is/2", 2), + I.PutValue(201, 1), + I.PutValue(202, 2), + I.PutValue(203, 3), + I.PutVariable(204, 4), + I.PutValue(205, 5), + I.Deallocate(), + I.Execute("sub_string", 5) +]; + +const shared_labels = { + "substring_range/4": 2145, + "substring_from/3": 2126, + "strip_brackets/2": 2112, + "string_member/2": 2093, + "L_ite_else_57": 2106, + "L_ite_cont_57": 2110, + "strict_option/11": 1909, + "L_ite_else_52": 1944, + "L_ite_cont_52": 1948, + "L_ite_else_51": 1949, + "L_ite_cont_51": 1953, + "L_ite_else_50": 1968, + "L_ite_else_53": 1991, + "L_ite_else_55": 2028, + "L_ite_cont_55": 2047, + "L_ite_else_54": 2048, + "L_ite_else_56": 2076, + "L_ite_cont_56": 2091, + "L_ite_cont_54": 2091, + "L_ite_cont_53": 2091, + "L_ite_cont_50": 2091, + "strict_loop/8": 1783, + "L_strict_loop_8_2": 1794, + "L_strict_loop_8_2_body": 1795, + "L_ite_else_45": 1824, + "L_ite_else_46": 1841, + "L_ite_else_48": 1852, + "L_ite_cont_48": 1854, + "L_ite_else_47": 1867, + "L_ite_else_49": 1892, + "L_ite_cont_49": 1907, + "L_ite_cont_47": 1907, + "L_ite_cont_46": 1907, + "L_ite_cont_45": 1907, + "starts_with/2": 1760, + "split_flag_token/3": 1718, + "L_ite_else_44": 1750, + "L_ite_cont_44": 1758, + "schema_for/5": 1677, + "L_ite_else_43": 1709, + "L_ite_cont_43": 1716, + "scan_leading_globals/4": 1566, + "L_scan_leading_globals_4_2": 1573, + "L_scan_leading_globals_4_2_body": 1574, + "L_ite_else_41": 1617, + "L_ite_else_42": 1644, + "L_ite_cont_42": 1655, + "L_ite_cont_41": 1655, + "L_ite_else_40": 1656, + "L_ite_cont_40": 1665, + "L_ite_else_39": 1666, + "L_ite_cont_39": 1675, + "registry_entry/3": 1541, + "L_ite_else_38": 1556, + "L_ite_cont_38": 1564, + "parse_strict/4": 1481, + "L_ite_else_36": 1509, + "L_ite_else_37": 1529, + "L_ite_cont_37": 1539, + "L_ite_cont_36": 1539, + "parse_lenient/3": 1466, + "parse_args/3": 1336, + "L_ite_else_31": 1360, + "L_ite_cont_31": 1362, + "L_ite_else_32": 1376, + "L_ite_cont_32": 1380, + "L_ite_else_34": 1413, + "L_ite_cont_34": 1422, + "L_ite_else_35": 1445, + "L_ite_cont_35": 1454, + "L_ite_else_33": 1455, + "L_ite_cont_33": 1459, + "L_ite_else_30": 1460, + "L_ite_cont_30": 1464, + "parse_args/2": 1326, + "pair_lookup/3": 1300, + "L_ite_else_29": 1319, + "L_ite_cont_29": 1324, + "option_kind/3": 1263, + "L_ite_else_27": 1278, + "L_ite_else_28": 1292, + "L_ite_cont_28": 1298, + "L_ite_cont_27": 1298, + "nth0_default/4": 1222, + "L_ite_else_26": 1245, + "L_ite_cont_26": 1256, + "L_ite_else_25": 1257, + "L_ite_cont_25": 1261, + "next_value/2": 1185, + "L_next_value_2_2": 1190, + "L_next_value_2_2_body": 1191, + "L_ite_else_24": 1208, + "L_ite_cont_24": 1210, + "L_ite_else_23": 1216, + "L_ite_cont_23": 1220, + "merge_flags_/3": 1159, + "L_merge_flags__3_2": 1165, + "L_merge_flags__3_2_body": 1166, + "merge_flags/3": 1150, + "looks_like_legacy_flag/1": 1131, + "long_flag_tail/1": 1107, + "L_long_flag_tail_1_2": 1111, + "L_long_flag_tail_1_2_body": 1112, + "L_ite_else_22": 1124, + "L_ite_cont_22": 1129, + "lenient_result/2": 1097, + "lenient_loop/5": 997, + "L_lenient_loop_5_2": 1005, + "L_lenient_loop_5_2_body": 1006, + "L_ite_else_19": 1043, + "L_ite_else_21": 1058, + "L_ite_cont_21": 1060, + "L_ite_else_20": 1073, + "L_ite_cont_20": 1085, + "L_ite_cont_19": 1085, + "L_ite_else_18": 1086, + "L_ite_cont_18": 1095, + "legacy_flag_tail/1": 983, + "L_legacy_flag_tail_1_2": 987, + "L_legacy_flag_tail_1_2_body": 988, + "last_element/2": 962, + "L_ite_else_17": 977, + "L_ite_cont_17": 981, + "js_object_prototype_keys/1": 925, + "js_object_prototype_key/1": 917, + "js_flag_char/1": 873, + "L_ite_else_14": 887, + "L_ite_else_15": 899, + "L_ite_else_16": 911, + "L_ite_cont_16": 915, + "L_ite_cont_15": 915, + "L_ite_cont_14": 915, + "js_alpha/1": 850, + "L_ite_else_13": 864, + "L_ite_cont_13": 871, + "is_long_flag/1": 831, + "is_global_key/1": 813, + "L_ite_else_12": 826, + "L_ite_cont_12": 829, + "global_options/1": 800, + "flags_set/4": 777, + "L_ite_else_11": 792, + "L_ite_cont_11": 798, + "flags_put/4": 723, + "L_flags_put_4_2": 735, + "L_flags_put_4_2_body": 736, + "L_ite_else_10": 761, + "L_ite_cont_10": 775, + "first_equals_index/2": 711, + "first_char_index/4": 673, + "L_first_char_index_4_2": 680, + "L_first_char_index_4_2_body": 681, + "L_ite_else_9": 698, + "L_ite_cont_9": 709, + "drop_brackets/2": 636, + "L_drop_brackets_2_2": 641, + "L_drop_brackets_2_2_body": 642, + "L_ite_else_8": 654, + "L_ite_cont_8": 658, + "L_ite_else_7": 663, + "L_ite_cont_7": 669, + "default_registry/1": 155, + "count_required/3": 110, + "L_count_required_3_2": 116, + "L_count_required_3_2_body": 117, + "L_ite_else_5": 133, + "L_ite_else_6": 144, + "L_ite_cont_6": 150, + "L_ite_cont_5": 150, + "check_arity/3": 26, + "L_ite_else_2": 49, + "L_ite_cont_2": 53, + "L_ite_else_3": 80, + "L_ite_else_4": 104, + "L_ite_cont_4": 108, + "L_ite_cont_3": 108, + "action_entry/3": 1, + "L_ite_else_1": 16, + "L_ite_cont_1": 24 +}; + +const dispatch = { + "substring_range/4": 2145, + "substring_from/3": 2126, + "strip_brackets/2": 2112, + "string_member/2": 2093, + "strict_option/11": 1909, + "strict_loop/8": 1783, + "starts_with/2": 1760, + "split_flag_token/3": 1718, + "schema_for/5": 1677, + "scan_leading_globals/4": 1566, + "registry_entry/3": 1541, + "parse_strict/4": 1481, + "parse_lenient/3": 1466, + "parse_args/3": 1336, + "parse_args/2": 1326, + "pair_lookup/3": 1300, + "option_kind/3": 1263, + "nth0_default/4": 1222, + "next_value/2": 1185, + "merge_flags_/3": 1159, + "merge_flags/3": 1150, + "looks_like_legacy_flag/1": 1131, + "long_flag_tail/1": 1107, + "lenient_result/2": 1097, + "lenient_loop/5": 997, + "legacy_flag_tail/1": 983, + "last_element/2": 962, + "js_object_prototype_keys/1": 925, + "js_object_prototype_key/1": 917, + "js_flag_char/1": 873, + "js_alpha/1": 850, + "is_long_flag/1": 831, + "is_global_key/1": 813, + "global_options/1": 800, + "flags_set/4": 777, + "flags_put/4": 723, + "first_equals_index/2": 711, + "first_char_index/4": 673, + "drop_brackets/2": 636, + "default_registry/1": 155, + "count_required/3": 110, + "check_arity/3": 26, + "action_entry/3": 1 +}; + +const foreign_handlers = { + +}; + +const fact_sources = { + +}; + +const shared_program = { + instructions: shared_instructions, + labels: shared_labels, + dispatch: dispatch, + foreign_handlers: foreign_handlers, + inline_facts: {}, + fact_sources: fact_sources, + indexed_atom_fact2: {}, + intern_table: intern_table +}; +Runtime.resolve_program(shared_program); + + +const lowered_dispatch = {}; + + +function substring_range(a1, a2, a3, a4) { + return Runtime.run_predicate(shared_program, 2145, [a1, a2, a3, a4]); +} +M.substring_range = substring_range; + +function substring_from(a1, a2, a3) { + return Runtime.run_predicate(shared_program, 2126, [a1, a2, a3]); +} +M.substring_from = substring_from; + +function strip_brackets(a1, a2) { + return Runtime.run_predicate(shared_program, 2112, [a1, a2]); +} +M.strip_brackets = strip_brackets; + +function string_member(a1, a2) { + return Runtime.run_predicate(shared_program, 2093, [a1, a2]); +} +M.string_member = string_member; + +function strict_option(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11) { + return Runtime.run_predicate(shared_program, 1909, [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11]); +} +M.strict_option = strict_option; + +function strict_loop(a1, a2, a3, a4, a5, a6, a7, a8) { + return Runtime.run_predicate(shared_program, 1783, [a1, a2, a3, a4, a5, a6, a7, a8]); +} +M.strict_loop = strict_loop; + +function starts_with(a1, a2) { + return Runtime.run_predicate(shared_program, 1760, [a1, a2]); +} +M.starts_with = starts_with; + +function split_flag_token(a1, a2, a3) { + return Runtime.run_predicate(shared_program, 1718, [a1, a2, a3]); +} +M.split_flag_token = split_flag_token; + +function schema_for(a1, a2, a3, a4, a5) { + return Runtime.run_predicate(shared_program, 1677, [a1, a2, a3, a4, a5]); +} +M.schema_for = schema_for; + +function scan_leading_globals(a1, a2, a3, a4) { + return Runtime.run_predicate(shared_program, 1566, [a1, a2, a3, a4]); +} +M.scan_leading_globals = scan_leading_globals; + +function registry_entry(a1, a2, a3) { + return Runtime.run_predicate(shared_program, 1541, [a1, a2, a3]); +} +M.registry_entry = registry_entry; + +function parse_strict(a1, a2, a3, a4) { + return Runtime.run_predicate(shared_program, 1481, [a1, a2, a3, a4]); +} +M.parse_strict = parse_strict; + +function parse_lenient(a1, a2, a3) { + return Runtime.run_predicate(shared_program, 1466, [a1, a2, a3]); +} +M.parse_lenient = parse_lenient; + +function parse_args(a1, a2, a3) { + return Runtime.run_predicate(shared_program, 1336, [a1, a2, a3]); +} +M.parse_args = parse_args; + +function parse_args(a1, a2) { + return Runtime.run_predicate(shared_program, 1326, [a1, a2]); +} +M.parse_args = parse_args; + +function pair_lookup(a1, a2, a3) { + return Runtime.run_predicate(shared_program, 1300, [a1, a2, a3]); +} +M.pair_lookup = pair_lookup; + +function option_kind(a1, a2, a3) { + return Runtime.run_predicate(shared_program, 1263, [a1, a2, a3]); +} +M.option_kind = option_kind; + +function nth0_default(a1, a2, a3, a4) { + return Runtime.run_predicate(shared_program, 1222, [a1, a2, a3, a4]); +} +M.nth0_default = nth0_default; + +function next_value(a1, a2) { + return Runtime.run_predicate(shared_program, 1185, [a1, a2]); +} +M.next_value = next_value; + +function merge_flags_(a1, a2, a3) { + return Runtime.run_predicate(shared_program, 1159, [a1, a2, a3]); +} +M.merge_flags_ = merge_flags_; + +function merge_flags(a1, a2, a3) { + return Runtime.run_predicate(shared_program, 1150, [a1, a2, a3]); +} +M.merge_flags = merge_flags; + +function looks_like_legacy_flag(a1) { + return Runtime.run_predicate(shared_program, 1131, [a1]); +} +M.looks_like_legacy_flag = looks_like_legacy_flag; + +function long_flag_tail(a1) { + return Runtime.run_predicate(shared_program, 1107, [a1]); +} +M.long_flag_tail = long_flag_tail; + +function lenient_result(a1, a2) { + return Runtime.run_predicate(shared_program, 1097, [a1, a2]); +} +M.lenient_result = lenient_result; + +function lenient_loop(a1, a2, a3, a4, a5) { + return Runtime.run_predicate(shared_program, 997, [a1, a2, a3, a4, a5]); +} +M.lenient_loop = lenient_loop; + +function legacy_flag_tail(a1) { + return Runtime.run_predicate(shared_program, 983, [a1]); +} +M.legacy_flag_tail = legacy_flag_tail; + +function last_element(a1, a2) { + return Runtime.run_predicate(shared_program, 962, [a1, a2]); +} +M.last_element = last_element; + +function js_object_prototype_keys(a1) { + return Runtime.run_predicate(shared_program, 925, [a1]); +} +M.js_object_prototype_keys = js_object_prototype_keys; + +function js_object_prototype_key(a1) { + return Runtime.run_predicate(shared_program, 917, [a1]); +} +M.js_object_prototype_key = js_object_prototype_key; + +function js_flag_char(a1) { + return Runtime.run_predicate(shared_program, 873, [a1]); +} +M.js_flag_char = js_flag_char; + +function js_alpha(a1) { + return Runtime.run_predicate(shared_program, 850, [a1]); +} +M.js_alpha = js_alpha; + +function is_long_flag(a1) { + return Runtime.run_predicate(shared_program, 831, [a1]); +} +M.is_long_flag = is_long_flag; + +function is_global_key(a1) { + return Runtime.run_predicate(shared_program, 813, [a1]); +} +M.is_global_key = is_global_key; + +function global_options(a1) { + return Runtime.run_predicate(shared_program, 800, [a1]); +} +M.global_options = global_options; + +function flags_set(a1, a2, a3, a4) { + return Runtime.run_predicate(shared_program, 777, [a1, a2, a3, a4]); +} +M.flags_set = flags_set; + +function flags_put(a1, a2, a3, a4) { + return Runtime.run_predicate(shared_program, 723, [a1, a2, a3, a4]); +} +M.flags_put = flags_put; + +function first_equals_index(a1, a2) { + return Runtime.run_predicate(shared_program, 711, [a1, a2]); +} +M.first_equals_index = first_equals_index; + +function first_char_index(a1, a2, a3, a4) { + return Runtime.run_predicate(shared_program, 673, [a1, a2, a3, a4]); +} +M.first_char_index = first_char_index; + +function drop_brackets(a1, a2) { + return Runtime.run_predicate(shared_program, 636, [a1, a2]); +} +M.drop_brackets = drop_brackets; + +function default_registry(a1) { + return Runtime.run_predicate(shared_program, 155, [a1]); +} +M.default_registry = default_registry; + +function count_required(a1, a2, a3) { + return Runtime.run_predicate(shared_program, 110, [a1, a2, a3]); +} +M.count_required = count_required; + +function check_arity(a1, a2, a3) { + return Runtime.run_predicate(shared_program, 26, [a1, a2, a3]); +} +M.check_arity = check_arity; + +function action_entry(a1, a2, a3) { + return Runtime.run_predicate(shared_program, 1, [a1, a2, a3]); +} +M.action_entry = action_entry; + + +M.program = shared_program; +M.Runtime = Runtime; +M.V = V; +M.I = I; + +function is_main() { + return require.main === module; +} + +if (is_main()) { + const predArity = process.argv[2]; + if (predArity === undefined) { + process.stdout.write("usage: node generated_program.js / ...\n"); + process.exit(0); + } + const startPc = shared_labels[predArity]; + const loweredFn = lowered_dispatch[predArity]; + if (startPc === undefined && typeof loweredFn !== "function") { + process.stdout.write("unknown predicate: " + predArity + "\n"); + process.exit(1); + } + const parsed = []; + for (let i = 3; i < process.argv.length; i++) { + parsed.push(Runtime.parse_cli_atom_or_int(shared_program, process.argv[i])); + } + 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; + const profiling = !!Runtime._prof; + let ok; + if (typeof loweredFn === "function") { + // Dispatch wrapper increments call counts; enter without a second count + // so exclusive wall time still attributes to this predicate. + if (profiling) Runtime.prof_enter(predArity, false); + ok = loweredFn(shared_program, state) === true; + if (profiling) Runtime.prof_leave(); + } else { + if (profiling) Runtime.prof_enter(predArity, true); + state.pc = startPc; + ok = Runtime.run(shared_program, state) === true; + if (profiling) Runtime.prof_leave(); + } + if (profiling) Runtime.profileReport(); + if (ok) { + const keys = Object.keys(state.regs).map(Number).filter(function (n) { return n >= 1 && n <= 16; }).sort(function (a, b) { return a - b; }); + if (keys.length > 0) { + for (let i = 0; i < keys.length; i++) { + const v = Runtime.get_reg(state, keys[i]); + if (v !== undefined) { + process.stdout.write("A" + keys[i] + " = " + Runtime.term_to_string(shared_program, state, v) + "\n"); + } + } + } + process.stdout.write("true\n"); + process.exit(0); + } + process.stdout.write("false\n"); + process.exit(1); +} + +module.exports = M; diff --git a/examples/cli_args/wamjs/js/wam_runtime.js b/examples/cli_args/wamjs/js/wam_runtime.js new file mode 100644 index 000000000..5cccaba43 --- /dev/null +++ b/examples/cli_args/wamjs/js/wam_runtime.js @@ -0,0 +1,4113 @@ +// Generated by UnifyWeaver JavaScript WAM Target +// Generated at: 2026-09-01 +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) 2026 John William Creighton (@s243a) +// +// Interpreter-tier Node WAM VM. Ported from the Lua WAM runtime with +// evalArith, call/1, findall/bagof/setof/aggregate_all, and the six +// WAM backend conventions (cons aliasing, trailing /arity parse, +// placeholder bind-through, deref-before-type-test, integral is/2, +// one-slot NoOp for unknown instructions). + +"use strict"; + +const V = {}; +const I = {}; +const Runtime = {}; +// Opt-in profiler handle. Stay null (falsy) unless UW_PROFILE / Runtime.profile() +// turns it on, so the hot path pays one property check and no allocation. +Runtime._prof = null; + +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 || [] }; }; + +function instr(op, fields) { + const out = fields || {}; + out.op = op; + return out; +} + +I.GetConstant = function (c, ai) { return instr("GetConstant", { c: c, ai: ai }); }; +I.GetVariable = function (xn, ai) { return instr("GetVariable", { xn: xn, ai: ai }); }; +I.GetValue = function (xn, ai) { return instr("GetValue", { xn: xn, ai: ai }); }; +I.GetStructure = function (fid, ai, arity) { return instr("GetStructure", { fid: fid, ai: ai, arity: arity }); }; +I.GetList = function (ai, fid) { return instr("GetList", { ai: ai, fid: fid }); }; +I.PutConstant = function (c, ai) { return instr("PutConstant", { c: c, ai: ai }); }; +I.PutVariable = function (xn, ai) { return instr("PutVariable", { xn: xn, ai: ai }); }; +I.PutValue = function (xn, ai) { return instr("PutValue", { xn: xn, ai: ai }); }; +I.PutStructure = function (fid, ai, arity) { return instr("PutStructure", { fid: fid, ai: ai, arity: arity }); }; +I.PutList = function (ai, fid) { return instr("PutList", { ai: ai, fid: fid }); }; +I.UnifyVariable = function (xn) { return instr("UnifyVariable", { xn: xn }); }; +I.UnifyValue = function (xn) { return instr("UnifyValue", { xn: xn }); }; +I.UnifyConstant = function (c) { return instr("UnifyConstant", { c: c }); }; +I.SetVariable = function (xn) { return instr("SetVariable", { xn: xn }); }; +I.SetValue = function (xn) { return instr("SetValue", { xn: xn }); }; +I.SetConstant = function (c) { return instr("SetConstant", { c: c }); }; +I.Call = function (p, n) { return instr("Call", { pred: p, arity: n }); }; +I.CallPc = function (pc, n) { return instr("CallPc", { pc: pc, arity: n }); }; +I.CallIndexedAtomFact2 = function (p) { return instr("CallIndexedAtomFact2", { pred: p }); }; +I.CallFactStream = function (p, n) { return instr("CallFactStream", { pred: p, arity: n }); }; +I.Execute = function (p, n) { return instr("Execute", { pred: p, arity: n }); }; +I.ExecutePc = function (pc) { return instr("ExecutePc", { pc: pc }); }; +I.CallForeign = function (p, n) { return instr("CallForeign", { pred: p, arity: n }); }; +I.ExecuteForeign = function (p, n) { return instr("ExecuteForeign", { pred: p, arity: n }); }; +I.Proceed = function () { return instr("Proceed"); }; +I.Fail = function () { return instr("Fail"); }; +I.Allocate = function () { return instr("Allocate"); }; +I.Deallocate = function () { return instr("Deallocate"); }; +I.TryMeElse = function (label) { return instr("TryMeElse", { label: label }); }; +I.TryMeElsePc = function (pc) { return instr("TryMeElsePc", { pc: pc }); }; +I.RetryMeElse = function (label) { return instr("RetryMeElse", { label: label }); }; +I.RetryMeElsePc = function (pc) { return instr("RetryMeElsePc", { pc: pc }); }; +I.TrustMe = function () { return instr("TrustMe"); }; +I.Try = function (label) { return instr("Try", { label: label }); }; +I.TryPc = function (pc) { return instr("TryPc", { pc: pc }); }; +I.Retry = function (label) { return instr("Retry", { label: label }); }; +I.RetryPc = function (pc) { return instr("RetryPc", { pc: pc }); }; +I.Trust = function (label) { return instr("Trust", { label: label }); }; +I.TrustPc = function (pc) { return instr("TrustPc", { pc: pc }); }; +I.Jump = function (label) { return instr("Jump", { label: label }); }; +I.JumpPc = function (pc) { return instr("JumpPc", { pc: pc }); }; +I.CutIte = function () { return instr("CutIte"); }; +I.GetLevel = function (yn) { return instr("GetLevel", { yn: yn }); }; +I.Cut = function (yn) { return instr("Cut", { yn: yn }); }; +I.BuiltinCall = function (p, n) { return instr("BuiltinCall", { pred: p, arity: n }); }; +I.ArgInstr = function (n, ai, out) { return instr("ArgInstr", { n: n, ai: ai, out: out }); }; +I.SwitchOnConstant = function (cases, fallthrough) { + return instr("SwitchOnConstant", { cases: cases, fallthrough: fallthrough === true }); +}; +I.SwitchOnConstantPc = function (cases, fallthrough) { + return instr("SwitchOnConstantPc", { cases: cases, fallthrough: fallthrough === true }); +}; +I.SwitchOnConstantA2 = function (cases, fallthrough) { + return instr("SwitchOnConstantA2", { cases: cases, fallthrough: fallthrough === true }); +}; +I.SwitchOnConstantA2Pc = function (cases, fallthrough) { + return instr("SwitchOnConstantA2Pc", { cases: cases, fallthrough: fallthrough === true }); +}; +I.SwitchOnStructure = function (cases) { return instr("SwitchOnStructure", { cases: cases }); }; +I.SwitchOnStructurePc = function (cases) { return instr("SwitchOnStructurePc", { cases: cases }); }; +I.SwitchOnStructureA2 = function (cases) { return instr("SwitchOnStructureA2", { cases: cases }); }; +I.SwitchOnStructureA2Pc = function (cases) { return instr("SwitchOnStructureA2Pc", { cases: cases }); }; +I.SwitchOnTerm = function (consts, structs, listLabel, reg) { + return instr("SwitchOnTerm", { + consts: consts || [], + structs: structs || [], + listLabel: listLabel || "none", + reg: reg || 1 + }); +}; +I.SwitchOnTermPc = function (consts, structs, listLabel, listPc, reg) { + return instr("SwitchOnTermPc", { + consts: consts || [], + structs: structs || [], + listLabel: listLabel || "none", + listPc: listPc, + reg: reg || 1 + }); +}; +I.BeginAggregate = function (kind, template, bag, witnesses) { + return instr("BeginAggregate", { + kind: kind, template: template, bag: bag, witnesses: witnesses || [] + }); +}; +I.EndAggregate = function (template) { return instr("EndAggregate", { template: template }); }; +I.Raw = function (text) { return instr("Raw", { text: text }); }; +I.NoOp = function () { return instr("NoOp"); }; + +Runtime.new_intern_table = function (seed) { + const t = { id_to_string: {}, string_to_id: {}, next_id: 0 }; + if (Array.isArray(seed)) { + for (let i = 0; i < seed.length; i++) { + const s = String(seed[i]); + t.id_to_string[i] = s; + t.string_to_id[s] = i; + t.next_id = i + 1; + } + } else if (seed && typeof seed === "object") { + const keys = Object.keys(seed); + for (let i = 0; i < keys.length; i++) { + const id = Number(keys[i]); + const s = String(seed[keys[i]]); + t.id_to_string[id] = s; + t.string_to_id[s] = id; + if (id + 1 > t.next_id) t.next_id = id + 1; + } + } + return t; +}; + +Runtime.intern = function (tab, s) { + s = String(s); + if (Object.prototype.hasOwnProperty.call(tab.string_to_id, s)) { + return tab.string_to_id[s]; + } + const id = tab.next_id; + tab.id_to_string[id] = s; + tab.string_to_id[s] = id; + tab.next_id = id + 1; + return id; +}; + +Runtime.string_of = function (tab, id) { + if (Object.prototype.hasOwnProperty.call(tab.id_to_string, id)) { + return tab.id_to_string[id]; + } + return ""; +}; + +Runtime.copy_table = function (t) { + const out = {}; + if (!t) return out; + const keys = Object.keys(t); + for (let i = 0; i < keys.length; i++) out[keys[i]] = t[keys[i]]; + return out; +}; + +Runtime.functor_name = function (tab, fid) { + const raw = Runtime.string_of(tab, fid); + return strip_trailing_arity(raw); +}; + +function strip_trailing_arity(name) { + const s = String(name); + const slash = s.lastIndexOf("/"); + if (slash <= 0) return s; + const rest = s.slice(slash + 1); + if (/^[0-9]+$/.test(rest)) return s.slice(0, slash); + return s; +} + +function is_cons_name(name) { + return name === "[|]" || name === "." || name === "./2" || name === "[|]/2"; +} + +function is_cons_fid(program, fid) { + return is_cons_name(Runtime.string_of(program.intern_table, fid)); +} + +function same_functor(program, aFid, aArity, bFid, bArity) { + if (aArity !== bArity) return false; + if (aFid === bFid) return true; + if (aArity === 2 && is_cons_fid(program, aFid) && is_cons_fid(program, bFid)) return true; + return false; +} + +Runtime.copy_term = function (state, v, seen) { + v = Runtime.deref(state, v); + if (typeof v !== "object" || v === null) return v; + seen = seen || new Map(); + if (seen.has(v)) return seen.get(v); + if (v.tag === "struct") { + const out = V.Struct(v.fid, []); + seen.set(v, out); + const args = v.args || []; + for (let i = 0; i < args.length; i++) out.args[i] = Runtime.copy_term(state, args[i], seen); + return out; + } + 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; +}; + +Runtime.new_state = function () { + return { + pc: 1, + cp: 0, + regs: {}, + stack: [], + cps: [], + bindings: {}, + trail: [], + var_counter: 0, + halt: false, + mode: null, + build_stack: [], + read_args: [], + read_cursor: 0, + read_stack: [], + y_save: [] + }; +}; + +Runtime.get_reg = function (state, idx) { return state.regs[idx]; }; +Runtime.put_reg = function (state, idx, val) { state.regs[idx] = val; }; + +Runtime.new_var = function (state) { + if (Runtime._prof) Runtime._prof.heapCells += 1; + const v = V.Unbound("V" + String(state.var_counter)); + state.var_counter += 1; + return v; +}; + +Runtime.deref = function (state, v) { + while (typeof v === "object" && v !== null && v.tag === "unbound") { + if (!Object.prototype.hasOwnProperty.call(state.bindings, v.name)) return v; + v = state.bindings[v.name]; + } + return v; +}; + +function same_atomic(a, b) { + if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return 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; +} + +function bind_var(state, variable, val) { + if (Runtime._prof) Runtime._prof.trailPushes += 1; + state.trail.push(variable.name); + state.bindings[variable.name] = val; + return true; +} + +Runtime.unify = function (state, a, b, program) { + if (Runtime._prof) Runtime._prof.unifyCalls += 1; + a = Runtime.deref(state, a); + b = Runtime.deref(state, b); + if (typeof a === "object" && a !== null && typeof b === "object" && b !== null + && a.tag === "unbound" && b.tag === "unbound" && a.name === b.name) { + return true; + } + if (typeof a === "object" && a !== null && a.tag === "unbound") return bind_var(state, a, b); + if (typeof b === "object" && b !== null && b.tag === "unbound") return bind_var(state, b, a); + if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return a === b; + if (a.tag === "struct" && b.tag === "struct") { + const aArity = (a.args || []).length; + const bArity = (b.args || []).length; + if (program) { + if (!same_functor(program, a.fid, aArity, b.fid, bArity)) return false; + } else if (a.fid !== b.fid || aArity !== bArity) { + return false; + } + for (let n = 0; n < aArity; n++) { + if (Runtime.unify(state, a.args[n], b.args[n], program) !== true) return false; + } + return true; + } + return same_atomic(a, b); +}; + +function is_unbound_cell(v) { + return typeof v === "object" && v !== null && v.tag === "unbound"; +} + +function bind_placeholder(state, target, built) { + // Convention 3: bind+trail the X/Y placeholder so outer-first + // nested construction is visible through the enclosing term. + // A-register exception (M139/M140): A slots (< 101) are staging. + if (target >= 101) { + const targetVal = Runtime.get_reg(state, target); + if (is_unbound_cell(targetVal)) { + const d = Runtime.deref(state, targetVal); + if (is_unbound_cell(d)) bind_var(state, d, built); + } + } +} + +function push_built_term(state, term) { + const stack = state.build_stack; + if (stack.length === 0) return false; + const top = stack[stack.length - 1]; + top.args.push(term); + if (top.args.length >= top.arity) { + stack.pop(); + if (stack.length === 0) state.mode = null; + } + return true; +} + +function begin_write(state, target, fid, arity, existing) { + let built = existing; + if (!built) { + built = V.Struct(fid, []); + if (Runtime._prof) Runtime._prof.heapCells += 1; + bind_placeholder(state, target, built); + Runtime.put_reg(state, target, built); + } + state.build_stack.push({ target: target, fid: fid, arity: arity, args: built.args, cell: built }); + state.mode = "write"; +} + +function begin_read(state, args) { + if (state.mode === "read") { + state.read_stack = state.read_stack || []; + state.read_stack.push({ args: state.read_args, cursor: state.read_cursor }); + } + state.mode = "read"; + state.read_args = args || []; + state.read_cursor = 0; +} + +function finish_read_if_done(state) { + if (state.mode !== "read") return; + if (state.read_cursor < (state.read_args || []).length) return; + if (state.read_stack && state.read_stack.length > 0) { + const prev = state.read_stack.pop(); + state.read_args = prev.args; + state.read_cursor = prev.cursor; + state.mode = "read"; + } else { + state.mode = null; + } +} + +function undo_trail(state, mark) { + const n = state.trail.length - mark; + while (state.trail.length > mark) { + const name = state.trail.pop(); + delete state.bindings[name]; + } + if (Runtime._prof && n > 0) Runtime._prof.trailUndos += 1; +} + +function snapshot_yregs(state) { + const y = {}; + const keys = Object.keys(state.regs); + for (let i = 0; i < keys.length; i++) { + const k = Number(keys[i]); + if (k >= 201) y[k] = state.regs[k]; + } + return y; +} + +function restore_yregs(state, y) { + const keys = Object.keys(state.regs); + for (let i = 0; i < keys.length; i++) { + const k = Number(keys[i]); + if (k >= 201) delete state.regs[k]; + } + if (!y) return; + const yk = Object.keys(y); + for (let i = 0; i < yk.length; i++) state.regs[yk[i]] = y[yk[i]]; +} + +function push_y_save(state) { + state.y_save = state.y_save || []; + state.y_save.push(snapshot_yregs(state)); +} + +function pop_y_save(state) { + if (!state.y_save || state.y_save.length === 0) return; + restore_yregs(state, state.y_save.pop()); +} + +// Return from the current predicate: restore the Caller's Y (if Call +// pushed a save) and jump to CP. Used by Proceed and by Execute of a +// builtin (the compiler emits Deallocate+Execute for a last-goal builtin +// with no user label; that must return, not halt the machine). +function proceed_to_cp(state) { + pop_y_save(state); + if (state.cp === 0) state.halt = true; + else { state.pc = state.cp; state.cp = 0; } +} + +function snapshot_machine(state) { + return { + regs: Runtime.copy_table(state.regs), + cp: state.cp, + trail_len: state.trail.length, + var_counter: state.var_counter, + mode: state.mode, + build_stack: state.build_stack.slice(), + stack: state.stack.slice(), + read_stack: (state.read_stack || []).slice(), + read_args: state.read_args, + read_cursor: state.read_cursor, + y_save: (state.y_save || []).slice() + }; +} + +function restore_machine(state, snap) { + undo_trail(state, snap.trail_len); + state.regs = Runtime.copy_table(snap.regs); + state.cp = snap.cp; + state.var_counter = snap.var_counter; + state.mode = snap.mode; + state.build_stack = snap.build_stack || []; + if (snap.stack) state.stack = snap.stack.slice(); + state.read_stack = (snap.read_stack || []).slice(); + if (snap.read_args !== undefined) state.read_args = snap.read_args; + if (snap.read_cursor !== undefined) state.read_cursor = snap.read_cursor; + state.y_save = (snap.y_save || []).slice(); +} + +function restore_cp_frame(state, cp) { + restore_machine(state, cp); +} + +let aggregate_result; + +Runtime.backtrack = function (state) { + if (state.cps.length === 0) return false; + const cp = state.cps[state.cps.length - 1]; + if (!cp) return false; + if (cp.kind === "indexed_atom_fact2") { + restore_cp_frame(state, cp); + const value = cp.values.shift(); + if (cp.values.length === 0) state.cps.pop(); + const atom = V.Atom(Runtime.intern(cp.table, value)); + if (Runtime.unify(state, Runtime.get_reg(state, 2), atom) !== true) { + return Runtime.backtrack(state); + } + state.pc = cp.next_pc; + state.halt = false; + return true; + } + if (cp.kind === "fact_stream") { + restore_cp_frame(state, cp); + const row = cp.rows.shift(); + if (cp.rows.length === 0) state.cps.pop(); + if (Runtime.unify(state, Runtime.get_reg(state, 1), row[0]) !== true) { + return Runtime.backtrack(state); + } + if (Runtime.unify(state, Runtime.get_reg(state, 2), row[1]) !== true) { + return Runtime.backtrack(state); + } + state.pc = cp.next_pc; + state.halt = false; + return true; + } + if (cp.kind === "member") { + restore_cp_frame(state, cp); + const value = cp.values.shift(); + if (cp.values.length === 0) state.cps.pop(); + if (Runtime.unify(state, Runtime.get_reg(state, 1), value) !== true) { + return Runtime.backtrack(state); + } + state.pc = cp.next_pc; + state.halt = false; + return true; + } + if (cp.kind === "enum_unify") { + restore_cp_frame(state, cp); + const sol = cp.sols.shift(); + if (cp.sols.length === 0) state.cps.pop(); + if (apply_enum_sol(state.program, state, sol) !== true) { + return Runtime.backtrack(state); + } + state.pc = cp.next_pc; + state.halt = false; + return true; + } + if (cp.kind === "between") { + restore_cp_frame(state, cp); + const n = cp.next; + if (n > cp.high) { + state.cps.pop(); + return Runtime.backtrack(state); + } + cp.next = n + 1; + if (cp.next > cp.high) state.cps.pop(); + if (Runtime.unify(state, Runtime.get_reg(state, 3), V.Int(n)) !== true) { + return Runtime.backtrack(state); + } + state.pc = cp.next_pc; + state.halt = false; + return true; + } + if (cp.kind === "aggregate") { + state.cps.pop(); + restore_cp_frame(state, cp); + return finalize_aggregate(state.program, state, cp); + } + if (cp.kind === "agg_next_group") { + restore_cp_frame(state, cp); + const group = cp.groups.shift(); + if (cp.groups.length === 0) state.cps.pop(); + if (apply_witness_group(state.program, state, cp, group) !== true) { + return Runtime.backtrack(state); + } + state.pc = cp.next_pc; + state.halt = false; + return true; + } + restore_machine(state, cp); + state.pc = cp.next_pc; + state.halt = false; + return true; +}; + +Runtime.list_from_terms = function (items, intern_table) { + const nilId = Runtime.intern(intern_table, "[]"); + const consId = Runtime.intern(intern_table, "[|]"); + let out = V.Atom(nilId); + for (let i = items.length - 1; i >= 0; i--) { + out = V.Struct(consId, [items[i], out]); + } + return out; +}; + +let bind_or_compare; + +function list_items(program, state, list) { + const nilId = Runtime.intern(program.intern_table, "[]"); + const items = []; + let cur = Runtime.deref(state, list); + while (typeof cur === "object" && cur !== null) { + if (cur.tag === "atom" && cur.id === nilId) return items; + if (cur.tag !== "struct" || (cur.args || []).length !== 2) return null; + if (!is_cons_fid(program, cur.fid)) return null; + items.push(cur.args[0]); + cur = Runtime.deref(state, cur.args[1]); + } + return null; +} + +function builtin_member(program, state) { + const items = list_items(program, state, Runtime.get_reg(state, 2)); + if (items === null || items.length === 0) return false; + if (items.length > 1) { + state.cps.push(snapshot_cp(state, { kind: "member", values: items.slice(1) })); + } + return Runtime.unify(state, Runtime.get_reg(state, 1), items[0], program); +} + +function builtin_length(program, state) { + const items = list_items(program, state, Runtime.get_reg(state, 1)); + if (items === null) return false; + return bind_or_compare(state, 2, V.Int(items.length), program); +} + +function term_to_string(program, state, term) { + term = Runtime.deref(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); + if (items !== null) { + const parts = []; + for (let i = 0; i < items.length; i++) parts.push(term_to_string(program, state, items[i])); + return "[" + parts.join(", ") + "]"; + } + const parts = []; + const args = term.args || []; + for (let i = 0; i < args.length; i++) parts.push(term_to_string(program, state, args[i])); + return Runtime.functor_name(program.intern_table, term.fid) + "(" + parts.join(", ") + ")"; + } + return String(term); +} + +function builtin_write(program, state) { + process.stdout.write(term_to_string(program, state, Runtime.get_reg(state, 1))); + return true; +} + +function builtin_writeq(program, state) { + process.stdout.write(quoted_term_to_string(program, state, Runtime.get_reg(state, 1))); + return true; +} + +function builtin_functor(program, state) { + const term = Runtime.deref(state, Runtime.get_reg(state, 1)); + if (typeof term === "object" && term !== null && term.tag === "unbound") { + const name = Runtime.deref(state, Runtime.get_reg(state, 2)); + const arity = Runtime.deref(state, Runtime.get_reg(state, 3)); + if (typeof arity !== "object" || arity === null || arity.tag !== "int" || arity.val < 0) return false; + if (typeof name !== "object" || name === null || name.tag === "unbound") return false; + let built; + if (arity.val === 0) { + built = name; + } else { + if (name.tag !== "atom") return false; + const args = []; + for (let i = 0; i < arity.val; i++) args.push(Runtime.new_var(state)); + built = V.Struct(name.id, args); + } + return Runtime.unify(state, term, built, program); + } + let name; + let arity; + if (typeof term === "object" && term !== null && term.tag === "struct") { + 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 === "string")) { + name = term; + arity = 0; + } else { + return false; + } + if (bind_or_compare(state, 2, name, program) !== true) return false; + return bind_or_compare(state, 3, V.Int(arity), program); +} + +function builtin_arg(program, state) { + const n = Runtime.deref(state, Runtime.get_reg(state, 1)); + const term = Runtime.deref(state, Runtime.get_reg(state, 2)); + if (typeof n !== "object" || n === null || n.tag !== "int" || n.val < 1) return false; + if (typeof term !== "object" || term === null || term.tag !== "struct") return false; + const arg = (term.args || [])[n.val - 1]; + if (arg === undefined) return false; + return bind_or_compare(state, 3, arg, program); +} + +function builtin_univ(program, state) { + const term = Runtime.deref(state, Runtime.get_reg(state, 1)); + if (typeof term === "object" && term !== null && term.tag === "unbound") { + const items = list_items(program, state, Runtime.get_reg(state, 2)); + if (items === null || items.length === 0) return false; + let built; + if (items.length === 1) { + built = items[0]; + } else { + const name = Runtime.deref(state, items[0]); + if (typeof name !== "object" || name === null || name.tag !== "atom") return false; + built = V.Struct(name.id, items.slice(1)); + } + return Runtime.unify(state, term, built, program); + } + let out; + if (typeof term === "object" && term !== null && term.tag === "struct") { + 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 === "string")) { + out = Runtime.list_from_terms([term], program.intern_table); + } else { + return false; + } + return bind_or_compare(state, 2, out, program); +} + +function copy_term_fresh(state, term, varMap, seen) { + term = Runtime.deref(state, term); + if (typeof term !== "object" || term === null) return term; + varMap = varMap || Object.create(null); + seen = seen || new Map(); + if (term.tag === "unbound") { + if (Object.prototype.hasOwnProperty.call(varMap, term.name)) return varMap[term.name]; + const fresh = Runtime.new_var(state); + varMap[term.name] = fresh; + return fresh; + } + if (seen.has(term)) return seen.get(term); + if (term.tag === "struct") { + const out = V.Struct(term.fid, []); + seen.set(term, out); + const args = term.args || []; + for (let i = 0; i < args.length; i++) out.args[i] = copy_term_fresh(state, args[i], varMap, seen); + return out; + } + 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; +} + +function builtin_copy_term(_program, state) { + const term = Runtime.get_reg(state, 1); + if (term === undefined || term === null) return false; + const copied = copy_term_fresh(state, term, Object.create(null), new Map()); + 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; +let goal_succeeds; + +function builtin_naf(program, state) { + const goal = Runtime.deref(state, Runtime.get_reg(state, 1)); + if (goal_succeeds(program, state, goal) === true) return false; + return true; +} + +function builtin_call1(program, state) { + const goal = Runtime.deref(state, Runtime.get_reg(state, 1)); + return invoke_goal(program, state, goal, true) === true; +} + +function numeric_value(v) { + if (typeof v === "object" && v !== null && (v.tag === "int" || v.tag === "float")) return v.val; + return null; +} + +function exact_equal(state, a, b) { + 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 !== b.tag) return false; + 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++) { + if (exact_equal(state, a.args[i], b.args[i]) !== true) return false; + } + return true; + } + return false; +} + +function type_builtin(program, state, pred) { + const p = strip_trailing_arity(pred); + const known = { + 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; + const val = Runtime.deref(state, Runtime.get_reg(state, 1)); + 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"; + if (p === "compound") return val.tag === "struct"; + if (p === "var") return val.tag === "unbound"; + if (p === "nonvar") return val.tag !== "unbound"; + if (p === "is_list") return list_items(program, state, val) !== null; + return null; +} + +function is_ground(state, term) { + term = Runtime.deref(state, term); + if (typeof term !== "object" || term === null) return true; + if (term.tag === "unbound") return false; + if (term.tag === "struct") { + const args = term.args || []; + for (let i = 0; i < args.length; i++) { + if (!is_ground(state, args[i])) return false; + } + } + return true; +} + +function term_key(state, v) { + v = Runtime.deref(state, v); + if (typeof v !== "object" || v === null) return String(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)]; + const args = v.args || []; + for (let i = 0; i < args.length; i++) parts.push(term_key(state, args[i])); + return parts.join("|"); + } + return String(v); +} + +function term_ord(state, v) { + v = Runtime.deref(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 === "string") return [3, v.val]; + if (v.tag === "atom") return [4, v.id]; + 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 [5, v.fid, args.length, inner]; + } + return [9, String(v)]; +} + +function cmp_ord(a, b) { + const n = Math.min(a.length, b.length); + for (let i = 0; i < n; i++) { + if (Array.isArray(a[i]) && Array.isArray(b[i])) { + const c = cmp_ord(a[i], b[i]); + if (c !== 0) return c; + } else if (a[i] < b[i]) { + return -1; + } else if (a[i] > b[i]) { + return 1; + } + } + if (a.length < b.length) return -1; + if (a.length > b.length) return 1; + return 0; +} + +aggregate_result = function (kind, items, intern_table) { + kind = kind || "collect"; + if (kind === "count") return V.Int(items.length); + if (kind === "sum") { + let total = 0; + let sawFloat = false; + for (let i = 0; i < items.length; i++) { + const n = numeric_value(items[i]); + if (n !== null) { + total += n; + if (typeof n === "number" && !Number.isInteger(n)) sawFloat = true; + } + } + if (sawFloat) return V.Float(total); + return V.Int(total); + } + if (kind === "min" || kind === "max") { + let best = null; + let bestN = null; + for (let i = 0; i < items.length; i++) { + const n = numeric_value(items[i]); + if (n !== null && (bestN === null + || (kind === "min" && n < bestN) + || (kind === "max" && n > bestN))) { + best = items[i]; + bestN = n; + } + } + return best || Runtime.list_from_terms([], intern_table); + } + if (kind === "set" || kind === "setof") { + return Runtime.list_from_terms( + sort_unique_terms(intern_table, empty_cmp_state(), items), + intern_table); + } + return Runtime.list_from_terms(items, intern_table); +}; + +function empty_cmp_state() { + return { bindings: {} }; +} + +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; + // 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; +} + +// 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) { + a = Runtime.deref(state, a); + b = Runtime.deref(state, b); + const ca = term_class_rank(a); + const cb = term_class_rank(b); + if (ca !== cb) return ca < cb ? -1 : 1; + if (ca === 0) { + if (a.name === b.name) return 0; + return a.name < b.name ? -1 : 1; + } + if (ca === 1) { + if (a.val === b.val) return 0; + 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 === 4) { + const aa = (a.args || []).length; + const ba = (b.args || []).length; + if (aa !== ba) return aa < ba ? -1 : 1; + const fa = Runtime.functor_name(intern, a.fid); + const fb = Runtime.functor_name(intern, b.fid); + if (fa !== fb) return fa < fb ? -1 : 1; + for (let i = 0; i < aa; i++) { + const c = compare_terms(intern, state, a.args[i], b.args[i]); + if (c !== 0) return c; + } + return 0; + } + return 0; +} + +function sort_unique_terms(intern, state, items) { + const copy = items.slice(); + copy.sort(function (x, y) { return compare_terms(intern, state, x, y); }); + const out = []; + for (let i = 0; i < copy.length; i++) { + if (i === 0 || compare_terms(intern, state, copy[i], copy[i - 1]) !== 0) { + out.push(copy[i]); + } + } + return out; +} + +function normalize_agg_entries(collected) { + const out = []; + for (let i = 0; i < collected.length; i++) { + const e = collected[i]; + if (e && typeof e === "object" && Object.prototype.hasOwnProperty.call(e, "template")) { + out.push({ template: e.template, witnesses: e.witnesses || [] }); + } else { + out.push({ template: e, witnesses: [] }); + } + } + return out; +} + +function witness_tuples_equal(intern, state, a, b) { + if (!a || !b || a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (compare_terms(intern, state, a[i], b[i]) !== 0) return false; + } + return true; +} + +// Encounter order of first solution per group (SWI), not sort-by-witness. +function group_by_witness(intern, state, entries, kind) { + const groups = []; + for (let i = 0; i < entries.length; i++) { + const e = entries[i]; + const w = e.witnesses || []; + let found = null; + for (let g = 0; g < groups.length; g++) { + if (witness_tuples_equal(intern, state, groups[g].witnesses, w)) { + found = groups[g]; + break; + } + } + if (found) found.templates.push(e.template); + else groups.push({ witnesses: w, templates: [e.template] }); + } + if (kind === "setof" || kind === "set") { + for (let g = 0; g < groups.length; g++) { + groups[g].templates = sort_unique_terms(intern, state, groups[g].templates); + } + } + return groups; +} + +function apply_witness_group(program, state, cp, group) { + if (!group) return false; + const intern = (program && program.intern_table) || cp.table; + const kind = cp.agg_kind || "bagof"; + const bag = aggregate_result(kind, group.templates, intern); + if (bind_or_compare(state, cp.bag_reg, bag, program) !== true) return false; + const cells = cp.witness_cells; + const regs = cp.witness_regs || []; + const wvals = group.witnesses || []; + if (cells && cells.length > 0) { + for (let i = 0; i < cells.length; i++) { + if (Runtime.unify(state, cells[i], wvals[i], program) !== true) return false; + } + } else { + for (let i = 0; i < regs.length; i++) { + if (bind_or_compare(state, regs[i], wvals[i], program) !== true) return false; + } + } + return true; +} + +function push_remaining_groups(state, cp, rest, intern) { + if (!rest || rest.length === 0) return; + const next = snapshot_machine(state); + next.kind = "agg_next_group"; + next.groups = rest; + next.witness_regs = cp.witness_regs || []; + next.witness_cells = cp.witness_cells; + next.bag_reg = cp.bag_reg; + next.agg_kind = cp.agg_kind; + next.table = intern; + next.next_pc = cp.next_pc; + state.cps.push(next); +} + +function finalize_aggregate(program, state, cp) { + const kind = cp.agg_kind || "collect"; + const intern = (program && program.intern_table) || cp.table; + const cmpState = empty_cmp_state(); + const entries = normalize_agg_entries(cp.collected || []); + const witnessRegs = cp.witness_regs || []; + const grouped = (kind === "bagof" || kind === "setof"); + const hasWitnesses = grouped && witnessRegs.length > 0 && entries.length > 0 + && entries.some(function (e) { return e.witnesses && e.witnesses.length > 0; }); + + if (grouped && entries.length === 0) return Runtime.backtrack(state); + + if (hasWitnesses) { + const groups = group_by_witness(intern, cmpState, entries, kind); + if (groups.length === 0) return Runtime.backtrack(state); + const first = groups[0]; + push_remaining_groups(state, cp, groups.slice(1), intern); + if (apply_witness_group(program, state, cp, first) !== true) { + return Runtime.backtrack(state); + } + state.pc = cp.next_pc; + state.halt = false; + return true; + } + + const templates = []; + for (let i = 0; i < entries.length; i++) templates.push(entries[i].template); + if (grouped && templates.length === 0) return Runtime.backtrack(state); + const bag = aggregate_result(kind, templates, intern); + if (bind_or_compare(state, cp.bag_reg, bag, program) !== true) { + return Runtime.backtrack(state); + } + state.pc = cp.next_pc; + state.halt = false; + return true; +} + +function collect_var_names(state, term, set) { + term = Runtime.deref(state, term); + if (typeof term !== "object" || term === null) return; + if (term.tag === "unbound") { + set[term.name] = true; + return; + } + if (term.tag === "struct") { + const args = term.args || []; + for (let i = 0; i < args.length; i++) collect_var_names(state, args[i], set); + } +} + +function is_caret_functor(program, fid) { + return Runtime.functor_name(program.intern_table, fid) === "^"; +} + +function strip_existentials(program, state, goal) { + const excluded = Object.create(null); + let g = Runtime.deref(state, goal); + while (typeof g === "object" && g !== null && g.tag === "struct" + && is_caret_functor(program, g.fid) && (g.args || []).length >= 2) { + collect_var_names(state, g.args[0], excluded); + g = Runtime.deref(state, g.args[1]); + } + return { goal: g, excluded: excluded }; +} + +function collect_witness_cells(program, state, goal, template, excluded) { + const seen = Object.create(null); + const cells = []; + collect_var_names(state, template, seen); + function walk(term) { + term = Runtime.deref(state, term); + if (typeof term !== "object" || term === null) return; + if (term.tag === "unbound") { + if (seen[term.name] || excluded[term.name]) return; + seen[term.name] = true; + cells.push(term); + return; + } + if (term.tag !== "struct") return; + const fname = Runtime.functor_name(program.intern_table, term.fid); + const args = term.args || []; + if (fname === "^" && args.length >= 2) { + collect_var_names(state, args[0], excluded); + walk(args[1]); + return; + } + if ((fname === "bagof" || fname === "setof" || fname === "findall" + || fname === "aggregate_all") && args.length >= 3) { + collect_var_names(state, args[0], seen); + collect_var_names(state, args[2], seen); + walk(args[1]); + return; + } + for (let i = 0; i < args.length; i++) walk(args[i]); + } + walk(goal); + return cells; +} + +function find_matching_end_aggregate(program, startPc) { + let depth = 1; + for (let pc = startPc + 1; pc < program.instructions.length; pc++) { + const inst = program.instructions[pc]; + if (!inst) continue; + if (inst.op === "BeginAggregate") depth += 1; + else if (inst.op === "EndAggregate") { + depth -= 1; + if (depth === 0) return pc; + } + } + return null; +} + +function is_switch_constant(v) { + return typeof v === "object" && v !== null + && (v.tag === "atom" || v.tag === "int" || v.tag === "float" + || v.tag === "string"); +} + +function switch_values_equal(a, b) { + if (!a || !b || a.tag !== b.tag) { + if (a && b && (a.tag === "int" || a.tag === "float") + && (b.tag === "int" || b.tag === "float")) { + return a.val === b.val; + } + 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; +} + +function switch_jump(program, state, label, pc) { + if (!label || label === "default" || label === "none") { + state.pc += 1; + return true; + } + const dest = (pc !== undefined && pc !== null) ? pc : program.labels[label]; + if (dest === undefined || dest === null) { + state.pc += 1; + return true; + } + state.indexed_entry = true; + state.pc = dest; + return true; +} + +function switch_constant(program, state, inst, reg) { + const v = Runtime.deref(state, Runtime.get_reg(state, reg)); + if (typeof v === "object" && v !== null && v.tag === "unbound") { + state.pc += 1; + return true; + } + if (!is_switch_constant(v)) { + state.pc += 1; + return true; + } + const cases = inst.cases || []; + for (let i = 0; i < cases.length; i++) { + const c = cases[i]; + if (c.value && switch_values_equal(c.value, v)) { + return switch_jump(program, state, c.label, c.pc); + } + } + if (inst.fallthrough === true) { + state.pc += 1; + return true; + } + return false; +} + +function switch_structure(program, state, inst, reg) { + const v = Runtime.deref(state, Runtime.get_reg(state, reg || 1)); + if (typeof v === "object" && v !== null && v.tag === "unbound") { + state.pc += 1; + return true; + } + if (typeof v !== "object" || v === null || v.tag !== "struct") { + state.pc += 1; + return true; + } + const cases = inst.cases || []; + for (let i = 0; i < cases.length; i++) { + const c = cases[i]; + if (same_functor(program, c.fid, c.arity, v.fid, (v.args || []).length)) { + return switch_jump(program, state, c.label, c.pc); + } + } + return false; +} + +function switch_term(program, state, inst, reg) { + const v = Runtime.deref(state, Runtime.get_reg(state, reg || 1)); + if (typeof v === "object" && v !== null && v.tag === "unbound") { + state.pc += 1; + return true; + } + if (is_switch_constant(v)) { + const cases = inst.consts || inst.cases || []; + for (let i = 0; i < cases.length; i++) { + const c = cases[i]; + if (c.value && switch_values_equal(c.value, v)) { + return switch_jump(program, state, c.label, c.pc); + } + } + return false; + } + if (typeof v === "object" && v !== null && v.tag === "struct") { + if (is_cons_fid(program, v.fid)) { + return switch_jump(program, state, inst.listLabel, inst.listPc); + } + const cases = inst.structs || []; + for (let i = 0; i < cases.length; i++) { + const c = cases[i]; + if (same_functor(program, c.fid, c.arity, v.fid, (v.args || []).length)) { + return switch_jump(program, state, c.label, c.pc); + } + } + return false; + } + state.pc += 1; + return true; +} + +function call_key(inst) { + return String(inst.pred) + "/" + String(inst.arity); +} + +function label_for_pred(program, pred, arity) { + const name = strip_trailing_arity(pred); + const key = name + "/" + String(arity); + if (program.labels[key] !== undefined) return program.labels[key]; + if (program.labels[pred] !== undefined) return program.labels[pred]; + if (program.labels[name] !== undefined) return program.labels[name]; + return null; +} + +function resolve_switch_cases(labels, cases) { + const resolved = []; + let any = false; + const list = cases || []; + for (let i = 0; i < list.length; i++) { + const c = list[i]; + const next = Runtime.copy_table(c); + if (!c.label || c.label === "default" || c.label === "none") { + next.pc = null; + resolved.push(next); + any = true; + } else { + const pc = labels[c.label]; + if (pc !== undefined && pc !== null) { + next.pc = pc; + resolved.push(next); + any = true; + } else { + resolved.push(next); + any = true; + } + } + } + return any ? resolved : null; +} + +Runtime.resolve_program = function (program) { + const labels = program.labels || {}; + const resolved = program.instructions.slice(); + for (let i = 1; i < resolved.length; i++) { + const inst0 = resolved[i]; + if (!inst0) continue; + let inst = inst0; + if (inst0.op === "Call") { + const pc = labels[call_key(inst0)]; + if (pc !== undefined) inst = I.CallPc(pc, inst0.arity); + } else if (inst0.op === "Execute") { + const pc = labels[call_key(inst0)]; + if (pc !== undefined) inst = I.ExecutePc(pc); + } else if (inst0.op === "Jump") { + const pc = labels[inst0.label]; + if (pc !== undefined) inst = I.JumpPc(pc); + } else if (inst0.op === "TryMeElse") { + const pc = labels[inst0.label]; + if (pc !== undefined) inst = I.TryMeElsePc(pc); + } else if (inst0.op === "RetryMeElse") { + const pc = labels[inst0.label]; + if (pc !== undefined) inst = I.RetryMeElsePc(pc); + } else if (inst0.op === "SwitchOnConstant") { + const cases = resolve_switch_cases(labels, inst0.cases); + if (cases !== null) inst = I.SwitchOnConstantPc(cases, inst0.fallthrough); + } else if (inst0.op === "SwitchOnConstantA2") { + const cases = resolve_switch_cases(labels, inst0.cases); + if (cases !== null) inst = I.SwitchOnConstantA2Pc(cases, inst0.fallthrough); + } else if (inst0.op === "SwitchOnStructure") { + const cases = resolve_switch_cases(labels, inst0.cases); + if (cases !== null) inst = I.SwitchOnStructurePc(cases); + } else if (inst0.op === "SwitchOnStructureA2") { + const cases = resolve_switch_cases(labels, inst0.cases); + if (cases !== null) inst = I.SwitchOnStructureA2Pc(cases); + } else if (inst0.op === "SwitchOnTerm") { + const consts = resolve_switch_cases(labels, inst0.consts); + const structs = resolve_switch_cases(labels, inst0.structs); + const listPc = inst0.listLabel && inst0.listLabel !== "default" + && inst0.listLabel !== "none" ? labels[inst0.listLabel] : null; + inst = I.SwitchOnTermPc(consts || inst0.consts, structs || inst0.structs, + inst0.listLabel, listPc, inst0.reg); + } else if (inst0.op === "Try") { + const pc = labels[inst0.label]; + if (pc !== undefined) inst = I.TryPc(pc); + } else if (inst0.op === "Retry") { + const pc = labels[inst0.label]; + if (pc !== undefined) inst = I.RetryPc(pc); + } else if (inst0.op === "Trust") { + const pc = labels[inst0.label]; + if (pc !== undefined) inst = I.TrustPc(pc); + } + resolved[i] = inst; + } + program.instructions = resolved; + program.resolved = true; + return program; +}; + +function as_arith_result(n) { + if (typeof n !== "number" || !Number.isFinite(n)) return null; + if (Number.isInteger(n)) return V.Int(n); + return V.Float(n); +} + +function eval_arith(program, state, term) { + term = Runtime.deref(state, term); + if (typeof term !== "object" || term === null) { + if (typeof term === "number") return term; + return null; + } + if (term.tag === "int" || term.tag === "float") return term.val; + if (term.tag === "atom") { + const s = Runtime.string_of(program.intern_table, term.id); + if (s === "pi") return Math.PI; + if (s === "e") return Math.E; + const n = Number(s); + if (s !== "" && Number.isFinite(n)) return n; + return null; + } + if (term.tag === "unbound") return null; + if (term.tag !== "struct") return null; + const op = strip_trailing_arity(Runtime.string_of(program.intern_table, term.fid)); + const args = term.args || []; + const vals = []; + for (let i = 0; i < args.length; i++) { + const v = eval_arith(program, state, args[i]); + if (v === null) return null; + vals.push(v); + } + if (vals.length === 1) { + if (op === "-") return -vals[0]; + if (op === "+") return +vals[0]; + if (op === "abs") return Math.abs(vals[0]); + return null; + } + if (vals.length === 2) { + if (op === "+") return vals[0] + vals[1]; + if (op === "-") return vals[0] - vals[1]; + if (op === "*") return vals[0] * vals[1]; + if (op === "/") return vals[0] / vals[1]; + if (op === "//") return Math.trunc(vals[0] / vals[1]); + if (op === "mod" || op === "rem") return vals[0] % vals[1]; + if (op === "div") return Math.trunc(vals[0] / vals[1]); + if (op === "**" || op === "^") return Math.pow(vals[0], vals[1]); + return null; + } + return null; +} + +bind_or_compare = function (state, idx, val, program) { + const cur = Runtime.get_reg(state, idx); + if (cur === undefined || cur === null) { + Runtime.put_reg(state, idx, val); + return true; + } + return Runtime.unify(state, cur, val, program); +}; + +function pred_name(p) { + return strip_trailing_arity(String(p)); +} + +function pred_arity(inst) { + if (inst && typeof inst.arity === "number") return inst.arity; + const p = String(inst && inst.pred || ""); + const slash = p.lastIndexOf("/"); + if (slash >= 0) { + const n = Number(p.slice(slash + 1)); + if (Number.isInteger(n)) return n; + } + return null; +} + +function is_unbound_term(v) { + return typeof v === "object" && v !== null && v.tag === "unbound"; +} + +function intern_atom(program, text) { + return V.Atom(Runtime.intern(program.intern_table, String(text))); +} + +function pair_term(program, key, val) { + const fid = Runtime.intern(program.intern_table, "-"); + return V.Struct(fid, [key, val]); +} + +function hyphen_pair(program, state, term) { + term = Runtime.deref(state, term); + if (typeof term !== "object" || term === null || term.tag !== "struct") return null; + if ((term.args || []).length !== 2) return null; + const name = Runtime.functor_name(program.intern_table, term.fid); + if (name !== "-") return null; + return { key: term.args[0], val: term.args[1] }; +} + +function atomic_text(program, state, term) { + term = Runtime.deref(state, term); + if (typeof term !== "object" || term === null) return null; + if (term.tag === "atom") return Runtime.string_of(program.intern_table, term.id); + if (term.tag === "string") return String(term.val); + if (term.tag === "int" || term.tag === "float") return String(term.val); + return null; +} + +function codes_to_string(program, state, items) { + let out = ""; + for (let i = 0; i < items.length; i++) { + const c = Runtime.deref(state, items[i]); + if (typeof c !== "object" || c === null || c.tag !== "int") return null; + out += String.fromCharCode(c.val); + } + return out; +} + +function string_to_codes(text) { + const items = []; + for (let i = 0; i < text.length; i++) items.push(V.Int(text.charCodeAt(i))); + return items; +} + +function string_to_chars(program, text) { + const items = []; + for (let i = 0; i < text.length; i++) items.push(intern_atom(program, text.charAt(i))); + return items; +} + +function chars_to_string(program, state, items) { + let out = ""; + for (let i = 0; i < items.length; i++) { + const t = atomic_text(program, state, items[i]); + if (t === null || t.length !== 1) return null; + out += t; + } + return out; +} + +function apply_enum_sol(program, state, sol) { + for (let i = 0; i < sol.length; i++) { + const spec = sol[i]; + if (Runtime.unify(state, Runtime.get_reg(state, spec[0]), spec[1], program) !== true) { + return false; + } + } + return true; +} + +function dispatch_enum(program, state, sols) { + if (!sols || sols.length === 0) return false; + const rest = sols.slice(1); + if (rest.length > 0) { + state.cps.push(snapshot_cp(state, { kind: "enum_unify", sols: rest })); + } + return apply_enum_sol(program, state, sols[0]); +} + +function bind_list(program, state, reg, items) { + return bind_or_compare(state, reg, Runtime.list_from_terms(items, program.intern_table), program); +} + +function builtin_compare3(program, state) { + const c = compare_terms(program.intern_table, state, + Runtime.get_reg(state, 2), Runtime.get_reg(state, 3)); + let atom = "="; + if (c < 0) atom = "<"; + else if (c > 0) atom = ">"; + return bind_or_compare(state, 1, intern_atom(program, atom), program); +} + +function sort_items(program, state, items, dedup) { + const copy = items.slice(); + copy.sort(function (x, y) { + return compare_terms(program.intern_table, state, x, y); + }); + if (!dedup) return copy; + const out = []; + for (let i = 0; i < copy.length; i++) { + if (i === 0 || compare_terms(program.intern_table, state, copy[i], copy[i - 1]) !== 0) { + out.push(copy[i]); + } + } + return out; +} + +function sort_key_of(program, state, term, key) { + if (key === 0) return term; + term = Runtime.deref(state, term); + if (typeof term !== "object" || term === null || term.tag !== "struct") return term; + const arg = (term.args || [])[key - 1]; + return arg === undefined ? term : arg; +} + +function builtin_sort_family(program, state, name) { + if (name === "sort" || name === "msort") { + const items = list_items(program, state, Runtime.get_reg(state, 1)); + if (items === null) return false; + return bind_list(program, state, 2, sort_items(program, state, items, name === "sort")); + } + if (name === "keysort") { + const items = list_items(program, state, Runtime.get_reg(state, 1)); + if (items === null) return false; + const keyed = []; + for (let i = 0; i < items.length; i++) { + const p = hyphen_pair(program, state, items[i]); + if (!p) return false; + keyed.push({ key: p.key, term: items[i], i: i }); + } + keyed.sort(function (a, b) { + const c = compare_terms(program.intern_table, state, a.key, b.key); + if (c !== 0) return c; + return a.i - b.i; + }); + const out = []; + for (let i = 0; i < keyed.length; i++) out.push(keyed[i].term); + return bind_list(program, state, 2, out); + } + return false; +} + +function builtin_sort4(program, state) { + const keyTerm = Runtime.deref(state, Runtime.get_reg(state, 1)); + const orderTerm = Runtime.deref(state, Runtime.get_reg(state, 2)); + const items = list_items(program, state, Runtime.get_reg(state, 3)); + if (items === null) return false; + if (typeof keyTerm !== "object" || keyTerm === null || keyTerm.tag !== "int") return false; + const key = keyTerm.val; + const orderName = atomic_text(program, state, orderTerm); + if (orderName === null) return false; + const desc = orderName === "@>" || orderName === ">"; + const unique = orderName === "@<" || orderName === "@>"; + const keyed = []; + for (let i = 0; i < items.length; i++) { + keyed.push({ key: sort_key_of(program, state, items[i], key), term: items[i], i: i }); + } + keyed.sort(function (a, b) { + const c = compare_terms(program.intern_table, state, a.key, b.key); + if (c !== 0) return desc ? -c : c; + return a.i - b.i; + }); + let out = []; + for (let i = 0; i < keyed.length; i++) out.push(keyed[i].term); + if (unique) { + const uniq = []; + for (let i = 0; i < out.length; i++) { + if (i === 0 || compare_terms(program.intern_table, state, + sort_key_of(program, state, out[i], key), + sort_key_of(program, state, out[i - 1], key)) !== 0) { + uniq.push(out[i]); + } + } + out = uniq; + } + return bind_list(program, state, 4, out); +} + +function builtin_predsort(program, state) { + const pred = Runtime.deref(state, Runtime.get_reg(state, 1)); + const items = list_items(program, state, Runtime.get_reg(state, 2)); + if (items === null) return false; + const predName = pred && pred.tag === "atom" + ? strip_trailing_arity(Runtime.string_of(program.intern_table, pred.id)) + : null; + const copy = items.slice(); + copy.sort(function (x, y) { + if (predName === "compare") { + return compare_terms(program.intern_table, state, x, y); + } + const fid = pred.tag === "atom" ? pred.id : (pred.tag === "struct" ? pred.fid : null); + if (fid === null) return 0; + const order = Runtime.new_var(state); + const goal = V.Struct(fid, [order, x, y]); + if (invoke_goal(program, state, goal, true) !== true) return 0; + const o = atomic_text(program, state, order); + if (o === "<") return -1; + if (o === ">") return 1; + return 0; + }); + return bind_list(program, state, 3, copy); +} + +function builtin_append(program, state) { + const a = list_items(program, state, Runtime.get_reg(state, 1)); + const b = list_items(program, state, Runtime.get_reg(state, 2)); + const c = list_items(program, state, Runtime.get_reg(state, 3)); + if (a !== null && b !== null) { + return bind_list(program, state, 3, a.concat(b)); + } + if (c === null) return false; + if (a !== null) { + if (c.length < a.length) return false; + for (let i = 0; i < a.length; i++) { + if (Runtime.unify(state, a[i], c[i], program) !== true) return false; + } + return bind_list(program, state, 2, c.slice(a.length)); + } + if (b !== null) { + if (c.length < b.length) return false; + const prefixLen = c.length - b.length; + const suffix = c.slice(prefixLen); + for (let i = 0; i < b.length; i++) { + if (Runtime.unify(state, b[i], suffix[i], program) !== true) return false; + } + return bind_list(program, state, 1, c.slice(0, prefixLen)); + } + const sols = []; + for (let i = 0; i <= c.length; i++) { + sols.push([ + [1, Runtime.list_from_terms(c.slice(0, i), program.intern_table)], + [2, Runtime.list_from_terms(c.slice(i), program.intern_table)] + ]); + } + return dispatch_enum(program, state, sols); +} + +function builtin_reverse(program, state) { + const a = list_items(program, state, Runtime.get_reg(state, 1)); + if (a !== null) return bind_list(program, state, 2, a.slice().reverse()); + const b = list_items(program, state, Runtime.get_reg(state, 2)); + if (b === null) return false; + return bind_list(program, state, 1, b.slice().reverse()); +} + +function builtin_nth(program, state, oneBased) { + const nTerm = Runtime.deref(state, Runtime.get_reg(state, 1)); + const items = list_items(program, state, Runtime.get_reg(state, 2)); + if (items === null) return false; + if (typeof nTerm === "object" && nTerm !== null && nTerm.tag === "int") { + const idx = oneBased ? nTerm.val - 1 : nTerm.val; + if (idx < 0 || idx >= items.length) return false; + return bind_or_compare(state, 3, items[idx], program); + } + if (!is_unbound_term(nTerm)) return false; + const sols = []; + for (let i = 0; i < items.length; i++) { + const n = oneBased ? i + 1 : i; + sols.push([[1, V.Int(n)], [3, items[i]]]); + } + return dispatch_enum(program, state, sols); +} + +function builtin_last(program, state) { + const items = list_items(program, state, Runtime.get_reg(state, 1)); + if (items === null || items.length === 0) return false; + return bind_or_compare(state, 2, items[items.length - 1], program); +} + +function builtin_sum_list(program, state) { + const items = list_items(program, state, Runtime.get_reg(state, 1)); + if (items === null) return false; + let total = 0; + let sawFloat = false; + for (let i = 0; i < items.length; i++) { + const n = numeric_value(Runtime.deref(state, items[i])); + if (n === null) return false; + total += n; + if (typeof n === "number" && !Number.isInteger(n)) sawFloat = true; + } + const boxed = sawFloat ? V.Float(total) : V.Int(total); + return bind_or_compare(state, 2, boxed, program); +} + +function builtin_minmax_list(program, state, wantMax) { + const items = list_items(program, state, Runtime.get_reg(state, 1)); + if (items === null || items.length === 0) return false; + let best = items[0]; + let bestN = numeric_value(Runtime.deref(state, best)); + if (bestN === null) return false; + for (let i = 1; i < items.length; i++) { + const n = numeric_value(Runtime.deref(state, items[i])); + if (n === null) return false; + if ((wantMax && n > bestN) || (!wantMax && n < bestN)) { + best = items[i]; + bestN = n; + } + } + return bind_or_compare(state, 2, best, program); +} + +function builtin_list_to_set(program, state) { + const items = list_items(program, state, Runtime.get_reg(state, 1)); + if (items === null) return false; + const out = []; + for (let i = 0; i < items.length; i++) { + let seen = false; + for (let j = 0; j < out.length; j++) { + if (compare_terms(program.intern_table, state, items[i], out[j]) === 0) { + seen = true; + break; + } + } + if (!seen) out.push(items[i]); + } + return bind_list(program, state, 2, out); +} + +function builtin_select(program, state) { + const items = list_items(program, state, Runtime.get_reg(state, 2)); + if (items === null || items.length === 0) return false; + const x = Runtime.get_reg(state, 1); + const sols = []; + for (let i = 0; i < items.length; i++) { + const rest = items.slice(0, i).concat(items.slice(i + 1)); + sols.push([ + [1, items[i]], + [3, Runtime.list_from_terms(rest, program.intern_table)] + ]); + } + const xd = Runtime.deref(state, x); + if (!is_unbound_term(xd)) { + const filtered = []; + for (let i = 0; i < sols.length; i++) { + const probe = Runtime.new_state(); + probe.bindings = Runtime.copy_table(state.bindings); + probe.trail = state.trail.slice(); + probe.var_counter = state.var_counter; + Runtime.put_reg(probe, 1, x); + if (Runtime.unify(probe, x, sols[i][0][1], program) === true) filtered.push(sols[i]); + } + return dispatch_enum(program, state, filtered); + } + return dispatch_enum(program, state, sols); +} + +function pred1_goal(program, predTerm, arg) { + predTerm = predTerm; + if (typeof predTerm !== "object" || predTerm === null) return null; + if (predTerm.tag === "atom") { + return V.Struct(predTerm.id, [arg]); + } + if (predTerm.tag === "struct") { + const args = (predTerm.args || []).concat([arg]); + return V.Struct(predTerm.fid, args); + } + return null; +} + +function builtin_include_exclude(program, state, keep) { + const pred = Runtime.deref(state, Runtime.get_reg(state, 1)); + const items = list_items(program, state, Runtime.get_reg(state, 2)); + if (items === null) return false; + const out = []; + for (let i = 0; i < items.length; i++) { + const goal = pred1_goal(program, pred, items[i]); + if (!goal) return false; + const ok = goal_succeeds(program, state, goal) === true; + if (ok === keep) out.push(items[i]); + } + return bind_list(program, state, 3, out); +} + +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, 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, 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, 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, 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; + return bind_or_compare(state, 2, V.Int(t.length), program); +} + +function text_char_length(s) { + let n = 0; + for (const _ch of String(s)) n++; + return n; +} + +function builtin_string_length(program, state) { + const t = atomic_text(program, state, Runtime.get_reg(state, 1)); + if (t === null) return false; + return bind_or_compare(state, 2, V.Int(text_char_length(t)), program); +} + +function builtin_atom_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, 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) { + return bind_list(program, state, 2, string_to_codes(t)); + } + const items = list_items(program, state, Runtime.get_reg(state, 2)); + if (items === null) return false; + const s = codes_to_string(program, state, items); + if (s === null) return false; + return bind_or_compare(state, 1, intern_atom(program, s), program); +} + +function builtin_char_code(program, state) { + const ch = atomic_text(program, state, Runtime.get_reg(state, 1)); + const code = Runtime.deref(state, Runtime.get_reg(state, 2)); + if (ch !== null) { + if (ch.length !== 1) return false; + return bind_or_compare(state, 2, V.Int(ch.charCodeAt(0)), program); + } + if (typeof code === "object" && code !== null && code.tag === "int") { + return bind_or_compare(state, 1, intern_atom(program, String.fromCharCode(code.val)), program); + } + return false; +} + +function builtin_sub_atom(program, state) { + const atom = atomic_text(program, state, Runtime.get_reg(state, 1)); + if (atom === null) return false; + const n = atom.length; + const before = Runtime.deref(state, Runtime.get_reg(state, 2)); + const length = Runtime.deref(state, Runtime.get_reg(state, 3)); + const after = Runtime.deref(state, Runtime.get_reg(state, 4)); + const sub = atomic_text(program, state, Runtime.get_reg(state, 5)); + function asInt(v) { + if (typeof v === "object" && v !== null && v.tag === "int") return v.val; + return null; + } + const b0 = asInt(before); + const l0 = asInt(length); + const a0 = asInt(after); + const sols = []; + const bLo = b0 === null ? 0 : b0; + const bHi = b0 === null ? n : b0; + for (let b = bLo; b <= bHi; b++) { + if (b < 0 || b > n) continue; + const lLo = l0 === null ? 0 : l0; + const lHi = l0 === null ? (n - b) : l0; + for (let l = lLo; l <= lHi; l++) { + if (l < 0 || b + l > n) continue; + const a = n - b - l; + if (a0 !== null && a !== a0) continue; + const piece = atom.slice(b, b + l); + if (sub !== null && piece !== sub) continue; + sols.push([ + [2, V.Int(b)], + [3, V.Int(l)], + [4, V.Int(a)], + [5, intern_atom(program, piece)] + ]); + } + } + return dispatch_enum(program, state, sols); +} + +function builtin_sub_string(program, state) { + const text = atomic_text(program, state, Runtime.get_reg(state, 1)); + if (text === null) return false; + const n = text_char_length(text); + const before = Runtime.deref(state, Runtime.get_reg(state, 2)); + const length = Runtime.deref(state, Runtime.get_reg(state, 3)); + const after = Runtime.deref(state, Runtime.get_reg(state, 4)); + const sub = atomic_text(program, state, Runtime.get_reg(state, 5)); + function asInt(v) { + if (typeof v === "object" && v !== null && v.tag === "int") return v.val; + return null; + } + function sliceChars(s, start, len) { + let out = ""; + let i = 0; + for (const ch of String(s)) { + if (i >= start && i < start + len) out += ch; + i++; + if (i >= start + len) break; + } + return out; + } + const b0 = asInt(before); + const l0 = asInt(length); + const a0 = asInt(after); + const sols = []; + const bLo = b0 === null ? 0 : b0; + const bHi = b0 === null ? n : b0; + for (let b = bLo; b <= bHi; b++) { + if (b < 0 || b > n) continue; + const lLo = l0 === null ? 0 : l0; + const lHi = l0 === null ? (n - b) : l0; + for (let l = lLo; l <= lHi; l++) { + if (l < 0 || b + l > n) continue; + const a = n - b - l; + if (a0 !== null && a !== a0) continue; + const piece = sliceChars(text, b, l); + if (sub !== null && piece !== sub) continue; + sols.push([ + [2, V.Int(b)], + [3, V.Int(l)], + [4, V.Int(a)], + [5, V.String(piece)] + ]); + } + } + return dispatch_enum(program, state, sols); +} + +function builtin_atom_string(program, state) { + 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; +} + +function builtin_number_codes(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_list(program, state, 2, string_to_codes(String(n.val))); + } + const items = list_items(program, state, Runtime.get_reg(state, 2)); + if (items === null) return false; + const s = codes_to_string(program, state, items); + if (s === null) return false; + const num = Number(s); + if (s === "" || Number.isNaN(num)) return false; + const boxed = Number.isInteger(num) && String(num) === s ? V.Int(num) : ( + /[.eE]/.test(s) ? V.Float(num) : (Number.isInteger(num) ? V.Int(num) : V.Float(num)) + ); + return bind_or_compare(state, 1, boxed, program); +} + +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, V.String(String(n.val)), program); + } + const s = atomic_text(program, state, Runtime.get_reg(state, 2)); + if (s === null) return false; + const num = Number(s); + if (s === "" || Number.isNaN(num)) return false; + const boxed = Number.isInteger(num) && String(num) === s ? V.Int(num) : ( + /[.eE]/.test(s) ? V.Float(num) : (Number.isInteger(num) ? V.Int(num) : V.Float(num)) + ); + return bind_or_compare(state, 1, boxed, program); +} + +function builtin_split_string(program, state) { + const src = atomic_text(program, state, Runtime.get_reg(state, 1)); + const sep = atomic_text(program, state, Runtime.get_reg(state, 2)); + const pad = atomic_text(program, state, Runtime.get_reg(state, 3)); + if (src === null || sep === null || pad === null) return false; + function trimPad(s) { + let lo = 0; + let hi = s.length; + while (lo < hi && pad.indexOf(s.charAt(lo)) >= 0) lo++; + while (hi > lo && pad.indexOf(s.charAt(hi - 1)) >= 0) hi--; + return s.slice(lo, hi); + } + let parts; + if (sep === "") { + parts = [trimPad(src)]; + } else { + parts = []; + let buf = ""; + for (let i = 0; i < src.length; i++) { + const ch = src.charAt(i); + if (sep.indexOf(ch) >= 0) { + parts.push(trimPad(buf)); + buf = ""; + } else { + buf += ch; + } + } + parts.push(trimPad(buf)); + } + 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) { + const t = atomic_text(program, state, Runtime.get_reg(state, 1)); + if (t === null) return false; + const out = up ? t.toUpperCase() : t.toLowerCase(); + return bind_or_compare(state, 2, intern_atom(program, out), program); +} + +// SWI-like needs-quoting (pragmatic subset): +// unquoted: [] ! ; {} ; a-z[A-Za-z0-9_]* ; ISO graphic +// (# $ & * + - . / : < = > ? @ ^ ~ \) except the lone atom '.' +// (end-of-term). Quoted otherwise (empty, spaces, uppercase, digits, +// '|', ',', mixed graphic+letter). Escapes \\ and \' only. +function atom_needs_quote(s) { + s = String(s); + if (s === "[]" || s === "!" || s === ";" || s === "{}") return false; + if (/^[a-z][a-zA-Z0-9_]*$/.test(s)) return false; + if (s !== "." && /^[#\$&*+\-./:<=>?@^~\\]+$/.test(s)) return false; + return true; +} + +function quote_atom_text(s) { + s = String(s); + if (!atom_needs_quote(s)) return s; + return "'" + s.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'"; +} + +function quote_string_text(s) { + return "\"" + String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\""; +} + +function quoted_list_to_string(program, state, term, nilId) { + const parts = []; + let cur = term; + while (typeof cur === "object" && cur !== null + && cur.tag === "struct" + && is_cons_fid(program, cur.fid) + && (cur.args || []).length === 2) { + parts.push(quoted_term_to_string(program, state, cur.args[0])); + cur = Runtime.deref(state, cur.args[1]); + } + if (typeof cur === "object" && cur !== null + && cur.tag === "atom" && cur.id === nilId) { + return "[" + parts.join(",") + "]"; + } + return "[" + parts.join(",") + "|" + + quoted_term_to_string(program, state, cur) + "]"; +} + +function quoted_term_to_string(program, state, term) { + term = Runtime.deref(state, term); + if (typeof term !== "object" || term === null) return String(term); + if (term.tag === "string") return quote_string_text(term.val); + if (term.tag === "int" || term.tag === "float") return String(term.val); + if (term.tag === "unbound") return "_" + String(term.name); + if (term.tag === "atom") { + return quote_atom_text(Runtime.string_of(program.intern_table, term.id)); + } + if (term.tag === "struct") { + const nilId = Runtime.intern(program.intern_table, "[]"); + if (is_cons_fid(program, term.fid) && (term.args || []).length === 2) { + return quoted_list_to_string(program, state, term, nilId); + } + const fname = Runtime.functor_name(program.intern_table, term.fid); + const args = term.args || []; + const parts = []; + for (let i = 0; i < args.length; i++) { + parts.push(quoted_term_to_string(program, state, args[i])); + } + return quote_atom_text(fname) + "(" + parts.join(",") + ")"; + } + return String(term); +} + +function format_write_term(program, state, term, quoted) { + if (quoted) return quoted_term_to_string(program, state, term); + return term_to_string(program, state, term); +} + +function format_render(program, state, fmtText, args) { + let out = ""; + let ai = 0; + for (let i = 0; i < fmtText.length; i++) { + const ch = fmtText.charAt(i); + if (ch !== "~") { + out += ch; + continue; + } + i++; + if (i >= fmtText.length) break; + const d = fmtText.charAt(i); + if (d === "~") { + out += "~"; + } else if (d === "n") { + out += "\n"; + } else if (d === "t") { + out += "\t"; + } else if (d === "w" || d === "p") { + if (ai >= args.length) return null; + out += format_write_term(program, state, args[ai++], false); + } else if (d === "q") { + if (ai >= args.length) return null; + out += format_write_term(program, state, args[ai++], true); + } else if (d === "a") { + if (ai >= args.length) return null; + const t = atomic_text(program, state, args[ai++]); + if (t === null) return null; + out += t; + } else if (d === "d") { + if (ai >= args.length) return null; + const n = Runtime.deref(state, args[ai++]); + if (!n || (n.tag !== "int" && n.tag !== "float")) return null; + out += String(n.tag === "int" ? (n.val | 0) : n.val); + } else if (d === "s") { + if (ai >= args.length) return null; + const txt = atomic_text(program, state, args[ai]); + if (txt !== null) { + out += txt; + ai++; + } else { + const codes = list_items(program, state, args[ai++]); + if (codes === null) return null; + const s = codes_to_string(program, state, codes); + if (s === null) return null; + out += s; + } + } else { + return null; + } + } + return out; +} + +function format_fmt_text(program, state, fmtTerm) { + const t = atomic_text(program, state, fmtTerm); + if (t !== null) return t; + const codes = list_items(program, state, fmtTerm); + if (codes === null) return null; + return codes_to_string(program, state, codes); +} + +function builtin_format2(program, state) { + const fmt = format_fmt_text(program, state, Runtime.get_reg(state, 1)); + const args = list_items(program, state, Runtime.get_reg(state, 2)); + if (fmt === null || args === null) return false; + const text = format_render(program, state, fmt, args); + if (text === null) return false; + process.stdout.write(text); + return true; +} + +function builtin_format3(program, state) { + const sink = Runtime.deref(state, Runtime.get_reg(state, 1)); + const fmt = format_fmt_text(program, state, Runtime.get_reg(state, 2)); + const args = list_items(program, state, Runtime.get_reg(state, 3)); + if (fmt === null || args === null) return false; + const text = format_render(program, state, fmt, args); + if (text === null) return false; + const boxed = intern_atom(program, text); + if (typeof sink === "object" && sink !== null && sink.tag === "struct" + && (sink.args || []).length === 1) { + const fname = Runtime.functor_name(program.intern_table, sink.fid); + if (fname === "atom" || fname === "string") { + return Runtime.unify(state, sink.args[0], boxed, program); + } + } + if (is_unbound_term(sink) || (sink && sink.tag === "atom")) { + return bind_or_compare(state, 1, boxed, program); + } + return false; +} + +function builtin_tab(program, state) { + const n = Runtime.deref(state, Runtime.get_reg(state, 1)); + if (typeof n !== "object" || n === null || n.tag !== "int" || n.val < 0) return false; + let s = ""; + for (let i = 0; i < n.val; i++) s += " "; + process.stdout.write(s); + return true; +} + +function assoc_pairs(program, state, assoc) { + assoc = Runtime.deref(state, assoc); + if (typeof assoc !== "object" || assoc === null || assoc.tag !== "struct") return null; + if (Runtime.functor_name(program.intern_table, assoc.fid) !== "assoc") return null; + if ((assoc.args || []).length !== 1) return null; + const items = list_items(program, state, assoc.args[0]); + if (items === null) return null; + const pairs = []; + for (let i = 0; i < items.length; i++) { + const p = hyphen_pair(program, state, items[i]); + if (!p) return null; + pairs.push(p); + } + return pairs; +} + +function assoc_term(program, pairs) { + const items = []; + for (let i = 0; i < pairs.length; i++) { + items.push(pair_term(program, pairs[i].key, pairs[i].val)); + } + const fid = Runtime.intern(program.intern_table, "assoc"); + return V.Struct(fid, [Runtime.list_from_terms(items, program.intern_table)]); +} + +function builtin_empty_assoc(program, state) { + return bind_or_compare(state, 1, assoc_term(program, []), program); +} + +function builtin_list_to_assoc(program, state) { + const items = list_items(program, state, Runtime.get_reg(state, 1)); + if (items === null) return false; + const pairs = []; + for (let i = 0; i < items.length; i++) { + const p = hyphen_pair(program, state, items[i]); + if (!p) return false; + pairs.push(p); + } + pairs.sort(function (a, b) { + return compare_terms(program.intern_table, state, a.key, b.key); + }); + for (let i = 1; i < pairs.length; i++) { + if (compare_terms(program.intern_table, state, pairs[i].key, pairs[i - 1].key) === 0) { + return false; + } + } + return bind_or_compare(state, 2, assoc_term(program, pairs), program); +} + +function builtin_get_assoc(program, state) { + const pairs = assoc_pairs(program, state, Runtime.get_reg(state, 2)); + if (pairs === null) return false; + const key = Runtime.deref(state, Runtime.get_reg(state, 1)); + if (is_unbound_term(key)) { + const sols = []; + for (let i = 0; i < pairs.length; i++) { + sols.push([[1, pairs[i].key], [3, pairs[i].val]]); + } + return dispatch_enum(program, state, sols); + } + for (let i = 0; i < pairs.length; i++) { + if (compare_terms(program.intern_table, state, key, pairs[i].key) === 0) { + return bind_or_compare(state, 3, pairs[i].val, program); + } + } + return false; +} + +function builtin_put_assoc(program, state) { + const key = Runtime.get_reg(state, 1); + const pairs0 = assoc_pairs(program, state, Runtime.get_reg(state, 2)); + const val = Runtime.get_reg(state, 3); + if (pairs0 === null) return false; + const pairs = []; + let replaced = false; + for (let i = 0; i < pairs0.length; i++) { + const c = compare_terms(program.intern_table, state, key, pairs0[i].key); + if (!replaced && c === 0) { + pairs.push({ key: key, val: val }); + replaced = true; + } else if (!replaced && c < 0) { + pairs.push({ key: key, val: val }); + pairs.push(pairs0[i]); + replaced = true; + } else { + pairs.push(pairs0[i]); + } + } + if (!replaced) pairs.push({ key: key, val: val }); + return bind_or_compare(state, 4, assoc_term(program, pairs), program); +} + +function builtin_assoc_to_list(program, state) { + const pairs = assoc_pairs(program, state, Runtime.get_reg(state, 1)); + if (pairs === null) return false; + const items = []; + for (let i = 0; i < pairs.length; i++) { + items.push(pair_term(program, pairs[i].key, pairs[i].val)); + } + return bind_list(program, state, 2, items); +} + +function builtin_assoc_to_keys(program, state) { + const pairs = assoc_pairs(program, state, Runtime.get_reg(state, 1)); + if (pairs === null) return false; + const items = []; + for (let i = 0; i < pairs.length; i++) items.push(pairs[i].key); + return bind_list(program, state, 2, items); +} + +function snapshot_cp(state, extra) { + const cp = snapshot_machine(state); + cp.next_pc = state.pc + 1; + if (extra) { + const keys = Object.keys(extra); + for (let i = 0; i < keys.length; i++) cp[keys[i]] = extra[keys[i]]; + } + return cp; +} + +function builtin_findall_family(program, state, pred) { + const name = pred_name(pred); + const template = Runtime.get_reg(state, 1); + const rawGoal = Runtime.deref(state, Runtime.get_reg(state, 2)); + let kind = "collect"; + let collectTemplate = template; + if (name === "aggregate_all") { + const spec = Runtime.deref(state, template); + if (typeof spec === "object" && spec !== null && spec.tag === "atom") { + const specName = Runtime.string_of(program.intern_table, spec.id); + if (specName === "count") { + kind = "count"; + collectTemplate = V.Int(1); + } else { + kind = specName; + } + } else if (typeof spec === "object" && spec !== null && spec.tag === "struct") { + kind = pred_name(Runtime.string_of(program.intern_table, spec.fid)); + collectTemplate = (spec.args || [])[0]; + } + } else if (name === "bagof") { + kind = "bagof"; + } else if (name === "setof") { + kind = "setof"; + } + const stripped = strip_existentials(program, state, rawGoal); + const goal = stripped.goal; + let witnessCells = []; + if (name === "bagof" || name === "setof") { + witnessCells = collect_witness_cells( + program, state, goal, collectTemplate, stripped.excluded); + } + const rows = collect_goal_rows(program, state, goal, collectTemplate, witnessCells); + if ((name === "bagof" || name === "setof") && rows.length === 0) return false; + if ((name === "bagof" || name === "setof") && witnessCells.length > 0) { + const groups = group_by_witness( + program.intern_table, empty_cmp_state(), rows, kind); + if (groups.length === 0) return false; + const first = groups[0]; + const rest = groups.slice(1); + if (rest.length > 0) { + state.cps.push(snapshot_cp(state, { + kind: "agg_next_group", + groups: rest, + witness_cells: witnessCells, + bag_reg: 3, + agg_kind: kind, + table: program.intern_table + })); + } + return apply_witness_group(program, state, { + bag_reg: 3, + witness_cells: witnessCells, + agg_kind: kind, + table: program.intern_table + }, first); + } + const items = []; + for (let i = 0; i < rows.length; i++) items.push(rows[i].template); + const bag = aggregate_result(kind, items, program.intern_table); + return bind_or_compare(state, 3, bag, program); +} + +function builtin_between(program, state) { + const lo = Runtime.deref(state, Runtime.get_reg(state, 1)); + const hi = Runtime.deref(state, Runtime.get_reg(state, 2)); + if (typeof lo !== "object" || lo === null || lo.tag !== "int") return false; + if (typeof hi !== "object" || hi === null || hi.tag !== "int") return false; + if (lo.val > hi.val) return false; + const x = Runtime.deref(state, Runtime.get_reg(state, 3)); + if (typeof x === "object" && x !== null && x.tag === "int") { + return x.val >= lo.val && x.val <= hi.val; + } + if (lo.val < hi.val) { + state.cps.push(snapshot_cp(state, { kind: "between", next: lo.val + 1, high: hi.val })); + } + 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); +} + +function op_resolve_names(program, state, nameV) { + nameV = Runtime.deref(state, nameV); + if (typeof nameV !== "object" || nameV === null) return null; + if (nameV.tag === "atom") { + return [Runtime.string_of(program.intern_table, nameV.id)]; + } + const items = list_items(program, state, nameV); + if (items === null) return null; + const out = []; + for (let i = 0; i < items.length; i++) { + const e = Runtime.deref(state, items[i]); + if (typeof e !== "object" || e === null || e.tag !== "atom") return null; + out.push(Runtime.string_of(program.intern_table, e.id)); + } + return out; +} + +function builtin_op(program, state) { + const precV = Runtime.deref(state, Runtime.get_reg(state, 1)); + const typeV = Runtime.deref(state, Runtime.get_reg(state, 2)); + const nameV = Runtime.get_reg(state, 3); + if (typeof precV !== "object" || precV === null || precV.tag !== "int") { + return false; + } + if (typeof typeV !== "object" || typeV === null || typeV.tag !== "atom") { + return false; + } + const prec = precV.val; + if (prec < 0 || prec > 1200) return false; + const typeStr = Runtime.string_of(program.intern_table, typeV.id); + const known = { + xfx: true, xfy: true, yfx: true, fx: true, fy: true, xf: true, yf: true + }; + if (!known[typeStr]) return false; + const names = op_resolve_names(program, state, nameV); + if (names === null) return false; + for (let i = 0; i < names.length; i++) { + Runtime.op_set(names[i], prec, typeStr); + } + return true; +} + +Runtime.builtin_call = function (program, state, inst) { + const p = inst.pred; + const name = pred_name(p); + const typeOk = type_builtin(program, state, p); + if (typeOk !== null) return typeOk; + if (name === "true") return true; + if (name === "fail" || name === "false") return false; + if (name === "deterministic") return state.cps.length === 0; + if (name === "!") { state.cps = []; return true; } + if (name === "\\+" || name === "not") return builtin_naf(program, state); + if (name === "call") return builtin_call1(program, state); + if (name === "^") { + return invoke_goal(program, state, Runtime.get_reg(state, 2), true) === true; + } + if (name === "findall" || name === "bagof" || name === "setof" || name === "aggregate_all") { + return builtin_findall_family(program, state, p); + } + if (name === "write" || name === "display" || name === "print") return builtin_write(program, state); + if (name === "writeq") return builtin_writeq(program, state); + if (name === "writeln") { + builtin_write(program, state); + process.stdout.write("\n"); + return true; + } + if (name === "nl") { process.stdout.write("\n"); return true; } + if (name === "tab") return builtin_tab(program, state); + if (name === "format") { + if (pred_arity(inst) === 3) return builtin_format3(program, state); + return builtin_format2(program, state); + } + if (name === "compare") return builtin_compare3(program, state); + if (name === "sort" && pred_arity(inst) === 4) return builtin_sort4(program, state); + if (name === "sort" || name === "msort" || name === "keysort") { + return builtin_sort_family(program, state, name); + } + if (name === "predsort") return builtin_predsort(program, state); + if (name === "append") return builtin_append(program, state); + if (name === "reverse") return builtin_reverse(program, state); + if (name === "nth0") return builtin_nth(program, state, false); + if (name === "nth1") return builtin_nth(program, state, true); + if (name === "last") return builtin_last(program, state); + if (name === "sum_list" || name === "sumlist") return builtin_sum_list(program, state); + if (name === "max_list") return builtin_minmax_list(program, state, true); + if (name === "min_list") return builtin_minmax_list(program, state, false); + if (name === "list_to_set") return builtin_list_to_set(program, state); + 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") 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 === "string_length") return builtin_string_length(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 === "sub_string") return builtin_sub_string(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); + if (name === "upcase_atom") return builtin_case_atom(program, state, true); + if (name === "downcase_atom") return builtin_case_atom(program, state, false); + if (name === "empty_assoc") return builtin_empty_assoc(program, state); + if (name === "list_to_assoc") return builtin_list_to_assoc(program, state); + if (name === "get_assoc") return builtin_get_assoc(program, state); + 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; + if (name === "member") return builtin_member(program, state); + if (name === "length") return builtin_length(program, state); + if (name === "functor") return builtin_functor(program, state); + 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 === "op") return builtin_op(program, state); + if (name === "between") return builtin_between(program, state); + if (name === "is") { + const n = eval_arith(program, state, Runtime.get_reg(state, 2)); + if (n === null) return false; + const boxed = as_arith_result(n); + if (boxed === null) return false; + return bind_or_compare(state, 1, boxed, program); + } + const cmp = name; + if (cmp === "=:=" || cmp === "=\\=" || cmp === ">" || cmp === "<" + || cmp === ">=" || cmp === "=<") { + const a = eval_arith(program, state, Runtime.get_reg(state, 1)); + const b = eval_arith(program, state, Runtime.get_reg(state, 2)); + if (a === null || b === null) return false; + if (cmp === "=:=") return a === b; + if (cmp === "=\\=") return a !== b; + if (cmp === ">") return a > b; + if (cmp === "<") return a < b; + if (cmp === ">=") return a >= b; + if (cmp === "=<") return a <= b; + } + return false; +}; + +function setup_goal_regs(state, goal) { + if (typeof goal !== "object" || goal === null) return null; + if (goal.tag === "atom") return { pred: null, arity: 0, atom: goal }; + if (goal.tag !== "struct") return null; + return { pred: goal.fid, arity: (goal.args || []).length, args: goal.args || [] }; +} + +function run_builtin_or_label(program, parent, goal, shareBindings) { + const info = setup_goal_regs(null, goal); + if (!info) return false; + const intern = program.intern_table; + let pred; + let arity; + let args; + if (info.atom) { + pred = Runtime.string_of(intern, info.atom.id); + arity = 0; + args = []; + } else { + pred = Runtime.string_of(intern, info.pred); + arity = info.arity; + args = info.args; + } + pred = strip_trailing_arity(pred); + if (pred === "true") return true; + if (pred === "fail" || pred === "false") return false; + if (pred === "^" && arity >= 2) { + return invoke_goal(program, parent, args[1], share === true); + } + + function fresh_sub() { + const sub = Runtime.new_state(); + if (shareBindings) { + sub.bindings = parent.bindings; + sub.trail = parent.trail; + sub.var_counter = parent.var_counter; + } else { + sub.bindings = Runtime.copy_table(parent.bindings); + sub.var_counter = parent.var_counter; + } + for (let i = 0; i < args.length; i++) Runtime.put_reg(sub, i + 1, args[i]); + return sub; + } + + const temp = { + instructions: [null, I.BuiltinCall(pred, arity), I.Proceed()], + labels: {}, + intern_table: intern, + indexed_atom_fact2: program.indexed_atom_fact2, + inline_facts: program.inline_facts, + fact_sources: program.fact_sources, + foreign_handlers: program.foreign_handlers + }; + let sub = fresh_sub(); + sub.pc = 1; + if (Runtime.run(temp, sub) === true) { + if (shareBindings) parent.var_counter = sub.var_counter; + else { + parent.bindings = sub.bindings; + parent.var_counter = sub.var_counter; + } + return true; + } + const target = label_for_pred(program, pred, arity); + if (target === null) return false; + sub = fresh_sub(); + sub.pc = target; + const ok = Runtime.run(program, sub) === true; + if (ok) { + if (shareBindings) parent.var_counter = sub.var_counter; + else { + parent.bindings = sub.bindings; + parent.var_counter = sub.var_counter; + } + } + return ok; +} + +goal_succeeds = function (program, parent, goal) { + goal = Runtime.deref(parent, goal); + return run_builtin_or_label(program, parent, goal, false); +}; + +invoke_goal = function (program, parent, goal, share) { + goal = Runtime.deref(parent, goal); + return run_builtin_or_label(program, parent, goal, share === true); +}; + +function snapshot_row(state, template, witnessCells) { + const wvals = []; + for (let i = 0; i < (witnessCells || []).length; i++) { + wvals.push(Runtime.copy_term(state, witnessCells[i])); + } + return { template: Runtime.copy_term(state, template), witnesses: wvals }; +} + +collect_goal_rows = function (program, parent, goal, template, witnessCells) { + const collected = []; + goal = Runtime.deref(parent, goal); + const info = setup_goal_regs(null, goal); + if (!info) return collected; + const intern = program.intern_table; + let pred; + let arity; + let args; + if (info.atom) { + pred = strip_trailing_arity(Runtime.string_of(intern, info.atom.id)); + arity = 0; + args = []; + } else { + pred = strip_trailing_arity(Runtime.string_of(intern, info.pred)); + arity = info.arity; + args = info.args; + } + if (pred === "true") { + collected.push(snapshot_row(parent, template, witnessCells)); + return collected; + } + if (pred === "fail" || pred === "false") return collected; + + function make_sub(instructions, startPc) { + const sub = Runtime.new_state(); + sub.bindings = Runtime.copy_table(parent.bindings); + sub.var_counter = parent.var_counter; + for (let i = 0; i < args.length; i++) Runtime.put_reg(sub, i + 1, args[i]); + sub.pc = startPc; + return { program: { instructions: instructions, labels: program.labels, intern_table: intern, + indexed_atom_fact2: program.indexed_atom_fact2, inline_facts: program.inline_facts, + fact_sources: program.fact_sources, foreign_handlers: program.foreign_handlers }, state: sub }; + } + + function harvest(ctx) { + Runtime.collect_run(ctx.program, ctx.state, function (s) { + collected.push(snapshot_row(s, template, witnessCells)); + }); + } + + const builtinProg = [null, I.BuiltinCall(pred, arity), I.Proceed()]; + const builtinCtx = make_sub(builtinProg, 1); + harvest(builtinCtx); + if (collected.length > 0) return collected; + const target = label_for_pred(program, pred, arity); + if (target === null) return collected; + const userCtx = make_sub(program.instructions, target); + userCtx.program = program; + harvest(userCtx); + return collected; +}; + +collect_goal_solutions = function (program, parent, goal, template) { + const rows = collect_goal_rows(program, parent, goal, template, []); + const collected = []; + for (let i = 0; i < rows.length; i++) collected.push(rows[i].template); + return collected; +}; + +Runtime.call_foreign = function (program, state, inst) { + const key = call_key(inst); + const handler = program.foreign_handlers && program.foreign_handlers[key]; + if (typeof handler !== "function") return false; + return handler(state, program.intern_table) === true; +}; + +function try_builtin_fallback(program, state, pred, arity) { + return Runtime.builtin_call(program, state, { pred: pred, arity: arity }) === true; +} + +function fact_trim(s) { + return String(s).replace(/^\s+|\s+$/g, ""); +} + +function fact_term_key(v) { + if (typeof v !== "object" || v === null) return String(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); + return term_key({ bindings: {} }, v); +} + +function parse_fact_source_value(program, text) { + const parsed = Runtime.parse_term(program, fact_trim(text)); + if (parsed !== null) return parsed; + return V.Atom(Runtime.intern(program.intern_table, fact_trim(text))); +} + +function parse_json_fact_value(program, val) { + if (typeof val === "number") { + return Number.isInteger(val) ? V.Int(val) : V.Float(val); + } + if (typeof val === "boolean") { + return V.Atom(Runtime.intern(program.intern_table, val ? "true" : "false")); + } + if (val === null) return V.Atom(Runtime.intern(program.intern_table, "[]")); + if (typeof val === "string") return parse_fact_source_value(program, val); + return null; +} + +function parse_delimited_fact_row(program, line) { + const sep = line.indexOf("\t") >= 0 ? "\t" : ","; + const parts = line.split(sep); + if (parts.length < 2) return null; + return [ + parse_fact_source_value(program, parts[0]), + parse_fact_source_value(program, parts[1]) + ]; +} + +function parse_jsonl_fact_row(program, line) { + let obj; + try { obj = JSON.parse(line); } catch (e) { return null; } + if (Array.isArray(obj) && obj.length >= 2) { + const a = parse_json_fact_value(program, obj[0]); + const b = parse_json_fact_value(program, obj[1]); + if (a === null || b === null) return null; + return [a, b]; + } + if (obj && typeof obj === "object") { + const args = obj.args; + if (Array.isArray(args) && args.length >= 2) { + const a = parse_json_fact_value(program, args[0]); + const b = parse_json_fact_value(program, args[1]); + if (a === null || b === null) return null; + return [a, b]; + } + if (obj.a1 !== undefined && obj.a2 !== undefined) { + const a = parse_json_fact_value(program, obj.a1); + const b = parse_json_fact_value(program, obj.a2); + if (a === null || b === null) return null; + return [a, b]; + } + } + return null; +} + +function first_data_line_looks_json(lines) { + for (let i = 0; i < lines.length; i++) { + const cleaned = fact_trim(lines[i]); + if (cleaned === "" || cleaned.charAt(0) === "#") continue; + const c = cleaned.charAt(0); + return c === "{" || c === "["; + } + return false; +} + +Runtime.read_facts_file = function (program, path) { + const rows = []; + const arg1_index = {}; + let text; + try { + text = require("fs").readFileSync(path, "utf8"); + } catch (e) { + return { rows: rows, arg1_index: arg1_index }; + } + const lines = String(text).split(/\r?\n/); + const jsonl = /\.jsonl$/i.test(path) || /\.ndjson$/i.test(path) || + first_data_line_looks_json(lines); + for (let i = 0; i < lines.length; i++) { + const cleaned = fact_trim(lines[i]); + if (cleaned === "" || cleaned.charAt(0) === "#") continue; + const pair = jsonl + ? parse_jsonl_fact_row(program, cleaned) + : parse_delimited_fact_row(program, cleaned); + if (!pair) continue; + rows.push(pair); + const key = fact_term_key(pair[0]); + if (!arg1_index[key]) arg1_index[key] = []; + arg1_index[key].push(pair); + } + return { rows: rows, arg1_index: arg1_index }; +}; + +function fact_source_rows(program, pred, state) { + const source = program.fact_sources && program.fact_sources[pred]; + if (!source) return null; + if (!source.cache) source.cache = Runtime.read_facts_file(program, source.path); + const a1 = Runtime.deref(state, Runtime.get_reg(state, 1)); + if (typeof a1 === "object" && a1 !== null && a1.tag !== "unbound") { + return source.cache.arg1_index[fact_term_key(a1)] || []; + } + return source.cache.rows; +} + +function fact_row_matches(state, row) { + const a1 = Runtime.deref(state, Runtime.get_reg(state, 1)); + const a2 = Runtime.deref(state, Runtime.get_reg(state, 2)); + if (typeof a1 === "object" && a1 !== null && a1.tag !== "unbound" && + same_atomic(a1, row[0]) !== true) return false; + if (typeof a2 === "object" && a2 !== null && a2.tag !== "unbound" && + same_atomic(a2, row[1]) !== true) return false; + return true; +} + +function filter_fact_rows(state, rows) { + const out = []; + const list = rows || []; + for (let i = 0; i < list.length; i++) { + if (fact_row_matches(state, list[i])) out.push(list[i]); + } + return out; +} + +function call_fact_stream(program, state, inst) { + let rows = program.inline_facts && program.inline_facts[inst.pred]; + if (!rows || rows.length === 0) { + rows = fact_source_rows(program, inst.pred, state); + } + rows = filter_fact_rows(state, rows); + if (!rows || rows.length === 0) return false; + if (rows.length > 1) { + state.cps.push(snapshot_cp(state, { kind: "fact_stream", rows: rows.slice(1) })); + } + if (Runtime.unify(state, Runtime.get_reg(state, 1), rows[0][0], program) !== true) { + return false; + } + if (Runtime.unify(state, Runtime.get_reg(state, 2), rows[0][1], program) !== true) { + return false; + } + state.pc += 1; + return true; +} + +// --------------------------------------------------------------------------- +// Opt-in profiler (UW_PROFILE=1 | json). OFF by default: Runtime._prof is +// null and every hook is a single falsy check. Reports go to stderr only. +// --------------------------------------------------------------------------- +Runtime._prof = null; +Runtime._prof_exit_hooked = false; + +function prof_now() { + return process.hrtime.bigint(); +} + +function prof_pred(p, key) { + let r = p.preds[key]; + if (!r) { + r = { calls: 0, instr: 0, ns: 0, cps: 0, maxCp: 0, lowered: false }; + p.preds[key] = r; + } + return r; +} + +function prof_enter(key, countCalls) { + const p = Runtime._prof; + if (!p) return; + const now = prof_now(); + const st = p.stack; + if (st.length) { + const top = st[st.length - 1]; + prof_pred(p, top.key).ns += Number(now - top.t0); + } + const rec = prof_pred(p, key); + if (countCalls !== false) rec.calls += 1; + st.push({ key: key, t0: now }); +} + +function prof_leave() { + const p = Runtime._prof; + if (!p || p.stack.length === 0) return; + const now = prof_now(); + const fr = p.stack.pop(); + prof_pred(p, fr.key).ns += Number(now - fr.t0); + if (p.stack.length) p.stack[p.stack.length - 1].t0 = now; +} + +function prof_flush() { + const p = Runtime._prof; + if (!p) return; + while (p.stack.length) prof_leave(); +} + +function pred_key_from_pc(program, pc) { + const labels = program.labels || {}; + const keys = Object.keys(labels); + let fallback = null; + for (let i = 0; i < keys.length; i++) { + if (labels[keys[i]] !== pc) continue; + if (/\/\d+$/.test(keys[i])) return keys[i]; + if (fallback === null) fallback = keys[i]; + } + return fallback !== null ? fallback : ("#" + String(pc)); +} + +function prof_pad(s, n) { + s = String(s); + if (s.length >= n) return s; + return s + new Array(n - s.length + 1).join(" "); +} + +function prof_padl(s, n) { + s = String(s); + if (s.length >= n) return s; + return new Array(n - s.length + 1).join(" ") + s; +} + +Runtime.prof_enter = function (key, countCalls) { prof_enter(key, countCalls); }; +Runtime.prof_leave = function () { prof_leave(); }; + +Runtime.prof_lowered_call = function (key) { + const p = Runtime._prof; + if (!p) return; + const rec = prof_pred(p, key); + rec.calls += 1; + rec.lowered = true; +}; + +Runtime.profile = function (on) { + if (!on) { + Runtime._prof = null; + return; + } + const mode = (on === "json" || on === "JSON") ? "json" : "table"; + Runtime._prof = { + mode: mode, + t0: prof_now(), + totalInstr: 0, + unifyCalls: 0, + trailPushes: 0, + heapCells: 0, + backtracks: 0, + trailUndos: 0, + stack: [], + preds: Object.create(null), + reported: false + }; + if (!Runtime._prof_exit_hooked) { + Runtime._prof_exit_hooked = true; + process.on("exit", function () { + if (Runtime._prof) Runtime.profileReport(); + }); + } +}; + +Runtime.profileReport = function () { + const p = Runtime._prof; + if (!p || p.reported) return; + p.reported = true; + prof_flush(); + const wall = Number(prof_now() - p.t0); + const names = Object.keys(p.preds); + names.sort(function (a, b) { + const da = p.preds[b].ns - p.preds[a].ns; + if (da !== 0) return da; + return p.preds[b].calls - p.preds[a].calls; + }); + const rows = []; + for (let i = 0; i < names.length; i++) { + const k = names[i]; + const r = p.preds[k]; + rows.push({ + pred: k, + calls: r.calls, + instr: r.instr, + ns: r.ns, + cps: r.cps, + max_cp_depth: r.maxCp, + lowered: r.lowered === true + }); + } + const global = { + instructions: p.totalInstr, + unify_calls: p.unifyCalls, + trail_pushes: p.trailPushes, + heap_cells: p.heapCells, + backtracks: p.backtracks, + trail_undos: p.trailUndos, + wall_ns: wall + }; + if (p.mode === "json") { + process.stderr.write(JSON.stringify({ + tier_note: "Lowered predicates: call counts only; instruction/CP/time detail is interpreter-tier.", + predicates: rows, + global: global + }) + "\n"); + return; + } + process.stderr.write("UW profile (interpreter instr/CP/time; lowered = calls only)\n"); + process.stderr.write( + prof_pad("pred", 28) + prof_padl("calls", 10) + prof_padl("instr", 12) + + prof_padl("CPs", 8) + prof_padl("maxCP", 8) + prof_padl("ns", 14) + "\n"); + for (let i = 0; i < rows.length; i++) { + const r = rows[i]; + const name = r.lowered ? r.pred + "*" : r.pred; + process.stderr.write( + prof_pad(name, 28) + prof_padl(r.calls, 10) + prof_padl(r.instr, 12) + + prof_padl(r.cps, 8) + prof_padl(r.max_cp_depth, 8) + prof_padl(r.ns, 14) + "\n"); + } + process.stderr.write("--\n"); + process.stderr.write( + "instr=" + global.instructions + + " unify=" + global.unify_calls + + " trail=" + global.trail_pushes + + " heap=" + global.heap_cells + + " backtracks=" + global.backtracks + + " undos=" + global.trail_undos + + " wall_ns=" + global.wall_ns + "\n"); +}; + +(function uw_profile_boot() { + const v = process.env && process.env.UW_PROFILE; + if (v === "1" || v === "true" || v === "yes") Runtime.profile("table"); + else if (v === "json") Runtime.profile("json"); +})(); + +function run_one_instruction(program, state) { + const inst = program.instructions[state.pc]; + if (inst === undefined || inst === null) return "missing"; + const prof = Runtime._prof; + const d0 = prof ? state.cps.length : 0; + if (prof) { + prof.totalInstr += 1; + if (prof.stack.length) { + prof_pred(prof, prof.stack[prof.stack.length - 1].key).instr += 1; + } + } + const pcBefore = state.pc; + const ok = Runtime.step(program, state, inst); + if (prof && prof.stack.length) { + const rec = prof_pred(prof, prof.stack[prof.stack.length - 1].key); + const d1 = state.cps.length; + if (d1 > d0) { + rec.cps += (d1 - d0); + if (d1 > rec.maxCp) rec.maxCp = d1; + } + } + if (ok !== true) { + if (prof) prof.backtracks += 1; + if (Runtime.backtrack(state) !== true) return "fail"; + return "backtracked"; + } + if (state.pc === pcBefore && !state.halt) state.pc += 1; + return "ok"; +} + +Runtime.step = function (program, state, inst) { + const op = inst.op; + if (op === "Proceed") { + state.indexed_entry = false; + if (Runtime._prof) prof_leave(); + proceed_to_cp(state); + return true; + } + if (op === "Fail") return false; + if (op === "Allocate") { + state.stack.push({ cp: state.cp, yregs: snapshot_yregs(state) }); + state.pc += 1; + return true; + } + if (op === "Deallocate") { + const f = state.stack.pop(); + if (f) { + state.cp = f.cp; + restore_yregs(state, f.yregs); + } + state.pc += 1; + return true; + } + if (op === "PutConstant") { + Runtime.put_reg(state, inst.ai, inst.c); + state.pc += 1; + return true; + } + if (op === "PutVariable") { + const v = Runtime.new_var(state); + Runtime.put_reg(state, inst.xn, v); + Runtime.put_reg(state, inst.ai, v); + state.pc += 1; + return true; + } + if (op === "PutValue") { + Runtime.put_reg(state, inst.ai, Runtime.get_reg(state, inst.xn)); + state.pc += 1; + return true; + } + if (op === "GetVariable") { + Runtime.put_reg(state, inst.xn, Runtime.get_reg(state, inst.ai)); + state.pc += 1; + return true; + } + if (op === "GetConstant") { + if (Runtime.unify(state, Runtime.get_reg(state, inst.ai), inst.c, program) !== true) return false; + state.pc += 1; + return true; + } + if (op === "GetValue") { + if (Runtime.unify(state, Runtime.get_reg(state, inst.ai), Runtime.get_reg(state, inst.xn), program) !== true) { + return false; + } + state.pc += 1; + return true; + } + if (op === "PutStructure" || op === "PutList") { + const fid = inst.fid !== undefined ? inst.fid : Runtime.intern(program.intern_table, "[|]"); + const arity = inst.arity || 2; + begin_write(state, inst.ai, fid, arity, null); + state.pc += 1; + return true; + } + if (op === "SetVariable" || op === "UnifyVariable") { + if (state.mode === "read") { + Runtime.put_reg(state, inst.xn, state.read_args[state.read_cursor]); + state.read_cursor += 1; + finish_read_if_done(state); + } else { + const v = Runtime.new_var(state); + Runtime.put_reg(state, inst.xn, v); + push_built_term(state, v); + } + state.pc += 1; + return true; + } + if (op === "SetValue" || op === "UnifyValue") { + const v = Runtime.get_reg(state, inst.xn); + if (state.mode === "read") { + if (Runtime.unify(state, v, state.read_args[state.read_cursor], program) !== true) return false; + state.read_cursor += 1; + finish_read_if_done(state); + } else { + push_built_term(state, v); + } + state.pc += 1; + return true; + } + if (op === "SetConstant" || op === "UnifyConstant") { + if (state.mode === "read") { + if (Runtime.unify(state, inst.c, state.read_args[state.read_cursor], program) !== true) return false; + state.read_cursor += 1; + finish_read_if_done(state); + } else { + push_built_term(state, inst.c); + } + state.pc += 1; + return true; + } + if (op === "GetStructure" || op === "GetList") { + const val = Runtime.deref(state, Runtime.get_reg(state, inst.ai)); + const fid = inst.fid !== undefined ? inst.fid : Runtime.intern(program.intern_table, "[|]"); + const arity = inst.arity || 2; + if (typeof val === "object" && val !== null && val.tag === "unbound") { + const s = V.Struct(fid, []); + bind_var(state, val, s); + Runtime.put_reg(state, inst.ai, s); + begin_write(state, inst.ai, fid, arity, s); + state.pc += 1; + return true; + } + if (typeof val === "object" && val !== null && val.tag === "struct" + && same_functor(program, val.fid, (val.args || []).length, fid, arity)) { + begin_read(state, val.args); + state.pc += 1; + return true; + } + return false; + } + if (op === "TryMeElse" || op === "TryMeElsePc") { + state.indexed_entry = false; + const cp = snapshot_machine(state); + cp.next_pc = inst.pc || program.labels[inst.label]; + state.cps.push(cp); + state.pc += 1; + return true; + } + if (op === "RetryMeElse" || op === "RetryMeElsePc") { + const dest = inst.pc || program.labels[inst.label]; + if (state.indexed_entry) { + state.indexed_entry = false; + const cp = snapshot_machine(state); + cp.next_pc = dest; + state.cps.push(cp); + state.pc += 1; + return true; + } + const cp = state.cps[state.cps.length - 1]; + if (cp === undefined || cp.kind === "aggregate") { + state.pc += 1; + return true; + } + restore_machine(state, cp); + cp.next_pc = dest; + state.pc += 1; + return true; + } + if (op === "TrustMe") { + if (state.indexed_entry) { + state.indexed_entry = false; + state.pc += 1; + return true; + } + const cp = state.cps[state.cps.length - 1]; + if (cp !== undefined && cp.kind !== "aggregate") state.cps.pop(); + state.pc += 1; + return true; + } + if (op === "Try" || op === "TryPc") { + state.indexed_entry = false; + const cp = snapshot_machine(state); + cp.next_pc = state.pc + 1; + state.cps.push(cp); + const dest = inst.pc || program.labels[inst.label]; + if (dest === undefined || dest === null) return false; + state.pc = dest; + return true; + } + if (op === "Retry" || op === "RetryPc") { + const cp = state.cps[state.cps.length - 1]; + if (cp === undefined || cp.kind === "aggregate") return false; + restore_machine(state, cp); + cp.next_pc = state.pc + 1; + const dest = inst.pc || program.labels[inst.label]; + if (dest === undefined || dest === null) return false; + state.pc = dest; + return true; + } + if (op === "Trust" || op === "TrustPc") { + const cp = state.cps[state.cps.length - 1]; + if (cp !== undefined && cp.kind !== "aggregate") state.cps.pop(); + const dest = inst.pc || program.labels[inst.label]; + if (dest === undefined || dest === null) return false; + state.pc = dest; + return true; + } + if (op === "CutIte") { + if (state.cps.length > 0) state.cps.pop(); + state.pc += 1; + return true; + } + if (op === "GetLevel") { + Runtime.put_reg(state, inst.yn, state.cps.length); + state.pc += 1; + return true; + } + if (op === "Cut") { + let lvl = Runtime.get_reg(state, inst.yn); + if (typeof lvl !== "number") lvl = 0; + while (state.cps.length > lvl) state.cps.pop(); + state.pc += 1; + return true; + } + if (op === "Jump") { + state.pc = program.labels[inst.label]; + return state.pc !== undefined && state.pc !== null; + } + if (op === "JumpPc") { + state.pc = inst.pc; + return state.pc !== undefined && state.pc !== null; + } + if (op === "SwitchOnConstant" || op === "SwitchOnConstantPc") return switch_constant(program, state, inst, 1); + if (op === "SwitchOnConstantA2" || op === "SwitchOnConstantA2Pc") return switch_constant(program, state, inst, 2); + if (op === "SwitchOnStructure" || op === "SwitchOnStructurePc") return switch_structure(program, state, inst, 1); + if (op === "SwitchOnStructureA2" || op === "SwitchOnStructureA2Pc") return switch_structure(program, state, inst, 2); + if (op === "SwitchOnTerm" || op === "SwitchOnTermPc") return switch_term(program, state, inst, inst.reg || 1); + if (op === "Call") { + const target = program.labels[call_key(inst)]; + if (target === undefined || target === null) { + if (try_builtin_fallback(program, state, inst.pred, inst.arity)) { + state.pc += 1; + return true; + } + return false; + } + state.cp = state.pc + 1; + state.pc = target; + push_y_save(state); + if (Runtime._prof) prof_enter(call_key(inst), true); + return true; + } + if (op === "CallIndexedAtomFact2") return false; + if (op === "CallFactStream") return call_fact_stream(program, state, inst); + if (op === "CallPc") { + state.cp = state.pc + 1; + state.pc = inst.pc; + if (state.pc !== undefined && state.pc !== null) push_y_save(state); + if (Runtime._prof && state.pc !== undefined && state.pc !== null) { + prof_enter(pred_key_from_pc(program, state.pc), true); + } + return state.pc !== undefined && state.pc !== null; + } + if (op === "Execute") { + const target = program.labels[call_key(inst)]; + if (target === undefined || target === null) { + if (try_builtin_fallback(program, state, inst.pred, inst.arity)) { + // Tail-call to a builtin: same as Proceed, not halt. Helpers such + // as substring_from/3 end with Execute sub_string/5; halting here + // dropped the rest of parse_args/3 (Result stayed unbound). + state.indexed_entry = false; + if (Runtime._prof) prof_leave(); + proceed_to_cp(state); + return true; + } + return false; + } + state.pc = target; + if (Runtime._prof) { + prof_leave(); + prof_enter(call_key(inst), true); + } + return true; + } + if (op === "ExecutePc") { + state.pc = inst.pc; + if (Runtime._prof && state.pc !== undefined && state.pc !== null) { + prof_leave(); + prof_enter(pred_key_from_pc(program, state.pc), true); + } + return state.pc !== undefined && state.pc !== null; + } + if (op === "BuiltinCall") { + if (Runtime.builtin_call(program, state, inst) !== true) return false; + state.pc += 1; + return true; + } + if (op === "CallForeign" || op === "ExecuteForeign") { + if (Runtime.call_foreign(program, state, inst) !== true) return false; + if (op === "ExecuteForeign") state.halt = true; + else state.pc += 1; + return true; + } + if (op === "ArgInstr") { + const term = Runtime.deref(state, Runtime.get_reg(state, inst.ai)); + if (typeof term !== "object" || term === null || term.tag !== "struct") return false; + return bind_or_compare(state, inst.out, term.args[inst.n - 1], program); + } + if (op === "BeginAggregate") { + const endPc = find_matching_end_aggregate(program, state.pc); + if (endPc === null) return false; + const agg = snapshot_cp(state, { + kind: "aggregate", + agg_kind: inst.kind, + template_reg: inst.template, + bag_reg: inst.bag, + witness_regs: inst.witnesses || [], + table: program.intern_table, + collected: [] + }); + agg.next_pc = endPc + 1; + state.cps.push(agg); + state.pc += 1; + return true; + } + if (op === "EndAggregate") { + let agg = null; + for (let i = state.cps.length - 1; i >= 0; i--) { + if (state.cps[i].kind === "aggregate") { agg = state.cps[i]; break; } + } + if (agg === null) return false; + const wregs = agg.witness_regs || []; + const wvals = []; + for (let wi = 0; wi < wregs.length; wi++) { + wvals.push(Runtime.copy_term(state, Runtime.get_reg(state, wregs[wi]))); + } + agg.collected.push({ + template: Runtime.copy_term(state, Runtime.get_reg(state, inst.template)), + witnesses: wvals + }); + return false; + } + // Convention 6: unknown instruction (including Raw / indexing hints) + // is a real one-slot NoOp so label PCs stay aligned. + if (op === "NoOp" || op === "Raw") { + state.pc += 1; + return true; + } + state.pc += 1; + return true; +}; + +Runtime.run = function (program, state) { + state.program = program; + state.halt = false; + let steps = 0; + const limit = 2000000; + if (!Runtime._prof) { + while (!state.halt) { + if (++steps > limit) return false; + const inst = program.instructions[state.pc]; + if (inst === undefined || inst === null) return false; + const pcBefore = state.pc; + if (Runtime.step(program, state, inst) !== true) { + if (Runtime.backtrack(state) !== true) return false; + } else if (state.pc === pcBefore && !state.halt) { + state.pc += 1; + } + } + return true; + } + while (!state.halt) { + if (++steps > limit) return false; + const r = run_one_instruction(program, state); + if (r === "missing") return false; + if (r === "fail") return false; + } + return true; +}; + +Runtime.collect_run = function (program, state, onSolution) { + state.program = program; + state.halt = false; + let steps = 0; + const limit = 2000000; + if (!Runtime._prof) { + while (true) { + while (!state.halt) { + if (++steps > limit) return; + const inst = program.instructions[state.pc]; + if (inst === undefined || inst === null) { + state.halt = true; + break; + } + const pcBefore = state.pc; + if (Runtime.step(program, state, inst) !== true) { + if (Runtime.backtrack(state) !== true) { + return; + } + } else if (state.pc === pcBefore && !state.halt) { + state.pc += 1; + } + } + if (!state.halt) return; + onSolution(state); + if (Runtime.backtrack(state) !== true) return; + state.halt = false; + } + } + while (true) { + while (!state.halt) { + if (++steps > limit) return; + const r = run_one_instruction(program, state); + if (r === "missing") { + state.halt = true; + break; + } + if (r === "fail") return; + } + if (!state.halt) return; + onSolution(state); + Runtime._prof.backtracks += 1; + if (Runtime.backtrack(state) !== true) return; + state.halt = false; + } +}; + +Runtime.run_predicate = function (program, startPc, args) { + const state = Runtime.new_state(); + const list = args || []; + for (let i = 0; i < list.length; i++) Runtime.put_reg(state, i + 1, list[i]); + state.pc = startPc; + if (Runtime._prof) { + prof_enter(pred_key_from_pc(program, startPc), true); + } + const ok = Runtime.run(program, state) === true; + if (Runtime._prof) prof_leave(); + return ok; +}; + +// ISO-ish operator tables (same coverage as the R WAM native reader). +// Format: name -> { prec, assoc }. Defaults are cloned into the live +// tables; op/3 and install_declared_ops mutate the live copies. +// Postfix starts empty; users add xf/yf via op/3. +Runtime.OP_INFIX_DEFAULT = { + ":-": { 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_DEFAULT = { + ":-": { 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_DEFAULT = {}; + +function copy_op_table(src) { + const out = {}; + const keys = Object.keys(src); + for (let i = 0; i < keys.length; i++) { + const k = keys[i]; + out[k] = { prec: src[k].prec, assoc: src[k].assoc }; + } + return out; +} + +Runtime.reset_op_tables = function () { + Runtime.OP_INFIX = copy_op_table(Runtime.OP_INFIX_DEFAULT); + Runtime.OP_PREFIX = copy_op_table(Runtime.OP_PREFIX_DEFAULT); + Runtime.OP_POSTFIX = copy_op_table(Runtime.OP_POSTFIX_DEFAULT); +}; + +// SWI op/3: Priority 0 removes the specifier. Infix and postfix of the +// same name cannot coexist (ISO 6.3.4.3); prefix may share a name with +// infix (e.g. + / - / :-). +Runtime.op_set = function (name, prec, typeStr) { + const isInfix = typeStr === "xfx" || typeStr === "xfy" || typeStr === "yfx"; + const isPrefix = typeStr === "fx" || typeStr === "fy"; + const isPostfix = typeStr === "xf" || typeStr === "yf"; + if (prec === 0) { + if (isInfix) delete Runtime.OP_INFIX[name]; + if (isPrefix) delete Runtime.OP_PREFIX[name]; + if (isPostfix) delete Runtime.OP_POSTFIX[name]; + return; + } + if (isInfix) { + Runtime.OP_INFIX[name] = { prec: prec, assoc: typeStr }; + delete Runtime.OP_POSTFIX[name]; + } else if (isPrefix) { + Runtime.OP_PREFIX[name] = { prec: prec, assoc: typeStr }; + } else if (isPostfix) { + Runtime.OP_POSTFIX[name] = { prec: prec, assoc: typeStr }; + delete Runtime.OP_INFIX[name]; + } +}; + +Runtime.install_declared_ops = function (decls) { + const list = decls || []; + for (let i = 0; i < list.length; i++) { + const d = list[i]; + Runtime.op_set(d.name, d.prec | 0, d.type); + } +}; + +Runtime.reset_op_tables(); + +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; + } + 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" || + 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 === "string") { + p.pos += 1; + return V.String(tok.value); + } + 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 === "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); + 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 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; + +module.exports = { Runtime: Runtime, V: V, I: I }; diff --git a/examples/cli_args/wamjs/run_differential_wamjs.sh b/examples/cli_args/wamjs/run_differential_wamjs.sh new file mode 100755 index 000000000..dde4ee69a --- /dev/null +++ b/examples/cli_args/wamjs/run_differential_wamjs.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT OR Apache-2.0 +# Copyright (c) 2026 John William Creighton (s243a) +# +# run_differential_wamjs.sh -- step A2 for the JS-WAM lane. +# +# Identical to examples/cli_args/run_differential.sh except for which parser is +# put opposite the oracle: the same generator, the same seed, the same case file, +# the same comparator. The Prolog reference is replaced by the JS-WAM compiled +# parser (wamjs/js/generated_program.js, reached through the edge shim). +# +# bash examples/cli_args/wamjs/run_differential_wamjs.sh +# +# Exits non-zero on any divergence. Artifacts land in ./.diff_out/ next to this +# script (cases.txt, oracle.jsonl, wamjs.jsonl). + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +UP="$HERE/.." +OUT="$HERE/.diff_out" +mkdir -p "$OUT" + +echo "== generating seeded cases (the harness's own generator, unchanged) ==" +node "$UP/gen_cases.mjs" > "$OUT/cases.txt" +echo "cases: $(wc -l < "$OUT/cases.txt") lines -> $OUT/cases.txt" + +echo "== running the JavaScript oracle (peerhailer parseArgs) ==" +START_ORACLE=$(date +%s%N) +node "$UP/diff_runner.mjs" < "$OUT/cases.txt" > "$OUT/oracle.jsonl" +END_ORACLE=$(date +%s%N) + +echo "== running the JS-WAM compiled parser ==" +START_WAM=$(date +%s%N) +node "$HERE/diff_runner_wamjs.mjs" < "$OUT/cases.txt" > "$OUT/wamjs.jsonl" +END_WAM=$(date +%s%N) + +awk -v o0="$START_ORACLE" -v o1="$END_ORACLE" -v w0="$START_WAM" -v w1="$END_WAM" 'BEGIN { + printf "timing: oracle %.3fs wamjs %.3fs\n", (o1-o0)/1e9, (w1-w0)/1e9 +}' + +echo "== comparing ==" +node "$UP/compare_jsonl.mjs" "$OUT/cases.txt" "$OUT/oracle.jsonl" "$OUT/wamjs.jsonl" diff --git a/src/unifyweaver/bindings/javascript_wam_bindings.pl b/src/unifyweaver/bindings/javascript_wam_bindings.pl index c5d018e16..9245b4b00 100644 --- a/src/unifyweaver/bindings/javascript_wam_bindings.pl +++ b/src/unifyweaver/bindings/javascript_wam_bindings.pl @@ -95,6 +95,7 @@ javascript_wam_builtin(atom_codes, 2, implemented). javascript_wam_builtin(char_code, 2, implemented). javascript_wam_builtin(sub_atom, 5, implemented). +javascript_wam_builtin(sub_string, 5, implemented). javascript_wam_builtin(atom_string, 2, implemented). javascript_wam_builtin(string_to_atom, 2, implemented). javascript_wam_builtin(string, 1, implemented). diff --git a/templates/targets/javascript_wam/runtime.js.mustache b/templates/targets/javascript_wam/runtime.js.mustache index 814f718a5..54e86a20b 100644 --- a/templates/targets/javascript_wam/runtime.js.mustache +++ b/templates/targets/javascript_wam/runtime.js.mustache @@ -232,7 +232,8 @@ Runtime.new_state = function () { build_stack: [], read_args: [], read_cursor: 0, - read_stack: [] + read_stack: [], + y_save: [] }; }; @@ -391,6 +392,26 @@ function restore_yregs(state, y) { for (let i = 0; i < yk.length; i++) state.regs[yk[i]] = y[yk[i]]; } +function push_y_save(state) { + state.y_save = state.y_save || []; + state.y_save.push(snapshot_yregs(state)); +} + +function pop_y_save(state) { + if (!state.y_save || state.y_save.length === 0) return; + restore_yregs(state, state.y_save.pop()); +} + +// Return from the current predicate: restore the Caller's Y (if Call +// pushed a save) and jump to CP. Used by Proceed and by Execute of a +// builtin (the compiler emits Deallocate+Execute for a last-goal builtin +// with no user label; that must return, not halt the machine). +function proceed_to_cp(state) { + pop_y_save(state); + if (state.cp === 0) state.halt = true; + else { state.pc = state.cp; state.cp = 0; } +} + function snapshot_machine(state) { return { regs: Runtime.copy_table(state.regs), @@ -402,7 +423,8 @@ function snapshot_machine(state) { stack: state.stack.slice(), read_stack: (state.read_stack || []).slice(), read_args: state.read_args, - read_cursor: state.read_cursor + read_cursor: state.read_cursor, + y_save: (state.y_save || []).slice() }; } @@ -417,6 +439,7 @@ function restore_machine(state, snap) { state.read_stack = (snap.read_stack || []).slice(); if (snap.read_args !== undefined) state.read_args = snap.read_args; if (snap.read_cursor !== undefined) state.read_cursor = snap.read_cursor; + state.y_save = (snap.y_save || []).slice(); } function restore_cp_frame(state, cp) { @@ -2010,6 +2033,55 @@ function builtin_sub_atom(program, state) { return dispatch_enum(program, state, sols); } +function builtin_sub_string(program, state) { + const text = atomic_text(program, state, Runtime.get_reg(state, 1)); + if (text === null) return false; + const n = text_char_length(text); + const before = Runtime.deref(state, Runtime.get_reg(state, 2)); + const length = Runtime.deref(state, Runtime.get_reg(state, 3)); + const after = Runtime.deref(state, Runtime.get_reg(state, 4)); + const sub = atomic_text(program, state, Runtime.get_reg(state, 5)); + function asInt(v) { + if (typeof v === "object" && v !== null && v.tag === "int") return v.val; + return null; + } + function sliceChars(s, start, len) { + let out = ""; + let i = 0; + for (const ch of String(s)) { + if (i >= start && i < start + len) out += ch; + i++; + if (i >= start + len) break; + } + return out; + } + const b0 = asInt(before); + const l0 = asInt(length); + const a0 = asInt(after); + const sols = []; + const bLo = b0 === null ? 0 : b0; + const bHi = b0 === null ? n : b0; + for (let b = bLo; b <= bHi; b++) { + if (b < 0 || b > n) continue; + const lLo = l0 === null ? 0 : l0; + const lHi = l0 === null ? (n - b) : l0; + for (let l = lLo; l <= lHi; l++) { + if (l < 0 || b + l > n) continue; + const a = n - b - l; + if (a0 !== null && a !== a0) continue; + const piece = sliceChars(text, b, l); + if (sub !== null && piece !== sub) continue; + sols.push([ + [2, V.Int(b)], + [3, V.Int(l)], + [4, V.Int(a)], + [5, V.String(piece)] + ]); + } + } + return dispatch_enum(program, state, sols); +} + function builtin_atom_string(program, state) { const aText = atomic_text(program, state, Runtime.get_reg(state, 1)); const bText = atomic_text(program, state, Runtime.get_reg(state, 2)); @@ -2617,6 +2689,7 @@ Runtime.builtin_call = function (program, state, inst) { 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 === "sub_string") return builtin_sub_string(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); @@ -3209,8 +3282,7 @@ Runtime.step = function (program, state, inst) { if (op === "Proceed") { state.indexed_entry = false; if (Runtime._prof) prof_leave(); - if (state.cp === 0) state.halt = true; - else { state.pc = state.cp; state.cp = 0; } + proceed_to_cp(state); return true; } if (op === "Fail") return false; @@ -3433,6 +3505,7 @@ Runtime.step = function (program, state, inst) { } state.cp = state.pc + 1; state.pc = target; + push_y_save(state); if (Runtime._prof) prof_enter(call_key(inst), true); return true; } @@ -3441,6 +3514,7 @@ Runtime.step = function (program, state, inst) { if (op === "CallPc") { state.cp = state.pc + 1; state.pc = inst.pc; + if (state.pc !== undefined && state.pc !== null) push_y_save(state); if (Runtime._prof && state.pc !== undefined && state.pc !== null) { prof_enter(pred_key_from_pc(program, state.pc), true); } @@ -3450,7 +3524,12 @@ Runtime.step = function (program, state, inst) { const target = program.labels[call_key(inst)]; if (target === undefined || target === null) { if (try_builtin_fallback(program, state, inst.pred, inst.arity)) { - state.halt = true; + // Tail-call to a builtin: same as Proceed, not halt. Helpers such + // as substring_from/3 end with Execute sub_string/5; halting here + // dropped the rest of parse_args/3 (Result stayed unbound). + state.indexed_entry = false; + if (Runtime._prof) prof_leave(); + proceed_to_cp(state); return true; } return false; diff --git a/tests/test_wam_javascript_builtins.pl b/tests/test_wam_javascript_builtins.pl index d4ef441d4..4eb6b3384 100644 --- a/tests/test_wam_javascript_builtins.pl +++ b/tests/test_wam_javascript_builtins.pl @@ -65,6 +65,11 @@ :- dynamic user:probe_string_tag/0. :- dynamic user:probe_string_polish/0. :- dynamic user:probe_string_literal/0. +:- dynamic user:probe_sub_string/0. +:- dynamic user:probe_tail_builtin/0. +:- dynamic user:probe_y_preserve/0. +:- dynamic user:tail_sub/2. +:- dynamic user:wide_list/1. :- dynamic user:lit_hi/1. :- dynamic user:eq_str/1. :- dynamic user:fib/2. @@ -114,6 +119,11 @@ retractall(user:probe_string_tag), retractall(user:probe_string_polish), retractall(user:probe_string_literal), + retractall(user:probe_sub_string), + retractall(user:probe_tail_builtin), + retractall(user:probe_y_preserve), + retractall(user:tail_sub/2), + retractall(user:wide_list/1), retractall(user:lit_hi/1), retractall(user:eq_str/1), assertz((user:probe_findall :- @@ -317,7 +327,29 @@ lit_hi(X), string(X), \+ atom(X), atom_string(hi, Expected), X == Expected, eq_str(Y), string(Y), Y == Expected, - writeq(X), nl)). + writeq(X), nl)), + assertz((user:probe_sub_string :- + sub_string("hello", 0, 2, After, Sub), + string(Sub), Sub == "he", After == 3, + sub_string("--flag", 0, 2, _, Pref), Pref == "--", + \+ sub_string("ab", 0, 3, _, _), + write(ok), nl)), + % Tail-call a builtin (compiler emits Deallocate+Execute sub_string/5). + % Execute-to-builtin must Proceed to CP, not halt the machine. + assertz((user:tail_sub(In, Out) :- + sub_string(In, 0, 2, _, Out))), + assertz((user:probe_tail_builtin :- + tail_sub("hello", S), + string(S), S == "he", + write(ok), nl)), + % A long ground list fact compiles as GetList/Unify* with no Allocate, + % so it would clobber the caller's Y. Call must save/restore Y. + assertz(user:wide_list([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79])), + assertz((user:probe_y_preserve :- + Marker = kept, + wide_list(_), + Marker == kept, + write(ok), nl)). probe_preds([ user:probe_findall/0, @@ -359,7 +391,12 @@ user:probe_string_polish/0, user:lit_hi/1, user:eq_str/1, - user:probe_string_literal/0 + user:probe_string_literal/0, + user:probe_sub_string/0, + user:tail_sub/2, + user:probe_tail_builtin/0, + user:wide_list/1, + user:probe_y_preserve/0 ]). :- dynamic user:ctw_js/0. From f0bbe0eda3d1ded910bb3794ba32453755736636 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 1 Sep 2026 01:55:43 +0000 Subject: [PATCH 3/3] Record A2 corpus 17/17 and 5067-line differential numbers. Zero divergences vs the JS oracle; wamjs 4.639s vs oracle 0.048s. Co-authored-by: johns243a --- docs/WAM_JAVASCRIPT_STATUS.md | 8 ++++---- examples/cli_args/wamjs/README.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/WAM_JAVASCRIPT_STATUS.md b/docs/WAM_JAVASCRIPT_STATUS.md index 8d94a44ba..74dbf6453 100644 --- a/docs/WAM_JAVASCRIPT_STATUS.md +++ b/docs/WAM_JAVASCRIPT_STATUS.md @@ -230,7 +230,7 @@ instr=1234 unify=56 trail=40 heap=30 backtracks=12 undos=8 wall_ns=100000 | Check | Result | |---|---| | Contract corpus (`cliArgs.wamjs.test.mjs`, oracle tests, import swapped) | **17 / 17** | -| Differential vs JS oracle (`run_differential_wamjs.sh`, same seed) | **≥5000 lines, 0 divergences, 0 message mismatches** | +| Differential vs JS oracle (`run_differential_wamjs.sh`, same seed) | **5067 lines, 0 divergences, 0 message mismatches** (oracle 0.048s, wamjs 4.639s) | Runtime gaps this program forced: `sub_string/5`; Y-register save/restore across `Call` of a non-`Allocate` fact; `Execute` of a builtin `Proceed`s to @@ -249,7 +249,7 @@ CP instead of halting. Probes: `probe_sub_string/0`, `probe_y_preserve/0`, | `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). | | `sub_string/5` | **Implemented.** Same enumeration as `sub_atom/5`; Sub is a `V.String`. | -| Peerhailer argparser (A2) | **Implemented.** `examples/cli_args/wamjs/` compiles `cli_args.pl` through `wam_javascript` (interpreter) and matches the JS oracle: **17/17** corpus, **≥5000-line** differential with **0 divergences, 0 message mismatches**. | +| Peerhailer argparser (A2) | **Implemented.** `examples/cli_args/wamjs/` compiles `cli_args.pl` through `wam_javascript` (interpreter) and matches the JS oracle: **17/17** corpus, **5067-line** differential with **0 divergences, 0 message mismatches**. | | 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. `string_length/2` accepts a string, atom, or number (code-point length). `write/1` prints text unquoted; `writeq/1` and `format` `~q` recurse through lists/compounds, double-quote strings, and quote atoms only when needed (see quoting subset below). **Compiled `"foo"` literals** are spelled with outer double quotes in WAM text (`quote_wam_constant/2`); the shared classifier still returns `atom(foo)` (no `string(_)` Class). JS consults `wam_constant_token_is_string/1` and builds `V.String`. Other runtimes intern the atom as before. 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. | @@ -317,6 +317,6 @@ compiled `"foo"` literals as `V.String` (double-quoted WAM spelling; classifier still returns `atom(_)`), then opt-in interpreter profiling (`UW_PROFILE=1` / `json`, stderr-only table or JSON; lowered = call counts), then the peerhailer CLI argparser through the interpreter (A2: -`examples/cli_args/wamjs/`, 17/17 corpus + seeded differential vs the -JS oracle). +`examples/cli_args/wamjs/`, 17/17 corpus + 5067-line differential vs the +JS oracle, 0 divergences). Source-verified against SWI-Prolog as the oracle (2026-09-01). diff --git a/examples/cli_args/wamjs/README.md b/examples/cli_args/wamjs/README.md index 5bc0f77f4..fd26ee0bc 100644 --- a/examples/cli_args/wamjs/README.md +++ b/examples/cli_args/wamjs/README.md @@ -18,7 +18,7 @@ measured against the same oracle. | | | | --- | --- | | contract corpus (`../oracle/cliArgs.test.mjs`, import swapped) | **17 / 17** | -| differential vs the JS oracle, same generator and seed | **≥5000 lines, 0 divergences, 0 message mismatches** | +| differential vs the JS oracle, same generator and seed | **5067 lines, 0 divergences, 0 message mismatches** | | `node --check` on the generated module | clean | | predicates omitted / goals dropped | none |