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/// Extract a single-module package head's public function signatures as a
90/// module record type — what the write-time gate hands to
91/// [`lex_types::check_program_with_modules`] when it resolves a dependency
92/// (#930 phase 2b). The dependency's op-log head is reconstructed,
93/// de-mangled to bare names (reusing the same [`FileRewrite`] the single-file
94/// renderer applies), and type-checked; each top-level function signature
95/// becomes a field of the returned [`lex_types::Ty::Record`].
96///
97/// Only single-module dependencies are supported for now — a multi-module
98/// head (every stage carries an `in_file`) returns
99/// [`StoreError::UnsupportedMultiModuleDependency`] rather than silently
100/// resolving the wrong surface; picking the imported module's file out of the
101/// de-flattened tree is a later extension. The dependency must also
102/// type-check on its own (a *leaf* — no unresolved dependencies of its own);
103/// resolving a dependency that itself has registry/git dependencies is the
104/// recursive extension that follows.
105pub fn module_record_at_op(store: &Store, head_op: &str) -> Result<lex_types::Ty, StoreError> {
106    let stages = demangled_head_stages(store, head_op)?;
107    let types = lex_types::check_program(&stages).map_err(StoreError::TypeError)?;
108    // Every top-level function is part of the module's callable surface.
109    let fields = types
110        .fn_signatures
111        .iter()
112        .map(|(name, scheme)| (name.clone(), scheme.ty.clone()));
113    Ok(lex_types::module_record_from_fields(fields))
114}
115
116/// A single-file package head as a *de-mangled* canonical program: the
117/// head's (stdlib) imports followed by its declarations under bare names —
118/// `gcd`, not `lib_<hash>.gcd`. This is the program a dependent's author
119/// sees, so it's the common substrate for everything that reasons about a
120/// head at the source level: extracting a dependency's public surface
121/// ([`module_record_at_op`]) and evaluating a typed issue's acceptance
122/// against it (`crate::issues`, #949).
123///
124/// "Multi-module" means the head spans MORE THAN ONE source file — then
125/// per-module de-mangling at the stage level isn't wired up yet (#942) and
126/// this returns [`StoreError::UnsupportedMultiModuleDependency`]. A
127/// single-file package still records an `in_file` for every stage when
128/// published via `lex publish <dir>` (so it renders to `src/<file>.lex`), but
129/// its whole surface is that one module; count distinct files rather than
130/// "every stage has a file", which misclassified that case.
131pub(crate) fn demangled_head_stages(
132    store: &Store,
133    head_op: &str,
134) -> Result<Vec<lex_ast::Stage>, StoreError> {
135    let head = package_head_at_op(store, head_op)?;
136    let distinct_files: BTreeSet<&String> = head.sig_files.values().collect();
137    if distinct_files.len() > 1 {
138        return Err(StoreError::UnsupportedMultiModuleDependency);
139    }
140    let pairs: Vec<(String, String)> =
141        head.map.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
142    let mut decls: Vec<lex_ast::Stage> = Vec::new();
143    for ast in store.get_asts_for_sigs_bulk(&pairs) {
144        decls.push(ast?);
145    }
146    // De-mangle exactly as `render_singlefile` does.
147    let own_prefix = decls.iter().find_map(stage_prefix).unwrap_or_default();
148    let mut bound_locals = BTreeSet::new();
149    for s in &decls {
150        collect_bound_locals(s, &mut bound_locals);
151    }
152    let mut rw = FileRewrite {
153        own_prefix: &own_prefix,
154        own_file: "",
155        prefix_to_file: &BTreeMap::new(),
156        bound_locals: &bound_locals,
157        local_imports: BTreeMap::new(),
158    };
159    for s in &mut decls {
160        rw.rewrite_stage(s);
161    }
162    let mut stages: Vec<lex_ast::Stage> = Vec::new();
163    for (reference, alias) in &head.flat_imports {
164        stages.push(lex_ast::Stage::Import(lex_ast::Import {
165            reference: reference.clone(),
166            alias: alias.clone(),
167        }));
168    }
169    stages.extend(decls);
170    Ok(stages)
171}
172
173/// The whole head as one source string (single module / #895 path). Imports
174/// first, then the head stages read per-SigId (so structurally identical
175/// stages that share a StageId keep their distinct names).
176fn render_singlefile(store: &Store, head: &PackageHead) -> Result<String, StoreError> {
177    let pairs: Vec<(String, String)> = head.map.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
178    let mut decls: Vec<lex_ast::Stage> = Vec::new();
179    for ast in store.get_asts_for_sigs_bulk(&pairs) {
180        decls.push(ast?);
181    }
182    // De-mangle: a single-module head still carries mangle prefixes (the
183    // package loader mangles every declaration, and an inlined dependency
184    // adds its own). With one module, everything belongs to this package's
185    // namespace, so strip every mangle prefix to a bare name — otherwise the
186    // rendered source has invalid dotted declarations (#930). FileRewrite with
187    // an empty `prefix_to_file` and the shared own-prefix does exactly this
188    // (own prefix stripped directly; any other mangle prefix via the inlined
189    // fallback in `rename`).
190    let own_prefix = decls.iter().find_map(stage_prefix).unwrap_or_default();
191    let mut bound_locals = BTreeSet::new();
192    for s in &decls {
193        collect_bound_locals(s, &mut bound_locals);
194    }
195    let mut rw = FileRewrite {
196        own_prefix: &own_prefix,
197        own_file: "",
198        prefix_to_file: &BTreeMap::new(),
199        bound_locals: &bound_locals,
200        local_imports: BTreeMap::new(),
201    };
202    for s in &mut decls {
203        rw.rewrite_stage(s);
204    }
205
206    let mut stages: Vec<lex_ast::Stage> = Vec::new();
207    for (reference, alias) in &head.flat_imports {
208        stages.push(lex_ast::Stage::Import(lex_ast::Import {
209            reference: reference.clone(),
210            alias: alias.clone(),
211        }));
212    }
213    stages.extend(decls);
214    Ok(lex_ast::print_stages(&stages))
215}
216
217/// De-flatten a mangled multi-module head into a `relpath → source` tree.
218fn render_multifile(store: &Store, head: &PackageHead) -> Result<BTreeMap<String, String>, StoreError> {
219    let mut prefix_to_file: BTreeMap<String, String> = BTreeMap::new();
220    let mut by_file: BTreeMap<String, Vec<lex_ast::Stage>> = BTreeMap::new();
221    // Read each stage through the SigId the head names it by (not by StageId,
222    // which is name-independent) so cross-module structural twins keep their
223    // own names and file (#818/#894).
224    let pairs: Vec<(String, String)> = head.map.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
225    let asts = store.get_asts_for_sigs_bulk(&pairs);
226    for ((sig, _), ast) in pairs.iter().zip(asts) {
227        let stage = ast?;
228        let file = head.sig_files.get(sig).cloned().unwrap_or_default();
229        if let Some(prefix) = stage_prefix(&stage) {
230            prefix_to_file.insert(prefix, file.clone());
231        }
232        by_file.entry(file).or_default().push(stage);
233    }
234
235    let mut out: BTreeMap<String, String> = BTreeMap::new();
236    for (file, stages) in &by_file {
237        let own_prefix = stages.iter().find_map(stage_prefix).unwrap_or_default();
238        let mut bound_locals = BTreeSet::new();
239        for s in stages {
240            collect_bound_locals(s, &mut bound_locals);
241        }
242        let mut rw = FileRewrite {
243            own_prefix: &own_prefix,
244            own_file: file,
245            prefix_to_file: &prefix_to_file,
246            bound_locals: &bound_locals,
247            local_imports: BTreeMap::new(),
248        };
249        let rewritten: Vec<lex_ast::Stage> = stages
250            .iter()
251            .cloned()
252            .map(|mut s| {
253                rw.rewrite_stage(&mut s);
254                s
255            })
256            .collect();
257
258        let mut imports: BTreeMap<String, String> = head.file_imports.get(file).cloned().unwrap_or_default();
259        imports.extend(rw.local_imports);
260
261        let mut out_stages: Vec<lex_ast::Stage> = Vec::new();
262        for (reference, alias) in &imports {
263            out_stages.push(lex_ast::Stage::Import(lex_ast::Import {
264                reference: reference.clone(),
265                alias: alias.clone(),
266            }));
267        }
268        out_stages.extend(rewritten);
269        out.insert(file.clone(), lex_ast::print_stages(&out_stages));
270    }
271    Ok(out)
272}
273
274/// Whether `q` looks like a per-file mangle prefix (`<stem>_<hex6+>`), as
275/// opposed to a stdlib import alias (`int`, `str`, `map`). Used to detect an
276/// inlined dependency's prefix so it can be flattened to a bare name.
277fn is_mangle_prefix(q: &str) -> bool {
278    match q.rsplit_once('_') {
279        Some((stem, hex)) => {
280            !stem.is_empty()
281                && hex.len() >= 6
282                && hex.chars().all(|c| c.is_ascii_hexdigit())
283        }
284        None => false,
285    }
286}
287
288/// The mangling prefix of a declaration (`schema_a1b2.validate` →
289/// `schema_a1b2`), or `None` for an import or an unmangled name.
290fn stage_prefix(s: &lex_ast::Stage) -> Option<String> {
291    let name = match s {
292        lex_ast::Stage::FnDecl(fd) => &fd.name,
293        lex_ast::Stage::TypeDecl(td) => &td.name,
294        lex_ast::Stage::Import(_) => return None,
295    };
296    name.split_once('.').map(|(p, _)| p.to_string())
297}
298
299/// `("src/schema.lex", "src/error.lex")` → `("./error", "error")`.
300fn relative_import(from: &str, to: &str) -> (String, String) {
301    let from_dir: Vec<&str> = from
302        .rsplit_once('/')
303        .map(|(d, _)| d)
304        .unwrap_or("")
305        .split('/')
306        .filter(|s| !s.is_empty())
307        .collect();
308    let to_noext = to.strip_suffix(".lex").unwrap_or(to);
309    let to_parts: Vec<&str> = to_noext.split('/').filter(|s| !s.is_empty()).collect();
310    let alias = to_parts.last().copied().unwrap_or("mod").to_string();
311    let mut i = 0;
312    while i < from_dir.len() && i + 1 < to_parts.len() && from_dir[i] == to_parts[i] {
313        i += 1;
314    }
315    let ups = from_dir.len() - i;
316    let mut rel = String::new();
317    if ups == 0 {
318        rel.push_str("./");
319    } else {
320        for _ in 0..ups {
321            rel.push_str("../");
322        }
323    }
324    rel.push_str(&to_parts[i..].join("/"));
325    (rel, alias)
326}
327
328fn collect_bound_locals(s: &lex_ast::Stage, out: &mut BTreeSet<String>) {
329    if let lex_ast::Stage::FnDecl(fd) = s {
330        for p in &fd.params {
331            out.insert(p.name.clone());
332        }
333        collect_expr_locals(&fd.body, out);
334        for ex in &fd.examples {
335            for a in &ex.args {
336                collect_expr_locals(a, out);
337            }
338            collect_expr_locals(&ex.expected, out);
339        }
340    }
341}
342
343fn collect_expr_locals(e: &lex_ast::CExpr, out: &mut BTreeSet<String>) {
344    use lex_ast::CExpr::*;
345    match e {
346        Let { name, value, body, .. } => {
347            out.insert(name.clone());
348            collect_expr_locals(value, out);
349            collect_expr_locals(body, out);
350        }
351        Lambda { params, body, .. } => {
352            for p in params {
353                out.insert(p.name.clone());
354            }
355            collect_expr_locals(body, out);
356        }
357        Match { scrutinee, arms } => {
358            collect_expr_locals(scrutinee, out);
359            for arm in arms {
360                collect_pattern_locals(&arm.pattern, out);
361                collect_expr_locals(&arm.body, out);
362            }
363        }
364        Call { callee, args } => {
365            collect_expr_locals(callee, out);
366            for a in args {
367                collect_expr_locals(a, out);
368            }
369        }
370        Block { statements, result } => {
371            for s in statements {
372                collect_expr_locals(s, out);
373            }
374            collect_expr_locals(result, out);
375        }
376        Constructor { args, .. } => {
377            for a in args {
378                collect_expr_locals(a, out);
379            }
380        }
381        RecordLit { fields } => {
382            for f in fields {
383                collect_expr_locals(&f.value, out);
384            }
385        }
386        TupleLit { items } | ListLit { items } => {
387            for i in items {
388                collect_expr_locals(i, out);
389            }
390        }
391        FieldAccess { value, .. } => collect_expr_locals(value, out),
392        BinOp { lhs, rhs, .. } => {
393            collect_expr_locals(lhs, out);
394            collect_expr_locals(rhs, out);
395        }
396        UnaryOp { expr, .. } => collect_expr_locals(expr, out),
397        Return { value } => collect_expr_locals(value, out),
398        Var { .. } | Literal { .. } => {}
399    }
400}
401
402fn collect_pattern_locals(p: &lex_ast::Pattern, out: &mut BTreeSet<String>) {
403    use lex_ast::Pattern::*;
404    match p {
405        PVar { name } => {
406            out.insert(name.clone());
407        }
408        PConstructor { args, .. } => {
409            for a in args {
410                collect_pattern_locals(a, out);
411            }
412        }
413        PRecord { fields } => {
414            for f in fields {
415                collect_pattern_locals(&f.pattern, out);
416            }
417        }
418        PTuple { items } => {
419            for i in items {
420                collect_pattern_locals(i, out);
421            }
422        }
423        PLiteral { .. } | PWild => {}
424    }
425}
426
427struct FileRewrite<'a> {
428    own_prefix: &'a str,
429    own_file: &'a str,
430    prefix_to_file: &'a BTreeMap<String, String>,
431    bound_locals: &'a BTreeSet<String>,
432    local_imports: BTreeMap<String, String>,
433}
434
435impl FileRewrite<'_> {
436    /// Un-mangle a dotted name for THIS file: own prefix → bare; another
437    /// package file's prefix → `alias.rest` (recording the import); anything
438    /// else (a stdlib alias like `int.to_str`, or a bare name) untouched.
439    fn rename(&mut self, name: &str) -> String {
440        if let Some(rest) = name.strip_prefix(&format!("{}.", self.own_prefix)) {
441            return rest.to_string();
442        }
443        if let Some((q, rest)) = name.split_once('.') {
444            if q != self.own_prefix {
445                if let Some(other_file) = self.prefix_to_file.get(q) {
446                    let (import_ref, stem) = relative_import(self.own_file, other_file);
447                    let alias = if self.bound_locals.contains(&stem) {
448                        q.to_string()
449                    } else {
450                        stem
451                    };
452                    self.local_imports.insert(import_ref, alias.clone());
453                    return format!("{alias}.{rest}");
454                }
455                // A mangle prefix (`<stem>_<hex>`) that maps to no file is an
456                // *inlined dependency* — the loader flattened a registry dep
457                // into this program (lex-lang#930). It has no file of its own,
458                // so render it as a bare top-level name (inlining folds it into
459                // this package's namespace); leaving `prefix.name` would emit
460                // an invalid dotted declaration/reference. Stdlib aliases
461                // (`int.to_str`) don't match the mangle pattern and pass through.
462                if is_mangle_prefix(q) {
463                    return rest.to_string();
464                }
465            }
466        }
467        name.to_string()
468    }
469
470    fn rewrite_stage(&mut self, s: &mut lex_ast::Stage) {
471        match s {
472            lex_ast::Stage::FnDecl(fd) => {
473                fd.name = self.rename(&fd.name);
474                for p in &mut fd.params {
475                    self.rewrite_type(&mut p.ty);
476                }
477                self.rewrite_type(&mut fd.return_type);
478                self.rewrite_expr(&mut fd.body);
479                for ex in &mut fd.examples {
480                    for a in &mut ex.args {
481                        self.rewrite_expr(a);
482                    }
483                    self.rewrite_expr(&mut ex.expected);
484                }
485            }
486            lex_ast::Stage::TypeDecl(td) => {
487                td.name = self.rename(&td.name);
488                self.rewrite_type(&mut td.definition);
489            }
490            lex_ast::Stage::Import(_) => {}
491        }
492    }
493
494    fn rewrite_expr(&mut self, e: &mut lex_ast::CExpr) {
495        use lex_ast::CExpr::*;
496        match e {
497            Var { name } => *name = self.rename(name),
498            Literal { .. } => {}
499            Call { callee, args } => {
500                self.rewrite_expr(callee);
501                for a in args {
502                    self.rewrite_expr(a);
503                }
504            }
505            Let { value, body, ty, .. } => {
506                if let Some(t) = ty {
507                    self.rewrite_type(t);
508                }
509                self.rewrite_expr(value);
510                self.rewrite_expr(body);
511            }
512            Match { scrutinee, arms } => {
513                self.rewrite_expr(scrutinee);
514                for arm in arms {
515                    self.rewrite_expr(&mut arm.body);
516                }
517            }
518            Block { statements, result } => {
519                for s in statements {
520                    self.rewrite_expr(s);
521                }
522                self.rewrite_expr(result);
523            }
524            Constructor { args, .. } => {
525                for a in args {
526                    self.rewrite_expr(a);
527                }
528            }
529            RecordLit { fields } => {
530                for f in fields {
531                    self.rewrite_expr(&mut f.value);
532                }
533            }
534            TupleLit { items } | ListLit { items } => {
535                for i in items {
536                    self.rewrite_expr(i);
537                }
538            }
539            FieldAccess { value, .. } => self.rewrite_expr(value),
540            Lambda { params, return_type, body, .. } => {
541                for p in params {
542                    self.rewrite_type(&mut p.ty);
543                }
544                self.rewrite_type(return_type);
545                self.rewrite_expr(body);
546            }
547            BinOp { lhs, rhs, .. } => {
548                self.rewrite_expr(lhs);
549                self.rewrite_expr(rhs);
550            }
551            UnaryOp { expr, .. } => self.rewrite_expr(expr),
552            Return { value } => self.rewrite_expr(value),
553        }
554    }
555
556    fn rewrite_type(&mut self, t: &mut lex_ast::TypeExpr) {
557        use lex_ast::TypeExpr::*;
558        match t {
559            Named { name, args } => {
560                *name = self.rename(name);
561                for a in args {
562                    self.rewrite_type(a);
563                }
564            }
565            Record { fields } => {
566                for f in fields {
567                    self.rewrite_type(&mut f.ty);
568                }
569            }
570            Tuple { items } => {
571                for i in items {
572                    self.rewrite_type(i);
573                }
574            }
575            Function { params, ret, .. } => {
576                for p in params {
577                    self.rewrite_type(p);
578                }
579                self.rewrite_type(ret);
580            }
581            Union { variants } => {
582                for v in variants {
583                    if let Some(pl) = &mut v.payload {
584                        self.rewrite_type(pl);
585                    }
586                }
587            }
588            RecordWithSpreads { spreads, fields } => {
589                for s in spreads {
590                    *s = self.rename(s);
591                }
592                for f in fields {
593                    self.rewrite_type(&mut f.ty);
594                }
595            }
596            Refined { base, predicate, .. } => {
597                self.rewrite_type(base);
598                self.rewrite_expr(predicate);
599            }
600        }
601    }
602}
603
604#[cfg(test)]
605mod prefix_tests {
606    use super::is_mangle_prefix;
607
608    #[test]
609    fn recognizes_mangle_prefixes_not_stdlib_aliases() {
610        // Inlined-dep / file mangle prefixes: <stem>_<hex6+>.
611        assert!(is_mangle_prefix("lib_56ce0533"));
612        assert!(is_mangle_prefix("schema_a1b2c3"));
613        // Stdlib import aliases and ordinary names are not prefixes.
614        assert!(!is_mangle_prefix("int"));
615        assert!(!is_mangle_prefix("str"));
616        assert!(!is_mangle_prefix("map_reduce")); // "reduce" isn't hex
617        assert!(!is_mangle_prefix("nt"));
618        assert!(!is_mangle_prefix("lib_xyz")); // too short / non-hex
619    }
620}