Skip to main content

nickel_lang_core/
program.rs

1//! Program handling, from file reading to evaluation.
2//!
3//! A program is Nickel source code loaded from an input. This module offers an interface to load a
4//! program source, parse it, evaluate it and report errors.
5//!
6//! # Standard library
7//!
8//! Some essential functions required for evaluation, such as builtin contracts, are written in
9//! pure Nickel. Standard library files must be record literals:
10//!
11//! ```text
12//! {
13//!     val1 = ...
14//!     val2 = ...
15//! }
16//! ```
17//!
18//! These .ncl file are not actually distributed as files, instead they are embedded, as plain
19//! text, in the Nickel executable. The embedding is done by way of the [crate::stdlib], which
20//! exposes the standard library files as strings. The embedded strings are then parsed by the
21//! functions in [`crate::cache`] (see [`crate::cache::CacheHub::mk_eval_env`]).
22//! Each such value is added to the initial environment before the evaluation of the program.
23use crate::{
24    ast::{AstAlloc, compat::ToMainline},
25    cache::*,
26    closurize::Closurize as _,
27    error::{
28        Error, EvalError, EvalErrorKind, IOError, ParseError, ParseErrors, Reporter,
29        warning::Warning,
30    },
31    eval::{
32        Closure, VirtualMachine, VmContext,
33        cache::Cache as EvalCache,
34        value::{Container, NickelValue, ValueContent},
35    },
36    files::{FileId, Files},
37    identifier::LocIdent,
38    label::Label,
39    metrics::{increment, measure_runtime},
40    package::PackageMap,
41    position::{PosIdx, PosTable, RawSpan},
42    term::{
43        BinaryOp, Import, MergePriority, RuntimeContract, Term,
44        make::{self as mk_term, builder},
45        record::Field,
46    },
47    typecheck::TypecheckMode,
48};
49
50use std::{
51    ffi::OsString,
52    fmt,
53    io::{self, Read, Write},
54    path::PathBuf,
55    result::Result,
56};
57
58/// A path of fields, that is a list, locating this field from the root of the configuration.
59#[derive(Clone, Default, PartialEq, Eq, Debug, Hash)]
60pub struct FieldPath(pub Vec<LocIdent>);
61
62impl FieldPath {
63    pub fn new() -> Self {
64        Self::default()
65    }
66
67    /// Parse a string as a query path. A query path is a sequence of dot-separated identifiers.
68    /// Identifiers can be enclosed by double quotes when they contain characters that aren't
69    /// allowed inside bare identifiers. The accepted grammar is the same as a sequence of record
70    /// accesses in Nickel, although string interpolation is forbidden.
71    ///
72    /// # Post-conditions
73    ///
74    /// If this function succeeds and returns `Ok(field_path)`, then `field_path.0` is non empty.
75    /// Indeed, there's no such thing as a valid empty field path (at least from the parsing point
76    /// of view): if `input` is empty, or consists only of spaces, `parse` returns a parse error.
77    pub fn parse(caches: &mut CacheHub, input: String) -> Result<Self, ParseError> {
78        use crate::parser::{
79            ErrorTolerantParserCompat, grammar::StaticFieldPathParser, lexer::Lexer,
80        };
81
82        let input_id = caches.replace_string(SourcePath::Query, input);
83        let s = caches.sources.source(input_id);
84
85        let parser = StaticFieldPathParser::new();
86        let field_path = parser
87            // This doesn't use the position table at all, since `LocIdent` currently stores a
88            // TermPos directly
89            .parse_strict_compat(&mut PosTable::new(), input_id, Lexer::new(s))
90            // We just need to report an error here
91            .map_err(|mut errs| {
92                errs.errors.pop().expect(
93                    "because parsing of the query path failed, the error \
94                    list must be non-empty, put .pop() failed",
95                )
96            })?;
97
98        Ok(FieldPath(field_path))
99    }
100
101    /// As [`Self::parse`], but accepts an `Option` to accomodate for the absence of path. If the
102    /// input is `None`, `Ok(FieldPath::default())` is returned (that is, an empty field path).
103    pub fn parse_opt(cache: &mut CacheHub, input: Option<String>) -> Result<Self, ParseError> {
104        Ok(input
105            .map(|path| Self::parse(cache, path))
106            .transpose()?
107            .unwrap_or_default())
108    }
109}
110
111impl fmt::Display for FieldPath {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        use crate::pretty::ident_quoted;
114
115        write!(
116            f,
117            "{}",
118            self.0
119                .iter()
120                .map(ident_quoted)
121                .collect::<Vec<_>>()
122                .join(".")
123        )
124    }
125}
126
127/// Several CLI commands accept additional overrides specified directly on the command line. They
128/// are represented by this structure.
129#[derive(Clone)]
130pub struct FieldOverride {
131    /// The field path identifying the (potentially nested) field to override.
132    pub path: FieldPath,
133    /// The overriding value.
134    pub value: String,
135    /// The priority associated with this override.
136    pub priority: MergePriority,
137}
138
139impl FieldOverride {
140    /// Parse an assignment `path.to.field=value` to a field override, with the priority given as a
141    /// separate argument.
142    ///
143    /// Internally, the parser entirely parses the `value` part to a [NickelValue] (have it accept
144    /// anything after the equal sign is in fact harder than actually parsing it), but what we need
145    /// at this point is just a string. Thus, `parse` uses the span to extract back the `value`
146    /// part of the input string.
147    ///
148    /// Theoretically, this means we parse two times the same string (the value part of an
149    /// assignment). In practice, we expect this cost to be completely negligible.
150    ///
151    /// # Selectors
152    ///
153    /// The value part accepts special selectors starting with a leading `@` that aren't part of
154    /// the core Nickel syntax. This list is subject to extensions.
155    ///
156    /// - `foo.bar=@env:<var>` will extract a string value from the environment variable `<var>`
157    ///   and put it in `foo.bar`.
158    pub fn parse(
159        cache: &mut CacheHub,
160        assignment: String,
161        priority: MergePriority,
162    ) -> Result<Self, ParseError> {
163        use crate::parser::{
164            ErrorTolerantParserCompat,
165            grammar::{CliFieldAssignmentParser, StaticFieldPathParser},
166            lexer::{Lexer, NormalToken, Token},
167        };
168
169        let input_id = cache.replace_string(SourcePath::CliFieldAssignment, assignment);
170        let s = cache.sources.source(input_id);
171
172        // We first look for a possible sigil `@` immediately following the (first not-in-a-string)
173        // equal sign. This can't be valid Nickel, so we always consider that this is a special CLI
174        // expression like `@env:VAR`.
175        let mut lexer = Lexer::new(s);
176        let equal_sign =
177            lexer.find(|t| matches!(t, Ok((_, Token::Normal(NormalToken::Equals), _))));
178        let after_equal = lexer.next();
179
180        match (equal_sign, after_equal) {
181            (
182                Some(Ok((start_eq, _, end_eq))),
183                Some(Ok((start_at, Token::Normal(NormalToken::At), _))),
184            ) if end_eq == start_at => {
185                let path = StaticFieldPathParser::new()
186                    // we don't use the position table for pure field paths
187                    .parse_strict_compat(&mut PosTable::new(), input_id, Lexer::new(&s[..start_eq]))
188                    // We just need to report one error here
189                    .map_err(|mut errs| {
190                        errs.errors.pop().expect(
191                            "because parsing of the field assignment failed, the error \
192                        list must be non-empty, put .pop() failed",
193                        )
194                    })?;
195                let value = s[start_at..].to_owned();
196
197                Ok(FieldOverride {
198                    path: FieldPath(path),
199                    value: value.to_owned(),
200                    priority,
201                })
202            }
203            _ => {
204                let (path, _, span_value) = CliFieldAssignmentParser::new()
205                    // once again, we ditch the value, so no PosIdx leaks outside of
206                    // `parse_strict_compat` and we can thus ignore the position table entirely
207                    .parse_strict_compat(&mut PosTable::new(), input_id, Lexer::new(s))
208                    // We just need to report one error here
209                    .map_err(|mut errs| {
210                        errs.errors.pop().expect(
211                            "because parsing of the field assignment failed, the error \
212                        list must be non-empty, put .pop() failed",
213                        )
214                    })?;
215
216                let value = cache.files().source_slice(span_value);
217
218                Ok(FieldOverride {
219                    path: FieldPath(path),
220                    value: value.to_owned(),
221                    priority,
222                })
223            }
224        }
225    }
226}
227
228/// Additional contracts to apply to the main program.
229pub enum ProgramContract {
230    /// Contract specified directly as a term. Typically used for contracts generated or at least
231    /// wrapped programmatically.
232    Term(RuntimeContract),
233    /// Contract specified as a source. They will be parsed and typechecked alongside the rest of
234    /// the program. Typically coming from the CLI `--apply-contract` argument.
235    Source(FileId),
236}
237
238/// A Nickel program.
239///
240/// Manage a file database, which stores the original source code of the program and eventually the
241/// code of imported expressions, and a dictionary which stores corresponding parsed terms.
242pub struct Program<EC: EvalCache> {
243    /// The id of the program source in the file database.
244    main_id: FileId,
245    /// The context/persistent state of the Nickel virtual machine.
246    vm_ctxt: VmContext<CacheHub, EC>,
247    /// A list of [`FieldOverride`]s. During [`prepare_eval`], each
248    /// override is imported in a separate in-memory source, for complete isolation (this way,
249    /// overrides can't accidentally or intentionally capture other fields of the configuration).
250    /// A stub record is then built, which has all fields defined by `overrides`, and values are
251    /// an import referring to the corresponding isolated value. This stub is finally merged with
252    /// the current program before being evaluated for import.
253    overrides: Vec<FieldOverride>,
254    /// A specific field to act on. It is empty by default, which means that the whole program will
255    /// be evaluated, but it can be set by the user (for example by the `--field` argument of the
256    /// CLI) to evaluate only a specific field.
257    pub field: FieldPath,
258    /// Extra contracts to apply to the main program source. Note that the contract is applied to
259    /// the whole value before fields are extracted.
260    pub contracts: Vec<ProgramContract>,
261}
262
263/// The Possible Input Sources, anything that a Nickel program can be created from
264pub enum Input<T, S> {
265    /// A filepath
266    Path(S),
267    /// The source is anything that can be Read from, the second argument is the name the source should have in the cache.
268    Source(T, S, InputFormat),
269}
270
271impl<EC: EvalCache> Program<EC> {
272    /// Create a program by reading it from the standard input.
273    pub fn new_from_stdin(
274        stdin_format: InputFormat,
275        trace: impl Write + 'static,
276        reporter: impl Reporter<(Warning, Files)> + 'static,
277    ) -> std::io::Result<Self> {
278        Program::new_from_source_with_format(io::stdin(), "<stdin>", stdin_format, trace, reporter)
279    }
280
281    /// Contructor that abstracts over the Input type (file, string, etc.). Used by
282    /// the other constructors. Published for those that need abstraction over the kind of Input.
283    ///
284    /// The format of the input is Nickel by default. However, for [Input::Path]s, the format is
285    /// determined from the file extension. This is useful to merge Nickel and non-Nickel files, or
286    /// to apply extra contracts to non-Nickel configurations.
287    pub fn new_from_input<T, S>(
288        input: Input<T, S>,
289        trace: impl Write + 'static,
290        reporter: impl Reporter<(Warning, Files)> + 'static,
291    ) -> std::io::Result<Self>
292    where
293        T: Read,
294        S: Into<OsString>,
295    {
296        increment!("Program::new");
297        let mut cache = CacheHub::new();
298
299        let main_id = match input {
300            Input::Path(path) => {
301                let path = path.into();
302                let format = InputFormat::from_path(&path).unwrap_or_default();
303                cache.sources.add_file(path, format)?
304            }
305            Input::Source(source, name, format) => {
306                let path = PathBuf::from(name.into());
307                cache
308                    .sources
309                    .add_source(SourcePath::Path(path, format), source)?
310            }
311        };
312
313        Ok(Self {
314            main_id,
315            vm_ctxt: VmContext::new(cache, trace, reporter),
316            overrides: Vec::new(),
317            field: FieldPath::new(),
318            contracts: Vec::new(),
319        })
320    }
321
322    /// Constructor that abstracts over an iterator of Inputs (file, strings, etc). Published for
323    /// those that need abstraction over the kind of Input or want to mix multiple different kinds
324    /// of Input.
325    ///
326    /// The format of each input is Nickel by default. However, for [Input::Path]s, the format is
327    /// determined from the file extension. This is useful to merge Nickel and non-Nickel files, or
328    /// to apply extra contracts to non-Nickel configurations.
329    pub fn new_from_inputs<I, T, S>(
330        inputs: I,
331        trace: impl Write + 'static,
332        reporter: impl Reporter<(Warning, Files)> + 'static,
333    ) -> std::io::Result<Self>
334    where
335        I: IntoIterator<Item = Input<T, S>>,
336        T: Read,
337        S: Into<OsString>,
338    {
339        increment!("Program::new");
340        let mut cache = CacheHub::new();
341
342        let merge_term = inputs
343            .into_iter()
344            .map(|input| match input {
345                Input::Path(path) => {
346                    let path = path.into();
347                    let format = InputFormat::from_path(&path).unwrap_or_default();
348
349                    NickelValue::from(Term::Import(Import::Path { path, format }))
350                }
351                Input::Source(source, name, format) => {
352                    let name = name.into();
353                    let mut import_path = OsString::new();
354                    // See https://github.com/tweag/nickel/issues/2362 and the documentation of
355                    // IN_MEMORY_SOURCE_PATH_PREFIX
356                    import_path.push(IN_MEMORY_SOURCE_PATH_PREFIX);
357                    import_path.push(name.clone());
358
359                    cache
360                        .sources
361                        .add_source(SourcePath::Path(name.into(), format), source)
362                        .unwrap();
363                    NickelValue::from(Term::Import(Import::Path {
364                        path: import_path,
365                        format,
366                    }))
367                }
368            })
369            .reduce(|acc, f| mk_term::op2(BinaryOp::Merge(Label::default().into()), acc, f))
370            .unwrap();
371
372        let main_id = cache.sources.add_string(
373            SourcePath::Generated("main".into()),
374            format!("{merge_term}"),
375        );
376
377        Ok(Self {
378            main_id,
379            vm_ctxt: VmContext::new(cache, trace, reporter),
380            overrides: Vec::new(),
381            field: FieldPath::new(),
382            contracts: Vec::new(),
383        })
384    }
385
386    /// Create program from possibly multiple files. Each input `path` is
387    /// turned into a [`Term::Import`] and the main program will be the
388    /// [`BinaryOp::Merge`] of all the inputs.
389    pub fn new_from_files<I, P>(
390        paths: I,
391        trace: impl Write + 'static,
392        reporter: impl Reporter<(Warning, Files)> + 'static,
393    ) -> std::io::Result<Self>
394    where
395        I: IntoIterator<Item = P>,
396        P: Into<OsString>,
397    {
398        // The File type parameter is a dummy type and not used.
399        // It just needed to be something that implements Read, and File seemed fitting.
400        Self::new_from_inputs(
401            paths.into_iter().map(Input::<std::fs::File, _>::Path),
402            trace,
403            reporter,
404        )
405    }
406
407    pub fn new_from_file(
408        path: impl Into<OsString>,
409        trace: impl Write + 'static,
410        reporter: impl Reporter<(Warning, Files)> + 'static,
411    ) -> std::io::Result<Self> {
412        // The File type parameter is a dummy type and not used.
413        // It just needed to be something that implements Read, and File seemed fitting.
414        Self::new_from_input(Input::<std::fs::File, _>::Path(path), trace, reporter)
415    }
416
417    /// Create a program by reading it from a generic source.
418    pub fn new_from_source<T, S>(
419        source: T,
420        source_name: S,
421        trace: impl Write + 'static,
422        reporter: impl Reporter<(Warning, Files)> + 'static,
423    ) -> std::io::Result<Self>
424    where
425        T: Read,
426        S: Into<OsString>,
427    {
428        Self::new_from_input(
429            Input::Source(source, source_name, InputFormat::Nickel),
430            trace,
431            reporter,
432        )
433    }
434
435    /// Create a program by reading it from a generic source. The format of the source may be
436    /// specified to be something other than Nickel.
437    pub fn new_from_source_with_format<T, S>(
438        source: T,
439        source_name: S,
440        source_format: InputFormat,
441        trace: impl Write + 'static,
442        reporter: impl Reporter<(Warning, Files)> + 'static,
443    ) -> std::io::Result<Self>
444    where
445        T: Read,
446        S: Into<OsString>,
447    {
448        Self::new_from_input(
449            Input::Source(source, source_name, source_format),
450            trace,
451            reporter,
452        )
453    }
454
455    /// Create program from possibly multiple sources. The main program will be
456    /// the [`BinaryOp::Merge`] of all the inputs.
457    pub fn new_from_sources<I, T, S>(
458        sources: I,
459        trace: impl Write + 'static,
460        reporter: impl Reporter<(Warning, Files)> + 'static,
461    ) -> std::io::Result<Self>
462    where
463        I: IntoIterator<Item = (T, S)>,
464        T: Read,
465        S: Into<OsString>,
466    {
467        let inputs = sources
468            .into_iter()
469            .map(|(s, n)| Input::Source(s, n, InputFormat::Nickel));
470        Self::new_from_inputs(inputs, trace, reporter)
471    }
472
473    /// Parse an assignment of the form `path.to_field=value` as an override, with the provided
474    /// merge priority. Assignments are typically provided by the user on the command line, as part
475    /// of the customize mode.
476    ///
477    /// This method simply calls [FieldOverride::parse] with the [crate::cache::CacheHub] of the
478    /// current program.
479    pub fn parse_override(
480        &mut self,
481        assignment: String,
482        priority: MergePriority,
483    ) -> Result<FieldOverride, ParseError> {
484        FieldOverride::parse(&mut self.vm_ctxt.import_resolver, assignment, priority)
485    }
486
487    /// Parse a dot-separated field path of the form `path.to.field`.
488    ///
489    /// This method simply calls [FieldPath::parse] with the [crate::cache::CacheHub] of the current
490    /// program.
491    pub fn parse_field_path(&mut self, path: String) -> Result<FieldPath, ParseError> {
492        FieldPath::parse(&mut self.vm_ctxt.import_resolver, path)
493    }
494
495    pub fn add_overrides(&mut self, overrides: impl IntoIterator<Item = FieldOverride>) {
496        self.overrides.extend(overrides);
497    }
498
499    /// Adds a contract to be applied to the final program.
500    pub fn add_contract(&mut self, contract: ProgramContract) {
501        self.contracts.push(contract);
502    }
503
504    /// Adds a list of contracts to be applied to the final program, specified as file paths. Those
505    /// contracts will be parsed, typechecked and further processed together with the rest of the
506    /// program.
507    pub fn add_contract_paths<P>(
508        &mut self,
509        contract_files: impl IntoIterator<Item = P>,
510    ) -> Result<(), Error>
511    where
512        OsString: From<P>,
513    {
514        let prog_contracts: Result<Vec<_>, _> = contract_files
515            .into_iter()
516            .map(|file| -> Result<_, Error> {
517                let file: OsString = file.into();
518                let file_str = file.to_string_lossy().into_owned();
519
520                let file_id = self
521                    .vm_ctxt
522                    .import_resolver
523                    .sources
524                    .add_file(file, InputFormat::Nickel)
525                    .map_err(|err| {
526                        Error::IOError(IOError(format!(
527                            "when opening contract file `{}`: {}",
528                            &file_str, err
529                        )))
530                    })?;
531
532                Ok(ProgramContract::Source(file_id))
533            })
534            .collect();
535
536        self.contracts.extend(prog_contracts?);
537        Ok(())
538    }
539
540    /// Adds import paths to the end of the list.
541    pub fn add_import_paths<P>(&mut self, paths: impl Iterator<Item = P>)
542    where
543        PathBuf: From<P>,
544    {
545        self.vm_ctxt.import_resolver.sources.add_import_paths(paths);
546    }
547
548    pub fn set_package_map(&mut self, map: PackageMap) {
549        self.vm_ctxt.import_resolver.sources.set_package_map(map)
550    }
551
552    /// Only parse the program (and any additional attached contracts), don't typecheck or
553    /// evaluate. Returns the [`NickelValue`] AST
554    pub fn parse(&mut self) -> Result<NickelValue, Error> {
555        self.vm_ctxt
556            .import_resolver
557            .parse_to_ast(self.main_id)
558            .map_err(Error::ParseErrors)?;
559
560        for source in self.contracts.iter() {
561            match source {
562                ProgramContract::Term(_) => (),
563                ProgramContract::Source(file_id) => {
564                    self.vm_ctxt
565                        .import_resolver
566                        .parse_to_ast(*file_id)
567                        .map_err(Error::ParseErrors)?;
568                }
569            }
570        }
571
572        Ok(self
573            .vm_ctxt
574            .import_resolver
575            .terms
576            .get_owned(self.main_id)
577            .expect("File parsed and then immediately accessed doesn't exist"))
578    }
579
580    /// Applies a custom transformation to the main term, assuming that it has been parsed but not
581    /// yet transformed.
582    ///
583    /// If multiple invocations of `custom_transform` are needed, each subsequent invocation must supply
584    /// `transform_id` with with a number higher than that of all previous invocations.
585    pub fn custom_transform<E, F>(
586        &mut self,
587        transform_id: usize,
588        mut transform: F,
589    ) -> Result<(), TermCacheError<E>>
590    where
591        F: FnMut(&mut CacheHub, &mut PosTable, NickelValue) -> Result<NickelValue, E>,
592    {
593        self.vm_ctxt.import_resolver.custom_transform(
594            self.main_id,
595            transform_id,
596            &mut |cache, value| transform(cache, &mut self.vm_ctxt.pos_table, value),
597        )
598    }
599
600    /// Retrieve the parsed term, typecheck it, and generate a fresh initial environment. If
601    /// `self.overrides` isn't empty, generate the required merge parts and return a merge
602    /// expression including the overrides. Extract the field corresponding to `self.field`, if not
603    /// empty.
604    fn prepare_eval(&mut self) -> Result<Closure, Error> {
605        self.prepare_eval_impl(false)
606    }
607
608    /// Retrieve the parsed term, typecheck it, and generate a fresh initial environment. If
609    /// `self.overrides` isn't empty, generate the required merge parts and return a merge
610    /// expression including the overrides. DO NOT extract the field corresponding to `self.field`,
611    /// because query does it itself. Otherwise, we would lose the associated metadata.
612    fn prepare_query(&mut self) -> Result<Closure, Error> {
613        self.prepare_eval_impl(true)
614    }
615
616    fn prepare_eval_impl(&mut self, for_query: bool) -> Result<Closure, Error> {
617        // If there are no overrides, we avoid the boilerplate of creating an empty record and
618        // merging it with the current program
619        let mut prepared_body = if self.overrides.is_empty() {
620            self.vm_ctxt.prepare_eval(self.main_id)?
621        } else {
622            let mut record = builder::Record::new();
623
624            for ovd in self.overrides.iter().cloned() {
625                let value_file_id = self
626                    .vm_ctxt
627                    .import_resolver
628                    .sources
629                    .add_string(SourcePath::Override(ovd.path.clone()), ovd.value);
630                let value_unparsed = self.vm_ctxt.import_resolver.sources.source(value_file_id);
631
632                if let Some('@') = value_unparsed.chars().next() {
633                    // We parse the sigil expression, which has the general form `@xxx/yyy:value` where
634                    // `/yyy` is optional.
635                    let value_sep = value_unparsed.find(':').ok_or_else(|| {
636                        ParseError::SigilExprMissingColon(RawSpan::from_range(
637                            value_file_id,
638                            0..value_unparsed.len(),
639                        ))
640                    })?;
641                    let attr_sep = value_unparsed[..value_sep].find('/');
642
643                    let attr = attr_sep.map(|attr_sep| &value_unparsed[attr_sep + 1..value_sep]);
644                    let selector = &value_unparsed[1..attr_sep.unwrap_or(value_sep)];
645                    let value = value_unparsed[value_sep + 1..].to_owned();
646
647                    match (selector, attr) {
648                        ("env", None) => match std::env::var(&value) {
649                            Ok(env_var) => {
650                                record = record.path(ovd.path.0).priority(ovd.priority).value(
651                                    NickelValue::string(
652                                        env_var,
653                                        self.vm_ctxt.pos_table.push(
654                                            RawSpan::from_range(
655                                                value_file_id,
656                                                value_sep + 1..value_unparsed.len(),
657                                            )
658                                            .into(),
659                                        ),
660                                    ),
661                                );
662
663                                Ok(())
664                            }
665                            Err(std::env::VarError::NotPresent) => Err(Error::IOError(IOError(
666                                format!("environment variable `{value}` not found"),
667                            ))),
668                            Err(std::env::VarError::NotUnicode(..)) => {
669                                Err(Error::IOError(IOError(format!(
670                                    "environment variable `{value}` has non-unicode content"
671                                ))))
672                            }
673                        },
674                        ("env", Some(attr)) => {
675                            Err(Error::ParseErrors(
676                                ParseError::UnknownSigilAttribute {
677                                    // unwrap(): if `attr` is `Some`, then `attr_sep` must be `Some`
678                                    selector: selector.to_owned(),
679                                    span: RawSpan::from_range(
680                                        value_file_id,
681                                        attr_sep.unwrap() + 1..value_sep,
682                                    ),
683                                    attribute: attr.to_owned(),
684                                }
685                                .into(),
686                            ))
687                        }
688                        (selector, _) => Err(Error::ParseErrors(
689                            ParseError::UnknownSigilSelector {
690                                span: RawSpan::from_range(
691                                    value_file_id,
692                                    1..attr_sep.unwrap_or(value_sep),
693                                ),
694                                selector: selector.to_owned(),
695                            }
696                            .into(),
697                        )),
698                    }?;
699                } else {
700                    self.vm_ctxt.prepare_eval(value_file_id)?;
701                    record = record
702                        .path(ovd.path.0)
703                        .priority(ovd.priority)
704                        .value(Term::ResolvedImport(value_file_id));
705                }
706            }
707
708            let t = self.vm_ctxt.prepare_eval(self.main_id)?;
709            let built_record = record.build();
710            // For now, we can't do much better than using `Label::default`, but this is
711            // hazardous. `Label::default` was originally written for tests, and although it
712            // doesn't happen in practice as of today, it could theoretically generate invalid
713            // codespan file ids (because it creates a new file database on the spot just to
714            // generate a dummy file id).
715            // We'll have to adapt `Label` and `MergeLabel` to be generated programmatically,
716            // without referring to any source position.
717            mk_term::op2(BinaryOp::Merge(Label::default().into()), t, built_record)
718        };
719
720        let runtime_contracts: Result<Vec<_>, _> = self
721            .contracts
722            .iter()
723            .map(|contract| -> Result<_, Error> {
724                match contract {
725                    ProgramContract::Term(contract) => Ok(contract.clone()),
726                    ProgramContract::Source(file_id) => {
727                        let cache = &mut self.vm_ctxt.import_resolver;
728                        cache.prepare(&mut self.vm_ctxt.pos_table, *file_id)?;
729
730                        // unwrap(): we just prepared the file above, so it must be in the cache.
731                        let value = cache.terms.get_owned(*file_id).unwrap();
732
733                        // The label needs a position to show where the contract application is coming from.
734                        // Since it's not really coming from source code, we reconstruct the CLI argument
735                        // somewhere in the source cache.
736                        let pos = value.pos_idx();
737                        let typ = crate::typ::Type {
738                            typ: crate::typ::TypeF::Contract(value.clone()),
739                            pos: self.vm_ctxt.pos_table.get(pos),
740                        };
741
742                        let source_name = cache.sources.name(*file_id).to_string_lossy();
743                        let arg_id = cache.sources.add_string(
744                            SourcePath::CliFieldAssignment,
745                            format!("--apply-contract {source_name}"),
746                        );
747
748                        let span = cache.sources.files().source_span(arg_id);
749
750                        Ok(RuntimeContract::new(
751                            value,
752                            Label {
753                                typ: std::rc::Rc::new(typ),
754                                span: self.vm_ctxt.pos_table.push(span.into()),
755                                ..Default::default()
756                            },
757                        ))
758                    }
759                }
760            })
761            .collect();
762
763        prepared_body = RuntimeContract::apply_all(prepared_body, runtime_contracts?, PosIdx::NONE);
764
765        let prepared: Closure = prepared_body.into();
766
767        let result = if for_query {
768            prepared
769        } else {
770            VirtualMachine::new(&mut self.vm_ctxt)
771                .extract_field_value_closure(prepared, &self.field)?
772        };
773
774        Ok(result)
775    }
776
777    /// Creates an new VM instance borrowing from [Self::vm_ctxt].
778    fn new_vm(&mut self) -> VirtualMachine<'_, CacheHub, EC> {
779        VirtualMachine::new(&mut self.vm_ctxt)
780    }
781
782    /// Parse if necessary, typecheck and then evaluate the program.
783    pub fn eval(&mut self) -> Result<NickelValue, Error> {
784        let prepared = self.prepare_eval()?;
785        Ok(self.new_vm().eval_closure(prepared)?.value)
786    }
787
788    /// Evaluate a closure using the same virtual machine (and import resolver)
789    /// as the main term. The closure should already have been prepared for
790    /// evaluation, with imports resolved and any necessary transformations
791    /// applied.
792    pub fn eval_closure(&mut self, closure: Closure) -> Result<NickelValue, EvalError> {
793        Ok(self.new_vm().eval_closure(closure)?.value)
794    }
795
796    /// Same as `eval`, but proceeds to a full evaluation.
797    pub fn eval_full(&mut self) -> Result<NickelValue, Error> {
798        let prepared = self.prepare_eval()?;
799
800        Ok(self.new_vm().eval_full_closure(prepared)?.value)
801    }
802
803    /// Same as `eval`, but proceeds to a full evaluation. Optionally take a set of overrides that
804    /// are to be applied to the term (in practice, to be merged with).
805    ///
806    /// Skips record fields marked `not_exported`.
807    ///
808    /// # Arguments
809    ///
810    /// - `override` is a list of overrides in the form of an iterator of [`FieldOverride`]s. Each
811    ///   override is imported in a separate in-memory source, for complete isolation (this way,
812    ///   overrides can't accidentally or intentionally capture other fields of the configuration).
813    ///   A stub record is then built, which has all fields defined by `overrides`, and values are
814    ///   an import referring to the corresponding isolated value. This stub is finally merged with
815    ///   the current program before being evaluated for import.
816    pub fn eval_full_for_export(&mut self) -> Result<NickelValue, Error> {
817        let prepared = self.prepare_eval()?;
818
819        Ok(self.new_vm().eval_full_for_export_closure(prepared)?)
820    }
821
822    /// Same as `eval_full`, but does not substitute all variables.
823    pub fn eval_deep(&mut self) -> Result<NickelValue, Error> {
824        let prepared = self.prepare_eval()?;
825
826        Ok(self.new_vm().eval_deep_closure(prepared)?)
827    }
828
829    /// Same as `eval_closure`, but does a full evaluation and does not substitute all variables.
830    ///
831    /// (Or, same as `eval_deep` but takes a closure.)
832    pub fn eval_deep_closure(&mut self, closure: Closure) -> Result<NickelValue, EvalError> {
833        self.new_vm().eval_deep_closure(closure)
834    }
835
836    /// Prepare for evaluation, then fetch the metadata of `self.field`, or list the fields of the
837    /// whole program if `self.field` is empty.
838    pub fn query(&mut self) -> Result<Field, Error> {
839        let prepared = self.prepare_query()?;
840
841        // We have to inline `new_vm()` to get the borrow checker to understand that we can both
842        // borrow `vm_ctxt` mutably and `field` immutably at the same time.
843        Ok(VirtualMachine::new(&mut self.vm_ctxt).query_closure(prepared, &self.field)?)
844    }
845
846    /// Load, parse, and typecheck the program (together with additional contracts) and the
847    /// standard library, if not already done.
848    pub fn typecheck(&mut self, initial_mode: TypecheckMode) -> Result<(), Error> {
849        // If the main file is known to not be Nickel, we don't bother parsing it into an AST
850        // (`cache.typecheck()` will ignore it anyway)
851        let is_nickel = matches!(
852            self.vm_ctxt.import_resolver.input_format(self.main_id),
853            None | Some(InputFormat::Nickel)
854        );
855
856        if is_nickel {
857            self.vm_ctxt.import_resolver.parse_to_ast(self.main_id)?;
858        }
859
860        for source in self.contracts.iter() {
861            match source {
862                ProgramContract::Term(_) => (),
863                ProgramContract::Source(file_id) => {
864                    self.vm_ctxt.import_resolver.parse_to_ast(*file_id)?;
865                }
866            }
867        }
868
869        self.vm_ctxt.import_resolver.load_stdlib()?;
870        self.vm_ctxt
871            .import_resolver
872            .typecheck(self.main_id, initial_mode)
873            .map_err(|cache_err| {
874                cache_err.unwrap_error("program::typecheck(): expected source to be parsed")
875            })?;
876
877        for source in self.contracts.iter() {
878            match source {
879                ProgramContract::Term(_) => (),
880                ProgramContract::Source(file_id) => {
881                    self.vm_ctxt
882                        .import_resolver
883                        .typecheck(*file_id, initial_mode)
884                        .map_err(|cache_err| {
885                            cache_err.unwrap_error(
886                                "program::typecheck(): expected contract to be parsed",
887                            )
888                        })?;
889                }
890            }
891        }
892
893        Ok(())
894    }
895
896    /// Parse and compile the stdlib and the program to the runtime representation. This is usually
897    /// done as part of the various `prepare_xxx` methods, but for some specific workflows (such as
898    /// `nickel test`), compilation might need to be performed explicitly.
899    pub fn compile(&mut self) -> Result<(), Error> {
900        let cache = &mut self.vm_ctxt.import_resolver;
901
902        cache.load_stdlib()?;
903        cache.parse_to_ast(self.main_id)?;
904        // unwrap(): We just loaded the stdlib, so it should be there
905        cache.compile_stdlib(&mut self.vm_ctxt.pos_table).unwrap();
906        cache
907            .compile(&mut self.vm_ctxt.pos_table, self.main_id)
908            .map_err(|cache_err| {
909                cache_err.unwrap_error("program::compile(): we just parsed the program")
910            })?;
911
912        Ok(())
913    }
914
915    /// Evaluate a program into a record spine, a form suitable for extracting the general
916    /// structure of a configuration, and in particular its interface (fields that might need to be
917    /// filled).
918    ///
919    /// This form is used to extract documentation through `nickel doc`, for example.
920    ///
921    /// ## Record spine
922    ///
923    /// By record spine, we mean that the result is a tree of evaluated nested records, and leafs
924    /// are either non-record values in WHNF or partial expressions left
925    /// unevaluated[^missing-field-def]. For example, the record spine of:
926    ///
927    /// ```nickel
928    /// {
929    ///   foo = {bar = 1 + 1} & {baz.subbaz = [some_func "some_arg"] @ ["snd" ++ "_elt"]},
930    ///   input,
931    ///   depdt = input & {extension = 2},
932    /// }
933    /// ```
934    ///
935    /// is
936    ///
937    /// ```nickel
938    /// {
939    ///   foo = {
940    ///     bar = 2,
941    ///     baz = {
942    ///       subbaz = [some_func "some_arg", "snd" ++ "_elt"],
943    ///     },
944    ///   },
945    ///   input,
946    ///   depdt = input & {extension = 2},
947    /// }
948    /// ```
949    ///
950    /// To evaluate a term to a record spine, we first evaluate it to a WHNF and then:
951    /// - If the result is a record, we recursively evaluate subfields to record spines
952    /// - If the result isn't a record, it is returned as it is
953    /// - If the evaluation fails with [EvalErrorKind::MissingFieldDef], the original
954    ///   term is returned unevaluated[^missing-field-def]
955    /// - If any other error occurs, the evaluation fails and returns the error.
956    ///
957    /// [^missing-field-def]: Because we want to handle partial configurations as well,
958    /// [EvalErrorKind::MissingFieldDef] errors are _ignored_: if this is encountered when
959    /// evaluating a field, this field is just left as it is and the evaluation proceeds.
960    pub fn eval_record_spine(&mut self) -> Result<NickelValue, Error> {
961        self.maybe_closurized_eval_record_spine(false)
962    }
963
964    /// Evaluate a program into a record spine, while closurizing all the
965    /// non-record "leaves" in the spine.
966    ///
967    /// To understand the difference between this function and
968    /// [`Program::eval_record_spine`], consider a term like
969    ///
970    /// ```nickel
971    /// let foo = 1 in { bar = [foo] }
972    /// ```
973    ///
974    /// `eval_record_spine` will evaluate this into a record containing the
975    /// field `bar`, and the value of that field will be a `Term::Array`
976    /// containing a `Term::Var("foo")`. In contrast, `eval_closurized` will
977    /// still evaluate the term into a record contining `bar`, but the value of
978    /// that field will be a `Term::Closure` containing that same `Term::Array`,
979    /// together with an `Environment` defining the variable "foo". In
980    /// particular, the closurized version is more useful if you intend to
981    /// further evaluate any record fields, while the non-closurized version is
982    /// more useful if you intend to do further static analysis.
983    pub fn eval_closurized_record_spine(&mut self) -> Result<NickelValue, Error> {
984        self.maybe_closurized_eval_record_spine(true)
985    }
986
987    fn maybe_closurized_eval_record_spine(
988        &mut self,
989        closurize: bool,
990    ) -> Result<NickelValue, Error> {
991        use crate::{
992            eval::Environment,
993            term::{RuntimeContract, record::RecordData},
994        };
995
996        let prepared = self.prepare_eval()?;
997
998        // Naively evaluating some legit recursive structures might lead to an infinite loop. Take
999        // for example this simple contract definition:
1000        //
1001        // ```nickel
1002        // {
1003        //   Tree = {
1004        //     data | Number,
1005        //     left | Tree | optional,
1006        //     right | Tree | optional,
1007        //   }
1008        // }
1009        // ```
1010        //
1011        // Here, we don't want to unfold the occurrences of `Tree` appearing in `left` and `right`,
1012        // or we will just go on indefinitely (until a stack overflow in practice). To avoid this,
1013        // we store the cache index (thunk) corresponding to the content of the `Tree` field before
1014        // evaluating it. After we have successfully evaluated it to a record, we mark it (lock),
1015        // and if we come across the same thunk while evaluating one of its children, here `left`
1016        // for example, we don't evaluate it further.
1017
1018        // Eval pending contracts as well, in order to extract more information from potential
1019        // record contract fields.
1020        fn eval_contracts<EC: EvalCache>(
1021            vm_ctxt: &mut VmContext<CacheHub, EC>,
1022            mut pending_contracts: Vec<RuntimeContract>,
1023            current_env: Environment,
1024            closurize: bool,
1025        ) -> Result<Vec<RuntimeContract>, Error> {
1026            for ctr in pending_contracts.iter_mut() {
1027                let rt = ctr.contract.clone();
1028                // Note that contracts can't be referred to recursively, as they aren't binding
1029                // anything. Only fields are. This is why we pass `None` for `self_idx`: there is
1030                // no locking required here.
1031                ctr.contract = eval_guarded(vm_ctxt, rt, current_env.clone(), closurize)?;
1032            }
1033
1034            Ok(pending_contracts)
1035        }
1036
1037        // Handles thunk locking (and unlocking upon errors) to detect infinite recursion, but
1038        // hands over the meat of the work to `do_eval`.
1039        fn eval_guarded<EC: EvalCache>(
1040            vm_ctxt: &mut VmContext<CacheHub, EC>,
1041            term: NickelValue,
1042            env: Environment,
1043            closurize: bool,
1044        ) -> Result<NickelValue, Error> {
1045            let curr_thunk = term.as_thunk();
1046
1047            if let Some(thunk) = curr_thunk {
1048                // If the thunk is already locked, it's the thunk of some parent field, and we stop
1049                // here to avoid infinite recursion.
1050                if !thunk.lock() {
1051                    return Ok(term);
1052                }
1053            }
1054
1055            let result = do_eval(vm_ctxt, term.clone(), env, closurize);
1056
1057            // Once we're done evaluating all the children, or if there was an error, we unlock the
1058            // current thunk
1059            if let Some(thunk) = curr_thunk {
1060                thunk.unlock();
1061            }
1062
1063            // We expect to hit `MissingFieldDef` errors. When a configuration
1064            // contains undefined record fields they most likely will be used
1065            // recursively in the definition of some other fields. So instead of
1066            // bubbling up an evaluation error in this case we just leave fields
1067            // that depend on as yet undefined fields unevaluated; we wouldn't
1068            // be able to extract dcoumentation from their values anyways. All
1069            // other evaluation errors should however be reported to the user
1070            // instead of resulting in documentation being silently skipped.
1071            if let Err(Error::EvalError(err_data)) = &result
1072                && let EvalErrorKind::MissingFieldDef { .. } = &err_data.error
1073            {
1074                return Ok(term);
1075            }
1076
1077            result
1078        }
1079
1080        // Evaluates the closure, and if it's a record, recursively evaluate its fields and their
1081        // contracts.
1082        fn do_eval<EC: EvalCache>(
1083            vm_ctxt: &mut VmContext<CacheHub, EC>,
1084            term: NickelValue,
1085            env: Environment,
1086            closurize: bool,
1087        ) -> Result<NickelValue, Error> {
1088            let evaled = VirtualMachine::new(vm_ctxt).eval_closure(Closure { value: term, env })?;
1089            let pos_idx = evaled.value.pos_idx();
1090
1091            match evaled.value.content() {
1092                ValueContent::Record(lens) => {
1093                    let Container::Alloc(data) = lens.take() else {
1094                        //unwrap(): will go away
1095                        return Ok(NickelValue::empty_record().with_pos_idx(pos_idx));
1096                    };
1097
1098                    let fields = data
1099                        .fields
1100                        .into_iter()
1101                        .map(|(id, field)| -> Result<_, Error> {
1102                            Ok((
1103                                id,
1104                                Field {
1105                                    value: field
1106                                        .value
1107                                        .map(|value| {
1108                                            eval_guarded(
1109                                                vm_ctxt,
1110                                                value,
1111                                                evaled.env.clone(),
1112                                                closurize,
1113                                            )
1114                                        })
1115                                        .transpose()?,
1116                                    pending_contracts: eval_contracts(
1117                                        vm_ctxt,
1118                                        field.pending_contracts,
1119                                        evaled.env.clone(),
1120                                        closurize,
1121                                    )?,
1122                                    ..field
1123                                },
1124                            ))
1125                        })
1126                        .collect::<Result<_, Error>>()?;
1127
1128                    Ok(NickelValue::record(RecordData { fields, ..data }, pos_idx))
1129                }
1130                lens => {
1131                    let value = lens.restore();
1132
1133                    if closurize {
1134                        Ok(value.closurize(&mut vm_ctxt.cache, evaled.env))
1135                    } else {
1136                        Ok(value)
1137                    }
1138                }
1139            }
1140        }
1141
1142        eval_guarded(&mut self.vm_ctxt, prepared.value, prepared.env, closurize)
1143    }
1144
1145    /// Extract documentation from the program
1146    #[cfg(feature = "doc")]
1147    pub fn extract_doc(&mut self) -> Result<doc::ExtractedDocumentation, Error> {
1148        use crate::error::ExportErrorKind;
1149
1150        let term = self.eval_record_spine()?;
1151        doc::ExtractedDocumentation::extract_from_term(&term).ok_or(Error::export_error(
1152            self.vm_ctxt.pos_table.clone(),
1153            ExportErrorKind::NoDocumentation(term.clone()),
1154        ))
1155    }
1156
1157    #[cfg(debug_assertions)]
1158    pub fn set_skip_stdlib(&mut self) {
1159        self.vm_ctxt.import_resolver.skip_stdlib = true;
1160    }
1161
1162    pub fn pprint_ast(
1163        &mut self,
1164        out: &mut impl std::io::Write,
1165        apply_transforms: bool,
1166    ) -> Result<(), Error> {
1167        use crate::{pretty::*, transform::transform};
1168
1169        let ast_alloc = AstAlloc::new();
1170        let ast = self
1171            .vm_ctxt
1172            .import_resolver
1173            .sources
1174            .parse_nickel(&ast_alloc, self.main_id)?;
1175        if apply_transforms {
1176            let allocator = Allocator::default();
1177            let rt = measure_runtime!(
1178                "runtime:ast_conversion",
1179                ast.to_mainline(&mut self.vm_ctxt.pos_table)
1180            );
1181            let rt = transform(&mut self.vm_ctxt.pos_table, rt, None)
1182                .map_err(|uvar_err| Error::ParseErrors(ParseErrors::from(uvar_err)))?;
1183            let doc: DocBuilder<_, ()> = rt.pretty(&allocator);
1184            doc.render(80, out).map_err(IOError::from)?;
1185            writeln!(out).map_err(IOError::from)?;
1186        } else {
1187            let allocator = crate::ast::pretty::Allocator::default();
1188            let doc: DocBuilder<_, ()> = ast.pretty(&allocator);
1189            doc.render(80, out).map_err(IOError::from)?;
1190            writeln!(out).map_err(IOError::from)?;
1191        }
1192
1193        Ok(())
1194    }
1195
1196    /// Returns a copy of the program's current `Files` database. This doesn't actually clone the content of the source files, see [crate::files::Files].
1197    pub fn files(&self) -> Files {
1198        self.vm_ctxt.import_resolver.files().clone()
1199    }
1200
1201    /// Returns a reference to the position table.
1202    pub fn pos_table(&self) -> &PosTable {
1203        &self.vm_ctxt.pos_table
1204    }
1205}
1206
1207#[cfg(feature = "doc")]
1208mod doc {
1209    use crate::{
1210        error::{Error, ExportErrorKind, IOError},
1211        eval::value::{Container, NickelValue, ValueContentRef},
1212        position::PosTable,
1213        term::{Term, record::RecordData},
1214    };
1215
1216    use comrak::{Arena, format_commonmark, parse_document};
1217    use comrak::{
1218        arena_tree::{Children, NodeEdge},
1219        nodes::{
1220            Ast, AstNode, ListDelimType, ListType, NodeCode, NodeHeading, NodeList, NodeValue,
1221        },
1222    };
1223
1224    use serde::{Deserialize, Serialize};
1225
1226    use std::{collections::HashMap, io::Write};
1227
1228    #[derive(Clone, Debug, Serialize, Deserialize)]
1229    #[serde(transparent)]
1230    pub struct ExtractedDocumentation {
1231        fields: HashMap<String, DocumentationField>,
1232    }
1233
1234    #[derive(Clone, Debug, Serialize, Deserialize)]
1235    struct DocumentationField {
1236        /// Field value [`ExtractedDocumentation`], if any
1237        fields: Option<ExtractedDocumentation>,
1238        /// Rendered type annotation, if any
1239        #[serde(rename = "type")]
1240        typ: Option<String>,
1241        /// Rendered contract annotations
1242        contracts: Vec<String>,
1243        /// Rendered documentation, if any
1244        documentation: Option<String>,
1245    }
1246
1247    fn ast_node<'a>(val: NodeValue) -> AstNode<'a> {
1248        // comrak allows for ast nodes to be tagged with source location. This location
1249        // isn't need for rendering; it seems to be mainly for plugins to use. Since our
1250        // markdown is generated anyway, we just stick in a dummy value.
1251        let pos = comrak::nodes::LineColumn::from((0, 0));
1252        AstNode::new(std::cell::RefCell::new(Ast::new(val, pos)))
1253    }
1254
1255    impl ExtractedDocumentation {
1256        pub fn extract_from_term(value: &NickelValue) -> Option<Self> {
1257            match value.content_ref() {
1258                ValueContentRef::Record(Container::Empty) => Some(Self {
1259                    fields: HashMap::new(),
1260                }),
1261                ValueContentRef::Record(Container::Alloc(record)) => {
1262                    Self::extract_from_record(record)
1263                }
1264                ValueContentRef::Term(Term::RecRecord(data)) => {
1265                    Self::extract_from_record(&data.record)
1266                }
1267                _ => None,
1268            }
1269        }
1270
1271        fn extract_from_record(record: &RecordData) -> Option<Self> {
1272            let fields = record
1273                .fields
1274                .iter()
1275                .map(|(ident, field)| {
1276                    let fields = field.value.as_ref().and_then(Self::extract_from_term);
1277
1278                    // We use the original user-written type stored
1279                    // in the label. Using `lt.typ` instead is often
1280                    // unreadable, since we evaluate terms to a record
1281                    // spine before extracting documentation
1282                    let typ = field
1283                        .metadata
1284                        .0
1285                        .as_ref()
1286                        .and_then(|m| m.annotation.typ.as_ref())
1287                        .map(|lt| lt.label.typ.to_string());
1288
1289                    let contracts = field
1290                        .metadata
1291                        .0
1292                        .iter()
1293                        .flat_map(|m| m.annotation.contracts.iter())
1294                        .map(|lt| lt.label.typ.to_string())
1295                        .collect();
1296
1297                    (
1298                        ident.label().to_owned(),
1299                        DocumentationField {
1300                            fields,
1301                            typ,
1302                            contracts,
1303                            documentation: field.metadata.doc().map(ToOwned::to_owned),
1304                        },
1305                    )
1306                })
1307                .collect();
1308
1309            Some(Self { fields })
1310        }
1311
1312        pub fn write_json(&self, out: &mut dyn Write) -> Result<(), Error> {
1313            serde_json::to_writer(out, self).map_err(|e| {
1314                Error::export_error(PosTable::new(), ExportErrorKind::Other(e.to_string()))
1315            })
1316        }
1317
1318        pub fn write_markdown(&self, out: &mut dyn Write) -> Result<(), Error> {
1319            // comrak expects a fmt::Write and we have an io::Write, so wrap it.
1320            // (There's also the fmt2io crate for this, but that's overkill)
1321            struct IoToFmt<'a>(&'a mut dyn Write);
1322            impl<'a> std::fmt::Write for IoToFmt<'a> {
1323                fn write_str(&mut self, s: &str) -> std::fmt::Result {
1324                    self.0.write_all(s.as_bytes()).map_err(|_| std::fmt::Error)
1325                }
1326            }
1327
1328            let document = ast_node(NodeValue::Document);
1329
1330            // Our nodes in the Markdown document are owned by this arena
1331            let arena = Arena::new();
1332
1333            // The default ComrakOptions disables all extensions (essentially reducing to
1334            // CommonMark)
1335            let options = comrak::Options::default();
1336
1337            self.markdown_append(0, &arena, &document, &options);
1338            format_commonmark(&document, &options, &mut IoToFmt(out))
1339                .map_err(|e| Error::IOError(IOError(e.to_string())))?;
1340
1341            Ok(())
1342        }
1343
1344        /// Recursively walk the given `DocOutput`, recursing into fields, looking for
1345        /// documentation. This documentation is then appended to the provided document.
1346        fn markdown_append<'a>(
1347            &'a self,
1348            header_level: u8,
1349            arena: &'a Arena<'a>,
1350            document: &'a AstNode<'a>,
1351            options: &comrak::Options,
1352        ) {
1353            let mut entries: Vec<(_, _)> = self.fields.iter().collect();
1354            entries.sort_by_key(|(k, _)| *k);
1355
1356            for (ident, field) in entries {
1357                let header = mk_header(ident, header_level + 1, arena);
1358                document.append(header);
1359
1360                if field.typ.is_some() || !field.contracts.is_empty() {
1361                    document.append(mk_types_and_contracts(
1362                        ident,
1363                        arena,
1364                        field.typ.as_deref(),
1365                        field.contracts.as_ref(),
1366                    ))
1367                }
1368
1369                if let Some(ref doc) = field.documentation {
1370                    for child in parse_markdown_string(header_level + 1, arena, doc, options) {
1371                        document.append(child);
1372                    }
1373                }
1374
1375                if let Some(ref subfields) = field.fields {
1376                    subfields.markdown_append(header_level + 1, arena, document, options);
1377                }
1378            }
1379        }
1380
1381        pub fn docstrings(&self) -> Vec<(Vec<&str>, &str)> {
1382            fn collect<'a>(
1383                slf: &'a ExtractedDocumentation,
1384                path: &[&'a str],
1385                acc: &mut Vec<(Vec<&'a str>, &'a str)>,
1386            ) {
1387                for (name, field) in &slf.fields {
1388                    let mut path = path.to_owned();
1389                    path.push(name);
1390
1391                    if let Some(fields) = &field.fields {
1392                        collect(fields, &path, acc);
1393                    }
1394
1395                    if let Some(doc) = &field.documentation {
1396                        acc.push((path, doc));
1397                    }
1398                }
1399            }
1400
1401            let mut ret = Vec::new();
1402            collect(self, &[], &mut ret);
1403            ret
1404        }
1405    }
1406
1407    /// Parses a string into markdown and increases any headers in the markdown by the specified
1408    /// level. This allows having headers in documentation without clashing with the structure of
1409    /// the document.
1410    ///
1411    /// Since this markdown chunk is going to be inserted into another document, we can't return a
1412    /// document node (a document within another document is considered ill-formed by `comrak`).
1413    /// Instead, we strip the root document node off, and return its children.
1414    fn parse_markdown_string<'a>(
1415        header_level: u8,
1416        arena: &'a Arena<'a>,
1417        md: &str,
1418        options: &comrak::Options,
1419    ) -> Children<'a, std::cell::RefCell<Ast>> {
1420        let node = parse_document(arena, md, options);
1421
1422        // Increase header level of every header
1423        for edge in node.traverse() {
1424            if let NodeEdge::Start(n) = edge {
1425                n.data
1426                    .replace_with(|ast| increase_header_level(header_level, ast).clone());
1427            }
1428        }
1429
1430        debug_assert!(node.data.borrow().value == NodeValue::Document);
1431        node.children()
1432    }
1433
1434    fn increase_header_level(header_level: u8, ast: &mut Ast) -> &Ast {
1435        if let NodeValue::Heading(NodeHeading {
1436            level,
1437            setext,
1438            closed,
1439        }) = ast.value
1440        {
1441            ast.value = NodeValue::Heading(NodeHeading {
1442                level: header_level + level,
1443                setext,
1444                closed,
1445            });
1446        }
1447        ast
1448    }
1449
1450    /// Creates a codespan header of the provided string with the provided header level.
1451    fn mk_header<'a>(ident: &str, header_level: u8, arena: &'a Arena<'a>) -> &'a AstNode<'a> {
1452        let res = arena.alloc(ast_node(NodeValue::Heading(NodeHeading {
1453            level: header_level,
1454            setext: false,
1455            closed: false,
1456        })));
1457
1458        let code = arena.alloc(ast_node(NodeValue::Code(NodeCode {
1459            num_backticks: 1,
1460            literal: ident.into(),
1461        })));
1462
1463        res.append(code);
1464
1465        res
1466    }
1467
1468    fn mk_types_and_contracts<'a>(
1469        ident: &str,
1470        arena: &'a Arena<'a>,
1471        typ: Option<&'a str>,
1472        contracts: &'a [String],
1473    ) -> &'a AstNode<'a> {
1474        let list = arena.alloc(ast_node(NodeValue::List(NodeList {
1475            list_type: ListType::Bullet,
1476            marker_offset: 1,
1477            padding: 0,
1478            start: 0,
1479            delimiter: ListDelimType::Period,
1480            bullet_char: b'*',
1481            tight: true,
1482            is_task_list: false,
1483        })));
1484
1485        if let Some(t) = typ {
1486            list.append(mk_type(ident, ':', t, arena));
1487        }
1488
1489        for contract in contracts {
1490            list.append(mk_type(ident, '|', contract, arena));
1491        }
1492
1493        list
1494    }
1495
1496    fn mk_type<'a>(
1497        ident: &str,
1498        separator: char,
1499        typ: &str,
1500        arena: &'a Arena<'a>,
1501    ) -> &'a AstNode<'a> {
1502        let list_item = arena.alloc(ast_node(NodeValue::Item(NodeList {
1503            list_type: ListType::Bullet,
1504            marker_offset: 1,
1505            padding: 0,
1506            start: 0,
1507            delimiter: ListDelimType::Period,
1508            bullet_char: b'*',
1509            tight: true,
1510            is_task_list: false,
1511        })));
1512
1513        // We have to wrap the content of the list item into a paragraph, otherwise the list won't
1514        // be properly separated from the next block coming after it, producing invalid output (for
1515        // example, the beginning of the documenantation of the current field might be merged with
1516        // the last type or contract item).
1517        //
1518        // We probably shouldn't have to, but afer diving into comrak's rendering engine, it seems
1519        // that some subtle interactions make things work correctly for parsed markdown (as opposed to
1520        // this one being programmatically generated) just because list items are always parsed as
1521        // paragraphs. We thus mimic this unspoken invariant here.
1522        let paragraph = arena.alloc(ast_node(NodeValue::Paragraph));
1523
1524        paragraph.append(arena.alloc(ast_node(NodeValue::Code(NodeCode {
1525            literal: format!("{ident} {separator} {typ}"),
1526            num_backticks: 1,
1527        }))));
1528        list_item.append(paragraph);
1529
1530        list_item
1531    }
1532}
1533
1534#[cfg(test)]
1535mod tests {
1536    use super::*;
1537    use crate::{error::NullReporter, eval::cache::CacheImpl};
1538    use assert_matches::assert_matches;
1539    use std::io::Cursor;
1540
1541    fn eval_full(s: &str) -> Result<NickelValue, Error> {
1542        let src = Cursor::new(s);
1543
1544        let mut p: Program<CacheImpl> =
1545            Program::new_from_source(src, "<test>", std::io::sink(), NullReporter {}).map_err(
1546                |io_err| {
1547                    Error::eval_error(
1548                        Default::default(),
1549                        EvalErrorKind::Other(format!("IO error: {io_err}"), PosIdx::NONE),
1550                    )
1551                },
1552            )?;
1553        p.eval_full()
1554    }
1555
1556    fn typecheck(s: &str) -> Result<(), Error> {
1557        let src = Cursor::new(s);
1558
1559        let mut p: Program<CacheImpl> =
1560            Program::new_from_source(src, "<test>", std::io::sink(), NullReporter {}).map_err(
1561                |io_err| {
1562                    Error::eval_error(
1563                        Default::default(),
1564                        EvalErrorKind::Other(format!("IO error: {io_err}"), PosIdx::NONE),
1565                    )
1566                },
1567            )?;
1568        p.typecheck(TypecheckMode::Walk)
1569    }
1570
1571    #[test]
1572    fn evaluation_full() {
1573        use crate::{mk_array, mk_record, term::make as mk_term};
1574
1575        let t = eval_full("[(1 + 1), (\"a\" ++ \"b\"), ([ 1, [1 + 2] ])]").unwrap();
1576
1577        // [2, "ab", [1, [3]]]
1578        let expd = mk_array!(
1579            mk_term::integer(2),
1580            NickelValue::string_posless("ab"),
1581            mk_array!(mk_term::integer(1), mk_array!(mk_term::integer(3)))
1582        );
1583
1584        assert_eq!(t.without_pos(), expd.without_pos());
1585
1586        let t = eval_full("let x = 1 in let y = 1 + x in let z = {foo.bar.baz = y} in z").unwrap();
1587        // Records are parsed as RecRecords, so we need to build one by hand
1588        let expd = mk_record!((
1589            "foo",
1590            mk_record!(("bar", mk_record!(("baz", mk_term::integer(2)))))
1591        ));
1592        assert_eq!(t.without_pos(), expd);
1593
1594        // /!\ [MAY OVERFLOW STACK]
1595        // Check that substitution do not replace bound variables. Before the fixing commit, this
1596        // example would go into an infinite loop, and stack overflow. If it does, this just means
1597        // that this test fails.
1598        eval_full("{y = fun x => x, x = fun y => y}").unwrap();
1599    }
1600
1601    #[test]
1602    // Regression test for issue 715 (https://github.com/tweag/nickel/issues/715)
1603    // Check that program::typecheck() fail on parse error
1604    fn typecheck_invalid_input() {
1605        assert_matches!(
1606            typecheck("{foo = 1 + `, bar : Str = \"a\"}"),
1607            Err(Error::ParseErrors(_))
1608        );
1609    }
1610}