@wllama/wllama
Version:
WebAssembly binding for llama.cpp - Enabling on-browser LLM inference
270 lines (229 loc) • 9.29 kB
JavaScript
// Usage: node scripts/build_source_map.js [--input name:buildDir] [--output out.ts]
// All flags are optional — defaults point to build/ and src/wasm/source-map.ts.
// Reads *.js.symbols from each build dir and produces cleaned function names.
import { readFileSync, writeFileSync, readdirSync } from 'fs';
import { resolve, join } from 'path';
import { gzipSync } from 'zlib';
const DEFAULT_INPUTS = [
{ name: 'default', symbolsPath: 'build' },
{ name: 'compat', symbolsPath: 'build-compat' },
];
const DEFAULT_OUTPUT = 'src/wasm/source-map.ts';
const args = process.argv.slice(2);
const inputs = [];
let outputFile = null;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--input') {
const next = args[++i];
if (!next) { console.error('--input must be name:buildDir'); process.exit(1); }
const [name, symbolsPath] = next.split(':');
if (!name || !symbolsPath) { console.error('--input must be name:buildDir'); process.exit(1); }
inputs.push({ name, symbolsPath: resolve(symbolsPath) });
} else if (args[i] === '--output') {
const next = args[++i];
if (!next) { console.error('--output requires a path'); process.exit(1); }
outputFile = resolve(next);
}
}
if (!inputs.length) {
for (const { name, symbolsPath } of DEFAULT_INPUTS) {
const resolved = resolve(symbolsPath);
try { readdirSync(resolved); inputs.push({ name, symbolsPath: resolved }); } catch { /* skip */ }
}
}
if (!outputFile) outputFile = resolve(DEFAULT_OUTPUT);
if (!inputs.length) { console.error('No build directories found.'); process.exit(1); }
// -- wasm binary: extract [firstFuncId, funcCount] ----------------------------
function readUleb(buf, pos) {
let result = 0, shift = 0;
while (true) {
const b = buf[pos++];
result |= (b & 0x7F) << shift;
if (!(b & 0x80)) break;
shift += 7;
}
return [result, pos];
}
function skipLimits(buf, pos) {
const kind = buf[pos++];
[, pos] = readUleb(buf, pos);
if (kind === 0x01 || kind === 0x03 || kind === 0x05 || kind === 0x07)
[, pos] = readUleb(buf, pos);
return pos;
}
function parseFuncIds(wasmBuf) {
let pos = 8, importCount = 0;
while (pos < wasmBuf.length) {
const sectionId = wasmBuf[pos++];
let sectionSize;
[sectionSize, pos] = readUleb(wasmBuf, pos);
const sectionEnd = pos + sectionSize;
if (sectionId === 2) {
let count; [count, pos] = readUleb(wasmBuf, pos);
for (let i = 0; i < count; i++) {
let nl;
[nl, pos] = readUleb(wasmBuf, pos); pos += nl;
[nl, pos] = readUleb(wasmBuf, pos); pos += nl;
const kind = wasmBuf[pos++];
if (kind === 0) { [, pos] = readUleb(wasmBuf, pos); importCount++; }
else if (kind === 1) { pos++; pos = skipLimits(wasmBuf, pos); }
else if (kind === 2) { pos = skipLimits(wasmBuf, pos); }
else if (kind === 3) { pos += 2; }
}
} else if (sectionId === 10) {
let funcCount; [funcCount, pos] = readUleb(wasmBuf, pos);
return [importCount, funcCount];
} else {
pos = sectionEnd;
}
}
throw new Error('No code section found');
}
// -- name simplification ------------------------------------------------------
function truncateTemplates(name, maxLen) {
let result = '', i = 0;
while (i < name.length) {
if (name[i] !== '<') { result += name[i++]; continue; }
// find matching >
let depth = 1, j = i + 1;
while (j < name.length && depth > 0) {
if (name[j] === '<') depth++;
else if (name[j] === '>') depth--;
j++;
}
const content = name.slice(i + 1, j - 1);
result += content.length > maxLen
? '<' + content.slice(0, maxLen) + '...>'
: '<' + content + '>';
i = j;
}
return result;
}
function stripParams(name) {
// Find first top-level '(' (not inside <>) and strip from there.
// If params are empty "()", keep them; otherwise drop entirely.
let depth = 0;
for (let i = 0; i < name.length; i++) {
const c = name[i];
if (c === '<') { depth++; continue; }
if (c === '>') { depth--; continue; }
if (c === '(' && depth === 0) {
const base = name.slice(0, i);
return (name[i + 1] === ')') ? base + '()' : base;
}
}
return name;
}
const STD_HINT = 'std::...';
function simplifyName(raw) {
if (!raw) return null;
let name = raw;
// Rule 0: collapse all std:: into a hint
if (/^std::/.test(name)) return STD_HINT;
// Rule 1: lambda / closure types
// Handles ::$_N, ::'lambda'(), 'lambda'()
// Strategy: find the marker, take everything inside the nearest enclosing <..> before it
const lambdaRe = /::[$']_?\d*|::'lambda'/;
const lambdaMatch = lambdaRe.exec(name);
if (lambdaMatch) {
const before = name.slice(0, lambdaMatch.index);
// Extract the innermost meaningful context: look for the last '<' before the marker
const lastAngle = before.lastIndexOf('<');
let parent = lastAngle >= 0 ? before.slice(lastAngle + 1) : before;
// Strip trailing qualifiers
parent = parent.replace(/\s+(const|volatile|noexcept|&&?)\s*$/, '').trim();
name = parent;
// fall through to further cleanup below
}
// Rule 2: strip parameter list
name = stripParams(name);
// Rule 3: remove libc++ internal sub-namespaces (::__2::, ::__1::, etc.)
name = name.replace(/::__\d+::/g, '::');
// Rule 4: remove ABI tags
name = name.replace(/\[abi:[^\]]+\]/g, '');
// Rule 5: truncate template args to 10 chars
name = truncateTemplates(name, 10);
// Rule 6: final cleanup
name = name.replace(/::::/g, '::').replace(/\s+/g, ' ').trim();
return name || null;
}
// -- binary encoder -----------------------------------------------------------
function encodeNames(funcCount, firstId, symbols) {
// Build deduplicated name table
const nameToIdx = new Map(); // string -> u16 index
const nameTable = []; // array of Buffer
const indices = new Uint16Array(funcCount); // 0xFFFF = unknown
indices.fill(0xFFFF);
let mapped = 0;
for (let i = 0; i < funcCount; i++) {
const raw = symbols.get(firstId + i) ?? null;
const cleaned = raw ? simplifyName(raw) : null;
if (!cleaned) continue;
let idx = nameToIdx.get(cleaned);
if (idx === undefined) {
idx = nameTable.length;
const b = Buffer.from(cleaned.slice(0, 254));
nameTable.push(Buffer.concat([Buffer.from([b.length]), b]));
nameToIdx.set(cleaned, idx);
}
indices[i] = idx;
mapped++;
}
const numNames = nameTable.length;
process.stderr.write(` ${mapped}/${funcCount} named, ${numNames} unique names\n`);
// u32 numNames + name table + u16 index array
const header = Buffer.alloc(4);
header.writeUInt32LE(numNames, 0);
const indexBuf = Buffer.from(indices.buffer);
return Buffer.concat([header, ...nameTable, indexBuf]);
}
// -- resolve symbolsPath (dir or file) ----------------------------------------
function resolveSymbolsFile(symbolsPath) {
if (symbolsPath.endsWith('.js.symbols')) return symbolsPath;
for (const entry of readdirSync(symbolsPath))
if (entry.endsWith('.js.symbols')) return join(symbolsPath, entry);
throw new Error(`No .js.symbols file in ${symbolsPath}`);
}
// -- per-build processing -----------------------------------------------------
function processBuild(symbolsPath) {
const resolvedSymbols = resolveSymbolsFile(symbolsPath);
const wasmPath = resolvedSymbols.replace(/\.js\.symbols$/, '.wasm');
process.stderr.write(` Parsing wasm binary...\n`);
const [firstId, funcCount] = parseFuncIds(readFileSync(wasmPath));
process.stderr.write(` ${funcCount} functions starting at index ${firstId}\n`);
process.stderr.write(` Loading symbols...\n`);
const symbols = new Map();
for (const line of readFileSync(resolvedSymbols, 'utf8').split('\n')) {
const colon = line.indexOf(':');
if (colon < 0) continue;
const id = parseInt(line.slice(0, colon));
if (!isNaN(id)) symbols.set(id, line.slice(colon + 1).trim());
}
process.stderr.write(` ${symbols.size} raw symbols\n`);
const header = Buffer.alloc(8);
header.writeUInt32LE(firstId, 0);
header.writeUInt32LE(funcCount, 4);
const nameData = encodeNames(funcCount, firstId, symbols);
const binary = Buffer.concat([header, nameData]);
const compressed = gzipSync(binary);
process.stderr.write(` ${binary.length.toLocaleString()} bytes -> ${compressed.length.toLocaleString()} bytes gzipped\n`);
return compressed.toString('base64');
}
// -- main ---------------------------------------------------------------------
const entries_ts = [];
for (const { name, symbolsPath } of inputs) {
process.stderr.write(`\n[${name}] ${symbolsPath}\n`);
entries_ts.push(` "${name}": "${processBuild(symbolsPath)}"`);
}
const tsContent = [
`// Auto-generated by scripts/build_source_map.js — do not edit`,
`// Format: gzip-compressed binary name table, base64-encoded`,
`// Structure: u32 firstId, u32 funcCount, u32 numNames, then name table (u8 len + bytes each), then u16 index array (0xFFFF = unknown)`,
`export const WASM_SOURCE_MAP: Record<string, string> = {`,
entries_ts.join(',\n'),
`};`,
``,
].join('\n');
writeFileSync(outputFile, tsContent);
process.stderr.write(`\nWrote ${outputFile}\n`);