diff --git a/src/wasm-lib/kcl/src/errors.rs b/src/wasm-lib/kcl/src/errors.rs index 285d21aef..860c091cd 100644 --- a/src/wasm-lib/kcl/src/errors.rs +++ b/src/wasm-lib/kcl/src/errors.rs @@ -5,7 +5,8 @@ use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity}; use crate::{ execution::{ArtifactCommand, ArtifactGraph, Operation}, lsp::IntoDiagnostic, - source_range::{ModuleId, SourceRange}, + source_range::SourceRange, + ModuleId, }; /// How did the KCL execution fail diff --git a/src/wasm-lib/kcl/src/execution/exec_ast.rs b/src/wasm-lib/kcl/src/execution/exec_ast.rs index c49560840..d734c03c5 100644 --- a/src/wasm-lib/kcl/src/execution/exec_ast.rs +++ b/src/wasm-lib/kcl/src/execution/exec_ast.rs @@ -10,16 +10,17 @@ use crate::{ annotations, cad_op::{OpArg, Operation}, state::ModuleState, - BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ModulePath, ModuleRepr, - ProgramMemory, TagEngineInfo, TagIdentifier, + BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ProgramMemory, TagEngineInfo, + TagIdentifier, }, + modules::{ModuleId, ModulePath, ModuleRepr}, parsing::ast::types::{ ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, NodeRef, NonCodeValue, ObjectExpression, PipeExpression, TagDeclarator, UnaryExpression, UnaryOperator, }, - source_range::{ModuleId, SourceRange}, + source_range::SourceRange, std::{ args::{Arg, KwArgs}, FunctionKind, diff --git a/src/wasm-lib/kcl/src/execution/mod.rs b/src/wasm-lib/kcl/src/execution/mod.rs index 918e78870..877a109b6 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::{fmt, path::PathBuf, sync::Arc}; +use std::{path::PathBuf, sync::Arc}; use anyhow::Result; pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId}; @@ -9,7 +9,9 @@ use cache::OldAstState; pub use cad_op::Operation; pub use exec_ast::FunctionParam; pub use geometry::*; -pub(crate) use import::{import_foreign, send_to_engine as send_import_to_engine, ZOO_COORD_SYSTEM}; +pub(crate) use import::{ + import_foreign, send_to_engine as send_import_to_engine, PreImportedGeometry, ZOO_COORD_SYSTEM, +}; use indexmap::IndexMap; pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen}; use kcmc::{ @@ -26,15 +28,15 @@ pub use state::{ExecState, IdGenerator, MetaSettings}; use crate::{ engine::EngineManager, - errors::{KclError, KclErrorDetails}, + errors::KclError, execution::{ artifact::build_artifact_graph, cache::{CacheInformation, CacheResult}, }, - fs::{FileManager, FileSystem}, - parsing::ast::types::{Expr, FunctionExpression, ImportPath, Node, NodeRef, Program}, + fs::FileManager, + parsing::ast::types::{Expr, FunctionExpression, Node, NodeRef, Program}, settings::types::UnitLength, - source_range::{ModuleId, SourceRange}, + source_range::SourceRange, std::{args::Arg, StdLib}, ExecError, KclErrorWithOutputs, }; @@ -162,118 +164,6 @@ pub enum BodyType { Block, } -/// Info about a module. Right now, this is pretty minimal. We hope to cache -/// modules here in the future. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct ModuleInfo { - /// The ID of the module. - id: ModuleId, - /// Absolute path of the module's source file. - 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 { - Root, - Kcl(Node), - 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)] @@ -884,7 +774,7 @@ mod tests { use pretty_assertions::assert_eq; use super::*; - use crate::errors::KclErrorDetails; + use crate::{errors::KclErrorDetails, ModuleId}; /// Convenience function to get a JSON value from memory and unwrap. #[track_caller] diff --git a/src/wasm-lib/kcl/src/execution/state.rs b/src/wasm-lib/kcl/src/execution/state.rs index d6585697f..7260c96d6 100644 --- a/src/wasm-lib/kcl/src/execution/state.rs +++ b/src/wasm-lib/kcl/src/execution/state.rs @@ -9,11 +9,11 @@ use crate::{ errors::{KclError, KclErrorDetails}, execution::{ annotations, kcl_value, Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, ExecOutcome, ExecutorSettings, - KclValue, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr, Operation, ProgramMemory, SolidLazyIds, UnitAngle, - UnitLen, + KclValue, Operation, ProgramMemory, SolidLazyIds, UnitAngle, UnitLen, }, + modules::{ModuleId, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr}, parsing::ast::types::NonCodeValue, - source_range::{ModuleId, SourceRange}, + source_range::SourceRange, }; /// State for executing a program. diff --git a/src/wasm-lib/kcl/src/lib.rs b/src/wasm-lib/kcl/src/lib.rs index e7df6e916..f283787d1 100644 --- a/src/wasm-lib/kcl/src/lib.rs +++ b/src/wasm-lib/kcl/src/lib.rs @@ -65,6 +65,7 @@ mod fs; pub mod lint; mod log; mod lsp; +mod modules; mod parsing; mod settings; #[cfg(test)] @@ -87,9 +88,10 @@ pub use lsp::{ copilot::Backend as CopilotLspBackend, kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand}, }; +pub use modules::ModuleId; pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions}; pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength}; -pub use source_range::{ModuleId, SourceRange}; +pub use source_range::SourceRange; // Rather than make executor public and make lots of it pub(crate), just re-export into a new module. // Ideally we wouldn't export these things at all, they should only be used for testing. diff --git a/src/wasm-lib/kcl/src/modules.rs b/src/wasm-lib/kcl/src/modules.rs new file mode 100644 index 000000000..2b2035cb1 --- /dev/null +++ b/src/wasm-lib/kcl/src/modules.rs @@ -0,0 +1,157 @@ +use std::{fmt, path::PathBuf}; + +use anyhow::Result; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ + errors::{KclError, KclErrorDetails}, + execution::PreImportedGeometry, + fs::{FileManager, FileSystem}, + parsing::ast::types::{ImportPath, Node, Program}, + source_range::SourceRange, +}; + +/// Identifier of a source file. Uses a u32 to keep the size small. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, ts_rs::TS, JsonSchema)] +#[ts(export)] +pub struct ModuleId(u32); + +impl ModuleId { + pub fn from_usize(id: usize) -> Self { + Self(u32::try_from(id).expect("module ID should fit in a u32")) + } + + pub fn as_usize(&self) -> usize { + usize::try_from(self.0).expect("module ID should fit in a usize") + } + + /// Top-level file is the one being executed. + /// Represented by module ID of 0, i.e. the default value. + pub fn is_top_level(&self) -> bool { + *self == Self::default() + } +} + +#[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); + } + } +} + +pub(crate) fn read_std(_mod_name: &str) -> Option<&'static str> { + None +} + +/// Info about a module. Right now, this is pretty minimal. We hope to cache +/// modules here in the future. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct ModuleInfo { + /// The ID of the module. + pub(crate) id: ModuleId, + /// Absolute path of the module's source file. + pub(crate) path: ModulePath, + pub(crate) repr: ModuleRepr, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub enum ModuleRepr { + Root, + Kcl(Node), + Foreign(PreImportedGeometry), +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)] +pub enum ModulePath { + Local(PathBuf), + Std(String), +} + +impl ModulePath { + pub(crate) fn expect_path(&self) -> &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(name) => read_std(name) + .ok_or_else(|| { + KclError::Semantic(KclErrorDetails { + message: format!("Cannot find standard library module to import: std::{name}."), + source_ranges: vec![source_range], + }) + }) + .map(str::to_owned), + } + } + + 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}"), + } + } +} diff --git a/src/wasm-lib/kcl/src/parsing/ast/mod.rs b/src/wasm-lib/kcl/src/parsing/ast/mod.rs index 0d4248d5d..a4e74fc00 100644 --- a/src/wasm-lib/kcl/src/parsing/ast/mod.rs +++ b/src/wasm-lib/kcl/src/parsing/ast/mod.rs @@ -4,7 +4,7 @@ pub mod types; use crate::{ parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject}, - source_range::ModuleId, + ModuleId, }; impl BodyItem { diff --git a/src/wasm-lib/kcl/src/parsing/ast/modify.rs b/src/wasm-lib/kcl/src/parsing/ast/modify.rs index 0f7090237..d176d1eea 100644 --- a/src/wasm-lib/kcl/src/parsing/ast/modify.rs +++ b/src/wasm-lib/kcl/src/parsing/ast/modify.rs @@ -15,8 +15,8 @@ use crate::{ ArrayExpression, CallExpression, ConstraintLevel, FormatOptions, Literal, Node, PipeExpression, PipeSubstitution, VariableDeclarator, }, - source_range::{ModuleId, SourceRange}, - Program, + source_range::SourceRange, + ModuleId, Program, }; type Point3d = kcmc::shared::Point3d; 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 b4b838891..329a4a701 100644 --- a/src/wasm-lib/kcl/src/parsing/ast/types/mod.rs +++ b/src/wasm-lib/kcl/src/parsing/ast/types/mod.rs @@ -28,7 +28,8 @@ use crate::{ execution::{annotations, KclValue, Metadata, TagIdentifier}, parsing::{ast::digest::Digest, PIPE_OPERATOR}, pretty::NumericSuffix, - source_range::{ModuleId, SourceRange}, + source_range::SourceRange, + ModuleId, }; mod condition; diff --git a/src/wasm-lib/kcl/src/parsing/math.rs b/src/wasm-lib/kcl/src/parsing/math.rs index acf665045..e069cadb8 100644 --- a/src/wasm-lib/kcl/src/parsing/math.rs +++ b/src/wasm-lib/kcl/src/parsing/math.rs @@ -131,7 +131,7 @@ mod tests { ast::types::{Literal, LiteralValue}, token::NumericSuffix, }, - source_range::ModuleId, + ModuleId, }; #[test] diff --git a/src/wasm-lib/kcl/src/parsing/mod.rs b/src/wasm-lib/kcl/src/parsing/mod.rs index 93ba33aac..1e6db35d4 100644 --- a/src/wasm-lib/kcl/src/parsing/mod.rs +++ b/src/wasm-lib/kcl/src/parsing/mod.rs @@ -4,7 +4,8 @@ use crate::{ ast::types::{Node, Program}, token::TokenStream, }, - source_range::{ModuleId, SourceRange}, + source_range::SourceRange, + ModuleId, }; pub(crate) mod ast; diff --git a/src/wasm-lib/kcl/src/parsing/token/mod.rs b/src/wasm-lib/kcl/src/parsing/token/mod.rs index b38bf8d0f..154b5e0eb 100644 --- a/src/wasm-lib/kcl/src/parsing/token/mod.rs +++ b/src/wasm-lib/kcl/src/parsing/token/mod.rs @@ -18,8 +18,8 @@ use winnow::{ use crate::{ errors::KclError, parsing::ast::types::{ItemVisibility, VariableKind}, - source_range::{ModuleId, SourceRange}, - CompilationError, + source_range::SourceRange, + CompilationError, ModuleId, }; mod tokeniser; diff --git a/src/wasm-lib/kcl/src/parsing/token/tokeniser.rs b/src/wasm-lib/kcl/src/parsing/token/tokeniser.rs index 3b47190c4..948aadb1c 100644 --- a/src/wasm-lib/kcl/src/parsing/token/tokeniser.rs +++ b/src/wasm-lib/kcl/src/parsing/token/tokeniser.rs @@ -13,7 +13,7 @@ use winnow::{ use super::TokenStream; use crate::{ parsing::token::{Token, TokenType}, - source_range::ModuleId, + ModuleId, }; lazy_static! { diff --git a/src/wasm-lib/kcl/src/simulation_tests.rs b/src/wasm-lib/kcl/src/simulation_tests.rs index 6ed3d276d..791ad342a 100644 --- a/src/wasm-lib/kcl/src/simulation_tests.rs +++ b/src/wasm-lib/kcl/src/simulation_tests.rs @@ -7,7 +7,7 @@ use crate::{ exec::ArtifactCommand, execution::{ArtifactGraph, Operation}, parsing::ast::types::{Node, Program}, - source_range::ModuleId, + ModuleId, }; /// Deserialize the data from a snapshot. diff --git a/src/wasm-lib/kcl/src/source_range.rs b/src/wasm-lib/kcl/src/source_range.rs index 7f150b338..4491536ac 100644 --- a/src/wasm-lib/kcl/src/source_range.rs +++ b/src/wasm-lib/kcl/src/source_range.rs @@ -2,26 +2,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange}; -/// Identifier of a source file. Uses a u32 to keep the size small. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, ts_rs::TS, JsonSchema)] -#[ts(export)] -pub struct ModuleId(u32); - -impl ModuleId { - pub fn from_usize(id: usize) -> Self { - Self(u32::try_from(id).expect("module ID should fit in a u32")) - } - - pub fn as_usize(&self) -> usize { - usize::try_from(self.0).expect("module ID should fit in a usize") - } - - /// Top-level file is the one being executed. - /// Represented by module ID of 0, i.e. the default value. - pub fn is_top_level(&self) -> bool { - *self == Self::default() - } -} +use crate::modules::ModuleId; /// The first two items are the start and end points (byte offsets from the start of the file). /// The third item is whether the source range belongs to the 'main' file, i.e., the file currently diff --git a/src/wasm-lib/kcl/src/unparser.rs b/src/wasm-lib/kcl/src/unparser.rs index d5f5be3b6..70017a224 100644 --- a/src/wasm-lib/kcl/src/unparser.rs +++ b/src/wasm-lib/kcl/src/unparser.rs @@ -795,7 +795,7 @@ mod tests { use pretty_assertions::assert_eq; use super::*; - use crate::{parsing::ast::types::FormatOptions, source_range::ModuleId}; + use crate::{parsing::ast::types::FormatOptions, ModuleId}; #[test] fn test_recast_if_else_if_same() { diff --git a/src/wasm-lib/src/main.rs b/src/wasm-lib/src/main.rs new file mode 100644 index 000000000..43924339a --- /dev/null +++ b/src/wasm-lib/src/main.rs @@ -0,0 +1,41 @@ +use std::{env, fs::File, io::Read}; + +use kcl_lib::{ExecState, ExecutorContext, ExecutorSettings, Program}; + +// An extremely simple script, definitely not to be released or used for anything important, but +// sometimes useful for debugging. It reads in a file specified on the command line and runs it. +// It will report any errors in a developer-oriented way and discard the result. +// +// e.g., `cargo run -- foo.kcl` +#[tokio::main] +async fn main() { + let mut args = env::args(); + args.next(); + let filename = args.next().unwrap_or_else(|| "main.kcl".to_owned()); + + let mut f = File::open(&filename).unwrap(); + let mut text = String::new(); + f.read_to_string(&mut text).unwrap(); + + let (program, errs) = Program::parse(&text).unwrap(); + if !errs.is_empty() { + for e in errs { + eprintln!("{e:#?}"); + } + } + let program = program.unwrap(); + + let project_directory = filename.rfind('/').map(|i| filename[..i].into()); + let ctx = ExecutorContext::new_with_client( + ExecutorSettings { + project_directory, + ..Default::default() + }, + None, + None, + ) + .await + .unwrap(); + let mut exec_state = ExecState::new(&ctx.settings); + ctx.run(&program, &mut exec_state).await.unwrap(); +}