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}