Remove just one enum (#1096)

# Problem

This is my proposal for fixing #1107 . I've only done it for one stdlib function, `tangentialArcTo` -- if y'all like it, I'll apply this idea to the rest of the stdlib.

Previously, if users want to put a tag on the arc, the function's parameters change type.

```
// Tag missing: first param is array
tangentialArcTo([x, y], %)
// Tag present: first param is object
tangentialArcTo({to: [x, y], tag: "myTag"}, %)
```

# Solution

My proposal in #1006 is that KCL should have optional values. This means we can change the stdlib `tangentialArcTo` function to use them. In this PR, the calls are now like

```
// Tag missing: first param is array
tangentialArcTo([x, y], %)
// Tag present: first param is array still, but we now pass a tag at the end.
tangentialArcTo([x, y], %, "myTag")
```

This adds an "option" type to KCL typesystem, but it's not really revealed to users (no KCL types are revealed to users right now, they write untyped code and only interact with types when they get type errors upon executing programs). Also adds a None type, which is the default case of the Optional enum.
This commit is contained in:
Adam Chalmers
2023-12-18 23:49:32 -06:00
committed by GitHub
parent a61d931826
commit 9d40f282a8
18 changed files with 774 additions and 141 deletions

View File

@ -19765,25 +19765,9 @@
"tags": [],
"args": [
{
"name": "data",
"type": "TangentialArcToData",
"name": "to",
"type": "[number]",
"schema": {
"description": "Data to draw a tangential arc to a specific point.",
"anyOf": [
{
"description": "A point with a tag.",
"type": "object",
"required": [
"tag",
"to"
],
"properties": {
"tag": {
"description": "The tag.",
"type": "string"
},
"to": {
"description": "Where the arc should end. Must lie in the same plane as the current path pen position. Must not be colinear with current path pen position.",
"type": "array",
"items": {
"type": "number",
@ -19791,20 +19775,6 @@
},
"maxItems": 2,
"minItems": 2
}
}
},
{
"description": "A point where the arc should end. Must lie in the same plane as the current path pen position. Must not be colinear with current path pen position.",
"type": "array",
"items": {
"type": "number",
"format": "double"
},
"maxItems": 2,
"minItems": 2
}
]
},
"required": true
},
@ -20246,6 +20216,15 @@
}
},
"required": true
},
{
"name": "tag",
"type": "String",
"schema": {
"type": "string",
"nullable": true
},
"required": true
}
],
"returnValue": {

View File

@ -3905,21 +3905,12 @@ Draw an arc.
```
tangentialArcTo(data: TangentialArcToData, sketch_group: SketchGroup) -> SketchGroup
tangentialArcTo(to: [number], sketch_group: SketchGroup, tag: String) -> SketchGroup
```
#### Arguments
* `data`: `TangentialArcToData` - Data to draw a tangential arc to a specific point.
```
{
// The tag.
tag: string,
// Where the arc should end. Must lie in the same plane as the current path pen position. Must not be colinear with current path pen position.
to: [number, number],
} |
[number, number]
```
* `to`: `[number]`
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths.
```
{
@ -3985,6 +3976,7 @@ tangentialArcTo(data: TangentialArcToData, sketch_group: SketchGroup) -> SketchG
}],
}
```
* `tag`: `String`
#### Returns

View File

@ -1537,7 +1537,8 @@ export function isLiteralArrayOrStatic(
if (!val) return false
if (Array.isArray(val)) {
const [a, b] = val
const a = val[0]
const b = val[1]
return isLiteralArrayOrStatic(a) && isLiteralArrayOrStatic(b)
}
return (
@ -1550,7 +1551,8 @@ export function isNotLiteralArrayOrStatic(
val: Value | [Value, Value] | [Value, Value, Value]
): boolean {
if (Array.isArray(val)) {
const [a, b] = val
const a = val[0]
const b = val[1]
return isNotLiteralArrayOrStatic(a) && isNotLiteralArrayOrStatic(b)
}
return (

View File

@ -719,24 +719,12 @@ version = "0.1.4"
dependencies = [
"convert_case",
"expectorate",
"once_cell",
"openapitor",
"pretty_assertions",
"proc-macro2",
"quote",
"serde",
"serde_tokenstream",
"syn 2.0.39",
]
[[package]]
name = "derive-docs"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c357dec14992ba88803535217ed83d6f6cd80efcb8fa8e3f8a30a9b84fadc1c7"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"regex",
"serde",
"serde_tokenstream",
"syn 2.0.39",
@ -1436,7 +1424,7 @@ dependencies = [
"criterion",
"dashmap",
"databake",
"derive-docs 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"derive-docs",
"expectorate",
"futures",
"insta",

View File

@ -14,8 +14,10 @@ proc-macro = true
[dependencies]
convert_case = "0.6.0"
once_cell = "1.18.0"
proc-macro2 = "1"
quote = "1"
regex = "1.10"
serde = { version = "1.0.193", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.39", features = ["full"] }

View File

@ -1,15 +1,11 @@
// Copyright 2023 Oxide Computer Company
//! This package defines macro attributes associated with HTTP handlers. These
//! attributes are used both to define an HTTP API and to generate an OpenAPI
//! Spec (OAS) v3 document that describes the API.
// Clippy's style advice is definitely valuable, but not worth the trouble for
// automated enforcement.
#![allow(clippy::style)]
use convert_case::Casing;
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::{
@ -209,6 +205,18 @@ fn do_stdlib_inner(
quote! {
Vec<#ty_ident>
}
} else if ty_string.starts_with("Option<") {
let ty_string = ty_string.trim_start_matches("Option<").trim_end_matches('>');
let ty_ident = format_ident!("{}", 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 = format_ident!("{}", 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 = format_ident!("{}", ty_string);
@ -222,10 +230,13 @@ fn do_stdlib_inner(
}
};
let ty_string = clean_type(&ty_string);
let ty_string = rust_type_to_openapi_type(&ty_string);
if ty_string != "Args" {
let schema = if ty_ident.to_string().starts_with("Vec < ") {
let schema = if ty_ident.to_string().starts_with("Vec < ")
|| ty_ident.to_string().starts_with("Option <")
|| ty_ident.to_string().starts_with('[')
{
quote! {
<#ty_ident>::json_schema(&mut generator)
}
@ -263,7 +274,7 @@ fn do_stdlib_inner(
ret_ty_string.trim().to_string()
};
let ret_ty_ident = format_ident!("{}", ret_ty_string);
let ret_ty_string = clean_type(&ret_ty_string);
let ret_ty_string = rust_type_to_openapi_type(&ret_ty_string);
quote! {
Some(#docs_crate::StdLibFnArg {
name: "".to_string(),
@ -488,15 +499,22 @@ impl Parse for ItemFnForSignature {
}
}
fn clean_type(t: &str) -> String {
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 let Some((inner_type, _length)) = parse_array_type(&t) {
t = format!("[{inner_type}]")
}
if t == "f64" {
return "number".to_string();
@ -507,6 +525,14 @@ fn clean_type(t: &str) -> 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))
}
#[cfg(test)]
mod tests {
@ -514,6 +540,19 @@ mod tests {
use super::*;
#[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_stdlib_line_to() {
let (item, errors) = do_stdlib(
@ -608,4 +647,46 @@ mod tests {
assert!(errors.is_empty());
expectorate::assert_contents("tests/box.gen", &openapitor::types::get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_option() {
let (item, errors) = do_stdlib(
quote! {
name = "show",
},
quote! {
fn inner_show(
/// The args to do shit to.
args: Option<f64>
) -> Box<f64> {
args
}
},
)
.unwrap();
assert!(errors.is_empty());
expectorate::assert_contents("tests/option.gen", &openapitor::types::get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_array() {
let (item, errors) = do_stdlib(
quote! {
name = "show",
},
quote! {
fn inner_show(
/// The args to do shit to.
args: [f64; 2]
) -> Box<f64> {
args
}
},
)
.unwrap();
assert!(errors.is_empty());
expectorate::assert_contents("tests/array.gen", &openapitor::types::get_text_fmt(&item).unwrap());
}
}

View File

@ -0,0 +1,82 @@
#[allow(non_camel_case_types, missing_docs)]
#[doc = "Std lib function: show"]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars :: JsonSchema, ts_rs :: TS)]
#[ts(export)]
pub(crate) struct Show {}
#[allow(non_upper_case_globals, missing_docs)]
#[doc = "Std lib function: show"]
pub(crate) const Show: Show = Show {};
fn boxed_show(
args: crate::std::Args,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = anyhow::Result<crate::executor::MemoryItem, crate::errors::KclError>,
>,
>,
> {
Box::pin(show(args))
}
impl crate::docs::StdLibFn for Show {
fn name(&self) -> String {
"show".to_string()
}
fn summary(&self) -> String {
"".to_string()
}
fn description(&self) -> String {
"".to_string()
}
fn tags(&self) -> Vec<String> {
vec![]
}
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
vec![crate::docs::StdLibFnArg {
name: "args".to_string(),
type_: "[number]".to_string(),
schema: <[f64; 2usize]>::json_schema(&mut generator),
required: true,
}]
}
fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
Some(crate::docs::StdLibFnArg {
name: "".to_string(),
type_: "number".to_string(),
schema: f64::json_schema(&mut generator),
required: true,
})
}
fn unpublished(&self) -> bool {
false
}
fn deprecated(&self) -> bool {
false
}
fn std_lib_fn(&self) -> crate::std::StdFn {
boxed_show
}
fn clone_box(&self) -> Box<dyn crate::docs::StdLibFn> {
Box::new(self.clone())
}
}
fn inner_show(#[doc = r" The args to do shit to."] args: [f64; 2]) -> Box<f64> {
args
}

View File

@ -0,0 +1,82 @@
#[allow(non_camel_case_types, missing_docs)]
#[doc = "Std lib function: show"]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars :: JsonSchema, ts_rs :: TS)]
#[ts(export)]
pub(crate) struct Show {}
#[allow(non_upper_case_globals, missing_docs)]
#[doc = "Std lib function: show"]
pub(crate) const Show: Show = Show {};
fn boxed_show(
args: crate::std::Args,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = anyhow::Result<crate::executor::MemoryItem, crate::errors::KclError>,
>,
>,
> {
Box::pin(show(args))
}
impl crate::docs::StdLibFn for Show {
fn name(&self) -> String {
"show".to_string()
}
fn summary(&self) -> String {
"".to_string()
}
fn description(&self) -> String {
"".to_string()
}
fn tags(&self) -> Vec<String> {
vec![]
}
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
vec![crate::docs::StdLibFnArg {
name: "args".to_string(),
type_: "number".to_string(),
schema: <Option<f64>>::json_schema(&mut generator),
required: true,
}]
}
fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
Some(crate::docs::StdLibFnArg {
name: "".to_string(),
type_: "number".to_string(),
schema: f64::json_schema(&mut generator),
required: true,
})
}
fn unpublished(&self) -> bool {
false
}
fn deprecated(&self) -> bool {
false
}
fn std_lib_fn(&self) -> crate::std::StdFn {
boxed_show
}
fn clone_box(&self) -> Box<dyn crate::docs::StdLibFn> {
Box::new(self.clone())
}
}
fn inner_show(#[doc = r" The args to do shit to."] args: Option<f64>) -> Box<f64> {
args
}

View File

@ -18,8 +18,8 @@ async-trait = "0.1.73"
clap = { version = "4.4.8", features = ["cargo", "derive", "env", "unicode"], optional = true }
dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] }
derive-docs = { version = "0.1.4" }
#derive-docs = { path = "../derive-docs" }
# derive-docs = { version = "0.1.4" }
derive-docs = { path = "../derive-docs" }
kittycad = { workspace = true }
lazy_static = "1.4.0"
parse-display = "0.8.2"

View File

@ -1,6 +1,6 @@
//! Data types for the AST.
use std::{collections::HashMap, fmt::Write};
use std::{collections::HashMap, fmt::Write, ops::RangeInclusive};
use anyhow::Result;
use databake::*;
@ -11,6 +11,7 @@ use serde_json::{Map, Value as JValue};
use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, DocumentSymbol, Range as LspRange, SymbolKind};
pub use self::literal_value::LiteralValue;
pub use self::none::KclNone;
use crate::{
docs::StdLibFn,
errors::{KclError, KclErrorDetails},
@ -20,6 +21,7 @@ use crate::{
};
mod literal_value;
mod none;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
@ -407,6 +409,7 @@ pub enum Value {
ObjectExpression(Box<ObjectExpression>),
MemberExpression(Box<MemberExpression>),
UnaryExpression(Box<UnaryExpression>),
None(KclNone),
}
impl Value {
@ -423,6 +426,9 @@ impl Value {
Value::PipeExpression(pipe_exp) => pipe_exp.recast(options, indentation_level),
Value::UnaryExpression(unary_exp) => unary_exp.recast(options),
Value::PipeSubstitution(_) => crate::parser::PIPE_SUBSTITUTION_OPERATOR.to_string(),
Value::None(_) => {
unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
}
}
}
@ -444,6 +450,7 @@ impl Value {
Value::PipeExpression(ref mut pipe_exp) => pipe_exp.replace_value(source_range, new_value),
Value::UnaryExpression(ref mut unary_exp) => unary_exp.replace_value(source_range, new_value),
Value::PipeSubstitution(_) => {}
Value::None(_) => {}
}
}
@ -460,6 +467,7 @@ impl Value {
Value::ObjectExpression(object_expression) => object_expression.start(),
Value::MemberExpression(member_expression) => member_expression.start(),
Value::UnaryExpression(unary_expression) => unary_expression.start(),
Value::None(none) => none.start,
}
}
@ -476,6 +484,7 @@ impl Value {
Value::ObjectExpression(object_expression) => object_expression.end(),
Value::MemberExpression(member_expression) => member_expression.end(),
Value::UnaryExpression(unary_expression) => unary_expression.end(),
Value::None(none) => none.end,
}
}
@ -483,19 +492,22 @@ impl Value {
/// This is really recursive so keep that in mind.
pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
match self {
Value::Literal(_literal) => None,
Value::Identifier(_identifier) => None,
Value::BinaryExpression(binary_expression) => binary_expression.get_hover_value_for_position(pos, code),
Value::FunctionExpression(function_expression) => {
function_expression.get_hover_value_for_position(pos, code)
}
Value::CallExpression(call_expression) => call_expression.get_hover_value_for_position(pos, code),
Value::PipeExpression(pipe_expression) => pipe_expression.get_hover_value_for_position(pos, code),
Value::PipeSubstitution(_) => None,
Value::ArrayExpression(array_expression) => array_expression.get_hover_value_for_position(pos, code),
Value::ObjectExpression(object_expression) => object_expression.get_hover_value_for_position(pos, code),
Value::MemberExpression(member_expression) => member_expression.get_hover_value_for_position(pos, code),
Value::UnaryExpression(unary_expression) => unary_expression.get_hover_value_for_position(pos, code),
// TODO: LSP hover information for values/types. https://github.com/KittyCAD/modeling-app/issues/1126
Value::None(_) => None,
Value::Literal(_) => None,
Value::Identifier(_) => None,
// TODO: LSP hover information for symbols. https://github.com/KittyCAD/modeling-app/issues/1127
Value::PipeSubstitution(_) => None,
}
}
@ -519,6 +531,7 @@ impl Value {
member_expression.rename_identifiers(old_name, new_name)
}
Value::UnaryExpression(ref mut unary_expression) => unary_expression.rename_identifiers(old_name, new_name),
Value::None(_) => {}
}
}
@ -539,6 +552,7 @@ impl Value {
Value::ObjectExpression(object_expression) => object_expression.get_constraint_level(),
Value::MemberExpression(member_expression) => member_expression.get_constraint_level(),
Value::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(),
Value::None(none) => none.get_constraint_level(),
}
}
}
@ -926,6 +940,7 @@ impl CallExpression {
for arg in &self.arguments {
let result: MemoryItem = match arg {
Value::None(none) => none.into(),
Value::Literal(literal) => literal.into(),
Value::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?;
@ -991,7 +1006,7 @@ impl CallExpression {
if fn_args.len() != function_expression.params.len() {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Expected {} arguments, got {}",
"this function expected {} arguments, got {}",
function_expression.params.len(),
fn_args.len(),
),
@ -1607,6 +1622,7 @@ impl ArrayExpression {
for element in &self.elements {
let result = match element {
Value::Literal(literal) => literal.into(),
Value::None(none) => none.into(),
Value::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?;
value.clone()
@ -1759,6 +1775,7 @@ impl ObjectExpression {
for property in &self.properties {
let result = match &property.value {
Value::Literal(literal) => literal.into(),
Value::None(none) => none.into(),
Value::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?;
value.clone()
@ -2646,6 +2663,23 @@ impl FunctionExpression {
}
}
/// Required parameters must be declared before optional parameters.
/// This gets all the required parameters.
pub fn required_params(&self) -> &[Parameter] {
let end_of_required_params = self
.params
.iter()
.position(|param| param.optional)
// If there's no optional params, then all the params are required params.
.unwrap_or(self.params.len());
&self.params[..end_of_required_params]
}
/// Minimum and maximum number of arguments this function can take.
pub fn number_of_args(&self) -> RangeInclusive<usize> {
self.required_params().len()..=self.params.len()
}
pub fn replace_value(&mut self, source_range: SourceRange, new_value: Value) {
self.body.replace_value(source_range, new_value);
}
@ -3400,4 +3434,107 @@ const thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
);
}
}
#[test]
fn required_params() {
for (i, (test_name, expected, function_expr)) in [
(
"no params",
(0..=0),
FunctionExpression {
start: 0,
end: 0,
params: vec![],
body: Program {
start: 0,
end: 0,
body: Vec::new(),
non_code_meta: Default::default(),
},
},
),
(
"all required params",
(1..=1),
FunctionExpression {
start: 0,
end: 0,
params: vec![Parameter {
identifier: Identifier {
start: 0,
end: 0,
name: "foo".to_owned(),
},
optional: false,
}],
body: Program {
start: 0,
end: 0,
body: Vec::new(),
non_code_meta: Default::default(),
},
},
),
(
"all optional params",
(0..=1),
FunctionExpression {
start: 0,
end: 0,
params: vec![Parameter {
identifier: Identifier {
start: 0,
end: 0,
name: "foo".to_owned(),
},
optional: true,
}],
body: Program {
start: 0,
end: 0,
body: Vec::new(),
non_code_meta: Default::default(),
},
},
),
(
"mixed params",
(1..=2),
FunctionExpression {
start: 0,
end: 0,
params: vec![
Parameter {
identifier: Identifier {
start: 0,
end: 0,
name: "foo".to_owned(),
},
optional: false,
},
Parameter {
identifier: Identifier {
start: 0,
end: 0,
name: "bar".to_owned(),
},
optional: true,
},
],
body: Program {
start: 0,
end: 0,
body: Vec::new(),
non_code_meta: Default::default(),
},
},
),
]
.into_iter()
.enumerate()
{
let actual = function_expr.number_of_args();
assert_eq!(expected, actual, "failed test #{i} '{test_name}'");
}
}
}

View File

@ -0,0 +1,58 @@
//! KCL has optional parameters. Their type is [`KclOption`].
//! If an optional parameter is not given, it will have a value of type [`KclNone`].
use databake::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::executor::{MemoryItem, SourceRange, UserVal};
use super::ConstraintLevel;
/// KCL value for an optional parameter which was not given an argument.
/// (remember, parameters are in the function declaration,
/// arguments are in the function call/application).
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake, Default)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
#[serde(tag = "type")]
pub struct KclNone {
// TODO: Convert this to be an Option<SourceRange>.
pub start: usize,
pub end: usize,
}
impl From<&KclNone> for SourceRange {
fn from(v: &KclNone) -> Self {
Self([v.start, v.end])
}
}
impl From<&KclNone> for UserVal {
fn from(none: &KclNone) -> Self {
UserVal {
value: serde_json::to_value(none).expect("can always serialize a None"),
meta: Default::default(),
}
}
}
impl From<&KclNone> for MemoryItem {
fn from(none: &KclNone) -> Self {
let val = UserVal::from(none);
MemoryItem::UserVal(val)
}
}
impl KclNone {
pub fn source_range(&self) -> SourceRange {
SourceRange([self.start, self.end])
}
/// Get the constraint level.
/// KCL None is never constrained.
pub fn get_constraint_level(&self) -> ConstraintLevel {
ConstraintLevel::None {
source_ranges: vec![self.source_range()],
}
}
}

View File

@ -4,7 +4,7 @@ use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
use crate::executor::SourceRange;
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone)]
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
#[ts(export)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum KclError {
@ -32,7 +32,7 @@ pub enum KclError {
Internal(KclErrorDetails),
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone)]
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
#[ts(export)]
pub struct KclErrorDetails {
#[serde(rename = "sourceRanges")]

View File

@ -8,10 +8,11 @@ use kittycad::types::{Color, ModelingCmd, Point3D};
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value as JValue;
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
use crate::{
ast::types::{BodyItem, FunctionExpression, Value},
ast::types::{BodyItem, FunctionExpression, KclNone, Value},
engine::{EngineConnection, EngineManager},
errors::{KclError, KclErrorDetails},
std::{FunctionKind, StdLib},
@ -49,6 +50,7 @@ impl ProgramMemory {
}
/// Get a value from the program memory.
/// Return Err if not found.
pub fn get(&self, key: &str, source_range: SourceRange) -> Result<&MemoryItem, KclError> {
self.root.get(key).ok_or_else(|| {
KclError::UndefinedValue(KclErrorDetails {
@ -349,6 +351,40 @@ impl MemoryItem {
}
}
/// Get a JSON value and deserialize it into some concrete type.
pub fn get_json<T: serde::de::DeserializeOwned>(&self) -> Result<T, KclError> {
let json = self.get_json_value()?;
serde_json::from_value(json).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to deserialize struct from JSON: {}", e),
source_ranges: self.clone().into(),
})
})
}
/// Get a JSON value and deserialize it into some concrete type.
/// If it's a KCL None, return None. Otherwise return Some.
pub fn get_json_opt<T: serde::de::DeserializeOwned>(&self) -> Result<Option<T>, KclError> {
let json = self.get_json_value()?;
if let JValue::Object(ref o) = json {
if let Some(JValue::String(s)) = o.get("type") {
if s == "KclNone" {
return Ok(None);
}
}
}
serde_json::from_value(json)
.map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to deserialize struct from JSON: {}", e),
source_ranges: self.clone().into(),
})
})
.map(Some)
}
/// 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(
@ -873,6 +909,9 @@ pub async fn execute(
let metadata = Metadata { source_range };
match &declaration.init {
Value::None(none) => {
memory.add(&var_name, none.into(), source_range)?;
}
Value::Literal(literal) => {
memory.add(&var_name, literal.into(), source_range)?;
}
@ -892,27 +931,8 @@ pub async fn execute(
_metadata: Vec<Metadata>,
ctx: ExecutorContext| {
Box::pin(async move {
let mut fn_memory = memory.clone();
if args.len() != function_expression.params.len() {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Expected {} arguments, got {}",
function_expression.params.len(),
args.len(),
),
source_ranges: vec![(&function_expression).into()],
}));
}
// Add the arguments to the memory.
for (index, param) in function_expression.params.iter().enumerate() {
fn_memory.add(
&param.identifier.name,
args.get(index).unwrap().clone(),
(&param.identifier).into(),
)?;
}
let mut fn_memory =
assign_args_to_params(&function_expression, args, memory.clone())?;
let result = execute(
function_expression.body.clone(),
@ -1010,6 +1030,9 @@ pub async fn execute(
}
Value::PipeSubstitution(_) => {}
Value::FunctionExpression(_) => {}
Value::None(none) => {
memory.return_ = Some(ProgramReturn::Value(MemoryItem::from(none)));
}
},
}
}
@ -1017,10 +1040,67 @@ pub async fn execute(
Ok(memory.clone())
}
/// For each argument given,
/// assign it to a parameter of the function, in the given block of function memory.
/// Returns Err if too few/too many arguments were given for the function.
fn assign_args_to_params(
function_expression: &FunctionExpression,
args: Vec<MemoryItem>,
mut fn_memory: ProgramMemory,
) -> Result<ProgramMemory, KclError> {
let num_args = function_expression.number_of_args();
let (min_params, max_params) = num_args.into_inner();
let n = args.len();
// Check if the user supplied too many arguments
// (we'll check for too few arguments below).
let err_wrong_number_args = KclError::Semantic(KclErrorDetails {
message: if min_params == max_params {
format!("Expected {min_params} arguments, got {n}")
} else {
format!("Expected {min_params}-{max_params} arguments, got {n}")
},
source_ranges: vec![function_expression.into()],
});
if n > max_params {
return Err(err_wrong_number_args);
}
// Add the arguments to the memory.
for (index, param) in function_expression.params.iter().enumerate() {
if let Some(arg) = args.get(index) {
// Argument was provided.
fn_memory.add(&param.identifier.name, arg.clone(), (&param.identifier).into())?;
} else {
// Argument was not provided.
if param.optional {
// If the corresponding parameter is optional,
// then it's fine, the user doesn't need to supply it.
let none = KclNone {
start: param.identifier.start,
end: param.identifier.end,
};
fn_memory.add(
&param.identifier.name,
MemoryItem::from(&none),
(&param.identifier).into(),
)?;
} else {
// But if the corresponding parameter was required,
// then the user has called with too few arguments.
return Err(err_wrong_number_args);
}
}
}
Ok(fn_memory)
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::ast::types::{Identifier, Parameter};
use super::*;
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
@ -1566,4 +1646,122 @@ show(bracket)
"#;
parse_execute(ast).await.unwrap();
}
#[test]
fn test_assign_args_to_params() {
// Set up a little framework for this test.
fn mem(number: usize) -> MemoryItem {
MemoryItem::UserVal(UserVal {
value: number.into(),
meta: Default::default(),
})
}
fn ident(s: &'static str) -> Identifier {
Identifier {
start: 0,
end: 0,
name: s.to_owned(),
}
}
fn opt_param(s: &'static str) -> Parameter {
Parameter {
identifier: ident(s),
optional: true,
}
}
fn req_param(s: &'static str) -> Parameter {
Parameter {
identifier: ident(s),
optional: false,
}
}
// Declare the test cases.
for (test_name, params, args, expected) in [
("empty", Vec::new(), Vec::new(), Ok(ProgramMemory::new())),
(
"all params required, and all given, should be OK",
vec![req_param("x")],
vec![mem(1)],
Ok(ProgramMemory {
return_: None,
root: HashMap::from([("x".to_owned(), mem(1))]),
}),
),
(
"all params required, none given, should error",
vec![req_param("x")],
vec![],
Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![SourceRange([0, 0])],
message: "Expected 1 arguments, got 0".to_owned(),
})),
),
(
"all params optional, none given, should be OK",
vec![opt_param("x")],
vec![],
Ok(ProgramMemory {
return_: None,
root: HashMap::from([("x".to_owned(), MemoryItem::from(&KclNone::default()))]),
}),
),
(
"mixed params, too few given",
vec![req_param("x"), opt_param("y")],
vec![],
Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![SourceRange([0, 0])],
message: "Expected 1-2 arguments, got 0".to_owned(),
})),
),
(
"mixed params, minimum given, should be OK",
vec![req_param("x"), opt_param("y")],
vec![mem(1)],
Ok(ProgramMemory {
return_: None,
root: HashMap::from([
("x".to_owned(), mem(1)),
("y".to_owned(), MemoryItem::from(&KclNone::default())),
]),
}),
),
(
"mixed params, maximum given, should be OK",
vec![req_param("x"), opt_param("y")],
vec![mem(1), mem(2)],
Ok(ProgramMemory {
return_: None,
root: HashMap::from([("x".to_owned(), mem(1)), ("y".to_owned(), mem(2))]),
}),
),
(
"mixed params, too many given",
vec![req_param("x"), opt_param("y")],
vec![mem(1), mem(2), mem(3)],
Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![SourceRange([0, 0])],
message: "Expected 1-2 arguments, got 3".to_owned(),
})),
),
] {
// Run each test.
let func_expr = &FunctionExpression {
start: 0,
end: 0,
params,
body: crate::ast::types::Program {
start: 0,
end: 0,
body: Vec::new(),
non_code_meta: Default::default(),
},
};
let actual = assign_args_to_params(func_expr, args, ProgramMemory::new());
assert_eq!(
actual, expected,
"failed test '{test_name}':\ngot {actual:?}\nbut expected\n{expected:?}"
);
}
}
}

View File

@ -299,6 +299,15 @@ fn operand(i: TokenSlice) -> PResult<BinaryPart> {
message: TODO_783.to_owned(),
}))
}
Value::None(_) => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges,
// TODO: Better error message here.
// Once we have ways to use None values (e.g. by replacing with a default value)
// we should suggest one of them here.
message: "cannot use a KCL None value as an operand".to_owned(),
}));
}
Value::UnaryExpression(x) => BinaryPart::UnaryExpression(x),
Value::Literal(x) => BinaryPart::Literal(x),
Value::Identifier(x) => BinaryPart::Identifier(x),

View File

@ -154,6 +154,7 @@ impl Default for StdLib {
}
}
#[derive(Debug)]
pub enum FunctionKind {
Core(Box<dyn StdLibFn>),
Std(Box<dyn KclStdLibFn>),

View File

@ -6,6 +6,7 @@ use kittycad::types::{Angle, ModelingCmd, Point3D};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::executor::SourceRange;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{
@ -1122,7 +1123,7 @@ async fn inner_tangential_arc(
path: sketch_group.id,
segment: kittycad::types::PathSegment::TangentialArc {
radius: *radius,
offset: kittycad::types::Angle {
offset: Angle {
unit: kittycad::types::UnitAngle::Degrees,
value: *offset,
},
@ -1178,27 +1179,32 @@ fn tan_arc_to(sketch_group: &SketchGroup, to: &[f64; 2]) -> ModelingCmd {
}
}
/// Data to draw a tangential arc to a specific point.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase", untagged)]
pub enum TangentialArcToData {
/// A point with a tag.
PointWithTag {
/// Where the arc should end. Must lie in the same plane as the current path pen position. Must not be colinear with current path pen position.
to: [f64; 2],
/// The tag.
tag: String,
},
/// A point where the arc should end. Must lie in the same plane as the current path pen position. Must not be colinear with current path pen position.
Point([f64; 2]),
fn too_few_args(source_range: SourceRange) -> KclError {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: "too few arguments".to_owned(),
})
}
fn get_arg<I: Iterator>(it: &mut I, src: SourceRange) -> Result<I::Item, KclError> {
it.next().ok_or_else(|| too_few_args(src))
}
/// Draw a tangential arc to a specific point.
pub async fn tangential_arc_to(args: Args) -> Result<MemoryItem, KclError> {
let (data, sketch_group): (TangentialArcToData, Box<SketchGroup>) = args.get_data_and_sketch_group()?;
let src = args.source_range;
let new_sketch_group = inner_tangential_arc_to(data, sketch_group, args).await?;
// Get arguments to function call
let mut it = args.args.iter();
let to: [f64; 2] = get_arg(&mut it, src)?.get_json()?;
let sketch_group: Box<SketchGroup> = get_arg(&mut it, src)?.get_json()?;
let tag = if let Ok(memory_item) = get_arg(&mut it, src) {
memory_item.get_json_opt()?
} else {
None
};
let new_sketch_group = inner_tangential_arc_to(to, sketch_group, tag, args).await?;
Ok(MemoryItem::SketchGroup(new_sketch_group))
}
@ -1207,29 +1213,23 @@ pub async fn tangential_arc_to(args: Args) -> Result<MemoryItem, KclError> {
name = "tangentialArcTo",
}]
async fn inner_tangential_arc_to(
data: TangentialArcToData,
to: [f64; 2],
sketch_group: Box<SketchGroup>,
tag: Option<String>,
args: Args,
) -> Result<Box<SketchGroup>, KclError> {
let from: Point2d = sketch_group.get_coords_from_paths()?;
let to = match &data {
TangentialArcToData::PointWithTag { to, .. } => to,
TangentialArcToData::Point(to) => to,
};
let [to_x, to_y] = to;
let delta = [to[0] - from.x, to[1] - from.y];
let delta = [to_x - from.x, to_y - from.y];
let id = uuid::Uuid::new_v4();
args.send_modeling_cmd(id, tan_arc_to(&sketch_group, &delta)).await?;
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
to: *to,
name: if let TangentialArcToData::PointWithTag { tag, .. } = data {
tag.to_string()
} else {
"".to_string()
},
to,
name: tag.unwrap_or_default(),
geo_meta: GeoMeta {
id,
metadata: args.source_range.into(),

View File

@ -474,6 +474,24 @@ show(square)
twenty_twenty::assert_image("tests/executor/outputs/holes.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn optional_params() {
let code = r#"
fn circle = (pos, radius, tag?) => {
const sg = startSketchOn('XY')
|> startProfileAt(pos, %)
|> arc({angle_end: 360, angle_start: 0, radius: radius}, %)
|> close(%)
return sg
}
show(circle([2, 2], 20))
"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/optional_params.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_rounded_with_holes() {
let code = r#"fn circle = (pos, radius) => {
@ -488,17 +506,21 @@ async fn serial_test_rounded_with_holes() {
return sg
}
fn tarc = (to, sketchGroup, tag?) => {
return tangentialArcTo(to, sketchGroup, tag)
}
fn roundedRectangle = (pos, w, l, cornerRadius) => {
const rr = startSketchOn('XY')
|> startProfileAt([pos[0] - w/2, 0], %)
|> lineTo([pos[0] - w/2, pos[1] - l/2 + cornerRadius], %)
|> tangentialArcTo([pos[0] - w/2 + cornerRadius, pos[1] - l/2], %)
|> tarc([pos[0] - w/2 + cornerRadius, pos[1] - l/2], %, "arc0")
|> lineTo([pos[0] + w/2 - cornerRadius, pos[1] - l/2], %)
|> tangentialArcTo([pos[0] + w/2, pos[1] - l/2 + cornerRadius], %)
|> tarc([pos[0] + w/2, pos[1] - l/2 + cornerRadius], %)
|> lineTo([pos[0] + w/2, pos[1] + l/2 - cornerRadius], %)
|> tangentialArcTo([pos[0] + w/2 - cornerRadius, pos[1] + l/2], %)
|> tarc([pos[0] + w/2 - cornerRadius, pos[1] + l/2], %, "arc2")
|> lineTo([pos[0] - w/2 + cornerRadius, pos[1] + l/2], %)
|> tangentialArcTo([pos[0] - w/2, pos[1] + l/2 - cornerRadius], %)
|> tarc([pos[0] - w/2, pos[1] + l/2 - cornerRadius], %)
|> close(%)
return rr
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB