Change to use nodePath instead of sourceRange for Operations (#7320)

* Add NodePath to operations

* Change to use nodePath to get pathToNode instead of sourceRange

* Add additional node path unit test

* Update output

* Fix import statement NodePaths

* Update output

* Factor into function
This commit is contained in:
Jonathan Tran
2025-06-05 12:24:34 -04:00
committed by GitHub
parent b5f81cb84a
commit f6a3a3d0cd
195 changed files with 66733 additions and 5281 deletions

View File

@ -676,7 +676,7 @@ impl EdgeCut {
#[serde(rename_all = "camelCase")]
pub struct ArtifactGraph {
map: IndexMap<ArtifactId, Artifact>,
item_count: usize,
pub(super) item_count: usize,
}
impl ArtifactGraph {

View File

@ -1,13 +1,12 @@
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::Serialize;
use super::{types::NumericType, ArtifactId, KclValue};
use crate::{ModuleId, SourceRange};
use crate::{ModuleId, NodePath, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations
/// timeline.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(tag = "type")]
pub enum Operation {
@ -18,6 +17,8 @@ pub enum Operation {
unlabeled_arg: Option<OpArg>,
/// The labeled keyword arguments to the function.
labeled_args: IndexMap<String, OpArg>,
/// The node path of the operation in the source code.
node_path: NodePath,
/// The source range of the operation in the source code.
source_range: SourceRange,
/// True if the operation resulted in an error.
@ -28,6 +29,8 @@ pub enum Operation {
GroupBegin {
/// The details of the group.
group: Group,
/// The node path of the operation in the source code.
node_path: NodePath,
/// The source range of the operation in the source code.
source_range: SourceRange,
},
@ -64,7 +67,7 @@ impl Operation {
}
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(tag = "type")]
#[expect(clippy::large_enum_variant)]
@ -95,7 +98,7 @@ pub enum Group {
}
/// An argument to a CAD modeling operation.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpArg {
@ -119,7 +122,7 @@ fn is_false(b: &bool) -> bool {
/// A KCL value used in Operations. `ArtifactId`s are used to refer to the
/// actual scene objects. Any data not needed in the UI may be omitted.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(tag = "type")]
pub enum OpKclValue {
@ -177,21 +180,21 @@ pub enum OpKclValue {
pub type OpKclObjectFields = IndexMap<String, OpKclValue>;
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpSketch {
artifact_id: ArtifactId,
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpSolid {
artifact_id: ArtifactId,
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpHelix {

View File

@ -14,7 +14,7 @@ use crate::{
parsing::ast::types::{CallExpressionKw, DefaultParamVal, FunctionExpression, Node, Program, Type},
source_range::SourceRange,
std::StdFn,
CompilationError,
CompilationError, NodePath,
};
#[derive(Debug, Clone)]
@ -322,6 +322,7 @@ impl FunctionDefinition<'_> {
.unlabeled_kw_arg_unconverted()
.map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
labeled_args: op_labeled_args,
node_path: NodePath::placeholder(),
source_range: callsite,
is_error: false,
})
@ -337,6 +338,7 @@ impl FunctionDefinition<'_> {
.map(|arg| OpArg::new(OpKclValue::from(&arg.1.value), arg.1.source_range)),
labeled_args: op_labeled_args,
},
node_path: NodePath::placeholder(),
source_range: callsite,
});

View File

@ -7,6 +7,7 @@ use anyhow::Result;
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketchOnFace, StartSketchOnPlane};
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;
@ -842,31 +843,14 @@ impl ExecutorContext {
let module_path = module_path.clone();
let source_range = SourceRange::from(import_stmt);
match &module_path {
ModulePath::Main => {
// This should never happen.
}
ModulePath::Local { value, .. } => {
// 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.push_op(Operation::GroupBegin {
group: Group::ModuleInstance {
name: value.file_name().unwrap_or_default(),
module_id,
},
source_range,
});
// Due to concurrent execution, we cannot easily
// group operations by module. So we leave the
// group empty and close it immediately.
exec_state.push_op(Operation::GroupEnd);
}
}
ModulePath::Std { .. } => {
// We don't want to display stdlib in the Feature Tree.
}
}
self.add_import_module_ops(
exec_state,
program,
module_id,
&module_path,
source_range,
&universe_map,
);
let repr = repr.clone();
let exec_state = exec_state.clone();
@ -1009,6 +993,67 @@ impl ExecutorContext {
Ok((universe, root_imports))
}
#[cfg(feature = "artifact-graph")]
fn add_import_module_ops(
&self,
exec_state: &mut ExecState,
program: &crate::Program,
module_id: ModuleId,
module_path: &ModulePath,
source_range: SourceRange,
universe_map: &UniverseMap,
) {
match module_path {
ModulePath::Main => {
// This should never happen.
}
ModulePath::Local { value, .. } => {
// We only want to display the top-level module imports in
// the Feature Tree, not transitive imports.
if universe_map.contains_key(value) {
use crate::NodePath;
let node_path = if source_range.is_top_level_module() {
let cached_body_items = exec_state.global.artifacts.cached_body_items();
NodePath::from_range(&program.ast, cached_body_items, source_range).unwrap_or_default()
} else {
// The frontend doesn't care about paths in
// files other than the top-level module.
NodePath::placeholder()
};
exec_state.push_op(Operation::GroupBegin {
group: Group::ModuleInstance {
name: value.file_name().unwrap_or_default(),
module_id,
},
node_path,
source_range,
});
// Due to concurrent execution, we cannot easily
// group operations by module. So we leave the
// group empty and close it immediately.
exec_state.push_op(Operation::GroupEnd);
}
}
ModulePath::Std { .. } => {
// We don't want to display stdlib in the Feature Tree.
}
}
}
#[cfg(not(feature = "artifact-graph"))]
fn add_import_module_ops(
&self,
_exec_state: &mut ExecState,
_program: &crate::Program,
_module_id: ModuleId,
_module_path: &ModulePath,
_source_range: SourceRange,
_universe_map: &UniverseMap,
) {
}
/// Perform the execution of a program. Accept all possible parameters and
/// output everything.
async fn inner_run(
@ -1059,6 +1104,11 @@ impl ExecutorContext {
// Don't early return! We need to build other outputs regardless of
// whether execution failed.
// Because of execution caching, we may start with operations from a
// previous run.
#[cfg(feature = "artifact-graph")]
let start_op = exec_state.global.artifacts.operations.len();
self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
.await?;
@ -1072,6 +1122,29 @@ impl ExecutorContext {
)
.await;
#[cfg(feature = "artifact-graph")]
{
// Fill in NodePath for operations.
let cached_body_items = exec_state.global.artifacts.cached_body_items();
for op in exec_state.global.artifacts.operations.iter_mut().skip(start_op) {
match op {
Operation::StdLibCall {
node_path,
source_range,
..
}
| Operation::GroupBegin {
node_path,
source_range,
..
} => {
node_path.fill_placeholder(program, cached_body_items, *source_range);
}
Operation::GroupEnd => {}
}
}
}
// Ensure all the async commands completed.
self.engine.ensure_async_commands_completed().await?;

View File

@ -381,6 +381,13 @@ impl GlobalState {
}
}
#[cfg(feature = "artifact-graph")]
impl ArtifactState {
pub fn cached_body_items(&self) -> usize {
self.graph.item_count
}
}
impl ModuleState {
pub(super) fn new(path: ModulePath, memory: Arc<ProgramMemory>, module_id: Option<ModuleId>) -> Self {
ModuleState {

View File

@ -60,6 +60,20 @@ pub enum Step {
}
impl NodePath {
/// Placeholder for when the AST isn't available to create a real path. It
/// will be filled in later.
pub(crate) fn placeholder() -> Self {
Self::default()
}
#[cfg(feature = "artifact-graph")]
pub(crate) fn fill_placeholder(&mut self, program: &Node<Program>, cached_body_items: usize, range: SourceRange) {
if !self.is_empty() {
return;
}
*self = Self::from_range(program, cached_body_items, range).unwrap_or_default();
}
/// Given a program and a [`SourceRange`], return the path to the node that
/// contains the range.
pub(crate) fn from_range(program: &Node<Program>, cached_body_items: usize, range: SourceRange) -> Option<Self> {
@ -390,4 +404,19 @@ mod tests {
}
);
}
#[test]
fn test_node_path_from_range_import() {
let code = r#"import "cube.step" as cube
import "cylinder.kcl" as cylinder
"#;
let program = crate::Program::parse_no_errs(code).unwrap();
// The entire cylinder import statement.
assert_eq!(
NodePath::from_range(&program.ast, 0, range(27, 60)).unwrap(),
NodePath {
steps: vec![Step::ProgramBodyItem { index: 1 }],
}
);
}
}

View File

@ -94,6 +94,11 @@ impl SourceRange {
ModuleId::from_usize(self.0[2])
}
/// True if this source range is from the top-level module.
pub fn is_top_level_module(&self) -> bool {
self.module_id().is_top_level()
}
/// Check if the range contains a position.
pub fn contains(&self, pos: usize) -> bool {
pos >= self.start() && pos <= self.end()