Skip to main content

kcl_lib/
unparser.rs

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