diff --git a/src/wasm-lib/kcl/src/execution/annotations.rs b/src/wasm-lib/kcl/src/execution/annotations.rs index 969c52fbe7..13d5cac61b 100644 --- a/src/wasm-lib/kcl/src/execution/annotations.rs +++ b/src/wasm-lib/kcl/src/execution/annotations.rs @@ -10,6 +10,7 @@ use crate::{ pub(crate) const SETTINGS: &str = "settings"; pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit"; pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit"; +pub(super) const NO_PRELUDE: &str = "no_prelude"; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(super) enum AnnotationScope { @@ -23,7 +24,7 @@ pub(super) fn expect_properties<'a>( ) -> Result<&'a [Node], KclError> { match annotation { NonCodeValue::Annotation { name, properties } => { - assert_eq!(name.name, for_key); + assert_eq!(name.as_ref().unwrap().name, for_key); Ok(&**properties.as_ref().ok_or_else(|| { KclError::Semantic(KclErrorDetails { message: format!("Empty `{for_key}` annotation"), diff --git a/src/wasm-lib/kcl/src/execution/cache.rs b/src/wasm-lib/kcl/src/execution/cache.rs index 689166f141..d8f77c7daf 100644 --- a/src/wasm-lib/kcl/src/execution/cache.rs +++ b/src/wasm-lib/kcl/src/execution/cache.rs @@ -128,7 +128,7 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf properties: new_properties, }, ) => { - name.digest == new_name.digest + name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest) && properties .as_ref() .map(|props| props.iter().map(|p| p.digest).collect::>()) diff --git a/src/wasm-lib/kcl/src/execution/exec_ast.rs b/src/wasm-lib/kcl/src/execution/exec_ast.rs index 213f5dfd16..c49560840c 100644 --- a/src/wasm-lib/kcl/src/execution/exec_ast.rs +++ b/src/wasm-lib/kcl/src/execution/exec_ast.rs @@ -10,10 +10,9 @@ use crate::{ annotations, cad_op::{OpArg, Operation}, state::ModuleState, - BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ModuleRepr, ProgramMemory, - TagEngineInfo, TagIdentifier, + BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ModulePath, ModuleRepr, + ProgramMemory, TagEngineInfo, TagIdentifier, }, - fs::FileSystem, parsing::ast::types::{ ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility, @@ -38,7 +37,8 @@ impl ExecutorContext { annotations: impl Iterator, scope: annotations::AnnotationScope, exec_state: &mut ExecState, - ) -> Result<(), KclError> { + ) -> Result { + let mut no_prelude = false; for (annotation, source_range) in annotations { if annotation.annotation_name() == Some(annotations::SETTINGS) { if scope == annotations::AnnotationScope::Module { @@ -48,7 +48,7 @@ impl ExecutorContext { .settings .update_from_annotation(annotation, source_range)?; let new_units = exec_state.length_unit(); - if old_units != new_units { + if !self.engine.execution_kind().is_isolated() && old_units != new_units { self.engine.set_units(new_units.into(), source_range).await?; } } else { @@ -58,9 +58,19 @@ impl ExecutorContext { })); } } + if annotation.annotation_name() == Some(annotations::NO_PRELUDE) { + if scope == annotations::AnnotationScope::Module { + no_prelude = true; + } else { + return Err(KclError::Semantic(KclErrorDetails { + message: "Prelude can only be skipped at the top level scope of a file".to_owned(), + source_ranges: vec![source_range], + })); + } + } // TODO warn on unknown annotations } - Ok(()) + Ok(no_prelude) } /// Execute an AST's program. @@ -71,22 +81,32 @@ impl ExecutorContext { exec_state: &mut ExecState, body_type: BodyType, ) -> Result, KclError> { - self.handle_annotations( - program - .non_code_meta - .start_nodes - .iter() - .filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))), - annotations::AnnotationScope::Module, - exec_state, - ) - .await?; + if body_type == BodyType::Root { + let _no_prelude = self + .handle_annotations( + program + .non_code_meta + .start_nodes + .iter() + .filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))), + annotations::AnnotationScope::Module, + exec_state, + ) + .await?; + } let mut last_expr = None; // Iterate over the body of the program. - for statement in &program.body { + for (i, statement) in program.body.iter().enumerate() { match statement { BodyItem::ImportStatement(import_stmt) => { + if 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 module_id = self.open_module(&import_stmt.path, exec_state, source_range).await?; @@ -178,6 +198,14 @@ impl ExecutorContext { let source_range = SourceRange::from(&variable_declaration.declaration.init); let metadata = Metadata { source_range }; + let _meta_nodes = if i == 0 { + &program.non_code_meta.start_nodes + } else if let Some(meta) = program.non_code_meta.non_code_nodes.get(&(i - 1)) { + meta + } else { + &Vec::new() + }; + let memory_item = self .execute_expr( &variable_declaration.declaration.init, @@ -231,63 +259,45 @@ impl ExecutorContext { exec_state: &mut ExecState, source_range: SourceRange, ) -> Result { + let resolved_path = ModulePath::from_import_path(path, &self.settings.project_directory); match path { - ImportPath::Kcl { filename } => { - let resolved_path = if let Some(project_dir) = &self.settings.project_directory { - project_dir.join(filename) - } else { - std::path::PathBuf::from(filename) - }; + ImportPath::Kcl { .. } => { + exec_state.global.mod_loader.cycle_check(&resolved_path, source_range)?; - if exec_state.mod_local.import_stack.contains(&resolved_path) { - return Err(KclError::ImportCycle(KclErrorDetails { - message: format!( - "circular import of modules is not allowed: {} -> {}", - exec_state - .mod_local - .import_stack - .iter() - .map(|p| p.as_path().to_string_lossy()) - .collect::>() - .join(" -> "), - resolved_path.to_string_lossy() - ), - source_ranges: vec![source_range], - })); - } - - if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) { - return Ok(*id); + if let Some(id) = exec_state.id_for_module(&resolved_path) { + return Ok(id); } - let source = self.fs.read_to_string(&resolved_path, source_range).await?; - let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len()); + let id = exec_state.next_module_id(); + let source = resolved_path.source(&self.fs, source_range).await?; // TODO handle parsing errors properly let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?; - let repr = ModuleRepr::Kcl(parsed); - - Ok(exec_state.add_module(id, resolved_path, repr)) + exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed)); + Ok(id) } - ImportPath::Foreign { path } => { - let resolved_path = if let Some(project_dir) = &self.settings.project_directory { - project_dir.join(path) - } else { - std::path::PathBuf::from(path) - }; + ImportPath::Foreign { .. } => { + if let Some(id) = exec_state.id_for_module(&resolved_path) { + return Ok(id); + } - if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) { - return Ok(*id); + let id = exec_state.next_module_id(); + let geom = + super::import::import_foreign(resolved_path.expect_path(), None, exec_state, self, source_range) + .await?; + exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom)); + Ok(id) + } + ImportPath::Std { .. } => { + if let Some(id) = exec_state.id_for_module(&resolved_path) { + return Ok(id); } - let geom = super::import::import_foreign(&resolved_path, None, exec_state, self, source_range).await?; - let repr = ModuleRepr::Foreign(geom); - let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len()); - Ok(exec_state.add_module(id, resolved_path, repr)) + let id = exec_state.next_module_id(); + let source = resolved_path.source(&self.fs, source_range).await?; + let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err().unwrap(); + exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed)); + Ok(id) } - i => Err(KclError::Semantic(KclErrorDetails { - message: format!("Unsupported import: `{i}`"), - source_ranges: vec![source_range], - })), } } @@ -307,22 +317,20 @@ impl ExecutorContext { message: format!( "circular import of modules is not allowed: {} -> {}", exec_state - .mod_local + .global + .mod_loader .import_stack .iter() .map(|p| p.as_path().to_string_lossy()) .collect::>() .join(" -> "), - info.path.display() + info.path ), source_ranges: vec![source_range], })), ModuleRepr::Kcl(program) => { - let mut local_state = ModuleState { - import_stack: exec_state.mod_local.import_stack.clone(), - ..ModuleState::new(&self.settings) - }; - local_state.import_stack.push(info.path.clone()); + let mut local_state = ModuleState::new(&self.settings); + exec_state.global.mod_loader.enter_module(&info.path); std::mem::swap(&mut exec_state.mod_local, &mut local_state); let original_execution = self.engine.replace_execution_kind(exec_kind); @@ -332,7 +340,8 @@ impl ExecutorContext { let new_units = exec_state.length_unit(); std::mem::swap(&mut exec_state.mod_local, &mut local_state); - if new_units != old_units { + exec_state.global.mod_loader.leave_module(&info.path); + if !exec_kind.is_isolated() && new_units != old_units { self.engine.set_units(old_units.into(), Default::default()).await?; } self.engine.replace_execution_kind(original_execution); @@ -345,7 +354,7 @@ impl ExecutorContext { KclError::Semantic(KclErrorDetails { message: format!( "Error loading imported file. Open it to view more details. {}: {}", - info.path.display(), + info.path, err.message() ), source_ranges: vec![source_range], diff --git a/src/wasm-lib/kcl/src/execution/mod.rs b/src/wasm-lib/kcl/src/execution/mod.rs index 6421406eec..918e78870c 100644 --- a/src/wasm-lib/kcl/src/execution/mod.rs +++ b/src/wasm-lib/kcl/src/execution/mod.rs @@ -1,6 +1,6 @@ //! The executor for the AST. -use std::{path::PathBuf, sync::Arc}; +use std::{fmt, path::PathBuf, sync::Arc}; use anyhow::Result; pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId}; @@ -26,13 +26,13 @@ pub use state::{ExecState, IdGenerator, MetaSettings}; use crate::{ engine::EngineManager, - errors::KclError, + errors::{KclError, KclErrorDetails}, execution::{ artifact::build_artifact_graph, cache::{CacheInformation, CacheResult}, }, - fs::FileManager, - parsing::ast::types::{Expr, FunctionExpression, Node, NodeRef, Program}, + fs::{FileManager, FileSystem}, + parsing::ast::types::{Expr, FunctionExpression, ImportPath, Node, NodeRef, Program}, settings::types::UnitLength, source_range::{ModuleId, SourceRange}, std::{args::Arg, StdLib}, @@ -169,10 +169,62 @@ pub struct ModuleInfo { /// The ID of the module. id: ModuleId, /// Absolute path of the module's source file. - path: std::path::PathBuf, + path: ModulePath, repr: ModuleRepr, } +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)] +pub enum ModulePath { + Local(std::path::PathBuf), + Std(String), +} + +impl ModulePath { + fn expect_path(&self) -> &std::path::PathBuf { + match self { + ModulePath::Local(p) => p, + _ => unreachable!(), + } + } + + pub(crate) async fn source(&self, fs: &FileManager, source_range: SourceRange) -> Result { + match self { + ModulePath::Local(p) => fs.read_to_string(p, source_range).await, + ModulePath::Std(_) => unimplemented!(), + } + } + + pub(crate) fn from_import_path(path: &ImportPath, project_directory: &Option) -> Self { + match path { + ImportPath::Kcl { filename: path } | ImportPath::Foreign { path } => { + let resolved_path = if let Some(project_dir) = project_directory { + project_dir.join(path) + } else { + std::path::PathBuf::from(path) + }; + ModulePath::Local(resolved_path) + } + ImportPath::Std { path } => { + // For now we only support importing from singly-nested modules inside std. + assert_eq!(path.len(), 2); + assert_eq!(&path[0], "std"); + + ModulePath::Std(path[1].clone()) + } + } + } +} + +impl fmt::Display for ModulePath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ModulePath::Local(path) => path.display().fmt(f), + ModulePath::Std(s) => write!(f, "std::{s}"), + } + } +} + #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum ModuleRepr { @@ -181,6 +233,47 @@ pub enum ModuleRepr { Foreign(import::PreImportedGeometry), } +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ModuleLoader { + /// The stack of import statements for detecting circular module imports. + /// If this is empty, we're not currently executing an import statement. + pub import_stack: Vec, +} + +impl ModuleLoader { + pub(crate) fn cycle_check(&self, path: &ModulePath, source_range: SourceRange) -> Result<(), KclError> { + if self.import_stack.contains(path.expect_path()) { + return Err(KclError::ImportCycle(KclErrorDetails { + message: format!( + "circular import of modules is not allowed: {} -> {}", + self.import_stack + .iter() + .map(|p| p.as_path().to_string_lossy()) + .collect::>() + .join(" -> "), + path, + ), + source_ranges: vec![source_range], + })); + } + Ok(()) + } + + pub(crate) fn enter_module(&mut self, path: &ModulePath) { + if let ModulePath::Local(ref path) = path { + self.import_stack.push(path.clone()); + } + } + + pub(crate) fn leave_module(&mut self, path: &ModulePath) { + if let ModulePath::Local(ref path) = path { + let popped = self.import_stack.pop().unwrap(); + assert_eq!(path, &popped); + } + } +} + /// Metadata. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)] #[ts(export)] diff --git a/src/wasm-lib/kcl/src/execution/state.rs b/src/wasm-lib/kcl/src/execution/state.rs index 09838d2608..d6585697f6 100644 --- a/src/wasm-lib/kcl/src/execution/state.rs +++ b/src/wasm-lib/kcl/src/execution/state.rs @@ -9,7 +9,8 @@ use crate::{ errors::{KclError, KclErrorDetails}, execution::{ annotations, kcl_value, Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, ExecOutcome, ExecutorSettings, - KclValue, ModuleInfo, ModuleRepr, Operation, ProgramMemory, SolidLazyIds, UnitAngle, UnitLen, + KclValue, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr, Operation, ProgramMemory, SolidLazyIds, UnitAngle, + UnitLen, }, parsing::ast::types::NonCodeValue, source_range::{ModuleId, SourceRange}, @@ -29,7 +30,7 @@ pub struct GlobalState { /// The stable artifact ID generator. pub id_generator: IdGenerator, /// Map from source file absolute path to module ID. - pub path_to_source_id: IndexMap, + pub path_to_source_id: IndexMap, /// Map from module ID to module info. pub module_infos: IndexMap, /// Output map of UUIDs to artifacts. @@ -45,6 +46,8 @@ pub struct GlobalState { pub artifact_responses: IndexMap, /// Output artifact graph. pub artifact_graph: ArtifactGraph, + /// Module loader. + pub mod_loader: ModuleLoader, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] @@ -59,9 +62,6 @@ pub struct ModuleState { pub pipe_value: Option, /// Identifiers that have been exported from the current module. pub module_exports: Vec, - /// The stack of import statements for detecting circular module imports. - /// If this is empty, we're not currently executing an import statement. - pub import_stack: Vec, /// Operations that have been performed in execution order, for display in /// the Feature Tree. pub operations: Vec, @@ -124,15 +124,21 @@ impl ExecState { self.global.artifacts.insert(id, artifact); } - pub(super) fn add_module(&mut self, id: ModuleId, path: std::path::PathBuf, repr: ModuleRepr) -> ModuleId { + pub(super) fn next_module_id(&self) -> ModuleId { + ModuleId::from_usize(self.global.path_to_source_id.len()) + } + + pub(super) fn id_for_module(&self, path: &ModulePath) -> Option { + self.global.path_to_source_id.get(path).cloned() + } + + pub(super) fn add_module(&mut self, id: ModuleId, path: ModulePath, repr: ModuleRepr) { debug_assert!(!self.global.path_to_source_id.contains_key(&path)); self.global.path_to_source_id.insert(path.clone(), id); let module_info = ModuleInfo { id, repr, path }; self.global.module_infos.insert(id, module_info); - - id } pub fn length_unit(&self) -> UnitLen { @@ -154,6 +160,7 @@ impl GlobalState { artifact_commands: Default::default(), artifact_responses: Default::default(), artifact_graph: Default::default(), + mod_loader: Default::default(), }; let root_id = ModuleId::default(); @@ -162,11 +169,11 @@ impl GlobalState { root_id, ModuleInfo { id: root_id, - path: root_path.clone(), + path: ModulePath::Local(root_path.clone()), repr: ModuleRepr::Root, }, ); - global.path_to_source_id.insert(root_path, root_id); + global.path_to_source_id.insert(ModulePath::Local(root_path), root_id); global } } @@ -178,7 +185,6 @@ impl ModuleState { dynamic_state: Default::default(), pipe_value: Default::default(), module_exports: Default::default(), - import_stack: Default::default(), operations: Default::default(), settings: MetaSettings { default_length_units: exec_settings.units.into(), diff --git a/src/wasm-lib/kcl/src/parsing/ast/digest.rs b/src/wasm-lib/kcl/src/parsing/ast/digest.rs index 79768fbd7a..5de6d3427d 100644 --- a/src/wasm-lib/kcl/src/parsing/ast/digest.rs +++ b/src/wasm-lib/kcl/src/parsing/ast/digest.rs @@ -121,7 +121,9 @@ impl NonCodeValue { ref mut name, properties, } => { - hasher.update(name.compute_digest()); + if let Some(name) = name { + hasher.update(name.compute_digest()); + } if let Some(properties) = properties { hasher.update(properties.len().to_ne_bytes()); for property in properties.iter_mut() { diff --git a/src/wasm-lib/kcl/src/parsing/ast/types/mod.rs b/src/wasm-lib/kcl/src/parsing/ast/types/mod.rs index 2b6f750213..b4b8388919 100644 --- a/src/wasm-lib/kcl/src/parsing/ast/types/mod.rs +++ b/src/wasm-lib/kcl/src/parsing/ast/types/mod.rs @@ -27,6 +27,7 @@ use crate::{ errors::KclError, execution::{annotations, KclValue, Metadata, TagIdentifier}, parsing::{ast::digest::Digest, PIPE_OPERATOR}, + pretty::NumericSuffix, source_range::{ModuleId, SourceRange}, }; @@ -868,6 +869,43 @@ impl Expr { } } + pub fn literal_bool(&self) -> Option { + match self { + Expr::Literal(lit) => match lit.value { + LiteralValue::Bool(b) => Some(b), + _ => None, + }, + _ => None, + } + } + + pub fn literal_num(&self) -> Option<(f64, NumericSuffix)> { + match self { + Expr::Literal(lit) => match lit.value { + LiteralValue::Number { value, suffix } => Some((value, suffix)), + _ => None, + }, + _ => None, + } + } + + pub fn literal_str(&self) -> Option<&str> { + match self { + Expr::Literal(lit) => match &lit.value { + LiteralValue::String(s) => Some(s), + _ => None, + }, + _ => None, + } + } + + pub fn ident_name(&self) -> Option<&str> { + match self { + Expr::Identifier(ident) => Some(&ident.name), + _ => None, + } + } + /// Describe this expression's type for a human, for typechecking. /// This is a best-effort function, it's OK to give a shitty string here (but we should work on improving it) pub fn human_friendly_type(&self) -> &'static str { @@ -1089,7 +1127,7 @@ impl NonCodeNode { NonCodeValue::BlockComment { value, style: _ } => value.clone(), NonCodeValue::NewLineBlockComment { value, style: _ } => value.clone(), NonCodeValue::NewLine => "\n\n".to_string(), - NonCodeValue::Annotation { name, .. } => name.name.clone(), + n @ NonCodeValue::Annotation { .. } => n.annotation_name().unwrap_or("").to_owned(), } } @@ -1158,7 +1196,7 @@ pub enum NonCodeValue { // This is also not a comment. NewLine, Annotation { - name: Node, + name: Option>, properties: Option>>, }, } @@ -1166,7 +1204,7 @@ pub enum NonCodeValue { impl NonCodeValue { pub fn annotation_name(&self) -> Option<&str> { match self { - NonCodeValue::Annotation { name, .. } => Some(&name.name), + NonCodeValue::Annotation { name, .. } => name.as_ref().map(|i| &*i.name), _ => None, } } @@ -1184,7 +1222,7 @@ impl NonCodeValue { )); } NonCodeValue::Annotation { - name: Identifier::new(annotations::SETTINGS), + name: Some(Identifier::new(annotations::SETTINGS)), properties: Some(properties), } } @@ -1212,6 +1250,20 @@ impl NonCodeMeta { pub fn non_code_nodes_len(&self) -> usize { self.non_code_nodes.values().map(|x| x.len()).sum() } + + pub fn insert(&mut self, i: usize, new: Node) { + self.non_code_nodes.entry(i).or_default().push(new); + } + + pub fn contains(&self, pos: usize) -> bool { + if self.start_nodes.iter().any(|node| node.contains(pos)) { + return true; + } + + self.non_code_nodes + .iter() + .any(|(_, nodes)| nodes.iter().any(|node| node.contains(pos))) + } } // implement Deserialize manually because we to force the keys of non_code_nodes to be usize @@ -1242,22 +1294,6 @@ impl<'de> Deserialize<'de> for NonCodeMeta { } } -impl NonCodeMeta { - pub fn insert(&mut self, i: usize, new: Node) { - self.non_code_nodes.entry(i).or_default().push(new); - } - - pub fn contains(&self, pos: usize) -> bool { - if self.start_nodes.iter().any(|node| node.contains(pos)) { - return true; - } - - self.non_code_nodes - .iter() - .any(|(_, nodes)| nodes.iter().any(|node| node.contains(pos))) - } -} - #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[ts(export)] #[serde(tag = "type")] @@ -1382,14 +1418,14 @@ impl ImportSelector { pub enum ImportPath { Kcl { filename: String }, Foreign { path: String }, - Std, + Std { path: Vec }, } impl fmt::Display for ImportPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => write!(f, "{s}"), - ImportPath::Std => write!(f, "std"), + ImportPath::Std { path } => write!(f, "{}", path.join("::")), } } } @@ -1746,7 +1782,7 @@ pub enum ItemVisibility { } impl ItemVisibility { - fn is_default(&self) -> bool { + pub fn is_default(&self) -> bool { matches!(self, Self::Default) } } @@ -2941,16 +2977,14 @@ impl PipeExpression { } } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, FromStr, Display)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] #[serde(tag = "type")] -#[display(style = "snake_case")] pub enum FnArgPrimitive { /// A string type. String, /// A number type. - Number, + Number(NumericSuffix), /// A boolean type. - #[display("bool")] #[serde(rename = "bool")] Boolean, /// A tag. @@ -2967,12 +3001,46 @@ impl FnArgPrimitive { pub fn digestable_id(&self) -> &[u8] { match self { FnArgPrimitive::String => b"string", - FnArgPrimitive::Number => b"number", - FnArgPrimitive::Boolean => b"boolean", + FnArgPrimitive::Number(suffix) => suffix.digestable_id(), + FnArgPrimitive::Boolean => b"bool", FnArgPrimitive::Tag => b"tag", - FnArgPrimitive::Sketch => b"sketch", - FnArgPrimitive::SketchSurface => b"sketch_surface", - FnArgPrimitive::Solid => b"solid", + FnArgPrimitive::Sketch => b"Sketch", + FnArgPrimitive::SketchSurface => b"SketchSurface", + FnArgPrimitive::Solid => b"Solid", + } + } + + pub fn from_str(s: &str, suffix: Option) -> Option { + match (s, suffix) { + ("string", None) => Some(FnArgPrimitive::String), + ("bool", None) => Some(FnArgPrimitive::Boolean), + ("tag", None) => Some(FnArgPrimitive::Tag), + ("Sketch", None) => Some(FnArgPrimitive::Sketch), + ("SketchSurface", None) => Some(FnArgPrimitive::SketchSurface), + ("Solid", None) => Some(FnArgPrimitive::Solid), + ("number", None) => Some(FnArgPrimitive::Number(NumericSuffix::None)), + ("number", Some(s)) => Some(FnArgPrimitive::Number(s)), + _ => None, + } + } +} + +impl fmt::Display for FnArgPrimitive { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FnArgPrimitive::Number(suffix) => { + write!(f, "number")?; + if *suffix != NumericSuffix::None { + write!(f, "({suffix})")?; + } + Ok(()) + } + FnArgPrimitive::String => write!(f, "string"), + FnArgPrimitive::Boolean => write!(f, "bool"), + FnArgPrimitive::Tag => write!(f, "tag"), + FnArgPrimitive::Sketch => write!(f, "Sketch"), + FnArgPrimitive::SketchSurface => write!(f, "SketchSurface"), + FnArgPrimitive::Solid => write!(f, "Solid"), } } } @@ -3029,8 +3097,7 @@ pub struct Parameter { pub identifier: Node, /// The type of the parameter. /// This is optional if the user defines a type. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(skip)] + #[serde(skip)] pub type_: Option, /// Is the parameter optional? /// If so, what is its default value? @@ -3516,7 +3583,7 @@ const cylinder = startSketchOn('-XZ') #[tokio::test(flavor = "multi_thread")] async fn test_parse_type_args_on_functions() { - let some_program_string = r#"fn thing = (arg0: number, arg1: string, tag?: string) => { + let some_program_string = r#"fn thing = (arg0: number(mm), arg1: string, tag?: string) => { return arg0 }"#; let program = crate::parsing::top_level_parse(some_program_string).unwrap(); @@ -3531,7 +3598,10 @@ const cylinder = startSketchOn('-XZ') }; let params = &func_expr.params; assert_eq!(params.len(), 3); - assert_eq!(params[0].type_, Some(FnArgType::Primitive(FnArgPrimitive::Number))); + assert_eq!( + params[0].type_, + Some(FnArgType::Primitive(FnArgPrimitive::Number(NumericSuffix::Mm))) + ); assert_eq!(params[1].type_, Some(FnArgType::Primitive(FnArgPrimitive::String))); assert_eq!(params[2].type_, Some(FnArgType::Primitive(FnArgPrimitive::String))); } @@ -3553,7 +3623,10 @@ const cylinder = startSketchOn('-XZ') }; let params = &func_expr.params; assert_eq!(params.len(), 3); - assert_eq!(params[0].type_, Some(FnArgType::Array(FnArgPrimitive::Number))); + assert_eq!( + params[0].type_, + Some(FnArgType::Array(FnArgPrimitive::Number(NumericSuffix::None))) + ); assert_eq!(params[1].type_, Some(FnArgType::Array(FnArgPrimitive::String))); assert_eq!(params[2].type_, Some(FnArgType::Primitive(FnArgPrimitive::String))); } @@ -3576,7 +3649,10 @@ const cylinder = startSketchOn('-XZ') }; let params = &func_expr.params; assert_eq!(params.len(), 3); - assert_eq!(params[0].type_, Some(FnArgType::Array(FnArgPrimitive::Number))); + assert_eq!( + params[0].type_, + Some(FnArgType::Array(FnArgPrimitive::Number(NumericSuffix::None))) + ); assert_eq!( params[1].type_, Some(FnArgType::Object { @@ -3591,7 +3667,7 @@ const cylinder = startSketchOn('-XZ') 40, module_id, ), - type_: Some(FnArgType::Primitive(FnArgPrimitive::Number)), + type_: Some(FnArgType::Primitive(FnArgPrimitive::Number(NumericSuffix::None))), default_value: None, labeled: true, digest: None, @@ -3664,7 +3740,7 @@ const cylinder = startSketchOn('-XZ') 18, module_id, ), - type_: Some(FnArgType::Primitive(FnArgPrimitive::Number)), + type_: Some(FnArgType::Primitive(FnArgPrimitive::Number(NumericSuffix::None))), default_value: None, labeled: true, digest: None diff --git a/src/wasm-lib/kcl/src/parsing/parser.rs b/src/wasm-lib/kcl/src/parsing/parser.rs index 30dd7048d2..455648cab0 100644 --- a/src/wasm-lib/kcl/src/parsing/parser.rs +++ b/src/wasm-lib/kcl/src/parsing/parser.rs @@ -1,7 +1,7 @@ // TODO optimise size of CompilationError #![allow(clippy::result_large_err)] -use std::{cell::RefCell, collections::BTreeMap, str::FromStr}; +use std::{cell::RefCell, collections::BTreeMap}; use winnow::{ combinator::{alt, delimited, opt, peek, preceded, repeat, separated, separated_pair, terminated}, @@ -286,8 +286,8 @@ fn non_code_node(i: &mut TokenSlice) -> PResult> { fn annotation(i: &mut TokenSlice) -> PResult> { let at = at_sign.parse_next(i)?; - let name = binding_name.parse_next(i)?; - let mut end = name.end; + let name = opt(binding_name).parse_next(i)?; + let mut end = name.as_ref().map(|n| n.end).unwrap_or(at.end); let properties = if peek(open_paren).parse_next(i).is_ok() { open_paren(i)?; @@ -320,6 +320,12 @@ fn annotation(i: &mut TokenSlice) -> PResult> { None }; + if name.is_none() && properties.is_none() { + return Err(ErrMode::Cut( + CompilationError::fatal(at.as_source_range(), format!("Unexpected token: {}", at.value)).into(), + )); + } + let value = NonCodeValue::Annotation { name, properties }; Ok(Node::new( NonCodeNode { value, digest: None }, @@ -491,7 +497,7 @@ pub(crate) fn unsigned_number_literal(i: &mut TokenSlice) -> PResult PResult<(Node, bool) // Optional return type. let return_type = opt(return_type).parse_next(i)?; ignore_whitespace(i); - open_brace(i)?; - let body = function_body(i)?; - let end = close_brace(i)?.end; + let brace = open_brace(i)?; + let close: Option<(Vec>, Token)> = opt((repeat(0.., whitespace), close_brace)).parse_next(i)?; + let (body, end) = match close { + Some((_, end)) => ( + Node::new( + Program { + body: Vec::new(), + non_code_meta: NonCodeMeta::default(), + shebang: None, + digest: None, + }, + brace.end, + brace.end, + brace.module_id, + ), + end.end, + ), + None => (function_body(i)?, close_brace(i)?.end), + }; let result = Node::new( FunctionExpression { params, @@ -1587,6 +1609,14 @@ fn import_stmt(i: &mut TokenSlice) -> PResult> { ) .into(), )); + } else if matches!(path, ImportPath::Std { .. }) && matches!(selector, ImportSelector::None { .. }) { + return Err(ErrMode::Cut( + CompilationError::fatal( + SourceRange::new(start, end, module_id), + "the standard library cannot be imported as a part", + ) + .into(), + )); } Ok(Node::boxed( @@ -1639,13 +1669,34 @@ fn validate_path_string(path_string: String, var_name: bool, path_range: SourceR } ImportPath::Kcl { filename: path_string } - } else if path_string.starts_with("std") { + } else if path_string.starts_with("std::") { ParseContext::warn(CompilationError::err( path_range, "explicit imports from the standard library are experimental, likely to be buggy, and likely to change.", )); - ImportPath::Std + let segments: Vec = path_string.split("::").map(str::to_owned).collect(); + + for s in &segments { + if s.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') || s.starts_with('_') { + return Err(ErrMode::Cut( + CompilationError::fatal(path_range, "invalid path in import statement.").into(), + )); + } + } + + // For now we only support importing from singly-nested modules inside std. + if segments.len() != 2 { + return Err(ErrMode::Cut( + CompilationError::fatal( + path_range, + format!("Invalid import path for import from std: {}.", path_string), + ) + .into(), + )); + } + + ImportPath::Std { path: segments } } else if path_string.contains('.') { let extn = &path_string[path_string.rfind('.').unwrap() + 1..]; if !FOREIGN_IMPORT_EXTENSIONS.contains(&extn) { @@ -2433,11 +2484,19 @@ fn argument_type(i: &mut TokenSlice) -> PResult { // TODO it is buggy to treat object fields like parameters since the parameters parser assumes a terminating `)`. (open_brace, parameters, close_brace).map(|(_, params, _)| Ok(FnArgType::Object { properties: params })), // Array types - (one_of(TokenType::Type), open_bracket, close_bracket).map(|(token, _, _)| { - FnArgPrimitive::from_str(&token.value) - .map(FnArgType::Array) - .map_err(|err| CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", err))) - }), + ( + one_of(TokenType::Type), + opt(delimited(open_paren, uom_for_type, close_paren)), + open_bracket, + close_bracket, + ) + .map(|(token, uom, _, _)| { + FnArgPrimitive::from_str(&token.value, uom) + .map(FnArgType::Array) + .ok_or_else(|| { + CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", token.value)) + }) + }), // Primitive types ( one_of(TokenType::Type), @@ -2445,14 +2504,16 @@ fn argument_type(i: &mut TokenSlice) -> PResult { ) .map(|(token, suffix)| { if suffix.is_some() { - ParseContext::err(CompilationError::err( + ParseContext::warn(CompilationError::err( (&token).into(), "Unit of Measure types are experimental and currently do nothing.", )); } - FnArgPrimitive::from_str(&token.value) + FnArgPrimitive::from_str(&token.value, suffix) .map(FnArgType::Primitive) - .map_err(|err| CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", err))) + .ok_or_else(|| { + CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", token.value)) + }) }), )) .parse_next(i)? @@ -2516,8 +2577,7 @@ fn parameters(i: &mut TokenSlice) -> PResult> { type_, default_value, }| { - let identifier = - Node::::try_from(arg_name).and_then(Node::::into_valid_binding_name)?; + let identifier = Node::::try_from(arg_name)?; Ok(Parameter { identifier, @@ -2564,24 +2624,9 @@ fn optional_after_required(params: &[Parameter]) -> Result<(), CompilationError> Ok(()) } -impl Node { - fn into_valid_binding_name(self) -> Result, CompilationError> { - // Make sure they are not assigning a variable to a stdlib function. - if crate::std::name_in_stdlib(&self.name) { - return Err(CompilationError::fatal( - SourceRange::from(&self), - format!("Cannot assign a variable to a reserved keyword: {}", self.name), - )); - } - Ok(self) - } -} - /// Introduce a new name, which binds some value. fn binding_name(i: &mut TokenSlice) -> PResult> { identifier - .context(expected("an identifier, which will be the name of some value")) - .try_map(Node::::into_valid_binding_name) .context(expected("an identifier, which will be the name of some value")) .parse_next(i) } @@ -4018,17 +4063,6 @@ e ); } - #[test] - fn test_error_stdlib_in_fn_name() { - assert_err( - r#"fn cos = () => { - return 1 - }"#, - "Cannot assign a variable to a reserved keyword: cos", - [3, 6], - ); - } - #[test] fn test_error_keyword_in_fn_args() { assert_err( @@ -4040,17 +4074,6 @@ e ) } - #[test] - fn test_error_stdlib_in_fn_args() { - assert_err( - r#"fn thing = (cos) => { - return 1 -}"#, - "Cannot assign a variable to a reserved keyword: cos", - [12, 15], - ) - } - #[test] fn bad_imports() { assert_err( @@ -4095,6 +4118,27 @@ e ); } + #[test] + fn std_fn_decl() { + let code = r#"/// Compute the cosine of a number (in radians). +/// +/// ``` +/// exampleSketch = startSketchOn("XZ") +/// |> startProfileAt([0, 0], %) +/// |> angledLine({ +/// angle = 30, +/// length = 3 / cos(toRadians(30)), +/// }, %) +/// |> yLineTo(0, %) +/// |> close(%) +/// +/// example = extrude(5, exampleSketch) +/// ``` +@(impl = std_rust) +export fn cos(num: number(rad)): number(_) {}"#; + let _ast = crate::parsing::top_level_parse(code).unwrap(); + } + #[test] fn warn_import() { let some_program_string = r#"import "foo.kcl""#; diff --git a/src/wasm-lib/kcl/src/parsing/snapshots/kcl_lib__parsing__parser__snapshot_tests__kw_function_decl_with_default_and_type.snap b/src/wasm-lib/kcl/src/parsing/snapshots/kcl_lib__parsing__parser__snapshot_tests__kw_function_decl_with_default_and_type.snap index ce17d8cbc9..e3cef2a140 100644 --- a/src/wasm-lib/kcl/src/parsing/snapshots/kcl_lib__parsing__parser__snapshot_tests__kw_function_decl_with_default_and_type.snap +++ b/src/wasm-lib/kcl/src/parsing/snapshots/kcl_lib__parsing__parser__snapshot_tests__kw_function_decl_with_default_and_type.snap @@ -47,10 +47,6 @@ expression: actual "start": 7, "type": "Identifier" }, - "type_": { - "type": "Primitive", - "type": "Number" - }, "default_value": { "type": "Literal", "type": "Literal", diff --git a/src/wasm-lib/kcl/src/parsing/token/mod.rs b/src/wasm-lib/kcl/src/parsing/token/mod.rs index bd64109116..b38bf8d0f4 100644 --- a/src/wasm-lib/kcl/src/parsing/token/mod.rs +++ b/src/wasm-lib/kcl/src/parsing/token/mod.rs @@ -54,6 +54,21 @@ impl NumericSuffix { pub fn is_some(self) -> bool { self != Self::None } + + pub fn digestable_id(&self) -> &[u8] { + match self { + NumericSuffix::None => &[], + NumericSuffix::Count => b"_", + NumericSuffix::Mm => b"mm", + NumericSuffix::Cm => b"cm", + NumericSuffix::M => b"m", + NumericSuffix::Inch => b"in", + NumericSuffix::Ft => b"ft", + NumericSuffix::Yd => b"yd", + NumericSuffix::Deg => b"deg", + NumericSuffix::Rad => b"rad", + } + } } impl FromStr for NumericSuffix { diff --git a/src/wasm-lib/kcl/src/unparser.rs b/src/wasm-lib/kcl/src/unparser.rs index 6bb76f3e75..d5f5be3b60 100644 --- a/src/wasm-lib/kcl/src/unparser.rs +++ b/src/wasm-lib/kcl/src/unparser.rs @@ -161,7 +161,9 @@ impl Node { NonCodeValue::NewLine => "\n\n".to_string(), NonCodeValue::Annotation { name, properties } => { let mut result = "@".to_owned(); - result.push_str(&name.name); + if let Some(name) = name { + result.push_str(&name.name); + } if let Some(properties) = properties { result.push('('); result.push_str( diff --git a/src/wasm-lib/kcl/tests/import_function_not_sketch/artifact_commands.snap b/src/wasm-lib/kcl/tests/import_function_not_sketch/artifact_commands.snap index ddc110e505..7be27702a3 100644 --- a/src/wasm-lib/kcl/tests/import_function_not_sketch/artifact_commands.snap +++ b/src/wasm-lib/kcl/tests/import_function_not_sketch/artifact_commands.snap @@ -1,7 +1,6 @@ --- source: kcl/src/simulation_tests.rs description: Artifact commands import_function_not_sketch.kcl -snapshot_kind: text --- [ { @@ -817,17 +816,5 @@ snapshot_kind: text "edge_id": "[uuid]", "face_id": "[uuid]" } - }, - { - "cmdId": "[uuid]", - "range": [ - 0, - 0, - 0 - ], - "command": { - "type": "set_scene_units", - "unit": "in" - } } ]