KCL stdlib reduce function (#3881)

Adds an `arrayReduce` function to KCL stdlib. Right now, it can only reduce SketchGroup values because my implementation of higher-order KCL functions sucks. But we will generalize it in the future to be able to reduce any type.

This simplifies sketching polygons, e.g.

```
fn decagon = (radius) => {
  let step = (1/10) * tau()
  let sketch = startSketchAt([
    (cos(0) * radius), 
    (sin(0) * radius),
  ])
  return arrayReduce([1..10], sketch, (i, sg) => {
      let x = cos(step * i) * radius
      let y = sin(step * i) * radius
      return lineTo([x, y], sg)
  })
}
```

Part of #3842
This commit is contained in:
Adam Chalmers
2024-09-14 00:10:17 -04:00
committed by GitHub
parent 3cd3e1af72
commit f235a950b0
9 changed files with 7651 additions and 2 deletions

858
docs/kcl/arrayReduce.md Normal file

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,7 @@ layout: manual
* [`angledLineToX`](kcl/angledLineToX)
* [`angledLineToY`](kcl/angledLineToY)
* [`arc`](kcl/arc)
* [`arrayReduce`](kcl/arrayReduce)
* [`asin`](kcl/asin)
* [`assert`](kcl/assert)
* [`assertEqual`](kcl/assertEqual)

File diff suppressed because it is too large Load Diff

View File

@ -548,7 +548,7 @@ fn array_end_start(i: TokenSlice) -> PResult<ArrayExpression> {
})
}
/// Parse n..m into a vec of numbers [n, n+1, ..., m]
/// Parse n..m into a vec of numbers [n, n+1, ..., m-1]
fn integer_range(i: TokenSlice) -> PResult<Vec<Expr>> {
let (token0, floor) = integer.parse_next(i)?;
double_period.parse_next(i)?;

View File

@ -503,6 +503,18 @@ 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,
@ -716,3 +728,13 @@ impl<'a> FromKclValue<'a> for Vec<SketchGroup> {
uv.get::<Vec<SketchGroup>>().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

@ -0,0 +1,96 @@
use derive_docs::stdlib;
use schemars::JsonSchema;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{KclValue, SketchGroup, SourceRange, UserVal},
function_param::FunctionParam,
};
use super::{args::FromArgs, Args, FnAsArg};
/// For each item in an array, update a value.
pub async fn array_reduce(args: Args) -> Result<KclValue, KclError> {
let (array, start, f): (Vec<u64>, SketchGroup, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;
let reduce_fn = FunctionParam {
inner: f.func,
fn_expr: f.expr,
meta: vec![args.source_range.into()],
ctx: args.ctx.clone(),
memory: *f.memory,
dynamic_state: args.dynamic_state.clone(),
};
inner_array_reduce(array, start, reduce_fn, &args)
.await
.map(|sg| KclValue::UserVal(UserVal::set(sg.meta.clone(), sg)))
}
/// Take a starting value. Then, for each element of an array, calculate the next value,
/// using the previous value and the element.
/// ```no_run
/// fn decagon = (radius) => {
/// let step = (1/10) * tau()
/// let sketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
/// return arrayReduce([1..10], sketch, (i, sg) => {
/// let x = cos(step * i) * radius
/// let y = sin(step * i) * radius
/// return lineTo([x, y], sg)
/// })
/// }
/// decagon(5.0) |> close(%)
/// ```
#[stdlib {
name = "arrayReduce",
}]
async fn inner_array_reduce<'a>(
array: Vec<u64>,
start: SketchGroup,
reduce_fn: FunctionParam<'a>,
args: &'a Args,
) -> Result<SketchGroup, KclError> {
let mut reduced = start;
for i in array {
reduced = call_reduce_closure(i, reduced, &reduce_fn, args.source_range).await?;
}
Ok(reduced)
}
async fn call_reduce_closure<'a>(
i: u64,
start: SketchGroup,
reduce_fn: &FunctionParam<'a>,
source_range: SourceRange,
) -> Result<SketchGroup, KclError> {
// Call the reduce fn for this repetition.
let reduce_fn_args = vec![
KclValue::UserVal(UserVal {
value: serde_json::Value::Number(i.into()),
meta: vec![source_range.into()],
}),
KclValue::new_user_val(start.meta.clone(), start),
];
let transform_fn_return = reduce_fn.call(reduce_fn_args).await?;
// Unpack the returned transform object.
let source_ranges = vec![source_range];
let closure_retval = transform_fn_return.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: "Reducer function must return a value".to_string(),
source_ranges: source_ranges.clone(),
})
})?;
let Some(out) = closure_retval.as_user_val() else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Reducer function must return a UserValue".to_string(),
source_ranges: source_ranges.clone(),
}));
};
let Some((out, _meta)) = out.get() else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Reducer function must return a SketchGroup".to_string(),
source_ranges: source_ranges.clone(),
}));
};
Ok(out)
}

View File

@ -1,6 +1,7 @@
//! Functions implemented for language execution.
pub mod args;
pub mod array;
pub mod assert;
pub mod chamfer;
pub mod convert;
@ -91,6 +92,7 @@ lazy_static! {
Box::new(crate::std::patterns::PatternCircular2D),
Box::new(crate::std::patterns::PatternCircular3D),
Box::new(crate::std::patterns::PatternTransform),
Box::new(crate::std::array::ArrayReduce),
Box::new(crate::std::chamfer::Chamfer),
Box::new(crate::std::fillet::Fillet),
Box::new(crate::std::fillet::GetOppositeEdge),

View File

@ -134,7 +134,7 @@ async fn inner_pattern_transform<'a>(
args: &'a Args,
) -> Result<Vec<Box<ExtrudeGroup>>, KclError> {
// Build the vec of transforms, one for each repetition.
let mut transform = Vec::new();
let mut transform = Vec::with_capacity(usize::try_from(num_repetitions).unwrap());
for i in 0..num_repetitions {
let t = make_transform(i, &transform_function, args.source_range).await?;
transform.push(t);

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB