Skip to main content

aura_lang/
agentdocs.rs

1//! The language, rendered for a coding assistant: `aura docs --agent`.
2//!
3//! An assistant asked to write a manifest in a language it has not memorised will
4//! guess, and guesses come out looking like YAML or Python. Aura is small enough
5//! that the alternative is practical — the whole language fits in a few thousand
6//! tokens, which is not true of the larger configuration languages.
7//!
8//! Everything here is assembled from what already exists: the keyword list the
9//! lexer reserves, the stdlib manifest the language server builds completion
10//! from, and the diagnostic catalogue a test keeps in step with the compiler. The
11//! prose that cannot be derived — the `:` against `=` distinction, the capability
12//! model, the mistakes people actually make — lives in `agent-preamble.md`.
13//!
14//! Assembling rather than writing is the whole point. A hand-maintained copy of
15//! the method list would be a second source of truth, and this repository has
16//! spent enough time discovering what those turn into.
17
18/// Prose that no generator could produce, kept as Markdown so it is readable on
19/// its own.
20const PREAMBLE: &str = include_str!("../agent-preamble.md");
21
22/// The diagnostic catalogue, which lives in this crate rather than in the book:
23/// it describes the compiler, and `include_str!` cannot reach outside a package —
24/// pointing it at the book made `cargo publish` fail to build the tarball. The
25/// book now includes this file instead, so there is still one copy.
26///
27/// `tests/diagnostic_catalogue.rs` asserts it lists exactly the codes the
28/// compiler can emit, so quoting it here cannot introduce a claim the compiler
29/// does not honour.
30const DIAGNOSTICS: &str = include_str!("../diagnostics.md");
31
32/// Render the complete reference.
33pub fn render() -> String {
34    let mut out = String::with_capacity(32 * 1024);
35
36    out.push_str("# Aura ");
37    out.push_str(env!("CARGO_PKG_VERSION"));
38    out.push_str(" — language reference for coding assistants\n\n");
39    out.push_str(
40        "Generated by `aura docs --agent` from the compiler's own definitions, so it \
41         describes the binary that produced it. Regenerate rather than edit.\n\n",
42    );
43
44    out.push_str(PREAMBLE.trim_end());
45    out.push_str("\n\n## Keywords\n\nAll ");
46    out.push_str(&crate::lexer::token::KEYWORDS.len().to_string());
47    out.push_str(" reserved words:\n\n");
48    out.push_str(&crate::lexer::token::KEYWORDS.join(", "));
49    out.push_str(
50        ".\n\nPlus `text`, which is *not* reserved: it opens a block string only \
51         where a value is expected, and is an ordinary identifier everywhere \
52         else.\n",
53    );
54
55    out.push_str("\n## Standard library\n\n");
56    out.push_str(
57        "Methods are called on a value (`\"a\".upper()`, `xs.map(f)`); builtins are \
58         global functions.\n\n",
59    );
60    out.push_str(&stdlib_section());
61
62    out.push_str("\n## Diagnostics\n\n");
63    out.push_str(
64        "Every code is stable. When one is reported, the fix is in this table rather \
65         than in guesswork.\n\n",
66    );
67    out.push_str(strip_front_matter(DIAGNOSTICS).trim_end());
68    out.push('\n');
69
70    out
71}
72
73/// Evaluate the stdlib manifest and render it as tables grouped by receiver.
74///
75/// The manifest is Aura, evaluated by Aura — the same dogfooding the language
76/// server does. If it ever fails to evaluate, that is a broken manifest and the
77/// test suite says so; here it degrades to a note rather than an empty section
78/// pretending the standard library is empty.
79fn stdlib_section() -> String {
80    let Ok(json) = eval_manifest(crate::STDLIB_MANIFEST) else {
81        return "_The stdlib manifest failed to evaluate; see crates/aura-lang/stdlib.aura._\n"
82            .to_string();
83    };
84    let Some(groups) = json.as_object() else {
85        return String::new();
86    };
87
88    let mut out = String::new();
89    for (receiver, methods) in groups {
90        let Some(methods) = methods.as_object() else {
91            continue;
92        };
93        out.push_str("### ");
94        out.push_str(if receiver == "builtins" {
95            "Builtins"
96        } else {
97            receiver
98        });
99        out.push_str("\n\n| Signature | |\n| --- | --- |\n");
100        for (name, spec) in methods {
101            let params = spec
102                .get("params")
103                .and_then(|p| p.as_array())
104                .map(|xs| {
105                    xs.iter()
106                        .filter_map(|v| v.as_str())
107                        .collect::<Vec<_>>()
108                        .join(", ")
109                })
110                .unwrap_or_default();
111            let returns = spec.get("returns").and_then(|v| v.as_str()).unwrap_or("?");
112            let doc = spec.get("doc").and_then(|v| v.as_str()).unwrap_or("");
113            out.push_str(&format!("| `{name}({params}) -> {returns}` | {doc} |\n"));
114        }
115        out.push('\n');
116    }
117    out
118}
119
120fn eval_manifest(src: &str) -> Result<serde_json::Value, ()> {
121    let tokens = crate::lexer::Lexer::new(src, 0)
122        .tokenize()
123        .map_err(|_| ())?;
124    let module = crate::parser::Parser::new(tokens)
125        .parse_module()
126        .map_err(|_| ())?;
127    let mut interp = crate::eval::Interpreter::new(crate::eval::Options {
128        strict: false,
129        dry_run: false,
130    });
131    let value = interp.eval_module(&module).map_err(|_| ())?;
132    crate::serialize::to_json(&value).map_err(|_| ())
133}
134
135/// Drop the catalogue's own title and preamble: this document supplies its own,
136/// and the heading levels have to nest under `## Diagnostics`.
137fn strip_front_matter(md: &str) -> String {
138    let body = md
139        .split_once("\n## ")
140        .map_or(md, |(_, rest)| rest)
141        .to_string();
142    // The remaining `## Lexing (E01xx)` headings become `###` one level down.
143    let mut out = String::from("### ");
144    out.push_str(&body.replace("\n## ", "\n### "));
145    out
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    /// The generated reference has to actually contain the things it promises,
153    /// or it is a confidently wrong document — worse for an assistant than no
154    /// document at all.
155    #[test]
156    fn the_reference_carries_every_section_it_claims() {
157        let doc = render();
158        for needle in [
159            "## Keywords",
160            "## Standard library",
161            "## Diagnostics",
162            "### Builtins",
163            "`:` exports, `=` does not",
164        ] {
165            assert!(doc.contains(needle), "missing from the reference: {needle}");
166        }
167    }
168
169    /// Specifically: the stdlib section must be built from the manifest, not
170    /// silently degraded. A generator that quietly emits nothing would still pass
171    /// a "contains the heading" check.
172    #[test]
173    fn the_stdlib_section_is_populated_from_the_manifest() {
174        let doc = render();
175        assert!(
176            !doc.contains("failed to evaluate"),
177            "the stdlib manifest did not evaluate"
178        );
179        for needle in ["`upper() -> String`", "`map(fn) -> List`", "### String"] {
180            assert!(
181                doc.contains(needle),
182                "expected {needle} in the stdlib tables"
183            );
184        }
185    }
186
187    /// Every keyword the lexer reserves is listed. An assistant that has never
188    /// seen `shadow` will not invent it.
189    #[test]
190    fn every_reserved_word_is_listed() {
191        let doc = render();
192        for kw in crate::lexer::token::KEYWORDS {
193            assert!(doc.contains(kw), "keyword absent from the reference: {kw}");
194        }
195    }
196}