Skip to main content

logicaffeine_lsp/
pipeline.rs

1use std::collections::HashMap;
2
3use logicaffeine_base::{Arena, Interner};
4use logicaffeine_compile::analysis::{
5    EscapeChecker, OwnershipChecker, VarState,
6};
7use logicaffeine_language::{
8    analysis::{DiscoveryPass, TypeRegistry, PolicyRegistry},
9    arena_ctx::AstContext,
10    ast::stmt::{Stmt, Expr, TypeExpr},
11    drs::WorldState,
12    error::ParseError,
13    lexer::Lexer,
14    mwe,
15    parser::Parser,
16    token::{Token, BlockType, Span, TokenType},
17};
18
19use crate::index::SymbolIndex;
20
21/// A lightweight owned analysis error from escape/ownership checking.
22#[derive(Debug, Clone)]
23pub struct AnalysisError {
24    /// Socratic error message
25    pub message: String,
26    /// Variable name for span resolution
27    pub variable: String,
28    /// Stable diagnostic code for code actions
29    pub code: &'static str,
30    /// Optional cause context for related information
31    pub cause_context: Option<String>,
32    /// The erring statement's span (from `Parser::stmt_spans`), when known.
33    pub use_span: Option<Span>,
34    /// The causing statement's span (the Give that moved it), when known.
35    pub cause_span: Option<Span>,
36}
37
38/// Result of running the full analysis pipeline on a document.
39pub struct AnalysisResult {
40    pub tokens: Vec<Token>,
41    pub interner: Interner,
42    pub type_registry: TypeRegistry,
43    pub policy_registry: PolicyRegistry,
44    pub errors: Vec<ParseError>,
45    pub escape_errors: Vec<AnalysisError>,
46    pub ownership_errors: Vec<AnalysisError>,
47    pub ownership_states: HashMap<String, VarState>,
48    pub symbol_index: SymbolIndex,
49}
50
51/// Run the full analysis pipeline: lex → MWE → discover → parse → index.
52///
53/// Uses block-level error recovery: if the full parse fails, splits source
54/// at `## BlockHeader` boundaries and parses each block independently.
55pub fn analyze(source: &str) -> AnalysisResult {
56    let mut interner = Interner::new();
57    let mut lexer = Lexer::new(source, &mut interner);
58    let tokens = lexer.tokenize();
59
60    let mwe_trie = mwe::build_mwe_trie();
61    let tokens = mwe::apply_mwe_pipeline(tokens, &mwe_trie, &mut interner);
62
63    let (type_registry, policy_registry) = {
64        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
65        let result = discovery.run_full();
66        (result.types, result.policies)
67    };
68
69    let parse_tokens = tokens.clone();
70
71    // Try full parse first (optimistic fast path)
72    let (errors, symbol_index, escape_errors, ownership_errors, ownership_states) = match try_full_parse(
73        parse_tokens.clone(),
74        &type_registry,
75        &mut interner,
76    ) {
77        Ok(result) => {
78            let idx = SymbolIndex::build(&result.owned_stmts, &tokens, &type_registry, result.interner, source);
79            (result.type_errors, idx, result.escape_errors, result.ownership_errors, result.ownership_states)
80        }
81        Err(first_error) => {
82            // Fall through to block-level recovery
83            let mut recovery = parse_with_recovery(
84                source,
85                &tokens,
86                &type_registry,
87                &mut interner,
88            );
89
90            // Extract function definitions from block header tokens (the standard
91            // parser cannot handle `## To funcName with param: Type` blocks)
92            let func_defs = extract_function_defs_from_tokens(&tokens, &interner);
93            recovery.stmts.extend(func_defs);
94
95            // The full parse saw the whole program; its error is the truest
96            // diagnosis for the region it names. The per-sentence reparse
97            // only sees the excised sentence and can degrade a specific
98            // teaching (IsValueEquality) to a generic ExpectedStatement —
99            // swap the same-line generic back for the specific one (the two
100            // anchor differently within the sentence, so lines, not spans,
101            // are the join).
102            let line_of = |offset: usize| {
103                source[..offset.min(source.len())].bytes().filter(|&b| b == b'\n').count()
104            };
105            let first_error_line = line_of(first_error.span.start);
106            let same_line = recovery
107                .parse_errors
108                .iter_mut()
109                .find(|e| line_of(e.span.start) == first_error_line);
110            if let Some(slot) = same_line {
111                *slot = first_error;
112            } else if recovery.parse_errors.is_empty() {
113                // Only report the first_error if it didn't originate from a
114                // function definition block that the parser can't handle
115                let has_function_blocks = tokens.iter().any(|t| {
116                    matches!(t.kind, TokenType::BlockHeader { block_type: BlockType::Function })
117                });
118                if !has_function_blocks {
119                    recovery.parse_errors.push(first_error);
120                }
121            }
122            let idx = SymbolIndex::build(&recovery.stmts, &tokens, &type_registry, &interner, source);
123            (recovery.parse_errors, idx, recovery.escape_errors, recovery.ownership_errors, recovery.ownership_states)
124        }
125    };
126
127    AnalysisResult {
128        tokens,
129        interner,
130        type_registry,
131        policy_registry,
132        errors,
133        escape_errors,
134        ownership_errors,
135        ownership_states,
136        symbol_index,
137    }
138}
139
140/// Result of a successful full parse, including analysis checker results.
141struct FullParseResult<'a> {
142    owned_stmts: Vec<OwnedStmt>,
143    interner: &'a Interner,
144    /// Typechecker findings mapped onto real statement spans.
145    type_errors: Vec<ParseError>,
146    escape_errors: Vec<AnalysisError>,
147    ownership_errors: Vec<AnalysisError>,
148    ownership_states: HashMap<String, VarState>,
149}
150
151/// Attempt a full parse of the token stream.
152fn try_full_parse<'a>(
153    tokens: Vec<Token>,
154    type_registry: &TypeRegistry,
155    interner: &'a mut Interner,
156) -> Result<FullParseResult<'a>, ParseError> {
157    let expr_arena = Arena::new();
158    let term_arena = Arena::new();
159    let np_arena = Arena::new();
160    let sym_arena = Arena::new();
161    let role_arena = Arena::new();
162    let pp_arena = Arena::new();
163    let stmt_arena: Arena<Stmt> = Arena::new();
164    let imperative_expr_arena: Arena<Expr> = Arena::new();
165    let type_expr_arena: Arena<TypeExpr> = Arena::new();
166
167    let ctx = AstContext::with_types(
168        &expr_arena,
169        &term_arena,
170        &np_arena,
171        &sym_arena,
172        &role_arena,
173        &pp_arena,
174        &stmt_arena,
175        &imperative_expr_arena,
176        &type_expr_arena,
177    );
178
179    let mut world_state = WorldState::new();
180    let mut parser = Parser::new(tokens, &mut world_state, interner, ctx, type_registry.clone());
181    let stmts = parser.parse_program()?;
182    let stmt_spans = parser.stmt_spans().to_vec();
183
184    // Typecheck while the arena AST is alive; every failing top-level
185    // statement reports, anchored on its span from the parser's side-table.
186    let type_errors = {
187        let (_env, indexed) = logicaffeine_compile::analysis::check_program_collect(
188            &stmts,
189            interner,
190            type_registry,
191        );
192        indexed
193            .into_iter()
194            .map(|found| {
195                let mut kind = found.error.to_parse_error_kind(interner);
196                // The bridge cannot see the registry; fill in the fields that
197                // DO exist so the message and quickfix can name them.
198                if let logicaffeine_language::error::ParseErrorKind::FieldNotFound {
199                    type_name,
200                    available,
201                    ..
202                } = &mut kind
203                {
204                    *available = fields_of_type(type_registry, type_name, interner);
205                }
206                ParseError {
207                    kind,
208                    span: found
209                        .stmt_index
210                        .and_then(|i| stmt_spans.get(i))
211                        .copied()
212                        .unwrap_or_default(),
213                }
214            })
215            .collect()
216    };
217
218    // Run escape analysis while arena-allocated AST is still alive
219    let escape_errors = {
220        let mut checker = EscapeChecker::new(interner);
221        checker
222            .check_program_collect(&stmts)
223            .into_iter()
224            .map(|(stmt_index, e)| {
225                let (code, cause_context) = match &e.kind {
226                    logicaffeine_compile::analysis::EscapeErrorKind::ReturnEscape { zone_name, .. } => {
227                        ("escape-return", Some(format!("zone '{}'", zone_name)))
228                    }
229                    logicaffeine_compile::analysis::EscapeErrorKind::AssignmentEscape { target, zone_name, .. } => {
230                        ("escape-assignment", Some(format!("zone '{}', target '{}'", zone_name, target)))
231                    }
232                };
233                let variable = match &e.kind {
234                    logicaffeine_compile::analysis::EscapeErrorKind::ReturnEscape { variable, .. } => variable.clone(),
235                    logicaffeine_compile::analysis::EscapeErrorKind::AssignmentEscape { variable, .. } => variable.clone(),
236                };
237                AnalysisError {
238                    message: e.to_string(),
239                    variable,
240                    code,
241                    cause_context,
242                    use_span: stmt_spans.get(stmt_index).copied(),
243                    cause_span: None,
244                }
245            })
246            .collect()
247    };
248
249    // Run ownership analysis while arena-allocated AST is still alive
250    let (ownership_errors, ownership_states) = {
251        let mut checker = OwnershipChecker::new(interner);
252        let errors = checker
253            .check_program_collect(&stmts)
254            .into_iter()
255            .map(|finding| {
256                let e = &finding.error;
257                let (code, variable, cause_context) = match &e.kind {
258                    logicaffeine_compile::analysis::OwnershipErrorKind::UseAfterMove { variable } => {
259                        ("use-after-move", variable.clone(), Some(format!("'{}' was given away here", variable)))
260                    }
261                    logicaffeine_compile::analysis::OwnershipErrorKind::UseAfterMaybeMove { variable, branch } => {
262                        ("maybe-moved", variable.clone(), Some(format!("'{}' might be given away in {}", variable, branch)))
263                    }
264                    logicaffeine_compile::analysis::OwnershipErrorKind::DoubleMoved { variable } => {
265                        ("double-move", variable.clone(), Some(format!("'{}' was first given away here", variable)))
266                    }
267                };
268                AnalysisError {
269                    message: e.to_string(),
270                    variable,
271                    code,
272                    cause_context,
273                    use_span: stmt_spans.get(finding.stmt_index).copied(),
274                    cause_span: finding
275                        .cause_stmt_index
276                        .and_then(|i| stmt_spans.get(i))
277                        .copied(),
278                }
279            })
280            .collect();
281
282        // Extract ownership states, resolving symbols to strings
283        let states: HashMap<String, VarState> = checker
284            .var_states()
285            .iter()
286            .map(|(sym, state)| (interner.resolve(*sym).to_string(), *state))
287            .collect();
288
289        (errors, states)
290    };
291
292    // Convert arena-allocated stmts to owned summaries for the symbol index
293    let owned = stmts.iter().map(|s| summarize_stmt(s, interner)).collect();
294    Ok(FullParseResult {
295        owned_stmts: owned,
296        interner,
297        type_errors,
298        escape_errors,
299        ownership_errors,
300        ownership_states,
301    })
302}
303
304/// The field names a struct type declares, for did-you-mean and message text.
305fn fields_of_type(registry: &TypeRegistry, type_name: &str, interner: &Interner) -> Vec<String> {
306    for (sym, typedef) in registry.iter_types() {
307        if interner.resolve(*sym) == type_name {
308            if let logicaffeine_language::analysis::TypeDef::Struct { fields, .. } = typedef {
309                return fields
310                    .iter()
311                    .map(|f| interner.resolve(f.name).to_string())
312                    .collect();
313            }
314        }
315    }
316    Vec::new()
317}
318
319/// Lightweight summary of a statement for symbol indexing.
320/// This avoids lifetime issues with arena-allocated AST nodes.
321#[derive(Debug, Clone)]
322pub enum OwnedStmt {
323    FunctionDef {
324        name: String,
325        params: Vec<(String, String)>,
326        return_type: Option<String>,
327    },
328    StructDef {
329        name: String,
330        fields: Vec<(String, String)>,
331    },
332    Let {
333        name: String,
334        ty: Option<String>,
335        inferred_type: Option<String>,
336        mutable: bool,
337    },
338    Theorem {
339        name: String,
340    },
341    Block {
342        name: String,
343        kind: String,
344    },
345    Other,
346}
347
348fn summarize_stmt(stmt: &Stmt, interner: &Interner) -> OwnedStmt {
349    match stmt {
350        Stmt::FunctionDef { name, params, return_type, .. } => OwnedStmt::FunctionDef {
351            name: interner.resolve(*name).to_string(),
352            params: params
353                .iter()
354                .map(|(n, ty)| {
355                    (interner.resolve(*n).to_string(), format_type_expr(ty, interner))
356                })
357                .collect(),
358            return_type: return_type.map(|ty| format_type_expr(ty, interner)),
359        },
360        Stmt::StructDef { name, fields, .. } => OwnedStmt::StructDef {
361            name: interner.resolve(*name).to_string(),
362            fields: fields
363                .iter()
364                .map(|(n, ty, _is_public)| {
365                    (interner.resolve(*n).to_string(), interner.resolve(*ty).to_string())
366                })
367                .collect(),
368        },
369        Stmt::Let { var, ty, mutable, value, .. } => {
370            let explicit_ty = ty.map(|t| format_type_expr(t, interner));
371            let inferred_type = if explicit_ty.is_none() {
372                infer_type_from_expr(value, interner)
373            } else {
374                None
375            };
376            OwnedStmt::Let {
377                name: interner.resolve(*var).to_string(),
378                ty: explicit_ty,
379                inferred_type,
380                mutable: *mutable,
381            }
382        }
383        Stmt::Theorem(t) => OwnedStmt::Theorem {
384            name: t.name.clone(),
385        },
386        _ => OwnedStmt::Other,
387    }
388}
389
390fn infer_type_from_expr(expr: &Expr, interner: &Interner) -> Option<String> {
391    use logicaffeine_language::ast::stmt::Literal;
392    match expr {
393        Expr::Literal(lit) => match lit {
394            Literal::Number(_) => Some("Int".to_string()),
395            Literal::Float(_) => Some("Real".to_string()),
396            Literal::Text(_) => Some("Text".to_string()),
397            Literal::Boolean(_) => Some("Bool".to_string()),
398            Literal::Nothing => Some("Unit".to_string()),
399            Literal::Char(_) => Some("Char".to_string()),
400            Literal::Duration(_) => Some("Duration".to_string()),
401            Literal::Date(_) => Some("Date".to_string()),
402            Literal::Moment(_) => Some("Moment".to_string()),
403            _ => None,
404        },
405        Expr::New { type_name, .. } => Some(interner.resolve(*type_name).to_string()),
406        Expr::List(_) => Some("Seq".to_string()),
407        Expr::Call { function, .. } => Some(format!("{}(..)", interner.resolve(*function))),
408        Expr::Copy { .. } => Some("copy".to_string()),
409        Expr::Length { .. } => Some("Int".to_string()),
410        Expr::Contains { .. } => Some("Bool".to_string()),
411        _ => None,
412    }
413}
414
415fn format_type_expr(ty: &TypeExpr, interner: &Interner) -> String {
416    match ty {
417        TypeExpr::Primitive(sym) => interner.resolve(*sym).to_string(),
418        TypeExpr::Named(sym) => interner.resolve(*sym).to_string(),
419        TypeExpr::Generic { base, params } => {
420            let base_name = interner.resolve(*base);
421            let param_strs: Vec<String> = params.iter().map(|p| format_type_expr(p, interner)).collect();
422            format!("{} of {}", base_name, param_strs.join(", "))
423        }
424        TypeExpr::Function { inputs, output } => {
425            let input_strs: Vec<String> = inputs.iter().map(|i| format_type_expr(i, interner)).collect();
426            let out = format_type_expr(output, interner);
427            format!("({}) -> {}", input_strs.join(", "), out)
428        }
429        TypeExpr::Refinement { base, .. } => {
430            format!("{} where ...", format_type_expr(base, interner))
431        }
432        TypeExpr::Persistent { inner } => {
433            format!("Persistent {}", format_type_expr(inner, interner))
434        }
435        TypeExpr::Mutable { inner } => {
436            format!("mutable {}", format_type_expr(inner, interner))
437        }
438    }
439}
440
441/// Result of block-level recovery parsing, including analysis errors.
442struct RecoveryResult {
443    stmts: Vec<OwnedStmt>,
444    parse_errors: Vec<ParseError>,
445    escape_errors: Vec<AnalysisError>,
446    ownership_errors: Vec<AnalysisError>,
447    ownership_states: HashMap<String, VarState>,
448}
449
450/// Parse with block-level recovery.
451///
452/// Splits source at `## BlockHeader` boundaries and parses each block
453/// independently, collecting successful parses and errors separately.
454/// Function definition blocks (`## To`) are skipped — they are handled
455/// separately by `extract_function_defs_from_tokens`.
456fn parse_with_recovery(
457    _source: &str,
458    tokens: &[Token],
459    type_registry: &TypeRegistry,
460    interner: &mut Interner,
461) -> RecoveryResult {
462    let mut result = RecoveryResult {
463        stmts: vec![],
464        parse_errors: vec![],
465        escape_errors: vec![],
466        ownership_errors: vec![],
467        ownership_states: HashMap::new(),
468    };
469
470    // Find block header positions in the token stream
471    let mut block_boundaries: Vec<usize> = vec![0]; // first token
472    for (i, tok) in tokens.iter().enumerate() {
473        if matches!(tok.kind, TokenType::BlockHeader { .. }) && i > 0 {
474            block_boundaries.push(i);
475        }
476    }
477
478    if block_boundaries.len() <= 1 {
479        // Single block — check if it's a function block (handled elsewhere)
480        let is_function_block = tokens.first().map_or(false, |t| {
481            matches!(t.kind, TokenType::BlockHeader { block_type: BlockType::Function })
482        });
483        if is_function_block {
484            return result;
485        }
486        // Non-function single block: attempt to parse for partial recovery
487        let block_tokens: Vec<Token> = tokens.to_vec();
488        let (block, errors) =
489            parse_block_excising_bad_sentences(block_tokens, type_registry, interner);
490        result.parse_errors.extend(errors);
491        if let Some(block) = block {
492            result.stmts = block.owned_stmts;
493            result.escape_errors = block.escape_errors;
494            result.ownership_errors = block.ownership_errors;
495            result.ownership_states = block.ownership_states;
496        }
497        return result;
498    }
499
500    let mut try_block_fn = |start: usize, end_excl: usize, result: &mut RecoveryResult| {
501        // Skip function blocks — they can't be parsed by the standard parser
502        if matches!(tokens[start].kind, TokenType::BlockHeader { block_type: BlockType::Function }) {
503            return;
504        }
505        let block_tokens: Vec<Token> = tokens[start..end_excl].to_vec();
506        let (block, errors) =
507            parse_block_excising_bad_sentences(block_tokens, type_registry, interner);
508        result.parse_errors.extend(errors);
509        if let Some(block) = block {
510            result.stmts.extend(block.owned_stmts);
511            result.escape_errors.extend(block.escape_errors);
512            result.ownership_errors.extend(block.ownership_errors);
513            result.ownership_states.extend(block.ownership_states);
514        }
515    };
516
517    for window in block_boundaries.windows(2) {
518        let start = window[0];
519        let end = window[1];
520        try_block_fn(start, end, &mut result);
521    }
522
523    // Parse the last block
524    let last_start = *block_boundaries.last().unwrap();
525    try_block_fn(last_start, tokens.len(), &mut result);
526
527    result
528}
529
530/// Extract function definitions from `## To` block header tokens.
531///
532/// The standard parser cannot handle function definition blocks, so we extract
533/// function name and parameters directly from the token stream.
534fn extract_function_defs_from_tokens(
535    tokens: &[Token],
536    interner: &Interner,
537) -> Vec<OwnedStmt> {
538    let mut results = Vec::new();
539    let mut i = 0;
540
541    while i < tokens.len() {
542        if let TokenType::BlockHeader { block_type: BlockType::Function } = &tokens[i].kind {
543            let mut j = i + 1;
544            let mut func_name: Option<String> = None;
545            let mut params: Vec<(String, String)> = Vec::new();
546            let mut after_with = false;
547            let mut current_param_name: Option<String> = None;
548            let mut return_type: Option<String> = None;
549
550            while j < tokens.len() {
551                match &tokens[j].kind {
552                    TokenType::Newline | TokenType::Indent | TokenType::BlockHeader { .. } => break,
553                    TokenType::Colon => {
554                        if let Some(param_name) = current_param_name.take() {
555                            // Next token after colon is the type
556                            j += 1;
557                            let type_name = if j < tokens.len()
558                                && !matches!(
559                                    tokens[j].kind,
560                                    TokenType::Newline
561                                        | TokenType::Indent
562                                        | TokenType::BlockHeader { .. }
563                                        | TokenType::Colon
564                                )
565                            {
566                                interner.resolve(tokens[j].lexeme).to_string()
567                            } else {
568                                "auto".to_string()
569                            };
570                            params.push((param_name, type_name));
571                        }
572                    }
573                    TokenType::Arrow => {
574                        // Next token is the return type
575                        j += 1;
576                        if j < tokens.len()
577                            && !matches!(
578                                tokens[j].kind,
579                                TokenType::Newline
580                                    | TokenType::Indent
581                                    | TokenType::BlockHeader { .. }
582                            )
583                        {
584                            return_type = Some(interner.resolve(tokens[j].lexeme).to_string());
585                        }
586                    }
587                    TokenType::And | TokenType::Comma => {
588                        // Parameter separator
589                    }
590                    TokenType::LParen => {
591                        // `(n: Int)` groups open the parameter list exactly
592                        // like the prose `with` does.
593                        if func_name.is_some() {
594                            after_with = true;
595                        }
596                    }
597                    TokenType::RParen => {}
598                    _ => {
599                        let text = interner.resolve(tokens[j].lexeme);
600                        if text == "with" && func_name.is_some() {
601                            after_with = true;
602                        } else if func_name.is_none() {
603                            func_name = Some(text.to_string());
604                        } else if after_with && current_param_name.is_none() {
605                            current_param_name = Some(text.to_string());
606                        }
607                    }
608                }
609                j += 1;
610            }
611
612            if let Some(name) = func_name {
613                results.push(OwnedStmt::FunctionDef {
614                    name,
615                    params,
616                    return_type,
617                });
618            }
619        }
620        i += 1;
621    }
622
623    results
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629
630    #[test]
631    fn recovery_keeps_the_full_parses_specific_error() {
632        // The full parse diagnosed IsValueEquality for `x is 5.`; the
633        // per-sentence reparse only sees the excised sentence and degrades
634        // it to a generic ExpectedStatement. The specific teaching must win.
635        let result = analyze("## Main\nLet x be 5.\nx is 5.\n");
636        assert!(
637            result
638                .errors
639                .iter()
640                .any(|e| matches!(e.kind, logicaffeine_language::error::ParseErrorKind::IsValueEquality { .. })),
641            "the specific IsValueEquality must survive recovery: {:?}",
642            result.errors
643        );
644    }
645
646    #[test]
647    fn recovered_function_defs_keep_parenthesized_params() {
648        // A theorem block sends the document down the recovery path, where
649        // function definitions come from token extraction — which must read
650        // the parenthesized `(n: Int)` form, not just the `with n: Int` prose.
651        let result = analyze(
652            "## To double (n: Int) -> Int:\n    Return n * 2.\n\n## Main\nLet x be double(3).\n\n## Theorem: Socrates\nGiven: All men are mortal. Socrates is a man.\nProve: Socrates is mortal.\nProof: Auto.\n",
653        );
654        let defs = result.symbol_index.definitions_of("double");
655        assert!(
656            defs.iter()
657                .any(|d| d.detail.as_deref() == Some("To double(n: Int) -> Int")),
658            "the recovered signature must keep its parameter: {:?}",
659            defs.iter().map(|d| d.detail.clone()).collect::<Vec<_>>()
660        );
661    }
662
663    #[test]
664    fn analyze_simple_let() {
665        let result = analyze("## Main\n    Let x be 5.\n");
666        assert!(result.errors.is_empty(), "Expected no errors, got: {:?}", result.errors);
667        assert!(!result.tokens.is_empty());
668        let defs: Vec<_> = result.symbol_index.definitions.iter()
669            .filter(|d| d.name == "x")
670            .collect();
671        assert!(!defs.is_empty(), "Expected definition for 'x'");
672        assert_eq!(defs[0].kind, crate::index::DefinitionKind::Variable);
673    }
674
675    #[test]
676    fn analyze_multiple_lets() {
677        let result = analyze("## Main\n    Let x be 5.\n    Let y be 10.\n");
678        assert!(result.errors.is_empty(), "Expected no errors, got: {:?}", result.errors);
679        let x_defs = result.symbol_index.definitions_of("x");
680        let y_defs = result.symbol_index.definitions_of("y");
681        assert_eq!(x_defs.len(), 1);
682        assert_eq!(y_defs.len(), 1);
683    }
684
685    #[test]
686    fn analyze_empty_source() {
687        let result = analyze("");
688        assert!(result.tokens.is_empty() || result.errors.is_empty(),
689            "Empty source should not crash");
690    }
691
692    #[test]
693    fn analyze_produces_tokens() {
694        let result = analyze("## Main\n    Let x be 5.\n");
695        assert!(result.tokens.len() >= 3, "Expected at least block header + Let + x tokens");
696    }
697
698    #[test]
699    fn analyze_syntax_error_produces_diagnostics() {
700        let result = analyze("## Main\n    Let be.\n");
701        assert!(!result.errors.is_empty(), "Expected parse errors for invalid syntax");
702    }
703
704    #[test]
705    fn analyze_function_def() {
706        let source = "## Main\n    Let x be 5.\n    Let y be x + 1.\n";
707        let result = analyze(source);
708        assert!(result.errors.is_empty(), "Expected no errors, got: {:?}", result.errors);
709        let x_defs = result.symbol_index.definitions_of("x");
710        let y_defs = result.symbol_index.definitions_of("y");
711        assert_eq!(x_defs.len(), 1, "Expected 1 def for 'x'");
712        assert_eq!(y_defs.len(), 1, "Expected 1 def for 'y'");
713        assert!(y_defs[0].detail.as_ref().unwrap().contains("Let"),
714            "y detail should mention Let: {:?}", y_defs[0].detail);
715    }
716
717    #[test]
718    fn analyze_indexes_references() {
719        let result = analyze("## Main\n    Let x be 5.\n    Show x.\n");
720        assert!(result.errors.is_empty(), "Expected no errors, got: {:?}", result.errors);
721        let refs = result.symbol_index.references_to("x");
722        assert!(refs.len() >= 1, "Expected at least one reference to 'x', got {}", refs.len());
723    }
724
725    #[test]
726    fn analyze_block_headers_indexed() {
727        let result = analyze("## Main\n    Let x be 5.\n");
728        assert!(!result.symbol_index.block_spans.is_empty(),
729            "Expected block spans to be indexed");
730    }
731
732    #[test]
733    fn analyze_statement_spans_indexed() {
734        let result = analyze("## Main\n    Let x be 5.\n");
735        assert!(result.errors.is_empty(), "Expected no errors, got: {:?}", result.errors);
736        assert!(!result.symbol_index.statement_spans.is_empty(),
737            "Expected statement spans to be indexed");
738    }
739
740    #[test]
741    fn analyze_single_block_with_error_still_reports() {
742        // Single block with a parse error should still produce diagnostics,
743        // not silently return empty
744        let result = analyze("## Main\n    Let be.\n");
745        assert!(
746            !result.errors.is_empty(),
747            "Single block with error should still report errors"
748        );
749    }
750
751    #[test]
752    fn analyze_function_def_summarized() {
753        let source = "## To greet (name: Text) -> Text:\n    Return name.\n";
754        let result = analyze(source);
755        let defs = result.symbol_index.definitions_of("greet");
756        let func_defs: Vec<_> = defs.iter()
757            .filter(|d| d.kind == crate::index::DefinitionKind::Function)
758            .collect();
759        if !func_defs.is_empty() {
760            let detail = func_defs[0].detail.as_ref().unwrap();
761            assert!(detail.contains("greet"), "Detail should contain function name: {}", detail);
762            assert!(detail.contains("name"), "Detail should contain param name: {}", detail);
763        }
764    }
765
766    #[test]
767    fn analyze_generic_type_in_detail() {
768        let source = "## Main\n    Let items: Seq of Int be an empty list.\n";
769        let result = analyze(source);
770        let defs = result.symbol_index.definitions_of("items");
771        if !defs.is_empty() {
772            if let Some(detail) = &defs[0].detail {
773                assert!(detail.contains("Seq") || detail.contains("Int"),
774                    "Detail should mention Seq or Int: {}", detail);
775            }
776        }
777    }
778
779    #[test]
780    fn analyze_block_recovery_preserves_good_block() {
781        let source = "## Main\n    Let be.\n## Note: readme\n    Just a note.\n";
782        let result = analyze(source);
783        assert!(result.symbol_index.block_spans.len() >= 1,
784            "Block spans should have at least 1 entry even with errors");
785    }
786
787    #[test]
788    fn analyze_single_block_error_does_not_swallow() {
789        // Even with only one block header, error recovery should attempt to parse it
790        let result = analyze("## Main\n    Let x be 5.\n    Let be.\n");
791        // The good statement should still be indexed even if an error occurs
792        assert!(
793            !result.errors.is_empty() || !result.symbol_index.definitions_of("x").is_empty(),
794            "Should either report errors or still index the valid statement"
795        );
796    }
797
798    #[test]
799    fn analyze_use_after_move_produces_ownership_error() {
800        // Give x to y moves x. Using x afterward should produce an ownership error
801        // (from the OwnershipChecker) or a UseAfterMove parse error (from the parser).
802        let source = "## Main\n    Let x be 5.\n    Let y be 0.\n    Give x to y.\n    Show x.\n";
803        let result = analyze(source);
804        let has_parse_move_error = result.errors.iter()
805            .any(|e| matches!(e.kind, logicaffeine_language::error::ParseErrorKind::UseAfterMove { .. }));
806        let has_ownership_error = !result.ownership_errors.is_empty();
807        assert!(
808            has_parse_move_error || has_ownership_error,
809            "Expected ownership error for use-after-move, got none. Parse errors: {:?}, Ownership errors: {:?}",
810            result.errors, result.ownership_errors
811        );
812    }
813
814    #[test]
815    fn analyze_clean_code_no_analysis_errors() {
816        let source = "## Main\n    Let x be 5.\n    Show x.\n";
817        let result = analyze(source);
818        assert!(result.errors.is_empty(), "Expected no parse errors: {:?}", result.errors);
819        assert!(result.escape_errors.is_empty(), "Expected no escape errors: {:?}", result.escape_errors);
820        assert!(result.ownership_errors.is_empty(), "Expected no ownership errors: {:?}", result.ownership_errors);
821    }
822
823    #[test]
824    fn analyze_ownership_states_populated() {
825        let source = "## Main\n    Let x be 5.\n    Show x.\n";
826        let result = analyze(source);
827        assert!(result.errors.is_empty(), "Expected no parse errors: {:?}", result.errors);
828        assert!(
829            !result.ownership_states.is_empty(),
830            "Expected ownership states for variables"
831        );
832        let x_state = result.ownership_states.get("x");
833        assert!(x_state.is_some(), "Expected state for 'x', got keys: {:?}", result.ownership_states.keys().collect::<Vec<_>>());
834    }
835
836    #[test]
837    fn analyze_moved_variable_has_moved_state() {
838        let source = "## Main\n    Let x be 5.\n    Let y be 0.\n    Give x to y.\n    Show x.\n";
839        let result = analyze(source);
840        // After Give, x should be Moved (even though there's a use-after-move error)
841        if let Some(state) = result.ownership_states.get("x") {
842            assert_eq!(*state, VarState::Moved, "x should be Moved after Give");
843        }
844    }
845}
846
847/// Result of parsing a single recovered block, including analysis errors.
848/// Parse a block, excising broken SENTENCES one at a time so every
849/// independently bad statement reports and the good ones stay analyzed.
850///
851/// On failure the sentence containing the error span (a `Period`-delimited
852/// token run) is removed — spans on the surviving tokens are original, so
853/// later errors and the symbol index stay source-accurate — and the block is
854/// reparsed. Excision stops when a failing sentence carries block structure
855/// (`Indent`/`Dedent`, where cutting would unbalance the tree), when the
856/// error lies outside any sentence, or after a hard iteration cap.
857fn parse_block_excising_bad_sentences(
858    mut block_tokens: Vec<Token>,
859    type_registry: &TypeRegistry,
860    interner: &mut Interner,
861) -> (Option<BlockParseResult>, Vec<ParseError>) {
862    const MAX_EXCISIONS: usize = 32;
863    let mut errors = Vec::new();
864
865    // Mid-stream block slices carry no EOF; the parser walks off the end of
866    // an EOF-less stream, so terminate the slice before any parse attempt.
867    if !matches!(block_tokens.last().map(|t| &t.kind), Some(TokenType::EOF)) {
868        let end = block_tokens.last().map(|t| t.span.end).unwrap_or(0);
869        let empty = interner.intern("");
870        block_tokens.push(Token::new(
871            TokenType::EOF,
872            empty,
873            logicaffeine_language::token::Span::new(end, end),
874        ));
875    }
876
877    for _ in 0..=MAX_EXCISIONS {
878        match try_parse_block(block_tokens.clone(), type_registry, interner) {
879            Ok(block) => return (Some(block), errors),
880            Err(e) => {
881                let sentence = sentence_token_range(&block_tokens, e.span.start);
882                errors.push(e);
883                match sentence {
884                    Some((start, end)) => {
885                        let cuts_structure = block_tokens[start..=end].iter().any(|t| {
886                            matches!(t.kind, TokenType::Indent | TokenType::Dedent)
887                        });
888                        if cuts_structure {
889                            return (None, errors);
890                        }
891                        block_tokens.drain(start..=end);
892                    }
893                    None => return (None, errors),
894                }
895            }
896        }
897    }
898    (None, errors)
899}
900
901/// The token range (inclusive) of the `Period`-delimited sentence containing
902/// `offset`. Sentences start after the previous `Period` (or the block
903/// header) and end at their own `Period`.
904fn sentence_token_range(tokens: &[Token], offset: usize) -> Option<(usize, usize)> {
905    let mut start = None;
906    for (i, token) in tokens.iter().enumerate() {
907        if matches!(
908            token.kind,
909            TokenType::Indent | TokenType::Dedent | TokenType::Newline | TokenType::EOF
910        ) || matches!(token.kind, TokenType::BlockHeader { .. })
911        {
912            continue;
913        }
914        if start.is_none() {
915            start = Some(i);
916        }
917        if matches!(token.kind, TokenType::Period) {
918            let s = start.take().unwrap();
919            if tokens[s].span.start <= offset && offset <= token.span.end {
920                return Some((s, i));
921            }
922        }
923    }
924    // An error past the last period belongs to the trailing unterminated run.
925    if let Some(s) = start {
926        if tokens[s].span.start <= offset {
927            return Some((s, tokens.len().saturating_sub(1)));
928        }
929    }
930    None
931}
932
933struct BlockParseResult {
934    owned_stmts: Vec<OwnedStmt>,
935    escape_errors: Vec<AnalysisError>,
936    ownership_errors: Vec<AnalysisError>,
937    ownership_states: HashMap<String, VarState>,
938}
939
940fn try_parse_block(
941    tokens: Vec<Token>,
942    type_registry: &TypeRegistry,
943    interner: &mut Interner,
944) -> Result<BlockParseResult, ParseError> {
945    let expr_arena = Arena::new();
946    let term_arena = Arena::new();
947    let np_arena = Arena::new();
948    let sym_arena = Arena::new();
949    let role_arena = Arena::new();
950    let pp_arena = Arena::new();
951    let stmt_arena: Arena<Stmt> = Arena::new();
952    let imperative_expr_arena: Arena<Expr> = Arena::new();
953    let type_expr_arena: Arena<TypeExpr> = Arena::new();
954
955    let ctx = AstContext::with_types(
956        &expr_arena,
957        &term_arena,
958        &np_arena,
959        &sym_arena,
960        &role_arena,
961        &pp_arena,
962        &stmt_arena,
963        &imperative_expr_arena,
964        &type_expr_arena,
965    );
966
967    let mut world_state = WorldState::new();
968    let mut parser = Parser::new(tokens, &mut world_state, interner, ctx, type_registry.clone());
969    let stmts = parser.parse_program()?;
970
971    let escape_errors = {
972        let mut checker = EscapeChecker::new(interner);
973        match checker.check_program(&stmts) {
974            Ok(()) => vec![],
975            Err(e) => {
976                let (code, cause_context) = match &e.kind {
977                    logicaffeine_compile::analysis::EscapeErrorKind::ReturnEscape { zone_name, .. } => {
978                        ("escape-return", Some(format!("zone '{}'", zone_name)))
979                    }
980                    logicaffeine_compile::analysis::EscapeErrorKind::AssignmentEscape { target, zone_name, .. } => {
981                        ("escape-assignment", Some(format!("zone '{}', target '{}'", zone_name, target)))
982                    }
983                };
984                let variable = match &e.kind {
985                    logicaffeine_compile::analysis::EscapeErrorKind::ReturnEscape { variable, .. } => variable.clone(),
986                    logicaffeine_compile::analysis::EscapeErrorKind::AssignmentEscape { variable, .. } => variable.clone(),
987                };
988                vec![AnalysisError { message: e.to_string(), variable, code, cause_context, use_span: None, cause_span: None }]
989            }
990        }
991    };
992
993    let (ownership_errors, ownership_states) = {
994        let mut checker = OwnershipChecker::new(interner);
995        let errors = match checker.check_program(&stmts) {
996            Ok(()) => vec![],
997            Err(e) => {
998                let (code, variable, cause_context) = match &e.kind {
999                    logicaffeine_compile::analysis::OwnershipErrorKind::UseAfterMove { variable } => {
1000                        ("use-after-move", variable.clone(), Some(format!("'{}' was given away here", variable)))
1001                    }
1002                    logicaffeine_compile::analysis::OwnershipErrorKind::UseAfterMaybeMove { variable, branch } => {
1003                        ("maybe-moved", variable.clone(), Some(format!("'{}' might be given away in {}", variable, branch)))
1004                    }
1005                    logicaffeine_compile::analysis::OwnershipErrorKind::DoubleMoved { variable } => {
1006                        ("double-move", variable.clone(), Some(format!("'{}' was first given away here", variable)))
1007                    }
1008                };
1009                vec![AnalysisError { message: e.to_string(), variable, code, cause_context, use_span: None, cause_span: None }]
1010            }
1011        };
1012        let states: HashMap<String, VarState> = checker
1013            .var_states()
1014            .iter()
1015            .map(|(sym, state)| (interner.resolve(*sym).to_string(), *state))
1016            .collect();
1017        (errors, states)
1018    };
1019
1020    let owned: Vec<OwnedStmt> = stmts.iter().map(|s| summarize_stmt(s, interner)).collect();
1021    Ok(BlockParseResult { owned_stmts: owned, escape_errors, ownership_errors, ownership_states })
1022}