Merge remote-tracking branch 'origin/main' into paultag/import
This commit is contained in:
@ -13,7 +13,7 @@ use itertools::Itertools;
|
||||
use serde_json::json;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use super::kcl_doc::{ConstData, DocData, FnData};
|
||||
use super::kcl_doc::{ConstData, DocData, ExampleProperties, FnData, TyData};
|
||||
use crate::{
|
||||
docs::{is_primitive, StdLibFn},
|
||||
std::StdLib,
|
||||
@ -22,6 +22,8 @@ use crate::{
|
||||
|
||||
const TYPES_DIR: &str = "../../docs/kcl/types";
|
||||
const LANG_TOPICS: [&str; 4] = ["Types", "Modules", "Settings", "Known Issues"];
|
||||
// These types are declared in std.
|
||||
const DECLARED_TYPES: [&str; 7] = ["number", "string", "tag", "bool", "Sketch", "Solid", "Plane"];
|
||||
|
||||
fn init_handlebars() -> Result<handlebars::Handlebars<'static>> {
|
||||
let mut hbs = handlebars::Handlebars::new();
|
||||
@ -299,6 +301,7 @@ fn init_handlebars() -> Result<handlebars::Handlebars<'static>> {
|
||||
hbs.register_template_string("function", include_str!("templates/function.hbs"))?;
|
||||
hbs.register_template_string("const", include_str!("templates/const.hbs"))?;
|
||||
hbs.register_template_string("type", include_str!("templates/type.hbs"))?;
|
||||
hbs.register_template_string("kclType", include_str!("templates/kclType.hbs"))?;
|
||||
|
||||
Ok(hbs)
|
||||
}
|
||||
@ -332,9 +335,11 @@ fn generate_index(combined: &IndexMap<String, Box<dyn StdLibFn>>, kcl_lib: &[Doc
|
||||
functions.entry(d.mod_name()).or_default().push(match d {
|
||||
DocData::Fn(f) => (f.name.clone(), d.file_name()),
|
||||
DocData::Const(c) => (c.name.clone(), d.file_name()),
|
||||
DocData::Ty(t) => (t.name.clone(), d.file_name()),
|
||||
});
|
||||
}
|
||||
|
||||
// TODO we should sub-divide into types, constants, and functions.
|
||||
let mut sorted: Vec<_> = functions
|
||||
.into_iter()
|
||||
.map(|(m, mut fns)| {
|
||||
@ -373,6 +378,61 @@ fn generate_index(combined: &IndexMap<String, Box<dyn StdLibFn>>, kcl_lib: &[Doc
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_example(index: usize, src: &str, props: &ExampleProperties, file_name: &str) -> Option<serde_json::Value> {
|
||||
if props.inline && props.norun {
|
||||
return None;
|
||||
}
|
||||
|
||||
let content = if props.inline { "" } else { src };
|
||||
|
||||
let image_base64 = if props.norun {
|
||||
String::new()
|
||||
} else {
|
||||
let image_path = format!(
|
||||
"{}/tests/outputs/serial_test_example_{}{}.png",
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
file_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)
|
||||
};
|
||||
|
||||
Some(json!({
|
||||
"content": content,
|
||||
"image_base64": image_base64,
|
||||
}))
|
||||
}
|
||||
|
||||
fn generate_type_from_kcl(ty: &TyData, file_name: String) -> Result<()> {
|
||||
if ty.properties.doc_hidden {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let hbs = init_handlebars()?;
|
||||
|
||||
let examples: Vec<serde_json::Value> = ty
|
||||
.examples
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, example)| generate_example(index, &example.0, &example.1, &file_name))
|
||||
.collect();
|
||||
|
||||
let data = json!({
|
||||
"name": ty.qual_name(),
|
||||
"summary": ty.summary,
|
||||
"description": ty.description,
|
||||
"deprecated": ty.properties.deprecated,
|
||||
"examples": examples,
|
||||
});
|
||||
|
||||
let output = hbs.render("kclType", &data)?;
|
||||
expectorate::assert_contents(format!("../../docs/kcl/types/{}.md", file_name), &output);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_function_from_kcl(function: &FnData, file_name: String) -> Result<()> {
|
||||
if function.properties.doc_hidden {
|
||||
return Ok(());
|
||||
@ -386,22 +446,7 @@ fn generate_function_from_kcl(function: &FnData, file_name: String) -> Result<()
|
||||
.examples
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, example)| {
|
||||
let image_path = format!(
|
||||
"{}/tests/outputs/serial_test_example_{}{}.png",
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
file_name,
|
||||
index
|
||||
);
|
||||
let image_data =
|
||||
std::fs::read(&image_path).unwrap_or_else(|_| panic!("Failed to read image file: {}", image_path));
|
||||
let image_base64 = base64::engine::general_purpose::STANDARD.encode(&image_data);
|
||||
|
||||
json!({
|
||||
"content": example,
|
||||
"image_base64": image_base64,
|
||||
})
|
||||
})
|
||||
.filter_map(|(index, example)| generate_example(index, &example.0, &example.1, &file_name))
|
||||
.collect();
|
||||
|
||||
let data = json!({
|
||||
@ -445,22 +490,7 @@ fn generate_const_from_kcl(cnst: &ConstData, file_name: String) -> Result<()> {
|
||||
.examples
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, example)| {
|
||||
let image_path = format!(
|
||||
"{}/tests/outputs/serial_test_example_{}{}.png",
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
file_name,
|
||||
index
|
||||
);
|
||||
let image_data =
|
||||
std::fs::read(&image_path).unwrap_or_else(|_| panic!("Failed to read image file: {}", image_path));
|
||||
let image_base64 = base64::engine::general_purpose::STANDARD.encode(&image_data);
|
||||
|
||||
json!({
|
||||
"content": example,
|
||||
"image_base64": image_base64,
|
||||
})
|
||||
})
|
||||
.filter_map(|(index, example)| generate_example(index, &example.0, &example.1, &file_name))
|
||||
.collect();
|
||||
|
||||
let data = json!({
|
||||
@ -564,7 +594,7 @@ fn generate_function(internal_fn: Box<dyn StdLibFn>) -> Result<BTreeMap<String,
|
||||
|
||||
let mut output = hbs.render("function", &data)?;
|
||||
// Fix the links to the types.
|
||||
output = cleanup_type_links(&output, types.keys().cloned().collect());
|
||||
output = cleanup_type_links(&output, types.keys());
|
||||
|
||||
expectorate::assert_contents(format!("../../docs/kcl/{}.md", fn_name), &output);
|
||||
|
||||
@ -586,10 +616,16 @@ fn cleanup_static_links(output: &str) -> String {
|
||||
}
|
||||
|
||||
// Fix the links to the types.
|
||||
fn cleanup_type_links(output: &str, types: Vec<String>) -> String {
|
||||
fn cleanup_type_links<'a>(output: &str, types: impl Iterator<Item = &'a String>) -> String {
|
||||
let mut cleaned_output = output.to_string();
|
||||
|
||||
// Cleanup our weird number arrays.
|
||||
// TODO: This is a hack for the handlebars template being too complex.
|
||||
cleaned_output = cleaned_output.replace("`[, `number`, `number`]`", "`[number, number]`");
|
||||
cleaned_output = cleaned_output.replace("`[, `number`, `number`, `number`]`", "`[number, number, number]`");
|
||||
|
||||
// Fix the links to the types.
|
||||
for type_name in types {
|
||||
for type_name in types.map(|s| &**s).chain(DECLARED_TYPES) {
|
||||
if type_name == "TagDeclarator" || type_name == "TagIdentifier" || type_name == "TagNode" {
|
||||
continue;
|
||||
} else {
|
||||
@ -602,11 +638,6 @@ fn cleanup_type_links(output: &str, types: Vec<String>) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup our weird number arrays.
|
||||
// TODO: This is a hack for the handlebars template being too complex.
|
||||
cleaned_output = cleaned_output.replace("`[, `number`, `number`]`", "`[number, number]`");
|
||||
cleaned_output = cleaned_output.replace("`[, `number`, `number`, `number`]`", "`[number, number, number]`");
|
||||
|
||||
cleanup_static_links(&cleaned_output)
|
||||
}
|
||||
|
||||
@ -619,6 +650,14 @@ fn add_to_types(
|
||||
return Err(anyhow::anyhow!("Empty type name"));
|
||||
}
|
||||
|
||||
if DECLARED_TYPES.contains(&name) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if name.starts_with("number(") {
|
||||
panic!("uom number");
|
||||
}
|
||||
|
||||
let schemars::schema::Schema::Object(o) = schema else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to get object schema, should have not been a primitive"
|
||||
@ -750,7 +789,7 @@ fn generate_type(
|
||||
|
||||
let mut output = hbs.render("type", &data)?;
|
||||
// Fix the links to the types.
|
||||
output = cleanup_type_links(&output, types.keys().cloned().collect());
|
||||
output = cleanup_type_links(&output, types.keys());
|
||||
expectorate::assert_contents(format!("{}/{}.md", TYPES_DIR, name), &output);
|
||||
|
||||
Ok(())
|
||||
@ -828,6 +867,10 @@ fn recurse_and_create_references(
|
||||
schema: &schemars::schema::Schema,
|
||||
types: &BTreeMap<String, schemars::schema::Schema>,
|
||||
) -> Result<schemars::schema::Schema> {
|
||||
if DECLARED_TYPES.contains(&name) {
|
||||
return Ok(schema.clone());
|
||||
}
|
||||
|
||||
let schemars::schema::Schema::Object(o) = schema else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to get object schema, should have not been a primitive"
|
||||
@ -838,6 +881,10 @@ fn recurse_and_create_references(
|
||||
if let Some(reference) = &o.reference {
|
||||
let mut obj = o.clone();
|
||||
let reference = reference.trim_start_matches("#/components/schemas/");
|
||||
if DECLARED_TYPES.contains(&reference) {
|
||||
return Ok(schema.clone());
|
||||
}
|
||||
|
||||
let t = types
|
||||
.get(reference)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get type: {} {:?}", reference, types.keys()))?;
|
||||
@ -982,6 +1029,7 @@ fn test_generate_stdlib_markdown_docs() {
|
||||
match d {
|
||||
DocData::Fn(f) => generate_function_from_kcl(f, d.file_name()).unwrap(),
|
||||
DocData::Const(c) => generate_const_from_kcl(c, d.file_name()).unwrap(),
|
||||
DocData::Ty(t) => generate_type_from_kcl(t, d.file_name()).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use tower_lsp::lsp_types::{
|
||||
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, Documentation, InsertTextFormat, MarkupContent,
|
||||
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
execution::annotations,
|
||||
parsing::{
|
||||
ast::types::{Annotation, Node, NonCodeNode, NonCodeValue, VariableKind},
|
||||
token::NumericSuffix,
|
||||
@ -69,6 +72,23 @@ impl CollectionVisitor {
|
||||
|
||||
self.result.push(dd);
|
||||
}
|
||||
crate::parsing::ast::types::BodyItem::TypeDeclaration(ty) if !ty.visibility.is_default() => {
|
||||
let qual_name = if self.name == "prelude" {
|
||||
"std::".to_owned()
|
||||
} else {
|
||||
format!("std::{}::", self.name)
|
||||
};
|
||||
let mut dd = DocData::Ty(TyData::from_ast(ty, qual_name));
|
||||
|
||||
// FIXME this association of metadata with items is pretty flaky.
|
||||
if i == 0 {
|
||||
dd.with_meta(&parsed.non_code_meta.start_nodes, &ty.outer_attrs);
|
||||
} else if let Some(meta) = parsed.non_code_meta.non_code_nodes.get(&(i - 1)) {
|
||||
dd.with_meta(meta, &ty.outer_attrs);
|
||||
}
|
||||
|
||||
self.result.push(dd);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@ -82,6 +102,7 @@ impl CollectionVisitor {
|
||||
pub enum DocData {
|
||||
Fn(FnData),
|
||||
Const(ConstData),
|
||||
Ty(TyData),
|
||||
}
|
||||
|
||||
impl DocData {
|
||||
@ -89,6 +110,7 @@ impl DocData {
|
||||
match self {
|
||||
DocData::Fn(f) => &f.name,
|
||||
DocData::Const(c) => &c.name,
|
||||
DocData::Ty(t) => &t.name,
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,6 +119,8 @@ impl DocData {
|
||||
match self {
|
||||
DocData::Fn(f) => f.qual_name.replace("::", "-"),
|
||||
DocData::Const(c) => format!("const_{}", c.qual_name.replace("::", "-")),
|
||||
// TODO might want to change this
|
||||
DocData::Ty(t) => t.name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,6 +129,12 @@ impl DocData {
|
||||
let q = match self {
|
||||
DocData::Fn(f) => &f.qual_name,
|
||||
DocData::Const(c) => &c.qual_name,
|
||||
DocData::Ty(t) => {
|
||||
if t.properties.impl_kind == annotations::Impl::Primitive {
|
||||
return "Primitive types".to_owned();
|
||||
}
|
||||
&t.qual_name
|
||||
}
|
||||
};
|
||||
q[0..q.rfind("::").unwrap()].to_owned()
|
||||
}
|
||||
@ -114,6 +144,7 @@ impl DocData {
|
||||
match self {
|
||||
DocData::Fn(f) => f.properties.doc_hidden || f.properties.deprecated,
|
||||
DocData::Const(c) => c.properties.doc_hidden || c.properties.deprecated,
|
||||
DocData::Ty(t) => t.properties.doc_hidden || t.properties.deprecated,
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,6 +152,7 @@ impl DocData {
|
||||
match self {
|
||||
DocData::Fn(f) => f.to_completion_item(),
|
||||
DocData::Const(c) => c.to_completion_item(),
|
||||
DocData::Ty(t) => t.to_completion_item(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,6 +160,7 @@ impl DocData {
|
||||
match self {
|
||||
DocData::Fn(f) => Some(f.to_signature_help()),
|
||||
DocData::Const(_) => None,
|
||||
DocData::Ty(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,15 +168,18 @@ impl DocData {
|
||||
match self {
|
||||
DocData::Fn(f) => f.with_meta(meta, attrs),
|
||||
DocData::Const(c) => c.with_meta(meta, attrs),
|
||||
DocData::Ty(t) => t.with_meta(meta, attrs),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn examples(&self) -> &[String] {
|
||||
fn examples(&self) -> impl Iterator<Item = &String> {
|
||||
match self {
|
||||
DocData::Fn(f) => &f.examples,
|
||||
DocData::Const(c) => &c.examples,
|
||||
DocData::Fn(f) => f.examples.iter(),
|
||||
DocData::Const(c) => c.examples.iter(),
|
||||
DocData::Ty(t) => t.examples.iter(),
|
||||
}
|
||||
.filter_map(|(s, p)| (!p.norun).then_some(s))
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,7 +198,7 @@ pub struct ConstData {
|
||||
pub description: Option<String>,
|
||||
/// Code examples.
|
||||
/// These are tested and we know they compile and execute.
|
||||
pub examples: Vec<String>,
|
||||
pub examples: Vec<(String, ExampleProperties)>,
|
||||
}
|
||||
|
||||
impl ConstData {
|
||||
@ -199,7 +235,7 @@ impl ConstData {
|
||||
exported: !var.visibility.is_default(),
|
||||
deprecated: false,
|
||||
doc_hidden: false,
|
||||
impl_kind: ImplKind::Kcl,
|
||||
impl_kind: annotations::Impl::Kcl,
|
||||
},
|
||||
summary: None,
|
||||
description: None,
|
||||
@ -270,7 +306,7 @@ pub struct FnData {
|
||||
pub description: Option<String>,
|
||||
/// Code examples.
|
||||
/// These are tested and we know they compile and execute.
|
||||
pub examples: Vec<String>,
|
||||
pub examples: Vec<(String, ExampleProperties)>,
|
||||
}
|
||||
|
||||
impl FnData {
|
||||
@ -290,7 +326,7 @@ impl FnData {
|
||||
exported: !var.visibility.is_default(),
|
||||
deprecated: false,
|
||||
doc_hidden: false,
|
||||
impl_kind: ImplKind::Kcl,
|
||||
impl_kind: annotations::Impl::Kcl,
|
||||
},
|
||||
summary: None,
|
||||
description: None,
|
||||
@ -410,13 +446,14 @@ pub struct Properties {
|
||||
pub doc_hidden: bool,
|
||||
#[allow(dead_code)]
|
||||
pub exported: bool,
|
||||
pub impl_kind: ImplKind,
|
||||
pub impl_kind: annotations::Impl,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ImplKind {
|
||||
Kcl,
|
||||
Rust,
|
||||
pub struct ExampleProperties {
|
||||
pub norun: bool,
|
||||
pub inline: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -502,11 +539,98 @@ impl ArgKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TyData {
|
||||
/// The name of the function.
|
||||
pub name: String,
|
||||
/// The fully qualified name.
|
||||
pub qual_name: String,
|
||||
pub properties: Properties,
|
||||
|
||||
/// The summary of the function.
|
||||
pub summary: Option<String>,
|
||||
/// The description of the function.
|
||||
pub description: Option<String>,
|
||||
/// Code examples.
|
||||
/// These are tested and we know they compile and execute.
|
||||
pub examples: Vec<(String, ExampleProperties)>,
|
||||
}
|
||||
|
||||
impl TyData {
|
||||
fn from_ast(ty: &crate::parsing::ast::types::TypeDeclaration, mut qual_name: String) -> Self {
|
||||
let name = ty.name.name.clone();
|
||||
qual_name.push_str(&name);
|
||||
TyData {
|
||||
name,
|
||||
qual_name,
|
||||
properties: Properties {
|
||||
exported: !ty.visibility.is_default(),
|
||||
deprecated: false,
|
||||
doc_hidden: false,
|
||||
impl_kind: annotations::Impl::Kcl,
|
||||
},
|
||||
summary: None,
|
||||
description: None,
|
||||
examples: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn qual_name(&self) -> &str {
|
||||
if self.properties.impl_kind == annotations::Impl::Primitive {
|
||||
&self.name
|
||||
} else {
|
||||
&self.qual_name
|
||||
}
|
||||
}
|
||||
|
||||
fn short_docs(&self) -> Option<String> {
|
||||
match (&self.summary, &self.description) {
|
||||
(None, None) => None,
|
||||
(None, Some(d)) | (Some(d), None) => Some(d.clone()),
|
||||
(Some(s), Some(d)) => Some(format!("{s}\n\n{d}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_completion_item(&self) -> CompletionItem {
|
||||
CompletionItem {
|
||||
label: self.name.clone(),
|
||||
label_details: None,
|
||||
kind: Some(CompletionItemKind::FUNCTION),
|
||||
detail: Some(self.qual_name().to_owned()),
|
||||
documentation: self.short_docs().map(|s| {
|
||||
Documentation::MarkupContent(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: s,
|
||||
})
|
||||
}),
|
||||
deprecated: Some(self.properties.deprecated),
|
||||
preselect: None,
|
||||
sort_text: None,
|
||||
filter_text: None,
|
||||
insert_text: Some(self.name.clone()),
|
||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
||||
insert_text_mode: None,
|
||||
text_edit: None,
|
||||
additional_text_edits: None,
|
||||
command: None,
|
||||
commit_characters: None,
|
||||
data: None,
|
||||
tags: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait ApplyMeta {
|
||||
fn apply_docs(&mut self, summary: Option<String>, description: Option<String>, examples: Vec<String>);
|
||||
fn apply_docs(
|
||||
&mut self,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
examples: Vec<(String, ExampleProperties)>,
|
||||
);
|
||||
fn deprecated(&mut self, deprecated: bool);
|
||||
fn doc_hidden(&mut self, doc_hidden: bool);
|
||||
fn impl_kind(&mut self, impl_kind: ImplKind);
|
||||
fn impl_kind(&mut self, impl_kind: annotations::Impl);
|
||||
|
||||
fn with_meta(&mut self, meta: &[Node<NonCodeNode>], attrs: &[Node<Annotation>]) {
|
||||
for attr in attrs {
|
||||
@ -518,13 +642,9 @@ trait ApplyMeta {
|
||||
{
|
||||
for p in props {
|
||||
match &*p.key.name {
|
||||
"impl" => {
|
||||
annotations::IMPL => {
|
||||
if let Some(s) = p.value.ident_name() {
|
||||
self.impl_kind(match s {
|
||||
"kcl" => ImplKind::Kcl,
|
||||
"std_rust" => ImplKind::Rust,
|
||||
_ => unreachable!(),
|
||||
});
|
||||
self.impl_kind(annotations::Impl::from_str(s).unwrap());
|
||||
}
|
||||
}
|
||||
"deprecated" => {
|
||||
@ -554,7 +674,7 @@ trait ApplyMeta {
|
||||
|
||||
let mut summary = None;
|
||||
let mut description = None;
|
||||
let mut example: Option<String> = None;
|
||||
let mut example: Option<(String, ExampleProperties)> = None;
|
||||
let mut examples = Vec::new();
|
||||
for l in comments.into_iter().filter(|l| l.starts_with('/')).map(|l| {
|
||||
if let Some(ll) = l.strip_prefix("/ ") {
|
||||
@ -579,19 +699,40 @@ trait ApplyMeta {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
#[allow(clippy::manual_strip)]
|
||||
if l.starts_with("```") {
|
||||
if let Some(e) = example {
|
||||
examples.push(e.trim().to_owned());
|
||||
if let Some((e, p)) = example {
|
||||
if p.inline {
|
||||
description.as_mut().unwrap().push_str("```\n");
|
||||
}
|
||||
|
||||
examples.push((e.trim().to_owned(), p));
|
||||
example = None;
|
||||
} else {
|
||||
example = Some(String::new());
|
||||
let args = l[3..].split(',');
|
||||
let mut inline = false;
|
||||
let mut norun = false;
|
||||
for a in args {
|
||||
match a.trim() {
|
||||
"inline" => inline = true,
|
||||
"norun" | "no_run" => norun = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
example = Some((String::new(), ExampleProperties { norun, inline }));
|
||||
|
||||
if inline {
|
||||
description.as_mut().unwrap().push_str("```js\n");
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(e) = &mut example {
|
||||
if let Some((e, p)) = &mut example {
|
||||
e.push_str(l);
|
||||
e.push('\n');
|
||||
continue;
|
||||
if !p.inline {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
match &mut description {
|
||||
Some(d) => {
|
||||
@ -617,7 +758,12 @@ trait ApplyMeta {
|
||||
}
|
||||
|
||||
impl ApplyMeta for ConstData {
|
||||
fn apply_docs(&mut self, summary: Option<String>, description: Option<String>, examples: Vec<String>) {
|
||||
fn apply_docs(
|
||||
&mut self,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
examples: Vec<(String, ExampleProperties)>,
|
||||
) {
|
||||
self.summary = summary;
|
||||
self.description = description;
|
||||
self.examples = examples;
|
||||
@ -631,11 +777,16 @@ impl ApplyMeta for ConstData {
|
||||
self.properties.doc_hidden = doc_hidden;
|
||||
}
|
||||
|
||||
fn impl_kind(&mut self, _impl_kind: ImplKind) {}
|
||||
fn impl_kind(&mut self, _impl_kind: annotations::Impl) {}
|
||||
}
|
||||
|
||||
impl ApplyMeta for FnData {
|
||||
fn apply_docs(&mut self, summary: Option<String>, description: Option<String>, examples: Vec<String>) {
|
||||
fn apply_docs(
|
||||
&mut self,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
examples: Vec<(String, ExampleProperties)>,
|
||||
) {
|
||||
self.summary = summary;
|
||||
self.description = description;
|
||||
self.examples = examples;
|
||||
@ -649,7 +800,32 @@ impl ApplyMeta for FnData {
|
||||
self.properties.doc_hidden = doc_hidden;
|
||||
}
|
||||
|
||||
fn impl_kind(&mut self, impl_kind: ImplKind) {
|
||||
fn impl_kind(&mut self, impl_kind: annotations::Impl) {
|
||||
self.properties.impl_kind = impl_kind;
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplyMeta for TyData {
|
||||
fn apply_docs(
|
||||
&mut self,
|
||||
summary: Option<String>,
|
||||
description: Option<String>,
|
||||
examples: Vec<(String, ExampleProperties)>,
|
||||
) {
|
||||
self.summary = summary;
|
||||
self.description = description;
|
||||
self.examples = examples;
|
||||
}
|
||||
|
||||
fn deprecated(&mut self, deprecated: bool) {
|
||||
self.properties.deprecated = deprecated;
|
||||
}
|
||||
|
||||
fn doc_hidden(&mut self, doc_hidden: bool) {
|
||||
self.properties.doc_hidden = doc_hidden;
|
||||
}
|
||||
|
||||
fn impl_kind(&mut self, impl_kind: annotations::Impl) {
|
||||
self.properties.impl_kind = impl_kind;
|
||||
}
|
||||
}
|
||||
@ -680,7 +856,7 @@ mod test {
|
||||
async fn test_examples() -> miette::Result<()> {
|
||||
let std = walk_prelude();
|
||||
for d in std {
|
||||
for (i, eg) in d.examples().iter().enumerate() {
|
||||
for (i, eg) in d.examples().enumerate() {
|
||||
let result =
|
||||
match crate::test_server::execute_and_snapshot(eg, crate::settings::types::UnitLength::Mm, None)
|
||||
.await
|
||||
|
4
rust/kcl-lib/src/docs/templates/function.hbs
vendored
4
rust/kcl-lib/src/docs/templates/function.hbs
vendored
@ -46,12 +46,16 @@ layout: manual
|
||||
### Examples
|
||||
|
||||
{{#each examples}}
|
||||
{{#if this.content}}
|
||||
```js
|
||||
{{{this.content}}}
|
||||
```
|
||||
{{/if}}
|
||||
|
||||
{{#unless @root.is_utilities}}
|
||||
{{#if this.image_base64}}
|
||||

|
||||
{{/if}}
|
||||
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
|
2
rust/kcl-lib/src/docs/templates/index.hbs
vendored
2
rust/kcl-lib/src/docs/templates/index.hbs
vendored
@ -15,7 +15,7 @@ layout: manual
|
||||
### Standard library
|
||||
|
||||
{{#each modules}}
|
||||
* **`{{name}}`**
|
||||
* **{{name}}**
|
||||
{{#each functions}}
|
||||
* [`{{name}}`](kcl/{{file_name}})
|
||||
{{/each}}
|
||||
|
32
rust/kcl-lib/src/docs/templates/kclType.hbs
vendored
Normal file
32
rust/kcl-lib/src/docs/templates/kclType.hbs
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
title: "{{name}}"
|
||||
excerpt: "{{safe_yaml summary}}"
|
||||
layout: manual
|
||||
---
|
||||
|
||||
{{#if deprecated}}
|
||||
**WARNING:** This type is deprecated.
|
||||
|
||||
{{/if}}
|
||||
{{{summary}}}
|
||||
|
||||
{{{description}}}
|
||||
|
||||
|
||||
{{#if examples}}
|
||||
### Examples
|
||||
|
||||
{{#each examples}}
|
||||
{{#if this.content}}
|
||||
```js
|
||||
{{{this.content}}}
|
||||
```
|
||||
{{/if}}
|
||||
|
||||
{{#if this.image_base64}}
|
||||

|
||||
{{/if}}
|
||||
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
@ -152,7 +152,7 @@ impl KclErrorWithOutputs {
|
||||
source_files: Default::default(),
|
||||
}
|
||||
}
|
||||
pub fn into_miette_report_with_outputs(self) -> anyhow::Result<ReportWithOutputs> {
|
||||
pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
|
||||
let mut source_ranges = self.error.source_ranges();
|
||||
|
||||
// Pop off the first source range to get the filename.
|
||||
@ -164,12 +164,19 @@ impl KclErrorWithOutputs {
|
||||
.source_files
|
||||
.get(&first_source_range.module_id())
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Could not find source file for module id: {:?}",
|
||||
first_source_range.module_id()
|
||||
)
|
||||
})?;
|
||||
.unwrap_or(ModuleSource {
|
||||
source: code.to_string(),
|
||||
path: self
|
||||
.filenames
|
||||
.get(&first_source_range.module_id())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Could not find filename for module id: {:?}",
|
||||
first_source_range.module_id()
|
||||
)
|
||||
})?
|
||||
.clone(),
|
||||
});
|
||||
let filename = source.path.to_string();
|
||||
let kcl_source = source.source.to_string();
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
//! Data on available annotations.
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use kittycad_modeling_cmds::coord::{System, KITTYCAD, OPENGL, VULKAN};
|
||||
|
||||
use crate::{
|
||||
@ -24,6 +26,33 @@ pub(super) const IMPORT_COORDS_VALUES: [(&str, &System); 3] =
|
||||
[("zoo", KITTYCAD), ("opengl", OPENGL), ("vulkan", VULKAN)];
|
||||
pub(super) const IMPORT_LENGTH_UNIT: &str = "lengthUnit";
|
||||
|
||||
pub(crate) const IMPL: &str = "impl";
|
||||
pub(crate) const IMPL_RUST: &str = "std_rust";
|
||||
pub(crate) const IMPL_KCL: &str = "kcl";
|
||||
pub(crate) const IMPL_PRIMITIVE: &str = "primitive";
|
||||
pub(super) const IMPL_VALUES: [&str; 3] = [IMPL_RUST, IMPL_KCL, IMPL_PRIMITIVE];
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, Default)]
|
||||
pub enum Impl {
|
||||
#[default]
|
||||
Kcl,
|
||||
Rust,
|
||||
Primitive,
|
||||
}
|
||||
|
||||
impl FromStr for Impl {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
IMPL_RUST => Ok(Self::Rust),
|
||||
IMPL_KCL => Ok(Self::Kcl),
|
||||
IMPL_PRIMITIVE => Ok(Self::Primitive),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn settings_completion_text() -> String {
|
||||
format!("@{SETTINGS}({SETTINGS_UNIT_LENGTH} = mm, {SETTINGS_UNIT_ANGLE} = deg)")
|
||||
}
|
||||
@ -58,6 +87,32 @@ pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_impl(annotations: &[Node<Annotation>], source_range: SourceRange) -> Result<Option<Impl>, KclError> {
|
||||
for attr in annotations {
|
||||
if attr.name.is_some() || attr.properties.is_none() {
|
||||
continue;
|
||||
}
|
||||
for p in attr.properties.as_ref().unwrap() {
|
||||
if &*p.key.name == IMPL {
|
||||
if let Some(s) = p.value.ident_name() {
|
||||
return Impl::from_str(s).map(Some).map_err(|_| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"Invalid value for {} attribute, expected one of: {}",
|
||||
IMPL,
|
||||
IMPL_VALUES.join(", ")
|
||||
),
|
||||
source_ranges: vec![source_range],
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
impl UnitLen {
|
||||
pub(super) fn from_str(s: &str, source_range: SourceRange) -> Result<Self, KclError> {
|
||||
match s {
|
||||
|
@ -252,7 +252,7 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::execution::parse_execute;
|
||||
use crate::execution::{parse_execute, ExecTestResults};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_changed_program_same_code() {
|
||||
@ -268,16 +268,16 @@ firstSketch = startSketchOn('XY')
|
||||
// Remove the end face for the extrusion.
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program, _, ctx, _) = parse_execute(new).await.unwrap();
|
||||
let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@ -311,18 +311,18 @@ firstSketch = startSketchOn('XY')
|
||||
// Remove the end face for the extrusion.
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program_old, _, ctx, _) = parse_execute(old).await.unwrap();
|
||||
let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
|
||||
|
||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
ast: &program_old.ast,
|
||||
settings: &ctx.settings,
|
||||
ast: &program.ast,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program_new.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@ -356,18 +356,18 @@ firstSketch = startSketchOn('XY')
|
||||
// Remove the end face for the extrusion.
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program, _, ctx, _) = parse_execute(old).await.unwrap();
|
||||
let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
|
||||
|
||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program_new.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@ -405,18 +405,18 @@ firstSketch = startSketchOn('XY')
|
||||
// Remove the end face for the extrusion.
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program, _, ctx, _) = parse_execute(old).await.unwrap();
|
||||
let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
|
||||
|
||||
let program_new = crate::Program::parse_no_errs(new).unwrap();
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program_new.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@ -439,10 +439,12 @@ firstSketch = startSketchOn('XY')
|
||||
// Remove the end face for the extrusion.
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program, _, mut ctx, _) = parse_execute(new).await.unwrap();
|
||||
let ExecTestResults {
|
||||
program, mut exec_ctxt, ..
|
||||
} = parse_execute(new).await.unwrap();
|
||||
|
||||
// Change the settings to cm.
|
||||
ctx.settings.units = crate::UnitLength::Cm;
|
||||
exec_ctxt.settings.units = crate::UnitLength::Cm;
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
@ -451,7 +453,7 @@ shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@ -481,10 +483,12 @@ firstSketch = startSketchOn('XY')
|
||||
// Remove the end face for the extrusion.
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program, _, mut ctx, _) = parse_execute(new).await.unwrap();
|
||||
let ExecTestResults {
|
||||
program, mut exec_ctxt, ..
|
||||
} = parse_execute(new).await.unwrap();
|
||||
|
||||
// Change the settings.
|
||||
ctx.settings.show_grid = !ctx.settings.show_grid;
|
||||
exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
@ -493,7 +497,7 @@ shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@ -516,10 +520,12 @@ firstSketch = startSketchOn('XY')
|
||||
// Remove the end face for the extrusion.
|
||||
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
|
||||
let (program, _, mut ctx, _) = parse_execute(new).await.unwrap();
|
||||
let ExecTestResults {
|
||||
program, mut exec_ctxt, ..
|
||||
} = parse_execute(new).await.unwrap();
|
||||
|
||||
// Change the settings.
|
||||
ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
|
||||
exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
@ -528,7 +534,7 @@ shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@ -536,8 +542,8 @@ shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
assert_eq!(result, CacheResult::NoAction(true));
|
||||
|
||||
// Change the settings back.
|
||||
let old_settings = ctx.settings.clone();
|
||||
ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
|
||||
let old_settings = exec_ctxt.settings.clone();
|
||||
exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
@ -546,7 +552,7 @@ shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@ -554,8 +560,8 @@ shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
assert_eq!(result, CacheResult::NoAction(true));
|
||||
|
||||
// Change the settings back.
|
||||
let old_settings = ctx.settings.clone();
|
||||
ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
|
||||
let old_settings = exec_ctxt.settings.clone();
|
||||
exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
|
||||
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
@ -564,7 +570,7 @@ shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@ -583,7 +589,7 @@ startSketchOn('XY')
|
||||
startSketchOn('XY')
|
||||
"#;
|
||||
|
||||
let (program, _, ctx, _) = parse_execute(old_code).await.unwrap();
|
||||
let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
|
||||
|
||||
let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
|
||||
new_program.compute_digest();
|
||||
@ -591,11 +597,11 @@ startSketchOn('XY')
|
||||
let result = get_changed_program(
|
||||
CacheInformation {
|
||||
ast: &program.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
CacheInformation {
|
||||
ast: &new_program.ast,
|
||||
settings: &ctx.settings,
|
||||
settings: &exec_ctxt.settings,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
@ -197,6 +197,7 @@ pub enum OpKclValue {
|
||||
},
|
||||
Function {},
|
||||
Module {},
|
||||
Type {},
|
||||
KclNone {},
|
||||
}
|
||||
|
||||
@ -233,7 +234,7 @@ impl From<&KclValue> for OpKclValue {
|
||||
ty: ty.clone(),
|
||||
},
|
||||
KclValue::String { value, .. } => Self::String { value: value.clone() },
|
||||
KclValue::Array { value, .. } => {
|
||||
KclValue::MixedArray { value, .. } => {
|
||||
let value = value.iter().map(Self::from).collect();
|
||||
Self::Array { value }
|
||||
}
|
||||
@ -293,6 +294,7 @@ impl From<&KclValue> for OpKclValue {
|
||||
KclValue::Function { .. } => Self::Function {},
|
||||
KclValue::Module { .. } => Self::Module {},
|
||||
KclValue::KclNone { .. } => Self::KclNone {},
|
||||
KclValue::Type { .. } => Self::Type {},
|
||||
KclValue::Tombstone { .. } => unreachable!("Tombstone OpKclValue"),
|
||||
}
|
||||
}
|
||||
|
@ -327,6 +327,50 @@ impl ExecutorContext {
|
||||
}
|
||||
last_expr = None;
|
||||
}
|
||||
BodyItem::TypeDeclaration(ty) => {
|
||||
let metadata = Metadata::from(&**ty);
|
||||
let impl_kind = annotations::get_impl(&ty.outer_attrs, metadata.source_range)?.unwrap_or_default();
|
||||
match impl_kind {
|
||||
annotations::Impl::Rust => {
|
||||
let std_path = match &exec_state.mod_local.settings.std_path {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "User-defined types are not yet supported.".to_owned(),
|
||||
source_ranges: vec![metadata.source_range],
|
||||
}));
|
||||
}
|
||||
};
|
||||
let value = KclValue::Type {
|
||||
value: Some(crate::std::std_ty(std_path, &ty.name.name)),
|
||||
meta: vec![metadata],
|
||||
};
|
||||
exec_state
|
||||
.mut_stack()
|
||||
.add(
|
||||
format!("{}{}", memory::TYPE_PREFIX, ty.name.name),
|
||||
value,
|
||||
metadata.source_range,
|
||||
)
|
||||
.map_err(|_| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Redefinition of type {}.", ty.name.name),
|
||||
source_ranges: vec![metadata.source_range],
|
||||
})
|
||||
})?;
|
||||
}
|
||||
// Do nothing for primitive types, they get special treatment and their declarations are just for documentation.
|
||||
annotations::Impl::Primitive => {}
|
||||
annotations::Impl::Kcl => {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "User-defined types are not yet supported.".to_owned(),
|
||||
source_ranges: vec![metadata.source_range],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
last_expr = None;
|
||||
}
|
||||
BodyItem::ReturnStatement(return_statement) => {
|
||||
let metadata = Metadata::from(return_statement);
|
||||
|
||||
@ -561,21 +605,9 @@ impl ExecutorContext {
|
||||
}
|
||||
Expr::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, self).await?,
|
||||
Expr::FunctionExpression(function_expression) => {
|
||||
let mut rust_impl = false;
|
||||
for attr in annotations {
|
||||
if attr.name.is_some() || attr.properties.is_none() {
|
||||
continue;
|
||||
}
|
||||
for p in attr.properties.as_ref().unwrap() {
|
||||
if &*p.key.name == "impl" {
|
||||
if let Some(s) = p.value.ident_name() {
|
||||
if s == "std_rust" {
|
||||
rust_impl = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let rust_impl = annotations::get_impl(annotations, metadata.source_range)?
|
||||
.map(|s| s == annotations::Impl::Rust)
|
||||
.unwrap_or(false);
|
||||
|
||||
if rust_impl {
|
||||
if let Some(std_path) = &exec_state.mod_local.settings.std_path {
|
||||
@ -598,6 +630,7 @@ impl ExecutorContext {
|
||||
KclValue::Function {
|
||||
value: FunctionSource::User {
|
||||
ast: function_expression.clone(),
|
||||
settings: exec_state.mod_local.settings.clone(),
|
||||
memory: exec_state.mut_stack().snapshot(),
|
||||
},
|
||||
meta: vec![metadata.to_owned()],
|
||||
@ -665,7 +698,12 @@ impl ExecutorContext {
|
||||
}
|
||||
|
||||
fn coerce(value: KclValue, ty: &Node<Type>, exec_state: &mut ExecState) -> Result<KclValue, KclValue> {
|
||||
let ty = RuntimeType::from_parsed(ty.inner.clone(), &exec_state.mod_local.settings).ok_or_else(|| value.clone())?;
|
||||
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, (&value).into())
|
||||
.map_err(|e| {
|
||||
exec_state.err(e);
|
||||
value.clone()
|
||||
})?
|
||||
.ok_or_else(|| value.clone())?;
|
||||
if value.has_type(&ty) {
|
||||
return Ok(value);
|
||||
}
|
||||
@ -759,7 +797,7 @@ impl Node<MemberExpression> {
|
||||
}
|
||||
};
|
||||
|
||||
let KclValue::Array { value: array, meta: _ } = array else {
|
||||
let KclValue::MixedArray { value: array, meta: _ } = array else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("MemberExpression array is not an array: {:?}", array),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
@ -809,7 +847,7 @@ impl Node<MemberExpression> {
|
||||
source_ranges: vec![self.clone().into()],
|
||||
}))
|
||||
}
|
||||
(KclValue::Array { value: arr, meta: _ }, Property::UInt(index)) => {
|
||||
(KclValue::MixedArray { value: arr, meta: _ }, Property::UInt(index)) => {
|
||||
let value_of_arr = arr.get(index);
|
||||
if let Some(value) = value_of_arr {
|
||||
Ok(value.to_owned())
|
||||
@ -820,7 +858,7 @@ impl Node<MemberExpression> {
|
||||
}))
|
||||
}
|
||||
}
|
||||
(KclValue::Array { .. }, p) => {
|
||||
(KclValue::MixedArray { .. }, p) => {
|
||||
let t = p.type_name();
|
||||
let article = article_for(t);
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
@ -1494,7 +1532,7 @@ impl Node<ArrayExpression> {
|
||||
results.push(value);
|
||||
}
|
||||
|
||||
Ok(KclValue::Array {
|
||||
Ok(KclValue::MixedArray {
|
||||
value: results,
|
||||
meta: vec![self.into()],
|
||||
})
|
||||
@ -1543,7 +1581,7 @@ impl Node<ArrayRangeExpression> {
|
||||
let meta = vec![Metadata {
|
||||
source_range: self.into(),
|
||||
}];
|
||||
Ok(KclValue::Array {
|
||||
Ok(KclValue::MixedArray {
|
||||
value: range
|
||||
.into_iter()
|
||||
.map(|num| KclValue::Number {
|
||||
@ -1957,7 +1995,7 @@ impl FunctionSource {
|
||||
|
||||
func(exec_state, args).await.map(Some)
|
||||
}
|
||||
FunctionSource::User { ast, memory } => {
|
||||
FunctionSource::User { ast, memory, .. } => {
|
||||
call_user_defined_function(args, *memory, ast, exec_state, ctx).await
|
||||
}
|
||||
FunctionSource::None => unreachable!(),
|
||||
@ -2109,9 +2147,11 @@ p = {
|
||||
"#;
|
||||
|
||||
let result = parse_execute(program).await.unwrap();
|
||||
let mem = result.3.stack();
|
||||
let mem = result.exec_state.stack();
|
||||
assert!(matches!(
|
||||
mem.memory.get_from("p", result.1, SourceRange::default(), 0).unwrap(),
|
||||
mem.memory
|
||||
.get_from("p", result.mem_env, SourceRange::default(), 0)
|
||||
.unwrap(),
|
||||
KclValue::Plane { .. }
|
||||
));
|
||||
|
||||
@ -2147,8 +2187,12 @@ p2 = -p
|
||||
"#;
|
||||
|
||||
let result = parse_execute(program).await.unwrap();
|
||||
let mem = result.3.stack();
|
||||
match mem.memory.get_from("p2", result.1, SourceRange::default(), 0).unwrap() {
|
||||
let mem = result.exec_state.stack();
|
||||
match mem
|
||||
.memory
|
||||
.get_from("p2", result.mem_env, SourceRange::default(), 0)
|
||||
.unwrap()
|
||||
{
|
||||
KclValue::Plane { value } => assert_eq!(value.z_axis.z, -1.0),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
@ -243,7 +243,6 @@ pub struct Helix {
|
||||
pub meta: Vec<Metadata>,
|
||||
}
|
||||
|
||||
/// A plane.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -480,47 +479,6 @@ pub enum PlaneType {
|
||||
Uninit,
|
||||
}
|
||||
|
||||
/// A sketch is a collection of paths.
|
||||
///
|
||||
/// When you define a sketch to a variable like:
|
||||
///
|
||||
/// ```kcl
|
||||
/// mySketch = startSketchOn('XY')
|
||||
/// |> startProfileAt([-12, 12], %)
|
||||
/// |> line(end = [24, 0])
|
||||
/// |> line(end = [0, -24])
|
||||
/// |> line(end = [-24, 0])
|
||||
/// |> close()
|
||||
/// ```
|
||||
///
|
||||
/// The `mySketch` variable will be an executed `Sketch` object. Executed being past
|
||||
/// tense, because the engine has already executed the commands to create the sketch.
|
||||
///
|
||||
/// The previous sketch commands will never be executed again, in this case.
|
||||
///
|
||||
/// If you would like to encapsulate the commands to create the sketch any time you call it,
|
||||
/// you can use a function.
|
||||
///
|
||||
/// ```kcl
|
||||
/// fn createSketch() {
|
||||
/// return startSketchOn('XY')
|
||||
/// |> startProfileAt([-12, 12], %)
|
||||
/// |> line(end = [24, 0])
|
||||
/// |> line(end = [0, -24])
|
||||
/// |> line(end = [-24, 0])
|
||||
/// |> close()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Now, every time you call `createSketch()`, the commands will be
|
||||
/// executed and a new sketch will be created.
|
||||
///
|
||||
/// When you assign the result of `createSketch()` to a variable (`mySketch = createSketch()`), you are assigning
|
||||
/// the executed sketch to that variable. Meaning that the sketch `mySketch` will not be executed
|
||||
/// again.
|
||||
///
|
||||
/// You can still execute _new_ commands on the sketch like `extrude`, `revolve`, `loft`, etc. and
|
||||
/// the sketch will be updated.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
@ -651,49 +609,6 @@ impl Sketch {
|
||||
}
|
||||
}
|
||||
|
||||
/// A solid is a collection of extrude surfaces.
|
||||
///
|
||||
/// When you define a solid to a variable like:
|
||||
///
|
||||
/// ```kcl
|
||||
/// myPart = startSketchOn('XY')
|
||||
/// |> startProfileAt([-12, 12], %)
|
||||
/// |> line(end = [24, 0])
|
||||
/// |> line(end = [0, -24])
|
||||
/// |> line(end = [-24, 0])
|
||||
/// |> close()
|
||||
/// |> extrude(length = 6)
|
||||
/// ```
|
||||
///
|
||||
/// The `myPart` variable will be an executed `Solid` object. Executed being past
|
||||
/// tense, because the engine has already executed the commands to create the solid.
|
||||
///
|
||||
/// The previous solid commands will never be executed again, in this case.
|
||||
///
|
||||
/// If you would like to encapsulate the commands to create the solid any time you call it,
|
||||
/// you can use a function.
|
||||
///
|
||||
/// ```kcl
|
||||
/// fn createPart() {
|
||||
/// return startSketchOn('XY')
|
||||
/// |> startProfileAt([-12, 12], %)
|
||||
/// |> line(end = [24, 0])
|
||||
/// |> line(end = [0, -24])
|
||||
/// |> line(end = [-24, 0])
|
||||
/// |> close()
|
||||
/// |> extrude(length = 6)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Now, every time you call `createPart()`, the commands will be
|
||||
/// executed and a new solid will be created.
|
||||
///
|
||||
/// When you assign the result of `createPart()` to a variable (`myPart = createPart()`), you are assigning
|
||||
/// the executed solid to that variable. Meaning that the solid `myPart` will not be executed
|
||||
/// again.
|
||||
///
|
||||
/// You can still execute _new_ commands on the solid like `shell`, `fillet`, `chamfer`, etc.
|
||||
/// and the solid will be updated.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
|
@ -4,7 +4,10 @@ use anyhow::Result;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{memory::EnvironmentRef, MetaSettings};
|
||||
use super::{
|
||||
memory::{self, EnvironmentRef},
|
||||
MetaSettings,
|
||||
};
|
||||
use crate::{
|
||||
errors::KclErrorDetails,
|
||||
execution::{
|
||||
@ -25,7 +28,7 @@ use crate::{
|
||||
pub type KclObjectFields = HashMap<String, KclValue>;
|
||||
|
||||
/// Any KCL value.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum KclValue {
|
||||
@ -50,7 +53,7 @@ pub enum KclValue {
|
||||
#[serde(rename = "__meta")]
|
||||
meta: Vec<Metadata>,
|
||||
},
|
||||
Array {
|
||||
MixedArray {
|
||||
value: Vec<KclValue>,
|
||||
#[serde(rename = "__meta")]
|
||||
meta: Vec<Metadata>,
|
||||
@ -96,6 +99,13 @@ pub enum KclValue {
|
||||
#[serde(rename = "__meta")]
|
||||
meta: Vec<Metadata>,
|
||||
},
|
||||
#[ts(skip)]
|
||||
Type {
|
||||
#[serde(skip)]
|
||||
value: Option<(PrimitiveType, StdFnProps)>,
|
||||
#[serde(rename = "__meta")]
|
||||
meta: Vec<Metadata>,
|
||||
},
|
||||
KclNone {
|
||||
value: KclNone,
|
||||
#[serde(rename = "__meta")]
|
||||
@ -119,6 +129,7 @@ pub enum FunctionSource {
|
||||
},
|
||||
User {
|
||||
ast: crate::parsing::ast::types::BoxNode<FunctionExpression>,
|
||||
settings: MetaSettings,
|
||||
memory: EnvironmentRef,
|
||||
},
|
||||
}
|
||||
@ -184,10 +195,11 @@ impl From<KclValue> for Vec<SourceRange> {
|
||||
KclValue::Bool { meta, .. } => to_vec_sr(&meta),
|
||||
KclValue::Number { meta, .. } => to_vec_sr(&meta),
|
||||
KclValue::String { meta, .. } => to_vec_sr(&meta),
|
||||
KclValue::Array { meta, .. } => to_vec_sr(&meta),
|
||||
KclValue::MixedArray { meta, .. } => to_vec_sr(&meta),
|
||||
KclValue::Object { meta, .. } => to_vec_sr(&meta),
|
||||
KclValue::Module { meta, .. } => to_vec_sr(&meta),
|
||||
KclValue::Uuid { meta, .. } => to_vec_sr(&meta),
|
||||
KclValue::Type { meta, .. } => to_vec_sr(&meta),
|
||||
KclValue::KclNone { meta, .. } => to_vec_sr(&meta),
|
||||
KclValue::Tombstone { .. } => unreachable!("Tombstone SourceRange"),
|
||||
}
|
||||
@ -216,15 +228,23 @@ impl From<&KclValue> for Vec<SourceRange> {
|
||||
KclValue::Number { meta, .. } => to_vec_sr(meta),
|
||||
KclValue::String { meta, .. } => to_vec_sr(meta),
|
||||
KclValue::Uuid { meta, .. } => to_vec_sr(meta),
|
||||
KclValue::Array { meta, .. } => to_vec_sr(meta),
|
||||
KclValue::MixedArray { meta, .. } => to_vec_sr(meta),
|
||||
KclValue::Object { meta, .. } => to_vec_sr(meta),
|
||||
KclValue::Module { meta, .. } => to_vec_sr(meta),
|
||||
KclValue::KclNone { meta, .. } => to_vec_sr(meta),
|
||||
KclValue::Type { meta, .. } => to_vec_sr(meta),
|
||||
KclValue::Tombstone { .. } => unreachable!("Tombstone &SourceRange"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&KclValue> for SourceRange {
|
||||
fn from(item: &KclValue) -> Self {
|
||||
let v: Vec<_> = item.into();
|
||||
v.into_iter().next().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl KclValue {
|
||||
pub(crate) fn metadata(&self) -> Vec<Metadata> {
|
||||
match self {
|
||||
@ -232,7 +252,7 @@ impl KclValue {
|
||||
KclValue::Bool { value: _, meta } => meta.clone(),
|
||||
KclValue::Number { meta, .. } => meta.clone(),
|
||||
KclValue::String { value: _, meta } => meta.clone(),
|
||||
KclValue::Array { value: _, meta } => meta.clone(),
|
||||
KclValue::MixedArray { value: _, meta } => meta.clone(),
|
||||
KclValue::Object { value: _, meta } => meta.clone(),
|
||||
KclValue::TagIdentifier(x) => x.meta.clone(),
|
||||
KclValue::TagDeclarator(x) => vec![x.metadata()],
|
||||
@ -247,6 +267,7 @@ impl KclValue {
|
||||
KclValue::Function { meta, .. } => meta.clone(),
|
||||
KclValue::Module { meta, .. } => meta.clone(),
|
||||
KclValue::KclNone { meta, .. } => meta.clone(),
|
||||
KclValue::Type { meta, .. } => meta.clone(),
|
||||
KclValue::Tombstone { .. } => unreachable!("Tombstone Metadata"),
|
||||
}
|
||||
}
|
||||
@ -268,7 +289,7 @@ impl KclValue {
|
||||
match self {
|
||||
KclValue::Solid { value } => Ok(SolidSet::Solid(value.clone())),
|
||||
KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())),
|
||||
KclValue::Array { value, .. } => {
|
||||
KclValue::MixedArray { value, .. } => {
|
||||
let solids: Vec<_> = value
|
||||
.iter()
|
||||
.enumerate()
|
||||
@ -314,9 +335,10 @@ impl KclValue {
|
||||
KclValue::Bool { .. } => "boolean (true/false value)",
|
||||
KclValue::Number { .. } => "number",
|
||||
KclValue::String { .. } => "string (text)",
|
||||
KclValue::Array { .. } => "array (list)",
|
||||
KclValue::MixedArray { .. } => "array (list)",
|
||||
KclValue::Object { .. } => "object",
|
||||
KclValue::Module { .. } => "module",
|
||||
KclValue::Type { .. } => "type",
|
||||
KclValue::KclNone { .. } => "None",
|
||||
KclValue::Tombstone { .. } => "TOMBSTONE",
|
||||
}
|
||||
@ -374,7 +396,7 @@ impl KclValue {
|
||||
|
||||
/// Put the point into a KCL value.
|
||||
pub fn from_point2d(p: [f64; 2], ty: NumericType, meta: Vec<Metadata>) -> Self {
|
||||
Self::Array {
|
||||
Self::MixedArray {
|
||||
value: vec![
|
||||
Self::Number {
|
||||
value: p[0],
|
||||
@ -430,7 +452,7 @@ impl KclValue {
|
||||
}
|
||||
|
||||
pub fn as_array(&self) -> Option<&[KclValue]> {
|
||||
if let KclValue::Array { value, meta: _ } = &self {
|
||||
if let KclValue::MixedArray { value, meta: _ } = &self {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
@ -589,22 +611,23 @@ impl KclValue {
|
||||
KclValue::Sketches { .. } => Some(RuntimeType::Array(PrimitiveType::Sketch)),
|
||||
KclValue::Solid { .. } => Some(RuntimeType::Primitive(PrimitiveType::Solid)),
|
||||
KclValue::Solids { .. } => Some(RuntimeType::Array(PrimitiveType::Solid)),
|
||||
KclValue::Array { value, .. } => Some(RuntimeType::Tuple(
|
||||
KclValue::MixedArray { value, .. } => Some(RuntimeType::Tuple(
|
||||
value
|
||||
.iter()
|
||||
.map(|v| v.principal_type().and_then(RuntimeType::primitive))
|
||||
.collect::<Option<Vec<_>>>()?,
|
||||
)),
|
||||
KclValue::Face { .. } => None,
|
||||
KclValue::Helix { .. } => None,
|
||||
KclValue::ImportedGeometry(..) => None,
|
||||
KclValue::Function { .. } => None,
|
||||
KclValue::Module { .. } => None,
|
||||
KclValue::TagIdentifier(_) => None,
|
||||
KclValue::TagDeclarator(_) => None,
|
||||
KclValue::KclNone { .. } => None,
|
||||
KclValue::Uuid { .. } => None,
|
||||
KclValue::Tombstone { .. } => None,
|
||||
KclValue::Helix { .. }
|
||||
| KclValue::ImportedGeometry(..)
|
||||
| KclValue::Function { .. }
|
||||
| KclValue::Module { .. }
|
||||
| KclValue::TagIdentifier(_)
|
||||
| KclValue::TagDeclarator(_)
|
||||
| KclValue::KclNone { .. }
|
||||
| KclValue::Type { .. }
|
||||
| KclValue::Uuid { .. }
|
||||
| KclValue::Tombstone { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -643,7 +666,7 @@ impl KclValue {
|
||||
result
|
||||
}
|
||||
KclValue::Function {
|
||||
value: FunctionSource::User { ast, memory },
|
||||
value: FunctionSource::User { ast, memory, .. },
|
||||
..
|
||||
} => crate::execution::exec_ast::call_user_defined_function(args, *memory, ast, exec_state, &ctx).await,
|
||||
_ => Err(KclError::Semantic(KclErrorDetails {
|
||||
@ -679,7 +702,7 @@ impl KclValue {
|
||||
todo!("Implement KCL stdlib fns with keyword args");
|
||||
}
|
||||
KclValue::Function {
|
||||
value: FunctionSource::User { ast, memory },
|
||||
value: FunctionSource::User { ast, memory, .. },
|
||||
..
|
||||
} => {
|
||||
crate::execution::exec_ast::call_user_defined_function_kw(args.kw_args, *memory, ast, exec_state, &ctx)
|
||||
@ -701,7 +724,7 @@ impl KclValue {
|
||||
KclValue::TagDeclarator(tag) => Some(format!("${}", tag.name)),
|
||||
KclValue::TagIdentifier(tag) => Some(format!("${}", tag.value)),
|
||||
// TODO better Array and Object stringification
|
||||
KclValue::Array { .. } => Some("[...]".to_owned()),
|
||||
KclValue::MixedArray { .. } => Some("[...]".to_owned()),
|
||||
KclValue::Object { .. } => Some("{ ... }".to_owned()),
|
||||
KclValue::Module { .. }
|
||||
| KclValue::Solid { .. }
|
||||
@ -714,6 +737,7 @@ impl KclValue {
|
||||
| KclValue::Plane { .. }
|
||||
| KclValue::Face { .. }
|
||||
| KclValue::KclNone { .. }
|
||||
| KclValue::Type { .. }
|
||||
| KclValue::Tombstone { .. } => None,
|
||||
}
|
||||
}
|
||||
@ -728,21 +752,29 @@ pub enum RuntimeType {
|
||||
}
|
||||
|
||||
impl RuntimeType {
|
||||
pub fn from_parsed(value: Type, settings: &super::MetaSettings) -> Option<Self> {
|
||||
match value {
|
||||
Type::Primitive(pt) => Some(RuntimeType::Primitive(PrimitiveType::from_parsed(pt, settings)?)),
|
||||
Type::Array(pt) => Some(RuntimeType::Array(PrimitiveType::from_parsed(pt, settings)?)),
|
||||
Type::Object { properties } => Some(RuntimeType::Object(
|
||||
properties
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
p.type_.and_then(|t| {
|
||||
RuntimeType::from_parsed(t.inner, settings).map(|ty| (p.identifier.inner.name, ty))
|
||||
})
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?,
|
||||
)),
|
||||
}
|
||||
pub fn from_parsed(
|
||||
value: Type,
|
||||
exec_state: &mut ExecState,
|
||||
source_range: SourceRange,
|
||||
) -> Result<Option<Self>, CompilationError> {
|
||||
Ok(match value {
|
||||
Type::Primitive(pt) => {
|
||||
PrimitiveType::from_parsed(pt, exec_state, source_range)?.map(RuntimeType::Primitive)
|
||||
}
|
||||
Type::Array(pt) => PrimitiveType::from_parsed(pt, exec_state, source_range)?.map(RuntimeType::Array),
|
||||
Type::Object { properties } => properties
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
let pt = match p.type_ {
|
||||
Some(t) => t,
|
||||
None => return Ok(None),
|
||||
};
|
||||
Ok(RuntimeType::from_parsed(pt.inner, exec_state, source_range)?
|
||||
.map(|ty| (p.identifier.inner.name, ty)))
|
||||
})
|
||||
.collect::<Result<Option<Vec<_>>, CompilationError>>()?
|
||||
.map(RuntimeType::Object),
|
||||
})
|
||||
}
|
||||
|
||||
// Subtype with no coercion, including refining numeric types.
|
||||
@ -802,16 +834,33 @@ pub enum PrimitiveType {
|
||||
}
|
||||
|
||||
impl PrimitiveType {
|
||||
fn from_parsed(value: AstPrimitiveType, settings: &super::MetaSettings) -> Option<Self> {
|
||||
match value {
|
||||
fn from_parsed(
|
||||
value: AstPrimitiveType,
|
||||
exec_state: &mut ExecState,
|
||||
source_range: SourceRange,
|
||||
) -> Result<Option<Self>, CompilationError> {
|
||||
Ok(match value {
|
||||
AstPrimitiveType::String => Some(PrimitiveType::String),
|
||||
AstPrimitiveType::Boolean => Some(PrimitiveType::Boolean),
|
||||
AstPrimitiveType::Number(suffix) => Some(PrimitiveType::Number(NumericType::from_parsed(suffix, settings))),
|
||||
AstPrimitiveType::Sketch => Some(PrimitiveType::Sketch),
|
||||
AstPrimitiveType::Solid => Some(PrimitiveType::Solid),
|
||||
AstPrimitiveType::Plane => Some(PrimitiveType::Plane),
|
||||
AstPrimitiveType::Number(suffix) => Some(PrimitiveType::Number(NumericType::from_parsed(
|
||||
suffix,
|
||||
&exec_state.mod_local.settings,
|
||||
))),
|
||||
AstPrimitiveType::Named(name) => {
|
||||
let ty_val = exec_state
|
||||
.stack()
|
||||
.get(&format!("{}{}", memory::TYPE_PREFIX, name.name), source_range)
|
||||
.map_err(|_| CompilationError::err(source_range, format!("Unknown type: {}", name.name)))?;
|
||||
|
||||
let (ty, _) = match ty_val {
|
||||
KclValue::Type { value: Some(ty), .. } => ty,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Some(ty.clone())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,6 +250,8 @@ use crate::{
|
||||
|
||||
/// The distinguished name of the return value of a function.
|
||||
pub(crate) const RETURN_NAME: &str = "__return";
|
||||
/// Low-budget namespacing for types.
|
||||
pub(crate) const TYPE_PREFIX: &str = "__ty_";
|
||||
|
||||
/// KCL memory. There should be only one ProgramMemory for the interpretation of a program (
|
||||
/// including other modules). Multiple interpretation runs should have fresh instances.
|
||||
@ -685,14 +687,6 @@ impl Stack {
|
||||
.insert_or_update(key, value, self.id);
|
||||
}
|
||||
|
||||
/// Delete an item from memory.
|
||||
///
|
||||
/// Item will be preserved in any snapshots.
|
||||
pub fn clear(&mut self, key: String) {
|
||||
self.memory.stats.mutation_count.fetch_add(1, Ordering::Relaxed);
|
||||
self.memory.get_env(self.current_env.index()).clear(key, self.id);
|
||||
}
|
||||
|
||||
/// Get a value from the program memory.
|
||||
/// Return Err if not found.
|
||||
pub fn get(&self, var: &str, source_range: SourceRange) -> Result<&KclValue, KclError> {
|
||||
@ -1165,19 +1159,6 @@ mod env {
|
||||
self.get_mut_bindings(owner).insert(key, value);
|
||||
}
|
||||
|
||||
/// Delete a key/value.
|
||||
///
|
||||
/// We want to preserve the snapshot, so we can't just remove the element. We copy the deleted
|
||||
/// value to the snapshot and replace the current value with a tombstone.
|
||||
pub(super) fn clear(&self, key: String, owner: usize) {
|
||||
if self.get_bindings().contains_key(&key) {
|
||||
let old = self.get_mut_bindings(owner).insert(key.clone(), tombstone()).unwrap();
|
||||
if let Some(s) = self.cur_snapshot(owner) {
|
||||
s.data.insert(key, old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Was the key contained in this environment at the specified point in time.
|
||||
fn snapshot_contains_key(&self, key: &str, snapshot: SnapshotRef) -> bool {
|
||||
for i in snapshot.index()..self.snapshots_len() {
|
||||
@ -1571,61 +1552,6 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_env_clear() {
|
||||
let mem = &mut Stack::new_for_tests();
|
||||
mem.add("a".to_owned(), val(1), sr()).unwrap();
|
||||
|
||||
mem.add("b".to_owned(), val(3), sr()).unwrap();
|
||||
let sn = mem.snapshot();
|
||||
|
||||
mem.push_new_env_for_call(sn);
|
||||
mem.snapshot();
|
||||
mem.add("b".to_owned(), val(4), sr()).unwrap();
|
||||
mem.snapshot();
|
||||
mem.clear("b".to_owned());
|
||||
mem.clear("a".to_owned());
|
||||
|
||||
assert_get(mem, "b", 3);
|
||||
assert_get(mem, "a", 1);
|
||||
|
||||
mem.pop_env();
|
||||
assert_get(mem, "b", 3);
|
||||
assert_get(mem, "a", 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_env_clear2() {
|
||||
let mem = &mut Stack::new_for_tests();
|
||||
mem.add("a".to_owned(), val(1), sr()).unwrap();
|
||||
mem.add("b".to_owned(), val(3), sr()).unwrap();
|
||||
let sn1 = mem.snapshot();
|
||||
mem.clear("b".to_owned());
|
||||
mem.clear("a".to_owned());
|
||||
mem.get("b", SourceRange::default()).unwrap_err();
|
||||
mem.get("a", SourceRange::default()).unwrap_err();
|
||||
|
||||
let sn = mem.snapshot();
|
||||
mem.push_new_env_for_call(sn);
|
||||
mem.add("b".to_owned(), val(4), sr()).unwrap();
|
||||
let sn2 = mem.snapshot();
|
||||
mem.clear("b".to_owned());
|
||||
mem.clear("a".to_owned());
|
||||
mem.get("b", SourceRange::default()).unwrap_err();
|
||||
mem.get("a", SourceRange::default()).unwrap_err();
|
||||
|
||||
mem.pop_env();
|
||||
mem.get("b", SourceRange::default()).unwrap_err();
|
||||
mem.get("a", SourceRange::default()).unwrap_err();
|
||||
|
||||
assert_get_from(mem, "a", 1, sn1);
|
||||
assert_get_from(mem, "b", 3, sn1);
|
||||
mem.memory
|
||||
.get_from_unchecked("a", sn2, SourceRange::default())
|
||||
.unwrap_err();
|
||||
assert_get_from(mem, "b", 4, sn2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn squash_env() {
|
||||
let mem = &mut Stack::new_for_tests();
|
||||
@ -1639,6 +1565,7 @@ mod test {
|
||||
KclValue::Function {
|
||||
value: FunctionSource::User {
|
||||
ast: crate::parsing::ast::types::FunctionExpression::dummy(),
|
||||
settings: crate::MetaSettings::default(),
|
||||
memory: sn2,
|
||||
},
|
||||
meta: Vec::new(),
|
||||
|
@ -14,7 +14,7 @@ pub(crate) use import::{
|
||||
import_foreign, send_to_engine as send_import_to_engine, PreImportedGeometry, ZOO_COORD_SYSTEM,
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen};
|
||||
pub use kcl_value::{KclObjectFields, KclValue, PrimitiveType, UnitAngle, UnitLen};
|
||||
use kcmc::{
|
||||
each_cmd as mcmd,
|
||||
ok_response::{output::TakeSnapshot, OkModelingCmdResponse},
|
||||
@ -55,7 +55,7 @@ mod memory;
|
||||
mod state;
|
||||
|
||||
/// Outcome of executing a program. This is used in TS.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS)]
|
||||
#[derive(Debug, Clone, Serialize, ts_rs::TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecOutcome {
|
||||
@ -517,7 +517,6 @@ impl ExecutorContext {
|
||||
&self,
|
||||
program: crate::Program,
|
||||
use_prev_memory: bool,
|
||||
variables: IndexMap<String, KclValue>,
|
||||
) -> Result<ExecOutcome, KclErrorWithOutputs> {
|
||||
assert!(self.is_mock());
|
||||
|
||||
@ -531,22 +530,9 @@ impl ExecutorContext {
|
||||
self.prepare_mem(&mut exec_state).await?
|
||||
};
|
||||
|
||||
let mut to_restore = Vec::new();
|
||||
{
|
||||
let mem = exec_state.mut_stack();
|
||||
|
||||
// Push a scope so that old variables can be overwritten (since we might be re-executing some
|
||||
// part of the scene).
|
||||
mem.push_new_env_for_scope();
|
||||
|
||||
// Add any extra variables to memory (we want to remove these variables after execution, but
|
||||
// can't do this using scopes because we want to keep the results of computation in other cases).
|
||||
for (k, v) in variables {
|
||||
to_restore.push((k.clone(), mem.get(&k, SourceRange::default()).ok().cloned()));
|
||||
mem.add(k, v, SourceRange::synthetic())
|
||||
.map_err(KclErrorWithOutputs::no_outputs)?;
|
||||
}
|
||||
}
|
||||
// Push a scope so that old variables can be overwritten (since we might be re-executing some
|
||||
// part of the scene).
|
||||
exec_state.mut_stack().push_new_env_for_scope();
|
||||
|
||||
let result = self.inner_run(&program, &mut exec_state, true).await?;
|
||||
|
||||
@ -558,12 +544,6 @@ impl ExecutorContext {
|
||||
let outcome = exec_state.to_mock_wasm_outcome(result.0);
|
||||
|
||||
mem.squash_env(result.0);
|
||||
for (k, v) in to_restore {
|
||||
match v {
|
||||
Some(v) => mem.insert_or_update(k, v),
|
||||
None => mem.clear(k),
|
||||
}
|
||||
}
|
||||
cache::write_old_memory(mem).await;
|
||||
|
||||
Ok(outcome)
|
||||
@ -645,8 +625,6 @@ impl ExecutorContext {
|
||||
let mut exec_state = old_state;
|
||||
exec_state.reset(&self.settings);
|
||||
|
||||
exec_state.add_root_module_contents(&program);
|
||||
|
||||
// We don't do this in mock mode since there is no engine connection
|
||||
// anyways and from the TS side we override memory and don't want to clear it.
|
||||
self.send_clear_scene(&mut exec_state, Default::default())
|
||||
@ -656,7 +634,6 @@ impl ExecutorContext {
|
||||
(exec_state, false)
|
||||
} else {
|
||||
old_state.mut_stack().restore_env(result_env);
|
||||
old_state.add_root_module_contents(&program);
|
||||
|
||||
(old_state, true)
|
||||
};
|
||||
@ -664,7 +641,6 @@ impl ExecutorContext {
|
||||
(program, exec_state, preserve_mem)
|
||||
} else {
|
||||
let mut exec_state = ExecState::new(&self.settings);
|
||||
exec_state.add_root_module_contents(&program);
|
||||
self.send_clear_scene(&mut exec_state, Default::default())
|
||||
.await
|
||||
.map_err(KclErrorWithOutputs::no_outputs)?;
|
||||
@ -703,28 +679,7 @@ impl ExecutorContext {
|
||||
&self,
|
||||
program: &crate::Program,
|
||||
exec_state: &mut ExecState,
|
||||
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclError> {
|
||||
self.run_with_ui_outputs(program, exec_state)
|
||||
.await
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Perform the execution of a program.
|
||||
///
|
||||
/// You can optionally pass in some initialization memory for partial
|
||||
/// execution.
|
||||
///
|
||||
/// The error includes additional outputs used for the feature tree and
|
||||
/// artifact graph.
|
||||
pub async fn run_with_ui_outputs(
|
||||
&self,
|
||||
program: &crate::Program,
|
||||
exec_state: &mut ExecState,
|
||||
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
|
||||
exec_state.add_root_module_contents(program);
|
||||
self.send_clear_scene(exec_state, Default::default())
|
||||
.await
|
||||
.map_err(KclErrorWithOutputs::no_outputs)?;
|
||||
self.inner_run(program, exec_state, false).await
|
||||
}
|
||||
|
||||
@ -736,6 +691,8 @@ impl ExecutorContext {
|
||||
exec_state: &mut ExecState,
|
||||
preserve_mem: bool,
|
||||
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
|
||||
exec_state.add_root_module_contents(program);
|
||||
|
||||
let _stats = crate::log::LogPerfStats::new("Interpretation");
|
||||
|
||||
// Re-apply the settings, in case the cache was busted.
|
||||
@ -902,18 +859,83 @@ impl ExecutorContext {
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
/// Export the current scene as a CAD file.
|
||||
pub async fn export(
|
||||
&self,
|
||||
format: kittycad_modeling_cmds::format::OutputFormat3d,
|
||||
) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
|
||||
let resp = self
|
||||
.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
crate::SourceRange::default(),
|
||||
&kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
|
||||
entity_ids: vec![],
|
||||
format,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
|
||||
return Err(KclError::Internal(crate::errors::KclErrorDetails {
|
||||
message: format!("Expected Export response, got {resp:?}",),
|
||||
source_ranges: vec![SourceRange::default()],
|
||||
}));
|
||||
};
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Export the current scene as a STEP file.
|
||||
pub async fn export_step(
|
||||
&self,
|
||||
deterministic_time: bool,
|
||||
) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
|
||||
let mut files = self
|
||||
.export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
|
||||
kittycad_modeling_cmds::format::step::export::Options {
|
||||
coords: *kittycad_modeling_cmds::coord::KITTYCAD,
|
||||
created: None,
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
|
||||
if deterministic_time {
|
||||
for kittycad_modeling_cmds::websocket::RawFile { contents, .. } in &mut files {
|
||||
use std::fmt::Write;
|
||||
let utf8 = std::str::from_utf8(contents).unwrap();
|
||||
let mut postprocessed = String::new();
|
||||
for line in utf8.lines() {
|
||||
if line.starts_with("FILE_NAME") {
|
||||
let name = "test.step";
|
||||
let time = "2021-01-01T00:00:00Z";
|
||||
let author = "Test";
|
||||
let org = "Zoo";
|
||||
let version = "zoo.dev beta";
|
||||
let system = "zoo.dev";
|
||||
let authorization = "Test";
|
||||
writeln!(&mut postprocessed, "FILE_NAME('{name}', '{time}', ('{author}'), ('{org}'), '{version}', '{system}', '{authorization}');").unwrap();
|
||||
} else {
|
||||
writeln!(&mut postprocessed, "{line}").unwrap();
|
||||
}
|
||||
}
|
||||
*contents = postprocessed.into_bytes();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
pub async fn close(&self) {
|
||||
self.engine.close().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn parse_execute(
|
||||
code: &str,
|
||||
) -> Result<(crate::Program, EnvironmentRef, ExecutorContext, ExecState), KclError> {
|
||||
pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
|
||||
let program = crate::Program::parse_no_errs(code)?;
|
||||
|
||||
let ctx = ExecutorContext {
|
||||
let exec_ctxt = ExecutorContext {
|
||||
engine: Arc::new(Box::new(
|
||||
crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
|
||||
KclError::Internal(crate::errors::KclErrorDetails {
|
||||
@ -927,10 +949,24 @@ pub(crate) async fn parse_execute(
|
||||
settings: Default::default(),
|
||||
context_type: ContextType::Mock,
|
||||
};
|
||||
let mut exec_state = ExecState::new(&ctx.settings);
|
||||
let result = ctx.run(&program, &mut exec_state).await?;
|
||||
let mut exec_state = ExecState::new(&exec_ctxt.settings);
|
||||
let result = exec_ctxt.run(&program, &mut exec_state).await?;
|
||||
|
||||
Ok((program, result.0, ctx, exec_state))
|
||||
Ok(ExecTestResults {
|
||||
program,
|
||||
mem_env: result.0,
|
||||
exec_ctxt,
|
||||
exec_state,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ExecTestResults {
|
||||
program: crate::Program,
|
||||
mem_env: EnvironmentRef,
|
||||
exec_ctxt: ExecutorContext,
|
||||
exec_state: ExecState,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -953,8 +989,8 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_execute_warn() {
|
||||
let text = "@blah";
|
||||
let (_, _, _, exec_state) = parse_execute(text).await.unwrap();
|
||||
let errs = exec_state.errors();
|
||||
let result = parse_execute(text).await.unwrap();
|
||||
let errs = result.exec_state.errors();
|
||||
assert_eq!(errs.len(), 1);
|
||||
assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
|
||||
assert!(
|
||||
@ -967,8 +1003,8 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_warn_on_deprecated() {
|
||||
let text = "p = pi()";
|
||||
let (_, _, _, exec_state) = parse_execute(text).await.unwrap();
|
||||
let errs = exec_state.errors();
|
||||
let result = parse_execute(text).await.unwrap();
|
||||
let errs = result.exec_state.errors();
|
||||
assert_eq!(errs.len(), 1);
|
||||
assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
|
||||
assert!(
|
||||
@ -1054,8 +1090,8 @@ const objExpShouldNotBeIncluded = { a: 1, b: 2, c: 3 }
|
||||
|
||||
const part001 = startSketchOn(XY)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> yLineTo(1, %)
|
||||
|> xLine(3.84, %) // selection-range-7ish-before-this
|
||||
|> yLine(endAbsolute = 1)
|
||||
|> xLine(length = 3.84) // selection-range-7ish-before-this
|
||||
|
||||
const variableBelowShouldNotBeIncluded = 3
|
||||
"#;
|
||||
@ -1344,8 +1380,8 @@ const answer = returnX()"#;
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_override_prelude() {
|
||||
let text = "PI = 3.0";
|
||||
let (_, _, _, exec_state) = parse_execute(text).await.unwrap();
|
||||
let errs = exec_state.errors();
|
||||
let result = parse_execute(text).await.unwrap();
|
||||
let errs = result.exec_state.errors();
|
||||
assert!(errs.is_empty());
|
||||
}
|
||||
|
||||
@ -1411,50 +1447,79 @@ let shape = layer() |> patternTransform(instances = 10, transform = transform)
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_execute_with_functions() {
|
||||
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
|
||||
let (_, env, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(5.0, mem_get_json(exec_state.stack(), env, "myVar").as_f64().unwrap());
|
||||
let result = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
5.0,
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
|
||||
.as_f64()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_execute() {
|
||||
let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
|
||||
let (_, env, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(7.4, mem_get_json(exec_state.stack(), env, "myVar").as_f64().unwrap());
|
||||
let result = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
7.4,
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
|
||||
.as_f64()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_execute_start_negative() {
|
||||
let ast = r#"const myVar = -5 + 6"#;
|
||||
let (_, env, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(1.0, mem_get_json(exec_state.stack(), env, "myVar").as_f64().unwrap());
|
||||
let result = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
1.0,
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
|
||||
.as_f64()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_execute_with_pi() {
|
||||
let ast = r#"const myVar = PI * 2"#;
|
||||
let (_, env, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
let result = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
std::f64::consts::TAU,
|
||||
mem_get_json(exec_state.stack(), env, "myVar").as_f64().unwrap()
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
|
||||
.as_f64()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_math_define_decimal_without_leading_zero() {
|
||||
let ast = r#"let thing = .4 + 7"#;
|
||||
let (_, env, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(7.4, mem_get_json(exec_state.stack(), env, "thing").as_f64().unwrap());
|
||||
let result = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
7.4,
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
|
||||
.as_f64()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_unit_default() {
|
||||
let ast = r#"const inMm = 25.4 * mm()
|
||||
const inInches = 1.0 * inch()"#;
|
||||
let (_, env, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(25.4, mem_get_json(exec_state.stack(), env, "inMm").as_f64().unwrap());
|
||||
let result = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
25.4,
|
||||
mem_get_json(exec_state.stack(), env, "inInches").as_f64().unwrap()
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "inMm")
|
||||
.as_f64()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
25.4,
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "inInches")
|
||||
.as_f64()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
@ -1463,12 +1528,20 @@ const inInches = 1.0 * inch()"#;
|
||||
let ast = r#"@settings(defaultLengthUnit = inch)
|
||||
const inMm = 25.4 * mm()
|
||||
const inInches = 1.0 * inch()"#;
|
||||
let (_, env, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
let result = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
1.0,
|
||||
mem_get_json(exec_state.stack(), env, "inMm").as_f64().unwrap().round()
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "inMm")
|
||||
.as_f64()
|
||||
.unwrap()
|
||||
.round()
|
||||
);
|
||||
assert_eq!(
|
||||
1.0,
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "inInches")
|
||||
.as_f64()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(1.0, mem_get_json(exec_state.stack(), env, "inInches").as_f64().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -1476,12 +1549,20 @@ const inInches = 1.0 * inch()"#;
|
||||
let ast = r#"@settings(defaultLengthUnit = in)
|
||||
const inMm = 25.4 * mm()
|
||||
const inInches = 2.0 * inch()"#;
|
||||
let (_, env, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
let result = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
1.0,
|
||||
mem_get_json(exec_state.stack(), env, "inMm").as_f64().unwrap().round()
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "inMm")
|
||||
.as_f64()
|
||||
.unwrap()
|
||||
.round()
|
||||
);
|
||||
assert_eq!(
|
||||
2.0,
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "inInches")
|
||||
.as_f64()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(2.0, mem_get_json(exec_state.stack(), env, "inInches").as_f64().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -1520,17 +1601,31 @@ fn check = (x) => {
|
||||
}
|
||||
check(false)
|
||||
"#;
|
||||
let (_, env, _, exec_state) = parse_execute(ast).await.unwrap();
|
||||
let result = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(
|
||||
false,
|
||||
mem_get_json(exec_state.stack(), env, "notTrue").as_bool().unwrap()
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
|
||||
.as_bool()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
true,
|
||||
mem_get_json(exec_state.stack(), env, "notFalse").as_bool().unwrap()
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
|
||||
.as_bool()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
true,
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "c")
|
||||
.as_bool()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
false,
|
||||
mem_get_json(result.exec_state.stack(), result.mem_env, "d")
|
||||
.as_bool()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(true, mem_get_json(exec_state.stack(), env, "c").as_bool().unwrap());
|
||||
assert_eq!(false, mem_get_json(exec_state.stack(), env, "d").as_bool().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -1783,9 +1878,9 @@ let w = f() + f()
|
||||
async fn kcl_test_ids_stable_between_executions() {
|
||||
let code = r#"sketch001 = startSketchOn(XZ)
|
||||
|> startProfileAt([61.74, 206.13], %)
|
||||
|> xLine(305.11, %, $seg01)
|
||||
|> yLine(-291.85, %)
|
||||
|> xLine(-segLen(seg01), %)
|
||||
|> xLine(length = 305.11, tag = $seg01)
|
||||
|> yLine(length = -291.85)
|
||||
|> xLine(length = -segLen(seg01))
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
|> extrude(length = 40.14)
|
||||
@ -1808,9 +1903,9 @@ let w = f() + f()
|
||||
|
||||
let code = r#"sketch001 = startSketchOn(XZ)
|
||||
|> startProfileAt([62.74, 206.13], %)
|
||||
|> xLine(305.11, %, $seg01)
|
||||
|> yLine(-291.85, %)
|
||||
|> xLine(-segLen(seg01), %)
|
||||
|> xLine(length = 305.11, tag = $seg01)
|
||||
|> yLine(length = -291.85)
|
||||
|> xLine(length = -segLen(seg01))
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
|> extrude(length = 40.14)
|
||||
@ -1834,9 +1929,9 @@ let w = f() + f()
|
||||
async fn kcl_test_changing_a_setting_updates_the_cached_state() {
|
||||
let code = r#"sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([61.74, 206.13], %)
|
||||
|> xLine(305.11, %, $seg01)
|
||||
|> yLine(-291.85, %)
|
||||
|> xLine(-segLen(seg01), %)
|
||||
|> xLine(length = 305.11, tag = $seg01)
|
||||
|> yLine(length = -291.85)
|
||||
|> xLine(length = -segLen(seg01))
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
|> extrude(length = 40.14)
|
||||
@ -1885,33 +1980,6 @@ let w = f() + f()
|
||||
assert_eq!(settings_state, ctx.settings);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn mock_variables() {
|
||||
let ctx = ExecutorContext::new_mock().await;
|
||||
|
||||
let program = crate::Program::parse_no_errs("x = y").unwrap();
|
||||
let mut vars = IndexMap::new();
|
||||
vars.insert(
|
||||
"y".to_owned(),
|
||||
KclValue::Number {
|
||||
value: 2.0,
|
||||
ty: kcl_value::NumericType::Unknown,
|
||||
meta: Vec::new(),
|
||||
},
|
||||
);
|
||||
let result = ctx.run_mock(program, true, vars).await.unwrap();
|
||||
assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
|
||||
cache::read_old_memory()
|
||||
.await
|
||||
.unwrap()
|
||||
.get("y", SourceRange::default())
|
||||
.unwrap_err();
|
||||
|
||||
let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
|
||||
let result = ctx.run_mock(program2, true, IndexMap::new()).await.unwrap();
|
||||
assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn mock_after_not_mock() {
|
||||
let ctx = ExecutorContext::new_with_default_client(UnitLength::Mm).await.unwrap();
|
||||
@ -1921,7 +1989,7 @@ let w = f() + f()
|
||||
|
||||
let ctx2 = ExecutorContext::new_mock().await;
|
||||
let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
|
||||
let result = ctx2.run_mock(program2, true, IndexMap::new()).await.unwrap();
|
||||
let result = ctx2.run_mock(program2, true).await.unwrap();
|
||||
assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,10 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails, Severity},
|
||||
execution::{
|
||||
annotations, kcl_value, memory::ProgramMemory, memory::Stack, Artifact, ArtifactCommand, ArtifactGraph,
|
||||
ArtifactId, EnvironmentRef, ExecOutcome, ExecutorSettings, KclValue, Operation, UnitAngle, UnitLen,
|
||||
annotations, kcl_value,
|
||||
memory::{ProgramMemory, Stack},
|
||||
Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, EnvironmentRef, ExecOutcome, ExecutorSettings, KclValue,
|
||||
Operation, UnitAngle, UnitLen,
|
||||
},
|
||||
modules::{ModuleId, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr, ModuleSource},
|
||||
parsing::ast::types::Annotation,
|
||||
|
@ -76,7 +76,7 @@ pub mod std;
|
||||
pub mod test_server;
|
||||
mod thread;
|
||||
mod unparser;
|
||||
pub mod walk;
|
||||
mod walk;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm;
|
||||
|
||||
|
@ -32,6 +32,10 @@ pub(super) enum Hover {
|
||||
callee_name: String,
|
||||
range: LspRange,
|
||||
},
|
||||
Type {
|
||||
name: String,
|
||||
range: LspRange,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -72,7 +76,6 @@ impl Program {
|
||||
}
|
||||
|
||||
let value = self.get_expr_for_position(pos)?;
|
||||
|
||||
value.get_hover_value_for_position(pos, code, opts)
|
||||
}
|
||||
}
|
||||
@ -120,8 +123,10 @@ impl Expr {
|
||||
Expr::TagDeclarator(_) => None,
|
||||
// TODO LSP hover info for tag
|
||||
Expr::LabelledExpression(expr) => expr.expr.get_hover_value_for_position(pos, code, opts),
|
||||
// TODO LSP hover info for type
|
||||
Expr::AscribedExpression(expr) => expr.expr.get_hover_value_for_position(pos, code, opts),
|
||||
Expr::AscribedExpression(expr) => expr
|
||||
.ty
|
||||
.get_hover_value_for_position(pos, code, opts)
|
||||
.or_else(|| expr.expr.get_hover_value_for_position(pos, code, opts)),
|
||||
// TODO: LSP hover information for symbols. https://github.com/KittyCAD/modeling-app/issues/1127
|
||||
Expr::PipeSubstitution(_) => None,
|
||||
}
|
||||
@ -334,8 +339,43 @@ impl PipeExpression {
|
||||
}
|
||||
}
|
||||
|
||||
impl Node<Type> {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, _opts: &HoverOpts) -> Option<Hover> {
|
||||
let range = self.as_source_range();
|
||||
if range.contains(pos) {
|
||||
match &self.inner {
|
||||
Type::Array(t) | Type::Primitive(t) => {
|
||||
let mut name = t.to_string();
|
||||
if name.ends_with(')') {
|
||||
name.truncate(name.find('(').unwrap());
|
||||
}
|
||||
return Some(Hover::Type {
|
||||
name,
|
||||
range: range.to_lsp_range(code),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionExpression {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
if let Some(ty) = &self.return_type {
|
||||
if let Some(h) = ty.get_hover_value_for_position(pos, code, opts) {
|
||||
return Some(h);
|
||||
}
|
||||
}
|
||||
for arg in &self.params {
|
||||
if let Some(ty) = &arg.type_ {
|
||||
if let Some(h) = ty.get_hover_value_for_position(pos, code, opts) {
|
||||
return Some(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(value) = self.body.get_expr_for_position(pos) {
|
||||
let mut vars = opts.vars.clone().unwrap_or_default();
|
||||
for arg in &self.params {
|
||||
|
@ -1105,6 +1105,34 @@ impl LanguageServer for Backend {
|
||||
range: Some(range),
|
||||
}))
|
||||
}
|
||||
Hover::Type { name, range } => {
|
||||
let Some(completion) = self.stdlib_completions.get(&name) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(docs) = &completion.documentation else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let docs = match docs {
|
||||
Documentation::String(docs) => docs,
|
||||
Documentation::MarkupContent(MarkupContent { value, .. }) => value,
|
||||
};
|
||||
|
||||
let docs = if docs.len() > 320 {
|
||||
let end = docs.find("\n\n").or_else(|| docs.find("\n\r\n")).unwrap_or(320);
|
||||
&docs[..end]
|
||||
} else {
|
||||
&**docs
|
||||
};
|
||||
|
||||
Ok(Some(LspHover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("```\n{}\n```\n\n{}", name, docs),
|
||||
}),
|
||||
range: Some(range),
|
||||
}))
|
||||
}
|
||||
Hover::KwArg {
|
||||
name,
|
||||
callee_name,
|
||||
|
@ -5,8 +5,8 @@ use crate::parsing::ast::types::{
|
||||
CallExpression, CallExpressionKw, DefaultParamVal, ElseIf, Expr, ExpressionStatement, FunctionExpression,
|
||||
Identifier, IfExpression, ImportItem, ImportSelector, ImportStatement, ItemVisibility, KclNone, LabelledExpression,
|
||||
Literal, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, ObjectExpression, ObjectProperty,
|
||||
Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement, TagDeclarator, Type, UnaryExpression,
|
||||
VariableDeclaration, VariableDeclarator, VariableKind,
|
||||
Parameter, PipeExpression, PipeSubstitution, PrimitiveType, Program, ReturnStatement, TagDeclarator, Type,
|
||||
TypeDeclaration, UnaryExpression, VariableDeclaration, VariableDeclarator, VariableKind,
|
||||
};
|
||||
|
||||
/// Position-independent digest of the AST node.
|
||||
@ -113,6 +113,7 @@ impl BodyItem {
|
||||
BodyItem::ImportStatement(s) => s.compute_digest(),
|
||||
BodyItem::ExpressionStatement(es) => es.compute_digest(),
|
||||
BodyItem::VariableDeclaration(vs) => vs.compute_digest(),
|
||||
BodyItem::TypeDeclaration(t) => t.compute_digest(),
|
||||
BodyItem::ReturnStatement(rs) => rs.compute_digest(),
|
||||
});
|
||||
|
||||
@ -191,11 +192,11 @@ impl Type {
|
||||
match self {
|
||||
Type::Primitive(prim) => {
|
||||
hasher.update(b"FnArgType::Primitive");
|
||||
hasher.update(prim.digestable_id())
|
||||
hasher.update(prim.compute_digest())
|
||||
}
|
||||
Type::Array(prim) => {
|
||||
hasher.update(b"FnArgType::Array");
|
||||
hasher.update(prim.digestable_id())
|
||||
hasher.update(prim.compute_digest())
|
||||
}
|
||||
Type::Object { properties } => {
|
||||
hasher.update(b"FnArgType::Object");
|
||||
@ -210,6 +211,21 @@ impl Type {
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType {
|
||||
pub fn compute_digest(&mut self) -> Digest {
|
||||
let mut hasher = Sha256::new();
|
||||
match self {
|
||||
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"),
|
||||
PrimitiveType::Tag => hasher.update(b"tag"),
|
||||
}
|
||||
|
||||
hasher.finalize().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Parameter {
|
||||
compute_digest!(|slf, hasher| {
|
||||
hasher.update(slf.identifier.compute_digest());
|
||||
@ -275,6 +291,18 @@ impl VariableDeclaration {
|
||||
});
|
||||
}
|
||||
|
||||
impl TypeDeclaration {
|
||||
compute_digest!(|slf, hasher| {
|
||||
hasher.update(slf.name.compute_digest());
|
||||
if let Some(args) = &mut slf.args {
|
||||
hasher.update([1]);
|
||||
for a in args {
|
||||
hasher.update(a.compute_digest());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl VariableKind {
|
||||
fn digestable_id(&self) -> [u8; 1] {
|
||||
match self {
|
||||
|
@ -13,6 +13,7 @@ impl BodyItem {
|
||||
BodyItem::ImportStatement(stmt) => stmt.module_id,
|
||||
BodyItem::ExpressionStatement(expression_statement) => expression_statement.module_id,
|
||||
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.module_id,
|
||||
BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.module_id,
|
||||
BodyItem::ReturnStatement(return_statement) => return_statement.module_id,
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,9 @@ pub async fn modify_ast_for_sketch(
|
||||
let constraint_level = match ast_sketch {
|
||||
super::types::Definition::Variable(var) => var.get_constraint_level(),
|
||||
super::types::Definition::Import(import) => import.get_constraint_level(),
|
||||
super::types::Definition::Type(_) => ConstraintLevel::Ignore {
|
||||
source_ranges: Vec::new(),
|
||||
},
|
||||
};
|
||||
match &constraint_level {
|
||||
ConstraintLevel::None { source_ranges: _ } => {}
|
||||
|
@ -38,6 +38,7 @@ mod none;
|
||||
pub enum Definition<'a> {
|
||||
Variable(&'a VariableDeclarator),
|
||||
Import(NodeRef<'a, ImportStatement>),
|
||||
Type(NodeRef<'a, TypeDeclaration>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
|
||||
@ -54,18 +55,6 @@ pub struct Node<T> {
|
||||
pub outer_attrs: NodeList<Annotation>,
|
||||
}
|
||||
|
||||
impl<T> Node<T> {
|
||||
pub fn metadata(&self) -> Metadata {
|
||||
Metadata {
|
||||
source_range: SourceRange::new(self.start, self.end, self.module_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, pos: usize) -> bool {
|
||||
self.start <= pos && pos <= self.end
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JsonSchema> schemars::JsonSchema for Node<T> {
|
||||
fn schema_name() -> String {
|
||||
T::schema_name()
|
||||
@ -126,6 +115,26 @@ impl<T> Node<T> {
|
||||
pub fn as_source_ranges(&self) -> Vec<SourceRange> {
|
||||
vec![self.as_source_range()]
|
||||
}
|
||||
|
||||
pub fn metadata(&self) -> Metadata {
|
||||
Metadata {
|
||||
source_range: SourceRange::new(self.start, self.end, self.module_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, pos: usize) -> bool {
|
||||
self.start <= pos && pos <= self.end
|
||||
}
|
||||
|
||||
pub fn map<U>(self, f: fn(T) -> U) -> Node<U> {
|
||||
Node {
|
||||
inner: f(self.inner),
|
||||
start: self.start,
|
||||
end: self.end,
|
||||
module_id: self.module_id,
|
||||
outer_attrs: self.outer_attrs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Node<T> {
|
||||
@ -352,7 +361,7 @@ impl Program {
|
||||
|
||||
// Recurse over the item.
|
||||
match item {
|
||||
BodyItem::ImportStatement(_) => None,
|
||||
BodyItem::ImportStatement(_) | BodyItem::TypeDeclaration(_) => None,
|
||||
BodyItem::ExpressionStatement(expression_statement) => Some(&expression_statement.expression),
|
||||
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.get_expr_for_position(pos),
|
||||
BodyItem::ReturnStatement(return_statement) => Some(&return_statement.argument),
|
||||
@ -373,6 +382,7 @@ impl Program {
|
||||
Some(BodyItem::VariableDeclaration(variable_declaration)) => {
|
||||
variable_declaration.get_expr_for_position(pos)
|
||||
}
|
||||
Some(BodyItem::TypeDeclaration(_)) => None,
|
||||
Some(BodyItem::ReturnStatement(return_statement)) => Some(&return_statement.argument),
|
||||
None => return false,
|
||||
};
|
||||
@ -395,7 +405,7 @@ impl Program {
|
||||
// We only care about the top level things in the program.
|
||||
for item in &self.body {
|
||||
match item {
|
||||
BodyItem::ImportStatement(_) => continue,
|
||||
BodyItem::ImportStatement(_) | BodyItem::TypeDeclaration(_) => continue,
|
||||
BodyItem::ExpressionStatement(expression_statement) => {
|
||||
if let Some(folding_range) = expression_statement.expression.get_lsp_folding_range() {
|
||||
ranges.push(folding_range)
|
||||
@ -425,16 +435,13 @@ impl Program {
|
||||
break;
|
||||
}
|
||||
}
|
||||
BodyItem::ExpressionStatement(_expression_statement) => {
|
||||
continue;
|
||||
}
|
||||
BodyItem::VariableDeclaration(ref mut variable_declaration) => {
|
||||
if let Some(var_old_name) = variable_declaration.rename_symbol(new_name, pos) {
|
||||
old_name = Some(var_old_name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
BodyItem::ReturnStatement(_return_statement) => continue,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -458,6 +465,7 @@ impl Program {
|
||||
BodyItem::VariableDeclaration(ref mut variable_declaration) => {
|
||||
variable_declaration.get_mut_expr_for_position(pos)
|
||||
}
|
||||
BodyItem::TypeDeclaration(_) => None,
|
||||
BodyItem::ReturnStatement(ref mut return_statement) => Some(&mut return_statement.argument),
|
||||
};
|
||||
|
||||
@ -483,16 +491,17 @@ impl Program {
|
||||
fn rename_identifiers(&mut self, old_name: &str, new_name: &str) {
|
||||
for item in &mut self.body {
|
||||
match item {
|
||||
BodyItem::ImportStatement(ref mut stmt) => {
|
||||
BodyItem::ImportStatement(stmt) => {
|
||||
stmt.rename_identifiers(old_name, new_name);
|
||||
}
|
||||
BodyItem::ExpressionStatement(ref mut expression_statement) => {
|
||||
BodyItem::ExpressionStatement(expression_statement) => {
|
||||
expression_statement.expression.rename_identifiers(old_name, new_name);
|
||||
}
|
||||
BodyItem::VariableDeclaration(ref mut variable_declaration) => {
|
||||
BodyItem::VariableDeclaration(variable_declaration) => {
|
||||
variable_declaration.rename_identifiers(old_name, new_name);
|
||||
}
|
||||
BodyItem::ReturnStatement(ref mut return_statement) => {
|
||||
BodyItem::TypeDeclaration(_) => {}
|
||||
BodyItem::ReturnStatement(return_statement) => {
|
||||
return_statement.argument.rename_identifiers(old_name, new_name);
|
||||
}
|
||||
}
|
||||
@ -506,7 +515,7 @@ impl Program {
|
||||
BodyItem::ImportStatement(_) => {
|
||||
continue;
|
||||
}
|
||||
BodyItem::ExpressionStatement(_expression_statement) => {
|
||||
BodyItem::ExpressionStatement(_) => {
|
||||
continue;
|
||||
}
|
||||
BodyItem::VariableDeclaration(ref mut variable_declaration) => {
|
||||
@ -515,7 +524,10 @@ impl Program {
|
||||
return;
|
||||
}
|
||||
}
|
||||
BodyItem::ReturnStatement(_return_statement) => continue,
|
||||
BodyItem::TypeDeclaration(_) => {
|
||||
continue;
|
||||
}
|
||||
BodyItem::ReturnStatement(_) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -531,6 +543,7 @@ impl Program {
|
||||
BodyItem::VariableDeclaration(ref mut variable_declaration) => {
|
||||
variable_declaration.replace_value(source_range, new_value.clone())
|
||||
}
|
||||
BodyItem::TypeDeclaration(_) => {}
|
||||
BodyItem::ReturnStatement(ref mut return_statement) => {
|
||||
return_statement.argument.replace_value(source_range, new_value.clone())
|
||||
}
|
||||
@ -555,6 +568,11 @@ impl Program {
|
||||
return Some(Definition::Variable(&variable_declaration.declaration));
|
||||
}
|
||||
}
|
||||
BodyItem::TypeDeclaration(ty_declaration) => {
|
||||
if ty_declaration.name.name == name {
|
||||
return Some(Definition::Type(ty_declaration));
|
||||
}
|
||||
}
|
||||
BodyItem::ReturnStatement(_return_statement) => continue,
|
||||
}
|
||||
}
|
||||
@ -588,6 +606,7 @@ pub enum BodyItem {
|
||||
ImportStatement(BoxNode<ImportStatement>),
|
||||
ExpressionStatement(Node<ExpressionStatement>),
|
||||
VariableDeclaration(BoxNode<VariableDeclaration>),
|
||||
TypeDeclaration(BoxNode<TypeDeclaration>),
|
||||
ReturnStatement(Node<ReturnStatement>),
|
||||
}
|
||||
|
||||
@ -597,6 +616,7 @@ impl BodyItem {
|
||||
BodyItem::ImportStatement(stmt) => stmt.start,
|
||||
BodyItem::ExpressionStatement(expression_statement) => expression_statement.start,
|
||||
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.start,
|
||||
BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.start,
|
||||
BodyItem::ReturnStatement(return_statement) => return_statement.start,
|
||||
}
|
||||
}
|
||||
@ -606,6 +626,7 @@ impl BodyItem {
|
||||
BodyItem::ImportStatement(stmt) => stmt.end,
|
||||
BodyItem::ExpressionStatement(expression_statement) => expression_statement.end,
|
||||
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.end,
|
||||
BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.end,
|
||||
BodyItem::ReturnStatement(return_statement) => return_statement.end,
|
||||
}
|
||||
}
|
||||
@ -615,6 +636,7 @@ impl BodyItem {
|
||||
BodyItem::ImportStatement(node) => node.outer_attrs = attr,
|
||||
BodyItem::ExpressionStatement(node) => node.outer_attrs = attr,
|
||||
BodyItem::VariableDeclaration(node) => node.outer_attrs = attr,
|
||||
BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.outer_attrs = attr,
|
||||
BodyItem::ReturnStatement(node) => node.outer_attrs = attr,
|
||||
}
|
||||
}
|
||||
@ -624,6 +646,7 @@ impl BodyItem {
|
||||
BodyItem::ImportStatement(node) => &node.outer_attrs,
|
||||
BodyItem::ExpressionStatement(node) => &node.outer_attrs,
|
||||
BodyItem::VariableDeclaration(node) => &node.outer_attrs,
|
||||
BodyItem::TypeDeclaration(ty_declaration) => &ty_declaration.outer_attrs,
|
||||
BodyItem::ReturnStatement(node) => &node.outer_attrs,
|
||||
}
|
||||
}
|
||||
@ -633,6 +656,7 @@ impl BodyItem {
|
||||
BodyItem::ImportStatement(node) => &mut node.outer_attrs,
|
||||
BodyItem::ExpressionStatement(node) => &mut node.outer_attrs,
|
||||
BodyItem::VariableDeclaration(node) => &mut node.outer_attrs,
|
||||
BodyItem::TypeDeclaration(ty_declaration) => &mut ty_declaration.outer_attrs,
|
||||
BodyItem::ReturnStatement(node) => &mut node.outer_attrs,
|
||||
}
|
||||
}
|
||||
@ -1765,6 +1789,20 @@ impl ItemVisibility {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub struct TypeDeclaration {
|
||||
pub name: Node<Identifier>,
|
||||
pub args: Option<NodeList<Identifier>>,
|
||||
#[serde(default, skip_serializing_if = "ItemVisibility::is_default")]
|
||||
pub visibility: ItemVisibility,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub digest: Option<Digest>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
@ -2832,7 +2870,8 @@ impl PipeExpression {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum PrimitiveType {
|
||||
@ -2845,39 +2884,16 @@ pub enum PrimitiveType {
|
||||
Boolean,
|
||||
/// A tag.
|
||||
Tag,
|
||||
/// A sketch type.
|
||||
Sketch,
|
||||
/// A sketch surface type.
|
||||
SketchSurface,
|
||||
/// An solid type.
|
||||
Solid,
|
||||
/// A plane.
|
||||
Plane,
|
||||
/// An identifier used as a type (not really a primitive type, but whatever).
|
||||
Named(Node<Identifier>),
|
||||
}
|
||||
|
||||
impl PrimitiveType {
|
||||
pub fn digestable_id(&self) -> &[u8] {
|
||||
match self {
|
||||
PrimitiveType::String => b"string",
|
||||
PrimitiveType::Number(suffix) => suffix.digestable_id(),
|
||||
PrimitiveType::Boolean => b"bool",
|
||||
PrimitiveType::Tag => b"tag",
|
||||
PrimitiveType::Sketch => b"Sketch",
|
||||
PrimitiveType::SketchSurface => b"SketchSurface",
|
||||
PrimitiveType::Solid => b"Solid",
|
||||
PrimitiveType::Plane => b"Plane",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str, suffix: Option<NumericSuffix>) -> Option<Self> {
|
||||
pub fn primitive_from_str(s: &str, suffix: Option<NumericSuffix>) -> Option<Self> {
|
||||
match (s, suffix) {
|
||||
("string", None) => Some(PrimitiveType::String),
|
||||
("bool", None) => Some(PrimitiveType::Boolean),
|
||||
("tag", None) => Some(PrimitiveType::Tag),
|
||||
("Sketch", None) => Some(PrimitiveType::Sketch),
|
||||
("SketchSurface", None) => Some(PrimitiveType::SketchSurface),
|
||||
("Solid", None) => Some(PrimitiveType::Solid),
|
||||
("Plane", None) => Some(PrimitiveType::Plane),
|
||||
("number", None) => Some(PrimitiveType::Number(NumericSuffix::None)),
|
||||
("number", Some(s)) => Some(PrimitiveType::Number(s)),
|
||||
_ => None,
|
||||
@ -2898,10 +2914,7 @@ impl fmt::Display for PrimitiveType {
|
||||
PrimitiveType::String => write!(f, "string"),
|
||||
PrimitiveType::Boolean => write!(f, "bool"),
|
||||
PrimitiveType::Tag => write!(f, "tag"),
|
||||
PrimitiveType::Sketch => write!(f, "Sketch"),
|
||||
PrimitiveType::SketchSurface => write!(f, "SketchSurface"),
|
||||
PrimitiveType::Solid => write!(f, "Solid"),
|
||||
PrimitiveType::Plane => write!(f, "Plane"),
|
||||
PrimitiveType::Named(n) => write!(f, "{}", n.name),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3456,11 +3469,11 @@ const cylinder = startSketchOn('-XZ')
|
||||
fn test_ast_in_comment_inline() {
|
||||
let some_program_string = r#"const part001 = startSketchOn('XY')
|
||||
|> startProfileAt([0,0], %)
|
||||
|> xLine(5, %) // lin
|
||||
|> xLine(length = 5) // lin
|
||||
"#;
|
||||
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
|
||||
|
||||
assert!(program.in_comment(86));
|
||||
assert!(program.in_comment(92));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
|
@ -27,7 +27,8 @@ use crate::{
|
||||
ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue, MemberExpression,
|
||||
MemberObject, Node, NodeList, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty,
|
||||
Parameter, PipeExpression, PipeSubstitution, PrimitiveType, Program, ReturnStatement, Shebang,
|
||||
TagDeclarator, Type, UnaryExpression, UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
|
||||
TagDeclarator, Type, TypeDeclaration, UnaryExpression, UnaryOperator, VariableDeclaration,
|
||||
VariableDeclarator, VariableKind,
|
||||
},
|
||||
math::BinaryExpressionToken,
|
||||
token::{Token, TokenSlice, TokenType},
|
||||
@ -1175,11 +1176,10 @@ fn function_decl(i: &mut TokenSlice) -> PResult<(Node<FunctionExpression>, bool)
|
||||
/// E.g. `person.name`
|
||||
fn member_expression_dot(i: &mut TokenSlice) -> PResult<(LiteralIdentifier, usize, bool)> {
|
||||
period.parse_next(i)?;
|
||||
let property = alt((
|
||||
sketch_keyword.map(Box::new).map(LiteralIdentifier::Identifier),
|
||||
nameable_identifier.map(Box::new).map(LiteralIdentifier::Identifier),
|
||||
))
|
||||
.parse_next(i)?;
|
||||
let property = nameable_identifier
|
||||
.map(Box::new)
|
||||
.map(LiteralIdentifier::Identifier)
|
||||
.parse_next(i)?;
|
||||
let end = property.end();
|
||||
Ok((property, end, false))
|
||||
}
|
||||
@ -1188,7 +1188,6 @@ fn member_expression_dot(i: &mut TokenSlice) -> PResult<(LiteralIdentifier, usiz
|
||||
fn member_expression_subscript(i: &mut TokenSlice) -> PResult<(LiteralIdentifier, usize, bool)> {
|
||||
let _ = open_bracket.parse_next(i)?;
|
||||
let property = alt((
|
||||
sketch_keyword.map(Box::new).map(LiteralIdentifier::Identifier),
|
||||
literal.map(LiteralIdentifier::Literal),
|
||||
nameable_identifier.map(Box::new).map(LiteralIdentifier::Identifier),
|
||||
))
|
||||
@ -1330,7 +1329,9 @@ fn body_items_within_function(i: &mut TokenSlice) -> PResult<WithinFunction> {
|
||||
// Any of the body item variants, each of which can optionally be followed by a comment.
|
||||
// If there is a comment, it may be preceded by whitespace.
|
||||
let item = dispatch! {peek(any);
|
||||
token if token.visibility_keyword().is_some() => (alt((declaration.map(BodyItem::VariableDeclaration), import_stmt.map(BodyItem::ImportStatement))), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
|
||||
token if token.visibility_keyword().is_some() => (alt((import_stmt.map(BodyItem::ImportStatement), ty_decl.map(BodyItem::TypeDeclaration), declaration.map(BodyItem::VariableDeclaration))), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
|
||||
token if token.value == "type" && matches!(token.token_type, TokenType::Keyword) =>
|
||||
(ty_decl.map(BodyItem::TypeDeclaration), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
|
||||
token if token.declaration_keyword().is_some() =>
|
||||
(declaration.map(BodyItem::VariableDeclaration), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
|
||||
token if token.value == "import" && matches!(token.token_type, TokenType::Keyword) =>
|
||||
@ -2058,6 +2059,52 @@ fn declaration(i: &mut TokenSlice) -> PResult<BoxNode<VariableDeclaration>> {
|
||||
}))
|
||||
}
|
||||
|
||||
fn ty_decl(i: &mut TokenSlice) -> PResult<BoxNode<TypeDeclaration>> {
|
||||
let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace))
|
||||
.parse_next(i)?
|
||||
.map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1)));
|
||||
|
||||
let decl_token = ty(i)?;
|
||||
let start = visibility_token.map(|t| t.start).unwrap_or_else(|| decl_token.start);
|
||||
whitespace(i)?;
|
||||
|
||||
let name = identifier(i)?;
|
||||
let mut end = name.end;
|
||||
|
||||
let args = if peek(open_paren).parse_next(i).is_ok() {
|
||||
ignore_whitespace(i);
|
||||
open_paren(i)?;
|
||||
ignore_whitespace(i);
|
||||
let args: Vec<_> = separated(0.., identifier, comma_sep).parse_next(i)?;
|
||||
ignore_trailing_comma(i);
|
||||
ignore_whitespace(i);
|
||||
end = close_paren(i)?.end;
|
||||
Some(args)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let result = Box::new(Node {
|
||||
start,
|
||||
end,
|
||||
module_id: name.module_id,
|
||||
outer_attrs: Vec::new(),
|
||||
inner: TypeDeclaration {
|
||||
name,
|
||||
args,
|
||||
visibility,
|
||||
digest: None,
|
||||
},
|
||||
});
|
||||
|
||||
ParseContext::warn(CompilationError::err(
|
||||
result.as_source_range(),
|
||||
"Type declarations are experimental, likely to change, and may or may not do anything useful.",
|
||||
));
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
impl TryFrom<Token> for Node<Identifier> {
|
||||
type Error = CompilationError;
|
||||
|
||||
@ -2109,29 +2156,6 @@ fn nameable_identifier(i: &mut TokenSlice) -> PResult<Node<Identifier>> {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn sketch_keyword(i: &mut TokenSlice) -> PResult<Node<Identifier>> {
|
||||
any.try_map(|token: Token| {
|
||||
if token.token_type == TokenType::Type && token.value == "sketch" {
|
||||
Ok(Node::new(
|
||||
Identifier {
|
||||
name: token.value,
|
||||
digest: None,
|
||||
},
|
||||
token.start,
|
||||
token.end,
|
||||
token.module_id,
|
||||
))
|
||||
} else {
|
||||
Err(CompilationError::fatal(
|
||||
token.as_source_range(),
|
||||
format!("Expected 'sketch' keyword, but found {}", token.value.as_str()),
|
||||
))
|
||||
}
|
||||
})
|
||||
.context(expected("the 'sketch' keyword"))
|
||||
.parse_next(i)
|
||||
}
|
||||
|
||||
impl TryFrom<Token> for Node<TagDeclarator> {
|
||||
type Error = CompilationError;
|
||||
|
||||
@ -2467,11 +2491,19 @@ fn at_sign(i: &mut TokenSlice) -> PResult<Token> {
|
||||
}
|
||||
|
||||
fn fun(i: &mut TokenSlice) -> PResult<Token> {
|
||||
keyword(i, "fn")
|
||||
}
|
||||
|
||||
fn ty(i: &mut TokenSlice) -> PResult<Token> {
|
||||
keyword(i, "type")
|
||||
}
|
||||
|
||||
fn keyword(i: &mut TokenSlice, expected: &str) -> PResult<Token> {
|
||||
any.try_map(|token: Token| match token.token_type {
|
||||
TokenType::Keyword if token.value == "fn" => Ok(token),
|
||||
TokenType::Keyword if token.value == expected => Ok(token),
|
||||
_ => Err(CompilationError::fatal(
|
||||
token.as_source_range(),
|
||||
format!("expected 'fn', found {}", token.value.as_str(),),
|
||||
format!("expected '{expected}', found {}", token.value.as_str(),),
|
||||
)),
|
||||
})
|
||||
.parse_next(i)
|
||||
@ -2520,43 +2552,33 @@ fn argument_type(i: &mut TokenSlice) -> PResult<Node<Type>> {
|
||||
))
|
||||
}),
|
||||
// Array types
|
||||
(
|
||||
one_of(TokenType::Type),
|
||||
opt(delimited(open_paren, uom_for_type, close_paren)),
|
||||
open_bracket,
|
||||
close_bracket,
|
||||
)
|
||||
.map(|(token, uom, _, _)| {
|
||||
PrimitiveType::from_str(&token.value, uom)
|
||||
.map(|t| Node::new(Type::Array(t), token.start, token.end, token.module_id))
|
||||
.ok_or_else(|| {
|
||||
CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", token.value))
|
||||
})
|
||||
}),
|
||||
(primitive_type, open_bracket, close_bracket).map(|(t, _, _)| Ok(t.map(Type::Array))),
|
||||
// Primitive types
|
||||
(
|
||||
one_of(TokenType::Type),
|
||||
opt(delimited(open_paren, uom_for_type, close_paren)),
|
||||
)
|
||||
.map(|(token, suffix)| {
|
||||
if suffix.is_some() {
|
||||
ParseContext::warn(CompilationError::err(
|
||||
(&token).into(),
|
||||
"Unit of Measure types are experimental and currently do nothing.",
|
||||
));
|
||||
}
|
||||
PrimitiveType::from_str(&token.value, suffix)
|
||||
.map(|t| Node::new(Type::Primitive(t), token.start, token.end, token.module_id))
|
||||
.ok_or_else(|| {
|
||||
CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", token.value))
|
||||
})
|
||||
}),
|
||||
primitive_type.map(|t| Ok(t.map(Type::Primitive))),
|
||||
))
|
||||
.parse_next(i)?
|
||||
.map_err(|e: CompilationError| ErrMode::Backtrack(ContextError::from(e)))?;
|
||||
Ok(type_)
|
||||
}
|
||||
|
||||
fn primitive_type(i: &mut TokenSlice) -> PResult<Node<PrimitiveType>> {
|
||||
let ident = identifier(i)?;
|
||||
|
||||
let suffix = opt(delimited(open_paren, uom_for_type, close_paren)).parse_next(i)?;
|
||||
|
||||
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));
|
||||
|
||||
if suffix.is_some() {
|
||||
ParseContext::warn(CompilationError::err(
|
||||
result.as_source_range(),
|
||||
"Unit of Measure types are experimental and currently do nothing.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn uom_for_type(i: &mut TokenSlice) -> PResult<NumericSuffix> {
|
||||
any.try_map(|t: Token| t.value.parse()).parse_next(i)
|
||||
}
|
||||
@ -4197,7 +4219,7 @@ e
|
||||
/// angle = 30,
|
||||
/// length = 3 / cos(toRadians(30)),
|
||||
/// }, %)
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close(%)
|
||||
///
|
||||
/// example = extrude(5, exampleSketch)
|
||||
@ -4528,18 +4550,6 @@ let myBox = box([0,0], -3, -16, -10)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_tag_starting_with_reserved_type() {
|
||||
let some_program_string = r#"
|
||||
startSketchOn('XY')
|
||||
|> line(%, $Sketch)
|
||||
"#;
|
||||
assert_err(
|
||||
some_program_string,
|
||||
"Cannot assign a tag to a reserved keyword: Sketch",
|
||||
[41, 47],
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_tag_with_reserved_in_middle_works() {
|
||||
let some_program_string = r#"
|
||||
|
@ -51,14 +51,6 @@ lazy_static! {
|
||||
set.insert("struct", TokenType::Keyword);
|
||||
set.insert("object", TokenType::Keyword);
|
||||
|
||||
set.insert("string", TokenType::Type);
|
||||
set.insert("number", TokenType::Type);
|
||||
set.insert("bool", TokenType::Type);
|
||||
set.insert("Sketch", TokenType::Type);
|
||||
set.insert("SketchSurface", TokenType::Type);
|
||||
set.insert("Solid", TokenType::Type);
|
||||
set.insert("Plane", TokenType::Type);
|
||||
|
||||
set
|
||||
};
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use insta::rounded_redaction;
|
||||
|
||||
@ -10,6 +10,36 @@ use crate::{
|
||||
ModuleId,
|
||||
};
|
||||
|
||||
mod kcl_samples;
|
||||
|
||||
/// A simulation test.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Test {
|
||||
/// The name of the test.
|
||||
name: String,
|
||||
/// The name of the KCL file that's the entry point, e.g. "main.kcl", in the
|
||||
/// `input_dir`.
|
||||
entry_point: String,
|
||||
/// Input KCL files are in this directory.
|
||||
input_dir: PathBuf,
|
||||
/// Expected snapshot output files are in this directory.
|
||||
output_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) const RENDERED_MODEL_NAME: &str = "rendered_model.png";
|
||||
//pub(crate) const EXPORTED_STEP_NAME: &str = "exported_step.step";
|
||||
|
||||
impl Test {
|
||||
fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_owned(),
|
||||
entry_point: "input.kcl".to_owned(),
|
||||
input_dir: Path::new("tests").join(name),
|
||||
output_dir: Path::new("tests").join(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize the data from a snapshot.
|
||||
fn get<T: serde::de::DeserializeOwned>(snapshot: &str) -> T {
|
||||
let mut parts = snapshot.split("---");
|
||||
@ -21,16 +51,16 @@ fn get<T: serde::de::DeserializeOwned>(snapshot: &str) -> T {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn assert_snapshot<F, R>(test_name: &str, operation: &str, f: F)
|
||||
fn assert_snapshot<F, R>(test: &Test, operation: &str, f: F)
|
||||
where
|
||||
F: FnOnce() -> R,
|
||||
{
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
// These make the snapshots more readable and match our dir structure.
|
||||
settings.set_omit_expression(true);
|
||||
settings.set_snapshot_path(format!("../tests/{test_name}"));
|
||||
settings.set_snapshot_path(Path::new("..").join(&test.output_dir));
|
||||
settings.set_prepend_module_to_snapshot(false);
|
||||
settings.set_description(format!("{operation} {test_name}.kcl"));
|
||||
settings.set_description(format!("{operation} {}.kcl", &test.name));
|
||||
// Sorting maps makes them easier to diff.
|
||||
settings.set_sort_maps(true);
|
||||
// Replace UUIDs with the string "[uuid]", because otherwise the tests would constantly
|
||||
@ -43,23 +73,34 @@ where
|
||||
settings.bind(f);
|
||||
}
|
||||
|
||||
fn read(filename: &'static str, test_name: &str) -> String {
|
||||
std::fs::read_to_string(format!("tests/{test_name}/{filename}")).unwrap()
|
||||
fn read<P>(filename: &str, dir: P) -> String
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
std::fs::read_to_string(dir.as_ref().join(filename)).unwrap()
|
||||
}
|
||||
|
||||
fn parse(test_name: &str) {
|
||||
let input = read("input.kcl", test_name);
|
||||
parse_test(&Test::new(test_name));
|
||||
}
|
||||
|
||||
fn parse_test(test: &Test) {
|
||||
let input = read(&test.entry_point, &test.input_dir);
|
||||
let tokens = crate::parsing::token::lex(&input, ModuleId::default()).unwrap();
|
||||
|
||||
// Parse the tokens into an AST.
|
||||
let parse_res = Result::<_, KclError>::Ok(crate::parsing::parse_tokens(tokens).unwrap());
|
||||
assert_snapshot(test_name, "Result of parsing", || {
|
||||
assert_snapshot(test, "Result of parsing", || {
|
||||
insta::assert_json_snapshot!("ast", parse_res);
|
||||
});
|
||||
}
|
||||
|
||||
fn unparse(test_name: &str) {
|
||||
let input = read("ast.snap", test_name);
|
||||
unparse_test(&Test::new(test_name));
|
||||
}
|
||||
|
||||
fn unparse_test(test: &Test) {
|
||||
let input = read("ast.snap", &test.output_dir);
|
||||
let ast_res: Result<Program, KclError> = get(&input);
|
||||
let Ok(ast) = ast_res else {
|
||||
return;
|
||||
@ -67,9 +108,9 @@ fn unparse(test_name: &str) {
|
||||
// Check recasting the AST produces the original string.
|
||||
let actual = ast.recast(&Default::default(), 0);
|
||||
if matches!(std::env::var("EXPECTORATE").as_deref(), Ok("overwrite")) {
|
||||
std::fs::write(format!("tests/{test_name}/input.kcl"), &actual).unwrap();
|
||||
std::fs::write(test.input_dir.join(&test.entry_point), &actual).unwrap();
|
||||
}
|
||||
let expected = read("input.kcl", test_name);
|
||||
let expected = read(&test.entry_point, &test.input_dir);
|
||||
pretty_assertions::assert_eq!(
|
||||
actual,
|
||||
expected,
|
||||
@ -78,42 +119,55 @@ fn unparse(test_name: &str) {
|
||||
}
|
||||
|
||||
async fn execute(test_name: &str, render_to_png: bool) {
|
||||
execute_test(&Test::new(test_name), render_to_png, false).await
|
||||
}
|
||||
|
||||
async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
|
||||
// Read the AST from disk.
|
||||
let input = read("ast.snap", test_name);
|
||||
let input = read("ast.snap", &test.output_dir);
|
||||
let ast_res: Result<Node<Program>, KclError> = get(&input);
|
||||
let Ok(ast) = ast_res else {
|
||||
return;
|
||||
};
|
||||
let ast = crate::Program {
|
||||
ast,
|
||||
original_file_contents: read("input.kcl", test_name),
|
||||
original_file_contents: read(&test.entry_point, &test.input_dir),
|
||||
};
|
||||
|
||||
// Run the program.
|
||||
let exec_res = crate::test_server::execute_and_snapshot_ast(
|
||||
ast,
|
||||
crate::settings::types::UnitLength::Mm,
|
||||
Some(Path::new("tests").join(test_name).join("input.kcl").to_owned()),
|
||||
Some(test.input_dir.join(&test.entry_point)),
|
||||
export_step,
|
||||
)
|
||||
.await;
|
||||
match exec_res {
|
||||
Ok((exec_state, env_ref, png)) => {
|
||||
let fail_path_str = format!("tests/{test_name}/execution_error.snap");
|
||||
let fail_path = Path::new(&fail_path_str);
|
||||
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/{fail_path_str}")
|
||||
Ok((exec_state, 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())
|
||||
}
|
||||
if render_to_png {
|
||||
twenty_twenty::assert_image(format!("tests/{test_name}/rendered_model.png"), &png, 0.99);
|
||||
twenty_twenty::assert_image(test.output_dir.join(RENDERED_MODEL_NAME), &png, 0.99);
|
||||
}
|
||||
if export_step {
|
||||
let step = step.unwrap();
|
||||
// TODO FIXME: This is failing because the step file is not deterministic.
|
||||
// But it should be, talk to @katie
|
||||
/*assert_snapshot(test, "Step file", || {
|
||||
insta::assert_binary_snapshot!(EXPORTED_STEP_NAME, step);
|
||||
});*/
|
||||
std::fs::write(test.output_dir.join("exported_step.snap.step"), step).unwrap();
|
||||
}
|
||||
let outcome = exec_state.to_wasm_outcome(env_ref);
|
||||
assert_common_snapshots(
|
||||
test_name,
|
||||
test,
|
||||
outcome.operations,
|
||||
outcome.artifact_commands,
|
||||
outcome.artifact_graph,
|
||||
);
|
||||
assert_snapshot(test_name, "Variables in memory after executing", || {
|
||||
assert_snapshot(test, "Variables in memory after executing", || {
|
||||
insta::assert_json_snapshot!("program_memory", outcome.variables, {
|
||||
".**.value" => rounded_redaction(4),
|
||||
".**[].value" => rounded_redaction(4),
|
||||
@ -127,9 +181,8 @@ async fn execute(test_name: &str, render_to_png: bool) {
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
let ok_path_str = format!("tests/{test_name}/program_memory.snap");
|
||||
let ok_path = Path::new(&ok_path_str);
|
||||
let previously_passed = std::fs::exists(ok_path).unwrap();
|
||||
let ok_path = test.output_dir.join("program_memory.snap");
|
||||
let previously_passed = std::fs::exists(&ok_path).unwrap();
|
||||
match e.error {
|
||||
crate::errors::ExecError::Kcl(error) => {
|
||||
// Snapshot the KCL error with a fancy graphical report.
|
||||
@ -139,24 +192,19 @@ async fn execute(test_name: &str, render_to_png: bool) {
|
||||
Box::new(miette::MietteHandlerOpts::new().show_related_errors_as_nested().build())
|
||||
}))
|
||||
.unwrap();
|
||||
let report = error.clone().into_miette_report_with_outputs().unwrap();
|
||||
let report = error.clone().into_miette_report_with_outputs(&input).unwrap();
|
||||
let report = miette::Report::new(report);
|
||||
if previously_passed {
|
||||
eprintln!("This test case failed, but it previously passed. If this is intended, and the test should actually be failing now, please delete kcl/{ok_path_str} and other associated passing artifacts");
|
||||
eprintln!("This test case failed, but it previously passed. If this is intended, and the test should actually be failing now, please delete kcl-lib/{} and other associated passing artifacts", ok_path.to_string_lossy());
|
||||
panic!("{report:?}");
|
||||
}
|
||||
let report = format!("{:?}", report);
|
||||
|
||||
assert_snapshot(test_name, "Error from executing", || {
|
||||
assert_snapshot(test, "Error from executing", || {
|
||||
insta::assert_snapshot!("execution_error", report);
|
||||
});
|
||||
|
||||
assert_common_snapshots(
|
||||
test_name,
|
||||
error.operations,
|
||||
error.artifact_commands,
|
||||
error.artifact_graph,
|
||||
);
|
||||
assert_common_snapshots(test, error.operations, error.artifact_commands, error.artifact_graph);
|
||||
}
|
||||
e => {
|
||||
// These kinds of errors aren't expected to occur. We don't
|
||||
@ -172,12 +220,12 @@ async fn execute(test_name: &str, render_to_png: bool) {
|
||||
/// Assert snapshots that should happen both when KCL execution succeeds and
|
||||
/// when it results in an error.
|
||||
fn assert_common_snapshots(
|
||||
test_name: &str,
|
||||
test: &Test,
|
||||
operations: Vec<Operation>,
|
||||
artifact_commands: Vec<ArtifactCommand>,
|
||||
artifact_graph: ArtifactGraph,
|
||||
) {
|
||||
assert_snapshot(test_name, "Operations executed", || {
|
||||
assert_snapshot(test, "Operations executed", || {
|
||||
insta::assert_json_snapshot!("ops", operations, {
|
||||
"[].unlabeledArg.*.value.**[].from[]" => rounded_redaction(4),
|
||||
"[].unlabeledArg.*.value.**[].to[]" => rounded_redaction(4),
|
||||
@ -185,14 +233,14 @@ fn assert_common_snapshots(
|
||||
"[].labeledArgs.*.value.**[].to[]" => rounded_redaction(4),
|
||||
});
|
||||
});
|
||||
assert_snapshot(test_name, "Artifact commands", || {
|
||||
assert_snapshot(test, "Artifact commands", || {
|
||||
insta::assert_json_snapshot!("artifact_commands", artifact_commands, {
|
||||
"[].command.segment.*.x" => rounded_redaction(4),
|
||||
"[].command.segment.*.y" => rounded_redaction(4),
|
||||
"[].command.segment.*.z" => rounded_redaction(4),
|
||||
});
|
||||
});
|
||||
assert_snapshot(test_name, "Artifact graph flowchart", || {
|
||||
assert_snapshot(test, "Artifact graph flowchart", || {
|
||||
let flowchart = artifact_graph
|
||||
.to_mermaid_flowchart()
|
||||
.unwrap_or_else(|e| format!("Failed to convert artifact graph to flowchart: {e}"));
|
||||
|
397
rust/kcl-lib/src/simulation_tests/kcl_samples.rs
Normal file
397
rust/kcl-lib/src/simulation_tests/kcl_samples.rs
Normal file
@ -0,0 +1,397 @@
|
||||
//! Run all the KCL samples in the `kcl_samples` directory.
|
||||
//!
|
||||
//! Use the `KCL_SAMPLES_ONLY=gear` environment variable to run only a subset of
|
||||
//! the samples, in this case, all those that start with "gear".
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use fnv::FnvHashSet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use super::Test;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// The directory containing the KCL samples source.
|
||||
static ref INPUTS_DIR: PathBuf = Path::new("../../public/kcl-samples").to_path_buf();
|
||||
/// The directory containing the expected output. We keep them isolated in
|
||||
/// their own directory, separate from other simulation tests, so that we
|
||||
/// know whether we've checked them all.
|
||||
static ref OUTPUTS_DIR: PathBuf = Path::new("tests/kcl_samples").to_path_buf();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
let write_new = matches!(
|
||||
std::env::var("INSTA_UPDATE").as_deref(),
|
||||
Ok("auto" | "always" | "new" | "unseen")
|
||||
);
|
||||
let filter = filter_from_env();
|
||||
let tests = kcl_samples_inputs(filter.as_deref());
|
||||
let expected_outputs = kcl_samples_outputs(filter.as_deref());
|
||||
|
||||
assert!(!tests.is_empty(), "No KCL samples found");
|
||||
|
||||
let input_names = FnvHashSet::from_iter(tests.iter().map(|t| t.name.clone()));
|
||||
|
||||
for test in tests {
|
||||
if write_new {
|
||||
// Ensure the directory exists for new tests.
|
||||
std::fs::create_dir_all(test.output_dir.clone()).unwrap();
|
||||
}
|
||||
super::parse_test(&test);
|
||||
}
|
||||
|
||||
// Ensure that inputs aren't missing.
|
||||
let missing = expected_outputs
|
||||
.into_iter()
|
||||
.filter(|name| !input_names.contains(name))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(missing.is_empty(), "Expected input kcl-samples for the following. If these are no longer tests, delete the expected output directories for them in {}: {missing:?}", OUTPUTS_DIR.to_string_lossy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unparse() {
|
||||
// kcl-samples don't always use correct formatting. We don't ignore the
|
||||
// test because we want to allow the just command to work. It's actually
|
||||
// fine when no test runs.
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_execute() {
|
||||
let filter = filter_from_env();
|
||||
let tests = kcl_samples_inputs(filter.as_deref());
|
||||
let expected_outputs = kcl_samples_outputs(filter.as_deref());
|
||||
|
||||
assert!(!tests.is_empty(), "No KCL samples found");
|
||||
|
||||
// Note: This is unordered.
|
||||
let mut tasks = JoinSet::new();
|
||||
// Mapping from task ID to test index.
|
||||
let mut id_to_index = HashMap::new();
|
||||
// Spawn a task for each test.
|
||||
for (index, test) in tests.iter().cloned().enumerate() {
|
||||
let handle = tasks.spawn(async move {
|
||||
super::execute_test(&test, true, true).await;
|
||||
});
|
||||
id_to_index.insert(handle.id(), index);
|
||||
}
|
||||
|
||||
// Join all the tasks and collect the failures. We cannot just join_all
|
||||
// because insta's error messages don't clearly indicate which test failed.
|
||||
let mut failed = vec![None; tests.len()];
|
||||
while let Some(result) = tasks.join_next().await {
|
||||
let Err(err) = result else {
|
||||
continue;
|
||||
};
|
||||
// When there's an error, store the test name and error message.
|
||||
let index = *id_to_index.get(&err.id()).unwrap();
|
||||
failed[index] = Some(format!("{}: {err}", &tests[index].name));
|
||||
}
|
||||
let failed = failed.into_iter().flatten().collect::<Vec<_>>();
|
||||
assert!(failed.is_empty(), "Failed tests: {}", failed.join("\n"));
|
||||
|
||||
// Ensure that inputs aren't missing.
|
||||
let input_names = FnvHashSet::from_iter(tests.iter().map(|t| t.name.clone()));
|
||||
let missing = expected_outputs
|
||||
.into_iter()
|
||||
.filter(|name| !input_names.contains(name))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(missing.is_empty(), "Expected input kcl-samples for the following. If these are no longer tests, delete the expected output directories for them in {}: {missing:?}", OUTPUTS_DIR.to_string_lossy());
|
||||
|
||||
// We want to move the step and screenshot for the inputs to the public/kcl-samples
|
||||
// directory so that they can be used as inputs for the next run.
|
||||
// First ensure each directory exists.
|
||||
let public_screenshot_dir = INPUTS_DIR.join("screenshots");
|
||||
let public_step_dir = INPUTS_DIR.join("step");
|
||||
for dir in [&public_step_dir, &public_screenshot_dir] {
|
||||
if !dir.exists() {
|
||||
std::fs::create_dir_all(dir).unwrap();
|
||||
}
|
||||
}
|
||||
for tests in &tests {
|
||||
let screenshot_file = OUTPUTS_DIR.join(&tests.name).join(super::RENDERED_MODEL_NAME);
|
||||
if !screenshot_file.exists() {
|
||||
panic!("Missing screenshot for test: {}", tests.name);
|
||||
}
|
||||
std::fs::copy(
|
||||
screenshot_file,
|
||||
public_screenshot_dir.join(format!("{}.png", &tests.name)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let step_file = OUTPUTS_DIR.join(&tests.name).join("exported_step.snap.step");
|
||||
if !step_file.exists() {
|
||||
panic!("Missing step for test: {}", tests.name);
|
||||
}
|
||||
std::fs::copy(step_file, public_step_dir.join(format!("{}.step", &tests.name))).unwrap();
|
||||
}
|
||||
|
||||
// Update the README.md with the new screenshots and steps.
|
||||
let mut new_content = String::new();
|
||||
for test in tests {
|
||||
// Format:
|
||||
new_content.push_str(&format!(
|
||||
r#"#### [{}]({}/main.kcl) ([step](step/{}.step)) ([screenshot](screenshots/{}.png))
|
||||
[]({}/main.kcl)
|
||||
"#,
|
||||
test.name, test.name, test.name, test.name, test.name, test.name, test.name
|
||||
));
|
||||
}
|
||||
update_readme(&INPUTS_DIR, &new_content).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_manifest() {
|
||||
// Generate the manifest.json
|
||||
generate_kcl_manifest(&INPUTS_DIR).unwrap();
|
||||
}
|
||||
|
||||
fn test(test_name: &str, entry_point: String) -> Test {
|
||||
Test {
|
||||
name: test_name.to_owned(),
|
||||
entry_point,
|
||||
input_dir: INPUTS_DIR.join(test_name),
|
||||
output_dir: OUTPUTS_DIR.join(test_name),
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_from_env() -> Option<String> {
|
||||
std::env::var("KCL_SAMPLES_ONLY").ok().filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
fn kcl_samples_inputs(filter: Option<&str>) -> Vec<Test> {
|
||||
let mut tests = Vec::new();
|
||||
|
||||
// Collect all directory entries first and sort them by name for consistent ordering
|
||||
let mut entries: Vec<_> = INPUTS_DIR
|
||||
.read_dir()
|
||||
.unwrap()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|e| e.path().is_dir())
|
||||
.collect();
|
||||
|
||||
// Sort directories by name for consistent ordering
|
||||
entries.sort_by_key(|a| a.file_name());
|
||||
|
||||
for entry in entries {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
// We're looking for directories only.
|
||||
continue;
|
||||
}
|
||||
let Some(dir_name) = path.file_name() else {
|
||||
continue;
|
||||
};
|
||||
let dir_name_str = dir_name.to_string_lossy();
|
||||
if dir_name_str.starts_with('.') {
|
||||
// Skip hidden directories.
|
||||
continue;
|
||||
}
|
||||
if matches!(dir_name_str.as_ref(), "step" | "screenshots") {
|
||||
// Skip output directories.
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = &filter {
|
||||
if !dir_name_str.starts_with(filter) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
eprintln!("Found KCL sample: {:?}", dir_name.to_string_lossy());
|
||||
// Look for the entry point inside the directory.
|
||||
let sub_dir = INPUTS_DIR.join(dir_name);
|
||||
let entry_point = if sub_dir.join("main.kcl").exists() {
|
||||
"main.kcl".to_owned()
|
||||
} else {
|
||||
panic!("No main.kcl found in {:?}", sub_dir);
|
||||
};
|
||||
tests.push(test(&dir_name_str, entry_point));
|
||||
}
|
||||
|
||||
tests
|
||||
}
|
||||
|
||||
fn kcl_samples_outputs(filter: Option<&str>) -> Vec<String> {
|
||||
let mut outputs = Vec::new();
|
||||
|
||||
for entry in OUTPUTS_DIR.read_dir().unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
// We're looking for directories only.
|
||||
continue;
|
||||
}
|
||||
let Some(dir_name) = path.file_name() else {
|
||||
continue;
|
||||
};
|
||||
let dir_name_str = dir_name.to_string_lossy();
|
||||
if dir_name_str.starts_with('.') {
|
||||
// Skip hidden.
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = &filter {
|
||||
if !dir_name_str.starts_with(filter) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("Found expected KCL sample: {:?}", &dir_name_str);
|
||||
outputs.push(dir_name_str.into_owned());
|
||||
}
|
||||
|
||||
outputs
|
||||
}
|
||||
|
||||
const MANIFEST_FILE: &str = "manifest.json";
|
||||
const COMMENT_PREFIX: &str = "//";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct KclMetadata {
|
||||
file: String,
|
||||
path_from_project_directory_to_first_file: String,
|
||||
multiple_files: bool,
|
||||
title: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
// Function to read and parse .kcl files
|
||||
fn get_kcl_metadata(project_path: &Path, files: &[String]) -> Option<KclMetadata> {
|
||||
// Find primary kcl file (main.kcl or first sorted file)
|
||||
let primary_kcl_file = files
|
||||
.iter()
|
||||
.find(|file| file.contains("main.kcl"))
|
||||
.unwrap_or_else(|| files.iter().min().unwrap())
|
||||
.clone();
|
||||
|
||||
let full_path_to_primary_kcl = project_path.join(&primary_kcl_file);
|
||||
|
||||
// Read the file content
|
||||
let content = match fs::read_to_string(&full_path_to_primary_kcl) {
|
||||
Ok(content) => content,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
|
||||
if lines.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Extract title and description from the first two lines
|
||||
let title = lines[0].trim_start_matches(COMMENT_PREFIX).trim().to_string();
|
||||
let description = lines[1].trim_start_matches(COMMENT_PREFIX).trim().to_string();
|
||||
|
||||
// Get the path components
|
||||
let path_components: Vec<String> = full_path_to_primary_kcl
|
||||
.components()
|
||||
.map(|comp| comp.as_os_str().to_string_lossy().to_string())
|
||||
.collect();
|
||||
|
||||
// Get the last two path components
|
||||
let len = path_components.len();
|
||||
let path_from_project_dir = if len >= 2 {
|
||||
format!("{}/{}", path_components[len - 2], path_components[len - 1])
|
||||
} else {
|
||||
primary_kcl_file.clone()
|
||||
};
|
||||
|
||||
Some(KclMetadata {
|
||||
file: primary_kcl_file,
|
||||
path_from_project_directory_to_first_file: path_from_project_dir,
|
||||
multiple_files: files.len() > 1,
|
||||
title,
|
||||
description,
|
||||
})
|
||||
}
|
||||
|
||||
// Function to scan the directory and generate the manifest.json
|
||||
fn generate_kcl_manifest(dir: &Path) -> Result<()> {
|
||||
let mut manifest = Vec::new();
|
||||
|
||||
// Collect all directory entries first and sort them by name for consistent ordering
|
||||
let mut entries: Vec<_> = fs::read_dir(dir)?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|e| e.path().is_dir())
|
||||
.collect();
|
||||
|
||||
// Sort directories by name for consistent ordering
|
||||
entries.sort_by_key(|a| a.file_name());
|
||||
|
||||
for entry in entries {
|
||||
let project_path = entry.path();
|
||||
|
||||
if project_path.is_dir() {
|
||||
// Get all .kcl files in the directory
|
||||
let files: Vec<String> = fs::read_dir(&project_path)?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|e| {
|
||||
if let Some(ext) = e.path().extension() {
|
||||
ext == "kcl"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||
.collect();
|
||||
|
||||
if files.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(metadata) = get_kcl_metadata(&project_path, &files) {
|
||||
manifest.push(metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the manifest.json
|
||||
let output_path = dir.join(MANIFEST_FILE);
|
||||
let manifest_json = serde_json::to_string_pretty(&manifest)?;
|
||||
|
||||
let mut file = fs::File::create(output_path.clone())?;
|
||||
file.write_all(manifest_json.as_bytes())?;
|
||||
|
||||
println!(
|
||||
"Manifest of {} items written to {}",
|
||||
manifest.len(),
|
||||
output_path.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates README.md by finding a specific search string and replacing all content after it
|
||||
/// with the new content provided.
|
||||
fn update_readme(dir: &Path, new_content: &str) -> Result<()> {
|
||||
let search_str = "---\n";
|
||||
let readme_path = dir.join("README.md");
|
||||
|
||||
// Read the file content
|
||||
let content = fs::read_to_string(&readme_path)?;
|
||||
|
||||
// Find the line containing the search string
|
||||
let Some(index) = content.find(search_str) else {
|
||||
anyhow::bail!(
|
||||
"Search string '{}' not found in `{}`",
|
||||
search_str,
|
||||
readme_path.display()
|
||||
);
|
||||
};
|
||||
|
||||
// Get the position just after the search string
|
||||
let position = index + search_str.len();
|
||||
|
||||
// Create the updated content
|
||||
let updated_content = format!("{}{}\n", &content[..position], new_content);
|
||||
|
||||
// Write the modified content back to the file
|
||||
std::fs::write(readme_path, updated_content)?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -341,7 +341,7 @@ impl Args {
|
||||
meta: vec![meta],
|
||||
ty: NumericType::Unknown,
|
||||
};
|
||||
Ok(KclValue::Array {
|
||||
Ok(KclValue::MixedArray {
|
||||
value: vec![x, y],
|
||||
meta: vec![meta],
|
||||
})
|
||||
@ -377,7 +377,7 @@ impl Args {
|
||||
ty: ty.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(KclValue::Array {
|
||||
Ok(KclValue::MixedArray {
|
||||
value: array,
|
||||
meta: vec![Metadata {
|
||||
source_range: self.source_range,
|
||||
@ -631,7 +631,7 @@ impl<'a> FromArgs<'a> for Vec<KclValue> {
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
};
|
||||
let KclValue::Array { value: array, meta: _ } = &arg.value else {
|
||||
let KclValue::MixedArray { value: array, meta: _ } = &arg.value else {
|
||||
let message = format!("Expected an array but found {}", arg.value.human_friendly_type());
|
||||
return Err(KclError::Type(KclErrorDetails {
|
||||
source_ranges: arg.source_ranges(),
|
||||
@ -733,7 +733,7 @@ where
|
||||
|
||||
impl<'a> FromKclValue<'a> for [f64; 2] {
|
||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||
let KclValue::Array { value, meta: _ } = arg else {
|
||||
let KclValue::MixedArray { value, meta: _ } = arg else {
|
||||
return None;
|
||||
};
|
||||
if value.len() != 2 {
|
||||
@ -748,7 +748,7 @@ impl<'a> FromKclValue<'a> for [f64; 2] {
|
||||
|
||||
impl<'a> FromKclValue<'a> for [usize; 3] {
|
||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||
let KclValue::Array { value, meta: _ } = arg else {
|
||||
let KclValue::MixedArray { value, meta: _ } = arg else {
|
||||
return None;
|
||||
};
|
||||
if value.len() != 3 {
|
||||
@ -764,7 +764,7 @@ impl<'a> FromKclValue<'a> for [usize; 3] {
|
||||
|
||||
impl<'a> FromKclValue<'a> for [f64; 3] {
|
||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||
let KclValue::Array { value, meta: _ } = arg else {
|
||||
let KclValue::MixedArray { value, meta: _ } = arg else {
|
||||
return None;
|
||||
};
|
||||
if value.len() != 3 {
|
||||
@ -1249,7 +1249,7 @@ impl_from_kcl_for_vec!(Sketch);
|
||||
|
||||
impl<'a> FromKclValue<'a> for SourceRange {
|
||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||
let KclValue::Array { value, meta: _ } = arg else {
|
||||
let KclValue::MixedArray { value, meta: _ } = arg else {
|
||||
return None;
|
||||
};
|
||||
if value.len() != 3 {
|
||||
@ -1517,7 +1517,7 @@ impl<'a> FromKclValue<'a> for SketchSet {
|
||||
match arg {
|
||||
KclValue::Sketch { value: sketch } => Some(SketchSet::from(sketch.to_owned())),
|
||||
KclValue::Sketches { value } => Some(SketchSet::from(value.to_owned())),
|
||||
KclValue::Array { .. } => {
|
||||
KclValue::MixedArray { .. } => {
|
||||
let v: Option<Vec<Sketch>> = FromKclValue::from_kcl_val(arg);
|
||||
Some(SketchSet::Sketches(v?.iter().cloned().map(Box::new).collect()))
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
|
||||
let (array, f): (Vec<KclValue>, &FunctionSource) = FromArgs::from_args(&args, 0)?;
|
||||
let meta = vec![args.source_range.into()];
|
||||
let new_array = inner_map(array, f, exec_state, &args).await?;
|
||||
Ok(KclValue::Array { value: new_array, meta })
|
||||
Ok(KclValue::MixedArray { value: new_array, meta })
|
||||
}
|
||||
|
||||
/// Apply a function to every element of a list.
|
||||
@ -230,7 +230,7 @@ async fn call_reduce_closure(
|
||||
async fn inner_push(mut array: Vec<KclValue>, elem: KclValue, args: &Args) -> Result<KclValue, KclError> {
|
||||
// Unwrap the KclValues to JValues for manipulation
|
||||
array.push(elem);
|
||||
Ok(KclValue::Array {
|
||||
Ok(KclValue::MixedArray {
|
||||
value: array,
|
||||
meta: vec![args.source_range.into()],
|
||||
})
|
||||
@ -241,7 +241,7 @@ pub async fn push(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
||||
let (val, elem): (KclValue, KclValue) = FromArgs::from_args(&args, 0)?;
|
||||
|
||||
let meta = vec![args.source_range];
|
||||
let KclValue::Array { value: array, meta: _ } = val else {
|
||||
let KclValue::MixedArray { value: array, meta: _ } = val else {
|
||||
let actual_type = val.human_friendly_type();
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: meta,
|
||||
@ -281,7 +281,7 @@ async fn inner_pop(array: Vec<KclValue>, args: &Args) -> Result<KclValue, KclErr
|
||||
// Create a new array with all elements except the last one
|
||||
let new_array = array[..array.len() - 1].to_vec();
|
||||
|
||||
Ok(KclValue::Array {
|
||||
Ok(KclValue::MixedArray {
|
||||
value: new_array,
|
||||
meta: vec![args.source_range.into()],
|
||||
})
|
||||
@ -292,7 +292,7 @@ pub async fn pop(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
||||
let val = args.get_unlabeled_kw_arg("array")?;
|
||||
|
||||
let meta = vec![args.source_range];
|
||||
let KclValue::Array { value: array, meta: _ } = val else {
|
||||
let KclValue::MixedArray { value: array, meta: _ } = val else {
|
||||
let actual_type = val.human_friendly_type();
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: meta,
|
||||
|
@ -108,7 +108,7 @@ pub async fn sqrt(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
||||
/// angle = 50,
|
||||
/// length = sqrt(2500),
|
||||
/// }, %)
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 5)
|
||||
@ -173,7 +173,7 @@ pub async fn round(_exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
||||
/// |> startProfileAt([0, 0], %)
|
||||
/// |> line(endAbsolute = [12, 10])
|
||||
/// |> line(end = [round(7.02986), 0])
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// extrude001 = extrude(sketch001, length = 5)
|
||||
@ -201,7 +201,7 @@ pub async fn floor(_exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
||||
/// |> startProfileAt([0, 0], %)
|
||||
/// |> line(endAbsolute = [12, 10])
|
||||
/// |> line(end = [floor(7.02986), 0])
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// extrude001 = extrude(sketch001, length = 5)
|
||||
@ -229,7 +229,7 @@ pub async fn ceil(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
||||
/// |> startProfileAt([0, 0], %)
|
||||
/// |> line(endAbsolute = [12, 10])
|
||||
/// |> line(end = [ceil(7.02986), 0])
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// extrude001 = extrude(sketch001, length = 5)
|
||||
@ -347,7 +347,7 @@ pub async fn pow(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
||||
/// angle = 50,
|
||||
/// length = pow(5, 2),
|
||||
/// }, %)
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 5)
|
||||
@ -408,7 +408,7 @@ pub async fn asin(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
||||
/// angle = toDegrees(asin(0.5)),
|
||||
/// length = 20,
|
||||
/// }, %)
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// extrude001 = extrude(sketch001, length = 5)
|
||||
@ -438,7 +438,7 @@ pub async fn atan(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
|
||||
/// angle = toDegrees(atan(1.25)),
|
||||
/// length = 20,
|
||||
/// }, %)
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// extrude001 = extrude(sketch001, length = 5)
|
||||
@ -468,7 +468,7 @@ pub async fn atan2(_exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
||||
/// angle = toDegrees(atan2(1.25, 2)),
|
||||
/// length = 20,
|
||||
/// }, %)
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// extrude001 = extrude(sketch001, length = 5)
|
||||
@ -632,7 +632,7 @@ pub async fn e(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclE
|
||||
/// angle = 30,
|
||||
/// length = 2 * e() ^ 2,
|
||||
/// }, %)
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 10)
|
||||
@ -664,7 +664,7 @@ pub async fn tau(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
||||
/// angle = 50,
|
||||
/// length = 10 * tau(),
|
||||
/// }, %)
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 5)
|
||||
@ -695,7 +695,7 @@ pub async fn to_radians(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
||||
/// angle = 50,
|
||||
/// length = 70 * cos(toRadians(45)),
|
||||
/// }, %)
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 5)
|
||||
@ -725,7 +725,7 @@ pub async fn to_degrees(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
||||
/// angle = 50,
|
||||
/// length = 70 * cos(toDegrees(pi()/4)),
|
||||
/// }, %)
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 5)
|
||||
|
@ -73,9 +73,7 @@ lazy_static! {
|
||||
Box::new(crate::std::shapes::CircleThreePoint),
|
||||
Box::new(crate::std::shapes::Polygon),
|
||||
Box::new(crate::std::sketch::Line),
|
||||
Box::new(crate::std::sketch::XLineTo),
|
||||
Box::new(crate::std::sketch::XLine),
|
||||
Box::new(crate::std::sketch::YLineTo),
|
||||
Box::new(crate::std::sketch::YLine),
|
||||
Box::new(crate::std::sketch::AngledLineToX),
|
||||
Box::new(crate::std::sketch::AngledLineToY),
|
||||
@ -203,6 +201,24 @@ pub(crate) fn std_fn(path: &str, fn_name: &str) -> (crate::std::StdFn, StdFnProp
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn std_ty(path: &str, fn_name: &str) -> (crate::execution::PrimitiveType, StdFnProps) {
|
||||
match (path, fn_name) {
|
||||
("prelude", "Sketch") => (
|
||||
crate::execution::PrimitiveType::Sketch,
|
||||
StdFnProps::default("std::Sketch"),
|
||||
),
|
||||
("prelude", "Solid") => (
|
||||
crate::execution::PrimitiveType::Solid,
|
||||
StdFnProps::default("std::Solid"),
|
||||
),
|
||||
("prelude", "Plane") => (
|
||||
crate::execution::PrimitiveType::Plane,
|
||||
StdFnProps::default("std::Plane"),
|
||||
),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StdLib {
|
||||
pub fns: IndexMap<String, Box<dyn StdLibFn>>,
|
||||
}
|
||||
|
@ -441,7 +441,7 @@ async fn make_transform<T: GeometryTrait>(
|
||||
})?;
|
||||
let transforms = match transform_fn_return {
|
||||
KclValue::Object { value, meta: _ } => vec![value],
|
||||
KclValue::Array { value, meta: _ } => {
|
||||
KclValue::MixedArray { value, meta: _ } => {
|
||||
let transforms: Vec<_> = value
|
||||
.into_iter()
|
||||
.map(|val| {
|
||||
@ -540,7 +540,7 @@ fn transform_from_obj_fields<T: GeometryTrait>(
|
||||
}
|
||||
|
||||
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
|
||||
let KclValue::Array { value: arr, meta } = val else {
|
||||
let KclValue::MixedArray { value: arr, meta } = val else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected an array of 3 numbers (i.e. a 3D point)".to_string(),
|
||||
source_ranges,
|
||||
@ -572,7 +572,7 @@ fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<P
|
||||
}
|
||||
|
||||
fn array_to_point2d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point2d, KclError> {
|
||||
let KclValue::Array { value: arr, meta } = val else {
|
||||
let KclValue::MixedArray { value: arr, meta } = val else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected an array of 2 numbers (i.e. a 2D point)".to_string(),
|
||||
source_ranges,
|
||||
@ -662,7 +662,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_array_to_point3d() {
|
||||
let input = KclValue::Array {
|
||||
let input = KclValue::MixedArray {
|
||||
value: vec![
|
||||
KclValue::Number {
|
||||
value: 1.1,
|
||||
|
@ -657,7 +657,7 @@ pub async fn angle_to_match_length_y(exec_state: &mut ExecState, args: Args) ->
|
||||
/// angle = angleToMatchLengthY(seg01, 15, %),
|
||||
/// length = 5,
|
||||
/// }, %)
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// extrusion = extrude(sketch001, length = 5)
|
||||
|
@ -260,113 +260,14 @@ async fn straight_line(
|
||||
Ok(new_sketch)
|
||||
}
|
||||
|
||||
/// Draw a line to a point on the x-axis.
|
||||
pub async fn x_line_to(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let (to, sketch, tag): (f64, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
|
||||
|
||||
let new_sketch = inner_x_line_to(to, sketch, tag, exec_state, args).await?;
|
||||
Ok(KclValue::Sketch {
|
||||
value: Box::new(new_sketch),
|
||||
})
|
||||
}
|
||||
|
||||
/// Draw a line parallel to the X axis, that ends at the given X.
|
||||
/// E.g. if the previous line ended at (1, 1),
|
||||
/// then xLineTo(4) draws a line from (1, 1) to (4, 1)
|
||||
///
|
||||
/// ```no_run
|
||||
/// exampleSketch = startSketchOn(XZ)
|
||||
/// |> startProfileAt([0, 0], %)
|
||||
/// |> xLineTo(15, %)
|
||||
/// |> angledLine({
|
||||
/// angle = 80,
|
||||
/// length = 15,
|
||||
/// }, %)
|
||||
/// |> line(end = [8, -10])
|
||||
/// |> xLineTo(40, %)
|
||||
/// |> angledLine({
|
||||
/// angle = 135,
|
||||
/// length = 30,
|
||||
/// }, %)
|
||||
/// |> xLineTo(10, %)
|
||||
/// |> close()
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 10)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "xLineTo",
|
||||
}]
|
||||
async fn inner_x_line_to(
|
||||
to: f64,
|
||||
sketch: Sketch,
|
||||
tag: Option<TagNode>,
|
||||
exec_state: &mut ExecState,
|
||||
args: Args,
|
||||
) -> Result<Sketch, KclError> {
|
||||
let from = sketch.current_pen_position()?;
|
||||
|
||||
let new_sketch = straight_line(
|
||||
StraightLineParams::absolute([to, from.y], sketch, tag),
|
||||
exec_state,
|
||||
args,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(new_sketch)
|
||||
}
|
||||
|
||||
/// Draw a line to a point on the y-axis.
|
||||
pub async fn y_line_to(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let (to, sketch, tag): (f64, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
|
||||
|
||||
let new_sketch = inner_y_line_to(to, sketch, tag, exec_state, args).await?;
|
||||
Ok(KclValue::Sketch {
|
||||
value: Box::new(new_sketch),
|
||||
})
|
||||
}
|
||||
|
||||
/// Draw a line parallel to the Y axis, that ends at the given Y.
|
||||
/// E.g. if the previous line ended at (1, 1),
|
||||
/// then yLineTo(4) draws a line from (1, 1) to (1, 4)
|
||||
///
|
||||
/// ```no_run
|
||||
/// exampleSketch = startSketchOn(XZ)
|
||||
/// |> startProfileAt([0, 0], %)
|
||||
/// |> angledLine({
|
||||
/// angle = 50,
|
||||
/// length = 45,
|
||||
/// }, %)
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> close()
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 5)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "yLineTo",
|
||||
}]
|
||||
async fn inner_y_line_to(
|
||||
to: f64,
|
||||
sketch: Sketch,
|
||||
tag: Option<TagNode>,
|
||||
exec_state: &mut ExecState,
|
||||
args: Args,
|
||||
) -> Result<Sketch, KclError> {
|
||||
let from = sketch.current_pen_position()?;
|
||||
|
||||
let new_sketch = straight_line(
|
||||
StraightLineParams::absolute([from.x, to], sketch, tag),
|
||||
exec_state,
|
||||
args,
|
||||
)
|
||||
.await?;
|
||||
Ok(new_sketch)
|
||||
}
|
||||
|
||||
/// Draw a line on the x-axis.
|
||||
pub async fn x_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let (length, sketch, tag): (f64, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
|
||||
let sketch = args.get_unlabeled_kw_arg("sketch")?;
|
||||
let length = args.get_kw_arg_opt("length")?;
|
||||
let end_absolute = args.get_kw_arg_opt("endAbsolute")?;
|
||||
let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
|
||||
|
||||
let new_sketch = inner_x_line(length, sketch, tag, exec_state, args).await?;
|
||||
let new_sketch = inner_x_line(sketch, length, end_absolute, tag, exec_state, args).await?;
|
||||
Ok(KclValue::Sketch {
|
||||
value: Box::new(new_sketch),
|
||||
})
|
||||
@ -378,34 +279,49 @@ pub async fn x_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
||||
/// ```no_run
|
||||
/// exampleSketch = startSketchOn(XZ)
|
||||
/// |> startProfileAt([0, 0], %)
|
||||
/// |> xLine(15, %)
|
||||
/// |> xLine(length = 15)
|
||||
/// |> angledLine({
|
||||
/// angle = 80,
|
||||
/// length = 15,
|
||||
/// }, %)
|
||||
/// |> line(end = [8, -10])
|
||||
/// |> xLine(10, %)
|
||||
/// |> xLine(length = 10)
|
||||
/// |> angledLine({
|
||||
/// angle = 120,
|
||||
/// length = 30,
|
||||
/// }, %)
|
||||
/// |> xLine(-15, %)
|
||||
/// |> xLine(length = -15)
|
||||
/// |> close()
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 10)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "xLine",
|
||||
keywords = true,
|
||||
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"},
|
||||
}
|
||||
}]
|
||||
async fn inner_x_line(
|
||||
length: f64,
|
||||
sketch: Sketch,
|
||||
length: Option<f64>,
|
||||
end_absolute: Option<f64>,
|
||||
tag: Option<TagNode>,
|
||||
exec_state: &mut ExecState,
|
||||
args: Args,
|
||||
) -> Result<Sketch, KclError> {
|
||||
let from = sketch.current_pen_position()?;
|
||||
straight_line(
|
||||
StraightLineParams::relative([length, 0.0], sketch, tag),
|
||||
StraightLineParams {
|
||||
sketch,
|
||||
end_absolute: end_absolute.map(|x| [x, from.y]),
|
||||
end: length.map(|x| [x, 0.0]),
|
||||
tag,
|
||||
},
|
||||
exec_state,
|
||||
args,
|
||||
)
|
||||
@ -414,9 +330,12 @@ async fn inner_x_line(
|
||||
|
||||
/// Draw a line on the y-axis.
|
||||
pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let (length, sketch, tag): (f64, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
|
||||
let sketch = args.get_unlabeled_kw_arg("sketch")?;
|
||||
let length = args.get_kw_arg_opt("length")?;
|
||||
let end_absolute = args.get_kw_arg_opt("endAbsolute")?;
|
||||
let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
|
||||
|
||||
let new_sketch = inner_y_line(length, sketch, tag, exec_state, args).await?;
|
||||
let new_sketch = inner_y_line(sketch, length, end_absolute, tag, exec_state, args).await?;
|
||||
Ok(KclValue::Sketch {
|
||||
value: Box::new(new_sketch),
|
||||
})
|
||||
@ -428,29 +347,44 @@ pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
|
||||
/// ```no_run
|
||||
/// exampleSketch = startSketchOn(XZ)
|
||||
/// |> startProfileAt([0, 0], %)
|
||||
/// |> yLine(15, %)
|
||||
/// |> yLine(length = 15)
|
||||
/// |> angledLine({
|
||||
/// angle = 30,
|
||||
/// length = 15,
|
||||
/// }, %)
|
||||
/// |> line(end = [8, -10])
|
||||
/// |> yLine(-5, %)
|
||||
/// |> yLine(length = -5)
|
||||
/// |> close()
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 10)
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "yLine",
|
||||
keywords = true,
|
||||
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"},
|
||||
}
|
||||
}]
|
||||
async fn inner_y_line(
|
||||
length: f64,
|
||||
sketch: Sketch,
|
||||
length: Option<f64>,
|
||||
end_absolute: Option<f64>,
|
||||
tag: Option<TagNode>,
|
||||
exec_state: &mut ExecState,
|
||||
args: Args,
|
||||
) -> Result<Sketch, KclError> {
|
||||
let from = sketch.current_pen_position()?;
|
||||
straight_line(
|
||||
StraightLineParams::relative([0.0, length], sketch, tag),
|
||||
StraightLineParams {
|
||||
sketch,
|
||||
end_absolute: end_absolute.map(|y| [from.x, y]),
|
||||
end: length.map(|y| [0.0, y]),
|
||||
tag,
|
||||
},
|
||||
exec_state,
|
||||
args,
|
||||
)
|
||||
@ -489,13 +423,13 @@ pub async fn angled_line(exec_state: &mut ExecState, args: Args) -> Result<KclVa
|
||||
/// ```no_run
|
||||
/// exampleSketch = startSketchOn(XZ)
|
||||
/// |> startProfileAt([0, 0], %)
|
||||
/// |> yLineTo(15, %)
|
||||
/// |> yLine(endAbsolute = 15)
|
||||
/// |> angledLine({
|
||||
/// angle = 30,
|
||||
/// length = 15,
|
||||
/// }, %)
|
||||
/// |> line(end = [8, -10])
|
||||
/// |> yLineTo(0, %)
|
||||
/// |> yLine(endAbsolute = 0)
|
||||
/// |> close()
|
||||
///
|
||||
/// example = extrude(exampleSketch, length = 10)
|
||||
@ -1069,9 +1003,9 @@ pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result<K
|
||||
/// })
|
||||
/// |> startProfileAt([0, 0], %)
|
||||
/// |> line(end = [100.0, 0])
|
||||
/// |> yLine(-100.0, %)
|
||||
/// |> xLine(-100.0, %)
|
||||
/// |> yLine(100.0, %)
|
||||
/// |> yLine(length = -100.0)
|
||||
/// |> xLine(length = -100.0)
|
||||
/// |> yLine(length = 100.0)
|
||||
/// |> close()
|
||||
/// |> extrude(length = 3.14)
|
||||
/// ```
|
||||
|
@ -40,11 +40,26 @@ pub async fn execute_and_snapshot_ast(
|
||||
ast: Program,
|
||||
units: UnitLength,
|
||||
current_file: Option<PathBuf>,
|
||||
) -> Result<(ExecState, EnvironmentRef, image::DynamicImage), ExecErrorWithState> {
|
||||
with_export_step: bool,
|
||||
) -> Result<(ExecState, EnvironmentRef, image::DynamicImage, Option<Vec<u8>>), ExecErrorWithState> {
|
||||
let ctx = new_context(units, true, current_file).await?;
|
||||
let res = do_execute_and_snapshot(&ctx, ast).await;
|
||||
let (exec_state, env, img) = do_execute_and_snapshot(&ctx, ast).await?;
|
||||
let mut step = None;
|
||||
if with_export_step {
|
||||
let files = match ctx.export_step(true).await {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
return Err(ExecErrorWithState::new(
|
||||
ExecError::BadExport(format!("Export failed: {:?}", err)),
|
||||
exec_state.clone(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
step = files.into_iter().next().map(|f| f.contents);
|
||||
}
|
||||
ctx.close().await;
|
||||
res
|
||||
Ok((exec_state, env, img, step))
|
||||
}
|
||||
|
||||
pub async fn execute_and_snapshot_no_auth(
|
||||
@ -68,7 +83,7 @@ async fn do_execute_and_snapshot(
|
||||
) -> Result<(ExecState, EnvironmentRef, image::DynamicImage), ExecErrorWithState> {
|
||||
let mut exec_state = ExecState::new(&ctx.settings);
|
||||
let result = ctx
|
||||
.run_with_ui_outputs(&program, &mut exec_state)
|
||||
.run(&program, &mut exec_state)
|
||||
.await
|
||||
.map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone()))?;
|
||||
for e in exec_state.errors() {
|
||||
@ -93,8 +108,6 @@ async fn do_execute_and_snapshot(
|
||||
.and_then(|x| x.decode().map_err(|e| ExecError::BadPng(e.to_string())))
|
||||
.map_err(|err| ExecErrorWithState::new(err, exec_state.clone()))?;
|
||||
|
||||
ctx.close().await;
|
||||
|
||||
Ok((exec_state, result.0, img))
|
||||
}
|
||||
|
||||
@ -147,7 +160,7 @@ pub async fn execute_and_export_step(
|
||||
let program = Program::parse_no_errs(code)
|
||||
.map_err(|err| ExecErrorWithState::new(KclErrorWithOutputs::no_outputs(err).into(), exec_state.clone()))?;
|
||||
let result = ctx
|
||||
.run_with_ui_outputs(&program, &mut exec_state)
|
||||
.run(&program, &mut exec_state)
|
||||
.await
|
||||
.map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone()))?;
|
||||
for e in exec_state.errors() {
|
||||
@ -159,56 +172,15 @@ pub async fn execute_and_export_step(
|
||||
}
|
||||
}
|
||||
|
||||
let resp = ctx
|
||||
.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
crate::SourceRange::default(),
|
||||
&kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
|
||||
entity_ids: vec![],
|
||||
format: kittycad_modeling_cmds::format::OutputFormat3d::Step(
|
||||
kittycad_modeling_cmds::format::step::export::Options {
|
||||
coords: *kittycad_modeling_cmds::coord::KITTYCAD,
|
||||
// We want all to have the same timestamp.
|
||||
created: Some(
|
||||
chrono::DateTime::parse_from_rfc3339("2021-01-01T00:00:00Z")
|
||||
.unwrap()
|
||||
.into(),
|
||||
),
|
||||
},
|
||||
),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ExecErrorWithState::new(KclErrorWithOutputs::no_outputs(err).into(), exec_state.clone()))?;
|
||||
|
||||
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { mut files } = resp else {
|
||||
return Err(ExecErrorWithState::new(
|
||||
ExecError::BadExport(format!("Expected export response, got: {:?}", resp)),
|
||||
exec_state.clone(),
|
||||
));
|
||||
};
|
||||
|
||||
for kittycad_modeling_cmds::websocket::RawFile { contents, .. } in &mut files {
|
||||
use std::fmt::Write;
|
||||
let utf8 = std::str::from_utf8(contents).unwrap();
|
||||
let mut postprocessed = String::new();
|
||||
for line in utf8.lines() {
|
||||
if line.starts_with("FILE_NAME") {
|
||||
let name = "test.step";
|
||||
let time = "2021-01-01T00:00:00Z";
|
||||
let author = "Test";
|
||||
let org = "Zoo";
|
||||
let version = "zoo.dev beta";
|
||||
let system = "zoo.dev";
|
||||
let authorization = "Test";
|
||||
writeln!(&mut postprocessed, "FILE_NAME('{name}', '{time}', ('{author}'), ('{org}'), '{version}', '{system}', '{authorization}');").unwrap();
|
||||
} else {
|
||||
writeln!(&mut postprocessed, "{line}").unwrap();
|
||||
}
|
||||
let files = match ctx.export_step(true).await {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
return Err(ExecErrorWithState::new(
|
||||
ExecError::BadExport(format!("Export failed: {:?}", err)),
|
||||
exec_state.clone(),
|
||||
));
|
||||
}
|
||||
*contents = postprocessed.into_bytes();
|
||||
}
|
||||
};
|
||||
|
||||
ctx.close().await;
|
||||
|
||||
|
@ -6,7 +6,8 @@ use crate::parsing::{
|
||||
CallExpression, CallExpressionKw, CommentStyle, DefaultParamVal, Expr, FormatOptions, FunctionExpression,
|
||||
IfExpression, ImportSelector, ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier,
|
||||
LiteralValue, MemberExpression, MemberObject, Node, NonCodeNode, NonCodeValue, ObjectExpression, Parameter,
|
||||
PipeExpression, Program, TagDeclarator, Type, UnaryExpression, VariableDeclaration, VariableKind,
|
||||
PipeExpression, Program, TagDeclarator, Type, TypeDeclaration, UnaryExpression, VariableDeclaration,
|
||||
VariableKind,
|
||||
},
|
||||
token::NumericSuffix,
|
||||
PIPE_OPERATOR,
|
||||
@ -48,6 +49,7 @@ impl Program {
|
||||
BodyItem::VariableDeclaration(variable_declaration) => {
|
||||
variable_declaration.recast(options, indentation_level)
|
||||
}
|
||||
BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.recast(),
|
||||
BodyItem::ReturnStatement(return_statement) => {
|
||||
format!(
|
||||
"{}return {}",
|
||||
@ -414,6 +416,28 @@ impl VariableDeclaration {
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeDeclaration {
|
||||
pub fn recast(&self) -> String {
|
||||
let vis = match self.visibility {
|
||||
ItemVisibility::Default => String::new(),
|
||||
ItemVisibility::Export => "export ".to_owned(),
|
||||
};
|
||||
|
||||
let mut arg_str = String::new();
|
||||
if let Some(args) = &self.args {
|
||||
arg_str.push('(');
|
||||
for a in args {
|
||||
if arg_str.len() > 1 {
|
||||
arg_str.push_str(", ");
|
||||
}
|
||||
arg_str.push_str(&a.name);
|
||||
}
|
||||
arg_str.push(')');
|
||||
}
|
||||
format!("{}type {}{}", vis, self.name.name, arg_str)
|
||||
}
|
||||
}
|
||||
|
||||
// Used by TS.
|
||||
pub fn format_number(value: f64, suffix: NumericSuffix) -> String {
|
||||
format!("{value}{suffix}")
|
||||
@ -426,7 +450,7 @@ impl Literal {
|
||||
if self.raw.contains('.') && value.fract() == 0.0 {
|
||||
format!("{value:?}{suffix}")
|
||||
} else {
|
||||
format!("{}{suffix}", self.raw)
|
||||
self.raw.clone()
|
||||
}
|
||||
}
|
||||
LiteralValue::String(ref s) => {
|
||||
@ -941,9 +965,9 @@ d = 1
|
||||
fn rect(x, y, w, h) {
|
||||
startSketchOn('XY')
|
||||
|> startProfileAt([x, y], %)
|
||||
|> xLine(w, %)
|
||||
|> yLine(h, %)
|
||||
|> xLine(-w, %)
|
||||
|> xLine(length = w)
|
||||
|> yLine(length = h)
|
||||
|> xLine(length = -w)
|
||||
|> close()
|
||||
|> extrude(d, %)
|
||||
}
|
||||
@ -961,11 +985,11 @@ fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
|
||||
fn crosshair(x, y) {
|
||||
startSketchOn('XY')
|
||||
|> startProfileAt([x, y], %)
|
||||
|> yLine(1, %)
|
||||
|> yLine(-2, %)
|
||||
|> yLine(1, %)
|
||||
|> xLine(1, %)
|
||||
|> xLine(-2, %)
|
||||
|> yLine(length = 1)
|
||||
|> yLine(length = -2)
|
||||
|> yLine(length = 1)
|
||||
|> xLine(length = 1)
|
||||
|> xLine(length = -2)
|
||||
}
|
||||
|
||||
fn z(z_x, z_y) {
|
||||
@ -1516,7 +1540,7 @@ tabs_l = startSketchOn({
|
||||
radius = hole_diam / 2
|
||||
), %)
|
||||
|> extrude(-thk, %)
|
||||
|> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
|
||||
|> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
|
||||
"#;
|
||||
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
|
||||
|
||||
@ -1633,7 +1657,7 @@ tabs_l = startSketchOn({
|
||||
radius = hole_diam / 2,
|
||||
), %)
|
||||
|> extrude(-thk, %)
|
||||
|> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
|
||||
|> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
|
||||
"#
|
||||
);
|
||||
}
|
||||
@ -2270,6 +2294,19 @@ thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
|
||||
assert_eq!(recasted.trim(), some_program_string);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recast_types() {
|
||||
let some_program_string = r#"type foo
|
||||
|
||||
// A comment
|
||||
@(impl = primitive)
|
||||
export type bar(unit, baz)
|
||||
"#;
|
||||
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
|
||||
let recasted = program.recast(&Default::default(), 0);
|
||||
assert_eq!(recasted, some_program_string);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recast_nested_fn() {
|
||||
let some_program_string = r#"fn f = () => {
|
||||
|
@ -12,6 +12,7 @@ pub enum Node<'a> {
|
||||
ImportStatement(NodeRef<'a, types::ImportStatement>),
|
||||
ExpressionStatement(NodeRef<'a, types::ExpressionStatement>),
|
||||
VariableDeclaration(NodeRef<'a, types::VariableDeclaration>),
|
||||
TypeDeclaration(NodeRef<'a, types::TypeDeclaration>),
|
||||
ReturnStatement(NodeRef<'a, types::ReturnStatement>),
|
||||
|
||||
VariableDeclarator(NodeRef<'a, types::VariableDeclarator>),
|
||||
@ -53,6 +54,7 @@ impl Node<'_> {
|
||||
Node::ImportStatement(n) => n.digest,
|
||||
Node::ExpressionStatement(n) => n.digest,
|
||||
Node::VariableDeclaration(n) => n.digest,
|
||||
Node::TypeDeclaration(n) => n.digest,
|
||||
Node::ReturnStatement(n) => n.digest,
|
||||
Node::VariableDeclarator(n) => n.digest,
|
||||
Node::Literal(n) => n.digest,
|
||||
@ -96,6 +98,7 @@ impl Node<'_> {
|
||||
Node::ImportStatement(n) => *n as *const _ as *const (),
|
||||
Node::ExpressionStatement(n) => *n as *const _ as *const (),
|
||||
Node::VariableDeclaration(n) => *n as *const _ as *const (),
|
||||
Node::TypeDeclaration(n) => *n as *const _ as *const (),
|
||||
Node::ReturnStatement(n) => *n as *const _ as *const (),
|
||||
Node::VariableDeclarator(n) => *n as *const _ as *const (),
|
||||
Node::Literal(n) => *n as *const _ as *const (),
|
||||
@ -139,6 +142,7 @@ impl TryFrom<&Node<'_>> for SourceRange {
|
||||
Node::ImportStatement(n) => SourceRange::from(*n),
|
||||
Node::ExpressionStatement(n) => SourceRange::from(*n),
|
||||
Node::VariableDeclaration(n) => SourceRange::from(*n),
|
||||
Node::TypeDeclaration(n) => SourceRange::from(*n),
|
||||
Node::ReturnStatement(n) => SourceRange::from(*n),
|
||||
Node::VariableDeclarator(n) => SourceRange::from(*n),
|
||||
Node::Literal(n) => SourceRange::from(*n),
|
||||
@ -177,6 +181,7 @@ impl<'tree> From<&'tree types::BodyItem> for Node<'tree> {
|
||||
types::BodyItem::ImportStatement(v) => v.as_ref().into(),
|
||||
types::BodyItem::ExpressionStatement(v) => v.into(),
|
||||
types::BodyItem::VariableDeclaration(v) => v.as_ref().into(),
|
||||
types::BodyItem::TypeDeclaration(v) => v.as_ref().into(),
|
||||
types::BodyItem::ReturnStatement(v) => v.into(),
|
||||
}
|
||||
}
|
||||
@ -264,6 +269,7 @@ impl_from!(Node, Program);
|
||||
impl_from!(Node, ImportStatement);
|
||||
impl_from!(Node, ExpressionStatement);
|
||||
impl_from!(Node, VariableDeclaration);
|
||||
impl_from!(Node, TypeDeclaration);
|
||||
impl_from!(Node, ReturnStatement);
|
||||
impl_from!(Node, VariableDeclarator);
|
||||
impl_from!(Node, Literal);
|
||||
|
@ -109,6 +109,7 @@ impl<'tree> Visitable<'tree> for Node<'tree> {
|
||||
children
|
||||
}
|
||||
Node::VariableDeclaration(n) => vec![(&n.declaration).into()],
|
||||
Node::TypeDeclaration(n) => vec![(&n.name).into()],
|
||||
Node::ReturnStatement(n) => {
|
||||
vec![(&n.argument).into()]
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
mod ast_node;
|
||||
mod ast_visitor;
|
||||
mod ast_walk;
|
||||
mod import_graph;
|
||||
|
||||
pub use ast_node::Node;
|
||||
pub use ast_visitor::{Visitable, Visitor};
|
||||
pub use ast_visitor::Visitable;
|
||||
pub use ast_walk::walk;
|
||||
pub use import_graph::import_graph;
|
||||
|
Reference in New Issue
Block a user