Skip to main content

draxl_rust/import/
mod.rs

1//! Importing ordinary Rust source into the Draxl AST.
2
3use draxl_ast as ast;
4use std::collections::BTreeMap;
5use std::error::Error as StdError;
6use std::fmt;
7use syn::spanned::Spanned;
8
9/// Imports ordinary Rust source into the Draxl AST.
10pub fn import_source(source: &str) -> Result<ast::File, ImportError> {
11    let parsed = syn::parse_file(source).map_err(ImportError::RustParse)?;
12    Importer::default().import_file(&parsed)
13}
14
15/// Import failure while converting ordinary Rust into Draxl.
16#[derive(Debug)]
17pub enum ImportError {
18    /// The Rust source could not be parsed by the import frontend.
19    RustParse(syn::Error),
20    /// The Rust source used syntax outside the currently supported import subset.
21    Unsupported(syn::Error),
22}
23
24impl fmt::Display for ImportError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::RustParse(error) => write!(f, "Rust parse failed: {error}"),
28            Self::Unsupported(error) => write!(f, "unsupported Rust syntax: {error}"),
29        }
30    }
31}
32
33impl StdError for ImportError {
34    fn source(&self) -> Option<&(dyn StdError + 'static)> {
35        match self {
36            Self::RustParse(error) => Some(error),
37            Self::Unsupported(error) => Some(error),
38        }
39    }
40}
41
42#[derive(Debug, Default)]
43struct Importer {
44    ids: IdAllocator,
45}
46
47#[derive(Debug, Default)]
48struct IdAllocator {
49    counts: BTreeMap<&'static str, usize>,
50}
51
52impl IdAllocator {
53    fn next(&mut self, prefix: &'static str) -> String {
54        let next = self.counts.entry(prefix).or_insert(0);
55        *next += 1;
56        format!("{prefix}{next:04}")
57    }
58}
59
60#[derive(Debug)]
61struct Placement {
62    slot: Option<&'static str>,
63    rank: Option<String>,
64}
65
66impl Placement {
67    fn file_item() -> Self {
68        Self {
69            slot: Some("file_items"),
70            rank: None,
71        }
72    }
73
74    fn ranked(slot: &'static str, index: usize) -> Self {
75        Self {
76            slot: Some(slot),
77            rank: Some(format!("r{:04}", index + 1)),
78        }
79    }
80}
81
82impl Importer {
83    fn import_file(mut self, file: &syn::File) -> Result<ast::File, ImportError> {
84        if file.shebang.is_some() {
85            return Err(unsupported(
86                file,
87                "shebang lines are unsupported in Rust import",
88            ));
89        }
90        self.check_attrs(&file.attrs, "file")?;
91        let items = file
92            .items
93            .iter()
94            .map(|item| self.import_item(item, Placement::file_item()))
95            .collect::<Result<Vec<_>, _>>()?;
96        Ok(ast::File { items })
97    }
98
99    fn import_item(
100        &mut self,
101        item: &syn::Item,
102        placement: Placement,
103    ) -> Result<ast::Item, ImportError> {
104        match item {
105            syn::Item::Mod(node) => self.import_mod(node, placement),
106            syn::Item::Use(node) => self.import_use(node, placement),
107            syn::Item::Struct(node) => self.import_struct(node, placement),
108            syn::Item::Enum(node) => self.import_enum(node, placement),
109            syn::Item::Fn(node) => self.import_fn(node, placement),
110            syn::Item::Macro(node) => Err(unsupported(node, "macro items are unsupported")),
111            _ => Err(unsupported(
112                item,
113                "only `mod`, `use`, `struct`, `enum`, and `fn` items are supported",
114            )),
115        }
116    }
117
118    fn import_mod(
119        &mut self,
120        node: &syn::ItemMod,
121        placement: Placement,
122    ) -> Result<ast::Item, ImportError> {
123        self.check_attrs(&node.attrs, "module")?;
124        self.ensure_inherited_visibility(&node.vis, "module visibility modifiers are unsupported")?;
125        if node.unsafety.is_some() {
126            return Err(unsupported(node, "`unsafe mod` is unsupported"));
127        }
128        let Some((_, items)) = &node.content else {
129            return Err(unsupported(
130                node,
131                "out-of-line `mod foo;` declarations are unsupported",
132            ));
133        };
134        let items = items
135            .iter()
136            .enumerate()
137            .map(|(index, item)| self.import_item(item, Placement::ranked("items", index)))
138            .collect::<Result<Vec<_>, _>>()?;
139        Ok(ast::Item::Mod(ast::ItemMod {
140            meta: self.meta("m", placement),
141            name: node.ident.to_string(),
142            items,
143        }))
144    }
145
146    fn import_use(
147        &mut self,
148        node: &syn::ItemUse,
149        placement: Placement,
150    ) -> Result<ast::Item, ImportError> {
151        self.check_attrs(&node.attrs, "use item")?;
152        self.ensure_inherited_visibility(
153            &node.vis,
154            "use item visibility modifiers are unsupported",
155        )?;
156        if node.leading_colon.is_some() {
157            return Err(unsupported(
158                node,
159                "leading `::` in `use` items is unsupported",
160            ));
161        }
162        Ok(ast::Item::Use(ast::ItemUse {
163            meta: self.meta("u", placement),
164            tree: self.import_use_tree(&node.tree)?,
165        }))
166    }
167
168    fn import_use_tree(&mut self, tree: &syn::UseTree) -> Result<ast::UseTree, ImportError> {
169        match tree {
170            syn::UseTree::Name(node) => Ok(ast::UseTree::Name(ast::UseName {
171                name: node.ident.to_string(),
172            })),
173            syn::UseTree::Path(node) => Ok(ast::UseTree::Path(ast::UsePathTree {
174                prefix: node.ident.to_string(),
175                tree: Box::new(self.import_use_tree(&node.tree)?),
176            })),
177            syn::UseTree::Group(node) => Ok(ast::UseTree::Group(ast::UseGroup {
178                items: node
179                    .items
180                    .iter()
181                    .map(|item| self.import_use_tree(item))
182                    .collect::<Result<Vec<_>, _>>()?,
183            })),
184            syn::UseTree::Glob(_) => Ok(ast::UseTree::Glob(ast::UseGlob)),
185            syn::UseTree::Rename(node) => Err(unsupported(
186                node,
187                "`use path as name` renames are unsupported",
188            )),
189        }
190    }
191
192    fn import_struct(
193        &mut self,
194        node: &syn::ItemStruct,
195        placement: Placement,
196    ) -> Result<ast::Item, ImportError> {
197        self.check_attrs(&node.attrs, "struct")?;
198        self.ensure_inherited_visibility(&node.vis, "struct visibility modifiers are unsupported")?;
199        self.ensure_no_generics(&node.generics, "struct generics are unsupported")?;
200        let syn::Fields::Named(fields) = &node.fields else {
201            return Err(unsupported(
202                node,
203                "only named-field structs are supported in Rust import",
204            ));
205        };
206        let fields = fields
207            .named
208            .iter()
209            .enumerate()
210            .map(|(index, field)| self.import_field(field, index))
211            .collect::<Result<Vec<_>, _>>()?;
212        Ok(ast::Item::Struct(ast::ItemStruct {
213            meta: self.meta("st", placement),
214            name: node.ident.to_string(),
215            fields,
216        }))
217    }
218
219    fn import_field(
220        &mut self,
221        field: &syn::Field,
222        index: usize,
223    ) -> Result<ast::Field, ImportError> {
224        self.check_attrs(&field.attrs, "struct field")?;
225        self.ensure_inherited_visibility(&field.vis, "field visibility modifiers are unsupported")?;
226        let Some(ident) = &field.ident else {
227            return Err(unsupported(field, "only named struct fields are supported"));
228        };
229        Ok(ast::Field {
230            meta: self.meta("fd", Placement::ranked("fields", index)),
231            name: ident.to_string(),
232            ty: self.import_type(&field.ty)?,
233        })
234    }
235
236    fn import_enum(
237        &mut self,
238        node: &syn::ItemEnum,
239        placement: Placement,
240    ) -> Result<ast::Item, ImportError> {
241        self.check_attrs(&node.attrs, "enum")?;
242        self.ensure_inherited_visibility(&node.vis, "enum visibility modifiers are unsupported")?;
243        self.ensure_no_generics(&node.generics, "enum generics are unsupported")?;
244        let variants = node
245            .variants
246            .iter()
247            .enumerate()
248            .map(|(index, variant)| self.import_variant(variant, index))
249            .collect::<Result<Vec<_>, _>>()?;
250        Ok(ast::Item::Enum(ast::ItemEnum {
251            meta: self.meta("en", placement),
252            name: node.ident.to_string(),
253            variants,
254        }))
255    }
256
257    fn import_variant(
258        &mut self,
259        variant: &syn::Variant,
260        index: usize,
261    ) -> Result<ast::Variant, ImportError> {
262        self.check_attrs(&variant.attrs, "enum variant")?;
263        if !matches!(variant.fields, syn::Fields::Unit) {
264            return Err(unsupported(
265                variant,
266                "only unit enum variants are supported in Rust import",
267            ));
268        }
269        if variant.discriminant.is_some() {
270            return Err(unsupported(
271                variant,
272                "enum variant discriminants are unsupported",
273            ));
274        }
275        Ok(ast::Variant {
276            meta: self.meta("v", Placement::ranked("variants", index)),
277            name: variant.ident.to_string(),
278        })
279    }
280
281    fn import_fn(
282        &mut self,
283        node: &syn::ItemFn,
284        placement: Placement,
285    ) -> Result<ast::Item, ImportError> {
286        self.check_attrs(&node.attrs, "function")?;
287        self.ensure_inherited_visibility(
288            &node.vis,
289            "function visibility modifiers are unsupported",
290        )?;
291        self.ensure_supported_signature(&node.sig)?;
292        let params = node
293            .sig
294            .inputs
295            .iter()
296            .enumerate()
297            .map(|(index, arg)| self.import_param(arg, index))
298            .collect::<Result<Vec<_>, _>>()?;
299        let ret_ty = match &node.sig.output {
300            syn::ReturnType::Default => None,
301            syn::ReturnType::Type(_, ty) => Some(self.import_type(ty)?),
302        };
303        Ok(ast::Item::Fn(ast::ItemFn {
304            meta: self.meta("f", placement),
305            name: node.sig.ident.to_string(),
306            params,
307            ret_ty,
308            body: self.import_block(&node.block)?,
309        }))
310    }
311
312    fn import_param(&mut self, arg: &syn::FnArg, index: usize) -> Result<ast::Param, ImportError> {
313        let syn::FnArg::Typed(node) = arg else {
314            return Err(unsupported(
315                arg,
316                "method receivers are unsupported; only free functions are supported",
317            ));
318        };
319        self.check_attrs(&node.attrs, "function parameter")?;
320        let name = self.import_param_name(&node.pat)?;
321        Ok(ast::Param {
322            meta: self.meta("p", Placement::ranked("params", index)),
323            name,
324            ty: self.import_type(&node.ty)?,
325        })
326    }
327
328    fn import_param_name(&mut self, pat: &syn::Pat) -> Result<String, ImportError> {
329        match pat {
330            syn::Pat::Ident(node) => {
331                self.check_attrs(&node.attrs, "function parameter pattern")?;
332                if node.by_ref.is_some() || node.mutability.is_some() || node.subpat.is_some() {
333                    return Err(unsupported(
334                        node,
335                        "parameter patterns must be plain identifiers",
336                    ));
337                }
338                Ok(node.ident.to_string())
339            }
340            _ => Err(unsupported(
341                pat,
342                "parameter patterns must be plain identifiers",
343            )),
344        }
345    }
346
347    fn import_block(&mut self, block: &syn::Block) -> Result<ast::Block, ImportError> {
348        let stmts = block
349            .stmts
350            .iter()
351            .enumerate()
352            .map(|(index, stmt)| self.import_stmt(stmt, index))
353            .collect::<Result<Vec<_>, _>>()?;
354        Ok(ast::Block { meta: None, stmts })
355    }
356
357    fn import_stmt(&mut self, stmt: &syn::Stmt, index: usize) -> Result<ast::Stmt, ImportError> {
358        match stmt {
359            syn::Stmt::Local(node) => {
360                self.check_attrs(&node.attrs, "let statement")?;
361                let Some(init) = &node.init else {
362                    return Err(unsupported(
363                        node,
364                        "`let` statements without initializers are unsupported",
365                    ));
366                };
367                if init.diverge.is_some() {
368                    return Err(unsupported(node, "`let ... else` is unsupported"));
369                }
370                Ok(ast::Stmt::Let(ast::StmtLet {
371                    meta: self.meta("s", Placement::ranked("body", index)),
372                    pat: self.import_pattern(&node.pat)?,
373                    value: self.import_expr(&init.expr)?,
374                }))
375            }
376            syn::Stmt::Item(item) => Ok(ast::Stmt::Item(
377                self.import_item(item, Placement::ranked("body", index))?,
378            )),
379            syn::Stmt::Expr(expr, semi) => Ok(ast::Stmt::Expr(ast::StmtExpr {
380                meta: self.meta("s", Placement::ranked("body", index)),
381                expr: self.import_expr(expr)?,
382                has_semi: semi.is_some(),
383            })),
384            syn::Stmt::Macro(node) => Err(unsupported(node, "statement macros are unsupported")),
385        }
386    }
387
388    fn import_pattern(&mut self, pat: &syn::Pat) -> Result<ast::Pattern, ImportError> {
389        match pat {
390            syn::Pat::Ident(node) => {
391                self.check_attrs(&node.attrs, "pattern")?;
392                if node.by_ref.is_some() || node.mutability.is_some() || node.subpat.is_some() {
393                    return Err(unsupported(
394                        node,
395                        "only plain identifier and wildcard patterns are supported",
396                    ));
397                }
398                Ok(ast::Pattern::Ident(ast::PatIdent {
399                    meta: Some(self.detached_meta("pt")),
400                    name: node.ident.to_string(),
401                }))
402            }
403            syn::Pat::Wild(node) => {
404                self.check_attrs(&node.attrs, "pattern")?;
405                Ok(ast::Pattern::Wild(ast::PatWild {
406                    meta: Some(self.detached_meta("pt")),
407                }))
408            }
409            _ => Err(unsupported(
410                pat,
411                "only plain identifier and wildcard patterns are supported",
412            )),
413        }
414    }
415
416    fn import_type(&mut self, ty: &syn::Type) -> Result<ast::Type, ImportError> {
417        match ty {
418            syn::Type::Path(node) => Ok(ast::Type::Path(ast::TypePath {
419                meta: self.detached_meta("t"),
420                path: self.import_path(&node.path, node.qself.is_some(), "type paths")?,
421            })),
422            _ => Err(unsupported(ty, "only path types are supported")),
423        }
424    }
425
426    fn import_expr(&mut self, expr: &syn::Expr) -> Result<ast::Expr, ImportError> {
427        self.import_expr_with_root_meta(expr, true)
428    }
429
430    fn import_expr_with_root_meta(
431        &mut self,
432        expr: &syn::Expr,
433        allow_root_meta: bool,
434    ) -> Result<ast::Expr, ImportError> {
435        match expr {
436            syn::Expr::Path(node) => {
437                self.check_attrs(&node.attrs, "expression")?;
438                Ok(ast::Expr::Path(ast::ExprPath {
439                    meta: allow_root_meta.then(|| self.detached_meta("e")),
440                    path: self.import_path(&node.path, node.qself.is_some(), "expression paths")?,
441                }))
442            }
443            syn::Expr::Lit(node) => {
444                self.check_attrs(&node.attrs, "expression")?;
445                Ok(ast::Expr::Lit(ast::ExprLit {
446                    meta: allow_root_meta.then(|| self.detached_meta("l")),
447                    value: self.import_lit(&node.lit, node)?,
448                }))
449            }
450            syn::Expr::Paren(node) => {
451                self.check_attrs(&node.attrs, "expression")?;
452                Ok(ast::Expr::Group(ast::ExprGroup {
453                    meta: allow_root_meta.then(|| self.detached_meta("e")),
454                    expr: Box::new(self.import_expr_with_root_meta(&node.expr, true)?),
455                }))
456            }
457            syn::Expr::Group(node) => {
458                self.check_attrs(&node.attrs, "expression")?;
459                Ok(ast::Expr::Group(ast::ExprGroup {
460                    meta: allow_root_meta.then(|| self.detached_meta("e")),
461                    expr: Box::new(self.import_expr_with_root_meta(&node.expr, true)?),
462                }))
463            }
464            syn::Expr::Binary(node) => {
465                self.check_attrs(&node.attrs, "expression")?;
466                Ok(ast::Expr::Binary(ast::ExprBinary {
467                    meta: allow_root_meta.then(|| self.detached_meta("e")),
468                    lhs: Box::new(self.import_expr_with_root_meta(&node.left, false)?),
469                    op: self.import_bin_op(&node.op)?,
470                    rhs: Box::new(self.import_expr_with_root_meta(&node.right, true)?),
471                }))
472            }
473            syn::Expr::Unary(node) => {
474                self.check_attrs(&node.attrs, "expression")?;
475                let syn::UnOp::Neg(_) = node.op else {
476                    return Err(unsupported(
477                        node,
478                        "only unary minus is supported in expressions",
479                    ));
480                };
481                Ok(ast::Expr::Unary(ast::ExprUnary {
482                    meta: allow_root_meta.then(|| self.detached_meta("e")),
483                    op: ast::UnaryOp::Neg,
484                    expr: Box::new(self.import_expr_with_root_meta(&node.expr, true)?),
485                }))
486            }
487            syn::Expr::Call(node) => {
488                self.check_attrs(&node.attrs, "expression")?;
489                Ok(ast::Expr::Call(ast::ExprCall {
490                    meta: allow_root_meta.then(|| self.detached_meta("e")),
491                    callee: Box::new(self.import_expr_with_root_meta(&node.func, false)?),
492                    args: node
493                        .args
494                        .iter()
495                        .map(|arg| self.import_expr_with_root_meta(arg, true))
496                        .collect::<Result<Vec<_>, _>>()?,
497                }))
498            }
499            syn::Expr::Match(node) => {
500                self.check_attrs(&node.attrs, "expression")?;
501                Ok(ast::Expr::Match(ast::ExprMatch {
502                    meta: allow_root_meta.then(|| self.detached_meta("e")),
503                    scrutinee: Box::new(self.import_expr_with_root_meta(&node.expr, true)?),
504                    arms: node
505                        .arms
506                        .iter()
507                        .enumerate()
508                        .map(|(index, arm)| self.import_arm(arm, index))
509                        .collect::<Result<Vec<_>, _>>()?,
510                }))
511            }
512            syn::Expr::Block(node) => {
513                self.check_attrs(&node.attrs, "expression")?;
514                if node.label.is_some() {
515                    return Err(unsupported(
516                        node,
517                        "labeled block expressions are unsupported",
518                    ));
519                }
520                let mut block = self.import_block(&node.block)?;
521                block.meta = allow_root_meta.then(|| self.detached_meta("e"));
522                Ok(ast::Expr::Block(block))
523            }
524            _ => Err(unsupported(
525                expr,
526                "expression is outside the currently supported Rust import subset",
527            )),
528        }
529    }
530
531    fn import_lit(
532        &mut self,
533        lit: &syn::Lit,
534        span_node: &impl Spanned,
535    ) -> Result<ast::Literal, ImportError> {
536        match lit {
537            syn::Lit::Int(node) => {
538                if !node.suffix().is_empty() {
539                    return Err(unsupported(
540                        span_node,
541                        "integer literal suffixes are unsupported",
542                    ));
543                }
544                node.base10_parse::<i64>()
545                    .map(ast::Literal::Int)
546                    .map_err(|_| {
547                        unsupported(
548                            span_node,
549                            "only decimal integer literals that fit in `i64` are supported",
550                        )
551                    })
552            }
553            syn::Lit::Str(node) => Ok(ast::Literal::Str(node.value())),
554            _ => Err(unsupported(
555                span_node,
556                "only integer and string literals are supported",
557            )),
558        }
559    }
560
561    fn import_bin_op(&mut self, op: &syn::BinOp) -> Result<ast::BinaryOp, ImportError> {
562        match op {
563            syn::BinOp::Add(_) => Ok(ast::BinaryOp::Add),
564            syn::BinOp::Sub(_) => Ok(ast::BinaryOp::Sub),
565            syn::BinOp::Lt(_) => Ok(ast::BinaryOp::Lt),
566            _ => Err(unsupported(
567                op,
568                "only `+`, `-`, and `<` binary operators are supported",
569            )),
570        }
571    }
572
573    fn import_arm(&mut self, arm: &syn::Arm, index: usize) -> Result<ast::MatchArm, ImportError> {
574        self.check_attrs(&arm.attrs, "match arm")?;
575        Ok(ast::MatchArm {
576            meta: self.meta("a", Placement::ranked("arms", index)),
577            pat: self.import_pattern(&arm.pat)?,
578            guard: arm
579                .guard
580                .as_ref()
581                .map(|(_, expr)| self.import_expr(expr))
582                .transpose()?,
583            body: self.import_expr(&arm.body)?,
584        })
585    }
586
587    fn import_path(
588        &mut self,
589        path: &syn::Path,
590        has_qself: bool,
591        context: &str,
592    ) -> Result<ast::Path, ImportError> {
593        if has_qself {
594            return Err(unsupported(
595                path,
596                format!("qualified self in {context} is unsupported"),
597            ));
598        }
599        if path.leading_colon.is_some() {
600            return Err(unsupported(
601                path,
602                format!("leading `::` in {context} is unsupported"),
603            ));
604        }
605        let mut segments = Vec::with_capacity(path.segments.len());
606        for segment in &path.segments {
607            if !matches!(segment.arguments, syn::PathArguments::None) {
608                return Err(unsupported(
609                    segment,
610                    format!("generic arguments in {context} are unsupported"),
611                ));
612            }
613            segments.push(segment.ident.to_string());
614        }
615        Ok(ast::Path { segments })
616    }
617
618    fn ensure_supported_signature(&self, sig: &syn::Signature) -> Result<(), ImportError> {
619        if sig.constness.is_some() {
620            return Err(unsupported(sig, "`const fn` is unsupported"));
621        }
622        if sig.asyncness.is_some() {
623            return Err(unsupported(sig, "`async fn` is unsupported"));
624        }
625        if sig.unsafety.is_some() {
626            return Err(unsupported(sig, "`unsafe fn` is unsupported"));
627        }
628        if sig.abi.is_some() {
629            return Err(unsupported(sig, "extern function ABIs are unsupported"));
630        }
631        self.ensure_no_generics(&sig.generics, "function generics are unsupported")?;
632        if sig.variadic.is_some() {
633            return Err(unsupported(sig, "variadic functions are unsupported"));
634        }
635        Ok(())
636    }
637
638    fn ensure_no_generics(
639        &self,
640        generics: &syn::Generics,
641        message: &str,
642    ) -> Result<(), ImportError> {
643        if !generics.params.is_empty() || generics.where_clause.is_some() {
644            return Err(unsupported(generics, message));
645        }
646        Ok(())
647    }
648
649    fn ensure_inherited_visibility(
650        &self,
651        vis: &syn::Visibility,
652        message: &str,
653    ) -> Result<(), ImportError> {
654        if !matches!(vis, syn::Visibility::Inherited) {
655            return Err(unsupported(vis, message));
656        }
657        Ok(())
658    }
659
660    fn check_attrs(&self, attrs: &[syn::Attribute], context: &str) -> Result<(), ImportError> {
661        for attr in attrs {
662            if attr.path().is_ident("doc") {
663                continue;
664            }
665            return Err(unsupported(
666                attr,
667                format!("unsupported attribute on {context}; only doc comments are ignored"),
668            ));
669        }
670        Ok(())
671    }
672
673    fn meta(&mut self, prefix: &'static str, placement: Placement) -> ast::Meta {
674        ast::Meta {
675            id: self.ids.next(prefix),
676            rank: placement.rank,
677            anchor: None,
678            slot: placement.slot.map(str::to_owned),
679            span: None,
680        }
681    }
682
683    fn detached_meta(&mut self, prefix: &'static str) -> ast::Meta {
684        ast::Meta {
685            id: self.ids.next(prefix),
686            rank: None,
687            anchor: None,
688            slot: None,
689            span: None,
690        }
691    }
692}
693
694fn unsupported<T: Spanned>(node: &T, message: impl Into<String>) -> ImportError {
695    ImportError::Unsupported(syn::Error::new(node.span(), message.into()))
696}
697
698#[cfg(test)]
699mod tests {
700    use super::import_source;
701    use crate::lower_file;
702    use draxl_ast as ast;
703
704    fn lower_imported(imported: &ast::File) -> String {
705        lower_file(imported)
706    }
707
708    #[test]
709    fn imports_a_simple_function_body() {
710        let source = r#"
711mod demo {
712    fn add_one(x: i64) -> i64 {
713        let y = (x + 1);
714        y
715    }
716}
717"#;
718
719        let imported = import_source(source).expect("simple function should import");
720
721        let [ast::Item::Mod(module)] = imported.items.as_slice() else {
722            panic!("expected one imported module, found {:?}", imported.items);
723        };
724        assert_eq!(module.meta.id, "m0001");
725        let [ast::Item::Fn(function)] = module.items.as_slice() else {
726            panic!("expected one imported function, found {:?}", module.items);
727        };
728        assert_eq!(function.meta.id, "f0001");
729        assert_eq!(function.params[0].meta.id, "p0001");
730        assert_eq!(function.body.stmts.len(), 2);
731        assert_eq!(
732            lower_imported(&imported),
733            "mod demo {\n  fn add_one(x: i64) -> i64 {\n    let y = (x + 1);\n    y\n  }\n}\n\n"
734        );
735    }
736
737    #[test]
738    fn imports_match_use_and_shapes() {
739        let source = r#"
740mod shapes {
741    use std::cmp::{self, *};
742
743    struct Point {
744        x: i64,
745        y: i64,
746    }
747
748    enum Color {
749        Red,
750        Green,
751    }
752
753    fn abs(x: i64) -> i64 {
754        match x {
755            n if (n < 0) => (-n),
756            _ => x,
757        }
758    }
759}
760"#;
761
762        let imported = import_source(source).expect("supported Rust subset should import");
763
764        let [ast::Item::Mod(module)] = imported.items.as_slice() else {
765            panic!("expected one imported module, found {:?}", imported.items);
766        };
767        assert_eq!(module.items.len(), 4);
768        assert!(matches!(
769            &module.items[0],
770            ast::Item::Use(node) if node.meta.id == "u0001"
771        ));
772        assert!(matches!(
773            &module.items[1],
774            ast::Item::Struct(node) if node.meta.id == "st0001" && node.fields.len() == 2
775        ));
776        assert!(matches!(
777            &module.items[2],
778            ast::Item::Enum(node) if node.meta.id == "en0001" && node.variants.len() == 2
779        ));
780        assert!(matches!(
781            &module.items[3],
782            ast::Item::Fn(node) if node.name == "abs" && node.body.stmts.len() == 1
783        ));
784        assert_eq!(
785            lower_imported(&imported),
786            "mod shapes {\n  use std::cmp::{self, *};\n\n  struct Point {\n    x: i64,\n    y: i64,\n  }\n\n  enum Color {\n    Red,\n    Green,\n  }\n\n  fn abs(x: i64) -> i64 {\n    match x {\n      n if (n < 0) => (-n),\n      _ => x,\n    }\n  }\n}\n\n"
787        );
788    }
789
790    #[test]
791    fn rejects_generics_and_visibility() {
792        let error =
793            import_source("pub fn run<T>(x: T) {}\n").expect_err("unsupported syntax should fail");
794        let message = error.to_string();
795        assert!(
796            message.contains("visibility modifiers") || message.contains("generics"),
797            "unexpected error: {message}"
798        );
799    }
800
801    #[test]
802    fn rejects_macros_and_tuple_structs() {
803        let macro_error = import_source("fn run() { println!(\"hi\"); }\n")
804            .expect_err("statement macro should fail");
805        assert!(
806            macro_error
807                .to_string()
808                .contains("statement macros are unsupported")
809                || macro_error.to_string().contains("expression is outside"),
810            "unexpected error: {macro_error}"
811        );
812
813        let struct_error =
814            import_source("struct Pair(i64, i64);\n").expect_err("tuple struct should fail");
815        assert!(
816            struct_error
817                .to_string()
818                .contains("only named-field structs are supported"),
819            "unexpected error: {struct_error}"
820        );
821    }
822}