kcl_lib/
modules.rs

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