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

482 lines
18 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);
case 'rust': return curataRust(text);
case 'swift': return curataSwift(text);
case 'kotlin': return curataKotlin(text);
default: return { code: text, siruri: [] };
}
}
// Sfarsitul unui comentariu bloc IMBRICAT (/* a /* b */ c */) care incepe la i; n daca nu se inchide. Comun pentru Rust, Swift si
// Kotlin: toate trei imbrica; un lexer C ar fi inchis comentariul la primul */ si ar fi lasat restul drept cod.
function sfarsitComentariuImbricat(text, i, n) {
let j = i + 2;
let ad = 1;
while (j < n && ad > 0) {
if (text[j] === '/' && text[j + 1] === '*') { ad++; j += 2; continue; }
if (text[j] === '*' && text[j + 1] === '/') { ad--; j += 2; continue; }
j++;
}
return j;
}
// Swift (0.4.0, 2026-10-18): comentariile bloc se imbrica; sirurile sunt "..." (un singur rand), """...""" (multi-rand), si formele
// brute #"..."# / #"""..."""# cu oricate diezuri; INTERPOLAREA \( expr ) (in sirurile brute \#( expr )) poate contine la randul ei
// siruri si paranteze, deci sfarsitul sirului se cauta cu o stiva de cadre, nu cu "pana la urmatorul ghilimea". Nu exista literale
// char. Scanarea e ITERATIVA (stiva explicita): un fisier facut anume din `"\(` repetat ar fi 85.000 de niveluri de recursie.
// Continutul sirului (interpolare inclusa) ramane un singur interval: un nume de API scris in interpolare nu e o folosire masurata.
function sfarsitSirSwift(text, start, n) {
// cadre: { fel: 'sir', diezi, multi } sau { fel: 'interp', ad } (ad = paranteze deschise in interpolare)
const stiva = [];
const deschide = (j) => {
let d = 0;
while (text[j] === '#') { d++; j++; }
if (text[j] !== '"') return -1;
const multi = text.startsWith('"""', j);
stiva.push({ fel: 'sir', diezi: d, multi });
return j + (multi ? 3 : 1);
};
let j = deschide(start);
if (j < 0) return -1;
while (j < n && stiva.length) {
const cadru = stiva[stiva.length - 1];
const c = text[j];
if (cadru.fel === 'sir') {
if (c === '\\') {
// escape-ul unui sir brut e \ urmat de exact `diezi` diezi; un \ singur in sirul brut e un caracter obisnuit
let k = j + 1;
let d = 0;
while (d < cadru.diezi && text[k] === '#') { d++; k++; }
if (d < cadru.diezi) { j++; continue; }
if (text[k] === '(') { stiva.push({ fel: 'interp', ad: 1 }); j = k + 1; continue; }
j = k + 1; continue;
}
if (c === '"') {
const inchidere = (cadru.multi ? '"""' : '"') + '#'.repeat(cadru.diezi);
if (text.startsWith(inchidere, j)) { stiva.pop(); j += inchidere.length; continue; }
j++; continue;
}
if (c === '\n' && !cadru.multi) { stiva.pop(); j++; continue; } // sir neterminat pe rand: se inchide la capatul randului
j++; continue;
}
// in interpolare: paranteze echilibrate, siruri imbricate
if (c === '(') { cadru.ad++; j++; continue; }
if (c === ')') { if (--cadru.ad === 0) stiva.pop(); j++; continue; }
if (c === '"' || (c === '#' && /^#+"/.test(text.slice(j, j + 8)))) {
const k = deschide(j);
if (k >= 0) { j = k; continue; }
}
j++;
}
return Math.min(j, n);
}
function curataSwift(text) {
const n = text.length;
const out = text.split('');
const goleste = golitor(out);
const siruri = [];
let i = 0;
while (i < n) {
const c = text[i];
const d = text[i + 1];
if (c === '/' && d === c) { // (scris altfel decat in lexerul C si in cel Rust: controlul negativ isi ancoreaza cioturile pe acelea)
let j = i;
while (j < n && text[j] !== '\n') j++;
goleste(i, j); i = j; continue;
}
if (c === '/' && d === '*') {
const jSw = sfarsitComentariuImbricat(text, i, n);
goleste(i, jSw); i = jSw; continue;
}
if (c === '"' || (c === '#' && (d === '#' || d === '"'))) {
const sf = sfarsitSirSwift(text, i, n);
if (sf > i) { siruri.push([i, sf]); i = sf; continue; }
}
if (/[A-Za-z_]/.test(c)) {
let j = i + 1;
while (j < n && /\w/.test(text[j])) j++;
i = j; continue;
}
i++;
}
return { code: out.join(''), siruri };
}
// Kotlin (0.4.0, 2026-10-18): comentariile bloc se imbrica; sirurile sunt "..." (un singur rand, cu escape-uri) si """...""" (brute,
// fara escape-uri); amandoua au sabloane $nume si ${ expr }, iar expr poate contine siruri si acolade, deci sfarsitul se cauta cu o
// stiva de cadre, iterativ. 'c' e un literal char ('a', '\n', 'A', '\''), nu deschide un sir. Continutul sirului ramane un singur
// interval: un nume de API scris intr-un sablon nu e o folosire masurata.
function sfarsitSirKotlin(text, start, n) {
const stiva = []; // { fel: 'sir', brut } | { fel: 'sablon', ad }
const deschide = (j) => {
if (text[j] !== '"') return -1;
const brut = text.startsWith('"""', j);
stiva.push({ fel: 'sir', brut });
return j + (brut ? 3 : 1);
};
let j = deschide(start);
if (j < 0) return -1;
while (j < n && stiva.length) {
const cadru = stiva[stiva.length - 1];
const c = text[j];
if (cadru.fel === 'sir') {
if (c === '\\' && !cadru.brut) { j += 2; continue; }
if (c === '$' && text[j + 1] === '{') { stiva.push({ fel: 'sablon', ad: 1 }); j += 2; continue; }
if (c === '"') {
if (cadru.brut) {
if (text.startsWith('"""', j)) { j += 3; while (text[j] === '"') j++; stiva.pop(); continue; } // """"" inchide si pastreaza ghilimelele in plus
j++; continue;
}
stiva.pop(); j++; continue;
}
if (c === '\n' && !cadru.brut) { stiva.pop(); j++; continue; }
j++; continue;
}
if (c === '{') { cadru.ad++; j++; continue; }
if (c === '}') { if (--cadru.ad === 0) stiva.pop(); j++; continue; }
if (c === '"') { const k = deschide(j); if (k >= 0) { j = k; continue; } }
if (c === "'") { const k = sfarsitCharKotlin(text, j, n); if (k > 0) { j = k; continue; } }
j++;
}
return Math.min(j, n);
}
// literal char Kotlin la i: 'x', '\n', '\'', 'A', un caracter astral; -1 daca la i nu incepe un literal char
function sfarsitCharKotlin(text, i, n) {
const d = text[i + 1];
if (d === undefined || d === '\n' || d === "'") return -1;
if (d === '\\') {
let j = i + 2;
while (j < n && text[j] !== "'" && text[j] !== '\n' && j - i < 8) j++;
return text[j] === "'" ? j + 1 : -1;
}
if (text[i + 2] === "'") return i + 3;
const cod = d.charCodeAt(0);
if (cod >= 0xd800 && cod <= 0xdbff && text[i + 3] === "'") return i + 4;
return -1;
}
function curataKotlin(text) {
const n = text.length;
const out = text.split('');
const goleste = golitor(out);
const siruri = [];
let i = 0;
while (i < n) {
const c = text[i];
const d = text[i + 1];
if (c === d && d === '/') { // (scris altfel decat in lexerele C, Rust si Swift: fiecare ciot al controlului negativ are ancora lui)
let j = i;
while (j < n && text[j] !== '\n') j++;
goleste(i, j); i = j; continue;
}
if (c === '/' && d === '*') {
const jKt = sfarsitComentariuImbricat(text, i, n);
goleste(i, jKt); i = jKt; continue;
}
if (c === '"') {
const sf = sfarsitSirKotlin(text, i, n);
if (sf > i) { siruri.push([i, sf]); i = sf; continue; }
}
if (c === "'") {
const sf = sfarsitCharKotlin(text, i, n);
if (sf > 0) { siruri.push([i, sf]); i = sf; continue; }
i++; continue;
}
if (/[A-Za-z_]/.test(c)) {
let j = i + 1;
while (j < n && /\w/.test(text[j])) j++;
i = j; continue;
}
i++;
}
return { code: out.join(''), 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 };
}
// Rust (0.3.0): comentariile bloc se IMBRICA; sirurile pot trece peste randuri si au forme brute (r"...", r#"..."#, br"...",
// cr#"..."#) si de octeti (b"..."); apostroful NU deschide un sir decat ca literal char ('a', '\n', '\u{1F600}', b'x'): altfel e
// o durata de viata ('a, 'static) sau o eticheta ('outer:), si un lexer C ar fi deschis acolo un "sir" pana la urmatorul apostrof.
function curataRust(text) {
const n = text.length;
const out = text.split('');
const goleste = golitor(out);
const siruri = [];
let i = 0;
while (i < n) {
const c = text[i];
const d = text[i + 1];
if (d === '/' && c === '/') { // (scris invers fata de lexerul C: controlul negativ isi ancoreaza ciotul pe acela)
let j = i;
while (j < n && text[j] !== '\n') j++;
goleste(i, j); i = j; continue;
}
if (c === '/' && d === '*') {
const j = sfarsitComentariuImbricat(text, i, n); // (0.4.0: aceeasi functie pentru Rust, Swift si Kotlin)
goleste(i, j); i = j; continue;
}
const dupaCuvant = i > 0 && /\w/.test(text[i - 1]);
if (!dupaCuvant && (c === 'r' || ((c === 'b' || c === 'c') && d === 'r'))) {
let j = c === 'r' ? i + 1 : i + 2;
let diezi = 0;
while (text[j] === '#') { diezi++; j++; }
if (text[j] === '"') {
const inchidere = '"' + '#'.repeat(diezi);
const k = text.indexOf(inchidere, j + 1);
const sf = k < 0 ? n : k + inchidere.length;
siruri.push([i, sf]); i = sf; continue;
}
}
if (c === '"' || (!dupaCuvant && (c === 'b' || c === 'c') && d === '"')) {
const start = i;
let j = c === '"' ? i + 1 : i + 2;
while (j < n && text[j] !== '"') { if (text[j] === '\\') j++; j++; }
j = Math.min(n, j + 1);
siruri.push([start, j]); i = j; continue;
}
if (c === "'") {
if (d === '\\') {
let j = i + 2;
while (j < n && text[j] !== "'" && text[j] !== '\n' && j - i < 16) j++;
if (text[j] === "'") { siruri.push([i, j + 1]); i = j + 1; continue; }
i++; continue;
}
if (d !== undefined && d !== '\n' && text[i + 2] === "'") { siruri.push([i, i + 3]); i += 3; continue; }
if (d !== undefined && d.charCodeAt(0) >= 0xd800 && d.charCodeAt(0) <= 0xdbff && text[i + 3] === "'") { siruri.push([i, i + 4]); i += 4; continue; }
i++; continue;
}
if (/[A-Za-z_]/.test(c)) {
let j = i + 1;
while (j < n && /\w/.test(text[j])) j++;
i = j; continue;
}
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 };
}