kcl_lib/
modules.rs

1use std::fmt;
2
3use anyhow::Result;
4pub use kcl_error::ModuleId;
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    SourceRange,
9    errors::{KclError, KclErrorDetails},
10    exec::KclValue,
11    execution::{EnvironmentRef, ModuleArtifactState, PreImportedGeometry, typed_path::TypedPath},
12    fs::{FileManager, FileSystem},
13    parsing::ast::types::{ImportPath, Node, Program},
14};
15
16#[derive(Debug, Clone, Default)]
17pub(crate) struct ModuleLoader {
18    /// The stack of import statements for detecting circular module imports.
19    /// If this is empty, we're not currently executing an import statement.
20    pub import_stack: Vec<TypedPath>,
21}
22
23impl ModuleLoader {
24    pub(crate) fn cycle_check(&self, path: &ModulePath, source_range: SourceRange) -> Result<(), KclError> {
25        if self.import_stack.contains(path.expect_path()) {
26            return Err(self.import_cycle_error(path, source_range));
27        }
28        Ok(())
29    }
30
31    pub(crate) fn import_cycle_error(&self, path: &ModulePath, source_range: SourceRange) -> KclError {
32        KclError::new_import_cycle(KclErrorDetails::new(
33            format!(
34                "circular import of modules is not allowed: {} -> {}",
35                self.import_stack
36                    .iter()
37                    .map(|p| p.to_string_lossy())
38                    .collect::<Vec<_>>()
39                    .join(" -> "),
40                path,
41            ),
42            vec![source_range],
43        ))
44    }
45
46    pub(crate) fn enter_module(&mut self, path: &ModulePath) {
47        if let ModulePath::Local { value: path, .. } = path {
48            self.import_stack.push(path.clone());
49        }
50    }
51
52    pub(crate) fn leave_module(&mut self, path: &ModulePath, source_range: SourceRange) -> Result<(), KclError> {
53        if let ModulePath::Local { value: path, .. } = path {
54            if let Some(popped) = self.import_stack.pop() {
55                debug_assert_eq!(path, &popped);
56                if path != &popped {
57                    return Err(KclError::new_internal(KclErrorDetails::new(
58                        format!(
59                            "Import stack mismatch when leaving module: expected to leave {}, but leaving {}",
60                            path.to_string_lossy(),
61                            popped.to_string_lossy(),
62                        ),
63                        vec![source_range],
64                    )));
65                }
66            } else {
67                let message = format!("Import stack underflow when leaving module: {path}");
68                debug_assert!(false, "{}", &message);
69                return Err(KclError::new_internal(KclErrorDetails::new(
70                    message,
71                    vec![source_range],
72                )));
73            }
74        }
75        Ok(())
76    }
77}
78
79pub(crate) fn read_std(mod_name: &str) -> Option<&'static str> {
80    match mod_name {
81        "prelude" => Some(include_str!("../std/prelude.kcl")),
82        "gdt" => Some(include_str!("../std/gdt.kcl")),
83        "math" => Some(include_str!("../std/math.kcl")),
84        "runtime" => Some(include_str!("../std/runtime.kcl")),
85        "sketch" => Some(include_str!("../std/sketch.kcl")),
86        "sketch2" => Some(include_str!("../std/sketch2.kcl")),
87        "turns" => Some(include_str!("../std/turns.kcl")),
88        "types" => Some(include_str!("../std/types.kcl")),
89        "solid" => Some(include_str!("../std/solid.kcl")),
90        "units" => Some(include_str!("../std/units.kcl")),
91        "array" => Some(include_str!("../std/array.kcl")),
92        "sweep" => Some(include_str!("../std/sweep.kcl")),
93        "appearance" => Some(include_str!("../std/appearance.kcl")),
94        "transform" => Some(include_str!("../std/transform.kcl")),
95        "vector" => Some(include_str!("../std/vector.kcl")),
96        "hole" => Some(include_str!("../std/hole.kcl")),
97        _ => None,
98    }
99}
100
101/// Info about a module.
102#[derive(Debug, Clone, PartialEq, Serialize)]
103pub struct ModuleInfo {
104    /// The ID of the module.
105    pub(crate) id: ModuleId,
106    /// Absolute path of the module's source file.
107    pub(crate) path: ModulePath,
108    pub(crate) repr: ModuleRepr,
109}
110
111impl ModuleInfo {
112    pub(crate) fn take_repr(&mut self) -> ModuleRepr {
113        let mut result = ModuleRepr::Dummy;
114        std::mem::swap(&mut self.repr, &mut result);
115        result
116    }
117
118    pub(crate) fn restore_repr(&mut self, repr: ModuleRepr) {
119        assert!(matches!(&self.repr, ModuleRepr::Dummy));
120        self.repr = repr;
121    }
122}
123
124#[allow(clippy::large_enum_variant)]
125#[derive(Debug, Clone, PartialEq, Serialize)]
126pub enum ModuleRepr {
127    Root,
128    // AST, memory, exported names
129    Kcl(
130        Node<Program>,
131        /// Cached execution outcome.
132        Option<ModuleExecutionOutcome>,
133    ),
134    Foreign(PreImportedGeometry, Option<(Option<KclValue>, ModuleArtifactState)>),
135    Dummy,
136}
137
138#[derive(Debug, Clone, PartialEq, Serialize)]
139pub struct ModuleExecutionOutcome {
140    pub last_expr: Option<KclValue>,
141    pub environment: EnvironmentRef,
142    pub exports: Vec<String>,
143    pub artifacts: ModuleArtifactState,
144}
145
146#[allow(clippy::large_enum_variant)]
147#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash, ts_rs::TS)]
148#[serde(tag = "type")]
149pub enum ModulePath {
150    // The main file of the project.
151    Main,
152    Local {
153        value: TypedPath,
154        original_import_path: Option<TypedPath>,
155    },
156    Std {
157        value: String,
158    },
159}
160
161impl ModulePath {
162    pub(crate) fn expect_path(&self) -> &TypedPath {
163        match self {
164            ModulePath::Local { value: p, .. } => p,
165            _ => unreachable!(),
166        }
167    }
168
169    pub(crate) async fn source(&self, fs: &FileManager, source_range: SourceRange) -> Result<ModuleSource, KclError> {
170        match self {
171            ModulePath::Local { value: p, .. } => Ok(ModuleSource {
172                source: fs.read_to_string(p, source_range).await?,
173                path: self.clone(),
174            }),
175            ModulePath::Std { value: name } => Ok(ModuleSource {
176                source: read_std(name)
177                    .ok_or_else(|| {
178                        KclError::new_semantic(KclErrorDetails::new(
179                            format!("Cannot find standard library module to import: std::{name}."),
180                            vec![source_range],
181                        ))
182                    })
183                    .map(str::to_owned)?,
184                path: self.clone(),
185            }),
186            ModulePath::Main => unreachable!(),
187        }
188    }
189
190    pub(crate) fn from_import_path(
191        path: &ImportPath,
192        project_directory: &Option<TypedPath>,
193        import_from: &ModulePath,
194    ) -> Result<Self, KclError> {
195        match path {
196            ImportPath::Kcl { filename: path } | ImportPath::Foreign { path } => {
197                let resolved_path = match import_from {
198                    ModulePath::Main => {
199                        if let Some(project_dir) = project_directory {
200                            project_dir.join_typed(path)
201                        } else {
202                            path.clone()
203                        }
204                    }
205                    ModulePath::Local { value, .. } => {
206                        let import_from_dir = value.parent();
207                        let base = import_from_dir.as_ref().or(project_directory.as_ref());
208                        if let Some(dir) = base {
209                            dir.join_typed(path)
210                        } else {
211                            path.clone()
212                        }
213                    }
214                    ModulePath::Std { .. } => {
215                        let message = format!("Cannot import a non-std KCL file from std: {path}.");
216                        debug_assert!(false, "{}", &message);
217                        return Err(KclError::new_internal(KclErrorDetails::new(message, vec![])));
218                    }
219                };
220
221                Ok(ModulePath::Local {
222                    value: resolved_path,
223                    original_import_path: Some(path.clone()),
224                })
225            }
226            ImportPath::Std { path } => Self::from_std_import_path(path),
227        }
228    }
229
230    pub(crate) fn from_std_import_path(path: &[String]) -> Result<Self, KclError> {
231        // For now we only support importing from singly-nested modules inside std.
232        if path.len() > 2 || path[0] != "std" {
233            let message = format!("Invalid std import path: {path:?}.");
234            debug_assert!(false, "{}", &message);
235            return Err(KclError::new_internal(KclErrorDetails::new(message, vec![])));
236        }
237
238        if path.len() == 1 {
239            Ok(ModulePath::Std {
240                value: "prelude".to_owned(),
241            })
242        } else {
243            Ok(ModulePath::Std { value: path[1].clone() })
244        }
245    }
246}
247
248impl fmt::Display for ModulePath {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        match self {
251            ModulePath::Main => write!(f, "main"),
252            ModulePath::Local { value: path, .. } => path.fmt(f),
253            ModulePath::Std { value: s } => write!(f, "std::{s}"),
254        }
255    }
256}
257
258#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, ts_rs::TS)]
259pub struct ModuleSource {
260    pub path: ModulePath,
261    pub source: String,
262}