Skip to main content

datalove_datafun_parser/
lib.rs

1//! Datafun parser module.
2//!
3//! Parses datafun source text into AST nodes.
4
5mod state;
6mod statement;
7mod expr;
8mod literal;
9
10use rmx::prelude::*;
11
12use bct::{
13    input::Source,
14    module_graph::ModuleId,
15    bracer::{Bracer, TreeToken},
16    split,
17    text::{Text, TextSpan},
18    source_map,
19    lexer,
20    bracer,
21};
22
23use datalove_datafun_ast::ast;
24use bct::diagnostic::DiagnosticBuilder;
25use datalove_diagnostic::DiagnosticBuilderExt;
26use state::{Parser, ScriptCounters};
27
28use salsa::Database as Db;
29
30/// Parse a Source into a datafun script with span information.
31///
32/// For script parsing (no module context).
33#[salsa::tracked(returns(ref))]
34pub fn parse<'db>(
35    db: &'db dyn Db,
36    source: Source,
37) -> ast::ParseResult<'db> {
38    parse_with_module_id(db, source, None)
39}
40
41/// Parse a Source into a datafun script with span information and module context.
42///
43/// The module_id is used to give functions stable identity for memoization.
44pub fn parse_with_module_id<'db>(
45    db: &'db dyn Db,
46    source: Source,
47    module_id: Option<ModuleId<'db>>,
48) -> ast::ParseResult<'db> {
49    let chunk = source_map::basic_source_map(db, source);
50    let source_text = chunk.text(db);
51    let chunk_lex = lexer::lex_chunk(db, chunk);
52    let bracer = bracer::bracer(db, chunk_lex);
53    emit_bracer_errors(db, bracer, source_text);
54    parse_bracer(db, bracer, source_text, module_id)
55}
56
57/// Parse a Source as a single expression.
58#[salsa::tracked(returns(copy))]
59pub fn parse_expr<'db>(
60    db: &'db dyn Db,
61    source: Source,
62) -> ast::ExprFun<'db> {
63    let chunk = source_map::basic_source_map(db, source);
64    let source_text = chunk.text(db);
65    let chunk_lex = lexer::lex_chunk(db, chunk);
66    let bracer = bracer::bracer(db, chunk_lex);
67    emit_bracer_errors(db, bracer, source_text);
68    // Expressions don't have module context.
69    parse_bracer_expr(db, bracer, source_text)
70}
71
72fn parse_bracer_expr<'db>(
73    db: &'db dyn Db,
74    bracer: Bracer<'db>,
75    source_text: bct::text::Text<'db>,
76) -> ast::ExprFun<'db> {
77    // Expressions don't have module context.
78    let mut parser = Parser::from_branch_with_context(db, bracer.iter(db), source_text, None, None);
79    let expr = parser.parse_expr_full();
80    parser.error_if_not_exhausted();
81    expr
82}
83
84/// Emit parse diagnostics for bracer errors (unclosed, mismatched, or stray braces).
85fn emit_bracer_errors<'db>(
86    db: &'db dyn Db,
87    bracer: Bracer<'db>,
88    source_text: Text<'db>,
89) {
90    let chunk = bracer.chunk(db);
91    let tokens = chunk.tokens(db);
92
93    for (token_range, sigil) in bracer.errors(db) {
94        // Single-token range means stray closing brace.
95        // Multi-token range means unclosed opening brace.
96        let is_stray_close = token_range.len() == 1;
97
98        if is_stray_close {
99            // Stray closing brace - no matching open.
100            if let Some(token) = tokens.get(token_range.start) {
101                let span = token.span();
102                let ts = TextSpan::new(source_text, span);
103                DiagnosticBuilder::error(db, &format!("unmatched '{}'", sigil.as_str()))
104                    .code("P050")
105                    .primary_label(ts, &format!("this '{}' has no matching '{}'",
106                        sigil.as_str(), sigil.open_sigil().as_str()))
107                    .emit_parse();
108            }
109        } else {
110            // Unclosed opening brace.
111            if let Some(open_token) = tokens.get(token_range.start) {
112                let span = open_token.span();
113                let ts = TextSpan::new(source_text, span);
114                DiagnosticBuilder::error(db, &format!("unclosed '{}'", sigil.as_str()))
115                    .code("P051")
116                    .primary_label(ts, &format!("this '{}' is never closed",
117                        sigil.as_str()))
118                    .emit_parse();
119            }
120        }
121    }
122}
123
124fn parse_bracer<'db>(
125    db: &'db dyn Db,
126    bracer: Bracer<'db>,
127    source_text: Text<'db>,
128    module_id: Option<ModuleId<'db>>,
129) -> ast::ParseResult<'db> {
130    // Split into lines. A newline inside balanced braces is not a line break,
131    // since the bracer has already put it inside a branch.
132    let groups = split::split_lines(db, bracer.iter(db));
133
134    // A `;` separates two statements, so one with nothing before it separates
135    // nothing. A blank line is not the same thing and is left alone.
136    for written in split::stray_delimiters(&groups) {
137        split::stray_delimiter_error(db, source_text, &written, "statements")
138            .code("P032")
139            .emit_parse();
140    }
141
142    let lines = split::nonempty_groups(groups).into_iter();
143    let (statements, spans) = parse_statements(db, lines, source_text, module_id);
144    let parsed = ast::ParsedStatements { statements: std::sync::Arc::new(statements) };
145    ast::ParseResult {
146        parsed,
147        expr_spans: spans.expr_spans,
148        break_spans: spans.break_spans,
149        continue_spans: spans.continue_spans,
150        ret_spans: spans.ret_spans,
151        set_spans: spans.set_spans,
152        fun_spans: spans.fun_spans,
153        type_alias_spans: spans.type_alias_spans,
154        import_spans: spans.import_spans,
155        alias_spans: spans.alias_spans,
156    }
157}
158
159/// Parsed statement spans result.
160struct ParsedSpans<'db> {
161    expr_spans: Vec<ast::ParseSpanEntry<'db>>,
162    break_spans: Vec<bct::diagnostic::SpanEntry>,
163    continue_spans: Vec<bct::diagnostic::SpanEntry>,
164    ret_spans: Vec<bct::diagnostic::SpanEntry>,
165    set_spans: Vec<bct::diagnostic::SpanEntry>,
166    fun_spans: Vec<bct::diagnostic::SpanEntry>,
167    type_alias_spans: Vec<bct::diagnostic::SpanEntry>,
168    import_spans: Vec<bct::diagnostic::SpanEntry>,
169    alias_spans: Vec<bct::diagnostic::SpanEntry>,
170}
171
172/// Parse statements from lines, creating a Parser for each line.
173///
174/// Returns statements and accumulated spans.
175fn parse_statements<'db>(
176    db: &'db dyn Db,
177    lines: impl Iterator<Item = Vec<TreeToken<'db>>>,
178    source_text: bct::text::Text<'db>,
179    module_id: Option<ModuleId<'db>>,
180) -> (Vec<ast::Statement<'db>>, ParsedSpans<'db>) {
181    let mut statements = vec![];
182    let mut all_expr_spans = vec![];
183    let mut all_break_spans = vec![];
184    let mut all_continue_spans = vec![];
185    let mut all_ret_spans = vec![];
186    let mut all_set_spans = vec![];
187    let mut all_fun_spans = vec![];
188    let mut all_type_alias_spans = vec![];
189    let mut all_import_spans = vec![];
190    let mut all_alias_spans = vec![];
191    let mut line_iter = lines.enumerate().peekable();
192    let mut counters = ScriptCounters::default();
193
194    while let Some((_line_num, line)) = line_iter.next() {
195        let mut parser = Parser::new(db, line, source_text, module_id, counters);
196        let statement = parser.parse_statement(&mut line_iter);
197        counters = parser.script_counters();
198        statements.push(statement);
199        all_expr_spans.extend(parser.take_expr_spans());
200        all_break_spans.extend(parser.take_break_spans());
201        all_continue_spans.extend(parser.take_continue_spans());
202        all_ret_spans.extend(parser.take_ret_spans());
203        all_set_spans.extend(parser.take_set_spans());
204        all_fun_spans.extend(parser.take_fun_spans());
205        all_type_alias_spans.extend(parser.take_type_alias_spans());
206        all_import_spans.extend(parser.take_import_spans());
207        all_alias_spans.extend(parser.take_alias_spans());
208    }
209
210    let spans = ParsedSpans {
211        expr_spans: all_expr_spans,
212        break_spans: all_break_spans,
213        continue_spans: all_continue_spans,
214        ret_spans: all_ret_spans,
215        set_spans: all_set_spans,
216        fun_spans: all_fun_spans,
217        type_alias_spans: all_type_alias_spans,
218        import_spans: all_import_spans,
219        alias_spans: all_alias_spans,
220    };
221    (statements, spans)
222}
223
224/// Tracked wrapper for parser tests that only need the ParsedStatements.
225#[salsa::tracked(returns(ref))]
226pub fn parse_for_test<'db>(
227    db: &'db dyn Db,
228    source: Source,
229) -> ast::ParsedStatements<'db> {
230    parse(db, source).parsed.clone()
231}
232
233/// Public tracked wrapper for integration tests that returns just the ParsedStatements.
234///
235/// Integration tests are compiled as separate binaries and need pub access.
236#[salsa::tracked(returns(ref))]
237pub fn parse_integration_test<'db>(
238    db: &'db dyn Db,
239    source: Source,
240) -> ast::ParsedStatements<'db> {
241    parse(db, source).parsed.clone()
242}
243
244/// Public tracked wrapper for integration code to enable diagnostic accumulation.
245///
246/// This function should be called before parse() to accumulate diagnostics,
247/// then parse() can be called separately to get the full ParseResult.
248/// Returns just the ParsedStatements to satisfy Salsa's type requirements.
249#[salsa::tracked(returns(ref))]
250pub fn parse_for_diagnostics<'db>(
251    db: &'db dyn Db,
252    source: Source,
253) -> ast::ParsedStatements<'db> {
254    parse(db, source).parsed.clone()
255}
256
257use datalove_datafun_ast::spans::{SpanMapEntry, DatafunSpans};
258
259/// Extract datafun expression spans from a parsed source.
260///
261/// Reads spans from the ParseResult side table (no accumulators).
262pub fn datafun_spans<'db>(
263    db: &'db dyn Db,
264    source: Source,
265) -> DatafunSpans {
266    use bct::diagnostic::SpanEntry;
267
268    let parse_result = parse(db, source);
269    let entries: Vec<SpanMapEntry> = parse_result.expr_spans
270        .iter()
271        .map(|e| SpanMapEntry {
272            expr_key: e.expr_key,
273            entry: SpanEntry::new(e.source, e.span.C()),
274        })
275        .collect();
276
277    DatafunSpans::with_stmt_spans(
278        entries,
279        parse_result.break_spans.C(),
280        parse_result.continue_spans.C(),
281        parse_result.ret_spans.C(),
282        parse_result.set_spans.C(),
283        parse_result.fun_spans.C(),
284        parse_result.type_alias_spans.C(),
285        parse_result.import_spans.C(),
286        parse_result.alias_spans.C(),
287    )
288}
289
290// ============================================================================
291// Parsing a module
292// ============================================================================
293
294/// Parse a module and return just its statements.
295///
296/// Derived from [`parse_module_full`] rather than parsing again, so a module is
297/// parsed once however many callers want only its statements. Splitting the
298/// statements out this way also firewalls them: an edit that moves spans
299/// without changing any statement re-runs `parse_module_full`, but this returns
300/// an equal value and backdates, so name resolution downstream does not re-run.
301#[salsa::tracked(returns(ref))]
302pub fn parse_module_ast<'db>(
303    db: &'db dyn Db,
304    module: bct::module_graph::Module<'db>,
305) -> ast::ParsedStatements<'db> {
306    parse_module_full(db, module).parsed.clone()
307}
308
309/// The spans of one module, in the form diagnostics look things up in.
310///
311/// `parse_module_full` returns the side tables as the parser filled them;
312/// this is the same data keyed for lookup. Tracked and keyed on the module,
313/// so building it is a cost only the modules something actually reports a
314/// diagnostic against pay, rather than every module on every compile.
315#[salsa::tracked(returns(ref))]
316pub fn module_spans<'db>(
317    db: &'db dyn Db,
318    module: bct::module_graph::Module<'db>,
319) -> DatafunSpans<'db> {
320    let full = parse_module_full(db, module);
321    DatafunSpans::with_stmt_spans(
322        full.expr_spans.iter().map(|e| SpanMapEntry {
323            expr_key: e.expr_key,
324            entry: bct::diagnostic::SpanEntry::new(e.source, e.span.clone()),
325        }).collect(),
326        full.break_spans.clone(),
327        full.continue_spans.clone(),
328        full.ret_spans.clone(),
329        full.set_spans.clone(),
330        full.fun_spans.clone(),
331        full.type_alias_spans.clone(),
332        full.import_spans.clone(),
333        full.alias_spans.clone(),
334    )
335}
336
337/// Parse a module and return its statements and spans together.
338///
339/// The two come from one parse, so the expression keys in the spans match the
340/// statements beside them.
341#[salsa::tracked(returns(ref))]
342pub fn parse_module_full<'db>(
343    db: &'db dyn Db,
344    module: bct::module_graph::Module<'db>,
345) -> ast::ParseResult<'db> {
346    let module_id = module.id(db);
347    datalove_ct::query_log::log_query("parse", module_id.path(db), datalove_ct::query_log::QueryPhase::Start);
348    let result = parse_with_module_id(db, module.source(db), Some(module_id));
349    datalove_ct::query_log::log_query("parse", module_id.path(db), datalove_ct::query_log::QueryPhase::End);
350    result
351}