Skip to main content

epics_base_rs/server/record/
record_trait.rs

1use crate::error::CaResult;
2use crate::types::{DbFieldType, EpicsValue, PvString, c_parse};
3
4use super::scan::ScanType;
5
6/// Which of a `devXxxSoftRaw` dset's two entry points is delivering a value to
7/// [`Record::raw_soft_input`]. They are not the same function in C, and they do
8/// not agree about `MASK`.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum RawSoftEntry {
11    /// `devXxxSoftRaw::init_record` — `recGblInitConstantLink(&prec->inp,
12    /// DBF_x, &prec->rval)` (`devAiSoftRaw.c:41`, `devBiSoftRaw.c:42`,
13    /// `devMbbiSoftRaw.c:42`, `devMbbiDirectSoftRaw.c:42`). A CONSTANT `INP`
14    /// (`field(INP,"12")`) is loaded ONCE, at iocInit, straight into `RVAL`.
15    ///
16    /// `recGblInitConstantLink` is a plain typed store — **no MASK**. The mask
17    /// lives in `read_xxx`, which a constant INP never reaches (a constant link
18    /// delivers nothing at process).
19    InitConstant,
20    /// `devXxxSoftRaw::read_xxx` — the per-cycle `dbGetLink(&prec->inp, ...)`
21    /// followed by the dset's own masking (`devBiSoftRaw.c:56-57` `if
22    /// (prec->mask) prec->rval &= prec->mask;`, `devMbbiSoftRaw.c:78-79`
23    /// unconditionally).
24    Read,
25}
26
27/// The `special(SPC_*)` dispatch code a field declares — C `special.h`.
28///
29/// C hands this to the record's `special(DBADDR *, int after)` on every put, and
30/// `dbAccess.c` acts on three of them itself before the record ever sees the
31/// write: `NoMod` refuses it (`S_db_noMod`), `DbAddr` means the field's type and
32/// element count come from the record's `cvt_dbaddr` rather than the `.dbd`, and
33/// `As` re-evaluates access security.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Special {
36    /// No `special()` declared.
37    None,
38    /// `SPC_NOMOD` (1) — the field must not be modified. Mirrored into
39    /// [`FieldDesc::read_only`], which is the bit the put gate reads.
40    NoMod,
41    /// `SPC_DBADDR` (2) — the record's `cvt_dbaddr` supplies the field's type
42    /// and element count. [`FieldDesc::dbf_type`] carries the type C serves at
43    /// the selector field's default; a record whose type is state-dependent
44    /// (`waveform.VAL` on `FTVL`, `mbbo.VAL` on `SDEF`) overrides it at runtime.
45    DbAddr,
46    /// `SPC_SCAN` (3) — a scan-related field; C re-registers the scan.
47    Scan,
48    /// `SPC_ALARMACK` (5) — an alarm acknowledgement.
49    AlarmAck,
50    /// `SPC_AS` (6) — access security; C re-computes the record's ASG.
51    As,
52    /// `SPC_ATTRIBUTE` (7) — a pseudo (attribute) field.
53    Attribute,
54    /// `SPC_MOD` (100) — the record's own `special()` runs on the put.
55    Mod,
56    /// `SPC_RESET` (101) — the `RES` field is being modified.
57    Reset,
58    /// `SPC_LINCONV` (102) — a linear-conversion field changed; C calls the
59    /// device support's `special_linconv`.
60    LinConv,
61    /// `SPC_CALC` (103) — the `CALC` expression changed; C recompiles it.
62    Calc,
63}
64
65/// A field's access-security level — C `.dbd` `asl(ASL0|ASL1)`.
66///
67/// `ASL1` is the `.dbd` default (`dbLexRoutines.c:570`); `asl(ASL0)` lowers a
68/// field to the level an operator may write.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum Asl {
71    Asl0,
72    Asl1,
73}
74
75/// The `.dbd` declaration of a single record field.
76///
77/// Every one of these is **generated** from the vendored EPICS `.dbd` by
78/// `tools/dbd-codegen` — see [`dbd_generated`](super::dbd_generated). They used
79/// to be hand-copied, which is what made a wrong `dbf_type` or a missed
80/// `special(SPC_NOMOD)` a recurring finding rather than an impossible state.
81///
82/// The struct carries the *whole* declaration, not just the three attributes the
83/// runtime consumes today: dropping the rest at the parser is how the port ended
84/// up unable to answer questions like "is this field `pp(TRUE)`?" without
85/// re-reading the `.dbd`.
86#[derive(Debug, Clone)]
87pub struct FieldDesc {
88    /// The field name, upper-case as declared.
89    pub name: &'static str,
90    /// The `DBF_*` type. This is the *field* type; the CA wire type is derived
91    /// from it by [`DbFieldType::ca_wire_type`], which owns the promotions CA
92    /// has no type for (`ULong`/`Int64`/`UInt64` -> `DBR_DOUBLE`, `UShort` ->
93    /// `DBR_LONG`, `UChar` -> `DBR_CHAR`). Do not pre-promote here: PVA serves
94    /// the native width.
95    ///
96    /// This is what the field is SERVED as, on every delivery path — see
97    /// [`RecordInstance::project_to_declared_type`](super::RecordInstance::project_to_declared_type),
98    /// which projects the stored value onto it. The one exception is a
99    /// [`Self::runtime_typed`] field, where C's `cvt_dbaddr` overrides.
100    pub dbf_type: DbFieldType,
101    /// C's `cvt_dbaddr` re-types this field at name-resolution time from the
102    /// record's own state — `waveform.VAL` from `FTVL`, `aSub.A` from `FTA`,
103    /// `mbbo.VAL` from `SDEF` — so the `.dbd` declaration is a placeholder
104    /// carrying only the selector's default. [`Self::dbf_type`] is therefore
105    /// NOT what such a field is served as; the record's stored variant is this
106    /// port's `cvt_dbaddr` answer, and it wins.
107    ///
108    /// A `special(SPC_DBADDR)` field whose type is nevertheless FIXED
109    /// (`compress.VAL` is always a double array, `histogram.VAL` always
110    /// `epicsUInt32`) is *not* runtime-typed: its row in `cvt_dbaddr.types`
111    /// carries no selector, so the declared type is the true one and it is
112    /// projected like any other field.
113    pub runtime_typed: bool,
114    /// `special(SPC_NOMOD)` — the field is immutable for this record type. The
115    /// static half of the no-modify declaration; see [`Record::field_no_mod`]
116    /// for the half a record decides at runtime.
117    pub read_only: bool,
118    /// The full `special()` code, of which [`Self::read_only`] is one case.
119    pub special: Special,
120    /// `pp(TRUE)` — a put to this field processes the record.
121    pub pp: bool,
122    /// `asl(...)` — the access-security level.
123    pub asl: Asl,
124    /// `size(N)` — the declared byte size of a `DBF_STRING` field, or 0.
125    pub size: u16,
126    /// `menu(...)` choice strings, in index order, for a `DBF_MENU` field. The
127    /// index is the stored value and the strings are what `get_enum_strs` serves,
128    /// so a client sees `"NO CONVERSION"` rather than `0`.
129    pub menu: Option<&'static [&'static str]>,
130    /// `initial("...")` — the value C's dbd loader seeds the field with.
131    pub initial: Option<&'static str>,
132    /// `interest(N)` — the `dbpr` verbosity level at which C prints the field.
133    pub interest: u8,
134    /// `prop(YES)` — the field is a property: a change to it posts a
135    /// `DBE_PROPERTY` event.
136    pub prop: bool,
137}
138
139impl FieldDesc {
140    /// A hand-written descriptor carrying only the three attributes the port
141    /// used to model.
142    ///
143    /// **Transitional.** Every record type is migrating to the generated table
144    /// in [`dbd_generated`](super::dbd_generated), which carries the whole `.dbd`
145    /// declaration; this constructor exists only so the not-yet-migrated records
146    /// keep compiling, and it goes away with the last of them. It does NOT know
147    /// the field's `pp`/`asl`/`size`/`menu`/`initial`, so it reports the neutral
148    /// value for each — a record still on this constructor answers "no menu"
149    /// here and resolves its choices through the
150    /// [`Record::menu_field_choices`] fallback instead.
151    pub const fn new(name: &'static str, dbf_type: DbFieldType, read_only: bool) -> Self {
152        Self {
153            name,
154            dbf_type,
155            // A hand-written table declares a plain field: the type it names is
156            // the type it is served as. The `cvt_dbaddr` records are all on the
157            // generated table, which sets this from `cvt_dbaddr.types`.
158            runtime_typed: false,
159            read_only,
160            special: if read_only {
161                Special::NoMod
162            } else {
163                Special::None
164            },
165            pp: false,
166            asl: Asl::Asl1,
167            size: 0,
168            menu: None,
169            initial: None,
170            interest: 0,
171            prop: false,
172        }
173    }
174}
175
176/// One `recGblInitConstantLink(&prec->LINK, DBF_x, &prec->TARGET)` call from a
177/// record's C `init_record` — the seed of a CONSTANT input link.
178///
179/// Declared by [`Record::constant_init_links`] and applied by the single owner
180/// `crate::server::database::PvDatabase::rec_gbl_init_constant_links`.
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub struct ConstantInitLink {
183    /// The link field holding the constant (`INPA`, `NVL`, `SELL`, `DOL1`,
184    /// `SUBL`, `DOL`, ...).
185    pub link_field: &'static str,
186    /// The value field the constant is loaded into (`A`, `SELN`, `DO1`,
187    /// `SNAM`, `VAL`, ...).
188    pub target_field: &'static str,
189    /// Whether a successful seed clears UDF — C's
190    /// `if (recGblInitConstantLink(&prec->dol, ...)) prec->udf = FALSE;`
191    /// (`aoRecord.c:112-113`, `longoutRecord.c:113`, `mbboRecord.c:133`,
192    /// `int64outRecord.c:110`, `dfanoutRecord.c:105`). The multi-input seeders
193    /// (calc/sub/sel/aSub/seq/fanout) do NOT clear UDF: they seed A..L, not
194    /// VAL.
195    pub clears_udf: bool,
196    /// Whether the loaded value is stored as its BOOLEAN — C `boRecord.c:146-148`
197    /// loads the constant into a temporary and stores `prec->val = !!ival`, so
198    /// `field(DOL,"5")` leaves a bo at VAL=1, not 5. The only seed whose stored
199    /// value differs from the loaded one.
200    pub normalize_bool: bool,
201}
202
203impl ConstantInitLink {
204    /// A seed that does not touch UDF — the INPA..L / SELL / NVL / DOLn form.
205    pub const fn new(link_field: &'static str, target_field: &'static str) -> Self {
206        Self {
207            link_field,
208            target_field,
209            clears_udf: false,
210            normalize_bool: false,
211        }
212    }
213
214    /// A DOL→VAL seed, which C follows with `prec->udf = FALSE`.
215    pub const fn dol_to_val(link_field: &'static str, target_field: &'static str) -> Self {
216        Self {
217            link_field,
218            target_field,
219            clears_udf: true,
220            normalize_bool: false,
221        }
222    }
223
224    /// bo's DOL→VAL seed: `prec->val = !!ival; prec->udf = FALSE;`
225    /// (`boRecord.c:146-149`).
226    pub const fn dol_to_bool_val(link_field: &'static str, target_field: &'static str) -> Self {
227        Self {
228            link_field,
229            target_field,
230            clears_udf: true,
231            normalize_bool: true,
232        }
233    }
234}
235
236/// The seed table for a record whose C seeds exactly the input links it
237/// fetches — the `for (i = 0; i < N; i++) recGblInitConstantLink(plink++,
238/// DBF_DOUBLE, pvalue++)` loop of calc / calcout / sub / sel / aSub /
239/// scalcout / acalcout / transform, expressed over the record's own
240/// [`Record::multi_input_links`] table.
241pub fn seed_input_links(pairs: &[(&'static str, &'static str)]) -> Vec<ConstantInitLink> {
242    pairs
243        .iter()
244        .map(|(link, value)| ConstantInitLink::new(link, value))
245        .collect()
246}
247
248/// Resolved metadata of an OUT-link TARGET, as C's soft device support
249/// obtains it before choosing its write buffer.
250///
251/// C's two sources, both mirrored by
252/// [`PvDatabase::resolve_out_target`](crate::server::database::PvDatabase):
253/// - `DB_LINK` — `dbNameToAddr` gives `field_type` and `no_elements`
254///   (`devsCalcoutSoft.c:127-131`, `devaCalcoutSoft.c:78-79`); an
255///   unresolvable name leaves the caller's initializers untouched.
256/// - `CA_LINK` — `dbCaGetLinkDBFtype` / `dbCaGetNelements`
257///   (`dbCa.c:662-704`), which both return `-1` on a disconnected link and
258///   likewise leave the initializers untouched.
259///
260/// A record reproduces its C device support's buffer switch on this in
261/// [`Record::multi_output_buffer`].
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub struct OutTarget {
264    /// Target field's DBF type. `None` = unresolved (disconnected CA link,
265    /// or a name this IOC cannot resolve) — C's `field_type` initializer.
266    pub field_type: Option<DbFieldType>,
267    /// Target field's element capacity — C `no_elements` / `dbCaGetNelements`.
268    /// `1` when unresolved, matching C's `n_elements = 1` initializer.
269    pub element_count: i64,
270    /// True when C would classify this link as a `CA_LINK`: an explicit
271    /// `ca://`/`pva://` link, or a DB-style name that is not a record of
272    /// this IOC (`dbInitLink` locality). Device support that splits its
273    /// buffer choice on sync-vs-async (`devsCalcoutSoft.c:76`, gated on
274    /// `plink->type == CA_LINK && pscalcout->wait`) reads this.
275    pub is_ca_link: bool,
276    /// True when the target field is one of the seven DBF classes C's soft
277    /// device support puts as `DBR_STRING` — `DBF_STRING`, `DBF_ENUM`,
278    /// `DBF_MENU`, `DBF_DEVICE`, `DBF_INLINK`, `DBF_OUTLINK`, `DBF_FWDLINK`
279    /// (`devsCalcoutSoft.c:83-85`, `:128-130`).
280    ///
281    /// Carried here rather than re-derived from [`Self::field_type`] because
282    /// [`DbFieldType`] is the DBR *wire* type: it has no `Menu` or `Device`
283    /// variant, so a menu target (`PRIO`, `STAT`, `SEVR`, `DISS`, `ACKT`, …)
284    /// or `DTYP` is indistinguishable from a plain numeric/string field by
285    /// type alone. The classification is made once, at resolution, by the
286    /// side that holds the target's field metadata
287    /// (`RecordInstance::field_puts_as_string`); a record's
288    /// [`Record::multi_output_buffer`] just reads the answer.
289    pub puts_as_string: bool,
290}
291
292impl OutTarget {
293    /// C's initializer state: `field_type = 0` is never *used* as a type by
294    /// the port (a `None` type routes to the device support's `default:`
295    /// arm), and `n_elements = 1`. An unresolved target is not in the string
296    /// class — C's `field_type = 0` matches no `case` and falls to `default:`.
297    pub const UNRESOLVED: Self = Self {
298        field_type: None,
299        element_count: 1,
300        is_ca_link: false,
301        puts_as_string: false,
302    };
303}
304
305/// The `dbrType` a record asks an INPUT link for — the second argument of C
306/// `dbGetLink(plink, dbrType, pbuffer, options, pnRequest)` (`dbLink.c:305`).
307///
308/// The READ twin of [`Record::typed_output_buffer`]'s destination switch. C's
309/// input-side switch is on the SOURCE's DBF class (`dbGetLinkDBFtype(&dol)`,
310/// `sseqRecord.c:640-705`) and each arm asks `dbGetLink` for a DIFFERENT
311/// `dbrType`, so the value a record receives is not the source's native one:
312/// a `DBF_ENUM`/`DBF_MENU` source read with `DBR_STRING` delivers its state
313/// LABEL, and a `DBF_CHAR` array read with `DBF_CHAR` delivers bytes, not a
314/// number. The record declares the request
315/// ([`Record::input_link_read_as`]); the framework, which is the side that
316/// can address the source, performs the conversion.
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub enum LinkReadAs {
319    /// The source's NATIVE value, coerced (or preserved) by the target field's
320    /// own `put_field_internal`. The framework default: every record whose C
321    /// `dbGetLink` request does not switch on the source class (compress `INP`,
322    /// waveform `INP`, sseq `SELL`, epid, motor, table) reads this way.
323    Native,
324    /// C `dbGetLink(..., DBR_STRING, ...)`. An `ENUM`/`MENU` source delivers its
325    /// state LABEL (`dbConvert.c` `getEnumString` → the record's
326    /// `get_enum_str`), never the index; a link/`DTYP` field delivers its text.
327    String,
328    /// C `dbGetLink(..., DBR_DOUBLE, ...)`.
329    Double,
330    /// C `dbGetLink(..., DBF_CHAR|DBF_UCHAR, buf, 0, &n)` — up to `max_elements`
331    /// bytes of the source's char array, taken as the string they spell
332    /// (`sseqRecord.c:682-686`: `n_elements` clamped to the record's 40-byte
333    /// `s` buffer, then `strcmp`/`atof` read it as a C string).
334    CharArrayAsString { max_elements: usize },
335}
336
337/// How C gates a secondary field named by
338/// [`Record::fields_posted_with_value_mask`] *inside* the guard that decides
339/// whether VAL posts at all.
340///
341/// Both variants share the outer guard (the field posts only on a cycle where
342/// VAL's own monitor mask is live, and carries that same mask); they differ in
343/// whether C re-tests the secondary field's own value once inside it. Folding
344/// the two into one rule is what over- or under-posts the field: gating
345/// `timestamp`'s RVAL on its own change silences it (see [`Self::WithValue`]),
346/// and NOT gating `ai`'s RVAL on its own change posts a raw count that never
347/// moved.
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
349pub enum ValuePostGate {
350    /// C re-tests the field's own previous value inside the guard, and posts
351    /// only if it moved: `ai` `RVAL` — `if (prec->oraw != prec->rval) {
352    /// db_post_events(&prec->rval, monitor_mask); prec->oraw = prec->rval; }`
353    /// (aiRecord.c:460-465).
354    OnChange,
355    /// C posts the field whenever the guard fires, with no test of its own
356    /// value: `timestamp` `RVAL` — `if (strncmp(oval, val, ...)) {
357    /// db_post_events(&val[0], mask); db_post_events(&rval, mask); }`
358    /// (timestampRecord.c:158-162). The VAL-string change is the *only* gate,
359    /// so a cycle that re-renders the same seconds count still re-posts RVAL.
360    WithValue,
361}
362
363/// The event mask ONE per-cycle mark posts with
364/// ([`Record::take_cycle_posted_fields`]).
365///
366/// A record can mark the same field from two different C `db_post_events` call
367/// sites in one cycle, and the two need not agree on the mask. aCalcout does
368/// exactly that with its arrays: `afterCalc` posts the AMASK-flagged ones with a
369/// LITERAL `DBE_VALUE|DBE_LOG` (`aCalcoutRecord.c:296`) while `monitor()` posts
370/// the NEWM-flagged ones with `monitor_mask|DBE_VALUE|DBE_LOG` (`:1034`). An
371/// array in BOTH masks gets BOTH events — the record marks it twice, and the
372/// variant carried with each mark is what keeps them distinguishable.
373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374pub enum CyclePostMask {
375    /// A literal `DBE_VALUE` — no LOG bit, no alarm bits. C's shape for a
376    /// field the record re-DERIVED from the one it was given: sseq re-renders
377    /// `STRn` after a `DOn` write and posts it with a bare `DBE_VALUE`
378    /// (`sseqRecord.c:679`, `:1115`), while the view actually written carries
379    /// `DBE_VALUE|DBE_LOG`.
380    Value,
381    /// A literal `DBE_VALUE | DBE_LOG` — the alarm-transition bits are NOT
382    /// folded in, because this C call site does not have `monitor_mask` in
383    /// scope (aCalcout `afterCalc`, `aCalcoutRecord.c:296`).
384    ValueLog,
385    /// `monitor_mask | DBE_VALUE | DBE_LOG` — C's usual `monitor()` shape
386    /// (aCalcout `monitor()`, `aCalcoutRecord.c:1034`).
387    MonitorValueLog,
388}
389
390/// The [`ValuePostGate`] a record declared for `field`, or `None` when `field`
391/// is not one of its secondary value-mask fields.
392///
393/// The single lookup for [`Record::fields_posted_with_value_mask`], shared by
394/// every monitor loop (both `process_record_*` paths, the deferred-completion
395/// path, and `RecordInstance::process_local`) so they cannot drift apart on how
396/// a secondary field is gated.
397pub(crate) fn value_gate(
398    value_masked: &'static [(&'static str, ValuePostGate)],
399    field: &str,
400) -> Option<ValuePostGate> {
401    value_masked
402        .iter()
403        .find(|(name, _)| *name == field)
404        .map(|(_, gate)| *gate)
405}
406
407/// The event mask a change-detected AUXILIARY field posts with — the single
408/// owner of that decision, built once per cycle from the record's declarations
409/// and shared by every monitor loop (both `process_record_*` paths, the
410/// deferred-completion path, and `RecordInstance::process_local`), so they
411/// cannot drift apart on what mask a field carries.
412///
413/// C's usual shape for the "post every input/aux field that changed" loop is
414/// `monitor_mask | DBE_VALUE | DBE_LOG` (calcRecord.c:420, subRecord.c:400,
415/// motor `DBE_VAL_LOG`) — that is the default. Three record-declared exceptions
416/// narrow it, and no two are the same narrowing:
417///
418/// * [`Record::value_only_change_fields`] — a literal `DBE_VALUE`
419///   (tableRecord.c:659, scaler `Sn`): alarm bits + `DBE_VALUE`, never `LOG`.
420/// * [`Record::fields_posted_with_monitor_mask`] — `monitor_mask | DBE_VALUE`
421///   (swaitRecord.c:650): VAL's own monitor mask, so `DBE_LOG` rides along
422///   exactly when VAL's ADEL deadband crossed.
423/// * [`Record::fields_posted_without_alarm_bits`] — a literal
424///   `DBE_VALUE | DBE_LOG` (epidRecord.c:376): both value classes, alarm bits
425///   discarded.
426#[derive(Clone, Copy)]
427pub(crate) struct AuxPostMask {
428    value_only: &'static [&'static str],
429    monitor_masked: &'static [&'static str],
430    no_alarm_bits: &'static [&'static str],
431}
432
433impl AuxPostMask {
434    /// Read the record's three declarations once, outside the per-field loop.
435    pub(crate) fn of(record: &dyn Record) -> Self {
436        Self {
437            value_only: record.value_only_change_fields(),
438            monitor_masked: record.fields_posted_with_monitor_mask(),
439            no_alarm_bits: record.fields_posted_without_alarm_bits(),
440        }
441    }
442
443    /// `alarm_bits` is this cycle's `recGblResetAlarms` result; `deadband_mask`
444    /// is VAL's own monitor mask (those alarm bits, plus `DBE_VALUE` when MDEL
445    /// crossed and `DBE_LOG` when ADEL crossed).
446    pub(crate) fn mask_for(
447        &self,
448        field: &str,
449        alarm_bits: crate::server::recgbl::EventMask,
450        deadband_mask: crate::server::recgbl::EventMask,
451    ) -> crate::server::recgbl::EventMask {
452        use crate::server::recgbl::EventMask;
453        if self.value_only.contains(&field) {
454            alarm_bits | EventMask::VALUE
455        } else if self.monitor_masked.contains(&field) {
456            deadband_mask | EventMask::VALUE
457        } else if self.no_alarm_bits.contains(&field) {
458            EventMask::VALUE | EventMask::LOG
459        } else {
460            alarm_bits | EventMask::VALUE | EventMask::LOG
461        }
462    }
463}
464
465/// Outcome of a record's array-style monitor decision, returned by
466/// [`Record::array_monitor_post`] (C waveform/aai/aao `monitor()`,
467/// waveformRecord.c:291-326).
468#[derive(Debug, Clone, Copy)]
469pub struct ArrayMonitorPost {
470    /// Include `DBE_VALUE` on the VAL post this cycle (MPST = Always, or
471    /// MPST = On Change with a changed hash).
472    pub post_value: bool,
473    /// Include `DBE_LOG` on the VAL post this cycle (APST = Always, or
474    /// APST = On Change with a changed hash).
475    pub post_archive: bool,
476    /// The content hash changed this cycle (On Change mode) — the owner
477    /// posts `HASH` with a literal `DBE_VALUE`.
478    pub hash_changed: bool,
479}
480
481/// The record type's RSET metadata slots — which of C's six nullable
482/// `get_*` property functions the record type implements.
483///
484/// This is the port's `rset` property table, transcribed slot by slot
485/// from the `#define get_xxx NULL` lines of each C record's `.c`. It is
486/// what `dbGet` consults to *narrow* the caller's `options` mask
487/// (`dbAccess.c:336-430`), and therefore what decides whether QSRV marks
488/// an NT leaf at all (pvxs `ioc/iocsource.cpp:263-305`). Without it the
489/// port fabricated every leaf it could name and marked it as supplied —
490/// telling the client a made-up `display.precision = 0` on a `longout`,
491/// or `valueAlarm` bands at zero on a `waveform`, were authoritative.
492///
493/// A slot counts as supplied when the C function pointer is non-NULL,
494/// even if the function writes nothing for the field in question — C
495/// leaves the option bit set either way (e.g. `boRecord.c:294-299`,
496/// whose `get_units` writes `"s"` only for `HIGH`, yet `DBR_UNITS`
497/// survives for every `bo` field).
498/// What a record type's C `get_control_double` writes for a field its switch
499/// does not list — the slot's LAST arm.
500///
501/// Independent of [`crate::server::snapshot::PropertySupport::control_double`],
502/// which says whether the slot EXISTS at all (a NULL slot makes
503/// `dbAccess.c:257` fail and clears the option bit, so no leaf is served).
504/// This says what a slot that DOES exist answers when it falls through. The
505/// two genuinely differ: `acalcout` supplies the slot and still writes nothing
506/// for an unlisted field.
507///
508/// Slot-neutral: the two arm SHAPES are the same for `get_control_double` and
509/// `get_graphic_double`, but which one a record type takes is asked per slot —
510/// [`control_default_arm`] and [`graphic_default_arm`] are separate answers.
511/// `aSub` is the type that proves they must be: its `get_control_double` is a
512/// bare `recGblGetControlDouble` (`aSubRecord.c:372-376`) while its
513/// `get_graphic_double` (`:350-368`) has no recGbl call at all.
514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515pub enum RsetDefaultArm {
516    /// The slot ends in `recGblGetControlDouble` / `recGblGetGraphicDouble` —
517    /// the field TYPE's numeric range (`recGbl.c:146-171`, table at
518    /// `:372-419`).
519    RecGblRange,
520    /// The slot returns without writing, so the `dbAccess.c:256` / `:216`
521    /// `(0.0, 0.0)` seed stands.
522    Seed,
523}
524
525/// The last arm of `rtype`'s C `get_control_double`.
526///
527/// Audited by reading each ported record type's own C rset. Every record in
528/// EPICS base delegates (`aiRecord.c:267`, `aoRecord.c:341`, `calcRecord.c:235`,
529/// `calcoutRecord.c:506`, `aSubRecord.c:372`, `subRecord.c:272`,
530/// `selRecord.c:203`, `seqRecord.c:342`, `dfanoutRecord.c:197`,
531/// `longinRecord.c:217`, `longoutRecord.c:268`, `int64inRecord.c:212`,
532/// `int64outRecord.c:251`, `boRecord.c:310`, `waveformRecord.c:268`,
533/// `aaiRecord.c:293`, `aaoRecord.c:296`, `subArrayRecord.c:262`,
534/// `compressRecord.c:487`, `histogramRecord.c:458`), as do the downstream
535/// types that supply the slot (`motorRecord.cc:3303`, `epidRecord.c:285`,
536/// `tableRecord.c:806`). The rest NULL it outright and never reach here
537/// (`biRecord.c`, `mbbiRecord.c`, `mbboRecord.c`, `mbbiDirectRecord.c`,
538/// `mbboDirectRecord.c`, `stringinRecord.c`, `stringoutRecord.c`,
539/// `lsiRecord.c`, `lsoRecord.c`, `eventRecord.c`, `fanoutRecord.c`,
540/// `permissiveRecord.c`, `printfRecord.c`, `stateRecord.c`,
541/// `sseqRecord.c:141`, `swaitRecord.c`, `transformRecord.c`, `busyRecord.c`,
542/// `asynRecord.c`, `throttleRecord.c:70`, `scalerRecord.c:157`).
543///
544/// Only the synApps calc pair writes nothing: both end `get_control_double`
545/// with a bare `return(0)` after their listed cases, so C serves the seed
546/// where a delegating record serves the type range. Measured on the
547/// differential oracle as 42 `acalcout`/`scalcout` fields.
548pub fn control_default_arm(rtype: &str) -> RsetDefaultArm {
549    match rtype {
550        // aCalcoutRecord.c:793-822, sCalcoutRecord.c:653 — the switch lists
551        // VAL/HIHI/HIGH/LOW/LOLO and the A-L / PA-PL ranges, then falls off
552        // the end into `return(0)` with no recGbl delegation.
553        "acalcout" | "scalcout" => RsetDefaultArm::Seed,
554        _ => RsetDefaultArm::RecGblRange,
555    }
556}
557
558/// The last arm of `rtype`'s C `get_graphic_double` — the twin of
559/// [`control_default_arm`], and NOT the same answer for every type.
560///
561/// Read from each ported type's own rset. `aSubRecord.c:350-368` is the one
562/// that separates the two slots: it tries `get_inlinkNumber` then
563/// `get_outlinkNumber` and, for a field that is neither, falls out of the
564/// function having written nothing — no `default:`, no recGbl call — so the
565/// `dbAccess.c:216` seed stands. Its `get_control_double` (`:372-376`) is a
566/// bare `recGblGetControlDouble` in the same file, which is why one shared bit
567/// could not answer both. Measured: `ASUB.PHAS` serves display 0/0 where
568/// `CALC.PHAS` serves the DBF_SHORT range ±32767.
569///
570/// The synApps calc pair ends the same way — the listed cases return early and
571/// the function ends `return(0)` with no delegation
572/// (`aCalcoutRecord.c:1046-1068`, `sCalcoutRecord.c:906-928`).
573///
574/// Every other ported type that supplies the slot delegates
575/// (`aiRecord.c:244`, `aoRecord.c:316`, `calcRecord.c:187`,
576/// `calcoutRecord.c:452`, `subRecord.c:222`, `selRecord.c:181`,
577/// `seqRecord.c:322`, `dfanoutRecord.c:181`, `longinRecord.c:190`,
578/// `int64inRecord.c:196`, `int64outRecord.c:235`, `longoutRecord.c:252`,
579/// `waveformRecord.c:251`, `aaiRecord.c:276`, `aaoRecord.c:279`,
580/// `subArrayRecord.c:231`, `compressRecord.c:471`, `histogramRecord.c:442`).
581pub fn graphic_default_arm(rtype: &str) -> RsetDefaultArm {
582    match rtype {
583        "aSub" | "acalcout" | "scalcout" => RsetDefaultArm::Seed,
584        _ => RsetDefaultArm::RecGblRange,
585    }
586}
587
588/// How `rtype`'s C `get_alarm_double` answers the fields it lists explicitly
589/// (see [`alarm_explicit_fields`]).
590///
591/// The severity gate is NOT universal, so this bit cannot be inferred from the
592/// base analog shape — it is read from each ported type's own rset.
593#[derive(Debug, Clone, Copy, PartialEq, Eq)]
594pub enum AlarmValArm {
595    /// `pad->upper_alarm_limit = prec->hhsv ? prec->hihi : epicsNAN` — each
596    /// limit is served only when its severity is enabled. The base analog
597    /// shape (`aiRecord.c:290-301`, and ao/longin/longout/calc/calcout/sel/
598    /// sub/dfanout alike).
599    Gated,
600    /// `pad->upper_alarm_limit = prec->hihi` — the four limits verbatim, with
601    /// no severity test at all, so an unset record serves 0 rather than NaN
602    /// (`int64inRecord.c:235-246`, `int64outRecord.c:279-290`,
603    /// `sCalcoutRecord.c:683-696`, `aCalcoutRecord.c:823-836`,
604    /// `motorRecord.cc:3344-3361`, `epidRecord.c:289-301`).
605    Unconditional,
606}
607
608/// The fields `rtype`'s C `get_alarm_double` lists BEFORE its
609/// `recGblGetAlarmDouble` fall-through — the ones that take
610/// [`alarm_val_arm`]'s answer instead of the four NaN.
611///
612/// Empty means the rset lists nothing: even VAL falls to the default arm.
613/// `seqRecord.c:355-367` routes only its `DOn` fields (through their `DOLn`
614/// link), `aSubRecord.c:378-404` only its `INPn`/`OUTn` links, and
615/// `swaitRecord.c:608-612` is a bare `recGblGetAlarmDouble(paddr,pad)` with no
616/// field test whatsoever. A constant link supplies no alarm limits, so the link
617/// arm lands on the same four NaN — which is why only the listed set needs a
618/// per-type answer and the link fields do not.
619///
620/// `motorRecord.cc:3344-3361` is the one type listing a second field: its case
621/// is `fieldIndex == motorRecordVAL || fieldIndex == motorRecordDVAL`, so the
622/// dial-coordinate readback carries the same limits as VAL.
623pub fn alarm_explicit_fields(rtype: &str) -> &'static [&'static str] {
624    match rtype {
625        "seq" | "aSub" | "swait" => &[],
626        "motor" => &["VAL", "DVAL"],
627        _ => &["VAL"],
628    }
629}
630
631/// See [`AlarmValArm`]. Types whose rset lists nothing
632/// ([`alarm_explicit_fields`] empty) never consult this.
633pub fn alarm_val_arm(rtype: &str) -> AlarmValArm {
634    match rtype {
635        "int64in" | "int64out" | "scalcout" | "acalcout" | "motor" | "epid" => {
636            AlarmValArm::Unconditional
637        }
638        _ => AlarmValArm::Gated,
639    }
640}
641
642pub fn default_property_support(rtype: &str) -> crate::server::snapshot::PropertySupport {
643    use crate::server::snapshot::PropertySupport as P;
644    match rtype {
645        // Every numeric slot, no enum strings.
646        // aiRecord.c:68-87, aoRecord.c:67-86, calcRecord.c:63-82,
647        // calcoutRecord.c:67-86, selRecord.c:58-77, subRecord.c:62-81,
648        // dfanoutRecord.c:66-85, seqRecord.c:56-75.
649        "ai" | "ao" | "calc" | "calcout" | "sel" | "sub" | "dfanout" | "seq" => P::NUMERIC,
650
651        // Integer scalars: `#define get_precision NULL`
652        // (longinRecord.c, longoutRecord.c, int64inRecord.c,
653        // int64outRecord.c). This is the measured `longout` case —
654        // pvxs leaves `display.precision` absent, the port sent 0.
655        "longin" | "longout" | "int64in" | "int64out" => P {
656            precision: false,
657            ..P::NUMERIC
658        },
659
660        // Arrays and compress: `#define get_alarm_double NULL`
661        // (waveformRecord.c, aaiRecord.c, aaoRecord.c,
662        // subArrayRecord.c, compressRecord.c, histogramRecord.c).
663        // This is the measured `waveform` case — pvxs leaves all four
664        // `valueAlarm.*Limit` absent, the port sent four zeros.
665        "waveform" | "aai" | "aao" | "subArray" | "compress" | "histogram" => P {
666            alarm_double: false,
667            ..P::NUMERIC
668        },
669
670        // No property slots at all: every `get_*` is `#define`d NULL.
671        // stringinRecord.c:62-81, stringoutRecord.c:64-83,
672        // lsiRecord.c:287-306, lsoRecord.c:328-347,
673        // eventRecord.c:62-81, permissiveRecord.c:56-75,
674        // stateRecord.c:58-77, printfRecord.c:456-475,
675        // fanoutRecord.c:60-79, timestampRecord (std-rs).
676        // This is the measured `stringout` case — pvxs leaves
677        // `display.units` absent, the port sent "".
678        "stringin" | "stringout" | "lsi" | "lso" | "event" | "permissive" | "state" | "printf"
679        | "fanout" | "timestamp" => P::NONE,
680
681        // Enum records. `biRecord.c:61-80` and `mbbiRecord.c:65-84` /
682        // `mbboRecord.c:64-83` NULL every numeric slot and supply only
683        // `get_enum_strs`. `boRecord.c:59-61` keeps `get_units`,
684        // `get_precision` and `get_control_double` (they serve the
685        // `HIGH` field) but NULLs `get_graphic_double` and
686        // `get_alarm_double`.
687        "bi" | "mbbi" | "mbbo" => P {
688            enum_strs: true,
689            ..P::NONE
690        },
691        "bo" => P {
692            units: true,
693            precision: true,
694            control_double: true,
695            enum_strs: true,
696            ..P::NONE
697        },
698        // busyRecord.c (synApps busy): units/graphic/control/alarm NULL,
699        // get_precision and get_enum_strs present.
700        "busy" => P {
701            precision: true,
702            enum_strs: true,
703            ..P::NONE
704        },
705        // mbbiDirectRecord.c:63-81 / mbboDirectRecord.c:63-81 — only
706        // `get_precision` survives, and C's DBF_FLOAT/DOUBLE gate
707        // (`dbAccess.c:388-395`) drops it again for their DBF_ENUM/LONG
708        // value, so nothing is marked. `Snapshot::precision` applies
709        // that gate.
710        "mbbiDirect" | "mbboDirect" => P {
711            precision: true,
712            ..P::NONE
713        },
714
715        // synApps, transcribed the same way.
716        // sCalcoutRecord.c / aCalcoutRecord.c / epidRecord.c /
717        // motorRecord.cc:259-279 (the rset table: get_units, get_precision,
718        // get_graphic_double, get_control_double and get_alarm_double all
719        // supplied, get_enum_strs NULL) / aSubRecord.c: full numeric set, no
720        // enum strings.
721        "scalcout" | "acalcout" | "motor" | "epid" | "aSub" => P::NUMERIC,
722        // scalerRecord.c:147-158 NULLs every property slot but one:
723        // `#define get_units NULL` (:151), `get_enum_strs` (:154),
724        // `get_graphic_double` (:156), `get_control_double` (:157) and
725        // `get_alarm_double` (:158). Only `get_precision` (:152) survives.
726        //
727        // Grouping scaler with the full-numeric synApps types claimed TEN
728        // leaves QSRV2 never serves: `display.units`, the two `display.limit*`,
729        // the two `control.limit*`, the four `valueAlarm.*Limit` — and
730        // `display.precision`, which pvxs assigns only inside its
731        // `DBR_GR_DOUBLE` branch (`iocsource.cpp:288-291`). That nesting is
732        // also why `precision` stays true here and yet marks nothing: it
733        // records what the rset supplies, exactly as `transform`/`sseq` do.
734        "scaler" => P {
735            precision: true,
736            ..P::NONE
737        },
738        // tableRecord.cc (optics): `#define get_alarm_double NULL`.
739        "table" => P {
740            alarm_double: false,
741            ..P::NUMERIC
742        },
743        // swaitRecord.c: get_units and get_control_double are NULL.
744        "swait" => P {
745            units: false,
746            control_double: false,
747            ..P::NUMERIC
748        },
749        // Only `get_precision` survives; the rset NULLs the other five.
750        // transformRecord.c; sseqRecord.c:124-144 (the rset table itself —
751        // `NULL, /* get_units */ get_precision, /* get_precision */ ...
752        // NULL, /* get_graphic_double */ NULL, /* get_control_double */
753        // NULL /* get_alarm_double */`). `sseq` was previously grouped with
754        // the full-numeric synApps types, which marked six leaves per field
755        // that QSRV2 omits entirely.
756        //
757        // `asyn` is NOT here: asyn-rs owns that record and declares its own
758        // row (asynRecord.c:84-91, the same shape) — a downstream crate
759        // cannot reach this table, which is why `Record::property_support`
760        // is the hook and this is only its default.
761        "transform" | "sseq" => P {
762            precision: true,
763            ..P::NONE
764        },
765        "throttle" => P {
766            precision: true,
767            graphic_double: true,
768            ..P::NONE
769        },
770
771        // A record type whose C rset the port has not transcribed keeps
772        // the pre-existing "supplies what it populated" behaviour rather
773        // than silently losing metadata. Add an arm above — with the C
774        // file and line — when porting a new record type.
775        _ => P::NUMERIC,
776    }
777}
778
779/// Per-field metadata deltas returned by
780/// [`Record::field_metadata_override`].
781///
782/// Each `Some` member replaces the corresponding member of the
783/// snapshot's record-level display/control metadata; `None` members
784/// keep the record-level value.
785#[derive(Debug, Clone, Default)]
786pub struct FieldMetadataOverride {
787    /// `display.units` — C RSET `get_units`.
788    pub units: Option<crate::types::PvString>,
789    /// `display.precision` — C RSET `get_precision`.
790    pub precision: Option<i16>,
791    /// `(upper, lower)` display limits — C RSET `get_graphic_double`.
792    pub disp_limits: Option<(f64, f64)>,
793    /// `(upper, lower)` control limits — C RSET `get_control_double`.
794    pub ctrl_limits: Option<(f64, f64)>,
795    /// `(hihi, high, low, lolo)` — C RSET `get_alarm_double`.
796    pub alarm_limits: Option<(f64, f64, f64, f64)>,
797}
798
799/// Side-effect actions that a record requests from the processing framework.
800///
801/// Records return these from `process()` via `ProcessOutcome::actions`.
802/// The framework executes them at the appropriate point in the processing
803/// cycle, keeping records as pure state machines without direct DB access.
804#[derive(Clone, Debug, PartialEq)]
805pub enum ProcessAction {
806    /// Write a value to a DB link. The framework reads `link_field` from the
807    /// record to get the target PV name, then writes `value` to that PV.
808    ///
809    /// Executed after alarm/snapshot, before FLNK.
810    /// Example: scaler writes CNT to COUT/COUTP links.
811    WriteDbLink {
812        link_field: &'static str,
813        value: EpicsValue,
814    },
815
816    /// Resolve an OUT link's TARGET ([`OutTarget`]) and hand it to the record
817    /// through [`Record::set_resolved_out_target`], BEFORE `process()` runs.
818    ///
819    /// **Pre-process action** — the OUT-link twin of [`Self::ReadDbLink`],
820    /// and C's `checkLinks`-cached `lnk_field_type`: a record whose fire-time
821    /// branch depends on the target's DBF class (sseq decides the wire buffer
822    /// AND whether a `WAITn` put-callback is issued from the one switch,
823    /// `sseqRecord.c:714-792`) must have the class in hand when it decides, not
824    /// after the framework's put path has already been entered.
825    ResolveOutTarget { link_field: &'static str },
826
827    /// Read a value from a DB link into a record field. The framework reads
828    /// `link_field` from the record to get the source PV name, reads that PV,
829    /// and writes the result into `target_field` via an internal put that
830    /// bypasses read-only checks.
831    ///
832    /// The value delivered is the link target's **native** [`EpicsValue`] — it
833    /// is NOT coerced to a numeric type on the way in. The record coerces (or
834    /// preserves) it at its own `put_field`/`put_field_internal` boundary, so a
835    /// string-class source can reach a string field byte-exact (the `sseq`
836    /// `DOLn`→`STRn` path, C `sseqRecord.c:643-705`). Records whose
837    /// `target_field` is numeric simply convert there, exactly as before.
838    ///
839    /// **Pre-process action**: executed BEFORE the next process() cycle so
840    /// the value is immediately available. This matches C EPICS `dbGetLink()`
841    /// which is synchronous/immediate.
842    ///
843    /// Example: throttle reads SINP into VAL when SYNC is triggered.
844    ReadDbLink {
845        link_field: &'static str,
846        target_field: &'static str,
847    },
848
849    /// Schedule a re-process of this record after the given duration.
850    /// The framework spawns `tokio::spawn(sleep(d) + process_record(name))`.
851    /// The current cycle's OUT/FLNK/notify proceed normally.
852    ///
853    /// Equivalent to C EPICS `callbackRequestDelayed()` + `scanOnce()`.
854    ReprocessAfter(std::time::Duration),
855
856    /// C `scanOnce(precord)` — queue ONE process of this record, now.
857    ///
858    /// A record's `special()` emits this when a put changed state the record
859    /// must act on but the put itself will not process the record. C guards
860    /// every such call with `if (precord->scan)` — scaler `special()`
861    /// (scalerRecord.c:655 CNT, :667 CONT), whose comment is exactly the
862    /// contract: *"Scan record if it's not Passive. (If it's Passive, it'll
863    /// get scanned automatically, since .cnt is a Process-Passive field.)"*
864    ///
865    /// The FRAMEWORK owns that gate, not the record: the framework owns SCAN
866    /// and owns the `pp(TRUE)` reprocess decision (`dbPutField`,
867    /// dbAccess.c:1265-1268), and a record's `special()` cannot see either. So
868    /// a record emits `ScanOnce` unconditionally wherever C calls `scanOnce`,
869    /// and the executor drops it for a Passive record — where the put's own
870    /// process already covers it and a second one would double-process.
871    ///
872    /// Queued, not inline: C's `scanOnce` hands the record to the scan-once
873    /// thread, which takes `dbScanLock` — so the process lands after the
874    /// putting thread leaves `dbPutField`.
875    ScanOnce,
876
877    /// Send a named command to the device support driver.
878    /// The framework calls `DeviceSupport::handle_command()` with this data.
879    /// Used by scaler to request reset/arm/write_preset operations
880    /// without the record holding a direct driver reference.
881    DeviceCommand {
882        command: &'static str,
883        args: Vec<EpicsValue>,
884    },
885
886    /// Write a value to a DB link as a put-*with-completion*, then re-enter
887    /// THIS record's `process()` when the downstream operation completes.
888    ///
889    /// The framework arms a put-notify wait-set (C `dbProcessNotify`),
890    /// writes `link_field`'s target through it, releases the initiator's
891    /// own count, and wires the completion to an async re-entry of this
892    /// record (`mint_async_token` + `reprocess_on_notify`). The record
893    /// returns [`RecordProcessResult::AsyncPending`] alongside this action
894    /// and is re-entered once the downstream record (and its FLNK/OUT
895    /// chain) finishes — the synApps `sseq` `WAITn` "wait for the put
896    /// callback" dependency (`sseqRecord.c::processNextLink`,
897    /// `dbCaPutLinkCallback`). Built on the same `new_put_notify` +
898    /// `reprocess_on_notify` primitive an out-of-band
899    /// [`crate::server::database::AsyncDbHandle`] caller uses.
900    ///
901    /// Executed before FLNK, like [`Self::WriteDbLink`].
902    WriteDbLinkNotify {
903        link_field: &'static str,
904        value: EpicsValue,
905    },
906
907    /// (Re)arm this record's monitor watchdog — C `histogramRecord.c::wdogInit`
908    /// (:126-152), whose `callbackRequestDelayed(&pcallback->callback,
909    /// prec->sdel)` starts (or restarts) the periodic
910    /// [`Record::watchdog_fire`] tick.
911    ///
912    /// Emitted from a record's `special()` when the put changed the watchdog's
913    /// period (histogram SDEL is `special(SPC_RESET)` precisely so it can
914    /// re-arm, `histogramRecord.c:266-268`). The framework also arms every
915    /// record's watchdog once at `iocInit`, which is C's other `wdogInit` call
916    /// site (`init_record` pass 1, `:168`).
917    ///
918    /// Arming supersedes any tick already pending for the record, exactly as
919    /// C's `callbackRequestDelayed` replaces an outstanding delayed callback.
920    ArmWatchdog,
921
922    /// Cancel this record's outstanding async re-entry (C
923    /// `callbackCancelDelayed`): the framework advances the record's
924    /// re-entry generation so any pending `ReprocessAfter` timer or
925    /// `WriteDbLinkNotify` completion re-entry becomes a structural no-op
926    /// (the `AsyncToken` gate), with no runtime "is-aborted" check on the
927    /// re-entry path. Used by `sseq` `ABORT` to drop a pending `DLYn`
928    /// delay or `WAITn` wait; the record resets its own sequence state in
929    /// the same `process()` cycle that emits this.
930    CancelReprocess,
931}
932
933/// Result of a record's process() call.
934///
935/// Determines how the framework handles the current processing cycle.
936/// Side-effect actions (link writes, delayed reprocess, etc.) are expressed
937/// separately in `ProcessOutcome::actions`.
938#[derive(Clone, Debug, PartialEq)]
939pub enum RecordProcessResult {
940    /// Processing completed synchronously this cycle.
941    /// Framework proceeds with alarm/timestamp/snapshot/OUT/FLNK.
942    Complete,
943    /// Processing started but not yet complete (PACT stays set).
944    /// Current cycle skips alarm/timestamp/snapshot/OUT/FLNK.
945    /// ProcessActions (if any) are still executed.
946    AsyncPending,
947    /// Async pending, but notify these intermediate field changes immediately.
948    /// Used by motor records to flush DMOV=0 before the move completes.
949    AsyncPendingNotify(Vec<(String, EpicsValue)>),
950    /// Completed synchronously (PACT cleared, unlike `AsyncPending`), but the
951    /// record produced no new value to publish this cycle — the framework must
952    /// skip the value-publication epilogue (UDF clear / timestamp / monitor /
953    /// FLNK). C parity `compressRecord.c:365` `if (status != 1)`: a compress
954    /// record still accumulating toward its next compressed sample runs none of
955    /// `recGblGetTimeStamp` / `monitor` / `recGblFwdLink` on that cycle.
956    CompleteNoEmit,
957    /// Ran the value-publication epilogue NOW (UDF clear / timestamp / monitor —
958    /// VAL and the alarm fields are posted this cycle), but the OUTPUT side (OUT
959    /// link write / OEVT / forward link) is deferred to a scheduled
960    /// reprocess, with PACT held across the wait. C parity `swaitRecord.c::process`
961    /// (lines 425-481): when `schedOutput` arms the ODLY watchdog it sets
962    /// `async=TRUE`, so `process` still runs `monitor()` (line 475) — posting the
963    /// value side at the START of the delay — but skips the `if(!async)
964    /// {recGblFwdLink; pact=FALSE;}` tail; the deferred `execOutput` (watchdog,
965    /// at delay-END) does the OUT write + OEVT + forward link and posts no
966    /// monitors. Unlike the calcout/scalcout/acalcout family, whose C `process`
967    /// `return`s BEFORE `monitor()` (calcoutRecord.c:282, only `dlya` posted), so
968    /// they defer the value side too and use `AsyncPendingNotify`. The deferral
969    /// must carry a [`ProcessAction::ReprocessAfter`] — that scheduled reprocess
970    /// is the continuation that releases the held PACT (same by-construction
971    /// invariant as the `AsyncPendingNotify` ODLY defer).
972    CompleteDeferOutput,
973    /// Completed synchronously (PACT cleared), and the framework runs the ALARM
974    /// epilogue ONLY: the UDF update, `check_alarms`, `recGblResetAlarms`
975    /// (committing SEVR/STAT/AMSG and posting those fields with their C masks)
976    /// and the timestamp. The VALUE side is skipped entirely — no `monitor()`
977    /// value posts (so the last-posted trackers stay put and the next publishing
978    /// cycle re-detects the change, exactly as C leaves `LA..LP` un-updated), no
979    /// OUT / OEVT write, no process actions, no forward link.
980    ///
981    /// C parity `transformRecord.c:554-560`: an INVALID input severity with
982    /// `IVLA == transformIVLA_DO_NOTHING` makes `process()` run
983    /// `recGblGetTimeStamp` + `checkAlarms` + `recGblResetAlarms`, clear `pact`
984    /// and `return` — skipping the calc loop, all 16 OUTx `dbPutLink` writes,
985    /// `monitor()` and `recGblFwdLink()`.
986    ///
987    /// Distinct from [`RecordProcessResult::CompleteNoEmit`], which skips the
988    /// alarm commit and the timestamp too (C `compressRecord.c:365` returns
989    /// before `checkAlarms`).
990    CompleteAlarmOnly,
991}
992
993/// Complete outcome of a record's process() call.
994///
995/// Contains the processing result (Complete, AsyncPending, etc.) and a list
996/// of side-effect actions for the framework to execute.
997#[derive(Clone, Debug)]
998pub struct ProcessOutcome {
999    pub result: RecordProcessResult,
1000    pub actions: Vec<ProcessAction>,
1001    /// Set by the framework when device support's read() returned
1002    /// `did_compute: true`. The record's process() can check this to
1003    /// skip its built-in computation (e.g., PID). Replaces the `pid_done`
1004    /// flag pattern.
1005    pub device_did_compute: bool,
1006}
1007
1008impl ProcessOutcome {
1009    /// Shorthand for a simple Complete with no actions.
1010    pub fn complete() -> Self {
1011        Self {
1012            result: RecordProcessResult::Complete,
1013            actions: Vec::new(),
1014            device_did_compute: false,
1015        }
1016    }
1017
1018    /// Shorthand for Complete with actions.
1019    pub fn complete_with(actions: Vec<ProcessAction>) -> Self {
1020        Self {
1021            result: RecordProcessResult::Complete,
1022            actions,
1023            device_did_compute: false,
1024        }
1025    }
1026
1027    /// Completed synchronously, but no new value was emitted this cycle, so
1028    /// the framework skips the value-publication epilogue (UDF clear /
1029    /// timestamp / monitor / FLNK). See `RecordProcessResult::CompleteNoEmit`.
1030    pub fn complete_no_emit() -> Self {
1031        Self {
1032            result: RecordProcessResult::CompleteNoEmit,
1033            actions: Vec::new(),
1034            device_did_compute: false,
1035        }
1036    }
1037
1038    /// Completed synchronously with the alarm epilogue only — no value posts,
1039    /// no output, no forward link. See `RecordProcessResult::CompleteAlarmOnly`.
1040    pub fn complete_alarm_only() -> Self {
1041        Self {
1042            result: RecordProcessResult::CompleteAlarmOnly,
1043            actions: Vec::new(),
1044            device_did_compute: false,
1045        }
1046    }
1047
1048    /// Shorthand for AsyncPending with no actions.
1049    pub fn async_pending() -> Self {
1050        Self {
1051            result: RecordProcessResult::AsyncPending,
1052            actions: Vec::new(),
1053            device_did_compute: false,
1054        }
1055    }
1056}
1057
1058impl Default for ProcessOutcome {
1059    fn default() -> Self {
1060        Self::complete()
1061    }
1062}
1063
1064/// Result of setting a common field, indicating what scan index updates are needed.
1065#[derive(Clone, Debug, PartialEq, Eq)]
1066pub enum CommonFieldPutResult {
1067    NoChange,
1068    ScanChanged {
1069        old_scan: ScanType,
1070        new_scan: ScanType,
1071        phas: i16,
1072    },
1073    PhasChanged {
1074        scan: ScanType,
1075        old_phas: i16,
1076        new_phas: i16,
1077    },
1078}
1079
1080/// Read-only snapshot of framework-owned `CommonFields` state that a
1081/// record's `process()` or device support's `read()` needs to see
1082/// *during* the processing cycle.
1083///
1084/// The framework owns `RecordInstance.common`; a record `process()`
1085/// receives only `&mut self` (the concrete record) and device support
1086/// `read()` receives only `&mut dyn Record`. Neither can reach
1087/// `CommonFields`. C records, by contrast, see `dbCommon` directly —
1088/// e.g. `epidRecord.c:195` reads `pepid->udf`, `timestampRecord.c:90`
1089/// reads `ptimestamp->tse`, `devTimeOfDay.c:122` reads `psi->phas`.
1090///
1091/// The framework builds a `ProcessContext` from `common` and pushes it
1092/// onto the record (via [`Record::set_process_context`]) and onto the
1093/// device support (via
1094/// [`crate::server::device_support::DeviceSupport::set_process_context`])
1095/// immediately before the respective call. This mirrors the existing
1096/// `set_device_did_compute` framework-set-hook pattern: additive,
1097/// no `process()` / `read()` signature change.
1098#[derive(Clone, Debug, PartialEq)]
1099pub struct ProcessContext {
1100    /// `dbCommon.udf` — value is undefined. C records check this at the
1101    /// top of `process()` (e.g. `epidRecord.c:195`).
1102    pub udf: bool,
1103    /// `dbCommon.udfs` — alarm severity raised for a UDF record.
1104    pub udfs: crate::server::record::AlarmSeverity,
1105    /// `dbCommon.nsev` — the *pending* (new) alarm severity this cycle has
1106    /// accumulated so far, BEFORE the record body runs. C `dbGetLink` folds an
1107    /// `MS`-class input link's severity into `nsev` at fetch time, so a record
1108    /// body that branches on the input severity reads it here — e.g.
1109    /// `transformRecord.c:554` `if ((ptran->nsev >= INVALID_ALARM) && (ptran->ivla
1110    /// == transformIVLA_DO_NOTHING))`. The framework folds every input-link alarm
1111    /// into `common.nsev` before building this snapshot, so `nsev` is the single
1112    /// source of truth; the record never re-derives it from the links.
1113    pub nsev: crate::server::record::AlarmSeverity,
1114    /// `dbCommon.phas` — phase. Used by device support for format
1115    /// selection (`devTimeOfDay.c:122`).
1116    pub phas: i16,
1117    /// `dbCommon.tse` — time-stamp event. `timestampRecord.c:90`
1118    /// branches on `tse == epicsTimeEventDeviceTime`.
1119    pub tse: i16,
1120    /// `dbCommon.time` — the record's current resolved time stamp at the
1121    /// start of this cycle (the previous cycle's stamp, or `UNIX_EPOCH`
1122    /// before the first process). Device support that has to format the
1123    /// record's time during `read()` — the std module's `devTimeOfDay.c`
1124    /// `recGblGetTimeStamp(psi)` call, which runs *before* the framework's
1125    /// per-cycle timestamp application — resolves the stamp with
1126    /// [`crate::server::recgbl::get_time_stamp`]`(tse, time)`. The `time`
1127    /// member is the device-provided value that helper returns verbatim on
1128    /// the `TSE == epicsTimeEventDeviceTime (-2)` branch.
1129    pub time: std::time::SystemTime,
1130    /// `dbCommon.tsel` — time-stamp event link string.
1131    pub tsel: String,
1132    /// `dbCommon.dtyp` — device-support type name. A record's
1133    /// `process()` / pre-process hooks can branch on the DTYP to mirror
1134    /// C device support that lives in a separate DSET (e.g. the epid
1135    /// record's `devEpidSoftCallback` callback DSET drives the TRIG
1136    /// readback link, whereas `devEpidSoft` does not).
1137    pub dtyp: String,
1138}
1139
1140/// C `epicsTime.h`: `epicsTimeEventDeviceTime` — the `TSE` sentinel
1141/// meaning "device support provides the time stamp". `timestampRecord.c`
1142/// uses it to take the OS-clock branch instead of `recGblGetTimeStamp`.
1143pub const EPICS_TIME_EVENT_DEVICE_TIME: i16 = -2;
1144
1145/// Snapshot of changes from a process cycle, used for notify outside lock.
1146pub struct ProcessSnapshot {
1147    /// `(field, value, mask)` — every posted field carries its own
1148    /// `DBE_*` posting mask, mirroring C's per-field
1149    /// `db_post_events(prec, &field, mask)`. One process cycle posts
1150    /// different classes per field: a deadband-gated readback narrows
1151    /// to the deadbands that actually crossed (MDEL → `DBE_VALUE`,
1152    /// ADEL → `DBE_LOG`; motorRecord.cc `monitor()` 3477-3507,
1153    /// aiRecord.c `monitor()`), while a change-detected auxiliary
1154    /// field posts `DBE_VALUE | DBE_LOG` (motorRecord.cc 3522-3645
1155    /// `DBE_VAL_LOG`; calcRecord.c:420). A single record-wide mask
1156    /// collapses that granularity — an archive-only deadband crossing
1157    /// would wrongly reach `DBE_VALUE` subscribers whenever any other
1158    /// field changed in the same pass.
1159    pub changed_fields: Vec<(String, EpicsValue, crate::server::recgbl::EventMask)>,
1160}
1161
1162/// What C's `fetch_values()` does when one of the record's input links fails
1163/// to read, and whether that failure gates the record body.
1164///
1165/// Every C record with an INPA..INPx block has a `fetch_values()` helper, but
1166/// they do not share a failure shape, so the framework cannot pick one rule
1167/// for all of them — each record declares its own via
1168/// [`Record::input_fetch_policy`].
1169///
1170/// The two dimensions C varies are "does the loop stop at the first failure"
1171/// and "does a failure gate the record body", and it uses three of the four
1172/// combinations. Whichever variant a record picks, the framework reduces the
1173/// cycle to ONE outcome — C's `fetch_values()` return status, zero or not —
1174/// and delivers it through a single owner: [`Record::set_fetch_gate_failed`]
1175/// for records that compute in their own `process()`, and
1176/// `RecordInstance::suppress_subroutine_run` for the two whose body is a
1177/// framework-dispatched subroutine (sub/aSub).
1178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1179pub enum InputFetchPolicy {
1180    /// Read every configured link; a failed read neither stops the loop nor
1181    /// gates the record body. C `transformRecord.c::process` (531-545) reads
1182    /// on through a failed `dbGetLink` and computes anyway.
1183    ReadAll,
1184    /// Read every configured link — a failure does NOT stop the loop, so the
1185    /// inputs behind it still refresh — but the body is skipped this cycle.
1186    ///
1187    /// C `calcRecord.c::fetch_values` (427-443) keeps the FIRST failing status
1188    /// while looping to the end (`if (status == 0) status = newStatus;`), and
1189    /// `calcRecord.c::process` (120) runs `calcPerform` only
1190    /// `if (fetch_values(prec) == 0)` — so VAL and UDF freeze, no CALC_ALARM is
1191    /// raised, and everything after the calc (timestamp, alarms, monitors,
1192    /// forward link) still runs. `calcoutRecord.c` (694-709 fetch, 237 gate) is
1193    /// the same shape, and its OOPT decision then runs against the frozen VAL.
1194    ReadAllGateOnFailure,
1195    /// Stop at the FIRST failed link and skip the record body this cycle.
1196    ///
1197    /// C `subRecord.c::fetch_values` (407-418) `return -1`s on the first
1198    /// failing `dbGetLink`, so the inputs behind it are never read and keep
1199    /// their previous values; `subRecord.c::process` (145-146) then runs
1200    /// `do_sub` only `if (status == 0)`, freezing VAL/UDF and raising none of
1201    /// the subroutine's alarms. `aSubRecord.c` (277-289 fetch, 216-218
1202    /// process), `sCalcoutRecord.c` (885-887 fetch, 356 gate),
1203    /// `aCalcoutRecord.c` (1068-1071 fetch, 399 gate) and
1204    /// `swaitRecord.c` (686-705 fetch, 408 gate) are the same shape.
1205    AbortOnFirstFailure,
1206}
1207
1208/// **The** field declaration of a record type, and the only way to obtain one.
1209///
1210/// Not implementable: the blanket `impl` below covers every [`Record`], so a
1211/// second `impl FieldDeclaration for MyRecord` is a coherence error. A record
1212/// type therefore cannot *supply* a field list — it can only be *asked* for
1213/// one, and the answer is resolved here:
1214///
1215/// * a record type **base's** vendored `.dbd` set covers is declared by the
1216///   table generated from that `.dbd` ([`crate::server::record::dbd_generated::record_fields`]);
1217/// * any other record type is asked for its own declaration,
1218///   [`Record::declared_fields`] — which for the downstream record types is the
1219///   table generated from the `.dbd` *their* crate vendors, and for a synthetic
1220///   record type (tests) is a hand-written table.
1221///
1222/// The two are mutually exclusive by construction, which is what closes the
1223/// invariant *one declaration per record type*. It used to be closed by luck:
1224/// both tables were live, `field_desc_of` merely happened to consult the
1225/// generated one first, and every consumer that reached for `field_list()`
1226/// directly (`dbpr`, the `dbpf` typo hint, `motor`'s field gate) read the
1227/// hand-written one — which is how `waveform.FTVL` was declared `DBF_SHORT`
1228/// with no menu while `waveformRecord.dbd` said `DBF_MENU`/`menu(menuFtype)`.
1229pub trait FieldDeclaration {
1230    /// The record type's field descriptors, in `.dbd` declaration order.
1231    fn field_list(&self) -> &'static [FieldDesc];
1232}
1233
1234impl<R: Record + ?Sized> FieldDeclaration for R {
1235    fn field_list(&self) -> &'static [FieldDesc] {
1236        super::dbd_generated::record_fields(self.record_type())
1237            .unwrap_or_else(|| self.declared_fields())
1238    }
1239}
1240
1241/// Trait that all EPICS record types must implement.
1242pub trait Record: Send + Sync + 'static {
1243    /// Return the record type name (e.g., "ai", "ao", "bi").
1244    fn record_type(&self) -> &'static str;
1245
1246    /// Process the record (scan/compute cycle).
1247    ///
1248    /// Returns a `ProcessOutcome` containing the processing result and any
1249    /// side-effect actions for the framework to execute.
1250    fn process(&mut self) -> CaResult<ProcessOutcome> {
1251        Ok(ProcessOutcome::complete())
1252    }
1253
1254    /// Optional: report whether this record's last `process()` call
1255    /// mutated a metadata-class field (EGU/PREC/HOPR/LOPR/HLM/LLM/
1256    /// alarm limits / DRVH/DRVL / state strings).
1257    ///
1258    /// The framework checks this after every `process()` call and, if
1259    /// true, invalidates the record's metadata cache so the next
1260    /// snapshot rebuilds from the new values.
1261    ///
1262    /// Default: `false` — most records never touch metadata fields
1263    /// during processing. Override only when your record dynamically
1264    /// adjusts limits or unit strings (e.g., a motor that recomputes
1265    /// HLM/LLM after a hardware homing operation).
1266    ///
1267    /// Implementations should reset their internal flag after returning
1268    /// `true` so the next cycle starts clean.
1269    fn took_metadata_change(&mut self) -> bool {
1270        false
1271    }
1272
1273    /// Get a field value by name.
1274    fn get_field(&self, name: &str) -> Option<EpicsValue>;
1275
1276    /// Set a field value by name.
1277    fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()>;
1278
1279    /// The field declaration of a record type **base's** `.dbd` set does not
1280    /// cover — a record type that lives in another crate.
1281    ///
1282    /// C has exactly one declaration per record type: the `.dbd`, read at
1283    /// runtime. The port compiles the vendored `.dbd`s into a generated table,
1284    /// and [`FieldDeclaration::field_list`] — the single resolver every consumer
1285    /// goes through — serves base's
1286    /// [`dbd_generated`](crate::server::record::dbd_generated) table for every
1287    /// record type IT covers and **never falls through to here**. So for a
1288    /// base record type this method is unreachable: it cannot declare one of its
1289    /// fields a second time, whatever it writes here.
1290    ///
1291    /// A record type outside base declares itself here, and the answer is still
1292    /// its `.dbd`: the downstream Tier-3 record types (`motor`, `table`,
1293    /// `scaler`, `epid`, `throttle`, `timestamp`) vendor their upstream `.dbd`
1294    /// into their OWN crate, `tools/dbd-codegen` generates a table into that
1295    /// crate (`tools/dbd-codegen/src/targets.rs`), and this method returns it.
1296    /// Each such crate carries the same ratchet base does
1297    /// (`one_declaration_per_record_type`): the table returned here must BE the
1298    /// generated one, so the `.dbd` stays the single declaration across a crate
1299    /// boundary the generator's output cannot cross on its own.
1300    ///
1301    /// A hand-written table is what is left when a record type has no `.dbd`
1302    /// anywhere — the synthetic record types the tests define. There are no
1303    /// others; a shipped record type has a `.dbd`, and its declaration is that
1304    /// `.dbd`.
1305    ///
1306    /// The declaration is a *spec*: it says what each field's type, menu and
1307    /// `special(SPC_NOMOD)` are. It does NOT say who implements the field. Ask
1308    /// [`Record::implements_field`] for that — see its docs for why the two
1309    /// must not be the same question.
1310    fn declared_fields(&self) -> &'static [FieldDesc] {
1311        &[]
1312    }
1313
1314    /// Does this record type implement `name` in its own `get_field` /
1315    /// `put_field`, as opposed to leaving it to the framework's dbCommon
1316    /// handling?
1317    ///
1318    /// This used to be answered by `field_list()` membership, which conflated
1319    /// two questions: "what is this field?" and "who owns it?". They give
1320    /// different answers for `INP`/`OUT`: every record type *declares* them in
1321    /// the `.dbd`, but only some drive the link themselves
1322    /// (`multi_output_links` for `acalcout`/`scalcout`, device support for
1323    /// `motorRecord`/`scalerRecord`); for the rest the framework arms
1324    /// `parsed_inp`/`parsed_out` and drives it. While `field_list()` was
1325    /// hand-written and incomplete the conflation was invisible, because the
1326    /// hand-written tables happened to omit exactly the fields the framework
1327    /// owns. A complete, spec-derived `field_list()` makes membership true for
1328    /// every record, so ownership needs its own predicate or the framework
1329    /// would stop arming any link at all.
1330    ///
1331    /// The default answers it truthfully — a record implements the fields its
1332    /// own `get_field` can produce. (Verified equivalent to the old
1333    /// `field_list()` membership on all 1,757 fields of all 40 record types at
1334    /// the time of the split, so the split changed no behaviour.)
1335    fn implements_field(&self, name: &str) -> bool {
1336        self.get_field(name).is_some()
1337    }
1338
1339    /// `SPC_NOMOD` that a record's `cvt_dbaddr` decides **at runtime, from
1340    /// record state** — the dynamic half of the no-modify declaration.
1341    ///
1342    /// [`FieldDesc::read_only`] is the static half: it carries the `.dbd`
1343    /// `special(SPC_NOMOD)` of a field that is immutable for the record type,
1344    /// full stop. But C lets a record's `cvt_dbaddr` *raise* SPC_NOMOD per
1345    /// dbAddr, keyed on the record's own fields, and one record does:
1346    ///
1347    /// ```c
1348    /// /* compressRecord.c:398-407 */
1349    /// static long cvt_dbaddr(DBADDR *paddr) {
1350    ///     ...
1351    ///     if (prec->balg == bufferingALG_LIFO)
1352    ///         paddr->special = SPC_NOMOD;
1353    /// }
1354    /// ```
1355    ///
1356    /// A compress VAL is writable under BALG=FIFO and refused under BALG=LIFO —
1357    /// a per-record-state fact no static `FieldDesc` can express. The one gate
1358    /// that owns field immutability (`field_io::check_no_mod`) consults this
1359    /// hook alongside the static set, so the dynamic refusal reaches EVERY put
1360    /// route exactly as the static one does.
1361    ///
1362    /// (C caches `paddr->special` in the DBADDR at name-resolution time, so a
1363    /// CA channel opened while FIFO keeps writing after a switch to LIFO until
1364    /// it reconnects; `dbpf`, which resolves fresh, is refused immediately. The
1365    /// port evaluates live on every put — the invariant C's own `dbpf` path
1366    /// shows, without the stale-cache hole.)
1367    ///
1368    /// `field` is upper-case. Default: no dynamic NOMOD.
1369    fn field_no_mod(&self, _field: &str) -> bool {
1370        false
1371    }
1372
1373    /// Choice strings for a record-specific `DBF_MENU` field served as
1374    /// `DBR_ENUM`, keyed by field name (uppercase, as declared).
1375    ///
1376    /// EPICS dbStaticLib serves a `DBF_MENU` field as `DBR_ENUM`: the value
1377    /// is the menu index and the field carries its `menu()` choice strings,
1378    /// so `caget`/`pvget` present the labels rather than a bare number
1379    /// (`dbStaticLib.c` `dbGetMenuChoices`; `dbAccess.c` `get_enum_str`).
1380    /// A record returns the label table (in index order) for each field it
1381    /// serves as [`DbFieldType::Enum`] from a `menu()`; the framework
1382    /// attaches it to the field snapshot's `EnumInfo` so the CA/PVA enum
1383    /// encoders present the labels — the same mechanism `bi`/`bo`/`mbbi`/
1384    /// `mbbo` already use for their `VAL` state strings, but per field
1385    /// rather than per record (a record can carry several distinct menus).
1386    ///
1387    /// This is the single owner of "menu field -> choice table": a record
1388    /// declares its menu fields here once, and `get_field` returns the menu
1389    /// index as [`EpicsValue::Enum`]. Default: no record-specific menu
1390    /// fields. The dbCommon menu fields (`SCAN`, etc.) are handled
1391    /// separately by the framework, not here.
1392    ///
1393    /// INVARIANT — answering here is a claim that the field is `DBF_MENU`, so
1394    /// [`FieldDeclaration::field_list`] MUST declare it [`DbFieldType::Enum`]: C's
1395    /// `mapDBFToDBR` serves every `DBF_MENU` as `DBR_ENUM`, and the DECLARED
1396    /// type is what goes on the wire
1397    /// ([`RecordInstance::project_to_declared_type`](super::RecordInstance::project_to_declared_type)).
1398    /// A field with choices but a `Short` declaration is a self-contradictory
1399    /// declaration: it would be served as a bare `DBR_SHORT` index while
1400    /// claiming to have labels for it. `menu_choices_are_served_as_dbr_enum`
1401    /// (`tests/menu_fields_serve_enum_choices.rs`) fails on any record type
1402    /// that breaks it — the storage may be a short, the declaration may not.
1403    fn menu_field_choices(&self, _field: &str) -> Option<&'static [&'static str]> {
1404        None
1405    }
1406
1407    /// Per-field override of the record-level display/control metadata
1408    /// for a GET / monitor snapshot of `field`.
1409    ///
1410    /// C record support serves metadata PER FIELD: the RSET functions
1411    /// `get_units` / `get_precision` / `get_graphic_double` /
1412    /// `get_control_double` / `get_alarm_double` all key on
1413    /// `dbGetFieldIndex(paddr)` and fall back to the `recGbl*` defaults
1414    /// for unlisted fields. The framework's metadata cache is per
1415    /// record (built by `populate_display_info` /
1416    /// `populate_control_info` from the VAL-class fields); a record
1417    /// whose RSET serves different metadata for non-VAL fields
1418    /// overrides this hook to patch the cached values for that field
1419    /// (e.g. the motor record: VELO's display range is VMAX/VBAS, not
1420    /// HLM/LLM — `motorRecord.cc:3247-3250`).
1421    ///
1422    /// Applied on both the GET path (`snapshot_for_field`) and the
1423    /// monitor path (`make_monitor_snapshot`), AFTER the cached
1424    /// record-level metadata — and computed live on each call, so an
1425    /// override derived from non-cached fields can never go stale.
1426    /// `field` is uppercase, as declared in [`FieldDeclaration::field_list`].
1427    /// Default: `None` — record-level metadata serves every field.
1428    fn field_metadata_override(&self, _field: &str) -> Option<FieldMetadataOverride> {
1429        None
1430    }
1431
1432    /// Which of C's six nullable `rset` `get_*` property slots THIS record
1433    /// type implements — the record's own `#define get_xxx NULL` lines.
1434    ///
1435    /// `dbGet` consults the rset to *narrow* the caller's options mask
1436    /// (`dbAccess.c:336-430`): a NULL slot clears the option bit, so the leaf
1437    /// never reaches the client. That is what decides whether QSRV marks an NT
1438    /// leaf at all (pvxs `ioc/iocsource.cpp:263-305`). A slot counts as
1439    /// supplied when the C function pointer is non-NULL **even if the function
1440    /// writes nothing for the field in question** — C leaves the option bit
1441    /// set either way (`boRecord.c:294-299` writes units only for `HIGH`, yet
1442    /// `DBR_UNITS` survives for every `bo` field).
1443    ///
1444    /// The record type owns this answer because the record type owns its C
1445    /// rset. A central table keyed on the record-type *string* cannot: a
1446    /// record implemented in a downstream crate (`asyn-rs`'s `asynRecord`,
1447    /// the motor/scaler/optics types) has no way to add a row to it, so it
1448    /// silently inherited a default that marked every leaf it could name —
1449    /// telling clients a fabricated `display.units` of `""` and
1450    /// `valueAlarm` bands of zero were authoritative.
1451    ///
1452    /// The default answers from `default_property_support`, the
1453    /// transcription of the record types epics-base-rs implements itself.
1454    /// Override it in the record's own file, citing the C rset lines.
1455    fn property_support(&self) -> crate::server::snapshot::PropertySupport {
1456        default_property_support(self.record_type())
1457    }
1458
1459    /// Field names this record serves as a *long string*: a `DBF_CHAR`
1460    /// array field that semantically holds a NUL-terminated string.
1461    ///
1462    /// In EPICS such a field is declared `DBF_NOACCESS` (or carries a `$`
1463    /// modifier) and is accessed through a `DBR_CHAR` array view whose
1464    /// `form` is `"String"`; pvxs maps that view to a scalar `pvString`
1465    /// rather than an `int8[]` (`ioc/channel.cpp:58-68`,
1466    /// `ioc/iocsource.cpp:619-643`). QSRV uses this list to serve those
1467    /// fields as scalar-string NTScalar values instead of byte scalars.
1468    ///
1469    /// The record keeps its `CharArray` storage; the QSRV boundary does
1470    /// the `CharArray <-> String` conversion. Default empty — only
1471    /// long-string record types (`lsi`/`lso` VAL/OVAL, `printf` VAL)
1472    /// override this. Names are matched case-insensitively.
1473    fn long_string_fields(&self) -> &'static [&'static str] {
1474        &[]
1475    }
1476
1477    /// Field names declared `pp(TRUE)` in this record type's DBD (empty if
1478    /// none, e.g. `event`/`histogram`, or if the type is unmodeled).
1479    ///
1480    /// Drives the `dbPutField` processing gate: C `dbAccess.c:1263`
1481    /// re-processes a record on a put only when the put field is `PROC` or it
1482    /// is `pp(TRUE)` **and** `SCAN == Passive`. The table is total and
1483    /// fail-safe — an unmodeled type returns `&[]` (and warns once), so its
1484    /// field puts never auto-process (only `PROC` does). The default consults
1485    /// the central DBD-sourced table keyed by [`Record::record_type`]; record
1486    /// types can override.
1487    fn process_passive_fields(&self) -> &'static [&'static str] {
1488        super::process_passive::pp_fields_for(self.record_type())
1489    }
1490
1491    /// Whether a put to `field` should reprocess this Passive record.
1492    ///
1493    /// The default is pure `pp(TRUE)` membership — the put gate's
1494    /// `field in process_passive_fields()` test. A record type overrides this
1495    /// when its C `special()` conditionally returns ERROR to suppress the
1496    /// reprocess for a `pp(TRUE)` field on certain values (e.g. motor STUP:
1497    /// only a `STUP == ON` put runs the status-update process; any other value
1498    /// is clamped to OFF and C returns ERROR so no process runs). Modeling that
1499    /// here keeps the suppression at the same gate as the pp test, with no
1500    /// per-put one-shot state — the post-clamp field value is deterministic.
1501    fn processes_after_put(&self, field: &str) -> bool {
1502        self.process_passive_fields()
1503            .iter()
1504            .any(|f| f.eq_ignore_ascii_case(field))
1505    }
1506
1507    /// The record's `DBF_ENUM` state strings — the C rset slot pair
1508    /// `get_enum_strs` / `put_enum_str`, which in C read the same fields and
1509    /// are therefore ONE table here.
1510    ///
1511    /// It is what a client reads as the `DBR_ENUM` choice list AND the set of
1512    /// names a `DBR_STRING` put to the record's `DBF_ENUM` `VAL` may name
1513    /// (`dbConvert.c::putStringEnum` → [`crate::server::record::resolve_enum_state_string`]). Taking
1514    /// both from one table is the point: a name the record advertises is a name
1515    /// a client may put, by construction — the two cannot drift.
1516    ///
1517    /// The table is already trimmed to C's `no_str`: `bi`/`bo`/`busy` drop
1518    /// `ONAM` when `ZNAM` is set and `ONAM` is empty (`boRecord.c:342-352`);
1519    /// `mbbi`/`mbbo` cut at the last non-empty state (`mbbiRecord.c:262-269`).
1520    ///
1521    /// `None` — the record type leaves both rset slots NULL. That is every
1522    /// record whose `VAL` is not `DBF_ENUM`, and `mbbiDirect`/`mbboDirect`
1523    /// (`mbbiDirectRecord.c:58` `#define put_enum_str NULL`), whose `VAL` is
1524    /// `DBF_LONG`. C then fails a `DBR_STRING` put with `S_db_noRSET`.
1525    fn enum_state_strings(&self) -> Option<Vec<PvString>> {
1526        None
1527    }
1528
1529    /// The record's `get_enum_str` rset slot — how a `DBR_STRING` READ of its
1530    /// `DBF_ENUM` `VAL` renders. A THIRD slot, distinct from the pair above:
1531    /// C's `get_enum_str` (singular) is not `get_enum_strs` (plural), and
1532    /// serving the read from the plural table is what made an undefined `mbbi`
1533    /// state come out as its index.
1534    ///
1535    /// The difference is the trimming. `get_enum_strs` reports `no_str`, so the
1536    /// label list stops at the last non-empty state; `get_enum_str` indexes the
1537    /// state array *untrimmed* (`mbbiRecord.c:246-250`: any `val <= 15` reads
1538    /// `zrst + val * sizeof(zrst)`, empty or not) and only an index past the
1539    /// array reaches the sentinel. Verified on the compiled C `softIoc`: an
1540    /// `mbbi` with `ZRST`/`ONST` set answers `caget -t` with `""` at `VAL=5` and
1541    /// `"Illegal Value"` at `VAL=20`.
1542    ///
1543    /// `None` — the record leaves the rset slot NULL (`#define get_enum_str
1544    /// NULL`), which is every record but `bi`/`bo`/`mbbi`/`mbbo`. A field on
1545    /// such a record renders from its menu instead; see
1546    /// `RecordInstance::enum_string_form_for`,
1547    /// the single owner that picks between the two.
1548    fn enum_string_form(&self) -> Option<crate::server::snapshot::EnumStringForm> {
1549        None
1550    }
1551
1552    /// Validate a put before it is applied. Return Err to reject.
1553    fn validate_put(&self, _field: &str, _value: &EpicsValue) -> CaResult<()> {
1554        Ok(())
1555    }
1556
1557    /// Hook called after a successful put_field.
1558    fn on_put(&mut self, _field: &str) {}
1559
1560    /// Whether a put to `field` names a subroutine that must resolve in the
1561    /// function registry — C `special(SPC_MOD)` → `registryFunctionFind`
1562    /// (`aSubRecord.c::special`, `subRecord.c::special`). C stores the name in
1563    /// `dbPut`, then `special(after)` looks it up and returns `S_db_BadSub`
1564    /// (→ rsrv `ECA_PUTFAIL`) when the name is non-empty and unregistered, so
1565    /// the client's write is REFUSED while the field keeps the value it was
1566    /// given. An empty name names no routine and is accepted.
1567    ///
1568    /// The record's `special()` has no database handle and so cannot reach the
1569    /// registry itself (the registry is the DB's single owner); this hook only
1570    /// says "a put to `field` is a subroutine name that must be resolved". The
1571    /// put owner — which holds the registry — extracts the name from the write,
1572    /// performs the lookup, and applies the `S_db_BadSub` refusal at the point
1573    /// C's `dbPut` returns the after-put `special()` status. Returns `false`
1574    /// for any field/mode C accepts without a lookup (a non-SNAM field, or an
1575    /// aSub in `LFLG=READ` where the name comes from the SUBL link at process
1576    /// time and a SNAM put is not validated).
1577    fn is_subroutine_name_field(&self, _field: &str) -> bool {
1578        false
1579    }
1580
1581    /// Primary field name (default "VAL"). Override for waveform etc.
1582    fn primary_field(&self) -> &'static str {
1583        "VAL"
1584    }
1585
1586    /// Whether a put to `field` directly defines the record — clears UDF
1587    /// exactly like a primary-value-field put. C `dbAccess.c::dbPut`
1588    /// (`:1409-1411`) clears `udf` synchronously only when the put target is
1589    /// `dbIsValueField` (i.e. `field == primary_field()`); this hook is where
1590    /// a record type says its own `special()` ALSO clears UDF for a
1591    /// non-value field, independent of `dbIsValueField`.
1592    ///
1593    /// `mbboDirect` is the case this exists for: `mbboDirectRecord.c::special`
1594    /// (`after==1`, B0..B1F, line 290) sets `prec->udf = FALSE` on a bit-field
1595    /// put — the bit write is a second value source alongside a VAL put and
1596    /// the closed-loop DOL fetch. Default: only the primary field.
1597    fn is_udf_defining_put(&self, field: &str) -> bool {
1598        field == self.primary_field()
1599    }
1600
1601    /// Get the primary value.
1602    fn val(&self) -> Option<EpicsValue> {
1603        self.get_field(self.primary_field())
1604    }
1605
1606    /// Set the primary value.
1607    ///
1608    /// Matches C EPICS `dbPut` behavior: if the value type doesn't match
1609    /// the field type, it is automatically coerced (e.g., Long→Double for
1610    /// ai, Long→Enum for bi/mbbi). This prevents silent failures when
1611    /// asyn device support provides Int32 values to Enum-typed records.
1612    fn set_val(&mut self, value: EpicsValue) -> CaResult<()> {
1613        // Soft-channel INP/DOL delivery into the record's value field is
1614        // internal delivery, so it takes the same single owner every other
1615        // link target takes — `put_field_internal`. It was a parallel path
1616        // (put_field, then a `TypeMismatch`-triggered `convert_to` off the
1617        // *current* value's type), which silently dropped a shape the typed
1618        // arm rejected and `convert_to` could not fix: an array source into a
1619        // scalar VAL stayed an array and never landed. C's link layer asks for
1620        // one element (`dbGetLink(..., nRequest = NULL)`), so a waveform INP
1621        // into an `ai.VAL` delivers `wf[0]`.
1622        let field = self.primary_field();
1623        self.put_field_internal(field, value)
1624    }
1625
1626    /// Whether this record's `INP` is read by DEVICE SUPPORT (a C `DSET`), as
1627    /// opposed to by the record body itself.
1628    ///
1629    /// The init-time load of a CONSTANT `INP` into the record's value field is
1630    /// soft device support's `init_record` (`devAiSoft.c`, `devLonginSoft.c`,
1631    /// … each call `recGblInitConstantLink(&prec->inp, …)`), so it exists only
1632    /// where a DSET exists. `compress` has no device support at all
1633    /// (`compressRecord.c` declares no `dset`; its `process` calls `dbGetLink`
1634    /// on `INP` itself), which is why C leaves a `field(INP,"5")` compress with
1635    /// an EMPTY circular buffer — the constant is loaded nowhere and delivers
1636    /// nothing at process (`dbConstGetValue`). Seeding it anyway put a phantom
1637    /// sample in the buffer before the first scan.
1638    fn input_read_by_device_support(&self) -> bool {
1639        true
1640    }
1641
1642    /// The rest of the soft INPUT device support's `init_record`, once the
1643    /// constant-INP load above has (or has not) landed.
1644    ///
1645    /// Most soft dsets are exactly `recGblInitConstantLink()` and stop there —
1646    /// a link they could not load leaves the record's own init state alone. The
1647    /// ARRAY dsets do not: `devWfSoft.c:39-51` runs `dbLoadLinkArray` on every
1648    /// waveform and sets `prec->nord = 0` when it fails (a real link, or none —
1649    /// `dbLoadLinkArray` has no `loadArray` lset outside a constant), which is
1650    /// why C serves `NORD = 0` on a `record(waveform,"X"){}` even though the
1651    /// record's own `init_record` seeded `nord = (nelm == 1)` a moment earlier.
1652    ///
1653    /// `loaded` is whether a constant INP reached the value field. Defaulted to
1654    /// a no-op: a dset that only seeds does not need this half.
1655    fn soft_input_dset_init(&mut self, loaded: bool) {
1656        let _ = loaded;
1657    }
1658
1659    /// The `DTYP="Raw Soft Channel"` INPUT dset — C's four `devXxxSoftRaw.c`
1660    /// read supports (`devAiSoftRaw`, `devBiSoftRaw`, `devMbbiSoftRaw`,
1661    /// `devMbbiDirectSoftRaw`).
1662    ///
1663    /// The value read from the INP link goes to **`RVAL`**, not `VAL`: the
1664    /// record's own `RVAL → VAL` convert then runs (that is the whole
1665    /// difference from `"Soft Channel"`, whose `read_xxx` returns 2 and writes
1666    /// `VAL` directly).
1667    ///
1668    /// **`Some`/`None` IS the dset table.** A record type that implements this
1669    /// is one for which C ships a SoftRaw input dset; a record type that does
1670    /// not, C has no such dset for, so `DTYP="Raw Soft Channel"` on it is a
1671    /// configuration C rejects at iocInit. There is no separate boolean saying
1672    /// whether the record "accepts" raw input — that boolean existed, defaulted
1673    /// to `false`, had ONE override in the workspace, and silently sent the
1674    /// other three input records' raw values into `VAL`, where their own convert
1675    /// then overwrote them from an unseeded `RVAL=0` (R19-66). A capability
1676    /// answer that can disagree with the implementation is the bug.
1677    fn raw_soft_input(&mut self, entry: RawSoftEntry, value: EpicsValue) -> Option<CaResult<()>> {
1678        let _ = (entry, value);
1679        None
1680    }
1681
1682    /// The `DTYP="Raw Soft Channel"` OUTPUT dset — C's four `devXxxSoftRaw.c`
1683    /// write supports: the value `write_xxx()` puts to the OUT link.
1684    ///
1685    /// `devAoSoftRaw.c` / `devBoSoftRaw.c` put `RVAL` (`dbPutLink(&prec->out,
1686    /// DBR_LONG, &prec->rval, 1)`); `devMbboSoftRaw.c` /
1687    /// `devMbboDirectSoftRaw.c` put `RVAL & MASK` as `DBR_ULONG`. All four
1688    /// write the RAW word — never the engineering `OVAL` that
1689    /// [`Record::output_link_value`] (the `"Soft Channel"` dset) puts.
1690    ///
1691    /// Same rule as [`Record::raw_soft_input`]: `Some`/`None` IS the dset
1692    /// table. Before this hook existed, a `DTYP="Raw Soft Channel"` output
1693    /// record matched neither the soft-OUT arm (which tests for `"Soft
1694    /// Channel"`) nor the device arm (it has no device), so it wrote **nothing
1695    /// at all** to OUT.
1696    fn raw_soft_output_value(&self) -> Option<EpicsValue> {
1697        None
1698    }
1699
1700    /// Apply a raw device value read *back* from an output record's device
1701    /// support (the asyn init seed and driver readback callback), the output
1702    /// counterpart of an input record's raw path, where device support
1703    /// writes `RVAL` and the record's own conversion produces `VAL`
1704    /// (`device_support.rs:43-46`). An output record whose
1705    /// `convert()` is forward (engineering → raw) must invert it here — store
1706    /// the raw value into `RVAL` and compute the engineering `VAL` — because
1707    /// the framework's forward convert would otherwise recompute `RVAL` from
1708    /// the stale `VAL` and discard the readback (C `processAo`/`initAo` set
1709    /// `rval`/`val` directly, devAsynInt32.c:955-957/:973-994).
1710    ///
1711    /// Returns `true` when the record fully produced `VAL` from the raw value
1712    /// (the asyn store path then reports `computed` so the forward convert is
1713    /// skipped). The default returns `false`: records whose own `convert()` is
1714    /// already `raw → eng` (`ai`) or that need no conversion (`longout`,
1715    /// `mbbo`, whose `set_val` re-derives from the raw value) keep the legacy
1716    /// raw → `RVAL` / direct-`VAL` path.
1717    fn apply_raw_readback(&mut self, _raw: i32) -> bool {
1718        false
1719    }
1720
1721    /// Apply a float64 device value read *back* from an output record's asyn
1722    /// device support — the `asynFloat64` analogue of
1723    /// [`Record::apply_raw_readback`]. A float64 output (`ao`) whose device
1724    /// value carries an `ASLO`/`AOFF` linear scaling must seed the engineering
1725    /// `VAL` here (`VAL = value * ASLO + AOFF`), because the asyn store path
1726    /// would otherwise write the raw device value straight into `VAL` and drop
1727    /// the scaling. Sets `VAL` only (a float64 `ao` carries no `RVAL`); the
1728    /// reverse scaling `(OVAL - AOFF) / ASLO` is applied on the device-write
1729    /// side. Mirrors C `initAo`/`processAo` (devAsynFloat64.c:627-629/:646-649).
1730    ///
1731    /// Returns `true` when the record produced `VAL` from the raw value (the
1732    /// asyn store path then reports `computed`, skipping the forward convert).
1733    /// The default returns `false`: records with no float64 readback scaling
1734    /// keep the raw `set_val` path.
1735    fn apply_float64_readback(&mut self, _raw: f64) -> bool {
1736        false
1737    }
1738
1739    /// Hand the record the database's breakpoint-table registry so an `ai`/`ao`
1740    /// with `LINR >= 3` can resolve and cache the table its `LINR` selects.
1741    /// Called once at iocInit, before the first `process`/`convert`. The record
1742    /// resolves the table lazily on the first conversion (and re-resolves when
1743    /// `LINR` changes at runtime), mirroring C `cvtRawToEngBpt`'s
1744    /// `init || *ppbrk == NULL` cache. The default is a no-op: only `ai`/`ao`
1745    /// carry `LINR`.
1746    fn install_breaktable_registry(
1747        &mut self,
1748        _registry: std::sync::Arc<crate::server::cvt_bpt::BreakTableRegistry>,
1749    ) {
1750    }
1751
1752    /// Apply IVOA=2 ("set outputs to IVOV") semantics: copy the
1753    /// IVOV value into whatever output staging field the OUT
1754    /// writeback consumes for this record type. Mirrors the
1755    /// per-record C `recXxx.c` behaviour:
1756    ///
1757    /// - `ao`/`lso`: `OVAL = IVOV; VAL = OVAL`
1758    /// - `bo`/`busy`/`mbbo`/`mbboDirect`: `RVAL = IVOV; VAL = IVOV`
1759    /// - `calcout`/`scalcout`: `OVAL = IVOV` (VAL is calc input, not
1760    ///   touched on invalid-output)
1761    /// - `dfanout`: `VAL = IVOV` (the broadcast value)
1762    ///
1763    /// Default uses [`Record::set_val`] for records whose OUT path
1764    /// reads VAL only.
1765    fn apply_invalid_output_value(&mut self, ivov: EpicsValue) -> CaResult<()> {
1766        self.set_val(ivov)
1767    }
1768
1769    /// Whether this record type supports device write (output records only).
1770    /// `aao` is included here even though it's served by the same
1771    /// concrete struct as `waveform`/`aai`/`subArray` — the
1772    /// WaveformRecord's `can_device_write` override picks the right
1773    /// answer per [`crate::server::records::waveform::ArrayKind`], but this default matters for code that
1774    /// only has the record-type string.
1775    fn can_device_write(&self) -> bool {
1776        matches!(
1777            self.record_type(),
1778            "ao" | "bo"
1779                | "longout"
1780                | "int64out"
1781                | "mbbo"
1782                | "mbboDirect"
1783                | "stringout"
1784                | "lso"
1785                | "printf"
1786                | "aao"
1787        )
1788    }
1789
1790    /// Whether async processing has completed and put_notify can respond.
1791    /// Records that return AsyncPendingNotify should return false while
1792    /// async work is in progress, and true when done.
1793    /// Default: true (synchronous records are always complete).
1794    fn is_put_complete(&self) -> bool {
1795        true
1796    }
1797
1798    /// Whether this record should fire its forward link after processing.
1799    fn should_fire_forward_link(&self) -> bool {
1800        true
1801    }
1802
1803    /// C parity: a record whose completion restamps `TIME` AFTER the VAL
1804    /// monitor post and the forward link, not before the value post like
1805    /// every standard record (`aoRecord.c:190` stamps ahead of `writeValue`).
1806    ///
1807    /// Only sseq's `asyncFinish` (`sseqRecord.c`) has this ordering: it posts
1808    /// VAL at `:474`, runs `recGblFwdLink` at `:499`, and only then calls
1809    /// `recGblGetTimeStamp` at `:501`. So the first VAL monitor event carries
1810    /// the record's pre-update timestamp, and `TIME` advances only for the
1811    /// following BUSY post and the next cycle — the VAL timestamp always lags
1812    /// one completion behind. The framework's synchronous `Complete` tail
1813    /// consults this to skip the pre-output restamp and apply it after the
1814    /// forward-link tail instead. Default false: every other record (including
1815    /// the base `seq`, whose `seqRecord.c:224` restamps BEFORE the `:229` VAL
1816    /// post) stamps before the value post.
1817    fn restamps_time_after_completion(&self) -> bool {
1818        false
1819    }
1820
1821    /// Whether this record's OUT link should be written after processing.
1822    /// Defaults to true. Override in calcout / longout to implement OOPT
1823    /// conditional output (epics-base 7.0.8).
1824    fn should_output(&self) -> bool {
1825        true
1826    }
1827
1828    /// Notify the record that the OUT-link / device write completed
1829    /// successfully on this cycle. The framework calls this right after
1830    /// the actual write so transition-detection state (e.g.
1831    /// `longout.pval`) can update for the next cycle's
1832    /// [`Self::should_output`] check. Default: no-op.
1833    fn on_output_complete(&mut self) {}
1834
1835    /// Whether this record uses MDEL/ADEL deadband for monitor posting.
1836    /// Binary records (bi, bo, busy, mbbi, mbbo) return false because
1837    /// C EPICS always posts monitors for these record types regardless
1838    /// of whether the value changed.
1839    fn uses_monitor_deadband(&self) -> bool {
1840        true
1841    }
1842
1843    /// Whether this record's process cycle posts its primary value (`VAL`)
1844    /// as a value monitor (`DBE_VALUE` / `DBE_LOG`).
1845    ///
1846    /// Default `true`: for most records C `monitor()` posts `VAL` whenever the
1847    /// value moved (deadband, change-gate, or always).
1848    ///
1849    /// `false` for the "trigger" records `fanout` and `seq`. Their `VAL` is
1850    /// `field(VAL,DBF_LONG){ pp(TRUE) }` — "Used to trigger" — and their C
1851    /// `process()` posts `VAL` ONLY with the alarm events `recGblResetAlarms`
1852    /// returns: `if (events) db_post_events(prec, &prec->val, events)`
1853    /// (fanoutRecord.c:148-150, seqRecord.c:227-229), never `DBE_VALUE` /
1854    /// `DBE_LOG`. Writing `VAL` fans out the forward links / sequences the
1855    /// `DOn`→`LNKn` writes; the value itself is not a monitored quantity, so a
1856    /// run of `caput VAL` posts no per-put value event (only the initial
1857    /// subscription snapshot fires). The alarm bits still reach `VAL` through
1858    /// the deadband post's `alarm_bits`, so an alarm transition posts `VAL`
1859    /// with `DBE_ALARM` exactly as C's `if (events)` does.
1860    fn process_posts_value_monitor(&self) -> bool {
1861        true
1862    }
1863
1864    /// Per-record VALUE/LOG monitor gate for record types that post a
1865    /// monitor *only when the value actually changed* — and have no
1866    /// MDEL/ADEL deadband to express that.
1867    ///
1868    /// `Some(changed)` makes the framework post the VALUE and LOG
1869    /// monitors iff `changed`; `None` (the default) leaves the decision
1870    /// to the deadband / always-post path.
1871    ///
1872    /// C `lsiRecord.c`/`lsoRecord.c` `monitor()` raise `DBE_VALUE |
1873    /// DBE_LOG` only when `len != olen || memcmp(oval, val, len)`. Those
1874    /// records return [`Self::uses_monitor_deadband`]`== false`, which
1875    /// otherwise routes them to the unconditional always-post path
1876    /// (correct for binary records, wrong for lsi/lso). Because the
1877    /// framework posts monitors *after* `process()` — by which point the
1878    /// record has already committed `oval`/`olen` — the implementation
1879    /// captures the comparison result during `process()` and returns the
1880    /// captured flag here, not a live re-comparison.
1881    fn monitor_value_changed(&self) -> Option<bool> {
1882        None
1883    }
1884
1885    /// `menuPost` "Always" override for the VALUE / LOG monitor masks.
1886    ///
1887    /// Returns `(post_value_always, post_archive_always)`. The framework
1888    /// ORs these into the change-gated mask from
1889    /// [`Self::monitor_value_changed`], so an *unchanged* process cycle
1890    /// still posts `DBE_VALUE` (resp. `DBE_LOG`) when the record's MPST
1891    /// (resp. APST) menu field is set to `Always`.
1892    ///
1893    /// C `lsiRecord.c`/`lsoRecord.c` `monitor()` compute the VAL post
1894    /// mask from three independent inputs:
1895    ///
1896    /// * the change test `len != olen || memcmp(oval, val, len)` →
1897    ///   `DBE_VALUE | DBE_LOG`,
1898    /// * `if (mpst == menuPost_Always) events |= DBE_VALUE;`,
1899    /// * `if (apst == menuPost_Always) events |= DBE_LOG;`.
1900    ///
1901    /// [`Self::monitor_value_changed`] carries the first input; this hook
1902    /// carries the other two. Records without a `menuPost` field keep the
1903    /// default `(false, false)`, which leaves the change gate unchanged.
1904    fn monitor_always_post(&self) -> (bool, bool) {
1905        (false, false)
1906    }
1907
1908    /// The value the MDEL/ADEL deadband is evaluated against.
1909    ///
1910    /// For most records C `monitor()` applies the value deadband to
1911    /// `VAL`, so the default is [`Self::val`]. A record whose monitored
1912    /// quantity is not its primary value must override this: the motor
1913    /// record, for instance, has `VAL` as the setpoint and applies
1914    /// MDEL/ADEL to `RBV` (the readback) — its C `monitor()` deadbands
1915    /// `RBV`, not `VAL`. Such a record returns its readback field here.
1916    ///
1917    /// Default is `val()`, so existing records are unaffected.
1918    fn monitor_deadband_value(&self) -> Option<EpicsValue> {
1919        self.val()
1920    }
1921
1922    /// The FIELD whose VALUE/LOG monitor delivery the MDEL/ADEL
1923    /// deadband gates — the field [`Self::monitor_deadband_value`]
1924    /// reads. A record overriding one must override both consistently.
1925    ///
1926    /// For most records the deadband gates the primary value itself,
1927    /// so the default returns [`Self::primary_field`] and nothing
1928    /// changes. The motor record deadbands RBV: C `monitor()`
1929    /// (motorRecord.cc:3468-3507) throttles the RBV post with
1930    /// MDEL/ADEL, while VAL is posted only when an actual setpoint
1931    /// change marked it (M_VAL). When this returns a non-primary
1932    /// field, the framework's snapshot builders:
1933    ///
1934    /// * deliver THIS field on the deadband triggers (instead of raw
1935    ///   change-detection), and
1936    /// * route the primary field through generic change-detection, so
1937    ///   an unchanged setpoint is not re-posted on every readback
1938    ///   poll.
1939    fn monitor_deadband_field(&self) -> &'static str {
1940        self.primary_field()
1941    }
1942
1943    /// Fields the record's C `monitor()` posts on every cycle whose
1944    /// alarm transition fired, even when their value did not change.
1945    ///
1946    /// C motorRecord.cc `monitor()` (3513-3645) computes
1947    /// `local_mask = monitor_mask | (MARKED(x) ? DBE_VAL_LOG : 0)`
1948    /// for each field in its posting list — when the alarm moved
1949    /// (`monitor_mask != 0`), `local_mask` is non-zero for UNMARKED
1950    /// fields too, so every listed field posts with `DBE_ALARM` and a
1951    /// `DBE_ALARM`-only subscriber observes the alarm moment on any of
1952    /// them. The framework's change-detection loop posts a listed,
1953    /// subscribed, unchanged field with the cycle's alarm bits when
1954    /// this list names it.
1955    ///
1956    /// Default: empty — most C record types post only their value
1957    /// field(s) on an alarm transition (aiRecord.c `monitor()` posts
1958    /// VAL with `monitor_mask` and RVAL only when it changed), which
1959    /// the deadband-field post already covers.
1960    fn alarm_cycle_monitored_fields(&self) -> &'static [&'static str] {
1961        &[]
1962    }
1963
1964    /// Fields the record's C `monitor()` re-posts with `DBE_VAL_LOG` on
1965    /// every cycle that recomputed them, even when the value did not
1966    /// change — the analogue of an unconditional `MARK(field)` in C.
1967    ///
1968    /// Unlike [`Self::alarm_cycle_monitored_fields`] (which posts unchanged
1969    /// fields only on a cycle whose alarm transition fired), these post on
1970    /// any cycle the record names them, with `DBE_VALUE | DBE_LOG` (plus the
1971    /// cycle's alarm bits when one fired). The framework's change-detection
1972    /// loop posts a listed, subscribed, unchanged field with that mask.
1973    ///
1974    /// C motorRecord `process_motor_info` (motorRecord.cc:3764-3767)
1975    /// `MARK`s `M_DIFF`/`M_RDIF` unconditionally on every `CALLBACK_DATA`
1976    /// pass, and `monitor()` (3522-3531) posts them with `monitor_mask |
1977    /// DBE_VAL_LOG`; a `camonitor DIFF` on an axis parked at a constant
1978    /// non-zero following error thus gets an event every poll. The record
1979    /// returns the fields ONLY on the cycles it actually re-marked them (it
1980    /// reads its own per-cycle state), so a pass that did not recompute them
1981    /// does not over-post.
1982    ///
1983    /// Default: empty — most record types post a field only when it
1984    /// changed (or on an alarm transition), which the existing gates cover.
1985    fn force_posted_fields(&self) -> &'static [&'static str] {
1986        &[]
1987    }
1988
1989    /// Fields this cycle's C `monitor()` posted UNCONDITIONALLY, chosen per
1990    /// cycle from record state — the DYNAMIC sibling of
1991    /// [`Self::force_posted_fields`].
1992    ///
1993    /// Some records decide which fields to post from a per-cycle BIT MASK
1994    /// rather than from a fixed list. aCalcout has two of them, and neither
1995    /// consults the value: `afterCalc` posts exactly the AMASK-flagged array
1996    /// fields — the ones the expression STORED into
1997    /// (`aCalcoutRecord.c:293-297`) — and `monitor()` posts exactly the
1998    /// NEWM-flagged ones — the input arrays whose link delivered a CHANGED
1999    /// value (`:1031-1036`). `AA := AA` therefore still posts AA.
2000    ///
2001    /// Neither existing gate can express that. The change-detection loop posts
2002    /// only what moved, so it drops the store-the-same-value case;
2003    /// [`Self::force_posted_fields`] is `&'static`, so it cannot name a set
2004    /// that varies per cycle (twelve arrays, 2^12 combinations) without
2005    /// over-posting every one of them every cycle.
2006    ///
2007    /// Each entry is ONE C `db_post_events` call, with the mask THAT call site
2008    /// uses ([`CyclePostMask`]) — not one entry per field. The two aCalcout call
2009    /// sites disagree on the mask (`afterCalc` posts a literal `DBE_VALUE|
2010    /// DBE_LOG`, `monitor()` posts `monitor_mask|DBE_VALUE|DBE_LOG`), and an
2011    /// array in BOTH masks is posted TWICE by C, once from each. So a field may
2012    /// legitimately appear twice, and the framework emits an event per entry.
2013    ///
2014    /// TAKE semantics: called exactly once per process cycle, and the record
2015    /// clears whatever state it answered from — C's `pcalc->newm = 0` (`:1036`)
2016    /// is part of the same step.
2017    ///
2018    /// Default: empty — and `Vec::new()` does not allocate.
2019    fn take_cycle_posted_fields(&mut self) -> Vec<(&'static str, CyclePostMask)> {
2020        Vec::new()
2021    }
2022
2023    /// Fields whose ONLY post path is the record's own per-cycle mark — C never
2024    /// change-detects them, so a value change alone must post nothing.
2025    ///
2026    /// aCalcout's arrays AA..LL are the case. C's `monitor()` compares scalar
2027    /// A..L against their PA..PL previous values (`aCalcoutRecord.c:1023-1029`)
2028    /// and OVAL against POVL (`:1039`), but it keeps NO previous copy of an
2029    /// array and runs no array comparison anywhere: an array posts if and only
2030    /// if the expression stored into it (AMASK, `afterCalc` `:293-297`) or its
2031    /// input link delivered a changed value (NEWM, `:1031-1036`) — both reported
2032    /// by [`Self::take_cycle_posted_fields`].
2033    ///
2034    /// So the change-detection arm must not see these fields at all. It is not
2035    /// merely redundant with the marks: a client `caput` to AA posts the put's
2036    /// value without advancing the subscriber's `last_posted`, and the next
2037    /// process — which stored nothing into AA and fetched nothing into it —
2038    /// then found AA "changed" and emitted a post C has no counterpart for.
2039    ///
2040    /// Default: empty — every other record's auxiliary fields post on change.
2041    fn fields_posted_only_when_marked(&self) -> &'static [&'static str] {
2042        &[]
2043    }
2044
2045    /// Fields the record's C `monitor()` re-posts with `DBE_LOG` ONLY on
2046    /// every cycle it names them, regardless of change — the analogue of
2047    /// an unconditional `db_post_events(field, DBE_LOG)` sweep.
2048    ///
2049    /// Distinct from [`Self::force_posted_fields`], which posts with
2050    /// `DBE_VALUE | DBE_LOG`: these post with `DBE_LOG` alone, so only a
2051    /// `DBE_LOG` (archiver) subscriber receives the event.
2052    ///
2053    /// The sweep is an INDEPENDENT post, NOT an alternative to the
2054    /// change-detected post. C's `db_post_events` calls compose: on the
2055    /// scaler's count-completion cycle `updateCounts()` posts each changed
2056    /// `Sn` with `DBE_VALUE` (scalerRecord.c:582) and then `monitor()` —
2057    /// reached because the done-interrupt set `ss = IDLE` (`:367`,
2058    /// `:510`) — posts the SAME `Sn` again with a literal `DBE_LOG`
2059    /// (`:757-773`). Two events, one field, one cycle. So the framework
2060    /// emits the sweep post in addition to whatever the change detection
2061    /// produced; gating it on "did not change" would silently drop the
2062    /// `DBE_LOG` half on exactly the cycle that carries the final counts.
2063    ///
2064    /// For a field that is ALSO a [`Self::value_only_change_fields`]
2065    /// member (scaler `Sn`) the change post carries `DBE_VALUE` only, so
2066    /// this sweep is the sole source of its `DBE_LOG` events, matching C.
2067    ///
2068    /// The record returns the names ONLY on the cycles whose C `monitor()`
2069    /// runs — the scaler reads its own `ss` state and returns `S1..Snch`
2070    /// while idle, empty while counting (a counting cycle never reaches C
2071    /// `monitor()`).
2072    ///
2073    /// The sweep post carries `DBE_LOG` plus the cycle's ALARM-transition bits
2074    /// (`recGblResetAlarms`). C's scaler computes that mask and then posts with a
2075    /// literal `DBE_LOG`, dropping the alarm bit — CBUG-B19, a deliberate
2076    /// deviation; see the post site in `collect_subscriber_posts`.
2077    ///
2078    /// Default: empty — most record types have no LOG-only sweep.
2079    fn log_swept_fields(&self) -> &'static [&'static str] {
2080        &[]
2081    }
2082
2083    /// Fields whose change-detected monitor post must carry `DBE_VALUE`
2084    /// only — the LOG bit is stripped — instead of the framework default
2085    /// `DBE_VALUE | DBE_LOG`.
2086    ///
2087    /// The generic change-detection post (and the deadband post for a
2088    /// deadband field named here) normally bundles `DBE_LOG` so an
2089    /// archiver subscribed `DBE_LOG` sees every value change. A record
2090    /// whose C `db_post_events` calls pass a literal `DBE_VALUE` for
2091    /// these fields names them here so the framework drops the LOG bit;
2092    /// the cycle's alarm bits are still OR'd in (alarm posting is a
2093    /// separate per-field contract, unaffected by this hook).
2094    ///
2095    /// C `scalerRecord.c` posts CNT/T/VAL/PR1/TP/FREQ and each active
2096    /// channel `S1..Snch` with a literal `DBE_VALUE` on a value change
2097    /// (scalerRecord.c:372,478,582,588 et al.); `DBE_LOG` appears ONLY in
2098    /// the idle `monitor()` sweep ([`Self::log_swept_fields`],
2099    /// scalerRecord.c:771). The two hooks are complementary: a `DBE_LOG`
2100    /// subscriber on `Sn` is served by the idle sweep, never by a
2101    /// counting-cycle value change — matching C.
2102    ///
2103    /// Default: empty — most record types post changes with
2104    /// `DBE_VALUE | DBE_LOG` (C `monitor_mask | DBE_VALUE | DBE_LOG`,
2105    /// calcRecord.c:420, subRecord.c:400).
2106    fn value_only_change_fields(&self) -> &'static [&'static str] {
2107        &[]
2108    }
2109
2110    /// Secondary value fields a record posts with the *primary VAL
2111    /// monitor mask*, from INSIDE the same guard C wraps its VAL post in —
2112    /// never with a forced `DBE_VALUE | DBE_LOG` on every change.
2113    ///
2114    /// Mirrors C records that drive a raw secondary field with the shared
2115    /// `monitor_mask` rather than `monitor_mask | DBE_VALUE | DBE_LOG`. Each
2116    /// entry pairs the field with the gate C applies to it *inside* that
2117    /// guard — see [`ValuePostGate`], which is the whole reason this is a
2118    /// pair and not a bare name: `ai` re-tests the raw value
2119    /// (`if (prec->oraw != prec->rval)`) while `timestamp` does not.
2120    ///
2121    /// Distinct from the default change-detected aux post (which carries
2122    /// `DBE_VALUE | DBE_LOG` unconditionally): ao `RVAL`/`RBV`, mbbo/
2123    /// mbboDirect/mbbiDirect `RVAL`/`RBV`, sel `SELN` and compress `NUSE`
2124    /// are all posted by C with the `DBE_VALUE | DBE_LOG`-forced mask, so
2125    /// they stay on the default path and must NOT be named here.
2126    ///
2127    /// Default: empty.
2128    fn fields_posted_with_value_mask(&self) -> &'static [(&'static str, ValuePostGate)] {
2129        &[]
2130    }
2131
2132    /// Change-detected auxiliary fields this record posts with C's
2133    /// `monitor_mask | DBE_VALUE` — VAL's monitor mask ORed with `DBE_VALUE`,
2134    /// and NOT the framework default `monitor_mask | DBE_VALUE | DBE_LOG`.
2135    ///
2136    /// The difference is the forced `DBE_LOG`. For a field named here the LOG
2137    /// bit is present only when it is already in VAL's monitor mask — i.e. only
2138    /// when VAL's own ADEL deadband crossed this cycle — so a `DBE_LOG`
2139    /// subscriber (an archiver) receives the field exactly on the cycles C
2140    /// sends it, instead of on every change.
2141    ///
2142    /// `swaitRecord.c::monitor` (646-653) is this shape for its A..L inputs:
2143    ///
2144    /// ```c
2145    /// if (*pnew != *pprev)
2146    ///     db_post_events(pwait, pnew, monitor_mask | DBE_VALUE);
2147    /// ```
2148    ///
2149    /// while `calcRecord.c:420` — the same loop, one module over — writes
2150    /// `monitor_mask | DBE_VALUE | DBE_LOG`. The two records genuinely differ,
2151    /// so the mask is a per-record property, not a framework-wide rule.
2152    ///
2153    /// Distinct from [`Self::value_only_change_fields`] (a literal `DBE_VALUE`,
2154    /// which drops the ADEL LOG bit as well) and from
2155    /// [`Self::fields_posted_with_value_mask`] (posted from INSIDE C's
2156    /// `if (monitor_mask)` guard, so they do not post at all on a cycle where
2157    /// VAL itself does not). The fields named here post on every change,
2158    /// guard or no guard.
2159    ///
2160    /// Default: empty.
2161    fn fields_posted_with_monitor_mask(&self) -> &'static [&'static str] {
2162        &[]
2163    }
2164
2165    /// Fields whose change post carries a LITERAL `DBE_VALUE | DBE_LOG` — this
2166    /// cycle's alarm bits DISCARDED.
2167    ///
2168    /// C's fourth mask shape, and the only one that *drops* information the
2169    /// record already computed. `epidRecord.c::monitor` builds VAL's mask from
2170    /// `recGblResetAlarms` (`:350`) and posts VAL with it, then REASSIGNS
2171    /// (not `|=`) before the secondaries:
2172    ///
2173    /// ```c
2174    /// monitor_mask = DBE_LOG|DBE_VALUE;          /* :376 */
2175    /// if (pepid->ovlp != pepid->oval) db_post_events(pepid, &pepid->oval, monitor_mask);
2176    /// ...                                        /* P, I, D, CT, DT, ERR, CVAL */
2177    /// ```
2178    ///
2179    /// so on an alarm-transition cycle a `DBE_ALARM`-only subscriber to one of
2180    /// those fields is sent NOTHING, while the generic aux mask
2181    /// (`alarm_bits | DBE_VALUE | DBE_LOG`) would send it an event.
2182    ///
2183    /// Distinct from the three narrower shapes: [`Self::value_only_change_fields`]
2184    /// (literal `DBE_VALUE`), [`Self::fields_posted_with_monitor_mask`]
2185    /// (`monitor_mask | DBE_VALUE` — keeps the alarm bits AND VAL's ADEL LOG bit)
2186    /// and [`Self::fields_posted_with_value_mask`] (VAL's mask, posted from inside
2187    /// C's `if (monitor_mask)` guard). A field named here posts on every change,
2188    /// with both value classes and no alarm class, whatever the cycle's alarms did.
2189    ///
2190    /// Resolved for every change-detected field by `AuxPostMask::mask_for`, the single
2191    /// owner of the aux-post mask.
2192    ///
2193    /// Default: empty.
2194    fn fields_posted_without_alarm_bits(&self) -> &'static [&'static str] {
2195        &[]
2196    }
2197
2198    /// The array-style monitor decision (C waveform/aai/aao `monitor()`,
2199    /// waveformRecord.c:291-326). `None` (the default) means the record has
2200    /// no MPST/APST/HASH mechanism and the generic MDEL/ADEL deadband
2201    /// decision applies. `Some(_)` lets the record replace that with its
2202    /// "Always vs On Change" rule: it hashes the array content, compares to
2203    /// the stored `HASH`, updates it, and reports whether `DBE_VALUE` /
2204    /// `DBE_LOG` should be on the VAL post this cycle and whether the hash
2205    /// changed (so the owner posts `HASH` with `DBE_VALUE`). Called by
2206    /// `check_deadband_ext` (the single owner of the VAL-mask decision).
2207    ///
2208    /// The hook is the VAL mask, not the MPST/APST rule specifically: any
2209    /// record whose C `monitor()` decides the mask by its own rule implements
2210    /// it. `histogram` is the other implementor — its rule is the MDEL COUNT
2211    /// deadband (`mcnt > mdel`, histogramRecord.c:287-291), and like waveform's
2212    /// it updates the state it keys on (there, `MCNT = 0`; here, `HASH`).
2213    fn array_monitor_post(&mut self) -> Option<ArrayMonitorPost> {
2214        None
2215    }
2216
2217    /// Fields the record posts itself via an event-driven, individually
2218    /// masked path rather than the generic change-detection loop. The
2219    /// framework excludes these from that loop so they are neither
2220    /// double-posted nor spuriously posted on a cycle the event did not
2221    /// fire. C waveform/aai/aao `monitor()` posts `HASH` this way —
2222    /// `db_post_events(prec, &prec->hash, DBE_VALUE)` only when the content
2223    /// hash changed (waveformRecord.c:317-319), never via VAL's change.
2224    ///
2225    /// The CLOSED set of fields a process cycle of this record may post —
2226    /// the record's C `process()` + `monitor()` `db_post_events` calls,
2227    /// enumerated.
2228    ///
2229    /// `None` (the default) leaves the framework's generic rule in force:
2230    /// every subscribed field that changed since its last post is posted.
2231    /// That rule is right for a record whose C `monitor()` walks its fields
2232    /// and posts whatever moved (calc, sub, ai …). It is WRONG for a record
2233    /// whose C `monitor()` posts a fixed list and leaves every other field it
2234    /// wrote silent — the framework then invents events C never sends:
2235    ///
2236    /// * a field the record WRITES during `process()` but C never posts
2237    ///   (scaler's gate→direction copy, `scalerRecord.c:413-414`: `pdir[i] =
2238    ///   pgate[i]` with no `db_post_events` — C posts `Dn` only from
2239    ///   `special()`).
2240    ///
2241    /// (A field a PUT already posted is NOT in that category: the put's own
2242    /// post advances `last_posted` — see the `RecordInstance::last_posted`
2243    /// contract — so the next process cycle does not change-detect it. This
2244    /// hook must not be used to paper over a framework double post.)
2245    ///
2246    /// `Some(list)` closes it by construction: a field outside the list is
2247    /// never posted by a process cycle — its only monitors come from its own
2248    /// put and from [`Self::monitor_side_effect_fields`]. The list is a
2249    /// whitelist, not a blacklist, so a field added to the record later stays
2250    /// silent unless C posts it.
2251    ///
2252    /// Fields inside the list keep their normal treatment (change detection,
2253    /// [`Self::value_only_change_fields`] mask, deadband, `log_swept_fields`).
2254    fn process_posted_fields(&self) -> Option<&'static [&'static str]> {
2255        None
2256    }
2257
2258    /// Fields the record posts itself via an event-driven, individually
2259    /// masked path rather than the generic change-detection loop. The
2260    /// framework excludes these from that loop so they are neither
2261    /// double-posted nor spuriously posted on a cycle the event did not
2262    /// fire. C waveform/aai/aao `monitor()` posts `HASH` this way —
2263    /// `db_post_events(prec, &prec->hash, DBE_VALUE)` only when the content
2264    /// hash changed (waveformRecord.c:317-319), never via VAL's change.
2265    ///
2266    /// Default: empty.
2267    fn event_posted_fields(&self) -> &'static [&'static str] {
2268        &[]
2269    }
2270
2271    /// Initialize record (pass 0: field defaults; pass 1: dependent init).
2272    fn init_record(&mut self, _pass: u8) -> CaResult<()> {
2273        Ok(())
2274    }
2275
2276    /// Did `init_record` leave the record PERMANENTLY ACTIVE — C's
2277    /// `prec->pact = TRUE` inside `init_record`, which is how a record type
2278    /// disables itself when it cannot possibly process?
2279    ///
2280    /// `subRecord.c:119-123` is the live case: an empty `SNAM` has no
2281    /// subroutine to call, so C prints `"%s.SNAM is empty"`, sets `pact = TRUE`
2282    /// and returns 0. The record exists and serves its fields, but `dbProcess`
2283    /// takes the PACT-active branch on every scan from then on — it never runs
2284    /// record support again. `caget X.PACT` on a bare `record(sub,"X"){}` reads
2285    /// 1 on a C IOC.
2286    ///
2287    /// PACT is a `dbCommon` field with a single owner
2288    /// ([`crate::server::record::RecordInstance::enter_pact`] / [`leave_pact`]), so a record cannot
2289    /// park it from `init_record`. It reports the fact here and the owner
2290    /// performs the transition, once, at the end of the init passes.
2291    ///
2292    /// [`leave_pact`]: crate::server::record::RecordInstance::leave_pact
2293    fn init_record_parks_pact(&self) -> bool {
2294        false
2295    }
2296
2297    /// Post-init finalisation hook with mutable access to the
2298    /// framework's UDF flag. Called once after both `init_record`
2299    /// passes complete. Default implementation is a no-op.
2300    ///
2301    /// epics-base PR `dabcf89` (mbboDirect): when VAL is undefined
2302    /// at init time but the user populated B0..B1F bits, the bits
2303    /// should be folded into VAL and UDF cleared. The framework
2304    /// owns `common.udf`, so the record cannot mutate it from
2305    /// `init_record` alone — this hook is the controlled point of
2306    /// access.
2307    fn post_init_finalize_undef(&mut self, _udf: &mut bool) -> CaResult<()> {
2308        Ok(())
2309    }
2310
2311    /// Whether this record type's C `init_record` resets the record to a
2312    /// defined, no-alarm state — `prec->udf = 0; recGblResetAlarms(prec)` — so
2313    /// a freshly loaded, never-processed record reads `UDF=0`,
2314    /// `STAT=NO_ALARM`, `SEVR=NO_ALARM` instead of the born `UDF`/`INVALID`/`1`.
2315    ///
2316    /// Almost no record does this: a not-yet-processed record is normally left
2317    /// `UDF` so an `MS` consumer inherits `INVALID` at IOC startup (see
2318    /// `RecordInstance::run_init_passes`). The asyn record is the exception —
2319    /// `asynRecord.c` `init_record` pass 0 does `pasynRec->udf = 0;
2320    /// recGblResetAlarms(pasynRec)` unconditionally, because a device-config
2321    /// record is defined the moment it loads, even against a disconnected port.
2322    /// `UDF` is a common field `init_record` cannot reach, so the record
2323    /// declares the fact here and the init owner performs the reset. Default
2324    /// `false`.
2325    fn init_resets_alarms(&self) -> bool {
2326        false
2327    }
2328
2329    /// The channel's native (maximum) element count for `field`, when it
2330    /// differs from the count of the field's current value.
2331    ///
2332    /// C's `cvt_dbaddr` fixes a channel's `no_elements` at the field's buffer
2333    /// capacity, while `get_array_info` reports the current valid length — so a
2334    /// client's `ca_element_count` is the capacity even though a GET returns
2335    /// fewer elements. Return `Some(capacity)` for such a field; `None`
2336    /// (default) means the channel count is the value's own count.
2337    ///
2338    /// - waveform `VAL` → `NELM` (buffer capacity; the value serves `NORD`).
2339    /// - asyn `BOUT` → `OMAX`, `BINP` → `IMAX` (the `SPC_DBADDR` octet buffers;
2340    ///   the value serves the transferred byte count `NOWT`/`NORD`).
2341    fn field_native_count(&self, _field: &str) -> Option<u32> {
2342        None
2343    }
2344
2345    /// Seed the monitor/archive/alarm deadband trackers (MLST/ALST/LALM)
2346    /// from the initial value at iocInit, called once by the builder after
2347    /// both `init_record` passes and `post_init_finalize_undef`.
2348    ///
2349    /// Every C value record's `init_record` ends with
2350    /// `prec->mlst = prec->alst = prec->lalm = prec->val`
2351    /// (e.g. `longinRecord.c:120-122`, `aiRecord.c`), so the first
2352    /// `monitor()` evaluates `DELTA(mlst, val) > mdel` with `mlst == val`
2353    /// (= 0) and posts no DBE_VALUE/DBE_LOG event when the value is
2354    /// unchanged from its initial state. Records expose MLST/ALST/LALM as
2355    /// plain `f64` fields default-initialised to `0.0`; that default
2356    /// conflates "never published" with "published 0", so a record
2357    /// initialised to a *nonzero* value (constant DOL, initial VAL) used
2358    /// to post a spurious first-cycle update that C does not.
2359    ///
2360    /// The default seeds whichever of MLST/ALST/LALM the record actually
2361    /// serves from its monitor-deadband value (`val` for most records),
2362    /// making the invariant hold by construction for every record rather
2363    /// than per-type `init_record` code. It is idempotent for the record
2364    /// types that already seed inside `init_record`, and a no-op for
2365    /// records that serve none of these fields.
2366    fn seed_deadband_tracking(&mut self) {
2367        let seed = match self.monitor_deadband_value().and_then(|v| v.to_f64()) {
2368            Some(v) if v.is_finite() => v,
2369            _ => return,
2370        };
2371        for field in ["MLST", "ALST", "LALM"] {
2372            if self.get_field(field).is_some() {
2373                let _ = self.put_field(field, EpicsValue::Double(seed));
2374            }
2375        }
2376    }
2377
2378    /// Called by the framework immediately after applying this cycle's
2379    /// [`Record::multi_input_links`] fetches, before `process()`.
2380    ///
2381    /// `resolved` lists the `link_field` names (the first element of
2382    /// each `multi_input_links` pair) whose fetch SUCCEEDED this cycle —
2383    /// C's `RTN_SUCCESS(dbGetLink(...))`, i.e. status 0. That includes a
2384    /// CONSTANT link, which returns success having delivered nothing
2385    /// (`dbConstGetValue`, `dbConstLink.c:219-225`) — `epidRecord.c:191`
2386    /// clears UDF on exactly that. A link field absent from the slice
2387    /// either had no link configured or its DB/CA fetch FAILED.
2388    ///
2389    /// This is the framework analogue of C device support inspecting
2390    /// `RTN_SUCCESS(dbGetLink(...))` — e.g. `epidRecord.c:191-193`
2391    /// clears `udf` only when `dbGetLink(&prec->stpl, ...)` returns
2392    /// success. A record's `process()` cannot otherwise observe whether
2393    /// an input link's fetch succeeded, because a failed fetch simply
2394    /// leaves the target field unwritten.
2395    ///
2396    /// Additive, framework-set-hook pattern (same shape as
2397    /// [`Record::set_process_context`]). Default: ignore.
2398    fn set_resolved_input_links(&mut self, _resolved: &[&'static str]) {}
2399
2400    /// Report this cycle's `fetch_values()` outcome: `failed == true` means C's
2401    /// helper would have returned a non-zero status, so the record body — the
2402    /// `calcPerform` / `do_sel` the C `process()` wraps in
2403    /// `if (fetch_values(prec) == 0)` — must NOT run, and VAL/UDF freeze.
2404    ///
2405    /// This is the single delivery point for that outcome, whichever
2406    /// [`InputFetchPolicy`] produced it (a failed link read) and whichever
2407    /// record-specific rule did (sel's `Specified`-mode selected-input read).
2408    /// Records that gate: calc (calcRecord.c:120), calcout (:237), sCalcout
2409    /// (sCalcoutRecord.c:356), aCalcout (aCalcoutRecord.c:399), swait
2410    /// (swaitRecord.c:408 — which additionally raises READ_ALARM/INVALID on the
2411    /// failure) and sel (selRecord.c:114). sub/aSub gate the same outcome, but
2412    /// their body is the framework-dispatched subroutine, so they consume it
2413    /// through `RecordInstance::suppress_subroutine_run` instead.
2414    ///
2415    /// Default: ignore (records with no fetch gate). Same framework-set hook
2416    /// pattern as [`Record::set_resolved_input_links`].
2417    fn set_fetch_gate_failed(&mut self, _failed: bool) {}
2418
2419    /// Report this cycle's subroutine status — C `process`'s `status` variable
2420    /// for `sub`/`aSub`:
2421    ///
2422    /// ```c
2423    /// status = fetch_values(prec);
2424    /// if (!status) { status = do_sub(prec); prec->val = status; }
2425    /// ...
2426    /// if (!status)                      /* aSubRecord.c:232-239 */
2427    ///     for (i = 0; i < NUM_ARGS; i++)
2428    ///         dbPutLink(&(&prec->outa)[i], (&prec->ftva)[i], (&prec->vala)[i],
2429    ///             (&prec->neva)[i]);
2430    /// ```
2431    ///
2432    /// so `0` — and only `0` — means the input fetch succeeded AND `do_sub` ran
2433    /// and returned success. It is the gate on aSub's OUT-link pushes.
2434    ///
2435    /// Delivered by `RecordInstance::run_registered_subroutine`, the single
2436    /// owner of the `do_sub` call, on every one of its exit paths (the
2437    /// suppressed cycle, no bound routine, the routine's own return).
2438    ///
2439    /// Default: ignore (records with no subroutine).
2440    fn set_subroutine_status(&mut self, _status: i64) {}
2441
2442    /// Called before/after a field put for side-effect processing.
2443    fn special(&mut self, _field: &str, _after: bool) -> CaResult<()> {
2444        Ok(())
2445    }
2446
2447    /// The period of this record's monitor watchdog, or `None` when it has
2448    /// none — C `histogramRecord.c::wdogInit` (:126-152):
2449    ///
2450    /// ```c
2451    /// static void wdogInit(histogramRecord *prec) {
2452    ///     if (prec->sdel > 0) { ... callbackRequestDelayed(&pcallback->callback, prec->sdel); }
2453    /// }
2454    /// ```
2455    ///
2456    /// A watchdog is NOT a process cycle: it posts monitors for a record whose
2457    /// value is changing but whose deadband (histogram MDEL) is holding the
2458    /// posts back, so a slow accumulation still reaches a display. The
2459    /// framework re-reads this on every tick, so clearing SDEL stops the
2460    /// watchdog at the next fire without any separate cancel.
2461    ///
2462    /// histogram is the only base record with one. Default: no watchdog.
2463    fn watchdog_interval(&self) -> Option<std::time::Duration> {
2464        None
2465    }
2466
2467    /// One watchdog tick — C `histogramRecord.c::wdogCallback` (:102-124):
2468    ///
2469    /// ```c
2470    /// if (prec->mcnt > 0) {
2471    ///     dbScanLock(prec);
2472    ///     recGblGetTimeStamp(prec);
2473    ///     db_post_events(prec, &prec->val, DBE_VALUE | DBE_LOG);
2474    ///     prec->mcnt = 0;
2475    ///     dbScanUnlock(prec);
2476    /// }
2477    /// ```
2478    ///
2479    /// The record performs its own state change (histogram: zero MCNT) and
2480    /// returns the fields whose monitors the framework must post — the
2481    /// `db_post_events` half, which a record cannot do itself. An empty slice
2482    /// means "nothing changed since the last tick": no timestamp, no post. The
2483    /// framework holds the record lock across the call (C `dbScanLock`) and
2484    /// re-arms afterwards from [`Self::watchdog_interval`].
2485    ///
2486    /// Default: nothing to post.
2487    fn watchdog_fire(&mut self) -> &'static [&'static str] {
2488        &[]
2489    }
2490
2491    /// Whether this record type's support reads SIML through the `recGbl`
2492    /// simulation helpers (`recGblGetSimm`/`recGblInitSimm`, and therefore
2493    /// `recGblSaveSimm`/`recGblCheckSimm`) rather than a bare `dbGetLink`.
2494    ///
2495    /// ONE C fact, two consequences — which is why it is one predicate:
2496    ///
2497    /// - **The SCAN swap.** The helpers take `&prec->sscn` and `&prec->oldsimm`,
2498    ///   so only a record that declares those fields can call them, and only
2499    ///   such a record swaps SCAN with SSCN on a SIMM transition
2500    ///   (`recGblCheckSimm`, `recGbl.c:427-437`).
2501    /// - **The alarm on a failed SIML read.** `recGblGetSimm` reads SIML with
2502    ///   `dbTryGetLink` — which does NOT call `setLinkAlarm` — and then raises
2503    ///   the alarm itself, by writing `nsta` DIRECTLY:
2504    ///   `if (status && !pcommon->nsev) pcommon->nsta = LINK_ALARM;`
2505    ///   (`recGbl.c:454`). SEVR is left alone. A record reading SIML with a
2506    ///   plain `dbGetLink` (`busyRecord.c:399`, `swaitRecord.c:402`) instead
2507    ///   gets `setLinkAlarm` (`dbLink.c:319-323`) →
2508    ///   `recGblSetSevrMsg(LINK_ALARM, INVALID_ALARM)`, a full severity raise.
2509    ///
2510    /// The 21 base records that declare SSCN answer `true`; `busy` and `swait`
2511    /// carry SIMM/SIML/SIOL but neither SSCN nor OLDSIMM. Records with no
2512    /// simulation block answer `false` trivially.
2513    ///
2514    /// The default consults [`record_type_has_sscn`](crate::server::recgbl::simm::record_type_has_sscn),
2515    /// which is enumerated from the C dbd files, so no record has to restate it.
2516    fn uses_recgbl_simm_helpers(&self) -> bool {
2517        crate::server::recgbl::simm::record_type_has_sscn(self.record_type())
2518    }
2519
2520    /// The record's `readValue`/`writeValue` ABORTS when the SIML read fails —
2521    /// it returns before performing any I/O, so the cycle does no device write,
2522    /// no SIOL redirect, and raises no SIMM_ALARM.
2523    ///
2524    /// `busy` is the only record that does this (busyRecord.c:397-400):
2525    ///
2526    /// ```c
2527    /// status = dbGetLink(&prec->siml, DBR_USHORT, &prec->simm, 0, 0);
2528    /// if (status)
2529    ///     return(status);          /* <-- before write_busy AND before dbPutLink */
2530    /// ```
2531    ///
2532    /// The LINK_ALARM that `dbGetLink`'s `setLinkAlarm` already raised is the
2533    /// cycle's only simulation alarm.
2534    ///
2535    /// The other two families do NOT abort, for different reasons:
2536    ///
2537    /// - The 21 [`Self::uses_recgbl_simm_helpers`] records look like they do —
2538    ///   `readValue` has `status = recGblGetSimm(...); if (status) return status;`
2539    ///   (longinRecord.c:403-405) — but `recGblGetSimm` ends with an
2540    ///   unconditional `return 0` (recGbl.c:456), so that branch is DEAD and the
2541    ///   record always proceeds to its `switch (prec->simm)`.
2542    /// - `swait` reads SIML with a plain `dbGetLink` and simply never tests the
2543    ///   status (swaitRecord.c:402), so it proceeds too.
2544    ///
2545    /// Default: `false`.
2546    fn aborts_on_failed_siml_read(&self) -> bool {
2547        false
2548    }
2549
2550    /// The record joined (`true`) or left (`false`) the `SCAN="I/O Intr"` list.
2551    ///
2552    /// C parity: `dbScan.c::scanAdd` calls the record's device support
2553    /// `get_ioint_info(0, precord, &iopvt)` when SCAN becomes `I/O Intr`, and
2554    /// `scanDelete` calls `get_ioint_info(1, ...)` when it leaves. Device
2555    /// support that registers driver interrupt callbacks does so there —
2556    /// `asynRecord.c:582-597` registers/cancels its per-interface interrupt
2557    /// users in exactly those two calls, and clears its `gotValue` cell on the
2558    /// register.
2559    ///
2560    /// The port's device supports own their subscription through
2561    /// [`crate::server::device_support::DeviceSupport::io_intr_receiver`],
2562    /// which the framework asks for once at `iocInit`. This hook is the
2563    /// *runtime* half: a record whose own state decides what to subscribe to
2564    /// (asynRecord's PORT/IFACE/UI32MASK/REASON) must (re)register when the
2565    /// operator moves SCAN in or out of `I/O Intr` after `iocInit`.
2566    ///
2567    /// Called from the single owner of the SCAN transition
2568    /// (`RecordInstance::put_common_field*`, and the `scanAdd`-failure demotion
2569    /// in `ioc_app`) only when I/O Intr membership actually changes, so it is
2570    /// never invoked twice for the same state. Default: ignore.
2571    fn set_io_intr_scan(&mut self, _active: bool) {}
2572
2573    /// The link writes a C `special()` performs *itself*, inside `dbPut`.
2574    ///
2575    /// `special()` takes the record alone — it has no database handle — so a
2576    /// record whose C `special()` calls `dbPutLink` cannot make that write from
2577    /// `special()`. It queues the write here instead, and the put owner
2578    /// (`field_io`'s `dbPut` paths) drains the queue immediately after
2579    /// `special(field, true)` returns and executes the actions BEFORE the put's
2580    /// `pp(TRUE)` process cycle. That is C's order: `dbPutField` → `dbPut` →
2581    /// `dbPutSpecial(paddr, 1)` — which runs the `dbPutLink` to completion,
2582    /// target processing included — → `dbProcess`.
2583    ///
2584    /// The scaler is the case this exists for: `scalerRecord.c:623-624` puts
2585    /// `CNT` to `COUTP` inside `special()`, so a record wired to `.COUTP` is
2586    /// processed while the scaler is still IDLE, before the count is armed. The
2587    /// port deferred that write to the head of the CNT-triggered process cycle,
2588    /// where the target saw an already-COUNTING scaler.
2589    ///
2590    /// This is neither the record's "should the link fire" state nor part of the
2591    /// process cycle's action list: a `special()` put and a `process()` put to
2592    /// the same link (scaler `COUTP` again, `:463`) are independent writes.
2593    ///
2594    /// The drain is unconditional — it runs even when `special()` returned an
2595    /// error — so a queued action can never survive the put that queued it and
2596    /// fire against a later, unrelated put.
2597    ///
2598    /// Default: none (a `special()` that writes no link).
2599    fn take_special_actions(&mut self) -> Vec<ProcessAction> {
2600        Vec::new()
2601    }
2602
2603    /// Other fields whose monitors must be posted because a put to
2604    /// `put_field` changed them as a side effect, without driving a full
2605    /// process cycle.
2606    ///
2607    /// Mirrors the explicit `db_post_events` calls a C `special()` makes:
2608    /// e.g. `compressRecord.c::reset` (invoked on a `SPC_RESET` write to
2609    /// `RES`) posts `NUSE` and `VAL` even though `RES` is not `pp(TRUE)`
2610    /// and so does not process. The framework posts a `VALUE|LOG` monitor
2611    /// for each returned field after the put. Default: none.
2612    fn monitor_side_effect_fields(&self, _put_field: &str) -> &'static [&'static str] {
2613        &[]
2614    }
2615
2616    /// True iff the C `special()` for a put to `put_field` runs the record's
2617    /// own `monitor()` — whose first act is `recGblResetAlarms(prec)`, which
2618    /// commits `nsta`/`nsev` into `stat`/`sevr` and clears the born-UDF alarm.
2619    ///
2620    /// `compress` is the case this exists for: `compressRecord.c::special`
2621    /// (:385-388) calls `reset(prec); monitor(prec);` on any SPC_RESET write
2622    /// (RES/ALG/PBUF/BALG/N), and `compressRecord.c::monitor` (:103) opens with
2623    /// `recGblResetAlarms`. A born-UDF compress record therefore transitions to
2624    /// NO_ALARM the moment one of those fields is put, with no process cycle.
2625    ///
2626    /// The framework already posts the `db_post_events` half of that `monitor()`
2627    /// through [`Record::monitor_side_effect_fields`]; this hook is the
2628    /// `recGblResetAlarms` half. When it returns true, the put owner
2629    /// (`field_io`'s `dbPut` paths) runs `rec_gbl_reset_alarms` and posts the
2630    /// resulting STAT/SEVR/AMSG/ACKS transition. Default: false (a `special()`
2631    /// that does not run the record's `monitor()`).
2632    fn special_commits_alarms(&self, _put_field: &str) -> bool {
2633        false
2634    }
2635
2636    /// True iff the C `special()` for a put to `put_field` runs code that
2637    /// writes `stat`/`sevr` DIRECTLY (not `nsta`/`nsev`) with NO `monitor()` /
2638    /// `recGblResetAlarms` after it — so the write STICKS and a later `caget`
2639    /// observes it, unlike the process path where `recGblResetAlarms` erases it.
2640    ///
2641    /// `histogram` is the case this exists for: a `.SGNL` caput is C's SPC_MOD
2642    /// `special()` → `add_count`, which writes `prec->stat = SOFT_ALARM` on
2643    /// inverted limits (histogramRecord.c:329-334) and returns with no monitor.
2644    /// When true, the put owner (`field_io`) runs
2645    /// [`Record::check_alarms`] after the store — the same direct write the
2646    /// process path makes, but here it persists because no process cycle
2647    /// follows. Default: false (no direct special-path alarm write).
2648    fn special_checks_alarms(&self, _put_field: &str) -> bool {
2649        false
2650    }
2651
2652    /// Downcast to concrete type for device support init injection.
2653    /// Override in record types that need device support to inject state (e.g., MotorRecord).
2654    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
2655        None
2656    }
2657
2658    /// Whether processing this record should clear UDF.
2659    /// Override to return false for record types that don't produce a valid value every cycle.
2660    fn clears_udf(&self) -> bool {
2661        true
2662    }
2663
2664    /// Whether this record's C `process()` clears UDF regardless of the read's
2665    /// status — i.e. even when `readValue` failed.
2666    ///
2667    /// Most records gate the clear on the read: `if (status == 0) prec->udf =
2668    /// FALSE;` inside `readValue`'s SIOL branch (`longinRecord.c:418`), so a
2669    /// failed simulation read leaves the record undefined. The array records do
2670    /// NOT: their `process()` clears UDF itself, unconditionally, on the line
2671    /// after `readValue` returns — `prec->pact = TRUE; prec->udf = FALSE;`
2672    /// (`waveformRecord.c:143-144`, `aaiRecord.c:173-174`) and
2673    /// `if (!pact) { prec->udf = FALSE; ... }` (`aaoRecord.c:164-165`) — whatever
2674    /// status came back, including the `-1` of an illegal SIMM. (waveform's
2675    /// readValue also clears UDF on a good SIOL read, `:353`, but the
2676    /// process-level clear runs after it and dominates.)
2677    ///
2678    /// Consulted by the simulation tail, which otherwise gates the clear on the
2679    /// SIOL fetch status. Default `false` — the status-gated majority.
2680    fn clears_udf_unconditionally(&self) -> bool {
2681        false
2682    }
2683
2684    /// Whether this record type's `.dbd` declares an `INP` field at all.
2685    ///
2686    /// The port keeps INP on `CommonFields` for every record, which is right for
2687    /// the input records (`aiRecord.dbd.pod` … all declare it) but wrong for the
2688    /// ones whose C `.dbd` has no INP: C's dbd is the gate there, and
2689    /// `field(INP,...)` on such a record is a load error ("field not found"),
2690    /// leaving the record inert.
2691    ///
2692    /// `histogram` is the case this exists for: `histogramRecord.dbd.pod`
2693    /// declares NO INP — its DBF_INLINK is `SVL` (:212), read into SGNL by
2694    /// `devHistogramSoft.c` — so a histogram driven from INP is a database that
2695    /// no C IOC can load. Default `true`.
2696    ///
2697    /// This is the whole namespace gate, not just the loader's: in C the dbd
2698    /// also decides which `.FIELD` channels exist, so a histogram's INP is not
2699    /// resolvable at all (`dbgf HI.INP` → `PV 'HI.INP' not found`). Both
2700    /// [`RecordInstance::get_common_field`] and
2701    /// [`RecordInstance::put_common_field`] consult this, so the field cannot
2702    /// be readable on one route while refused on the other.
2703    ///
2704    /// [`RecordInstance::get_common_field`]: crate::server::record::RecordInstance::get_common_field
2705    /// [`RecordInstance::put_common_field`]: crate::server::record::RecordInstance::put_common_field
2706    fn declares_inp_link(&self) -> bool {
2707        true
2708    }
2709
2710    /// Process-time INP read when the INP link is CONSTANT (or unset) — the
2711    /// per-record exception to the load-once rule.
2712    ///
2713    /// Nearly every soft input device support skips a constant INP at process:
2714    /// `devWfSoft.c::read_wf` and `devAaiSoft.c::read_aai` open with
2715    /// `if (dbLinkIsConstant(pinp)) return 0;`, and the scalar ones call
2716    /// `dbGetLink`, whose `dbConstGetValue` (`dbConstLink.c:219-225`) writes
2717    /// nothing. The constant reaches such a record ONCE, at init, through
2718    /// `recGblInitConstantLink` / `dbLoadLinkArray`
2719    /// (`PvDatabase::rec_gbl_init_constant_inp`), so a client's caput to VAL is
2720    /// never clobbered by a re-delivered constant.
2721    ///
2722    /// `devSASoft.c::read_sa` (92-123) is the documented exception: it re-runs
2723    /// `dbLoadLinkArray` on a constant INP EVERY process, and on an EMPTY INP
2724    /// (`S_db_badField`) it sets `nRequest = prec->nord` and still subsets — so
2725    /// the record re-slices the client-written VAL by INDX each cycle. C draws
2726    /// that line at the device-support layer (`devSASoft` vs `devAaiSoft`), and
2727    /// so does this hook.
2728    ///
2729    /// Called by the framework on a soft-DTYP cycle whose INP is constant, with
2730    /// `value = Some(constant)` for a non-empty constant and `None` for an
2731    /// empty/unset INP. Returns whether the record consumed the input stage.
2732    /// Default `false` — the load-once rule.
2733    fn read_constant_inp(&mut self, _value: Option<EpicsValue>) -> bool {
2734        false
2735    }
2736
2737    /// Whether this record type raises UDF_ALARM at all.
2738    ///
2739    /// C has no framework-level UDF alarm: every record that reports one does
2740    /// it itself, with the guard at the top of its own `checkAlarms` —
2741    /// `if (prec->udf) { recGblSetSevr(prec, UDF_ALARM, prec->udfs); return; }`
2742    /// (`aiRecord.c:319-323`, `calcRecord.c:300-304`, …). A record whose
2743    /// support has no such guard NEVER reports UDF_ALARM, no matter what its
2744    /// `UDF` field says — `swaitRecord.c` is the case in point: its two only
2745    /// `udf` statements are `udf = FALSE` (`:411`, `:419`); it has no
2746    /// `checkAlarms` and never names UDF_ALARM.
2747    ///
2748    /// The framework raises UDF_ALARM centrally (`rec_gbl_check_udf`), so this
2749    /// hook is where a record type says C does not. Default `true` — the base
2750    /// analog/binary/string records all carry the guard.
2751    fn raises_udf_alarm(&self) -> bool {
2752        true
2753    }
2754
2755    /// Whether this record's C `checkAlarms` tests the UDF byte with
2756    /// `if (prec->udf == TRUE)` (EXACT-ONE) rather than `if (prec->udf)`
2757    /// (truthy). See [`crate::server::recgbl::udf_alarm_active`]: exact-one
2758    /// records (`boRecord.c:371`, `stringoutRecord.c:146`, `biRecord.c:225`,
2759    /// `busyRecord.c:337`) do NOT raise UDF_ALARM for a `udf` byte that is
2760    /// neither 0 nor 1.
2761    ///
2762    /// This only changes behavior for a record whose `udf` byte can actually
2763    /// hold such a value at `checkAlarms` time — one that does NOT re-derive
2764    /// `udf` every cycle ([`Record::clears_udf`] `== false`), reached via a
2765    /// direct `caput .UDF 255` (or `-1`, stored as `255` in the `DBF_UCHAR`
2766    /// field). For the re-deriving records (`clears_udf() == true`, e.g.
2767    /// `bi`/`busy`/the calc family) the byte is always 0/1 here, so exact-one
2768    /// and truthy agree and the flag is left at its default. Default `false`.
2769    fn udf_alarm_on_exact_one(&self) -> bool {
2770        false
2771    }
2772
2773    /// The alarm message C attaches when raising `UDF_ALARM`.
2774    ///
2775    /// Almost every base record raises UDF with plain
2776    /// `recGblSetSevr(prec, UDF_ALARM, prec->udfs)` — and `recGblSetSevr`
2777    /// forwards a NULL message to `recGblSetSevrMsg`, which sets
2778    /// `namsg[0] = '\0'` (`recGbl.c:249-251,258-261`). So the C amsg for a
2779    /// UDF record is EMPTY, and pvxs then serves the `"UDF"` condition
2780    /// string for `alarm.message` (`iocsource.cpp:230-236`). Default `""`
2781    /// models exactly that.
2782    ///
2783    /// The sole exception in base is `mbboDirectRecord.c:191`, which raises
2784    /// `recGblSetSevrMsg(prec, UDF_ALARM, prec->udfs, "UDFS")` — a bespoke
2785    /// literal. That record overrides this to `"UDFS"`.
2786    fn udf_alarm_message(&self) -> &str {
2787        ""
2788    }
2789
2790    /// Whether the record's current `VAL` is undefined (UDF must
2791    /// stay set).
2792    ///
2793    /// C parity: `aiRecord.c:285` / `calcRecord.c::checkAlarms` /
2794    /// `int64inRecord.c:144` clear `UDF` **only** when the computed /
2795    /// read value is valid — `if (status == 0)` and, for floating
2796    /// records, only when `VAL` is not NaN. The framework owns
2797    /// `common.udf`; it calls `clears_udf()` to decide whether this
2798    /// record type clears UDF at all, then this method to decide
2799    /// whether the *value produced this cycle* is actually defined.
2800    ///
2801    /// Default: a floating `VAL` that is NaN (e.g. a calc
2802    /// divide-by-zero, or a soft input whose link read failed and
2803    /// left VAL un-updated) is undefined; everything else is defined.
2804    /// A record whose `val()` yields `None` (no primary value) is
2805    /// also treated as undefined.
2806    fn value_is_undefined(&self) -> bool {
2807        match self.val() {
2808            Some(EpicsValue::Double(v)) => v.is_nan(),
2809            Some(EpicsValue::Float(v)) => v.is_nan(),
2810            Some(_) => false,
2811            None => true,
2812        }
2813    }
2814
2815    /// Per-record alarm hook — evaluate record-type-specific alarms
2816    /// (STATE / COS / analog limit / SOFT) and accumulate them into
2817    /// `nsta`/`nsev` via `recGblSetSevr`.
2818    ///
2819    /// The framework centralises the generic alarm machinery (UDF
2820    /// check, `recGblResetAlarms` transfer, MS/MSI/MSS link-alarm
2821    /// inheritance). The record-type-specific severity logic that C
2822    /// puts in each record's `checkAlarms()` belongs here so a record
2823    /// can raise its own alarms without the framework hardcoding a
2824    /// per-type `match` on `record_type()`.
2825    ///
2826    /// `common` is the record's [`crate::server::record::CommonFields`]; implementations
2827    /// raise alarms with [`crate::server::recgbl::rec_gbl_set_sevr`]
2828    /// / [`crate::server::recgbl::rec_gbl_set_sevr_msg`].
2829    ///
2830    /// Default: no-op — records that have not yet migrated their
2831    /// `checkAlarms` logic here are still covered by the framework's
2832    /// legacy centralised `evaluate_alarms` match.
2833    fn check_alarms(&mut self, _common: &mut crate::server::record::CommonFields) {}
2834
2835    /// Return multi-input link field pairs: (link_field, value_field).
2836    /// Override in calc, calcout, sel, sub to return INPA..INPL → A..L mappings.
2837    fn multi_input_links(&self) -> &[(&'static str, &'static str)] {
2838        &[]
2839    }
2840
2841    /// The `(link_field, value_field)` pairs whose CONSTANT value this record's
2842    /// C `special()` RE-SEEDS on a runtime put to the link field —
2843    /// `recGblInitConstantLink(plink, DBF_DOUBLE, pvalue)` +
2844    /// `db_post_events(prec, pvalue, DBE_VALUE)` + `INAV = CON`
2845    /// (`calcoutRecord.c:367-378`, `sCalcoutRecord.c:512-517`,
2846    /// `aCalcoutRecord.c:534-540`).
2847    ///
2848    /// Without it a constant link is load-once dead state: `caput CO.INPB 7`
2849    /// stores the link text, the link layer then delivers nothing at process
2850    /// time (a constant link is not read), and `B` keeps its `.db`-load value
2851    /// forever.
2852    ///
2853    /// Declaring the pair is all a record does — the put path
2854    /// (`database::field_io::special_after_put`, the one `special(field, true)`
2855    /// owner) runs the load through
2856    /// [`crate::server::record::rec_gbl_init_constant_link`], the same owner the
2857    /// init seed uses, and posts the value field. A record cannot declare the
2858    /// pair and forget to implement the re-seed.
2859    ///
2860    /// Default: EMPTY — and that is the correct answer for every record whose C
2861    /// `special()` does NOT re-seed. `recGblInitConstantLink` appears inside a
2862    /// `special()` body in exactly FOUR record types across base and synApps
2863    /// calc — calcout, sCalcout, aCalcout, transform. Everywhere else (calc,
2864    /// sub, sel, aSub, seq, fanout, dfanout, swait, sseq, ao/bo/longout/…) it is
2865    /// called only from `init_record`, so those records seed once and never
2866    /// again, and they inherit this empty default.
2867    ///
2868    /// The overriding records list only the inputs C actually re-seeds:
2869    /// sCalcout/aCalcout guard with `fieldIndex <= INPL` (their string/array
2870    /// inputs are init-load only) and transform with
2871    /// `fieldIndex < transformRecordOUTA` (its OUT half is not an input).
2872    fn special_reseed_input_links(&self) -> &[(&'static str, &'static str)] {
2873        &[]
2874    }
2875
2876    /// The event mask of the `db_post_events` call in that record's `special()`
2877    /// re-seed arm — the C call sites do not agree:
2878    ///
2879    ///  * calcout (`calcoutRecord.c:376`), sCalcout (`:516`), aCalcout (`:537`)
2880    ///    post a literal `DBE_VALUE`.
2881    ///  * transform (`transformRecord.c:718`) posts `DBE_VALUE | DBE_LOG`.
2882    ///
2883    /// Default: `DBE_VALUE`, the majority shape. Only consulted for the fields
2884    /// named by [`Self::special_reseed_input_links`].
2885    fn special_reseed_post_mask(&self) -> crate::server::recgbl::EventMask {
2886        crate::server::recgbl::EventMask::VALUE
2887    }
2888
2889    /// Every CONSTANT input link this record seeds ONCE, at `init_record` —
2890    /// the record's own `recGblInitConstantLink` / `dbLoadLinkArray` table,
2891    /// transcribed from its C.
2892    ///
2893    /// This is the OTHER half of the one rule the link layer enforces: a
2894    /// constant link delivers NOTHING at process time (`dbConstGetValue`,
2895    /// `dbConstLink.c:219-225`), so the ONLY way a `field(INPA,"5")` ever
2896    /// reaches `A` is this table, applied by the single init-seed owner
2897    /// `crate::server::database::PvDatabase::rec_gbl_init_constant_links`.
2898    /// A record that fetches an input link but declares no seed for it (swait,
2899    /// whose C uses `recDynLink` and seeds nothing) simply never sees the
2900    /// constant — which is what its C does.
2901    ///
2902    /// Default: none.
2903    fn constant_init_links(&self) -> Vec<ConstantInitLink> {
2904        Vec::new()
2905    }
2906
2907    /// The link field this record loads into its long-string VAL at init through
2908    /// C's `dbLoadLinkLS` — `"DOL"` for `lso` (`lsoRecord.c:82`), `"INP"` for
2909    /// `lsi` (its soft device support, `devLsiSoft.c:24`). `loadLS` is a lset
2910    /// entry of its own, so this is a SEPARATE table from
2911    /// [`Self::constant_init_links`], not a variant of it; the same init-seed
2912    /// owner runs both.
2913    ///
2914    /// Default: none — a record with no long-string VAL has no `loadLS` seed.
2915    fn constant_ls_link(&self) -> Option<&'static str> {
2916        None
2917    }
2918
2919    /// Apply the [`Self::constant_ls_link`] load, and return the resulting LEN.
2920    /// The record clamps the text at its own `SIZV` and runs C's init tail
2921    /// (`if (prec->len) { strcpy(prec->oval, prec->val); prec->olen = prec->len; }`,
2922    /// lsoRecord.c:92-95 / lsiRecord.c:85-88); the owner turns a non-zero LEN
2923    /// into `udf = FALSE`.
2924    fn apply_ls_load(&mut self, _load: crate::server::record::LsLoad) -> u32 {
2925        0
2926    }
2927
2928    /// Whether this record's CONSTANT input links deliver their value on
2929    /// EVERY process cycle instead of only at init.
2930    ///
2931    /// `false` for every record that fetches with a plain `dbGetLink`. `printf`
2932    /// is the one exception in the whole database: its `GET_PRINT` macro
2933    /// (`printfRecord.c:49-52`) tests `dbLinkIsConstant` and re-runs
2934    /// `recGblInitConstantLink` on every `doPrintf`, so a constant INP0..9
2935    /// really is re-read each cycle.
2936    fn constant_inputs_deliver_at_process(&self) -> bool {
2937        false
2938    }
2939
2940    /// The subset of [`Self::multi_input_links`] the framework should
2941    /// actually fetch this cycle, given an optional externally-resolved
2942    /// selector index (sel's NVL→SELN value, or `None` when no NVL link
2943    /// drove it). Default `None` = fetch every input link.
2944    ///
2945    /// C `selRecord.c::fetch_values` (lines 421-431) fetches ONLY `INP[SELN]`
2946    /// in `Specified` mode and all inputs otherwise; sel returns
2947    /// `Some(vec![INP[SELN]])` so the non-selected inputs are never read and
2948    /// raise no monitors or link-alarm SEVR.
2949    fn select_input_links(
2950        &self,
2951        _selector: Option<u16>,
2952    ) -> Option<Vec<(&'static str, &'static str)>> {
2953        None
2954    }
2955
2956    /// A `SIMM != NO` cycle substitutes only this record's INPUT STAGE — the
2957    /// rest of its `process()` still runs.
2958    ///
2959    /// C's SIML/SIMM/SIOL group has three shapes, and this hook names the third:
2960    ///
2961    /// * `readValue` (ai, bi, longin, …): the simulated read replaces the device
2962    ///   read, which is the whole of the record's input; the framework performs
2963    ///   the SIOL read and completes the cycle itself.
2964    /// * `writeValue` (ao, bo, …): the simulated write replaces the device write
2965    ///   at the END of the body, so the body runs and only the output is
2966    ///   redirected to SIOL.
2967    /// * swait (`swaitRecord.c:401-421`): the simulated read replaces
2968    ///   `fetch_values()` **and** `calcPerform()` and nothing else — VAL comes
2969    ///   from SIOL through SVAL, and the OOPT switch, `execOutput`, the monitors
2970    ///   and the forward link all still run from the record's own `process()`.
2971    ///
2972    /// A record that returns `true` gets, on a simulated cycle: SIMM resolved
2973    /// from SIML, SIOL read into SVAL, `VAL = SVAL` and `UDF = FALSE` when that
2974    /// read succeeded (C `:417-420` — a failed read changes neither), SIMM_ALARM
2975    /// raised at SIMS *before* the body so it maximizes against whatever the
2976    /// body raises (C `:421`), no input-link fetch, and
2977    /// [`Self::set_simulation_active`] pushed before `process()`.
2978    fn simulation_substitutes_input_stage(&self) -> bool {
2979        false
2980    }
2981
2982    /// Land the scalar a simulated cycle read from SIOL (through SVAL, where
2983    /// the record has one) — C `readValue`'s assignment plus whatever the
2984    /// record's `process()` body then does with it, under the same
2985    /// `status == 0` gate the framework applies before calling this.
2986    ///
2987    /// The base records assign the value straight to VAL — `longinRecord.c:417`
2988    /// `prec->val = prec->sval;` — which is the default (`set_val`).
2989    ///
2990    /// `histogram` does NOT: `histogramRecord.c:385-386` lands it in SGNL
2991    /// (`prec->sgnl = prec->sval;`), and `process()` (`:218-219`,
2992    /// `if (status == 0) add_count(prec);`) bins that signal into the VAL
2993    /// bin-count array. Its VAL is the array, so a `set_val` of the scalar
2994    /// no-ops and the simulated record is frozen. It overrides.
2995    fn land_simulated_value(&mut self, value: EpicsValue) -> CaResult<()> {
2996        self.set_val(value)
2997    }
2998
2999    /// Whether the record's C `switch (prec->simm)` carries a `default:` arm
3000    /// that REFUSES a SIMM value outside its own menu:
3001    ///
3002    /// ```c
3003    /// default:
3004    ///     recGblSetSevr(prec, SOFT_ALARM, INVALID_ALARM);
3005    ///     status = -1;
3006    /// ```
3007    ///
3008    /// Every record in the framework has it — all 21 base records
3009    /// (`longinRecord.c:436-438` and its twins; `aaiRecord.c:381-384` writes the
3010    /// same arm as an `else if (prec->simm != menuYesNoNO)`) and `busy`
3011    /// (`busyRecord.c:409-413`, the `else` of its YES test).
3012    ///
3013    /// `swait` is the sole exception: `swaitRecord.c:407-421` is a plain
3014    /// `if (pwait->simm == menuYesNoNO) { … } else { /* SIMULATION MODE */ … }`,
3015    /// so every non-NO value — legal or not — simulates. It overrides to
3016    /// `false`.
3017    ///
3018    /// Consumed by [`resolve_sim_mode`](crate::server::recgbl::simm::resolve_sim_mode),
3019    /// the single owner of the SIMM dispatch.
3020    fn rejects_illegal_sim_mode(&self) -> bool {
3021        true
3022    }
3023
3024    /// This cycle's simulation state, pushed by the framework before
3025    /// `process()` — the twin of [`Self::set_fetch_gate_failed`], and only for a
3026    /// record that declares [`Self::simulation_substitutes_input_stage`].
3027    ///
3028    /// It is pushed on EVERY cycle of such a record (`false` included), so the
3029    /// flag cannot survive the cycle it belongs to. The record uses it to skip
3030    /// exactly what C's simulation branch skips — for swait, `fetch_values()`
3031    /// (through [`Self::select_input_links`]) and `calcPerform()`.
3032    fn set_simulation_active(&mut self, _active: bool) {}
3033
3034    /// How C's `fetch_values()` for this record type reacts to a link read
3035    /// that fails. Drives the framework's [`Self::multi_input_links`] fetch
3036    /// loop; see [`InputFetchPolicy`]. Default: [`InputFetchPolicy::ReadAll`].
3037    ///
3038    /// It governs the [`Self::multi_input_links`] loop ONLY.
3039    /// [`Self::string_input_links`] is C's *second*, separately-gated fetch
3040    /// loop and never participates in this policy.
3041    fn input_fetch_policy(&self) -> InputFetchPolicy {
3042        InputFetchPolicy::ReadAll
3043    }
3044
3045    /// String-valued input links: `(link_field, value_field)` pairs read as
3046    /// DBR_STRING, C `sCalcoutRecord.c::fetch_values` (890-941) — the SECOND
3047    /// loop of that function, over `INAA`..`INLL` → `AA`..`LL`:
3048    ///
3049    /// ```c
3050    /// for (i=0, plink=&pcalc->inaa, psvalue=pcalc->strs; i<STRING_MAX_FIELDS; ...) {
3051    ///     ...
3052    ///     if (((field_type==DBR_CHAR) || (field_type==DBR_UCHAR)) && nelm>1) {
3053    ///         status = dbGetLink(plink, field_type, tmpstr, 0, &nelm);
3054    ///         epicsStrSnPrintEscaped(*psvalue, STRING_SIZE-1, tmpstr, strlen(tmpstr));
3055    ///     } else {
3056    ///         status = dbGetLink(plink, DBR_STRING, *psvalue, 0, 0);
3057    ///     }
3058    ///     if (!RTN_SUCCESS(status))
3059    ///         epicsSnprintf(*psvalue, STRING_SIZE-1, "%s:fetch(%s) failed", pcalc->name, sFldnames[i]);
3060    /// }
3061    /// return(0);
3062    /// ```
3063    ///
3064    /// Three properties this loop does NOT share with [`Self::multi_input_links`],
3065    /// which is why it is a separate list rather than more entries in that one:
3066    ///
3067    /// 1. **Ungated.** It ends in `return(0)` — a failing string link never
3068    ///    makes `fetch_values` non-zero, so it cannot suppress the record body.
3069    ///    The record's single [`Self::input_fetch_policy`] describes the numeric
3070    ///    loop (`AbortOnFirstFailure` for scalcout) and cannot also describe this
3071    ///    one.
3072    /// 2. **A failed read still writes the field** — with the diagnostic text
3073    ///    `"<record>:fetch(<FIELD>) failed"`, not with the previous value.
3074    /// 3. **A `DBF_CHAR`/`DBF_UCHAR` array source is read as text**, C-escaped
3075    ///    (`epicsStrSnPrintEscaped`), which is how a >40-char string reaches a
3076    ///    string calc; every other source type converts as DBR_STRING.
3077    ///
3078    /// The value is delivered through [`Self::put_field_internal`], so the
3079    /// target field's declared `DbFieldType` performs the final coercion.
3080    fn string_input_links(&self) -> &'static [(&'static str, &'static str)] {
3081        &[]
3082    }
3083
3084    /// Input links this record reads at OUTPUT time instead of during the
3085    /// input-fetch phase: `(link_name_field, value_field)` pairs. The framework
3086    /// reads each configured link immediately before the OUT write, and ONLY on
3087    /// a cycle where the output actually fires ([`Self::should_output`] and no
3088    /// IVOA veto), then writes the value into `value_field` via
3089    /// [`Self::put_field`]; a failed read leaves the field alone.
3090    ///
3091    /// C `swaitRecord.c::execOutput` (763-772) does exactly this for `DOL`:
3092    ///
3093    /// ```c
3094    /// if (pwait->dopt) {                    /* DOPT = "Use DOL" */
3095    ///     if (!pwait->dolv) {               /* DOL PV connected */
3096    ///         oldDold = pwait->dold;
3097    ///         recDynLinkGet(&pcbst->caLinkStruct[DOL_INDEX], &(pwait->dold), ...);
3098    ///         if (pwait->dold != oldDold)
3099    ///             db_post_events(pwait, &pcbst->pwait->dold, DBE_VALUE);
3100    ///     }
3101    ///     outValue = pwait->dold;
3102    /// }
3103    /// ```
3104    ///
3105    /// The timing is the point: the value written out is the one the link holds
3106    /// at output time (ODLY delay-end included), and a cycle whose output does
3107    /// not fire never refreshes — or posts — the field. Fetching such a link in
3108    /// the normal input phase would do both. Default: none.
3109    fn output_time_input_links(&self) -> &'static [(&'static str, &'static str)] {
3110        &[]
3111    }
3112
3113    /// The value the framework writes to the OUT link. The single owner of
3114    /// "what goes out", shared by the soft-OUT write, the async-completion
3115    /// write and the simulated SIOL redirect.
3116    ///
3117    /// The default is the C staging convention: the record computed the output
3118    /// into `OVAL` during `process()` (`calcout`/`ao`/`bo`/...), falling back to
3119    /// `VAL` for records that have no `OVAL`. Override when the record's C
3120    /// composes the output value at *output* time rather than staging it — e.g.
3121    /// swait, whose `execOutput` (`swaitRecord.c:763-772`) picks between `VAL`
3122    /// and the just-fetched `DOLD` and whose `OVAL` field is C's "Old Value"
3123    /// (the previous VAL, used only by the OOPT test), not an output stage.
3124    fn output_link_value(&self) -> Option<EpicsValue> {
3125        self.get_field("OVAL").or_else(|| self.val())
3126    }
3127
3128    /// Return multi-output link field pairs: (link_field, value_field).
3129    /// Override in transform to return OUTA..OUTP → A..P mappings.
3130    fn multi_output_links(&self) -> &[(&'static str, &'static str)] {
3131        &[]
3132    }
3133
3134    /// The record's C soft device support write-buffer switch for a
3135    /// multi-output pair: given the pair's staged value (the value field
3136    /// named by [`Self::multi_output_links`]) and the RESOLVED TARGET
3137    /// metadata, return the buffer C would actually put.
3138    ///
3139    /// C's soft device supports do not blindly write one field: they read
3140    /// the target's DBF type and element count and pick a buffer from them —
3141    /// `devaCalcoutSoft.c::write_acalcout` (75-87) picks
3142    /// `nelm == 1 ? &scalar : array`, `devsCalcoutSoft.c::write_scalcout`
3143    /// (66-144) routes a string-class target to the computed string, a
3144    /// `CHAR`/`UCHAR` array to the string's bytes, and everything else to
3145    /// the numeric. The framework resolves the target
3146    /// ([`PvDatabase::resolve_out_target`](crate::server::database::PvDatabase))
3147    /// and hands it here; the record reproduces its device support's switch.
3148    ///
3149    /// Default: write the staged value unchanged (no device-support switch).
3150    fn multi_output_buffer(
3151        &self,
3152        link_field: &str,
3153        staged: EpicsValue,
3154        target: &OutTarget,
3155    ) -> EpicsValue {
3156        let _ = (link_field, target);
3157        staged
3158    }
3159
3160    /// The buffer to put on an OUT link the record drives itself, chosen from
3161    /// the RESOLVED TARGET — the no-staged-value sibling of
3162    /// [`Self::multi_output_buffer`].
3163    ///
3164    /// C `sseqRecord.c::processCallback` (706-793) is the case: the value a
3165    /// step forwards is not one field but a *switch on the destination*
3166    /// (`dbGetLinkDBFtype(&lnk)` / `dbGetNelements(&lnk)`), taken at fire
3167    /// time — the string view `s` for a string-class target, the double view
3168    /// `dov` for a numeric one, `s`'s bytes for a `CHAR`/`UCHAR` array, and
3169    /// **no put at all** for a target whose type does not resolve (C's
3170    /// `default: break`). `None` is that no-put: the caller issues no write.
3171    ///
3172    /// The record calls this ITSELF, on the target
3173    /// [`ProcessAction::ResolveOutTarget`] handed it before `process()` — one
3174    /// decision, made before anything is issued, because the same switch
3175    /// decides more than the buffer (sseq: whether a `WAITn` put-callback goes
3176    /// out, and hence whether `WTGn` is raised). See
3177    /// [`Self::set_resolved_out_target`].
3178    ///
3179    /// Default: `None`. Only a record that resolves its own OUT target reaches
3180    /// this, and it must override.
3181    fn typed_output_buffer(&self, link_field: &str, target: &OutTarget) -> Option<EpicsValue> {
3182        let _ = (link_field, target);
3183        None
3184    }
3185
3186    /// Receive the RESOLVED target of an OUT link the record asked to have
3187    /// resolved before this cycle's `process()`
3188    /// ([`ProcessAction::ResolveOutTarget`]).
3189    ///
3190    /// C resolves an OUT link's DBF class OUTSIDE the put — `checkLinks` caches
3191    /// it in the record (`sseqRecord.c:202-250`) — so `processCallback` can make
3192    /// ONE decision from it: which view goes on the wire, AND whether a
3193    /// put-callback is issued (hence whether `waiting` is raised). Its
3194    /// `default:` arm (`:790`) does neither. A record that learns the class only
3195    /// from inside the framework's put path cannot keep those two halves
3196    /// together: it has to raise `waiting` first and find out afterwards that no
3197    /// put was made. This hook is that cached class — the record decides, then
3198    /// acts.
3199    ///
3200    /// Default: no-op. Only a record that emits the action reaches this.
3201    fn set_resolved_out_target(&mut self, link_field: &str, target: OutTarget) {
3202        let _ = (link_field, target);
3203    }
3204
3205    /// The `dbrType` this record's [`ProcessAction::ReadDbLink`] on
3206    /// `link_field` asks the SOURCE for — the READ twin of
3207    /// [`Self::typed_output_buffer`], and C's `dbGetLink` second argument.
3208    ///
3209    /// `source` is the far end of the input link as C's
3210    /// `dbGetLinkDBFtype`/`dbGetNelements` report it (the same lset accessors
3211    /// the OUT side uses — sseq asks them of `dol` at `sseqRecord.c:641` and of
3212    /// `lnk` at `:709`), resolved by the framework
3213    /// ([`PvDatabase::resolve_out_target`](crate::server::database::PvDatabase)).
3214    ///
3215    /// `None` means C's `default: break` — **no read at all**, the arm a source
3216    /// class the record's switch does not name falls to (an unresolvable /
3217    /// constant / disconnected source, and sseq's un-cased `DBF_INT64`). The
3218    /// link is left untouched and no LINK alarm is raised, exactly as C's
3219    /// skipped `dbGetLink` call leaves `status` alone.
3220    ///
3221    /// Default: [`LinkReadAs::Native`] — the source's native value, coerced at
3222    /// the target field's own put boundary.
3223    fn input_link_read_as(&self, link_field: &str, source: &OutTarget) -> Option<LinkReadAs> {
3224        let _ = (link_field, source);
3225        Some(LinkReadAs::Native)
3226    }
3227
3228    /// Return the name of the output event (`OEVT`) to post this cycle, or
3229    /// `None`. The event-subsystem twin of the OUT write: a downstream
3230    /// `SCAN="Event"` / `EVNT="<name>"` record is woken each time the record
3231    /// drives output. Mirrors C `calcout`/`sCalcout`/`aCalcout` `execOutput`,
3232    /// which calls `postEvent(epvt)` / `post_event(oevt)` immediately after
3233    /// `writeValue` in every OUT-driving branch.
3234    ///
3235    /// The override MUST fold in the record's own output-fire decision
3236    /// (`should_output()` for `calcout`; the cached OOPT/calc-fail/ODLY
3237    /// decision for `sCalcout`/`aCalcout`) and return `None` when output did
3238    /// not fire or when `OEVT` is unset. The framework adds the only gate the
3239    /// record cannot see — the IVOA `Don't_drive` veto on an INVALID cycle —
3240    /// so the post fires on exactly the cycles the OUT write does. Numeric
3241    /// `OEVT` (DBF_USHORT) stringifies to match the `EVNT` ingest; a string
3242    /// `OEVT` (DBF_STRING) is the event name verbatim.
3243    fn output_event(&self) -> Option<String> {
3244        None
3245    }
3246
3247    /// Internal field write that bypasses read-only checks.
3248    /// Used by the framework to write values from ReadDbLink actions
3249    /// into fields that are normally read-only (e.g., epid.CVAL).
3250    /// Default implementation delegates to put_field().
3251    ///
3252    /// On the `ReadDbLink` path this is also where a pvalink NTEnum
3253    /// carrier ([`EpicsValue::EnumWithChoices`]) is resolved. The
3254    /// dbrType-blind link resolver produces it for an NTEnum source;
3255    /// pvxs `pvaGetValue` (`pvalink_lset.cpp:330-360`) picks
3256    /// label-vs-index by the TARGET field's dbrType — only a DBR_STRING
3257    /// target gets the `choices[index]` label, every other type takes
3258    /// the numeric index. Route it through [`EpicsValue::convert_to`]
3259    /// (the single value-coercion owner) against the target field's
3260    /// `db_field_type`, so the transient carrier is consumed before any
3261    /// record `put_field` / storage / wire path can see it. The
3262    /// single-INP→VAL apply path reaches the same `convert_to` via
3263    /// `set_val`'s `TypeMismatch` auto-coerce.
3264    fn put_field_internal(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
3265        put_field_internal_default(self, name, value)
3266    }
3267
3268    /// Return pre-process actions (ReadDbLink) that the framework should
3269    /// execute BEFORE calling process(). This is called once per cycle.
3270    /// Default returns empty. Override in records that need link reads
3271    /// to be available during process().
3272    fn pre_process_actions(&mut self) -> Vec<ProcessAction> {
3273        Vec::new()
3274    }
3275
3276    /// Return actions the framework must execute BEFORE the input-link
3277    /// (`multi_input_links`, INP -> value-field) fetch for this cycle.
3278    ///
3279    /// This is strictly earlier than [`Self::pre_process_actions`]: the
3280    /// framework resolves input links *before* it calls
3281    /// `pre_process_actions`, so an action that must affect what an
3282    /// input link reads cannot be expressed there.
3283    ///
3284    /// The motivating case is the epid record's `devEpidSoftCallback`
3285    /// DB-type TRIG link: C `devEpidSoftCallback.c:120-132` writes the
3286    /// readback-trigger link with `dbPutLink` — which synchronously
3287    /// processes the triggered source chain — and only *then*
3288    /// (`devEpidSoftCallback.c:151`) does `dbGetLink(&pepid->inp, ...)`
3289    /// read `CVAL`. The trigger write therefore has to land before the
3290    /// `INP -> CVAL` fetch, in the same process pass.
3291    ///
3292    /// Called once per cycle, while a record write lock is held; the
3293    /// framework executes the returned actions (currently `WriteDbLink`
3294    /// and `ReadDbLink`) and then performs the input-link fetch.
3295    /// Default returns empty.
3296    fn pre_input_link_actions(&mut self) -> Vec<ProcessAction> {
3297        Vec::new()
3298    }
3299
3300    /// Called by the framework immediately before `process()` to push a
3301    /// read-only snapshot of framework-owned [`crate::server::record::CommonFields`] state
3302    /// ([`ProcessContext`]) that the record's `process()` needs to see.
3303    ///
3304    /// The framework owns `RecordInstance.common`; a record `process()`
3305    /// only gets `&mut self`. C records read `dbCommon` directly — e.g.
3306    /// `epidRecord.c:195` checks `pepid->udf` at the top of `process()`,
3307    /// `timestampRecord.c:90` branches on `ptimestamp->tse`. This hook
3308    /// is the controlled equivalent: a record that needs `udf`/`phas`/
3309    /// `tse`/`tsel` during `process()` overrides this to stash the
3310    /// values into its own fields.
3311    ///
3312    /// Additive, framework-set-hook pattern (same shape as
3313    /// [`Record::set_device_did_compute`]). Default: ignore — most
3314    /// records never need common state during `process()`.
3315    fn set_process_context(&mut self, _ctx: &ProcessContext) {}
3316
3317    /// Called once by the framework when the record is registered
3318    /// (`add_record`), delivering the record its own canonical name plus a
3319    /// cycle-free [`crate::server::database::AsyncDbHandle`] for driving
3320    /// async-side updates from OUTSIDE a `process()` cycle.
3321    ///
3322    /// The handle wraps a `Weak` reference to the database, so a record
3323    /// that stashes it creates no ownership cycle (the database owns the
3324    /// record; a stored strong handle would leak it). It is the controlled
3325    /// equivalent of C device support capturing `precord` plus the
3326    /// dbCommon scan lock for an out-of-band `db_post_events` /
3327    /// `callbackRequest`: e.g. the asyn TRACE/exception callback posts
3328    /// trace-flag fields immediately from the driver thread, and AQR
3329    /// cancels a queued I/O re-entry — neither happens inside `process()`.
3330    ///
3331    /// The in-band counterpart for a record's *own* process cycle is the
3332    /// completion-driven [`ProcessAction`] family
3333    /// ([`ProcessAction::WriteDbLinkNotify`],
3334    /// [`ProcessAction::CancelReprocess`],
3335    /// [`ProcessAction::ReprocessAfter`]); this hook exists for the
3336    /// out-of-band path that has no `process()` return to ride on.
3337    ///
3338    /// Additive, framework-set-hook pattern (same shape as
3339    /// [`Self::set_process_context`]). Default: ignore — most records do
3340    /// no out-of-band async posting.
3341    fn set_async_context(&mut self, _name: String, _db: crate::server::database::AsyncDbHandle) {}
3342
3343    /// Framework init hook: called once at record load *after* the common
3344    /// link fields (`INP`/`OUT`/`FLNK`/...) have been resolved and the
3345    /// `init_record` passes have run, with the record's resolved
3346    /// [`CommonFields`](crate::server::record::CommonFields).
3347    ///
3348    /// This is the seam for records that classify their links into status
3349    /// diagnostics at init the way C `init_record` does (e.g. calcout's
3350    /// `INAV..INUV`/`OUTV` `menu(calcoutINAV)` checkLinks loop): a record's
3351    /// *common* link strings (`OUT` is a common field, not a record field)
3352    /// are invisible to [`Self::set_async_context`] — which runs at
3353    /// `add_record`, *before* the common fields are applied — and to
3354    /// `init_record`, which carries no `CommonFields`. The record captures
3355    /// whichever common links it needs here so a passive, never-processed
3356    /// record already exposes its link status. Records whose links are all
3357    /// record-owned (e.g. sseq DOLn/LNKn) do not need this hook.
3358    ///
3359    /// Additive, framework-set-hook pattern. Default: ignore.
3360    fn init_links(&mut self, _common: &crate::server::record::CommonFields) {}
3361
3362    /// Called by the framework before process() to indicate whether device
3363    /// support's read() already performed the record's compute step.
3364    /// Override in records that have a built-in compute (e.g., epid PID)
3365    /// to skip it when device support already ran it.
3366    /// Default: ignore.
3367    fn set_device_did_compute(&mut self, _did_compute: bool) {}
3368
3369    /// Whether this record has a raw-to-engineering (`RVAL → VAL`)
3370    /// `convert()` step that must be skipped on a `Soft Channel` input.
3371    ///
3372    /// C `devAiSoft.c:65` `read_ai` (and the other soft-channel input
3373    /// `read_xxx`) always returns 2 ("don't convert"), so `aiRecord.c`'s
3374    /// `if (status==0) convert(prec)` is bypassed for a `Soft Channel`
3375    /// input record. The framework expresses this by calling
3376    /// [`Record::set_device_did_compute`]`(true)` on the record before
3377    /// `process()`.
3378    ///
3379    /// This hook exists so the framework only suppresses `convert()` —
3380    /// NOT a record's entire built-in compute. Records like `epid` also
3381    /// override `set_device_did_compute` but interpret it as "skip the
3382    /// whole compute step" (the PID loop); those records have no
3383    /// `RVAL → VAL` convert and MUST keep the default `false` so a
3384    /// `Soft Channel` `epid` still runs `do_pid()` in `process()`.
3385    ///
3386    /// Default `false`: a record is only opted into the soft-channel
3387    /// convert-skip when it explicitly returns `true`.
3388    fn soft_channel_skips_convert(&self) -> bool {
3389        false
3390    }
3391
3392    /// Whether this output record's forward `VAL → RVAL` `convert()` must be
3393    /// SKIPPED on a process cycle where VAL is still undefined (`UDF != 0`) and
3394    /// no value source ran.
3395    ///
3396    /// C's output records take an early `goto CONTINUE` before `convert()` when
3397    /// the record is undefined and no value was sourced this cycle:
3398    ///
3399    /// ```c
3400    /// /* mbboRecord.c:199-217 (and ao/bo/mbboDirect alike) */
3401    /// if (!pact) {
3402    ///     if (!dbLinkIsConstant(&prec->dol) && omsl == closed_loop) {
3403    ///         ... prec->val = <DOL>;          /* value sourced -> udf cleared */
3404    ///     }
3405    ///     else if (prec->udf) {
3406    ///         recGblSetSevr(prec, UDF_ALARM, prec->udfs);
3407    ///         goto CONTINUE;                  /* skip udf=FALSE AND convert() */
3408    ///     }
3409    ///     prec->udf = FALSE;
3410    ///     convert(prec);                      /* VAL -> RVAL */
3411    /// }
3412    /// ```
3413    ///
3414    /// So a `caput REC.RVAL 1` on a bare `record(mbbo,"M"){}` (UDF still 1, no
3415    /// VAL put, no closed-loop DOL) leaves RVAL at the client value: `convert`
3416    /// never runs to recompute `RVAL = VAL(=0)`. Verified on the compiled
3417    /// softIoc — bare RVAL put reads back the put value; after a VAL put clears
3418    /// UDF, the next RVAL put IS overwritten by `convert`.
3419    ///
3420    /// The framework consults this before `process()`: an opted-in output record
3421    /// with `UDF != 0` and no value source this cycle is told
3422    /// [`Self::set_device_did_compute`]`(true)` so its `process()` skips the
3423    /// forward convert. A VAL put (UDF cleared in `field_io`) or a closed-loop
3424    /// DOL fetch (UDF cleared at the DOL-apply site) leaves `UDF == 0`, so the
3425    /// convert runs exactly as C's fall-through does.
3426    ///
3427    /// Default `false`. The rest of C's output family (ao/bo/mbboDirect) shares
3428    /// the same `goto CONTINUE`; they are a separate change and stay opted out
3429    /// here.
3430    fn skips_forward_convert_when_undefined(&self) -> bool {
3431        false
3432    }
3433
3434    /// C `mbboRecord.c:210-221` / `mbboDirectRecord.c:190-202` — the same
3435    /// `else if (prec->udf) goto CONTINUE` that skips the forward `convert()`
3436    /// ALSO jumps past the pre-output `recGblGetTimeStampSimm` call. So a soft
3437    /// (synchronous) mbbo/mbboDirect that is still UNDEFINED never stamps TIME
3438    /// on the first-pass output stage: the only other stamp after `CONTINUE:`
3439    /// is guarded by `if (pact)` (mbboRecord.c:256-258), which fires on
3440    /// ASYNCHRONOUS completion re-entry only. A sync UDF record therefore keeps
3441    /// TIME at the EPICS epoch ("never processed") until a VAL put clears UDF.
3442    ///
3443    /// Contrast ao/bo/longout/int64out/stringout: their `if (!pact)` block
3444    /// calls `recGblGetTimeStampSimm` UNCONDITIONALLY (aoRecord.c:192,
3445    /// boRecord.c:215), so they stamp even while undefined and do NOT opt in.
3446    ///
3447    /// The framework consults this at the synchronous output-stage stamp
3448    /// (`processing.rs` `process_record_with_links_inner`, the pre-output
3449    /// `apply_timestamp`): an opted-in record with `UDF != 0` skips that stamp,
3450    /// mirroring C's `goto CONTINUE`. The async-completion stamp
3451    /// (`complete_async_record_inner`) stays unconditional, matching C's
3452    /// `if (pact)` re-stamp on async devices.
3453    ///
3454    /// This is a SEPARATE hook from [`Self::skips_forward_convert_when_undefined`]:
3455    /// mbboDirect's VAL is bit-derived and does NOT opt into the convert-skip,
3456    /// yet it DOES share this timestamp-skip. Do not conflate the two.
3457    ///
3458    /// Default `false`. Only mbbo/mbboDirect carry the `goto CONTINUE`
3459    /// timestamp-skip in C; every other record stays opted out.
3460    fn skips_timestamp_when_undefined(&self) -> bool {
3461        false
3462    }
3463}
3464
3465/// The body of [`Record::put_field_internal`] — the framework's internal write
3466/// path — as a free function, so a record that needs to observe an internal
3467/// write can WRAP it instead of re-implementing it.
3468///
3469/// A record overriding `put_field_internal` and ending in `self.put_field(..)`
3470/// silently drops the coercion below for every field it does not special-case.
3471/// Calling this instead keeps the one owner of that coercion.
3472///
3473/// Input-link / internal delivery coerces the source to the target field's
3474/// stored type before `put_field`, mirroring C `dbGetLink(DBF_<target>)`: the
3475/// link layer converts any numeric source to the requested type, so a record's
3476/// typed `put_field` arm never sees a mismatched type. This covers every
3477/// `ReadDbLink` target by construction (e.g. a `compress` INP from a `DBF_LONG`
3478/// record delivers a `Long`/`LongArray` that must become `Double`/`DoubleArray`
3479/// for the Double-only VAL arm, which otherwise drops it and never advances the
3480/// buffer). An `EnumWithChoices` carrier is always collapsed to a bare index by
3481/// `convert_to`, even when the target is already `Enum`.
3482pub fn put_field_internal_default<R: Record + ?Sized>(
3483    record: &mut R,
3484    name: &str,
3485    value: EpicsValue,
3486) -> CaResult<()> {
3487    // Coerce to the type the record STORES, not the type it SERVES. The two are
3488    // the same for most fields, but a `menu()` field is declared `DBF_MENU` and
3489    // served as `DBR_ENUM` with its choices (`promote_menu_value`) while the
3490    // record stores the bare choice index as a `Short` — and `put_field`'s arms
3491    // match on what is stored. Coercing to the served type would hand every
3492    // `put_field` an `Enum` its `Short` arm cannot match. This is the inverse of
3493    // `promote_menu_value`, and asking the record what it holds keeps the rule
3494    // uniform instead of special-casing menus here.
3495    //
3496    // The `.dbd` type is the fallback for a field the record cannot currently
3497    // produce a value for (an uninitialised array, a port-internal field).
3498    let target_type = record
3499        .get_field(name)
3500        .map(|v| v.db_field_type())
3501        .or_else(|| crate::server::record::record_instance::declared_field_type_of(record, name));
3502    // An array source into a SCALAR destination delivers element 0. C's link
3503    // layer asks for exactly one element (`dbGetLink(..., nRequest = NULL)`), so
3504    // `dbGet` converts the field at offset 0 and the record sees a scalar — a
3505    // waveform INP into an `ai.VAL` lands `wf[0]`, it is not dropped. Without the
3506    // reduction the array reached the record's typed `put_field` arm, which
3507    // rejected it and left the field at its stale value. Same clamp as
3508    // `field_io::dbput_request` (C `dbPut` `nRequest -> no_elements`), through the
3509    // same primitive; a `CharArray` into a `DBF_STRING` field is likewise exempt —
3510    // that shape is the dbChannel `$` char view of a string field, decoded by
3511    // `convert_to`.
3512    let dest_is_array = record.get_field(name).is_some_and(|v| v.is_array());
3513    let is_char_string_view =
3514        matches!(value, EpicsValue::CharArray(_)) && target_type == Some(DbFieldType::String);
3515    let value = if !dest_is_array && value.is_array() && !is_char_string_view {
3516        value.first_element().unwrap_or(value)
3517    } else {
3518        value
3519    };
3520    let is_enum_carrier = matches!(value, EpicsValue::EnumWithChoices { .. });
3521    let value = match target_type {
3522        // A String target routes through the converter even on a type match: C's
3523        // `putStringString` truncates to `field_size - 1` (see `coerce_put_value`).
3524        Some(target)
3525            if is_enum_carrier
3526                || ((value.db_field_type() != target || target == DbFieldType::String)
3527                    && !value.is_empty_array()) =>
3528        {
3529            coerce_put_value(record, name, target, value)?
3530        }
3531        // Carrier with no known target field: collapse to a bare index (the prior
3532        // fallback) rather than letting it reach storage.
3533        None if is_enum_carrier => value.convert_to(DbFieldType::Long),
3534        _ => value,
3535    };
3536    record.put_field(name, value)
3537}
3538
3539/// Coerce a written value to a field's stored type — the single owner of C
3540/// `dbConvert.c`'s `dbFastPutConvertRoutine[dbrType][field_type]` table, shared
3541/// by the two paths a value can enter a record's field through: a client
3542/// `dbPut` (`crate::server::database::field_io`) and an internal link /
3543/// device-support delivery ([`put_field_internal_default`]).
3544///
3545/// Every `DBR_STRING` row of C's put table is a converter that can FAIL, and
3546/// none of them is `EpicsValue::convert_to`:
3547///
3548/// * `DBF_MENU` → `putStringMenu` — exact label, else an index below `nChoice`
3549///   ([`crate::server::record::resolve_menu_field_string`]).
3550/// * `DBF_ENUM` → `putStringEnum` — the record's state strings, else an index
3551///   below `no_str` ([`crate::server::record::resolve_enum_state_string`]).
3552/// * `DBF_STRING` → `putStringString` — a byte copy, the one row that cannot
3553///   fail.
3554/// * every numeric width → `putStringChar` … `putStringDouble`, i.e.
3555///   `epicsParse*`, which refuses the put on overflow and on unparseable text
3556///   ([`c_parse::put_string`]).
3557///
3558/// `convert_to` cannot express any of the failures — it is field-blind and
3559/// total, mapping unparseable text to `0` and an out-of-range number to the
3560/// nearest representable one. That is how `caput MY:VALVE Open` became a silent
3561/// no-op that drove `VAL` to state 0, and how `caput REC.PREC 32768` — which the
3562/// compiled softIoc REFUSES — stored 32767.
3563///
3564/// An ARRAY destination keeps the coercion path. C reaches it through the same
3565/// `putString*` routine (`nRequest` elements, parsed one at a time), but this
3566/// port's array records carry their own element-type conversion, so the row is
3567/// theirs to own; routing a string here would break the string→`DBF_CHAR[]`
3568/// carry that `convert_to` provides for them.
3569pub fn coerce_put_value<R: Record + ?Sized>(
3570    record: &R,
3571    field: &str,
3572    target: DbFieldType,
3573    value: EpicsValue,
3574) -> CaResult<EpicsValue> {
3575    if let EpicsValue::String(s) = &value {
3576        // DTYP (DBF_DEVICE) validates against the record type's FULL device
3577        // menu — static `device()` lines PLUS runtime-contributed device
3578        // support — the same set the read/announce path exposes via
3579        // `RecordInstance::device_choices`. `menu_choices_of`'s DTYP branch
3580        // returns only the static half, so a contributed device-support name
3581        // (asyn's `asynInt32`, scaler-rs's `Asyn Scaler`, ...) would wrongly
3582        // fail this put even though a client can read it in the DTYP choices.
3583        // Resolve DTYP against the merged menu to keep put and read symmetric.
3584        if field.eq_ignore_ascii_case("DTYP") {
3585            let choices = super::merged_device_menu(record.record_type());
3586            if !choices.is_empty() {
3587                return super::resolve_menu_field_string(
3588                    field,
3589                    &choices,
3590                    target,
3591                    &s.as_str_lossy(),
3592                );
3593            }
3594            // No device menu declared or contributed for this record type: fall
3595            // through to the generic handling below (unchanged behavior).
3596        } else if let Some(choices) = super::record_instance::menu_choices_of(record, field) {
3597            return super::resolve_menu_field_string(field, choices, target, &s.as_str_lossy());
3598        }
3599        if target == DbFieldType::Enum {
3600            return super::resolve_enum_state_string(
3601                field,
3602                record.enum_state_strings().as_deref(),
3603                s,
3604            );
3605        }
3606        let dest_is_array = record.get_field(field).is_some_and(|v| v.is_array());
3607        if !dest_is_array {
3608            if let Some(numeric) = c_parse::NumericField::of(target) {
3609                return c_parse::put_string(field, numeric, &s.as_str_lossy());
3610            }
3611            if target == DbFieldType::String {
3612                // C `putStringString` (dbConvert.c:916-925): `strncpy(pdst, psrc,
3613                // field_size); pdst[field_size-1] = 0` — the DBF_STRING put
3614                // truncates to `field_size - 1` bytes. The row is NOT a no-op even
3615                // for a String source, so it must run even when source and stored
3616                // type match (the two gates that call this converter skip it on a
3617                // type match; both route a String target here regardless). The CA
3618                // wire already caps a DBR_STRING at `MAX_STRING_SIZE - 1` (39), so
3619                // this only bites a field whose `.dbd` `size(N)` is under 40 —
3620                // dbCommon `ASG` `size(29)` → 28, and the like.
3621                return Ok(EpicsValue::String(cap_string_to_field_size(
3622                    record, field, s,
3623                )));
3624            }
3625        }
3626    }
3627    Ok(value.convert_to(target))
3628}
3629
3630/// C `putStringString`'s truncation: a `DBF_STRING` field stores at most
3631/// `field_size - 1` bytes (its `.dbd` `size(N)` less the forced NUL). A field
3632/// with no declared size (`0` — a Tier 3 hand table, or a field with no
3633/// declaration) is left uncapped beyond the wire's own `MAX_STRING_SIZE - 1`.
3634fn cap_string_to_field_size<R: Record + ?Sized>(record: &R, field: &str, s: &PvString) -> PvString {
3635    match super::record_instance::field_desc_of(record, field) {
3636        Some(desc) if desc.size > 0 => {
3637            let cap = (desc.size as usize).saturating_sub(1);
3638            let bytes = s.as_bytes();
3639            if bytes.len() > cap {
3640                PvString::from_bytes(bytes[..cap].to_vec())
3641            } else {
3642                s.clone()
3643            }
3644        }
3645        _ => s.clone(),
3646    }
3647}
3648
3649/// Subroutine function type for `sub`/`aSub` records.
3650///
3651/// The return value is the subroutine's C `long` status
3652/// (`subRecord.c::do_sub` / `aSubRecord.c::do_sub`): `< 0` raises
3653/// `SOFT_ALARM` at the record's `BRSV` severity, and for `aSub` the status
3654/// is published as `VAL` (`aSubRecord.c:223`). Return `Ok(0)` for the
3655/// normal no-alarm path. `Err(..)` is reserved for an infrastructure
3656/// failure inside the closure (e.g. a field write error), which aborts
3657/// processing — it is distinct from a negative status.
3658pub type SubroutineFn = Box<dyn Fn(&mut dyn Record) -> CaResult<i64> + Send + Sync>;