backhopper_core/compat/
source_macros.rs1use std::collections::{BTreeMap, BTreeSet};
12use std::path::{Path, PathBuf};
13
14use backhopper_erlang_scan::{
15 dot_terminates, is_name_byte, number_span, quoted_atom_span, skip_char_literal_span,
16 string_span,
17};
18
19use crate::erlang_macros::{MacroTable, parse_define};
20
21pub trait HeaderResolver {
25 fn resolve(&self, source_dir: Option<&Path>, raw: &Path) -> Option<HeaderBody>;
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct HeaderBody {
30 pub path: PathBuf,
31 pub body: String,
32}
33
34pub fn build_macro_table(
38 source: &str,
39 source_path: &Path,
40 resolver: &dyn HeaderResolver,
41) -> MacroTable {
42 let mut macros = MacroTable::new();
43 let mut seen: BTreeSet<PathBuf> = BTreeSet::new();
44 let mut pending: Vec<PathBuf> = Vec::new();
45 fold_attributes(source, &mut macros, &mut pending);
46 let source_dir = source_path.parent().map(Path::to_path_buf);
47 while let Some(raw) = pending.pop() {
48 let Some(entry) = resolver.resolve(source_dir.as_deref(), &raw) else {
49 continue;
50 };
51 if !seen.insert(entry.path.clone()) {
52 continue;
53 }
54 fold_attributes(&entry.body, &mut macros, &mut pending);
55 }
56 macros
57}
58
59fn fold_attributes(source: &str, macros: &mut MacroTable, pending: &mut Vec<PathBuf>) {
62 let bytes = source.as_bytes();
63 let mut i = 0usize;
64 let mut at_line_start = true;
65 while i < bytes.len() {
66 let c = bytes[i];
67 if c == b'\n' {
68 at_line_start = true;
69 i += 1;
70 continue;
71 }
72 if c == b'%' {
73 while i < bytes.len() && bytes[i] != b'\n' {
74 i += 1;
75 }
76 continue;
77 }
78 if c.is_ascii_whitespace() {
79 i += 1;
80 continue;
81 }
82 if at_line_start && c == b'-' {
83 i += 1;
84 let name_start = i;
85 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
86 i += 1;
87 }
88 let name = &source[name_start..i];
89 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
90 i += 1;
91 }
92 let body_start = i;
93 i = scan_to_terminator(source, i);
94 let body = &source[body_start..i];
95 if i < bytes.len() && bytes[i] == b'.' {
96 i += 1;
97 }
98 match name {
99 "define" => {
100 let trimmed = body
101 .trim()
102 .strip_prefix('(')
103 .map(|x| x.strip_suffix(')').unwrap_or(x))
104 .unwrap_or(body);
105 if let Some((key, value)) = parse_define(trimmed) {
106 macros.insert(key, value);
107 }
108 }
109 "include" | "include_lib" => {
110 if let Some(path) = parse_include_path(body) {
111 pending.push(path);
112 }
113 }
114 _ => {}
115 }
116 at_line_start = false;
117 continue;
118 }
119 at_line_start = false;
120 match c {
121 b'"' | b'~' => i += string_span(bytes, i).unwrap_or(1),
122 b'\'' => i += quoted_atom_span(bytes, i).unwrap_or(1),
123 b'$' => i += skip_char_literal_span(bytes, i),
124 _ => i += 1,
125 }
126 }
127}
128
129fn scan_to_terminator(source: &str, start: usize) -> usize {
130 let bytes = source.as_bytes();
131 let mut i = start;
132 let mut depth_paren = 0i32;
133 let mut depth_brack = 0i32;
134 let mut depth_brace = 0i32;
135 while i < bytes.len() {
136 let c = bytes[i];
137 if c.is_ascii_digit() && (i == start || !is_name_byte(bytes[i - 1])) {
138 i += number_span(bytes, i).unwrap_or(1);
139 continue;
140 }
141 match c {
142 b'(' => {
143 depth_paren += 1;
144 i += 1;
145 }
146 b'[' => {
147 depth_brack += 1;
148 i += 1;
149 }
150 b'{' => {
151 depth_brace += 1;
152 i += 1;
153 }
154 b')' => {
155 depth_paren -= 1;
156 i += 1;
157 }
158 b']' => {
159 depth_brack -= 1;
160 i += 1;
161 }
162 b'}' => {
163 depth_brace -= 1;
164 i += 1;
165 }
166 b'.' if depth_paren <= 0
167 && depth_brack <= 0
168 && depth_brace <= 0
169 && dot_terminates(bytes, i) =>
170 {
171 return i;
172 }
173 b'"' | b'~' => i += string_span(bytes, i).unwrap_or(1),
174 b'\'' => i += quoted_atom_span(bytes, i).unwrap_or(1),
175 b'$' => i += skip_char_literal_span(bytes, i),
176 b'%' => {
177 while i < bytes.len() && bytes[i] != b'\n' {
178 i += 1;
179 }
180 }
181 _ => i += 1,
182 }
183 }
184 i
185}
186
187fn parse_include_path(body: &str) -> Option<PathBuf> {
188 let s = body
189 .trim()
190 .trim_start_matches('(')
191 .trim_end_matches(')')
192 .trim();
193 let s = s.strip_prefix('"')?;
194 let s = s.strip_suffix('"')?;
195 if s.is_empty() {
196 return None;
197 }
198 Some(PathBuf::from(s))
199}
200
201#[derive(Debug, Default)]
204pub struct FileMap {
205 by_path: BTreeMap<PathBuf, String>,
206 by_basename: BTreeMap<String, Vec<HeaderBody>>,
207 by_include_lib: BTreeMap<String, HeaderBody>,
208}
209
210impl FileMap {
211 pub fn new() -> Self {
212 Self::default()
213 }
214
215 pub fn insert(&mut self, path: PathBuf, body: String) {
216 let base = path.file_name().and_then(|n| n.to_str()).map(str::to_owned);
217 let lib_key = include_lib_key(&path);
218 if let Some(base) = base {
219 self.by_basename.entry(base).or_default().push(HeaderBody {
220 path: path.clone(),
221 body: body.clone(),
222 });
223 }
224 if let Some(key) = lib_key {
225 self.by_include_lib.insert(
226 key,
227 HeaderBody {
228 path: path.clone(),
229 body: body.clone(),
230 },
231 );
232 }
233 self.by_path.insert(path, body);
234 }
235
236 pub fn get(&self, path: &Path) -> Option<&str> {
237 self.by_path.get(path).map(String::as_str)
238 }
239}
240
241impl HeaderResolver for FileMap {
242 fn resolve(&self, source_dir: Option<&Path>, raw: &Path) -> Option<HeaderBody> {
243 if let Some(dir) = source_dir {
244 let candidate = dir.join(raw);
245 if let Some(body) = self.by_path.get(&candidate) {
246 return Some(HeaderBody {
247 path: candidate,
248 body: body.clone(),
249 });
250 }
251 }
252 if let Some(body) = self.by_path.get(raw) {
253 return Some(HeaderBody {
254 path: raw.to_path_buf(),
255 body: body.clone(),
256 });
257 }
258 let key = raw.to_string_lossy();
259 if let Some(entry) = self.by_include_lib.get(key.as_ref()) {
260 return Some(entry.clone());
261 }
262 let base = raw.file_name().and_then(|n| n.to_str())?;
263 self.by_basename.get(base).and_then(|v| v.first().cloned())
264 }
265}
266
267fn include_lib_key(path: &Path) -> Option<String> {
268 let s = path.to_string_lossy();
269 let parts: Vec<&str> = s.split('/').collect();
270 let include_idx = parts.iter().position(|p| *p == "include")?;
271 if include_idx == 0 {
272 return None;
273 }
274 let app = parts[include_idx - 1];
275 let rest = &parts[include_idx..];
276 let tail = rest.join("/");
277 Some(format!("{app}/{tail}"))
278}