Files
scriptc/packages/compiler/src/executable/prepare.ts
T
Chris Tate fa7bc7c7a0 Warn about likely-hot functions that use untyped values
A build now names the functions of the program's own files that compile
to the dynamic tier and likely run often, so a slow program with a little
untyped code says where the time goes:

  warning SC6101: 3 functions that likely run often use untyped values, which runs slower than typed code:
    advance (nbody.js:22): untyped JavaScript file, in a loop
    ...

- The functions come from the tier assignment lowering already makes
  (Lowerer.dynamicFunctions). Without a profile, a function counts as
  likely hot when a dynamic operation runs inside one of its loops, when
  a loop calls it, or when it calls itself (lowering/dynamic-tier.ts).
- A function is named only when its source says why its values are
  untyped: an `any` (for a for...of variable, the value it iterates), an
  untyped JSON.parse result, or an untyped JavaScript file. Generated
  %dyn.* helpers, module top levels and functions that are dynamic for an
  operation without a static lowering are left out.
- At most five functions are named, most likely hot first, with the
  total; dynamic-tier code in npm packages is counted per package on one
  line. All of the warning's text is in diagnostics/dynamic-tier.ts.
- SC6101 opens the SC61xx band, performance notes, with a new
  "performance" category in the diagnostics envelope; SC60xx stay the
  divergences.
- scriptc build prints it after the divergence warnings, and the
  executable cache stores it with the entry, so a cached build (including
  the CLI's routed fast path) repeats it. scriptc coverage reports it, and
  a comment-only edit no longer replays a verdict that carries it, since
  its text holds line numbers.
- --no-dynamic-tier-warning leaves it out of build and coverage;
  coverage --fail-on=dynamic fails on it.
- Docs: coverage.mdx (Dynamic tier, Gating builds and CI, the envelope
  categories) and cli.mdx.
2026-10-21 18:53:43 -05:00

128 lines
4.4 KiB
TypeScript

import type { CompileFailure, CompileRequestOptions } from "../compile-types.js";
import type { FfiProfile } from "../ffi/ffi-manifest.js";
import { frontendDisposed } from "../frontend/dispose-hook.js";
import {
checkerPanicDiag,
npmStaticRefusalDiag,
iceDiag,
isCheckerPanic,
type ScrDiagnostic,
} from "../diagnostics/diagnostic.js";
import type { IrModule, SrcLoc } from "../ir/ir.js";
import { validateModule } from "../ir/validate.js";
import { checkContracts } from "../contracts/check.js";
import {
fenceSpeculativeWasiFunctions,
moduleWasiUnavailableSurface,
targetRefusalDiag,
} from "../backend/target-diagnostics.js";
import type { LowerResult, LowerStats } from "../frontend/lowering/lowerer.js";
import type { FrontendFactory } from "../frontend/pipeline.js";
import type { CompilationTiming } from "../timing.js";
export interface PreparedExecutableModule {
ok: true;
mod: IrModule;
sourceTexts: Map<string, string>;
stats: LowerStats;
/** Divergence warnings (SC6xxx): the build succeeds; these sites can
* behave differently from Node. */
warnings: ScrDiagnostic[];
}
/** Release the parser/checker before native object generation begins. */
export function prepareExecutableModule(
entryPath: string,
opts: CompileRequestOptions,
ffi: FfiProfile | null,
buildPlatform: string,
createFrontend: FrontendFactory,
timing: CompilationTiming = () => {},
): PreparedExecutableModule | CompileFailure {
const fe = createFrontend(entryPath, opts.npmStatic);
timing("frontend-load");
let lowered: LowerResult;
let sourceTexts: Map<string, string>;
// The frontend (and its tsgo server) is released as soon as lowering
// ends — clang and the link never hold it open.
try {
const fail = (diagnostics: ScrDiagnostic[]): CompileFailure => ({
ok: false,
diagnostics,
sourceTexts: fe.sourceTexts(),
});
const refused = fe.npmStatic.filter((entry) => entry.status === "refused");
if (refused.length > 0) {
return fail(
refused.map((entry) =>
npmStaticRefusalDiag(
entry.package,
entry.detail ?? "its static compilation was refused",
fe.npmImportSites.get(entry.package) ?? { file: entryPath, start: 0, end: 0 },
entry.hint,
),
),
);
}
if (fe.preflight.length > 0) return fail(fe.preflight);
try {
lowered = fe.lower({
targetPlatform: buildPlatform,
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
});
timing("lower");
} catch (e) {
// The last-resort panic fence: an upstream tsgo panic that crossed a
// checker call no statement/collection fence wrapped still becomes a
// clean failed compile (anchored at the entry), never a crashed CLI.
if (!isCheckerPanic(e)) throw e;
return fail([
checkerPanicDiag(e.message.split("\n", 1)[0]!, { file: entryPath, start: 0, end: 0 }),
]);
}
if (lowered.module === null) return fail(lowered.diagnostics);
const validation = validateModule(lowered.module);
timing("ir-validate");
if (validation.length > 0) {
return fail(validation.map((v) => iceDiag(v.message, v.loc)));
}
if (buildPlatform === "wasi") {
const entryLoc: SrcLoc = { file: entryPath, start: 0, end: 0 };
if (opts.sanitize) {
return fail([targetRefusalDiag("wasm32-wasi", "--sanitize", entryLoc)]);
}
if (ffi !== null) {
return fail([targetRefusalDiag("wasm32-wasi", "native FFI manifests", entryLoc)]);
}
fenceSpeculativeWasiFunctions(lowered.module);
const unavailable = moduleWasiUnavailableSurface(lowered.module);
if (unavailable !== null) {
return fail([targetRefusalDiag("wasm32-wasi", unavailable.surface, unavailable.loc)]);
}
}
sourceTexts = fe.sourceTexts();
if (lowered.contracts) {
const checked = checkContracts(lowered.module, lowered.contracts, sourceTexts);
timing("contracts");
if (checked.diagnostics.length > 0) return fail(checked.diagnostics);
if (checked.contracts) lowered.module.contracts = checked.contracts;
}
} finally {
fe.dispose();
frontendDisposed();
timing("frontend-dispose");
}
return {
ok: true,
mod: lowered.module!,
sourceTexts,
stats: lowered.stats,
warnings: [...(lowered.divergences ?? []), ...(lowered.performance ?? [])],
};
}