diff --git a/crates/els/server.rs b/crates/els/server.rs index 191869e21..28b89fc6d 100644 --- a/crates/els/server.rs +++ b/crates/els/server.rs @@ -14,7 +14,9 @@ use erg_common::env::erg_path; use erg_common::pathutil::NormalizedPathBuf; use erg_common::shared::{MappedRwLockReadGuard, Shared}; use erg_common::spawn::{safe_yield, spawn_new_thread}; -use erg_common::{fn_name, normalize_path}; +use erg_common::traits::Stream; +use erg_common::vfs::VFS; +use erg_common::{fn_name, lsp_log, normalize_path}; use erg_compiler::artifact::BuildRunnable; use erg_compiler::build_package::PackageBuilder; @@ -233,6 +235,7 @@ impl Clone for Server { impl Server { pub fn new(cfg: ErgConfig, stdout_redirect: Option>) -> Self { let flags = Flags::default(); + let cfg = Self::register_packages(cfg); Self { comp_cache: CompletionCache::new(cfg.copy(), flags.clone()), shared: SharedCompilerResource::new(cfg.copy()), @@ -253,6 +256,68 @@ impl Server { } } + fn register_packages(mut cfg: ErgConfig) -> ErgConfig { + use erg_compiler::erg_parser::ast; + let home = normalize_path(std::env::current_dir().unwrap_or_default()); + let Ok(src) = VFS.read(home.join("package.lock.er")) else { + lsp_log!("package.lock.er not found"); + return cfg; + }; + let Ok(artifact) = ::parse(src) else { + lsp_log!("failed to parse package.lock.er"); + return cfg; + }; + let Some(pkgs) = artifact.ast.get_attr("packages") else { + lsp_log!("failed to get packages: {}", artifact.ast); + return cfg; + }; + let Some(ast::Expr::Array(ast::Array::Normal(arr))) = pkgs.body.block.first() else { + lsp_log!("packages must be an array: {pkgs}"); + return cfg; + }; + for rec in arr.iter() { + let ast::Expr::Record(rec) = rec else { + lsp_log!("packages must be records: {rec}"); + return cfg; + }; + let Some(ast::Expr::Literal(name)) = + rec.get("name").and_then(|name| name.body.block.first()) + else { + return cfg; + }; + let name = name.token.content.replace('\"', ""); + let Some(ast::Expr::Literal(as_name)) = rec + .get("as_name") + .and_then(|as_name| as_name.body.block.first()) + else { + return cfg; + }; + let as_name = as_name.token.content.replace('\"', ""); + let Some(ast::Expr::Literal(version)) = rec + .get("version") + .and_then(|version| version.body.block.first()) + else { + return cfg; + }; + let version = version.token.content.replace('\"', ""); + let path = rec.get("path").and_then(|path| { + if let Some(ast::Expr::Literal(lit)) = path.body.block.first() { + Some(lit.token.content.replace('\"', "")) + } else { + None + } + }); + let package = erg_common::config::Package::new( + name.leak(), + as_name.leak(), + version.leak(), + path.map(|p| &*p.leak()), + ); + cfg.packages.push(package); + } + cfg + } + pub fn run(&mut self) -> Result<(), Box> { loop { let msg = self.read_message()?; diff --git a/crates/erg_common/config.rs b/crates/erg_common/config.rs index 283c483e0..1407d2388 100644 --- a/crates/erg_common/config.rs +++ b/crates/erg_common/config.rs @@ -92,6 +92,30 @@ impl From<&str> for TranspileTarget { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Package { + pub name: &'static str, + pub as_name: &'static str, + pub version: &'static str, + pub path: Option<&'static str>, +} + +impl Package { + pub const fn new( + name: &'static str, + as_name: &'static str, + version: &'static str, + path: Option<&'static str>, + ) -> Self { + Self { + name, + as_name, + version, + path, + } + } +} + #[derive(Debug, Clone)] pub struct ErgConfig { pub mode: ErgMode, @@ -122,6 +146,7 @@ pub struct ErgConfig { pub ps1: &'static str, pub ps2: &'static str, pub runtime_args: Vec<&'static str>, + pub packages: Vec, } impl Default for ErgConfig { @@ -146,6 +171,7 @@ impl Default for ErgConfig { ps1: ">>> ", ps2: "... ", runtime_args: vec![], + packages: vec![], } } } @@ -267,6 +293,50 @@ impl ErgConfig { process::exit(1); }); } + "--use-package" => { + let name = args + .next() + .expect("`name` of `--use-package` is not passed") + .into_boxed_str(); + let as_name = args + .next() + .expect("`as_name` of `--use-package` is not passed") + .into_boxed_str(); + let version = args + .next() + .expect("`version` of `--use-package` is not passed") + .into_boxed_str(); + cfg.packages.push(Package::new( + Box::leak(name), + Box::leak(as_name), + Box::leak(version), + None, + )); + } + "--use-local-package" => { + let name = args + .next() + .expect("`name` of `--use-package` is not passed") + .into_boxed_str(); + let as_name = args + .next() + .expect("`as_name` of `--use-package` is not passed") + .into_boxed_str(); + let version = args + .next() + .expect("`version` of `--use-package` is not passed") + .into_boxed_str(); + let path = args + .next() + .expect("`path` of `--use-package` is not passed") + .into_boxed_str(); + cfg.packages.push(Package::new( + Box::leak(name), + Box::leak(as_name), + Box::leak(version), + Some(Box::leak(path)), + )); + } "--ping" => { println!("pong"); process::exit(0); diff --git a/crates/erg_common/env.rs b/crates/erg_common/env.rs index 6a6b7ea16..0eb940e0b 100644 --- a/crates/erg_common/env.rs +++ b/crates/erg_common/env.rs @@ -72,6 +72,16 @@ fn _erg_external_lib_path() -> PathBuf { fallback_erg_path().join("lib/external") }) } +fn _erg_pkgs_path() -> PathBuf { + _erg_path() + .join("lib") + .join("pkgs") + .canonicalize() + .unwrap_or_else(|_| { + eprintln!("{RED}[ERR] ERG_PATH/lib/pkgs not found {RESET}"); + fallback_erg_path().join("lib/pkgs") + }) +} fn _python_site_packages() -> impl Iterator { get_sys_path(None) .unwrap_or_default() @@ -91,6 +101,7 @@ pub static ERG_CORE_DECL_PATH: OnceLock = OnceLock::new(); pub static ERG_STD_PATH: OnceLock = OnceLock::new(); pub static ERG_PYSTD_PATH: OnceLock = OnceLock::new(); pub static ERG_EXTERNAL_LIB_PATH: OnceLock = OnceLock::new(); +pub static ERG_PKGS_PATH: OnceLock = OnceLock::new(); pub static PYTHON_SITE_PACKAGES: OnceLock> = OnceLock::new(); /// == `Path::new("~/.erg")` if ERG_PATH is not set @@ -123,6 +134,11 @@ pub fn erg_py_external_lib_path() -> &'static PathBuf { ERG_EXTERNAL_LIB_PATH.get_or_init(|| normalize_path(_erg_external_lib_path())) } +/// == `Path::new("~/.erg/lib/pkgs")` if ERG_PATH is not set +pub fn erg_pkgs_path() -> &'static PathBuf { + ERG_PKGS_PATH.get_or_init(|| normalize_path(_erg_pkgs_path())) +} + pub fn python_site_packages() -> &'static Vec { PYTHON_SITE_PACKAGES.get_or_init(|| _python_site_packages().collect()) } diff --git a/crates/erg_common/io.rs b/crates/erg_common/io.rs index 5d54c7ef1..e7aeb4d4f 100644 --- a/crates/erg_common/io.rs +++ b/crates/erg_common/io.rs @@ -5,9 +5,11 @@ use std::path::{Path, PathBuf}; use std::process; use std::process::Stdio; +use crate::config::ErgConfig; use crate::consts::EXPERIMENTAL_MODE; use crate::env::{ - erg_path, erg_py_external_lib_path, erg_pystd_path, erg_std_path, python_site_packages, + erg_path, erg_pkgs_path, erg_py_external_lib_path, erg_pystd_path, erg_std_path, + python_site_packages, }; use crate::pathutil::{add_postfix_foreach, remove_postfix}; use crate::python_util::get_sys_path; @@ -479,8 +481,8 @@ impl Input { )) } - pub fn resolve_path(&self, path: &Path) -> Option { - self.resolve_real_path(path) + pub fn resolve_path(&self, path: &Path, cfg: &ErgConfig) -> Option { + self.resolve_real_path(path, cfg) .or_else(|| self.resolve_decl_path(path)) } @@ -489,7 +491,8 @@ impl Input { /// 2. `./{path/to}/__init__.er` /// 3. `std/{path/to}.er` /// 4. `std/{path/to}/__init__.er` - pub fn resolve_real_path(&self, path: &Path) -> Option { + /// 5. `pkgs/{path/to}/lib.er` + pub fn resolve_real_path(&self, path: &Path, cfg: &ErgConfig) -> Option { if let Ok(path) = self.resolve_local(path) { Some(path) } else if let Ok(path) = erg_std_path() @@ -503,6 +506,8 @@ impl Input { .canonicalize() { Some(normalize_path(path)) + } else if let Some(pkg) = self.resolve_project_dep_path(path, cfg) { + Some(normalize_path(pkg)) } else if path == Path::new("unsound") { Some(PathBuf::from("unsound")) } else { @@ -510,6 +515,17 @@ impl Input { } } + fn resolve_project_dep_path(&self, path: &Path, cfg: &ErgConfig) -> Option { + let name = format!("{}", path.display()); + let pkg = cfg.packages.iter().find(|p| p.as_name == name)?; + let path = if let Some(path) = pkg.path { + PathBuf::from(path).canonicalize().ok()? + } else { + erg_pkgs_path().join(pkg.name).join(pkg.version) + }; + Some(path.join("src").join("lib.er")) + } + /// resolution order: /// 1. `{path/to}.d.er` /// 2. `{path/to}/__init__.d.er` diff --git a/crates/erg_common/str.rs b/crates/erg_common/str.rs index db2802bf0..b201ea782 100644 --- a/crates/erg_common/str.rs +++ b/crates/erg_common/str.rs @@ -191,6 +191,13 @@ impl Str { Str::Rc(s.into()) } + pub fn leak(self) -> &'static str { + match self { + Str::Rc(s) => Box::leak(s.into()), + Str::Static(s) => s, + } + } + pub fn into_rc(self) -> ArcStr { match self { Str::Rc(s) => s, diff --git a/crates/erg_compiler/build_package.rs b/crates/erg_compiler/build_package.rs index a02432ec6..12d6c09b0 100644 --- a/crates/erg_compiler/build_package.rs +++ b/crates/erg_compiler/build_package.rs @@ -492,7 +492,7 @@ impl return Ok(()); } let path = Path::new(&__name__[..]); - let import_path = match cfg.input.resolve_path(path) { + let import_path = match cfg.input.resolve_path(path, cfg) { Some(path) => path, None => { for _ in 0..600 { diff --git a/crates/erg_compiler/context/initialize/const_func.rs b/crates/erg_compiler/context/initialize/const_func.rs index f791950ca..15c9d7ff6 100644 --- a/crates/erg_compiler/context/initialize/const_func.rs +++ b/crates/erg_compiler/context/initialize/const_func.rs @@ -689,7 +689,7 @@ pub(crate) fn resolve_path_func(mut args: ValueArgs, ctx: &Context) -> EvalValue return Err(type_mismatch("Str", other, "Path")); } }; - let Some(path) = ctx.cfg.input.resolve_path(path) else { + let Some(path) = ctx.cfg.input.resolve_path(path, &ctx.cfg) else { return Err(ErrorCore::new( vec![SubMessage::only_loc(Location::Unknown)], format!("Path {} is not found", path.display()), diff --git a/crates/erg_compiler/context/inquire.rs b/crates/erg_compiler/context/inquire.rs index a0a9d67d5..59b81b3ea 100644 --- a/crates/erg_compiler/context/inquire.rs +++ b/crates/erg_compiler/context/inquire.rs @@ -3086,7 +3086,7 @@ impl Context { str_namespace.push_str(namespaces.remove(0)); } let path = Path::new(&str_namespace); - let mut path = self.cfg.input.resolve_path(path)?; + let mut path = self.cfg.input.resolve_path(path, &self.cfg)?; for p in namespaces.into_iter() { path = Input::try_push_path(path, Path::new(p)).ok()?; } diff --git a/crates/erg_compiler/context/register.rs b/crates/erg_compiler/context/register.rs index 9db9eb323..376ca72b0 100644 --- a/crates/erg_compiler/context/register.rs +++ b/crates/erg_compiler/context/register.rs @@ -2132,7 +2132,11 @@ impl Context { } fn import_erg_mod(&self, __name__: &Str, loc: &impl Locational) -> CompileResult { - let path = match self.cfg.input.resolve_real_path(Path::new(&__name__[..])) { + let path = match self + .cfg + .input + .resolve_real_path(Path::new(&__name__[..]), &self.cfg) + { Some(path) => path, None => { return Err(self.import_err(line!(), __name__, loc)); diff --git a/crates/erg_compiler/hir.rs b/crates/erg_compiler/hir.rs index 45e1f9b7c..fabc1164e 100644 --- a/crates/erg_compiler/hir.rs +++ b/crates/erg_compiler/hir.rs @@ -1209,6 +1209,10 @@ impl RecordAttrs { pub fn extend(&mut self, attrs: RecordAttrs) { self.0.extend(attrs.0); } + + pub fn get(&self, name: &str) -> Option<&Def> { + self.0.iter().find(|def| def.sig.ident().inspect() == name) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -1264,6 +1268,10 @@ impl Record { t.insert(Field::from(attr.sig.ident()), attr.body.block.t()); self.attrs.push(attr); } + + pub fn get(&self, name: &str) -> Option<&Def> { + self.attrs.get(name) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -2983,6 +2991,15 @@ impl Locational for Module { impl_stream!(Module, Expr); +impl Module { + pub fn get_attr(&self, name: &str) -> Option<&Def> { + self.0.iter().find_map(|e| match e { + Expr::Def(def) if def.sig.ident().inspect() == name => Some(def), + _ => None, + }) + } +} + /// High-level Intermediate Representation /// AST with type information added #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/crates/erg_parser/ast.rs b/crates/erg_parser/ast.rs index 070696438..20e995fc1 100644 --- a/crates/erg_parser/ast.rs +++ b/crates/erg_parser/ast.rs @@ -888,6 +888,10 @@ impl NormalArray { pub fn get(&self, index: usize) -> Option<&Expr> { self.elems.pos_args.get(index).map(|a| &a.expr) } + + pub fn iter(&self) -> impl Iterator { + self.elems.pos_args.iter().map(|a| &a.expr) + } } #[pyclass] @@ -1229,6 +1233,14 @@ impl From> for RecordAttrs { } } +impl RecordAttrs { + pub fn get(&self, name: &str) -> Option<&Def> { + self.0 + .iter() + .find(|attr| attr.sig.ident().is_some_and(|n| n.inspect() == name)) + } +} + #[pyclass(get_all, set_all)] #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct NormalRecord { @@ -1279,7 +1291,7 @@ impl From for NormalRecord { #[pymethods] impl NormalRecord { #[pyo3(name = "get")] - fn _get(&self, name: &str) -> Option { + fn _get(&self, name: &str) -> Option { self.get(name).cloned() } @@ -1299,15 +1311,8 @@ impl NormalRecord { } impl NormalRecord { - pub fn get(&self, name: &str) -> Option<&Expr> { - for attr in self.attrs.iter() { - if let Signature::Var(var) = &attr.sig { - if var.inspect().is_some_and(|n| n == name) { - return attr.body.block.last(); - } - } - } - None + pub fn get(&self, name: &str) -> Option<&Def> { + self.attrs.get(name) } pub fn iter(&self) -> impl Iterator { @@ -1379,6 +1384,16 @@ impl Record { Self::Mixed(mixed) => mixed.keys().collect(), } } + + pub fn get(&self, name: &str) -> Option<&Def> { + match self { + Self::Normal(normal) => normal.get(name), + Self::Mixed(mixed) => mixed.get(name).and_then(|attr| match attr { + RecordAttrOrIdent::Attr(attr) => Some(attr), + RecordAttrOrIdent::Ident(_) => None, + }), + } + } } /// Record can be defined with shorthend/normal mixed style, i.e. {x; y=expr; z; ...} @@ -6131,6 +6146,13 @@ impl Module { pub fn block(&self) -> &Block { &self.0 } + + pub fn get_attr(&self, name: &str) -> Option<&Def> { + self.0.iter().find_map(|e| match e { + Expr::Def(def) if def.sig.ident().is_some_and(|id| id.inspect() == name) => Some(def), + _ => None, + }) + } } #[pyclass(get_all, set_all)] diff --git a/doc/EN/tools/pack.md b/doc/EN/tools/pack.md index a80b9844d..06e5ed51f 100644 --- a/doc/EN/tools/pack.md +++ b/doc/EN/tools/pack.md @@ -103,10 +103,12 @@ dependency ::= name '=' package_name | name '=' '{' 'name' '=' package_name (';' 'version' '=' version_spec)? ';'? '}' | name '=' '{' 'git' '=' git_url ';'? '}' + | name '=' '{' 'path' '=' path ';'? '}' name ::= package_name ::= version_spec ::= git_url ::= +path ::= ``` `name` is the package name to be specified when importing, and by giving it a different name, you can also use a different version of the same dependency. @@ -117,6 +119,8 @@ git_url ::= `git` is specified when installing a package directly from a git repository without using the registry. `git_url` is the URL of the git repository. +`path` is specified when using a local package. + ### deprecated If the package is no longer maintained for some reason, specify `True`. diff --git a/doc/JA/tools/pack.md b/doc/JA/tools/pack.md index 8f2d896d9..9d5a19f58 100644 --- a/doc/JA/tools/pack.md +++ b/doc/JA/tools/pack.md @@ -105,14 +105,17 @@ dependency ::= name '=' package_name | name '=' '{' 'name' '=' package_name (';' 'version' '=' version_spec)? ';'? '}' | name '=' '{' 'git' '=' git_url ';'? '}' + | name '=' '{' 'path' '=' path ';'? '}' name ::= package_name ::= version_spec ::= git_url ::= +path ::= ``` `name`はimportする際に指定するパッケージ名であり、別名をつけることで同じ依存関係の別バージョンも利用できる。`package_name`はレジストリに登録されているパッケージの識別子である。`version_spec`はパッケージのバージョンであり、省略可能である。省略した場合は最新のバージョンが利用される。セマンティックバージョニングに従う必要がある。 `git`はレジストリを使わずにgitリポジトリから直接パッケージをインストールする場合に指定する。`git_url`はgitリポジトリのURLである。 +`path`はローカルのパッケージを使用する場合に指定する。 ### deprecated