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/src/codegen.js ADDED
@@ -0,0 +1,864 @@
1
+ // codegen.js — walks the AST and emits JavaScript source text.
2
+
3
+ import { METHODS, PROPERTIES, GLOBALS, MATH_CONSTANTS } from './stdlib.js';
4
+ import { styleProp, styleValue, isStyleWord } from './style.js';
5
+ import { DevabhashaError } from './errors.js';
6
+
7
+ export const PRELUDE = `// --- देवभाषा prelude (host-independent) ---
8
+ const __RT = {
9
+ // परिणाम (Result): explicit success/failure values for fallible operations.
10
+ // keys are the raw Sanskrit field names, since member access (फल.सफल) emits
11
+ // the property name unchanged.
12
+ ok(v) { return { "सफल": true, "मूल्यम्": v, "दोषः": null }; },
13
+ err(e) { return { "सफल": false, "मूल्यम्": null, "दोषः": e }; },
14
+ // result अथवा fallback — the Result's value if सफल, else the (lazy) fallback.
15
+ // A non-Result value is returned as-is (so अथवा is also a null/Err guard).
16
+ orElse(r, fb) {
17
+ if (r && typeof r === "object" && "सफल" in r) return r["सफल"] ? r["मूल्यम्"] : fb();
18
+ return r == null ? fb() : r;
19
+ },
20
+ // प्रकारः — a value's kind, named in Sanskrit (for reflection / tests).
21
+ typeOf(v) {
22
+ if (v === null || v === undefined) return "रिक्त"; // null/undefined
23
+ if (Array.isArray(v)) return "सूची"; // array
24
+ const t = typeof v;
25
+ if (t === "number") return "अङ्क"; // number
26
+ if (t === "string") return "वाक्"; // string
27
+ if (t === "boolean") return "सत्यासत्य"; // boolean
28
+ if (t === "function") return "कार्य"; // function
29
+ return "कोष"; // object/record
30
+ },
31
+ // प्रदत्त (data) — JSON parse/serialize, both returning परिणाम since
32
+ // JSON.parse throws and the language has no exceptions.
33
+ json: {
34
+ विश्लेषय(text) { // parse
35
+ try { return __RT.ok(JSON.parse(text)); }
36
+ catch (e) { return __RT.err(String(e && e.message || e)); }
37
+ },
38
+ सूत्रय(v, pretty) { // stringify (pretty by default)
39
+ try { return __RT.ok(JSON.stringify(v, null, pretty === false ? undefined : 2)); }
40
+ catch (e) { return __RT.err(String(e && e.message || e)); }
41
+ },
42
+ },
43
+ // अङ्कय — parse a string to a number → परिणाम (Err if not a number).
44
+ toNumber(s) {
45
+ const n = Number(s);
46
+ return Number.isNaN(n) ? __RT.err("अङ्कः न (not a number): " + s) : __RT.ok(n);
47
+ },
48
+ };
49
+ `;
50
+
51
+ const RUNTIME = `// --- देवभाषा runtime ---
52
+ const __DB = {
53
+ el(tag, ...rest) {
54
+ const node = document.createElement(tag);
55
+ for (const r of rest) {
56
+ if (r == null) continue;
57
+ if (typeof r === 'object' && !(r instanceof Node) && !Array.isArray(r)) {
58
+ // props/attrs object
59
+ for (const [k, v] of Object.entries(r)) {
60
+ if (k.startsWith('on') && typeof v === 'function') {
61
+ node.addEventListener(k.slice(2).toLowerCase(), v);
62
+ } else if (k === 'style' && typeof v === 'object') {
63
+ Object.assign(node.style, v);
64
+ } else {
65
+ node.setAttribute(k, v);
66
+ }
67
+ }
68
+ } else if (Array.isArray(r)) {
69
+ r.forEach(c => node.append(c instanceof Node ? c : document.createTextNode(String(c))));
70
+ } else {
71
+ node.append(r instanceof Node ? r : document.createTextNode(String(r)));
72
+ }
73
+ }
74
+ return node;
75
+ },
76
+ mount(node, target) {
77
+ const t = typeof target === 'string' ? document.querySelector(target) : (target || document.body);
78
+ t.append(node);
79
+ return node;
80
+ },
81
+ listen(node, event, handler) {
82
+ node.addEventListener(event, handler);
83
+ return node;
84
+ },
85
+ construct({ tag, content, contentBind, event, handler, parent, prop, source, children, style, styleBind }) {
86
+ const node = document.createElement(tag);
87
+ if (contentBind != null) {
88
+ // fine-grained: a bound text node that updates in place on dep change
89
+ node.append(__DB.bindText(contentBind));
90
+ } else if (content != null && content.__isSutra) {
91
+ // a सूत्र reactive reference passed as content → bind fine-grained
92
+ node.append(__DB.bindText(content));
93
+ } else if (content != null) {
94
+ if (Array.isArray(content)) content.forEach(c => node.append(c instanceof Node ? c : document.createTextNode(String(c))));
95
+ else node.append(content instanceof Node ? content : document.createTextNode(String(content)));
96
+ }
97
+ if (children) {
98
+ // DOM append moves nodes, so nested child constructs are correctly
99
+ // re-parented into this element (समास composition). An array child is
100
+ // flattened — this is what makes list rendering (.प्रतिचित्रय → nodes) work.
101
+ const appendChild = c => {
102
+ if (c == null) return;
103
+ if (Array.isArray(c)) { c.forEach(appendChild); return; }
104
+ node.append(c instanceof Node ? c : document.createTextNode(String(c)));
105
+ };
106
+ for (const c of children) appendChild(c);
107
+ }
108
+ if (style && typeof style === 'object') {
109
+ Object.assign(node.style, style);
110
+ }
111
+ if (styleBind && typeof styleBind === 'object') {
112
+ // each dynamic style property gets its own effect → only that property
113
+ // updates when its dependencies change (fine-grained, no rebuild).
114
+ for (const [k, thunk] of Object.entries(styleBind)) {
115
+ __DB.effect(() => { node.style[k] = thunk(); });
116
+ }
117
+ }
118
+ if (prop && typeof prop === 'object') {
119
+ for (const [k, v] of Object.entries(prop)) node.setAttribute(k, v);
120
+ }
121
+ if (event && handler) node.addEventListener(event, handler);
122
+ if (parent != null) {
123
+ const t = typeof parent === 'string' ? document.querySelector(parent) : parent;
124
+ (t || document.body).append(node);
125
+ }
126
+ return node;
127
+ },
128
+
129
+ // ----- reactivity -----
130
+ // A subscriber stack: whatever is on top when a भाव cell is READ becomes a
131
+ // dependency of that subscriber. Both the coarse दृश्य (a whole-view render)
132
+ // and a fine-grained प्रभाव (effect) push themselves here. A subscriber is an
133
+ // object { run, deps } where deps is the set of cells it currently reads.
134
+ _subStack: [],
135
+ _currentSub() { return __DB._subStack.length ? __DB._subStack[__DB._subStack.length - 1] : null; },
136
+ state(initial) {
137
+ let value = initial;
138
+ const subs = new Set(); // subscribers depending on this cell
139
+ const cell = (...args) => {
140
+ if (args.length === 0) { // read — track the current subscriber
141
+ const sub = __DB._currentSub();
142
+ if (sub) { subs.add(sub); if (sub.deps) sub.deps.add(cell); }
143
+ return value;
144
+ }
145
+ const next = args[0]; // write
146
+ // skip re-render only when an unchanged PRIMITIVE is written; object/
147
+ // array state is usually mutated in place, so always re-render those.
148
+ if (next === value && (next === null || typeof next !== 'object')) return value;
149
+ value = next;
150
+ // notify every subscriber (snapshot first — re-running mutates the set)
151
+ for (const sub of Array.from(subs)) {
152
+ if (typeof sub === 'function') sub(); // legacy view render
153
+ else if (sub && sub.run) sub.run(); // effect / binding
154
+ }
155
+ return value;
156
+ };
157
+ cell.__isState = true;
158
+ cell.__unsubscribe = (sub) => subs.delete(sub);
159
+ return cell;
160
+ },
161
+ // प्रभाव — a fine-grained effect. Runs fn now, tracking which भाव cells it
162
+ // reads, and re-runs ONLY fn when any of those change. Before each re-run it
163
+ // unsubscribes from its previous dependencies (so conditional reads don't
164
+ // leave stale subscriptions) and re-tracks fresh ones.
165
+ effect(fn) {
166
+ const sub = {
167
+ deps: new Set(),
168
+ cleanups: [],
169
+ run() {
170
+ // run any registered cleanups from the previous run (teardown)
171
+ for (const c of sub.cleanups) { try { c(); } catch (e) {} }
172
+ sub.cleanups = [];
173
+ // drop old subscriptions, then re-track on this run
174
+ for (const cell of sub.deps) if (cell.__unsubscribe) cell.__unsubscribe(sub);
175
+ sub.deps.clear();
176
+ __DB._subStack.push(sub);
177
+ const prevEffect = __DB._activeEffect; __DB._activeEffect = sub;
178
+ try { fn(); } finally { __DB._activeEffect = prevEffect; __DB._subStack.pop(); }
179
+ },
180
+ };
181
+ sub.run();
182
+ return sub;
183
+ },
184
+ _activeEffect: null,
185
+ // सफाई — register a cleanup that runs before the current effect's next run
186
+ // (and could run on disposal). The standard teardown hook for timers/listeners.
187
+ onCleanup(fn) { if (__DB._activeEffect) __DB._activeEffect.cleanups.push(fn); },
188
+ // सूत्र — tag a thunk as a reactive reference so content slots / बन्ध bind it.
189
+ sutra(thunk) { thunk.__isSutra = true; return thunk; },
190
+ // आलस्यचित्रम् — a lazy-loaded image. Renders an img showing the placeholder
191
+ // (or nothing) and swaps in the real src only once it scrolls into view, via
192
+ // IntersectionObserver. opts: { alt, placeholder, rootMargin }. Falls back to
193
+ // eager loading where IntersectionObserver is unavailable.
194
+ lazyImage(src, opts) {
195
+ opts = opts || {};
196
+ const img = document.createElement('img');
197
+ if (opts.alt != null) img.setAttribute('alt', opts.alt);
198
+ if (opts.placeholder) img.setAttribute('src', opts.placeholder);
199
+ img.setAttribute('data-src', src);
200
+ img.setAttribute('loading', 'lazy'); // native hint where supported
201
+ const load = () => { if (img.getAttribute('src') !== src) img.setAttribute('src', src); };
202
+ if (typeof IntersectionObserver === 'function') {
203
+ const io = new IntersectionObserver((entries) => {
204
+ for (const e of entries) {
205
+ if (e.isIntersecting) { load(); io.unobserve(img); }
206
+ }
207
+ }, { rootMargin: opts.rootMargin || '200px' });
208
+ io.observe(img);
209
+ } else {
210
+ load(); // no observer → load now
211
+ }
212
+ return img;
213
+ },
214
+ // bindText — fine-grained: a text node whose content is produced by thunk();
215
+ // only this node's text updates when the thunk's dependencies change.
216
+ bindText(thunk) {
217
+ const node = document.createTextNode('');
218
+ __DB.effect(() => { node.textContent = String(thunk()); });
219
+ return node;
220
+ },
221
+ // आवली — keyed list reconciliation. dataThunk() returns the current array;
222
+ // keyFn(item, i) gives a STABLE identity; renderFn(item, i) builds a node for
223
+ // a new key. Wrapped in an effect, so it re-runs when the data signal changes.
224
+ // On each run it reuses the DOM nodes of surviving keys (preserving their
225
+ // state/focus), creates nodes for new keys, removes vanished ones, and
226
+ // reorders children to match the new sequence — without rebuilding everything.
227
+ keyedList(dataThunk, keyFn, renderFn) {
228
+ const host = document.createElement('div');
229
+ host.style.display = 'contents'; // transparent wrapper, no layout box
230
+ let prev = new Map(); // key → node (from the last run)
231
+ __DB.effect(() => {
232
+ const items = dataThunk() || [];
233
+ const next = new Map();
234
+ const ordered = [];
235
+ items.forEach((item, i) => {
236
+ const k = String(keyFn(item, i));
237
+ let node = prev.get(k);
238
+ if (node === undefined) node = renderFn(item, i); // new key → build
239
+ next.set(k, node);
240
+ ordered.push(node);
241
+ });
242
+ // remove nodes whose key vanished
243
+ for (const [k, node] of prev) {
244
+ if (!next.has(k) && node.parentNode === host) host.removeChild(node);
245
+ }
246
+ // place nodes in the new order (reusing/moving existing ones)
247
+ let ref = null; // insert before the previous sibling
248
+ for (let i = ordered.length - 1; i >= 0; i--) {
249
+ const node = ordered[i];
250
+ if (node.nextSibling !== ref || node.parentNode !== host) host.insertBefore(node, ref);
251
+ ref = node;
252
+ }
253
+ prev = next;
254
+ });
255
+ return host;
256
+ },
257
+ view(container, viewFn) {
258
+ const host = container ? (typeof container === 'string' ? document.querySelector(container) : container) : document.body;
259
+ const render = () => {
260
+ __DB._subStack.push(render);
261
+ let out;
262
+ try { out = viewFn(); } finally { __DB._subStack.pop(); }
263
+ host.innerHTML = '';
264
+ const append = c => {
265
+ if (c == null) return;
266
+ if (Array.isArray(c)) { c.forEach(append); return; }
267
+ host.append(c instanceof Node ? c : document.createTextNode(String(c)));
268
+ };
269
+ append(out);
270
+ };
271
+ render();
272
+ return host;
273
+ },
274
+ // ----- timing & input (for animation loops / games) -----
275
+ interval(fn, ms) { return setInterval(fn, ms); },
276
+ clearTimer(id) { clearInterval(id); },
277
+ onKey(fn) {
278
+ const h = (e) => fn(e.key);
279
+ document.addEventListener('keydown', h);
280
+ return h;
281
+ }
282
+ };
283
+ `;
284
+
285
+ export function generate(ast, { includeRuntime = true, withMeta = false, sourceMap = false } = {}) {
286
+ let out = '';
287
+ // output position tracking (for source maps)
288
+ let outLine = 0; // 0-based
289
+ let outCol = 0; // 0-based
290
+ const emit = (s) => {
291
+ out += s;
292
+ // advance the output cursor
293
+ let nl = -1, from = 0, count = 0, last = -1;
294
+ for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) { count++; last = i; }
295
+ if (count === 0) { outCol += s.length; }
296
+ else { outLine += count; outCol = s.length - last - 1; }
297
+ };
298
+ // collected mappings: { genLine, genCol, srcLine (1-based), srcCol (1-based) }
299
+ const mappings = [];
300
+ const recordMapping = (node) => {
301
+ if (!sourceMap || !node || node.line == null) return;
302
+ mappings.push({ genLine: outLine, genCol: outCol, srcLine: node.line, srcCol: node.col || 1 });
303
+ };
304
+ // names declared with भाव are reactive cells: reads → x(), writes → x(v)
305
+ const stateNames = new Set();
306
+ // true while generating a दृश्य body (coarse re-render owns updates there)
307
+ let inView = false;
308
+ // does an expression read any भाव cell? If so, a रचय content slot bound to it
309
+ // is DYNAMIC and gets compiled to a fine-grained thunk (auto बन्ध) instead of
310
+ // an eagerly-evaluated value. A function expression's body is its own scope —
311
+ // we don't descend into it (an event handler reading state isn't a text dep).
312
+ function readsState(n) {
313
+ if (!n || typeof n !== 'object') return false;
314
+ if (n.type === 'Identifier') return stateNames.has(n.name);
315
+ if (n.type === 'FuncExpr' || n.type === 'FuncDecl') return false;
316
+ for (const k of Object.keys(n)) {
317
+ if (k === 'line' || k === 'col' || k === 'namePos' || k === 'paramPos') continue;
318
+ const v = n[k];
319
+ if (Array.isArray(v)) { if (v.some(readsState)) return true; }
320
+ else if (v && typeof v === 'object' && v.type) { if (readsState(v)) return true; }
321
+ }
322
+ return false;
323
+ }
324
+ // module metadata collected during codegen
325
+ const exports = [];
326
+ const imports = [];
327
+ // names bound to imported namespaces (आयात * रूपेण ग): member access on
328
+ // these uses id(prop), not the stdlib method tables.
329
+ const namespaceAliases = new Set();
330
+ // async-context depth: प्रतीक्षा (await) is only valid when > 0
331
+ let asyncDepth = 0;
332
+ // inside a रूप style-value expression: bare color/style words → CSS literals
333
+ let inStyleValue = false;
334
+
335
+ function gen(node, indent = '') {
336
+ switch (node.type) {
337
+ case 'Program':
338
+ node.body.forEach(s => { emit(indent); genStatement(s, indent); emit('\n'); });
339
+ break;
340
+ default:
341
+ genStatement(node, indent);
342
+ }
343
+ }
344
+
345
+ function genStatement(node, indent) {
346
+ recordMapping(node);
347
+ switch (node.type) {
348
+ case 'VarDecl': {
349
+ const kw = node.kind === 'CONST' ? 'const' : 'let';
350
+ emit(`${kw} ${id(node.name)}`);
351
+ if (node.init) { emit(' = '); genExpr(node.init); }
352
+ emit(';');
353
+ break;
354
+ }
355
+ case 'StyleDecl': {
356
+ // रूपनाम X = रूप {...} → const X = { ...translated style... };
357
+ emit(`const ${id(node.name)} = `);
358
+ emitStyleObject(node.pairs);
359
+ emit(';');
360
+ break;
361
+ }
362
+ case 'StateDecl': {
363
+ // भाव x = init → const x = __DB.state(init);
364
+ stateNames.add(node.name);
365
+ emit(`const ${id(node.name)} = __DB.state(`);
366
+ if (node.init) genExpr(node.init); else emit('undefined');
367
+ emit(');');
368
+ break;
369
+ }
370
+ case 'View': {
371
+ // दृश्य (container) { body } → __DB.view(container, () => { … return last });
372
+ // inside a दृश्य the coarse re-render owns updates, so content slots stay
373
+ // eager (no fine-grained binding) to avoid double-updating.
374
+ emit('__DB.view(');
375
+ if (node.container) genExpr(node.container); else emit('null');
376
+ emit(', () => ');
377
+ const savedInView = inView; inView = true;
378
+ genViewBody(node.body, indent);
379
+ inView = savedInView;
380
+ emit(');');
381
+ break;
382
+ }
383
+ case 'Export':
384
+ // emit the underlying declaration; the export is recorded as metadata
385
+ exports.push(node.name);
386
+ genStatement(node.decl, indent);
387
+ break;
388
+ case 'Import':
389
+ // imports are resolved by the bundler; record metadata and emit
390
+ // nothing inline (the bundler prepends linked module code).
391
+ imports.push(node);
392
+ if (node.kind === 'namespace') namespaceAliases.add(node.alias);
393
+ break;
394
+ case 'FuncDecl': {
395
+ emit(`${node.async ? 'async ' : ''}function ${id(node.name)}(${node.params.map(id).join(', ')}) `);
396
+ const savedAD = asyncDepth;
397
+ asyncDepth = node.async ? 1 : 0; // entering a function resets context
398
+ genBlock(node.body, indent);
399
+ asyncDepth = savedAD;
400
+ break;
401
+ }
402
+ case 'Return':
403
+ emit('return');
404
+ if (node.argument) { emit(' '); genExpr(node.argument); }
405
+ emit(';');
406
+ break;
407
+ case 'If':
408
+ emit('if (');
409
+ genExpr(node.test);
410
+ emit(') ');
411
+ genBlock(node.consequent, indent);
412
+ if (node.alternate) {
413
+ emit(' else ');
414
+ if (node.alternate.type === 'If') genStatement(node.alternate, indent);
415
+ else genBlock(node.alternate, indent);
416
+ }
417
+ break;
418
+ case 'While':
419
+ emit('while (');
420
+ genExpr(node.test);
421
+ emit(') ');
422
+ genBlock(node.body, indent);
423
+ break;
424
+ case 'ForOf':
425
+ emit(`for (const ${id(node.item)} of `);
426
+ genExpr(node.iterable);
427
+ emit(') ');
428
+ genBlock(node.body, indent);
429
+ break;
430
+ case 'Break': emit('break;'); break;
431
+ case 'Continue': emit('continue;'); break;
432
+ case 'Print':
433
+ emit('console.log(');
434
+ node.args.forEach((a, i) => { if (i) emit(', '); genExpr(a); });
435
+ emit(');');
436
+ break;
437
+ case 'Block':
438
+ genBlock(node, indent);
439
+ break;
440
+ case 'ExpressionStatement':
441
+ genExpr(node.expression);
442
+ emit(';');
443
+ break;
444
+ default:
445
+ throw new Error(`codegen: unknown statement ${node.type}`);
446
+ }
447
+ }
448
+
449
+ function genBlock(block, indent) {
450
+ const inner = indent + ' ';
451
+ emit('{\n');
452
+ block.body.forEach(s => { emit(inner); genStatement(s, inner); emit('\n'); });
453
+ emit(indent + '}');
454
+ }
455
+
456
+ // A view body: like a block, but the LAST statement, if it's an expression,
457
+ // becomes the returned value (the thing to render).
458
+ function genViewBody(block, indent) {
459
+ const inner = indent + ' ';
460
+ emit('{\n');
461
+ block.body.forEach((s, i) => {
462
+ const last = i === block.body.length - 1;
463
+ emit(inner);
464
+ if (last && s.type === 'ExpressionStatement') {
465
+ emit('return ');
466
+ genExpr(s.expression);
467
+ emit(';');
468
+ } else {
469
+ genStatement(s, inner);
470
+ }
471
+ emit('\n');
472
+ });
473
+ emit(indent + '}');
474
+ }
475
+
476
+ // emit a { } object literal from रूप (key, value) pairs, translating
477
+ // Sanskrit property names and known value words into CSS.
478
+ // emit a style object for the given pairs. Static pairs go into a plain
479
+ // object; pairs whose value reads a भाव cell (outside a view) are emitted
480
+ // separately as styleBind thunks so the runtime updates just that property
481
+ // fine-grained. Returns { hasStatic, hasBind } so the caller can wire slots.
482
+ function partitionStylePairs(pairs) {
483
+ const staticPairs = [], bindPairs = [];
484
+ for (const p of pairs) {
485
+ let dynamic = false;
486
+ if (!inView) {
487
+ if (p.value.kind === 'word') {
488
+ // a bare identifier that is a भाव cell (and not a style keyword) is dynamic
489
+ dynamic = stateNames.has(p.value.value) && !isStyleWord(p.value.value);
490
+ } else {
491
+ dynamic = readsState(p.value.value);
492
+ }
493
+ }
494
+ (dynamic ? bindPairs : staticPairs).push(p);
495
+ }
496
+ return { staticPairs, bindPairs };
497
+ }
498
+ function emitStylePair(p) {
499
+ emit(JSON.stringify(styleProp(p.key)));
500
+ emit(': ');
501
+ if (p.value.kind === 'word') {
502
+ if (isStyleWord(p.value.value)) emit(JSON.stringify(styleValue(p.value.value)));
503
+ else emit(id(p.value.value));
504
+ } else {
505
+ const saved = inStyleValue; inStyleValue = true;
506
+ genExpr(p.value.value);
507
+ inStyleValue = saved;
508
+ }
509
+ }
510
+ function emitStyleObject(pairs) {
511
+ emit('{ ');
512
+ pairs.forEach((p, i) => { if (i) emit(', '); emitStylePair(p); });
513
+ emit(' }');
514
+ }
515
+ // a styleBind object: { "prop": () => (value), ... } — each a reactive thunk
516
+ function emitStyleBindObject(pairs) {
517
+ emit('{ ');
518
+ pairs.forEach((p, i) => {
519
+ if (i) emit(', ');
520
+ emit(JSON.stringify(styleProp(p.key)));
521
+ emit(': () => (');
522
+ const saved = inStyleValue; inStyleValue = true;
523
+ if (p.value.kind === 'word') {
524
+ // a bare state identifier → its reactive read (cell())
525
+ if (isStyleWord(p.value.value) && !stateNames.has(p.value.value)) emit(JSON.stringify(styleValue(p.value.value)));
526
+ else emit(id(p.value.value) + '()');
527
+ } else {
528
+ genExpr(p.value.value);
529
+ }
530
+ inStyleValue = saved;
531
+ emit(')');
532
+ });
533
+ emit(' }');
534
+ }
535
+
536
+ function genExpr(node) {
537
+ switch (node.type) {
538
+ case 'Number': emit(node.value); break;
539
+ case 'String': emit(JSON.stringify(unescapeStr(node.value))); break;
540
+ case 'Template': {
541
+ // string interpolation → a JS template literal
542
+ const esc = s => s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
543
+ emit('`');
544
+ emit(esc(unescapeStr(node.chunks[0])));
545
+ node.parts.forEach((p, idx) => {
546
+ emit('${');
547
+ genExpr(p);
548
+ emit('}');
549
+ emit(esc(unescapeStr(node.chunks[idx + 1] ?? '')));
550
+ });
551
+ emit('`');
552
+ break;
553
+ }
554
+ case 'Boolean': emit(node.value ? 'true' : 'false'); break;
555
+ case 'Null': emit('null'); break;
556
+ case 'Identifier': {
557
+ // inside a रूप style-value expression, a known color/style word is a
558
+ // CSS literal (so ternaries like सक्रियः ? रक्तः : धूसरः work)
559
+ if (inStyleValue && isStyleWord(node.name) && !stateNames.has(node.name)) {
560
+ emit(JSON.stringify(styleValue(node.name)));
561
+ break;
562
+ }
563
+ // a भाव state cell reads as x(); a Sanskrit global builtin
564
+ // (संकेताक्षर → String.fromCharCode); else an ordinary identifier
565
+ if (stateNames.has(node.name)) { emit(id(node.name) + '()'); break; }
566
+ const g = GLOBALS[node.name];
567
+ emit(g !== undefined ? g : id(node.name));
568
+ break;
569
+ }
570
+ case 'FuncExpr': {
571
+ emit(`${node.async ? 'async ' : ''}function (${node.params.map(id).join(', ')}) `);
572
+ const savedAD = asyncDepth;
573
+ asyncDepth = node.async ? 1 : 0;
574
+ genBlock(node.body, '');
575
+ asyncDepth = savedAD;
576
+ break;
577
+ }
578
+ case 'Await':
579
+ if (asyncDepth === 0) {
580
+ throw new DevabhashaError('प्रतीक्षादोषः: प्रतीक्षा (await) is only valid inside an असमकालिक (async) function',
581
+ { line: node.line, col: node.col, kind: 'codegen' });
582
+ }
583
+ emit('await '); genExpr(node.argument);
584
+ break;
585
+ case 'Array':
586
+ emit('[');
587
+ node.elements.forEach((e, i) => { if (i) emit(', '); genExpr(e); });
588
+ emit(']');
589
+ break;
590
+ case 'Binary':
591
+ emit('('); genExpr(node.left); emit(` ${node.op} `); genExpr(node.right); emit(')');
592
+ break;
593
+ case 'Unary':
594
+ emit(node.op); genExpr(node.argument);
595
+ break;
596
+ case 'Assign':
597
+ // a भाव state cell write: x = v → x(v)
598
+ if (node.target.type === 'Identifier' && stateNames.has(node.target.name)) {
599
+ emit(id(node.target.name) + '('); genExpr(node.value); emit(')');
600
+ } else {
601
+ genExpr(node.target); emit(' = '); genExpr(node.value);
602
+ }
603
+ break;
604
+ case 'Ternary':
605
+ emit('('); genExpr(node.test); emit(' ? ');
606
+ genExpr(node.consequent); emit(' : ');
607
+ genExpr(node.alternate); emit(')');
608
+ break;
609
+ case 'OrElse':
610
+ // result अथवा fallback → __RT.orElse(result, () => (fallback))
611
+ // (lazy thunk; parens so an object-literal fallback isn't read as a block)
612
+ emit('__RT.orElse(');
613
+ genExpr(node.value);
614
+ emit(', () => (');
615
+ genExpr(node.fallback);
616
+ emit('))');
617
+ break;
618
+ case 'Sutra':
619
+ // सूत्र(expr) → a tagged reactive thunk; components bind it fine-grained
620
+ emit('__DB.sutra(() => (');
621
+ genExpr(node.expr);
622
+ emit('))');
623
+ break;
624
+ case 'Update':
625
+ // postfix ++/--; for a भाव cell: x++ → x(x() + 1)
626
+ if (node.target.type === 'Identifier' && stateNames.has(node.target.name)) {
627
+ const nm = id(node.target.name);
628
+ emit(`${nm}(${nm}() ${node.op === '++' ? '+' : '-'} 1)`);
629
+ } else {
630
+ genExpr(node.target); emit(node.op);
631
+ }
632
+ break;
633
+ case 'Call':
634
+ genExpr(node.callee);
635
+ emit('(');
636
+ node.args.forEach((a, i) => { if (i) emit(', '); genExpr(a); });
637
+ emit(')');
638
+ break;
639
+ case 'Member':
640
+ // namespace member (आयात * रूपेण ग → ग.पाई) uses the export key id(),
641
+ // bypassing the stdlib method/constant tables.
642
+ if (!node.computed && node.object.type === 'Identifier'
643
+ && namespaceAliases.has(node.object.name)) {
644
+ emit(id(node.object.name));
645
+ emit(`[${JSON.stringify(id(node.property))}]`);
646
+ break;
647
+ }
648
+ genExpr(node.object);
649
+ if (node.computed) { emit('['); genExpr(node.property); emit(']'); }
650
+ else {
651
+ // translate Sanskrit stdlib names → JS; pass through otherwise
652
+ const jsName = METHODS[node.property] || PROPERTIES[node.property]
653
+ || MATH_CONSTANTS[node.property] || node.property;
654
+ emit(`.${jsName}`);
655
+ }
656
+ break;
657
+ case 'ObjectLiteral':
658
+ emit('{ ');
659
+ node.props.forEach((p, i) => {
660
+ if (i) emit(', ');
661
+ emit(JSON.stringify(p.key.value)); emit(': '); genExpr(p.value);
662
+ });
663
+ emit(' }');
664
+ break;
665
+ case 'ElementExpr':
666
+ emit('__DB.el(');
667
+ node.args.forEach((a, i) => { if (i) emit(', '); genExpr(a); });
668
+ emit(')');
669
+ break;
670
+ case 'Construct': {
671
+ // Assemble from role slots; order in source is irrelevant.
672
+ const s = node.slots;
673
+ emit('__DB.construct({ tag: ');
674
+ genExpr(s.tag);
675
+ if (s.content) {
676
+ if (!inView && readsState(s.content)) {
677
+ // dynamic content outside a दृश्य → fine-grained: bind only this node
678
+ emit(', contentBind: () => ('); genExpr(s.content); emit(')');
679
+ } else {
680
+ emit(', content: '); genExpr(s.content);
681
+ }
682
+ }
683
+ if (s.event) { emit(', event: '); genExpr(s.event); }
684
+ if (s.handler) { emit(', handler: '); genExpr(s.handler); }
685
+ if (s.parent) { emit(', parent: '); genExpr(s.parent); }
686
+ if (s.prop) { emit(', prop: '); genExpr(s.prop); }
687
+ if (s.source) { emit(', source: '); genExpr(s.source); }
688
+ // रूप style: optional named base merged with translated inline pairs.
689
+ // Dynamic pairs (reading भाव cells, outside a view) become fine-grained
690
+ // styleBind thunks; static pairs stay a plain object (zero overhead).
691
+ if (node.style && (node.style.base || (node.style.pairs && node.style.pairs.length))) {
692
+ const { base, pairs } = node.style;
693
+ const { staticPairs, bindPairs } = partitionStylePairs(pairs || []);
694
+ if (base || staticPairs.length) {
695
+ emit(', style: ');
696
+ if (base) {
697
+ emit('Object.assign({}, ');
698
+ genExpr(base);
699
+ if (staticPairs.length) { emit(', '); emitStyleObject(staticPairs); }
700
+ emit(')');
701
+ } else {
702
+ emitStyleObject(staticPairs);
703
+ }
704
+ }
705
+ if (bindPairs.length) { emit(', styleBind: '); emitStyleBindObject(bindPairs); }
706
+ }
707
+ // समास children: a sibling list of nested elements / text
708
+ if (node.children && node.children.length) {
709
+ emit(', children: [');
710
+ node.children.forEach((c, i) => { if (i) emit(', '); genExpr(c); });
711
+ emit(']');
712
+ }
713
+ emit(' })');
714
+ break;
715
+ }
716
+ case 'Mount':
717
+ emit('__DB.mount(');
718
+ node.args.forEach((a, i) => { if (i) emit(', '); genExpr(a); });
719
+ emit(')');
720
+ break;
721
+ case 'Listen':
722
+ emit('__DB.listen(');
723
+ node.args.forEach((a, i) => { if (i) emit(', '); genExpr(a); });
724
+ emit(')');
725
+ break;
726
+ default:
727
+ throw new Error(`codegen: unknown expression ${node.type}`);
728
+ }
729
+ }
730
+
731
+ if (includeRuntime) emit(PRELUDE + RUNTIME + '\n');
732
+ gen(ast);
733
+
734
+ if (sourceMap || withMeta) {
735
+ const result = { code: out, exports, imports };
736
+ if (sourceMap) result.map = buildSourceMap(mappings, out);
737
+ return result;
738
+ }
739
+ return out;
740
+ }
741
+
742
+ // ----- Source Map v3 (VLQ-encoded) -----
743
+ const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
744
+ function vlqEncode(num) {
745
+ let vlq = num < 0 ? ((-num) << 1) | 1 : num << 1;
746
+ let out = '';
747
+ do {
748
+ let digit = vlq & 0b11111;
749
+ vlq >>>= 5;
750
+ if (vlq > 0) digit |= 0b100000;
751
+ out += B64[digit];
752
+ } while (vlq > 0);
753
+ return out;
754
+ }
755
+
756
+ // Build a standard Source Map v3 object from statement-level mappings.
757
+ // `mappings` entries are 0-based genLine/genCol with 1-based source line/col.
758
+ function buildSourceMap(mappings, code, { file = 'out.js', source = 'input.deva' } = {}) {
759
+ // group by generated line, sort by generated column
760
+ const byLine = new Map();
761
+ for (const m of mappings) {
762
+ if (!byLine.has(m.genLine)) byLine.set(m.genLine, []);
763
+ byLine.get(m.genLine).push(m);
764
+ }
765
+ const totalLines = code.split('\n').length;
766
+
767
+ // VLQ deltas are relative to previous values across the whole file:
768
+ // [genColDelta, sourceIndexDelta, srcLineDelta, srcColDelta]
769
+ let prevGenCol = 0, prevSrcLine = 0, prevSrcCol = 0;
770
+ const lineStrings = [];
771
+ for (let line = 0; line < totalLines; line++) {
772
+ const segs = (byLine.get(line) || []).sort((a, b) => a.genCol - b.genCol);
773
+ prevGenCol = 0; // generated column resets each output line
774
+ const parts = [];
775
+ for (const m of segs) {
776
+ const seg =
777
+ vlqEncode(m.genCol - prevGenCol) +
778
+ vlqEncode(0) + // single source, index 0
779
+ vlqEncode((m.srcLine - 1) - prevSrcLine) +
780
+ vlqEncode((m.srcCol - 1) - prevSrcCol);
781
+ prevGenCol = m.genCol;
782
+ prevSrcLine = m.srcLine - 1;
783
+ prevSrcCol = m.srcCol - 1;
784
+ parts.push(seg);
785
+ }
786
+ lineStrings.push(parts.join(','));
787
+ }
788
+
789
+ return {
790
+ version: 3,
791
+ file,
792
+ sources: [source],
793
+ names: [],
794
+ mappings: lineStrings.join(';'),
795
+ };
796
+ }
797
+
798
+ // Identifiers are transliterated to stable ASCII so the generated JS is
799
+ // portable and debuggable. The mapping is deterministic, so the same
800
+ // Sanskrit name always yields the same JS name (declaration == call site).
801
+ const TRANSLIT = {
802
+ // independent vowels
803
+ 'अ':'a','आ':'aa','इ':'i','ई':'ii','उ':'u','ऊ':'uu','ऋ':'ri','ॠ':'rii',
804
+ 'ऌ':'li','ए':'e','ऐ':'ai','ओ':'o','औ':'au',
805
+ // consonants (inherent 'a')
806
+ 'क':'ka','ख':'kha','ग':'ga','घ':'gha','ङ':'nga',
807
+ 'च':'ca','छ':'cha','ज':'ja','झ':'jha','ञ':'nya',
808
+ 'ट':'tta','ठ':'ttha','ड':'dda','ढ':'ddha','ण':'nna',
809
+ 'त':'ta','थ':'tha','द':'da','ध':'dha','न':'na',
810
+ 'प':'pa','फ':'pha','ब':'ba','भ':'bha','म':'ma',
811
+ 'य':'ya','र':'ra','ल':'la','व':'va','श':'sha','ष':'ssa','स':'sa','ह':'ha',
812
+ 'ळ':'lla',
813
+ // dependent vowel signs (matras) — replace the inherent 'a'
814
+ '\u093E':'aa','\u093F':'i','\u0940':'ii','\u0941':'u','\u0942':'uu',
815
+ '\u0943':'ri','\u0947':'e','\u0948':'ai','\u094B':'o','\u094C':'au',
816
+ // anusvara / visarga / chandrabindu
817
+ '\u0902':'m','\u0903':'h','\u0901':'n',
818
+ };
819
+ const VIRAMA = '\u094D';
820
+
821
+ export function id(name) {
822
+ let result = '';
823
+ const chars = [...name];
824
+ for (let k = 0; k < chars.length; k++) {
825
+ const ch = chars[k];
826
+ if (/[A-Za-z0-9_$]/.test(ch)) { result += ch; continue; }
827
+ if (ch === VIRAMA) {
828
+ // virama removes the inherent 'a' of the preceding consonant
829
+ if (result.endsWith('a')) result = result.slice(0, -1);
830
+ continue;
831
+ }
832
+ const next = chars[k + 1];
833
+ const map = TRANSLIT[ch];
834
+ if (map !== undefined) {
835
+ // a matra replaces the inherent 'a' just emitted for the consonant
836
+ if (/[\u093E-\u094C]/.test(ch) && result.endsWith('a')) {
837
+ result = result.slice(0, -1) + map;
838
+ } else {
839
+ result += map;
840
+ }
841
+ } else {
842
+ result += '_u' + ch.codePointAt(0).toString(16);
843
+ }
844
+ }
845
+ if (/^[0-9]/.test(result)) result = '_' + result;
846
+ // A transliterated name might collide with a JS reserved word
847
+ // (e.g. दो → "do"). Suffix an underscore to keep it a valid identifier.
848
+ if (JS_RESERVED.has(result)) result = result + '_';
849
+ return result || '_';
850
+ }
851
+
852
+ const JS_RESERVED = new Set([
853
+ 'do','if','in','for','new','var','let','try','case','else','enum','eval',
854
+ 'null','this','true','void','with','break','catch','class','const','false',
855
+ 'super','throw','while','yield','delete','export','import','return','switch',
856
+ 'typeof','default','extends','finally','continue','debugger','function',
857
+ 'arguments','await','async','instanceof',
858
+ ]);
859
+
860
+ function unescapeStr(s) {
861
+ return s
862
+ .replace(/\\n/g, '\n').replace(/\\t/g, '\t')
863
+ .replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\\\\/g, '\\');
864
+ }