Skip to main content

bock_vocab/
lib.rs

1//! Bock vocabulary emitter.
2//!
3//! Produces a single JSON document describing the language surface area
4//! — keywords, operators, annotations, stdlib methods, diagnostic codes,
5//! target names, CLI commands — for downstream tooling (editor extensions,
6//! documentation sites). The contents are not hardcoded here: every
7//! section is produced by querying the source-of-truth registry in the
8//! relevant crate, so the extension's vocabulary stays in sync with the
9//! compiler.
10
11use std::collections::BTreeMap;
12
13pub mod schema;
14
15pub use schema::Vocab;
16
17use bock_errors::Severity;
18
19/// Compiler version string matching the workspace `Cargo.toml`.
20pub const COMPILER_VERSION: &str = env!("CARGO_PKG_VERSION");
21
22/// Build the complete vocabulary by querying each source crate's registry.
23///
24/// This is the single entry point. The returned [`Vocab`] is ready to
25/// serialize via `serde_json`.
26#[must_use]
27pub fn build_vocab() -> Vocab {
28    Vocab {
29        version: COMPILER_VERSION.to_string(),
30        language: build_language(),
31        stdlib: build_stdlib(),
32        diagnostics: build_diagnostics(),
33        tooling: build_tooling(),
34    }
35}
36
37// ─── Language ────────────────────────────────────────────────────────────────
38
39fn build_language() -> schema::LanguageVocab {
40    let keywords = bock_lexer::vocab::keywords()
41        .into_iter()
42        .map(|kw| schema::Keyword {
43            name: kw.text.to_string(),
44            category: kw.category.to_string(),
45            spec_ref: kw.spec_ref.map(String::from),
46        })
47        .collect();
48
49    let operators = bock_lexer::vocab::operators()
50        .into_iter()
51        .map(|op| schema::Operator {
52            symbol: op.symbol.to_string(),
53            precedence: op.precedence,
54            associativity: op.associativity.to_string(),
55            kind: op.kind.to_string(),
56            spec_ref: op.spec_ref.map(String::from),
57        })
58        .collect();
59
60    let annotations = bock_types::vocab::annotations()
61        .into_iter()
62        .map(|a| schema::Annotation {
63            name: a.name.to_string(),
64            params: a.params.to_string(),
65            purpose: a.purpose.to_string(),
66            spec_ref: a.spec_ref.map(String::from),
67        })
68        .collect();
69
70    let strictness_levels = bock_types::vocab::strictness_levels()
71        .into_iter()
72        .map(|s| schema::StrictnessLevel {
73            name: s.name.to_string(),
74            description: s.description.to_string(),
75            spec_ref: s.spec_ref.map(String::from),
76        })
77        .collect();
78
79    let primitive_types = bock_air::prelude_vocab::PRIMITIVE_TYPES
80        .iter()
81        .map(|name| schema::PrimitiveType {
82            name: (*name).to_string(),
83            spec_ref: Some("§2.1".into()),
84        })
85        .collect();
86
87    let prelude_types = bock_air::prelude_vocab::PRELUDE_TYPES
88        .iter()
89        .map(|name| schema::Symbol {
90            name: (*name).to_string(),
91            kind: "type".into(),
92            signature: (*name).to_string(),
93            doc: None,
94            spec_ref: None,
95            since: None,
96        })
97        .collect();
98
99    let prelude_functions = bock_air::prelude_vocab::PRELUDE_FUNCTIONS
100        .iter()
101        .map(|name| schema::Symbol {
102            name: (*name).to_string(),
103            kind: "function".into(),
104            signature: format!("{name}(..)"),
105            doc: None,
106            spec_ref: None,
107            since: None,
108        })
109        .collect();
110
111    let prelude_traits = bock_air::prelude_vocab::PRELUDE_TRAITS
112        .iter()
113        .map(|name| schema::Symbol {
114            name: (*name).to_string(),
115            kind: "trait".into(),
116            signature: (*name).to_string(),
117            doc: None,
118            spec_ref: None,
119            since: None,
120        })
121        .collect();
122
123    let prelude_constructors = bock_air::prelude_vocab::PRELUDE_CONSTRUCTORS
124        .iter()
125        .map(|name| schema::Symbol {
126            name: (*name).to_string(),
127            kind: "constructor".into(),
128            signature: (*name).to_string(),
129            doc: None,
130            spec_ref: None,
131            since: None,
132        })
133        .collect();
134
135    schema::LanguageVocab {
136        keywords,
137        operators,
138        annotations,
139        strictness_levels,
140        primitive_types,
141        prelude_types,
142        prelude_functions,
143        prelude_traits,
144        prelude_constructors,
145    }
146}
147
148// ─── Stdlib ──────────────────────────────────────────────────────────────────
149
150fn build_stdlib() -> schema::StdlibVocab {
151    // Populate a fresh BuiltinRegistry with the full core library, then
152    // introspect its dispatch table. This keeps the method list identical
153    // to what the runtime actually ships.
154    let mut registry = bock_interp::BuiltinRegistry::new();
155    registry.register_defaults();
156    bock_core::register_core(&mut registry);
157
158    let mut by_receiver: BTreeMap<String, Vec<String>> = BTreeMap::new();
159    for (tag, name) in registry.method_keys() {
160        by_receiver
161            .entry(tag.name().to_string())
162            .or_default()
163            .push(name.to_string());
164    }
165    for methods in by_receiver.values_mut() {
166        methods.sort();
167        methods.dedup();
168    }
169
170    let builtin_methods = by_receiver
171        .into_iter()
172        .map(|(receiver, methods)| schema::BuiltinMethodGroup { receiver, methods })
173        .collect();
174
175    let mut builtin_globals: Vec<String> = registry
176        .global_names()
177        .map(|s| s.to_string())
178        .collect();
179    builtin_globals.sort();
180    builtin_globals.dedup();
181
182    // Top-level core modules. These track the stub modules declared in
183    // `bock_core::lib.rs`; they are the namespaces editors should offer
184    // for completion, even though some are placeholders today.
185    let modules = vec![
186        core_module("core.primitives", "§14.1"),
187        core_module("core.collections", "§14.2"),
188        core_module("core.option_result", "§14.3"),
189        core_module("core.iterator", "§14.4"),
190        core_module("core.string_builder", "§14.5"),
191        core_module("core.time", "§14.6"),
192        core_module("core.concurrency", "§14.7"),
193        core_module("core.effect", "§14.8"),
194        core_module("core.error", "§14.9"),
195        core_module("core.math", "§14.10"),
196        core_module("core.memory", "§14.11"),
197        core_module("core.test", "§14.12"),
198        core_module("core.traits", "§14.13"),
199    ];
200
201    schema::StdlibVocab {
202        modules,
203        builtin_methods,
204        builtin_globals,
205    }
206}
207
208fn core_module(path: &str, spec_ref: &str) -> schema::Module {
209    schema::Module {
210        path: path.to_string(),
211        types: Vec::new(),
212        functions: Vec::new(),
213        effects: Vec::new(),
214        traits: Vec::new(),
215        spec_ref: Some(spec_ref.to_string()),
216    }
217}
218
219// ─── Diagnostics ─────────────────────────────────────────────────────────────
220
221fn build_diagnostics() -> schema::DiagnosticsVocab {
222    let codes = bock_errors::catalog::diagnostic_catalog()
223        .into_iter()
224        .map(|info| schema::DiagnosticCode {
225            code: info.code.to_string(),
226            severity: severity_name(info.severity).to_string(),
227            summary: info.summary.to_string(),
228            description: info.description.to_string(),
229            bad_example: None,
230            good_example: None,
231            spec_refs: info.spec_refs.iter().map(|s| (*s).to_string()).collect(),
232            related_codes: Vec::new(),
233        })
234        .collect();
235
236    schema::DiagnosticsVocab { codes }
237}
238
239fn severity_name(s: Severity) -> &'static str {
240    match s {
241        Severity::Error => "error",
242        Severity::Warning => "warning",
243        Severity::Info => "info",
244        Severity::Hint => "hint",
245    }
246}
247
248// ─── Tooling ─────────────────────────────────────────────────────────────────
249
250fn build_tooling() -> schema::ToolingVocab {
251    let targets = bock_codegen::profile::TargetProfile::all_builtins()
252        .into_iter()
253        .map(|p| schema::Target {
254            id: p.id,
255            display_name: p.display_name,
256        })
257        .collect();
258
259    let ai_providers = bock_ai::known_providers()
260        .iter()
261        .map(|s| (*s).to_string())
262        .collect();
263
264    let commands = command_catalog()
265        .into_iter()
266        .map(|(name, summary)| schema::Command {
267            name: name.to_string(),
268            summary: summary.to_string(),
269        })
270        .collect();
271
272    schema::ToolingVocab {
273        targets,
274        ai_providers,
275        commands,
276    }
277}
278
279fn command_catalog() -> Vec<(&'static str, &'static str)> {
280    vec![
281        ("new", "Scaffold a new Bock project."),
282        ("build", "Transpile and compile a Bock project."),
283        ("run", "Execute a Bock program via the interpreter."),
284        ("check", "Type-check and lint without building."),
285        ("test", "Run tests."),
286        ("fmt", "Format Bock source files."),
287        ("repl", "Start an interactive REPL session."),
288        ("inspect", "Browse AI decisions, rule cache, and AI response cache."),
289        ("pin", "Pin AI decisions so they replay deterministically."),
290        ("unpin", "Clear pin metadata from a decision."),
291        ("override", "Override or promote an AI decision."),
292        ("cache", "Manage on-disk AI, decision, and rule caches."),
293        ("promote", "Analyze a project at the next strictness level."),
294        ("pkg", "Package manager commands."),
295        ("model", "Query or interact with AI models."),
296        ("doc", "Generate documentation."),
297        ("lsp", "Start the Bock language server."),
298    ]
299}
300
301// ─── Tests ───────────────────────────────────────────────────────────────────
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn version_matches_workspace() {
309        // This is the source of truth for the compiler version.
310        assert_eq!(COMPILER_VERSION, env!("CARGO_PKG_VERSION"));
311    }
312
313    #[test]
314    fn language_sections_non_empty() {
315        let v = build_language();
316        assert!(!v.keywords.is_empty(), "no keywords");
317        assert!(!v.operators.is_empty(), "no operators");
318        assert!(!v.annotations.is_empty(), "no annotations");
319        assert_eq!(v.strictness_levels.len(), 3);
320        assert!(!v.primitive_types.is_empty(), "no primitives");
321        assert!(!v.prelude_types.is_empty(), "no prelude types");
322        assert!(!v.prelude_functions.is_empty(), "no prelude fns");
323        assert!(!v.prelude_traits.is_empty(), "no prelude traits");
324        assert!(!v.prelude_constructors.is_empty(), "no prelude ctors");
325    }
326
327    #[test]
328    fn stdlib_section_non_empty() {
329        let v = build_stdlib();
330        assert!(!v.builtin_methods.is_empty(), "no builtin methods");
331        assert!(!v.builtin_globals.is_empty(), "no builtin globals");
332        assert!(!v.modules.is_empty(), "no modules");
333    }
334
335    #[test]
336    fn diagnostics_section_non_empty() {
337        let v = build_diagnostics();
338        assert!(!v.codes.is_empty(), "no diagnostic codes");
339    }
340
341    #[test]
342    fn tooling_section_non_empty() {
343        let v = build_tooling();
344        assert_eq!(v.targets.len(), 5, "expected 5 builtin targets");
345        assert!(!v.ai_providers.is_empty(), "no ai providers");
346        assert!(!v.commands.is_empty(), "no commands");
347    }
348
349    #[test]
350    fn vocab_round_trips_through_json() {
351        let vocab = build_vocab();
352        let json = serde_json::to_string(&vocab).expect("serialize");
353        let parsed: Vocab = serde_json::from_str(&json).expect("deserialize");
354        assert_eq!(vocab, parsed);
355    }
356
357    #[test]
358    fn vocab_pretty_json_is_parseable() {
359        let vocab = build_vocab();
360        let json = serde_json::to_string_pretty(&vocab).expect("serialize");
361        let _: Vocab = serde_json::from_str(&json).expect("deserialize");
362    }
363
364    #[test]
365    fn builtin_methods_contain_int_add() {
366        let v = build_stdlib();
367        let int_group = v
368            .builtin_methods
369            .iter()
370            .find(|g| g.receiver == "Int")
371            .expect("Int receiver group present");
372        assert!(int_group.methods.iter().any(|m| m == "add"));
373    }
374
375    #[test]
376    fn targets_cover_primary_set() {
377        let v = build_tooling();
378        let ids: Vec<_> = v.targets.iter().map(|t| t.id.as_str()).collect();
379        for expected in ["js", "ts", "python", "rust", "go"] {
380            assert!(ids.contains(&expected), "missing target {expected}");
381        }
382    }
383}