Skip to main content

microcad_lang/parse/
source_file.rs

1// Copyright © 2024-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4use crate::{parse::*, parser::*};
5use microcad_lang_base::{FormatTree, SrcReferrer};
6use microcad_syntax::ast;
7use std::fs::read_to_string;
8
9impl SourceFile {
10    /// Load µcad source file from given `path`
11    pub fn load(
12        path: impl AsRef<std::path::Path> + std::fmt::Debug,
13    ) -> Result<std::rc::Rc<Self>, ParseErrorsWithSource> {
14        let (source, error) = Self::load_with_name(&path, Self::name_from_path(&path));
15        match error {
16            Some(error) => Err(error),
17            None => Ok(source),
18        }
19    }
20
21    /// Load µcad source file from given `path`
22    pub fn load_with_name(
23        path: impl AsRef<std::path::Path> + std::fmt::Debug,
24        name: QualifiedName,
25    ) -> (std::rc::Rc<Self>, Option<ParseErrorsWithSource>) {
26        let path = path.as_ref();
27        log::trace!(
28            "{load} file {path} [{name}]",
29            path = path.display(),
30            load = microcad_lang_base::mark!(LOAD)
31        );
32
33        let buf = match read_to_string(path) {
34            Ok(buf) => buf,
35            Err(error) => {
36                let error = ParseError::LoadSource(name.src_ref(), path.into(), error);
37                let mut source_file =
38                    SourceFile::new(None, StatementList::default(), String::new(), 0);
39                source_file.name = name;
40                return (std::rc::Rc::new(source_file), Some(error.into()));
41            }
42        };
43
44        let (mut source_file, errors) = Self::load_inner(None, path, &buf);
45        source_file.name = name;
46        log::debug!(
47            "Successfully loaded external file {} to {}",
48            path.display(),
49            source_file.name
50        );
51
52        (std::rc::Rc::new(source_file), errors)
53    }
54
55    /// Create `SourceFile` from string
56    /// The hash of the result will be of `crate::from_str!()`.
57    pub fn load_from_str(
58        name: Option<&str>,
59        path: impl AsRef<std::path::Path>,
60        source_str: &str,
61    ) -> Result<std::rc::Rc<Self>, Vec<ParseError>> {
62        let (source, error) = Self::load_inner(name, path, source_str);
63        match error {
64            Some(error) => Err(error.errors),
65            None => Ok(std::rc::Rc::new(source)),
66        }
67    }
68
69    /// Create `SourceFile` from string, returning the empty `SourceFile` on error
70    /// The hash of the result will be of `crate::from_str!()`.
71    pub fn load_from_str_with_recovery(
72        name: Option<&str>,
73        path: impl AsRef<std::path::Path>,
74        source_str: &str,
75    ) -> (std::rc::Rc<Self>, Option<Vec<ParseError>>) {
76        let (source, error) = Self::load_inner(name, path, source_str);
77        (std::rc::Rc::new(source), error.map(|err| err.errors))
78    }
79
80    fn load_inner(
81        name: Option<&str>,
82        path: impl AsRef<std::path::Path>,
83        source_str: &str,
84    ) -> (Self, Option<ParseErrorsWithSource>) {
85        log::trace!(
86            "{load} source from string",
87            load = microcad_lang_base::mark!(LOAD)
88        );
89        let parse_context = ParseContext::new(source_str);
90
91        let dummy_source = || {
92            let mut source = SourceFile::new(
93                None,
94                StatementList::default(),
95                source_str.into(),
96                parse_context.source_file_hash,
97            );
98            source.filename = Some(path.as_ref().into());
99            source
100        };
101
102        let ast = match build_ast(source_str, &parse_context) {
103            Ok(ast) => ast,
104            Err(error) => {
105                return (dummy_source(), Some(error));
106            }
107        };
108
109        let mut source_file =
110            match Self::from_ast(&ast, &parse_context).map_err(|error| vec![error]) {
111                Ok(source_file) => source_file,
112                Err(errors) => {
113                    return (
114                        dummy_source(),
115                        Some(ParseErrorsWithSource {
116                            errors,
117                            source_code: Some(source_str.into()),
118                            source_hash: parse_context.source_file_hash,
119                        }),
120                    );
121                }
122            };
123        if let Some(name) = name {
124            source_file.set_name(QualifiedName::from_id(Identifier::no_ref(name)));
125        } else {
126            source_file.set_name(Self::name_from_path(&path));
127        };
128        source_file.set_filename(path);
129        log::debug!("Successfully loaded source from string");
130        log::trace!("Syntax tree:\n{}", FormatTree(&source_file));
131        (source_file, None)
132    }
133
134    /// Get the source file name from path.
135    pub fn name_from_path(path: impl AsRef<std::path::Path>) -> QualifiedName {
136        QualifiedName::from_id(Identifier::no_ref(
137            &path
138                .as_ref()
139                .file_stem()
140                .expect("illegal file name")
141                .to_string_lossy(),
142        ))
143    }
144}
145
146impl FromAst for SourceFile {
147    type AstNode = ast::SourceFile;
148
149    fn from_ast(node: &Self::AstNode, context: &ParseContext) -> Result<Self, ParseError> {
150        Ok(SourceFile::new(
151            None, // todo
152            StatementList::from_ast(&node.statements, context)?,
153            context.source.into(),
154            context.source_file_hash,
155        ))
156    }
157}
158
159#[test]
160fn parse_source_file() {
161    let source_file = SourceFile::load_from_str(
162        None,
163        "test.µcad",
164        r#"use std::log::info;
165            part Foo(r: Scalar) {
166                info("Hello, world, {r}!");
167            }
168            Foo(20.0);
169            "#,
170    )
171    .expect("test error");
172
173    assert_eq!(source_file.statements.len(), 3);
174}