| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327 |
- //! 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;
- loop {
- let next_struct = find_word(s, b"struct", last);
- let next_union = find_word(s, b"union", last);
- let Some((t, keyword_len)) = (match (next_struct, next_union) {
- (Some(st), Some(un)) if st < un => Some((st, 6)),
- (Some(_), Some(un)) => Some((un, 5)),
- (Some(st), None) => Some((st, 6)),
- (None, Some(un)) => Some((un, 5)),
- (None, None) => None,
- }) else {
- break;
- };
- let after_kw = t + keyword_len;
- 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 keyword in [Some(b"struct".as_slice()), Some(b"union".as_slice()), None] {
- let q = if let Some(keyword) = keyword {
- if s.len() >= pos + keyword.len() && &s[pos..pos + keyword.len()] == keyword {
- let e = pos + keyword.len();
- 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|union)[ \t]+)* greedy, k-descending on value failure.
- let mut stack = vec![v0];
- loop {
- let cur = *stack.last().unwrap();
- let keyword_len = if line.len() >= cur + 6 && &line[cur..cur + 6] == b"struct" {
- Some(6)
- } else if line.len() >= cur + 5 && &line[cur..cur + 5] == b"union" {
- Some(5)
- } else {
- None
- };
- if let Some(keyword_len) = keyword_len {
- let e = cur + keyword_len;
- 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"]);
- }
- }
|