refactor(compiler-cli): make file/shim split 1:n instead of 1:1 (#38105)

Previously in the template type-checking engine, it was assumed that every
input file would have an associated type-checking shim. The type check block
code for all components in the input file would be generated into this shim.

This is fine for whole-program type checking operations, but to support the
language service's requirements for low latency, it would be ideal to be
able to check a single component in isolation, especially if the component
is declared along with many others in a single file.

This commit removes the assumption that the file/shim mapping is 1:1, and
introduces the concept of component-to-shim mapping. Any
`TypeCheckingProgramStrategy` must provide such a mapping.

To achieve this:

 * type checking record information is now split into file-level data as
   well as per-shim data.
 * components are now assigned a stable `TemplateId` which is unique to the
   file in which they're declared.

PR Close #38105
This commit is contained in:
Alex Rickabaugh 2020-06-22 12:32:24 -07:00 committed by Misko Hevery
parent 9c8bc4a239
commit 736f6337b2
8 changed files with 200 additions and 89 deletions

View File

@ -12,21 +12,21 @@ import {AbsoluteFsPath} from '../file_system';
/**
* Interface of the incremental build engine.
*
* `W` is a generic type representing a unit of work. This is generic to avoid a cyclic dependency
* between the incremental engine API definition and its consumer(s).
* `T` is a generic type representing template type-checking data for a particular file, which is
* generic for the same reason.
* `AnalysisT` is a generic type representing a unit of work. This is generic to avoid a cyclic
* dependency between the incremental engine API definition and its consumer(s).
* `FileTypeCheckDataT` is a generic type representing template type-checking data for a particular
* input file, which is generic for the same reason.
*/
export interface IncrementalBuild<W, T> {
export interface IncrementalBuild<AnalysisT, FileTypeCheckDataT> {
/**
* Retrieve the prior analysis work, if any, done for the given source file.
*/
priorWorkFor(sf: ts.SourceFile): W[]|null;
priorWorkFor(sf: ts.SourceFile): AnalysisT[]|null;
/**
* Retrieve the prior type-checking work, if any, that's been done for the given source file.
*/
priorTypeCheckingResultsFor(sf: ts.SourceFile): T|null;
priorTypeCheckingResultsFor(fileSf: ts.SourceFile): FileTypeCheckDataT|null;
}
/**

View File

@ -13,6 +13,7 @@ import {AbsoluteFsPath} from '../../file_system';
import {Reference} from '../../imports';
import {TemplateGuardMeta} from '../../metadata';
import {ClassDeclaration} from '../../reflection';
import {ComponentToShimMappingStrategy} from './context';
/**
@ -284,7 +285,7 @@ export interface ExternalTemplateSourceMapping {
* This abstraction allows both the Angular compiler itself as well as the language service to
* implement efficient template type-checking using common infrastructure.
*/
export interface TypeCheckingProgramStrategy {
export interface TypeCheckingProgramStrategy extends ComponentToShimMappingStrategy {
/**
* Retrieve the latest version of the program, containing all the updates made thus far.
*/

View File

@ -8,11 +8,12 @@
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../file_system';
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system';
import {retagAllTsFiles, untagAllTsFiles} from '../../shims';
import {TypeCheckingProgramStrategy, UpdateMode} from './api';
import {TypeCheckProgramHost} from './host';
import {TypeCheckShimGenerator} from './shim';
/**
* Implements a template type-checking program using `ts.createProgram` and TypeScript's program
@ -78,4 +79,8 @@ export class ReusedProgramStrategy implements TypeCheckingProgramStrategy {
untagAllTsFiles(this.program);
untagAllTsFiles(oldProgram);
}
shimPathForComponent(node: ts.ClassDeclaration): AbsoluteFsPath {
return TypeCheckShimGenerator.shimFor(absoluteFromSourceFile(node.getSourceFile()));
}
}

View File

@ -16,7 +16,7 @@ import {isShim} from '../../shims';
import {TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from './api';
import {FileTypeCheckingData, TypeCheckContext, TypeCheckRequest} from './context';
import {shouldReportDiagnostic, translateDiagnostic} from './diagnostics';
import {shouldReportDiagnostic, TemplateSourceResolver, translateDiagnostic} from './diagnostics';
/**
* Interface to trigger generation of type-checking code for a program given a new
@ -49,8 +49,8 @@ export class TemplateTypeChecker {
refresh(): TypeCheckRequest {
this.files.clear();
const ctx =
new TypeCheckContext(this.config, this.compilerHost, this.refEmitter, this.reflector);
const ctx = new TypeCheckContext(
this.config, this.compilerHost, this.typeCheckingStrategy, this.refEmitter, this.reflector);
// Typecheck all the files.
for (const sf of this.originalProgram.getSourceFiles()) {
@ -86,25 +86,32 @@ export class TemplateTypeChecker {
if (!this.files.has(path)) {
return [];
}
const record = this.files.get(path)!;
const fileRecord = this.files.get(path)!;
const typeCheckProgram = this.typeCheckingStrategy.getProgram();
const typeCheckSf = getSourceFileOrError(typeCheckProgram, record.typeCheckFile);
const rawDiagnostics = [];
rawDiagnostics.push(...typeCheckProgram.getSemanticDiagnostics(typeCheckSf));
if (record.hasInlines) {
const diagnostics: (ts.Diagnostic|null)[] = [];
if (fileRecord.hasInlines) {
const inlineSf = getSourceFileOrError(typeCheckProgram, path);
rawDiagnostics.push(...typeCheckProgram.getSemanticDiagnostics(inlineSf));
diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(inlineSf).map(
diag => convertDiagnostic(diag, fileRecord.sourceResolver)));
}
for (const [shimPath, shimRecord] of fileRecord.shimData) {
const shimSf = getSourceFileOrError(typeCheckProgram, shimPath);
diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(shimSf).map(
diag => convertDiagnostic(diag, fileRecord.sourceResolver)));
diagnostics.push(...shimRecord.genesisDiagnostics);
}
return rawDiagnostics
.map(diag => {
if (!shouldReportDiagnostic(diag)) {
return null;
}
return translateDiagnostic(diag, record.sourceResolver);
})
.filter((diag: ts.Diagnostic|null): diag is ts.Diagnostic => diag !== null)
.concat(record.genesisDiagnostics);
return diagnostics.filter((diag: ts.Diagnostic|null): diag is ts.Diagnostic => diag !== null);
}
}
function convertDiagnostic(
diag: ts.Diagnostic, sourceResolver: TemplateSourceResolver): ts.Diagnostic|null {
if (!shouldReportDiagnostic(diag)) {
return null;
}
return translateDiagnostic(diag, sourceResolver);
}

View File

@ -14,12 +14,11 @@ import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports';
import {ClassDeclaration, ReflectionHost} from '../../reflection';
import {ImportManager} from '../../translator';
import {TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckingConfig, TypeCtorMetadata} from './api';
import {TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckingConfig, TypeCtorMetadata} from './api';
import {TemplateSourceResolver} from './diagnostics';
import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom';
import {Environment} from './environment';
import {OutOfBandDiagnosticRecorder, OutOfBandDiagnosticRecorderImpl} from './oob';
import {TypeCheckShimGenerator} from './shim';
import {TemplateSourceManager} from './source';
import {generateTypeCheckBlock, requiresInlineTypeCheckBlock} from './type_check_block';
import {TypeCheckFile} from './type_check_file';
@ -46,7 +45,8 @@ export interface TypeCheckRequest {
}
/**
* Data for a type-checking shim which is required to support generation of diagnostics.
* Data for template type-checking related to a specific input file in the user's program (which
* contains components to be checked).
*/
export interface FileTypeCheckingData {
/**
@ -56,25 +56,39 @@ export interface FileTypeCheckingData {
hasInlines: boolean;
/**
* Source mapping information for mapping diagnostics back to the original template.
* Source mapping information for mapping diagnostics from inlined type check blocks back to the
* original template.
*/
sourceResolver: TemplateSourceResolver;
/**
* Data for each shim generated from this input file.
*
* A single input file will generate one or more shim files that actually contain template
* type-checking code.
*/
shimData: Map<AbsoluteFsPath, ShimTypeCheckingData>;
}
/**
* Data specific to a single shim generated from an input file.
*/
export interface ShimTypeCheckingData {
/**
* Path to the shim file.
*/
path: AbsoluteFsPath;
/**
* Any `ts.Diagnostic`s which were produced during the generation of this shim.
*
* Some diagnostics are produced during creation time and are tracked here.
*/
genesisDiagnostics: ts.Diagnostic[];
/**
* Path to the shim file.
*/
typeCheckFile: AbsoluteFsPath;
}
/**
* Data for a type-checking shim which is still having its code generated.
* Data for an input file which is still in the process of template type-checking code generation.
*/
export interface PendingFileTypeCheckingData {
/**
@ -83,10 +97,18 @@ export interface PendingFileTypeCheckingData {
hasInlines: boolean;
/**
* `TemplateSourceManager` being used to track source mapping information for this shim.
* Source mapping information for mapping diagnostics from inlined type check blocks back to the
* original template.
*/
sourceManager: TemplateSourceManager;
/**
* Map of in-progress shim data for shims generated from this input file.
*/
shimData: Map<AbsoluteFsPath, PendingShimData>;
}
export interface PendingShimData {
/**
* Recorder for out-of-band diagnostics which are raised during generation.
*/
@ -98,9 +120,28 @@ export interface PendingFileTypeCheckingData {
domSchemaChecker: DomSchemaChecker;
/**
* Path to the shim file.
* Shim file in the process of being generated.
*/
typeCheckFile: TypeCheckFile;
file: TypeCheckFile;
}
/**
* Abstracts the operation of determining which shim file will host a particular component's
* template type-checking code.
*
* Different consumers of the type checking infrastructure may choose different approaches to
* optimize for their specific use case (for example, the command-line compiler optimizes for
* efficient `ts.Program` reuse in watch mode).
*/
export interface ComponentToShimMappingStrategy {
/**
* Given a component, determine a path to the shim file into which that component's type checking
* code will be generated.
*
* A major constraint is that components in different input files must not share the same shim
* file. The behavior of the template type-checking system is undefined if this is violated.
*/
shimPathForComponent(node: ts.ClassDeclaration): AbsoluteFsPath;
}
/**
@ -115,6 +156,7 @@ export class TypeCheckContext {
constructor(
private config: TypeCheckingConfig,
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
private componentMappingStrategy: ComponentToShimMappingStrategy,
private refEmitter: ReferenceEmitter, private reflector: ReflectionHost) {}
/**
@ -160,8 +202,7 @@ export class TypeCheckContext {
schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping,
file: ParseSourceFile): void {
const fileData = this.dataForFile(ref.node.getSourceFile());
const id = fileData.sourceManager.captureSource(sourceMapping, file);
const shimData = this.pendingShimForComponent(ref.node);
// Get all of the directives used in the template and record type constructors for all of them.
for (const dir of boundTarget.getUsedDirectives()) {
const dirRef = dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
@ -184,15 +225,19 @@ export class TypeCheckContext {
}
}
const tcbMetadata: TypeCheckBlockMetadata = {id, boundTarget, pipes, schemas};
const meta = {
id: fileData.sourceManager.captureSource(ref.node, sourceMapping, file),
boundTarget,
pipes,
schemas,
};
if (requiresInlineTypeCheckBlock(ref.node)) {
// This class didn't meet the requirements for external type checking, so generate an inline
// TCB for the class.
this.addInlineTypeCheckBlock(fileData, ref, tcbMetadata);
this.addInlineTypeCheckBlock(fileData, shimData, ref, meta);
} else {
// The class can be type-checked externally as normal.
fileData.typeCheckFile.addTypeCheckBlock(
ref, tcbMetadata, fileData.domSchemaChecker, fileData.oobRecorder);
shimData.file.addTypeCheckBlock(ref, meta, shimData.domSchemaChecker, shimData.oobRecorder);
}
}
@ -278,17 +323,27 @@ export class TypeCheckContext {
perFileData: new Map<AbsoluteFsPath, FileTypeCheckingData>(),
};
for (const [sfPath, fileData] of this.fileMap.entries()) {
updates.set(fileData.typeCheckFile.fileName, fileData.typeCheckFile.render());
results.perFileData.set(sfPath, {
genesisDiagnostics: [
...fileData.domSchemaChecker.diagnostics,
...fileData.oobRecorder.diagnostics,
],
hasInlines: fileData.hasInlines,
sourceResolver: fileData.sourceManager,
typeCheckFile: fileData.typeCheckFile.fileName,
});
// Then go through each input file that has pending code generation operations.
for (const [sfPath, pendingFileData] of this.fileMap) {
const fileData: FileTypeCheckingData = {
hasInlines: pendingFileData.hasInlines,
shimData: new Map(),
sourceResolver: pendingFileData.sourceManager,
};
// For each input file, consider generation operations for each of its shims.
for (const [shimPath, pendingShimData] of pendingFileData.shimData) {
updates.set(pendingShimData.file.fileName, pendingShimData.file.render());
fileData.shimData.set(shimPath, {
genesisDiagnostics: [
...pendingShimData.domSchemaChecker.diagnostics,
...pendingShimData.oobRecorder.diagnostics,
],
path: pendingShimData.file.fileName,
});
}
results.perFileData.set(sfPath, fileData);
}
for (const [sfPath, fileData] of this.adoptedFiles.entries()) {
@ -299,7 +354,8 @@ export class TypeCheckContext {
}
private addInlineTypeCheckBlock(
fileData: PendingFileTypeCheckingData, ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
fileData: PendingFileTypeCheckingData, shimData: PendingShimData,
ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
tcbMeta: TypeCheckBlockMetadata): void {
const sf = ref.node.getSourceFile();
if (!this.opMap.has(sf)) {
@ -307,24 +363,35 @@ export class TypeCheckContext {
}
const ops = this.opMap.get(sf)!;
ops.push(new TcbOp(
ref, tcbMeta, this.config, this.reflector, fileData.domSchemaChecker,
fileData.oobRecorder));
ref, tcbMeta, this.config, this.reflector, shimData.domSchemaChecker,
shimData.oobRecorder));
fileData.hasInlines = true;
}
private pendingShimForComponent(node: ts.ClassDeclaration): PendingShimData {
const fileData = this.dataForFile(node.getSourceFile());
const shimPath = this.componentMappingStrategy.shimPathForComponent(node);
if (!fileData.shimData.has(shimPath)) {
fileData.shimData.set(shimPath, {
domSchemaChecker: new RegistryDomSchemaChecker(fileData.sourceManager),
oobRecorder: new OutOfBandDiagnosticRecorderImpl(fileData.sourceManager),
file: new TypeCheckFile(
shimPath, this.config, this.refEmitter, this.reflector, this.compilerHost),
});
}
return fileData.shimData.get(shimPath)!;
}
private dataForFile(sf: ts.SourceFile): PendingFileTypeCheckingData {
const sfPath = absoluteFromSourceFile(sf);
const sourceManager = new TemplateSourceManager();
if (!this.fileMap.has(sfPath)) {
const sourceManager = new TemplateSourceManager();
const data: PendingFileTypeCheckingData = {
domSchemaChecker: new RegistryDomSchemaChecker(sourceManager),
oobRecorder: new OutOfBandDiagnosticRecorderImpl(sourceManager),
typeCheckFile: new TypeCheckFile(
TypeCheckShimGenerator.shimFor(sfPath), this.config, this.refEmitter, this.reflector,
this.compilerHost),
hasInlines: false,
sourceManager,
shimData: new Map(),
};
this.fileMap.set(sfPath, data);
}

View File

@ -7,6 +7,7 @@
*/
import {AbsoluteSourceSpan, ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler';
import * as ts from 'typescript';
import {TemplateId, TemplateSourceMapping} from './api';
import {TemplateSourceResolver} from './diagnostics';
@ -47,7 +48,6 @@ export class TemplateSource {
* Implements `TemplateSourceResolver` to resolve the source of a template based on these IDs.
*/
export class TemplateSourceManager implements TemplateSourceResolver {
private nextTemplateId: number = 1;
/**
* This map keeps track of all template sources that have been type-checked by the id that is
* attached to a TCB's function declaration as leading trivia. This enables translation of
@ -55,8 +55,9 @@ export class TemplateSourceManager implements TemplateSourceResolver {
*/
private templateSources = new Map<TemplateId, TemplateSource>();
captureSource(mapping: TemplateSourceMapping, file: ParseSourceFile): TemplateId {
const id = `tcb${this.nextTemplateId++}` as TemplateId;
captureSource(node: ts.ClassDeclaration, mapping: TemplateSourceMapping, file: ParseSourceFile):
TemplateId {
const id = getTemplateId(node);
this.templateSources.set(id, new TemplateSource(mapping, file));
return id;
}
@ -76,3 +77,28 @@ export class TemplateSourceManager implements TemplateSourceResolver {
return templateSource.toParseSourceSpan(span.start, span.end);
}
}
const TEMPLATE_ID = Symbol('ngTemplateId');
const NEXT_TEMPLATE_ID = Symbol('ngNextTemplateId');
interface HasTemplateId {
[TEMPLATE_ID]: TemplateId;
}
interface HasNextTemplateId {
[NEXT_TEMPLATE_ID]: number;
}
function getTemplateId(node: ts.ClassDeclaration&Partial<HasTemplateId>): TemplateId {
if (node[TEMPLATE_ID] === undefined) {
node[TEMPLATE_ID] = allocateTemplateId(node.getSourceFile());
}
return node[TEMPLATE_ID]!;
}
function allocateTemplateId(sf: ts.SourceFile&Partial<HasNextTemplateId>): TemplateId {
if (sf[NEXT_TEMPLATE_ID] === undefined) {
sf[NEXT_TEMPLATE_ID] = 1;
}
return (`tcb${sf[NEXT_TEMPLATE_ID]!++}`) as TemplateId;
}

View File

@ -7,7 +7,7 @@
*/
import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, getSourceFileOrError, LogicalFileSystem, NgtscCompilerHost} from '../../file_system';
import {absoluteFrom, AbsoluteFsPath, getFileSystem, getSourceFileOrError, LogicalFileSystem, NgtscCompilerHost} from '../../file_system';
import {runInEachFileSystem, TestFile} from '../../file_system/testing';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
import {isNamedClassDeclaration, ReflectionHost, TypeScriptReflectionHost} from '../../reflection';
@ -15,8 +15,7 @@ import {getDeclaration, makeProgram} from '../../testing';
import {getRootDirs} from '../../util/src/typescript';
import {UpdateMode} from '../src/api';
import {ReusedProgramStrategy} from '../src/augmented_program';
import {PendingFileTypeCheckingData, TypeCheckContext} from '../src/context';
import {RegistryDomSchemaChecker} from '../src/dom';
import {ComponentToShimMappingStrategy, PendingFileTypeCheckingData, TypeCheckContext} from '../src/context';
import {TemplateSourceManager} from '../src/source';
import {TypeCheckFile} from '../src/type_check_file';
@ -73,10 +72,11 @@ TestClass.ngTypeCtor({value: 'test'});
new AbsoluteModuleStrategy(program, checker, moduleResolver, reflectionHost),
new LogicalProjectStrategy(reflectionHost, logicalFs),
]);
const ctx = new TypeCheckContext(ALL_ENABLED_CONFIG, host, emitter, reflectionHost);
const ctx = new TypeCheckContext(
ALL_ENABLED_CONFIG, host, new TestMappingStrategy(), emitter, reflectionHost);
const TestClass =
getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration);
const pendingFile = makePendingFile(reflectionHost, host);
const pendingFile = makePendingFile();
ctx.addInlineTypeCtor(
pendingFile, getSourceFileOrError(program, _('/main.ts')), new Reference(TestClass), {
fnName: 'ngTypeCtor',
@ -109,8 +109,9 @@ TestClass.ngTypeCtor({value: 'test'});
new AbsoluteModuleStrategy(program, checker, moduleResolver, reflectionHost),
new LogicalProjectStrategy(reflectionHost, logicalFs),
]);
const pendingFile = makePendingFile(reflectionHost, host);
const ctx = new TypeCheckContext(ALL_ENABLED_CONFIG, host, emitter, reflectionHost);
const pendingFile = makePendingFile();
const ctx = new TypeCheckContext(
ALL_ENABLED_CONFIG, host, new TestMappingStrategy(), emitter, reflectionHost);
const TestClass =
getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration);
ctx.addInlineTypeCtor(
@ -152,8 +153,9 @@ TestClass.ngTypeCtor({value: 'test'});
new AbsoluteModuleStrategy(program, checker, moduleResolver, reflectionHost),
new LogicalProjectStrategy(reflectionHost, logicalFs),
]);
const pendingFile = makePendingFile(reflectionHost, host);
const ctx = new TypeCheckContext(ALL_ENABLED_CONFIG, host, emitter, reflectionHost);
const pendingFile = makePendingFile();
const ctx = new TypeCheckContext(
ALL_ENABLED_CONFIG, host, new TestMappingStrategy(), emitter, reflectionHost);
const TestClass =
getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration);
ctx.addInlineTypeCtor(
@ -184,16 +186,16 @@ TestClass.ngTypeCtor({value: 'test'});
}
});
function makePendingFile(
reflector: ReflectionHost, compilerHost: ts.CompilerHost): PendingFileTypeCheckingData {
const manager = new TemplateSourceManager();
function makePendingFile(): PendingFileTypeCheckingData {
return {
domSchemaChecker: new RegistryDomSchemaChecker(manager),
hasInlines: false,
oobRecorder: new NoopOobRecorder(),
sourceManager: manager,
typeCheckFile: new TypeCheckFile(
absoluteFrom('/typecheck.ts'), ALL_ENABLED_CONFIG, new ReferenceEmitter([]), reflector,
compilerHost)
sourceManager: new TemplateSourceManager(),
shimData: new Map(),
};
}
class TestMappingStrategy implements ComponentToShimMappingStrategy {
shimPathForComponent(): AbsoluteFsPath {
return absoluteFrom('/typecheck.ts');
}
}

View File

@ -9,9 +9,9 @@
import {CompilerOptions} from '@angular/compiler-cli';
import {NgCompiler, NgCompilerHost} from '@angular/compiler-cli/src/ngtsc/core';
import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
import {absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
import {PatchedProgramIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental';
import {TypeCheckingProgramStrategy, UpdateMode} from '@angular/compiler-cli/src/ngtsc/typecheck';
import {TypeCheckingProgramStrategy, TypeCheckShimGenerator, UpdateMode} from '@angular/compiler-cli/src/ngtsc/typecheck';
import * as ts from 'typescript/lib/tsserverlibrary';
import {makeCompilerHostFromProject} from './compiler_host';
@ -75,6 +75,9 @@ export class Compiler {
function createTypeCheckingProgramStrategy(project: ts.server.Project):
TypeCheckingProgramStrategy {
return {
shimPathForComponent(component: ts.ClassDeclaration): AbsoluteFsPath {
return TypeCheckShimGenerator.shimFor(absoluteFromSourceFile(component.getSourceFile()));
},
getProgram(): ts.Program {
const program = project.getLanguageService().getProgram();
if (!program) {