Merge remote-tracking branch 'origin/main' into paultag/import

This commit is contained in:
Paul R. Tagliamonte
2025-03-10 12:27:50 -04:00
993 changed files with 1194155 additions and 37560 deletions

View File

@ -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(),
}
}
}

View File

@ -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

View File

@ -46,12 +46,16 @@ layout: manual
### Examples
{{#each examples}}
{{#if this.content}}
```js
{{{this.content}}}
```
{{/if}}
{{#unless @root.is_utilities}}
{{#if this.image_base64}}
![Rendered example of {{@root.name}} {{@index}}](data:image/png;base64,{{{this.image_base64}}})
{{/if}}
{{/unless}}
{{/each}}

View File

@ -15,7 +15,7 @@ layout: manual
### Standard library
{{#each modules}}
* **`{{name}}`**
* **{{name}}**
{{#each functions}}
* [`{{name}}`](kcl/{{file_name}})
{{/each}}

View 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}}
![Rendered example of {{@root.name}} {{@index}}](data:image/png;base64,{{{this.image_base64}}})
{{/if}}
{{/each}}
{{/if}}

View File

@ -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();

View File

@ -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 {

View File

@ -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;

View File

@ -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"),
}
}

View File

@ -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!(),
}

View File

@ -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")]

View File

@ -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,
}
})
}
}

View File

@ -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(),

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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;

View File

@ -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 {

View File

@ -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,

View File

@ -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 {

View File

@ -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,
}
}

View File

@ -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: _ } => {}

View File

@ -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")]

View File

@ -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#"

View File

@ -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
};
}

View File

@ -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}"));

View 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))
[![{}](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(())
}

View File

@ -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()))
}

View File

@ -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,

View File

@ -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)

View File

@ -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>>,
}

View File

@ -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,

View File

@ -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)

View File

@ -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)
/// ```

View File

@ -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;

View File

@ -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 = () => {

View File

@ -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);

View File

@ -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()]
}

View File

@ -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;