Refactor execution module (#5162)
* cargo update, etc Signed-off-by: Nick Cameron <nrc@ncameron.org> * Refactor execution/mod.rs (code motion) Signed-off-by: Nick Cameron <nrc@ncameron.org> * Refactor caching out of ExecutorContext plus some tidying up Signed-off-by: Nick Cameron <nrc@ncameron.org> * Move caching logic to inside execution Signed-off-by: Nick Cameron <nrc@ncameron.org> --------- Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
		
							
								
								
									
										748
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										748
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -3,9 +3,8 @@ name = "wasm-lib" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| repository = "https://github.com/KittyCAD/modeling-app" | ||||
| rust-version = "1.73" | ||||
| rust-version = "1.83" | ||||
|  | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
| [lib] | ||||
| crate-type = ["cdylib"] | ||||
|  | ||||
|  | ||||
| @ -831,7 +831,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr | ||||
|                 context_type: crate::execution::ContextType::Mock, | ||||
|             }; | ||||
|  | ||||
|             if let Err(e) = ctx.run(program.into(), &mut crate::ExecState::new(&ctx.settings)).await { | ||||
|             if let Err(e) = ctx.run(&program, &mut crate::ExecState::new(&ctx.settings)).await { | ||||
|                     return Err(miette::Report::new(crate::errors::Report { | ||||
|                         error: e, | ||||
|                         filename: format!("{}{}", #fn_name, #index), | ||||
|  | ||||
| @ -15,7 +15,7 @@ mod test_examples_someFn { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -15,7 +15,7 @@ mod test_examples_someFn { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -16,7 +16,7 @@ mod test_examples_show { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
| @ -73,7 +73,7 @@ mod test_examples_show { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -16,7 +16,7 @@ mod test_examples_show { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -17,7 +17,7 @@ mod test_examples_my_func { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
| @ -74,7 +74,7 @@ mod test_examples_my_func { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -17,7 +17,7 @@ mod test_examples_line_to { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
| @ -74,7 +74,7 @@ mod test_examples_line_to { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -16,7 +16,7 @@ mod test_examples_min { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
| @ -73,7 +73,7 @@ mod test_examples_min { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -16,7 +16,7 @@ mod test_examples_show { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -16,7 +16,7 @@ mod test_examples_import { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -16,7 +16,7 @@ mod test_examples_import { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -16,7 +16,7 @@ mod test_examples_import { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -16,7 +16,7 @@ mod test_examples_show { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -15,7 +15,7 @@ mod test_examples_some_function { | ||||
|             context_type: crate::execution::ContextType::Mock, | ||||
|         }; | ||||
|         if let Err(e) = ctx | ||||
|             .run(program.into(), &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .run(&program, &mut crate::ExecState::new(&ctx.settings)) | ||||
|             .await | ||||
|         { | ||||
|             return Err(miette::Report::new(crate::errors::Report { | ||||
|  | ||||
| @ -151,7 +151,7 @@ async fn handle_request(req: hyper::Request<Body>, state3: Arc<ServerState>) -> | ||||
| /// KCL errors (from engine or the executor) respond with HTTP Bad Gateway. | ||||
| /// Malformed requests are HTTP Bad Request. | ||||
| /// Successful requests contain a PNG as the body. | ||||
| async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body> { | ||||
| async fn snapshot_endpoint(body: Bytes, ctxt: ExecutorContext) -> Response<Body> { | ||||
|     let body = match serde_json::from_slice::<RequestBody>(body.as_ref()) { | ||||
|         Ok(bd) => bd, | ||||
|         Err(e) => return bad_request(format!("Invalid request JSON: {e}")), | ||||
| @ -164,11 +164,11 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body | ||||
|     }; | ||||
|  | ||||
|     eprintln!("Executing {test_name}"); | ||||
|     let mut exec_state = ExecState::new(&state.settings); | ||||
|     let mut exec_state = ExecState::new(&ctxt.settings); | ||||
|     // This is a shitty source range, I don't know what else to use for it though. | ||||
|     // There's no actual KCL associated with this reset_scene call. | ||||
|     if let Err(e) = state | ||||
|         .reset_scene(&mut exec_state, kcl_lib::SourceRange::default()) | ||||
|     if let Err(e) = ctxt | ||||
|         .send_clear_scene(&mut exec_state, kcl_lib::SourceRange::default()) | ||||
|         .await | ||||
|     { | ||||
|         return kcl_err(e); | ||||
| @ -176,8 +176,11 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body | ||||
|     // Let users know if the test is taking a long time. | ||||
|     let (done_tx, done_rx) = oneshot::channel::<()>(); | ||||
|     let timer = time_until(done_rx); | ||||
|     let snapshot = match state.execute_and_prepare_snapshot(&program, &mut exec_state).await { | ||||
|         Ok(sn) => sn, | ||||
|     if let Err(e) = ctxt.run(&program, &mut exec_state).await { | ||||
|         return kcl_err(e); | ||||
|     } | ||||
|     let snapshot = match ctxt.prepare_snapshot().await { | ||||
|         Ok(s) => s, | ||||
|         Err(e) => return kcl_err(e), | ||||
|     }; | ||||
|     let _ = done_tx.send(()); | ||||
|  | ||||
| @ -16,7 +16,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> { | ||||
|     let ctx = ExecutorContext::new_forwarded_mock(Arc::new(Box::new( | ||||
|         crate::conn_mock_core::EngineConnection::new(ref_result).await?, | ||||
|     ))); | ||||
|     ctx.run(program.into(), &mut ExecState::new(&ctx.settings)).await?; | ||||
|     ctx.run(&program, &mut ExecState::new(&ctx.settings)).await?; | ||||
|  | ||||
|     let result = result.lock().expect("mutex lock").clone(); | ||||
|     Ok(result) | ||||
|  | ||||
| @ -63,6 +63,7 @@ unsafe impl Sync for EngineConnection {} | ||||
|  | ||||
| impl EngineConnection { | ||||
|     pub async fn new(manager: EngineCommandManager) -> Result<EngineConnection, JsValue> { | ||||
|         #[allow(clippy::arc_with_non_send_sync)] | ||||
|         Ok(EngineConnection { | ||||
|             manager: Arc::new(manager), | ||||
|             batch: Arc::new(Mutex::new(Vec::new())), | ||||
|  | ||||
| @ -621,3 +621,68 @@ pub enum PlaneName { | ||||
|     /// The opposite side of the YZ plane. | ||||
|     NegYz, | ||||
| } | ||||
|  | ||||
| /// Create a new zoo api client. | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> anyhow::Result<kittycad::Client> { | ||||
|     let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),); | ||||
|     let http_client = reqwest::Client::builder() | ||||
|         .user_agent(user_agent) | ||||
|         // For file conversions we need this to be long. | ||||
|         .timeout(std::time::Duration::from_secs(600)) | ||||
|         .connect_timeout(std::time::Duration::from_secs(60)); | ||||
|     let ws_client = reqwest::Client::builder() | ||||
|         .user_agent(user_agent) | ||||
|         // For file conversions we need this to be long. | ||||
|         .timeout(std::time::Duration::from_secs(600)) | ||||
|         .connect_timeout(std::time::Duration::from_secs(60)) | ||||
|         .connection_verbose(true) | ||||
|         .tcp_keepalive(std::time::Duration::from_secs(600)) | ||||
|         .http1_only(); | ||||
|  | ||||
|     let zoo_token_env = std::env::var("ZOO_API_TOKEN"); | ||||
|  | ||||
|     let token = if let Some(token) = token { | ||||
|         token | ||||
|     } else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") { | ||||
|         if let Ok(zoo_token) = zoo_token_env { | ||||
|             if zoo_token != token { | ||||
|                 return Err(anyhow::anyhow!( | ||||
|                     "Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.", | ||||
|                     token, | ||||
|                     zoo_token | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
|         token | ||||
|     } else if let Ok(token) = zoo_token_env { | ||||
|         token | ||||
|     } else { | ||||
|         return Err(anyhow::anyhow!( | ||||
|             "No API token found in environment variables. Use KITTYCAD_API_TOKEN or ZOO_API_TOKEN" | ||||
|         )); | ||||
|     }; | ||||
|  | ||||
|     // Create the client. | ||||
|     let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client); | ||||
|     // Set an engine address if it's set. | ||||
|     let kittycad_host_env = std::env::var("KITTYCAD_HOST"); | ||||
|     if let Some(addr) = engine_addr { | ||||
|         client.set_base_url(addr); | ||||
|     } else if let Ok(addr) = std::env::var("ZOO_HOST") { | ||||
|         if let Ok(kittycad_host) = kittycad_host_env { | ||||
|             if kittycad_host != addr { | ||||
|                 return Err(anyhow::anyhow!( | ||||
|                     "Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.", | ||||
|                     kittycad_host, | ||||
|                     addr | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
|         client.set_base_url(addr); | ||||
|     } else if let Ok(addr) = kittycad_host_env { | ||||
|         client.set_base_url(addr); | ||||
|     } | ||||
|  | ||||
|     Ok(client) | ||||
| } | ||||
|  | ||||
| @ -445,6 +445,12 @@ pub struct ArtifactGraph { | ||||
|     map: IndexMap<ArtifactId, Artifact>, | ||||
| } | ||||
|  | ||||
| impl ArtifactGraph { | ||||
|     pub fn len(&self) -> usize { | ||||
|         self.map.len() | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub(super) fn build_artifact_graph( | ||||
|     artifact_commands: &[ArtifactCommand], | ||||
|     responses: &IndexMap<Uuid, WebSocketResponse>, | ||||
|  | ||||
| @ -1,23 +1,46 @@ | ||||
| //! Functions for helping with caching an ast and finding the parts the changed. | ||||
|  | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::sync::Arc; | ||||
|  | ||||
| use tokio::sync::RwLock; | ||||
|  | ||||
| use crate::{ | ||||
|     execution::ExecState, | ||||
|     execution::{ExecState, ExecutorSettings}, | ||||
|     parsing::ast::types::{Node, Program}, | ||||
|     walk::Node as WalkNode, | ||||
| }; | ||||
|  | ||||
| lazy_static::lazy_static! { | ||||
|     /// A static mutable lock for updating the last successful execution state for the cache. | ||||
|     static ref OLD_AST_MEMORY: Arc<RwLock<Option<OldAstState>>> = Default::default(); | ||||
| } | ||||
|  | ||||
| /// Read the old ast memory from the lock. | ||||
| pub(super) async fn read_old_ast_memory() -> Option<OldAstState> { | ||||
|     let old_ast = OLD_AST_MEMORY.read().await; | ||||
|     old_ast.clone() | ||||
| } | ||||
|  | ||||
| pub(super) async fn write_old_ast_memory(old_state: OldAstState) { | ||||
|     let mut old_ast = OLD_AST_MEMORY.write().await; | ||||
|     *old_ast = Some(old_state); | ||||
| } | ||||
|  | ||||
| pub async fn bust_cache() { | ||||
|     let mut old_ast = OLD_AST_MEMORY.write().await; | ||||
|     // Set the cache to None. | ||||
|     *old_ast = None; | ||||
| } | ||||
|  | ||||
| /// Information for the caching an AST and smartly re-executing it if we can. | ||||
| #[derive(Debug, Clone, Deserialize, Serialize)] | ||||
| pub struct CacheInformation { | ||||
|     /// The old information. | ||||
|     pub old: Option<OldAstState>, | ||||
|     /// The new ast to executed. | ||||
|     pub new_ast: Node<Program>, | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct CacheInformation<'a> { | ||||
|     pub ast: &'a Node<Program>, | ||||
|     pub settings: &'a ExecutorSettings, | ||||
| } | ||||
|  | ||||
| /// The old ast and program memory. | ||||
| #[derive(Debug, Clone, Deserialize, Serialize)] | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct OldAstState { | ||||
|     /// The ast. | ||||
|     pub ast: Node<Program>, | ||||
| @ -27,20 +50,420 @@ pub struct OldAstState { | ||||
|     pub settings: crate::execution::ExecutorSettings, | ||||
| } | ||||
|  | ||||
| impl From<crate::Program> for CacheInformation { | ||||
|     fn from(program: crate::Program) -> Self { | ||||
|         CacheInformation { | ||||
|             old: None, | ||||
|             new_ast: program.ast, | ||||
| /// The result of a cache check. | ||||
| #[derive(Debug, Clone, PartialEq)] | ||||
| #[allow(clippy::large_enum_variant)] | ||||
| pub(super) enum CacheResult { | ||||
|     ReExecute { | ||||
|         /// Should we clear the scene and start over? | ||||
|         clear_scene: bool, | ||||
|         /// Do we need to reapply settings? | ||||
|         reapply_settings: bool, | ||||
|         /// The program that needs to be executed. | ||||
|         program: Node<Program>, | ||||
|     }, | ||||
|     /// Argument is whether we need to reapply settings. | ||||
|     NoAction(bool), | ||||
| } | ||||
|  | ||||
| /// Given an old ast, old program memory and new ast, find the parts of the code that need to be | ||||
| /// re-executed. | ||||
| /// This function should never error, because in the case of any internal error, we should just pop | ||||
| /// the cache. | ||||
| /// | ||||
| /// Returns `None` when there are no changes to the program, i.e. it is | ||||
| /// fully cached. | ||||
| pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult { | ||||
|     let mut try_reapply_settings = false; | ||||
|  | ||||
|     // If the settings are different we might need to bust the cache. | ||||
|     // We specifically do this before checking if they are the exact same. | ||||
|     if old.settings != new.settings { | ||||
|         // If the units are different we need to re-execute the whole thing. | ||||
|         if old.settings.units != new.settings.units { | ||||
|             return CacheResult::ReExecute { | ||||
|                 clear_scene: true, | ||||
|                 reapply_settings: true, | ||||
|                 program: new.ast.clone(), | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         // If anything else is different we may not need to re-execute, but rather just | ||||
|         // run the settings again. | ||||
|         try_reapply_settings = true; | ||||
|     } | ||||
|  | ||||
|     // If the ASTs are the EXACT same we return None. | ||||
|     // We don't even need to waste time computing the digests. | ||||
|     if old.ast == new.ast { | ||||
|         return CacheResult::NoAction(try_reapply_settings); | ||||
|     } | ||||
|  | ||||
|     // We have to clone just because the digests are stored inline :-( | ||||
|     let mut old_ast = old.ast.clone(); | ||||
|     let mut new_ast = new.ast.clone(); | ||||
|  | ||||
|     // The digests should already be computed, but just in case we don't | ||||
|     // want to compare against none. | ||||
|     old_ast.compute_digest(); | ||||
|     new_ast.compute_digest(); | ||||
|  | ||||
|     // Check if the digest is the same. | ||||
|     if old_ast.digest == new_ast.digest { | ||||
|         return CacheResult::NoAction(try_reapply_settings); | ||||
|     } | ||||
|  | ||||
|     // Check if the changes were only to Non-code areas, like comments or whitespace. | ||||
|     let (clear_scene, program) = generate_changed_program(old_ast, new_ast); | ||||
|     CacheResult::ReExecute { | ||||
|         clear_scene, | ||||
|         reapply_settings: try_reapply_settings, | ||||
|         program, | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Force-generate a new CacheResult, even if one shouldn't be made. The | ||||
| /// way in which this gets invoked should always be through | ||||
| /// [get_changed_program]. This is purely to contain the logic on | ||||
| /// how we construct a new [CacheResult]. | ||||
| fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>) -> (bool, Node<Program>) { | ||||
|     if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| { | ||||
|         let old_node: WalkNode = old.into(); | ||||
|         let new_node: WalkNode = new.into(); | ||||
|         old_node.digest() == new_node.digest() | ||||
|     }) { | ||||
|         // If any of the nodes are different in the stretch of body that | ||||
|         // overlaps, we have to bust cache and rebuild the scene. This | ||||
|         // means a single insertion or deletion will result in a cache | ||||
|         // bust. | ||||
|  | ||||
|         return (true, new_ast); | ||||
|     } | ||||
|  | ||||
|     // otherwise the overlapping section of the ast bodies matches. | ||||
|     // Let's see what the rest of the slice looks like. | ||||
|  | ||||
|     match new_ast.body.len().cmp(&old_ast.body.len()) { | ||||
|         std::cmp::Ordering::Less => { | ||||
|             // the new AST is shorter than the old AST -- statements | ||||
|             // were removed from the "current" code in the "new" code. | ||||
|             // | ||||
|             // Statements up until now match which means this is a | ||||
|             // "pure delete" of the remaining slice, when we get to | ||||
|             // supporting that. | ||||
|  | ||||
|             // Cache bust time. | ||||
|             (true, new_ast) | ||||
|         } | ||||
|         std::cmp::Ordering::Greater => { | ||||
|             // the new AST is longer than the old AST, which means | ||||
|             // statements were added to the new code we haven't previously | ||||
|             // seen. | ||||
|             // | ||||
|             // Statements up until now are the same, which means this | ||||
|             // is a "pure addition" of the remaining slice. | ||||
|  | ||||
|             new_ast.body = new_ast.body[old_ast.body.len()..].to_owned(); | ||||
|  | ||||
|             (false, new_ast) | ||||
|         } | ||||
|         std::cmp::Ordering::Equal => { | ||||
|             // currently unreachable, but let's pretend like the code | ||||
|             // above can do something meaningful here for when we get | ||||
|             // to diffing and yanking chunks of the program apart. | ||||
|  | ||||
|             // We don't actually want to do anything here; so we're going | ||||
|             // to not clear and do nothing. Is this wrong? I don't think | ||||
|             // so but i think many things. This def needs to change | ||||
|             // when the code above changes. | ||||
|  | ||||
|             new_ast.body = vec![]; | ||||
|  | ||||
|             (false, new_ast) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// The result of a cache check. | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] | ||||
| pub struct CacheResult { | ||||
|     /// Should we clear the scene and start over? | ||||
|     pub clear_scene: bool, | ||||
|     /// The program that needs to be executed. | ||||
|     pub program: Node<Program>, | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|     use crate::execution::parse_execute; | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_get_changed_program_same_code() { | ||||
|         let new = r#"// Remove the end face for the extrusion. | ||||
| firstSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([-12, 12], %) | ||||
|   |> line(end = [24, 0]) | ||||
|   |> line(end = [0, -24]) | ||||
|   |> line(end = [-24, 0]) | ||||
|   |> close() | ||||
|   |> extrude(length = 6) | ||||
|  | ||||
| // Remove the end face for the extrusion. | ||||
| shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; | ||||
|  | ||||
|         let (program, ctx, _) = parse_execute(new).await.unwrap(); | ||||
|  | ||||
|         let result = get_changed_program( | ||||
|             CacheInformation { | ||||
|                 ast: &program.ast, | ||||
|                 settings: &ctx.settings, | ||||
|             }, | ||||
|             CacheInformation { | ||||
|                 ast: &program.ast, | ||||
|                 settings: &ctx.settings, | ||||
|             }, | ||||
|         ) | ||||
|         .await; | ||||
|  | ||||
|         assert_eq!(result, CacheResult::NoAction(false)); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_get_changed_program_same_code_changed_whitespace() { | ||||
|         let old = r#" // Remove the end face for the extrusion. | ||||
| firstSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([-12, 12], %) | ||||
|   |> line(end = [24, 0]) | ||||
|   |> line(end = [0, -24]) | ||||
|   |> line(end = [-24, 0]) | ||||
|   |> close() | ||||
|   |> extrude(length = 6) | ||||
|  | ||||
| // Remove the end face for the extrusion. | ||||
| shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#; | ||||
|  | ||||
|         let new = r#"// Remove the end face for the extrusion. | ||||
| firstSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([-12, 12], %) | ||||
|   |> line(end = [24, 0]) | ||||
|   |> line(end = [0, -24]) | ||||
|   |> line(end = [-24, 0]) | ||||
|   |> close() | ||||
|   |> extrude(length = 6) | ||||
|  | ||||
| // Remove the end face for the extrusion. | ||||
| shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; | ||||
|  | ||||
|         let (program_old, ctx, _) = parse_execute(old).await.unwrap(); | ||||
|  | ||||
|         let program_new = crate::Program::parse_no_errs(new).unwrap(); | ||||
|  | ||||
|         let result = get_changed_program( | ||||
|             CacheInformation { | ||||
|                 ast: &program_old.ast, | ||||
|                 settings: &ctx.settings, | ||||
|             }, | ||||
|             CacheInformation { | ||||
|                 ast: &program_new.ast, | ||||
|                 settings: &ctx.settings, | ||||
|             }, | ||||
|         ) | ||||
|         .await; | ||||
|  | ||||
|         assert_eq!(result, CacheResult::NoAction(false)); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() { | ||||
|         let old = r#" // Removed the end face for the extrusion. | ||||
| firstSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([-12, 12], %) | ||||
|   |> line(end = [24, 0]) | ||||
|   |> line(end = [0, -24]) | ||||
|   |> line(end = [-24, 0]) | ||||
|   |> close() | ||||
|   |> extrude(length = 6) | ||||
|  | ||||
| // Remove the end face for the extrusion. | ||||
| shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#; | ||||
|  | ||||
|         let new = r#"// Remove the end face for the extrusion. | ||||
| firstSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([-12, 12], %) | ||||
|   |> line(end = [24, 0]) | ||||
|   |> line(end = [0, -24]) | ||||
|   |> line(end = [-24, 0]) | ||||
|   |> close() | ||||
|   |> extrude(length = 6) | ||||
|  | ||||
| // Remove the end face for the extrusion. | ||||
| shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; | ||||
|  | ||||
|         let (program, ctx, _) = parse_execute(old).await.unwrap(); | ||||
|  | ||||
|         let program_new = crate::Program::parse_no_errs(new).unwrap(); | ||||
|  | ||||
|         let result = get_changed_program( | ||||
|             CacheInformation { | ||||
|                 ast: &program.ast, | ||||
|                 settings: &ctx.settings, | ||||
|             }, | ||||
|             CacheInformation { | ||||
|                 ast: &program_new.ast, | ||||
|                 settings: &ctx.settings, | ||||
|             }, | ||||
|         ) | ||||
|         .await; | ||||
|  | ||||
|         assert_eq!(result, CacheResult::NoAction(false)); | ||||
|     } | ||||
|  | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_get_changed_program_same_code_changed_code_comments() { | ||||
|         let old = r#" // Removed the end face for the extrusion. | ||||
| firstSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([-12, 12], %) | ||||
|   |> line(end = [24, 0]) | ||||
|   |> line(end = [0, -24]) | ||||
|   |> line(end = [-24, 0]) // my thing | ||||
|   |> close() | ||||
|   |> extrude(length = 6) | ||||
|  | ||||
| // Remove the end face for the extrusion. | ||||
| shell({ faces = ['end'], thickness = 0.25 }, firstSketch) "#; | ||||
|  | ||||
|         let new = r#"// Remove the end face for the extrusion. | ||||
| firstSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([-12, 12], %) | ||||
|   |> line(end = [24, 0]) | ||||
|   |> line(end = [0, -24]) | ||||
|   |> line(end = [-24, 0]) | ||||
|   |> close() | ||||
|   |> extrude(length = 6) | ||||
|  | ||||
| // Remove the end face for the extrusion. | ||||
| shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; | ||||
|  | ||||
|         let (program, ctx, _) = parse_execute(old).await.unwrap(); | ||||
|  | ||||
|         let program_new = crate::Program::parse_no_errs(new).unwrap(); | ||||
|  | ||||
|         let result = get_changed_program( | ||||
|             CacheInformation { | ||||
|                 ast: &program.ast, | ||||
|                 settings: &ctx.settings, | ||||
|             }, | ||||
|             CacheInformation { | ||||
|                 ast: &program_new.ast, | ||||
|                 settings: &ctx.settings, | ||||
|             }, | ||||
|         ) | ||||
|         .await; | ||||
|  | ||||
|         assert_eq!(result, CacheResult::NoAction(false)); | ||||
|     } | ||||
|  | ||||
|     // Changing the units with the exact same file should bust the cache. | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_get_changed_program_same_code_but_different_units() { | ||||
|         let new = r#"// Remove the end face for the extrusion. | ||||
| firstSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([-12, 12], %) | ||||
|   |> line(end = [24, 0]) | ||||
|   |> line(end = [0, -24]) | ||||
|   |> line(end = [-24, 0]) | ||||
|   |> close() | ||||
|   |> extrude(length = 6) | ||||
|  | ||||
| // Remove the end face for the extrusion. | ||||
| shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; | ||||
|  | ||||
|         let (program, mut ctx, _) = parse_execute(new).await.unwrap(); | ||||
|  | ||||
|         // Change the settings to cm. | ||||
|         ctx.settings.units = crate::UnitLength::Cm; | ||||
|  | ||||
|         let result = get_changed_program( | ||||
|             CacheInformation { | ||||
|                 ast: &program.ast, | ||||
|                 settings: &Default::default(), | ||||
|             }, | ||||
|             CacheInformation { | ||||
|                 ast: &program.ast, | ||||
|                 settings: &ctx.settings, | ||||
|             }, | ||||
|         ) | ||||
|         .await; | ||||
|  | ||||
|         assert_eq!( | ||||
|             result, | ||||
|             CacheResult::ReExecute { | ||||
|                 clear_scene: true, | ||||
|                 reapply_settings: true, | ||||
|                 program: program.ast | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     // Changing the grid settings with the exact same file should NOT bust the cache. | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_get_changed_program_same_code_but_different_grid_setting() { | ||||
|         let new = r#"// Remove the end face for the extrusion. | ||||
| firstSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([-12, 12], %) | ||||
|   |> line(end = [24, 0]) | ||||
|   |> line(end = [0, -24]) | ||||
|   |> line(end = [-24, 0]) | ||||
|   |> close() | ||||
|   |> extrude(length = 6) | ||||
|  | ||||
| // Remove the end face for the extrusion. | ||||
| shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; | ||||
|  | ||||
|         let (program, mut ctx, _) = parse_execute(new).await.unwrap(); | ||||
|  | ||||
|         // Change the settings. | ||||
|         ctx.settings.show_grid = !ctx.settings.show_grid; | ||||
|  | ||||
|         let result = get_changed_program( | ||||
|             CacheInformation { | ||||
|                 ast: &program.ast, | ||||
|                 settings: &Default::default(), | ||||
|             }, | ||||
|             CacheInformation { | ||||
|                 ast: &program.ast, | ||||
|                 settings: &ctx.settings, | ||||
|             }, | ||||
|         ) | ||||
|         .await; | ||||
|  | ||||
|         assert_eq!(result, CacheResult::NoAction(true)); | ||||
|     } | ||||
|  | ||||
|     // Changing the edge visibility settings with the exact same file should NOT bust the cache. | ||||
|     #[tokio::test(flavor = "multi_thread")] | ||||
|     async fn test_get_changed_program_same_code_but_different_edge_visiblity_setting() { | ||||
|         let new = r#"// Remove the end face for the extrusion. | ||||
| firstSketch = startSketchOn('XY') | ||||
|   |> startProfileAt([-12, 12], %) | ||||
|   |> line(end = [24, 0]) | ||||
|   |> line(end = [0, -24]) | ||||
|   |> line(end = [-24, 0]) | ||||
|   |> close() | ||||
|   |> extrude(length = 6) | ||||
|  | ||||
| // Remove the end face for the extrusion. | ||||
| shell({ faces = ['end'], thickness = 0.25 }, firstSketch)"#; | ||||
|  | ||||
|         let (program, mut ctx, _) = parse_execute(new).await.unwrap(); | ||||
|  | ||||
|         // Change the settings. | ||||
|         ctx.settings.highlight_edges = !ctx.settings.highlight_edges; | ||||
|  | ||||
|         let result = get_changed_program( | ||||
|             CacheInformation { | ||||
|                 ast: &program.ast, | ||||
|                 settings: &Default::default(), | ||||
|             }, | ||||
|             CacheInformation { | ||||
|                 ast: &program.ast, | ||||
|                 settings: &ctx.settings, | ||||
|             }, | ||||
|         ) | ||||
|         .await; | ||||
|  | ||||
|         assert_eq!(result, CacheResult::NoAction(true)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,25 +1,460 @@ | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use async_recursion::async_recursion; | ||||
| use schemars::JsonSchema; | ||||
|  | ||||
| use super::cad_op::{OpArg, Operation}; | ||||
| use crate::{ | ||||
|     engine::ExecutionKind, | ||||
|     errors::{KclError, KclErrorDetails}, | ||||
|     execution::{ | ||||
|         BodyType, ExecState, ExecutorContext, KclValue, Metadata, StatementKind, TagEngineInfo, TagIdentifier, | ||||
|         annotations, | ||||
|         cad_op::{OpArg, Operation}, | ||||
|         state::ModuleState, | ||||
|         BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ModuleRepr, ProgramMemory, | ||||
|         TagEngineInfo, TagIdentifier, | ||||
|     }, | ||||
|     fs::FileSystem, | ||||
|     parsing::ast::types::{ | ||||
|         ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, CallExpression, | ||||
|         CallExpressionKw, Expr, IfExpression, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, | ||||
|         ObjectExpression, PipeExpression, TagDeclarator, UnaryExpression, UnaryOperator, | ||||
|         ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, | ||||
|         CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility, | ||||
|         LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, NodeRef, NonCodeValue, ObjectExpression, | ||||
|         PipeExpression, TagDeclarator, UnaryExpression, UnaryOperator, | ||||
|     }, | ||||
|     source_range::SourceRange, | ||||
|     source_range::{ModuleId, SourceRange}, | ||||
|     std::{ | ||||
|         args::{Arg, KwArgs}, | ||||
|         FunctionKind, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| enum StatementKind<'a> { | ||||
|     Declaration { name: &'a str }, | ||||
|     Expression, | ||||
| } | ||||
|  | ||||
| impl ExecutorContext { | ||||
|     async fn handle_annotations( | ||||
|         &self, | ||||
|         annotations: impl Iterator<Item = (&NonCodeValue, SourceRange)>, | ||||
|         scope: annotations::AnnotationScope, | ||||
|         exec_state: &mut ExecState, | ||||
|     ) -> Result<(), KclError> { | ||||
|         for (annotation, source_range) in annotations { | ||||
|             if annotation.annotation_name() == Some(annotations::SETTINGS) { | ||||
|                 if scope == annotations::AnnotationScope::Module { | ||||
|                     let old_units = exec_state.length_unit(); | ||||
|                     exec_state | ||||
|                         .mod_local | ||||
|                         .settings | ||||
|                         .update_from_annotation(annotation, source_range)?; | ||||
|                     let new_units = exec_state.length_unit(); | ||||
|                     if old_units != new_units { | ||||
|                         self.engine.set_units(new_units.into(), source_range).await?; | ||||
|                     } | ||||
|                 } else { | ||||
|                     return Err(KclError::Semantic(KclErrorDetails { | ||||
|                         message: "Settings can only be modified at the top level scope of a file".to_owned(), | ||||
|                         source_ranges: vec![source_range], | ||||
|                     })); | ||||
|                 } | ||||
|             } | ||||
|             // TODO warn on unknown annotations | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Execute an AST's program. | ||||
|     #[async_recursion] | ||||
|     pub(super) async fn exec_program<'a>( | ||||
|         &'a self, | ||||
|         program: NodeRef<'a, crate::parsing::ast::types::Program>, | ||||
|         exec_state: &mut ExecState, | ||||
|         body_type: BodyType, | ||||
|     ) -> Result<Option<KclValue>, KclError> { | ||||
|         self.handle_annotations( | ||||
|             program | ||||
|                 .non_code_meta | ||||
|                 .start_nodes | ||||
|                 .iter() | ||||
|                 .filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))), | ||||
|             annotations::AnnotationScope::Module, | ||||
|             exec_state, | ||||
|         ) | ||||
|         .await?; | ||||
|  | ||||
|         let mut last_expr = None; | ||||
|         // Iterate over the body of the program. | ||||
|         for statement in &program.body { | ||||
|             match statement { | ||||
|                 BodyItem::ImportStatement(import_stmt) => { | ||||
|                     let source_range = SourceRange::from(import_stmt); | ||||
|                     let module_id = self.open_module(&import_stmt.path, exec_state, source_range).await?; | ||||
|  | ||||
|                     match &import_stmt.selector { | ||||
|                         ImportSelector::List { items } => { | ||||
|                             let (_, module_memory, module_exports) = self | ||||
|                                 .exec_module(module_id, exec_state, ExecutionKind::Isolated, source_range) | ||||
|                                 .await?; | ||||
|                             for import_item in items { | ||||
|                                 // Extract the item from the module. | ||||
|                                 let item = | ||||
|                                     module_memory | ||||
|                                         .get(&import_item.name.name, import_item.into()) | ||||
|                                         .map_err(|_err| { | ||||
|                                             KclError::UndefinedValue(KclErrorDetails { | ||||
|                                                 message: format!("{} is not defined in module", import_item.name.name), | ||||
|                                                 source_ranges: vec![SourceRange::from(&import_item.name)], | ||||
|                                             }) | ||||
|                                         })?; | ||||
|                                 // Check that the item is allowed to be imported. | ||||
|                                 if !module_exports.contains(&import_item.name.name) { | ||||
|                                     return Err(KclError::Semantic(KclErrorDetails { | ||||
|                                         message: format!( | ||||
|                                             "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.", | ||||
|                                             import_item.name.name | ||||
|                                         ), | ||||
|                                         source_ranges: vec![SourceRange::from(&import_item.name)], | ||||
|                                     })); | ||||
|                                 } | ||||
|  | ||||
|                                 // Add the item to the current module. | ||||
|                                 exec_state.mut_memory().add( | ||||
|                                     import_item.identifier(), | ||||
|                                     item.clone(), | ||||
|                                     SourceRange::from(&import_item.name), | ||||
|                                 )?; | ||||
|  | ||||
|                                 if let ItemVisibility::Export = import_stmt.visibility { | ||||
|                                     exec_state | ||||
|                                         .mod_local | ||||
|                                         .module_exports | ||||
|                                         .push(import_item.identifier().to_owned()); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         ImportSelector::Glob(_) => { | ||||
|                             let (_, module_memory, module_exports) = self | ||||
|                                 .exec_module(module_id, exec_state, ExecutionKind::Isolated, source_range) | ||||
|                                 .await?; | ||||
|                             for name in module_exports.iter() { | ||||
|                                 let item = module_memory.get(name, source_range).map_err(|_err| { | ||||
|                                     KclError::Internal(KclErrorDetails { | ||||
|                                         message: format!("{} is not defined in module (but was exported?)", name), | ||||
|                                         source_ranges: vec![source_range], | ||||
|                                     }) | ||||
|                                 })?; | ||||
|                                 exec_state.mut_memory().add(name, item.clone(), source_range)?; | ||||
|  | ||||
|                                 if let ItemVisibility::Export = import_stmt.visibility { | ||||
|                                     exec_state.mod_local.module_exports.push(name.clone()); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         ImportSelector::None { .. } => { | ||||
|                             let name = import_stmt.module_name().unwrap(); | ||||
|                             let item = KclValue::Module { | ||||
|                                 value: module_id, | ||||
|                                 meta: vec![source_range.into()], | ||||
|                             }; | ||||
|                             exec_state.mut_memory().add(&name, item, source_range)?; | ||||
|                         } | ||||
|                     } | ||||
|                     last_expr = None; | ||||
|                 } | ||||
|                 BodyItem::ExpressionStatement(expression_statement) => { | ||||
|                     let metadata = Metadata::from(expression_statement); | ||||
|                     last_expr = Some( | ||||
|                         self.execute_expr( | ||||
|                             &expression_statement.expression, | ||||
|                             exec_state, | ||||
|                             &metadata, | ||||
|                             StatementKind::Expression, | ||||
|                         ) | ||||
|                         .await?, | ||||
|                     ); | ||||
|                 } | ||||
|                 BodyItem::VariableDeclaration(variable_declaration) => { | ||||
|                     let var_name = variable_declaration.declaration.id.name.to_string(); | ||||
|                     let source_range = SourceRange::from(&variable_declaration.declaration.init); | ||||
|                     let metadata = Metadata { source_range }; | ||||
|  | ||||
|                     let memory_item = self | ||||
|                         .execute_expr( | ||||
|                             &variable_declaration.declaration.init, | ||||
|                             exec_state, | ||||
|                             &metadata, | ||||
|                             StatementKind::Declaration { name: &var_name }, | ||||
|                         ) | ||||
|                         .await?; | ||||
|                     exec_state.mut_memory().add(&var_name, memory_item, source_range)?; | ||||
|  | ||||
|                     // Track exports. | ||||
|                     if let ItemVisibility::Export = variable_declaration.visibility { | ||||
|                         exec_state.mod_local.module_exports.push(var_name); | ||||
|                     } | ||||
|                     last_expr = None; | ||||
|                 } | ||||
|                 BodyItem::ReturnStatement(return_statement) => { | ||||
|                     let metadata = Metadata::from(return_statement); | ||||
|                     let value = self | ||||
|                         .execute_expr( | ||||
|                             &return_statement.argument, | ||||
|                             exec_state, | ||||
|                             &metadata, | ||||
|                             StatementKind::Expression, | ||||
|                         ) | ||||
|                         .await?; | ||||
|                     exec_state.mut_memory().return_ = Some(value); | ||||
|                     last_expr = None; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if BodyType::Root == body_type { | ||||
|             // Flush the batch queue. | ||||
|             self.engine | ||||
|                 .flush_batch( | ||||
|                     // True here tells the engine to flush all the end commands as well like fillets | ||||
|                     // and chamfers where the engine would otherwise eat the ID of the segments. | ||||
|                     true, | ||||
|                     SourceRange::new(program.end, program.end, program.module_id), | ||||
|                 ) | ||||
|                 .await?; | ||||
|         } | ||||
|  | ||||
|         Ok(last_expr) | ||||
|     } | ||||
|  | ||||
|     async fn open_module( | ||||
|         &self, | ||||
|         path: &ImportPath, | ||||
|         exec_state: &mut ExecState, | ||||
|         source_range: SourceRange, | ||||
|     ) -> Result<ModuleId, KclError> { | ||||
|         match path { | ||||
|             ImportPath::Kcl { filename } => { | ||||
|                 let resolved_path = if let Some(project_dir) = &self.settings.project_directory { | ||||
|                     project_dir.join(filename) | ||||
|                 } else { | ||||
|                     std::path::PathBuf::from(filename) | ||||
|                 }; | ||||
|  | ||||
|                 if exec_state.mod_local.import_stack.contains(&resolved_path) { | ||||
|                     return Err(KclError::ImportCycle(KclErrorDetails { | ||||
|                         message: format!( | ||||
|                             "circular import of modules is not allowed: {} -> {}", | ||||
|                             exec_state | ||||
|                                 .mod_local | ||||
|                                 .import_stack | ||||
|                                 .iter() | ||||
|                                 .map(|p| p.as_path().to_string_lossy()) | ||||
|                                 .collect::<Vec<_>>() | ||||
|                                 .join(" -> "), | ||||
|                             resolved_path.to_string_lossy() | ||||
|                         ), | ||||
|                         source_ranges: vec![source_range], | ||||
|                     })); | ||||
|                 } | ||||
|  | ||||
|                 if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) { | ||||
|                     return Ok(*id); | ||||
|                 } | ||||
|  | ||||
|                 let source = self.fs.read_to_string(&resolved_path, source_range).await?; | ||||
|                 let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len()); | ||||
|                 // TODO handle parsing errors properly | ||||
|                 let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?; | ||||
|                 let repr = ModuleRepr::Kcl(parsed); | ||||
|  | ||||
|                 Ok(exec_state.add_module(id, resolved_path, repr)) | ||||
|             } | ||||
|             ImportPath::Foreign { path } => { | ||||
|                 let resolved_path = if let Some(project_dir) = &self.settings.project_directory { | ||||
|                     project_dir.join(path) | ||||
|                 } else { | ||||
|                     std::path::PathBuf::from(path) | ||||
|                 }; | ||||
|  | ||||
|                 if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) { | ||||
|                     return Ok(*id); | ||||
|                 } | ||||
|  | ||||
|                 let geom = super::import::import_foreign(&resolved_path, None, exec_state, self, source_range).await?; | ||||
|                 let repr = ModuleRepr::Foreign(geom); | ||||
|                 let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len()); | ||||
|                 Ok(exec_state.add_module(id, resolved_path, repr)) | ||||
|             } | ||||
|             i => Err(KclError::Semantic(KclErrorDetails { | ||||
|                 message: format!("Unsupported import: `{i}`"), | ||||
|                 source_ranges: vec![source_range], | ||||
|             })), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async fn exec_module( | ||||
|         &self, | ||||
|         module_id: ModuleId, | ||||
|         exec_state: &mut ExecState, | ||||
|         exec_kind: ExecutionKind, | ||||
|         source_range: SourceRange, | ||||
|     ) -> Result<(Option<KclValue>, ProgramMemory, Vec<String>), KclError> { | ||||
|         let old_units = exec_state.length_unit(); | ||||
|         // TODO It sucks that we have to clone the whole module AST here | ||||
|         let info = exec_state.global.module_infos[&module_id].clone(); | ||||
|  | ||||
|         match &info.repr { | ||||
|             ModuleRepr::Root => Err(KclError::ImportCycle(KclErrorDetails { | ||||
|                 message: format!( | ||||
|                     "circular import of modules is not allowed: {} -> {}", | ||||
|                     exec_state | ||||
|                         .mod_local | ||||
|                         .import_stack | ||||
|                         .iter() | ||||
|                         .map(|p| p.as_path().to_string_lossy()) | ||||
|                         .collect::<Vec<_>>() | ||||
|                         .join(" -> "), | ||||
|                     info.path.display() | ||||
|                 ), | ||||
|                 source_ranges: vec![source_range], | ||||
|             })), | ||||
|             ModuleRepr::Kcl(program) => { | ||||
|                 let mut local_state = ModuleState { | ||||
|                     import_stack: exec_state.mod_local.import_stack.clone(), | ||||
|                     ..ModuleState::new(&self.settings) | ||||
|                 }; | ||||
|                 local_state.import_stack.push(info.path.clone()); | ||||
|                 std::mem::swap(&mut exec_state.mod_local, &mut local_state); | ||||
|                 let original_execution = self.engine.replace_execution_kind(exec_kind); | ||||
|  | ||||
|                 let result = self | ||||
|                     .exec_program(program, exec_state, crate::execution::BodyType::Root) | ||||
|                     .await; | ||||
|  | ||||
|                 let new_units = exec_state.length_unit(); | ||||
|                 std::mem::swap(&mut exec_state.mod_local, &mut local_state); | ||||
|                 if new_units != old_units { | ||||
|                     self.engine.set_units(old_units.into(), Default::default()).await?; | ||||
|                 } | ||||
|                 self.engine.replace_execution_kind(original_execution); | ||||
|  | ||||
|                 let result = result.map_err(|err| { | ||||
|                     if let KclError::ImportCycle(_) = err { | ||||
|                         // It was an import cycle.  Keep the original message. | ||||
|                         err.override_source_ranges(vec![source_range]) | ||||
|                     } else { | ||||
|                         KclError::Semantic(KclErrorDetails { | ||||
|                             message: format!( | ||||
|                                 "Error loading imported file. Open it to view more details. {}: {}", | ||||
|                                 info.path.display(), | ||||
|                                 err.message() | ||||
|                             ), | ||||
|                             source_ranges: vec![source_range], | ||||
|                         }) | ||||
|                     } | ||||
|                 })?; | ||||
|  | ||||
|                 Ok((result, local_state.memory, local_state.module_exports)) | ||||
|             } | ||||
|             ModuleRepr::Foreign(geom) => { | ||||
|                 let geom = super::import::send_to_engine(geom.clone(), self).await?; | ||||
|                 Ok((Some(KclValue::ImportedGeometry(geom)), ProgramMemory::new(), Vec::new())) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[async_recursion] | ||||
|     async fn execute_expr<'a: 'async_recursion>( | ||||
|         &self, | ||||
|         init: &Expr, | ||||
|         exec_state: &mut ExecState, | ||||
|         metadata: &Metadata, | ||||
|         statement_kind: StatementKind<'a>, | ||||
|     ) -> Result<KclValue, KclError> { | ||||
|         let item = match init { | ||||
|             Expr::None(none) => KclValue::from(none), | ||||
|             Expr::Literal(literal) => KclValue::from(literal), | ||||
|             Expr::TagDeclarator(tag) => tag.execute(exec_state).await?, | ||||
|             Expr::Identifier(identifier) => { | ||||
|                 let value = exec_state.memory().get(&identifier.name, identifier.into())?.clone(); | ||||
|                 if let KclValue::Module { value: module_id, meta } = value { | ||||
|                     let (result, _, _) = self | ||||
|                         .exec_module(module_id, exec_state, ExecutionKind::Normal, metadata.source_range) | ||||
|                         .await?; | ||||
|                     result.unwrap_or_else(|| { | ||||
|                         // The module didn't have a return value.  Currently, | ||||
|                         // the only way to have a return value is with the final | ||||
|                         // statement being an expression statement. | ||||
|                         // | ||||
|                         // TODO: Make a warning when we support them in the | ||||
|                         // execution phase. | ||||
|                         let mut new_meta = vec![metadata.to_owned()]; | ||||
|                         new_meta.extend(meta); | ||||
|                         KclValue::KclNone { | ||||
|                             value: Default::default(), | ||||
|                             meta: new_meta, | ||||
|                         } | ||||
|                     }) | ||||
|                 } else { | ||||
|                     value | ||||
|                 } | ||||
|             } | ||||
|             Expr::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, self).await?, | ||||
|             Expr::FunctionExpression(function_expression) => { | ||||
|                 // Cloning memory here is crucial for semantics so that we close | ||||
|                 // over variables.  Variables defined lexically later shouldn't | ||||
|                 // be available to the function body. | ||||
|                 KclValue::Function { | ||||
|                     expression: function_expression.clone(), | ||||
|                     meta: vec![metadata.to_owned()], | ||||
|                     func: None, | ||||
|                     memory: Box::new(exec_state.memory().clone()), | ||||
|                 } | ||||
|             } | ||||
|             Expr::CallExpression(call_expression) => call_expression.execute(exec_state, self).await?, | ||||
|             Expr::CallExpressionKw(call_expression) => call_expression.execute(exec_state, self).await?, | ||||
|             Expr::PipeExpression(pipe_expression) => pipe_expression.get_result(exec_state, self).await?, | ||||
|             Expr::PipeSubstitution(pipe_substitution) => match statement_kind { | ||||
|                 StatementKind::Declaration { name } => { | ||||
|                     let message = format!( | ||||
|                         "you cannot declare variable {name} as %, because % can only be used in function calls" | ||||
|                     ); | ||||
|  | ||||
|                     return Err(KclError::Semantic(KclErrorDetails { | ||||
|                         message, | ||||
|                         source_ranges: vec![pipe_substitution.into()], | ||||
|                     })); | ||||
|                 } | ||||
|                 StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() { | ||||
|                     Some(x) => x, | ||||
|                     None => { | ||||
|                         return Err(KclError::Semantic(KclErrorDetails { | ||||
|                             message: "cannot use % outside a pipe expression".to_owned(), | ||||
|                             source_ranges: vec![pipe_substitution.into()], | ||||
|                         })); | ||||
|                     } | ||||
|                 }, | ||||
|             }, | ||||
|             Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?, | ||||
|             Expr::ArrayRangeExpression(range_expression) => range_expression.execute(exec_state, self).await?, | ||||
|             Expr::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?, | ||||
|             Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?, | ||||
|             Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?, | ||||
|             Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?, | ||||
|             Expr::LabelledExpression(expr) => { | ||||
|                 let result = self | ||||
|                     .execute_expr(&expr.expr, exec_state, metadata, statement_kind) | ||||
|                     .await?; | ||||
|                 exec_state | ||||
|                     .mut_memory() | ||||
|                     .add(&expr.label.name, result.clone(), init.into())?; | ||||
|                 // TODO this lets us use the label as a variable name, but not as a tag in most cases | ||||
|                 result | ||||
|             } | ||||
|         }; | ||||
|         Ok(item) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl BinaryPart { | ||||
|     #[async_recursion] | ||||
|     pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { | ||||
| @ -862,7 +1297,7 @@ impl Node<IfExpression> { | ||||
|             .await? | ||||
|             .get_bool()?; | ||||
|         if cond { | ||||
|             let block_result = ctx.inner_execute(&self.then_val, exec_state, BodyType::Block).await?; | ||||
|             let block_result = ctx.exec_program(&self.then_val, exec_state, BodyType::Block).await?; | ||||
|             // Block must end in an expression, so this has to be Some. | ||||
|             // Enforced by the parser. | ||||
|             // See https://github.com/KittyCAD/modeling-app/issues/4015 | ||||
| @ -881,9 +1316,7 @@ impl Node<IfExpression> { | ||||
|                 .await? | ||||
|                 .get_bool()?; | ||||
|             if cond { | ||||
|                 let block_result = ctx | ||||
|                     .inner_execute(&else_if.then_val, exec_state, BodyType::Block) | ||||
|                     .await?; | ||||
|                 let block_result = ctx.exec_program(&else_if.then_val, exec_state, BodyType::Block).await?; | ||||
|                 // Block must end in an expression, so this has to be Some. | ||||
|                 // Enforced by the parser. | ||||
|                 // See https://github.com/KittyCAD/modeling-app/issues/4015 | ||||
| @ -892,7 +1325,7 @@ impl Node<IfExpression> { | ||||
|         } | ||||
|  | ||||
|         // Run the final `else` branch. | ||||
|         ctx.inner_execute(&self.final_else, exec_state, BodyType::Block) | ||||
|         ctx.exec_program(&self.final_else, exec_state, BodyType::Block) | ||||
|             .await | ||||
|             .map(|expr| expr.unwrap()) | ||||
|     } | ||||
| @ -1000,3 +1433,343 @@ impl Node<PipeExpression> { | ||||
|         execute_pipe_body(exec_state, &self.body, self.into(), ctx).await | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// For each argument given, | ||||
| /// assign it to a parameter of the function, in the given block of function memory. | ||||
| /// Returns Err if too few/too many arguments were given for the function. | ||||
| fn assign_args_to_params( | ||||
|     function_expression: NodeRef<'_, FunctionExpression>, | ||||
|     args: Vec<Arg>, | ||||
|     mut fn_memory: ProgramMemory, | ||||
| ) -> Result<ProgramMemory, KclError> { | ||||
|     let num_args = function_expression.number_of_args(); | ||||
|     let (min_params, max_params) = num_args.into_inner(); | ||||
|     let n = args.len(); | ||||
|  | ||||
|     // Check if the user supplied too many arguments | ||||
|     // (we'll check for too few arguments below). | ||||
|     let err_wrong_number_args = KclError::Semantic(KclErrorDetails { | ||||
|         message: if min_params == max_params { | ||||
|             format!("Expected {min_params} arguments, got {n}") | ||||
|         } else { | ||||
|             format!("Expected {min_params}-{max_params} arguments, got {n}") | ||||
|         }, | ||||
|         source_ranges: vec![function_expression.into()], | ||||
|     }); | ||||
|     if n > max_params { | ||||
|         return Err(err_wrong_number_args); | ||||
|     } | ||||
|  | ||||
|     // Add the arguments to the memory.  A new call frame should have already | ||||
|     // been created. | ||||
|     for (index, param) in function_expression.params.iter().enumerate() { | ||||
|         if let Some(arg) = args.get(index) { | ||||
|             // Argument was provided. | ||||
|             fn_memory.add(¶m.identifier.name, arg.value.clone(), (¶m.identifier).into())?; | ||||
|         } else { | ||||
|             // Argument was not provided. | ||||
|             if let Some(ref default_val) = param.default_value { | ||||
|                 // If the corresponding parameter is optional, | ||||
|                 // then it's fine, the user doesn't need to supply it. | ||||
|                 fn_memory.add( | ||||
|                     ¶m.identifier.name, | ||||
|                     default_val.clone().into(), | ||||
|                     (¶m.identifier).into(), | ||||
|                 )?; | ||||
|             } else { | ||||
|                 // But if the corresponding parameter was required, | ||||
|                 // then the user has called with too few arguments. | ||||
|                 return Err(err_wrong_number_args); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Ok(fn_memory) | ||||
| } | ||||
|  | ||||
| fn assign_args_to_params_kw( | ||||
|     function_expression: NodeRef<'_, FunctionExpression>, | ||||
|     mut args: crate::std::args::KwArgs, | ||||
|     mut fn_memory: ProgramMemory, | ||||
| ) -> Result<ProgramMemory, KclError> { | ||||
|     // Add the arguments to the memory.  A new call frame should have already | ||||
|     // been created. | ||||
|     let source_ranges = vec![function_expression.into()]; | ||||
|     for param in function_expression.params.iter() { | ||||
|         if param.labeled { | ||||
|             let arg = args.labeled.get(¶m.identifier.name); | ||||
|             let arg_val = match arg { | ||||
|                 Some(arg) => arg.value.clone(), | ||||
|                 None => match param.default_value { | ||||
|                     Some(ref default_val) => KclValue::from(default_val.clone()), | ||||
|                     None => { | ||||
|                         return Err(KclError::Semantic(KclErrorDetails { | ||||
|                             source_ranges, | ||||
|                             message: format!( | ||||
|                                 "This function requires a parameter {}, but you haven't passed it one.", | ||||
|                                 param.identifier.name | ||||
|                             ), | ||||
|                         })); | ||||
|                     } | ||||
|                 }, | ||||
|             }; | ||||
|             fn_memory.add(¶m.identifier.name, arg_val, (¶m.identifier).into())?; | ||||
|         } else { | ||||
|             let Some(unlabeled) = args.unlabeled.take() else { | ||||
|                 let param_name = ¶m.identifier.name; | ||||
|                 return Err(if args.labeled.contains_key(param_name) { | ||||
|                     KclError::Semantic(KclErrorDetails { | ||||
|                         source_ranges, | ||||
|                         message: format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"), | ||||
|                     }) | ||||
|                 } else { | ||||
|                     KclError::Semantic(KclErrorDetails { | ||||
|                         source_ranges, | ||||
|                         message: "This function expects an unlabeled first parameter, but you haven't passed it one." | ||||
|                             .to_owned(), | ||||
|                     }) | ||||
|                 }); | ||||
|             }; | ||||
|             fn_memory.add( | ||||
|                 ¶m.identifier.name, | ||||
|                 unlabeled.value.clone(), | ||||
|                 (¶m.identifier).into(), | ||||
|             )?; | ||||
|         } | ||||
|     } | ||||
|     Ok(fn_memory) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn call_user_defined_function( | ||||
|     args: Vec<Arg>, | ||||
|     memory: &ProgramMemory, | ||||
|     function_expression: NodeRef<'_, FunctionExpression>, | ||||
|     exec_state: &mut ExecState, | ||||
|     ctx: &ExecutorContext, | ||||
| ) -> Result<Option<KclValue>, KclError> { | ||||
|     // Create a new environment to execute the function body in so that local | ||||
|     // variables shadow variables in the parent scope.  The new environment's | ||||
|     // parent should be the environment of the closure. | ||||
|     let mut body_memory = memory.clone(); | ||||
|     let body_env = body_memory.new_env_for_call(memory.current_env); | ||||
|     body_memory.current_env = body_env; | ||||
|     let fn_memory = assign_args_to_params(function_expression, args, body_memory)?; | ||||
|  | ||||
|     // Execute the function body using the memory we just created. | ||||
|     let (result, fn_memory) = { | ||||
|         let previous_memory = std::mem::replace(&mut exec_state.mod_local.memory, fn_memory); | ||||
|         let result = ctx | ||||
|             .exec_program(&function_expression.body, exec_state, BodyType::Block) | ||||
|             .await; | ||||
|         // Restore the previous memory. | ||||
|         let fn_memory = std::mem::replace(&mut exec_state.mod_local.memory, previous_memory); | ||||
|  | ||||
|         (result, fn_memory) | ||||
|     }; | ||||
|  | ||||
|     result.map(|_| fn_memory.return_) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn call_user_defined_function_kw( | ||||
|     args: crate::std::args::KwArgs, | ||||
|     memory: &ProgramMemory, | ||||
|     function_expression: NodeRef<'_, FunctionExpression>, | ||||
|     exec_state: &mut ExecState, | ||||
|     ctx: &ExecutorContext, | ||||
| ) -> Result<Option<KclValue>, KclError> { | ||||
|     // Create a new environment to execute the function body in so that local | ||||
|     // variables shadow variables in the parent scope.  The new environment's | ||||
|     // parent should be the environment of the closure. | ||||
|     let mut body_memory = memory.clone(); | ||||
|     let body_env = body_memory.new_env_for_call(memory.current_env); | ||||
|     body_memory.current_env = body_env; | ||||
|     let fn_memory = assign_args_to_params_kw(function_expression, args, body_memory)?; | ||||
|  | ||||
|     // Execute the function body using the memory we just created. | ||||
|     let (result, fn_memory) = { | ||||
|         let previous_memory = std::mem::replace(&mut exec_state.mod_local.memory, fn_memory); | ||||
|         let result = ctx | ||||
|             .exec_program(&function_expression.body, exec_state, BodyType::Block) | ||||
|             .await; | ||||
|         // Restore the previous memory. | ||||
|         let fn_memory = std::mem::replace(&mut exec_state.mod_local.memory, previous_memory); | ||||
|  | ||||
|         (result, fn_memory) | ||||
|     }; | ||||
|  | ||||
|     result.map(|_| fn_memory.return_) | ||||
| } | ||||
|  | ||||
| /// A function being used as a parameter into a stdlib function.  This is a | ||||
| /// closure, plus everything needed to execute it. | ||||
| pub struct FunctionParam<'a> { | ||||
|     pub inner: Option<&'a MemoryFunction>, | ||||
|     pub memory: ProgramMemory, | ||||
|     pub fn_expr: crate::parsing::ast::types::BoxNode<FunctionExpression>, | ||||
|     pub meta: Vec<Metadata>, | ||||
|     pub ctx: ExecutorContext, | ||||
| } | ||||
|  | ||||
| impl<'a> FunctionParam<'a> { | ||||
|     pub async fn call(&self, exec_state: &mut ExecState, args: Vec<Arg>) -> Result<Option<KclValue>, KclError> { | ||||
|         if let Some(inner) = self.inner { | ||||
|             inner( | ||||
|                 args, | ||||
|                 self.memory.clone(), | ||||
|                 self.fn_expr.clone(), | ||||
|                 self.meta.clone(), | ||||
|                 exec_state, | ||||
|                 self.ctx.clone(), | ||||
|             ) | ||||
|             .await | ||||
|         } else { | ||||
|             call_user_defined_function(args, &self.memory, self.fn_expr.as_ref(), exec_state, &self.ctx).await | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl JsonSchema for FunctionParam<'_> { | ||||
|     fn schema_name() -> String { | ||||
|         "FunctionParam".to_owned() | ||||
|     } | ||||
|  | ||||
|     fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { | ||||
|         // TODO: Actually generate a reasonable schema. | ||||
|         gen.subschema_for::<()>() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use crate::parsing::ast::types::{DefaultParamVal, Identifier, Parameter}; | ||||
|  | ||||
|     use super::*; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_assign_args_to_params() { | ||||
|         // Set up a little framework for this test. | ||||
|         fn mem(number: usize) -> KclValue { | ||||
|             KclValue::Int { | ||||
|                 value: number as i64, | ||||
|                 meta: Default::default(), | ||||
|             } | ||||
|         } | ||||
|         fn ident(s: &'static str) -> Node<Identifier> { | ||||
|             Node::no_src(Identifier { | ||||
|                 name: s.to_owned(), | ||||
|                 digest: None, | ||||
|             }) | ||||
|         } | ||||
|         fn opt_param(s: &'static str) -> Parameter { | ||||
|             Parameter { | ||||
|                 identifier: ident(s), | ||||
|                 type_: None, | ||||
|                 default_value: Some(DefaultParamVal::none()), | ||||
|                 labeled: true, | ||||
|                 digest: None, | ||||
|             } | ||||
|         } | ||||
|         fn req_param(s: &'static str) -> Parameter { | ||||
|             Parameter { | ||||
|                 identifier: ident(s), | ||||
|                 type_: None, | ||||
|                 default_value: None, | ||||
|                 labeled: true, | ||||
|                 digest: None, | ||||
|             } | ||||
|         } | ||||
|         fn additional_program_memory(items: &[(String, KclValue)]) -> ProgramMemory { | ||||
|             let mut program_memory = ProgramMemory::new(); | ||||
|             for (name, item) in items { | ||||
|                 program_memory | ||||
|                     .add(name.as_str(), item.clone(), SourceRange::default()) | ||||
|                     .unwrap(); | ||||
|             } | ||||
|             program_memory | ||||
|         } | ||||
|         // Declare the test cases. | ||||
|         for (test_name, params, args, expected) in [ | ||||
|             ("empty", Vec::new(), Vec::new(), Ok(ProgramMemory::new())), | ||||
|             ( | ||||
|                 "all params required, and all given, should be OK", | ||||
|                 vec![req_param("x")], | ||||
|                 vec![mem(1)], | ||||
|                 Ok(additional_program_memory(&[("x".to_owned(), mem(1))])), | ||||
|             ), | ||||
|             ( | ||||
|                 "all params required, none given, should error", | ||||
|                 vec![req_param("x")], | ||||
|                 vec![], | ||||
|                 Err(KclError::Semantic(KclErrorDetails { | ||||
|                     source_ranges: vec![SourceRange::default()], | ||||
|                     message: "Expected 1 arguments, got 0".to_owned(), | ||||
|                 })), | ||||
|             ), | ||||
|             ( | ||||
|                 "all params optional, none given, should be OK", | ||||
|                 vec![opt_param("x")], | ||||
|                 vec![], | ||||
|                 Ok(additional_program_memory(&[("x".to_owned(), KclValue::none())])), | ||||
|             ), | ||||
|             ( | ||||
|                 "mixed params, too few given", | ||||
|                 vec![req_param("x"), opt_param("y")], | ||||
|                 vec![], | ||||
|                 Err(KclError::Semantic(KclErrorDetails { | ||||
|                     source_ranges: vec![SourceRange::default()], | ||||
|                     message: "Expected 1-2 arguments, got 0".to_owned(), | ||||
|                 })), | ||||
|             ), | ||||
|             ( | ||||
|                 "mixed params, minimum given, should be OK", | ||||
|                 vec![req_param("x"), opt_param("y")], | ||||
|                 vec![mem(1)], | ||||
|                 Ok(additional_program_memory(&[ | ||||
|                     ("x".to_owned(), mem(1)), | ||||
|                     ("y".to_owned(), KclValue::none()), | ||||
|                 ])), | ||||
|             ), | ||||
|             ( | ||||
|                 "mixed params, maximum given, should be OK", | ||||
|                 vec![req_param("x"), opt_param("y")], | ||||
|                 vec![mem(1), mem(2)], | ||||
|                 Ok(additional_program_memory(&[ | ||||
|                     ("x".to_owned(), mem(1)), | ||||
|                     ("y".to_owned(), mem(2)), | ||||
|                 ])), | ||||
|             ), | ||||
|             ( | ||||
|                 "mixed params, too many given", | ||||
|                 vec![req_param("x"), opt_param("y")], | ||||
|                 vec![mem(1), mem(2), mem(3)], | ||||
|                 Err(KclError::Semantic(KclErrorDetails { | ||||
|                     source_ranges: vec![SourceRange::default()], | ||||
|                     message: "Expected 1-2 arguments, got 3".to_owned(), | ||||
|                 })), | ||||
|             ), | ||||
|         ] { | ||||
|             // Run each test. | ||||
|             let func_expr = &Node::no_src(FunctionExpression { | ||||
|                 params, | ||||
|                 body: Node { | ||||
|                     inner: crate::parsing::ast::types::Program { | ||||
|                         body: Vec::new(), | ||||
|                         non_code_meta: Default::default(), | ||||
|                         shebang: None, | ||||
|                         digest: None, | ||||
|                     }, | ||||
|                     start: 0, | ||||
|                     end: 0, | ||||
|                     module_id: ModuleId::default(), | ||||
|                 }, | ||||
|                 return_type: None, | ||||
|                 digest: None, | ||||
|             }); | ||||
|             let args = args.into_iter().map(Arg::synthetic).collect(); | ||||
|             let actual = assign_args_to_params(func_expr, args, ProgramMemory::new()); | ||||
|             assert_eq!( | ||||
|                 actual, expected, | ||||
|                 "failed test '{test_name}':\ngot {actual:?}\nbut expected\n{expected:?}" | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,49 +0,0 @@ | ||||
| use schemars::JsonSchema; | ||||
|  | ||||
| use crate::{ | ||||
|     errors::KclError, | ||||
|     execution::{ | ||||
|         call_user_defined_function, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ProgramMemory, | ||||
|     }, | ||||
|     parsing::ast::types::FunctionExpression, | ||||
|     std::args::Arg, | ||||
| }; | ||||
|  | ||||
| /// A function being used as a parameter into a stdlib function.  This is a | ||||
| /// closure, plus everything needed to execute it. | ||||
| pub struct FunctionParam<'a> { | ||||
|     pub inner: Option<&'a MemoryFunction>, | ||||
|     pub memory: ProgramMemory, | ||||
|     pub fn_expr: crate::parsing::ast::types::BoxNode<FunctionExpression>, | ||||
|     pub meta: Vec<Metadata>, | ||||
|     pub ctx: ExecutorContext, | ||||
| } | ||||
|  | ||||
| impl<'a> FunctionParam<'a> { | ||||
|     pub async fn call(&self, exec_state: &mut ExecState, args: Vec<Arg>) -> Result<Option<KclValue>, KclError> { | ||||
|         if let Some(inner) = self.inner { | ||||
|             inner( | ||||
|                 args, | ||||
|                 self.memory.clone(), | ||||
|                 self.fn_expr.clone(), | ||||
|                 self.meta.clone(), | ||||
|                 exec_state, | ||||
|                 self.ctx.clone(), | ||||
|             ) | ||||
|             .await | ||||
|         } else { | ||||
|             call_user_defined_function(args, &self.memory, self.fn_expr.as_ref(), exec_state, &self.ctx).await | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl JsonSchema for FunctionParam<'_> { | ||||
|     fn schema_name() -> String { | ||||
|         "FunctionParam".to_owned() | ||||
|     } | ||||
|  | ||||
|     fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { | ||||
|         // TODO: Actually generate a reasonable schema. | ||||
|         gen.subschema_for::<()>() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										1036
									
								
								src/wasm-lib/kcl/src/execution/geometry.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1036
									
								
								src/wasm-lib/kcl/src/execution/geometry.rs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -533,7 +533,7 @@ impl KclValue { | ||||
|             ) | ||||
|             .await | ||||
|         } else { | ||||
|             crate::execution::call_user_defined_function( | ||||
|             crate::execution::exec_ast::call_user_defined_function( | ||||
|                 args, | ||||
|                 closure_memory.as_ref(), | ||||
|                 expression.as_ref(), | ||||
| @ -568,7 +568,7 @@ impl KclValue { | ||||
|         if let Some(_func) = func { | ||||
|             todo!("Implement calling KCL stdlib fns that are aliased. Part of https://github.com/KittyCAD/modeling-app/issues/4600"); | ||||
|         } else { | ||||
|             crate::execution::call_user_defined_function_kw( | ||||
|             crate::execution::exec_ast::call_user_defined_function_kw( | ||||
|                 args.kw_args, | ||||
|                 closure_memory.as_ref(), | ||||
|                 expression.as_ref(), | ||||
|  | ||||
							
								
								
									
										191
									
								
								src/wasm-lib/kcl/src/execution/memory.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								src/wasm-lib/kcl/src/execution/memory.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,191 @@ | ||||
| use anyhow::Result; | ||||
| use indexmap::IndexMap; | ||||
| use schemars::JsonSchema; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::{ | ||||
|     errors::{KclError, KclErrorDetails}, | ||||
|     execution::{KclValue, Metadata, Sketch, Solid, TagIdentifier}, | ||||
|     source_range::SourceRange, | ||||
| }; | ||||
|  | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| #[ts(export)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct ProgramMemory { | ||||
|     pub environments: Vec<Environment>, | ||||
|     pub current_env: EnvironmentRef, | ||||
|     #[serde(rename = "return")] | ||||
|     pub return_: Option<KclValue>, | ||||
| } | ||||
|  | ||||
| impl ProgramMemory { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             environments: vec![Environment::root()], | ||||
|             current_env: EnvironmentRef::root(), | ||||
|             return_: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn new_env_for_call(&mut self, parent: EnvironmentRef) -> EnvironmentRef { | ||||
|         let new_env_ref = EnvironmentRef(self.environments.len()); | ||||
|         let new_env = Environment::new(parent); | ||||
|         self.environments.push(new_env); | ||||
|         new_env_ref | ||||
|     } | ||||
|  | ||||
|     /// Add to the program memory in the current scope. | ||||
|     pub fn add(&mut self, key: &str, value: KclValue, source_range: SourceRange) -> Result<(), KclError> { | ||||
|         if self.environments[self.current_env.index()].contains_key(key) { | ||||
|             return Err(KclError::ValueAlreadyDefined(KclErrorDetails { | ||||
|                 message: format!("Cannot redefine `{}`", key), | ||||
|                 source_ranges: vec![source_range], | ||||
|             })); | ||||
|         } | ||||
|  | ||||
|         self.environments[self.current_env.index()].insert(key.to_string(), value); | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn update_tag(&mut self, tag: &str, value: TagIdentifier) -> Result<(), KclError> { | ||||
|         self.environments[self.current_env.index()].insert(tag.to_string(), KclValue::TagIdentifier(Box::new(value))); | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Get a value from the program memory. | ||||
|     /// Return Err if not found. | ||||
|     pub fn get(&self, var: &str, source_range: SourceRange) -> Result<&KclValue, KclError> { | ||||
|         let mut env_ref = self.current_env; | ||||
|         loop { | ||||
|             let env = &self.environments[env_ref.index()]; | ||||
|             if let Some(item) = env.bindings.get(var) { | ||||
|                 return Ok(item); | ||||
|             } | ||||
|             if let Some(parent) = env.parent { | ||||
|                 env_ref = parent; | ||||
|             } else { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Err(KclError::UndefinedValue(KclErrorDetails { | ||||
|             message: format!("memory item key `{}` is not defined", var), | ||||
|             source_ranges: vec![source_range], | ||||
|         })) | ||||
|     } | ||||
|  | ||||
|     /// Returns all bindings in the current scope. | ||||
|     #[allow(dead_code)] | ||||
|     fn get_all_cur_scope(&self) -> IndexMap<String, KclValue> { | ||||
|         let env = &self.environments[self.current_env.index()]; | ||||
|         env.bindings.clone() | ||||
|     } | ||||
|  | ||||
|     /// Find all solids in the memory that are on a specific sketch id. | ||||
|     /// This does not look inside closures.  But as long as we do not allow | ||||
|     /// mutation of variables in KCL, closure memory should be a subset of this. | ||||
|     #[allow(clippy::vec_box)] | ||||
|     pub fn find_solids_on_sketch(&self, sketch_id: uuid::Uuid) -> Vec<Box<Solid>> { | ||||
|         self.environments | ||||
|             .iter() | ||||
|             .flat_map(|env| { | ||||
|                 env.bindings | ||||
|                     .values() | ||||
|                     .filter_map(|item| match item { | ||||
|                         KclValue::Solid { value } if value.sketch.id == sketch_id => Some(value.clone()), | ||||
|                         _ => None, | ||||
|                     }) | ||||
|                     .collect::<Vec<_>>() | ||||
|             }) | ||||
|             .collect() | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for ProgramMemory { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// An index pointing to an environment. | ||||
| #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| #[schemars(transparent)] | ||||
| pub struct EnvironmentRef(usize); | ||||
|  | ||||
| impl EnvironmentRef { | ||||
|     pub fn root() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
|  | ||||
|     pub fn index(&self) -> usize { | ||||
|         self.0 | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| pub struct Environment { | ||||
|     pub(super) bindings: IndexMap<String, KclValue>, | ||||
|     parent: Option<EnvironmentRef>, | ||||
| } | ||||
|  | ||||
| const NO_META: Vec<Metadata> = Vec::new(); | ||||
|  | ||||
| impl Environment { | ||||
|     pub fn root() -> Self { | ||||
|         Self { | ||||
|             // Prelude | ||||
|             bindings: IndexMap::from([ | ||||
|                 ("ZERO".to_string(), KclValue::from_number(0.0, NO_META)), | ||||
|                 ("QUARTER_TURN".to_string(), KclValue::from_number(90.0, NO_META)), | ||||
|                 ("HALF_TURN".to_string(), KclValue::from_number(180.0, NO_META)), | ||||
|                 ("THREE_QUARTER_TURN".to_string(), KclValue::from_number(270.0, NO_META)), | ||||
|             ]), | ||||
|             parent: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn new(parent: EnvironmentRef) -> Self { | ||||
|         Self { | ||||
|             bindings: IndexMap::new(), | ||||
|             parent: Some(parent), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get(&self, key: &str, source_range: SourceRange) -> Result<&KclValue, KclError> { | ||||
|         self.bindings.get(key).ok_or_else(|| { | ||||
|             KclError::UndefinedValue(KclErrorDetails { | ||||
|                 message: format!("memory item key `{}` is not defined", key), | ||||
|                 source_ranges: vec![source_range], | ||||
|             }) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn insert(&mut self, key: String, value: KclValue) { | ||||
|         self.bindings.insert(key, value); | ||||
|     } | ||||
|  | ||||
|     pub fn contains_key(&self, key: &str) -> bool { | ||||
|         self.bindings.contains_key(key) | ||||
|     } | ||||
|  | ||||
|     pub fn update_sketch_tags(&mut self, sg: &Sketch) { | ||||
|         if sg.tags.is_empty() { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         for (_, val) in self.bindings.iter_mut() { | ||||
|             let KclValue::Sketch { value } = val else { continue }; | ||||
|             let mut sketch = value.to_owned(); | ||||
|  | ||||
|             if sketch.original_id == sg.original_id { | ||||
|                 for tag in sg.tags.iter() { | ||||
|                     sketch.tags.insert(tag.0.clone(), tag.1.clone()); | ||||
|                 } | ||||
|             } | ||||
|             *val = KclValue::Sketch { value: sketch }; | ||||
|         } | ||||
|     } | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										300
									
								
								src/wasm-lib/kcl/src/execution/state.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								src/wasm-lib/kcl/src/execution/state.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,300 @@ | ||||
| use anyhow::Result; | ||||
| use indexmap::IndexMap; | ||||
| use kittycad_modeling_cmds::websocket::WebSocketResponse; | ||||
| use schemars::JsonSchema; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use uuid::Uuid; | ||||
|  | ||||
| use crate::{ | ||||
|     errors::{KclError, KclErrorDetails}, | ||||
|     execution::{ | ||||
|         annotations, kcl_value, Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, ExecOutcome, ExecutorSettings, | ||||
|         KclValue, ModuleInfo, ModuleRepr, Operation, ProgramMemory, SolidLazyIds, UnitAngle, UnitLen, | ||||
|     }, | ||||
|     parsing::ast::types::NonCodeValue, | ||||
|     source_range::{ModuleId, SourceRange}, | ||||
| }; | ||||
|  | ||||
| /// State for executing a program. | ||||
| #[derive(Debug, Clone, Deserialize, Serialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct ExecState { | ||||
|     pub global: GlobalState, | ||||
|     pub mod_local: ModuleState, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Deserialize, Serialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct GlobalState { | ||||
|     /// The stable artifact ID generator. | ||||
|     pub id_generator: IdGenerator, | ||||
|     /// Map from source file absolute path to module ID. | ||||
|     pub path_to_source_id: IndexMap<std::path::PathBuf, ModuleId>, | ||||
|     /// Map from module ID to module info. | ||||
|     pub module_infos: IndexMap<ModuleId, ModuleInfo>, | ||||
|     /// Output map of UUIDs to artifacts. | ||||
|     pub artifacts: IndexMap<ArtifactId, Artifact>, | ||||
|     /// Output commands to allow building the artifact graph by the caller. | ||||
|     /// These are accumulated in the [`ExecutorContext`] but moved here for | ||||
|     /// convenience of the execution cache. | ||||
|     pub artifact_commands: Vec<ArtifactCommand>, | ||||
|     /// Responses from the engine for `artifact_commands`.  We need to cache | ||||
|     /// this so that we can build the artifact graph.  These are accumulated in | ||||
|     /// the [`ExecutorContext`] but moved here for convenience of the execution | ||||
|     /// cache. | ||||
|     pub artifact_responses: IndexMap<Uuid, WebSocketResponse>, | ||||
|     /// Output artifact graph. | ||||
|     pub artifact_graph: ArtifactGraph, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct ModuleState { | ||||
|     /// Program variable bindings. | ||||
|     pub memory: ProgramMemory, | ||||
|     /// Dynamic state that follows dynamic flow of the program. | ||||
|     pub dynamic_state: DynamicState, | ||||
|     /// The current value of the pipe operator returned from the previous | ||||
|     /// expression.  If we're not currently in a pipeline, this will be None. | ||||
|     pub pipe_value: Option<KclValue>, | ||||
|     /// Identifiers that have been exported from the current module. | ||||
|     pub module_exports: Vec<String>, | ||||
|     /// The stack of import statements for detecting circular module imports. | ||||
|     /// If this is empty, we're not currently executing an import statement. | ||||
|     pub import_stack: Vec<std::path::PathBuf>, | ||||
|     /// Operations that have been performed in execution order, for display in | ||||
|     /// the Feature Tree. | ||||
|     pub operations: Vec<Operation>, | ||||
|     /// Settings specified from annotations. | ||||
|     pub settings: MetaSettings, | ||||
| } | ||||
|  | ||||
| impl ExecState { | ||||
|     pub fn new(exec_settings: &ExecutorSettings) -> Self { | ||||
|         ExecState { | ||||
|             global: GlobalState::new(exec_settings), | ||||
|             mod_local: ModuleState::new(exec_settings), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub(super) fn reset(&mut self, exec_settings: &ExecutorSettings) { | ||||
|         let mut id_generator = self.global.id_generator.clone(); | ||||
|         // We do not pop the ids, since we want to keep the same id generator. | ||||
|         // This is for the front end to keep track of the ids. | ||||
|         id_generator.next_id = 0; | ||||
|  | ||||
|         let mut global = GlobalState::new(exec_settings); | ||||
|         global.id_generator = id_generator; | ||||
|  | ||||
|         *self = ExecState { | ||||
|             global, | ||||
|             mod_local: ModuleState::new(exec_settings), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /// Convert to execution outcome when running in WebAssembly.  We want to | ||||
|     /// reduce the amount of data that crosses the WASM boundary as much as | ||||
|     /// possible. | ||||
|     pub fn to_wasm_outcome(self) -> ExecOutcome { | ||||
|         // Fields are opt-in so that we don't accidentally leak private internal | ||||
|         // state when we add more to ExecState. | ||||
|         ExecOutcome { | ||||
|             memory: self.mod_local.memory, | ||||
|             operations: self.mod_local.operations, | ||||
|             artifacts: self.global.artifacts, | ||||
|             artifact_commands: self.global.artifact_commands, | ||||
|             artifact_graph: self.global.artifact_graph, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn memory(&self) -> &ProgramMemory { | ||||
|         &self.mod_local.memory | ||||
|     } | ||||
|  | ||||
|     pub fn mut_memory(&mut self) -> &mut ProgramMemory { | ||||
|         &mut self.mod_local.memory | ||||
|     } | ||||
|  | ||||
|     pub fn next_uuid(&mut self) -> Uuid { | ||||
|         self.global.id_generator.next_uuid() | ||||
|     } | ||||
|  | ||||
|     pub fn add_artifact(&mut self, artifact: Artifact) { | ||||
|         let id = artifact.id(); | ||||
|         self.global.artifacts.insert(id, artifact); | ||||
|     } | ||||
|  | ||||
|     pub(super) fn add_module(&mut self, id: ModuleId, path: std::path::PathBuf, repr: ModuleRepr) -> ModuleId { | ||||
|         debug_assert!(!self.global.path_to_source_id.contains_key(&path)); | ||||
|  | ||||
|         self.global.path_to_source_id.insert(path.clone(), id); | ||||
|  | ||||
|         let module_info = ModuleInfo { id, repr, path }; | ||||
|         self.global.module_infos.insert(id, module_info); | ||||
|  | ||||
|         id | ||||
|     } | ||||
|  | ||||
|     pub fn length_unit(&self) -> UnitLen { | ||||
|         self.mod_local.settings.default_length_units | ||||
|     } | ||||
|  | ||||
|     pub fn angle_unit(&self) -> UnitAngle { | ||||
|         self.mod_local.settings.default_angle_units | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl GlobalState { | ||||
|     fn new(settings: &ExecutorSettings) -> Self { | ||||
|         let mut global = GlobalState { | ||||
|             id_generator: Default::default(), | ||||
|             path_to_source_id: Default::default(), | ||||
|             module_infos: Default::default(), | ||||
|             artifacts: Default::default(), | ||||
|             artifact_commands: Default::default(), | ||||
|             artifact_responses: Default::default(), | ||||
|             artifact_graph: Default::default(), | ||||
|         }; | ||||
|  | ||||
|         let root_id = ModuleId::default(); | ||||
|         let root_path = settings.current_file.clone().unwrap_or_default(); | ||||
|         global.module_infos.insert( | ||||
|             root_id, | ||||
|             ModuleInfo { | ||||
|                 id: root_id, | ||||
|                 path: root_path.clone(), | ||||
|                 repr: ModuleRepr::Root, | ||||
|             }, | ||||
|         ); | ||||
|         global.path_to_source_id.insert(root_path, root_id); | ||||
|         global | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ModuleState { | ||||
|     pub(super) fn new(exec_settings: &ExecutorSettings) -> Self { | ||||
|         ModuleState { | ||||
|             memory: Default::default(), | ||||
|             dynamic_state: Default::default(), | ||||
|             pipe_value: Default::default(), | ||||
|             module_exports: Default::default(), | ||||
|             import_stack: Default::default(), | ||||
|             operations: Default::default(), | ||||
|             settings: MetaSettings { | ||||
|                 default_length_units: exec_settings.units.into(), | ||||
|                 default_angle_units: Default::default(), | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| #[ts(export)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct MetaSettings { | ||||
|     pub default_length_units: kcl_value::UnitLen, | ||||
|     pub default_angle_units: kcl_value::UnitAngle, | ||||
| } | ||||
|  | ||||
| impl MetaSettings { | ||||
|     pub(crate) fn update_from_annotation( | ||||
|         &mut self, | ||||
|         annotation: &NonCodeValue, | ||||
|         source_range: SourceRange, | ||||
|     ) -> Result<(), KclError> { | ||||
|         let properties = annotations::expect_properties(annotations::SETTINGS, annotation, source_range)?; | ||||
|  | ||||
|         for p in properties { | ||||
|             match &*p.inner.key.name { | ||||
|                 annotations::SETTINGS_UNIT_LENGTH => { | ||||
|                     let value = annotations::expect_ident(&p.inner.value)?; | ||||
|                     let value = kcl_value::UnitLen::from_str(value, source_range)?; | ||||
|                     self.default_length_units = value; | ||||
|                 } | ||||
|                 annotations::SETTINGS_UNIT_ANGLE => { | ||||
|                     let value = annotations::expect_ident(&p.inner.value)?; | ||||
|                     let value = kcl_value::UnitAngle::from_str(value, source_range)?; | ||||
|                     self.default_angle_units = value; | ||||
|                 } | ||||
|                 name => { | ||||
|                     return Err(KclError::Semantic(KclErrorDetails { | ||||
|                         message: format!( | ||||
|                             "Unexpected settings key: `{name}`; expected one of `{}`, `{}`", | ||||
|                             annotations::SETTINGS_UNIT_LENGTH, | ||||
|                             annotations::SETTINGS_UNIT_ANGLE | ||||
|                         ), | ||||
|                         source_ranges: vec![source_range], | ||||
|                     })) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Dynamic state that depends on the dynamic flow of the program, like the call | ||||
| /// stack.  If the language had exceptions, for example, you could store the | ||||
| /// stack of exception handlers here. | ||||
| #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] | ||||
| pub struct DynamicState { | ||||
|     pub solid_ids: Vec<SolidLazyIds>, | ||||
| } | ||||
|  | ||||
| impl DynamicState { | ||||
|     #[must_use] | ||||
|     pub(super) fn merge(&self, memory: &ProgramMemory) -> Self { | ||||
|         let mut merged = self.clone(); | ||||
|         merged.append(memory); | ||||
|         merged | ||||
|     } | ||||
|  | ||||
|     fn append(&mut self, memory: &ProgramMemory) { | ||||
|         for env in &memory.environments { | ||||
|             for item in env.bindings.values() { | ||||
|                 if let KclValue::Solid { value } = item { | ||||
|                     self.solid_ids.push(SolidLazyIds::from(value.as_ref())); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn edge_cut_ids_on_sketch(&self, sketch_id: uuid::Uuid) -> Vec<uuid::Uuid> { | ||||
|         self.solid_ids | ||||
|             .iter() | ||||
|             .flat_map(|eg| { | ||||
|                 if eg.sketch_id == sketch_id { | ||||
|                     eg.edge_cuts.clone() | ||||
|                 } else { | ||||
|                     Vec::new() | ||||
|                 } | ||||
|             }) | ||||
|             .collect::<Vec<_>>() | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// A generator for ArtifactIds that can be stable across executions. | ||||
| #[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct IdGenerator { | ||||
|     pub(super) next_id: usize, | ||||
|     ids: Vec<uuid::Uuid>, | ||||
| } | ||||
|  | ||||
| impl IdGenerator { | ||||
|     pub fn new() -> Self { | ||||
|         Self::default() | ||||
|     } | ||||
|  | ||||
|     pub fn next_uuid(&mut self) -> uuid::Uuid { | ||||
|         if let Some(id) = self.ids.get(self.next_id) { | ||||
|             self.next_id += 1; | ||||
|             *id | ||||
|         } else { | ||||
|             let id = uuid::Uuid::new_v4(); | ||||
|             self.ids.push(id); | ||||
|             self.next_id += 1; | ||||
|             id | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -183,6 +183,6 @@ impl FileSystem for FileManager { | ||||
|             }) | ||||
|         })?; | ||||
|  | ||||
|         Ok(files.into_iter().map(|s| std::path::PathBuf::from(s)).collect()) | ||||
|         Ok(files.into_iter().map(std::path::PathBuf::from).collect()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -82,10 +82,7 @@ mod wasm; | ||||
| pub use coredump::CoreDump; | ||||
| pub use engine::{EngineManager, ExecutionKind}; | ||||
| pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs}; | ||||
| pub use execution::{ | ||||
|     cache::{CacheInformation, OldAstState}, | ||||
|     ExecState, ExecutorContext, ExecutorSettings, MetaSettings, Point2d, | ||||
| }; | ||||
| pub use execution::{bust_cache, ExecOutcome, ExecState, ExecutorContext, ExecutorSettings, MetaSettings, Point2d}; | ||||
| pub use lsp::{ | ||||
|     copilot::Backend as CopilotLspBackend, | ||||
|     kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand}, | ||||
|  | ||||
| @ -36,19 +36,19 @@ macro_rules! logln { | ||||
| } | ||||
| pub(crate) use logln; | ||||
|  | ||||
| #[cfg(not(feature = "disable-println"))] | ||||
| #[cfg(all(not(feature = "disable-println"), not(target_arch = "wasm32")))] | ||||
| #[inline] | ||||
| fn log_inner(msg: String) { | ||||
|     eprintln!("{msg}"); | ||||
| } | ||||
|  | ||||
| #[cfg(all(feature = "disable-println", target_arch = "wasm32"))] | ||||
| #[cfg(all(not(feature = "disable-println"), target_arch = "wasm32"))] | ||||
| #[inline] | ||||
| fn log_inner(msg: String) { | ||||
|     web_sys::console::log_1(&msg.into()); | ||||
| } | ||||
|  | ||||
| #[cfg(all(feature = "disable-println", not(target_arch = "wasm32")))] | ||||
| #[cfg(feature = "disable-println")] | ||||
| #[inline] | ||||
| fn log_inner(_msg: String) {} | ||||
|  | ||||
|  | ||||
| @ -49,7 +49,7 @@ use crate::{ | ||||
|         token::TokenStream, | ||||
|         PIPE_OPERATOR, | ||||
|     }, | ||||
|     CacheInformation, ExecState, ModuleId, OldAstState, Program, SourceRange, | ||||
|     ModuleId, Program, SourceRange, | ||||
| }; | ||||
| const SEMANTIC_TOKEN_TYPES: [SemanticTokenType; 10] = [ | ||||
|     SemanticTokenType::NUMBER, | ||||
| @ -102,12 +102,6 @@ pub struct Backend { | ||||
|     pub(super) token_map: DashMap<String, TokenStream>, | ||||
|     /// AST maps. | ||||
|     pub ast_map: DashMap<String, Node<crate::parsing::ast::types::Program>>, | ||||
|     /// Last successful execution. | ||||
|     /// This gets set to None when execution errors, or we want to bust the cache on purpose to | ||||
|     /// force a re-execution. | ||||
|     /// We do not need to manually bust the cache for changed units, that's handled by the cache | ||||
|     /// information. | ||||
|     pub last_successful_ast_state: Arc<RwLock<Option<OldAstState>>>, | ||||
|     /// Memory maps. | ||||
|     pub memory_map: DashMap<String, crate::execution::ProgramMemory>, | ||||
|     /// Current code. | ||||
| @ -192,7 +186,6 @@ impl Backend { | ||||
|             diagnostics_map: Default::default(), | ||||
|             symbols_map: Default::default(), | ||||
|             semantic_tokens_map: Default::default(), | ||||
|             last_successful_ast_state: Default::default(), | ||||
|             is_initialized: Default::default(), | ||||
|         }) | ||||
|     } | ||||
| @ -267,10 +260,7 @@ impl crate::lsp::backend::Backend for Backend { | ||||
|  | ||||
|     async fn inner_on_change(&self, params: TextDocumentItem, force: bool) { | ||||
|         if force { | ||||
|             // Bust the execution cache. | ||||
|             let mut old_ast_state = self.last_successful_ast_state.write().await; | ||||
|             *old_ast_state = None; | ||||
|             drop(old_ast_state); | ||||
|             crate::bust_cache().await; | ||||
|         } | ||||
|  | ||||
|         let filename = params.uri.to_string(); | ||||
| @ -688,53 +678,28 @@ impl Backend { | ||||
|             return Ok(()); | ||||
|         } | ||||
|  | ||||
|         let mut last_successful_ast_state = self.last_successful_ast_state.write().await; | ||||
|  | ||||
|         let mut exec_state = if let Some(last_successful_ast_state) = last_successful_ast_state.clone() { | ||||
|             last_successful_ast_state.exec_state | ||||
|         } else { | ||||
|             ExecState::new(&executor_ctx.settings) | ||||
|         }; | ||||
|  | ||||
|         if let Err(err) = executor_ctx | ||||
|             .run( | ||||
|                 CacheInformation { | ||||
|                     old: last_successful_ast_state.clone(), | ||||
|                     new_ast: ast.ast.clone(), | ||||
|                 }, | ||||
|                 &mut exec_state, | ||||
|             ) | ||||
|             .await | ||||
|         { | ||||
|         match executor_ctx.run_with_caching(ast.clone()).await { | ||||
|             Err(err) => { | ||||
|                 self.memory_map.remove(params.uri.as_str()); | ||||
|             self.add_to_diagnostics(params, &[err], false).await; | ||||
|  | ||||
|             // Update the last successful ast state to be None. | ||||
|             *last_successful_ast_state = None; | ||||
|                 self.add_to_diagnostics(params, &[err.error], false).await; | ||||
|  | ||||
|                 // Since we already published the diagnostics we don't really care about the error | ||||
|                 // string. | ||||
|             return Err(anyhow::anyhow!("failed to execute code")); | ||||
|                 Err(anyhow::anyhow!("failed to execute code")) | ||||
|             } | ||||
|  | ||||
|         // Update the last successful ast state. | ||||
|         *last_successful_ast_state = Some(OldAstState { | ||||
|             ast: ast.ast.clone(), | ||||
|             exec_state: exec_state.clone(), | ||||
|             settings: executor_ctx.settings.clone(), | ||||
|         }); | ||||
|         drop(last_successful_ast_state); | ||||
|  | ||||
|         self.memory_map | ||||
|             .insert(params.uri.to_string(), exec_state.memory().clone()); | ||||
|             Ok(outcome) => { | ||||
|                 let memory = outcome.memory; | ||||
|                 self.memory_map.insert(params.uri.to_string(), memory.clone()); | ||||
|  | ||||
|                 // Send the notification to the client that the memory was updated. | ||||
|                 self.client | ||||
|             .send_notification::<custom_notifications::MemoryUpdated>(exec_state.mod_local.memory) | ||||
|                     .send_notification::<custom_notifications::MemoryUpdated>(memory) | ||||
|                     .await; | ||||
|  | ||||
|                 Ok(()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_semantic_token_type_index(&self, token_type: &SemanticTokenType) -> Option<u32> { | ||||
|         SEMANTIC_TOKEN_TYPES | ||||
|  | ||||
| @ -9,7 +9,7 @@ pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> { | ||||
|     let stdlib_completions = crate::lsp::kcl::get_completions_from_stdlib(&stdlib)?; | ||||
|     let stdlib_signatures = crate::lsp::kcl::get_signatures_from_stdlib(&stdlib)?; | ||||
|  | ||||
|     let zoo_client = crate::execution::new_zoo_client(None, None)?; | ||||
|     let zoo_client = crate::engine::new_zoo_client(None, None)?; | ||||
|  | ||||
|     let executor_ctx = if execute { | ||||
|         Some(crate::execution::ExecutorContext::new(&zoo_client, Default::default()).await?) | ||||
| @ -37,7 +37,6 @@ pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> { | ||||
|         can_send_telemetry: true, | ||||
|         executor_ctx: Arc::new(tokio::sync::RwLock::new(executor_ctx)), | ||||
|         can_execute: Arc::new(tokio::sync::RwLock::new(can_execute)), | ||||
|         last_successful_ast_state: Default::default(), | ||||
|         is_initialized: Default::default(), | ||||
|     }) | ||||
|     .custom_method("kcl/updateUnits", crate::lsp::kcl::Backend::update_units) | ||||
|  | ||||
| @ -7,7 +7,7 @@ use winnow::{ | ||||
|     prelude::*, | ||||
|     stream::{Location, Stream}, | ||||
|     token::{any, none_of, one_of, take_till, take_until}, | ||||
|     Located, Stateful, | ||||
|     LocatingSlice, Stateful, | ||||
| }; | ||||
|  | ||||
| use super::TokenStream; | ||||
| @ -65,13 +65,13 @@ lazy_static! { | ||||
| pub(super) fn lex(i: &str, module_id: ModuleId) -> Result<TokenStream, ParseError<Input<'_>, ContextError>> { | ||||
|     let state = State::new(module_id); | ||||
|     let input = Input { | ||||
|         input: Located::new(i), | ||||
|         input: LocatingSlice::new(i), | ||||
|         state, | ||||
|     }; | ||||
|     Ok(TokenStream::new(repeat(0.., token).parse(input)?)) | ||||
| } | ||||
|  | ||||
| pub(super) type Input<'a> = Stateful<Located<&'a str>, State>; | ||||
| pub(super) type Input<'a> = Stateful<LocatingSlice<&'a str>, State>; | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub(super) struct State { | ||||
| @ -361,7 +361,7 @@ fn keyword_type_or_word(i: &mut Input<'_>) -> PResult<Token> { | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use winnow::Located; | ||||
|     use winnow::LocatingSlice; | ||||
|  | ||||
|     use super::*; | ||||
|     use crate::parsing::token::TokenSlice; | ||||
| @ -373,7 +373,7 @@ mod tests { | ||||
|     { | ||||
|         let state = State::new(ModuleId::default()); | ||||
|         let mut input = Input { | ||||
|             input: Located::new(s), | ||||
|             input: LocatingSlice::new(s), | ||||
|             state, | ||||
|         }; | ||||
|         assert!(p.parse_next(&mut input).is_err(), "parsed {s} but should have failed"); | ||||
| @ -388,7 +388,7 @@ mod tests { | ||||
|     { | ||||
|         let state = State::new(ModuleId::default()); | ||||
|         let mut input = Input { | ||||
|             input: Located::new(s), | ||||
|             input: LocatingSlice::new(s), | ||||
|             state, | ||||
|         }; | ||||
|         let res = p.parse_next(&mut input); | ||||
| @ -422,7 +422,7 @@ mod tests { | ||||
|  | ||||
|         let module_id = ModuleId::from_usize(1); | ||||
|         let input = Input { | ||||
|             input: Located::new("0.0000000000"), | ||||
|             input: LocatingSlice::new("0.0000000000"), | ||||
|             state: State::new(module_id), | ||||
|         }; | ||||
|  | ||||
|  | ||||
| @ -3,8 +3,9 @@ | ||||
| use std::path::PathBuf; | ||||
|  | ||||
| use crate::{ | ||||
|     engine::new_zoo_client, | ||||
|     errors::ExecErrorWithState, | ||||
|     execution::{new_zoo_client, ExecutorContext, ExecutorSettings}, | ||||
|     execution::{ExecutorContext, ExecutorSettings}, | ||||
|     settings::types::UnitLength, | ||||
|     ConnectionError, ExecError, ExecState, KclErrorWithOutputs, Program, | ||||
| }; | ||||
| @ -66,8 +67,11 @@ async fn do_execute_and_snapshot( | ||||
|     program: Program, | ||||
| ) -> Result<(ExecState, image::DynamicImage), ExecErrorWithState> { | ||||
|     let mut exec_state = ExecState::new(&ctx.settings); | ||||
|     ctx.run_with_ui_outputs(&program, &mut exec_state) | ||||
|         .await | ||||
|         .map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone()))?; | ||||
|     let snapshot_png_bytes = ctx | ||||
|         .execute_and_prepare_snapshot(&program, &mut exec_state) | ||||
|         .prepare_snapshot() | ||||
|         .await | ||||
|         .map_err(|err| ExecErrorWithState::new(err, exec_state.clone()))? | ||||
|         .contents | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| ///! Web assembly utils. | ||||
| //! Web assembly utils. | ||||
| use std::{ | ||||
|     pin::Pin, | ||||
|     task::{Context, Poll}, | ||||
|  | ||||
| @ -1,12 +1,11 @@ | ||||
| --- | ||||
| source: kcl/src/simulation_tests.rs | ||||
| description: Error from executing kw_fn_too_few_args.kcl | ||||
| snapshot_kind: text | ||||
| --- | ||||
| KCL Semantic error | ||||
|  | ||||
|   × semantic: This function requires a parameter y, but you haven't passed | ||||
|   │ it one. | ||||
|   × semantic: This function requires a parameter y, but you haven't passed it | ||||
|   │ one. | ||||
|    ╭─[1:7] | ||||
|  1 │ ╭─▶ fn add(x, y) { | ||||
|  2 │ │     return x + y | ||||
|  | ||||
| @ -5,33 +5,11 @@ use std::sync::Arc; | ||||
| use futures::stream::TryStreamExt; | ||||
| use gloo_utils::format::JsValueSerdeExt; | ||||
| use kcl_lib::{ | ||||
|     exec::IdGenerator, pretty::NumericSuffix, CacheInformation, CoreDump, EngineManager, ExecState, ModuleId, | ||||
|     OldAstState, Point2d, Program, | ||||
|     bust_cache, exec::IdGenerator, pretty::NumericSuffix, CoreDump, EngineManager, ModuleId, Point2d, Program, | ||||
| }; | ||||
| use tokio::sync::RwLock; | ||||
| use tower_lsp::{LspService, Server}; | ||||
| use wasm_bindgen::prelude::*; | ||||
|  | ||||
| lazy_static::lazy_static! { | ||||
|     /// A static mutable lock for updating the last successful execution state for the cache. | ||||
|     static ref OLD_AST_MEMORY: Arc<RwLock<Option<OldAstState>>> = Default::default(); | ||||
| } | ||||
|  | ||||
| // Read the old ast memory from the lock, this should never fail since | ||||
| // in failure scenarios we should just bust the cache and send back None as the previous | ||||
| // state. | ||||
| async fn read_old_ast_memory() -> Option<OldAstState> { | ||||
|     let lock = OLD_AST_MEMORY.read().await; | ||||
|     lock.clone() | ||||
| } | ||||
|  | ||||
| async fn bust_cache() { | ||||
|     // We don't use the cache in mock mode. | ||||
|     let mut current_cache = OLD_AST_MEMORY.write().await; | ||||
|     // Set the cache to None. | ||||
|     *current_cache = None; | ||||
| } | ||||
|  | ||||
| // wasm_bindgen wrapper for clearing the scene and busting the cache. | ||||
| #[wasm_bindgen] | ||||
| pub async fn clear_scene_and_bust_cache( | ||||
| @ -39,7 +17,6 @@ pub async fn clear_scene_and_bust_cache( | ||||
| ) -> Result<(), String> { | ||||
|     console_error_panic_hook::set_once(); | ||||
|  | ||||
|     // Bust the cache. | ||||
|     bust_cache().await; | ||||
|  | ||||
|     let engine = kcl_lib::wasm_engine::EngineConnection::new(engine_manager) | ||||
| @ -70,74 +47,31 @@ pub async fn execute( | ||||
|     let program: Program = serde_json::from_str(program_ast_json).map_err(|e| e.to_string())?; | ||||
|     let program_memory_override: Option<kcl_lib::exec::ProgramMemory> = | ||||
|         serde_json::from_str(program_memory_override_str).map_err(|e| e.to_string())?; | ||||
|  | ||||
|     // If we have a program memory override, assume we are in mock mode. | ||||
|     // You cannot override the memory in non-mock mode. | ||||
|     let is_mock = program_memory_override.is_some(); | ||||
|  | ||||
|     let config: kcl_lib::Configuration = serde_json::from_str(settings).map_err(|e| e.to_string())?; | ||||
|     let mut settings: kcl_lib::ExecutorSettings = config.into(); | ||||
|     if let Some(path) = path { | ||||
|         settings.with_current_file(std::path::PathBuf::from(path)); | ||||
|     } | ||||
|  | ||||
|     let ctx = if is_mock { | ||||
|         kcl_lib::ExecutorContext::new_mock(fs_manager, settings).await? | ||||
|     // If we have a program memory override, assume we are in mock mode. | ||||
|     // You cannot override the memory in non-mock mode. | ||||
|     if program_memory_override.is_some() { | ||||
|         let ctx = kcl_lib::ExecutorContext::new_mock(fs_manager, settings.into()).await?; | ||||
|         match ctx.run_mock(program, program_memory_override).await { | ||||
|             // The serde-wasm-bindgen does not work here because of weird HashMap issues. | ||||
|             // DO NOT USE serde_wasm_bindgen::to_value it will break the frontend. | ||||
|             Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()), | ||||
|             Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?), | ||||
|         } | ||||
|     } else { | ||||
|         kcl_lib::ExecutorContext::new(engine_manager, fs_manager, settings).await? | ||||
|     }; | ||||
|  | ||||
|     let mut exec_state = ExecState::new(&ctx.settings); | ||||
|     let mut old_ast_memory = None; | ||||
|  | ||||
|     // Populate from the old exec state if it exists. | ||||
|     if let Some(program_memory_override) = program_memory_override { | ||||
|         // We are in mock mode, so don't use any cache. | ||||
|         exec_state.mod_local.memory = program_memory_override; | ||||
|     } else { | ||||
|         // If we are in mock mode, we don't want to use any cache. | ||||
|         if let Some(old) = read_old_ast_memory().await { | ||||
|             exec_state = old.exec_state.clone(); | ||||
|             old_ast_memory = Some(old); | ||||
|         let ctx = kcl_lib::ExecutorContext::new(engine_manager, fs_manager, settings.into()).await?; | ||||
|         match ctx.run_with_caching(program).await { | ||||
|             // The serde-wasm-bindgen does not work here because of weird HashMap issues. | ||||
|             // DO NOT USE serde_wasm_bindgen::to_value it will break the frontend. | ||||
|             Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()), | ||||
|             Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if let Err(err) = ctx | ||||
|         .run_with_ui_outputs( | ||||
|             CacheInformation { | ||||
|                 old: old_ast_memory, | ||||
|                 new_ast: program.ast.clone(), | ||||
|             }, | ||||
|             &mut exec_state, | ||||
|         ) | ||||
|         .await | ||||
|     { | ||||
|         if !is_mock { | ||||
|             bust_cache().await; | ||||
|         } | ||||
|  | ||||
|         // Throw the error. | ||||
|         return Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?); | ||||
|     } | ||||
|  | ||||
|     if !is_mock { | ||||
|         // We don't use the cache in mock mode. | ||||
|         let mut current_cache = OLD_AST_MEMORY.write().await; | ||||
|  | ||||
|         // If we aren't in mock mode, save this as the last successful execution to the cache. | ||||
|         *current_cache = Some(OldAstState { | ||||
|             ast: program.ast.clone(), | ||||
|             exec_state: exec_state.clone(), | ||||
|             settings: ctx.settings.clone(), | ||||
|         }); | ||||
|         drop(current_cache); | ||||
|     } | ||||
|  | ||||
|     // The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the | ||||
|     // gloo-serialize crate instead. | ||||
|     // DO NOT USE serde_wasm_bindgen::to_value(&exec_state).map_err(|e| e.to_string()) | ||||
|     // it will break the frontend. | ||||
|     JsValue::from_serde(&exec_state.to_wasm_outcome()).map_err(|e| e.to_string()) | ||||
| } | ||||
|  | ||||
| // wasm_bindgen wrapper for execute | ||||
| @ -160,7 +94,6 @@ pub async fn make_default_planes( | ||||
|     engine_manager: kcl_lib::wasm_engine::EngineCommandManager, | ||||
| ) -> Result<JsValue, String> { | ||||
|     console_error_panic_hook::set_once(); | ||||
|     // deserialize the ast from a stringified json | ||||
|  | ||||
|     let engine = kcl_lib::wasm_engine::EngineConnection::new(engine_manager) | ||||
|         .await | ||||
| @ -170,12 +103,9 @@ pub async fn make_default_planes( | ||||
|         .await | ||||
|         .map_err(String::from)?; | ||||
|  | ||||
|     // The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the | ||||
|     // gloo-serialize crate instead. | ||||
|     JsValue::from_serde(&default_planes).map_err(|e| e.to_string()) | ||||
| } | ||||
|  | ||||
| // wasm_bindgen wrapper for execute | ||||
| #[wasm_bindgen] | ||||
| pub async fn modify_ast_for_sketch_wasm( | ||||
|     manager: kcl_lib::wasm_engine::EngineCommandManager, | ||||
| @ -208,8 +138,6 @@ pub async fn modify_ast_for_sketch_wasm( | ||||
|     .await | ||||
|     .map_err(String::from)?; | ||||
|  | ||||
|     // The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the | ||||
|     // gloo-serialize crate instead. | ||||
|     JsValue::from_serde(&program).map_err(|e| e.to_string()) | ||||
| } | ||||
|  | ||||
| @ -384,11 +312,7 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, baseurl: Strin | ||||
|     let mut zoo_client = kittycad::Client::new(token); | ||||
|     zoo_client.set_base_url(baseurl.as_str()); | ||||
|  | ||||
|     let dev_mode = if baseurl == "https://api.dev.zoo.dev" { | ||||
|         true | ||||
|     } else { | ||||
|         false | ||||
|     }; | ||||
|     let dev_mode = baseurl == "https://api.dev.zoo.dev"; | ||||
|  | ||||
|     let (service, socket) = | ||||
|         LspService::build(|client| kcl_lib::CopilotLspBackend::new_wasm(client, fs, zoo_client, dev_mode)) | ||||
| @ -523,7 +447,7 @@ pub fn default_app_settings() -> Result<JsValue, String> { | ||||
| pub fn parse_app_settings(toml_str: &str) -> Result<JsValue, String> { | ||||
|     console_error_panic_hook::set_once(); | ||||
|  | ||||
|     let settings = kcl_lib::Configuration::backwards_compatible_toml_parse(&toml_str).map_err(|e| e.to_string())?; | ||||
|     let settings = kcl_lib::Configuration::backwards_compatible_toml_parse(toml_str).map_err(|e| e.to_string())?; | ||||
|  | ||||
|     // The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the | ||||
|     // gloo-serialize crate instead. | ||||
| @ -548,7 +472,7 @@ pub fn parse_project_settings(toml_str: &str) -> Result<JsValue, String> { | ||||
|     console_error_panic_hook::set_once(); | ||||
|  | ||||
|     let settings = | ||||
|         kcl_lib::ProjectConfiguration::backwards_compatible_toml_parse(&toml_str).map_err(|e| e.to_string())?; | ||||
|         kcl_lib::ProjectConfiguration::backwards_compatible_toml_parse(toml_str).map_err(|e| e.to_string())?; | ||||
|  | ||||
|     // The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the | ||||
|     // gloo-serialize crate instead. | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| //! Cache testing framework. | ||||
|  | ||||
| use anyhow::Result; | ||||
| use kcl_lib::{ExecError, ExecState}; | ||||
| use kcl_lib::{bust_cache, ExecError, ExecOutcome}; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| struct Variation<'a> { | ||||
| @ -12,15 +12,14 @@ struct Variation<'a> { | ||||
| async fn cache_test( | ||||
|     test_name: &str, | ||||
|     variations: Vec<Variation<'_>>, | ||||
| ) -> Result<Vec<(String, image::DynamicImage, ExecState)>> { | ||||
| ) -> Result<Vec<(String, image::DynamicImage, ExecOutcome)>> { | ||||
|     let first = variations | ||||
|         .first() | ||||
|         .ok_or_else(|| anyhow::anyhow!("No variations provided for test '{}'", test_name))?; | ||||
|  | ||||
|     let mut ctx = kcl_lib::ExecutorContext::new_with_client(first.settings.clone(), None, None).await?; | ||||
|     let mut exec_state = kcl_lib::ExecState::new(&ctx.settings); | ||||
|  | ||||
|     let mut old_ast_state = None; | ||||
|     bust_cache().await; | ||||
|     let mut img_results = Vec::new(); | ||||
|     for (index, variation) in variations.iter().enumerate() { | ||||
|         let program = kcl_lib::Program::parse_no_errs(variation.code)?; | ||||
| @ -28,14 +27,7 @@ async fn cache_test( | ||||
|         // set the new settings. | ||||
|         ctx.settings = variation.settings.clone(); | ||||
|  | ||||
|         ctx.run( | ||||
|             kcl_lib::CacheInformation { | ||||
|                 old: old_ast_state, | ||||
|                 new_ast: program.ast.clone(), | ||||
|             }, | ||||
|             &mut exec_state, | ||||
|         ) | ||||
|         .await?; | ||||
|         let outcome = ctx.run_with_caching(program).await?; | ||||
|         let snapshot_png_bytes = ctx.prepare_snapshot().await?.contents.0; | ||||
|  | ||||
|         // Decode the snapshot, return it. | ||||
| @ -46,14 +38,7 @@ async fn cache_test( | ||||
|         // Save the snapshot. | ||||
|         let path = crate::assert_out(&format!("cache_{}_{}", test_name, index), &img); | ||||
|  | ||||
|         img_results.push((path, img, exec_state.clone())); | ||||
|  | ||||
|         // Prepare the last state. | ||||
|         old_ast_state = Some(kcl_lib::OldAstState { | ||||
|             ast: program.ast, | ||||
|             exec_state: exec_state.clone(), | ||||
|             settings: variation.settings.clone(), | ||||
|         }); | ||||
|         img_results.push((path, img, outcome)); | ||||
|     } | ||||
|  | ||||
|     ctx.close().await; | ||||
| @ -254,19 +239,19 @@ extrude(sketch001, length = 4) | ||||
|     .await | ||||
|     .unwrap(); | ||||
|  | ||||
|     let first = result.first().unwrap(); | ||||
|     let second = result.last().unwrap(); | ||||
|     let first = &result.first().unwrap().2; | ||||
|     let second = &result.last().unwrap().2; | ||||
|  | ||||
|     assert!( | ||||
|         first.2.global.artifact_commands.len() < second.2.global.artifact_commands.len(), | ||||
|         first.artifact_commands.len() < second.artifact_commands.len(), | ||||
|         "Second should have all the artifact commands of the first, plus more. first={:?}, second={:?}", | ||||
|         first.2.global.artifact_commands.len(), | ||||
|         second.2.global.artifact_commands.len() | ||||
|         first.artifact_commands.len(), | ||||
|         second.artifact_commands.len() | ||||
|     ); | ||||
|     assert!( | ||||
|         first.2.global.artifact_responses.len() < second.2.global.artifact_responses.len(), | ||||
|         "Second should have all the artifact responses of the first, plus more. first={:?}, second={:?}", | ||||
|         first.2.global.artifact_responses.len(), | ||||
|         second.2.global.artifact_responses.len() | ||||
|         first.artifact_graph.len() < second.artifact_graph.len(), | ||||
|         "Second should have all the artifacts of the first, plus more. first={:?}, second={:?}", | ||||
|         first.artifact_graph.len(), | ||||
|         second.artifact_graph.len() | ||||
|     ); | ||||
| } | ||||
|  | ||||
| @ -11,7 +11,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, Modu | ||||
|     let program = Program::parse_no_errs(code)?; | ||||
|     let ctx = kcl_lib::ExecutorContext::new_with_default_client(Default::default()).await?; | ||||
|     let mut exec_state = ExecState::new(&ctx.settings); | ||||
|     ctx.run(program.clone().into(), &mut exec_state).await?; | ||||
|     ctx.run(&program, &mut exec_state).await?; | ||||
|  | ||||
|     // We need to get the sketch ID. | ||||
|     // Get the sketch ID from memory. | ||||
|  | ||||
		Reference in New Issue
	
	Block a user