epics_libcom_rs/runtime/mac_lib.rs
1//! libCom `macLib`: the `$(name)` / `${name}` expansion engine (`macCore.c`)
2//! and the `name=value,...` definition grammar (`macUtil.c`).
3//!
4//! It sits in this crate, not under the database loader that is its heaviest
5//! user, for the reason C has it in libCom: a consumer with a macro string to honour — an
6//! areaDetector `NDAttributesMacros`, a `seq` program's arguments — need not
7//! link the database to do it.
8
9use std::collections::HashMap;
10
11/// Resolution options for the macLib expansion engine ([`expand_macros`]).
12#[derive(Clone, Copy, Debug, Default)]
13pub struct MacroExpandOptions {
14 /// Fall back to the process environment when a name is unset,
15 /// matching C `macCreateHandle(&h, environ)`. The `.db` parser
16 /// leaves this off (its substitutions come only from the
17 /// `dbLoadRecords` / `.substitutions` macro map); `dbLoadGroup`
18 /// and autosave turn it on.
19 pub env_fallback: bool,
20 /// Treat `$$` as a literal `$`. An autosave `.req` convenience, NOT
21 /// a macLib behavior — C macLib leaves `$$` verbatim (`$` is only
22 /// special before `(`/`{`), so the `.db` parser leaves it off.
23 pub dollar_escape: bool,
24 /// C `macSuppressWarning` / `FLAG_SUPPRESS_WARNINGS`
25 /// (`macCore.c:155-168`). It silences the `macLib:` diagnostics AND
26 /// changes the text: a suppressed undefined reference is written back
27 /// as `$(name)` where a warned one is `$(name,undefined)`
28 /// (`refer`, `macCore.c:920-928`). Off is C's default; the `.db`
29 /// loader turns it on from `dbQuietMacroWarnings`
30 /// (`dbLexRoutines.c:58`, `:273`) and `msi` turns it on outright
31 /// (`msi.cpp:154`).
32 pub suppress_warnings: bool,
33}
34
35/// Outcome of [`expand_macros`]: the expanded text, plus the fault that
36/// made it wrong if one did.
37///
38/// Three faults reach here — a name with no definition, a name that
39/// resolves into itself, and a reference whose closing delimiter never
40/// matched its opener — and they are C's single `entry->error`
41/// (`macCore.c:216-224`) split by cause. All three lists are therefore
42/// private, and the only way to read them is [`Self::fault`], or
43/// [`Self::errored`] for the same answer as a bool. A consumer able to
44/// reach one list directly hard-fails on the fault it named and returns
45/// the other one's broken text as success; that is not hypothetical, it
46/// is what the autosave `.req` reader did for as long as `undefined`
47/// was `pub`.
48#[derive(Clone, Debug, Default)]
49pub struct MacroExpansion {
50 pub text: String,
51 faults: MacroFaults,
52}
53
54/// C `MAC_ENTRY.error`, kept as the names that set it rather than a
55/// bool, and kept as ONE value because C keeps one: `refer` translates a
56/// reference name and a used default through the caller's own `entry`
57/// so their faults land here, and translates scoped definitions through
58/// a separate `MAC_ENTRY subs` whose `error` is never merged back
59/// (`macCore.c:820-826`). That second rule is why this is a struct and
60/// not three loose fields on [`ExpandCtx`] — [`ExpandCtx::detached`]
61/// swaps the whole set out for the scoped region in one move, so no
62/// future arm can leak into the caller by being added to only two of
63/// three lists.
64#[derive(Clone, Debug, Default)]
65struct MacroFaults {
66 /// Every macro referenced with neither a definition (nor an env
67 /// value, when `env_fallback`) nor a default. The text still carries
68 /// C's `$(name,undefined)` placeholder for each
69 /// (`refer:errval = ",undefined)"`).
70 undefined: Vec<String>,
71 /// Every macro this expansion refused to resolve because resolving
72 /// it re-entered itself — C `refer` finding `refentry->visited`
73 /// already set (`macCore.c:895-904`). Separate from
74 /// [`Self::undefined`] because the two are different faults with
75 /// different placeholders, and a caller that hard-fails on an
76 /// undefined name must not be told a recursive one was undefined.
77 recursive: Vec<String>,
78 /// Every `$(`/`${` whose closing delimiter never arrived — C
79 /// `refer`'s first error arm (`macCore.c:862-875`). Each entry is
80 /// the raw text C copied through verbatim, from the `$` to the end
81 /// of the string, because that arm writes no placeholder and names
82 /// no macro: the reference never became a name to look up.
83 unterminated: Vec<String>,
84}
85
86/// Which fault an expansion hit, and the first text that hit it — the
87/// macro name for the two arms that parsed one, the copied-through
88/// source for the arm that did not. One variant per way C sets
89/// `entry->error`, so a caller cannot report a recursion as an undefined
90/// name, nor either of them as a reference that was never closed.
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92pub enum MacroFault<'a> {
93 /// No definition, no env value, no default — C's
94 /// `$(name,undefined)` placeholder (`macCore.c:913-928`).
95 Undefined(&'a str),
96 /// Resolving the name re-entered itself — C's `refentry->visited`
97 /// guard (`macCore.c:895-904`).
98 Recursive(&'a str),
99 /// The closing delimiter never matched the opener, so C copied the
100 /// reference and everything after it through verbatim
101 /// (`macCore.c:862-875`). The payload is that copied text — this arm
102 /// carries no macro NAME because the reference never produced one.
103 Unterminated(&'a str),
104}
105
106impl MacroExpansion {
107 /// The single owner of "did this expansion come out wrong, and
108 /// why". Every arm is read here so that no caller can read only
109 /// some of them.
110 ///
111 /// A macro is never in more than one list: `recursive` is recorded
112 /// only where the name resolved to a table entry, `undefined` only
113 /// where it resolved to nothing, `unterminated` only where no name
114 /// was ever parsed. So the order below decides nothing but which of
115 /// several independent faults a string carrying more than one
116 /// reports first.
117 #[must_use]
118 pub fn fault(&self) -> Option<MacroFault<'_>> {
119 if let Some(name) = self.faults.undefined.first() {
120 return Some(MacroFault::Undefined(name.as_str()));
121 }
122 if let Some(name) = self.faults.recursive.first() {
123 return Some(MacroFault::Recursive(name.as_str()));
124 }
125 self.faults
126 .unterminated
127 .first()
128 .map(|raw| MacroFault::Unterminated(raw.as_str()))
129 }
130
131 /// C `MAC_ENTRY.error`: whether the expansion came out wrong, by any
132 /// of the arms that set it. `macExpandString` returns a negative
133 /// length for all of them alike (`macCore.c:216-224`), and that
134 /// negative length is the whole of what the `.db` reader's per-line
135 /// warning and `macDefExpand`'s `NULL` are looking at. Delegates to
136 /// [`Self::fault`] so the bool and the name can never disagree.
137 #[must_use]
138 pub fn errored(&self) -> bool {
139 self.fault().is_some()
140 }
141}
142
143/// C `entry->type`: the word every `macLib:` notice prints in front of
144/// the entry name, and the reason those notices are not all about
145/// `string`. C carries six (`macCore.c:208`, `:283`, `:418`, `:595`,
146/// `:811`, `:826`); the `"default value"` seat is the one this port has
147/// no use for, because it slices a default out instead of running C's
148/// discarding first pass over it.
149const KIND_STRING: &str = "string";
150const KIND_MACRO: &str = "macro";
151const KIND_ENVIRONMENT: &str = "environment variable";
152const KIND_SCOPE_MARKER: &str = "scope marker";
153const KIND_SCOPED_MACRO: &str = "scoped macro";
154
155/// One `MAC_ENTRY.error`, carrying the text that raised it.
156///
157/// C keeps a bare `int error` and merges it with `entry->error =
158/// entry->error || refentry->error` (`macCore.c:885`), which is all its
159/// callers need: every one of them only compares `macExpandString`'s
160/// returned length against zero. This port's callers ask WHICH fault
161/// ([`MacroExpansion::fault`]), so the cause travels with the flag and a
162/// fault merged out of a cached value still names the macro that broke.
163#[derive(Clone, Debug)]
164enum TableFault {
165 Undefined(String),
166 Recursive(String),
167 Unterminated(String),
168}
169
170impl MacroFaults {
171 /// The only way a fault gets in, so an arm added to [`MacroFault`]
172 /// cannot be routed to the wrong list or to no list at all.
173 fn raise(&mut self, fault: TableFault) {
174 match fault {
175 TableFault::Undefined(name) => self.undefined.push(name),
176 TableFault::Recursive(name) => self.recursive.push(name),
177 TableFault::Unterminated(text) => self.unterminated.push(text),
178 }
179 }
180
181 /// C `entry->error = entry->error || refentry->error`
182 /// (`macCore.c:885`): a reference that resolves to a cached value
183 /// inherits that value's fault.
184 ///
185 /// C inherits one bit and so cannot say what the macro's fault was;
186 /// this inherits the causes, which costs nothing — only
187 /// [`MacroExpansion::fault`] reads them, and it reads the first.
188 fn merge(&mut self, other: &MacroFaults) {
189 self.undefined.extend_from_slice(&other.undefined);
190 self.recursive.extend_from_slice(&other.recursive);
191 self.unterminated.extend_from_slice(&other.unterminated);
192 }
193
194 /// Re-seat every recursion in this set onto `name`.
195 ///
196 /// A recursion is a property of the macro that could not be
197 /// resolved, not of the inner reference the resolution had to refuse
198 /// to find that out — C's own notice says so in as many words,
199 /// `macro A is recursive (expanding macro B)` (`macCore.c:895-901`).
200 /// So a fault merged out of `A`'s cached value reports `A`, which is
201 /// the name the caller wrote. The other two arms are properties of
202 /// something INSIDE the value — a name that resolves to nothing, a
203 /// bracket that never closes — and keep their own text.
204 fn rename_recursive(&mut self, name: &str) {
205 for entry in &mut self.recursive {
206 name.clone_into(entry);
207 }
208 }
209}
210
211/// C `MAC_ENTRY` (`macLib.h:34-45`): one macro, holding both the
212/// definition as given and the expansion cached from it.
213///
214/// The cache is the point of the type. C expands every raw value into
215/// `entry->value` in one pass over the whole table and then resolves a
216/// reference by COPYING that value (`refer`, `macCore.c:882-886`), so a
217/// macro whose own value is faulty raises its notice once, under its own
218/// name, before any caller's string is looked at — and every later
219/// reference to it reports the cached fault instead of deriving a fresh
220/// one from a different seat.
221#[derive(Clone, Debug)]
222struct MacEntry {
223 /// C `entry->name`. The scope markers carry the literal `<scope>`.
224 name: String,
225 /// C `entry->type` — one of the `KIND_*` constants above.
226 kind: &'static str,
227 /// C `entry->rawval`: the definition exactly as given.
228 rawval: String,
229 /// C `entry->value`: the definition with its own references
230 /// resolved. `None` until [`expand_table`] fills it, and set back to
231 /// `None` by any redefinition of this entry.
232 value: Option<String>,
233 /// C `entry->error`, as the causes that raised it — see
234 /// [`TableFault`]. Reset at the top of every [`expand_table`] pass,
235 /// exactly where C resets the bool (`macCore.c:670`).
236 faults: MacroFaults,
237 /// C `entry->visited`: raised around a translation of THIS entry's
238 /// raw value, so a reference that comes back round to it is refused
239 /// rather than followed (`macCore.c:888-893`).
240 visited: bool,
241 /// C `entry->special`: a `<scope>` marker rather than a macro
242 /// (`macPushScope`, `macCore.c:416-419`).
243 special: bool,
244 /// C `entry->level`: the scope depth this entry was defined at,
245 /// which decides whether a redefinition overwrites it or shadows it.
246 level: usize,
247}
248
249/// The `macPutValue` calls a caller wants made, in the order it wants
250/// them made in.
251///
252/// C builds its table one `macPutValue` at a time — `macInstallMacros`
253/// walks the `pairs` array `macParseDefns` produced, in file order
254/// (`macUtil.c:250-275`) — and `expand` then walks the table in that
255/// same order (`macCore.c:655`), so the sequence a `.db` load's macLib
256/// notices come out in is the sequence the definitions were written in.
257/// A [`HashMap`] cannot carry that, and sorting its keys by name only
258/// looked right because the shapes measured so far happened to be in
259/// alphabetical order already.
260///
261/// So the order is carried here instead, and the conversion from a
262/// [`HashMap`] is the one place the loss is named: those definitions
263/// arrive in no order at all, and sorting them by name at least makes
264/// the notices reproducible from run to run.
265#[derive(Clone, Debug, Default, PartialEq, Eq)]
266pub struct MacroDefs {
267 /// Name/value pairs in definition order, at most one entry per name.
268 defs: Vec<(String, String)>,
269}
270
271impl MacroDefs {
272 /// An empty set of definitions.
273 #[must_use]
274 pub fn new() -> Self {
275 Self::default()
276 }
277
278 /// C `macPutValue` (`macCore.c:262-289`): a redefinition replaces the
279 /// value and keeps the entry where it is, because `rawval` writes
280 /// through the entry `lookup` found rather than appending a second
281 /// one.
282 pub fn put(&mut self, name: impl Into<String>, value: impl Into<String>) {
283 let name = name.into();
284 match self.defs.iter_mut().find(|(n, _)| *n == name) {
285 Some(slot) => slot.1 = value.into(),
286 None => self.defs.push((name, value.into())),
287 }
288 }
289
290 /// The definitions in order.
291 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
292 self.defs.iter().map(|(n, v)| (n.as_str(), v.as_str()))
293 }
294
295 /// How many names are defined.
296 #[must_use]
297 pub fn len(&self) -> usize {
298 self.defs.len()
299 }
300
301 /// Whether nothing is defined.
302 #[must_use]
303 pub fn is_empty(&self) -> bool {
304 self.defs.is_empty()
305 }
306}
307
308impl FromIterator<(String, String)> for MacroDefs {
309 /// Definition order, last definition of a name winning — C's, since
310 /// every one of these is a `macPutValue`.
311 fn from_iter<I: IntoIterator<Item = (String, String)>>(iter: I) -> Self {
312 let mut defs = Self::new();
313 for (name, value) in iter {
314 defs.put(name, value);
315 }
316 defs
317 }
318}
319
320impl From<Vec<(String, String)>> for MacroDefs {
321 fn from(pairs: Vec<(String, String)>) -> Self {
322 pairs.into_iter().collect()
323 }
324}
325
326impl From<&MacroDefs> for MacroDefs {
327 fn from(defs: &MacroDefs) -> Self {
328 defs.clone()
329 }
330}
331
332impl From<&HashMap<String, String>> for MacroDefs {
333 /// The lossy direction, and the only one: a hash map has no
334 /// definition order to carry, so the names are sorted to make the
335 /// notice order at least the same on every run. A caller that knows
336 /// the order the operator wrote should build a [`MacroDefs`]
337 /// directly.
338 fn from(macros: &HashMap<String, String>) -> Self {
339 let mut names: Vec<&String> = macros.keys().collect();
340 names.sort_unstable();
341 names
342 .into_iter()
343 .map(|name| (name.clone(), macros[name].clone()))
344 .collect()
345 }
346}
347
348/// C `MAC_HANDLE` (`macLib.h:47-57`): the macro table, the scope depth,
349/// and the one bit that says whether the cached values can be trusted.
350///
351/// This is the unit C creates once per FILE and expands once per line
352/// (`dbReadCOM` holds `macHandle` across the whole `.db`,
353/// `dbLexRoutines.c:256-300`), which is what makes a macro's own fault a
354/// once-per-file notice rather than a once-per-line one. Callers that
355/// have a single string and no file keep using [`expand_macros`], which
356/// is this type for the length of one call.
357///
358/// Entries are in the order they were defined in, which is C's
359/// `macPutValue` call order and the order the expansion pass walks them
360/// in (C `expand`, `macCore.c:655`) — so it is the order the notices a
361/// faulty definition raises come out in.
362/// That order is a property of [`MacroDefs`], not of this type: a table
363/// built from one is in the caller's order by construction, and a table
364/// built from a [`HashMap`] is in the only order a hash map can offer.
365pub struct MacroTable {
366 /// C `handle->list`. Ordered and searched from the tail.
367 ///
368 /// While an expansion is running it is only appended to or truncated
369 /// at the tail — see [`MacroTable::pop_scope`] — which is what lets
370 /// `refer` hold an index across a nested translation.
371 /// [`MacroTable::undefine`] is the one mutation that removes from the
372 /// middle, and it is reachable only from the file reader, between
373 /// lines, where no such index exists.
374 entries: Vec<MacEntry>,
375 /// C `handle->level`: how many scopes are open.
376 level: usize,
377 /// C `handle->dirty`: some raw value has changed since the last
378 /// [`expand_table`], so no cached value may be used. Raised by every
379 /// definition and every scope pop, lowered only by a completed
380 /// expansion pass.
381 dirty: bool,
382 /// C's handle flags, plus this port's `$$` convenience.
383 opts: MacroExpandOptions,
384}
385
386impl MacroTable {
387 /// C `macCreateHandle` + one `macPutValue` per pair
388 /// (`macCore.c:64-118`). The table starts dirty: nothing is expanded
389 /// until something asks for an expansion.
390 ///
391 /// The definitions arrive in [`MacroDefs`] order and are installed in
392 /// it, so the notice order of the first expansion pass is the
393 /// caller's own definition order and does not have to be arranged
394 /// for afterwards.
395 #[must_use]
396 pub fn new(defs: impl Into<MacroDefs>, opts: MacroExpandOptions) -> Self {
397 let entries = defs
398 .into()
399 .defs
400 .into_iter()
401 .map(|(name, rawval)| MacEntry {
402 name,
403 kind: KIND_MACRO,
404 rawval,
405 value: None,
406 faults: MacroFaults::default(),
407 visited: false,
408 special: false,
409 level: 0,
410 })
411 .collect();
412 Self {
413 entries,
414 level: 0,
415 dirty: true,
416 opts,
417 }
418 }
419
420 /// C `macExpandString` (`macCore.c:175-227`): bring the cached
421 /// values up to date, then translate `src` under a stack entry typed
422 /// `"string"` whose name is `src` itself.
423 ///
424 /// Both halves matter to what comes out. The table pass is what
425 /// decides the text of a reference into a cycle and the seat of
426 /// every notice a macro's own value raises; the string pass is the
427 /// only place the caller's own text is ever looked at.
428 #[must_use]
429 pub fn expand(&mut self, src: &str) -> MacroExpansion {
430 let suppressed = self.opts.suppress_warnings;
431 let mut ctx = ExpandCtx {
432 table: self,
433 seat: Seat {
434 kind: KIND_STRING,
435 name: src.to_string(),
436 faults: MacroFaults::default(),
437 },
438 suppressed,
439 };
440 expand_table(&mut ctx);
441 let chars: Vec<char> = src.chars().collect();
442 let mut out = String::with_capacity(src.len());
443 trans(&chars, 0, &mut ctx, &mut out);
444 MacroExpansion {
445 text: out,
446 faults: ctx.seat.faults,
447 }
448 }
449
450 /// C `macPutValue( handle, name, value )` (`macCore.c:262-289`) with
451 /// a non-NULL value: install `rawval` for `name` at the current scope
452 /// level.
453 ///
454 /// The value is stored RAW. C never expands a definition as it is
455 /// installed — `macInstallMacros` hands `macPutValue` exactly the
456 /// bytes `macParseDefns` cut out (`macUtil.c:250-275`) — so a
457 /// definition that mentions another macro stays live and follows
458 /// whatever that macro is when it is finally read.
459 pub fn define(&mut self, name: &str, rawval: String) {
460 self.put(name, KIND_MACRO, rawval);
461 }
462
463 /// C `macPutValue( handle, name, NULL )` (`macCore.c:274-280`): the
464 /// name is deleted rather than defined, which is what a definition
465 /// with no `=` in it means (`macParseDefns`'s `del[i]`,
466 /// `macUtil.c:105-110`).
467 ///
468 /// Deleting is not the same as defining nothing: an OUTER definition
469 /// of the same name is uncovered by it, and a reference that finds
470 /// nothing at all is undefined rather than empty.
471 pub fn undefine(&mut self, name: &str) {
472 let Some(idx) = self.lookup(name) else {
473 return;
474 };
475 self.entries.remove(idx);
476 self.dirty = true;
477 }
478
479 /// C `lookup( handle, name, FALSE )` (`macCore.c:571-585`): search
480 /// backwards "so scoping works" — the newest definition of a name
481 /// wins — and never match a scope marker.
482 fn lookup(&self, name: &str) -> Option<usize> {
483 self.entries
484 .iter()
485 .rposition(|e| !e.special && e.name == name)
486 }
487
488 /// [`Self::lookup`] with the rest of C's `lookup`: on a miss under
489 /// `FLAG_USE_ENVIRONMENT`, the environment is read and the value is
490 /// INSTALLED as an entry typed `"environment variable"`
491 /// (`macCore.c:586-598`).
492 ///
493 /// Installing it is not an optimisation, it is why an environment
494 /// hit dirties the table: the entry arrives with no cached value, so
495 /// every reference after it in the same string resolves from raw
496 /// values until the next expansion pass.
497 fn lookup_or_env(&mut self, name: &str) -> Option<usize> {
498 if let Some(i) = self.lookup(name) {
499 return Some(i);
500 }
501 if !self.opts.env_fallback || name.is_empty() {
502 return None;
503 }
504 let value = crate::runtime::env::get(name)?;
505 Some(self.put(name, KIND_ENVIRONMENT, value))
506 }
507
508 /// C `macPutValue` (`macCore.c:262-289`) and the `rawval` it ends in
509 /// (`:610-619`).
510 ///
511 /// A definition at or below the current scope level overwrites in
512 /// place; one that came from an OUTER scope is shadowed by a new
513 /// entry instead, so popping the scope brings the outer value back.
514 /// Either way the whole table goes dirty, because any other entry
515 /// may reference this one and no cached value can be trusted until
516 /// they are all rebuilt.
517 fn put(&mut self, name: &str, kind: &'static str, rawval: String) -> usize {
518 let idx = match self.lookup(name) {
519 Some(i) if self.entries[i].level >= self.level => i,
520 _ => {
521 self.entries.push(MacEntry {
522 name: name.to_string(),
523 kind,
524 rawval: String::new(),
525 value: None,
526 faults: MacroFaults::default(),
527 visited: false,
528 special: false,
529 level: self.level,
530 });
531 self.entries.len() - 1
532 }
533 };
534 let entry = &mut self.entries[idx];
535 entry.kind = kind;
536 entry.rawval = rawval;
537 entry.value = None;
538 entry.faults = MacroFaults::default();
539 self.dirty = true;
540 idx
541 }
542
543 /// C `macPushScope` (`macCore.c:400-424`): a marker entry at the
544 /// tail, which everything defined from here on sits after.
545 fn push_scope(&mut self) {
546 self.level += 1;
547 self.entries.push(MacEntry {
548 name: String::from("<scope>"),
549 kind: KIND_SCOPE_MARKER,
550 rawval: String::new(),
551 value: None,
552 faults: MacroFaults::default(),
553 visited: false,
554 special: true,
555 level: self.level,
556 });
557 }
558
559 /// C `macPopScope` (`macCore.c:434-475`): delete the most recent
560 /// `<scope>` marker and every entry defined since it.
561 ///
562 /// Those entries are exactly the tail — nothing is ever inserted
563 /// before an existing entry — so the deletion is a truncation, and
564 /// an index held by an enclosing [`refer`] frame into anything below
565 /// the marker survives it. C's `delete` dirties the table for the
566 /// same reason a redefinition does: a surviving entry may have
567 /// referenced what just went away.
568 fn pop_scope(&mut self) {
569 let at = self
570 .entries
571 .iter()
572 .rposition(|e| e.special)
573 .expect("refer pushes the scope marker before it can pop one");
574 self.entries.truncate(at);
575 self.level -= 1;
576 self.dirty = true;
577 }
578}
579
580/// The `MAC_ENTRY` a translation runs under: the two words every
581/// `macLib:` notice prints, and the `error` field the faults land in.
582///
583/// C swaps it four ways and the swaps are the whole of why one notice
584/// says `string` and the next says `macro`. `macExpandString` seats a
585/// stack entry typed `"string"` whose name is the caller's whole source
586/// string (`macCore.c:206-209`); [`expand_table`] seats the table entry
587/// being expanded (`:668`); and `refer` seats a throwaway `dflt` around
588/// the default's discarding pass (`:805-816`) and a throwaway `subs`
589/// around the scoped definitions (`:820-826`), neither of which merges
590/// its error back. Everything else — a resolved value translated raw, a
591/// used default — keeps the caller's seat, which is why a fault found
592/// three macros deep still names the entry the chain started from.
593struct Seat {
594 kind: &'static str,
595 name: String,
596 faults: MacroFaults,
597}
598
599/// Engine state threaded through [`trans`] / [`refer`] / [`parse_scoped`]:
600/// the table being resolved against, the [`Seat`] the current
601/// translation runs under, and the suppression bit the guards below
602/// raise and lower.
603///
604/// The scope stack and the recursion stack that used to be here are both
605/// gone into the table, where C keeps them: a scoped definition is an
606/// entry between a `<scope>` marker and the tail, and "currently being
607/// expanded" is [`MacEntry::visited`].
608struct ExpandCtx<'a> {
609 table: &'a mut MacroTable,
610 seat: Seat,
611 /// C `handle->flags & FLAG_SUPPRESS_WARNINGS`, which `refer` raises
612 /// and lowers around regions rather than setting once
613 /// (`macCore.c:795-800`, `:805-816`, `:822-859`). Seeded from
614 /// [`MacroExpandOptions::suppress_warnings`] and read wherever a
615 /// `macLib:` notice is about to be written, so a caller's knob and a
616 /// region's own quiet are the same bit.
617 suppressed: bool,
618}
619
620impl ExpandCtx<'_> {
621 /// C's `flags = handle->flags; handle->flags |=
622 /// FLAG_SUPPRESS_WARNINGS; …; handle->flags = flags` around the
623 /// translation of a reference NAME (`macCore.c:795-800`). The
624 /// notices go quiet, but the seat is unchanged, so a fault inside the
625 /// name still fails the surrounding expansion. Measured on `softIoc
626 /// R7.0.10`: `$($(NAMEREF))` writes ONE line and it names the
627 /// suppressed placeholder, `macro $(NAMEREF) is undefined`.
628 fn suppressing<R>(&mut self, body: impl FnOnce(&mut Self) -> R) -> R {
629 let saved = std::mem::replace(&mut self.suppressed, true);
630 let out = body(self);
631 self.suppressed = saved;
632 out
633 }
634
635 /// Run `body` under a different [`Seat`] and hand back the faults it
636 /// raised, for the caller to merge or to drop. Every seat change in
637 /// C is this shape, so this is the only way the seat moves.
638 fn seated<R>(&mut self, seat: Seat, body: impl FnOnce(&mut Self) -> R) -> (R, MacroFaults) {
639 let saved = std::mem::replace(&mut self.seat, seat);
640 let out = body(self);
641 let raised = std::mem::replace(&mut self.seat, saved).faults;
642 (out, raised)
643 }
644
645 /// C's `MAC_ENTRY subs` for the scoped-definition region
646 /// (`macCore.c:820-826`): a fresh seat whose `error` is never merged
647 /// back, under a raised suppression flag. So a fault inside
648 /// `,K=$(UNDEF)` neither warns nor fails the expansion around it —
649 /// measured on `softIoc R7.0.10`, `$(P,K=$(UNDEF))` with `P` defined
650 /// is silent and loads as `pval`.
651 ///
652 /// `name` is C's `subs.name`, which it re-seats between the two
653 /// halves of a definition: the reference's own name while the
654 /// definition's NAME is translated, the definition's name while its
655 /// VALUE is (`:840`, `:847`).
656 fn detached<R>(&mut self, name: String, body: impl FnOnce(&mut Self) -> R) -> R {
657 let seat = Seat {
658 kind: KIND_SCOPED_MACRO,
659 name,
660 faults: MacroFaults::default(),
661 };
662 let (out, _discarded) = self.suppressing(|ctx| ctx.seated(seat, body));
663 out
664 }
665}
666
667/// C `expand` (`macCore.c:645-679`): translate every raw value into its
668/// cached value, each under its OWN entry, then mark the table clean.
669///
670/// This is not a pass over "the macros the string mentions" — C expands
671/// the whole table on the first use after any change, and that is what
672/// makes a resolved reference a COPY (`refer`, `:882-886`) rather than a
673/// re-translation. Both halves of the difference are observable.
674/// Measured on `softIoc R7.0.10` with `A=$(B)`, `B=$(A)` delivered
675/// through `dbLoadTemplate` and a `.db` line reading `$(A)`: C writes
676/// `macro A is recursive (expanding macro B)` and then `macro B is
677/// recursive (expanding macro A)` — both seated on the table entry, not
678/// on the string — and loads `$(B,recursive)`, which is `A`'s cached
679/// value and not anything the string pass could have produced.
680///
681/// The table stays dirty for the duration, so a reference met while
682/// expanding takes `refer`'s raw branch and [`MacEntry::visited`] is the
683/// only thing between a cycle and an unbounded recursion — exactly as in
684/// C, where `handle->dirty` is cleared only after the loop.
685fn expand_table(ctx: &mut ExpandCtx) {
686 if !ctx.table.dirty {
687 return;
688 }
689 let mut i = 0;
690 // Not a `for` over a snapshot: resolving one entry can APPEND
691 // another (an environment value materialises as an entry), and C's
692 // list walk reaches those too.
693 while i < ctx.table.entries.len() {
694 // A `<scope>` marker has no raw value at all — `create` leaves it
695 // NULL (`macCore.c:556`) and C would fault here. It never does,
696 // because every scope `refer` opens is closed inside the same
697 // reference, and the public `macPushScope` callers expand between
698 // scopes rather than inside one.
699 if ctx.table.entries[i].special {
700 i += 1;
701 continue;
702 }
703 let raw: Vec<char> = ctx.table.entries[i].rawval.chars().collect();
704 let seat = Seat {
705 kind: ctx.table.entries[i].kind,
706 name: ctx.table.entries[i].name.clone(),
707 faults: MacroFaults::default(),
708 };
709 let mut value = String::new();
710 // C starts at level 1 "so quotes and escapes will be removed
711 // from expanded value" (`macCore.c:672-673`) — a cached value is
712 // never the user's own text.
713 let (_, raised) = ctx.seated(seat, |ctx| trans(&raw, 1, ctx, &mut value));
714 let entry = &mut ctx.table.entries[i];
715 entry.value = Some(value);
716 entry.faults = raised;
717 i += 1;
718 }
719 ctx.table.dirty = false;
720}
721
722/// Expand `$(...)` / `${...}` macro references, mirroring the C `macLib`
723/// engine (`modules/libcom/src/macLib/macCore.c` `expand` / `trans` /
724/// `refer`). This is the single macLib implementation for the crate; the
725/// `.db` parser, `dbLoadGroup`, and autosave all route through it (with
726/// per-caller [`MacroExpandOptions`]) rather than re-implementing it.
727///
728/// One call is one [`MacroTable`], which is C's handle for the length of
729/// one string. Callers that expand many strings against one set of
730/// macros — a file's worth of lines — should build the table once and
731/// call [`MacroTable::expand`] per line instead, because a macro whose
732/// own value is faulty raises its notice once per TABLE, not once per
733/// string.
734///
735/// Implemented behaviors:
736///
737/// - every raw value is expanded into a cached value before the
738/// caller's string is looked at, and a reference to a macro COPIES
739/// that cached value with its error merged rather than translating
740/// the raw value again (C `expand`, `macCore.c:645-679`; `refer`,
741/// `:882-886`). A definition, a scope pop or an environment hit
742/// invalidates the cache, and until the next pass references
743/// resolve from raw values.
744/// - `\<char>` blocks macro detection; both bytes reach the output in
745/// the caller's own string, and the backslash is dropped from
746/// anything that arrived through a macro (`trans:701-703,739-744`;
747/// `macLib.plt:52`).
748/// - macros are NOT expanded inside single quotes (`trans:733-736`);
749/// the quote characters themselves survive only at level 0.
750/// - a reference name is itself macro-expanded before lookup
751/// (`refer` runs `trans` on the name — `$($(WHICH))`).
752/// - the name terminates at `=`, `,` or the closing bracket
753/// (`macEnd = "=,)"`); `,name=val` introduces scoped macro
754/// definitions visible only inside that reference's expansion, and
755/// visible to the definitions AFTER them in the same list
756/// (`macPushScope` precedes the loop, `macCore.c:827-850`).
757/// - a self- or mutually-referential macro is refused at C's
758/// per-entry `visited` guard (`macCore.c:888-904`), leaving
759/// `$(name,recursive)` and [`MacroFault::Recursive`].
760/// - an undefined macro with no default emits the placeholder
761/// `$(name,undefined)` (`refer:errval = ",undefined)"`) and comes
762/// back as [`MacroFault::Undefined`].
763/// - a reference whose closing delimiter never matched its opener is
764/// copied through verbatim together with everything after it, and
765/// nothing in that tail is expanded a second time
766/// (`refer`, `macCore.c:862-875`) — [`MacroFault::Unterminated`].
767/// - with [`MacroExpandOptions::env_fallback`], an otherwise-unset
768/// name resolves from the process environment before the default
769/// (C `macCreateHandle(&h, environ)`).
770#[must_use]
771pub fn expand_macros(
772 input: &str,
773 macros: impl Into<MacroDefs>,
774 opts: MacroExpandOptions,
775) -> MacroExpansion {
776 MacroTable::new(macros, opts).expand(input)
777}
778
779/// Expand `$(...)` / `${...}` macro references with the default
780/// macLib options (no env fallback, no `$` escape, undefined →
781/// placeholder). Thin wrapper over [`expand_macros`], for the callers
782/// those defaults are right for: the `include` / `path` / `substitute`
783/// directives of the `.db` reader, and `.acf` text through
784/// [`substitute_macros_per_line`]. `dbLoadGroup` and autosave reuse the
785/// same engine but not this wrapper — both need options of their own,
786/// and both call [`expand_macros`] directly.
787pub fn substitute_macros(input: &str, macros: impl Into<MacroDefs>) -> String {
788 expand_macros(input, macros, MacroExpandOptions::default()).text
789}
790
791/// [`substitute_macros`], one line at a time against ONE table — how C's
792/// file readers feed macLib.
793///
794/// `dbLoadRecords` hands `macExpandString` a single `fgets` line
795/// (`dbLexRoutines.c:375-391`), and `asInitFile` does the same
796/// (`asLibRoutines.c:202-219`), so the expander's quote tracking (`trans`'s
797/// `quote`, C `macCore.c`) resets at every newline. Expanding a whole file
798/// in one [`substitute_macros`] call let one line's quote state leak into
799/// the next: an apostrophe in a `#` comment opened single-quote suppression
800/// and silently disabled every `$(...)` on the lines after it, until the
801/// parser failed on an unexpanded record name. Within a line the quote
802/// rules are unchanged — `'$(X)'` still suppresses.
803///
804/// The table is the file's, not the line's, because C's is: `asInitFile`
805/// creates one handle and reads every line through it, so a macro whose
806/// own value is faulty is expanded — and complained about — once.
807pub fn substitute_macros_per_line(input: &str, macros: impl Into<MacroDefs>) -> String {
808 let mut table = MacroTable::new(macros, MacroExpandOptions::default());
809 input
810 .split_inclusive('\n')
811 .map(|line| table.expand(line).text)
812 .collect()
813}
814
815/// Translate `chars` into `out`, expanding macro references.
816///
817/// `level` is C's: 0 is the string the caller handed
818/// [`MacroTable::expand`], and every recursion — a macro's value, a
819/// reference name, a default, a scoped definition, a cached value being
820/// built — runs one deeper, which is what decides whether quotes and
821/// backslashes are syntax or text. Scopes and the recursion guard live
822/// in [`ExpandCtx::table`].
823fn trans(chars: &[char], level: usize, ctx: &mut ExpandCtx, out: &mut String) {
824 // C `macCore.c:701-703`: "discard quotes and escapes if level is > 0
825 // (i.e. if these aren't the user's quotes and escapes)".
826 let discard = level > 0;
827 let mut quote: Option<char> = None;
828 let mut i = 0;
829 while i < chars.len() {
830 let c = chars[i];
831
832 // Track single/double quote state (C `trans` `quote` var).
833 if let Some(q) = quote {
834 if c == q {
835 quote = None;
836 if discard {
837 i += 1;
838 continue;
839 }
840 }
841 } else if c == '"' || c == '\'' {
842 quote = Some(c);
843 if discard {
844 i += 1;
845 continue;
846 }
847 }
848
849 // `$$` → literal `$` (opt-in; autosave `.req` convenience).
850 if ctx.table.opts.dollar_escape && c == '$' && i + 1 < chars.len() && chars[i + 1] == '$' {
851 out.push('$');
852 i += 2;
853 continue;
854 }
855
856 // `\<char>`: skip macro detection; the backslash itself is
857 // emitted only at level 0 (C `if (v < valend && !discard)`).
858 if c == '\\' && i + 1 < chars.len() {
859 if !discard {
860 out.push('\\');
861 }
862 out.push(chars[i + 1]);
863 i += 2;
864 continue;
865 }
866
867 // Macro reference: `$` followed by `(` or `{`, and NOT inside
868 // single quotes (C `macRef && quote != '\''`).
869 let mac_ref =
870 c == '$' && i + 1 < chars.len() && (chars[i + 1] == '(' || chars[i + 1] == '{');
871 if mac_ref && quote != Some('\'') {
872 i = refer(chars, i, level, ctx, out);
873 continue;
874 }
875
876 out.push(c);
877 i += 1;
878 }
879}
880
881/// Expand one macro reference starting at `chars[start]` (`$`). Returns
882/// the index just past the closing bracket, or — when the closing
883/// delimiter never matched the opener — the index past the whole
884/// remaining scan, which this then copies out verbatim (C
885/// `macCore.c:862-875`). `None` is never returned: every `$(`/`${` the
886/// caller hands over is consumed here, terminated or not.
887fn refer(
888 chars: &[char],
889 start: usize,
890 level: usize,
891 ctx: &mut ExpandCtx,
892 out: &mut String,
893) -> usize {
894 let close = if chars[start + 1] == '(' { ')' } else { '}' };
895 // Find the matching close bracket, honoring nested `$(`/`${`.
896 let body_start = start + 2;
897 let mut depth = 1usize;
898 let mut j = body_start;
899 while j < chars.len() && depth > 0 {
900 if j + 1 < chars.len() && chars[j] == '$' && (chars[j + 1] == '(' || chars[j + 1] == '{') {
901 depth += 1;
902 j += 2;
903 continue;
904 }
905 if depth == 1 && chars[j] == close || depth > 1 && (chars[j] == ')' || chars[j] == '}') {
906 depth -= 1;
907 if depth == 0 {
908 break;
909 }
910 }
911 j += 1;
912 }
913 if depth != 0 {
914 // C `refer`'s first error arm (`macCore.c:862-875`): the closing
915 // delimiter never matched the opener, so this is not a
916 // reference. C rewinds the output pointer to where the reference
917 // began (`v = *value`) and copies the raw text from the `$`
918 // through the last character `trans` scanned — the whole rest of
919 // the string, because the failed name translation ran to the end
920 // of it — then sets `entry->error` and says so.
921 //
922 // Copying the tail HERE, rather than handing the caller a `$` to
923 // re-scan, is what makes the text match: a re-scan re-enters
924 // `trans` just past the `$` and expands whatever `$(…)` follows,
925 // where C never looks at the tail again. Measured on `softIoc
926 // R7.0.10` with `Q=ZZZ`, C writes `x$(A $(Q) y` for the line
927 // `"x$(A $(Q) y"` — the inner `$(Q)` was consumed as part of the
928 // unterminated reference's NAME and is not expanded a second
929 // time. This port used to write `x$(A ZZZ y`.
930 //
931 // The arm is reached from a pre-scan for the matching bracket,
932 // before any scoped definition in the body has been parsed,
933 // where C discovers the mismatch only after parsing them and
934 // pushing a scope it then pops. Neither the text nor this notice
935 // can see the difference; the one thing that can is that C
936 // leaves the table dirty afterwards and this does not, so C may
937 // re-expand — and re-announce — a faulty macro value that this
938 // announces once.
939 let verbatim: String = chars[start..].iter().collect();
940 ctx.seat
941 .faults
942 .raise(TableFault::Unterminated(verbatim.clone()));
943 if !ctx.suppressed {
944 // C's third `macLib:` notice, alongside `undefined` and
945 // `recursive` below and painted by the same `ANSI_MAGENTA`
946 // (`errlog.h:301`) that `errlog` strips off a non-terminal
947 // console. It quotes `entry->type entry->name`, which is
948 // whatever seat this translation runs under: the caller's
949 // whole string when the `$(` is in the string, the macro's
950 // own name when it is in a macro's value.
951 let unterminated = if crate::runtime::log::errlog_console_paints() {
952 "\x1b[35;1munterminated\x1b[0m"
953 } else {
954 "unterminated"
955 };
956 crate::runtime::log::errlog_printf(&format!(
957 "macLib: {unterminated} macro reference in {} {}\n",
958 ctx.seat.kind, ctx.seat.name
959 ));
960 }
961 out.push_str(&verbatim);
962 return chars.len();
963 }
964 let body = &chars[body_start..j];
965 let after = j + 1;
966
967 // Split the body at the first top-level `=` or `,` (the C
968 // `macEnd` terminator set). Nested `$(...)` brackets are skipped
969 // so a `=`/`,` inside an inner reference does not terminate.
970 let split = top_level_terminator(body);
971 let (name_chars, rest) = match split {
972 Some(k) => (&body[..k], &body[k..]),
973 None => (body, &body[body.len()..]),
974 };
975
976 // The name itself may contain macro references — expand it, quietly.
977 // C raises `FLAG_SUPPRESS_WARNINGS` for exactly this translation and
978 // lowers it again (`macCore.c:795-800`), so the placeholder an
979 // unresolved inner reference leaves in the name is the SHORT `$(X)`
980 // form and no notice is written for it. The fault still lands,
981 // because C hands the name translation the caller's own seat.
982 let mut name = String::new();
983 ctx.suppressing(|ctx| trans(name_chars, level + 1, ctx, &mut name));
984
985 // Default value (`=...`) and scoped definitions (`,k=v`).
986 let mut default: Option<&[char]> = None;
987 let mut scoped: Option<&[char]> = None;
988 if let Some(first) = rest.first() {
989 if *first == '=' {
990 // Default runs until the first top-level `,` or end.
991 let dflt = &rest[1..];
992 match top_level_comma(dflt) {
993 Some(k) => {
994 default = Some(&dflt[..k]);
995 scoped = Some(&dflt[k..]);
996 }
997 None => default = Some(dflt),
998 }
999 } else if *first == ',' {
1000 scoped = Some(rest);
1001 }
1002 }
1003
1004 // C pushes the scope only when a `,` list follows, and pops it at
1005 // the single exit (`macCore.c:822-830`, `:932-935`). The condition
1006 // is not a saving: a push and its pop each dirty the table, so an
1007 // unconditional pair would make every reference in a string throw
1008 // away the cached values the reference before it was resolved from.
1009 //
1010 // The frame goes on BEFORE the definitions are read, because C's
1011 // `macPushScope` does and each `macPutValue` lands in it as the loop
1012 // reaches it (`:850`). So definition N's value is translated with
1013 // definitions 1..N-1 visible, and only those: measured on `softIoc
1014 // R7.0.10` with an outer `A=outer`, `$(B,A=1,B=$(A))` is `1` while
1015 // the reverse `$(B,B=$(A),A=1)` is `outer`.
1016 let pop = scoped.is_some();
1017 if let Some(defs) = scoped {
1018 ctx.table.push_scope();
1019 parse_scoped(defs, level, ctx, &name);
1020 }
1021
1022 match ctx.table.lookup_or_env(&name) {
1023 Some(idx) => {
1024 if ctx.table.entries[idx].visited {
1025 // C `refer` finding `refentry->visited` already set
1026 // (`macCore.c:895-904`): the reference is refused, NOT
1027 // resolved. The port used to emit the value verbatim,
1028 // which broke the cycle but left the operator with a
1029 // silently half-expanded `.db`.
1030 //
1031 // The seat is the entry whose raw value was being
1032 // translated when the cycle closed, and `name` is the
1033 // reference that closed it — C's `entry` and
1034 // `refentry`, in that order. Reached from
1035 // [`expand_table`] the seat is a table entry, so the
1036 // notice reads `macro A is recursive (expanding macro
1037 // B)`, which is what `softIoc R7.0.10` writes.
1038 let refkind = ctx.table.entries[idx].kind;
1039 ctx.seat.faults.raise(TableFault::Recursive(name.clone()));
1040 if !ctx.suppressed {
1041 let recursive = if crate::runtime::log::errlog_console_paints() {
1042 "\x1b[35;1mrecursive\x1b[0m"
1043 } else {
1044 "recursive"
1045 };
1046 crate::runtime::log::errlog_printf(&format!(
1047 "macLib: {} {} is {recursive} (expanding {refkind} {name})\n",
1048 ctx.seat.kind, ctx.seat.name
1049 ));
1050 }
1051 out.push('$');
1052 out.push('(');
1053 out.push_str(&name);
1054 // Same knob, same two texts as the undefined arm
1055 // (`macCore.c:920-928`).
1056 if ctx.suppressed {
1057 out.push(')');
1058 } else {
1059 out.push_str(",recursive)");
1060 }
1061 } else if ctx.table.dirty {
1062 // No cached value can be trusted, so translate the raw
1063 // one under the CALLER's seat — C passes `entry`, not
1064 // `refentry` (`macCore.c:890`) — with the entry's
1065 // `visited` guard raised around it.
1066 let raw: Vec<char> = ctx.table.entries[idx].rawval.chars().collect();
1067 ctx.table.entries[idx].visited = true;
1068 trans(&raw, level + 1, ctx, out);
1069 ctx.table.entries[idx].visited = false;
1070 } else {
1071 // C `cpy2val( refentry->value, … )` plus `entry->error =
1072 // entry->error || refentry->error` (`macCore.c:882-886`).
1073 // The value is COPIED, not re-scanned: whatever the
1074 // table pass made of it — including the placeholder a
1075 // cycle left in it — is what the string gets, and the
1076 // fault that produced it comes across without being
1077 // raised a second time.
1078 let entry = &ctx.table.entries[idx];
1079 let value = entry.value.clone().unwrap_or_default();
1080 let mut faults = entry.faults.clone();
1081 faults.rename_recursive(&name);
1082 out.push_str(&value);
1083 ctx.seat.faults.merge(&faults);
1084 }
1085 }
1086 None => match default {
1087 Some(def_chars) => {
1088 // C `refer` translates the default at `level + 1`
1089 // (`macCore.c:909`), so every quote in it is discarded —
1090 // not just a surrounding pair.
1091 trans(def_chars, level + 1, ctx, out);
1092 }
1093 None => {
1094 // C `refer` (`macCore.c:913-917`), through `errlogPrintf`
1095 // and not `fprintf` — which is why the magenta on
1096 // `undefined` follows the console while the
1097 // `ERROR`/`WARNING` words of the `.db` loader do not:
1098 // errlog strips escapes when its console is not a
1099 // terminal (`errlog.c:672-681`) and a direct `fprintf`
1100 // never enters that pump.
1101 ctx.seat.faults.raise(TableFault::Undefined(name.clone()));
1102 if !ctx.suppressed {
1103 let undefined = if crate::runtime::log::errlog_console_paints() {
1104 "\x1b[35;1mundefined\x1b[0m"
1105 } else {
1106 "undefined"
1107 };
1108 crate::runtime::log::errlog_printf(&format!(
1109 "macLib: macro {name} is {undefined} (expanding {} {})\n",
1110 ctx.seat.kind, ctx.seat.name
1111 ));
1112 }
1113 out.push('$');
1114 out.push('(');
1115 out.push_str(&name);
1116 // C writes the bare `)` under suppression and the
1117 // `,undefined)` tail otherwise (`macCore.c:920-928`), so
1118 // the knob changes the loader's own view of the value and
1119 // not just what the operator reads.
1120 if ctx.suppressed {
1121 out.push(')');
1122 } else {
1123 out.push_str(",undefined)");
1124 }
1125 }
1126 },
1127 }
1128
1129 if pop {
1130 ctx.table.pop_scope();
1131 }
1132 after
1133}
1134
1135/// Parse a `,key=val,key2=val2,...` scoped-definition tail into the
1136/// scope [`refer`] has already pushed. A bare `,key` with no `=`
1137/// defines nothing (C silently skips it).
1138///
1139/// Each definition lands in the table as the loop reaches it, so a later
1140/// one can reference an earlier one and not the other way round — C
1141/// `macPutValue` inside the `while (*r == ',')` loop (`macCore.c:850`).
1142/// Every definition also dirties the table, which is C's explicit
1143/// `handle->dirty = TRUE` on the next line and the reason the rest of
1144/// the enclosing string resolves from raw values.
1145///
1146/// Both halves of a definition are translated through
1147/// [`ExpandCtx::detached`], C's `MAC_ENTRY subs` (`macCore.c:820-826`):
1148/// a fault in a scoped name or value is neither warned about nor merged
1149/// into the enclosing expansion. `refname` is the seat name C uses for
1150/// the first half and the definition's own name the seat for the second.
1151fn parse_scoped(rest: &[char], level: usize, ctx: &mut ExpandCtx, refname: &str) {
1152 let mut k = 0;
1153 while k < rest.len() {
1154 if rest[k] != ',' {
1155 break;
1156 }
1157 k += 1; // step over ','
1158 // Scoped name: up to next top-level `=` or `,`.
1159 let seg = &rest[k..];
1160 let (name_part, tail) = match top_level_terminator(seg) {
1161 Some(t) => (&seg[..t], &seg[t..]),
1162 None => (seg, &seg[seg.len()..]),
1163 };
1164 let mut sname = String::new();
1165 ctx.detached(refname.to_string(), |ctx| {
1166 trans(name_part, level + 1, ctx, &mut sname);
1167 });
1168 k += name_part.len();
1169 if let Some('=') = tail.first() {
1170 let valseg = &tail[1..];
1171 let (val_part, _) = match top_level_comma(valseg) {
1172 Some(t) => (&valseg[..t], &valseg[t..]),
1173 None => (valseg, &valseg[valseg.len()..]),
1174 };
1175 let mut sval = String::new();
1176 ctx.detached(sname.clone(), |ctx| {
1177 trans(val_part, level + 1, ctx, &mut sval);
1178 });
1179 // C `macPutValue`, which types the new entry `"macro"` even
1180 // here (`macCore.c:283`) — `"scoped macro"` is the seat the
1181 // definition was translated under, not the entry's own type.
1182 ctx.table.put(&sname, KIND_MACRO, sval);
1183 k += 1 + val_part.len();
1184 }
1185 // else: bare `,name` — no value, defines nothing.
1186 }
1187}
1188
1189/// Index of the first top-level `=` or `,` in `body`, skipping any
1190/// `$(...)` / `${...}` nested reference.
1191fn top_level_terminator(body: &[char]) -> Option<usize> {
1192 let mut depth = 0usize;
1193 let mut i = 0;
1194 while i < body.len() {
1195 let c = body[i];
1196 if c == '$' && i + 1 < body.len() && (body[i + 1] == '(' || body[i + 1] == '{') {
1197 depth += 1;
1198 i += 2;
1199 continue;
1200 }
1201 if (c == ')' || c == '}') && depth > 0 {
1202 depth -= 1;
1203 } else if depth == 0 && (c == '=' || c == ',') {
1204 return Some(i);
1205 }
1206 i += 1;
1207 }
1208 None
1209}
1210
1211/// Index of the first top-level `,` in `body` (used to split a
1212/// default value from trailing scoped definitions).
1213fn top_level_comma(body: &[char]) -> Option<usize> {
1214 let mut depth = 0usize;
1215 let mut i = 0;
1216 while i < body.len() {
1217 let c = body[i];
1218 if c == '$' && i + 1 < body.len() && (body[i + 1] == '(' || body[i + 1] == '{') {
1219 depth += 1;
1220 i += 2;
1221 continue;
1222 }
1223 if (c == ')' || c == '}') && depth > 0 {
1224 depth -= 1;
1225 } else if depth == 0 && c == ',' {
1226 return Some(i);
1227 }
1228 i += 1;
1229 }
1230 None
1231}
1232
1233/// Split an IOC macro definition string into `(name, value)` pairs the
1234/// way libCom `macParseDefns` does (`macUtil.c:74-196`): commas separate
1235/// pairs and `=` separates a name from its value, but a separator inside
1236/// single/double quotes or escaped with a backslash is a literal, and
1237/// unquoted whitespace around names and values is trimmed. A name with
1238/// no `=` (e.g. `,FOO,`) is a deletion and yields `None`.
1239///
1240/// Quotes and escapes are stripped from both the name and the value. C
1241/// strips them from names in `macParseDefns` and from values later in
1242/// `macExpandString`; this port substitutes the value directly with no
1243/// second `macExpandString` pass, so both are stripped here to reach the
1244/// same observable substitution.
1245///
1246/// Exposed (re-exported as `iocsh::macro_defn_pairs`) so that other macLib
1247/// consumers — e.g. QSRV's `dbLoadGroup` macro parser — split definition
1248/// strings through this one owner of the `macParseDefns` grammar instead
1249/// of a second raw `split(',')` that would tear a quoted value on an
1250/// embedded comma. Callers that defer `$(...)` expansion to their own
1251/// `macExpandString` equivalent use these raw split pairs directly and do
1252/// NOT run `parse_macro_string`, which additionally substitutes the
1253/// environment eagerly.
1254pub fn macro_defn_pairs(s: &str) -> Vec<(String, Option<String>)> {
1255 #[derive(PartialEq, Clone, Copy)]
1256 enum St {
1257 PreName,
1258 InName,
1259 PreValue,
1260 InValue,
1261 }
1262 let mut out: Vec<(String, Option<String>)> = Vec::new();
1263 let mut state = St::PreName;
1264 let mut name = String::new();
1265 let mut value = String::new();
1266 // Unquoted whitespace seen mid-token: buffered so trailing whitespace
1267 // before a delimiter is dropped while interior whitespace is kept.
1268 let mut pending_ws = String::new();
1269 let mut quote: Option<char> = None;
1270
1271 // Enter the token from a "pre" state if needed and report whether it
1272 // is a VALUE: C removes quotes and escapes from names in place
1273 // "(unlike values, they will not be re-parsed)" (`macUtil.c:198-200`),
1274 // so a value keeps them for the expander's `discard` to strip.
1275 macro_rules! enter_token {
1276 () => {{
1277 match state {
1278 St::PreName => state = St::InName,
1279 St::PreValue => state = St::InValue,
1280 _ => {}
1281 }
1282 matches!(state, St::InValue)
1283 }};
1284 }
1285
1286 // Append a literal char to the token for the current state, entering
1287 // the token from a "pre" state if needed and flushing buffered ws.
1288 macro_rules! push_lit {
1289 ($c:expr) => {{
1290 match state {
1291 St::PreName => state = St::InName,
1292 St::PreValue => state = St::InValue,
1293 _ => {}
1294 }
1295 let target = if matches!(state, St::InName) {
1296 &mut name
1297 } else {
1298 &mut value
1299 };
1300 target.push_str(&pending_ws);
1301 pending_ws.clear();
1302 target.push($c);
1303 }};
1304 }
1305
1306 let chars: Vec<char> = s.chars().collect();
1307 let mut i = 0;
1308 while i < chars.len() {
1309 let c = chars[i];
1310
1311 // Escape: `\X` makes `X` a literal (and not a delimiter).
1312 // Quotes do not suppress escapes.
1313 if c == '\\' && i + 1 < chars.len() {
1314 if enter_token!() {
1315 push_lit!('\\');
1316 }
1317 push_lit!(chars[i + 1]);
1318 i += 2;
1319 continue;
1320 }
1321
1322 // Inside a quote: every char is literal until the matching quote.
1323 if let Some(q) = quote {
1324 if c == q {
1325 quote = None;
1326 if enter_token!() {
1327 push_lit!(c);
1328 }
1329 } else {
1330 push_lit!(c);
1331 }
1332 i += 1;
1333 continue;
1334 }
1335 if c == '\'' || c == '"' {
1336 quote = Some(c);
1337 // An opening quote also begins the token (e.g. `=""`).
1338 if enter_token!() {
1339 push_lit!(c);
1340 }
1341 i += 1;
1342 continue;
1343 }
1344
1345 match state {
1346 St::PreName => {
1347 if c == '=' {
1348 state = St::PreValue;
1349 } else if !(crate::runtime::stdlib::c_isspace(c) || c == ',') {
1350 state = St::InName;
1351 name.push(c);
1352 }
1353 // leading whitespace and bare commas: skip
1354 }
1355 St::InName => {
1356 if c == '=' {
1357 pending_ws.clear();
1358 state = St::PreValue;
1359 } else if c == ',' {
1360 // name with no '=' → deletion
1361 pending_ws.clear();
1362 out.push((std::mem::take(&mut name), None));
1363 state = St::PreName;
1364 } else if crate::runtime::stdlib::c_isspace(c) {
1365 pending_ws.push(c);
1366 } else {
1367 name.push_str(&pending_ws);
1368 pending_ws.clear();
1369 name.push(c);
1370 }
1371 }
1372 St::PreValue => {
1373 if c == ',' {
1374 out.push((std::mem::take(&mut name), Some(String::new())));
1375 state = St::PreName;
1376 } else if !crate::runtime::stdlib::c_isspace(c) {
1377 state = St::InValue;
1378 value.push(c);
1379 }
1380 // leading value whitespace: skip
1381 }
1382 St::InValue => {
1383 if c == ',' {
1384 pending_ws.clear();
1385 out.push((std::mem::take(&mut name), Some(std::mem::take(&mut value))));
1386 state = St::PreName;
1387 } else if crate::runtime::stdlib::c_isspace(c) {
1388 pending_ws.push(c);
1389 } else {
1390 value.push_str(&pending_ws);
1391 pending_ws.clear();
1392 value.push(c);
1393 }
1394 }
1395 }
1396 i += 1;
1397 }
1398
1399 // Flush the token open at end of string.
1400 match state {
1401 St::PreName => {}
1402 St::InName => out.push((std::mem::take(&mut name), None)),
1403 St::PreValue => out.push((std::mem::take(&mut name), Some(String::new()))),
1404 St::InValue => out.push((std::mem::take(&mut name), Some(std::mem::take(&mut value)))),
1405 }
1406 out
1407}
1408
1409#[cfg(test)]
1410mod tests {
1411 use super::*;
1412
1413 // `expand_macros` reports every undefined macro so a hard-fail
1414 // caller (autosave) can surface it; the text still carries the
1415 // C `$(name,undefined)` placeholder for the no-fail callers.
1416 #[test]
1417 fn expand_macros_reports_undefined_names() {
1418 let macros = HashMap::new();
1419 let r = expand_macros("$(A)$(B=def)$(C)", ¯os, MacroExpandOptions::default());
1420 assert_eq!(r.text, "$(A,undefined)def$(C,undefined)");
1421 // B had a default → not undefined; A and C are, in scan order.
1422 assert_eq!(r.faults.undefined, vec!["A".to_string(), "C".to_string()]);
1423 }
1424
1425 // The default options match `.db` parse semantics: no env fallback,
1426 // and `$$` is NOT an escape (macLib leaves it verbatim).
1427 #[test]
1428 fn expand_macros_default_opts_leave_dollar_dollar_verbatim() {
1429 let macros = HashMap::new();
1430 assert_eq!(substitute_macros("$$100", ¯os), "$$100");
1431 let r = expand_macros("$$100", ¯os, MacroExpandOptions::default());
1432 assert_eq!(r.text, "$$100");
1433 assert!(r.faults.undefined.is_empty());
1434 }
1435
1436 // `env_fallback` resolves an otherwise-unset name from the process
1437 // environment (C `macCreateHandle(&h, environ)`), at the same level
1438 // as a defined macro — before any default.
1439 #[test]
1440 fn expand_macros_env_fallback_opt_in() {
1441 let var = "_EPICS_LIBCOM_RS_MACRO_ENV_TEST";
1442 // Off by default: unset macro stays undefined even with the env set.
1443 unsafe { std::env::set_var(var, "FROM_ENV") };
1444 let macros = HashMap::new();
1445 let off = expand_macros(&format!("$({var})"), ¯os, MacroExpandOptions::default());
1446 assert_eq!(off.text, format!("$({var},undefined)"));
1447 // On: resolves from the environment.
1448 let on = expand_macros(
1449 &format!("$({var})"),
1450 ¯os,
1451 MacroExpandOptions {
1452 env_fallback: true,
1453 ..MacroExpandOptions::default()
1454 },
1455 );
1456 assert_eq!(on.text, "FROM_ENV");
1457 assert!(on.faults.undefined.is_empty());
1458 unsafe { std::env::remove_var(var) };
1459 }
1460}