#!/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"); const TIERS = Object.freeze({ BETA_SUPPORTED: "beta-supported", EXPERIMENTAL: "experimental", INTERNAL: "internal", }); const EXPERIMENTAL_MODULES = new Set(["json", "net", "random", "time"]); const EXPERIMENTAL_HELPERS = new Map([ [ "fs", new Set([ "open_text_read_result", "read_open_text_result", "close_result", "read_text_via_handle_result", "close_ok", ]), ], ]); const CONCRETE_VEC_MODULES = new Set(["vec_bool", "vec_f64", "vec_i32", "vec_i64", "vec_string"]); function helperTier(moduleName, exportName) { if (EXPERIMENTAL_MODULES.has(moduleName)) { return TIERS.EXPERIMENTAL; } const experimentalHelpers = EXPERIMENTAL_HELPERS.get(moduleName); if (experimentalHelpers && experimentalHelpers.has(exportName)) { return TIERS.EXPERIMENTAL; } return TIERS.BETA_SUPPORTED; } function uniqueSorted(values) { return Array.from(new Set(values)).sort(); } 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] || ""}`); } 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({ name: exported, signature: functionSignature(functions.get(exported), aliases, relativePath), tier: helperTier(module.name, exported), }); } 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, tiers: uniqueSorted(signatures.map((signature) => signature.tier)), 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 tierCounts = new Map([ [TIERS.BETA_SUPPORTED, 0], [TIERS.EXPERIMENTAL, 0], ]); for (const module of modules) { for (const signature of module.signatures) { tierCounts.set(signature.tier, (tierCounts.get(signature.tier) || 0) + 1); } } 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\`: exported from \`lib/std\` in \`${version}\`, but still has beta caveats around host behavior, resource handles, or API shape.`); 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("Concrete `std.vec_*` modules are beta-supported as concrete helper families only; this does not freeze a generic collection API."); 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(`- \`${TIERS.BETA_SUPPORTED}\` helper signatures: ${tierCounts.get(TIERS.BETA_SUPPORTED) || 0}`); out.push(`- \`${TIERS.EXPERIMENTAL}\` helper signatures: ${tierCounts.get(TIERS.EXPERIMENTAL) || 0}`); 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(`- Tiers: ${module.tiers.map((tier) => `\`${tier}\``).join(", ")}`); out.push(`- Exported helper signatures: ${module.signatures.length}`); if (CONCRETE_VEC_MODULES.has(module.name)) { out.push("- Note: concrete-only vector helper family; no generic collection freeze."); } out.push(""); for (const { signature, tier } of module.signatures) { out.push(`- \`${tier}\` \`${signature}\``); } out.push(""); } return `${out.join("\n")}\n`; } fs.writeFileSync(outputPath, render(stdModules(), releaseVersion()));