Declare std kwarg functions in KCL and migrate circle (#5955)
* Support calling KCL std KW fns, and move circle to KCL std Signed-off-by: Nick Cameron <nrc@ncameron.org> * Doc comments on parameters Signed-off-by: Nick Cameron <nrc@ncameron.org> * Update grammar Signed-off-by: Nick Cameron <nrc@ncameron.org> * Change use of counterClockWise to ccw Signed-off-by: Nick Cameron <nrc@ncameron.org> --------- Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
@ -467,6 +467,10 @@ fn generate_type_from_kcl(ty: &TyData, file_name: String, example_name: String)
|
||||
});
|
||||
|
||||
let output = hbs.render("kclType", &data)?;
|
||||
let output = cleanup_type_links(
|
||||
&output,
|
||||
ty.referenced_types.iter().filter(|t| !DECLARED_TYPES.contains(&&***t)),
|
||||
);
|
||||
expectorate::assert_contents(format!("../../docs/kcl/{}.md", file_name), &output);
|
||||
|
||||
Ok(())
|
||||
@ -514,6 +518,13 @@ fn generate_function_from_kcl(function: &FnData, file_name: String) -> Result<()
|
||||
});
|
||||
|
||||
let output = hbs.render("function", &data)?;
|
||||
let output = cleanup_type_links(
|
||||
&output,
|
||||
function
|
||||
.referenced_types
|
||||
.iter()
|
||||
.filter(|t| !DECLARED_TYPES.contains(&&***t)),
|
||||
);
|
||||
expectorate::assert_contents(format!("../../docs/kcl/{}.md", file_name), &output);
|
||||
|
||||
Ok(())
|
||||
@ -677,6 +688,12 @@ fn cleanup_type_links<'a>(output: &str, types: impl Iterator<Item = &'a String>)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO handle union types generically rather than special casing them.
|
||||
cleaned_output = cleaned_output.replace(
|
||||
"`Sketch | Plane | Face`",
|
||||
"[`Sketch`](/docs/kcl/types/Sketch) `|` [`Plane`](/docs/kcl/types/Face) `|` [`Plane`](/docs/kcl/types/Face)",
|
||||
);
|
||||
|
||||
cleanup_static_links(&cleaned_output)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::str::FromStr;
|
||||
use std::{collections::HashSet, str::FromStr};
|
||||
|
||||
use regex::Regex;
|
||||
use tower_lsp::lsp_types::{
|
||||
@ -9,7 +9,7 @@ use tower_lsp::lsp_types::{
|
||||
use crate::{
|
||||
execution::annotations,
|
||||
parsing::{
|
||||
ast::types::{Annotation, Node, NonCodeNode, VariableKind},
|
||||
ast::types::{Annotation, Node, PrimitiveType, Type, VariableKind},
|
||||
token::NumericSuffix,
|
||||
},
|
||||
ModuleId,
|
||||
@ -59,7 +59,6 @@ impl CollectionVisitor {
|
||||
format!("std::{}::", self.name)
|
||||
};
|
||||
let mut dd = match var.kind {
|
||||
// TODO metadata for args
|
||||
VariableKind::Fn => DocData::Fn(FnData::from_ast(var, qual_name)),
|
||||
VariableKind::Const => DocData::Const(ConstData::from_ast(var, qual_name)),
|
||||
};
|
||||
@ -322,6 +321,8 @@ pub struct FnData {
|
||||
/// Code examples.
|
||||
/// These are tested and we know they compile and execute.
|
||||
pub examples: Vec<(String, ExampleProperties)>,
|
||||
#[allow(dead_code)]
|
||||
pub referenced_types: Vec<String>,
|
||||
}
|
||||
|
||||
impl FnData {
|
||||
@ -332,6 +333,17 @@ impl FnData {
|
||||
};
|
||||
let name = var.declaration.id.name.clone();
|
||||
qual_name.push_str(&name);
|
||||
|
||||
let mut referenced_types = HashSet::new();
|
||||
if let Some(t) = &expr.return_type {
|
||||
collect_type_names(&mut referenced_types, t);
|
||||
}
|
||||
for p in &expr.params {
|
||||
if let Some(t) = &p.type_ {
|
||||
collect_type_names(&mut referenced_types, t);
|
||||
}
|
||||
}
|
||||
|
||||
FnData {
|
||||
name,
|
||||
qual_name,
|
||||
@ -346,6 +358,7 @@ impl FnData {
|
||||
summary: None,
|
||||
description: None,
|
||||
examples: Vec::new(),
|
||||
referenced_types: referenced_types.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -414,7 +427,7 @@ impl FnData {
|
||||
}
|
||||
|
||||
#[allow(clippy::literal_string_with_formatting_args)]
|
||||
fn to_autocomplete_snippet(&self) -> String {
|
||||
pub(super) fn to_autocomplete_snippet(&self) -> String {
|
||||
if self.name == "loft" {
|
||||
return "loft([${0:sketch000}, ${1:sketch001}])${}".to_owned();
|
||||
} else if self.name == "hole" {
|
||||
@ -480,12 +493,12 @@ pub struct ArgData {
|
||||
/// If the argument is required.
|
||||
pub kind: ArgKind,
|
||||
/// Additional information that could be used instead of the type's description.
|
||||
/// This is helpful if the type is really basic, like "u32" -- that won't tell the user much about
|
||||
/// This is helpful if the type is really basic, like "number" -- that won't tell the user much about
|
||||
/// how this argument is meant to be used.
|
||||
pub docs: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ArgKind {
|
||||
Special,
|
||||
// Parameter is whether the arg is optional.
|
||||
@ -495,38 +508,47 @@ pub enum ArgKind {
|
||||
|
||||
impl ArgData {
|
||||
fn from_ast(arg: &crate::parsing::ast::types::Parameter) -> Self {
|
||||
ArgData {
|
||||
let mut result = ArgData {
|
||||
name: arg.identifier.name.clone(),
|
||||
ty: arg.type_.as_ref().map(|t| t.to_string()),
|
||||
// Doc comments are not yet supported on parameters.
|
||||
docs: None,
|
||||
kind: if arg.labeled {
|
||||
ArgKind::Labelled(arg.optional())
|
||||
} else {
|
||||
ArgKind::Special
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn _with_meta(&mut self, _meta: &[Node<NonCodeNode>]) {
|
||||
// TODO use comments for docs (we can't currently get the comments for an argument)
|
||||
result.with_comments(&arg.identifier.pre_comments);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn get_autocomplete_snippet(&self, index: usize) -> Option<(usize, String)> {
|
||||
match &self.ty {
|
||||
Some(s)
|
||||
if [
|
||||
"Sketch",
|
||||
"SketchSet",
|
||||
"Solid",
|
||||
"SolidSet",
|
||||
"SketchSurface",
|
||||
"SketchOrSurface",
|
||||
]
|
||||
.contains(&&**s) =>
|
||||
{
|
||||
Some((index, format!("${{{}:{}}}", index, "%")))
|
||||
let label = if self.kind == ArgKind::Special {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{} = ", self.name)
|
||||
};
|
||||
match self.ty.as_deref() {
|
||||
Some(s) if ["Sketch", "Solid", "Plane | Face", "Sketch | Plane | Face"].contains(&s) => {
|
||||
Some((index, format!("{label}${{{}:{}}}", index, "%")))
|
||||
}
|
||||
Some("number") if self.kind.required() => Some((index, format!(r#"{label}${{{}:3.14}}"#, index))),
|
||||
Some("Point2d") if self.kind.required() => Some((
|
||||
index + 1,
|
||||
format!(r#"{label}[${{{}:3.14}}, ${{{}:3.14}}]"#, index, index + 1),
|
||||
)),
|
||||
Some("Point3d") if self.kind.required() => Some((
|
||||
index + 2,
|
||||
format!(
|
||||
r#"{label}[${{{}:3.14}}, ${{{}:3.14}}, ${{{}:3.14}}]"#,
|
||||
index,
|
||||
index + 1,
|
||||
index + 2
|
||||
),
|
||||
)),
|
||||
Some("string") if self.kind.required() => Some((index, format!(r#"{label}${{{}:"string"}}"#, index))),
|
||||
Some("bool") if self.kind.required() => Some((index, format!(r#"{label}${{{}:false}}"#, index))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -570,12 +592,19 @@ pub struct TyData {
|
||||
/// Code examples.
|
||||
/// These are tested and we know they compile and execute.
|
||||
pub examples: Vec<(String, ExampleProperties)>,
|
||||
#[allow(dead_code)]
|
||||
pub referenced_types: Vec<String>,
|
||||
}
|
||||
|
||||
impl TyData {
|
||||
fn from_ast(ty: &crate::parsing::ast::types::TypeDeclaration, mut qual_name: String) -> Self {
|
||||
let name = ty.name.name.clone();
|
||||
qual_name.push_str(&name);
|
||||
let mut referenced_types = HashSet::new();
|
||||
if let Some(t) = &ty.alias {
|
||||
collect_type_names(&mut referenced_types, t);
|
||||
}
|
||||
|
||||
TyData {
|
||||
name,
|
||||
qual_name,
|
||||
@ -589,6 +618,7 @@ impl TyData {
|
||||
summary: None,
|
||||
description: None,
|
||||
examples: Vec::new(),
|
||||
referenced_types: referenced_types.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -852,6 +882,66 @@ impl ApplyMeta for TyData {
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplyMeta for ArgData {
|
||||
fn apply_docs(
|
||||
&mut self,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
_examples: Vec<(String, ExampleProperties)>,
|
||||
) {
|
||||
let Some(mut docs) = summary else {
|
||||
return;
|
||||
};
|
||||
if let Some(desc) = description {
|
||||
docs.push_str("\n\n");
|
||||
docs.push_str(&desc);
|
||||
}
|
||||
|
||||
self.docs = Some(docs);
|
||||
}
|
||||
|
||||
fn deprecated(&mut self, _deprecated: bool) {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
fn doc_hidden(&mut self, _doc_hidden: bool) {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
fn impl_kind(&mut self, _impl_kind: annotations::Impl) {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_type_names(acc: &mut HashSet<String>, ty: &Type) {
|
||||
match ty {
|
||||
Type::Primitive(primitive_type) => {
|
||||
acc.insert(collect_type_names_from_primitive(primitive_type));
|
||||
}
|
||||
Type::Array { ty, .. } => {
|
||||
acc.insert(collect_type_names_from_primitive(ty));
|
||||
}
|
||||
Type::Union { tys } => tys.iter().for_each(|t| {
|
||||
acc.insert(collect_type_names_from_primitive(t));
|
||||
}),
|
||||
Type::Object { properties } => properties.iter().for_each(|p| {
|
||||
if let Some(t) = &p.type_ {
|
||||
collect_type_names(acc, t)
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_type_names_from_primitive(ty: &PrimitiveType) -> String {
|
||||
match ty {
|
||||
PrimitiveType::String => "string".to_owned(),
|
||||
PrimitiveType::Number(_) => "number".to_owned(),
|
||||
PrimitiveType::Boolean => "bool".to_owned(),
|
||||
PrimitiveType::Tag => "tag".to_owned(),
|
||||
PrimitiveType::Named(id) => id.name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
@ -927,6 +927,8 @@ fn get_autocomplete_string_from_schema(schema: &schemars::schema::Schema) -> Res
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::docs::kcl_doc::{self, DocData};
|
||||
|
||||
use super::StdLibFn;
|
||||
|
||||
#[test]
|
||||
@ -1007,8 +1009,11 @@ mod tests {
|
||||
#[test]
|
||||
#[allow(clippy::literal_string_with_formatting_args)]
|
||||
fn get_autocomplete_snippet_circle() {
|
||||
let circle_fn: Box<dyn StdLibFn> = Box::new(crate::std::shapes::Circle);
|
||||
let snippet = circle_fn.to_autocomplete_snippet().unwrap();
|
||||
let data = kcl_doc::walk_prelude();
|
||||
let DocData::Fn(circle_fn) = data.into_iter().find(|d| d.name() == "circle").unwrap() else {
|
||||
panic!();
|
||||
};
|
||||
let snippet = circle_fn.to_autocomplete_snippet();
|
||||
assert_eq!(
|
||||
snippet,
|
||||
r#"circle(${0:%}, center = [${1:3.14}, ${2:3.14}], radius = ${3:3.14})${}"#
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use async_recursion::async_recursion;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use crate::{
|
||||
engine::ExecutionKind,
|
||||
@ -618,7 +619,11 @@ impl ExecutorContext {
|
||||
if let Some(std_path) = &exec_state.mod_local.settings.std_path {
|
||||
let (func, props) = crate::std::std_fn(std_path, statement_kind.expect_name());
|
||||
KclValue::Function {
|
||||
value: FunctionSource::Std { func, props },
|
||||
value: FunctionSource::Std {
|
||||
func,
|
||||
props,
|
||||
ast: function_expression.clone(),
|
||||
},
|
||||
meta: vec![metadata.to_owned()],
|
||||
}
|
||||
} else {
|
||||
@ -630,7 +635,7 @@ impl ExecutorContext {
|
||||
}
|
||||
} else {
|
||||
// Snapshotting memory here is crucial for semantics so that we close
|
||||
// over variables. Variables defined lexically later shouldn't
|
||||
// over variables. Variables defined lexically later shouldn't
|
||||
// be available to the function body.
|
||||
KclValue::Function {
|
||||
value: FunctionSource::User {
|
||||
@ -1130,7 +1135,7 @@ impl Node<CallExpressionKw> {
|
||||
let callsite: SourceRange = self.into();
|
||||
|
||||
// Build a hashmap from argument labels to the final evaluated values.
|
||||
let mut fn_args = HashMap::with_capacity(self.arguments.len());
|
||||
let mut fn_args = IndexMap::with_capacity(self.arguments.len());
|
||||
for arg_expr in &self.arguments {
|
||||
let source_range = SourceRange::from(arg_expr.arg.clone());
|
||||
let metadata = Metadata { source_range };
|
||||
@ -1191,10 +1196,34 @@ impl Node<CallExpressionKw> {
|
||||
None
|
||||
};
|
||||
|
||||
let formals = func.args(false);
|
||||
for (label, arg) in &args.kw_args.labeled {
|
||||
match formals.iter().find(|p| &p.name == label) {
|
||||
Some(p) => {
|
||||
if !p.label_required {
|
||||
exec_state.err(CompilationError::err(
|
||||
arg.source_range,
|
||||
format!(
|
||||
"The function `{fn_name}` expects an unlabeled first parameter (`{label}`), but it is labelled in the call"
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
exec_state.err(CompilationError::err(
|
||||
arg.source_range,
|
||||
format!("`{label}` is not an argument of `{fn_name}`"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to call the function.
|
||||
let mut return_value = {
|
||||
// Don't early-return in this block.
|
||||
exec_state.mut_stack().push_new_env_for_rust_call();
|
||||
let result = func.std_lib_fn()(exec_state, args).await;
|
||||
exec_state.mut_stack().pop_env();
|
||||
|
||||
if let Some(mut op) = op {
|
||||
op.set_std_lib_call_is_error(result.is_err());
|
||||
@ -1213,7 +1242,6 @@ impl Node<CallExpressionKw> {
|
||||
Ok(return_value)
|
||||
}
|
||||
FunctionKind::UserDefined => {
|
||||
let source_range = SourceRange::from(self);
|
||||
// Clone the function so that we can use a mutable reference to
|
||||
// exec_state.
|
||||
let func = fn_name.get_result(exec_state, ctx).await?.clone();
|
||||
@ -1237,17 +1265,21 @@ impl Node<CallExpressionKw> {
|
||||
source_range: callsite,
|
||||
});
|
||||
|
||||
let return_value = func
|
||||
.call_fn_kw(args, exec_state, ctx.clone(), callsite)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Add the call expression to the source ranges.
|
||||
// TODO currently ignored by the frontend
|
||||
e.add_source_ranges(vec![source_range])
|
||||
})?;
|
||||
let Some(fn_src) = func.as_fn() else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "cannot call this because it isn't a function".to_string(),
|
||||
source_ranges: vec![callsite],
|
||||
}));
|
||||
};
|
||||
|
||||
let return_value = fn_src.call_kw(exec_state, ctx, args, callsite).await.map_err(|e| {
|
||||
// Add the call expression to the source ranges.
|
||||
// TODO currently ignored by the frontend
|
||||
e.add_source_ranges(vec![callsite])
|
||||
})?;
|
||||
|
||||
let result = return_value.ok_or_else(move || {
|
||||
let mut source_ranges: Vec<SourceRange> = vec![source_range];
|
||||
let mut source_ranges: Vec<SourceRange> = vec![callsite];
|
||||
// We want to send the source range of the original function.
|
||||
if let KclValue::Function { meta, .. } = func {
|
||||
source_ranges = meta.iter().map(|m| m.source_range).collect();
|
||||
@ -1364,14 +1396,17 @@ impl Node<CallExpression> {
|
||||
source_range: callsite,
|
||||
});
|
||||
|
||||
let return_value = func
|
||||
.call_fn(fn_args, exec_state, ctx.clone(), source_range)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Add the call expression to the source ranges.
|
||||
// TODO currently ignored by the frontend
|
||||
e.add_source_ranges(vec![source_range])
|
||||
})?;
|
||||
let Some(fn_src) = func.as_fn() else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "cannot call this because it isn't a function".to_string(),
|
||||
source_ranges: vec![source_range],
|
||||
}));
|
||||
};
|
||||
let return_value = fn_src.call(exec_state, ctx, fn_args, source_range).await.map_err(|e| {
|
||||
// Add the call expression to the source ranges.
|
||||
// TODO currently ignored by the frontend
|
||||
e.add_source_ranges(vec![source_range])
|
||||
})?;
|
||||
|
||||
let result = return_value.ok_or_else(move || {
|
||||
let mut source_ranges: Vec<SourceRange> = vec![source_range];
|
||||
@ -1857,6 +1892,27 @@ fn assign_args_to_params_kw(
|
||||
mut args: crate::std::args::KwArgs,
|
||||
exec_state: &mut ExecState,
|
||||
) -> Result<(), KclError> {
|
||||
for (label, arg) in &args.labeled {
|
||||
match function_expression.params.iter().find(|p| &p.identifier.name == label) {
|
||||
Some(p) => {
|
||||
if !p.labeled {
|
||||
exec_state.err(CompilationError::err(
|
||||
arg.source_range,
|
||||
format!(
|
||||
"This function expects an unlabeled first parameter (`{label}`), but it is labelled in the call"
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
exec_state.err(CompilationError::err(
|
||||
arg.source_range,
|
||||
format!("`{label}` is not an argument of this function"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the arguments to the memory. A new call frame should have already
|
||||
// been created.
|
||||
let source_ranges = vec![function_expression.into()];
|
||||
@ -1905,10 +1961,11 @@ fn assign_args_to_params_kw(
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn call_user_defined_function(
|
||||
async fn call_user_defined_function(
|
||||
args: Vec<Arg>,
|
||||
memory: EnvironmentRef,
|
||||
function_expression: NodeRef<'_, FunctionExpression>,
|
||||
@ -1941,7 +1998,7 @@ pub(crate) async fn call_user_defined_function(
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) async fn call_user_defined_function_kw(
|
||||
async fn call_user_defined_function_kw(
|
||||
args: crate::std::args::KwArgs,
|
||||
memory: EnvironmentRef,
|
||||
function_expression: NodeRef<'_, FunctionExpression>,
|
||||
@ -1979,35 +2036,138 @@ impl FunctionSource {
|
||||
&self,
|
||||
exec_state: &mut ExecState,
|
||||
ctx: &ExecutorContext,
|
||||
args: Vec<Arg>,
|
||||
source_range: SourceRange,
|
||||
mut args: Vec<Arg>,
|
||||
callsite: SourceRange,
|
||||
) -> Result<Option<KclValue>, KclError> {
|
||||
match self {
|
||||
FunctionSource::Std { func, props } => {
|
||||
FunctionSource::Std { props, .. } => {
|
||||
if args.len() <= 1 {
|
||||
let args = crate::std::Args::new_kw(
|
||||
KwArgs {
|
||||
unlabeled: args.pop(),
|
||||
labeled: IndexMap::new(),
|
||||
},
|
||||
callsite,
|
||||
ctx.clone(),
|
||||
exec_state.mod_local.pipe_value.clone().map(|v| Arg::new(v, callsite)),
|
||||
);
|
||||
self.call_kw(exec_state, ctx, args, callsite).await
|
||||
} else {
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("{} requires its arguments to be labelled", props.name),
|
||||
source_ranges: vec![callsite],
|
||||
}))
|
||||
}
|
||||
}
|
||||
FunctionSource::User { ast, memory, .. } => {
|
||||
call_user_defined_function(args, *memory, ast, exec_state, ctx).await
|
||||
}
|
||||
FunctionSource::None => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_kw(
|
||||
&self,
|
||||
exec_state: &mut ExecState,
|
||||
ctx: &ExecutorContext,
|
||||
mut args: crate::std::Args,
|
||||
callsite: SourceRange,
|
||||
) -> Result<Option<KclValue>, KclError> {
|
||||
match self {
|
||||
FunctionSource::Std { func, ast, props } => {
|
||||
if props.deprecated {
|
||||
exec_state.warn(CompilationError::err(
|
||||
source_range,
|
||||
callsite,
|
||||
format!(
|
||||
"`{}` is deprecated, see the docs for a recommended replacement",
|
||||
props.name
|
||||
),
|
||||
));
|
||||
}
|
||||
let args = crate::std::Args::new(
|
||||
args,
|
||||
source_range,
|
||||
ctx.clone(),
|
||||
exec_state
|
||||
.mod_local
|
||||
.pipe_value
|
||||
.clone()
|
||||
.map(|v| Arg::new(v, source_range)),
|
||||
);
|
||||
|
||||
func(exec_state, args).await.map(Some)
|
||||
for (label, arg) in &mut args.kw_args.labeled {
|
||||
match ast.params.iter().find(|p| &p.identifier.name == label) {
|
||||
Some(p) => {
|
||||
if !p.labeled {
|
||||
exec_state.err(CompilationError::err(
|
||||
arg.source_range,
|
||||
format!(
|
||||
"The function `{}` expects an unlabeled first parameter (`{label}`), but it is labelled in the call",
|
||||
props.name
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(ty) = &p.type_ {
|
||||
arg.value = arg
|
||||
.value
|
||||
.coerce(
|
||||
&RuntimeType::from_parsed(ty.inner.clone(), exec_state, arg.source_range)
|
||||
.unwrap(),
|
||||
exec_state,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"{label} requires a value with type `{}`, but found {}",
|
||||
ty.inner,
|
||||
arg.value.human_friendly_type()
|
||||
),
|
||||
source_ranges: vec![callsite],
|
||||
})
|
||||
})?;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
exec_state.err(CompilationError::err(
|
||||
arg.source_range,
|
||||
format!("`{label}` is not an argument of `{}`", props.name),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(arg) = &mut args.kw_args.unlabeled {
|
||||
if let Some(p) = ast.params.iter().find(|p| !p.labeled) {
|
||||
if let Some(ty) = &p.type_ {
|
||||
arg.value = arg
|
||||
.value
|
||||
.coerce(
|
||||
&RuntimeType::from_parsed(ty.inner.clone(), exec_state, arg.source_range).unwrap(),
|
||||
exec_state,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"The input argument of {} requires a value with type `{}`, but found {}",
|
||||
props.name,
|
||||
ty.inner,
|
||||
arg.value.human_friendly_type()
|
||||
),
|
||||
source_ranges: vec![callsite],
|
||||
})
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to call the function.
|
||||
exec_state.mut_stack().push_new_env_for_rust_call();
|
||||
let mut result = {
|
||||
// Don't early-return in this block.
|
||||
let result = func(exec_state, args).await;
|
||||
exec_state.mut_stack().pop_env();
|
||||
|
||||
// TODO support recording op into the feature tree
|
||||
result
|
||||
}?;
|
||||
|
||||
update_memory_for_tags_of_geometry(&mut result, exec_state)?;
|
||||
|
||||
Ok(Some(result))
|
||||
}
|
||||
FunctionSource::User { ast, memory, .. } => {
|
||||
call_user_defined_function(args, *memory, ast, exec_state, ctx).await
|
||||
call_user_defined_function_kw(args.kw_args, *memory, ast, exec_state, ctx).await
|
||||
}
|
||||
FunctionSource::None => unreachable!(),
|
||||
}
|
||||
|
@ -9,13 +9,13 @@ use crate::{
|
||||
errors::KclErrorDetails,
|
||||
execution::{
|
||||
types::{NumericType, PrimitiveType, RuntimeType},
|
||||
ExecState, ExecutorContext, Face, Helix, ImportedGeometry, Metadata, Plane, Sketch, Solid, TagIdentifier,
|
||||
Face, Helix, ImportedGeometry, Metadata, Plane, Sketch, Solid, TagIdentifier,
|
||||
},
|
||||
parsing::ast::types::{
|
||||
DefaultParamVal, FunctionExpression, KclNone, Literal, LiteralValue, Node, TagDeclarator, TagNode,
|
||||
},
|
||||
std::{args::Arg, StdFnProps},
|
||||
CompilationError, KclError, ModuleId, SourceRange,
|
||||
std::StdFnProps,
|
||||
KclError, ModuleId, SourceRange,
|
||||
};
|
||||
|
||||
pub type KclObjectFields = HashMap<String, KclValue>;
|
||||
@ -113,6 +113,7 @@ pub enum FunctionSource {
|
||||
None,
|
||||
Std {
|
||||
func: crate::std::StdFn,
|
||||
ast: crate::parsing::ast::types::BoxNode<FunctionExpression>,
|
||||
props: StdFnProps,
|
||||
},
|
||||
User {
|
||||
@ -550,91 +551,10 @@ impl KclValue {
|
||||
Ok(*b)
|
||||
}
|
||||
|
||||
/// If this memory item is a function, call it with the given arguments, return its val as Ok.
|
||||
/// If it's not a function, return Err.
|
||||
pub async fn call_fn(
|
||||
&self,
|
||||
args: Vec<Arg>,
|
||||
exec_state: &mut ExecState,
|
||||
ctx: ExecutorContext,
|
||||
source_range: SourceRange,
|
||||
) -> Result<Option<KclValue>, KclError> {
|
||||
pub fn as_fn(&self) -> Option<&FunctionSource> {
|
||||
match self {
|
||||
KclValue::Function {
|
||||
value: FunctionSource::Std { func, props },
|
||||
..
|
||||
} => {
|
||||
if props.deprecated {
|
||||
exec_state.warn(CompilationError::err(
|
||||
source_range,
|
||||
format!(
|
||||
"`{}` is deprecated, see the docs for a recommended replacement",
|
||||
props.name
|
||||
),
|
||||
));
|
||||
}
|
||||
exec_state.mut_stack().push_new_env_for_rust_call();
|
||||
let args = crate::std::Args::new(
|
||||
args,
|
||||
source_range,
|
||||
ctx.clone(),
|
||||
exec_state
|
||||
.mod_local
|
||||
.pipe_value
|
||||
.clone()
|
||||
.map(|v| Arg::new(v, source_range)),
|
||||
);
|
||||
let result = func(exec_state, args).await.map(Some);
|
||||
exec_state.mut_stack().pop_env();
|
||||
result
|
||||
}
|
||||
KclValue::Function {
|
||||
value: FunctionSource::User { ast, memory, .. },
|
||||
..
|
||||
} => crate::execution::exec_ast::call_user_defined_function(args, *memory, ast, exec_state, &ctx).await,
|
||||
_ => Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "cannot call this because it isn't a function".to_string(),
|
||||
source_ranges: vec![source_range],
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// If this is a function, call it by applying keyword arguments.
|
||||
/// If it's not a function, returns an error.
|
||||
pub async fn call_fn_kw(
|
||||
&self,
|
||||
args: crate::std::Args,
|
||||
exec_state: &mut ExecState,
|
||||
ctx: ExecutorContext,
|
||||
callsite: SourceRange,
|
||||
) -> Result<Option<KclValue>, KclError> {
|
||||
match self {
|
||||
KclValue::Function {
|
||||
value: FunctionSource::Std { func: _, props },
|
||||
..
|
||||
} => {
|
||||
if props.deprecated {
|
||||
exec_state.warn(CompilationError::err(
|
||||
callsite,
|
||||
format!(
|
||||
"`{}` is deprecated, see the docs for a recommended replacement",
|
||||
props.name
|
||||
),
|
||||
));
|
||||
}
|
||||
todo!("Implement KCL stdlib fns with keyword args");
|
||||
}
|
||||
KclValue::Function {
|
||||
value: FunctionSource::User { ast, memory, .. },
|
||||
..
|
||||
} => {
|
||||
crate::execution::exec_ast::call_user_defined_function_kw(args.kw_args, *memory, ast, exec_state, &ctx)
|
||||
.await
|
||||
}
|
||||
_ => Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "cannot call this because it isn't a function".to_string(),
|
||||
source_ranges: vec![callsite],
|
||||
})),
|
||||
KclValue::Function { value, .. } => Some(value),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,6 +87,7 @@ pub(crate) fn read_std(mod_name: &str) -> Option<&'static str> {
|
||||
match mod_name {
|
||||
"prelude" => Some(include_str!("../std/prelude.kcl")),
|
||||
"math" => Some(include_str!("../std/math.kcl")),
|
||||
"sketch" => Some(include_str!("../std/sketch.kcl")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -2883,15 +2883,40 @@ fn uom_for_type(i: &mut TokenSlice) -> PResult<NumericSuffix> {
|
||||
any.try_map(|t: Token| t.value.parse()).parse_next(i)
|
||||
}
|
||||
|
||||
fn comment(i: &mut TokenSlice) -> PResult<Node<String>> {
|
||||
any.verify_map(|token: Token| {
|
||||
let value = match token.token_type {
|
||||
TokenType::LineComment => token.value,
|
||||
TokenType::BlockComment => token.value,
|
||||
_ => return None,
|
||||
};
|
||||
Some(Node::new(value, token.start, token.end, token.module_id))
|
||||
})
|
||||
.context(expected("Comment"))
|
||||
.parse_next(i)
|
||||
}
|
||||
|
||||
fn comments(i: &mut TokenSlice) -> PResult<Node<Vec<String>>> {
|
||||
let comments: Vec<Node<String>> = repeat(1.., (comment, opt(whitespace)).map(|(c, _)| c)).parse_next(i)?;
|
||||
let start = comments[0].start;
|
||||
let module_id = comments[0].module_id;
|
||||
let end = comments.last().unwrap().end;
|
||||
let inner = comments.into_iter().map(|n| n.inner).collect();
|
||||
Ok(Node::new(inner, start, end, module_id))
|
||||
}
|
||||
|
||||
struct ParamDescription {
|
||||
labeled: bool,
|
||||
arg_name: Token,
|
||||
type_: std::option::Option<Node<Type>>,
|
||||
default_value: Option<DefaultParamVal>,
|
||||
comments: Option<Node<Vec<String>>>,
|
||||
}
|
||||
|
||||
fn parameter(i: &mut TokenSlice) -> PResult<ParamDescription> {
|
||||
let (found_at_sign, arg_name, question_mark, _, type_, _ws, default_literal) = (
|
||||
let (_, comments, found_at_sign, arg_name, question_mark, _, type_, _ws, default_literal) = (
|
||||
opt(whitespace),
|
||||
opt(comments),
|
||||
opt(at_sign),
|
||||
any.verify(|token: &Token| !matches!(token.token_type, TokenType::Brace) || token.value != ")"),
|
||||
opt(question_mark),
|
||||
@ -2901,6 +2926,7 @@ fn parameter(i: &mut TokenSlice) -> PResult<ParamDescription> {
|
||||
opt((equals, opt(whitespace), literal).map(|(_, _, literal)| literal)),
|
||||
)
|
||||
.parse_next(i)?;
|
||||
|
||||
Ok(ParamDescription {
|
||||
labeled: found_at_sign.is_none(),
|
||||
arg_name,
|
||||
@ -2915,6 +2941,7 @@ fn parameter(i: &mut TokenSlice) -> PResult<ParamDescription> {
|
||||
return Err(ErrMode::Backtrack(ContextError::from(e)));
|
||||
}
|
||||
},
|
||||
comments,
|
||||
})
|
||||
}
|
||||
|
||||
@ -2924,6 +2951,7 @@ fn parameters(i: &mut TokenSlice) -> PResult<Vec<Parameter>> {
|
||||
let candidates: Vec<_> = separated(0.., parameter, comma_sep)
|
||||
.context(expected("function parameters"))
|
||||
.parse_next(i)?;
|
||||
opt(comma_sep).parse_next(i)?;
|
||||
|
||||
// Make sure all those tokens are valid parameters.
|
||||
let params: Vec<Parameter> = candidates
|
||||
@ -2934,8 +2962,13 @@ fn parameters(i: &mut TokenSlice) -> PResult<Vec<Parameter>> {
|
||||
arg_name,
|
||||
type_,
|
||||
default_value,
|
||||
comments,
|
||||
}| {
|
||||
let identifier = Node::<Identifier>::try_from(arg_name)?;
|
||||
let mut identifier = Node::<Identifier>::try_from(arg_name)?;
|
||||
if let Some(comments) = comments {
|
||||
identifier.comment_start = comments.start;
|
||||
identifier.pre_comments = comments.inner;
|
||||
}
|
||||
|
||||
Ok(Parameter {
|
||||
identifier,
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::{collections::HashMap, num::NonZeroU32};
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use anyhow::Result;
|
||||
use indexmap::IndexMap;
|
||||
use kcmc::{
|
||||
websocket::{ModelingCmdReq, OkWebSocketResponseData},
|
||||
ModelingCmd,
|
||||
@ -57,7 +58,7 @@ pub struct KwArgs {
|
||||
/// Unlabeled keyword args. Currently only the first arg can be unlabeled.
|
||||
pub unlabeled: Option<Arg>,
|
||||
/// Labeled args.
|
||||
pub labeled: HashMap<String, Arg>,
|
||||
pub labeled: IndexMap<String, Arg>,
|
||||
}
|
||||
|
||||
impl KwArgs {
|
||||
@ -1000,48 +1001,54 @@ where
|
||||
|
||||
impl<'a> FromKclValue<'a> for [f64; 2] {
|
||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||
let KclValue::MixedArray { value, meta: _ } = arg else {
|
||||
return None;
|
||||
};
|
||||
if value.len() != 2 {
|
||||
return None;
|
||||
match arg {
|
||||
KclValue::MixedArray { value, meta: _ } | KclValue::HomArray { value, .. } => {
|
||||
if value.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
let v0 = value.first()?;
|
||||
let v1 = value.get(1)?;
|
||||
let array = [v0.as_f64()?, v1.as_f64()?];
|
||||
Some(array)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
let v0 = value.first()?;
|
||||
let v1 = value.get(1)?;
|
||||
let array = [v0.as_f64()?, v1.as_f64()?];
|
||||
Some(array)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromKclValue<'a> for [usize; 3] {
|
||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||
let KclValue::MixedArray { value, meta: _ } = arg else {
|
||||
return None;
|
||||
};
|
||||
if value.len() != 3 {
|
||||
return None;
|
||||
match arg {
|
||||
KclValue::MixedArray { value, meta: _ } | KclValue::HomArray { value, .. } => {
|
||||
if value.len() != 3 {
|
||||
return None;
|
||||
}
|
||||
let v0 = value.first()?;
|
||||
let v1 = value.get(1)?;
|
||||
let v2 = value.get(2)?;
|
||||
let array = [v0.as_usize()?, v1.as_usize()?, v2.as_usize()?];
|
||||
Some(array)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
let v0 = value.first()?;
|
||||
let v1 = value.get(1)?;
|
||||
let v2 = value.get(2)?;
|
||||
let array = [v0.as_usize()?, v1.as_usize()?, v2.as_usize()?];
|
||||
Some(array)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromKclValue<'a> for [f64; 3] {
|
||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||
let KclValue::MixedArray { value, meta: _ } = arg else {
|
||||
return None;
|
||||
};
|
||||
if value.len() != 3 {
|
||||
return None;
|
||||
match arg {
|
||||
KclValue::MixedArray { value, meta: _ } | KclValue::HomArray { value, .. } => {
|
||||
if value.len() != 3 {
|
||||
return None;
|
||||
}
|
||||
let v0 = value.first()?;
|
||||
let v1 = value.get(1)?;
|
||||
let v2 = value.get(2)?;
|
||||
let array = [v0.as_f64()?, v1.as_f64()?, v2.as_f64()?];
|
||||
Some(array)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
let v0 = value.first()?;
|
||||
let v1 = value.get(1)?;
|
||||
let v2 = value.get(2)?;
|
||||
let array = [v0.as_f64()?, v1.as_f64()?, v2.as_f64()?];
|
||||
Some(array)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,19 +47,19 @@ fn inner_rem(num: f64, divisor: f64) -> f64 {
|
||||
|
||||
/// Compute the cosine of a number (in radians).
|
||||
pub async fn cos(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let num = args.get_number()?;
|
||||
let num: f64 = args.get_unlabeled_kw_arg("input")?;
|
||||
Ok(args.make_user_val_from_f64_with_type(TyF64::count(num.cos())))
|
||||
}
|
||||
|
||||
/// Compute the sine of a number (in radians).
|
||||
pub async fn sin(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let num = args.get_number()?;
|
||||
let num: f64 = args.get_unlabeled_kw_arg("input")?;
|
||||
Ok(args.make_user_val_from_f64_with_type(TyF64::count(num.sin())))
|
||||
}
|
||||
|
||||
/// Compute the tangent of a number (in radians).
|
||||
pub async fn tan(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let num = args.get_number()?;
|
||||
let num: f64 = args.get_unlabeled_kw_arg("input")?;
|
||||
Ok(args.make_user_val_from_f64_with_type(TyF64::count(num.tan())))
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,6 @@ lazy_static! {
|
||||
Box::new(crate::std::segment::TangentToEnd),
|
||||
Box::new(crate::std::segment::AngleToMatchLengthX),
|
||||
Box::new(crate::std::segment::AngleToMatchLengthY),
|
||||
Box::new(crate::std::shapes::Circle),
|
||||
Box::new(crate::std::shapes::CircleThreePoint),
|
||||
Box::new(crate::std::shapes::Polygon),
|
||||
Box::new(crate::std::sketch::Line),
|
||||
@ -203,6 +202,10 @@ pub(crate) fn std_fn(path: &str, fn_name: &str) -> (crate::std::StdFn, StdFnProp
|
||||
|e, a| Box::pin(crate::std::math::tan(e, a)),
|
||||
StdFnProps::default("std::math::tan"),
|
||||
),
|
||||
("sketch", "circle") => (
|
||||
|e, a| Box::pin(crate::std::shapes::circle(e, a)),
|
||||
StdFnProps::default("std::sketch::circle"),
|
||||
),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
@ -46,38 +46,6 @@ pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct a 2-dimensional circle, of the specified radius, centered at
|
||||
/// the provided (x, y) origin point.
|
||||
///
|
||||
/// ```no_run
|
||||
/// exampleSketch = startSketchOn("-XZ")
|
||||
/// |> circle( center = [0, 0], radius = 10 )
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 5)
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// exampleSketch = startSketchOn("XZ")
|
||||
/// |> startProfileAt([-15, 0], %)
|
||||
/// |> line(end = [30, 0])
|
||||
/// |> line(end = [0, 30])
|
||||
/// |> line(end = [-30, 0])
|
||||
/// |> close()
|
||||
/// |> hole(circle( center = [0, 15], radius = 5), %)
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 5)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "circle",
|
||||
keywords = true,
|
||||
unlabeled_first = true,
|
||||
args = {
|
||||
sketch_or_surface = {docs = "Plane or surface to sketch on."},
|
||||
center = {docs = "The center of the circle."},
|
||||
radius = {docs = "The radius of the circle."},
|
||||
tag = { docs = "Create a new tag which refers to this circle"},
|
||||
}
|
||||
}]
|
||||
async fn inner_circle(
|
||||
sketch_or_surface: SketchOrSurface,
|
||||
center: [f64; 2],
|
||||
@ -152,13 +120,13 @@ async fn inner_circle(
|
||||
|
||||
/// Sketch a 3-point circle.
|
||||
pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let sketch_surface_or_group = args.get_unlabeled_kw_arg("sketch_surface_or_group")?;
|
||||
let p1 = args.get_kw_arg("p1")?;
|
||||
let p2 = args.get_kw_arg("p2")?;
|
||||
let p3 = args.get_kw_arg("p3")?;
|
||||
let sketch_surface_or_group = args.get_unlabeled_kw_arg("sketch_surface_or_group")?;
|
||||
let tag = args.get_kw_arg_opt("tag")?;
|
||||
|
||||
let sketch = inner_circle_three_point(p1, p2, p3, sketch_surface_or_group, tag, exec_state, args).await?;
|
||||
let sketch = inner_circle_three_point(sketch_surface_or_group, p1, p2, p3, tag, exec_state, args).await?;
|
||||
Ok(KclValue::Sketch {
|
||||
value: Box::new(sketch),
|
||||
})
|
||||
@ -176,10 +144,10 @@ pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Resul
|
||||
keywords = true,
|
||||
unlabeled_first = true,
|
||||
args = {
|
||||
sketch_surface_or_group = {docs = "Plane or surface to sketch on."},
|
||||
p1 = {docs = "1st point to derive the circle."},
|
||||
p2 = {docs = "2nd point to derive the circle."},
|
||||
p3 = {docs = "3rd point to derive the circle."},
|
||||
sketch_surface_or_group = {docs = "Plane or surface to sketch on."},
|
||||
tag = {docs = "Identifier for the circle to reference elsewhere."},
|
||||
}
|
||||
}]
|
||||
@ -187,10 +155,10 @@ pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Resul
|
||||
// Similar to inner_circle, but needs to retain 3-point information in the
|
||||
// path so it can be used for other features, otherwise it's lost.
|
||||
async fn inner_circle_three_point(
|
||||
sketch_surface_or_group: SketchOrSurface,
|
||||
p1: [f64; 2],
|
||||
p2: [f64; 2],
|
||||
p3: [f64; 2],
|
||||
sketch_surface_or_group: SketchOrSurface,
|
||||
tag: Option<TagNode>,
|
||||
exec_state: &mut ExecState,
|
||||
args: Args,
|
||||
|
Reference in New Issue
Block a user