future-lang 0.3.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/ARCHITECTURE.md +424 -0
- package/MIGRATION.md +365 -0
- package/README.md +370 -0
- package/ROADMAP.md +263 -0
- package/examples/adult.future +8 -0
- package/examples/api.future +11 -0
- package/examples/assistant.future +8 -0
- package/examples/browser-demo.html +164 -0
- package/examples/greet.future +7 -0
- package/examples/hello.future +1 -0
- package/examples/math.future +8 -0
- package/examples/mini-app.html +301 -0
- package/examples/smarthome.future +10 -0
- package/future-browser.js +102 -0
- package/future-playground.html +650 -0
- package/package.json +27 -0
- package/runtime/ai.js +92 -0
- package/runtime/browser.js +458 -0
- package/runtime/device.js +36 -0
- package/runtime/home.js +19 -0
- package/runtime/http.js +32 -0
- package/runtime/index.js +403 -0
- package/runtime/lsp-metadata.js +104 -0
- package/runtime/math.js +16 -0
- package/runtime/memory.js +61 -0
- package/runtime/mqtt.js +49 -0
- package/runtime/providers/anthropic.js +59 -0
- package/runtime/providers/index.js +93 -0
- package/runtime/providers/openai-compat.js +85 -0
- package/runtime/providers/util.js +70 -0
- package/runtime/rag/chunker.js +65 -0
- package/runtime/rag/pipeline.js +86 -0
- package/runtime/rag/vector-store.js +119 -0
- package/runtime/rag.js +94 -0
- package/runtime/schedule.js +77 -0
- package/runtime/system.js +101 -0
- package/runtime/tts.js +38 -0
- package/runtime/vision.js +85 -0
- package/server.js +42 -0
- package/src/ast.js +202 -0
- package/src/cli.js +391 -0
- package/src/errors.js +21 -0
- package/src/formatter.js +48 -0
- package/src/generator.js +457 -0
- package/src/index.js +48 -0
- package/src/lexer.js +248 -0
- package/src/parser.js +469 -0
package/src/parser.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
// parser.js
|
|
2
|
+
// Phase 2 — Parser.
|
|
3
|
+
// A hand-written recursive-descent parser that turns a token list into an AST.
|
|
4
|
+
//
|
|
5
|
+
// Statement boundaries are determined structurally (not by newlines), so
|
|
6
|
+
// `name = "John" age = 30` correctly parses as two statements: after the
|
|
7
|
+
// expression `"John"` there is no operator, so the assignment ends and a new
|
|
8
|
+
// statement begins.
|
|
9
|
+
|
|
10
|
+
import { FutureError } from './errors.js';
|
|
11
|
+
import * as AST from './ast.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Tokens that can never *start* an expression. Used to decide whether an
|
|
15
|
+
* optional expression (e.g. the value after `return`) is present.
|
|
16
|
+
*/
|
|
17
|
+
const EXPR_TERMINATORS = new Set(['END', 'ELSE', 'CATCH', 'EOF']);
|
|
18
|
+
|
|
19
|
+
export class Parser {
|
|
20
|
+
/** @param {import('./lexer.js').Token[]} tokens */
|
|
21
|
+
constructor(tokens) {
|
|
22
|
+
this.tokens = tokens;
|
|
23
|
+
this.pos = 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- token stream helpers ---------------------------------------------------
|
|
27
|
+
|
|
28
|
+
peek(offset = 0) {
|
|
29
|
+
const i = Math.min(this.pos + offset, this.tokens.length - 1);
|
|
30
|
+
return this.tokens[i];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
advance() {
|
|
34
|
+
const token = this.tokens[this.pos];
|
|
35
|
+
if (this.pos < this.tokens.length - 1) this.pos++;
|
|
36
|
+
return token;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
check(type) {
|
|
40
|
+
return this.peek().type === type;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Consume and return the next token if it matches any given type, else null. */
|
|
44
|
+
match(...types) {
|
|
45
|
+
return types.includes(this.peek().type) ? this.advance() : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Consume a token of `type` or throw a diagnostic. */
|
|
49
|
+
expect(type, description) {
|
|
50
|
+
if (this.check(type)) return this.advance();
|
|
51
|
+
const tok = this.peek();
|
|
52
|
+
const found = tok.type === 'EOF' ? 'end of file' : `'${tok.value}'`;
|
|
53
|
+
throw new FutureError(
|
|
54
|
+
`Expected ${description ?? type} but found ${found}`,
|
|
55
|
+
tok.line, tok.column, 'parse',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- grammar ----------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/** Entry point. @returns {object} a Program node. */
|
|
62
|
+
parse() {
|
|
63
|
+
const body = [];
|
|
64
|
+
while (!this.check('EOF')) {
|
|
65
|
+
body.push(this.parseStatement());
|
|
66
|
+
}
|
|
67
|
+
return AST.Program(body);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
parseStatement() {
|
|
71
|
+
const tok = this.peek();
|
|
72
|
+
switch (tok.type) {
|
|
73
|
+
case 'PRINT': return this.parsePrint();
|
|
74
|
+
case 'IF': return this.parseIf();
|
|
75
|
+
case 'FUNCTION': return this.parseFunction();
|
|
76
|
+
case 'RETURN': return this.parseReturn();
|
|
77
|
+
case 'ON': return this.parseOn();
|
|
78
|
+
case 'EVERY': return this.parseEvery();
|
|
79
|
+
case 'FOR': return this.parseFor();
|
|
80
|
+
case 'WHILE': return this.parseWhile();
|
|
81
|
+
case 'TRY': return this.parseTry();
|
|
82
|
+
case 'STREAM': return this.parseStream();
|
|
83
|
+
case 'AGENT': return this.parseAgent();
|
|
84
|
+
case 'USE': return this.parseUse();
|
|
85
|
+
case 'IDENTIFIER':
|
|
86
|
+
// One token of lookahead separates assignment from a bare call/expression.
|
|
87
|
+
if (this.peek(1).type === 'ASSIGN') return this.parseAssignment();
|
|
88
|
+
return this.parseExpressionStatement();
|
|
89
|
+
default:
|
|
90
|
+
return this.parseExpressionStatement();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
parsePrint() {
|
|
95
|
+
const kw = this.advance(); // PRINT
|
|
96
|
+
const expression = this.parseExpression();
|
|
97
|
+
return AST.PrintStatement(expression, kw.line, kw.column);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Top-level `use "./file.future"` or `use "./file.future" as alias`.
|
|
102
|
+
* Inside an agent block, `use capability` (IDENTIFIER) is handled by parseAgent.
|
|
103
|
+
*/
|
|
104
|
+
parseUse() {
|
|
105
|
+
const kw = this.advance(); // USE
|
|
106
|
+
const path = this.expect('STRING', 'file path string after "use"');
|
|
107
|
+
let alias = null;
|
|
108
|
+
if (this.match('AS')) {
|
|
109
|
+
alias = this.expect('IDENTIFIER', 'alias name after "as"').value;
|
|
110
|
+
}
|
|
111
|
+
return AST.UseStatement(path.value, alias, kw.line, kw.column);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
parseAssignment() {
|
|
115
|
+
const name = this.advance(); // IDENTIFIER
|
|
116
|
+
this.expect('ASSIGN', "'='");
|
|
117
|
+
const value = this.parseExpression();
|
|
118
|
+
return AST.Assignment(name.value, value, name.line, name.column);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
parseIf() {
|
|
122
|
+
const kw = this.advance(); // IF
|
|
123
|
+
const condition = this.parseExpression();
|
|
124
|
+
const consequent = this.parseBlock(['ELSE', 'END']);
|
|
125
|
+
let alternate = null;
|
|
126
|
+
if (this.check('ELSE')) {
|
|
127
|
+
this.advance();
|
|
128
|
+
alternate = this.parseBlock(['END']);
|
|
129
|
+
}
|
|
130
|
+
this.expect('END', "'end'");
|
|
131
|
+
return AST.IfStatement(condition, consequent, alternate, kw.line, kw.column);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
parseFunction() {
|
|
135
|
+
const kw = this.advance(); // FUNCTION
|
|
136
|
+
const name = this.expect('IDENTIFIER', 'function name');
|
|
137
|
+
this.expect('LPAREN', "'('");
|
|
138
|
+
const params = [];
|
|
139
|
+
if (!this.check('RPAREN')) {
|
|
140
|
+
do {
|
|
141
|
+
params.push(this.expect('IDENTIFIER', 'parameter name').value);
|
|
142
|
+
} while (this.match('COMMA'));
|
|
143
|
+
}
|
|
144
|
+
this.expect('RPAREN', "')'");
|
|
145
|
+
const body = this.parseBlock(['END']);
|
|
146
|
+
this.expect('END', "'end'");
|
|
147
|
+
return AST.FunctionDeclaration(name.value, params, body, kw.line, kw.column);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
parseReturn() {
|
|
151
|
+
const kw = this.advance(); // RETURN
|
|
152
|
+
let argument = null;
|
|
153
|
+
if (!EXPR_TERMINATORS.has(this.peek().type)) {
|
|
154
|
+
argument = this.parseExpression();
|
|
155
|
+
}
|
|
156
|
+
return AST.ReturnStatement(argument, kw.line, kw.column);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
parseExpressionStatement() {
|
|
160
|
+
const expr = this.parseExpression();
|
|
161
|
+
return AST.ExpressionStatement(expr, expr.line, expr.column);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* `for <variable> in <iterable-expr> ... end`
|
|
166
|
+
*/
|
|
167
|
+
parseFor() {
|
|
168
|
+
const kw = this.advance(); // FOR
|
|
169
|
+
const variable = this.expect('IDENTIFIER', 'loop variable name');
|
|
170
|
+
this.expect('IN', "'in'");
|
|
171
|
+
const iterable = this.parseExpression();
|
|
172
|
+
const body = this.parseBlock(['END']);
|
|
173
|
+
this.expect('END', "'end'");
|
|
174
|
+
return AST.ForStatement(variable.value, iterable, body, kw.line, kw.column);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* `try ... catch <errorVar> ... end`
|
|
179
|
+
*/
|
|
180
|
+
parseTry() {
|
|
181
|
+
const kw = this.advance(); // TRY
|
|
182
|
+
const body = this.parseBlock(['CATCH']);
|
|
183
|
+
this.expect('CATCH', "'catch'");
|
|
184
|
+
const errVar = this.expect('IDENTIFIER', 'error variable name');
|
|
185
|
+
const catchBody = this.parseBlock(['END']);
|
|
186
|
+
this.expect('END', "'end'");
|
|
187
|
+
return AST.TryStatement(body, errVar.value, catchBody, kw.line, kw.column);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* `agent <name>
|
|
192
|
+
* use <capability>
|
|
193
|
+
* ... body statements ...
|
|
194
|
+
* end`
|
|
195
|
+
*
|
|
196
|
+
* `use <cap>` lines are collected as the capabilities list.
|
|
197
|
+
* The body receives an implicit `goal` variable — the argument passed at call-site.
|
|
198
|
+
* Compiles to: async function name(goal) { ... }
|
|
199
|
+
*/
|
|
200
|
+
parseAgent() {
|
|
201
|
+
const kw = this.advance(); // AGENT
|
|
202
|
+
const name = this.expect('IDENTIFIER', 'agent name');
|
|
203
|
+
const capabilities = [];
|
|
204
|
+
const body = [];
|
|
205
|
+
while (!this.check('END') && !this.check('EOF')) {
|
|
206
|
+
if (this.check('USE')) {
|
|
207
|
+
this.advance(); // USE
|
|
208
|
+
const cap = this.expect('IDENTIFIER', 'capability name after "use"');
|
|
209
|
+
capabilities.push(cap.value);
|
|
210
|
+
} else {
|
|
211
|
+
body.push(this.parseStatement());
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.expect('END', "'end'");
|
|
215
|
+
return AST.AgentDeclaration(name.value, capabilities, body, kw.line, kw.column);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* `while <condition> ... end`
|
|
220
|
+
*/
|
|
221
|
+
parseWhile() {
|
|
222
|
+
const kw = this.advance(); // WHILE
|
|
223
|
+
const condition = this.parseExpression();
|
|
224
|
+
const body = this.parseBlock(['END']);
|
|
225
|
+
this.expect('END', "'end'");
|
|
226
|
+
return AST.WhileStatement(condition, body, kw.line, kw.column);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* `stream <call-expr> ... end`
|
|
231
|
+
* The call must be a capability call (e.g. ai.ask("prompt")).
|
|
232
|
+
* The body receives an implicit `chunk` variable with each streamed piece.
|
|
233
|
+
* Compiles to: await __rt.<namespace>.stream(<args>, async (chunk) => { body })
|
|
234
|
+
*/
|
|
235
|
+
parseStream() {
|
|
236
|
+
const kw = this.advance(); // STREAM
|
|
237
|
+
const call = this.parseExpression();
|
|
238
|
+
const body = this.parseBlock(['END']);
|
|
239
|
+
this.expect('END', "'end'");
|
|
240
|
+
return AST.StreamStatement(call, body, kw.line, kw.column);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* `on <source> <channel-expr> ... end`
|
|
245
|
+
* e.g. `on mqtt "house/temp" ... end`
|
|
246
|
+
* The body receives an implicit `message` variable containing the event payload.
|
|
247
|
+
*/
|
|
248
|
+
parseOn() {
|
|
249
|
+
const kw = this.advance(); // ON
|
|
250
|
+
const source = this.expect('IDENTIFIER', 'event source name (e.g. mqtt)');
|
|
251
|
+
const channel = this.parseExpression();
|
|
252
|
+
const body = this.parseBlock(['END']);
|
|
253
|
+
this.expect('END', "'end'");
|
|
254
|
+
return AST.OnStatement(source.value, channel, body, kw.line, kw.column);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* `every <interval-expr> ... end`
|
|
259
|
+
* e.g. `every "30m" ... end`
|
|
260
|
+
* Compiles to schedule.every(interval, async () => { ... }).
|
|
261
|
+
*/
|
|
262
|
+
parseEvery() {
|
|
263
|
+
const kw = this.advance(); // EVERY
|
|
264
|
+
const interval = this.parseExpression();
|
|
265
|
+
const body = this.parseBlock(['END']);
|
|
266
|
+
this.expect('END', "'end'");
|
|
267
|
+
return AST.EveryStatement(interval, body, kw.line, kw.column);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Collect statements until one of `terminators` (or EOF) is next.
|
|
272
|
+
* Throws if EOF is reached before a terminator (e.g. a missing `end`).
|
|
273
|
+
*/
|
|
274
|
+
parseBlock(terminators) {
|
|
275
|
+
const statements = [];
|
|
276
|
+
while (!this.check('EOF') && !terminators.includes(this.peek().type)) {
|
|
277
|
+
statements.push(this.parseStatement());
|
|
278
|
+
}
|
|
279
|
+
if (this.check('EOF')) {
|
|
280
|
+
const tok = this.peek();
|
|
281
|
+
const expected = terminators.map((t) => `'${t.toLowerCase()}'`).join(' or ');
|
|
282
|
+
throw new FutureError(
|
|
283
|
+
`Unexpected end of file, expected ${expected}`,
|
|
284
|
+
tok.line, tok.column, 'parse',
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return statements;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- expressions (precedence climbing, lowest to highest) -------------------
|
|
291
|
+
|
|
292
|
+
parseExpression() {
|
|
293
|
+
return this.parseOr();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
parseOr() {
|
|
297
|
+
let left = this.parseAnd();
|
|
298
|
+
while (this.check('OR')) {
|
|
299
|
+
const op = this.advance();
|
|
300
|
+
left = AST.BinaryExpression('or', left, this.parseAnd(), op.line, op.column);
|
|
301
|
+
}
|
|
302
|
+
return left;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
parseAnd() {
|
|
306
|
+
let left = this.parseEquality();
|
|
307
|
+
while (this.check('AND')) {
|
|
308
|
+
const op = this.advance();
|
|
309
|
+
left = AST.BinaryExpression('and', left, this.parseEquality(), op.line, op.column);
|
|
310
|
+
}
|
|
311
|
+
return left;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
parseEquality() {
|
|
315
|
+
let left = this.parseComparison();
|
|
316
|
+
while (this.check('EQ') || this.check('NEQ')) {
|
|
317
|
+
const op = this.advance();
|
|
318
|
+
left = AST.BinaryExpression(op.value, left, this.parseComparison(), op.line, op.column);
|
|
319
|
+
}
|
|
320
|
+
return left;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
parseComparison() {
|
|
324
|
+
let left = this.parseAdditive();
|
|
325
|
+
while (['GT', 'GTE', 'LT', 'LTE'].includes(this.peek().type)) {
|
|
326
|
+
const op = this.advance();
|
|
327
|
+
left = AST.BinaryExpression(op.value, left, this.parseAdditive(), op.line, op.column);
|
|
328
|
+
}
|
|
329
|
+
return left;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
parseAdditive() {
|
|
333
|
+
let left = this.parseMultiplicative();
|
|
334
|
+
while (this.check('PLUS') || this.check('MINUS')) {
|
|
335
|
+
const op = this.advance();
|
|
336
|
+
left = AST.BinaryExpression(op.value, left, this.parseMultiplicative(), op.line, op.column);
|
|
337
|
+
}
|
|
338
|
+
return left;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
parseMultiplicative() {
|
|
342
|
+
let left = this.parseUnary();
|
|
343
|
+
while (this.check('STAR') || this.check('SLASH')) {
|
|
344
|
+
const op = this.advance();
|
|
345
|
+
left = AST.BinaryExpression(op.value, left, this.parseUnary(), op.line, op.column);
|
|
346
|
+
}
|
|
347
|
+
return left;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
parseUnary() {
|
|
351
|
+
if (this.check('NOT') || this.check('MINUS')) {
|
|
352
|
+
const op = this.advance();
|
|
353
|
+
const operator = op.type === 'NOT' ? 'not' : '-';
|
|
354
|
+
return AST.UnaryExpression(operator, this.parseUnary(), op.line, op.column);
|
|
355
|
+
}
|
|
356
|
+
return this.parsePrimary();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// primary = atom followed by any chain of `.member` and `(call)` postfixes.
|
|
360
|
+
// This is what powers capability calls like http.get("...") and ai.ask("..."),
|
|
361
|
+
// as well as ordinary property access like todo.title.
|
|
362
|
+
parsePrimary() {
|
|
363
|
+
let node = this.parseAtom();
|
|
364
|
+
while (true) {
|
|
365
|
+
if (this.match('DOT')) {
|
|
366
|
+
const prop = this.expect('IDENTIFIER', 'property name after "."');
|
|
367
|
+
node = AST.MemberExpression(node, prop.value, prop.line, prop.column);
|
|
368
|
+
} else if (this.check('LPAREN')) {
|
|
369
|
+
node = this.finishCall(node);
|
|
370
|
+
} else {
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return node;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
parseAtom() {
|
|
378
|
+
const tok = this.peek();
|
|
379
|
+
switch (tok.type) {
|
|
380
|
+
case 'NUMBER':
|
|
381
|
+
this.advance();
|
|
382
|
+
return AST.NumberLiteral(tok.value, tok.line, tok.column);
|
|
383
|
+
case 'STRING':
|
|
384
|
+
this.advance();
|
|
385
|
+
return AST.StringLiteral(tok.value, tok.line, tok.column);
|
|
386
|
+
case 'TRUE':
|
|
387
|
+
this.advance();
|
|
388
|
+
return AST.BooleanLiteral(true, tok.line, tok.column);
|
|
389
|
+
case 'FALSE':
|
|
390
|
+
this.advance();
|
|
391
|
+
return AST.BooleanLiteral(false, tok.line, tok.column);
|
|
392
|
+
case 'IDENTIFIER':
|
|
393
|
+
this.advance();
|
|
394
|
+
return AST.Identifier(tok.value, tok.line, tok.column);
|
|
395
|
+
case 'LPAREN': {
|
|
396
|
+
this.advance();
|
|
397
|
+
const expr = this.parseExpression();
|
|
398
|
+
this.expect('RPAREN', "')'");
|
|
399
|
+
return expr;
|
|
400
|
+
}
|
|
401
|
+
case 'NULL':
|
|
402
|
+
this.advance();
|
|
403
|
+
return AST.NullLiteral(tok.line, tok.column);
|
|
404
|
+
case 'LBRACKET':
|
|
405
|
+
return this.parseArrayLiteral();
|
|
406
|
+
case 'LBRACE':
|
|
407
|
+
return this.parseObjectLiteral();
|
|
408
|
+
default: {
|
|
409
|
+
const found = tok.type === 'EOF' ? 'end of file' : `'${tok.value}'`;
|
|
410
|
+
throw new FutureError(`Unexpected token ${found}`, tok.line, tok.column, 'parse');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** `[expr, expr, ...]` — commas between elements are required. */
|
|
416
|
+
parseArrayLiteral() {
|
|
417
|
+
const tok = this.peek();
|
|
418
|
+
this.expect('LBRACKET', "'['");
|
|
419
|
+
const elements = [];
|
|
420
|
+
while (!this.check('RBRACKET') && !this.check('EOF')) {
|
|
421
|
+
elements.push(this.parseExpression());
|
|
422
|
+
if (!this.match('COMMA')) break; // trailing comma OK, comma required between elements
|
|
423
|
+
}
|
|
424
|
+
this.expect('RBRACKET', "']'");
|
|
425
|
+
return AST.ArrayLiteral(elements, tok.line, tok.column);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* `{ key: expr key: expr }` — no commas, whitespace/newline as separator.
|
|
430
|
+
* The lexer skips all whitespace, so properties are separated implicitly.
|
|
431
|
+
*/
|
|
432
|
+
parseObjectLiteral() {
|
|
433
|
+
const tok = this.peek();
|
|
434
|
+
this.expect('LBRACE', "'{'");
|
|
435
|
+
const properties = [];
|
|
436
|
+
while (!this.check('RBRACE') && !this.check('EOF')) {
|
|
437
|
+
const key = this.expect('IDENTIFIER', 'property name');
|
|
438
|
+
this.expect('COLON', "':'");
|
|
439
|
+
const value = this.parseExpression();
|
|
440
|
+
properties.push({ key: key.value, value });
|
|
441
|
+
this.match('COMMA'); // optional comma — allows both styles
|
|
442
|
+
}
|
|
443
|
+
this.expect('RBRACE', "'}'");
|
|
444
|
+
return AST.ObjectLiteral(properties, tok.line, tok.column);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// `callee` is the already-parsed expression preceding the '(' (an Identifier
|
|
448
|
+
// such as greet, or a MemberExpression such as http.get).
|
|
449
|
+
finishCall(callee) {
|
|
450
|
+
this.expect('LPAREN', "'('");
|
|
451
|
+
const args = [];
|
|
452
|
+
if (!this.check('RPAREN')) {
|
|
453
|
+
do {
|
|
454
|
+
args.push(this.parseExpression());
|
|
455
|
+
} while (this.match('COMMA'));
|
|
456
|
+
}
|
|
457
|
+
this.expect('RPAREN', "')'");
|
|
458
|
+
return AST.CallExpression(callee, args, callee.line, callee.column);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Convenience wrapper.
|
|
464
|
+
* @param {import('./lexer.js').Token[]} tokens
|
|
465
|
+
* @returns {object} Program node.
|
|
466
|
+
*/
|
|
467
|
+
export function parse(tokens) {
|
|
468
|
+
return new Parser(tokens).parse();
|
|
469
|
+
}
|