@@ -172,6 +172,93 @@ fn build_isabelle_temp_dir(tag: &str) -> Result<std::path::PathBuf> {
172172 ) ) )
173173}
174174
175+ /// EI-1 helper: find a parent session for a probe theory by reading the
176+ /// project's ROOT file.
177+ ///
178+ /// Isabelle ROOT files declare sessions like:
179+ ///
180+ /// session Tropical_Semirings in "." = "HOL-Library" +
181+ /// theories
182+ /// Tropical_v2
183+ /// Tropical_Kleene
184+ /// ...
185+ ///
186+ /// When a probe theory is going to do `imports Tropical_v2`, the temp
187+ /// probe session needs to inherit from the session that owns
188+ /// `Tropical_v2`, OR a parent of it. We pick the first session in the
189+ /// project's ROOT that declares `theory_name` in its theories list.
190+ /// Falls back to the first session declaration found, or to "HOL".
191+ ///
192+ /// This is intentionally permissive — we want to surface the existence
193+ /// of richer ROOT-parsing requirements as we hit them, not pre-build a
194+ /// full Isabelle ROOT parser. For Tropical_Semirings this matches the
195+ /// only declared session, which is what we want.
196+ fn detect_parent_session ( root_text : & str , theory_name : & str ) -> String {
197+ let mut current_session: Option < String > = None ;
198+ let mut first_session: Option < String > = None ;
199+ let mut in_theories_block = false ;
200+
201+ for raw_line in root_text. lines ( ) {
202+ let line = raw_line. trim ( ) ;
203+ if line. is_empty ( ) || line. starts_with ( '(' ) {
204+ continue ;
205+ }
206+
207+ // session FOO ... or session "FOO" ...
208+ if let Some ( rest) = line. strip_prefix ( "session " ) {
209+ let name: String = rest
210+ . trim_start_matches ( '"' )
211+ . chars ( )
212+ . take_while ( |c| !c. is_whitespace ( ) && * c != '"' )
213+ . collect ( ) ;
214+ if !name. is_empty ( ) {
215+ current_session = Some ( name. clone ( ) ) ;
216+ first_session. get_or_insert ( name) ;
217+ }
218+ in_theories_block = false ;
219+ continue ;
220+ }
221+
222+ if line. starts_with ( "theories" ) {
223+ in_theories_block = true ;
224+ // Tail of the same line may already list theories
225+ // ("theories Foo Bar"), so check both.
226+ for tok in line. split_whitespace ( ) . skip ( 1 ) {
227+ if tok. trim_matches ( '"' ) == theory_name {
228+ if let Some ( s) = & current_session {
229+ return s. clone ( ) ;
230+ }
231+ }
232+ }
233+ continue ;
234+ }
235+
236+ // Sections that close a theories block.
237+ if line. starts_with ( "document_files" )
238+ || line. starts_with ( "export_files" )
239+ || line. starts_with ( "description" )
240+ || line. starts_with ( "sessions" )
241+ || line. starts_with ( "global_theories" )
242+ || line. starts_with ( "ML_files" )
243+ {
244+ in_theories_block = false ;
245+ continue ;
246+ }
247+
248+ if in_theories_block {
249+ for tok in line. split_whitespace ( ) {
250+ if tok. trim_matches ( '"' ) == theory_name {
251+ if let Some ( s) = & current_session {
252+ return s. clone ( ) ;
253+ }
254+ }
255+ }
256+ }
257+ }
258+
259+ first_session. unwrap_or_else ( || "HOL" . to_string ( ) )
260+ }
261+
175262// Isabelle-specific term representation
176263#[ derive( Debug , Clone , Serialize , Deserialize , PartialEq ) ]
177264pub enum IsabelleTerm {
@@ -494,23 +581,63 @@ impl ProverBackend for IsabelleBackend {
494581 tokio:: fs:: write ( temp_dir. join ( format ! ( "{}.thy" , theory_name) ) , raw)
495582 . await
496583 . context ( "Failed to write raw theory file" ) ?;
497- let root = format ! (
498- "session UserSession = HOL +\n theories\n {}\n " ,
499- theory_name
500- ) ;
501- tokio:: fs:: write ( temp_dir. join ( "ROOT" ) , root)
584+
585+ // EI-1: when a project root is supplied, parse its ROOT to find a
586+ // parent session, declare the probe session as `= ParentSession +`,
587+ // and add `-d <project_root>` to the build invocation. This is
588+ // what makes goals importing project-specific theories
589+ // (Tropical_v2, walks_def, ...) parse instead of failing.
590+ let ( root_text, parent_session, project_root_arg) =
591+ if let Some ( pr) = self . config . project_root . as_ref ( ) {
592+ let project_root_path = pr. clone ( ) ;
593+ let project_root_file = project_root_path. join ( "ROOT" ) ;
594+ let parent = match tokio:: fs:: read_to_string ( & project_root_file) . await {
595+ Ok ( text) => detect_parent_session ( & text, & theory_name) ,
596+ Err ( e) => {
597+ tracing:: warn!(
598+ project_root = %project_root_path. display( ) ,
599+ err = %e,
600+ "EI-1: project_root supplied but ROOT not readable; falling back to HOL"
601+ ) ;
602+ "HOL" . to_string ( )
603+ }
604+ } ;
605+ (
606+ format ! (
607+ "session UserSession = \" {parent}\" +\n theories\n {theory_name}\n "
608+ ) ,
609+ parent,
610+ Some ( project_root_path) ,
611+ )
612+ } else {
613+ (
614+ format ! ( "session UserSession = HOL +\n theories\n {theory_name}\n " ) ,
615+ "HOL" . to_string ( ) ,
616+ None ,
617+ )
618+ } ;
619+
620+ tokio:: fs:: write ( temp_dir. join ( "ROOT" ) , & root_text)
502621 . await
503622 . context ( "Failed to write ROOT file" ) ?;
504623
505- let output = tokio:: process:: Command :: new ( & self . config . executable )
506- . arg ( "build" )
507- . arg ( "-D" )
624+ let mut cmd = tokio:: process:: Command :: new ( & self . config . executable ) ;
625+ cmd. arg ( "build" ) ;
626+ if let Some ( pr) = project_root_arg. as_ref ( ) {
627+ cmd. arg ( "-d" ) . arg ( pr) ;
628+ tracing:: info!(
629+ project_root = %pr. display( ) ,
630+ parent_session = %parent_session,
631+ "EI-1: project-aware probe (parent session detected from ROOT)"
632+ ) ;
633+ }
634+ cmd. arg ( "-D" )
508635 . arg ( & temp_dir)
509636 . arg ( "-o" )
510637 . arg ( "document=false" )
511638 . arg ( "-o" )
512- . arg ( "browser_info=false" )
513- . output ( )
639+ . arg ( "browser_info=false" ) ;
640+ let output = cmd . output ( )
514641 . await
515642 . context ( "Failed to run Isabelle build" ) ?;
516643
@@ -772,4 +899,45 @@ mod tests {
772899 assert ! ( matches!( & state. goals[ 0 ] . target, Term :: Const ( c) if c. starts_with( "<check:" ) ) ) ;
773900 assert_eq ! ( state. context. theorems. len( ) , 0 ) ;
774901 }
902+
903+ // --- EI-1: project-aware probe (parent-session detection) -------------
904+
905+ #[ test]
906+ fn test_detect_parent_session_finds_owning_session ( ) {
907+ // Mirrors the live tropical-resource-typing/ROOT structure.
908+ let root = r#"
909+ session Tropical_Semirings in "." = "HOL-Library" +
910+ description "tropical algebra"
911+ sessions
912+ "HOL-Combinatorics"
913+ theories
914+ Tropical_v2
915+ Tropical_Matrices_Full
916+ Tropical_Kleene
917+ "# ;
918+ assert_eq ! ( detect_parent_session( root, "Tropical_v2" ) , "Tropical_Semirings" ) ;
919+ assert_eq ! ( detect_parent_session( root, "Tropical_Kleene" ) , "Tropical_Semirings" ) ;
920+ }
921+
922+ #[ test]
923+ fn test_detect_parent_session_falls_back_to_first_session ( ) {
924+ let root = r#"
925+ session OnlyOne = HOL +
926+ theories
927+ SomethingElse
928+ "# ;
929+ // theory_name not declared → first session wins.
930+ assert_eq ! ( detect_parent_session( root, "Missing_Theory" ) , "OnlyOne" ) ;
931+ }
932+
933+ #[ test]
934+ fn test_detect_parent_session_falls_back_to_hol_when_root_empty ( ) {
935+ assert_eq ! ( detect_parent_session( "" , "Anything" ) , "HOL" ) ;
936+ }
937+
938+ #[ test]
939+ fn test_detect_parent_session_handles_inline_theories_list ( ) {
940+ let root = "session Inline = HOL +\n theories Foo Bar Baz\n " ;
941+ assert_eq ! ( detect_parent_session( root, "Bar" ) , "Inline" ) ;
942+ }
775943}
0 commit comments