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#[derive(Debug, Clone)]
23pub struct AnalysisError {
24 pub message: String,
26 pub variable: String,
28 pub code: &'static str,
30 pub cause_context: Option<String>,
32 pub use_span: Option<Span>,
34 pub cause_span: Option<Span>,
36}
37
38pub 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
51pub 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 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 let mut recovery = parse_with_recovery(
84 source,
85 &tokens,
86 &type_registry,
87 &mut interner,
88 );
89
90 let func_defs = extract_function_defs_from_tokens(&tokens, &interner);
93 recovery.stmts.extend(func_defs);
94
95 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 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
140struct FullParseResult<'a> {
142 owned_stmts: Vec<OwnedStmt>,
143 interner: &'a Interner,
144 type_errors: Vec<ParseError>,
146 escape_errors: Vec<AnalysisError>,
147 ownership_errors: Vec<AnalysisError>,
148 ownership_states: HashMap<String, VarState>,
149}
150
151fn 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 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 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 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 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 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 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
304fn 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#[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
441struct 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
450fn 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 let mut block_boundaries: Vec<usize> = vec![0]; 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 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 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 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 let last_start = *block_boundaries.last().unwrap();
525 try_block_fn(last_start, tokens.len(), &mut result);
526
527 result
528}
529
530fn 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 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 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 }
590 TokenType::LParen => {
591 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 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 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 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 let result = analyze("## Main\n Let x be 5.\n Let be.\n");
791 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 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 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
847fn 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 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
901fn 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 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}