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.
- package/CONCURRENCY.md +207 -0
- package/HOW_IT_WORKS.md +235 -0
- package/INDEPENDENCE.md +24 -0
- package/MYXO_PROMPT.md +139 -0
- package/README.md +494 -0
- package/ROADMAP.md +200 -0
- package/SPEC.md +181 -0
- package/VISION.md +249 -0
- package/builtins.js +361 -0
- package/command-fence.js +75 -0
- package/errors.js +45 -0
- package/examples/agent.myx +31 -0
- package/examples/fenced-agent.js +78 -0
- package/examples/fib.myx +11 -0
- package/examples/fibers.myx +57 -0
- package/examples/flow-routing.myx +12 -0
- package/examples/geo.myx +12 -0
- package/examples/hello.myx +2 -0
- package/examples/host.js +30 -0
- package/examples/james-myxo-demo.js +60 -0
- package/examples/living-mesh.myx +22 -0
- package/examples/match.myx +18 -0
- package/examples/mathlib.js +8 -0
- package/examples/mathlib.pl +11 -0
- package/examples/mathlib.py +23 -0
- package/examples/mcp-host.js +37 -0
- package/examples/nexus-mesh.myx +22 -0
- package/examples/nexus.myx +29 -0
- package/examples/ouroboros.myx +2 -0
- package/examples/outward-gate.myx +23 -0
- package/examples/physarum.myx +75 -0
- package/examples/polyglot-host.js +17 -0
- package/examples/polyglot.myx +11 -0
- package/examples/resilient.myx +28 -0
- package/examples/scheduler.myx +46 -0
- package/examples/the-law.myx +27 -0
- package/examples/use-geo.myx +9 -0
- package/format.js +206 -0
- package/interpreter.js +1092 -0
- package/lexer.js +173 -0
- package/mcp-bridge.js +77 -0
- package/mcp-framing.js +34 -0
- package/mcp-server.js +57 -0
- package/myxo-concurrent.js +110 -0
- package/myxo-live-worker.js +30 -0
- package/myxo-live.js +83 -0
- package/myxo-lsp.js +226 -0
- package/myxo-par-worker.js +39 -0
- package/myxo-plan.js +207 -0
- package/myxo-run.js +63 -0
- package/myxo.js +296 -0
- package/package.json +27 -0
- package/parser.js +662 -0
- package/polyglot-host.js +39 -0
- package/polyglot.js +191 -0
- package/receipt.js +71 -0
- package/std.myx +88 -0
- package/tools/memo-fuzz.js +224 -0
package/parser.js
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// parser.js — turns a token stream into an Abstract Syntax Tree (AST).
|
|
3
|
+
// Recursive descent for statements; precedence climbing for expressions.
|
|
4
|
+
|
|
5
|
+
const { tokenize } = require('./lexer');
|
|
6
|
+
const { MyxoError } = require('./errors');
|
|
7
|
+
|
|
8
|
+
// Binary operator precedence, low to high. Each level is parsed by folding
|
|
9
|
+
// the level above it. `and`/`or` are keywords; the rest are operator tokens.
|
|
10
|
+
const PRECEDENCE = [
|
|
11
|
+
{ match: t => t.type === 'KEYWORD' && t.value === 'or', ops: ['or'] },
|
|
12
|
+
{ match: t => t.type === 'KEYWORD' && t.value === 'and', ops: ['and'] },
|
|
13
|
+
{ match: t => t.type === 'EQ' || t.type === 'NE', ops: ['==', '!='] },
|
|
14
|
+
{ match: t => ['GT', 'LT', 'GE', 'LE'].includes(t.type), ops: ['>', '<', '>=', '<='] },
|
|
15
|
+
{ match: t => t.type === 'PLUS' || t.type === 'MINUS', ops: ['+', '-'] },
|
|
16
|
+
{ match: t => ['STAR', 'SLASH', 'PERCENT'].includes(t.type), ops: ['*', '/', '%'] },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Gradual types: optional annotations from this set. `any` opts back into dynamic.
|
|
20
|
+
const VALID_TYPES = new Set(['number', 'string', 'bool', 'list', 'mesh', 'agent', 'void', 'any']);
|
|
21
|
+
|
|
22
|
+
class Parser {
|
|
23
|
+
constructor(tokens) { this.tokens = tokens; this.pos = 0; }
|
|
24
|
+
|
|
25
|
+
peek(o = 0) { return this.tokens[this.pos + o]; }
|
|
26
|
+
next() { return this.tokens[this.pos++]; }
|
|
27
|
+
atEnd() { return this.peek().type === 'EOF'; }
|
|
28
|
+
|
|
29
|
+
isKeyword(word) {
|
|
30
|
+
const t = this.peek();
|
|
31
|
+
return t.type === 'KEYWORD' && t.value === word;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
expect(type, what) {
|
|
35
|
+
const t = this.peek();
|
|
36
|
+
if (t.type !== type) {
|
|
37
|
+
throw new MyxoError(`expected ${what || type} but found '${t.value ?? t.type}'`, t.line, t.col);
|
|
38
|
+
}
|
|
39
|
+
return this.next();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
expectKeyword(word) {
|
|
43
|
+
if (!this.isKeyword(word)) {
|
|
44
|
+
const t = this.peek();
|
|
45
|
+
throw new MyxoError(`expected '${word}' but found '${t.value ?? t.type}'`, t.line, t.col);
|
|
46
|
+
}
|
|
47
|
+
return this.next();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A CONTEXTUAL word (a plain identifier used as a connector, e.g. `to`/`from` in send/recv) — not reserved,
|
|
51
|
+
// so it stays usable as an ordinary variable name everywhere else.
|
|
52
|
+
expectWord(word) {
|
|
53
|
+
const t = this.peek();
|
|
54
|
+
if (t.type !== 'IDENT' || t.value !== word) {
|
|
55
|
+
throw new MyxoError(`expected '${word}' but found '${t.value ?? t.type}'`, t.line, t.col);
|
|
56
|
+
}
|
|
57
|
+
return this.next();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---- statements ---------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
parseProgram() {
|
|
63
|
+
const body = [];
|
|
64
|
+
while (!this.atEnd()) body.push(this.parseStatement());
|
|
65
|
+
return { type: 'Program', body };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
parseBlock() {
|
|
69
|
+
this.expect('LBRACE', '{');
|
|
70
|
+
const body = [];
|
|
71
|
+
while (this.peek().type !== 'RBRACE' && !this.atEnd()) body.push(this.parseStatement());
|
|
72
|
+
const close = this.expect('RBRACE', '}');
|
|
73
|
+
body.endLine = close.line; // the `}` line, so the formatter can keep block-tail comments inside the block
|
|
74
|
+
return body;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
parseStatement() {
|
|
78
|
+
const t = this.peek();
|
|
79
|
+
if (t.type === 'KEYWORD') {
|
|
80
|
+
switch (t.value) {
|
|
81
|
+
case 'seed': return this.parseSeed();
|
|
82
|
+
case 'decay': return this.parseDecay();
|
|
83
|
+
case 'emit': return this.parseEmit();
|
|
84
|
+
case 'when': return this.parseWhen();
|
|
85
|
+
case 'reinforce': return this.parseReinforce();
|
|
86
|
+
case 'for': return this.parseForEach();
|
|
87
|
+
case 'report': return this.parseReport();
|
|
88
|
+
case 'attempt': return this.parseAttempt();
|
|
89
|
+
case 'fail': return this.parseFail();
|
|
90
|
+
case 'weave': return this.parseWeave();
|
|
91
|
+
case 'expose': return this.parseExpose();
|
|
92
|
+
case 'needs': return this.parseNeeds();
|
|
93
|
+
case 'test': return this.parseTest();
|
|
94
|
+
case 'expect': return this.parseExpect();
|
|
95
|
+
case 'match': return this.parseMatch();
|
|
96
|
+
case 'give': return this.parseGive();
|
|
97
|
+
case 'take': return this.parseTake();
|
|
98
|
+
case 'yield': { const kw = this.next(); return { type: 'Yield', line: kw.line }; }
|
|
99
|
+
case 'rescue':
|
|
100
|
+
throw new MyxoError("'rescue' without a matching 'attempt'", t.line, t.col);
|
|
101
|
+
case 'as':
|
|
102
|
+
throw new MyxoError("'as' only follows a 'weave' path", t.line, t.col);
|
|
103
|
+
case 'agent':
|
|
104
|
+
// `agent name(...)` is a declaration; `agent(...)` is an expression.
|
|
105
|
+
if (this.peek(1).type === 'IDENT') return this.parseAgentDecl();
|
|
106
|
+
break;
|
|
107
|
+
case 'otherwise':
|
|
108
|
+
throw new MyxoError("'otherwise' without a matching 'when'", t.line, t.col);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return this.parseExpressionStatement();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
parseSeed() {
|
|
115
|
+
const kw = this.expectKeyword('seed');
|
|
116
|
+
const t = this.peek();
|
|
117
|
+
// Destructuring: `seed [a, ...rest] = xs` / `seed { a, b } = m` — bind many names from a list/mesh at once.
|
|
118
|
+
if (t.type === 'LBRACKET' || t.type === 'LBRACE') {
|
|
119
|
+
const pattern = t.type === 'LBRACKET' ? this.parseListPattern() : this.parseMeshPattern();
|
|
120
|
+
this.assertLinear(pattern, new Set(), kw.line);
|
|
121
|
+
this.expect('ASSIGN', '=');
|
|
122
|
+
const value = this.parseExpression();
|
|
123
|
+
return { type: 'SeedDestructure', pattern, value, line: kw.line };
|
|
124
|
+
}
|
|
125
|
+
const name = this.expect('IDENT', 'a name').value;
|
|
126
|
+
let declType = null;
|
|
127
|
+
if (this.peek().type === 'COLON') { this.next(); declType = this.parseType(); } // seed n: number = 5
|
|
128
|
+
this.expect('ASSIGN', '=');
|
|
129
|
+
const value = this.parseExpression();
|
|
130
|
+
return { type: 'Seed', name, value, declType, line: kw.line };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// An optional gradual-type annotation: number/string/bool/list/mesh/agent/void/any.
|
|
134
|
+
parseType() {
|
|
135
|
+
const t = this.peek();
|
|
136
|
+
let name;
|
|
137
|
+
if (t.type === 'KEYWORD' && (t.value === 'void' || t.value === 'agent')) { this.next(); name = t.value; } // void/agent are keywords AND type names
|
|
138
|
+
else if (t.type === 'IDENT') { this.next(); name = t.value; }
|
|
139
|
+
else {
|
|
140
|
+
const hint = (t.type === 'STRING' || t.type === 'NUMBER') ? " — parameter defaults now use '=' (name = default), not ':'" : '';
|
|
141
|
+
throw new MyxoError(`expected a type name, got '${t.value ?? t.type}'${hint}`, t.line, t.col);
|
|
142
|
+
}
|
|
143
|
+
if (!VALID_TYPES.has(name)) throw new MyxoError(`unknown type '${name}' — use number/string/bool/list/mesh/agent/void/any`, t.line, t.col);
|
|
144
|
+
return name;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
parseDecay() {
|
|
148
|
+
const kw = this.expectKeyword('decay');
|
|
149
|
+
const target = this.parseExpression();
|
|
150
|
+
if (target.type !== 'Identifier' && target.type !== 'Index') {
|
|
151
|
+
throw new MyxoError('can only decay a pathway or an indexed slot', kw.line, kw.col);
|
|
152
|
+
}
|
|
153
|
+
return { type: 'Decay', target, line: kw.line };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
parseEmit() {
|
|
157
|
+
const kw = this.expectKeyword('emit');
|
|
158
|
+
const args = [this.parseExpression()];
|
|
159
|
+
while (this.peek().type === 'COMMA') { this.next(); args.push(this.parseExpression()); }
|
|
160
|
+
return { type: 'Emit', args, line: kw.line };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
parseWhen() {
|
|
164
|
+
const kw = this.expectKeyword('when');
|
|
165
|
+
const cond = this.parseExpression();
|
|
166
|
+
const thenBlock = this.parseBlock();
|
|
167
|
+
let elseBlock = null;
|
|
168
|
+
if (this.isKeyword('otherwise')) {
|
|
169
|
+
this.next();
|
|
170
|
+
// `otherwise when ...` chains; otherwise a plain block.
|
|
171
|
+
elseBlock = this.isKeyword('when') ? [this.parseWhen()] : this.parseBlock();
|
|
172
|
+
}
|
|
173
|
+
return { type: 'When', cond, thenBlock, elseBlock, line: kw.line };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
parseReinforce() {
|
|
177
|
+
const kw = this.expectKeyword('reinforce');
|
|
178
|
+
const expr = this.parseExpression();
|
|
179
|
+
if (this.isKeyword('times')) {
|
|
180
|
+
this.next();
|
|
181
|
+
const body = this.parseBlock();
|
|
182
|
+
return { type: 'ReinforceTimes', count: expr, body, line: kw.line };
|
|
183
|
+
}
|
|
184
|
+
const body = this.parseBlock();
|
|
185
|
+
return { type: 'Reinforce', cond: expr, body, line: kw.line };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
parseForEach() {
|
|
189
|
+
const kw = this.expectKeyword('for');
|
|
190
|
+
this.expectKeyword('each');
|
|
191
|
+
const varName = this.expect('IDENT', 'a name').value;
|
|
192
|
+
this.expectKeyword('in');
|
|
193
|
+
const iterable = this.parseExpression();
|
|
194
|
+
const body = this.parseBlock();
|
|
195
|
+
return { type: 'ForEach', varName, iterable, body, line: kw.line };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
parseAgentDecl() {
|
|
199
|
+
const kw = this.expectKeyword('agent');
|
|
200
|
+
const name = this.expect('IDENT', 'an agent name').value;
|
|
201
|
+
const params = this.parseParams();
|
|
202
|
+
let returnType = null;
|
|
203
|
+
if (this.peek().type === 'COLON') { this.next(); returnType = this.parseType(); } // agent f(x): number { }
|
|
204
|
+
const body = this.parseBlock();
|
|
205
|
+
return { type: 'Agent', name, params, body, returnType, line: kw.line };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
parseReport() {
|
|
209
|
+
const kw = this.expectKeyword('report');
|
|
210
|
+
// `report` may stand alone (reports void) or carry a value.
|
|
211
|
+
const ends = this.peek().type === 'RBRACE' || this.atEnd();
|
|
212
|
+
const value = ends ? { type: 'Void' } : this.parseExpression();
|
|
213
|
+
return { type: 'Report', value, line: kw.line };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// attempt { ... } rescue err { ... } — run a block; if a pathway fails, the
|
|
217
|
+
// failure is caught and bound to `err` (a mesh: message, line, value).
|
|
218
|
+
parseAttempt() {
|
|
219
|
+
const kw = this.expectKeyword('attempt');
|
|
220
|
+
const tryBlock = this.parseBlock();
|
|
221
|
+
this.expectKeyword('rescue');
|
|
222
|
+
const errName = this.expect('IDENT', 'a name for the failure').value;
|
|
223
|
+
const catchBlock = this.parseBlock();
|
|
224
|
+
return { type: 'Attempt', tryBlock, errName, catchBlock, line: kw.line };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// fail expr — raise a failure that an enclosing `attempt` can rescue.
|
|
228
|
+
parseFail() {
|
|
229
|
+
const kw = this.expectKeyword('fail');
|
|
230
|
+
const ends = this.peek().type === 'RBRACE' || this.atEnd();
|
|
231
|
+
const value = ends ? { type: 'String', value: 'failed' } : this.parseExpression();
|
|
232
|
+
return { type: 'Fail', value, line: kw.line };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// weave "path" [as alias] — import a strand. Flat by default; `as` namespaces
|
|
236
|
+
// the exposed names into a mesh you index with ["name"].
|
|
237
|
+
parseWeave() {
|
|
238
|
+
const kw = this.expectKeyword('weave');
|
|
239
|
+
const path = this.parseExpression();
|
|
240
|
+
let alias = null;
|
|
241
|
+
if (this.isKeyword('as')) { this.next(); alias = this.expect('IDENT', 'an alias name').value; }
|
|
242
|
+
return { type: 'Weave', path, alias, line: kw.line };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// expose name — mark a pathway public so weaving strands can import it.
|
|
246
|
+
parseExpose() {
|
|
247
|
+
const kw = this.expectKeyword('expose');
|
|
248
|
+
const name = this.expect('IDENT', 'a name to expose').value;
|
|
249
|
+
return { type: 'Expose', name, line: kw.line };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// needs cap, cap(max N), ... — declare the host capabilities this script may
|
|
253
|
+
// call. Once declared, calling an undeclared capability is refused; an optional
|
|
254
|
+
// `(max N)` caps the capability's first argument, runtime-enforced and logged.
|
|
255
|
+
parseNeeds() {
|
|
256
|
+
const kw = this.expectKeyword('needs');
|
|
257
|
+
const items = [this.parseNeedItem()];
|
|
258
|
+
while (this.peek().type === 'COMMA') { this.next(); items.push(this.parseNeedItem()); }
|
|
259
|
+
return { type: 'Needs', items, line: kw.line };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
parseNeedItem() {
|
|
263
|
+
const name = this.expect('IDENT', 'a capability name').value;
|
|
264
|
+
let limit = null;
|
|
265
|
+
if (this.peek().type === 'LPAREN') {
|
|
266
|
+
this.next();
|
|
267
|
+
limit = {};
|
|
268
|
+
this.parseConstraint(limit);
|
|
269
|
+
while (this.peek().type === 'COMMA') { this.next(); this.parseConstraint(limit); }
|
|
270
|
+
this.expect('RPAREN', "')' to close the constraint");
|
|
271
|
+
}
|
|
272
|
+
return { name, limit };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// a single capability constraint: `max N` (per-call ceiling) or `total N`
|
|
276
|
+
// (cumulative budget across the whole run). Folds into the limit object.
|
|
277
|
+
parseConstraint(limit) {
|
|
278
|
+
const kind = this.expect('IDENT', "a constraint like 'max' or 'total'").value;
|
|
279
|
+
if (kind !== 'max' && kind !== 'total') {
|
|
280
|
+
throw new MyxoError(`unknown capability constraint '${kind}' — use 'max' or 'total'`, this.peek().line);
|
|
281
|
+
}
|
|
282
|
+
limit[kind] = this.expect('NUMBER', 'a number for the cap').value;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// test "name" { ... } — a named test case. Its body runs only under `myxo test`
|
|
286
|
+
// (inert in a normal run), and `expect` assertions inside it are collected.
|
|
287
|
+
parseTest() {
|
|
288
|
+
const kw = this.expectKeyword('test');
|
|
289
|
+
const t = this.peek();
|
|
290
|
+
if (t.type !== 'STRING') throw new MyxoError('a test needs a name string: test "..." { }', t.line, t.col);
|
|
291
|
+
this.next();
|
|
292
|
+
const body = this.parseBlock();
|
|
293
|
+
return { type: 'Test', name: t.value, body, line: kw.line };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// expect EXPR — the value must be live (truthy)
|
|
297
|
+
// expect EXPR is EXPR — deep equality (`is not` for inequality)
|
|
298
|
+
// expect EXPR to fail — evaluating EXPR must raise an Myxo failure
|
|
299
|
+
// expect EXPR to fail with STRING — ...and its message must contain STRING
|
|
300
|
+
// `is` / `to` / `with` are contextual here, not reserved words.
|
|
301
|
+
parseExpect() {
|
|
302
|
+
const kw = this.expectKeyword('expect');
|
|
303
|
+
const actual = this.parseExpression();
|
|
304
|
+
let matcher = { kind: 'truthy' };
|
|
305
|
+
const t = this.peek(), nx = this.peek(1);
|
|
306
|
+
// `is`/`to` are contextual: only treat them as matcher words when they actually start one, so a bare
|
|
307
|
+
// `expect X` followed by a statement that happens to begin with a var named `is`/`to` doesn't merge.
|
|
308
|
+
if (t.type === 'IDENT' && t.value === 'is' && nx.type !== 'ASSIGN') {
|
|
309
|
+
this.next();
|
|
310
|
+
if (this.isKeyword('not')) { this.next(); matcher = { kind: 'isnot', expected: this.parseExpression() }; }
|
|
311
|
+
else matcher = { kind: 'is', expected: this.parseExpression() };
|
|
312
|
+
} else if (t.type === 'IDENT' && t.value === 'to' && nx.type === 'KEYWORD' && nx.value === 'fail') {
|
|
313
|
+
this.next();
|
|
314
|
+
this.expectKeyword('fail');
|
|
315
|
+
if (this.peek().type === 'IDENT' && this.peek().value === 'with') {
|
|
316
|
+
this.next();
|
|
317
|
+
matcher = { kind: 'failwith', expected: this.parseExpression() };
|
|
318
|
+
} else {
|
|
319
|
+
matcher = { kind: 'fail' };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { type: 'Expect', actual, matcher, line: kw.line };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// match SUBJECT { PATTERN { ... } ... } — run the first arm whose pattern matches, binding the
|
|
326
|
+
// pattern's names. If no arm matches, nothing runs (use `_` for a catch-all).
|
|
327
|
+
// `give VALUE to CHANNEL` and `take NAME from CHANNEL` — cooperative channel ops (suspension points in a fiber).
|
|
328
|
+
parseGive() {
|
|
329
|
+
const kw = this.expectKeyword('give');
|
|
330
|
+
const value = this.parseExpression();
|
|
331
|
+
this.expectWord('to');
|
|
332
|
+
const channel = this.parseExpression();
|
|
333
|
+
return { type: 'Give', value, channel, line: kw.line };
|
|
334
|
+
}
|
|
335
|
+
parseTake() {
|
|
336
|
+
const kw = this.expectKeyword('take');
|
|
337
|
+
const name = this.expect('IDENT', 'a name to bind the taken value').value;
|
|
338
|
+
this.expectWord('from');
|
|
339
|
+
const channel = this.parseExpression();
|
|
340
|
+
return { type: 'Take', name, channel, line: kw.line };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
parseMatch() {
|
|
344
|
+
const kw = this.expectKeyword('match');
|
|
345
|
+
const subject = this.parseExpression();
|
|
346
|
+
this.expect('LBRACE', '{');
|
|
347
|
+
const arms = [];
|
|
348
|
+
while (this.peek().type !== 'RBRACE' && !this.atEnd()) {
|
|
349
|
+
const armLine = this.peek().line;
|
|
350
|
+
const pattern = this.parsePattern();
|
|
351
|
+
this.assertLinear(pattern, new Set(), armLine);
|
|
352
|
+
const body = this.parseBlock();
|
|
353
|
+
arms.push({ pattern, body });
|
|
354
|
+
}
|
|
355
|
+
const close = this.expect('RBRACE', "'}' to close the match");
|
|
356
|
+
return { type: 'Match', subject, arms, line: kw.line, matchEnd: close.line };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Patterns are LINEAR: a name may bind at most once per arm. Myxo has no non-linear/equality patterns,
|
|
360
|
+
// so `[x, x]` is a mistake — reject it instead of silently last-winning the second binding.
|
|
361
|
+
assertLinear(pat, seen, line) {
|
|
362
|
+
switch (pat.type) {
|
|
363
|
+
case 'PBind':
|
|
364
|
+
if (seen.has(pat.name)) throw new MyxoError(`pattern binds '${pat.name}' more than once`, line);
|
|
365
|
+
seen.add(pat.name); return;
|
|
366
|
+
case 'PList':
|
|
367
|
+
for (const e of pat.elements) this.assertLinear(e, seen, line);
|
|
368
|
+
if (pat.rest) {
|
|
369
|
+
if (seen.has(pat.rest)) throw new MyxoError(`pattern binds '${pat.rest}' more than once`, line);
|
|
370
|
+
seen.add(pat.rest);
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
case 'PMesh':
|
|
374
|
+
for (const pr of pat.pairs) this.assertLinear(pr.pattern, seen, line);
|
|
375
|
+
return;
|
|
376
|
+
default: return; // PWild / PLit bind nothing
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// A pattern: `_` (wildcard), a name (binds the value), a literal, `[p, ..., ...rest]`, or `{ "key": p }`.
|
|
381
|
+
parsePattern() {
|
|
382
|
+
const t = this.peek();
|
|
383
|
+
if (t.type === 'IDENT') { this.next(); return t.value === '_' ? { type: 'PWild' } : { type: 'PBind', name: t.value }; }
|
|
384
|
+
if (t.type === 'NUMBER') { this.next(); return { type: 'PLit', expr: { type: 'Number', value: t.value } }; }
|
|
385
|
+
if (t.type === 'STRING') { this.next(); return { type: 'PLit', expr: { type: 'String', value: t.value } }; }
|
|
386
|
+
if (t.type === 'MINUS') { this.next(); const n = this.expect('NUMBER', 'a number after -'); return { type: 'PLit', expr: { type: 'Number', value: -n.value } }; }
|
|
387
|
+
if (t.type === 'KEYWORD') {
|
|
388
|
+
if (t.value === 'live') { this.next(); return { type: 'PLit', expr: { type: 'Bool', value: true } }; }
|
|
389
|
+
if (t.value === 'dead') { this.next(); return { type: 'PLit', expr: { type: 'Bool', value: false } }; }
|
|
390
|
+
if (t.value === 'void') { this.next(); return { type: 'PLit', expr: { type: 'Void' } }; }
|
|
391
|
+
}
|
|
392
|
+
if (t.type === 'LBRACKET') return this.parseListPattern();
|
|
393
|
+
if (t.type === 'LBRACE') return this.parseMeshPattern();
|
|
394
|
+
throw new MyxoError(`bad pattern: unexpected '${t.value ?? t.type}'`, t.line, t.col);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
parseListPattern() {
|
|
398
|
+
this.expect('LBRACKET', '[');
|
|
399
|
+
const elements = [];
|
|
400
|
+
let rest = null;
|
|
401
|
+
while (this.peek().type !== 'RBRACKET') {
|
|
402
|
+
if (this.peek().type === 'ELLIPSIS') {
|
|
403
|
+
this.next();
|
|
404
|
+
rest = this.expect('IDENT', 'a name after ...').value;
|
|
405
|
+
break; // ...rest must be last
|
|
406
|
+
}
|
|
407
|
+
elements.push(this.parsePattern());
|
|
408
|
+
if (this.peek().type === 'COMMA') { this.next(); continue; }
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
this.expect('RBRACKET', "']' to close the list pattern");
|
|
412
|
+
return { type: 'PList', elements, rest };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
parseMeshPattern() {
|
|
416
|
+
this.expect('LBRACE', '{');
|
|
417
|
+
const pairs = [];
|
|
418
|
+
while (this.peek().type !== 'RBRACE') {
|
|
419
|
+
const t = this.peek();
|
|
420
|
+
if (t.type === 'STRING') { // explicit: "key": pattern
|
|
421
|
+
this.next();
|
|
422
|
+
this.expect('COLON', ':');
|
|
423
|
+
pairs.push({ key: t.value, pattern: this.parsePattern() });
|
|
424
|
+
} else if (t.type === 'IDENT') { // shorthand: { name } == { "name": name }
|
|
425
|
+
this.next();
|
|
426
|
+
if (this.peek().type === 'COLON') throw new MyxoError(`mesh pattern keys are strings — write "${t.value}": pattern, or just ${t.value} to bind`, t.line, t.col);
|
|
427
|
+
pairs.push({ key: t.value, pattern: { type: 'PBind', name: t.value } });
|
|
428
|
+
} else {
|
|
429
|
+
throw new MyxoError(`a mesh pattern key must be a "string" or a name, got '${t.value ?? t.type}'`, t.line, t.col);
|
|
430
|
+
}
|
|
431
|
+
if (this.peek().type === 'COMMA') { this.next(); continue; }
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
this.expect('RBRACE', "'}' to close the mesh pattern");
|
|
435
|
+
return { type: 'PMesh', pairs };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
parseExpressionStatement() {
|
|
439
|
+
const line = this.peek().line;
|
|
440
|
+
const expr = this.parseExpression();
|
|
441
|
+
if (this.peek().type === 'ASSIGN') {
|
|
442
|
+
this.next();
|
|
443
|
+
if (expr.type !== 'Identifier' && expr.type !== 'Index') {
|
|
444
|
+
throw new MyxoError('can only assign to a pathway or an indexed slot', line);
|
|
445
|
+
}
|
|
446
|
+
const value = this.parseExpression();
|
|
447
|
+
return { type: 'Assign', target: expr, value, line };
|
|
448
|
+
}
|
|
449
|
+
return { type: 'ExpressionStatement', expr, line };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
parseParams() {
|
|
453
|
+
this.expect('LPAREN', '(');
|
|
454
|
+
const params = [];
|
|
455
|
+
if (this.peek().type !== 'RPAREN') {
|
|
456
|
+
params.push(this.parseParam());
|
|
457
|
+
while (this.peek().type === 'COMMA') { this.next(); params.push(this.parseParam()); }
|
|
458
|
+
}
|
|
459
|
+
this.expect('RPAREN', ')');
|
|
460
|
+
return params;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// one parameter: `name`, `name: Type`, `name = default`, `name: Type = default`, or `...rest`.
|
|
464
|
+
parseParam() {
|
|
465
|
+
if (this.peek().type === 'ELLIPSIS') {
|
|
466
|
+
this.next();
|
|
467
|
+
const name = this.expect('IDENT', 'a rest-parameter name').value;
|
|
468
|
+
return { name, def: null, rest: true, paramType: null };
|
|
469
|
+
}
|
|
470
|
+
const name = this.expect('IDENT', 'a parameter name').value;
|
|
471
|
+
let paramType = null, def = null;
|
|
472
|
+
if (this.peek().type === 'COLON') { this.next(); paramType = this.parseType(); } // : Type
|
|
473
|
+
if (this.peek().type === 'ASSIGN') { this.next(); def = this.parseExpression(); } // = default
|
|
474
|
+
return { name, def, rest: false, paramType };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ---- expressions --------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
parseExpression() { return this.parsePipe(); }
|
|
480
|
+
|
|
481
|
+
// Pipelines: `x | f | g(a)` reads top-to-bottom. `x | f` is `f(x)`; `x | f(a)` is `f(x, a)`
|
|
482
|
+
// (the piped value becomes the FIRST argument). Lowest precedence, left-associative.
|
|
483
|
+
parsePipe() {
|
|
484
|
+
let left = this.parseBinary(0);
|
|
485
|
+
while (this.peek().type === 'PIPE') {
|
|
486
|
+
const t = this.next();
|
|
487
|
+
const right = this.parseBinary(0);
|
|
488
|
+
left = { type: 'Pipe', left, right, line: t.line };
|
|
489
|
+
}
|
|
490
|
+
return left;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
parseBinary(level) {
|
|
494
|
+
if (level >= PRECEDENCE.length) return this.parseUnary();
|
|
495
|
+
let left = this.parseBinary(level + 1);
|
|
496
|
+
const rule = PRECEDENCE[level];
|
|
497
|
+
while (rule.match(this.peek())) {
|
|
498
|
+
const opTok = this.next();
|
|
499
|
+
const op = opTok.value;
|
|
500
|
+
const right = this.parseBinary(level + 1);
|
|
501
|
+
left = { type: 'Binary', op, left, right, line: opTok.line };
|
|
502
|
+
}
|
|
503
|
+
return left;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
parseUnary() {
|
|
507
|
+
const t = this.peek();
|
|
508
|
+
if (this.isKeyword('not')) { this.next(); return { type: 'Unary', op: 'not', operand: this.parseUnary(), line: t.line }; }
|
|
509
|
+
if (t.type === 'MINUS') { this.next(); return { type: 'Unary', op: '-', operand: this.parseUnary(), line: t.line }; }
|
|
510
|
+
// Concurrency: `dispatch f(x)` builds an unstarted task; `gather [t1, t2]` runs them in parallel.
|
|
511
|
+
if (this.isKeyword('dispatch')) {
|
|
512
|
+
this.next();
|
|
513
|
+
const call = this.parsePostfix();
|
|
514
|
+
if (!call || call.type !== 'Call') {
|
|
515
|
+
throw new MyxoError("dispatch needs an agent call, e.g. dispatch work(x)", t.line, t.col);
|
|
516
|
+
}
|
|
517
|
+
return { type: 'Dispatch', call, line: t.line };
|
|
518
|
+
}
|
|
519
|
+
if (this.isKeyword('gather')) {
|
|
520
|
+
this.next();
|
|
521
|
+
return { type: 'Gather', expr: this.parseUnary(), line: t.line };
|
|
522
|
+
}
|
|
523
|
+
// Cooperative concurrency: `spawn f(x)` starts a fiber and returns its handle.
|
|
524
|
+
if (this.isKeyword('spawn')) {
|
|
525
|
+
this.next();
|
|
526
|
+
const call = this.parsePostfix();
|
|
527
|
+
if (!call || call.type !== 'Call') {
|
|
528
|
+
throw new MyxoError("spawn needs an agent call, e.g. spawn worker(ch)", t.line, t.col);
|
|
529
|
+
}
|
|
530
|
+
return { type: 'Spawn', call, line: t.line };
|
|
531
|
+
}
|
|
532
|
+
return this.parsePostfix();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Calls and indexing bind tighter than anything and chain left-to-right.
|
|
536
|
+
parsePostfix() {
|
|
537
|
+
let expr = this.parsePrimary();
|
|
538
|
+
for (;;) {
|
|
539
|
+
const t = this.peek();
|
|
540
|
+
if (t.type === 'LPAREN') {
|
|
541
|
+
this.next();
|
|
542
|
+
const args = [];
|
|
543
|
+
if (this.peek().type !== 'RPAREN') {
|
|
544
|
+
args.push(this.parseExpression());
|
|
545
|
+
while (this.peek().type === 'COMMA') { this.next(); args.push(this.parseExpression()); }
|
|
546
|
+
}
|
|
547
|
+
this.expect('RPAREN', ')');
|
|
548
|
+
expr = { type: 'Call', callee: expr, args, line: t.line };
|
|
549
|
+
} else if (t.type === 'LBRACKET') {
|
|
550
|
+
this.next();
|
|
551
|
+
const index = this.parseExpression();
|
|
552
|
+
this.expect('RBRACKET', ']');
|
|
553
|
+
expr = { type: 'Index', object: expr, index, line: t.line };
|
|
554
|
+
} else {
|
|
555
|
+
return expr;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
parsePrimary() {
|
|
561
|
+
const t = this.peek();
|
|
562
|
+
|
|
563
|
+
if (t.type === 'NUMBER') { this.next(); return { type: 'Number', value: t.value }; }
|
|
564
|
+
if (t.type === 'STRING') { this.next(); return { type: 'String', value: t.value }; }
|
|
565
|
+
if (t.type === 'TEMPLATE') {
|
|
566
|
+
this.next();
|
|
567
|
+
const parts = t.value.map(seg => seg.t === 'lit'
|
|
568
|
+
? { type: 'String', value: seg.v }
|
|
569
|
+
: parseFragment(seg.v, t.line));
|
|
570
|
+
return { type: 'Interp', parts, line: t.line };
|
|
571
|
+
}
|
|
572
|
+
if (t.type === 'IDENT') { this.next(); return { type: 'Identifier', name: t.value, line: t.line }; }
|
|
573
|
+
|
|
574
|
+
if (t.type === 'KEYWORD') {
|
|
575
|
+
if (t.value === 'live') { this.next(); return { type: 'Bool', value: true }; }
|
|
576
|
+
if (t.value === 'dead') { this.next(); return { type: 'Bool', value: false }; }
|
|
577
|
+
if (t.value === 'void') { this.next(); return { type: 'Void' }; }
|
|
578
|
+
if (t.value === 'agent') return this.parseAgentExpr();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (t.type === 'LPAREN') {
|
|
582
|
+
this.next();
|
|
583
|
+
const expr = this.parseExpression();
|
|
584
|
+
this.expect('RPAREN', ')');
|
|
585
|
+
return expr;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (t.type === 'LBRACKET') return this.parseList();
|
|
589
|
+
if (t.type === 'LBRACE') return this.parseMesh();
|
|
590
|
+
|
|
591
|
+
throw new MyxoError(`unexpected '${t.value ?? t.type}'`, t.line, t.col);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
parseAgentExpr() {
|
|
595
|
+
const kw = this.expectKeyword('agent');
|
|
596
|
+
const params = this.parseParams();
|
|
597
|
+
let returnType = null;
|
|
598
|
+
if (this.peek().type === 'COLON') { this.next(); returnType = this.parseType(); }
|
|
599
|
+
const body = this.parseBlock();
|
|
600
|
+
return { type: 'AgentExpr', params, body, returnType, line: kw.line };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
parseList() {
|
|
604
|
+
const open = this.expect('LBRACKET', '[');
|
|
605
|
+
const elements = [];
|
|
606
|
+
if (this.peek().type !== 'RBRACKET') {
|
|
607
|
+
elements.push(this.parseExpression());
|
|
608
|
+
while (this.peek().type === 'COMMA') {
|
|
609
|
+
this.next();
|
|
610
|
+
if (this.peek().type === 'RBRACKET') break; // trailing comma
|
|
611
|
+
elements.push(this.parseExpression());
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
this.expect('RBRACKET', ']');
|
|
615
|
+
return { type: 'List', elements, line: open.line };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
parseMesh() {
|
|
619
|
+
const open = this.expect('LBRACE', '{');
|
|
620
|
+
const pairs = [];
|
|
621
|
+
if (this.peek().type !== 'RBRACE') {
|
|
622
|
+
pairs.push(this.parsePair());
|
|
623
|
+
while (this.peek().type === 'COMMA') {
|
|
624
|
+
this.next();
|
|
625
|
+
if (this.peek().type === 'RBRACE') break; // trailing comma
|
|
626
|
+
pairs.push(this.parsePair());
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
this.expect('RBRACE', '}');
|
|
630
|
+
return { type: 'Mesh', pairs, line: open.line };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
parsePair() {
|
|
634
|
+
const key = this.parseExpression();
|
|
635
|
+
this.expect('COLON', ':');
|
|
636
|
+
const value = this.parseExpression();
|
|
637
|
+
return [key, value];
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Parse a raw expression fragment (a {…} interpolation chunk) into one AST node.
|
|
642
|
+
function parseFragment(src, line) {
|
|
643
|
+
const p = new Parser(tokenize(src));
|
|
644
|
+
const expr = p.parseExpression();
|
|
645
|
+
if (!p.atEnd()) throw new MyxoError(`bad expression in interpolation: '${src}'`, line);
|
|
646
|
+
offsetLines(expr, line - 1); // fragment tokens count from line 1 — shift them onto the template's real source line
|
|
647
|
+
return expr;
|
|
648
|
+
}
|
|
649
|
+
// Shift every node's source line by delta (so an interpolated expression reports the line it actually appears on).
|
|
650
|
+
function offsetLines(node, delta) {
|
|
651
|
+
if (!node || typeof node !== 'object') return;
|
|
652
|
+
if (Array.isArray(node)) { for (const n of node) offsetLines(n, delta); return; }
|
|
653
|
+
if (typeof node.line === 'number') node.line += delta;
|
|
654
|
+
for (const k of Object.keys(node)) { if (k !== 'line') offsetLines(node[k], delta); }
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function parse(src) {
|
|
658
|
+
const tokens = typeof src === 'string' ? tokenize(src) : src;
|
|
659
|
+
return new Parser(tokens).parseProgram();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
module.exports = { parse, Parser };
|