1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
use std::collections::HashMap;
use super::error::{AutosaveError, AutosaveResult};
use crate::server::db_loader::MacroFault;
/// Macro expansion context for `$(KEY)` and `${KEY}` patterns.
#[derive(Debug, Clone, Default)]
pub struct MacroContext {
macros: HashMap<String, String>,
}
impl MacroContext {
pub fn new() -> Self {
Self::default()
}
pub fn from_map(macros: HashMap<String, String>) -> Self {
Self { macros }
}
/// Parse inline macro definitions like `"P=IOC:,M=m1"` into their RAW
/// values — no `$(...)` expansion, matching C `macParseDefns`
/// (`macUtil.c:74-196`), which keeps values verbatim because "unlike
/// values, they will not be re-parsed". The caller expands each value
/// afterwards, which is the order that keeps a comma arriving from
/// inside a value from terminating the definition after it.
///
/// The grammar comes from
/// [`macro_defn_pairs`](crate::server::iocsh::macro_defn_pairs), the
/// crate's one owner of it, so a quoted or escaped comma (`S='a,b'`)
/// stays inside its value here exactly as it does for `dbLoadRecords`.
/// A fragment with no `=` is C's macro DELETION; autosave has no
/// delete path, so it is ignored rather than silently defining an
/// empty macro.
pub fn parse_inline(s: &str) -> HashMap<String, String> {
crate::server::iocsh::macro_defn_pairs(s)
.into_iter()
.filter(|(name, _)| !name.is_empty())
.filter_map(|(name, value)| value.map(|v| (name, v)))
.collect()
}
/// Create a child context by merging additional macros (child overrides parent).
pub fn with_overrides(&self, overrides: &HashMap<String, String>) -> Self {
let mut merged = self.macros.clone();
merged.extend(overrides.iter().map(|(k, v)| (k.clone(), v.clone())));
Self { macros: merged }
}
/// Expand `$(...)` / `${...}` macro references in `input` using the
/// crate's single macLib engine
/// ([`crate::server::db_loader::expand_macros`]), so autosave `.req`
/// expansion gets the full language — nested defaults
/// (`$(BAR=$(FOO))`), scoped definitions (`$(X,X=$(Y))`),
/// name-of-name (`$($(WHICH))`), chained expansion, single-quote
/// suppression — not just the flat `$(KEY)` / `$(KEY=default)` subset.
///
/// Autosave-specific options: `env_fallback` (C `macEnvExpand`) and
/// `dollar_escape` (`$$` → literal `$`, a `.req` convenience).
///
/// Every fault the engine can record is a hard error here — an
/// undefined macro with no default, a macro that resolves into
/// itself, and a reference whose closing delimiter never arrived —
/// because all three leave text where the `.req` wanted a PV name,
/// and a `.req` line built from a placeholder (or from a raw
/// passed-through `$(`) names a PV that does not exist. Which one it
/// was is read from
/// [`MacroExpansion::fault`], never from one of its lists, so a new
/// fault arm cannot be silently accepted here.
///
/// [`MacroExpansion::fault`]: crate::server::db_loader::MacroExpansion::fault
pub fn expand(&self, input: &str, source: &str, line: usize) -> AutosaveResult<String> {
let result = crate::server::db_loader::expand_macros(
input,
&self.macros,
crate::server::db_loader::MacroExpandOptions {
env_fallback: true,
dollar_escape: true,
// Not a base C path, and this caller turns the first
// fault into an error rather than accepting the
// placeholder — so macLib's own warning would be noise
// printed just before a hard failure that names the same
// macro.
suppress_warnings: true,
},
);
let fault = result.fault().map(|fault| match fault {
MacroFault::Undefined(key) => AutosaveError::UndefinedMacro {
key: key.to_string(),
source: source.to_string(),
line,
},
MacroFault::Recursive(key) => AutosaveError::RecursiveMacro {
key: key.to_string(),
source: source.to_string(),
line,
},
MacroFault::Unterminated(reference) => AutosaveError::UnterminatedMacro {
reference: reference.to_string(),
source: source.to_string(),
line,
},
});
match fault {
Some(err) => Err(err),
None => Ok(result.text),
}
}
pub fn get(&self, key: &str) -> Option<&str> {
self.macros.get(key).map(|s| s.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_expand() {
let ctx = MacroContext::from_map([("P".into(), "IOC:".into())].into());
assert_eq!(ctx.expand("$(P)temp", "test", 1).unwrap(), "IOC:temp");
}
#[test]
fn test_default_value() {
let ctx = MacroContext::new();
assert_eq!(ctx.expand("$(P=DEFAULT)", "test", 1).unwrap(), "DEFAULT");
}
#[test]
fn test_undefined_error() {
let ctx = MacroContext::new();
let err = ctx.expand("$(UNDEF)", "test.req", 5).unwrap_err();
match err {
AutosaveError::UndefinedMacro { key, source, line } => {
assert_eq!(key, "UNDEF");
assert_eq!(source, "test.req");
assert_eq!(line, 5);
}
_ => panic!("expected UndefinedMacro"),
}
}
#[test]
fn test_parse_inline() {
let map = MacroContext::parse_inline("P=IOC:,M=m1");
assert_eq!(map.get("P").unwrap(), "IOC:");
assert_eq!(map.get("M").unwrap(), "m1");
}
#[test]
fn test_dollar_literal() {
let ctx = MacroContext::new();
assert_eq!(ctx.expand("$$100", "test", 1).unwrap(), "$100");
}
#[test]
fn test_both_pv_and_path() {
let ctx = MacroContext::from_map(
[
("P".into(), "IOC:".into()),
("FILE".into(), "settings".into()),
]
.into(),
);
assert_eq!(
ctx.expand("${FILE}/$(P)temp", "test", 1).unwrap(),
"settings/IOC:temp"
);
}
// The full macLib language now reaches autosave .req expansion via
// the shared engine — previously `expand` only did the flat
// `$(KEY)` / `$(KEY=default)` subset.
#[test]
fn nested_default_is_expanded() {
// `${BAR=${FOO}}`: BAR unset, default is itself a macro ref.
let ctx = MacroContext::from_map([("FOO".into(), "fromfoo".into())].into());
assert_eq!(ctx.expand("${BAR=${FOO}}", "test", 1).unwrap(), "fromfoo");
}
#[test]
fn scoped_definition_is_honored() {
// `$(INNER,A=$(FOO))`: A is defined only for this reference and
// its value is itself expanded.
let ctx = MacroContext::from_map(
[
("INNER".into(), "$(A)".into()),
("FOO".into(), "scoped".into()),
]
.into(),
);
assert_eq!(
ctx.expand("$(INNER,A=$(FOO))", "test", 1).unwrap(),
"scoped"
);
}
#[test]
fn resolved_value_is_chained() {
// P=$(Q), Q=IOC: → $(P) expands through to IOC:.
let ctx = MacroContext::from_map(
[("P".into(), "$(Q)".into()), ("Q".into(), "IOC:".into())].into(),
);
assert_eq!(ctx.expand("$(P)TEMP", "test", 1).unwrap(), "IOC:TEMP");
}
#[test]
fn undefined_without_default_still_errors() {
let ctx = MacroContext::new();
let err = ctx.expand("$(NOPE)", "f.req", 7).unwrap_err();
match err {
AutosaveError::UndefinedMacro { key, source, line } => {
assert_eq!(key, "NOPE");
assert_eq!(source, "f.req");
assert_eq!(line, 7);
}
other => panic!("expected UndefinedMacro, got {other:?}"),
}
}
/// A `.req` whose macros resolve into one another expanded to the
/// recursion placeholder and returned `Ok`, because `expand` read
/// the undefined list and a recursive name is never in it. The
/// caller then built a PV name out of the placeholder.
#[test]
fn recursive_macro_is_an_error_not_placeholder_text() {
let ctx = MacroContext::from_map(
[("A".into(), "$(B)".into()), ("B".into(), "$(A)".into())].into(),
);
let err = ctx.expand("$(A)", "cycle.req", 7).unwrap_err();
match err {
AutosaveError::RecursiveMacro { key, source, line } => {
assert_eq!(key, "A");
assert_eq!(source, "cycle.req");
assert_eq!(line, 7);
}
other => panic!("expected RecursiveMacro, got {other:?}"),
}
}
}