use std::{
env,
io::{self, Write},
path::{Path, PathBuf},
sync::Arc,
};
use fs_err as fs;
use miden_assembly::{
Assembler, Report,
ast::{self, Module},
debuginfo::DefaultSourceManager,
diagnostics::{IntoDiagnostic, reporting::PrintDiagnostic},
};
const ASM_DIR_PATH: &str = "asm";
const ASL_DIR_PATH: &str = "assets";
const DOC_DIR_PATH: &str = "docs";
pub struct MarkdownRenderer {}
impl MarkdownRenderer {
fn write_docs_header(mut writer: &fs::File, ns: &str) {
let header =
format!("\n## {ns}\n| Procedure | Description |\n| ----------- | ------------- |\n");
writer.write_all(header.as_bytes()).expect("unable to write header to writer");
}
fn write_docs_procedure(mut writer: &fs::File, name: &str, docs: Option<&str>) {
if let Some(docs) = docs {
let escaped = docs.replace('|', "\\|").replace('\n', "<br />");
let line = format!("| {name} | {escaped} |\n");
writer.write_all(line.as_bytes()).expect("unable to write func to writer");
}
}
}
fn markdown_file_name(ns: &miden_assembly_syntax::Path) -> String {
use miden_assembly_syntax::Path as MasmPath;
let ns = ns.strip_prefix(MasmPath::new("miden::core")).unwrap_or(ns);
let mut buf = String::with_capacity(256);
for (i, part) in ns.components().enumerate() {
let part = part.unwrap();
if i > 0 {
buf.push('/');
}
buf.push_str(part.as_str());
}
if buf.is_empty() {
buf.push_str("mod");
}
buf.push_str(".md");
buf
}
pub fn build_core_lib_docs(asm_dir: &Path, output_dir: &str) -> io::Result<()> {
use miden_assembly_syntax::{Path as MasmPath, ast::ModuleKind, parser};
let output_path = Path::new(output_dir);
match fs::remove_dir_all(output_path) {
Ok(()) => {},
Err(e) if e.kind() == io::ErrorKind::NotFound => {},
Err(e) => return Err(e),
}
fs::create_dir_all(output_path)?;
let namespace = Arc::<MasmPath>::from(MasmPath::new("::miden::core"));
let source_manager = Arc::new(DefaultSourceManager::default());
let (root, support) = parser::read_modules_from_root(
asm_dir.join("mod.masm"),
Some(namespace),
Some(ModuleKind::Library),
source_manager,
true,
)
.unwrap_or_else(|err| panic!("{}", PrintDiagnostic::new(err)));
for module in core::slice::from_ref(&root).iter().chain(support.iter()) {
let label = module.path().to_relative();
let relative = markdown_file_name(label);
let out = output_path.join(&relative);
if let Some(parent) = out.parent() {
fs::create_dir_all(parent)?;
}
let mut f = fs::File::create(&out)?;
let (module_docs, procedures) = extract_docs(module, &support);
if let Some(docs) = module_docs {
let escaped = docs.replace('|', "\\|").replace('\n', "<br />");
f.write_all(escaped.as_bytes())?;
f.write_all(b"\n\n")?;
}
MarkdownRenderer::write_docs_header(&f, label.as_str());
for (name, docs) in procedures {
MarkdownRenderer::write_docs_procedure(&f, &name, docs.as_deref());
}
}
Ok(())
}
type DocPayload = (Option<String>, Vec<(String, Option<String>)>);
fn extract_docs(module: &Module, modules: &[Box<Module>]) -> DocPayload {
let module_docs = module.docs().map(|d| d.to_string());
let mut procedures = local_procedure_docs(module);
for import in module.imports() {
let ast::Import::Item(import) = import else {
continue;
};
if !import.visibility().is_public() {
continue;
}
if let Some(docs) = reexport_target_docs(import, module.path(), modules) {
procedures.push((import.local_name().to_string(), docs));
}
}
(module_docs, procedures)
}
fn local_procedure_docs(module: &Module) -> Vec<(String, Option<String>)> {
let mut procedures = Vec::new();
for (index, name) in module.exported() {
match &module[index] {
ast::Item::Procedure(proc) => {
let docs = proc.docs().map(|d| d.to_string());
procedures.push((name.name().to_string(), docs));
},
ast::Item::Constant(_) | ast::Item::Type(_) => {},
}
}
procedures
}
fn reexport_target_docs(
import: &ast::ItemImport,
current_module_path: &miden_assembly_syntax::Path,
modules: &[Box<Module>],
) -> Option<Option<String>> {
use std::borrow::Cow;
let target_path = import.target_path().into_inner();
let target_path = if target_path.starts_with("self") {
let (_, rest) = target_path.split_first()?;
Cow::Owned(current_module_path.join(rest))
} else {
target_path.to_absolute().unwrap()
};
let target_module_path = target_path.parent()?;
let target_module = modules.iter().find(|m| m.path() == target_module_path)?;
target_module
.procedures()
.find(|proc| proc.name().as_str() == import.source_name().as_str())
.map(|proc| proc.docs().map(|docs| docs.to_string()))
}
fn main() -> Result<(), Report> {
use miden_assembly::diagnostics::reporting::ReportHandlerOpts;
println!("cargo:rerun-if-changed=asm");
println!("cargo:rerun-if-env-changed=MIDEN_BUILD_LIB_DOCS");
println!("cargo:rerun-if-changed=../../assembly/src");
miden_assembly::diagnostics::reporting::set_hook(Box::new(|_| {
Box::new(ReportHandlerOpts::new().build())
}))
.unwrap();
miden_assembly::diagnostics::reporting::set_panic_hook();
env_logger::Builder::from_env("MIDEN_LOG").format_timestamp(None).init();
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let asm_dir = Path::new(manifest_dir).join(ASM_DIR_PATH);
let assembler = Assembler::default();
let mut registry = miden_package_registry::InMemoryPackageRegistry::default();
let mut project_assembler =
assembler.for_project_at_path(asm_dir.join("miden-project.toml"), &mut registry)?;
let package =
project_assembler.assemble(miden_assembly::ProjectTargetSelector::Library, "release")?;
let build_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
package.write_masp_file(build_dir.join(ASL_DIR_PATH)).into_diagnostic()?;
if env::var("MIDEN_BUILD_LIB_DOCS").is_ok() {
build_core_lib_docs(&asm_dir, DOC_DIR_PATH).into_diagnostic()?;
}
Ok(())
}