Merge branch 'main' into achalmers/grid-scale

This commit is contained in:
Adam Chalmers
2025-06-05 15:09:33 -05:00
484 changed files with 78683 additions and 158859 deletions

View File

@ -2,48 +2,11 @@ use std::{collections::HashMap, fs, path::Path};
use anyhow::Result;
use base64::Engine;
use convert_case::Casing;
use indexmap::IndexMap;
use itertools::Itertools;
use serde_json::json;
use tokio::task::JoinSet;
use super::kcl_doc::{ConstData, DocData, ExampleProperties, FnData, ModData, TyData};
use crate::{
docs::{StdLibFn, DECLARED_TYPES},
std::StdLib,
ExecutorContext,
};
// Types with special handling.
const SPECIAL_TYPES: [&str; 4] = ["TagDeclarator", "TagIdentifier", "Start", "End"];
const TYPE_REWRITES: [(&str, &str); 11] = [
("TagNode", "TagDeclarator"),
("SketchData", "Plane | Solid"),
("SketchOrSurface", "Sketch | Plane | Face"),
("SketchSurface", "Plane | Face"),
("SolidOrImportedGeometry", "[Solid] | ImportedGeometry"),
(
"SolidOrSketchOrImportedGeometry",
"[Solid] | [Sketch] | ImportedGeometry",
),
("KclValue", "any"),
("[KclValue]", "[any]"),
("FaceTag", "TagIdentifier | Start | End"),
("GeometryWithImportedGeometry", "Solid | Sketch | ImportedGeometry"),
("SweepPath", "Sketch | Helix"),
];
fn rename_type(input: &str) -> &str {
for (i, o) in TYPE_REWRITES {
if input == i {
return o;
}
}
input
}
use crate::ExecutorContext;
fn init_handlebars() -> Result<handlebars::Handlebars<'static>> {
let mut hbs = handlebars::Handlebars::new();
@ -104,7 +67,7 @@ fn init_handlebars() -> Result<handlebars::Handlebars<'static>> {
Ok(hbs)
}
fn generate_index(combined: &IndexMap<String, Box<dyn StdLibFn>>, kcl_lib: &ModData) -> Result<()> {
fn generate_index(kcl_lib: &ModData) -> Result<()> {
let hbs = init_handlebars()?;
let mut functions = HashMap::new();
@ -115,31 +78,6 @@ fn generate_index(combined: &IndexMap<String, Box<dyn StdLibFn>>, kcl_lib: &ModD
let mut types = HashMap::new();
types.insert("Primitive types".to_owned(), Vec::new());
for key in combined.keys() {
let internal_fn = combined
.get(key)
.ok_or_else(|| anyhow::anyhow!("Failed to get internal function: {}", key))?;
if internal_fn.unpublished() || internal_fn.deprecated() {
continue;
}
let tags = internal_fn.tags();
let module = tags.first().map(|s| format!("std::{s}")).unwrap_or("std".to_owned());
functions
.entry(module.to_owned())
.or_default()
.push((internal_fn.name(), format!("/docs/kcl-std/{}", internal_fn.name())));
}
for name in SPECIAL_TYPES {
types
.get_mut("Primitive types")
.unwrap()
.push((name.to_owned(), format!("/docs/kcl-lang/types#{name}")));
}
for d in kcl_lib.all_docs() {
if d.hide() {
continue;
@ -257,8 +195,8 @@ fn generate_example(index: usize, src: &str, props: &ExampleProperties, file_nam
}))
}
fn generate_type_from_kcl(ty: &TyData, file_name: String, example_name: String) -> Result<()> {
if ty.properties.doc_hidden || !DECLARED_TYPES.contains(&&*ty.name) {
fn generate_type_from_kcl(ty: &TyData, file_name: String, example_name: String, kcl_std: &ModData) -> Result<()> {
if ty.properties.doc_hidden {
return Ok(());
}
@ -282,18 +220,14 @@ fn generate_type_from_kcl(ty: &TyData, file_name: String, example_name: String)
});
let output = hbs.render("kclType", &data)?;
let output = cleanup_types(&output);
let output = cleanup_types(&output, kcl_std);
expectorate::assert_contents(format!("../../docs/kcl-std/{}.md", file_name), &output);
Ok(())
}
fn generate_mod_from_kcl(m: &ModData, file_name: String, combined: &IndexMap<String, Box<dyn StdLibFn>>) -> Result<()> {
fn list_items(
m: &ModData,
namespace: &str,
combined: &IndexMap<String, Box<dyn StdLibFn>>,
) -> Vec<gltf_json::Value> {
fn generate_mod_from_kcl(m: &ModData, file_name: String) -> Result<()> {
fn list_items(m: &ModData, namespace: &str) -> Vec<gltf_json::Value> {
let mut items: Vec<_> = m
.children
.iter()
@ -301,25 +235,6 @@ fn generate_mod_from_kcl(m: &ModData, file_name: String, combined: &IndexMap<Str
.map(|(_, v)| (v.preferred_name().to_owned(), v.file_name()))
.collect();
if namespace == "I:" {
// Add in functions declared in Rust
items.extend(
combined
.values()
.filter(|f| {
if f.unpublished() || f.deprecated() {
return false;
}
let tags = f.tags();
let module = tags.first().map(|s| format!("std::{s}")).unwrap_or("std".to_owned());
module == m.qual_name
})
.map(|f| (f.name(), f.name())),
)
}
items.sort();
items
.into_iter()
@ -333,9 +248,9 @@ fn generate_mod_from_kcl(m: &ModData, file_name: String, combined: &IndexMap<Str
}
let hbs = init_handlebars()?;
let functions = list_items(m, "I:", combined);
let modules = list_items(m, "M:", combined);
let types = list_items(m, "T:", combined);
let functions = list_items(m, "I:");
let modules = list_items(m, "M:");
let types = list_items(m, "T:");
let data = json!({
"name": m.name,
@ -391,7 +306,7 @@ fn generate_function_from_kcl(
json!({
"name": arg.name,
"type_": arg.ty,
"description": docs.or_else(|| arg.ty.as_ref().and_then(|t| super::docs_for_type(t, kcl_std))).unwrap_or_default(),
"description": docs.or_else(|| arg.ty.as_ref().and_then(|t| docs_for_type(t, kcl_std))).unwrap_or_default(),
"required": arg.kind.required(),
})
}).collect::<Vec<_>>();
@ -408,18 +323,30 @@ fn generate_function_from_kcl(
"return_value": function.return_type.as_ref().map(|t| {
json!({
"type_": t,
"description": super::docs_for_type(t, kcl_std).unwrap_or_default(),
"description": docs_for_type(t, kcl_std).unwrap_or_default(),
})
}),
});
let output = hbs.render("function", &data)?;
let output = &cleanup_types(&output);
let output = &cleanup_types(&output, kcl_std);
expectorate::assert_contents(format!("../../docs/kcl-std/{}.md", file_name), output);
Ok(())
}
fn docs_for_type(ty: &str, kcl_std: &ModData) -> Option<String> {
let key = if ty.starts_with("number") { "number" } else { ty };
if !key.contains('|') && !key.contains('[') {
if let Some(data) = kcl_std.find_by_name(key) {
return data.summary().cloned();
}
}
None
}
fn generate_const_from_kcl(cnst: &ConstData, file_name: String, example_name: String) -> Result<()> {
if cnst.properties.doc_hidden {
return Ok(());
@ -450,83 +377,7 @@ fn generate_const_from_kcl(cnst: &ConstData, file_name: String, example_name: St
Ok(())
}
fn generate_function(internal_fn: Box<dyn StdLibFn>, kcl_std: &ModData) -> Result<()> {
let hbs = init_handlebars()?;
if internal_fn.unpublished() {
return Ok(());
}
let fn_name = internal_fn.name();
let snake_case_name = clean_function_name(&fn_name);
let examples: Vec<serde_json::Value> = internal_fn
.examples()
.iter()
.enumerate()
.map(|(index, (example, norun))| {
let image_base64 = if !norun {
let image_path = format!(
"{}/tests/outputs/serial_test_example_{}{}.png",
env!("CARGO_MANIFEST_DIR"),
snake_case_name,
index
);
let image_data =
std::fs::read(&image_path).unwrap_or_else(|_| panic!("Failed to read image file: {}", image_path));
base64::engine::general_purpose::STANDARD.encode(&image_data)
} else {
String::new()
};
json!({
"content": example,
"image_base64": image_base64,
})
})
.collect();
let tags = internal_fn.tags();
let module = tags
.first()
.map(|s| &**s)
.map(|m| format!("std::{m}"))
.unwrap_or("std".to_owned());
let data = json!({
"name": fn_name,
"module": module,
"summary": internal_fn.summary(),
"description": internal_fn.description(),
"deprecated": internal_fn.deprecated(),
"fn_signature": internal_fn.fn_signature(true),
"examples": examples,
"args": internal_fn.args(false).iter().map(|arg| {
json!({
"name": arg.name,
"type_": rename_type(&arg.type_),
"description": arg.description(Some(kcl_std)),
"required": arg.required,
})
}).collect::<Vec<_>>(),
"return_value": internal_fn.return_value(false).map(|ret| {
json!({
"type_": rename_type(&ret.type_),
"description": ret.description(Some(kcl_std)),
})
}),
});
let mut output = hbs.render("function", &data)?;
// Fix the links to the types.
output = cleanup_types(&output);
expectorate::assert_contents(format!("../../docs/kcl-std/{}.md", fn_name), &output);
Ok(())
}
fn cleanup_types(input: &str) -> String {
fn cleanup_types(input: &str, kcl_std: &ModData) -> String {
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum State {
Text,
@ -550,7 +401,7 @@ fn cleanup_types(input: &str) -> String {
if code_type.starts_with(' ') {
code.push(' ');
}
code.push_str(&cleanup_type_string(code_type.trim(), false));
code.push_str(&cleanup_type_string(code_type.trim(), false, kcl_std));
if code_type.ends_with(' ') {
code.push(' ');
}
@ -586,7 +437,7 @@ fn cleanup_types(input: &str) -> String {
}
ticks = 0;
} else if state == State::Text && ticks == 2 && !code.is_empty() {
output.push_str(&cleanup_type_string(&code, true));
output.push_str(&cleanup_type_string(&code, true, kcl_std));
code = String::new();
ticks = 0;
} else if state == State::CodeBlock {
@ -631,14 +482,12 @@ fn cleanup_types(input: &str) -> String {
output
}
fn cleanup_type_string(input: &str, fmt_for_text: bool) -> String {
fn cleanup_type_string(input: &str, fmt_for_text: bool, kcl_std: &ModData) -> String {
assert!(
!(input.starts_with('[') && input.ends_with(']') && input.contains('|')),
"Arrays of unions are not supported"
);
let input = rename_type(input);
let tys: Vec<_> = input
.split('|')
.map(|ty| {
@ -676,9 +525,7 @@ fn cleanup_type_string(input: &str, fmt_for_text: bool) -> String {
format!("[{prefix}{ty}{suffix}](/docs/kcl-std/types/std-types-number)")
} else if fmt_for_text && ty.starts_with("fn") {
format!("[{prefix}{ty}{suffix}](/docs/kcl-std/types/std-types-fn)")
} else if fmt_for_text && SPECIAL_TYPES.contains(&ty) {
format!("[{prefix}{ty}{suffix}](/docs/kcl-lang/types#{ty})")
} else if fmt_for_text && DECLARED_TYPES.contains(&ty) {
} else if fmt_for_text && matches!(kcl_std.find_by_name(ty), Some(DocData::Ty(_))) {
format!("[{prefix}{ty}{suffix}](/docs/kcl-std/types/std-types-{ty})")
} else {
format!("{prefix}{ty}{suffix}")
@ -689,73 +536,22 @@ fn cleanup_type_string(input: &str, fmt_for_text: bool) -> String {
tys.join(if fmt_for_text { " or " } else { " | " })
}
fn clean_function_name(name: &str) -> String {
// Convert from camel case to snake case.
let mut fn_name = name.to_case(convert_case::Case::Snake);
// Clean the fn name.
if fn_name.starts_with("last_seg_") {
fn_name = fn_name.replace("last_seg_", "last_segment_");
} else if fn_name.contains("_2_d") {
fn_name = fn_name.replace("_2_d", "_2d");
} else if fn_name.contains("_3_d") {
fn_name = fn_name.replace("_3_d", "_3d");
} else if fn_name == "seg_ang" {
fn_name = "segment_angle".to_string();
} else if fn_name == "seg_len" {
fn_name = "segment_length".to_string();
} else if fn_name.starts_with("seg_") {
fn_name = fn_name.replace("seg_", "segment_");
}
fn_name
}
#[test]
fn test_generate_stdlib_markdown_docs() {
let stdlib = StdLib::new();
let combined = stdlib.combined();
let kcl_std = crate::docs::kcl_doc::walk_prelude();
// Generate the index which is the table of contents.
generate_index(&combined, &kcl_std).unwrap();
for key in combined.keys().sorted() {
let internal_fn = combined.get(key).unwrap();
generate_function(internal_fn.clone(), &kcl_std).unwrap();
}
generate_index(&kcl_std).unwrap();
for d in kcl_std.all_docs() {
match d {
DocData::Fn(f) => generate_function_from_kcl(f, d.file_name(), d.example_name(), &kcl_std).unwrap(),
DocData::Const(c) => generate_const_from_kcl(c, d.file_name(), d.example_name()).unwrap(),
DocData::Ty(t) => generate_type_from_kcl(t, d.file_name(), d.example_name()).unwrap(),
DocData::Mod(m) => generate_mod_from_kcl(m, d.file_name(), &combined).unwrap(),
DocData::Ty(t) => generate_type_from_kcl(t, d.file_name(), d.example_name(), &kcl_std).unwrap(),
DocData::Mod(m) => generate_mod_from_kcl(m, d.file_name()).unwrap(),
}
}
generate_mod_from_kcl(&kcl_std, "modules/std".to_owned(), &combined).unwrap();
}
#[test]
fn test_generate_stdlib_json_schema() {
// If this test fails and you've modified the AST or something else which affects the json repr
// of stdlib functions, you should rerun the test with `EXPECTORATE=overwrite` to create new
// test data, then check `/docs/kcl-std/std.json` to ensure the changes are expected.
// Alternatively, run `just redo-kcl-stdlib-docs` (make sure to have just installed).
let stdlib = StdLib::new();
let combined = stdlib.combined();
let json_data: Vec<_> = combined
.keys()
.sorted()
.map(|key| {
let internal_fn = combined.get(key).unwrap();
internal_fn.to_json().unwrap()
})
.collect();
expectorate::assert_contents(
"../../docs/kcl-std/std.json",
&serde_json::to_string_pretty(&json_data).unwrap(),
);
generate_mod_from_kcl(&kcl_std, "modules/std".to_owned()).unwrap();
}
#[tokio::test(flavor = "multi_thread")]

View File

@ -302,6 +302,7 @@ impl DocData {
}
}
#[allow(dead_code)]
pub(super) fn summary(&self) -> Option<&String> {
match self {
DocData::Fn(f) => f.summary.as_ref(),
@ -462,6 +463,7 @@ impl ModData {
}
}
#[allow(dead_code)]
pub fn find_by_name(&self, name: &str) -> Option<&DocData> {
if let Some(result) = self
.children
@ -812,6 +814,7 @@ impl ArgData {
return Some((index + n - 1, snippet));
}
match self.ty.as_deref() {
Some("Sketch") if self.kind == ArgKind::Special => None,
Some(s) if s.starts_with("number") => Some((index, format!(r#"{label}${{{}:10}}"#, index))),
Some("Point2d") => Some((index + 1, format!(r#"{label}[${{{}:0}}, ${{{}:0}}]"#, index, index + 1))),
Some("Point3d") => Some((
@ -827,7 +830,7 @@ impl ArgData {
Some("Sketch") | Some("Sketch | Helix") => Some((index, format!(r#"{label}${{{index}:sketch000}}"#))),
Some("Edge") => Some((index, format!(r#"{label}${{{index}:tag_or_edge_fn}}"#))),
Some("[Edge; 1+]") => Some((index, format!(r#"{label}[${{{index}:tag_or_edge_fn}}]"#))),
Some("Plane") => Some((index, format!(r#"{label}${{{}:XY}}"#, index))),
Some("Plane") | Some("Solid | Plane") => Some((index, format!(r#"{label}${{{}:XY}}"#, index))),
Some("[tag; 2]") => Some((
index + 1,
format!(r#"{label}[${{{}:tag}}, ${{{}:tag}}]"#, index, index + 1),
@ -989,7 +992,7 @@ trait ApplyMeta {
}
let mut summary = None;
let mut description = None;
let mut description: Option<String> = None;
let mut example: Option<(String, ExampleProperties)> = None;
let mut examples = Vec::new();
for l in comments.iter().filter(|l| l.starts_with("///")).map(|l| {
@ -999,22 +1002,6 @@ trait ApplyMeta {
&l[3..]
}
}) {
if description.is_none() && summary.is_none() {
summary = Some(l.to_owned());
continue;
}
if description.is_none() {
if l.is_empty() {
description = Some(String::new());
} else {
description = summary;
summary = None;
let d = description.as_mut().unwrap();
d.push('\n');
d.push_str(l);
}
continue;
}
#[allow(clippy::manual_strip)]
if l.starts_with("```") {
if let Some((e, p)) = example {
@ -1050,12 +1037,36 @@ trait ApplyMeta {
continue;
}
}
// An empty line outside of an example. This either starts the description (with or
// without a summary) or adds a blank line to the description.
if l.is_empty() {
match &mut description {
Some(d) => {
d.push('\n');
}
None => description = Some(String::new()),
}
continue;
}
// Our first line, start the summary.
if description.is_none() && summary.is_none() {
summary = Some(l.to_owned());
continue;
}
// Append the line to either the description or summary.
match &mut description {
Some(d) => {
d.push_str(l);
d.push('\n');
}
None => unreachable!(),
None => {
let s = summary.as_mut().unwrap();
s.push(' ');
s.push_str(l);
}
}
}
assert!(example.is_none());

File diff suppressed because it is too large Load Diff

View File

@ -67,6 +67,7 @@ pub struct TcpRead {
/// Occurs when client couldn't read from the WebSocket to the engine.
// #[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum WebSocketReadError {
/// Could not read a message due to WebSocket errors.
Read(tokio_tungstenite::tungstenite::Error),
@ -206,7 +207,7 @@ impl EngineConnection {
async fn inner_send_to_engine(request: WebSocketRequest, tcp_write: &mut WebSocketTcpWrite) -> Result<()> {
let msg = serde_json::to_string(&request).map_err(|e| anyhow!("could not serialize json: {e}"))?;
tcp_write
.send(WsMsg::Text(msg))
.send(WsMsg::Text(msg.into()))
.await
.map_err(|e| anyhow!("could not send json over websocket: {e}"))?;
Ok(())
@ -216,19 +217,17 @@ impl EngineConnection {
async fn inner_send_to_engine_binary(request: WebSocketRequest, tcp_write: &mut WebSocketTcpWrite) -> Result<()> {
let msg = bson::to_vec(&request).map_err(|e| anyhow!("could not serialize bson: {e}"))?;
tcp_write
.send(WsMsg::Binary(msg))
.send(WsMsg::Binary(msg.into()))
.await
.map_err(|e| anyhow!("could not send json over websocket: {e}"))?;
Ok(())
}
pub async fn new(ws: reqwest::Upgraded) -> Result<EngineConnection> {
let wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig {
let wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
// 4294967296 bytes, which is around 4.2 GB.
max_message_size: Some(usize::MAX),
max_frame_size: Some(usize::MAX),
..Default::default()
};
.max_message_size(Some(usize::MAX))
.max_frame_size(Some(usize::MAX));
let ws_stream = tokio_tungstenite::WebSocketStream::from_raw_socket(
ws,

View File

@ -80,12 +80,12 @@ impl ResponseContext {
}
// Add a response to the context.
pub async fn send_response(&self, data: js_sys::Uint8Array) -> Result<(), JsValue> {
pub async fn send_response(&self, data: js_sys::Uint8Array) {
let ws_result: WebSocketResponse = match bson::from_slice(&data.to_vec()) {
Ok(res) => res,
Err(_) => {
// We don't care about the error if we can't parse it.
return Ok(());
return;
}
};
@ -96,13 +96,11 @@ impl ResponseContext {
let Some(id) = id else {
// We only care if we have an id.
return Ok(());
return;
};
// Add this response to our responses.
self.add(id, ws_result.clone()).await;
Ok(())
}
}

View File

@ -108,7 +108,10 @@ pub enum KclError {
#[error("value already defined: {details:?}")]
ValueAlreadyDefined { details: KclErrorDetails },
#[error("undefined value: {details:?}")]
UndefinedValue { details: KclErrorDetails },
UndefinedValue {
details: KclErrorDetails,
name: Option<String>,
},
#[error("invalid expression: {details:?}")]
InvalidExpression { details: KclErrorDetails },
#[error("engine: {details:?}")]
@ -451,8 +454,8 @@ impl KclError {
KclError::Lexical { details }
}
pub fn new_undefined_value(details: KclErrorDetails) -> KclError {
KclError::UndefinedValue { details }
pub fn new_undefined_value(details: KclErrorDetails, name: Option<String>) -> KclError {
KclError::UndefinedValue { details, name }
}
pub fn new_type(details: KclErrorDetails) -> KclError {
@ -491,7 +494,7 @@ impl KclError {
KclError::Io { details: e } => e.source_ranges.clone(),
KclError::Unexpected { details: e } => e.source_ranges.clone(),
KclError::ValueAlreadyDefined { details: e } => e.source_ranges.clone(),
KclError::UndefinedValue { details: e } => e.source_ranges.clone(),
KclError::UndefinedValue { details: e, .. } => e.source_ranges.clone(),
KclError::InvalidExpression { details: e } => e.source_ranges.clone(),
KclError::Engine { details: e } => e.source_ranges.clone(),
KclError::Internal { details: e } => e.source_ranges.clone(),
@ -509,7 +512,7 @@ impl KclError {
KclError::Io { details: e } => &e.message,
KclError::Unexpected { details: e } => &e.message,
KclError::ValueAlreadyDefined { details: e } => &e.message,
KclError::UndefinedValue { details: e } => &e.message,
KclError::UndefinedValue { details: e, .. } => &e.message,
KclError::InvalidExpression { details: e } => &e.message,
KclError::Engine { details: e } => &e.message,
KclError::Internal { details: e } => &e.message,
@ -526,7 +529,7 @@ impl KclError {
| KclError::Io { details: e }
| KclError::Unexpected { details: e }
| KclError::ValueAlreadyDefined { details: e }
| KclError::UndefinedValue { details: e }
| KclError::UndefinedValue { details: e, .. }
| KclError::InvalidExpression { details: e }
| KclError::Engine { details: e }
| KclError::Internal { details: e } => e.backtrace.clone(),
@ -544,7 +547,7 @@ impl KclError {
| KclError::Io { details: e }
| KclError::Unexpected { details: e }
| KclError::ValueAlreadyDefined { details: e }
| KclError::UndefinedValue { details: e }
| KclError::UndefinedValue { details: e, .. }
| KclError::InvalidExpression { details: e }
| KclError::Engine { details: e }
| KclError::Internal { details: e } => {
@ -562,30 +565,6 @@ impl KclError {
new
}
pub(crate) fn set_last_backtrace_fn_name(&self, last_fn_name: Option<String>) -> Self {
let mut new = self.clone();
match &mut new {
KclError::Lexical { details: e }
| KclError::Syntax { details: e }
| KclError::Semantic { details: e }
| KclError::ImportCycle { details: e }
| KclError::Type { details: e }
| KclError::Io { details: e }
| KclError::Unexpected { details: e }
| KclError::ValueAlreadyDefined { details: e }
| KclError::UndefinedValue { details: e }
| KclError::InvalidExpression { details: e }
| KclError::Engine { details: e }
| KclError::Internal { details: e } => {
if let Some(item) = e.backtrace.last_mut() {
item.fn_name = last_fn_name;
}
}
}
new
}
pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
let mut new = self.clone();
match &mut new {
@ -597,7 +576,7 @@ impl KclError {
| KclError::Io { details: e }
| KclError::Unexpected { details: e }
| KclError::ValueAlreadyDefined { details: e }
| KclError::UndefinedValue { details: e }
| KclError::UndefinedValue { details: e, .. }
| KclError::InvalidExpression { details: e }
| KclError::Engine { details: e }
| KclError::Internal { details: e } => {

View File

@ -676,6 +676,7 @@ impl EdgeCut {
#[serde(rename_all = "camelCase")]
pub struct ArtifactGraph {
map: IndexMap<ArtifactId, Artifact>,
pub(super) item_count: usize,
}
impl ArtifactGraph {
@ -711,10 +712,10 @@ pub(super) fn build_artifact_graph(
artifact_commands: &[ArtifactCommand],
responses: &IndexMap<Uuid, WebSocketResponse>,
ast: &Node<Program>,
cached_body_items: usize,
exec_artifacts: &mut IndexMap<ArtifactId, Artifact>,
initial_graph: ArtifactGraph,
) -> Result<ArtifactGraph, KclError> {
let item_count = initial_graph.item_count;
let mut map = initial_graph.into_map();
let mut path_to_plane_id_map = FnvHashMap::default();
@ -725,7 +726,7 @@ pub(super) fn build_artifact_graph(
for exec_artifact in exec_artifacts.values_mut() {
// Note: We only have access to the new AST. So if these artifacts
// somehow came from cached AST, this won't fill in anything.
fill_in_node_paths(exec_artifact, ast, cached_body_items);
fill_in_node_paths(exec_artifact, ast, item_count);
}
for artifact_command in artifact_commands {
@ -752,7 +753,7 @@ pub(super) fn build_artifact_graph(
&flattened_responses,
&path_to_plane_id_map,
ast,
cached_body_items,
item_count,
exec_artifacts,
)?;
for artifact in artifact_updates {
@ -765,7 +766,10 @@ pub(super) fn build_artifact_graph(
merge_artifact_into_map(&mut map, exec_artifact.clone());
}
Ok(ArtifactGraph { map })
Ok(ArtifactGraph {
map,
item_count: item_count + ast.body.len(),
})
}
/// These may have been created with placeholder `CodeRef`s because we didn't

View File

@ -6,25 +6,31 @@ use itertools::{EitherOrBoth, Itertools};
use tokio::sync::RwLock;
use crate::{
execution::{annotations, memory::Stack, state::ModuleInfoMap, EnvironmentRef, ExecState, ExecutorSettings},
execution::{
annotations,
memory::Stack,
state::{self as exec_state, ModuleInfoMap},
EnvironmentRef, ExecutorSettings,
},
parsing::ast::types::{Annotation, Node, Program},
walk::Node as WalkNode,
ExecOutcome, ExecutorContext,
};
lazy_static::lazy_static! {
/// A static mutable lock for updating the last successful execution state for the cache.
static ref OLD_AST: Arc<RwLock<Option<OldAstState>>> = Default::default();
static ref OLD_AST: Arc<RwLock<Option<GlobalState>>> = Default::default();
// The last successful run's memory. Not cleared after an unssuccessful run.
static ref PREV_MEMORY: Arc<RwLock<Option<(Stack, ModuleInfoMap)>>> = Default::default();
}
/// Read the old ast memory from the lock.
pub(crate) async fn read_old_ast() -> Option<OldAstState> {
pub(super) async fn read_old_ast() -> Option<GlobalState> {
let old_ast = OLD_AST.read().await;
old_ast.clone()
}
pub(super) async fn write_old_ast(old_state: OldAstState) {
pub(super) async fn write_old_ast(old_state: GlobalState) {
let mut old_ast = OLD_AST.write().await;
*old_ast = Some(old_state);
}
@ -34,7 +40,7 @@ pub(crate) async fn read_old_memory() -> Option<(Stack, ModuleInfoMap)> {
old_mem.clone()
}
pub(super) async fn write_old_memory(mem: (Stack, ModuleInfoMap)) {
pub(crate) async fn write_old_memory(mem: (Stack, ModuleInfoMap)) {
let mut old_mem = PREV_MEMORY.write().await;
*old_mem = Some(mem);
}
@ -56,16 +62,73 @@ pub struct CacheInformation<'a> {
pub settings: &'a ExecutorSettings,
}
/// The old ast and program memory.
/// The cached state of the whole program.
#[derive(Debug, Clone)]
pub struct OldAstState {
/// The ast.
pub ast: Node<Program>,
pub(super) struct GlobalState {
pub(super) main: ModuleState,
/// The exec state.
pub exec_state: ExecState,
pub(super) exec_state: exec_state::GlobalState,
/// The last settings used for execution.
pub settings: crate::execution::ExecutorSettings,
pub result_env: EnvironmentRef,
pub(super) settings: ExecutorSettings,
}
impl GlobalState {
pub fn new(
state: exec_state::ExecState,
settings: ExecutorSettings,
ast: Node<Program>,
result_env: EnvironmentRef,
) -> Self {
Self {
main: ModuleState {
ast,
exec_state: state.mod_local,
result_env,
},
exec_state: state.global,
settings,
}
}
pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
self.settings = settings;
self
}
pub fn reconstitute_exec_state(&self) -> exec_state::ExecState {
exec_state::ExecState {
global: self.exec_state.clone(),
mod_local: self.main.exec_state.clone(),
}
}
pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState.
ExecOutcome {
variables: self.main.exec_state.variables(self.main.result_env),
filenames: self.exec_state.filenames(),
#[cfg(feature = "artifact-graph")]
operations: self.exec_state.artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_commands: self.exec_state.artifacts.commands,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.exec_state.artifacts.graph,
errors: self.exec_state.errors,
default_planes: ctx.engine.get_default_planes().read().await.clone(),
}
}
}
/// Per-module cached state
#[derive(Debug, Clone)]
pub(super) struct ModuleState {
/// The AST of the module.
pub(super) ast: Node<Program>,
/// The ExecState of the module.
pub(super) exec_state: exec_state::ModuleState,
/// The memory env for the module.
pub(super) result_env: EnvironmentRef,
}
/// The result of a cache check.
@ -79,9 +142,6 @@ pub(super) enum CacheResult {
reapply_settings: bool,
/// The program that needs to be executed.
program: Node<Program>,
/// The number of body items that were cached and omitted from the
/// program that needs to be executed. Used to compute [`crate::NodePath`].
cached_body_items: usize,
},
/// Check only the imports, and not the main program.
/// Before sending this we already checked the main program and it is the same.
@ -146,7 +206,6 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf
// We know they have the same imports because the ast is the same.
// If we have no imports, we can skip this.
if !old.ast.has_import_statements() {
println!("No imports, no need to check.");
return CacheResult::NoAction(reapply_settings);
}
@ -194,7 +253,6 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf
clear_scene: true,
reapply_settings: true,
program: new.ast.clone(),
cached_body_items: 0,
};
}
@ -223,7 +281,6 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
clear_scene: true,
reapply_settings,
program: new_ast,
cached_body_items: 0,
};
}
@ -244,7 +301,6 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
clear_scene: true,
reapply_settings,
program: new_ast,
cached_body_items: 0,
}
}
std::cmp::Ordering::Greater => {
@ -261,7 +317,6 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
clear_scene: false,
reapply_settings,
program: new_ast,
cached_body_items: old_ast.body.len(),
}
}
std::cmp::Ordering::Equal => {
@ -600,7 +655,6 @@ startSketchOn(XY)
clear_scene: true,
reapply_settings: true,
program: new_program.ast,
cached_body_items: 0,
}
);
}
@ -639,7 +693,6 @@ startSketchOn(XY)
clear_scene: true,
reapply_settings: true,
program: new_program.ast,
cached_body_items: 0,
}
);
}

View File

@ -1,13 +1,12 @@
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::Serialize;
use super::{types::NumericType, ArtifactId, KclValue};
use crate::{ModuleId, SourceRange};
use crate::{ModuleId, NodePath, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations
/// timeline.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(tag = "type")]
pub enum Operation {
@ -18,6 +17,8 @@ pub enum Operation {
unlabeled_arg: Option<OpArg>,
/// The labeled keyword arguments to the function.
labeled_args: IndexMap<String, OpArg>,
/// The node path of the operation in the source code.
node_path: NodePath,
/// The source range of the operation in the source code.
source_range: SourceRange,
/// True if the operation resulted in an error.
@ -28,6 +29,8 @@ pub enum Operation {
GroupBegin {
/// The details of the group.
group: Group,
/// The node path of the operation in the source code.
node_path: NodePath,
/// The source range of the operation in the source code.
source_range: SourceRange,
},
@ -64,7 +67,7 @@ impl Operation {
}
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(tag = "type")]
#[expect(clippy::large_enum_variant)]
@ -95,7 +98,7 @@ pub enum Group {
}
/// An argument to a CAD modeling operation.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpArg {
@ -119,7 +122,7 @@ fn is_false(b: &bool) -> bool {
/// A KCL value used in Operations. `ArtifactId`s are used to refer to the
/// actual scene objects. Any data not needed in the UI may be omitted.
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(tag = "type")]
pub enum OpKclValue {
@ -177,21 +180,21 @@ pub enum OpKclValue {
pub type OpKclObjectFields = IndexMap<String, OpKclValue>;
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpSketch {
artifact_id: ArtifactId,
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpSolid {
artifact_id: ArtifactId,
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpHelix {

View File

@ -19,8 +19,8 @@ use crate::{
parsing::ast::types::{
Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryOperator,
BinaryPart, BodyItem, Expr, IfExpression, ImportPath, ImportSelector, ItemVisibility, LiteralIdentifier,
LiteralValue, MemberExpression, MemberObject, Name, Node, NodeRef, ObjectExpression, PipeExpression, Program,
TagDeclarator, Type, UnaryExpression, UnaryOperator,
LiteralValue, MemberExpression, Name, Node, NodeRef, ObjectExpression, PipeExpression, Program, TagDeclarator,
Type, UnaryExpression, UnaryOperator,
},
source_range::SourceRange,
std::args::TyF64,
@ -164,10 +164,13 @@ impl ExecutorContext {
let mut mod_value = mem.get_from(&mod_name, env_ref, import_item.into(), 0).cloned();
if value.is_err() && ty.is_err() && mod_value.is_err() {
return Err(KclError::new_undefined_value(KclErrorDetails::new(
format!("{} is not defined in module", import_item.name.name),
vec![SourceRange::from(&import_item.name)],
)));
return Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("{} is not defined in module", import_item.name.name),
vec![SourceRange::from(&import_item.name)],
),
None,
));
}
// Check that the item is allowed to be imported (in at least one namespace).
@ -301,7 +304,12 @@ impl ExecutorContext {
let annotations = &variable_declaration.outer_attrs;
let value = self
// During the evaluation of the variable's RHS, set context that this is all happening inside a variable
// declaration, for the given name. This helps improve user-facing error messages.
let lhs = variable_declaration.inner.name().to_owned();
let prev_being_declared = exec_state.mod_local.being_declared.take();
exec_state.mod_local.being_declared = Some(lhs);
let rhs_result = self
.execute_expr(
&variable_declaration.declaration.init,
exec_state,
@ -309,10 +317,14 @@ impl ExecutorContext {
annotations,
StatementKind::Declaration { name: &var_name },
)
.await?;
.await;
// Declaration over, so unset this context.
exec_state.mod_local.being_declared = prev_being_declared;
let rhs = rhs_result?;
exec_state
.mut_stack()
.add(var_name.clone(), value.clone(), source_range)?;
.add(var_name.clone(), rhs.clone(), source_range)?;
// Track exports.
if let ItemVisibility::Export = variable_declaration.visibility {
@ -326,7 +338,7 @@ impl ExecutorContext {
}
}
// Variable declaration can be the return value of a module.
last_expr = matches!(body_type, BodyType::Root).then_some(value);
last_expr = matches!(body_type, BodyType::Root).then_some(rhs);
}
BodyItem::TypeDeclaration(ty) => {
let metadata = Metadata::from(&**ty);
@ -635,7 +647,12 @@ impl ExecutorContext {
Expr::Literal(literal) => KclValue::from_literal((**literal).clone(), exec_state),
Expr::TagDeclarator(tag) => tag.execute(exec_state).await?,
Expr::Name(name) => {
let value = name.get_result(exec_state, self).await?.clone();
let being_declared = exec_state.mod_local.being_declared.clone();
let value = name
.get_result(exec_state, self)
.await
.map_err(|e| var_in_own_ref_err(e, &being_declared))?
.clone();
if let KclValue::Module { value: module_id, meta } = value {
self.exec_module_for_result(
module_id,
@ -722,7 +739,7 @@ impl ExecutorContext {
Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?,
Expr::ArrayRangeExpression(range_expression) => range_expression.execute(exec_state, self).await?,
Expr::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?,
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state, self).await?,
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
Expr::LabelledExpression(expr) => {
@ -741,6 +758,24 @@ impl ExecutorContext {
}
}
/// If the error is about an undefined name, and that name matches the name being defined,
/// make the error message more specific.
fn var_in_own_ref_err(e: KclError, being_declared: &Option<String>) -> KclError {
let KclError::UndefinedValue { name, mut details } = e else {
return e;
};
// TODO after June 26th: replace this with a let-chain,
// which will be available in Rust 1.88
// https://rust-lang.github.io/rfcs/2497-if-let-chains.html
match (&being_declared, &name) {
(Some(name0), Some(name1)) if name0 == name1 => {
details.message = format!("You can't use `{name0}` because you're currently trying to define it. Use a different variable here instead.");
}
_ => {}
}
KclError::UndefinedValue { details, name }
}
impl Node<AscribedExpression> {
#[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
@ -790,7 +825,7 @@ impl BinaryPart {
BinaryPart::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, ctx).await,
BinaryPart::CallExpressionKw(call_expression) => call_expression.execute(exec_state, ctx).await,
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, ctx).await,
BinaryPart::MemberExpression(member_expression) => member_expression.get_result(exec_state),
BinaryPart::MemberExpression(member_expression) => member_expression.get_result(exec_state, ctx).await,
BinaryPart::IfExpression(e) => e.get_result(exec_state, ctx).await,
BinaryPart::AscribedExpression(e) => e.get_result(exec_state, ctx).await,
}
@ -802,6 +837,17 @@ impl Node<Name> {
&self,
exec_state: &'a mut ExecState,
ctx: &ExecutorContext,
) -> Result<&'a KclValue, KclError> {
let being_declared = exec_state.mod_local.being_declared.clone();
self.get_result_inner(exec_state, ctx)
.await
.map_err(|e| var_in_own_ref_err(e, &being_declared))
}
async fn get_result_inner<'a>(
&self,
exec_state: &'a mut ExecState,
ctx: &ExecutorContext,
) -> Result<&'a KclValue, KclError> {
if self.abs_path {
return Err(KclError::new_semantic(KclErrorDetails::new(
@ -896,16 +942,14 @@ impl Node<Name> {
}
impl Node<MemberExpression> {
fn get_result(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let property = Property::try_from(self.computed, self.property.clone(), exec_state, self.into())?;
let object = match &self.object {
// TODO: Don't use recursion here, use a loop.
MemberObject::MemberExpression(member_expr) => member_expr.get_result(exec_state)?,
MemberObject::Identifier(identifier) => {
let value = exec_state.stack().get(&identifier.name, identifier.into())?;
value.clone()
}
let meta = Metadata {
source_range: SourceRange::from(self),
};
let object = ctx
.execute_expr(&self.object, exec_state, &meta, &[], StatementKind::Expression)
.await?;
// Check the property and object match -- e.g. ints for arrays, strs for objects.
match (object, property, self.computed) {
@ -913,10 +957,13 @@ impl Node<MemberExpression> {
if let Some(value) = map.get(&property) {
Ok(value.to_owned())
} else {
Err(KclError::new_undefined_value(KclErrorDetails::new(
format!("Property '{property}' not found in object"),
vec![self.clone().into()],
)))
Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in object"),
vec![self.clone().into()],
),
None,
))
}
}
(KclValue::Object { .. }, Property::String(property), true) => {
@ -938,10 +985,13 @@ impl Node<MemberExpression> {
if let Some(value) = value_of_arr {
Ok(value.to_owned())
} else {
Err(KclError::new_undefined_value(KclErrorDetails::new(
format!("The array doesn't have any item at index {index}"),
vec![self.clone().into()],
)))
Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("The array doesn't have any item at index {index}"),
vec![self.clone().into()],
),
None,
))
}
}
// Singletons and single-element arrays should be interchangeable, but only indexing by 0 should work.
@ -1311,7 +1361,7 @@ pub(crate) async fn execute_pipe_body(
// Now that we've evaluated the first child expression in the pipeline, following child expressions
// should use the previous child expression for %.
// This means there's no more need for the previous pipe_value from the parent AST node above this one.
let previous_pipe_value = std::mem::replace(&mut exec_state.mod_local.pipe_value, Some(output));
let previous_pipe_value = exec_state.mod_local.pipe_value.replace(output);
// Evaluate remaining elements.
let result = inner_execute_pipe_body(exec_state, body, ctx).await;
// Restore the previous pipe value.
@ -1861,7 +1911,6 @@ d = b + c
project_directory: Some(crate::TypedPath(tmpdir.path().into())),
..Default::default()
},
stdlib: Arc::new(crate::std::StdLib::new()),
context_type: ContextType::Mock,
};
let mut exec_state = ExecState::new(&exec_ctxt);

View File

@ -2,7 +2,6 @@ use async_recursion::async_recursion;
use indexmap::IndexMap;
use crate::{
docs::StdLibFn,
errors::{KclError, KclErrorDetails},
execution::{
cad_op::{Group, OpArg, OpKclValue, Operation},
@ -15,7 +14,7 @@ use crate::{
parsing::ast::types::{CallExpressionKw, DefaultParamVal, FunctionExpression, Node, Program, Type},
source_range::SourceRange,
std::StdFn,
CompilationError,
CompilationError, NodePath,
};
#[derive(Debug, Clone)]
@ -184,40 +183,6 @@ impl<'a> From<&'a FunctionSource> for FunctionDefinition<'a> {
}
}
impl From<&dyn StdLibFn> for FunctionDefinition<'static> {
fn from(value: &dyn StdLibFn) -> Self {
let mut input_arg = None;
let mut named_args = IndexMap::new();
for a in value.args(false) {
if !a.label_required {
input_arg = Some((a.name.clone(), None));
continue;
}
named_args.insert(
a.name.clone(),
(
if a.required {
None
} else {
Some(DefaultParamVal::none())
},
None,
),
);
}
FunctionDefinition {
input_arg,
named_args,
return_type: None,
deprecated: value.deprecated(),
include_in_feature_tree: value.feature_tree_operation(),
is_std: true,
body: FunctionBody::Rust(value.std_lib_fn()),
}
}
}
impl Node<CallExpressionKw> {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
@ -274,59 +239,44 @@ impl Node<CallExpressionKw> {
exec_state.pipe_value().map(|v| Arg::new(v.clone(), callsite)),
);
match ctx.stdlib.get_rust_function(fn_name) {
Some(func) => {
let def: FunctionDefinition = (&*func).into();
// All std lib functions return a value, so the unwrap is safe.
def.call_kw(Some(func.name()), exec_state, ctx, args, callsite)
.await
.map(Option::unwrap)
.map_err(|e| {
// This is used for the backtrace display. We don't add
// another location the way we do for user-defined
// functions because the error uses the Args, which
// already points here.
e.set_last_backtrace_fn_name(Some(func.name()))
})
}
None => {
// 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();
// 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();
let Some(fn_src) = func.as_function() else {
return Err(KclError::new_semantic(KclErrorDetails::new(
"cannot call this because it isn't a function".to_string(),
vec![callsite],
)));
};
let Some(fn_src) = func.as_function() else {
return Err(KclError::new_semantic(KclErrorDetails::new(
"cannot call this because it isn't a function".to_string(),
vec![callsite],
)));
};
let return_value = fn_src
.call_kw(Some(fn_name.to_string()), exec_state, ctx, args, callsite)
.await
.map_err(|e| {
// Add the call expression to the source ranges.
//
// TODO: Use the name that the function was defined
// with, not the identifier it was used with.
e.add_unwind_location(Some(fn_name.name.name.clone()), callsite)
})?;
let return_value = fn_src
.call_kw(Some(fn_name.to_string()), exec_state, ctx, args, callsite)
.await
.map_err(|e| {
// Add the call expression to the source ranges.
//
// TODO: Use the name that the function was defined
// with, not the identifier it was used with.
e.add_unwind_location(Some(fn_name.name.name.clone()), callsite)
})?;
let result = return_value.ok_or_else(move || {
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();
};
KclError::new_undefined_value(KclErrorDetails::new(
format!("Result of user-defined function {} is undefined", fn_name),
source_ranges,
))
})?;
let result = return_value.ok_or_else(move || {
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();
};
KclError::new_undefined_value(
KclErrorDetails::new(
format!("Result of user-defined function {} is undefined", fn_name),
source_ranges,
),
None,
)
})?;
Ok(result)
}
}
Ok(result)
}
}
@ -372,6 +322,7 @@ impl FunctionDefinition<'_> {
.unlabeled_kw_arg_unconverted()
.map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
labeled_args: op_labeled_args,
node_path: NodePath::placeholder(),
source_range: callsite,
is_error: false,
})
@ -387,6 +338,7 @@ impl FunctionDefinition<'_> {
.map(|arg| OpArg::new(OpKclValue::from(&arg.1.value), arg.1.source_range)),
labeled_args: op_labeled_args,
},
node_path: NodePath::placeholder(),
source_range: callsite,
});
@ -600,30 +552,33 @@ fn type_check_params_kw(
for (label, arg) in &mut args.labeled {
match fn_def.named_args.get(label) {
Some((_, ty)) => {
if let Some(ty) = ty {
arg.value = arg
.value
.coerce(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::new_semantic(e.into()))?,
true,
exec_state,
)
.map_err(|e| {
let mut message = format!(
"{label} requires a value with type `{}`, but found {}",
ty,
arg.value.human_friendly_type(),
);
if let Some(ty) = e.explicit_coercion {
// TODO if we have access to the AST for the argument we could choose which example to suggest.
message = format!("{message}\n\nYou may need to add information about the type of the argument, for example:\n using a numeric suffix: `42{ty}`\n or using type ascription: `foo(): number({ty})`");
}
KclError::new_semantic(KclErrorDetails::new(
message,
vec![arg.source_range],
))
})?;
Some((def, ty)) => {
// For optional args, passing None should be the same as not passing an arg.
if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) {
if let Some(ty) = ty {
arg.value = arg
.value
.coerce(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::new_semantic(e.into()))?,
true,
exec_state,
)
.map_err(|e| {
let mut message = format!(
"{label} requires a value with type `{}`, but found {}",
ty,
arg.value.human_friendly_type(),
);
if let Some(ty) = e.explicit_coercion {
// TODO if we have access to the AST for the argument we could choose which example to suggest.
message = format!("{message}\n\nYou may need to add information about the type of the argument, for example:\n using a numeric suffix: `42{ty}`\n or using type ascription: `foo(): number({ty})`");
}
KclError::new_semantic(KclErrorDetails::new(
message,
vec![arg.source_range],
))
})?;
}
}
}
None => {
@ -703,7 +658,7 @@ fn type_check_params_kw(
exec_state.err(CompilationError::err(
arg.source_range,
format!(
"{} expects an unlabeled first parameter (`@{name}`), but it is labelled in the call",
"{} expects an unlabeled first argument (`@{name}`), but it is labelled in the call",
fn_name
.map(|n| format!("The function `{}`", n))
.unwrap_or_else(|| "This function".to_owned()),
@ -937,7 +892,6 @@ mod test {
crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
)),
fs: Arc::new(crate::fs::FileManager::new()),
stdlib: Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
context_type: ContextType::Mock,
};

View File

@ -24,6 +24,7 @@ type Point3D = kcmc::shared::Point3d<f64>;
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum Geometry {
Sketch(Sketch),
Solid(Solid),
@ -52,6 +53,7 @@ impl Geometry {
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum GeometryWithImportedGeometry {
Sketch(Sketch),
Solid(Solid),

View File

@ -31,7 +31,7 @@ pub(crate) type Universe = HashMap<String, DependencyInfo>;
/// run concurrently. Each "stage" is blocking in this model, which will
/// change in the future. Don't use this function widely, yet.
#[allow(clippy::iter_over_hash_type)]
pub fn import_graph(progs: &Universe, ctx: &ExecutorContext) -> Result<Vec<Vec<String>>, KclError> {
pub(crate) fn import_graph(progs: &Universe, ctx: &ExecutorContext) -> Result<Vec<Vec<String>>, KclError> {
let mut graph = Graph::new();
for (name, (_, _, path, repr)) in progs.iter() {
@ -120,7 +120,7 @@ fn topsort(all_modules: &[&str], graph: Graph) -> Result<Vec<Vec<String>>, KclEr
type ImportDependencies = Vec<(String, AstNode<ImportStatement>, ModulePath)>;
pub(crate) fn import_dependencies(
fn import_dependencies(
path: &ModulePath,
repr: &ModuleRepr,
ctx: &ExecutorContext,

View File

@ -367,10 +367,10 @@ impl ProgramMemory {
let name = var.trim_start_matches(TYPE_PREFIX).trim_start_matches(MODULE_PREFIX);
Err(KclError::new_undefined_value(KclErrorDetails::new(
format!("`{name}` is not defined"),
vec![source_range],
)))
Err(KclError::new_undefined_value(
KclErrorDetails::new(format!("`{name}` is not defined"), vec![source_range]),
Some(name.to_owned()),
))
}
/// Iterate over all key/value pairs in the specified environment which satisfy the provided
@ -488,10 +488,10 @@ impl ProgramMemory {
};
}
Err(KclError::new_undefined_value(KclErrorDetails::new(
format!("`{}` is not defined", var),
vec![],
)))
Err(KclError::new_undefined_value(
KclErrorDetails::new(format!("`{}` is not defined", var), vec![]),
Some(var.to_owned()),
))
}
}

View File

@ -5,7 +5,7 @@ use std::sync::Arc;
use anyhow::Result;
#[cfg(feature = "artifact-graph")]
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketchOnFace, StartSketchOnPlane};
use cache::OldAstState;
use cache::GlobalState;
pub use cache::{bust_cache, clear_mem_cache};
#[cfg(feature = "artifact-graph")]
pub use cad_op::{Group, Operation};
@ -27,13 +27,12 @@ use serde::{Deserialize, Serialize};
pub use state::{ExecState, MetaSettings};
use uuid::Uuid;
#[cfg(feature = "artifact-graph")]
use crate::execution::artifact::build_artifact_graph;
use crate::{
engine::EngineManager,
errors::{KclError, KclErrorDetails},
execution::{
cache::{CacheInformation, CacheResult},
import_graph::{Universe, UniverseMap},
typed_path::TypedPath,
types::{UnitAngle, UnitLen},
},
@ -41,8 +40,6 @@ use crate::{
modules::{ModuleId, ModulePath, ModuleRepr},
parsing::ast::types::{Expr, ImportPath, NodeRef},
source_range::SourceRange,
std::StdLib,
walk::{Universe, UniverseMap},
CompilationError, ExecError, KclErrorWithOutputs,
};
@ -56,6 +53,7 @@ pub mod fn_call;
mod geometry;
mod id_generator;
mod import;
mod import_graph;
pub(crate) mod kcl_value;
mod memory;
mod state;
@ -273,7 +271,6 @@ pub enum ContextType {
pub struct ExecutorContext {
pub engine: Arc<Box<dyn EngineManager>>,
pub fs: Arc<FileManager>,
pub stdlib: Arc<StdLib>,
pub settings: ExecutorSettings,
pub context_type: ContextType,
}
@ -412,7 +409,6 @@ impl ExecutorContext {
Ok(Self {
engine,
fs: Arc::new(FileManager::new()),
stdlib: Arc::new(StdLib::new()),
settings,
context_type: ContextType::Live,
})
@ -423,7 +419,6 @@ impl ExecutorContext {
ExecutorContext {
engine,
fs,
stdlib: Arc::new(StdLib::new()),
settings,
context_type: ContextType::Live,
}
@ -436,7 +431,6 @@ impl ExecutorContext {
crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
)),
fs: Arc::new(FileManager::new()),
stdlib: Arc::new(StdLib::new()),
settings: settings.unwrap_or_default(),
context_type: ContextType::Mock,
}
@ -447,7 +441,6 @@ impl ExecutorContext {
ExecutorContext {
engine,
fs,
stdlib: Arc::new(StdLib::new()),
settings,
context_type: ContextType::Mock,
}
@ -458,7 +451,6 @@ impl ExecutorContext {
ExecutorContext {
engine,
fs: Arc::new(FileManager::new()),
stdlib: Arc::new(StdLib::new()),
settings: Default::default(),
context_type: ContextType::MockCustomForwarded,
}
@ -575,7 +567,7 @@ impl ExecutorContext {
// part of the scene).
exec_state.mut_stack().push_new_env_for_scope();
let result = self.inner_run(&program, 0, &mut exec_state, true).await?;
let result = self.inner_run(&program, &mut exec_state, true).await?;
// Restore any temporary variables, then save any newly created variables back to
// memory in case another run wants to use them. Note this is just saved to the preserved
@ -583,7 +575,7 @@ impl ExecutorContext {
let mut mem = exec_state.stack().clone();
let module_infos = exec_state.global.module_infos.clone();
let outcome = exec_state.to_mock_exec_outcome(result.0).await;
let outcome = exec_state.to_mock_exec_outcome(result.0, self).await;
mem.squash_env(result.0);
cache::write_old_memory((mem, module_infos)).await;
@ -600,169 +592,179 @@ impl ExecutorContext {
.map(|s| s.default_length_units)
.map(kcmc::units::UnitLength::from);
let (program, mut exec_state, preserve_mem, cached_body_items, imports_info) = if let Some(OldAstState {
ast: old_ast,
exec_state: mut old_state,
settings: old_settings,
result_env,
}) =
cache::read_old_ast().await
{
let old = CacheInformation {
ast: &old_ast,
settings: &old_settings,
};
let new = CacheInformation {
ast: &program.ast,
settings: &self.settings,
};
// Get the program that actually changed from the old and new information.
let (clear_scene, program, body_items, import_check_info) = match cache::get_changed_program(old, new).await
{
CacheResult::ReExecute {
clear_scene,
reapply_settings,
program: changed_program,
cached_body_items,
} => {
if reapply_settings
&& self
.engine
.reapply_settings(&self.settings, Default::default(), old_state.id_generator(), grid_scale)
.await
.is_err()
{
(true, program, cached_body_items, None)
} else {
(
clear_scene,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
cached_body_items,
None,
)
}
}
CacheResult::CheckImportsOnly {
reapply_settings,
ast: changed_program,
} => {
if reapply_settings
&& self
.engine
.reapply_settings(&self.settings, Default::default(), old_state.id_generator(), grid_scale)
.await
.is_err()
{
(true, program, old_ast.body.len(), None)
} else {
// We need to check our imports to see if they changed.
let mut new_exec_state = ExecState::new(self);
let (new_universe, new_universe_map) = self.get_universe(&program, &mut new_exec_state).await?;
let mut clear_scene = false;
let mut keys = new_universe.keys().clone().collect::<Vec<_>>();
keys.sort();
for key in keys {
let (_, id, _, _) = &new_universe[key];
if let (Some(source0), Some(source1)) =
(old_state.get_source(*id), new_exec_state.get_source(*id))
{
if source0.source != source1.source {
clear_scene = true;
break;
}
}
}
if !clear_scene {
// Return early we don't need to clear the scene.
let outcome = old_state.to_exec_outcome(result_env).await;
return Ok(outcome);
}
(
clear_scene,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
old_ast.body.len(),
// We only care about this if we are clearing the scene.
if clear_scene {
Some((new_universe, new_universe_map, new_exec_state))
} else {
None
},
)
}
}
CacheResult::NoAction(true) => {
if self
.engine
.reapply_settings(&self.settings, Default::default(), old_state.id_generator(), grid_scale)
.await
.is_ok()
{
// We need to update the old ast state with the new settings!!
cache::write_old_ast(OldAstState {
ast: old_ast,
exec_state: old_state.clone(),
settings: self.settings.clone(),
result_env,
})
.await;
let outcome = old_state.to_exec_outcome(result_env).await;
return Ok(outcome);
}
(true, program, old_ast.body.len(), None)
}
CacheResult::NoAction(false) => {
let outcome = old_state.to_exec_outcome(result_env).await;
return Ok(outcome);
}
};
let (exec_state, preserve_mem, universe_info) =
if let Some((new_universe, new_universe_map, mut new_exec_state)) = import_check_info {
// Clear the scene if the imports changed.
self.send_clear_scene(&mut new_exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
(new_exec_state, false, Some((new_universe, new_universe_map)))
} else if clear_scene {
// Pop the execution state, since we are starting fresh.
let mut exec_state = old_state;
exec_state.reset(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
(exec_state, false, None)
} else {
old_state.mut_stack().restore_env(result_env);
(old_state, true, None)
let (program, exec_state, result) = match cache::read_old_ast().await {
Some(mut cached_state) => {
let old = CacheInformation {
ast: &cached_state.main.ast,
settings: &cached_state.settings,
};
let new = CacheInformation {
ast: &program.ast,
settings: &self.settings,
};
(program, exec_state, preserve_mem, body_items, universe_info)
} else {
let mut exec_state = ExecState::new(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
(program, exec_state, false, 0, None)
};
// Get the program that actually changed from the old and new information.
let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
CacheResult::ReExecute {
clear_scene,
reapply_settings,
program: changed_program,
} => {
if reapply_settings
&& self
.engine
.reapply_settings(
&self.settings,
Default::default(),
&mut cached_state.main.exec_state.id_generator,
grid_scale,
)
.await
.is_err()
{
(true, program, None)
} else {
(
clear_scene,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
None,
)
}
}
CacheResult::CheckImportsOnly {
reapply_settings,
ast: changed_program,
} => {
if reapply_settings
&& self
.engine
.reapply_settings(
&self.settings,
Default::default(),
&mut cached_state.main.exec_state.id_generator,
grid_scale,
)
.await
.is_err()
{
(true, program, None)
} else {
// We need to check our imports to see if they changed.
let mut new_exec_state = ExecState::new(self);
let (new_universe, new_universe_map) =
self.get_universe(&program, &mut new_exec_state).await?;
let result = self
.run_concurrent(&program, cached_body_items, &mut exec_state, imports_info, preserve_mem)
.await;
let clear_scene = new_universe.keys().any(|key| {
let id = new_universe[key].1;
match (
cached_state.exec_state.get_source(id),
new_exec_state.global.get_source(id),
) {
(Some(s0), Some(s1)) => s0.source != s1.source,
_ => false,
}
});
if !clear_scene {
// Return early we don't need to clear the scene.
return Ok(cached_state.into_exec_outcome(self).await);
}
(
true,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
Some((new_universe, new_universe_map, new_exec_state)),
)
}
}
CacheResult::NoAction(true) => {
if self
.engine
.reapply_settings(
&self.settings,
Default::default(),
&mut cached_state.main.exec_state.id_generator,
grid_scale,
)
.await
.is_ok()
{
// We need to update the old ast state with the new settings!!
cache::write_old_ast(GlobalState::with_settings(
cached_state.clone(),
self.settings.clone(),
))
.await;
return Ok(cached_state.into_exec_outcome(self).await);
}
(true, program, None)
}
CacheResult::NoAction(false) => {
return Ok(cached_state.into_exec_outcome(self).await);
}
};
let (exec_state, result) = match import_check_info {
Some((new_universe, new_universe_map, mut new_exec_state)) => {
// Clear the scene if the imports changed.
self.send_clear_scene(&mut new_exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let result = self
.run_concurrent(
&program,
&mut new_exec_state,
Some((new_universe, new_universe_map)),
false,
)
.await;
(new_exec_state, result)
}
None if clear_scene => {
// Pop the execution state, since we are starting fresh.
let mut exec_state = cached_state.reconstitute_exec_state();
exec_state.reset(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
(exec_state, result)
}
None => {
let mut exec_state = cached_state.reconstitute_exec_state();
exec_state.mut_stack().restore_env(cached_state.main.result_env);
let result = self.run_concurrent(&program, &mut exec_state, None, true).await;
(exec_state, result)
}
};
(program, exec_state, result)
}
None => {
let mut exec_state = ExecState::new(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
(program, exec_state, result)
}
};
if result.is_err() {
cache::bust_cache().await;
@ -772,15 +774,15 @@ impl ExecutorContext {
let result = result?;
// Save this as the last successful execution to the cache.
cache::write_old_ast(OldAstState {
ast: program.ast,
exec_state: exec_state.clone(),
settings: self.settings.clone(),
result_env: result.0,
})
cache::write_old_ast(GlobalState::new(
exec_state.clone(),
self.settings.clone(),
program.ast,
result.0,
))
.await;
let outcome = exec_state.to_exec_outcome(result.0).await;
let outcome = exec_state.to_exec_outcome(result.0, self).await;
Ok(outcome)
}
@ -795,11 +797,11 @@ impl ExecutorContext {
program: &crate::Program,
exec_state: &mut ExecState,
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
self.run_concurrent(program, 0, exec_state, None, false).await
self.run_concurrent(program, exec_state, None, false).await
}
/// Perform the execution of a program using a concurrent
/// execution model. This has the same signature as [Self::run].
/// execution model.
///
/// You can optionally pass in some initialization memory for partial
/// execution.
@ -808,13 +810,12 @@ impl ExecutorContext {
pub async fn run_concurrent(
&self,
program: &crate::Program,
cached_body_items: usize,
exec_state: &mut ExecState,
universe_info: Option<(Universe, UniverseMap)>,
preserve_mem: bool,
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
// Reuse our cached universe if we have one.
#[allow(unused_variables)]
let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
(universe, universe_map)
} else {
@ -828,29 +829,8 @@ impl ExecutorContext {
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
for modules in crate::walk::import_graph(&universe, self)
.map_err(|err| {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
err,
exec_state.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
exec_state.global.operations.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_commands.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes.clone(),
)
})?
for modules in import_graph::import_graph(&universe, self)
.map_err(|err| exec_state.error_with_outputs(err, default_planes.clone()))?
.into_iter()
{
#[cfg(not(target_arch = "wasm32"))]
@ -872,32 +852,14 @@ impl ExecutorContext {
let module_path = module_path.clone();
let source_range = SourceRange::from(import_stmt);
#[cfg(feature = "artifact-graph")]
match &module_path {
ModulePath::Main => {
// This should never happen.
}
ModulePath::Local { value, .. } => {
// We only want to display the top-level module imports in
// the Feature Tree, not transitive imports.
if universe_map.contains_key(value) {
exec_state.global.operations.push(Operation::GroupBegin {
group: Group::ModuleInstance {
name: value.file_name().unwrap_or_default(),
module_id,
},
source_range,
});
// Due to concurrent execution, we cannot easily
// group operations by module. So we leave the
// group empty and close it immediately.
exec_state.global.operations.push(Operation::GroupEnd);
}
}
ModulePath::Std { .. } => {
// We don't want to display stdlib in the Feature Tree.
}
}
self.add_import_module_ops(
exec_state,
program,
module_id,
&module_path,
source_range,
&universe_map,
);
let repr = repr.clone();
let exec_state = exec_state.clone();
@ -936,7 +898,6 @@ impl ExecutorContext {
#[cfg(target_arch = "wasm32")]
{
wasm_bindgen_futures::spawn_local(async move {
//set.spawn(async move {
let mut exec_state = exec_state;
let exec_ctxt = exec_ctxt;
@ -1006,33 +967,13 @@ impl ExecutorContext {
exec_state.global.module_infos[&module_id].restore_repr(repr);
}
Err(e) => {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
return Err(KclErrorWithOutputs::new(
e,
exec_state.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
exec_state.global.operations.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_commands.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes,
));
return Err(exec_state.error_with_outputs(e, default_planes));
}
}
}
}
self.inner_run(program, cached_body_items, exec_state, preserve_mem)
.await
self.inner_run(program, exec_state, preserve_mem).await
}
/// Get the universe & universe map of the program.
@ -1048,7 +989,7 @@ impl ExecutorContext {
let default_planes = self.engine.get_default_planes().read().await.clone();
let root_imports = crate::walk::import_universe(
let root_imports = import_graph::import_universe(
self,
&ModulePath::Main,
&ModuleRepr::Kcl(program.ast.clone(), None),
@ -1056,51 +997,89 @@ impl ExecutorContext {
exec_state,
)
.await
.map_err(|err| {
println!("Error: {err:?}");
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
err,
exec_state.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
exec_state.global.operations.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_commands.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes,
)
})?;
.map_err(|err| exec_state.error_with_outputs(err, default_planes))?;
Ok((universe, root_imports))
}
#[cfg(feature = "artifact-graph")]
fn add_import_module_ops(
&self,
exec_state: &mut ExecState,
program: &crate::Program,
module_id: ModuleId,
module_path: &ModulePath,
source_range: SourceRange,
universe_map: &UniverseMap,
) {
match module_path {
ModulePath::Main => {
// This should never happen.
}
ModulePath::Local { value, .. } => {
// We only want to display the top-level module imports in
// the Feature Tree, not transitive imports.
if universe_map.contains_key(value) {
use crate::NodePath;
let node_path = if source_range.is_top_level_module() {
let cached_body_items = exec_state.global.artifacts.cached_body_items();
NodePath::from_range(&program.ast, cached_body_items, source_range).unwrap_or_default()
} else {
// The frontend doesn't care about paths in
// files other than the top-level module.
NodePath::placeholder()
};
exec_state.push_op(Operation::GroupBegin {
group: Group::ModuleInstance {
name: value.file_name().unwrap_or_default(),
module_id,
},
node_path,
source_range,
});
// Due to concurrent execution, we cannot easily
// group operations by module. So we leave the
// group empty and close it immediately.
exec_state.push_op(Operation::GroupEnd);
}
}
ModulePath::Std { .. } => {
// We don't want to display stdlib in the Feature Tree.
}
}
}
#[cfg(not(feature = "artifact-graph"))]
fn add_import_module_ops(
&self,
_exec_state: &mut ExecState,
_program: &crate::Program,
_module_id: ModuleId,
_module_path: &ModulePath,
_source_range: SourceRange,
_universe_map: &UniverseMap,
) {
}
/// Perform the execution of a program. Accept all possible parameters and
/// output everything.
async fn inner_run(
&self,
program: &crate::Program,
cached_body_items: usize,
exec_state: &mut ExecState,
preserve_mem: bool,
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
let _stats = crate::log::LogPerfStats::new("Interpretation");
// Re-apply the settings, in case the cache was busted.
let grid_scale = program
.meta_settings()
.ok()
.flatten()
.map(|s| s.default_length_units)
.map(kcmc::units::UnitLength::from);
// Re-apply the settings, in case the cache was busted.
self.engine
.reapply_settings(
&self.settings,
@ -1113,7 +1092,7 @@ impl ExecutorContext {
let default_planes = self.engine.get_default_planes().read().await.clone();
let result = self
.execute_and_build_graph(&program.ast, cached_body_items, exec_state, preserve_mem)
.execute_and_build_graph(&program.ast, exec_state, preserve_mem)
.await;
crate::log::log(format!(
@ -1122,28 +1101,7 @@ impl ExecutorContext {
));
crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
let env_ref = result.map_err(|e| {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
e,
exec_state.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
exec_state.global.operations.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_commands.clone(),
#[cfg(feature = "artifact-graph")]
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes.clone(),
)
})?;
let env_ref = result.map_err(|e| exec_state.error_with_outputs(e, default_planes))?;
if !self.is_mock() {
let mut mem = exec_state.stack().deep_clone();
@ -1160,13 +1118,17 @@ impl ExecutorContext {
async fn execute_and_build_graph(
&self,
program: NodeRef<'_, crate::parsing::ast::types::Program>,
#[cfg_attr(not(feature = "artifact-graph"), expect(unused))] cached_body_items: usize,
exec_state: &mut ExecState,
preserve_mem: bool,
) -> Result<EnvironmentRef, KclError> {
// Don't early return! We need to build other outputs regardless of
// whether execution failed.
// Because of execution caching, we may start with operations from a
// previous run.
#[cfg(feature = "artifact-graph")]
let start_op = exec_state.global.artifacts.operations.len();
self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
.await?;
@ -1180,6 +1142,29 @@ impl ExecutorContext {
)
.await;
#[cfg(feature = "artifact-graph")]
{
// Fill in NodePath for operations.
let cached_body_items = exec_state.global.artifacts.cached_body_items();
for op in exec_state.global.artifacts.operations.iter_mut().skip(start_op) {
match op {
Operation::StdLibCall {
node_path,
source_range,
..
}
| Operation::GroupBegin {
node_path,
source_range,
..
} => {
node_path.fill_placeholder(program, cached_body_items, *source_range);
}
Operation::GroupEnd => {}
}
}
}
// Ensure all the async commands completed.
self.engine.ensure_async_commands_completed().await?;
@ -1187,40 +1172,9 @@ impl ExecutorContext {
// and should be dropped.
self.engine.clear_queues().await;
#[cfg(feature = "artifact-graph")]
{
let new_commands = self.engine.take_artifact_commands().await;
let new_responses = self.engine.take_responses().await;
let initial_graph = exec_state.global.artifact_graph.clone();
// Build the artifact graph.
let graph_result = build_artifact_graph(
&new_commands,
&new_responses,
program,
cached_body_items,
&mut exec_state.global.artifacts,
initial_graph,
);
// Move the artifact commands and responses into ExecState to
// simplify cache management and error creation.
exec_state.global.artifact_commands.extend(new_commands);
exec_state.global.artifact_responses.extend(new_responses);
match graph_result {
Ok(artifact_graph) => {
exec_state.global.artifact_graph = artifact_graph;
exec_result.map(|(_, env_ref, _)| env_ref)
}
Err(err) => {
// Prefer the exec error.
exec_result.and(Err(err))
}
}
}
#[cfg(not(feature = "artifact-graph"))]
{
exec_result.map(|(_, env_ref, _)| env_ref)
match exec_state.build_artifact_graph(&self.engine, program).await {
Ok(_) => exec_result.map(|(_, env_ref, _)| env_ref),
Err(err) => exec_result.and(Err(err)),
}
}
@ -1407,7 +1361,6 @@ pub(crate) async fn parse_execute_with_project_dir(
})?,
)),
fs: Arc::new(crate::fs::FileManager::new()),
stdlib: Arc::new(crate::std::StdLib::new()),
settings: ExecutorSettings {
project_directory,
..Default::default()
@ -2089,7 +2042,7 @@ notPipeSub = 1 |> identity(!%))";
// a runtime error instead.
parse_execute(code11).await.unwrap_err(),
KclError::new_syntax(KclErrorDetails::new(
"There was an unexpected !. Try removing it.".to_owned(),
"There was an unexpected `!`. Try removing it.".to_owned(),
vec![SourceRange::new(56, 57, ModuleId::default())],
))
);
@ -2223,7 +2176,7 @@ w = f() + f()
}
// Get the id_generator from the first execution.
let id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
let code = r#"sketch001 = startSketchOn(XZ)
|> startProfile(at = [62.74, 206.13])
@ -2244,7 +2197,7 @@ w = f() + f()
// Execute the program.
ctx.run_with_caching(program).await.unwrap();
let new_id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
assert_eq!(id_generator, new_id_generator);
}

View File

@ -12,19 +12,19 @@ use uuid::Uuid;
use crate::execution::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
use crate::{
errors::{KclError, KclErrorDetails, Severity},
exec::DefaultPlanes,
execution::{
annotations,
cad_op::Operation,
id_generator::IdGenerator,
memory::{ProgramMemory, Stack},
types,
types::NumericType,
types::{self, NumericType},
EnvironmentRef, ExecOutcome, ExecutorSettings, KclValue, UnitAngle, UnitLen,
},
modules::{ModuleId, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr, ModuleSource},
parsing::ast::types::Annotation,
parsing::ast::types::{Annotation, NodeRef},
source_range::SourceRange,
CompilationError,
CompilationError, EngineManager, ExecutorContext, KclErrorWithOutputs,
};
/// State for executing a program.
@ -32,7 +32,6 @@ use crate::{
pub struct ExecState {
pub(super) global: GlobalState,
pub(super) mod_local: ModuleState,
pub(super) exec_context: Option<super::ExecutorContext>,
}
pub type ModuleInfoMap = IndexMap<ModuleId, ModuleInfo>;
@ -45,33 +44,39 @@ pub(super) struct GlobalState {
pub id_to_source: IndexMap<ModuleId, ModuleSource>,
/// Map from module ID to module info.
pub module_infos: ModuleInfoMap,
/// Output map of UUIDs to artifacts.
#[cfg(feature = "artifact-graph")]
pub artifacts: IndexMap<ArtifactId, Artifact>,
/// Output commands to allow building the artifact graph by the caller.
/// These are accumulated in the [`ExecutorContext`] but moved here for
/// convenience of the execution cache.
#[cfg(feature = "artifact-graph")]
pub artifact_commands: Vec<ArtifactCommand>,
/// Responses from the engine for `artifact_commands`. We need to cache
/// this so that we can build the artifact graph. These are accumulated in
/// the [`ExecutorContext`] but moved here for convenience of the execution
/// cache.
#[cfg(feature = "artifact-graph")]
pub artifact_responses: IndexMap<Uuid, WebSocketResponse>,
/// Output artifact graph.
#[cfg(feature = "artifact-graph")]
pub artifact_graph: ArtifactGraph,
/// Operations that have been performed in execution order, for display in
/// the Feature Tree.
#[cfg(feature = "artifact-graph")]
pub operations: Vec<Operation>,
/// Module loader.
pub mod_loader: ModuleLoader,
/// Errors and warnings.
pub errors: Vec<CompilationError>,
#[allow(dead_code)]
pub artifacts: ArtifactState,
}
#[cfg(feature = "artifact-graph")]
#[derive(Debug, Clone, Default)]
pub(super) struct ArtifactState {
/// Output map of UUIDs to artifacts.
pub artifacts: IndexMap<ArtifactId, Artifact>,
/// Output commands to allow building the artifact graph by the caller.
/// These are accumulated in the [`ExecutorContext`] but moved here for
/// convenience of the execution cache.
pub commands: Vec<ArtifactCommand>,
/// Responses from the engine for `artifact_commands`. We need to cache
/// this so that we can build the artifact graph. These are accumulated in
/// the [`ExecutorContext`] but moved here for convenience of the execution
/// cache.
pub responses: IndexMap<Uuid, WebSocketResponse>,
/// Output artifact graph.
pub graph: ArtifactGraph,
/// Operations that have been performed in execution order, for display in
/// the Feature Tree.
pub operations: Vec<Operation>,
}
#[cfg(not(feature = "artifact-graph"))]
#[derive(Debug, Clone, Default)]
pub(super) struct ArtifactState {}
#[derive(Debug, Clone)]
pub(super) struct ModuleState {
/// The id generator for this module.
@ -80,6 +85,11 @@ pub(super) struct ModuleState {
/// The current value of the pipe operator returned from the previous
/// expression. If we're not currently in a pipeline, this will be None.
pub pipe_value: Option<KclValue>,
/// The closest variable declaration being executed in any parent node in the AST.
/// This is used to provide better error messages, e.g. noticing when the user is trying
/// to use the variable `length` inside the RHS of its own definition, like `length = tan(length)`.
/// TODO: Make this a reference.
pub being_declared: Option<String>,
/// Identifiers that have been exported from the current module.
pub module_exports: Vec<String>,
/// Settings specified from annotations.
@ -93,7 +103,6 @@ impl ExecState {
ExecState {
global: GlobalState::new(&exec_context.settings),
mod_local: ModuleState::new(ModulePath::Main, ProgramMemory::new(), Default::default()),
exec_context: Some(exec_context.clone()),
}
}
@ -103,7 +112,6 @@ impl ExecState {
*self = ExecState {
global,
mod_local: ModuleState::new(self.mod_local.path.clone(), ProgramMemory::new(), Default::default()),
exec_context: Some(exec_context.clone()),
};
}
@ -125,45 +133,26 @@ impl ExecState {
/// Convert to execution outcome when running in WebAssembly. We want to
/// reduce the amount of data that crosses the WASM boundary as much as
/// possible.
pub async fn to_exec_outcome(self, main_ref: EnvironmentRef) -> ExecOutcome {
pub async fn to_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState.
ExecOutcome {
variables: self
.stack()
.find_all_in_env(main_ref)
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
variables: self.mod_local.variables(main_ref),
filenames: self.global.filenames(),
#[cfg(feature = "artifact-graph")]
operations: self.global.operations,
operations: self.global.artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_commands: self.global.artifact_commands,
artifact_commands: self.global.artifacts.commands,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.global.artifact_graph,
artifact_graph: self.global.artifacts.graph,
errors: self.global.errors,
filenames: self
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect(),
default_planes: if let Some(ctx) = &self.exec_context {
ctx.engine.get_default_planes().read().await.clone()
} else {
None
},
default_planes: ctx.engine.get_default_planes().read().await.clone(),
}
}
pub async fn to_mock_exec_outcome(self, main_ref: EnvironmentRef) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState.
pub async fn to_mock_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
ExecOutcome {
variables: self
.stack()
.find_all_in_env(main_ref)
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
variables: self.mod_local.variables(main_ref),
#[cfg(feature = "artifact-graph")]
operations: Default::default(),
#[cfg(feature = "artifact-graph")]
@ -172,11 +161,7 @@ impl ExecState {
artifact_graph: Default::default(),
errors: self.global.errors,
filenames: Default::default(),
default_planes: if let Some(ctx) = &self.exec_context {
ctx.engine.get_default_planes().read().await.clone()
} else {
None
},
default_planes: ctx.engine.get_default_planes().read().await.clone(),
}
}
@ -199,12 +184,12 @@ impl ExecState {
#[cfg(feature = "artifact-graph")]
pub(crate) fn add_artifact(&mut self, artifact: Artifact) {
let id = artifact.id();
self.global.artifacts.insert(id, artifact);
self.global.artifacts.artifacts.insert(id, artifact);
}
pub(crate) fn push_op(&mut self, op: Operation) {
#[cfg(feature = "artifact-graph")]
self.global.operations.push(op);
self.global.artifacts.operations.push(op);
#[cfg(not(feature = "artifact-graph"))]
drop(op);
}
@ -246,10 +231,6 @@ impl ExecState {
self.global.id_to_source.insert(id, source.clone());
}
pub(super) fn get_source(&self, id: ModuleId) -> Option<&ModuleSource> {
self.global.id_to_source.get(&id)
}
pub(super) fn add_module(&mut self, id: ModuleId, path: ModulePath, repr: ModuleRepr) {
debug_assert!(self.global.path_to_source_id.contains_key(&path));
let module_info = ModuleInfo { id, repr, path };
@ -295,6 +276,71 @@ impl ExecState {
pub(crate) fn pipe_value(&self) -> Option<&KclValue> {
self.mod_local.pipe_value.as_ref()
}
pub(crate) fn error_with_outputs(
&self,
error: KclError,
default_planes: Option<DefaultPlanes>,
) -> KclErrorWithOutputs {
let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = self
.global
.path_to_source_id
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect();
KclErrorWithOutputs::new(
error,
self.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
self.global.artifacts.operations.clone(),
#[cfg(feature = "artifact-graph")]
self.global.artifacts.commands.clone(),
#[cfg(feature = "artifact-graph")]
self.global.artifacts.graph.clone(),
module_id_to_module_path,
self.global.id_to_source.clone(),
default_planes,
)
}
#[cfg(feature = "artifact-graph")]
pub(crate) async fn build_artifact_graph(
&mut self,
engine: &Arc<Box<dyn EngineManager>>,
program: NodeRef<'_, crate::parsing::ast::types::Program>,
) -> Result<(), KclError> {
let new_commands = engine.take_artifact_commands().await;
let new_responses = engine.take_responses().await;
let initial_graph = self.global.artifacts.graph.clone();
// Build the artifact graph.
let graph_result = crate::execution::artifact::build_artifact_graph(
&new_commands,
&new_responses,
program,
&mut self.global.artifacts.artifacts,
initial_graph,
);
// Move the artifact commands and responses into ExecState to
// simplify cache management and error creation.
self.global.artifacts.commands.extend(new_commands);
self.global.artifacts.responses.extend(new_responses);
let artifact_graph = graph_result?;
self.global.artifacts.graph = artifact_graph;
Ok(())
}
#[cfg(not(feature = "artifact-graph"))]
pub(crate) async fn build_artifact_graph(
&mut self,
_engine: &Arc<Box<dyn EngineManager>>,
_program: NodeRef<'_, crate::parsing::ast::types::Program>,
) -> Result<(), KclError> {
Ok(())
}
}
impl GlobalState {
@ -302,16 +348,7 @@ impl GlobalState {
let mut global = GlobalState {
path_to_source_id: Default::default(),
module_infos: Default::default(),
#[cfg(feature = "artifact-graph")]
artifacts: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_commands: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_responses: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_graph: Default::default(),
#[cfg(feature = "artifact-graph")]
operations: Default::default(),
mod_loader: Default::default(),
errors: Default::default(),
id_to_source: Default::default(),
@ -334,6 +371,21 @@ impl GlobalState {
.insert(ModulePath::Local { value: root_path }, root_id);
global
}
pub(super) fn filenames(&self) -> IndexMap<ModuleId, ModulePath> {
self.path_to_source_id.iter().map(|(k, v)| ((*v), k.clone())).collect()
}
pub(super) fn get_source(&self, id: ModuleId) -> Option<&ModuleSource> {
self.id_to_source.get(&id)
}
}
#[cfg(feature = "artifact-graph")]
impl ArtifactState {
pub fn cached_body_items(&self) -> usize {
self.graph.item_count
}
}
impl ModuleState {
@ -342,6 +394,7 @@ impl ModuleState {
id_generator: IdGenerator::new(module_id),
stack: memory.new_stack(),
pipe_value: Default::default(),
being_declared: Default::default(),
module_exports: Default::default(),
explicit_length_units: false,
path,
@ -352,6 +405,13 @@ impl ModuleState {
},
}
}
pub(super) fn variables(&self, main_ref: EnvironmentRef) -> IndexMap<String, KclValue> {
self.stack
.find_all_in_env(main_ref)
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]

View File

@ -220,6 +220,7 @@ impl schemars::JsonSchema for TypedPath {
///
/// * Does **not** touch `..` or symlinks call `canonicalize()` if you need that.
/// * Returns an owned `PathBuf` only when normalisation was required.
#[cfg(not(target_arch = "wasm32"))]
fn normalise_import<S: AsRef<str>>(raw: S) -> std::path::PathBuf {
let s = raw.as_ref();
// On Unix we need to swap `\` → `/`. On Windows we leave it alone.

View File

@ -187,7 +187,7 @@ impl RuntimeType {
};
RuntimeType::Primitive(PrimitiveType::Number(ty))
}
AstPrimitiveType::Named(name) => Self::from_alias(&name.name, exec_state, source_range)?,
AstPrimitiveType::Named { id } => Self::from_alias(&id.name, exec_state, source_range)?,
AstPrimitiveType::Tag => RuntimeType::Primitive(PrimitiveType::Tag),
AstPrimitiveType::ImportedGeometry => RuntimeType::Primitive(PrimitiveType::ImportedGeometry),
AstPrimitiveType::Function(_) => RuntimeType::Primitive(PrimitiveType::Function),

View File

@ -247,17 +247,6 @@ impl ObjectProperty {
}
}
impl MemberObject {
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
match self {
MemberObject::MemberExpression(member_expression) => {
member_expression.get_hover_value_for_position(pos, code, opts)
}
MemberObject::Identifier(_identifier) => None,
}
}
}
impl MemberExpression {
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
let object_source_range: SourceRange = self.object.clone().into();

View File

@ -175,11 +175,10 @@ impl Backend {
zoo_client: kittycad::Client,
can_send_telemetry: bool,
) -> Result<Self, String> {
let stdlib = crate::std::StdLib::new();
let kcl_std = crate::docs::kcl_doc::walk_prelude();
let stdlib_completions = get_completions_from_stdlib(&stdlib, &kcl_std).map_err(|e| e.to_string())?;
let stdlib_signatures = get_signatures_from_stdlib(&stdlib, &kcl_std);
let stdlib_args = get_arg_maps_from_stdlib(&stdlib, &kcl_std);
let stdlib_completions = get_completions_from_stdlib(&kcl_std).map_err(|e| e.to_string())?;
let stdlib_signatures = get_signatures_from_stdlib(&kcl_std);
let stdlib_args = get_arg_maps_from_stdlib(&kcl_std);
Ok(Self {
client,
@ -1189,7 +1188,6 @@ impl LanguageServer for Backend {
}
async fn completion(&self, params: CompletionParams) -> RpcResult<Option<CompletionResponse>> {
// ADAM: This is the entrypoint.
let mut completions = vec![CompletionItem {
label: PIPE_OPERATOR.to_string(),
label_details: None,
@ -1635,16 +1633,8 @@ impl LanguageServer for Backend {
}
/// Get completions from our stdlib.
pub fn get_completions_from_stdlib(
stdlib: &crate::std::StdLib,
kcl_std: &ModData,
) -> Result<HashMap<String, CompletionItem>> {
pub fn get_completions_from_stdlib(kcl_std: &ModData) -> Result<HashMap<String, CompletionItem>> {
let mut completions = HashMap::new();
let combined = stdlib.combined();
for internal_fn in combined.values() {
completions.insert(internal_fn.name(), internal_fn.to_completion_item()?);
}
for d in kcl_std.all_docs() {
if let Some(ci) = d.to_completion_item() {
@ -1661,13 +1651,8 @@ pub fn get_completions_from_stdlib(
}
/// Get signatures from our stdlib.
pub fn get_signatures_from_stdlib(stdlib: &crate::std::StdLib, kcl_std: &ModData) -> HashMap<String, SignatureHelp> {
pub fn get_signatures_from_stdlib(kcl_std: &ModData) -> HashMap<String, SignatureHelp> {
let mut signatures = HashMap::new();
let combined = stdlib.combined();
for internal_fn in combined.values() {
signatures.insert(internal_fn.name(), internal_fn.to_signature_help());
}
for d in kcl_std.all_docs() {
if let Some(sig) = d.to_signature_help() {
@ -1679,44 +1664,32 @@ pub fn get_signatures_from_stdlib(stdlib: &crate::std::StdLib, kcl_std: &ModData
}
/// Get signatures from our stdlib.
pub fn get_arg_maps_from_stdlib(
stdlib: &crate::std::StdLib,
kcl_std: &ModData,
) -> HashMap<String, HashMap<String, String>> {
pub fn get_arg_maps_from_stdlib(kcl_std: &ModData) -> HashMap<String, HashMap<String, String>> {
let mut result = HashMap::new();
let combined = stdlib.combined();
for internal_fn in combined.values() {
let arg_map: HashMap<String, String> = internal_fn
.args(false)
.into_iter()
for d in kcl_std.all_docs() {
let crate::docs::kcl_doc::DocData::Fn(f) = d else {
continue;
};
let arg_map: HashMap<String, String> = f
.args
.iter()
.map(|data| {
let mut tip = "```\n".to_owned();
tip.push_str(&data.name.clone());
if !data.required {
tip.push('?');
}
if !data.type_.is_empty() {
tip.push_str(": ");
tip.push_str(&data.type_);
}
tip.push_str(&data.to_string());
tip.push_str("\n```");
if !data.description.is_empty() {
if let Some(docs) = &data.docs {
tip.push_str("\n\n");
tip.push_str(&data.description);
tip.push_str(docs);
}
(data.name, tip)
(data.name.clone(), tip)
})
.collect();
if !arg_map.is_empty() {
result.insert(internal_fn.name(), arg_map);
result.insert(f.name.clone(), arg_map);
}
}
for _d in kcl_std.all_docs() {
// TODO add KCL std fns
}
result
}

View File

@ -5,11 +5,10 @@ use tower_lsp::LanguageServer;
// Create a fake kcl lsp server for testing.
pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
let stdlib = crate::std::StdLib::new();
let kcl_std = crate::docs::kcl_doc::walk_prelude();
let stdlib_completions = crate::lsp::kcl::get_completions_from_stdlib(&stdlib, &kcl_std)?;
let stdlib_signatures = crate::lsp::kcl::get_signatures_from_stdlib(&stdlib, &kcl_std);
let stdlib_args = crate::lsp::kcl::get_arg_maps_from_stdlib(&stdlib, &kcl_std);
let stdlib_completions = crate::lsp::kcl::get_completions_from_stdlib(&kcl_std)?;
let stdlib_signatures = crate::lsp::kcl::get_signatures_from_stdlib(&kcl_std);
let stdlib_args = crate::lsp::kcl::get_arg_maps_from_stdlib(&kcl_std);
let zoo_client = crate::engine::new_zoo_client(None, None)?;

View File

@ -928,7 +928,7 @@ startSketchOn(XY)
match hover.unwrap().contents {
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
assert!(value.contains("startSketchOn"));
assert!(value.contains(": SketchSurface"));
assert!(value.contains(": Plane | Face"));
assert!(value.contains("Start a new 2-dimensional sketch on a specific"));
}
_ => unreachable!(),
@ -1113,13 +1113,7 @@ async fn test_kcl_lsp_signature_help() {
"Expected one signature, got {:?}",
signature_help.signatures
);
assert_eq!(
signature_help.signatures[0].label,
r#"startSketchOn(
@planeOrSolid: SketchData,
face?: FaceTag,
): SketchSurface"#
);
assert!(signature_help.signatures[0].label.starts_with("startSketchOn"));
} else {
panic!("Expected signature help");
}
@ -3884,7 +3878,7 @@ startSketchOn(XY)
match hover.unwrap().contents {
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
assert!(value.contains("startSketchOn"));
assert!(value.contains(": SketchSurface"));
assert!(value.contains(": Plane | Face"));
assert!(value.contains("Start a new 2-dimensional sketch on a specific"));
}
_ => unreachable!(),

View File

@ -4,7 +4,7 @@ use crate::parsing::ast::types::{
Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryPart, BodyItem,
CallExpressionKw, DefaultParamVal, ElseIf, Expr, ExpressionStatement, FunctionExpression, FunctionType, Identifier,
IfExpression, ImportItem, ImportSelector, ImportStatement, ItemVisibility, KclNone, LabelledExpression, Literal,
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Name, ObjectExpression, ObjectProperty, Parameter,
LiteralIdentifier, LiteralValue, MemberExpression, Name, ObjectExpression, ObjectProperty, Parameter,
PipeExpression, PipeSubstitution, PrimitiveType, Program, ReturnStatement, TagDeclarator, Type, TypeDeclaration,
UnaryExpression, VariableDeclaration, VariableDeclarator, VariableKind,
};
@ -167,15 +167,6 @@ impl BinaryPart {
}
}
impl MemberObject {
pub fn compute_digest(&mut self) -> Digest {
match self {
MemberObject::MemberExpression(me) => me.compute_digest(),
MemberObject::Identifier(id) => id.compute_digest(),
}
}
}
impl LiteralIdentifier {
pub fn compute_digest(&mut self) -> Digest {
match self {
@ -228,7 +219,7 @@ impl PrimitiveType {
let mut hasher = Sha256::new();
match self {
PrimitiveType::Any => hasher.update(b"any"),
PrimitiveType::Named(id) => hasher.update(id.compute_digest()),
PrimitiveType::Named { id } => hasher.update(id.compute_digest()),
PrimitiveType::String => hasher.update(b"string"),
PrimitiveType::Number(suffix) => hasher.update(suffix.digestable_id()),
PrimitiveType::Boolean => hasher.update(b"bool"),

View File

@ -2,7 +2,7 @@ pub(crate) mod digest;
pub mod types;
use crate::{
parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject},
parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier},
ModuleId,
};
@ -57,15 +57,6 @@ impl BinaryPart {
}
}
impl MemberObject {
pub fn module_id(&self) -> ModuleId {
match self {
MemberObject::MemberExpression(member_expression) => member_expression.module_id,
MemberObject::Identifier(identifier) => identifier.module_id,
}
}
}
impl LiteralIdentifier {
pub fn module_id(&self) -> ModuleId {
match self {

View File

@ -25,7 +25,6 @@ pub use crate::parsing::ast::types::{
none::KclNone,
};
use crate::{
docs::StdLibFn,
errors::KclError,
execution::{
annotations,
@ -454,7 +453,7 @@ impl Node<Program> {
alpha: c.a,
},
};
if colors.borrow().iter().any(|c| *c == color) {
if colors.borrow().contains(&color) {
return;
}
colors.borrow_mut().push(color);
@ -529,7 +528,7 @@ impl Node<Program> {
let new_color = csscolorparser::Color::new(color.red, color.green, color.blue, color.alpha);
Ok(Some(ColorPresentation {
// The label will be what they replace the color with.
label: new_color.to_hex_string(),
label: new_color.to_css_hex(),
text_edit: None,
additional_text_edits: None,
}))
@ -1973,31 +1972,6 @@ impl CallExpressionKw {
}
}
/// A function declaration.
#[derive(Debug, Clone, Default, Serialize, Deserialize, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum Function {
/// A stdlib function written in Rust (aka core lib).
StdLib {
/// The function.
func: Box<dyn StdLibFn>,
},
/// A function that is defined in memory.
#[default]
InMemory,
}
impl PartialEq for Function {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Function::StdLib { func: func1 }, Function::StdLib { func: func2 }) => func1.name() == func2.name(),
(Function::InMemory, Function::InMemory) => true,
_ => false,
}
}
}
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
@ -2782,47 +2756,6 @@ impl ObjectProperty {
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum MemberObject {
MemberExpression(BoxNode<MemberExpression>),
Identifier(BoxNode<Identifier>),
}
impl MemberObject {
pub fn start(&self) -> usize {
match self {
MemberObject::MemberExpression(member_expression) => member_expression.start,
MemberObject::Identifier(identifier) => identifier.start,
}
}
pub fn end(&self) -> usize {
match self {
MemberObject::MemberExpression(member_expression) => member_expression.end,
MemberObject::Identifier(identifier) => identifier.end,
}
}
pub(crate) fn contains_range(&self, range: &SourceRange) -> bool {
let sr = SourceRange::from(self);
sr.contains_range(range)
}
}
impl From<MemberObject> for SourceRange {
fn from(obj: MemberObject) -> Self {
Self::new(obj.start(), obj.end(), obj.module_id())
}
}
impl From<&MemberObject> for SourceRange {
fn from(obj: &MemberObject) -> Self {
Self::new(obj.start(), obj.end(), obj.module_id())
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
@ -2868,7 +2801,7 @@ impl From<&LiteralIdentifier> for SourceRange {
#[ts(export)]
#[serde(tag = "type")]
pub struct MemberExpression {
pub object: MemberObject,
pub object: Expr,
pub property: LiteralIdentifier,
pub computed: bool,
@ -2890,12 +2823,7 @@ impl Node<MemberExpression> {
impl MemberExpression {
/// Rename all identifiers that have the old name to the new given name.
fn rename_identifiers(&mut self, old_name: &str, new_name: &str) {
match &mut self.object {
MemberObject::MemberExpression(ref mut member_expression) => {
member_expression.rename_identifiers(old_name, new_name)
}
MemberObject::Identifier(ref mut identifier) => identifier.rename(old_name, new_name),
}
self.object.rename_identifiers(old_name, new_name);
match &mut self.property {
LiteralIdentifier::Identifier(ref mut identifier) => identifier.rename(old_name, new_name),
@ -3230,7 +3158,7 @@ pub enum PrimitiveType {
/// `fn`, type of functions.
Function(FunctionType),
/// An identifier used as a type (not really a primitive type, but whatever).
Named(Node<Identifier>),
Named { id: Node<Identifier> },
}
impl PrimitiveType {
@ -3286,7 +3214,7 @@ impl fmt::Display for PrimitiveType {
}
Ok(())
}
PrimitiveType::Named(n) => write!(f, "{}", n.name),
PrimitiveType::Named { id: n } => write!(f, "{}", n.name),
}
}
}

View File

@ -1,6 +1,6 @@
use serde::Serialize;
use super::{BodyItem, Expr, MemberObject, Node, Program};
use super::{BodyItem, Expr, Node, Program};
use crate::SourceRange;
/// A traversal path through the AST to a node.
@ -60,6 +60,20 @@ pub enum Step {
}
impl NodePath {
/// Placeholder for when the AST isn't available to create a real path. It
/// will be filled in later.
pub(crate) fn placeholder() -> Self {
Self::default()
}
#[cfg(feature = "artifact-graph")]
pub(crate) fn fill_placeholder(&mut self, program: &Node<Program>, cached_body_items: usize, range: SourceRange) {
if !self.is_empty() {
return;
}
*self = Self::from_range(program, cached_body_items, range).unwrap_or_default();
}
/// Given a program and a [`SourceRange`], return the path to the node that
/// contains the range.
pub(crate) fn from_range(program: &Node<Program>, cached_body_items: usize, range: SourceRange) -> Option<Self> {
@ -248,7 +262,7 @@ impl NodePath {
Expr::MemberExpression(node) => {
if node.object.contains_range(&range) {
path.push(Step::MemberExpressionObject);
return Self::from_member_expr_object(&node.object, range, path);
return NodePath::from_expr(&node.object, range, path);
}
if node.property.contains_range(&range) {
path.push(Step::MemberExpressionProperty);
@ -313,18 +327,6 @@ impl NodePath {
Some(path)
}
fn from_member_expr_object(mut expr: &MemberObject, range: SourceRange, mut path: NodePath) -> Option<NodePath> {
while let MemberObject::MemberExpression(node) = expr {
if !node.object.contains_range(&range) {
break;
}
path.push(Step::MemberExpressionObject);
expr = &node.object;
}
Some(path)
}
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
@ -402,4 +404,19 @@ mod tests {
}
);
}
#[test]
fn test_node_path_from_range_import() {
let code = r#"import "cube.step" as cube
import "cylinder.kcl" as cylinder
"#;
let program = crate::Program::parse_no_errs(code).unwrap();
// The entire cylinder import statement.
assert_eq!(
NodePath::from_range(&program.ast, 0, range(27, 60)).unwrap(),
NodePath {
steps: vec![Step::ProgramBodyItem { index: 1 }],
}
);
}
}

View File

@ -26,8 +26,8 @@ use crate::{
Annotation, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem,
BoxNode, CallExpressionKw, CommentStyle, DefaultParamVal, ElseIf, Expr, ExpressionStatement,
FunctionExpression, FunctionType, Identifier, IfExpression, ImportItem, ImportSelector, ImportStatement,
ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Name,
Node, NodeList, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter,
ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue, MemberExpression, Name, Node,
NodeList, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter,
PipeExpression, PipeSubstitution, PrimitiveType, Program, ReturnStatement, Shebang, TagDeclarator, Type,
TypeDeclaration, UnaryExpression, UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
},
@ -1326,10 +1326,7 @@ fn member_expression_subscript(i: &mut TokenSlice) -> ModalResult<(LiteralIdenti
/// Get a property of an object, or an index of an array, or a member of a collection.
/// Can be arbitrarily nested, e.g. `people[i]['adam'].age`.
fn member_expression(i: &mut TokenSlice) -> ModalResult<Node<MemberExpression>> {
// This is an identifier, followed by a sequence of members (aka properties)
// First, the identifier.
let id = nameable_identifier.context(expected("the identifier of the object whose property you're trying to access, e.g. in 'shape.size.width', 'shape' is the identifier")).parse_next(i)?;
fn build_member_expression(object: Expr, i: &mut TokenSlice) -> ModalResult<Node<MemberExpression>> {
// Now a sequence of members.
let member = alt((member_expression_dot, member_expression_subscript)).context(expected("a member/property, e.g. size.x and size['height'] and size[0] are all different ways to access a member/property of 'size'"));
let mut members: Vec<_> = repeat(1.., member)
@ -1340,11 +1337,11 @@ fn member_expression(i: &mut TokenSlice) -> ModalResult<Node<MemberExpression>>
// It's safe to call remove(0), because the vec is created from repeat(1..),
// which is guaranteed to have >=1 elements.
let (property, end, computed) = members.remove(0);
let start = id.start;
let module_id = id.module_id;
let start = object.start();
let module_id = object.module_id();
let initial_member_expression = Node::new(
MemberExpression {
object: MemberObject::Identifier(Box::new(id)),
object,
computed,
property,
digest: None,
@ -1362,7 +1359,7 @@ fn member_expression(i: &mut TokenSlice) -> ModalResult<Node<MemberExpression>>
.fold(initial_member_expression, |accumulated, (property, end, computed)| {
Node::new(
MemberExpression {
object: MemberObject::MemberExpression(Box::new(accumulated)),
object: Expr::MemberExpression(Box::new(accumulated)),
computed,
property,
digest: None,
@ -2085,8 +2082,7 @@ fn unnecessarily_bracketed(i: &mut TokenSlice) -> ModalResult<Expr> {
}
fn expr_allowed_in_pipe_expr(i: &mut TokenSlice) -> ModalResult<Expr> {
alt((
member_expression.map(Box::new).map(Expr::MemberExpression),
let parsed_expr = alt((
bool_value.map(Box::new).map(Expr::Literal),
tag.map(Box::new).map(Expr::TagDeclarator),
literal.map(Expr::Literal),
@ -2100,14 +2096,19 @@ fn expr_allowed_in_pipe_expr(i: &mut TokenSlice) -> ModalResult<Expr> {
unnecessarily_bracketed,
))
.context(expected("a KCL expression (but not a pipe expression)"))
.parse_next(i)
.parse_next(i)?;
let maybe_member = build_member_expression(parsed_expr.clone(), i);
if let Ok(mem) = maybe_member {
return Ok(Expr::MemberExpression(Box::new(mem)));
}
Ok(parsed_expr)
}
fn possible_operands(i: &mut TokenSlice) -> ModalResult<Expr> {
let mut expr = alt((
unary_expression.map(Box::new).map(Expr::UnaryExpression),
bool_value.map(Box::new).map(Expr::Literal),
member_expression.map(Box::new).map(Expr::MemberExpression),
literal.map(Expr::Literal),
fn_call_kw.map(Box::new).map(Expr::CallExpressionKw),
name.map(Box::new).map(Expr::Name),
@ -2118,6 +2119,10 @@ fn possible_operands(i: &mut TokenSlice) -> ModalResult<Expr> {
"a KCL value which can be used as an argument/operand to an operator",
))
.parse_next(i)?;
let maybe_member = build_member_expression(expr.clone(), i);
if let Ok(mem) = maybe_member {
expr = Expr::MemberExpression(Box::new(mem));
}
let ty = opt((colon, opt(whitespace), type_)).parse_next(i)?;
if let Some((_, _, ty)) = ty {
@ -2480,32 +2485,13 @@ impl TryFrom<Token> for Node<TagDeclarator> {
}
}
impl Node<TagDeclarator> {
fn into_valid_binding_name(self) -> Result<Self, CompilationError> {
// Make sure they are not assigning a variable to a stdlib function.
if crate::std::name_in_stdlib(&self.name) {
return Err(CompilationError::fatal(
SourceRange::from(&self),
format!("Cannot assign a tag to a reserved keyword: {}", self.name),
));
}
Ok(self)
}
}
/// Parse a Kcl tag that starts with a `$`.
fn tag(i: &mut TokenSlice) -> ModalResult<Node<TagDeclarator>> {
dollar.parse_next(i)?;
let tag_declarator = any
.try_map(Node::<TagDeclarator>::try_from)
any.try_map(Node::<TagDeclarator>::try_from)
.context(expected("a tag, e.g. '$seg01' or '$line01'"))
.parse_next(i)
.map_err(|e: ErrMode<ContextError>| e.cut())?;
// Now that we've parsed a tag declarator, verify that it's not a stdlib
// name. If it is, stop backtracking.
tag_declarator
.into_valid_binding_name()
.map_err(|e| ErrMode::Cut(ContextError::from(e)))
.map_err(|e: ErrMode<ContextError>| e.cut())
}
/// Helper function. Matches any number of whitespace tokens and ignores them.
@ -2938,7 +2924,7 @@ fn primitive_type(i: &mut TokenSlice) -> ModalResult<Node<PrimitiveType>> {
(identifier, opt(delimited(open_paren, uom_for_type, close_paren))).map(|(ident, suffix)| {
let mut result = Node::new(PrimitiveType::Boolean, ident.start, ident.end, ident.module_id);
result.inner =
PrimitiveType::primitive_from_str(&ident.name, suffix).unwrap_or(PrimitiveType::Named(ident));
PrimitiveType::primitive_from_str(&ident.name, suffix).unwrap_or(PrimitiveType::Named { id: ident });
result
}),
))
@ -3277,7 +3263,7 @@ fn fn_call_kw(i: &mut TokenSlice) -> ModalResult<Node<CallExpressionKw>> {
return Err(ErrMode::Cut(
CompilationError::fatal(
SourceRange::from(&tok),
format!("There was an unexpected {}. Try removing it.", tok.value),
format!("There was an unexpected `{}`. Try removing it.", tok.value),
)
.into(),
));
@ -4446,7 +4432,7 @@ z(-[["#,
assert_err(
r#"z
(--#"#,
"There was an unexpected -. Try removing it.",
"There was an unexpected `-`. Try removing it.",
[3, 4],
);
}
@ -4898,19 +4884,6 @@ let myBox = box(p=[0,0], h=-3, l=-16, w=-10)
}
}
#[test]
fn test_parse_tag_named_std_lib() {
let some_program_string = r#"startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(%, end = [5, 5], tag = $xLine)
"#;
assert_err(
some_program_string,
"Cannot assign a tag to a reserved keyword: xLine",
[86, 92],
);
}
#[test]
fn test_parse_empty_tag_brace() {
let some_program_string = r#"startSketchOn(XY)
@ -5029,6 +5002,17 @@ type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
assert_no_err(some_program_string);
}
#[test]
fn test_parse_fn_call_then_field() {
let some_program_string = "myFunction().field";
let module_id = ModuleId::default();
let tokens = crate::parsing::token::lex(some_program_string, module_id).unwrap(); // Updated import path
let actual = expression.parse(tokens.as_slice()).unwrap();
let Expr::MemberExpression(_expr) = actual else {
panic!("expected member expression")
};
}
#[test]
fn test_parse_array_missing_closing_bracket() {
let some_program_string = r#"
@ -5217,7 +5201,7 @@ bar = 1
.cause
.as_ref()
.expect("Found an error, but there was no cause. Add a cause.");
assert_eq!(cause.message, "There was an unexpected }. Try removing it.",);
assert_eq!(cause.message, "There was an unexpected `}`. Try removing it.",);
assert_eq!(cause.source_range.start(), expected_src_start);
}

View File

@ -117,12 +117,20 @@ expression: actual
"computed": false,
"end": 45,
"object": {
"abs_path": false,
"commentStart": 40,
"end": 43,
"name": "obj",
"name": {
"commentStart": 40,
"end": 43,
"name": "obj",
"start": 40,
"type": "Identifier"
},
"path": [],
"start": 40,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 44,

View File

@ -117,12 +117,20 @@ expression: actual
"computed": false,
"end": 49,
"object": {
"abs_path": false,
"commentStart": 41,
"end": 44,
"name": "obj",
"name": {
"commentStart": 41,
"end": 44,
"name": "obj",
"start": 41,
"type": "Identifier"
},
"path": [],
"start": 41,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 45,

View File

@ -104,12 +104,20 @@ expression: actual
"computed": false,
"end": 44,
"object": {
"abs_path": false,
"commentStart": 36,
"end": 39,
"name": "obj",
"name": {
"commentStart": 36,
"end": 39,
"name": "obj",
"start": 36,
"type": "Identifier"
},
"path": [],
"start": 36,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 40,

View File

@ -120,12 +120,20 @@ expression: actual
"computed": false,
"end": 49,
"object": {
"abs_path": false,
"commentStart": 41,
"end": 44,
"name": "obj",
"name": {
"commentStart": 41,
"end": 44,
"name": "obj",
"start": 41,
"type": "Identifier"
},
"path": [],
"start": 41,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 45,

View File

@ -107,12 +107,20 @@ expression: actual
"computed": false,
"end": 45,
"object": {
"abs_path": false,
"commentStart": 37,
"end": 40,
"name": "obj",
"name": {
"commentStart": 37,
"end": 40,
"name": "obj",
"start": 37,
"type": "Identifier"
},
"path": [],
"start": 37,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 41,

View File

@ -107,12 +107,20 @@ expression: actual
"computed": false,
"end": 44,
"object": {
"abs_path": false,
"commentStart": 36,
"end": 39,
"name": "obj",
"name": {
"commentStart": 36,
"end": 39,
"name": "obj",
"start": 36,
"type": "Identifier"
},
"path": [],
"start": 36,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 40,

View File

@ -37,12 +37,20 @@ expression: actual
"computed": false,
"end": 18,
"object": {
"abs_path": false,
"commentStart": 13,
"end": 16,
"name": "obj",
"name": {
"commentStart": 13,
"end": 16,
"name": "obj",
"start": 13,
"type": "Identifier"
},
"path": [],
"start": 13,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 17,

View File

@ -24,12 +24,20 @@ expression: actual
"computed": false,
"end": 19,
"object": {
"abs_path": false,
"commentStart": 11,
"end": 14,
"name": "obj",
"name": {
"commentStart": 11,
"end": 14,
"name": "obj",
"start": 11,
"type": "Identifier"
},
"path": [],
"start": 11,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 15,

View File

@ -101,12 +101,20 @@ expression: actual
"computed": false,
"end": 44,
"object": {
"abs_path": false,
"commentStart": 36,
"end": 39,
"name": "obj",
"name": {
"commentStart": 36,
"end": 39,
"name": "obj",
"start": 36,
"type": "Identifier"
},
"path": [],
"start": 36,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 40,

View File

@ -25,12 +25,20 @@ expression: actual
"computed": false,
"end": 16,
"object": {
"abs_path": false,
"commentStart": 7,
"end": 9,
"name": "yo",
"name": {
"commentStart": 7,
"end": 9,
"name": "yo",
"start": 7,
"type": "Identifier"
},
"path": [],
"start": 7,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 10,

View File

@ -21,12 +21,20 @@ expression: actual
"computed": true,
"end": 11,
"object": {
"abs_path": false,
"commentStart": 6,
"end": 8,
"name": "b1",
"name": {
"commentStart": 6,
"end": 8,
"name": "b1",
"start": 6,
"type": "Identifier"
},
"path": [],
"start": 6,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 9,

View File

@ -33,12 +33,20 @@ expression: actual
"computed": false,
"end": 13,
"object": {
"abs_path": false,
"commentStart": 7,
"end": 9,
"name": "yo",
"name": {
"commentStart": 7,
"end": 9,
"name": "yo",
"start": 7,
"type": "Identifier"
},
"path": [],
"start": 7,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 10,

View File

@ -21,12 +21,20 @@ expression: actual
"computed": false,
"end": 11,
"object": {
"abs_path": false,
"commentStart": 6,
"end": 8,
"name": "b1",
"name": {
"commentStart": 6,
"end": 8,
"name": "b1",
"start": 6,
"type": "Identifier"
},
"path": [],
"start": 6,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 9,

View File

@ -21,12 +21,20 @@ expression: actual
"computed": false,
"end": 16,
"object": {
"abs_path": false,
"commentStart": 6,
"end": 8,
"name": "b1",
"name": {
"commentStart": 6,
"end": 8,
"name": "b1",
"start": 6,
"type": "Identifier"
},
"path": [],
"start": 6,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 9,

View File

@ -21,12 +21,20 @@ expression: actual
"computed": false,
"end": 13,
"object": {
"abs_path": false,
"commentStart": 6,
"end": 8,
"name": "b1",
"name": {
"commentStart": 6,
"end": 8,
"name": "b1",
"start": 6,
"type": "Identifier"
},
"path": [],
"start": 6,
"type": "Identifier",
"type": "Identifier"
"type": "Name",
"type": "Name"
},
"property": {
"commentStart": 9,

View File

@ -163,7 +163,7 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
// Run the program.
let exec_res = crate::test_server::execute_and_snapshot_ast(ast, Some(test.entry_point.clone()), export_step).await;
match exec_res {
Ok((exec_state, env_ref, png, step)) => {
Ok((exec_state, ctx, env_ref, png, step)) => {
let fail_path = test.output_dir.join("execution_error.snap");
if std::fs::exists(&fail_path).unwrap() {
panic!("This test case is expected to fail, but it passed. If this is intended, and the test should actually be passing now, please delete kcl-lib/{}", fail_path.to_string_lossy())
@ -181,7 +181,7 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
panic!("Step data was empty");
}
}
let outcome = exec_state.to_exec_outcome(env_ref).await;
let outcome = exec_state.to_exec_outcome(env_ref, &ctx).await;
let mem_result = catch_unwind(AssertUnwindSafe(|| {
assert_snapshot(test, "Variables in memory after executing", || {
@ -3483,3 +3483,66 @@ mod spheres {
super::execute(TEST_NAME, true).await
}
}
mod var_ref_in_own_def {
const TEST_NAME: &str = "var_ref_in_own_def";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod ascription_unknown_type {
const TEST_NAME: &str = "ascription_unknown_type";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod var_ref_in_own_def_decl {
const TEST_NAME: &str = "var_ref_in_own_def_decl";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}

View File

@ -94,6 +94,11 @@ impl SourceRange {
ModuleId::from_usize(self.0[2])
}
/// True if this source range is from the top-level module.
pub fn is_top_level_module(&self) -> bool {
self.module_id().is_top_level()
}
/// Check if the range contains a position.
pub fn contains(&self, pos: usize) -> bool {
pos >= self.start() && pos <= self.end()

View File

@ -143,9 +143,14 @@ impl Args {
where
T: for<'a> FromKclValue<'a>,
{
if self.kw_args.labeled.get(label).is_none() {
return Ok(None);
};
match self.kw_args.labeled.get(label) {
None => return Ok(None),
Some(a) => {
if let KclValue::KclNone { .. } = &a.value {
return Ok(None);
}
}
}
self.get_kw_arg_typed(label, ty, exec_state).map(Some)
}
@ -174,7 +179,7 @@ impl Args {
{
let Some(arg) = self.kw_args.labeled.get(label) else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!("This function requires a keyword argument '{label}'"),
format!("This function requires a keyword argument `{label}`"),
vec![self.source_range],
)));
};
@ -186,7 +191,7 @@ impl Args {
.map(|t| t.to_string())
.unwrap_or_else(|| arg.value.human_friendly_type().to_owned());
let msg_base = format!(
"This function expected the input argument to be {} but it's actually of type {actual_type_name}",
"This function expected its `{label}` argument to be {} but it's actually of type {actual_type_name}",
ty.human_friendly_type(),
);
let suggestion = match (ty, actual_type) {

View File

@ -23,7 +23,7 @@ pub async fn union(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
if solids.len() < 2 {
return Err(KclError::new_undefined_value(KclErrorDetails::new(
return Err(KclError::new_semantic(KclErrorDetails::new(
"At least two solids are required for a union operation.".to_string(),
vec![args.source_range],
)));
@ -88,7 +88,7 @@ pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValu
let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
if solids.len() < 2 {
return Err(KclError::new_undefined_value(KclErrorDetails::new(
return Err(KclError::new_semantic(KclErrorDetails::new(
"At least two solids are required for an intersect operation.".to_string(),
vec![args.source_range],
)));

View File

@ -28,14 +28,10 @@ pub mod utils;
use anyhow::Result;
pub use args::Args;
use indexmap::IndexMap;
use lazy_static::lazy_static;
use crate::{
docs::StdLibFn,
errors::KclError,
execution::{types::PrimitiveType, ExecState, KclValue},
parsing::ast::types::Name,
};
pub type StdFn = fn(
@ -43,46 +39,6 @@ pub type StdFn = fn(
Args,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<KclValue, KclError>> + Send + '_>>;
lazy_static! {
static ref CORE_FNS: Vec<Box<dyn StdLibFn>> = vec![
Box::new(crate::std::segment::SegEnd),
Box::new(crate::std::segment::SegEndX),
Box::new(crate::std::segment::SegEndY),
Box::new(crate::std::segment::SegStart),
Box::new(crate::std::segment::SegStartX),
Box::new(crate::std::segment::SegStartY),
Box::new(crate::std::segment::LastSegX),
Box::new(crate::std::segment::LastSegY),
Box::new(crate::std::segment::SegLen),
Box::new(crate::std::segment::SegAng),
Box::new(crate::std::segment::TangentToEnd),
Box::new(crate::std::sketch::InvoluteCircular),
Box::new(crate::std::sketch::Line),
Box::new(crate::std::sketch::XLine),
Box::new(crate::std::sketch::YLine),
Box::new(crate::std::sketch::AngledLine),
Box::new(crate::std::sketch::AngledLineThatIntersects),
Box::new(crate::std::sketch::StartSketchOn),
Box::new(crate::std::sketch::StartProfile),
Box::new(crate::std::sketch::ProfileStartX),
Box::new(crate::std::sketch::ProfileStartY),
Box::new(crate::std::sketch::ProfileStart),
Box::new(crate::std::sketch::Close),
Box::new(crate::std::sketch::Arc),
Box::new(crate::std::sketch::TangentialArc),
Box::new(crate::std::sketch::BezierCurve),
Box::new(crate::std::sketch::Subtract2D),
];
}
pub fn name_in_stdlib(name: &str) -> bool {
CORE_FNS.iter().any(|f| f.name() == name)
}
pub fn get_stdlib_fn(name: &str) -> Option<Box<dyn StdLibFn>> {
CORE_FNS.iter().find(|f| f.name() == name).cloned()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StdFnProps {
pub name: String,
@ -206,7 +162,7 @@ pub(crate) fn std_fn(path: &str, fn_name: &str) -> (crate::std::StdFn, StdFnProp
),
("transform", "mirror2d") => (
|e, a| Box::pin(crate::std::mirror::mirror_2d(e, a)),
StdFnProps::default("std::transform::mirror2d"),
StdFnProps::default("std::transform::mirror2d").include_in_feature_tree(),
),
("transform", "translate") => (
|e, a| Box::pin(crate::std::transform::translate(e, a)),
@ -352,6 +308,114 @@ pub(crate) fn std_fn(path: &str, fn_name: &str) -> (crate::std::StdFn, StdFnProp
|e, a| Box::pin(crate::std::patterns::pattern_circular_2d(e, a)),
StdFnProps::default("std::sketch::patternCircular2d"),
),
("sketch", "segEnd") => (
|e, a| Box::pin(crate::std::segment::segment_end(e, a)),
StdFnProps::default("std::sketch::segEnd"),
),
("sketch", "segEndX") => (
|e, a| Box::pin(crate::std::segment::segment_end_x(e, a)),
StdFnProps::default("std::sketch::segEndX"),
),
("sketch", "segEndY") => (
|e, a| Box::pin(crate::std::segment::segment_end_y(e, a)),
StdFnProps::default("std::sketch::segEndY"),
),
("sketch", "segStart") => (
|e, a| Box::pin(crate::std::segment::segment_start(e, a)),
StdFnProps::default("std::sketch::segStart"),
),
("sketch", "segStartX") => (
|e, a| Box::pin(crate::std::segment::segment_start_x(e, a)),
StdFnProps::default("std::sketch::segStartX"),
),
("sketch", "segStartY") => (
|e, a| Box::pin(crate::std::segment::segment_start_y(e, a)),
StdFnProps::default("std::sketch::segStartY"),
),
("sketch", "lastSegX") => (
|e, a| Box::pin(crate::std::segment::last_segment_x(e, a)),
StdFnProps::default("std::sketch::lastSegX"),
),
("sketch", "lastSegY") => (
|e, a| Box::pin(crate::std::segment::last_segment_y(e, a)),
StdFnProps::default("std::sketch::lastSegY"),
),
("sketch", "segLen") => (
|e, a| Box::pin(crate::std::segment::segment_length(e, a)),
StdFnProps::default("std::sketch::segLen"),
),
("sketch", "segAng") => (
|e, a| Box::pin(crate::std::segment::segment_angle(e, a)),
StdFnProps::default("std::sketch::segAng"),
),
("sketch", "tangentToEnd") => (
|e, a| Box::pin(crate::std::segment::tangent_to_end(e, a)),
StdFnProps::default("std::sketch::tangentToEnd"),
),
("sketch", "profileStart") => (
|e, a| Box::pin(crate::std::sketch::profile_start(e, a)),
StdFnProps::default("std::sketch::profileStart"),
),
("sketch", "profileStartX") => (
|e, a| Box::pin(crate::std::sketch::profile_start_x(e, a)),
StdFnProps::default("std::sketch::profileStartX"),
),
("sketch", "profileStartY") => (
|e, a| Box::pin(crate::std::sketch::profile_start_y(e, a)),
StdFnProps::default("std::sketch::profileStartY"),
),
("sketch", "startSketchOn") => (
|e, a| Box::pin(crate::std::sketch::start_sketch_on(e, a)),
StdFnProps::default("std::sketch::startSketchOn").include_in_feature_tree(),
),
("sketch", "startProfile") => (
|e, a| Box::pin(crate::std::sketch::start_profile(e, a)),
StdFnProps::default("std::sketch::startProfile"),
),
("sketch", "involuteCircular") => (
|e, a| Box::pin(crate::std::sketch::involute_circular(e, a)),
StdFnProps::default("std::sketch::startProfile"),
),
("sketch", "line") => (
|e, a| Box::pin(crate::std::sketch::line(e, a)),
StdFnProps::default("std::sketch::startProfile"),
),
("sketch", "xLine") => (
|e, a| Box::pin(crate::std::sketch::x_line(e, a)),
StdFnProps::default("std::sketch::startProfile"),
),
("sketch", "yLine") => (
|e, a| Box::pin(crate::std::sketch::y_line(e, a)),
StdFnProps::default("std::sketch::startProfile"),
),
("sketch", "angledLine") => (
|e, a| Box::pin(crate::std::sketch::angled_line(e, a)),
StdFnProps::default("std::sketch::startProfile"),
),
("sketch", "angledLineThatIntersects") => (
|e, a| Box::pin(crate::std::sketch::angled_line_that_intersects(e, a)),
StdFnProps::default("std::sketch::startProfile"),
),
("sketch", "close") => (
|e, a| Box::pin(crate::std::sketch::close(e, a)),
StdFnProps::default("std::sketch::startProfile"),
),
("sketch", "arc") => (
|e, a| Box::pin(crate::std::sketch::arc(e, a)),
StdFnProps::default("std::sketch::startProfile"),
),
("sketch", "tangentialArc") => (
|e, a| Box::pin(crate::std::sketch::tangential_arc(e, a)),
StdFnProps::default("std::sketch::startProfile"),
),
("sketch", "bezierCurve") => (
|e, a| Box::pin(crate::std::sketch::bezier_curve(e, a)),
StdFnProps::default("std::sketch::startProfile"),
),
("sketch", "subtract2d") => (
|e, a| Box::pin(crate::std::sketch::subtract_2d(e, a)),
StdFnProps::default("std::sketch::startProfile").include_in_feature_tree(),
),
("appearance", "hexString") => (
|e, a| Box::pin(crate::std::appearance::hex_string(e, a)),
StdFnProps::default("std::appearance::hexString"),
@ -376,56 +440,5 @@ pub(crate) fn std_ty(path: &str, fn_name: &str) -> (PrimitiveType, StdFnProps) {
}
}
pub struct StdLib {
pub fns: IndexMap<String, Box<dyn StdLibFn>>,
}
impl std::fmt::Debug for StdLib {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StdLib").field("fns.len()", &self.fns.len()).finish()
}
}
impl StdLib {
pub fn new() -> Self {
let fns = CORE_FNS
.clone()
.into_iter()
.map(|internal_fn| (internal_fn.name(), internal_fn))
.collect();
Self { fns }
}
// Get the combined hashmaps.
pub fn combined(&self) -> IndexMap<String, Box<dyn StdLibFn>> {
self.fns.clone()
}
pub fn get(&self, name: &str) -> Option<Box<dyn StdLibFn>> {
self.fns.get(name).cloned()
}
pub fn get_rust_function(&self, name: &Name) -> Option<Box<dyn StdLibFn>> {
if let Some(name) = name.local_ident() {
if let Some(f) = self.get(name.inner) {
return Some(f);
}
}
None
}
pub fn contains_key(&self, key: &str) -> bool {
self.fns.contains_key(key)
}
}
impl Default for StdLib {
fn default() -> Self {
Self::new()
}
}
/// The default tolerance for modeling commands in [`kittycad_modeling_cmds::length_unit::LengthUnit`].
const DEFAULT_TOLERANCE: f64 = 0.0000001;

View File

@ -1,7 +1,6 @@
//! Functions related to line segments.
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kittycad_modeling_cmds::shared::Angle;
use super::utils::untype_point;
@ -22,39 +21,6 @@ pub async fn segment_end(exec_state: &mut ExecState, args: Args) -> Result<KclVa
args.make_kcl_val_from_point([pt[0].n, pt[1].n], pt[0].ty.clone())
}
/// Compute the ending point of the provided line segment.
///
/// ```no_run
/// w = 15
/// cube = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> line(end = [w, 0], tag = $line1)
/// |> line(end = [0, w], tag = $line2)
/// |> line(end = [-w, 0], tag = $line3)
/// |> line(end = [0, -w], tag = $line4)
/// |> close()
/// |> extrude(length = 5)
///
/// fn cylinder(radius, tag) {
/// return startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> circle(radius = radius, center = segEnd(tag) )
/// |> extrude(length = radius)
/// }
///
/// cylinder(radius = 1, tag = line1)
/// cylinder(radius = 2, tag = line2)
/// cylinder(radius = 3, tag = line3)
/// cylinder(radius = 4, tag = line4)
/// ```
#[stdlib {
name = "segEnd",
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
},
tags = ["sketch"]
}]
fn inner_segment_end(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<[TyF64; 2], KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
@ -78,27 +44,6 @@ pub async fn segment_end_x(exec_state: &mut ExecState, args: Args) -> Result<Kcl
Ok(args.make_user_val_from_f64_with_type(result))
}
/// Compute the ending point of the provided line segment along the 'x' axis.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [20, 0], tag = $thing)
/// |> line(end = [0, 5])
/// |> line(end = [segEndX(thing), 0])
/// |> line(end = [-20, 10])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "segEndX",
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
},
tags = ["sketch"]
}]
fn inner_segment_end_x(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<TyF64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
@ -119,28 +64,6 @@ pub async fn segment_end_y(exec_state: &mut ExecState, args: Args) -> Result<Kcl
Ok(args.make_user_val_from_f64_with_type(result))
}
/// Compute the ending point of the provided line segment along the 'y' axis.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [20, 0])
/// |> line(end = [0, 3], tag = $thing)
/// |> line(end = [-10, 0])
/// |> line(end = [0, segEndY(thing)])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "segEndY",
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
},
tags = ["sketch"]
}]
fn inner_segment_end_y(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<TyF64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
@ -161,39 +84,6 @@ pub async fn segment_start(exec_state: &mut ExecState, args: Args) -> Result<Kcl
args.make_kcl_val_from_point([pt[0].n, pt[1].n], pt[0].ty.clone())
}
/// Compute the starting point of the provided line segment.
///
/// ```no_run
/// w = 15
/// cube = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> line(end = [w, 0], tag = $line1)
/// |> line(end = [0, w], tag = $line2)
/// |> line(end = [-w, 0], tag = $line3)
/// |> line(end = [0, -w], tag = $line4)
/// |> close()
/// |> extrude(length = 5)
///
/// fn cylinder(radius, tag) {
/// return startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> circle( radius = radius, center = segStart(tag) )
/// |> extrude(length = radius)
/// }
///
/// cylinder(radius = 1, tag = line1)
/// cylinder(radius = 2, tag = line2)
/// cylinder(radius = 3, tag = line3)
/// cylinder(radius = 4, tag = line4)
/// ```
#[stdlib {
name = "segStart",
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
},
tags = ["sketch"]
}]
fn inner_segment_start(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<[TyF64; 2], KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
@ -217,27 +107,6 @@ pub async fn segment_start_x(exec_state: &mut ExecState, args: Args) -> Result<K
Ok(args.make_user_val_from_f64_with_type(result))
}
/// Compute the starting point of the provided line segment along the 'x' axis.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [20, 0], tag = $thing)
/// |> line(end = [0, 5])
/// |> line(end = [20 - segStartX(thing), 0])
/// |> line(end = [-20, 10])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "segStartX",
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
},
tags = ["sketch"]
}]
fn inner_segment_start_x(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<TyF64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
@ -258,28 +127,6 @@ pub async fn segment_start_y(exec_state: &mut ExecState, args: Args) -> Result<K
Ok(args.make_user_val_from_f64_with_type(result))
}
/// Compute the starting point of the provided line segment along the 'y' axis.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [20, 0])
/// |> line(end = [0, 3], tag = $thing)
/// |> line(end = [-10, 0])
/// |> line(end = [0, 20-segStartY(thing)])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "segStartY",
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
},
tags = ["sketch"]
}]
fn inner_segment_start_y(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<TyF64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
@ -300,28 +147,6 @@ pub async fn last_segment_x(exec_state: &mut ExecState, args: Args) -> Result<Kc
Ok(args.make_user_val_from_f64_with_type(result))
}
/// Extract the 'x' axis value of the last line segment in the provided 2-d
/// sketch.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [5, 0])
/// |> line(end = [20, 5])
/// |> line(end = [lastSegX(%), 0])
/// |> line(end = [-15, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "lastSegX",
unlabeled_first = true,
args = {
sketch = { docs = "The sketch whose line segment is being queried"},
},
tags = ["sketch"]
}]
fn inner_last_segment_x(sketch: Sketch, args: Args) -> Result<TyF64, KclError> {
let last_line = sketch
.paths
@ -346,28 +171,6 @@ pub async fn last_segment_y(exec_state: &mut ExecState, args: Args) -> Result<Kc
Ok(args.make_user_val_from_f64_with_type(result))
}
/// Extract the 'y' axis value of the last line segment in the provided 2-d
/// sketch.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [5, 0])
/// |> line(end = [20, 5])
/// |> line(end = [0, lastSegY(%)])
/// |> line(end = [-15, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "lastSegY",
unlabeled_first = true,
args = {
sketch = { docs = "The sketch whose line segment is being queried"},
},
tags = ["sketch"]
}]
fn inner_last_segment_y(sketch: Sketch, args: Args) -> Result<TyF64, KclError> {
let last_line = sketch
.paths
@ -390,33 +193,6 @@ pub async fn segment_length(exec_state: &mut ExecState, args: Args) -> Result<Kc
Ok(args.make_user_val_from_f64_with_type(result))
}
/// Compute the length of the provided line segment.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> angledLine(
/// angle = 60,
/// length = 10,
/// tag = $thing,
/// )
/// |> tangentialArc(angle = -120, radius = 5)
/// |> angledLine(
/// angle = -60,
/// length = segLen(thing),
/// )
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "segLen",
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
},
tags = ["sketch"]
}]
fn inner_segment_length(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<TyF64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
@ -437,29 +213,6 @@ pub async fn segment_angle(exec_state: &mut ExecState, args: Args) -> Result<Kcl
Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, NumericType::degrees())))
}
/// Compute the angle (in degrees) of the provided line segment.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> line(end = [5, 10], tag = $seg01)
/// |> line(end = [-10, 0])
/// |> angledLine(angle = segAng(seg01), length = 10)
/// |> line(end = [-10, 0])
/// |> angledLine(angle = segAng(seg01), length = -15)
/// |> close()
///
/// example = extrude(exampleSketch, length = 4)
/// ```
#[stdlib {
name = "segAng",
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
},
tags = ["sketch"]
}]
fn inner_segment_angle(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {
@ -482,85 +235,6 @@ pub async fn tangent_to_end(exec_state: &mut ExecState, args: Args) -> Result<Kc
Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, NumericType::degrees())))
}
/// Returns the angle coming out of the end of the segment in degrees.
///
/// ```no_run
/// // Horizontal pill.
/// pillSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [20, 0])
/// |> tangentialArc(end = [0, 10], tag = $arc1)
/// |> angledLine(
/// angle = tangentToEnd(arc1),
/// length = 20,
/// )
/// |> tangentialArc(end = [0, -10])
/// |> close()
///
/// pillExtrude = extrude(pillSketch, length = 10)
/// ```
///
/// ```no_run
/// // Vertical pill. Use absolute coordinate for arc.
/// pillSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [0, 20])
/// |> tangentialArc(endAbsolute = [10, 20], tag = $arc1)
/// |> angledLine(
/// angle = tangentToEnd(arc1),
/// length = 20,
/// )
/// |> tangentialArc(end = [-10, 0])
/// |> close()
///
/// pillExtrude = extrude(pillSketch, length = 10)
/// ```
///
/// ```no_run
/// rectangleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0], tag = $seg1)
/// |> angledLine(
/// angle = tangentToEnd(seg1),
/// length = 10,
/// )
/// |> line(end = [0, 10])
/// |> line(end = [-20, 0])
/// |> close()
///
/// rectangleExtrude = extrude(rectangleSketch, length = 10)
/// ```
///
/// ```no_run
/// bottom = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> arc(
/// endAbsolute = [10, 10],
/// interiorAbsolute = [5, 1],
/// tag = $arc1,
/// )
/// |> angledLine(angle = tangentToEnd(arc1), length = 20)
/// |> close()
/// ```
///
/// ```no_run
/// circSketch = startSketchOn(XY)
/// |> circle( center= [0, 0], radius= 3 , tag= $circ)
///
/// triangleSketch = startSketchOn(XY)
/// |> startProfile(at = [-5, 0])
/// |> angledLine(angle = tangentToEnd(circ), length = 10)
/// |> line(end = [-15, 0])
/// |> close()
/// ```
#[stdlib {
name = "tangentToEnd",
unlabeled_first = true,
args = {
tag = { docs = "The line segment being queried by its tag"},
},
tags = ["sketch"]
}]
async fn inner_tangent_to_end(tag: &TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<f64, KclError> {
let line = args.get_tag_engine_info(exec_state, tag)?;
let path = line.path.clone().ok_or_else(|| {

View File

@ -2,7 +2,6 @@
use anyhow::Result;
use indexmap::IndexMap;
use kcl_derive_docs::stdlib;
use kcmc::shared::Point2d as KPoint2d; // Point2d is already defined in this pkg, to impl ts_rs traits.
use kcmc::shared::Point3d as KPoint3d; // Point3d is already defined in this pkg, to impl ts_rs traits.
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, websocket::ModelingCmdReq, ModelingCmd};
@ -100,8 +99,7 @@ pub enum StartOrEnd {
pub const NEW_TAG_KW: &str = "tag";
pub async fn involute_circular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch =
args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
let sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
let start_radius: TyF64 = args.get_kw_arg_typed("startRadius", &RuntimeType::length(), exec_state)?;
let end_radius: TyF64 = args.get_kw_arg_typed("endRadius", &RuntimeType::length(), exec_state)?;
@ -122,29 +120,6 @@ fn involute_curve(radius: f64, angle: f64) -> (f64, f64) {
)
}
/// Extend the current sketch with a new involute circular curve.
///
/// ```no_run
/// a = 10
/// b = 14
/// startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> involuteCircular(startRadius = a, endRadius = b, angle = 60)
/// |> involuteCircular(startRadius = a, endRadius = b, angle = 60, reverse = true)
/// ```
#[stdlib {
name = "involuteCircular",
unlabeled_first = true,
args = {
sketch = { docs = "Which sketch should this path be added to?"},
start_radius = { docs = "The involute is described between two circles, start_radius is the radius of the inner circle."},
end_radius = { docs = "The involute is described between two circles, end_radius is the radius of the outer circle."},
angle = { docs = "The angle to rotate the involute by. A value of zero will produce a curve with a tangent along the x-axis at the start point of the curve."},
reverse = { docs = "If reverse is true, the segment will start from the end of the involute, otherwise it will start from that start. Defaults to false."},
tag = { docs = "Create a new tag which refers to this line"},
},
tags = ["sketch"]
}]
#[allow(clippy::too_many_arguments)]
async fn inner_involute_circular(
sketch: Sketch,
@ -228,41 +203,6 @@ pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
})
}
/// Extend the current sketch with a new straight line.
///
/// ```no_run
/// triangle = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// // The END argument means it ends at exactly [10, 0].
/// // This is an absolute measurement, it is NOT relative to
/// // the start of the sketch.
/// |> line(endAbsolute = [10, 0])
/// |> line(endAbsolute = [0, 10])
/// |> line(endAbsolute = [-10, 0], tag = $thirdLineOfTriangle)
/// |> close()
/// |> extrude(length = 5)
///
/// box = startSketchOn(XZ)
/// |> startProfile(at = [10, 10])
/// // The 'to' argument means move the pen this much.
/// // So, [10, 0] is a relative distance away from the current point.
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> line(end = [-10, 0], tag = $thirdLineOfBox)
/// |> close()
/// |> extrude(length = 5)
/// ```
#[stdlib {
name = "line",
unlabeled_first = true,
args = {
sketch = { docs = "Which sketch should this path be added to?"},
end_absolute = { docs = "Which absolute point should this line go to? Incompatible with `end`."},
end = { docs = "How far away (along the X and Y axes) should this line go? Incompatible with `endAbsolute`.", include_in_snippet = true},
tag = { docs = "Create a new tag which refers to this line"},
},
tags = ["sketch"]
}]
async fn inner_line(
sketch: Sketch,
end_absolute: Option<[TyF64; 2]>,
@ -401,39 +341,6 @@ pub async fn x_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
})
}
/// Draw a line relative to the current origin to a specified distance away
/// from the current position along the 'x' axis.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> xLine(length = 15)
/// |> angledLine(
/// angle = 80,
/// length = 15,
/// )
/// |> line(end = [8, -10])
/// |> xLine(length = 10)
/// |> angledLine(
/// angle = 120,
/// length = 30,
/// )
/// |> xLine(length = -15)
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
/// ```
#[stdlib {
name = "xLine",
unlabeled_first = true,
args = {
sketch = { docs = "Which sketch should this path be added to?"},
length = { docs = "How far away along the X axis should this line go? Incompatible with `endAbsolute`.", include_in_snippet = true},
end_absolute = { docs = "Which absolute X value should this line go to? Incompatible with `length`."},
tag = { docs = "Create a new tag which refers to this line"},
},
tags = ["sketch"]
}]
async fn inner_x_line(
sketch: Sketch,
length: Option<TyF64>,
@ -471,34 +378,6 @@ pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
})
}
/// Draw a line relative to the current origin to a specified distance away
/// from the current position along the 'y' axis.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> yLine(length = 15)
/// |> angledLine(
/// angle = 30,
/// length = 15,
/// )
/// |> line(end = [8, -10])
/// |> yLine(length = -5)
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
/// ```
#[stdlib {
name = "yLine",
unlabeled_first = true,
args = {
sketch = { docs = "Which sketch should this path be added to?"},
length = { docs = "How far away along the Y axis should this line go? Incompatible with `endAbsolute`.", include_in_snippet = true},
end_absolute = { docs = "Which absolute Y value should this line go to? Incompatible with `length`."},
tag = { docs = "Create a new tag which refers to this line"},
},
tags = ["sketch"]
}]
async fn inner_y_line(
sketch: Sketch,
length: Option<TyF64>,
@ -553,38 +432,6 @@ pub async fn angled_line(exec_state: &mut ExecState, args: Args) -> Result<KclVa
})
}
/// Draw a line segment relative to the current origin using the polar
/// measure of some angle and distance.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> yLine(endAbsolute = 15)
/// |> angledLine(
/// angle = 30,
/// length = 15,
/// )
/// |> line(end = [8, -10])
/// |> yLine(endAbsolute = 0)
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
/// ```
#[stdlib {
name = "angledLine",
unlabeled_first = true,
args = {
sketch = { docs = "Which sketch should this path be added to?"},
angle = { docs = "Which angle should the line be drawn at?" },
length = { docs = "Draw the line this distance along the given angle. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
length_x = { docs = "Draw the line this distance along the X axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
length_y = { docs = "Draw the line this distance along the Y axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
end_absolute_x = { docs = "Draw the line along the given angle until it reaches this point along the X axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
end_absolute_y = { docs = "Draw the line along the given angle until it reaches this point along the Y axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
tag = { docs = "Create a new tag which refers to this line"},
},
tags = ["sketch"]
}]
#[allow(clippy::too_many_arguments)]
async fn inner_angled_line(
sketch: Sketch,
@ -850,37 +697,6 @@ pub async fn angled_line_that_intersects(exec_state: &mut ExecState, args: Args)
})
}
/// Draw an angled line from the current origin, constructing a line segment
/// such that the newly created line intersects the desired target line
/// segment.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(endAbsolute = [5, 10])
/// |> line(endAbsolute = [-10, 10], tag = $lineToIntersect)
/// |> line(endAbsolute = [0, 20])
/// |> angledLineThatIntersects(
/// angle = 80,
/// intersectTag = lineToIntersect,
/// offset = 10,
/// )
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
/// ```
#[stdlib {
name = "angledLineThatIntersects",
unlabeled_first = true,
args = {
sketch = { docs = "Which sketch should this path be added to?"},
angle = { docs = "Which angle should the line be drawn at?" },
intersect_tag = { docs = "The tag of the line to intersect with" },
offset = { docs = "The offset from the intersecting line. Defaults to 0." },
tag = { docs = "Create a new tag which refers to this line"},
},
tags = ["sketch"]
}]
pub async fn inner_angled_line_that_intersects(
sketch: Sketch,
angle: TyF64,
@ -971,190 +787,6 @@ pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result<K
}
}
/// Start a new 2-dimensional sketch on a specific plane or face.
///
/// ### Sketch on Face Behavior
///
/// There are some important behaviors to understand when sketching on a face:
///
/// The resulting sketch will _include_ the face and thus Solid
/// that was sketched on. So say you were to export the resulting Sketch / Solid
/// from a sketch on a face, you would get both the artifact of the sketch
/// on the face and the parent face / Solid itself.
///
/// This is important to understand because if you were to then sketch on the
/// resulting Solid, it would again include the face and parent Solid that was
/// sketched on. This could go on indefinitely.
///
/// The point is if you want to export the result of a sketch on a face, you
/// only need to export the final Solid that was created from the sketch on the
/// face, since it will include all the parent faces and Solids.
///
///
/// ```no_run
/// exampleSketch = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
///
/// exampleSketch002 = startSketchOn(example, face = END)
/// |> startProfile(at = [1, 1])
/// |> line(end = [8, 0])
/// |> line(end = [0, 8])
/// |> line(end = [-8, 0])
/// |> close()
///
/// example002 = extrude(exampleSketch002, length = 5)
///
/// exampleSketch003 = startSketchOn(example002, face = END)
/// |> startProfile(at = [2, 2])
/// |> line(end = [6, 0])
/// |> line(end = [0, 6])
/// |> line(end = [-6, 0])
/// |> close()
///
/// example003 = extrude(exampleSketch003, length = 5)
/// ```
///
/// ```no_run
/// // Sketch on the end of an extruded face by tagging the end face.
///
/// exampleSketch = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5, tagEnd = $end01)
///
/// exampleSketch002 = startSketchOn(example, face = end01)
/// |> startProfile(at = [1, 1])
/// |> line(end = [8, 0])
/// |> line(end = [0, 8])
/// |> line(end = [-8, 0])
/// |> close()
///
/// example002 = extrude(exampleSketch002, length = 5, tagEnd = $end02)
///
/// exampleSketch003 = startSketchOn(example002, face = end02)
/// |> startProfile(at = [2, 2])
/// |> line(end = [6, 0])
/// |> line(end = [0, 6])
/// |> line(end = [-6, 0])
/// |> close()
///
/// example003 = extrude(exampleSketch003, length = 5)
/// ```
///
/// ```no_run
/// exampleSketch = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10], tag = $sketchingFace)
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
///
/// exampleSketch002 = startSketchOn(example, face = sketchingFace)
/// |> startProfile(at = [1, 1])
/// |> line(end = [8, 0])
/// |> line(end = [0, 8])
/// |> line(end = [-8, 0])
/// |> close(tag = $sketchingFace002)
///
/// example002 = extrude(exampleSketch002, length = 10)
///
/// exampleSketch003 = startSketchOn(example002, face = sketchingFace002)
/// |> startProfile(at = [-8, 12])
/// |> line(end = [0, 6])
/// |> line(end = [6, 0])
/// |> line(end = [0, -6])
/// |> close()
///
/// example003 = extrude(exampleSketch003, length = 5)
/// ```
///
/// ```no_run
/// exampleSketch = startSketchOn(XY)
/// |> startProfile(at = [4, 12])
/// |> line(end = [2, 0])
/// |> line(end = [0, -6])
/// |> line(end = [4, -6])
/// |> line(end = [0, -6])
/// |> line(end = [-3.75, -4.5])
/// |> line(end = [0, -5.5])
/// |> line(end = [-2, 0])
/// |> close()
///
/// example = revolve(exampleSketch, axis = Y, angle = 180)
///
/// exampleSketch002 = startSketchOn(example, face = END)
/// |> startProfile(at = [4.5, -5])
/// |> line(end = [0, 5])
/// |> line(end = [5, 0])
/// |> line(end = [0, -5])
/// |> close()
///
/// example002 = extrude(exampleSketch002, length = 5)
/// ```
///
/// ```no_run
/// // Sketch on the end of a revolved face by tagging the end face.
///
/// exampleSketch = startSketchOn(XY)
/// |> startProfile(at = [4, 12])
/// |> line(end = [2, 0])
/// |> line(end = [0, -6])
/// |> line(end = [4, -6])
/// |> line(end = [0, -6])
/// |> line(end = [-3.75, -4.5])
/// |> line(end = [0, -5.5])
/// |> line(end = [-2, 0])
/// |> close()
///
/// example = revolve(exampleSketch, axis = Y, angle = 180, tagEnd = $end01)
///
/// exampleSketch002 = startSketchOn(example, face = end01)
/// |> startProfile(at = [4.5, -5])
/// |> line(end = [0, 5])
/// |> line(end = [5, 0])
/// |> line(end = [0, -5])
/// |> close()
///
/// example002 = extrude(exampleSketch002, length = 5)
/// ```
///
/// ```no_run
/// a1 = startSketchOn({
/// origin = { x = 0, y = 0, z = 0 },
/// xAxis = { x = 1, y = 0, z = 0 },
/// yAxis = { x = 0, y = 1, z = 0 },
/// zAxis = { x = 0, y = 0, z = 1 }
/// })
/// |> startProfile(at = [0, 0])
/// |> line(end = [100.0, 0])
/// |> yLine(length = -100.0)
/// |> xLine(length = -100.0)
/// |> yLine(length = 100.0)
/// |> close()
/// |> extrude(length = 3.14)
/// ```
#[stdlib {
name = "startSketchOn",
feature_tree_operation = true,
unlabeled_first = true,
args = {
plane_or_solid = { docs = "The plane or solid to sketch on"},
face = { docs = "Identify a face of a solid if a solid is specified as the input argument (`plane_or_solid`)"},
},
tags = ["sketch"]
}]
async fn inner_start_sketch_on(
plane_or_solid: SketchData,
face: Option<FaceTag>,
@ -1280,50 +912,6 @@ pub async fn start_profile(exec_state: &mut ExecState, args: Args) -> Result<Kcl
})
}
/// Start a new profile at a given point.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
///
/// ```no_run
/// exampleSketch = startSketchOn(-XZ)
/// |> startProfile(at = [10, 10])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
///
/// ```no_run
/// exampleSketch = startSketchOn(-XZ)
/// |> startProfile(at = [-10, 23])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 5)
/// ```
#[stdlib {
name = "startProfile",
unlabeled_first = true,
args = {
sketch_surface = { docs = "What to start the profile on" },
at = { docs = "Where to start the profile. An absolute point.", snippet_value_array = ["0", "0"] },
tag = { docs = "Tag this first starting point" },
},
tags = ["sketch"]
}]
pub(crate) async fn inner_start_profile(
sketch_surface: SketchSurface,
at: [TyF64; 2],
@ -1440,91 +1028,36 @@ pub(crate) async fn inner_start_profile(
/// Returns the X component of the sketch profile start point.
pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
let sketch: Sketch = args.get_unlabeled_kw_arg_typed("profile", &RuntimeType::sketch(), exec_state)?;
let ty = sketch.units.into();
let x = inner_profile_start_x(sketch)?;
Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
}
/// Extract the provided 2-dimensional sketch's profile's origin's 'x'
/// value.
///
/// ```no_run
/// sketch001 = startSketchOn(XY)
/// |> startProfile(at = [5, 2])
/// |> angledLine(angle = -26.6, length = 50)
/// |> angledLine(angle = 90, length = 50)
/// |> angledLine(angle = 30, endAbsoluteX = profileStartX(%))
/// ```
#[stdlib {
name = "profileStartX",
unlabeled_first = true,
args = {
profile = {docs = "Profile whose start is being used"},
},
tags = ["sketch"]
}]
pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
Ok(profile.start.to[0])
}
/// Returns the Y component of the sketch profile start point.
pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
let sketch: Sketch = args.get_unlabeled_kw_arg_typed("profile", &RuntimeType::sketch(), exec_state)?;
let ty = sketch.units.into();
let x = inner_profile_start_y(sketch)?;
Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
}
/// Extract the provided 2-dimensional sketch's profile's origin's 'y'
/// value.
///
/// ```no_run
/// sketch001 = startSketchOn(XY)
/// |> startProfile(at = [5, 2])
/// |> angledLine(angle = -60, length = 14 )
/// |> angledLine(angle = 30, endAbsoluteY = profileStartY(%))
/// ```
#[stdlib {
name = "profileStartY",
unlabeled_first = true,
args = {
profile = {docs = "Profile whose start is being used"},
},
tags = ["sketch"]
}]
pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
Ok(profile.start.to[1])
}
/// Returns the sketch profile start point.
pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
let sketch: Sketch = args.get_unlabeled_kw_arg_typed("profile", &RuntimeType::sketch(), exec_state)?;
let ty = sketch.units.into();
let point = inner_profile_start(sketch)?;
Ok(KclValue::from_point2d(point, ty, args.into()))
}
/// Extract the provided 2-dimensional sketch's profile's origin
/// value.
///
/// ```no_run
/// sketch001 = startSketchOn(XY)
/// |> startProfile(at = [5, 2])
/// |> angledLine(angle = 120, length = 50 , tag = $seg01)
/// |> angledLine(angle = segAng(seg01) + 120, length = 50 )
/// |> line(end = profileStart(%))
/// |> close()
/// |> extrude(length = 20)
/// ```
#[stdlib {
name = "profileStart",
unlabeled_first = true,
args = {
profile = {docs = "Profile whose start is being used"},
},
tags = ["sketch"]
}]
pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
Ok(profile.start.to)
}
@ -1540,41 +1073,6 @@ pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
})
}
/// Construct a line segment from the current origin back to the profile's
/// origin, ensuring the resulting 2-dimensional sketch is not open-ended.
///
/// If you want to perform some 3-dimensional operation on a sketch, like
/// extrude or sweep, you must `close` it first. `close` must be called even
/// if the end point of the last segment is coincident with the sketch
/// starting point.
///
/// ```no_run
/// startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 10])
/// |> line(end = [10, 0])
/// |> close()
/// |> extrude(length = 10)
/// ```
///
/// ```no_run
/// exampleSketch = startSketchOn(-XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
/// ```
#[stdlib {
name = "close",
unlabeled_first = true,
args = {
sketch = { docs = "The sketch you want to close"},
tag = { docs = "Create a new tag which refers to this line"},
},
tags = ["sketch"]
}]
pub(crate) async fn inner_close(
sketch: Sketch,
tag: Option<TagNode>,
@ -1644,54 +1142,6 @@ pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
})
}
/// Draw a curved line segment along an imaginary circle.
///
/// The arc is constructed such that the current position of the sketch is
/// placed along an imaginary circle of the specified radius, at angleStart
/// degrees. The resulting arc is the segment of the imaginary circle from
/// that origin point to angleEnd, radius away from the center of the imaginary
/// circle.
///
/// Unless this makes a lot of sense and feels like what you're looking
/// for to construct your shape, you're likely looking for tangentialArc.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> arc(
/// angleStart = 0,
/// angleEnd = 280,
/// radius = 16
/// )
/// |> close()
/// example = extrude(exampleSketch, length = 10)
/// ```
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> arc(
/// endAbsolute = [10,0],
/// interiorAbsolute = [5,5]
/// )
/// |> close()
/// example = extrude(exampleSketch, length = 10)
/// ```
#[stdlib {
name = "arc",
unlabeled_first = true,
args = {
sketch = { docs = "Which sketch should this path be added to?" },
angle_start = { docs = "Where along the circle should this arc start?", include_in_snippet = true },
angle_end = { docs = "Where along the circle should this arc end?", include_in_snippet = true },
radius = { docs = "How large should the circle be? Incompatible with `diameter`." },
diameter = { docs = "How large should the circle be? Incompatible with `radius`.", include_in_snippet = true },
interior_absolute = { docs = "Any point between the arc's start and end? Requires `endAbsolute`. Incompatible with `angleStart` or `angleEnd`" },
end_absolute = { docs = "Where should this arc end? Requires `interiorAbsolute`. Incompatible with `angleStart` or `angleEnd`" },
tag = { docs = "Create a new tag which refers to this line"},
},
tags = ["sketch"]
}]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn inner_arc(
sketch: Sketch,
@ -1880,74 +1330,6 @@ pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<Kc
})
}
/// Starting at the current sketch's origin, draw a curved line segment along
/// some part of an imaginary circle until it reaches the desired (x, y)
/// coordinates.
///
/// When using radius and angle, draw a curved line segment along part of an
/// imaginary circle. The arc is constructed such that the last line segment is
/// placed tangent to the imaginary circle of the specified radius. The
/// resulting arc is the segment of the imaginary circle from that tangent point
/// for 'angle' degrees along the imaginary circle.
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> angledLine(
/// angle = 45,
/// length = 10,
/// )
/// |> tangentialArc(end = [0, -10])
/// |> line(end = [-10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
/// ```
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> angledLine(
/// angle = 60,
/// length = 10,
/// )
/// |> tangentialArc(endAbsolute = [15, 15])
/// |> line(end = [10, -15])
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
/// ```
///
/// ```no_run
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> angledLine(
/// angle = 60,
/// length = 10,
/// )
/// |> tangentialArc(radius = 10, angle = -120)
/// |> angledLine(
/// angle = -60,
/// length = 10,
/// )
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
/// ```
#[stdlib {
name = "tangentialArc",
unlabeled_first = true,
args = {
sketch = { docs = "Which sketch should this path be added to?"},
end_absolute = { docs = "Which absolute point should this arc go to? Incompatible with `end`, `radius`, and `offset`."},
end = { docs = "How far away (along the X and Y axes) should this arc go? Incompatible with `endAbsolute`, `radius`, and `offset`.", include_in_snippet = true },
radius = { docs = "Radius of the imaginary circle. `angle` must be given. Incompatible with `end` and `endAbsolute` and `diameter`."},
diameter = { docs = "Diameter of the imaginary circle. `angle` must be given. Incompatible with `end` and `endAbsolute` and `radius`."},
angle = { docs = "Offset of the arc in degrees. `radius` must be given. Incompatible with `end` and `endAbsolute`."},
tag = { docs = "Create a new tag which refers to this arc"},
},
tags = ["sketch"]
}]
#[allow(clippy::too_many_arguments)]
async fn inner_tangential_arc(
sketch: Sketch,
@ -2206,48 +1588,6 @@ pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclV
})
}
/// Draw a smooth, continuous, curved line segment from the current origin to
/// the desired (x, y), using a number of control points to shape the curve's
/// shape.
///
/// ```no_run
/// // Example using relative control points.
/// exampleSketch = startSketchOn(XZ)
/// |> startProfile(at = [0, 0])
/// |> line(end = [0, 10])
/// |> bezierCurve(
/// control1 = [5, 0],
/// control2 = [5, 10],
/// end = [10, 10],
/// )
/// |> line(endAbsolute = [10, 0])
/// |> close()
///
/// example = extrude(exampleSketch, length = 10)
/// ```
/// ```no_run
/// // Example using absolute control points.
/// startSketchOn(XY)
/// |> startProfile(at = [300, 300])
/// |> bezierCurve(control1Absolute = [600, 300], control2Absolute = [-300, -100], endAbsolute = [600, 600])
/// |> close()
/// |> extrude(length = 10)
/// ```
#[stdlib {
name = "bezierCurve",
unlabeled_first = true,
args = {
sketch = { docs = "Which sketch should this path be added to?"},
control1 = { docs = "First control point for the cubic" },
control2 = { docs = "Second control point for the cubic" },
end = { docs = "How far away (along the X and Y axes) should this line go?" },
control1_absolute = { docs = "First control point for the cubic. Absolute point." },
control2_absolute = { docs = "Second control point for the cubic. Absolute point." },
end_absolute = { docs = "Coordinate on the plane at which this line should end." },
tag = { docs = "Create a new tag which refers to this line"},
},
tags = ["sketch"]
}]
#[allow(clippy::too_many_arguments)]
async fn inner_bezier_curve(
sketch: Sketch,
@ -2364,47 +1704,6 @@ pub async fn subtract_2d(exec_state: &mut ExecState, args: Args) -> Result<KclVa
})
}
/// Use a 2-dimensional sketch to cut a hole in another 2-dimensional sketch.
///
/// ```no_run
/// exampleSketch = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> line(end = [0, 5])
/// |> line(end = [5, 0])
/// |> line(end = [0, -5])
/// |> close()
/// |> subtract2d(tool =circle( center = [1, 1], radius = .25 ))
/// |> subtract2d(tool =circle( center = [1, 4], radius = .25 ))
///
/// example = extrude(exampleSketch, length = 1)
/// ```
///
/// ```no_run
/// fn squareHoleSketch() {
/// squareSketch = startSketchOn(-XZ)
/// |> startProfile(at = [-1, -1])
/// |> line(end = [2, 0])
/// |> line(end = [0, 2])
/// |> line(end = [-2, 0])
/// |> close()
/// return squareSketch
/// }
///
/// exampleSketch = startSketchOn(-XZ)
/// |> circle( center = [0, 0], radius = 3 )
/// |> subtract2d(tool = squareHoleSketch())
/// example = extrude(exampleSketch, length = 1)
/// ```
#[stdlib {
name = "subtract2d",
feature_tree_operation = true,
unlabeled_first = true,
args = {
sketch = { docs = "Which sketch should this path be added to?" },
tool = { docs = "The shape(s) which should be cut out of the sketch." },
},
tags = ["sketch"]
}]
async fn inner_subtract_2d(
sketch: Sketch,
tool: Vec<Sketch>,

View File

@ -21,6 +21,7 @@ use crate::{
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum SweepPath {
Sketch(Sketch),
Helix(Box<Helix>),

View File

@ -36,7 +36,16 @@ pub async fn execute_and_snapshot_ast(
ast: Program,
current_file: Option<PathBuf>,
with_export_step: bool,
) -> Result<(ExecState, EnvironmentRef, image::DynamicImage, Option<Vec<u8>>), ExecErrorWithState> {
) -> Result<
(
ExecState,
ExecutorContext,
EnvironmentRef,
image::DynamicImage,
Option<Vec<u8>>,
),
ExecErrorWithState,
> {
let ctx = new_context(true, current_file).await?;
let (exec_state, env, img) = match do_execute_and_snapshot(&ctx, ast).await {
Ok((exec_state, env_ref, img)) => (exec_state, env_ref, img),
@ -64,7 +73,7 @@ pub async fn execute_and_snapshot_ast(
step = files.into_iter().next().map(|f| f.contents);
}
ctx.close().await;
Ok((exec_state, env, img, step))
Ok((exec_state, ctx, env, img, step))
}
pub async fn execute_and_snapshot_no_auth(

View File

@ -5,8 +5,8 @@ use crate::parsing::{
Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryOperator,
BinaryPart, BodyItem, CallExpressionKw, CommentStyle, DefaultParamVal, Expr, FormatOptions, FunctionExpression,
IfExpression, ImportSelector, ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier,
LiteralValue, MemberExpression, MemberObject, Node, NonCodeNode, NonCodeValue, ObjectExpression, Parameter,
PipeExpression, Program, TagDeclarator, TypeDeclaration, UnaryExpression, VariableDeclaration, VariableKind,
LiteralValue, MemberExpression, Node, NonCodeNode, NonCodeValue, ObjectExpression, Parameter, PipeExpression,
Program, TagDeclarator, TypeDeclaration, UnaryExpression, VariableDeclaration, VariableKind,
},
deprecation, DeprecationKind, PIPE_OPERATOR,
};
@ -278,11 +278,11 @@ impl Expr {
ctxt = ExprContext::Other;
}
match &self {
Expr::BinaryExpression(bin_exp) => bin_exp.recast(options),
Expr::BinaryExpression(bin_exp) => bin_exp.recast(options, indentation_level, ctxt),
Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, ctxt),
Expr::ArrayRangeExpression(range_exp) => range_exp.recast(options, indentation_level, ctxt),
Expr::ObjectExpression(ref obj_exp) => obj_exp.recast(options, indentation_level, ctxt),
Expr::MemberExpression(mem_exp) => mem_exp.recast(),
Expr::MemberExpression(mem_exp) => mem_exp.recast(options, indentation_level, ctxt),
Expr::Literal(literal) => literal.recast(),
Expr::FunctionExpression(func_exp) => {
let mut result = if is_decl { String::new() } else { "fn".to_owned() };
@ -299,7 +299,7 @@ impl Expr {
}
Expr::TagDeclarator(tag) => tag.recast(),
Expr::PipeExpression(pipe_exp) => pipe_exp.recast(options, indentation_level),
Expr::UnaryExpression(unary_exp) => unary_exp.recast(options),
Expr::UnaryExpression(unary_exp) => unary_exp.recast(options, indentation_level, ctxt),
Expr::IfExpression(e) => e.recast(options, indentation_level, ctxt),
Expr::PipeSubstitution(_) => crate::parsing::PIPE_SUBSTITUTION_OPERATOR.to_string(),
Expr::LabelledExpression(e) => {
@ -332,7 +332,7 @@ impl AscribedExpression {
}
impl BinaryPart {
fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
match &self {
BinaryPart::Literal(literal) => literal.recast(),
BinaryPart::Name(name) => {
@ -342,12 +342,16 @@ impl BinaryPart {
None => result,
}
}
BinaryPart::BinaryExpression(binary_expression) => binary_expression.recast(options),
BinaryPart::BinaryExpression(binary_expression) => {
binary_expression.recast(options, indentation_level, ctxt)
}
BinaryPart::CallExpressionKw(call_expression) => {
call_expression.recast(options, indentation_level, ExprContext::Other)
}
BinaryPart::UnaryExpression(unary_expression) => unary_expression.recast(options),
BinaryPart::MemberExpression(member_expression) => member_expression.recast(),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.recast(options, indentation_level, ctxt),
BinaryPart::MemberExpression(member_expression) => {
member_expression.recast(options, indentation_level, ctxt)
}
BinaryPart::IfExpression(e) => e.recast(options, indentation_level, ExprContext::Other),
BinaryPart::AscribedExpression(e) => e.recast(options, indentation_level, ExprContext::Other),
}
@ -670,7 +674,7 @@ impl ObjectExpression {
}
impl MemberExpression {
fn recast(&self) -> String {
fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
let key_str = match &self.property {
LiteralIdentifier::Identifier(identifier) => {
if self.computed {
@ -682,15 +686,12 @@ impl MemberExpression {
LiteralIdentifier::Literal(lit) => format!("[{}]", &(*lit.raw)),
};
match &self.object {
MemberObject::MemberExpression(member_exp) => member_exp.recast() + key_str.as_str(),
MemberObject::Identifier(identifier) => identifier.name.to_string() + key_str.as_str(),
}
self.object.recast(options, indentation_level, ctxt) + key_str.as_str()
}
}
impl BinaryExpression {
fn recast(&self, options: &FormatOptions) -> String {
fn recast(&self, options: &FormatOptions, _indentation_level: usize, ctxt: ExprContext) -> String {
let maybe_wrap_it = |a: String, doit: bool| -> String {
if doit {
format!("({})", a)
@ -715,15 +716,15 @@ impl BinaryExpression {
format!(
"{} {} {}",
maybe_wrap_it(self.left.recast(options, 0), should_wrap_left),
maybe_wrap_it(self.left.recast(options, 0, ctxt), should_wrap_left),
self.operator,
maybe_wrap_it(self.right.recast(options, 0), should_wrap_right)
maybe_wrap_it(self.right.recast(options, 0, ctxt), should_wrap_right)
)
}
}
impl UnaryExpression {
fn recast(&self, options: &FormatOptions) -> String {
fn recast(&self, options: &FormatOptions, _indentation_level: usize, ctxt: ExprContext) -> String {
match self.argument {
BinaryPart::Literal(_)
| BinaryPart::Name(_)
@ -731,10 +732,10 @@ impl UnaryExpression {
| BinaryPart::IfExpression(_)
| BinaryPart::AscribedExpression(_)
| BinaryPart::CallExpressionKw(_) => {
format!("{}{}", &self.operator, self.argument.recast(options, 0))
format!("{}{}", &self.operator, self.argument.recast(options, 0, ctxt))
}
BinaryPart::BinaryExpression(_) | BinaryPart::UnaryExpression(_) => {
format!("{}({})", &self.operator, self.argument.recast(options, 0))
format!("{}({})", &self.operator, self.argument.recast(options, 0, ctxt))
}
}
}

View File

@ -226,15 +226,6 @@ impl<'tree> From<&'tree types::BinaryPart> for Node<'tree> {
}
}
impl<'tree> From<&'tree types::MemberObject> for Node<'tree> {
fn from(node: &'tree types::MemberObject) -> Self {
match node {
types::MemberObject::MemberExpression(me) => me.as_ref().into(),
types::MemberObject::Identifier(id) => id.as_ref().into(),
}
}
}
impl<'tree> From<&'tree types::LiteralIdentifier> for Node<'tree> {
fn from(node: &'tree types::LiteralIdentifier) -> Self {
match node {

View File

@ -3,10 +3,7 @@
mod ast_node;
mod ast_visitor;
mod ast_walk;
mod import_graph;
pub use ast_node::Node;
pub use ast_visitor::Visitable;
pub use ast_walk::walk;
pub use import_graph::import_graph;
pub(crate) use import_graph::{import_universe, Universe, UniverseMap};
pub(crate) use ast_node::Node;
pub(crate) use ast_visitor::Visitable;
pub(crate) use ast_walk::walk;