Execution refactoring (#7376)

* Move import graph to execution

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor artifact handling

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor caching to separate global state from per-module state

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2025-06-05 15:56:43 +12:00
committed by GitHub
parent c25dfabc94
commit 33d5a9cdc1
8 changed files with 421 additions and 426 deletions

View File

@ -676,6 +676,7 @@ impl EdgeCut {
#[serde(rename_all = "camelCase")]
pub struct ArtifactGraph {
map: IndexMap<ArtifactId, Artifact>,
item_count: usize,
}
impl ArtifactGraph {
@ -711,10 +712,10 @@ pub(super) fn build_artifact_graph(
artifact_commands: &[ArtifactCommand],
responses: &IndexMap<Uuid, WebSocketResponse>,
ast: &Node<Program>,
cached_body_items: usize,
exec_artifacts: &mut IndexMap<ArtifactId, Artifact>,
initial_graph: ArtifactGraph,
) -> Result<ArtifactGraph, KclError> {
let item_count = initial_graph.item_count;
let mut map = initial_graph.into_map();
let mut path_to_plane_id_map = FnvHashMap::default();
@ -725,7 +726,7 @@ pub(super) fn build_artifact_graph(
for exec_artifact in exec_artifacts.values_mut() {
// Note: We only have access to the new AST. So if these artifacts
// somehow came from cached AST, this won't fill in anything.
fill_in_node_paths(exec_artifact, ast, cached_body_items);
fill_in_node_paths(exec_artifact, ast, item_count);
}
for artifact_command in artifact_commands {
@ -752,7 +753,7 @@ pub(super) fn build_artifact_graph(
&flattened_responses,
&path_to_plane_id_map,
ast,
cached_body_items,
item_count,
exec_artifacts,
)?;
for artifact in artifact_updates {
@ -765,7 +766,10 @@ pub(super) fn build_artifact_graph(
merge_artifact_into_map(&mut map, exec_artifact.clone());
}
Ok(ArtifactGraph { map })
Ok(ArtifactGraph {
map,
item_count: item_count + ast.body.len(),
})
}
/// These may have been created with placeholder `CodeRef`s because we didn't

View File

@ -6,25 +6,31 @@ use itertools::{EitherOrBoth, Itertools};
use tokio::sync::RwLock;
use crate::{
execution::{annotations, memory::Stack, state::ModuleInfoMap, EnvironmentRef, ExecState, ExecutorSettings},
execution::{
annotations,
memory::Stack,
state::{self as exec_state, ModuleInfoMap},
EnvironmentRef, ExecutorSettings,
},
parsing::ast::types::{Annotation, Node, Program},
walk::Node as WalkNode,
ExecOutcome, ExecutorContext,
};
lazy_static::lazy_static! {
/// A static mutable lock for updating the last successful execution state for the cache.
static ref OLD_AST: Arc<RwLock<Option<OldAstState>>> = Default::default();
static ref OLD_AST: Arc<RwLock<Option<GlobalState>>> = Default::default();
// The last successful run's memory. Not cleared after an unssuccessful run.
static ref PREV_MEMORY: Arc<RwLock<Option<(Stack, ModuleInfoMap)>>> = Default::default();
}
/// Read the old ast memory from the lock.
pub(crate) async fn read_old_ast() -> Option<OldAstState> {
pub(super) async fn read_old_ast() -> Option<GlobalState> {
let old_ast = OLD_AST.read().await;
old_ast.clone()
}
pub(super) async fn write_old_ast(old_state: OldAstState) {
pub(super) async fn write_old_ast(old_state: GlobalState) {
let mut old_ast = OLD_AST.write().await;
*old_ast = Some(old_state);
}
@ -34,7 +40,7 @@ pub(crate) async fn read_old_memory() -> Option<(Stack, ModuleInfoMap)> {
old_mem.clone()
}
pub(super) async fn write_old_memory(mem: (Stack, ModuleInfoMap)) {
pub(crate) async fn write_old_memory(mem: (Stack, ModuleInfoMap)) {
let mut old_mem = PREV_MEMORY.write().await;
*old_mem = Some(mem);
}
@ -56,16 +62,73 @@ pub struct CacheInformation<'a> {
pub settings: &'a ExecutorSettings,
}
/// The old ast and program memory.
/// The cached state of the whole program.
#[derive(Debug, Clone)]
pub struct OldAstState {
/// The ast.
pub ast: Node<Program>,
pub(super) struct GlobalState {
pub(super) main: ModuleState,
/// The exec state.
pub exec_state: ExecState,
pub(super) exec_state: exec_state::GlobalState,
/// The last settings used for execution.
pub settings: crate::execution::ExecutorSettings,
pub result_env: EnvironmentRef,
pub(super) settings: ExecutorSettings,
}
impl GlobalState {
pub fn new(
state: exec_state::ExecState,
settings: ExecutorSettings,
ast: Node<Program>,
result_env: EnvironmentRef,
) -> Self {
Self {
main: ModuleState {
ast,
exec_state: state.mod_local,
result_env,
},
exec_state: state.global,
settings,
}
}
pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
self.settings = settings;
self
}
pub fn reconstitute_exec_state(&self) -> exec_state::ExecState {
exec_state::ExecState {
global: self.exec_state.clone(),
mod_local: self.main.exec_state.clone(),
}
}
pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState.
ExecOutcome {
variables: self.main.exec_state.variables(self.main.result_env),
filenames: self.exec_state.filenames(),
#[cfg(feature = "artifact-graph")]
operations: self.exec_state.artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_commands: self.exec_state.artifacts.commands,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.exec_state.artifacts.graph,
errors: self.exec_state.errors,
default_planes: ctx.engine.get_default_planes().read().await.clone(),
}
}
}
/// Per-module cached state
#[derive(Debug, Clone)]
pub(super) struct ModuleState {
/// The AST of the module.
pub(super) ast: Node<Program>,
/// The ExecState of the module.
pub(super) exec_state: exec_state::ModuleState,
/// The memory env for the module.
pub(super) result_env: EnvironmentRef,
}
/// The result of a cache check.
@ -79,9 +142,6 @@ pub(super) enum CacheResult {
reapply_settings: bool,
/// The program that needs to be executed.
program: Node<Program>,
/// The number of body items that were cached and omitted from the
/// program that needs to be executed. Used to compute [`crate::NodePath`].
cached_body_items: usize,
},
/// Check only the imports, and not the main program.
/// Before sending this we already checked the main program and it is the same.
@ -146,7 +206,6 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf
// We know they have the same imports because the ast is the same.
// If we have no imports, we can skip this.
if !old.ast.has_import_statements() {
println!("No imports, no need to check.");
return CacheResult::NoAction(reapply_settings);
}
@ -194,7 +253,6 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf
clear_scene: true,
reapply_settings: true,
program: new.ast.clone(),
cached_body_items: 0,
};
}
@ -223,7 +281,6 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
clear_scene: true,
reapply_settings,
program: new_ast,
cached_body_items: 0,
};
}
@ -244,7 +301,6 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
clear_scene: true,
reapply_settings,
program: new_ast,
cached_body_items: 0,
}
}
std::cmp::Ordering::Greater => {
@ -261,7 +317,6 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
clear_scene: false,
reapply_settings,
program: new_ast,
cached_body_items: old_ast.body.len(),
}
}
std::cmp::Ordering::Equal => {
@ -600,7 +655,6 @@ startSketchOn(XY)
clear_scene: true,
reapply_settings: true,
program: new_program.ast,
cached_body_items: 0,
}
);
}
@ -639,7 +693,6 @@ startSketchOn(XY)
clear_scene: true,
reapply_settings: true,
program: new_program.ast,
cached_body_items: 0,
}
);
}

View File

@ -0,0 +1,358 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use anyhow::Result;
use crate::{
errors::KclErrorDetails,
execution::typed_path::TypedPath,
modules::{ModulePath, ModuleRepr},
parsing::ast::types::{ImportPath, ImportStatement, Node as AstNode},
walk::{Node, Visitable},
ExecState, ExecutorContext, KclError, ModuleId, SourceRange,
};
/// Specific dependency between two modules. The 0th element of this info
/// is the "importing" module, the 1st is the "imported" module. The 0th
/// module *depends on* the 1st module.
type Dependency = (String, String);
type Graph = Vec<Dependency>;
pub(crate) type DependencyInfo = (AstNode<ImportStatement>, ModuleId, ModulePath, ModuleRepr);
pub(crate) type UniverseMap = HashMap<TypedPath, AstNode<ImportStatement>>;
pub(crate) type Universe = HashMap<String, DependencyInfo>;
/// Process a number of programs, returning the graph of dependencies.
///
/// This will (currently) return a list of lists of IDs that can be safely
/// run concurrently. Each "stage" is blocking in this model, which will
/// change in the future. Don't use this function widely, yet.
#[allow(clippy::iter_over_hash_type)]
pub(crate) fn import_graph(progs: &Universe, ctx: &ExecutorContext) -> Result<Vec<Vec<String>>, KclError> {
let mut graph = Graph::new();
for (name, (_, _, path, repr)) in progs.iter() {
graph.extend(
import_dependencies(path, repr, ctx)?
.into_iter()
.map(|(dependency, _, _)| (name.clone(), dependency))
.collect::<Vec<_>>(),
);
}
let all_modules: Vec<&str> = progs.keys().map(|v| v.as_str()).collect();
topsort(&all_modules, graph)
}
#[allow(clippy::iter_over_hash_type)]
fn topsort(all_modules: &[&str], graph: Graph) -> Result<Vec<Vec<String>>, KclError> {
if all_modules.is_empty() {
return Ok(vec![]);
}
let mut dep_map = HashMap::<String, Vec<String>>::new();
for (dependent, dependency) in graph.iter() {
let mut dependencies = dep_map.remove(dependent).unwrap_or_default();
dependencies.push(dependency.to_owned());
dep_map.insert(dependent.to_owned(), dependencies);
}
// dep_map now contains reverse dependencies. For each module, it's a
// list of what things are "waiting on it". A non-empty value for a key
// means it's currently blocked.
let mut waiting_modules = all_modules.to_owned();
let mut order = vec![];
loop {
// Each pass through we need to find any modules which have nothing
// "pointing at it" -- so-called reverse dependencies. This is an entry
// that is either not in the dep_map OR an empty list.
let mut stage_modules: Vec<String> = vec![];
for module in &waiting_modules {
let module = module.to_string();
if dep_map.get(&module).map(|v| v.len()).unwrap_or(0) == 0 {
// if it's None or empty, this is a node that we can process,
// and remove from the graph.
stage_modules.push(module.to_string());
}
}
for stage_module in &stage_modules {
// remove the ready-to-run module from the waiting list
waiting_modules.retain(|v| *v != stage_module.as_str());
// remove any dependencies for the next run
for (_, waiting_for) in dep_map.iter_mut() {
waiting_for.retain(|v| v != stage_module);
}
}
if stage_modules.is_empty() {
waiting_modules.sort();
return Err(KclError::new_import_cycle(KclErrorDetails::new(
format!("circular import of modules not allowed: {}", waiting_modules.join(", ")),
// TODO: we can get the right import lines from the AST, but we don't
vec![SourceRange::default()],
)));
}
// not strictly needed here, but perhaps helpful to avoid thinking
// there's any implied ordering as well as helping to make tests
// easier.
stage_modules.sort();
order.push(stage_modules);
if waiting_modules.is_empty() {
break;
}
}
Ok(order)
}
type ImportDependencies = Vec<(String, AstNode<ImportStatement>, ModulePath)>;
fn import_dependencies(
path: &ModulePath,
repr: &ModuleRepr,
ctx: &ExecutorContext,
) -> Result<ImportDependencies, KclError> {
let ModuleRepr::Kcl(prog, _) = repr else {
// It has no dependencies, so return an empty list.
return Ok(vec![]);
};
let ret = Arc::new(Mutex::new(vec![]));
fn walk(
ret: Arc<Mutex<ImportDependencies>>,
node: Node<'_>,
import_from: &ModulePath,
ctx: &ExecutorContext,
) -> Result<(), KclError> {
if let Node::ImportStatement(is) = node {
// We only care about Kcl and Foreign imports for now.
let resolved_path = ModulePath::from_import_path(&is.path, &ctx.settings.project_directory, import_from)?;
match &is.path {
ImportPath::Kcl { filename } => {
// We need to lock the mutex to push the dependency.
// This is a bit of a hack, but it works for now.
ret.lock()
.map_err(|err| {
KclError::new_internal(KclErrorDetails::new(
format!("Failed to lock mutex: {}", err),
Default::default(),
))
})?
.push((filename.to_string(), is.clone(), resolved_path));
}
ImportPath::Foreign { path } => {
ret.lock()
.map_err(|err| {
KclError::new_internal(KclErrorDetails::new(
format!("Failed to lock mutex: {}", err),
Default::default(),
))
})?
.push((path.to_string(), is.clone(), resolved_path));
}
ImportPath::Std { .. } => { // do nothing
}
}
}
for child in node.children().iter() {
walk(ret.clone(), *child, import_from, ctx)?;
}
Ok(())
}
walk(ret.clone(), prog.into(), path, ctx)?;
let ret = ret.lock().map_err(|err| {
KclError::new_internal(KclErrorDetails::new(
format!("Failed to lock mutex: {}", err),
Default::default(),
))
})?;
Ok(ret.clone())
}
/// Mutates the `out` universe with the imported modules. Returns the imports of
/// only `repr`'s non-transitive imports.
pub(crate) async fn import_universe(
ctx: &ExecutorContext,
path: &ModulePath,
repr: &ModuleRepr,
out: &mut Universe,
exec_state: &mut ExecState,
) -> Result<UniverseMap, KclError> {
let modules = import_dependencies(path, repr, ctx)?;
let mut module_imports = HashMap::new();
for (filename, import_stmt, module_path) in modules {
match &module_path {
ModulePath::Main => {
// We only care about what the root module imports.
}
ModulePath::Local { value, .. } => {
module_imports.insert(value.clone(), import_stmt.clone());
}
ModulePath::Std { .. } => {
// We don't care about std imports.
}
}
if out.contains_key(&filename) {
continue;
}
let source_range = SourceRange::from(&import_stmt);
let attrs = &import_stmt.outer_attrs;
let module_id = ctx
.open_module(&import_stmt.path, attrs, &module_path, exec_state, source_range)
.await?;
let repr = {
let Some(module_info) = exec_state.get_module(module_id) else {
return Err(KclError::new_internal(KclErrorDetails::new(
format!("Module {} not found", module_id),
vec![import_stmt.into()],
)));
};
module_info.repr.clone()
};
out.insert(filename, (import_stmt, module_id, module_path.clone(), repr.clone()));
Box::pin(import_universe(ctx, &module_path, &repr, out, exec_state)).await?;
}
Ok(module_imports)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parsing::ast::types::{ImportSelector, Program};
macro_rules! kcl {
( $kcl:expr ) => {{
$crate::parsing::top_level_parse($kcl).unwrap()
}};
}
fn into_module_info(program: AstNode<Program>) -> DependencyInfo {
(
AstNode::no_src(ImportStatement {
selector: ImportSelector::None { alias: None },
path: ImportPath::Kcl { filename: "".into() },
visibility: Default::default(),
digest: None,
}),
ModuleId::default(),
ModulePath::Local { value: "".into() },
ModuleRepr::Kcl(program, None),
)
}
#[tokio::test]
async fn order_imports() {
let mut modules = HashMap::new();
let a = kcl!("");
modules.insert("a.kcl".to_owned(), into_module_info(a));
let b = kcl!(
"
import \"a.kcl\"
"
);
modules.insert("b.kcl".to_owned(), into_module_info(b));
let ctx = ExecutorContext::new_mock(None).await;
let order = import_graph(&modules, &ctx).unwrap();
assert_eq!(vec![vec!["a.kcl".to_owned()], vec!["b.kcl".to_owned()]], order);
}
#[tokio::test]
async fn order_imports_none() {
let mut modules = HashMap::new();
let a = kcl!(
"
y = 2
"
);
modules.insert("a.kcl".to_owned(), into_module_info(a));
let b = kcl!(
"
x = 1
"
);
modules.insert("b.kcl".to_owned(), into_module_info(b));
let ctx = ExecutorContext::new_mock(None).await;
let order = import_graph(&modules, &ctx).unwrap();
assert_eq!(vec![vec!["a.kcl".to_owned(), "b.kcl".to_owned()]], order);
}
#[tokio::test]
async fn order_imports_2() {
let mut modules = HashMap::new();
let a = kcl!("");
modules.insert("a.kcl".to_owned(), into_module_info(a));
let b = kcl!(
"
import \"a.kcl\"
"
);
modules.insert("b.kcl".to_owned(), into_module_info(b));
let c = kcl!(
"
import \"a.kcl\"
"
);
modules.insert("c.kcl".to_owned(), into_module_info(c));
let ctx = ExecutorContext::new_mock(None).await;
let order = import_graph(&modules, &ctx).unwrap();
assert_eq!(
vec![vec!["a.kcl".to_owned()], vec!["b.kcl".to_owned(), "c.kcl".to_owned()]],
order
);
}
#[tokio::test]
async fn order_imports_cycle() {
let mut modules = HashMap::new();
let a = kcl!(
"
import \"b.kcl\"
"
);
modules.insert("a.kcl".to_owned(), into_module_info(a));
let b = kcl!(
"
import \"a.kcl\"
"
);
modules.insert("b.kcl".to_owned(), into_module_info(b));
let ctx = ExecutorContext::new_mock(None).await;
import_graph(&modules, &ctx).unwrap_err();
}
}

View File

@ -5,9 +5,8 @@ use std::sync::Arc;
use anyhow::Result;
#[cfg(feature = "artifact-graph")]
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketchOnFace, StartSketchOnPlane};
use cache::OldAstState;
use cache::GlobalState;
pub use cache::{bust_cache, clear_mem_cache};
#[cfg(feature = "artifact-graph")]
pub use cad_op::{Group, Operation};
pub use geometry::*;
pub use id_generator::IdGenerator;
@ -27,13 +26,12 @@ use serde::{Deserialize, Serialize};
pub use state::{ExecState, MetaSettings};
use uuid::Uuid;
#[cfg(feature = "artifact-graph")]
use crate::execution::artifact::build_artifact_graph;
use crate::{
engine::EngineManager,
errors::{KclError, KclErrorDetails},
execution::{
cache::{CacheInformation, CacheResult},
import_graph::{Universe, UniverseMap},
typed_path::TypedPath,
types::{UnitAngle, UnitLen},
},
@ -41,7 +39,6 @@ use crate::{
modules::{ModuleId, ModulePath, ModuleRepr},
parsing::ast::types::{Expr, ImportPath, NodeRef},
source_range::SourceRange,
walk::{Universe, UniverseMap},
CompilationError, ExecError, KclErrorWithOutputs,
};
@ -55,6 +52,7 @@ pub mod fn_call;
mod geometry;
mod id_generator;
mod import;
mod import_graph;
pub(crate) mod kcl_value;
mod memory;
mod state;
@ -568,7 +566,7 @@ impl ExecutorContext {
// part of the scene).
exec_state.mut_stack().push_new_env_for_scope();
let result = self.inner_run(&program, 0, &mut exec_state, true).await?;
let result = self.inner_run(&program, &mut exec_state, true).await?;
// Restore any temporary variables, then save any newly created variables back to
// memory in case another run wants to use them. Note this is just saved to the preserved
@ -576,7 +574,7 @@ impl ExecutorContext {
let mut mem = exec_state.stack().clone();
let module_infos = exec_state.global.module_infos.clone();
let outcome = exec_state.to_mock_exec_outcome(result.0).await;
let outcome = exec_state.to_mock_exec_outcome(result.0, self).await;
mem.squash_env(result.0);
cache::write_old_memory((mem, module_infos)).await;
@ -587,169 +585,176 @@ impl ExecutorContext {
pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
assert!(!self.is_mock());
let (program, mut exec_state, preserve_mem, cached_body_items, imports_info) = if let Some(OldAstState {
ast: old_ast,
exec_state: mut old_state,
settings: old_settings,
result_env,
}) =
cache::read_old_ast().await
{
let old = CacheInformation {
ast: &old_ast,
settings: &old_settings,
};
let new = CacheInformation {
ast: &program.ast,
settings: &self.settings,
};
// Get the program that actually changed from the old and new information.
let (clear_scene, program, body_items, import_check_info) = match cache::get_changed_program(old, new).await
{
CacheResult::ReExecute {
clear_scene,
reapply_settings,
program: changed_program,
cached_body_items,
} => {
if reapply_settings
&& self
.engine
.reapply_settings(&self.settings, Default::default(), old_state.id_generator())
.await
.is_err()
{
(true, program, cached_body_items, None)
} else {
(
clear_scene,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
cached_body_items,
None,
)
}
}
CacheResult::CheckImportsOnly {
reapply_settings,
ast: changed_program,
} => {
if reapply_settings
&& self
.engine
.reapply_settings(&self.settings, Default::default(), old_state.id_generator())
.await
.is_err()
{
(true, program, old_ast.body.len(), None)
} else {
// We need to check our imports to see if they changed.
let mut new_exec_state = ExecState::new(self);
let (new_universe, new_universe_map) = self.get_universe(&program, &mut new_exec_state).await?;
let mut clear_scene = false;
let mut keys = new_universe.keys().clone().collect::<Vec<_>>();
keys.sort();
for key in keys {
let (_, id, _, _) = &new_universe[key];
if let (Some(source0), Some(source1)) =
(old_state.get_source(*id), new_exec_state.get_source(*id))
{
if source0.source != source1.source {
clear_scene = true;
break;
}
}
}
if !clear_scene {
// Return early we don't need to clear the scene.
let outcome = old_state.to_exec_outcome(result_env).await;
return Ok(outcome);
}
(
clear_scene,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
old_ast.body.len(),
// We only care about this if we are clearing the scene.
if clear_scene {
Some((new_universe, new_universe_map, new_exec_state))
} else {
None
},
)
}
}
CacheResult::NoAction(true) => {
if self
.engine
.reapply_settings(&self.settings, Default::default(), old_state.id_generator())
.await
.is_ok()
{
// We need to update the old ast state with the new settings!!
cache::write_old_ast(OldAstState {
ast: old_ast,
exec_state: old_state.clone(),
settings: self.settings.clone(),
result_env,
})
.await;
let outcome = old_state.to_exec_outcome(result_env).await;
return Ok(outcome);
}
(true, program, old_ast.body.len(), None)
}
CacheResult::NoAction(false) => {
let outcome = old_state.to_exec_outcome(result_env).await;
return Ok(outcome);
}
};
let (exec_state, preserve_mem, universe_info) =
if let Some((new_universe, new_universe_map, mut new_exec_state)) = import_check_info {
// Clear the scene if the imports changed.
self.send_clear_scene(&mut new_exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
(new_exec_state, false, Some((new_universe, new_universe_map)))
} else if clear_scene {
// Pop the execution state, since we are starting fresh.
let mut exec_state = old_state;
exec_state.reset(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
(exec_state, false, None)
} else {
old_state.mut_stack().restore_env(result_env);
(old_state, true, None)
let (program, exec_state, result) = match cache::read_old_ast().await {
Some(mut cached_state) => {
let old = CacheInformation {
ast: &cached_state.main.ast,
settings: &cached_state.settings,
};
let new = CacheInformation {
ast: &program.ast,
settings: &self.settings,
};
(program, exec_state, preserve_mem, body_items, universe_info)
} else {
let mut exec_state = ExecState::new(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
(program, exec_state, false, 0, None)
};
// Get the program that actually changed from the old and new information.
let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
CacheResult::ReExecute {
clear_scene,
reapply_settings,
program: changed_program,
} => {
if reapply_settings
&& self
.engine
.reapply_settings(
&self.settings,
Default::default(),
&mut cached_state.main.exec_state.id_generator,
)
.await
.is_err()
{
(true, program, None)
} else {
(
clear_scene,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
None,
)
}
}
CacheResult::CheckImportsOnly {
reapply_settings,
ast: changed_program,
} => {
if reapply_settings
&& self
.engine
.reapply_settings(
&self.settings,
Default::default(),
&mut cached_state.main.exec_state.id_generator,
)
.await
.is_err()
{
(true, program, None)
} else {
// We need to check our imports to see if they changed.
let mut new_exec_state = ExecState::new(self);
let (new_universe, new_universe_map) =
self.get_universe(&program, &mut new_exec_state).await?;
let result = self
.run_concurrent(&program, cached_body_items, &mut exec_state, imports_info, preserve_mem)
.await;
let clear_scene = new_universe.keys().any(|key| {
let id = new_universe[key].1;
match (
cached_state.exec_state.get_source(id),
new_exec_state.global.get_source(id),
) {
(Some(s0), Some(s1)) => s0.source != s1.source,
_ => false,
}
});
if !clear_scene {
// Return early we don't need to clear the scene.
return Ok(cached_state.into_exec_outcome(self).await);
}
(
true,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
Some((new_universe, new_universe_map, new_exec_state)),
)
}
}
CacheResult::NoAction(true) => {
if self
.engine
.reapply_settings(
&self.settings,
Default::default(),
&mut cached_state.main.exec_state.id_generator,
)
.await
.is_ok()
{
// We need to update the old ast state with the new settings!!
cache::write_old_ast(GlobalState::with_settings(
cached_state.clone(),
self.settings.clone(),
))
.await;
return Ok(cached_state.into_exec_outcome(self).await);
}
(true, program, None)
}
CacheResult::NoAction(false) => {
return Ok(cached_state.into_exec_outcome(self).await);
}
};
let (exec_state, result) = match import_check_info {
Some((new_universe, new_universe_map, mut new_exec_state)) => {
// Clear the scene if the imports changed.
self.send_clear_scene(&mut new_exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let result = self
.run_concurrent(
&program,
&mut new_exec_state,
Some((new_universe, new_universe_map)),
false,
)
.await;
(new_exec_state, result)
}
None if clear_scene => {
// Pop the execution state, since we are starting fresh.
let mut exec_state = cached_state.reconstitute_exec_state();
exec_state.reset(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
(exec_state, result)
}
None => {
let mut exec_state = cached_state.reconstitute_exec_state();
exec_state.mut_stack().restore_env(cached_state.main.result_env);
let result = self.run_concurrent(&program, &mut exec_state, None, true).await;
(exec_state, result)
}
};
(program, exec_state, result)
}
None => {
let mut exec_state = ExecState::new(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
(program, exec_state, result)
}
};
if result.is_err() {
cache::bust_cache().await;
@ -759,15 +764,15 @@ impl ExecutorContext {
let result = result?;
// Save this as the last successful execution to the cache.
cache::write_old_ast(OldAstState {
ast: program.ast,
exec_state: exec_state.clone(),
settings: self.settings.clone(),
result_env: result.0,
})
cache::write_old_ast(GlobalState::new(
exec_state.clone(),
self.settings.clone(),
program.ast,
result.0,
))
.await;
let outcome = exec_state.to_exec_outcome(result.0).await;
let outcome = exec_state.to_exec_outcome(result.0, self).await;
Ok(outcome)
}
@ -782,11 +787,11 @@ impl ExecutorContext {
program: &crate::Program,
exec_state: &mut ExecState,
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
self.run_concurrent(program, 0, exec_state, None, false).await
self.run_concurrent(program, exec_state, None, false).await
}
/// Perform the execution of a program using a concurrent
/// execution model. This has the same signature as [Self::run].
/// execution model.
///
/// You can optionally pass in some initialization memory for partial
/// execution.
@ -795,13 +800,12 @@ impl ExecutorContext {
pub async fn run_concurrent(
&self,
program: &crate::Program,
cached_body_items: usize,
exec_state: &mut ExecState,
universe_info: Option<(Universe, UniverseMap)>,
preserve_mem: bool,
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
// Reuse our cached universe if we have one.
#[allow(unused_variables)]
let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
(universe, universe_map)
} else {
@ -815,29 +819,8 @@ impl ExecutorContext {
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
for modules in crate::walk::import_graph(&universe, self)
.map_err(|err| {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
err,
exec_state.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
exec_state.global.operations.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_commands.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes.clone(),
)
})?
for modules in import_graph::import_graph(&universe, self)
.map_err(|err| exec_state.error_with_outputs(err, default_planes.clone()))?
.into_iter()
{
#[cfg(not(target_arch = "wasm32"))]
@ -859,7 +842,6 @@ impl ExecutorContext {
let module_path = module_path.clone();
let source_range = SourceRange::from(import_stmt);
#[cfg(feature = "artifact-graph")]
match &module_path {
ModulePath::Main => {
// This should never happen.
@ -868,7 +850,7 @@ impl ExecutorContext {
// We only want to display the top-level module imports in
// the Feature Tree, not transitive imports.
if universe_map.contains_key(value) {
exec_state.global.operations.push(Operation::GroupBegin {
exec_state.push_op(Operation::GroupBegin {
group: Group::ModuleInstance {
name: value.file_name().unwrap_or_default(),
module_id,
@ -878,7 +860,7 @@ impl ExecutorContext {
// Due to concurrent execution, we cannot easily
// group operations by module. So we leave the
// group empty and close it immediately.
exec_state.global.operations.push(Operation::GroupEnd);
exec_state.push_op(Operation::GroupEnd);
}
}
ModulePath::Std { .. } => {
@ -923,7 +905,6 @@ impl ExecutorContext {
#[cfg(target_arch = "wasm32")]
{
wasm_bindgen_futures::spawn_local(async move {
//set.spawn(async move {
let mut exec_state = exec_state;
let exec_ctxt = exec_ctxt;
@ -993,33 +974,13 @@ impl ExecutorContext {
exec_state.global.module_infos[&module_id].restore_repr(repr);
}
Err(e) => {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
return Err(KclErrorWithOutputs::new(
e,
exec_state.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
exec_state.global.operations.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_commands.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes,
));
return Err(exec_state.error_with_outputs(e, default_planes));
}
}
}
}
self.inner_run(program, cached_body_items, exec_state, preserve_mem)
.await
self.inner_run(program, exec_state, preserve_mem).await
}
/// Get the universe & universe map of the program.
@ -1035,7 +996,7 @@ impl ExecutorContext {
let default_planes = self.engine.get_default_planes().read().await.clone();
let root_imports = crate::walk::import_universe(
let root_imports = import_graph::import_universe(
self,
&ModulePath::Main,
&ModuleRepr::Kcl(program.ast.clone(), None),
@ -1043,29 +1004,7 @@ impl ExecutorContext {
exec_state,
)
.await
.map_err(|err| {
println!("Error: {err:?}");
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
err,
exec_state.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
exec_state.global.operations.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_commands.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes,
)
})?;
.map_err(|err| exec_state.error_with_outputs(err, default_planes))?;
Ok((universe, root_imports))
}
@ -1075,7 +1014,6 @@ impl ExecutorContext {
async fn inner_run(
&self,
program: &crate::Program,
cached_body_items: usize,
exec_state: &mut ExecState,
preserve_mem: bool,
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
@ -1089,7 +1027,7 @@ impl ExecutorContext {
let default_planes = self.engine.get_default_planes().read().await.clone();
let result = self
.execute_and_build_graph(&program.ast, cached_body_items, exec_state, preserve_mem)
.execute_and_build_graph(&program.ast, exec_state, preserve_mem)
.await;
crate::log::log(format!(
@ -1098,28 +1036,7 @@ impl ExecutorContext {
));
crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
let env_ref = result.map_err(|e| {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
e,
exec_state.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
exec_state.global.operations.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_commands.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes.clone(),
)
})?;
let env_ref = result.map_err(|e| exec_state.error_with_outputs(e, default_planes))?;
if !self.is_mock() {
let mut mem = exec_state.stack().deep_clone();
@ -1136,7 +1053,6 @@ impl ExecutorContext {
async fn execute_and_build_graph(
&self,
program: NodeRef<'_, crate::parsing::ast::types::Program>,
#[cfg_attr(not(feature = "artifact-graph"), expect(unused))] cached_body_items: usize,
exec_state: &mut ExecState,
preserve_mem: bool,
) -> Result<EnvironmentRef, KclError> {
@ -1163,40 +1079,9 @@ impl ExecutorContext {
// and should be dropped.
self.engine.clear_queues().await;
#[cfg(feature = "artifact-graph")]
{
let new_commands = self.engine.take_artifact_commands().await;
let new_responses = self.engine.take_responses().await;
let initial_graph = exec_state.global.artifact_graph.clone();
// Build the artifact graph.
let graph_result = build_artifact_graph(
&new_commands,
&new_responses,
program,
cached_body_items,
&mut exec_state.global.artifacts,
initial_graph,
);
// Move the artifact commands and responses into ExecState to
// simplify cache management and error creation.
exec_state.global.artifact_commands.extend(new_commands);
exec_state.global.artifact_responses.extend(new_responses);
match graph_result {
Ok(artifact_graph) => {
exec_state.global.artifact_graph = artifact_graph;
exec_result.map(|(_, env_ref, _)| env_ref)
}
Err(err) => {
// Prefer the exec error.
exec_result.and(Err(err))
}
}
}
#[cfg(not(feature = "artifact-graph"))]
{
exec_result.map(|(_, env_ref, _)| env_ref)
match exec_state.build_artifact_graph(&self.engine, program).await {
Ok(_) => exec_result.map(|(_, env_ref, _)| env_ref),
Err(err) => exec_result.and(Err(err)),
}
}
@ -2198,7 +2083,7 @@ w = f() + f()
}
// Get the id_generator from the first execution.
let id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
let code = r#"sketch001 = startSketchOn(XZ)
|> startProfile(at = [62.74, 206.13])
@ -2219,7 +2104,7 @@ w = f() + f()
// Execute the program.
ctx.run_with_caching(program).await.unwrap();
let new_id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
assert_eq!(id_generator, new_id_generator);
}

View File

@ -12,19 +12,19 @@ use uuid::Uuid;
use crate::execution::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
use crate::{
errors::{KclError, KclErrorDetails, Severity},
exec::DefaultPlanes,
execution::{
annotations,
cad_op::Operation,
id_generator::IdGenerator,
memory::{ProgramMemory, Stack},
types,
types::NumericType,
types::{self, NumericType},
EnvironmentRef, ExecOutcome, ExecutorSettings, KclValue, UnitAngle, UnitLen,
},
modules::{ModuleId, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr, ModuleSource},
parsing::ast::types::Annotation,
parsing::ast::types::{Annotation, NodeRef},
source_range::SourceRange,
CompilationError,
CompilationError, EngineManager, ExecutorContext, KclErrorWithOutputs,
};
/// State for executing a program.
@ -32,7 +32,6 @@ use crate::{
pub struct ExecState {
pub(super) global: GlobalState,
pub(super) mod_local: ModuleState,
pub(super) exec_context: Option<super::ExecutorContext>,
}
pub type ModuleInfoMap = IndexMap<ModuleId, ModuleInfo>;
@ -45,33 +44,39 @@ pub(super) struct GlobalState {
pub id_to_source: IndexMap<ModuleId, ModuleSource>,
/// Map from module ID to module info.
pub module_infos: ModuleInfoMap,
/// Output map of UUIDs to artifacts.
#[cfg(feature = "artifact-graph")]
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.
#[cfg(feature = "artifact-graph")]
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.
#[cfg(feature = "artifact-graph")]
pub artifact_responses: IndexMap<Uuid, WebSocketResponse>,
/// Output artifact graph.
#[cfg(feature = "artifact-graph")]
pub artifact_graph: ArtifactGraph,
/// Operations that have been performed in execution order, for display in
/// the Feature Tree.
#[cfg(feature = "artifact-graph")]
pub operations: Vec<Operation>,
/// Module loader.
pub mod_loader: ModuleLoader,
/// Errors and warnings.
pub errors: Vec<CompilationError>,
#[allow(dead_code)]
pub artifacts: ArtifactState,
}
#[cfg(feature = "artifact-graph")]
#[derive(Debug, Clone, Default)]
pub(super) struct ArtifactState {
/// 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 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 responses: IndexMap<Uuid, WebSocketResponse>,
/// Output artifact graph.
pub graph: ArtifactGraph,
/// Operations that have been performed in execution order, for display in
/// the Feature Tree.
pub operations: Vec<Operation>,
}
#[cfg(not(feature = "artifact-graph"))]
#[derive(Debug, Clone, Default)]
pub(super) struct ArtifactState {}
#[derive(Debug, Clone)]
pub(super) struct ModuleState {
/// The id generator for this module.
@ -98,7 +103,6 @@ impl ExecState {
ExecState {
global: GlobalState::new(&exec_context.settings),
mod_local: ModuleState::new(ModulePath::Main, ProgramMemory::new(), Default::default()),
exec_context: Some(exec_context.clone()),
}
}
@ -108,7 +112,6 @@ impl ExecState {
*self = ExecState {
global,
mod_local: ModuleState::new(self.mod_local.path.clone(), ProgramMemory::new(), Default::default()),
exec_context: Some(exec_context.clone()),
};
}
@ -130,45 +133,26 @@ impl ExecState {
/// 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 async fn to_exec_outcome(self, main_ref: EnvironmentRef) -> ExecOutcome {
pub async fn to_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState.
ExecOutcome {
variables: self
.stack()
.find_all_in_env(main_ref)
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
variables: self.mod_local.variables(main_ref),
filenames: self.global.filenames(),
#[cfg(feature = "artifact-graph")]
operations: self.global.operations,
operations: self.global.artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_commands: self.global.artifact_commands,
artifact_commands: self.global.artifacts.commands,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.global.artifact_graph,
artifact_graph: self.global.artifacts.graph,
errors: self.global.errors,
filenames: self
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect(),
default_planes: if let Some(ctx) = &self.exec_context {
ctx.engine.get_default_planes().read().await.clone()
} else {
None
},
default_planes: ctx.engine.get_default_planes().read().await.clone(),
}
}
pub async fn to_mock_exec_outcome(self, main_ref: EnvironmentRef) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState.
pub async fn to_mock_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
ExecOutcome {
variables: self
.stack()
.find_all_in_env(main_ref)
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
variables: self.mod_local.variables(main_ref),
#[cfg(feature = "artifact-graph")]
operations: Default::default(),
#[cfg(feature = "artifact-graph")]
@ -177,11 +161,7 @@ impl ExecState {
artifact_graph: Default::default(),
errors: self.global.errors,
filenames: Default::default(),
default_planes: if let Some(ctx) = &self.exec_context {
ctx.engine.get_default_planes().read().await.clone()
} else {
None
},
default_planes: ctx.engine.get_default_planes().read().await.clone(),
}
}
@ -204,12 +184,12 @@ impl ExecState {
#[cfg(feature = "artifact-graph")]
pub(crate) fn add_artifact(&mut self, artifact: Artifact) {
let id = artifact.id();
self.global.artifacts.insert(id, artifact);
self.global.artifacts.artifacts.insert(id, artifact);
}
pub(crate) fn push_op(&mut self, op: Operation) {
#[cfg(feature = "artifact-graph")]
self.global.operations.push(op);
self.global.artifacts.operations.push(op);
#[cfg(not(feature = "artifact-graph"))]
drop(op);
}
@ -251,10 +231,6 @@ impl ExecState {
self.global.id_to_source.insert(id, source.clone());
}
pub(super) fn get_source(&self, id: ModuleId) -> Option<&ModuleSource> {
self.global.id_to_source.get(&id)
}
pub(super) fn add_module(&mut self, id: ModuleId, path: ModulePath, repr: ModuleRepr) {
debug_assert!(self.global.path_to_source_id.contains_key(&path));
let module_info = ModuleInfo { id, repr, path };
@ -300,6 +276,71 @@ impl ExecState {
pub(crate) fn pipe_value(&self) -> Option<&KclValue> {
self.mod_local.pipe_value.as_ref()
}
pub(crate) fn error_with_outputs(
&self,
error: KclError,
default_planes: Option<DefaultPlanes>,
) -> KclErrorWithOutputs {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = self
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
error,
self.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
self.global.artifacts.operations.clone(),
#[cfg(feature = "artifact-graph")]
self.global.artifacts.commands.clone(),
#[cfg(feature = "artifact-graph")]
self.global.artifacts.graph.clone(),
module_id_to_module_path,
self.global.id_to_source.clone(),
default_planes,
)
}
#[cfg(feature = "artifact-graph")]
pub(crate) async fn build_artifact_graph(
&mut self,
engine: &Arc<Box<dyn EngineManager>>,
program: NodeRef<'_, crate::parsing::ast::types::Program>,
) -> Result<(), KclError> {
let new_commands = engine.take_artifact_commands().await;
let new_responses = engine.take_responses().await;
let initial_graph = self.global.artifacts.graph.clone();
// Build the artifact graph.
let graph_result = crate::execution::artifact::build_artifact_graph(
&new_commands,
&new_responses,
program,
&mut self.global.artifacts.artifacts,
initial_graph,
);
// Move the artifact commands and responses into ExecState to
// simplify cache management and error creation.
self.global.artifacts.commands.extend(new_commands);
self.global.artifacts.responses.extend(new_responses);
let artifact_graph = graph_result?;
self.global.artifacts.graph = artifact_graph;
Ok(())
}
#[cfg(not(feature = "artifact-graph"))]
pub(crate) async fn build_artifact_graph(
&mut self,
_engine: &Arc<Box<dyn EngineManager>>,
_program: NodeRef<'_, crate::parsing::ast::types::Program>,
) -> Result<(), KclError> {
Ok(())
}
}
impl GlobalState {
@ -307,16 +348,7 @@ impl GlobalState {
let mut global = GlobalState {
path_to_source_id: Default::default(),
module_infos: Default::default(),
#[cfg(feature = "artifact-graph")]
artifacts: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_commands: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_responses: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_graph: Default::default(),
#[cfg(feature = "artifact-graph")]
operations: Default::default(),
mod_loader: Default::default(),
errors: Default::default(),
id_to_source: Default::default(),
@ -339,6 +371,14 @@ impl GlobalState {
.insert(ModulePath::Local { value: root_path }, root_id);
global
}
pub(super) fn filenames(&self) -> IndexMap<ModuleId, ModulePath> {
self.path_to_source_id.iter().map(|(k, v)| ((*v), k.clone())).collect()
}
pub(super) fn get_source(&self, id: ModuleId) -> Option<&ModuleSource> {
self.id_to_source.get(&id)
}
}
impl ModuleState {
@ -358,6 +398,13 @@ impl ModuleState {
},
}
}
pub(super) fn variables(&self, main_ref: EnvironmentRef) -> IndexMap<String, KclValue> {
self.stack
.find_all_in_env(main_ref)
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]