Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions scripts/analyze-diagnostics.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env node
// Analyze type-trace JSONL produced by --diag-trace=type-trace.
// Usage: node scripts/analyze-diagnostics.cjs [path]
const fs = require("fs");
const path = process.argv[2] || "chad-diagnostics.jsonl";

const lines = fs.readFileSync(path, "utf8").split("\n").filter(Boolean);
console.error(`Loaded ${lines.length} events from ${path}`);
const events = lines.map((l) => JSON.parse(l));

function pickRealSite(sites) {
if (!Array.isArray(sites)) return sites || "?";
const skip = [
/codegen\/infrastructure\/base-generator\.js/,
/codegen\/infrastructure\/generator-context\.js/,
/codegen\/infrastructure\/type-inference\.js/,
/codegen\/infrastructure\/ir-builders\.js/,
/diagnostics\/tracers\.js/,
];
for (const s of sites) {
if (skip.some((r) => r.test(s))) continue;
return s;
}
return sites[0] || "?";
}
for (const e of events) e.site = pickRealSite(e.sites);

const typeTrace = events.filter((e) => e.cat === "type-trace");
const sets = typeTrace.filter((e) => e.k === "set");
const gets = typeTrace.filter((e) => e.k === "get");
const riches = typeTrace.filter((e) => e.k === "rich");

function countBy(arr, keyFn) {
const m = new Map();
for (const x of arr) {
const k = keyFn(x);
m.set(k, (m.get(k) || 0) + 1);
}
return m;
}

const setSiteCount = countBy(sets, (s) => s.site);
const getSiteCount = countBy(gets, (g) => g.site);
const richSiteCount = countBy(riches, (r) => r.site);
const richSiteNull = countBy(
riches.filter((r) => r.result === null),
(r) => r.site,
);

// Orphan analysis: sets whose name is not read before being re-set.
const lastSetIdByName = new Map();
const setReadCount = new Map();
const orphanGets = [];
for (const e of typeTrace) {
if (e.k === "set") {
lastSetIdByName.set(e.name, e.i);
setReadCount.set(e.i, 0);
} else if (e.k === "get") {
const setId = lastSetIdByName.get(e.name);
if (setId !== undefined) {
setReadCount.set(setId, (setReadCount.get(setId) || 0) + 1);
} else if (e.result !== null) {
orphanGets.push(e);
}
}
}
const orphanSets = sets.filter((s) => (setReadCount.get(s.i) || 0) === 0);
const orphanSetSite = countBy(orphanSets, (s) => s.site);
const orphanGetSite = countBy(orphanGets, (g) => g.site);

// Per-site orphan ratio (for sets that happen N times at site X, how many are orphans?)
const orphanRatioBySite = [];
for (const [site, count] of setSiteCount.entries()) {
if (count < 50) continue;
const orphans = orphanSetSite.get(site) || 0;
orphanRatioBySite.push({ site, count, orphans, ratio: orphans / count });
}
orphanRatioBySite.sort((a, b) => b.ratio - a.ratio);

const fmt = (m, n = 30) =>
[...m.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, n)
.map(([k, v]) => ` ${String(v).padStart(7)} ${k}`)
.join("\n");

const pct = (p, w) => (w ? ((100 * p) / w).toFixed(1) : "0.0") + "%";

console.log(`# Diagnostics type-trace analysis

Total type-trace events: ${typeTrace.length}
setVariableType: ${sets.length}
getVariableType: ${gets.length} (${gets.filter((g) => g.result !== null).length} hits, ${gets.filter((g) => g.result === null).length} misses)
resolveRich: ${riches.length} (${riches.filter((r) => r.result === null).length} null results)

## Top setVariableType call sites
${fmt(setSiteCount)}

## Top getVariableType call sites (consumers)
${fmt(getSiteCount)}

## Top resolveExpressionTypeRich call sites (consumers)
${fmt(richSiteCount)}

## Orphan SETs (no subsequent read before next set of same name) — DROP CANDIDATES
Total orphan sets: ${orphanSets.length} (${pct(orphanSets.length, sets.length)} of all sets)

${fmt(orphanSetSite)}

## Orphan-set ratio per site (set-count >= 50)
${orphanRatioBySite
.slice(0, 30)
.map(
(r) =>
` ${String(r.orphans).padStart(7)}/${String(r.count).padStart(7)} (${pct(r.orphans, r.count)}) ${r.site}`,
)
.join("\n")}

## Orphan GETs (read returns non-null but never set in trace — comes from symbolTable)
Total: ${orphanGets.length}

${fmt(orphanGetSite)}

## resolveRich sites with highest null-result rate (count >= 10)
${[...richSiteCount.entries()]
.filter(([, c]) => c >= 10)
.map(([site, c]) => [site, c, richSiteNull.get(site) || 0])
.sort((a, b) => b[2] / b[1] - a[2] / a[1])
.slice(0, 20)
.map(([site, c, n]) => ` ${n}/${c} (${pct(n, c)}) ${site}`)
.join("\n")}
`);
14 changes: 14 additions & 0 deletions src/chad-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ parser.addScopedOption(
"",
"build,run",
);
parser.addScopedOption(
"diag-trace",
"",
"Enable diagnostic trace categories (csv), e.g. 'type-trace'",
"",
"build,run,ir",
);
parser.addScopedOption(
"diag-trace-out",
"",
"Output path for diagnostic trace JSONL (default: chad-diagnostics.jsonl)",
"",
"build,run,ir",
);
parser.addPositional("input", "Input .ts or .js file");

parser.parse(process.argv);
Expand Down
33 changes: 33 additions & 0 deletions src/chad-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import * as fs from "fs";
import { execSync, spawn as spawnProc, ChildProcess } from "child_process";
import { installTargetSDK, listInstalledSDKs, getSDKBaseDir } from "./cross-compile.js";
import { VERSION } from "./version.js";
import { enableSink, flushDiagnostics } from "./diagnostics/sink.js";
import { parseCategories, CAT_TYPE_TRACE } from "./diagnostics/categories.js";
import { enableTypeTrace } from "./diagnostics/tracers.js";

const parser = new ArgumentParser("chad", "compile TypeScript to native binaries via LLVM");
parser.setColorEnabled(process.stdout.isTTY === true);
Expand Down Expand Up @@ -96,6 +99,20 @@ parser.addScopedOption(
"",
"build,run",
);
parser.addScopedOption(
"diag-trace",
"",
"Enable diagnostic trace categories (csv), e.g. 'type-trace'",
"",
"build,run,ir",
);
parser.addScopedOption(
"diag-trace-out",
"",
"Output path for diagnostic trace JSONL (default: chad-diagnostics.jsonl)",
"",
"build,run,ir",
);
parser.addPositional("input", "Input .ts or .js file");

// Node's process.argv includes [node, script, ...] — skip both.
Expand Down Expand Up @@ -302,6 +319,19 @@ if (parser.getFlag("verbose")) logLevel = LogLevel_Verbose;
if (parser.getFlag("debug")) logLevel = LogLevel_Debug;
if (parser.getFlag("trace")) logLevel = LogLevel_Trace;

const diagTraceCsv = parser.getOption("diag-trace");
if (diagTraceCsv) {
const cats = parseCategories(diagTraceCsv);
const outPath = parser.getOption("diag-trace-out") || "chad-diagnostics.jsonl";
enableSink(outPath);
process.on("exit", () => {
flushDiagnostics();
});
for (const c of cats) {
if (c === CAT_TYPE_TRACE) enableTypeTrace();
}
}

if (parser.getFlag("skip-semantic-analysis")) setSkipSemanticAnalysis(true);
if (parser.getFlag("keep-temps")) setKeepTemps(true);
const diagFormat = parser.getOption("diagnostics");
Expand Down Expand Up @@ -396,10 +426,13 @@ if (diagFormat === "json") {
compile(inputFile, outputFile, logLevel);
} catch (error) {
logger.error((error as Error).message);
flushDiagnostics();
process.exit(1);
}
}

flushDiagnostics();

if (command === "run") {
const bin = path.resolve(outputFile);
if (!fs.existsSync(bin)) {
Expand Down
16 changes: 13 additions & 3 deletions src/codegen/infrastructure/base-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
SymbolKind_UrlSearchParams,
} from "./symbol-table.js";
import type { ResolvedType } from "./type-system.js";
import { traceTypeSet, traceTypeGet } from "../../diagnostics/tracers.js";

export {
SymbolTable,
Expand Down Expand Up @@ -589,13 +590,21 @@ export class BaseGenerator {
* Checks SymbolTable for named variables, then variableTypes for temporary registers
*/
getVariableType(name: string): string | undefined {
if (!name) return undefined;
if (!name) {
traceTypeGet(name, undefined);
return undefined;
}
// Check named variables in SymbolTable first
const symbolType = this.symbolTable.getType(name);
if (symbolType) return symbolType;
if (symbolType) {
traceTypeGet(name, symbolType);
return symbolType;
}

// Fall back to temporary register types
return this.variableTypes.get(name);
const t = this.variableTypes.get(name);
traceTypeGet(name, t);
return t;
}

/**
Expand All @@ -614,6 +623,7 @@ export class BaseGenerator {
`Cannot set type 'unknown' for register '${name}'. Type inference failed in the codegen pipeline.`,
);
}
traceTypeSet(name, type);
this.variableTypes.set(name, type);
}

Expand Down
11 changes: 9 additions & 2 deletions src/codegen/infrastructure/generator-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import type { JsonObjectMeta } from "../expressions/access/member.js";
import type { DiagnosticEngine } from "../../diagnostics/engine.js";
import { TypeContext } from "./type-context.js";
import { classifyTerminator } from "./terminator-classifier.js";
import { traceTypeSet, traceTypeGet } from "../../diagnostics/tracers.js";

interface ExprBase {
type: string;
Expand Down Expand Up @@ -1624,17 +1625,23 @@ export class MockGeneratorContext implements IGeneratorContext {
getVariableType(name: string): string | undefined {
// Check named variables in SymbolTable first
const symbolType = this.symbolTable.getType(name);
if (symbolType) return symbolType;
if (symbolType) {
traceTypeGet(name, symbolType);
return symbolType;
}

// Fall back to temporary register types
return this.variableTypes.get(name);
const t = this.variableTypes.get(name);
traceTypeGet(name, t);
return t;
}

hasVariableType(name: string): boolean {
return this.getVariableType(name) !== undefined;
}

setVariableType(name: string, type: string): void {
traceTypeSet(name, type);
this.variableTypes.set(name, type);
}

Expand Down
20 changes: 17 additions & 3 deletions src/codegen/infrastructure/type-inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type {
ArrayStorageStrategy,
} from "./type-system.js";
import type { TypeContext } from "./type-context.js";
import { traceTypeRich } from "../../diagnostics/tracers.js";

interface ExprBase {
type: string;
Expand Down Expand Up @@ -155,22 +156,35 @@ export class TypeInference {
// etc.) never gain stray enrichment. Fields are eager in P1a; lazy-getter
// optimization deferred to P1b — callers today only invoke this on the hot path.
resolveExpressionTypeRich(expr: Expression): ResolvedType | null {
const exprTypeTag: string =
expr && typeof expr === "object" ? (expr as ExprBase).type || "" : "";
if (expr && typeof expr === "object" && this.isCacheableExprType((expr as ExprBase).type)) {
const cached = this.richCacheLookup(expr);
if (cached) return cached;
if (cached) {
traceTypeRich(exprTypeTag, cached.base || null);
return cached;
}
const baseType = this.resolveExpressionType(expr);
if (!baseType) return null;
if (!baseType) {
traceTypeRich(exprTypeTag, null);
return null;
}
const enriched = this.enrichResolvedType(baseType);
this.populateArrayStorage(enriched, expr);
if (enriched.base && enriched.sourceKind && enriched.sourceKind !== "unknown") {
this.richCacheStore(expr, enriched);
}
traceTypeRich(exprTypeTag, enriched.base || null);
return enriched;
}
const baseType = this.resolveExpressionType(expr);
if (!baseType) return null;
if (!baseType) {
traceTypeRich(exprTypeTag, null);
return null;
}
const enriched = this.enrichResolvedType(baseType);
this.populateArrayStorage(enriched, expr);
traceTypeRich(exprTypeTag, enriched.base || null);
return enriched;
}

Expand Down
26 changes: 26 additions & 0 deletions src/diagnostics/categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const CAT_TYPE_TRACE = "type-trace";

export const KNOWN_CATEGORIES: string[] = [CAT_TYPE_TRACE];

export function parseCategories(csv: string): string[] {
const result: string[] = [];
if (!csv) return result;
const parts = csv.split(",");
for (const raw of parts) {
const name = raw.trim();
if (!name) continue;
let known = false;
for (const k of KNOWN_CATEGORIES) {
if (k === name) {
known = true;
break;
}
}
if (!known) {
console.error("warning: unknown diagnostic category " + name);
continue;
}
result.push(name);
}
return result;
}
9 changes: 9 additions & 0 deletions src/diagnostics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export { enableSink, isSinkEnabled, recordEvent, flushDiagnostics } from "./sink.js";
export { CAT_TYPE_TRACE, KNOWN_CATEGORIES, parseCategories } from "./categories.js";
export {
enableTypeTrace,
isTypeTraceEnabled,
traceTypeSet,
traceTypeGet,
traceTypeRich,
} from "./tracers.js";
Loading
Loading