Files
codegraph/codegraph-kernel/src/cfnptr.rs
T
69ea438bac perf(kernel): cFnPtr native extraction sweep — step 2, pass 230→151s across the arc (§7a.10) (#1365)
Task #5 step 2. The fuse-then-link refactor (#1364) left the extraction
sweep as a clean per-file boundary: raw text in → collected facts out.
This ports that sweep to the native kernel: `cfnptr_scan_files`
(codegraph-kernel/src/cfnptr.rs) strips and scans a batch of 16 files
per NAPI call, and the TS side only reads files, ships batches, interns
the returned facts, and resolves include paths. The JS sweep remains as
the fallback (no binary, feature detection against older binaries,
CODEGRAPH_KERNEL=0, or CODEGRAPH_KERNEL_CFNPTR=0).

Parity discipline: the JS regexes are the spec, so the scanners are
hand-rolled byte machines reproducing that engine — ASCII \w/\b next to
UNICODE \s (NBSP/U+2000-200A/FEFF decoded from UTF-8), alternation
order, lastIndex resume, and the observable backtracking dimensions
(INIT/ARRAY modifier and struct/star/bracket optionals, DISPATCH's
greedy segment loop); greedy shortcuts only where backtracking provably
can't rescue a match. The native stripper blanks per UTF-16 code unit,
so its output is string-identical to the TS stripper — pinned by a new
kernel arm on the strip differential oracle (fixtures + 500 seeded
random cases).

Gates, all green: new differential suite (adversarial fixture project —
CRLF, NBSP, continuations, decoy strings, unterminated comments,
backtracking shapes — indexed native-vs-JS: identical edge streams,
plus a record-level scanner check); repo differential on
git/redis/vim/SameBoy (identical, 705/852/433/180 edges); probe-hash on
the live linux kernel DB reproduced f6e1713d… (279,335 rows); linux
init counts exact 2,049,153/6,413,518; dump sha 6dd1185b… reproduced
(10,446,478 lines); full suite green ×2 (153 files / 2588 tests).

Measured (8c cg1212, quiet host): cFnPtr sub A=47.9s B=1.1 C=40.9
D=24.1 E=36.8 = 150.9s vs step 1's 179s and the pre-arc 230s (−34%
cumulative); the sweep itself halved (94.5→47.9s, JS strips
132.4k→68.9k). callback-synthesis phase 199.9→171.1s. E's attributed
wall grew from overlap shift under parallel synthesis; the phase total
is the honest number. Full record: plan §7a.10.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:48:27 -05:00

1310 lines
42 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Native port of the cFnPtr synthesizer's EXTRACTION SWEEP (task #5 step 2,
//! plan §7a.9): raw file text in → collected per-file facts out. The linking
//! stages, gates, and every registration/dispatch decision stay TS-side; this
//! module only reproduces, bug-for-bug, what the JS sweep computes per file in
//! `src/resolution/c-fnptr-synthesizer.ts`:
//!
//! • `stripCommentsForRegex(text, 'c')` — the C-style comment/string state
//! machine (comments blanked to spaces, string interiors skipped, backtick
//! treated as a multi-line string delimiter — quirks and all);
//! • the typedef scans (fn-pointer + fn-type forms);
//! • struct-node field declarations (structural parse; classification stays
//! TS-side where the complete typedef sets live);
//! • the survival-filter scans (inline structs, initializers, bare arrays,
//! alias-shaped object macros, field-assign pairs, dispatch fields, array
//! dispatch names);
//! • the raw-text `#include "..."` capture (path resolution stays TS-side —
//! it needs the filesystem).
//!
//! Parity discipline: the JS side runs these as JavaScript REGEXES, so every
//! scanner here is a hand-rolled byte machine replicating THAT engine's
//! semantics, not idiomatic Rust regex:
//! • JS `\w`/`\b` are ASCII (non-ASCII chars are non-word) — byte checks
//! against `[A-Za-z0-9_]` reproduce them exactly, because UTF-8
//! continuation bytes are non-ASCII and therefore non-word on both sides.
//! • JS `\s` is the UNICODE whitespace class (NBSP, U+2000-200A, U+FEFF, …)
//! — `jsws_len` decodes exactly that set from UTF-8.
//! • Backtracking is reproduced where it is observable (INIT/ARRAY modifier
//! and `struct` keyword ambiguity, DISPATCH's greedy segment loop,
//! optional groups) and elided only where analysis shows no input can
//! distinguish greedy from backtracked (documented per scanner).
//! • `lastIndex` advancement (resume after each match, +1 on failure) is
//! reproduced so overlapping-match selection is identical.
//!
//! The stripper blanks per UTF-16 code unit (see `strip_c`), so its output
//! equals the JS stripper's output EXACTLY as a string — every scanner here
//! runs over the identical character stream the JS regexes see, and the strip
//! differential oracle test pins that equality directly. The record-level
//! differential suite (JS sweep vs this sweep over fixtures and whole repos)
//! then pins the scanners themselves.
/// One struct node's extent, as the TS side reads it from the graph.
pub struct StructExtent {
pub id: String,
pub start_line: u32,
pub end_line: u32,
}
/// A structurally-parsed struct field — mirror of the TS `RawFieldDecl`
/// (`name: null` is represented as an empty string; the TS side treats them
/// identically everywhere).
pub struct RawField {
pub name: String,
pub index: u32,
pub ptr: bool,
pub ty: String,
}
pub struct StructFields {
pub id: String,
/// False when the body never parsed (no `{`, unbalanced braces, or a
/// falsy start line) — the TS side then records nothing for this node,
/// exactly like the JS sweep.
pub parsed: bool,
pub fields: Vec<RawField>,
}
/// Everything the sweep collects for one file.
pub struct FileFacts {
pub fn_ptr_typedefs: Vec<String>,
pub fn_type_typedefs: Vec<String>,
pub structs: Vec<StructFields>,
pub inline_ptr: bool,
pub inline_types: Vec<String>,
pub inline_tags: Vec<String>,
pub init_tokens: Vec<String>,
/// `*`-prefixed when the declaration carried the pointer star.
pub array_elems: Vec<String>,
pub alias_names: Vec<String>,
/// `lfield\0rfield`, distinct.
pub d_pairs: Vec<String>,
pub dispatch_fields: Vec<String>,
pub array_dispatch_names: Vec<String>,
/// Raw `#include "…"` captures, in source order, NOT deduplicated —
/// extension filtering and path resolution happen TS-side.
pub includes: Vec<String>,
}
/// Mirror of the TS `C_TYPE_KEYWORDS` set — keep in exact sync.
const C_TYPE_KEYWORDS: [&[u8]; 17] = [
b"void", b"int", b"char", b"short", b"long", b"unsigned", b"signed", b"float", b"double",
b"const", b"struct", b"union", b"enum", b"static", b"volatile", b"register", b"inline",
];
fn is_type_keyword(w: &[u8]) -> bool {
C_TYPE_KEYWORDS.iter().any(|k| *k == w)
}
const MODIFIERS: [&[u8]; 5] = [b"static", b"const", b"extern", b"register", b"volatile"];
#[inline]
fn is_word(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
#[inline]
fn is_word_at(s: &[u8], i: usize) -> bool {
i < s.len() && is_word(s[i])
}
/// Byte length of the JS `\s` character starting at `i`, or 0 when `s[i]`
/// doesn't start one. JS \s = [\t\n\v\f\r   -
///  ].
#[inline]
fn jsws_len(s: &[u8], i: usize) -> usize {
let Some(&b0) = s.get(i) else { return 0 };
match b0 {
0x09..=0x0D | 0x20 => 1,
0xC2 if s.get(i + 1) == Some(&0xA0) => 2, // U+00A0
0xE1 if s.get(i + 1) == Some(&0x9A) && s.get(i + 2) == Some(&0x80) => 3, // U+1680
0xE2 => match (s.get(i + 1), s.get(i + 2)) {
(Some(&0x80), Some(&b2)) if (0x80..=0x8A).contains(&b2) => 3, // U+2000-200A
(Some(&0x80), Some(&0xA8)) => 3, // U+2028
(Some(&0x80), Some(&0xA9)) => 3, // U+2029
(Some(&0x80), Some(&0xAF)) => 3, // U+202F
(Some(&0x81), Some(&0x9F)) => 3, // U+205F
_ => 0,
},
0xE3 if s.get(i + 1) == Some(&0x80) && s.get(i + 2) == Some(&0x80) => 3, // U+3000
0xEF if s.get(i + 1) == Some(&0xBB) && s.get(i + 2) == Some(&0xBF) => 3, // U+FEFF
_ => 0,
}
}
/// Advance past `\s*`.
#[inline]
fn skip_jsws(s: &[u8], mut i: usize) -> usize {
loop {
let l = jsws_len(s, i);
if l == 0 {
return i;
}
i += l;
}
}
/// End of the `\w+` run starting at `i` (caller checks `is_word_at(s, i)`).
#[inline]
fn word_end(s: &[u8], mut i: usize) -> usize {
while i < s.len() && is_word(s[i]) {
i += 1;
}
i
}
/// JS `\b` before position `i` (position 0, or previous byte non-word).
#[inline]
fn boundary_before(s: &[u8], i: usize) -> bool {
i == 0 || !is_word(s[i - 1])
}
fn find_bytes(s: &[u8], needle: &[u8], from: usize) -> Option<usize> {
if needle.is_empty() || s.len() < needle.len() {
return None;
}
let mut i = from;
while i + needle.len() <= s.len() {
// memchr on the first byte keeps this fast on 20KB+ files.
match s[i..s.len() - needle.len() + 1].iter().position(|&b| b == needle[0]) {
None => return None,
Some(off) => {
i += off;
if &s[i..i + needle.len()] == needle {
return Some(i);
}
i += 1;
}
}
}
None
}
fn contains_bytes(s: &[u8], needle: &[u8]) -> bool {
find_bytes(s, needle, 0).is_some()
}
/// `\bWORD\b` occurrence search from `from`.
fn find_word(s: &[u8], word: &[u8], mut from: usize) -> Option<usize> {
loop {
let t = find_bytes(s, word, from)?;
if boundary_before(s, t) && !is_word_at(s, t + word.len()) {
return Some(t);
}
from = t + 1;
}
}
// ---------- stripCommentsForRegex(src, 'c') ----------
/// Port of `stripCStyle(src, /*allowSingleQuoteStrings*/ false)`:
/// `/* */` and `//` comments blanked to spaces (newlines preserved), `"` and
/// backtick string interiors skipped verbatim (backtick spans lines — the JS
/// helper treats it as a template literal even for C), `'` NOT special.
///
/// Blanking is per UTF-16 CODE UNIT (one space per BMP char, two per astral
/// char), so the result equals the JS stripper's output EXACTLY as a string —
/// the scanners downstream see the identical character stream the JS regexes
/// see, and the strip differential oracle pins byte equality directly.
/// (Comment boundaries — `/*`, `*/`, `//`, quotes, `\n` — are all ASCII, so
/// the state machine's byte positions always land on char boundaries.)
pub fn strip_c(src: &[u8]) -> Vec<u8> {
let n = src.len();
let mut out = Vec::with_capacity(n);
let mut copied = 0usize; // src[..copied] already emitted
let mut i = 0;
{
let mut blank_to = |out: &mut Vec<u8>, start: usize, end: usize| {
out.extend_from_slice(&src[copied..start]);
emit_blank(out, &src[start..end]);
copied = end;
};
while i < n {
let c = src[i];
let c2 = if i + 1 < n { src[i + 1] } else { 0 };
if c == b'/' && c2 == b'*' {
let start = i;
i += 2;
while i < n && !(src[i] == b'*' && i + 1 < n && src[i + 1] == b'/') {
i += 1;
}
if i < n {
i += 2;
}
blank_to(&mut out, start, i.min(n));
continue;
}
if c == b'/' && c2 == b'/' {
let start = i;
while i < n && src[i] != b'\n' {
i += 1;
}
blank_to(&mut out, start, i);
continue;
}
if c == b'"' || c == b'`' {
let quote = c;
i += 1;
while i < n && src[i] != quote {
if src[i] == b'\\' && i + 1 < n {
i += 2;
continue;
}
if quote != b'`' && src[i] == b'\n' {
break;
}
i += 1;
}
if i < n && src[i] == quote {
i += 1;
}
continue;
}
i += 1;
}
}
out.extend_from_slice(&src[copied..]);
out
}
/// One space per UTF-16 code unit (`\n` preserved): ASCII and 2-3-byte chars
/// are one unit, 4-byte (astral) chars are a surrogate pair — two units.
fn emit_blank(out: &mut Vec<u8>, region: &[u8]) {
let mut i = 0;
while i < region.len() {
let b = region[i];
if b == b'\n' {
out.push(b'\n');
i += 1;
continue;
}
let len = if b < 0x80 {
1
} else if b < 0xC0 {
1 // continuation byte at region start — invalid UTF-8; count singly
} else if b < 0xE0 {
2
} else if b < 0xF0 {
3
} else {
4
};
out.push(b' ');
if len == 4 {
out.push(b' ');
}
i += len.min(region.len() - i);
}
}
// ---------- shared regex tails ----------
/// `\(\s*(?:\w+\s+)*\*\s*(\w+)\s*\)\s*\(` matched at `open` (which must hold
/// `(`). Returns (name_range, end_after_second_paren). The `(?:\w+\s+)*`
/// group is greedy without backtracking: giving back an iteration repositions
/// `\*` onto a word char, which can never match, so greedy ≡ backtracked.
fn fnptr_paren_tail(s: &[u8], open: usize) -> Option<((usize, usize), usize)> {
let mut i = skip_jsws(s, open + 1);
loop {
if !is_word_at(s, i) {
break;
}
let we = word_end(s, i);
let wse = skip_jsws(s, we);
if wse == we {
break; // \w+ not followed by \s+ — the iteration fails, word not consumed
}
i = wse;
}
if s.get(i) != Some(&b'*') {
return None;
}
i = skip_jsws(s, i + 1);
if !is_word_at(s, i) {
return None;
}
let name = (i, word_end(s, i));
i = skip_jsws(s, name.1);
if s.get(i) != Some(&b')') {
return None;
}
i = skip_jsws(s, i + 1);
if s.get(i) != Some(&b'(') {
return None;
}
Some((name, i + 1))
}
/// `\s*\)?\s*\(` at `i` → position after the `(`. The optional `)` needs no
/// backtracking: retrying without a consumed `)` lands `\(` on that `)`.
fn close_call_tail(s: &[u8], i: usize) -> Option<usize> {
let mut j = skip_jsws(s, i);
if s.get(j) == Some(&b')') {
j = skip_jsws(s, j + 1);
}
if s.get(j) == Some(&b'(') {
return Some(j + 1);
}
None
}
// ---------- scanners ----------
/// FNPTR_TYPEDEF_RE: /\btypedef\b[^;{}]*?\(\s*(?:\w+\s+)*\*\s*(\w+)\s*\)\s*\(/g
fn scan_fnptr_typedefs(s: &[u8], out: &mut Vec<String>) {
let mut last = 0;
while let Some(t) = find_word(s, b"typedef", last) {
let mut j = t + 7;
let mut matched = None;
// Lazy [^;{}]*?: try the paren tail at each `(` in order; the class
// may also expand ACROSS a failed `(` (it admits parens).
while j < s.len() {
let ch = s[j];
if ch == b';' || ch == b'{' || ch == b'}' {
break;
}
if ch == b'(' {
if let Some((name, end)) = fnptr_paren_tail(s, j) {
matched = Some((name, end));
break;
}
}
j += 1;
}
match matched {
Some(((ns, ne), end)) => {
push_str(out, &s[ns..ne]);
last = end;
}
None => last = t + 1,
}
}
}
/// FNTYPE_TYPEDEF_STMT_RE (/\btypedef\b([^;{}]*);/g) + the TS-side guts
/// checks: skip when guts contains `(*` or `( *`; else the FIRST
/// /\b(\w+)\s*\(/ capture, filtered through C_TYPE_KEYWORDS.
fn scan_fntype_typedefs(s: &[u8], out: &mut Vec<String>) {
let mut last = 0;
while let Some(t) = find_word(s, b"typedef", last) {
let mut j = t + 7;
while j < s.len() && s[j] != b';' && s[j] != b'{' && s[j] != b'}' {
j += 1;
}
if j >= s.len() || s[j] != b';' {
last = t + 1;
continue;
}
let guts = &s[t + 7..j];
if !contains_bytes(guts, b"(*") && !contains_bytes(guts, b"( *") {
// first \b(\w+)\s*\( in guts
let mut p = 0;
while p < guts.len() {
if is_word(guts[p]) && boundary_before(guts, p) {
let we = word_end(guts, p);
let k = skip_jsws(guts, we);
if guts.get(k) == Some(&b'(') {
let w = &guts[p..we];
if !is_type_keyword(w) {
push_str(out, w);
}
break;
}
p = we;
} else {
p += 1;
}
}
}
last = j + 1;
}
}
/// INLINE_STRUCT_RE (/\bstruct\s+(\w+)\s*\{/g), sweep flavor: NO cursor jump
/// (the filter needs a superset of the registration pass's jump-scan), each
/// valid candidate (balanced braces + the `^\s*(\w+)…` var check) contributes
/// its tag and a structural field summary.
struct InlineScan {
ptr: bool,
types: Vec<String>,
tags: Vec<String>,
}
fn scan_inline_structs(s: &[u8]) -> InlineScan {
let mut out = InlineScan { ptr: false, types: Vec::new(), tags: Vec::new() };
let mut last = 0;
while let Some(t) = find_word(s, b"struct", last) {
let after_kw = t + 6;
let ws = skip_jsws(s, after_kw);
if ws == after_kw || !is_word_at(s, ws) {
last = t + 1;
continue;
}
let te = word_end(s, ws);
let open = skip_jsws(s, te);
if s.get(open) != Some(&b'{') {
last = t + 1;
continue;
}
last = open + 1; // lastIndex = end of match (after `{`)
let Some(close) = match_brace(s, open) else { continue };
// vm: /^\s*(\w+)…/ on the text after `}` — only vm[1] matters here.
let v = skip_jsws(s, close + 1);
if !is_word_at(s, v) {
continue;
}
push_str(&mut out.tags, &s[ws..te]);
for f in parse_struct_fields_raw(&s[open + 1..close]) {
if f.name.is_empty() {
continue;
}
if f.ptr {
out.ptr = true;
} else if !f.ty.is_empty() {
out.types.push(f.ty);
}
}
}
out
}
/// matchBrace: index of the `}` matching the `{` at `open`, or None.
fn match_brace(s: &[u8], open: usize) -> Option<usize> {
let mut depth = 0i64;
let mut i = open;
while i < s.len() {
match s[i] {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
i += 1;
}
None
}
/// The `(?:(?:static|const|extern|register|volatile)\s+)*` modifier loop:
/// greedy positions after 0..=k iterations, for the k-descending backtrack the
/// INIT/ARRAY skeletons need. No two alternatives share a prefix, so at most
/// one literal can match at a position; an alternative that matches without
/// trailing `\s+` ends the loop (JS: iteration fails, no other alt can fire).
fn modifier_positions(s: &[u8], start: usize) -> Vec<usize> {
let mut stack = vec![start];
loop {
let cur = *stack.last().unwrap();
let mut advanced = None;
for m in MODIFIERS {
if s.len() >= cur + m.len() && &s[cur..cur + m.len()] == m {
let e = cur + m.len();
let w = skip_jsws(s, e);
if w > e {
advanced = Some(w);
}
break; // exactly one alternative can literal-match here
}
}
match advanced {
Some(w) => stack.push(w),
None => return stack,
}
}
}
/// `\[[^\]]*\]` at `i` (the INIT/ARRAY declarator form — the class admits
/// newlines): position after the FIRST `]`, or None.
fn bracket_span(s: &[u8], i: usize) -> Option<usize> {
if s.get(i) != Some(&b'[') {
return None;
}
let mut j = i + 1;
while j < s.len() && s[j] != b']' {
j += 1;
}
if j < s.len() {
Some(j + 1)
} else {
None
}
}
/// Anchor-skeleton driver shared by INIT_RE and ARRAY_TABLE_RE: both match
/// `(?:^|[;{}])` then a body, and resume from the end of each match. `body`
/// returns (token, match_end) when the body matches at the position after the
/// anchor.
fn scan_anchored<F>(s: &[u8], mut body: F, out: &mut Vec<String>)
where
F: FnMut(&[u8], usize) -> Option<(String, usize)>,
{
let mut last = 0usize;
// The `^` branch consumes nothing and only exists at position 0.
if last == 0 {
if let Some((tok, end)) = body(s, 0) {
out.push(tok);
last = end;
}
}
let mut p = last;
while p < s.len() {
let ch = s[p];
if ch == b';' || ch == b'{' || ch == b'}' {
if let Some((tok, end)) = body(s, p + 1) {
out.push(tok);
p = end;
continue;
}
}
p += 1;
}
}
/// INIT_RE body after the anchor:
/// `\s*(?:MOD\s+)*(?:struct\s+)?(\w+)\s+(\w+)\s*(\[[^\]]*\])?\s*=\s*\{`
/// Backtracks: modifier count (desc), `struct` with/without, bracket
/// with/without — exactly the observable dimensions of the JS engine.
fn init_body(s: &[u8], p: usize) -> Option<(String, usize)> {
let i = skip_jsws(s, p);
let mods = modifier_positions(s, i);
for &pos in mods.iter().rev() {
for with_struct in [true, false] {
let q = if with_struct {
if s.len() >= pos + 6 && &s[pos..pos + 6] == b"struct" {
let e = pos + 6;
let w = skip_jsws(s, e);
if w == e {
continue;
}
w
} else {
continue;
}
} else {
pos
};
if !is_word_at(s, q) {
continue;
}
let te = word_end(s, q);
let w = skip_jsws(s, te);
if w == te {
continue; // \s+ needs ≥1
}
if !is_word_at(s, w) {
continue;
}
let ne = word_end(s, w);
let r = skip_jsws(s, ne);
for with_bracket in [true, false] {
let r2 = if with_bracket {
match bracket_span(s, r) {
Some(e) => e,
None => continue,
}
} else {
r
};
let r3 = skip_jsws(s, r2);
if s.get(r3) != Some(&b'=') {
continue;
}
let r4 = skip_jsws(s, r3 + 1);
if s.get(r4) != Some(&b'{') {
continue;
}
return Some((bytes_to_string(&s[q..te]), r4 + 1));
}
}
}
None
}
/// ARRAY_TABLE_RE body after the anchor:
/// `\s*(?:MOD\s+)*(\w+)\s+(\*\s*)?(\w+)\s*\[[^\]]*\]\s*=\s*\{`
/// Token is `*`-prefixed when the star declarator is present.
fn array_table_body(s: &[u8], p: usize) -> Option<(String, usize)> {
let i = skip_jsws(s, p);
let mods = modifier_positions(s, i);
for &pos in mods.iter().rev() {
if !is_word_at(s, pos) {
continue;
}
let te = word_end(s, pos);
let w = skip_jsws(s, te);
if w == te {
continue;
}
for with_star in [true, false] {
let q = if with_star {
if s.get(w) == Some(&b'*') {
skip_jsws(s, w + 1)
} else {
continue;
}
} else {
w
};
if !is_word_at(s, q) {
continue;
}
let ne = word_end(s, q);
let r = skip_jsws(s, ne);
let Some(r2) = bracket_span(s, r) else { continue };
let r3 = skip_jsws(s, r2);
if s.get(r3) != Some(&b'=') {
continue;
}
let r4 = skip_jsws(s, r3 + 1);
if s.get(r4) != Some(&b'{') {
continue;
}
let mut tok = String::new();
if with_star {
tok.push('*');
}
tok.push_str(&bytes_to_string(&s[pos..te]));
return Some((tok, r4 + 1));
}
}
None
}
/// OBJ_ALIAS_RE over the continuation-joined text:
/// /^[ \t]*#[ \t]*define[ \t]+(\w+)[ \t]+(?:struct[ \t]+)*[A-Za-z_]\w*[ \t\r]*$/gm
fn scan_alias_names(stripped: &[u8], out: &mut Vec<String>) {
// joined = stripped.replace(/\\\r?\n/g, ' ')
let mut joined = Vec::with_capacity(stripped.len());
let mut i = 0;
while i < stripped.len() {
let b = stripped[i];
if b == b'\\' {
if stripped.get(i + 1) == Some(&b'\n') {
joined.push(b' ');
i += 2;
continue;
}
if stripped.get(i + 1) == Some(&b'\r') && stripped.get(i + 2) == Some(&b'\n') {
joined.push(b' ');
i += 3;
continue;
}
}
joined.push(b);
i += 1;
}
for line in joined.split(|&b| b == b'\n') {
if let Some(name) = alias_line(line) {
push_str(out, name);
}
}
}
#[inline]
fn skip_sp_tab(line: &[u8], mut i: usize) -> usize {
while i < line.len() && (line[i] == b' ' || line[i] == b'\t') {
i += 1;
}
i
}
fn alias_line(line: &[u8]) -> Option<&[u8]> {
let mut i = skip_sp_tab(line, 0);
if line.get(i) != Some(&b'#') {
return None;
}
i = skip_sp_tab(line, i + 1);
if line.len() < i + 6 || &line[i..i + 6] != b"define" {
return None;
}
i += 6;
let w = skip_sp_tab(line, i);
if w == i || !is_word_at(line, w) {
return None;
}
let name_end = word_end(line, w);
let name = &line[w..name_end];
let v0 = skip_sp_tab(line, name_end);
if v0 == name_end {
return None; // [ \t]+ before the value
}
// (?:struct[ \t]+)* greedy, k-descending on value failure.
let mut stack = vec![v0];
loop {
let cur = *stack.last().unwrap();
if line.len() >= cur + 6 && &line[cur..cur + 6] == b"struct" {
let e = cur + 6;
let w2 = skip_sp_tab(line, e);
if w2 > e {
stack.push(w2);
continue;
}
}
break;
}
for &vp in stack.iter().rev() {
let Some(&b0) = line.get(vp) else { continue };
if !(b0.is_ascii_alphabetic() || b0 == b'_') {
continue; // value must start [A-Za-z_]
}
let ve = word_end(line, vp);
// [ \t\r]*$
let mut t = ve;
while t < line.len() && (line[t] == b' ' || line[t] == b'\t' || line[t] == b'\r') {
t += 1;
}
if t == line.len() {
return Some(name);
}
}
None
}
/// FIELD_ASSIGN_RE: /(\w+)\s*(?:->|\.)\s*(\w+)\s*=\s*(\w+)\s*(?:->|\.)\s*(\w+)/g
/// Pairs collected as `lfield\0rfield`. Every byte position is a candidate
/// start (JS advances one unit on failure — suffix starts included); matches
/// resume at their end.
fn scan_field_assign(s: &[u8], out: &mut Vec<String>) {
let mut pos = 0usize;
while pos < s.len() {
if !is_word(s[pos]) {
pos += 1;
continue;
}
match field_assign_at(s, pos) {
Some((lf, rf, end)) => {
let mut pair = bytes_to_string(&s[lf.0..lf.1]);
pair.push('\0');
pair.push_str(&bytes_to_string(&s[rf.0..rf.1]));
out.push(pair);
pos = end;
}
None => pos += 1,
}
}
}
#[inline]
fn arrow_at(s: &[u8], i: usize) -> Option<usize> {
if s.get(i) == Some(&b'-') && s.get(i + 1) == Some(&b'>') {
Some(i + 2)
} else if s.get(i) == Some(&b'.') {
Some(i + 1)
} else {
None
}
}
type Range = (usize, usize);
fn field_assign_at(s: &[u8], p: usize) -> Option<(Range, Range, usize)> {
let w1 = word_end(s, p);
let a1 = arrow_at(s, skip_jsws(s, w1))?;
let f1s = skip_jsws(s, a1);
if !is_word_at(s, f1s) {
return None;
}
let f1e = word_end(s, f1s);
let eq = skip_jsws(s, f1e);
if s.get(eq) != Some(&b'=') {
return None;
}
let r1s = skip_jsws(s, eq + 1);
if !is_word_at(s, r1s) {
return None;
}
let r1e = word_end(s, r1s);
let a2 = arrow_at(s, skip_jsws(s, r1e))?;
let f2s = skip_jsws(s, a2);
if !is_word_at(s, f2s) {
return None;
}
let f2e = word_end(s, f2s);
Some(((f1s, f1e), (f2s, f2e), f2e))
}
/// DISPATCH_RE: /((?:\w+(?:\s*\[[^\][]*\])?\s*(?:->|\.)\s*)+)(\w+)\s*\)?\s*\(/g
/// The `+` loop is consumed greedily, then the field tail is tried at each
/// segment count k descending — the JS engine's observable backtracking. The
/// per-segment optional subscript needs no cross-product: the with/without
/// parses diverge at the arrow and at most one can complete a segment.
fn scan_dispatch(s: &[u8], out: &mut Vec<String>) {
let mut pos = 0usize;
while pos < s.len() {
if !is_word(s[pos]) {
pos += 1;
continue;
}
// Greedy segment loop.
let mut seg_ends: Vec<usize> = Vec::new();
let mut cur = pos;
while is_word_at(s, cur) {
let we = word_end(s, cur);
let with_sub = subscript_span(s, skip_jsws(s, we)).and_then(|e| arrow_tail(s, e));
let seg = with_sub.or_else(|| arrow_tail(s, we));
match seg {
Some(e) => {
seg_ends.push(e);
cur = e;
}
None => break,
}
}
let mut matched = None;
for k in (1..=seg_ends.len()).rev() {
let fpos = seg_ends[k - 1];
if !is_word_at(s, fpos) {
continue;
}
let fe = word_end(s, fpos);
if let Some(end) = close_call_tail(s, fe) {
matched = Some(((fpos, fe), end));
break;
}
}
match matched {
Some(((fs_, fe), end)) => {
push_str(out, &s[fs_..fe]);
pos = end;
}
None => pos += 1,
}
}
}
/// `\[[^\][]*\]` at `i` (the DISPATCH subscript form — no nested brackets):
/// position after `]`, or None.
fn subscript_span(s: &[u8], i: usize) -> Option<usize> {
if s.get(i) != Some(&b'[') {
return None;
}
let mut j = i + 1;
while j < s.len() && s[j] != b']' && s[j] != b'[' {
j += 1;
}
if j < s.len() && s[j] == b']' {
Some(j + 1)
} else {
None
}
}
/// `\s*(?:->|\.)\s*` at `i` → position after.
#[inline]
fn arrow_tail(s: &[u8], i: usize) -> Option<usize> {
let a = arrow_at(s, skip_jsws(s, i))?;
Some(skip_jsws(s, a))
}
/// ARRAY_DISPATCH_RE: /(?:\(\s*\*\s*)?\b(\w+)\s*\[[^\][]*\]\s*\)?\s*\(/g
fn scan_array_dispatch(s: &[u8], out: &mut Vec<String>) {
let mut pos = 0usize;
while pos < s.len() {
let b = s[pos];
if b != b'(' && !(is_word(b) && boundary_before(s, pos)) {
pos += 1;
continue;
}
let name_start = if b == b'(' {
let i = skip_jsws(s, pos + 1);
if s.get(i) == Some(&b'*') {
let j = skip_jsws(s, i + 1);
// \b holds: the previous char is `*` or whitespace.
if is_word_at(s, j) { Some(j) } else { None }
} else {
None
}
} else {
Some(pos)
};
let matched = name_start.and_then(|ns| {
let ne = word_end(s, ns);
let sub = subscript_span(s, skip_jsws(s, ne))?;
let end = close_call_tail(s, sub)?;
Some(((ns, ne), end))
});
match matched {
Some(((ns, ne), end)) => {
push_str(out, &s[ns..ne]);
pos = end;
}
None => pos += 1,
}
}
}
/// INCLUDE_RE over RAW text: /#[ \t]*include[ \t]+"([^"\n]+)"/g
fn scan_includes(raw: &[u8], out: &mut Vec<String>) {
let mut pos = 0usize;
while pos < raw.len() {
let Some(h) = find_bytes(raw, b"#", pos) else { break };
let mut i = skip_sp_tab(raw, h + 1);
if raw.len() < i + 7 || &raw[i..i + 7] != b"include" {
pos = h + 1;
continue;
}
i += 7;
let q = skip_sp_tab(raw, i);
if q == i || raw.get(q) != Some(&b'"') {
pos = h + 1;
continue;
}
let mut j = q + 1;
while j < raw.len() && raw[j] != b'"' && raw[j] != b'\n' {
j += 1;
}
if j > q + 1 && j < raw.len() && raw[j] == b'"' {
out.push(bytes_to_string(&raw[q + 1..j]));
pos = j + 1;
} else {
pos = h + 1;
}
}
}
// ---------- struct field parsing ----------
/// splitTopLevel(body, sep): split on `sep` at brace/paren/bracket depth 0.
fn split_top_level(body: &[u8], sep: u8) -> Vec<Range> {
let mut out = Vec::new();
let mut depth = 0i64;
let mut start = 0usize;
for (i, &c) in body.iter().enumerate() {
match c {
b'{' | b'(' | b'[' => depth += 1,
b'}' | b')' | b']' => depth -= 1,
_ if c == sep && depth == 0 => {
out.push((start, i));
start = i + 1;
}
_ => {}
}
}
out.push((start, body.len()));
out
}
/// JS String.prototype.trim over bytes (the JS set == our jsws set).
fn jsws_trim(s: &[u8], mut a: usize, mut b: usize) -> (usize, usize) {
loop {
let l = jsws_len(s, a);
if l == 0 || a + l > b {
break;
}
a += l;
}
// Trailing: walk from the front to find the last non-ws position (ws
// lengths vary, so scan forward tracking the end of the last non-ws char).
let mut i = a;
let mut last_end = a;
while i < b {
let l = jsws_len(s, i);
if l == 0 {
i += 1;
last_end = i;
} else {
i += l;
}
}
b = last_end;
(a, b)
}
/// /(\w+)\s+\**\s*(\w+)\s*$/ — leftmost match whose tail reaches the end.
/// Deterministic per start (greedy words/ws cannot backtrack usefully);
/// candidate starts advance one byte at a time like the JS engine.
fn first_typed(part: &[u8]) -> Option<(Range, Range)> {
let n = part.len();
let mut p = 0usize;
while p < n {
if !is_word(part[p]) {
p += 1;
continue;
}
let te = word_end(part, p);
let w = skip_jsws(part, te);
if w == te {
p += 1;
continue;
}
let mut q = w;
while q < n && part[q] == b'*' {
q += 1;
}
let q = skip_jsws(part, q);
if is_word_at(part, q) {
let ne = word_end(part, q);
let t = skip_jsws(part, ne);
if t == n {
return Some(((p, te), (q, ne)));
}
}
p += 1;
}
None
}
/// FNPTR_DECL_RE (first match): /\(\s*(?:\w+\s+)*\*\s*(\w+)\s*\)\s*\(/
fn fnptr_decl(part: &[u8]) -> Option<Range> {
let mut i = 0usize;
while i < part.len() {
if part[i] == b'(' {
if let Some((name, _)) = fnptr_paren_tail(part, i) {
return Some(name);
}
}
i += 1;
}
None
}
/// Port of `parseStructFieldsRaw` — structure only, classification TS-side.
pub fn parse_struct_fields_raw(inner: &[u8]) -> Vec<RawField> {
let mut fields = Vec::new();
let mut idx: u32 = 0;
for (ds, de) in split_top_level(inner, b';') {
let (ds, de) = jsws_trim(inner, ds, de);
if ds >= de {
continue;
}
let decl = &inner[ds..de];
let parts = split_top_level(decl, b',');
let ft = first_typed(&decl[parts[0].0..parts[0].1]);
let shared_type: &[u8] = match &ft {
Some(((ts, te), _)) => &decl[parts[0].0 + ts..parts[0].0 + te],
None => b"",
};
for (pi, &(ps, pe)) in parts.iter().enumerate() {
let (ps2, pe2) = jsws_trim(decl, ps, pe);
let p = &decl[ps2..pe2];
let mut name: &[u8] = b"";
let mut ty: &[u8] = b"";
let mut ptr = false;
if let Some((ns, ne)) = fnptr_decl(p) {
name = &p[ns..ne];
ptr = true;
} else if pi == 0 {
if let Some((_, (ns, ne))) = &ft {
name = &decl[parts[0].0 + ns..parts[0].0 + ne];
ty = shared_type;
}
} else {
// /^\**\s*(\w+)/
let mut q = 0usize;
while q < p.len() && p[q] == b'*' {
q += 1;
}
let q = skip_jsws(p, q);
if is_word_at(p, q) {
name = &p[q..word_end(p, q)];
ty = shared_type;
}
}
fields.push(RawField {
name: bytes_to_string(name),
index: idx,
ptr,
ty: bytes_to_string(ty),
});
idx += 1;
}
}
fields
}
// ---------- per-file entry ----------
fn push_str(out: &mut Vec<String>, bytes: &[u8]) {
out.push(bytes_to_string(bytes));
}
#[inline]
fn bytes_to_string(bytes: &[u8]) -> String {
// All slice boundaries land on ASCII delimiters, so the content is valid
// UTF-8 whenever the input string was; lossy keeps us total anyway.
String::from_utf8_lossy(bytes).into_owned()
}
fn dedup_in_order(v: Vec<String>) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
let mut out = Vec::with_capacity(v.len());
for x in v {
if seen.insert(x.clone()) {
out.push(x);
}
}
out
}
/// Line start offsets (byte offset of each line's first byte).
fn line_starts(s: &[u8]) -> Vec<usize> {
let mut out = vec![0usize];
for (i, &b) in s.iter().enumerate() {
if b == b'\n' {
out.push(i + 1);
}
}
out
}
/// Run the full extraction sweep for one file. `raw` is the file text exactly
/// as the TS side read it; `structs` are the file's struct-node extents.
pub fn scan_file(raw: &str, structs: &[StructExtent]) -> FileFacts {
let raw_b = raw.as_bytes();
let stripped = strip_c(raw_b);
let s: &[u8] = &stripped;
let mut facts = FileFacts {
fn_ptr_typedefs: Vec::new(),
fn_type_typedefs: Vec::new(),
structs: Vec::new(),
inline_ptr: false,
inline_types: Vec::new(),
inline_tags: Vec::new(),
init_tokens: Vec::new(),
array_elems: Vec::new(),
alias_names: Vec::new(),
d_pairs: Vec::new(),
dispatch_fields: Vec::new(),
array_dispatch_names: Vec::new(),
includes: Vec::new(),
};
// Typedefs (gated like the JS sweep — purely a fast path, the scans find
// nothing without the substring anyway).
if contains_bytes(s, b"typedef") {
scan_fnptr_typedefs(s, &mut facts.fn_ptr_typedefs);
scan_fntype_typedefs(s, &mut facts.fn_type_typedefs);
}
// Struct-node field declarations.
if !structs.is_empty() {
let lines = line_starts(s);
for st in structs {
let mut sf = StructFields { id: st.id.clone(), parsed: false, fields: Vec::new() };
// sliceLinesPre: falsy startLine → '' (never parses). end_line
// arrives with the TS side's `?? startLine` already applied; a
// slice whose end ≤ start is empty, exactly like Array.slice.
if st.start_line >= 1 {
let a = (st.start_line - 1) as usize;
let b = st.end_line as usize;
if a < lines.len() && b > a {
let body_start = lines[a];
// End of line (b-1): next line start minus the `\n`, or EOF.
let body_end = if b < lines.len() { lines[b] - 1 } else { s.len() };
let body = &s[body_start..body_end.max(body_start)];
if let Some(open) = body.iter().position(|&c| c == b'{') {
if let Some(close) = match_brace(body, open) {
sf.parsed = true;
sf.fields = parse_struct_fields_raw(&body[open + 1..close]);
}
}
}
}
facts.structs.push(sf);
}
}
// Registration filters.
if contains_bytes(s, b"{") {
let inline = scan_inline_structs(s);
facts.inline_ptr = inline.ptr;
facts.inline_types = dedup_in_order(inline.types);
facts.inline_tags = dedup_in_order(inline.tags);
if contains_bytes(s, b"=") {
scan_anchored(s, init_body, &mut facts.init_tokens);
facts.init_tokens = dedup_in_order(std::mem::take(&mut facts.init_tokens));
scan_anchored(s, array_table_body, &mut facts.array_elems);
facts.array_elems = dedup_in_order(std::mem::take(&mut facts.array_elems));
}
}
// Alias-shaped object macros.
if contains_bytes(s, b"#define") || contains_bytes(s, b"# define") {
scan_alias_names(s, &mut facts.alias_names);
facts.alias_names = dedup_in_order(std::mem::take(&mut facts.alias_names));
}
// Propagation + dispatch filters.
if contains_bytes(s, b"=") {
scan_field_assign(s, &mut facts.d_pairs);
facts.d_pairs = dedup_in_order(std::mem::take(&mut facts.d_pairs));
}
scan_dispatch(s, &mut facts.dispatch_fields);
facts.dispatch_fields = dedup_in_order(std::mem::take(&mut facts.dispatch_fields));
scan_array_dispatch(s, &mut facts.array_dispatch_names);
facts.array_dispatch_names = dedup_in_order(std::mem::take(&mut facts.array_dispatch_names));
// Includes come from the RAW text (string contents survive there).
if contains_bytes(raw_b, b"include") {
scan_includes(raw_b, &mut facts.includes);
}
facts
}
#[cfg(test)]
mod tests {
use super::*;
fn facts(src: &str) -> FileFacts {
scan_file(src, &[])
}
#[test]
fn strip_blanks_comments_keeps_strings() {
let s = strip_c(b"a /* x\ny */ b // c\nd \"in//str\" e");
assert_eq!(&s, b"a \n b \nd \"in//str\" e".as_slice());
}
#[test]
fn typedef_forms() {
let f = facts("typedef void (*hook_fn)(int);\ntypedef void redisCommandProc(int c);\n");
assert_eq!(f.fn_ptr_typedefs, vec!["hook_fn"]);
assert_eq!(f.fn_type_typedefs, vec!["redisCommandProc"]);
}
#[test]
fn init_modifier_backtrack() {
// `static x = {` must match with type token `static` (the JS engine
// backtracks the modifier loop) — harmless downstream, but collected.
let f = facts("; static x = {1};\n; static struct cmd t[] = { {0} };");
assert!(f.init_tokens.contains(&"static".to_string()));
assert!(f.init_tokens.contains(&"cmd".to_string()));
}
#[test]
fn dispatch_backtracks_segments() {
let f = facts("int go(struct c *x){ x->cmd->proc(1); tbl[i](2); (*ops[k])(3); }");
assert!(f.dispatch_fields.contains(&"proc".to_string()));
assert!(f.array_dispatch_names.contains(&"tbl".to_string()));
assert!(f.array_dispatch_names.contains(&"ops".to_string()));
}
#[test]
fn field_assign_pairs() {
let f = facts("void g(void){ a->f = b->g; h.x = k.y; m == n; }");
assert!(f.d_pairs.contains(&"f\0g".to_string()));
assert!(f.d_pairs.contains(&"x\0y".to_string()));
assert_eq!(f.d_pairs.len(), 2);
}
#[test]
fn alias_shapes() {
let f = facts("#define A redisCommand\n#define B struct foo\n#define C 0x12\n#define D(x) x\n");
assert!(f.alias_names.contains(&"A".to_string()));
assert!(f.alias_names.contains(&"B".to_string()));
assert!(!f.alias_names.contains(&"C".to_string()));
assert!(!f.alias_names.contains(&"D".to_string()));
}
#[test]
fn includes_from_raw() {
let f = facts("#include \"commands.def\"\n// #include \"in-comment.h\"\n");
// Raw-text scan: the commented include IS captured (parity with the
// JS INCLUDE_RE over raw text).
assert_eq!(f.includes, vec!["commands.def", "in-comment.h"]);
}
}