Compare commits

..

2 Commits

Author SHA1 Message Date
0bebd544a3 Clippy, shut up man. 2024-03-22 02:56:05 -04:00
64c9de09aa Add batch support to current KCL implementation 2024-03-22 02:45:08 -04:00
13 changed files with 155 additions and 94 deletions

View File

@ -1473,7 +1473,6 @@ version = "0.1.0"
dependencies = [
"image",
"kcl-lib",
"kcl-macros",
"kittycad",
"kittycad-execution-plan",
"kittycad-execution-plan-macros",

View File

@ -8,7 +8,6 @@ description = "A new executor for KCL which compiles to Execution Plans"
[dependencies]
image = { version = "0.24.7", default-features = false, features = ["png"] }
kcl-lib = { path = "../kcl" }
kcl-macros = { path = "../kcl-macros/" }
kittycad = { workspace = true }
kittycad-execution-plan = { workspace = true }
kittycad-execution-plan-traits = { workspace = true }

View File

@ -11,8 +11,6 @@ use kcl_lib::{
ast,
ast::types::{BodyItem, FunctionExpressionParts, KclNone, LiteralValue, Program},
};
extern crate alloc;
use kcl_macros::parse_file;
use kcl_value_group::into_single_value;
use kittycad_execution_plan::{self as ep, Destination, Instruction};
use kittycad_execution_plan_traits as ept;
@ -26,11 +24,9 @@ use self::{
};
/// Execute a KCL program by compiling into an execution plan, then running that.
/// Include a `prelude.kcl` inlined as a `Program` struct thanks to a proc_macro `parse!`.
/// This makes additional functions available to the user.
pub async fn execute(ast_user: Program, session: &mut Option<Session>) -> Result<ep::Memory, Error> {
pub async fn execute(ast: Program, session: &mut Option<Session>) -> Result<ep::Memory, Error> {
let mut planner = Planner::new();
let (plan, _retval) = planner.build_plan(ast_user)?;
let (plan, _retval) = planner.build_plan(ast)?;
let mut mem = ep::Memory::default();
ep::execute(&mut mem, plan, session).await?;
Ok(mem)
@ -58,9 +54,7 @@ impl Planner {
/// If successful, return the KCEP instructions for executing the given program.
/// If the program is a function with a return, then it also returns the KCL function's return value.
fn build_plan(&mut self, ast_user: Program) -> Result<(Vec<Instruction>, Option<EpBinding>), CompileError> {
let ast_prelude = parse_file!("./prelude.kcl");
let program = ast_prelude.merge(ast_user);
fn build_plan(&mut self, program: Program) -> Result<(Vec<Instruction>, Option<EpBinding>), CompileError> {
program
.body
.into_iter()

View File

@ -32,23 +32,23 @@ impl<'a> Context<'a> {
/// Unary operator macro to quickly create new bindings.
macro_rules! define_unary {
() => {};
($fn_name:ident$( $rest:ident)*) => {
($h:ident$( $r:ident)*) => {
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct $fn_name;
pub struct $h;
impl Callable for $fn_name {
impl Callable for $h {
fn call(&self, ctx: &mut Context<'_>, mut args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
if args.len() > 1 {
return Err(CompileError::TooManyArgs {
fn_name: "$fn_name".into(),
fn_name: "$h".into(),
maximum: 1,
actual: args.len(),
});
}
let not_enough_args = CompileError::NotEnoughArgs {
fn_name: "$fn_name".into(),
fn_name: "$h".into(),
required: 1,
actual: args.len(),
};
@ -61,7 +61,7 @@ macro_rules! define_unary {
let instructions = vec![
Instruction::UnaryArithmetic {
arithmetic: UnaryArithmetic {
operation: UnaryOperation::$fn_name,
operation: UnaryOperation::$h,
operand: Operand::Reference(arg0)
},
destination: Destination::Address(destination)
@ -75,7 +75,7 @@ macro_rules! define_unary {
}
}
define_unary!($($rest)*);
define_unary!($($r)*);
};
}
@ -115,27 +115,27 @@ impl Callable for Id {
/// Binary operator macro to quickly create new bindings.
macro_rules! define_binary {
() => {};
($fn_name:ident$( $rest:ident)*) => {
($h:ident$( $r:ident)*) => {
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct $fn_name;
pub struct $h;
impl Callable for $fn_name {
impl Callable for $h {
fn call(&self, ctx: &mut Context<'_>, mut args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
let len = args.len();
if len > 2 {
return Err(CompileError::TooManyArgs {
fn_name: "$fn_name".into(),
fn_name: "$h".into(),
maximum: 2,
actual: len,
});
}
let not_enough_args = CompileError::NotEnoughArgs {
fn_name: "$fn_name".into(),
fn_name: "$h".into(),
required: 2,
actual: len,
};
const ERR: &str = "cannot use composite values (e.g. array) as arguments to $fn_name";
const ERR: &str = "cannot use composite values (e.g. array) as arguments to $h";
let EpBinding::Single(arg1) = args.pop().ok_or(not_enough_args.clone())? else {
return Err(CompileError::InvalidOperand(ERR));
};
@ -146,7 +146,7 @@ macro_rules! define_binary {
Ok(EvalPlan {
instructions: vec![Instruction::BinaryArithmetic {
arithmetic: BinaryArithmetic {
operation: BinaryOperation::$fn_name,
operation: BinaryOperation::$h,
operand0: Operand::Reference(arg0),
operand1: Operand::Reference(arg1),
},
@ -157,7 +157,7 @@ macro_rules! define_binary {
}
}
define_binary!($($rest)*);
define_binary!($($r)*);
};
}

View File

@ -1442,23 +1442,3 @@ async fn cos_sin_pi() {
// Constants don't live in memory.
assert_eq!(*z, constants::PI);
}
#[tokio::test]
async fn kcl_prelude() {
let program = "
let the_answer_to_the_universe_is = hey_from_one_of_the_devs_of_the_past_i_wish_you_a_great_day_and_maybe_gave_you_a_little_smile_as_youve_found_this()
";
let (_plan, scope, _) = must_plan(program);
let Some(EpBinding::Single(the_answer_to_the_universe_is)) = scope.get("the_answer_to_the_universe_is") else {
panic!(
"Unexpected binding for variable 'the_answer_to_the_universe_is': {:?}",
scope.get("the_answer_to_the_universe_is")
);
};
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
.ast()
.unwrap();
let mem = crate::execute(ast, &mut None).await.unwrap();
use ept::ReadMemory;
assert_eq!(*mem.get(the_answer_to_the_universe_is).unwrap(), Primitive::from(42i64));
}

View File

@ -8,6 +8,7 @@ use syn::{parse_macro_input, LitStr};
/// This macro takes exactly one argument: A string literal containing KCL.
/// # Examples
/// ```
/// extern crate alloc;
/// use kcl_compile_macro::parse_kcl;
/// let ast: kcl_lib::ast::types::Program = parse_kcl!("const y = 4");
/// ```
@ -20,14 +21,3 @@ pub fn parse(input: TokenStream) -> TokenStream {
let ast_struct = ast.bake(&Default::default());
quote!(#ast_struct).into()
}
/// Same as `parse!` but read in a file.
#[proc_macro]
pub fn parse_file(input: TokenStream) -> TokenStream {
let file_name = parse_macro_input!(input as LitStr);
let kcl_src = std::fs::read_to_string(file_name.value()).unwrap();
let tokens = kcl_lib::token::lexer(&kcl_src);
let ast = kcl_lib::parser::Parser::new(tokens).ast().unwrap();
let ast_struct = ast.bake(&Default::default());
quote!(#ast_struct).into()
}

View File

@ -351,31 +351,6 @@ impl Program {
None
}
/// Merge two `Program`s together.
pub fn merge(self, ast_other: Program) -> Program {
let mut body_new: Vec<BodyItem> = vec![];
body_new.extend(self.body);
body_new.extend(ast_other.body);
let mut non_code_nodes_new: HashMap<usize, Vec<NonCodeNode>> = HashMap::new();
non_code_nodes_new.extend(self.non_code_meta.non_code_nodes);
non_code_nodes_new.extend(ast_other.non_code_meta.non_code_nodes);
let mut start_new: Vec<NonCodeNode> = vec![];
start_new.extend(self.non_code_meta.start);
start_new.extend(ast_other.non_code_meta.start);
Program {
start: self.start,
end: self.end + (ast_other.end - ast_other.start),
body: body_new,
non_code_meta: NonCodeMeta {
non_code_nodes: non_code_nodes_new,
start: start_new,
},
}
}
}
pub trait ValueMeta {

View File

@ -3,6 +3,7 @@
use std::sync::{Arc, Mutex};
use crate::executor::SourceRange;
use anyhow::{anyhow, Result};
use dashmap::DashMap;
use futures::{SinkExt, StreamExt};
@ -71,20 +72,113 @@ struct ToEngineReq {
request_sent: oneshot::Sender<Result<()>>,
}
fn is_cmd_with_return_values(cmd: &kittycad::types::ModelingCmd) -> bool {
let (kittycad::types::ModelingCmd::Export { .. }
| kittycad::types::ModelingCmd::Extrude { .. }
| kittycad::types::ModelingCmd::SketchModeDisable { .. }
| kittycad::types::ModelingCmd::ObjectBringToFront { .. }
| kittycad::types::ModelingCmd::SelectWithPoint { .. }
| kittycad::types::ModelingCmd::HighlightSetEntity { .. }
| kittycad::types::ModelingCmd::EntityGetChildUuid { .. }
| kittycad::types::ModelingCmd::EntityGetNumChildren { .. }
| kittycad::types::ModelingCmd::EntityGetParentId { .. }
| kittycad::types::ModelingCmd::EntityGetAllChildUuids { .. }
| kittycad::types::ModelingCmd::CameraDragMove { .. }
| kittycad::types::ModelingCmd::CameraDragEnd { .. }
| kittycad::types::ModelingCmd::DefaultCameraGetSettings { .. }
| kittycad::types::ModelingCmd::DefaultCameraZoom { .. }
| kittycad::types::ModelingCmd::SelectGet { .. }
| kittycad::types::ModelingCmd::Solid3DGetAllEdgeFaces { .. }
| kittycad::types::ModelingCmd::Solid3DGetAllOppositeEdges { .. }
| kittycad::types::ModelingCmd::Solid3DGetOppositeEdge { .. }
| kittycad::types::ModelingCmd::Solid3DGetNextAdjacentEdge { .. }
| kittycad::types::ModelingCmd::Solid3DGetPrevAdjacentEdge { .. }
| kittycad::types::ModelingCmd::GetEntityType { .. }
| kittycad::types::ModelingCmd::CurveGetControlPoints { .. }
| kittycad::types::ModelingCmd::CurveGetType { .. }
| kittycad::types::ModelingCmd::MouseClick { .. }
| kittycad::types::ModelingCmd::TakeSnapshot { .. }
| kittycad::types::ModelingCmd::PathGetInfo { .. }
| kittycad::types::ModelingCmd::PathGetCurveUuidsForVertices { .. }
| kittycad::types::ModelingCmd::PathGetVertexUuids { .. }
| kittycad::types::ModelingCmd::CurveGetEndPoints { .. }
| kittycad::types::ModelingCmd::FaceIsPlanar { .. }
| kittycad::types::ModelingCmd::FaceGetPosition { .. }
| kittycad::types::ModelingCmd::FaceGetGradient { .. }
| kittycad::types::ModelingCmd::PlaneIntersectAndProject { .. }
| kittycad::types::ModelingCmd::ImportFiles { .. }
| kittycad::types::ModelingCmd::Mass { .. }
| kittycad::types::ModelingCmd::Volume { .. }
| kittycad::types::ModelingCmd::Density { .. }
| kittycad::types::ModelingCmd::SurfaceArea { .. }
| kittycad::types::ModelingCmd::CenterOfMass { .. }
| kittycad::types::ModelingCmd::GetSketchModePlane { .. }
| kittycad::types::ModelingCmd::EntityGetDistance { .. }
| kittycad::types::ModelingCmd::EntityLinearPattern { .. }
| kittycad::types::ModelingCmd::EntityCircularPattern { .. }
| kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo { .. }) = cmd
else {
return false;
};
true
}
impl EngineConnection {
/// Start waiting for incoming engine requests, and send each one over the WebSocket to the engine.
async fn start_write_actor(mut tcp_write: WebSocketTcpWrite, mut engine_req_rx: mpsc::Receiver<ToEngineReq>) {
let mut batch: Vec<kittycad::types::ModelingCmdReq> = vec![];
while let Some(req) = engine_req_rx.recv().await {
let ToEngineReq { req, request_sent } = req;
let res = if let kittycad::types::WebSocketRequest::ModelingCmdReq {
cmd: kittycad::types::ModelingCmd::ImportFiles { .. },
cmd_id: _,
} = &req
{
let kittycad::types::WebSocketRequest::ModelingCmdReq { cmd, cmd_id } = &req else {
return;
};
let res = if let kittycad::types::ModelingCmd::ImportFiles { .. } = cmd {
// Send it as binary.
Self::inner_send_to_engine_binary(req, &mut tcp_write).await
} else {
Self::inner_send_to_engine(req, &mut tcp_write).await
// Backported from the new Grackle-core KCL.
// We will batch all commands until we hit one which has
// return values we want to wait for. Currently, that means
// waiting for API requests with return values that are not just
// request confirmations (ex. face or edge data).
batch.push(kittycad::types::ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: *cmd_id,
});
if is_cmd_with_return_values(cmd) || *cmd_id == uuid::Uuid::nil() {
// If the batch has zero commands, and we're about to load
// a command that has return values, don't wrap it in a
// ModelingCmdBatchReq.
let future = if batch.len() == 1 {
Self::inner_send_to_engine(
kittycad::types::WebSocketRequest::ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: *cmd_id,
},
&mut tcp_write,
)
} else {
// Serde will properly serialize these.
Self::inner_send_to_engine(
kittycad::types::WebSocketRequest::ModelingCmdBatchReq {
requests: batch.clone(),
},
&mut tcp_write,
)
};
// Prepare for a new batch of instructions.
batch.clear();
future.await
} else {
Ok(())
}
};
let _ = request_sent.send(res);
}
@ -163,7 +257,7 @@ impl EngineManager for EngineConnection {
async fn send_modeling_cmd(
&self,
id: uuid::Uuid,
source_range: crate::executor::SourceRange,
source_range: SourceRange,
cmd: kittycad::types::ModelingCmd,
) -> Result<OkWebSocketResponseData, KclError> {
let (tx, rx) = oneshot::channel();
@ -200,7 +294,18 @@ impl EngineManager for EngineConnection {
})
})?;
// Wait for the response.
// Only wait for a response if it's a command *with* return values
// So most of the time, this condition will be true.
if !is_cmd_with_return_values(&cmd) {
// Simulate an empty response type
return Ok(OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Empty {},
});
}
// If it's a submitted batch, we want to check the batch return value
// in case of any errors.
let current_time = std::time::Instant::now();
while current_time.elapsed().as_secs() < 60 {
if let Ok(guard) = self.socket_health.lock() {

View File

@ -10,6 +10,24 @@ pub mod conn_wasm;
#[async_trait::async_trait]
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Tell the EngineManager there will be no more commands.
/// We send a "dummy command" to signal EngineConnection that we're
/// at the end of the program and there'll be no more requests.
/// This means in tests, where it's impossible to look ahead, we'll need to
/// add this to mark the end of commands.
/// In compiled KCL tests, it will be auto-inserted.
async fn signal_end(&self) -> Result<kittycad::types::OkWebSocketResponseData, crate::errors::KclError> {
self.send_modeling_cmd(
// THE NIL UUID IS THE SIGNAL OF THE END OF TIMES FOR THIS POOR PROGRAM.
uuid::Uuid::nil(),
// This will be ignored.
crate::executor::SourceRange([0, 0]),
// This will be ignored. It was one I found with no fields.
kittycad::types::ModelingCmd::EditModeExit {},
)
.await
}
/// Send a modeling command and wait for the response message.
async fn send_modeling_cmd(
&self,

View File

@ -1219,6 +1219,9 @@ pub async fn execute(
}
}
// Signal to engine we're done. Flush the batch.
ctx.engine.signal_end().await?;
Ok(memory.clone())
}

View File

@ -165,6 +165,7 @@ async fn inner_get_opposite_edge(tag: String, extrude_group: Box<ExtrudeGroup>,
},
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::Solid3DGetOppositeEdge { data: opposite_edge },
} = &resp

View File

@ -915,7 +915,7 @@ async fn start_sketch_on_face(
})
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a face with the tag `{}`", tag),
message: format!("Expected a face with the tag `{}` for sketch", tag),
source_ranges: vec![args.source_range],
})
})??,

View File

@ -1,3 +0,0 @@
fn hey_from_one_of_the_devs_of_the_past_i_wish_you_a_great_day_and_maybe_gave_you_a_little_smile_as_youve_found_this = () => {
return 42
}