nickel-lang-parser 0.3.0

The Nickel parser
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
//! Various helpers and companion code for the parser are put here to keep the grammar definition
//! uncluttered.
use std::{
    ffi::OsString,
    iter,
    {collections::HashSet, fmt::Debug},
};

use super::error::ParseError;

use crate::{
    app,
    ast::{
        Number,
        combine::merge_doc,
        pattern::bindings::Bindings as _,
        record::{FieldDef, FieldMetadata, Include},
        *,
    },
    combine::CombineAlloc,
    files::FileId,
    fun,
    identifier::LocIdent,
    position::{RawSpan, TermPos},
    primop_app,
};

use malachite::{
    Integer,
    base::num::conversion::traits::{FromSciString, FromStringBase},
};

pub struct ParseNumberError;

pub fn parse_number_sci(slice: &str) -> Result<Number, ParseNumberError> {
    Number::from_sci_string(slice).ok_or(ParseNumberError)
}

pub fn parse_number_base(base: u8, slice: &str) -> Result<Number, ParseNumberError> {
    Ok(Number::from(
        Integer::from_string_base(base, slice).ok_or(ParseNumberError)?,
    ))
}

/// Distinguish between the standard string opening delimiter `"`, the multi-line string
/// opening delimiter `m%"`, and the symbolic string opening delimiter `s%"`.
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum StringStartDelimiter<'input> {
    Standard,
    Multiline,
    Symbolic(&'input str),
}

impl StringStartDelimiter<'_> {
    pub fn is_closed_by(&self, close: &StringEndDelimiter) -> bool {
        matches!(
            (self, close),
            (StringStartDelimiter::Standard, StringEndDelimiter::Standard)
                | (StringStartDelimiter::Multiline, StringEndDelimiter::Special)
                | (
                    StringStartDelimiter::Symbolic(_),
                    StringEndDelimiter::Special
                )
        )
    }

    pub fn needs_strip_indent(&self) -> bool {
        match self {
            StringStartDelimiter::Standard => false,
            StringStartDelimiter::Multiline | StringStartDelimiter::Symbolic(_) => true,
        }
    }
}

/// Distinguish between the standard string closing delimiter `"` and the "special" string
/// closing delimiter `"%`.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum StringEndDelimiter {
    Standard,
    Special,
}

/// A string chunk literal atom, being either a string or a single char.
///
/// Because of the way the lexer handles escaping and interpolation, a contiguous static string
/// `"Some \\ \%{escaped} string"` will be lexed as a sequence of such atoms.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ChunkLiteralPart {
    Str(String),
    Char(char),
}

/// The last field of a record, that can either be a normal field declaration or an ellipsis.
#[allow(
    clippy::large_enum_variant,
    reason = "the large variant is the common one"
)]
#[derive(Clone, Debug)]
pub enum LastField<'ast> {
    FieldDecl(FieldDecl<'ast>),
    Ellipsis,
}

/// A record field declaration, that can be either a standard field definition or an `include`
/// expression.
#[derive(Clone, Debug)]
pub enum FieldDecl<'ast> {
    Def(FieldDef<'ast>),
    Include(Include<'ast>),
    IncludeList(Vec<Include<'ast>>),
}

/// The last match in a data structure pattern. This can either be a normal match, or an ellipsis
/// which can capture the rest of the data structure. The type parameter `P` is the type of the
/// pattern of the data structure (ellipsis are supported for both array and record patterns).
///
/// # Example
///
/// - In `{foo={}, bar}`, the last match is an normal match.
/// - In `{foo={}, bar, ..}`, the last match is a non-capturing ellipsis.
/// - In `{foo={}, bar, ..rest}`, the last match is a capturing ellipsis.
#[derive(Debug, PartialEq, Clone)]
pub enum LastPattern<P> {
    /// The last field is a normal match. In this case the pattern is "closed" so every record
    /// fields should be matched.
    Normal(P),
    /// The pattern is "open" `, ..}`. Optionally you can bind a record containing the remaining
    /// fields to an `Identifier` using the syntax `, ..y}`.
    Ellipsis(Option<LocIdent>),
}

/// Trait for operators that can be eta-expanded to a function.
pub(super) trait EtaExpand {
    /// Eta-expand an operator. This wraps an operator, for example `==`, as a function `fun x1 x2
    /// => x1 == x2`. Propagate the position of the curried operator to the generated primop apps
    /// for better error reporting.
    fn eta_expand(self, alloc: &AstAlloc, pos: TermPos) -> Node<'_>;
}

/// An infix operator that is not applied. Used for the curried operator syntax (e.g `(==)`)
pub(super) struct InfixOp(pub(super) primop::PrimOp);

impl EtaExpand for InfixOp {
    fn eta_expand(self, alloc: &AstAlloc, pos: TermPos) -> Node<'_> {
        // We could use `LocIdent::fresh` for the  newly introduced function parameters. However,
        // it has the issue that pretty printing them doesn't result in valid Nickel anymore. This
        // is why we prefer normal identifier like `x` or `y`.
        match self {
            // We treat `UnaryOp::BoolAnd` and `UnaryOp::BoolOr` separately.
            //
            // They are unary operators taking a second lazy argument, but the current mainine
            // evaluator expects that they are always fully applied (including to their argument).
            // That is, Nickel currently doesn't support a partial application like `%bool_or%
            // <arg1>` (which is fine, because the latter isn't actually representable in the
            // source language: `BoolOr` is only expressible through the infix syntax `<arg1> ||
            // <arg2>`). Thus, instead of eta-expanding to `fun x => <op> x` as we would for other
            // unary operators, we eta-expand to `fun x1 x2 => <op> x1 x2`.
            InfixOp(op @ primop::PrimOp::BoolAnd) | InfixOp(op @ primop::PrimOp::BoolOr) => {
                let fst_arg = LocIdent::from("x");
                let snd_arg = LocIdent::from("y");

                fun!(
                    alloc,
                    fst_arg,
                    snd_arg,
                    app!(
                        alloc,
                        primop_app!(alloc, op, builder::var(fst_arg)),
                        builder::var(snd_arg),
                    )
                    .with_pos(pos),
                )
                .node
            }
            // `RecordGet field record` corresponds to `record."%{field}"`. Using the curried
            // version `(.)` has thus reversed argument corresponding to the `RecordGet` primop, so
            // we need to flip them.
            InfixOp(op @ primop::PrimOp::RecordGet) => {
                let fst_arg = LocIdent::new("x");
                let snd_arg = LocIdent::new("y");

                fun!(
                    alloc,
                    fst_arg,
                    snd_arg,
                    primop_app!(alloc, op, builder::var(snd_arg), builder::var(fst_arg))
                        .with_pos(pos),
                )
                .node
            }
            InfixOp(op) => {
                let vars: Vec<_> = (0..op.arity())
                    .map(|i| LocIdent::from(format!("x{i}")))
                    .collect();
                let fun_args: Vec<_> = vars.iter().map(|arg| pattern::Pattern::any(*arg)).collect();
                let args: Vec<_> = vars.into_iter().map(builder::var).collect();

                alloc.fun(fun_args, alloc.prim_op(op, args).spanned(pos))
            }
        }
    }
}

/// Additional infix operators that aren't proper primitive operations in the Nickel AST but are
/// still available in the surface syntax (and desugared at parsing time). They can still be used
/// in a curried form so they need a wrapper and an `EtaExpand` implementation.
pub(super) enum ExtendedInfixOp {
    /// The reverse application operation or pipe operator `|>`.
    ReverseApp,
    /// The inequality operator `!=`.
    NotEqual,
}

impl EtaExpand for ExtendedInfixOp {
    fn eta_expand(self, alloc: &AstAlloc, pos: TermPos) -> Node<'_> {
        match self {
            ExtendedInfixOp::ReverseApp => {
                let fst_arg = LocIdent::from("x");
                let snd_arg = LocIdent::from("y");

                fun!(
                    alloc,
                    fst_arg,
                    snd_arg,
                    app!(alloc, builder::var(snd_arg), builder::var(fst_arg)).with_pos(pos),
                )
                .node
            }
            ExtendedInfixOp::NotEqual => {
                let fst_arg = LocIdent::from("x");
                let snd_arg = LocIdent::from("y");

                fun!(
                    alloc,
                    fst_arg,
                    snd_arg,
                    primop_app!(
                        alloc,
                        primop::PrimOp::BoolNot,
                        primop_app!(
                            alloc,
                            primop::PrimOp::Eq,
                            builder::var(fst_arg),
                            builder::var(snd_arg),
                        )
                        .with_pos(pos),
                    )
                    .with_pos(pos),
                )
                .node
            }
        }
    }
}

/// Trait for structures representing annotations which can be combined with a term to build
/// another term, or another structure holding a term, such as a field. `T` is the said target
/// structure.
pub trait AttachToAst<'ast, T> {
    fn attach_to_ast(self, alloc: &'ast AstAlloc, ast: Ast<'ast>) -> T;
}

impl<'ast> CombineAlloc<'ast> for FieldMetadata<'ast> {
    /// Combine two field metadata into one. If data that can't be combined (typically, the
    /// documentation or the type annotation) are set by both, the left one's are kept.
    fn combine(alloc: &'ast AstAlloc, left: Self, right: Self) -> Self {
        let priority = match (left.priority, right.priority) {
            // Neutral corresponds to the case where no priority was specified. In that case, the
            // other priority takes precedence.
            (MergePriority::Neutral, p) | (p, MergePriority::Neutral) => p,
            // Otherwise, we keep the maximum of both priorities, as we would do when merging
            // values.
            (p1, p2) => std::cmp::max(p1, p2),
        };

        FieldMetadata {
            doc: merge_doc(left.doc, right.doc),
            annotation: CombineAlloc::combine(alloc, left.annotation, right.annotation),
            opt: left.opt || right.opt,
            // The resulting field will be suppressed from serialization if either of the fields to be merged is.
            not_exported: left.not_exported || right.not_exported,
            priority,
        }
    }
}

impl<'ast> CombineAlloc<'ast> for LetMetadata<'ast> {
    /// Combine two let metadata into one. Same as `FieldMetadata::combine` but restricted to the
    /// metadata that can be associated to a let block.
    fn combine(alloc: &'ast AstAlloc, left: Self, right: Self) -> Self {
        LetMetadata {
            doc: merge_doc(left.doc, right.doc),
            annotation: CombineAlloc::combine(alloc, left.annotation, right.annotation),
        }
    }
}

impl<'ast> CombineAlloc<'ast> for Annotation<'ast> {
    /// Combine two annotations. If both have `types` set, the final type
    /// is the one of the left annotation, while the right one's type is put
    /// inside the final `contracts`.
    ///
    /// Contracts are combined from left to right; the left one's are put first,
    /// then maybe the right one's type annotation and then the right one's
    /// contracts.
    fn combine(alloc: &'ast AstAlloc, left: Self, right: Self) -> Self {
        let (typ, leftover) = match (left.typ, right.typ) {
            (left_ty @ Some(_), right_ty @ Some(_)) => (left_ty, right_ty),
            (left_ty, right_ty) => (left_ty.or(right_ty), None),
        };

        let contracts: Vec<_> = left
            .contracts
            .iter()
            .cloned()
            .chain(leftover)
            .chain(right.contracts.iter().cloned())
            .collect();

        alloc.annotation(typ, contracts)
    }
}

impl<'ast> AttachToAst<'ast, Ast<'ast>> for Annotation<'ast> {
    fn attach_to_ast(self, alloc: &'ast AstAlloc, ast: Ast<'ast>) -> Ast<'ast> {
        if self.is_empty() {
            return ast;
        }

        let pos = ast.pos;
        Ast {
            node: alloc.annotated(self, ast),
            pos,
        }
    }
}

/// Takes a record access written as `foo."<access>"`, and either turn it into a static access
/// whenever possible (when `<access>` is a static string without interpolation), or into a dynamic
/// `%record/get%` access otherwise.
pub fn mk_access<'ast>(alloc: &'ast AstAlloc, access: Ast<'ast>, root: Ast<'ast>) -> Node<'ast> {
    if let Some(label) = access.node.try_str_chunk_as_static_str() {
        alloc.prim_op(
            primop::PrimOp::RecordStatAccess(LocIdent::new_with_pos(label, access.pos)),
            iter::once(root),
        )
    } else {
        alloc.prim_op(primop::PrimOp::RecordGet, [access, root])
    }
}

/// Make a span from parser byte offsets.
pub fn mk_span(src_id: FileId, l: usize, r: usize) -> RawSpan {
    RawSpan {
        src_id,
        start: (l as u32).into(),
        end: (r as u32).into(),
    }
}

pub fn mk_pos(src_id: FileId, l: usize, r: usize) -> TermPos {
    TermPos::Original(mk_span(src_id, l, r))
}

/// Checks that there are no duplicate bindings in a let block (when bindings are simple, that is
/// they aren't pattern), and builds the corresponding let block node if the check passes.
pub fn mk_let<'ast>(
    alloc: &'ast AstAlloc,
    rec: bool,
    bindings: Vec<LetBinding<'ast>>,
    body: Ast<'ast>,
) -> Result<Node<'ast>, ParseError> {
    // Check for duplicate names across the different bindings. We
    // don't check for duplicate names within a single binding because
    // there are backwards-compatibility constraints (e.g., see
    // `RecordPattern::check_dup`).
    let mut seen_bindings: HashSet<LocIdent> = HashSet::new();

    for b in &bindings {
        let new_bindings = b.pattern.bindings();
        for binding in &new_bindings {
            if let Some(old) = seen_bindings.get(&binding.id) {
                return Err(ParseError::DuplicateIdentInLetBlock {
                    ident: binding.id,
                    prev_ident: *old,
                });
            }
        }

        seen_bindings.extend(new_bindings.into_iter().map(|binding| binding.id));
    }

    Ok(alloc.let_block(bindings, body, rec))
}

pub fn mk_import_based_on_filename(
    alloc: &AstAlloc,
    path: String,
    _span: RawSpan,
) -> Result<Node<'_>, ParseError> {
    let path = OsString::from(path);
    let format: Option<InputFormat> = InputFormat::from_path(&path);

    // Fall back to InputFormat::Nickel in case of unknown filename extension for backwards compatibility.
    let format = format.unwrap_or_default();

    Ok(alloc.import_path(path, format))
}

pub fn mk_import_explicit(
    alloc: &AstAlloc,
    path: String,
    format: LocIdent,
    span: RawSpan,
) -> Result<Node<'_>, ParseError> {
    let path = OsString::from(path);
    let Ok(format) = format.label().parse::<InputFormat>() else {
        return Err(ParseError::InvalidImportFormat { span });
    };

    Ok(alloc.import_path(path, format))
}

/// Determine the minimal level of indentation of a multi-line string.
///
/// The result is determined by computing the minimum indentation level among all lines, where the
/// indentation level of a line is the number of consecutive whitespace characters, which are
/// either a space or a tab, counted from the beginning of the line. If a line is empty or consist
/// only of whitespace characters, it is ignored.
pub fn min_indent(chunks: &[StringChunk<Ast<'_>>]) -> usize {
    let mut min: usize = usize::MAX;
    let mut current = 0;
    let mut start_line = true;

    for chunk in chunks.iter() {
        match chunk {
            StringChunk::Expr(_, _) if start_line => {
                if current < min {
                    min = current;
                }
                start_line = false;
            }
            StringChunk::Expr(_, _) => (),
            StringChunk::Literal(s) => {
                for c in s.chars() {
                    match c {
                        ' ' | '\t' if start_line => current += 1,
                        '\n' => {
                            current = 0;
                            start_line = true;
                        }
                        _ if start_line => {
                            if current < min {
                                min = current;
                            }
                            start_line = false;
                        }
                        _ => (),
                    }
                }
            }
        }
    }

    min
}

/// Strip the common indentation prefix from a multi-line string.
///
/// Determine the minimum indentation level of a multi-line string via [`min_indent`], and strip an
/// equal number of whitespace characters (` ` or `\t`) from the beginning of each line. If the last
/// line is empty or consist only of whitespace characters, it is filtered out.
///
/// The indentation of interpolated expressions in a multi-line string follow the rules:
/// - if an interpolated expression is alone on a line with whitespaces, its indentation -- minus
///   the common minimal indentation -- is stored and when the expression will be evaluated, each
///   new line will be prepended with this indentation level.
/// - if there are other non whitespace characters or interpolated expressions on the line, then it
///   is just replaced by its content. The common indentation is still stripped before the start of
///   this expression, but newlines inside it won't be affected..
///
/// Examples:
///
/// ```text
/// let x = "I\nam\nindented" in
/// m%"
///   baseline
///     ${x}
///   end
/// "%
/// ```
///
/// gives
///
/// ```text
///"baseline
///  I
///  am
///  indented
/// end"
/// ```
///
/// While
///
/// ```text
/// let x = "I\nam\nnot" in
/// m%"
///   baseline
///     ${x} sth
///   end
/// "%
/// ```
///
/// gives
///
/// ```text
///"baseline
///  I
///am
///not sth
/// end"
/// ```
pub fn strip_indent(chunks: &mut [StringChunk<Ast<'_>>]) {
    if chunks.is_empty() {
        return;
    }

    let min = min_indent(chunks);
    let mut current = 0;
    let mut start_line = true;
    let chunks_len = chunks.len();

    // When processing a line with an indented interpolated expression, as in:
    //
    // ```
    // m%"
    //  some
    //    ${x} ${y}
    //    ${x}
    //  string
    // "%
    // ```
    //
    // We don't know at the time we process the expression `${x}` if it will have to be re-indented,
    // as it depends on the rest of the line being only whitespace or not, according to the
    // indentation rule. Here, the first occurrence should not, while the second one should. We can
    // only know this once we process the next chunks, here when arriving at `${y}`. To handle
    // this, we set all indentation levels as if expressions were alone on their line during the
    // main loop, but also store the index of such chunks which indentation level must be revisited
    // once the information becomes available. Then, their indentation level is erased in a last
    // pass.
    let mut unindent: Vec<usize> = Vec::new();
    let mut expr_on_line: Option<usize> = None;

    for (index, chunk) in chunks.iter_mut().enumerate() {
        match chunk {
            StringChunk::Literal(s) => {
                let mut buffer = String::new();
                for c in s.chars() {
                    match c {
                        ' ' | '\t' if start_line && current < min => current += 1,
                        ' ' | '\t' if start_line => {
                            current += 1;
                            buffer.push(c);
                        }
                        '\n' => {
                            current = 0;
                            start_line = true;
                            expr_on_line = None;
                            buffer.push(c);
                        }
                        c if start_line => {
                            start_line = false;
                            buffer.push(c);
                        }
                        c => buffer.push(c),
                    }
                }

                // Strip the first line, if it is only whitespace characters
                if index == 0
                    && let Some(first_index) = buffer.find('\n')
                    && (first_index == 0
                        || buffer.as_bytes()[..first_index]
                            .iter()
                            .all(|c| *c == b' ' || *c == b'\t'))
                {
                    buffer = String::from(&buffer[(first_index + 1)..]);
                }

                // Strip the last line, if it is only whitespace characters.
                if index == chunks_len - 1
                    && let Some(last_index) = buffer.rfind('\n')
                    && (last_index == buffer.len() - 1
                        || buffer.as_bytes()[(last_index + 1)..]
                            .iter()
                            .all(|c| *c == b' ' || *c == b'\t'))
                {
                    buffer.truncate(last_index);
                }

                *s = buffer;
            }
            StringChunk::Expr(_, indent) => {
                if start_line {
                    debug_assert!(current >= min);
                    debug_assert!(expr_on_line.is_none());
                    *indent = current - min;
                    start_line = false;
                    expr_on_line = Some(index);
                } else if let Some(expr_index) = expr_on_line.take() {
                    unindent.push(expr_index);
                }
            }
        }
    }

    for index in unindent.into_iter() {
        match chunks.get_mut(index) {
            Some(StringChunk::Expr(_, indent)) => *indent = 0,
            _ => unreachable!(
                "all elements in `unindent` should be expressions, but found a literal"
            ),
        }
    }
}