KCL stdlib 'map' function (#4054)

I had to revert https://github.com/KittyCAD/modeling-app/pull/4031 because it broke syntax highlighting. This is the same PR, but updated to fix syntax highlighting.

Highlighting broke because the KCL LSP could not determine how to autocomplete the `map` function. The first argument of `map` is `[KclValue]` and the LSP doesn't know any good suggestions for "any KCL value", so it error'd out. I am using the value `[0..9]` for this case now. Tested that syntax highlighting works again.
This commit is contained in:
Adam Chalmers
2024-10-01 08:50:23 -05:00
committed by GitHub
parent 9ca49c6366
commit 2a3693651a
37 changed files with 12424 additions and 33 deletions

View File

@ -81,6 +81,8 @@ impl StdLibFnArg {
} else if self.type_ == "TagIdentifier" && self.required {
// TODO: actually use the ast to populate this.
return Ok(Some((index, format!("${{{}:{}}}", index, "myTag"))));
} else if self.type_ == "[KclValue]" && self.required {
return Ok(Some((index, "[0..9]".to_owned())));
}
get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index)
}
@ -903,6 +905,12 @@ mod tests {
);
}
#[test]
fn get_autocomplete_snippet_map() {
let map_fn: Box<dyn StdLibFn> = Box::new(crate::std::array::Map);
let _snippet = map_fn.to_autocomplete_snippet().unwrap();
}
#[test]
fn get_autocomplete_snippet_pattern_linear_2d() {
let pattern_fn: Box<dyn StdLibFn> = Box::new(crate::std::patterns::PatternLinear2D);

View File

@ -4,13 +4,14 @@ use anyhow::Result;
use kcmc::{websocket::OkWebSocketResponseData, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use serde::de::DeserializeOwned;
use serde_json::Value as JValue;
use crate::{
ast::types::{parse_json_number_as_f64, TagDeclarator},
errors::{KclError, KclErrorDetails},
executor::{
ExecState, ExecutorContext, ExtrudeSurface, KclValue, Metadata, Sketch, SketchSet, SketchSurface, Solid,
SolidSet, SourceRange, TagIdentifier,
SolidSet, SourceRange, TagIdentifier, UserVal,
},
std::{shapes::SketchOrSurface, sketch::FaceTag, FnAsArg},
};
@ -497,18 +498,6 @@ where
}
}
impl<'a> FromArgs<'a> for KclValue {
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let Some(v) = args.args.get(i) else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Argument at index {i} was missing",),
source_ranges: vec![args.source_range],
}));
};
Ok(v.to_owned())
}
}
impl<'a, T> FromArgs<'a> for Option<T>
where
T: FromKclValue<'a> + Sized,
@ -587,6 +576,20 @@ impl<'a> FromKclValue<'a> for i64 {
}
}
impl<'a> FromKclValue<'a> for UserVal {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
arg.as_user_val().map(|x| x.to_owned())
}
}
impl<'a> FromKclValue<'a> for Vec<JValue> {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
arg.as_user_val()
.and_then(|uv| uv.value.as_array())
.map(ToOwned::to_owned)
}
}
impl<'a> FromKclValue<'a> for TagDeclarator {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
arg.get_tag_declarator().ok()
@ -599,6 +602,12 @@ impl<'a> FromKclValue<'a> for TagIdentifier {
}
}
impl<'a> FromKclValue<'a> for KclValue {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
Some(arg.clone())
}
}
macro_rules! impl_from_arg_via_json {
($typ:path) => {
impl<'a> FromKclValue<'a> for $typ {
@ -609,6 +618,15 @@ macro_rules! impl_from_arg_via_json {
};
}
impl<'a, T> FromKclValue<'a> for Vec<T>
where
T: serde::de::DeserializeOwned + FromKclValue<'a>,
{
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
from_user_val(arg)
}
}
macro_rules! impl_from_arg_for_array {
($n:literal) => {
impl<'a, T> FromKclValue<'a> for [T; $n]
@ -722,23 +740,3 @@ impl<'a> FromKclValue<'a> for SketchSurface {
}
}
}
impl<'a> FromKclValue<'a> for Vec<Sketch> {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
let KclValue::UserVal(uv) = arg else {
return None;
};
uv.get::<Vec<Sketch>>().map(|x| x.0)
}
}
impl<'a> FromKclValue<'a> for Vec<u64> {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
let KclValue::UserVal(uv) = arg else {
return None;
};
uv.get::<Vec<u64>>().map(|x| x.0)
}
}

View File

@ -1,4 +1,5 @@
use derive_docs::stdlib;
use serde_json::Value as JValue;
use super::{args::FromArgs, Args, FnAsArg};
use crate::{
@ -7,6 +8,94 @@ use crate::{
function_param::FunctionParam,
};
/// Apply a function to each element of an array.
pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (array, f): (Vec<JValue>, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;
let array: Vec<KclValue> = array
.into_iter()
.map(|jval| {
KclValue::UserVal(UserVal {
value: jval,
meta: vec![args.source_range.into()],
})
})
.collect();
let map_fn = FunctionParam {
inner: f.func,
fn_expr: f.expr,
meta: vec![args.source_range.into()],
ctx: args.ctx.clone(),
memory: *f.memory,
};
let new_array = inner_map(array, map_fn, exec_state, &args).await?;
let uv = UserVal::new(vec![args.source_range.into()], new_array);
Ok(KclValue::UserVal(uv))
}
/// Apply a function to every element of a list.
///
/// Given a list like `[a, b, c]`, and a function like `f`, returns
/// `[f(a), f(b), f(c)]`
/// ```no_run
/// const r = 10 // radius
/// fn drawCircle = (id) => {
/// return startSketchOn("XY")
/// |> circle({ center: [id * 2 * r, 0], radius: r}, %)
/// }
///
/// // Call `drawCircle`, passing in each element of the array.
/// // The outputs from each `drawCircle` form a new array,
/// // which is the return value from `map`.
/// const circles = map(
/// [1..3],
/// drawCircle
/// )
/// ```
/// ```no_run
/// const r = 10 // radius
/// // Call `map`, using an anonymous function instead of a named one.
/// const circles = map(
/// [1..3],
/// (id) => {
/// return startSketchOn("XY")
/// |> circle({ center: [id * 2 * r, 0], radius: r}, %)
/// }
/// )
/// ```
#[stdlib {
name = "map",
}]
async fn inner_map<'a>(
array: Vec<KclValue>,
map_fn: FunctionParam<'a>,
exec_state: &mut ExecState,
args: &'a Args,
) -> Result<Vec<KclValue>, KclError> {
let mut new_array = Vec::with_capacity(array.len());
for elem in array {
let new_elem = call_map_closure(elem, &map_fn, args.source_range, exec_state).await?;
new_array.push(new_elem);
}
Ok(new_array)
}
async fn call_map_closure<'a>(
input: KclValue,
map_fn: &FunctionParam<'a>,
source_range: SourceRange,
exec_state: &mut ExecState,
) -> Result<KclValue, KclError> {
let output = map_fn.call(exec_state, vec![input]).await?;
let source_ranges = vec![source_range];
let output = output.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: "Map function must return a value".to_string(),
source_ranges,
})
})?;
Ok(output)
}
/// For each item in an array, update a value.
pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (array, start, f): (Vec<u64>, Sketch, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;

View File

@ -98,6 +98,7 @@ lazy_static! {
Box::new(crate::std::patterns::PatternCircular3D),
Box::new(crate::std::patterns::PatternTransform),
Box::new(crate::std::array::Reduce),
Box::new(crate::std::array::Map),
Box::new(crate::std::chamfer::Chamfer),
Box::new(crate::std::fillet::Fillet),
Box::new(crate::std::fillet::GetOppositeEdge),

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB