Compare commits

...

3 Commits

Author SHA1 Message Date
58b9382990 Add angledLine/To/XY 2024-03-21 13:57:56 -04:00
0947d970a6 Implement the first angledLine stdlib fn in prelude.kcl 2024-03-21 10:55:14 -04:00
e8769eb543 Add ability to write prelude in KCL 2024-03-20 14:31:42 -04:00
12 changed files with 254 additions and 32 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -43,7 +43,12 @@ impl EpBinding {
/// Look up the given property of this binding. /// Look up the given property of this binding.
pub fn property_of(&self, property: LiteralIdentifier) -> Result<&Self, CompileError> { pub fn property_of(&self, property: LiteralIdentifier) -> Result<&Self, CompileError> {
match property { match property {
LiteralIdentifier::Identifier(_) => todo!("Support identifier properties"), LiteralIdentifier::Identifier(ident) => {
let EpBinding::Map { length_at: _, properties } = self else {
return Err(CompileError::CannotIndex)
};
properties.get(&ident.name).ok_or(CompileError::CannotIndex)
}
LiteralIdentifier::Literal(litval) => match litval.value { LiteralIdentifier::Literal(litval) => match litval.value {
// Arrays can be indexed by integers. // Arrays can be indexed by integers.
LiteralValue::IInteger(i) => match self { LiteralValue::IInteger(i) => match self {

View File

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

View File

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

View File

@ -1144,10 +1144,6 @@ async fn stdlib_cube_xline_yline() {
|> close(%) |> close(%)
|> extrude(100.0, %) |> extrude(100.0, %)
"#; "#;
kcvm_dbg(
program,
"/home/lee/Code/Zoo/modeling-api/execution-plan-debugger/cube_xyline.json",
);
let (_plan, _scope, _last_address) = must_plan(program); let (_plan, _scope, _last_address) = must_plan(program);
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program)) let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
@ -1442,3 +1438,154 @@ async fn cos_sin_pi() {
// Constants don't live in memory. // Constants don't live in memory.
assert_eq!(*z, constants::PI); 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));
}
#[tokio::test]
async fn angled_line() {
let program = r#"
let zigzag = startSketchAt([0.0, 0.0], "test")
|> angledLine({ angle: 45.0, length: 100 }, %, "")
|> angledLine({ angle: 45.0 + 90.0, length: 100 }, %, "")
|> angledLine({ angle: 45.0, length: 100 }, %, "")
|> angledLine({ angle: 45.0 + 90.0, length: 100 }, %, "")
|> close(%)
|> extrude(100.0, %)
"#;
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
.ast()
.unwrap();
let mut client = Some(test_client().await);
let mem = match crate::execute(ast, &mut client).await {
Ok(mem) => mem,
Err(e) => panic!("{e}"),
};
use kittycad_modeling_cmds::{each_cmd, ok_response::OkModelingCmdResponse, ImageFormat};
let out = client
.unwrap()
.run_command(
uuid::Uuid::new_v4().into(),
kittycad_modeling_cmds::ModelingCmd::from(each_cmd::TakeSnapshot {
format: ImageFormat::Png,
}),
)
.await
.unwrap();
let out = match out {
OkModelingCmdResponse::TakeSnapshot(kittycad_modeling_cmds::output::TakeSnapshot { contents: b }) => b,
other => panic!("wrong output: {other:?}"),
};
use image::io::Reader as ImageReader;
let img = ImageReader::new(std::io::Cursor::new(out))
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
twenty_twenty::assert_image("fixtures/zigzag_angledLine.png", &img, 0.9999);
}
#[tokio::test]
async fn angled_line_to_x() {
let program = r#"
let zigzag = startSketchAt([0.0, 0.0], "test")
|> angledLineToX({ angle: toRadians(85.0), to: 300.0 }, %, "")
|> angledLineToX({ angle: toRadians(35.0), to: 550.0 }, %, "")
|> close(%)
|> extrude(100.0, %)
"#;
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
.ast()
.unwrap();
let mut client = Some(test_client().await);
let mem = match crate::execute(ast, &mut client).await {
Ok(mem) => mem,
Err(e) => panic!("{e}"),
};
use kittycad_modeling_cmds::{each_cmd, ok_response::OkModelingCmdResponse, ImageFormat};
let out = client
.unwrap()
.run_command(
uuid::Uuid::new_v4().into(),
kittycad_modeling_cmds::ModelingCmd::from(each_cmd::TakeSnapshot {
format: ImageFormat::Png,
}),
)
.await
.unwrap();
let out = match out {
OkModelingCmdResponse::TakeSnapshot(kittycad_modeling_cmds::output::TakeSnapshot { contents: b }) => b,
other => panic!("wrong output: {other:?}"),
};
use image::io::Reader as ImageReader;
let img = ImageReader::new(std::io::Cursor::new(out))
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
twenty_twenty::assert_image("fixtures/tri_angledLineToX.png", &img, 0.9999);
}
#[tokio::test]
async fn angled_line_x() {
let program = r#"
let zigzag = startSketchAt([0.0, 0.0], "test")
|> angledLineX({ angle: toRadians(85.0), to: 300.0 }, %, "")
|> angledLineX({ angle: toRadians(35.0), to: 550.0 }, %, "")
|> close(%)
|> extrude(100.0, %)
"#;
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
.ast()
.unwrap();
let mut client = Some(test_client().await);
let mem = match crate::execute(ast, &mut client).await {
Ok(mem) => mem,
Err(e) => panic!("{e}"),
};
use kittycad_modeling_cmds::{each_cmd, ok_response::OkModelingCmdResponse, ImageFormat};
let out = client
.unwrap()
.run_command(
uuid::Uuid::new_v4().into(),
kittycad_modeling_cmds::ModelingCmd::from(each_cmd::TakeSnapshot {
format: ImageFormat::Png,
}),
)
.await
.unwrap();
let out = match out {
OkModelingCmdResponse::TakeSnapshot(kittycad_modeling_cmds::output::TakeSnapshot { contents: b }) => b,
other => panic!("wrong output: {other:?}"),
};
use image::io::Reader as ImageReader;
let img = ImageReader::new(std::io::Cursor::new(out))
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
twenty_twenty::assert_image("fixtures/tri_angledLineX.png", &img, 0.9999);
}

View File

@ -8,7 +8,6 @@ use syn::{parse_macro_input, LitStr};
/// This macro takes exactly one argument: A string literal containing KCL. /// This macro takes exactly one argument: A string literal containing KCL.
/// # Examples /// # Examples
/// ``` /// ```
/// extern crate alloc;
/// use kcl_compile_macro::parse_kcl; /// use kcl_compile_macro::parse_kcl;
/// let ast: kcl_lib::ast::types::Program = parse_kcl!("const y = 4"); /// let ast: kcl_lib::ast::types::Program = parse_kcl!("const y = 4");
/// ``` /// ```
@ -21,3 +20,14 @@ pub fn parse(input: TokenStream) -> TokenStream {
let ast_struct = ast.bake(&Default::default()); let ast_struct = ast.bake(&Default::default());
quote!(#ast_struct).into() 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,6 +351,31 @@ impl Program {
None 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 { pub trait ValueMeta {

View File

@ -958,7 +958,7 @@ impl TryFrom<Token> for Identifier {
Err(KclError::Syntax(KclErrorDetails { Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(), source_ranges: token.as_source_ranges(),
message: format!( message: format!(
"Cannot assign a variable to a reserved keyword: {}", "Word Cannot assign a variable to a reserved keyword: {}",
token.value.as_str() token.value.as_str()
), ),
})) }))
@ -1283,13 +1283,6 @@ fn optional_after_required(params: &[Parameter]) -> Result<(), KclError> {
impl Identifier { impl Identifier {
fn into_valid_binding_name(self) -> Result<Identifier, KclError> { fn into_valid_binding_name(self) -> Result<Identifier, KclError> {
// Make sure they are not assigning a variable to a stdlib function.
if crate::std::name_in_stdlib(&self.name) {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![SourceRange([self.start, self.end])],
message: format!("Cannot assign a variable to a reserved keyword: {}", self.name),
}));
}
Ok(self) Ok(self)
} }
} }

35
src/wasm-lib/prelude.kcl Normal file
View File

@ -0,0 +1,35 @@
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
}
let TAU = PI * 2.0
fn angledLine = (args, segment_group, tag?) => {
let x = cos(args.angle) * args.length
let y = sin(args.angle) * args.length
return line([x, y], segment_group, tag)
}
fn angledLineTo_ = (args, axis) => {
let fn = { X: [cos, sin], Y: [sin, cos] }
let a = args.to
let length = args.to / fn[axis][0](args.angle)
let b = fn[axis][1](args.angle) * length
return [x, y]
}
fn angledLineToX = (args, segment_group, tag?) => {
return lineTo(angledLineTo_(args, "X"), segment_group, tag)
}
fn angledLineX = (args, segment_group, tag?) => {
return line(angledLineTo_(args, "X"), segment_group, tag)
}
fn angledLineToY = (args, segment_group, tag?) => {
return lineTo(angledLineTo_(args, "Y"), segment_group, tag)
}
fn angledLineY = (args, segment_group, tag?) => {
return line(angledLineTo_(args, "Y"), segment_group, tag)
}