introduce Path as branded string type, switch FileMap to use Path

This commit is contained in:
Vladimir Matveev
2015-10-29 16:24:00 -07:00
parent 93e942a6de
commit e1b4f01e77
6 changed files with 129 additions and 102 deletions
+33 -23
View File
@@ -17,45 +17,55 @@ namespace ts {
True = -1
}
export function createFileMap<T>(getCanonicalFileName: (fileName: string) => string, currentDirectory: string): FileMap<T> {
export function createFileMap<T>(keyMapper?: (key: string) => string): FileMap<T> {
let files: Map<T> = {};
return {
get,
set,
contains,
remove,
clear,
forEachValue: forEachValueInMap
getPath,
setPath,
containsPath,
removePath,
forEachValue: forEachValueInMap,
clear
};
function set(fileName: string, value: T) {
files[normalizeKey(fileName)] = value;
function forEachValueInMap(f: (key: Path, value: T) => void) {
for (let key in files) {
f(<Path>key, files[key]);
}
}
function get(fileName: string) {
return files[normalizeKey(fileName)];
// path should already be well-formed so it does not need to be normalized
function getPath(path: Path): T {
return files[toKey(path)];
}
function contains(fileName: string) {
return hasProperty(files, normalizeKey(fileName));
function setPath(path: Path, value: T) {
files[toKey(path)] = value;
}
function remove (fileName: string) {
let key = normalizeKey(fileName);
function containsPath(path: Path) {
return hasProperty(files, toKey(path));
}
function removePath(path: Path) {
const key = toKey(path);
delete files[key];
}
function forEachValueInMap(f: (value: T) => void) {
forEachValue(files, f);
}
function normalizeKey(key: string) {
return getCanonicalFileName(getNormalizedAbsolutePath(key, currentDirectory));
}
function clear() {
files = {};
}
function toKey(path: Path): string {
return keyMapper ? keyMapper(path) : path;
}
}
export function toPath(fileName: string, basePath: string, getCanonicalFileName: (path: string) => string): Path {
const nonCanonicalizedPath = isRootedDiskPath(fileName)
? normalizePath(fileName)
: getNormalizedAbsolutePath(fileName, basePath);
return <Path>getCanonicalFileName(nonCanonicalizedPath);
}
export const enum Comparison {
+21 -23
View File
@@ -346,10 +346,10 @@ namespace ts {
? ((moduleNames: string[], containingFile: string) => host.resolveModuleNames(moduleNames, containingFile))
: ((moduleNames: string[], containingFile: string) => map(moduleNames, moduleName => resolveModuleName(moduleName, containingFile, options, host).resolvedModule));
let filesByName = createFileMap<SourceFile>(getCanonicalFileName, currentDirectory);
let filesByName = createFileMap<SourceFile>();
// stores 'filename -> file association' ignoring case
// used to track cases when two file names differ only in casing
let filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createFileMap<SourceFile>(fileName => fileName.toLowerCase(), currentDirectory) : undefined;
let filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createFileMap<SourceFile>(fileName => fileName.toLowerCase()) : undefined;
if (oldProgram) {
// check properties that can affect structure of the program or module resolution strategy
@@ -384,7 +384,7 @@ namespace ts {
program = {
getRootFileNames: () => rootNames,
getSourceFile: getSourceFile,
getSourceFile,
getSourceFiles: () => files,
getCompilerOptions: () => options,
getSyntacticDiagnostics,
@@ -435,7 +435,7 @@ namespace ts {
// check if program source files has changed in the way that can affect structure of the program
let newSourceFiles: SourceFile[] = [];
let normalizedAbsoluteFileNames: string[] = [];
let filePaths: Path[] = [];
let modifiedSourceFiles: SourceFile[] = [];
for (let oldSourceFile of oldProgram.getSourceFiles()) {
@@ -444,8 +444,8 @@ namespace ts {
return false;
}
const normalizedAbsolutePath = getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory);
normalizedAbsoluteFileNames.push(normalizedAbsolutePath);
newSourceFile.path = oldSourceFile.path;
filePaths.push(newSourceFile.path);
if (oldSourceFile !== newSourceFile) {
if (oldSourceFile.hasNoDefaultLib !== newSourceFile.hasNoDefaultLib) {
@@ -469,7 +469,7 @@ namespace ts {
if (resolveModuleNamesWorker) {
let moduleNames = map(newSourceFile.imports, name => name.text);
let resolutions = resolveModuleNamesWorker(moduleNames, normalizedAbsolutePath);
let resolutions = resolveModuleNamesWorker(moduleNames, getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory));
// ensure that module resolution results are still correct
for (let i = 0; i < moduleNames.length; ++i) {
let newResolution = resolutions[i];
@@ -500,7 +500,7 @@ namespace ts {
// update fileName -> file mapping
for (let i = 0, len = newSourceFiles.length; i < len; ++i) {
filesByName.set(normalizedAbsoluteFileNames[i], newSourceFiles[i]);
filesByName.setPath(filePaths[i], newSourceFiles[i]);
}
files = newSourceFiles;
@@ -570,7 +570,7 @@ namespace ts {
}
function getSourceFile(fileName: string): SourceFile {
return filesByName.get(getNormalizedAbsolutePath(fileName, currentDirectory));
return filesByName.getPath(toPath(fileName, currentDirectory, getCanonicalFileName));
}
function getDiagnosticsHelper(
@@ -741,7 +741,7 @@ namespace ts {
diagnostic = Diagnostics.File_0_has_unsupported_extension_The_only_supported_extensions_are_1;
diagnosticArgument = [fileName, "'" + supportedExtensions.join("', '") + "'"];
}
else if (!findSourceFile(fileName, getNormalizedAbsolutePath(fileName, currentDirectory), isDefaultLib, refFile, refPos, refEnd)) {
else if (!findSourceFile(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), isDefaultLib, refFile, refPos, refEnd)) {
diagnostic = Diagnostics.File_0_not_found;
diagnosticArgument = [fileName];
}
@@ -751,13 +751,13 @@ namespace ts {
}
}
else {
let nonTsFile: SourceFile = options.allowNonTsExtensions && findSourceFile(fileName, getNormalizedAbsolutePath(fileName, currentDirectory), isDefaultLib, refFile, refPos, refEnd);
let nonTsFile: SourceFile = options.allowNonTsExtensions && findSourceFile(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), isDefaultLib, refFile, refPos, refEnd);
if (!nonTsFile) {
if (options.allowNonTsExtensions) {
diagnostic = Diagnostics.File_0_not_found;
diagnosticArgument = [fileName];
}
else if (!forEach(supportedExtensions, extension => findSourceFile(fileName + extension, getNormalizedAbsolutePath(fileName + extension, currentDirectory), isDefaultLib, refFile, refPos, refEnd))) {
else if (!forEach(supportedExtensions, extension => findSourceFile(fileName + extension, toPath(fileName + extension, currentDirectory, getCanonicalFileName), isDefaultLib, refFile, refPos, refEnd))) {
diagnostic = Diagnostics.File_0_not_found;
fileName += ".ts";
diagnosticArgument = [fileName];
@@ -786,9 +786,9 @@ namespace ts {
}
// Get source file from normalized fileName
function findSourceFile(fileName: string, normalizedAbsolutePath: string, isDefaultLib: boolean, refFile?: SourceFile, refPos?: number, refEnd?: number): SourceFile {
if (filesByName.contains(normalizedAbsolutePath)) {
const file = filesByName.get(normalizedAbsolutePath);
function findSourceFile(fileName: string, normalizedAbsolutePath: Path, isDefaultLib: boolean, refFile?: SourceFile, refPos?: number, refEnd?: number): SourceFile {
if (filesByName.containsPath(normalizedAbsolutePath)) {
const file = filesByName.getPath(normalizedAbsolutePath);
// try to check if we've already seen this file but with a different casing in path
// NOTE: this only makes sense for case-insensitive file systems
if (file && options.forceConsistentCasingInFileNames && getNormalizedAbsolutePath(file.fileName, currentDirectory) !== normalizedAbsolutePath) {
@@ -809,16 +809,18 @@ namespace ts {
}
});
filesByName.set(normalizedAbsolutePath, file);
filesByName.setPath(normalizedAbsolutePath, file);
if (file) {
file.path = normalizedAbsolutePath;
if (host.useCaseSensitiveFileNames()) {
// for case-sensitive file systems check if we've already seen some file with similar filename ignoring case
const existingFile = filesByNameIgnoreCase.get(normalizedAbsolutePath);
const existingFile = filesByNameIgnoreCase.getPath(normalizedAbsolutePath);
if (existingFile) {
reportFileNamesDifferOnlyInCasingError(fileName, existingFile.fileName, refFile, refPos, refEnd);
}
else {
filesByNameIgnoreCase.set(normalizedAbsolutePath, file);
filesByNameIgnoreCase.setPath(normalizedAbsolutePath, file);
}
}
@@ -865,11 +867,7 @@ namespace ts {
let resolution = resolutions[i];
setResolvedModule(file, moduleNames[i], resolution);
if (resolution && !options.noResolve) {
const absoluteImportPath = isRootedDiskPath(resolution.resolvedFileName)
? resolution.resolvedFileName
: getNormalizedAbsolutePath(resolution.resolvedFileName, currentDirectory);
const importedFile = findSourceFile(resolution.resolvedFileName, absoluteImportPath, /* isDefaultLib */ false, file, skipTrivia(file.text, file.imports[i].pos), file.imports[i].end);
const importedFile = findSourceFile(resolution.resolvedFileName, toPath(resolution.resolvedFileName, currentDirectory, getCanonicalFileName), /* isDefaultLib */ false, file, skipTrivia(file.text, file.imports[i].pos), file.imports[i].end);
if (importedFile && resolution.isExternalLibraryImport) {
if (!isExternalModule(importedFile)) {
+11 -5
View File
@@ -3,12 +3,17 @@ namespace ts {
[index: string]: T;
}
// branded string type used to store absolute, normalized and canonicalized paths
// arbitrary file name can be converted to Path via toPath function
export type Path = string & { __pathBrand: any };
export interface FileMap<T> {
get(fileName: string): T;
set(fileName: string, value: T): void;
contains(fileName: string): boolean;
remove(fileName: string): void;
forEachValue(f: (v: T) => void): void;
getPath(fileName: Path): T;
setPath(fileName: Path, value: T): void;
containsPath(fileName: Path): boolean;
removePath(fileName: Path): void;
forEachValue(f: (key: Path, v: T) => void): void;
clear(): void;
}
@@ -1250,6 +1255,7 @@ namespace ts {
endOfFileToken: Node;
fileName: string;
/* internal */ path: Path;
text: string;
amdDependencies: {path: string; name: string}[];
-1
View File
@@ -1357,7 +1357,6 @@ namespace ts {
export function tryResolveScriptReference(host: ScriptReferenceHost, sourceFile: SourceFile, reference: FileReference) {
if (!host.getCompilerOptions().noResolve) {
let referenceFileName = isRootedDiskPath(reference.fileName) ? reference.fileName : combinePaths(getDirectoryPath(sourceFile.fileName), reference.fileName);
referenceFileName = getNormalizedAbsolutePath(referenceFileName, host.getCurrentDirectory());
return host.getSourceFile(referenceFileName);
}
}
+25 -17
View File
@@ -33,8 +33,10 @@ namespace ts.server {
defaultProject: Project; // project to use by default for file
fileWatcher: FileWatcher;
formatCodeOptions = ts.clone(CompilerService.defaultFormatCodeOptions);
path: Path;
constructor(private host: ServerHost, public fileName: string, public content: string, public isOpen = false) {
this.path = toPath(fileName, host.getCurrentDirectory(), createGetCanonicalFileName(host.useCaseSensitiveFileNames));
this.svc = ScriptVersionCache.fromString(host, content);
}
@@ -90,11 +92,12 @@ namespace ts.server {
roots: ScriptInfo[] = [];
private resolvedModuleNames: ts.FileMap<Map<TimestampedResolvedModule>>;
private moduleResolutionHost: ts.ModuleResolutionHost;
private getCanonicalFileName: (fileName: string) => string;
constructor(public host: ServerHost, public project: Project) {
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
this.resolvedModuleNames = createFileMap<Map<TimestampedResolvedModule>>(getCanonicalFileName, host.getCurrentDirectory());
this.filenameToScript = createFileMap<ScriptInfo>(getCanonicalFileName, host.getCurrentDirectory());
this.getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
this.resolvedModuleNames = createFileMap<Map<TimestampedResolvedModule>>();
this.filenameToScript = createFileMap<ScriptInfo>();
this.moduleResolutionHost = {
fileExists: fileName => this.fileExists(fileName),
readFile: fileName => this.host.readFile(fileName)
@@ -102,7 +105,8 @@ namespace ts.server {
}
resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModule[] {
let currentResolutionsInFile = this.resolvedModuleNames.get(containingFile);
let path = toPath(containingFile, this.host.getCurrentDirectory(), this.getCanonicalFileName);
let currentResolutionsInFile = this.resolvedModuleNames.getPath(path);
let newResolutions: Map<TimestampedResolvedModule> = {};
let resolvedModules: ResolvedModule[] = [];
@@ -131,7 +135,7 @@ namespace ts.server {
}
// replace old results with a new one
this.resolvedModuleNames.set(containingFile, newResolutions);
this.resolvedModuleNames.setPath(path, newResolutions);
return resolvedModules;
function moduleResolutionIsValid(resolution: TimestampedResolvedModule): boolean {
@@ -201,34 +205,35 @@ namespace ts.server {
removeReferencedFile(info: ScriptInfo) {
if (!info.isOpen) {
this.filenameToScript.remove(info.fileName);
this.resolvedModuleNames.remove(info.fileName);
this.filenameToScript.removePath(info.path);
this.resolvedModuleNames.removePath(info.path);
}
}
getScriptInfo(filename: string): ScriptInfo {
let scriptInfo = this.filenameToScript.get(filename);
let path = toPath(filename, this.host.getCurrentDirectory(), this.getCanonicalFileName);
let scriptInfo = this.filenameToScript.getPath(path);
if (!scriptInfo) {
scriptInfo = this.project.openReferencedFile(filename);
if (scriptInfo) {
this.filenameToScript.set(scriptInfo.fileName, scriptInfo);
this.filenameToScript.setPath(path, scriptInfo);
}
}
return scriptInfo;
}
addRoot(info: ScriptInfo) {
if (!this.filenameToScript.contains(info.fileName)) {
this.filenameToScript.set(info.fileName, info);
if (!this.filenameToScript.containsPath(info.path)) {
this.filenameToScript.setPath(info.path, info);
this.roots.push(info);
}
}
removeRoot(info: ScriptInfo) {
if (!this.filenameToScript.contains(info.fileName)) {
this.filenameToScript.remove(info.fileName);
if (!this.filenameToScript.containsPath(info.path)) {
this.filenameToScript.removePath(info.path);
this.roots = copyListRemovingItem(info, this.roots);
this.resolvedModuleNames.remove(info.fileName);
this.resolvedModuleNames.removePath(info.path);
}
}
@@ -277,7 +282,8 @@ namespace ts.server {
* @param line 1 based index
*/
lineToTextSpan(filename: string, line: number): ts.TextSpan {
const script: ScriptInfo = this.filenameToScript.get(filename);
let path = toPath(filename, this.host.getCurrentDirectory(), this.getCanonicalFileName);
const script: ScriptInfo = this.filenameToScript.getPath(path);
const index = script.snap().index;
const lineInfo = index.lineNumberToInfo(line + 1);
@@ -297,7 +303,8 @@ namespace ts.server {
* @param offset 1 based index
*/
lineOffsetToPosition(filename: string, line: number, offset: number): number {
const script: ScriptInfo = this.filenameToScript.get(filename);
let path = toPath(filename, this.host.getCurrentDirectory(), this.getCanonicalFileName);
const script: ScriptInfo = this.filenameToScript.getPath(path);
const index = script.snap().index;
const lineInfo = index.lineNumberToInfo(line);
@@ -310,7 +317,8 @@ namespace ts.server {
* @param offset 1-based index
*/
positionToLineOffset(filename: string, position: number): ILineInfo {
const script: ScriptInfo = this.filenameToScript.get(filename);
let path = toPath(filename, this.host.getCurrentDirectory(), this.getCanonicalFileName);
const script: ScriptInfo = this.filenameToScript.getPath(path);
const index = script.snap().index;
const lineOffset = index.charOffsetToLineNumberAndPos(position);
return { line: lineOffset.line, offset: lineOffset.offset + 1 };
+39 -33
View File
@@ -773,6 +773,7 @@ namespace ts {
class SourceFileObject extends NodeObject implements SourceFile {
public _declarationBrand: any;
public fileName: string;
public path: Path;
public text: string;
public scriptSnapshot: IScriptSnapshot;
public lineMap: number[];
@@ -1695,15 +1696,17 @@ namespace ts {
class HostCache {
private fileNameToEntry: FileMap<HostFileInformation>;
private _compilationSettings: CompilerOptions;
private currentDirectory: string;
constructor(private host: LanguageServiceHost, getCanonicalFileName: (fileName: string) => string) {
constructor(private host: LanguageServiceHost, private getCanonicalFileName: (fileName: string) => string) {
// script id => script index
this.fileNameToEntry = createFileMap<HostFileInformation>(getCanonicalFileName, host.getCurrentDirectory());
this.currentDirectory = host.getCurrentDirectory();
this.fileNameToEntry = createFileMap<HostFileInformation>();
// Initialize the list with the root file names
let rootFileNames = host.getScriptFileNames();
for (let fileName of rootFileNames) {
this.createEntry(fileName);
this.createEntry(fileName, toPath(fileName, this.currentDirectory, getCanonicalFileName));
}
// store the compilation settings
@@ -1714,7 +1717,7 @@ namespace ts {
return this._compilationSettings;
}
private createEntry(fileName: string) {
private createEntry(fileName: string, path: Path) {
let entry: HostFileInformation;
let scriptSnapshot = this.host.getScriptSnapshot(fileName);
if (scriptSnapshot) {
@@ -1725,30 +1728,31 @@ namespace ts {
};
}
this.fileNameToEntry.set(fileName, entry);
this.fileNameToEntry.setPath(path, entry);
return entry;
}
private getEntry(fileName: string): HostFileInformation {
return this.fileNameToEntry.get(fileName);
private getEntry(path: Path): HostFileInformation {
return this.fileNameToEntry.getPath(path);
}
private contains(fileName: string): boolean {
return this.fileNameToEntry.contains(fileName);
private contains(path: Path): boolean {
return this.fileNameToEntry.containsPath(path);
}
public getOrCreateEntry(fileName: string): HostFileInformation {
if (this.contains(fileName)) {
return this.getEntry(fileName);
let path = toPath(fileName, this.currentDirectory, this.getCanonicalFileName)
if (this.contains(path)) {
return this.getEntry(path);
}
return this.createEntry(fileName);
return this.createEntry(fileName, path);
}
public getRootFileNames(): string[] {
let fileNames: string[] = [];
this.fileNameToEntry.forEachValue(value => {
this.fileNameToEntry.forEachValue((path, value) => {
if (value) {
fileNames.push(value.hostFileName);
}
@@ -1757,13 +1761,13 @@ namespace ts {
return fileNames;
}
public getVersion(fileName: string): string {
let file = this.getEntry(fileName);
public getVersion(path: Path): string {
let file = this.getEntry(path);
return file && file.version;
}
public getScriptSnapshot(fileName: string): IScriptSnapshot {
let file = this.getEntry(fileName);
public getScriptSnapshot(path: Path): IScriptSnapshot {
let file = this.getEntry(path);
return file && file.scriptSnapshot;
}
}
@@ -2007,7 +2011,7 @@ namespace ts {
let key = getKeyFromCompilationSettings(settings);
let bucket = lookUp(buckets, key);
if (!bucket && createIfMissing) {
buckets[key] = bucket = createFileMap<DocumentRegistryEntry>(getCanonicalFileName, currentDirectory);
buckets[key] = bucket = createFileMap<DocumentRegistryEntry>();
}
return bucket;
}
@@ -2016,14 +2020,13 @@ namespace ts {
let bucketInfoArray = Object.keys(buckets).filter(name => name && name.charAt(0) === '_').map(name => {
let entries = lookUp(buckets, name);
let sourceFiles: { name: string; refCount: number; references: string[]; }[] = [];
for (let i in entries) {
let entry = entries.get(i);
entries.forEachValue((key, entry) => {
sourceFiles.push({
name: i,
name: key,
refCount: entry.languageServiceRefCount,
references: entry.owners.slice(0)
});
}
});
sourceFiles.sort((x, y) => y.refCount - x.refCount);
return {
bucket: name,
@@ -2049,7 +2052,8 @@ namespace ts {
acquiring: boolean): SourceFile {
let bucket = getBucketForCompilationSettings(compilationSettings, /*createIfMissing*/ true);
let entry = bucket.get(fileName);
let path = toPath(fileName, currentDirectory, getCanonicalFileName);
let entry = bucket.getPath(path);
if (!entry) {
Debug.assert(acquiring, "How could we be trying to update a document that the registry doesn't have?");
@@ -2061,7 +2065,7 @@ namespace ts {
languageServiceRefCount: 0,
owners: []
};
bucket.set(fileName, entry);
bucket.setPath(path, entry);
}
else {
// We have an entry for this file. However, it may be for a different version of
@@ -2089,12 +2093,14 @@ namespace ts {
let bucket = getBucketForCompilationSettings(compilationSettings, false);
Debug.assert(bucket !== undefined);
let entry = bucket.get(fileName);
let path = toPath(fileName, currentDirectory, getCanonicalFileName);
let entry = bucket.getPath(path);
entry.languageServiceRefCount--;
Debug.assert(entry.languageServiceRefCount >= 0);
if (entry.languageServiceRefCount === 0) {
bucket.remove(fileName);
bucket.removePath(path);
}
}
@@ -2567,6 +2573,7 @@ namespace ts {
let useCaseSensitivefileNames = false;
let cancellationToken = new CancellationTokenObject(host.getCancellationToken && host.getCancellationToken());
let currentDirectory = host.getCurrentDirectory();
// Check if the localized messages json is set, otherwise query the host for it
if (!localizedDiagnosticMessages && host.getLocalizedDiagnosticMessages) {
localizedDiagnosticMessages = host.getLocalizedDiagnosticMessages();
@@ -2581,8 +2588,7 @@ namespace ts {
let getCanonicalFileName = createGetCanonicalFileName(useCaseSensitivefileNames);
function getValidSourceFile(fileName: string): SourceFile {
fileName = normalizeSlashes(fileName);
let sourceFile = program.getSourceFile(getCanonicalFileName(fileName));
let sourceFile = program.getSourceFile(fileName);
if (!sourceFile) {
throw new Error("Could not find file: '" + fileName + "'.");
}
@@ -2643,7 +2649,7 @@ namespace ts {
getNewLine: () => getNewLineOrDefaultFromHost(host),
getDefaultLibFileName: (options) => host.getDefaultLibFileName(options),
writeFile: (fileName, data, writeByteOrderMark) => { },
getCurrentDirectory: () => host.getCurrentDirectory(),
getCurrentDirectory: () => currentDirectory,
fileExists: (fileName): boolean => {
// stub missing host functionality
Debug.assert(!host.resolveModuleNames);
@@ -2667,9 +2673,8 @@ namespace ts {
if (program) {
let oldSourceFiles = program.getSourceFiles();
for (let oldSourceFile of oldSourceFiles) {
let fileName = oldSourceFile.fileName;
if (!newProgram.getSourceFile(fileName) || changesInCompilationSettingsAffectSyntax) {
documentRegistry.releaseDocument(fileName, oldSettings);
if (!newProgram.getSourceFile(oldSourceFile.fileName) || changesInCompilationSettingsAffectSyntax) {
documentRegistry.releaseDocument(oldSourceFile.fileName, oldSettings);
}
}
}
@@ -2734,7 +2739,8 @@ namespace ts {
}
function sourceFileUpToDate(sourceFile: SourceFile): boolean {
return sourceFile && sourceFile.version === hostCache.getVersion(sourceFile.fileName);
let path = sourceFile.path || toPath(sourceFile.fileName, currentDirectory, getCanonicalFileName);
return sourceFile && sourceFile.version === hostCache.getVersion(path);
}
function programUpToDate(): boolean {