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