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/symbols.js ADDED
@@ -0,0 +1,194 @@
1
+ // symbols.js — a scope-aware symbol table over the AST.
2
+ //
3
+ // Powers go-to-definition and rename in the language server. It walks the AST
4
+ // tracking lexical scopes: every binding (चर/नियत/कार्य/भाव/रूपनाम, function
5
+ // parameters, प्रत्येकम् loop variables) records its name and source position,
6
+ // and every Identifier reference is resolved to the binding it sees under
7
+ // normal shadowing rules. The result lets us answer:
8
+ // • definitionOf(line, col) → where the name at this position is bound
9
+ // • referencesOf(binding) → every occurrence bound to THAT binding
10
+ //
11
+ // Positions are 1-based (line, col), matching the lexer/parser.
12
+
13
+ import { tokenize } from './lexer.js';
14
+ import { parse } from './parser.js';
15
+
16
+ // A Scope maps a name → binding record. Bindings live in the scope where the
17
+ // declaration appears; lookups walk outward to enclosing scopes.
18
+ function makeScope(parent) {
19
+ return { parent, names: new Map() };
20
+ }
21
+ function declare(scope, name, binding) {
22
+ scope.names.set(name, binding);
23
+ }
24
+ function resolve(scope, name) {
25
+ for (let s = scope; s; s = s.parent) {
26
+ if (s.names.has(name)) return s.names.get(name);
27
+ }
28
+ return null;
29
+ }
30
+
31
+ // Build the symbol table. Returns { bindings, references }:
32
+ // bindings — array of { name, line, col, id }
33
+ // references — array of { name, line, col, binding|null }
34
+ export function buildSymbols(source) {
35
+ let ast;
36
+ try { ast = parse(tokenize(source)); }
37
+ catch { return { bindings: [], references: [], ok: false }; }
38
+
39
+ const bindings = [];
40
+ const references = [];
41
+ let nextId = 0;
42
+
43
+ const addBinding = (name, pos, scope) => {
44
+ if (!pos) return null;
45
+ const b = { name, line: pos.line, col: pos.col, id: nextId++ };
46
+ bindings.push(b);
47
+ declare(scope, name, b);
48
+ return b;
49
+ };
50
+ const addRef = (name, pos, scope) => {
51
+ if (!pos) return;
52
+ references.push({ name, line: pos.line, col: pos.col, binding: resolve(scope, name) });
53
+ };
54
+
55
+ // walk an expression, recording references
56
+ function walkExpr(node, scope) {
57
+ if (!node || typeof node !== 'object') return;
58
+ switch (node.type) {
59
+ case 'Identifier':
60
+ addRef(node.name, { line: node.line, col: node.col }, scope);
61
+ return;
62
+ case 'FuncExpr': {
63
+ const fscope = makeScope(scope);
64
+ (node.params || []).forEach((p, i) =>
65
+ addBinding(p, node.params.__pos && node.params.__pos[i], fscope));
66
+ walkBlock(node.body, fscope);
67
+ return;
68
+ }
69
+ case 'Member':
70
+ walkExpr(node.object, scope);
71
+ if (node.computed) walkExpr(node.property, scope);
72
+ // non-computed property is not a reference to a binding
73
+ return;
74
+ default:
75
+ // generic structural walk over child nodes/arrays
76
+ for (const k of Object.keys(node)) {
77
+ if (k === 'line' || k === 'col' || k === 'namePos' || k === 'paramPos') continue;
78
+ const v = node[k];
79
+ if (Array.isArray(v)) v.forEach(c => walkExpr(c, scope));
80
+ else if (v && typeof v === 'object' && v.type) walkExpr(v, scope);
81
+ }
82
+ }
83
+ }
84
+
85
+ // walk a block/body (array of statements or a {body:[]} node) in a NEW scope
86
+ function walkBlock(block, scope) {
87
+ const body = Array.isArray(block) ? block : (block && block.body) || [];
88
+ for (const s of body) walkStmt(s, scope);
89
+ }
90
+
91
+ function walkStmt(node, scope) {
92
+ if (!node || typeof node !== 'object') return;
93
+ switch (node.type) {
94
+ case 'VarDecl':
95
+ if (node.init) walkExpr(node.init, scope); // init sees the OUTER scope
96
+ addBinding(node.name, node.namePos, scope);
97
+ return;
98
+ case 'StateDecl':
99
+ if (node.init) walkExpr(node.init, scope);
100
+ addBinding(node.name, node.namePos, scope);
101
+ return;
102
+ case 'StyleDecl':
103
+ addBinding(node.name, node.namePos, scope);
104
+ (node.pairs || []).forEach(p => p.value && p.value.kind === 'expr' && walkExpr(p.value.value, scope));
105
+ return;
106
+ case 'FuncDecl': {
107
+ addBinding(node.name, node.namePos, scope); // function name in outer scope
108
+ const fscope = makeScope(scope);
109
+ (node.params || []).forEach((p, i) =>
110
+ addBinding(p, node.paramPos && node.paramPos[i], fscope));
111
+ walkBlock(node.body, fscope);
112
+ return;
113
+ }
114
+ case 'ForOf': {
115
+ walkExpr(node.iterable, scope);
116
+ const lscope = makeScope(scope);
117
+ addBinding(node.item, node.namePos, lscope);
118
+ walkBlock(node.body, lscope);
119
+ return;
120
+ }
121
+ case 'Block':
122
+ walkBlock(node.body, makeScope(scope));
123
+ return;
124
+ case 'If':
125
+ walkExpr(node.test, scope);
126
+ node.consequent && walkBlock(node.consequent.body || node.consequent, makeScope(scope));
127
+ node.alternate && walkBlock(node.alternate.body || node.alternate, makeScope(scope));
128
+ return;
129
+ case 'While':
130
+ walkExpr(node.test, scope);
131
+ node.body && walkBlock(node.body.body || node.body, makeScope(scope));
132
+ return;
133
+ case 'Export':
134
+ walkStmt(node.decl, scope);
135
+ return;
136
+ case 'View':
137
+ node.container && walkExpr(node.container, scope);
138
+ node.body && walkBlock(node.body.body || node.body, makeScope(scope));
139
+ return;
140
+ case 'ExpressionStatement':
141
+ walkExpr(node.expression, scope);
142
+ return;
143
+ default:
144
+ // structural fallback: walk children as expressions
145
+ for (const k of Object.keys(node)) {
146
+ if (k === 'line' || k === 'col' || k === 'namePos' || k === 'paramPos') continue;
147
+ const v = node[k];
148
+ if (Array.isArray(v)) v.forEach(c => c && c.type && walkExpr(c, scope));
149
+ else if (v && typeof v === 'object' && v.type) walkExpr(v, scope);
150
+ }
151
+ }
152
+ }
153
+
154
+ const top = makeScope(null);
155
+ walkBlock(ast, top);
156
+ // compound assignment (x += y) desugars to x = x + y, which visits the
157
+ // target identifier twice; dedup references sharing an exact position.
158
+ const seen = new Set();
159
+ const deduped = references.filter(r => {
160
+ const key = r.name + '@' + r.line + ':' + r.col;
161
+ if (seen.has(key)) return false;
162
+ seen.add(key); return true;
163
+ });
164
+ return { bindings, references: deduped, ok: true };
165
+ }
166
+
167
+ // Find the binding referenced at (line, col): either a reference sitting on
168
+ // that position, or a binding declared there. Returns the binding record.
169
+ export function definitionAt(source, line, col) {
170
+ const { bindings, references } = buildSymbols(source);
171
+ // a reference whose span covers the position?
172
+ const ref = references.find(r => r.line === line && col >= r.col && col < r.col + r.name.length);
173
+ if (ref) return ref.binding || null;
174
+ // already on a binding?
175
+ const b = bindings.find(b => b.line === line && col >= b.col && col < b.col + b.name.length);
176
+ return b || null;
177
+ }
178
+
179
+ // All occurrences (the binding + every reference bound to it) for the symbol
180
+ // at (line, col). Returns an array of { line, col, name } edit locations.
181
+ export function occurrencesAt(source, line, col) {
182
+ const { bindings, references } = buildSymbols(source);
183
+ // resolve the target binding from the cursor
184
+ let target = null;
185
+ const ref = references.find(r => r.line === line && col >= r.col && col < r.col + r.name.length);
186
+ if (ref) target = ref.binding;
187
+ if (!target) target = bindings.find(b => b.line === line && col >= b.col && col < b.col + b.name.length);
188
+ if (!target) return [];
189
+ const out = [{ line: target.line, col: target.col, name: target.name }];
190
+ for (const r of references) {
191
+ if (r.binding && r.binding.id === target.id) out.push({ line: r.line, col: r.col, name: r.name });
192
+ }
193
+ return out;
194
+ }
@@ -0,0 +1,87 @@
1
+ // vibhakti.js — the kāraka/case engine.
2
+ //
3
+ // Given an inflected Devanagari noun, detect its vibhakti (case ending),
4
+ // recover the stem, and map the case to a kāraka (semantic role).
5
+ //
6
+ // SCOPE (honest): full Sanskrit declension is stem-class- and gender-
7
+ // dependent and very large. We implement the अकारान्त पुंल्लिङ्ग/नपुंसकलिङ्ग
8
+ // (a-stem masc/neut) singular paradigm robustly — this covers the great
9
+ // majority of coined technical vocabulary — plus a particle-style escape
10
+ // hatch. Extending to other stems = adding rows to PARADIGMS.
11
+
12
+ // The six kārakas + two non-kāraka relations we also accept (sambandha =
13
+ // genitive "of", which is structurally useful for property access).
14
+ export const KARAKA = {
15
+ KARTR: 'kartr', // agent — nominative (प्रथमा)
16
+ KARMAN: 'karman', // patient — accusative (द्वितीया)
17
+ KARANA: 'karana', // instrument — instrumental (तृतीया)
18
+ SAMPRADANA: 'sampradana', // recipient — dative (चतुर्थी)
19
+ APADANA: 'apadana', // source — ablative (पञ्चमी)
20
+ SAMBANDHA: 'sambandha', // relation — genitive (षष्ठी)
21
+ ADHIKARANA: 'adhikarana', // locus — locative (सप्तमी)
22
+ };
23
+
24
+ // a-stem singular endings (after the stem, which ends in the inherent 'अ').
25
+ // We express each ending as the sequence that REPLACES the stem-final 'अ'.
26
+ // Written in Devanagari as the surface suffix on a consonant-final stem.
27
+ // Example stem: रक्त (rakta) → रक्तः (nom), रक्तम् (acc), रक्तेन (instr)…
28
+ //
29
+ // We store endings as the trailing string of the FULL word so the matcher
30
+ // can do longest-suffix matching. Order longest-first.
31
+ const A_STEM_SINGULAR = [
32
+ // [ surface ending, case, kāraka, stemEndsWith ]
33
+ // The stem is recovered by removing `ending` and restoring final 'अ'
34
+ // where the paradigm fuses it.
35
+ { end: 'स्मिन्', case: 'locative', karaka: KARAKA.ADHIKARANA, restore: 'अ' }, // rare pronominal; keep before others
36
+ { end: 'ेषु', case: 'locative_pl', karaka: KARAKA.ADHIKARANA, restore: 'अ' },
37
+ { end: 'ेन', case: 'instrumental', karaka: KARAKA.KARANA, restore: 'अ' }, // रक्तेन
38
+ { end: 'ाय', case: 'dative', karaka: KARAKA.SAMPRADANA, restore: 'अ' }, // रक्ताय
39
+ { end: 'ात्', case: 'ablative', karaka: KARAKA.APADANA, restore: 'अ' }, // रक्तात्
40
+ { end: 'स्य', case: 'genitive', karaka: KARAKA.SAMBANDHA, restore: 'अ' }, // रक्तस्य
41
+ { end: 'े', case: 'locative', karaka: KARAKA.ADHIKARANA, restore: 'अ' }, // रक्ते
42
+ { end: 'म्', case: 'accusative', karaka: KARAKA.KARMAN, restore: 'अ' }, // रक्तम्
43
+ { end: 'ः', case: 'nominative', karaka: KARAKA.KARTR, restore: 'अ' }, // रक्तः
44
+ ];
45
+
46
+ // Virama / halant and the inherent vowel handling.
47
+ const VIRAMA = '\u094D';
48
+ const INHERENT = 'अ';
49
+
50
+ // Some words are written with an explicit final consonant+virama in the
51
+ // stem (e.g. अङ्गम् the keyword). For nominal arguments we expect the
52
+ // surface forms above.
53
+
54
+ // Detect: returns { stem, case, karaka } or null if no case ending found.
55
+ export function analyze(word) {
56
+ for (const row of A_STEM_SINGULAR) {
57
+ if (word.endsWith(row.end)) {
58
+ let stem = word.slice(0, word.length - row.end.length);
59
+ // The a-stem fuses the stem-final inherent 'अ' with the ending.
60
+ // After stripping the ending, `stem` ends in a bare consonant
61
+ // (no virama shown because the inherent vowel was consumed by the
62
+ // ending). We restore the citation form by appending nothing —
63
+ // the consonant already implies 'अ' in Devanagari. So `stem` IS
64
+ // the dictionary stem in surface form.
65
+ if (stem.length === 0) continue;
66
+ return { stem, case: row.case, karaka: row.karaka, ending: row.end };
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+
72
+ // Map a kāraka role-name (used by codegen) from a detected analysis.
73
+ export function karakaOf(word) {
74
+ const a = analyze(word);
75
+ return a ? a.karaka : null;
76
+ }
77
+
78
+ // For diagnostics / REPL: pretty Sanskrit name of a kāraka.
79
+ export const KARAKA_NAME_SA = {
80
+ [KARAKA.KARTR]: 'कर्तृ',
81
+ [KARAKA.KARMAN]: 'कर्म',
82
+ [KARAKA.KARANA]: 'करण',
83
+ [KARAKA.SAMPRADANA]: 'सम्प्रदान',
84
+ [KARAKA.APADANA]: 'अपादान',
85
+ [KARAKA.SAMBANDHA]: 'सम्बन्ध',
86
+ [KARAKA.ADHIKARANA]: 'अधिकरण',
87
+ };