spl-lang 0.0.0-stage → 0.1.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/dist/parser.js ADDED
@@ -0,0 +1,709 @@
1
+ import { SplSyntaxError } from './errors.js';
2
+ import { tokenize } from './lexer.js';
3
+ export const TOOL_NAMES = ['read_file', 'write_file', 'web_fetch', 'web_search', 'shell'];
4
+ const KEYWORDS = new Set([
5
+ 'let', 'if', 'elif', 'else', 'for', 'in', 'while', 'def', 'return', 'try', 'except', 'assert', 'fail',
6
+ 'success', 'enforce', 'forbid', 'restrict', 'think', 'break', 'continue', 'pass', 'and', 'or', 'not',
7
+ 'True', 'False', 'None', ...TOOL_NAMES,
8
+ ]);
9
+ /** Fixed modifier schema per command; anything else is a syntax error. */
10
+ export const MODIFIER_SCHEMA = {
11
+ '?': {
12
+ len: ['word', 'sentence', 'paragraph', 'long'],
13
+ fmt: ['text', 'object'],
14
+ },
15
+ think: {
16
+ level: ['low', 'medium', 'high'],
17
+ show: [true, false],
18
+ },
19
+ };
20
+ /**
21
+ * Tool parameter schema: the allowed names, and the `required` ones that let the runtime run the tool itself.
22
+ * Every parameter is optional in a script; the LLM supplies the missing required ones when it calls the tool.
23
+ */
24
+ export const TOOL_SCHEMA = {
25
+ read_file: { params: ['path'], required: ['path'] },
26
+ write_file: { params: ['path', 'content'], required: ['path', 'content'] },
27
+ web_fetch: { params: ['url'], required: ['url'] },
28
+ web_search: { params: ['query'], required: ['query'] },
29
+ shell: { params: ['command'], required: ['command'] },
30
+ };
31
+ const DIRECTIVE_SCHEMA = { model: 'string' };
32
+ const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '//=', '%=']);
33
+ export function parse(source) {
34
+ const lines = source.split(/\r?\n/);
35
+ const parser = new Parser(tokenize(source), lines);
36
+ return parser.parseProgram();
37
+ }
38
+ class Parser {
39
+ toks;
40
+ lines;
41
+ pos = 0;
42
+ loopDepth = 0;
43
+ constructor(toks, lines) {
44
+ this.toks = toks;
45
+ this.lines = lines;
46
+ }
47
+ // ---- token helpers -------------------------------------------------
48
+ get tok() {
49
+ return this.toks[this.pos];
50
+ }
51
+ peekAt(n) {
52
+ return this.toks[Math.min(this.pos + n, this.toks.length - 1)];
53
+ }
54
+ next() {
55
+ const t = this.toks[this.pos];
56
+ if (t.type !== 'eof')
57
+ this.pos++;
58
+ return t;
59
+ }
60
+ fail(msg, t = this.tok) {
61
+ throw new SplSyntaxError(msg, t.line, t.col);
62
+ }
63
+ describe(t) {
64
+ switch (t.type) {
65
+ case 'eof': return 'end of file';
66
+ case 'nl': return 'end of line';
67
+ case 'num': return `number ${t.value}`;
68
+ case 'str': return 'string';
69
+ default: return `'${t.value}'`;
70
+ }
71
+ }
72
+ isOp(v, t = this.tok) {
73
+ return t.type === 'op' && t.value === v;
74
+ }
75
+ isName(v, t = this.tok) {
76
+ return t.type === 'name' && t.value === v;
77
+ }
78
+ expectOp(v) {
79
+ if (!this.isOp(v))
80
+ this.fail(`expected '${v}' but found ${this.describe(this.tok)}`);
81
+ return this.next();
82
+ }
83
+ expectKeyword(v) {
84
+ if (!this.isName(v))
85
+ this.fail(`expected '${v}' but found ${this.describe(this.tok)}`);
86
+ return this.next();
87
+ }
88
+ expectIdent(what) {
89
+ const t = this.tok;
90
+ if (t.type !== 'name')
91
+ this.fail(`expected ${what} but found ${this.describe(t)}`);
92
+ const name = t.value;
93
+ if (KEYWORDS.has(name))
94
+ this.fail(`'${name}' is a reserved word and cannot be used as ${what}`);
95
+ this.next();
96
+ return name;
97
+ }
98
+ skipNl() {
99
+ while (this.tok.type === 'nl')
100
+ this.next();
101
+ }
102
+ skipSeparators() {
103
+ while (this.tok.type === 'nl' || this.isOp(';'))
104
+ this.next();
105
+ }
106
+ /** True when the next non-newline token is the given keyword. */
107
+ keywordAfterNl(v) {
108
+ let k = 0;
109
+ while (this.peekAt(k).type === 'nl')
110
+ k++;
111
+ return this.isName(v, this.peekAt(k));
112
+ }
113
+ endOfStatement() {
114
+ const t = this.tok;
115
+ if (t.type === 'nl' || t.type === 'eof' || this.isOp(';') || this.isOp('}'))
116
+ return;
117
+ this.fail(`unexpected ${this.describe(t)} at end of statement`);
118
+ }
119
+ atStatementEnd() {
120
+ const t = this.tok;
121
+ return t.type === 'nl' || t.type === 'eof' || this.isOp(';') || this.isOp('}');
122
+ }
123
+ // ---- program & statements -----------------------------------------
124
+ parseProgram() {
125
+ const directives = {};
126
+ this.skipSeparators();
127
+ while (this.isOp('@')) {
128
+ this.parseDirective(directives);
129
+ this.endOfStatement();
130
+ this.skipSeparators();
131
+ }
132
+ const body = [];
133
+ while (this.tok.type !== 'eof') {
134
+ body.push(this.parseStatement());
135
+ this.endOfStatement();
136
+ this.skipSeparators();
137
+ }
138
+ return { directives, body, lines: this.lines };
139
+ }
140
+ parseDirective(into) {
141
+ this.expectOp('@');
142
+ const open = this.expectOp('{');
143
+ this.skipNl();
144
+ const seen = new Set();
145
+ while (!this.isOp('}')) {
146
+ const nameTok = this.tok;
147
+ if (nameTok.type !== 'name')
148
+ this.fail('expected directive name');
149
+ const name = nameTok.value;
150
+ this.next();
151
+ if (!(name in DIRECTIVE_SCHEMA)) {
152
+ this.fail(`unknown directive '${name}' (allowed: ${Object.keys(DIRECTIVE_SCHEMA).join(', ')})`, nameTok);
153
+ }
154
+ if (seen.has(name))
155
+ this.fail(`duplicate directive '${name}'`, nameTok);
156
+ seen.add(name);
157
+ this.expectOp('=');
158
+ const value = this.parseLiteralString(`directive '${name}'`);
159
+ into[name] = value;
160
+ this.skipNl();
161
+ if (this.isOp(',')) {
162
+ this.next();
163
+ this.skipNl();
164
+ }
165
+ else if (!this.isOp('}')) {
166
+ this.fail(`expected ',' or '}' in directive`);
167
+ }
168
+ }
169
+ this.next();
170
+ if (seen.size === 0)
171
+ this.fail('empty directive', open);
172
+ }
173
+ parseLiteralString(what) {
174
+ const t = this.tok;
175
+ if (t.type !== 'str')
176
+ this.fail(`${what} requires a quoted string value`);
177
+ const parts = t.parts;
178
+ if (parts.some((p) => 'expr' in p))
179
+ this.fail(`${what} cannot contain interpolation`);
180
+ this.next();
181
+ return parts.map((p) => p.text).join('');
182
+ }
183
+ parseBlock() {
184
+ this.expectOp('{');
185
+ const body = [];
186
+ this.skipSeparators();
187
+ while (!this.isOp('}')) {
188
+ if (this.tok.type === 'eof')
189
+ this.fail("unclosed block, missing '}'");
190
+ body.push(this.parseStatement());
191
+ this.endOfStatement();
192
+ this.skipSeparators();
193
+ }
194
+ this.next();
195
+ return body;
196
+ }
197
+ parseStatement() {
198
+ const t = this.tok;
199
+ const line = t.line;
200
+ if (t.type === 'op' && t.value === '@')
201
+ this.fail('directives are only allowed at the top of the script');
202
+ if (t.type === 'name') {
203
+ switch (t.value) {
204
+ case 'let': return this.parseLet();
205
+ case 'if': return this.parseIf();
206
+ case 'for': {
207
+ this.next();
208
+ const vars = [this.expectIdent('a loop variable')];
209
+ while (this.isOp(',')) {
210
+ this.next();
211
+ vars.push(this.expectIdent('a loop variable'));
212
+ }
213
+ this.expectKeyword('in');
214
+ const iter = this.parseExpr();
215
+ return { t: 'for', line, vars, iter, body: this.parseLoopBody() };
216
+ }
217
+ case 'while': {
218
+ this.next();
219
+ const cond = this.parseExpr();
220
+ return { t: 'while', line, cond, body: this.parseLoopBody() };
221
+ }
222
+ case 'break':
223
+ case 'continue':
224
+ if (this.loopDepth === 0)
225
+ this.fail(`'${t.value}' outside of a loop`);
226
+ this.next();
227
+ return { t: t.value, line };
228
+ case 'pass':
229
+ this.next();
230
+ return { t: 'pass', line };
231
+ case 'def': return this.parseDef();
232
+ case 'return': {
233
+ this.next();
234
+ return { t: 'return', line, value: this.atStatementEnd() ? undefined : this.parseExpr() };
235
+ }
236
+ case 'try': {
237
+ this.next();
238
+ const body = this.parseBlock();
239
+ if (!this.keywordAfterNl('except'))
240
+ this.fail("'try' requires an 'except <name>' block");
241
+ this.skipNl();
242
+ this.next();
243
+ const errName = this.expectIdent('a name for the caught failure');
244
+ return { t: 'try', line, body, errName, handler: this.parseBlock() };
245
+ }
246
+ case 'except':
247
+ this.fail("'except' without matching 'try'");
248
+ break;
249
+ case 'elif':
250
+ case 'else':
251
+ this.fail(`'${t.value}' without matching 'if'`);
252
+ break;
253
+ case 'assert': {
254
+ this.next();
255
+ const cond = this.parseExpr();
256
+ const message = this.atStatementEnd() ? undefined : this.parseExpr();
257
+ return { t: 'assert', line, cond, message };
258
+ }
259
+ case 'fail': {
260
+ this.next();
261
+ return { t: 'fail', line, reason: this.parseExpr() };
262
+ }
263
+ case 'success': {
264
+ this.next();
265
+ return { t: 'success', line, value: this.atStatementEnd() ? undefined : this.parseExpr() };
266
+ }
267
+ case 'enforce':
268
+ case 'forbid': {
269
+ const kind = t.value;
270
+ this.next();
271
+ const text = this.parseExpr();
272
+ return { t: 'constraint', line, kind, text, body: this.parseBlock() };
273
+ }
274
+ case 'restrict': return this.parseRestrict();
275
+ default:
276
+ break;
277
+ }
278
+ }
279
+ if (t.type === 'op' && t.value === '{')
280
+ return { t: 'block', line, body: this.parseBlock() };
281
+ return this.parseSimpleStatement();
282
+ }
283
+ parseLet() {
284
+ const line = this.tok.line;
285
+ this.next();
286
+ const startTok = this.tok;
287
+ if (startTok.type === 'name' && KEYWORDS.has(startTok.value)) {
288
+ this.fail(`'${startTok.value}' is a reserved word and cannot be used as a variable name`);
289
+ }
290
+ if (startTok.type !== 'name')
291
+ this.fail(`expected a variable name but found ${this.describe(startTok)}`);
292
+ const target = this.toTarget(this.parsePostfix(), startTok);
293
+ const opTok = this.tok;
294
+ if (opTok.type !== 'op' || !ASSIGN_OPS.has(opTok.value))
295
+ this.fail(`expected '=' after the target of 'let' but found ${this.describe(opTok)}`);
296
+ this.next();
297
+ return { t: 'let', line, target, op: opTok.value, value: this.parseExpr() };
298
+ }
299
+ parseIf() {
300
+ const line = this.tok.line;
301
+ this.next();
302
+ const branches = [];
303
+ let orelse;
304
+ const cond = this.parseExpr();
305
+ branches.push({ cond, body: this.parseBlock() });
306
+ for (;;) {
307
+ if (this.keywordAfterNl('elif')) {
308
+ this.skipNl();
309
+ this.next();
310
+ const c = this.parseExpr();
311
+ branches.push({ cond: c, body: this.parseBlock() });
312
+ }
313
+ else if (this.keywordAfterNl('else')) {
314
+ this.skipNl();
315
+ this.next();
316
+ orelse = this.parseBlock();
317
+ break;
318
+ }
319
+ else {
320
+ break;
321
+ }
322
+ }
323
+ return { t: 'if', line, branches, orelse };
324
+ }
325
+ parseDef() {
326
+ const line = this.tok.line;
327
+ this.next();
328
+ const name = this.expectIdent('a function name');
329
+ this.expectOp('(');
330
+ const params = [];
331
+ while (!this.isOp(')')) {
332
+ const nameTok = this.tok;
333
+ const p = this.expectIdent('a parameter name');
334
+ if (params.includes(p))
335
+ this.fail(`duplicate parameter '${p}'`, nameTok);
336
+ params.push(p);
337
+ if (this.isOp(','))
338
+ this.next();
339
+ else if (!this.isOp(')'))
340
+ this.fail("expected ',' or ')' in parameter list");
341
+ }
342
+ this.next();
343
+ const savedLoop = this.loopDepth;
344
+ this.loopDepth = 0;
345
+ const body = this.parseBlock();
346
+ this.loopDepth = savedLoop;
347
+ return { t: 'def', line, name, params, body };
348
+ }
349
+ parseLoopBody() {
350
+ this.loopDepth++;
351
+ const body = this.parseBlock();
352
+ this.loopDepth--;
353
+ return body;
354
+ }
355
+ parseRestrict() {
356
+ const line = this.tok.line;
357
+ this.next();
358
+ let kind = 'fs';
359
+ const t = this.tok;
360
+ if (t.type === 'name' && (t.value === 'fs' || t.value === 'net' || t.value === 'exec')) {
361
+ kind = t.value;
362
+ this.next();
363
+ }
364
+ const arg = this.parseExpr();
365
+ return { t: 'restrict', line, kind, arg, body: this.parseBlock() };
366
+ }
367
+ parseSimpleStatement() {
368
+ const line = this.tok.line;
369
+ const e = this.parseExpr();
370
+ const t = this.tok;
371
+ if (t.type === 'op' && ASSIGN_OPS.has(t.value)) {
372
+ this.fail(`variables are modified with 'let' (write 'let <target> ${t.value} ...')`, t);
373
+ }
374
+ return { t: 'expr', line, e };
375
+ }
376
+ toTarget(e, at) {
377
+ if (e.t === 'name')
378
+ return { t: 'name', name: e.name };
379
+ if (e.t === 'member')
380
+ return { t: 'member', obj: e.obj, name: e.name };
381
+ if (e.t === 'index')
382
+ return { t: 'index', obj: e.obj, index: e.index };
383
+ return this.fail('invalid assignment target', at);
384
+ }
385
+ // ---- expressions ---------------------------------------------------
386
+ parseExpr() {
387
+ const e = this.parseOr();
388
+ if (this.isName('if')) {
389
+ this.next();
390
+ const cond = this.parseOr();
391
+ this.expectKeyword('else');
392
+ return { t: 'ternary', cond, then: e, else: this.parseExpr() };
393
+ }
394
+ return e;
395
+ }
396
+ parseOr() {
397
+ let l = this.parseAnd();
398
+ while (this.isName('or')) {
399
+ this.next();
400
+ l = { t: 'logic', op: 'or', l, r: this.parseAnd() };
401
+ }
402
+ return l;
403
+ }
404
+ parseAnd() {
405
+ let l = this.parseNot();
406
+ while (this.isName('and')) {
407
+ this.next();
408
+ l = { t: 'logic', op: 'and', l, r: this.parseNot() };
409
+ }
410
+ return l;
411
+ }
412
+ parseNot() {
413
+ if (this.isName('not')) {
414
+ this.next();
415
+ return { t: 'unary', op: 'not', e: this.parseNot() };
416
+ }
417
+ return this.parseComparison();
418
+ }
419
+ parseComparison() {
420
+ const first = this.parseAdd();
421
+ const ops = [];
422
+ const operands = [first];
423
+ for (;;) {
424
+ const t = this.tok;
425
+ let op;
426
+ if (t.type === 'op' && ['==', '!=', '<', '<=', '>', '>='].includes(t.value)) {
427
+ op = t.value;
428
+ this.next();
429
+ }
430
+ else if (this.isName('in')) {
431
+ op = 'in';
432
+ this.next();
433
+ }
434
+ else if (this.isName('not') && this.isName('in', this.peekAt(1))) {
435
+ op = 'not in';
436
+ this.next();
437
+ this.next();
438
+ }
439
+ if (!op)
440
+ break;
441
+ ops.push(op);
442
+ operands.push(this.parseAdd());
443
+ }
444
+ return ops.length ? { t: 'compare', ops, operands } : first;
445
+ }
446
+ parseAdd() {
447
+ let l = this.parseMul();
448
+ while (this.isOp('+') || this.isOp('-')) {
449
+ const op = this.next().value;
450
+ l = { t: 'binary', op, l, r: this.parseMul() };
451
+ }
452
+ return l;
453
+ }
454
+ parseMul() {
455
+ let l = this.parseUnary();
456
+ while (this.isOp('*') || this.isOp('/') || this.isOp('//') || this.isOp('%')) {
457
+ const op = this.next().value;
458
+ l = { t: 'binary', op, l, r: this.parseUnary() };
459
+ }
460
+ return l;
461
+ }
462
+ parseUnary() {
463
+ if (this.isOp('-') || this.isOp('+')) {
464
+ const op = this.next().value;
465
+ return { t: 'unary', op, e: this.parseUnary() };
466
+ }
467
+ if (this.isOp('?')) {
468
+ this.next();
469
+ const mods = this.isOp('{') ? this.parseMods('?') : {};
470
+ return { t: 'ask', mods, prompt: this.parsePrompt('?') };
471
+ }
472
+ return this.parsePower();
473
+ }
474
+ parsePower() {
475
+ const base = this.parsePostfix();
476
+ if (this.isOp('**')) {
477
+ this.next();
478
+ return { t: 'binary', op: '**', l: base, r: this.parseUnary() };
479
+ }
480
+ return base;
481
+ }
482
+ parsePostfix() {
483
+ let e = this.parsePrimary();
484
+ for (;;) {
485
+ if (this.isOp('(')) {
486
+ this.next();
487
+ const args = [];
488
+ while (!this.isOp(')')) {
489
+ args.push(this.parseExpr());
490
+ if (this.isOp(','))
491
+ this.next();
492
+ else if (!this.isOp(')'))
493
+ this.fail("expected ',' or ')' in call");
494
+ }
495
+ this.next();
496
+ e = { t: 'call', callee: e, args };
497
+ }
498
+ else if (this.isOp('[')) {
499
+ this.next();
500
+ e = this.parseSubscript(e);
501
+ }
502
+ else if (this.isOp('.')) {
503
+ this.next();
504
+ const t = this.tok;
505
+ if (t.type !== 'name')
506
+ this.fail("expected a member name after '.'");
507
+ this.next();
508
+ e = { t: 'member', obj: e, name: t.value };
509
+ }
510
+ else {
511
+ return e;
512
+ }
513
+ }
514
+ }
515
+ parseSubscript(obj) {
516
+ const lo = this.isOp(':') ? undefined : this.parseExpr();
517
+ if (lo && !this.isOp(':')) {
518
+ this.expectOp(']');
519
+ return { t: 'index', obj, index: lo };
520
+ }
521
+ this.expectOp(':');
522
+ const hi = this.isOp(']') ? undefined : this.parseExpr();
523
+ this.expectOp(']');
524
+ return { t: 'slice', obj, lo, hi };
525
+ }
526
+ parsePrimary() {
527
+ const t = this.tok;
528
+ switch (t.type) {
529
+ case 'num':
530
+ this.next();
531
+ return { t: 'num', v: t.value };
532
+ case 'str':
533
+ this.next();
534
+ return { t: 'str', parts: this.convertParts(t.parts) };
535
+ case 'op':
536
+ return this.parseOpPrimary(t);
537
+ case 'name':
538
+ return this.parseNamePrimary(t);
539
+ default:
540
+ return this.fail(`unexpected ${this.describe(t)}`);
541
+ }
542
+ }
543
+ parseOpPrimary(t) {
544
+ if (t.value === '(') {
545
+ this.next();
546
+ const e = this.parseExpr();
547
+ this.expectOp(')');
548
+ return e;
549
+ }
550
+ if (t.value === '[') {
551
+ this.next();
552
+ const items = [];
553
+ while (!this.isOp(']')) {
554
+ items.push(this.parseExpr());
555
+ if (this.isOp(','))
556
+ this.next();
557
+ else if (!this.isOp(']'))
558
+ this.fail("expected ',' or ']' in list");
559
+ }
560
+ this.next();
561
+ return { t: 'list', items };
562
+ }
563
+ if (t.value === '{') {
564
+ this.next();
565
+ this.skipNl();
566
+ const entries = [];
567
+ while (!this.isOp('}')) {
568
+ const k = this.parseExpr();
569
+ this.expectOp(':');
570
+ this.skipNl();
571
+ entries.push([k, this.parseExpr()]);
572
+ this.skipNl();
573
+ if (this.isOp(',')) {
574
+ this.next();
575
+ this.skipNl();
576
+ }
577
+ else if (!this.isOp('}')) {
578
+ this.fail("expected ',' or '}' in dict");
579
+ }
580
+ }
581
+ this.next();
582
+ return { t: 'dict', entries };
583
+ }
584
+ if (t.value === '?') {
585
+ return this.fail("'?' is a prefix operator and cannot be used here");
586
+ }
587
+ return this.fail(`unexpected ${this.describe(t)}`);
588
+ }
589
+ parseNamePrimary(t) {
590
+ switch (t.value) {
591
+ case 'True':
592
+ this.next();
593
+ return { t: 'bool', v: true };
594
+ case 'False':
595
+ this.next();
596
+ return { t: 'bool', v: false };
597
+ case 'None':
598
+ this.next();
599
+ return { t: 'none' };
600
+ case 'think': {
601
+ this.next();
602
+ const mods = this.isOp('{') ? this.parseMods('think') : {};
603
+ return { t: 'think', mods, prompt: this.parsePrompt('think') };
604
+ }
605
+ case 'read_file':
606
+ case 'write_file':
607
+ case 'web_fetch':
608
+ case 'web_search':
609
+ case 'shell':
610
+ return this.parseToolCall(t.value);
611
+ default:
612
+ if (KEYWORDS.has(t.value))
613
+ this.fail(`unexpected keyword '${t.value}'`);
614
+ this.next();
615
+ return { t: 'name', name: t.value, line: t.line };
616
+ }
617
+ }
618
+ parsePrompt(cmd) {
619
+ if (this.atStatementEnd())
620
+ this.fail(`'${cmd}' requires a prompt`);
621
+ return this.parsePostfix();
622
+ }
623
+ parseMods(cmd) {
624
+ const schema = MODIFIER_SCHEMA[cmd];
625
+ this.expectOp('{');
626
+ this.skipNl();
627
+ const out = {};
628
+ while (!this.isOp('}')) {
629
+ const nameTok = this.tok;
630
+ if (nameTok.type !== 'name')
631
+ this.fail('expected a modifier name');
632
+ const name = nameTok.value;
633
+ this.next();
634
+ if (!(name in schema)) {
635
+ this.fail(`unknown modifier '${name}' for '${cmd}' (allowed: ${Object.keys(schema).join(', ')})`, nameTok);
636
+ }
637
+ if (name in out)
638
+ this.fail(`duplicate modifier '${name}'`, nameTok);
639
+ this.expectOp('=');
640
+ const valTok = this.tok;
641
+ let value;
642
+ if (valTok.type === 'str')
643
+ value = this.parseLiteralString(`modifier '${name}'`);
644
+ else if (this.isName('True') || this.isName('False'))
645
+ value = this.next().value === 'True';
646
+ else
647
+ return this.fail(`modifier '${name}' requires a quoted string or True/False`);
648
+ if (!schema[name].includes(value)) {
649
+ const allowed = schema[name].map((v) => (typeof v === 'string' ? `'${v}'` : v ? 'True' : 'False'));
650
+ this.fail(`invalid value ${typeof value === 'string' ? `'${value}'` : value ? 'True' : 'False'} for modifier '${name}' (allowed: ${allowed.join(', ')})`, valTok);
651
+ }
652
+ out[name] = value;
653
+ this.skipNl();
654
+ if (this.isOp(',')) {
655
+ this.next();
656
+ this.skipNl();
657
+ }
658
+ else if (!this.isOp('}')) {
659
+ this.fail("expected ',' or '}' in modifiers");
660
+ }
661
+ }
662
+ this.next();
663
+ return out;
664
+ }
665
+ parseToolCall(tool) {
666
+ this.next();
667
+ const schema = TOOL_SCHEMA[tool];
668
+ const params = {};
669
+ if (this.isOp('{')) {
670
+ this.next();
671
+ this.skipNl();
672
+ while (!this.isOp('}')) {
673
+ const nameTok = this.tok;
674
+ if (nameTok.type !== 'name')
675
+ this.fail('expected a parameter name');
676
+ const name = nameTok.value;
677
+ this.next();
678
+ if (!schema.params.includes(name)) {
679
+ this.fail(`unknown parameter '${name}' for '${tool}' (allowed: ${schema.params.join(', ')})`, nameTok);
680
+ }
681
+ if (name in params)
682
+ this.fail(`duplicate parameter '${name}'`, nameTok);
683
+ this.expectOp('=');
684
+ params[name] = this.parseExpr();
685
+ this.skipNl();
686
+ if (this.isOp(',')) {
687
+ this.next();
688
+ this.skipNl();
689
+ }
690
+ else if (!this.isOp('}')) {
691
+ this.fail("expected ',' or '}' in tool parameters");
692
+ }
693
+ }
694
+ this.next();
695
+ }
696
+ return { t: 'tool', tool, params, instructions: this.parsePrompt(tool) };
697
+ }
698
+ convertParts(parts) {
699
+ return parts.map((p) => {
700
+ if ('text' in p)
701
+ return p.text;
702
+ const sub = new Parser(tokenize(p.expr, { inExpr: true, line: p.line, col: p.col }), this.lines);
703
+ const e = sub.parseExpr();
704
+ if (sub.tok.type !== 'eof')
705
+ sub.fail(`unexpected ${sub.describe(sub.tok)} in interpolation`);
706
+ return e;
707
+ });
708
+ }
709
+ }