backhopper_core/
erlang_macros.rs1use std::collections::BTreeMap;
13
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
15pub struct MacroKey {
16 pub name: String,
17 pub arity: Option<u8>,
18}
19
20pub type MacroTable = BTreeMap<MacroKey, String>;
21
22pub fn parse_define(body: &str) -> Option<(MacroKey, String)> {
26 let s = body.trim();
27 let s = s
28 .strip_prefix('(')
29 .map(|x| x.strip_suffix(')').unwrap_or(x))
30 .unwrap_or(s);
31 let (name_part, value_part) = split_define_head(s)?;
32 if let Some((name, params)) = parse_parameterized_head(name_part) {
33 let arity = u8::try_from(params).ok()?;
34 return Some((
35 MacroKey {
36 name,
37 arity: Some(arity),
38 },
39 value_part.trim().to_owned(),
40 ));
41 }
42 let name = name_part.trim().to_owned();
43 if name.is_empty() {
44 return None;
45 }
46 Some((MacroKey { name, arity: None }, value_part.trim().to_owned()))
47}
48
49fn split_define_head(s: &str) -> Option<(&str, &str)> {
50 let bytes = s.as_bytes();
51 let mut depth = 0i32;
52 let mut in_str = false;
53 let mut in_atom = false;
54 let mut i = 0usize;
55 while i < bytes.len() {
56 let c = bytes[i];
57 if c == b'\\' && (in_str || in_atom) {
58 i = (i + 2).min(bytes.len());
59 continue;
60 }
61 match c {
62 b'"' if !in_atom => in_str = !in_str,
63 b'\'' if !in_str => in_atom = !in_atom,
64 b'(' | b'[' | b'{' if !in_str && !in_atom => depth += 1,
65 b')' | b']' | b'}' if !in_str && !in_atom => depth -= 1,
66 b',' if depth == 0 && !in_str && !in_atom => {
67 return Some((&s[..i], &s[i + 1..]));
68 }
69 _ => {}
70 }
71 i += 1;
72 }
73 None
74}
75
76fn parse_parameterized_head(s: &str) -> Option<(String, usize)> {
77 let s = s.trim();
78 let (name, after) = s.split_once('(')?;
79 let after = after.strip_suffix(')')?;
80 let name = name.trim().to_owned();
81 if name.is_empty() {
82 return None;
83 }
84 let trimmed = after.trim();
85 if trimmed.is_empty() {
86 return Some((name, 0));
87 }
88 let params = trimmed.split(',').count();
89 Some((name, params))
90}
91
92pub fn expand_value_macro_to_atom(table: &MacroTable, name: &str) -> Option<String> {
96 let body = table.get(&MacroKey {
97 name: name.to_owned(),
98 arity: None,
99 })?;
100 let trimmed = body.trim();
101 if looks_like_lowercase_atom(trimmed) {
102 Some(trimmed.to_owned())
103 } else {
104 None
105 }
106}
107
108pub fn expand_value_macro_to_mf(table: &MacroTable, name: &str) -> Option<(String, String)> {
111 let body = table.get(&MacroKey {
112 name: name.to_owned(),
113 arity: None,
114 })?;
115 let trimmed = body.trim();
116 let (m, f) = trimmed.split_once(':')?;
117 let m = m.trim();
118 let f = f.trim();
119 if looks_like_lowercase_atom(m) && looks_like_lowercase_atom(f) {
120 Some((m.to_owned(), f.to_owned()))
121 } else {
122 None
123 }
124}
125
126fn looks_like_lowercase_atom(s: &str) -> bool {
127 let mut chars = s.chars();
128 match chars.next() {
129 Some(c) if c.is_ascii_lowercase() => {}
130 _ => return false,
131 }
132 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '@')
133}