slovo/scripts/render-stdlib-api-doc.js

278 lines
8.4 KiB
JavaScript
Executable File

#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const repoRoot = path.resolve(__dirname, "..");
const stdDir = path.join(repoRoot, "lib", "std");
const outputPath = path.join(repoRoot, "docs", "language", "STDLIB_API.md");
const readmePath = path.join(repoRoot, "README.md");
function tokenize(source) {
const tokens = [];
let index = 0;
while (index < source.length) {
const ch = source[index];
if (/\s/.test(ch)) {
index += 1;
continue;
}
if (ch === ";") {
while (index < source.length && source[index] !== "\n") {
index += 1;
}
continue;
}
if (ch === "(" || ch === ")") {
tokens.push(ch);
index += 1;
continue;
}
if (ch === "\"") {
const start = index;
index += 1;
while (index < source.length) {
if (source[index] === "\\") {
index += 2;
continue;
}
if (source[index] === "\"") {
index += 1;
break;
}
index += 1;
}
if (source[index - 1] !== "\"") {
throw new Error("unterminated string literal");
}
tokens.push(source.slice(start, index));
continue;
}
const start = index;
while (
index < source.length &&
!/\s/.test(source[index]) &&
source[index] !== "(" &&
source[index] !== ")" &&
source[index] !== ";"
) {
index += 1;
}
tokens.push(source.slice(start, index));
}
return tokens;
}
function parseList(tokens, cursor) {
if (tokens[cursor.index] !== "(") {
throw new Error(`expected list at token ${cursor.index}`);
}
cursor.index += 1;
const list = [];
while (cursor.index < tokens.length && tokens[cursor.index] !== ")") {
if (tokens[cursor.index] === "(") {
list.push(parseList(tokens, cursor));
} else {
list.push(tokens[cursor.index]);
cursor.index += 1;
}
}
if (tokens[cursor.index] !== ")") {
throw new Error("unterminated list");
}
cursor.index += 1;
return list;
}
function topLevelForms(source, file) {
const tokens = tokenize(source);
const cursor = { index: 0 };
const forms = [];
while (cursor.index < tokens.length) {
if (tokens[cursor.index] !== "(") {
throw new Error(`${file}: expected top-level list at token ${cursor.index}`);
}
forms.push(parseList(tokens, cursor));
}
return forms;
}
function moduleForm(forms, file) {
const form = forms.find((item) => Array.isArray(item) && item[0] === "module");
if (!form) {
throw new Error(`${file}: expected module form`);
}
if (form[0] !== "module" || typeof form[1] !== "string") {
throw new Error(`${file}: expected (module name ...) form`);
}
const exportForm = form.find((item) => Array.isArray(item) && item[0] === "export");
if (!exportForm) {
throw new Error(`${file}: module must have an explicit export list`);
}
return {
name: form[1],
exports: exportForm.slice(1),
};
}
function renderType(type, aliases, stack = []) {
if (Array.isArray(type)) {
return `(${type.map((part) => renderType(part, aliases, stack)).join(" ")})`;
}
if (aliases.has(type)) {
if (stack.includes(type)) {
throw new Error(`cyclic type alias while rendering ${stack.concat(type).join(" -> ")}`);
}
return renderType(aliases.get(type), aliases, stack.concat(type));
}
return type;
}
function functionSignature(fnForm, aliases, file) {
if (fnForm.length < 5 || typeof fnForm[1] !== "string" || !Array.isArray(fnForm[2]) || fnForm[3] !== "->") {
throw new Error(`${file}: malformed (fn ...) form for ${fnForm[1] || "<unknown>"}`);
}
const name = fnForm[1];
const params = fnForm[2].map((param) => {
if (!Array.isArray(param) || param.length !== 2 || typeof param[0] !== "string") {
throw new Error(`${file}: malformed parameter in ${name}`);
}
return `(${param[0]} ${renderType(param[1], aliases)})`;
});
const renderedParams = params.length === 0 ? "()" : `(${params.join(" ")})`;
const returnType = renderType(fnForm[4], aliases);
return `${name} ${renderedParams} -> ${returnType}`;
}
function indexForms(forms, file) {
const aliases = new Map();
const functions = new Map();
for (const form of forms) {
if (!Array.isArray(form) || form.length < 1) {
continue;
}
if (form[0] === "type") {
if (form.length !== 3 || typeof form[1] !== "string") {
throw new Error(`${file}: malformed (type ...) form`);
}
if (aliases.has(form[1]) || functions.has(form[1])) {
throw new Error(`${file}: duplicate public name ${form[1]}`);
}
aliases.set(form[1], form[2]);
}
if (form[0] === "fn") {
if (typeof form[1] !== "string") {
throw new Error(`${file}: malformed (fn ...) form`);
}
if (functions.has(form[1]) || aliases.has(form[1])) {
throw new Error(`${file}: duplicate public name ${form[1]}`);
}
functions.set(form[1], form);
}
}
return { aliases, functions };
}
function stdModules() {
return fs
.readdirSync(stdDir)
.filter((name) => name.endsWith(".slo"))
.sort()
.map((fileName) => {
const relativePath = path.join("lib", "std", fileName);
const source = fs.readFileSync(path.join(stdDir, fileName), "utf8");
const forms = topLevelForms(source, relativePath);
const module = moduleForm(forms, relativePath);
const { aliases, functions } = indexForms(forms, relativePath);
const omittedAliasExports = [];
const signatures = [];
for (const exported of module.exports) {
if (functions.has(exported)) {
signatures.push(functionSignature(functions.get(exported), aliases, relativePath));
} else if (aliases.has(exported)) {
omittedAliasExports.push(exported);
} else {
throw new Error(`${relativePath}: export ${exported} has no matching (fn ...) form`);
}
}
return {
fileName,
relativePath,
name: module.name,
signatures,
omittedAliasExports,
};
});
}
function releaseVersion() {
const readme = fs.readFileSync(readmePath, "utf8");
const versionMatch = readme.match(/^Current release:\s*`([^`]+)`\.$/m);
if (!versionMatch) {
throw new Error("README.md must declare the current release");
}
return versionMatch[1];
}
function render(modules, version) {
const totalExports = modules.reduce((count, module) => count + module.signatures.length, 0);
const totalOmittedAliases = modules.reduce((count, module) => count + module.omittedAliasExports.length, 0);
const out = [];
out.push("# Slovo Standard Library API Catalog");
out.push("");
out.push("Generated from `lib/std/*.slo` by `scripts/render-stdlib-api-doc.js`.");
out.push("Do not edit this file by hand.");
out.push("");
out.push("## Stability Tiers");
out.push("");
out.push("- `beta-supported`: exported from `lib/std` and covered by source-search, promotion, or facade gates in the current beta line.");
out.push(`- \`experimental\`: not used for exported \`lib/std\` helpers in \`${version}\`; future releases may mark new helpers this way before they graduate.`);
out.push("- `internal`: helper names that are not exported from their module; they are intentionally omitted from this catalog.");
out.push("");
out.push("The catalog is a beta API discovery aid, not a stable `1.0.0` standard-library freeze.");
out.push("Module-local concrete aliases are normalized in signatures so names such as `VecI32` and `ResultU64` do not leak into the public catalog.");
out.push("Only exported `(fn ...)` helpers are listed; `(type ...)` aliases and non-exported helpers are omitted.");
out.push("");
out.push("## Summary");
out.push("");
out.push(`- Modules: ${modules.length}`);
out.push(`- Exported helper signatures: ${totalExports}`);
out.push(`- Exported type aliases omitted: ${totalOmittedAliases}`);
out.push("- Default tier: `beta-supported`");
out.push("");
out.push("## Modules");
out.push("");
for (const module of modules) {
out.push(`### std.${module.name}`);
out.push("");
out.push(`- Path: \`${module.relativePath}\``);
out.push("- Tier: `beta-supported`");
out.push(`- Exported helper signatures: ${module.signatures.length}`);
out.push("");
for (const signature of module.signatures) {
out.push(`- \`${signature}\``);
}
out.push("");
}
return `${out.join("\n")}\n`;
}
fs.writeFileSync(outputPath, render(stdModules(), releaseVersion()));