594 lines
18 KiB
Rust
594 lines
18 KiB
Rust
use std::{
|
|
collections::{BTreeMap, BTreeSet},
|
|
fs,
|
|
path::Path,
|
|
};
|
|
|
|
use crate::{
|
|
ast::Program,
|
|
diag::Diagnostic,
|
|
lexer, lower,
|
|
project::{self, ProjectArtifact, SourceFile, ToolFailure, WorkspaceArtifact},
|
|
sexpr::{Atom, SExpr, SExprKind},
|
|
types::Type,
|
|
};
|
|
|
|
pub fn generate(input: &str, output_dir: &str) -> Result<(), ToolFailure> {
|
|
let docs = if project::is_project_input(input) {
|
|
let loaded = project::load_project_sources_for_tools(input)?;
|
|
render_project(&loaded.artifact, &loaded.sources)?
|
|
} else {
|
|
let source = fs::read_to_string(input).map_err(|err| ToolFailure {
|
|
diagnostics: vec![Diagnostic::new(
|
|
input,
|
|
"InputReadFailed",
|
|
format!("cannot read `{}`: {}", input, err),
|
|
)],
|
|
sources: Vec::new(),
|
|
artifact: None,
|
|
})?;
|
|
let sources = vec![SourceFile {
|
|
path: input.to_string(),
|
|
source,
|
|
}];
|
|
render_file(input, &sources)?
|
|
};
|
|
|
|
fs::create_dir_all(output_dir).map_err(|err| ToolFailure {
|
|
diagnostics: vec![Diagnostic::new(
|
|
output_dir,
|
|
"OutputWriteFailed",
|
|
format!(
|
|
"cannot create documentation directory `{}`: {}",
|
|
output_dir, err
|
|
),
|
|
)],
|
|
sources: Vec::new(),
|
|
artifact: None,
|
|
})?;
|
|
fs::write(Path::new(output_dir).join("index.md"), docs).map_err(|err| ToolFailure {
|
|
diagnostics: vec![Diagnostic::new(
|
|
output_dir,
|
|
"OutputWriteFailed",
|
|
format!("cannot write documentation into `{}`: {}", output_dir, err),
|
|
)],
|
|
sources: Vec::new(),
|
|
artifact: None,
|
|
})
|
|
}
|
|
|
|
fn render_project(
|
|
artifact: &ProjectArtifact,
|
|
sources: &[SourceFile],
|
|
) -> Result<String, ToolFailure> {
|
|
let modules = sources
|
|
.iter()
|
|
.map(document_source)
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
let mut out = String::new();
|
|
out.push_str("# Project ");
|
|
out.push_str(&artifact.project_name);
|
|
out.push_str("\n\n");
|
|
if let Some(workspace) = &artifact.workspace {
|
|
render_workspace(&mut out, workspace);
|
|
render_workspace_package_public_api(&mut out, workspace, &modules);
|
|
} else {
|
|
render_project_package_public_api(&mut out, artifact, &modules);
|
|
}
|
|
for module in &modules {
|
|
render_module(&mut out, module);
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
fn render_workspace(out: &mut String, workspace: &WorkspaceArtifact) {
|
|
out.push_str("## Workspace\n\n");
|
|
render_list(out, "Members", &workspace.members);
|
|
|
|
let packages = workspace
|
|
.packages
|
|
.iter()
|
|
.map(|package| {
|
|
format!(
|
|
"{} {} (entry {})",
|
|
package.name, package.version, package.entry
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
render_list(out, "Packages", &packages);
|
|
|
|
let dependencies = workspace
|
|
.dependencies
|
|
.iter()
|
|
.map(|dependency| {
|
|
format!(
|
|
"{} -> {} ({}, {})",
|
|
dependency.from, dependency.to, dependency.kind, dependency.path
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
render_list(out, "Package Dependencies", &dependencies);
|
|
}
|
|
|
|
fn render_file(title: &str, sources: &[SourceFile]) -> Result<String, ToolFailure> {
|
|
let mut out = String::new();
|
|
out.push_str("# File ");
|
|
out.push_str(title);
|
|
out.push_str("\n\n");
|
|
let module = document_source(&sources[0])?;
|
|
render_module(&mut out, &module);
|
|
Ok(out)
|
|
}
|
|
|
|
fn document_source(source: &SourceFile) -> Result<DocModule, ToolFailure> {
|
|
let tokens = lexer::lex(&source.path, &source.source).map_err(|diagnostics| ToolFailure {
|
|
diagnostics,
|
|
sources: vec![source.clone()],
|
|
artifact: None,
|
|
})?;
|
|
let forms = crate::sexpr::parse(&source.path, &tokens).map_err(|diagnostics| ToolFailure {
|
|
diagnostics,
|
|
sources: vec![source.clone()],
|
|
artifact: None,
|
|
})?;
|
|
let lowerable_forms = forms
|
|
.iter()
|
|
.filter(|form| !matches!(list_head(form), Some("import")))
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
let program = lower::lower_program(&source.path, &lowerable_forms).ok();
|
|
Ok(module_from_forms(&source.path, &forms, program.as_ref()))
|
|
}
|
|
|
|
struct DocModule {
|
|
path: String,
|
|
title: String,
|
|
imports: Vec<String>,
|
|
exports: Vec<String>,
|
|
structs: Vec<String>,
|
|
functions: Vec<String>,
|
|
tests: Vec<String>,
|
|
public_api: PublicApi,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct PublicApi {
|
|
functions: Vec<DocFunction>,
|
|
structs: Vec<DocStruct>,
|
|
enums: Vec<DocEnum>,
|
|
}
|
|
|
|
struct DocFunction {
|
|
name: String,
|
|
signature: String,
|
|
}
|
|
|
|
struct DocStruct {
|
|
name: String,
|
|
fields: Vec<String>,
|
|
}
|
|
|
|
struct DocEnum {
|
|
name: String,
|
|
variants: Vec<String>,
|
|
}
|
|
|
|
fn module_from_forms(file: &str, forms: &[SExpr], program: Option<&Program>) -> DocModule {
|
|
let mut module = DocModule {
|
|
path: file.to_string(),
|
|
title: file.to_string(),
|
|
imports: Vec::new(),
|
|
exports: Vec::new(),
|
|
structs: Vec::new(),
|
|
functions: Vec::new(),
|
|
tests: Vec::new(),
|
|
public_api: PublicApi::default(),
|
|
};
|
|
|
|
for form in forms {
|
|
let Some(items) = list(form) else {
|
|
continue;
|
|
};
|
|
match items.first().and_then(ident) {
|
|
Some("module") => {
|
|
if let Some(name) = items.get(1).and_then(ident) {
|
|
module.title = name.to_string();
|
|
}
|
|
if let Some(exports) = items.get(2).and_then(list) {
|
|
if matches!(exports.first().and_then(ident), Some("export")) {
|
|
module
|
|
.exports
|
|
.extend(exports[1..].iter().filter_map(ident).map(str::to_string));
|
|
}
|
|
}
|
|
}
|
|
Some("import") => {
|
|
if let Some(name) = items.get(1).and_then(ident) {
|
|
module.imports.push(name.to_string());
|
|
}
|
|
}
|
|
Some("struct") => {
|
|
if let Some(name) = items.get(1).and_then(ident) {
|
|
module.structs.push(name.to_string());
|
|
}
|
|
}
|
|
Some("fn") => {
|
|
if let Some(name) = items.get(1).and_then(ident) {
|
|
module.functions.push(name.to_string());
|
|
}
|
|
}
|
|
Some("test") => {
|
|
if let Some(name) = items.get(1).and_then(string_atom) {
|
|
module.tests.push(name.to_string());
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
if let Some(program) = program {
|
|
module.title = program.module.clone();
|
|
module.structs = program
|
|
.structs
|
|
.iter()
|
|
.map(|decl| decl.name.clone())
|
|
.collect();
|
|
module.functions = program
|
|
.functions
|
|
.iter()
|
|
.map(|function| {
|
|
let params = function
|
|
.params
|
|
.iter()
|
|
.map(|param| format!("{} {}", param.name, param.ty))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
format!("{}({}) -> {}", function.name, params, function.return_type)
|
|
})
|
|
.collect();
|
|
module.tests = program.tests.iter().map(|test| test.name.clone()).collect();
|
|
module.public_api = public_api_from_program(program, &module.exports);
|
|
}
|
|
|
|
module.imports.sort();
|
|
module.exports.sort();
|
|
module.structs.sort();
|
|
module.functions.sort();
|
|
module.tests.sort();
|
|
module
|
|
}
|
|
|
|
fn render_module(out: &mut String, module: &DocModule) {
|
|
out.push_str("## Module ");
|
|
out.push_str(&module.title);
|
|
out.push_str("\n\n");
|
|
render_list(out, "Imports", &module.imports);
|
|
render_list(out, "Exports", &module.exports);
|
|
render_public_api_section(out, "###", &module.public_api);
|
|
render_list(out, "Structs", &module.structs);
|
|
render_list(out, "Functions", &module.functions);
|
|
render_list(out, "Tests", &module.tests);
|
|
}
|
|
|
|
fn render_project_package_public_api(
|
|
out: &mut String,
|
|
artifact: &ProjectArtifact,
|
|
modules: &[DocModule],
|
|
) {
|
|
let local_root = Path::new(&artifact.project_root).join(&artifact.source_root);
|
|
let local_modules = modules
|
|
.iter()
|
|
.filter(|module| Path::new(&module.path).starts_with(&local_root))
|
|
.collect::<Vec<_>>();
|
|
render_package_public_api(out, &artifact.project_name, &local_modules);
|
|
}
|
|
|
|
fn render_workspace_package_public_api(
|
|
out: &mut String,
|
|
workspace: &WorkspaceArtifact,
|
|
modules: &[DocModule],
|
|
) {
|
|
let by_path = modules
|
|
.iter()
|
|
.map(|module| (module.path.as_str(), module))
|
|
.collect::<BTreeMap<_, _>>();
|
|
|
|
for package in &workspace.packages {
|
|
let local_root = Path::new(&package.root).join(&package.source_root);
|
|
let package_modules = package
|
|
.modules
|
|
.iter()
|
|
.filter(|module| Path::new(&module.path).starts_with(&local_root))
|
|
.filter_map(|module| by_path.get(module.path.as_str()).copied())
|
|
.collect::<Vec<_>>();
|
|
render_package_public_api(
|
|
out,
|
|
&format!("{} {}", package.name, package.version),
|
|
&package_modules,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn render_package_public_api(out: &mut String, package_name: &str, modules: &[&DocModule]) {
|
|
out.push_str("## Package API ");
|
|
out.push_str(package_name);
|
|
out.push_str("\n\n");
|
|
|
|
let public_modules = modules
|
|
.iter()
|
|
.copied()
|
|
.filter(|module| !module.public_api.is_empty())
|
|
.collect::<Vec<_>>();
|
|
if public_modules.is_empty() {
|
|
out.push_str("None.\n\n");
|
|
return;
|
|
}
|
|
|
|
for module in public_modules {
|
|
out.push_str("### Module ");
|
|
out.push_str(&module.title);
|
|
out.push_str("\n\n");
|
|
render_public_api_body(out, "####", &module.public_api);
|
|
}
|
|
}
|
|
|
|
fn render_public_api_section(out: &mut String, heading: &str, public_api: &PublicApi) {
|
|
out.push_str(heading);
|
|
out.push_str(" Public API\n\n");
|
|
render_public_api_body(out, "####", public_api);
|
|
}
|
|
|
|
fn render_public_api_body(out: &mut String, heading: &str, public_api: &PublicApi) {
|
|
if public_api.is_empty() {
|
|
out.push_str("None.\n\n");
|
|
return;
|
|
}
|
|
|
|
if !public_api.functions.is_empty() {
|
|
out.push_str(heading);
|
|
out.push_str(" Functions\n");
|
|
for function in &public_api.functions {
|
|
render_code_bullet(out, &function.signature);
|
|
}
|
|
out.push('\n');
|
|
}
|
|
|
|
if !public_api.structs.is_empty() {
|
|
out.push_str(heading);
|
|
out.push_str(" Structs\n");
|
|
for struct_decl in &public_api.structs {
|
|
render_code_bullet(out, &format!("struct {}", struct_decl.name));
|
|
for field in &struct_decl.fields {
|
|
render_indented_code_bullet(out, field);
|
|
}
|
|
}
|
|
out.push('\n');
|
|
}
|
|
|
|
if !public_api.enums.is_empty() {
|
|
out.push_str(heading);
|
|
out.push_str(" Enums\n");
|
|
for enum_decl in &public_api.enums {
|
|
render_code_bullet(out, &format!("enum {}", enum_decl.name));
|
|
for variant in &enum_decl.variants {
|
|
render_indented_code_bullet(out, variant);
|
|
}
|
|
}
|
|
out.push('\n');
|
|
}
|
|
}
|
|
|
|
fn render_list(out: &mut String, title: &str, values: &[String]) {
|
|
out.push_str("### ");
|
|
out.push_str(title);
|
|
out.push('\n');
|
|
if values.is_empty() {
|
|
out.push_str("\nNone.\n\n");
|
|
return;
|
|
}
|
|
for value in values {
|
|
out.push_str("- `");
|
|
out.push_str(&value.replace('`', "\\`"));
|
|
out.push_str("`\n");
|
|
}
|
|
out.push('\n');
|
|
}
|
|
|
|
fn list_head(expr: &SExpr) -> Option<&str> {
|
|
list(expr).and_then(|items| items.first()).and_then(ident)
|
|
}
|
|
|
|
fn render_code_bullet(out: &mut String, value: &str) {
|
|
out.push_str("- `");
|
|
out.push_str(&value.replace('`', "\\`"));
|
|
out.push_str("`\n");
|
|
}
|
|
|
|
fn render_indented_code_bullet(out: &mut String, value: &str) {
|
|
out.push_str(" - `");
|
|
out.push_str(&value.replace('`', "\\`"));
|
|
out.push_str("`\n");
|
|
}
|
|
|
|
impl PublicApi {
|
|
fn is_empty(&self) -> bool {
|
|
self.functions.is_empty() && self.structs.is_empty() && self.enums.is_empty()
|
|
}
|
|
}
|
|
|
|
fn public_api_from_program(program: &Program, exports: &[String]) -> PublicApi {
|
|
let export_names = exports.iter().cloned().collect::<BTreeSet<_>>();
|
|
let aliases = alias_targets(program);
|
|
|
|
let mut functions = program
|
|
.functions
|
|
.iter()
|
|
.filter(|function| export_names.contains(&function.name))
|
|
.map(|function| DocFunction {
|
|
name: function.name.clone(),
|
|
signature: function_signature(
|
|
&function.name,
|
|
function
|
|
.params
|
|
.iter()
|
|
.map(|param| (param.name.as_str(), ¶m.ty)),
|
|
&function.return_type,
|
|
&aliases,
|
|
),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
functions.sort_by(|left, right| left.name.cmp(&right.name));
|
|
|
|
let mut structs = program
|
|
.structs
|
|
.iter()
|
|
.filter(|struct_decl| export_names.contains(&struct_decl.name))
|
|
.map(|struct_decl| DocStruct {
|
|
name: struct_decl.name.clone(),
|
|
fields: struct_decl
|
|
.fields
|
|
.iter()
|
|
.map(|field| {
|
|
format!(
|
|
"{}: {}",
|
|
field.name,
|
|
display_public_type(&field.ty, &aliases)
|
|
)
|
|
})
|
|
.collect(),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
structs.sort_by(|left, right| left.name.cmp(&right.name));
|
|
|
|
let mut enums = program
|
|
.enums
|
|
.iter()
|
|
.filter(|enum_decl| export_names.contains(&enum_decl.name))
|
|
.map(|enum_decl| DocEnum {
|
|
name: enum_decl.name.clone(),
|
|
variants: enum_decl
|
|
.variants
|
|
.iter()
|
|
.map(|variant| match &variant.payload_ty {
|
|
Some(payload_ty) => {
|
|
format!(
|
|
"{}({})",
|
|
variant.name,
|
|
display_public_type(payload_ty, &aliases)
|
|
)
|
|
}
|
|
None => variant.name.clone(),
|
|
})
|
|
.collect(),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
enums.sort_by(|left, right| left.name.cmp(&right.name));
|
|
|
|
PublicApi {
|
|
functions,
|
|
structs,
|
|
enums,
|
|
}
|
|
}
|
|
|
|
fn function_signature<'a>(
|
|
name: &str,
|
|
params: impl Iterator<Item = (&'a str, &'a Type)>,
|
|
return_type: &Type,
|
|
aliases: &BTreeMap<String, Type>,
|
|
) -> String {
|
|
let params = params
|
|
.map(|(name, ty)| format!("{}: {}", name, display_public_type(ty, aliases)))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
format!(
|
|
"fn {}({}) -> {}",
|
|
name,
|
|
params,
|
|
display_public_type(return_type, aliases)
|
|
)
|
|
}
|
|
|
|
fn alias_targets(program: &Program) -> BTreeMap<String, Type> {
|
|
let raw = program
|
|
.type_aliases
|
|
.iter()
|
|
.map(|alias| (alias.name.clone(), alias.target.clone()))
|
|
.collect::<BTreeMap<_, _>>();
|
|
raw.keys()
|
|
.map(|name| {
|
|
let mut visiting = BTreeSet::new();
|
|
(
|
|
name.clone(),
|
|
resolve_alias_type(&Type::Named(name.clone()), &raw, &mut visiting),
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn display_public_type(ty: &Type, aliases: &BTreeMap<String, Type>) -> String {
|
|
let mut visiting = BTreeSet::new();
|
|
resolve_alias_type(ty, aliases, &mut visiting).to_string()
|
|
}
|
|
|
|
fn resolve_alias_type(
|
|
ty: &Type,
|
|
aliases: &BTreeMap<String, Type>,
|
|
visiting: &mut BTreeSet<String>,
|
|
) -> Type {
|
|
match ty {
|
|
Type::Named(name) => {
|
|
let Some(target) = aliases.get(name) else {
|
|
return ty.clone();
|
|
};
|
|
if !visiting.insert(name.clone()) {
|
|
return ty.clone();
|
|
}
|
|
let resolved = resolve_alias_type(target, aliases, visiting);
|
|
visiting.remove(name);
|
|
resolved
|
|
}
|
|
Type::Ptr(inner) => Type::Ptr(Box::new(resolve_alias_type(inner, aliases, visiting))),
|
|
Type::Array(inner, len) => {
|
|
Type::Array(Box::new(resolve_alias_type(inner, aliases, visiting)), *len)
|
|
}
|
|
Type::Vec(inner) => Type::Vec(Box::new(resolve_alias_type(inner, aliases, visiting))),
|
|
Type::Slice(inner) => Type::Slice(Box::new(resolve_alias_type(inner, aliases, visiting))),
|
|
Type::Option(inner) => Type::Option(Box::new(resolve_alias_type(inner, aliases, visiting))),
|
|
Type::Result(ok, err) => Type::Result(
|
|
Box::new(resolve_alias_type(ok, aliases, visiting)),
|
|
Box::new(resolve_alias_type(err, aliases, visiting)),
|
|
),
|
|
Type::I32
|
|
| Type::I64
|
|
| Type::U32
|
|
| Type::U64
|
|
| Type::F64
|
|
| Type::Bool
|
|
| Type::Unit
|
|
| Type::String => ty.clone(),
|
|
}
|
|
}
|
|
|
|
fn list(expr: &SExpr) -> Option<&[SExpr]> {
|
|
match &expr.kind {
|
|
SExprKind::List(items) => Some(items),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn ident(expr: &SExpr) -> Option<&str> {
|
|
match &expr.kind {
|
|
SExprKind::Atom(Atom::Ident(value)) => Some(value),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn string_atom(expr: &SExpr) -> Option<&str> {
|
|
match &expr.kind {
|
|
SExprKind::Atom(Atom::String(value)) => Some(value),
|
|
_ => None,
|
|
}
|
|
}
|