aere-quantum/crypto-inventory/lib/lexer.mjs

231 lines
8.5 KiB
JavaScript

// Lexer minimal pe limbaje: comentariile devin spatii (liniile si coloanele raman pe loc),
// sirurile raman in text, dar li se tin minte intervalele, ca un detector sa poata cere
// "numele API-ului e COD, nu text dintr-un sir".
//
// Nu e un parser: e cat trebuie ca un tipar sa nu se potriveasca in comentarii, in
// docstring-uri Python sau in textul unui sir.
const KW_REGEX = new Set(['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void',
'throw', 'case', 'do', 'else', 'yield', 'await']);
const PUNCT_REGEX = '(,=:[!&|?{};+-*%<>~^';
export function curata(text, limbaj) {
switch (limbaj) {
case 'javascript': return curataC(text, { template: true, regex: true });
case 'java': return curataC(text, { textBlock: true });
case 'go': return curataC(text, { raw: true });
case 'python': return curataPython(text);
default: return { code: text, siruri: [] };
}
}
function golitor(out) {
return (a, b) => {
for (let k = a; k < b; k++) if (out[k] !== '\n' && out[k] !== '\r') out[k] = ' ';
};
}
function curataC(text, o) {
const n = text.length;
const out = text.split('');
const goleste = golitor(out);
const siruri = [];
const stiva = [];
let acolade = 0;
let prevSig = '';
let prevWord = '';
let i = 0;
let esecRegex = null; // bit 1: din (pozitie, in afara clasei) nu se ajunge la sfarsitul literalului; bit 2: la fel, in clasa
// scaneaza un segment de template literal pornind de la j; intoarce pozitia si daca s-a deschis ${
const scanTpl = (inceput, j) => {
while (j < n) {
const ch = text[j];
if (ch === '\\') { j += 2; continue; }
if (ch === '`') { siruri.push([inceput, j + 1]); return { pos: j + 1, deschis: false }; }
if (ch === '$' && text[j + 1] === '{') { siruri.push([inceput, j + 2]); return { pos: j + 2, deschis: true }; }
j++;
}
siruri.push([inceput, n]);
return { pos: n, deschis: false };
};
while (i < n) {
const c = text[i];
const d = text[i + 1];
if (c === '/' && d === '/') {
let j = i;
while (j < n && text[j] !== '\n') j++;
goleste(i, j); i = j; continue;
}
if (c === '/' && d === '*') {
let j = text.indexOf('*/', i + 2);
j = j < 0 ? n : j + 2;
goleste(i, j); i = j; continue;
}
if (o.textBlock && c === '"' && text.startsWith('"""', i)) {
let j = text.indexOf('"""', i + 3);
j = j < 0 ? n : j + 3;
siruri.push([i, j]); i = j; prevSig = '"'; continue;
}
if (c === '"' || c === "'") {
let j = i + 1;
while (j < n && text[j] !== c && text[j] !== '\n') { if (text[j] === '\\') j++; j++; }
j = Math.min(n, j + 1);
siruri.push([i, j]); i = j; prevSig = c; continue;
}
if (c === '`' && o.raw) {
let j = text.indexOf('`', i + 1);
j = j < 0 ? n : j + 1;
siruri.push([i, j]); i = j; prevSig = '`'; continue;
}
if (c === '`' && o.template) {
const r = scanTpl(i, i + 1);
i = r.pos;
if (r.deschis) { stiva.push(acolade); acolade++; }
prevSig = '`'; continue;
}
if (c === '/' && o.regex) {
const permis = prevSig === '' || PUNCT_REGEX.includes(prevSig) || (prevSig === 'a' && KW_REGEX.has(prevWord));
if (permis) {
// Cautarea sfarsitului unui literal regex e determinista din (pozitie, "in clasa"), deci o stare din care s-a ajuns o data
// la capatul randului fara sfarsit duce acolo si a doua oara. Starile acestea se tin minte (2026-09-29, revizuirea
// adversariala): fara ele un rand lung cu multe `/` nedeschise (de ex. `=/[` repetat) costa patratic, minute pe 1 MB.
let j = i + 1;
let inClasa = false;
let bun = false;
const vizitate = [];
while (j < n && text[j] !== '\n') {
if (esecRegex && esecRegex[j] & (inClasa ? 2 : 1)) break;
vizitate.push(inClasa ? -j - 1 : j);
const ch = text[j];
if (ch === '\\') { j += 2; continue; }
if (ch === '[') inClasa = true;
else if (ch === ']') inClasa = false;
else if (ch === '/' && !inClasa) { bun = true; break; }
j++;
}
if (!bun) {
if (!esecRegex) esecRegex = new Uint8Array(n);
for (const v of vizitate) { if (v < 0) esecRegex[-v - 1] |= 2; else esecRegex[v] |= 1; }
}
if (bun) {
j++;
while (j < n && /[a-z]/i.test(text[j])) j++;
siruri.push([i, j]); i = j; prevSig = '/'; continue;
}
}
prevSig = '/'; i++; continue;
}
if (c === '{') { acolade++; prevSig = '{'; i++; continue; }
if (c === '}') {
acolade--;
if (stiva.length && acolade === stiva[stiva.length - 1]) {
stiva.pop();
const r = scanTpl(i, i + 1);
i = r.pos;
if (r.deschis) { stiva.push(acolade); acolade++; }
prevSig = '`'; continue;
}
prevSig = '}'; i++; continue;
}
if (/[A-Za-z_$]/.test(c)) {
let j = i + 1;
while (j < n && /[\w$]/.test(text[j])) j++;
prevWord = text.slice(i, j); prevSig = 'a'; i = j; continue;
}
if (/[0-9]/.test(c)) {
let j = i + 1;
while (j < n && /[\w.]/.test(text[j])) j++;
prevWord = ''; prevSig = '0'; i = j; continue;
}
if (!/\s/.test(c)) prevSig = c;
i++;
}
return { code: out.join(''), siruri };
}
function curataPython(text) {
const n = text.length;
const out = text.split('');
const goleste = golitor(out);
const siruri = [];
let i = 0;
// primul caracter care nu e spatiu pe randul curent (-1 = niciunul inca). Tinut din mers (2026-09-29, revizuirea adversariala):
// forma veche cauta inapoi inceputul randului la FIECARE sir triplu, deci un rand lung cu multe siruri triple costa patratic.
let primulPeRand = -1;
while (i < n) {
const c = text[i];
if (c === '\n') { primulPeRand = -1; i++; continue; }
if (c === '#') {
let j = i;
while (j < n && text[j] !== '\n') j++;
goleste(i, j); i = j; continue;
}
if (c === '"' || c === "'") {
// prefixul (r, b, f, u, rb, ...) face parte din sir
let start = i;
while (start > 0 && /[rRbBuUfF]/.test(text[start - 1]) && i - start < 2) start--;
if (start < i && start > 0 && /[\w]/.test(text[start - 1])) start = i;
const triplu = text.startsWith(c.repeat(3), i);
let j;
// inainte de sir, pe randul lui, sunt numai spatii (prefixul r/b/f/u face parte din sir)?
const singurPeRand = primulPeRand < 0 || primulPeRand >= start;
let ultimaLinieNoua = -1;
if (triplu) {
j = i + 3;
while (j < n && !text.startsWith(c.repeat(3), j)) {
if (text[j] === '\\') { if (text[j + 1] === '\n') ultimaLinieNoua = j + 1; j++; } else if (text[j] === '\n') ultimaLinieNoua = j;
j++;
}
j = Math.min(n, j + 3);
} else {
j = i + 1;
while (j < n && text[j] !== c && text[j] !== '\n') { if (text[j] === '\\') { if (text[j + 1] === '\n') ultimaLinieNoua = j + 1; j++; } j++; }
if (j < n && text[j] === '\n') ultimaLinieNoua = j; // sir neterminat: intervalul lui se opreste DUPA acest rand nou
j = Math.min(n, j + 1);
}
// dupa sir: daca sirul a trecut peste un rand nou, randul curent incepe in el (coada lui nu e spatiu), afara de cazul in
// care sirul se termina chiar cu randul nou; altfel randul curent are acum cel putin sirul
if (ultimaLinieNoua >= 0) primulPeRand = ultimaLinieNoua === j - 1 ? -1 : ultimaLinieNoua + 1;
else if (primulPeRand < 0) primulPeRand = start;
// docstring: sirul triplu e singur pe randul lui, ca instructiune; se trateaza ca un comentariu
if (triplu && singurPeRand) { goleste(start, j); i = j; continue; }
siruri.push([start, j]); i = j; continue;
}
if (primulPeRand < 0 && c !== ' ' && c !== '\t') primulPeRand = i;
i++;
}
return { code: out.join(''), siruri };
}
// cautare binara: pozitia e inauntrul unui sir?
export function inSir(siruri, pos) {
let lo = 0;
let hi = siruri.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
const [s, e] = siruri[mid];
if (pos < s) hi = mid - 1;
else if (pos >= e) lo = mid + 1;
else return true;
}
return false;
}
export function inceputuriDeRand(text) {
const r = [0];
for (let k = 0; k < text.length; k++) if (text.charCodeAt(k) === 10) r.push(k + 1);
return r;
}
export function randul(inceputuri, pos) {
let lo = 0;
let hi = inceputuri.length - 1;
while (lo < hi) {
const mid = (lo + hi + 1) >> 1;
if (inceputuri[mid] <= pos) lo = mid; else hi = mid - 1;
}
return { line: lo + 1, column: pos - inceputuri[lo] + 1 };
}