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 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#[derive(Debug, Clone, PartialEq, Serialize)]
103pub struct ModuleInfo {
104 pub(crate) id: ModuleId,
106 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 Kcl(
130 Node<Program>,
131 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 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 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}