kcl_lib/
unparser.rs

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