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