cfnptr.rs 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327
  1. //! Native port of the cFnPtr synthesizer's EXTRACTION SWEEP (task #5 step 2,
  2. //! plan §7a.9): raw file text in → collected per-file facts out. The linking
  3. //! stages, gates, and every registration/dispatch decision stay TS-side; this
  4. //! module only reproduces, bug-for-bug, what the JS sweep computes per file in
  5. //! `src/resolution/c-fnptr-synthesizer.ts`:
  6. //!
  7. //! • `stripCommentsForRegex(text, 'c')` — the C-style comment/string state
  8. //! machine (comments blanked to spaces, string interiors skipped, backtick
  9. //! treated as a multi-line string delimiter — quirks and all);
  10. //! • the typedef scans (fn-pointer + fn-type forms);
  11. //! • struct-node field declarations (structural parse; classification stays
  12. //! TS-side where the complete typedef sets live);
  13. //! • the survival-filter scans (inline structs, initializers, bare arrays,
  14. //! alias-shaped object macros, field-assign pairs, dispatch fields, array
  15. //! dispatch names);
  16. //! • the raw-text `#include "..."` capture (path resolution stays TS-side —
  17. //! it needs the filesystem).
  18. //!
  19. //! Parity discipline: the JS side runs these as JavaScript REGEXES, so every
  20. //! scanner here is a hand-rolled byte machine replicating THAT engine's
  21. //! semantics, not idiomatic Rust regex:
  22. //! • JS `\w`/`\b` are ASCII (non-ASCII chars are non-word) — byte checks
  23. //! against `[A-Za-z0-9_]` reproduce them exactly, because UTF-8
  24. //! continuation bytes are non-ASCII and therefore non-word on both sides.
  25. //! • JS `\s` is the UNICODE whitespace class (NBSP, U+2000-200A, U+FEFF, …)
  26. //! — `jsws_len` decodes exactly that set from UTF-8.
  27. //! • Backtracking is reproduced where it is observable (INIT/ARRAY modifier
  28. //! and `struct` keyword ambiguity, DISPATCH's greedy segment loop,
  29. //! optional groups) and elided only where analysis shows no input can
  30. //! distinguish greedy from backtracked (documented per scanner).
  31. //! • `lastIndex` advancement (resume after each match, +1 on failure) is
  32. //! reproduced so overlapping-match selection is identical.
  33. //!
  34. //! The stripper blanks per UTF-16 code unit (see `strip_c`), so its output
  35. //! equals the JS stripper's output EXACTLY as a string — every scanner here
  36. //! runs over the identical character stream the JS regexes see, and the strip
  37. //! differential oracle test pins that equality directly. The record-level
  38. //! differential suite (JS sweep vs this sweep over fixtures and whole repos)
  39. //! then pins the scanners themselves.
  40. /// One struct node's extent, as the TS side reads it from the graph.
  41. pub struct StructExtent {
  42. pub id: String,
  43. pub start_line: u32,
  44. pub end_line: u32,
  45. }
  46. /// A structurally-parsed struct field — mirror of the TS `RawFieldDecl`
  47. /// (`name: null` is represented as an empty string; the TS side treats them
  48. /// identically everywhere).
  49. pub struct RawField {
  50. pub name: String,
  51. pub index: u32,
  52. pub ptr: bool,
  53. pub ty: String,
  54. }
  55. pub struct StructFields {
  56. pub id: String,
  57. /// False when the body never parsed (no `{`, unbalanced braces, or a
  58. /// falsy start line) — the TS side then records nothing for this node,
  59. /// exactly like the JS sweep.
  60. pub parsed: bool,
  61. pub fields: Vec<RawField>,
  62. }
  63. /// Everything the sweep collects for one file.
  64. pub struct FileFacts {
  65. pub fn_ptr_typedefs: Vec<String>,
  66. pub fn_type_typedefs: Vec<String>,
  67. pub structs: Vec<StructFields>,
  68. pub inline_ptr: bool,
  69. pub inline_types: Vec<String>,
  70. pub inline_tags: Vec<String>,
  71. pub init_tokens: Vec<String>,
  72. /// `*`-prefixed when the declaration carried the pointer star.
  73. pub array_elems: Vec<String>,
  74. pub alias_names: Vec<String>,
  75. /// `lfield\0rfield`, distinct.
  76. pub d_pairs: Vec<String>,
  77. pub dispatch_fields: Vec<String>,
  78. pub array_dispatch_names: Vec<String>,
  79. /// Raw `#include "…"` captures, in source order, NOT deduplicated —
  80. /// extension filtering and path resolution happen TS-side.
  81. pub includes: Vec<String>,
  82. }
  83. /// Mirror of the TS `C_TYPE_KEYWORDS` set — keep in exact sync.
  84. const C_TYPE_KEYWORDS: [&[u8]; 17] = [
  85. b"void", b"int", b"char", b"short", b"long", b"unsigned", b"signed", b"float", b"double",
  86. b"const", b"struct", b"union", b"enum", b"static", b"volatile", b"register", b"inline",
  87. ];
  88. fn is_type_keyword(w: &[u8]) -> bool {
  89. C_TYPE_KEYWORDS.iter().any(|k| *k == w)
  90. }
  91. const MODIFIERS: [&[u8]; 5] = [b"static", b"const", b"extern", b"register", b"volatile"];
  92. #[inline]
  93. fn is_word(b: u8) -> bool {
  94. b.is_ascii_alphanumeric() || b == b'_'
  95. }
  96. #[inline]
  97. fn is_word_at(s: &[u8], i: usize) -> bool {
  98. i < s.len() && is_word(s[i])
  99. }
  100. /// Byte length of the JS `\s` character starting at `i`, or 0 when `s[i]`
  101. /// doesn't start one. JS \s = [\t\n\v\f\r    -
  102. /// 
   ].
  103. #[inline]
  104. fn jsws_len(s: &[u8], i: usize) -> usize {
  105. let Some(&b0) = s.get(i) else { return 0 };
  106. match b0 {
  107. 0x09..=0x0D | 0x20 => 1,
  108. 0xC2 if s.get(i + 1) == Some(&0xA0) => 2, // U+00A0
  109. 0xE1 if s.get(i + 1) == Some(&0x9A) && s.get(i + 2) == Some(&0x80) => 3, // U+1680
  110. 0xE2 => match (s.get(i + 1), s.get(i + 2)) {
  111. (Some(&0x80), Some(&b2)) if (0x80..=0x8A).contains(&b2) => 3, // U+2000-200A
  112. (Some(&0x80), Some(&0xA8)) => 3, // U+2028
  113. (Some(&0x80), Some(&0xA9)) => 3, // U+2029
  114. (Some(&0x80), Some(&0xAF)) => 3, // U+202F
  115. (Some(&0x81), Some(&0x9F)) => 3, // U+205F
  116. _ => 0,
  117. },
  118. 0xE3 if s.get(i + 1) == Some(&0x80) && s.get(i + 2) == Some(&0x80) => 3, // U+3000
  119. 0xEF if s.get(i + 1) == Some(&0xBB) && s.get(i + 2) == Some(&0xBF) => 3, // U+FEFF
  120. _ => 0,
  121. }
  122. }
  123. /// Advance past `\s*`.
  124. #[inline]
  125. fn skip_jsws(s: &[u8], mut i: usize) -> usize {
  126. loop {
  127. let l = jsws_len(s, i);
  128. if l == 0 {
  129. return i;
  130. }
  131. i += l;
  132. }
  133. }
  134. /// End of the `\w+` run starting at `i` (caller checks `is_word_at(s, i)`).
  135. #[inline]
  136. fn word_end(s: &[u8], mut i: usize) -> usize {
  137. while i < s.len() && is_word(s[i]) {
  138. i += 1;
  139. }
  140. i
  141. }
  142. /// JS `\b` before position `i` (position 0, or previous byte non-word).
  143. #[inline]
  144. fn boundary_before(s: &[u8], i: usize) -> bool {
  145. i == 0 || !is_word(s[i - 1])
  146. }
  147. fn find_bytes(s: &[u8], needle: &[u8], from: usize) -> Option<usize> {
  148. if needle.is_empty() || s.len() < needle.len() {
  149. return None;
  150. }
  151. let mut i = from;
  152. while i + needle.len() <= s.len() {
  153. // memchr on the first byte keeps this fast on 20KB+ files.
  154. match s[i..s.len() - needle.len() + 1].iter().position(|&b| b == needle[0]) {
  155. None => return None,
  156. Some(off) => {
  157. i += off;
  158. if &s[i..i + needle.len()] == needle {
  159. return Some(i);
  160. }
  161. i += 1;
  162. }
  163. }
  164. }
  165. None
  166. }
  167. fn contains_bytes(s: &[u8], needle: &[u8]) -> bool {
  168. find_bytes(s, needle, 0).is_some()
  169. }
  170. /// `\bWORD\b` occurrence search from `from`.
  171. fn find_word(s: &[u8], word: &[u8], mut from: usize) -> Option<usize> {
  172. loop {
  173. let t = find_bytes(s, word, from)?;
  174. if boundary_before(s, t) && !is_word_at(s, t + word.len()) {
  175. return Some(t);
  176. }
  177. from = t + 1;
  178. }
  179. }
  180. // ---------- stripCommentsForRegex(src, 'c') ----------
  181. /// Port of `stripCStyle(src, /*allowSingleQuoteStrings*/ false)`:
  182. /// `/* */` and `//` comments blanked to spaces (newlines preserved), `"` and
  183. /// backtick string interiors skipped verbatim (backtick spans lines — the JS
  184. /// helper treats it as a template literal even for C), `'` NOT special.
  185. ///
  186. /// Blanking is per UTF-16 CODE UNIT (one space per BMP char, two per astral
  187. /// char), so the result equals the JS stripper's output EXACTLY as a string —
  188. /// the scanners downstream see the identical character stream the JS regexes
  189. /// see, and the strip differential oracle pins byte equality directly.
  190. /// (Comment boundaries — `/*`, `*/`, `//`, quotes, `\n` — are all ASCII, so
  191. /// the state machine's byte positions always land on char boundaries.)
  192. pub fn strip_c(src: &[u8]) -> Vec<u8> {
  193. let n = src.len();
  194. let mut out = Vec::with_capacity(n);
  195. let mut copied = 0usize; // src[..copied] already emitted
  196. let mut i = 0;
  197. {
  198. let mut blank_to = |out: &mut Vec<u8>, start: usize, end: usize| {
  199. out.extend_from_slice(&src[copied..start]);
  200. emit_blank(out, &src[start..end]);
  201. copied = end;
  202. };
  203. while i < n {
  204. let c = src[i];
  205. let c2 = if i + 1 < n { src[i + 1] } else { 0 };
  206. if c == b'/' && c2 == b'*' {
  207. let start = i;
  208. i += 2;
  209. while i < n && !(src[i] == b'*' && i + 1 < n && src[i + 1] == b'/') {
  210. i += 1;
  211. }
  212. if i < n {
  213. i += 2;
  214. }
  215. blank_to(&mut out, start, i.min(n));
  216. continue;
  217. }
  218. if c == b'/' && c2 == b'/' {
  219. let start = i;
  220. while i < n && src[i] != b'\n' {
  221. i += 1;
  222. }
  223. blank_to(&mut out, start, i);
  224. continue;
  225. }
  226. if c == b'"' || c == b'`' {
  227. let quote = c;
  228. i += 1;
  229. while i < n && src[i] != quote {
  230. if src[i] == b'\\' && i + 1 < n {
  231. i += 2;
  232. continue;
  233. }
  234. if quote != b'`' && src[i] == b'\n' {
  235. break;
  236. }
  237. i += 1;
  238. }
  239. if i < n && src[i] == quote {
  240. i += 1;
  241. }
  242. continue;
  243. }
  244. i += 1;
  245. }
  246. }
  247. out.extend_from_slice(&src[copied..]);
  248. out
  249. }
  250. /// One space per UTF-16 code unit (`\n` preserved): ASCII and 2-3-byte chars
  251. /// are one unit, 4-byte (astral) chars are a surrogate pair — two units.
  252. fn emit_blank(out: &mut Vec<u8>, region: &[u8]) {
  253. let mut i = 0;
  254. while i < region.len() {
  255. let b = region[i];
  256. if b == b'\n' {
  257. out.push(b'\n');
  258. i += 1;
  259. continue;
  260. }
  261. let len = if b < 0x80 {
  262. 1
  263. } else if b < 0xC0 {
  264. 1 // continuation byte at region start — invalid UTF-8; count singly
  265. } else if b < 0xE0 {
  266. 2
  267. } else if b < 0xF0 {
  268. 3
  269. } else {
  270. 4
  271. };
  272. out.push(b' ');
  273. if len == 4 {
  274. out.push(b' ');
  275. }
  276. i += len.min(region.len() - i);
  277. }
  278. }
  279. // ---------- shared regex tails ----------
  280. /// `\(\s*(?:\w+\s+)*\*\s*(\w+)\s*\)\s*\(` matched at `open` (which must hold
  281. /// `(`). Returns (name_range, end_after_second_paren). The `(?:\w+\s+)*`
  282. /// group is greedy without backtracking: giving back an iteration repositions
  283. /// `\*` onto a word char, which can never match, so greedy ≡ backtracked.
  284. fn fnptr_paren_tail(s: &[u8], open: usize) -> Option<((usize, usize), usize)> {
  285. let mut i = skip_jsws(s, open + 1);
  286. loop {
  287. if !is_word_at(s, i) {
  288. break;
  289. }
  290. let we = word_end(s, i);
  291. let wse = skip_jsws(s, we);
  292. if wse == we {
  293. break; // \w+ not followed by \s+ — the iteration fails, word not consumed
  294. }
  295. i = wse;
  296. }
  297. if s.get(i) != Some(&b'*') {
  298. return None;
  299. }
  300. i = skip_jsws(s, i + 1);
  301. if !is_word_at(s, i) {
  302. return None;
  303. }
  304. let name = (i, word_end(s, i));
  305. i = skip_jsws(s, name.1);
  306. if s.get(i) != Some(&b')') {
  307. return None;
  308. }
  309. i = skip_jsws(s, i + 1);
  310. if s.get(i) != Some(&b'(') {
  311. return None;
  312. }
  313. Some((name, i + 1))
  314. }
  315. /// `\s*\)?\s*\(` at `i` → position after the `(`. The optional `)` needs no
  316. /// backtracking: retrying without a consumed `)` lands `\(` on that `)`.
  317. fn close_call_tail(s: &[u8], i: usize) -> Option<usize> {
  318. let mut j = skip_jsws(s, i);
  319. if s.get(j) == Some(&b')') {
  320. j = skip_jsws(s, j + 1);
  321. }
  322. if s.get(j) == Some(&b'(') {
  323. return Some(j + 1);
  324. }
  325. None
  326. }
  327. // ---------- scanners ----------
  328. /// FNPTR_TYPEDEF_RE: /\btypedef\b[^;{}]*?\(\s*(?:\w+\s+)*\*\s*(\w+)\s*\)\s*\(/g
  329. fn scan_fnptr_typedefs(s: &[u8], out: &mut Vec<String>) {
  330. let mut last = 0;
  331. while let Some(t) = find_word(s, b"typedef", last) {
  332. let mut j = t + 7;
  333. let mut matched = None;
  334. // Lazy [^;{}]*?: try the paren tail at each `(` in order; the class
  335. // may also expand ACROSS a failed `(` (it admits parens).
  336. while j < s.len() {
  337. let ch = s[j];
  338. if ch == b';' || ch == b'{' || ch == b'}' {
  339. break;
  340. }
  341. if ch == b'(' {
  342. if let Some((name, end)) = fnptr_paren_tail(s, j) {
  343. matched = Some((name, end));
  344. break;
  345. }
  346. }
  347. j += 1;
  348. }
  349. match matched {
  350. Some(((ns, ne), end)) => {
  351. push_str(out, &s[ns..ne]);
  352. last = end;
  353. }
  354. None => last = t + 1,
  355. }
  356. }
  357. }
  358. /// FNTYPE_TYPEDEF_STMT_RE (/\btypedef\b([^;{}]*);/g) + the TS-side guts
  359. /// checks: skip when guts contains `(*` or `( *`; else the FIRST
  360. /// /\b(\w+)\s*\(/ capture, filtered through C_TYPE_KEYWORDS.
  361. fn scan_fntype_typedefs(s: &[u8], out: &mut Vec<String>) {
  362. let mut last = 0;
  363. while let Some(t) = find_word(s, b"typedef", last) {
  364. let mut j = t + 7;
  365. while j < s.len() && s[j] != b';' && s[j] != b'{' && s[j] != b'}' {
  366. j += 1;
  367. }
  368. if j >= s.len() || s[j] != b';' {
  369. last = t + 1;
  370. continue;
  371. }
  372. let guts = &s[t + 7..j];
  373. if !contains_bytes(guts, b"(*") && !contains_bytes(guts, b"( *") {
  374. // first \b(\w+)\s*\( in guts
  375. let mut p = 0;
  376. while p < guts.len() {
  377. if is_word(guts[p]) && boundary_before(guts, p) {
  378. let we = word_end(guts, p);
  379. let k = skip_jsws(guts, we);
  380. if guts.get(k) == Some(&b'(') {
  381. let w = &guts[p..we];
  382. if !is_type_keyword(w) {
  383. push_str(out, w);
  384. }
  385. break;
  386. }
  387. p = we;
  388. } else {
  389. p += 1;
  390. }
  391. }
  392. }
  393. last = j + 1;
  394. }
  395. }
  396. /// INLINE_STRUCT_RE (/\bstruct\s+(\w+)\s*\{/g), sweep flavor: NO cursor jump
  397. /// (the filter needs a superset of the registration pass's jump-scan), each
  398. /// valid candidate (balanced braces + the `^\s*(\w+)…` var check) contributes
  399. /// its tag and a structural field summary.
  400. struct InlineScan {
  401. ptr: bool,
  402. types: Vec<String>,
  403. tags: Vec<String>,
  404. }
  405. fn scan_inline_structs(s: &[u8]) -> InlineScan {
  406. let mut out = InlineScan { ptr: false, types: Vec::new(), tags: Vec::new() };
  407. let mut last = 0;
  408. loop {
  409. let next_struct = find_word(s, b"struct", last);
  410. let next_union = find_word(s, b"union", last);
  411. let Some((t, keyword_len)) = (match (next_struct, next_union) {
  412. (Some(st), Some(un)) if st < un => Some((st, 6)),
  413. (Some(_), Some(un)) => Some((un, 5)),
  414. (Some(st), None) => Some((st, 6)),
  415. (None, Some(un)) => Some((un, 5)),
  416. (None, None) => None,
  417. }) else {
  418. break;
  419. };
  420. let after_kw = t + keyword_len;
  421. let ws = skip_jsws(s, after_kw);
  422. if ws == after_kw || !is_word_at(s, ws) {
  423. last = t + 1;
  424. continue;
  425. }
  426. let te = word_end(s, ws);
  427. let open = skip_jsws(s, te);
  428. if s.get(open) != Some(&b'{') {
  429. last = t + 1;
  430. continue;
  431. }
  432. last = open + 1; // lastIndex = end of match (after `{`)
  433. let Some(close) = match_brace(s, open) else { continue };
  434. // vm: /^\s*(\w+)…/ on the text after `}` — only vm[1] matters here.
  435. let v = skip_jsws(s, close + 1);
  436. if !is_word_at(s, v) {
  437. continue;
  438. }
  439. push_str(&mut out.tags, &s[ws..te]);
  440. for f in parse_struct_fields_raw(&s[open + 1..close]) {
  441. if f.name.is_empty() {
  442. continue;
  443. }
  444. if f.ptr {
  445. out.ptr = true;
  446. } else if !f.ty.is_empty() {
  447. out.types.push(f.ty);
  448. }
  449. }
  450. }
  451. out
  452. }
  453. /// matchBrace: index of the `}` matching the `{` at `open`, or None.
  454. fn match_brace(s: &[u8], open: usize) -> Option<usize> {
  455. let mut depth = 0i64;
  456. let mut i = open;
  457. while i < s.len() {
  458. match s[i] {
  459. b'{' => depth += 1,
  460. b'}' => {
  461. depth -= 1;
  462. if depth == 0 {
  463. return Some(i);
  464. }
  465. }
  466. _ => {}
  467. }
  468. i += 1;
  469. }
  470. None
  471. }
  472. /// The `(?:(?:static|const|extern|register|volatile)\s+)*` modifier loop:
  473. /// greedy positions after 0..=k iterations, for the k-descending backtrack the
  474. /// INIT/ARRAY skeletons need. No two alternatives share a prefix, so at most
  475. /// one literal can match at a position; an alternative that matches without
  476. /// trailing `\s+` ends the loop (JS: iteration fails, no other alt can fire).
  477. fn modifier_positions(s: &[u8], start: usize) -> Vec<usize> {
  478. let mut stack = vec![start];
  479. loop {
  480. let cur = *stack.last().unwrap();
  481. let mut advanced = None;
  482. for m in MODIFIERS {
  483. if s.len() >= cur + m.len() && &s[cur..cur + m.len()] == m {
  484. let e = cur + m.len();
  485. let w = skip_jsws(s, e);
  486. if w > e {
  487. advanced = Some(w);
  488. }
  489. break; // exactly one alternative can literal-match here
  490. }
  491. }
  492. match advanced {
  493. Some(w) => stack.push(w),
  494. None => return stack,
  495. }
  496. }
  497. }
  498. /// `\[[^\]]*\]` at `i` (the INIT/ARRAY declarator form — the class admits
  499. /// newlines): position after the FIRST `]`, or None.
  500. fn bracket_span(s: &[u8], i: usize) -> Option<usize> {
  501. if s.get(i) != Some(&b'[') {
  502. return None;
  503. }
  504. let mut j = i + 1;
  505. while j < s.len() && s[j] != b']' {
  506. j += 1;
  507. }
  508. if j < s.len() {
  509. Some(j + 1)
  510. } else {
  511. None
  512. }
  513. }
  514. /// Anchor-skeleton driver shared by INIT_RE and ARRAY_TABLE_RE: both match
  515. /// `(?:^|[;{}])` then a body, and resume from the end of each match. `body`
  516. /// returns (token, match_end) when the body matches at the position after the
  517. /// anchor.
  518. fn scan_anchored<F>(s: &[u8], mut body: F, out: &mut Vec<String>)
  519. where
  520. F: FnMut(&[u8], usize) -> Option<(String, usize)>,
  521. {
  522. let mut last = 0usize;
  523. // The `^` branch consumes nothing and only exists at position 0.
  524. if last == 0 {
  525. if let Some((tok, end)) = body(s, 0) {
  526. out.push(tok);
  527. last = end;
  528. }
  529. }
  530. let mut p = last;
  531. while p < s.len() {
  532. let ch = s[p];
  533. if ch == b';' || ch == b'{' || ch == b'}' {
  534. if let Some((tok, end)) = body(s, p + 1) {
  535. out.push(tok);
  536. p = end;
  537. continue;
  538. }
  539. }
  540. p += 1;
  541. }
  542. }
  543. /// INIT_RE body after the anchor:
  544. /// `\s*(?:MOD\s+)*(?:struct\s+)?(\w+)\s+(\w+)\s*(\[[^\]]*\])?\s*=\s*\{`
  545. /// Backtracks: modifier count (desc), `struct` with/without, bracket
  546. /// with/without — exactly the observable dimensions of the JS engine.
  547. fn init_body(s: &[u8], p: usize) -> Option<(String, usize)> {
  548. let i = skip_jsws(s, p);
  549. let mods = modifier_positions(s, i);
  550. for &pos in mods.iter().rev() {
  551. for keyword in [Some(b"struct".as_slice()), Some(b"union".as_slice()), None] {
  552. let q = if let Some(keyword) = keyword {
  553. if s.len() >= pos + keyword.len() && &s[pos..pos + keyword.len()] == keyword {
  554. let e = pos + keyword.len();
  555. let w = skip_jsws(s, e);
  556. if w == e {
  557. continue;
  558. }
  559. w
  560. } else {
  561. continue;
  562. }
  563. } else {
  564. pos
  565. };
  566. if !is_word_at(s, q) {
  567. continue;
  568. }
  569. let te = word_end(s, q);
  570. let w = skip_jsws(s, te);
  571. if w == te {
  572. continue; // \s+ needs ≥1
  573. }
  574. if !is_word_at(s, w) {
  575. continue;
  576. }
  577. let ne = word_end(s, w);
  578. let r = skip_jsws(s, ne);
  579. for with_bracket in [true, false] {
  580. let r2 = if with_bracket {
  581. match bracket_span(s, r) {
  582. Some(e) => e,
  583. None => continue,
  584. }
  585. } else {
  586. r
  587. };
  588. let r3 = skip_jsws(s, r2);
  589. if s.get(r3) != Some(&b'=') {
  590. continue;
  591. }
  592. let r4 = skip_jsws(s, r3 + 1);
  593. if s.get(r4) != Some(&b'{') {
  594. continue;
  595. }
  596. return Some((bytes_to_string(&s[q..te]), r4 + 1));
  597. }
  598. }
  599. }
  600. None
  601. }
  602. /// ARRAY_TABLE_RE body after the anchor:
  603. /// `\s*(?:MOD\s+)*(\w+)\s+(\*\s*)?(\w+)\s*\[[^\]]*\]\s*=\s*\{`
  604. /// Token is `*`-prefixed when the star declarator is present.
  605. fn array_table_body(s: &[u8], p: usize) -> Option<(String, usize)> {
  606. let i = skip_jsws(s, p);
  607. let mods = modifier_positions(s, i);
  608. for &pos in mods.iter().rev() {
  609. if !is_word_at(s, pos) {
  610. continue;
  611. }
  612. let te = word_end(s, pos);
  613. let w = skip_jsws(s, te);
  614. if w == te {
  615. continue;
  616. }
  617. for with_star in [true, false] {
  618. let q = if with_star {
  619. if s.get(w) == Some(&b'*') {
  620. skip_jsws(s, w + 1)
  621. } else {
  622. continue;
  623. }
  624. } else {
  625. w
  626. };
  627. if !is_word_at(s, q) {
  628. continue;
  629. }
  630. let ne = word_end(s, q);
  631. let r = skip_jsws(s, ne);
  632. let Some(r2) = bracket_span(s, r) else { continue };
  633. let r3 = skip_jsws(s, r2);
  634. if s.get(r3) != Some(&b'=') {
  635. continue;
  636. }
  637. let r4 = skip_jsws(s, r3 + 1);
  638. if s.get(r4) != Some(&b'{') {
  639. continue;
  640. }
  641. let mut tok = String::new();
  642. if with_star {
  643. tok.push('*');
  644. }
  645. tok.push_str(&bytes_to_string(&s[pos..te]));
  646. return Some((tok, r4 + 1));
  647. }
  648. }
  649. None
  650. }
  651. /// OBJ_ALIAS_RE over the continuation-joined text:
  652. /// /^[ \t]*#[ \t]*define[ \t]+(\w+)[ \t]+(?:struct[ \t]+)*[A-Za-z_]\w*[ \t\r]*$/gm
  653. fn scan_alias_names(stripped: &[u8], out: &mut Vec<String>) {
  654. // joined = stripped.replace(/\\\r?\n/g, ' ')
  655. let mut joined = Vec::with_capacity(stripped.len());
  656. let mut i = 0;
  657. while i < stripped.len() {
  658. let b = stripped[i];
  659. if b == b'\\' {
  660. if stripped.get(i + 1) == Some(&b'\n') {
  661. joined.push(b' ');
  662. i += 2;
  663. continue;
  664. }
  665. if stripped.get(i + 1) == Some(&b'\r') && stripped.get(i + 2) == Some(&b'\n') {
  666. joined.push(b' ');
  667. i += 3;
  668. continue;
  669. }
  670. }
  671. joined.push(b);
  672. i += 1;
  673. }
  674. for line in joined.split(|&b| b == b'\n') {
  675. if let Some(name) = alias_line(line) {
  676. push_str(out, name);
  677. }
  678. }
  679. }
  680. #[inline]
  681. fn skip_sp_tab(line: &[u8], mut i: usize) -> usize {
  682. while i < line.len() && (line[i] == b' ' || line[i] == b'\t') {
  683. i += 1;
  684. }
  685. i
  686. }
  687. fn alias_line(line: &[u8]) -> Option<&[u8]> {
  688. let mut i = skip_sp_tab(line, 0);
  689. if line.get(i) != Some(&b'#') {
  690. return None;
  691. }
  692. i = skip_sp_tab(line, i + 1);
  693. if line.len() < i + 6 || &line[i..i + 6] != b"define" {
  694. return None;
  695. }
  696. i += 6;
  697. let w = skip_sp_tab(line, i);
  698. if w == i || !is_word_at(line, w) {
  699. return None;
  700. }
  701. let name_end = word_end(line, w);
  702. let name = &line[w..name_end];
  703. let v0 = skip_sp_tab(line, name_end);
  704. if v0 == name_end {
  705. return None; // [ \t]+ before the value
  706. }
  707. // (?:(?:struct|union)[ \t]+)* greedy, k-descending on value failure.
  708. let mut stack = vec![v0];
  709. loop {
  710. let cur = *stack.last().unwrap();
  711. let keyword_len = if line.len() >= cur + 6 && &line[cur..cur + 6] == b"struct" {
  712. Some(6)
  713. } else if line.len() >= cur + 5 && &line[cur..cur + 5] == b"union" {
  714. Some(5)
  715. } else {
  716. None
  717. };
  718. if let Some(keyword_len) = keyword_len {
  719. let e = cur + keyword_len;
  720. let w2 = skip_sp_tab(line, e);
  721. if w2 > e {
  722. stack.push(w2);
  723. continue;
  724. }
  725. }
  726. break;
  727. }
  728. for &vp in stack.iter().rev() {
  729. let Some(&b0) = line.get(vp) else { continue };
  730. if !(b0.is_ascii_alphabetic() || b0 == b'_') {
  731. continue; // value must start [A-Za-z_]
  732. }
  733. let ve = word_end(line, vp);
  734. // [ \t\r]*$
  735. let mut t = ve;
  736. while t < line.len() && (line[t] == b' ' || line[t] == b'\t' || line[t] == b'\r') {
  737. t += 1;
  738. }
  739. if t == line.len() {
  740. return Some(name);
  741. }
  742. }
  743. None
  744. }
  745. /// FIELD_ASSIGN_RE: /(\w+)\s*(?:->|\.)\s*(\w+)\s*=\s*(\w+)\s*(?:->|\.)\s*(\w+)/g
  746. /// Pairs collected as `lfield\0rfield`. Every byte position is a candidate
  747. /// start (JS advances one unit on failure — suffix starts included); matches
  748. /// resume at their end.
  749. fn scan_field_assign(s: &[u8], out: &mut Vec<String>) {
  750. let mut pos = 0usize;
  751. while pos < s.len() {
  752. if !is_word(s[pos]) {
  753. pos += 1;
  754. continue;
  755. }
  756. match field_assign_at(s, pos) {
  757. Some((lf, rf, end)) => {
  758. let mut pair = bytes_to_string(&s[lf.0..lf.1]);
  759. pair.push('\0');
  760. pair.push_str(&bytes_to_string(&s[rf.0..rf.1]));
  761. out.push(pair);
  762. pos = end;
  763. }
  764. None => pos += 1,
  765. }
  766. }
  767. }
  768. #[inline]
  769. fn arrow_at(s: &[u8], i: usize) -> Option<usize> {
  770. if s.get(i) == Some(&b'-') && s.get(i + 1) == Some(&b'>') {
  771. Some(i + 2)
  772. } else if s.get(i) == Some(&b'.') {
  773. Some(i + 1)
  774. } else {
  775. None
  776. }
  777. }
  778. type Range = (usize, usize);
  779. fn field_assign_at(s: &[u8], p: usize) -> Option<(Range, Range, usize)> {
  780. let w1 = word_end(s, p);
  781. let a1 = arrow_at(s, skip_jsws(s, w1))?;
  782. let f1s = skip_jsws(s, a1);
  783. if !is_word_at(s, f1s) {
  784. return None;
  785. }
  786. let f1e = word_end(s, f1s);
  787. let eq = skip_jsws(s, f1e);
  788. if s.get(eq) != Some(&b'=') {
  789. return None;
  790. }
  791. let r1s = skip_jsws(s, eq + 1);
  792. if !is_word_at(s, r1s) {
  793. return None;
  794. }
  795. let r1e = word_end(s, r1s);
  796. let a2 = arrow_at(s, skip_jsws(s, r1e))?;
  797. let f2s = skip_jsws(s, a2);
  798. if !is_word_at(s, f2s) {
  799. return None;
  800. }
  801. let f2e = word_end(s, f2s);
  802. Some(((f1s, f1e), (f2s, f2e), f2e))
  803. }
  804. /// DISPATCH_RE: /((?:\w+(?:\s*\[[^\][]*\])?\s*(?:->|\.)\s*)+)(\w+)\s*\)?\s*\(/g
  805. /// The `+` loop is consumed greedily, then the field tail is tried at each
  806. /// segment count k descending — the JS engine's observable backtracking. The
  807. /// per-segment optional subscript needs no cross-product: the with/without
  808. /// parses diverge at the arrow and at most one can complete a segment.
  809. fn scan_dispatch(s: &[u8], out: &mut Vec<String>) {
  810. let mut pos = 0usize;
  811. while pos < s.len() {
  812. if !is_word(s[pos]) {
  813. pos += 1;
  814. continue;
  815. }
  816. // Greedy segment loop.
  817. let mut seg_ends: Vec<usize> = Vec::new();
  818. let mut cur = pos;
  819. while is_word_at(s, cur) {
  820. let we = word_end(s, cur);
  821. let with_sub = subscript_span(s, skip_jsws(s, we)).and_then(|e| arrow_tail(s, e));
  822. let seg = with_sub.or_else(|| arrow_tail(s, we));
  823. match seg {
  824. Some(e) => {
  825. seg_ends.push(e);
  826. cur = e;
  827. }
  828. None => break,
  829. }
  830. }
  831. let mut matched = None;
  832. for k in (1..=seg_ends.len()).rev() {
  833. let fpos = seg_ends[k - 1];
  834. if !is_word_at(s, fpos) {
  835. continue;
  836. }
  837. let fe = word_end(s, fpos);
  838. if let Some(end) = close_call_tail(s, fe) {
  839. matched = Some(((fpos, fe), end));
  840. break;
  841. }
  842. }
  843. match matched {
  844. Some(((fs_, fe), end)) => {
  845. push_str(out, &s[fs_..fe]);
  846. pos = end;
  847. }
  848. None => pos += 1,
  849. }
  850. }
  851. }
  852. /// `\[[^\][]*\]` at `i` (the DISPATCH subscript form — no nested brackets):
  853. /// position after `]`, or None.
  854. fn subscript_span(s: &[u8], i: usize) -> Option<usize> {
  855. if s.get(i) != Some(&b'[') {
  856. return None;
  857. }
  858. let mut j = i + 1;
  859. while j < s.len() && s[j] != b']' && s[j] != b'[' {
  860. j += 1;
  861. }
  862. if j < s.len() && s[j] == b']' {
  863. Some(j + 1)
  864. } else {
  865. None
  866. }
  867. }
  868. /// `\s*(?:->|\.)\s*` at `i` → position after.
  869. #[inline]
  870. fn arrow_tail(s: &[u8], i: usize) -> Option<usize> {
  871. let a = arrow_at(s, skip_jsws(s, i))?;
  872. Some(skip_jsws(s, a))
  873. }
  874. /// ARRAY_DISPATCH_RE: /(?:\(\s*\*\s*)?\b(\w+)\s*\[[^\][]*\]\s*\)?\s*\(/g
  875. fn scan_array_dispatch(s: &[u8], out: &mut Vec<String>) {
  876. let mut pos = 0usize;
  877. while pos < s.len() {
  878. let b = s[pos];
  879. if b != b'(' && !(is_word(b) && boundary_before(s, pos)) {
  880. pos += 1;
  881. continue;
  882. }
  883. let name_start = if b == b'(' {
  884. let i = skip_jsws(s, pos + 1);
  885. if s.get(i) == Some(&b'*') {
  886. let j = skip_jsws(s, i + 1);
  887. // \b holds: the previous char is `*` or whitespace.
  888. if is_word_at(s, j) { Some(j) } else { None }
  889. } else {
  890. None
  891. }
  892. } else {
  893. Some(pos)
  894. };
  895. let matched = name_start.and_then(|ns| {
  896. let ne = word_end(s, ns);
  897. let sub = subscript_span(s, skip_jsws(s, ne))?;
  898. let end = close_call_tail(s, sub)?;
  899. Some(((ns, ne), end))
  900. });
  901. match matched {
  902. Some(((ns, ne), end)) => {
  903. push_str(out, &s[ns..ne]);
  904. pos = end;
  905. }
  906. None => pos += 1,
  907. }
  908. }
  909. }
  910. /// INCLUDE_RE over RAW text: /#[ \t]*include[ \t]+"([^"\n]+)"/g
  911. fn scan_includes(raw: &[u8], out: &mut Vec<String>) {
  912. let mut pos = 0usize;
  913. while pos < raw.len() {
  914. let Some(h) = find_bytes(raw, b"#", pos) else { break };
  915. let mut i = skip_sp_tab(raw, h + 1);
  916. if raw.len() < i + 7 || &raw[i..i + 7] != b"include" {
  917. pos = h + 1;
  918. continue;
  919. }
  920. i += 7;
  921. let q = skip_sp_tab(raw, i);
  922. if q == i || raw.get(q) != Some(&b'"') {
  923. pos = h + 1;
  924. continue;
  925. }
  926. let mut j = q + 1;
  927. while j < raw.len() && raw[j] != b'"' && raw[j] != b'\n' {
  928. j += 1;
  929. }
  930. if j > q + 1 && j < raw.len() && raw[j] == b'"' {
  931. out.push(bytes_to_string(&raw[q + 1..j]));
  932. pos = j + 1;
  933. } else {
  934. pos = h + 1;
  935. }
  936. }
  937. }
  938. // ---------- struct field parsing ----------
  939. /// splitTopLevel(body, sep): split on `sep` at brace/paren/bracket depth 0.
  940. fn split_top_level(body: &[u8], sep: u8) -> Vec<Range> {
  941. let mut out = Vec::new();
  942. let mut depth = 0i64;
  943. let mut start = 0usize;
  944. for (i, &c) in body.iter().enumerate() {
  945. match c {
  946. b'{' | b'(' | b'[' => depth += 1,
  947. b'}' | b')' | b']' => depth -= 1,
  948. _ if c == sep && depth == 0 => {
  949. out.push((start, i));
  950. start = i + 1;
  951. }
  952. _ => {}
  953. }
  954. }
  955. out.push((start, body.len()));
  956. out
  957. }
  958. /// JS String.prototype.trim over bytes (the JS set == our jsws set).
  959. fn jsws_trim(s: &[u8], mut a: usize, mut b: usize) -> (usize, usize) {
  960. loop {
  961. let l = jsws_len(s, a);
  962. if l == 0 || a + l > b {
  963. break;
  964. }
  965. a += l;
  966. }
  967. // Trailing: walk from the front to find the last non-ws position (ws
  968. // lengths vary, so scan forward tracking the end of the last non-ws char).
  969. let mut i = a;
  970. let mut last_end = a;
  971. while i < b {
  972. let l = jsws_len(s, i);
  973. if l == 0 {
  974. i += 1;
  975. last_end = i;
  976. } else {
  977. i += l;
  978. }
  979. }
  980. b = last_end;
  981. (a, b)
  982. }
  983. /// /(\w+)\s+\**\s*(\w+)\s*$/ — leftmost match whose tail reaches the end.
  984. /// Deterministic per start (greedy words/ws cannot backtrack usefully);
  985. /// candidate starts advance one byte at a time like the JS engine.
  986. fn first_typed(part: &[u8]) -> Option<(Range, Range)> {
  987. let n = part.len();
  988. let mut p = 0usize;
  989. while p < n {
  990. if !is_word(part[p]) {
  991. p += 1;
  992. continue;
  993. }
  994. let te = word_end(part, p);
  995. let w = skip_jsws(part, te);
  996. if w == te {
  997. p += 1;
  998. continue;
  999. }
  1000. let mut q = w;
  1001. while q < n && part[q] == b'*' {
  1002. q += 1;
  1003. }
  1004. let q = skip_jsws(part, q);
  1005. if is_word_at(part, q) {
  1006. let ne = word_end(part, q);
  1007. let t = skip_jsws(part, ne);
  1008. if t == n {
  1009. return Some(((p, te), (q, ne)));
  1010. }
  1011. }
  1012. p += 1;
  1013. }
  1014. None
  1015. }
  1016. /// FNPTR_DECL_RE (first match): /\(\s*(?:\w+\s+)*\*\s*(\w+)\s*\)\s*\(/
  1017. fn fnptr_decl(part: &[u8]) -> Option<Range> {
  1018. let mut i = 0usize;
  1019. while i < part.len() {
  1020. if part[i] == b'(' {
  1021. if let Some((name, _)) = fnptr_paren_tail(part, i) {
  1022. return Some(name);
  1023. }
  1024. }
  1025. i += 1;
  1026. }
  1027. None
  1028. }
  1029. /// Port of `parseStructFieldsRaw` — structure only, classification TS-side.
  1030. pub fn parse_struct_fields_raw(inner: &[u8]) -> Vec<RawField> {
  1031. let mut fields = Vec::new();
  1032. let mut idx: u32 = 0;
  1033. for (ds, de) in split_top_level(inner, b';') {
  1034. let (ds, de) = jsws_trim(inner, ds, de);
  1035. if ds >= de {
  1036. continue;
  1037. }
  1038. let decl = &inner[ds..de];
  1039. let parts = split_top_level(decl, b',');
  1040. let ft = first_typed(&decl[parts[0].0..parts[0].1]);
  1041. let shared_type: &[u8] = match &ft {
  1042. Some(((ts, te), _)) => &decl[parts[0].0 + ts..parts[0].0 + te],
  1043. None => b"",
  1044. };
  1045. for (pi, &(ps, pe)) in parts.iter().enumerate() {
  1046. let (ps2, pe2) = jsws_trim(decl, ps, pe);
  1047. let p = &decl[ps2..pe2];
  1048. let mut name: &[u8] = b"";
  1049. let mut ty: &[u8] = b"";
  1050. let mut ptr = false;
  1051. if let Some((ns, ne)) = fnptr_decl(p) {
  1052. name = &p[ns..ne];
  1053. ptr = true;
  1054. } else if pi == 0 {
  1055. if let Some((_, (ns, ne))) = &ft {
  1056. name = &decl[parts[0].0 + ns..parts[0].0 + ne];
  1057. ty = shared_type;
  1058. }
  1059. } else {
  1060. // /^\**\s*(\w+)/
  1061. let mut q = 0usize;
  1062. while q < p.len() && p[q] == b'*' {
  1063. q += 1;
  1064. }
  1065. let q = skip_jsws(p, q);
  1066. if is_word_at(p, q) {
  1067. name = &p[q..word_end(p, q)];
  1068. ty = shared_type;
  1069. }
  1070. }
  1071. fields.push(RawField {
  1072. name: bytes_to_string(name),
  1073. index: idx,
  1074. ptr,
  1075. ty: bytes_to_string(ty),
  1076. });
  1077. idx += 1;
  1078. }
  1079. }
  1080. fields
  1081. }
  1082. // ---------- per-file entry ----------
  1083. fn push_str(out: &mut Vec<String>, bytes: &[u8]) {
  1084. out.push(bytes_to_string(bytes));
  1085. }
  1086. #[inline]
  1087. fn bytes_to_string(bytes: &[u8]) -> String {
  1088. // All slice boundaries land on ASCII delimiters, so the content is valid
  1089. // UTF-8 whenever the input string was; lossy keeps us total anyway.
  1090. String::from_utf8_lossy(bytes).into_owned()
  1091. }
  1092. fn dedup_in_order(v: Vec<String>) -> Vec<String> {
  1093. let mut seen = std::collections::HashSet::new();
  1094. let mut out = Vec::with_capacity(v.len());
  1095. for x in v {
  1096. if seen.insert(x.clone()) {
  1097. out.push(x);
  1098. }
  1099. }
  1100. out
  1101. }
  1102. /// Line start offsets (byte offset of each line's first byte).
  1103. fn line_starts(s: &[u8]) -> Vec<usize> {
  1104. let mut out = vec![0usize];
  1105. for (i, &b) in s.iter().enumerate() {
  1106. if b == b'\n' {
  1107. out.push(i + 1);
  1108. }
  1109. }
  1110. out
  1111. }
  1112. /// Run the full extraction sweep for one file. `raw` is the file text exactly
  1113. /// as the TS side read it; `structs` are the file's struct-node extents.
  1114. pub fn scan_file(raw: &str, structs: &[StructExtent]) -> FileFacts {
  1115. let raw_b = raw.as_bytes();
  1116. let stripped = strip_c(raw_b);
  1117. let s: &[u8] = &stripped;
  1118. let mut facts = FileFacts {
  1119. fn_ptr_typedefs: Vec::new(),
  1120. fn_type_typedefs: Vec::new(),
  1121. structs: Vec::new(),
  1122. inline_ptr: false,
  1123. inline_types: Vec::new(),
  1124. inline_tags: Vec::new(),
  1125. init_tokens: Vec::new(),
  1126. array_elems: Vec::new(),
  1127. alias_names: Vec::new(),
  1128. d_pairs: Vec::new(),
  1129. dispatch_fields: Vec::new(),
  1130. array_dispatch_names: Vec::new(),
  1131. includes: Vec::new(),
  1132. };
  1133. // Typedefs (gated like the JS sweep — purely a fast path, the scans find
  1134. // nothing without the substring anyway).
  1135. if contains_bytes(s, b"typedef") {
  1136. scan_fnptr_typedefs(s, &mut facts.fn_ptr_typedefs);
  1137. scan_fntype_typedefs(s, &mut facts.fn_type_typedefs);
  1138. }
  1139. // Struct-node field declarations.
  1140. if !structs.is_empty() {
  1141. let lines = line_starts(s);
  1142. for st in structs {
  1143. let mut sf = StructFields { id: st.id.clone(), parsed: false, fields: Vec::new() };
  1144. // sliceLinesPre: falsy startLine → '' (never parses). end_line
  1145. // arrives with the TS side's `?? startLine` already applied; a
  1146. // slice whose end ≤ start is empty, exactly like Array.slice.
  1147. if st.start_line >= 1 {
  1148. let a = (st.start_line - 1) as usize;
  1149. let b = st.end_line as usize;
  1150. if a < lines.len() && b > a {
  1151. let body_start = lines[a];
  1152. // End of line (b-1): next line start minus the `\n`, or EOF.
  1153. let body_end = if b < lines.len() { lines[b] - 1 } else { s.len() };
  1154. let body = &s[body_start..body_end.max(body_start)];
  1155. if let Some(open) = body.iter().position(|&c| c == b'{') {
  1156. if let Some(close) = match_brace(body, open) {
  1157. sf.parsed = true;
  1158. sf.fields = parse_struct_fields_raw(&body[open + 1..close]);
  1159. }
  1160. }
  1161. }
  1162. }
  1163. facts.structs.push(sf);
  1164. }
  1165. }
  1166. // Registration filters.
  1167. if contains_bytes(s, b"{") {
  1168. let inline = scan_inline_structs(s);
  1169. facts.inline_ptr = inline.ptr;
  1170. facts.inline_types = dedup_in_order(inline.types);
  1171. facts.inline_tags = dedup_in_order(inline.tags);
  1172. if contains_bytes(s, b"=") {
  1173. scan_anchored(s, init_body, &mut facts.init_tokens);
  1174. facts.init_tokens = dedup_in_order(std::mem::take(&mut facts.init_tokens));
  1175. scan_anchored(s, array_table_body, &mut facts.array_elems);
  1176. facts.array_elems = dedup_in_order(std::mem::take(&mut facts.array_elems));
  1177. }
  1178. }
  1179. // Alias-shaped object macros.
  1180. if contains_bytes(s, b"#define") || contains_bytes(s, b"# define") {
  1181. scan_alias_names(s, &mut facts.alias_names);
  1182. facts.alias_names = dedup_in_order(std::mem::take(&mut facts.alias_names));
  1183. }
  1184. // Propagation + dispatch filters.
  1185. if contains_bytes(s, b"=") {
  1186. scan_field_assign(s, &mut facts.d_pairs);
  1187. facts.d_pairs = dedup_in_order(std::mem::take(&mut facts.d_pairs));
  1188. }
  1189. scan_dispatch(s, &mut facts.dispatch_fields);
  1190. facts.dispatch_fields = dedup_in_order(std::mem::take(&mut facts.dispatch_fields));
  1191. scan_array_dispatch(s, &mut facts.array_dispatch_names);
  1192. facts.array_dispatch_names = dedup_in_order(std::mem::take(&mut facts.array_dispatch_names));
  1193. // Includes come from the RAW text (string contents survive there).
  1194. if contains_bytes(raw_b, b"include") {
  1195. scan_includes(raw_b, &mut facts.includes);
  1196. }
  1197. facts
  1198. }
  1199. #[cfg(test)]
  1200. mod tests {
  1201. use super::*;
  1202. fn facts(src: &str) -> FileFacts {
  1203. scan_file(src, &[])
  1204. }
  1205. #[test]
  1206. fn strip_blanks_comments_keeps_strings() {
  1207. let s = strip_c(b"a /* x\ny */ b // c\nd \"in//str\" e");
  1208. assert_eq!(&s, b"a \n b \nd \"in//str\" e".as_slice());
  1209. }
  1210. #[test]
  1211. fn typedef_forms() {
  1212. let f = facts("typedef void (*hook_fn)(int);\ntypedef void redisCommandProc(int c);\n");
  1213. assert_eq!(f.fn_ptr_typedefs, vec!["hook_fn"]);
  1214. assert_eq!(f.fn_type_typedefs, vec!["redisCommandProc"]);
  1215. }
  1216. #[test]
  1217. fn init_modifier_backtrack() {
  1218. // `static x = {` must match with type token `static` (the JS engine
  1219. // backtracks the modifier loop) — harmless downstream, but collected.
  1220. let f = facts("; static x = {1};\n; static struct cmd t[] = { {0} };");
  1221. assert!(f.init_tokens.contains(&"static".to_string()));
  1222. assert!(f.init_tokens.contains(&"cmd".to_string()));
  1223. }
  1224. #[test]
  1225. fn dispatch_backtracks_segments() {
  1226. let f = facts("int go(struct c *x){ x->cmd->proc(1); tbl[i](2); (*ops[k])(3); }");
  1227. assert!(f.dispatch_fields.contains(&"proc".to_string()));
  1228. assert!(f.array_dispatch_names.contains(&"tbl".to_string()));
  1229. assert!(f.array_dispatch_names.contains(&"ops".to_string()));
  1230. }
  1231. #[test]
  1232. fn field_assign_pairs() {
  1233. let f = facts("void g(void){ a->f = b->g; h.x = k.y; m == n; }");
  1234. assert!(f.d_pairs.contains(&"f\0g".to_string()));
  1235. assert!(f.d_pairs.contains(&"x\0y".to_string()));
  1236. assert_eq!(f.d_pairs.len(), 2);
  1237. }
  1238. #[test]
  1239. fn alias_shapes() {
  1240. let f = facts("#define A redisCommand\n#define B struct foo\n#define C 0x12\n#define D(x) x\n");
  1241. assert!(f.alias_names.contains(&"A".to_string()));
  1242. assert!(f.alias_names.contains(&"B".to_string()));
  1243. assert!(!f.alias_names.contains(&"C".to_string()));
  1244. assert!(!f.alias_names.contains(&"D".to_string()));
  1245. }
  1246. #[test]
  1247. fn includes_from_raw() {
  1248. let f = facts("#include \"commands.def\"\n// #include \"in-comment.h\"\n");
  1249. // Raw-text scan: the commented include IS captured (parity with the
  1250. // JS INCLUDE_RE over raw text).
  1251. assert_eq!(f.includes, vec!["commands.def", "in-comment.h"]);
  1252. }
  1253. }