py2erg/
gen_decl.rs

1use std::fs::{create_dir_all, File};
2use std::io::{BufWriter, Write};
3use std::path::Path;
4
5use erg_common::pathutil::{mod_name, NormalizedPathBuf};
6use erg_common::set::Set;
7use erg_common::traits::LimitedDisplay;
8use erg_common::{log, Str};
9use erg_compiler::build_package::{CheckStatus, PylyzerStatus};
10use erg_compiler::hir::{ClassDef, Expr, HIR};
11use erg_compiler::module::SharedModuleCache;
12use erg_compiler::ty::value::{GenTypeObj, TypeObj};
13use erg_compiler::ty::{HasType, Type};
14
15pub struct DeclFile {
16    pub filename: String,
17    pub code: String,
18}
19pub struct DeclFileGenerator {
20    filename: String,
21    namespace: String,
22    imported: Set<Str>,
23    code: String,
24}
25
26impl DeclFileGenerator {
27    pub fn new(path: &NormalizedPathBuf, status: CheckStatus) -> std::io::Result<Self> {
28        let (timestamp, hash) = {
29            let metadata = std::fs::metadata(path)?;
30            let dummy_hash = metadata.len();
31            (metadata.modified()?, dummy_hash)
32        };
33        let status = PylyzerStatus {
34            status,
35            file: path.to_path_buf(),
36            timestamp,
37            hash,
38        };
39        let code = format!("{status}\n");
40        Ok(Self {
41            filename: path
42                .file_name()
43                .unwrap_or_default()
44                .to_string_lossy()
45                .replace(".py", ".d.er"),
46            namespace: "".to_string(),
47            imported: Set::new(),
48            code,
49        })
50    }
51
52    pub fn gen_decl_er(mut self, hir: &HIR) -> DeclFile {
53        for chunk in hir.module.iter() {
54            self.gen_chunk_decl(chunk);
55        }
56        log!("code:\n{}", self.code);
57        DeclFile {
58            filename: self.filename,
59            code: self.code,
60        }
61    }
62
63    fn escape_type(&self, typ: String) -> String {
64        typ.replace('%', "Type_")
65            .replace("<module>", "")
66            .replace('/', ".")
67            .trim_start_matches(self.filename.trim_end_matches(".d.er"))
68            .trim_start_matches(&self.namespace)
69            .to_string()
70    }
71
72    // e.g. `x: foo.Bar` => `foo = pyimport "foo"; x: foo.Bar`
73    fn prepare_using_type(&mut self, typ: &Type) {
74        let namespace = Str::rc(
75            typ.namespace()
76                .split('/')
77                .next()
78                .unwrap()
79                .split('.')
80                .next()
81                .unwrap(),
82        );
83        if namespace != self.namespace
84            && !namespace.is_empty()
85            && self.imported.insert(namespace.clone())
86        {
87            self.code += &format!("{namespace} = pyimport \"{namespace}\"\n");
88        }
89    }
90
91    fn gen_chunk_decl(&mut self, chunk: &Expr) {
92        match chunk {
93            Expr::Def(def) => {
94                let mut name = def
95                    .sig
96                    .ident()
97                    .inspect()
98                    .replace('\0', "")
99                    .replace(['%', '*'], "___");
100                let ref_t = def.sig.ident().ref_t();
101                self.prepare_using_type(ref_t);
102                let typ = self.escape_type(ref_t.replace_failure().to_string_unabbreviated());
103                // Erg can automatically import nested modules
104                // `import http.client` => `http = pyimport "http"`
105                let decl = if ref_t.is_py_module() && ref_t.typarams()[0].is_str_value() {
106                    name = name.split('.').next().unwrap().to_string();
107                    let full_path_str = ref_t.typarams()[0].to_string_unabbreviated();
108                    let mod_name = mod_name(Path::new(full_path_str.trim_matches('"')));
109                    let imported = if self.imported.insert(mod_name.clone()) {
110                        format!("{}.{mod_name} = pyimport \"{mod_name}\"", self.namespace)
111                    } else {
112                        "".to_string()
113                    };
114                    if self.imported.insert(name.clone().into()) {
115                        format!(
116                            "{}.{name} = pyimport \"{mod_name}\"\n{imported}",
117                            self.namespace,
118                        )
119                    } else {
120                        imported
121                    }
122                } else {
123                    format!("{}.{name}: {typ}", self.namespace)
124                };
125                self.code += &decl;
126            }
127            Expr::ClassDef(def) => {
128                let class_name = def
129                    .sig
130                    .ident()
131                    .inspect()
132                    .replace('\0', "")
133                    .replace(['%', '*'], "___");
134                let src = format!("{}.{class_name}", self.namespace);
135                let stash = std::mem::replace(&mut self.namespace, src);
136                let decl = format!(".{class_name}: ClassType");
137                self.code += &decl;
138                self.code.push('\n');
139                if let GenTypeObj::Subclass(class) = def.obj.as_ref() {
140                    let sup = class
141                        .sup
142                        .as_ref()
143                        .typ()
144                        .replace_failure()
145                        .to_string_unabbreviated();
146                    self.prepare_using_type(class.sup.typ());
147                    let sup = self.escape_type(sup);
148                    let decl = format!(".{class_name} <: {sup}\n");
149                    self.code += &decl;
150                }
151                if let Some(TypeObj::Builtin {
152                    t: Type::Record(rec),
153                    ..
154                }) = def.obj.base_or_sup()
155                {
156                    for (attr, t) in rec.iter() {
157                        self.prepare_using_type(t);
158                        let typ = self.escape_type(t.replace_failure().to_string_unabbreviated());
159                        let decl = format!("{}.{}: {typ}\n", self.namespace, attr.symbol);
160                        self.code += &decl;
161                    }
162                }
163                if let Some(TypeObj::Builtin {
164                    t: Type::Record(rec),
165                    ..
166                }) = def.obj.additional()
167                {
168                    for (attr, t) in rec.iter() {
169                        self.prepare_using_type(t);
170                        let typ = self.escape_type(t.replace_failure().to_string_unabbreviated());
171                        let decl = format!("{}.{}: {typ}\n", self.namespace, attr.symbol);
172                        self.code += &decl;
173                    }
174                }
175                for attr in ClassDef::get_all_methods(&def.methods_list) {
176                    self.gen_chunk_decl(attr);
177                }
178                self.namespace = stash;
179            }
180            Expr::Dummy(dummy) => {
181                for chunk in dummy.iter() {
182                    self.gen_chunk_decl(chunk);
183                }
184            }
185            Expr::Compound(compound) => {
186                for chunk in compound.iter() {
187                    self.gen_chunk_decl(chunk);
188                }
189            }
190            Expr::Call(call) if call.control_kind().is_some() => {
191                for arg in call.args.iter() {
192                    self.gen_chunk_decl(arg);
193                }
194            }
195            Expr::Lambda(lambda) => {
196                for arg in lambda.body.iter() {
197                    self.gen_chunk_decl(arg);
198                }
199            }
200            _ => {}
201        }
202        self.code.push('\n');
203    }
204}
205
206fn dump_decl_er(path: &NormalizedPathBuf, hir: &HIR, status: CheckStatus) -> std::io::Result<()> {
207    let decl_gen = DeclFileGenerator::new(path, status)?;
208    let file = decl_gen.gen_decl_er(hir);
209    let Some(dir) = path.parent().and_then(|p| p.canonicalize().ok()) else {
210        return Ok(());
211    };
212    let cache_dir = dir.join("__pycache__");
213    if !cache_dir.exists() {
214        let _ = create_dir_all(&cache_dir);
215    }
216    let path = cache_dir.join(file.filename);
217    if !path.exists() {
218        File::create(&path)?;
219    }
220    let f = File::options().write(true).open(path)?;
221    let mut f = BufWriter::new(f);
222    f.write_all(file.code.as_bytes())
223}
224
225pub fn dump_decl_package(modules: &SharedModuleCache) {
226    for (path, module) in modules.raw_iter() {
227        if let Some(hir) = module.hir.as_ref() {
228            let _ = dump_decl_er(path, hir, module.status);
229        }
230    }
231}