progmune-runtime 3.7.2 → 3.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,707 @@
1
+ "use strict";
2
+ /**
3
+ * C IR Extractor — pure-TS function extraction for the merged multi-language
4
+ * registry (extract-project-ir.ts LANGUAGE_EXTRACTORS). No child_process
5
+ * bridge, no native deps.
6
+ *
7
+ * Route: the registry "c" entry calls extractIRC; the result merges into the
8
+ * same FunctionInfo list as TypeScript/Python and flows into call-sequence
9
+ * (P4.6), SSG validation and the agent loop with no further rewiring.
10
+ *
11
+ * Extraction is a best-effort lexical parse of C89-style definitions:
12
+ * signatures (multi-line, static/inline/__attribute__, pointer/array params),
13
+ * direct calls (member calls yield the token before `(`), goto_<label>
14
+ * synthesis, @progmune/@protocol annotations and doc tags from comment blocks.
15
+ *
16
+ * Known limits (see docs/c-language-status.md):
17
+ * - Function-pointer dispatch is statically invisible (cf->close_one()
18
+ * yields close_one — the call NAME — but the callee is not resolvable).
19
+ * - Macros and K&R definitions are not parsed; C++ constructs out of scope.
20
+ * - No dataflow / pointer / CFG analysis (L3/L4 conclusions unchanged).
21
+ */
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.parseCSource = parseCSource;
57
+ exports.extractIRC = extractIRC;
58
+ const fs = __importStar(require("fs"));
59
+ const path = __importStar(require("path"));
60
+ // ── Constants ──
61
+ const C_EXTENSIONS = new Set([".c", ".h"]);
62
+ /** Walk skip set: engine languageToExtensions parity + "benchmarks" (self-host guard). */
63
+ const SKIP_DIRS = new Set([
64
+ "node_modules", "dist", "build", ".git", ".progmune_corpus",
65
+ "__pycache__", "venv", ".venv", "benchmarks",
66
+ ]);
67
+ /** Copied from sequence-extractor.ts:107-111 (kept local — decoupled benchmark path must not change). */
68
+ const C_KEYWORDS = new Set([
69
+ "if", "for", "while", "switch", "return", "sizeof", "typeof",
70
+ "goto", "break", "continue", "case", "default", "do", "else",
71
+ "struct", "union", "enum", "typedef", "extern", "volatile", "const",
72
+ ]);
73
+ /** Casts like `(void) fclose(f)` would otherwise register the type name as a call. */
74
+ const C_TYPE_NAMES = new Set([
75
+ "void", "int", "char", "float", "double", "long", "short",
76
+ "signed", "unsigned", "bool", "_Bool",
77
+ "size_t", "ssize_t", "int8_t", "uint8_t", "int16_t", "uint16_t",
78
+ "int32_t", "uint32_t", "int64_t", "uint64_t", "FILE", "time_t",
79
+ ]);
80
+ /**
81
+ * Balanced-paren scan starting at the position of `(`; returns the text
82
+ * inside the matching parens, or null if unbalanced. String/char-literal
83
+ * aware so `)` inside "…"/'…' does not terminate early.
84
+ */
85
+ function findBalancedParens(text, openIdx) {
86
+ let depth = 0;
87
+ let inStr = false;
88
+ let inCh = false;
89
+ for (let i = openIdx; i < text.length; i++) {
90
+ const c = text[i];
91
+ if (inStr) {
92
+ if (c === "\\")
93
+ i++;
94
+ else if (c === '"')
95
+ inStr = false;
96
+ continue;
97
+ }
98
+ if (inCh) {
99
+ if (c === "\\")
100
+ i++;
101
+ else if (c === "'")
102
+ inCh = false;
103
+ continue;
104
+ }
105
+ if (c === '"')
106
+ inStr = true;
107
+ else if (c === "'")
108
+ inCh = true;
109
+ else if (c === "(")
110
+ depth++;
111
+ else if (c === ")") {
112
+ depth--;
113
+ if (depth === 0)
114
+ return text.slice(openIdx + 1, i);
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+ /** Single-line doc tag capture (`@tag value` to end of line — Python `.` semantics). */
120
+ function docTag(text, tag) {
121
+ const m = new RegExp(`@${tag}\\s+(.+)$`, "m").exec(text);
122
+ return m ? m[1].trim() : undefined;
123
+ }
124
+ /** Split semantics mirroring tools/extract_ir.py + extract-ir.ts doc tags. */
125
+ function splitComma(s) {
126
+ return s.split(",").map((x) => x.trim()).filter(Boolean);
127
+ }
128
+ function splitWords(s) {
129
+ return s.split(/[,\s]+/).map((x) => x.trim()).filter(Boolean);
130
+ }
131
+ function splitUseWhen(s) {
132
+ return s.split(/[;;]/).map((x) => x.trim()).filter(Boolean);
133
+ }
134
+ /**
135
+ * Parse a comment block for @progmune/@protocol annotations and doc tags.
136
+ * Returns null when the comment carries no annotation fields (plain file
137
+ * headers must not attach to the first function).
138
+ */
139
+ function parseCAnnotation(text) {
140
+ // ── Decorator: @progmune(...) / @protocol(...) — balanced kwargs text ──
141
+ let protocol;
142
+ const dec = /@?(?:progmune|protocol)\s*\(/.exec(text);
143
+ if (dec) {
144
+ const openIdx = dec.index + dec[0].length - 1;
145
+ const inner = findBalancedParens(text, openIdx);
146
+ if (inner !== null) {
147
+ // kwargs regex mirrors tools/extract_ir.py:906-929
148
+ const kwargRe = /(\w+)\s*=\s*(?:(\[[^\]]*\])|"([^"]*)"|'([^']*)'|(\w+))/g;
149
+ const kwargs = {};
150
+ let m;
151
+ while ((m = kwargRe.exec(inner)) !== null) {
152
+ let value;
153
+ if (m[2] !== undefined) {
154
+ const quoted = m[2].match(/["']([^"']*)["']/g);
155
+ if (quoted && quoted.length)
156
+ value = quoted.map((q) => q.slice(1, -1));
157
+ else
158
+ value = m[2].replace(/^\[|\]$/g, "").split(",").map((s) => s.trim()).filter(Boolean);
159
+ }
160
+ else if (m[3] !== undefined)
161
+ value = [m[3]];
162
+ else if (m[4] !== undefined)
163
+ value = [m[4]];
164
+ else
165
+ value = [m[5]];
166
+ kwargs[m[1]] = value;
167
+ }
168
+ if (Object.keys(kwargs).length > 0) {
169
+ protocol = {
170
+ pre_states: kwargs["pre"] ?? kwargs["pre_states"] ?? [],
171
+ post_states: kwargs["post"] ?? kwargs["post_states"] ?? [],
172
+ };
173
+ if (kwargs["invalidate"] ?? kwargs["inv"])
174
+ protocol.invalidate = kwargs["invalidate"] ?? kwargs["inv"];
175
+ if (kwargs["namespace"]?.[0])
176
+ protocol.namespace = kwargs["namespace"][0];
177
+ }
178
+ }
179
+ }
180
+ // ── Doc tags (single-line captures, Python mirror) ──
181
+ const purpose = docTag(text, "purpose");
182
+ const description = docTag(text, "description");
183
+ const tagsRaw = docTag(text, "tags");
184
+ const requiresRaw = docTag(text, "requires");
185
+ const producesRaw = docTag(text, "produces");
186
+ const useWhenRaw = docTag(text, "useWhen");
187
+ const inputsRaw = docTag(text, "inputs");
188
+ const outputsRaw = docTag(text, "outputs");
189
+ const tags = tagsRaw ? splitComma(tagsRaw) : undefined;
190
+ const requires = requiresRaw ? splitWords(requiresRaw) : undefined;
191
+ const produces = producesRaw ? splitWords(producesRaw) : undefined;
192
+ const useWhen = useWhenRaw ? splitUseWhen(useWhenRaw) : undefined;
193
+ const inputs = inputsRaw ? splitComma(inputsRaw) : undefined;
194
+ const outputs = outputsRaw ? splitComma(outputsRaw) : undefined;
195
+ if (!protocol && !purpose && !description && !tags && !requires &&
196
+ !produces && !useWhen && !inputs && !outputs)
197
+ return null;
198
+ return { protocol, purpose, description, tags, requires, produces, useWhen, inputs, outputs };
199
+ }
200
+ /**
201
+ * Replace comment and string/char-literal content with spaces (1:1 column
202
+ * preservation) so brace counting and call regexes cannot be corrupted by
203
+ * braces or `name(` sequences inside comments or strings. State persists
204
+ * across lines.
205
+ */
206
+ function maskLine(line, state) {
207
+ const out = [];
208
+ let i = 0;
209
+ while (i < line.length) {
210
+ const c = line[i];
211
+ if (state.inBlock) {
212
+ if (c === "*" && line[i + 1] === "/") {
213
+ state.inBlock = false;
214
+ out.push(" ", " ");
215
+ i += 2;
216
+ }
217
+ else {
218
+ out.push(" ");
219
+ i++;
220
+ }
221
+ continue;
222
+ }
223
+ if (state.inString) {
224
+ if (c === "\\") {
225
+ out.push(" ", " ");
226
+ i += 2;
227
+ }
228
+ else if (c === '"') {
229
+ state.inString = false;
230
+ out.push(" ");
231
+ i++;
232
+ }
233
+ else {
234
+ out.push(" ");
235
+ i++;
236
+ }
237
+ continue;
238
+ }
239
+ if (state.inChar) {
240
+ if (c === "\\") {
241
+ out.push(" ", " ");
242
+ i += 2;
243
+ }
244
+ else if (c === "'") {
245
+ state.inChar = false;
246
+ out.push(" ");
247
+ i++;
248
+ }
249
+ else {
250
+ out.push(" ");
251
+ i++;
252
+ }
253
+ continue;
254
+ }
255
+ if (c === "/" && line[i + 1] === "*") {
256
+ state.inBlock = true;
257
+ out.push(" ", " ");
258
+ i += 2;
259
+ continue;
260
+ }
261
+ if (c === "/" && line[i + 1] === "/") {
262
+ while (i < line.length) {
263
+ out.push(" ");
264
+ i++;
265
+ }
266
+ break;
267
+ }
268
+ if (c === '"') {
269
+ state.inString = true;
270
+ out.push(" ");
271
+ i++;
272
+ continue;
273
+ }
274
+ if (c === "'") {
275
+ state.inChar = true;
276
+ out.push(" ");
277
+ i++;
278
+ continue;
279
+ }
280
+ out.push(c);
281
+ i++;
282
+ }
283
+ return out.join("");
284
+ }
285
+ // ── Params ──
286
+ /** Depth-aware top-level comma split (nested ()/[]/{} + string/char literals). */
287
+ function splitTopLevelParams(s) {
288
+ const out = [];
289
+ let depth = 0;
290
+ let start = 0;
291
+ let inStr = false;
292
+ let inCh = false;
293
+ for (let i = 0; i < s.length; i++) {
294
+ const c = s[i];
295
+ if (inStr) {
296
+ if (c === "\\")
297
+ i++;
298
+ else if (c === '"')
299
+ inStr = false;
300
+ continue;
301
+ }
302
+ if (inCh) {
303
+ if (c === "\\")
304
+ i++;
305
+ else if (c === "'")
306
+ inCh = false;
307
+ continue;
308
+ }
309
+ if (c === '"')
310
+ inStr = true;
311
+ else if (c === "'")
312
+ inCh = true;
313
+ else if (c === "(" || c === "[" || c === "{")
314
+ depth++;
315
+ else if (c === ")" || c === "]" || c === "}")
316
+ depth--;
317
+ else if (c === "," && depth === 0) {
318
+ out.push(s.slice(start, i).trim());
319
+ start = i + 1;
320
+ }
321
+ }
322
+ const last = s.slice(start).trim();
323
+ if (last)
324
+ out.push(last);
325
+ return out;
326
+ }
327
+ /**
328
+ * Parse one param segment → {name, type}.
329
+ * Precedence: bare "void" → null; "..." variadic; function pointer
330
+ * `(*name)`; otherwise the last identifier is the name (handles
331
+ * `const char* user`, `char buf[256]`, `unsigned long long n`).
332
+ */
333
+ function parseParamSegment(seg) {
334
+ if (seg === "void")
335
+ return null;
336
+ if (seg === "...")
337
+ return { name: "...", type: "..." };
338
+ const fp = seg.match(/\(\s*\*\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)/);
339
+ if (fp)
340
+ return { name: fp[1], type: seg.replace(/\s+/g, " ").trim() };
341
+ const m = seg.match(/^(.+?)\s+([a-zA-Z_][a-zA-Z0-9_]*)((?:\[[^\]]*\])*\s*)$/);
342
+ if (m)
343
+ return { name: m[2], type: m[1].replace(/\s+/g, " ").trim() };
344
+ return { name: seg.trim(), type: "" };
345
+ }
346
+ // ── Signature & body parsing ──
347
+ /**
348
+ * Strip GNU `__attribute__((...))` / MSVC `__declspec(...)` groups
349
+ * (balanced-paren aware) so the signature regex only deals with tokens.
350
+ * Returns the stripped text plus a char-position map back into the
351
+ * original string, so the `{` position can be re-located afterwards.
352
+ */
353
+ function stripAttributes(s) {
354
+ const re = /\b(?:__attribute__|__declspec)\s*\(/g;
355
+ let text = "";
356
+ const origIndex = [];
357
+ let last = 0;
358
+ let m;
359
+ while ((m = re.exec(s)) !== null) {
360
+ const openIdx = m.index + m[0].length - 1;
361
+ const inner = findBalancedParens(s, openIdx);
362
+ if (inner === null)
363
+ break;
364
+ const end = openIdx + inner.length + 2;
365
+ for (let k = last; k < m.index; k++) {
366
+ text += s[k];
367
+ origIndex.push(k);
368
+ }
369
+ last = end;
370
+ re.lastIndex = end;
371
+ }
372
+ for (let k = last; k < s.length; k++) {
373
+ text += s[k];
374
+ origIndex.push(k);
375
+ }
376
+ return { text, origIndex };
377
+ }
378
+ /**
379
+ * Function-candidate regex: `name(...) {` anchored at line start or after
380
+ * whitespace. NO return-type token loop — the v2 pattern's nested
381
+ * quantifiers backtracked exponentially on `name = some_long_ident(...)`
382
+ * style lines (44-char buffer → ~11s; token-split space is 2^k). Instead
383
+ * we iterate ALL candidates in the buffer (`g` flag), skip keyword /
384
+ * type-name candidates, and derive the return type from the buffer text
385
+ * BEFORE the chosen candidate. `[^;]*?` keeps the params scan bounded.
386
+ * Groups: 1 = name, 2 = params.
387
+ */
388
+ const FUNC_CAND_RE = /(?:^|\s)([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^;]*?)\)\s*\{/g;
389
+ /** struct/union/enum definition bodies must be skipped, not parsed as functions. */
390
+ const STRUCT_DECL_RE = /^(?:typedef\s+)?(?:struct|union|enum)\b/;
391
+ /** Call-name + goto extraction filters. */
392
+ const CALL_RE = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
393
+ const GOTO_RE = /\bgoto\s+([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
394
+ function isFilteredCall(name) {
395
+ return C_KEYWORDS.has(name) || C_TYPE_NAMES.has(name) || name.startsWith("__");
396
+ }
397
+ // ── Conditional compilation ──
398
+ /**
399
+ * Strip `#if 0` … `#endif` dead regions (dead lines replaced with "" so
400
+ * line indices stay 1:1 for the annotation/mask/parse passes). Real-world
401
+ * C (openssl etc.) has dead blocks with unbalanced braces that would
402
+ * corrupt body brace counting and produce phantom top-level functions.
403
+ *
404
+ * Only a BARE `#if 0` (single token, optional trailing comment) enters a
405
+ * dead region — `#if 0 || X` is not evaluated and treated as active.
406
+ * Nested `#if` inside a dead region stays dead (`#else`/`#elif` included);
407
+ * unmatched `#endif` is ignored.
408
+ */
409
+ function stripDeadConditionalBlocks(rawLines) {
410
+ const out = rawLines.slice();
411
+ // 每层条件状态:false = 死区;true = 活区(未求值条件按活区处理)
412
+ const stack = [];
413
+ const inDeadRegion = () => stack.includes(false);
414
+ const DEAD_IF_RE = /^#\s*if\s+0\s*(?:\/\/[^\n]*|\/\*.*?\*\/)?$/;
415
+ const IF_RE = /^#\s*if(n?def)?\b/;
416
+ const ELIF_ELSE_RE = /^#\s*(?:elif|else)\b/;
417
+ const ENDIF_RE = /^#\s*endif\b/;
418
+ for (let i = 0; i < out.length; i++) {
419
+ const t = out[i].trim();
420
+ if (DEAD_IF_RE.test(t))
421
+ stack.push(false);
422
+ else if (IF_RE.test(t))
423
+ stack.push(true);
424
+ else if (ELIF_ELSE_RE.test(t)) { /* 保持当前层状态:死区内仍死,活区内不翻转 */ }
425
+ else if (ENDIF_RE.test(t)) {
426
+ if (stack.length > 0)
427
+ stack.pop();
428
+ }
429
+ if (inDeadRegion())
430
+ out[i] = "";
431
+ }
432
+ return out;
433
+ }
434
+ // ── Top-level parse ──
435
+ /**
436
+ * Parse one C source file into FunctionInfo entries.
437
+ *
438
+ * Two passes over the raw lines (ordering is load-bearing):
439
+ * 1. Annotation pass on RAW lines — collects comment blocks above
440
+ * definitions (masking would erase them).
441
+ * 2. Parse pass on MASKED lines — signature scan + brace counting + calls.
442
+ *
443
+ * @param content - C source text
444
+ * @param filePath - Absolute path of the source file
445
+ * @param projectRoot - Absolute project root (for relative `file` field)
446
+ */
447
+ function parseCSource(content, filePath, projectRoot) {
448
+ // 条件编译死代码剥离(#if 0 块内花括号不平衡是真实仓库的普遍陷阱)
449
+ const rawLines = stripDeadConditionalBlocks(content.split("\n"));
450
+ // ── Pass 1: annotation pass (raw lines) — Map<line index, CAnnotation> ──
451
+ const annotations = new Map();
452
+ {
453
+ let pending = null;
454
+ let inBlock = false;
455
+ for (let i = 0; i < rawLines.length; i++) {
456
+ let line = rawLines[i];
457
+ if (inBlock) {
458
+ const end = line.indexOf("*/");
459
+ if (end === -1) {
460
+ pending?.push(line.trim().replace(/^\*+\s?/, "").trim());
461
+ continue;
462
+ }
463
+ pending?.push(line.slice(0, end).trim().replace(/^\*+\s?/, "").trim());
464
+ inBlock = false;
465
+ line = line.slice(end + 2);
466
+ }
467
+ const rest = line.trim();
468
+ if (rest === "")
469
+ continue; // blank line keeps the pending comment attached
470
+ if (rest.startsWith("//")) {
471
+ if (!pending)
472
+ pending = [];
473
+ pending.push(rest.slice(2).trim());
474
+ continue;
475
+ }
476
+ if (rest.startsWith("/*")) {
477
+ if (!pending)
478
+ pending = [];
479
+ const end = rest.indexOf("*/", 2);
480
+ if (end === -1) {
481
+ pending.push(rest.slice(2).trim());
482
+ inBlock = true;
483
+ }
484
+ else {
485
+ pending.push(rest.slice(2, end).trim());
486
+ }
487
+ continue;
488
+ }
489
+ // code line: terminate pending comment (plain headers are discarded by parseCAnnotation → null)
490
+ if (pending && pending.length) {
491
+ const ann = parseCAnnotation(pending.join("\n"));
492
+ if (ann)
493
+ annotations.set(i, ann);
494
+ pending = null;
495
+ }
496
+ }
497
+ }
498
+ // ── Pass 2: masking ──
499
+ const maskedLines = [];
500
+ {
501
+ const state = { inBlock: false, inString: false, inChar: false };
502
+ for (const line of rawLines)
503
+ maskedLines.push(maskLine(line, state));
504
+ }
505
+ // ── Pass 3: signature scan + body scan ──
506
+ const fns = [];
507
+ const relFile = path.relative(projectRoot, filePath) || path.basename(filePath);
508
+ let i = 0;
509
+ while (i < rawLines.length) {
510
+ while (i < rawLines.length && maskedLines[i].trim() === "")
511
+ i++;
512
+ if (i >= rawLines.length)
513
+ break;
514
+ const bufStart = i;
515
+ const bufLineIdx = [];
516
+ const bufLineStarts = [];
517
+ let buf = "";
518
+ while (i < rawLines.length) {
519
+ const line = maskedLines[i].trim();
520
+ const sep = buf ? 1 : 0;
521
+ bufLineStarts.push(buf.length + sep);
522
+ bufLineIdx.push(i);
523
+ buf += (sep ? " " : "") + line;
524
+ if (line.includes("{") || line.includes(";"))
525
+ break;
526
+ i++;
527
+ }
528
+ if (i >= rawLines.length)
529
+ break;
530
+ // 预处理行只跳过自身,不回退到缓冲区末尾——否则 `#endif\nvoid f() {...}`
531
+ // 这类相邻行会被整个缓冲区吞掉,函数定义随之丢失
532
+ if (buf.trimStart().startsWith("#")) {
533
+ i = bufStart + 1;
534
+ continue;
535
+ }
536
+ // ── function definition? ──
537
+ const { text: cleanBuf, origIndex } = stripAttributes(buf);
538
+ let sig = null;
539
+ {
540
+ // 遍历缓冲区内所有 `name(...) {` 候选,跳过关键字/类型名候选
541
+ // (`int authenticate(...)` 的 int 是类型前缀,authenticate 才是函数名)
542
+ FUNC_CAND_RE.lastIndex = 0;
543
+ let cand;
544
+ while ((cand = FUNC_CAND_RE.exec(cleanBuf)) !== null) {
545
+ if (!isFilteredCall(cand[1])) {
546
+ sig = cand;
547
+ break;
548
+ }
549
+ }
550
+ }
551
+ if (sig) {
552
+ const name = sig[1];
553
+ const braceInClean = sig.index + sig[0].length - 1;
554
+ const braceIdx = origIndex[braceInClean] ?? (braceInClean + (buf.length - cleanBuf.length));
555
+ // locate the buffered line containing the brace
556
+ let lineK = bufLineIdx.length - 1;
557
+ while (lineK > 0 && bufLineStarts[lineK] > braceIdx)
558
+ lineK--;
559
+ const braceInLine = braceIdx - bufLineStarts[lineK];
560
+ const rawIdxK = bufLineIdx[lineK];
561
+ const maskedK = maskedLines[rawIdxK];
562
+ const trimStart = maskedK.indexOf(maskedK.trim());
563
+ // body text on line K starts right after the opening brace
564
+ const bodyStart = trimStart + braceInLine + 1;
565
+ // ── body scan: comment/string-aware brace counting + calls ──
566
+ const calls = [];
567
+ // 不去重:状态机验证中重复调用有语义(double close / 重复 logout 正是
568
+ // 要抓的违规形态);与 TS/Python 提取器(ts-morph/ast 均保留重复)一致
569
+ const pushCall = (c) => {
570
+ if (isFilteredCall(c) || c === name)
571
+ return;
572
+ calls.push(c);
573
+ };
574
+ let depth = 1;
575
+ let j = rawIdxK;
576
+ let text = maskedK.slice(bodyStart);
577
+ let finished = false;
578
+ while (j < rawLines.length && !finished) {
579
+ if (!text.trimStart().startsWith("#")) {
580
+ // find how much of this line belongs to the body (until depth hits 0)
581
+ let stop = text.length;
582
+ for (let k = 0; k < text.length; k++) {
583
+ const c = text[k];
584
+ if (c === "{")
585
+ depth++;
586
+ else if (c === "}") {
587
+ depth--;
588
+ if (depth === 0) {
589
+ stop = k;
590
+ finished = true;
591
+ break;
592
+ }
593
+ }
594
+ }
595
+ const prefix = text.slice(0, stop);
596
+ let m;
597
+ CALL_RE.lastIndex = 0;
598
+ while ((m = CALL_RE.exec(prefix)) !== null)
599
+ pushCall(m[1]);
600
+ GOTO_RE.lastIndex = 0;
601
+ while ((m = GOTO_RE.exec(prefix)) !== null)
602
+ pushCall(`goto_${m[1]}`);
603
+ }
604
+ j++;
605
+ if (!finished && j < rawLines.length)
606
+ text = maskedLines[j];
607
+ }
608
+ i = j;
609
+ // ── assemble FunctionInfo ──
610
+ const ann = annotations.get(bufStart);
611
+ // 返回类型 = 候选名之前的缓冲区文本(static/inline/属性前缀一并含在内)
612
+ const retPrefix = cleanBuf.slice(0, sig.index);
613
+ const isStatic = /^static\b/.test(retPrefix.trim());
614
+ const returnType = retPrefix
615
+ .replace(/^static\s+/, "")
616
+ .replace(/^inline\s+/, "")
617
+ .replace(/^OSSL_DEPRECATEDIN\S*\s*/, "")
618
+ .replace(/\s+/g, " ")
619
+ .trim() || "any";
620
+ const params = splitTopLevelParams(sig[2])
621
+ .map((seg) => parseParamSegment(seg))
622
+ .filter((p) => p !== null);
623
+ fns.push({
624
+ name,
625
+ params,
626
+ returnType,
627
+ file: relFile,
628
+ calls,
629
+ exported: !isStatic,
630
+ external: false, // isProjectFn contract (call-sequence.ts)
631
+ description: ann?.description ?? ann?.purpose ?? "",
632
+ purpose: ann?.purpose ?? "",
633
+ tags: ann?.tags?.length ? ann.tags : ["c"],
634
+ inputs: ann?.inputs ?? [],
635
+ outputs: ann?.outputs ?? [],
636
+ requires: ann?.requires ?? [],
637
+ produces: ann?.produces ?? [],
638
+ useWhen: ann?.useWhen ?? [],
639
+ protocol: ann?.protocol,
640
+ });
641
+ continue;
642
+ }
643
+ // ── struct/union/enum definition body — consume to the terminating `;` ──
644
+ if (STRUCT_DECL_RE.test(buf.trim()) && buf.includes("{")) {
645
+ i++;
646
+ while (i < rawLines.length && !buf.trimEnd().endsWith(";")) {
647
+ buf += " " + maskedLines[i].trim();
648
+ i++;
649
+ }
650
+ continue;
651
+ }
652
+ // plain declaration / prototype / anything else — skip one line
653
+ i++;
654
+ }
655
+ return fns;
656
+ }
657
+ // ── Project-level API ──
658
+ /** Bounded recursive walk collecting absolute paths of .c/.h files. */
659
+ function collectCFiles(projectRoot) {
660
+ const out = [];
661
+ const stack = [projectRoot];
662
+ const seen = new Set();
663
+ while (stack.length > 0) {
664
+ const dir = stack.pop();
665
+ if (seen.has(dir))
666
+ continue;
667
+ seen.add(dir);
668
+ let entries;
669
+ try {
670
+ entries = fs.readdirSync(dir, { withFileTypes: true });
671
+ }
672
+ catch {
673
+ continue;
674
+ }
675
+ for (const e of entries) {
676
+ const full = path.join(dir, e.name);
677
+ if (e.isDirectory()) {
678
+ if (!SKIP_DIRS.has(e.name) && !e.name.startsWith("."))
679
+ stack.push(full);
680
+ }
681
+ else if (C_EXTENSIONS.has(path.extname(e.name))) {
682
+ out.push(full);
683
+ }
684
+ }
685
+ }
686
+ return out;
687
+ }
688
+ /**
689
+ * Extract C function IR from a project tree (.c/.h files, vendored dirs
690
+ * skipped). Per-file failures are logged and skipped (best-effort parity
691
+ * with the registry's per-language isolation).
692
+ *
693
+ * @param projectRoot - Absolute path to project root
694
+ * @returns FunctionInfo[] merged across all C files
695
+ */
696
+ function extractIRC(projectRoot) {
697
+ const out = [];
698
+ for (const f of collectCFiles(projectRoot)) {
699
+ try {
700
+ out.push(...parseCSource(fs.readFileSync(f, "utf-8"), f, projectRoot));
701
+ }
702
+ catch (err) {
703
+ console.error(`[extractIRC] ${f} 解析失败: ${err?.message || err}`);
704
+ }
705
+ }
706
+ return out;
707
+ }