kcl_lib/
unparser.rs

1use std::fmt::Write;
2
3use crate::{
4    KclError, ModuleId,
5    parsing::{
6        DeprecationKind, PIPE_OPERATOR,
7        ast::types::{
8            Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, Associativity, BinaryExpression,
9            BinaryOperator, BinaryPart, Block, BodyItem, CallExpressionKw, CommentStyle, DefaultParamVal, Expr,
10            FormatOptions, FunctionExpression, Identifier, IfExpression, ImportSelector, ImportStatement,
11            ItemVisibility, LabeledArg, Literal, LiteralValue, MemberExpression, Name, Node, NodeList, NonCodeMeta,
12            NonCodeNode, NonCodeValue, NumericLiteral, ObjectExpression, Parameter, PipeExpression, Program,
13            SketchBlock, SketchVar, TagDeclarator, TypeDeclaration, UnaryExpression, VariableDeclaration, VariableKind,
14        },
15        deprecation,
16    },
17};
18
19#[allow(dead_code)]
20pub fn fmt(input: &str) -> Result<String, KclError> {
21    let program = crate::parsing::parse_str(input, ModuleId::default()).parse_errs_as_err()?;
22    Ok(program.recast_top(&Default::default(), 0))
23}
24
25impl Program {
26    pub fn recast_top(&self, options: &FormatOptions, indentation_level: usize) -> String {
27        let mut buf = String::with_capacity(1024);
28        self.recast(&mut buf, options, indentation_level);
29        buf
30    }
31
32    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
33        if let Some(sh) = self.shebang.as_ref() {
34            write!(buf, "{}\n\n", sh.inner.content).no_fail();
35        }
36
37        recast_body(
38            &self.body,
39            &self.non_code_meta,
40            &self.inner_attrs,
41            buf,
42            options,
43            indentation_level,
44        );
45    }
46}
47
48fn recast_body(
49    items: &[BodyItem],
50    non_code_meta: &NonCodeMeta,
51    inner_attrs: &NodeList<Annotation>,
52    buf: &mut String,
53    options: &FormatOptions,
54    indentation_level: usize,
55) {
56    let indentation = options.get_indentation(indentation_level);
57
58    let has_non_newline_start_node = non_code_meta
59        .start_nodes
60        .iter()
61        .any(|noncode| !matches!(noncode.value, NonCodeValue::NewLine));
62    if has_non_newline_start_node {
63        let mut pending_newline = false;
64        for start_node in &non_code_meta.start_nodes {
65            match start_node.value {
66                NonCodeValue::NewLine => pending_newline = true,
67                _ => {
68                    if pending_newline {
69                        // If the previous emission already ended with '\n', only add one more.
70                        if buf.ends_with('\n') {
71                            buf.push('\n');
72                        } else {
73                            buf.push_str("\n\n");
74                        }
75                        pending_newline = false;
76                    }
77                    let noncode_recast = start_node.recast(options, indentation_level);
78                    buf.push_str(&noncode_recast);
79                }
80            }
81        }
82        // Handle any trailing newlines that weren't flushed yet.
83        if pending_newline {
84            if buf.ends_with('\n') {
85                buf.push('\n');
86            } else {
87                buf.push_str("\n\n");
88            }
89        }
90    }
91
92    for attr in inner_attrs {
93        options.write_indentation(buf, indentation_level);
94        attr.recast(buf, options, indentation_level);
95    }
96    if !inner_attrs.is_empty() {
97        buf.push('\n');
98    }
99
100    let body_item_lines = items.iter().map(|body_item| {
101        let mut result = String::with_capacity(256);
102        for comment in body_item.get_comments() {
103            if !comment.is_empty() {
104                result.push_str(&indentation);
105                result.push_str(comment);
106            }
107            if comment.is_empty() && !result.ends_with("\n") {
108                result.push('\n');
109            }
110            if !result.ends_with("\n\n") && result != "\n" {
111                result.push('\n');
112            }
113        }
114        for attr in body_item.get_attrs() {
115            attr.recast(&mut result, options, indentation_level);
116        }
117        match body_item {
118            BodyItem::ImportStatement(stmt) => {
119                result.push_str(&stmt.recast(options, indentation_level));
120            }
121            BodyItem::ExpressionStatement(expression_statement) => {
122                expression_statement
123                    .expression
124                    .recast(&mut result, options, indentation_level, ExprContext::Other)
125            }
126            BodyItem::VariableDeclaration(variable_declaration) => {
127                variable_declaration.recast(&mut result, options, indentation_level);
128            }
129            BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.recast(&mut result),
130            BodyItem::ReturnStatement(return_statement) => {
131                write!(&mut result, "{indentation}return ").no_fail();
132                let mut tmp_buf = String::with_capacity(256);
133                return_statement
134                    .argument
135                    .recast(&mut tmp_buf, options, indentation_level, ExprContext::Other);
136                write!(&mut result, "{}", tmp_buf.trim_start()).no_fail();
137            }
138        };
139        result
140    });
141    for (index, recast_str) in body_item_lines.enumerate() {
142        write!(buf, "{recast_str}").no_fail();
143
144        // determine the value of the end string
145        // basically if we are inside a nested function we want to end with a new line
146        let needs_line_break = !(index == items.len() - 1 && indentation_level == 0);
147
148        let custom_white_space_or_comment = non_code_meta.non_code_nodes.get(&index).map(|noncodes| {
149            noncodes.iter().enumerate().map(|(i, custom_white_space_or_comment)| {
150                let formatted = custom_white_space_or_comment.recast(options, indentation_level);
151                if i == 0 && !formatted.trim().is_empty() {
152                    if let NonCodeValue::BlockComment { .. } = custom_white_space_or_comment.value {
153                        format!("\n{formatted}")
154                    } else {
155                        formatted
156                    }
157                } else {
158                    formatted
159                }
160            })
161        });
162
163        if let Some(custom) = custom_white_space_or_comment {
164            for to_write in custom {
165                write!(buf, "{to_write}").no_fail();
166            }
167        } else if needs_line_break {
168            buf.push('\n')
169        }
170    }
171    trim_end(buf);
172
173    // Insert a final new line if the user wants it.
174    if options.insert_final_newline && !buf.is_empty() {
175        buf.push('\n');
176    }
177}
178
179impl NonCodeValue {
180    fn should_cause_array_newline(&self) -> bool {
181        match self {
182            Self::InlineComment { .. } => false,
183            Self::BlockComment { .. } | Self::NewLineBlockComment { .. } | Self::NewLine => true,
184        }
185    }
186}
187
188impl Node<NonCodeNode> {
189    fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
190        let indentation = options.get_indentation(indentation_level);
191        match &self.value {
192            NonCodeValue::InlineComment {
193                value,
194                style: CommentStyle::Line,
195            } => format!(" // {value}\n"),
196            NonCodeValue::InlineComment {
197                value,
198                style: CommentStyle::Block,
199            } => format!(" /* {value} */"),
200            NonCodeValue::BlockComment { value, style } => match style {
201                CommentStyle::Block => format!("{indentation}/* {value} */"),
202                CommentStyle::Line => {
203                    if value.trim().is_empty() {
204                        format!("{indentation}//\n")
205                    } else {
206                        format!("{}// {}\n", indentation, value.trim())
207                    }
208                }
209            },
210            NonCodeValue::NewLineBlockComment { value, style } => {
211                let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
212                match style {
213                    CommentStyle::Block => format!("{add_start_new_line}{indentation}/* {value} */\n"),
214                    CommentStyle::Line => {
215                        if value.trim().is_empty() {
216                            format!("{add_start_new_line}{indentation}//\n")
217                        } else {
218                            format!("{}{}// {}\n", add_start_new_line, indentation, value.trim())
219                        }
220                    }
221                }
222            }
223            NonCodeValue::NewLine => "\n\n".to_string(),
224        }
225    }
226}
227
228impl Node<Annotation> {
229    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
230        let indentation = options.get_indentation(indentation_level);
231        let mut result = String::new();
232        for comment in &self.pre_comments {
233            if !comment.is_empty() {
234                result.push_str(&indentation);
235                result.push_str(comment);
236            }
237            if !result.ends_with("\n\n") && result != "\n" {
238                result.push('\n');
239            }
240        }
241        result.push('@');
242        if let Some(name) = &self.name {
243            result.push_str(&name.name);
244        }
245        if let Some(properties) = &self.properties {
246            result.push('(');
247            result.push_str(
248                &properties
249                    .iter()
250                    .map(|prop| {
251                        let mut temp = format!("{} = ", prop.key.name);
252                        prop.value
253                            .recast(&mut temp, options, indentation_level + 1, ExprContext::Other);
254                        temp.trim().to_owned()
255                    })
256                    .collect::<Vec<String>>()
257                    .join(", "),
258            );
259            result.push(')');
260            result.push('\n');
261        }
262
263        buf.push_str(&result)
264    }
265}
266
267impl ImportStatement {
268    pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
269        let indentation = options.get_indentation(indentation_level);
270        let vis = if self.visibility == ItemVisibility::Export {
271            "export "
272        } else {
273            ""
274        };
275        let mut string = format!("{vis}{indentation}import ");
276        match &self.selector {
277            ImportSelector::List { items } => {
278                for (i, item) in items.iter().enumerate() {
279                    if i > 0 {
280                        string.push_str(", ");
281                    }
282                    string.push_str(&item.name.name);
283                    if let Some(alias) = &item.alias {
284                        // If the alias is the same, don't output it.
285                        if item.name.name != alias.name {
286                            string.push_str(&format!(" as {}", alias.name));
287                        }
288                    }
289                }
290                string.push_str(" from ");
291            }
292            ImportSelector::Glob(_) => string.push_str("* from "),
293            ImportSelector::None { .. } => {}
294        }
295        string.push_str(&format!("\"{}\"", self.path));
296
297        if let ImportSelector::None { alias: Some(alias) } = &self.selector {
298            string.push_str(" as ");
299            string.push_str(&alias.name);
300        }
301        string
302    }
303}
304
305#[derive(Copy, Clone, Debug, Eq, PartialEq)]
306pub(crate) enum ExprContext {
307    Pipe,
308    Decl,
309    Other,
310}
311
312impl Expr {
313    pub(crate) fn recast(
314        &self,
315        buf: &mut String,
316        options: &FormatOptions,
317        indentation_level: usize,
318        mut ctxt: ExprContext,
319    ) {
320        let is_decl = matches!(ctxt, ExprContext::Decl);
321        if is_decl {
322            // Just because this expression is being bound to a variable, doesn't mean that every child
323            // expression is being bound. So, reset the expression context if necessary.
324            // This will still preserve the "::Pipe" context though.
325            ctxt = ExprContext::Other;
326        }
327        match &self {
328            Expr::BinaryExpression(bin_exp) => bin_exp.recast(buf, options, indentation_level, ctxt),
329            Expr::ArrayExpression(array_exp) => array_exp.recast(buf, options, indentation_level, ctxt),
330            Expr::ArrayRangeExpression(range_exp) => range_exp.recast(buf, options, indentation_level, ctxt),
331            Expr::ObjectExpression(obj_exp) => obj_exp.recast(buf, options, indentation_level, ctxt),
332            Expr::MemberExpression(mem_exp) => mem_exp.recast(buf, options, indentation_level, ctxt),
333            Expr::Literal(literal) => {
334                literal.recast(buf);
335            }
336            Expr::FunctionExpression(func_exp) => {
337                if !is_decl {
338                    buf.push_str("fn");
339                }
340                func_exp.recast(buf, options, indentation_level);
341            }
342            Expr::CallExpressionKw(call_exp) => call_exp.recast(buf, options, indentation_level, ctxt),
343            Expr::Name(name) => {
344                let result = &name.inner.name.inner.name;
345                match deprecation(result, DeprecationKind::Const) {
346                    Some(suggestion) => buf.push_str(suggestion),
347                    None => {
348                        for prefix in &name.path {
349                            buf.push_str(&prefix.name);
350                            buf.push(':');
351                            buf.push(':');
352                        }
353                        buf.push_str(result);
354                    }
355                }
356            }
357            Expr::TagDeclarator(tag) => tag.recast(buf),
358            Expr::PipeExpression(pipe_exp) => pipe_exp.recast(buf, options, indentation_level, !is_decl),
359            Expr::UnaryExpression(unary_exp) => unary_exp.recast(buf, options, indentation_level, ctxt),
360            Expr::IfExpression(e) => e.recast(buf, options, indentation_level, ctxt),
361            Expr::PipeSubstitution(_) => buf.push_str(crate::parsing::PIPE_SUBSTITUTION_OPERATOR),
362            Expr::LabelledExpression(e) => {
363                e.expr.recast(buf, options, indentation_level, ctxt);
364                buf.push_str(" as ");
365                buf.push_str(&e.label.name);
366            }
367            Expr::AscribedExpression(e) => e.recast(buf, options, indentation_level, ctxt),
368            Expr::SketchBlock(e) => e.recast(buf, options, indentation_level, ctxt),
369            Expr::SketchVar(e) => e.recast(buf),
370            Expr::None(_) => {
371                unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
372            }
373        }
374    }
375}
376
377impl AscribedExpression {
378    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
379        if matches!(
380            self.expr,
381            Expr::BinaryExpression(..) | Expr::PipeExpression(..) | Expr::UnaryExpression(..)
382        ) {
383            buf.push('(');
384            self.expr.recast(buf, options, indentation_level, ctxt);
385            buf.push(')');
386        } else {
387            self.expr.recast(buf, options, indentation_level, ctxt);
388        }
389        buf.push_str(": ");
390        write!(buf, "{}", self.ty).no_fail();
391    }
392}
393
394impl BinaryPart {
395    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
396        match &self {
397            BinaryPart::Literal(literal) => {
398                literal.recast(buf);
399            }
400            BinaryPart::Name(name) => match deprecation(&name.inner.name.inner.name, DeprecationKind::Const) {
401                Some(suggestion) => write!(buf, "{suggestion}").no_fail(),
402                None => name.write_to(buf).no_fail(),
403            },
404            BinaryPart::BinaryExpression(binary_expression) => {
405                binary_expression.recast(buf, options, indentation_level, ctxt)
406            }
407            BinaryPart::CallExpressionKw(call_expression) => {
408                call_expression.recast(buf, options, indentation_level, ExprContext::Other)
409            }
410            BinaryPart::UnaryExpression(unary_expression) => {
411                unary_expression.recast(buf, options, indentation_level, ctxt)
412            }
413            BinaryPart::MemberExpression(member_expression) => {
414                member_expression.recast(buf, options, indentation_level, ctxt)
415            }
416            BinaryPart::ArrayExpression(e) => e.recast(buf, options, indentation_level, ctxt),
417            BinaryPart::ArrayRangeExpression(e) => e.recast(buf, options, indentation_level, ctxt),
418            BinaryPart::ObjectExpression(e) => e.recast(buf, options, indentation_level, ctxt),
419            BinaryPart::IfExpression(e) => e.recast(buf, options, indentation_level, ExprContext::Other),
420            BinaryPart::AscribedExpression(e) => e.recast(buf, options, indentation_level, ExprContext::Other),
421            BinaryPart::SketchVar(e) => e.recast(buf),
422        }
423    }
424}
425
426impl CallExpressionKw {
427    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
428        recast_call(
429            &self.callee,
430            self.unlabeled.as_ref(),
431            &self.arguments,
432            buf,
433            options,
434            indentation_level,
435            ctxt,
436        );
437    }
438}
439
440fn recast_args(
441    unlabeled: Option<&Expr>,
442    arguments: &[LabeledArg],
443    options: &FormatOptions,
444    indentation_level: usize,
445    ctxt: ExprContext,
446) -> Vec<String> {
447    let mut arg_list = if let Some(first_arg) = unlabeled {
448        let mut first = String::with_capacity(256);
449        first_arg.recast(&mut first, options, indentation_level, ctxt);
450        vec![first.trim().to_owned()]
451    } else {
452        Vec::with_capacity(arguments.len())
453    };
454    arg_list.extend(arguments.iter().map(|arg| {
455        let mut buf = String::with_capacity(256);
456        arg.recast(&mut buf, options, indentation_level, ctxt);
457        buf
458    }));
459    arg_list
460}
461
462fn recast_call(
463    callee: &Name,
464    unlabeled: Option<&Expr>,
465    arguments: &[LabeledArg],
466    buf: &mut String,
467    options: &FormatOptions,
468    indentation_level: usize,
469    ctxt: ExprContext,
470) {
471    let smart_indent_level = if ctxt == ExprContext::Pipe {
472        0
473    } else {
474        indentation_level
475    };
476    let name = callee;
477
478    if let Some(suggestion) = deprecation(&name.name.inner.name, DeprecationKind::Function) {
479        options.write_indentation(buf, smart_indent_level);
480        return write!(buf, "{suggestion}").no_fail();
481    }
482
483    let arg_list = recast_args(unlabeled, arguments, options, indentation_level, ctxt);
484    let has_lots_of_args = arg_list.len() >= 4;
485    let args = arg_list.join(", ");
486    let some_arg_is_already_multiline = arg_list.len() > 1 && arg_list.iter().any(|arg| arg.contains('\n'));
487    let multiline = has_lots_of_args || some_arg_is_already_multiline;
488    if multiline {
489        let next_indent = indentation_level + 1;
490        let inner_indentation = if ctxt == ExprContext::Pipe {
491            options.get_indentation_offset_pipe(next_indent)
492        } else {
493            options.get_indentation(next_indent)
494        };
495        let arg_list = recast_args(unlabeled, arguments, options, next_indent, ctxt);
496        let mut args = arg_list.join(&format!(",\n{inner_indentation}"));
497        args.push(',');
498        let args = args;
499        let end_indent = if ctxt == ExprContext::Pipe {
500            options.get_indentation_offset_pipe(indentation_level)
501        } else {
502            options.get_indentation(indentation_level)
503        };
504        options.write_indentation(buf, smart_indent_level);
505        name.write_to(buf).no_fail();
506        buf.push('(');
507        buf.push('\n');
508        write!(buf, "{inner_indentation}").no_fail();
509        write!(buf, "{args}").no_fail();
510        buf.push('\n');
511        write!(buf, "{end_indent}").no_fail();
512        buf.push(')');
513    } else {
514        options.write_indentation(buf, smart_indent_level);
515        name.write_to(buf).no_fail();
516        buf.push('(');
517        write!(buf, "{args}").no_fail();
518        buf.push(')');
519    }
520}
521
522impl LabeledArg {
523    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
524        if let Some(l) = &self.label {
525            buf.push_str(&l.name);
526            buf.push_str(" = ");
527        }
528        self.arg.recast(buf, options, indentation_level, ctxt);
529    }
530}
531
532impl VariableDeclaration {
533    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
534        options.write_indentation(buf, indentation_level);
535        match self.visibility {
536            ItemVisibility::Default => {}
537            ItemVisibility::Export => buf.push_str("export "),
538        };
539
540        let (keyword, eq) = match self.kind {
541            VariableKind::Fn => ("fn ", ""),
542            VariableKind::Const => ("", " = "),
543        };
544        buf.push_str(keyword);
545        buf.push_str(&self.declaration.id.name);
546        buf.push_str(eq);
547
548        // Unfortunately, allocate a temporary buffer here so that we can trim the start.
549        // Otherwise, some expression kinds will write indentation at the start, because
550        // they don't know they're inside a declaration.
551        // TODO: Pass the ExprContext throughout every Expr kind, so that they can conditionally
552        // emit whitespace in an ExprStmt and not when they're in a DeclarationStmt.
553        let mut tmp_buf = String::new();
554        self.declaration
555            .init
556            .recast(&mut tmp_buf, options, indentation_level, ExprContext::Decl);
557        buf.push_str(tmp_buf.trim_start());
558    }
559}
560
561impl TypeDeclaration {
562    pub fn recast(&self, buf: &mut String) {
563        match self.visibility {
564            ItemVisibility::Default => {}
565            ItemVisibility::Export => buf.push_str("export "),
566        };
567        buf.push_str("type ");
568        buf.push_str(&self.name.name);
569
570        if let Some(args) = &self.args {
571            buf.push('(');
572            for (i, a) in args.iter().enumerate() {
573                buf.push_str(&a.name);
574                if i < args.len() - 1 {
575                    buf.push_str(", ");
576                }
577            }
578            buf.push(')');
579        }
580        if let Some(alias) = &self.alias {
581            buf.push_str(" = ");
582            write!(buf, "{alias}").no_fail();
583        }
584    }
585}
586
587fn write<W: std::fmt::Write>(f: &mut W, s: impl std::fmt::Display) {
588    f.write_fmt(format_args!("{s}"))
589        .expect("writing to a string should always succeed")
590}
591
592fn write_dbg<W: std::fmt::Write>(f: &mut W, s: impl std::fmt::Debug) {
593    f.write_fmt(format_args!("{s:?}"))
594        .expect("writing to a string should always succeed")
595}
596
597impl NumericLiteral {
598    fn recast(&self, buf: &mut String) {
599        if self.raw.contains('.') && self.value.fract() == 0.0 {
600            write_dbg(buf, self.value);
601            write(buf, self.suffix);
602        } else {
603            write(buf, &self.raw);
604        }
605    }
606}
607
608impl Literal {
609    fn recast(&self, buf: &mut String) {
610        match self.value {
611            LiteralValue::Number { value, suffix } => {
612                if self.raw.contains('.') && value.fract() == 0.0 {
613                    write_dbg(buf, value);
614                    write(buf, suffix);
615                } else {
616                    write(buf, &self.raw);
617                }
618            }
619            LiteralValue::String(ref s) => {
620                if let Some(suggestion) = deprecation(s, DeprecationKind::String) {
621                    return write!(buf, "{suggestion}").unwrap();
622                }
623                let quote = if self.raw.trim().starts_with('"') { '"' } else { '\'' };
624                write(buf, quote);
625                write(buf, s);
626                write(buf, quote);
627            }
628            LiteralValue::Bool(_) => {
629                write(buf, &self.raw);
630            }
631        }
632    }
633}
634
635impl TagDeclarator {
636    pub fn recast(&self, buf: &mut String) {
637        // TagDeclarators are always prefixed with a dollar sign.
638        buf.push('$');
639        buf.push_str(&self.name);
640    }
641}
642
643impl ArrayExpression {
644    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
645        // Reconstruct the order of items in the array.
646        // An item can be an element (i.e. an expression for a KCL value),
647        // or a non-code item (e.g. a comment)
648        let num_items = self.elements.len() + self.non_code_meta.non_code_nodes_len();
649        let mut elems = self.elements.iter();
650        let mut found_line_comment = false;
651        let mut format_items: Vec<_> = Vec::with_capacity(num_items);
652        for i in 0..num_items {
653            if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
654                format_items.extend(noncode.iter().map(|nc| {
655                    found_line_comment |= nc.value.should_cause_array_newline();
656                    nc.recast(options, 0)
657                }));
658            } else {
659                let el = elems.next().unwrap();
660                let mut s = String::with_capacity(256);
661                el.recast(&mut s, options, 0, ExprContext::Other);
662                s.push_str(", ");
663                format_items.push(s);
664            }
665        }
666
667        // Format these items into a one-line array.
668        if let Some(item) = format_items.last_mut()
669            && let Some(norm) = item.strip_suffix(", ")
670        {
671            *item = norm.to_owned();
672        }
673        let mut flat_recast = String::with_capacity(256);
674        flat_recast.push('[');
675        for fi in &format_items {
676            flat_recast.push_str(fi)
677        }
678        flat_recast.push(']');
679
680        // We might keep the one-line representation, if it's short enough.
681        let max_array_length = 40;
682        let multi_line = flat_recast.len() > max_array_length || found_line_comment;
683        if !multi_line {
684            buf.push_str(&flat_recast);
685            return;
686        }
687
688        // Otherwise, we format a multi-line representation.
689        buf.push_str("[\n");
690        let inner_indentation = if ctxt == ExprContext::Pipe {
691            options.get_indentation_offset_pipe(indentation_level + 1)
692        } else {
693            options.get_indentation(indentation_level + 1)
694        };
695        for format_item in format_items {
696            buf.push_str(&inner_indentation);
697            buf.push_str(if let Some(x) = format_item.strip_suffix(" ") {
698                x
699            } else {
700                &format_item
701            });
702            if !format_item.ends_with('\n') {
703                buf.push('\n')
704            }
705        }
706        let end_indent = if ctxt == ExprContext::Pipe {
707            options.get_indentation_offset_pipe(indentation_level)
708        } else {
709            options.get_indentation(indentation_level)
710        };
711        buf.push_str(&end_indent);
712        buf.push(']');
713    }
714}
715
716/// An expression is syntactically trivial: i.e., a literal, identifier, or similar.
717fn expr_is_trivial(expr: &Expr) -> bool {
718    matches!(
719        expr,
720        Expr::Literal(_) | Expr::Name(_) | Expr::TagDeclarator(_) | Expr::PipeSubstitution(_) | Expr::None(_)
721    )
722}
723
724trait CannotActuallyFail {
725    fn no_fail(self);
726}
727
728impl CannotActuallyFail for std::fmt::Result {
729    fn no_fail(self) {
730        self.expect("writing to a string cannot fail, there's no IO happening")
731    }
732}
733
734impl ArrayRangeExpression {
735    fn recast(&self, buf: &mut String, options: &FormatOptions, _: usize, _: ExprContext) {
736        buf.push('[');
737        self.start_element.recast(buf, options, 0, ExprContext::Other);
738
739        let range_op = if self.end_inclusive { ".." } else { "..<" };
740        // Format these items into a one-line array. Put spaces around the `..` if either expression
741        // is non-trivial. This is a bit arbitrary but people seem to like simple ranges to be formatted
742        // tightly, but this is a misleading visual representation of the precedence if the range
743        // components are compound expressions.
744        let no_spaces = expr_is_trivial(&self.start_element) && expr_is_trivial(&self.end_element);
745        if no_spaces {
746            write!(buf, "{range_op}").no_fail()
747        } else {
748            write!(buf, " {range_op} ").no_fail()
749        }
750        self.end_element.recast(buf, options, 0, ExprContext::Other);
751        buf.push(']');
752        // Assume a range expression fits on one line.
753    }
754}
755
756fn trim_end(buf: &mut String) {
757    buf.truncate(buf.trim_end().len())
758}
759
760impl ObjectExpression {
761    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
762        if self
763            .non_code_meta
764            .non_code_nodes
765            .values()
766            .any(|nc| nc.iter().any(|nc| nc.value.should_cause_array_newline()))
767        {
768            return self.recast_multi_line(buf, options, indentation_level, ctxt);
769        }
770        let mut flat_recast_buf = String::new();
771        flat_recast_buf.push_str("{ ");
772        for (i, prop) in self.properties.iter().enumerate() {
773            let obj_key = &prop.key.name;
774            write!(flat_recast_buf, "{obj_key} = ").no_fail();
775            prop.value
776                .recast(&mut flat_recast_buf, options, indentation_level, ctxt);
777            if i < self.properties.len() - 1 {
778                flat_recast_buf.push_str(", ");
779            }
780        }
781        flat_recast_buf.push_str(" }");
782        let max_array_length = 40;
783        let needs_multiple_lines = flat_recast_buf.len() > max_array_length;
784        if !needs_multiple_lines {
785            buf.push_str(&flat_recast_buf);
786        } else {
787            self.recast_multi_line(buf, options, indentation_level, ctxt);
788        }
789    }
790
791    /// Recast, but always outputs the object with newlines between each property.
792    fn recast_multi_line(
793        &self,
794        buf: &mut String,
795        options: &FormatOptions,
796        indentation_level: usize,
797        ctxt: ExprContext,
798    ) {
799        let inner_indentation = if ctxt == ExprContext::Pipe {
800            options.get_indentation_offset_pipe(indentation_level + 1)
801        } else {
802            options.get_indentation(indentation_level + 1)
803        };
804        let num_items = self.properties.len() + self.non_code_meta.non_code_nodes_len();
805        let mut props = self.properties.iter();
806        let format_items: Vec<_> = (0..num_items)
807            .flat_map(|i| {
808                if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
809                    noncode.iter().map(|nc| nc.recast(options, 0)).collect::<Vec<_>>()
810                } else {
811                    let prop = props.next().unwrap();
812                    // Use a comma unless it's the last item
813                    let comma = if i == num_items - 1 { "" } else { ",\n" };
814                    let mut s = String::new();
815                    prop.value.recast(&mut s, options, indentation_level + 1, ctxt);
816                    // TODO: Get rid of this vector allocation
817                    vec![format!("{} = {}{comma}", prop.key.name, s.trim())]
818                }
819            })
820            .collect();
821        let end_indent = if ctxt == ExprContext::Pipe {
822            options.get_indentation_offset_pipe(indentation_level)
823        } else {
824            options.get_indentation(indentation_level)
825        };
826        write!(
827            buf,
828            "{{\n{inner_indentation}{}\n{end_indent}}}",
829            format_items.join(&inner_indentation),
830        )
831        .no_fail();
832    }
833}
834
835impl MemberExpression {
836    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
837        // The object
838        self.object.recast(buf, options, indentation_level, ctxt);
839        // The key
840        if self.computed {
841            buf.push('[');
842            self.property.recast(buf, options, indentation_level, ctxt);
843            buf.push(']');
844        } else {
845            buf.push('.');
846            self.property.recast(buf, options, indentation_level, ctxt);
847        };
848    }
849}
850
851impl BinaryExpression {
852    fn recast(&self, buf: &mut String, options: &FormatOptions, _indentation_level: usize, ctxt: ExprContext) {
853        let maybe_wrap_it = |a: String, doit: bool| -> String { if doit { format!("({a})") } else { a } };
854
855        // It would be better to always preserve the user's parentheses but since we've dropped that
856        // info from the AST, we bracket expressions as necessary.
857        let should_wrap_left = match &self.left {
858            BinaryPart::BinaryExpression(bin_exp) => {
859                self.precedence() > bin_exp.precedence()
860                    || ((self.precedence() == bin_exp.precedence())
861                        && (!(self.operator.associative() && self.operator == bin_exp.operator)
862                            && self.operator.associativity() == Associativity::Right))
863            }
864            _ => false,
865        };
866
867        let should_wrap_right = match &self.right {
868            BinaryPart::BinaryExpression(bin_exp) => {
869                self.precedence() > bin_exp.precedence()
870                    // These two lines preserve previous reformatting behaviour.
871                    || self.operator == BinaryOperator::Sub
872                    || self.operator == BinaryOperator::Div
873                    || ((self.precedence() == bin_exp.precedence())
874                        && (!(self.operator.associative() && self.operator == bin_exp.operator)
875                            && self.operator.associativity() == Associativity::Left))
876            }
877            _ => false,
878        };
879
880        let mut left = String::new();
881        self.left.recast(&mut left, options, 0, ctxt);
882        let mut right = String::new();
883        self.right.recast(&mut right, options, 0, ctxt);
884        write!(
885            buf,
886            "{} {} {}",
887            maybe_wrap_it(left, should_wrap_left),
888            self.operator,
889            maybe_wrap_it(right, should_wrap_right)
890        )
891        .no_fail();
892    }
893}
894
895impl UnaryExpression {
896    fn recast(&self, buf: &mut String, options: &FormatOptions, _indentation_level: usize, ctxt: ExprContext) {
897        match self.argument {
898            BinaryPart::Literal(_)
899            | BinaryPart::Name(_)
900            | BinaryPart::MemberExpression(_)
901            | BinaryPart::ArrayExpression(_)
902            | BinaryPart::ArrayRangeExpression(_)
903            | BinaryPart::ObjectExpression(_)
904            | BinaryPart::IfExpression(_)
905            | BinaryPart::AscribedExpression(_)
906            | BinaryPart::CallExpressionKw(_) => {
907                write!(buf, "{}", self.operator).no_fail();
908                self.argument.recast(buf, options, 0, ctxt)
909            }
910            BinaryPart::BinaryExpression(_) | BinaryPart::UnaryExpression(_) | BinaryPart::SketchVar(_) => {
911                write!(buf, "{}", self.operator).no_fail();
912                buf.push('(');
913                self.argument.recast(buf, options, 0, ctxt);
914                buf.push(')');
915            }
916        }
917    }
918}
919
920impl IfExpression {
921    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
922        // We can calculate how many lines this will take, so let's do it and avoid growing the vec.
923        // Total lines = starting lines, else-if lines, ending lines.
924        let n = 2 + (self.else_ifs.len() * 2) + 3;
925        let mut lines = Vec::with_capacity(n);
926
927        let cond = {
928            let mut tmp_buf = String::new();
929            self.cond.recast(&mut tmp_buf, options, indentation_level, ctxt);
930            tmp_buf
931        };
932        lines.push((0, format!("if {cond} {{")));
933        lines.push((1, {
934            let mut tmp_buf = String::new();
935            self.then_val.recast(&mut tmp_buf, options, indentation_level + 1);
936            tmp_buf
937        }));
938        for else_if in &self.else_ifs {
939            let cond = {
940                let mut tmp_buf = String::new();
941                else_if.cond.recast(&mut tmp_buf, options, indentation_level, ctxt);
942                tmp_buf
943            };
944            lines.push((0, format!("}} else if {cond} {{")));
945            lines.push((1, {
946                let mut tmp_buf = String::new();
947                else_if.then_val.recast(&mut tmp_buf, options, indentation_level + 1);
948                tmp_buf
949            }));
950        }
951        lines.push((0, "} else {".to_owned()));
952        lines.push((1, {
953            let mut tmp_buf = String::new();
954            self.final_else.recast(&mut tmp_buf, options, indentation_level + 1);
955            tmp_buf
956        }));
957        lines.push((0, "}".to_owned()));
958        let out = lines
959            .into_iter()
960            .map(|(ind, line)| format!("{}{}", options.get_indentation(indentation_level + ind), line.trim()))
961            .collect::<Vec<_>>()
962            .join("\n");
963        buf.push_str(&out);
964    }
965}
966
967impl Node<PipeExpression> {
968    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, preceding_indent: bool) {
969        if preceding_indent {
970            options.write_indentation(buf, indentation_level);
971        }
972        for (index, statement) in self.body.iter().enumerate() {
973            statement.recast(buf, options, indentation_level + 1, ExprContext::Pipe);
974            let non_code_meta = &self.non_code_meta;
975            if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
976                for val in non_code_meta_value {
977                    // TODO: Remove allocation here by switching val.recast to accept buf.
978                    let formatted = if val.end == self.end {
979                        val.recast(options, indentation_level)
980                            .trim_end_matches('\n')
981                            .to_string()
982                    } else {
983                        val.recast(options, indentation_level + 1)
984                            .trim_end_matches('\n')
985                            .to_string()
986                    };
987                    if let NonCodeValue::BlockComment { .. } = val.value {
988                        buf.push('\n');
989                    }
990                    buf.push_str(&formatted);
991                }
992            }
993
994            if index != self.body.len() - 1 {
995                buf.push('\n');
996                options.write_indentation(buf, indentation_level + 1);
997                buf.push_str(PIPE_OPERATOR);
998                buf.push(' ');
999            }
1000        }
1001    }
1002}
1003
1004impl FunctionExpression {
1005    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
1006        // We don't want to end with a new line inside nested functions.
1007        let mut new_options = options.clone();
1008        new_options.insert_final_newline = false;
1009
1010        buf.push('(');
1011        for (i, param) in self.params.iter().enumerate() {
1012            param.recast(buf, options, indentation_level);
1013            if i < self.params.len() - 1 {
1014                buf.push_str(", ");
1015            }
1016        }
1017        buf.push(')');
1018        if let Some(return_type) = &self.return_type {
1019            write!(buf, ": {return_type}").no_fail();
1020        }
1021        writeln!(buf, " {{").no_fail();
1022        self.body.recast(buf, &new_options, indentation_level + 1);
1023        buf.push('\n');
1024        options.write_indentation(buf, indentation_level);
1025        buf.push('}');
1026    }
1027}
1028
1029impl Parameter {
1030    pub fn recast(&self, buf: &mut String, _options: &FormatOptions, _indentation_level: usize) {
1031        if !self.labeled {
1032            buf.push('@');
1033        }
1034        buf.push_str(&self.identifier.name);
1035        if self.default_value.is_some() {
1036            buf.push('?');
1037        };
1038        if let Some(ty) = &self.param_type {
1039            buf.push_str(": ");
1040            write!(buf, "{ty}").no_fail();
1041        }
1042        if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
1043            buf.push_str(" = ");
1044            literal.recast(buf);
1045        };
1046    }
1047}
1048
1049impl SketchBlock {
1050    pub(crate) fn recast(
1051        &self,
1052        buf: &mut String,
1053        options: &FormatOptions,
1054        indentation_level: usize,
1055        ctxt: ExprContext,
1056    ) {
1057        let name = Name {
1058            name: Node {
1059                inner: Identifier {
1060                    name: SketchBlock::CALLEE_NAME.to_owned(),
1061                    digest: None,
1062                },
1063                start: Default::default(),
1064                end: Default::default(),
1065                module_id: Default::default(),
1066                outer_attrs: Default::default(),
1067                pre_comments: Default::default(),
1068                comment_start: Default::default(),
1069            },
1070            path: Vec::new(),
1071            abs_path: false,
1072            digest: None,
1073        };
1074        recast_call(&name, None, &self.arguments, buf, options, indentation_level, ctxt);
1075
1076        // We don't want to end with a new line inside nested blocks.
1077        let mut new_options = options.clone();
1078        new_options.insert_final_newline = false;
1079
1080        writeln!(buf, " {{").no_fail();
1081        self.body.recast(buf, &new_options, indentation_level + 1);
1082        buf.push('\n');
1083        options.write_indentation(buf, indentation_level);
1084        buf.push('}');
1085    }
1086}
1087
1088impl Block {
1089    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
1090        recast_body(
1091            &self.items,
1092            &self.non_code_meta,
1093            &self.inner_attrs,
1094            buf,
1095            options,
1096            indentation_level,
1097        );
1098    }
1099}
1100
1101impl SketchVar {
1102    fn recast(&self, buf: &mut String) {
1103        if let Some(initial) = &self.initial {
1104            write!(buf, "var ").no_fail();
1105            initial.recast(buf);
1106        } else {
1107            write!(buf, "var").no_fail();
1108        }
1109    }
1110}
1111
1112/// Collect all the kcl (and other relevant) files in a directory, recursively.
1113#[cfg(not(target_arch = "wasm32"))]
1114#[async_recursion::async_recursion]
1115pub async fn walk_dir(dir: &std::path::PathBuf) -> Result<Vec<std::path::PathBuf>, anyhow::Error> {
1116    // Make sure we actually have a directory.
1117    if !dir.is_dir() {
1118        anyhow::bail!("`{}` is not a directory", dir.display());
1119    }
1120
1121    let mut entries = tokio::fs::read_dir(dir).await?;
1122
1123    let mut files = Vec::new();
1124    while let Some(entry) = entries.next_entry().await? {
1125        let path = entry.path();
1126
1127        if path.is_dir() {
1128            files.extend(walk_dir(&path).await?);
1129        } else if path
1130            .extension()
1131            .is_some_and(|ext| crate::RELEVANT_FILE_EXTENSIONS.contains(&ext.to_string_lossy().to_lowercase()))
1132        {
1133            files.push(path);
1134        }
1135    }
1136
1137    Ok(files)
1138}
1139
1140/// Recast all the kcl files in a directory, recursively.
1141#[cfg(not(target_arch = "wasm32"))]
1142pub async fn recast_dir(dir: &std::path::Path, options: &crate::FormatOptions) -> Result<(), anyhow::Error> {
1143    let files = walk_dir(&dir.to_path_buf()).await.map_err(|err| {
1144        crate::KclError::new_internal(crate::errors::KclErrorDetails::new(
1145            format!("Failed to walk directory `{}`: {:?}", dir.display(), err),
1146            vec![crate::SourceRange::default()],
1147        ))
1148    })?;
1149
1150    let futures = files
1151        .into_iter()
1152        .filter(|file| file.extension().is_some_and(|ext| ext == "kcl")) // We only care about kcl
1153        // files here.
1154        .map(|file| {
1155            let options = options.clone();
1156            tokio::spawn(async move {
1157                let contents = tokio::fs::read_to_string(&file)
1158                    .await
1159                    .map_err(|err| anyhow::anyhow!("Failed to read file `{}`: {:?}", file.display(), err))?;
1160                let (program, ces) = crate::Program::parse(&contents).map_err(|err| {
1161                    let report = crate::Report {
1162                        kcl_source: contents.to_string(),
1163                        error: err,
1164                        filename: file.to_string_lossy().to_string(),
1165                    };
1166                    let report = miette::Report::new(report);
1167                    anyhow::anyhow!("{:?}", report)
1168                })?;
1169                for ce in &ces {
1170                    if ce.severity != crate::errors::Severity::Warning {
1171                        let report = crate::Report {
1172                            kcl_source: contents.to_string(),
1173                            error: crate::KclError::new_semantic(ce.clone().into()),
1174                            filename: file.to_string_lossy().to_string(),
1175                        };
1176                        let report = miette::Report::new(report);
1177                        anyhow::bail!("{:?}", report);
1178                    }
1179                }
1180                let Some(program) = program else {
1181                    anyhow::bail!("Failed to parse file `{}`", file.display());
1182                };
1183                let recast = program.recast_with_options(&options);
1184                tokio::fs::write(&file, recast)
1185                    .await
1186                    .map_err(|err| anyhow::anyhow!("Failed to write file `{}`: {:?}", file.display(), err))?;
1187
1188                Ok::<(), anyhow::Error>(())
1189            })
1190        })
1191        .collect::<Vec<_>>();
1192
1193    // Join all futures and await their completion
1194    let results = futures::future::join_all(futures).await;
1195
1196    // Check if any of the futures failed.
1197    let mut errors = Vec::new();
1198    for result in results {
1199        if let Err(err) = result? {
1200            errors.push(err);
1201        }
1202    }
1203
1204    if !errors.is_empty() {
1205        anyhow::bail!("Failed to recast some files: {:?}", errors);
1206    }
1207
1208    Ok(())
1209}
1210
1211#[cfg(test)]
1212mod tests {
1213    use pretty_assertions::assert_eq;
1214
1215    use super::*;
1216    use crate::{ModuleId, parsing::ast::types::FormatOptions};
1217
1218    #[test]
1219    fn test_recast_annotations_without_body_items() {
1220        let input = r#"@settings(defaultLengthUnit = in)
1221"#;
1222        let program = crate::parsing::top_level_parse(input).unwrap();
1223        let output = program.recast_top(&Default::default(), 0);
1224        assert_eq!(output, input);
1225    }
1226
1227    #[test]
1228    fn test_recast_annotations_in_function_body() {
1229        let input = r#"fn myFunc() {
1230  @meta(yes = true)
1231
1232  x = 2
1233}
1234"#;
1235        let program = crate::parsing::top_level_parse(input).unwrap();
1236        let output = program.recast_top(&Default::default(), 0);
1237        assert_eq!(output, input);
1238    }
1239
1240    #[test]
1241    fn test_recast_annotations_in_function_body_without_items() {
1242        let input = "\
1243fn myFunc() {
1244  @meta(yes = true)
1245}
1246";
1247        let program = crate::parsing::top_level_parse(input).unwrap();
1248        let output = program.recast_top(&Default::default(), 0);
1249        assert_eq!(output, input);
1250    }
1251
1252    #[test]
1253    fn recast_annotations_with_comments() {
1254        let input = r#"// Start comment
1255
1256// Comment on attr
1257@settings(defaultLengthUnit = in)
1258
1259// Comment on item
1260foo = 42
1261
1262// Comment on another item
1263@(impl = kcl)
1264bar = 0
1265"#;
1266        let program = crate::parsing::top_level_parse(input).unwrap();
1267        let output = program.recast_top(&Default::default(), 0);
1268        assert_eq!(output, input);
1269    }
1270
1271    #[test]
1272    fn recast_annotations_with_block_comment() {
1273        let input = r#"/* Start comment
1274
1275sdfsdfsdfs */
1276@settings(defaultLengthUnit = in)
1277
1278foo = 42
1279"#;
1280        let program = crate::parsing::top_level_parse(input).unwrap();
1281        let output = program.recast_top(&Default::default(), 0);
1282        assert_eq!(output, input);
1283    }
1284
1285    #[test]
1286    fn test_recast_if_else_if_same() {
1287        let input = r#"b = if false {
1288  3
1289} else if true {
1290  4
1291} else {
1292  5
1293}
1294"#;
1295        let program = crate::parsing::top_level_parse(input).unwrap();
1296        let output = program.recast_top(&Default::default(), 0);
1297        assert_eq!(output, input);
1298    }
1299
1300    #[test]
1301    fn test_recast_if_same() {
1302        let input = r#"b = if false {
1303  3
1304} else {
1305  5
1306}
1307"#;
1308        let program = crate::parsing::top_level_parse(input).unwrap();
1309        let output = program.recast_top(&Default::default(), 0);
1310        assert_eq!(output, input);
1311    }
1312
1313    #[test]
1314    fn test_recast_import() {
1315        let input = r#"import a from "a.kcl"
1316import a as aaa from "a.kcl"
1317import a, b from "a.kcl"
1318import a as aaa, b from "a.kcl"
1319import a, b as bbb from "a.kcl"
1320import a as aaa, b as bbb from "a.kcl"
1321import "a_b.kcl"
1322import "a-b.kcl" as b
1323import * from "a.kcl"
1324export import a as aaa from "a.kcl"
1325export import a, b from "a.kcl"
1326export import a as aaa, b from "a.kcl"
1327export import a, b as bbb from "a.kcl"
1328"#;
1329        let program = crate::parsing::top_level_parse(input).unwrap();
1330        let output = program.recast_top(&Default::default(), 0);
1331        assert_eq!(output, input);
1332    }
1333
1334    #[test]
1335    fn test_recast_import_as_same_name() {
1336        let input = r#"import a as a from "a.kcl"
1337"#;
1338        let program = crate::parsing::top_level_parse(input).unwrap();
1339        let output = program.recast_top(&Default::default(), 0);
1340        let expected = r#"import a from "a.kcl"
1341"#;
1342        assert_eq!(output, expected);
1343    }
1344
1345    #[test]
1346    fn test_recast_export_fn() {
1347        let input = r#"export fn a() {
1348  return 0
1349}
1350"#;
1351        let program = crate::parsing::top_level_parse(input).unwrap();
1352        let output = program.recast_top(&Default::default(), 0);
1353        assert_eq!(output, input);
1354    }
1355
1356    #[test]
1357    fn test_recast_sketch_block_with_no_args() {
1358        let input = r#"sketch() {
1359  return 0
1360}
1361"#;
1362        let program = crate::parsing::top_level_parse(input).unwrap();
1363        let output = program.recast_top(&Default::default(), 0);
1364        assert_eq!(output, input);
1365    }
1366
1367    #[test]
1368    fn test_recast_sketch_block_with_labeled_args() {
1369        let input = r#"sketch(on = XY) {
1370  return 0
1371}
1372"#;
1373        let program = crate::parsing::top_level_parse(input).unwrap();
1374        let output = program.recast_top(&Default::default(), 0);
1375        assert_eq!(output, input);
1376    }
1377
1378    #[test]
1379    fn test_recast_sketch_block_with_statements_in_block() {
1380        let input = r#"sketch() {
1381  // Comments inside block.
1382  x = 5
1383  y = 2
1384}
1385"#;
1386        let program = crate::parsing::top_level_parse(input).unwrap();
1387        let output = program.recast_top(&Default::default(), 0);
1388        assert_eq!(output, input);
1389    }
1390
1391    #[test]
1392    fn test_recast_bug_fn_in_fn() {
1393        let some_program_string = r#"// Start point (top left)
1394zoo_x = -20
1395zoo_y = 7
1396// Scale
1397s = 1 // s = 1 -> height of Z is 13.4mm
1398// Depth
1399d = 1
1400
1401fn rect(x, y, w, h) {
1402  startSketchOn(XY)
1403    |> startProfile(at = [x, y])
1404    |> xLine(length = w)
1405    |> yLine(length = h)
1406    |> xLine(length = -w)
1407    |> close()
1408    |> extrude(d)
1409}
1410
1411fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
1412  startSketchOn(XY)
1413    |> startProfile(at = [x1, y1])
1414    |> line(endAbsolute = [x2, y2])
1415    |> line(endAbsolute = [x3, y3])
1416    |> line(endAbsolute = [x4, y4])
1417    |> close()
1418    |> extrude(d)
1419}
1420
1421fn crosshair(x, y) {
1422  startSketchOn(XY)
1423    |> startProfile(at = [x, y])
1424    |> yLine(length = 1)
1425    |> yLine(length = -2)
1426    |> yLine(length = 1)
1427    |> xLine(length = 1)
1428    |> xLine(length = -2)
1429}
1430
1431fn z(z_x, z_y) {
1432  z_end_w = s * 8.4
1433  z_end_h = s * 3
1434  z_corner = s * 2
1435  z_w = z_end_w + 2 * z_corner
1436  z_h = z_w * 1.08130081300813
1437  rect(
1438    z_x,
1439    a = z_y,
1440    b = z_end_w,
1441    c = -z_end_h,
1442  )
1443  rect(
1444    z_x + z_w,
1445    a = z_y,
1446    b = -z_corner,
1447    c = -z_corner,
1448  )
1449  rect(
1450    z_x + z_w,
1451    a = z_y - z_h,
1452    b = -z_end_w,
1453    c = z_end_h,
1454  )
1455  rect(
1456    z_x,
1457    a = z_y - z_h,
1458    b = z_corner,
1459    c = z_corner,
1460  )
1461}
1462
1463fn o(c_x, c_y) {
1464  // Outer and inner radii
1465  o_r = s * 6.95
1466  i_r = 0.5652173913043478 * o_r
1467
1468  // Angle offset for diagonal break
1469  a = 7
1470
1471  // Start point for the top sketch
1472  o_x1 = c_x + o_r * cos((45 + a) / 360 * TAU)
1473  o_y1 = c_y + o_r * sin((45 + a) / 360 * TAU)
1474
1475  // Start point for the bottom sketch
1476  o_x2 = c_x + o_r * cos((225 + a) / 360 * TAU)
1477  o_y2 = c_y + o_r * sin((225 + a) / 360 * TAU)
1478
1479  // End point for the bottom startSketch
1480  o_x3 = c_x + o_r * cos((45 - a) / 360 * TAU)
1481  o_y3 = c_y + o_r * sin((45 - a) / 360 * TAU)
1482
1483  // Where is the center?
1484  // crosshair(c_x, c_y)
1485
1486
1487  startSketchOn(XY)
1488    |> startProfile(at = [o_x1, o_y1])
1489    |> arc(radius = o_r, angle_start = 45 + a, angle_end = 225 - a)
1490    |> angledLine(angle = 45, length = o_r - i_r)
1491    |> arc(radius = i_r, angle_start = 225 - a, angle_end = 45 + a)
1492    |> close()
1493    |> extrude(d)
1494
1495  startSketchOn(XY)
1496    |> startProfile(at = [o_x2, o_y2])
1497    |> arc(radius = o_r, angle_start = 225 + a, angle_end = 360 + 45 - a)
1498    |> angledLine(angle = 225, length = o_r - i_r)
1499    |> arc(radius = i_r, angle_start = 45 - a, angle_end = 225 + a - 360)
1500    |> close()
1501    |> extrude(d)
1502}
1503
1504fn zoo(x0, y0) {
1505  z(x = x0, y = y0)
1506  o(x = x0 + s * 20, y = y0 - (s * 6.7))
1507  o(x = x0 + s * 35, y = y0 - (s * 6.7))
1508}
1509
1510zoo(x = zoo_x, y = zoo_y)
1511"#;
1512        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1513
1514        let recasted = program.recast_top(&Default::default(), 0);
1515        assert_eq!(recasted, some_program_string);
1516    }
1517
1518    #[test]
1519    fn test_nested_fns_indent() {
1520        let some_program_string = "\
1521x = 1
1522fn rect(x, y, w, h) {
1523  y = 2
1524  z = 3
1525  startSketchOn(XY)
1526    |> startProfile(at = [x, y])
1527    |> xLine(length = w)
1528    |> yLine(length = h)
1529    |> xLine(length = -w)
1530    |> close()
1531    |> extrude(d)
1532}
1533";
1534        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1535
1536        let recasted = program.recast_top(&Default::default(), 0);
1537        assert_eq!(recasted, some_program_string);
1538    }
1539
1540    #[test]
1541    fn test_recast_bug_extra_parens() {
1542        let some_program_string = r#"// Ball Bearing
1543// A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads. 
1544
1545// Define constants like ball diameter, inside diameter, overhange length, and thickness
1546sphereDia = 0.5
1547insideDia = 1
1548thickness = 0.25
1549overHangLength = .4
1550
1551// Sketch and revolve the inside bearing piece
1552insideRevolve = startSketchOn(XZ)
1553  |> startProfile(at = [insideDia / 2, 0])
1554  |> line(end = [0, thickness + sphereDia / 2])
1555  |> line(end = [overHangLength, 0])
1556  |> line(end = [0, -thickness])
1557  |> line(end = [-overHangLength + thickness, 0])
1558  |> line(end = [0, -sphereDia])
1559  |> line(end = [overHangLength - thickness, 0])
1560  |> line(end = [0, -thickness])
1561  |> line(end = [-overHangLength, 0])
1562  |> close()
1563  |> revolve(axis = Y)
1564
1565// Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis)
1566sphere = startSketchOn(XZ)
1567  |> startProfile(at = [
1568       0.05 + insideDia / 2 + thickness,
1569       0 - 0.05
1570     ])
1571  |> line(end = [sphereDia - 0.1, 0])
1572  |> arc(
1573       angle_start = 0,
1574       angle_end = -180,
1575       radius = sphereDia / 2 - 0.05
1576     )
1577  |> close()
1578  |> revolve(axis = X)
1579  |> patternCircular3d(
1580       axis = [0, 0, 1],
1581       center = [0, 0, 0],
1582       repetitions = 10,
1583       arcDegrees = 360,
1584       rotateDuplicates = true
1585     )
1586
1587// Sketch and revolve the outside bearing
1588outsideRevolve = startSketchOn(XZ)
1589  |> startProfile(at = [
1590       insideDia / 2 + thickness + sphereDia,
1591       0
1592       ]
1593     )
1594  |> line(end = [0, sphereDia / 2])
1595  |> line(end = [-overHangLength + thickness, 0])
1596  |> line(end = [0, thickness])
1597  |> line(end = [overHangLength, 0])
1598  |> line(end = [0, -2 * thickness - sphereDia])
1599  |> line(end = [-overHangLength, 0])
1600  |> line(end = [0, thickness])
1601  |> line(end = [overHangLength - thickness, 0])
1602  |> close()
1603  |> revolve(axis = Y)"#;
1604        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1605
1606        let recasted = program.recast_top(&Default::default(), 0);
1607        assert_eq!(
1608            recasted,
1609            r#"// Ball Bearing
1610// A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads.
1611
1612// Define constants like ball diameter, inside diameter, overhange length, and thickness
1613sphereDia = 0.5
1614insideDia = 1
1615thickness = 0.25
1616overHangLength = .4
1617
1618// Sketch and revolve the inside bearing piece
1619insideRevolve = startSketchOn(XZ)
1620  |> startProfile(at = [insideDia / 2, 0])
1621  |> line(end = [0, thickness + sphereDia / 2])
1622  |> line(end = [overHangLength, 0])
1623  |> line(end = [0, -thickness])
1624  |> line(end = [-overHangLength + thickness, 0])
1625  |> line(end = [0, -sphereDia])
1626  |> line(end = [overHangLength - thickness, 0])
1627  |> line(end = [0, -thickness])
1628  |> line(end = [-overHangLength, 0])
1629  |> close()
1630  |> revolve(axis = Y)
1631
1632// Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis)
1633sphere = startSketchOn(XZ)
1634  |> startProfile(at = [
1635       0.05 + insideDia / 2 + thickness,
1636       0 - 0.05
1637     ])
1638  |> line(end = [sphereDia - 0.1, 0])
1639  |> arc(angle_start = 0, angle_end = -180, radius = sphereDia / 2 - 0.05)
1640  |> close()
1641  |> revolve(axis = X)
1642  |> patternCircular3d(
1643       axis = [0, 0, 1],
1644       center = [0, 0, 0],
1645       repetitions = 10,
1646       arcDegrees = 360,
1647       rotateDuplicates = true,
1648     )
1649
1650// Sketch and revolve the outside bearing
1651outsideRevolve = startSketchOn(XZ)
1652  |> startProfile(at = [
1653       insideDia / 2 + thickness + sphereDia,
1654       0
1655     ])
1656  |> line(end = [0, sphereDia / 2])
1657  |> line(end = [-overHangLength + thickness, 0])
1658  |> line(end = [0, thickness])
1659  |> line(end = [overHangLength, 0])
1660  |> line(end = [0, -2 * thickness - sphereDia])
1661  |> line(end = [-overHangLength, 0])
1662  |> line(end = [0, thickness])
1663  |> line(end = [overHangLength - thickness, 0])
1664  |> close()
1665  |> revolve(axis = Y)
1666"#
1667        );
1668    }
1669
1670    #[test]
1671    fn test_recast_fn_in_object() {
1672        let some_program_string = r#"bing = { yo = 55 }
1673myNestedVar = [{ prop = callExp(bing.yo) }]
1674"#;
1675        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1676
1677        let recasted = program.recast_top(&Default::default(), 0);
1678        assert_eq!(recasted, some_program_string);
1679    }
1680
1681    #[test]
1682    fn test_recast_fn_in_array() {
1683        let some_program_string = r#"bing = { yo = 55 }
1684myNestedVar = [callExp(bing.yo)]
1685"#;
1686        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1687
1688        let recasted = program.recast_top(&Default::default(), 0);
1689        assert_eq!(recasted, some_program_string);
1690    }
1691
1692    #[test]
1693    fn test_recast_ranges() {
1694        let some_program_string = r#"foo = [0..10]
1695ten = 10
1696bar = [0 + 1 .. ten]
1697"#;
1698        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1699
1700        let recasted = program.recast_top(&Default::default(), 0);
1701        assert_eq!(recasted, some_program_string);
1702    }
1703
1704    #[test]
1705    fn test_recast_space_in_fn_call() {
1706        let some_program_string = r#"fn thing (x) {
1707    return x + 1
1708}
1709
1710thing ( 1 )
1711"#;
1712        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1713
1714        let recasted = program.recast_top(&Default::default(), 0);
1715        assert_eq!(
1716            recasted,
1717            r#"fn thing(x) {
1718  return x + 1
1719}
1720
1721thing(1)
1722"#
1723        );
1724    }
1725
1726    #[test]
1727    fn test_recast_typed_fn() {
1728        let some_program_string = r#"fn thing(x: string, y: [bool]): number {
1729  return x + 1
1730}
1731"#;
1732        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1733
1734        let recasted = program.recast_top(&Default::default(), 0);
1735        assert_eq!(recasted, some_program_string);
1736    }
1737
1738    #[test]
1739    fn test_recast_typed_consts() {
1740        let some_program_string = r#"a = 42: number
1741export b = 3.2: number(ft)
1742c = "dsfds": A | B | C
1743d = [1]: [number]
1744e = foo: [number; 3]
1745f = [1, 2, 3]: [number; 1+]
1746f = [1, 2, 3]: [number; 3+]
1747"#;
1748        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1749
1750        let recasted = program.recast_top(&Default::default(), 0);
1751        assert_eq!(recasted, some_program_string);
1752    }
1753
1754    #[test]
1755    fn test_recast_object_fn_in_array_weird_bracket() {
1756        let some_program_string = r#"bing = { yo = 55 }
1757myNestedVar = [
1758  {
1759  prop:   line(a = [bing.yo, 21], b = sketch001)
1760}
1761]
1762"#;
1763        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1764
1765        let recasted = program.recast_top(&Default::default(), 0);
1766        assert_eq!(
1767            recasted,
1768            r#"bing = { yo = 55 }
1769myNestedVar = [
1770  {
1771  prop = line(a = [bing.yo, 21], b = sketch001)
1772}
1773]
1774"#
1775        );
1776    }
1777
1778    #[test]
1779    fn test_recast_empty_file() {
1780        let some_program_string = r#""#;
1781        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1782
1783        let recasted = program.recast_top(&Default::default(), 0);
1784        // Its VERY important this comes back with zero new lines.
1785        assert_eq!(recasted, r#""#);
1786    }
1787
1788    #[test]
1789    fn test_recast_empty_file_new_line() {
1790        let some_program_string = r#"
1791"#;
1792        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1793
1794        let recasted = program.recast_top(&Default::default(), 0);
1795        // Its VERY important this comes back with zero new lines.
1796        assert_eq!(recasted, r#""#);
1797    }
1798
1799    #[test]
1800    fn test_recast_shebang() {
1801        let some_program_string = r#"#!/usr/local/env zoo kcl
1802part001 = startSketchOn(XY)
1803  |> startProfile(at = [-10, -10])
1804  |> line(end = [20, 0])
1805  |> line(end = [0, 20])
1806  |> line(end = [-20, 0])
1807  |> close()
1808"#;
1809
1810        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1811
1812        let recasted = program.recast_top(&Default::default(), 0);
1813        assert_eq!(
1814            recasted,
1815            r#"#!/usr/local/env zoo kcl
1816
1817part001 = startSketchOn(XY)
1818  |> startProfile(at = [-10, -10])
1819  |> line(end = [20, 0])
1820  |> line(end = [0, 20])
1821  |> line(end = [-20, 0])
1822  |> close()
1823"#
1824        );
1825    }
1826
1827    #[test]
1828    fn test_recast_shebang_new_lines() {
1829        let some_program_string = r#"#!/usr/local/env zoo kcl
1830        
1831
1832
1833part001 = startSketchOn(XY)
1834  |> startProfile(at = [-10, -10])
1835  |> line(end = [20, 0])
1836  |> line(end = [0, 20])
1837  |> line(end = [-20, 0])
1838  |> close()
1839"#;
1840
1841        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1842
1843        let recasted = program.recast_top(&Default::default(), 0);
1844        assert_eq!(
1845            recasted,
1846            r#"#!/usr/local/env zoo kcl
1847
1848part001 = startSketchOn(XY)
1849  |> startProfile(at = [-10, -10])
1850  |> line(end = [20, 0])
1851  |> line(end = [0, 20])
1852  |> line(end = [-20, 0])
1853  |> close()
1854"#
1855        );
1856    }
1857
1858    #[test]
1859    fn test_recast_shebang_with_comments() {
1860        let some_program_string = r#"#!/usr/local/env zoo kcl
1861        
1862// Yo yo my comments.
1863part001 = startSketchOn(XY)
1864  |> startProfile(at = [-10, -10])
1865  |> line(end = [20, 0])
1866  |> line(end = [0, 20])
1867  |> line(end = [-20, 0])
1868  |> close()
1869"#;
1870
1871        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1872
1873        let recasted = program.recast_top(&Default::default(), 0);
1874        assert_eq!(
1875            recasted,
1876            r#"#!/usr/local/env zoo kcl
1877
1878// Yo yo my comments.
1879part001 = startSketchOn(XY)
1880  |> startProfile(at = [-10, -10])
1881  |> line(end = [20, 0])
1882  |> line(end = [0, 20])
1883  |> line(end = [-20, 0])
1884  |> close()
1885"#
1886        );
1887    }
1888
1889    #[test]
1890    fn test_recast_empty_function_body_with_comments() {
1891        let input = r#"fn myFunc() {
1892  // Yo yo my comments.
1893}
1894"#;
1895
1896        let program = crate::parsing::top_level_parse(input).unwrap();
1897        let output = program.recast_top(&Default::default(), 0);
1898        assert_eq!(output, input);
1899    }
1900
1901    #[test]
1902    fn test_recast_large_file() {
1903        let some_program_string = r#"@settings(units=mm)
1904// define nts
1905radius = 6.0
1906width = 144.0
1907length = 83.0
1908depth = 45.0
1909thk = 5
1910hole_diam = 5
1911// define a rectangular shape func
1912fn rectShape(pos, w, l) {
1913  rr = startSketchOn(XY)
1914    |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
1915    |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
1916    |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
1917    |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
1918    |> close($edge4)
1919  return rr
1920}
1921// build the body of the focusrite scarlett solo gen 4
1922// only used for visualization
1923scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
1924  |> extrude(depth)
1925  |> fillet(
1926       radius = radius,
1927       tags = [
1928  edge2,
1929  edge4,
1930  getOppositeEdge(edge2),
1931  getOppositeEdge(edge4)
1932]
1933   )
1934  // build the bracket sketch around the body
1935fn bracketSketch(w, d, t) {
1936  s = startSketchOn({
1937         plane = {
1938  origin = { x = 0, y = length / 2 + thk, z = 0 },
1939  x_axis = { x = 1, y = 0, z = 0 },
1940  y_axis = { x = 0, y = 0, z = 1 },
1941  z_axis = { x = 0, y = 1, z = 0 }
1942}
1943       })
1944    |> startProfile(at = [-w / 2 - t, d + t])
1945    |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
1946    |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
1947    |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
1948    |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
1949    |> line(endAbsolute = [w / 2, 0], tag = $edge5)
1950    |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
1951    |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
1952    |> close($edge8)
1953  return s
1954}
1955// build the body of the bracket
1956bracket_body = bracketSketch(w = width, d = depth, t = thk)
1957  |> extrude(length + 10)
1958  |> fillet(
1959       radius = radius,
1960       tags = [
1961  getNextAdjacentEdge(edge7),
1962  getNextAdjacentEdge(edge2),
1963  getNextAdjacentEdge(edge3),
1964  getNextAdjacentEdge(edge6)
1965]
1966     )
1967  // build the tabs of the mounting bracket (right side)
1968tabs_r = startSketchOn({
1969       plane = {
1970  origin = { x = 0, y = 0, z = depth + thk },
1971  x_axis = { x = 1, y = 0, z = 0 },
1972  y_axis = { x = 0, y = 1, z = 0 },
1973  z_axis = { x = 0, y = 0, z = 1 }
1974}
1975     })
1976  |> startProfile(at = [width / 2 + thk, length / 2 + thk])
1977  |> line(end = [10, -5])
1978  |> line(end = [0, -10])
1979  |> line(end = [-10, -5])
1980  |> close()
1981  |> subtract2d(tool = circle(
1982       center = [
1983         width / 2 + thk + hole_diam,
1984         length / 2 - hole_diam
1985       ],
1986       radius = hole_diam / 2
1987     ))
1988  |> extrude(-thk)
1989  |> patternLinear3d(
1990       axis = [0, -1, 0],
1991       repetitions = 1,
1992       distance = length - 10
1993     )
1994  // build the tabs of the mounting bracket (left side)
1995tabs_l = startSketchOn({
1996       plane = {
1997  origin = { x = 0, y = 0, z = depth + thk },
1998  x_axis = { x = 1, y = 0, z = 0 },
1999  y_axis = { x = 0, y = 1, z = 0 },
2000  z_axis = { x = 0, y = 0, z = 1 }
2001}
2002     })
2003  |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2004  |> line(end = [-10, -5])
2005  |> line(end = [0, -10])
2006  |> line(end = [10, -5])
2007  |> close()
2008  |> subtract2d(tool = circle(
2009       center = [
2010         -width / 2 - thk - hole_diam,
2011         length / 2 - hole_diam
2012       ],
2013       radius = hole_diam / 2
2014     ))
2015  |> extrude(-thk)
2016  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2017"#;
2018        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2019
2020        let recasted = program.recast_top(&Default::default(), 0);
2021        // Its VERY important this comes back with zero new lines.
2022        assert_eq!(
2023            recasted,
2024            r#"@settings(units = mm)
2025
2026// define nts
2027radius = 6.0
2028width = 144.0
2029length = 83.0
2030depth = 45.0
2031thk = 5
2032hole_diam = 5
2033// define a rectangular shape func
2034fn rectShape(pos, w, l) {
2035  rr = startSketchOn(XY)
2036    |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2037    |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2038    |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2039    |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2040    |> close($edge4)
2041  return rr
2042}
2043// build the body of the focusrite scarlett solo gen 4
2044// only used for visualization
2045scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2046  |> extrude(depth)
2047  |> fillet(
2048       radius = radius,
2049       tags = [
2050         edge2,
2051         edge4,
2052         getOppositeEdge(edge2),
2053         getOppositeEdge(edge4)
2054       ],
2055     )
2056// build the bracket sketch around the body
2057fn bracketSketch(w, d, t) {
2058  s = startSketchOn({
2059         plane = {
2060           origin = { x = 0, y = length / 2 + thk, z = 0 },
2061           x_axis = { x = 1, y = 0, z = 0 },
2062           y_axis = { x = 0, y = 0, z = 1 },
2063           z_axis = { x = 0, y = 1, z = 0 }
2064         }
2065       })
2066    |> startProfile(at = [-w / 2 - t, d + t])
2067    |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2068    |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2069    |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2070    |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2071    |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2072    |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2073    |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2074    |> close($edge8)
2075  return s
2076}
2077// build the body of the bracket
2078bracket_body = bracketSketch(w = width, d = depth, t = thk)
2079  |> extrude(length + 10)
2080  |> fillet(
2081       radius = radius,
2082       tags = [
2083         getNextAdjacentEdge(edge7),
2084         getNextAdjacentEdge(edge2),
2085         getNextAdjacentEdge(edge3),
2086         getNextAdjacentEdge(edge6)
2087       ],
2088     )
2089// build the tabs of the mounting bracket (right side)
2090tabs_r = startSketchOn({
2091       plane = {
2092         origin = { x = 0, y = 0, z = depth + thk },
2093         x_axis = { x = 1, y = 0, z = 0 },
2094         y_axis = { x = 0, y = 1, z = 0 },
2095         z_axis = { x = 0, y = 0, z = 1 }
2096       }
2097     })
2098  |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2099  |> line(end = [10, -5])
2100  |> line(end = [0, -10])
2101  |> line(end = [-10, -5])
2102  |> close()
2103  |> subtract2d(tool = circle(
2104       center = [
2105         width / 2 + thk + hole_diam,
2106         length / 2 - hole_diam
2107       ],
2108       radius = hole_diam / 2,
2109     ))
2110  |> extrude(-thk)
2111  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
2112// build the tabs of the mounting bracket (left side)
2113tabs_l = startSketchOn({
2114       plane = {
2115         origin = { x = 0, y = 0, z = depth + thk },
2116         x_axis = { x = 1, y = 0, z = 0 },
2117         y_axis = { x = 0, y = 1, z = 0 },
2118         z_axis = { x = 0, y = 0, z = 1 }
2119       }
2120     })
2121  |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2122  |> line(end = [-10, -5])
2123  |> line(end = [0, -10])
2124  |> line(end = [10, -5])
2125  |> close()
2126  |> subtract2d(tool = circle(
2127       center = [
2128         -width / 2 - thk - hole_diam,
2129         length / 2 - hole_diam
2130       ],
2131       radius = hole_diam / 2,
2132     ))
2133  |> extrude(-thk)
2134  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2135"#
2136        );
2137    }
2138
2139    #[test]
2140    fn test_recast_nested_var_declaration_in_fn_body() {
2141        let some_program_string = r#"fn cube(pos, scale) {
2142   sg = startSketchOn(XY)
2143  |> startProfile(at = pos)
2144  |> line(end = [0, scale])
2145  |> line(end = [scale, 0])
2146  |> line(end = [0, -scale])
2147  |> close()
2148  |> extrude(scale)
2149}"#;
2150        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2151
2152        let recasted = program.recast_top(&Default::default(), 0);
2153        assert_eq!(
2154            recasted,
2155            r#"fn cube(pos, scale) {
2156  sg = startSketchOn(XY)
2157    |> startProfile(at = pos)
2158    |> line(end = [0, scale])
2159    |> line(end = [scale, 0])
2160    |> line(end = [0, -scale])
2161    |> close()
2162    |> extrude(scale)
2163}
2164"#
2165        );
2166    }
2167
2168    #[test]
2169    fn test_as() {
2170        let some_program_string = r#"fn cube(pos, scale) {
2171  x = dfsfs + dfsfsd as y
2172
2173  sg = startSketchOn(XY)
2174    |> startProfile(at = pos) as foo
2175    |> line([0, scale])
2176    |> line([scale, 0]) as bar
2177    |> line([0 as baz, -scale] as qux)
2178    |> close()
2179    |> extrude(length = scale)
2180}
2181
2182cube(pos = 0, scale = 0) as cub
2183"#;
2184        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2185
2186        let recasted = program.recast_top(&Default::default(), 0);
2187        assert_eq!(recasted, some_program_string,);
2188    }
2189
2190    #[test]
2191    fn test_recast_with_bad_indentation() {
2192        let some_program_string = r#"part001 = startSketchOn(XY)
2193  |> startProfile(at = [0.0, 5.0])
2194              |> line(end = [0.4900857016, -0.0240763666])
2195    |> line(end = [0.6804562304, 0.9087880491])"#;
2196        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2197
2198        let recasted = program.recast_top(&Default::default(), 0);
2199        assert_eq!(
2200            recasted,
2201            r#"part001 = startSketchOn(XY)
2202  |> startProfile(at = [0.0, 5.0])
2203  |> line(end = [0.4900857016, -0.0240763666])
2204  |> line(end = [0.6804562304, 0.9087880491])
2205"#
2206        );
2207    }
2208
2209    #[test]
2210    fn test_recast_with_bad_indentation_and_inline_comment() {
2211        let some_program_string = r#"part001 = startSketchOn(XY)
2212  |> startProfile(at = [0.0, 5.0])
2213              |> line(end = [0.4900857016, -0.0240763666]) // hello world
2214    |> line(end = [0.6804562304, 0.9087880491])"#;
2215        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2216
2217        let recasted = program.recast_top(&Default::default(), 0);
2218        assert_eq!(
2219            recasted,
2220            r#"part001 = startSketchOn(XY)
2221  |> startProfile(at = [0.0, 5.0])
2222  |> line(end = [0.4900857016, -0.0240763666]) // hello world
2223  |> line(end = [0.6804562304, 0.9087880491])
2224"#
2225        );
2226    }
2227    #[test]
2228    fn test_recast_with_bad_indentation_and_line_comment() {
2229        let some_program_string = r#"part001 = startSketchOn(XY)
2230  |> startProfile(at = [0.0, 5.0])
2231              |> line(end = [0.4900857016, -0.0240763666])
2232        // hello world
2233    |> line(end = [0.6804562304, 0.9087880491])"#;
2234        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2235
2236        let recasted = program.recast_top(&Default::default(), 0);
2237        assert_eq!(
2238            recasted,
2239            r#"part001 = startSketchOn(XY)
2240  |> startProfile(at = [0.0, 5.0])
2241  |> line(end = [0.4900857016, -0.0240763666])
2242  // hello world
2243  |> line(end = [0.6804562304, 0.9087880491])
2244"#
2245        );
2246    }
2247
2248    #[test]
2249    fn test_recast_comment_in_a_fn_block() {
2250        let some_program_string = r#"fn myFn() {
2251  // this is a comment
2252  yo = { a = { b = { c = '123' } } } /* block
2253  comment */
2254
2255  key = 'c'
2256  // this is also a comment
2257    return things
2258}"#;
2259        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2260
2261        let recasted = program.recast_top(&Default::default(), 0);
2262        assert_eq!(
2263            recasted,
2264            r#"fn myFn() {
2265  // this is a comment
2266  yo = { a = { b = { c = '123' } } } /* block
2267  comment */
2268
2269  key = 'c'
2270  // this is also a comment
2271  return things
2272}
2273"#
2274        );
2275    }
2276
2277    #[test]
2278    fn test_recast_comment_under_variable() {
2279        let some_program_string = r#"key = 'c'
2280// this is also a comment
2281thing = 'foo'
2282"#;
2283        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2284
2285        let recasted = program.recast_top(&Default::default(), 0);
2286        assert_eq!(
2287            recasted,
2288            r#"key = 'c'
2289// this is also a comment
2290thing = 'foo'
2291"#
2292        );
2293    }
2294
2295    #[test]
2296    fn test_recast_multiline_comment_start_file() {
2297        let some_program_string = r#"// hello world
2298// I am a comment
2299key = 'c'
2300// this is also a comment
2301// hello
2302thing = 'foo'
2303"#;
2304        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2305
2306        let recasted = program.recast_top(&Default::default(), 0);
2307        assert_eq!(
2308            recasted,
2309            r#"// hello world
2310// I am a comment
2311key = 'c'
2312// this is also a comment
2313// hello
2314thing = 'foo'
2315"#
2316        );
2317    }
2318
2319    #[test]
2320    fn test_recast_empty_comment() {
2321        let some_program_string = r#"// hello world
2322//
2323// I am a comment
2324key = 'c'
2325
2326//
2327// I am a comment
2328thing = 'c'
2329
2330foo = 'bar' //
2331"#;
2332        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2333
2334        let recasted = program.recast_top(&Default::default(), 0);
2335        assert_eq!(
2336            recasted,
2337            r#"// hello world
2338//
2339// I am a comment
2340key = 'c'
2341
2342//
2343// I am a comment
2344thing = 'c'
2345
2346foo = 'bar' //
2347"#
2348        );
2349    }
2350
2351    #[test]
2352    fn test_recast_multiline_comment_under_variable() {
2353        let some_program_string = r#"key = 'c'
2354// this is also a comment
2355// hello
2356thing = 'foo'
2357"#;
2358        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2359
2360        let recasted = program.recast_top(&Default::default(), 0);
2361        assert_eq!(
2362            recasted,
2363            r#"key = 'c'
2364// this is also a comment
2365// hello
2366thing = 'foo'
2367"#
2368        );
2369    }
2370
2371    #[test]
2372    fn test_recast_only_line_comments() {
2373        let code = r#"// comment at start
2374"#;
2375        let program = crate::parsing::top_level_parse(code).unwrap();
2376
2377        assert_eq!(program.recast_top(&Default::default(), 0), code);
2378    }
2379
2380    #[test]
2381    fn test_recast_comment_at_start() {
2382        let test_program = r#"
2383/* comment at start */
2384
2385mySk1 = startSketchOn(XY)
2386  |> startProfile(at = [0, 0])"#;
2387        let program = crate::parsing::top_level_parse(test_program).unwrap();
2388
2389        let recasted = program.recast_top(&Default::default(), 0);
2390        assert_eq!(
2391            recasted,
2392            r#"/* comment at start */
2393
2394mySk1 = startSketchOn(XY)
2395  |> startProfile(at = [0, 0])
2396"#
2397        );
2398    }
2399
2400    #[test]
2401    fn test_recast_lots_of_comments() {
2402        let some_program_string = r#"// comment at start
2403mySk1 = startSketchOn(XY)
2404  |> startProfile(at = [0, 0])
2405  |> line(endAbsolute = [1, 1])
2406  // comment here
2407  |> line(endAbsolute = [0, 1], tag = $myTag)
2408  |> line(endAbsolute = [1, 1])
2409  /* and
2410  here
2411  */
2412  // a comment between pipe expression statements
2413  |> rx(90)
2414  // and another with just white space between others below
2415  |> ry(45)
2416  |> rx(45)
2417// one more for good measure"#;
2418        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2419
2420        let recasted = program.recast_top(&Default::default(), 0);
2421        assert_eq!(
2422            recasted,
2423            r#"// comment at start
2424mySk1 = startSketchOn(XY)
2425  |> startProfile(at = [0, 0])
2426  |> line(endAbsolute = [1, 1])
2427  // comment here
2428  |> line(endAbsolute = [0, 1], tag = $myTag)
2429  |> line(endAbsolute = [1, 1])
2430  /* and
2431  here */
2432  // a comment between pipe expression statements
2433  |> rx(90)
2434  // and another with just white space between others below
2435  |> ry(45)
2436  |> rx(45)
2437// one more for good measure
2438"#
2439        );
2440    }
2441
2442    #[test]
2443    fn test_recast_multiline_object() {
2444        let some_program_string = r#"x = {
2445  a = 1000000000,
2446  b = 2000000000,
2447  c = 3000000000,
2448  d = 4000000000,
2449  e = 5000000000
2450}"#;
2451        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2452
2453        let recasted = program.recast_top(&Default::default(), 0);
2454        assert_eq!(recasted.trim(), some_program_string);
2455    }
2456
2457    #[test]
2458    fn test_recast_first_level_object() {
2459        let some_program_string = r#"three = 3
2460
2461yo = {
2462  aStr = 'str',
2463  anum = 2,
2464  identifier = three,
2465  binExp = 4 + 5
2466}
2467yo = [
2468  1,
2469  "  2,",
2470  "three",
2471  4 + 5,
2472  "  hey oooooo really long long long"
2473]
2474"#;
2475        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2476
2477        let recasted = program.recast_top(&Default::default(), 0);
2478        assert_eq!(recasted, some_program_string);
2479    }
2480
2481    #[test]
2482    fn test_recast_new_line_before_comment() {
2483        let some_program_string = r#"
2484// this is a comment
2485yo = { a = { b = { c = '123' } } }
2486
2487key = 'c'
2488things = "things"
2489
2490// this is also a comment"#;
2491        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2492
2493        let recasted = program.recast_top(&Default::default(), 0);
2494        let expected = some_program_string.trim();
2495        // Currently new parser removes an empty line
2496        let actual = recasted.trim();
2497        assert_eq!(actual, expected);
2498    }
2499
2500    #[test]
2501    fn test_recast_comment_tokens_inside_strings() {
2502        let some_program_string = r#"b = {
2503  end = 141,
2504  start = 125,
2505  type_ = "NonCodeNode",
2506  value = "
2507 // a comment
2508   "
2509}"#;
2510        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2511
2512        let recasted = program.recast_top(&Default::default(), 0);
2513        assert_eq!(recasted.trim(), some_program_string.trim());
2514    }
2515
2516    #[test]
2517    fn test_recast_array_new_line_in_pipe() {
2518        let some_program_string = r#"myVar = 3
2519myVar2 = 5
2520myVar3 = 6
2521myAng = 40
2522myAng2 = 134
2523part001 = startSketchOn(XY)
2524  |> startProfile(at = [0, 0])
2525  |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
2526  |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2527  |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
2528        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2529
2530        let recasted = program.recast_top(&Default::default(), 0);
2531        assert_eq!(recasted.trim(), some_program_string);
2532    }
2533
2534    #[test]
2535    fn test_recast_array_new_line_in_pipe_custom() {
2536        let some_program_string = r#"myVar = 3
2537myVar2 = 5
2538myVar3 = 6
2539myAng = 40
2540myAng2 = 134
2541part001 = startSketchOn(XY)
2542   |> startProfile(at = [0, 0])
2543   |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
2544   |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2545   |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
2546"#;
2547        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2548
2549        let recasted = program.recast_top(
2550            &FormatOptions {
2551                tab_size: 3,
2552                use_tabs: false,
2553                insert_final_newline: true,
2554            },
2555            0,
2556        );
2557        assert_eq!(recasted, some_program_string);
2558    }
2559
2560    #[test]
2561    fn test_recast_after_rename_std() {
2562        let some_program_string = r#"part001 = startSketchOn(XY)
2563  |> startProfile(at = [0.0000000000, 5.0000000000])
2564    |> line(end = [0.4900857016, -0.0240763666])
2565
2566part002 = "part002"
2567things = [part001, 0.0]
2568blah = 1
2569foo = false
2570baz = {a: 1, part001: "thing"}
2571
2572fn ghi(part001) {
2573  return part001
2574}
2575"#;
2576        let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2577        program.rename_symbol("mySuperCoolPart", 6);
2578
2579        let recasted = program.recast_top(&Default::default(), 0);
2580        assert_eq!(
2581            recasted,
2582            r#"mySuperCoolPart = startSketchOn(XY)
2583  |> startProfile(at = [0.0, 5.0])
2584  |> line(end = [0.4900857016, -0.0240763666])
2585
2586part002 = "part002"
2587things = [mySuperCoolPart, 0.0]
2588blah = 1
2589foo = false
2590baz = { a = 1, part001 = "thing" }
2591
2592fn ghi(part001) {
2593  return part001
2594}
2595"#
2596        );
2597    }
2598
2599    #[test]
2600    fn test_recast_after_rename_fn_args() {
2601        let some_program_string = r#"fn ghi(x, y, z) {
2602  return x
2603}"#;
2604        let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2605        program.rename_symbol("newName", 7);
2606
2607        let recasted = program.recast_top(&Default::default(), 0);
2608        assert_eq!(
2609            recasted,
2610            r#"fn ghi(newName, y, z) {
2611  return newName
2612}
2613"#
2614        );
2615    }
2616
2617    #[test]
2618    fn test_recast_trailing_comma() {
2619        let some_program_string = r#"startSketchOn(XY)
2620  |> startProfile(at = [0, 0])
2621  |> arc({
2622    radius = 1,
2623    angle_start = 0,
2624    angle_end = 180,
2625  })"#;
2626        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2627
2628        let recasted = program.recast_top(&Default::default(), 0);
2629        assert_eq!(
2630            recasted,
2631            r#"startSketchOn(XY)
2632  |> startProfile(at = [0, 0])
2633  |> arc({
2634       radius = 1,
2635       angle_start = 0,
2636       angle_end = 180
2637     })
2638"#
2639        );
2640    }
2641
2642    #[test]
2643    fn test_recast_array_no_trailing_comma_with_comments() {
2644        let some_program_string = r#"[
2645  1, // one
2646  2, // two
2647  3  // three
2648]"#;
2649        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2650
2651        let recasted = program.recast_top(&Default::default(), 0);
2652        assert_eq!(
2653            recasted,
2654            r#"[
2655  1,
2656  // one
2657  2,
2658  // two
2659  3,
2660  // three
2661]
2662"#
2663        );
2664    }
2665
2666    #[test]
2667    fn test_recast_object_no_trailing_comma_with_comments() {
2668        let some_program_string = r#"{
2669  x=1, // one
2670  y=2, // two
2671  z=3  // three
2672}"#;
2673        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2674
2675        let recasted = program.recast_top(&Default::default(), 0);
2676        // TODO: We should probably not add an extra new line after the last
2677        // comment.
2678        assert_eq!(
2679            recasted,
2680            r#"{
2681  x = 1,
2682  // one
2683  y = 2,
2684  // two
2685  z = 3,
2686  // three
2687
2688}
2689"#
2690        );
2691    }
2692
2693    #[test]
2694    fn test_recast_negative_var() {
2695        let some_program_string = r#"w = 20
2696l = 8
2697h = 10
2698
2699firstExtrude = startSketchOn(XY)
2700  |> startProfile(at = [0,0])
2701  |> line(end = [0, l])
2702  |> line(end = [w, 0])
2703  |> line(end = [0, -l])
2704  |> close()
2705  |> extrude(h)
2706"#;
2707        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2708
2709        let recasted = program.recast_top(&Default::default(), 0);
2710        assert_eq!(
2711            recasted,
2712            r#"w = 20
2713l = 8
2714h = 10
2715
2716firstExtrude = startSketchOn(XY)
2717  |> startProfile(at = [0, 0])
2718  |> line(end = [0, l])
2719  |> line(end = [w, 0])
2720  |> line(end = [0, -l])
2721  |> close()
2722  |> extrude(h)
2723"#
2724        );
2725    }
2726
2727    #[test]
2728    fn test_recast_multiline_comment() {
2729        let some_program_string = r#"w = 20
2730l = 8
2731h = 10
2732
2733// This is my comment
2734// It has multiple lines
2735// And it's really long
2736firstExtrude = startSketchOn(XY)
2737  |> startProfile(at = [0,0])
2738  |> line(end = [0, l])
2739  |> line(end = [w, 0])
2740  |> line(end = [0, -l])
2741  |> close()
2742  |> extrude(h)
2743"#;
2744        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2745
2746        let recasted = program.recast_top(&Default::default(), 0);
2747        assert_eq!(
2748            recasted,
2749            r#"w = 20
2750l = 8
2751h = 10
2752
2753// This is my comment
2754// It has multiple lines
2755// And it's really long
2756firstExtrude = startSketchOn(XY)
2757  |> startProfile(at = [0, 0])
2758  |> line(end = [0, l])
2759  |> line(end = [w, 0])
2760  |> line(end = [0, -l])
2761  |> close()
2762  |> extrude(h)
2763"#
2764        );
2765    }
2766
2767    #[test]
2768    fn test_recast_math_start_negative() {
2769        let some_program_string = r#"myVar = -5 + 6"#;
2770        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2771
2772        let recasted = program.recast_top(&Default::default(), 0);
2773        assert_eq!(recasted.trim(), some_program_string);
2774    }
2775
2776    #[test]
2777    fn test_recast_math_negate_parens() {
2778        let some_program_string = r#"wallMountL = 3.82
2779thickness = 0.5
2780
2781startSketchOn(XY)
2782  |> startProfile(at = [0, 0])
2783  |> line(end = [0, -(wallMountL - thickness)])
2784  |> line(end = [0, -(5 - thickness)])
2785  |> line(end = [0, -(5 - 1)])
2786  |> line(end = [0, -(-5 - 1)])"#;
2787        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2788
2789        let recasted = program.recast_top(&Default::default(), 0);
2790        assert_eq!(recasted.trim(), some_program_string);
2791    }
2792
2793    #[test]
2794    fn test_recast_math_nested_parens() {
2795        let some_program_string = r#"distance = 5
2796p = 3: Plane
2797FOS = { a = 3, b = 42 }: Sketch
2798sigmaAllow = 8: number(mm)
2799width = 20
2800thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
2801        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2802
2803        let recasted = program.recast_top(&Default::default(), 0);
2804        assert_eq!(recasted.trim(), some_program_string);
2805    }
2806
2807    #[test]
2808    fn no_vardec_keyword() {
2809        let some_program_string = r#"distance = 5"#;
2810        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2811
2812        let recasted = program.recast_top(&Default::default(), 0);
2813        assert_eq!(recasted.trim(), some_program_string);
2814    }
2815
2816    #[test]
2817    fn recast_types() {
2818        let some_program_string = r#"type foo
2819
2820// A comment
2821@(impl = primitive)
2822export type bar(unit, baz)
2823type baz = Foo | Bar
2824type UnionOfArrays = [Foo] | [Bar] | Foo | { a: T, b: Foo | Bar | [Baz] }
2825"#;
2826        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2827        let recasted = program.recast_top(&Default::default(), 0);
2828        assert_eq!(recasted, some_program_string);
2829    }
2830
2831    #[test]
2832    fn recast_nested_fn() {
2833        let some_program_string = r#"fn f() {
2834  return fn() {
2835  return 1
2836}
2837}"#;
2838        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2839        let recasted = program.recast_top(&Default::default(), 0);
2840        let expected = "\
2841fn f() {
2842  return fn() {
2843    return 1
2844  }
2845}";
2846        assert_eq!(recasted.trim(), expected);
2847    }
2848
2849    #[test]
2850    fn recast_literal() {
2851        use winnow::Parser;
2852        for (i, (raw, expected, reason)) in [
2853            (
2854                "5.0",
2855                "5.0",
2856                "fractional numbers should stay fractional, i.e. don't reformat this to '5'",
2857            ),
2858            (
2859                "5",
2860                "5",
2861                "integers should stay integral, i.e. don't reformat this to '5.0'",
2862            ),
2863            (
2864                "5.0000000",
2865                "5.0",
2866                "if the number is f64 but not fractional, use its canonical format",
2867            ),
2868            ("5.1", "5.1", "straightforward case works"),
2869        ]
2870        .into_iter()
2871        .enumerate()
2872        {
2873            let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
2874            let literal = crate::parsing::parser::unsigned_number_literal
2875                .parse(tokens.as_slice())
2876                .unwrap();
2877            let mut actual = String::new();
2878            literal.recast(&mut actual);
2879            assert_eq!(actual, expected, "failed test {i}, which is testing that {reason}");
2880        }
2881    }
2882
2883    #[test]
2884    fn recast_objects_no_comments() {
2885        let input = r#"
2886sketch002 = startSketchOn({
2887       plane: {
2888    origin: { x = 1, y = 2, z = 3 },
2889    x_axis = { x = 4, y = 5, z = 6 },
2890    y_axis = { x = 7, y = 8, z = 9 },
2891    z_axis = { x = 10, y = 11, z = 12 }
2892       }
2893  })
2894"#;
2895        let expected = r#"sketch002 = startSketchOn({
2896  plane = {
2897    origin = { x = 1, y = 2, z = 3 },
2898    x_axis = { x = 4, y = 5, z = 6 },
2899    y_axis = { x = 7, y = 8, z = 9 },
2900    z_axis = { x = 10, y = 11, z = 12 }
2901  }
2902})
2903"#;
2904        let ast = crate::parsing::top_level_parse(input).unwrap();
2905        let actual = ast.recast_top(&FormatOptions::new(), 0);
2906        assert_eq!(actual, expected);
2907    }
2908
2909    #[test]
2910    fn unparse_fn_unnamed() {
2911        let input = "\
2912squares_out = reduce(
2913  arr,
2914  n = 0: number,
2915  f = fn(@i, accum) {
2916    return 1
2917  },
2918)
2919";
2920        let ast = crate::parsing::top_level_parse(input).unwrap();
2921        let actual = ast.recast_top(&FormatOptions::new(), 0);
2922        assert_eq!(actual, input);
2923    }
2924
2925    #[test]
2926    fn unparse_fn_named() {
2927        let input = r#"fn f(x) {
2928  return 1
2929}
2930"#;
2931        let ast = crate::parsing::top_level_parse(input).unwrap();
2932        let actual = ast.recast_top(&FormatOptions::new(), 0);
2933        assert_eq!(actual, input);
2934    }
2935
2936    #[test]
2937    fn unparse_call_inside_function_single_line() {
2938        let input = r#"fn foo() {
2939  toDegrees(atan(0.5), foo = 1)
2940  return 0
2941}
2942"#;
2943        let ast = crate::parsing::top_level_parse(input).unwrap();
2944        let actual = ast.recast_top(&FormatOptions::new(), 0);
2945        assert_eq!(actual, input);
2946    }
2947
2948    #[test]
2949    fn recast_function_types() {
2950        let input = r#"foo = x: fn
2951foo = x: fn(number)
2952fn foo(x: fn(): number): fn {
2953  return 0
2954}
2955fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn {
2956  return 0
2957}
2958type fn
2959type foo = fn
2960type foo = fn(a: string, b: { f: fn(): any })
2961type foo = fn([fn])
2962type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
2963"#;
2964        let ast = crate::parsing::top_level_parse(input).unwrap();
2965        let actual = ast.recast_top(&FormatOptions::new(), 0);
2966        assert_eq!(actual, input);
2967    }
2968
2969    #[test]
2970    fn unparse_call_inside_function_args_multiple_lines() {
2971        let input = r#"fn foo() {
2972  toDegrees(
2973    atan(0.5),
2974    foo = 1,
2975    bar = 2,
2976    baz = 3,
2977    qux = 4,
2978  )
2979  return 0
2980}
2981"#;
2982        let ast = crate::parsing::top_level_parse(input).unwrap();
2983        let actual = ast.recast_top(&FormatOptions::new(), 0);
2984        assert_eq!(actual, input);
2985    }
2986
2987    #[test]
2988    fn unparse_call_inside_function_single_arg_multiple_lines() {
2989        let input = r#"fn foo() {
2990  toDegrees(
2991    [
2992      profile0,
2993      profile1,
2994      profile2,
2995      profile3,
2996      profile4,
2997      profile5
2998    ],
2999    key = 1,
3000  )
3001  return 0
3002}
3003"#;
3004        let ast = crate::parsing::top_level_parse(input).unwrap();
3005        let actual = ast.recast_top(&FormatOptions::new(), 0);
3006        assert_eq!(actual, input);
3007    }
3008
3009    #[test]
3010    fn recast_objects_with_comments() {
3011        use winnow::Parser;
3012        for (i, (input, expected, reason)) in [(
3013            "\
3014{
3015  a = 1,
3016  // b = 2,
3017  c = 3
3018}",
3019            "\
3020{
3021  a = 1,
3022  // b = 2,
3023  c = 3
3024}",
3025            "preserves comments",
3026        )]
3027        .into_iter()
3028        .enumerate()
3029        {
3030            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3031            crate::parsing::parser::print_tokens(tokens.as_slice());
3032            let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
3033            let mut actual = String::new();
3034            expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3035            assert_eq!(
3036                actual, expected,
3037                "failed test {i}, which is testing that recasting {reason}"
3038            );
3039        }
3040    }
3041
3042    #[test]
3043    fn recast_array_with_comments() {
3044        use winnow::Parser;
3045        for (i, (input, expected, reason)) in [
3046            (
3047                "\
3048[
3049  1,
3050  2,
3051  3,
3052  4,
3053  5,
3054  6,
3055  7,
3056  8,
3057  9,
3058  10,
3059  11,
3060  12,
3061  13,
3062  14,
3063  15,
3064  16,
3065  17,
3066  18,
3067  19,
3068  20,
3069]",
3070                "\
3071[
3072  1,
3073  2,
3074  3,
3075  4,
3076  5,
3077  6,
3078  7,
3079  8,
3080  9,
3081  10,
3082  11,
3083  12,
3084  13,
3085  14,
3086  15,
3087  16,
3088  17,
3089  18,
3090  19,
3091  20
3092]",
3093                "preserves multi-line arrays",
3094            ),
3095            (
3096                "\
3097[
3098  1,
3099  // 2,
3100  3
3101]",
3102                "\
3103[
3104  1,
3105  // 2,
3106  3
3107]",
3108                "preserves comments",
3109            ),
3110            (
3111                "\
3112[
3113  1,
3114  2,
3115  // 3
3116]",
3117                "\
3118[
3119  1,
3120  2,
3121  // 3
3122]",
3123                "preserves comments at the end of the array",
3124            ),
3125        ]
3126        .into_iter()
3127        .enumerate()
3128        {
3129            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3130            let expr = crate::parsing::parser::array_elem_by_elem
3131                .parse(tokens.as_slice())
3132                .unwrap();
3133            let mut actual = String::new();
3134            expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3135            assert_eq!(
3136                actual, expected,
3137                "failed test {i}, which is testing that recasting {reason}"
3138            );
3139        }
3140    }
3141
3142    #[test]
3143    fn code_with_comment_and_extra_lines() {
3144        let code = r#"yo = 'c'
3145
3146/* this is
3147a
3148comment */
3149yo = 'bing'
3150"#;
3151        let ast = crate::parsing::top_level_parse(code).unwrap();
3152        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3153        assert_eq!(recasted, code);
3154    }
3155
3156    #[test]
3157    fn comments_in_a_fn_block() {
3158        let code = r#"fn myFn() {
3159  // this is a comment
3160  yo = { a = { b = { c = '123' } } }
3161
3162  /* block
3163  comment */
3164  key = 'c'
3165  // this is also a comment
3166}
3167"#;
3168        let ast = crate::parsing::top_level_parse(code).unwrap();
3169        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3170        assert_eq!(recasted, code);
3171    }
3172
3173    #[test]
3174    fn array_range_end_exclusive() {
3175        let code = "myArray = [0..<4]\n";
3176        let ast = crate::parsing::top_level_parse(code).unwrap();
3177        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3178        assert_eq!(recasted, code);
3179    }
3180
3181    #[test]
3182    fn paren_precedence() {
3183        let code = r#"x = 1 - 2 - 3
3184x = (1 - 2) - 3
3185x = 1 - (2 - 3)
3186x = 1 + 2 + 3
3187x = (1 + 2) + 3
3188x = 1 + (2 + 3)
3189x = 2 * (y % 2)
3190x = (2 * y) % 2
3191x = 2 % (y * 2)
3192x = (2 % y) * 2
3193x = 2 * y % 2
3194"#;
3195
3196        let expected = r#"x = 1 - 2 - 3
3197x = 1 - 2 - 3
3198x = 1 - (2 - 3)
3199x = 1 + 2 + 3
3200x = 1 + 2 + 3
3201x = 1 + 2 + 3
3202x = 2 * (y % 2)
3203x = 2 * y % 2
3204x = 2 % (y * 2)
3205x = 2 % y * 2
3206x = 2 * y % 2
3207"#;
3208        let ast = crate::parsing::top_level_parse(code).unwrap();
3209        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3210        assert_eq!(recasted, expected);
3211    }
3212
3213    #[test]
3214    fn gap_between_body_item_and_documented_fn() {
3215        let code = "\
3216x = 360
3217
3218// Watermelon
3219fn myFn() {
3220}
3221";
3222        let ast = crate::parsing::top_level_parse(code).unwrap();
3223        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3224        let expected = code;
3225        assert_eq!(recasted, expected);
3226    }
3227
3228    #[test]
3229    fn simple_assignment_in_fn() {
3230        let code = "\
3231fn function001() {
3232  extrude002 = extrude()
3233}\n";
3234
3235        let ast = crate::parsing::top_level_parse(code).unwrap();
3236        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3237        let expected = code;
3238        assert_eq!(recasted, expected);
3239    }
3240
3241    #[test]
3242    fn no_weird_extra_lines() {
3243        // Regression test, this used to insert a lot of new lines
3244        // between the initial comment and the @settings.
3245        let code = "\
3246// Initial comment
3247
3248@settings(defaultLengthUnit = mm)
3249
3250x = 1
3251";
3252        let ast = crate::parsing::top_level_parse(code).unwrap();
3253        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3254        let expected = code;
3255        assert_eq!(recasted, expected);
3256    }
3257
3258    #[test]
3259    fn module_prefix() {
3260        let code = "x = std::sweep::SKETCH_PLANE\n";
3261        let ast = crate::parsing::top_level_parse(code).unwrap();
3262        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3263        let expected = code;
3264        assert_eq!(recasted, expected);
3265    }
3266}