diff --git a/.gitignore b/.gitignore index 6062a4be..22a0c347 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ Cargo.lock *.pdb typings/ +node_modules +out/ +client/dist/ diff --git a/Cargo.toml b/Cargo.toml index 6c8bf18c..c85db9d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,16 @@ [workspace] -members = ["parser", "cli", "typechecker", "ruff_python_import_resolver", "lsp"] +members = ["parser", "enderpy", "typechecker", "ruff_python_import_resolver", "lsp"] resolver = "2" +[workspace.package] +edition = "2021" +rust-version = "1.72.0" +homepage = "https://github.com/Glyphack/enderpy" +documentation = "https://github.com/Glyphack/enderpy" +authors = ["Shaygan Hooshyari"] +license = "APGL-3.0" +repository = "https://github.com/Glyphack/enderpy" + [workspace.dependencies] log = { version = "0.4.17" } serde = { version = "1.0.152", features = ["derive"] } @@ -10,6 +19,5 @@ insta = { version = "1.31.0", feature = ["filters", "glob"] } [profile.dev.package.insta] opt-level = 3 - [profile.dev.package.similar] opt-level = 3 diff --git a/enderpy/Cargo.toml b/enderpy/Cargo.toml new file mode 100644 index 00000000..fccc7615 --- /dev/null +++ b/enderpy/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "enderpy" +description = "A typechecker for Python" +authors = ["Shaygan Hooshyari"] +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.71" +clap = { version = "4.2.7", features = ["derive"] } +parser = { path = "../parser" , version = "0.1.0" } +typechecker = { path = "../typechecker" , version = "0.1.0" } diff --git a/enderpy/src/cli.rs b/enderpy/src/cli.rs new file mode 100644 index 00000000..e4dd8c3e --- /dev/null +++ b/enderpy/src/cli.rs @@ -0,0 +1,38 @@ +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; + +/// Enderpy CLI +#[derive(Parser)] +#[command(name = "Enderpy", author, version, about)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Print lexer tokens + Tokenize { + /// Path to source file + file: PathBuf, + }, + /// Print abstract syntax tree + Parse { + /// Path to source file + file: PathBuf, + }, + /// Type check + Check { path: PathBuf }, + /// Symbol table + Symbols { path: PathBuf }, + + /// Watch changes to type check + Watch, +} + +#[test] +fn verify_cli() { + use clap::CommandFactory; + Cli::command().debug_assert() +} diff --git a/enderpy/src/main.rs b/enderpy/src/main.rs new file mode 100644 index 00000000..caefeac3 --- /dev/null +++ b/enderpy/src/main.rs @@ -0,0 +1,105 @@ +use anyhow::{anyhow, Result, bail}; +use clap::Parser as ClapParser; +use cli::{Cli, Commands}; +use parser::{token, Lexer, Parser}; +use std::{fs, path::PathBuf}; +use typechecker::{ + build::{BuildManager, BuildSource}, + settings::{ImportDiscovery, Settings}, project::find_project_root, +}; + +mod cli; + +fn main() -> Result<()> { + let cli = Cli::parse(); + + match &cli.command { + Commands::Tokenize { file } => tokenize(file), + Commands::Parse { file } => parse(file), + Commands::Check { path } => check(path), + Commands::Watch => watch(), + Commands::Symbols { path } => symbols(path), + } +} + +fn symbols(path: &PathBuf) -> std::result::Result<(), anyhow::Error> { + let source = fs::read_to_string(path)?; + let initial_source = BuildSource { + path: path.to_owned(), + module: String::from("test"), + source, + followed: false, + }; + let dir_of_path = path.parent().unwrap(); + let python_executable = Some( get_python_executable()? ); + let settings = Settings { debug: true, root: dir_of_path.to_path_buf(), import_discovery: ImportDiscovery { python_executable } }; + + let mut manager = BuildManager::new(vec![initial_source], settings); + manager.build(); + + for (name, module) in manager.modules.iter() { + println!("{}", name); + println!("{}", module.get_symbol_table()); + } + + Ok(()) +} + +fn get_python_executable() -> Result { + let output = std::process::Command::new("python") + .arg("-c") + .arg("import sys; print(sys.executable)") + .output()?; + let path = String::from_utf8(output.stdout)?; + Ok(PathBuf::from(path)) +} + +fn tokenize(file: &PathBuf) -> Result<()> { + let source = fs::read_to_string(file)?; + let mut lexer = Lexer::new(&source); + let mut tokens = Vec::new(); + while let Ok(token) = lexer.next_token() { + tokens.push(token.clone()); + if token.kind == token::Kind::Eof { + break; + } + } + println!("{:#?}", tokens); + Ok(()) +} + +fn parse(file: &PathBuf) -> Result<()> { + let source = fs::read_to_string(file)?; + let mut parser = Parser::new(source); + let ast = parser.parse(); + println!("{:#?}", ast); + Ok(()) +} + +fn check(path: &PathBuf) -> Result<()> { + if path.is_dir() { + return bail!("Path must be a file"); + } + let source = fs::read_to_string(path)?; + let initial_source = BuildSource { + path: path.to_owned(), + module: String::from("test"), + source, + followed: false, + }; + let root = find_project_root(path); + let python_executable = Some( get_python_executable()? ); + let settings = Settings { debug: true, root: PathBuf::from(root), import_discovery: ImportDiscovery { python_executable } }; + let mut build_manager = BuildManager::new(vec![initial_source], settings); + build_manager.type_check(); + + for err in build_manager.get_errors() { + println!("{:#?}", err); + } + + Ok(()) +} + +fn watch() -> Result<()> { + todo!() +} diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 125b8713..9654b08c 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,7 +1,15 @@ [package] -name = "parser" +name = "enderpy-python-parser" +description = "A Python parser written in Rust" version = "0.1.0" -edition = "2021" +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +license = { workspace = true } +readme = "../../README.md" [dependencies] serde = { version = "1.0", features = ["derive"] } diff --git a/typechecker/Cargo.toml b/typechecker/Cargo.toml index 87816bc7..f61cbde8 100644 --- a/typechecker/Cargo.toml +++ b/typechecker/Cargo.toml @@ -10,6 +10,8 @@ config = "0.13.3" serde = { version = "1.0.164", features = ["derive"] } miette = "5.10.0" thiserror = "1.0.48" +log.workspace = true +env_logger = "0.10.0" [dev-dependencies] insta = { version = "1.28.0", features = ["yaml"] } diff --git a/typechecker/src/build.rs b/typechecker/src/build.rs index ec902ca2..e2a0702a 100644 --- a/typechecker/src/build.rs +++ b/typechecker/src/build.rs @@ -1,4 +1,5 @@ use std::{collections::HashMap, path::PathBuf}; +use log::info; use parser::Parser; use ruff_python_resolver::config::Config; @@ -21,9 +22,17 @@ pub struct BuildSource { pub followed: bool, } +#[derive(Debug, Clone)] +pub struct BuildError { + pub msg: String, + pub line: u32, + pub start: u32, + pub end: u32, +} + #[derive(Debug)] pub struct BuildManager { - errors: Vec, + errors: Vec, pub modules: HashMap, options: Settings, } @@ -64,7 +73,7 @@ impl BuildManager { self.modules.insert(module, State::new(file)); } - pub fn get_errors(&self) -> Vec { + pub fn get_errors(&self) -> Vec { self.errors.clone() } @@ -81,7 +90,7 @@ impl BuildManager { pub fn get_module_name(path: &PathBuf) -> String { path.to_str() - .unwrap() + .unwrap_or_default() .replace("/", ".") .replace("\\", ".") } @@ -89,12 +98,12 @@ impl BuildManager { // Entry point to analyze the program pub fn build(&mut self) { let files = self.modules.values().collect(); + info!("files: {:#?}", files); let new_files = self.gather_files(files); for file in new_files { self.modules.insert(file.file.module_name.clone(), file); } - println!("modules: {:#?}", self.modules.keys().collect::>()); self.pre_analysis(); } @@ -116,8 +125,12 @@ impl BuildManager { } for error in checker.errors { let line = get_line_number_of_character_position(&state.1.file.source, error.start); - let error = format!("{} line {}: {}", state.0, line, error.msg); - self.errors.push(error); + self.errors.push(BuildError { + msg: error.msg, + line: line as u32, + start: error.start as u32, + end: error.end as u32, + }); } } } diff --git a/typechecker/src/lib.rs b/typechecker/src/lib.rs index 8afe2e9c..1d6f5ea6 100644 --- a/typechecker/src/lib.rs +++ b/typechecker/src/lib.rs @@ -10,6 +10,7 @@ mod type_check; pub mod build; pub mod semantic_analyzer; pub mod settings; +pub mod project; pub use parser::ast; pub use ruff_python_resolver; diff --git a/typechecker/src/project.rs b/typechecker/src/project.rs new file mode 100644 index 00000000..00cf9098 --- /dev/null +++ b/typechecker/src/project.rs @@ -0,0 +1,18 @@ +use std::path::{Path, PathBuf}; +const PROJECT_ROOT_MARKERS: [&str; 2] = ["__init__.py", "pyproject.toml"]; + +pub fn find_project_root(path: &PathBuf) -> &Path { + let root = path + .ancestors() + .find(|p| PROJECT_ROOT_MARKERS.iter().any(|m| p.join(m).exists())); + match root { + Some(root) => root, + None => { + if path.is_dir() { + path + } else { + path.parent().unwrap_or(path) + } + } + } +}