Skip to main content

backhopper_core/model/
macro_decl.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! A header's `-define`: the macro's identity and what it expands to.
6
7use std::fmt;
8use std::str::FromStr;
9
10use backhopper_erlang_scan::{count_top_level_commas, take_balanced_parens};
11use serde::{Deserialize, Serialize};
12
13use crate::errors::NameError;
14use crate::model::names::{Arity, MacroName};
15use crate::model::normalised::NormalisedMacroBody;
16
17/// Erlang treats `NAME` and `NAME(Args)` as different macros, and macros of
18/// different arities as different macros, so the arity is part of the key.
19#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
20pub struct MacroKey {
21    pub name: MacroName,
22    pub arity: Option<Arity>,
23}
24
25impl MacroKey {
26    #[must_use]
27    pub fn object_like(name: MacroName) -> Self {
28        Self { name, arity: None }
29    }
30
31    #[must_use]
32    pub fn function_like(name: MacroName, arity: Arity) -> Self {
33        Self {
34            name,
35            arity: Some(arity),
36        }
37    }
38
39    #[must_use]
40    pub fn parse_define(body: &str) -> Option<(Self, &str)> {
41        let trimmed = body.trim_start();
42        let name_end = trimmed
43            .char_indices()
44            .find(|(_, c)| !(c.is_ascii_alphanumeric() || *c == '_' || *c == '@'))
45            .map_or(trimmed.len(), |(i, _)| i);
46        let name = MacroName::new(&trimmed[..name_end]).ok()?;
47        let rest = trimmed[name_end..].trim_start();
48        let (arity, after) = if rest.starts_with('(') {
49            let (args, after_args) = take_balanced_parens(rest)?;
50            let count = if args.trim().is_empty() {
51                0
52            } else {
53                count_top_level_commas(args) + 1
54            };
55            (Some(Arity::try_from(count).ok()?), after_args.trim_start())
56        } else {
57            (None, rest)
58        };
59        let value = after.strip_prefix(',')?;
60        Some((Self { name, arity }, value))
61    }
62}
63
64impl fmt::Display for MacroKey {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self.arity {
67            Some(arity) => write!(f, "{}/{arity}", self.name),
68            None => write!(f, "{}", self.name),
69        }
70    }
71}
72
73impl FromStr for MacroKey {
74    type Err = NameError;
75
76    fn from_str(s: &str) -> Result<Self, NameError> {
77        match s.split_once('/') {
78            Some((name, arity)) => Ok(Self::function_like(name.parse()?, arity.parse()?)),
79            None => Ok(Self::object_like(s.parse()?)),
80        }
81    }
82}
83
84/// `Conditional` is a macro defined under any `-ifdef`, `-ifndef` or `-if`
85/// other than a `TEST` guard: no environment here could say which branch is
86/// live, so no body is recorded.
87#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
88pub enum MacroBody {
89    Text(NormalisedMacroBody),
90    Conditional,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
94pub struct MacroDecl {
95    pub key: MacroKey,
96    pub body: MacroBody,
97}
98
99impl MacroDecl {
100    #[must_use]
101    pub fn merge(self, other: Self) -> Self {
102        if self.body == other.body {
103            return self;
104        }
105        Self {
106            key: self.key,
107            body: MacroBody::Conditional,
108        }
109    }
110}