Skip to content

Commit c7bc265

Browse files
committed
feat: add string constant loading
1 parent 99d09cf commit c7bc265

1 file changed

Lines changed: 169 additions & 0 deletions

File tree

src/constants.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import type { SyntaxNode } from "tree-sitter";
2+
import { childForField, parseCpp } from "./cpp-ast.js";
3+
import { CountingSemaphore } from "./concurrency.js";
4+
import { readTextFile } from "./fs.js";
5+
import { listSourceFiles, resolveRawGithubUrl } from "./source.js";
6+
7+
export async function loadStringConstants(
8+
rootDir: string,
9+
sourceBranch: string,
10+
): Promise<ReadonlyMap<string, string>> {
11+
const candidateFiles = await listSourceFiles(sourceBranch, rootDir, [".h", ".hpp", ".cpp"]);
12+
const semaphore = new CountingSemaphore(8);
13+
const sourceTexts = await Promise.all(
14+
candidateFiles.map(async (filePath) => {
15+
await semaphore.acquire();
16+
try {
17+
return await readTextFile(resolveRawGithubUrl(sourceBranch, filePath));
18+
} finally {
19+
semaphore.release();
20+
}
21+
}),
22+
);
23+
24+
const allEntries = sourceTexts.flatMap(scanConstants);
25+
return new Map(
26+
[...allEntries].reverse().map(({ name, value }): [string, string] => [name, value]),
27+
);
28+
}
29+
30+
interface ConstantEntry {
31+
readonly name: string;
32+
readonly value: string;
33+
}
34+
35+
interface ScanAccumulator {
36+
readonly pendingLines: readonly string[];
37+
readonly entries: readonly ConstantEntry[];
38+
}
39+
40+
function scanConstants(sourceText: string): readonly ConstantEntry[] {
41+
return sourceText
42+
.replaceAll("\r\n", "\n")
43+
.split("\n")
44+
.reduce<ScanAccumulator>(
45+
(acc, line) => scanConstantLine(line, acc),
46+
{ pendingLines: [], entries: [] },
47+
).entries;
48+
}
49+
50+
function scanConstantLine(line: string, acc: ScanAccumulator): ScanAccumulator {
51+
const trimmedLine = line.trim();
52+
if (trimmedLine.length === 0) {
53+
return { pendingLines: [], entries: acc.entries };
54+
}
55+
56+
if (trimmedLine.startsWith("#define")) {
57+
const entry = parseDefineConstant(trimmedLine);
58+
return {
59+
pendingLines: [],
60+
entries: entry !== undefined ? [...acc.entries, entry] : acc.entries,
61+
};
62+
}
63+
64+
const pendingLines = [...acc.pendingLines, trimmedLine];
65+
if (!trimmedLine.endsWith(";")) {
66+
return { pendingLines, entries: acc.entries };
67+
}
68+
69+
const entry = parseDeclarationConstant(pendingLines.join(" "));
70+
return {
71+
pendingLines: [],
72+
entries: entry !== undefined ? [...acc.entries, entry] : acc.entries,
73+
};
74+
}
75+
76+
function parseDefineConstant(sourceLine: string): ConstantEntry | undefined {
77+
const rootNode = parseCpp(`${sourceLine}\n`);
78+
const defineNode = rootNode.namedChildren.find(
79+
(child) => child.type === "preproc_def",
80+
);
81+
if (defineNode === undefined) {
82+
return undefined;
83+
}
84+
85+
const nameNode = defineNode.namedChildren.find(
86+
(child) => child.type === "identifier",
87+
);
88+
const valueNode = childForField(defineNode, "value");
89+
const name = nameNode?.text;
90+
const value =
91+
valueNode === undefined ? undefined : extractStringLiteral(valueNode.text);
92+
if (name === undefined || value === undefined) {
93+
return undefined;
94+
}
95+
return { name, value };
96+
}
97+
98+
function parseDeclarationConstant(sourceLine: string): ConstantEntry | undefined {
99+
const rootNode = parseCpp(`${sourceLine}\n`);
100+
const declarationNode = rootNode.namedChildren.find(
101+
(child) => child.type === "declaration",
102+
);
103+
if (declarationNode === undefined) {
104+
return undefined;
105+
}
106+
107+
const initDeclaratorNode = childForField(declarationNode, "declarator");
108+
if (
109+
initDeclaratorNode === undefined ||
110+
initDeclaratorNode.type !== "init_declarator"
111+
) {
112+
return undefined;
113+
}
114+
115+
const declaratorNode = childForField(initDeclaratorNode, "declarator");
116+
const valueNode = childForField(initDeclaratorNode, "value");
117+
const name = extractDeclaratorName(declaratorNode);
118+
const value =
119+
valueNode === undefined ? undefined : extractStringLiteral(valueNode.text);
120+
if (name === undefined || value === undefined) {
121+
return undefined;
122+
}
123+
return { name, value };
124+
}
125+
126+
function extractDeclaratorName(
127+
node: SyntaxNode | undefined,
128+
): string | undefined {
129+
if (node === undefined) {
130+
return undefined;
131+
}
132+
133+
if (node.type === "identifier" || node.type === "field_identifier") {
134+
return node.text;
135+
}
136+
137+
const nestedDeclarator = childForField(node, "declarator");
138+
if (nestedDeclarator !== undefined) {
139+
return extractDeclaratorName(nestedDeclarator);
140+
}
141+
142+
for (const child of node.namedChildren) {
143+
const name = extractDeclaratorName(child);
144+
if (name !== undefined) {
145+
return name;
146+
}
147+
}
148+
149+
return undefined;
150+
}
151+
152+
function extractStringLiteral(value: string): string | undefined {
153+
const trimmedValue = value.trim();
154+
return trimmedValue.startsWith('"') && trimmedValue.endsWith('"')
155+
? trimmedValue.slice(1, trimmedValue.length - 1)
156+
: undefined;
157+
}
158+
159+
export function resolveConstantValue(
160+
token: string,
161+
constants: ReadonlyMap<string, string>,
162+
): string | undefined {
163+
const trimmedToken = token.trim();
164+
const literalValue = extractStringLiteral(trimmedToken);
165+
if (literalValue !== undefined) {
166+
return literalValue;
167+
}
168+
return constants.get(trimmedToken);
169+
}

0 commit comments

Comments
 (0)