devabhasha 1.0.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/LICENSE +21 -0
- package/README.md +974 -0
- package/package.json +47 -0
- package/src/analyzer.js +125 -0
- package/src/bundler.js +129 -0
- package/src/cli.js +99 -0
- package/src/codegen.js +864 -0
- package/src/devserver.js +148 -0
- package/src/errors.js +71 -0
- package/src/index.js +30 -0
- package/src/io-browser.js +31 -0
- package/src/io-node.js +102 -0
- package/src/karaka-web.js +49 -0
- package/src/keywords.js +64 -0
- package/src/lexer.js +140 -0
- package/src/parser.js +687 -0
- package/src/server-node.js +120 -0
- package/src/server.js +182 -0
- package/src/stdlib.js +137 -0
- package/src/style.js +103 -0
- package/src/symbols.js +194 -0
- package/src/vibhakti.js +87 -0
package/src/parser.js
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
// parser.js — builds an AST from the token stream.
|
|
2
|
+
// Statements: recursive descent. Expressions: Pratt (precedence climbing).
|
|
3
|
+
|
|
4
|
+
import { analyze } from './vibhakti.js';
|
|
5
|
+
import { KARAKA_TO_SLOT, TAG_STEMS, EVENT_STEMS } from './karaka-web.js';
|
|
6
|
+
import { KEYWORDS } from './keywords.js';
|
|
7
|
+
import { DevabhashaError } from './errors.js';
|
|
8
|
+
import { tokenize } from './lexer.js';
|
|
9
|
+
|
|
10
|
+
// Set of token types produced by keywords — these may still be used as
|
|
11
|
+
// property names after a dot (property namespace is separate).
|
|
12
|
+
const KEYWORD_TOKENS = new Set(Object.values(KEYWORDS));
|
|
13
|
+
|
|
14
|
+
export function parse(tokens) {
|
|
15
|
+
let pos = 0;
|
|
16
|
+
|
|
17
|
+
const peek = (k = 0) => tokens[pos + k];
|
|
18
|
+
const next = () => tokens[pos++];
|
|
19
|
+
const check = (type, value) =>
|
|
20
|
+
peek().type === type && (value === undefined || peek().value === value);
|
|
21
|
+
|
|
22
|
+
function expect(type, value) {
|
|
23
|
+
if (!check(type, value)) {
|
|
24
|
+
const t = peek();
|
|
25
|
+
throw new DevabhashaError(
|
|
26
|
+
`expected ${value ?? type} but found '${t.value}' (${t.type})`,
|
|
27
|
+
{ line: t.line, col: t.col, kind: 'parse' }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return next();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// optional statement terminator
|
|
34
|
+
function eatSemi() {
|
|
35
|
+
while (check('SEMI')) next();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---- statements ----------------------------------------------------
|
|
39
|
+
|
|
40
|
+
function parseProgram() {
|
|
41
|
+
const body = [];
|
|
42
|
+
while (!check('EOF')) {
|
|
43
|
+
eatSemi();
|
|
44
|
+
if (check('EOF')) break;
|
|
45
|
+
body.push(parseStatement());
|
|
46
|
+
eatSemi();
|
|
47
|
+
}
|
|
48
|
+
return { type: 'Program', body };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseStatement() {
|
|
52
|
+
const startTok = peek();
|
|
53
|
+
const node = parseStatementInner();
|
|
54
|
+
// stamp source position (line/col of the statement's first token) so the
|
|
55
|
+
// codegen can emit a source map. Non-invasive: statement granularity only.
|
|
56
|
+
if (node && node.line == null && startTok) {
|
|
57
|
+
node.line = startTok.line;
|
|
58
|
+
node.col = startTok.col;
|
|
59
|
+
}
|
|
60
|
+
return node;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseStatementInner() {
|
|
64
|
+
if (check('LET') || check('CONST')) return parseVarDecl();
|
|
65
|
+
if (check('FUNC')) return parseFuncDecl(false);
|
|
66
|
+
if (check('ASYNC')) {
|
|
67
|
+
next(); // ASYNC
|
|
68
|
+
if (!check('FUNC')) {
|
|
69
|
+
throw new DevabhashaError('असमकालिकदोषः: असमकालिक must be followed by कार्य',
|
|
70
|
+
{ line: peek().line, col: peek().col, kind: 'parse' });
|
|
71
|
+
}
|
|
72
|
+
return parseFuncDecl(true);
|
|
73
|
+
}
|
|
74
|
+
if (check('RETURN')) return parseReturn();
|
|
75
|
+
if (check('IF')) return parseIf();
|
|
76
|
+
if (check('WHILE')) return parseWhile();
|
|
77
|
+
if (check('FOR')) return parseFor();
|
|
78
|
+
if (check('BREAK')) { next(); return { type: 'Break' }; }
|
|
79
|
+
if (check('CONTINUE')) { next(); return { type: 'Continue' }; }
|
|
80
|
+
if (check('PRINT')) return parsePrint();
|
|
81
|
+
if (check('STYLENAME')) return parseStyleName();
|
|
82
|
+
if (check('STATE')) return parseStateDecl();
|
|
83
|
+
if (check('VIEW')) return parseView();
|
|
84
|
+
if (check('EXPORT')) return parseExport();
|
|
85
|
+
if (check('IMPORT')) return parseImport();
|
|
86
|
+
if (check('OP', '{')) return parseBlock();
|
|
87
|
+
// expression statement
|
|
88
|
+
const expr = parseExpression();
|
|
89
|
+
return { type: 'ExpressionStatement', expression: expr };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseBlock() {
|
|
93
|
+
expect('OP', '{');
|
|
94
|
+
const body = [];
|
|
95
|
+
while (!check('OP', '}') && !check('EOF')) {
|
|
96
|
+
eatSemi();
|
|
97
|
+
if (check('OP', '}')) break;
|
|
98
|
+
body.push(parseStatement());
|
|
99
|
+
eatSemi();
|
|
100
|
+
}
|
|
101
|
+
expect('OP', '}');
|
|
102
|
+
return { type: 'Block', body };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseVarDecl() {
|
|
106
|
+
const kind = next().type; // LET | CONST
|
|
107
|
+
const nt = expect('IDENT');
|
|
108
|
+
let init = null;
|
|
109
|
+
if (check('OP', '=')) { next(); init = parseExpression(); }
|
|
110
|
+
return { type: 'VarDecl', kind, name: nt.value, init, namePos: { line: nt.line, col: nt.col } };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseFuncDecl(isAsync = false) {
|
|
114
|
+
next(); // FUNC
|
|
115
|
+
const nt = expect('IDENT');
|
|
116
|
+
const params = parseParams();
|
|
117
|
+
const body = parseBlock();
|
|
118
|
+
return { type: 'FuncDecl', name: nt.value, params, body, async: isAsync,
|
|
119
|
+
namePos: { line: nt.line, col: nt.col }, paramPos: params.__pos };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseParams() {
|
|
123
|
+
expect('OP', '(');
|
|
124
|
+
const params = [];
|
|
125
|
+
const pos = [];
|
|
126
|
+
while (!check('OP', ')')) {
|
|
127
|
+
const pt = expect('IDENT');
|
|
128
|
+
params.push(pt.value);
|
|
129
|
+
pos.push({ line: pt.line, col: pt.col });
|
|
130
|
+
if (check('OP', ',')) next();
|
|
131
|
+
}
|
|
132
|
+
expect('OP', ')');
|
|
133
|
+
Object.defineProperty(params, '__pos', { value: pos, enumerable: false });
|
|
134
|
+
return params;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseReturn() {
|
|
138
|
+
next(); // RETURN
|
|
139
|
+
let arg = null;
|
|
140
|
+
if (!check('SEMI') && !check('OP', '}') && !check('EOF')) {
|
|
141
|
+
arg = parseExpression();
|
|
142
|
+
}
|
|
143
|
+
return { type: 'Return', argument: arg };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseIf() {
|
|
147
|
+
next(); // IF
|
|
148
|
+
expect('OP', '(');
|
|
149
|
+
const test = parseExpression();
|
|
150
|
+
expect('OP', ')');
|
|
151
|
+
const consequent = parseBlock();
|
|
152
|
+
let alternate = null;
|
|
153
|
+
if (check('ELSE')) {
|
|
154
|
+
next();
|
|
155
|
+
alternate = check('IF') ? parseIf() : parseBlock();
|
|
156
|
+
}
|
|
157
|
+
return { type: 'If', test, consequent, alternate };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseWhile() {
|
|
161
|
+
next(); // WHILE
|
|
162
|
+
expect('OP', '(');
|
|
163
|
+
const test = parseExpression();
|
|
164
|
+
expect('OP', ')');
|
|
165
|
+
const body = parseBlock();
|
|
166
|
+
return { type: 'While', test, body };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// प्रत्येकम् (वस्तु : समूह) { ... } → for (const वस्तु of समूह)
|
|
170
|
+
function parseFor() {
|
|
171
|
+
next(); // FOR
|
|
172
|
+
expect('OP', '(');
|
|
173
|
+
const it = expect('IDENT');
|
|
174
|
+
expect('OP', ':');
|
|
175
|
+
const iterable = parseExpression();
|
|
176
|
+
expect('OP', ')');
|
|
177
|
+
const body = parseBlock();
|
|
178
|
+
return { type: 'ForOf', item: it.value, iterable, body, namePos: { line: it.line, col: it.col } };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parsePrint() {
|
|
182
|
+
next(); // PRINT
|
|
183
|
+
expect('OP', '(');
|
|
184
|
+
const args = [];
|
|
185
|
+
while (!check('OP', ')')) {
|
|
186
|
+
args.push(parseExpression());
|
|
187
|
+
if (check('OP', ',')) next();
|
|
188
|
+
}
|
|
189
|
+
expect('OP', ')');
|
|
190
|
+
return { type: 'Print', args };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---- expressions (Pratt) ------------------------------------------
|
|
194
|
+
|
|
195
|
+
const BINARY_PREC = {
|
|
196
|
+
'??': 1,
|
|
197
|
+
'||': 1, '&&': 2,
|
|
198
|
+
'==': 3, '!=': 3, '===': 3, '!==': 3,
|
|
199
|
+
'<': 4, '>': 4, '<=': 4, '>=': 4,
|
|
200
|
+
'+': 5, '-': 5,
|
|
201
|
+
'*': 6, '/': 6, '%': 6,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// compound assignment operators → the underlying binary op
|
|
205
|
+
const COMPOUND = {
|
|
206
|
+
'+=': '+', '-=': '-', '*=': '*', '/=': '/', '%=': '%',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function parseExpression() {
|
|
210
|
+
return parseAssignment();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseAssignment() {
|
|
214
|
+
const left = parseOrElse();
|
|
215
|
+
|
|
216
|
+
// compound assignment: x += y → x = x + y
|
|
217
|
+
const tok = peek();
|
|
218
|
+
if (tok.type === 'OP' && COMPOUND[tok.value]) {
|
|
219
|
+
const op = COMPOUND[tok.value];
|
|
220
|
+
next();
|
|
221
|
+
const right = parseAssignment(); // RHS may itself be assignment/ternary
|
|
222
|
+
if (left.type !== 'Identifier' && left.type !== 'Member') {
|
|
223
|
+
throw new DevabhashaError('अमान्यं नियोजनम् (invalid assignment target)', { line: peek().line, col: peek().col, kind: 'parse' });
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
type: 'Assign',
|
|
227
|
+
target: left,
|
|
228
|
+
value: { type: 'Binary', op, left, right },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (check('OP', '=')) {
|
|
233
|
+
next();
|
|
234
|
+
const right = parseAssignment();
|
|
235
|
+
if (left.type !== 'Identifier' && left.type !== 'Member') {
|
|
236
|
+
throw new DevabhashaError('अमान्यं नियोजनम् (invalid assignment target)', { line: peek().line, col: peek().col, kind: 'parse' });
|
|
237
|
+
}
|
|
238
|
+
return { type: 'Assign', target: left, value: right };
|
|
239
|
+
}
|
|
240
|
+
return left;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// result अथवा fallback — yield the Result's मूल्यम् if सफल, else the fallback.
|
|
244
|
+
// Sits just below assignment, above ternary, and is right-associative so
|
|
245
|
+
// chains read left-to-right: अ अथवा ब अथवा ग = अ अथवा (ब अथवा ग).
|
|
246
|
+
function parseOrElse() {
|
|
247
|
+
const left = parseTernary();
|
|
248
|
+
if (check('ORELSE')) {
|
|
249
|
+
next();
|
|
250
|
+
const fallback = parseOrElse();
|
|
251
|
+
return { type: 'OrElse', value: left, fallback };
|
|
252
|
+
}
|
|
253
|
+
return left;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ternary: परीक्षा ? तदा : अन्यथा (sits between assignment and binary)
|
|
257
|
+
function parseTernary() {
|
|
258
|
+
const cond = parseBinary(0);
|
|
259
|
+
if (check('OP', '?')) {
|
|
260
|
+
next();
|
|
261
|
+
const consequent = parseAssignment();
|
|
262
|
+
expect('OP', ':');
|
|
263
|
+
const alternate = parseAssignment();
|
|
264
|
+
return { type: 'Ternary', test: cond, consequent, alternate };
|
|
265
|
+
}
|
|
266
|
+
return cond;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parseBinary(minPrec) {
|
|
270
|
+
let left = parseUnary();
|
|
271
|
+
while (check('OP') && BINARY_PREC[peek().value] !== undefined) {
|
|
272
|
+
const op = peek().value;
|
|
273
|
+
const prec = BINARY_PREC[op];
|
|
274
|
+
if (prec < minPrec) break;
|
|
275
|
+
next();
|
|
276
|
+
const right = parseBinary(prec + 1);
|
|
277
|
+
left = { type: 'Binary', op, left, right };
|
|
278
|
+
}
|
|
279
|
+
return left;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function parseUnary() {
|
|
283
|
+
if (check('OP', '!') || check('OP', '-')) {
|
|
284
|
+
const op = next().value;
|
|
285
|
+
return { type: 'Unary', op, argument: parseUnary() };
|
|
286
|
+
}
|
|
287
|
+
// प्रतीक्षा <expr> — await a promise (only valid in an async function)
|
|
288
|
+
if (check('AWAIT')) {
|
|
289
|
+
next();
|
|
290
|
+
return { type: 'Await', argument: parseUnary() };
|
|
291
|
+
}
|
|
292
|
+
return parsePostfix();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// calls, member access, indexing
|
|
296
|
+
function parsePostfix() {
|
|
297
|
+
let node = parsePrimary();
|
|
298
|
+
while (true) {
|
|
299
|
+
if (check('OP', '(')) {
|
|
300
|
+
next();
|
|
301
|
+
const args = [];
|
|
302
|
+
while (!check('OP', ')')) {
|
|
303
|
+
args.push(parseExpression());
|
|
304
|
+
if (check('OP', ',')) next();
|
|
305
|
+
}
|
|
306
|
+
expect('OP', ')');
|
|
307
|
+
node = { type: 'Call', callee: node, args };
|
|
308
|
+
} else if (check('OP', '.')) {
|
|
309
|
+
next();
|
|
310
|
+
// Property names live in their own namespace: a word that is a
|
|
311
|
+
// keyword elsewhere (e.g. योजय = MOUNT) is still a valid property
|
|
312
|
+
// name here (योजय = array push). Accept any word token's value.
|
|
313
|
+
const t = peek();
|
|
314
|
+
if (t.type === 'IDENT' || KEYWORD_TOKENS.has(t.type)) {
|
|
315
|
+
next();
|
|
316
|
+
node = { type: 'Member', object: node, property: t.value, computed: false };
|
|
317
|
+
} else {
|
|
318
|
+
throw new DevabhashaError(`expected property name after '.' but found '${t.value}'`, { line: t.line, col: t.col, kind: 'parse' });
|
|
319
|
+
}
|
|
320
|
+
} else if (check('OP', '[')) {
|
|
321
|
+
next();
|
|
322
|
+
const prop = parseExpression();
|
|
323
|
+
expect('OP', ']');
|
|
324
|
+
node = { type: 'Member', object: node, property: prop, computed: true };
|
|
325
|
+
} else break;
|
|
326
|
+
}
|
|
327
|
+
// postfix ++ / -- : x++ → Update node
|
|
328
|
+
if (check('OP', '++') || check('OP', '--')) {
|
|
329
|
+
const op = next().value;
|
|
330
|
+
if (node.type !== 'Identifier' && node.type !== 'Member') {
|
|
331
|
+
throw new DevabhashaError('++/-- needs a variable target', { line: peek().line, col: peek().col, kind: 'parse' });
|
|
332
|
+
}
|
|
333
|
+
node = { type: 'Update', op, target: node, prefix: false };
|
|
334
|
+
}
|
|
335
|
+
return node;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function parsePrimary() {
|
|
339
|
+
if (check('NUMBER')) return { type: 'Number', value: next().value };
|
|
340
|
+
if (check('STRING')) return { type: 'String', value: next().value };
|
|
341
|
+
// सूत्र(expr) — a reactive reference: captures expr unevaluated (a live
|
|
342
|
+
// thread to the cells it reads) so a component can bind it fine-grained.
|
|
343
|
+
if (check('SUTRA')) {
|
|
344
|
+
next();
|
|
345
|
+
expect('OP', '(');
|
|
346
|
+
const expr = parseExpression();
|
|
347
|
+
expect('OP', ')');
|
|
348
|
+
return { type: 'Sutra', expr };
|
|
349
|
+
}
|
|
350
|
+
if (check('TEMPLATE')) {
|
|
351
|
+
const t = next();
|
|
352
|
+
// parse each embedded expression source into an AST
|
|
353
|
+
const parts = t.exprs.map(srcExpr => {
|
|
354
|
+
const sub = parse(tokenize(srcExpr));
|
|
355
|
+
if (!sub.body.length || sub.body[0].type !== 'ExpressionStatement') {
|
|
356
|
+
throw new DevabhashaError('अन्तर्न्यासदोषः: {…} must contain a single expression',
|
|
357
|
+
{ line: t.line, col: t.col, kind: 'parse' });
|
|
358
|
+
}
|
|
359
|
+
return sub.body[0].expression;
|
|
360
|
+
});
|
|
361
|
+
return { type: 'Template', chunks: t.chunks, parts };
|
|
362
|
+
}
|
|
363
|
+
if (check('TRUE')) { next(); return { type: 'Boolean', value: true }; }
|
|
364
|
+
if (check('FALSE')) { next(); return { type: 'Boolean', value: false }; }
|
|
365
|
+
if (check('NULL')) { next(); return { type: 'Null' }; }
|
|
366
|
+
if (check('IDENT')) {
|
|
367
|
+
const t = next();
|
|
368
|
+
return { type: 'Identifier', name: t.value, line: t.line, col: t.col };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// anonymous function expression: कार्य (params) { ... }
|
|
372
|
+
if (check('FUNC')) {
|
|
373
|
+
next();
|
|
374
|
+
const params = parseParams();
|
|
375
|
+
const body = parseBlock();
|
|
376
|
+
return { type: 'FuncExpr', params, body, async: false };
|
|
377
|
+
}
|
|
378
|
+
// async function expression: असमकालिक कार्य (params) { ... }
|
|
379
|
+
if (check('ASYNC')) {
|
|
380
|
+
next();
|
|
381
|
+
if (!check('FUNC')) {
|
|
382
|
+
throw new DevabhashaError('असमकालिकदोषः: असमकालिक must be followed by कार्य',
|
|
383
|
+
{ line: peek().line, col: peek().col, kind: 'parse' });
|
|
384
|
+
}
|
|
385
|
+
next(); // FUNC
|
|
386
|
+
const params = parseParams();
|
|
387
|
+
const body = parseBlock();
|
|
388
|
+
return { type: 'FuncExpr', params, body, async: true };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// web-layer builtins as expressions
|
|
392
|
+
if (check('ELEMENT')) return parseElement();
|
|
393
|
+
if (check('MOUNT')) return parseBuiltinCall('Mount');
|
|
394
|
+
if (check('LISTEN')) return parseBuiltinCall('Listen');
|
|
395
|
+
if (check('CONSTRUCT')) return parseConstruct();
|
|
396
|
+
if (check('OBJECT')) return parseObjectLiteral();
|
|
397
|
+
|
|
398
|
+
// array literal
|
|
399
|
+
if (check('OP', '[')) {
|
|
400
|
+
next();
|
|
401
|
+
const elements = [];
|
|
402
|
+
while (!check('OP', ']')) {
|
|
403
|
+
elements.push(parseExpression());
|
|
404
|
+
if (check('OP', ',')) next();
|
|
405
|
+
}
|
|
406
|
+
expect('OP', ']');
|
|
407
|
+
return { type: 'Array', elements };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// grouping
|
|
411
|
+
if (check('OP', '(')) {
|
|
412
|
+
next();
|
|
413
|
+
const e = parseExpression();
|
|
414
|
+
expect('OP', ')');
|
|
415
|
+
return e;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const t = peek();
|
|
419
|
+
throw new DevabhashaError(`अनपेक्षितम् (unexpected) '${t.value}' (${t.type})`, { line: t.line, col: t.col, kind: 'parse' });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// अङ्गम्("div", "नमस्ते", [...children])
|
|
423
|
+
function parseElement() {
|
|
424
|
+
next(); // ELEMENT
|
|
425
|
+
expect('OP', '(');
|
|
426
|
+
const args = [];
|
|
427
|
+
while (!check('OP', ')')) {
|
|
428
|
+
args.push(parseExpression());
|
|
429
|
+
if (check('OP', ',')) next();
|
|
430
|
+
}
|
|
431
|
+
expect('OP', ')');
|
|
432
|
+
return { type: 'ElementExpr', args };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function parseBuiltinCall(kind) {
|
|
436
|
+
next();
|
|
437
|
+
expect('OP', '(');
|
|
438
|
+
const args = [];
|
|
439
|
+
while (!check('OP', ')')) {
|
|
440
|
+
args.push(parseExpression());
|
|
441
|
+
if (check('OP', ',')) next();
|
|
442
|
+
}
|
|
443
|
+
expect('OP', ')');
|
|
444
|
+
return { type: kind, args };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ---- kāraka construction -------------------------------------------
|
|
448
|
+
//
|
|
449
|
+
// रचय <case-noun> [value] <case-noun> [value] …
|
|
450
|
+
//
|
|
451
|
+
// Each case-marked noun contributes a ROLE (from its vibhakti ending).
|
|
452
|
+
// Arguments may appear in ANY ORDER — they're collected into a slot map,
|
|
453
|
+
// not a positional list. A noun whose stem names a tag/event is
|
|
454
|
+
// self-valued; otherwise the following expression is its value.
|
|
455
|
+
function isCaseNoun(tok) {
|
|
456
|
+
if (!tok || tok.type !== 'IDENT') return null;
|
|
457
|
+
return analyze(tok.value); // {stem, case, karaka} | null
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Distinguish a रूप override block `{ वर्णः: … }` from a समास children
|
|
461
|
+
// block `{ रचय … }`. We're positioned at '{'. It's a style block iff the
|
|
462
|
+
// token after '{' is a name/string AND the one after that is ':'.
|
|
463
|
+
function looksLikeStyleBlock() {
|
|
464
|
+
const a = peek(1), b = peek(2);
|
|
465
|
+
if (!a || !b) return false;
|
|
466
|
+
const nameLike = a.type === 'IDENT' || a.type === 'STRING' || KEYWORD_TOKENS.has(a.type);
|
|
467
|
+
return nameLike && b.type === 'OP' && b.value === ':';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Parse a { key: value, ... } style body into translated pairs.
|
|
471
|
+
// Assumes the current token is '{'.
|
|
472
|
+
function parseStylePairs() {
|
|
473
|
+
expect('OP', '{');
|
|
474
|
+
const pairs = [];
|
|
475
|
+
while (!check('OP', '}') && !check('EOF')) {
|
|
476
|
+
const t = peek();
|
|
477
|
+
let key;
|
|
478
|
+
if (t.type === 'STRING' || t.type === 'IDENT' || KEYWORD_TOKENS.has(t.type)) {
|
|
479
|
+
next();
|
|
480
|
+
key = t.value;
|
|
481
|
+
} else {
|
|
482
|
+
throw new DevabhashaError('रूपदोषः: style property must be a name', { line: t.line, col: t.col, kind: 'parse' });
|
|
483
|
+
}
|
|
484
|
+
expect('OP', ':');
|
|
485
|
+
const valTok = peek();
|
|
486
|
+
let value;
|
|
487
|
+
const loneWord = (valTok.type === 'IDENT' || KEYWORD_TOKENS.has(valTok.type)) &&
|
|
488
|
+
peek(1) && (peek(1).type === 'OP' && (peek(1).value === ',' || peek(1).value === '}'));
|
|
489
|
+
if (loneWord) {
|
|
490
|
+
next();
|
|
491
|
+
value = { kind: 'word', value: valTok.value };
|
|
492
|
+
} else {
|
|
493
|
+
value = { kind: 'expr', value: parseExpression() };
|
|
494
|
+
}
|
|
495
|
+
pairs.push({ key, value });
|
|
496
|
+
if (check('OP', ',')) next();
|
|
497
|
+
}
|
|
498
|
+
expect('OP', '}');
|
|
499
|
+
return pairs;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// रूपनाम X = रूप { ... } — declare a reusable named style.
|
|
503
|
+
// Desugars to a const binding whose value is the style object.
|
|
504
|
+
function parseStyleName() {
|
|
505
|
+
next(); // STYLENAME
|
|
506
|
+
const nt = expect('IDENT');
|
|
507
|
+
expect('OP', '=');
|
|
508
|
+
expect('STYLE');
|
|
509
|
+
const pairs = parseStylePairs();
|
|
510
|
+
return { type: 'StyleDecl', name: nt.value, pairs, namePos: { line: nt.line, col: nt.col } };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// भाव x = init — declare a reactive state cell.
|
|
514
|
+
function parseStateDecl() {
|
|
515
|
+
next(); // STATE
|
|
516
|
+
const nt = expect('IDENT');
|
|
517
|
+
let init = null;
|
|
518
|
+
if (check('OP', '=')) { next(); init = parseExpression(); }
|
|
519
|
+
return { type: 'StateDecl', name: nt.value, init, namePos: { line: nt.line, col: nt.col } };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// दृश्य { ... } or दृश्य (container) { ... }
|
|
523
|
+
// The block's final expression statement is the rendered view. With no
|
|
524
|
+
// container, the runtime mounts to the default root (stage/body).
|
|
525
|
+
function parseView() {
|
|
526
|
+
next(); // VIEW
|
|
527
|
+
let container = null;
|
|
528
|
+
if (check('OP', '(')) {
|
|
529
|
+
next();
|
|
530
|
+
container = parseExpression();
|
|
531
|
+
expect('OP', ')');
|
|
532
|
+
}
|
|
533
|
+
const body = parseBlock();
|
|
534
|
+
return { type: 'View', container, body };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// निर्यात <declaration> — mark a declaration as exported.
|
|
538
|
+
// निर्यात कार्य द्वि(न){ … }
|
|
539
|
+
// निर्यात नियत पाई = ३.१४।
|
|
540
|
+
// निर्यात रूपनाम कार्डः = रूप { … }।
|
|
541
|
+
function parseExport() {
|
|
542
|
+
next(); // EXPORT
|
|
543
|
+
const decl = parseStatement();
|
|
544
|
+
const exportable = new Set(['VarDecl', 'FuncDecl', 'StyleDecl', 'StateDecl']);
|
|
545
|
+
if (!exportable.has(decl.type)) {
|
|
546
|
+
throw new DevabhashaError('निर्यातदोषः: only declarations (चर/नियत/कार्य/रूपनाम/भाव) can be exported',
|
|
547
|
+
{ line: peek().line, col: peek().col, kind: 'parse' });
|
|
548
|
+
}
|
|
549
|
+
// the exported binding's name
|
|
550
|
+
const name = decl.name;
|
|
551
|
+
return { type: 'Export', name, decl };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// आयात — three forms:
|
|
555
|
+
// आयात { द्वि, त्रि } आ "गणित"। named imports
|
|
556
|
+
// आयात * रूपेण ग आ "गणित"। namespace import (ग.द्वि …)
|
|
557
|
+
// आयात "उपकरणम्"। side-effect import (run the module)
|
|
558
|
+
function parseImport() {
|
|
559
|
+
next(); // IMPORT
|
|
560
|
+
// namespace: * रूपेण <name>
|
|
561
|
+
if (check('OP', '*')) {
|
|
562
|
+
next();
|
|
563
|
+
// रूपेण ("as") — accept an IDENT meaning "as"; we use आ-less alias via 'रूपेण'
|
|
564
|
+
const asTok = peek();
|
|
565
|
+
if (asTok.type === 'IDENT' && asTok.value === 'रूपेण') next();
|
|
566
|
+
const alias = expect('IDENT').value;
|
|
567
|
+
expect('FROM');
|
|
568
|
+
const source = expect('STRING').value;
|
|
569
|
+
return { type: 'Import', kind: 'namespace', alias, names: null, source };
|
|
570
|
+
}
|
|
571
|
+
// named: { a, b, c } आ "..."
|
|
572
|
+
if (check('OP', '{')) {
|
|
573
|
+
next();
|
|
574
|
+
const names = [];
|
|
575
|
+
while (!check('OP', '}') && !check('EOF')) {
|
|
576
|
+
names.push(expect('IDENT').value);
|
|
577
|
+
if (check('OP', ',')) next();
|
|
578
|
+
}
|
|
579
|
+
expect('OP', '}');
|
|
580
|
+
expect('FROM');
|
|
581
|
+
const source = expect('STRING').value;
|
|
582
|
+
return { type: 'Import', kind: 'named', names, alias: null, source };
|
|
583
|
+
}
|
|
584
|
+
// side-effect: आयात "..."
|
|
585
|
+
if (check('STRING')) {
|
|
586
|
+
const source = next().value;
|
|
587
|
+
return { type: 'Import', kind: 'effect', names: null, alias: null, source };
|
|
588
|
+
}
|
|
589
|
+
throw new DevabhashaError('आयातदोषः: expected { names } आ "module", * रूपेण name आ "module", or "module"',
|
|
590
|
+
{ line: peek().line, col: peek().col, kind: 'parse' });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function parseConstruct() {
|
|
594
|
+
next(); // CONSTRUCT
|
|
595
|
+
const slots = {}; // slotName -> AST/value
|
|
596
|
+
const order = []; // record kāraka order purely for diagnostics
|
|
597
|
+
|
|
598
|
+
while (true) {
|
|
599
|
+
const tok = peek();
|
|
600
|
+
const a = isCaseNoun(tok);
|
|
601
|
+
if (!a) break; // no more case-marked arguments → construction ends
|
|
602
|
+
|
|
603
|
+
next(); // consume the case-marked noun
|
|
604
|
+
const slot = KARAKA_TO_SLOT[a.karaka];
|
|
605
|
+
order.push(a.karaka);
|
|
606
|
+
|
|
607
|
+
if (slot === 'tag' && TAG_STEMS[a.stem]) {
|
|
608
|
+
slots.tag = { type: 'String', value: TAG_STEMS[a.stem] };
|
|
609
|
+
} else if (slot === 'event' && EVENT_STEMS[a.stem]) {
|
|
610
|
+
slots.event = { type: 'String', value: EVENT_STEMS[a.stem] };
|
|
611
|
+
} else {
|
|
612
|
+
// role marker followed by its value expression
|
|
613
|
+
slots[slot] = parseExpression();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (!slots.tag) {
|
|
618
|
+
throw new DevabhashaError('रचयदोषः: no कर्तृ (nominative) naming the element kind', { line: peek().line, col: peek().col, kind: 'parse' });
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// रूप — style. Three forms:
|
|
622
|
+
// रूप { वर्णः: रक्तः } inline block
|
|
623
|
+
// रूप मुख्यपटः named-style reference (an identifier)
|
|
624
|
+
// रूप मुख्यपटः { अन्तरालः: … } named base + inline overrides
|
|
625
|
+
let style = null;
|
|
626
|
+
if (check('STYLE')) {
|
|
627
|
+
next();
|
|
628
|
+
let base = null;
|
|
629
|
+
if (check('IDENT')) {
|
|
630
|
+
base = { type: 'Identifier', name: next().value };
|
|
631
|
+
}
|
|
632
|
+
let pairs = [];
|
|
633
|
+
// A '{' here is style-overrides ONLY if it looks like key:value pairs
|
|
634
|
+
// (i.e. `{ word : ...`). Otherwise the '{' belongs to the समास children
|
|
635
|
+
// block — important after a bare named ref: रूप कार्डः { ...children }.
|
|
636
|
+
if (check('OP', '{') && looksLikeStyleBlock()) {
|
|
637
|
+
pairs = parseStylePairs();
|
|
638
|
+
}
|
|
639
|
+
style = { base, pairs };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// समास (compound) block form: a तत्पुरुष container whose body is a
|
|
643
|
+
// द्वन्द्व (sibling list) of child constructions and/or expressions.
|
|
644
|
+
let children = null;
|
|
645
|
+
if (check('OP', '{')) {
|
|
646
|
+
next();
|
|
647
|
+
children = [];
|
|
648
|
+
while (!check('OP', '}') && !check('EOF')) {
|
|
649
|
+
eatSemi();
|
|
650
|
+
if (check('OP', '}')) break;
|
|
651
|
+
// a child is either a nested रचय or any expression (text/value)
|
|
652
|
+
children.push(parseExpression());
|
|
653
|
+
eatSemi();
|
|
654
|
+
}
|
|
655
|
+
expect('OP', '}');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return { type: 'Construct', slots, order, children, style };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// कोष { कुञ्जी: मूल्यम्, अन्या: मूल्यम् } → object literal
|
|
662
|
+
function parseObjectLiteral() {
|
|
663
|
+
next(); // OBJECT
|
|
664
|
+
expect('OP', '{');
|
|
665
|
+
const props = [];
|
|
666
|
+
while (!check('OP', '}')) {
|
|
667
|
+
let key;
|
|
668
|
+
const t = peek();
|
|
669
|
+
// Keys live in their own namespace: a STRING, an IDENT, or any word
|
|
670
|
+
// that happens to be a keyword elsewhere (चर, यदि…) is a valid key.
|
|
671
|
+
if (t.type === 'STRING' || t.type === 'IDENT' || KEYWORD_TOKENS.has(t.type)) {
|
|
672
|
+
next();
|
|
673
|
+
key = { type: 'String', value: t.value };
|
|
674
|
+
} else {
|
|
675
|
+
throw new DevabhashaError('कोषदोषः: object key must be a name or string', { line: peek().line, col: peek().col, kind: 'parse' });
|
|
676
|
+
}
|
|
677
|
+
expect('OP', ':');
|
|
678
|
+
const value = parseExpression();
|
|
679
|
+
props.push({ key, value });
|
|
680
|
+
if (check('OP', ',')) next();
|
|
681
|
+
}
|
|
682
|
+
expect('OP', '}');
|
|
683
|
+
return { type: 'ObjectLiteral', props };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return parseProgram();
|
|
687
|
+
}
|