Factor out a modules module and add main.rs (#5329)

* Factor out a modules module

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Very simple cargo run

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2025-02-11 13:52:46 +13:00
committed by GitHub
parent 9c18060d73
commit 0e9d37c0a0
17 changed files with 233 additions and 158 deletions

View File

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

View File

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

View File

@ -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<String, KclError> {
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<PathBuf>) -> 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<Program>),
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<PathBuf>,
}
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::<Vec<_>>()
.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]

View File

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

View File

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

View File

@ -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<PathBuf>,
}
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::<Vec<_>>()
.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<Program>),
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<String, KclError> {
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<PathBuf>) -> 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}"),
}
}
}

View File

@ -4,7 +4,7 @@ pub mod types;
use crate::{
parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject},
source_range::ModuleId,
ModuleId,
};
impl BodyItem {

View File

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

View File

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

View File

@ -131,7 +131,7 @@ mod tests {
ast::types::{Literal, LiteralValue},
token::NumericSuffix,
},
source_range::ModuleId,
ModuleId,
};
#[test]

View File

@ -4,7 +4,8 @@ use crate::{
ast::types::{Node, Program},
token::TokenStream,
},
source_range::{ModuleId, SourceRange},
source_range::SourceRange,
ModuleId,
};
pub(crate) mod ast;

View File

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

View File

@ -13,7 +13,7 @@ use winnow::{
use super::TokenStream;
use crate::{
parsing::token::{Token, TokenType},
source_range::ModuleId,
ModuleId,
};
lazy_static! {

View File

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

View File

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

View File

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

41
src/wasm-lib/src/main.rs Normal file
View File

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