Skip to main content

latexml_core/
parameter.rs

1use std::{fmt, rc::Rc};
2
3use once_cell::sync::Lazy;
4#[cfg(feature = "codegen")]
5use proc_macro2::TokenStream;
6#[cfg(feature = "codegen")]
7use quote::{ToTokens, quote};
8use regex::Regex;
9
10use crate::{
11  Digested,
12  common::{
13    arena::{self, SymStr},
14    error::{emit_warn, *},
15    object::Object,
16  },
17  definition::{
18    BeforeDigestClosure, Definition, DigestionClosure, argument::ArgWrap, constructor::Constructor,
19  },
20  gullet,
21  mouth::Mouth,
22  pin,
23  state::*,
24  token::{Catcode, Token},
25  tokens::Tokens,
26  whatsit::Whatsit,
27};
28
29pub type ReaderFn = dyn Fn(Option<&Parameters>, &[Tokens]) -> Result<ArgWrap>;
30pub type ReaderPredigestFn = dyn Fn(ArgWrap, &[Tokens]) -> Result<Option<Digested>>;
31pub type ReaderPredigestClosure = Rc<ReaderPredigestFn>;
32pub type ReaderClosure = Rc<ReaderFn>;
33
34// Rust Note:
35// the reversion functions initially had "&mut Gullet" as a parameter.
36// This turned out to be infeasible if we are to maintain the latexml code flow
37// as we have calls into reversions from arbitrary binding closures, at ALL phases.
38// Compromise: use the gated Stomach in state::whenever you need gullet in reversion, as in
39// let mut stomach = state::stomach.borrow_mut();
40//
41//
42pub type ReversionClosure =
43  Rc<dyn Fn(Vec<Token>, Option<&Parameters>, &[Tokens]) -> Result<Tokens>>;
44
45/// A reversion closure that operates on the original Digested argument,
46/// enabling access to structured data (e.g., KeyVals) for custom reversion formatting.
47/// Perl equivalent: the `reversion` option on DefParameterType, which receives the raw value.
48pub type DigestedReversionClosure = Rc<dyn Fn(&Digested) -> Result<Tokens>>;
49
50static LAST_WCHAR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\w$").unwrap());
51static FIRST_WCHAR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\w").unwrap());
52
53#[derive(Clone)]
54pub struct Parameter {
55  pub novalue:            bool,
56  pub semiverbatim:       Option<Vec<char>>,
57  pub optional:           bool,
58  pub name:               SymStr,
59  pub spec:               SymStr,
60  pub extra:              Vec<Tokens>,
61  pub inner:              Option<Parameters>,
62  pub reader:             ReaderClosure,
63  pub predigest:          Option<ReaderPredigestClosure>,
64  pub reversion:          Option<ReversionClosure>,
65  /// Reversion closure that operates on the original Digested argument.
66  /// Takes precedence over `reversion` when the argument is a digested value.
67  /// Perl equivalent: `reversion` option on DefParameterType with `undigested => 1`.
68  pub digested_reversion: Option<DigestedReversionClosure>,
69  pub before_digest:      Vec<BeforeDigestClosure>,
70  pub after_digest:       Vec<DigestionClosure>,
71}
72impl Default for Parameter {
73  fn default() -> Self {
74    Parameter {
75      novalue:            false,
76      semiverbatim:       None,
77      optional:           false,
78      name:               pin!("parameter_default"),
79      spec:               pin!(""),
80      extra:              Vec::new(),
81      inner:              None,
82      reader:             Rc::new(|_args, _extra| {
83        Warn!(
84          "Parameter",
85          "mock_reader",
86          "Please define a real reader, this is a mock fallback!"
87        );
88        Ok(ArgWrap::None)
89      }),
90      predigest:          None,
91      reversion:          None,
92      digested_reversion: None,
93      before_digest:      Vec::new(),
94      after_digest:       Vec::new(),
95    }
96  }
97}
98impl fmt::Debug for Parameter {
99  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
100    writeln!(
101      f,
102      "Parameter(\n\t name:{:?}, novalue:{:?}, semiverbatim:{:?},",
103      self.name, self.novalue, self.semiverbatim,
104    )?;
105    writeln!(f, "\t optional:{:?}, spec:{:?}", self.optional, self.spec)?;
106    writeln!(f, "\t inner: {:?}", self.inner)?;
107    writeln!(
108      f,
109      "\t extra: {:?}\n\t reversion: {:?}, before_digest: {:?}, after_digest: {:?} )",
110      self.extra,
111      self.reversion.is_some(),
112      self.before_digest.len(),
113      self.after_digest.len()
114    )
115  }
116}
117impl fmt::Display for Parameter {
118  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
119    arena::with(self.name, |name| write!(f, "{name}"))
120  }
121}
122
123impl PartialEq for Parameter {
124  fn eq(&self, other: &Parameter) -> bool { self.name == other.name }
125}
126impl Object for Parameter {
127  fn stringify(&self) -> String { arena::to_string(self.spec) }
128}
129
130static OPTIONAL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^Optional(.+)$").unwrap());
131static SKIP_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^Skip(.+)$").unwrap());
132
133impl Parameter {
134  pub fn new<T: AsRef<str>>(name: T, spec: T, extra: Option<Vec<Tokens>>) -> Result<Self> {
135    Parameter {
136      name: arena::pin(name),
137      spec: arena::pin(spec),
138      extra: extra.unwrap_or_default(),
139      ..Parameter::default()
140    }
141    .init()
142  }
143  pub fn init(mut self) -> Result<Self> {
144    // Create a parameter reading object for a specific type.
145    // If either a declared entry or a function Read<Type> accessible from LaTeXML::Package::Pool
146    // is defined.
147    let mut descriptor: Option<Rc<Parameter>> =
148      with_mapping_sym(pin!("PARAMETER_TYPES"), self.name, |looked_up_mapping| {
149        if let Some(Stored::Parameter(d_lookup)) = looked_up_mapping {
150          Some(Rc::clone(d_lookup))
151        } else {
152          None
153        }
154      });
155    if descriptor.is_none() {
156      // TODO: see discussion on line 168
157      let basetype_opt = arena::with(self.name, |name| {
158        OPTIONAL_REGEX
159          .captures(name)
160          .map(|captures| captures.get(1).map_or("", |m| m.as_str()).to_string())
161      })
162      .map(arena::pin);
163      if let Some(basetype) = basetype_opt {
164        descriptor = with_mapping_sym(pin!("PARAMETER_TYPES"), basetype, |basetype_param_opt| {
165          match basetype_param_opt {
166            Some(Stored::Parameter(d_lookup)) => Ok(Some(d_lookup.clone())),
167            _ => match Parameter::check_reader_function(&arena::with(self.name, |name| {
168              s!("Read{name}")
169            })) {
170              Some(reader) => Ok(Some(Rc::new(Parameter {
171                reader,
172                optional: true,
173                ..Parameter::default()
174              }))),
175              None => match Parameter::check_reader_function(&arena::with(basetype, |type_str| {
176                s!("Read{type_str}")
177              })) {
178                Some(reader) => Ok(Some(Rc::new(Parameter {
179                  reader,
180                  optional: true,
181                  novalue: true,
182                  ..Parameter::default()
183                }))),
184                None => fatal!(
185                  Parameter,
186                  Init,
187                  s!("Can't initialize parameter {:?}, unknown?", self.name)
188                ),
189              },
190            },
191          }
192        })?;
193        self.optional = true;
194      } else {
195        // TODO: This looks like a code smell. Do we need a new arena method?
196        // We start with a ticket, do a non-allocation operation on the underlying &str,
197        // Then want to ping the newly acquired &str slice in the arena. Clearly that is only
198        // possible *AFTER* the original &str is released, but how do we avoid allocating?
199        // Is this a use for unsafe{} or is there a more idiomatic way?
200        // Maybe, with_mut... which allows an inner pin?
201        let basetype_opt = arena::with(self.name, |name| {
202          SKIP_REGEX
203            .captures(name)
204            .map(|captures| captures.get(1).map_or("", |m| m.as_str()).to_string())
205        })
206        .map(arena::pin);
207        if let Some(basetype) = basetype_opt {
208          descriptor = with_mapping_sym(pin!("PARAMETER_TYPES"), basetype, |basetype_param_opt| {
209            match basetype_param_opt {
210              Some(Stored::Parameter(d_lookup)) => Some(d_lookup.clone()),
211              _ => match arena::with(self.name, |name| Parameter::check_reader_function(name)) {
212                Some(reader) => Some(Rc::new(Parameter {
213                  reader,
214                  optional: true,
215                  novalue: true,
216                  ..Parameter::default()
217                })),
218                None => Parameter::check_reader_function(&arena::with(basetype, |type_str| {
219                  s!("Read{type_str}")
220                }))
221                .map(|reader| {
222                  Rc::new(Parameter {
223                    reader,
224                    optional: true,
225                    novalue: true,
226                    ..Parameter::default()
227                  })
228                }),
229              },
230            }
231          });
232          if let Some(ref _desc) = descriptor {
233            self.novalue = true;
234            self.optional = true;
235          }
236        } else {
237          descriptor =
238            Parameter::check_reader_function(&arena::with(self.name, |name| s!("Read{name}")))
239              .map(|reader| Rc::new(Parameter { reader, ..Parameter::default() }));
240        }
241      }
242    }
243    match descriptor {
244      Some(descriptor) => {
245        // descriptor needs to get integrated into Self
246        //  except `spec` and `name` which are always preserved!
247        self.reader = descriptor.reader.clone(); // What else?
248        if descriptor.novalue {
249          self.novalue = true;
250        }
251        self.semiverbatim.clone_from(&descriptor.semiverbatim);
252        // Also doing optional setting on the fly, so don't override unless true
253        // self.optional = descriptor.optional;
254        if descriptor.optional {
255          self.optional = true;
256        }
257        self.reversion.clone_from(&descriptor.reversion);
258        self
259          .digested_reversion
260          .clone_from(&descriptor.digested_reversion);
261        self.before_digest.clone_from(&descriptor.before_digest);
262        self.after_digest.clone_from(&descriptor.after_digest);
263        self.predigest.clone_from(&descriptor.predigest);
264      },
265      None => fatal!(
266        Parameter,
267        Unknown,
268        arena::with2(self.name, self.spec, |name, spec| s!(
269          "Unrecognized parameter type with name {:?}, spec {:?}",
270          name,
271          spec
272        ))
273      ),
274    }
275    // Last but not least, initialize any "inner" parameters
276    self.inner = self.inner.map(|inner_ps| match inner_ps.clone().init() {
277      Ok(ps) => ps,
278      Err(e) => {
279        emit_warn(
280          "internal",
281          "parameter",
282          &format!("inner parameter init failed: {e}"),
283        );
284        inner_ps
285      },
286    });
287    Ok(self)
288  }
289
290  /// Obtain the reader of a given parameter name, if available
291  pub fn check_reader_function(name: &str) -> Option<ReaderClosure> {
292    // TODO: This function doesn't have a direct Rust equivalent, since the metaprogramming isn't
293    // possible But what is the exact purpose of seeking through the pool namespace? Wouldn't
294    // any parameter be already assigned in the state::
295    with_mapping("PARAMETER_TYPES", name, |param_opt| {
296      if let Some(Stored::Parameter(param)) = param_opt {
297        Some(param.reader.clone())
298      } else {
299        None
300      }
301    })
302  }
303
304  pub fn setup_catcodes(&self) {
305    if self.semiverbatim.is_some() {
306      begin_semiverbatim(self.semiverbatim.as_deref());
307    }
308  }
309
310  pub fn revert_catcodes(&self) -> Result<()> {
311    if self.semiverbatim.is_some() {
312      end_semiverbatim()?;
313    }
314    Ok(())
315  }
316
317  pub fn read(&self, fordefn: Option<&dyn Definition>) -> Result<ArgWrap> {
318    // For semiverbatim, I had messed with catcodes, but there are cases
319    // (eg. \caption(...\label{badchars}}) where you really need to
320    // cleanup after the fact!
321    // Hmmm, seem to still need it...
322    self.setup_catcodes();
323
324    let closure = &self.reader;
325    let value_from_reader: ArgWrap = closure(self.inner.as_ref(), &self.extra)?;
326    // Direct enum destructure: was `is_tokens() then owned_tokens()`
327    // which matched twice (once for the is_tokens check, again for
328    // the owned_tokens dispatch over all ArgWrap variants). This
329    // function fires on every parameter read of every macro call —
330    // ~2M times on si.tex per callgrind.
331    let value_arg = match value_from_reader {
332      ArgWrap::Tokens(mut value) => {
333        if let Some(ref semi_chars) = self.semiverbatim {
334          value = value.neutralize(semi_chars);
335        }
336        ArgWrap::Tokens(value)
337      },
338      other => other,
339    };
340    self.revert_catcodes()?;
341
342    // Single arena borrow to compute both name-prefix checks that
343    // this function needs — was two separate `arena::with` calls
344    // (each a RefCell borrow + interner resolve), now a single
345    // closure that returns the pair.
346    let (is_optional_match, is_until) = arena::with(self.name, |name| {
347      (name.starts_with("OptionalMatch"), name.starts_with("Until"))
348    });
349
350    // Perl: experiment: skip spaces after a successful OptionalMatch read
351    if !value_arg.is_none() && self.optional && is_optional_match {
352      gullet::skip_spaces()?;
353    }
354
355    let checked_value =
356      if !self.optional && !self.novalue && (value_arg.is_none() && self.predigest.is_none()) {
357        // Deyan: Special exception, which may motivate switching the reader type to Option<Tokens>
358        // in the long-run        Until *may* have a value, but it also may *not*, both OK.
359        // So... except it from the error message here
360        if !is_until {
361          let fordefn_str = fordefn.map(|fdefn| fdefn.stringify()).unwrap_or_default();
362          Error!(
363            "expected",
364            self,
365            s!("Missing argument {} for {}", self.stringify(), fordefn_str)
366          );
367          ArgWrap::Tokens(Tokens!(T_OTHER!("missing")))
368        } else {
369          value_arg
370        }
371      } else {
372        value_arg
373      };
374    Ok(checked_value)
375  }
376
377  pub fn digest(
378    &self,
379    mut value_arg: ArgWrap,
380    _fordefn: Option<&Constructor>,
381  ) -> Result<Option<Digested>> {
382    // Perl Parameter.pm lines 122,139-141: capture MODE, check after digest
383    let mode = lookup_string_from_sym(crate::pin!("MODE"));
384    // If semiverbatim, Expand (before digest), so tokens can be neutralized; BLECH!!!!
385    if self.semiverbatim.is_some() {
386      self.setup_catcodes();
387      if value_arg.is_tokens() {
388        if let Some(value) = value_arg.owned_tokens() {
389          let neutralized = gullet::reading_from_mouth(Mouth::default(), move || {
390            gullet::unread(value);
391            let mut tokens = Vec::new();
392            loop {
393              match gullet::get_pending_comment() {
394                Some(token) => tokens.push(token),
395                None => match gullet::read_x_token(Some(true), false, None) {
396                  Ok(token_opt) => match token_opt {
397                    Some(token) => tokens.push(token),
398                    None => break,
399                  },
400                  Err(x) => return Err(x),
401                },
402              }
403            }
404            Ok(Tokens::new(tokens).neutralize(&[]))
405          })?;
406          value_arg = ArgWrap::Tokens(neutralized);
407        } else {
408          value_arg = ArgWrap::default();
409        }
410      }
411    }
412
413    for pre in self.before_digest.iter() {
414      // Done for effect only.
415      pre()?; // maybe pass extras?
416    }
417    let digested_value = if let Some(closure) = &self.predigest {
418      closure(value_arg, &self.extra)?
419    } else {
420      // Note: we have an open question for the type interface.
421      //  What happens when a wrapped "None" value,
422      // (such as the missing value of an Optional [] argument)
423      // gets digested?
424      //
425      // currently a `Digested::default` gets returned, which has an empty TBox and also gets
426      // returned for e.g. empty mandatory Plain arguments {}.
427      // But we need *different* values, as the explicit "\foo[]" is an override to empty, while
428      // "\foo" will use the default value for the Optional.
429      if self.optional && value_arg.is_none() {
430        None
431      } else {
432        Some(value_arg.be_digested()?)
433      }
434    };
435    for post in self.after_digest.iter() {
436      // Done for effect only.
437      let mut w = Whatsit::default();
438      post(&mut w)?; // maybe pass extras?
439    }
440
441    self.revert_catcodes()?;
442
443    // Perl Parameter.pm lines 139-141: avoid mode change leaking out of parameter digestion
444    let newmode = lookup_string_from_sym(crate::pin!("MODE"));
445    if mode != newmode && mode != "horizontal" {
446      crate::stomach::leave_horizontal_internal();
447    }
448
449    Ok(digested_value)
450  }
451
452  pub fn revert(&self, value_opt: Option<Tokens>) -> Result<Option<Tokens>> {
453    if let Some(ref reverter) = self.reversion {
454      if let Some(value) = value_opt {
455        Ok(Some((reverter)(
456          value.unlist(),
457          self.inner.as_ref(),
458          &self.extra,
459        )?))
460      } else {
461        Ok(None)
462      }
463    } else if let Some(value) = value_opt {
464      Ok(Some(Tokens::new(value.revert())))
465    } else {
466      Ok(None)
467    }
468  }
469
470  /// This is needed by structured parameter types like KeyVals
471  /// where the argument may already have been tokenized before the KeyVals
472  /// (and the parameter types for the keys) had a chance to properly parse.
473  // Yuck!
474  pub fn reparse(&self, tokens: Tokens) -> Result<ArgWrap> {
475    // Needs neutralization, since the keyvals may have been tokenized already???
476    // perhaps a better test would involve whether $tokens is, in fact, Tokens?
477    if self.name == pin!("Plain") || self.predigest.is_some() {
478      // Gack!
479      Ok(ArgWrap::Tokens(tokens))
480    } else if self.semiverbatim.is_some() {
481      // Needs neutralization
482      // but maybe specific to catcodes
483      Ok(ArgWrap::Tokens(
484        tokens.neutralize(self.semiverbatim.as_ref().unwrap().as_slice()),
485      ))
486    } else {
487      gullet::reading_from_mouth(Mouth::default(), || {
488        // start with empty mouth
489        let mut tokens = tokens.unlist();
490        if !tokens.is_empty() // Strip outer braces from dimensions & friends
491          && arena::with(self.name,|name|
492              matches!(name, "Number"|"Dimension"|"Glue"|"MuDimension"|"MuGlue"))
493          && tokens.first().map(|t| t.get_catcode() == Catcode::BEGIN)
494              .unwrap_or(false)
495          && tokens.last().map(|t| t.get_catcode() == Catcode::END).unwrap_or(false)
496        {
497          tokens.remove(0);
498          tokens.pop();
499        }
500        gullet::unread_vec(tokens); // but put back tokens to be read
501        let value = self.read(None)?;
502        gullet::skip_spaces()?;
503        Ok(value)
504      })
505    }
506  }
507}
508
509#[derive(Clone, Debug, Default)]
510pub struct Parameters(Vec<Parameter>);
511
512impl PartialEq for Parameters {
513  fn eq(&self, other: &Parameters) -> bool { self.0 == other.0 }
514}
515impl Object for Parameters {
516  fn stringify(&self) -> String {
517    let mut result = String::new();
518    for parameter in self.0.iter() {
519      let s = parameter.stringify();
520      let lead_letter = match s.chars().next() {
521        Some(c) => c.is_alphanumeric(),
522        None => false,
523      };
524      let trail_letter = match result.chars().last() {
525        Some(c) => c.is_alphanumeric(),
526        None => false,
527      };
528      if lead_letter && trail_letter {
529        result.push(' ');
530      }
531      result.push_str(&s);
532    }
533    result
534  }
535}
536
537impl Parameters {
538  pub fn new(params: Vec<Parameter>) -> Self { Parameters(params) }
539  pub fn get_num_args(&self) -> usize { self.0.iter().filter(|&p| !p.novalue).count() }
540  pub fn get_parameters(&self) -> Vec<&Parameter> { self.0.iter().collect() }
541  pub fn take_parameters(self) -> Vec<Parameter> { self.0 }
542  pub fn revert_arguments(&self, args: Vec<Option<Tokens>>) -> Result<Vec<Token>> {
543    let mut tokens = Vec::new();
544    for (parameter, arg) in self.0.iter().zip(args) {
545      if !parameter.novalue
546        && let Some(reverted_tks) = parameter.revert(arg)?
547      {
548        tokens.extend(reverted_tks.unlist());
549      }
550    }
551    Ok(tokens)
552  }
553
554  /// Revert arguments from their digested form, using `digested_reversion` when available.
555  /// This allows parameter types (like BoxSpecification) to control reversion formatting
556  /// based on the structured digested data rather than token-level reversion.
557  /// Perl equivalent: `$parameters->revertArguments($self->getArgs)`
558  pub fn revert_digested_arguments(
559    &self,
560    digested_args: &[Option<Digested>],
561  ) -> Result<Vec<Token>> {
562    let mut tokens = Vec::new();
563    for (parameter, arg_opt) in self.0.iter().zip(digested_args) {
564      if !parameter.novalue {
565        let reverted = if let Some(ref digested_rev) = parameter.digested_reversion {
566          // Use digested_reversion: operates on the raw Digested value
567          match arg_opt {
568            Some(arg) => Some(digested_rev(arg)?),
569            None => None,
570          }
571        } else {
572          // Fall back to standard reversion: Digested → Tokens → Parameter::revert
573          let token_reverted = match arg_opt {
574            Some(arg) => Some(arg.revert()?),
575            None => None,
576          };
577          parameter.revert(token_reverted)?
578        };
579        if let Some(tks) = reverted {
580          tokens.extend(tks.unlist());
581        }
582      }
583    }
584    Ok(tokens)
585  }
586  // Try to initialize each associated Parameter
587  pub fn init(mut self) -> Result<Self> {
588    let mut initialized = Vec::new();
589    for param in self.0.drain(..) {
590      initialized.push(param.init()?);
591    }
592    self.0 = initialized;
593    Ok(self)
594  }
595
596  pub fn read_arguments(&self, fordefn: Option<&dyn Definition>) -> Result<Vec<ArgWrap>> {
597    let mut args = Vec::with_capacity(self.0.len());
598    for parameter in &self.0 {
599      let values = parameter.read(fordefn)?;
600      if parameter.predigest.is_some() {
601        // TODO: Sometimes we legitimately want to use e.g. Number parameters without the predigest
602        // closure... so this shouldn't be an error, not even an info -- but leaving it here
603        // if something changes in the future. error!(
604        //   target: &s!("parameter:{}", parameter.name),
605        //   "parameter with predigest closure was invoked in an expandable context. Parameter
606        // digestion won't execute." );
607      }
608      if !parameter.novalue {
609        args.push(values);
610      }
611    }
612    Ok(args)
613  }
614
615  pub fn read_arguments_and_digest(&self, fordefn: &Constructor) -> Result<Vec<Option<Digested>>> {
616    let mut args = Vec::with_capacity(self.0.len());
617    for parameter in &self.0 {
618      let value = parameter.read(Some(fordefn))?;
619      if !parameter.novalue {
620        let digested_value = parameter.digest(value, Some(fordefn))?;
621        args.push(digested_value);
622      }
623    }
624    Ok(args)
625  }
626
627  pub fn reparse_argument(&self, value: ArgWrap) -> Result<Vec<ArgWrap>> {
628    if value.is_none() {
629      return Ok(Vec::new());
630    }
631    let value_tokens = value.revert()?;
632    // start with empty mouth
633    let reader_mouth = Mouth::new("", None)?;
634    gullet::reading_from_mouth(reader_mouth, || {
635      gullet::unread(value_tokens); // but put back tokens to be read
636      let values = self.read_arguments(None)?;
637      gullet::skip_spaces()?;
638      Ok(values)
639    })
640  }
641
642  pub fn as_keysets(&self) -> Vec<String> { self.0.iter().map(|p| p.stringify()).collect() }
643}
644impl fmt::Display for Parameters {
645  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
646    let mut content = String::new();
647    for parameter in &self.0 {
648      let param_content = parameter.to_string();
649      if LAST_WCHAR_RE.is_match(&content) && FIRST_WCHAR_RE.is_match(&param_content) {
650        content.push(' ');
651      }
652      content.push_str(&param_content);
653    }
654    write!(f, "{content}")
655  }
656}
657
658impl From<Parameters> for Vec<Parameter> {
659  fn from(ps: Parameters) -> Vec<Parameter> { ps.0 }
660}
661
662// ToTokens impls gated by `codegen` feature — see comment in
663// `tokens.rs` for rationale (audit DEP-14, 2026-05-18).
664#[cfg(feature = "codegen")]
665impl ToTokens for Parameters {
666  fn to_tokens(&self, stream: &mut TokenStream) {
667    let params = &self.0;
668    stream.extend(quote! {
669        Parameters::new(<[Parameter]>::into_vec(Box::new([ #(#params),* ])))
670    });
671  }
672}
673
674#[cfg(feature = "codegen")]
675impl ToTokens for Parameter {
676  fn to_tokens(&self, stream: &mut TokenStream) {
677    let name = arena::with(self.name, |name| quote!(arena::pin_static(#name)));
678    let spec = arena::with(self.spec, |spec| quote!(arena::pin_static(#spec)));
679    let extra = &self.extra;
680    let inner = match &self.inner {
681      None => quote!(None),
682      Some(inner_ps) => quote!(Some(#inner_ps)),
683    };
684    stream.extend(quote! {
685      Parameter {
686        name: #name,
687        spec: #spec,
688        extra: <[Tokens]>::into_vec(Box::new([ #(#extra),* ])),
689        inner: #inner,
690        ..Parameter::default()
691      }
692    });
693  }
694}
695
696#[cfg(test)]
697mod tests {
698  use super::*;
699
700  #[test]
701  fn parameter_default_has_expected_fields() {
702    let p = Parameter::default();
703    assert!(!p.novalue);
704    assert!(p.semiverbatim.is_none());
705    assert!(!p.optional);
706    assert!(p.inner.is_none());
707    assert!(p.extra.is_empty());
708    assert!(p.before_digest.is_empty());
709    assert!(p.after_digest.is_empty());
710    assert!(p.predigest.is_none());
711    assert!(p.reversion.is_none());
712    assert!(p.digested_reversion.is_none());
713    assert_eq!(arena::to_string(p.name), "parameter_default");
714    assert_eq!(arena::to_string(p.spec), "");
715  }
716
717  #[test]
718  fn parameter_display_is_name() {
719    let p = Parameter {
720      name: arena::pin("Plain"),
721      ..Default::default()
722    };
723    assert_eq!(format!("{p}"), "Plain");
724  }
725
726  #[test]
727  fn parameter_stringify_is_spec() {
728    let p = Parameter {
729      spec: arena::pin("{}"),
730      ..Default::default()
731    };
732    assert_eq!(p.stringify(), "{}");
733  }
734
735  #[test]
736  fn parameter_partial_eq_by_name() {
737    // PartialEq compares by name only — Perl parity (closures can't
738    // be structurally compared).
739    let mut a = Parameter::default();
740    let mut b = Parameter::default();
741    a.name = arena::pin("x");
742    b.name = arena::pin("x");
743    assert_eq!(a, b);
744    b.name = arena::pin("y");
745    assert_ne!(a, b);
746  }
747
748  #[test]
749  fn parameters_new_and_take() {
750    let p = Parameter::default();
751    let ps = Parameters::new(vec![p]);
752    let taken = ps.take_parameters();
753    assert_eq!(taken.len(), 1);
754  }
755
756  #[test]
757  fn parameters_get_num_args_counts_valued() {
758    // novalue=true parameters don't count toward num_args.
759    let mut a = Parameter::default();
760    let mut b = Parameter::default();
761    let mut c = Parameter::default();
762    a.novalue = false;
763    b.novalue = true;
764    c.novalue = false;
765    let ps = Parameters::new(vec![a, b, c]);
766    assert_eq!(ps.get_num_args(), 2);
767  }
768
769  #[test]
770  fn parameters_empty() {
771    let ps = Parameters::new(vec![]);
772    assert_eq!(ps.get_num_args(), 0);
773    assert_eq!(ps.get_parameters().len(), 0);
774  }
775
776  #[test]
777  fn parameters_get_parameters_returns_refs_to_all() {
778    // get_parameters returns ALL, including novalue ones (num_args
779    // filters; get_parameters doesn't).
780    let mut a = Parameter::default();
781    let mut b = Parameter::default();
782    a.novalue = false;
783    b.novalue = true;
784    let ps = Parameters::new(vec![a, b]);
785    assert_eq!(ps.get_parameters().len(), 2);
786  }
787}