use std::{
env,
io::{self, Write},
path::{Path, PathBuf},
};
use fs_err as fs;
use miden_assembly::{
Assembler, Library, LibraryNamespace, LibraryPath, Parse, ParseOptions, ast::ModuleKind,
debuginfo::DefaultSourceManager,
};
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## {}\n| Procedure | Description |\n| ----------- | ------------- |\n", ns);
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!("| {} | {} |\n", name, escaped);
writer.write_all(line.as_bytes()).expect("unable to write func to writer");
}
}
}
fn markdown_file_name(ns: &str) -> String {
let parts: Vec<&str> = ns.split("::").collect();
if parts.len() > 1 && parts[0] == "std" {
format!("{}.md", parts[1..].join("/"))
} else {
format!("{}.md", parts.join("/"))
}
}
pub fn build_stdlib_docs(asm_dir: &Path, output_dir: &str) -> io::Result<()> {
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 modules = find_masm_modules(asm_dir, asm_dir)?;
for (label, file_path) in modules {
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) = parse_module_with_ast(&label, &file_path)?;
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);
for (name, docs) in procedures {
MarkdownRenderer::write_docs_procedure(&f, &name, docs.as_deref());
}
}
Ok(())
}
fn find_masm_modules(base_dir: &Path, current_dir: &Path) -> io::Result<Vec<(String, PathBuf)>> {
let mut modules = Vec::new();
let entries = match fs::read_dir(current_dir) {
Ok(e) => e,
Err(e) => {
eprintln!("Warning: read_dir({}): {e}", current_dir.display());
return Ok(modules);
},
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension()
&& ext == "masm"
{
let relative_path = path.strip_prefix(base_dir).unwrap();
let module_path = relative_path
.with_extension("")
.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("::");
let label = format!("std::{}", module_path);
modules.push((label, path));
}
} else if path.is_dir() {
modules.extend(find_masm_modules(base_dir, &path)?);
}
}
Ok(modules)
}
type DocPayload = (Option<String>, Vec<(String, Option<String>)>);
fn parse_module_with_ast(label: &str, file_path: &Path) -> io::Result<DocPayload> {
let path = LibraryPath::new(label).map_err(|e| io::Error::other(e.to_string()))?;
let module = file_path
.parse_with_options(
&DefaultSourceManager::default(),
ParseOptions {
kind: ModuleKind::Library,
warnings_as_errors: false,
path: Some(path),
},
)
.map_err(|e| io::Error::other(e.to_string()))?;
let module_docs = module.docs().map(|d| d.to_string());
let mut procedures = Vec::new();
for export in module.procedures() {
let name = export.name().to_string();
let docs = export.docs().map(|d| d.to_string());
procedures.push((name, docs));
}
Ok((module_docs, procedures))
}
fn main() -> io::Result<()> {
println!("cargo:rerun-if-changed=asm");
println!("cargo:rerun-if-env-changed=MIDEN_BUILD_STDLIB_DOCS");
println!("cargo:rerun-if-changed=../assembly/src");
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().with_debug_mode(cfg!(feature = "with-debug-info"));
let namespace = "std".parse::<LibraryNamespace>().expect("invalid base namespace");
let stdlib = assembler
.assemble_library_from_dir(&asm_dir, namespace)
.map_err(|e| io::Error::other(e.to_string()))?;
let build_dir = env::var("OUT_DIR").unwrap();
let build_dir = Path::new(&build_dir);
let output_file = build_dir
.join(ASL_DIR_PATH)
.join("std")
.with_extension(Library::LIBRARY_EXTENSION);
stdlib.write_to_file(output_file).map_err(|e| io::Error::other(e.to_string()))?;
if std::env::var("MIDEN_BUILD_STDLIB_DOCS").is_ok() {
build_stdlib_docs(&asm_dir, DOC_DIR_PATH)?;
}
Ok(())
}