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.
@@ -0,0 +1,148 @@
1
+ // devserver.js — a zero-dependency dev server with live reload for .deva web
2
+ // programs. Serves an HTML page that runs the compiled program, watches the
3
+ // source (and its directory, catching आयात imports), recompiles on change, and
4
+ // pushes a reload signal to the browser over Server-Sent Events. Compile errors
5
+ // are shown on the page instead of a blank screen.
6
+ //
7
+ // Uses only Node built-ins (http, fs) — no external packages — so it runs in
8
+ // the same dependency-free environment as the rest of the toolchain.
9
+
10
+ import { createServer } from 'http';
11
+ import { readFileSync, watch } from 'fs';
12
+ import { dirname, basename } from 'path';
13
+ import { bundle } from './bundler.js';
14
+ import { DevabhashaError, formatError } from './errors.js';
15
+
16
+ // Build the browser bundle for the entry file. Returns { ok, code | error }.
17
+ function buildBundle(entry) {
18
+ try {
19
+ const code = bundle(entry, { includeRuntime: true });
20
+ return { ok: true, code };
21
+ } catch (e) {
22
+ const src = (() => { try { return readFileSync(entry, 'utf8'); } catch { return ''; } })();
23
+ const msg = e instanceof DevabhashaError ? formatError(e, e.source || src) : ('दोषः: ' + e.message);
24
+ return { ok: false, error: msg };
25
+ }
26
+ }
27
+
28
+ // The HTML shell. #मूलम् is the mount root; the live-reload client listens on
29
+ // /__live (SSE) and reloads on the 'reload' event. A compile error is rendered
30
+ // into the page (passed via a global the bundle replaces) rather than crashing.
31
+ function pageHtml(entryName) {
32
+ return `<!DOCTYPE html>
33
+ <html lang="sa">
34
+ <head>
35
+ <meta charset="utf-8">
36
+ <meta name="viewport" content="width=device-width, initial-scale=1">
37
+ <title>${entryName} — देवभाषा</title>
38
+ <style>
39
+ body { margin: 0; font-family: system-ui, -apple-system, sans-serif; background: #f4f1ea; }
40
+ #__err { white-space: pre-wrap; font-family: ui-monospace, monospace; color: #a33;
41
+ background: #fff5f5; border: 1px solid #f3c0c0; border-radius: 8px;
42
+ margin: 20px; padding: 16px 20px; font-size: 13px; }
43
+ #मूलम् { min-height: 100vh; }
44
+ </style>
45
+ </head>
46
+ <body>
47
+ <div id="मूलम्"></div>
48
+ <script>
49
+ // live reload over Server-Sent Events
50
+ try {
51
+ const es = new EventSource('/__live');
52
+ es.addEventListener('reload', () => location.reload());
53
+ } catch (e) {}
54
+ </script>
55
+ <script src="/__bundle.js"></script>
56
+ </body>
57
+ </html>`;
58
+ }
59
+
60
+ // Wrap a compiled bundle so the DOM runtime mounts to #मूलम् by default and
61
+ // any runtime error is shown on the page.
62
+ function wrapBundle(code) {
63
+ return `(function(){
64
+ var __root = document.getElementById('मूलम्');
65
+ try {
66
+ ${code}
67
+ } catch (e) {
68
+ var d = document.createElement('div'); d.id = '__err';
69
+ d.textContent = 'दोषः (runtime): ' + (e && e.message || e);
70
+ document.body.appendChild(d);
71
+ console.error(e);
72
+ }
73
+ })();`;
74
+ }
75
+
76
+ function errorPageScript(message) {
77
+ return `(function(){
78
+ var d = document.createElement('div'); d.id = '__err';
79
+ d.textContent = ${JSON.stringify(message)};
80
+ document.body.appendChild(d);
81
+ })();`;
82
+ }
83
+
84
+ export function serve(entry, { port = 5173, open = false } = {}) {
85
+ const entryName = basename(entry);
86
+ const clients = new Set(); // open SSE responses
87
+
88
+ const server = createServer((req, res) => {
89
+ if (req.url === '/' || req.url === '/index.html') {
90
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
91
+ res.end(pageHtml(entryName));
92
+ return;
93
+ }
94
+ if (req.url === '/__bundle.js') {
95
+ const built = buildBundle(entry);
96
+ res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
97
+ res.end(built.ok ? wrapBundle(built.code) : errorPageScript(built.error));
98
+ return;
99
+ }
100
+ if (req.url === '/__live') {
101
+ res.writeHead(200, {
102
+ 'Content-Type': 'text/event-stream',
103
+ 'Cache-Control': 'no-cache',
104
+ Connection: 'keep-alive',
105
+ });
106
+ res.write('retry: 1000\n\n');
107
+ clients.add(res);
108
+ req.on('close', () => clients.delete(res));
109
+ return;
110
+ }
111
+ res.writeHead(404); res.end('not found');
112
+ });
113
+
114
+ function notifyReload() {
115
+ for (const res of clients) { try { res.write('event: reload\ndata: 1\n\n'); } catch {} }
116
+ }
117
+
118
+ // Watch the entry's directory so edits to it OR its imports trigger a reload.
119
+ // Debounced — editors often fire several events per save.
120
+ let timer = null;
121
+ const dir = dirname(entry) || '.';
122
+ let watcher = null;
123
+ try {
124
+ watcher = watch(dir, { persistent: true }, (_evt, fname) => {
125
+ if (fname && !/\.deva$/.test(fname)) return; // only .deva changes
126
+ clearTimeout(timer);
127
+ timer = setTimeout(() => {
128
+ const built = buildBundle(entry);
129
+ console.log(built.ok ? `↻ recompiled ${entryName}` : `✗ ${entryName} has errors (shown in browser)`);
130
+ notifyReload();
131
+ }, 60);
132
+ });
133
+ } catch (e) {
134
+ console.error('चेतावनी: file watching unavailable — live reload disabled.');
135
+ }
136
+
137
+ server.listen(port, () => {
138
+ // initial build feedback
139
+ const built = buildBundle(entry);
140
+ if (!built.ok) console.log(`✗ ${entryName} has errors (shown in browser):\n` + built.error);
141
+ console.log(`\n देवभाषा dev server\n ▸ http://localhost:${port}/\n watching ${entryName} for changes (live reload on)\n`);
142
+ });
143
+
144
+ return {
145
+ server,
146
+ close() { if (watcher) watcher.close(); for (const r of clients) { try { r.end(); } catch {} } server.close(); },
147
+ };
148
+ }
package/src/errors.js ADDED
@@ -0,0 +1,71 @@
1
+ // errors.js — structured compiler errors with source-context formatting.
2
+ //
3
+ // A good error shows WHERE the problem is, in the source, with a caret.
4
+ // DevabhashaError carries the position; formatError renders the offending
5
+ // line and points at the column:
6
+ //
7
+ // दोषः (parse error): expected ')' but found ';'
8
+ // line 3, column 12
9
+ //
10
+ // ३ | दर्शय(अ + ब;
11
+ // | ^
12
+ //
13
+ // kind is a short tag ('lex' | 'parse' | 'codegen') used for the heading.
14
+
15
+ export class DevabhashaError extends Error {
16
+ constructor(message, { line = null, col = null, kind = 'parse' } = {}) {
17
+ super(message);
18
+ this.name = 'DevabhashaError';
19
+ this.line = line;
20
+ this.col = col;
21
+ this.kind = kind;
22
+ }
23
+ }
24
+
25
+ const KIND_LABEL = {
26
+ lex: 'अक्षरदोषः (lex error)',
27
+ parse: 'पाठदोषः (parse error)',
28
+ codegen: 'कूटदोषः (codegen error)',
29
+ runtime: 'क्रियादोषः (runtime error)',
30
+ };
31
+
32
+ // Convert a Devanagari/ASCII digit line number into Devanagari for display.
33
+ const DEVA_DIGITS = '०१२३४५६७८९';
34
+ function toDeva(n) {
35
+ return String(n).split('').map(d => /[0-9]/.test(d) ? DEVA_DIGITS[+d] : d).join('');
36
+ }
37
+
38
+ // Render an error with source context. `source` is the original program text.
39
+ export function formatError(err, source) {
40
+ const label = KIND_LABEL[err.kind] || 'दोषः (error)';
41
+ let out = `${label}: ${err.message}`;
42
+
43
+ if (err.line == null) return out;
44
+
45
+ out += `\n line ${err.line}, column ${err.col ?? '?'}`;
46
+
47
+ if (source != null) {
48
+ const lines = source.split('\n');
49
+ const srcLine = lines[err.line - 1];
50
+ if (srcLine !== undefined) {
51
+ const gutter = toDeva(err.line);
52
+ const pad = ' '.repeat(gutter.length);
53
+ out += `\n\n ${gutter} | ${srcLine}`;
54
+ if (err.col != null) {
55
+ // caret: account for the gutter and the column (1-based)
56
+ const caretPad = ' '.repeat(Math.max(0, err.col - 1));
57
+ out += `\n ${pad} | ${caretPad}^`;
58
+ }
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+
64
+ // Helper used by the lexer/parser to raise a positioned error.
65
+ export function raise(message, pos, kind = 'parse') {
66
+ throw new DevabhashaError(message, {
67
+ line: pos && pos.line,
68
+ col: pos && pos.col,
69
+ kind,
70
+ });
71
+ }
package/src/index.js ADDED
@@ -0,0 +1,30 @@
1
+ // index.js — public API: compile(source) -> javascript string.
2
+
3
+ import { tokenize } from './lexer.js';
4
+ import { parse } from './parser.js';
5
+ import { generate } from './codegen.js';
6
+
7
+ export function compile(source, options = {}) {
8
+ const tokens = tokenize(source);
9
+ const ast = parse(tokens);
10
+ return generate(ast, options);
11
+ }
12
+
13
+ // Like compile, but returns { code, exports, imports } for the bundler.
14
+ // Never includes the runtime (the bundler adds it once for the whole program).
15
+ export function compileModule(source) {
16
+ const tokens = tokenize(source);
17
+ const ast = parse(tokens);
18
+ return generate(ast, { includeRuntime: false, withMeta: true });
19
+ }
20
+
21
+ // Like compile, but also returns a Source Map v3 object: { code, map }.
22
+ export function compileWithMap(source, options = {}) {
23
+ const tokens = tokenize(source);
24
+ const ast = parse(tokens);
25
+ return generate(ast, { ...options, sourceMap: true });
26
+ }
27
+
28
+ export { tokenize, parse, generate };
29
+ export { PRELUDE } from './codegen.js';
30
+ export { DevabhashaError, formatError } from './errors.js';
@@ -0,0 +1,31 @@
1
+ // io-browser.js — the browser backend for the layered I/O interface (__IO).
2
+ //
3
+ // The frontend half of a full-stack Devabhāṣā app runs in the browser and uses
4
+ // जाल (network) to talk to its backend. The codegen lowers जाल.आनय /
5
+ // आनयप्रदत्त to __IO.net.fetch / fetchJson, so a browser page that runs a
6
+ // compiled frontend needs an __IO whose `net` uses the browser's native fetch.
7
+ //
8
+ // File ops have no meaning in a browser, so सञ्चिका.* returns a clear failure
9
+ // Result rather than throwing — keeping the same program runnable on either
10
+ // host, with the Result telling it what isn't available here.
11
+ //
12
+ // Exported as a source string (like IO_NODE_SOURCE) so the server can prepend
13
+ // it to the frontend bundle it serves.
14
+
15
+ export const IO_BROWSER_SOURCE = `// --- देवभाषा I/O (browser backend) ---
16
+ const __IO = (() => {
17
+ const ok = (v) => ({ "सफल": true, "मूल्यम्": v, "दोषः": null });
18
+ const err = (e) => ({ "सफल": false, "मूल्यम्": null, "दोषः": String(e && e.message || e) });
19
+ const noFile = async () => err('file I/O is not available in the browser');
20
+ return {
21
+ file: {
22
+ read: noFile, write: noFile, exists: async () => ok(false),
23
+ remove: noFile, list: noFile, readJson: noFile, writeJson: noFile,
24
+ },
25
+ net: {
26
+ async fetch(url, options){ try { const res = await fetch(url, options||undefined); const text = await res.text(); return ok({ "स्थितिः": res.status, "पाठः": text, "सफलम्": res.ok }); } catch(e){ return err(e); } },
27
+ async fetchJson(url, options){ try { const res = await fetch(url, options||undefined); const text = await res.text(); try { return ok({ "स्थितिः": res.status, "प्रदत्तम्": JSON.parse(text), "सफलम्": res.ok }); } catch(e){ return err('JSON: '+(e&&e.message||e)); } } catch(e){ return err(e); } },
28
+ },
29
+ };
30
+ })();
31
+ `;
package/src/io-node.js ADDED
@@ -0,0 +1,102 @@
1
+ // io-node.js — the Node backend for the __IO interface.
2
+ //
3
+ // Layered design: a Devabhāṣā program calls सञ्चिका.पठ / जाल.आनय, which the
4
+ // codegen lowers to __IO.file.read / __IO.net.fetch. The program is bound to
5
+ // the __IO *interface*, never to Node — this file is one concrete backend.
6
+ // A browser backend or an in-memory test backend can implement the same
7
+ // shape and be injected instead.
8
+ //
9
+ // THE __IO CONTRACT (every operation is async and returns a परिणाम/Result):
10
+ // __IO.file.read(path) → Promise<Result<string>>
11
+ // __IO.file.write(path, data) → Promise<Result<true>>
12
+ // __IO.file.exists(path) → Promise<Result<boolean>>
13
+ // __IO.file.remove(path) → Promise<Result<true>>
14
+ // __IO.file.list(dir) → Promise<Result<string[]>>
15
+ // __IO.net.fetch(url, options?) → Promise<Result<{ status, text, ok }>>
16
+ //
17
+ // A Result is { सफल: boolean, मूल्यम्: value, दोषः: error } — the same shape
18
+ // the __RT prelude produces, so awaited results inspect with फल.सफल etc.
19
+
20
+ import { readFile, writeFile, unlink, readdir, access } from 'fs/promises';
21
+
22
+ const ok = (v) => ({ 'सफल': true, 'मूल्यम्': v, 'दोषः': null });
23
+ const err = (e) => ({ 'सफल': false, 'मूल्यम्': null, 'दोषः': String(e && e.message || e) });
24
+
25
+ export const __IO = {
26
+ file: {
27
+ async read(path) {
28
+ try { return ok(await readFile(path, 'utf8')); }
29
+ catch (e) { return err(e); }
30
+ },
31
+ async write(path, data) {
32
+ try { await writeFile(path, String(data), 'utf8'); return ok(true); }
33
+ catch (e) { return err(e); }
34
+ },
35
+ async exists(path) {
36
+ try { await access(path); return ok(true); }
37
+ catch { return ok(false); } // absence is not an error — it's `false`
38
+ },
39
+ async remove(path) {
40
+ try { await unlink(path); return ok(true); }
41
+ catch (e) { return err(e); }
42
+ },
43
+ async list(dir) {
44
+ try { return ok(await readdir(dir)); }
45
+ catch (e) { return err(e); }
46
+ },
47
+ async readJson(path) { // read + parse JSON → परिणाम
48
+ try {
49
+ const text = await readFile(path, 'utf8');
50
+ try { return ok(JSON.parse(text)); }
51
+ catch (e) { return err('JSON: ' + (e && e.message || e)); }
52
+ } catch (e) { return err(e); }
53
+ },
54
+ async writeJson(path, value) { // serialize + write
55
+ try { await writeFile(path, JSON.stringify(value, null, 2), 'utf8'); return ok(true); }
56
+ catch (e) { return err(e); }
57
+ },
58
+ },
59
+ net: {
60
+ async fetch(url, options) {
61
+ try {
62
+ const res = await fetch(url, options || undefined);
63
+ const text = await res.text();
64
+ return ok({ 'स्थितिः': res.status, 'पाठः': text, 'सफलम्': res.ok });
65
+ } catch (e) { return err(e); }
66
+ },
67
+ async fetchJson(url, options) { // fetch + parse JSON → परिणाम
68
+ try {
69
+ const res = await fetch(url, options || undefined);
70
+ const text = await res.text();
71
+ try { return ok({ 'स्थितिः': res.status, 'प्रदत्तम्': JSON.parse(text), 'सफलम्': res.ok }); }
72
+ catch (e) { return err('JSON: ' + (e && e.message || e)); }
73
+ } catch (e) { return err(e); }
74
+ },
75
+ },
76
+ };
77
+
78
+ // A source string that defines the same __IO for embedding in `build` output.
79
+ // (For build, we inline the backend so the produced .js is self-contained
80
+ // when run under Node.)
81
+ export const IO_NODE_SOURCE = `// --- देवभाषा I/O (Node backend) ---
82
+ const __IO = (() => {
83
+ const { readFile, writeFile, unlink, readdir, access } = require('fs/promises');
84
+ const ok = (v) => ({ "सफल": true, "मूल्यम्": v, "दोषः": null });
85
+ const err = (e) => ({ "सफल": false, "मूल्यम्": null, "दोषः": String(e && e.message || e) });
86
+ return {
87
+ file: {
88
+ async read(p){ try { return ok(await readFile(p,'utf8')); } catch(e){ return err(e); } },
89
+ async write(p,d){ try { await writeFile(p,String(d),'utf8'); return ok(true); } catch(e){ return err(e); } },
90
+ async exists(p){ try { await access(p); return ok(true); } catch { return ok(false); } },
91
+ async remove(p){ try { await unlink(p); return ok(true); } catch(e){ return err(e); } },
92
+ async list(dir){ try { return ok(await readdir(dir)); } catch(e){ return err(e); } },
93
+ async readJson(p){ try { const t = await readFile(p,'utf8'); try { return ok(JSON.parse(t)); } catch(e){ return err('JSON: '+(e&&e.message||e)); } } catch(e){ return err(e); } },
94
+ async writeJson(p,v){ try { await writeFile(p,JSON.stringify(v,null,2),'utf8'); return ok(true); } catch(e){ return err(e); } },
95
+ },
96
+ net: {
97
+ async fetch(url, options){ try { const res = await fetch(url, options||undefined); const text = await res.text(); return ok({ "स्थितिः": res.status, "पाठः": text, "सफलम्": res.ok }); } catch(e){ return err(e); } },
98
+ async fetchJson(url, options){ try { const res = await fetch(url, options||undefined); const text = await res.text(); try { return ok({ "स्थितिः": res.status, "प्रदत्तम्": JSON.parse(text), "सफलम्": res.ok }); } catch(e){ return err('JSON: '+(e&&e.message||e)); } } catch(e){ return err(e); } },
99
+ },
100
+ };
101
+ })();
102
+ `;
@@ -0,0 +1,49 @@
1
+ // karaka-web.js — maps kāraka roles to web/DOM construction semantics,
2
+ // and defines which stems name which DOM concepts.
3
+ //
4
+ // The signature verb रचय (racaya, "construct") takes a BAG of case-marked
5
+ // arguments in ANY ORDER and assembles a DOM element. Each kāraka fills a
6
+ // distinct slot, so order is irrelevant — exactly the Pāṇinian promise.
7
+
8
+ import { KARAKA } from './vibhakti.js';
9
+
10
+ // kāraka → which slot of a DOM construction it fills.
11
+ export const KARAKA_TO_SLOT = {
12
+ [KARAKA.KARTR]: 'tag', // कर्तृ (nom) — what the element IS (div, button…)
13
+ [KARAKA.KARMAN]: 'content', // कर्म (acc) — content placed into it
14
+ [KARAKA.KARANA]: 'handler', // करण (instr)— the handler function (instrument)
15
+ [KARAKA.SAMPRADANA]: 'event', // सम्प्रदान (dat) — event it responds to
16
+ [KARAKA.ADHIKARANA]: 'parent', // अधिकरण (loc) — where it mounts (locus)
17
+ [KARAKA.APADANA]: 'source', // अपादान (abl) — data source it derives from
18
+ [KARAKA.SAMBANDHA]: 'prop', // सम्बन्ध (gen) — attribute/property relation
19
+ };
20
+
21
+ // Stem vocabulary: nominative-case stems that name HTML tags.
22
+ // The user writes these inflected (पटः, मूलकम्…); the engine reads the
23
+ // ending for the role and this table for the tag name.
24
+ export const TAG_STEMS = {
25
+ 'पट': 'button', // paṭa — "cloth/panel" → button
26
+ 'मूल': 'div', // mūla — "root/base" → div (generic container)
27
+ 'शीर्ष': 'h1', // śīrṣa — "head" → heading
28
+ 'वाक्य': 'p', // vākya — "sentence" → paragraph
29
+ 'सूची': 'ul', // sūcī — "list" → list
30
+ 'पङ्क्ति':'li', // paṅkti — "row/line" → list item
31
+ 'पीठ': 'input', // pīṭha — "seat/field" → input
32
+ 'चित्र': 'img', // citra — "picture" → image
33
+ 'सेतु': 'a', // setu — "bridge/link" → anchor
34
+ 'क्षेत्र':'span', // kṣetra — "field/area"→ span
35
+ };
36
+
37
+ // Event stems (used in सम्प्रदान / dative position).
38
+ export const EVENT_STEMS = {
39
+ 'स्पर्श': 'click', // sparśa — "touch" → click
40
+ 'परिवर्तन':'change', // parivartana → change
41
+ 'निवेश': 'input', // niveśa — "entry" → input
42
+ 'प्रेषण': 'submit', // preṣaṇa — "sending"→ submit
43
+ };
44
+
45
+ // Construction / action verbs.
46
+ export const KARAKA_VERBS = {
47
+ 'रचय': 'CONSTRUCT', // racaya — "construct" → build DOM element from kārakas
48
+ 'योजय': 'ATTACH', // yojaya — "join" → mount (also legacy keyword)
49
+ };
@@ -0,0 +1,64 @@
1
+ // keywords.js — the Sanskrit-language surface layer.
2
+ // Edit vocabulary here without touching the lexer/parser/codegen.
3
+ //
4
+ // Each keyword maps a Devanagari word -> an internal token type.
5
+ // The internal token types are language-neutral so the rest of the
6
+ // compiler never sees Sanskrit directly.
7
+
8
+ export const KEYWORDS = {
9
+ // declarations
10
+ 'चर': 'LET', // cara — "it varies" → mutable binding
11
+ 'नियत': 'CONST', // niyata — "fixed" → constant
12
+ 'कार्य': 'FUNC', // kārya — "work to be done" → function
13
+ 'फलम्': 'RETURN', // phalam — "fruit/result" → return
14
+
15
+ // control flow
16
+ 'यदि': 'IF', // yadi — if
17
+ 'अन्यथा': 'ELSE', // anyathā — otherwise
18
+ 'यावत्': 'WHILE', // yāvat — "as long as" → while
19
+ 'प्रत्येकम्': 'FOR', // pratyekam — "for each" → for-of
20
+ 'भङ्ग': 'BREAK', // bhaṅga — "breaking" → break
21
+ 'अनुवृत्तम्': 'CONTINUE', // anuvṛttam — "continuing" → continue
22
+
23
+ // literals
24
+ 'सत्यम्': 'TRUE', // satyam — true
25
+ 'असत्यम्': 'FALSE', // asatyam — false
26
+ 'शून्यम्': 'NULL', // śūnyam — "void/zero" → null
27
+
28
+ // web / DOM layer
29
+ 'दर्शय': 'PRINT', // darśaya — "cause to show" → console.log
30
+ 'अङ्गम्': 'ELEMENT', // aṅgam — "limb/part" → createElement
31
+ 'योजय': 'MOUNT', // yojaya — "join/attach" → append to DOM
32
+ 'श्रोता': 'LISTEN', // śrotā — "listener" → addEventListener
33
+ 'रचय': 'CONSTRUCT', // racaya — "construct" → kāraka-based DOM builder
34
+ 'कोष': 'OBJECT', // kośa — "treasury/dictionary" → object literal
35
+ 'रूप': 'STYLE', // rūpa — "form/appearance" → style block
36
+ 'रूपनाम': 'STYLENAME', // rūpanāma — "form-name" → named reusable style
37
+ 'भाव': 'STATE', // bhāva — "state/condition" → reactive state cell
38
+ 'दृश्य': 'VIEW', // dṛśya — "view/visible" → reactive view region
39
+ 'निर्यात': 'EXPORT', // niryāta — "sending out" → export
40
+ 'आयात': 'IMPORT', // āyāta — "incoming" → import
41
+ 'आ': 'FROM', // ā — "from" → module source preposition
42
+ 'असमकालिक': 'ASYNC', // asamakālika — "asynchronous" → async function
43
+ 'प्रतीक्षा': 'AWAIT', // pratīkṣā — "waiting" → await
44
+ 'अथवा': 'ORELSE', // athavā — "or else" → Result value-or-fallback
45
+ 'सूत्र': 'SUTRA', // sūtra — "thread" → a reactive reference (lazy, live)
46
+ };
47
+
48
+ // Reverse map for error messages / pretty-printing.
49
+ export const TOKEN_TO_WORD = Object.fromEntries(
50
+ Object.entries(KEYWORDS).map(([word, tok]) => [tok, word])
51
+ );
52
+
53
+ // Multi-character operators must be listed longest-first so the lexer
54
+ // matches '==' before '=', '+=' before '+', etc.
55
+ export const OPERATORS = [
56
+ '===', '!==', '??',
57
+ '==', '!=', '<=', '>=', '&&', '||',
58
+ '+=', '-=', '*=', '/=', '%=', '++', '--',
59
+ '+', '-', '*', '/', '%', '=', '<', '>', '!', '?',
60
+ '(', ')', '{', '}', '[', ']', ',', ';', '.', ':',
61
+ ];
62
+
63
+ // Devanagari danda (।) is accepted as a statement terminator, like ';'.
64
+ export const DANDA = '।';
package/src/lexer.js ADDED
@@ -0,0 +1,140 @@
1
+ // lexer.js — turns Devanagari source text into a flat token stream.
2
+
3
+ import { KEYWORDS, OPERATORS, DANDA } from './keywords.js';
4
+ import { DevabhashaError } from './errors.js';
5
+
6
+ // Devanagari block: U+0900–U+097F. We treat a "word" as a run of
7
+ // Devanagari letters, virama, matras, anusvara, etc. plus ASCII letters.
8
+ // We exclude Devanagari punctuation: danda । (U+0964) and double danda ॥
9
+ // (U+0965), which are statement terminators, not identifier characters.
10
+ const DEVA = /[\u0900-\u0963\u0966-\u097F]/;
11
+ const DEVA_DIGITS = '०१२३४५६७८९';
12
+
13
+ function isWordChar(ch) {
14
+ return DEVA.test(ch) || /[A-Za-z_]/.test(ch);
15
+ }
16
+ function isDigit(ch) {
17
+ return /[0-9]/.test(ch) || DEVA_DIGITS.includes(ch);
18
+ }
19
+ function normalizeDigit(ch) {
20
+ const i = DEVA_DIGITS.indexOf(ch);
21
+ return i >= 0 ? String(i) : ch;
22
+ }
23
+
24
+ export function tokenize(src) {
25
+ const tokens = [];
26
+ let i = 0;
27
+ let line = 1;
28
+ let col = 1;
29
+
30
+ const push = (type, value) => tokens.push({ type, value, line, col });
31
+ const advance = (n = 1) => { i += n; col += n; };
32
+
33
+ while (i < src.length) {
34
+ const ch = src[i];
35
+
36
+ // whitespace
37
+ if (ch === '\n') { line++; col = 1; i++; continue; }
38
+ if (/\s/.test(ch)) { advance(); continue; }
39
+
40
+ // comments: # to end of line
41
+ if (ch === '#') {
42
+ while (i < src.length && src[i] !== '\n') i++;
43
+ continue;
44
+ }
45
+
46
+ // danda → statement terminator
47
+ if (ch === DANDA) { push('SEMI', ';'); advance(); continue; }
48
+
49
+ // strings: "..." with \ escapes (plain — no interpolation by default).
50
+ // Interpolation is opt-in via the पाठ"…{expr}…" marker (handled below in
51
+ // the word branch), so existing strings containing literal braces are safe.
52
+ if (ch === '"' || ch === "'") {
53
+ const quote = ch;
54
+ const startLine = line, startCol = col;
55
+ advance();
56
+ let str = '';
57
+ while (i < src.length && src[i] !== quote) {
58
+ if (src[i] === '\\') { str += src[i] + src[i + 1]; advance(2); }
59
+ else if (src[i] === '\n') { line++; col = 1; str += '\n'; i++; }
60
+ else { str += src[i]; advance(); }
61
+ }
62
+ advance(); // closing quote
63
+ tokens.push({ type: 'STRING', value: str, line: startLine, col: startCol });
64
+ continue;
65
+ }
66
+
67
+ // numbers (supports Devanagari digits and decimals)
68
+ if (isDigit(ch)) {
69
+ let num = '';
70
+ while (i < src.length && (isDigit(src[i]) || src[i] === '.')) {
71
+ num += normalizeDigit(src[i]);
72
+ advance();
73
+ }
74
+ push('NUMBER', num);
75
+ continue;
76
+ }
77
+
78
+ // words → keyword or identifier
79
+ if (isWordChar(ch)) {
80
+ const wStartLine = line, wStartCol = col;
81
+ let word = '';
82
+ while (i < src.length && (isWordChar(src[i]) || isDigit(src[i]))) {
83
+ word += src[i];
84
+ advance();
85
+ }
86
+ // पाठ"…{expr}…" — an interpolated string. Only when पाठ is immediately
87
+ // followed by a quote (no space), so the plain identifier पाठ is unaffected.
88
+ if (word === 'पाठ' && (src[i] === '"' || src[i] === "'")) {
89
+ const quote = src[i];
90
+ advance(); // opening quote
91
+ const chunks = [''];
92
+ const exprs = [];
93
+ while (i < src.length && src[i] !== quote) {
94
+ if (src[i] === '\\') {
95
+ const nxt = src[i + 1];
96
+ if (nxt === '{' || nxt === '}') { chunks[chunks.length - 1] += nxt; advance(2); }
97
+ else { chunks[chunks.length - 1] += src[i] + nxt; advance(2); }
98
+ } else if (src[i] === '{') {
99
+ advance(); // {
100
+ let depth = 1, expr = '';
101
+ while (i < src.length && depth > 0) {
102
+ if (src[i] === '{') depth++;
103
+ else if (src[i] === '}') { depth--; if (depth === 0) break; }
104
+ if (src[i] === '\n') { line++; col = 1; expr += '\n'; i++; }
105
+ else { expr += src[i]; advance(); }
106
+ }
107
+ advance(); // }
108
+ exprs.push(expr);
109
+ chunks.push('');
110
+ } else if (src[i] === '\n') { line++; col = 1; chunks[chunks.length - 1] += '\n'; i++; }
111
+ else { chunks[chunks.length - 1] += src[i]; advance(); }
112
+ }
113
+ advance(); // closing quote
114
+ tokens.push({ type: 'TEMPLATE', chunks, exprs, line: wStartLine, col: wStartCol });
115
+ continue;
116
+ }
117
+ if (KEYWORDS[word]) push(KEYWORDS[word], word);
118
+ else push('IDENT', word);
119
+ continue;
120
+ }
121
+
122
+ // operators / punctuation
123
+ const three = src.slice(i, i + 3);
124
+ const two = src.slice(i, i + 2);
125
+ if (OPERATORS.includes(three)) { push('OP', three); advance(3); continue; }
126
+ if (OPERATORS.includes(two)) {
127
+ const t = two === ';' ? 'SEMI' : 'OP';
128
+ push(t, two); advance(2); continue;
129
+ }
130
+ if (OPERATORS.includes(ch)) {
131
+ const t = ch === ';' ? 'SEMI' : 'OP';
132
+ push(t, ch); advance(); continue;
133
+ }
134
+
135
+ throw new DevabhashaError(`अज्ञातं चिह्नम् (unknown character) '${ch}'`, { line, col, kind: 'lex' });
136
+ }
137
+
138
+ push('EOF', null);
139
+ return tokens;
140
+ }