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