1use std::collections::BTreeMap;
12
13pub mod schema;
14
15pub use schema::Vocab;
16
17use bock_errors::Severity;
18
19pub const COMPILER_VERSION: &str = env!("CARGO_PKG_VERSION");
21
22#[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
37fn 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
148fn build_stdlib() -> schema::StdlibVocab {
151 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 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
219fn 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
248fn 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#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn version_matches_workspace() {
309 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}