@ -27,15 +27,4 @@ criterion_main!(benches);
 | 
			
		||||
 | 
			
		||||
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
 | 
			
		||||
const PIPES_PROGRAM: &str = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
 | 
			
		||||
const CUBE_PROGRAM: &str = r#"fn cube = (pos, scale) => {
 | 
			
		||||
  const sg = startSketchAt(pos)
 | 
			
		||||
    |> line([0, scale], %)
 | 
			
		||||
    |> line([scale, 0], %)
 | 
			
		||||
    |> line([0, -scale], %)
 | 
			
		||||
 | 
			
		||||
  return sg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const b1 = cube([0,0], 10)
 | 
			
		||||
const pt1 = b1[0]
 | 
			
		||||
show(b1)"#;
 | 
			
		||||
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
 | 
			
		||||
 | 
			
		||||
@ -82,7 +82,10 @@ impl Program {
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    let custom_white_space_or_comment = match self.non_code_meta.non_code_nodes.get(&index) {
 | 
			
		||||
                        Some(custom_white_space_or_comment) => custom_white_space_or_comment.format(&indentation),
 | 
			
		||||
                        Some(noncodes) => noncodes
 | 
			
		||||
                            .iter()
 | 
			
		||||
                            .map(|custom_white_space_or_comment| custom_white_space_or_comment.format(&indentation))
 | 
			
		||||
                            .collect::<String>(),
 | 
			
		||||
                        None => String::new(),
 | 
			
		||||
                    };
 | 
			
		||||
                    let end_string = if custom_white_space_or_comment.is_empty() {
 | 
			
		||||
@ -707,30 +710,35 @@ pub struct NonCodeNode {
 | 
			
		||||
impl NonCodeNode {
 | 
			
		||||
    pub fn value(&self) -> String {
 | 
			
		||||
        match &self.value {
 | 
			
		||||
            NonCodeValue::InlineComment { value } => value.clone(),
 | 
			
		||||
            NonCodeValue::BlockComment { value } => value.clone(),
 | 
			
		||||
            NonCodeValue::NewLineBlockComment { value } => value.clone(),
 | 
			
		||||
            NonCodeValue::InlineComment { value, style: _ } => value.clone(),
 | 
			
		||||
            NonCodeValue::BlockComment { value, style: _ } => value.clone(),
 | 
			
		||||
            NonCodeValue::NewLineBlockComment { value, style: _ } => value.clone(),
 | 
			
		||||
            NonCodeValue::NewLine => "\n\n".to_string(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn format(&self, indentation: &str) -> String {
 | 
			
		||||
        match &self.value {
 | 
			
		||||
            NonCodeValue::InlineComment { value } => format!(" // {}\n", value),
 | 
			
		||||
            NonCodeValue::BlockComment { value } => {
 | 
			
		||||
            NonCodeValue::InlineComment {
 | 
			
		||||
                value,
 | 
			
		||||
                style: CommentStyle::Line,
 | 
			
		||||
            } => format!(" // {}\n", value),
 | 
			
		||||
            NonCodeValue::InlineComment {
 | 
			
		||||
                value,
 | 
			
		||||
                style: CommentStyle::Block,
 | 
			
		||||
            } => format!(" /* {} */", value),
 | 
			
		||||
            NonCodeValue::BlockComment { value, style } => {
 | 
			
		||||
                let add_start_new_line = if self.start == 0 { "" } else { "\n" };
 | 
			
		||||
                if value.contains('\n') {
 | 
			
		||||
                    format!("{}{}/* {} */\n", add_start_new_line, indentation, value)
 | 
			
		||||
                } else {
 | 
			
		||||
                    format!("{}{}// {}\n", add_start_new_line, indentation, value)
 | 
			
		||||
                match style {
 | 
			
		||||
                    CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
 | 
			
		||||
                    CommentStyle::Line => format!("{}{}// {}\n", add_start_new_line, indentation, value),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            NonCodeValue::NewLineBlockComment { value } => {
 | 
			
		||||
            NonCodeValue::NewLineBlockComment { value, style } => {
 | 
			
		||||
                let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
 | 
			
		||||
                if value.contains('\n') {
 | 
			
		||||
                    format!("{}{}/* {} */\n", add_start_new_line, indentation, value)
 | 
			
		||||
                } else {
 | 
			
		||||
                    format!("{}{}// {}\n", add_start_new_line, indentation, value)
 | 
			
		||||
                match style {
 | 
			
		||||
                    CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
 | 
			
		||||
                    CommentStyle::Line => format!("{}{}// {}\n", add_start_new_line, indentation, value),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            NonCodeValue::NewLine => "\n\n".to_string(),
 | 
			
		||||
@ -738,14 +746,27 @@ impl NonCodeNode {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
#[serde(tag = "type", rename_all = "camelCase")]
 | 
			
		||||
pub enum CommentStyle {
 | 
			
		||||
    /// Like // foo
 | 
			
		||||
    Line,
 | 
			
		||||
    /// Like /* foo */
 | 
			
		||||
    Block,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
#[serde(tag = "type", rename_all = "camelCase")]
 | 
			
		||||
pub enum NonCodeValue {
 | 
			
		||||
    /// An inline comment.
 | 
			
		||||
    /// An example of this is the following: `1 + 1 // This is an inline comment`.
 | 
			
		||||
    /// Here are examples:
 | 
			
		||||
    /// `1 + 1 // This is an inline comment`.
 | 
			
		||||
    /// `1 + 1 /* Here's another */`.
 | 
			
		||||
    InlineComment {
 | 
			
		||||
        value: String,
 | 
			
		||||
        style: CommentStyle,
 | 
			
		||||
    },
 | 
			
		||||
    /// A block comment.
 | 
			
		||||
    /// An example of this is the following:
 | 
			
		||||
@ -759,11 +780,13 @@ pub enum NonCodeValue {
 | 
			
		||||
    /// If it did it would be a `NewLineBlockComment`.
 | 
			
		||||
    BlockComment {
 | 
			
		||||
        value: String,
 | 
			
		||||
        style: CommentStyle,
 | 
			
		||||
    },
 | 
			
		||||
    /// A block comment that has a new line above it.
 | 
			
		||||
    /// The user explicitly added a new line above the block comment.
 | 
			
		||||
    NewLineBlockComment {
 | 
			
		||||
        value: String,
 | 
			
		||||
        style: CommentStyle,
 | 
			
		||||
    },
 | 
			
		||||
    // A new line like `\n\n` NOT a new line like `\n`.
 | 
			
		||||
    // This is also not a comment.
 | 
			
		||||
@ -774,7 +797,7 @@ pub enum NonCodeValue {
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
#[serde(rename_all = "camelCase")]
 | 
			
		||||
pub struct NonCodeMeta {
 | 
			
		||||
    pub non_code_nodes: HashMap<usize, NonCodeNode>,
 | 
			
		||||
    pub non_code_nodes: HashMap<usize, Vec<NonCodeNode>>,
 | 
			
		||||
    pub start: Option<NonCodeNode>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -795,7 +818,10 @@ impl<'de> Deserialize<'de> for NonCodeMeta {
 | 
			
		||||
        let helper = NonCodeMetaHelper::deserialize(deserializer)?;
 | 
			
		||||
        let mut non_code_nodes = HashMap::new();
 | 
			
		||||
        for (key, value) in helper.non_code_nodes {
 | 
			
		||||
            non_code_nodes.insert(key.parse().map_err(serde::de::Error::custom)?, value);
 | 
			
		||||
            non_code_nodes
 | 
			
		||||
                .entry(key.parse().map_err(serde::de::Error::custom)?)
 | 
			
		||||
                .or_insert(Vec::new())
 | 
			
		||||
                .push(value);
 | 
			
		||||
        }
 | 
			
		||||
        Ok(NonCodeMeta {
 | 
			
		||||
            non_code_nodes,
 | 
			
		||||
@ -804,6 +830,12 @@ impl<'de> Deserialize<'de> for NonCodeMeta {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl NonCodeMeta {
 | 
			
		||||
    pub fn insert(&mut self, i: usize, new: NonCodeNode) {
 | 
			
		||||
        self.non_code_nodes.entry(i).or_default().push(new);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
#[serde(tag = "type")]
 | 
			
		||||
@ -2385,7 +2417,9 @@ impl PipeExpression {
 | 
			
		||||
                let mut s = statement.recast(options, indentation_level + 1, true);
 | 
			
		||||
                let non_code_meta = self.non_code_meta.clone();
 | 
			
		||||
                if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
 | 
			
		||||
                    s += non_code_meta_value.format(&indentation).trim_end_matches('\n')
 | 
			
		||||
                    for val in non_code_meta_value {
 | 
			
		||||
                        s += val.format(&indentation).trim_end_matches('\n')
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if index != self.body.len() - 1 {
 | 
			
		||||
@ -2869,9 +2903,9 @@ show(part001)"#;
 | 
			
		||||
            recasted,
 | 
			
		||||
            r#"fn myFn = () => {
 | 
			
		||||
  // this is a comment
 | 
			
		||||
  const yo = { a: { b: { c: '123' } } }
 | 
			
		||||
  /* block
 | 
			
		||||
  const yo = { a: { b: { c: '123' } } } /* block
 | 
			
		||||
  comment */
 | 
			
		||||
 | 
			
		||||
  const key = 'c'
 | 
			
		||||
  // this is also a comment
 | 
			
		||||
  return things
 | 
			
		||||
@ -2913,9 +2947,8 @@ const mySk1 = startSketchOn('XY')
 | 
			
		||||
  |> lineTo({ to: [0, 1], tag: 'myTag' }, %)
 | 
			
		||||
  |> lineTo([1, 1], %)
 | 
			
		||||
  /* and
 | 
			
		||||
  here
 | 
			
		||||
 | 
			
		||||
a comment between pipe expression statements */
 | 
			
		||||
  here */
 | 
			
		||||
  // a comment between pipe expression statements
 | 
			
		||||
  |> rx(90, %)
 | 
			
		||||
  // and another with just white space between others below
 | 
			
		||||
  |> ry(45, %)
 | 
			
		||||
@ -2988,16 +3021,19 @@ const things = "things"
 | 
			
		||||
        let program = parser.ast().unwrap();
 | 
			
		||||
 | 
			
		||||
        let recasted = program.recast(&Default::default(), 0);
 | 
			
		||||
        assert_eq!(recasted.trim(), some_program_string.trim());
 | 
			
		||||
        let expected = some_program_string.trim();
 | 
			
		||||
        // Currently new parser removes an empty line
 | 
			
		||||
        let actual = recasted.trim();
 | 
			
		||||
        assert_eq!(actual, expected);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_recast_comment_tokens_inside_strings() {
 | 
			
		||||
        let some_program_string = r#"let b = {
 | 
			
		||||
  "end": 141,
 | 
			
		||||
  "start": 125,
 | 
			
		||||
  "type": "NonCodeNode",
 | 
			
		||||
  "value": "
 | 
			
		||||
  end: 141,
 | 
			
		||||
  start: 125,
 | 
			
		||||
  type: "NonCodeNode",
 | 
			
		||||
  value: "
 | 
			
		||||
 // a comment
 | 
			
		||||
   "
 | 
			
		||||
}"#;
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
 | 
			
		||||
 | 
			
		||||
use crate::executor::SourceRange;
 | 
			
		||||
 | 
			
		||||
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS)]
 | 
			
		||||
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
#[serde(tag = "kind", rename_all = "snake_case")]
 | 
			
		||||
pub enum KclError {
 | 
			
		||||
@ -28,7 +28,7 @@ pub enum KclError {
 | 
			
		||||
    Engine(KclErrorDetails),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, ts_rs::TS)]
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone)]
 | 
			
		||||
#[ts(export)]
 | 
			
		||||
pub struct KclErrorDetails {
 | 
			
		||||
    #[serde(rename = "sourceRanges")]
 | 
			
		||||
@ -78,6 +78,22 @@ impl KclError {
 | 
			
		||||
            KclError::Engine(e) => e.source_ranges.clone(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the inner error message.
 | 
			
		||||
    pub fn message(&self) -> &str {
 | 
			
		||||
        match &self {
 | 
			
		||||
            KclError::Syntax(e) => &e.message,
 | 
			
		||||
            KclError::Semantic(e) => &e.message,
 | 
			
		||||
            KclError::Type(e) => &e.message,
 | 
			
		||||
            KclError::Unimplemented(e) => &e.message,
 | 
			
		||||
            KclError::Unexpected(e) => &e.message,
 | 
			
		||||
            KclError::ValueAlreadyDefined(e) => &e.message,
 | 
			
		||||
            KclError::UndefinedValue(e) => &e.message,
 | 
			
		||||
            KclError::InvalidExpression(e) => &e.message,
 | 
			
		||||
            KclError::Engine(e) => &e.message,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
 | 
			
		||||
        let (message, _, _) = self.get_message_line_column(code);
 | 
			
		||||
        let source_ranges = self.source_ranges();
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ use std::{collections::HashMap, str::FromStr};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    ast::types::{
 | 
			
		||||
        ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, ExpressionStatement,
 | 
			
		||||
        ArrayExpression, BinaryExpression, BinaryPart, BodyItem, CallExpression, CommentStyle, ExpressionStatement,
 | 
			
		||||
        FunctionExpression, Identifier, Literal, LiteralIdentifier, MemberExpression, MemberObject, NonCodeMeta,
 | 
			
		||||
        NonCodeNode, NonCodeValue, ObjectExpression, ObjectKeyInfo, ObjectProperty, PipeExpression, PipeSubstitution,
 | 
			
		||||
        Program, ReturnStatement, UnaryExpression, UnaryOperator, Value, VariableDeclaration, VariableDeclarator,
 | 
			
		||||
@ -13,6 +13,8 @@ use crate::{
 | 
			
		||||
    token::{Token, TokenType},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
mod parser_impl;
 | 
			
		||||
 | 
			
		||||
pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%";
 | 
			
		||||
pub const PIPE_OPERATOR: &str = "|>";
 | 
			
		||||
 | 
			
		||||
@ -180,24 +182,7 @@ impl Parser {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn ast(&self) -> Result<Program, KclError> {
 | 
			
		||||
        let body = self.make_body(
 | 
			
		||||
            0,
 | 
			
		||||
            vec![],
 | 
			
		||||
            NonCodeMeta {
 | 
			
		||||
                non_code_nodes: HashMap::new(),
 | 
			
		||||
                start: None,
 | 
			
		||||
            },
 | 
			
		||||
        )?;
 | 
			
		||||
        let end = match self.get_token(body.last_index) {
 | 
			
		||||
            Ok(token) => token.end,
 | 
			
		||||
            Err(_) => self.tokens[self.tokens.len() - 1].end,
 | 
			
		||||
        };
 | 
			
		||||
        Ok(Program {
 | 
			
		||||
            start: 0,
 | 
			
		||||
            end,
 | 
			
		||||
            body: body.body,
 | 
			
		||||
            non_code_meta: body.non_code_meta,
 | 
			
		||||
        })
 | 
			
		||||
        parser_impl::run_parser(&mut self.tokens.as_slice())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn make_identifier(&self, index: usize) -> Result<Identifier, KclError> {
 | 
			
		||||
@ -209,7 +194,7 @@ impl Parser {
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn make_literal(&self, index: usize) -> Result<Literal, KclError> {
 | 
			
		||||
    fn make_literal(&self, index: usize) -> Result<Literal, KclError> {
 | 
			
		||||
        let token = self.get_token(index)?;
 | 
			
		||||
        let value = if token.token_type == TokenType::Number {
 | 
			
		||||
            if let Ok(value) = token.value.parse::<i64>() {
 | 
			
		||||
@ -295,6 +280,11 @@ impl Parser {
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let is_block_style = non_code_tokens
 | 
			
		||||
            .first()
 | 
			
		||||
            .map(|tok| matches!(tok.token_type, TokenType::BlockComment))
 | 
			
		||||
            .unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
        let full_string = non_code_tokens
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|t| {
 | 
			
		||||
@ -336,11 +326,32 @@ impl Parser {
 | 
			
		||||
            value: if start_end_string.starts_with("\n\n") && is_new_line_comment {
 | 
			
		||||
                // Preserve if they want a whitespace line before the comment.
 | 
			
		||||
                // But let's just allow one.
 | 
			
		||||
                NonCodeValue::NewLineBlockComment { value: full_string }
 | 
			
		||||
            } else if is_new_line_comment {
 | 
			
		||||
                NonCodeValue::BlockComment { value: full_string }
 | 
			
		||||
                NonCodeValue::NewLineBlockComment {
 | 
			
		||||
                    value: full_string,
 | 
			
		||||
                    style: if is_block_style {
 | 
			
		||||
                        CommentStyle::Block
 | 
			
		||||
                    } else {
 | 
			
		||||
                NonCodeValue::InlineComment { value: full_string }
 | 
			
		||||
                        CommentStyle::Line
 | 
			
		||||
                    },
 | 
			
		||||
                }
 | 
			
		||||
            } else if is_new_line_comment {
 | 
			
		||||
                NonCodeValue::BlockComment {
 | 
			
		||||
                    value: full_string,
 | 
			
		||||
                    style: if is_block_style {
 | 
			
		||||
                        CommentStyle::Block
 | 
			
		||||
                    } else {
 | 
			
		||||
                        CommentStyle::Line
 | 
			
		||||
                    },
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                NonCodeValue::InlineComment {
 | 
			
		||||
                    value: full_string,
 | 
			
		||||
                    style: if is_block_style {
 | 
			
		||||
                        CommentStyle::Block
 | 
			
		||||
                    } else {
 | 
			
		||||
                        CommentStyle::Line
 | 
			
		||||
                    },
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
        Ok((Some(node), end_index - 1))
 | 
			
		||||
@ -1064,7 +1075,7 @@ impl Parser {
 | 
			
		||||
        let mut _non_code_meta: NonCodeMeta;
 | 
			
		||||
        if let Some(node) = next_pipe.non_code_node {
 | 
			
		||||
            _non_code_meta = non_code_meta;
 | 
			
		||||
            _non_code_meta.non_code_nodes.insert(previous_values.len(), node);
 | 
			
		||||
            _non_code_meta.insert(previous_values.len(), node);
 | 
			
		||||
        } else {
 | 
			
		||||
            _non_code_meta = non_code_meta;
 | 
			
		||||
        }
 | 
			
		||||
@ -1435,7 +1446,7 @@ impl Parser {
 | 
			
		||||
        self.make_params(next_brace_or_comma_token.index, _previous_params)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> {
 | 
			
		||||
    fn make_unary_expression(&self, index: usize) -> Result<UnaryExpressionResult, KclError> {
 | 
			
		||||
        let current_token = self.get_token(index)?;
 | 
			
		||||
        let next_token = self.next_meaningful_token(index, None)?;
 | 
			
		||||
        if next_token.token.is_none() {
 | 
			
		||||
@ -1633,7 +1644,7 @@ impl Parser {
 | 
			
		||||
                if previous_body.is_empty() {
 | 
			
		||||
                    non_code_meta.start = next_token.non_code_node;
 | 
			
		||||
                } else {
 | 
			
		||||
                    non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
 | 
			
		||||
                    non_code_meta.insert(previous_body.len(), node.clone());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return self.make_body(next_token.index, previous_body, non_code_meta);
 | 
			
		||||
@ -1641,14 +1652,14 @@ impl Parser {
 | 
			
		||||
 | 
			
		||||
        let next = self.next_meaningful_token(token_index, None)?;
 | 
			
		||||
        if let Some(node) = &next.non_code_node {
 | 
			
		||||
            non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
 | 
			
		||||
            non_code_meta.insert(previous_body.len(), node.clone());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if token.token_type == TokenType::Keyword && VariableKind::from_str(&token.value).is_ok() {
 | 
			
		||||
            let declaration = self.make_variable_declaration(token_index)?;
 | 
			
		||||
            let next_thing = self.next_meaningful_token(declaration.last_index, None)?;
 | 
			
		||||
            if let Some(node) = &next_thing.non_code_node {
 | 
			
		||||
                non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
 | 
			
		||||
                non_code_meta.insert(previous_body.len(), node.clone());
 | 
			
		||||
            }
 | 
			
		||||
            let mut _previous_body = previous_body;
 | 
			
		||||
            _previous_body.push(BodyItem::VariableDeclaration(VariableDeclaration {
 | 
			
		||||
@ -1669,7 +1680,7 @@ impl Parser {
 | 
			
		||||
            let statement = self.make_return_statement(token_index)?;
 | 
			
		||||
            let next_thing = self.next_meaningful_token(statement.last_index, None)?;
 | 
			
		||||
            if let Some(node) = &next_thing.non_code_node {
 | 
			
		||||
                non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
 | 
			
		||||
                non_code_meta.insert(previous_body.len(), node.clone());
 | 
			
		||||
            }
 | 
			
		||||
            let mut _previous_body = previous_body;
 | 
			
		||||
            _previous_body.push(BodyItem::ReturnStatement(ReturnStatement {
 | 
			
		||||
@ -1693,7 +1704,7 @@ impl Parser {
 | 
			
		||||
                let expression = self.make_expression_statement(token_index)?;
 | 
			
		||||
                let next_thing = self.next_meaningful_token(expression.last_index, None)?;
 | 
			
		||||
                if let Some(node) = &next_thing.non_code_node {
 | 
			
		||||
                    non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
 | 
			
		||||
                    non_code_meta.insert(previous_body.len(), node.clone());
 | 
			
		||||
                }
 | 
			
		||||
                let mut _previous_body = previous_body;
 | 
			
		||||
                _previous_body.push(BodyItem::ExpressionStatement(ExpressionStatement {
 | 
			
		||||
@ -1716,7 +1727,7 @@ impl Parser {
 | 
			
		||||
                && next_thing_token.token_type == TokenType::Operator
 | 
			
		||||
            {
 | 
			
		||||
                if let Some(node) = &next_thing.non_code_node {
 | 
			
		||||
                    non_code_meta.non_code_nodes.insert(previous_body.len(), node.clone());
 | 
			
		||||
                    non_code_meta.insert(previous_body.len(), node.clone());
 | 
			
		||||
                }
 | 
			
		||||
                let expression = self.make_expression_statement(token_index)?;
 | 
			
		||||
                let mut _previous_body = previous_body;
 | 
			
		||||
@ -1913,6 +1924,7 @@ const key = 'c'"#,
 | 
			
		||||
                end: 60,
 | 
			
		||||
                value: NonCodeValue::BlockComment {
 | 
			
		||||
                    value: "this is a comment".to_string(),
 | 
			
		||||
                    style: CommentStyle::Line,
 | 
			
		||||
                },
 | 
			
		||||
            }),
 | 
			
		||||
            31,
 | 
			
		||||
@ -1966,6 +1978,35 @@ const key = 'c'"#,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_math_parse() {
 | 
			
		||||
        let tokens = crate::token::lexer(r#"5 + "a""#);
 | 
			
		||||
        let actual = Parser::new(tokens).ast().unwrap().body;
 | 
			
		||||
        let expr = BinaryExpression {
 | 
			
		||||
            start: 0,
 | 
			
		||||
            end: 7,
 | 
			
		||||
            operator: BinaryOperator::Add,
 | 
			
		||||
            left: BinaryPart::Literal(Box::new(Literal {
 | 
			
		||||
                start: 0,
 | 
			
		||||
                end: 1,
 | 
			
		||||
                value: serde_json::Value::Number(serde_json::Number::from(5)),
 | 
			
		||||
                raw: "5".to_owned(),
 | 
			
		||||
            })),
 | 
			
		||||
            right: BinaryPart::Literal(Box::new(Literal {
 | 
			
		||||
                start: 4,
 | 
			
		||||
                end: 7,
 | 
			
		||||
                value: serde_json::Value::String("a".to_owned()),
 | 
			
		||||
                raw: r#""a""#.to_owned(),
 | 
			
		||||
            })),
 | 
			
		||||
        };
 | 
			
		||||
        let expected = vec![BodyItem::ExpressionStatement(ExpressionStatement {
 | 
			
		||||
            start: 0,
 | 
			
		||||
            end: 7,
 | 
			
		||||
            expression: Value::BinaryExpression(Box::new(expr)),
 | 
			
		||||
        })];
 | 
			
		||||
        assert_eq!(expected, actual);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_is_code_token() {
 | 
			
		||||
        let tokens = [
 | 
			
		||||
@ -2812,10 +2853,6 @@ z(-[["#,
 | 
			
		||||
        let parser = Parser::new(tokens);
 | 
			
		||||
        let result = parser.ast();
 | 
			
		||||
        assert!(result.is_err());
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            result.err().unwrap().to_string(),
 | 
			
		||||
            r#"syntax: KclErrorDetails { source_ranges: [SourceRange([1, 2])], message: "missing a closing brace for the function call" }"#
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
@ -2831,7 +2868,7 @@ z(-[["#,
 | 
			
		||||
        // https://github.com/KittyCAD/modeling-app/issues/696
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            result.err().unwrap().to_string(),
 | 
			
		||||
            r#"semantic: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
 | 
			
		||||
            r#"syntax: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -2845,7 +2882,7 @@ z(-[["#,
 | 
			
		||||
        // https://github.com/KittyCAD/modeling-app/issues/696
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            result.err().unwrap().to_string(),
 | 
			
		||||
            r#"semantic: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
 | 
			
		||||
            r#"syntax: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -2863,7 +2900,7 @@ e
 | 
			
		||||
            .err()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .to_string()
 | 
			
		||||
            .contains("expected to be started on a identifier or literal"));
 | 
			
		||||
            .contains("expected whitespace, found ')' which is brace"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
@ -2872,7 +2909,11 @@ e
 | 
			
		||||
        let parser = Parser::new(tokens);
 | 
			
		||||
        let result = parser.ast();
 | 
			
		||||
        assert!(result.is_err());
 | 
			
		||||
        assert!(result.err().unwrap().to_string().contains("expected another token"));
 | 
			
		||||
        assert!(result
 | 
			
		||||
            .err()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .to_string()
 | 
			
		||||
            .contains("expected whitespace, found ')' which is brace"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
@ -2884,11 +2925,7 @@ e
 | 
			
		||||
        let parser = Parser::new(tokens);
 | 
			
		||||
        let result = parser.ast();
 | 
			
		||||
        assert!(result.is_err());
 | 
			
		||||
        assert!(result
 | 
			
		||||
            .err()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .to_string()
 | 
			
		||||
            .contains("unexpected end of expression"));
 | 
			
		||||
        assert!(result.err().unwrap().to_string().contains("Unexpected token"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
@ -3022,7 +3059,9 @@ e
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_error_stdlib_in_fn_name() {
 | 
			
		||||
        let some_program_string = r#"fn cos = () {}"#;
 | 
			
		||||
        let some_program_string = r#"fn cos = () => {
 | 
			
		||||
            return 1
 | 
			
		||||
        }"#;
 | 
			
		||||
        let tokens = crate::token::lexer(some_program_string);
 | 
			
		||||
        let parser = Parser::new(tokens);
 | 
			
		||||
        let result = parser.ast();
 | 
			
		||||
@ -3123,9 +3162,12 @@ thing(false)
 | 
			
		||||
        let parser = Parser::new(tokens);
 | 
			
		||||
        let result = parser.ast();
 | 
			
		||||
        assert!(result.is_err());
 | 
			
		||||
        // TODO: https://github.com/KittyCAD/modeling-app/issues/784
 | 
			
		||||
        // Improve this error message.
 | 
			
		||||
        // It should say that the compiler is expecting a function expression on the RHS.
 | 
			
		||||
        assert_eq!(
 | 
			
		||||
            result.err().unwrap().to_string(),
 | 
			
		||||
            r#"syntax: KclErrorDetails { source_ranges: [SourceRange([0, 2])], message: "Expected a `let` variable kind, found: `fn`" }"#
 | 
			
		||||
            r#"syntax: KclErrorDetails { source_ranges: [SourceRange([11, 18])], message: "Unexpected token" }"#
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -3163,15 +3205,6 @@ let other_thing = 2 * cos(3)"#;
 | 
			
		||||
        parser.ast().unwrap();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_parse_pipes_on_pipes() {
 | 
			
		||||
        let code = include_str!("../../tests/executor/inputs/pipes_on_pipes.kcl");
 | 
			
		||||
 | 
			
		||||
        let tokens = crate::token::lexer(code);
 | 
			
		||||
        let parser = Parser::new(tokens);
 | 
			
		||||
        parser.ast().unwrap();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_negative_arguments() {
 | 
			
		||||
        let some_program_string = r#"fn box = (p, h, l, w) => {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1967
									
								
								src/wasm-lib/kcl/src/parser/parser_impl.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1967
									
								
								src/wasm-lib/kcl/src/parser/parser_impl.rs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										107
									
								
								src/wasm-lib/kcl/src/parser/parser_impl/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/wasm-lib/kcl/src/parser/parser_impl/error.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,107 @@
 | 
			
		||||
use winnow::error::{ErrorKind, ParseError, StrContext};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    errors::{KclError, KclErrorDetails},
 | 
			
		||||
    token::Token,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Accumulate context while backtracking errors
 | 
			
		||||
/// Very similar to [`winnow::error::ContextError`] type,
 | 
			
		||||
/// but the 'cause' field is always a [`KclError`],
 | 
			
		||||
/// instead of a dynamic [`std::error::Error`] trait object.
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub struct ContextError<C = StrContext> {
 | 
			
		||||
    pub context: Vec<C>,
 | 
			
		||||
    pub cause: Option<KclError>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<ParseError<&[Token], ContextError>> for KclError {
 | 
			
		||||
    fn from(err: ParseError<&[Token], ContextError>) -> Self {
 | 
			
		||||
        let Some(last_token) = err.input().last() else {
 | 
			
		||||
            return KclError::Syntax(KclErrorDetails {
 | 
			
		||||
                source_ranges: Default::default(),
 | 
			
		||||
                message: "file is empty".to_owned(),
 | 
			
		||||
            });
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let (input, offset, err) = (err.input().to_vec(), err.offset(), err.into_inner());
 | 
			
		||||
 | 
			
		||||
        if let Some(e) = err.cause {
 | 
			
		||||
            return e;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // See docs on `offset`.
 | 
			
		||||
        if offset >= input.len() {
 | 
			
		||||
            let context = err.context.first();
 | 
			
		||||
            return KclError::Syntax(KclErrorDetails {
 | 
			
		||||
                source_ranges: last_token.as_source_ranges(),
 | 
			
		||||
                message: match context {
 | 
			
		||||
                    Some(what) => format!("Unexpected end of file. The compiler {what}"),
 | 
			
		||||
                    None => "Unexpected end of file while still parsing".to_owned(),
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let bad_token = &input[offset];
 | 
			
		||||
        // TODO: Add the Winnow parser context to the error.
 | 
			
		||||
        // See https://github.com/KittyCAD/modeling-app/issues/784
 | 
			
		||||
        KclError::Syntax(KclErrorDetails {
 | 
			
		||||
            source_ranges: bad_token.as_source_ranges(),
 | 
			
		||||
            message: "Unexpected token".to_owned(),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<C> From<KclError> for ContextError<C> {
 | 
			
		||||
    fn from(e: KclError) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            context: Default::default(),
 | 
			
		||||
            cause: Some(e),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<C> std::default::Default for ContextError<C> {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            context: Default::default(),
 | 
			
		||||
            cause: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<I, C> winnow::error::ParserError<I> for ContextError<C> {
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self {
 | 
			
		||||
        Self::default()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn append(self, _input: &I, _kind: ErrorKind) -> Self {
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn or(self, other: Self) -> Self {
 | 
			
		||||
        other
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<C, I> winnow::error::AddContext<I, C> for ContextError<C> {
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn add_context(mut self, _input: &I, ctx: C) -> Self {
 | 
			
		||||
        self.context.push(ctx);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<C, I> winnow::error::FromExternalError<I, KclError> for ContextError<C> {
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn from_external_error(_input: &I, _kind: ErrorKind, e: KclError) -> Self {
 | 
			
		||||
        let mut err = Self::default();
 | 
			
		||||
        {
 | 
			
		||||
            err.cause = Some(e);
 | 
			
		||||
        }
 | 
			
		||||
        err
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -6,6 +6,8 @@ use schemars::JsonSchema;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use tower_lsp::lsp_types::SemanticTokenType;
 | 
			
		||||
 | 
			
		||||
use crate::{ast::types::VariableKind, executor::SourceRange};
 | 
			
		||||
 | 
			
		||||
mod tokeniser;
 | 
			
		||||
 | 
			
		||||
/// The types of tokens.
 | 
			
		||||
@ -142,15 +144,39 @@ impl Token {
 | 
			
		||||
            TokenType::Whitespace | TokenType::LineComment | TokenType::BlockComment
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn as_source_range(&self) -> SourceRange {
 | 
			
		||||
        SourceRange([self.start, self.end])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
impl From<Token> for crate::executor::SourceRange {
 | 
			
		||||
    pub fn as_source_ranges(&self) -> Vec<SourceRange> {
 | 
			
		||||
        vec![self.as_source_range()]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Is this token the beginning of a variable/function declaration?
 | 
			
		||||
    /// If so, what kind?
 | 
			
		||||
    /// If not, returns None.
 | 
			
		||||
    pub fn declaration_keyword(&self) -> Option<VariableKind> {
 | 
			
		||||
        if !matches!(self.token_type, TokenType::Keyword) {
 | 
			
		||||
            return None;
 | 
			
		||||
        }
 | 
			
		||||
        Some(match self.value.as_str() {
 | 
			
		||||
            "var" => VariableKind::Var,
 | 
			
		||||
            "let" => VariableKind::Let,
 | 
			
		||||
            "fn" => VariableKind::Fn,
 | 
			
		||||
            "const" => VariableKind::Const,
 | 
			
		||||
            _ => return None,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<Token> for SourceRange {
 | 
			
		||||
    fn from(token: Token) -> Self {
 | 
			
		||||
        Self([token.start, token.end])
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&Token> for crate::executor::SourceRange {
 | 
			
		||||
impl From<&Token> for SourceRange {
 | 
			
		||||
    fn from(token: &Token) -> Self {
 | 
			
		||||
        Self([token.start, token.end])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								src/wasm-lib/tests/executor/inputs/cube.kcl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/wasm-lib/tests/executor/inputs/cube.kcl
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
fn cube = (pos, scale) => {
 | 
			
		||||
  const sg = startSketchAt(pos)
 | 
			
		||||
    |> line([0, scale], %)
 | 
			
		||||
    |> line([scale, 0], %)
 | 
			
		||||
    |> line([0, -scale], %)
 | 
			
		||||
 | 
			
		||||
  return sg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const b1 = cube([0,0], 10)
 | 
			
		||||
const pt1 = b1[0]
 | 
			
		||||
show(b1)
 | 
			
		||||
		Reference in New Issue
	
	Block a user