-
Notifications
You must be signed in to change notification settings - Fork 264
Expand file tree
/
Copy pathrosetta-translator.ts
More file actions
312 lines (270 loc) · 10.1 KB
/
rosetta-translator.ts
File metadata and controls
312 lines (270 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
import * as spec from '@jsii/spec';
import { promises as fs } from 'fs';
import { TypeFingerprinter } from './jsii/fingerprinting';
import { TARGET_LANGUAGES } from './languages';
import * as logging from './logging';
import { TypeScriptSnippet, completeSource } from './snippet';
import { collectDependencies, validateAvailableDependencies, prepareDependencyDirectory } from './snippet-dependencies';
import { snippetKey } from './tablets/key';
import { LanguageTablet, TranslatedSnippet } from './tablets/tablets';
import { translateAll, TranslateAllResult } from './translate_all';
export interface RosettaTranslatorOptions {
/**
* Assemblies to use for fingerprinting
*
* The set of assemblies here are used to invalidate the cache. Any types that are
* used in snippets are looked up in this set of assemblies. If found, their type
* information is fingerprinted and compared to the type information at the time
* compilation of the cached sample. If different, this is considered to be a cache
* miss.
*
* You must use the same set of assemblies when generating and reading the cache
* file, otherwise the fingerprint is guaranteed to be different and the cache will
* be useless (e.g. if you generate the cache WITH assembly information but
* read it without, or vice versa).
*
* @default No assemblies.
*/
readonly assemblies?: spec.Assembly[];
/**
* Whether to include compiler diagnostics in the compilation results.
*
* @default false
*/
readonly includeCompilerDiagnostics?: boolean;
/**
* Allow reading dirty translations from cache
*
* @default false
*/
readonly allowDirtyTranslations?: boolean;
}
/**
* Entry point for consumers that want to translate code on-the-fly
*
* If you want to generate and translate code on-the-fly, in ways that cannot
* be achieved by the rosetta CLI, use this class.
*/
export class RosettaTranslator {
/**
* Tablet with fresh translations
*
* All new translations (not read from cache) are added to this tablet.
*/
public readonly tablet = new LanguageTablet();
public readonly cache = new LanguageTablet();
private readonly fingerprinter: TypeFingerprinter;
private readonly includeCompilerDiagnostics: boolean;
private readonly allowDirtyTranslations: boolean;
public constructor(options: RosettaTranslatorOptions = {}) {
this.fingerprinter = new TypeFingerprinter(options?.assemblies ?? []);
this.includeCompilerDiagnostics = options.includeCompilerDiagnostics ?? false;
this.allowDirtyTranslations = options.allowDirtyTranslations ?? false;
}
/**
* @deprecated use `addToCache` instead
*/
public async loadCache(fileName: string) {
try {
await this.cache.load(fileName);
} catch (e: any) {
logging.warn(`Error reading cache ${fileName}: ${e.message}`);
}
}
public async addToCache(filename: string) {
const tab = await LanguageTablet.fromOptionalFile(filename);
this.cache.addTablet(tab);
}
public addTabletsToCache(...tablets: LanguageTablet[]) {
for (const tab of tablets) {
this.cache.addTablet(tab);
}
}
public hasCache() {
return this.cache.count > 0;
}
/**
* For all the given snippets, try to read translations from the cache
*
* Will remove the cached snippets from the input array.
*/
public readFromCache(snippets: TypeScriptSnippet[], addToTablet = true, compiledOnly = false): ReadFromCacheResults {
const translations = new Array<TranslatedSnippet>();
const remaining = new Array<TypeScriptSnippet>();
let infusedCount = 0;
let dirtyCount = 0;
let dirtySourceCount = 0;
let dirtyTypesCount = 0;
let dirtyTranslatorCount = 0;
let dirtyDidntCompile = 0;
for (const snippet of snippets) {
const fromCache = tryReadFromCache(snippet, this.cache, this.fingerprinter, compiledOnly);
switch (fromCache.type) {
case 'hit':
if (addToTablet) {
this.tablet.addSnippet(fromCache.snippet);
}
translations.push(fromCache.snippet);
infusedCount += fromCache.infused ? 1 : 0;
break;
case 'dirty':
dirtyCount += 1;
dirtySourceCount += fromCache.dirtySource ? 1 : 0;
dirtyTranslatorCount += fromCache.dirtyTranslator ? 1 : 0;
dirtyTypesCount += fromCache.dirtyTypes ? 1 : 0;
dirtyDidntCompile += fromCache.dirtyDidntCompile ? 1 : 0;
if (this.allowDirtyTranslations) {
translations.push(fromCache.translation);
} else {
remaining.push(snippet);
}
break;
case 'miss':
remaining.push(snippet);
break;
}
}
return {
translations,
remaining,
infusedCount,
dirtyCount,
dirtySourceCount,
dirtyTranslatorCount,
dirtyTypesCount,
dirtyDidntCompile,
};
}
public async translateAll(snippets: TypeScriptSnippet[], addToTablet?: boolean): Promise<TranslateAllResult>;
public async translateAll(snippets: TypeScriptSnippet[], options?: TranslateAllOptions): Promise<TranslateAllResult>;
public async translateAll(
snippets: TypeScriptSnippet[],
optionsOrAddToTablet?: boolean | TranslateAllOptions,
): Promise<TranslateAllResult> {
const options =
optionsOrAddToTablet && typeof optionsOrAddToTablet === 'object'
? optionsOrAddToTablet
: { addToTablet: optionsOrAddToTablet };
const exampleDependencies = collectDependencies(snippets);
let compilationDirectory;
let cleanCompilationDir = false;
if (options?.compilationDirectory) {
// If the user provided a directory, we're going to trust-but-confirm.
await validateAvailableDependencies(options.compilationDirectory, exampleDependencies);
compilationDirectory = options.compilationDirectory;
} else {
compilationDirectory = await prepareDependencyDirectory(exampleDependencies);
cleanCompilationDir = true;
}
const origDir = process.cwd();
// Easiest way to get a fixed working directory (for sources) in is to chdir
process.chdir(compilationDirectory);
let result;
try {
result = await translateAll(snippets, this.includeCompilerDiagnostics);
} finally {
process.chdir(origDir);
if (cleanCompilationDir) {
await fs.rm(compilationDirectory, { force: true, recursive: true });
}
}
const fingerprinted = result.translatedSnippets.map((snippet) =>
snippet.withFingerprint(this.fingerprinter.fingerprintAll(snippet.fqnsReferenced())),
);
if (options?.addToTablet ?? true) {
for (const translation of fingerprinted) {
this.tablet.addSnippet(translation);
}
}
return {
translatedSnippets: fingerprinted,
diagnostics: result.diagnostics,
};
}
}
/**
* Try to find the translation for the given snippet in the given cache
*
* Rules for cacheability are:
* - id is the same (== visible source didn't change)
* - complete source is the same (== fixture didn't change)
* - all types involved have the same fingerprint (== API surface didn't change)
* - the versions of all translations match the versions on the available translators (== translator itself didn't change)
*
* For the versions check: we could have selectively picked some translations
* from the cache while performing others. However, since the big work is in
* parsing the TypeScript, and the rendering itself is peanutes (assumption), it
* doesn't really make a lot of difference. So, for simplification's sake,
* we'll regen all translations if there's at least one that's outdated.
*/
function tryReadFromCache(
sourceSnippet: TypeScriptSnippet,
cache: LanguageTablet,
fingerprinter: TypeFingerprinter,
compiledOnly: boolean,
): CacheHit {
const fromCache = cache.tryGetSnippet(snippetKey(sourceSnippet));
if (!fromCache) {
return { type: 'miss' };
}
// infused snippets won't pass the full source check or the fingerprinter
// but there is no reason to try to recompile it, so return cached snippet
// if there exists one.
if (isInfused(sourceSnippet)) {
return { type: 'hit', snippet: fromCache, infused: true };
}
const dirtySource = completeSource(sourceSnippet) !== fromCache.snippet.fullSource;
const dirtyTranslator = !Object.entries(TARGET_LANGUAGES).every(
([lang, translator]) => fromCache.snippet.translations?.[lang]?.version === translator.version,
);
const dirtyTypes = fingerprinter.fingerprintAll(fromCache.fqnsReferenced()) !== fromCache.snippet.fqnsFingerprint;
const dirtyDidntCompile = compiledOnly && !fromCache.snippet.didCompile;
if (dirtySource || dirtyTranslator || dirtyTypes || dirtyDidntCompile) {
return { type: 'dirty', translation: fromCache, dirtySource, dirtyTranslator, dirtyTypes, dirtyDidntCompile };
}
return { type: 'hit', snippet: fromCache, infused: false };
}
export type CacheHit =
| { readonly type: 'miss' }
| { readonly type: 'hit'; readonly snippet: TranslatedSnippet; readonly infused: boolean }
| {
readonly type: 'dirty';
readonly translation: TranslatedSnippet;
readonly dirtySource: boolean;
readonly dirtyTranslator: boolean;
readonly dirtyTypes: boolean;
readonly dirtyDidntCompile: boolean;
};
function isInfused(snippet: TypeScriptSnippet) {
return snippet.parameters?.infused !== undefined;
}
export interface ReadFromCacheResults {
/**
* Successful translations
*/
readonly translations: TranslatedSnippet[];
/**
* Successful but dirty hits
*/
readonly remaining: TypeScriptSnippet[];
/**
* How many successfully hit translations were infused
*/
readonly infusedCount: number;
readonly dirtyCount: number;
// Counts for dirtiness (a single snippet may be dirty for more than one reason)
readonly dirtySourceCount: number;
readonly dirtyTranslatorCount: number;
readonly dirtyTypesCount: number;
readonly dirtyDidntCompile: number;
}
export interface TranslateAllOptions {
/**
* @default - Create a temporary directory with all necessary packages
*/
readonly compilationDirectory?: string;
/**
* @default true
*/
readonly addToTablet?: boolean;
}