Skip to main content

bock_lsp/
pipeline.rs

1//! Run the Bock `check` pipeline on a single in-memory document.
2//!
3//! The LSP processes one document at a time — cross-file name resolution is
4//! not performed here because the client only hands us the current buffer's
5//! contents. A full workspace build happens via `bock check`; this path is
6//! scoped to fast per-keystroke analysis.
7
8use std::path::PathBuf;
9
10use bock_air::{lower_module, resolve_names_with_registry, ModuleRegistry, NodeIdGen, SymbolTable};
11use bock_errors::{Diagnostic, DiagnosticBag};
12use bock_lexer::Lexer;
13use bock_parser::Parser;
14use bock_source::SourceMap;
15use bock_types::{seed_imports, FnType, PrimitiveType, Strictness, Type, TypeChecker};
16
17/// Result of running the check pipeline on a single document.
18pub struct CheckResult {
19    /// Owned source map containing the document (keeps `SourceFile`
20    /// borrows valid for the lifetime of the result).
21    pub source_map: SourceMap,
22    /// Id of the added file inside [`CheckResult::source_map`].
23    pub file_id: bock_errors::FileId,
24    /// All diagnostics produced by any pipeline stage.
25    pub diagnostics: Vec<Diagnostic>,
26}
27
28/// Run lex → parse → resolve → lower → type-check → analyze on a document.
29///
30/// The pipeline short-circuits if lexing or parsing produces error-level
31/// diagnostics (those stages must succeed before later passes can run), but
32/// non-fatal stages downstream all contribute their diagnostics to the
33/// returned vector regardless of earlier warnings.
34#[must_use]
35pub fn check_document(path: PathBuf, content: String) -> CheckResult {
36    let mut source_map = SourceMap::new();
37    let file_id = source_map.add_file(path, content);
38    let mut diagnostics: Vec<Diagnostic> = Vec::new();
39
40    // Borrow the just-added file for the lexer. `SourceMap::add_file` appends
41    // to an internal Vec, so the reference stays valid as long as we don't
42    // add more files (we don't).
43    let source_file = source_map.get_file(file_id);
44
45    // 1. Lex
46    let mut lexer = Lexer::new(source_file);
47    let tokens = lexer.tokenize();
48    push_all(&mut diagnostics, lexer.diagnostics());
49
50    if has_errors(&diagnostics) {
51        return CheckResult {
52            source_map,
53            file_id,
54            diagnostics,
55        };
56    }
57
58    // 2. Parse
59    let mut parser = Parser::new(tokens, source_file);
60    let module = parser.parse_module();
61    push_all(&mut diagnostics, parser.diagnostics());
62
63    if has_errors(&diagnostics) {
64        return CheckResult {
65            source_map,
66            file_id,
67            diagnostics,
68        };
69    }
70
71    // 3. Resolve names (empty registry — single-file check)
72    let registry = ModuleRegistry::new();
73    let mut symbols = SymbolTable::new();
74    let resolve_diags = resolve_names_with_registry(&module, &mut symbols, &registry);
75    push_all(&mut diagnostics, &resolve_diags);
76
77    if has_errors(&diagnostics) {
78        return CheckResult {
79            source_map,
80            file_id,
81            diagnostics,
82        };
83    }
84
85    // 4. Lower to S-AIR
86    let id_gen = NodeIdGen::new();
87    let mut air_module = lower_module(&module, &id_gen, &symbols);
88
89    // 5. Type check
90    let mut checker = TypeChecker::new();
91    register_builtins(&mut checker);
92    seed_imports(&mut checker, &module.imports, &registry);
93    checker.check_module(&mut air_module);
94    push_all(&mut diagnostics, &checker.diags);
95
96    // 6. Analysis passes (always run — they are useful even with type errors)
97    let ownership_diags = bock_types::analyze_ownership(&air_module);
98    push_all(&mut diagnostics, &ownership_diags);
99
100    let strictness = Strictness::Development;
101    let effect_diags = bock_types::track_effects(&air_module, strictness);
102    push_all(&mut diagnostics, &effect_diags);
103
104    let capability_diags = bock_types::verify_capabilities(&air_module, strictness);
105    push_all(&mut diagnostics, &capability_diags);
106
107    CheckResult {
108        source_map,
109        file_id,
110        diagnostics,
111    }
112}
113
114fn push_all(acc: &mut Vec<Diagnostic>, bag: &DiagnosticBag) {
115    for diag in bag.iter() {
116        acc.push(diag.clone());
117    }
118}
119
120fn has_errors(diagnostics: &[Diagnostic]) -> bool {
121    diagnostics
122        .iter()
123        .any(|d| d.severity == bock_errors::Severity::Error)
124}
125
126/// Define the prelude builtins expected by hand-written Bock programs.
127///
128/// Kept in sync with `bock-cli`'s `register_type_builtins` — the LSP must
129/// treat the same set of identifiers as predefined, otherwise buffers that
130/// type-check on disk would show spurious "undefined variable" diagnostics
131/// in the editor.
132fn register_builtins(checker: &mut TypeChecker) {
133    let io_fn_ty = Type::Function(FnType {
134        params: vec![Type::Primitive(PrimitiveType::String)],
135        ret: Box::new(Type::Primitive(PrimitiveType::Void)),
136        effects: vec![],
137    });
138    for name in ["print", "println", "debug"] {
139        checker.env.define(name, io_fn_ty.clone());
140    }
141
142    let assert_ty = Type::Function(FnType {
143        params: vec![Type::Primitive(PrimitiveType::Bool)],
144        ret: Box::new(Type::Primitive(PrimitiveType::Void)),
145        effects: vec![],
146    });
147    checker.env.define("assert", assert_ty);
148
149    let expect_ty = Type::Function(FnType {
150        params: vec![Type::Error],
151        ret: Box::new(Type::Error),
152        effects: vec![],
153    });
154    checker.env.define("expect", expect_ty);
155
156    let never_fn_ty = Type::Function(FnType {
157        params: vec![],
158        ret: Box::new(Type::Primitive(PrimitiveType::Never)),
159        effects: vec![],
160    });
161    for name in ["todo", "unreachable"] {
162        checker.env.define(name, never_fn_ty.clone());
163    }
164
165    let constructor_ty = Type::Function(FnType {
166        params: vec![Type::Error],
167        ret: Box::new(Type::Error),
168        effects: vec![],
169    });
170    for name in ["Ok", "Err", "Some"] {
171        checker.env.define(name, constructor_ty.clone());
172    }
173    checker.env.define("None", Type::Error);
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn valid_empty_module_has_no_errors() {
182        let src = "module m\n";
183        let result = check_document(PathBuf::from("test.bock"), src.to_string());
184        let errors: Vec<_> = result
185            .diagnostics
186            .iter()
187            .filter(|d| d.severity == bock_errors::Severity::Error)
188            .collect();
189        assert!(errors.is_empty(), "unexpected errors: {errors:#?}");
190    }
191
192    #[test]
193    fn syntax_error_produces_diagnostic() {
194        // Missing `=` should produce a parse error.
195        let src = "module m\nlet x 1\n";
196        let result = check_document(PathBuf::from("test.bock"), src.to_string());
197        assert!(
198            result
199                .diagnostics
200                .iter()
201                .any(|d| d.severity == bock_errors::Severity::Error),
202            "expected at least one error diagnostic",
203        );
204    }
205}