typeline_core/options/
chain_settings.rs

1use crate::{
2    cli::{
3        call_expr::{Argument, Span},
4        try_parse_bool,
5    },
6    record_data::{
7        field_value::FieldValue,
8        field_value_ref::FieldValueRef,
9        scope_manager::{Atom, ScopeId, ScopeManager, ScopeValue},
10    },
11    typelist,
12    utils::{
13        int_string_conversions::parse_int_with_units_from_bytes,
14        maybe_text::MaybeTextRef, string_store::StringStoreEntry,
15    },
16};
17
18use bstr::ByteSlice;
19use num::Zero;
20use std::{
21    ffi::OsString, marker::PhantomData, os::unix::ffi::OsStringExt,
22    path::PathBuf, sync::Arc,
23};
24use thiserror::Error;
25
26pub trait SettingTypeConverter {
27    type Type;
28    fn convert_to_type_from_maybe_text(
29        value: MaybeTextRef,
30    ) -> Result<Self::Type, SettingConversionError>;
31    fn convert_to_type(
32        value: FieldValueRef,
33    ) -> Result<Self::Type, SettingConversionError>;
34    fn convert_from_type(
35        value: Self::Type,
36    ) -> Result<FieldValue, SettingConversionError>;
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Error)]
40#[error("{message}")]
41pub struct SettingConversionError {
42    pub message: String,
43}
44
45#[derive(Clone, Copy, Default)]
46pub enum BufferingMode {
47    BlockBuffer,
48    LineBuffer,
49    LineBufferStdin,
50    #[default]
51    LineBufferIfTTY,
52    LineBufferStdinIfTTY,
53}
54
55pub const RARTIONAL_DECIMALS_DEFAULT_CUTOFF: u32 = 40;
56
57#[derive(Clone, Copy)]
58pub enum RationalsPrintMode {
59    Cutoff(u32),
60    Raw,
61    Dynamic,
62    // attempts to use plain if losslessly possible, otherwise uses raw
63}
64
65pub type ChainSettingNames = [StringStoreEntry; chain_settings_list::COUNT];
66
67pub trait ChainSetting: chain_settings_list::TypeList {
68    const NAME: &'static str;
69    const DEFAULT: Self::Type;
70    type Type;
71    type Converter: SettingTypeConverter<Type = Self::Type>;
72
73    fn lookup(
74        sm: &ScopeManager,
75        names: &ChainSettingNames,
76        scope_id: ScopeId,
77    ) -> Option<(Result<Self::Type, SettingConversionError>, Span)> {
78        sm.visit_value(scope_id, names[Self::INDEX], |v| {
79            let atom = v.atom()?;
80            let value = atom.value.read().unwrap();
81            if let FieldValue::Argument(arg) = &*value {
82                Some((
83                    Self::Converter::convert_to_type(arg.value.as_ref()),
84                    arg.span,
85                ))
86            } else {
87                Some((
88                    Self::Converter::convert_to_type(value.as_ref()),
89                    Span::Generated,
90                ))
91            }
92        })
93    }
94
95    fn assign_raw(
96        sm: &mut ScopeManager,
97        names: &ChainSettingNames,
98        scope_id: ScopeId,
99        value: FieldValue,
100    ) {
101        match sm.scopes[scope_id].values.entry(names[Self::INDEX]) {
102            std::collections::hash_map::Entry::Occupied(mut e) => {
103                match e.get_mut() {
104                    ScopeValue::Atom(v) => {
105                        *v.value.write().unwrap() = value;
106                    }
107                    other => {
108                        *other = ScopeValue::Atom(Arc::new(Atom::new(value)));
109                    }
110                }
111            }
112            std::collections::hash_map::Entry::Vacant(e) => {
113                e.insert(ScopeValue::Atom(Arc::new(Atom::new(value))));
114            }
115        }
116    }
117
118    fn assign(
119        sm: &mut ScopeManager,
120        chain_names: &ChainSettingNames,
121        scope_id: ScopeId,
122        value: Self::Type,
123        span: Span,
124    ) -> Result<(), SettingConversionError> {
125        let value = FieldValue::Argument(Box::new(Argument {
126            value: Self::Converter::convert_from_type(value)?,
127            span,
128            source_scope: scope_id,
129            meta_info: None,
130        }));
131        Self::assign_raw(sm, chain_names, scope_id, value);
132        Ok(())
133    }
134}
135
136impl SettingConversionError {
137    pub fn new(message: String) -> Self {
138        Self { message }
139    }
140}
141
142impl Default for RationalsPrintMode {
143    fn default() -> Self {
144        RationalsPrintMode::Cutoff(RARTIONAL_DECIMALS_DEFAULT_CUTOFF)
145    }
146}
147
148pub struct SettingConverterUsize<
149    S: ChainSetting,
150    const ALLOW_ZERO: bool = true,
151>(PhantomData<S>);
152
153impl<S: ChainSetting, const ALLOW_ZERO: bool> SettingTypeConverter
154    for SettingConverterUsize<S, ALLOW_ZERO>
155{
156    type Type = usize;
157    fn convert_to_type(
158        v: FieldValueRef,
159    ) -> Result<usize, SettingConversionError> {
160        let FieldValueRef::Int(&value) = v else {
161            return Err(SettingConversionError::new(format!(
162                "value for setting %{} must be an integer",
163                S::NAME
164            )));
165        };
166        if value == 0 && !ALLOW_ZERO {
167            return Err(SettingConversionError::new(format!(
168                "value for setting %{} cannot be zero",
169                S::NAME
170            )));
171        }
172        match usize::try_from(value) {
173            Ok(v) => Ok(v),
174            Err(_) => Err(SettingConversionError::new(format!(
175                "value for setting %{} must be a positive integer below 2^{}",
176                S::NAME,
177                usize::BITS
178            ))),
179        }
180    }
181
182    fn convert_from_type(
183        value: usize,
184    ) -> Result<FieldValue, SettingConversionError> {
185        if value == 0 && !ALLOW_ZERO {
186            return Err(SettingConversionError::new(format!(
187                "value for setting %{} cannot be zero",
188                S::NAME,
189            )));
190        }
191        match i64::try_from(value) {
192            Ok(v) => Ok(FieldValue::Int(v)),
193            Err(_) => Err(SettingConversionError::new(format!(
194                "value for setting %{} must be below 2^{}",
195                S::NAME,
196                usize::BITS
197            ))),
198        }
199    }
200
201    fn convert_to_type_from_maybe_text(
202        value: MaybeTextRef,
203    ) -> Result<Self::Type, SettingConversionError> {
204        let v = parse_int_with_units_from_bytes(value.as_bytes()).map_err(
205            |e| {
206                SettingConversionError::new(format!(
207                    "value for setting %{} must be an integer: {e}",
208                    S::NAME
209                ))
210            },
211        )?;
212        if v == 0 && !ALLOW_ZERO {
213            return Err(SettingConversionError::new(format!(
214                "value for setting %{} cannot be zero",
215                S::NAME
216            )));
217        }
218        Ok(v)
219    }
220}
221
222pub struct SettingConverterBool<S: ChainSetting>(PhantomData<S>);
223impl<S: ChainSetting> SettingTypeConverter for SettingConverterBool<S> {
224    type Type = bool;
225    fn convert_to_type(
226        value: FieldValueRef,
227    ) -> Result<bool, SettingConversionError> {
228        match value {
229            FieldValueRef::Undefined | FieldValueRef::Null => Ok(false),
230            FieldValueRef::Int(v) => Ok(*v == 0),
231            FieldValueRef::BigInt(v) => Ok(v.is_zero()),
232            FieldValueRef::Float(v) => Ok(*v == 0.0),
233            FieldValueRef::BigRational(v) => Ok(v.is_zero()),
234            FieldValueRef::Text(_) | FieldValueRef::Bytes(_) => {
235                // TODO: get rid of this once we have a bool type?
236                Self::convert_to_type_from_maybe_text(
237                    value.as_maybe_text_ref().unwrap(),
238                )
239            }
240            FieldValueRef::Argument(v) => {
241                Self::convert_to_type(v.value.as_ref())
242            }
243            FieldValueRef::Array(_)
244            | FieldValueRef::Object(_)
245            | FieldValueRef::Custom(_)
246            | FieldValueRef::Error(_)
247            | FieldValueRef::OpDecl(_)
248            | FieldValueRef::StreamValueId(_)
249            | FieldValueRef::FieldReference(_)
250            | FieldValueRef::SlicedFieldReference(_) => {
251                Err(SettingConversionError::new(format!(
252                    "setting %{} expects a boolean, got type {}",
253                    S::NAME,
254                    value.repr().kind().to_str()
255                )))
256            }
257        }
258    }
259    fn convert_to_type_from_maybe_text(
260        value: MaybeTextRef,
261    ) -> Result<Self::Type, SettingConversionError> {
262        if let Some(v) = try_parse_bool(value.as_bytes()) {
263            return Ok(v);
264        };
265        Err(SettingConversionError::new(format!(
266            "setting %{} expects a boolean, got '{}'",
267            S::NAME,
268            value.as_bytes().to_str_lossy()
269        )))
270    }
271
272    fn convert_from_type(
273        value: bool,
274    ) -> Result<FieldValue, SettingConversionError> {
275        Ok(FieldValue::Text(
276            if value { "true" } else { "false" }.to_string(),
277        ))
278    }
279}
280
281pub struct SettingConverterBufferingMode<S: ChainSetting>(PhantomData<S>);
282impl<S: ChainSetting> SettingTypeConverter
283    for SettingConverterBufferingMode<S>
284{
285    type Type = BufferingMode;
286
287    fn convert_to_type_from_maybe_text(
288        value: MaybeTextRef,
289    ) -> Result<Self::Type, SettingConversionError> {
290        Self::convert_to_type(FieldValueRef::from(value))
291    }
292
293    fn convert_to_type(
294        value: FieldValueRef,
295    ) -> Result<BufferingMode, SettingConversionError> {
296        let FieldValueRef::Text(value) = value else {
297            return Err(SettingConversionError::new(format!(
298                "invalid line buffering condition for %{}, got type `{}`",
299                S::NAME,
300                value.kind().to_str()
301            )));
302        };
303        match value {
304            "never" => Ok(BufferingMode::BlockBuffer),
305            "always" => Ok(BufferingMode::LineBuffer),
306            "stdin" => Ok(BufferingMode::LineBufferStdin),
307            "tty" => Ok(BufferingMode::LineBufferIfTTY),
308            "stdin-if-tty" => Ok(BufferingMode::LineBufferStdinIfTTY),
309            other => Err(SettingConversionError::new(format!(
310                "invalid line buffering condition for %{}, got '{}'",
311                S::NAME,
312                other,
313            ))),
314        }
315    }
316
317    fn convert_from_type(
318        value: BufferingMode,
319    ) -> Result<FieldValue, SettingConversionError> {
320        let v = match value {
321            BufferingMode::BlockBuffer => "never",
322            BufferingMode::LineBuffer => "always",
323            BufferingMode::LineBufferStdin => "stdin",
324            BufferingMode::LineBufferIfTTY => "tty",
325            BufferingMode::LineBufferStdinIfTTY => "stdin-if-tty",
326        };
327        Ok(FieldValue::Text(v.to_string()))
328    }
329}
330
331pub struct SettingConverterRationalsPrintMode<S: ChainSetting>(PhantomData<S>);
332impl<S: ChainSetting> SettingTypeConverter
333    for SettingConverterRationalsPrintMode<S>
334{
335    type Type = RationalsPrintMode;
336
337    fn convert_to_type_from_maybe_text(
338        value: MaybeTextRef,
339    ) -> Result<Self::Type, SettingConversionError> {
340        Self::convert_to_type(FieldValueRef::from(value))
341    }
342    fn convert_to_type(
343        value: FieldValueRef,
344    ) -> Result<RationalsPrintMode, SettingConversionError> {
345        let FieldValueRef::Text(value) = value else {
346            return Err(SettingConversionError::new(format!(
347                "invalid rational printing variant for %{}, got type `{}`",
348                S::NAME,
349                value.kind().to_str()
350            )));
351        };
352        match value {
353            "raw" => return Ok(RationalsPrintMode::Raw),
354            "dynamic" => return Ok(RationalsPrintMode::Dynamic),
355            "cutoff" => {
356                return Ok(RationalsPrintMode::Cutoff(
357                    RARTIONAL_DECIMALS_DEFAULT_CUTOFF,
358                ))
359            }
360            _ => (),
361        }
362        if let Some(amount) = value.strip_prefix("cutoff-") {
363            if let Ok(n) = amount.parse::<u32>() {
364                return Ok(RationalsPrintMode::Cutoff(n));
365            }
366            return Err(SettingConversionError::new(format!(
367                "cutoff amount for %{} must be a valid integer, got '{}'",
368                S::NAME,
369                amount,
370            )));
371        }
372        Err(SettingConversionError::new(format!(
373            "invalid rational printing variant for %{}, got '{}'",
374            S::NAME,
375            value,
376        )))
377    }
378
379    fn convert_from_type(
380        value: RationalsPrintMode,
381    ) -> Result<FieldValue, SettingConversionError> {
382        let v = match value {
383            RationalsPrintMode::Cutoff(v) => {
384                if v == RARTIONAL_DECIMALS_DEFAULT_CUTOFF {
385                    "cutoff".to_string()
386                } else {
387                    format!("cutoff-{v}")
388                }
389            }
390            RationalsPrintMode::Raw => "raw".to_string(),
391            RationalsPrintMode::Dynamic => "dynamic".to_string(),
392        };
393        Ok(FieldValue::Text(v))
394    }
395}
396
397pub struct SettingConverterPath<S: ChainSetting>(PhantomData<S>);
398impl<S: ChainSetting> SettingTypeConverter for SettingConverterPath<S> {
399    type Type = PathBuf;
400
401    fn convert_to_type_from_maybe_text(
402        value: MaybeTextRef,
403    ) -> Result<Self::Type, SettingConversionError> {
404        Self::convert_to_type(FieldValueRef::from(value))
405    }
406
407    fn convert_to_type(
408        value: FieldValueRef,
409    ) -> Result<PathBuf, SettingConversionError> {
410        match value {
411            FieldValueRef::Text(v) => Ok(PathBuf::from(v)),
412            FieldValueRef::Bytes(v) => {
413                Ok(PathBuf::from(OsString::from_vec(v.to_owned())))
414            }
415             FieldValueRef::Undefined | FieldValueRef::Null  |
416            FieldValueRef::Int(_)
417            | FieldValueRef::BigInt(_)
418            | FieldValueRef::Float(_)
419            | FieldValueRef::BigRational(_)
420            | FieldValueRef::Array(_)
421            | FieldValueRef::Object(_)
422            | FieldValueRef::Custom(_)
423            | FieldValueRef::Error(_)
424            | FieldValueRef::OpDecl(_)
425            | FieldValueRef::Argument(_)
426            | FieldValueRef::StreamValueId(_)
427            | FieldValueRef::FieldReference(_)
428            | FieldValueRef::SlicedFieldReference(_) => {
429                Err(SettingConversionError::new(format!(
430                    "invalid value for setting %{}, expected string, got type `{}`",
431                    S::NAME,
432                    value.kind().to_str()
433                )))
434            }
435        }
436    }
437
438    fn convert_from_type(
439        value: PathBuf,
440    ) -> Result<FieldValue, SettingConversionError> {
441        match value.into_os_string().into_string() {
442            Ok(v) => Ok(FieldValue::Text(v)),
443            Err(v) => Ok(FieldValue::Bytes(v.into_encoded_bytes())),
444        }
445    }
446}
447
448pub struct SettingConverterOptional<
449    SC: SettingTypeConverter,
450    const EMPTY_STRING_AS_NONE: bool = false,
451>(PhantomData<SC>);
452
453impl<SC: SettingTypeConverter, const EMPTY_STRING_AS_NONE: bool>
454    SettingTypeConverter
455    for SettingConverterOptional<SC, EMPTY_STRING_AS_NONE>
456{
457    type Type = Option<SC::Type>;
458
459    fn convert_to_type_from_maybe_text(
460        value: MaybeTextRef,
461    ) -> Result<Self::Type, SettingConversionError> {
462        if EMPTY_STRING_AS_NONE && value.is_empty() {
463            return Ok(None);
464        }
465        Ok(Some(SC::convert_to_type_from_maybe_text(value)?))
466    }
467
468    fn convert_to_type(
469        value: FieldValueRef,
470    ) -> Result<Option<SC::Type>, SettingConversionError> {
471        match value {
472            FieldValueRef::Null | FieldValueRef::Undefined => Ok(None),
473            FieldValueRef::Text(v) if v.is_empty() && EMPTY_STRING_AS_NONE => {
474                Ok(None)
475            }
476            FieldValueRef::Bytes(v)
477                if v.is_empty() && EMPTY_STRING_AS_NONE =>
478            {
479                Ok(None)
480            }
481            _ => Ok(Some(SC::convert_to_type(value)?)),
482        }
483    }
484
485    fn convert_from_type(
486        value: Option<SC::Type>,
487    ) -> Result<FieldValue, SettingConversionError> {
488        match value {
489            Some(v) => SC::convert_from_type(v),
490            None => Ok(FieldValue::Null),
491        }
492    }
493}
494
495pub struct SettingBatchSize;
496impl ChainSetting for SettingBatchSize {
497    type Type = usize;
498    const NAME: &'static str = "bs";
499    const DEFAULT: usize = 1024;
500    type Converter = SettingConverterUsize<Self, false>;
501}
502
503pub struct SettingStreamSizeThreshold;
504impl ChainSetting for SettingStreamSizeThreshold {
505    type Type = usize;
506    const NAME: &'static str = "sst";
507    const DEFAULT: usize = 1024;
508    type Converter = SettingConverterUsize<Self, false>;
509}
510
511pub struct SettingStreamBufferSize;
512impl ChainSetting for SettingStreamBufferSize {
513    type Type = usize;
514    const NAME: &'static str = "sbs";
515    const DEFAULT: usize = 8192;
516    type Converter = SettingConverterUsize<Self, false>;
517}
518
519pub struct SettingRationalsPrintMode;
520impl ChainSetting for SettingRationalsPrintMode {
521    type Type = RationalsPrintMode;
522    const NAME: &'static str = "rpm";
523    const DEFAULT: RationalsPrintMode =
524        RationalsPrintMode::Cutoff(RARTIONAL_DECIMALS_DEFAULT_CUTOFF);
525    type Converter = SettingConverterRationalsPrintMode<Self>;
526}
527
528pub struct SettingUseFloatingPointMath;
529impl ChainSetting for SettingUseFloatingPointMath {
530    type Type = bool;
531    const NAME: &'static str = "fpm";
532    const DEFAULT: bool = false;
533    type Converter = SettingConverterBool<Self>;
534}
535
536pub struct SettingBufferingMode;
537impl ChainSetting for SettingBufferingMode {
538    type Type = BufferingMode;
539    const NAME: &'static str = "lb";
540    const DEFAULT: BufferingMode = BufferingMode::LineBufferIfTTY;
541    type Converter = SettingConverterBufferingMode<Self>;
542}
543
544pub struct SettingDebugLog;
545impl ChainSetting for SettingDebugLog {
546    type Type = Option<PathBuf>;
547    const NAME: &'static str = "debug_log";
548    const DEFAULT: Option<PathBuf> = None;
549    type Converter = SettingConverterOptional<SettingConverterPath<Self>>;
550}
551
552pub struct SettingDebugLogNoApply;
553impl ChainSetting for SettingDebugLogNoApply {
554    type Type = bool;
555    const NAME: &'static str = "debug_log_no_apply";
556    const DEFAULT: bool = false;
557    type Converter = SettingConverterBool<Self>;
558}
559
560pub struct SettingDebugLogStepMin;
561impl ChainSetting for SettingDebugLogStepMin {
562    type Type = usize;
563    const NAME: &'static str = "debug_log_step_min";
564    const DEFAULT: usize = 0;
565    type Converter = SettingConverterUsize<Self>;
566}
567
568pub struct SettingDebugBreakOnStep;
569impl ChainSetting for SettingDebugBreakOnStep {
570    type Type = Option<usize>;
571    const NAME: &'static str = "debug_break_on_step";
572    const DEFAULT: Option<usize> = None;
573    type Converter = SettingConverterOptional<SettingConverterUsize<Self>>;
574}
575
576pub struct SettingMaxThreads;
577impl ChainSetting for SettingMaxThreads {
578    type Type = usize;
579    const NAME: &'static str = "j";
580    const DEFAULT: usize = 0;
581    type Converter = SettingConverterUsize<Self, true>;
582}
583
584pub struct SettingActionListCleanupFrequency;
585impl ChainSetting for SettingActionListCleanupFrequency {
586    type Type = usize;
587    const NAME: &'static str = "alcf";
588    const DEFAULT: usize = 64;
589    type Converter = SettingConverterUsize<Self, true>;
590}
591
592typelist! {
593    pub mod chain_settings_list: (ChainSetting) = [
594        SettingBatchSize,
595        SettingStreamSizeThreshold,
596        SettingStreamBufferSize,
597        SettingRationalsPrintMode,
598        SettingUseFloatingPointMath,
599        SettingBufferingMode,
600        SettingDebugLog,
601        SettingDebugLogNoApply,
602        SettingDebugLogStepMin,
603        SettingDebugBreakOnStep,
604        SettingMaxThreads,
605        SettingActionListCleanupFrequency
606    ]{}
607}