kcl_lib/parsing/
parser.rs

1// TODO optimise size of CompilationError
2#![allow(clippy::result_large_err)]
3
4use std::{cell::RefCell, collections::BTreeMap};
5
6use indexmap::IndexMap;
7use winnow::{
8    combinator::{alt, delimited, opt, peek, preceded, repeat, repeat_till, separated, separated_pair, terminated},
9    dispatch,
10    error::{ErrMode, StrContext, StrContextValue},
11    prelude::*,
12    stream::Stream,
13    token::{any, none_of, one_of, take_till},
14};
15
16use super::{
17    DeprecationKind,
18    ast::types::{AscribedExpression, ImportPath, LabelledExpression},
19    token::{NumericSuffix, RESERVED_WORDS},
20};
21use crate::{
22    IMPORT_FILE_EXTENSIONS, MetaSettings, SourceRange, TypedPath,
23    errors::{CompilationError, Severity, Tag},
24    execution::{annotations, types::ArrayLen},
25    parsing::{
26        PIPE_OPERATOR, PIPE_SUBSTITUTION_OPERATOR,
27        ast::types::{
28            Annotation, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem,
29            BoxNode, CallExpressionKw, CommentStyle, DefaultParamVal, ElseIf, Expr, ExpressionStatement,
30            FunctionExpression, FunctionType, Identifier, IfExpression, ImportItem, ImportSelector, ImportStatement,
31            ItemVisibility, LabeledArg, Literal, LiteralValue, MemberExpression, Name, Node, NodeList, NonCodeMeta,
32            NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter, PipeExpression, PipeSubstitution,
33            PrimitiveType, Program, ReturnStatement, Shebang, TagDeclarator, Type, TypeDeclaration, UnaryExpression,
34            UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
35        },
36        math::BinaryExpressionToken,
37        token::{Token, TokenSlice, TokenType},
38    },
39};
40
41thread_local! {
42    /// The current `ParseContext`. `None` if parsing is not currently happening on this thread.
43    static CTXT: RefCell<Option<ParseContext>> = const { RefCell::new(None) };
44}
45
46const MISSING_ELSE: &str = "This `if` block needs a matching `else` block";
47const IF_ELSE_CANNOT_BE_EMPTY: &str = "`if` and `else` blocks cannot be empty";
48const ELSE_STRUCTURE: &str = "This `else` should be followed by a {, then a block of code, then a }";
49const ELSE_MUST_END_IN_EXPR: &str = "This `else` block needs to end in an expression, which will be the value if no preceding `if` condition was matched";
50
51const KEYWORD_EXPECTING_IDENTIFIER: &str = "Expected an identifier, but found a reserved keyword.";
52
53pub fn run_parser(i: TokenSlice) -> super::ParseResult {
54    let _stats = crate::log::LogPerfStats::new("Parsing");
55    ParseContext::init();
56
57    let result = match program.parse(i) {
58        Ok(result) => Some(result),
59        Err(e) => {
60            ParseContext::err(e.into());
61            None
62        }
63    };
64    let ctxt = ParseContext::take();
65    (result, ctxt.errors).into()
66}
67
68/// Context built up while parsing a program.
69///
70/// When returned from parsing contains the errors and warnings from the current parse.
71#[derive(Debug, Clone, Default)]
72struct ParseContext {
73    pub errors: Vec<CompilationError>,
74    settings: MetaSettings,
75}
76
77impl ParseContext {
78    fn new() -> Self {
79        ParseContext {
80            errors: Vec::new(),
81            settings: Default::default(),
82        }
83    }
84
85    /// Set a new `ParseContext` in thread-local storage. Panics if one already exists.
86    fn init() {
87        assert!(CTXT.with_borrow(|ctxt| ctxt.is_none()));
88        CTXT.with_borrow_mut(|ctxt| *ctxt = Some(ParseContext::new()));
89    }
90
91    /// Take the current `ParseContext` from thread-local storage, leaving `None`. Panics if a `ParseContext`
92    /// is not present.
93    fn take() -> ParseContext {
94        CTXT.with_borrow_mut(|ctxt| ctxt.take()).unwrap()
95    }
96
97    fn handle_attribute(attr: &Node<Annotation>) {
98        // TODO handle scoping of attributes - not currently much of an issue because if settings is
99        // not top-level, then we'll get an error later.
100        match attr.name() {
101            Some(annotations::SETTINGS) => {
102                CTXT.with_borrow_mut(|ctxt| {
103                    let _ = ctxt.as_mut().unwrap().settings.update_from_annotation(attr);
104                });
105            }
106            Some(annotations::WARNINGS) => {
107                // TODO https://github.com/KittyCAD/modeling-app/issues/8021
108            }
109            _ => {}
110        }
111    }
112
113    /// Add an error to the current `ParseContext`, panics if there is none.
114    fn err(err: CompilationError) {
115        CTXT.with_borrow_mut(|ctxt| {
116            // Avoid duplicating errors. This is possible since the parser can try one path, find
117            // a warning, then backtrack and decide not to take that path and try another. This can
118            // happen 'high up the stack', so it's impossible to fix where the errors are generated.
119            // Ideally we would pass errors up the call stack rather than use a context object or
120            // have some way to mark errors as speculative or committed, but I don't think Winnow
121            // is flexible enough for that (or at least, not without significant changes to the
122            // parser).
123            let errors = &mut ctxt.as_mut().unwrap().errors;
124            for e in errors.iter_mut().rev() {
125                if e.source_range == err.source_range {
126                    *e = err;
127                    return;
128                }
129            }
130            errors.push(err);
131        });
132    }
133
134    /// Add a warning to the current `ParseContext`, panics if there is none.
135    fn warn(mut e: CompilationError) {
136        e.severity = Severity::Warning;
137        Self::err(e);
138    }
139
140    fn experimental(feature: &str, source_range: SourceRange) {
141        CTXT.with_borrow_mut(|ctxt| {
142            let ctxt = ctxt.as_mut().unwrap();
143            let Some(severity) = ctxt.settings.experimental_features.severity() else {
144                return;
145            };
146            let error = CompilationError {
147                source_range,
148                message: format!("Use of {feature} is experimental and may change or be removed."),
149                suggestion: None,
150                severity,
151                tag: crate::errors::Tag::None,
152            };
153
154            ctxt.errors.push(error);
155        });
156    }
157}
158
159/// Accumulate context while backtracking errors
160/// Very similar to [`winnow::error::ContextError`] type,
161/// but the 'cause' field is always a [`CompilationError`],
162/// instead of a dynamic [`std::error::Error`] trait object.
163#[derive(Debug, Clone)]
164pub(crate) struct ContextError<C = StrContext> {
165    pub context: Vec<C>,
166    pub cause: Option<CompilationError>,
167}
168
169impl From<winnow::error::ParseError<TokenSlice<'_>, ContextError>> for CompilationError {
170    fn from(err: winnow::error::ParseError<TokenSlice<'_>, ContextError>) -> Self {
171        let Some(last_token) = err.input().last() else {
172            return CompilationError::fatal(Default::default(), "file is empty");
173        };
174
175        let (input, offset, err) = (err.input(), err.offset(), err.clone().into_inner());
176
177        if let Some(e) = err.cause {
178            return e;
179        }
180
181        // See docs on `offset`.
182        if offset >= input.len() {
183            let context = err.context.first();
184            return CompilationError::fatal(
185                last_token.as_source_range(),
186                match context {
187                    Some(what) => format!("Unexpected end of file. The compiler {what}"),
188                    None => "Unexpected end of file while still parsing".to_owned(),
189                },
190            );
191        }
192
193        let bad_token = input.token(offset);
194        // TODO: Add the Winnow parser context to the error.
195        // See https://github.com/KittyCAD/modeling-app/issues/784
196        CompilationError::fatal(
197            bad_token.as_source_range(),
198            format!("Unexpected token: {}", bad_token.value),
199        )
200    }
201}
202
203impl<C> From<CompilationError> for ContextError<C> {
204    fn from(e: CompilationError) -> Self {
205        Self {
206            context: Default::default(),
207            cause: Some(e),
208        }
209    }
210}
211
212impl<C> std::default::Default for ContextError<C> {
213    fn default() -> Self {
214        Self {
215            context: Default::default(),
216            cause: None,
217        }
218    }
219}
220
221impl<I, C> winnow::error::ParserError<I> for ContextError<C>
222where
223    I: Stream,
224{
225    /// Generally, `Self`
226    ///
227    /// Mostly used for [`ErrMode`]
228    type Inner = Self;
229
230    /// Creates an error from the input position
231    fn from_input(_input: &I) -> Self {
232        Self::default()
233    }
234
235    /// Unwrap the mode, returning the underlying error, if present
236    fn into_inner(self) -> winnow::Result<Self::Inner, Self> {
237        Ok(self)
238    }
239}
240
241impl<C, I> winnow::error::AddContext<I, C> for ContextError<C>
242where
243    I: Stream,
244{
245    #[inline]
246    fn add_context(mut self, _input: &I, _input_checkpoint: &<I as Stream>::Checkpoint, ctx: C) -> Self {
247        self.context.push(ctx);
248        self
249    }
250}
251
252impl<C, I> winnow::error::FromExternalError<I, CompilationError> for ContextError<C> {
253    /// Like [`ParserError::from_input`] but also include an external error.
254    #[inline]
255    fn from_external_error(_input: &I, e: CompilationError) -> Self {
256        let mut err = Self::default();
257        {
258            err.cause = Some(e);
259        }
260        err
261    }
262}
263
264type ModalResult<O, E = ContextError> = winnow::prelude::ModalResult<O, E>;
265
266fn expected(what: &'static str) -> StrContext {
267    StrContext::Expected(StrContextValue::Description(what))
268}
269
270fn program(i: &mut TokenSlice) -> ModalResult<Node<Program>> {
271    let shebang = opt(shebang).parse_next(i)?;
272    let mut out: Node<Program> = function_body.parse_next(i)?;
273    out.shebang = shebang;
274
275    // Match original parser behaviour, for now.
276    // Once this is merged and stable, consider changing this as I think it's more accurate
277    // without the -1.
278    out.end -= 1;
279    Ok(out)
280}
281
282fn pipe_surrounded_by_whitespace(i: &mut TokenSlice) -> ModalResult<()> {
283    (
284        repeat(0.., whitespace).map(|_: Vec<_>| ()),
285        pipe_operator,
286        repeat(0.., whitespace).map(|_: Vec<_>| ()),
287    )
288        .parse_next(i)?;
289    Ok(())
290}
291
292/// Note this is O(n).
293fn count_in(target: char, s: &str) -> usize {
294    s.chars().filter(|&c| c == target).count()
295}
296
297/// Matches all four cases of NonCodeValue
298fn non_code_node(i: &mut TokenSlice) -> ModalResult<Node<NonCodeNode>> {
299    /// Matches one case of NonCodeValue
300    /// See docstring on [NonCodeValue::NewLineBlockComment] for why that case is different to the others.
301    fn non_code_node_leading_whitespace(i: &mut TokenSlice) -> ModalResult<Node<NonCodeNode>> {
302        let leading_whitespace = one_of(TokenType::Whitespace)
303            .context(expected("whitespace, with a newline"))
304            .parse_next(i)?;
305        let has_empty_line = count_in('\n', &leading_whitespace.value) >= 2;
306        non_code_node_no_leading_whitespace
307            .verify_map(|node: Node<NonCodeNode>| match node.inner.value {
308                NonCodeValue::BlockComment { value, style } => Some(Node::new(
309                    NonCodeNode {
310                        value: if has_empty_line {
311                            NonCodeValue::NewLineBlockComment { value, style }
312                        } else {
313                            NonCodeValue::BlockComment { value, style }
314                        },
315                        digest: None,
316                    },
317                    leading_whitespace.start,
318                    node.end + 1,
319                    node.module_id,
320                )),
321                _ => None,
322            })
323            .context(expected("a comment or whitespace"))
324            .parse_next(i)
325    }
326
327    alt((non_code_node_leading_whitespace, non_code_node_no_leading_whitespace)).parse_next(i)
328}
329
330fn outer_annotation(i: &mut TokenSlice) -> ModalResult<Node<Annotation>> {
331    peek((at_sign, open_paren)).parse_next(i)?;
332    annotation(i)
333}
334
335fn annotation(i: &mut TokenSlice) -> ModalResult<Node<Annotation>> {
336    let at = at_sign.parse_next(i)?;
337    let name = opt(binding_name).parse_next(i)?;
338    let mut end = name.as_ref().map(|n| n.end).unwrap_or(at.end);
339
340    let properties = if peek(open_paren).parse_next(i).is_ok() {
341        open_paren(i)?;
342        ignore_whitespace(i);
343        let properties: Vec<_> = separated(
344            0..,
345            separated_pair(
346                terminated(identifier, opt(whitespace)),
347                terminated(one_of((TokenType::Operator, "=")), opt(whitespace)),
348                expression,
349            )
350            .map(|(key, value)| {
351                Node::new_node(
352                    key.start,
353                    value.end(),
354                    key.module_id,
355                    ObjectProperty {
356                        key,
357                        value,
358                        digest: None,
359                    },
360                )
361            }),
362            comma_sep,
363        )
364        .parse_next(i)?;
365        ignore_trailing_comma(i);
366        ignore_whitespace(i);
367        end = close_paren(i)?.end;
368        Some(properties)
369    } else {
370        None
371    };
372
373    if name.is_none() && properties.is_none() {
374        return Err(ErrMode::Cut(
375            CompilationError::fatal(at.as_source_range(), format!("Unexpected token: {}", at.value)).into(),
376        ));
377    }
378
379    let value = Node::new(
380        Annotation {
381            name,
382            properties,
383            digest: None,
384        },
385        at.start,
386        end,
387        at.module_id,
388    );
389
390    ParseContext::handle_attribute(&value);
391
392    Ok(value)
393}
394
395// Matches remaining three cases of NonCodeValue
396fn non_code_node_no_leading_whitespace(i: &mut TokenSlice) -> ModalResult<Node<NonCodeNode>> {
397    any.verify_map(|token: Token| {
398        if token.is_code_token() {
399            None
400        } else {
401            let value = match token.token_type {
402                TokenType::Whitespace if token.value.contains("\n\n") || token.value.contains("\n\r\n") => {
403                    NonCodeValue::NewLine
404                }
405                TokenType::LineComment => NonCodeValue::BlockComment {
406                    value: token.value.trim_start_matches("//").trim().to_owned(),
407                    style: CommentStyle::Line,
408                },
409                TokenType::BlockComment => NonCodeValue::BlockComment {
410                    style: CommentStyle::Block,
411                    value: token
412                        .value
413                        .trim_start_matches("/*")
414                        .trim_end_matches("*/")
415                        .trim()
416                        .to_owned(),
417                },
418                _ => return None,
419            };
420            Some(Node::new(
421                NonCodeNode { value, digest: None },
422                token.start,
423                token.end,
424                token.module_id,
425            ))
426        }
427    })
428    .context(expected("Non-code token (comments or whitespace)"))
429    .parse_next(i)
430}
431
432fn expression(i: &mut TokenSlice) -> ModalResult<Expr> {
433    let head = expression_but_not_pipe.parse_next(i)?;
434    let head_checkpoint = i.checkpoint();
435
436    // Check for any non-code followed by `|>`
437    let noncode: Result<Vec<_>, _> = terminated(
438        repeat(0.., preceded(whitespace, non_code_node)),
439        peek(pipe_surrounded_by_whitespace),
440    )
441    .parse_next(i);
442
443    let Ok(noncode) = noncode else {
444        // No `|>`, so return a single expression.
445        i.reset(&head_checkpoint);
446        return Ok(head);
447    };
448    // Continue to parse the rest of the pipeline and return a pipeline expression.
449
450    let mut non_code_meta = NonCodeMeta::default();
451    for nc in noncode {
452        non_code_meta.insert(0, nc);
453    }
454    let mut values = vec![head];
455
456    let value_surrounded_by_comments = (
457        repeat(0.., preceded(opt(whitespace), non_code_node)), // Before the expression.
458        preceded(opt(whitespace), labelled_fn_call),           // The expression.
459        repeat(0.., noncode_just_after_code),                  // After the expression.
460    );
461    let tail: Vec<(Vec<_>, _, Vec<_>)> = repeat(
462        1..,
463        preceded(pipe_surrounded_by_whitespace, value_surrounded_by_comments),
464    )
465    .context(expected(
466        "a sequence of at least one |> (pipe) operator, followed by an expression",
467    ))
468    .parse_next(i)?;
469
470    // Time to structure the return value.
471    let mut code_count = 0;
472    let mut max_noncode_end = 0;
473    for (noncode_before, code, noncode_after) in tail {
474        for nc in noncode_before {
475            max_noncode_end = nc.end.max(max_noncode_end);
476            non_code_meta.insert(code_count, nc);
477        }
478        values.push(code);
479        code_count += 1;
480        for nc in noncode_after {
481            max_noncode_end = nc.end.max(max_noncode_end);
482            non_code_meta.insert(code_count, nc);
483        }
484    }
485    Ok(Expr::PipeExpression(Node::boxed(
486        values.first().unwrap().start(),
487        values.last().unwrap().end().max(max_noncode_end),
488        values.first().unwrap().module_id(),
489        PipeExpression {
490            body: values,
491            non_code_meta,
492            digest: None,
493        },
494    )))
495}
496
497fn bool_value(i: &mut TokenSlice) -> ModalResult<Node<Literal>> {
498    let (value, token) = any
499        .try_map(|token: Token| match token.token_type {
500            TokenType::Keyword if token.value == "true" => Ok((true, token)),
501            TokenType::Keyword if token.value == "false" => Ok((false, token)),
502            _ => Err(CompilationError::fatal(
503                token.as_source_range(),
504                "invalid boolean literal",
505            )),
506        })
507        .context(expected("a boolean literal (either true or false)"))
508        .parse_next(i)?;
509    Ok(Node::new(
510        Literal {
511            value: LiteralValue::Bool(value),
512            raw: value.to_string(),
513            digest: None,
514        },
515        token.start,
516        token.end,
517        token.module_id,
518    ))
519}
520
521fn literal(i: &mut TokenSlice) -> ModalResult<BoxNode<Literal>> {
522    alt((string_literal, unsigned_number_literal, bool_value))
523        .map(Box::new)
524        .context(expected("a KCL literal, like 'myPart' or 3"))
525        .parse_next(i)
526}
527
528/// Parse a KCL string literal
529fn string_literal(i: &mut TokenSlice) -> ModalResult<Node<Literal>> {
530    let (value, token) = any
531        .try_map(|token: Token| match token.token_type {
532            TokenType::String => {
533                let s = token.value[1..token.value.len() - 1].to_string();
534                Ok((LiteralValue::from(s), token))
535            }
536            _ => Err(CompilationError::fatal(
537                token.as_source_range(),
538                "invalid string literal",
539            )),
540        })
541        .context(expected("string literal (like \"myPart\""))
542        .parse_next(i)?;
543
544    let result = Node::new(
545        Literal {
546            value,
547            raw: token.value,
548            digest: None,
549        },
550        token.start,
551        token.end,
552        token.module_id,
553    );
554
555    if let Some(suggestion) = super::deprecation(result.value.string_value().unwrap(), DeprecationKind::String) {
556        ParseContext::warn(
557            CompilationError::err(
558                result.as_source_range(),
559                format!(
560                    "Using `\"{}\"` is deprecated, prefer using `{}`.",
561                    result.value.string_value().unwrap(),
562                    suggestion
563                ),
564            )
565            .with_suggestion(
566                format!(
567                    "Replace `\"{}\"` with `{}`",
568                    result.value.string_value().unwrap(),
569                    suggestion
570                ),
571                suggestion,
572                None,
573                Tag::Deprecated,
574            ),
575        );
576    }
577
578    Ok(result)
579}
580
581/// Parse a KCL literal number, with no - sign.
582pub(crate) fn unsigned_number_literal(i: &mut TokenSlice) -> ModalResult<Node<Literal>> {
583    let (value, token) = any
584        .try_map(|token: Token| match token.token_type {
585            TokenType::Number => {
586                let value: f64 = token.numeric_value().ok_or_else(|| {
587                    CompilationError::fatal(token.as_source_range(), format!("Invalid float: {}", token.value))
588                })?;
589
590                let suffix = token.numeric_suffix();
591                if let NumericSuffix::Unknown = suffix {
592                    ParseContext::warn(CompilationError::err(token.as_source_range(), "The 'unknown' numeric suffix is not properly supported; it is likely to change or be removed, and may be buggy."));
593                }
594
595                Ok((
596                    LiteralValue::Number {
597                        value,
598                        suffix,
599                    },
600                    token,
601                ))
602            }
603            _ => Err(CompilationError::fatal(token.as_source_range(), "invalid literal")),
604        })
605        .context(expected("an unsigned number literal (e.g. 3 or 12.5)"))
606        .parse_next(i)?;
607    Ok(Node::new(
608        Literal {
609            value,
610            raw: token.value,
611            digest: None,
612        },
613        token.start,
614        token.end,
615        token.module_id,
616    ))
617}
618
619/// Parse a KCL operator that takes a left- and right-hand side argument.
620fn binary_operator(i: &mut TokenSlice) -> ModalResult<BinaryOperator> {
621    any.try_map(|token: Token| {
622        if !matches!(token.token_type, TokenType::Operator) {
623            return Err(CompilationError::fatal(
624                token.as_source_range(),
625                format!("unexpected token, should be an operator but was {}", token.token_type),
626            ));
627        }
628        let op = match token.value.as_str() {
629            "+" => BinaryOperator::Add,
630            "-" => BinaryOperator::Sub,
631            "/" => BinaryOperator::Div,
632            "*" => BinaryOperator::Mul,
633            "%" => BinaryOperator::Mod,
634            "^" => BinaryOperator::Pow,
635            "==" => BinaryOperator::Eq,
636            "!=" => BinaryOperator::Neq,
637            ">" => BinaryOperator::Gt,
638            ">=" => BinaryOperator::Gte,
639            "<" => BinaryOperator::Lt,
640            "<=" => BinaryOperator::Lte,
641            "|" => BinaryOperator::Or,
642            "&" => BinaryOperator::And,
643            "||" => {
644                ParseContext::err(
645                    CompilationError::err(
646                        token.as_source_range(),
647                        "`||` is not an operator, did you mean to use `|`?",
648                    )
649                    .with_suggestion("Replace `||` with `|`", "|", None, Tag::None),
650                );
651                BinaryOperator::Or
652            }
653            "&&" => {
654                ParseContext::err(
655                    CompilationError::err(
656                        token.as_source_range(),
657                        "`&&` is not an operator, did you mean to use `&`?",
658                    )
659                    .with_suggestion("Replace `&&` with `&`", "&", None, Tag::None),
660                );
661                BinaryOperator::And
662            }
663            _ => {
664                return Err(CompilationError::fatal(
665                    token.as_source_range(),
666                    format!("{} is not a binary operator", token.value.as_str()),
667                ));
668            }
669        };
670        Ok(op)
671    })
672    .context(expected("a binary operator (like + or *)"))
673    .parse_next(i)
674}
675
676/// Parse a KCL operand that can be used with an operator.
677fn operand(i: &mut TokenSlice) -> ModalResult<BinaryPart> {
678    const TODO_783: &str = "found a value, but this kind of value cannot be used as the operand to an operator yet (see https://github.com/KittyCAD/modeling-app/issues/783)";
679    let op = possible_operands
680        .try_map(|part| {
681            let source_range = SourceRange::from(&part);
682            let expr = match part {
683                // TODO: these should be valid operands eventually,
684                // users should be able to run "let x = f() + g()"
685                // see https://github.com/KittyCAD/modeling-app/issues/783
686                Expr::FunctionExpression(_)
687                | Expr::PipeExpression(_)
688                | Expr::PipeSubstitution(_)
689                | Expr::LabelledExpression(..) => return Err(CompilationError::fatal(source_range, TODO_783)),
690                Expr::None(_) => {
691                    return Err(CompilationError::fatal(
692                        source_range,
693                        // TODO: Better error message here.
694                        // Once we have ways to use None values (e.g. by replacing with a default value)
695                        // we should suggest one of them here.
696                        "cannot use a KCL None value as an operand",
697                    ));
698                }
699                Expr::TagDeclarator(_) => {
700                    return Err(CompilationError::fatal(
701                        source_range,
702                        // TODO: Better error message here.
703                        // Once we have ways to use None values (e.g. by replacing with a default value)
704                        // we should suggest one of them here.
705                        "cannot use a KCL tag declaration as an operand",
706                    ));
707                }
708                Expr::UnaryExpression(x) => BinaryPart::UnaryExpression(x),
709                Expr::Literal(x) => BinaryPart::Literal(x),
710                Expr::Name(x) => BinaryPart::Name(x),
711                Expr::BinaryExpression(x) => BinaryPart::BinaryExpression(x),
712                Expr::CallExpressionKw(x) => BinaryPart::CallExpressionKw(x),
713                Expr::MemberExpression(x) => BinaryPart::MemberExpression(x),
714                Expr::ArrayExpression(x) => BinaryPart::ArrayExpression(x),
715                Expr::ArrayRangeExpression(x) => BinaryPart::ArrayRangeExpression(x),
716                Expr::ObjectExpression(x) => BinaryPart::ObjectExpression(x),
717                Expr::IfExpression(x) => BinaryPart::IfExpression(x),
718                Expr::AscribedExpression(x) => BinaryPart::AscribedExpression(x),
719            };
720            Ok(expr)
721        })
722        .context(expected("an operand (a value which can be used with an operator)"))
723        .parse_next(i)?;
724    Ok(op)
725}
726
727impl TokenType {
728    fn parse_from(self, i: &mut TokenSlice) -> ModalResult<Token> {
729        any.try_map(|token: Token| {
730            if token.token_type == self {
731                Ok(token)
732            } else {
733                Err(CompilationError::fatal(
734                    token.as_source_range(),
735                    format!(
736                        "expected {self} but found {} which is a {}",
737                        token.value.as_str(),
738                        token.token_type
739                    ),
740                ))
741            }
742        })
743        .parse_next(i)
744    }
745}
746
747/// Parse some whitespace (i.e. at least one whitespace token)
748fn whitespace(i: &mut TokenSlice) -> ModalResult<Vec<Token>> {
749    repeat(
750        1..,
751        any.try_map(|token: Token| {
752            if token.token_type == TokenType::Whitespace {
753                Ok(token)
754            } else {
755                Err(CompilationError::fatal(
756                    token.as_source_range(),
757                    format!(
758                        "expected whitespace, found '{}' which is {}",
759                        token.value.as_str(),
760                        token.token_type
761                    ),
762                ))
763            }
764        }),
765    )
766    .context(expected("some whitespace (e.g. spaces, tabs, new lines)"))
767    .parse_next(i)
768}
769
770/// A shebang is a line at the start of a file that starts with `#!`.
771/// If the shebang is present it takes up the whole line.
772fn shebang(i: &mut TokenSlice) -> ModalResult<Node<Shebang>> {
773    // Parse the hash and the bang.
774    hash.parse_next(i)?;
775    let tok = bang.parse_next(i)?;
776    // Get the rest of the line.
777    // Parse everything until the next newline.
778    let tokens = take_till(0.., |token: Token| token.value.contains('\n')).parse_next(i)?;
779    let value = tokens.iter().map(|t| t.value.as_str()).collect::<String>();
780
781    if tokens.is_empty() {
782        return Err(ErrMode::Cut(
783            CompilationError::fatal(tok.as_source_range(), "expected a shebang value after #!").into(),
784        ));
785    }
786
787    // Strip all the whitespace after the shebang.
788    opt(whitespace).parse_next(i)?;
789
790    Ok(Node::new(
791        Shebang::new(format!("#!{value}")),
792        0,
793        tokens.last().unwrap().end,
794        tokens.first().unwrap().module_id,
795    ))
796}
797
798#[allow(clippy::large_enum_variant)]
799pub enum NonCodeOr<T> {
800    NonCode(Node<NonCodeNode>),
801    Code(T),
802}
803
804/// Parse a KCL array of elements.
805fn array(i: &mut TokenSlice) -> ModalResult<Expr> {
806    alt((
807        array_empty.map(Box::new).map(Expr::ArrayExpression),
808        array_end_start.map(Box::new).map(Expr::ArrayRangeExpression),
809        array_elem_by_elem.map(Box::new).map(Expr::ArrayExpression),
810    ))
811    .parse_next(i)
812}
813
814/// Match an empty array.
815fn array_empty(i: &mut TokenSlice) -> ModalResult<Node<ArrayExpression>> {
816    let open = open_bracket(i)?;
817    let start = open.start;
818    ignore_whitespace(i);
819    let end = close_bracket(i)?.end;
820    Ok(Node::new(
821        ArrayExpression {
822            elements: Default::default(),
823            non_code_meta: Default::default(),
824            digest: None,
825        },
826        start,
827        end,
828        open.module_id,
829    ))
830}
831
832/// Match something that separates elements of an array.
833fn array_separator(i: &mut TokenSlice) -> ModalResult<()> {
834    alt((
835        // Normally you need a comma.
836        comma_sep,
837        // But, if the array is ending, no need for a comma.
838        peek(preceded((opt(non_code_node), opt(whitespace)), close_bracket)).void(),
839    ))
840    .parse_next(i)
841}
842
843pub(crate) fn array_elem_by_elem(i: &mut TokenSlice) -> ModalResult<Node<ArrayExpression>> {
844    let open = open_bracket(i)?;
845    let start = open.start;
846    ignore_whitespace(i);
847    let elements: Vec<_> = repeat(
848        0..,
849        alt((
850            terminated(expression.map(NonCodeOr::Code), array_separator),
851            terminated(non_code_node.map(NonCodeOr::NonCode), opt(whitespace)),
852        )),
853    )
854    .context(expected("array contents, a list of elements (like [1, 2, 3])"))
855    .parse_next(i)?;
856    ignore_trailing_comma(i);
857    ignore_whitespace(i);
858
859    let maybe_end = close_bracket(i).map_err(|e| {
860        if let Ok(mut err) = e.clone().into_inner() {
861            let start_range = open.as_source_range();
862            let end_range = i.as_source_range();
863            err.cause = Some(CompilationError::fatal(
864                SourceRange::from([start_range.start(), end_range.start(), end_range.module_id().as_usize()]),
865                "Encountered an unexpected character(s) before finding a closing bracket(`]`) for the array",
866            ));
867            ErrMode::Cut(err)
868        } else {
869            // ErrMode::Incomplete, not sure if it's actually possible to end up with this here
870            e
871        }
872    });
873
874    if maybe_end.is_err() {
875        // if there is a closing bracket at some point, but it wasn't the next token, it's likely that they forgot a comma between some
876        // of the elements
877        let maybe_closing_bracket: ModalResult<((), Token)> = peek(repeat_till(
878            0..,
879            none_of(|token: Token| {
880                // bail out early if we encounter something that is for sure not allowed in an
881                // array, otherwise we could seek to find a closing bracket until the end of the
882                // file
883                RESERVED_WORDS
884                    .keys()
885                    .chain([",,", "{", "}", "["].iter())
886                    .any(|word| *word == token.value)
887            })
888            .void(),
889            one_of(|term: Token| term.value == "]"),
890        ))
891        .parse_next(i);
892        let has_closing_bracket = maybe_closing_bracket.is_ok();
893        if has_closing_bracket {
894            let start_range = i.as_source_range();
895            // safe to unwrap here because we checked it was Ok above
896            let end_range = maybe_closing_bracket.unwrap().1.as_source_range();
897            let e = ContextError {
898                context: vec![],
899                cause: Some(CompilationError::fatal(
900                    SourceRange::from([start_range.start(), end_range.end(), end_range.module_id().as_usize()]),
901                    "Unexpected character encountered. You might be missing a comma in between elements.",
902                )),
903            };
904            return Err(ErrMode::Cut(e));
905        }
906    }
907    let end = maybe_end?.end;
908
909    // Sort the array's elements (i.e. expression nodes) from the noncode nodes.
910    let (elements, non_code_nodes): (Vec<_>, BTreeMap<usize, _>) = elements.into_iter().enumerate().fold(
911        (Vec::new(), BTreeMap::new()),
912        |(mut elements, mut non_code_nodes), (i, e)| {
913            match e {
914                NonCodeOr::NonCode(x) => {
915                    non_code_nodes.insert(i, vec![x]);
916                }
917                NonCodeOr::Code(x) => {
918                    elements.push(x);
919                }
920            }
921            (elements, non_code_nodes)
922        },
923    );
924    let non_code_meta = NonCodeMeta {
925        non_code_nodes,
926        start_nodes: Vec::new(),
927        digest: None,
928    };
929    Ok(Node::new(
930        ArrayExpression {
931            elements,
932            non_code_meta,
933            digest: None,
934        },
935        start,
936        end,
937        open.module_id,
938    ))
939}
940
941fn array_end_start(i: &mut TokenSlice) -> ModalResult<Node<ArrayRangeExpression>> {
942    let open = open_bracket(i)?;
943    let start = open.start;
944    ignore_whitespace(i);
945    let start_element = expression.parse_next(i)?;
946    ignore_whitespace(i);
947    let end_inclusive = alt((end_inclusive_range.map(|_| true), end_exclusive_range.map(|_| false))).parse_next(i)?;
948    ignore_whitespace(i);
949    let end_element = expression.parse_next(i)?;
950    ignore_whitespace(i);
951    let end = close_bracket(i)?.end;
952    Ok(Node::new(
953        ArrayRangeExpression {
954            start_element,
955            end_element,
956            end_inclusive,
957            digest: None,
958        },
959        start,
960        end,
961        open.module_id,
962    ))
963}
964
965fn object_property_same_key_and_val(i: &mut TokenSlice) -> ModalResult<Node<ObjectProperty>> {
966    let key = nameable_identifier.context(expected("the property's key (the name or identifier of the property), e.g. in 'height = 4', 'height' is the property key")).parse_next(i)?;
967    ignore_whitespace(i);
968    Ok(Node::new_node(
969        key.start,
970        key.end,
971        key.module_id,
972        ObjectProperty {
973            value: Expr::Name(Box::new(key.clone().into())),
974            key,
975            digest: None,
976        },
977    ))
978}
979
980fn object_property(i: &mut TokenSlice) -> ModalResult<Node<ObjectProperty>> {
981    let key_token = identifier_or_keyword.context(expected("the property's key (the name or identifier of the property), e.g. in 'height = 4', 'height' is the property key")).parse_next(i)?;
982    ignore_whitespace(i);
983    // Temporarily accept both `:` and `=` for compatibility.
984    let sep = alt((colon, equals))
985        .context(expected(
986            "`=`, which separates the property's key from the value you're setting it to, e.g. 'height = 4'",
987        ))
988        .parse_next(i)?;
989    ignore_whitespace(i);
990    let expr = match expression
991        .context(expected(
992            "the value which you're setting the property to, e.g. in 'height = 4', the value is 4",
993        ))
994        .parse_next(i)
995    {
996        Ok(expr) => expr,
997        Err(_) => {
998            return Err(ErrMode::Cut(
999                CompilationError::fatal(
1000                    SourceRange::from(sep),
1001                    "This property has a label, but no value. Put some value after the equals sign",
1002                )
1003                .into(),
1004            ));
1005        }
1006    };
1007
1008    // Now that we've verified that we can parse everything, ensure that the key
1009    // is valid.  If not, we can cut.
1010    let key = Node::<Identifier>::try_from(key_token).map_err(|comp_err| {
1011        ErrMode::Cut(ContextError {
1012            context: Default::default(),
1013            cause: Some(CompilationError::err(
1014                comp_err.source_range,
1015                KEYWORD_EXPECTING_IDENTIFIER,
1016            )),
1017        })
1018    })?;
1019
1020    let result = Node::new_node(
1021        key.start,
1022        expr.end(),
1023        key.module_id,
1024        ObjectProperty {
1025            key,
1026            value: expr,
1027            digest: None,
1028        },
1029    );
1030
1031    if sep.token_type == TokenType::Colon {
1032        ParseContext::err(
1033            CompilationError::err(
1034                sep.into(),
1035                "Using `:` to initialize objects is deprecated, prefer using `=`.",
1036            )
1037            .with_suggestion("Replace `:` with `=`", " =", None, Tag::Deprecated),
1038        );
1039    }
1040
1041    Ok(result)
1042}
1043
1044/// Match something that separates properties of an object.
1045fn property_separator(i: &mut TokenSlice) -> ModalResult<()> {
1046    alt((
1047        // Normally you need a comma.
1048        comma_sep,
1049        // But, if the object is ending, no need for a comma.
1050        peek(preceded((opt(non_code_node), opt(whitespace)), close_brace)).void(),
1051    ))
1052    .parse_next(i)
1053}
1054
1055/// Match something that separates the labeled arguments of a fn call.
1056/// Returns the source range of the erroneous separator, if any was found.
1057fn labeled_arg_separator(i: &mut TokenSlice) -> ModalResult<Option<SourceRange>> {
1058    alt((
1059        // Normally you need a comma.
1060        comma_sep.map(|_| None),
1061        // But, if the argument list is ending, no need for a comma.
1062        peek(preceded(opt(whitespace), close_paren)).void().map(|_| None),
1063        whitespace.map(|mut tokens| {
1064            // Safe to unwrap here because `whitespace` is guaranteed to return at least 1 whitespace.
1065            let first_token = tokens.pop().unwrap();
1066            Some(SourceRange::from(&first_token))
1067        }),
1068    ))
1069    .parse_next(i)
1070}
1071
1072/// Parse a KCL object value.
1073pub(crate) fn object(i: &mut TokenSlice) -> ModalResult<Node<ObjectExpression>> {
1074    let open = open_brace(i)?;
1075    let start = open.start;
1076    ignore_whitespace(i);
1077    let properties: Vec<_> = repeat(
1078        0..,
1079        alt((
1080            terminated(non_code_node.map(NonCodeOr::NonCode), opt(whitespace)),
1081            terminated(
1082                alt((object_property, object_property_same_key_and_val)),
1083                property_separator,
1084            )
1085            .map(NonCodeOr::Code),
1086        )),
1087    )
1088    .context(expected(
1089        "a comma-separated list of key-value pairs, e.g. 'height = 4, width = 3'",
1090    ))
1091    .parse_next(i)?;
1092    ignore_trailing_comma(i);
1093    ignore_whitespace(i);
1094
1095    let maybe_end = close_brace(i).map_err(|e| {
1096        if let Ok(mut err) = e.clone().into_inner() {
1097            let start_range = open.as_source_range();
1098            let end_range = i.as_source_range();
1099            err.cause = Some(CompilationError::fatal(
1100                SourceRange::from([start_range.start(), end_range.start(), end_range.module_id().as_usize()]),
1101                "Encountered an unexpected character(s) before finding a closing brace(`}`) for the object",
1102            ));
1103            ErrMode::Cut(err)
1104        } else {
1105            // ErrMode::Incomplete, not sure if it's actually possible to end up with this here
1106            e
1107        }
1108    });
1109    if maybe_end.is_err() {
1110        // if there is a closing brace at some point, but it wasn't the next token, it's likely that they forgot a comma between some
1111        // of the properties
1112        let maybe_closing_brace: ModalResult<((), Token)> = peek(repeat_till(
1113            0..,
1114            none_of(|token: Token| {
1115                // bail out early if we encounter something that is for sure not allowed in an
1116                // object, otherwise we could seek to find a closing brace until the end of the
1117                // file
1118                RESERVED_WORDS
1119                    .keys()
1120                    .chain([",,", "[", "]", "{"].iter())
1121                    .any(|word| *word == token.value)
1122            })
1123            .void(),
1124            one_of(|c: Token| c.value == "}"),
1125        ))
1126        .parse_next(i);
1127        let has_closing_brace = maybe_closing_brace.is_ok();
1128        if has_closing_brace {
1129            let start_range = i.as_source_range();
1130            // okay to unwrap here because we checked it was Ok above
1131            let end_range = maybe_closing_brace.unwrap().1.as_source_range();
1132
1133            let e = ContextError {
1134                context: vec![],
1135                cause: Some(CompilationError::fatal(
1136                    SourceRange::from([start_range.start(), end_range.end(), end_range.module_id().as_usize()]),
1137                    "Unexpected character encountered. You might be missing a comma in between properties.",
1138                )),
1139            };
1140            return Err(ErrMode::Cut(e));
1141        }
1142    }
1143
1144    let end = maybe_end?.end;
1145    // Sort the object's properties from the noncode nodes.
1146    let (properties, non_code_nodes): (Vec<_>, BTreeMap<usize, _>) = properties.into_iter().enumerate().fold(
1147        (Vec::new(), BTreeMap::new()),
1148        |(mut properties, mut non_code_nodes), (i, e)| {
1149            match e {
1150                NonCodeOr::NonCode(x) => {
1151                    non_code_nodes.insert(i, vec![x]);
1152                }
1153                NonCodeOr::Code(x) => {
1154                    properties.push(x);
1155                }
1156            }
1157            (properties, non_code_nodes)
1158        },
1159    );
1160
1161    let non_code_meta = NonCodeMeta {
1162        non_code_nodes,
1163        ..Default::default()
1164    };
1165    Ok(Node::new(
1166        ObjectExpression {
1167            properties,
1168            non_code_meta,
1169            digest: None,
1170        },
1171        start,
1172        end,
1173        open.module_id,
1174    ))
1175}
1176
1177/// Parse the % symbol, used to substitute a curried argument from a |> (pipe).
1178fn pipe_sub(i: &mut TokenSlice) -> ModalResult<Node<PipeSubstitution>> {
1179    any.try_map(|token: Token| {
1180        if matches!(token.token_type, TokenType::Operator) && token.value == PIPE_SUBSTITUTION_OPERATOR {
1181            Ok(Node::new(
1182                PipeSubstitution { digest: None },
1183                token.start,
1184                token.end,
1185                token.module_id,
1186            ))
1187        } else {
1188            Err(CompilationError::fatal(
1189                token.as_source_range(),
1190                format!(
1191                    "expected a pipe substitution symbol (%) but found {}",
1192                    token.value.as_str()
1193                ),
1194            ))
1195        }
1196    })
1197    .context(expected("the substitution symbol, %"))
1198    .parse_next(i)
1199}
1200
1201fn else_if(i: &mut TokenSlice) -> ModalResult<Node<ElseIf>> {
1202    let else_ = any
1203        .try_map(|token: Token| {
1204            if matches!(token.token_type, TokenType::Keyword) && token.value == "else" {
1205                Ok(token)
1206            } else {
1207                Err(CompilationError::fatal(
1208                    token.as_source_range(),
1209                    format!("{} is not 'else'", token.value.as_str()),
1210                ))
1211            }
1212        })
1213        .context(expected("the 'else' keyword"))
1214        .parse_next(i)?;
1215    ignore_whitespace(i);
1216    let _if = any
1217        .try_map(|token: Token| {
1218            if matches!(token.token_type, TokenType::Keyword) && token.value == "if" {
1219                Ok(token.start)
1220            } else {
1221                Err(CompilationError::fatal(
1222                    token.as_source_range(),
1223                    format!("{} is not 'if'", token.value.as_str()),
1224                ))
1225            }
1226        })
1227        .context(expected("the 'if' keyword"))
1228        .parse_next(i)?;
1229    ignore_whitespace(i);
1230    let cond = expression(i)?;
1231    ignore_whitespace(i);
1232    let _ = open_brace(i)?;
1233    let then_val = program
1234        .verify(|block| block.ends_with_expr())
1235        .parse_next(i)
1236        .map(Box::new)?;
1237    ignore_whitespace(i);
1238    let end = close_brace(i)?.end;
1239    ignore_whitespace(i);
1240    Ok(Node::new(
1241        ElseIf {
1242            cond,
1243            then_val,
1244            digest: Default::default(),
1245        },
1246        else_.start,
1247        end,
1248        else_.module_id,
1249    ))
1250}
1251
1252fn if_expr(i: &mut TokenSlice) -> ModalResult<BoxNode<IfExpression>> {
1253    let if_ = any
1254        .try_map(|token: Token| {
1255            if matches!(token.token_type, TokenType::Keyword) && token.value == "if" {
1256                Ok(token)
1257            } else {
1258                Err(CompilationError::fatal(
1259                    token.as_source_range(),
1260                    format!("{} is not 'if'", token.value.as_str()),
1261                ))
1262            }
1263        })
1264        .context(expected("the 'if' keyword"))
1265        .parse_next(i)?;
1266    let _ = whitespace(i)?;
1267    let cond = expression(i).map(Box::new)?;
1268    let _ = whitespace(i)?;
1269    let _ = open_brace(i)?;
1270    ignore_whitespace(i);
1271    let then_val = program
1272        .verify(|block| block.ends_with_expr())
1273        .parse_next(i)
1274        .map_err(|e| e.cut())
1275        .map(Box::new)?;
1276    ignore_whitespace(i);
1277    let _ = close_brace(i)?;
1278    ignore_whitespace(i);
1279    let else_ifs: NodeList<_> = repeat(0.., else_if).parse_next(i)?;
1280
1281    ignore_whitespace(i);
1282
1283    // If there's a non-fatal parser error (e.g. a problem with the `else` branch),
1284    // return this after emitting the nonfatal error.
1285    let if_with_no_else = |cond, then_val, else_ifs| {
1286        Ok(Node::boxed(
1287            if_.start,
1288            if_.end,
1289            if_.module_id,
1290            IfExpression {
1291                cond,
1292                then_val,
1293                else_ifs,
1294                final_else: Node::boxed(0, 0, if_.module_id, Default::default()),
1295                digest: Default::default(),
1296            },
1297        ))
1298    };
1299
1300    // Parse the else keyword
1301    let else_: ModalResult<_> = any
1302        .try_map(|token: Token| {
1303            if matches!(token.token_type, TokenType::Keyword) && token.value == "else" {
1304                Ok(token.start)
1305            } else {
1306                Err(CompilationError::fatal(
1307                    token.as_source_range(),
1308                    format!("{} is not 'else'", token.value.as_str()),
1309                ))
1310            }
1311        })
1312        .context(expected("the 'else' keyword"))
1313        .parse_next(i);
1314    let Ok(else_) = else_ else {
1315        ParseContext::err(CompilationError::err(if_.as_source_range(), MISSING_ELSE));
1316        return if_with_no_else(cond, then_val, else_ifs);
1317    };
1318    let else_range = SourceRange::new(else_, else_ + 4, if_.module_id);
1319    ignore_whitespace(i);
1320
1321    // Parse the else clause
1322    if open_brace(i).is_err() {
1323        ParseContext::err(CompilationError::err(else_range, ELSE_STRUCTURE));
1324        return if_with_no_else(cond, then_val, else_ifs);
1325    }
1326    ignore_whitespace(i);
1327    let Ok(final_else) = program.parse_next(i).map(Box::new) else {
1328        ParseContext::err(CompilationError::err(else_range, IF_ELSE_CANNOT_BE_EMPTY));
1329        let _ = opt(close_brace).parse_next(i);
1330        return if_with_no_else(cond, then_val, else_ifs);
1331    };
1332    ignore_whitespace(i);
1333
1334    if final_else.body.is_empty() {
1335        ParseContext::err(CompilationError::err(else_range, IF_ELSE_CANNOT_BE_EMPTY));
1336        let _ = opt(close_brace).parse_next(i);
1337        return if_with_no_else(cond, then_val, else_ifs);
1338    }
1339    if !final_else.ends_with_expr() {
1340        ParseContext::err(CompilationError::err(else_range, ELSE_MUST_END_IN_EXPR));
1341        let _ = opt(close_brace).parse_next(i);
1342        return if_with_no_else(cond, then_val, else_ifs);
1343    }
1344
1345    let end = close_brace(i)?.end;
1346    Ok(Node::boxed(
1347        if_.start,
1348        end,
1349        if_.module_id,
1350        IfExpression {
1351            cond,
1352            then_val,
1353            else_ifs,
1354            final_else,
1355            digest: Default::default(),
1356        },
1357    ))
1358}
1359
1360fn function_expr(i: &mut TokenSlice) -> ModalResult<Expr> {
1361    let fn_tok = opt(fun).parse_next(i)?;
1362    ignore_whitespace(i);
1363    let result = function_decl.parse_next(i)?;
1364    if fn_tok.is_none() {
1365        let err = CompilationError::fatal(result.as_source_range(), "Anonymous function requires `fn` before `(`");
1366        return Err(ErrMode::Cut(err.into()));
1367    }
1368    Ok(Expr::FunctionExpression(Box::new(result)))
1369}
1370
1371// Looks like
1372// (arg0, arg1) {
1373//     const x = arg0 + arg1;
1374//     return x
1375// }
1376fn function_decl(i: &mut TokenSlice) -> ModalResult<Node<FunctionExpression>> {
1377    fn return_type(i: &mut TokenSlice) -> ModalResult<Node<Type>> {
1378        colon(i)?;
1379        ignore_whitespace(i);
1380        type_(i)
1381    }
1382
1383    let open = open_paren(i)?;
1384    let start = open.start;
1385    let params = parameters(i)?;
1386    close_paren(i)?;
1387    ignore_whitespace(i);
1388    // Optional return type.
1389    let return_type = opt(return_type).parse_next(i)?;
1390    ignore_whitespace(i);
1391    let brace = open_brace(i)?;
1392    let close: Option<(Vec<Vec<Token>>, Token)> = opt((repeat(0.., whitespace), close_brace)).parse_next(i)?;
1393    let (body, end) = match close {
1394        Some((_, end)) => (
1395            Node::new(Program::default(), brace.end, brace.end, brace.module_id),
1396            end.end,
1397        ),
1398        None => (function_body(i)?, close_brace(i)?.end),
1399    };
1400    let result = Node::new(
1401        FunctionExpression {
1402            params,
1403            body,
1404            return_type,
1405            digest: None,
1406        },
1407        start,
1408        end,
1409        open.module_id,
1410    );
1411
1412    Ok(result)
1413}
1414
1415/// E.g. `person.name`
1416fn member_expression_dot(i: &mut TokenSlice) -> ModalResult<(Expr, usize, bool)> {
1417    period.parse_next(i)?;
1418    let property = nameable_identifier
1419        .map(Box::new)
1420        .map(|p| {
1421            let ni: Node<Identifier> = *p;
1422            let nn: Node<Name> = ni.into();
1423            Expr::Name(Box::new(nn))
1424        })
1425        .parse_next(i)?;
1426    let end = property.end();
1427    let computed = false;
1428    Ok((property, end, computed))
1429}
1430
1431fn member_expression_subscript(i: &mut TokenSlice) -> ModalResult<(Expr, usize, bool)> {
1432    let _ = open_bracket.parse_next(i)?;
1433    ignore_whitespace(i);
1434    let property = expression.parse_next(i)?;
1435    ignore_whitespace(i);
1436    let end = close_bracket.parse_next(i)?.end;
1437    let computed = true;
1438    Ok((property, end, computed))
1439}
1440
1441fn find_members(i: &mut TokenSlice) -> ModalResult<Vec<(Expr, usize, bool)>> {
1442    // Now a sequence of members.
1443    let member = alt((member_expression_dot, member_expression_subscript)).context(expected("a member/property, e.g. size.x and size['height'] and size[0] are all different ways to access a member/property of 'size'"));
1444    repeat(1.., member)
1445        .context(expected("a sequence of at least one members/properties"))
1446        .parse_next(i)
1447}
1448
1449/// Get a property of an object, or an index of an array, or a member of a collection.
1450/// Can be arbitrarily nested, e.g. `people[i]['adam'].age`.
1451fn build_member_expression(object: Expr, mut members: Vec<(Expr, usize, bool)>) -> Node<MemberExpression> {
1452    // Process the first member.
1453    // It's safe to call remove(0), because the vec is created from repeat(1..),
1454    // which is guaranteed to have >=1 elements.
1455    let (property, end, computed) = members.remove(0);
1456    let start = object.start();
1457    let module_id = object.module_id();
1458    let initial_member_expression = Node::new(
1459        MemberExpression {
1460            object,
1461            computed,
1462            property,
1463            digest: None,
1464        },
1465        start,
1466        end,
1467        module_id,
1468    );
1469
1470    // Each remaining member wraps the current member expression inside another member expression.
1471    members
1472        .into_iter()
1473        // Take the accumulated member expression from the previous iteration,
1474        // and use it as the `object` of a new, bigger member expression.
1475        .fold(initial_member_expression, |accumulated, (property, end, computed)| {
1476            Node::new(
1477                MemberExpression {
1478                    object: Expr::MemberExpression(Box::new(accumulated)),
1479                    computed,
1480                    property,
1481                    digest: None,
1482                },
1483                start,
1484                end,
1485                module_id,
1486            )
1487        })
1488}
1489
1490/// Find a noncode node which occurs just after a body item,
1491/// such that if the noncode item is a comment, it might be an inline comment.
1492fn noncode_just_after_code(i: &mut TokenSlice) -> ModalResult<Node<NonCodeNode>> {
1493    let ws = opt(whitespace).parse_next(i)?;
1494
1495    // What is the preceding whitespace like?
1496    let (has_newline, has_empty_line) = if let Some(ref ws) = ws {
1497        (
1498            ws.iter().any(|token| token.value.contains('\n')),
1499            ws.iter().any(|token| count_in('\n', &token.value) >= 2),
1500        )
1501    } else {
1502        (false, false)
1503    };
1504
1505    // Look for a non-code node (e.g. comment)
1506    let nc = non_code_node_no_leading_whitespace
1507        .map(|nc| {
1508            if has_empty_line {
1509                // There's an empty line between the body item and the comment,
1510                // This means the comment is a NewLineBlockComment!
1511                let value = match nc.inner.value {
1512                    // Change block comments to inline, as discussed above
1513                    NonCodeValue::BlockComment { value, style } => NonCodeValue::NewLineBlockComment { value, style },
1514                    // Other variants don't need to change.
1515                    x @ NonCodeValue::InlineComment { .. } => x,
1516                    x @ NonCodeValue::NewLineBlockComment { .. } => x,
1517                    x @ NonCodeValue::NewLine => x,
1518                };
1519                Node::new(
1520                    NonCodeNode { value, ..nc.inner },
1521                    nc.start.saturating_sub(1),
1522                    nc.end,
1523                    nc.module_id,
1524                )
1525            } else if has_newline {
1526                // Nothing has to change, a single newline does not need preserving.
1527                nc
1528            } else {
1529                // There's no newline between the body item and comment,
1530                // so if this is a comment, it must be inline with code.
1531                let value = match nc.inner.value {
1532                    // Change block comments to inline, as discussed above
1533                    NonCodeValue::BlockComment { value, style } => NonCodeValue::InlineComment { value, style },
1534                    // Other variants don't need to change.
1535                    x @ NonCodeValue::InlineComment { .. } => x,
1536                    x @ NonCodeValue::NewLineBlockComment { .. } => x,
1537                    x @ NonCodeValue::NewLine => x,
1538                };
1539                Node::new(NonCodeNode { value, ..nc.inner }, nc.start, nc.end, nc.module_id)
1540            }
1541        })
1542        .map(|nc| Node::new(nc.inner, nc.start.saturating_sub(1), nc.end, nc.module_id))
1543        .parse_next(i)?;
1544    Ok(nc)
1545}
1546
1547// the large_enum_variant lint below introduces a LOT of code complexity in a
1548// match!() that's super clean that isn't worth it for the marginal space
1549// savings. revisit if that's a lie.
1550#[derive(Debug)]
1551#[allow(clippy::large_enum_variant)]
1552enum WithinFunction {
1553    Annotation(Node<Annotation>),
1554    BodyItem((BodyItem, Option<Node<NonCodeNode>>)),
1555    NonCode(Node<NonCodeNode>),
1556}
1557
1558impl WithinFunction {
1559    fn is_newline(&self) -> bool {
1560        match self {
1561            WithinFunction::NonCode(nc) => nc.value == NonCodeValue::NewLine,
1562            _ => false,
1563        }
1564    }
1565}
1566
1567fn body_items_within_function(i: &mut TokenSlice) -> ModalResult<WithinFunction> {
1568    // Any of the body item variants, each of which can optionally be followed by a comment.
1569    // If there is a comment, it may be preceded by whitespace.
1570    let item = dispatch! {peek(any);
1571        token if token.visibility_keyword().is_some() => (alt((import_stmt.map(BodyItem::ImportStatement), ty_decl.map(BodyItem::TypeDeclaration), declaration.map(BodyItem::VariableDeclaration))), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
1572        token if token.value == "type" && matches!(token.token_type, TokenType::Keyword) =>
1573            (ty_decl.map(BodyItem::TypeDeclaration), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
1574        token if token.declaration_keyword().is_some() =>
1575            (declaration.map(BodyItem::VariableDeclaration), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
1576        token if token.value == "import" && matches!(token.token_type, TokenType::Keyword) =>
1577            (import_stmt.map(BodyItem::ImportStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
1578        Token { ref value, .. } if value == "return" =>
1579            (return_stmt.map(BodyItem::ReturnStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
1580        token if !token.is_code_token() => {
1581            non_code_node.map(WithinFunction::NonCode)
1582        },
1583        token if token.token_type == TokenType::At => {
1584            annotation.map(WithinFunction::Annotation)
1585        },
1586        _ =>
1587            alt((
1588                (
1589                    declaration.map(BodyItem::VariableDeclaration),
1590                    opt(noncode_just_after_code)
1591                ).map(WithinFunction::BodyItem),
1592                (
1593                    expression_stmt.map(BodyItem::ExpressionStatement),
1594                    opt(noncode_just_after_code)
1595                ).map(WithinFunction::BodyItem),
1596            ))
1597    }
1598    .context(expected("a function body items (functions are made up of variable declarations, expressions, and return statements, each of those is a possible body item"))
1599    .parse_next(i)?;
1600    Ok(item)
1601}
1602
1603/// Parse the body of a user-defined function.
1604fn function_body(i: &mut TokenSlice) -> ModalResult<Node<Program>> {
1605    let leading_whitespace_start = alt((
1606        peek(non_code_node).map(|_| None),
1607        // Subtract 1 from `t.start` to match behaviour of the old parser.
1608        // Consider removing the -1 in the future because I think it's inaccurate, but for now,
1609        // I prefer to match the old parser exactly when I can.
1610        opt(whitespace).map(|tok| tok.and_then(|t| t.first().map(|t| (t.start.saturating_sub(1), t.module_id)))),
1611    ))
1612    .parse_next(i)?;
1613
1614    let mut things_within_body = Vec::new();
1615    // Parse the first item
1616    things_within_body.push(body_items_within_function.parse_next(i)?);
1617
1618    // This loop is complicated! I'm sorry!
1619    // It's almost identical to the loop in `winnow::combinator::separated1`,
1620    // see <https://docs.rs/winnow/latest/winnow/combinator/fn.separated1.html>,
1621    // where the "main" parser is body_items_within_function and the `sep` (separator) parser is
1622    // ws_with_newline.
1623    //
1624    // Except for one thing.
1625    //
1626    // In this case, one of the body items being matched could be a whitespace with a newline,
1627    // and that could _also_ be the separator.
1628    //
1629    // So, if both the main parser and the `sep` parser within `separated1` try to match the same
1630    // token, the main parser will consume it and then the `sep` parser will fail.
1631    //
1632    // The solution is that this parser should check if the last matched body item was an empty line,
1633    // and if so, then ignore the separator parser for the current iteration.
1634    loop {
1635        let last_match_was_empty_line = things_within_body.last().map(|wf| wf.is_newline()).unwrap_or(false);
1636
1637        use winnow::stream::Stream;
1638
1639        let start = i.checkpoint();
1640        let len = i.eof_offset();
1641
1642        let found_ws = ws_with_newline.parse_next(i);
1643
1644        // The separator whitespace might be important:
1645        // if it has an empty line, it should be considered a noncode token, because the user
1646        // deliberately put an empty line there. We should track this and preserve it.
1647        if let Ok(ref ws_token) = found_ws
1648            && (ws_token.value.contains("\n\n") || ws_token.value.contains("\n\r\n"))
1649        {
1650            things_within_body.push(WithinFunction::NonCode(Node::new(
1651                NonCodeNode {
1652                    value: NonCodeValue::NewLine,
1653                    digest: None,
1654                },
1655                ws_token.start,
1656                ws_token.end,
1657                ws_token.module_id,
1658            )));
1659        }
1660
1661        match (found_ws, last_match_was_empty_line) {
1662            (Ok(_), _) | (_, true) => {
1663                // Infinite loop check: this loop must always consume tokens from the input.
1664                // That can either happen through the `sep` parser (i.e. ws_with_newline) or through
1665                // the main parser (body_items_within_function).
1666                // LHS of this checks fht
1667                if i.eof_offset() == len && !last_match_was_empty_line {
1668                    use winnow::error::ParserError;
1669                    return Err(ErrMode::assert(i, "sep parsers must always consume"));
1670                }
1671
1672                match body_items_within_function.parse_next(i) {
1673                    Err(ErrMode::Backtrack(_)) => {
1674                        i.reset(&start);
1675                        break;
1676                    }
1677                    Err(e) => return Err(e),
1678                    Ok(o) => {
1679                        things_within_body.push(o);
1680                    }
1681                }
1682            }
1683            (Err(ErrMode::Backtrack(_)), _) => {
1684                i.reset(&start);
1685                break;
1686            }
1687            (Err(e), _) => return Err(e),
1688        }
1689    }
1690
1691    let mut body = Vec::new();
1692    let mut inner_attrs = Vec::new();
1693    let mut pending_attrs = Vec::new();
1694    let mut non_code_meta = NonCodeMeta::default();
1695    let mut pending_non_code: Vec<Node<NonCodeNode>> = Vec::new();
1696    let mut end = 0;
1697    let mut start = leading_whitespace_start;
1698
1699    macro_rules! handle_pending_non_code {
1700        ($node: ident) => {
1701            if !pending_non_code.is_empty() {
1702                let start = pending_non_code[0].start;
1703                let force_disoc = matches!(
1704                    &pending_non_code.last().unwrap().inner.value,
1705                    NonCodeValue::NewLine
1706                );
1707                let mut comments = Vec::new();
1708                for nc in pending_non_code {
1709                    match nc.inner.value {
1710                        NonCodeValue::BlockComment { value, style } if !force_disoc => {
1711                            comments.push(style.render_comment(&value));
1712                        }
1713                        NonCodeValue::NewLineBlockComment { value, style } if !force_disoc => {
1714                            if comments.is_empty() && nc.start != 0 {
1715                                comments.push(String::new());
1716                                comments.push(String::new());
1717                            }
1718                            comments.push(style.render_comment(&value));
1719                        }
1720                        NonCodeValue::NewLine if !force_disoc && !comments.is_empty() => {
1721                            comments.push(String::new());
1722                            comments.push(String::new());
1723                        }
1724                        _ => {
1725                            if body.is_empty() {
1726                                non_code_meta.start_nodes.push(nc);
1727                            } else {
1728                                non_code_meta.insert(body.len() - 1, nc);
1729                            }
1730                        }
1731                    }
1732                }
1733                $node.set_comments(comments, start);
1734                pending_non_code = Vec::new();
1735            }
1736        };
1737    }
1738
1739    for thing_in_body in things_within_body {
1740        match thing_in_body {
1741            WithinFunction::Annotation(mut attr) => {
1742                if start.is_none() {
1743                    start = Some((attr.start, attr.module_id))
1744                }
1745                handle_pending_non_code!(attr);
1746                if attr.is_inner() {
1747                    if !body.is_empty() {
1748                        ParseContext::warn(CompilationError::err(
1749                            attr.as_source_range(),
1750                            "Named attributes should appear before any declarations or expressions.\n\nBecause named attributes apply to the whole function or module, including code written before them, it can be confusing for readers to not have these attributes at the top of code blocks.",
1751                        ));
1752                    }
1753                    inner_attrs.push(attr);
1754                } else {
1755                    pending_attrs.push(attr);
1756                }
1757            }
1758            WithinFunction::BodyItem((mut b, maybe_noncode)) => {
1759                if start.is_none() {
1760                    start = Some((b.start(), b.module_id()));
1761                }
1762                end = b.end();
1763                if !pending_attrs.is_empty() {
1764                    b.set_attrs(pending_attrs);
1765                    pending_attrs = Vec::new();
1766                }
1767                handle_pending_non_code!(b);
1768                body.push(b);
1769                if let Some(nc) = maybe_noncode {
1770                    end = nc.end;
1771                    pending_non_code.push(nc);
1772                }
1773            }
1774            WithinFunction::NonCode(nc) => {
1775                if start.is_none() {
1776                    start = Some((nc.start, nc.module_id));
1777                }
1778                end = nc.end;
1779                pending_non_code.push(nc);
1780            }
1781        }
1782    }
1783
1784    let start = start.expect(
1785        "the `things_within_body` vec should have looped at least once, and each loop overwrites `start` if it is None",
1786    );
1787
1788    if !pending_attrs.is_empty() {
1789        for a in pending_attrs {
1790            ParseContext::err(CompilationError::err(
1791                a.as_source_range(),
1792                "Attribute is not attached to any item",
1793            ));
1794        }
1795        return Err(ErrMode::Cut(
1796            CompilationError::fatal(
1797                SourceRange::new(start.0, end, start.1),
1798                "Block contains un-attached attributes",
1799            )
1800            .into(),
1801        ));
1802    }
1803
1804    for nc in pending_non_code {
1805        if body.is_empty() {
1806            non_code_meta.start_nodes.push(nc);
1807        } else {
1808            non_code_meta.insert(body.len() - 1, nc);
1809        }
1810    }
1811
1812    // Safe to unwrap `body.first()` because `body` is `separated1` therefore guaranteed
1813    // to have len >= 1.
1814    let end_ws = opt(whitespace)
1815        .parse_next(i)?
1816        .and_then(|ws| ws.first().map(|tok| tok.end));
1817    if let Some(end_ws) = end_ws {
1818        end = end.max(end_ws);
1819    }
1820    end += 1;
1821    Ok(Node::new(
1822        Program {
1823            body,
1824            non_code_meta,
1825            inner_attrs,
1826            shebang: None,
1827            digest: None,
1828        },
1829        start.0,
1830        end,
1831        start.1,
1832    ))
1833}
1834
1835fn import_items(i: &mut TokenSlice) -> ModalResult<NodeList<ImportItem>> {
1836    separated(1.., import_item, comma_sep)
1837        .parse_next(i)
1838        .map_err(|e| e.cut())
1839}
1840
1841fn glob(i: &mut TokenSlice) -> ModalResult<Token> {
1842    one_of((TokenType::Operator, "*"))
1843        .context(expected("the multiple import operator, *"))
1844        .parse_next(i)
1845}
1846
1847pub(super) fn import_stmt(i: &mut TokenSlice) -> ModalResult<BoxNode<ImportStatement>> {
1848    let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace))
1849        .parse_next(i)?
1850        .map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1)));
1851    let import_token = any
1852        .try_map(|token: Token| {
1853            if matches!(token.token_type, TokenType::Keyword) && token.value == "import" {
1854                Ok(token)
1855            } else {
1856                Err(CompilationError::fatal(
1857                    token.as_source_range(),
1858                    format!("{} is not the 'import' keyword", token.value.as_str()),
1859                ))
1860            }
1861        })
1862        .context(expected("the 'import' keyword"))
1863        .parse_next(i)?;
1864
1865    let module_id = import_token.module_id;
1866    let start = visibility_token.unwrap_or(import_token).start;
1867
1868    require_whitespace(i)?;
1869
1870    let (mut selector, path) = alt((
1871        string_literal.map(|s| (ImportSelector::None { alias: None }, Some(s))),
1872        glob.map(|t| {
1873            let s = t.as_source_range();
1874            (
1875                ImportSelector::Glob(Node::new((), s.start(), s.end(), s.module_id())),
1876                None,
1877            )
1878        }),
1879        import_items.map(|items| (ImportSelector::List { items }, None)),
1880    ))
1881    .parse_next(i)?;
1882
1883    let path = match path {
1884        Some(path) => path,
1885        None => {
1886            require_whitespace(i)?;
1887            any.try_map(|token: Token| {
1888                if matches!(token.token_type, TokenType::Keyword | TokenType::Word) && token.value == "from" {
1889                    Ok(())
1890                } else {
1891                    Err(CompilationError::fatal(
1892                        token.as_source_range(),
1893                        format!("{} is not the 'from' keyword", token.value.as_str()),
1894                    ))
1895                }
1896            })
1897            .context(expected("the 'from' keyword"))
1898            .parse_next(i)
1899            .map_err(|e: ErrMode<ContextError>| e.cut())?;
1900
1901            require_whitespace(i)?;
1902
1903            string_literal(i)?
1904        }
1905    };
1906
1907    let mut end: usize = path.end;
1908
1909    if let ImportSelector::None {
1910        alias: ref mut selector_alias,
1911    } = selector
1912        && let Some(alias) = opt(preceded(
1913            (whitespace, import_as_keyword, whitespace),
1914            identifier.context(expected("an identifier to alias the import")),
1915        ))
1916        .parse_next(i)?
1917    {
1918        end = alias.end;
1919        *selector_alias = Some(alias);
1920    }
1921
1922    let path_string = match path.inner.value {
1923        LiteralValue::String(s) => s,
1924        _ => unreachable!(),
1925    };
1926    let path = validate_path_string(
1927        path_string,
1928        selector.exposes_imported_name(),
1929        SourceRange::new(path.start, path.end, path.module_id),
1930    )?;
1931
1932    if matches!(path, ImportPath::Foreign { .. }) && selector.imports_items() {
1933        return Err(ErrMode::Cut(
1934            CompilationError::fatal(
1935                SourceRange::new(start, end, module_id),
1936                "individual items can only be imported from KCL files",
1937            )
1938            .into(),
1939        ));
1940    }
1941
1942    Ok(Node::boxed(
1943        start,
1944        end,
1945        module_id,
1946        ImportStatement {
1947            selector,
1948            visibility,
1949            path,
1950            digest: None,
1951        },
1952    ))
1953}
1954
1955/// Validates the path string in an `import` statement.
1956///
1957/// `var_name` is `true` if the path will be used as a variable name.
1958fn validate_path_string(path_string: String, var_name: bool, path_range: SourceRange) -> ModalResult<ImportPath> {
1959    if path_string.is_empty() {
1960        return Err(ErrMode::Cut(
1961            CompilationError::fatal(path_range, "import path cannot be empty").into(),
1962        ));
1963    }
1964
1965    if var_name
1966        && (path_string.starts_with("_")
1967            || path_string.contains('-')
1968            || path_string.chars().filter(|c| *c == '.').count() > 1)
1969    {
1970        return Err(ErrMode::Cut(
1971            CompilationError::fatal(path_range, "import path is not a valid identifier and must be aliased.").into(),
1972        ));
1973    }
1974
1975    let path = if path_string.ends_with(".kcl") {
1976        if path_string
1977            .chars()
1978            .any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != '.' && c != '/' && c != '\\')
1979        {
1980            return Err(ErrMode::Cut(
1981                CompilationError::fatal(
1982                    path_range,
1983                    "import path may only contain alphanumeric characters, `_`, `-`, `.`, `/`, and `\\`.",
1984                )
1985                .into(),
1986            ));
1987        }
1988
1989        if path_string.starts_with("..") {
1990            return Err(ErrMode::Cut(
1991                CompilationError::fatal(
1992                    path_range,
1993                    "import path may not start with '..'. Cannot traverse to something outside the bounds of your project. If this path is inside your project please find a better way to reference it.",
1994                )
1995                .into(),
1996            ));
1997        }
1998
1999        // Make sure they are not using an absolute path.
2000        if path_string.starts_with('/') || path_string.starts_with('\\') {
2001            return Err(ErrMode::Cut(
2002                CompilationError::fatal(
2003                    path_range,
2004                    "import path may not start with '/' or '\\'. Cannot traverse to something outside the bounds of your project. If this path is inside your project please find a better way to reference it.",
2005                )
2006                .into(),
2007            ));
2008        }
2009
2010        if (path_string.contains('/') || path_string.contains('\\'))
2011            && !(path_string.ends_with("/main.kcl") || path_string.ends_with("\\main.kcl"))
2012        {
2013            return Err(ErrMode::Cut(
2014                CompilationError::fatal(path_range, "import path to a subdirectory must only refer to main.kcl.")
2015                    .into(),
2016            ));
2017        }
2018
2019        ImportPath::Kcl {
2020            filename: TypedPath::new(&path_string),
2021        }
2022    } else if path_string.starts_with("std") {
2023        ParseContext::experimental("explicit imports from the standard library", path_range);
2024
2025        let segments: Vec<String> = path_string.split("::").map(str::to_owned).collect();
2026
2027        for s in &segments {
2028            if s.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') || s.starts_with('_') {
2029                return Err(ErrMode::Cut(
2030                    CompilationError::fatal(path_range, "invalid path in import statement.").into(),
2031                ));
2032            }
2033        }
2034
2035        // For now we only support importing from singly-nested modules inside std.
2036        if segments.len() > 2 {
2037            return Err(ErrMode::Cut(
2038                CompilationError::fatal(
2039                    path_range,
2040                    format!("Invalid import path for import from std: {path_string}."),
2041                )
2042                .into(),
2043            ));
2044        }
2045
2046        ImportPath::Std { path: segments }
2047    } else if path_string.contains('.') {
2048        let extn = std::path::Path::new(&path_string).extension().unwrap_or_default();
2049        if !IMPORT_FILE_EXTENSIONS.contains(&extn.to_string_lossy().to_string()) {
2050            ParseContext::warn(CompilationError::err(
2051                path_range,
2052                format!(
2053                    "unsupported import path format. KCL files can be imported from the current project, CAD files with the following formats are supported: {}",
2054                    IMPORT_FILE_EXTENSIONS.join(", ")
2055                ),
2056            ))
2057        }
2058        ImportPath::Foreign {
2059            path: TypedPath::new(&path_string),
2060        }
2061    } else {
2062        return Err(ErrMode::Cut(
2063            CompilationError::fatal(
2064                path_range,
2065                format!("unsupported import path format. KCL files can be imported from the current project, CAD files with the following formats are supported: {}", IMPORT_FILE_EXTENSIONS.join(", ")),
2066            )
2067            .into(),
2068        ));
2069    };
2070
2071    Ok(path)
2072}
2073
2074fn import_item(i: &mut TokenSlice) -> ModalResult<Node<ImportItem>> {
2075    let name = nameable_identifier
2076        .context(expected("an identifier to import"))
2077        .parse_next(i)?;
2078    let start = name.start;
2079    let module_id = name.module_id;
2080    let alias = opt(preceded(
2081        (whitespace, import_as_keyword, whitespace),
2082        identifier.context(expected("an identifier to alias the import")),
2083    ))
2084    .parse_next(i)?;
2085    let end = if let Some(ref alias) = alias {
2086        alias.end
2087    } else {
2088        name.end
2089    };
2090    Ok(Node::new(
2091        ImportItem {
2092            name,
2093            alias,
2094            digest: None,
2095        },
2096        start,
2097        end,
2098        module_id,
2099    ))
2100}
2101
2102fn import_as_keyword(i: &mut TokenSlice) -> ModalResult<Token> {
2103    any.try_map(|token: Token| {
2104        if matches!(token.token_type, TokenType::Keyword | TokenType::Word) && token.value == "as" {
2105            Ok(token)
2106        } else {
2107            Err(CompilationError::fatal(
2108                token.as_source_range(),
2109                format!("{} is not the 'as' keyword", token.value.as_str()),
2110            ))
2111        }
2112    })
2113    .context(expected("the 'as' keyword"))
2114    .parse_next(i)
2115}
2116
2117/// Parse a return statement of a user-defined function, e.g. `return x`.
2118fn return_stmt(i: &mut TokenSlice) -> ModalResult<Node<ReturnStatement>> {
2119    let ret = any
2120        .try_map(|token: Token| {
2121            if matches!(token.token_type, TokenType::Keyword) && token.value == "return" {
2122                Ok(token)
2123            } else {
2124                Err(CompilationError::fatal(
2125                    token.as_source_range(),
2126                    format!("{} is not a return keyword", token.value.as_str()),
2127                ))
2128            }
2129        })
2130        .context(expected(
2131            "the 'return' keyword, which ends your function (and becomes this function's value when it's called)",
2132        ))
2133        .parse_next(i)?;
2134    require_whitespace(i)?;
2135    let argument = expression(i)?;
2136    Ok(Node::new_node(
2137        ret.start,
2138        argument.end(),
2139        ret.module_id,
2140        ReturnStatement { argument, digest: None },
2141    ))
2142}
2143
2144fn expression_but_not_pipe(i: &mut TokenSlice) -> ModalResult<Expr> {
2145    let mut expr = alt((
2146        binary_expression.map(Box::new).map(Expr::BinaryExpression),
2147        unary_expression.map(Box::new).map(Expr::UnaryExpression),
2148        expr_allowed_in_pipe_expr,
2149    ))
2150    .context(expected("a KCL value"))
2151    .parse_next(i)?;
2152
2153    let ty = opt((colon, opt(whitespace), type_)).parse_next(i)?;
2154    if let Some((_, _, ty)) = ty {
2155        expr = Expr::AscribedExpression(Box::new(AscribedExpression::new(expr, ty)))
2156    }
2157    let label = opt(label).parse_next(i)?;
2158    match label {
2159        Some(label) => Ok(Expr::LabelledExpression(Box::new(LabelledExpression::new(expr, label)))),
2160        None => Ok(expr),
2161    }
2162}
2163
2164fn label(i: &mut TokenSlice) -> ModalResult<Node<Identifier>> {
2165    let result = preceded(
2166        (whitespace, import_as_keyword, whitespace),
2167        identifier.context(expected("an identifier")),
2168    )
2169    .parse_next(i)?;
2170
2171    ParseContext::experimental(
2172        "`as` for tagging expressions",
2173        SourceRange::new(result.start, result.end, result.module_id),
2174    );
2175
2176    Ok(result)
2177}
2178
2179fn unnecessarily_bracketed(i: &mut TokenSlice) -> ModalResult<Expr> {
2180    delimited(
2181        terminated(open_paren, opt(whitespace)),
2182        expression,
2183        preceded(opt(whitespace), close_paren),
2184    )
2185    .parse_next(i)
2186}
2187
2188fn expr_allowed_in_pipe_expr(i: &mut TokenSlice) -> ModalResult<Expr> {
2189    let parsed_expr = alt((
2190        bool_value.map(Box::new).map(Expr::Literal),
2191        tag.map(Box::new).map(Expr::TagDeclarator),
2192        literal.map(Expr::Literal),
2193        fn_call_kw.map(Box::new).map(Expr::CallExpressionKw),
2194        name.map(Box::new).map(Expr::Name),
2195        array,
2196        object.map(Box::new).map(Expr::ObjectExpression),
2197        pipe_sub.map(Box::new).map(Expr::PipeSubstitution),
2198        function_expr,
2199        if_expr.map(Expr::IfExpression),
2200        unnecessarily_bracketed,
2201    ))
2202    .context(expected("a KCL expression (but not a pipe expression)"))
2203    .parse_next(i)?;
2204
2205    if let Ok(Some(members)) = opt(find_members).parse_next(i) {
2206        let mem = build_member_expression(parsed_expr, members);
2207        return Ok(Expr::MemberExpression(Box::new(mem)));
2208    }
2209    Ok(parsed_expr)
2210}
2211
2212fn possible_operands(i: &mut TokenSlice) -> ModalResult<Expr> {
2213    let mut expr = alt((
2214        if_expr.map(Expr::IfExpression),
2215        unary_expression.map(Box::new).map(Expr::UnaryExpression),
2216        bool_value.map(Box::new).map(Expr::Literal),
2217        literal.map(Expr::Literal),
2218        fn_call_kw.map(Box::new).map(Expr::CallExpressionKw),
2219        name.map(Box::new).map(Expr::Name),
2220        array,
2221        object.map(Box::new).map(Expr::ObjectExpression),
2222        binary_expr_in_parens.map(Box::new).map(Expr::BinaryExpression),
2223        unnecessarily_bracketed,
2224    ))
2225    .context(expected(
2226        "a KCL value which can be used as an argument/operand to an operator",
2227    ))
2228    .parse_next(i)?;
2229    if let Ok(Some(members)) = opt(find_members).parse_next(i) {
2230        let mem = build_member_expression(expr, members);
2231        expr = Expr::MemberExpression(Box::new(mem));
2232    }
2233
2234    let ty = opt((colon, opt(whitespace), type_)).parse_next(i)?;
2235    if let Some((_, _, ty)) = ty {
2236        expr = Expr::AscribedExpression(Box::new(AscribedExpression::new(expr, ty)))
2237    }
2238
2239    Ok(expr)
2240}
2241
2242/// Parse an item visibility specifier, e.g. export.
2243fn item_visibility(i: &mut TokenSlice) -> ModalResult<(ItemVisibility, Token)> {
2244    any.verify_map(|token: Token| {
2245        if token.token_type == TokenType::Keyword && token.value == "export" {
2246            Some((ItemVisibility::Export, token))
2247        } else {
2248            None
2249        }
2250    })
2251    .context(expected("item visibility, e.g. 'export'"))
2252    .parse_next(i)
2253}
2254
2255fn declaration_keyword(i: &mut TokenSlice) -> ModalResult<(VariableKind, Token)> {
2256    let res = any
2257        .verify_map(|token: Token| token.declaration_keyword().map(|kw| (kw, token)))
2258        .parse_next(i)?;
2259    Ok(res)
2260}
2261
2262/// Parse a variable/constant declaration.
2263fn declaration(i: &mut TokenSlice) -> ModalResult<BoxNode<VariableDeclaration>> {
2264    let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace))
2265        .parse_next(i)?
2266        .map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1)));
2267    let decl_token = opt(declaration_keyword).parse_next(i)?;
2268    if decl_token.is_some() {
2269        // If there was a declaration keyword like `fn`, then it must be followed by some spaces.
2270        // `fnx = ...` is not valid!
2271        require_whitespace(i)?;
2272    }
2273
2274    let id = binding_name
2275        .context(expected(
2276            "an identifier, which becomes name you're binding the value to",
2277        ))
2278        .parse_next(i)?;
2279    let (kind, mut start, dec_end) = if let Some((kind, token)) = &decl_token {
2280        (*kind, token.start, token.end)
2281    } else {
2282        (VariableKind::Const, id.start, id.end)
2283    };
2284    if let Some(token) = visibility_token {
2285        start = token.start;
2286    }
2287
2288    ignore_whitespace(i);
2289
2290    let val =
2291        if kind == VariableKind::Fn {
2292            let eq = opt(equals).parse_next(i)?;
2293            ignore_whitespace(i);
2294
2295            let val = function_decl
2296                .map(Box::new)
2297                .map(Expr::FunctionExpression)
2298                .context(expected("a KCL function expression, like () { return 1 }"))
2299                .parse_next(i);
2300
2301            if let Some(t) = eq {
2302                ParseContext::warn(
2303                    CompilationError::err(t.as_source_range(), "Unnecessary `=` in function declaration")
2304                        .with_suggestion("Remove `=`", "", None, Tag::Unnecessary),
2305                );
2306            }
2307
2308            val
2309        } else {
2310            equals(i)?;
2311            ignore_whitespace(i);
2312
2313            let val = expression
2314                .try_map(|val| {
2315                    // Function bodies can be used if and only if declaring a function.
2316                    // Check the 'if' direction:
2317                    if matches!(val, Expr::FunctionExpression(_)) {
2318                        return Err(CompilationError::fatal(
2319                            SourceRange::new(start, dec_end, id.module_id),
2320                            format!("Expected a `fn` variable kind, found: `{kind}`"),
2321                        ));
2322                    }
2323                    Ok(val)
2324                })
2325                .context(expected("a KCL value, which is being bound to a variable"))
2326                .parse_next(i);
2327
2328            if let Some((_, tok)) = decl_token {
2329                let range_to_remove = SourceRange::new(tok.start, id.start, id.module_id);
2330                ParseContext::err(
2331                    CompilationError::err(
2332                        tok.as_source_range(),
2333                        format!(
2334                            "Using `{}` to declare constants is deprecated; no keyword is required",
2335                            tok.value
2336                        ),
2337                    )
2338                    .with_suggestion(
2339                        format!("Remove `{}`", tok.value),
2340                        "",
2341                        Some(range_to_remove),
2342                        Tag::Deprecated,
2343                    ),
2344                );
2345            }
2346
2347            val
2348        }
2349        .map_err(|e| e.cut())?;
2350
2351    let end = val.end();
2352    let module_id = id.module_id;
2353    Ok(Node::boxed(
2354        start,
2355        end,
2356        module_id,
2357        VariableDeclaration {
2358            declaration: Node::new_node(
2359                id.start,
2360                end,
2361                module_id,
2362                VariableDeclarator {
2363                    id,
2364                    init: val,
2365                    digest: None,
2366                },
2367            ),
2368            visibility,
2369            kind,
2370            digest: None,
2371        },
2372    ))
2373}
2374
2375fn ty_decl(i: &mut TokenSlice) -> ModalResult<BoxNode<TypeDeclaration>> {
2376    let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace))
2377        .parse_next(i)?
2378        .map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1)));
2379
2380    let decl_token = ty(i)?;
2381    let start = visibility_token.map(|t| t.start).unwrap_or_else(|| decl_token.start);
2382    whitespace(i)?;
2383
2384    let name = alt((
2385        fun.map(|t| {
2386            Node::new(
2387                Identifier {
2388                    name: "fn".to_owned(),
2389                    digest: None,
2390                },
2391                t.start,
2392                t.end,
2393                t.module_id,
2394            )
2395        }),
2396        identifier,
2397    ))
2398    .parse_next(i)?;
2399    let mut end = name.end;
2400
2401    let args = if peek((opt(whitespace), open_paren)).parse_next(i).is_ok() {
2402        ignore_whitespace(i);
2403        open_paren(i)?;
2404        ignore_whitespace(i);
2405        let args: Vec<_> = separated(0.., identifier, comma_sep).parse_next(i)?;
2406        ignore_trailing_comma(i);
2407        ignore_whitespace(i);
2408        end = close_paren(i)?.end;
2409        Some(args)
2410    } else {
2411        None
2412    };
2413
2414    let alias = if peek((opt(whitespace), equals)).parse_next(i).is_ok() {
2415        ignore_whitespace(i);
2416        equals(i)?;
2417        ignore_whitespace(i);
2418        let ty = type_(i)?;
2419
2420        ParseContext::experimental("type aliases", ty.as_source_range());
2421
2422        Some(ty)
2423    } else {
2424        None
2425    };
2426
2427    let module_id = name.module_id;
2428    let result = Node::boxed(
2429        start,
2430        end,
2431        module_id,
2432        TypeDeclaration {
2433            name,
2434            args,
2435            alias,
2436            visibility,
2437            digest: None,
2438        },
2439    );
2440
2441    ParseContext::experimental("type declarations", result.as_source_range());
2442
2443    Ok(result)
2444}
2445
2446impl TryFrom<Token> for Node<Identifier> {
2447    type Error = CompilationError;
2448
2449    fn try_from(token: Token) -> Result<Self, Self::Error> {
2450        if token.token_type == TokenType::Word {
2451            Ok(Node::new(
2452                Identifier {
2453                    name: token.value,
2454                    digest: None,
2455                },
2456                token.start,
2457                token.end,
2458                token.module_id,
2459            ))
2460        } else {
2461            Err(CompilationError::fatal(
2462                token.as_source_range(),
2463                format!(
2464                    "Cannot assign a variable to a reserved keyword: {}",
2465                    token.value.as_str()
2466                ),
2467            ))
2468        }
2469    }
2470}
2471
2472/// Parse a KCL identifier (name of a constant/variable/function)
2473fn identifier(i: &mut TokenSlice) -> ModalResult<Node<Identifier>> {
2474    any.try_map(Node::<Identifier>::try_from)
2475        .context(expected("an identifier, e.g. 'width' or 'myPart'"))
2476        .parse_next(i)
2477}
2478
2479fn identifier_or_keyword(i: &mut TokenSlice) -> ModalResult<Token> {
2480    any.verify(|token: &Token| token.token_type == TokenType::Word || token.token_type == TokenType::Keyword)
2481        .context(expected("an identifier or keyword"))
2482        .parse_next(i)
2483}
2484
2485fn nameable_identifier(i: &mut TokenSlice) -> ModalResult<Node<Identifier>> {
2486    let result = identifier.parse_next(i)?;
2487
2488    if !result.is_nameable() {
2489        let desc = if result.name == "_" {
2490            "Underscores"
2491        } else {
2492            "Names with a leading underscore"
2493        };
2494        ParseContext::err(CompilationError::err(
2495            SourceRange::new(result.start, result.end, result.module_id),
2496            format!("{desc} cannot be referred to, only declared."),
2497        ));
2498    }
2499
2500    Ok(result)
2501}
2502
2503fn name(i: &mut TokenSlice) -> ModalResult<Node<Name>> {
2504    let abs_path = opt(double_colon).parse_next(i)?;
2505    let mut idents: NodeList<Identifier> = separated(1.., nameable_identifier, double_colon)
2506        .parse_next(i)
2507        .map_err(|e| e.backtrack())?;
2508
2509    let mut start = idents[0].start;
2510    if let Some(abs_path) = &abs_path {
2511        start = abs_path.start;
2512    }
2513    let abs_path = abs_path.is_some();
2514
2515    let name = idents.pop().unwrap();
2516    let end = name.end;
2517    let module_id = name.module_id;
2518    let result = Node::new(
2519        Name {
2520            name,
2521            path: idents,
2522            abs_path,
2523            digest: None,
2524        },
2525        start,
2526        end,
2527        module_id,
2528    );
2529
2530    if let Some(suggestion) = super::deprecation(&result.to_string(), DeprecationKind::Const) {
2531        ParseContext::warn(
2532            CompilationError::err(
2533                result.as_source_range(),
2534                format!("Using `{result}` is deprecated, prefer using `{suggestion}`."),
2535            )
2536            .with_suggestion(
2537                format!("Replace `{result}` with `{suggestion}`"),
2538                suggestion,
2539                None,
2540                Tag::Deprecated,
2541            ),
2542        );
2543    }
2544
2545    Ok(result)
2546}
2547
2548impl TryFrom<Token> for Node<TagDeclarator> {
2549    type Error = CompilationError;
2550
2551    fn try_from(token: Token) -> Result<Self, Self::Error> {
2552        match token.token_type {
2553            TokenType::Word => {
2554                Ok(Node::new(
2555                    TagDeclarator {
2556                        // We subtract 1 from the start because the tag starts with a `$`.
2557                        name: token.value,
2558                        digest: None,
2559                    },
2560                    token.start - 1,
2561                    token.end,
2562                    token.module_id,
2563                ))
2564            }
2565            TokenType::Number => Err(CompilationError::fatal(
2566                token.as_source_range(),
2567                format!(
2568                    "Tag names must not start with a number. Tag starts with `{}`",
2569                    token.value.as_str()
2570                ),
2571            )),
2572
2573            // e.g. `line(%, $)` or `line(%, $ , 5)`
2574            TokenType::Brace | TokenType::Whitespace | TokenType::Comma => Err(CompilationError::fatal(
2575                token.as_source_range(),
2576                "Tag names must not be empty".to_string(),
2577            )),
2578
2579            TokenType::Type => Err(CompilationError::fatal(
2580                token.as_source_range(),
2581                format!("Cannot assign a tag to a reserved keyword: {}", token.value.as_str()),
2582            )),
2583
2584            _ => Err(CompilationError::fatal(
2585                token.as_source_range(),
2586                // this is `start with` because if most of these cases are in the middle, it ends
2587                // up hitting a different error path(e.g. including a bang) or being valid(e.g. including a comment) since it will get broken up into
2588                // multiple tokens
2589                format!("Tag names must not start with a {}", token.token_type),
2590            )),
2591        }
2592    }
2593}
2594
2595/// Parse a Kcl tag that starts with a `$`.
2596fn tag(i: &mut TokenSlice) -> ModalResult<Node<TagDeclarator>> {
2597    dollar.parse_next(i)?;
2598    any.try_map(Node::<TagDeclarator>::try_from)
2599        .context(expected("a tag, e.g. '$seg01' or '$line01'"))
2600        .parse_next(i)
2601        .map_err(|e: ErrMode<ContextError>| e.cut())
2602}
2603
2604/// Helper function. Matches any number of whitespace tokens and ignores them.
2605fn ignore_whitespace(i: &mut TokenSlice) {
2606    let _: ModalResult<()> = repeat(0.., whitespace).parse_next(i);
2607}
2608
2609// A helper function to ignore a trailing comma.
2610fn ignore_trailing_comma(i: &mut TokenSlice) {
2611    let _ = opt(comma).parse_next(i);
2612}
2613
2614/// Matches at least 1 whitespace.
2615fn require_whitespace(i: &mut TokenSlice) -> ModalResult<()> {
2616    repeat(1.., whitespace).parse_next(i)
2617}
2618
2619fn unary_expression(i: &mut TokenSlice) -> ModalResult<Node<UnaryExpression>> {
2620    const EXPECTED: &str = "expected a unary operator (like '-', the negative-numeric operator),";
2621    let (operator, op_token) = any
2622        .try_map(|token: Token| match token.token_type {
2623            TokenType::Operator if token.value == "-" => Ok((UnaryOperator::Neg, token)),
2624            TokenType::Operator => Err(CompilationError::fatal(
2625                 token.as_source_range(),
2626                 format!("{EXPECTED} but found {} which is an operator, but not a unary one (unary operators apply to just a single operand, your operator applies to two or more operands)", token.value.as_str(),),
2627            )),
2628            TokenType::Bang => Ok((UnaryOperator::Not, token)),
2629            other => Err(CompilationError::fatal(  token.as_source_range(), format!("{EXPECTED} but found {} which is {}", token.value.as_str(), other,) )),
2630        })
2631        .context(expected("a unary expression, e.g. -x or -3"))
2632        .parse_next(i)?;
2633    ignore_whitespace(i);
2634    let argument = operand.parse_next(i)?;
2635    Ok(Node::new_node(
2636        op_token.start,
2637        argument.end(),
2638        op_token.module_id,
2639        UnaryExpression {
2640            operator,
2641            argument,
2642            digest: None,
2643        },
2644    ))
2645}
2646
2647/// Consume tokens that make up a binary expression, but don't actually return them.
2648/// Why not?
2649/// Because this is designed to be used with .take() within the `binary_expression` parser.
2650fn binary_expression_tokens(i: &mut TokenSlice) -> ModalResult<Vec<BinaryExpressionToken>> {
2651    let first = operand.parse_next(i).map(BinaryExpressionToken::from)?;
2652    let remaining: Vec<_> = repeat(
2653        1..,
2654        (
2655            preceded(opt(whitespace), binary_operator).map(BinaryExpressionToken::from),
2656            preceded(opt(whitespace), operand).map(BinaryExpressionToken::from),
2657        ),
2658    )
2659    .context(expected(
2660        "one or more binary operators (like + or -) and operands for them, e.g. 1 + 2 - 3",
2661    ))
2662    .parse_next(i)?;
2663    let mut out = Vec::with_capacity(1 + 2 * remaining.len());
2664    out.push(first);
2665    out.extend(remaining.into_iter().flat_map(|(a, b)| [a, b]));
2666    Ok(out)
2667}
2668
2669/// Parse an infix binary expression.
2670fn binary_expression(i: &mut TokenSlice) -> ModalResult<Node<BinaryExpression>> {
2671    // Find the slice of tokens which makes up the binary expression
2672    let tokens = binary_expression_tokens.parse_next(i)?;
2673
2674    // Pass the token slice into the specialized math parser, for things like
2675    // precedence and converting infix operations to an AST.
2676    let expr = super::math::parse(tokens).map_err(|e| ErrMode::Backtrack(e.into()))?;
2677    Ok(expr)
2678}
2679
2680fn binary_expr_in_parens(i: &mut TokenSlice) -> ModalResult<Node<BinaryExpression>> {
2681    let span_with_brackets = bracketed_section.take().parse_next(i)?;
2682    let mut span_no_brackets = span_with_brackets.without_ends();
2683    let expr = binary_expression.parse_next(&mut span_no_brackets)?;
2684    Ok(expr)
2685}
2686
2687/// Match a starting bracket, then match to the corresponding end bracket.
2688/// Return the count of how many tokens are in that span
2689/// (not including the bracket tokens).
2690fn bracketed_section(i: &mut TokenSlice) -> ModalResult<usize> {
2691    // Find the start of this bracketed expression.
2692    let _ = open_paren.parse_next(i)?;
2693    let mut opened_braces = 1usize;
2694    let mut tokens_examined = 0;
2695    while opened_braces > 0 {
2696        let tok = any.parse_next(i)?;
2697        tokens_examined += 1;
2698        if matches!(tok.token_type, TokenType::Brace) {
2699            if tok.value == "(" {
2700                opened_braces += 1;
2701            } else if tok.value == ")" {
2702                opened_braces -= 1;
2703            }
2704        }
2705    }
2706    Ok(tokens_examined)
2707}
2708
2709/// Parse a KCL expression statement.
2710fn expression_stmt(i: &mut TokenSlice) -> ModalResult<Node<ExpressionStatement>> {
2711    let val = expression
2712        .context(expected(
2713            "an expression (i.e. a value, or an algorithm for calculating one), e.g. 'x + y' or '3' or 'width * 2'",
2714        ))
2715        .parse_next(i)?;
2716    Ok(Node::new_node(
2717        val.start(),
2718        val.end(),
2719        val.module_id(),
2720        ExpressionStatement {
2721            expression: val,
2722            digest: None,
2723        },
2724    ))
2725}
2726
2727/// Parse the given brace symbol.
2728fn some_brace(symbol: &'static str, i: &mut TokenSlice) -> ModalResult<Token> {
2729    one_of((TokenType::Brace, symbol))
2730        .context(expected(symbol))
2731        .parse_next(i)
2732}
2733
2734/// Parse a |> operator.
2735fn pipe_operator(i: &mut TokenSlice) -> ModalResult<Token> {
2736    one_of((TokenType::Operator, PIPE_OPERATOR))
2737        .context(expected(
2738            "the |> operator, used for 'piping' one function's output into another function's input",
2739        ))
2740        .parse_next(i)
2741}
2742
2743fn ws_with_newline(i: &mut TokenSlice) -> ModalResult<Token> {
2744    one_of(TokenType::Whitespace)
2745        .verify(|token: &Token| token.value.contains('\n'))
2746        .context(expected("a newline, possibly with whitespace"))
2747        .parse_next(i)
2748}
2749
2750/// (
2751fn open_paren(i: &mut TokenSlice) -> ModalResult<Token> {
2752    some_brace("(", i)
2753}
2754
2755/// )
2756fn close_paren(i: &mut TokenSlice) -> ModalResult<Token> {
2757    some_brace(")", i)
2758}
2759
2760/// [
2761fn open_bracket(i: &mut TokenSlice) -> ModalResult<Token> {
2762    some_brace("[", i)
2763}
2764
2765/// ]
2766fn close_bracket(i: &mut TokenSlice) -> ModalResult<Token> {
2767    some_brace("]", i)
2768}
2769
2770/// {
2771fn open_brace(i: &mut TokenSlice) -> ModalResult<Token> {
2772    some_brace("{", i)
2773}
2774
2775/// }
2776fn close_brace(i: &mut TokenSlice) -> ModalResult<Token> {
2777    some_brace("}", i)
2778}
2779
2780fn comma(i: &mut TokenSlice) -> ModalResult<()> {
2781    TokenType::Comma.parse_from(i)?;
2782    Ok(())
2783}
2784
2785fn hash(i: &mut TokenSlice) -> ModalResult<()> {
2786    TokenType::Hash.parse_from(i)?;
2787    Ok(())
2788}
2789
2790fn bang(i: &mut TokenSlice) -> ModalResult<Token> {
2791    TokenType::Bang.parse_from(i)
2792}
2793
2794fn dollar(i: &mut TokenSlice) -> ModalResult<()> {
2795    TokenType::Dollar.parse_from(i)?;
2796    Ok(())
2797}
2798
2799fn period(i: &mut TokenSlice) -> ModalResult<()> {
2800    TokenType::Period.parse_from(i)?;
2801    Ok(())
2802}
2803
2804fn end_inclusive_range(i: &mut TokenSlice) -> ModalResult<Token> {
2805    any.try_map(|token: Token| {
2806        if matches!(token.token_type, TokenType::DoublePeriod) {
2807            Ok(token)
2808        } else {
2809            Err(CompilationError::fatal(
2810                token.as_source_range(),
2811                format!(
2812                    "expected a '..' (double period) found {} which is {}",
2813                    token.value.as_str(),
2814                    token.token_type
2815                ),
2816            ))
2817        }
2818    })
2819    .context(expected("the .. operator, used for array ranges like [0..10]"))
2820    .parse_next(i)
2821}
2822
2823fn end_exclusive_range(i: &mut TokenSlice) -> ModalResult<Token> {
2824    any.try_map(|token: Token| {
2825        if matches!(token.token_type, TokenType::DoublePeriodLessThan) {
2826            Ok(token)
2827        } else {
2828            Err(CompilationError::fatal(
2829                token.as_source_range(),
2830                format!("expected a '..<' but found {}", token.value.as_str()),
2831            ))
2832        }
2833    })
2834    .context(expected("the ..< operator, used for array ranges like [0..<10]"))
2835    .parse_next(i)
2836}
2837
2838fn colon(i: &mut TokenSlice) -> ModalResult<Token> {
2839    TokenType::Colon.parse_from(i)
2840}
2841
2842fn semi_colon(i: &mut TokenSlice) -> ModalResult<Token> {
2843    TokenType::SemiColon.parse_from(i)
2844}
2845
2846fn plus(i: &mut TokenSlice) -> ModalResult<Token> {
2847    one_of((TokenType::Operator, "+")).parse_next(i)
2848}
2849
2850fn double_colon(i: &mut TokenSlice) -> ModalResult<Token> {
2851    TokenType::DoubleColon.parse_from(i)
2852}
2853
2854fn equals(i: &mut TokenSlice) -> ModalResult<Token> {
2855    one_of((TokenType::Operator, "="))
2856        .context(expected("the equals operator, ="))
2857        .parse_next(i)
2858}
2859
2860fn question_mark(i: &mut TokenSlice) -> ModalResult<()> {
2861    TokenType::QuestionMark.parse_from(i)?;
2862    Ok(())
2863}
2864
2865fn at_sign(i: &mut TokenSlice) -> ModalResult<Token> {
2866    TokenType::At.parse_from(i)
2867}
2868
2869fn fun(i: &mut TokenSlice) -> ModalResult<Token> {
2870    keyword(i, "fn")
2871}
2872
2873fn ty(i: &mut TokenSlice) -> ModalResult<Token> {
2874    keyword(i, "type")
2875}
2876
2877fn any_keyword(i: &mut TokenSlice) -> ModalResult<Token> {
2878    any.try_map(|token: Token| match token.token_type {
2879        TokenType::Keyword => Ok(token),
2880        _ => Err(CompilationError::fatal(
2881            token.as_source_range(),
2882            "expected some reserved keyword".to_owned(),
2883        )),
2884    })
2885    .parse_next(i)
2886}
2887
2888fn keyword(i: &mut TokenSlice, expected: &str) -> ModalResult<Token> {
2889    any.try_map(|token: Token| match token.token_type {
2890        TokenType::Keyword if token.value == expected => Ok(token),
2891        _ => Err(CompilationError::fatal(
2892            token.as_source_range(),
2893            format!("expected '{expected}', found {}", token.value.as_str(),),
2894        )),
2895    })
2896    .parse_next(i)
2897}
2898
2899/// Parse a comma, optionally followed by some whitespace.
2900fn comma_sep(i: &mut TokenSlice) -> ModalResult<()> {
2901    (opt(whitespace), comma, opt(whitespace))
2902        .context(expected("a comma, optionally followed by whitespace"))
2903        .parse_next(i)?;
2904    Ok(())
2905}
2906
2907/// Parse a `|`, optionally followed by some whitespace.
2908fn pipe_sep(i: &mut TokenSlice) -> ModalResult<()> {
2909    (opt(whitespace), one_of((TokenType::Operator, "|")), opt(whitespace)).parse_next(i)?;
2910    Ok(())
2911}
2912
2913fn labeled_argument(i: &mut TokenSlice) -> ModalResult<LabeledArg> {
2914    (
2915        opt((
2916            terminated(nameable_identifier, opt(whitespace)),
2917            terminated(one_of((TokenType::Operator, "=")), opt(whitespace)),
2918        )),
2919        expression,
2920    )
2921        .map(|(label, arg)| LabeledArg {
2922            label: label.map(|(l, _)| l),
2923            arg,
2924        })
2925        .parse_next(i)
2926}
2927
2928fn record_ty_field(i: &mut TokenSlice) -> ModalResult<(Node<Identifier>, Node<Type>)> {
2929    (identifier, colon, opt(whitespace), type_)
2930        .map(|(id, _, _, ty)| (id, ty))
2931        .parse_next(i)
2932}
2933
2934/// Parse a type in various positions.
2935fn type_(i: &mut TokenSlice) -> ModalResult<Node<Type>> {
2936    separated(1.., type_not_union, pipe_sep)
2937        .map(|mut tys: Vec<_>| {
2938            if tys.len() == 1 {
2939                tys.pop().unwrap()
2940            } else {
2941                let start = tys[0].start;
2942                let module_id = tys[0].module_id;
2943                let end = tys.last().unwrap().end;
2944                Node::new(Type::Union { tys }, start, end, module_id)
2945            }
2946        })
2947        .parse_next(i)
2948}
2949
2950fn type_not_union(i: &mut TokenSlice) -> ModalResult<Node<Type>> {
2951    alt((
2952        // Object types
2953        (
2954            open_brace,
2955            opt(whitespace),
2956            separated(0.., record_ty_field, comma_sep),
2957            opt(whitespace),
2958            close_brace,
2959        )
2960            .try_map(|(open, _, params, _, close)| {
2961                Ok(Node::new(
2962                    Type::Object { properties: params },
2963                    open.start,
2964                    close.end,
2965                    open.module_id,
2966                ))
2967            }),
2968        // Array types
2969        array_type,
2970        // Primitive types
2971        primitive_type.map(|t| t.map(Type::Primitive)),
2972    ))
2973    .parse_next(i)
2974}
2975
2976fn primitive_type(i: &mut TokenSlice) -> ModalResult<Node<PrimitiveType>> {
2977    alt((
2978        // A function type: `fn` (`(` type?, (id: type,)* `)` (`:` type)?)?
2979        (
2980            fun,
2981            opt((
2982                // `(` type?, (id: type,)* `)`
2983                delimited(
2984                    open_paren,
2985                    opt(alt((
2986                        // type, (id: type,)+
2987                        (
2988                            type_,
2989                            comma,
2990                            opt(whitespace),
2991                            separated(
2992                                1..,
2993                                (identifier, colon, opt(whitespace), type_).map(|(id, _, _, ty)| (id, ty)),
2994                                comma_sep,
2995                            ),
2996                        )
2997                            .map(|(t, _, _, args)| (Some(t), args)),
2998                        // (id: type,)+
2999                        separated(
3000                            1..,
3001                            (identifier, colon, opt(whitespace), type_).map(|(id, _, _, ty)| (id, ty)),
3002                            comma_sep,
3003                        )
3004                        .map(|args| (None, args)),
3005                        // type
3006                        type_.map(|t| (Some(t), Vec::new())),
3007                    ))),
3008                    close_paren,
3009                ),
3010                // `:` type
3011                opt((colon, opt(whitespace), type_)),
3012            )),
3013        )
3014            .map(|(t, tys)| {
3015                let mut ft = FunctionType::empty_fn_type();
3016
3017                if let Some((args, ret)) = tys {
3018                    if let Some((unnamed, named)) = args {
3019                        if let Some(unnamed) = unnamed {
3020                            ft.unnamed_arg = Some(Box::new(unnamed));
3021                        }
3022                        ft.named_args = named;
3023                    }
3024                    if let Some((_, _, ty)) = ret {
3025                        ft.return_type = Some(Box::new(ty));
3026                    }
3027                }
3028
3029                Node::new(PrimitiveType::Function(ft), t.start, t.end, t.module_id)
3030            }),
3031        // A named type, possibly with a numeric suffix.
3032        (identifier, opt(delimited(open_paren, uom_for_type, close_paren))).map(|(ident, suffix)| {
3033            let mut result = Node::new(PrimitiveType::Boolean, ident.start, ident.end, ident.module_id);
3034            result.inner =
3035                PrimitiveType::primitive_from_str(&ident.name, suffix).unwrap_or(PrimitiveType::Named { id: ident });
3036            result
3037        }),
3038    ))
3039    .parse_next(i)
3040}
3041
3042fn array_type(i: &mut TokenSlice) -> ModalResult<Node<Type>> {
3043    fn opt_whitespace(i: &mut TokenSlice) -> ModalResult<()> {
3044        ignore_whitespace(i);
3045        Ok(())
3046    }
3047
3048    open_bracket(i)?;
3049    let ty = type_(i)?;
3050    let len = opt((
3051        semi_colon,
3052        opt_whitespace,
3053        any.try_map(|token: Token| match token.token_type {
3054            TokenType::Number => {
3055                let value = token.uint_value().ok_or_else(|| {
3056                    CompilationError::fatal(
3057                        token.as_source_range(),
3058                        format!("Expected unsigned integer literal, found: {}", token.value),
3059                    )
3060                })?;
3061
3062                Ok(value as usize)
3063            }
3064            _ => Err(CompilationError::fatal(token.as_source_range(), "invalid array length")),
3065        }),
3066        opt(plus),
3067    ))
3068    .parse_next(i)?;
3069    close_bracket(i)?;
3070
3071    let len = if let Some((_, _, n, plus)) = len {
3072        if plus.is_some() {
3073            ArrayLen::Minimum(n)
3074        } else {
3075            ArrayLen::Known(n)
3076        }
3077    } else {
3078        ArrayLen::None
3079    };
3080
3081    Ok(ty.map(|ty| Type::Array { ty: Box::new(ty), len }))
3082}
3083
3084fn uom_for_type(i: &mut TokenSlice) -> ModalResult<NumericSuffix> {
3085    any.try_map(|t: Token| t.value.parse()).parse_next(i)
3086}
3087
3088fn comment(i: &mut TokenSlice) -> ModalResult<Node<String>> {
3089    any.verify_map(|token: Token| {
3090        let value = match token.token_type {
3091            TokenType::LineComment => token.value,
3092            TokenType::BlockComment => token.value,
3093            _ => return None,
3094        };
3095        Some(Node::new(value, token.start, token.end, token.module_id))
3096    })
3097    .context(expected("Comment"))
3098    .parse_next(i)
3099}
3100
3101fn comments(i: &mut TokenSlice) -> ModalResult<Node<Vec<String>>> {
3102    let comments: Vec<Node<String>> = repeat(1.., (comment, opt(whitespace)).map(|(c, _)| c)).parse_next(i)?;
3103    let start = comments[0].start;
3104    let module_id = comments[0].module_id;
3105    let end = comments.last().unwrap().end;
3106    let inner = comments.into_iter().map(|n| n.inner).collect();
3107    Ok(Node::new(inner, start, end, module_id))
3108}
3109
3110struct ParamDescription {
3111    labeled: bool,
3112    arg_name: Token,
3113    type_: std::option::Option<Node<Type>>,
3114    default_value: Option<DefaultParamVal>,
3115    attr: Option<Node<Annotation>>,
3116    comments: Option<Node<Vec<String>>>,
3117}
3118
3119fn parameter(i: &mut TokenSlice) -> ModalResult<ParamDescription> {
3120    let (_, comments, _, attr, _, found_at_sign, arg_name, question_mark, _, type_, _ws, default_literal) = (
3121        opt(whitespace),
3122        opt(comments),
3123        opt(whitespace),
3124        opt(outer_annotation),
3125        opt(whitespace),
3126        opt(at_sign),
3127        any.verify(|token: &Token| !matches!(token.token_type, TokenType::Brace) || token.value != ")"),
3128        opt(question_mark),
3129        opt(whitespace),
3130        opt((colon, opt(whitespace), type_).map(|tup| tup.2)),
3131        opt(whitespace),
3132        opt((equals, opt(whitespace), literal).map(|(_, _, literal)| literal)),
3133    )
3134        .parse_next(i)?;
3135
3136    Ok(ParamDescription {
3137        labeled: found_at_sign.is_none(),
3138        arg_name,
3139        type_,
3140        default_value: match (question_mark.is_some(), default_literal) {
3141            (true, Some(lit)) => Some(DefaultParamVal::Literal(*lit)),
3142            (true, None) => Some(DefaultParamVal::none()),
3143            (false, None) => None,
3144            (false, Some(lit)) => {
3145                let msg = "You're trying to set a default value for an argument, but only optional arguments can have default values, and this argument is mandatory. Try putting a ? after the argument name, to make the argument optional.";
3146                let e = CompilationError::fatal((&lit).into(), msg);
3147                return Err(ErrMode::Backtrack(ContextError::from(e)));
3148            }
3149        },
3150        attr,
3151        comments,
3152    })
3153}
3154
3155/// Parameters are declared in a function signature, and used within a function.
3156fn parameters(i: &mut TokenSlice) -> ModalResult<Vec<Parameter>> {
3157    // Get all tokens until the next ), because that ends the parameter list.
3158    let candidates: Vec<_> = separated(0.., parameter, comma_sep)
3159        .context(expected("function parameters"))
3160        .parse_next(i)?;
3161    opt(comma_sep).parse_next(i)?;
3162
3163    // Make sure all those tokens are valid parameters.
3164    let params: Vec<Parameter> = candidates
3165        .into_iter()
3166        .map(
3167            |ParamDescription {
3168                 labeled,
3169                 arg_name,
3170                 type_,
3171                 default_value,
3172                 attr,
3173                 comments,
3174             }| {
3175                let mut identifier = Node::<Identifier>::try_from(arg_name)?;
3176                if let Some(comments) = comments {
3177                    identifier.comment_start = comments.start;
3178                    identifier.pre_comments = comments.inner;
3179                }
3180                if let Some(attr) = attr {
3181                    identifier.outer_attrs.push(attr);
3182                }
3183
3184                Ok(Parameter {
3185                    identifier,
3186                    type_,
3187                    default_value,
3188                    labeled,
3189                    digest: None,
3190                })
3191            },
3192        )
3193        .collect::<Result<_, _>>()
3194        .map_err(|e: CompilationError| ErrMode::Backtrack(ContextError::from(e)))?;
3195
3196    // Make sure the only unlabeled parameter is the first one.
3197    if let Some(param) = params.iter().skip(1).find(|param| !param.labeled) {
3198        let source_range = SourceRange::from(param);
3199        return Err(ErrMode::Cut(ContextError::from(CompilationError::fatal(
3200            source_range,
3201            "Only the first parameter can be declared unlabeled",
3202        ))));
3203    }
3204
3205    // Make sure optional parameters are last.
3206    if let Err(e) = optional_after_required(&params) {
3207        return Err(ErrMode::Cut(ContextError::from(e)));
3208    }
3209    Ok(params)
3210}
3211
3212fn optional_after_required(params: &[Parameter]) -> Result<(), CompilationError> {
3213    let mut found_optional = false;
3214    for p in params {
3215        if p.optional() {
3216            found_optional = true;
3217        }
3218        if !p.optional() && found_optional {
3219            let e = CompilationError::fatal(
3220                (&p.identifier).into(),
3221                "mandatory parameters must be declared before optional parameters",
3222            );
3223            return Err(e);
3224        }
3225    }
3226    Ok(())
3227}
3228
3229/// Introduce a new name, which binds some value.
3230fn binding_name(i: &mut TokenSlice) -> ModalResult<Node<Identifier>> {
3231    identifier
3232        .context(expected("an identifier, which will be the name of some value"))
3233        .parse_next(i)
3234}
3235
3236fn labelled_fn_call(i: &mut TokenSlice) -> ModalResult<Expr> {
3237    let expr = fn_call_kw.map(Box::new).map(Expr::CallExpressionKw).parse_next(i)?;
3238
3239    let label = opt(label).parse_next(i)?;
3240    match label {
3241        Some(label) => Ok(Expr::LabelledExpression(Box::new(LabelledExpression::new(expr, label)))),
3242        None => Ok(expr),
3243    }
3244}
3245
3246fn fn_call_kw(i: &mut TokenSlice) -> ModalResult<Node<CallExpressionKw>> {
3247    let fn_name = name(i)?;
3248    opt(whitespace).parse_next(i)?;
3249    let _ = open_paren.parse_next(i)?;
3250    ignore_whitespace(i);
3251
3252    // Special case: no args or one arg (unlabeled)
3253    let checkpoint = i.checkpoint();
3254    let mut initial_unlabeled_arg = opt(expression).parse_next(i)?;
3255
3256    // A label would parse as an expression, but if there's an `=` following then it is more likely
3257    // to be a labelled arg.
3258    if peek((opt(whitespace), equals)).parse_next(i).is_ok() {
3259        i.reset(&checkpoint);
3260        initial_unlabeled_arg = None;
3261    } else {
3262        let early_close = peek((opt(whitespace), close_paren)).parse_next(i);
3263        if early_close.is_ok() {
3264            ignore_whitespace(i);
3265            let end = close_paren.parse_next(i)?.end;
3266            let result = Node::new_node(
3267                fn_name.start,
3268                end,
3269                fn_name.module_id,
3270                CallExpressionKw {
3271                    callee: fn_name,
3272                    unlabeled: initial_unlabeled_arg,
3273                    arguments: Default::default(),
3274                    digest: None,
3275                    non_code_meta: Default::default(),
3276                },
3277            );
3278            return Ok(result);
3279        }
3280    }
3281
3282    if initial_unlabeled_arg.is_some() {
3283        // The comma following the unlabelled arg. If there are no more arguments, then we should
3284        // hit the above path, so the comma is not optional.
3285        labeled_arg_separator.parse_next(i)?;
3286    }
3287
3288    #[derive(Debug)]
3289    #[allow(clippy::large_enum_variant)]
3290    enum ArgPlace {
3291        NonCode(Node<NonCodeNode>),
3292        LabeledArg((LabeledArg, Option<SourceRange>)),
3293        UnlabeledArg(Expr),
3294        Keyword(Token),
3295    }
3296    let args: Vec<_> = repeat(
3297        0..,
3298        alt((
3299            terminated(non_code_node.map(ArgPlace::NonCode), whitespace),
3300            terminated(any_keyword.map(ArgPlace::Keyword), whitespace),
3301            (labeled_argument, labeled_arg_separator).map(ArgPlace::LabeledArg),
3302            expression.map(ArgPlace::UnlabeledArg),
3303        )),
3304    )
3305    .parse_next(i)?;
3306    let (args, non_code_nodes): (Vec<_>, BTreeMap<usize, _>) = args.into_iter().enumerate().try_fold(
3307        (Vec::new(), BTreeMap::new()),
3308        |(mut args, mut non_code_nodes), (index, e)| {
3309            match e {
3310                ArgPlace::NonCode(x) => {
3311                    non_code_nodes.insert(index, vec![x]);
3312                }
3313                ArgPlace::LabeledArg((x, bad_token_source_range)) => {
3314                    if let Some(bad_token_source_range) = bad_token_source_range {
3315                        return Err(ErrMode::Cut(
3316                            CompilationError::fatal(
3317                                bad_token_source_range,
3318                                "Missing comma between arguments, try adding a comma in",
3319                            )
3320                            .into(),
3321                        ));
3322                    }
3323                    args.push(x);
3324                }
3325                ArgPlace::Keyword(kw) => {
3326                    return Err(ErrMode::Cut(
3327                        CompilationError::fatal(
3328                            SourceRange::from(kw.clone()),
3329                            format!(
3330                                "`{}` is not the name of an argument (it's a reserved keyword)",
3331                                kw.value
3332                            ),
3333                        )
3334                        .into(),
3335                    ));
3336                }
3337                ArgPlace::UnlabeledArg(arg) => {
3338                    let followed_by_equals = peek((opt(whitespace), equals)).parse_next(i).is_ok();
3339                    if followed_by_equals {
3340                        return Err(ErrMode::Cut(
3341                            CompilationError::fatal(
3342                                SourceRange::from(arg),
3343                                "This argument has a label, but no value. Put some value after the equals sign",
3344                            )
3345                            .into(),
3346                        ));
3347                    } else {
3348                        args.push(LabeledArg { label: None, arg });
3349                    }
3350                }
3351            }
3352            Ok((args, non_code_nodes))
3353        },
3354    )?;
3355    ignore_whitespace(i);
3356    opt(comma_sep).parse_next(i)?;
3357    let end = match close_paren.parse_next(i) {
3358        Ok(tok) => tok.end,
3359        Err(e) => {
3360            if let Some(tok) = i.next_token() {
3361                return Err(ErrMode::Cut(
3362                    CompilationError::fatal(
3363                        SourceRange::from(&tok),
3364                        format!("There was an unexpected `{}`. Try removing it.", tok.value),
3365                    )
3366                    .into(),
3367                ));
3368            } else {
3369                return Err(e);
3370            }
3371        }
3372    };
3373
3374    // Validate there aren't any duplicate labels.
3375    let mut counted_labels = IndexMap::with_capacity(args.len());
3376    for arg in &args {
3377        if let Some(l) = &arg.label {
3378            *counted_labels.entry(&l.inner.name).or_insert(0) += 1;
3379        }
3380    }
3381    if let Some((duplicated, n)) = counted_labels.iter().find(|(_label, n)| n > &&1) {
3382        let msg = format!(
3383            "You've used the parameter labelled '{duplicated}' {n} times in a single function call. You can only set each parameter once! Remove all but one use."
3384        );
3385        ParseContext::err(CompilationError::err(
3386            SourceRange::new(fn_name.start, end, fn_name.module_id),
3387            msg,
3388        ));
3389    }
3390
3391    let non_code_meta = NonCodeMeta {
3392        non_code_nodes,
3393        ..Default::default()
3394    };
3395    let result = Node::new_node(
3396        fn_name.start,
3397        end,
3398        fn_name.module_id,
3399        CallExpressionKw {
3400            callee: fn_name,
3401            unlabeled: initial_unlabeled_arg,
3402            arguments: args,
3403            digest: None,
3404            non_code_meta,
3405        },
3406    );
3407
3408    let callee_str = result.callee.name.name.to_string();
3409    if let Some(suggestion) = super::deprecation(&callee_str, DeprecationKind::Function) {
3410        ParseContext::warn(
3411            CompilationError::err(
3412                result.as_source_range(),
3413                format!("Calling `{callee_str}` is deprecated, prefer using `{suggestion}`."),
3414            )
3415            .with_suggestion(
3416                format!("Replace `{callee_str}` with `{suggestion}`"),
3417                suggestion,
3418                None,
3419                Tag::Deprecated,
3420            ),
3421        );
3422    }
3423
3424    Ok(result)
3425}
3426
3427#[cfg(test)]
3428mod tests {
3429    use itertools::Itertools;
3430    use pretty_assertions::assert_eq;
3431
3432    use super::*;
3433    use crate::{
3434        ModuleId,
3435        parsing::ast::types::{BodyItem, Expr, VariableKind},
3436    };
3437
3438    fn assert_reserved(word: &str) {
3439        // Try to use it as a variable name.
3440        let code = format!(r#"{word} = 0"#);
3441        let result = crate::parsing::top_level_parse(code.as_str());
3442        let err = &result.unwrap_errs().next().unwrap();
3443        // Which token causes the error may change.  In "return = 0", for
3444        // example, "return" is the problem.
3445        assert!(
3446            err.message.starts_with("Unexpected token: ")
3447                || err.message.starts_with("= is not")
3448                || err
3449                    .message
3450                    .starts_with("Cannot assign a variable to a reserved keyword: "),
3451            "Error message is: `{}`",
3452            err.message,
3453        );
3454    }
3455
3456    #[test]
3457    fn reserved_words() {
3458        // Since these are stored in a set, we sort to make the tests
3459        // deterministic.
3460        for word in crate::parsing::token::RESERVED_WORDS.keys().sorted() {
3461            assert_reserved(word);
3462        }
3463        assert_reserved("import");
3464    }
3465
3466    #[test]
3467    fn parse_names() {
3468        for (test, expected_len) in [("someVar", 0), ("::foo", 0), ("foo::bar::baz", 2)] {
3469            let tokens = crate::parsing::token::lex(test, ModuleId::default()).unwrap();
3470            match name.parse(tokens.as_slice()) {
3471                Ok(n) => assert_eq!(n.path.len(), expected_len, "Could not parse name from `{test}`: {n:?}"),
3472                Err(e) => panic!("Could not parse name from `{test}`: {e:?}"),
3473            }
3474        }
3475    }
3476
3477    #[test]
3478    fn weird_program_unclosed_paren() {
3479        let tokens = crate::parsing::token::lex("fn firstPrime(", ModuleId::default()).unwrap();
3480        let tokens = tokens.as_slice();
3481        let last = tokens.last().unwrap().as_source_range();
3482        let err: CompilationError = program.parse(tokens).unwrap_err().into();
3483        assert_eq!(err.source_range, last);
3484        // TODO: Better comment. This should explain the compiler expected ) because the user had started declaring the function's parameters.
3485        // Part of https://github.com/KittyCAD/modeling-app/issues/784
3486        assert_eq!(err.message, "Unexpected end of file. The compiler expected )");
3487    }
3488
3489    #[test]
3490    fn kw_call_as_operand() {
3491        let tokens = crate::parsing::token::lex("f(x = 1)", ModuleId::default()).unwrap();
3492        let tokens = tokens.as_slice();
3493        operand.parse(tokens).unwrap();
3494    }
3495
3496    #[test]
3497    fn parse_binary_operator_on_array() {
3498        let tokens = crate::parsing::token::lex("[0] + 1", ModuleId::default()).unwrap();
3499        let tokens = tokens.as_slice();
3500        binary_expression.parse(tokens).unwrap();
3501    }
3502
3503    #[test]
3504    fn expression_in_array_index() {
3505        let tokens = crate::parsing::token::lex("arr[x + 1]", ModuleId::default()).unwrap();
3506        let tokens = tokens.as_slice();
3507        let Expr::MemberExpression(expr) = expression.parse(tokens).unwrap() else {
3508            panic!();
3509        };
3510        let Expr::BinaryExpression(be) = expr.inner.property else {
3511            panic!();
3512        };
3513        assert_eq!(be.inner.operator, BinaryOperator::Add);
3514    }
3515
3516    #[test]
3517    fn parse_binary_operator_on_object() {
3518        let tokens = crate::parsing::token::lex("{ a = 1 } + 2", ModuleId::default()).unwrap();
3519        let tokens = tokens.as_slice();
3520        binary_expression.parse(tokens).unwrap();
3521    }
3522
3523    #[test]
3524    fn parse_call_array_operator() {
3525        let tokens = crate::parsing::token::lex("f([0] + 1)", ModuleId::default()).unwrap();
3526        let tokens = tokens.as_slice();
3527        fn_call_kw.parse(tokens).unwrap();
3528    }
3529
3530    #[test]
3531    fn weird_program_just_a_pipe() {
3532        let tokens = crate::parsing::token::lex("|", ModuleId::default()).unwrap();
3533        let err: CompilationError = program.parse(tokens.as_slice()).unwrap_err().into();
3534        assert_eq!(err.source_range, SourceRange::new(0, 1, ModuleId::default()));
3535        assert_eq!(err.message, "Unexpected token: |");
3536    }
3537
3538    #[test]
3539    fn parse_binary_expressions() {
3540        for (i, test_program) in ["1 + 2 + 3"].into_iter().enumerate() {
3541            let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
3542            let _actual = match binary_expression.parse_next(&mut tokens.as_slice()) {
3543                Ok(x) => x,
3544                Err(e) => panic!("Failed test {i}, could not parse binary expressions from \"{test_program}\": {e:?}"),
3545            };
3546        }
3547    }
3548
3549    #[test]
3550    fn test_vardec_no_keyword() {
3551        let tokens = crate::parsing::token::lex("x = 4", ModuleId::default()).unwrap();
3552        let vardec = declaration(&mut tokens.as_slice()).unwrap();
3553        assert_eq!(vardec.inner.kind, VariableKind::Const);
3554        let vardec = &vardec.declaration;
3555        assert_eq!(vardec.id.name, "x");
3556        let Expr::Literal(init_val) = &vardec.init else {
3557            panic!("weird init value")
3558        };
3559        assert_eq!(init_val.raw, "4");
3560    }
3561
3562    #[test]
3563    fn test_negative_operands() {
3564        let tokens = crate::parsing::token::lex("-leg2", ModuleId::default()).unwrap();
3565        let _s = operand.parse_next(&mut tokens.as_slice()).unwrap();
3566    }
3567
3568    #[test]
3569    fn test_comments_in_function1() {
3570        let test_program = r#"() {
3571            // comment 0
3572            a = 1
3573            // comment 1
3574            b = 2
3575            /// comment 2
3576            return 1
3577        }"#;
3578        let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
3579        let expr = function_decl.parse_next(&mut tokens.as_slice()).unwrap();
3580        assert_eq!(expr.params, vec![]);
3581        let comment_start = expr.body.body[0].get_comments();
3582        let comment0 = expr.body.body[1].get_comments();
3583        let comment1 = expr.body.body[2].get_comments();
3584        assert_eq!(comment_start, vec!["// comment 0".to_owned()]);
3585        assert_eq!(comment0, vec!["// comment 1".to_owned()]);
3586        assert_eq!(comment1, vec!["/// comment 2".to_owned()]);
3587    }
3588
3589    #[test]
3590    fn test_comments_in_function2() {
3591        let test_program = r#"() {
3592  yo = { a = { b = { c = '123' } } } /* block
3593comment */
3594}"#;
3595        let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
3596        let expr = function_decl.parse_next(&mut tokens.as_slice()).unwrap();
3597        let comment0 = &expr.body.non_code_meta.non_code_nodes.get(&0).unwrap()[0];
3598        assert_eq!(comment0.value(), "block\ncomment");
3599    }
3600
3601    #[test]
3602    fn test_comment_at_start_of_program() {
3603        let test_program = r#"
3604/* comment at start */
3605
3606mySk1 = startSketchOn(XY)
3607  |> startProfile(at = [0, 0])"#;
3608        let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
3609        let program = program.parse(tokens.as_slice()).unwrap();
3610        let mut starting_comments = program.inner.non_code_meta.start_nodes;
3611        assert_eq!(starting_comments.len(), 2);
3612        let start0 = starting_comments.remove(0);
3613        let start1 = starting_comments.remove(0);
3614        assert_eq!(
3615            start0.value,
3616            NonCodeValue::BlockComment {
3617                value: "comment at start".to_owned(),
3618                style: CommentStyle::Block
3619            }
3620        );
3621        assert_eq!(start1.value, NonCodeValue::NewLine);
3622    }
3623
3624    #[test]
3625    fn test_comment_in_pipe() {
3626        let tokens = crate::parsing::token::lex(r#"x = y() |> /*hi*/ z(%)"#, ModuleId::default()).unwrap();
3627        let mut body = program.parse(tokens.as_slice()).unwrap().inner.body;
3628        let BodyItem::VariableDeclaration(item) = body.remove(0) else {
3629            panic!("expected vardec");
3630        };
3631        let val = item.inner.declaration.inner.init;
3632        let Expr::PipeExpression(pipe) = val else {
3633            panic!("expected pipe");
3634        };
3635        let mut noncode = pipe.inner.non_code_meta;
3636        assert_eq!(noncode.non_code_nodes.len(), 1);
3637        let comment = noncode.non_code_nodes.remove(&0).unwrap().pop().unwrap();
3638        assert_eq!(
3639            comment.value,
3640            NonCodeValue::BlockComment {
3641                value: "hi".to_owned(),
3642                style: CommentStyle::Block
3643            }
3644        );
3645    }
3646
3647    #[test]
3648    fn test_whitespace_in_function() {
3649        let test_program = r#"() {
3650            return sg
3651            return sg
3652          }"#;
3653        let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
3654        let _expr = function_decl.parse_next(&mut tokens.as_slice()).unwrap();
3655    }
3656
3657    #[test]
3658    fn test_empty_lines_in_function() {
3659        let test_program = "() {
3660
3661                return 2
3662            }";
3663        let module_id = ModuleId::from_usize(1);
3664        let tokens = crate::parsing::token::lex(test_program, module_id).unwrap();
3665        let expr = function_decl.parse_next(&mut tokens.as_slice()).unwrap();
3666        assert_eq!(
3667            expr.body.non_code_meta.start_nodes,
3668            vec![Node::new(
3669                NonCodeNode {
3670                    value: NonCodeValue::NewLine,
3671                    digest: None
3672                },
3673                4,
3674                22,
3675                module_id,
3676            )]
3677        );
3678    }
3679
3680    #[test]
3681    fn inline_comment_pipe_expression() {
3682        let test_input = r#"a(XY)
3683        |> b(%)
3684        |> c(%) // inline-comment
3685        |> d(%)"#;
3686
3687        let tokens = crate::parsing::token::lex(test_input, ModuleId::default()).unwrap();
3688        let (body, non_code_meta) = match expression.parse_next(&mut tokens.as_slice()).unwrap() {
3689            Expr::PipeExpression(e) => (e.inner.body, e.inner.non_code_meta),
3690            _ => panic!(),
3691        };
3692
3693        assert_eq!(non_code_meta.non_code_nodes.len(), 1);
3694        assert_eq!(
3695            non_code_meta.non_code_nodes.get(&2).unwrap()[0].value,
3696            NonCodeValue::InlineComment {
3697                value: "inline-comment".to_owned(),
3698                style: CommentStyle::Line
3699            }
3700        );
3701        assert_eq!(body.len(), 4);
3702    }
3703
3704    #[test]
3705    fn many_comments() {
3706        let test_program = r#"// this is a comment
3707  yo = { a = { b = { c = '123' } } } /* block
3708  comment */
3709
3710  key = 'c'
3711  // this is also a comment
3712  return things
3713"#;
3714
3715        let module_id = ModuleId::default();
3716        let tokens = crate::parsing::token::lex(test_program, module_id).unwrap();
3717        let Program {
3718            body, non_code_meta, ..
3719        } = function_body.parse(tokens.as_slice()).unwrap().inner;
3720        assert_eq!(body[0].get_comments(), vec!["// this is a comment".to_owned()],);
3721
3722        assert_eq!(
3723            Some(&vec![
3724                Node::new(
3725                    NonCodeNode {
3726                        value: NonCodeValue::InlineComment {
3727                            value: "block\n  comment".to_owned(),
3728                            style: CommentStyle::Block
3729                        },
3730                        digest: None,
3731                    },
3732                    57,
3733                    79,
3734                    module_id,
3735                ),
3736                Node::new(
3737                    NonCodeNode {
3738                        value: NonCodeValue::NewLine,
3739                        digest: None,
3740                    },
3741                    79,
3742                    83,
3743                    module_id,
3744                )
3745            ]),
3746            non_code_meta.non_code_nodes.get(&0),
3747        );
3748
3749        assert_eq!(body[2].get_comments(), vec!["// this is also a comment".to_owned()],);
3750    }
3751
3752    #[test]
3753    fn inline_block_comments() {
3754        let test_program = r#"yo = 3 /* block
3755  comment */
3756  return 1"#;
3757
3758        let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
3759        let actual = program.parse(tokens.as_slice()).unwrap();
3760        assert_eq!(actual.non_code_meta.non_code_nodes.len(), 1);
3761        assert_eq!(
3762            actual.non_code_meta.non_code_nodes.get(&0).unwrap()[0].value,
3763            NonCodeValue::InlineComment {
3764                value: "block\n  comment".to_owned(),
3765                style: CommentStyle::Block
3766            }
3767        );
3768    }
3769
3770    #[test]
3771    fn test_bracketed_binary_expression() {
3772        let input = "(2 - 3)";
3773        let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3774        let actual = match binary_expr_in_parens.parse(tokens.as_slice()) {
3775            Ok(x) => x,
3776            Err(e) => panic!("{e:?}"),
3777        };
3778        assert_eq!(actual.operator, BinaryOperator::Sub);
3779    }
3780
3781    #[test]
3782    fn test_arg() {
3783        for input in [
3784            "( sigmaAllow * width )",
3785            "6 / ( sigmaAllow * width )",
3786            "sqrt(distance * p * FOS * 6 / ( sigmaAllow * width ))",
3787        ] {
3788            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3789            let _actual = match expression.parse(tokens.as_slice()) {
3790                Ok(x) => x,
3791                Err(e) => panic!("{e:?}"),
3792            };
3793        }
3794    }
3795
3796    #[test]
3797    fn test_arithmetic() {
3798        let input = "1 * (2 - 3)";
3799        let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3800        // The RHS should be a binary expression.
3801        let actual = binary_expression.parse(tokens.as_slice()).unwrap();
3802        assert_eq!(actual.operator, BinaryOperator::Mul);
3803        let BinaryPart::BinaryExpression(rhs) = actual.inner.right else {
3804            panic!("Expected RHS to be another binary expression");
3805        };
3806        assert_eq!(rhs.operator, BinaryOperator::Sub);
3807        match &rhs.right {
3808            BinaryPart::Literal(lit) => {
3809                assert!(lit.start == 9 && lit.end == 10);
3810                assert!(
3811                    lit.value
3812                        == LiteralValue::Number {
3813                            value: 3.0,
3814                            suffix: NumericSuffix::None
3815                        }
3816                        && &lit.raw == "3"
3817                        && lit.digest.is_none()
3818                );
3819            }
3820            _ => panic!(),
3821        }
3822    }
3823
3824    #[test]
3825    fn assign_brackets() {
3826        for (i, test_input) in [
3827            "thickness_squared = (1 + 1)",
3828            "thickness_squared = ( 1 + 1)",
3829            "thickness_squared = (1 + 1 )",
3830            "thickness_squared = ( 1 + 1 )",
3831        ]
3832        .into_iter()
3833        .enumerate()
3834        {
3835            let tokens = crate::parsing::token::lex(test_input, ModuleId::default()).unwrap();
3836            let actual = match declaration.parse(tokens.as_slice()) {
3837                Err(e) => panic!("Could not parse test {i}: {e:#?}"),
3838                Ok(a) => a,
3839            };
3840            let Expr::BinaryExpression(_expr) = &actual.declaration.inner.init else {
3841                panic!(
3842                    "Expected test {i} to be a binary expression but it wasn't, it was {:?}",
3843                    actual.declaration
3844                );
3845            };
3846            // TODO: check both sides are 1... probably not necessary but should do.
3847        }
3848    }
3849
3850    #[test]
3851    fn test_function_call() {
3852        for (i, test_input) in ["x = f(1)", "x = f( 1 )"].into_iter().enumerate() {
3853            let tokens = crate::parsing::token::lex(test_input, ModuleId::default()).unwrap();
3854            let _actual = match declaration.parse(tokens.as_slice()) {
3855                Err(e) => panic!("Could not parse test {i}: {e:#?}"),
3856                Ok(a) => a,
3857            };
3858        }
3859    }
3860
3861    #[test]
3862    fn test_nested_arithmetic() {
3863        let input = "1 * ((2 - 3) / 4)";
3864        let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3865        // The RHS should be a binary expression.
3866        let outer = binary_expression.parse(tokens.as_slice()).unwrap();
3867        assert_eq!(outer.operator, BinaryOperator::Mul);
3868        let BinaryPart::BinaryExpression(middle) = outer.inner.right else {
3869            panic!("Expected RHS to be another binary expression");
3870        };
3871
3872        assert_eq!(middle.operator, BinaryOperator::Div);
3873        let BinaryPart::BinaryExpression(inner) = middle.inner.left else {
3874            panic!("expected nested binary expression");
3875        };
3876        assert_eq!(inner.operator, BinaryOperator::Sub);
3877    }
3878
3879    #[test]
3880    fn binary_expression_ignores_whitespace() {
3881        let tests = ["1 - 2", "1- 2", "1 -2", "1-2"];
3882        for test in tests {
3883            let tokens = crate::parsing::token::lex(test, ModuleId::default()).unwrap();
3884            let actual = binary_expression.parse(tokens.as_slice()).unwrap();
3885            assert_eq!(actual.operator, BinaryOperator::Sub);
3886            let BinaryPart::Literal(left) = actual.inner.left else {
3887                panic!("should be expression");
3888            };
3889            assert_eq!(
3890                left.value,
3891                LiteralValue::Number {
3892                    value: 1.0,
3893                    suffix: NumericSuffix::None
3894                }
3895            );
3896            let BinaryPart::Literal(right) = actual.inner.right else {
3897                panic!("should be expression");
3898            };
3899            assert_eq!(
3900                right.value,
3901                LiteralValue::Number {
3902                    value: 2.0,
3903                    suffix: NumericSuffix::None
3904                }
3905            );
3906        }
3907    }
3908
3909    #[test]
3910    fn some_pipe_expr() {
3911        let test_program = r#"x()
3912        |> y(%) /* this is
3913        a comment
3914        spanning a few lines */
3915        |> z(%)"#;
3916        let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
3917        let non_code_meta = match expression.parse(tokens.as_slice()).unwrap() {
3918            Expr::PipeExpression(e) => e.non_code_meta.clone(),
3919            _ => panic!(),
3920        };
3921        let n = non_code_meta.non_code_nodes.len();
3922        assert_eq!(n, 1, "expected one comment in pipe expression but found {n}");
3923        let nc = &non_code_meta.non_code_nodes.get(&1).unwrap()[0];
3924        assert!(nc.value().starts_with("this"));
3925        assert!(nc.value().ends_with("lines"));
3926    }
3927
3928    #[test]
3929    fn comments_in_pipe_expr() {
3930        for (i, test_program) in [
3931            r#"y() |> /*hi*/ z(%)"#,
3932            "1 |>/*hi*/ f(%)",
3933            r#"y() |> /*hi*/ z(%)"#,
3934            "1 /*hi*/ |> f(%)",
3935            "1
3936        // Hi
3937        |> f(%)",
3938            "1
3939        /* Hi 
3940        there
3941        */
3942        |> f(%)",
3943        ]
3944        .into_iter()
3945        .enumerate()
3946        {
3947            let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
3948            let actual = expression.parse(tokens.as_slice());
3949            assert!(actual.is_ok(), "could not parse test {i}, '{test_program}'");
3950
3951            match actual.unwrap() {
3952                Expr::PipeExpression(e) => {
3953                    let n = e.non_code_meta.non_code_nodes.len();
3954                    assert_eq!(n, 1, "expected one comment in pipe expression but found {n}",);
3955                }
3956                _ => panic!(),
3957            }
3958        }
3959    }
3960
3961    #[test]
3962    fn comments() {
3963        let module_id = ModuleId::from_usize(1);
3964        for (i, (test_program, expected)) in [
3965            (
3966                "//hi",
3967                Node::new(
3968                    NonCodeNode {
3969                        value: NonCodeValue::BlockComment {
3970                            value: "hi".to_owned(),
3971                            style: CommentStyle::Line,
3972                        },
3973                        digest: None,
3974                    },
3975                    0,
3976                    4,
3977                    module_id,
3978                ),
3979            ),
3980            (
3981                "/*hello*/",
3982                Node::new(
3983                    NonCodeNode {
3984                        value: NonCodeValue::BlockComment {
3985                            value: "hello".to_owned(),
3986                            style: CommentStyle::Block,
3987                        },
3988                        digest: None,
3989                    },
3990                    0,
3991                    9,
3992                    module_id,
3993                ),
3994            ),
3995            (
3996                "/* hello */",
3997                Node::new(
3998                    NonCodeNode {
3999                        value: NonCodeValue::BlockComment {
4000                            value: "hello".to_owned(),
4001                            style: CommentStyle::Block,
4002                        },
4003                        digest: None,
4004                    },
4005                    0,
4006                    11,
4007                    module_id,
4008                ),
4009            ),
4010            (
4011                "/* \nhello */",
4012                Node::new(
4013                    NonCodeNode {
4014                        value: NonCodeValue::BlockComment {
4015                            value: "hello".to_owned(),
4016                            style: CommentStyle::Block,
4017                        },
4018                        digest: None,
4019                    },
4020                    0,
4021                    12,
4022                    module_id,
4023                ),
4024            ),
4025            (
4026                "
4027                /* hello */",
4028                Node::new(
4029                    NonCodeNode {
4030                        value: NonCodeValue::BlockComment {
4031                            value: "hello".to_owned(),
4032                            style: CommentStyle::Block,
4033                        },
4034                        digest: None,
4035                    },
4036                    0,
4037                    29,
4038                    module_id,
4039                ),
4040            ),
4041            (
4042                // Empty line with trailing whitespace
4043                "
4044  
4045                /* hello */",
4046                Node::new(
4047                    NonCodeNode {
4048                        value: NonCodeValue::NewLineBlockComment {
4049                            value: "hello".to_owned(),
4050                            style: CommentStyle::Block,
4051                        },
4052                        digest: None,
4053                    },
4054                    0,
4055                    32,
4056                    module_id,
4057                ),
4058            ),
4059            (
4060                // Empty line, no trailing whitespace
4061                "
4062
4063                /* hello */",
4064                Node::new(
4065                    NonCodeNode {
4066                        value: NonCodeValue::NewLineBlockComment {
4067                            value: "hello".to_owned(),
4068                            style: CommentStyle::Block,
4069                        },
4070                        digest: None,
4071                    },
4072                    0,
4073                    30,
4074                    module_id,
4075                ),
4076            ),
4077            (
4078                r#"/* block
4079                    comment */"#,
4080                Node::new(
4081                    NonCodeNode {
4082                        value: NonCodeValue::BlockComment {
4083                            value: "block\n                    comment".to_owned(),
4084                            style: CommentStyle::Block,
4085                        },
4086                        digest: None,
4087                    },
4088                    0,
4089                    39,
4090                    module_id,
4091                ),
4092            ),
4093        ]
4094        .into_iter()
4095        .enumerate()
4096        {
4097            let tokens = crate::parsing::token::lex(test_program, module_id).unwrap();
4098            let actual = non_code_node.parse(tokens.as_slice());
4099            assert!(actual.is_ok(), "could not parse test {i}: {actual:#?}");
4100            let actual = actual.unwrap();
4101            assert_eq!(actual, expected, "failed test {i}");
4102        }
4103    }
4104
4105    #[test]
4106    fn recognize_invalid_params() {
4107        let test_fn = "(let) => { return 1 }";
4108        let module_id = ModuleId::from_usize(2);
4109        let tokens = crate::parsing::token::lex(test_fn, module_id).unwrap();
4110        let err = function_decl.parse(tokens.as_slice()).unwrap_err().into_inner();
4111        let cause = err.cause.unwrap();
4112        // This is the token `let`
4113        assert_eq!(cause.source_range, SourceRange::new(1, 4, ModuleId::from_usize(2)));
4114        assert_eq!(cause.message, "Cannot assign a variable to a reserved keyword: let");
4115    }
4116
4117    #[test]
4118    fn comment_in_string() {
4119        let string_literal = r#""
4120           // a comment
4121             ""#;
4122        let tokens = crate::parsing::token::lex(string_literal, ModuleId::default()).unwrap();
4123        let parsed_literal = literal.parse(tokens.as_slice()).unwrap();
4124        assert_eq!(
4125            parsed_literal.value,
4126            "
4127           // a comment
4128             "
4129            .into()
4130        );
4131    }
4132
4133    #[test]
4134    fn pipes_on_pipes_minimal() {
4135        let test_program = r#"startSketchOn(XY)
4136        |> startProfile(at = [0, 0])
4137        |> line(endAbsolute = [0, -0]) // MoveRelative
4138
4139        "#;
4140        let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
4141        let tokens = &mut tokens.as_slice();
4142        let _actual = expression.parse_next(tokens).unwrap();
4143        assert_eq!(tokens.first().unwrap().token_type, TokenType::Whitespace);
4144    }
4145
4146    #[test]
4147    fn test_pipes_on_pipes() {
4148        let test_program = include_str!("../../e2e/executor/inputs/pipes_on_pipes.kcl");
4149        let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
4150        let _ = run_parser(tokens.as_slice()).unwrap();
4151    }
4152
4153    #[test]
4154    fn test_cube() {
4155        let test_program = include_str!("../../e2e/executor/inputs/cube.kcl");
4156        let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
4157        match program.parse(tokens.as_slice()) {
4158            Ok(_) => {}
4159            Err(e) => {
4160                panic!("{e:#?}");
4161            }
4162        }
4163    }
4164
4165    #[test]
4166    fn parse_numeric() {
4167        let test_program = "fn foo(x: number(Length)) {}";
4168        let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
4169        run_parser(tokens.as_slice()).unwrap();
4170
4171        let test_program = "42_mm";
4172        let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
4173        assert_eq!(tokens.iter().count(), 1);
4174        run_parser(tokens.as_slice()).unwrap();
4175
4176        let test_program = "42_Length";
4177        let tokens = crate::parsing::token::lex(test_program, ModuleId::default()).unwrap();
4178        assert_eq!(tokens.iter().count(), 2);
4179        assert_eq!(run_parser(tokens.as_slice()).unwrap_errs().count(), 1);
4180    }
4181
4182    #[test]
4183    fn test_parameter_list() {
4184        let tests = [
4185            ("", vec![]),
4186            ("a", vec!["a"]),
4187            ("a, b", vec!["a", "b"]),
4188            ("a,b", vec!["a", "b"]),
4189        ];
4190        for (i, (input, expected)) in tests.into_iter().enumerate() {
4191            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
4192            let actual = parameters.parse(tokens.as_slice());
4193            assert!(actual.is_ok(), "could not parse test {i}");
4194            let actual_ids: Vec<_> = actual.unwrap().into_iter().map(|p| p.identifier.inner.name).collect();
4195            assert_eq!(actual_ids, expected);
4196        }
4197    }
4198
4199    #[test]
4200    fn test_user_function() {
4201        let input = "() {
4202            return 2
4203        }";
4204
4205        let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
4206        let actual = function_decl.parse(tokens.as_slice());
4207        assert!(actual.is_ok(), "could not parse test function");
4208    }
4209
4210    #[test]
4211    fn test_declaration() {
4212        let tests = ["myVar = 5", "myVar=5", "myVar =5", "myVar= 5"];
4213        for test in tests {
4214            // Run the original parser
4215            let tokens = crate::parsing::token::lex(test, ModuleId::default()).unwrap();
4216            let mut expected_body = crate::parsing::parse_tokens(tokens.clone()).unwrap().inner.body;
4217            assert_eq!(expected_body.len(), 1);
4218            let BodyItem::VariableDeclaration(expected) = expected_body.pop().unwrap() else {
4219                panic!("Expected variable declaration");
4220            };
4221
4222            // Run the second parser, check it matches the first parser.
4223            let actual = declaration.parse(tokens.as_slice()).unwrap();
4224            assert_eq!(expected, actual);
4225
4226            // Inspect its output in more detail.
4227            assert_eq!(actual.inner.kind, VariableKind::Const);
4228            assert_eq!(actual.start, 0);
4229            let decl = &actual.declaration;
4230            assert_eq!(decl.id.name, "myVar");
4231            let Expr::Literal(value) = &decl.inner.init else {
4232                panic!("value should be a literal")
4233            };
4234            assert_eq!(value.end, test.len());
4235            assert_eq!(value.raw, "5");
4236        }
4237    }
4238
4239    #[test]
4240    fn test_math_parse() {
4241        let module_id = ModuleId::default();
4242        let actual = crate::parsing::parse_str(r#"5 + "a""#, module_id).unwrap().inner.body;
4243        let expr = Node::boxed(
4244            0,
4245            7,
4246            module_id,
4247            BinaryExpression {
4248                operator: BinaryOperator::Add,
4249                left: BinaryPart::Literal(Box::new(Node::new(
4250                    Literal {
4251                        value: LiteralValue::Number {
4252                            value: 5.0,
4253                            suffix: NumericSuffix::None,
4254                        },
4255                        raw: "5".to_owned(),
4256                        digest: None,
4257                    },
4258                    0,
4259                    1,
4260                    module_id,
4261                ))),
4262                right: BinaryPart::Literal(Box::new(Node::new(
4263                    Literal {
4264                        value: "a".into(),
4265                        raw: r#""a""#.to_owned(),
4266                        digest: None,
4267                    },
4268                    4,
4269                    7,
4270                    module_id,
4271                ))),
4272                digest: None,
4273            },
4274        );
4275        let expected = vec![BodyItem::ExpressionStatement(Node::new(
4276            ExpressionStatement {
4277                expression: Expr::BinaryExpression(expr),
4278                digest: None,
4279            },
4280            0,
4281            7,
4282            module_id,
4283        ))];
4284        assert_eq!(expected, actual);
4285    }
4286
4287    #[test]
4288    fn test_abstract_syntax_tree() {
4289        let code = "5 +6";
4290        let module_id = ModuleId::default();
4291        let result = crate::parsing::parse_str(code, module_id).unwrap();
4292        let expected_result = Node::new(
4293            Program {
4294                body: vec![BodyItem::ExpressionStatement(Node::new(
4295                    ExpressionStatement {
4296                        expression: Expr::BinaryExpression(Node::boxed(
4297                            0,
4298                            4,
4299                            module_id,
4300                            BinaryExpression {
4301                                left: BinaryPart::Literal(Box::new(Node::new(
4302                                    Literal {
4303                                        value: LiteralValue::Number {
4304                                            value: 5.0,
4305                                            suffix: NumericSuffix::None,
4306                                        },
4307                                        raw: "5".to_string(),
4308                                        digest: None,
4309                                    },
4310                                    0,
4311                                    1,
4312                                    module_id,
4313                                ))),
4314                                operator: BinaryOperator::Add,
4315                                right: BinaryPart::Literal(Box::new(Node::new(
4316                                    Literal {
4317                                        value: LiteralValue::Number {
4318                                            value: 6.0,
4319                                            suffix: NumericSuffix::None,
4320                                        },
4321                                        raw: "6".to_string(),
4322                                        digest: None,
4323                                    },
4324                                    3,
4325                                    4,
4326                                    module_id,
4327                                ))),
4328                                digest: None,
4329                            },
4330                        )),
4331                        digest: None,
4332                    },
4333                    0,
4334                    4,
4335                    module_id,
4336                ))],
4337                shebang: None,
4338                non_code_meta: NonCodeMeta::default(),
4339                inner_attrs: Vec::new(),
4340                digest: None,
4341            },
4342            0,
4343            4,
4344            module_id,
4345        );
4346
4347        assert_eq!(result, expected_result);
4348    }
4349
4350    #[test]
4351    fn test_empty_file() {
4352        let some_program_string = r#""#;
4353        let result = crate::parsing::top_level_parse(some_program_string);
4354        assert!(result.is_ok());
4355    }
4356
4357    #[track_caller]
4358    fn assert_no_err(p: &str) -> (Node<Program>, Vec<CompilationError>) {
4359        let result = crate::parsing::top_level_parse(p);
4360        let result = result.0.unwrap();
4361        assert!(result.1.iter().all(|e| !e.severity.is_err()), "found: {:#?}", result.1);
4362        (result.0.unwrap(), result.1)
4363    }
4364
4365    #[track_caller]
4366    fn assert_no_fatal(p: &str) -> (Node<Program>, Vec<CompilationError>) {
4367        let result = crate::parsing::top_level_parse(p);
4368        let result = result.0.unwrap();
4369        assert!(
4370            result.1.iter().all(|e| e.severity != Severity::Fatal),
4371            "found: {:#?}",
4372            result.1
4373        );
4374        (result.0.unwrap(), result.1)
4375    }
4376
4377    #[track_caller]
4378    fn assert_err(p: &str, msg: &str, src_expected: [usize; 2]) {
4379        let result = crate::parsing::top_level_parse(p);
4380        let err = result.unwrap_errs().next().unwrap();
4381        assert!(
4382            err.message.starts_with(msg),
4383            "Found `{}`, expected `{msg}`",
4384            err.message
4385        );
4386        let src_actual = [err.source_range.start(), err.source_range.end()];
4387        assert_eq!(
4388            src_expected,
4389            src_actual,
4390            "expected error would highlight `{}` but it actually highlighted `{}`",
4391            &p[src_expected[0]..src_expected[1]],
4392            &p[src_actual[0]..src_actual[1]],
4393        );
4394    }
4395
4396    #[track_caller]
4397    fn assert_err_contains(p: &str, expected: &str) {
4398        let result = crate::parsing::top_level_parse(p);
4399        let err = &result.unwrap_errs().next().unwrap().message;
4400        assert!(err.contains(expected), "actual='{err}'");
4401    }
4402
4403    #[test]
4404    fn test_parse_half_pipe_small() {
4405        assert_err_contains(
4406            "secondExtrude = startSketchOn(XY)
4407  |> startProfile(at = [0,0])
4408  |",
4409            "Unexpected token: |",
4410        );
4411    }
4412
4413    #[test]
4414    fn test_parse_member_expression_double_nested_braces() {
4415        let code = r#"prop = yo["one"][two]"#;
4416        crate::parsing::top_level_parse(code).unwrap();
4417    }
4418
4419    #[test]
4420    fn test_parse_member_expression_binary_expression_period_number_first() {
4421        let code = r#"obj = { a: 1, b: 2 }
4422height = 1 - obj.a"#;
4423        crate::parsing::top_level_parse(code).unwrap();
4424    }
4425
4426    #[test]
4427    fn test_parse_member_expression_allowed_type_in_expression() {
4428        let code = r#"obj = { thing: 1 }
4429startSketchOn(obj.sketch)"#;
4430
4431        crate::parsing::top_level_parse(code).unwrap();
4432    }
4433
4434    #[test]
4435    fn test_parse_member_expression_binary_expression_brace_number_first() {
4436        let code = r#"obj = { a: 1, b: 2 }
4437height = 1 - obj["a"]"#;
4438        crate::parsing::top_level_parse(code).unwrap();
4439    }
4440
4441    #[test]
4442    fn test_parse_member_expression_binary_expression_brace_number_second() {
4443        let code = r#"obj = { a: 1, b: 2 }
4444height = obj["a"] - 1"#;
4445        crate::parsing::top_level_parse(code).unwrap();
4446    }
4447
4448    #[test]
4449    fn test_parse_member_expression_binary_expression_in_array_number_first() {
4450        let code = r#"obj = { a: 1, b: 2 }
4451height = [1 - obj["a"], 0]"#;
4452        crate::parsing::top_level_parse(code).unwrap();
4453    }
4454
4455    #[test]
4456    fn test_parse_member_expression_binary_expression_in_array_number_second() {
4457        let code = r#"obj = { a: 1, b: 2 }
4458height = [obj["a"] - 1, 0]"#;
4459        crate::parsing::top_level_parse(code).unwrap();
4460    }
4461
4462    #[test]
4463    fn test_parse_member_expression_binary_expression_in_array_number_second_missing_space() {
4464        let code = r#"obj = { a: 1, b: 2 }
4465height = [obj["a"] -1, 0]"#;
4466        crate::parsing::top_level_parse(code).unwrap();
4467    }
4468
4469    #[test]
4470    fn test_anon_fn() {
4471        crate::parsing::top_level_parse("foo(num=42, closure=fn(x) { return x + 1 })").unwrap();
4472    }
4473
4474    #[test]
4475    fn test_annotation_fn() {
4476        crate::parsing::top_level_parse(
4477            r#"fn foo() {
4478  @annotated
4479  return 1
4480}"#,
4481        )
4482        .unwrap();
4483    }
4484
4485    #[test]
4486    fn test_annotation_settings() {
4487        crate::parsing::top_level_parse("@settings(units = mm)").unwrap();
4488    }
4489
4490    #[test]
4491    fn test_anon_fn_no_fn() {
4492        assert_err_contains("foo(42, (x) { return x + 1 })", "Anonymous function requires `fn`");
4493    }
4494
4495    #[test]
4496    fn test_parse_half_pipe() {
4497        let code = "height = 10
4498
4499firstExtrude = startSketchOn(XY)
4500  |> startProfile(at = [0,0])
4501  |> line(at = [0, 8])
4502  |> line(at = [20, 0])
4503  |> line(at = [0, -8])
4504  |> close()
4505  |> extrude(length=2)
4506
4507secondExtrude = startSketchOn(XY)
4508  |> startProfile(at = [0,0])
4509  |";
4510        assert_err_contains(code, "Unexpected token: |");
4511    }
4512
4513    #[test]
4514    fn test_parse_greater_bang() {
4515        assert_err(">!", "Unexpected token: >", [0, 1]);
4516    }
4517
4518    #[test]
4519    fn test_parse_unlabeled_param_not_allowed() {
4520        assert_err(
4521            "fn f(@x, @y) { return 1 }",
4522            "Only the first parameter can be declared unlabeled",
4523            [9, 11],
4524        );
4525        assert_err(
4526            "fn f(x, @y) { return 1 }",
4527            "Only the first parameter can be declared unlabeled",
4528            [8, 10],
4529        );
4530    }
4531
4532    #[test]
4533    fn test_parse_z_percent_parens() {
4534        assert_err("z%)", "Unexpected token: %", [1, 2]);
4535    }
4536
4537    #[test]
4538    fn test_parse_parens_unicode() {
4539        let result = crate::parsing::top_level_parse("(ޜ");
4540        let details = result.0.unwrap().1.pop().unwrap();
4541        // TODO: Highlight where the unmatched open parenthesis is.
4542        // https://github.com/KittyCAD/modeling-app/issues/696
4543        assert_eq!(details.message, "Unexpected end of file. The compiler expected )");
4544    }
4545
4546    #[test]
4547    fn test_parse_negative_in_array_binary_expression() {
4548        let code = r#"leg1 = 5
4549thickness = 0.56
4550
4551bracket = [-leg2 + thickness, 0]
4552"#;
4553        crate::parsing::top_level_parse(code).unwrap();
4554    }
4555
4556    #[test]
4557    fn test_parse_nested_open_brackets() {
4558        let _ = crate::parsing::top_level_parse(
4559            r#"
4560z(-[["#,
4561        )
4562        .unwrap_errs();
4563    }
4564
4565    #[test]
4566    fn test_parse_weird_new_line_function() {
4567        assert_err(
4568            r#"z
4569(--#"#,
4570            "There was an unexpected `-`. Try removing it.",
4571            [3, 4],
4572        );
4573    }
4574
4575    #[test]
4576    fn test_parse_weird_lots_of_fancy_brackets() {
4577        assert_err(
4578            r#"zz({{{{{{{{)iegAng{{{{{{{##"#,
4579            "Encountered an unexpected character(s) before finding a closing brace(`}`) for the object",
4580            [3, 4],
4581        );
4582    }
4583
4584    #[test]
4585    fn test_parse_weird_close_before_open() {
4586        assert_err_contains(
4587            r#"fn)n
4588e
4589["#,
4590            "expected whitespace, found ')' which is brace",
4591        );
4592    }
4593
4594    #[test]
4595    fn test_parse_weird_close_before_nada() {
4596        assert_err_contains(r#"fn)n-"#, "expected whitespace, found ')' which is brace");
4597    }
4598
4599    #[test]
4600    fn test_parse_weird_lots_of_slashes() {
4601        assert_err_contains(
4602            r#"J///////////o//+///////////P++++*++++++P///////ËŸ
4603++4"#,
4604            "Unexpected token: +",
4605        );
4606    }
4607
4608    #[test]
4609    fn test_optional_param_order() {
4610        for (i, (params, expect_ok)) in [
4611            (
4612                vec![Parameter {
4613                    identifier: Node::no_src(Identifier {
4614                        name: "a".to_owned(),
4615                        digest: None,
4616                    }),
4617                    type_: None,
4618                    default_value: Some(DefaultParamVal::none()),
4619                    labeled: true,
4620                    digest: None,
4621                }],
4622                true,
4623            ),
4624            (
4625                vec![Parameter {
4626                    identifier: Node::no_src(Identifier {
4627                        name: "a".to_owned(),
4628                        digest: None,
4629                    }),
4630                    type_: None,
4631                    default_value: None,
4632                    labeled: true,
4633                    digest: None,
4634                }],
4635                true,
4636            ),
4637            (
4638                vec![
4639                    Parameter {
4640                        identifier: Node::no_src(Identifier {
4641                            name: "a".to_owned(),
4642                            digest: None,
4643                        }),
4644                        type_: None,
4645                        default_value: None,
4646                        labeled: true,
4647                        digest: None,
4648                    },
4649                    Parameter {
4650                        identifier: Node::no_src(Identifier {
4651                            name: "b".to_owned(),
4652                            digest: None,
4653                        }),
4654                        type_: None,
4655                        default_value: Some(DefaultParamVal::none()),
4656                        labeled: true,
4657                        digest: None,
4658                    },
4659                ],
4660                true,
4661            ),
4662            (
4663                vec![
4664                    Parameter {
4665                        identifier: Node::no_src(Identifier {
4666                            name: "a".to_owned(),
4667                            digest: None,
4668                        }),
4669                        type_: None,
4670                        default_value: Some(DefaultParamVal::none()),
4671                        labeled: true,
4672                        digest: None,
4673                    },
4674                    Parameter {
4675                        identifier: Node::no_src(Identifier {
4676                            name: "b".to_owned(),
4677                            digest: None,
4678                        }),
4679                        type_: None,
4680                        default_value: None,
4681                        labeled: true,
4682                        digest: None,
4683                    },
4684                ],
4685                false,
4686            ),
4687        ]
4688        .into_iter()
4689        .enumerate()
4690        {
4691            let actual = optional_after_required(&params);
4692            assert_eq!(actual.is_ok(), expect_ok, "failed test {i}");
4693        }
4694    }
4695
4696    #[test]
4697    fn test_error_keyword_in_variable() {
4698        assert_err(
4699            r#"const let = "thing""#,
4700            "Cannot assign a variable to a reserved keyword: let",
4701            [6, 9],
4702        );
4703    }
4704
4705    #[test]
4706    fn test_error_keyword_in_fn_name() {
4707        assert_err(
4708            r#"fn let = () {}"#,
4709            "Cannot assign a variable to a reserved keyword: let",
4710            [3, 6],
4711        );
4712    }
4713
4714    #[test]
4715    fn test_error_keyword_in_fn_args() {
4716        assert_err(
4717            r#"fn thing = (let) => {
4718    return 1
4719}"#,
4720            "Cannot assign a variable to a reserved keyword: let",
4721            [12, 15],
4722        )
4723    }
4724
4725    #[test]
4726    fn bad_imports() {
4727        assert_err(
4728            r#"import cube from "../cube.kcl""#,
4729            "import path may not start with '..'. Cannot traverse to something outside the bounds of your project. If this path is inside your project please find a better way to reference it.",
4730            [17, 30],
4731        );
4732        assert_err(
4733            r#"import cube from "/cube.kcl""#,
4734            "import path may not start with '/' or '\\'. Cannot traverse to something outside the bounds of your project. If this path is inside your project please find a better way to reference it.",
4735            [17, 28],
4736        );
4737        assert_err(
4738            r#"import cube from "C:\cube.kcl""#,
4739            "import path may only contain alphanumeric characters, `_`, `-`, `.`, `/`, and `\\`.",
4740            [17, 30],
4741        );
4742        assert_err(
4743            r#"import cube from "cube/cube.kcl""#,
4744            "import path to a subdirectory must only refer to main.kcl.",
4745            [17, 32],
4746        );
4747        assert_err(
4748            r#"import * as foo from "dsfs""#,
4749            "as is not the 'from' keyword",
4750            [9, 11],
4751        );
4752        assert_err(
4753            r#"import a from "dsfs" as b"#,
4754            "unsupported import path format",
4755            [14, 20],
4756        );
4757        assert_err(
4758            r#"import * from "dsfs" as b"#,
4759            "unsupported import path format",
4760            [14, 20],
4761        );
4762        assert_err(r#"import a from b"#, "invalid string literal", [14, 15]);
4763        assert_err(r#"import * "dsfs""#, "\"dsfs\" is not the 'from' keyword", [9, 15]);
4764        assert_err(r#"import from "dsfs""#, "\"dsfs\" is not the 'from' keyword", [12, 18]);
4765        assert_err(r#"import "dsfs.kcl" as *"#, "Unexpected token: as", [18, 20]);
4766        assert_err(r#"import "dsfs""#, "unsupported import path format", [7, 13]);
4767        assert_err(
4768            r#"import "foo.bar.kcl""#,
4769            "import path is not a valid identifier and must be aliased.",
4770            [7, 20],
4771        );
4772        assert_err(
4773            r#"import "_foo.kcl""#,
4774            "import path is not a valid identifier and must be aliased.",
4775            [7, 17],
4776        );
4777        assert_err(
4778            r#"import "foo-bar.kcl""#,
4779            "import path is not a valid identifier and must be aliased.",
4780            [7, 20],
4781        );
4782    }
4783
4784    #[test]
4785    fn std_fn_decl() {
4786        let code = r#"/// Compute the cosine of a number (in radians).
4787///
4788/// ```
4789/// exampleSketch = startSketchOn(XZ)
4790///   |> startProfile(at = [0, 0])
4791///   |> angledLine(
4792///        angle = 30,
4793///        length = 3 / cos(toRadians(30)),
4794///      )
4795///   |> yLine(endAbsolute = 0)
4796///   |> close(%)
4797/// 
4798/// example = extrude(exampleSketch, length = 5)
4799/// ```
4800@(impl = std_rust)
4801export fn cos(num: number(rad)): number(_) {}"#;
4802        let _ast = crate::parsing::top_level_parse(code).unwrap();
4803    }
4804
4805    #[test]
4806    fn warn_import() {
4807        let some_program_string = r#"import "foo.bad""#;
4808        let (_, errs) = assert_no_err(some_program_string);
4809        assert_eq!(errs.len(), 1, "{errs:#?}");
4810    }
4811
4812    #[test]
4813    fn warn_late_settings() {
4814        let some_program_string = r#"foo = 42
4815@settings(defaultLengthUnit = mm)
4816"#;
4817        let (_, errs) = assert_no_err(some_program_string);
4818        assert_eq!(errs.len(), 1, "{errs:#?}");
4819    }
4820
4821    #[test]
4822    fn warn_unknown_suffix() {
4823        let some_program_string = r#"foo = 42_?
4824"#;
4825        let (_, errs) = assert_no_err(some_program_string);
4826        assert_eq!(errs.len(), 1, "{errs:#?}");
4827    }
4828
4829    #[test]
4830    fn fn_decl_uom_ty() {
4831        let some_program_string = r#"fn foo(x: number(mm)): number(_) { return 1 }"#;
4832        let (_, errs) = assert_no_fatal(some_program_string);
4833        assert!(errs.is_empty(), "Expected no errors, found: {errs:?}");
4834    }
4835
4836    #[test]
4837    fn error_underscore() {
4838        let (_, errs) = assert_no_fatal("_foo(a=_blah, b=_)");
4839        assert_eq!(errs.len(), 3, "found: {errs:#?}");
4840    }
4841
4842    #[test]
4843    fn error_double_and() {
4844        let (_, errs) = assert_no_fatal("foo = true && false");
4845        assert_eq!(errs.len(), 1, "found: {errs:#?}");
4846        assert!(errs[0].message.contains("`&&`") && errs[0].message.contains("`&`") && errs[0].suggestion.is_some());
4847    }
4848
4849    #[test]
4850    fn error_type_ascription() {
4851        let (_, errs) = assert_no_fatal("a + b: number");
4852        assert!(errs.is_empty());
4853    }
4854
4855    #[test]
4856    fn zero_param_function() {
4857        let code = r#"
4858        fn firstPrimeNumber() {
4859            return 2
4860        }
4861        firstPrimeNumber()
4862        "#;
4863        let _ast = crate::parsing::top_level_parse(code).unwrap();
4864    }
4865
4866    #[test]
4867    fn array() {
4868        let program = r#"[1, 2, 3]"#;
4869        let module_id = ModuleId::default();
4870        let tokens = crate::parsing::token::lex(program, module_id).unwrap();
4871        let _arr = array_elem_by_elem(&mut tokens.as_slice()).unwrap();
4872    }
4873
4874    #[test]
4875    fn array_linesep_trailing_comma() {
4876        let program = r#"[
4877            1,
4878            2,
4879            3,
4880        ]"#;
4881        let module_id = ModuleId::default();
4882        let tokens = crate::parsing::token::lex(program, module_id).unwrap();
4883        let _arr = array_elem_by_elem(&mut tokens.as_slice()).unwrap();
4884    }
4885
4886    #[allow(unused)]
4887    #[test]
4888    fn array_linesep_no_trailing_comma() {
4889        let program = r#"[
4890            1,
4891            2,
4892            3
4893        ]"#;
4894        let module_id = ModuleId::default();
4895        let tokens = crate::parsing::token::lex(program, module_id).unwrap();
4896        let _arr = array_elem_by_elem(&mut tokens.as_slice()).unwrap();
4897    }
4898
4899    #[test]
4900    fn array_no_trailing_comma_with_comment() {
4901        let program = r#"[
4902            1, // one
4903            2, // two
4904            3  // three
4905        ]"#;
4906        let module_id = ModuleId::default();
4907        let tokens = crate::parsing::token::lex(program, module_id).unwrap();
4908        let _arr = array_elem_by_elem(&mut tokens.as_slice()).unwrap();
4909    }
4910
4911    #[test]
4912    fn array_block_comment_no_whitespace() {
4913        let program = r#"[1/* comment*/]"#;
4914        let module_id = ModuleId::default();
4915        let tokens = crate::parsing::token::lex(program, module_id).unwrap();
4916        let _arr = array_elem_by_elem(&mut tokens.as_slice()).unwrap();
4917    }
4918
4919    #[test]
4920    fn object_no_trailing_comma_with_comment() {
4921        let program = r#"{
4922            x=1, // one
4923            y=2, // two
4924            z=3  // three
4925        }"#;
4926        let module_id = ModuleId::default();
4927        let tokens = crate::parsing::token::lex(program, module_id).unwrap();
4928        let _arr = object(&mut tokens.as_slice()).unwrap();
4929    }
4930
4931    #[test]
4932    fn object_block_comment_no_whitespace() {
4933        let program = r#"{x=1/* comment*/}"#;
4934        let module_id = ModuleId::default();
4935        let tokens = crate::parsing::token::lex(program, module_id).unwrap();
4936        let _arr = object(&mut tokens.as_slice()).unwrap();
4937    }
4938
4939    #[test]
4940    fn basic_if_else() {
4941        let some_program_string = "if true {
4942            3
4943        } else {
4944            4
4945        }";
4946        let module_id = ModuleId::default();
4947        let tokens = crate::parsing::token::lex(some_program_string, module_id).unwrap();
4948        let _res = if_expr(&mut tokens.as_slice()).unwrap();
4949    }
4950
4951    #[test]
4952    fn basic_else_if() {
4953        let some_program_string = "else if true {
4954            4
4955        }";
4956        let module_id = ModuleId::default();
4957        let tokens = crate::parsing::token::lex(some_program_string, module_id).unwrap();
4958        let _res = else_if(&mut tokens.as_slice()).unwrap();
4959    }
4960
4961    #[test]
4962    fn basic_if_else_if() {
4963        let some_program_string = "if true {
4964            3  
4965        } else if true {
4966            4
4967        } else {
4968            5
4969        }";
4970        let module_id = ModuleId::default();
4971        let tokens = crate::parsing::token::lex(some_program_string, module_id).unwrap();
4972        let _res = if_expr(&mut tokens.as_slice()).unwrap();
4973    }
4974
4975    #[test]
4976    fn test_keyword_ok_in_fn_args_return() {
4977        let some_program_string = r#"fn thing(param) {
4978    return true
4979}
4980
4981thing(false)
4982"#;
4983        crate::parsing::top_level_parse(some_program_string).unwrap();
4984    }
4985
4986    #[test]
4987    fn test_mul_if() {
4988        let some_program_string = r#"10 * if true { 1 } else { 0}"#;
4989        let tokens = crate::parsing::token::lex(some_program_string, ModuleId::default()).unwrap();
4990        super::binary_expression_tokens.parse(tokens.as_slice()).unwrap();
4991    }
4992
4993    #[test]
4994    fn test_error_define_var_as_function() {
4995        // TODO: https://github.com/KittyCAD/modeling-app/issues/784
4996        // Improve this error message.
4997        // It should say that the compiler is expecting a function expression on the RHS.
4998        assert_err(r#"fn thing = "thing""#, "Unexpected token: \"thing\"", [11, 18]);
4999    }
5000
5001    #[test]
5002    fn random_words_fail() {
5003        let test_program = r#"part001 = startSketchOn(-XZ)
5004    |> startProfile(at = [8.53, 11.8])
5005    asdasd asdasd
5006    |> line(at = [11.12, -14.82])
5007    |> line(at = [-13.27, -6.98])
5008    |> line(at = [-5.09, 12.33])
5009    asdasd
5010"#;
5011        let _ = crate::parsing::top_level_parse(test_program).unwrap_errs();
5012    }
5013
5014    #[test]
5015    fn test_member_expression_sketch() {
5016        let some_program_string = r#"fn cube(pos, scale) {
5017  sg = startSketchOn(XY)
5018  |> startProfile(pos)
5019    |> line(at = [0, scale])
5020    |> line(at = [scale, 0])
5021    |> line(at = [0, -scale])
5022
5023  return sg
5024}
5025
5026b1 = cube(pos=[0,0], scale=10)
5027b2 = cube(pos=[3,3], scale=4)
5028
5029pt1 = b1[0]
5030pt2 = b2[0]
5031"#;
5032        crate::parsing::top_level_parse(some_program_string).unwrap();
5033    }
5034
5035    #[test]
5036    fn test_math_with_stdlib() {
5037        let some_program_string = r#"d2r = pi() / 2
5038let other_thing = 2 * cos(3)"#;
5039        crate::parsing::top_level_parse(some_program_string).unwrap();
5040    }
5041
5042    #[test]
5043    fn test_negative_arguments() {
5044        let some_program_string = r#"fn box(p, h, l, w) {
5045 myBox = startSketchOn(XY)
5046    |> startProfile(p)
5047    |> line(at = [0, l])
5048    |> line(at = [w, 0])
5049    |> line(at = [0, -l])
5050    |> close()
5051    |> extrude(length=h)
5052
5053  return myBox
5054}
5055let myBox = box(p=[0,0], h=-3, l=-16, w=-10)
5056"#;
5057        crate::parsing::top_level_parse(some_program_string).unwrap();
5058    }
5059
5060    #[test]
5061    fn kw_fn() {
5062        for input in ["val = foo(x, y = z)", "val = foo(y = z)"] {
5063            let module_id = ModuleId::default();
5064            let tokens = crate::parsing::token::lex(input, module_id).unwrap();
5065            super::program.parse(tokens.as_slice()).unwrap();
5066        }
5067    }
5068
5069    #[test]
5070    fn test_parse_empty_tag_brace() {
5071        let some_program_string = r#"startSketchOn(XY)
5072    |> startProfile(at = [0, 0])
5073    |> line(%, $)
5074    "#;
5075        assert_err(some_program_string, "Tag names must not be empty", [67, 68]);
5076    }
5077    #[test]
5078    fn test_parse_empty_tag_whitespace() {
5079        let some_program_string = r#"startSketchOn(XY)
5080    |> startProfile(at = [0, 0])
5081    |> line(%, $ ,01)
5082    "#;
5083        assert_err(some_program_string, "Tag names must not be empty", [67, 68]);
5084    }
5085
5086    #[test]
5087    fn test_parse_empty_tag_comma() {
5088        let some_program_string = r#"startSketchOn(XY)
5089    |> startProfile(at = [0, 0])
5090    |> line(%, $,)
5091    "#;
5092        assert_err(some_program_string, "Tag names must not be empty", [67, 68]);
5093    }
5094    #[test]
5095    fn test_parse_tag_starting_with_digit() {
5096        let some_program_string = r#"
5097    startSketchOn(XY)
5098    |> startProfile(at = [0, 0])
5099    |> line(%, $01)"#;
5100        assert_err(
5101            some_program_string,
5102            "Tag names must not start with a number. Tag starts with `01`",
5103            [72, 74],
5104        );
5105    }
5106    #[test]
5107    fn test_parse_tag_including_digit() {
5108        let some_program_string = r#"
5109    startSketchOn(XY)
5110    |> startProfile(at = [0, 0])
5111    |> line(%, tag = $var01)"#;
5112        assert_no_err(some_program_string);
5113    }
5114
5115    #[test]
5116    fn test_parse_param_bool_default() {
5117        let some_program_string = r#"fn patternTransform(
5118  use_original?: boolean = false,
5119) {}"#;
5120        assert_no_err(some_program_string);
5121    }
5122
5123    #[test]
5124    fn parse_function_types() {
5125        let code = r#"@settings(experimentalFeatures = allow)
5126foo = x: fn
5127foo = x: fn(number)
5128fn foo(x: fn(): number): fn { return 0 }
5129fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn { return 0 }
5130type fn
5131type foo = fn
5132type foo = fn(a: string, b: { f: fn(): any })
5133type foo = fn(a: string, b: {})
5134type foo = fn(a: string, b: { })
5135type foo = fn([fn])
5136type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
5137    "#;
5138        assert_no_err(code);
5139    }
5140
5141    #[test]
5142    fn experimental() {
5143        let code = r#"4 as foo"#;
5144        assert_err(code, "Use of `as` for tagging expressions is experimental", [5, 8]);
5145
5146        let code = r#"@settings(experimentalFeatures = allow)
51474 as foo
5148    "#;
5149        assert_no_err(code);
5150
5151        let code = r#"@settings(experimentalFeatures = warn)
51524 as foo
5153    "#;
5154        let (_, errs) = assert_no_err(code);
5155        assert_eq!(errs.len(), 1);
5156    }
5157
5158    #[test]
5159    fn test_parse_tag_starting_with_bang() {
5160        let some_program_string = r#"startSketchOn(XY)
5161    |> startProfile(at = [0, 0])
5162    |> line(%, $!var,01)
5163    "#;
5164        assert_err(some_program_string, "Tag names must not start with a bang", [67, 68]);
5165    }
5166    #[test]
5167    fn test_parse_tag_starting_with_dollar() {
5168        let some_program_string = r#"startSketchOn(XY)
5169    |> startProfile(at = [0, 0])
5170    |> line(%, $$,01)
5171    "#;
5172        assert_err(some_program_string, "Tag names must not start with a dollar", [67, 68]);
5173    }
5174    #[test]
5175    fn test_parse_tag_starting_with_fn() {
5176        let some_program_string = r#"startSketchOn(XY)
5177    |> startProfile(at = [0, 0])
5178    |> line(%, $fn,01)
5179    "#;
5180        assert_err(some_program_string, "Tag names must not start with a keyword", [67, 69]);
5181    }
5182    #[test]
5183    fn test_parse_tag_starting_with_a_comment() {
5184        let some_program_string = r#"startSketchOn(XY)
5185    |> startProfile(at = [0, 0])
5186    |> line(%, $//
5187    ,01)
5188    "#;
5189        assert_err(
5190            some_program_string,
5191            "Tag names must not start with a lineComment",
5192            [67, 69],
5193        );
5194    }
5195
5196    #[test]
5197    fn test_parse_tag_with_reserved_in_middle_works() {
5198        let some_program_string = r#"
5199    startSketchOn(XY)
5200    |> startProfile(at = [0, 0])
5201    |> line(end = [5, 5], tag = $sketching)
5202    "#;
5203        assert_no_err(some_program_string);
5204    }
5205
5206    #[test]
5207    fn test_parse_fn_call_then_field() {
5208        let some_program_string = "myFunction().field";
5209        let module_id = ModuleId::default();
5210        let tokens = crate::parsing::token::lex(some_program_string, module_id).unwrap(); // Updated import path
5211        let actual = expression.parse(tokens.as_slice()).unwrap();
5212        let Expr::MemberExpression(_expr) = actual else {
5213            panic!("expected member expression")
5214        };
5215    }
5216
5217    #[test]
5218    fn test_parse_array_missing_closing_bracket() {
5219        let some_program_string = r#"
5220sketch001 = startSketchOn(XZ) |> startProfile(at = [90.45, 119.09)"#;
5221        assert_err(
5222            some_program_string,
5223            "Encountered an unexpected character(s) before finding a closing bracket(`]`) for the array",
5224            [52, 60],
5225        );
5226    }
5227    #[test]
5228    fn test_parse_array_missing_comma() {
5229        let some_program_string = r#"
5230sketch001 = startSketchOn(XZ) |> startProfile(at = [90.45 119.09])"#;
5231        assert_err(
5232            some_program_string,
5233            "Unexpected character encountered. You might be missing a comma in between elements.",
5234            [53, 66],
5235        );
5236    }
5237    #[test]
5238    fn test_parse_array_reserved_word_early_exit() {
5239        // since there is an early exit if encountering a reserved word, the error should be about
5240        // that and not the missing comma
5241        let some_program_string = r#"
5242sketch001 = startSketchOn(XZ) |> startProfile(at = [90.45 $struct])"#;
5243        assert_err(
5244            some_program_string,
5245            "Encountered an unexpected character(s) before finding a closing bracket(`]`) for the array",
5246            [52, 53],
5247        );
5248    }
5249
5250    #[test]
5251    fn test_parse_array_random_brace() {
5252        let some_program_string = r#"
5253sketch001 = startSketchOn(XZ) |> startProfile(at = [}])"#;
5254        assert_err(
5255            some_program_string,
5256            "Encountered an unexpected character(s) before finding a closing bracket(`]`) for the array",
5257            [52, 53],
5258        );
5259    }
5260
5261    #[test]
5262    fn test_parse_object_missing_closing_brace() {
5263        let some_program_string = r#"{
5264            foo = bar,"#;
5265
5266        assert_err(
5267            some_program_string,
5268            "Encountered an unexpected character(s) before finding a closing brace(`}`) for the object",
5269            [0, 23],
5270        );
5271    }
5272    #[test]
5273    fn test_parse_object_reserved_word_early_exit() {
5274        // since there is an early exit if encountering a reserved word, the error should be about
5275        // that and not the missing comma
5276        let some_program_string = r#"{bar = foo struct = man}"#;
5277
5278        assert_err(
5279            some_program_string,
5280            "Encountered an unexpected character(s) before finding a closing brace(`}`) for the object",
5281            [0, 1],
5282        );
5283    }
5284    #[test]
5285    fn test_parse_object_missing_comma() {
5286        let some_program_string = r#"{
5287            foo = bar,
5288            bar = foo
5289            bat = man
5290        }"#;
5291
5292        assert_err(
5293            some_program_string,
5294            "Unexpected character encountered. You might be missing a comma in between properties.",
5295            [37, 78],
5296        );
5297    }
5298
5299    #[test]
5300    fn test_parse_object_missing_comma_one_line() {
5301        let some_program_string = r#"{bar = foo bat = man}"#;
5302
5303        assert_err(
5304            some_program_string,
5305            "Unexpected character encountered. You might be missing a comma in between properties.",
5306            [1, 21],
5307        );
5308    }
5309
5310    #[test]
5311    fn test_parse_object_random_bracket() {
5312        let some_program_string = r#"{]}"#;
5313
5314        assert_err(
5315            some_program_string,
5316            "Encountered an unexpected character(s) before finding a closing brace(`}`) for the object",
5317            [0, 1],
5318        );
5319    }
5320
5321    #[test]
5322    fn test_parse_object_shorthand_missing_comma() {
5323        let some_program_string = r#"
5324bar = 1
5325        {
5326            foo = bar,
5327            bar
5328            bat = man
5329        }"#;
5330
5331        assert_err(
5332            some_program_string,
5333            "Unexpected character encountered. You might be missing a comma in between properties.",
5334            [54, 89],
5335        );
5336    }
5337
5338    #[test]
5339    fn test_unary_not_on_keyword_bool() {
5340        let some_program_string = r#"!true"#;
5341        let module_id = ModuleId::default();
5342        let tokens = crate::parsing::token::lex(some_program_string, module_id).unwrap(); // Updated import path
5343        let actual = match unary_expression.parse(tokens.as_slice()) {
5344            // Use tokens.as_slice() for parsing
5345            Ok(x) => x,
5346            Err(e) => panic!("{e:?}"),
5347        };
5348        assert_eq!(actual.operator, UnaryOperator::Not);
5349        crate::parsing::top_level_parse(some_program_string).unwrap(); // Updated import path
5350    }
5351    #[test]
5352    fn test_sensible_error_when_missing_comma_between_fn_args() {
5353        let program_source = "startSketchOn(XY)
5354|> arc(
5355    endAbsolute = [0, 50]
5356    interiorAbsolute = [-50, 0]
5357)";
5358        let expected_src_start = program_source.find("]").unwrap();
5359        let tokens = crate::parsing::token::lex(program_source, ModuleId::default()).unwrap();
5360        ParseContext::init();
5361        let err = program
5362            .parse(tokens.as_slice())
5363            .expect_err("Program succeeded, but it should have failed");
5364        let cause = err
5365            .inner()
5366            .cause
5367            .as_ref()
5368            .expect("Found an error, but there was no cause. Add a cause.");
5369        assert_eq!(cause.message, "Missing comma between arguments, try adding a comma in",);
5370        assert_eq!(cause.source_range.start() - 1, expected_src_start);
5371    }
5372
5373    #[test]
5374    fn test_sensible_error_when_missing_rhs_of_kw_arg() {
5375        for (i, program) in ["f(x, y=)"].into_iter().enumerate() {
5376            let tokens = crate::parsing::token::lex(program, ModuleId::default()).unwrap();
5377            let err = fn_call_kw.parse(tokens.as_slice()).unwrap_err();
5378            let cause = err.inner().cause.as_ref().unwrap();
5379            assert_eq!(
5380                cause.message, "This argument has a label, but no value. Put some value after the equals sign",
5381                "failed test {i}: {program}"
5382            );
5383            assert_eq!(
5384                cause.source_range.start(),
5385                program.find("y").unwrap(),
5386                "failed test {i}: {program}"
5387            );
5388        }
5389    }
5390
5391    #[test]
5392    fn test_sensible_error_when_unexpected_token_in_fn_call() {
5393        let program_source = "1
5394|> extrude(
5395  length=depth,
5396})";
5397        let expected_src_start = program_source.find("}").expect("Program should have an extraneous }");
5398        let tokens = crate::parsing::token::lex(program_source, ModuleId::default()).unwrap();
5399        ParseContext::init();
5400        let err = program.parse(tokens.as_slice()).unwrap_err();
5401        let cause = err
5402            .inner()
5403            .cause
5404            .as_ref()
5405            .expect("Found an error, but there was no cause. Add a cause.");
5406        assert_eq!(cause.message, "There was an unexpected `}`. Try removing it.",);
5407        assert_eq!(cause.source_range.start(), expected_src_start);
5408    }
5409
5410    #[test]
5411    fn test_sensible_error_when_using_keyword_as_arg_label() {
5412        for (i, program) in ["pow(2, fn = 8)"].into_iter().enumerate() {
5413            let tokens = crate::parsing::token::lex(program, ModuleId::default()).unwrap();
5414            let err = match fn_call_kw.parse(tokens.as_slice()) {
5415                Err(e) => e,
5416                Ok(_ast) => {
5417                    panic!("Expected this to error but it didn't");
5418                }
5419            };
5420            let cause = err.inner().cause.as_ref().unwrap();
5421            assert_eq!(
5422                cause.message, "`fn` is not the name of an argument (it's a reserved keyword)",
5423                "failed test {i}: {program}"
5424            );
5425            assert_eq!(
5426                cause.source_range.start(),
5427                program.find("fn").unwrap(),
5428                "failed test {i}: {program}"
5429            );
5430        }
5431    }
5432
5433    #[test]
5434    fn test_sensible_error_when_missing_rhs_of_obj_property() {
5435        for (i, program) in ["{x = 1, y =}"].into_iter().enumerate() {
5436            let tokens = crate::parsing::token::lex(program, ModuleId::default()).unwrap();
5437            let err = object.parse(tokens.as_slice()).unwrap_err();
5438            let cause = err.inner().cause.as_ref().unwrap();
5439            assert_eq!(
5440                cause.message, "This property has a label, but no value. Put some value after the equals sign",
5441                "failed test {i}: {program}"
5442            );
5443            assert_eq!(
5444                cause.source_range.start(),
5445                program.rfind('=').unwrap(),
5446                "failed test {i}: {program}"
5447            );
5448        }
5449    }
5450
5451    #[test]
5452    fn test_sensible_error_duplicated_args() {
5453        let program = r#"f(arg = 1, normal = 44, arg = 2)"#;
5454        let (_, mut errs) = assert_no_fatal(program);
5455        assert_eq!(errs.len(), 1);
5456        let err = errs.pop().unwrap();
5457        assert_eq!(
5458            err.message,
5459            "You've used the parameter labelled 'arg' 2 times in a single function call. You can only set each parameter once! Remove all but one use.",
5460        );
5461    }
5462
5463    #[test]
5464    fn test_sensible_error_when_missing_else() {
5465        let program_source = "x = if (true) { 2 }";
5466        let expected_src_start = program_source.find("if").unwrap();
5467        let cause = must_fail_compilation(program_source);
5468        assert!(!cause.was_fatal);
5469        assert_eq!(cause.err.message, MISSING_ELSE);
5470        assert_eq!(cause.err.source_range.start(), expected_src_start);
5471    }
5472
5473    #[test]
5474    fn test_sensible_error_when_using_keyword_as_object_key() {
5475        let source = r#"material = {
5476  type = "Steel 45"
5477}"#;
5478        let expected_src_start = source.find("type").unwrap();
5479        let cause = must_fail_compilation(source);
5480        assert!(cause.was_fatal);
5481        assert_eq!(cause.err.message, KEYWORD_EXPECTING_IDENTIFIER);
5482        assert_eq!(cause.err.source_range.start(), expected_src_start);
5483    }
5484
5485    struct MustFail {
5486        err: CompilationError,
5487        was_fatal: bool,
5488    }
5489
5490    fn must_fail_compilation(program_source: &str) -> MustFail {
5491        let tokens = crate::parsing::token::lex(program_source, ModuleId::default()).unwrap();
5492        ParseContext::init();
5493        match program.parse(tokens.as_slice()) {
5494            // Fatal parse error
5495            Err(e) => {
5496                let err = e
5497                    .into_inner()
5498                    .cause
5499                    .expect("found a parse error, but no cause. Add a cause.");
5500                MustFail { err, was_fatal: true }
5501            } // Nonfatal parse error
5502            Ok(_) => {
5503                let mut e = None;
5504                CTXT.with_borrow(|v| e = v.clone().map(|ctxt| ctxt.errors));
5505                let err = e
5506                    .unwrap()
5507                    .first()
5508                    .expect("Program succeeded, but it should have failed")
5509                    .to_owned();
5510                MustFail { err, was_fatal: false }
5511            }
5512        }
5513    }
5514
5515    #[test]
5516    fn test_sensible_error_when_missing_else_brace() {
5517        let program_source = "x = if (true) { 2 } else ";
5518        let expected_src_start = program_source.find("else").unwrap();
5519        let cause = must_fail_compilation(program_source);
5520        assert!(!cause.was_fatal);
5521        assert_eq!(cause.err.message, ELSE_STRUCTURE);
5522        assert_eq!(cause.err.source_range.start(), expected_src_start);
5523    }
5524
5525    #[test]
5526    fn test_sensible_error_when_does_not_end_in_expr() {
5527        let program_source = "x = if (true) { 2 } else {y = 3}";
5528        let expected_src_start = program_source.find("else").unwrap();
5529        let cause = must_fail_compilation(program_source);
5530        assert!(!cause.was_fatal);
5531        assert_eq!(cause.err.message, ELSE_MUST_END_IN_EXPR);
5532        assert_eq!(cause.err.source_range.start(), expected_src_start);
5533    }
5534}
5535
5536#[cfg(test)]
5537mod snapshot_math_tests {
5538    use super::*;
5539
5540    // This macro generates a test function with the given function name.
5541    // The macro takes a KCL program, ensures it tokenizes and parses, then compares
5542    // its parsed AST to a snapshot (kept in this repo in a file under snapshots/ dir)
5543    macro_rules! snapshot_test {
5544        ($func_name:ident, $test_kcl_program:expr_2021) => {
5545            #[test]
5546            fn $func_name() {
5547                let module_id = crate::ModuleId::default();
5548                let tokens = crate::parsing::token::lex($test_kcl_program, module_id).unwrap();
5549                ParseContext::init();
5550
5551                let actual = match binary_expression.parse(tokens.as_slice()) {
5552                    Ok(x) => x,
5553                    Err(_e) => panic!("could not parse test"),
5554                };
5555                insta::assert_json_snapshot!(actual);
5556                let _ = ParseContext::take();
5557            }
5558        };
5559    }
5560
5561    snapshot_test!(a, "1 + 2");
5562    snapshot_test!(b, "1+2");
5563    snapshot_test!(c, "1 -2");
5564    snapshot_test!(d, "1 + 2 * 3");
5565    snapshot_test!(e, "1 * ( 2 + 3 )");
5566    snapshot_test!(f, "1 * ( 2 + 3 ) / 4");
5567    snapshot_test!(g, "1 + ( 2 + 3 ) / 4");
5568    snapshot_test!(h, "1 * (( 2 + 3 ) / 4 + 5 )");
5569    snapshot_test!(i, "1 * ((( 2 + 3 )))");
5570    snapshot_test!(j, "distance * p * FOS * 6 / (sigmaAllow * width)");
5571    snapshot_test!(k, "2 + (((3)))");
5572}
5573
5574#[cfg(test)]
5575mod snapshot_tests {
5576    use super::*;
5577
5578    // This macro generates a test function with the given function name.
5579    // The macro takes a KCL program, ensures it tokenizes and parses, then compares
5580    // its parsed AST to a snapshot (kept in this repo in a file under snapshots/ dir)
5581    macro_rules! snapshot_test {
5582        ($func_name:ident, $test_kcl_program:expr_2021) => {
5583            #[test]
5584            fn $func_name() {
5585                let module_id = crate::ModuleId::default();
5586                println!("{}", $test_kcl_program);
5587                let tokens = crate::parsing::token::lex($test_kcl_program, module_id).unwrap();
5588                print_tokens(tokens.as_slice());
5589                ParseContext::init();
5590                let actual = match program.parse(tokens.as_slice()) {
5591                    Ok(x) => x,
5592                    Err(e) => panic!("could not parse test: {e:?}"),
5593                };
5594                let mut settings = insta::Settings::clone_current();
5595                settings.set_sort_maps(true);
5596                settings.bind(|| {
5597                    insta::assert_json_snapshot!(actual);
5598                });
5599                let _ = ParseContext::take();
5600            }
5601        };
5602    }
5603
5604    snapshot_test!(
5605        a,
5606        r#"boxSketch = startSketchOn(XY)
5607    |> startProfileAt(at = [0, 0])
5608    |> line(at = [0, 10])
5609    |> tangentialArc(end = [-5, 5])
5610    |> line(at = [5, -15])
5611    |> extrude(length=10)
5612"#
5613    );
5614    snapshot_test!(b, "myVar = min(x=5 , y=-legLen(5, z=4))"); // Space before comma
5615
5616    snapshot_test!(c, "myVar = min(x=-legLen(a=5, b=4), y=5)");
5617    snapshot_test!(d, "myVar = 5 + 6 |> myFunc(45)");
5618    snapshot_test!(e, "x = 1 * (3 - 4)");
5619    snapshot_test!(f, r#"x = 1 // this is an inline comment"#);
5620    snapshot_test!(
5621        g,
5622        r#"fn x() {
5623        return sg
5624        return sg
5625      }"#
5626    );
5627    snapshot_test!(d2, r#"x = -leg2 + thickness"#);
5628    snapshot_test!(
5629        h,
5630        r#"obj = { a = 1, b = 2 }
5631    height = 1 - obj.a"#
5632    );
5633    snapshot_test!(
5634        i,
5635        r#"obj = { a = 1, b = 2 }
5636     height = 1 - obj["a"]"#
5637    );
5638    snapshot_test!(
5639        j,
5640        r#"obj = { a = 1, b = 2 }
5641    height = obj["a"] - 1"#
5642    );
5643    snapshot_test!(
5644        k,
5645        r#"obj = { a = 1, b = 2 }
5646    height = [1 - obj["a"], 0]"#
5647    );
5648    snapshot_test!(
5649        l,
5650        r#"obj = { a = 1, b = 2 }
5651    height = [obj["a"] - 1, 0]"#
5652    );
5653    snapshot_test!(
5654        m,
5655        r#"obj = {a = 1, b = 2 }
5656    height = [obj["a"] -1, 0]"#
5657    );
5658    snapshot_test!(n, "height = 1 - obj.a");
5659    snapshot_test!(o, "six = 1 + 2 + 3");
5660    snapshot_test!(p, "five = 3 * 1 + 2");
5661    snapshot_test!(q, r#"height = [ obj["a"], 0 ]"#);
5662    snapshot_test!(
5663        r,
5664        r#"obj = { a = 1, b = 2 }
5665    height = obj["a"]"#
5666    );
5667    snapshot_test!(s, r#"prop = yo["one"][two]"#);
5668    snapshot_test!(t, r#"pt1 = b1[x]"#);
5669    snapshot_test!(u, "prop = yo.one.two.three.four");
5670    snapshot_test!(v, r#"pt1 = b1[0]"#);
5671    snapshot_test!(w, r#"pt1 = b1['zero']"#);
5672    snapshot_test!(x, r#"pt1 = b1.zero"#);
5673    snapshot_test!(y, r#"sg = startSketchOn(XY) |> startProfile(pos)"#);
5674    snapshot_test!(
5675        z,
5676        "sg = startSketchOn(XY)
5677    |> startProfile(pos) |> line([0, -scale])"
5678    );
5679    snapshot_test!(aa, r#"sg = -scale"#);
5680    snapshot_test!(ab, "line(endAbsolute = [0, -1])");
5681    snapshot_test!(
5682        ad,
5683        r#"
5684    fn firstPrimeNumber() {
5685        return 2
5686    }
5687    firstPrimeNumber()"#
5688    );
5689    snapshot_test!(
5690        ae,
5691        r#"fn thing(param) {
5692        return true
5693    }
5694    thing(false)"#
5695    );
5696    snapshot_test!(
5697        af,
5698        r#"mySketch = startSketchOn(XY)
5699        |> startProfile(at = [0,0])
5700        |> line(endAbsolute = [0, 1], tag = $myPath)
5701        |> line(endAbsolute = [1, 1])
5702        |> line(endAbsolute = [1, 0], tag = $rightPath)
5703        |> close()"#
5704    );
5705    snapshot_test!(
5706        ag,
5707        "mySketch = startSketchOn(XY) |> startProfile(at = [0,0]) |> line(endAbsolute = [1, 1]) |> close()"
5708    );
5709    snapshot_test!(ah, "myBox = startSketchOn(XY) |> startProfile(at = p)");
5710    snapshot_test!(ai, r#"myBox = f(1) |> g(2)"#);
5711    snapshot_test!(
5712        aj,
5713        r#"myBox = startSketchOn(XY) |> startProfile(at = p) |> line(end = [0, l])"#
5714    );
5715    snapshot_test!(ak, "line(endAbsolute = [0, 1])");
5716    snapshot_test!(ap, "mySketch = startSketchOn(XY) |> startProfile(at = [0,0])");
5717    snapshot_test!(aq, "log(number = 5, msg = \"hello\", id=aIdentifier)");
5718    snapshot_test!(ar, r#"5 + "a""#);
5719    snapshot_test!(at, "line([0, l])");
5720    snapshot_test!(au, include_str!("../../e2e/executor/inputs/cylinder.kcl"));
5721    snapshot_test!(av, "fn f(angle?) { return default(maybe=angle, otherwise=360) }");
5722    snapshot_test!(
5723        aw,
5724        "numbers = [
5725            1,
5726            // A,
5727            // B,
5728            3,
5729        ]"
5730    );
5731    snapshot_test!(
5732        ax,
5733        "numbers = [
5734            1,
5735            2,
5736            // A,
5737            // B,
5738        ]"
5739    );
5740    snapshot_test!(
5741        ay,
5742        "let props = {
5743            a: 1,
5744            // b: 2,
5745            c: 3,
5746        }"
5747    );
5748    snapshot_test!(
5749        az,
5750        "props = {
5751            a: 1,
5752            // b: 2,
5753            c: 3
5754        }"
5755    );
5756    snapshot_test!(
5757        bb,
5758        r#"
5759my14 = 4 ^ 2 - 3 ^ 2 * 2
5760"#
5761    );
5762    snapshot_test!(
5763        bc,
5764        r#"x = if true {
5765            3
5766        } else {
5767            4
5768        }"#
5769    );
5770    snapshot_test!(
5771        bd,
5772        r#"x = if true {
5773            3
5774        } else if func(radius) {
5775            4
5776        } else {
5777            5
5778        }"#
5779    );
5780    snapshot_test!(be, "x = 3 == 3");
5781    snapshot_test!(bf, "x = 3 != 3");
5782    snapshot_test!(bg, r#"x = 4"#);
5783    snapshot_test!(bh, "obj = {center = [10, 10], radius =5}");
5784    snapshot_test!(
5785        bi,
5786        r#"x = 3
5787        obj = { x, y = 4}"#
5788    );
5789    snapshot_test!(bj, "true");
5790    snapshot_test!(bk, "truee");
5791    snapshot_test!(bl, "x = !true");
5792    snapshot_test!(bm, "x = true & false");
5793    snapshot_test!(bn, "x = true | false");
5794    snapshot_test!(kw_function_unnamed_first, r#"val = foo(x, y = z)"#);
5795    snapshot_test!(kw_function_all_named, r#"val = foo(x = a, y = b)"#);
5796    snapshot_test!(kw_function_decl_all_labeled, r#"fn foo(x, y) { return 1 }"#);
5797    snapshot_test!(kw_function_decl_first_unlabeled, r#"fn foo(@x, y) { return 1 }"#);
5798    snapshot_test!(kw_function_decl_with_default_no_type, r#"fn foo(x? = 2) { return 1 }"#);
5799    snapshot_test!(
5800        kw_function_decl_with_default_and_type,
5801        r#"fn foo(x?: number = 2) { return 1 }"#
5802    );
5803    snapshot_test!(kw_function_call_in_pipe, r#"val = 1 |> f(arg = x)"#);
5804    snapshot_test!(
5805        kw_function_call_multiline,
5806        r#"val = f(
5807             arg = x,
5808             foo = x,
5809             bar = x,
5810           )"#
5811    );
5812    snapshot_test!(
5813        kw_function_call_multiline_with_comments,
5814        r#"val = f(
5815             arg = x,
5816             // foo = x,
5817             bar = x,
5818           )"#
5819    );
5820    snapshot_test!(kw_function_in_binary_op, r#"val = f(x = 1) + 1"#);
5821    snapshot_test!(
5822        array_ranges,
5823        r#"incl = [1..10]
5824        excl = [0..<10]"#
5825    );
5826    snapshot_test!(space_between_unary_and_operand, r#"x = - 1"#);
5827    snapshot_test!(
5828        space_between_expr_and_array,
5829        r#"outer_points = [1, 2, 3]
5830i = 0
5831outer_points[ if i + 1 < 5 { i + 1 } else { 0 } ]"#
5832    );
5833}
5834
5835#[allow(unused)]
5836#[cfg(test)]
5837pub(crate) fn print_tokens(tokens: TokenSlice) {
5838    for (i, tok) in tokens.iter().enumerate() {
5839        println!("{i:.2}: ({:?}):) '{}'", tok.token_type, tok.value.replace("\n", "\\n"));
5840    }
5841}