Skip to main content

draxl_lower_rust/
lib.rs

1#![forbid(unsafe_code)]
2//! Lowering from the current Draxl Rust profile to ordinary Rust source.
3//!
4//! Lowering assumes the input already passed structural validation. The output
5//! strips Draxl metadata and preserves only the modeled Rust subset.
6
7use draxl_ast::{
8    BinaryOp, Block, CommentNode, DocNode, Expr, File, Item, ItemFn, Literal, Path, Pattern, Stmt,
9    Type, UnaryOp, UseTree,
10};
11use draxl_printer::canonicalize_file;
12
13/// Lowers a validated Draxl file to ordinary Rust.
14pub fn lower_file(file: &File) -> String {
15    let canonical = canonicalize_file(file);
16    let mut out = String::new();
17    write_item_list(&canonical.items, &mut out, 0);
18    out.push('\n');
19    out
20}
21
22fn write_item_list(items: &[Item], out: &mut String, indent: usize) {
23    for (index, item) in items.iter().enumerate() {
24        if index > 0 && needs_item_gap(items, index - 1, index) {
25            out.push('\n');
26        }
27        write_item(item, out, indent);
28    }
29}
30
31fn write_item(item: &Item, out: &mut String, indent: usize) {
32    match item {
33        Item::Mod(node) => {
34            indent_line(out, indent);
35            out.push_str("mod ");
36            out.push_str(&node.name);
37            out.push_str(" {");
38            if node.items.is_empty() {
39                out.push_str("}\n");
40                return;
41            }
42            out.push('\n');
43            write_item_list(&node.items, out, indent + 1);
44            indent_line(out, indent);
45            out.push_str("}\n");
46        }
47        Item::Use(node) => {
48            indent_line(out, indent);
49            out.push_str("use ");
50            out.push_str(&format_use_tree(&node.tree));
51            out.push_str(";\n");
52        }
53        Item::Struct(node) => {
54            indent_line(out, indent);
55            out.push_str("struct ");
56            out.push_str(&node.name);
57            out.push_str(" {\n");
58            for field in &node.fields {
59                indent_line(out, indent + 1);
60                out.push_str(&field.name);
61                out.push_str(": ");
62                out.push_str(&format_type(&field.ty));
63                out.push_str(",\n");
64            }
65            indent_line(out, indent);
66            out.push_str("}\n");
67        }
68        Item::Enum(node) => {
69            indent_line(out, indent);
70            out.push_str("enum ");
71            out.push_str(&node.name);
72            out.push_str(" {\n");
73            for variant in &node.variants {
74                indent_line(out, indent + 1);
75                out.push_str(&variant.name);
76                out.push_str(",\n");
77            }
78            indent_line(out, indent);
79            out.push_str("}\n");
80        }
81        Item::Fn(node) => write_fn(node, out, indent),
82        Item::Doc(node) => write_doc(node, out, indent),
83        Item::Comment(node) => write_comment(node, out, indent),
84    }
85}
86
87fn write_fn(node: &ItemFn, out: &mut String, indent: usize) {
88    indent_line(out, indent);
89    out.push_str("fn ");
90    out.push_str(&node.name);
91    out.push('(');
92    for (index, param) in node.params.iter().enumerate() {
93        if index > 0 {
94            out.push_str(", ");
95        }
96        out.push_str(&param.name);
97        out.push_str(": ");
98        out.push_str(&format_type(&param.ty));
99    }
100    out.push(')');
101    if let Some(ret_ty) = &node.ret_ty {
102        out.push_str(" -> ");
103        out.push_str(&format_type(ret_ty));
104    }
105    out.push_str(" {\n");
106    write_stmt_list(&node.body.stmts, out, indent + 1);
107    indent_line(out, indent);
108    out.push_str("}\n");
109}
110
111fn write_stmt_list(stmts: &[Stmt], out: &mut String, indent: usize) {
112    for stmt in stmts {
113        write_stmt(stmt, out, indent);
114    }
115}
116
117fn write_stmt(stmt: &Stmt, out: &mut String, indent: usize) {
118    match stmt {
119        Stmt::Let(node) => {
120            indent_line(out, indent);
121            out.push_str("let ");
122            out.push_str(&format_pattern(&node.pat));
123            out.push_str(" = ");
124            out.push_str(&format_expr(&node.value, indent));
125            out.push_str(";\n");
126        }
127        Stmt::Expr(node) => {
128            indent_line(out, indent);
129            out.push_str(&format_expr(&node.expr, indent));
130            if node.has_semi {
131                out.push(';');
132            }
133            out.push('\n');
134        }
135        Stmt::Item(item) => write_item(item, out, indent),
136        Stmt::Doc(node) => write_doc(node, out, indent),
137        Stmt::Comment(node) => write_comment(node, out, indent),
138    }
139}
140
141fn write_doc(node: &DocNode, out: &mut String, indent: usize) {
142    indent_line(out, indent);
143    out.push_str("///");
144    if !node.text.is_empty() {
145        out.push(' ');
146        out.push_str(&node.text);
147    }
148    out.push('\n');
149}
150
151fn write_comment(node: &CommentNode, out: &mut String, indent: usize) {
152    indent_line(out, indent);
153    out.push_str("//");
154    if !node.text.is_empty() {
155        out.push(' ');
156        out.push_str(&node.text);
157    }
158    out.push('\n');
159}
160
161fn format_pattern(pattern: &Pattern) -> String {
162    match pattern {
163        Pattern::Ident(node) => node.name.clone(),
164        Pattern::Wild(_) => "_".to_owned(),
165    }
166}
167
168fn format_type(ty: &Type) -> String {
169    match ty {
170        Type::Path(node) => format_path(&node.path),
171    }
172}
173
174fn format_expr(expr: &Expr, indent: usize) -> String {
175    match expr {
176        Expr::Path(node) => format_path(&node.path),
177        Expr::Lit(node) => match &node.value {
178            Literal::Int(value) => value.to_string(),
179            Literal::Str(value) => format!("\"{}\"", escape_string(value)),
180        },
181        Expr::Group(node) => format!("({})", format_expr(&node.expr, indent)),
182        Expr::Binary(node) => format!(
183            "{} {} {}",
184            format_expr(&node.lhs, indent),
185            match node.op {
186                BinaryOp::Add => "+",
187                BinaryOp::Sub => "-",
188                BinaryOp::Lt => "<",
189            },
190            format_expr(&node.rhs, indent)
191        ),
192        Expr::Unary(node) => format!(
193            "{}{}",
194            match node.op {
195                UnaryOp::Neg => "-",
196            },
197            format_expr(&node.expr, indent)
198        ),
199        Expr::Call(node) => {
200            let mut out = format_expr(&node.callee, indent);
201            out.push('(');
202            for (index, arg) in node.args.iter().enumerate() {
203                if index > 0 {
204                    out.push_str(", ");
205                }
206                out.push_str(&format_expr(arg, indent));
207            }
208            out.push(')');
209            out
210        }
211        Expr::Match(node) => {
212            let mut out = String::new();
213            out.push_str("match ");
214            out.push_str(&format_expr(&node.scrutinee, indent));
215            out.push_str(" {\n");
216            for arm in &node.arms {
217                indent_line(&mut out, indent + 1);
218                out.push_str(&format_pattern(&arm.pat));
219                if let Some(guard) = &arm.guard {
220                    out.push_str(" if ");
221                    out.push_str(&format_expr(guard, indent + 1));
222                }
223                out.push_str(" => ");
224                out.push_str(&format_expr(&arm.body, indent + 1));
225                out.push_str(",\n");
226            }
227            indent_line(&mut out, indent);
228            out.push('}');
229            out
230        }
231        Expr::Block(Block { stmts, .. }) => {
232            let mut out = String::new();
233            out.push_str("{\n");
234            write_stmt_list(stmts, &mut out, indent + 1);
235            indent_line(&mut out, indent);
236            out.push('}');
237            out
238        }
239    }
240}
241
242fn needs_item_gap(items: &[Item], current: usize, next: usize) -> bool {
243    let current = &items[current];
244    let next = &items[next];
245    match current {
246        Item::Doc(_) | Item::Comment(_) => {
247            resolved_item_target(items, current) != resolved_item_target(items, next)
248        }
249        _ => true,
250    }
251}
252
253fn resolved_item_target<'a>(items: &'a [Item], item: &'a Item) -> Option<&'a str> {
254    if !matches!(item, Item::Doc(_) | Item::Comment(_)) {
255        return Some(item.meta().id.as_str());
256    }
257    if let Some(anchor) = item.meta().anchor.as_deref() {
258        return Some(anchor);
259    }
260    let index = items
261        .iter()
262        .position(|candidate| std::ptr::eq(candidate, item))?;
263    for candidate in &items[index + 1..] {
264        if !matches!(candidate, Item::Doc(_) | Item::Comment(_)) {
265            return Some(candidate.meta().id.as_str());
266        }
267    }
268    None
269}
270
271fn indent_line(out: &mut String, indent: usize) {
272    for _ in 0..indent {
273        out.push_str("  ");
274    }
275}
276
277fn format_path(path: &Path) -> String {
278    path.segments.join("::")
279}
280
281fn format_use_tree(tree: &UseTree) -> String {
282    match tree {
283        UseTree::Name(node) => node.name.clone(),
284        UseTree::Path(node) => format!("{}::{}", node.prefix, format_use_tree(&node.tree)),
285        UseTree::Group(node) => {
286            let mut out = String::from("{");
287            for (index, item) in node.items.iter().enumerate() {
288                if index > 0 {
289                    out.push_str(", ");
290                }
291                out.push_str(&format_use_tree(item));
292            }
293            out.push('}');
294            out
295        }
296        UseTree::Glob(_) => "*".to_owned(),
297    }
298}
299
300fn escape_string(value: &str) -> String {
301    let mut out = String::new();
302    for ch in value.chars() {
303        match ch {
304            '"' => out.push_str("\\\""),
305            '\\' => out.push_str("\\\\"),
306            '\n' => out.push_str("\\n"),
307            '\r' => out.push_str("\\r"),
308            '\t' => out.push_str("\\t"),
309            _ => out.push(ch),
310        }
311    }
312    out
313}