Skip to main content

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