myxo-lang 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CONCURRENCY.md +207 -0
  2. package/HOW_IT_WORKS.md +235 -0
  3. package/INDEPENDENCE.md +24 -0
  4. package/MYXO_PROMPT.md +139 -0
  5. package/README.md +494 -0
  6. package/ROADMAP.md +200 -0
  7. package/SPEC.md +181 -0
  8. package/VISION.md +249 -0
  9. package/builtins.js +361 -0
  10. package/command-fence.js +75 -0
  11. package/errors.js +45 -0
  12. package/examples/agent.myx +31 -0
  13. package/examples/fenced-agent.js +78 -0
  14. package/examples/fib.myx +11 -0
  15. package/examples/fibers.myx +57 -0
  16. package/examples/flow-routing.myx +12 -0
  17. package/examples/geo.myx +12 -0
  18. package/examples/hello.myx +2 -0
  19. package/examples/host.js +30 -0
  20. package/examples/james-myxo-demo.js +60 -0
  21. package/examples/living-mesh.myx +22 -0
  22. package/examples/match.myx +18 -0
  23. package/examples/mathlib.js +8 -0
  24. package/examples/mathlib.pl +11 -0
  25. package/examples/mathlib.py +23 -0
  26. package/examples/mcp-host.js +37 -0
  27. package/examples/nexus-mesh.myx +22 -0
  28. package/examples/nexus.myx +29 -0
  29. package/examples/ouroboros.myx +2 -0
  30. package/examples/outward-gate.myx +23 -0
  31. package/examples/physarum.myx +75 -0
  32. package/examples/polyglot-host.js +17 -0
  33. package/examples/polyglot.myx +11 -0
  34. package/examples/resilient.myx +28 -0
  35. package/examples/scheduler.myx +46 -0
  36. package/examples/the-law.myx +27 -0
  37. package/examples/use-geo.myx +9 -0
  38. package/format.js +206 -0
  39. package/interpreter.js +1092 -0
  40. package/lexer.js +173 -0
  41. package/mcp-bridge.js +77 -0
  42. package/mcp-framing.js +34 -0
  43. package/mcp-server.js +57 -0
  44. package/myxo-concurrent.js +110 -0
  45. package/myxo-live-worker.js +30 -0
  46. package/myxo-live.js +83 -0
  47. package/myxo-lsp.js +226 -0
  48. package/myxo-par-worker.js +39 -0
  49. package/myxo-plan.js +207 -0
  50. package/myxo-run.js +63 -0
  51. package/myxo.js +296 -0
  52. package/package.json +27 -0
  53. package/parser.js +662 -0
  54. package/polyglot-host.js +39 -0
  55. package/polyglot.js +191 -0
  56. package/receipt.js +71 -0
  57. package/std.myx +88 -0
  58. package/tools/memo-fuzz.js +224 -0
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+ // polyglot-host.js — run an .myx script WITH the polyglot capabilities (python/node/perl/bridgeExec/sh) installed,
3
+ // under the capability fence: the script must declare a `needs` manifest, and every foreign call is audited.
4
+ // node polyglot-host.js <file.myx> [--audit]
5
+ // This is the "real host" for scripts that orchestrate other runtimes — the fence + audit are what make that safe.
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { makeInterpreter } = require('./myxo');
10
+ const { installPolyglot } = require('./polyglot');
11
+ const { parse } = require('./parser');
12
+
13
+ function main() {
14
+ const argv = process.argv.slice(2);
15
+ const showAudit = argv.includes('--audit');
16
+ const file = argv.find(a => !a.startsWith('--'));
17
+ if (!file) { process.stderr.write('usage: node polyglot-host.js <file.myx> [--audit]\n'); process.exit(2); }
18
+ let src;
19
+ try { src = fs.readFileSync(file, 'utf8'); }
20
+ catch { process.stderr.write(`polyglot-host: no such file: ${file}\n`); process.exit(2); }
21
+
22
+ let out = '';
23
+ const interp = makeInterpreter({ output: (s) => { out += s; }, dir: path.dirname(path.resolve(file)), requireManifest: true });
24
+ installPolyglot(interp); // grant python/node/perl/bridgeExec/sh — all fenced by the script's `needs` + audited
25
+
26
+ let failed = null;
27
+ try { interp.run(parse(src)); }
28
+ catch (e) { failed = e && e.message ? e.message : String(e); }
29
+
30
+ process.stdout.write(out);
31
+ if (showAudit) {
32
+ process.stderr.write('--- audit ledger (every fenced capability call) ---\n');
33
+ for (const a of interp.audit) process.stderr.write(` ${a.ok ? 'ok ' : 'XX '} ${a.cap}\n`);
34
+ }
35
+ if (failed) { process.stderr.write('nx: ' + failed + '\n'); process.exit(1); }
36
+ }
37
+
38
+ if (require.main === module) main();
39
+ module.exports = { };
package/polyglot.js ADDED
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+ // polyglot.js — Myxo as the connective layer for OTHER LANGUAGES. The keystone: an Myxo capability is just
3
+ // `name -> fn`, so Myxo neither knows nor cares what language is behind it; each runner is a FENCED native, so the
4
+ // one law governs every cross-language call. TWO tiers (honest — these are NOT one contract):
5
+ // 1. RICH bridge via `defineLang()` -> `Xcall(file,func,...args)` + `Xeval(expr)`, value-mapped (mesh/list/
6
+ // number/bool round-trip, verified identical across languages) with structured returns. Needs a runtime
7
+ // with an eval-a-string flag. BUILT + TESTED: Python (-c), Node (-e), Perl (-e); Ruby (-e) fits the shape.
8
+ // Each harness is small but NOT trivial — it carries that language's module-load, hard-exit, JSON and eval
9
+ // quirks; adding a language means getting those right, not just pasting a string.
10
+ // 2. WEAK bridge via `bridgeExec(cmd)` -> raw stdout string in/out, NO value mapping. This is the ONLY path
11
+ // for COMPILED languages with no eval flag (Go, Rust, C++ binaries) and for CLIs; plus `sh` for the shell.
12
+ // Only Python/Node/Perl are installed + tested here; other languages are a forward design, not a present claim.
13
+ // Non-finite numbers (NaN/Infinity) are an EXPLICIT error on every path (never a silent null): args via toJson,
14
+ // Python via allow_nan=False, Node via a throwing replacer, Perl via parse. Anti-spoof hard-exit: Python os._exit
15
+ // and Perl POSIX::_exit are TRUE hard exits (skip atexit/END); Node process.exit runs 'exit' handlers, so a
16
+ // loaded module's exit-time stdout could (in trusted code) trail and, worst case, spoof the result.
17
+ //
18
+ // ⚠️ HONEST SECURITY MODEL — read before trusting this. The fence bounds WHICH languages/verbs a script may
19
+ // reach (needs-manifest), HOW MANY / HOW MUCH (value budgets: numeric first-arg = spend; otherwise = call count),
20
+ // and LOGS every call (audit ledger). It does NOT sandbox the code that runs INSIDE a granted verb. `Xcall`/
21
+ // `Xeval`/`sh` are ARBITRARY-CODE capabilities: granting one grants the FULL power of that runtime (filesystem,
22
+ // network, subprocess). Use them only for code YOU trust. For untrusted / model-generated callers, expose only
23
+ // STRUCTURED capabilities (one specific function/tool), where the fence is meaningful end to end. Calls are
24
+ // synchronous (execFileSync, built on spawnSync); a persistent-worker fast path is a future plank. Note: Myxo
25
+ // numbers are float64, so an integer above 2^53 returned from a language loses precision. Zero deps.
26
+ // Built 2026-06-26 (two-agent gated).
27
+
28
+ const { execFileSync, execSync } = require('child_process');
29
+ const { MyxoError } = require('./errors');
30
+ const { VOID } = require('./interpreter');
31
+
32
+ const CAP = 8 * 1024 * 1024, TIMEOUT = 15000;
33
+ const OK = String.fromCharCode(1), ERR = String.fromCharCode(2); // result-framing markers (U+0001 / U+0002)
34
+
35
+ function detail(e) { const s = e.stderr && String(e.stderr).trim(); return s || e.message || (e.code ? String(e.code) : 'failed'); }
36
+
37
+ // ---- Myxo <-> JS value mapping (mesh<->object, list<->array, VOID<->null), with a cycle guard ----
38
+ function nxToJs(v, seen) {
39
+ if (v === VOID) return null;
40
+ if (Array.isArray(v) || v instanceof Map) {
41
+ seen = seen || new Set();
42
+ if (seen.has(v)) throw new MyxoError('cannot send a cyclic value across the language bridge');
43
+ seen.add(v);
44
+ let out;
45
+ if (Array.isArray(v)) out = v.map(x => nxToJs(x, seen));
46
+ else { out = {}; for (const [k, val] of v) out[k] = nxToJs(val, seen); }
47
+ seen.delete(v); // pop on exit: only the ACTIVE path is "seen" -> shared (diamond) refs are fine; true cycles still caught
48
+ return out;
49
+ }
50
+ return v;
51
+ }
52
+ // JSON for the wire, but a non-finite number is an explicit error (consistent across languages — never a silent null)
53
+ function toJson(v) {
54
+ return JSON.stringify(v, (k, val) => {
55
+ if (typeof val === 'number' && !isFinite(val)) throw new MyxoError('cannot send a non-finite number across the language bridge');
56
+ return val;
57
+ });
58
+ }
59
+ function jsToNx(v) {
60
+ if (v === null || v === undefined) return VOID;
61
+ if (Array.isArray(v)) return v.map(jsToNx);
62
+ if (typeof v === 'object') { const m = new Map(); for (const k of Object.keys(v)) m.set(k, jsToNx(v[k])); return m; }
63
+ return v;
64
+ }
65
+
66
+ // A harness writes its framed result then HARD-EXITS, so nothing (atexit/threads/shutdown noise) can write
67
+ // after it and spoof the marker. This parser is language-agnostic: U+0001 + JSON on success, U+0002 + msg on error.
68
+ function parseFramed(out, what) {
69
+ const i = out.lastIndexOf(OK), j = out.lastIndexOf(ERR);
70
+ if (j > i) throw new MyxoError(`${what} error: ` + out.slice(j + 1).trim());
71
+ if (i < 0) throw new MyxoError(`${what} returned no result`);
72
+ try { return jsToNx(JSON.parse(out.slice(i + 1))); }
73
+ catch (e) { throw new MyxoError(`${what} returned a value Myxo can't represent (non-finite or non-JSON): ` + e.message); }
74
+ }
75
+
76
+ function runHarness(cmd, flag, harness, extraArgs, label) {
77
+ let out;
78
+ try { out = execFileSync(cmd, [flag, harness, ...extraArgs], { encoding: 'utf8', timeout: TIMEOUT, maxBuffer: CAP }); }
79
+ catch (e) { throw new MyxoError(`${label} failed: ` + detail(e)); }
80
+ return parseFramed(out, label);
81
+ }
82
+
83
+ // defineLang — the heart: turn any language into fenced `Xcall(file, func, ...args)` + `Xeval(expr)` natives.
84
+ // `callHarness`/`evalHarness` are programs IN that language implementing the framing protocol above.
85
+ function defineLang({ name, cmd, flag, callHarness, evalHarness }) {
86
+ const call = (args) => {
87
+ const [file, func, ...rest] = args;
88
+ if (typeof file !== 'string' || typeof func !== 'string') throw new MyxoError(`${name}call(file, func, ...args) needs file and func as strings`);
89
+ return runHarness(cmd, flag, callHarness, [file, func, toJson(rest.map(a => nxToJs(a)))], name);
90
+ };
91
+ const evl = (args) => {
92
+ if (typeof args[0] !== 'string') throw new MyxoError(`${name}eval(expr) needs a string`);
93
+ return runHarness(cmd, flag, evalHarness, [args[0]], name);
94
+ };
95
+ return { call, eval: evl };
96
+ }
97
+
98
+ // ---- Python (python -c) — json.dumps(allow_nan=False): non-finite -> clean framed error; os._exit -> no trailing spoof
99
+ const PY = process.platform === 'win32' ? 'python' : 'python3';
100
+ const python = defineLang({
101
+ name: 'python', cmd: PY, flag: '-c',
102
+ callHarness: [
103
+ 'import sys, json, os, importlib.util',
104
+ 'p, fn, a = sys.argv[1], sys.argv[2], sys.argv[3]',
105
+ 'try:',
106
+ ' spec = importlib.util.spec_from_file_location("nx_mod", p)',
107
+ ' m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)',
108
+ ' o = "\\u0001" + json.dumps(getattr(m, fn)(*json.loads(a)), allow_nan=False)',
109
+ 'except Exception as e:',
110
+ ' o = "\\u0002" + str(e).replace("\\u0001", "").replace("\\u0002", "")',
111
+ 'sys.stdout.write(o); sys.stdout.flush(); os._exit(0)',
112
+ ].join('\n'),
113
+ evalHarness: [
114
+ 'import sys, json, os',
115
+ 'try: o = "\\u0001" + json.dumps(eval(sys.argv[1]), allow_nan=False)',
116
+ 'except Exception as e: o = "\\u0002" + str(e).replace("\\u0001", "").replace("\\u0002", "")',
117
+ 'sys.stdout.write(o); sys.stdout.flush(); os._exit(0)',
118
+ ].join('\n'),
119
+ });
120
+
121
+ // ---- Node (node -e) — the .js file must be a CommonJS module exporting the function; fs.writeSync + process.exit
122
+ const node = defineLang({
123
+ name: 'node', cmd: 'node', flag: '-e',
124
+ callHarness: [
125
+ "const p=require('path'),fs=require('fs');",
126
+ "const J=x=>JSON.stringify(x,(k,v)=>{if(typeof v==='number'&&!isFinite(v))throw new Error('non-finite number');return v;});",
127
+ "try{const m=require(p.resolve(process.argv[1]));",
128
+ "let r=m[process.argv[2]](...JSON.parse(process.argv[3]));if(r===undefined)r=null;",
129
+ "fs.writeSync(1,'\\u0001'+J(r));}",
130
+ "catch(e){fs.writeSync(1,'\\u0002'+String(e&&e.message||e).replace(/[\\u0001\\u0002]/g,''));}process.exit(0);",
131
+ ].join(''),
132
+ evalHarness: [
133
+ "const fs=require('fs');",
134
+ "const J=x=>JSON.stringify(x,(k,v)=>{if(typeof v==='number'&&!isFinite(v))throw new Error('non-finite number');return v;});",
135
+ "try{let r=(0,eval)(process.argv[1]);if(r===undefined)r=null;",
136
+ "fs.writeSync(1,'\\u0001'+J(r));}",
137
+ "catch(e){fs.writeSync(1,'\\u0002'+String(e&&e.message||e).replace(/[\\u0001\\u0002]/g,''));}process.exit(0);",
138
+ ].join(''),
139
+ });
140
+
141
+ // ---- Perl (perl -e) — `do $file` loads subs; return a scalar or a ref (arrayref/hashref) for structured data
142
+ const perl = defineLang({
143
+ name: 'perl', cmd: 'perl', flag: '-e',
144
+ callHarness: [
145
+ "use JSON::PP; use File::Spec; use POSIX; $|=1; my($f,$fn,$a)=@ARGV;",
146
+ "my $j=JSON::PP->new->allow_nonref->canonical; my $abs=File::Spec->rel2abs($f);", // '.' left @INC in modern perl -> absolutize
147
+ "my $r=eval{ do $abs; die $@ if $@; no strict 'refs'; &{$fn}(@{$j->decode($a)}) };",
148
+ "if($@){ my $m=$@; $m=~s/[\\x01\\x02]//g; print \"\\x02\".$m } else { print \"\\x01\".$j->encode($r) } POSIX::_exit(0);",
149
+ ].join(' '),
150
+ evalHarness: [
151
+ "use JSON::PP; use POSIX; $|=1; my $j=JSON::PP->new->allow_nonref->canonical;",
152
+ "my $r=eval $ARGV[0]; if($@){ my $m=$@; $m=~s/[\\x01\\x02]//g; print \"\\x02\".$m } else { print \"\\x01\".$j->encode($r) } POSIX::_exit(0);",
153
+ ].join(' '),
154
+ });
155
+
156
+ const pycall = python.call, pyeval = python.eval;
157
+ const jscall = node.call, jseval = node.eval;
158
+ const plcall = perl.call, pleval = perl.eval;
159
+
160
+ // argv is strings: scalars stringify; structured values go as JSON (no silent flattening to "[object Object]")
161
+ function toArg(a) {
162
+ if (a === VOID) return '';
163
+ if (Array.isArray(a) || a instanceof Map) return JSON.stringify(nxToJs(a));
164
+ return String(a);
165
+ }
166
+ // bridgeExec('/path/to/binary' [, fixedArgs]) -> an Myxo native; how a compiled C++/Go/Rust binary or CLI plugs in
167
+ function bridgeExec(cmd, fixedArgs = []) {
168
+ return function (args) {
169
+ let out;
170
+ try { out = execFileSync(cmd, [...fixedArgs, ...args.map(toArg)], { encoding: 'utf8', timeout: TIMEOUT, maxBuffer: CAP }); }
171
+ catch (e) { throw new MyxoError(`exec '${cmd}' failed: ` + detail(e)); }
172
+ return out.replace(/\r?\n$/, '');
173
+ };
174
+ }
175
+ // sh(command) -> run a shell command line, return stdout. (Shell parsing: hand only trusted strings.)
176
+ function sh(args) {
177
+ if (typeof args[0] !== 'string') throw new MyxoError('sh(command) needs a string');
178
+ try { return execSync(args[0], { encoding: 'utf8', timeout: TIMEOUT, maxBuffer: CAP }).replace(/\r?\n$/, ''); }
179
+ catch (e) { throw new MyxoError('shell failed: ' + detail(e)); }
180
+ }
181
+
182
+ // Convenience: wire the bridges into an interpreter as FENCED capabilities (still gated by `needs`).
183
+ function installPolyglot(interp, extra = {}) {
184
+ const fns = { pycall, pyeval, jscall, jseval, plcall, pleval, sh };
185
+ for (const [name, fn] of Object.entries({ ...fns, ...extra })) interp.registerNative(name, fn, true);
186
+ }
187
+
188
+ module.exports = {
189
+ pycall, pyeval, jscall, jseval, plcall, pleval, sh, bridgeExec,
190
+ defineLang, installPolyglot, nxToJs, jsToNx,
191
+ };
package/receipt.js ADDED
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+ // receipt.js -- a tamper-evident RECEIPT over an Myxo audit ledger.
3
+ //
4
+ // An Myxo run produces an audit array of { cap, args[], ok, error? } entries (every fenced capability call, allowed or
5
+ // refused). This turns that ledger into a proof: a sha256 hash CHAIN (each entry commits the previous), SEALED with an
6
+ // HMAC over {count, root} using a HOST-HELD key. In-place edits, reordering, and re-chaining change the root; dropping
7
+ // or adding entries changes the count; and neither can be re-sealed without the key. So a saved/shared receipt can be
8
+ // verified later as an untampered record of exactly what a (possibly untrusted) script did.
9
+ //
10
+ // Shared by Warden (warden/warden.js) and JAMES (james_nx_run) so both speak the same receipt.
11
+
12
+ const crypto = require('crypto');
13
+ const fs = require('fs');
14
+
15
+ const GENESIS = crypto.createHash('sha256').update('nx-receipt-genesis-v1').digest('hex');
16
+
17
+ // exactly the fields the receipt retains + reconciles on; any change to these breaks the chain
18
+ function bodyOf(e) {
19
+ return JSON.stringify({ seq: e.seq, cap: e.cap, args: e.args, ok: e.ok, error: e.error || null });
20
+ }
21
+
22
+ function chain(audit) {
23
+ let prev = GENESIS;
24
+ return (audit || []).map((e, i) => {
25
+ const entry = { seq: i, cap: e.cap, args: e.args, ok: e.ok, error: e.error || null, prev };
26
+ entry.hash = crypto.createHash('sha256').update(prev + bodyOf(entry)).digest('hex');
27
+ prev = entry.hash;
28
+ return entry;
29
+ });
30
+ }
31
+
32
+ // commit LENGTH + final ROOT under an HMAC. Truncation changes count; any edit/re-chain changes root; forging needs the key.
33
+ function sealReceipt(receipt, key) {
34
+ const root = receipt.length ? receipt[receipt.length - 1].hash : GENESIS;
35
+ return { count: receipt.length, root, mac: crypto.createHmac('sha256', key).update(receipt.length + ':' + root).digest('hex') };
36
+ }
37
+
38
+ function verifyChain(receipt, seal, key) {
39
+ let prev = GENESIS;
40
+ for (const e of receipt) {
41
+ if (e.prev !== prev) return { ok: false, brokeAt: e.seq, why: 'prev-hash mismatch' };
42
+ const h = crypto.createHash('sha256').update(prev + bodyOf(e)).digest('hex');
43
+ if (h !== e.hash) return { ok: false, brokeAt: e.seq, why: 'entry hash mismatch (tampered)' };
44
+ prev = e.hash;
45
+ }
46
+ if (seal) {
47
+ if (seal.count !== receipt.length) return { ok: false, brokeAt: receipt.length, why: 'length seal mismatch (truncated/extended)' };
48
+ if (seal.root !== prev) return { ok: false, brokeAt: receipt.length, why: 'root seal mismatch' };
49
+ if (key) {
50
+ const good = crypto.createHmac('sha256', key).update(seal.count + ':' + seal.root).digest('hex');
51
+ if (good !== seal.mac) return { ok: false, brokeAt: receipt.length, why: 'HMAC seal mismatch (forged without the key)' };
52
+ }
53
+ }
54
+ return { ok: true, root: prev };
55
+ }
56
+
57
+ // a persistent host key: prefer an env var (hex), else a 0600 keyfile created once. Persistent so receipts verify later.
58
+ function hostKey(opts = {}) {
59
+ const envName = opts.env || 'NX_RECEIPT_KEY';
60
+ if (process.env[envName]) return Buffer.from(process.env[envName], 'hex');
61
+ const file = opts.file;
62
+ if (file) {
63
+ try { if (fs.existsSync(file)) return Buffer.from(fs.readFileSync(file, 'utf8').trim(), 'hex'); } catch {}
64
+ const key = crypto.randomBytes(32);
65
+ try { fs.writeFileSync(file, key.toString('hex'), { mode: 0o600 }); } catch {}
66
+ return key;
67
+ }
68
+ return crypto.randomBytes(32); // ephemeral (verify only within this process)
69
+ }
70
+
71
+ module.exports = { GENESIS, bodyOf, chain, sealReceipt, verifyChain, hostKey };
package/std.myx ADDED
@@ -0,0 +1,88 @@
1
+ # std.myx — the Myxo standard library, written in Myxo itself.
2
+ # Loaded before every program. These agents lean on the native builtins
3
+ # (push, len, range, keys...) but everything here is plain Myxo.
4
+
5
+ # Apply an agent to every item, collecting the results.
6
+ agent map(xs, f) {
7
+ seed out = []
8
+ for each x in xs {
9
+ push(out, f(x))
10
+ }
11
+ report out
12
+ }
13
+
14
+ # Keep only the items an agent says are live.
15
+ agent filter(xs, f) {
16
+ seed out = []
17
+ for each x in xs {
18
+ when f(x) {
19
+ push(out, x)
20
+ }
21
+ }
22
+ report out
23
+ }
24
+
25
+ # Fold a list down to a single value.
26
+ agent reduce(xs, start, f) {
27
+ seed acc = start
28
+ for each x in xs {
29
+ acc = f(acc, x)
30
+ }
31
+ report acc
32
+ }
33
+
34
+ # Sum of a list of numbers.
35
+ agent sum(xs) {
36
+ report reduce(xs, 0, agent(a, b) { report a + b })
37
+ }
38
+
39
+ # Largest value in a list.
40
+ agent biggest(xs) {
41
+ seed best = xs[0]
42
+ for each x in xs {
43
+ when x > best {
44
+ best = x
45
+ }
46
+ }
47
+ report best
48
+ }
49
+
50
+ # Is a value present in a list?
51
+ agent contains(xs, target) {
52
+ for each x in xs {
53
+ when x == target {
54
+ report live
55
+ }
56
+ }
57
+ report dead
58
+ }
59
+
60
+ # Count items that satisfy an agent.
61
+ agent count(xs, f) {
62
+ seed n = 0
63
+ for each x in xs {
64
+ when f(x) {
65
+ n = n + 1
66
+ }
67
+ }
68
+ report n
69
+ }
70
+
71
+ # — constants —
72
+ seed PI = 3.141592653589793
73
+ seed E = 2.718281828459045
74
+
75
+ # First and last of a list.
76
+ agent first(xs) { report xs[0] }
77
+ agent last(xs) { report xs[len(xs) - 1] }
78
+
79
+ # Keep a number within a range.
80
+ agent clamp(x, lo, hi) {
81
+ when x < lo { report lo }
82
+ when x > hi { report hi }
83
+ report x
84
+ }
85
+
86
+ # Split text into words and lines (leans on the native split/trim).
87
+ agent words(s) { report split(trim(s), " ") }
88
+ agent lines(s) { report split(s, "\n") }
@@ -0,0 +1,224 @@
1
+ 'use strict';
2
+ // ---------------------------------------------------------------------------------------------------
3
+ // Memo-soundness differential fuzzer for Myxo.
4
+ //
5
+ // Myxo's load-bearing promise: memoization is INVISIBLE. A program's output with the hot-promotion
6
+ // memoizer ON (promoteAt = 2) must equal its output with it OFF (promoteAt = 1e9). Every memo bug we
7
+ // have found is a violation of exactly that equality:
8
+ // router-freeze .......... a pure agent wrapping route() cached the router's first pick forever
9
+ // prune/metabolize ghost .. a reaped/rebound dependency kept being served from a stale cache
10
+ // keyOf collision ......... two distinct arg lists forged the same memo key (\x1f join; -0 vs 0)
11
+ // Instead of one hand-written repro per bug, we GENERATE programs that stress the memoizer, run each
12
+ // ON and OFF, and flag any that disagree. It reproduces the router and keyOf classes on its own when
13
+ // their fixes are reverted (see `node tools/memo-fuzz.js` against a reverted build), and the `reap`
14
+ // family guards the epoch-invalidation class the same way.
15
+ //
16
+ // HONEST SCOPE: the invariant is "memo-invisible EXCEPT for documented mesh introspection" — strength()
17
+ // / mesh() / metabolize()-return values are deliberately allowed to observe promotion (interpreter.js
18
+ // disclaims mesh-strength invisibility). The generators therefore never EMIT those introspection values,
19
+ // so the ON==OFF oracle stays a true soundness test and not a false alarm on intended behavior.
20
+ //
21
+ // COVERAGE IS MEASURED, NOT ASSUMED: each program's ON run reports its real memo cache-hit count, and a
22
+ // family that never exercises the cache (hollow) is a test failure — that is exactly how the original
23
+ // `reap` family (a free-DATA read that tainted purity and never promoted) hid behind a green suite.
24
+ //
25
+ // Pure JS, zero deps. Deterministic (seeded PRNG) so every failure reproduces byte-for-byte.
26
+ // ---------------------------------------------------------------------------------------------------
27
+
28
+ const { makeInterpreter } = require('../myxo');
29
+ const { parse } = require('../parser');
30
+
31
+ // --- deterministic PRNG (mulberry32) ---------------------------------------------------------------
32
+ function makeRng(seed) {
33
+ let s = seed >>> 0;
34
+ return () => {
35
+ s = (s + 0x6D2B79F5) >>> 0;
36
+ let t = s;
37
+ t = Math.imul(t ^ (t >>> 15), t | 1);
38
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
39
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
40
+ };
41
+ }
42
+ const pick = (rnd, xs) => xs[Math.floor(rnd() * xs.length)];
43
+ const rint = (rnd, lo, hi) => lo + Math.floor(rnd() * (hi - lo + 1));
44
+
45
+ // --- run one program and report output + program-attributable memo cache HITS ----------------------
46
+ // Replicates myxo.run()'s capture mode, but keeps the interpreter so we can read its memo records.
47
+ // Boot hits (stdlib promoting during install) are discounted so `hits` reflects only the program.
48
+ function runStats(src, promoteAt) {
49
+ const chunks = [];
50
+ const interp = makeInterpreter({ output: (s) => chunks.push(s), promoteAt });
51
+ let base = 0;
52
+ for (const rec of interp.memo.values()) base += rec.hits;
53
+ let r;
54
+ try { interp.run(parse(src)); r = { ok: true, v: chunks.join('') }; }
55
+ catch (e) { r = { ok: false, v: String((e && e.message) || e) }; }
56
+ let hits = 0;
57
+ for (const rec of interp.memo.values()) hits += rec.hits;
58
+ r.hits = hits - base;
59
+ return r;
60
+ }
61
+
62
+ // --- the oracle: one ON-vs-OFF differential run ----------------------------------------------------
63
+ function differential(src) {
64
+ const on = runStats(src, 2); // memoizer ON (default hot-promote threshold)
65
+ const off = runStats(src, 1e9); // memoizer effectively OFF
66
+ const violation = (on.ok !== off.ok || on.v !== off.v) ? { on, off } : null;
67
+ return { violation, onHits: on.hits };
68
+ }
69
+
70
+ // "nasty" bytes: content that looks like keyOf's own tags/separator — most able to forge a key boundary.
71
+ const SEP = '\x1f';
72
+ // Myxo string literal preserving RAW bytes (incl. control bytes like \x1f). JSON.stringify would escape
73
+ // \x1f to a 6-char sequence and defuse the very collision we hunt; only " and \ need escaping.
74
+ const q = (s) => '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
75
+
76
+ // --- FAMILY: keyOf boundary-forge (string separator collision) -------------------------------------
77
+ // Two DISTINCT arg lists that collide under a naive separator-join key:
78
+ // f(p+SEP+"s:"+w, r) and f(p, w+SEP+"s:"+r) both flatten to s:<p><SEP>s:<w><SEP>s:<r>.
79
+ function genKeyCollision(rnd) {
80
+ const p = pick(rnd, ['a', 'xy', '', 'k']);
81
+ const w = pick(rnd, ['b', 'q', '', 'm']);
82
+ const r = pick(rnd, ['c', 'r', 'z', '']);
83
+ const a1 = p + SEP + 's:' + w, b1 = r;
84
+ const a2 = p, b2 = w + SEP + 's:' + r;
85
+ return [
86
+ 'agent f(a, b){ report a }',
87
+ `emit str(f(${q(a1)}, ${q(b1)}))`,
88
+ `emit str(f(${q(a1)}, ${q(b1)}))`,
89
+ `emit str(f(${q(a1)}, ${q(b1)}))`, // promote + cache under the key
90
+ `emit str(f(${q(a2)}, ${q(b2)}))`, // distinct args, old-collision -> must NOT return the cached value
91
+ ].join('\n');
92
+ }
93
+
94
+ // --- FAMILY: keyOf numeric collision (-0 vs 0) -----------------------------------------------------
95
+ // pow(x,-1) is sign-observable: pow(0,-1)=Infinity, pow(-0,-1)=-Infinity. String(-0)==="0", so a naive
96
+ // number tag keys -0 and 0 the same and a cached f(0) ghost-answers f(-0). Deterministically pair them.
97
+ function genNumeric(rnd) {
98
+ const zeroFirst = rnd() < 0.5;
99
+ const first = zeroFirst ? '0' : '(0-1)*0'; // (0-1)*0 evaluates to -0
100
+ const second = zeroFirst ? '(0-1)*0' : '0';
101
+ return [
102
+ 'agent f(x){ report pow(x, 0-1) }',
103
+ `emit str(f(${first}))`,
104
+ `emit str(f(${first}))`,
105
+ `emit str(f(${first}))`, // promote + cache under the numeric key
106
+ `emit str(f(${second}))`, // sign partner: a -0/0 key collision ghosts the cached answer
107
+ ].join('\n');
108
+ }
109
+
110
+ // --- FAMILY: router-freeze -------------------------------------------------------------------------
111
+ // A pure agent wraps route() over two equally-good providers, driven with a constant arg. Sound runs
112
+ // share traffic (the wrapping agent must be tainted, not cached); a frozen run monopolizes one provider.
113
+ function genRouter(rnd) {
114
+ const drives = rint(rnd, 20, 60);
115
+ const arg = rint(rnd, 0, 3);
116
+ return [
117
+ 'agent A(x){ report "A" }',
118
+ 'agent B(x){ report "B" }',
119
+ 'seed r = route("fz", [A, B])',
120
+ 'agent ask(q){ report r(q) }',
121
+ 'seed g = []',
122
+ `reinforce ${drives} times { push(g, ask(${arg})) }`,
123
+ 'seed nA = count(g, agent(v){ report v == "A" })', // count() promotes -> real cache hits
124
+ 'emit nA > 0, nA < ' + drives, // sound: both seen -> "live live"; frozen -> a "dead"
125
+ ].join('\n');
126
+ }
127
+
128
+ // --- FAMILY: reap / epoch invalidation -------------------------------------------------------------
129
+ // f reads a FREE AGENT g (the recursion exemption keeps f pure, so it genuinely PROMOTES and caches).
130
+ // Then a dependency change must bump the epoch and drop f's stale cache: either g is rebound, or the
131
+ // mesh is metabolized. If epoch invalidation regresses, ON ghost-serves the old answer -> divergence.
132
+ function genReap(rnd) {
133
+ const n1 = rint(rnd, 1, 9), n2 = rint(rnd, 10, 99), a = rint(rnd, 0, 3);
134
+ const base = [
135
+ `agent g(x){ report x + ${n1} }`,
136
+ 'agent f(x){ report g(x) }',
137
+ `emit f(${a})`, `emit f(${a})`, `emit f(${a})`, // promote + cache f(${a}) under the current g
138
+ ];
139
+ if (rnd() < 0.5) {
140
+ // ghost-rebind: redefining g must invalidate f's cache (epoch bump)
141
+ return base.concat([`agent g(x){ report x + ${n2} }`, `emit f(${a})`]).join('\n');
142
+ }
143
+ // ghost-reap: metabolize reaps the mesh; a sound epoch invalidates so f re-runs (and finds g reaped)
144
+ return base.concat(['seed _ = metabolize(1000)', `attempt { emit f(${a}) } rescue e { emit "reaped" }`]).join('\n');
145
+ }
146
+
147
+ // --- FAMILY: broad nasty-arg coverage (pure agents, mixed types) -----------------------------------
148
+ function genNasty(rnd) {
149
+ const arity = rint(rnd, 1, 3);
150
+ const params = ['a', 'b', 'c'].slice(0, arity);
151
+ const rep = pick(rnd, params);
152
+ const nasty = ['', 'a', SEP, 's:', 'n:1', 'v', 'b:true', 'a' + SEP + 's:b', SEP + 's:', 's:' + SEP];
153
+ const lit = () => {
154
+ const k = rnd();
155
+ if (k < 0.55) return q(pick(rnd, nasty));
156
+ if (k < 0.8) return String(rint(rnd, -3, 3)); // small ints incl. negatives
157
+ return pick(rnd, ['void', 'live', 'dead', '(0-1)*0']); // incl. -0
158
+ };
159
+ const lines = [`agent f(${params.join(', ')}){ report ${rep} }`];
160
+ const nCalls = rint(rnd, 4, 9);
161
+ const pools = [];
162
+ for (let i = 0; i < 3; i++) pools.push(params.map(lit).join(', ')); // repeated lists -> promotion
163
+ for (let i = 0; i < nCalls; i++) {
164
+ const args = rnd() < 0.55 ? pick(rnd, pools) : params.map(lit).join(', ');
165
+ lines.push(`emit str(f(${args}))`);
166
+ }
167
+ return lines.join('\n');
168
+ }
169
+
170
+ const FAMILIES = [
171
+ { name: 'keyCollision', gen: genKeyCollision, weight: 3 },
172
+ { name: 'numeric', gen: genNumeric, weight: 2 },
173
+ { name: 'router', gen: genRouter, weight: 3 },
174
+ { name: 'reap', gen: genReap, weight: 2 },
175
+ { name: 'nasty', gen: genNasty, weight: 2 },
176
+ ];
177
+ const WEIGHTED = FAMILIES.flatMap((f) => Array(f.weight).fill(f));
178
+
179
+ // --- driver ----------------------------------------------------------------------------------------
180
+ // fuzz({ seed, count }) -> { checked, violations, byFamily, byFamilyHits }
181
+ // byFamily = programs generated per family
182
+ // byFamilyHits = programs per family whose ON run reached >=1 real memo cache hit (hollow-family guard)
183
+ function fuzz({ seed = 0x1CE, count = 400 } = {}) {
184
+ const rnd = makeRng(seed);
185
+ const violations = [];
186
+ const byFamily = Object.fromEntries(FAMILIES.map((f) => [f.name, 0]));
187
+ const byFamilyHits = Object.fromEntries(FAMILIES.map((f) => [f.name, 0]));
188
+ let checked = 0;
189
+ for (let i = 0; i < count; i++) {
190
+ const fam = pick(rnd, WEIGHTED);
191
+ const src = fam.gen(rnd);
192
+ checked++;
193
+ byFamily[fam.name]++;
194
+ const { violation, onHits } = differential(src);
195
+ if (onHits > 0) byFamilyHits[fam.name]++;
196
+ if (violation) violations.push({ family: fam.name, src, on: violation.on, off: violation.off });
197
+ }
198
+ return { checked, violations, byFamily, byFamilyHits };
199
+ }
200
+
201
+ module.exports = { fuzz, differential, runStats, makeRng };
202
+
203
+ // --- CLI: `node tools/memo-fuzz.js [seed] [count]` -------------------------------------------------
204
+ if (require.main === module) {
205
+ const seed = process.argv[2] ? (parseInt(process.argv[2], 16) || parseInt(process.argv[2], 10)) : 0x1CE;
206
+ const count = process.argv[3] ? parseInt(process.argv[3], 10) : 400;
207
+ const { checked, violations, byFamily, byFamilyHits } = fuzz({ seed, count });
208
+ console.log(`memo-soundness fuzz: seed=0x${seed.toString(16)} programs=${checked}`);
209
+ console.log(` generated : ${JSON.stringify(byFamily)}`);
210
+ console.log(` cache-hit : ${JSON.stringify(byFamilyHits)} (programs that reached >=1 real memo hit)`);
211
+ if (!violations.length) {
212
+ console.log(`RESULT: SOUND — memo-ON == memo-OFF on all ${checked} programs.`);
213
+ process.exit(0);
214
+ }
215
+ console.log(`RESULT: ${violations.length} SOUNDNESS VIOLATION(S) — memo corrupted a result:`);
216
+ for (const v of violations.slice(0, 5)) {
217
+ console.log('\n--- family:', v.family, '---');
218
+ console.log(v.src);
219
+ console.log(' memo-ON :', JSON.stringify(v.on.ok ? v.on.v : 'THREW ' + v.on.v));
220
+ console.log(' memo-OFF:', JSON.stringify(v.off.ok ? v.off.v : 'THREW ' + v.off.v));
221
+ }
222
+ if (violations.length > 5) console.log(`\n... and ${violations.length - 5} more.`);
223
+ process.exit(1);
224
+ }