diff --git a/tsc/internal/compiler/checkerpool.go b/tsc/internal/compiler/checkerpool.go index e129951e423df..302300e6a43ab 100644 --- a/tsc/internal/compiler/checkerpool.go +++ b/tsc/internal/compiler/checkerpool.go @@ -492,4 +492,16 @@ func (p *checkerPool) forEachCheckerGroupDo(ctx context.Context, files []*ast.So wg.RunAndWait() } +// groupFilesByChecker returns the indices of files grouped by the checker each file is associated with. Files keep +// their order within each group. +func (p *checkerPool) groupFilesByChecker(files []*ast.SourceFile) [][]int { + p.createCheckers() + groups := make([][]int, len(p.checkers)) + for i, file := range files { + idx := slices.Index(p.checkers, p.fileAssociations[file]) + groups[idx] = append(groups[idx], i) + } + return groups +} + func noop() {} diff --git a/tsc/internal/compiler/emitter.go b/tsc/internal/compiler/emitter.go index 7cf9d58b5fcef..06d6c1636f7eb 100644 --- a/tsc/internal/compiler/emitter.go +++ b/tsc/internal/compiler/emitter.go @@ -41,9 +41,12 @@ type emitter struct { forceEmit bool writeFile WriteFile tr *tracing.Tracing + prints []func() } -func (e *emitter) emit() { +// transform runs the JS and declaration transformers for the file, the part of emit that uses the checker. print +// prints and writes what transform produced. +func (e *emitter) transform() { if e.tr != nil { defer e.tr.Push(tracing.PhaseEmit, "emit", map[string]any{"path": string(e.sourceFile.PathKey())}, true)() } @@ -51,6 +54,18 @@ func (e *emitter) emit() { emitResolver := e.host.NewEmitResolver(emitContext) e.emitJSFile(emitResolver, e.sourceFile, e.paths.JsFilePath(), e.paths.SourceMapFilePath()) e.emitDeclarationFile(emitResolver, e.sourceFile, e.paths.DeclarationFilePath(), e.paths.DeclarationMapPath()) +} + +// print prints and writes the outputs transformed by transform. It must not use the checker, which may be in use by +// another file by then. +func (e *emitter) print() { + if e.tr != nil { + defer e.tr.Push(tracing.PhaseEmit, "emitPrint", map[string]any{"path": string(e.sourceFile.PathKey())}, true)() + } + for _, print := range e.prints { + print() + } + e.prints = nil e.emitResult.Diagnostics = e.emitterDiagnostics.GetDiagnostics() } @@ -219,7 +234,9 @@ func (e *emitter) emitJSFile(emitResolver printer.EmitResolver, sourceFile *ast. // !!! }, emitContext) - e.printSourceFile(emitContext, jsFilePath, sourceMapFilePath, sourceFile, printer, options, shouldEmitSourceMaps(options, sourceFile)) + e.prints = append(e.prints, func() { + e.printSourceFile(emitContext, jsFilePath, sourceMapFilePath, sourceFile, printer, options, shouldEmitSourceMaps(options, sourceFile)) + }) } func (e *emitter) emitDeclarationFile(emitResolver printer.EmitResolver, sourceFile *ast.SourceFile, declarationFilePath tspath.RootedFilePath, declarationMapPath tspath.RootedFilePath) { @@ -292,7 +309,9 @@ func (e *emitter) emitDeclarationFile(emitResolver printer.EmitResolver, sourceF MapRoot: options.MapRoot, // Explicitly do not pass through either inline option. } - e.printSourceFile(emitContext, declarationFilePath, declarationMapPath, sourceFile, printer, declarationMapOptions, shouldEmitSourceMaps(declarationMapOptions, sourceFile)) + e.prints = append(e.prints, func() { + e.printSourceFile(emitContext, declarationFilePath, declarationMapPath, sourceFile, printer, declarationMapOptions, shouldEmitSourceMaps(declarationMapOptions, sourceFile)) + }) } type declarationMapSource struct { diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index 9946362def3ad..5b8b8d86d4e32 100644 --- a/tsc/internal/compiler/program.go +++ b/tsc/internal/compiler/program.go @@ -1933,37 +1933,68 @@ func (p *Program) Emit(ctx context.Context, options EmitOptions) *EmitResult { sourceFiles := p.getSourceFilesToEmit(options.TargetSourceFiles, forceDtsEmit, forceJsEmit) for _, sourceFile := range sourceFiles { - emitter := &emitter{ + emitters = append(emitters, &emitter{ writer: nil, sourceFile: sourceFile, emitOnly: options.EmitOnly, forceEmit: options.ForceEmit, writeFile: options.WriteFile, tr: p.hosts.Tracing, + }) + } + transformFile := func(emitter *emitter) { + sourceFile := emitter.sourceFile + host, done := newEmitHost(ctx, p, sourceFile) + defer done() + emitter.host = host + emitter.paths = outputpaths.GetOutputPathsFor(sourceFile, host.Options(), host, outputpaths.ForceEmitPaths{ + Dts: forceDtsEmit, + Js: forceJsEmit, + DeclarationMap: options.ForceEmit && options.EmitOnly == EmitOnlyDts, + }) + emitter.transform() + } + printFile := func(emitter *emitter) { + // take an unused writer + writer := writerPool.Get().(printer.EmitTextWriter) + writer.Clear() + + // attach writer and print + emitter.writer = writer + emitter.print() + emitter.writer = nil + + // put the writer back in the pool + writerPool.Put(writer) + } + if p.compilerCheckerPool != nil && !p.SingleThreaded() { + // Transform the files of each checker in one task. Transforming can create types and resolve symbols, so a + // checker shared by concurrent tasks would do that work in an order that depends on scheduling. Files are + // transformed in reverse program order, the order in which a single-threaded work group runs per-file tasks, + // so that a program with one checker produces the same output whether or not it is single-threaded. Printing + // does not use the checker, so it runs in tasks of its own. Single-threaded programs emit file by file instead, + // since queued prints would only run after every file is transformed. + for _, group := range p.compilerCheckerPool.groupFilesByChecker(sourceFiles) { + if len(group) == 0 { + continue + } + wg.Queue(func() { + for _, i := range slices.Backward(group) { + emitter := emitters[i] + transformFile(emitter) + wg.Queue(func() { + printFile(emitter) + }) + } + }) } - emitters = append(emitters, emitter) - wg.Queue(func() { - host, done := newEmitHost(ctx, p, sourceFile) - defer done() - emitter.host = host - - // take an unused writer - writer := writerPool.Get().(printer.EmitTextWriter) - writer.Clear() - - // attach writer and perform emit - emitter.writer = writer - emitter.paths = outputpaths.GetOutputPathsFor(sourceFile, host.Options(), host, outputpaths.ForceEmitPaths{ - Dts: forceDtsEmit, - Js: forceJsEmit, - DeclarationMap: options.ForceEmit && options.EmitOnly == EmitOnlyDts, + } else { + for _, emitter := range emitters { + wg.Queue(func() { + transformFile(emitter) + printFile(emitter) }) - emitter.emit() - emitter.writer = nil - - // put the writer back in the pool - writerPool.Put(writer) - }) + } } // wait for emit to complete diff --git a/tsc/testdata/baselines/reference/compiler/noFlakyDeclarationEmitSharedCheckerOrder.js b/tsc/testdata/baselines/reference/compiler/noFlakyDeclarationEmitSharedCheckerOrder.js new file mode 100644 index 0000000000000..9d0b0d5c36d1b --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/noFlakyDeclarationEmitSharedCheckerOrder.js @@ -0,0 +1,173 @@ +//// [tests/cases/compiler/noFlakyDeclarationEmitSharedCheckerOrder.ts] //// + +//// [ma.js] +export { v as pa } from "./xa"; +export const ma = 1; + +//// [mb.js] +export { v as pb } from "./xb"; +export const mb = 1; + +//// [xa.js] +/** @type {typeof import("./mb")} */ +const M = /** @type {any} */ (null); +const o = { v: { ...M } }; +module.exports = o; + +//// [xb.js] +/** @type {typeof import("./ma")} */ +const M = /** @type {any} */ (null); +const o = { v: { ...M } }; +module.exports = o; + +//// [a.ts] +import * as MA from "./ma"; +export const fromA = MA.pa; + +//// [b.ts] +import * as MB from "./mb"; +export const fromB = MB.pb; + +//// [c.ts] +export const c = 1; + + +//// [xb.js] +"use strict"; +/** @type {typeof import("./ma")} */ +const M = /** @type {any} */ (null); +const o = { v: { ...M } }; +module.exports = o; +//// [mb.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.mb = exports.pb = void 0; +var xb_1 = require("./xb"); +Object.defineProperty(exports, "pb", { enumerable: true, get: function () { return xb_1.v; } }); +exports.mb = 1; +//// [xa.js] +"use strict"; +/** @type {typeof import("./mb")} */ +const M = /** @type {any} */ (null); +const o = { v: { ...M } }; +module.exports = o; +//// [ma.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ma = exports.pa = void 0; +var xa_1 = require("./xa"); +Object.defineProperty(exports, "pa", { enumerable: true, get: function () { return xa_1.v; } }); +exports.ma = 1; +//// [a.js] +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.fromA = void 0; +const MA = __importStar(require("./ma")); +exports.fromA = MA.pa; +//// [b.js] +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.fromB = void 0; +const MB = __importStar(require("./mb")); +exports.fromB = MB.pb; +//// [c.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.c = void 0; +exports.c = 1; + + +//// [xb.d.ts] +export = o; +declare const o: { + v: { + pa: {}; + ma: 1; + }; +}; +//// [mb.d.ts] +export { v as pb } from "./xb"; +export declare const mb = 1; +//// [xa.d.ts] +export = o; +declare const o: { + v: {}; +}; +//// [ma.d.ts] +export { v as pa } from "./xa"; +export declare const ma = 1; +//// [a.d.ts] +export declare const fromA: {}; +//// [b.d.ts] +export declare const fromB: { + pa: {}; + ma: 1; +}; +//// [c.d.ts] +export declare const c = 1; diff --git a/tsc/testdata/baselines/reference/compiler/noFlakyDeclarationEmitSharedCheckerOrder.symbols b/tsc/testdata/baselines/reference/compiler/noFlakyDeclarationEmitSharedCheckerOrder.symbols new file mode 100644 index 0000000000000..b400eedf066bc --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/noFlakyDeclarationEmitSharedCheckerOrder.symbols @@ -0,0 +1,74 @@ +//// [tests/cases/compiler/noFlakyDeclarationEmitSharedCheckerOrder.ts] //// + +=== /ma.js === +export { v as pa } from "./xa"; +>v : Symbol(v, Decl(xa.js, 2, 11)) +>pa : Symbol(pa, Decl(ma.js, 0, 8)) + +export const ma = 1; +>ma : Symbol(ma, Decl(ma.js, 1, 12)) + +=== /mb.js === +export { v as pb } from "./xb"; +>v : Symbol(v, Decl(xb.js, 2, 11)) +>pb : Symbol(pb, Decl(mb.js, 0, 8)) + +export const mb = 1; +>mb : Symbol(mb, Decl(mb.js, 1, 12)) + +=== /xa.js === +/** @type {typeof import("./mb")} */ +const M = /** @type {any} */ (null); +>M : Symbol(M, Decl(xa.js, 1, 5)) + +const o = { v: { ...M } }; +>o : Symbol(o, Decl(xa.js, 2, 5)) +>v : Symbol(v, Decl(xa.js, 2, 11)) +>M : Symbol(M, Decl(xa.js, 1, 5)) + +module.exports = o; +>module.exports : Symbol(exports, Decl(xa.js, 0, 0)) +>module : Symbol(module, Decl(xa.js, 0, 0)) +>exports : Symbol(exports, Decl(xa.js, 0, 0)) +>o : Symbol(o, Decl(xa.js, 2, 5)) + +=== /xb.js === +/** @type {typeof import("./ma")} */ +const M = /** @type {any} */ (null); +>M : Symbol(M, Decl(xb.js, 1, 5)) + +const o = { v: { ...M } }; +>o : Symbol(o, Decl(xb.js, 2, 5)) +>v : Symbol(v, Decl(xb.js, 2, 11)) +>M : Symbol(M, Decl(xb.js, 1, 5)) + +module.exports = o; +>module.exports : Symbol(exports, Decl(xb.js, 0, 0)) +>module : Symbol(module, Decl(xb.js, 0, 0)) +>exports : Symbol(exports, Decl(xb.js, 0, 0)) +>o : Symbol(o, Decl(xb.js, 2, 5)) + +=== /a.ts === +import * as MA from "./ma"; +>MA : Symbol(MA, Decl(a.ts, 0, 6)) + +export const fromA = MA.pa; +>fromA : Symbol(fromA, Decl(a.ts, 1, 12)) +>MA.pa : Symbol(MA.pa, Decl(ma.js, 0, 8)) +>MA : Symbol(MA, Decl(a.ts, 0, 6)) +>pa : Symbol(MA.pa, Decl(ma.js, 0, 8)) + +=== /b.ts === +import * as MB from "./mb"; +>MB : Symbol(MB, Decl(b.ts, 0, 6)) + +export const fromB = MB.pb; +>fromB : Symbol(fromB, Decl(b.ts, 1, 12)) +>MB.pb : Symbol(MB.pb, Decl(mb.js, 0, 8)) +>MB : Symbol(MB, Decl(b.ts, 0, 6)) +>pb : Symbol(MB.pb, Decl(mb.js, 0, 8)) + +=== /c.ts === +export const c = 1; +>c : Symbol(c, Decl(c.ts, 0, 12)) + diff --git a/tsc/testdata/baselines/reference/compiler/noFlakyDeclarationEmitSharedCheckerOrder.types b/tsc/testdata/baselines/reference/compiler/noFlakyDeclarationEmitSharedCheckerOrder.types new file mode 100644 index 0000000000000..cde44436bf6e0 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/noFlakyDeclarationEmitSharedCheckerOrder.types @@ -0,0 +1,85 @@ +//// [tests/cases/compiler/noFlakyDeclarationEmitSharedCheckerOrder.ts] //// + +=== /ma.js === +export { v as pa } from "./xa"; +>v : {} +>pa : {} + +export const ma = 1; +>ma : 1 +>1 : 1 + +=== /mb.js === +export { v as pb } from "./xb"; +>v : { pa: {}; ma: 1; } +>pb : { pa: {}; ma: 1; } + +export const mb = 1; +>mb : 1 +>1 : 1 + +=== /xa.js === +/** @type {typeof import("./mb")} */ +const M = /** @type {any} */ (null); +>M : typeof import("./mb") +>(null) : any + +const o = { v: { ...M } }; +>o : { v: {}; } +>{ v: { ...M } } : { v: { pb: { pa: {}; ma: 1; }; mb: 1; }; } +>v : { pb: { pa: {}; ma: 1; }; mb: 1; } +>{ ...M } : { pb: { pa: {}; ma: 1; }; mb: 1; } +>M : typeof import("./mb") + +module.exports = o; +>module.exports = o : { v: {}; } +>module.exports : { v: {}; } +>module : { exports: { v: {}; }; } +>exports : { v: {}; } +>o : { v: {}; } + +=== /xb.js === +/** @type {typeof import("./ma")} */ +const M = /** @type {any} */ (null); +>M : typeof import("./ma") +>(null) : any + +const o = { v: { ...M } }; +>o : { v: { pa: {}; ma: 1; }; } +>{ v: { ...M } } : { v: { pa: {}; ma: 1; }; } +>v : { pa: {}; ma: 1; } +>{ ...M } : { pa: {}; ma: 1; } +>M : typeof import("./ma") + +module.exports = o; +>module.exports = o : { v: { pa: {}; ma: 1; }; } +>module.exports : { v: { pa: {}; ma: 1; }; } +>module : { exports: { v: { pa: {}; ma: 1; }; }; } +>exports : { v: { pa: {}; ma: 1; }; } +>o : { v: { pa: {}; ma: 1; }; } + +=== /a.ts === +import * as MA from "./ma"; +>MA : typeof MA + +export const fromA = MA.pa; +>fromA : {} +>MA.pa : {} +>MA : typeof MA +>pa : {} + +=== /b.ts === +import * as MB from "./mb"; +>MB : typeof MB + +export const fromB = MB.pb; +>fromB : { pa: {}; ma: 1; } +>MB.pb : { pa: {}; ma: 1; } +>MB : typeof MB +>pb : { pa: {}; ma: 1; } + +=== /c.ts === +export const c = 1; +>c : 1 +>1 : 1 + diff --git a/tsc/testdata/tests/cases/compiler/noFlakyDeclarationEmitSharedCheckerOrder.ts b/tsc/testdata/tests/cases/compiler/noFlakyDeclarationEmitSharedCheckerOrder.ts new file mode 100644 index 0000000000000..08ecb7f9f868c --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/noFlakyDeclarationEmitSharedCheckerOrder.ts @@ -0,0 +1,37 @@ +// @module: commonjs +// @allowJs: true +// @checkJs: false +// @declaration: true +// @outDir: /out +// @checkers: 1 + +// @Filename: /ma.js +export { v as pa } from "./xa"; +export const ma = 1; + +// @Filename: /mb.js +export { v as pb } from "./xb"; +export const mb = 1; + +// @Filename: /xa.js +/** @type {typeof import("./mb")} */ +const M = /** @type {any} */ (null); +const o = { v: { ...M } }; +module.exports = o; + +// @Filename: /xb.js +/** @type {typeof import("./ma")} */ +const M = /** @type {any} */ (null); +const o = { v: { ...M } }; +module.exports = o; + +// @Filename: /a.ts +import * as MA from "./ma"; +export const fromA = MA.pa; + +// @Filename: /b.ts +import * as MB from "./mb"; +export const fromB = MB.pb; + +// @Filename: /c.ts +export const c = 1;