Skip to content

Commit 1fbaabc

Browse files
refactor: compiler-cmd (clice-io#92)
1 parent 9e81427 commit 1fbaabc

44 files changed

Lines changed: 5217 additions & 2585 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/src/cmd/archiver-cmd.ts

Lines changed: 103 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,41 @@
1-
import type { Analysis as AnyAnalysis, Analyzer } from "./model.js";
2-
import { Analysis } from "./model.js";
1+
import { fromThrowable, type Result } from "../neverthrow/index.js";
2+
import { Analysis, AnalysisError, Analyzer, AnalyzedData } from "./model.js";
3+
4+
export class ArchiverNotRecognizedError extends AnalysisError {
5+
readonly kind = "archiver-not-recognized" as const;
6+
}
7+
8+
export class ArchiverUnsupportedError extends AnalysisError {
9+
readonly kind = "archiver-unsupported" as const;
10+
}
11+
12+
export class ArchiverParseError extends AnalysisError {
13+
readonly kind = "archiver-parse" as const;
14+
}
15+
16+
export type ArchiverAnalysisError =
17+
| ArchiverNotRecognizedError
18+
| ArchiverUnsupportedError
19+
| ArchiverParseError;
20+
21+
function toArchiverAnalysisError(
22+
value: unknown,
23+
context: string,
24+
): ArchiverAnalysisError {
25+
if (
26+
value instanceof ArchiverNotRecognizedError ||
27+
value instanceof ArchiverUnsupportedError ||
28+
value instanceof ArchiverParseError
29+
) {
30+
return value;
31+
}
32+
33+
if (value instanceof Error) {
34+
return new ArchiverParseError(`${context}: ${value.message}`);
35+
}
36+
37+
return new ArchiverParseError(`${context}: ${String(value)}`);
38+
}
339

440
/**
541
* Supported single-letter archive operations.
@@ -41,16 +77,15 @@ export type ArchiverOperation =
4177
*/
4278
export type ArchiverExe = "ar" | "llvm-ar" | "gcc-ar";
4379

44-
type ArchiverModel = {
45-
exe: ArchiverExe;
80+
export type ArchiverModel = {
4681
operation: ArchiverOperation;
4782
modifiers: string[];
4883
thin: boolean;
4984
archive?: string;
5085
members: string[];
5186
scriptMode: boolean;
52-
consume: string[];
53-
produce: string[];
87+
reads: string[];
88+
writes: string[];
5489
};
5590

5691
const ARCHIVER_EXE_NAMES = new Set<ArchiverExe>(["ar", "llvm-ar", "gcc-ar"]);
@@ -139,25 +174,28 @@ function parseOperationToken(
139174
};
140175
}
141176

142-
function analyzeArchiverModel(
143-
cmd: readonly string[],
144-
): ArchiverModel | undefined {
145-
if (cmd.length === 0) {
146-
return undefined;
177+
function analyzeArchiverModel(command: AnalyzedData): ArchiverModel {
178+
if (command.argv.length === 0) {
179+
throw new ArchiverNotRecognizedError("empty argv");
147180
}
148181

149-
const exe = exeStem(cmd[0]);
182+
const exe = exeStem(command.exe);
150183
if (!isArchiverExe(exe)) {
151-
return undefined;
184+
throw new ArchiverNotRecognizedError(
185+
`not a recognized archiver executable: ${command.exe}`,
186+
);
152187
}
153188

189+
const cmd = command.argv;
154190
let index = 1;
155191
let thin = false;
156192

157193
while (index < cmd.length) {
158194
const token = cmd[index];
159195
if (token === "-M") {
160-
return undefined;
196+
throw new ArchiverUnsupportedError(
197+
"archiver MRI script mode is not supported",
198+
);
161199
}
162200
if (token === "--thin" || token === "-T") {
163201
thin = true;
@@ -192,145 +230,108 @@ function analyzeArchiverModel(
192230
continue;
193231
}
194232
if (isOptionToken(token)) {
195-
return undefined;
233+
throw new ArchiverUnsupportedError(
234+
`unsupported archiver option: ${token}`,
235+
);
196236
}
197237
break;
198238
}
199239

200240
if (index >= cmd.length) {
201-
return undefined;
241+
throw new ArchiverUnsupportedError(
242+
"archiver command has no operation token",
243+
);
202244
}
203245

204246
const parsedOperation = parseOperationToken(cmd[index]);
205247
if (parsedOperation === undefined) {
206-
return undefined;
248+
throw new ArchiverUnsupportedError(
249+
`unsupported archiver operation token: ${cmd[index]}`,
250+
);
207251
}
208252
++index;
209253

210254
const archive = cmd[index];
211255
if (archive === undefined) {
212-
return undefined;
256+
throw new ArchiverUnsupportedError("archiver command has no archive path");
213257
}
214258
++index;
215259

216260
if (!MODELED_ARCHIVER_OPERATIONS.has(parsedOperation.operation)) {
217-
return undefined;
261+
throw new ArchiverUnsupportedError(
262+
`archiver operation is not modeled: ${parsedOperation.operation}`,
263+
);
218264
}
219265

220266
const members = cmd.slice(index);
221-
const produce =
267+
const writes =
222268
parsedOperation.operation === ArchiverOperation.QuickAppend ||
223269
parsedOperation.operation === ArchiverOperation.ReplaceOrInsert
224270
? [archive]
225271
: [];
226-
const consume =
272+
const reads =
227273
parsedOperation.operation === ArchiverOperation.Print ||
228274
parsedOperation.operation === ArchiverOperation.Table
229275
? [archive]
230276
: [...members];
231277

232278
return {
233-
exe,
234279
operation: parsedOperation.operation,
235280
modifiers: parsedOperation.modifiers,
236281
thin,
237282
archive,
238283
members: [...members],
239284
scriptMode: false,
240-
consume,
241-
produce,
285+
reads,
286+
writes,
242287
};
243288
}
244289

245-
export class ArchiverAnalysis extends Analysis<ArchiverExe> {
246-
/**
247-
* Stable registry key for the archiver analyzer.
248-
*
249-
* @example
250-
* ```ts
251-
* cmd.defaultRegistry.unregister(cmd.ArchiverAnalysis.key);
252-
* ```
253-
*/
254-
static readonly key = "archiver";
255-
256-
/**
257-
* Checks whether a command looks like a supported archiver invocation.
258-
*
259-
* @example
260-
* ```ts
261-
* const ok = cmd.ArchiverAnalysis.supports(["llvm-ar", "rcs", "liba.a", "a.o"]);
262-
* ```
263-
*/
264-
static supports(cmd: readonly string[]): boolean {
265-
return analyzeArchiverModel(cmd) !== undefined;
266-
}
267-
268-
/**
269-
* Analyzes an archiver command.
270-
*
271-
* @example
272-
* ```ts
273-
* const analysis = cmd.ArchiverAnalysis.analyze([
274-
* "llvm-ar",
275-
* "rcs",
276-
* "liba.a",
277-
* "a.o",
278-
* ]);
279-
* ```
280-
*/
281-
static analyze(cmd: readonly string[]): ArchiverAnalysis | undefined {
282-
return ArchiverAnalysis.supports(cmd)
283-
? new ArchiverAnalysis(cmd)
284-
: undefined;
285-
}
286-
287-
/**
288-
* Narrows a generic analysis back to an archiver analysis.
289-
*
290-
* @example
291-
* ```ts
292-
* const analysis = cmd.ArchiverAnalysis.from(cmd.analyze(["ar", "rcs", "liba.a", "a.o"]));
293-
* ```
294-
*/
295-
static from(analysis: AnyAnalysis | undefined): ArchiverAnalysis | undefined {
296-
return analysis instanceof ArchiverAnalysis ? analysis : undefined;
297-
}
298-
290+
export class ArchiverAnalysis extends Analysis {
291+
/** Discriminator for command analysis unions. */
292+
readonly kind = "archiver" as const;
299293
/** The parsed archive operation. */
300294
readonly operation: ArchiverOperation;
301295
/** Extra modifier letters attached to the operation token. */
302-
readonly modifiers: string[];
296+
readonly modifiers: readonly string[];
303297
/** Whether thin-archive mode was requested. */
304298
readonly thin: boolean;
305299
/** Archive file path when the command syntax provides one. */
306300
readonly archive?: string;
307301
/** Member file paths listed after the archive path. */
308-
readonly members: string[];
302+
readonly members: readonly string[];
309303
/** Whether GNU MRI script mode was requested. */
310304
readonly scriptMode: boolean;
311305

312-
/**
313-
* Creates an archiver analysis from raw argv.
314-
*
315-
* @example
316-
* ```ts
317-
* const analysis = new cmd.ArchiverAnalysis(["ar", "rcs", "liba.a", "a.o"]);
318-
* ```
319-
*/
320-
constructor(cmd: readonly string[]) {
321-
const resolved = analyzeArchiverModel(cmd);
322-
if (resolved === undefined) {
323-
throw new Error("archiver command analysis required");
324-
}
325-
326-
super(resolved.exe, resolved.consume, resolved.produce);
327-
this.operation = resolved.operation;
328-
this.modifiers = [...resolved.modifiers];
329-
this.thin = resolved.thin;
330-
this.archive = resolved.archive;
331-
this.members = [...resolved.members];
332-
this.scriptMode = resolved.scriptMode;
306+
constructor(model: ArchiverModel, command: AnalyzedData) {
307+
super({
308+
exe: command.exe,
309+
argv: command.argv,
310+
reads: model.reads,
311+
writes: model.writes,
312+
edges: model.writes.map((output) => ({
313+
output,
314+
inputs: [...model.reads],
315+
})),
316+
});
317+
this.operation = model.operation;
318+
this.modifiers = [...model.modifiers];
319+
this.thin = model.thin;
320+
this.archive = model.archive;
321+
this.members = [...model.members];
322+
this.scriptMode = model.scriptMode;
333323
}
334324
}
335325

336-
const _archiverAnalyzerCheck: Analyzer<ArchiverAnalysis> = ArchiverAnalysis;
326+
export class ArchiverAnalyzer extends Analyzer {
327+
readonly kind = "archiver" as const;
328+
329+
analyze(
330+
command: AnalyzedData,
331+
): Result<ArchiverAnalysis, ArchiverAnalysisError> {
332+
return fromThrowable(
333+
() => new ArchiverAnalysis(analyzeArchiverModel(command), command),
334+
(error) => toArchiverAnalysisError(error, "archiver analysis failed"),
335+
)();
336+
}
337+
}

api/src/cmd/cdb-manager.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import * as fs from "../fs.js";
22
import * as io from "../io.js";
33

4+
export class CDBError extends Error {
5+
constructor(message: string) {
6+
super(message);
7+
this.name = new.target.name;
8+
}
9+
}
10+
11+
export class CDBValidationError extends CDBError {}
12+
13+
export class CDBFileError extends CDBError {}
14+
415
/**
516
* A single compile_commands.json entry.
617
*
@@ -54,36 +65,44 @@ function readEntireText(path: string): string {
5465

5566
function asItem(value: unknown, context: string): CDBItem {
5667
if (!isRecord(value)) {
57-
throw new Error(`${context}: expected object item`);
68+
throw new CDBValidationError(`${context}: expected object item`);
5869
}
5970

6071
const directory = value.directory;
6172
if (typeof directory !== "string" || directory.length === 0) {
62-
throw new Error(`${context}: "directory" must be a non-empty string`);
73+
throw new CDBValidationError(
74+
`${context}: "directory" must be a non-empty string`,
75+
);
6376
}
6477

6578
const file = value.file;
6679
if (typeof file !== "string" || file.length === 0) {
67-
throw new Error(`${context}: "file" must be a non-empty string`);
80+
throw new CDBValidationError(
81+
`${context}: "file" must be a non-empty string`,
82+
);
6883
}
6984

7085
const command = value.command;
7186
if (command !== undefined && typeof command !== "string") {
72-
throw new Error(`${context}: "command" must be a string`);
87+
throw new CDBValidationError(`${context}: "command" must be a string`);
7388
}
7489

7590
const argumentsValue = value.arguments;
7691
if (argumentsValue !== undefined && !isStringList(argumentsValue)) {
77-
throw new Error(`${context}: "arguments" must be a string array`);
92+
throw new CDBValidationError(
93+
`${context}: "arguments" must be a string array`,
94+
);
7895
}
7996

8097
if (command === undefined && argumentsValue === undefined) {
81-
throw new Error(`${context}: expected "command" or "arguments"`);
98+
throw new CDBValidationError(
99+
`${context}: expected "command" or "arguments"`,
100+
);
82101
}
83102

84103
const output = value.output;
85104
if (output !== undefined && typeof output !== "string") {
86-
throw new Error(`${context}: "output" must be a string`);
105+
throw new CDBValidationError(`${context}: "output" must be a string`);
87106
}
88107

89108
const item: CDBItem = {
@@ -134,7 +153,7 @@ function readItemsFromPath(path: string): CDBItem[] {
134153
return [];
135154
}
136155
if (!fs.isFile(path)) {
137-
throw new Error(`CDB path is not a file: ${path}`);
156+
throw new CDBFileError(`CDB path is not a file: ${path}`);
138157
}
139158

140159
const raw = readEntireText(path).trim();
@@ -144,7 +163,7 @@ function readItemsFromPath(path: string): CDBItem[] {
144163

145164
const parsed: unknown = JSON.parse(raw);
146165
if (!Array.isArray(parsed)) {
147-
throw new Error(`CDB file must contain a JSON array: ${path}`);
166+
throw new CDBValidationError(`CDB file must contain a JSON array: ${path}`);
148167
}
149168

150169
return parsed.map((item, index) => asItem(item, `${path}[${index}]`));

0 commit comments

Comments
 (0)