Move sketch functions to KCL; remove Rust decl dead code (#7335)

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2025-06-05 07:41:01 +12:00
committed by GitHub
parent 5ceb92d117
commit 5235a731ba
104 changed files with 1647 additions and 143019 deletions

View File

@ -12,21 +12,9 @@ proc-macro = true
bench = false
[dependencies]
Inflector = "0.11.4"
convert_case = "0.8.0"
once_cell = "1.21.3"
proc-macro2 = "1"
quote = "1"
regex = "1.11"
serde = { workspace = true }
serde_tokenstream = "0.2"
syn = { version = "2.0.96", features = ["full"] }
[dev-dependencies]
anyhow = { workspace = true }
expectorate = "1.1.0"
pretty_assertions = "1.4.1"
rustfmt-wrapper = "0.2.1"
[lints]
workspace = true

View File

@ -149,6 +149,35 @@ pub const TEST_NAMES: &[&str] = &[
"std-sketch-tangentToEnd-2",
"std-sketch-tangentToEnd-3",
"std-sketch-tangentToEnd-4",
"std-sketch-profileStart-0",
"std-sketch-profileStartX-0",
"std-sketch-profileStartY-0",
"std-sketch-startProfile-0",
"std-sketch-startProfile-1",
"std-sketch-startProfile-2",
"std-sketch-startSketchOn-0",
"std-sketch-startSketchOn-1",
"std-sketch-startSketchOn-2",
"std-sketch-startSketchOn-3",
"std-sketch-startSketchOn-4",
"std-sketch-startSketchOn-5",
"std-sketch-angledLineThatIntersects-0",
"std-sketch-angledLine-0",
"std-sketch-arc-0",
"std-sketch-arc-1",
"std-sketch-bezierCurve-0",
"std-sketch-bezierCurve-1",
"std-sketch-close-0",
"std-sketch-close-1",
"std-sketch-involuteCircular-0",
"std-sketch-line-0",
"std-sketch-subtract2d-0",
"std-sketch-subtract2d-1",
"std-sketch-tangentialArc-0",
"std-sketch-tangentialArc-1",
"std-sketch-tangentialArc-2",
"std-sketch-xLine-0",
"std-sketch-yLine-0",
"std-solid-appearance-0",
"std-solid-appearance-1",
"std-solid-appearance-2",

View File

@ -3,29 +3,6 @@
#![allow(clippy::style)]
mod example_tests;
#[cfg(test)]
mod tests;
mod unbox;
use std::collections::HashMap;
use convert_case::Casing;
use inflector::{cases::camelcase::to_camel_case, Inflector};
use once_cell::sync::Lazy;
use quote::{format_ident, quote, quote_spanned, ToTokens};
use regex::Regex;
use serde::Deserialize;
use serde_tokenstream::{from_tokenstream, Error};
use syn::{
parse::{Parse, ParseStream},
Attribute, Signature, Visibility,
};
use unbox::unbox;
#[proc_macro_attribute]
pub fn stdlib(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
do_output(do_stdlib(attr.into(), item.into()))
}
#[proc_macro_attribute]
pub fn for_each_example_test(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
@ -36,845 +13,3 @@ pub fn for_each_example_test(_attr: proc_macro::TokenStream, item: proc_macro::T
pub fn for_all_example_test(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
example_tests::do_for_all_example_test(item.into()).into()
}
/// Describes an argument of a stdlib function.
#[derive(Deserialize, Debug)]
struct ArgMetadata {
/// Docs for the argument.
docs: String,
/// If this argument is optional, it should still be included in completion snippets.
/// Does not do anything if the argument is already required.
#[serde(default)]
include_in_snippet: bool,
/// The snippet should suggest this value for the arg.
#[serde(default)]
snippet_value: Option<String>,
/// The snippet should suggest this value for the arg.
#[serde(default)]
snippet_value_array: Option<Vec<String>>,
}
#[derive(Deserialize, Debug)]
struct StdlibMetadata {
/// The name of the function in the API.
name: String,
/// Tags for the function.
#[serde(default)]
tags: Vec<String>,
/// Whether the function is unpublished.
/// Then docs will not be generated.
#[serde(default)]
unpublished: bool,
/// Whether the function is deprecated.
/// Then specific docs detailing that this is deprecated will be generated.
#[serde(default)]
deprecated: bool,
/// Whether the function is displayed in the feature tree.
/// If true, calls to the function will be available for display.
/// If false, calls to the function will never be displayed.
#[serde(default)]
feature_tree_operation: bool,
/// If true, the first argument is unlabeled.
/// If false, all arguments require labels.
#[serde(default)]
unlabeled_first: bool,
/// Key = argument name, value = argument doc.
#[serde(default)]
args: HashMap<String, ArgMetadata>,
}
fn do_stdlib(
attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
let metadata = from_tokenstream(&attr)?;
do_stdlib_inner(metadata, attr, item)
}
fn do_output(res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>) -> proc_macro::TokenStream {
match res {
Err(err) => err.to_compile_error().into(),
Ok((stdlib_docs, errors)) => {
let compiler_errors = errors.iter().map(|err| err.to_compile_error());
let output = quote! {
#stdlib_docs
#( #compiler_errors )*
};
output.into()
}
}
}
fn do_stdlib_inner(
metadata: StdlibMetadata,
_attr: proc_macro2::TokenStream,
item: proc_macro2::TokenStream,
) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
let ast: ItemFnForSignature = syn::parse2(item.clone())?;
let mut errors = Vec::new();
if ast.sig.constness.is_some() {
errors.push(Error::new_spanned(
&ast.sig.constness,
"stdlib functions may not be const functions",
));
}
if ast.sig.unsafety.is_some() {
errors.push(Error::new_spanned(
&ast.sig.unsafety,
"stdlib functions may not be unsafe",
));
}
if ast.sig.abi.is_some() {
errors.push(Error::new_spanned(
&ast.sig.abi,
"stdlib functions may not use an alternate ABI",
));
}
if !ast.sig.generics.params.is_empty() {
if ast.sig.generics.params.iter().any(|generic_type| match generic_type {
syn::GenericParam::Lifetime(_) => false,
syn::GenericParam::Type(_) => true,
syn::GenericParam::Const(_) => true,
}) {
errors.push(Error::new_spanned(
&ast.sig.generics,
"Stdlib functions may not be generic over types or constants, only lifetimes.",
));
}
}
if ast.sig.variadic.is_some() {
errors.push(Error::new_spanned(&ast.sig.variadic, "no language C here"));
}
let name = metadata.name;
// Fail if the name is not camel case.
// Remove some known suffix exceptions first.
let name_cleaned = name.strip_suffix("2d").unwrap_or(name.as_str());
let name_cleaned = name.strip_suffix("3d").unwrap_or(name_cleaned);
if !name_cleaned.is_camel_case() {
errors.push(Error::new_spanned(
&ast.sig.ident,
format!("stdlib function names must be in camel case: `{}`", name),
));
}
let name_ident = format_ident!("{}", name.to_case(convert_case::Case::UpperCamel));
let name_str = name.to_string();
let fn_name = &ast.sig.ident;
let fn_name_str = fn_name.to_string().replace("inner_", "");
let fn_name_ident = format_ident!("{}", fn_name_str);
let boxed_fn_name_ident = format_ident!("boxed_{}", fn_name_str);
let _visibility = &ast.vis;
let doc_info = extract_doc_from_attrs(&ast.attrs);
let comment_text = {
let mut buf = String::new();
buf.push_str("Std lib function: ");
buf.push_str(&name_str);
if let Some(s) = &doc_info.summary {
buf.push_str("\n");
buf.push_str(&s);
}
if let Some(s) = &doc_info.description {
buf.push_str("\n");
buf.push_str(&s);
}
buf
};
let description_doc_comment = quote! {
#[doc = #comment_text]
};
let summary = if let Some(summary) = doc_info.summary {
quote! { #summary }
} else {
quote! { "" }
};
let description = if let Some(description) = doc_info.description {
quote! { #description }
} else {
quote! { "" }
};
if doc_info.code_blocks.is_empty() {
errors.push(Error::new_spanned(
&ast.sig,
"stdlib functions must have at least one code block",
));
}
// Make sure the function name is in all the code blocks.
for code_block in doc_info.code_blocks.iter() {
if !code_block.0.contains(&name) {
errors.push(Error::new_spanned(
&ast.sig,
format!(
"stdlib functions must have the function name `{}` in the code block",
name
),
));
}
}
let test_code_blocks = doc_info
.code_blocks
.iter()
.enumerate()
.map(|(index, (code_block, norun))| {
if !norun {
generate_code_block_test(&fn_name_str, code_block, index)
} else {
quote! {}
}
})
.collect::<Vec<_>>();
let (cb, norun): (Vec<_>, Vec<_>) = doc_info.code_blocks.into_iter().unzip();
let code_blocks = quote! {
let code_blocks = vec![#(#cb),*];
let norun = vec![#(#norun),*];
code_blocks.iter().zip(norun).map(|(cb, norun)| {
let program = crate::Program::parse_no_errs(cb).unwrap();
let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
options.insert_final_newline = false;
(program.ast.recast(&options, 0), norun)
}).collect::<Vec<(String, bool)>>()
};
let tags = metadata
.tags
.iter()
.map(|tag| {
quote! { #tag.to_string() }
})
.collect::<Vec<_>>();
let deprecated = if metadata.deprecated {
quote! { true }
} else {
quote! { false }
};
let unpublished = if metadata.unpublished {
quote! { true }
} else {
quote! { false }
};
let feature_tree_operation = if metadata.feature_tree_operation {
quote! { true }
} else {
quote! { false }
};
let docs_crate = get_crate(None);
// When the user attaches this proc macro to a function with the wrong type
// signature, the resulting errors can be deeply inscrutable. To attempt to
// make failures easier to understand, we inject code that asserts the types
// of the various parameters. We do this by calling dummy functions that
// require a type that satisfies SharedExtractor or ExclusiveExtractor.
let mut arg_types = Vec::new();
for (i, arg) in ast.sig.inputs.iter().enumerate() {
// Get the name of the argument.
let arg_name = match arg {
syn::FnArg::Receiver(pat) => {
let span = pat.self_token.span.unwrap();
span.source_text().unwrap().to_string()
}
syn::FnArg::Typed(pat) => match &*pat.pat {
syn::Pat::Ident(ident) => ident.ident.to_string(),
_ => {
errors.push(Error::new_spanned(
&pat.pat,
"stdlib functions may not use destructuring patterns",
));
continue;
}
},
}
.trim_start_matches('_')
.to_string();
// These aren't really KCL args, they're just state that each stdlib function's impl needs.
if arg_name == "exec_state" || arg_name == "args" {
continue;
}
let ty = match arg {
syn::FnArg::Receiver(pat) => pat.ty.as_ref().into_token_stream(),
syn::FnArg::Typed(pat) => pat.ty.as_ref().into_token_stream(),
};
let (ty_string, ty_ident) = clean_ty_string(ty.to_string().as_str());
let ty_string = rust_type_to_openapi_type(&ty_string);
let required = !ty_ident.to_string().starts_with("Option <");
let Some(arg_meta) = metadata.args.get(&arg_name) else {
errors.push(Error::new_spanned(arg, format!("arg {arg_name} not found")));
continue;
};
let description = arg_meta.docs.clone();
let include_in_snippet = required || arg_meta.include_in_snippet;
let snippet_value = arg_meta.snippet_value.clone();
let snippet_value_array = arg_meta.snippet_value_array.clone();
if snippet_value.is_some() && snippet_value_array.is_some() {
errors.push(Error::new_spanned(arg, format!("arg {arg_name} has set both snippet_value and snippet_value array, but at most one of these may be set. Please delete one of them.")));
}
let label_required = !(i == 0 && metadata.unlabeled_first);
let camel_case_arg_name = to_camel_case(&arg_name);
if ty_string != "ExecState" && ty_string != "Args" {
let schema = quote! {
generator.root_schema_for::<#ty_ident>()
};
let q0 = quote! {
name: #camel_case_arg_name.to_string(),
type_: #ty_string.to_string(),
schema: #schema,
required: #required,
label_required: #label_required,
description: #description.to_string(),
include_in_snippet: #include_in_snippet,
};
let q1 = if let Some(snippet_value) = snippet_value {
quote! {
snippet_value: Some(#snippet_value.to_owned()),
}
} else {
quote! {
snippet_value: None,
}
};
let q2 = if let Some(snippet_value_array) = snippet_value_array {
quote! {
snippet_value_array: Some(vec![
#(#snippet_value_array.to_owned()),*
]),
}
} else {
quote! {
snippet_value_array: None,
}
};
arg_types.push(quote! {
#docs_crate::StdLibFnArg {
#q0
#q1
#q2
}
});
}
}
let return_type_inner = match &ast.sig.output {
syn::ReturnType::Default => quote! { () },
syn::ReturnType::Type(_, ty) => {
// Get the inside of the result.
match &**ty {
syn::Type::Path(syn::TypePath { path, .. }) => {
let path = &path.segments;
if path.len() == 1 {
let seg = &path[0];
if seg.ident == "Result" {
if let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
args,
..
}) = &seg.arguments
{
if args.len() == 2 || args.len() == 1 {
let mut args = args.iter();
let ok = args.next().unwrap();
if let syn::GenericArgument::Type(ty) = ok {
let ty = unbox(ty.clone());
quote! { #ty }
} else {
quote! { () }
}
} else {
quote! { () }
}
} else {
quote! { () }
}
} else {
let ty = unbox(*ty.clone());
quote! { #ty }
}
} else {
quote! { () }
}
}
_ => {
quote! { () }
}
}
}
};
let ret_ty_string = return_type_inner.to_string().replace(' ', "");
let return_type = if !ret_ty_string.is_empty() || ret_ty_string != "()" {
let ret_ty_string = rust_type_to_openapi_type(&ret_ty_string);
quote! {
let schema = generator.root_schema_for::<#return_type_inner>();
Some(#docs_crate::StdLibFnArg {
name: "".to_string(),
type_: #ret_ty_string.to_string(),
schema,
required: true,
label_required: true,
description: String::new(),
include_in_snippet: true,
snippet_value: None,
snippet_value_array: None,
})
}
} else {
quote! {
None
}
};
// For reasons that are not well understood unused constants that use the
// (default) call_site() Span do not trigger the dead_code lint. Because
// defining but not using an endpoint is likely a programming error, we
// want to be sure to have the compiler flag this. We force this by using
// the span from the name of the function to which this macro was applied.
let span = ast.sig.ident.span();
let const_struct = quote_spanned! {span=>
pub(crate) const #name_ident: #name_ident = #name_ident {};
};
let test_mod_name = format_ident!("test_examples_{}", fn_name_str);
// The final TokenStream returned will have a few components that reference
// `#name_ident`, the name of the function to which this macro was applied...
let stream = quote! {
#[cfg(test)]
mod #test_mod_name {
#(#test_code_blocks)*
}
// ... a struct type called `#name_ident` that has no members
#[allow(non_camel_case_types, missing_docs)]
#description_doc_comment
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars::JsonSchema, ts_rs::TS)]
#[ts(export)]
pub(crate) struct #name_ident {}
// ... a constant of type `#name` whose identifier is also #name_ident
#[allow(non_upper_case_globals, missing_docs)]
#description_doc_comment
#const_struct
fn #boxed_fn_name_ident(
exec_state: &mut crate::execution::ExecState,
args: crate::std::Args,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>> + Send + '_>,
> {
Box::pin(#fn_name_ident(exec_state, args))
}
impl #docs_crate::StdLibFn for #name_ident
{
fn name(&self) -> String {
#name_str.to_string()
}
fn summary(&self) -> String {
#summary.to_string()
}
fn description(&self) -> String {
#description.to_string()
}
fn tags(&self) -> Vec<String> {
vec![#(#tags),*]
}
fn args(&self, inline_subschemas: bool) -> Vec<#docs_crate::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
// We set this to false so we can recurse them later.
settings.inline_subschemas = inline_subschemas;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
vec![#(#arg_types),*]
}
fn return_value(&self, inline_subschemas: bool) -> Option<#docs_crate::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
// We set this to false so we can recurse them later.
settings.inline_subschemas = inline_subschemas;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
#return_type
}
fn unpublished(&self) -> bool {
#unpublished
}
fn deprecated(&self) -> bool {
#deprecated
}
fn feature_tree_operation(&self) -> bool {
#feature_tree_operation
}
fn examples(&self) -> Vec<(String, bool)> {
#code_blocks
}
fn std_lib_fn(&self) -> crate::std::StdFn {
#boxed_fn_name_ident
}
fn clone_box(&self) -> Box<dyn #docs_crate::StdLibFn> {
Box::new(self.clone())
}
}
#item
};
// Prepend the usage message if any errors were detected.
if !errors.is_empty() {
errors.insert(0, Error::new_spanned(&ast.sig, ""));
}
Ok((stream, errors))
}
#[allow(dead_code)]
fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
let compile_errors = errors.iter().map(syn::Error::to_compile_error);
quote!(#(#compile_errors)*)
}
fn get_crate(var: Option<String>) -> proc_macro2::TokenStream {
if let Some(s) = var {
if let Ok(ts) = syn::parse_str(s.as_str()) {
return ts;
}
}
quote!(crate::docs)
}
#[derive(Debug)]
struct DocInfo {
pub summary: Option<String>,
pub description: Option<String>,
pub code_blocks: Vec<(String, bool)>,
}
fn extract_doc_from_attrs(attrs: &[syn::Attribute]) -> DocInfo {
let doc = syn::Ident::new("doc", proc_macro2::Span::call_site());
let raw_lines = attrs.iter().flat_map(|attr| {
if let syn::Meta::NameValue(nv) = &attr.meta {
if nv.path.is_ident(&doc) {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s), ..
}) = &nv.value
{
return normalize_comment_string(s.value());
}
}
}
Vec::new()
});
// Parse any code blocks from the doc string.
let mut code_blocks: Vec<(String, bool)> = Vec::new();
let mut code_block: Option<(String, bool)> = None;
let mut parsed_lines = Vec::new();
for line in raw_lines {
if line.starts_with("```") {
if let Some((inner_code_block, norun)) = code_block {
code_blocks.push((inner_code_block.trim().to_owned(), norun));
code_block = None;
} else {
let norun = line.contains("kcl,norun") || line.contains("kcl,no_run");
code_block = Some((String::new(), norun));
}
continue;
}
if let Some((code_block, _)) = &mut code_block {
code_block.push_str(&line);
code_block.push('\n');
} else {
parsed_lines.push(line);
}
}
if let Some((code_block, norun)) = code_block {
code_blocks.push((code_block.trim().to_string(), norun));
}
let mut summary = None;
let mut description: Option<String> = None;
for line in parsed_lines {
if line.is_empty() {
if let Some(desc) = &mut description {
// Handle fully blank comments as newlines we keep.
if !desc.is_empty() && !desc.ends_with('\n') {
if desc.ends_with(' ') {
desc.pop().unwrap();
}
desc.push_str("\n\n");
}
} else if summary.is_some() {
description = Some(String::new());
}
continue;
}
if let Some(desc) = &mut description {
desc.push_str(&line);
// Default to space-separating comment fragments.
desc.push(' ');
continue;
}
if summary.is_none() {
summary = Some(String::new());
}
match &mut summary {
Some(summary) => {
summary.push_str(&line);
// Default to space-separating comment fragments.
summary.push(' ');
}
None => unreachable!(),
}
}
// Trim the summary and description.
if let Some(s) = &mut summary {
while s.ends_with(' ') || s.ends_with('\n') {
s.pop().unwrap();
}
if s.is_empty() {
summary = None;
}
}
if let Some(d) = &mut description {
while d.ends_with(' ') || d.ends_with('\n') {
d.pop().unwrap();
}
if d.is_empty() {
description = None;
}
}
DocInfo {
summary,
description,
code_blocks,
}
}
fn normalize_comment_string(s: String) -> Vec<String> {
s.split('\n')
.map(|s| {
// Rust-style comments are intrinsically single-line.
// We only want to trim a single space character from the start of
// a line, and only if it's the first character.
s.strip_prefix(' ').unwrap_or(s).trim_end().to_owned()
})
.collect()
}
/// Represent an item without concern for its body which may (or may not)
/// contain syntax errors.
#[derive(Clone)]
struct ItemFnForSignature {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub sig: Signature,
pub _block: proc_macro2::TokenStream,
}
impl Parse for ItemFnForSignature {
fn parse(input: ParseStream) -> syn::parse::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let vis: Visibility = input.parse()?;
let sig: Signature = input.parse()?;
let block = input.parse()?;
Ok(ItemFnForSignature {
attrs,
vis,
sig,
_block: block,
})
}
}
fn clean_ty_string(t: &str) -> (String, proc_macro2::TokenStream) {
let mut ty_string = t
.replace("& 'a", "")
.replace('&', "")
.replace("mut", "")
.replace("< 'a >", "")
.replace(' ', "");
if ty_string.starts_with("ExecState") {
ty_string = "ExecState".to_string();
}
if ty_string.starts_with("Args") {
ty_string = "Args".to_string();
}
let ty_string = ty_string.trim().to_string();
let ty_ident = if ty_string.starts_with("Vec<") {
let ty_string = ty_string.trim_start_matches("Vec<").trim_end_matches('>');
let (_, ty_ident) = clean_ty_string(&ty_string);
quote! {
Vec<#ty_ident>
}
} else if ty_string.starts_with("kittycad::types::") {
let ty_string = ty_string.trim_start_matches("kittycad::types::").trim_end_matches('>');
let ty_ident = format_ident!("{}", ty_string);
quote! {
kittycad::types::#ty_ident
}
} else if ty_string.starts_with("Option<") {
let ty_string = ty_string.trim_start_matches("Option<").trim_end_matches('>');
let (_, ty_ident) = clean_ty_string(&ty_string);
quote! {
Option<#ty_ident>
}
} else if let Some((inner_array_type, num)) = parse_array_type(&ty_string) {
let ty_string = inner_array_type.to_owned();
let (_, ty_ident) = clean_ty_string(&ty_string);
quote! {
[#ty_ident; #num]
}
} else if ty_string.starts_with("Box<") {
let ty_string = ty_string.trim_start_matches("Box<").trim_end_matches('>');
let (_, ty_ident) = clean_ty_string(&ty_string);
quote! {
#ty_ident
}
} else {
let ty_ident = format_ident!("{}", ty_string);
quote! {
#ty_ident
}
};
(ty_string, ty_ident)
}
fn rust_type_to_openapi_type(t: &str) -> String {
let mut t = t.to_string();
// Turn vecs into arrays.
// TODO: handle nested types
if t.starts_with("Vec<") {
t = t.replace("Vec<", "[").replace('>', "]");
}
if t.starts_with("Box<") {
t = t.replace("Box<", "").replace('>', "");
}
if t.starts_with("Option<") {
t = t.replace("Option<", "").replace('>', "");
}
if t == "[TyF64;2]" {
return "Point2d".to_owned();
}
if t == "[TyF64;3]" {
return "Point3d".to_owned();
}
if let Some((inner_type, _length)) = parse_array_type(&t) {
t = format!("[{inner_type}]")
}
if t == "f64" || t == "TyF64" || t == "u32" || t == "NonZeroU32" {
return "number".to_string();
} else if t == "str" || t == "String" {
return "string".to_string();
} else {
return t.replace("f64", "number").replace("TyF64", "number").to_string();
}
}
fn parse_array_type(type_name: &str) -> Option<(&str, usize)> {
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[([a-zA-Z0-9<>]+); ?(\d+)\]").unwrap());
let cap = RE.captures(type_name)?;
let inner_type = cap.get(1)?;
let length = cap.get(2)?.as_str().parse().ok()?;
Some((inner_type.as_str(), length))
}
// For each kcl code block, we want to generate a test that checks that the
// code block is valid kcl code and compiles and executes.
fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> proc_macro2::TokenStream {
let test_name = format_ident!("kcl_test_example_{}{}", fn_name, index);
let test_name_mock = format_ident!("test_mock_example_{}{}", fn_name, index);
let output_test_name_str = format!("serial_test_example_{}{}", fn_name, index);
quote! {
#[tokio::test(flavor = "multi_thread")]
async fn #test_name_mock() -> miette::Result<()> {
let program = crate::Program::parse_no_errs(#code_block).unwrap();
let ctx = crate::ExecutorContext {
engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())),
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
context_type: crate::execution::ContextType::Mock,
};
if let Err(e) = ctx.run(&program, &mut crate::execution::ExecState::new(&ctx)).await {
return Err(miette::Report::new(crate::errors::Report {
error: e.error,
filename: format!("{}{}", #fn_name, #index),
kcl_source: #code_block.to_string(),
}));
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
async fn #test_name() -> miette::Result<()> {
let code = #code_block;
// Note, `crate` must be kcl_lib
let result = match crate::test_server::execute_and_snapshot(code, None).await {
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e.error,
filename: format!("{}{}", #fn_name, #index),
kcl_source: #code_block.to_string(),
}));
}
Err(other_err)=> panic!("{}", other_err),
Ok(img) => img,
};
twenty_twenty::assert_image(&format!("tests/outputs/{}.png", #output_test_name_str), &result, 0.99);
Ok(())
}
}
}

View File

@ -1,508 +0,0 @@
use anyhow::Result;
use quote::quote;
use crate::{do_stdlib, parse_array_type};
fn clean_text(s: &str) -> String {
// Add newlines after end-braces at <= two levels of indentation.
if cfg!(not(windows)) {
let regex = regex::Regex::new(r"(})(\n\s{0,8}[^} ])").unwrap();
regex.replace_all(s, "$1\n$2").to_string()
} else {
let regex = regex::Regex::new(r"(})(\r\n\s{0,8}[^} ])").unwrap();
regex.replace_all(s, "$1\r\n$2").to_string()
}
}
/// Format a TokenStream as a string and run `rustfmt` on the result.
fn get_text_fmt(output: &proc_macro2::TokenStream) -> Result<String> {
// Format the file with rustfmt.
let content = rustfmt_wrapper::rustfmt(output).unwrap();
Ok(clean_text(&content))
}
#[test]
fn test_get_inner_array_type() {
for (expected, input) in [
(Some(("f64", 2)), "[f64;2]"),
(Some(("String", 2)), "[String; 2]"),
(Some(("Option<String>", 12)), "[Option<String>;12]"),
(Some(("Option<String>", 12)), "[Option<String>; 12]"),
] {
let actual = parse_array_type(input);
assert_eq!(actual, expected);
}
}
#[test]
fn test_args_with_refs() {
let (item, mut errors) = do_stdlib(
quote! {
name = "someFn",
args = {
data = { docs = "The data for this function"},
},
},
quote! {
/// Docs
/// ```
/// someFn()
/// ```
fn someFn(
data: &'a str,
) -> i32 {
3
}
},
)
.unwrap();
if let Some(e) = errors.pop() {
panic!("{e}");
}
expectorate::assert_contents("tests/args_with_refs.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_args_with_lifetime() {
let (item, mut errors) = do_stdlib(
quote! {
name = "someFn",
args = {
data = { docs = "Arg for the function" },
}
},
quote! {
/// Docs
/// ```
/// someFn()
/// ```
fn someFn<'a>(
data: Foo<'a>,
) -> i32 {
3
}
},
)
.unwrap();
if let Some(e) = errors.pop() {
panic!("{e}");
}
expectorate::assert_contents("tests/args_with_lifetime.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_args_with_exec_state() {
let (item, mut errors) = do_stdlib(
quote! {
name = "someFunction",
},
quote! {
/// Docs
/// ```
/// someFunction()
/// ```
fn inner_some_function<'a>(
exec_state: &mut ExecState,
args: &Args,
) -> i32 {
3
}
},
)
.unwrap();
if let Some(e) = errors.pop() {
panic!("{e}");
}
expectorate::assert_contents("tests/test_args_with_exec_state.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_line_to() {
let (item, errors) = do_stdlib(
quote! {
name = "lineTo",
args = {
data = { docs = "the sketch you're adding the line to" },
sketch = { docs = "the sketch you're adding the line to" },
}
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is another code block.
/// yes sirrr.
/// lineTo
/// ```
fn inner_line_to(
data: LineToData,
sketch: Sketch,
args: &Args,
) -> Result<Sketch, KclError> {
Ok(())
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/lineTo.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_min() {
let (item, errors) = do_stdlib(
quote! {
name = "min",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is another code block.
/// yes sirrr.
/// min
/// ```
fn inner_min(
/// The args to do shit to.
args: Vec<f64>
) -> f64 {
let mut min = std::f64::MAX;
for arg in args.iter() {
if *arg < min {
min = *arg;
}
}
min
}
},
)
.unwrap();
let _expected = quote! {};
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/min.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_show() {
let (item, errors) = do_stdlib(
quote! {
name = "show",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is code.
/// It does other shit.
/// show
/// ```
fn inner_show(
/// The args to do shit to.
_args: Vec<f64>
) {
}
},
)
.unwrap();
let _expected = quote! {};
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/show.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_box() {
let (item, errors) = do_stdlib(
quote! {
name = "show",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is code.
/// It does other shit.
/// show
/// ```
fn inner_show(
/// The args to do shit to.
args: Box<f64>
) -> Box<f64> {
args
}
},
)
.unwrap();
let _expected = quote! {};
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/box.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_option() {
let (item, errors) = do_stdlib(
quote! {
name = "show",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is code.
/// It does other shit.
/// show
/// ```
fn inner_show(
/// The args to do shit to.
args: Option<f64>
) -> Result<Box<f64>> {
args
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/option.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_array() {
let (item, errors) = do_stdlib(
quote! {
name = "show",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is another code block.
/// yes sirrr.
/// show
/// ```
fn inner_show(
/// The args to do shit to.
args: [f64; 2]
) -> Result<Box<f64>> {
args
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/array.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_option_input_format() {
let (item, errors) = do_stdlib(
quote! {
name = "import",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is code.
/// It does other shit.
/// import
/// ```
fn inner_import(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Box<f64>> {
args
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/option_input_format.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_return_vec_sketch() {
let (item, errors) = do_stdlib(
quote! {
name = "import",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is code.
/// It does other shit.
/// import
/// ```
fn inner_import(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Vec<Sketch>> {
args
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/return_vec_sketch.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_return_vec_box_sketch() {
let (item, errors) = do_stdlib(
quote! {
name = "import",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is code.
/// It does other shit.
/// import
/// ```
fn inner_import(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Vec<Box<Sketch>>> {
args
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/return_vec_box_sketch.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_doc_comment_with_code() {
let (item, errors) = do_stdlib(
quote! {
name = "myFunc",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is another code block.
/// yes sirrr.
/// myFunc
/// ```
fn inner_my_func(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Vec<Box<Sketch>>> {
args
}
},
)
.unwrap();
assert!(errors.is_empty(), "{errors:?}");
expectorate::assert_contents("tests/doc_comment_with_code.gen", &get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_fail_non_camel_case() {
let (_, errors) = do_stdlib(
quote! {
name = "import_thing",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is another code block.
/// yes sirrr.
/// ```
fn inner_import_thing(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Vec<Box<Sketch>>> {
args
}
},
)
.unwrap();
assert!(!errors.is_empty());
assert_eq!(
errors[1].to_string(),
"stdlib function names must be in camel case: `import_thing`"
);
}
#[test]
fn test_stdlib_fail_no_code_block() {
let (_, errors) = do_stdlib(
quote! {
name = "import",
},
quote! {
fn inner_import(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Vec<Box<Sketch>>> {
args
}
},
)
.unwrap();
assert!(!errors.is_empty());
assert_eq!(
errors[1].to_string(),
"stdlib functions must have at least one code block"
);
}
#[test]
fn test_stdlib_fail_name_not_in_code_block() {
let (_, errors) = do_stdlib(
quote! {
name = "import",
},
quote! {
/// This is some function.
/// It does shit.
///
/// ```
/// This is another code block.
/// yes sirrr.
/// ```
fn inner_import(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Result<Vec<Box<Sketch>>> {
args
}
},
)
.unwrap();
assert!(!errors.is_empty());
assert_eq!(
errors[1].to_string(),
"stdlib functions must have the function name `import` in the code block"
);
}

View File

@ -1,91 +0,0 @@
// Unbox a Vec<Box<T>> to Vec<T>.
// Unbox a Box<T> to T.
pub(crate) fn unbox(t: syn::Type) -> syn::Type {
unbox_inner(unbox_vec(t))
}
// Unbox a syn::Type that is boxed to the inner object.
fn unbox_inner(t: syn::Type) -> syn::Type {
match t {
syn::Type::Path(syn::TypePath { ref path, .. }) => {
let path = &path.segments;
if path.len() == 1 {
let seg = &path[0];
if seg.ident == "Box" {
if let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { args, .. }) =
&seg.arguments
{
if args.len() == 1 {
let mut args = args.iter();
let ok = args.next().unwrap();
if let syn::GenericArgument::Type(ty) = ok {
return ty.clone();
}
}
}
}
}
}
_ => {
return t;
}
}
t
}
// For a Vec<Box<T>> return Vec<T>.
// For a Vec<T> return Vec<T>.
fn unbox_vec(t: syn::Type) -> syn::Type {
match t {
syn::Type::Path(syn::TypePath { ref path, .. }) => {
let path = &path.segments;
if path.len() == 1 {
let seg = &path[0];
if seg.ident == "Vec" {
if let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { args, .. }) =
&seg.arguments
{
if args.len() == 1 {
let mut args = args.iter();
let ok = args.next().unwrap();
if let syn::GenericArgument::Type(ty) = ok {
let unboxed = unbox(ty.clone());
// Wrap it back in a vec.
let wrapped = syn::Type::Path(syn::TypePath {
qself: None,
path: syn::Path {
leading_colon: None,
segments: {
let mut segments = syn::punctuated::Punctuated::new();
segments.push_value(syn::PathSegment {
ident: syn::Ident::new("Vec", proc_macro2::Span::call_site()),
arguments: syn::PathArguments::AngleBracketed(
syn::AngleBracketedGenericArguments {
colon2_token: None,
lt_token: syn::token::Lt::default(),
args: {
let mut args = syn::punctuated::Punctuated::new();
args.push_value(syn::GenericArgument::Type(unboxed));
args
},
gt_token: syn::token::Gt::default(),
},
),
});
segments
},
},
});
return wrapped;
}
}
}
}
}
}
_ => {
return t;
}
}
t
}