Iter 13f: rustdoc polish for ailang-codegen + ail CLI
Third docwriter mission (combined). Pure rustdoc additions, no API or behaviour change. ailang-codegen (113 LOC of doc): - Crate root: intra-doc links (emit_ir, lower_workspace, CodegenError::MissingEntryMain) + precondition note (both entry points assume type-checked input). - /// on CodegenError + every variant, naming AST trigger. - /// on emit_ir and lower_workspace (single vs multi-module split, cross-linked). ail CLI (49 LOC of doc): - Module-level //! expanded from 5 lines to full subcommand list with one-liners (11 subcommands verified against Cmd enum), clang-on-PATH note, design-intent paragraph. Verification: cargo doc --no-deps zero warnings (also under RUSTDOCFLAGS='-D rustdoc::broken_intra_doc_links'); build green; tests 64/64 + 3 ignored doctests green. Diff is 100% doc lines (verified by filtering). Findings (not fixed; orchestrator-deferred): - CodegenError::Internal is one catch-all variant for ~30 invariant-violation sites; splitting would help test ergonomics but is out of doc scope. - emit_ir synthesises a Workspace with root_dir="."; harmless today, surfaces if codegen ever reads root_dir. Process note: my brief said the CLI had 9 subcommands; agent found and documented 11. Useful counter-pressure on orchestrator sloppiness — recorded in JOURNAL. Workspace-wide rustdoc invariant (DESIGN.md item 6) now load-bearing across all four crates. Docwriter shifts from sweep to maintenance mode going forward. Next: 14a (polymorphic list_map using Iter-13a parameterised ADTs) is unblocked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
//! LLVM IR text emitter for AILang (MVP).
|
||||
//!
|
||||
//! Third stage of the compiler pipeline (`core` → `check` → `codegen`
|
||||
//! → `ail` CLI). Consumes a fully type-checked [`Module`] (single-file
|
||||
//! mode) or [`Workspace`] (multi-module mode) and produces LLVM IR as
|
||||
//! a UTF-8 string ready to be written as a `.ll` file and handed to
|
||||
//! `clang`. The two entry points are [`emit_ir`] (single module) and
|
||||
//! [`lower_workspace`] (multi-module); both share the same mangling
|
||||
//! scheme and ABI.
|
||||
//!
|
||||
//! Strategy: we generate LLVM IR as a string, write it as `.ll`, and
|
||||
//! link it with `clang`. No binding to a specific libllvm version.
|
||||
//!
|
||||
@@ -13,47 +21,113 @@
|
||||
//! even for single-module programs. The old form `@ail_<def>` is gone.
|
||||
//! - Global string/constant symbols are mangled per module:
|
||||
//! `@.str_<module>_<idx>` and `@ail_<module>_<def>` for constant globals.
|
||||
//! - The entry point remains `main` (LLVM/C ABI). The generator emits
|
||||
//! `define i64 @main() { call @ail_<entry-module>_main() ... }` as a
|
||||
//! trampoline to the entry module's `main`. If missing, the build
|
||||
//! fails with `MissingEntryMain`.
|
||||
//! - The entry point remains `main` (LLVM/C ABI). [`lower_workspace`]
|
||||
//! emits `define i64 @main() { call @ail_<entry-module>_main() ... }`
|
||||
//! as a trampoline to the entry module's `main`. If missing, the
|
||||
//! build fails with [`CodegenError::MissingEntryMain`].
|
||||
//! - `source_filename` appears exactly once at the top, with
|
||||
//! `<entry-module>.ail` as value (per workspace).
|
||||
//!
|
||||
//! **Precondition.** Neither [`emit_ir`] nor [`lower_workspace`] runs
|
||||
//! the typechecker. Callers must have run `ailang_check::check_module`
|
||||
//! (or `check_workspace`) first; codegen will panic or emit malformed
|
||||
//! IR if invariants the checker enforces (resolved metavars, declared
|
||||
//! effects, ctor arity) are violated.
|
||||
|
||||
use ailang_core::ast::*;
|
||||
use ailang_core::Workspace;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// Failure modes of [`emit_ir`] / [`lower_workspace`].
|
||||
///
|
||||
/// Most variants signal a compiler invariant violation rather than a
|
||||
/// user-facing diagnostic — by the time a module reaches codegen the
|
||||
/// typechecker has already accepted it. The exceptions are
|
||||
/// [`CodegenError::MissingEntryMain`] (a workspace-level shape check
|
||||
/// that the typechecker doesn't enforce) and the wrapping variants
|
||||
/// [`CodegenError::Def`] / [`CodegenError::InModule`] which add path
|
||||
/// context to an inner error.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CodegenError {
|
||||
/// Wraps an inner error with the name of the def being lowered.
|
||||
/// Attached by the per-def lowering loop in [`lower_workspace`] so
|
||||
/// the failing definition is named in the message even when the
|
||||
/// underlying error is structural (e.g. an [`CodegenError::Internal`]
|
||||
/// from deep inside `lower_term`).
|
||||
#[error("def `{0}`: {1}")]
|
||||
Def(String, Box<CodegenError>),
|
||||
|
||||
/// Wraps an inner error with the name of the module being lowered.
|
||||
/// Attached by [`lower_workspace`]'s per-module loop so multi-module
|
||||
/// builds report which module failed without requiring the caller
|
||||
/// to thread a module name through every call site.
|
||||
#[error("module `{0}`: {1}")]
|
||||
InModule(String, Box<CodegenError>),
|
||||
|
||||
/// `llvm_type` was asked to lower an AILang [`Type`] it does not
|
||||
/// know how to represent. In the MVP this fires for an unresolved
|
||||
/// rigid `Type::Var` reaching codegen (a substitution bug; see
|
||||
/// Iter 13b notes in `DESIGN.md`) or for any non-`Con`/`Fn`/`Var`
|
||||
/// shape that has not yet been wired through.
|
||||
#[error("unsupported type: {0}")]
|
||||
UnsupportedType(String),
|
||||
|
||||
/// A `Term::Var { name }` could not be resolved against the local
|
||||
/// SSA stack, the current module's top-level fns, or a qualified
|
||||
/// import. A correctly type-checked module never produces this; if
|
||||
/// it does, the typechecker and the codegen-side resolver have
|
||||
/// drifted out of sync.
|
||||
#[error("unknown variable: `{0}`")]
|
||||
UnknownVar(String),
|
||||
|
||||
/// A `Def::Fn` was reached whose `ty` is not a `Type::Fn`. The
|
||||
/// typechecker ([`ailang_check::CheckError::FnTypeRequired`]) should
|
||||
/// have rejected this case before us; emit_fn re-checks defensively
|
||||
/// because a stale typechecker contract would otherwise produce
|
||||
/// malformed IR.
|
||||
#[error("expected fn type, got {0}")]
|
||||
NotFnType(String),
|
||||
|
||||
/// The entry module of the workspace has no `main : () -> Unit !IO`
|
||||
/// def, so [`lower_workspace`] cannot emit the C-ABI trampoline.
|
||||
/// This is a workspace-level shape requirement that the typechecker
|
||||
/// does not enforce (a library module is well-typed without a main),
|
||||
/// so it surfaces here instead.
|
||||
#[error("entry module `{0}` has no `main` def")]
|
||||
MissingEntryMain(String),
|
||||
|
||||
/// Catch-all for codegen-side invariant violations: missing
|
||||
/// ctor entry, lambda environment shape mismatch, mono-queue
|
||||
/// inconsistency, etc. The string carries the precise diagnostic;
|
||||
/// a user-facing build never produces this if the workspace
|
||||
/// type-checks cleanly.
|
||||
#[error("internal: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, CodegenError>;
|
||||
|
||||
/// Lowers a single module. Backwards compatibility for tests / CLI calls
|
||||
/// that want to operate without a workspace. Internally a trivial
|
||||
/// workspace with just this module is built, so the mangling scheme
|
||||
/// stays consistent.
|
||||
/// Single-module entry point. Lowers `m` to a `.ll` string with `m`
|
||||
/// itself as the entry module. Returns the full LLVM IR text, ready to
|
||||
/// be written to disk and handed to `clang`.
|
||||
///
|
||||
/// Used by tests, by `ail emit-ir`, and by `ail build`/`run` whenever
|
||||
/// the input is a single `.ail.json` file rather than a workspace.
|
||||
/// Internally builds a trivial [`Workspace`] containing only `m` and
|
||||
/// delegates to [`lower_workspace`] — the mangling scheme,
|
||||
/// `source_filename`, and the `@main` trampoline are therefore
|
||||
/// identical between the two entry points.
|
||||
///
|
||||
/// **Precondition.** `m` must already type-check. This function does
|
||||
/// **not** call `ailang_check`; passing a module with unresolved
|
||||
/// metavars, undeclared effects, or arity mismatches will produce
|
||||
/// either a [`CodegenError`] or malformed IR. For any input that came
|
||||
/// from disk, run `ailang_check::check_module(m)` first and only call
|
||||
/// `emit_ir` when the diagnostic list is empty.
|
||||
///
|
||||
/// Use [`lower_workspace`] instead when the program spans multiple
|
||||
/// modules (cross-module calls, transitive imports) — `emit_ir` is the
|
||||
/// short-cut for the single-file demo case.
|
||||
pub fn emit_ir(m: &Module) -> Result<String> {
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert(m.name.clone(), m.clone());
|
||||
@@ -65,14 +139,31 @@ pub fn emit_ir(m: &Module) -> Result<String> {
|
||||
lower_workspace(&ws)
|
||||
}
|
||||
|
||||
/// Lowers a whole workspace to a `.ll` string. Module order is
|
||||
/// alphabetic (BTreeMap order = deterministic). Within a module, def
|
||||
/// order matches the AST.
|
||||
/// Multi-module entry point. Lowers an entire [`Workspace`] (entry
|
||||
/// module plus its transitive imports, as produced by
|
||||
/// `ailang_core::load_workspace`) to a single `.ll` string and emits
|
||||
/// the C-ABI `@main` trampoline pointing at the entry module's `main`.
|
||||
/// This is what `ail build` and `ail run` call for any real
|
||||
/// multi-module program.
|
||||
///
|
||||
/// Module order is alphabetic (BTreeMap order = deterministic). Within
|
||||
/// a module, def order matches the AST.
|
||||
///
|
||||
/// Cross-module calls: `Term::Var { name }` with exactly one dot
|
||||
/// (`<prefix>.<def>`) is resolved via the calling module's import map
|
||||
/// to `@ail_<actual_module>_<def>`. Local var lookups (no dot) stay
|
||||
/// stack locals or local top-level defs of the current module.
|
||||
///
|
||||
/// **Precondition.** Every module in `ws.modules` must already
|
||||
/// type-check (`ailang_check::check_workspace(ws)` returns no errors).
|
||||
/// `lower_workspace` does **not** invoke the typechecker itself;
|
||||
/// running it on an unchecked workspace is a caller bug. Beyond
|
||||
/// type-checking, this function additionally requires that the entry
|
||||
/// module declares `main : () -> Unit !IO` — otherwise it returns
|
||||
/// [`CodegenError::MissingEntryMain`].
|
||||
///
|
||||
/// Use [`emit_ir`] for the single-file shortcut when there are no
|
||||
/// imports.
|
||||
pub fn lower_workspace(ws: &Workspace) -> Result<String> {
|
||||
let mut header = String::new();
|
||||
let mut body = String::new();
|
||||
|
||||
Reference in New Issue
Block a user