kittycad_execution_plan/
lib.rs

1//! A KittyCAD execution plan (KCEP) is a list of
2//! - KittyCAD API requests to make
3//! - Values to send in API requests
4//! - Values to assign from API responses
5//! - Computation to perform on values
6//! You can think of it as a domain-specific language for making KittyCAD API calls and using
7//! the results to make other API calls.
8
9use events::{Event, EventWriter};
10use kittycad_execution_plan_traits::events;
11use kittycad_execution_plan_traits::Address;
12use kittycad_execution_plan_traits::{MemoryError, Primitive, ReadMemory};
13use kittycad_modeling_cmds::websocket::ModelingBatch;
14use kittycad_modeling_session::{RunCommandError, Session as ModelingSession};
15pub use memory::{Memory, Stack, StaticMemoryInitializer};
16use serde::{Deserialize, Serialize};
17
18use self::api_request::ApiRequest;
19pub use self::arithmetic::{
20    operator::{BinaryOperation, Operation, UnaryOperation},
21    BinaryArithmetic, UnaryArithmetic,
22};
23use self::import_files::ImportFiles;
24pub use self::instruction::{Instruction, InstructionKind};
25
26pub mod api_request;
27mod arithmetic;
28/// Defined constants and ability to create more.
29pub mod constants;
30/// Expose feature to import external geometry files.
31pub mod import_files;
32/// KCVM aka KCEP instructions.
33pub mod instruction;
34mod memory;
35pub mod sketch_types;
36#[cfg(test)]
37mod tests;
38
39/// Somewhere values can be written to.
40#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
41pub enum Destination {
42    /// Write to main memory at the given address.
43    Address(Address),
44    /// Push onto the stack.
45    StackPush,
46    /// Extend what is already on the stack.
47    StackExtend,
48}
49
50impl std::fmt::Display for Destination {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match self {
53            Destination::Address(a) => a.fmt(f),
54            Destination::StackPush => "StackPush".fmt(f),
55            Destination::StackExtend => "StackExtend".fmt(f),
56        }
57    }
58}
59
60/// Argument to an operation.
61#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
62pub enum Operand {
63    /// A literal value.
64    Literal(Primitive),
65    /// An address which contains some literal value.
66    Reference(Address),
67    /// Pop the value from the top of the stack.
68    StackPop,
69}
70
71impl Operand {
72    /// Evaluate the operand, getting its value.
73    fn eval(&self, mem: &mut Memory) -> Result<Primitive> {
74        match self {
75            Operand::Literal(v) => Ok(v.to_owned()),
76            Operand::Reference(addr) => match mem.get(addr) {
77                None => Err(ExecutionError::MemoryEmpty { addr: *addr }),
78                Some(v) => Ok(v.to_owned()),
79            },
80            Operand::StackPop => mem.stack.pop_single(),
81        }
82    }
83}
84
85/// Executing the program failed.
86#[derive(Debug)]
87pub struct ExecutionFailed {
88    /// What error occurred.
89    pub error: ExecutionError,
90    /// Which instruction was being executed when the error occurred?
91    pub instruction: Option<Instruction>,
92    /// Which instruction number was being executed when the error occurred?
93    pub instruction_index: usize,
94}
95
96/// Execute the plan.
97pub async fn execute(
98    mem: &mut Memory,
99    plan: Vec<Instruction>,
100    session: &mut Option<ModelingSession>,
101) -> std::result::Result<(), ExecutionFailed> {
102    let mut events = EventWriter::default();
103    let mut batch_queue = ModelingBatch::default();
104    let n = plan.len();
105    for (i, instruction) in plan.into_iter().enumerate() {
106        if let Err(e) = instruction
107            .clone()
108            .execute(mem, session, &mut events, &mut batch_queue)
109            .await
110        {
111            return Err(ExecutionFailed {
112                error: e,
113                instruction: Some(instruction),
114                instruction_index: i,
115            });
116        }
117    }
118    cleanup(session, batch_queue, &mut events, n).await?;
119    Ok(())
120}
121
122async fn cleanup(
123    session: &mut Option<ModelingSession>,
124    batch_queue: ModelingBatch,
125    events: &mut EventWriter,
126    n: usize,
127) -> std::result::Result<(), ExecutionFailed> {
128    if batch_queue.is_empty() {
129        return Ok(());
130    }
131    let Some(session) = session else {
132        return Err(ExecutionFailed {
133            error: ExecutionError::NoApiClient,
134            instruction: None,
135            instruction_index: n,
136        });
137    };
138    crate::api_request::flush_batch_queue(session, batch_queue, events)
139        .await
140        .map_err(|e| ExecutionFailed {
141            error: e,
142            instruction: None,
143            instruction_index: n,
144        })?;
145    Ok(())
146}
147
148/// Current state of execution.
149pub struct ExecutionState {
150    /// State of memory after executing the instruction
151    pub mem: Memory,
152    /// Which instruction was executed? Index into the `Vec<Instruction>` for the plan.
153    pub active_instruction: usize,
154    /// Which events occurred during execution of this instruction?
155    pub events: Vec<Event>,
156}
157
158/// Execute the plan, returning the state at every moment of execution.
159/// Also return the index of the final instruction executed.
160/// This will be the last instruction if execution succeeded, but it might be earlier if
161/// execution had an error and quit.
162pub async fn execute_time_travel(
163    mem: &mut Memory,
164    plan: Vec<Instruction>,
165    session: &mut Option<ModelingSession>,
166) -> (Vec<ExecutionState>, usize) {
167    let mut out = Vec::new();
168    let mut events = EventWriter::default();
169    let mut batch_queue = Default::default();
170    let n = plan.len();
171    for (active_instruction, instruction) in plan.into_iter().enumerate() {
172        let res = instruction.execute(mem, session, &mut events, &mut batch_queue).await;
173
174        let mut crashed = false;
175        if let Err(e) = res {
176            events.push(Event {
177                text: e.to_string(),
178                severity: events::Severity::Error,
179                related_addresses: Vec::new(),
180            });
181            crashed = true;
182        }
183        let state = ExecutionState {
184            mem: mem.clone(),
185            active_instruction,
186            events: events.drain(),
187        };
188
189        out.push(state);
190        if crashed {
191            return (out, active_instruction);
192        }
193    }
194    if let Err(e) = cleanup(session, batch_queue, &mut events, n).await {
195        events.push(Event {
196            text: e.error.to_string(),
197            severity: events::Severity::Error,
198            related_addresses: Default::default(),
199        });
200        out.push(ExecutionState {
201            mem: mem.clone(),
202            active_instruction: n - 1,
203            events: events.drain(),
204        });
205    }
206    (out, n - 1)
207}
208
209type Result<T> = std::result::Result<T, ExecutionError>;
210
211/// Errors that could occur when executing a KittyCAD execution plan.
212#[derive(Debug, thiserror::Error)]
213pub enum ExecutionError {
214    /// Memory address was not set.
215    #[error("Memory address {addr} was not set")]
216    MemoryEmpty {
217        /// Which address was missing
218        addr: Address,
219    },
220    /// Type error, cannot apply the operation to the given operands.
221    #[error("Cannot apply operation {op} to operands {operands:?}")]
222    CannotApplyOperation {
223        /// Operation being attempted
224        op: Operation,
225        /// Operands being attempted
226        operands: Vec<Primitive>,
227    },
228    /// You tried to call a KittyCAD endpoint that doesn't exist or isn't implemented.
229    #[error("No endpoint {name} recognized")]
230    UnrecognizedEndpoint {
231        /// Endpoint name being attempted.
232        name: String,
233    },
234    /// Error running a modeling command.
235    #[error("Error sending command to API: {0}")]
236    ModelingApiError(#[from] RunCommandError),
237    /// Error reading value from memory.
238    #[error("{0}")]
239    MemoryError(#[from] MemoryError),
240    /// List index out of bounds.
241    #[error("you tried to access element {index} in a list of size {count}")]
242    ListIndexOutOfBounds {
243        /// Number of elements in the list.
244        count: usize,
245        /// Index which user attempted to access.
246        index: usize,
247    },
248    /// Could not make API call because no KittyCAD API client was provided
249    #[error("could not make API call because no KittyCAD API client was provided")]
250    NoApiClient,
251    /// Property not found in object.
252    #[error("No property '{property}' exists in the object starting at {address}")]
253    UndefinedProperty {
254        /// Which property the program was trying to access.
255        property: String,
256        /// Starting address of the object
257        address: Address,
258    },
259    /// No such SketchGroup exists.
260    #[error("No SketchGroup exists at index {index}")]
261    NoSketchGroup {
262        /// Index into the vector of SketchGroups.
263        index: usize,
264    },
265    /// SketchGroup storage cannot have gaps.
266    #[error(
267        "You tried to set a SketchGroup into destination {destination} but no such index exists. The last slot available is {len}."
268    )]
269    SketchGroupNoGaps {
270        /// Index user tried to write into.
271        destination: usize,
272        /// Current SketchGroup vec length.
273        len: usize,
274    },
275    /// Invalid argument type
276    #[error("An argument of the wrong type was used.")]
277    BadArg {
278        /// The reason why the argument is bad.
279        reason: String,
280    },
281    /// A general execution error.
282    #[error("A general execution error.")]
283    General {
284        /// The reason for the error.
285        reason: String,
286    },
287}