Skip to main content

epics_base_rs/server/record/
record_trait.rs

1use crate::error::CaResult;
2use crate::types::c_parse::Converted;
3use crate::types::{DbFieldType, DbfCode, EpicsValue, PvString, c_parse};
4
5use super::alarm::AlarmLimit;
6use super::scan::ScanType;
7
8/// Which of a `devXxxSoftRaw` dset's two entry points is delivering a value to
9/// [`Record::raw_soft_input`]. They are not the same function in C, and they do
10/// not agree about `MASK`.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum RawSoftEntry {
13    /// `devXxxSoftRaw::init_record` — `recGblInitConstantLink(&prec->inp,
14    /// DBF_x, &prec->rval)` (`devAiSoftRaw.c:41`, `devBiSoftRaw.c:42`,
15    /// `devMbbiSoftRaw.c:42`, `devMbbiDirectSoftRaw.c:42`). A CONSTANT `INP`
16    /// (`field(INP,"12")`) is loaded ONCE, at iocInit, straight into `RVAL`.
17    ///
18    /// `recGblInitConstantLink` is a plain typed store — **no MASK**. The mask
19    /// lives in `read_xxx`, which a constant INP never reaches (a constant link
20    /// delivers nothing at process).
21    InitConstant,
22    /// `devXxxSoftRaw::read_xxx` — the per-cycle `dbGetLink(&prec->inp, ...)`
23    /// followed by the dset's own masking (`devBiSoftRaw.c:56-57` `if
24    /// (prec->mask) prec->rval &= prec->mask;`, `devMbbiSoftRaw.c:78-79`
25    /// unconditionally).
26    Read,
27}
28
29/// The `special(SPC_*)` dispatch code a field declares — C `special.h`.
30///
31/// C hands this to the record's `special(DBADDR *, int after)` on every put, and
32/// `dbAccess.c` acts on three of them itself before the record ever sees the
33/// write: `NoMod` refuses it (`S_db_noMod`), `DbAddr` means the field's type and
34/// element count come from the record's `cvt_dbaddr` rather than the `.dbd`, and
35/// `As` re-evaluates access security.
36///
37/// The discriminants ARE C's `SPC_*` numbers (`special.h:26-39` @R7.0.10),
38/// so `special as i16` is the integer C prints and stores. They are written
39/// out rather than left implicit because the table is not contiguous — C has
40/// no code 4, and the record-specific half starts at 100 — so the obvious
41/// cast on an implicitly-numbered enum would have answered 4 for
42/// [`Self::AlarmAck`] and 7 for [`Self::Mod`]. Carrying the number on the
43/// type is what makes every future cast right by construction.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[repr(i16)]
46pub enum Special {
47    /// No `special()` declared — C leaves `pdbFldDes->special` 0.
48    None = 0,
49    /// `SPC_NOMOD` — the field must not be modified. Mirrored into
50    /// [`FieldDesc::read_only`], which is the bit the put gate reads.
51    NoMod = 1,
52    /// `SPC_DBADDR` — the record's `cvt_dbaddr` supplies the field's type
53    /// and element count. [`FieldDesc::dbf_type`] carries the type C serves at
54    /// the selector field's default; a record whose type is state-dependent
55    /// (`waveform.VAL` on `FTVL`, `mbbo.VAL` on `SDEF`) overrides it at runtime.
56    DbAddr = 2,
57    /// `SPC_SCAN` — a scan-related field; C re-registers the scan.
58    Scan = 3,
59    /// `SPC_ALARMACK` — an alarm acknowledgement. C skips 4.
60    AlarmAck = 5,
61    /// `SPC_AS` — access security; C re-computes the record's ASG.
62    As = 6,
63    /// `SPC_ATTRIBUTE` — a pseudo (attribute) field. Set internally; it
64    /// is the one code `pamapspcType` omits, so no `.dbd` declares it.
65    Attribute = 7,
66    /// `SPC_MOD` — the record's own `special()` runs on the put. C's
67    /// record-specific range starts here.
68    Mod = 100,
69    /// `SPC_RESET` — the `RES` field is being modified.
70    Reset = 101,
71    /// `SPC_LINCONV` — a linear-conversion field changed; C calls the
72    /// device support's `special_linconv`.
73    LinConv = 102,
74    /// `SPC_CALC` — the `CALC` expression changed; C recompiles it.
75    Calc = 103,
76}
77
78/// A field's access-security level — C `.dbd` `asl(ASL0|ASL1)`.
79///
80/// `ASL1` is the `.dbd` default (`dbLexRoutines.c:570`); `asl(ASL0)` lowers a
81/// field to the level an operator may write.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum Asl {
84    Asl0,
85    Asl1,
86}
87
88/// The numeric base `dbGetString` renders an integer field in — C's
89/// `ctType` (`dbBase.h:63`), set by the `.dbd` `base(DECIMAL|HEX)` item
90/// (`dbLexRoutines.c:652-661`).
91///
92/// It selects the renderer inside `dbGetStringNum` (`dbStaticLib.c:2074-2124`),
93/// so it changes what `dbpr`, `dbDumpRecord` and the `.db` writer print and
94/// nothing else: `dbgf` reads through `dbConvert`, which has no base. In
95/// EPICS base the whole `HEX` population is `mbbi`/`mbbo`'s sixteen `*VL`
96/// fields, which C prints as `ZRVL: 0xa` where the port printed `ZRVL: 10`.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum Base {
99    /// `CT_DECIMAL` — the `.dbd` default.
100    Decimal,
101    /// `CT_HEX`.
102    Hex,
103}
104
105/// A record type's index for one of its fields — C's `dbFldDes::offset`
106/// as a handle rather than a byte count. Meaningful only to the type that
107/// handed it out ([`Record::field_slot`]).
108#[derive(Clone, Copy, PartialEq, Eq, Debug)]
109pub struct FieldSlot(pub u16);
110
111/// The `.dbd` declaration of a single record field.
112///
113/// Every one of these is **generated** from the vendored EPICS `.dbd` by
114/// `tools/dbd-codegen` — see [`dbd_generated`](super::dbd_generated). They used
115/// to be hand-copied, which is what made a wrong `dbf_type` or a missed
116/// `special(SPC_NOMOD)` a recurring finding rather than an impossible state.
117///
118/// The struct carries the *whole* declaration, not just the three attributes the
119/// runtime consumes today: dropping the rest at the parser is how the port ended
120/// up unable to answer questions like "is this field `pp(TRUE)`?" without
121/// re-reading the `.dbd`.
122#[derive(Debug, Clone)]
123pub struct FieldDesc {
124    /// The field name, upper-case as declared.
125    pub name: &'static str,
126    /// The type the field is **SERVED** as, on every delivery path — see
127    /// [`RecordInstance::project_to_declared_type`](super::RecordInstance::project_to_declared_type),
128    /// which projects the stored value onto it. The one exception is a
129    /// [`Self::runtime_typed`] field, where C's `cvt_dbaddr` overrides.
130    ///
131    /// It is NOT the `DBF_*` token the `.dbd` declared — that is
132    /// [`Self::declared_dbf`], and the two differ for every link and menu
133    /// field. The CA wire type is derived from this one by
134    /// [`DbFieldType::ca_wire_type`], which owns the promotions CA has no type
135    /// for (`ULong`/`Int64`/`UInt64` -> `DBR_DOUBLE`, `UShort` -> `DBR_LONG`,
136    /// `UChar` -> `DBR_CHAR`). Do not pre-promote here: PVA serves the native
137    /// width.
138    pub dbf_type: DbFieldType,
139    /// The `DBF_*` token the `.dbd` declared, carried verbatim from the
140    /// generator's input — C's `pdbFldDes->field_type`, which is what
141    /// `dbDumpField` prints and `dbGetFieldType`/`dbGetFieldTypeString`
142    /// (`dbStaticLib.c:964-977`) answer for a `DBENTRY`.
143    ///
144    /// C's `dba` prints `paddr->field_type` instead, which is this token for
145    /// every field EXCEPT the 83 `special(SPC_DBADDR)` rows, where
146    /// `dbNameToAddr` lets the record's `cvt_dbaddr` overwrite it; on those
147    /// rows [`Self::dbf_type`] carries the overwritten type. 80 of the 83
148    /// declare `DBF_NOACCESS` here, which is precisely why [`Self::no_access`]
149    /// reads this field and not `dbf_type`.
150    ///
151    /// A SECOND fact, not a spelling of [`Self::dbf_type`]. The generator maps
152    /// six C tokens onto two served types (`DBF_ENUM|DBF_MENU|DBF_DEVICE` all
153    /// serve as [`DbFieldType::Enum`], `DBF_INLINK|DBF_OUTLINK|DBF_FWDLINK` all
154    /// as [`DbFieldType::String`]), and that collapse is one-way: no mapping
155    /// out of `dbf_type` can recover the token, because `ai.INP` and
156    /// `dbCommon.FLNK` are the same served variant. Nor can the field NAME —
157    /// `LNK1` is `DBF_OUTLINK` on `sseq` and `DBF_FWDLINK` on `fanout`. So the
158    /// declaration is carried, not derived.
159    pub declared_dbf: DbfCode,
160    /// C's `cvt_dbaddr` re-types this field at name-resolution time from the
161    /// record's own state — `waveform.VAL` from `FTVL`, `aSub.A` from `FTA`,
162    /// `mbbo.VAL` from `SDEF` — so the `.dbd` declaration is a placeholder
163    /// carrying only the selector's default. [`Self::dbf_type`] is therefore
164    /// NOT what such a field is served as; the record's stored variant is this
165    /// port's `cvt_dbaddr` answer, and it wins.
166    ///
167    /// A `special(SPC_DBADDR)` field whose type is nevertheless FIXED
168    /// (`compress.VAL` is always a double array, `histogram.VAL` always
169    /// `epicsUInt32`) is *not* runtime-typed: its row in `cvt_dbaddr.types`
170    /// carries no selector, so the declared type is the true one and it is
171    /// projected like any other field.
172    pub runtime_typed: bool,
173    /// `special(SPC_NOMOD)` — the field is immutable for this record type. The
174    /// static half of the no-modify declaration; see [`Record::field_no_mod`]
175    /// for the half a record decides at runtime.
176    pub read_only: bool,
177    /// The full `special()` code AS RESOLVED, of which [`Self::read_only`] is
178    /// one case: the `.dbd` declaration for most fields, but for a
179    /// `special(SPC_DBADDR)` field the code its `cvt_dbaddr` raises instead —
180    /// `SPC_MOD` on `lsi.VAL`, `SPC_NOMOD` on `lsi.OVAL`
181    /// (`lsiRecord.c:127-134`). That is the code the put gate and the CA
182    /// write-access answer must read, because it is what C's `DBADDR` carries
183    /// after name resolution.
184    ///
185    /// It is NOT what the `.dbd` said — see [`Self::declared_special`], which
186    /// is. The pair splits the same way [`Self::dbf_type`] and
187    /// [`Self::declared_dbf`] do, and for the same reason: one cell holding
188    /// both answers is a cell that is wrong for one of its two readers.
189    pub special: Special,
190    /// The `special()` code the `.dbd` DECLARED — C's `pdbFldDes->special`,
191    /// which `cvt_dbaddr` never touches because it writes the per-resolution
192    /// `DBADDR` and not the shared field descriptor.
193    ///
194    /// Every reader that asks about the DECLARATION rather than about a
195    /// resolved address wants this one. The db loader's field-name suggestion
196    /// is the case in hand: it skips `SPC_NOMOD` and `SPC_DBADDR` candidates
197    /// (`dbLexRoutines.c:1283-1285`), and reading [`Self::special`] there would
198    /// let `lsi.VAL` and `lso.VAL` — declared `SPC_DBADDR`, resolved `SPC_MOD`
199    /// — be proposed where C does not propose them.
200    pub declared_special: Special,
201    /// `pp(TRUE)` — a put to this field processes the record.
202    pub pp: bool,
203    /// `asl(...)` — the access-security level.
204    pub asl: Asl,
205    /// The byte width C's `pdbFldDes->size` carries.
206    ///
207    /// Two sources, one meaning. A `DBF_STRING` field takes it from `size(N)`,
208    /// which C's loader REQUIRES on every such row
209    /// (`dbLexRoutines.c:755-758`). A `DBF_NOACCESS` internal takes it from
210    /// the width of the C struct member [`Self::extra`] declares, which is
211    /// what a C IOC's generated `<rec>RecordSizeOffset` writes into the same
212    /// cell — the `.dbd` never spells it.
213    ///
214    /// Not string-only, and not decoration on the second kind: `dbpr`'s
215    /// `DBF_NOACCESS` arm dispatches on it (`dbTest.c:1235`, `:1241`) and then
216    /// prints exactly this many bytes as hex (`:1249-1262`), so a width of 0
217    /// prints an empty row where C prints the field.
218    ///
219    /// 0 for every other declaration, where C carries `sizeof` the member and
220    /// no reader in this port asks.
221    pub size: u16,
222    /// `extra("...")` — the C struct member a `DBF_NOACCESS` field stands for,
223    /// verbatim, and `None` for a field declaring none.
224    ///
225    /// C's loader requires it on exactly the rows [`Self::size`]'s second
226    /// source covers (`dbLexRoutines.c:759-762`), and it is not documentation:
227    /// `dbpr` reads the declaration TEXT to choose a renderer — a `*` in it
228    /// means print a pointer, a leading `ELLLIST` means print a list header
229    /// (`dbTest.c:1235-1247`). The type name is the only thing that says how
230    /// wide the member is, since no `.dbd` states it.
231    pub extra: Option<&'static str>,
232    /// `menu(...)` choice strings, in index order, for a `DBF_MENU` field. The
233    /// index is the stored value and the strings are what `get_enum_strs` serves,
234    /// so a client sees `"NO CONVERSION"` rather than `0`.
235    pub menu: Option<&'static [&'static str]>,
236    /// `initial("...")` — the value C's dbd loader seeds the field with.
237    pub initial: Option<&'static str>,
238    /// `interest(N)` — the `dbpr` verbosity level at which C prints the field.
239    pub interest: u8,
240    /// `prop(YES)` — the field is a property: a change to it posts a
241    /// `DBE_PROPERTY` event.
242    pub prop: bool,
243    /// `prompt("...")` — the operator-facing label. C prints it in the
244    /// parenthesised half of the db loader's field suggestion
245    /// (`dbLexRoutines.c:1380-1384`), which is the only reason it is carried;
246    /// a field that declares none makes C print the bare `Did you mean` line.
247    pub prompt: Option<&'static str>,
248    /// `promptgroup("...")` — the DCT group the field belongs to, or `None`
249    /// when the `.dbd` declares none. C stores it as a 1-based key into
250    /// `guiGroupList` (`findOrAddGuiGroup`, `dbLexRoutines.c:1176-1189`), so
251    /// zero means absent and this is `Option`, not the group's index. The
252    /// suggestion halves a groupless field's score (`:1288-1289`): a field no
253    /// DCT screen offers is an unlikely thing to have misspelled.
254    pub promptgroup: Option<&'static str>,
255    /// `base(DECIMAL|HEX)` — the base `db_get_string` renders this field in.
256    pub base: Base,
257}
258
259impl FieldDesc {
260    /// A hand-written descriptor carrying only the three attributes the port
261    /// used to model.
262    ///
263    /// **Transitional.** Every record type is migrating to the generated table
264    /// in [`dbd_generated`](super::dbd_generated), which carries the whole `.dbd`
265    /// declaration; this constructor exists only so the not-yet-migrated records
266    /// keep compiling, and it goes away with the last of them. It does NOT know
267    /// the field's `pp`/`asl`/`size`/`menu`/`initial`, so it reports the neutral
268    /// value for each — a record still on this constructor answers "no menu"
269    /// here and resolves its choices through the
270    /// [`Record::menu_field_choices`] fallback instead.
271    pub const fn new(name: &'static str, dbf_type: DbFieldType, read_only: bool) -> Self {
272        Self {
273            name,
274            dbf_type,
275            // A hand-written table names one type and means both facts by it:
276            // it has no `.dbd` behind it to disagree with, so the declaration
277            // is the served type's own code. That also makes `no_access()`
278            // false for every such row, which is right — a hand-written table
279            // never declares a C internal.
280            declared_dbf: dbf_type.dbf_code(),
281            // A plain field: the type it names is the type it is served as.
282            // The `cvt_dbaddr` records are all on the generated table, which
283            // sets this from `cvt_dbaddr.types`.
284            runtime_typed: false,
285            read_only,
286            special: if read_only {
287                Special::NoMod
288            } else {
289                Special::None
290            },
291            // A hand-written row has no `.dbd` behind it to disagree with, so
292            // the declaration is the resolved code.
293            declared_special: if read_only {
294                Special::NoMod
295            } else {
296                Special::None
297            },
298            pp: false,
299            asl: Asl::Asl1,
300            size: 0,
301            extra: None,
302            menu: None,
303            initial: None,
304            interest: 0,
305            prop: false,
306            prompt: None,
307            promptgroup: None,
308            base: Base::Decimal,
309        }
310    }
311
312    /// The field is declared `DBF_NOACCESS` — a C internal with no dbStatic
313    /// representation. C's `dbPutString` switches on exactly this and answers
314    /// `S_dbLib_badField` with "Can't set array field before iocInit()"
315    /// (`dbStaticLib.c:2646-2650`), so a `.db` file cannot assign one.
316    ///
317    /// Derived rather than stored, so the two cannot disagree. It is NOT
318    /// implied by [`Self::special`] — `mbbo.VAL` is `DBF_ENUM` + `SPC_DBADDR`
319    /// and is perfectly settable, while `waveform.VAL` is `DBF_NOACCESS` +
320    /// `SPC_DBADDR` and is not — nor by [`Self::dbf_type`], which for these
321    /// rows carries the type C SERVES rather than the one declared.
322    pub const fn no_access(&self) -> bool {
323        matches!(self.declared_dbf, DbfCode::NoAccess)
324    }
325
326    /// The field resolves as a NAME but has no readable value — C `dbGet`'s
327    /// validity gate, `field_type > DBF_DEVICE` (`dbAccess.c:667-675`
328    /// @R7.0.10).
329    ///
330    /// Not the same question as [`Self::no_access`], and the difference is
331    /// the whole point. `dbEntryToAddr` seeds `paddr->field_type` from the
332    /// DECLARATION and then overwrites it from the record's `cvt_dbaddr` —
333    /// but only for a field whose `special` is `SPC_DBADDR`
334    /// (`dbAccess.c:640-647`). So a `DBF_NOACCESS` row carrying
335    /// `SPC_DBADDR` — `waveform.VAL` — arrives at the gate re-typed to what
336    /// it serves and PASSES, while one that does not — `dbCommon`'s `BKPT`
337    /// and `TIME` — arrives still holding 17 and FAILS. Both facts are
338    /// needed; neither alone answers it.
339    ///
340    /// Derived rather than stored, for the same reason `no_access` is: a
341    /// stored copy is a second answer that can drift from the declaration.
342    pub const fn unreadable(&self) -> bool {
343        self.no_access() && !matches!(self.declared_special, Special::DbAddr)
344    }
345}
346
347/// One `recGblInitConstantLink(&prec->LINK, DBF_x, &prec->TARGET)` call from a
348/// record's C `init_record` — the seed of a CONSTANT input link.
349///
350/// Declared by [`Record::constant_init_links`] and applied by the single owner
351/// `crate::server::database::PvDatabase::rec_gbl_init_constant_links`.
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub struct ConstantInitLink {
354    /// The link field holding the constant (`INPA`, `NVL`, `SELL`, `DOL1`,
355    /// `SUBL`, `DOL`, ...).
356    pub link_field: &'static str,
357    /// The value field the constant is loaded into (`A`, `SELN`, `DO1`,
358    /// `SNAM`, `VAL`, ...).
359    pub target_field: &'static str,
360    /// Whether a successful seed clears UDF — C's
361    /// `if (recGblInitConstantLink(&prec->dol, ...)) prec->udf = FALSE;`
362    /// (`aoRecord.c:112-113`, `longoutRecord.c:113`, `mbboRecord.c:133`,
363    /// `int64outRecord.c:110`, `dfanoutRecord.c:105`). The multi-input seeders
364    /// (calc/sub/sel/aSub/seq/fanout) do NOT clear UDF: they seed A..L, not
365    /// VAL.
366    pub clears_udf: bool,
367    /// Whether the loaded value is stored as its BOOLEAN — C `boRecord.c:146-148`
368    /// loads the constant into a temporary and stores `prec->val = !!ival`, so
369    /// `field(DOL,"5")` leaves a bo at VAL=1, not 5. The only seed whose stored
370    /// value differs from the loaded one.
371    pub normalize_bool: bool,
372}
373
374impl ConstantInitLink {
375    /// A seed that does not touch UDF — the INPA..L / SELL / NVL / DOLn form.
376    pub const fn new(link_field: &'static str, target_field: &'static str) -> Self {
377        Self {
378            link_field,
379            target_field,
380            clears_udf: false,
381            normalize_bool: false,
382        }
383    }
384
385    /// A DOL→VAL seed, which C follows with `prec->udf = FALSE`.
386    pub const fn dol_to_val(link_field: &'static str, target_field: &'static str) -> Self {
387        Self {
388            link_field,
389            target_field,
390            clears_udf: true,
391            normalize_bool: false,
392        }
393    }
394
395    /// bo's DOL→VAL seed: `prec->val = !!ival; prec->udf = FALSE;`
396    /// (`boRecord.c:146-149`).
397    pub const fn dol_to_bool_val(link_field: &'static str, target_field: &'static str) -> Self {
398        Self {
399            link_field,
400            target_field,
401            clears_udf: true,
402            normalize_bool: true,
403        }
404    }
405}
406
407/// The seed table for a record whose C seeds exactly the input links it
408/// fetches — the `for (i = 0; i < N; i++) recGblInitConstantLink(plink++,
409/// DBF_DOUBLE, pvalue++)` loop of calc / calcout / sub / sel / aSub /
410/// scalcout / acalcout / transform, expressed over the record's own
411/// [`Record::multi_input_links`] table.
412pub fn seed_input_links(pairs: &[(&'static str, &'static str)]) -> Vec<ConstantInitLink> {
413    pairs
414        .iter()
415        .map(|(link, value)| ConstantInitLink::new(link, value))
416        .collect()
417}
418
419/// Resolved metadata of an OUT-link TARGET, as C's soft device support
420/// obtains it before choosing its write buffer.
421///
422/// C's two sources, both mirrored by
423/// [`PvDatabase::resolve_out_target`](crate::server::database::PvDatabase):
424/// - `DB_LINK` — `dbNameToAddr` gives `field_type` and `no_elements`
425///   (`devsCalcoutSoft.c:127-131`, `devaCalcoutSoft.c:78-79`); an
426///   unresolvable name leaves the caller's initializers untouched.
427/// - `CA_LINK` — `dbCaGetLinkDBFtype` / `dbCaGetNelements`
428///   (`dbCa.c:662-704`), which both return `-1` on a disconnected link and
429///   likewise leave the initializers untouched.
430///
431/// A record reproduces its C device support's buffer switch on this in
432/// [`Record::multi_output_buffer`].
433#[derive(Debug, Clone, Copy, PartialEq, Eq)]
434pub struct OutTarget {
435    /// Target field's DBF type. `None` = unresolved (disconnected CA link,
436    /// or a name this IOC cannot resolve) — C's `field_type` initializer.
437    pub field_type: Option<DbFieldType>,
438    /// Target field's element capacity — C `no_elements` / `dbCaGetNelements`.
439    /// `1` when unresolved, matching C's `n_elements = 1` initializer.
440    pub element_count: i64,
441    /// True when C would classify this link as a `CA_LINK`: an explicit
442    /// `ca://`/`pva://` link, or a DB-style name that is not a record of
443    /// this IOC (`dbInitLink` locality). Device support that splits its
444    /// buffer choice on sync-vs-async (`devsCalcoutSoft.c:76`, gated on
445    /// `plink->type == CA_LINK && pscalcout->wait`) reads this.
446    pub is_ca_link: bool,
447    /// True when the target field is one of the seven DBF classes C's soft
448    /// device support puts as `DBR_STRING` — `DBF_STRING`, `DBF_ENUM`,
449    /// `DBF_MENU`, `DBF_DEVICE`, `DBF_INLINK`, `DBF_OUTLINK`, `DBF_FWDLINK`
450    /// (`devsCalcoutSoft.c:83-85`, `:128-130`).
451    ///
452    /// Carried here rather than re-derived from [`Self::field_type`] because
453    /// [`DbFieldType`] is the DBR *wire* type: it has no `Menu` or `Device`
454    /// variant, so a menu target (`PRIO`, `STAT`, `SEVR`, `DISS`, `ACKT`, …)
455    /// or `DTYP` is indistinguishable from a plain numeric/string field by
456    /// type alone. The classification is made once, at resolution, by the
457    /// side that holds the target's field metadata
458    /// (`RecordInstance::field_puts_as_string`); a record's
459    /// [`Record::multi_output_buffer`] just reads the answer.
460    pub puts_as_string: bool,
461}
462
463impl OutTarget {
464    /// C's initializer state: `field_type = 0` is never *used* as a type by
465    /// the port (a `None` type routes to the device support's `default:`
466    /// arm), and `n_elements = 1`. An unresolved target is not in the string
467    /// class — C's `field_type = 0` matches no `case` and falls to `default:`.
468    pub const UNRESOLVED: Self = Self {
469        field_type: None,
470        element_count: 1,
471        is_ca_link: false,
472        puts_as_string: false,
473    };
474}
475
476/// The `dbrType` a record asks an INPUT link for — the second argument of C
477/// `dbGetLink(plink, dbrType, pbuffer, options, pnRequest)` (`dbLink.c:305`).
478///
479/// The READ twin of [`Record::typed_output_buffer`]'s destination switch. C's
480/// input-side switch is on the SOURCE's DBF class (`dbGetLinkDBFtype(&dol)`,
481/// `sseqRecord.c:640-705`) and each arm asks `dbGetLink` for a DIFFERENT
482/// `dbrType`, so the value a record receives is not the source's native one:
483/// a `DBF_ENUM`/`DBF_MENU` source read with `DBR_STRING` delivers its state
484/// LABEL, and a `DBF_CHAR` array read with `DBF_CHAR` delivers bytes, not a
485/// number. The record declares the request
486/// ([`Record::input_link_request`]); the framework, which is the side that
487/// can address the source, performs the conversion.
488#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub enum LinkReadAs {
490    /// The source's NATIVE value, coerced (or preserved) by the target field's
491    /// own `put_field_internal`. The framework default: every record whose C
492    /// `dbGetLink` request does not switch on the source class (compress `INP`,
493    /// waveform `INP`, sseq `SELL`, epid, motor, table) reads this way.
494    Native,
495    /// C `dbGetLink(..., DBR_STRING, ...)`. An `ENUM`/`MENU` source delivers its
496    /// state LABEL (`dbConvert.c` `getEnumString` → the record's
497    /// `get_enum_str`), never the index; a link/`DTYP` field delivers its text.
498    String,
499    /// C `dbGetLink(..., DBR_DOUBLE, ...)`.
500    Double,
501    /// C `dbGetLink(..., DBF_CHAR|DBF_UCHAR, buf, 0, &n)` — up to `max_elements`
502    /// bytes of the source's char array, taken as the string they spell
503    /// (`sseqRecord.c:682-686`: `n_elements` clamped to the record's 40-byte
504    /// `s` buffer, then `strcmp`/`atof` read it as a C string).
505    CharArrayAsString { max_elements: usize },
506}
507
508/// A record's per-link `dbGetLink` request, asked BEFORE the link's source is
509/// resolved.
510///
511/// Resolving the source ([`PvDatabase::resolve_out_target`](crate::server::database::PvDatabase))
512/// costs a records-map lookup and the TARGET record's read lock, and it has to
513/// happen with no reader lock held, so it cannot be deferred into
514/// [`Record::input_link_read_as_from_source`]. Only four record types let the
515/// source decide (`sseq` `DOLn`, `aSub`'s STRING channels, `lsi` `INP`/`SIOL`,
516/// `lso` `DOL`) — every other type's C switch is on the LINK FIELD alone. This
517/// enum is what lets those four ask for the walk and everyone else skip it,
518/// measured at 0.10 us per link per cycle.
519///
520/// [`Self::FromSource`] is the only way to reach
521/// [`Record::input_link_read_as_from_source`], so the two answers cannot
522/// disagree: a record that never asks for the source is never handed one.
523#[derive(Debug, Clone, Copy, PartialEq, Eq)]
524pub enum InputLinkRequest {
525    /// The request, settled by the link field alone.
526    As(LinkReadAs),
527    /// C's `default: break` — this record does not read that link at all.
528    NotRead,
529    /// The request depends on the source's DBF class / element count: resolve
530    /// the target and ask [`Record::input_link_read_as_from_source`].
531    FromSource,
532}
533
534/// How C gates a secondary field named by
535/// [`Record::fields_posted_with_value_mask`] *inside* the guard that decides
536/// whether VAL posts at all.
537///
538/// Both variants share the outer guard (the field posts only on a cycle where
539/// VAL's own monitor mask is live, and carries that same mask); they differ in
540/// whether C re-tests the secondary field's own value once inside it. Folding
541/// the two into one rule is what over- or under-posts the field: gating
542/// `timestamp`'s RVAL on its own change silences it (see [`Self::WithValue`]),
543/// and NOT gating `ai`'s RVAL on its own change posts a raw count that never
544/// moved.
545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
546pub enum ValuePostGate {
547    /// C re-tests the field's own previous value inside the guard, and posts
548    /// only if it moved: `ai` `RVAL` — `if (prec->oraw != prec->rval) {
549    /// db_post_events(&prec->rval, monitor_mask); prec->oraw = prec->rval; }`
550    /// (aiRecord.c:460-465).
551    OnChange,
552    /// C posts the field whenever the guard fires, with no test of its own
553    /// value: `timestamp` `RVAL` — `if (strncmp(oval, val, ...)) {
554    /// db_post_events(&val[0], mask); db_post_events(&rval, mask); }`
555    /// (timestampRecord.c:158-162). The VAL-string change is the *only* gate,
556    /// so a cycle that re-renders the same seconds count still re-posts RVAL.
557    WithValue,
558    /// [`Self::OnChange`]'s own-value test, but the guard is the record's
559    /// widened one ([`Record::take_secondary_value_mask`]) and the event
560    /// carries C's forced `monitor_mask | DBE_VALUE | DBE_LOG`: ao `RVAL` /
561    /// `RBV` — `if(prec->oraw != prec->rval) { db_post_events(&prec->rval,
562    /// monitor_mask|DBE_VALUE|DBE_LOG); prec->oraw = prec->rval; }`
563    /// (aoRecord.c:539-548), all inside `if(monitor_mask)` (`:536`).
564    ///
565    /// Distinct from [`Self::OnChange`] in BOTH halves, which is why it is a
566    /// third variant and not a flag: ai's RVAL rides VAL's own mask and VAL's
567    /// own guard, while ao's rides a forced mask and a guard `omod` can open
568    /// when VAL's is shut.
569    ///
570    /// The own-value test AND the advance of the record's "old" copy are one
571    /// step, [`Record::take_secondary_value_change`], never split across the
572    /// code that computed the new value — C assigns `oraw` INSIDE the post it
573    /// guards, so a cycle that changes RVAL without posting it must leave
574    /// `oraw` stale for the next cycle to find.
575    OnChangeForced,
576}
577
578/// The event mask ONE per-cycle mark posts with
579/// ([`Record::take_cycle_posted_fields`]).
580///
581/// A record can mark the same field from two different C `db_post_events` call
582/// sites in one cycle, and the two need not agree on the mask. aCalcout does
583/// exactly that with its arrays: `afterCalc` posts the AMASK-flagged ones with a
584/// LITERAL `DBE_VALUE|DBE_LOG` (`aCalcoutRecord.c:296`) while `monitor()` posts
585/// the NEWM-flagged ones with `monitor_mask|DBE_VALUE|DBE_LOG` (`:1034`). An
586/// array in BOTH masks gets BOTH events — the record marks it twice, and the
587/// variant carried with each mark is what keeps them distinguishable.
588#[derive(Debug, Clone, Copy, PartialEq, Eq)]
589pub enum CyclePostMask {
590    /// A literal `DBE_VALUE` — no LOG bit, no alarm bits. C's shape for a
591    /// field the record re-DERIVED from the one it was given: sseq re-renders
592    /// `STRn` after a `DOn` write and posts it with a bare `DBE_VALUE`
593    /// (`sseqRecord.c:679`, `:1115`), while the view actually written carries
594    /// `DBE_VALUE|DBE_LOG`.
595    Value,
596    /// A literal `DBE_VALUE | DBE_LOG` — the alarm-transition bits are NOT
597    /// folded in, because this C call site does not have `monitor_mask` in
598    /// scope (aCalcout `afterCalc`, `aCalcoutRecord.c:296`).
599    ValueLog,
600    /// `monitor_mask | DBE_VALUE | DBE_LOG` — C's usual `monitor()` shape
601    /// (aCalcout `monitor()`, `aCalcoutRecord.c:1034`).
602    MonitorValueLog,
603}
604
605/// The [`ValuePostGate`] a record declared for `field`, or `None` when `field`
606/// is not one of its secondary value-mask fields.
607///
608/// The single lookup for [`Record::fields_posted_with_value_mask`], shared by
609/// every monitor loop (both `process_record_*` paths, the deferred-completion
610/// path, and `RecordInstance::process_local`) so they cannot drift apart on how
611/// a secondary field is gated.
612pub(crate) fn value_gate(
613    value_masked: &'static [(&'static str, ValuePostGate)],
614    field: &str,
615) -> Option<ValuePostGate> {
616    value_masked
617        .iter()
618        .find(|(name, _)| *name == field)
619        .map(|(_, gate)| *gate)
620}
621
622/// [`Record::set_input_link_slots`] answered off the record's own link texts,
623/// in [`Record::multi_input_links`] order.
624///
625/// Every slot is lent a text, so the "unknown" half of the pair is empty by
626/// construction: a type that can answer this way knows all of its own links.
627pub fn input_link_slots_of<S: AsRef<str>>(texts: &[S]) -> Option<(u64, u64)> {
628    debug_assert!(texts.len() <= u64::BITS as usize);
629    let mut set = 0u64;
630    for (slot, text) in texts.iter().enumerate() {
631        if !text.as_ref().is_empty() {
632            set |= 1 << slot;
633        }
634    }
635    Some((set, 0))
636}
637
638/// The event mask a change-detected AUXILIARY field posts with — the single
639/// owner of that decision, built once per cycle from the record's declarations
640/// and shared by every monitor loop (both `process_record_*` paths, the
641/// deferred-completion path, and `RecordInstance::process_local`), so they
642/// cannot drift apart on what mask a field carries.
643///
644/// C's usual shape for the "post every input/aux field that changed" loop is
645/// `monitor_mask | DBE_VALUE | DBE_LOG` (calcRecord.c:420, subRecord.c:400,
646/// motor `DBE_VAL_LOG`) — that is the default. Three record-declared exceptions
647/// narrow it, and no two are the same narrowing:
648///
649/// * [`Record::value_only_change_fields`] — a literal `DBE_VALUE`
650///   (tableRecord.c:659, scaler `Sn`): alarm bits + `DBE_VALUE`, never `LOG`.
651/// * [`Record::fields_posted_with_monitor_mask`] — `monitor_mask | DBE_VALUE`
652///   (swaitRecord.c:650): VAL's own monitor mask, so `DBE_LOG` rides along
653///   exactly when VAL's ADEL deadband crossed.
654/// * [`Record::fields_posted_without_alarm_bits`] — a literal
655///   `DBE_VALUE | DBE_LOG` (epidRecord.c:376): both value classes, alarm bits
656///   discarded.
657#[derive(Clone, Copy)]
658pub(crate) struct AuxPostMask {
659    value_only: &'static [&'static str],
660    monitor_masked: &'static [&'static str],
661    no_alarm_bits: &'static [&'static str],
662}
663
664impl AuxPostMask {
665    /// Read the record's three declarations once, outside the per-field loop.
666    pub(crate) fn of(record: &dyn Record) -> Self {
667        Self {
668            value_only: record.value_only_change_fields(),
669            monitor_masked: record.fields_posted_with_monitor_mask(),
670            no_alarm_bits: record.fields_posted_without_alarm_bits(),
671        }
672    }
673
674    /// `alarm_bits` is this cycle's `recGblResetAlarms` result; `deadband_mask`
675    /// is VAL's own monitor mask (those alarm bits, plus `DBE_VALUE` when MDEL
676    /// crossed and `DBE_LOG` when ADEL crossed).
677    pub(crate) fn mask_for(
678        &self,
679        field: &str,
680        alarm_bits: crate::server::recgbl::EventMask,
681        deadband_mask: crate::server::recgbl::EventMask,
682    ) -> crate::server::recgbl::EventMask {
683        use crate::server::recgbl::EventMask;
684        if self.value_only.contains(&field) {
685            alarm_bits | EventMask::VALUE
686        } else if self.monitor_masked.contains(&field) {
687            deadband_mask | EventMask::VALUE
688        } else if self.no_alarm_bits.contains(&field) {
689            EventMask::VALUE | EventMask::LOG
690        } else {
691            alarm_bits | EventMask::VALUE | EventMask::LOG
692        }
693    }
694}
695
696/// Outcome of a record's array-style monitor decision, returned by
697/// [`Record::array_monitor_post`] (C waveform/aai/aao `monitor()`,
698/// waveformRecord.c:291-326).
699#[derive(Debug, Clone, Copy)]
700pub struct ArrayMonitorPost {
701    /// Include `DBE_VALUE` on the VAL post this cycle (MPST = Always, or
702    /// MPST = On Change with a changed hash).
703    pub post_value: bool,
704    /// Include `DBE_LOG` on the VAL post this cycle (APST = Always, or
705    /// APST = On Change with a changed hash).
706    pub post_archive: bool,
707    /// The content hash changed this cycle (On Change mode) — the owner
708    /// posts `HASH` with a literal `DBE_VALUE`.
709    pub hash_changed: bool,
710}
711
712/// The record type's RSET metadata slots — which of C's six nullable
713/// `get_*` property functions the record type implements.
714///
715/// This is the port's `rset` property table, transcribed slot by slot
716/// from the `#define get_xxx NULL` lines of each C record's `.c`. It is
717/// what `dbGet` consults to *narrow* the caller's `options` mask
718/// (`dbAccess.c:336-427`), and therefore what decides whether QSRV marks
719/// an NT leaf at all (pvxs `ioc/iocsource.cpp:263-305`). Without it the
720/// port fabricated every leaf it could name and marked it as supplied —
721/// telling the client a made-up `display.precision = 0` on a `longout`,
722/// or `valueAlarm` bands at zero on a `waveform`, were authoritative.
723///
724/// A slot counts as supplied when the C function pointer is non-NULL,
725/// even if the function writes nothing for the field in question — C
726/// leaves the option bit set either way (e.g. `boRecord.c:294-299`,
727/// whose `get_units` writes `"s"` only for `HIGH`, yet `DBR_UNITS`
728/// survives for every `bo` field).
729/// What a record type's C `get_control_double` writes for a field its switch
730/// does not list — the slot's LAST arm.
731///
732/// Independent of [`crate::server::snapshot::PropertySupport::control_double`],
733/// which says whether the slot EXISTS at all (a NULL slot makes
734/// `dbAccess.c:257` fail and clears the option bit, so no leaf is served).
735/// This says what a slot that DOES exist answers when it falls through. The
736/// two genuinely differ: `acalcout` supplies the slot and still writes nothing
737/// for an unlisted field.
738///
739/// Slot-neutral: the two arm SHAPES are the same for `get_control_double` and
740/// `get_graphic_double`, but which one a record type takes is asked per slot —
741/// [`control_default_arm`] and [`graphic_default_arm`] are separate answers.
742/// `aSub` is the type that proves they must be: its `get_control_double` is a
743/// bare `recGblGetControlDouble` (`aSubRecord.c:372-376`) while its
744/// `get_graphic_double` (`:350-368`) has no recGbl call at all.
745#[derive(Debug, Clone, Copy, PartialEq, Eq)]
746pub enum RsetDefaultArm {
747    /// The slot ends in `recGblGetControlDouble` / `recGblGetGraphicDouble` —
748    /// the field TYPE's numeric range (`recGbl.c:146-171`, table at
749    /// `:372-419`).
750    RecGblRange,
751    /// The slot returns without writing, so the `dbAccess.c:256` / `:216`
752    /// `(0.0, 0.0)` seed stands.
753    Seed,
754}
755
756/// The last arm of `rtype`'s C `get_control_double`.
757///
758/// Audited by reading each ported record type's own C rset. Every record in
759/// EPICS base delegates (`aiRecord.c:267`, `aoRecord.c:341`, `calcRecord.c:235`,
760/// `calcoutRecord.c:506`, `aSubRecord.c:372`, `subRecord.c:272`,
761/// `selRecord.c:203`, `seqRecord.c:342`, `dfanoutRecord.c:197`,
762/// `longinRecord.c:217`, `longoutRecord.c:268`, `int64inRecord.c:212`,
763/// `int64outRecord.c:251`, `boRecord.c:310`, `waveformRecord.c:268`,
764/// `aaiRecord.c:293`, `aaoRecord.c:296`, `subArrayRecord.c:262`,
765/// `compressRecord.c:487`, `histogramRecord.c:458`), as do the downstream
766/// types that supply the slot (`motorRecord.cc:3303`, `epidRecord.c:285`,
767/// `tableRecord.c:806`). The rest NULL it outright and never reach here
768/// (`biRecord.c`, `mbbiRecord.c`, `mbboRecord.c`, `mbbiDirectRecord.c`,
769/// `mbboDirectRecord.c`, `stringinRecord.c`, `stringoutRecord.c`,
770/// `lsiRecord.c`, `lsoRecord.c`, `eventRecord.c`, `fanoutRecord.c`,
771/// `permissiveRecord.c`, `printfRecord.c`, `stateRecord.c`,
772/// `sseqRecord.c:141`, `swaitRecord.c`, `transformRecord.c`, `busyRecord.c`,
773/// `asynRecord.c`, `throttleRecord.c:70`, `scalerRecord.c:157`).
774///
775/// Only the synApps calc pair writes nothing: both end `get_control_double`
776/// with a bare `return(0)` after their listed cases, so C serves the seed
777/// where a delegating record serves the type range. Measured on the
778/// differential oracle as 42 `acalcout`/`scalcout` fields.
779pub fn control_default_arm(rtype: &str) -> RsetDefaultArm {
780    match rtype {
781        // aCalcoutRecord.c:793-822, sCalcoutRecord.c:653-682 — the switch lists
782        // VAL/HIHI/HIGH/LOW/LOLO and the A-L / PA-PL ranges, then falls off
783        // the end into `return(0)` with no recGbl delegation.
784        "acalcout" | "scalcout" => RsetDefaultArm::Seed,
785        _ => RsetDefaultArm::RecGblRange,
786    }
787}
788
789/// The last arm of `rtype`'s C `get_graphic_double` — the twin of
790/// [`control_default_arm`], and NOT the same answer for every type.
791///
792/// Read from each ported type's own rset. `aSubRecord.c:350-368` is the one
793/// that separates the two slots: it tries `get_inlinkNumber` then
794/// `get_outlinkNumber` and, for a field that is neither, falls out of the
795/// function having written nothing — no `default:`, no recGbl call — so the
796/// `dbAccess.c:216` seed stands. Its `get_control_double` (`:372-376`) is a
797/// bare `recGblGetControlDouble` in the same file, which is why one shared bit
798/// could not answer both. Measured: `ASUB.PHAS` serves display 0/0 where
799/// `CALC.PHAS` serves the DBF_SHORT range ±32767.
800///
801/// The synApps calc pair ends the same way — the listed cases return early and
802/// the function ends `return(0)` with no delegation
803/// (`aCalcoutRecord.c:762-791`, `sCalcoutRecord.c:622-651`).
804///
805/// Every other ported type that supplies the slot delegates
806/// (`aiRecord.c:244`, `aoRecord.c:316`, `calcRecord.c:187`,
807/// `calcoutRecord.c:452`, `subRecord.c:222`, `selRecord.c:181`,
808/// `seqRecord.c:322`, `dfanoutRecord.c:181`, `longinRecord.c:190`,
809/// `int64inRecord.c:196`, `int64outRecord.c:235`, `longoutRecord.c:252`,
810/// `waveformRecord.c:251`, `aaiRecord.c:276`, `aaoRecord.c:279`,
811/// `subArrayRecord.c:231`, `compressRecord.c:471`, `histogramRecord.c:442`).
812pub fn graphic_default_arm(rtype: &str) -> RsetDefaultArm {
813    match rtype {
814        "aSub" | "acalcout" | "scalcout" => RsetDefaultArm::Seed,
815        _ => RsetDefaultArm::RecGblRange,
816    }
817}
818
819/// How `rtype`'s C `get_alarm_double` answers the fields it lists explicitly
820/// (see [`alarm_explicit_fields`]).
821///
822/// The severity gate is NOT universal, so this bit cannot be inferred from the
823/// base analog shape — it is read from each ported type's own rset.
824#[derive(Debug, Clone, Copy, PartialEq, Eq)]
825pub enum AlarmValArm {
826    /// `pad->upper_alarm_limit = prec->hhsv ? prec->hihi : epicsNAN` — each
827    /// limit is served only when its severity is enabled. The base analog
828    /// shape (`aiRecord.c:290-301`, and ao/longin/longout/calc/calcout/sel/
829    /// sub/dfanout alike).
830    Gated,
831    /// `pad->upper_alarm_limit = prec->hihi` — the four limits verbatim, with
832    /// no severity test at all, so an unset record serves 0 rather than NaN
833    /// (`int64inRecord.c:235-246`, `int64outRecord.c:279-290`,
834    /// `sCalcoutRecord.c:683-696`, `aCalcoutRecord.c:823-836`,
835    /// `motorRecord.cc:3344-3361`, `epidRecord.c:289-301`).
836    Unconditional,
837}
838
839/// The fields `rtype`'s C `get_alarm_double` lists BEFORE its
840/// `recGblGetAlarmDouble` fall-through — the ones that take
841/// [`alarm_val_arm`]'s answer instead of the four NaN.
842///
843/// Empty means the rset lists nothing: even VAL falls to the default arm.
844/// `seqRecord.c:355-367` routes only its `DOn` fields (through their `DOLn`
845/// link), `aSubRecord.c:378-404` only its `INPn`/`OUTn` links, and
846/// `swaitRecord.c:608-612` is a bare `recGblGetAlarmDouble(paddr,pad)` with no
847/// field test whatsoever. A constant link supplies no alarm limits, so the link
848/// arm lands on the same four NaN — which is why only the listed set needs a
849/// per-type answer and the link fields do not.
850///
851/// `motorRecord.cc:3344-3361` is the one type listing a second field: its case
852/// is `fieldIndex == motorRecordVAL || fieldIndex == motorRecordDVAL`, so the
853/// dial-coordinate readback carries the same limits as VAL.
854pub fn alarm_explicit_fields(rtype: &str) -> &'static [&'static str] {
855    match rtype {
856        "seq" | "aSub" | "swait" => &[],
857        "motor" => &["VAL", "DVAL"],
858        _ => &["VAL"],
859    }
860}
861
862/// See [`AlarmValArm`]. Types whose rset lists nothing
863/// ([`alarm_explicit_fields`] empty) never consult this.
864pub fn alarm_val_arm(rtype: &str) -> AlarmValArm {
865    match rtype {
866        "int64in" | "int64out" | "scalcout" | "acalcout" | "motor" | "epid" => {
867            AlarmValArm::Unconditional
868        }
869        _ => AlarmValArm::Gated,
870    }
871}
872
873/// The record fields `rtype`'s C `get_graphic_double` reads for the fields its
874/// rset lists — the source of the record-level display limits.
875///
876/// `HOPR`/`LOPR` in every ported type but one: `motorRecord.cc:3221-3225`
877/// answers the soft travel limits `HLM`/`LLM` and never reads its own
878/// HOPR/LOPR.
879///
880/// The `"motor"` arm decides nothing today and is kept deliberately.
881/// `MotorRecord::field_metadata_override` (`motor-rs/src/record/mod.rs:201-205`)
882/// answers `Some(..)` for EVERY field, and the override layer runs after
883/// `route_field_metadata` (pinned by
884/// `epics-base-rs/tests/the_override_layer_runs_last.rs`), so every served
885/// motor field takes its window from `motor-rs` and never from this table.
886/// The arm states what C's rset does for the day a motor field reaches the
887/// routing layer without an override.
888pub fn graphic_limit_fields(rtype: &str) -> (&'static str, &'static str) {
889    match rtype {
890        "motor" => ("HLM", "LLM"),
891        _ => ("HOPR", "LOPR"),
892    }
893}
894
895/// Which of the record's own fields `rtype`'s C `get_control_double` answers
896/// with for the fields its rset lists.
897#[derive(Debug, Clone, Copy, PartialEq, Eq)]
898pub enum ControlLimitSource {
899    /// `DRVH`/`DRVL` unconditionally (`aoRecord.c:356-357`).
900    Drive,
901    /// `DRVH`/`DRVL` when `DRVH > DRVL`, else `HOPR`/`LOPR`
902    /// (`longoutRecord.c:282-287`, `int64outRecord.c:265-270`).
903    DriveWhenSet,
904    /// The soft travel limits `HLM`/`LLM` (`motorRecord.cc:3272-3276`).
905    SoftLimits,
906    /// `HOPR`/`LOPR` — every other ported type that supplies the slot.
907    Operator,
908}
909
910/// See [`ControlLimitSource`]. Read from each ported type's own rset; the
911/// three named types are the only ones in base or the ported modules whose
912/// control limits are not the operator range.
913///
914/// The `"motor"` arm is dead for the same reason, and kept for the same
915/// reason, as the `"motor"` arm of [`graphic_limit_fields`].
916pub fn control_limit_source(rtype: &str) -> ControlLimitSource {
917    match rtype {
918        "ao" => ControlLimitSource::Drive,
919        "longout" | "int64out" => ControlLimitSource::DriveWhenSet,
920        "motor" => ControlLimitSource::SoftLimits,
921        _ => ControlLimitSource::Operator,
922    }
923}
924
925pub fn default_property_support(rtype: &str) -> crate::server::snapshot::PropertySupport {
926    use crate::server::snapshot::PropertySupport as P;
927    match rtype {
928        // Every numeric slot, no enum strings.
929        // aiRecord.c:68-87, aoRecord.c:67-86, calcRecord.c:63-82,
930        // calcoutRecord.c:67-86, selRecord.c:58-77, subRecord.c:62-81,
931        // dfanoutRecord.c:66-85, seqRecord.c:56-75.
932        "ai" | "ao" | "calc" | "calcout" | "sel" | "sub" | "dfanout" | "seq" => P::NUMERIC,
933
934        // Integer scalars: `#define get_precision NULL`
935        // (longinRecord.c, longoutRecord.c, int64inRecord.c,
936        // int64outRecord.c). This is the measured `longout` case —
937        // pvxs leaves `display.precision` absent, the port sent 0.
938        "longin" | "longout" | "int64in" | "int64out" => P {
939            precision: false,
940            ..P::NUMERIC
941        },
942
943        // Arrays and compress: `#define get_alarm_double NULL`
944        // (waveformRecord.c, aaiRecord.c, aaoRecord.c,
945        // subArrayRecord.c, compressRecord.c, histogramRecord.c).
946        // This is the measured `waveform` case — pvxs leaves all four
947        // `valueAlarm.*Limit` absent, the port sent four zeros.
948        "waveform" | "aai" | "aao" | "subArray" | "compress" | "histogram" => P {
949            alarm_double: false,
950            ..P::NUMERIC
951        },
952
953        // No property slots at all: every `get_*` is `#define`d NULL.
954        // stringinRecord.c:62-81, stringoutRecord.c:64-83,
955        // lsiRecord.c:287-306, lsoRecord.c:328-347,
956        // eventRecord.c:62-81, permissiveRecord.c:56-75,
957        // stateRecord.c:58-77, printfRecord.c:456-475,
958        // fanoutRecord.c:60-79, timestampRecord (std-rs).
959        // This is the measured `stringout` case — pvxs leaves
960        // `display.units` absent, the port sent "".
961        "stringin" | "stringout" | "lsi" | "lso" | "event" | "permissive" | "state" | "printf"
962        | "fanout" | "timestamp" => P::NONE,
963
964        // Enum records. `biRecord.c:61-80` and `mbbiRecord.c:65-84` /
965        // `mbboRecord.c:64-83` NULL every numeric slot and supply only
966        // `get_enum_strs`. `boRecord.c:54-61` keeps `get_units`,
967        // `get_precision` and `get_control_double` (they serve the
968        // `HIGH` field) but NULLs `get_graphic_double` and
969        // `get_alarm_double`.
970        "bi" | "mbbi" | "mbbo" => P {
971            enum_strs: true,
972            ..P::NONE
973        },
974        "bo" => P {
975            units: true,
976            precision: true,
977            control_double: true,
978            enum_strs: true,
979            ..P::NONE
980        },
981        // busyRecord.c (synApps busy): units/graphic/control/alarm NULL,
982        // get_precision and get_enum_strs present.
983        "busy" => P {
984            precision: true,
985            enum_strs: true,
986            ..P::NONE
987        },
988        // mbbiDirectRecord.c:63-81 / mbboDirectRecord.c:63-81 — only
989        // `get_precision` survives, and C's DBF_FLOAT/DOUBLE gate
990        // (`dbAccess.c:388-395`) drops it again for their DBF_ENUM/LONG
991        // value, so nothing is marked. `Snapshot::precision` applies
992        // that gate.
993        "mbbiDirect" | "mbboDirect" => P {
994            precision: true,
995            ..P::NONE
996        },
997
998        // synApps, transcribed the same way.
999        // sCalcoutRecord.c / aCalcoutRecord.c / epidRecord.c /
1000        // motorRecord.cc:259-279 (the rset table: get_units, get_precision,
1001        // get_graphic_double, get_control_double and get_alarm_double all
1002        // supplied, get_enum_strs NULL) / aSubRecord.c: full numeric set, no
1003        // enum strings.
1004        "scalcout" | "acalcout" | "motor" | "epid" | "aSub" => P::NUMERIC,
1005        // scalerRecord.c:147-158 NULLs every property slot but one:
1006        // `#define get_units NULL` (:151), `get_enum_strs` (:154),
1007        // `get_graphic_double` (:156), `get_control_double` (:157) and
1008        // `get_alarm_double` (:158). Only `get_precision` (:152) survives.
1009        //
1010        // Grouping scaler with the full-numeric synApps types claimed TEN
1011        // leaves QSRV2 never serves: `display.units`, the two `display.limit*`,
1012        // the two `control.limit*`, the four `valueAlarm.*Limit` — and
1013        // `display.precision`, which pvxs assigns only inside its
1014        // `DBR_GR_DOUBLE` branch (`iocsource.cpp:288-291`). That nesting is
1015        // also why `precision` stays true here and yet marks nothing: it
1016        // records what the rset supplies, exactly as `transform`/`sseq` do.
1017        "scaler" => P {
1018            precision: true,
1019            ..P::NONE
1020        },
1021        // tableRecord.cc (optics): `#define get_alarm_double NULL`.
1022        "table" => P {
1023            alarm_double: false,
1024            ..P::NUMERIC
1025        },
1026        // swaitRecord.c: get_units and get_control_double are NULL.
1027        "swait" => P {
1028            units: false,
1029            control_double: false,
1030            ..P::NUMERIC
1031        },
1032        // Only `get_precision` survives; the rset NULLs the other five.
1033        // transformRecord.c; sseqRecord.c:124-144 (the rset table itself —
1034        // `NULL, /* get_units */ get_precision, /* get_precision */ ...
1035        // NULL, /* get_graphic_double */ NULL, /* get_control_double */
1036        // NULL /* get_alarm_double */`). `sseq` was previously grouped with
1037        // the full-numeric synApps types, which marked six leaves per field
1038        // that QSRV2 omits entirely.
1039        //
1040        // `asyn` is NOT here: asyn-rs owns that record and declares its own
1041        // row (asynRecord.c:84-91, the same shape) — a downstream crate
1042        // cannot reach this table, which is why `Record::property_support`
1043        // is the hook and this is only its default.
1044        "transform" | "sseq" => P {
1045            precision: true,
1046            ..P::NONE
1047        },
1048        "throttle" => P {
1049            precision: true,
1050            graphic_double: true,
1051            ..P::NONE
1052        },
1053
1054        // A record type whose C rset the port has not transcribed keeps
1055        // the pre-existing "supplies what it populated" behaviour rather
1056        // than silently losing metadata. Add an arm above — with the C
1057        // file and line — when porting a new record type.
1058        _ => P::NUMERIC,
1059    }
1060}
1061
1062/// Per-field metadata deltas returned by
1063/// [`Record::field_metadata_override`].
1064///
1065/// Each `Some` member replaces the corresponding member of the
1066/// snapshot's record-level display/control metadata; `None` members
1067/// keep the record-level value.
1068#[derive(Debug, Clone, Default)]
1069pub struct FieldMetadataOverride {
1070    /// `display.units` — C RSET `get_units`.
1071    pub units: Option<crate::types::PvString>,
1072    /// `display.precision` — C RSET `get_precision`.
1073    pub precision: Option<i16>,
1074    /// `(upper, lower)` display limits — C RSET `get_graphic_double`.
1075    pub disp_limits: Option<(f64, f64)>,
1076    /// `(upper, lower)` control limits — C RSET `get_control_double`.
1077    pub ctrl_limits: Option<(f64, f64)>,
1078    /// `(hihi, high, low, lolo)` — C RSET `get_alarm_double`.
1079    pub alarm_limits: Option<(f64, f64, f64, f64)>,
1080}
1081
1082/// Side-effect actions that a record requests from the processing framework.
1083///
1084/// Records return these from `process()` via `ProcessOutcome::actions`.
1085/// The framework executes them at the appropriate point in the processing
1086/// cycle, keeping records as pure state machines without direct DB access.
1087#[derive(Clone, Debug, PartialEq)]
1088pub enum ProcessAction {
1089    /// Write a value to a DB link. The framework reads `link_field` from the
1090    /// record to get the target PV name, then writes `value` to that PV.
1091    ///
1092    /// Executed after alarm/snapshot, before FLNK.
1093    /// Example: scaler writes CNT to COUT/COUTP links.
1094    WriteDbLink {
1095        link_field: &'static str,
1096        value: EpicsValue,
1097    },
1098
1099    /// Resolve an OUT link's TARGET ([`OutTarget`]) and hand it to the record
1100    /// through [`Record::set_resolved_out_target`], BEFORE `process()` runs.
1101    ///
1102    /// **Pre-process action** — the OUT-link twin of [`Self::ReadDbLink`],
1103    /// and C's `checkLinks`-cached `lnk_field_type`: a record whose fire-time
1104    /// branch depends on the target's DBF class (sseq decides the wire buffer
1105    /// AND whether a `WAITn` put-callback is issued from the one switch,
1106    /// `sseqRecord.c:714-792`) must have the class in hand when it decides, not
1107    /// after the framework's put path has already been entered.
1108    ResolveOutTarget { link_field: &'static str },
1109
1110    /// Read a value from a DB link into a record field. The framework reads
1111    /// `link_field` from the record to get the source PV name, reads that PV,
1112    /// and writes the result into `target_field` via an internal put that
1113    /// bypasses read-only checks.
1114    ///
1115    /// The value delivered is the link target's **native** [`EpicsValue`] — it
1116    /// is NOT coerced to a numeric type on the way in. The record coerces (or
1117    /// preserves) it at its own `put_field`/`put_field_internal` boundary, so a
1118    /// string-class source can reach a string field byte-exact (the `sseq`
1119    /// `DOLn`→`STRn` path, C `sseqRecord.c:643-705`). Records whose
1120    /// `target_field` is numeric simply convert there, exactly as before.
1121    ///
1122    /// **Pre-process action**: executed BEFORE the next process() cycle so
1123    /// the value is immediately available. This matches C EPICS `dbGetLink()`
1124    /// which is synchronous/immediate.
1125    ///
1126    /// Example: throttle reads SINP into VAL when SYNC is triggered.
1127    ReadDbLink {
1128        link_field: &'static str,
1129        target_field: &'static str,
1130    },
1131
1132    /// Schedule a re-process of this record after the given duration.
1133    /// The framework defers it through
1134    /// `database/processing.rs` (`schedule_delayed_reprocess`), which uses
1135    /// `spawn_background` + `sleep_background` — *not* the ambient
1136    /// `tokio::spawn`/`tokio::time::sleep`, which a record-processing thread
1137    /// has no runtime for. The current cycle's OUT/FLNK/notify proceed
1138    /// normally.
1139    ///
1140    /// Equivalent to C EPICS `callbackRequestDelayed()` + `scanOnce()`
1141    /// (`db/callback.c:410` (`callbackRequestDelayed`), `db/dbScan.c:660`
1142    /// (`scanOnce`); epics-base R7.0.10).
1143    ReprocessAfter(std::time::Duration),
1144
1145    /// C `callbackRequestDelayed()` armed with a handler that mutates the
1146    /// record BEFORE it calls `dbProcess` — `boRecord.c::myCallbackFunc`
1147    /// (:105-118) and its `busyRecord.c:107-124` twin, the HIGH one-shot.
1148    ///
1149    /// Distinct from [`Self::ReprocessAfter`], whose timer re-enters
1150    /// `process()` unchanged. Here the fire runs
1151    /// [`Record::delayed_callback_fire`] under the record gate first, and that
1152    /// hook — not `process()` — performs the timer's own mutation. The record
1153    /// therefore keeps no "a timer is pending" flag for `process()` to consume,
1154    /// so a foreign scan, a caput or a FLNK arriving inside the delay window
1155    /// cannot take the one-shot the timer owns.
1156    DelayedCallbackAfter(std::time::Duration),
1157
1158    /// C `scanOnce(precord)` — queue ONE process of this record, now.
1159    ///
1160    /// A record's `special()` emits this when a put changed state the record
1161    /// must act on but the put itself will not process the record. C guards
1162    /// every such call with `if (precord->scan)` — scaler `special()`
1163    /// (scalerRecord.c:655 CNT, :667 CONT), whose comment is exactly the
1164    /// contract: *"Scan record if it's not Passive. (If it's Passive, it'll
1165    /// get scanned automatically, since .cnt is a Process-Passive field.)"*
1166    ///
1167    /// The FRAMEWORK owns that gate, not the record: the framework owns SCAN
1168    /// and owns the `pp(TRUE)` reprocess decision (`dbPutField`,
1169    /// dbAccess.c:1265-1268), and a record's `special()` cannot see either. So
1170    /// a record emits `ScanOnce` unconditionally wherever C calls `scanOnce`,
1171    /// and the executor drops it for a Passive record — where the put's own
1172    /// process already covers it and a second one would double-process.
1173    ///
1174    /// Queued, not inline: C's `scanOnce` hands the record to the scan-once
1175    /// thread, which takes `dbScanLock` — so the process lands after the
1176    /// putting thread leaves `dbPutField`.
1177    ScanOnce,
1178
1179    /// Send a named command to the device support driver.
1180    /// The framework calls `DeviceSupport::handle_command()` with this data.
1181    /// Used by scaler to request reset/arm/write_preset operations
1182    /// without the record holding a direct driver reference.
1183    DeviceCommand {
1184        command: &'static str,
1185        args: Vec<EpicsValue>,
1186    },
1187
1188    /// Write a value to a DB link as a put-*with-completion*, then re-enter
1189    /// THIS record's `process()` when the downstream operation completes.
1190    ///
1191    /// The framework arms a put-notify wait-set (C `dbProcessNotify`),
1192    /// writes `link_field`'s target through it, releases the initiator's
1193    /// own count, and wires the completion to an async re-entry of this
1194    /// record (`mint_async_token` + `reprocess_on_notify`). The record
1195    /// returns [`RecordProcessResult::AsyncPending`] alongside this action
1196    /// and is re-entered once the downstream record (and its FLNK/OUT
1197    /// chain) finishes — the synApps `sseq` `WAITn` "wait for the put
1198    /// callback" dependency (`sseqRecord.c::processNextLink`,
1199    /// `dbCaPutLinkCallback`). Built on the same `new_put_notify` +
1200    /// `reprocess_on_notify` primitive an out-of-band
1201    /// [`crate::server::database::AsyncDbHandle`] caller uses.
1202    ///
1203    /// Executed before FLNK, like [`Self::WriteDbLink`].
1204    WriteDbLinkNotify {
1205        link_field: &'static str,
1206        value: EpicsValue,
1207    },
1208
1209    /// (Re)arm this record's monitor watchdog — C `histogramRecord.c::wdogInit`
1210    /// (:126-152), whose `callbackRequestDelayed(&pcallback->callback,
1211    /// prec->sdel)` starts (or restarts) the periodic
1212    /// [`Record::watchdog_fire`] tick.
1213    ///
1214    /// Emitted from a record's `special()` when the put changed the watchdog's
1215    /// period (histogram SDEL is `special(SPC_RESET)` precisely so it can
1216    /// re-arm, `histogramRecord.c:266-268`). The framework also arms every
1217    /// record's watchdog once at `iocInit`, which is C's other `wdogInit` call
1218    /// site (`init_record` pass 1, `:168`).
1219    ///
1220    /// Arming supersedes any tick already pending for the record, exactly as
1221    /// C's `callbackRequestDelayed` replaces an outstanding delayed callback.
1222    ArmWatchdog,
1223
1224    /// Cancel this record's outstanding async re-entry (C
1225    /// `callbackCancelDelayed`): the framework advances the record's
1226    /// re-entry generation so any pending `ReprocessAfter` timer or
1227    /// `WriteDbLinkNotify` completion re-entry becomes a structural no-op
1228    /// (the `AsyncToken` gate), with no runtime "is-aborted" check on the
1229    /// re-entry path. Used by `sseq` `ABORT` to drop a pending `DLYn`
1230    /// delay or `WAITn` wait; the record resets its own sequence state in
1231    /// the same `process()` cycle that emits this.
1232    CancelReprocess,
1233}
1234
1235/// A list a record hands the framework for one cycle — the actions of
1236/// [`Record::pre_input_link_actions`] and [`Record::pre_process_actions`],
1237/// and the shape a `process()` outcome's lists are carried in.
1238///
1239/// An empty list is the answer of every record type that never fills it,
1240/// on every cycle, so discarding one must cost nothing: the list owns no
1241/// allocation while its capacity is zero, and the drop is a single inline
1242/// test then. A bare `Vec` pays an out-of-line drop glue call for the same
1243/// nothing, once per list per cycle.
1244#[derive(Debug)]
1245pub struct CycleList<T>(std::mem::ManuallyDrop<Vec<T>>);
1246
1247/// The actions a record hands the framework for one cycle.
1248pub type ProcessActions = CycleList<ProcessAction>;
1249
1250impl<T> CycleList<T> {
1251    /// An empty list.
1252    pub fn new() -> Self {
1253        Self::default()
1254    }
1255
1256    /// Add one entry, after those already listed.
1257    pub fn push(&mut self, entry: T) {
1258        self.0.push(entry);
1259    }
1260
1261    /// The list as the `Vec` it is, for the consumers that take it apart.
1262    pub fn into_vec(mut self) -> Vec<T> {
1263        // The take leaves a capacity-0 `Vec` behind, which the drop below
1264        // then has nothing to release.
1265        std::mem::take(&mut *self.0)
1266    }
1267
1268    #[cold]
1269    #[inline(never)]
1270    fn release(entries: &mut std::mem::ManuallyDrop<Vec<T>>) {
1271        // SAFETY: called once, from `drop`, on a field nothing else drops —
1272        // it is `ManuallyDrop`, and `into_vec` leaves a fresh `Vec` in it.
1273        unsafe { std::mem::ManuallyDrop::drop(entries) }
1274    }
1275}
1276
1277impl<T> Default for CycleList<T> {
1278    fn default() -> Self {
1279        Self(std::mem::ManuallyDrop::new(Vec::new()))
1280    }
1281}
1282
1283impl<T> From<Vec<T>> for CycleList<T> {
1284    fn from(entries: Vec<T>) -> Self {
1285        Self(std::mem::ManuallyDrop::new(entries))
1286    }
1287}
1288
1289impl<T> Drop for CycleList<T> {
1290    #[inline]
1291    fn drop(&mut self) {
1292        // A capacity-0 `Vec` holds no elements and owns no allocation, so
1293        // there is nothing to run for it. The release is a frame of its own
1294        // so that this test is all the drop glue holds, and is inlined.
1295        if self.0.capacity() != 0 {
1296            Self::release(&mut self.0);
1297        }
1298    }
1299}
1300
1301impl<T> std::ops::Deref for CycleList<T> {
1302    type Target = [T];
1303
1304    fn deref(&self) -> &[T] {
1305        &self.0
1306    }
1307}
1308
1309impl<T> IntoIterator for CycleList<T> {
1310    type Item = T;
1311    type IntoIter = std::vec::IntoIter<T>;
1312
1313    fn into_iter(self) -> Self::IntoIter {
1314        self.into_vec().into_iter()
1315    }
1316}
1317
1318/// What the [`ProcessAction::DelayedCallbackAfter`] timer does once the
1319/// record's [`Record::delayed_callback_fire`] handler has run — the three arms
1320/// of C `boRecord.c::myCallbackFunc` (:105-118).
1321#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1322pub enum DelayedCallbackOutcome {
1323    /// C's `else` branch (`boRecord.c:115-117`): the handler made its change,
1324    /// so re-enter `process()` now.
1325    Reprocess,
1326    /// C's `if (prec->pact)` branch (`boRecord.c:107-114`): the record is
1327    /// mid-async-cycle, so the handler changed nothing and the timer is re-armed
1328    /// for this long instead.
1329    Rearm(std::time::Duration),
1330    /// C's `if (prec->pact)` branch with its inner test false: the timer expires
1331    /// with nothing left to do, neither mutating nor processing.
1332    Drop,
1333}
1334
1335/// Result of a record's process() call.
1336///
1337/// Determines how the framework handles the current processing cycle.
1338/// Side-effect actions (link writes, delayed reprocess, etc.) are expressed
1339/// separately in `ProcessOutcome::actions`.
1340#[derive(Clone, Debug, PartialEq)]
1341pub enum RecordProcessResult {
1342    /// Processing completed synchronously this cycle.
1343    /// Framework proceeds with alarm/timestamp/snapshot/OUT/FLNK.
1344    Complete,
1345    /// Processing started but not yet complete (PACT stays set).
1346    /// Current cycle skips alarm/timestamp/snapshot/OUT/FLNK.
1347    /// ProcessActions (if any) are still executed.
1348    AsyncPending,
1349    /// Async pending, but notify these intermediate field changes immediately.
1350    /// Used by motor records to flush DMOV=0 before the move completes.
1351    AsyncPendingNotify(Vec<(String, EpicsValue)>),
1352    /// Completed synchronously (PACT cleared, unlike `AsyncPending`), but the
1353    /// record produced no new value to publish this cycle — the framework must
1354    /// skip the value-publication epilogue (UDF clear / timestamp / monitor /
1355    /// FLNK). C parity `compressRecord.c:365` `if (status != 1)`: a compress
1356    /// record still accumulating toward its next compressed sample runs none of
1357    /// `recGblGetTimeStamp` / `monitor` / `recGblFwdLink` on that cycle.
1358    CompleteNoEmit,
1359    /// Ran the value-publication epilogue NOW (UDF clear / timestamp / monitor —
1360    /// VAL and the alarm fields are posted this cycle), but the OUTPUT side (OUT
1361    /// link write / OEVT / forward link) is deferred to a scheduled
1362    /// reprocess, with PACT held across the wait. C parity `swaitRecord.c::process`
1363    /// (lines 425-481): when `schedOutput` arms the ODLY watchdog it sets
1364    /// `async=TRUE`, so `process` still runs `monitor()` (line 475) — posting the
1365    /// value side at the START of the delay — but skips the `if(!async)
1366    /// {recGblFwdLink; pact=FALSE;}` tail; the deferred `execOutput` (watchdog,
1367    /// at delay-END) does the OUT write + OEVT + forward link and posts no
1368    /// monitors. Unlike the calcout/scalcout/acalcout family, whose C `process`
1369    /// `return`s BEFORE `monitor()` (calcoutRecord.c:282, only `dlya` posted), so
1370    /// they defer the value side too and use `AsyncPendingNotify`. The deferral
1371    /// must carry a [`ProcessAction::ReprocessAfter`] — that scheduled reprocess
1372    /// is the continuation that releases the held PACT (same by-construction
1373    /// invariant as the `AsyncPendingNotify` ODLY defer).
1374    CompleteDeferOutput,
1375    /// Completed synchronously (PACT cleared), and the framework runs the ALARM
1376    /// epilogue ONLY: the UDF update, `check_alarms`, `recGblResetAlarms`
1377    /// (committing SEVR/STAT/AMSG and posting those fields with their C masks)
1378    /// and the timestamp. The VALUE side is skipped entirely — no `monitor()`
1379    /// value posts (so the last-posted trackers stay put and the next publishing
1380    /// cycle re-detects the change, exactly as C leaves `LA..LP` un-updated), no
1381    /// OUT / OEVT write, no process actions, no forward link.
1382    ///
1383    /// C parity `transformRecord.c:554-560`: an INVALID input severity with
1384    /// `IVLA == transformIVLA_DO_NOTHING` makes `process()` run
1385    /// `recGblGetTimeStamp` + `checkAlarms` + `recGblResetAlarms`, clear `pact`
1386    /// and `return` — skipping the calc loop, all 16 OUTx `dbPutLink` writes,
1387    /// `monitor()` and `recGblFwdLink()`.
1388    ///
1389    /// Distinct from [`RecordProcessResult::CompleteNoEmit`], which skips the
1390    /// alarm commit and the timestamp too (C `compressRecord.c:365` returns
1391    /// before `checkAlarms`).
1392    CompleteAlarmOnly,
1393}
1394
1395/// Complete outcome of a record's process() call.
1396///
1397/// Contains the processing result (Complete, AsyncPending, etc.) and a list
1398/// of side-effect actions for the framework to execute.
1399#[derive(Clone, Debug)]
1400pub struct ProcessOutcome {
1401    pub result: RecordProcessResult,
1402    pub actions: Vec<ProcessAction>,
1403    /// Set by the framework when device support's read() returned
1404    /// `did_compute: true`. The record's process() can check this to
1405    /// skip its built-in computation (e.g., PID). Replaces the `pid_done`
1406    /// flag pattern.
1407    pub device_did_compute: bool,
1408    /// Field stores this cycle owes the framework, to be applied only AFTER
1409    /// the cycle's queued [`ProcessAction::WriteDbLink`] have executed.
1410    ///
1411    /// C runs a record's `dbPutLink` calls and the completion-flag clear that
1412    /// follows them inside ONE `dbScanLock` — `sseqRecord.c::processCallback`
1413    /// puts `LNKn` (`:714-792`) and `asyncFinish` clears `busy` (`:498-505`)
1414    /// with the lock held throughout, and `scalerRecord.c::process` clears
1415    /// `cnt` (`:370`) and puts `COUT`/`COUTP` (`:457`, `:463`) in one cycle —
1416    /// so `dbGetField`, which takes the same lock, can never observe the flag
1417    /// clear ahead of the writes. The port cannot hold the record's data guard
1418    /// across the writes (a self/cyclic OUT link would dead-lock the
1419    /// non-reentrant guard), so a `process()` that both queues a write and
1420    /// clears its flag exposes a state C cannot produce: `BUSY == 0` with
1421    /// `LNKn` unwritten, `CNT == 0` with `COUT` unwritten.
1422    ///
1423    /// A record therefore does not store such a field in `process()`; it emits
1424    /// it here, and the framework applies it — store plus a `DBE_VALUE` monitor
1425    /// post — immediately after this cycle's link writes and before the
1426    /// cycle's snapshot notification. One owner for one transition.
1427    ///
1428    /// Applied in order, so a cycle that raises and clears the same field
1429    /// (`sseqRecord.c:302-305` `busy = 1` then an invalid `SELN` reaching
1430    /// `asyncFinish`) publishes both transitions exactly as C does.
1431    ///
1432    /// Only the STORE moves. A flag whose value is an input to a later
1433    /// decision in the same cycle keeps its `process()` store: motor's `DMOV`
1434    /// gates `recGblFwdLink` at `motorRecord.cc:1509-1510`, so it is read after
1435    /// it is set and does not belong here.
1436    pub post_write_fields: Vec<(String, EpicsValue)>,
1437}
1438
1439impl ProcessOutcome {
1440    /// Shorthand for a simple Complete with no actions.
1441    pub fn complete() -> Self {
1442        Self {
1443            result: RecordProcessResult::Complete,
1444            actions: Vec::new(),
1445            device_did_compute: false,
1446            post_write_fields: Vec::new(),
1447        }
1448    }
1449
1450    /// Shorthand for Complete with actions.
1451    pub fn complete_with(actions: Vec<ProcessAction>) -> Self {
1452        Self {
1453            result: RecordProcessResult::Complete,
1454            actions,
1455            device_did_compute: false,
1456            post_write_fields: Vec::new(),
1457        }
1458    }
1459
1460    /// Completed synchronously, but no new value was emitted this cycle, so
1461    /// the framework skips the value-publication epilogue (UDF clear /
1462    /// timestamp / monitor / FLNK). See `RecordProcessResult::CompleteNoEmit`.
1463    pub fn complete_no_emit() -> Self {
1464        Self {
1465            result: RecordProcessResult::CompleteNoEmit,
1466            actions: Vec::new(),
1467            device_did_compute: false,
1468            post_write_fields: Vec::new(),
1469        }
1470    }
1471
1472    /// Completed synchronously with the alarm epilogue only — no value posts,
1473    /// no output, no forward link. See `RecordProcessResult::CompleteAlarmOnly`.
1474    pub fn complete_alarm_only() -> Self {
1475        Self {
1476            result: RecordProcessResult::CompleteAlarmOnly,
1477            actions: Vec::new(),
1478            device_did_compute: false,
1479            post_write_fields: Vec::new(),
1480        }
1481    }
1482
1483    /// Shorthand for AsyncPending with no actions.
1484    pub fn async_pending() -> Self {
1485        Self {
1486            result: RecordProcessResult::AsyncPending,
1487            actions: Vec::new(),
1488            device_did_compute: false,
1489            post_write_fields: Vec::new(),
1490        }
1491    }
1492}
1493
1494impl Default for ProcessOutcome {
1495    fn default() -> Self {
1496        Self::complete()
1497    }
1498}
1499
1500/// Result of setting a common field, indicating what scan index updates are needed.
1501#[derive(Clone, Debug, PartialEq, Eq)]
1502pub enum CommonFieldPutResult {
1503    NoChange,
1504    ScanChanged {
1505        old_scan: ScanType,
1506        new_scan: ScanType,
1507        phas: i16,
1508    },
1509    PhasChanged {
1510        scan: ScanType,
1511        old_phas: i16,
1512        new_phas: i16,
1513    },
1514}
1515
1516/// Read-only snapshot of framework-owned `CommonFields` state that a
1517/// record's `process()` or device support's `read()` needs to see
1518/// *during* the processing cycle.
1519///
1520/// The framework owns `RecordInstance.common`; a record `process()`
1521/// receives only `&mut self` (the concrete record) and device support
1522/// `read()` receives only `&mut dyn Record`. Neither can reach
1523/// `CommonFields`. C records, by contrast, see `dbCommon` directly —
1524/// e.g. `epidRecord.c:195` reads `pepid->udf`, `timestampRecord.c:90`
1525/// reads `ptimestamp->tse`, `devTimeOfDay.c:122` reads `psi->phas`.
1526///
1527/// The framework builds a `ProcessContext` from `common` and pushes it
1528/// onto the record (via [`Record::set_process_context`]) and onto the
1529/// device support (via
1530/// [`crate::server::device_support::DeviceSupport::set_process_context`])
1531/// immediately before the respective call. This mirrors the existing
1532/// `set_device_did_compute` framework-set-hook pattern: additive,
1533/// no `process()` / `read()` signature change.
1534#[derive(Clone, Debug, PartialEq)]
1535pub struct ProcessContext<'a> {
1536    /// `dbCommon.udf` — value is undefined. C records check this at the
1537    /// top of `process()` (e.g. `epidRecord.c:195`).
1538    pub udf: bool,
1539    /// `dbCommon.udfs` — alarm severity raised for a UDF record.
1540    pub udfs: crate::server::record::AlarmSeverity,
1541    /// `dbCommon.nsev` — the *pending* (new) alarm severity this cycle has
1542    /// accumulated so far, BEFORE the record body runs. C `dbGetLink` folds an
1543    /// `MS`-class input link's severity into `nsev` at fetch time, so a record
1544    /// body that branches on the input severity reads it here — e.g.
1545    /// `transformRecord.c:554` `if ((ptran->nsev >= INVALID_ALARM) && (ptran->ivla
1546    /// == transformIVLA_DO_NOTHING))`. The framework folds every input-link alarm
1547    /// into `common.nsev` before building this snapshot, so `nsev` is the single
1548    /// source of truth; the record never re-derives it from the links.
1549    pub nsev: crate::server::record::AlarmSeverity,
1550    /// `dbCommon.phas` — phase. Used by device support for format
1551    /// selection (`devTimeOfDay.c:122`).
1552    pub phas: i16,
1553    /// `dbCommon.tse` — time-stamp event. `timestampRecord.c:90`
1554    /// branches on `tse == epicsTimeEventDeviceTime`.
1555    pub tse: i16,
1556    /// `dbCommon.time` — the record's current resolved time stamp at the
1557    /// start of this cycle (the previous cycle's stamp, or `UNIX_EPOCH`
1558    /// before the first process). Device support that has to format the
1559    /// record's time during `read()` — the std module's `devTimeOfDay.c`
1560    /// `recGblGetTimeStamp(psi)` call, which runs *before* the framework's
1561    /// per-cycle timestamp application — resolves the stamp with
1562    /// [`crate::server::recgbl::get_time_stamp`]`(tse, time)`. The `time`
1563    /// member is the device-provided value that helper returns verbatim on
1564    /// the `TSE == epicsTimeEventDeviceTime (-2)` branch.
1565    pub time: std::time::SystemTime,
1566    /// `dbCommon.dtyp` — device-support type name. A record's
1567    /// `process()` / pre-process hooks can branch on the DTYP to mirror
1568    /// C device support that lives in a separate DSET (e.g. the epid
1569    /// record's `devEpidSoftCallback` callback DSET drives the TRIG
1570    /// readback link, whereas `devEpidSoft` does not).
1571    ///
1572    /// Borrowed from `dbCommon`, not owned: the context is built under the
1573    /// record's own guard and handed straight to the hook, which is the whole
1574    /// of its life. Owning it cost a `String` per build — two per cycle, three
1575    /// for a record with device support — and the one reader
1576    /// (`EpidRecord::set_process_context`) copies into its own cell anyway.
1577    ///
1578    /// `dbCommon.tsel` used to sit beside it and was read by nobody, so it is
1579    /// not here; a hook that needs the TSEL text should take it the same way
1580    /// this one does.
1581    pub dtyp: &'a str,
1582    /// The callback band `dbCommon.prio` selects for work this cycle defers —
1583    /// C `seqRecord.c:145-146`, which re-runs
1584    /// `callbackSetPriority(prec->prio, &pcb->callback)` at the top of every
1585    /// `process()` so a `PRIO` written between cycles takes effect on the next
1586    /// one. Already converted: a record stashes this and hands it to
1587    /// [`crate::runtime::task::spawn_background`] rather than re-deriving a
1588    /// band from a raw menu index.
1589    pub callback_priority: crate::runtime::task::CallbackPriority,
1590}
1591
1592/// C `epicsTime.h`: `epicsTimeEventDeviceTime` — the `TSE` sentinel
1593/// meaning "device support provides the time stamp". `timestampRecord.c`
1594/// uses it to take the OS-clock branch instead of `recGblGetTimeStamp`.
1595pub const EPICS_TIME_EVENT_DEVICE_TIME: i16 = -2;
1596
1597/// One `db_post_events(prec, &field, mask)` a process cycle owes.
1598///
1599/// Every posted field carries its OWN `DBE_*` mask, mirroring C's per-field
1600/// call. One cycle posts different classes per field: a deadband-gated
1601/// readback narrows to the deadbands that actually crossed (MDEL → `DBE_VALUE`,
1602/// ADEL → `DBE_LOG`; motorRecord.cc `monitor()` 3476-3507, aiRecord.c
1603/// `monitor()`), while a change-detected auxiliary field posts
1604/// `DBE_VALUE | DBE_LOG` (motorRecord.cc 3522-3645 `DBE_VAL_LOG`;
1605/// calcRecord.c:420). A single record-wide mask collapses that granularity —
1606/// an archive-only deadband crossing would wrongly reach `DBE_VALUE`
1607/// subscribers whenever any other field changed in the same pass.
1608///
1609/// The name is `Cow` because most posts name a field the record type declared
1610/// — a `&'static str` already in the binary, which C posts as `&prec->val`, a
1611/// pointer. Owning it charged one malloc and one free per posted field per
1612/// cycle. Subscriber-driven posts, whose names come from the subscription map,
1613/// are the owned case.
1614pub type FieldPost = (
1615    std::borrow::Cow<'static, str>,
1616    EpicsValue,
1617    crate::server::recgbl::EventMask,
1618);
1619
1620/// Snapshot of changes from a process cycle, used for notify outside lock.
1621///
1622/// The first post is held inline and the rest spill to a `Vec`, the same shape
1623/// as [`ProcStack`](crate::server::database::ProcStack) and for the same
1624/// reason: the overwhelmingly common cycle posts exactly one field — the
1625/// record's deadband-tracked value, with nothing subscribed behind it — and a
1626/// `Vec` charged that cycle a 256-byte allocation and its free. It was the last
1627/// per-cycle allocation on the process path.
1628///
1629/// `head` and `rest` carry no meaning of their own: `head` is post 0 and `rest`
1630/// is posts 1.., so nothing may read them apart from [`Self::iter`].
1631#[derive(Default)]
1632pub struct ProcessSnapshot {
1633    head: Option<FieldPost>,
1634    rest: Vec<FieldPost>,
1635}
1636
1637impl ProcessSnapshot {
1638    /// A cycle that posted nothing.
1639    pub fn new() -> Self {
1640        Self::default()
1641    }
1642
1643    /// Add one post, after every post already added.
1644    pub fn push(&mut self, post: FieldPost) {
1645        match self.head {
1646            None => self.head = Some(post),
1647            Some(_) => self.rest.push(post),
1648        }
1649    }
1650
1651    /// Whether this cycle posted nothing at all.
1652    pub fn is_empty(&self) -> bool {
1653        self.head.is_none()
1654    }
1655
1656    /// How many fields this cycle posts.
1657    pub fn len(&self) -> usize {
1658        usize::from(self.head.is_some()) + self.rest.len()
1659    }
1660
1661    /// The cycle's posts, in the order they were added.
1662    pub fn iter(&self) -> impl Iterator<Item = &FieldPost> {
1663        self.head.iter().chain(self.rest.iter())
1664    }
1665
1666    /// Keep only the posts `keep` accepts; `keep` sees every post once, in
1667    /// the order they were added.
1668    pub fn retain(&mut self, mut keep: impl FnMut(&FieldPost) -> bool) {
1669        if self.head.as_ref().is_some_and(|post| !keep(post)) {
1670            self.head = None;
1671        }
1672        self.rest.retain(|post| keep(post));
1673        if self.head.is_none() && !self.rest.is_empty() {
1674            self.head = Some(self.rest.remove(0));
1675        }
1676    }
1677
1678    /// The union of every `DBE_*` class this cycle actually published — the
1679    /// port's answer to "what did `db_post_events` send for this record".
1680    ///
1681    /// A field carrying an empty mask is not posted at all
1682    /// (`RecordInstance::notify_from_snapshot` skips it), so an empty union
1683    /// means the cycle published nothing and no monitor of any class fired.
1684    pub fn published_mask(&self) -> crate::server::recgbl::EventMask {
1685        self.iter()
1686            .fold(crate::server::recgbl::EventMask::NONE, |acc, (_, _, m)| {
1687                acc | *m
1688            })
1689    }
1690}
1691
1692impl Extend<FieldPost> for ProcessSnapshot {
1693    fn extend<I: IntoIterator<Item = FieldPost>>(&mut self, iter: I) {
1694        for post in iter {
1695            self.push(post);
1696        }
1697    }
1698}
1699
1700impl FromIterator<FieldPost> for ProcessSnapshot {
1701    fn from_iter<I: IntoIterator<Item = FieldPost>>(iter: I) -> Self {
1702        let mut out = Self::new();
1703        out.extend(iter);
1704        out
1705    }
1706}
1707
1708/// What C's `fetch_values()` does when one of the record's input links fails
1709/// to read, and whether that failure gates the record body.
1710///
1711/// Every C record with an INPA..INPx block has a `fetch_values()` helper, but
1712/// they do not share a failure shape, so the framework cannot pick one rule
1713/// for all of them — each record declares its own via
1714/// [`Record::input_fetch_policy`].
1715///
1716/// The two dimensions C varies are "does the loop stop at the first failure"
1717/// and "does a failure gate the record body", and it uses three of the four
1718/// combinations. Whichever variant a record picks, the framework reduces the
1719/// cycle to ONE outcome — C's `fetch_values()` return status, zero or not —
1720/// and delivers it through a single owner: [`Record::set_fetch_gate_failed`]
1721/// for records that compute in their own `process()`, and
1722/// `RecordInstance::suppress_subroutine_run` for the two whose body is a
1723/// framework-dispatched subroutine (sub/aSub).
1724#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1725pub enum InputFetchPolicy {
1726    /// Read every configured link; a failed read neither stops the loop nor
1727    /// gates the record body. C `transformRecord.c::process` (534-547) reads
1728    /// on through a failed `dbGetLink` and computes anyway.
1729    #[default]
1730    ReadAll,
1731    /// Read every configured link — a failure does NOT stop the loop, so the
1732    /// inputs behind it still refresh — but the body is skipped this cycle.
1733    ///
1734    /// C `calcRecord.c::fetch_values` (427-443) keeps the FIRST failing status
1735    /// while looping to the end (`if (status == 0) status = newStatus;`), and
1736    /// `calcRecord.c::process` (120) runs `calcPerform` only
1737    /// `if (fetch_values(prec) == 0)` — so VAL and UDF freeze, no CALC_ALARM is
1738    /// raised, and everything after the calc (timestamp, alarms, monitors,
1739    /// forward link) still runs. `calcoutRecord.c` (694-709 fetch, 237 gate) is
1740    /// the same shape, and its OOPT decision then runs against the frozen VAL.
1741    ReadAllGateOnFailure,
1742    /// Stop at the FIRST failed link and skip the record body this cycle.
1743    ///
1744    /// C `subRecord.c::fetch_values` (407-418) `return -1`s on the first
1745    /// failing `dbGetLink`, so the inputs behind it are never read and keep
1746    /// their previous values; `subRecord.c::process` (144 fetch, 147 gate)
1747    /// then runs `do_sub` only `if (status == 0)`, freezing VAL/UDF and raising
1748    /// none of the subroutine's alarms. `aSubRecord.c` (277-289 fetch, 216-218
1749    /// process), `sCalcoutRecord.c` (885-887 fetch, 356 gate),
1750    /// `aCalcoutRecord.c` (1068-1071 fetch, 399 gate) and
1751    /// `swaitRecord.c` (686-705 fetch, 408 gate) are the same shape.
1752    AbortOnFirstFailure,
1753    /// Read every configured link and gate the body on the LAST link's status.
1754    ///
1755    /// C `selRecord.c::fetch_values` (434-437) assigns `status` unguarded on
1756    /// every pass of its all-inputs loop and returns it, so what
1757    /// `selRecord.c::process` (114-116) gates `do_sel` on is INPL's status
1758    /// alone — a failed INPA is read, posted, and then ignored. In
1759    /// `Specified` mode the loop is one link (`:421-432`), so the same rule
1760    /// reads as "the selected input's status". Faithful to C, quirk included:
1761    /// see `sel`'s doc for why the quirk is C's and not ours.
1762    ReadAllGateOnLastFailure,
1763}
1764
1765/// **The** field declaration of a record type, and the only way to obtain one.
1766///
1767/// Not implementable: the blanket `impl` below covers every [`Record`], so a
1768/// second `impl FieldDeclaration for MyRecord` is a coherence error. A record
1769/// type therefore cannot *supply* a field list — it can only be *asked* for
1770/// one, and the answer is resolved here:
1771///
1772/// * a record type **base's** vendored `.dbd` set covers is declared by the
1773///   table generated from that `.dbd` ([`crate::server::record::dbd_generated::record_fields`]);
1774/// * any other record type is asked for its own declaration,
1775///   [`Record::declared_fields`] — which for the downstream record types is the
1776///   table generated from the `.dbd` *their* crate vendors, and for a synthetic
1777///   record type (tests) is a hand-written table.
1778///
1779/// The two are mutually exclusive by construction, which is what closes the
1780/// invariant *one declaration per record type*. It used to be closed by luck:
1781/// both tables were live, `field_desc_of` merely happened to consult the
1782/// generated one first, and every consumer that reached for `field_list()`
1783/// directly (`dbpr`, the `dbpf` typo hint, `motor`'s field gate) read the
1784/// hand-written one — which is how `waveform.FTVL` was declared `DBF_SHORT`
1785/// with no menu while `waveformRecord.dbd` said `DBF_MENU`/`menu(menuFtype)`.
1786/// `field`'s offset into a contiguous argument block whose members are the
1787/// single letters `A`..`A+nargs-1` behind `prefix` — C's `get_linkNumber`
1788/// index test (`idx >= indexof(A) && idx < indexof(A) + NARGS`) written as a
1789/// letter test, which is the same set because the `.dbd` declares those
1790/// fields contiguously in letter order (`calcRecord.dbd.pod:792-980`).
1791///
1792/// `prefix` is `""` for the plain arg letters, `"L"` for calc's previous-value
1793/// twins `LA`..`LU`, `"VAL"` for aSub's output slots `VALA`..`VALU`.
1794pub fn arg_letter_offset(field: &str, prefix: &str, nargs: u8) -> Option<u8> {
1795    let rest = field.strip_prefix(prefix)?;
1796    let &[c] = rest.as_bytes() else { return None };
1797    if !c.is_ascii_uppercase() {
1798        return None;
1799    }
1800    let n = c - b'A';
1801    (n < nargs).then_some(n)
1802}
1803
1804/// The `nargs`-wide link field named by [`arg_letter_offset`]'s answer:
1805/// offset 0 behind `"INP"` is `"INPA"`. The one place the port turns a C
1806/// `&prec->inpa + linkNumber` pointer walk into a field name.
1807pub fn arg_link_field(prefix: &str, offset: u8) -> String {
1808    format!("{prefix}{}", (b'A' + offset) as char)
1809}
1810
1811/// C `CALCPERFORM_NARGS` (`postfix.h:29`) and `subRecord.c:89`'s
1812/// `INP_ARG_MAX` — the same 21, which is why calc, calcout and sub share one
1813/// arg-link mapping.
1814pub const CALC_CLASS_NARGS: u8 = 21;
1815
1816/// The calc-class `get_linkNumber` mapping (`calcRecord.c:161-167`,
1817/// `calcoutRecord.c:417-423`, `subRecord.c:198-204`): the arg letters
1818/// `A`..`U` AND their previous-value twins `LA`..`LU` both index
1819/// `&prec->inpa + n`, so both answer their metadata from `INPn`.
1820///
1821/// `sel` names its args the same way and is deliberately NOT routed here: its
1822/// rset lists them explicitly on HOPR/LOPR and calls `dbGetGraphicLimits`
1823/// nowhere.
1824pub fn calc_class_link_backed_metadata_field(field: &str) -> Option<String> {
1825    arg_letter_offset(field, "", CALC_CLASS_NARGS)
1826        .or_else(|| arg_letter_offset(field, "L", CALC_CLASS_NARGS))
1827        .map(|n| arg_link_field("INP", n))
1828}
1829
1830pub trait FieldDeclaration {
1831    /// The record type's field descriptors, in `.dbd` declaration order.
1832    fn field_list(&self) -> &'static [FieldDesc];
1833
1834    /// The record-own `DBF_NOACCESS` internal names the generator dropped
1835    /// from [`FieldDeclaration::field_list`] (`BPTR`, `RPVT`, ...): names
1836    /// C's `dbNameToAddr` resolves — so a SEARCH is answered — but whose
1837    /// channel is refused at creation. Same base-table-then-own resolution
1838    /// as `field_list`.
1839    fn noaccess_names(&self) -> &'static [&'static str];
1840
1841    /// The channel's native (maximum) element count for `field`, when it
1842    /// differs from the count of the field's current value — C's `cvt_dbaddr`
1843    /// `paddr->no_elements` against `get_array_info`'s current valid length, so
1844    /// a client's `ca_element_count` is the capacity even though a GET returns
1845    /// fewer elements.
1846    ///
1847    /// Only a `special(SPC_DBADDR)` field reaches `cvt_dbaddr` in C, and the
1848    /// `.dbd` is what says which fields those are. This reads that from
1849    /// [`FieldDeclaration::field_list`] and only then asks the record for the
1850    /// number ([`Record::dbaddr_capacity`]) — so the population comes from the
1851    /// one declaration every consumer already goes through, and a record cannot
1852    /// advertise a capacity for a field C would never have handed one.
1853    fn field_native_count(&self, field: &str) -> Option<u32>;
1854
1855    /// C `paddr->pfldDes->special == SPC_DBADDR` — the field's STATIC `.dbd`
1856    /// declaration, and the single owner of "is this an ARRAY destination".
1857    ///
1858    /// `dbPut` reads exactly this to pick its array arm
1859    /// (`dbAccess.c:1350`, tag `R7.0.10`:
1860    /// `if (nRequest>1 || paddr->pfldDes->special == SPC_DBADDR)`), and it is
1861    /// `pfldDes` — the shared field descriptor — NOT the per-`DBADDR`
1862    /// `paddr->special` that `cvt_dbaddr` is free to overwrite. The two differ:
1863    /// `lsiRecord.c:127-134` raises `paddr->special` to `SPC_MOD` on VAL and
1864    /// `SPC_NOMOD` on OVAL, and the branch still takes the array arm.
1865    ///
1866    /// That distinction is why this cannot be one lookup here either.
1867    /// [`FieldDesc::special`] carries the `cvt_dbaddr` special for a DBADDR
1868    /// field, not the declaration (`dbd/cvt_dbaddr.types` says so, and derives
1869    /// `read_only` from it) — which preserves `SPC_DBADDR` for every such field
1870    /// except the five long-string ones, whose `cvt_dbaddr` overwrites it. Those
1871    /// five are exactly [`Record::long_string_fields`] (`lsi`/`lso` VAL+OVAL,
1872    /// `printf` VAL), so the second half asks the record for them.
1873    fn field_is_dbaddr(&self, field: &str) -> bool;
1874}
1875
1876impl<R: Record + ?Sized> FieldDeclaration for R {
1877    fn field_list(&self) -> &'static [FieldDesc] {
1878        super::dbd_generated::record_fields(self.record_type())
1879            .unwrap_or_else(|| self.declared_fields())
1880    }
1881
1882    fn noaccess_names(&self) -> &'static [&'static str] {
1883        super::dbd_generated::record_noaccess_fields(self.record_type())
1884            .unwrap_or_else(|| self.declared_noaccess_fields())
1885    }
1886
1887    fn field_native_count(&self, field: &str) -> Option<u32> {
1888        if !self.field_is_dbaddr(field) {
1889            return None;
1890        }
1891        self.dbaddr_capacity(field)
1892    }
1893
1894    fn field_is_dbaddr(&self, field: &str) -> bool {
1895        self.field_list()
1896            .iter()
1897            .any(|d| d.name.eq_ignore_ascii_case(field) && d.special == Special::DbAddr)
1898            || self
1899                .long_string_fields()
1900                .iter()
1901                .any(|f| f.eq_ignore_ascii_case(field))
1902    }
1903}
1904
1905/// Trait that all EPICS record types must implement.
1906/// The per-cycle report of which input links produced a value — C's
1907/// `RTN_SUCCESS(dbGetLink(..))` per link, as [`Record::set_resolved_input_links`]
1908/// is handed it. It is the bits the multi-input fetch set over the record's
1909/// own [`Record::multi_input_links`] and the names the pre-process reads
1910/// resolved, so making it costs a cycle nothing and asking it is one scan
1911/// of a static list; no name list is built for the types that ignore it.
1912#[derive(Clone, Copy)]
1913pub struct ResolvedInputLinks<'a> {
1914    /// The record's `multi_input_links`, the slots `mask` covers.
1915    multi: &'static [(&'static str, &'static str)],
1916    mask: u64,
1917    /// The pre-process `ReadDbLink` fields that resolved.
1918    pre: &'a [&'static str],
1919}
1920
1921impl<'a> ResolvedInputLinks<'a> {
1922    pub(crate) fn new(
1923        multi: &'static [(&'static str, &'static str)],
1924        mask: u64,
1925        pre: &'a [&'static str],
1926    ) -> Self {
1927        ResolvedInputLinks { multi, mask, pre }
1928    }
1929
1930    /// A report of these link fields alone — for a test that plays the
1931    /// framework.
1932    pub fn of_names(names: &'a [&'static str]) -> Self {
1933        ResolvedInputLinks {
1934            multi: &[],
1935            mask: 0,
1936            pre: names,
1937        }
1938    }
1939
1940    /// Whether `link_field`'s read produced a value this cycle.
1941    pub fn contains(&self, link_field: &str) -> bool {
1942        self.multi_names().any(|name| name == link_field) || self.pre.contains(&link_field)
1943    }
1944
1945    /// Every link field whose read produced a value this cycle.
1946    pub fn names(&self) -> impl Iterator<Item = &'static str> + 'a {
1947        self.multi_names().chain(self.pre.iter().copied())
1948    }
1949
1950    fn multi_names(&self) -> impl Iterator<Item = &'static str> + 'a {
1951        let multi = self.multi;
1952        let mut mask = self.mask;
1953        std::iter::from_fn(move || {
1954            if mask == 0 {
1955                return None;
1956            }
1957            let slot = mask.trailing_zeros() as usize;
1958            mask &= mask - 1;
1959            Some(multi[slot].0)
1960        })
1961    }
1962}
1963
1964pub trait Record: Send + Sync + 'static {
1965    /// Return the record type name (e.g., "ai", "ao", "bi").
1966    fn record_type(&self) -> &'static str;
1967
1968    /// Process the record (scan/compute cycle).
1969    ///
1970    /// Returns a `ProcessOutcome` containing the processing result and any
1971    /// side-effect actions for the framework to execute.
1972    fn process(&mut self) -> CaResult<ProcessOutcome> {
1973        Ok(ProcessOutcome::complete())
1974    }
1975
1976    /// Optional: report whether this record's last `process()` call
1977    /// mutated a metadata-class field (EGU/PREC/HOPR/LOPR/HLM/LLM/
1978    /// alarm limits / DRVH/DRVL / state strings).
1979    ///
1980    /// The framework checks this after every `process()` call and, if
1981    /// true, invalidates the record's metadata cache so the next
1982    /// snapshot rebuilds from the new values.
1983    ///
1984    /// Default: `false` — most records never touch metadata fields
1985    /// during processing. Override only when your record dynamically
1986    /// adjusts limits or unit strings (e.g., a motor that recomputes
1987    /// HLM/LLM after a hardware homing operation).
1988    ///
1989    /// Implementations should reset their internal flag after returning
1990    /// `true` so the next cycle starts clean.
1991    fn took_metadata_change(&mut self) -> bool {
1992        false
1993    }
1994
1995    /// Get a field value by name.
1996    fn get_field(&self, name: &str) -> Option<EpicsValue>;
1997
1998    /// Set a field value by name.
1999    fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()>;
2000
2001    /// The field declaration of a record type **base's** `.dbd` set does not
2002    /// cover — a record type that lives in another crate.
2003    ///
2004    /// C has exactly one declaration per record type: the `.dbd`, read at
2005    /// runtime. The port compiles the vendored `.dbd`s into a generated table,
2006    /// and [`FieldDeclaration::field_list`] — the single resolver every consumer
2007    /// goes through — serves base's
2008    /// [`dbd_generated`](crate::server::record::dbd_generated) table for every
2009    /// record type IT covers and **never falls through to here**. So for a
2010    /// base record type this method is unreachable: it cannot declare one of its
2011    /// fields a second time, whatever it writes here.
2012    ///
2013    /// A record type outside base declares itself here, and the answer is still
2014    /// its `.dbd`: the downstream Tier-3 record types (`motor`, `table`,
2015    /// `scaler`, `epid`, `throttle`, `timestamp`) vendor their upstream `.dbd`
2016    /// into their OWN crate, `tools/dbd-codegen` generates a table into that
2017    /// crate (`tools/dbd-codegen/src/targets.rs`), and this method returns it.
2018    /// Each such crate carries the same ratchet base does
2019    /// (`one_declaration_per_record_type`): the table returned here must BE the
2020    /// generated one, so the `.dbd` stays the single declaration across a crate
2021    /// boundary the generator's output cannot cross on its own.
2022    ///
2023    /// A hand-written table is what is left when a record type has no `.dbd`
2024    /// anywhere — the synthetic record types the tests define. There are no
2025    /// others; a shipped record type has a `.dbd`, and its declaration is that
2026    /// `.dbd`.
2027    ///
2028    /// The declaration is a *spec*: it says what each field's type, menu and
2029    /// `special(SPC_NOMOD)` are. It does NOT say who implements the field. Ask
2030    /// [`Record::implements_field`] for that — see its docs for why the two
2031    /// must not be the same question.
2032    fn declared_fields(&self) -> &'static [FieldDesc] {
2033        &[]
2034    }
2035
2036    /// The analogue of [`Record::declared_fields`] for
2037    /// [`FieldDeclaration::noaccess_names`]: the record-own `DBF_NOACCESS`
2038    /// internal names of a record type no base `.dbd` declares. A downstream
2039    /// crate whose generated table reports dropped internals returns its
2040    /// `record_noaccess_fields` entry here, next to its `declared_fields`.
2041    fn declared_noaccess_fields(&self) -> &'static [&'static str] {
2042        &[]
2043    }
2044
2045    /// Does this record type implement `name` in its own `get_field` /
2046    /// `put_field`, as opposed to leaving it to the framework's dbCommon
2047    /// handling?
2048    ///
2049    /// This used to be answered by `field_list()` membership, which conflated
2050    /// two questions: "what is this field?" and "who owns it?". They give
2051    /// different answers for `INP`/`OUT`: every record type *declares* them in
2052    /// the `.dbd`, but only some drive the link themselves
2053    /// (`multi_output_links` for `acalcout`/`scalcout`, device support for
2054    /// `motorRecord`/`scalerRecord`); for the rest the framework arms
2055    /// `parsed_inp`/`parsed_out` and drives it. While `field_list()` was
2056    /// hand-written and incomplete the conflation was invisible, because the
2057    /// hand-written tables happened to omit exactly the fields the framework
2058    /// owns. A complete, spec-derived `field_list()` makes membership true for
2059    /// every record, so ownership needs its own predicate or the framework
2060    /// would stop arming any link at all.
2061    ///
2062    /// The default answers it truthfully — a record implements the fields its
2063    /// own `get_field` can produce. (Verified equivalent to the old
2064    /// `field_list()` membership on all 1,757 fields of all 40 record types at
2065    /// the time of the split, so the split changed no behaviour.)
2066    fn implements_field(&self, name: &str) -> bool {
2067        self.get_field(name).is_some()
2068    }
2069
2070    /// `SPC_NOMOD` that a record's `cvt_dbaddr` decides **at runtime, from
2071    /// record state** — the dynamic half of the no-modify declaration.
2072    ///
2073    /// [`FieldDesc::read_only`] is the static half: it carries the `.dbd`
2074    /// `special(SPC_NOMOD)` of a field that is immutable for the record type,
2075    /// full stop. But C lets a record's `cvt_dbaddr` *raise* SPC_NOMOD per
2076    /// dbAddr, keyed on the record's own fields, and one record does:
2077    ///
2078    /// ```c
2079    /// /* compressRecord.c:395-407 */
2080    /// static long cvt_dbaddr(DBADDR *paddr) {
2081    ///     ...
2082    ///     if (prec->balg == bufferingALG_LIFO)
2083    ///         paddr->special = SPC_NOMOD;
2084    /// }
2085    /// ```
2086    ///
2087    /// A compress VAL is writable under BALG=FIFO and refused under BALG=LIFO —
2088    /// a per-record-state fact no static `FieldDesc` can express. The one gate
2089    /// that owns field immutability (`field_io::check_no_mod`) consults this
2090    /// hook alongside the static set, so the dynamic refusal reaches EVERY put
2091    /// route exactly as the static one does.
2092    ///
2093    /// (C caches `paddr->special` in the DBADDR at name-resolution time, so a
2094    /// CA channel opened while FIFO keeps writing after a switch to LIFO until
2095    /// it reconnects; `dbpf`, which resolves fresh, is refused immediately. The
2096    /// port evaluates live on every put — the invariant C's own `dbpf` path
2097    /// shows, without the stale-cache hole.)
2098    ///
2099    /// `field` is upper-case. Default: no dynamic NOMOD.
2100    fn field_no_mod(&self, _field: &str) -> bool {
2101        false
2102    }
2103
2104    /// Choice strings for a record-specific `DBF_MENU` field served as
2105    /// `DBR_ENUM`, keyed by field name (uppercase, as declared).
2106    ///
2107    /// EPICS dbStaticLib serves a `DBF_MENU` field as `DBR_ENUM`: the value
2108    /// is the menu index and the field carries its `menu()` choice strings,
2109    /// so `caget`/`pvget` present the labels rather than a bare number
2110    /// (`dbStaticLib.c` `dbGetMenuChoices`; `dbAccess.c` `get_enum_str`).
2111    /// A record returns the label table (in index order) for each field it
2112    /// serves as [`DbFieldType::Enum`] from a `menu()`; the framework
2113    /// attaches it to the field snapshot's `EnumInfo` so the CA/PVA enum
2114    /// encoders present the labels — the same mechanism `bi`/`bo`/`mbbi`/
2115    /// `mbbo` already use for their `VAL` state strings, but per field
2116    /// rather than per record (a record can carry several distinct menus).
2117    ///
2118    /// This is the single owner of "menu field -> choice table": a record
2119    /// declares its menu fields here once, and `get_field` returns the menu
2120    /// index as [`EpicsValue::Enum`]. Default: no record-specific menu
2121    /// fields. The dbCommon menu fields (`SCAN`, etc.) are handled
2122    /// separately by the framework, not here.
2123    ///
2124    /// INVARIANT — answering here is a claim that the field is `DBF_MENU`, so
2125    /// [`FieldDeclaration::field_list`] MUST declare it [`DbFieldType::Enum`]: C's
2126    /// `mapDBFToDBR` serves every `DBF_MENU` as `DBR_ENUM`, and the DECLARED
2127    /// type is what goes on the wire
2128    /// ([`RecordInstance::project_to_declared_type`](super::RecordInstance::project_to_declared_type)).
2129    /// A field with choices but a `Short` declaration is a self-contradictory
2130    /// declaration: it would be served as a bare `DBR_SHORT` index while
2131    /// claiming to have labels for it. `menu_choices_are_served_as_dbr_enum`
2132    /// (`tests/menu_fields_serve_enum_choices.rs`) fails on any record type
2133    /// that breaks it — the storage may be a short, the declaration may not.
2134    fn menu_field_choices(&self, _field: &str) -> Option<&'static [&'static str]> {
2135        None
2136    }
2137
2138    /// Per-field override of the record-level display/control metadata
2139    /// for a GET / monitor snapshot of `field`.
2140    ///
2141    /// C record support serves metadata PER FIELD: the RSET functions
2142    /// `get_units` / `get_precision` / `get_graphic_double` /
2143    /// `get_control_double` / `get_alarm_double` all key on
2144    /// `dbGetFieldIndex(paddr)` and fall back to the `recGbl*` defaults
2145    /// for unlisted fields. The framework's metadata cache is per
2146    /// record (built by `populate_display_info` /
2147    /// `populate_control_info` from the VAL-class fields); a record
2148    /// whose RSET serves different metadata for non-VAL fields
2149    /// overrides this hook to patch the cached values for that field
2150    /// (e.g. the motor record: VELO's display range is VMAX/VBAS, not
2151    /// HLM/LLM — `motorRecord.cc:3247-3250`).
2152    ///
2153    /// Applied on both the GET path (`snapshot_for_field`) and the
2154    /// monitor path (`make_monitor_snapshot`), AFTER the cached
2155    /// record-level metadata — and computed live on each call, so an
2156    /// override derived from non-cached fields can never go stale.
2157    /// `field` is uppercase, as declared in [`FieldDeclaration::field_list`].
2158    /// Default: `None` — record-level metadata serves every field.
2159    fn field_metadata_override(&self, _field: &str) -> Option<FieldMetadataOverride> {
2160        None
2161    }
2162
2163    /// Which of THIS record's own link fields C's rset reads `field`'s
2164    /// `get_units` / `get_precision` / `get_graphic_double` /
2165    /// `get_alarm_double` from — C's `get_linkNumber` (`calcRecord.c:161-167`,
2166    /// `calcoutRecord.c:417-423`, `subRecord.c:198-204`), `get_dol`
2167    /// (`seqRecord.c:279-280`) and aSub's `get_inlinkNumber` /
2168    /// `get_outlinkNumber` pair (`aSubRecord.c:294-304`).
2169    ///
2170    /// Twenty-four call sites across five record types answer four of the six
2171    /// metadata slots from a LINK rather than from the record — the target's
2172    /// EGU, PREC, HOPR/LOPR and alarm ladder, not the source's. Control is
2173    /// deliberately absent: `dbGetControlLimits` has no caller in all of base,
2174    /// and `aSubRecord.c:372-376` is the type specimen calling
2175    /// `recGblGetControlDouble` with no link branch.
2176    ///
2177    /// The record type owns this answer for the same reason it owns
2178    /// [`Self::property_support`]: it is a transcription of the record's own C
2179    /// rset, and a central table keyed on the record-type *string* silently
2180    /// answers "no link" for every type nobody remembered to add. `aSub` was
2181    /// exactly that omission — its eight sites are the largest group of the
2182    /// twenty-four and it alone routes OUT links, a shape a
2183    /// `match rtype { "calc" | "calcout" | "sub" => ... }` cannot express at
2184    /// all.
2185    ///
2186    /// Returns the link field's name as declared (`"INPA"`, `"OUTC"`,
2187    /// `"DOL3"`), uppercase. Default: `None` — the record answers every field
2188    /// from itself.
2189    fn link_backed_metadata_field(&self, _field: &str) -> Option<String> {
2190        None
2191    }
2192
2193    /// Which of C's six nullable `rset` `get_*` property slots THIS record
2194    /// type implements — the record's own `#define get_xxx NULL` lines.
2195    ///
2196    /// `dbGet` consults the rset to *narrow* the caller's options mask
2197    /// (`dbAccess.c:336-427`): a NULL slot clears the option bit, so the leaf
2198    /// never reaches the client. That is what decides whether QSRV marks an NT
2199    /// leaf at all (pvxs `ioc/iocsource.cpp:263-305`). A slot counts as
2200    /// supplied when the C function pointer is non-NULL **even if the function
2201    /// writes nothing for the field in question** — C leaves the option bit
2202    /// set either way (`boRecord.c:294-299` writes units only for `HIGH`, yet
2203    /// `DBR_UNITS` survives for every `bo` field).
2204    ///
2205    /// The record type owns this answer because the record type owns its C
2206    /// rset. A central table keyed on the record-type *string* cannot: a
2207    /// record implemented in a downstream crate (`asyn-rs`'s `asynRecord`,
2208    /// the motor/scaler/optics types) has no way to add a row to it, so it
2209    /// silently inherited a default that marked every leaf it could name —
2210    /// telling clients a fabricated `display.units` of `""` and
2211    /// `valueAlarm` bands of zero were authoritative.
2212    ///
2213    /// The default answers from `default_property_support`, the
2214    /// transcription of the record types epics-base-rs implements itself.
2215    /// Override it in the record's own file, citing the C rset lines.
2216    fn property_support(&self) -> crate::server::snapshot::PropertySupport {
2217        default_property_support(self.record_type())
2218    }
2219
2220    /// Field names this record serves as a *long string*: a `DBF_CHAR`
2221    /// array field that semantically holds a NUL-terminated string.
2222    ///
2223    /// In EPICS such a field is declared `DBF_NOACCESS` (or carries a `$`
2224    /// modifier) and is accessed through a `DBR_CHAR` array view whose
2225    /// `form` is `"String"`; pvxs maps that view to a scalar `pvString`
2226    /// rather than an `int8[]` (`ioc/channel.cpp:58-68`,
2227    /// `ioc/iocsource.cpp:619-643`). QSRV uses this list to serve those
2228    /// fields as scalar-string NTScalar values instead of byte scalars.
2229    ///
2230    /// The record keeps its `CharArray` storage; the QSRV boundary does
2231    /// the `CharArray <-> String` conversion. Default empty — only
2232    /// long-string record types (`lsi`/`lso` VAL/OVAL, `printf` VAL)
2233    /// override this. Names are matched case-insensitively.
2234    fn long_string_fields(&self) -> &'static [&'static str] {
2235        &[]
2236    }
2237
2238    /// Field names declared `pp(TRUE)` in this record type's DBD (empty if
2239    /// none, e.g. `event`/`histogram`, or if the type is unmodeled).
2240    ///
2241    /// Drives the `dbPutField` processing gate: C `dbAccess.c:1263`
2242    /// re-processes a record on a put only when the put field is `PROC` or it
2243    /// is `pp(TRUE)` **and** `SCAN == Passive`. The table is total and
2244    /// fail-safe — an unmodeled type returns `&[]` (and warns once), so its
2245    /// field puts never auto-process (only `PROC` does). The default consults
2246    /// the central DBD-sourced table keyed by [`Record::record_type`]; record
2247    /// types can override.
2248    fn process_passive_fields(&self) -> &'static [&'static str] {
2249        super::process_passive::pp_fields_for(self.record_type())
2250    }
2251
2252    /// Whether a put to `field` should reprocess this Passive record.
2253    ///
2254    /// The default is pure `pp(TRUE)` membership — the put gate's
2255    /// `field in process_passive_fields()` test. A record type overrides this
2256    /// when its C `special()` conditionally returns ERROR to suppress the
2257    /// reprocess for a `pp(TRUE)` field on certain values (e.g. motor STUP:
2258    /// only a `STUP == ON` put runs the status-update process; any other value
2259    /// is clamped to OFF and C returns ERROR so no process runs). Modeling that
2260    /// here keeps the suppression at the same gate as the pp test, with no
2261    /// per-put one-shot state — the post-clamp field value is deterministic.
2262    fn processes_after_put(&self, field: &str) -> bool {
2263        self.process_passive_fields()
2264            .iter()
2265            .any(|f| f.eq_ignore_ascii_case(field))
2266    }
2267
2268    /// The record's `DBF_ENUM` state strings — the C rset slot pair
2269    /// `get_enum_strs` / `put_enum_str`, which in C read the same fields and
2270    /// are therefore ONE table here.
2271    ///
2272    /// It is what a client reads as the `DBR_ENUM` choice list AND the set of
2273    /// names a `DBR_STRING` put to the record's `DBF_ENUM` `VAL` may name
2274    /// (`dbConvert.c::putStringEnum` → [`crate::server::record::resolve_enum_state_string`]). Taking
2275    /// both from one table is the point: a name the record advertises is a name
2276    /// a client may put, by construction — the two cannot drift.
2277    ///
2278    /// The table is already trimmed to C's `no_str`: `bi`/`bo`/`busy` drop
2279    /// `ONAM` when `ZNAM` is set and `ONAM` is empty (`boRecord.c:342-352`);
2280    /// `mbbi`/`mbbo` cut at the last non-empty state (`mbbiRecord.c:262-269`).
2281    ///
2282    /// `None` — the record type leaves both rset slots NULL. That is every
2283    /// record whose `VAL` is not `DBF_ENUM`, and `mbbiDirect`/`mbboDirect`
2284    /// (`mbbiDirectRecord.c:58` `#define put_enum_str NULL`), whose `VAL` is
2285    /// `DBF_LONG`. C then fails a `DBR_STRING` put with `S_db_noRSET`.
2286    fn enum_state_strings(&self) -> Option<Vec<PvString>> {
2287        None
2288    }
2289
2290    /// The record's `get_enum_str` rset slot — how a `DBR_STRING` READ of its
2291    /// `DBF_ENUM` `VAL` renders. A THIRD slot, distinct from the pair above:
2292    /// C's `get_enum_str` (singular) is not `get_enum_strs` (plural), and
2293    /// serving the read from the plural table is what made an undefined `mbbi`
2294    /// state come out as its index.
2295    ///
2296    /// The difference is the trimming. `get_enum_strs` reports `no_str`, so the
2297    /// label list stops at the last non-empty state; `get_enum_str` indexes the
2298    /// state array *untrimmed* (`mbbiRecord.c:246-250`: any `val <= 15` reads
2299    /// `zrst + val * sizeof(zrst)`, empty or not) and only an index past the
2300    /// array reaches the sentinel. Verified on the compiled C `softIoc`: an
2301    /// `mbbi` with `ZRST`/`ONST` set answers `caget -t` with `""` at `VAL=5` and
2302    /// `"Illegal Value"` at `VAL=20`.
2303    ///
2304    /// `None` — the record leaves the rset slot NULL (`#define get_enum_str
2305    /// NULL`), which is every record but `bi`/`bo`/`mbbi`/`mbbo`. A field on
2306    /// such a record renders from its menu instead; see
2307    /// `RecordInstance::enum_string_form_for`,
2308    /// the single owner that picks between the two.
2309    fn enum_string_form(&self) -> Option<crate::server::snapshot::EnumStringForm> {
2310        None
2311    }
2312
2313    /// Validate a put before it is applied. Return Err to reject.
2314    fn validate_put(&self, _field: &str, _value: &EpicsValue) -> CaResult<()> {
2315        Ok(())
2316    }
2317
2318    /// Hook called after a successful put_field.
2319    fn on_put(&mut self, _field: &str) {}
2320
2321    /// Whether a put to `field` names a subroutine that must resolve in the
2322    /// function registry — C `special(SPC_MOD)` → `registryFunctionFind`
2323    /// (`aSubRecord.c::special`, `subRecord.c::special`). C stores the name in
2324    /// `dbPut`, then `special(after)` looks it up and returns `S_db_BadSub`
2325    /// (→ rsrv `ECA_PUTFAIL`) when the name is non-empty and unregistered, so
2326    /// the client's write is REFUSED while the field keeps the value it was
2327    /// given. An empty name names no routine and is accepted.
2328    ///
2329    /// The record's `special()` has no database handle and so cannot reach the
2330    /// registry itself (the registry is the DB's single owner); this hook only
2331    /// says "a put to `field` is a subroutine name that must be resolved". The
2332    /// put owner — which holds the registry — extracts the name from the write,
2333    /// performs the lookup, and applies the `S_db_BadSub` refusal at the point
2334    /// C's `dbPut` returns the after-put `special()` status. Returns `false`
2335    /// for any field/mode C accepts without a lookup (a non-SNAM field, or an
2336    /// aSub in `LFLG=READ` where the name comes from the SUBL link at process
2337    /// time and a SNAM put is not validated).
2338    fn is_subroutine_name_field(&self, _field: &str) -> bool {
2339        false
2340    }
2341
2342    /// Primary field name (default "VAL"). Override for waveform etc.
2343    fn primary_field(&self) -> &'static str {
2344        "VAL"
2345    }
2346
2347    /// Whether a put to `field` directly defines the record — clears UDF
2348    /// exactly like a primary-value-field put. C `dbAccess.c::dbPut`
2349    /// (`:1409-1410`) clears `udf` synchronously only when the put target is
2350    /// `dbIsValueField` (i.e. `field == primary_field()`); this hook is where
2351    /// a record type says its own `special()` ALSO clears UDF for a
2352    /// non-value field, independent of `dbIsValueField`.
2353    ///
2354    /// `mbboDirect` is the case this exists for: `mbboDirectRecord.c::special`
2355    /// (`after==1`, B0..B1F, line 290) sets `prec->udf = FALSE` on a bit-field
2356    /// put — the bit write is a second value source alongside a VAL put and
2357    /// the closed-loop DOL fetch. Default: only the primary field.
2358    fn is_udf_defining_put(&self, field: &str) -> bool {
2359        field == self.primary_field()
2360    }
2361
2362    /// Get the primary value.
2363    fn val(&self) -> Option<EpicsValue> {
2364        self.get_field(self.primary_field())
2365    }
2366
2367    /// Set the primary value.
2368    ///
2369    /// Matches C EPICS `dbPut` behavior: if the value type doesn't match
2370    /// the field type, it is automatically coerced (e.g., Long→Double for
2371    /// ai, Long→Enum for bi/mbbi). This prevents silent failures when
2372    /// asyn device support provides Int32 values to Enum-typed records.
2373    fn set_val(&mut self, value: EpicsValue) -> CaResult<()> {
2374        // Soft-channel INP/DOL delivery into the record's value field is
2375        // internal delivery, so it takes the same single owner every other
2376        // link target takes — `put_field_internal`. It was a parallel path
2377        // (put_field, then a `TypeMismatch`-triggered `convert_to` off the
2378        // *current* value's type), which silently dropped a shape the typed
2379        // arm rejected and `convert_to` could not fix: an array source into a
2380        // scalar VAL stayed an array and never landed. C's link layer asks for
2381        // one element (`dbGetLink(..., nRequest = NULL)`), so a waveform INP
2382        // into an `ai.VAL` delivers `wf[0]`.
2383        let field = self.primary_field();
2384        self.put_field_internal(field, value)
2385    }
2386
2387    /// Whether this record's `INP` is read by DEVICE SUPPORT (a C `DSET`), as
2388    /// opposed to by the record body itself.
2389    ///
2390    /// The init-time load of a CONSTANT `INP` into the record's value field is
2391    /// soft device support's `init_record` (`devAiSoft.c`, `devLonginSoft.c`,
2392    /// … each call `recGblInitConstantLink(&prec->inp, …)`), so it exists only
2393    /// where a DSET exists. `compress` has no device support at all
2394    /// (`compressRecord.c` declares no `dset`; its `process` calls `dbGetLink`
2395    /// on `INP` itself), which is why C leaves a `field(INP,"5")` compress with
2396    /// an EMPTY circular buffer — the constant is loaded nowhere and delivers
2397    /// nothing at process (`dbConstGetValue`). Seeding it anyway put a phantom
2398    /// sample in the buffer before the first scan.
2399    fn input_read_by_device_support(&self) -> bool {
2400        true
2401    }
2402
2403    /// The rest of the soft INPUT device support's `init_record`, once the
2404    /// constant-INP load above has (or has not) landed.
2405    ///
2406    /// Most soft dsets are exactly `recGblInitConstantLink()` and stop there —
2407    /// a link they could not load leaves the record's own init state alone. The
2408    /// ARRAY dsets do not: `devWfSoft.c:39-51` runs `dbLoadLinkArray` on every
2409    /// waveform and sets `prec->nord = 0` when it fails (a real link, or none —
2410    /// `dbLoadLinkArray` has no `loadArray` lset outside a constant), which is
2411    /// why C serves `NORD = 0` on a `record(waveform,"X"){}` even though the
2412    /// record's own `init_record` seeded `nord = (nelm == 1)` a moment earlier.
2413    ///
2414    /// `loaded` is whether a constant INP reached the value field. Defaulted to
2415    /// a no-op: a dset that only seeds does not need this half.
2416    fn soft_input_dset_init(&mut self, loaded: bool) {
2417        let _ = loaded;
2418    }
2419
2420    /// The plain `DTYP="Soft Channel"` INPUT dset body — C's `devXxxSoft.c`
2421    /// (and `devXxxSoftCallback.c`) `read_xxx`, handed the outcome of its own
2422    /// `dbGetLink(&prec->inp, ...)` once per process cycle.
2423    ///
2424    /// `Some(value)` is C's `if (!status)` arm, `None` its failure arm. Most
2425    /// soft dsets store what the link delivered and do nothing on failure,
2426    /// which is this default. `ai`'s two do not: `devAiSoft.c:81-93` and
2427    /// `devAiSoftCallback.c:180-194` blend the reading into the previous `VAL`
2428    /// through `SMOO` and keep their own "a read has completed" state, which
2429    /// they clear on a failed read so the next good one is unsmoothed.
2430    ///
2431    /// That filter belongs here and not in `process()`: `aiRecord.c:440-444`
2432    /// is the copy the RAW dset needs (`devAiSoftRaw::read_ai` returns 0, so
2433    /// `convert()` runs), while both soft dsets return 2 and `convert()` never
2434    /// runs for them at all.
2435    fn soft_input_read(&mut self, value: Option<EpicsValue>) -> CaResult<()> {
2436        match value {
2437            Some(value) => self.set_val(value),
2438            None => Ok(()),
2439        }
2440    }
2441
2442    /// The `DTYP="Raw Soft Channel"` INPUT dset — C's four `devXxxSoftRaw.c`
2443    /// read supports (`devAiSoftRaw`, `devBiSoftRaw`, `devMbbiSoftRaw`,
2444    /// `devMbbiDirectSoftRaw`).
2445    ///
2446    /// The value read from the INP link goes to **`RVAL`**, not `VAL`: the
2447    /// record's own `RVAL → VAL` convert then runs (that is the whole
2448    /// difference from `"Soft Channel"`, whose `read_xxx` returns 2 and writes
2449    /// `VAL` directly).
2450    ///
2451    /// **`Some`/`None` IS the dset table.** A record type that implements this
2452    /// is one for which C ships a SoftRaw input dset; a record type that does
2453    /// not, C has no such dset for, so `DTYP="Raw Soft Channel"` on it is a
2454    /// configuration C rejects at iocInit. There is no separate boolean saying
2455    /// whether the record "accepts" raw input — that boolean existed, defaulted
2456    /// to `false`, had ONE override in the workspace, and silently sent the
2457    /// other three input records' raw values into `VAL`, where their own convert
2458    /// then overwrote them from an unseeded `RVAL=0` (R19-66). A capability
2459    /// answer that can disagree with the implementation is the bug.
2460    fn raw_soft_input(&mut self, entry: RawSoftEntry, value: EpicsValue) -> Option<CaResult<()>> {
2461        let _ = (entry, value);
2462        None
2463    }
2464
2465    /// The `DTYP="Raw Soft Channel"` OUTPUT dset — C's four `devXxxSoftRaw.c`
2466    /// write supports: the value `write_xxx()` puts to the OUT link.
2467    ///
2468    /// `devAoSoftRaw.c` / `devBoSoftRaw.c` put `RVAL` (`dbPutLink(&prec->out,
2469    /// DBR_LONG, &prec->rval, 1)`); `devMbboSoftRaw.c` /
2470    /// `devMbboDirectSoftRaw.c` put `RVAL & MASK` as `DBR_ULONG`. All four
2471    /// write the RAW word — never the engineering `OVAL` that
2472    /// [`Record::output_link_value`] (the `"Soft Channel"` dset) puts.
2473    ///
2474    /// Same rule as [`Record::raw_soft_input`]: `Some`/`None` IS the dset
2475    /// table. Before this hook existed, a `DTYP="Raw Soft Channel"` output
2476    /// record matched neither the soft-OUT arm (which tests for `"Soft
2477    /// Channel"`) nor the device arm (it has no device), so it wrote **nothing
2478    /// at all** to OUT.
2479    fn raw_soft_output_value(&self) -> Option<EpicsValue> {
2480        None
2481    }
2482
2483    /// Apply a raw device value read *back* from an output record's device
2484    /// support (the asyn init seed and driver readback callback), the output
2485    /// counterpart of an input record's raw path, where device support
2486    /// writes `RVAL` and the record's own conversion produces `VAL`
2487    /// (`device_support.rs:43-46`). An output record whose
2488    /// `convert()` is forward (engineering → raw) must invert it here — store
2489    /// the raw value into `RVAL` and compute the engineering `VAL` — because
2490    /// the framework's forward convert would otherwise recompute `RVAL` from
2491    /// the stale `VAL` and discard the readback (C `processAo`/`initAo` set
2492    /// `rval`/`val` directly, devAsynInt32.c:955-957/:973-994).
2493    ///
2494    /// Returns `true` when the record fully produced `VAL` from the raw value
2495    /// (the asyn store path then reports `computed` so the forward convert is
2496    /// skipped). The default returns `false`: records whose own `convert()` is
2497    /// already `raw → eng` (`ai`) or that need no conversion (`longout`,
2498    /// `mbbo`, whose `set_val` re-derives from the raw value) keep the legacy
2499    /// raw → `RVAL` / direct-`VAL` path.
2500    fn apply_raw_readback(&mut self, _raw: i32) -> bool {
2501        false
2502    }
2503
2504    /// Apply a float64 device value read *back* from an output record's asyn
2505    /// device support — the `asynFloat64` analogue of
2506    /// [`Record::apply_raw_readback`]. A float64 output (`ao`) whose device
2507    /// value carries an `ASLO`/`AOFF` linear scaling must seed the engineering
2508    /// `VAL` here (`VAL = value * ASLO + AOFF`), because the asyn store path
2509    /// would otherwise write the raw device value straight into `VAL` and drop
2510    /// the scaling. Sets `VAL` only (a float64 `ao` carries no `RVAL`); the
2511    /// reverse scaling `(OVAL - AOFF) / ASLO` is applied on the device-write
2512    /// side. Mirrors C `initAo`/`processAo` (devAsynFloat64.c:628-630/:647-649).
2513    ///
2514    /// Returns `true` when the record produced `VAL` from the raw value (the
2515    /// asyn store path then reports `computed`, skipping the forward convert).
2516    /// The default returns `false`: records with no float64 readback scaling
2517    /// keep the raw `set_val` path.
2518    fn apply_float64_readback(&mut self, _raw: f64) -> bool {
2519        false
2520    }
2521
2522    /// Hand the record the database's breakpoint-table registry so an `ai`/`ao`
2523    /// with `LINR >= 3` can resolve and cache the table its `LINR` selects.
2524    /// Called once at iocInit, before the first `process`/`convert`. The record
2525    /// resolves the table lazily on the first conversion (and re-resolves when
2526    /// `LINR` changes at runtime), mirroring C `cvtRawToEngBpt`'s
2527    /// `init || *ppbrk == NULL` cache. The default is a no-op: only `ai`/`ao`
2528    /// carry `LINR`.
2529    fn install_breaktable_registry(
2530        &mut self,
2531        _registry: std::sync::Arc<crate::server::cvt_bpt::BreakTableRegistry>,
2532    ) {
2533    }
2534
2535    /// Apply IVOA=2 ("set outputs to IVOV") semantics. C line numbers below
2536    /// resolve at epics-base `R7.0.10`; `busy` is module `busy` at
2537    /// `R1-7-4-6-g2dfe92d`.
2538    ///
2539    /// **The rule for every record that has a raw/staged output word is
2540    /// `VAL = IVOV`, then the record's OWN conversion — never `RVAL = IVOV`
2541    /// or `OVAL = IVOV`.** IVOV is a value in VAL's engineering units, so it
2542    /// has to travel the same VAL->raw path an ordinary cycle uses:
2543    ///
2544    /// - `ao` (`aoRecord.c:207-212`): `val = ivov; value = ivov;
2545    ///   convert(prec, value);` — OROC, linearisation and raw rounding all
2546    ///   run, so OVAL *and* RVAL are the converted IVOV.
2547    /// - `bo` (`boRecord.c:230-238`), `busy` (`busyRecord.c:235-243`):
2548    ///   `val = ivov;` then the inline `/* convert val to rval */` MASK rule.
2549    /// - `mbbo` (`mbboRecord.c:232-236`), `mbboDirect`
2550    ///   (`mbboDirectRecord.c:210-214`): `val = ivov; convert(prec);` — the
2551    ///   ZRVL..FFVL state-value lookup (mbbo only) and the SHFT shift.
2552    ///
2553    /// Records with no raw word assign directly and there is nothing to
2554    /// convert:
2555    ///
2556    /// - `calcout` (`calcoutRecord.c:646-647`), `scalcout`, `acalcout`:
2557    ///   `oval = ivov` — and inside the `doOutput`-gated `execOutput`, so a
2558    ///   non-output INVALID cycle must not touch OVAL at all. VAL is the calc
2559    ///   result, not the output.
2560    /// - `lso` (`lsoRecord.c:131-138`): `strncpy(val, ivov, sizv-1)` and
2561    ///   `len = strlen(val)+1`. OVAL is `monitor()`'s tracker here, not
2562    ///   output staging, and seeding it suppresses the write's own post.
2563    /// - `dfanout` (`dfanoutRecord.c:137-139`): `val = ivov;` then
2564    ///   `push_values(prec)`.
2565    ///
2566    /// Default uses [`Record::set_val`], which is the `dfanout` shape and is
2567    /// correct for any record whose OUT path reads VAL only.
2568    fn apply_invalid_output_value(&mut self, ivov: EpicsValue) -> CaResult<()> {
2569        self.set_val(ivov)
2570    }
2571
2572    /// Whether this record type supports device write (output records only).
2573    /// `aao` is included here even though it's served by the same
2574    /// concrete struct as `waveform`/`aai`/`subArray` — the
2575    /// WaveformRecord's `can_device_write` override picks the right
2576    /// answer per [`crate::server::records::waveform::ArrayKind`], but this default matters for code that
2577    /// only has the record-type string.
2578    fn can_device_write(&self) -> bool {
2579        matches!(
2580            self.record_type(),
2581            "ao" | "bo"
2582                | "longout"
2583                | "int64out"
2584                | "mbbo"
2585                | "mbboDirect"
2586                | "stringout"
2587                | "lso"
2588                | "printf"
2589                | "aao"
2590        )
2591    }
2592
2593    /// Has this cycle REACHED the record's `recGblFwdLink` line?
2594    ///
2595    /// C `if (!pact && prec->pact) return(0)` — device support took the write
2596    /// asynchronously, so `process()` returns before the tail and the client's
2597    /// `ca_put_callback` is still owed. A record that returns
2598    /// `AsyncPendingNotify` answers `false` until its device round-trip is
2599    /// done. Default: true, the synchronous record that runs to its own tail.
2600    ///
2601    /// One of the two inputs to `complete_put_notify`; see
2602    /// [`Self::should_fire_forward_link`] for the other and for the invariant
2603    /// they share.
2604    fn is_put_complete(&self) -> bool {
2605        true
2606    }
2607
2608    /// Does this cycle TAKE the record's `recGblFwdLink` line, having reached
2609    /// it?
2610    ///
2611    /// # Invariant (CONTRACT)
2612    ///
2613    /// A cycle completes an outstanding put-notify if and only if it runs
2614    /// `recGblFwdLink`. C `recGbl.c:290-303` is why: `if (pdbc->ppn)
2615    /// dbNotifyCompletion(pdbc)` sits INSIDE the function a record type
2616    /// chooses whether to call, so declining the forward link withholds the
2617    /// client's `ca_put_callback` by construction. The two are one decision in
2618    /// C and must stay one here — mca's own source says so where it makes the
2619    /// choice: "Process forward-linked record. Tell EPICS dbPutNotify
2620    /// mechanism that processing is finished." (`mcaRecord.c:820-826`).
2621    ///
2622    /// The single owner of that decision is
2623    /// `database::processing::complete_put_notify`, which reads this method
2624    /// and [`Self::is_put_complete`] together. A record type states its C gate
2625    /// HERE and nowhere else; it must not carry a second, separately-drifting
2626    /// `is_put_complete` override to say the same thing.
2627    ///
2628    /// Six types gate the call, each transcribing one C line:
2629    ///
2630    /// - `busy` (`busyRecord.c:271`): `if ((prec->val == 0) || (prec->oval ==
2631    ///   0)) recGblFwdLink(prec);`
2632    /// - `motor` (`motorRecord.cc:1509`): `if (pmr->dmov != 0)`
2633    /// - `mca` (`mcaRecord.c:824`): `if (!pmca->acqg)`
2634    /// - `scaler` (`scalerRecord.c:475`): `if ((pscal->pcnt==0) && (pscal->us
2635    ///   == USER_STATE_IDLE))` inside the `ss == SCALER_STATE_IDLE` arm
2636    /// - `epid` (`epidRecord.c:201`): the UDF-gated `return(0)` above the
2637    ///   `:212` call
2638    /// - `throttle` (`throttleRecord.c:308`): the call is commented out in
2639    ///   `process()` and fired only from `valuePut`'s real-OUT-write branch
2640    ///   (`:582`)
2641    ///
2642    /// Default: true — the standard record, which calls it unconditionally.
2643    fn should_fire_forward_link(&self) -> bool {
2644        true
2645    }
2646
2647    /// C parity: a record whose completion restamps `TIME` AFTER the VAL
2648    /// monitor post and the forward link, not before the value post like
2649    /// every standard record (`aoRecord.c:190` stamps ahead of `writeValue`).
2650    ///
2651    /// Only sseq's `asyncFinish` (`sseqRecord.c`) has this ordering: it posts
2652    /// VAL at `:474`, runs `recGblFwdLink` at `:499`, and only then calls
2653    /// `recGblGetTimeStamp` at `:501`. So the first VAL monitor event carries
2654    /// the record's pre-update timestamp, and `TIME` advances only for the
2655    /// following BUSY post and the next cycle — the VAL timestamp always lags
2656    /// one completion behind. The framework's synchronous `Complete` tail
2657    /// consults this to skip the pre-output restamp and apply it after the
2658    /// forward-link tail instead. Default false: every other record (including
2659    /// the base `seq`, whose `seqRecord.c:224` restamps BEFORE the `:229` VAL
2660    /// post) stamps before the value post.
2661    fn restamps_time_after_completion(&self) -> bool {
2662        false
2663    }
2664
2665    /// Whether this record's OUT link should be written after processing.
2666    /// Defaults to true. Override in calcout / longout to implement OOPT
2667    /// conditional output (epics-base 7.0.8).
2668    fn should_output(&self) -> bool {
2669        true
2670    }
2671
2672    /// The epilogue of C's `conditional_write` — where the record advances
2673    /// the reference the NEXT cycle's [`Self::should_output`] compares
2674    /// against (`longout.pval`, and the `outpvt` first-cycle bit with it).
2675    ///
2676    /// `longoutRecord.c:489-493` puts `prec->pval = prec->val;
2677    /// prec->outpvt = DONT_EXEC_OUTPUT;` OUTSIDE `if (doDevSupWrite)`, so a
2678    /// cycle that OOPT suppressed still advances. That unconditionality is
2679    /// the whole transition mechanism: `Transition_To_Zero` fires on the
2680    /// cycle where `val` reaches 0 and `pval` does not, which can only
2681    /// happen if the earlier nonzero cycle — which wrote nothing — latched.
2682    ///
2683    /// The framework therefore calls this once per cycle that reaches
2684    /// `conditional_write`, not once per write: C skips it only where
2685    /// `writeValue` returns before the switch (SIMM simulation, a failed
2686    /// SIML read) or where IVOA vetoes the call outright. Default: no-op —
2687    /// `calcout`/`sCalcout`/`aCalcout`/`swait` latch inside their own
2688    /// `process()`, as their C does.
2689    fn after_output_decision(&mut self) {}
2690
2691    /// Whether [`Record::after_output_decision`] does anything for this type —
2692    /// whether its C `process()` has a `conditional_write` epilogue outside
2693    /// `if (doDevSupWrite)` at all. Answered so the cycle can settle it once
2694    /// (`ProcessPlan::redecides_after_output`) instead of taking the record's
2695    /// write lock every cycle to call an empty body; 42 of the 43 types have
2696    /// one. Override it in the same impl block as the epilogue — a type that
2697    /// forgets is caught by `a_type_that_redecides_after_output_says_so`.
2698    fn redecides_after_output(&self) -> bool {
2699        false
2700    }
2701
2702    /// Whether this record uses MDEL/ADEL deadband for monitor posting.
2703    /// Binary records (bi, bo, busy, mbbi, mbbo) return false because
2704    /// C EPICS always posts monitors for these record types regardless
2705    /// of whether the value changed.
2706    fn uses_monitor_deadband(&self) -> bool {
2707        true
2708    }
2709
2710    /// Whether this record's process cycle posts its primary value (`VAL`)
2711    /// as a value monitor (`DBE_VALUE` / `DBE_LOG`).
2712    ///
2713    /// Default `true`: for most records C `monitor()` posts `VAL` whenever the
2714    /// value moved (deadband, change-gate, or always).
2715    ///
2716    /// `false` for the "trigger" records `fanout` and `seq`. Their `VAL` is
2717    /// `field(VAL,DBF_LONG){ pp(TRUE) }` — "Used to trigger" — and their C
2718    /// `process()` posts `VAL` ONLY with the alarm events `recGblResetAlarms`
2719    /// returns: `if (events) db_post_events(prec, &prec->val, events)`
2720    /// (fanoutRecord.c:148-150, seqRecord.c:227-229), never `DBE_VALUE` /
2721    /// `DBE_LOG`. Writing `VAL` fans out the forward links / sequences the
2722    /// `DOn`→`LNKn` writes; the value itself is not a monitored quantity, so a
2723    /// run of `caput VAL` posts no per-put value event (only the initial
2724    /// subscription snapshot fires). The alarm bits still reach `VAL` through
2725    /// the deadband post's `alarm_bits`, so an alarm transition posts `VAL`
2726    /// with `DBE_ALARM` exactly as C's `if (events)` does.
2727    fn process_posts_value_monitor(&self) -> bool {
2728        true
2729    }
2730
2731    /// Per-record VALUE/LOG monitor gate for record types that post a
2732    /// monitor *only when the value actually changed* — and have no
2733    /// MDEL/ADEL deadband to express that.
2734    ///
2735    /// `Some(changed)` makes the framework post the VALUE and LOG
2736    /// monitors iff `changed`; `None` (the default) leaves the decision
2737    /// to the deadband / always-post path.
2738    ///
2739    /// C `lsiRecord.c`/`lsoRecord.c` `monitor()` raise `DBE_VALUE |
2740    /// DBE_LOG` only when `len != olen || memcmp(oval, val, len)`. Those
2741    /// records return [`Self::uses_monitor_deadband`]`== false`, which
2742    /// otherwise routes them to the unconditional always-post path
2743    /// (correct for binary records, wrong for lsi/lso).
2744    ///
2745    /// **THIS HOOK IS C's `monitor()`.** It takes `&mut self` because that is
2746    /// where C both compares and commits: `if (prec->mlst != prec->val) {
2747    /// events |= DBE_VALUE | DBE_LOG; prec->mlst = prec->val; }`
2748    /// (`boRecord.c:395-400`) and its `oval`/`olen` twin
2749    /// (`lsoRecord.c:248-252`). The implementation compares LIVE and commits
2750    /// its own previous-value tracker here — it must NOT capture a flag during
2751    /// `process()`.
2752    ///
2753    /// The distinction is not cosmetic. A record whose value can still change
2754    /// between `process()` returning and the monitors being posted — every
2755    /// record with an `IVOA = Set_output_to_IVOV` arm, since the port hoists
2756    /// that decision into the framework's single IVOA owner where C keeps it
2757    /// inside `process()` — reports a flag captured against the PRE-arm value
2758    /// if it captures early, so the post for the IVOV write lands a cycle late
2759    /// or, if the arm also seeds the tracker, never. Comparing here is the
2760    /// uniform rule that removes the ordering from each record's concern.
2761    ///
2762    /// Called exactly once per completed cycle, from
2763    /// `RecordInstance::value_include_classes`, which
2764    /// the three cycle owners — the sync epilogue, the async completion, and
2765    /// the SIMM tail — reach on mutually exclusive paths.
2766    fn monitor_value_changed(&mut self) -> Option<bool> {
2767        None
2768    }
2769
2770    /// `menuPost` "Always" override for the VALUE / LOG monitor masks.
2771    ///
2772    /// Returns `(post_value_always, post_archive_always)`. The framework
2773    /// ORs these into the change-gated mask from
2774    /// [`Self::monitor_value_changed`], so an *unchanged* process cycle
2775    /// still posts `DBE_VALUE` (resp. `DBE_LOG`) when the record's MPST
2776    /// (resp. APST) menu field is set to `Always`.
2777    ///
2778    /// C `lsiRecord.c`/`lsoRecord.c` `monitor()` compute the VAL post
2779    /// mask from three independent inputs:
2780    ///
2781    /// * the change test `len != olen || memcmp(oval, val, len)` →
2782    ///   `DBE_VALUE | DBE_LOG`,
2783    /// * `if (mpst == menuPost_Always) events |= DBE_VALUE;`,
2784    /// * `if (apst == menuPost_Always) events |= DBE_LOG;`.
2785    ///
2786    /// [`Self::monitor_value_changed`] carries the first input; this hook
2787    /// carries the other two. Records without a `menuPost` field keep the
2788    /// default `(false, false)`, which leaves the change gate unchanged.
2789    fn monitor_always_post(&self) -> (bool, bool) {
2790        (false, false)
2791    }
2792
2793    /// The value the MDEL/ADEL deadband is evaluated against.
2794    ///
2795    /// For most records C `monitor()` applies the value deadband to
2796    /// `VAL`, so the default is [`Self::val`]. A record whose monitored
2797    /// quantity is not its primary value must override this: the motor
2798    /// record, for instance, has `VAL` as the setpoint and applies
2799    /// MDEL/ADEL to `RBV` (the readback) — its C `monitor()` deadbands
2800    /// `RBV`, not `VAL`. Such a record returns its readback field here.
2801    ///
2802    /// In the deadband's own domain: C compares `double`s
2803    /// (`recGblCheckDeadband`), so a record that stores the cell as one hands
2804    /// it over without an `EpicsValue` round trip. `None` is a value the
2805    /// deadband cannot compare, which posts unconditionally.
2806    ///
2807    /// Default is `val()`, so existing records are unaffected.
2808    fn monitor_deadband_value(&self) -> Option<f64> {
2809        self.val().and_then(|v| v.to_f64())
2810    }
2811
2812    /// The FIELD whose VALUE/LOG monitor delivery the MDEL/ADEL
2813    /// deadband gates — the field [`Self::monitor_deadband_value`]
2814    /// reads. A record overriding one must override both consistently.
2815    ///
2816    /// For most records the deadband gates the primary value itself,
2817    /// so the default returns [`Self::primary_field`] and nothing
2818    /// changes. The motor record deadbands RBV: C `monitor()`
2819    /// (motorRecord.cc:3468-3507) throttles the RBV post with
2820    /// MDEL/ADEL, while VAL is posted only when an actual setpoint
2821    /// change marked it (M_VAL). When this returns a non-primary
2822    /// field, the framework's snapshot builders:
2823    ///
2824    /// * deliver THIS field on the deadband triggers (instead of raw
2825    ///   change-detection), and
2826    /// * route the primary field through generic change-detection, so
2827    ///   an unchanged setpoint is not re-posted on every readback
2828    ///   poll.
2829    fn monitor_deadband_field(&self) -> &'static str {
2830        self.primary_field()
2831    }
2832
2833    /// Fields the record's C `monitor()` posts on every cycle whose
2834    /// alarm transition fired, even when their value did not change.
2835    ///
2836    /// C motorRecord.cc `monitor()` (3456-3646) computes
2837    /// `local_mask = monitor_mask | (MARKED(x) ? DBE_VAL_LOG : 0)`
2838    /// for each field in its posting list — when the alarm moved
2839    /// (`monitor_mask != 0`), `local_mask` is non-zero for UNMARKED
2840    /// fields too, so every listed field posts with `DBE_ALARM` and a
2841    /// `DBE_ALARM`-only subscriber observes the alarm moment on any of
2842    /// them. The framework's change-detection loop posts a listed,
2843    /// subscribed, unchanged field with the cycle's alarm bits when
2844    /// this list names it.
2845    ///
2846    /// Default: empty — most C record types post only their value
2847    /// field(s) on an alarm transition (aiRecord.c `monitor()` posts
2848    /// VAL with `monitor_mask` and RVAL only when it changed), which
2849    /// the deadband-field post already covers.
2850    fn alarm_cycle_monitored_fields(&self) -> &'static [&'static str] {
2851        &[]
2852    }
2853
2854    /// Fields the record's C `monitor()` re-posts with `DBE_VAL_LOG` on
2855    /// every cycle that recomputed them, even when the value did not
2856    /// change — the analogue of an unconditional `MARK(field)` in C.
2857    ///
2858    /// Unlike [`Self::alarm_cycle_monitored_fields`] (which posts unchanged
2859    /// fields only on a cycle whose alarm transition fired), these post on
2860    /// any cycle the record names them, with `DBE_VALUE | DBE_LOG` (plus the
2861    /// cycle's alarm bits when one fired). The framework's change-detection
2862    /// loop posts a listed, subscribed, unchanged field with that mask.
2863    ///
2864    /// C motorRecord `process_motor_info` (motorRecord.cc:3764-3767)
2865    /// `MARK`s `M_DIFF`/`M_RDIF` unconditionally on every `CALLBACK_DATA`
2866    /// pass, and `monitor()` (3522-3531) posts them with `monitor_mask |
2867    /// DBE_VAL_LOG`; a `camonitor DIFF` on an axis parked at a constant
2868    /// non-zero following error thus gets an event every poll. The record
2869    /// returns the fields ONLY on the cycles it actually re-marked them (it
2870    /// reads its own per-cycle state), so a pass that did not recompute them
2871    /// does not over-post.
2872    ///
2873    /// Default: empty — most record types post a field only when it
2874    /// changed (or on an alarm transition), which the existing gates cover.
2875    fn force_posted_fields(&self) -> &'static [&'static str] {
2876        &[]
2877    }
2878
2879    /// Fields this cycle's C `monitor()` posted UNCONDITIONALLY, chosen per
2880    /// cycle from record state — the DYNAMIC sibling of
2881    /// [`Self::force_posted_fields`].
2882    ///
2883    /// Some records decide which fields to post from a per-cycle BIT MASK
2884    /// rather than from a fixed list. aCalcout has two of them, and neither
2885    /// consults the value: `afterCalc` posts exactly the AMASK-flagged array
2886    /// fields — the ones the expression STORED into
2887    /// (`aCalcoutRecord.c:294-298`) — and `monitor()` posts exactly the
2888    /// NEWM-flagged ones — the input arrays whose link delivered a CHANGED
2889    /// value (`:1031-1036`). `AA := AA` therefore still posts AA.
2890    ///
2891    /// Neither existing gate can express that. The change-detection loop posts
2892    /// only what moved, so it drops the store-the-same-value case;
2893    /// [`Self::force_posted_fields`] is `&'static`, so it cannot name a set
2894    /// that varies per cycle (twelve arrays, 2^12 combinations) without
2895    /// over-posting every one of them every cycle.
2896    ///
2897    /// Each entry is ONE C `db_post_events` call, with the mask THAT call site
2898    /// uses ([`CyclePostMask`]) — not one entry per field. The two aCalcout call
2899    /// sites disagree on the mask (`afterCalc` posts a literal `DBE_VALUE|
2900    /// DBE_LOG`, `monitor()` posts `monitor_mask|DBE_VALUE|DBE_LOG`), and an
2901    /// array in BOTH masks is posted TWICE by C, once from each. So a field may
2902    /// legitimately appear twice, and the framework emits an event per entry.
2903    ///
2904    /// TAKE semantics: called exactly once per process cycle, and the record
2905    /// clears whatever state it answered from — C's `pcalc->newm = 0` (`:1036`)
2906    /// is part of the same step.
2907    ///
2908    /// Default: empty — and `Vec::new()` does not allocate.
2909    fn take_cycle_posted_fields(&mut self) -> Vec<(&'static str, CyclePostMask)> {
2910        Vec::new()
2911    }
2912
2913    /// Fields this record posts on its FIRST monitor cycle whether or not they
2914    /// changed — C's `|| (prpvt->firstCalcPosted == 0)` term
2915    /// (`transformRecord.c:798`), which `monitor()` disarms unconditionally
2916    /// afterwards (`:807`) so it fires once per IOC lifetime.
2917    ///
2918    /// Deliberately NOT [`Self::take_cycle_posted_fields`], which carries marks
2919    /// the RUNNING cycle made and which iocInit drops wholesale
2920    /// (`seed_record_after_init`, so a seed put's mark cannot become a late
2921    /// event). This flag is initial record state, made by no put, so it has to
2922    /// survive the seed — putting it on the per-cycle channel would let the
2923    /// init drain consume it and the first cycle would post nothing.
2924    ///
2925    /// Same TAKE semantics and same masks: called once per process cycle, and
2926    /// the record clears what it answered from.
2927    ///
2928    /// Default: empty.
2929    fn take_first_monitor_cycle(&mut self) -> Vec<(&'static str, CyclePostMask)> {
2930        Vec::new()
2931    }
2932
2933    /// Fields whose ONLY post path is the record's own per-cycle mark — C never
2934    /// change-detects them, so a value change alone must post nothing.
2935    ///
2936    /// aCalcout's arrays AA..LL are the case. C's `monitor()` compares scalar
2937    /// A..L against their PA..PL previous values (`aCalcoutRecord.c:1024-1029`)
2938    /// and OVAL against POVL (`:1039`), but it keeps NO previous copy of an
2939    /// array and runs no array comparison anywhere: an array posts if and only
2940    /// if the expression stored into it (AMASK, `afterCalc` `:294-298`) or its
2941    /// input link delivered a changed value (NEWM, `:1031-1036`) — both reported
2942    /// by [`Self::take_cycle_posted_fields`].
2943    ///
2944    /// So the change-detection arm must not see these fields at all. It is not
2945    /// merely redundant with the marks: a client `caput` to AA posts the put's
2946    /// value without advancing the subscriber's `last_posted`, and the next
2947    /// process — which stored nothing into AA and fetched nothing into it —
2948    /// then found AA "changed" and emitted a post C has no counterpart for.
2949    ///
2950    /// Default: empty — every other record's auxiliary fields post on change.
2951    fn fields_posted_only_when_marked(&self) -> &'static [&'static str] {
2952        &[]
2953    }
2954
2955    /// Fields the record's C `monitor()` re-posts with `DBE_LOG` ONLY on
2956    /// every cycle it names them, regardless of change — the analogue of
2957    /// an unconditional `db_post_events(field, DBE_LOG)` sweep.
2958    ///
2959    /// Distinct from [`Self::force_posted_fields`], which posts with
2960    /// `DBE_VALUE | DBE_LOG`: these post with `DBE_LOG` alone, so only a
2961    /// `DBE_LOG` (archiver) subscriber receives the event.
2962    ///
2963    /// The sweep is an INDEPENDENT post, NOT an alternative to the
2964    /// change-detected post. C's `db_post_events` calls compose: on the
2965    /// scaler's count-completion cycle `updateCounts()` posts each changed
2966    /// `Sn` with `DBE_VALUE` (scalerRecord.c:582) and then `monitor()` —
2967    /// reached because the done-interrupt set `ss = IDLE` (`:367`,
2968    /// `:510`) — posts the SAME `Sn` again with a literal `DBE_LOG`
2969    /// (`:757-773`). Two events, one field, one cycle. So the framework
2970    /// emits the sweep post in addition to whatever the change detection
2971    /// produced; gating it on "did not change" would silently drop the
2972    /// `DBE_LOG` half on exactly the cycle that carries the final counts.
2973    ///
2974    /// For a field that is ALSO a [`Self::value_only_change_fields`]
2975    /// member (scaler `Sn`) the change post carries `DBE_VALUE` only, so
2976    /// this sweep is the sole source of its `DBE_LOG` events, matching C.
2977    ///
2978    /// The record returns the names ONLY on the cycles whose C `monitor()`
2979    /// runs — the scaler reads its own `ss` state and returns `S1..Snch`
2980    /// while idle, empty while counting (a counting cycle never reaches C
2981    /// `monitor()`).
2982    ///
2983    /// The sweep post carries `DBE_LOG` plus the cycle's ALARM-transition bits
2984    /// (`recGblResetAlarms`). C's scaler computes that mask and then posts with a
2985    /// literal `DBE_LOG`, dropping the alarm bit — CBUG-B19, a deliberate
2986    /// deviation; see the post site in `collect_subscriber_posts`.
2987    ///
2988    /// Default: empty — most record types have no LOG-only sweep.
2989    fn log_swept_fields(&self) -> &'static [&'static str] {
2990        &[]
2991    }
2992
2993    /// Fields whose change-detected monitor post must carry `DBE_VALUE`
2994    /// only — the LOG bit is stripped — instead of the framework default
2995    /// `DBE_VALUE | DBE_LOG`.
2996    ///
2997    /// The generic change-detection post (and the deadband post for a
2998    /// deadband field named here) normally bundles `DBE_LOG` so an
2999    /// archiver subscribed `DBE_LOG` sees every value change. A record
3000    /// whose C `db_post_events` calls pass a literal `DBE_VALUE` for
3001    /// these fields names them here so the framework drops the LOG bit;
3002    /// the cycle's alarm bits are still OR'd in (alarm posting is a
3003    /// separate per-field contract, unaffected by this hook).
3004    ///
3005    /// C `scalerRecord.c` posts CNT/T/VAL/PR1/TP/FREQ and each active
3006    /// channel `S1..Snch` with a literal `DBE_VALUE` on a value change
3007    /// (scalerRecord.c:372,478,582,588 et al.); `DBE_LOG` appears ONLY in
3008    /// the idle `monitor()` sweep ([`Self::log_swept_fields`],
3009    /// scalerRecord.c:771). The two hooks are complementary: a `DBE_LOG`
3010    /// subscriber on `Sn` is served by the idle sweep, never by a
3011    /// counting-cycle value change — matching C.
3012    ///
3013    /// Default: empty — most record types post changes with
3014    /// `DBE_VALUE | DBE_LOG` (C `monitor_mask | DBE_VALUE | DBE_LOG`,
3015    /// calcRecord.c:420, subRecord.c:400).
3016    fn value_only_change_fields(&self) -> &'static [&'static str] {
3017        &[]
3018    }
3019
3020    /// Secondary value fields a record posts with the *primary VAL
3021    /// monitor mask*, from INSIDE the same guard C wraps its VAL post in —
3022    /// never with a forced `DBE_VALUE | DBE_LOG` on every change.
3023    ///
3024    /// Mirrors C records that drive a raw secondary field with the shared
3025    /// `monitor_mask` rather than `monitor_mask | DBE_VALUE | DBE_LOG`. Each
3026    /// entry pairs the field with the gate C applies to it *inside* that
3027    /// guard — see [`ValuePostGate`], which is the whole reason this is a
3028    /// pair and not a bare name: `ai` re-tests the raw value
3029    /// (`if (prec->oraw != prec->rval)`) while `timestamp` does not.
3030    ///
3031    /// Distinct from the default change-detected aux post (which carries
3032    /// `DBE_VALUE | DBE_LOG` unconditionally): ao `RVAL`/`RBV`, mbbo/
3033    /// mbboDirect/mbbiDirect `RVAL`/`RBV`, sel `SELN` and compress `NUSE`
3034    /// are all posted by C with the `DBE_VALUE | DBE_LOG`-forced mask, so
3035    /// they stay on the default path and must NOT be named here.
3036    ///
3037    /// Default: empty.
3038    fn fields_posted_with_value_mask(&self) -> &'static [(&'static str, ValuePostGate)] {
3039        &[]
3040    }
3041
3042    /// Per-cycle widening of the guard [`ValuePostGate::OnChangeForced`]
3043    /// fields sit behind — C `if (prec->omod) monitor_mask |=
3044    /// (DBE_VALUE|DBE_LOG);` (aoRecord.c:535), which opens the secondary block
3045    /// on a cycle where VAL's own monitor mask is empty.
3046    ///
3047    /// ao needs it because `omod` tracks OVAL, not VAL: OROC can walk the
3048    /// output one step per cycle toward a VAL that has not moved since MLST,
3049    /// so C posts OVAL/RVAL on cycles that post no VAL at all.
3050    ///
3051    /// Clear-on-read, because C's is: `omod` can only ever be true on a cycle
3052    /// whose guard therefore fires, and the guard's first act is
3053    /// `prec->omod = FALSE` (`:537`).
3054    ///
3055    /// Default: empty — no widening, so the guard is VAL's own mask.
3056    fn take_secondary_value_mask(&mut self) -> crate::server::recgbl::EventMask {
3057        crate::server::recgbl::EventMask::NONE
3058    }
3059
3060    /// C's whole `if (prec->oraw != prec->rval) { db_post_events(...);
3061    /// prec->oraw = prec->rval; }` (aoRecord.c:541-543) minus the post: TEST
3062    /// the [`ValuePostGate::OnChangeForced`] field against the record's own
3063    /// "old" copy and, when it moved, ADVANCE that copy — one indivisible
3064    /// step, because C's two statements are inseparable and splitting them is
3065    /// what the port got wrong.
3066    ///
3067    /// Called once per cycle per declared field, by the framework, only when
3068    /// the guard is open — and independently of whether anyone is subscribed,
3069    /// since C's `db_post_events` runs whether or not anyone is listening. A
3070    /// record must not advance the copy anywhere else: an eager `oraw = rval`
3071    /// in the compute step makes the next guarded cycle see no change and
3072    /// withhold an event C sends.
3073    ///
3074    /// Default: `false` — nothing declared, nothing to test.
3075    fn take_secondary_value_change(&mut self, field: &str) -> bool {
3076        let _ = field;
3077        false
3078    }
3079
3080    /// Change-detected auxiliary fields this record posts with C's
3081    /// `monitor_mask | DBE_VALUE` — VAL's monitor mask ORed with `DBE_VALUE`,
3082    /// and NOT the framework default `monitor_mask | DBE_VALUE | DBE_LOG`.
3083    ///
3084    /// The difference is the forced `DBE_LOG`. For a field named here the LOG
3085    /// bit is present only when it is already in VAL's monitor mask — i.e. only
3086    /// when VAL's own ADEL deadband crossed this cycle — so a `DBE_LOG`
3087    /// subscriber (an archiver) receives the field exactly on the cycles C
3088    /// sends it, instead of on every change.
3089    ///
3090    /// `swaitRecord.c::monitor` (647-654) is this shape for its A..L inputs:
3091    ///
3092    /// ```c
3093    /// if (*pnew != *pprev)
3094    ///     db_post_events(pwait, pnew, monitor_mask | DBE_VALUE);
3095    /// ```
3096    ///
3097    /// while `calcRecord.c:420` — the same loop, one module over — writes
3098    /// `monitor_mask | DBE_VALUE | DBE_LOG`. The two records genuinely differ,
3099    /// so the mask is a per-record property, not a framework-wide rule.
3100    ///
3101    /// Distinct from [`Self::value_only_change_fields`] (a literal `DBE_VALUE`,
3102    /// which drops the ADEL LOG bit as well) and from
3103    /// [`Self::fields_posted_with_value_mask`] (posted from INSIDE C's
3104    /// `if (monitor_mask)` guard, so they do not post at all on a cycle where
3105    /// VAL itself does not). The fields named here post on every change,
3106    /// guard or no guard.
3107    ///
3108    /// Default: empty.
3109    fn fields_posted_with_monitor_mask(&self) -> &'static [&'static str] {
3110        &[]
3111    }
3112
3113    /// Fields whose change post carries a LITERAL `DBE_VALUE | DBE_LOG` — this
3114    /// cycle's alarm bits DISCARDED.
3115    ///
3116    /// C's fourth mask shape, and the only one that *drops* information the
3117    /// record already computed. `epidRecord.c::monitor` builds VAL's mask from
3118    /// `recGblResetAlarms` (`:351`) and posts VAL with it, then REASSIGNS
3119    /// (not `|=`) before the secondaries:
3120    ///
3121    /// ```c
3122    /// monitor_mask = DBE_LOG|DBE_VALUE;          /* :376 */
3123    /// if (pepid->ovlp != pepid->oval) db_post_events(pepid, &pepid->oval, monitor_mask);
3124    /// ...                                        /* P, I, D, CT, DT, ERR, CVAL */
3125    /// ```
3126    ///
3127    /// so on an alarm-transition cycle a `DBE_ALARM`-only subscriber to one of
3128    /// those fields is sent NOTHING, while the generic aux mask
3129    /// (`alarm_bits | DBE_VALUE | DBE_LOG`) would send it an event.
3130    ///
3131    /// Distinct from the three narrower shapes: [`Self::value_only_change_fields`]
3132    /// (literal `DBE_VALUE`), [`Self::fields_posted_with_monitor_mask`]
3133    /// (`monitor_mask | DBE_VALUE` — keeps the alarm bits AND VAL's ADEL LOG bit)
3134    /// and [`Self::fields_posted_with_value_mask`] (VAL's mask, posted from inside
3135    /// C's `if (monitor_mask)` guard). A field named here posts on every change,
3136    /// with both value classes and no alarm class, whatever the cycle's alarms did.
3137    ///
3138    /// Resolved for every change-detected field by `AuxPostMask::mask_for`, the single
3139    /// owner of the aux-post mask.
3140    ///
3141    /// Default: empty.
3142    fn fields_posted_without_alarm_bits(&self) -> &'static [&'static str] {
3143        &[]
3144    }
3145
3146    /// The array-style monitor decision (C waveform/aai/aao `monitor()`,
3147    /// waveformRecord.c:291-326). `None` (the default) means the record has
3148    /// no MPST/APST/HASH mechanism and the generic MDEL/ADEL deadband
3149    /// decision applies. `Some(_)` lets the record replace that with its
3150    /// "Always vs On Change" rule: it hashes the array content, compares to
3151    /// the stored `HASH`, updates it, and reports whether `DBE_VALUE` /
3152    /// `DBE_LOG` should be on the VAL post this cycle and whether the hash
3153    /// changed (so the owner posts `HASH` with `DBE_VALUE`). Called by
3154    /// `check_deadband_ext` (the single owner of the VAL-mask decision).
3155    ///
3156    /// The hook is the VAL mask, not the MPST/APST rule specifically: any
3157    /// record whose C `monitor()` decides the mask by its own rule implements
3158    /// it. `histogram` is the other implementor — its rule is the MDEL COUNT
3159    /// deadband (`mcnt > mdel`, histogramRecord.c:287-291), and like waveform's
3160    /// it updates the state it keys on (there, `MCNT = 0`; here, `HASH`).
3161    fn array_monitor_post(&mut self) -> Option<ArrayMonitorPost> {
3162        None
3163    }
3164
3165    /// Fields the record posts itself via an event-driven, individually
3166    /// masked path rather than the generic change-detection loop. The
3167    /// framework excludes these from that loop so they are neither
3168    /// double-posted nor spuriously posted on a cycle the event did not
3169    /// fire. C waveform/aai/aao `monitor()` posts `HASH` this way —
3170    /// `db_post_events(prec, &prec->hash, DBE_VALUE)` only when the content
3171    /// hash changed (waveformRecord.c:317-319), never via VAL's change.
3172    ///
3173    /// The CLOSED set of fields a process cycle of this record may post —
3174    /// the record's C `process()` + `monitor()` `db_post_events` calls,
3175    /// enumerated.
3176    ///
3177    /// `None` (the default) leaves the framework's generic rule in force:
3178    /// every subscribed field that changed since its last post is posted.
3179    /// That rule is right for a record whose C `monitor()` walks its fields
3180    /// and posts whatever moved (calc, sub, ai …). It is WRONG for a record
3181    /// whose C `monitor()` posts a fixed list and leaves every other field it
3182    /// wrote silent — the framework then invents events C never sends:
3183    ///
3184    /// * a field the record WRITES during `process()` but C never posts
3185    ///   (scaler's gate→direction copy, `scalerRecord.c:413-414`: `pdir[i] =
3186    ///   pgate[i]` with no `db_post_events` — C posts `Dn` only from
3187    ///   `special()`).
3188    ///
3189    /// (A field a PUT already posted is NOT in that category: the put's own
3190    /// post advances `last_posted` — see the `RecordInstance::last_posted`
3191    /// contract — so the next process cycle does not change-detect it. This
3192    /// hook must not be used to paper over a framework double post.)
3193    ///
3194    /// `Some(list)` closes it by construction: a field outside the list is
3195    /// never posted by a process cycle — its only monitors come from its own
3196    /// put and from [`Self::monitor_side_effect_fields`]. The list is a
3197    /// whitelist, not a blacklist, so a field added to the record later stays
3198    /// silent unless C posts it.
3199    ///
3200    /// Fields inside the list keep their normal treatment (change detection,
3201    /// [`Self::value_only_change_fields`] mask, deadband, `log_swept_fields`).
3202    fn process_posted_fields(&self) -> Option<&'static [&'static str]> {
3203        None
3204    }
3205
3206    /// Fields the record posts itself via an event-driven, individually
3207    /// masked path rather than the generic change-detection loop. The
3208    /// framework excludes these from that loop so they are neither
3209    /// double-posted nor spuriously posted on a cycle the event did not
3210    /// fire. C waveform/aai/aao `monitor()` posts `HASH` this way —
3211    /// `db_post_events(prec, &prec->hash, DBE_VALUE)` only when the content
3212    /// hash changed (waveformRecord.c:317-319), never via VAL's change.
3213    ///
3214    /// Default: empty.
3215    fn event_posted_fields(&self) -> &'static [&'static str] {
3216        &[]
3217    }
3218
3219    /// Initialize record (pass 0: field defaults; pass 1: dependent init).
3220    fn init_record(&mut self, _pass: u8) -> CaResult<()> {
3221        Ok(())
3222    }
3223
3224    /// Did `init_record` leave the record PERMANENTLY ACTIVE — C's
3225    /// `prec->pact = TRUE` inside `init_record`, which is how a record type
3226    /// disables itself when it cannot possibly process?
3227    ///
3228    /// `subRecord.c:119-123` is the live case: an empty `SNAM` has no
3229    /// subroutine to call, so C prints `"%s.SNAM is empty"`, sets `pact = TRUE`
3230    /// and returns 0. `dbProcess` then takes the PACT-active branch on every
3231    /// scan — the record serves its fields but never runs record support.
3232    /// `caget X.PACT` on a bare `record(sub,"X"){}` reads 1 on a C IOC.
3233    ///
3234    /// This is a STATE PREDICATE over the record's current fields, not a
3235    /// one-time init verdict: C re-asks it on every put to a field in
3236    /// [`Self::pact_park_fields`], through the two passes of `special()`
3237    /// (`subRecord.c:170-194`). Pass 0 releases the park, pass 1 re-takes it if
3238    /// the value just stored still leaves the record unable to run, so
3239    /// `caput X.SNAM mySub` revives a parked record and `caput X.SNAM ""` parks
3240    /// a running one.
3241    ///
3242    /// PACT is a `dbCommon` field with a single owner
3243    /// ([`crate::server::record::RecordInstance::enter_pact`] / [`leave_pact`]), so a record cannot
3244    /// park it itself. It answers here and the owner performs the transition —
3245    /// at the end of the init passes, and either side of a park-field put.
3246    ///
3247    /// [`leave_pact`]: crate::server::record::RecordInstance::leave_pact
3248    fn parks_pact(&self) -> bool {
3249        false
3250    }
3251
3252    /// The fields whose put can change [`Self::parks_pact`]'s answer — C's
3253    /// `special(SPC_MOD)` set for the PACT park (`subRecord.dbd.pod` marks only
3254    /// `SNAM`). Nothing else re-asks the question, so a put to an unrelated
3255    /// field of a parked record cannot disturb the park the way an
3256    /// every-put re-assertion would. Default: none, so the whole mechanism is
3257    /// inert for every record type that does not use PACT as a self-disable.
3258    fn pact_park_fields(&self) -> &'static [&'static str] {
3259        &[]
3260    }
3261
3262    /// Post-init finalisation hook with mutable access to the
3263    /// framework's UDF flag. Called once after both `init_record`
3264    /// passes complete. Default implementation is a no-op.
3265    ///
3266    /// epics-base PR `dabcf89` (mbboDirect): when VAL is undefined
3267    /// at init time but the user populated B0..B1F bits, the bits
3268    /// should be folded into VAL and UDF cleared. The framework
3269    /// owns `common.udf`, so the record cannot mutate it from
3270    /// `init_record` alone — this hook is the controlled point of
3271    /// access.
3272    fn post_init_finalize_undef(&mut self, _udf: &mut bool) -> CaResult<()> {
3273        Ok(())
3274    }
3275
3276    /// Whether this record type's C `init_record` resets the record to a
3277    /// defined, no-alarm state — `prec->udf = 0; recGblResetAlarms(prec)` — so
3278    /// a freshly loaded, never-processed record reads `UDF=0`,
3279    /// `STAT=NO_ALARM`, `SEVR=NO_ALARM` instead of the born `UDF`/`INVALID`/`1`.
3280    ///
3281    /// Almost no record does this: a not-yet-processed record is normally left
3282    /// `UDF` so an `MS` consumer inherits `INVALID` at IOC startup (see
3283    /// `RecordInstance::run_init_passes`). The asyn record is the exception —
3284    /// `asynRecord.c` `init_record` pass 0 does `pasynRec->udf = 0;
3285    /// recGblResetAlarms(pasynRec)` unconditionally, because a device-config
3286    /// record is defined the moment it loads, even against a disconnected port.
3287    /// `UDF` is a common field `init_record` cannot reach, so the record
3288    /// declares the fact here and the init owner performs the reset. Default
3289    /// `false`.
3290    fn init_resets_alarms(&self) -> bool {
3291        false
3292    }
3293
3294    /// C `cvt_dbaddr`'s `paddr->no_elements` for one of this record type's
3295    /// `special(SPC_DBADDR)` fields — the CHANNEL's capacity, which is not the
3296    /// count `get_array_info` serves.
3297    ///
3298    /// Answer the number only. WHICH fields reach `cvt_dbaddr` is not a record's
3299    /// question: it is the `.dbd`'s, and
3300    /// [`FieldDeclaration::field_native_count`] reads it from there before
3301    /// asking. A record that hand-lists its own array fields here is copying a
3302    /// declaration it already has, which is how the two drift apart.
3303    ///
3304    /// Returning `None` (the default, and the answer for a record type with no
3305    /// `SPC_DBADDR` field) means the channel count is the value's own count.
3306    ///
3307    /// - waveform `VAL` → `NELM` (buffer capacity; the value serves `NORD`).
3308    /// - asyn `BOUT` → `OMAX`, `BINP` → `IMAX` (the `SPC_DBADDR` octet buffers;
3309    ///   the value serves the transferred byte count `NOWT`/`NORD`).
3310    /// - acalcout `AVAL`/`AA..LL`/`OAV` → `NELM`, or the `NUSE` window under
3311    ///   `SIZE=NUSE` (`aCalcoutRecord.c:627-631`).
3312    /// - mca `VAL`/`BG` → `NMAX` (`mcaRecord.c:857`).
3313    fn dbaddr_capacity(&self, _field: &str) -> Option<u32> {
3314        None
3315    }
3316
3317    /// The part of C's `init_record` tail that is NOT tracker seeding: an
3318    /// output record's closing VAL→RVAL conversion, run at the same point
3319    /// [`Self::seed_deadband_tracking`] is and immediately before it, which is
3320    /// the order C writes the two in (`boRecord.c:167-175` converts, then
3321    /// seeds).
3322    ///
3323    /// It cannot live in [`Self::init_record`]: that runs before the
3324    /// `recGblInitConstantLink` table, so a record whose VAL arrives from a
3325    /// constant DOL would convert the pre-seed VAL. C has no such split —
3326    /// `busyRecord.c:151-179` is one function with the constant load at the
3327    /// top and the conversion at the bottom.
3328    ///
3329    /// Every record whose C init tail does non-tracker work implements it here
3330    /// — the conversion and the output-tracking stores C writes around the
3331    /// `mlst`/`alst`/`lalm` lines:
3332    ///
3333    /// - busy `:176-179` — the convert alone; it seeds no tracker at all.
3334    /// - bo `:166-170,174-175` — convert through MASK, then `oraw`/`orbv`.
3335    /// - mbbo `:176-177,181-182` — `convert()`, then `oraw`/`orbv`.
3336    /// - mbboDirect `:142-143,161-162` — `bitsFromVAL`, then `oraw`/`orbv`.
3337    /// - ao `:156,160-161` — `oval`/`pval`, then `oraw`/`orbv`.
3338    ///
3339    /// The split is by MEANING, not by convenience: this hook is the tail's
3340    /// derived state (RVAL and the bit cells from VAL, ORAW/ORBV from RVAL/RBV,
3341    /// ao's OVAL/PVAL), and [`Self::seed_deadband_tracking`] is the tail's
3342    /// deadband trackers and nothing else. Four records used to carry both
3343    /// halves in `seed_deadband_tracking`, which made "the trackers are seeded"
3344    /// and "the record is converted" the same call — so a caller that wanted
3345    /// one got the other, and busy (which needs the convert without the
3346    /// trackers) had to be the exception. Default: empty.
3347    fn init_record_tail(&mut self) {}
3348
3349    /// Seed the monitor/archive/alarm deadband trackers (MLST/ALST/LALM)
3350    /// from the initial value at iocInit, called once by the builder after
3351    /// both `init_record` passes and `post_init_finalize_undef`.
3352    ///
3353    /// Most C value records end `init_record` with
3354    /// `prec->mlst = prec->alst = prec->lalm = prec->val`
3355    /// (`aiRecord.c:129-131`, `longinRecord.c:120-122`), so their first
3356    /// `monitor()` evaluates `DELTA(mlst, val) > mdel` with `mlst == val`
3357    /// and posts nothing for a value that has not moved since init. The
3358    /// default does that for whichever of MLST/ALST/LALM the record serves,
3359    /// so a record given a nonzero initial VAL (constant DOL, `field(VAL,..)`)
3360    /// does not post a spurious first-cycle update.
3361    ///
3362    /// It is NOT universal, which is why it stays overridable: `calcRecord.c`
3363    /// (:90-114), `dfanoutRecord.c` (:96-111), `selRecord.c` (:88-110),
3364    /// `sCalcoutRecord.c` (:203-322), `aCalcoutRecord.c` (:171-281) and
3365    /// `busyRecord.c` (:127-183) end `init_record` without touching the
3366    /// trackers, so those six leave them at 0 and DO post that first update.
3367    /// Each overrides this with an empty body. The output records override it
3368    /// only to name the subset their C tail actually writes (`mbboDirect`
3369    /// seeds MLST and no LALM, `boRecord.c:172-173` seeds MLST and LALM but no
3370    /// ALST); everything else their tail does is
3371    /// [`Self::init_record_tail`]'s.
3372    fn seed_deadband_tracking(&mut self) {
3373        let seed = match self.monitor_deadband_value() {
3374            Some(v) if v.is_finite() => v,
3375            _ => return,
3376        };
3377        for field in ["MLST", "ALST", "LALM"] {
3378            // Coerce to the cell's own type first. `bi`/`mbbi` declare MLST
3379            // DBF_USHORT and reject a Double outright, so seeding them as a
3380            // Double left them at 0 with the error dropped on the floor —
3381            // silently the no-seed behaviour, for two records C does seed.
3382            let Some(current) = self.get_field(field) else {
3383                continue;
3384            };
3385            let coerced = EpicsValue::Double(seed).convert_to(current.db_field_type());
3386            let _ = self.put_field(field, coerced);
3387        }
3388    }
3389
3390    /// Called by the framework immediately after applying this cycle's
3391    /// [`Record::multi_input_links`] fetches, before `process()`.
3392    ///
3393    /// `resolved` lists the `link_field` names (the first element of
3394    /// each `multi_input_links` pair) whose fetch SUCCEEDED this cycle —
3395    /// C's `RTN_SUCCESS(dbGetLink(...))`, i.e. status 0. That includes a
3396    /// CONSTANT link, which returns success having delivered nothing
3397    /// (`dbConstGetValue`, `dbConstLink.c:219-225`) — `epidRecord.c:191`
3398    /// clears UDF on exactly that. A link field absent from the slice
3399    /// either had no link configured or its DB/CA fetch FAILED.
3400    ///
3401    /// This is the framework analogue of C device support inspecting
3402    /// `RTN_SUCCESS(dbGetLink(...))` — e.g. `epidRecord.c:191-193`
3403    /// clears `udf` only when `dbGetLink(&prec->stpl, ...)` returns
3404    /// success. A record's `process()` cannot otherwise observe whether
3405    /// an input link's fetch succeeded, because a failed fetch simply
3406    /// leaves the target field unwritten.
3407    ///
3408    /// Additive, framework-set-hook pattern (same shape as
3409    /// [`Record::set_process_context`]). Default: ignore.
3410    fn set_resolved_input_links(&mut self, _resolved: ResolvedInputLinks<'_>) {}
3411
3412    /// Report this cycle's `fetch_values()` outcome: `failed == true` means C's
3413    /// helper would have returned a non-zero status, so the record body — the
3414    /// `calcPerform` / `do_sel` the C `process()` wraps in
3415    /// `if (fetch_values(prec) == 0)` — must NOT run, and VAL/UDF freeze.
3416    ///
3417    /// This is the single delivery point for that outcome, whichever
3418    /// [`InputFetchPolicy`] produced it (a failed link read) and whichever
3419    /// record-specific rule did (sel's `Specified`-mode selected-input read).
3420    /// Records that gate: calc (calcRecord.c:120), calcout (:237), sCalcout
3421    /// (sCalcoutRecord.c:356), aCalcout (aCalcoutRecord.c:399), swait
3422    /// (swaitRecord.c:408 — which additionally raises READ_ALARM/INVALID on the
3423    /// failure) and sel (selRecord.c:114). sub/aSub gate the same outcome, but
3424    /// their body is the framework-dispatched subroutine, so they consume it
3425    /// through `RecordInstance::suppress_subroutine_run` instead.
3426    ///
3427    /// Default: ignore (records with no fetch gate). Same framework-set hook
3428    /// pattern as [`Record::set_resolved_input_links`].
3429    fn set_fetch_gate_failed(&mut self, _failed: bool) {}
3430
3431    /// Whether this record's multi-input fetch ([`Self::multi_input_links`]) is
3432    /// C `dbGetLink` — which raises `setLinkAlarm` (LINK/INVALID, AMSG
3433    /// `field <NAME>`) on every failed read — or a `recDynLink` CA-style get,
3434    /// which raises nothing.
3435    ///
3436    /// `true` for every base record and for sCalcout/aCalcout/transform
3437    /// (`calcRecord.c:439`, `calcoutRecord.c:705`, `subRecord.c:414`,
3438    /// `aSubRecord.c:282`, `selRecord.c:430`, `printfRecord.c:54`,
3439    /// `sCalcoutRecord.c:886`, `aCalcoutRecord.c:1070`,
3440    /// `transformRecord.c:537`). `false` only for swait, whose `fetch_values`
3441    /// (`swaitRecord.c:702`) reads INAA..INPL with `recDynLinkGet` and answers a
3442    /// failure with `recGblSetSevr(READ_ALARM, INVALID_ALARM)` at
3443    /// `swaitRecord.c:413` instead. Both alarms are INVALID and
3444    /// `rec_gbl_set_sevr*` is strict-greater, so raising the wrong one first
3445    /// wins the tie and publishes the wrong STAT.
3446    fn multi_input_fetch_is_db_get_link(&self) -> bool {
3447        true
3448    }
3449
3450    /// The generation of this record's [`Self::multi_input_links`] texts: a
3451    /// count that moves on EVERY write of any of them, so a parse validated
3452    /// against the texts at generation `g` is still the parse of those texts
3453    /// while the record answers `g`. A type answers `Some` only when its link
3454    /// texts have ONE writer that counts — `calc`'s `set_inp_link`, behind
3455    /// private fields — and the count is then what C's `dbPutSpecial` on a
3456    /// `DBF_INLINK` gives `dbDbInitLink`: the moment the link changed. `None`
3457    /// (the default) validates every parse against the text itself, byte for
3458    /// byte, on every read.
3459    fn input_links_generation(&self) -> Option<u64> {
3460        None
3461    }
3462
3463    /// Whether a FAILED read of this multi-input link leaves the cycle
3464    /// untouched — no `setLinkAlarm`, no [`Self::input_fetch_policy`] gate,
3465    /// no value stored.
3466    ///
3467    /// One record needs it: acalcout guards its ARRAY half with the link's own
3468    /// connection status (`aCalcoutRecord.c:1078`)
3469    ///
3470    /// ```c
3471    /// if ((*plinkValid==acalcoutINAV_EXT) || (*plinkValid==acalcoutINAV_LOC))
3472    /// ```
3473    ///
3474    /// so an array link C believes cannot deliver is never read at all, and
3475    /// `fetch_values` returns 0 with the calc still running. C moves a link
3476    /// between `EXT_NC` and `EXT` from a `dbCaIsLinkConnected` watchdog
3477    /// (`aCalcoutRecord.c:1155`, `:1187`); this port has no CA client, so it
3478    /// publishes `EXT_NC` for EVERY external link
3479    /// ([`crate::server::records::link_status::classify_link`]) and cannot make
3480    /// that test at select time. Reading and then discarding the failure is the
3481    /// same observable behaviour for every state the port can reach — a link
3482    /// that delivers is C's `EXT`/`LOC`, a link that does not is C's `EXT_NC` —
3483    /// and, unlike gating on the published `IAAV`, it does not stop reading the
3484    /// external array links whose values the resolver really does serve.
3485    ///
3486    /// The SCALAR half is deliberately NOT inert: its loop (`:1068-1071`) has
3487    /// no status test and `return`s at the first failing `dbGetLink`.
3488    /// The text of a link field as the record stores it, lent rather than
3489    /// copied.
3490    ///
3491    /// C reads a link off `dbCommon` as a `struct link` and copies nothing; a
3492    /// scan cycle that asks [`Self::get_field`] instead pays a `String` clone,
3493    /// a [`PvString`] wrap, an
3494    /// [`EpicsValue`] construction and a drop for
3495    /// every link the type declares — 21 of them per `calc` cycle, whether or
3496    /// not a single one is wired.
3497    ///
3498    /// `Some("")` is a declared link that is unset, which is what an unwired
3499    /// `INPA` holds. `None` is this type declining to lend the field, and the
3500    /// caller falls back to the owned [`Self::get_field`] path, so a type that
3501    /// does not override this loses nothing.
3502    fn link_text_ref(&self, _link_field: &str) -> Option<&str> {
3503        None
3504    }
3505
3506    fn input_link_failure_is_inert(&self, _link_field: &str) -> bool {
3507        false
3508    }
3509
3510    /// C `monitor()`'s four deadband cells, read in one question: the MDEL /
3511    /// ADEL thresholds and the MLST / ALST values the record last posted at.
3512    /// `None` is a cell this type does not carry, which is the "nothing
3513    /// posted yet" state `check_deadband`
3514    /// takes for MLST/ALST and a zero deadband for MDEL/ADEL.
3515    ///
3516    /// Asked as one question because the cycle otherwise asks
3517    /// [`Self::get_field`] four times through the vtable and pays an
3518    /// [`EpicsValue`] per answer. This default body
3519    /// is codegen'd per record type, so the four asks inside it are static
3520    /// calls with literal field names on the type's own cells; overriding it
3521    /// buys nothing and no record type should.
3522    fn monitor_deadband_cells(&self) -> MonitorDeadbandCells {
3523        MonitorDeadbandCells {
3524            mdel: self.get_field("MDEL").and_then(|v| v.to_f64()),
3525            adel: self.get_field("ADEL").and_then(|v| v.to_f64()),
3526            mlst: self.get_field("MLST").and_then(|v| v.to_f64()),
3527            alst: self.get_field("ALST").and_then(|v| v.to_f64()),
3528        }
3529    }
3530
3531    /// Store the value this cycle posted at into the cells whose deadband it
3532    /// crossed — C `monitor()`'s `prec->mlst = prec->val` /
3533    /// `prec->alst = prec->val`.
3534    ///
3535    /// C declares MLST/ALST with the record's VAL type, so the double is
3536    /// converted to whatever the cell holds. Same one-question reason as
3537    /// [`Self::monitor_deadband_cells`] — this is four more vtable calls
3538    /// otherwise.
3539    fn store_monitor_last_posted(&mut self, val: f64, mlst: bool, alst: bool) {
3540        for (field, store) in [("ALST", alst), ("MLST", mlst)] {
3541            if !store {
3542                continue;
3543            }
3544            let target = self
3545                .get_field(field)
3546                .map(|v| v.db_field_type())
3547                .unwrap_or(crate::types::DbFieldType::Double);
3548            let _ = self.put_field(field, EpicsValue::Double(val).convert_to(target));
3549        }
3550    }
3551
3552    /// The three cells C `checkAlarms` reads straight off the record — VAL,
3553    /// HYST and LALM — in one question, in the ladder's own domain. `None`
3554    /// is a VAL the ladder has no arm for; a `None` cell is one this type
3555    /// does not carry.
3556    ///
3557    /// Same reason as [`Self::monitor_deadband_cells`]: the ladder runs on
3558    /// every cycle of every analog record, and asking [`Self::get_field`] by
3559    /// name is a full name match per cell, each answered in an `EpicsValue`
3560    /// the ladder then converts back to the number C reads as a struct
3561    /// member.
3562    fn analog_alarm_input(&self) -> Option<AnalogAlarmInput> {
3563        let val = AlarmLimit::from_ladder_value(&self.val()?)?;
3564        Some(AnalogAlarmInput {
3565            val,
3566            hyst: self
3567                .get_field("HYST")
3568                .and_then(|v| AlarmLimit::from_stored(&v)),
3569            lalm: self
3570                .get_field("LALM")
3571                .and_then(|v| AlarmLimit::from_stored(&v)),
3572        })
3573    }
3574
3575    /// C `checkAlarms`'s AFTC low-pass cells — the time constant and the
3576    /// filter accumulator — in one question.
3577    ///
3578    /// `None` is a record type that carries no AFTC field, and so has no
3579    /// filter block in its C `checkAlarms` at all: `ao`, `longout`,
3580    /// `int64out` and `calcout` (confirmed against their `.dbd.pod`). That
3581    /// used to be a hard-coded list of the four types that DO carry it,
3582    /// beside the field reads that would have answered the same question.
3583    fn alarm_filter_cells(&self) -> Option<(f64, f64)> {
3584        let aftc = self.get_field("AFTC")?.to_f64().unwrap_or(0.0);
3585        let afvl = self
3586            .get_field("AFVL")
3587            .and_then(|v| v.to_f64())
3588            .unwrap_or(0.0);
3589        Some((aftc, afvl))
3590    }
3591
3592    /// Store the AFTC filter accumulator — C's `prec->afvl = afvl`.
3593    fn store_alarm_filter_value(&mut self, afvl: f64) {
3594        let _ = self.put_field("AFVL", EpicsValue::Double(afvl));
3595    }
3596
3597    /// Arm C's `prec->lalm` latch, coerced to the type the cell is declared
3598    /// with: C declares LALM with the record's VAL type, so an `epicsInt64`
3599    /// latch must not round through a double.
3600    fn store_analog_lalm(&mut self, lalm: AlarmLimit) {
3601        let target = self
3602            .get_field("LALM")
3603            .map(|v| v.db_field_type())
3604            .unwrap_or(crate::types::DbFieldType::Double);
3605        let _ = self.put_field("LALM", lalm.to_epics_value().convert_to(target));
3606    }
3607
3608    /// Which slots of [`Self::multi_input_links`] are wired, as two bitmasks
3609    /// over that list: `.0` the slots [`Self::link_text_ref`] lends a
3610    /// non-empty text for, `.1` the slots it declines to lend at all, which
3611    /// the caller must resolve through the instance instead.
3612    ///
3613    /// Asked as ONE question because the cycle otherwise asks one per declared
3614    /// link — 21 trips through the vtable for a `calc`, to learn that a stock
3615    /// database wires none of them. A trait default body is codegen'd per
3616    /// implementing type, so `multi_input_links` and `link_text_ref` inside
3617    /// this one are static calls the compiler can inline.
3618    ///
3619    /// Inlined is still one name match per declared link on every cycle of
3620    /// every record of the type — the cycle's most-repeated question, measured
3621    /// at 232 of a `calc` cycle's ~2570 TSC. A type whose link texts sit in one
3622    /// array answers off that array through [`input_link_slots_of`] and skips
3623    /// the walk; a type whose texts are separate fields hands the same helper
3624    /// an array of references to them. Either way the answer must be the one
3625    /// this body would give, which is what
3626    /// `a_record_that_reads_its_own_link_slots_answers_what_the_name_walk_would`
3627    /// pins for every declared type.
3628    ///
3629    /// `None` for a type declaring more links than a mask holds; the caller
3630    /// then walks the list link by link, as it did before this existed.
3631    fn set_input_link_slots(&self) -> Option<(u64, u64)> {
3632        let links = self.multi_input_links();
3633        if links.len() > u64::BITS as usize {
3634            return None;
3635        }
3636        let (mut set, mut unknown) = (0u64, 0u64);
3637        for (slot, (link_field, _)) in links.iter().enumerate() {
3638            match self.link_text_ref(link_field) {
3639                Some("") => {}
3640                Some(_) => set |= 1 << slot,
3641                None => unknown |= 1 << slot,
3642            }
3643        }
3644        Some((set, unknown))
3645    }
3646
3647    /// Whether this record's C `process()` performs the SCALAR closed-loop DOL
3648    /// fetch — `dbGetLink(&prec->dol, <dbr>, &prec->val, 0, 0)` guarded by
3649    /// `prec->dol.type != CONSTANT && prec->omsl == menuOmslclosed_loop`
3650    /// (`boRecord.c:191-205`, `busyRecord.c:196-208`, `dfanoutRecord.c:116-122`,
3651    /// and the same block in ao/longout/int64out/mbbo/mbboDirect/stringout/lso).
3652    /// The framework then owns the fetch, its LINK/INVALID failure arm
3653    /// ([`Self::closed_loop_dol_read_failed`]) and the UDF clear.
3654    ///
3655    /// Declaring both `menu(menuOmsl)` and `field(DOL,DBF_INLINK)` is NOT the
3656    /// same question and cannot stand in for this one: `aao` declares both and
3657    /// copies DOL as an ARRAY (`aaoRecord.c::fetchValue`), `motor` declares both
3658    /// and drives DOL through its own C code. Both source DOL record-locally and
3659    /// answer `false` here. `epid` has `menuOmsl` and no DOL at all — it fetches
3660    /// from `STPL`.
3661    ///
3662    /// This was a record-name match inside the process cycle. A list the
3663    /// compiler cannot check, in a file no record author reads, had already
3664    /// been wrong twice — `dfanout` once and `busy` again — so the fact moved
3665    /// to the record that owns it. Default: false.
3666    fn fetches_dol_closed_loop(&self) -> bool {
3667        false
3668    }
3669
3670    /// C's failure arm for THIS cycle's closed-loop DOL read: the framework
3671    /// calls it when `dbGetLink(&prec->dol, ...)` returned a non-zero status
3672    /// with `OMSL = closed_loop` and a non-constant DOL.
3673    ///
3674    /// The LINK/INVALID alarm is not this hook's business — `db_get_link` owns
3675    /// it, exactly as C's `dbGetLink` does. What is left is the per-record body
3676    /// gate, and C writes a different one in each record:
3677    ///
3678    /// * ao — `fetch_value` (aoRecord.c:442) sets `prec->val = prec->pval`
3679    ///   BEFORE the read, and `if(!status) convert(prec, value)` (:188) then
3680    ///   skips the convert, so VAL falls back to the last actual output and
3681    ///   OVAL/PVAL/RVAL freeze (an OROC ramp stops advancing).
3682    /// * longout (:155) / int64out (:146) — same `if (!status) convert(...)`,
3683    ///   whose convert is the DRVH/DRVL clamp.
3684    /// * mbbo (:206) / mbboDirect (:186) — `goto CONTINUE` past `udf = FALSE`,
3685    ///   `bitsFromVAL`, `convert` and the pre-output timestamp.
3686    /// * bo (:200-204), stringout, lso, dfanout — nothing: bo converts VAL to
3687    ///   RVAL whether the read succeeded or not, and the other three have no
3688    ///   convert step at all. They keep the default no-op.
3689    ///
3690    /// Additive, framework-set-hook pattern, same shape as
3691    /// [`Self::set_fetch_gate_failed`]. Default: ignore.
3692    fn closed_loop_dol_read_failed(&mut self) {}
3693
3694    /// The INP counterpart of [`Self::closed_loop_dol_read_failed`]: the
3695    /// framework calls it when a soft-channel `read_xxx`'s `dbGetLink(&prec->inp,
3696    /// ...)` returned a non-zero status, so no value was sourced this cycle.
3697    ///
3698    /// The LINK/INVALID alarm is not this hook's business — `db_get_link` owns
3699    /// it, as C's `dbGetLink` does. What is left is what the record makes of a
3700    /// cycle that read nothing, and C writes that per record:
3701    ///
3702    /// * subArray — `read_sa` skips `subset()` entirely on a non-zero status
3703    ///   (`devSASoft.c:118-120`), `readValue` returns it, and `process` does
3704    ///   `prec->udf = !!status` (`subArrayRecord.c:148`). An UNDEFINED subArray
3705    ///   then serves ZERO elements, not the stale slice
3706    ///   (`get_array_info`, `:181-184`) — the only array record with that rule.
3707    /// * waveform/aai/aao — nothing: they clear UDF on the line after
3708    ///   `readValue` whatever it returned ([`Self::clears_udf_unconditionally`]).
3709    /// * the scalar soft records — nothing here either; C leaves `prec->udf`
3710    ///   untouched on a failed read (`if (status == 0) … prec->udf = FALSE`),
3711    ///   which is the framework's per-cycle re-derive, not a record hook.
3712    ///
3713    /// Additive, framework-set-hook pattern. Default: ignore.
3714    fn soft_input_read_failed(&mut self) {}
3715
3716    /// Report this cycle's subroutine status — C `process`'s `status` variable
3717    /// for `sub`/`aSub`:
3718    ///
3719    /// ```c
3720    /// status = fetch_values(prec);
3721    /// if (!status) { status = do_sub(prec); prec->val = status; }
3722    /// ...
3723    /// if (!status)                      /* aSubRecord.c:234-240 */
3724    ///     for (i = 0; i < NUM_ARGS; i++)
3725    ///         dbPutLink(&(&prec->outa)[i], (&prec->ftva)[i], (&prec->vala)[i],
3726    ///             (&prec->neva)[i]);
3727    /// ```
3728    ///
3729    /// so `0` — and only `0` — means the input fetch succeeded AND `do_sub` ran
3730    /// and returned success. It is the gate on aSub's OUT-link pushes.
3731    ///
3732    /// Delivered by `RecordInstance::run_registered_subroutine`, the single
3733    /// owner of the `do_sub` call, on every one of its exit paths (the
3734    /// suppressed cycle, no bound routine, the routine's own return).
3735    ///
3736    /// Default: ignore (records with no subroutine).
3737    fn set_subroutine_status(&mut self, _status: i64) {}
3738
3739    /// Called before/after a field put for side-effect processing.
3740    fn special(&mut self, _field: &str, _after: bool) -> CaResult<()> {
3741        Ok(())
3742    }
3743
3744    /// The period of this record's monitor watchdog, or `None` when it has
3745    /// none — C `histogramRecord.c::wdogInit` (:126-152):
3746    ///
3747    /// ```c
3748    /// static void wdogInit(histogramRecord *prec) {
3749    ///     if (prec->sdel > 0) { ... callbackRequestDelayed(&pcallback->callback, prec->sdel); }
3750    /// }
3751    /// ```
3752    ///
3753    /// A watchdog is NOT a process cycle: it posts monitors for a record whose
3754    /// value is changing but whose deadband (histogram MDEL) is holding the
3755    /// posts back, so a slow accumulation still reaches a display. The
3756    /// framework re-reads this on every tick, so clearing SDEL stops the
3757    /// watchdog at the next fire without any separate cancel.
3758    ///
3759    /// histogram is the only base record with one. Default: no watchdog.
3760    fn watchdog_interval(&self) -> Option<std::time::Duration> {
3761        None
3762    }
3763
3764    /// One watchdog tick — C `histogramRecord.c::wdogCallback` (:102-124):
3765    ///
3766    /// ```c
3767    /// if (prec->mcnt > 0) {
3768    ///     dbScanLock(prec);
3769    ///     recGblGetTimeStamp(prec);
3770    ///     db_post_events(prec, &prec->val, DBE_VALUE | DBE_LOG);
3771    ///     prec->mcnt = 0;
3772    ///     dbScanUnlock(prec);
3773    /// }
3774    /// ```
3775    ///
3776    /// The record performs its own state change (histogram: zero MCNT) and
3777    /// returns the fields whose monitors the framework must post — the
3778    /// `db_post_events` half, which a record cannot do itself. An empty slice
3779    /// means "nothing changed since the last tick": no timestamp, no post. The
3780    /// framework holds the record lock across the call (C `dbScanLock`) and
3781    /// re-arms afterwards from [`Self::watchdog_interval`].
3782    ///
3783    /// Default: nothing to post.
3784    fn watchdog_fire(&mut self) -> &'static [&'static str] {
3785        &[]
3786    }
3787
3788    /// The body of the delayed callback armed by
3789    /// [`ProcessAction::DelayedCallbackAfter`] — C `boRecord.c::myCallbackFunc`
3790    /// (:105-118):
3791    ///
3792    /// ```c
3793    /// if (prec->pact) {
3794    ///     if ((prec->val == 1) && (prec->high > 0)) { callbackRequestDelayed(cb, prec->high); }
3795    /// } else {
3796    ///     prec->val = 0;
3797    ///     dbProcess((struct dbCommon *)prec);
3798    /// }
3799    /// ```
3800    ///
3801    /// Run under the record gate (C `dbScanLock`) when the timer fires, and
3802    /// before any `process()` re-entry. `pact` is the record's PACT at fire
3803    /// time, which the record cannot read for itself.
3804    ///
3805    /// This hook is the ONLY place the one-shot's mutation may happen: keeping
3806    /// it out of `process()` is what stops an unrelated process cycle from
3807    /// consuming it. Default: re-enter `process()` and change nothing, so a
3808    /// record that emits the action without overriding still behaves as a plain
3809    /// [`ProcessAction::ReprocessAfter`].
3810    fn delayed_callback_fire(&mut self, _pact: bool) -> DelayedCallbackOutcome {
3811        DelayedCallbackOutcome::Reprocess
3812    }
3813
3814    /// Whether this record type's support reads SIML through the `recGbl`
3815    /// simulation helpers (`recGblGetSimm`/`recGblInitSimm`, and therefore
3816    /// `recGblSaveSimm`/`recGblCheckSimm`) rather than a bare `dbGetLink`.
3817    ///
3818    /// ONE C fact, two consequences — which is why it is one predicate:
3819    ///
3820    /// - **The SCAN swap.** The helpers take `&prec->sscn` and `&prec->oldsimm`,
3821    ///   so only a record that declares those fields can call them, and only
3822    ///   such a record swaps SCAN with SSCN on a SIMM transition
3823    ///   (`recGblCheckSimm`, `recGbl.c:427-437`).
3824    /// - **The alarm on a failed SIML read.** `recGblGetSimm` reads SIML with
3825    ///   `dbTryGetLink` — which does NOT call `setLinkAlarm` — and then raises
3826    ///   the alarm itself, by writing `nsta` DIRECTLY:
3827    ///   `if (status && !pcommon->nsev) pcommon->nsta = LINK_ALARM;`
3828    ///   (`recGbl.c:454`). SEVR is left alone. A record reading SIML with a
3829    ///   plain `dbGetLink` (`busyRecord.c:399`, `swaitRecord.c:402`) instead
3830    ///   gets `setLinkAlarm` (`dbLink.c:319-323`) →
3831    ///   `recGblSetSevrMsg(LINK_ALARM, INVALID_ALARM)`, a full severity raise.
3832    ///
3833    /// The 21 base records that declare SSCN answer `true`; `busy` and `swait`
3834    /// carry SIMM/SIML/SIOL but neither SSCN nor OLDSIMM. Records with no
3835    /// simulation block answer `false` trivially.
3836    ///
3837    /// The default consults [`record_type_has_sscn`](crate::server::recgbl::simm::record_type_has_sscn),
3838    /// which is enumerated from the C dbd files, so no record has to restate it.
3839    fn uses_recgbl_simm_helpers(&self) -> bool {
3840        crate::server::recgbl::simm::record_type_has_sscn(self.record_type())
3841    }
3842
3843    /// The record's `readValue`/`writeValue` ABORTS when the SIML read fails —
3844    /// it returns before performing any I/O, so the cycle does no device write,
3845    /// no SIOL redirect, and raises no SIMM_ALARM.
3846    ///
3847    /// `busy` is the only record that does this (busyRecord.c:399-401):
3848    ///
3849    /// ```c
3850    /// status = dbGetLink(&prec->siml, DBR_USHORT, &prec->simm, 0, 0);
3851    /// if (status)
3852    ///     return(status);          /* <-- before write_busy AND before dbPutLink */
3853    /// ```
3854    ///
3855    /// The LINK_ALARM that `dbGetLink`'s `setLinkAlarm` already raised is the
3856    /// cycle's only simulation alarm.
3857    ///
3858    /// The other two families do NOT abort, for different reasons:
3859    ///
3860    /// - The 21 [`Self::uses_recgbl_simm_helpers`] records look like they do —
3861    ///   `readValue` has `status = recGblGetSimm(...); if (status) return status;`
3862    ///   (longinRecord.c:403-405) — but `recGblGetSimm` ends with an
3863    ///   unconditional `return 0` (recGbl.c:456), so that branch is DEAD and the
3864    ///   record always proceeds to its `switch (prec->simm)`.
3865    /// - `swait` reads SIML with a plain `dbGetLink` and simply never tests the
3866    ///   status (swaitRecord.c:402), so it proceeds too.
3867    ///
3868    /// Default: `false`.
3869    fn aborts_on_failed_siml_read(&self) -> bool {
3870        false
3871    }
3872
3873    /// The record joined (`true`) or left (`false`) the `SCAN="I/O Intr"` list.
3874    ///
3875    /// C parity: `dbScan.c::scanAdd` calls the record's device support
3876    /// `get_ioint_info(0, precord, &iopvt)` when SCAN becomes `I/O Intr`, and
3877    /// `scanDelete` calls `get_ioint_info(1, ...)` when it leaves. Device
3878    /// support that registers driver interrupt callbacks does so there —
3879    /// `asynRecord.c:582-597` registers/cancels its per-interface interrupt
3880    /// users in exactly those two calls, and clears its `gotValue` cell on the
3881    /// register.
3882    ///
3883    /// The port's device supports own their subscription through
3884    /// [`crate::server::device_support::DeviceSupport::io_intr_receiver`],
3885    /// which the framework asks for once at `iocInit`. This hook is the
3886    /// *runtime* half: a record whose own state decides what to subscribe to
3887    /// (asynRecord's PORT/IFACE/UI32MASK/REASON) must (re)register when the
3888    /// operator moves SCAN in or out of `I/O Intr` after `iocInit`.
3889    ///
3890    /// Called from the single owner of the SCAN transition
3891    /// (`RecordInstance::put_common_field*`, and the `scanAdd`-failure demotion
3892    /// in `ioc_app`) only when I/O Intr membership actually changes, so it is
3893    /// never invoked twice for the same state. Default: ignore.
3894    fn set_io_intr_scan(&mut self, _active: bool) {}
3895
3896    /// The link writes a C `special()` performs *itself*, inside `dbPut`.
3897    ///
3898    /// `special()` takes the record alone — it has no database handle — so a
3899    /// record whose C `special()` calls `dbPutLink` cannot make that write from
3900    /// `special()`. It queues the write here instead, and the put owner
3901    /// (`field_io`'s `dbPut` paths) drains the queue immediately after
3902    /// `special(field, true)` returns and executes the actions BEFORE the put's
3903    /// `pp(TRUE)` process cycle. That is C's order: `dbPutField` → `dbPut` →
3904    /// `dbPutSpecial(paddr, 1)` — which runs the `dbPutLink` to completion,
3905    /// target processing included — → `dbProcess`.
3906    ///
3907    /// The scaler is the case this exists for: `scalerRecord.c:623-624` puts
3908    /// `CNT` to `COUTP` inside `special()`, so a record wired to `.COUTP` is
3909    /// processed while the scaler is still IDLE, before the count is armed. The
3910    /// port deferred that write to the head of the CNT-triggered process cycle,
3911    /// where the target saw an already-COUNTING scaler.
3912    ///
3913    /// This is neither the record's "should the link fire" state nor part of the
3914    /// process cycle's action list: a `special()` put and a `process()` put to
3915    /// the same link (scaler `COUTP` again, `:463`) are independent writes.
3916    ///
3917    /// The drain is unconditional — it runs even when `special()` returned an
3918    /// error — so a queued action can never survive the put that queued it and
3919    /// fire against a later, unrelated put.
3920    ///
3921    /// Default: none (a `special()` that writes no link).
3922    fn take_special_actions(&mut self) -> Vec<ProcessAction> {
3923        Vec::new()
3924    }
3925
3926    /// A `special()`/`put_field` pass that ended on C's `prec->udf = FALSE`.
3927    ///
3928    /// UDF is a common field, so a record method cannot write it — the same
3929    /// wall [`Self::post_init_finalize_undef`] exists for at init time. This is
3930    /// the runtime half: `histogramRecord.c:354-364`'s `clear_histogram` ends
3931    /// on `prec->udf = FALSE` and is reached from two puts (the `CMD <= 1`
3932    /// SPC_CALC arm `:246-259` and the ULIM/LLIM SPC_RESET arm `:266-273`), so
3933    /// latching inside the clear rather than at each caller is what makes the
3934    /// two agree by construction.
3935    ///
3936    /// Drained unconditionally by the after-put owner, for
3937    /// [`Self::take_special_actions`]'s reason: C performs this assignment
3938    /// inside `special()`, before any status can divert the caller.
3939    ///
3940    /// Default: none (the record's puts never clear UDF).
3941    fn take_udf_clear(&mut self) -> bool {
3942        false
3943    }
3944
3945    /// Other fields whose monitors must be posted because a put to
3946    /// `put_field` changed them as a side effect, without driving a full
3947    /// process cycle.
3948    ///
3949    /// Mirrors the explicit `db_post_events` calls a C `special()` makes:
3950    /// e.g. `compressRecord.c::reset` (invoked on a `SPC_RESET` write to
3951    /// `RES`) posts `NUSE` and `VAL` even though `RES` is not `pp(TRUE)`
3952    /// and so does not process. The framework posts a `VALUE|LOG` monitor
3953    /// for each returned field after the put. Default: none.
3954    fn monitor_side_effect_fields(&self, _put_field: &str) -> &'static [&'static str] {
3955        &[]
3956    }
3957
3958    /// True iff the C `special()` for a put to `put_field` runs the record's
3959    /// own `monitor()` — whose first act is `recGblResetAlarms(prec)`, which
3960    /// commits `nsta`/`nsev` into `stat`/`sevr` and clears the born-UDF alarm.
3961    ///
3962    /// `compress` is the case this exists for: `compressRecord.c::special`
3963    /// (:385-388) calls `reset(prec); monitor(prec);` on any SPC_RESET write
3964    /// (RES/ALG/PBUF/BALG/N), and `compressRecord.c::monitor` (:103) opens with
3965    /// `recGblResetAlarms`. A born-UDF compress record therefore transitions to
3966    /// NO_ALARM the moment one of those fields is put, with no process cycle.
3967    ///
3968    /// The framework already posts the `db_post_events` half of that `monitor()`
3969    /// through [`Record::monitor_side_effect_fields`]; this hook is the
3970    /// `recGblResetAlarms` half. When it returns true, the put owner
3971    /// (`field_io`'s `dbPut` paths) runs `rec_gbl_reset_alarms` and posts the
3972    /// resulting STAT/SEVR/AMSG/ACKS transition. Default: false (a `special()`
3973    /// that does not run the record's `monitor()`).
3974    fn special_commits_alarms(&self, _put_field: &str) -> bool {
3975        false
3976    }
3977
3978    /// True iff the C `special()` for a put to `put_field` runs code that
3979    /// writes `stat`/`sevr` DIRECTLY (not `nsta`/`nsev`) with NO `monitor()` /
3980    /// `recGblResetAlarms` after it — so the write STICKS and a later `caget`
3981    /// observes it, unlike the process path where `recGblResetAlarms` erases it.
3982    ///
3983    /// `histogram` is the case this exists for: a `.SGNL` caput is C's SPC_MOD
3984    /// `special()` → `add_count`, which writes `prec->stat = SOFT_ALARM` on
3985    /// inverted limits (histogramRecord.c:329-334) and returns with no monitor.
3986    /// When true, the put owner (`field_io`) runs
3987    /// [`Record::check_alarms`] after the store — the same direct write the
3988    /// process path makes, but here it persists because no process cycle
3989    /// follows. Default: false (no direct special-path alarm write).
3990    fn special_checks_alarms(&self, _put_field: &str) -> bool {
3991        false
3992    }
3993
3994    /// Downcast to concrete type for device support init injection.
3995    /// Override in record types that need device support to inject state (e.g., MotorRecord).
3996    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
3997        None
3998    }
3999
4000    /// Whether processing this record should clear UDF.
4001    /// Override to return false for record types that don't produce a valid value every cycle.
4002    fn clears_udf(&self) -> bool {
4003        true
4004    }
4005
4006    /// Does this record's C `process()` re-derive `udf` after a DEVICE read
4007    /// that wrote VAL directly (C `return 2`)?
4008    ///
4009    /// True where the `else if (status == 2) status = 0;` fold happens BEFORE
4010    /// the UDF assignment, so a `2` arrives at it as a `0` and the record
4011    /// re-derives whatever the dset already wrote: `aiRecord.c:158-161` is the
4012    /// shape, and it is the majority.
4013    ///
4014    /// False for the five records that keep the assignment INSIDE
4015    /// `if (status == 0)` with the fold after it — `biRecord.c:55-60`,
4016    /// `mbbiRecord.c:61-86`, `mbbiDirectRecord.c:58-69` — or that have no fold
4017    /// at all — `longinRecord.c:58`, `int64inRecord.c:58`. There `udf` belongs
4018    /// to the dset, which is exactly where C puts it: `devBiSoft.c::readLocked`
4019    /// writes `prec->udf = FALSE` and returns 2, and `devBiDbState.c:67` does
4020    /// the same. The port's equivalent is
4021    /// [`DeviceUdf`](crate::server::device_support::DeviceUdf), applied before
4022    /// this rule runs.
4023    ///
4024    /// Not consulted for a soft-channel INP read: there the framework IS the
4025    /// dset, so it owns the clear the way `readLocked` does.
4026    fn rederives_udf_on_computed_read(&self) -> bool {
4027        true
4028    }
4029
4030    /// Whether this record's C `process()` clears UDF regardless of the read's
4031    /// status — i.e. even when `readValue` failed.
4032    ///
4033    /// Most records gate the clear on the read: `if (status == 0) prec->udf =
4034    /// FALSE;` inside `readValue`'s SIOL branch (`longinRecord.c:418`), so a
4035    /// failed simulation read leaves the record undefined. The array records do
4036    /// NOT: their `process()` clears UDF itself, unconditionally, on the line
4037    /// after `readValue` returns — `prec->pact = TRUE; prec->udf = FALSE;`
4038    /// (`waveformRecord.c:143-144`, `aaiRecord.c:173-174`) and
4039    /// `if (!pact) { prec->udf = FALSE; ... }` (`aaoRecord.c:164-165`) — whatever
4040    /// status came back, including the `-1` of an illegal SIMM. (waveform's
4041    /// readValue also clears UDF on a good SIOL read, `:353`, but the
4042    /// process-level clear runs after it and dominates.)
4043    ///
4044    /// Consulted by the simulation tail, which otherwise gates the clear on the
4045    /// SIOL fetch status. Default `false` — the status-gated majority.
4046    fn clears_udf_unconditionally(&self) -> bool {
4047        false
4048    }
4049
4050    /// Does this record's C `process()` assign `udf` on a cycle whose INP read
4051    /// FAILED?
4052    ///
4053    /// The scalar input records do not — the assignment sits inside
4054    /// `if (status == 0)`: `aiRecord.c:161`, `biRecord.c:136-140`,
4055    /// `mbbiRecord.c:168-174`, `mbbiDirectRecord.c:155-164`,
4056    /// `longinRecord.c:148`, `int64inRecord.c:144`. A broken link therefore
4057    /// leaves the record at whatever UDF it already had, and that is what makes
4058    /// the `if (prec->udf) recGblSetSevr(prec, UDF_ALARM, ...)` on the next line
4059    /// reachable at all (`mbbiDirectRecord.c:168-169`).
4060    ///
4061    /// The array records and `compress` do — `prec->udf = FALSE` runs after
4062    /// `readValue` whatever it returned (`waveformRecord.c:144`,
4063    /// `aaiRecord.c:174`, `aaoRecord.c:165`); `compressRecord.c:342-366` folds a
4064    /// failed `dbGetLink` into `status = 0` and clears anyway; and subArray's
4065    /// `prec->udf = !!status` (`subArrayRecord.c:148`) IS the status.
4066    ///
4067    /// Default `false` — the status-gated majority. Records whose
4068    /// [`Self::clears_udf`] is false never reach the question: their gate is
4069    /// already "a value was sourced this cycle", which a failed read fails.
4070    fn derives_udf_on_read_failure(&self) -> bool {
4071        false
4072    }
4073
4074    /// Whether this record type's `.dbd` declares an `INP` field at all.
4075    ///
4076    /// The port keeps INP on `CommonFields` for every record, which is right for
4077    /// the input records (`aiRecord.dbd.pod` … all declare it) but wrong for the
4078    /// ones whose C `.dbd` has no INP: C's dbd is the gate there, and
4079    /// `field(INP,...)` on such a record is a load error ("field not found"),
4080    /// leaving the record inert.
4081    ///
4082    /// `histogram` is the case this exists for: `histogramRecord.dbd.pod`
4083    /// declares NO INP — its DBF_INLINK is `SVL` (:212), read into SGNL by
4084    /// `devHistogramSoft.c` — so a histogram driven from INP is a database that
4085    /// no C IOC can load. Default `true`.
4086    ///
4087    /// This is the whole namespace gate, not just the loader's: in C the dbd
4088    /// also decides which `.FIELD` channels exist, so a histogram's INP is not
4089    /// resolvable at all (`dbgf HI.INP` → `PV 'HI.INP' not found`). Both
4090    /// [`RecordInstance::get_common_field`] and
4091    /// [`RecordInstance::put_common_field`] consult this, so the field cannot
4092    /// be readable on one route while refused on the other.
4093    ///
4094    /// [`RecordInstance::get_common_field`]: crate::server::record::RecordInstance::get_common_field
4095    /// [`RecordInstance::put_common_field`]: crate::server::record::RecordInstance::put_common_field
4096    fn declares_inp_link(&self) -> bool {
4097        true
4098    }
4099
4100    /// Process-time INP read when the INP link is CONSTANT (or unset) — the
4101    /// per-record exception to the load-once rule.
4102    ///
4103    /// Nearly every soft input device support skips a constant INP at process:
4104    /// `devWfSoft.c::read_wf` and `devAaiSoft.c::read_aai` open with
4105    /// `if (dbLinkIsConstant(pinp)) return 0;`, and the scalar ones call
4106    /// `dbGetLink`, whose `dbConstGetValue` (`dbConstLink.c:219-225`) writes
4107    /// nothing. The constant reaches such a record ONCE, at init, through
4108    /// `recGblInitConstantLink` / `dbLoadLinkArray`
4109    /// (`PvDatabase::rec_gbl_init_constant_inp`), so a client's caput to VAL is
4110    /// never clobbered by a re-delivered constant.
4111    ///
4112    /// `devSASoft.c::read_sa` (92-123) is the documented exception: it re-runs
4113    /// `dbLoadLinkArray` on a constant INP EVERY process, and on an EMPTY INP
4114    /// (`S_db_badField`) it sets `nRequest = prec->nord` and still subsets — so
4115    /// the record re-slices the client-written VAL by INDX each cycle. C draws
4116    /// that line at the device-support layer (`devSASoft` vs `devAaiSoft`), and
4117    /// so does this hook.
4118    ///
4119    /// Called by the framework on a soft-DTYP cycle whose INP is constant, with
4120    /// `value = Some(constant)` for a non-empty constant and `None` for an
4121    /// empty/unset INP. Returns whether the record consumed the input stage.
4122    /// Default `false` — the load-once rule.
4123    fn read_constant_inp(&mut self, _value: Option<EpicsValue>) -> bool {
4124        false
4125    }
4126
4127    /// Whether this record type raises UDF_ALARM at all.
4128    ///
4129    /// C has no framework-level UDF alarm: every record that reports one does
4130    /// it itself, with the guard at the top of its own `checkAlarms` —
4131    /// `if (prec->udf) { recGblSetSevr(prec, UDF_ALARM, prec->udfs); return; }`
4132    /// (`aiRecord.c:319-323`, `calcRecord.c:300-304`, …). A record whose
4133    /// support has no such guard NEVER reports UDF_ALARM, no matter what its
4134    /// `UDF` field says — `swaitRecord.c` is the case in point: its two only
4135    /// `udf` statements are `udf = FALSE` (`:411`, `:419`); it has no
4136    /// `checkAlarms` and never names UDF_ALARM.
4137    ///
4138    /// The framework raises UDF_ALARM centrally (`rec_gbl_check_udf`), so this
4139    /// hook is where a record type says C does not. Default `true` — the base
4140    /// analog/binary/string records all carry the guard.
4141    fn raises_udf_alarm(&self) -> bool {
4142        true
4143    }
4144
4145    /// Whether this record's C `checkAlarms` tests the UDF byte with
4146    /// `if (prec->udf == TRUE)` (EXACT-ONE) rather than `if (prec->udf)`
4147    /// (truthy). See [`crate::server::recgbl::udf_alarm_active`]: exact-one
4148    /// records (`boRecord.c:371`, `stringoutRecord.c:146`, `biRecord.c:225`,
4149    /// `busyRecord.c:337`) do NOT raise UDF_ALARM for a `udf` byte that is
4150    /// neither 0 nor 1.
4151    ///
4152    /// This only changes behavior for a record whose `udf` byte can actually
4153    /// hold such a value at `checkAlarms` time — one that does NOT re-derive
4154    /// `udf` every cycle ([`Record::clears_udf`] `== false`), reached via a
4155    /// direct `caput .UDF 255` (or `-1`, stored as `255` in the `DBF_UCHAR`
4156    /// field). For the re-deriving records (`clears_udf() == true`, e.g.
4157    /// `bi`/`busy`/the calc family) the byte is always 0/1 here, so exact-one
4158    /// and truthy agree and the flag is left at its default. Default `false`.
4159    fn udf_alarm_on_exact_one(&self) -> bool {
4160        false
4161    }
4162
4163    /// The severity C raises `UDF_ALARM` at, or `None` to use the record's
4164    /// live `UDFS` field.
4165    ///
4166    /// 19 of the 21 `UDF_ALARM` raise sites in `std/rec` pass `prec->udfs`;
4167    /// exactly two pass the literal `INVALID_ALARM` — `lsoRecord.c:117-118`
4168    /// and `mbbiDirectRecord.c:168-169`. Nothing derives that split: the
4169    /// Direct pair disagrees with itself (`mbboDirectRecord.c:191` passes
4170    /// `prec->udfs`), and so does the long-string pair (`lsi` raises no UDF
4171    /// alarm at all). It is a per-record fact, so it belongs on the record and
4172    /// not in a two-name branch inside `rec_gbl_check_udf`.
4173    ///
4174    /// Overriding this makes `UDFS` inert for that record, which is the point:
4175    /// `rec_gbl_set_sevr_msg` is strict-greater, so `UDFS=NO_ALARM` on an `lso`
4176    /// otherwise raised nothing at all where C reports INVALID/UDF.
4177    fn udf_alarm_severity(&self) -> Option<crate::server::record::AlarmSeverity> {
4178        None
4179    }
4180
4181    /// The alarm message C attaches when raising `UDF_ALARM`.
4182    ///
4183    /// Almost every base record raises UDF with plain
4184    /// `recGblSetSevr(prec, UDF_ALARM, prec->udfs)` — and `recGblSetSevr`
4185    /// forwards a NULL message to `recGblSetSevrMsg`, which sets
4186    /// `namsg[0] = '\0'` (`recGbl.c:249-251,258-261`). So the C amsg for a
4187    /// UDF record is EMPTY, and pvxs then serves the `"UDF"` condition
4188    /// string for `alarm.message` (`iocsource.cpp:230-236`). Default `""`
4189    /// models exactly that.
4190    ///
4191    /// The sole exception in base is `mbboDirectRecord.c:191`, which raises
4192    /// `recGblSetSevrMsg(prec, UDF_ALARM, prec->udfs, "UDFS")` — a bespoke
4193    /// literal. That record overrides this to `"UDFS"`.
4194    fn udf_alarm_message(&self) -> &'static str {
4195        ""
4196    }
4197
4198    /// Whether the record's current `VAL` is undefined (UDF must
4199    /// stay set).
4200    ///
4201    /// C parity: `aiRecord.c:285` / `calcRecord.c::checkAlarms` /
4202    /// `int64inRecord.c:144` clear `UDF` **only** when the computed /
4203    /// read value is valid — `if (status == 0)` and, for floating
4204    /// records, only when `VAL` is not NaN. The framework owns
4205    /// `common.udf`; it calls `clears_udf()` to decide whether this
4206    /// record type clears UDF at all, then this method to decide
4207    /// whether the *value produced this cycle* is actually defined.
4208    ///
4209    /// Default: a floating `VAL` that is NaN (e.g. a calc
4210    /// divide-by-zero, or a soft input whose link read failed and
4211    /// left VAL un-updated) is undefined; everything else is defined.
4212    /// A record whose `val()` yields `None` (no primary value) is
4213    /// also treated as undefined.
4214    fn value_is_undefined(&self) -> bool {
4215        match self.val() {
4216            Some(EpicsValue::Double(v)) => v.is_nan(),
4217            Some(EpicsValue::Float(v)) => v.is_nan(),
4218            Some(_) => false,
4219            None => true,
4220        }
4221    }
4222
4223    /// Per-record alarm hook — evaluate record-type-specific alarms
4224    /// (STATE / COS / analog limit / SOFT) and accumulate them into
4225    /// `nsta`/`nsev` via `recGblSetSevr`.
4226    ///
4227    /// The framework centralises the generic alarm machinery (UDF
4228    /// check, `recGblResetAlarms` transfer, MS/MSI/MSS link-alarm
4229    /// inheritance). The record-type-specific severity logic that C
4230    /// puts in each record's `checkAlarms()` belongs here so a record
4231    /// can raise its own alarms without the framework hardcoding a
4232    /// per-type `match` on `record_type()`.
4233    ///
4234    /// `common` is the record's [`crate::server::record::CommonFields`]; implementations
4235    /// raise alarms with [`crate::server::recgbl::rec_gbl_set_sevr`]
4236    /// / [`crate::server::recgbl::rec_gbl_set_sevr_msg`].
4237    ///
4238    /// Default: no-op — records that have not yet migrated their
4239    /// `checkAlarms` logic here are still covered by the framework's
4240    /// legacy centralised `evaluate_alarms` match.
4241    fn check_alarms(&mut self, _common: &mut crate::server::record::CommonFields) {}
4242
4243    /// Return multi-input link field pairs: (link_field, value_field).
4244    /// Override in calc, calcout, sel, sub to return INPA..INPL → A..L mappings.
4245    fn multi_input_links(&self) -> &'static [(&'static str, &'static str)] {
4246        &[]
4247    }
4248
4249    /// Deliver a multi-input link's scalar read to `field`, a value field of
4250    /// [`Self::multi_input_links`] — C's `dbGetLink(plink, DBR_DOUBLE,
4251    /// pvalue, 0, 0)` landing in `&prec->a` (`calcRecord.c:434`). The
4252    /// default routes through [`Self::put_field_internal`]; a record whose
4253    /// value fields are a plain `f64` block stores directly, since the
4254    /// by-name path resolves the field three times to reach the same store.
4255    fn put_multi_input_f64(&mut self, field: &'static str, value: f64) -> CaResult<()> {
4256        self.put_field_internal(field, EpicsValue::Double(value))
4257    }
4258
4259    /// C `dbFldDes::offset` for `field` (upper-case): a handle into the
4260    /// record's own storage that [`Self::get_slot_f64`] takes in place of
4261    /// the name, settled once per link target as `dbDbInitLink` settles a
4262    /// `dbAddr`. A type hands out a slot ONLY for a field the typed accessor
4263    /// answers; `None` (the default) sends every read of the field through
4264    /// [`Self::get_field`] by name.
4265    fn field_slot(&self, _field: &str) -> Option<FieldSlot> {
4266        None
4267    }
4268
4269    /// The field at `slot` as `dbGet(DBR_DOUBLE)` delivers it: what
4270    /// [`Self::get_field`] followed by the numeric funnel
4271    /// ([`EpicsValue::into_double`] / `get_convert_f64`) produces, without
4272    /// the value built and taken apart on the way. Defined for every slot
4273    /// [`Self::field_slot`] hands out; `None` for any other.
4274    fn get_slot_f64(&self, _slot: FieldSlot) -> Option<f64> {
4275        None
4276    }
4277
4278    /// Store a multi-input link's scalar read at `slot`, the
4279    /// [`Self::field_slot`] of one of [`Self::multi_input_links`]' value
4280    /// fields — C's `dbGetLink(plink, DBR_DOUBLE, &prec->a + i, ...)` store
4281    /// through the address instead of the name. `true` when stored; the
4282    /// default stores nothing and the fetch falls back to
4283    /// [`Self::put_multi_input_f64`]. A type that overrides this must store
4284    /// exactly what `put_multi_input_f64` stores for that field.
4285    fn put_slot_f64(&mut self, _slot: FieldSlot, _value: f64) -> bool {
4286        false
4287    }
4288
4289    /// The `(link_field, value_field)` pairs whose CONSTANT value this record's
4290    /// C `special()` RE-SEEDS on a runtime put to the link field —
4291    /// `recGblInitConstantLink(plink, DBF_DOUBLE, pvalue)` +
4292    /// `db_post_events(prec, pvalue, DBE_VALUE)` + `INAV = CON`
4293    /// (`calcoutRecord.c:373-378`, `sCalcoutRecord.c:513-518`,
4294    /// `aCalcoutRecord.c:533-538`).
4295    ///
4296    /// Without it a constant link is load-once dead state: `caput CO.INPB 7`
4297    /// stores the link text, the link layer then delivers nothing at process
4298    /// time (a constant link is not read), and `B` keeps its `.db`-load value
4299    /// forever.
4300    ///
4301    /// Declaring the pair is all a record does — the put path
4302    /// (`database::field_io::special_after_put`, the one `special(field, true)`
4303    /// owner) runs the load through
4304    /// [`crate::server::record::rec_gbl_init_constant_link`], the same owner the
4305    /// init seed uses, and posts the value field. A record cannot declare the
4306    /// pair and forget to implement the re-seed.
4307    ///
4308    /// Default: EMPTY — and that is the correct answer for every record whose C
4309    /// `special()` does NOT re-seed. `recGblInitConstantLink` appears inside a
4310    /// `special()` body in exactly FOUR record types across base and synApps
4311    /// calc — calcout, sCalcout, aCalcout, transform. Everywhere else (calc,
4312    /// sub, sel, aSub, seq, fanout, dfanout, swait, sseq, ao/bo/longout/…) it is
4313    /// called only from `init_record`, so those records seed once and never
4314    /// again, and they inherit this empty default.
4315    ///
4316    /// The overriding records list only the inputs C actually re-seeds:
4317    /// sCalcout/aCalcout guard with `fieldIndex <= INPL` (their string/array
4318    /// inputs are init-load only) and transform with
4319    /// `fieldIndex < transformRecordOUTA` (its OUT half is not an input).
4320    fn special_reseed_input_links(&self) -> &[(&'static str, &'static str)] {
4321        &[]
4322    }
4323
4324    /// The event mask of the `db_post_events` call in that record's `special()`
4325    /// re-seed arm — the C call sites do not agree:
4326    ///
4327    ///  * calcout (`calcoutRecord.c:377`), sCalcout (`sCalcoutRecord.c:516`),
4328    ///    aCalcout (`aCalcoutRecord.c:536`)
4329    ///    post a literal `DBE_VALUE`.
4330    ///  * transform (`transformRecord.c:719`) posts `DBE_VALUE | DBE_LOG`.
4331    ///
4332    /// Default: `DBE_VALUE`, the majority shape. Only consulted for the fields
4333    /// named by [`Self::special_reseed_input_links`].
4334    fn special_reseed_post_mask(&self) -> crate::server::recgbl::EventMask {
4335        crate::server::recgbl::EventMask::VALUE
4336    }
4337
4338    /// Every CONSTANT input link this record seeds ONCE, at `init_record` —
4339    /// the record's own `recGblInitConstantLink` / `dbLoadLinkArray` table,
4340    /// transcribed from its C.
4341    ///
4342    /// This is the OTHER half of the one rule the link layer enforces: a
4343    /// constant link delivers NOTHING at process time (`dbConstGetValue`,
4344    /// `dbConstLink.c:219-225`), so the ONLY way a `field(INPA,"5")` ever
4345    /// reaches `A` is this table, applied by the single init-seed owner
4346    /// `crate::server::database::PvDatabase::rec_gbl_init_constant_links`.
4347    /// A record that fetches an input link but declares no seed for it (swait,
4348    /// whose C uses `recDynLink` and seeds nothing) simply never sees the
4349    /// constant — which is what its C does.
4350    ///
4351    /// Default: none.
4352    fn constant_init_links(&self) -> Vec<ConstantInitLink> {
4353        Vec::new()
4354    }
4355
4356    /// The link field this record loads into its long-string VAL at init through
4357    /// C's `dbLoadLinkLS` — `"DOL"` for `lso` (`lsoRecord.c:82`), `"INP"` for
4358    /// `lsi` (its soft device support, `devLsiSoft.c:24`). `loadLS` is a lset
4359    /// entry of its own, so this is a SEPARATE table from
4360    /// [`Self::constant_init_links`], not a variant of it; the same init-seed
4361    /// owner runs both.
4362    ///
4363    /// Default: none — a record with no long-string VAL has no `loadLS` seed.
4364    fn constant_ls_link(&self) -> Option<&'static str> {
4365        None
4366    }
4367
4368    /// Apply the [`Self::constant_ls_link`] load, and return the resulting LEN.
4369    /// The record clamps the text at its own `SIZV` and runs C's init tail
4370    /// (`if (prec->len) { strcpy(prec->oval, prec->val); prec->olen = prec->len; }`,
4371    /// lsoRecord.c:92-95 / lsiRecord.c:85-88); the owner turns a non-zero LEN
4372    /// into `udf = FALSE`.
4373    fn apply_ls_load(&mut self, _load: crate::server::record::LsLoad) -> u32 {
4374        0
4375    }
4376
4377    /// Whether this record's CONSTANT input links deliver their value on
4378    /// EVERY process cycle instead of only at init.
4379    ///
4380    /// `false` for every record that fetches with a plain `dbGetLink`. `printf`
4381    /// is the one exception in the whole database: its `GET_PRINT` macro
4382    /// (`printfRecord.c:49-52`) tests `dbLinkIsConstant` and re-runs
4383    /// `recGblInitConstantLink` on every `doPrintf`, so a constant INP0..9
4384    /// really is re-read each cycle.
4385    fn constant_inputs_deliver_at_process(&self) -> bool {
4386        false
4387    }
4388
4389    /// The subset of [`Self::multi_input_links`] the framework should
4390    /// actually fetch this cycle, given an optional externally-resolved
4391    /// selector index (sel's NVL→SELN value, or `None` when no NVL link
4392    /// drove it). Default `None` = fetch every input link.
4393    ///
4394    /// C `selRecord.c::fetch_values` (lines 421-432) fetches ONLY `INP[SELN]`
4395    /// in `Specified` mode and all inputs otherwise; sel returns
4396    /// `Some(vec![INP[SELN]])` so the non-selected inputs are never read and
4397    /// raise no monitors or link-alarm SEVR.
4398    fn select_input_links(
4399        &self,
4400        _selector: Option<u16>,
4401    ) -> Option<&'static [(&'static str, &'static str)]> {
4402        None
4403    }
4404
4405    /// Whether [`Self::select_input_links`] can EVER narrow this type's input
4406    /// list — `sel` in `Specified` mode, `swait` while simulating. The answer
4407    /// is a property of the type, not of its state, and the cycle carries it
4408    /// in its [`ProcessPlan`](crate::server::record::RecordInstance), so the
4409    /// other forty-odd types no longer take the record lock to be told `None`.
4410    fn narrows_input_links(&self) -> bool {
4411        false
4412    }
4413
4414    /// A `SIMM != NO` cycle substitutes only this record's INPUT STAGE — the
4415    /// rest of its `process()` still runs.
4416    ///
4417    /// C's SIML/SIMM/SIOL group has three shapes, and this hook names the third:
4418    ///
4419    /// * `readValue` (ai, bi, longin, …): the simulated read replaces the device
4420    ///   read, which is the whole of the record's input; the framework performs
4421    ///   the SIOL read and completes the cycle itself.
4422    /// * `writeValue` (ao, bo, …): the simulated write replaces the device write
4423    ///   at the END of the body, so the body runs and only the output is
4424    ///   redirected to SIOL.
4425    /// * swait (`swaitRecord.c:402-422`): the simulated read replaces
4426    ///   `fetch_values()` **and** `calcPerform()` and nothing else — VAL comes
4427    ///   from SIOL through SVAL, and the OOPT switch, `execOutput`, the monitors
4428    ///   and the forward link all still run from the record's own `process()`.
4429    ///
4430    /// A record that returns `true` gets, on a simulated cycle: SIMM resolved
4431    /// from SIML, SIOL read into SVAL, `VAL = SVAL` and `UDF = FALSE` when that
4432    /// read succeeded (C `:417-420` — a failed read changes neither), SIMM_ALARM
4433    /// raised at SIMS *before* the body so it maximizes against whatever the
4434    /// body raises (C `:421`), no input-link fetch, and
4435    /// [`Self::set_simulation_active`] pushed before `process()`.
4436    fn simulation_substitutes_input_stage(&self) -> bool {
4437        false
4438    }
4439
4440    /// Land the scalar a simulated cycle read from SIOL (through SVAL, where
4441    /// the record has one) — C `readValue`'s assignment plus whatever the
4442    /// record's `process()` body then does with it, under the same
4443    /// `status == 0` gate the framework applies before calling this.
4444    ///
4445    /// The base records assign the value straight to VAL — `longinRecord.c:417`
4446    /// `prec->val = prec->sval;` — which is the default (`set_val`).
4447    ///
4448    /// `histogram` does NOT: `histogramRecord.c:385-386` lands it in SGNL
4449    /// (`prec->sgnl = prec->sval;`), and `process()` (`:218-219`,
4450    /// `if (status == 0) add_count(prec);`) bins that signal into the VAL
4451    /// bin-count array. Its VAL is the array, so a `set_val` of the scalar
4452    /// no-ops and the simulated record is frozen. It overrides.
4453    fn land_simulated_value(&mut self, value: EpicsValue) -> CaResult<()> {
4454        self.set_val(value)
4455    }
4456
4457    /// Whether the record's C `switch (prec->simm)` carries a `default:` arm
4458    /// that REFUSES a SIMM value outside its own menu:
4459    ///
4460    /// ```c
4461    /// default:
4462    ///     recGblSetSevr(prec, SOFT_ALARM, INVALID_ALARM);
4463    ///     status = -1;
4464    /// ```
4465    ///
4466    /// Every record in the framework has it — all 21 base records
4467    /// (`longinRecord.c:436-438` and its twins; `aaiRecord.c:381-384` writes the
4468    /// same arm as an `else if (prec->simm != menuYesNoNO)`) and `busy`
4469    /// (`busyRecord.c:409-413`, the `else` of its YES test).
4470    ///
4471    /// `swait` is the sole exception: `swaitRecord.c:407-422` is a plain
4472    /// `if (pwait->simm == menuYesNoNO) { … } else { /* SIMULATION MODE */ … }`,
4473    /// so every non-NO value — legal or not — simulates. It overrides to
4474    /// `false`.
4475    ///
4476    /// Consumed by [`resolve_sim_mode`](crate::server::recgbl::simm::resolve_sim_mode),
4477    /// the single owner of the SIMM dispatch.
4478    fn rejects_illegal_sim_mode(&self) -> bool {
4479        true
4480    }
4481
4482    /// Whether this record's `readValue` raises `SIMM_ALARM` AFTER its SIOL
4483    /// read instead of before it.
4484    ///
4485    /// `recGblSetSevr` is strict-greater, so the ORDER of the SIMM raise
4486    /// against the LINK_ALARM/INVALID that a failed `dbGetLink` raises inside
4487    /// itself (`dbLink.c:319` `setLinkAlarm`, reached from `:336`) decides an
4488    /// equal-severity tie. With `SIMS = INVALID` and a broken SIOL the record
4489    /// publishes whichever of the two was raised FIRST.
4490    ///
4491    /// The base records raise it first, at the top of the `case menuYesNoYES:`
4492    /// arm — `longinRecord.c:414` then `:416` — so they publish
4493    /// `STAT = SIMM_ALARM`. That is the default, and it is what the generic
4494    /// raise in `check_simulation_mode` performs.
4495    ///
4496    /// `mca` is the other order: `mcaRecord.c:1118` reads SIOL and only then
4497    /// `:1129` runs `recGblSetSevr(pmca, SIMM_ALARM, pmca->sims)`, past the
4498    /// `if/else` on SIMM, so the LINK_ALARM raised inside the read WINS the tie
4499    /// and C publishes `STAT = LINK_ALARM` with `AMSG = "field SIOL"`.
4500    /// (Read against `mca` at `687d563`; that tree records no pin.)
4501    ///
4502    /// `swait` is the same order but does not answer here — it has its own
4503    /// `input_stage` path in `check_simulation_mode`, which already reads
4504    /// before it raises (`swaitRecord.c:416` then `:421`).
4505    ///
4506    /// Declared per record rather than special-cased at the raise site so a
4507    /// record type carries its own C ordering; overriding to `true` is the
4508    /// whole opt-in.
4509    fn raises_simm_after_read(&self) -> bool {
4510        false
4511    }
4512
4513    /// This cycle's simulation state, pushed by the framework before
4514    /// `process()` — the twin of [`Self::set_fetch_gate_failed`], and only for a
4515    /// record that declares [`Self::simulation_substitutes_input_stage`].
4516    ///
4517    /// It is pushed on EVERY cycle of such a record (`false` included), so the
4518    /// flag cannot survive the cycle it belongs to. The record uses it to skip
4519    /// exactly what C's simulation branch skips — for swait, `fetch_values()`
4520    /// (through [`Self::select_input_links`]) and `calcPerform()`.
4521    fn set_simulation_active(&mut self, _active: bool) {}
4522
4523    /// How C's `fetch_values()` for this record type reacts to a link read
4524    /// that fails. Drives the framework's [`Self::multi_input_links`] fetch
4525    /// loop; see [`InputFetchPolicy`]. Default: [`InputFetchPolicy::ReadAll`].
4526    ///
4527    /// It governs the [`Self::multi_input_links`] loop ONLY.
4528    /// [`Self::string_input_links`] is C's *second*, separately-gated fetch
4529    /// loop and never participates in this policy.
4530    fn input_fetch_policy(&self) -> InputFetchPolicy {
4531        InputFetchPolicy::ReadAll
4532    }
4533
4534    /// String-valued input links: `(link_field, value_field)` pairs read as
4535    /// DBR_STRING, C `sCalcoutRecord.c::fetch_values` (890-941) — the SECOND
4536    /// loop of that function, over `INAA`..`INLL` → `AA`..`LL`:
4537    ///
4538    /// ```c
4539    /// for (i=0, plink=&pcalc->inaa, psvalue=pcalc->strs; i<STRING_MAX_FIELDS; ...) {
4540    ///     ...
4541    ///     if (((field_type==DBR_CHAR) || (field_type==DBR_UCHAR)) && nelm>1) {
4542    ///         status = dbGetLink(plink, field_type, tmpstr, 0, &nelm);
4543    ///         epicsStrSnPrintEscaped(*psvalue, STRING_SIZE-1, tmpstr, strlen(tmpstr));
4544    ///     } else {
4545    ///         status = dbGetLink(plink, DBR_STRING, *psvalue, 0, 0);
4546    ///     }
4547    ///     if (!RTN_SUCCESS(status))
4548    ///         epicsSnprintf(*psvalue, STRING_SIZE-1, "%s:fetch(%s) failed", pcalc->name, sFldnames[i]);
4549    /// }
4550    /// return(0);
4551    /// ```
4552    ///
4553    /// Three properties this loop does NOT share with [`Self::multi_input_links`],
4554    /// which is why it is a separate list rather than more entries in that one:
4555    ///
4556    /// 1. **Ungated.** It ends in `return(0)` — a failing string link never
4557    ///    makes `fetch_values` non-zero, so it cannot suppress the record body.
4558    ///    The record's single [`Self::input_fetch_policy`] describes the numeric
4559    ///    loop (`AbortOnFirstFailure` for scalcout) and cannot also describe this
4560    ///    one.
4561    /// 2. **A failed read still writes the field** — with the diagnostic text
4562    ///    `"<record>:fetch(<FIELD>) failed"`, not with the previous value.
4563    /// 3. **A `DBF_CHAR`/`DBF_UCHAR` array source is read as text**, C-escaped
4564    ///    (`epicsStrSnPrintEscaped`), which is how a >40-char string reaches a
4565    ///    string calc; every other source type converts as DBR_STRING.
4566    ///
4567    /// The value is delivered through [`Self::put_field_internal`], so the
4568    /// target field's declared `DbFieldType` performs the final coercion.
4569    fn string_input_links(&self) -> &'static [(&'static str, &'static str)] {
4570        &[]
4571    }
4572
4573    /// Input links this record reads at OUTPUT time instead of during the
4574    /// input-fetch phase: `(link_name_field, value_field)` pairs. The framework
4575    /// reads each configured link immediately before the OUT write, and ONLY on
4576    /// a cycle where the output actually fires ([`Self::should_output`] and no
4577    /// IVOA veto), then writes the value into `value_field` via
4578    /// [`Self::put_field`]; a failed read leaves the field alone.
4579    ///
4580    /// C `swaitRecord.c::execOutput` (763-772) does exactly this for `DOL`:
4581    ///
4582    /// ```c
4583    /// if (pwait->dopt) {                    /* DOPT = "Use DOL" */
4584    ///     if (!pwait->dolv) {               /* DOL PV connected */
4585    ///         oldDold = pwait->dold;
4586    ///         recDynLinkGet(&pcbst->caLinkStruct[DOL_INDEX], &(pwait->dold), ...);
4587    ///         if (pwait->dold != oldDold)
4588    ///             db_post_events(pwait, &pcbst->pwait->dold, DBE_VALUE);
4589    ///     }
4590    ///     outValue = pwait->dold;
4591    /// }
4592    /// ```
4593    ///
4594    /// The timing is the point: the value written out is the one the link holds
4595    /// at output time (ODLY delay-end included), and a cycle whose output does
4596    /// not fire never refreshes — or posts — the field. Fetching such a link in
4597    /// the normal input phase would do both. Default: none.
4598    fn output_time_input_links(&self) -> &'static [(&'static str, &'static str)] {
4599        &[]
4600    }
4601
4602    /// The value the framework writes to the OUT link. The single owner of
4603    /// "what goes out", shared by the soft-OUT write, the async-completion
4604    /// write and the simulated SIOL redirect.
4605    ///
4606    /// The default is the C staging convention: the record computed the output
4607    /// into `OVAL` during `process()` (`calcout`/`ao`/`bo`/...), falling back to
4608    /// `VAL` for records that have no `OVAL`. Override when the record's C
4609    /// composes the output value at *output* time rather than staging it — e.g.
4610    /// swait, whose `execOutput` (`swaitRecord.c:763-772`) picks between `VAL`
4611    /// and the just-fetched `DOLD` and whose `OVAL` field is C's "Old Value"
4612    /// (the previous VAL, used only by the OOPT test), not an output stage.
4613    fn output_link_value(&self) -> Option<EpicsValue> {
4614        self.get_field("OVAL").or_else(|| self.val())
4615    }
4616
4617    /// Return multi-output link field pairs: (link_field, value_field).
4618    /// Override in transform to return OUTA..OUTP → A..P mappings.
4619    fn multi_output_links(&self) -> &[(&'static str, &'static str)] {
4620        &[]
4621    }
4622
4623    /// Does this TYPE declare a [`Self::multi_output_links`] table at all,
4624    /// whether or not this cycle drives it?
4625    ///
4626    /// `multi_output_links` is a per-CYCLE answer: `scalcout` withholds its
4627    /// pair while its own put-callback is in flight, `aSub` while the
4628    /// subroutine returned non-zero, `acalcout` and `epid` while the cycle
4629    /// computed nothing. Every one of them therefore answers `&[]` at
4630    /// construction and on most cycles, so the engine cannot settle "does
4631    /// this type have multi-outputs" by asking it. This is that settled
4632    /// answer, carried by `ProcessPlan`, so a record type with no such table
4633    /// never takes its own lock once a cycle to be told so.
4634    ///
4635    /// MUST be `true` for every type whose `multi_output_links` can ever
4636    /// return a non-empty slice. `dispatch_multi_output_values` debug-asserts
4637    /// the implication on every cycle it declines, so a type that adds the
4638    /// table and forgets this fails the test suite rather than losing its
4639    /// outputs in release.
4640    fn declares_multi_output_links(&self) -> bool {
4641        false
4642    }
4643
4644    /// The record's C soft device support write-buffer switch for a
4645    /// multi-output pair: given the pair's staged value (the value field
4646    /// named by [`Self::multi_output_links`]) and the RESOLVED TARGET
4647    /// metadata, return the buffer C would actually put.
4648    ///
4649    /// C's soft device supports do not blindly write one field: they read
4650    /// the target's DBF type and element count and pick a buffer from them —
4651    /// `devaCalcoutSoft.c::write_acalcout` (75-87) picks
4652    /// `nelm == 1 ? &scalar : array`, `devsCalcoutSoft.c::write_scalcout`
4653    /// (66-144) routes a string-class target to the computed string, a
4654    /// `CHAR`/`UCHAR` array to the string's bytes, and everything else to
4655    /// the numeric. The framework resolves the target
4656    /// ([`PvDatabase::resolve_out_target`](crate::server::database::PvDatabase))
4657    /// and hands it here; the record reproduces its device support's switch.
4658    ///
4659    /// Default: write the staged value unchanged (no device-support switch).
4660    fn multi_output_buffer(
4661        &self,
4662        link_field: &str,
4663        staged: EpicsValue,
4664        target: &OutTarget,
4665    ) -> EpicsValue {
4666        let _ = (link_field, target);
4667        staged
4668    }
4669
4670    /// The buffer to put on an OUT link the record drives itself, chosen from
4671    /// the RESOLVED TARGET — the no-staged-value sibling of
4672    /// [`Self::multi_output_buffer`].
4673    ///
4674    /// C `sseqRecord.c::processCallback` (708-795) is the case: the value a
4675    /// step forwards is not one field but a *switch on the destination*
4676    /// (`dbGetLinkDBFtype(&lnk)` / `dbGetNelements(&lnk)`), taken at fire
4677    /// time — the string view `s` for a string-class target, the double view
4678    /// `dov` for a numeric one, `s`'s bytes for a `CHAR`/`UCHAR` array, and
4679    /// **no put at all** for a target whose type does not resolve (C's
4680    /// `default: break`). `None` is that no-put: the caller issues no write.
4681    ///
4682    /// The record calls this ITSELF, on the target
4683    /// [`ProcessAction::ResolveOutTarget`] handed it before `process()` — one
4684    /// decision, made before anything is issued, because the same switch
4685    /// decides more than the buffer (sseq: whether a `WAITn` put-callback goes
4686    /// out, and hence whether `WTGn` is raised). See
4687    /// [`Self::set_resolved_out_target`].
4688    ///
4689    /// Default: `None`. Only a record that resolves its own OUT target reaches
4690    /// this, and it must override.
4691    fn typed_output_buffer(&self, link_field: &str, target: &OutTarget) -> Option<EpicsValue> {
4692        let _ = (link_field, target);
4693        None
4694    }
4695
4696    /// Receive the RESOLVED target of an OUT link the record asked to have
4697    /// resolved before this cycle's `process()`
4698    /// ([`ProcessAction::ResolveOutTarget`]).
4699    ///
4700    /// C resolves an OUT link's DBF class OUTSIDE the put — `checkLinks` caches
4701    /// it in the record (`sseqRecord.c:203-240`) — so `processCallback` can make
4702    /// ONE decision from it: which view goes on the wire, AND whether a
4703    /// put-callback is issued (hence whether `waiting` is raised). Its
4704    /// `default:` arm (`:790`) does neither. A record that learns the class only
4705    /// from inside the framework's put path cannot keep those two halves
4706    /// together: it has to raise `waiting` first and find out afterwards that no
4707    /// put was made. This hook is that cached class — the record decides, then
4708    /// acts.
4709    ///
4710    /// Default: no-op. Only a record that emits the action reaches this.
4711    fn set_resolved_out_target(&mut self, link_field: &str, target: OutTarget) {
4712        let _ = (link_field, target);
4713    }
4714
4715    /// The `dbrType` this record's framework-run input read on `link_field`
4716    /// asks the SOURCE for — the READ twin of [`Self::typed_output_buffer`],
4717    /// and C's `dbGetLink` second argument. Consulted by every framework
4718    /// fetch path: the pre-input [`ProcessAction::ReadDbLink`] stage, the
4719    /// single-INP soft fetch, the closed-loop DOL fetch, the multi-input
4720    /// loop and the SIOL simulation read.
4721    ///
4722    /// `source` is the far end of the input link as C's
4723    /// `dbGetLinkDBFtype`/`dbGetNelements` report it (the same lset accessors
4724    /// the OUT side uses — sseq asks them of `dol` at `sseqRecord.c:641` and of
4725    /// `lnk` at `:709`), resolved by the framework
4726    /// ([`PvDatabase::resolve_out_target`](crate::server::database::PvDatabase)).
4727    ///
4728    /// `None` means C's `default: break` — **no read at all**, the arm a source
4729    /// class the record's switch does not name falls to (an unresolvable /
4730    /// constant / disconnected source, and sseq's un-cased `DBF_INT64`). The
4731    /// link is left untouched and no LINK alarm is raised, exactly as C's
4732    /// skipped `dbGetLink` call leaves `status` alone.
4733    ///
4734    /// Default: [`LinkReadAs::Native`] — the source's native value, coerced at
4735    /// the target field's own put boundary.
4736    fn input_link_request(&self, link_field: &str) -> InputLinkRequest {
4737        let _ = link_field;
4738        InputLinkRequest::As(LinkReadAs::Native)
4739    }
4740
4741    /// Whether [`Self::input_link_request`] and
4742    /// [`Self::input_link_failure_is_inert`] are settled by the link field
4743    /// alone, so the process plan may ask them once for every declared link
4744    /// when the record is built. `false` for the two types whose answer reads
4745    /// instance state — aSub (`FTA..FTU`) and printf (`FMT`) — which the fetch
4746    /// loop then asks per link, per cycle, through the vtable.
4747    fn input_link_answers_fixed_at_type(&self) -> bool {
4748        true
4749    }
4750
4751    /// The same request for a record whose [`Self::input_link_request`]
4752    /// answered [`InputLinkRequest::FromSource`] — C's `dbGetLinkLS` switch on
4753    /// `dbGetLinkDBFtype`, and sseq's on the same accessor. Reached ONLY
4754    /// through that answer, so a record that never gives it needs no
4755    /// implementation.
4756    fn input_link_read_as_from_source(
4757        &self,
4758        link_field: &str,
4759        source: &OutTarget,
4760    ) -> Option<LinkReadAs> {
4761        let _ = (link_field, source);
4762        Some(LinkReadAs::Native)
4763    }
4764
4765    /// Return the name of the output event (`OEVT`) to post this cycle, or
4766    /// `None`. The event-subsystem twin of the OUT write: a downstream
4767    /// `SCAN="Event"` / `EVNT="<name>"` record is woken each time the record
4768    /// drives output. Mirrors C `calcout`/`sCalcout`/`aCalcout` `execOutput`,
4769    /// which calls `postEvent(epvt)` / `post_event(oevt)` immediately after
4770    /// `writeValue` in every OUT-driving branch.
4771    ///
4772    /// The override MUST fold in the record's own output-fire decision
4773    /// (`should_output()` for `calcout`; the cached OOPT/calc-fail/ODLY
4774    /// decision for `sCalcout`/`aCalcout`) and return `None` when output did
4775    /// not fire or when `OEVT` is unset. The framework adds the only gate the
4776    /// record cannot see — the IVOA `Don't_drive` veto on an INVALID cycle —
4777    /// so the post fires on exactly the cycles the OUT write does. Numeric
4778    /// `OEVT` (DBF_USHORT) stringifies to match the `EVNT` ingest; a string
4779    /// `OEVT` (DBF_STRING) is the event name verbatim.
4780    fn output_event(&self) -> Option<String> {
4781        None
4782    }
4783
4784    /// Internal field write that bypasses read-only checks.
4785    /// Used by the framework to write values from ReadDbLink actions
4786    /// into fields that are normally read-only (e.g., epid.CVAL).
4787    /// Default implementation delegates to put_field().
4788    ///
4789    /// On the `ReadDbLink` path this is also where a pvalink NTEnum
4790    /// carrier ([`EpicsValue::EnumWithChoices`]) is resolved. The
4791    /// dbrType-blind link resolver produces it for an NTEnum source;
4792    /// pvxs `pvaGetValue` (`pvxs/ioc/pvalink_lset.cpp:330-360`) picks
4793    /// label-vs-index by the TARGET field's dbrType — only a DBR_STRING
4794    /// target gets the `choices[index]` label, every other type takes
4795    /// the numeric index. Route it through [`EpicsValue::convert_to`]
4796    /// (the single value-coercion owner) against the target field's
4797    /// `db_field_type`, so the transient carrier is consumed before any
4798    /// record `put_field` / storage / wire path can see it. The
4799    /// single-INP→VAL apply path reaches the same `convert_to` via
4800    /// `set_val`'s `TypeMismatch` auto-coerce.
4801    fn put_field_internal(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
4802        put_field_internal_default(self, name, value)
4803    }
4804
4805    /// Return pre-process actions (ReadDbLink) that the framework should
4806    /// execute BEFORE calling process(). This is called once per cycle.
4807    /// Default returns empty. Override in records that need link reads
4808    /// to be available during process().
4809    fn pre_process_actions(&mut self) -> ProcessActions {
4810        ProcessActions::new()
4811    }
4812
4813    /// Return actions the framework must execute BEFORE the input-link
4814    /// (`multi_input_links`, INP -> value-field) fetch for this cycle.
4815    ///
4816    /// This is strictly earlier than [`Self::pre_process_actions`]: the
4817    /// framework resolves input links *before* it calls
4818    /// `pre_process_actions`, so an action that must affect what an
4819    /// input link reads cannot be expressed there.
4820    ///
4821    /// The motivating case is the epid record's `devEpidSoftCallback`
4822    /// DB-type TRIG link: C `devEpidSoftCallback.c:120-132` writes the
4823    /// readback-trigger link with `dbPutLink` — which synchronously
4824    /// processes the triggered source chain — and only *then*
4825    /// (`devEpidSoftCallback.c:151`) does `dbGetLink(&pepid->inp, ...)`
4826    /// read `CVAL`. The trigger write therefore has to land before the
4827    /// `INP -> CVAL` fetch, in the same process pass.
4828    ///
4829    /// Called once per cycle, while a record write lock is held; the
4830    /// framework executes the returned actions (currently `WriteDbLink`
4831    /// and `ReadDbLink`) and then performs the input-link fetch.
4832    /// Default returns empty.
4833    fn pre_input_link_actions(&mut self) -> ProcessActions {
4834        ProcessActions::new()
4835    }
4836
4837    /// Called by the framework immediately before `process()` to push a
4838    /// read-only snapshot of framework-owned [`crate::server::record::CommonFields`] state
4839    /// ([`ProcessContext`]) that the record's `process()` needs to see.
4840    ///
4841    /// The framework owns `RecordInstance.common`; a record `process()`
4842    /// only gets `&mut self`. C records read `dbCommon` directly — e.g.
4843    /// `epidRecord.c:195` checks `pepid->udf` at the top of `process()`,
4844    /// `timestampRecord.c:90` branches on `ptimestamp->tse`. This hook
4845    /// is the controlled equivalent: a record that needs `udf`/`phas`/
4846    /// `tse`/`tsel` during `process()` overrides this to stash the
4847    /// values into its own fields.
4848    ///
4849    /// Additive, framework-set-hook pattern (same shape as
4850    /// [`Record::set_device_did_compute`]). Default: ignore — most
4851    /// records never need common state during `process()`.
4852    fn set_process_context(&mut self, _ctx: &ProcessContext) {}
4853
4854    /// Called by the framework immediately before `process()` to say whether
4855    /// this cycle is the record's OWN scheduled re-entry — the
4856    /// [`ProcessAction::ReprocessAfter`] timer firing, or a put-notify
4857    /// completion — rather than a fresh put / scan / forward-link process.
4858    ///
4859    /// C hands records this distinction for free: `callbackRequestDelayed`
4860    /// dispatches to a callback function of the record's own, never to
4861    /// `process()`. A port that models the timer with `ReprocessAfter` has
4862    /// both events arriving at the same entry point, so a record whose C
4863    /// original keeps the two paths separate — throttle's `delayFuncCallback`
4864    /// -> `valuePut` versus `process()` -> `enterValue`
4865    /// (`throttleRecord.c:518-613`) — cannot route the cycle without knowing
4866    /// which one it is. The framework already knows; this hook is what it
4867    /// tells the record, so the record never has to infer it from a clock
4868    /// reading (which a mid-flight interval change silently falsifies).
4869    ///
4870    /// Records that hold PACT across their delay (calcout / scalcout /
4871    /// acalcout / swait / sseq ODLY) already disambiguate through PACT and do
4872    /// not need this. Additive, framework-set-hook pattern (same shape as
4873    /// [`Record::set_process_context`]). Default: ignore.
4874    fn set_process_continuation(&mut self, _continuation: bool) {}
4875
4876    /// Called by the framework once for every [`ProcessAction::WriteDbLink`]
4877    /// this record emitted, reporting the value it carried and whether the
4878    /// put failed — the port's stand-in for the `dbPutLink` return C reads
4879    /// inline.
4880    ///
4881    /// A record whose C original derives a field from that return needs it:
4882    /// throttle's `STS` is `throttleSTS_SUC` only on success and its `SENT`
4883    /// advances only then (`throttleRecord.c:564-575`). Without this the
4884    /// record can only commit its own intent, which reports Success for a put
4885    /// that never landed. The framework raises the LINK/INVALID alarm either
4886    /// way; this is the record-owned half.
4887    ///
4888    /// Called after the put, while the cycle's monitor snapshot is still
4889    /// ahead, so a field set here is posted by this cycle. Every emitted
4890    /// action reports exactly once, an unresolvable link included (reported
4891    /// as failed). Additive, framework-set-hook pattern (same shape as
4892    /// [`Record::set_process_context`]). Default: ignore — most records
4893    /// derive nothing from the put's result, as their C originals do not.
4894    fn set_out_link_write_status(
4895        &mut self,
4896        _link_field: &'static str,
4897        _value: &EpicsValue,
4898        _failed: bool,
4899    ) {
4900    }
4901
4902    /// Called once by the framework when the record is registered
4903    /// (`add_record`), delivering the record its own canonical name plus a
4904    /// cycle-free [`crate::server::database::AsyncDbHandle`] for driving
4905    /// async-side updates from OUTSIDE a `process()` cycle.
4906    ///
4907    /// The handle wraps a `Weak` reference to the database, so a record
4908    /// that stashes it creates no ownership cycle (the database owns the
4909    /// record; a stored strong handle would leak it). It is the controlled
4910    /// equivalent of C device support capturing `precord` plus the
4911    /// dbCommon scan lock for an out-of-band `db_post_events` /
4912    /// `callbackRequest`: e.g. the asyn TRACE/exception callback posts
4913    /// trace-flag fields immediately from the driver thread, and AQR
4914    /// cancels a queued I/O re-entry — neither happens inside `process()`.
4915    ///
4916    /// The in-band counterpart for a record's *own* process cycle is the
4917    /// completion-driven [`ProcessAction`] family
4918    /// ([`ProcessAction::WriteDbLinkNotify`],
4919    /// [`ProcessAction::CancelReprocess`],
4920    /// [`ProcessAction::ReprocessAfter`]); this hook exists for the
4921    /// out-of-band path that has no `process()` return to ride on.
4922    ///
4923    /// Additive, framework-set-hook pattern (same shape as
4924    /// [`Self::set_process_context`]). Default: ignore — most records do
4925    /// no out-of-band async posting.
4926    fn set_async_context(&mut self, _name: String, _db: crate::server::database::AsyncDbHandle) {}
4927
4928    /// Framework init hook: called once at record load *after* the common
4929    /// link fields (`INP`/`OUT`/`FLNK`/...) have been resolved and the
4930    /// `init_record` passes have run, with the record's resolved
4931    /// [`CommonFields`](crate::server::record::CommonFields).
4932    ///
4933    /// This is the seam for records that classify their links into status
4934    /// diagnostics at init the way C `init_record` does (e.g. calcout's
4935    /// `INAV..INUV`/`OUTV` `menu(calcoutINAV)` checkLinks loop): a record's
4936    /// *common* link strings (`OUT` is a common field, not a record field)
4937    /// are invisible to [`Self::set_async_context`] — which runs at
4938    /// `add_record`, *before* the common fields are applied — and to
4939    /// `init_record`, which carries no `CommonFields`. The record captures
4940    /// whichever common links it needs here so a passive, never-processed
4941    /// record already exposes its link status. Records whose links are all
4942    /// record-owned (e.g. sseq DOLn/LNKn) do not need this hook.
4943    ///
4944    /// Additive, framework-set-hook pattern. Default: ignore.
4945    fn init_links(&mut self, _common: &crate::server::record::CommonFields) {}
4946
4947    /// Called by the framework before process() to indicate whether device
4948    /// support's read() already performed the record's compute step.
4949    /// Override in records that have a built-in compute (e.g., epid PID)
4950    /// to skip it when device support already ran it.
4951    /// Default: ignore.
4952    fn set_device_did_compute(&mut self, _did_compute: bool) {}
4953
4954    /// Whether this record has a raw-to-engineering (`RVAL → VAL`)
4955    /// `convert()` step that must be skipped on a `Soft Channel` input.
4956    ///
4957    /// C `devAiSoft.c:65` `read_ai` (and the other soft-channel input
4958    /// `read_xxx`) always returns 2 ("don't convert"), so `aiRecord.c`'s
4959    /// `if (status==0) convert(prec)` is bypassed for a `Soft Channel`
4960    /// input record. The framework expresses this by calling
4961    /// [`Record::set_device_did_compute`]`(true)` on the record before
4962    /// `process()`.
4963    ///
4964    /// This hook exists so the framework only suppresses `convert()` —
4965    /// NOT a record's entire built-in compute. Records like `epid` also
4966    /// override `set_device_did_compute` but interpret it as "skip the
4967    /// whole compute step" (the PID loop); those records have no
4968    /// `RVAL → VAL` convert and MUST keep the default `false` so a
4969    /// `Soft Channel` `epid` still runs `do_pid()` in `process()`.
4970    ///
4971    /// Default `false`: a record is only opted into the soft-channel
4972    /// convert-skip when it explicitly returns `true`.
4973    fn soft_channel_skips_convert(&self) -> bool {
4974        false
4975    }
4976
4977    /// Whether this output record's forward `VAL → RVAL` `convert()` must be
4978    /// SKIPPED on a process cycle where VAL is still undefined (`UDF != 0`) and
4979    /// no value source ran.
4980    ///
4981    /// C's output records take an early `goto CONTINUE` before `convert()` when
4982    /// the record is undefined and no value was sourced this cycle:
4983    ///
4984    /// ```c
4985    /// /* mbboRecord.c:199-217 (and ao/bo/mbboDirect alike) */
4986    /// if (!pact) {
4987    ///     if (!dbLinkIsConstant(&prec->dol) && omsl == closed_loop) {
4988    ///         ... prec->val = <DOL>;          /* value sourced -> udf cleared */
4989    ///     }
4990    ///     else if (prec->udf) {
4991    ///         recGblSetSevr(prec, UDF_ALARM, prec->udfs);
4992    ///         goto CONTINUE;                  /* skip udf=FALSE AND convert() */
4993    ///     }
4994    ///     prec->udf = FALSE;
4995    ///     convert(prec);                      /* VAL -> RVAL */
4996    /// }
4997    /// ```
4998    ///
4999    /// So a `caput REC.RVAL 1` on a bare `record(mbbo,"M"){}` (UDF still 1, no
5000    /// VAL put, no closed-loop DOL) leaves RVAL at the client value: `convert`
5001    /// never runs to recompute `RVAL = VAL(=0)`. Verified on the compiled
5002    /// softIoc — bare RVAL put reads back the put value; after a VAL put clears
5003    /// UDF, the next RVAL put IS overwritten by `convert`.
5004    ///
5005    /// The framework consults this before `process()`: an opted-in output record
5006    /// with `UDF != 0` and no value source this cycle is told
5007    /// [`Self::set_device_did_compute`]`(true)` so its `process()` skips the
5008    /// forward convert. A VAL put (UDF cleared in `field_io`) or a closed-loop
5009    /// DOL fetch (UDF cleared at the DOL-apply site) leaves `UDF == 0`, so the
5010    /// convert runs exactly as C's fall-through does.
5011    ///
5012    /// Default `false`. The rest of C's output family (ao/bo/mbboDirect) shares
5013    /// the same `goto CONTINUE`; they are a separate change and stay opted out
5014    /// here.
5015    fn skips_forward_convert_when_undefined(&self) -> bool {
5016        false
5017    }
5018
5019    /// C `mbboRecord.c:210-221` / `mbboDirectRecord.c:190-202` — the same
5020    /// `else if (prec->udf) goto CONTINUE` that skips the forward `convert()`
5021    /// ALSO jumps past the pre-output `recGblGetTimeStampSimm` call. So a soft
5022    /// (synchronous) mbbo/mbboDirect that is still UNDEFINED never stamps TIME
5023    /// on the first-pass output stage: the only other stamp after `CONTINUE:`
5024    /// is guarded by `if (pact)` (mbboRecord.c:256-258), which fires on
5025    /// ASYNCHRONOUS completion re-entry only. A sync UDF record therefore keeps
5026    /// TIME at the EPICS epoch ("never processed") until a VAL put clears UDF.
5027    ///
5028    /// Contrast ao/bo/longout/int64out/stringout: their `if (!pact)` block
5029    /// calls `recGblGetTimeStampSimm` UNCONDITIONALLY (aoRecord.c:192,
5030    /// boRecord.c:215), so they stamp even while undefined and do NOT opt in.
5031    ///
5032    /// The framework consults this at the synchronous output-stage stamp
5033    /// (`processing.rs` `process_record_with_links_inner`, the pre-output
5034    /// `apply_timestamp`): an opted-in record with `UDF != 0` skips that stamp,
5035    /// mirroring C's `goto CONTINUE`. The async-completion stamp
5036    /// (`complete_async_record_inner`) stays unconditional, matching C's
5037    /// `if (pact)` re-stamp on async devices.
5038    ///
5039    /// This is a SEPARATE hook from [`Self::skips_forward_convert_when_undefined`]:
5040    /// mbboDirect's VAL is bit-derived and does NOT opt into the convert-skip,
5041    /// yet it DOES share this timestamp-skip. Do not conflate the two.
5042    ///
5043    /// Default `false`. Only mbbo/mbboDirect carry the `goto CONTINUE`
5044    /// timestamp-skip in C; every other record stays opted out.
5045    fn skips_timestamp_when_undefined(&self) -> bool {
5046        false
5047    }
5048}
5049
5050/// The body of [`Record::put_field_internal`] — the framework's internal write
5051/// path — as a free function, so a record that needs to observe an internal
5052/// write can WRAP it instead of re-implementing it.
5053///
5054/// A record overriding `put_field_internal` and ending in `self.put_field(..)`
5055/// silently drops the coercion below for every field it does not special-case.
5056/// Calling this instead keeps the one owner of that coercion.
5057///
5058/// Input-link / internal delivery coerces the source to the target field's
5059/// stored type before `put_field`, mirroring C `dbGetLink(DBF_<target>)`: the
5060/// link layer converts any numeric source to the requested type, so a record's
5061/// typed `put_field` arm never sees a mismatched type. This covers every
5062/// `ReadDbLink` target by construction (e.g. a `compress` INP from a `DBF_LONG`
5063/// record delivers a `Long`/`LongArray` that must become `Double`/`DoubleArray`
5064/// for the Double-only VAL arm, which otherwise drops it and never advances the
5065/// buffer). An `EnumWithChoices` carrier is always collapsed to a bare index by
5066/// `convert_to`, even when the target is already `Enum`.
5067/// Render a put request in the DESTINATION field's shape — C `dbPut`'s value
5068/// branch (`dbAccess.c:1350-1367`, tag `R7.0.10`) expressed as a value
5069/// transform, and the single owner of both of its directions.
5070///
5071/// C never asks a record to accept a shape. `dbPut` copies `nRequest` elements
5072/// through `dbPutConvertRoutine` into the field, so a ONE-element request over
5073/// a `special(SPC_DBADDR)` destination lands element 0 of a BUFFER (which is
5074/// how pvxs's `putScalar` — `doDbPut(chan, dbr, &value, 1)`,
5075/// `iocsource.cpp:599-601` — writes a one-element channel), and a
5076/// MULTI-element request over a scalar destination is clamped to `no_elements`
5077/// at `:1360` and lands element 0 of a SCALAR. This port hands the record one
5078/// `EpicsValue` whose variant its `put_field` arm matches on, so C's two arms
5079/// have to be a rendering, and one every client put shares: the two PVA
5080/// servers each carried the scalar-to-buffer half themselves, and a third
5081/// caller would have needed its own copy.
5082///
5083/// This is the `dbPut` contract only — [`dbput_coerce_value`] is the entry
5084/// that applies it. Internal delivery has [`link_value_in_field_shape`], and
5085/// the two are separate functions rather than one with a flag because they
5086/// disagree about the same request: one sample into a `compress` VAL is a
5087/// one-element buffer to `dbPut` and a `compress_scalar` sample to a link.
5088///
5089/// An `EnumWithChoices` carrier is left alone in both — it is a transient link
5090/// payload, not a stored value, and `convert_to` collapses it to a bare index.
5091/// `shaped_destination` carries the rest of the shared rule.
5092pub fn put_value_in_field_shape<R: Record + ?Sized>(
5093    record: &R,
5094    field: &str,
5095    value: EpicsValue,
5096) -> EpicsValue {
5097    let Some(dest_is_array) = shaped_destination(record, field, &value) else {
5098        return value;
5099    };
5100    match (dest_is_array, value.is_array()) {
5101        // C's SCALAR arm: the clamp at `:1360` leaves one element.
5102        (false, true) => value.first_element().unwrap_or(value),
5103        // C's ARRAY arm at `nRequest == 1` — except into a long-string field.
5104        // C's `cvt_dbaddr` re-types those five to `DBF_STRING`
5105        // (`lsiRecord.c:127-134`), so their conversion row is
5106        // `putStringString`, a byte copy of the text, and this port carries
5107        // that text as the value itself — `lsi.VAL` as a `String`, `printf.VAL`
5108        // as a `CharArray`. Wrapping a `String` request into a one-element
5109        // `StringArray` hands that row an array and the record a variant no
5110        // `put_field` arm takes.
5111        (true, false) if !is_long_string_field(record, field) => {
5112            one_element_buffer(&value).unwrap_or(value)
5113        }
5114        _ => value,
5115    }
5116}
5117
5118/// `field` is one of the record's long-string fields (`lsi`/`lso` VAL+OVAL,
5119/// `printf` VAL), matched as [`Record::long_string_fields`] specifies.
5120fn is_long_string_field<R: Record + ?Sized>(record: &R, field: &str) -> bool {
5121    record
5122        .long_string_fields()
5123        .iter()
5124        .any(|f| f.eq_ignore_ascii_case(field))
5125}
5126
5127/// The same question for INTERNAL DELIVERY — an input link or device support
5128/// handing a record a value — which has only C's scalar arm.
5129///
5130/// C's link layer asks for exactly ONE element (`dbGetLink(..., nRequest =
5131/// NULL)`), so `dbGet` converts the field at offset 0 and the record sees a
5132/// scalar: a waveform INP into an `ai.VAL` lands `wf[0]` rather than being
5133/// dropped by the typed `put_field` arm it does not match.
5134///
5135/// There is no array arm to render into, and that is the whole reason this is
5136/// a second entry rather than a flag on [`put_value_in_field_shape`]: internal
5137/// delivery is not a `dbPut`. A `compress` INP delivering ONE sample means
5138/// `compress_scalar` — the running `cvb` accumulator — and rendering it as a
5139/// one-element buffer runs `push_array`'s array algorithm instead, which
5140/// writes the N clamp back into the record.
5141pub fn link_value_in_field_shape<R: Record + ?Sized>(
5142    record: &R,
5143    field: &str,
5144    value: EpicsValue,
5145) -> EpicsValue {
5146    match shaped_destination(record, field, &value) {
5147        Some(false) if value.is_array() => value.first_element().unwrap_or(value),
5148        _ => value,
5149    }
5150}
5151
5152/// `Some(dest_is_array)` when `field` has a shape this row may render into,
5153/// `None` when it must be left alone.
5154///
5155/// The destination test is the field's CURRENT VALUE SHAPE, not
5156/// [`FieldDeclaration::field_is_dbaddr`]. `mbbo.VAL` is `special(SPC_DBADDR)`
5157/// and stored as a scalar; wrapping it would put a one-element array in front
5158/// of the menu row that owns that put.
5159///
5160/// The exempt shape is a `CharArray` into a `DBF_STRING` field: that is the
5161/// dbChannel `$` char view of a string field, decoded by `convert_to`.
5162fn shaped_destination<R: Record + ?Sized>(
5163    record: &R,
5164    field: &str,
5165    value: &EpicsValue,
5166) -> Option<bool> {
5167    let target = record
5168        .get_field(field)
5169        .map(|v| v.db_field_type())
5170        .or_else(|| super::record_instance::declared_field_type_of(record, field));
5171    if matches!(value, EpicsValue::CharArray(_)) && target == Some(DbFieldType::String) {
5172        return None;
5173    }
5174    Some(record.get_field(field).is_some_and(|v| v.is_array()))
5175}
5176
5177/// The client `dbPut` entry to the write-side converter: C `dbPut`'s value
5178/// branch in full — the destination-shape arm
5179/// ([`put_value_in_field_shape`]) and then the `dbPutConvertRoutine` row for
5180/// the request that arm produced ([`coerce_put_value`]).
5181///
5182/// Separate from `coerce_put_value` because the two ways a value enters a
5183/// record field have different contracts, and one function that renders the
5184/// shape for both means two things by context. Only a client `dbPut` gets the
5185/// array arm; internal delivery calls `coerce_put_value` with a request whose
5186/// shape [`link_value_in_field_shape`] already settled, and
5187/// `RecordInstance::put_declared_override` calls it for a destination that has
5188/// no shape at all (a shadow cell for a field the record does not serve).
5189///
5190/// It is UNCONDITIONAL. Its caller must not skip it when the request already
5191/// carries the destination's DBF: that skip is what let a scalar reach a
5192/// buffer field unrendered, so that `histogram.VAL` had to grow a scalar arm
5193/// of its own to accept what `dbPut` converts for every other array field.
5194pub fn dbput_coerce_value<R: Record + ?Sized>(
5195    record: &R,
5196    field: &str,
5197    target: DbFieldType,
5198    value: EpicsValue,
5199) -> CaResult<Converted> {
5200    let value = put_value_in_field_shape(record, field, value);
5201    coerce_put_value(record, field, target, value)
5202}
5203
5204/// `value` as the one-element buffer of its OWN element type. The element type
5205/// is not this function's business — [`coerce_put_value`] runs C's
5206/// `dbPutConvertRoutine` row over the request afterwards, array to array.
5207fn one_element_buffer(value: &EpicsValue) -> Option<EpicsValue> {
5208    Some(match value {
5209        EpicsValue::Short(v) => EpicsValue::ShortArray(vec![*v]),
5210        EpicsValue::Float(v) => EpicsValue::FloatArray(vec![*v]),
5211        EpicsValue::Enum(v) => EpicsValue::EnumArray(vec![*v]),
5212        EpicsValue::Double(v) => EpicsValue::DoubleArray(vec![*v]),
5213        EpicsValue::Long(v) => EpicsValue::LongArray(vec![*v]),
5214        EpicsValue::Int64(v) => EpicsValue::Int64Array(vec![*v]),
5215        EpicsValue::UInt64(v) => EpicsValue::UInt64Array(vec![*v]),
5216        EpicsValue::UShort(v) => EpicsValue::UShortArray(vec![*v]),
5217        EpicsValue::ULong(v) => EpicsValue::ULongArray(vec![*v]),
5218        EpicsValue::UChar(v) => EpicsValue::UCharArray(vec![*v]),
5219        EpicsValue::Char(v) => EpicsValue::CharArray(vec![*v]),
5220        EpicsValue::String(v) => EpicsValue::StringArray(vec![v.clone()]),
5221        // Not a stored value; `convert_to` collapses it to a bare index.
5222        EpicsValue::EnumWithChoices { .. } => return None,
5223        // Already a buffer.
5224        _ => return None,
5225    })
5226}
5227
5228pub fn put_field_internal_default<R: Record + ?Sized>(
5229    record: &mut R,
5230    name: &str,
5231    value: EpicsValue,
5232) -> CaResult<()> {
5233    // Coerce to the type the record STORES, not the type it SERVES. The two are
5234    // the same for most fields, but a `menu()` field is declared `DBF_MENU` and
5235    // served as `DBR_ENUM` with its choices (`promote_menu_value`) while the
5236    // record stores the bare choice index as a `Short` — and `put_field`'s arms
5237    // match on what is stored. Coercing to the served type would hand every
5238    // `put_field` an `Enum` its `Short` arm cannot match. This is the inverse of
5239    // `promote_menu_value`, and asking the record what it holds keeps the rule
5240    // uniform instead of special-casing menus here.
5241    //
5242    // The `.dbd` type is the fallback for a field the record cannot currently
5243    // produce a value for (an uninitialised array, a port-internal field).
5244    let target_type = record
5245        .get_field(name)
5246        .map(|v| v.db_field_type())
5247        .or_else(|| crate::server::record::record_instance::declared_field_type_of(record, name));
5248    let value = link_value_in_field_shape(&*record, name, value);
5249    let is_enum_carrier = matches!(value, EpicsValue::EnumWithChoices { .. });
5250    let value = match target_type {
5251        // A String target routes through the converter even on a type match: C's
5252        // `putStringString` truncates to `field_size - 1` (see `coerce_put_value`).
5253        Some(target)
5254            if is_enum_carrier
5255                || ((value.db_field_type() != target || target == DbFieldType::String)
5256                    && !value.is_empty_array()) =>
5257        {
5258            match coerce_put_value(record, name, target, value)? {
5259                Converted::Stored(v) => v,
5260                // C's converter returned success without storing: the field
5261                // keeps its old value and the put still succeeds.
5262                Converted::Unchanged => return Ok(()),
5263            }
5264        }
5265        // Carrier with no known target field: collapse to a bare index (the prior
5266        // fallback) rather than letting it reach storage.
5267        None if is_enum_carrier => value.convert_to(DbFieldType::Long),
5268        _ => value,
5269    };
5270    record.put_field(name, value)
5271}
5272
5273/// C's `putString*` row when the destination is an ARRAY — `Some` when this row
5274/// owns the put, `None` when it does not apply and the scalar rows below do.
5275///
5276/// `dbPut` reaches it for every `special(SPC_DBADDR)` field regardless of
5277/// `nRequest` (`dbAccess.c:1350`), so a one-element string put is this row too;
5278/// keeping the two counts on one owner is what stops them drifting apart.
5279///
5280/// The two array destinations whose row is a byte copy rather than a parse — a
5281/// long-string field (`lsi`/`lso`/`printf` VAL, `DBF_STRING` in their
5282/// `cvt_dbaddr`) and an `FTVL=STRING` buffer, whose `NumericField::of` is
5283/// `None` — still belong to this row; it hands them the text unchanged for
5284/// their record to store.
5285fn put_string_array_row<R: Record + ?Sized>(
5286    record: &R,
5287    field: &str,
5288    target: DbFieldType,
5289    value: &EpicsValue,
5290) -> CaResult<Option<Converted>> {
5291    let texts: &[crate::types::PvString] = match value {
5292        EpicsValue::String(s) => std::slice::from_ref(s),
5293        EpicsValue::StringArray(a) => a,
5294        _ => return Ok(None),
5295    };
5296    if !record.get_field(field).is_some_and(|v| v.is_array()) {
5297        return Ok(None);
5298    }
5299    // `putStringString` is a byte copy, and the record stores the text: hand the
5300    // value over unchanged rather than falling through to the SCALAR numeric
5301    // row below, which would parse `printf.VAL`'s `"7"` into the number 7.
5302    if record
5303        .long_string_fields()
5304        .iter()
5305        .any(|f| f.eq_ignore_ascii_case(field))
5306    {
5307        return Ok(Some(Converted::Stored(value.clone())));
5308    }
5309    let Some(numeric) = c_parse::NumericField::of(target) else {
5310        return Ok(Some(Converted::Stored(value.clone())));
5311    };
5312    c_parse::put_string_elements(field, numeric, texts).map(Some)
5313}
5314
5315/// Coerce a written value to a field's stored type — the single owner of C
5316/// `dbConvert.c`'s `dbFastPutConvertRoutine[dbrType][field_type]` table, shared
5317/// by the two paths a value can enter a record's field through: a client
5318/// `dbPut` (`crate::server::database::field_io`) and an internal link /
5319/// device-support delivery ([`put_field_internal_default`]).
5320///
5321/// Every `DBR_STRING` row of C's put table is a converter that can FAIL, and
5322/// none of them is `EpicsValue::convert_to`:
5323///
5324/// * `DBF_MENU` → `putStringMenu` — exact label, else an index below `nChoice`
5325///   ([`crate::server::record::resolve_menu_field_string`]).
5326/// * `DBF_ENUM` → `putStringEnum` — the record's state strings, else an index
5327///   below `no_str` ([`crate::server::record::resolve_enum_state_string`]).
5328/// * `DBF_STRING` → `putStringString` — a byte copy, the one row that cannot
5329///   fail.
5330/// * every numeric width → `putStringChar` … `putStringDouble`, i.e.
5331///   `epicsParse*`, which refuses the put on overflow and on unparseable text
5332///   ([`c_parse::put_string`]).
5333///
5334/// `convert_to` cannot express any of the failures — it is field-blind and
5335/// total, mapping unparseable text to `0` and an out-of-range number to the
5336/// nearest representable one. That is how `caput MY:VALVE Open` became a silent
5337/// no-op that drove `VAL` to state 0, and how `caput REC.PREC 32768` — which the
5338/// compiled softIoc REFUSES — stored 32767.
5339///
5340/// This is the TYPE row alone. The request's shape is settled before it
5341/// arrives, by whichever entry the value came through
5342/// ([`dbput_coerce_value`] for a client `dbPut`,
5343/// [`link_value_in_field_shape`] for internal delivery), so nothing here
5344/// renders a destination shape that one of those two contracts would disagree
5345/// with.
5346///
5347/// An ARRAY destination takes the same rule through
5348/// [`c_parse::put_string_elements`], which runs `putString*` over every element
5349/// of the request. It is run HERE rather than delegated to the record, because
5350/// delegation made the row a convention nothing enforced: `waveform` ran it
5351/// while `compress.VAL` and `histogram.VAL` answered `TypeMismatch` to a put the
5352/// compiled softIoc accepts, and the `StringArray` half of the same row never
5353/// reached this function at all — it fell through to the total `convert_to`,
5354/// which stored `0.0` for text `epicsParseFloat64` refuses.
5355///
5356/// Two array destinations are outside the numeric row, and both are C's own
5357/// distinction rather than a carve-out here: a LONG-STRING field
5358/// ([`Record::long_string_fields`]) is `DBF_STRING` in its `cvt_dbaddr`, so its
5359/// row is `putStringString`, a byte copy; and an `FTVL=STRING` buffer is the
5360/// same copy per element. Those keep the string and their records store it. A
5361/// string reaching a char buffer as its BYTES is the DBR_CHAR row (`caput -S`),
5362/// which arrives as a `CharArray` and needs no conversion.
5363pub fn coerce_put_value<R: Record + ?Sized>(
5364    record: &R,
5365    field: &str,
5366    target: DbFieldType,
5367    value: EpicsValue,
5368) -> CaResult<Converted> {
5369    // The ARRAY row first, so both counts of the SAME C routine have one owner:
5370    // a scalar `String` and a `StringArray` into an array destination are both
5371    // `dbPutConvertRoutine[DBR_STRING][target]` over `nRequest` elements
5372    // (`dbAccess.c:1350` takes that arm for every `special(SPC_DBADDR)` field,
5373    // whatever the count). Menu/DTYP/enum fields are never array destinations,
5374    // so nothing below is shadowed.
5375    if let Some(converted) = put_string_array_row(record, field, target, &value)? {
5376        return Ok(converted);
5377    }
5378    if let EpicsValue::String(s) = &value {
5379        // DTYP (DBF_DEVICE) validates against the record type's FULL device
5380        // menu — static `device()` lines PLUS runtime-contributed device
5381        // support — the same set the read/announce path exposes via
5382        // `RecordInstance::device_choices`. `menu_choices_of`'s DTYP branch
5383        // returns only the static half, so a contributed device-support name
5384        // (asyn's `asynInt32`, scaler-rs's `Asyn Scaler`, ...) would wrongly
5385        // fail this put even though a client can read it in the DTYP choices.
5386        // Resolve DTYP against the merged menu to keep put and read symmetric.
5387        if field.eq_ignore_ascii_case("DTYP") {
5388            let choices = super::merged_device_menu(record.record_type());
5389            if !choices.is_empty() {
5390                return super::resolve_menu_field_string(
5391                    field,
5392                    &choices,
5393                    target,
5394                    &s.as_str_lossy(),
5395                )
5396                .map(Converted::Stored);
5397            }
5398            // No device menu declared or contributed for this record type: fall
5399            // through to the generic handling below (unchanged behavior).
5400        } else if let Some(choices) = super::record_instance::menu_choices_of(record, field) {
5401            return super::resolve_menu_field_string(field, choices, target, &s.as_str_lossy())
5402                .map(Converted::Stored);
5403        }
5404        if target == DbFieldType::Enum {
5405            return super::resolve_enum_state_string(
5406                field,
5407                record.enum_state_strings().as_deref(),
5408                s,
5409            )
5410            .map(Converted::Stored);
5411        }
5412        if let Some(numeric) = c_parse::NumericField::of(target) {
5413            return c_parse::put_string(field, numeric, &s.as_str_lossy());
5414        }
5415        if target == DbFieldType::String {
5416            // C `putStringString` (dbConvert.c:916-925): `strncpy(pdst, psrc,
5417            // field_size); pdst[field_size-1] = 0` — the DBF_STRING put
5418            // truncates to `field_size - 1` bytes. The row is NOT a no-op even
5419            // for a String source, so it must run even when source and stored
5420            // type match (the two gates that call this converter skip it on a
5421            // type match; both route a String target here regardless). The CA
5422            // wire already caps a DBR_STRING at `MAX_STRING_SIZE - 1` (39), so
5423            // this only bites a field whose `.dbd` `size(N)` is under 40 —
5424            // dbCommon `ASG` `size(29)` → 28, and the like.
5425            return Ok(Converted::Stored(EpicsValue::String(
5426                cap_string_to_field_size(record, field, s),
5427            )));
5428        }
5429    }
5430    // The float/double rows of C's DBR_STRING put column render through the
5431    // record's `get_precision`, seeded 6: `putFloatString`/`putDoubleString`
5432    // (`dbConvert.c:1558`/`:1600`) for the array path,
5433    // `cvt_f_st`/`cvt_d_st` (`dbFastLinkConv.c:1216`/`:1333`) for the scalar
5434    // one that `dbAccess.c:1391` actually takes. `convert_to` cannot express
5435    // it — it is field-blind by contract, and precision is the record's — so
5436    // the row belongs here, next to the menu and enum rows it also cannot
5437    // express. Rendering through the GET direction's own converter is what
5438    // keeps `dbtgf REC.DESC` and a `caput` of the same number agreeing.
5439    if target == DbFieldType::String
5440        && let Some(rendered) =
5441            crate::types::codec::dbr_string_at_precision(&value, put_string_precision(record))
5442    {
5443        return Ok(Converted::Stored(rendered));
5444    }
5445    Ok(Converted::Stored(value.convert_to(target)))
5446}
5447
5448/// C's `prset->get_precision` answer for a **`DBF_STRING`** destination — the
5449/// precision `putFloatString`/`putDoubleString` render a numeric put with.
5450///
5451/// The seed is 6 and only `get_precision` overwrites it
5452/// (`dbConvert.c:1562-1568`, `dbFastLinkConv.c:1220-1228`). For a STRING field
5453/// the shared tail is a no-op: `recGblGetPrec`'s switch has no `DBF_STRING`
5454/// case (`recGbl.c:141-142`), so whatever the body seeded survives. And no
5455/// `get_precision` in base or in the ported modules names a `DBF_STRING` field
5456/// in its own switch, so the answer is per RECORD rather than per field: it is
5457/// `PREC` for every body that seeds `*precision = prec->prec` ahead of the tail
5458/// — ai, ao, aai, aao, aSub, calc, calcout, compress, dfanout, sel, seq, sub,
5459/// subArray, waveform, sCalcout, aCalcout, epid, mca, motor, scaler, sseq,
5460/// swait, throttle, transform. The two ported types that do not seed:
5461///
5462/// * `histogram` — a switch with no seed and no case a dbCommon field reaches,
5463///   so the caller's 6 arrives at `cvtDoubleToString`
5464///   (`histogramRecord.c:420-438`).
5465/// * `asyn` — `*precision = 0;` before the tail (`asynRecord.c::get_precision`).
5466///
5467/// `bo`, `busy`, `mbbiDirect` and `mbboDirect` are unseeded too and need no arm:
5468/// none of them declares `PREC`, so the fallback is already C's 6. No record
5469/// type declares `PREC` while NULLing `get_precision` (checked across base and
5470/// the ported modules), which is what lets "has a PREC field" stand in for
5471/// "supplies the slot" without a second table to keep in step.
5472///
5473/// A negative PREC is not clamped, for the reason
5474/// [`crate::types::codec`]'s GET side spells out: C hands the `long` to
5475/// `cvtDoubleToString`'s `epicsUInt16` parameter and the conversion
5476/// reinterprets it.
5477fn put_string_precision<R: Record + ?Sized>(record: &R) -> u16 {
5478    match record.record_type() {
5479        "histogram" => 6,
5480        "asyn" => 0,
5481        _ => record
5482            .get_field("PREC")
5483            .and_then(|v| v.as_int_i64())
5484            .map_or(6, |p| p as i16 as u16),
5485    }
5486}
5487
5488/// C `putStringString`'s truncation: a `DBF_STRING` field stores at most
5489/// `field_size - 1` bytes (its `.dbd` `size(N)` less the forced NUL). A field
5490/// with no declared size (`0` — a Tier 3 hand table, or a field with no
5491/// declaration) is left uncapped beyond the wire's own `MAX_STRING_SIZE - 1`.
5492fn cap_string_to_field_size<R: Record + ?Sized>(record: &R, field: &str, s: &PvString) -> PvString {
5493    match super::record_instance::field_desc_of(record, field) {
5494        Some(desc) if desc.size > 0 => {
5495            let cap = (desc.size as usize).saturating_sub(1);
5496            let bytes = s.as_bytes();
5497            if bytes.len() > cap {
5498                PvString::from_bytes(bytes[..cap].to_vec())
5499            } else {
5500                s.clone()
5501            }
5502        }
5503        _ => s.clone(),
5504    }
5505}
5506
5507/// Subroutine function type for `sub`/`aSub` records.
5508///
5509/// The return value is the subroutine's C `long` status
5510/// (`subRecord.c::do_sub` / `aSubRecord.c::do_sub`): `< 0` raises
5511/// `SOFT_ALARM` at the record's `BRSV` severity, and for `aSub` the status
5512/// is published as `VAL` (`aSubRecord.c:224`). Return `Ok(0)` for the
5513/// normal no-alarm path. `Err(..)` is reserved for an infrastructure
5514/// failure inside the closure (e.g. a field write error), which aborts
5515/// processing — it is distinct from a negative status.
5516pub type SubroutineFn = Box<dyn Fn(&mut dyn Record) -> CaResult<i64> + Send + Sync>;
5517
5518#[cfg(test)]
5519mod declaration_numbering_tests {
5520    use super::*;
5521    use crate::server::record::dbd_generated::{DB_COMMON_FIELDS, record_fields};
5522
5523    /// C's `special.h:26-39` @R7.0.10 in full, plus the 0 C leaves in
5524    /// `pdbFldDes->special` for a field with no `special()`.
5525    ///
5526    /// The WHOLE table, not a sample, because the numbering is not
5527    /// contiguous: C has no 4, and the record-specific half jumps to 100. A
5528    /// twelfth variant inserted in the middle must fail on its own row here
5529    /// rather than shift every code after it.
5530    const C_SPECIAL: [(Special, i16, &str); 11] = [
5531        (Special::None, 0, "(none)"),
5532        (Special::NoMod, 1, "SPC_NOMOD"),
5533        (Special::DbAddr, 2, "SPC_DBADDR"),
5534        (Special::Scan, 3, "SPC_SCAN"),
5535        (Special::AlarmAck, 5, "SPC_ALARMACK"),
5536        (Special::As, 6, "SPC_AS"),
5537        (Special::Attribute, 7, "SPC_ATTRIBUTE"),
5538        (Special::Mod, 100, "SPC_MOD"),
5539        (Special::Reset, 101, "SPC_RESET"),
5540        (Special::LinConv, 102, "SPC_LINCONV"),
5541        (Special::Calc, 103, "SPC_CALC"),
5542    ];
5543
5544    /// Exhaustive by construction: a twelfth variant stops the build here, so
5545    /// the distinctness check below turns 11 rows into a proof of coverage.
5546    fn _the_enum_has_no_variant_outside_the_table(s: Special) {
5547        match s {
5548            Special::None
5549            | Special::NoMod
5550            | Special::DbAddr
5551            | Special::Scan
5552            | Special::AlarmAck
5553            | Special::As
5554            | Special::Attribute
5555            | Special::Mod
5556            | Special::Reset
5557            | Special::LinConv
5558            | Special::Calc => (),
5559        }
5560    }
5561
5562    #[test]
5563    fn every_special_carries_cs_spc_number() {
5564        for (spc, n, name) in C_SPECIAL {
5565            assert_eq!(spc as i16, n, "{name}: wrong SPC_ number");
5566        }
5567        for (a, (x, ..)) in C_SPECIAL.iter().enumerate() {
5568            for (y, ..) in C_SPECIAL.iter().skip(a + 1) {
5569                assert_ne!(x, y, "{x:?} listed twice");
5570            }
5571        }
5572        // The gap is the reason the discriminants are written out: an
5573        // implicitly-numbered enum would have answered 4 here and 7 for
5574        // `Mod`, and `special as i16` is the integer C prints.
5575        assert!(!C_SPECIAL.iter().any(|(_, n, _)| *n == 4), "C has no SPC 4");
5576        // `pamapspcType[SPC_NTYPES]` (`special.h:42,51-61`) is the `.dbd`
5577        // parser's table: nine entries, omitting SPC_ATTRIBUTE — which C sets
5578        // internally — and the 0 that means no `special()` at all.
5579        assert_eq!(
5580            C_SPECIAL
5581                .iter()
5582                .filter(|(s, ..)| !matches!(s, Special::None | Special::Attribute))
5583                .count(),
5584            9,
5585        );
5586    }
5587
5588    #[test]
5589    fn the_declared_token_survives_the_generators_collapse() {
5590        // `dbf_to_rust` maps DBF_INLINK|DBF_OUTLINK|DBF_FWDLINK onto one
5591        // served type, so `dbf_type` cannot tell a forward link from an
5592        // output link — and the field NAME cannot either: `LNK1` is
5593        // DBF_OUTLINK on sseq and DBF_FWDLINK on fanout. Only the carried
5594        // declaration separates them, which is what `dba` and `dbDumpField`
5595        // have to print.
5596        let field = |rec: &str, name: &str| {
5597            record_fields(rec)
5598                .unwrap_or_else(|| panic!("no field table for {rec}"))
5599                .iter()
5600                .find(|f| f.name == name)
5601                .unwrap_or_else(|| panic!("{rec}.{name} not in the table"))
5602        };
5603        let sseq_lnk1 = field("sseq", "LNK1");
5604        let fanout_lnk1 = field("fanout", "LNK1");
5605        let ai_inp = field("ai", "INP");
5606        let flnk = DB_COMMON_FIELDS
5607            .iter()
5608            .find(|f| f.name == "FLNK")
5609            .expect("dbCommon.FLNK");
5610
5611        for f in [sseq_lnk1, fanout_lnk1, ai_inp, flnk] {
5612            assert_eq!(f.dbf_type, DbFieldType::String, "{}: served type", f.name);
5613        }
5614        assert_eq!(sseq_lnk1.declared_dbf, DbfCode::Outlink);
5615        assert_eq!(fanout_lnk1.declared_dbf, DbfCode::Fwdlink);
5616        assert_eq!(ai_inp.declared_dbf, DbfCode::Inlink);
5617        assert_eq!(flnk.declared_dbf, DbfCode::Fwdlink);
5618
5619        // The other half of the collapse: DBF_ENUM|DBF_MENU|DBF_DEVICE all
5620        // serve as Enum. `DTYP` is the one DBF_DEVICE field in all of base.
5621        let dtyp = DB_COMMON_FIELDS
5622            .iter()
5623            .find(|f| f.name == "DTYP")
5624            .expect("dbCommon.DTYP");
5625        let scan = DB_COMMON_FIELDS
5626            .iter()
5627            .find(|f| f.name == "SCAN")
5628            .expect("dbCommon.SCAN");
5629        assert_eq!(dtyp.dbf_type, DbFieldType::Enum);
5630        assert_eq!(dtyp.declared_dbf, DbfCode::Device);
5631        assert_eq!(scan.dbf_type, DbFieldType::Enum);
5632        assert_eq!(scan.declared_dbf, DbfCode::Menu);
5633    }
5634
5635    #[test]
5636    fn no_access_reads_the_declaration_not_the_served_type() {
5637        // `waveform.VAL` is declared DBF_NOACCESS and re-typed by
5638        // `cvt_dbaddr`, so its served type says nothing; the declaration is
5639        // what `dbPutString` refuses a `.db` assignment on.
5640        let val = record_fields("waveform")
5641            .expect("waveform")
5642            .iter()
5643            .find(|f| f.name == "VAL")
5644            .expect("waveform.VAL");
5645        assert_eq!(val.declared_dbf, DbfCode::NoAccess);
5646        assert!(val.no_access());
5647        assert_ne!(val.dbf_type, DbFieldType::String, "re-typed by cvt_dbaddr");
5648
5649        // `mbbo.VAL` is the counter-example the doc names: SPC_DBADDR too,
5650        // but declared DBF_ENUM, so it is settable.
5651        let mbbo_val = record_fields("mbbo")
5652            .expect("mbbo")
5653            .iter()
5654            .find(|f| f.name == "VAL")
5655            .expect("mbbo.VAL");
5656        assert!(!mbbo_val.no_access());
5657
5658        // A hand-written descriptor has no `.dbd` behind it, so its
5659        // declaration is its served type's own code and it is never a C
5660        // internal.
5661        let hand = FieldDesc::new("VAL", DbFieldType::Double, false);
5662        assert_eq!(hand.declared_dbf, DbfCode::Double);
5663        assert!(!hand.no_access());
5664    }
5665}
5666
5667#[cfg(test)]
5668mod tests {
5669    use super::*;
5670    use crate::server::records::compress::CompressRecord;
5671
5672    /// A type that reads its own link texts to answer
5673    /// [`Record::set_input_link_slots`] must give the answer the default body's
5674    /// name walk would, or the cycle fetches the wrong set of input links —
5675    /// silently, since an unwired slot and a slot the override forgot look the
5676    /// same to it. Pinned against the default body's own algorithm, for every
5677    /// declared type and for each slot wired alone as well as none and all.
5678    #[test]
5679    fn a_record_that_reads_its_own_link_slots_answers_what_the_name_walk_would() {
5680        use crate::server::record::dbd_generated::RECORD_TYPES;
5681
5682        for record_type in RECORD_TYPES {
5683            let Ok(probe) = crate::server::db_loader::create_record(record_type) else {
5684                continue;
5685            };
5686            let links = probe.multi_input_links();
5687            if links.is_empty() || links.len() > u64::BITS as usize {
5688                continue;
5689            }
5690            let wirings = std::iter::once(Vec::new())
5691                .chain((0..links.len()).map(|i| vec![i]))
5692                .chain(std::iter::once((0..links.len()).collect::<Vec<_>>()));
5693            for wired in wirings {
5694                let Ok(mut record) = crate::server::db_loader::create_record(record_type) else {
5695                    continue;
5696                };
5697                for &slot in &wired {
5698                    let _ = record.put_field(links[slot].0, EpicsValue::String("SRC".into()));
5699                }
5700                let walked = {
5701                    let (mut set, mut unknown) = (0u64, 0u64);
5702                    for (slot, (link_field, _)) in links.iter().enumerate() {
5703                        match record.link_text_ref(link_field) {
5704                            Some("") => {}
5705                            Some(_) => set |= 1 << slot,
5706                            None => unknown |= 1 << slot,
5707                        }
5708                    }
5709                    Some((set, unknown))
5710                };
5711                assert_eq!(
5712                    record.set_input_link_slots(),
5713                    walked,
5714                    "{record_type}: set_input_link_slots() disagrees with the name walk \
5715                     with slots {wired:?} wired"
5716                );
5717            }
5718        }
5719    }
5720
5721    /// The cycle skips [`Record::select_input_links`] — and the record read
5722    /// lock it needs — whenever [`Record::narrows_input_links`] is false. A
5723    /// type that answers `false` must never narrow, or the cycle skips a real
5724    /// restriction to save the lock that would have found it.
5725    #[test]
5726    fn a_type_that_never_narrows_its_inputs_says_so() {
5727        use crate::server::record::dbd_generated::RECORD_TYPES;
5728        for record_type in RECORD_TYPES {
5729            let Ok(record) = crate::server::db_loader::create_record(record_type) else {
5730                continue;
5731            };
5732            if record.narrows_input_links() {
5733                continue;
5734            }
5735            for selector in std::iter::once(None).chain((0..=20u16).map(Some)) {
5736                assert!(
5737                    record.select_input_links(selector).is_none(),
5738                    "{record_type}: narrows_input_links() is false but \
5739                     select_input_links({selector:?}) narrowed the list"
5740                );
5741            }
5742        }
5743    }
5744
5745    /// The cycle skips [`Record::after_output_decision`] — and the record write
5746    /// lock it needs — whenever [`Record::redecides_after_output`] is false
5747    /// (`ProcessPlan::redecides_after_output`). A type that grows an epilogue
5748    /// and forgets the predicate would be silently skipped, so every type is
5749    /// driven through both and the two are required to agree.
5750    #[test]
5751    fn a_type_that_redecides_after_output_says_so() {
5752        use crate::server::record::dbd_generated::RECORD_TYPES;
5753
5754        for record_type in RECORD_TYPES {
5755            let (Ok(mut ran), Ok(untouched)) = (
5756                crate::server::db_loader::create_record(record_type),
5757                crate::server::db_loader::create_record(record_type),
5758            ) else {
5759                continue;
5760            };
5761            // C `longoutRecord.c:492` latches VAL into PVAL, so a record left at
5762            // its initial VAL would hide the epilogue behind `pval == val`.
5763            let _ = ran.put_field("VAL", EpicsValue::Long(7));
5764            let before = field_dump(&*ran);
5765            ran.after_output_decision();
5766            let changed = field_dump(&*ran) != before;
5767
5768            let diff: Vec<_> = field_dump(&*ran)
5769                .into_iter()
5770                .zip(before.iter())
5771                .filter(|(a, b)| a.1 != b.1)
5772                .map(|(a, b)| (a.0, b.1.clone(), a.1.clone()))
5773                .collect();
5774            assert_eq!(
5775                changed,
5776                untouched.redecides_after_output(),
5777                "{record_type}: after_output_decision changed {diff:?}, but \
5778                 redecides_after_output said {}",
5779                untouched.redecides_after_output()
5780            );
5781        }
5782    }
5783
5784    /// Every field the type declares, rendered — `sel` initialises A..L to
5785    /// `NaN`, which is never equal to itself, so the comparison is over the
5786    /// `Debug` form rather than the values.
5787    fn field_dump(r: &dyn Record) -> Vec<(&'static str, String)> {
5788        r.field_list()
5789            .iter()
5790            .map(|d| (d.name, format!("{:?}", r.get_field(d.name))))
5791            .collect()
5792    }
5793
5794    /// Every one-question cycle accessor must answer exactly what the
5795    /// `get_field` path it replaces would answer, for every record type in
5796    /// the database — the overrides exist only to skip a name match, never to
5797    /// change what is read.
5798    ///
5799    /// The cells are seeded with DISTINCT values first, through the generic
5800    /// put path and in the type the cell is declared with, so an override
5801    /// that returns the neighbouring cell cannot pass on a default-valued
5802    /// record where every cell reads zero.
5803    #[test]
5804    fn every_record_answers_the_cycle_cells_as_get_field_does() {
5805        use crate::server::record::dbd_generated::RECORD_TYPES;
5806
5807        let f64_of = |r: &dyn Record, field: &str| r.get_field(field).and_then(|v| v.to_f64());
5808
5809        for record_type in RECORD_TYPES {
5810            let Ok(mut r) = crate::server::db_loader::create_record(record_type) else {
5811                continue;
5812            };
5813            seed_cycle_cells(&mut *r);
5814
5815            let r = &*r;
5816            let deadband = r.monitor_deadband_cells();
5817            assert_eq!(deadband.mdel, f64_of(r, "MDEL"), "{record_type} MDEL");
5818            assert_eq!(deadband.adel, f64_of(r, "ADEL"), "{record_type} ADEL");
5819            assert_eq!(deadband.mlst, f64_of(r, "MLST"), "{record_type} MLST");
5820            assert_eq!(deadband.alst, f64_of(r, "ALST"), "{record_type} ALST");
5821
5822            let ladder = r
5823                .val()
5824                .and_then(|v| AlarmLimit::from_ladder_value(&v))
5825                .map(|val| AnalogAlarmInput {
5826                    val,
5827                    hyst: r
5828                        .get_field("HYST")
5829                        .and_then(|v| AlarmLimit::from_stored(&v)),
5830                    lalm: r
5831                        .get_field("LALM")
5832                        .and_then(|v| AlarmLimit::from_stored(&v)),
5833                });
5834            assert_eq!(
5835                r.analog_alarm_input(),
5836                ladder,
5837                "{record_type} analog_alarm_input"
5838            );
5839
5840            let filter = r.get_field("AFTC").map(|aftc| {
5841                (
5842                    aftc.to_f64().unwrap_or(0.0),
5843                    f64_of(r, "AFVL").unwrap_or(0.0),
5844                )
5845            });
5846            assert_eq!(r.alarm_filter_cells(), filter, "{record_type} AFTC/AFVL");
5847        }
5848    }
5849
5850    /// The write half, pinned against the path it replaces rather than
5851    /// against a restatement of it: two records of the same type, one driven
5852    /// through the store accessor and one through the `get_field` type probe
5853    /// and `put_field` the accessor stands in for. Their cells must end up
5854    /// identical, including where the put is refused — which is how the port
5855    /// behaved before the accessor existed.
5856    #[test]
5857    fn every_record_stores_the_cycle_cells_as_put_field_does() {
5858        use crate::server::record::dbd_generated::RECORD_TYPES;
5859
5860        for record_type in RECORD_TYPES {
5861            let (Ok(mut fast), Ok(mut slow)) = (
5862                crate::server::db_loader::create_record(record_type),
5863                crate::server::db_loader::create_record(record_type),
5864            ) else {
5865                continue;
5866            };
5867            seed_cycle_cells(&mut *fast);
5868            seed_cycle_cells(&mut *slow);
5869
5870            fast.store_monitor_last_posted(23.0, true, true);
5871            for field in ["ALST", "MLST"] {
5872                let target = slow
5873                    .get_field(field)
5874                    .map(|v| v.db_field_type())
5875                    .unwrap_or(crate::types::DbFieldType::Double);
5876                let _ = slow.put_field(field, EpicsValue::Double(23.0).convert_to(target));
5877            }
5878
5879            fast.store_alarm_filter_value(29.0);
5880            let _ = slow.put_field("AFVL", EpicsValue::Double(29.0));
5881
5882            fast.store_analog_lalm(AlarmLimit::Double(31.0));
5883            let target = slow
5884                .get_field("LALM")
5885                .map(|v| v.db_field_type())
5886                .unwrap_or(crate::types::DbFieldType::Double);
5887            let _ = slow.put_field("LALM", EpicsValue::Double(31.0).convert_to(target));
5888
5889            for field in ["MLST", "ALST", "AFVL", "LALM"] {
5890                assert_eq!(
5891                    fast.get_field(field),
5892                    slow.get_field(field),
5893                    "{record_type} {field}"
5894                );
5895            }
5896        }
5897    }
5898
5899    /// `Record::val` and `Record::output_link_value` carry SEMANTICS that
5900    /// several types legitimately override (`stringout` serves neither from
5901    /// OVAL), so they cannot be pinned database-wide. The types whose
5902    /// override exists only to skip the name match are listed here, and each
5903    /// must still answer what the default body would.
5904    #[test]
5905    fn a_speed_override_of_val_answers_what_the_default_would() {
5906        const SPEED_OVERRIDES: &[&str] = &["calc"];
5907
5908        for record_type in SPEED_OVERRIDES {
5909            let mut r = crate::server::db_loader::create_record(record_type)
5910                .expect("a listed type is creatable");
5911            seed_cycle_cells(&mut *r);
5912            let _ = r.put_field("VAL", EpicsValue::Double(41.0));
5913
5914            let r = &*r;
5915            assert_eq!(r.val(), r.get_field(r.primary_field()), "{record_type} val");
5916            assert_eq!(
5917                r.output_link_value(),
5918                r.get_field("OVAL").or_else(|| r.val()),
5919                "{record_type} output_link_value"
5920            );
5921        }
5922    }
5923
5924    /// Distinct values in every cell the cycle accessors read, each in the
5925    /// type its record declares it with.
5926    fn seed_cycle_cells(r: &mut dyn Record) {
5927        let cells = [
5928            "MDEL", "ADEL", "MLST", "ALST", "HYST", "LALM", "AFTC", "AFVL", "OVAL",
5929        ];
5930        for (i, field) in cells.iter().enumerate() {
5931            let Some(target) = r.get_field(field).map(|v| v.db_field_type()) else {
5932                continue;
5933            };
5934            let _ = r.put_field(
5935                field,
5936                EpicsValue::Double(11.0 + i as f64).convert_to(target),
5937            );
5938        }
5939    }
5940
5941    /// Internal delivery is not a `dbPut`, and the two entries have to stay
5942    /// two. A `compress` INP reading a `DBF_LONG` source delivers ONE sample
5943    /// as an `EpicsValue::Long`; C's link layer converts it to the target's
5944    /// type and the record folds it into `compress_scalar`'s running `cvb`.
5945    /// Rendering it as a one-element buffer sends it to `push_array` instead,
5946    /// whose array algorithm writes the N clamp back into the record
5947    /// (`compressRecord.c:171-172`) — a field the scalar algorithm never
5948    /// touches.
5949    #[test]
5950    fn a_one_sample_link_delivery_is_not_a_one_element_buffer() {
5951        let mut rec = CompressRecord::new(4, 0);
5952        rec.put_field("N", EpicsValue::Long(0)).unwrap();
5953        assert_eq!(rec.get_field("N"), Some(EpicsValue::ULong(0)));
5954
5955        // The source's DBF differs from VAL's, so this delivery reaches the
5956        // converter rather than being handed over unchanged.
5957        put_field_internal_default(&mut rec, "VAL", EpicsValue::Long(5)).unwrap();
5958
5959        assert_eq!(
5960            rec.get_field("N"),
5961            Some(EpicsValue::ULong(0)),
5962            "compressRecord.c:273-304 never touches prec->n"
5963        );
5964    }
5965}
5966
5967/// The three cells C `checkAlarms` compares — see
5968/// [`Record::analog_alarm_input`], which is the only producer.
5969#[derive(Clone, Copy, Debug, PartialEq)]
5970pub struct AnalogAlarmInput {
5971    /// VAL, in the variant that picks the ladder's arm.
5972    pub val: AlarmLimit,
5973    /// HYST, or `None` for a type that keeps it in `CommonFields`.
5974    pub hyst: Option<AlarmLimit>,
5975    /// LALM, the armed latch, or `None` for a type with no cell for it.
5976    pub lalm: Option<AlarmLimit>,
5977}
5978
5979/// The four cells C `monitor()`'s deadband compares against — see
5980/// [`Record::monitor_deadband_cells`], which is the only producer.
5981#[derive(Clone, Copy, Debug, Default)]
5982pub struct MonitorDeadbandCells {
5983    /// MDEL, the monitor deadband.
5984    pub mdel: Option<f64>,
5985    /// ADEL, the archive deadband.
5986    pub adel: Option<f64>,
5987    /// MLST, the value the last `DBE_VALUE` post carried.
5988    pub mlst: Option<f64>,
5989    /// ALST, the value the last `DBE_LOG` post carried.
5990    pub alst: Option<f64>,
5991}