diff --git a/Cargo.lock b/Cargo.lock index c05ebf4..dadab0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,7 +227,7 @@ dependencies = [ [[package]] name = "erg_common" version = "0.6.0-beta.3" -source = "git+https://github.com/erg-lang/erg?branch=main#da2ff544f9cc4f6b1f875c24e513599d5d7fc4f7" +source = "git+https://github.com/erg-lang/erg?branch=main#9c83663c52479420dc9df4c46a780820f1364fda" dependencies = [ "hermit-abi", "libc", @@ -237,7 +237,7 @@ dependencies = [ [[package]] name = "erg_compiler" version = "0.6.0-beta.3" -source = "git+https://github.com/erg-lang/erg?branch=main#da2ff544f9cc4f6b1f875c24e513599d5d7fc4f7" +source = "git+https://github.com/erg-lang/erg?branch=main#9c83663c52479420dc9df4c46a780820f1364fda" dependencies = [ "erg_common", "erg_parser", @@ -246,7 +246,7 @@ dependencies = [ [[package]] name = "erg_parser" version = "0.6.0-beta.3" -source = "git+https://github.com/erg-lang/erg?branch=main#da2ff544f9cc4f6b1f875c24e513599d5d7fc4f7" +source = "git+https://github.com/erg-lang/erg?branch=main#9c83663c52479420dc9df4c46a780820f1364fda" dependencies = [ "erg_common", "unicode-xid 0.2.4", diff --git a/Cargo.toml b/Cargo.toml index 3255cca..0e1e303 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,6 @@ erg_compiler = { git = "https://github.com/erg-lang/erg", branch = "main" } erg_common = { git = "https://github.com/erg-lang/erg", branch = "main" } els = { git = "https://github.com/erg-lang/erg-language-server", branch = "main" } py2erg = { path = "./crates/py2erg" } + +[lib] +path = "src/lib.rs" diff --git a/crates/py2erg/convert.rs b/crates/py2erg/convert.rs index 5288fc4..42a5b8c 100644 --- a/crates/py2erg/convert.rs +++ b/crates/py2erg/convert.rs @@ -5,6 +5,7 @@ use erg_compiler::artifact::CompleteArtifact; use rustpython_parser::ast::{StatementType, ExpressionType, Located, Program, Number, StringGroup, Operator, BooleanOperator, UnaryOperator, Suite, Parameters, Parameter, Comparison}; use rustpython_parser::ast::Location as PyLocation; +use erg_common::set; use erg_common::set::Set as HashSet; use erg_common::dict::Dict as HashMap; use erg_compiler::erg_parser::token::{Token, TokenKind, EQUAL, COLON, DOT}; @@ -101,57 +102,63 @@ impl NameInfo { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ShadowingMode { + Invisible, + Visible, +} + #[derive(Debug)] pub struct ASTConverter { cfg: ErgConfig, + shadowing: ShadowingMode, namespace: Vec, /// Erg does not allow variables to be defined multiple times, so rename them using this - names: Vec>, + names: HashMap, warns: CompileErrors, } impl ASTConverter { - pub fn new(cfg: ErgConfig) -> Self { + pub fn new(cfg: ErgConfig, shadowing: ShadowingMode) -> Self { Self { + shadowing, cfg, namespace: vec![String::from("")], - names: vec![HashMap::new()], + names: HashMap::new(), warns: CompileErrors::empty(), } } - fn get_name_global(&self, name: &str) -> Option<&NameInfo> { - for space in self.names.iter().rev() { - if let Some(name) = space.get(name) { - return Some(name); - } - } - None + fn get_name(&self, name: &str) -> Option<&NameInfo> { + self.names.get(name) } - fn get_mut_name_global(&mut self, name: &str) -> Option<&mut NameInfo> { - for space in self.names.iter_mut().rev() { - if let Some(name) = space.get_mut(name) { - return Some(name); - } - } - None + fn get_mut_name(&mut self, name: &str) -> Option<&mut NameInfo> { + self.names.get_mut(name) } fn convert_ident(&mut self, name: String, loc: PyLocation) -> Identifier { + let shadowing = self.shadowing; let referrer = self.namespace.last().unwrap().clone(); let name = escape_name(name); - let cont = if let Some(name_info) = self.get_mut_name_global(&name) { + let cont = if let Some(name_info) = self.get_mut_name(&name) { if referrer != "" { name_info.add_referrer(referrer); } if name_info.defined_times > 1 { - // HACK: add zero-width characters as postfix - format!("{name}{}", "\0".repeat(name_info.defined_times)) + if shadowing == ShadowingMode::Invisible { + // HACK: add zero-width characters as postfix + format!("{name}{}", "\0".repeat(name_info.defined_times)) + } else { + format!("{name}__{}", name_info.defined_times) + } } else { name } } else { + let mut info = NameInfo::new(0); + info.add_referrer(referrer); + self.names.insert(name.clone(), info); name }; let token = Token::new(TokenKind::Symbol, cont, loc.row(), loc.column() - 1); @@ -542,11 +549,11 @@ impl ASTConverter { } fn register_name_info(&mut self, name: &str) { - if let Some(name_info) = self.names.last_mut().unwrap().get_mut(name) { + if let Some(name_info) = self.names.get_mut(name) { // name_info.last_defined_line = loc.row(); name_info.defined_times += 1; } else { - self.names.last_mut().unwrap().insert(String::from(name), NameInfo::new(1)); + self.names.insert(String::from(name), NameInfo::new(1)); } } @@ -686,10 +693,9 @@ impl ASTConverter { returns } => { // if reassigning of a function referenced by other functions is occurred, it is an error - if self.get_name_global(&name).map(|info| !info.referenced.is_empty()).unwrap_or(false) { + if self.get_name(&name).map(|info| info.defined_times > 0 && !info.referenced.difference(&set!{name.clone()}).is_empty()).unwrap_or(false) { let warn = reassign_func_error( self.cfg.input.clone(), - line!() as usize, pyloc_to_ergloc(stmt.location, name.len()), self.namespace.join("."), &name diff --git a/src/analyze.rs b/src/analyze.rs index 81af108..e64a1f0 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -6,6 +6,7 @@ use erg_compiler::context::Context; use erg_compiler::erg_parser::ast::AST; use erg_compiler::error::{CompileErrors, CompileError}; use erg_compiler::lower::ASTLowerer; +use py2erg::ShadowingMode; use py2erg::dump_decl_er; use rustpython_parser::parser; @@ -68,6 +69,10 @@ impl Buildable for PythonAnalyzer { impl BuildRunnable for PythonAnalyzer {} impl PythonAnalyzer { + pub fn new(cfg: ErgConfig) -> Self { + Runnable::new(cfg) + } + pub fn analyze(&mut self, py_code: String, mode: &str) -> Result { let filename = self.cfg.input.filename(); let py_program = parser::parse_program(&py_code).map_err(|err| { @@ -81,16 +86,22 @@ impl PythonAnalyzer { let err = CompileError::new(core, self.cfg.input.clone(), "".into()); IncompleteArtifact::new(None, CompileErrors::from(err), CompileErrors::empty()) })?; - let converter = py2erg::ASTConverter::new(self.cfg.copy()); + let converter = py2erg::ASTConverter::new(self.cfg.copy(), ShadowingMode::Invisible); let CompleteArtifact{ object: erg_module, mut warns } = converter.convert_program(py_program); let erg_ast = AST::new(erg_common::Str::rc(filename), erg_module); erg_common::log!("AST: {erg_ast}"); - self.checker.lower(erg_ast, mode).map_err(|iart| { - let errors = handle_err::filter_errors(self.checker.get_mod_ctx(), iart.errors); - let ws = handle_err::filter_errors(self.checker.get_mod_ctx(), iart.warns); - warns.extend(ws); - IncompleteArtifact::new(iart.object, errors, warns) - }) + match self.checker.lower(erg_ast, mode) { + Ok(mut artifact) => { + artifact.warns.extend(warns); + Ok(artifact) + } + Err(iart) => { + let errors = handle_err::filter_errors(self.checker.get_mod_ctx(), iart.errors); + let ws = handle_err::filter_errors(self.checker.get_mod_ctx(), iart.warns); + warns.extend(ws); + Err(IncompleteArtifact::new(iart.object, errors, warns)) + } + } } pub fn run(&mut self) { diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..98d6da8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +mod analyze; +mod handle_err; + +pub use analyze::PythonAnalyzer; diff --git a/src/main.rs b/src/main.rs index b9b8830..c0fd311 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,6 @@ use analyze::PythonAnalyzer; use els::Server; use erg_common::config::{Input, ErgConfig}; use erg_common::spawn::exec_new_thread; -use erg_common::traits::Runnable; pub fn parse_args() -> ErgConfig { let mut args = env::args(); diff --git a/tests/e0001.py b/tests/e0001.py new file mode 100644 index 0000000..5987f77 --- /dev/null +++ b/tests/e0001.py @@ -0,0 +1,9 @@ +def a(): return 1 +def a(): return "a" # OK + +print(a()) + +def g(): return f() + +def f(): return 1 +def f(): return "a" # E0001: Reassignment of a function referenced by other functions diff --git a/tests/import.py b/tests/import.py index 90086f3..c86667d 100644 --- a/tests/import.py +++ b/tests/import.py @@ -8,4 +8,4 @@ rdi(0, 1, 2) # ERR print(export.test) print(export.add(1, 2)) -assert export.add("a", "b") == 1 +assert export.add("a", "b") == 1 # ERR diff --git a/tests/test.py b/tests/test.py index c77ca31..c440ca3 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,30 +1,32 @@ + def add(x, y): return x + y print(add(1, 2)) -print(add(1, "a")) -add.x = 1 +print(add(1, "a")) # ERR +add.x = 1 # ERR -def add2(x: int, y: int) -> str: +def add2(x: int, y: int) -> str: # ERR return x + y print(add2(1, 2)) +# ERR for i in [1, 2, 3]: j = i + "aa" print(j) -while "aaa": +while "aaa": # ERR print("invalid") break class C: - x = 1 + "a" + x = 1 + "a" # ERR dic = {"a": 1, "b": 2} -print(dic["c"]) +print(dic["c"]) # ERR a = [1, 2, 3] -print(a[4]) +print(a[4]) # ERR -a_ = "aa" if True else "bb" \ No newline at end of file +a_ = "aa" if True else "bb" diff --git a/tests/test.rs b/tests/test.rs new file mode 100644 index 0000000..ec6f0d6 --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,51 @@ +use std::path::PathBuf; + +use erg_common::traits::Stream; +use erg_common::config::{ErgConfig, Input}; +use erg_compiler::artifact::{IncompleteArtifact, CompleteArtifact}; +use pylyzer::PythonAnalyzer; + +pub fn exec_analyzer(file_path: &'static str) -> Result { + let cfg = ErgConfig { python_compatible_mode: true, input: Input::File(PathBuf::from(file_path)), ..Default::default() }; + let mut analyzer = PythonAnalyzer::new(cfg); + let py_code = analyzer.cfg.input.read(); + analyzer.analyze(py_code, "exec") +} + +pub fn expect(file_path: &'static str, warns: usize, errors: usize) { + match exec_analyzer(file_path) { + Ok(artifact) => { + assert_eq!(artifact.warns.len(), warns); + assert_eq!(errors, 0); + } + Err(artifact) => { + assert_eq!(artifact.warns.len(), warns); + assert_eq!(artifact.errors.len(), errors); + } + } +} + +#[test] +fn exec_test() { + expect("tests/test.py", 0, 9); +} + +#[test] +fn exec_import() { + expect("tests/import.py", 0, 2); +} + +#[test] +fn exec_export() { + expect("tests/export.py", 0, 0); +} + +#[test] +fn exec_class() { + expect("tests/class.py", 0, 1); +} + +#[test] +fn exec_e0001() { + expect("tests/e0001.py", 1, 0); +}