Skip to main content

abyss_core/
format.rs

1use crate::ast::{AST, AssignmentOp, Type};
2
3fn type_keyword(var_type: &Type) -> String {
4    match var_type {
5        Type::Arcana => "arcana".to_string(),
6        Type::Aether => "aether".to_string(),
7        Type::Rune => "rune".to_string(),
8        Type::Omen => "omen".to_string(),
9        Type::Abyss => "abyss".to_string(),
10        Type::Scroll => "scroll".to_string(),
11        Type::Lexicon => "lexicon".to_string(),
12        Type::Materia => "materia".to_string(),
13        Type::Glyph => "glyph".to_string(),
14        Type::Artifact(name) => name.clone(),
15    }
16}
17
18/// Formats an AST node into a readable string with appropriate indentation.
19/// This function handles various types of AST nodes, applying formatting rules based on node type.
20/// It also manages operator precedence to ensure correct placement of parentheses.
21///
22/// # Arguments
23/// * `ast` - The AST node to format.
24/// * `indent_level` - The level of indentation for the formatted output.
25///
26/// # Returns
27/// A formatted string representation of the AST node.
28pub fn format_ast(ast: &AST, indent_level: usize) -> String {
29    let indent = "    ".repeat(indent_level);
30
31    // Determines the precedence level for an AST node to handle operator precedence.
32    let precedence = |node: &AST| match node {
33        AST::LogicalOr(_, _, _) => 10,
34        AST::LogicalAnd(_, _, _) => 20,
35        AST::Equal(_, _, _) | AST::NotEqual(_, _, _) => 30,
36        AST::LessThan(_, _, _)
37        | AST::LessThanOrEqual(_, _, _)
38        | AST::GreaterThan(_, _, _)
39        | AST::GreaterThanOrEqual(_, _, _) => 40,
40        AST::Add(_, _, _) | AST::Sub(_, _, _) => 50,
41        AST::Mul(_, _, _) | AST::Div(_, _, _) | AST::Mod(_, _, _) => 60,
42        AST::PowArcana(_, _, _) | AST::PowAether(_, _, _) => 70,
43        AST::LogicalNot(_, _) => 80,
44        AST::IndexAccess { .. } | AST::FieldAccess { .. } => 90,
45        _ => 100,
46    };
47
48    let current_precedence = precedence(ast);
49
50    // Formats a sub-expression, adding parentheses if necessary based on precedence.
51    let format_with_parentheses = |expr: &AST, parent_precedence: u8| -> String {
52        let sub_precedence = precedence(expr);
53        let code = format_ast(expr, indent_level);
54
55        if sub_precedence < parent_precedence {
56            format!("({})", code)
57        } else {
58            code
59        }
60    };
61
62    match ast {
63        AST::Statement(statement, _) => {
64            format!("{}{};", indent, format_ast(statement, indent_level))
65        }
66        AST::Add(left, right, _)
67        | AST::Sub(left, right, _)
68        | AST::Mul(left, right, _)
69        | AST::Div(left, right, _)
70        | AST::Mod(left, right, _)
71        | AST::PowArcana(left, right, _)
72        | AST::PowAether(left, right, _)
73        | AST::LogicalAnd(left, right, _)
74        | AST::LogicalOr(left, right, _)
75        | AST::Equal(left, right, _)
76        | AST::NotEqual(left, right, _)
77        | AST::LessThan(left, right, _)
78        | AST::LessThanOrEqual(left, right, _)
79        | AST::GreaterThan(left, right, _)
80        | AST::GreaterThanOrEqual(left, right, _) => {
81            let operator = match ast {
82                AST::Add(_, _, _) => "+",
83                AST::Sub(_, _, _) => "-",
84                AST::Mul(_, _, _) => "*",
85                AST::Div(_, _, _) => "/",
86                AST::Mod(_, _, _) => "%",
87                AST::PowArcana(_, _, _) => "^",
88                AST::PowAether(_, _, _) => "**",
89                AST::LogicalAnd(_, _, _) => "&&",
90                AST::LogicalOr(_, _, _) => "||",
91                AST::Equal(_, _, _) => "==",
92                AST::NotEqual(_, _, _) => "!=",
93                AST::LessThan(_, _, _) => "<",
94                AST::LessThanOrEqual(_, _, _) => "<=",
95                AST::GreaterThan(_, _, _) => ">",
96                AST::GreaterThanOrEqual(_, _, _) => ">=",
97                _ => unreachable!(),
98            };
99            format!(
100                "{} {} {}",
101                format_with_parentheses(left, current_precedence),
102                operator,
103                format_with_parentheses(right, current_precedence)
104            )
105        }
106        AST::LogicalNot(expr, _) => {
107            format!("!{}", format_with_parentheses(expr, current_precedence))
108        }
109        AST::VarAssign {
110            name,
111            value,
112            var_type,
113            is_morph,
114            ..
115        } => {
116            format!(
117                "forge {}{}: {} = {}",
118                if *is_morph { "morph " } else { "" },
119                name,
120                type_keyword(var_type),
121                format_ast(value, indent_level)
122            )
123        }
124        AST::Assignment {
125            name, value, op, ..
126        } => match op {
127            AssignmentOp::Assign => format!("{} = {}", name, format_ast(value, indent_level)),
128            AssignmentOp::AddAssign => {
129                format!("{} += {}", name, format_ast(value, indent_level))
130            }
131            AssignmentOp::SubAssign => {
132                format!("{} -= {}", name, format_ast(value, indent_level))
133            }
134            AssignmentOp::MulAssign => {
135                format!("{} *= {}", name, format_ast(value, indent_level))
136            }
137            AssignmentOp::DivAssign => {
138                format!("{} /= {}", name, format_ast(value, indent_level))
139            }
140            AssignmentOp::ModAssign => {
141                format!("{} %= {}", name, format_ast(value, indent_level))
142            }
143            AssignmentOp::PowArcanaAssign => {
144                format!("{} ^= {}", name, format_ast(value, indent_level))
145            }
146            AssignmentOp::PowAetherAssign => {
147                format!("{} **= {}", name, format_ast(value, indent_level))
148            }
149        },
150        AST::Var(name, _) => name.clone(),
151        AST::FieldAccess { target, field, .. } => {
152            format!("{}.{}", format_ast(target, indent_level), field)
153        }
154        AST::Arcana(value, _) => format!("{}", value),
155        AST::Aether(value, _) => {
156            if value.fract() == 0.0 {
157                format!("{:.1}", value)
158            } else {
159                format!("{}", value)
160            }
161        }
162        AST::Rune(value, _) => format!("\"{}\"", value),
163        AST::Omen(value, _) => match value {
164            true => "boon".to_string(),
165            false => "hex".to_string(),
166        },
167        AST::Abyss(_) => "abyss".to_string(),
168        AST::Reveal(value, _) => {
169            let val = format_ast(value, indent_level);
170            let trimmed_val = val.trim();
171            match trimmed_val {
172                "abyss" => "reveal".to_string(),
173                _ => format!("reveal {}", trimmed_val),
174            }
175        }
176        AST::Block(statements, _) => {
177            let mut result = format!("{}{{\n", indent);
178            for statement in statements {
179                result.push_str(&format!("{}\n", format_ast(statement, indent_level + 1)));
180            }
181            result.push_str(&format!("{}}}", indent));
182            result
183        }
184        AST::Oracle {
185            is_match,
186            conditionals,
187            branches,
188            ..
189        } => {
190            let mut result = "oracle".to_string();
191            if !conditionals.is_empty() {
192                let conditions = conditionals
193                    .iter()
194                    .map(|cond| {
195                        if *is_match {
196                            format_ast(cond.expression.as_ref(), indent_level)
197                        } else {
198                            format!(
199                                "{} = {}",
200                                cond.variable,
201                                format_ast(cond.expression.as_ref(), indent_level)
202                            )
203                        }
204                    })
205                    .collect::<Vec<_>>()
206                    .join(", ");
207                result.push_str(&format!(" ({})", conditions));
208            }
209            result.push_str(" {\n");
210            for branch in branches {
211                if let AST::Comment(text, _) = branch {
212                    result.push_str(&format!("{}{}\n", "    ".repeat(indent_level + 1), text));
213                    continue;
214                }
215
216                if let AST::OracleBranch {
217                    pattern,
218                    guard,
219                    body,
220                    ..
221                } = branch
222                {
223                    // Top-level scroll / artifact patterns keep their natural
224                    // form (`[…] =>`, `Player { name } =>`) rather than
225                    // getting re-wrapped in `(…)`. The single-element AST
226                    // already self-formats in the right shape, so the outer
227                    // parens would be redundant noise on round-trip.
228                    let pattern_text = match pattern.as_slice() {
229                        [] => "_".to_string(),
230                        [only @ AST::OracleScrollPattern { .. }]
231                        | [only @ AST::OracleArtifactPattern { .. }]
232                        | [only @ AST::OracleLexiconPattern { .. }] => {
233                            format_ast(only, indent_level + 1)
234                        }
235                        elems => {
236                            let inner = elems
237                                .iter()
238                                .map(|pat| format_ast(pat, indent_level + 1))
239                                .collect::<Vec<_>>()
240                                .join(", ");
241                            format!("({})", inner)
242                        }
243                    };
244                    let guard_text = guard
245                        .as_ref()
246                        .map(|expr| {
247                            format!(" ward {}", format_ast(expr.as_ref(), indent_level + 1))
248                        })
249                        .unwrap_or_default();
250                    result.push_str(&format!(
251                        "{}{}{} => {}\n",
252                        "    ".repeat(indent_level + 1),
253                        pattern_text,
254                        guard_text,
255                        format_ast(body.as_ref(), indent_level + 1).trim()
256                    ));
257                }
258            }
259            result.push_str(&format!("{}}}", indent));
260            result
261        }
262        AST::OracleDontCareItem(_) => "_".to_string(),
263        AST::OracleScrollPattern { elements, .. } => {
264            let inner = elements
265                .iter()
266                .map(|elem| format_ast(elem, indent_level))
267                .collect::<Vec<_>>()
268                .join(", ");
269            format!("[{}]", inner)
270        }
271        AST::OracleScrollRest { name, .. } => match name {
272            Some(name) => format!("..{}", name),
273            None => "..".to_string(),
274        },
275        AST::OracleArtifactPattern {
276            type_name, fields, ..
277        } => {
278            if fields.is_empty() {
279                // `Type {}` matches by type alone (any artifact of this
280                // type). Mirror `ArtifactLiteral`'s empty-fields formatting
281                // rather than emitting `Type {  }` with double spaces.
282                format!("{} {{}}", type_name)
283            } else {
284                let inner = fields
285                    .iter()
286                    .map(|(name, sub)| match sub {
287                        // Shorthand `{ name }` keeps its compact form when the
288                        // sub-pattern is just `Var(name)` with the same name.
289                        AST::Var(var_name, _) if var_name == name => name.clone(),
290                        other => format!("{}: {}", name, format_ast(other, indent_level)),
291                    })
292                    .collect::<Vec<_>>()
293                    .join(", ");
294                format!("{} {{ {} }}", type_name, inner)
295            }
296        }
297        AST::OracleLexiconPattern { entries, .. } => {
298            if entries.is_empty() {
299                // `{}` matches any lexicon (the "by shape" catch-all).
300                "{}".to_string()
301            } else {
302                let inner = entries
303                    .iter()
304                    .map(|(key, sub)| format!("\"{}\": {}", key, format_ast(sub, indent_level)))
305                    .collect::<Vec<_>>()
306                    .join(", ");
307                format!("{{ {} }}", inner)
308            }
309        }
310        AST::Orbit { params, body, .. } => {
311            let mut result = "orbit".to_string();
312            if !params.is_empty() {
313                let params_str = params
314                    .iter()
315                    .map(|param| format_ast(param, indent_level))
316                    .collect::<Vec<_>>()
317                    .join(", ");
318                result.push_str(&format!(" ({})", params_str));
319            }
320            result.push_str(format_ast(body.as_ref(), indent_level).trim());
321            result
322        }
323        AST::OrbitParam {
324            name,
325            start,
326            end,
327            op,
328            ..
329        } => {
330            let start_expr = format_ast(start, 0);
331            let end_expr = format_ast(end, 0);
332            format!("{} = {}{}{}", name, start_expr, op, end_expr)
333        }
334        AST::Resume(value, _) => match value {
335            Some(idendifier) => format!("resume {}", idendifier),
336            None => "resume".to_string(),
337        },
338        AST::Eject(value, _) => match value {
339            Some(idendifier) => format!("eject {}", idendifier),
340            None => "eject".to_string(),
341        },
342        AST::Engrave {
343            name,
344            params,
345            return_type,
346            body,
347            method_target,
348            ..
349        } => {
350            let return_type_str = match return_type {
351                Type::Abyss => None,
352                _ => Some(type_keyword(return_type)),
353            };
354            let mut param_strings = Vec::new();
355            let mut iter = params.iter();
356            if let Some(target) = method_target {
357                let receiver = if target.requires_morph {
358                    "morph core"
359                } else {
360                    "core"
361                };
362                param_strings.push(receiver.to_string());
363                debug_assert!(
364                    !params.is_empty(),
365                    "Artifact method with method_target must have at least one parameter (the receiver)"
366                );
367                iter.next();
368            }
369            for param in iter {
370                param_strings.push(format_ast(param, indent_level));
371            }
372            let params_str = param_strings.join(", ");
373            let qualified_name = if let Some(target) = method_target {
374                format!("{}::{}", target.artifact, name)
375            } else {
376                name.clone()
377            };
378            match return_type_str {
379                None => format!(
380                    "engrave {}({}) {}",
381                    qualified_name,
382                    params_str,
383                    format_ast(body, indent_level)
384                ),
385                Some(ret) => format!(
386                    "engrave {}({}) -> {} {}",
387                    qualified_name,
388                    params_str,
389                    ret,
390                    format_ast(body, indent_level)
391                ),
392            }
393        }
394        AST::EngraveParam {
395            name,
396            param_type,
397            is_morph,
398            ..
399        } => {
400            let qualifier = if *is_morph { "morph " } else { "" };
401            format!("{}{}: {}", qualifier, name, type_keyword(param_type))
402        }
403        AST::FuncCall { name, args, .. } => {
404            let args_str = args
405                .iter()
406                .map(|arg| format_ast(arg, indent_level))
407                .collect::<Vec<_>>()
408                .join(", ");
409            format!("{}({})", name, args_str)
410        }
411        AST::ListLiteral { elements, .. } => {
412            let contents = elements
413                .iter()
414                .map(|elem| format_ast(elem, indent_level))
415                .collect::<Vec<_>>()
416                .join(", ");
417            format!("[{}]", contents)
418        }
419        AST::MapLiteral { entries, .. } => {
420            let contents = entries
421                .iter()
422                .map(|(key, value)| format!("\"{}\": {}", key, format_ast(value, indent_level)))
423                .collect::<Vec<_>>()
424                .join(", ");
425            format!("{{{}}}", contents)
426        }
427        AST::ArtifactLiteral {
428            type_name, fields, ..
429        } => {
430            if fields.is_empty() {
431                format!("{} {{}}", type_name)
432            } else {
433                let contents = fields
434                    .iter()
435                    .map(|(field, value)| format!("{}: {}", field, format_ast(value, indent_level)))
436                    .collect::<Vec<_>>()
437                    .join(", ");
438                format!("{} {{ {} }}", type_name, contents)
439            }
440        }
441        AST::IndexAccess { target, index, .. } => {
442            format!(
443                "{}[{}]",
444                format_ast(target, indent_level),
445                format_ast(index, indent_level)
446            )
447        }
448        AST::IndexAssignment {
449            target,
450            index,
451            value,
452            ..
453        } => format!(
454            "{}[{}] = {}",
455            format_ast(target, indent_level),
456            format_ast(index, indent_level),
457            format_ast(value, indent_level)
458        ),
459        AST::FieldAssignment {
460            target,
461            field,
462            value,
463            ..
464        } => format!(
465            "{}.{} = {}",
466            format_ast(target, indent_level),
467            field,
468            format_ast(value, indent_level)
469        ),
470        AST::MethodCall {
471            receiver,
472            method,
473            args,
474            ..
475        } => {
476            let args_str = args
477                .iter()
478                .map(|arg| format_ast(arg, indent_level))
479                .collect::<Vec<_>>()
480                .join(", ");
481            format!(
482                "{}.{}({})",
483                format_ast(receiver, indent_level),
484                method,
485                args_str
486            )
487        }
488        AST::ArtifactDef { name, fields, .. } => {
489            let mut result = format!("artifact {} {{\n", name);
490            for field in fields {
491                result.push_str(&format!(
492                    "{}{}: {};\n",
493                    "    ".repeat(indent_level + 1),
494                    field.name,
495                    type_keyword(&field.field_type)
496                ));
497            }
498            result.push_str(&format!("{}}}", indent));
499            result
500        }
501        AST::Comment(text, _) => text.clone(),
502        _ => format!("Not implemented: {:?}", ast),
503    }
504}