Skip to main content

lex_store/
render.rs

1//! Rendering a package's op-log head back to **source** — single `src.lex`
2//! for a single-module package, or the de-flattened `src/*.lex` tree for a
3//! multi-module one (#894).
4//!
5//! Declarations published through the package loader carry a per-file
6//! mangling prefix (`schema_a1b2.validate`); each `AddFunction`/`AddType` op
7//! records the source file it came from (`in_file`, #903). To render source
8//! we group the head's stages by file, strip each file's own prefix, and
9//! rewrite a reference to *another* file's prefix into `alias.name` plus a
10//! local `import`.
11//!
12//! This lives in `lex-store` (not the CLI) so both `lex export-git` and the
13//! hosted registry's archive endpoint render identically — the same source a
14//! human reads in the git mirror is the source a consumer installs.
15
16use std::collections::{BTreeMap, BTreeSet};
17
18use lex_vcs::{default_import_alias, OpLog, OperationKind};
19
20use crate::store::{Store, StoreError};
21
22/// A package head decomposed into what the renderer needs: the SigId→StageId
23/// head map, each SigId's source file, and the imports (flat, and per-file).
24#[derive(Debug, Default, Clone)]
25pub struct PackageHead {
26    /// SigId → StageId at the head.
27    pub map: BTreeMap<String, String>,
28    /// SigId → the source file its declaration came from (`in_file`).
29    pub sig_files: BTreeMap<String, String>,
30    /// module → alias, flattened across files (single-module render).
31    pub flat_imports: BTreeMap<String, String>,
32    /// file → (module → alias) (multi-module render).
33    pub file_imports: BTreeMap<String, BTreeMap<String, String>>,
34}
35
36/// Rendered package source: one module, or a `relpath → source` tree.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum RenderedSource {
39    Single(String),
40    Multi(BTreeMap<String, String>),
41}
42
43/// Walk the op-log from `head_op` and assemble the [`PackageHead`] — the same
44/// bookkeeping `lex export-git` does incrementally, done once for a single
45/// head (used by the registry archive endpoint).
46pub fn package_head_at_op(store: &Store, head_op: &str) -> Result<PackageHead, StoreError> {
47    let log = OpLog::open(store.root())?;
48    let mut head = PackageHead::default();
49    for rec in log.walk_forward(&head_op.to_string(), None)? {
50        crate::branches::apply_transition(&mut head.map, &rec.produces);
51        match &rec.op.kind {
52            OperationKind::AddFunction { sig_id, in_file: Some(f), .. }
53            | OperationKind::AddType { sig_id, in_file: Some(f), .. } => {
54                head.sig_files.insert(sig_id.clone(), f.clone());
55            }
56            OperationKind::AddImport { in_file, module, alias } => {
57                let alias = alias.clone().unwrap_or_else(|| default_import_alias(module));
58                head.flat_imports.insert(module.clone(), alias.clone());
59                head.file_imports.entry(in_file.clone()).or_default().insert(module.clone(), alias);
60            }
61            OperationKind::RemoveImport { in_file, module } => {
62                head.flat_imports.remove(module);
63                if let Some(m) = head.file_imports.get_mut(in_file) {
64                    m.remove(module);
65                }
66            }
67            OperationKind::RenameSymbol { from, to, .. } => {
68                if let Some(f) = head.sig_files.remove(from) {
69                    head.sig_files.insert(to.clone(), f);
70                }
71            }
72            _ => {}
73        }
74    }
75    Ok(head)
76}
77
78/// Render a package head to source. Multi-module iff every head fn/type stage
79/// records its source file; otherwise a single module.
80pub fn render_source(store: &Store, head: &PackageHead) -> Result<RenderedSource, StoreError> {
81    let multi = !head.map.is_empty() && head.map.keys().all(|s| head.sig_files.contains_key(s));
82    if multi {
83        Ok(RenderedSource::Multi(render_multifile(store, head)?))
84    } else {
85        Ok(RenderedSource::Single(render_singlefile(store, head)?))
86    }
87}
88
89/// The whole head as one source string (single module / #895 path). Imports
90/// first, then the head stages read per-SigId (so structurally identical
91/// stages that share a StageId keep their distinct names).
92fn render_singlefile(store: &Store, head: &PackageHead) -> Result<String, StoreError> {
93    let mut stages: Vec<lex_ast::Stage> = Vec::new();
94    for (reference, alias) in &head.flat_imports {
95        stages.push(lex_ast::Stage::Import(lex_ast::Import {
96            reference: reference.clone(),
97            alias: alias.clone(),
98        }));
99    }
100    let pairs: Vec<(String, String)> = head.map.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
101    for ast in store.get_asts_for_sigs_bulk(&pairs) {
102        stages.push(ast?);
103    }
104    Ok(lex_ast::print_stages(&stages))
105}
106
107/// De-flatten a mangled multi-module head into a `relpath → source` tree.
108fn render_multifile(store: &Store, head: &PackageHead) -> Result<BTreeMap<String, String>, StoreError> {
109    let mut prefix_to_file: BTreeMap<String, String> = BTreeMap::new();
110    let mut by_file: BTreeMap<String, Vec<lex_ast::Stage>> = BTreeMap::new();
111    // Read each stage through the SigId the head names it by (not by StageId,
112    // which is name-independent) so cross-module structural twins keep their
113    // own names and file (#818/#894).
114    let pairs: Vec<(String, String)> = head.map.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
115    let asts = store.get_asts_for_sigs_bulk(&pairs);
116    for ((sig, _), ast) in pairs.iter().zip(asts) {
117        let stage = ast?;
118        let file = head.sig_files.get(sig).cloned().unwrap_or_default();
119        if let Some(prefix) = stage_prefix(&stage) {
120            prefix_to_file.insert(prefix, file.clone());
121        }
122        by_file.entry(file).or_default().push(stage);
123    }
124
125    let mut out: BTreeMap<String, String> = BTreeMap::new();
126    for (file, stages) in &by_file {
127        let own_prefix = stages.iter().find_map(stage_prefix).unwrap_or_default();
128        let mut bound_locals = BTreeSet::new();
129        for s in stages {
130            collect_bound_locals(s, &mut bound_locals);
131        }
132        let mut rw = FileRewrite {
133            own_prefix: &own_prefix,
134            own_file: file,
135            prefix_to_file: &prefix_to_file,
136            bound_locals: &bound_locals,
137            local_imports: BTreeMap::new(),
138        };
139        let rewritten: Vec<lex_ast::Stage> = stages
140            .iter()
141            .cloned()
142            .map(|mut s| {
143                rw.rewrite_stage(&mut s);
144                s
145            })
146            .collect();
147
148        let mut imports: BTreeMap<String, String> = head.file_imports.get(file).cloned().unwrap_or_default();
149        imports.extend(rw.local_imports);
150
151        let mut out_stages: Vec<lex_ast::Stage> = Vec::new();
152        for (reference, alias) in &imports {
153            out_stages.push(lex_ast::Stage::Import(lex_ast::Import {
154                reference: reference.clone(),
155                alias: alias.clone(),
156            }));
157        }
158        out_stages.extend(rewritten);
159        out.insert(file.clone(), lex_ast::print_stages(&out_stages));
160    }
161    Ok(out)
162}
163
164/// The mangling prefix of a declaration (`schema_a1b2.validate` →
165/// `schema_a1b2`), or `None` for an import or an unmangled name.
166fn stage_prefix(s: &lex_ast::Stage) -> Option<String> {
167    let name = match s {
168        lex_ast::Stage::FnDecl(fd) => &fd.name,
169        lex_ast::Stage::TypeDecl(td) => &td.name,
170        lex_ast::Stage::Import(_) => return None,
171    };
172    name.split_once('.').map(|(p, _)| p.to_string())
173}
174
175/// `("src/schema.lex", "src/error.lex")` → `("./error", "error")`.
176fn relative_import(from: &str, to: &str) -> (String, String) {
177    let from_dir: Vec<&str> = from
178        .rsplit_once('/')
179        .map(|(d, _)| d)
180        .unwrap_or("")
181        .split('/')
182        .filter(|s| !s.is_empty())
183        .collect();
184    let to_noext = to.strip_suffix(".lex").unwrap_or(to);
185    let to_parts: Vec<&str> = to_noext.split('/').filter(|s| !s.is_empty()).collect();
186    let alias = to_parts.last().copied().unwrap_or("mod").to_string();
187    let mut i = 0;
188    while i < from_dir.len() && i + 1 < to_parts.len() && from_dir[i] == to_parts[i] {
189        i += 1;
190    }
191    let ups = from_dir.len() - i;
192    let mut rel = String::new();
193    if ups == 0 {
194        rel.push_str("./");
195    } else {
196        for _ in 0..ups {
197            rel.push_str("../");
198        }
199    }
200    rel.push_str(&to_parts[i..].join("/"));
201    (rel, alias)
202}
203
204fn collect_bound_locals(s: &lex_ast::Stage, out: &mut BTreeSet<String>) {
205    if let lex_ast::Stage::FnDecl(fd) = s {
206        for p in &fd.params {
207            out.insert(p.name.clone());
208        }
209        collect_expr_locals(&fd.body, out);
210        for ex in &fd.examples {
211            for a in &ex.args {
212                collect_expr_locals(a, out);
213            }
214            collect_expr_locals(&ex.expected, out);
215        }
216    }
217}
218
219fn collect_expr_locals(e: &lex_ast::CExpr, out: &mut BTreeSet<String>) {
220    use lex_ast::CExpr::*;
221    match e {
222        Let { name, value, body, .. } => {
223            out.insert(name.clone());
224            collect_expr_locals(value, out);
225            collect_expr_locals(body, out);
226        }
227        Lambda { params, body, .. } => {
228            for p in params {
229                out.insert(p.name.clone());
230            }
231            collect_expr_locals(body, out);
232        }
233        Match { scrutinee, arms } => {
234            collect_expr_locals(scrutinee, out);
235            for arm in arms {
236                collect_pattern_locals(&arm.pattern, out);
237                collect_expr_locals(&arm.body, out);
238            }
239        }
240        Call { callee, args } => {
241            collect_expr_locals(callee, out);
242            for a in args {
243                collect_expr_locals(a, out);
244            }
245        }
246        Block { statements, result } => {
247            for s in statements {
248                collect_expr_locals(s, out);
249            }
250            collect_expr_locals(result, out);
251        }
252        Constructor { args, .. } => {
253            for a in args {
254                collect_expr_locals(a, out);
255            }
256        }
257        RecordLit { fields } => {
258            for f in fields {
259                collect_expr_locals(&f.value, out);
260            }
261        }
262        TupleLit { items } | ListLit { items } => {
263            for i in items {
264                collect_expr_locals(i, out);
265            }
266        }
267        FieldAccess { value, .. } => collect_expr_locals(value, out),
268        BinOp { lhs, rhs, .. } => {
269            collect_expr_locals(lhs, out);
270            collect_expr_locals(rhs, out);
271        }
272        UnaryOp { expr, .. } => collect_expr_locals(expr, out),
273        Return { value } => collect_expr_locals(value, out),
274        Var { .. } | Literal { .. } => {}
275    }
276}
277
278fn collect_pattern_locals(p: &lex_ast::Pattern, out: &mut BTreeSet<String>) {
279    use lex_ast::Pattern::*;
280    match p {
281        PVar { name } => {
282            out.insert(name.clone());
283        }
284        PConstructor { args, .. } => {
285            for a in args {
286                collect_pattern_locals(a, out);
287            }
288        }
289        PRecord { fields } => {
290            for f in fields {
291                collect_pattern_locals(&f.pattern, out);
292            }
293        }
294        PTuple { items } => {
295            for i in items {
296                collect_pattern_locals(i, out);
297            }
298        }
299        PLiteral { .. } | PWild => {}
300    }
301}
302
303struct FileRewrite<'a> {
304    own_prefix: &'a str,
305    own_file: &'a str,
306    prefix_to_file: &'a BTreeMap<String, String>,
307    bound_locals: &'a BTreeSet<String>,
308    local_imports: BTreeMap<String, String>,
309}
310
311impl FileRewrite<'_> {
312    /// Un-mangle a dotted name for THIS file: own prefix → bare; another
313    /// package file's prefix → `alias.rest` (recording the import); anything
314    /// else (a stdlib alias like `int.to_str`, or a bare name) untouched.
315    fn rename(&mut self, name: &str) -> String {
316        if let Some(rest) = name.strip_prefix(&format!("{}.", self.own_prefix)) {
317            return rest.to_string();
318        }
319        if let Some((q, rest)) = name.split_once('.') {
320            if q != self.own_prefix {
321                if let Some(other_file) = self.prefix_to_file.get(q) {
322                    let (import_ref, stem) = relative_import(self.own_file, other_file);
323                    let alias = if self.bound_locals.contains(&stem) {
324                        q.to_string()
325                    } else {
326                        stem
327                    };
328                    self.local_imports.insert(import_ref, alias.clone());
329                    return format!("{alias}.{rest}");
330                }
331            }
332        }
333        name.to_string()
334    }
335
336    fn rewrite_stage(&mut self, s: &mut lex_ast::Stage) {
337        match s {
338            lex_ast::Stage::FnDecl(fd) => {
339                fd.name = self.rename(&fd.name);
340                for p in &mut fd.params {
341                    self.rewrite_type(&mut p.ty);
342                }
343                self.rewrite_type(&mut fd.return_type);
344                self.rewrite_expr(&mut fd.body);
345                for ex in &mut fd.examples {
346                    for a in &mut ex.args {
347                        self.rewrite_expr(a);
348                    }
349                    self.rewrite_expr(&mut ex.expected);
350                }
351            }
352            lex_ast::Stage::TypeDecl(td) => {
353                td.name = self.rename(&td.name);
354                self.rewrite_type(&mut td.definition);
355            }
356            lex_ast::Stage::Import(_) => {}
357        }
358    }
359
360    fn rewrite_expr(&mut self, e: &mut lex_ast::CExpr) {
361        use lex_ast::CExpr::*;
362        match e {
363            Var { name } => *name = self.rename(name),
364            Literal { .. } => {}
365            Call { callee, args } => {
366                self.rewrite_expr(callee);
367                for a in args {
368                    self.rewrite_expr(a);
369                }
370            }
371            Let { value, body, ty, .. } => {
372                if let Some(t) = ty {
373                    self.rewrite_type(t);
374                }
375                self.rewrite_expr(value);
376                self.rewrite_expr(body);
377            }
378            Match { scrutinee, arms } => {
379                self.rewrite_expr(scrutinee);
380                for arm in arms {
381                    self.rewrite_expr(&mut arm.body);
382                }
383            }
384            Block { statements, result } => {
385                for s in statements {
386                    self.rewrite_expr(s);
387                }
388                self.rewrite_expr(result);
389            }
390            Constructor { args, .. } => {
391                for a in args {
392                    self.rewrite_expr(a);
393                }
394            }
395            RecordLit { fields } => {
396                for f in fields {
397                    self.rewrite_expr(&mut f.value);
398                }
399            }
400            TupleLit { items } | ListLit { items } => {
401                for i in items {
402                    self.rewrite_expr(i);
403                }
404            }
405            FieldAccess { value, .. } => self.rewrite_expr(value),
406            Lambda { params, return_type, body, .. } => {
407                for p in params {
408                    self.rewrite_type(&mut p.ty);
409                }
410                self.rewrite_type(return_type);
411                self.rewrite_expr(body);
412            }
413            BinOp { lhs, rhs, .. } => {
414                self.rewrite_expr(lhs);
415                self.rewrite_expr(rhs);
416            }
417            UnaryOp { expr, .. } => self.rewrite_expr(expr),
418            Return { value } => self.rewrite_expr(value),
419        }
420    }
421
422    fn rewrite_type(&mut self, t: &mut lex_ast::TypeExpr) {
423        use lex_ast::TypeExpr::*;
424        match t {
425            Named { name, args } => {
426                *name = self.rename(name);
427                for a in args {
428                    self.rewrite_type(a);
429                }
430            }
431            Record { fields } => {
432                for f in fields {
433                    self.rewrite_type(&mut f.ty);
434                }
435            }
436            Tuple { items } => {
437                for i in items {
438                    self.rewrite_type(i);
439                }
440            }
441            Function { params, ret, .. } => {
442                for p in params {
443                    self.rewrite_type(p);
444                }
445                self.rewrite_type(ret);
446            }
447            Union { variants } => {
448                for v in variants {
449                    if let Some(pl) = &mut v.payload {
450                        self.rewrite_type(pl);
451                    }
452                }
453            }
454            RecordWithSpreads { spreads, fields } => {
455                for s in spreads {
456                    *s = self.rename(s);
457                }
458                for f in fields {
459                    self.rewrite_type(&mut f.ty);
460                }
461            }
462            Refined { base, predicate, .. } => {
463                self.rewrite_type(base);
464                self.rewrite_expr(predicate);
465            }
466        }
467    }
468}