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,75 @@
1
+ # physarum.myx — the slime mold's law, run properly, in the language named after it.
2
+ #
3
+ # Physarum polycephalum has no brain, yet it rebuilds the Tokyo rail network:
4
+ # it floods every route, then each tube THICKENS with the flow it carries and
5
+ # THINS continuously on its own. Useful paths reinforce; the rest decay and
6
+ # vanish. Tero (2010) showed the network's character is set by HOW flow feeds
7
+ # back into a tube: sharp feedback (flow squared) → one winner survives (lean);
8
+ # soft feedback (sqrt of flow) → the detours stay alive as backups (resilient).
9
+ # One organism, one dial. Myxo's old mesh did a toy version (+1 per read, manual
10
+ # prune); this is the real dynamic, and the dial is passed in as a function.
11
+ #
12
+ # Run: node myxo.js examples/physarum.myx
13
+
14
+ # Two food sources (A, B). Each reachable by a short DIRECT route and a longer
15
+ # DETOUR. Shorter tubes conduct more flow, so direct routes win — but how hard
16
+ # they win depends entirely on the feedback shape.
17
+ seed len = { "A.direct": 1, "A.detour": 3, "B.direct": 1, "B.detour": 4 }
18
+ seed dest = { "A.direct": "A", "A.detour": "A", "B.direct": "B", "B.detour": "B" }
19
+ seed dests = ["A", "B"]
20
+ seed REINFORCE = 0.9
21
+ seed FADE = 0.92 # gentle continuous decay — same for both runs
22
+
23
+ agent fresh() {
24
+ report { "A.direct": 1, "A.detour": 1, "B.direct": 1, "B.detour": 1 }
25
+ }
26
+
27
+ # One forage cycle. `shape` is the feedback law — the dial Tero found.
28
+ # Flow splits across a destination's routes by conductance (strength / length);
29
+ # each tube thickens by REINFORCE * shape(flow); then EVERY tube thins by FADE.
30
+ agent step(cond, shape) {
31
+ for each d in dests {
32
+ seed total = 0
33
+ for each r in keys(cond) {
34
+ when dest[r] == d { total = total + (cond[r] / len[r]) }
35
+ }
36
+ for each r in keys(cond) {
37
+ when dest[r] == d {
38
+ seed flow = (cond[r] / len[r]) / total
39
+ cond[r] = cond[r] + REINFORCE * shape(flow)
40
+ }
41
+ }
42
+ }
43
+ for each r in keys(cond) { cond[r] = cond[r] * FADE }
44
+ report cond
45
+ }
46
+
47
+ # Run the network to rest, then show each tube's share of its destination.
48
+ agent simulate(shape, cycles, label) {
49
+ seed cond = fresh()
50
+ seed t = 0
51
+ reinforce t < cycles {
52
+ cond = step(cond, shape)
53
+ t = t + 1
54
+ }
55
+ emit ""
56
+ emit label
57
+ for each d in dests {
58
+ seed tot = 0
59
+ for each r in keys(cond) { when dest[r] == d { tot = tot + cond[r] } }
60
+ for each r in keys(cond) {
61
+ when dest[r] == d {
62
+ seed pct = floor(cond[r] / tot * 100)
63
+ seed mark = "pruned"
64
+ when pct >= 20 { mark = "ALIVE" }
65
+ emit " " + r + " " + str(pct) + "% of route to " + d + " [" + mark + "]"
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ emit "Physarum routing in Myxo — same forage, one dial (the feedback shape):"
72
+ simulate(agent(q) { report q * q }, 18, "[ LEAN — sharp feedback (flow squared) ] one winner, detours starve:")
73
+ simulate(agent(q) { report sqrt(q) }, 18, "[ RESILIENT — soft feedback (sqrt flow) ] detours kept as live backups:")
74
+ emit ""
75
+ emit "Brainless. One rule. Efficiency or fault-tolerance, by a single dial — exactly what Tero found."
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+ // Host: wire the language runners as FENCED capabilities, run the .myx, print the audit ledger.
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { run } = require('../myxo');
6
+ const { pycall, pyeval, jscall, jseval, plcall, pleval, sh } = require('../polyglot');
7
+
8
+ let audit = [];
9
+ const src = fs.readFileSync(path.join(__dirname, 'polyglot.myx'), 'utf8');
10
+ const out = run(src, {
11
+ capture: true,
12
+ natives: { pycall, pyeval, jscall, jseval, plcall, pleval, sh }, // language runners -> fenced capabilities (gated by `needs`)
13
+ onAudit: (l) => { audit = l; },
14
+ });
15
+ process.stdout.write(out);
16
+ console.log('--- audit ledger (the law saw every cross-language call) ---');
17
+ for (const e of audit) console.log(' ', e.cap, e.ok ? 'OK -> ' + e.result : 'REFUSED (' + e.error + ')');
@@ -0,0 +1,11 @@
1
+ # Myxo as the connective layer for ALL languages: each language is a FENCED pathway, governed by the one law.
2
+ # Python, Node, and Perl plug in through the SAME contract (defineLang); one audit ledger records every call.
3
+ needs pycall, jscall, plcall, sh
4
+
5
+ emit "python add =", pycall("examples/mathlib.py", "add", 2, 3)
6
+ emit "node add =", jscall("examples/mathlib.js", "add", 10, 20)
7
+ emit "perl add =", plcall("examples/mathlib.pl", "add", 4, 5)
8
+
9
+ seed s = jscall("examples/mathlib.js", "stats", [10, 4, 7, 2])
10
+ emit "node stats: sum =", s["sum"], " max =", s["max"], " n =", s["n"]
11
+ emit "shell =", sh("echo one-contract-many-languages")
@@ -0,0 +1,28 @@
1
+ # resilient.myx — error handling + the expanded stdlib, the Nexus way.
2
+ # Shows: words/split, mesh-as-counter, value-returning `or`, attempt/rescue/fail.
3
+
4
+ # Rank words by how often they appear (split + mesh + the `or default` idiom).
5
+ agent word_counts(text) {
6
+ seed counts = {}
7
+ for each w in words(lower(text)) {
8
+ counts[w] = (counts[w] or 0) + 1 # missing key is void; void or 0 -> 0
9
+ }
10
+ report counts
11
+ }
12
+
13
+ seed text = "the mesh reinforces the path the mesh keeps"
14
+ emit "counts:", word_counts(text)
15
+
16
+ # A safe lookup: a pathway that might not exist, handled instead of crashing.
17
+ agent safe_strength(counts, key) {
18
+ attempt {
19
+ when not has(counts, key) { fail "no such pathway: " + key }
20
+ report counts[key]
21
+ } rescue err {
22
+ report err["message"] # turn a failure into a graceful value
23
+ }
24
+ }
25
+
26
+ seed counts = word_counts(text)
27
+ emit safe_strength(counts, "mesh") # present -> 2
28
+ emit safe_strength(counts, "wallet") # absent -> a clean message, no crash
@@ -0,0 +1,46 @@
1
+ # scheduler.myx — the Physarum scheduler: the reinforce/decay law applied to EXECUTION.
2
+ # `schedule(name, workers, items)` distributes a batch across a learning pool of worker-agents, runs the
3
+ # chunks on real worker threads IN PARALLEL, and updates each worker's conductance from its measured speed —
4
+ # so fast workers pull more of the flux on later calls, and a failed worker decays and its items reroute.
5
+ #
6
+ # A worker takes a CHUNK (a list of items) and returns a results list — one result per item, in order.
7
+
8
+ # --- a simple homogeneous pool: square every number, across two workers, in parallel ---
9
+ agent square(chunk) {
10
+ seed out = []
11
+ for each x in chunk { out = out + [x * x] }
12
+ report out
13
+ }
14
+
15
+ emit "squares: " + str(schedule("squares", [square, square], [1, 2, 3, 4, 5, 6, 7, 8]))
16
+
17
+ # --- a heterogeneous pool: one fast worker, one slow worker; watch the pool LEARN ---
18
+ agent fast(chunk) {
19
+ seed out = []
20
+ for each x in chunk { out = out + [x] }
21
+ report out
22
+ }
23
+ agent slow(chunk) {
24
+ seed out = []
25
+ for each x in chunk {
26
+ seed j = 0
27
+ reinforce j < 300000 { j = j + 1 } # this tube is genuinely slower
28
+ out = out + [x]
29
+ }
30
+ report out
31
+ }
32
+
33
+ # run a few batches so the pool can measure speed and re-balance the flux
34
+ reinforce 4 times {
35
+ schedule("work", [fast, slow], [1, 2, 3, 4, 5, 6, 7, 8])
36
+ }
37
+ emit "learned conductances (fast should dominate): " + str(flows("work"))
38
+
39
+ # --- failover: a dead tube; its work reroutes to the survivor, result intact ---
40
+ agent boom(chunk) { fail "this tube is dead" }
41
+ agent ok(chunk) {
42
+ seed out = []
43
+ for each x in chunk { out = out + [x * 10] }
44
+ report out
45
+ }
46
+ emit "failover (boom dies, ok takes over): " + str(schedule("resilient", [boom, ok], [1, 2, 3]))
@@ -0,0 +1,27 @@
1
+ # the-law.myx — the core law of the Nexus, executed.
2
+ # "Agents do not merge. Useful pathways reinforce. Useless pathways decay."
3
+ # Run with --trace to watch the mesh: node myxo.js examples/the-law.myx --trace
4
+
5
+ agent reinforce_path(name, pulses) {
6
+ seed strength = 0
7
+ reinforce strength < pulses {
8
+ strength = strength + 1
9
+ }
10
+ report name + " reinforced to " + str(strength)
11
+ }
12
+
13
+ seed telegram = reinforce_path("telegram", 5)
14
+ seed wallet = reinforce_path("wallet", 3)
15
+ seed bridge = reinforce_path("bridge", 1)
16
+
17
+ emit telegram
18
+ emit wallet
19
+ emit bridge
20
+
21
+ # Useful pathways reinforce through use — reading telegram raises its strength.
22
+ emit telegram
23
+ emit telegram
24
+
25
+ # A pathway gone quiet is let go.
26
+ decay bridge
27
+ emit "bridge has decayed from the mesh"
@@ -0,0 +1,9 @@
1
+ # use-geo.myx — weave a strand and use what it exposes (v0.3 modules).
2
+
3
+ weave "geo.myx" # flat: pulls area_circle, area_rect, TAU into scope
4
+ emit round(area_circle(2), 2) # PI * 4 -> 12.57
5
+ emit area_rect(3, 4) # 12
6
+ emit round(TAU, 4) # 6.2832
7
+
8
+ weave "geo.myx" as geo # namespaced: the exposed names as a mesh
9
+ emit round(geo["area_circle"](1), 2) # PI -> 3.14
package/format.js ADDED
@@ -0,0 +1,206 @@
1
+ 'use strict';
2
+ // format.js - canonical Myxo source printer. It formats the parsed AST, so invalid code is refused instead of
3
+ // guessed through. Comments ARE preserved: they aren't in the AST, so we collect them from the REAL lexer
4
+ // (string/interpolation-aware) and re-attach by line — own-line comments above the next statement, a trailing
5
+ // comment on its statement's line. v1 scope is statement-granular: none are ever dropped or fabricated, but a
6
+ // comment trailing a one-line block or a multi-line construct, or one inside an inline `agent(){}` expression,
7
+ // may render on its own line / shift to the nearest statement boundary. `--drop-comments` strips them.
8
+
9
+ const { parse } = require('./parser');
10
+ const { tokenize } = require('./lexer');
11
+
12
+ const PREC = { or: 1, and: 2, '==': 3, '!=': 3, '>': 4, '<': 4, '>=': 4, '<=': 4, '+': 5, '-': 5, '*': 6, '/': 6, '%': 6 };
13
+ const IND = ' ';
14
+
15
+ function q(s) { return JSON.stringify(String(s)); }
16
+ function pad(n) { return IND.repeat(n); }
17
+
18
+ function formatSource(src, opts = {}) {
19
+ if (opts.dropComments) return formatProgram(parse(src), []);
20
+ const comments = [];
21
+ tokenize(src, comments); // the real, string-aware scanner — never mistakes a '#' inside a string for a comment
22
+ return formatProgram(parse(src), comments);
23
+ }
24
+
25
+ function newCtx(comments) { return { comments: comments || [], i: 0 }; }
26
+ function ownLineComments(ctx, beforeLine, level) { // flush every comment strictly before `beforeLine`, each on its own line
27
+ let s = '';
28
+ while (ctx.i < ctx.comments.length && ctx.comments[ctx.i].line < beforeLine) {
29
+ const c = ctx.comments[ctx.i++];
30
+ s += pad(level) + (c.text ? '# ' + c.text : '#') + '\n';
31
+ }
32
+ return s;
33
+ }
34
+ function trailingComment(ctx, line) { // a comment that trails code on this exact line
35
+ if (ctx.i < ctx.comments.length && ctx.comments[ctx.i].line === line && ctx.comments[ctx.i].trailing) {
36
+ const c = ctx.comments[ctx.i++];
37
+ return c.text ? ' # ' + c.text : ' #';
38
+ }
39
+ return '';
40
+ }
41
+
42
+ // A statement's LAST source line — its closing `}` for a block statement — so a comment trailing that line
43
+ // attaches to the whole statement, not to whatever inner statement happens to start on the same line.
44
+ function stmtEndLine(s) {
45
+ const b = (s.elseBlock && s.elseBlock.endLine) || (s.catchBlock && s.catchBlock.endLine)
46
+ || (s.body && s.body.endLine) || (s.thenBlock && s.thenBlock.endLine) || s.matchEnd;
47
+ return b || s.line;
48
+ }
49
+
50
+ function formatProgram(ast, comments = []) {
51
+ const ctx = newCtx(comments);
52
+ let out = '';
53
+ for (const s of ast.body) {
54
+ out += ownLineComments(ctx, s.line, 0) + fmtStmt(s, 0, ctx) + trailingComment(ctx, stmtEndLine(s)) + '\n';
55
+ }
56
+ out += ownLineComments(ctx, Infinity, 0); // footer / trailing comments after the last statement
57
+ return out;
58
+ }
59
+
60
+ function fmtBlock(body, level, ctx = newCtx()) {
61
+ const tail = body.endLine ? () => ownLineComments(ctx, body.endLine, level + 1).replace(/\n+$/, '') : () => '';
62
+ if (!body.length) { const t = tail(); return t ? ' {\n' + t + '\n' + pad(level) + '}' : ' {}'; }
63
+ const parts = body.map(s => ownLineComments(ctx, s.line, level + 1) + fmtStmt(s, level + 1, ctx) + trailingComment(ctx, stmtEndLine(s)));
64
+ let inner = parts.join('\n');
65
+ const t = tail(); // comments after the last statement but before the closing brace stay INSIDE
66
+ if (t) inner += '\n' + t;
67
+ return ' {\n' + inner + '\n' + pad(level) + '}';
68
+ }
69
+
70
+ function fmtParams(params) {
71
+ return '(' + params.map(p => {
72
+ if (p.rest) return '...' + p.name;
73
+ return p.name + (p.paramType ? ': ' + p.paramType : '') + (p.def != null ? ' = ' + fmtExpr(p.def) : '');
74
+ }).join(', ') + ')';
75
+ }
76
+
77
+ function fmtNeed(item) {
78
+ if (!item.limit) return item.name;
79
+ const parts = [];
80
+ if (Object.prototype.hasOwnProperty.call(item.limit, 'max')) parts.push('max ' + item.limit.max);
81
+ if (Object.prototype.hasOwnProperty.call(item.limit, 'total')) parts.push('total ' + item.limit.total);
82
+ return item.name + '(' + parts.join(', ') + ')';
83
+ }
84
+
85
+ function fmtStmt(s, level, ctx = newCtx()) {
86
+ const p = pad(level);
87
+ switch (s.type) {
88
+ case 'Seed': return p + `seed ${s.name}${s.declType ? ': ' + s.declType : ''} = ${fmtExpr(s.value)}`;
89
+ case 'SeedDestructure': return p + `seed ${fmtPattern(s.pattern)} = ${fmtExpr(s.value)}`;
90
+ case 'Decay': return p + `decay ${fmtExpr(s.target)}`;
91
+ case 'Emit': return p + 'emit ' + s.args.map(a => fmtExpr(a)).join(', ');
92
+ case 'When': {
93
+ let out = p + `when ${fmtExpr(s.cond)}` + fmtBlock(s.thenBlock, level, ctx);
94
+ if (s.elseBlock) {
95
+ if (s.elseBlock.length === 1 && s.elseBlock[0].type === 'When') {
96
+ out += ' otherwise ' + fmtStmt(s.elseBlock[0], 0, ctx);
97
+ } else {
98
+ out += ' otherwise' + fmtBlock(s.elseBlock, level, ctx);
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+ case 'Reinforce': return p + `reinforce ${fmtExpr(s.cond)}` + fmtBlock(s.body, level, ctx);
104
+ case 'ReinforceTimes': return p + `reinforce ${fmtExpr(s.count)} times` + fmtBlock(s.body, level, ctx);
105
+ case 'ForEach': return p + `for each ${s.varName} in ${fmtExpr(s.iterable)}` + fmtBlock(s.body, level, ctx);
106
+ case 'Agent': return p + `agent ${s.name}${fmtParams(s.params)}${s.returnType ? ': ' + s.returnType : ''}` + fmtBlock(s.body, level, ctx);
107
+ case 'Report': return p + (s.value && s.value.type !== 'Void' ? 'report ' + fmtExpr(s.value) : 'report');
108
+ case 'Attempt': return p + 'attempt' + fmtBlock(s.tryBlock, level, ctx) + ` rescue ${s.errName}` + fmtBlock(s.catchBlock, level, ctx);
109
+ case 'Fail': {
110
+ const isDefault = s.value && s.value.type === 'String' && s.value.value === 'failed';
111
+ return p + (isDefault ? 'fail' : 'fail ' + fmtExpr(s.value));
112
+ }
113
+ case 'Weave': return p + 'weave ' + fmtExpr(s.path) + (s.alias ? ` as ${s.alias}` : '');
114
+ case 'Expose': return p + `expose ${s.name}`;
115
+ case 'Needs': return p + 'needs ' + s.items.map(fmtNeed).join(', ');
116
+ case 'Assign': return p + `${fmtExpr(s.target)} = ${fmtExpr(s.value)}`;
117
+ case 'Test': return p + `test ${q(s.name)}` + fmtBlock(s.body, level, ctx);
118
+ case 'Expect': return p + 'expect ' + fmtMatcher(s);
119
+ case 'Match': {
120
+ if (!s.arms.length) return p + `match ${fmtExpr(s.subject)} {}`;
121
+ const arms = s.arms.map(a => pad(level + 1) + fmtPattern(a.pattern) + fmtBlock(a.body, level + 1, ctx)).join('\n');
122
+ return p + `match ${fmtExpr(s.subject)} {\n${arms}\n${pad(level)}}`;
123
+ }
124
+ case 'Give': return p + `give ${fmtExpr(s.value)} to ${fmtExpr(s.channel)}`;
125
+ case 'Take': return p + `take ${s.name} from ${fmtExpr(s.channel)}`;
126
+ case 'Yield': return p + 'yield';
127
+ case 'ExpressionStatement': return p + fmtExpr(s.expr);
128
+ default: throw new Error(`cannot format statement type ${s.type}`);
129
+ }
130
+ }
131
+
132
+ function fmtMatcher(s) {
133
+ const a = fmtExpr(s.actual);
134
+ const m = s.matcher;
135
+ switch (m.kind) {
136
+ case 'is': return `${a} is ${fmtExpr(m.expected)}`;
137
+ case 'isnot': return `${a} is not ${fmtExpr(m.expected)}`;
138
+ case 'fail': return `${a} to fail`;
139
+ case 'failwith': return `${a} to fail with ${fmtExpr(m.expected)}`;
140
+ default: return a; // truthy
141
+ }
142
+ }
143
+
144
+ function fmtPattern(pp) {
145
+ switch (pp.type) {
146
+ case 'PWild': return '_';
147
+ case 'PBind': return pp.name;
148
+ case 'PLit': return fmtExpr(pp.expr);
149
+ case 'PList': {
150
+ const parts = pp.elements.map(fmtPattern);
151
+ if (pp.rest) parts.push('...' + pp.rest);
152
+ return '[' + parts.join(', ') + ']';
153
+ }
154
+ case 'PMesh': return pp.pairs.length ? '{ ' + pp.pairs.map(pr =>
155
+ (pr.pattern.type === 'PBind' && pr.pattern.name === pr.key) ? pr.key : `${q(pr.key)}: ${fmtPattern(pr.pattern)}`
156
+ ).join(', ') + ' }' : '{}';
157
+ default: throw new Error(`cannot format pattern ${pp.type}`);
158
+ }
159
+ }
160
+
161
+ function exprPrec(e) {
162
+ if (e.type === 'Pipe') return 0;
163
+ if (e.type === 'Binary') return PREC[e.op] || 0;
164
+ if (e.type === 'Unary') return 7;
165
+ if (e.type === 'Call' || e.type === 'Index') return 9;
166
+ return 10;
167
+ }
168
+
169
+ function par(s, need) { return need ? '(' + s + ')' : s; }
170
+
171
+ function fmtExpr(e, parent = 0, side = '') {
172
+ switch (e.type) {
173
+ case 'Number': return String(e.value);
174
+ case 'String': return q(e.value);
175
+ case 'Bool': return e.value ? 'live' : 'dead';
176
+ case 'Void': return 'void';
177
+ case 'Identifier': return e.name;
178
+ case 'Interp':
179
+ return '"' + e.parts.map(part => part.type === 'String'
180
+ ? String(part.value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/{/g, '\\{')
181
+ : '{' + fmtExpr(part) + '}').join('') + '"';
182
+ case 'Unary': {
183
+ const mine = exprPrec(e);
184
+ const out = e.op === 'not' ? `not ${fmtExpr(e.operand, mine)}` : '-' + fmtExpr(e.operand, mine);
185
+ return par(out, mine < parent);
186
+ }
187
+ case 'Binary': {
188
+ const mine = exprPrec(e);
189
+ const left = fmtExpr(e.left, mine, 'left');
190
+ const right = fmtExpr(e.right, mine + (['-', '/', '%'].includes(e.op) ? 1 : 0), 'right');
191
+ return par(`${left} ${e.op} ${right}`, mine < parent || (mine === parent && side === 'right'));
192
+ }
193
+ case 'Pipe': return par(`${fmtExpr(e.left, 0)} | ${fmtExpr(e.right, 1)}`, parent > 0);
194
+ case 'Call': return fmtExpr(e.callee, 9) + '(' + e.args.map(a => fmtExpr(a)).join(', ') + ')';
195
+ case 'Index': return fmtExpr(e.object, 9) + '[' + fmtExpr(e.index) + ']';
196
+ case 'List': return '[' + e.elements.map(x => fmtExpr(x)).join(', ') + ']';
197
+ case 'Mesh': return e.pairs.length ? '{ ' + e.pairs.map(([k, v]) => `${fmtExpr(k)}: ${fmtExpr(v)}`).join(', ') + ' }' : '{}';
198
+ case 'AgentExpr': return 'agent' + fmtParams(e.params) + (e.returnType ? ': ' + e.returnType : '') + fmtBlock(e.body, 0);
199
+ case 'Dispatch': return 'dispatch ' + fmtExpr(e.call);
200
+ case 'Gather': return 'gather ' + fmtExpr(e.expr, 8);
201
+ case 'Spawn': return 'spawn ' + fmtExpr(e.call);
202
+ default: throw new Error(`cannot format expression type ${e.type}`);
203
+ }
204
+ }
205
+
206
+ module.exports = { formatSource, formatProgram };