datex_core/fmt/
mod.rs

1use core::ops::Range;
2
3use crate::ast::expressions::{DatexExpression, VariableAccess};
4use crate::ast::type_expressions::{
5    CallableTypeExpression, TypeExpression, TypeExpressionData,
6    TypeVariantAccess,
7};
8use crate::parser::ParserOptions;
9use crate::{
10    compiler::precompiler::precompiled_ast::RichAst,
11    compiler::{CompileOptions, parse_datex_script_to_rich_ast_simple_error},
12    fmt::options::{FormattingOptions, TypeDeclarationFormatting},
13    global::operators::{BinaryOperator, ComparisonOperator, UnaryOperator},
14    libs::core::CoreLibPointerId,
15};
16use pretty::{DocAllocator, DocBuilder, RcAllocator, RcDoc};
17
18mod bracketing;
19mod formatting;
20pub mod options;
21
22pub type Format<'a> = DocBuilder<'a, RcAllocator, ()>;
23
24pub struct Formatter<'a> {
25    ast: RichAst,
26    script: &'a str,
27    options: FormattingOptions,
28    alloc: RcAllocator,
29}
30
31#[derive(Debug)]
32/// Represents a parent operation for formatting decisions.
33pub enum Operation<'a> {
34    Binary(&'a BinaryOperator),
35    Comparison(&'a ComparisonOperator),
36    Unary(&'a UnaryOperator),
37    Statements,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum Assoc {
42    Left,
43    Right,
44    None,
45}
46
47pub struct ParentContext<'a> {
48    precedence: u8,
49    associativity: Assoc,
50    operation: Operation<'a>,
51}
52
53impl<'a> Formatter<'a> {
54    pub fn new(script: &'a str, options: FormattingOptions) -> Self {
55        let ast = parse_datex_script_to_rich_ast_simple_error(
56            script,
57            &mut CompileOptions {
58                // Preserve scoping information for accurate formatting
59                parser_options: ParserOptions {
60                    preserve_scoping: true,
61                },
62                ..Default::default()
63            },
64        )
65        .expect("Failed to parse Datex script");
66        Self {
67            ast,
68            script,
69            options,
70            alloc: RcAllocator,
71        }
72    }
73
74    fn tokens_at(&self, span: &Range<usize>) -> &'a str {
75        &self.script[span.start..span.end]
76    }
77
78    pub fn render(&self) -> String {
79        self.render_expression(&self.ast.ast)
80    }
81
82    /// Renders a DatexExpression into a source code string.
83    fn render_expression(&self, expr: &DatexExpression) -> String {
84        self.format_datex_expression(expr)
85            .pretty(self.options.max_width)
86            .to_string()
87    }
88
89    /// Returns the indentation level
90    fn indent(&self) -> isize {
91        self.options.indent as isize
92    }
93
94    // Formats a DatexExpression into a DocBuilder for pretty printing.
95    fn format_datex_expression(
96        &'a self,
97        expr: &'a DatexExpression,
98    ) -> Format<'a> {
99        self.format_datex_expression_with_parent(expr, None, false)
100    }
101
102    /// Formats a DatexExpression into a DocBuilder for pretty printing.
103    fn format_datex_expression_with_parent(
104        &'a self,
105        expr: &'a DatexExpression,
106        parent_ctx: Option<ParentContext<'a>>,
107        is_left_child_of_parent: bool,
108    ) -> Format<'a> {
109        self.handle_bracketing(
110            expr,
111            self.datex_expression_to_source_code(expr),
112            parent_ctx,
113            is_left_child_of_parent,
114        )
115    }
116
117    /// Wraps a DocBuilder in parentheses with proper line breaks.
118    fn wrap_in_parens(&'a self, doc: Format<'a>) -> Format<'a> {
119        let a = &self.alloc;
120        (a.text("(") + a.line_() + doc + a.line_() + a.text(")")).group()
121    }
122
123    /// Formats a TypeExpression into a DocBuilder for pretty printing.
124    fn format_type_expression(
125        &'a self,
126        type_expr: &'a TypeExpression,
127    ) -> Format<'a> {
128        let a = &self.alloc;
129        println!("formatting type expression: {:?}", type_expr);
130        match &type_expr.data {
131            TypeExpressionData::VariantAccess(TypeVariantAccess {
132                name,
133                variant,
134                ..
135            }) => a.text(format!("{}/{}", name, variant)),
136            TypeExpressionData::Integer(ti) => a.text(ti.to_string()),
137            TypeExpressionData::Decimal(td) => a.text(td.to_string()),
138            TypeExpressionData::Boolean(b) => a.text(b.to_string()),
139            TypeExpressionData::Text(t) => a.text(format!("{:?}", t)),
140            TypeExpressionData::Endpoint(ep) => a.text(ep.to_string()),
141            TypeExpressionData::Null => a.text("null"),
142            TypeExpressionData::Unit => a.text("()"),
143
144            TypeExpressionData::Ref(inner) => {
145                a.text("&") + self.format_type_expression(inner)
146            }
147            TypeExpressionData::RefMut(inner) => {
148                a.text("&mut") + a.space() + self.format_type_expression(inner)
149            }
150            TypeExpressionData::Identifier(lit) => a.text(lit.to_string()),
151            TypeExpressionData::VariableAccess(VariableAccess {
152                name, ..
153            }) => a.text(name.clone()),
154
155            TypeExpressionData::GetReference(ptr) => {
156                if let Ok(core_lib) = CoreLibPointerId::try_from(ptr) {
157                    a.text(core_lib.to_string())
158                } else {
159                    a.text(ptr.to_string())
160                }
161            }
162
163            TypeExpressionData::TypedInteger(typed_integer) => {
164                a.text(typed_integer.to_string())
165                // TODO #625: handle variant formatting
166            }
167            TypeExpressionData::TypedDecimal(typed_decimal) => {
168                a.text(typed_decimal.to_string())
169                // TODO #626: handle variant formatting
170            }
171
172            // Lists — `[T, U, V]` or multiline depending on settings
173            TypeExpressionData::StructuralList(elements) => {
174                let docs =
175                    elements.0.iter().map(|e| self.format_type_expression(e));
176                self.wrap_collection(docs, ("[", "]"), ",")
177            }
178
179            TypeExpressionData::FixedSizeList(list) => {
180                core::todo!("#627 Undescribed by author.")
181            }
182            TypeExpressionData::SliceList(_) => {
183                core::todo!("#628 Undescribed by author.")
184            }
185
186            // Intersection: `A & B & C`
187            TypeExpressionData::Intersection(items) => {
188                self.wrap_type_collection(&items.0, "&")
189            }
190
191            // Union: `A | B | C`
192            TypeExpressionData::Union(items) => {
193                self.wrap_type_collection(&items.0, "|")
194            }
195
196            TypeExpressionData::GenericAccess(access) => {
197                core::todo!("#629 Undescribed by author.")
198            }
199
200            // Callable type, e.g. `function (x: integer, y: text) -> boolean`
201            TypeExpressionData::Callable(CallableTypeExpression {
202                kind,
203                parameter_types,
204                rest_parameter_type,
205                return_type,
206                yeet_type,
207            }) => {
208                // TODO #630: handle full signature
209                let params = parameter_types.iter().map(|(name, ty)| {
210                    a.text(name.clone().unwrap_or_else(|| "_".to_string()))
211                        + self.type_declaration_colon()
212                        + self.format_type_expression(ty)
213                });
214                let params_doc =
215                    RcDoc::intersperse(params, a.text(",") + a.space());
216                let arrow = self.operator_with_spaces(a.text("->"));
217                todo!("#631 Undescribed by author.")
218            }
219
220            TypeExpressionData::StructuralMap(items) => {
221                let pairs = items.0.iter().map(|(k, v)| {
222                    let key_doc = self.format_type_expression(k);
223                    key_doc
224                        + self.type_declaration_colon()
225                        + self.format_type_expression(v)
226                });
227                self.wrap_collection(pairs, ("{", "}"), ",")
228            }
229
230            TypeExpressionData::Recover => a.text("/*recover*/"),
231        }
232    }
233
234    /// Wraps a collection of type expressions with a specified operator.
235    fn wrap_type_collection(
236        &'a self,
237        list: &'a [TypeExpression],
238        op: &'a str,
239    ) -> Format<'a> {
240        let a = &self.alloc;
241
242        // Operator doc with configurable spacing or line breaks
243        let op_doc = if self.options.spaces_around_operators {
244            a.softline() + a.text(op) + a.softline()
245        } else {
246            a.text(op)
247        };
248
249        // Format all type expressions
250        let docs = list.iter().map(|expr| self.format_type_expression(expr));
251
252        // Combine elements with operator between
253        a.nil().append(
254            RcDoc::intersperse(docs, op_doc).group().nest(self.indent()),
255        )
256    }
257
258    /// Returns a DocBuilder for the colon in type declarations based on formatting options.
259    fn type_declaration_colon(&'a self) -> Format<'a> {
260        let a = &self.alloc;
261        match self.options.type_declaration_formatting {
262            TypeDeclarationFormatting::Compact => a.text(":"),
263            TypeDeclarationFormatting::SpaceAroundColon => {
264                a.space() + a.text(":") + a.space()
265            }
266            TypeDeclarationFormatting::SpaceAfterColon => {
267                a.text(":") + a.space()
268            }
269        }
270    }
271
272    /// Returns an operator DocBuilder with optional spaces around it.
273    fn operator_with_spaces(&'a self, text: Format<'a>) -> Format<'a> {
274        let a = &self.alloc;
275        if self.options.spaces_around_operators {
276            a.space() + text + a.space()
277        } else {
278            text
279        }
280    }
281
282    /// Wraps a collection of DocBuilders with specified brackets and separator.
283    fn wrap_collection(
284        &'a self,
285        list: impl Iterator<Item = DocBuilder<'a, RcAllocator, ()>> + 'a,
286        brackets: (&'a str, &'a str),
287        sep: &'a str,
288    ) -> DocBuilder<'a, RcAllocator, ()> {
289        let a = &self.alloc;
290        let sep_doc = a.text(sep);
291
292        // Optional spacing inside brackets
293        let padding = if self.options.spaced_collections {
294            a.line()
295        } else {
296            a.line_()
297        };
298
299        // Build joined elements
300        let separator = if self.options.space_in_collection {
301            sep_doc + a.line()
302        } else {
303            sep_doc + a.line_()
304        };
305
306        let joined = RcDoc::intersperse(list, separator).append(
307            if self.options.trailing_comma {
308                a.text(sep)
309            } else {
310                a.nil()
311            },
312        );
313
314        a.text(brackets.0)
315            .append((padding.clone() + joined).nest(self.indent()))
316            .append(padding)
317            .append(a.text(brackets.1))
318            .group()
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use crate::fmt::options::VariantFormatting;
325
326    use super::*;
327    use crate::parser::Parser;
328    use indoc::indoc;
329
330    #[test]
331    fn ensure_unchanged() {
332        let script = "const x = {a: 1000000, b: [1,2,3,4,5,\"jfdjfsjdfjfsdjfdsjf\", 42, true, {a:1,b:3}], c: 123.456}; x";
333        let ast_original = Parser::parse_with_default_options(script).unwrap();
334        let formatted = to_string(script, FormattingOptions::default());
335        let ast_new = Parser::parse_with_default_options(&formatted).unwrap();
336        assert_eq!(ast_original, ast_new);
337    }
338
339    #[test]
340    #[ignore]
341    fn demo() {
342        let expr = "const x: &mut integer/u8 | text = {a: 1000000, b: [1,2,3,4,5,\"jfdjfsjdfjfsdjfdsjf\", 42, true, {a:1,b:3}], c: 123.456}; x";
343        print(expr, FormattingOptions::default());
344        print(expr, FormattingOptions::compact());
345
346        let expr = "const x = [1,2,3,4,5,6,7]";
347        print(expr, FormattingOptions::default());
348    }
349
350    #[test]
351    fn variant_formatting() {
352        let expr = "42u8";
353        assert_eq!(
354            to_string(
355                expr,
356                FormattingOptions {
357                    variant_formatting: VariantFormatting::WithoutSuffix,
358                    ..Default::default()
359                }
360            ),
361            "42"
362        );
363        assert_eq!(
364            to_string(
365                expr,
366                FormattingOptions {
367                    variant_formatting: VariantFormatting::WithSuffix,
368                    ..Default::default()
369                }
370            ),
371            "42u8"
372        );
373        assert_eq!(
374            to_string(
375                expr,
376                FormattingOptions {
377                    variant_formatting: VariantFormatting::KeepAll,
378                    ..Default::default()
379                }
380            ),
381            "42u8"
382        );
383    }
384
385    #[test]
386    fn statements() {
387        let expr = "1 + 2; var x: integer/u8 = 42; x * 10;";
388        assert_eq!(
389            to_string(expr, FormattingOptions::default()),
390            indoc! {"
391            1 + 2;
392            var x: integer/u8 = 42;
393            x * 10;"
394            }
395        );
396        assert_eq!(
397            to_string(expr, FormattingOptions::compact()),
398            "1+2;var x:integer/u8=42;x*10;"
399        );
400    }
401
402    #[test]
403    fn type_declarations() {
404        let expr = "type<&mut integer/u8>";
405        assert_eq!(
406            to_string(expr, FormattingOptions::default()),
407            "type<&mut integer/u8>"
408        );
409
410        let expr = "type<text | integer/u16 | decimal/f32>";
411        assert_eq!(
412            to_string(expr, FormattingOptions::default()),
413            "type<text | integer/u16 | decimal/f32>"
414        );
415        assert_eq!(
416            to_string(expr, FormattingOptions::compact()),
417            "type<text|integer/u16|decimal/f32>"
418        );
419    }
420
421    #[test]
422    fn variable_declaration() {
423        let expr = "var x: &mut integer/u8 = 42;";
424        assert_eq!(
425            to_string(expr, FormattingOptions::default()),
426            "var x: &mut integer/u8 = 42;"
427        );
428
429        assert_eq!(
430            to_string(expr, FormattingOptions::compact()),
431            "var x:&mut integer/u8=42;"
432        );
433    }
434
435    #[test]
436    fn binary_operations() {
437        let expr = "1 + 2 * 3 - 4 / 5";
438        assert_eq!(
439            to_string(expr, FormattingOptions::default()),
440            "1 + 2 * 3 - 4 / 5"
441        );
442        assert_eq!(to_string(expr, FormattingOptions::compact()), "1+2*3-4/5");
443    }
444
445    #[test]
446    fn text() {
447        let expr = r#""Hello, \"World\"!""#;
448        assert_eq!(
449            to_string(expr, FormattingOptions::default()),
450            r#""Hello, \"World\"!""#
451        );
452    }
453
454    #[test]
455    fn lists() {
456        // simple list
457        let expr = "[1,2,3,4,5,6,7]";
458        assert_eq!(
459            to_string(
460                expr,
461                FormattingOptions {
462                    max_width: 40,
463                    space_in_collection: false,
464                    trailing_comma: false,
465                    spaced_collections: false,
466                    ..Default::default()
467                }
468            ),
469            "[1,2,3,4,5,6,7]"
470        );
471
472        // spaced list
473        assert_eq!(
474            to_string(
475                expr,
476                FormattingOptions {
477                    max_width: 40,
478                    space_in_collection: true,
479                    trailing_comma: false,
480                    spaced_collections: false,
481                    ..Default::default()
482                }
483            ),
484            "[1, 2, 3, 4, 5, 6, 7]"
485        );
486
487        // spaced list with trailing comma
488        assert_eq!(
489            to_string(
490                expr,
491                FormattingOptions {
492                    max_width: 40,
493                    space_in_collection: true,
494                    trailing_comma: true,
495                    spaced_collections: true,
496                    ..Default::default()
497                }
498            ),
499            "[ 1, 2, 3, 4, 5, 6, 7, ]"
500        );
501
502        // wrapped list
503        assert_eq!(
504            to_string(
505                expr,
506                FormattingOptions {
507                    indent: 4,
508                    max_width: 10,
509                    space_in_collection: true,
510                    trailing_comma: true,
511                    spaced_collections: true,
512                    ..Default::default()
513                }
514            ),
515            indoc! {"
516            [
517                1,
518                2,
519                3,
520                4,
521                5,
522                6,
523                7,
524            ]"}
525        );
526    }
527
528    fn to_string(script: &str, options: FormattingOptions) -> String {
529        let formatter = Formatter::new(script, options);
530        formatter.render()
531    }
532
533    fn print(script: &str, options: FormattingOptions) {
534        println!("{}", to_string(script, options));
535    }
536}