use std::collections::HashMap; use async_recursion::async_recursion; use indexmap::IndexMap; #[cfg(feature = "artifact-graph")] use crate::execution::cad_op::{Group, OpArg, OpKclValue, Operation}; use crate::{ errors::{KclError, KclErrorDetails}, execution::{ annotations, kcl_value::{FunctionSource, TypeDef}, memory, state::ModuleState, types::{NumericType, PrimitiveType, RuntimeType}, BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, PlaneType, TagEngineInfo, TagIdentifier, }, fmt, modules::{ModuleId, ModulePath, ModuleRepr}, parsing::ast::types::{ Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Name, Node, NodeRef, ObjectExpression, PipeExpression, Program, TagDeclarator, Type, UnaryExpression, UnaryOperator, }, source_range::SourceRange, std::{ args::{Arg, Args, KwArgs, TyF64}, FunctionKind, }, CompilationError, }; enum StatementKind<'a> { Declaration { name: &'a str }, Expression, } impl<'a> StatementKind<'a> { fn expect_name(&self) -> &'a str { match self { StatementKind::Declaration { name } => name, StatementKind::Expression => unreachable!(), } } } impl ExecutorContext { /// Returns true if importing the prelude should be skipped. async fn handle_annotations( &self, annotations: impl Iterator>, body_type: BodyType, exec_state: &mut ExecState, ) -> Result { let mut no_prelude = false; for annotation in annotations { if annotation.name() == Some(annotations::SETTINGS) { if matches!(body_type, BodyType::Root) { if exec_state.mod_local.settings.update_from_annotation(annotation)? { exec_state.mod_local.explicit_length_units = true; } } else { exec_state.err(CompilationError::err( annotation.as_source_range(), "Settings can only be modified at the top level scope of a file", )); } } else if annotation.name() == Some(annotations::NO_PRELUDE) { if matches!(body_type, BodyType::Root) { no_prelude = true; } else { exec_state.err(CompilationError::err( annotation.as_source_range(), "The standard library can only be skipped at the top level scope of a file", )); } } else { exec_state.warn(CompilationError::err( annotation.as_source_range(), "Unknown annotation", )); } } Ok(no_prelude) } pub(super) async fn exec_module_body( &self, program: &Node, exec_state: &mut ExecState, preserve_mem: bool, module_id: ModuleId, path: &ModulePath, ) -> Result<(Option, EnvironmentRef, Vec), KclError> { crate::log::log(format!("enter module {path} {}", exec_state.stack())); let mut local_state = ModuleState::new(path.std_path(), exec_state.stack().memory.clone(), Some(module_id)); if !preserve_mem { std::mem::swap(&mut exec_state.mod_local, &mut local_state); } let no_prelude = self .handle_annotations(program.inner_attrs.iter(), crate::execution::BodyType::Root, exec_state) .await?; if !preserve_mem { exec_state.mut_stack().push_new_root_env(!no_prelude); } let result = self .exec_block(program, exec_state, crate::execution::BodyType::Root) .await; let env_ref = if preserve_mem { exec_state.mut_stack().pop_and_preserve_env() } else { exec_state.mut_stack().pop_env() }; if !preserve_mem { std::mem::swap(&mut exec_state.mod_local, &mut local_state); } crate::log::log(format!("leave {path}")); result.map(|result| (result, env_ref, local_state.module_exports)) } /// Execute an AST's program. #[async_recursion] pub(super) async fn exec_block<'a>( &'a self, program: NodeRef<'a, Program>, exec_state: &mut ExecState, body_type: BodyType, ) -> Result, KclError> { let mut last_expr = None; // Iterate over the body of the program. for statement in &program.body { match statement { BodyItem::ImportStatement(import_stmt) => { if !matches!(body_type, BodyType::Root) { return Err(KclError::Semantic(KclErrorDetails { message: "Imports are only supported at the top-level of a file.".to_owned(), source_ranges: vec![import_stmt.into()], })); } let source_range = SourceRange::from(import_stmt); let attrs = &import_stmt.outer_attrs; let module_id = self .open_module(&import_stmt.path, attrs, exec_state, source_range) .await?; match &import_stmt.selector { ImportSelector::List { items } => { let (env_ref, module_exports) = self.exec_module_for_items(module_id, exec_state, source_range).await?; for import_item in items { // Extract the item from the module. let mem = &exec_state.stack().memory; let mut value = mem .get_from(&import_item.name.name, env_ref, import_item.into(), 0) .cloned(); let ty_name = format!("{}{}", memory::TYPE_PREFIX, import_item.name.name); let mut ty = mem.get_from(&ty_name, env_ref, import_item.into(), 0).cloned(); if value.is_err() && ty.is_err() { return Err(KclError::UndefinedValue(KclErrorDetails { message: format!("{} is not defined in module", import_item.name.name), source_ranges: vec![SourceRange::from(&import_item.name)], })); } // Check that the item is allowed to be imported (in at least one namespace). if value.is_ok() && !module_exports.contains(&import_item.name.name) { value = Err(KclError::Semantic(KclErrorDetails { message: format!( "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.", import_item.name.name ), source_ranges: vec![SourceRange::from(&import_item.name)], })); } if ty.is_ok() && !module_exports.contains(&ty_name) { ty = Err(KclError::Semantic(KclErrorDetails { message: String::new(), source_ranges: vec![], })); } if value.is_err() && ty.is_err() { return value.map(Option::Some); } // Add the item to the current module. if let Ok(value) = value { exec_state.mut_stack().add( import_item.identifier().to_owned(), value, SourceRange::from(&import_item.name), )?; if let ItemVisibility::Export = import_stmt.visibility { exec_state .mod_local .module_exports .push(import_item.identifier().to_owned()); } } if let Ok(ty) = ty { let ty_name = format!("{}{}", memory::TYPE_PREFIX, import_item.identifier()); // Add the item to the current module. exec_state.mut_stack().add( ty_name.clone(), ty, SourceRange::from(&import_item.name), )?; if let ItemVisibility::Export = import_stmt.visibility { exec_state.mod_local.module_exports.push(ty_name); } } } } ImportSelector::Glob(_) => { let (env_ref, module_exports) = self.exec_module_for_items(module_id, exec_state, source_range).await?; for name in module_exports.iter() { let item = exec_state .stack() .memory .get_from(name, env_ref, source_range, 0) .map_err(|_err| { KclError::Internal(KclErrorDetails { message: format!("{} is not defined in module (but was exported?)", name), source_ranges: vec![source_range], }) })? .clone(); exec_state.mut_stack().add(name.to_owned(), item, source_range)?; if let ItemVisibility::Export = import_stmt.visibility { exec_state.mod_local.module_exports.push(name.clone()); } } } ImportSelector::None { .. } => { let name = import_stmt.module_name().unwrap(); let item = KclValue::Module { value: module_id, meta: vec![source_range.into()], }; exec_state.mut_stack().add(name, item, source_range)?; } } last_expr = None; } BodyItem::ExpressionStatement(expression_statement) => { let metadata = Metadata::from(expression_statement); last_expr = Some( self.execute_expr( &expression_statement.expression, exec_state, &metadata, &[], StatementKind::Expression, ) .await?, ); } BodyItem::VariableDeclaration(variable_declaration) => { let var_name = variable_declaration.declaration.id.name.to_string(); let source_range = SourceRange::from(&variable_declaration.declaration.init); let metadata = Metadata { source_range }; let annotations = &variable_declaration.outer_attrs; let value = self .execute_expr( &variable_declaration.declaration.init, exec_state, &metadata, annotations, StatementKind::Declaration { name: &var_name }, ) .await?; exec_state .mut_stack() .add(var_name.clone(), value.clone(), source_range)?; // Track exports. if let ItemVisibility::Export = variable_declaration.visibility { exec_state.mod_local.module_exports.push(var_name); } // Variable declaration can be the return value of a module. last_expr = matches!(body_type, BodyType::Root).then_some(value); } 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.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 (t, props) = crate::std::std_ty(std_path, &ty.name.name); let value = KclValue::Type { value: TypeDef::RustRepr(t, props), meta: vec![metadata], }; let name_in_mem = format!("{}{}", memory::TYPE_PREFIX, ty.name.name); exec_state .mut_stack() .add(name_in_mem.clone(), value, metadata.source_range) .map_err(|_| { KclError::Semantic(KclErrorDetails { message: format!("Redefinition of type {}.", ty.name.name), source_ranges: vec![metadata.source_range], }) })?; if let ItemVisibility::Export = ty.visibility { exec_state.mod_local.module_exports.push(name_in_mem); } } // Do nothing for primitive types, they get special treatment and their declarations are just for documentation. annotations::Impl::Primitive => {} annotations::Impl::Kcl => match &ty.alias { Some(alias) => { let value = KclValue::Type { value: TypeDef::Alias( RuntimeType::from_parsed( alias.inner.clone(), exec_state, metadata.source_range, ) .map_err(|e| KclError::Semantic(e.into()))?, ), meta: vec![metadata], }; let name_in_mem = format!("{}{}", memory::TYPE_PREFIX, ty.name.name); exec_state .mut_stack() .add(name_in_mem.clone(), value, metadata.source_range) .map_err(|_| { KclError::Semantic(KclErrorDetails { message: format!("Redefinition of type {}.", ty.name.name), source_ranges: vec![metadata.source_range], }) })?; if let ItemVisibility::Export = ty.visibility { exec_state.mod_local.module_exports.push(name_in_mem); } } None => { 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); if matches!(body_type, BodyType::Root) { return Err(KclError::Semantic(KclErrorDetails { message: "Cannot return from outside a function.".to_owned(), source_ranges: vec![metadata.source_range], })); } let value = self .execute_expr( &return_statement.argument, exec_state, &metadata, &[], StatementKind::Expression, ) .await?; exec_state .mut_stack() .add(memory::RETURN_NAME.to_owned(), value, metadata.source_range) .map_err(|_| { KclError::Semantic(KclErrorDetails { message: "Multiple returns from a single function.".to_owned(), source_ranges: vec![metadata.source_range], }) })?; last_expr = None; } } } if matches!(body_type, BodyType::Root) { // Flush the batch queue. self.engine .flush_batch( // True here tells the engine to flush all the end commands as well like fillets // and chamfers where the engine would otherwise eat the ID of the segments. true, SourceRange::new(program.end, program.end, program.module_id), ) .await?; } Ok(last_expr) } pub async fn open_module( &self, path: &ImportPath, attrs: &[Node], exec_state: &mut ExecState, source_range: SourceRange, ) -> Result { let resolved_path = ModulePath::from_import_path(path, &self.settings.project_directory); match path { ImportPath::Kcl { .. } => { exec_state.global.mod_loader.cycle_check(&resolved_path, source_range)?; if let Some(id) = exec_state.id_for_module(&resolved_path) { return Ok(id); } let id = exec_state.next_module_id(); // Add file path string to global state even if it fails to import exec_state.add_path_to_source_id(resolved_path.clone(), id); let source = resolved_path.source(&self.fs, source_range).await?; exec_state.add_id_to_source(id, source.clone()); // TODO handle parsing errors properly let parsed = crate::parsing::parse_str(&source.source, id).parse_errs_as_err()?; exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed, None)); Ok(id) } ImportPath::Foreign { .. } => { if let Some(id) = exec_state.id_for_module(&resolved_path) { return Ok(id); } let id = exec_state.next_module_id(); let path = resolved_path.expect_path(); // Add file path string to global state even if it fails to import exec_state.add_path_to_source_id(resolved_path.clone(), id); let format = super::import::format_from_annotations(attrs, path, source_range)?; let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?; exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom, None)); Ok(id) } ImportPath::Std { .. } => { if let Some(id) = exec_state.id_for_module(&resolved_path) { return Ok(id); } let id = exec_state.next_module_id(); // Add file path string to global state even if it fails to import exec_state.add_path_to_source_id(resolved_path.clone(), id); let source = resolved_path.source(&self.fs, source_range).await?; exec_state.add_id_to_source(id, source.clone()); let parsed = crate::parsing::parse_str(&source.source, id) .parse_errs_as_err() .unwrap(); exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed, None)); Ok(id) } } } pub(super) async fn exec_module_for_items( &self, module_id: ModuleId, exec_state: &mut ExecState, source_range: SourceRange, ) -> Result<(EnvironmentRef, Vec), KclError> { let path = exec_state.global.module_infos[&module_id].path.clone(); let mut repr = exec_state.global.module_infos[&module_id].take_repr(); // DON'T EARLY RETURN! We need to restore the module repr let result = match &mut repr { ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)), ModuleRepr::Kcl(_, Some((_, env_ref, items))) => Ok((*env_ref, items.clone())), ModuleRepr::Kcl(program, cache) => self .exec_module_from_ast(program, module_id, &path, exec_state, source_range, false) .await .map(|(val, er, items)| { *cache = Some((val, er, items.clone())); (er, items) }), ModuleRepr::Foreign(geom, _) => Err(KclError::Semantic(KclErrorDetails { message: "Cannot import items from foreign modules".to_owned(), source_ranges: vec![geom.source_range], })), ModuleRepr::Dummy => unreachable!("Looking up {}, but it is still being interpreted", path), }; exec_state.global.module_infos[&module_id].restore_repr(repr); result } async fn exec_module_for_result( &self, module_id: ModuleId, exec_state: &mut ExecState, source_range: SourceRange, ) -> Result, KclError> { let path = exec_state.global.module_infos[&module_id].path.clone(); let mut repr = exec_state.global.module_infos[&module_id].take_repr(); // DON'T EARLY RETURN! We need to restore the module repr let result = match &mut repr { ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)), ModuleRepr::Kcl(_, Some((val, _, _))) => Ok(val.clone()), ModuleRepr::Kcl(program, cached_items) => { let result = self .exec_module_from_ast(program, module_id, &path, exec_state, source_range, false) .await; match result { Ok((val, env, items)) => { *cached_items = Some((val.clone(), env, items)); Ok(val) } Err(e) => Err(e), } } ModuleRepr::Foreign(_, Some(imported)) => Ok(Some(imported.clone())), ModuleRepr::Foreign(geom, cached) => { let result = super::import::send_to_engine(geom.clone(), self) .await .map(|geom| Some(KclValue::ImportedGeometry(geom))); match result { Ok(val) => { *cached = val.clone(); Ok(val) } Err(e) => Err(e), } } ModuleRepr::Dummy => unreachable!(), }; exec_state.global.module_infos[&module_id].restore_repr(repr); result } pub async fn exec_module_from_ast( &self, program: &Node, module_id: ModuleId, path: &ModulePath, exec_state: &mut ExecState, source_range: SourceRange, preserve_mem: bool, ) -> Result<(Option, EnvironmentRef, Vec), KclError> { exec_state.global.mod_loader.enter_module(path); let result = self .exec_module_body(program, exec_state, preserve_mem, module_id, path) .await; exec_state.global.mod_loader.leave_module(path); result.map_err(|err| { if let KclError::ImportCycle(_) = err { // It was an import cycle. Keep the original message. err.override_source_ranges(vec![source_range]) } else { // TODO would be great to have line/column for the underlying error here KclError::Semantic(KclErrorDetails { message: format!( "Error loading imported file ({path}). Open it to view more details.\n {}", err.message() ), source_ranges: vec![source_range], }) } }) } #[async_recursion] async fn execute_expr<'a: 'async_recursion>( &self, init: &Expr, exec_state: &mut ExecState, metadata: &Metadata, annotations: &[Node], statement_kind: StatementKind<'a>, ) -> Result { let item = match init { Expr::None(none) => KclValue::from(none), Expr::Literal(literal) => KclValue::from_literal((**literal).clone(), exec_state), Expr::TagDeclarator(tag) => tag.execute(exec_state).await?, Expr::Name(name) => { let value = name.get_result(exec_state, self).await?.clone(); if let KclValue::Module { value: module_id, meta } = value { self.exec_module_for_result( module_id, exec_state, metadata.source_range ).await? .unwrap_or_else(|| { exec_state.warn(CompilationError::err( metadata.source_range, "Imported module has no return value. The last statement of the module must be an expression, usually the Solid.", )); let mut new_meta = vec![metadata.to_owned()]; new_meta.extend(meta); KclValue::KclNone { value: Default::default(), meta: new_meta, } }) } else { value } } Expr::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, self).await?, Expr::FunctionExpression(function_expression) => { 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.std_path { let (func, props) = crate::std::std_fn(std_path, statement_kind.expect_name()); KclValue::Function { value: FunctionSource::Std { func, props, ast: function_expression.clone(), }, meta: vec![metadata.to_owned()], } } else { return Err(KclError::Semantic(KclErrorDetails { message: "Rust implementation of functions is restricted to the standard library" .to_owned(), source_ranges: vec![metadata.source_range], })); } } else { // Snapshotting memory here is crucial for semantics so that we close // over variables. Variables defined lexically later shouldn't // be available to the function body. 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()], } } } Expr::CallExpressionKw(call_expression) => call_expression.execute(exec_state, self).await?, Expr::PipeExpression(pipe_expression) => pipe_expression.get_result(exec_state, self).await?, Expr::PipeSubstitution(pipe_substitution) => match statement_kind { StatementKind::Declaration { name } => { let message = format!( "you cannot declare variable {name} as %, because % can only be used in function calls" ); return Err(KclError::Semantic(KclErrorDetails { message, source_ranges: vec![pipe_substitution.into()], })); } StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() { Some(x) => x, None => { return Err(KclError::Semantic(KclErrorDetails { message: "cannot use % outside a pipe expression".to_owned(), source_ranges: vec![pipe_substitution.into()], })); } }, }, Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?, Expr::ArrayRangeExpression(range_expression) => range_expression.execute(exec_state, self).await?, Expr::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?, Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?, Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?, Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?, Expr::LabelledExpression(expr) => { let result = self .execute_expr(&expr.expr, exec_state, metadata, &[], statement_kind) .await?; exec_state .mut_stack() .add(expr.label.name.clone(), result.clone(), init.into())?; // TODO this lets us use the label as a variable name, but not as a tag in most cases result } Expr::AscribedExpression(expr) => expr.get_result(exec_state, self).await?, }; Ok(item) } } impl Node { #[async_recursion] pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result { let metadata = Metadata { source_range: SourceRange::from(self), }; let result = ctx .execute_expr(&self.expr, exec_state, &metadata, &[], StatementKind::Expression) .await?; apply_ascription(&result, &self.ty, exec_state, self.into()) } } fn apply_ascription( value: &KclValue, ty: &Node, exec_state: &mut ExecState, source_range: SourceRange, ) -> Result { let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into()) .map_err(|e| KclError::Semantic(e.into()))?; if let KclValue::Number { value, meta, .. } = value { // If the number has unknown units but the user is explicitly specifying them, treat the value as having had it's units erased, // rather than forcing the user to explicitly erase them. KclValue::Number { ty: NumericType::Any, value: *value, meta: meta.clone(), } .coerce(&ty, exec_state) } else { value.coerce(&ty, exec_state) } .map_err(|_| { KclError::Semantic(KclErrorDetails { message: format!("could not coerce {} value to type {}", value.human_friendly_type(), ty), source_ranges: vec![source_range], }) }) } impl BinaryPart { #[async_recursion] pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result { match self { BinaryPart::Literal(literal) => Ok(KclValue::from_literal((**literal).clone(), exec_state)), BinaryPart::Name(name) => name.get_result(exec_state, ctx).await.cloned(), BinaryPart::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, ctx).await, BinaryPart::CallExpressionKw(call_expression) => call_expression.execute(exec_state, ctx).await, BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, ctx).await, BinaryPart::MemberExpression(member_expression) => member_expression.get_result(exec_state), BinaryPart::IfExpression(e) => e.get_result(exec_state, ctx).await, BinaryPart::AscribedExpression(e) => e.get_result(exec_state, ctx).await, } } } impl Node { async fn get_result<'a>( &self, exec_state: &'a mut ExecState, ctx: &ExecutorContext, ) -> Result<&'a KclValue, KclError> { if self.abs_path { return Err(KclError::Semantic(KclErrorDetails { message: "Absolute paths (names beginning with `::` are not yet supported)".to_owned(), source_ranges: self.as_source_ranges(), })); } if self.path.is_empty() { return exec_state.stack().get(&self.name.name, self.into()); } let mut mem_spec: Option<(EnvironmentRef, Vec)> = None; for p in &self.path { let value = match mem_spec { Some((env, exports)) => { if !exports.contains(&p.name) { return Err(KclError::Semantic(KclErrorDetails { message: format!("Item {} not found in module's exported items", p.name), source_ranges: p.as_source_ranges(), })); } exec_state .stack() .memory .get_from(&p.name, env, p.as_source_range(), 0)? } None => exec_state.stack().get(&p.name, self.into())?, }; let KclValue::Module { value: module_id, .. } = value else { return Err(KclError::Semantic(KclErrorDetails { message: format!( "Identifier in path must refer to a module, found {}", value.human_friendly_type() ), source_ranges: p.as_source_ranges(), })); }; mem_spec = Some( ctx.exec_module_for_items(*module_id, exec_state, p.as_source_range()) .await?, ); } let (env, exports) = mem_spec.unwrap(); if !exports.contains(&self.name.name) { return Err(KclError::Semantic(KclErrorDetails { message: format!("Item {} not found in module's exported items", self.name.name), source_ranges: self.name.as_source_ranges(), })); } exec_state .stack() .memory .get_from(&self.name.name, env, self.name.as_source_range(), 0) } } impl Node { fn get_result(&self, exec_state: &mut ExecState) -> Result { let property = Property::try_from(self.computed, self.property.clone(), exec_state, self.into())?; let object = match &self.object { // TODO: Don't use recursion here, use a loop. MemberObject::MemberExpression(member_expr) => member_expr.get_result(exec_state)?, MemberObject::Identifier(identifier) => { let value = exec_state.stack().get(&identifier.name, identifier.into())?; value.clone() } }; // Check the property and object match -- e.g. ints for arrays, strs for objects. match (object, property, self.computed) { (KclValue::Object { value: map, meta: _ }, Property::String(property), false) => { if let Some(value) = map.get(&property) { Ok(value.to_owned()) } else { Err(KclError::UndefinedValue(KclErrorDetails { message: format!("Property '{property}' not found in object"), source_ranges: vec![self.clone().into()], })) } } (KclValue::Object { .. }, Property::String(property), true) => Err(KclError::Semantic(KclErrorDetails { message: format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"), source_ranges: vec![self.clone().into()], })), (KclValue::Object { .. }, p, _) => { let t = p.type_name(); let article = article_for(t); Err(KclError::Semantic(KclErrorDetails { message: format!( "Only strings can be used as the property of an object, but you're using {article} {t}", ), source_ranges: vec![self.clone().into()], })) } (KclValue::HomArray { value: arr, .. }, Property::UInt(index), _) => { let value_of_arr = arr.get(index); if let Some(value) = value_of_arr { Ok(value.to_owned()) } else { Err(KclError::UndefinedValue(KclErrorDetails { message: format!("The array doesn't have any item at index {index}"), source_ranges: vec![self.clone().into()], })) } } (KclValue::HomArray { .. }, p, _) => { let t = p.type_name(); let article = article_for(t); Err(KclError::Semantic(KclErrorDetails { message: format!( "Only integers >= 0 can be used as the index of an array, but you're using {article} {t}", ), source_ranges: vec![self.clone().into()], })) } (KclValue::Solid { value }, Property::String(prop), false) if prop == "sketch" => Ok(KclValue::Sketch { value: Box::new(value.sketch), }), (KclValue::Sketch { value: sk }, Property::String(prop), false) if prop == "tags" => Ok(KclValue::Object { meta: vec![Metadata { source_range: SourceRange::from(self.clone()), }], value: sk .tags .iter() .map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned())))) .collect(), }), (being_indexed, _, _) => { let t = being_indexed.human_friendly_type(); let article = article_for(&t); Err(KclError::Semantic(KclErrorDetails { message: format!("Only arrays can be indexed, but you're trying to index {article} {t}"), source_ranges: vec![self.clone().into()], })) } } } } impl Node { #[async_recursion] pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result { let left_value = self.left.get_result(exec_state, ctx).await?; let right_value = self.right.get_result(exec_state, ctx).await?; let mut meta = left_value.metadata(); meta.extend(right_value.metadata()); // First check if we are doing string concatenation. if self.operator == BinaryOperator::Add { if let (KclValue::String { value: left, meta: _ }, KclValue::String { value: right, meta: _ }) = (&left_value, &right_value) { return Ok(KclValue::String { value: format!("{}{}", left, right), meta, }); } } // Then check if we have solids. if self.operator == BinaryOperator::Add || self.operator == BinaryOperator::Or { if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) { let args = Args::new(Default::default(), self.into(), ctx.clone(), None); let result = crate::std::csg::inner_union( vec![*left.clone(), *right.clone()], Default::default(), exec_state, args, ) .await?; return Ok(result.into()); } } else if self.operator == BinaryOperator::Sub { // Check if we have solids. if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) { let args = Args::new(Default::default(), self.into(), ctx.clone(), None); let result = crate::std::csg::inner_subtract( vec![*left.clone()], vec![*right.clone()], Default::default(), exec_state, args, ) .await?; return Ok(result.into()); } } else if self.operator == BinaryOperator::And { // Check if we have solids. if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) { let args = Args::new(Default::default(), self.into(), ctx.clone(), None); let result = crate::std::csg::inner_intersect( vec![*left.clone(), *right.clone()], Default::default(), exec_state, args, ) .await?; return Ok(result.into()); } } // Check if we are doing logical operations on booleans. if self.operator == BinaryOperator::Or || self.operator == BinaryOperator::And { let KclValue::Bool { value: left_value, meta: _, } = left_value else { return Err(KclError::Semantic(KclErrorDetails { message: format!( "Cannot apply logical operator to non-boolean value: {}", left_value.human_friendly_type() ), source_ranges: vec![self.left.clone().into()], })); }; let KclValue::Bool { value: right_value, meta: _, } = right_value else { return Err(KclError::Semantic(KclErrorDetails { message: format!( "Cannot apply logical operator to non-boolean value: {}", right_value.human_friendly_type() ), source_ranges: vec![self.right.clone().into()], })); }; let raw_value = match self.operator { BinaryOperator::Or => left_value || right_value, BinaryOperator::And => left_value && right_value, _ => unreachable!(), }; return Ok(KclValue::Bool { value: raw_value, meta }); } let left = number_as_f64(&left_value, self.left.clone().into())?; let right = number_as_f64(&right_value, self.right.clone().into())?; let value = match self.operator { BinaryOperator::Add => { let (l, r, ty) = NumericType::combine_eq_coerce(left, right); self.warn_on_unknown(&ty, "Adding", exec_state); KclValue::Number { value: l + r, meta, ty } } BinaryOperator::Sub => { let (l, r, ty) = NumericType::combine_eq_coerce(left, right); self.warn_on_unknown(&ty, "Subtracting", exec_state); KclValue::Number { value: l - r, meta, ty } } BinaryOperator::Mul => { let (l, r, ty) = NumericType::combine_mul(left, right); self.warn_on_unknown(&ty, "Multiplying", exec_state); KclValue::Number { value: l * r, meta, ty } } BinaryOperator::Div => { let (l, r, ty) = NumericType::combine_div(left, right); self.warn_on_unknown(&ty, "Dividing", exec_state); KclValue::Number { value: l / r, meta, ty } } BinaryOperator::Mod => { let (l, r, ty) = NumericType::combine_div(left, right); self.warn_on_unknown(&ty, "Modulo of", exec_state); KclValue::Number { value: l % r, meta, ty } } BinaryOperator::Pow => KclValue::Number { value: left.n.powf(right.n), meta, ty: exec_state.current_default_units(), }, BinaryOperator::Neq => { let (l, r, ty) = NumericType::combine_eq(left, right); self.warn_on_unknown(&ty, "Comparing", exec_state); KclValue::Bool { value: l != r, meta } } BinaryOperator::Gt => { let (l, r, ty) = NumericType::combine_eq(left, right); self.warn_on_unknown(&ty, "Comparing", exec_state); KclValue::Bool { value: l > r, meta } } BinaryOperator::Gte => { let (l, r, ty) = NumericType::combine_eq(left, right); self.warn_on_unknown(&ty, "Comparing", exec_state); KclValue::Bool { value: l >= r, meta } } BinaryOperator::Lt => { let (l, r, ty) = NumericType::combine_eq(left, right); self.warn_on_unknown(&ty, "Comparing", exec_state); KclValue::Bool { value: l < r, meta } } BinaryOperator::Lte => { let (l, r, ty) = NumericType::combine_eq(left, right); self.warn_on_unknown(&ty, "Comparing", exec_state); KclValue::Bool { value: l <= r, meta } } BinaryOperator::Eq => { let (l, r, ty) = NumericType::combine_eq(left, right); self.warn_on_unknown(&ty, "Comparing", exec_state); KclValue::Bool { value: l == r, meta } } BinaryOperator::And | BinaryOperator::Or => unreachable!(), }; Ok(value) } fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) { if ty == &NumericType::Unknown { // TODO suggest how to fix this exec_state.warn(CompilationError::err( self.as_source_range(), format!("{} numbers which have unknown or incompatible units.", verb), )); } } } impl Node { pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result { if self.operator == UnaryOperator::Not { let value = self.argument.get_result(exec_state, ctx).await?; let KclValue::Bool { value: bool_value, meta: _, } = value else { return Err(KclError::Semantic(KclErrorDetails { message: format!( "Cannot apply unary operator ! to non-boolean value: {}", value.human_friendly_type() ), source_ranges: vec![self.into()], })); }; let meta = vec![Metadata { source_range: self.into(), }]; let negated = KclValue::Bool { value: !bool_value, meta, }; return Ok(negated); } let value = &self.argument.get_result(exec_state, ctx).await?; let err = || { KclError::Semantic(KclErrorDetails { message: format!( "You can only negate numbers, planes, or lines, but this is a {}", value.human_friendly_type() ), source_ranges: vec![self.into()], }) }; match value { KclValue::Number { value, ty, .. } => { let meta = vec![Metadata { source_range: self.into(), }]; Ok(KclValue::Number { value: -value, meta, ty: ty.clone(), }) } KclValue::Plane { value } => { let mut plane = value.clone(); if plane.info.x_axis.x != 0.0 { plane.info.x_axis.x *= -1.0; } if plane.info.x_axis.y != 0.0 { plane.info.x_axis.y *= -1.0; } if plane.info.x_axis.z != 0.0 { plane.info.x_axis.z *= -1.0; } plane.value = PlaneType::Uninit; plane.id = exec_state.next_uuid(); Ok(KclValue::Plane { value: plane }) } KclValue::Object { value: values, meta } => { // Special-case for negating line-like objects. let Some(direction) = values.get("direction") else { return Err(err()); }; let direction = match direction { KclValue::Tuple { value: values, meta } => { let values = values .iter() .map(|v| match v { KclValue::Number { value, ty, meta } => Ok(KclValue::Number { value: *value * -1.0, ty: ty.clone(), meta: meta.clone(), }), _ => Err(err()), }) .collect::, _>>()?; KclValue::Tuple { value: values, meta: meta.clone(), } } KclValue::HomArray { value: values, ty: ty @ RuntimeType::Primitive(PrimitiveType::Number(_)), } => { let values = values .iter() .map(|v| match v { KclValue::Number { value, ty, meta } => Ok(KclValue::Number { value: *value * -1.0, ty: ty.clone(), meta: meta.clone(), }), _ => Err(err()), }) .collect::, _>>()?; KclValue::HomArray { value: values, ty: ty.clone(), } } _ => return Err(err()), }; let mut value = values.clone(); value.insert("direction".to_owned(), direction); Ok(KclValue::Object { value, meta: meta.clone(), }) } _ => Err(err()), } } } pub(crate) async fn execute_pipe_body( exec_state: &mut ExecState, body: &[Expr], source_range: SourceRange, ctx: &ExecutorContext, ) -> Result { let Some((first, body)) = body.split_first() else { return Err(KclError::Semantic(KclErrorDetails { message: "Pipe expressions cannot be empty".to_owned(), source_ranges: vec![source_range], })); }; // Evaluate the first element in the pipeline. // They use the pipe_value from some AST node above this, so that if pipe expression is nested in a larger pipe expression, // they use the % from the parent. After all, this pipe expression hasn't been executed yet, so it doesn't have any % value // of its own. let meta = Metadata { source_range: SourceRange::from(first), }; let output = ctx .execute_expr(first, exec_state, &meta, &[], StatementKind::Expression) .await?; // Now that we've evaluated the first child expression in the pipeline, following child expressions // should use the previous child expression for %. // This means there's no more need for the previous pipe_value from the parent AST node above this one. let previous_pipe_value = std::mem::replace(&mut exec_state.mod_local.pipe_value, Some(output)); // Evaluate remaining elements. let result = inner_execute_pipe_body(exec_state, body, ctx).await; // Restore the previous pipe value. exec_state.mod_local.pipe_value = previous_pipe_value; result } /// Execute the tail of a pipe expression. exec_state.pipe_value must be set by /// the caller. #[async_recursion] async fn inner_execute_pipe_body( exec_state: &mut ExecState, body: &[Expr], ctx: &ExecutorContext, ) -> Result { for expression in body { if let Expr::TagDeclarator(_) = expression { return Err(KclError::Semantic(KclErrorDetails { message: format!("This cannot be in a PipeExpression: {:?}", expression), source_ranges: vec![expression.into()], })); } let metadata = Metadata { source_range: SourceRange::from(expression), }; let output = ctx .execute_expr(expression, exec_state, &metadata, &[], StatementKind::Expression) .await?; exec_state.mod_local.pipe_value = Some(output); } // Safe to unwrap here, because pipe_value always has something pushed in when the `match first` executes. let final_output = exec_state.mod_local.pipe_value.take().unwrap(); Ok(final_output) } impl Node { #[async_recursion] pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result { let fn_name = &self.callee; let callsite: SourceRange = self.into(); // Build a hashmap from argument labels to the final evaluated values. let mut fn_args = IndexMap::with_capacity(self.arguments.len()); let mut errors = Vec::new(); for arg_expr in &self.arguments { let source_range = SourceRange::from(arg_expr.arg.clone()); let metadata = Metadata { source_range }; let value = ctx .execute_expr(&arg_expr.arg, exec_state, &metadata, &[], StatementKind::Expression) .await?; let arg = Arg::new(value, source_range); match &arg_expr.label { Some(l) => { fn_args.insert(l.name.clone(), arg); } None => { if let Some(id) = arg_expr.arg.ident_name() { fn_args.insert(id.to_owned(), arg); } else { errors.push(arg); } } } } // Evaluate the unlabeled first param, if any exists. let unlabeled = if let Some(ref arg_expr) = self.unlabeled { let source_range = SourceRange::from(arg_expr.clone()); let metadata = Metadata { source_range }; let value = ctx .execute_expr(arg_expr, exec_state, &metadata, &[], StatementKind::Expression) .await?; let label = arg_expr.ident_name().map(str::to_owned); Some((label, Arg::new(value, source_range))) } else { None }; let mut args = Args::new_kw( KwArgs { unlabeled, labeled: fn_args, errors, }, self.into(), ctx.clone(), exec_state.pipe_value().map(|v| Arg::new(v.clone(), callsite)), ); match ctx.stdlib.get_either(fn_name) { FunctionKind::Core(func) => { if func.deprecated() { exec_state.warn(CompilationError::err( self.callee.as_source_range(), format!("`{fn_name}` is deprecated, see the docs for a recommended replacement"), )); } let formals = func.args(false); // If it's possible the input arg was meant to be labelled and we probably don't want to use // it as the input arg, then treat it as labelled. if let Some((Some(label), _)) = &args.kw_args.unlabeled { if (formals.iter().all(|a| a.label_required) || exec_state.pipe_value().is_some()) && formals.iter().any(|a| &a.name == label && a.label_required) && !args.kw_args.labeled.contains_key(label) { let (label, arg) = args.kw_args.unlabeled.take().unwrap(); args.kw_args.labeled.insert(label.unwrap(), arg); } } #[cfg(feature = "artifact-graph")] let op = if func.feature_tree_operation() { let op_labeled_args = args .kw_args .labeled .iter() .map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range))) .collect(); Some(Operation::StdLibCall { std_lib_fn: (&func).into(), unlabeled_arg: args .unlabeled_kw_arg_unconverted() .map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)), labeled_args: op_labeled_args, source_range: callsite, is_error: false, }) } else { None }; for (label, arg) in &args.kw_args.labeled { match formals.iter().find(|p| &p.name == label) { Some(p) => { if !p.label_required { exec_state.err(CompilationError::err( arg.source_range, format!( "The function `{fn_name}` expects an unlabeled first parameter (`{label}`), but it is labelled in the call" ), )); } } None => { exec_state.err(CompilationError::err( arg.source_range, format!("`{label}` is not an argument of `{fn_name}`"), )); } } } // Attempt to call the function. let mut return_value = { // Don't early-return in this block. exec_state.mut_stack().push_new_env_for_rust_call(); let result = func.std_lib_fn()(exec_state, args).await; exec_state.mut_stack().pop_env(); #[cfg(feature = "artifact-graph")] if let Some(mut op) = op { op.set_std_lib_call_is_error(result.is_err()); // Track call operation. We do this after the call // since things like patternTransform may call user code // before running, and we will likely want to use the // return value. The call takes ownership of the args, // so we need to build the op before the call. exec_state.global.operations.push(op); } result }?; update_memory_for_tags_of_geometry(&mut return_value, exec_state)?; Ok(return_value) } FunctionKind::UserDefined => { // Clone the function so that we can use a mutable reference to // exec_state. let func = fn_name.get_result(exec_state, ctx).await?.clone(); let Some(fn_src) = func.as_fn() else { return Err(KclError::Semantic(KclErrorDetails { message: "cannot call this because it isn't a function".to_string(), source_ranges: vec![callsite], })); }; let return_value = fn_src .call_kw(Some(fn_name.to_string()), exec_state, ctx, args, callsite) .await .map_err(|e| { // Add the call expression to the source ranges. // TODO currently ignored by the frontend e.add_source_ranges(vec![callsite]) })?; let result = return_value.ok_or_else(move || { let mut source_ranges: Vec = vec![callsite]; // We want to send the source range of the original function. if let KclValue::Function { meta, .. } = func { source_ranges = meta.iter().map(|m| m.source_range).collect(); }; KclError::UndefinedValue(KclErrorDetails { message: format!("Result of user-defined function {} is undefined", fn_name), source_ranges, }) })?; Ok(result) } } } } fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut ExecState) -> Result<(), KclError> { // If the return result is a sketch or solid, we want to update the // memory for the tags of the group. // TODO: This could probably be done in a better way, but as of now this was my only idea // and it works. match result { KclValue::Sketch { value } => { for (name, tag) in value.tags.iter() { if exec_state.stack().cur_frame_contains(name) { exec_state.mut_stack().update(name, |v, _| { v.as_mut_tag().unwrap().merge_info(tag); }); } else { exec_state .mut_stack() .add( name.to_owned(), KclValue::TagIdentifier(Box::new(tag.clone())), SourceRange::default(), ) .unwrap(); } } } KclValue::Solid { ref mut value } => { for v in &value.value { if let Some(tag) = v.get_tag() { // Get the past tag and update it. let tag_id = if let Some(t) = value.sketch.tags.get(&tag.name) { let mut t = t.clone(); let Some(info) = t.get_cur_info() else { return Err(KclError::Internal(KclErrorDetails { message: format!("Tag {} does not have path info", tag.name), source_ranges: vec![tag.into()], })); }; let mut info = info.clone(); info.surface = Some(v.clone()); info.sketch = value.id; t.info.push((exec_state.stack().current_epoch(), info)); t } else { // It's probably a fillet or a chamfer. // Initialize it. TagIdentifier { value: tag.name.clone(), info: vec![( exec_state.stack().current_epoch(), TagEngineInfo { id: v.get_id(), surface: Some(v.clone()), path: None, sketch: value.id, }, )], meta: vec![Metadata { source_range: tag.clone().into(), }], } }; // update the sketch tags. value.sketch.merge_tags(Some(&tag_id).into_iter()); if exec_state.stack().cur_frame_contains(&tag.name) { exec_state.mut_stack().update(&tag.name, |v, _| { v.as_mut_tag().unwrap().merge_info(&tag_id); }); } else { exec_state .mut_stack() .add( tag.name.clone(), KclValue::TagIdentifier(Box::new(tag_id)), SourceRange::default(), ) .unwrap(); } } } // Find the stale sketch in memory and update it. if !value.sketch.tags.is_empty() { let sketches_to_update: Vec<_> = exec_state .stack() .find_keys_in_current_env(|v| match v { KclValue::Sketch { value: sk } => sk.original_id == value.sketch.original_id, _ => false, }) .cloned() .collect(); for k in sketches_to_update { exec_state.mut_stack().update(&k, |v, _| { let sketch = v.as_mut_sketch().unwrap(); sketch.merge_tags(value.sketch.tags.values()); }); } } } KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => { for v in value { update_memory_for_tags_of_geometry(v, exec_state)?; } } _ => {} } Ok(()) } impl Node { pub async fn execute(&self, exec_state: &mut ExecState) -> Result { let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier { value: self.name.clone(), info: Vec::new(), meta: vec![Metadata { source_range: self.into(), }], })); exec_state .mut_stack() .add(self.name.clone(), memory_item.clone(), self.into())?; Ok(self.into()) } } impl Node { #[async_recursion] pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result { let mut results = Vec::with_capacity(self.elements.len()); for element in &self.elements { let metadata = Metadata::from(element); // TODO: Carry statement kind here so that we know if we're // inside a variable declaration. let value = ctx .execute_expr(element, exec_state, &metadata, &[], StatementKind::Expression) .await?; results.push(value); } Ok(KclValue::HomArray { value: results, ty: RuntimeType::Primitive(PrimitiveType::Any), }) } } impl Node { #[async_recursion] pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result { let metadata = Metadata::from(&self.start_element); let start_val = ctx .execute_expr( &self.start_element, exec_state, &metadata, &[], StatementKind::Expression, ) .await?; let (start, start_ty) = start_val.as_int_with_ty().ok_or(KclError::Semantic(KclErrorDetails { source_ranges: vec![self.into()], message: format!("Expected int but found {}", start_val.human_friendly_type()), }))?; let metadata = Metadata::from(&self.end_element); let end_val = ctx .execute_expr(&self.end_element, exec_state, &metadata, &[], StatementKind::Expression) .await?; let (end, end_ty) = end_val.as_int_with_ty().ok_or(KclError::Semantic(KclErrorDetails { source_ranges: vec![self.into()], message: format!("Expected int but found {}", end_val.human_friendly_type()), }))?; if start_ty != end_ty { let start = start_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: start_ty }); let start = fmt::human_display_number(start.n, start.ty); let end = end_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: end_ty }); let end = fmt::human_display_number(end.n, end.ty); return Err(KclError::Semantic(KclErrorDetails { source_ranges: vec![self.into()], message: format!("Range start and end must be of the same type, but found {start} and {end}"), })); } if end < start { return Err(KclError::Semantic(KclErrorDetails { source_ranges: vec![self.into()], message: format!("Range start is greater than range end: {start} .. {end}"), })); } let range: Vec<_> = if self.end_inclusive { (start..=end).collect() } else { (start..end).collect() }; let meta = vec![Metadata { source_range: self.into(), }]; Ok(KclValue::HomArray { value: range .into_iter() .map(|num| KclValue::Number { value: num as f64, ty: start_ty.clone(), meta: meta.clone(), }) .collect(), ty: RuntimeType::Primitive(PrimitiveType::Number(start_ty)), }) } } impl Node { #[async_recursion] pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result { let mut object = HashMap::with_capacity(self.properties.len()); for property in &self.properties { let metadata = Metadata::from(&property.value); let result = ctx .execute_expr(&property.value, exec_state, &metadata, &[], StatementKind::Expression) .await?; object.insert(property.key.name.clone(), result); } Ok(KclValue::Object { value: object, meta: vec![Metadata { source_range: self.into(), }], }) } } fn article_for>(s: S) -> &'static str { // '[' is included since it's an array. if s.as_ref().starts_with(['a', 'e', 'i', 'o', 'u', '[']) { "an" } else { "a" } } fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result { v.as_ty_f64().ok_or_else(|| { let actual_type = v.human_friendly_type(); KclError::Semantic(KclErrorDetails { source_ranges: vec![source_range], message: format!("Expected a number, but found {actual_type}",), }) }) } impl Node { #[async_recursion] pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result { // Check the `if` branch. let cond = ctx .execute_expr( &self.cond, exec_state, &Metadata::from(self), &[], StatementKind::Expression, ) .await? .get_bool()?; if cond { let block_result = ctx.exec_block(&self.then_val, exec_state, BodyType::Block).await?; // Block must end in an expression, so this has to be Some. // Enforced by the parser. // See https://github.com/KittyCAD/modeling-app/issues/4015 return Ok(block_result.unwrap()); } // Check any `else if` branches. for else_if in &self.else_ifs { let cond = ctx .execute_expr( &else_if.cond, exec_state, &Metadata::from(self), &[], StatementKind::Expression, ) .await? .get_bool()?; if cond { let block_result = ctx.exec_block(&else_if.then_val, exec_state, BodyType::Block).await?; // Block must end in an expression, so this has to be Some. // Enforced by the parser. // See https://github.com/KittyCAD/modeling-app/issues/4015 return Ok(block_result.unwrap()); } } // Run the final `else` branch. ctx.exec_block(&self.final_else, exec_state, BodyType::Block) .await .map(|expr| expr.unwrap()) } } #[derive(Debug)] enum Property { UInt(usize), String(String), } impl Property { fn try_from( computed: bool, value: LiteralIdentifier, exec_state: &ExecState, sr: SourceRange, ) -> Result { let property_sr = vec![sr]; let property_src: SourceRange = value.clone().into(); match value { LiteralIdentifier::Identifier(identifier) => { let name = &identifier.name; if !computed { // This is dot syntax. Treat the property as a literal. Ok(Property::String(name.to_string())) } else { // This is bracket syntax. Actually evaluate memory to // compute the property. let prop = exec_state.stack().get(name, property_src)?; jvalue_to_prop(prop, property_sr, name) } } LiteralIdentifier::Literal(literal) => { let value = literal.value.clone(); match value { LiteralValue::Number { value, .. } => { if let Some(x) = crate::try_f64_to_usize(value) { Ok(Property::UInt(x)) } else { Err(KclError::Semantic(KclErrorDetails { source_ranges: property_sr, message: format!("{value} is not a valid index, indices must be whole numbers >= 0"), })) } } _ => Err(KclError::Semantic(KclErrorDetails { source_ranges: vec![sr], message: "Only numbers (>= 0) can be indexes".to_owned(), })), } } } } } fn jvalue_to_prop(value: &KclValue, property_sr: Vec, name: &str) -> Result { let make_err = |message: String| { Err::(KclError::Semantic(KclErrorDetails { source_ranges: property_sr, message, })) }; match value { KclValue::Number{value: num, .. } => { let num = *num; if num < 0.0 { return make_err(format!("'{num}' is negative, so you can't index an array with it")) } let nearest_int = crate::try_f64_to_usize(num); if let Some(nearest_int) = nearest_int { Ok(Property::UInt(nearest_int)) } else { make_err(format!("'{num}' is not an integer, so you can't index an array with it")) } } KclValue::String{value: x, meta:_} => Ok(Property::String(x.to_owned())), _ => { make_err(format!("{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array")) } } } impl Property { fn type_name(&self) -> &'static str { match self { Property::UInt(_) => "number", Property::String(_) => "string", } } } impl Node { #[async_recursion] pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result { execute_pipe_body(exec_state, &self.body, self.into(), ctx).await } } fn type_check_params_kw( fn_name: Option<&str>, function_expression: NodeRef<'_, FunctionExpression>, args: &mut KwArgs, exec_state: &mut ExecState, ) -> Result<(), KclError> { // If it's possible the input arg was meant to be labelled and we probably don't want to use // it as the input arg, then treat it as labelled. if let Some((Some(label), _)) = &args.unlabeled { if (function_expression.params.iter().all(|p| p.labeled) || exec_state.pipe_value().is_some()) && function_expression .params .iter() .any(|p| &p.identifier.name == label && p.labeled) && !args.labeled.contains_key(label) { let (label, arg) = args.unlabeled.take().unwrap(); args.labeled.insert(label.unwrap(), arg); } } for (label, arg) in &mut args.labeled { match function_expression.params.iter().find(|p| &p.identifier.name == label) { Some(p) => { if !p.labeled { exec_state.err(CompilationError::err( arg.source_range, format!( "{} expects an unlabeled first parameter (`{label}`), but it is labelled in the call", fn_name .map(|n| format!("The function `{}`", n)) .unwrap_or_else(|| "This function".to_owned()), ), )); } if let Some(ty) = &p.type_ { arg.value = arg .value .coerce( &RuntimeType::from_parsed(ty.inner.clone(), exec_state, arg.source_range).map_err(|e| KclError::Semantic(e.into()))?, exec_state, ) .map_err(|e| { let mut message = format!( "{label} requires a value with type `{}`, but found {}", ty.inner, arg.value.human_friendly_type(), ); if let Some(ty) = e.explicit_coercion { // TODO if we have access to the AST for the argument we could choose which example to suggest. message = format!("{message}\n\nYou may need to add information about the type of the argument, for example:\n using a numeric suffix: `42{ty}`\n or using type ascription: `foo(): number({ty})`"); } KclError::Semantic(KclErrorDetails { message, source_ranges: vec![arg.source_range], }) })?; } } None => { exec_state.err(CompilationError::err( arg.source_range, format!( "`{label}` is not an argument of {}", fn_name .map(|n| format!("`{}`", n)) .unwrap_or_else(|| "this function".to_owned()), ), )); } } } if !args.errors.is_empty() { let actuals = args.labeled.keys(); let formals: Vec<_> = function_expression .params .iter() .filter_map(|p| { if !p.labeled { return None; } let name = &p.identifier.name; if actuals.clone().any(|a| a == name) { return None; } Some(format!("`{name}`")) }) .collect(); let suggestion = if formals.is_empty() { String::new() } else { format!("; suggested labels: {}", formals.join(", ")) }; let mut errors = args.errors.iter().map(|e| { CompilationError::err( e.source_range, format!("This argument needs a label, but it doesn't have one{suggestion}"), ) }); let first = errors.next().unwrap(); errors.for_each(|e| exec_state.err(e)); return Err(KclError::Semantic(first.into())); } if let Some(arg) = &mut args.unlabeled { if let Some(p) = function_expression.params.iter().find(|p| !p.labeled) { if let Some(ty) = &p.type_ { arg.1.value = arg .1 .value .coerce( &RuntimeType::from_parsed(ty.inner.clone(), exec_state, arg.1.source_range) .map_err(|e| KclError::Semantic(e.into()))?, exec_state, ) .map_err(|_| { KclError::Semantic(KclErrorDetails { message: format!( "The input argument of {} requires a value with type `{}`, but found {}", fn_name .map(|n| format!("`{}`", n)) .unwrap_or_else(|| "this function".to_owned()), ty.inner, arg.1.value.human_friendly_type() ), source_ranges: vec![arg.1.source_range], }) })?; } } } Ok(()) } fn assign_args_to_params_kw( fn_name: Option<&str>, function_expression: NodeRef<'_, FunctionExpression>, mut args: Args, exec_state: &mut ExecState, ) -> Result<(), KclError> { type_check_params_kw(fn_name, function_expression, &mut args.kw_args, exec_state)?; // Add the arguments to the memory. A new call frame should have already // been created. let source_ranges = vec![function_expression.into()]; for param in function_expression.params.iter() { if param.labeled { let arg = args.kw_args.labeled.get(¶m.identifier.name); let arg_val = match arg { Some(arg) => arg.value.clone(), None => match param.default_value { Some(ref default_val) => KclValue::from_default_param(default_val.clone(), exec_state), None => { return Err(KclError::Semantic(KclErrorDetails { source_ranges, message: format!( "This function requires a parameter {}, but you haven't passed it one.", param.identifier.name ), })); } }, }; exec_state .mut_stack() .add(param.identifier.name.clone(), arg_val, (¶m.identifier).into())?; } else { let unlabelled = args.unlabeled_kw_arg_unconverted(); let Some(unlabeled) = unlabelled else { let param_name = ¶m.identifier.name; return Err(if args.kw_args.labeled.contains_key(param_name) { KclError::Semantic(KclErrorDetails { source_ranges, message: format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"), }) } else { KclError::Semantic(KclErrorDetails { source_ranges, message: "This function expects an unlabeled first parameter, but you haven't passed it one." .to_owned(), }) }); }; exec_state.mut_stack().add( param.identifier.name.clone(), unlabeled.value.clone(), (¶m.identifier).into(), )?; } } Ok(()) } fn coerce_result_type( result: Result, KclError>, function_expression: NodeRef<'_, FunctionExpression>, exec_state: &mut ExecState, ) -> Result, KclError> { if let Ok(Some(val)) = result { if let Some(ret_ty) = &function_expression.return_type { let ty = RuntimeType::from_parsed(ret_ty.inner.clone(), exec_state, ret_ty.as_source_range()) .map_err(|e| KclError::Semantic(e.into()))?; let val = val.coerce(&ty, exec_state).map_err(|_| { KclError::Semantic(KclErrorDetails { message: format!( "This function requires its result to be of type `{}`, but found {}", ty.human_friendly_type(), val.human_friendly_type(), ), source_ranges: ret_ty.as_source_ranges(), }) })?; Ok(Some(val)) } else { Ok(Some(val)) } } else { result } } async fn call_user_defined_function_kw( fn_name: Option<&str>, args: Args, memory: EnvironmentRef, function_expression: NodeRef<'_, FunctionExpression>, exec_state: &mut ExecState, ctx: &ExecutorContext, ) -> Result, KclError> { // Create a new environment to execute the function body in so that local // variables shadow variables in the parent scope. The new environment's // parent should be the environment of the closure. exec_state.mut_stack().push_new_env_for_call(memory); if let Err(e) = assign_args_to_params_kw(fn_name, function_expression, args, exec_state) { exec_state.mut_stack().pop_env(); return Err(e); } // Execute the function body using the memory we just created. let result = ctx .exec_block(&function_expression.body, exec_state, BodyType::Block) .await; let mut result = result.map(|_| { exec_state .stack() .get(memory::RETURN_NAME, function_expression.as_source_range()) .ok() .cloned() }); result = coerce_result_type(result, function_expression, exec_state); // Restore the previous memory. exec_state.mut_stack().pop_env(); result } impl FunctionSource { pub async fn call_kw( &self, fn_name: Option, exec_state: &mut ExecState, ctx: &ExecutorContext, mut args: Args, callsite: SourceRange, ) -> Result, KclError> { match self { FunctionSource::Std { func, ast, props } => { if props.deprecated { exec_state.warn(CompilationError::err( callsite, format!( "`{}` is deprecated, see the docs for a recommended replacement", props.name ), )); } type_check_params_kw(Some(&props.name), ast, &mut args.kw_args, exec_state)?; if let Some(arg) = &mut args.kw_args.unlabeled { if let Some(p) = ast.params.iter().find(|p| !p.labeled) { if let Some(ty) = &p.type_ { arg.1.value = arg .1 .value .coerce( &RuntimeType::from_parsed(ty.inner.clone(), exec_state, arg.1.source_range) .map_err(|e| KclError::Semantic(e.into()))?, exec_state, ) .map_err(|_| { KclError::Semantic(KclErrorDetails { message: format!( "The input argument of {} requires a value with type `{}`, but found {}", props.name, ty.inner, arg.1.value.human_friendly_type(), ), source_ranges: vec![callsite], }) })?; } } } #[cfg(feature = "artifact-graph")] let op = if props.include_in_feature_tree { let op_labeled_args = args .kw_args .labeled .iter() .map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range))) .collect(); Some(Operation::KclStdLibCall { name: fn_name.unwrap_or_default(), unlabeled_arg: args .unlabeled_kw_arg_unconverted() .map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)), labeled_args: op_labeled_args, source_range: callsite, is_error: false, }) } else { None }; // Attempt to call the function. exec_state.mut_stack().push_new_env_for_rust_call(); let mut result = { // Don't early-return in this block. let result = func(exec_state, args).await; exec_state.mut_stack().pop_env(); #[cfg(feature = "artifact-graph")] if let Some(mut op) = op { op.set_std_lib_call_is_error(result.is_err()); // Track call operation. We do this after the call // since things like patternTransform may call user code // before running, and we will likely want to use the // return value. The call takes ownership of the args, // so we need to build the op before the call. exec_state.global.operations.push(op); } result }?; update_memory_for_tags_of_geometry(&mut result, exec_state)?; Ok(Some(result)) } FunctionSource::User { ast, memory, .. } => { // Track call operation. #[cfg(feature = "artifact-graph")] { let op_labeled_args = args .kw_args .labeled .iter() .map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range))) .collect(); exec_state.global.operations.push(Operation::GroupBegin { group: Group::FunctionCall { name: fn_name.clone(), function_source_range: ast.as_source_range(), unlabeled_arg: args .kw_args .unlabeled .as_ref() .map(|arg| OpArg::new(OpKclValue::from(&arg.1.value), arg.1.source_range)), labeled_args: op_labeled_args, }, source_range: callsite, }); } let result = call_user_defined_function_kw(fn_name.as_deref(), args, *memory, ast, exec_state, ctx).await; // Track return operation. #[cfg(feature = "artifact-graph")] exec_state.global.operations.push(Operation::GroupEnd); result } FunctionSource::None => unreachable!(), } } } #[cfg(test)] mod test { use std::sync::Arc; use tokio::io::AsyncWriteExt; use super::*; use crate::{ exec::UnitType, execution::{memory::Stack, parse_execute, ContextType}, parsing::ast::types::{DefaultParamVal, Identifier, Parameter}, ExecutorSettings, UnitLen, }; #[tokio::test(flavor = "multi_thread")] async fn test_assign_args_to_params() { // Set up a little framework for this test. fn mem(number: usize) -> KclValue { KclValue::Number { value: number as f64, ty: NumericType::count(), meta: Default::default(), } } fn ident(s: &'static str) -> Node { Node::no_src(Identifier { name: s.to_owned(), digest: None, }) } fn opt_param(s: &'static str) -> Parameter { Parameter { identifier: ident(s), type_: None, default_value: Some(DefaultParamVal::none()), labeled: true, digest: None, } } fn req_param(s: &'static str) -> Parameter { Parameter { identifier: ident(s), type_: None, default_value: None, labeled: true, digest: None, } } fn additional_program_memory(items: &[(String, KclValue)]) -> Stack { let mut program_memory = Stack::new_for_tests(); for (name, item) in items { program_memory .add(name.clone(), item.clone(), SourceRange::default()) .unwrap(); } program_memory } // Declare the test cases. for (test_name, params, args, expected) in [ ("empty", Vec::new(), Vec::new(), Ok(additional_program_memory(&[]))), ( "all params required, and all given, should be OK", vec![req_param("x")], vec![("x", mem(1))], Ok(additional_program_memory(&[("x".to_owned(), mem(1))])), ), ( "all params required, none given, should error", vec![req_param("x")], vec![], Err(KclError::Semantic(KclErrorDetails { source_ranges: vec![SourceRange::default()], message: "This function requires a parameter x, but you haven't passed it one.".to_owned(), })), ), ( "all params optional, none given, should be OK", vec![opt_param("x")], vec![], Ok(additional_program_memory(&[("x".to_owned(), KclValue::none())])), ), ( "mixed params, too few given", vec![req_param("x"), opt_param("y")], vec![], Err(KclError::Semantic(KclErrorDetails { source_ranges: vec![SourceRange::default()], message: "This function requires a parameter x, but you haven't passed it one.".to_owned(), })), ), ( "mixed params, minimum given, should be OK", vec![req_param("x"), opt_param("y")], vec![("x", mem(1))], Ok(additional_program_memory(&[ ("x".to_owned(), mem(1)), ("y".to_owned(), KclValue::none()), ])), ), ( "mixed params, maximum given, should be OK", vec![req_param("x"), opt_param("y")], vec![("x", mem(1)), ("y", mem(2))], Ok(additional_program_memory(&[ ("x".to_owned(), mem(1)), ("y".to_owned(), mem(2)), ])), ), ] { // Run each test. let func_expr = &Node::no_src(FunctionExpression { params, body: Program::empty(), return_type: None, digest: None, }); let labeled = args .iter() .map(|(name, value)| { let arg = Arg::new(value.clone(), SourceRange::default()); ((*name).to_owned(), arg) }) .collect::>(); let exec_ctxt = ExecutorContext { engine: Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new().await.unwrap(), )), fs: Arc::new(crate::fs::FileManager::new()), stdlib: Arc::new(crate::std::StdLib::new()), settings: Default::default(), context_type: ContextType::Mock, }; let mut exec_state = ExecState::new(&exec_ctxt); exec_state.mod_local.stack = Stack::new_for_tests(); let args = Args::new_kw( KwArgs { unlabeled: None, labeled, errors: Vec::new(), }, SourceRange::default(), exec_ctxt, None, ); let actual = assign_args_to_params_kw(None, func_expr, args, &mut exec_state).map(|_| exec_state.mod_local.stack); assert_eq!( actual, expected, "failed test '{test_name}':\ngot {actual:?}\nbut expected\n{expected:?}" ); } } #[tokio::test(flavor = "multi_thread")] async fn ascription() { let program = r#" a = 42: number b = a: number p = { origin = { x = 0, y = 0, z = 0 }, xAxis = { x = 1, y = 0, z = 0 }, yAxis = { x = 0, y = 1, z = 0 }, zAxis = { x = 0, y = 0, z = 1 } }: Plane arr1 = [42]: [number(cm)] "#; let result = parse_execute(program).await.unwrap(); let mem = result.exec_state.stack(); assert!(matches!( mem.memory .get_from("p", result.mem_env, SourceRange::default(), 0) .unwrap(), KclValue::Plane { .. } )); let arr1 = mem .memory .get_from("arr1", result.mem_env, SourceRange::default(), 0) .unwrap(); if let KclValue::HomArray { value, ty } = arr1 { assert_eq!(value.len(), 1, "Expected Vec with specific length: found {:?}", value); assert_eq!(*ty, RuntimeType::known_length(UnitLen::Cm)); // Compare, ignoring meta. if let KclValue::Number { value, ty, .. } = &value[0] { // Converted from mm to cm. assert_eq!(*value, 4.2); assert_eq!(*ty, NumericType::Known(UnitType::Length(UnitLen::Cm))); } else { panic!("Expected a number; found {:?}", value[0]); } } else { panic!("Expected HomArray; found {arr1:?}"); } let program = r#" a = 42: string "#; let result = parse_execute(program).await; let err = result.unwrap_err(); assert!( err.to_string() .contains("could not coerce number(default units) value to type string"), "Expected error but found {err:?}" ); let program = r#" a = 42: Plane "#; let result = parse_execute(program).await; let err = result.unwrap_err(); assert!( err.to_string() .contains("could not coerce number(default units) value to type Plane"), "Expected error but found {err:?}" ); let program = r#" arr = [0]: [string] "#; let result = parse_execute(program).await; let err = result.unwrap_err(); assert!( err.to_string() .contains("could not coerce [any; 1] value to type [string]"), "Expected error but found {err:?}" ); let program = r#" mixedArr = [0, "a"]: [number(mm)] "#; let result = parse_execute(program).await; let err = result.unwrap_err(); assert!( err.to_string() .contains("could not coerce [any; 2] value to type [number(mm)]"), "Expected error but found {err:?}" ); } #[tokio::test(flavor = "multi_thread")] async fn neg_plane() { let program = r#" p = { origin = { x = 0, y = 0, z = 0 }, xAxis = { x = 1, y = 0, z = 0 }, yAxis = { x = 0, y = 1, z = 0 }, }: Plane p2 = -p "#; let result = parse_execute(program).await.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.info.x_axis.x, -1.0); assert_eq!(value.info.x_axis.y, 0.0); assert_eq!(value.info.x_axis.z, 0.0); } _ => unreachable!(), } } #[tokio::test(flavor = "multi_thread")] async fn multiple_returns() { let program = r#"fn foo() { return 0 return 42 } a = foo() "#; let result = parse_execute(program).await; assert!(result.unwrap_err().to_string().contains("return")); } #[tokio::test(flavor = "multi_thread")] async fn load_all_modules() { // program a.kcl let program_a_kcl = r#" export a = 1 "#; // program b.kcl let program_b_kcl = r#" import a from 'a.kcl' export b = a + 1 "#; // program c.kcl let program_c_kcl = r#" import a from 'a.kcl' export c = a + 2 "#; // program main.kcl let main_kcl = r#" import b from 'b.kcl' import c from 'c.kcl' d = b + c "#; let main = crate::parsing::parse_str(main_kcl, ModuleId::default()) .parse_errs_as_err() .unwrap(); let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_load_all_modules").unwrap(); tokio::fs::File::create(tmpdir.path().join("main.kcl")) .await .unwrap() .write_all(main_kcl.as_bytes()) .await .unwrap(); tokio::fs::File::create(tmpdir.path().join("a.kcl")) .await .unwrap() .write_all(program_a_kcl.as_bytes()) .await .unwrap(); tokio::fs::File::create(tmpdir.path().join("b.kcl")) .await .unwrap() .write_all(program_b_kcl.as_bytes()) .await .unwrap(); tokio::fs::File::create(tmpdir.path().join("c.kcl")) .await .unwrap() .write_all(program_c_kcl.as_bytes()) .await .unwrap(); let exec_ctxt = ExecutorContext { engine: Arc::new(Box::new( crate::engine::conn_mock::EngineConnection::new() .await .map_err(|err| { KclError::Internal(crate::errors::KclErrorDetails { message: format!("Failed to create mock engine connection: {}", err), source_ranges: vec![SourceRange::default()], }) }) .unwrap(), )), fs: Arc::new(crate::fs::FileManager::new()), settings: ExecutorSettings { project_directory: Some(crate::TypedPath(tmpdir.path().into())), ..Default::default() }, stdlib: Arc::new(crate::std::StdLib::new()), context_type: ContextType::Mock, }; let mut exec_state = ExecState::new(&exec_ctxt); exec_ctxt .run( &crate::Program { ast: main.clone(), original_file_contents: "".to_owned(), }, &mut exec_state, ) .await .unwrap(); } #[tokio::test(flavor = "multi_thread")] async fn user_coercion() { let program = r#"fn foo(x: Axis2d) { return 0 } foo(x = { direction = [0, 0], origin = [0, 0]}) "#; parse_execute(program).await.unwrap(); let program = r#"fn foo(x: Axis3d) { return 0 } foo(x = { direction = [0, 0], origin = [0, 0]}) "#; parse_execute(program).await.unwrap_err(); } #[tokio::test(flavor = "multi_thread")] async fn coerce_return() { let program = r#"fn foo(): number(mm) { return 42 } a = foo() "#; parse_execute(program).await.unwrap(); let program = r#"fn foo(): number(mm) { return { bar: 42 } } a = foo() "#; parse_execute(program).await.unwrap_err(); } #[tokio::test(flavor = "multi_thread")] async fn test_sensible_error_when_missing_equals_in_kwarg() { for (i, call) in ["f(x=1,3,0)", "f(x=1,3,z)", "f(x=1,0,z=1)", "f(x=1, 3 + 4, z)"] .into_iter() .enumerate() { let program = format!( "fn foo() {{ return 0 }} z = 0 fn f(x, y, z) {{ return 0 }} {call}" ); let err = parse_execute(&program).await.unwrap_err(); let msg = err.message(); assert!( msg.contains("This argument needs a label, but it doesn't have one"), "failed test {i}: {msg}" ); assert!(msg.contains("`y`"), "failed test {i}, missing `y`: {msg}"); if i == 0 { assert!(msg.contains("`z`"), "failed test {i}, missing `z`: {msg}"); } } } #[tokio::test(flavor = "multi_thread")] async fn default_param_for_unlabeled() { // Tests that the input param for myExtrude is taken from the pipeline value and same-name // keyword args. let ast = r#"fn myExtrude(@sk, length) { return extrude(sk, length) } sketch001 = startSketchOn(XY) |> circle(center = [0, 0], radius = 93.75) |> myExtrude(length = 40) "#; parse_execute(ast).await.unwrap(); } #[tokio::test(flavor = "multi_thread")] async fn dont_use_unlabelled_as_input() { // `length` should be used as the `length` argument to extrude, not the unlabelled input let ast = r#"length = 10 startSketchOn(XY) |> circle(center = [0, 0], radius = 93.75) |> extrude(length) "#; parse_execute(ast).await.unwrap(); } #[tokio::test(flavor = "multi_thread")] async fn ascription_in_binop() { let ast = r#"foo = tan(0): number(rad) - 4deg"#; parse_execute(ast).await.unwrap(); } #[tokio::test(flavor = "multi_thread")] async fn neg_sqrt() { let ast = r#"bad = sqrt(-2)"#; let e = parse_execute(ast).await.unwrap_err(); // Make sure we get a useful error message and not an engine error. assert!(e.message().contains("sqrt"), "Error message: '{}'", e.message()); } }