use std::collections::{BTreeMap, BTreeSet};
use lex_vcs::{default_import_alias, is_local_import, OpLog, OperationKind};
use crate::store::{SkippedStage, Store, StoreError};
#[derive(Debug, Default, Clone)]
pub struct PackageHead {
pub map: BTreeMap<String, String>,
pub sig_files: BTreeMap<String, String>,
pub flat_imports: BTreeMap<String, String>,
pub file_imports: BTreeMap<String, BTreeMap<String, String>>,
}
impl PackageHead {
pub fn add_import(&mut self, in_file: &str, module: &str, alias: Option<&str>) {
let alias = alias.map(str::to_string).unwrap_or_else(|| default_import_alias(module));
if !is_local_import(module) {
self.flat_imports.insert(module.to_string(), alias.clone());
}
self.file_imports
.entry(in_file.to_string())
.or_default()
.insert(module.to_string(), alias);
}
pub fn remove_import(&mut self, in_file: &str, module: &str) {
if !is_local_import(module) {
self.flat_imports.remove(module);
}
if let Some(m) = self.file_imports.get_mut(in_file) {
m.remove(module);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenderedSource {
Single { path: Option<String>, src: String },
Multi(BTreeMap<String, String>),
}
fn single_module_path(head: &PackageHead) -> Option<String> {
let distinct: std::collections::BTreeSet<&String> =
head.sig_files.values().filter(|f| !f.is_empty()).collect();
match distinct.len() {
1 => Some(distinct.into_iter().next().expect("length checked").clone()),
_ => None,
}
}
pub fn package_head_at_op(store: &Store, head_op: &str) -> Result<PackageHead, StoreError> {
let log = OpLog::open(store.root())?;
let mut head = PackageHead::default();
for rec in log.walk_forward(&head_op.to_string(), None)? {
crate::branches::apply_transition(&mut head.map, &rec.produces);
match &rec.op.kind {
OperationKind::AddFunction { sig_id, in_file: Some(f), .. }
| OperationKind::AddType { sig_id, in_file: Some(f), .. } => {
head.sig_files.insert(sig_id.clone(), f.clone());
}
OperationKind::AddImport { in_file, module, alias } => {
head.add_import(in_file, module, alias.as_deref());
}
OperationKind::RemoveImport { in_file, module } => {
head.remove_import(in_file, module);
}
other => carry_sig_file(&mut head.sig_files, other),
}
}
Ok(head)
}
pub fn carry_sig_file(sig_files: &mut BTreeMap<String, String>, kind: &OperationKind) {
let (from, to, moved_to) = match kind {
OperationKind::RenameSymbol { from, to, in_file, .. } => (from, to, in_file.as_ref()),
OperationKind::ChangeEffectSig { sig_id: from, to_sig_id: Some(to), .. }
| OperationKind::ModifyBody { sig_id: from, to_sig_id: Some(to), .. }
| OperationKind::ModifyType { sig_id: from, to_sig_id: Some(to), .. } => (from, to, None),
_ => return,
};
if let Some(old) = sig_files.remove(from) {
sig_files.insert(to.clone(), moved_to.cloned().unwrap_or(old));
}
}
pub fn render_source(store: &Store, head: &PackageHead) -> Result<RenderedSource, StoreError> {
render_source_inner(store, head).map_err(|e| match e {
StoreError::UnknownStage(_) => store.check_pairs_satisfiable(&head.map).err().unwrap_or(e),
other => other,
})
}
fn render_source_inner(store: &Store, head: &PackageHead) -> Result<RenderedSource, StoreError> {
let multi = !head.map.is_empty() && head.map.keys().all(|s| head.sig_files.contains_key(s));
if multi {
Ok(RenderedSource::Multi(render_multifile(store, head)?))
} else {
Ok(RenderedSource::Single {
path: single_module_path(head),
src: render_singlefile(store, head)?,
})
}
}
pub fn module_record_at_op(store: &Store, head_op: &str) -> Result<lex_types::Ty, StoreError> {
module_record_at_op_for(store, head_op, None)
}
pub fn module_record_at_op_for(
store: &Store,
head_op: &str,
module: Option<&str>,
) -> Result<lex_types::Ty, StoreError> {
let stages = match module {
Some(m) => demangled_module_stages(store, head_op, m)?,
None => demangled_head_stages(store, head_op)?,
};
let types = lex_types::check_program(&stages).map_err(StoreError::TypeError)?;
let fields = types
.fn_signatures
.iter()
.filter(|(name, _)| !name.contains('.'))
.map(|(name, scheme)| (name.clone(), scheme.ty.clone()));
Ok(lex_types::module_record_from_fields(fields))
}
pub(crate) fn module_file<'a>(files: impl Iterator<Item = &'a String>, module: &str) -> Option<String> {
let want_src = format!("src/{module}.lex");
let want_flat = format!("{module}.lex");
let mut stem_match: Option<String> = None;
for f in files {
if *f == want_src || *f == want_flat {
return Some(f.clone());
}
let stem = f.rsplit('/').next().unwrap_or(f).trim_end_matches(".lex");
if stem == module && stem_match.is_none() {
stem_match = Some(f.clone());
}
}
stem_match
}
pub(crate) fn demangled_module_stages(
store: &Store,
head_op: &str,
module: &str,
) -> Result<Vec<lex_ast::Stage>, StoreError> {
let head = package_head_at_op(store, head_op)?;
let pairs: Vec<(String, String)> =
head.map.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
let asts = store.get_asts_for_sigs_bulk(&pairs);
let mut by_file: BTreeMap<String, Vec<lex_ast::Stage>> = BTreeMap::new();
for ((sig, _), ast) in pairs.iter().zip(asts) {
let stage = ast?;
let file = head.sig_files.get(sig).cloned().unwrap_or_default();
by_file.entry(file).or_default().push(stage);
}
let target = module_file(by_file.keys(), module)
.ok_or(StoreError::UnsupportedMultiModuleDependency)?;
let own_stages = by_file.get(&target).cloned().unwrap_or_default();
let own_prefix = own_stages.iter().find_map(stage_prefix).unwrap_or_default();
let mut bound_locals = BTreeSet::new();
for s in &own_stages {
collect_bound_locals(s, &mut bound_locals);
}
let mut rw = FileRewrite {
own_prefix: &own_prefix,
own_file: &target,
prefix_to_file: &BTreeMap::new(),
bound_locals: &bound_locals,
local_imports: BTreeMap::new(),
recorded_local: BTreeMap::new(),
flatten_unknown_prefixes: false,
};
let mut stages: Vec<lex_ast::Stage> = head
.flat_imports
.iter()
.map(|(reference, alias)| {
lex_ast::Stage::Import(lex_ast::Import {
reference: reference.clone(),
alias: alias.clone(),
})
})
.collect();
for file_stages in by_file.values() {
for s in file_stages {
let mut s = s.clone();
rw.rewrite_stage(&mut s);
stages.push(s);
}
}
Ok(stages)
}
pub(crate) fn demangled_head_stages(
store: &Store,
head_op: &str,
) -> Result<Vec<lex_ast::Stage>, StoreError> {
Ok(demangled_head_stages_impl(store, head_op, false)?.0)
}
pub(crate) fn demangled_head_stages_impl(
store: &Store,
head_op: &str,
skip_unloadable: bool,
) -> Result<(Vec<lex_ast::Stage>, Vec<SkippedStage>), StoreError> {
let head = package_head_at_op(store, head_op)?;
let distinct_files: BTreeSet<&String> = head.sig_files.values().collect();
if distinct_files.len() > 1 {
return Err(StoreError::UnsupportedMultiModuleDependency);
}
let pairs: Vec<(String, String)> =
head.map.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
let (mut decls, skipped) = store.load_head_decls(&pairs, skip_unloadable)?;
let own_prefix = decls.iter().find_map(stage_prefix).unwrap_or_default();
let mut bound_locals = BTreeSet::new();
for s in &decls {
collect_bound_locals(s, &mut bound_locals);
}
let mut rw = FileRewrite {
own_prefix: &own_prefix,
own_file: "",
prefix_to_file: &BTreeMap::new(),
bound_locals: &bound_locals,
local_imports: BTreeMap::new(),
recorded_local: BTreeMap::new(),
flatten_unknown_prefixes: true,
};
for s in &mut decls {
rw.rewrite_stage(s);
}
let mut stages: Vec<lex_ast::Stage> = Vec::new();
for (reference, alias) in &head.flat_imports {
stages.push(lex_ast::Stage::Import(lex_ast::Import {
reference: reference.clone(),
alias: alias.clone(),
}));
}
stages.extend(decls);
Ok((stages, skipped))
}
fn restore_doc(store: &Store, pairs: &[(String, String)], decls: &mut [lex_ast::Stage]) {
for ((sig_id, stage_id), decl) in pairs.iter().zip(decls.iter_mut()) {
let Ok(meta) = store.get_metadata_for_sig(sig_id, stage_id) else { continue };
if meta.doc.is_empty() {
continue;
}
match decl {
lex_ast::Stage::FnDecl(fd) => fd.doc = meta.doc,
lex_ast::Stage::TypeDecl(td) => td.doc = meta.doc,
lex_ast::Stage::Import(_) => {}
}
}
}
fn render_singlefile(store: &Store, head: &PackageHead) -> Result<String, StoreError> {
let pairs: Vec<(String, String)> = head.map.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
let mut decls: Vec<lex_ast::Stage> = Vec::new();
for ast in store.get_asts_for_sigs_bulk(&pairs) {
decls.push(ast?);
}
restore_doc(store, &pairs, &mut decls);
let own_prefix = decls.iter().find_map(stage_prefix).unwrap_or_default();
let mut bound_locals = BTreeSet::new();
for s in &decls {
collect_bound_locals(s, &mut bound_locals);
}
let mut rw = FileRewrite {
own_prefix: &own_prefix,
own_file: "",
prefix_to_file: &BTreeMap::new(),
bound_locals: &bound_locals,
local_imports: BTreeMap::new(),
recorded_local: BTreeMap::new(),
flatten_unknown_prefixes: true,
};
for s in &mut decls {
rw.rewrite_stage(s);
}
let mut stages: Vec<lex_ast::Stage> = Vec::new();
for (reference, alias) in &head.flat_imports {
stages.push(lex_ast::Stage::Import(lex_ast::Import {
reference: reference.clone(),
alias: alias.clone(),
}));
}
stages.extend(decls);
Ok(lex_ast::print_stages(&stages))
}
fn render_multifile(store: &Store, head: &PackageHead) -> Result<BTreeMap<String, String>, StoreError> {
let mut prefix_to_file: BTreeMap<String, String> = BTreeMap::new();
let mut by_file: BTreeMap<String, Vec<lex_ast::Stage>> = BTreeMap::new();
let pairs: Vec<(String, String)> = head.map.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
let asts = store.get_asts_for_sigs_bulk(&pairs);
for ((sig, stage_id), ast) in pairs.iter().zip(asts) {
let mut stage = ast?;
if let Ok(meta) = store.get_metadata_for_sig(sig, stage_id) {
if !meta.doc.is_empty() {
match &mut stage {
lex_ast::Stage::FnDecl(fd) => fd.doc = meta.doc,
lex_ast::Stage::TypeDecl(td) => td.doc = meta.doc,
lex_ast::Stage::Import(_) => {}
}
}
}
let file = head.sig_files.get(sig).cloned().unwrap_or_default();
if let Some(prefix) = stage_prefix(&stage) {
prefix_to_file.insert(prefix, file.clone());
}
by_file.entry(file).or_default().push(stage);
}
let mut out: BTreeMap<String, String> = BTreeMap::new();
for (file, stages) in &by_file {
let own_prefix = stages.iter().find_map(stage_prefix).unwrap_or_default();
let mut bound_locals = BTreeSet::new();
for s in stages {
collect_bound_locals(s, &mut bound_locals);
}
let mut recorded_local: BTreeMap<String, (String, String)> = BTreeMap::new();
if let Some(imports) = head.file_imports.get(file) {
for (reference, alias) in imports {
if !is_local_import(reference) {
continue;
}
if let Some(target) = resolve_local_import(file, reference) {
recorded_local.entry(target).or_insert((reference.clone(), alias.clone()));
}
}
}
let mut rw = FileRewrite {
own_prefix: &own_prefix,
own_file: file,
prefix_to_file: &prefix_to_file,
bound_locals: &bound_locals,
local_imports: BTreeMap::new(),
recorded_local,
flatten_unknown_prefixes: true,
};
let rewritten: Vec<lex_ast::Stage> = stages
.iter()
.cloned()
.map(|mut s| {
rw.rewrite_stage(&mut s);
s
})
.collect();
let mut imports: BTreeMap<String, String> = head.file_imports.get(file).cloned().unwrap_or_default();
imports.extend(rw.local_imports);
let mut out_stages: Vec<lex_ast::Stage> = Vec::new();
for (reference, alias) in &imports {
out_stages.push(lex_ast::Stage::Import(lex_ast::Import {
reference: reference.clone(),
alias: alias.clone(),
}));
}
out_stages.extend(rewritten);
out.insert(file.clone(), lex_ast::print_stages(&out_stages));
}
Ok(out)
}
fn is_mangle_prefix(q: &str) -> bool {
match q.rsplit_once('_') {
Some((stem, hex)) => {
!stem.is_empty()
&& hex.len() >= 6
&& hex.chars().all(|c| c.is_ascii_hexdigit())
}
None => false,
}
}
pub(crate) fn stage_prefix(s: &lex_ast::Stage) -> Option<String> {
let name = match s {
lex_ast::Stage::FnDecl(fd) => &fd.name,
lex_ast::Stage::TypeDecl(td) => &td.name,
lex_ast::Stage::Import(_) => return None,
};
name.split_once('.').map(|(p, _)| p.to_string())
}
pub(crate) fn resolve_local_import(from: &str, reference: &str) -> Option<String> {
if reference.starts_with('/') {
return None;
}
let mut parts: Vec<&str> = from
.rsplit_once('/')
.map(|(d, _)| d)
.unwrap_or("")
.split('/')
.filter(|s| !s.is_empty())
.collect();
for seg in reference.split('/') {
match seg {
"" | "." => {}
".." => {
parts.pop()?;
}
s => parts.push(s),
}
}
let last = parts.last()?;
let mut out = parts.join("/");
if std::path::Path::new(last).extension().is_none() {
out.push_str(".lex");
}
Some(out)
}
fn relative_import(from: &str, to: &str) -> (String, String) {
let from_dir: Vec<&str> = from
.rsplit_once('/')
.map(|(d, _)| d)
.unwrap_or("")
.split('/')
.filter(|s| !s.is_empty())
.collect();
let to_noext = to.strip_suffix(".lex").unwrap_or(to);
let to_parts: Vec<&str> = to_noext.split('/').filter(|s| !s.is_empty()).collect();
let alias = to_parts.last().copied().unwrap_or("mod").to_string();
let mut i = 0;
while i < from_dir.len() && i + 1 < to_parts.len() && from_dir[i] == to_parts[i] {
i += 1;
}
let ups = from_dir.len() - i;
let mut rel = String::new();
if ups == 0 {
rel.push_str("./");
} else {
for _ in 0..ups {
rel.push_str("../");
}
}
rel.push_str(&to_parts[i..].join("/"));
(rel, alias)
}
fn collect_bound_locals(s: &lex_ast::Stage, out: &mut BTreeSet<String>) {
if let lex_ast::Stage::FnDecl(fd) = s {
for p in &fd.params {
out.insert(p.name.clone());
}
collect_expr_locals(&fd.body, out);
for ex in &fd.examples {
for a in &ex.args {
collect_expr_locals(a, out);
}
collect_expr_locals(&ex.expected, out);
}
}
}
fn collect_expr_locals(e: &lex_ast::CExpr, out: &mut BTreeSet<String>) {
use lex_ast::CExpr::*;
match e {
Let { name, value, body, .. } => {
out.insert(name.clone());
collect_expr_locals(value, out);
collect_expr_locals(body, out);
}
Lambda { params, body, .. } => {
for p in params {
out.insert(p.name.clone());
}
collect_expr_locals(body, out);
}
Match { scrutinee, arms } => {
collect_expr_locals(scrutinee, out);
for arm in arms {
collect_pattern_locals(&arm.pattern, out);
collect_expr_locals(&arm.body, out);
}
}
Call { callee, args } => {
collect_expr_locals(callee, out);
for a in args {
collect_expr_locals(a, out);
}
}
Block { statements, result } => {
for s in statements {
collect_expr_locals(s, out);
}
collect_expr_locals(result, out);
}
Constructor { args, .. } => {
for a in args {
collect_expr_locals(a, out);
}
}
RecordLit { fields } => {
for f in fields {
collect_expr_locals(&f.value, out);
}
}
TupleLit { items } | ListLit { items } => {
for i in items {
collect_expr_locals(i, out);
}
}
FieldAccess { value, .. } => collect_expr_locals(value, out),
BinOp { lhs, rhs, .. } => {
collect_expr_locals(lhs, out);
collect_expr_locals(rhs, out);
}
UnaryOp { expr, .. } => collect_expr_locals(expr, out),
Return { value } => collect_expr_locals(value, out),
Var { .. } | Literal { .. } => {}
}
}
fn collect_pattern_locals(p: &lex_ast::Pattern, out: &mut BTreeSet<String>) {
use lex_ast::Pattern::*;
match p {
PVar { name } => {
out.insert(name.clone());
}
PConstructor { args, .. } => {
for a in args {
collect_pattern_locals(a, out);
}
}
PRecord { fields } => {
for f in fields {
collect_pattern_locals(&f.pattern, out);
}
}
PTuple { items } => {
for i in items {
collect_pattern_locals(i, out);
}
}
PLiteral { .. } | PWild => {}
}
}
struct FileRewrite<'a> {
own_prefix: &'a str,
own_file: &'a str,
prefix_to_file: &'a BTreeMap<String, String>,
bound_locals: &'a BTreeSet<String>,
local_imports: BTreeMap<String, String>,
recorded_local: BTreeMap<String, (String, String)>,
flatten_unknown_prefixes: bool,
}
impl FileRewrite<'_> {
fn rename(&mut self, name: &str) -> String {
if let Some(rest) = name.strip_prefix(&format!("{}.", self.own_prefix)) {
return rest.to_string();
}
if let Some((q, rest)) = name.split_once('.') {
if q != self.own_prefix {
if let Some(other_file) = self.prefix_to_file.get(q) {
if let Some((_, alias)) = self.recorded_local.get(other_file) {
return format!("{alias}.{rest}");
}
let (import_ref, stem) = relative_import(self.own_file, other_file);
let alias = if self.bound_locals.contains(&stem) {
q.to_string()
} else {
stem
};
self.local_imports.insert(import_ref, alias.clone());
return format!("{alias}.{rest}");
}
if self.flatten_unknown_prefixes && is_mangle_prefix(q) {
return rest.to_string();
}
}
}
name.to_string()
}
fn rewrite_stage(&mut self, s: &mut lex_ast::Stage) {
map_stage_names(s, &mut |_site, name| self.rename(name));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum NameSite {
Decl,
Value,
Type,
}
pub(crate) fn map_stage_names(s: &mut lex_ast::Stage, f: &mut dyn FnMut(NameSite, &str) -> String) {
match s {
lex_ast::Stage::FnDecl(fd) => {
fd.name = f(NameSite::Decl, &fd.name);
for p in &mut fd.params {
map_type_names(&mut p.ty, f);
}
map_type_names(&mut fd.return_type, f);
map_expr_names(&mut fd.body, f);
for ex in &mut fd.examples {
for a in &mut ex.args {
map_expr_names(a, f);
}
map_expr_names(&mut ex.expected, f);
}
}
lex_ast::Stage::TypeDecl(td) => {
td.name = f(NameSite::Decl, &td.name);
map_type_names(&mut td.definition, f);
}
lex_ast::Stage::Import(_) => {}
}
}
fn map_expr_names(e: &mut lex_ast::CExpr, f: &mut dyn FnMut(NameSite, &str) -> String) {
use lex_ast::CExpr::*;
match e {
Var { name } => *name = f(NameSite::Value, name),
Literal { .. } => {}
Call { callee, args } => {
map_expr_names(callee, f);
for a in args {
map_expr_names(a, f);
}
}
Let { value, body, ty, .. } => {
if let Some(t) = ty {
map_type_names(t, f);
}
map_expr_names(value, f);
map_expr_names(body, f);
}
Match { scrutinee, arms } => {
map_expr_names(scrutinee, f);
for arm in arms {
map_expr_names(&mut arm.body, f);
}
}
Block { statements, result } => {
for s in statements {
map_expr_names(s, f);
}
map_expr_names(result, f);
}
Constructor { args, .. } => {
for a in args {
map_expr_names(a, f);
}
}
RecordLit { fields } => {
for fl in fields {
map_expr_names(&mut fl.value, f);
}
}
TupleLit { items } | ListLit { items } => {
for i in items {
map_expr_names(i, f);
}
}
FieldAccess { value, .. } => map_expr_names(value, f),
Lambda { params, return_type, body, .. } => {
for p in params {
map_type_names(&mut p.ty, f);
}
map_type_names(return_type, f);
map_expr_names(body, f);
}
BinOp { lhs, rhs, .. } => {
map_expr_names(lhs, f);
map_expr_names(rhs, f);
}
UnaryOp { expr, .. } => map_expr_names(expr, f),
Return { value } => map_expr_names(value, f),
}
}
fn map_type_names(t: &mut lex_ast::TypeExpr, f: &mut dyn FnMut(NameSite, &str) -> String) {
use lex_ast::TypeExpr::*;
match t {
Named { name, args } => {
*name = f(NameSite::Type, name);
for a in args {
map_type_names(a, f);
}
}
Record { fields } => {
for fl in fields {
map_type_names(&mut fl.ty, f);
}
}
Tuple { items } => {
for i in items {
map_type_names(i, f);
}
}
Function { params, ret, .. } => {
for p in params {
map_type_names(p, f);
}
map_type_names(ret, f);
}
Union { variants } => {
for v in variants {
if let Some(pl) = &mut v.payload {
map_type_names(pl, f);
}
}
}
RecordWithSpreads { spreads, fields } => {
for s in spreads {
*s = f(NameSite::Type, s);
}
for fl in fields {
map_type_names(&mut fl.ty, f);
}
}
Refined { base, predicate, .. } => {
map_type_names(base, f);
map_expr_names(predicate, f);
}
}
}
#[cfg(test)]
mod prefix_tests {
use super::is_mangle_prefix;
#[test]
fn recognizes_mangle_prefixes_not_stdlib_aliases() {
assert!(is_mangle_prefix("lib_56ce0533"));
assert!(is_mangle_prefix("schema_a1b2c3"));
assert!(!is_mangle_prefix("int"));
assert!(!is_mangle_prefix("str"));
assert!(!is_mangle_prefix("map_reduce")); assert!(!is_mangle_prefix("nt"));
assert!(!is_mangle_prefix("lib_xyz")); }
}