1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::path::{Component, Path, PathBuf};
4
5use serde::Serialize;
6use sha2::{Digest, Sha256};
7
8use crate::ast::Program;
9use crate::incremental_parse::{ParserSession, StatementSpan};
10use crate::module::LoadedModule;
11use crate::parser::parse_program;
12
13use super::ModuleResolver;
14
15const MANIFEST_SCHEMA_VERSION: &str = "xlog.resolved-program-manifest.v1";
16const MODULE_ID_DOMAIN: &[u8] = b"xlog.resolved-module.v1\0";
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
20pub struct ResolvedProgramManifest {
21 pub schema_version: String,
23 pub entry_module_id: String,
25 pub modules: Vec<ResolvedModuleManifest>,
27 pub imports: Vec<ResolvedImportManifest>,
29 pub construct_inventory: Vec<ResolvedConstructCount>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
35pub struct ResolvedModuleManifest {
36 pub module_id: String,
38 pub source_path: String,
40 pub content_sha256: String,
42 pub logical_paths: Vec<String>,
44 pub source_objects: Vec<ResolvedSourceObject>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
50pub struct ResolvedImportManifest {
51 pub importer_module_id: String,
53 pub target_module_id: String,
55 pub authored_ordinal: usize,
57 pub source_object_id: String,
59 pub declared_path: Vec<String>,
61 pub resolved_path: Vec<String>,
63 pub imported_items: Option<Vec<String>>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
69pub struct ResolvedSourceObject {
70 pub object_id: String,
72 pub kind: ResolvedSourceObjectKind,
74 pub provenance: ResolvedSourceObjectProvenance,
76 pub authored_ordinal: usize,
78 pub kind_ordinal: usize,
80 pub statement_ordinal: usize,
82 pub content_sha256: String,
84 pub span: ResolvedSourceSpan,
86 pub primary_name: Option<String>,
88 pub arity: Option<usize>,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
94#[serde(rename_all = "snake_case")]
95pub enum ResolvedSourceObjectProvenance {
96 Authored,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
102pub struct ResolvedSourceSpan {
103 pub start: usize,
105 pub end: usize,
107 pub line: usize,
109 pub column: usize,
111}
112
113impl From<StatementSpan> for ResolvedSourceSpan {
114 fn from(span: StatementSpan) -> Self {
115 Self {
116 start: span.start,
117 end: span.end,
118 line: span.line,
119 column: span.column,
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
126#[serde(rename_all = "snake_case")]
127pub enum ResolvedSourceObjectKind {
128 Import,
130 Function,
132 Domain,
134 Predicate,
136 Rule,
138 Constraint,
140 Query,
142 ProbabilisticFact,
144 AnnotatedDisjunction,
146 Evidence,
148 ProbabilisticQuery,
150 NeuralPredicate,
152 LearnableRule,
154 Directive,
156}
157
158impl ResolvedSourceObjectKind {
159 fn as_str(self) -> &'static str {
160 match self {
161 Self::Import => "import",
162 Self::Function => "function",
163 Self::Domain => "domain",
164 Self::Predicate => "predicate",
165 Self::Rule => "rule",
166 Self::Constraint => "constraint",
167 Self::Query => "query",
168 Self::ProbabilisticFact => "probabilistic_fact",
169 Self::AnnotatedDisjunction => "annotated_disjunction",
170 Self::Evidence => "evidence",
171 Self::ProbabilisticQuery => "probabilistic_query",
172 Self::NeuralPredicate => "neural_predicate",
173 Self::LearnableRule => "learnable_rule",
174 Self::Directive => "directive",
175 }
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
181pub struct ResolvedConstructCount {
182 pub kind: ResolvedSourceObjectKind,
184 pub count: usize,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
190pub enum ResolvedProgramManifestError {
191 MissingEntry,
193 ModuleValidation {
195 message: String,
197 },
198 SourceRoot {
200 path: PathBuf,
202 message: String,
204 },
205 SourceOutsideRoot {
207 path: PathBuf,
209 source_root: PathBuf,
211 },
212 NonUtf8Path {
214 path: PathBuf,
216 },
217 MissingSourceText {
219 path: PathBuf,
221 },
222 MissingLogicalPath {
224 path: PathBuf,
226 },
227 StatementParse {
229 source_path: String,
231 statement_ordinal: usize,
233 message: String,
235 },
236 InvalidStatementSpan {
238 source_path: String,
240 statement_ordinal: usize,
242 start: usize,
244 end: usize,
246 source_len: usize,
248 },
249 StatementWithoutObject {
251 source_path: String,
253 statement_ordinal: usize,
255 },
256 ImportGraphMismatch {
258 source_path: String,
260 declarations: usize,
262 resolved_edges: usize,
264 import_objects: usize,
266 },
267 MissingImportTarget {
269 path: PathBuf,
271 },
272}
273
274impl fmt::Display for ResolvedProgramManifestError {
275 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
276 match self {
277 Self::MissingEntry => write!(
278 formatter,
279 "resolved program manifest requires an entry file"
280 ),
281 Self::ModuleValidation { message } => {
282 write!(formatter, "resolved program validation failed: {message}")
283 }
284 Self::SourceRoot { path, message } => write!(
285 formatter,
286 "invalid source root '{}': {message}",
287 path.display()
288 ),
289 Self::SourceOutsideRoot { path, source_root } => write!(
290 formatter,
291 "resolved source '{}' is outside source root '{}'",
292 path.display(),
293 source_root.display()
294 ),
295 Self::NonUtf8Path { path } => {
296 write!(
297 formatter,
298 "resolved source path is not UTF-8: '{}'",
299 path.display()
300 )
301 }
302 Self::MissingSourceText { path } => write!(
303 formatter,
304 "resolver did not retain parsed source bytes for '{}'",
305 path.display()
306 ),
307 Self::MissingLogicalPath { path } => write!(
308 formatter,
309 "resolved source has no logical path in the current entry closure: '{}'",
310 path.display()
311 ),
312 Self::StatementParse {
313 source_path,
314 statement_ordinal,
315 message,
316 } => write!(
317 formatter,
318 "failed to classify statement {statement_ordinal} in '{source_path}': {message}"
319 ),
320 Self::InvalidStatementSpan {
321 source_path,
322 statement_ordinal,
323 start,
324 end,
325 source_len,
326 } => write!(
327 formatter,
328 "invalid byte span {start}..{end} for statement {statement_ordinal} in \
329 '{source_path}' (source length {source_len})"
330 ),
331 Self::StatementWithoutObject {
332 source_path,
333 statement_ordinal,
334 } => write!(
335 formatter,
336 "statement {statement_ordinal} in '{source_path}' produced no source object"
337 ),
338 Self::ImportGraphMismatch {
339 source_path,
340 declarations,
341 resolved_edges,
342 import_objects,
343 } => write!(
344 formatter,
345 "import graph mismatch in '{source_path}': {declarations} declarations, \
346 {resolved_edges} resolved edges, {import_objects} import objects"
347 ),
348 Self::MissingImportTarget { path } => write!(
349 formatter,
350 "resolved import target is absent from the manifest closure: '{}'",
351 path.display()
352 ),
353 }
354 }
355}
356
357impl std::error::Error for ResolvedProgramManifestError {}
358
359#[derive(Clone)]
360struct ModuleIdentity {
361 module_id: String,
362 source_path: String,
363 content_sha256: String,
364}
365
366struct SourceModule<'a> {
367 source: &'a PathBuf,
368 module: &'a LoadedModule,
369 source_text: &'a str,
370 is_entry: bool,
371}
372
373#[derive(Debug)]
374struct ObjectDescriptor {
375 kind: ResolvedSourceObjectKind,
376 primary_name: Option<String>,
377 arity: Option<usize>,
378}
379
380impl ModuleResolver {
381 pub fn resolved_program_manifest(
387 &self,
388 source_root: &Path,
389 ) -> Result<ResolvedProgramManifest, ResolvedProgramManifestError> {
390 let source_root = std::fs::canonicalize(source_root).map_err(|error| {
391 ResolvedProgramManifestError::SourceRoot {
392 path: source_root.to_path_buf(),
393 message: error.to_string(),
394 }
395 })?;
396 if !source_root.is_dir() {
397 return Err(ResolvedProgramManifestError::SourceRoot {
398 path: source_root,
399 message: "path is not a directory".to_string(),
400 });
401 }
402 let entry = self
403 .entry
404 .as_ref()
405 .ok_or(ResolvedProgramManifestError::MissingEntry)?;
406 let entry_source = self
407 .entry_source
408 .as_ref()
409 .ok_or(ResolvedProgramManifestError::MissingEntry)?;
410 let entry_source_text = self.entry_source_text.as_deref().ok_or_else(|| {
411 ResolvedProgramManifestError::MissingSourceText {
412 path: entry_source.clone(),
413 }
414 })?;
415 self.merge_imports(entry.program.clone()).map_err(|error| {
416 ResolvedProgramManifestError::ModuleValidation {
417 message: error.to_string(),
418 }
419 })?;
420
421 let mut reachable_sources = BTreeSet::new();
422 let mut pending_sources = self
423 .entry_resolved_imports
424 .iter()
425 .map(|edge| edge.source.clone())
426 .collect::<Vec<_>>();
427 while let Some(source) = pending_sources.pop() {
428 if !reachable_sources.insert(source.clone()) {
429 continue;
430 }
431 if let Some(edges) = self.resolved_imports.get(&source) {
432 pending_sources.extend(edges.iter().map(|edge| edge.source.clone()));
433 }
434 }
435
436 let mut source_modules = vec![SourceModule {
437 source: entry_source,
438 module: entry,
439 source_text: entry_source_text,
440 is_entry: true,
441 }];
442 for source in &reachable_sources {
443 let module = self.loaded.get(source).ok_or_else(|| {
444 ResolvedProgramManifestError::MissingImportTarget {
445 path: source.clone(),
446 }
447 })?;
448 let source_text = self.loaded_source_texts.get(source).ok_or_else(|| {
449 ResolvedProgramManifestError::MissingSourceText {
450 path: source.clone(),
451 }
452 })?;
453 source_modules.push(SourceModule {
454 source,
455 module,
456 source_text,
457 is_entry: false,
458 });
459 }
460
461 let mut identities = BTreeMap::new();
462 for source_module in &source_modules {
463 let source_path = relative_source_path(&source_root, source_module.source)?;
464 let content_sha256 = sha256_prefixed(source_module.source_text.as_bytes());
465 let module_id = module_id(&source_path, &content_sha256);
466 identities.insert(
467 source_module.source.clone(),
468 ModuleIdentity {
469 module_id,
470 source_path,
471 content_sha256,
472 },
473 );
474 }
475 source_modules.sort_by(|left, right| {
476 identities[left.source]
477 .source_path
478 .cmp(&identities[right.source].source_path)
479 });
480
481 let mut logical_paths_by_source = BTreeMap::<PathBuf, BTreeSet<String>>::new();
482 logical_paths_by_source
483 .entry(entry_source.clone())
484 .or_default()
485 .insert(entry.path.join("/"));
486 for edge in &self.entry_resolved_imports {
487 logical_paths_by_source
488 .entry(edge.source.clone())
489 .or_default()
490 .insert(edge.module_path.join("/"));
491 }
492 for source in &reachable_sources {
493 if let Some(edges) = self.resolved_imports.get(source) {
494 for edge in edges {
495 logical_paths_by_source
496 .entry(edge.source.clone())
497 .or_default()
498 .insert(edge.module_path.join("/"));
499 }
500 }
501 }
502
503 let entry_module_id = identities[entry_source].module_id.clone();
504 let mut modules = Vec::with_capacity(source_modules.len());
505 for source_module in &source_modules {
506 let identity = &identities[source_module.source];
507 let logical_paths = logical_paths_by_source
508 .get(source_module.source)
509 .ok_or_else(|| ResolvedProgramManifestError::MissingLogicalPath {
510 path: source_module.source.clone(),
511 })?;
512 modules.push(ResolvedModuleManifest {
513 module_id: identity.module_id.clone(),
514 source_path: identity.source_path.clone(),
515 content_sha256: identity.content_sha256.clone(),
516 logical_paths: logical_paths.iter().cloned().collect(),
517 source_objects: build_source_objects(
518 &identity.module_id,
519 &identity.source_path,
520 source_module.source_text,
521 )?,
522 });
523 }
524
525 let module_by_id = modules
526 .iter()
527 .map(|module| (module.module_id.clone(), module))
528 .collect::<BTreeMap<_, _>>();
529 let mut imports = Vec::new();
530 for source_module in &source_modules {
531 let identity = &identities[source_module.source];
532 let resolved_edges = if source_module.is_entry {
533 self.entry_resolved_imports.as_slice()
534 } else {
535 self.resolved_imports
536 .get(source_module.source)
537 .map(Vec::as_slice)
538 .unwrap_or_default()
539 };
540 let import_objects = module_by_id[&identity.module_id]
541 .source_objects
542 .iter()
543 .filter(|object| object.kind == ResolvedSourceObjectKind::Import)
544 .collect::<Vec<_>>();
545 if source_module.module.program.imports.len() != resolved_edges.len()
546 || resolved_edges.len() != import_objects.len()
547 {
548 return Err(ResolvedProgramManifestError::ImportGraphMismatch {
549 source_path: identity.source_path.clone(),
550 declarations: source_module.module.program.imports.len(),
551 resolved_edges: resolved_edges.len(),
552 import_objects: import_objects.len(),
553 });
554 }
555 for (authored_ordinal, ((declaration, edge), source_object)) in source_module
556 .module
557 .program
558 .imports
559 .iter()
560 .zip(resolved_edges)
561 .zip(import_objects)
562 .enumerate()
563 {
564 let target = identities.get(&edge.source).ok_or_else(|| {
565 ResolvedProgramManifestError::MissingImportTarget {
566 path: edge.source.clone(),
567 }
568 })?;
569 imports.push(ResolvedImportManifest {
570 importer_module_id: identity.module_id.clone(),
571 target_module_id: target.module_id.clone(),
572 authored_ordinal,
573 source_object_id: source_object.object_id.clone(),
574 declared_path: declaration.module_path.clone(),
575 resolved_path: edge.module_path.clone(),
576 imported_items: declaration.imports.clone(),
577 });
578 }
579 }
580
581 let mut inventory = BTreeMap::new();
582 for object in modules.iter().flat_map(|module| &module.source_objects) {
583 *inventory.entry(object.kind).or_insert(0usize) += 1;
584 }
585
586 Ok(ResolvedProgramManifest {
587 schema_version: MANIFEST_SCHEMA_VERSION.to_string(),
588 entry_module_id,
589 modules,
590 imports,
591 construct_inventory: inventory
592 .into_iter()
593 .map(|(kind, count)| ResolvedConstructCount { kind, count })
594 .collect(),
595 })
596 }
597}
598
599fn relative_source_path(
600 source_root: &Path,
601 source: &Path,
602) -> Result<String, ResolvedProgramManifestError> {
603 let relative = source.strip_prefix(source_root).map_err(|_| {
604 ResolvedProgramManifestError::SourceOutsideRoot {
605 path: source.to_path_buf(),
606 source_root: source_root.to_path_buf(),
607 }
608 })?;
609 let mut parts = Vec::new();
610 for component in relative.components() {
611 match component {
612 Component::Normal(part) => parts.push(
613 part.to_str()
614 .ok_or_else(|| ResolvedProgramManifestError::NonUtf8Path {
615 path: source.to_path_buf(),
616 })?
617 .to_string(),
618 ),
619 Component::CurDir => {}
620 _ => {
621 return Err(ResolvedProgramManifestError::SourceOutsideRoot {
622 path: source.to_path_buf(),
623 source_root: source_root.to_path_buf(),
624 });
625 }
626 }
627 }
628 if parts.is_empty() {
629 return Err(ResolvedProgramManifestError::NonUtf8Path {
630 path: source.to_path_buf(),
631 });
632 }
633 Ok(parts.join("/"))
634}
635
636fn module_id(source_path: &str, content_sha256: &str) -> String {
637 let mut digest = Sha256::new();
638 digest.update(MODULE_ID_DOMAIN);
639 digest.update((source_path.len() as u64).to_le_bytes());
640 digest.update(source_path.as_bytes());
641 digest.update((content_sha256.len() as u64).to_le_bytes());
642 digest.update(content_sha256.as_bytes());
643 format!("sha256:{}", hex_digest(digest.finalize()))
644}
645
646fn build_source_objects(
647 module_id: &str,
648 source_path: &str,
649 source_text: &str,
650) -> Result<Vec<ResolvedSourceObject>, ResolvedProgramManifestError> {
651 let mut objects = Vec::new();
652 let mut kind_ordinals = BTreeMap::new();
653 for (statement_ordinal, statement) in ParserSession::split_statements(source_text)
654 .into_iter()
655 .enumerate()
656 {
657 let parsed = parse_program(&statement.text).map_err(|error| {
658 ResolvedProgramManifestError::StatementParse {
659 source_path: source_path.to_string(),
660 statement_ordinal,
661 message: error.to_string(),
662 }
663 })?;
664 let descriptors = describe_objects(&parsed);
665 if descriptors.is_empty() {
666 return Err(ResolvedProgramManifestError::StatementWithoutObject {
667 source_path: source_path.to_string(),
668 statement_ordinal,
669 });
670 }
671 let span = statement.span;
672 let source_bytes = source_text.as_bytes();
673 if span.start > span.end || span.end > source_bytes.len() {
674 return Err(ResolvedProgramManifestError::InvalidStatementSpan {
675 source_path: source_path.to_string(),
676 statement_ordinal,
677 start: span.start,
678 end: span.end,
679 source_len: source_bytes.len(),
680 });
681 }
682 let content_sha256 = sha256_prefixed(&source_bytes[span.start..span.end]);
683 for descriptor in descriptors {
684 let kind_ordinal = kind_ordinals.entry(descriptor.kind).or_insert(0usize);
685 let object_id = format!("{module_id}#{}:{}", descriptor.kind.as_str(), *kind_ordinal);
686 objects.push(ResolvedSourceObject {
687 object_id,
688 kind: descriptor.kind,
689 provenance: ResolvedSourceObjectProvenance::Authored,
690 authored_ordinal: objects.len(),
691 kind_ordinal: *kind_ordinal,
692 statement_ordinal,
693 content_sha256: content_sha256.clone(),
694 span: span.into(),
695 primary_name: descriptor.primary_name,
696 arity: descriptor.arity,
697 });
698 *kind_ordinal += 1;
699 }
700 }
701 Ok(objects)
702}
703
704fn describe_objects(program: &Program) -> Vec<ObjectDescriptor> {
705 let mut objects = Vec::new();
706 objects.extend(program.imports.iter().map(|declaration| ObjectDescriptor {
707 kind: ResolvedSourceObjectKind::Import,
708 primary_name: Some(declaration.module_path.join("/")),
709 arity: None,
710 }));
711 objects.extend(program.functions.iter().map(|definition| ObjectDescriptor {
712 kind: ResolvedSourceObjectKind::Function,
713 primary_name: Some(definition.name.clone()),
714 arity: Some(definition.params.len()),
715 }));
716 objects.extend(program.domains.iter().map(|declaration| ObjectDescriptor {
717 kind: ResolvedSourceObjectKind::Domain,
718 primary_name: Some(declaration.name.clone()),
719 arity: None,
720 }));
721 objects.extend(
722 program
723 .predicates
724 .iter()
725 .map(|declaration| ObjectDescriptor {
726 kind: ResolvedSourceObjectKind::Predicate,
727 primary_name: Some(declaration.name.clone()),
728 arity: Some(declaration.schema_columns().len()),
729 }),
730 );
731 objects.extend(program.rules.iter().map(|rule| ObjectDescriptor {
732 kind: ResolvedSourceObjectKind::Rule,
733 primary_name: Some(rule.head.predicate.clone()),
734 arity: Some(rule.head.terms.len()),
735 }));
736 objects.extend(program.constraints.iter().map(|_| ObjectDescriptor {
737 kind: ResolvedSourceObjectKind::Constraint,
738 primary_name: None,
739 arity: None,
740 }));
741 objects.extend(program.queries.iter().map(|query| ObjectDescriptor {
742 kind: ResolvedSourceObjectKind::Query,
743 primary_name: Some(query.atom.predicate.clone()),
744 arity: Some(query.atom.terms.len()),
745 }));
746 objects.extend(program.prob_facts.iter().map(|fact| ObjectDescriptor {
747 kind: ResolvedSourceObjectKind::ProbabilisticFact,
748 primary_name: Some(fact.atom.predicate.clone()),
749 arity: Some(fact.atom.terms.len()),
750 }));
751 objects.extend(
752 program
753 .annotated_disjunctions
754 .iter()
755 .map(|_| ObjectDescriptor {
756 kind: ResolvedSourceObjectKind::AnnotatedDisjunction,
757 primary_name: None,
758 arity: None,
759 }),
760 );
761 objects.extend(program.evidence.iter().map(|evidence| ObjectDescriptor {
762 kind: ResolvedSourceObjectKind::Evidence,
763 primary_name: Some(evidence.atom.predicate.clone()),
764 arity: Some(evidence.atom.terms.len()),
765 }));
766 objects.extend(program.prob_queries.iter().map(|query| ObjectDescriptor {
767 kind: ResolvedSourceObjectKind::ProbabilisticQuery,
768 primary_name: Some(query.atom.predicate.clone()),
769 arity: Some(query.atom.terms.len()),
770 }));
771 objects.extend(
772 program
773 .neural_predicates
774 .iter()
775 .map(|declaration| ObjectDescriptor {
776 kind: ResolvedSourceObjectKind::NeuralPredicate,
777 primary_name: Some(declaration.predicate.predicate.clone()),
778 arity: Some(declaration.predicate.terms.len()),
779 }),
780 );
781 objects.extend(program.learnable_rules.iter().map(|rule| ObjectDescriptor {
782 kind: ResolvedSourceObjectKind::LearnableRule,
783 primary_name: Some(rule.head.predicate.clone()),
784 arity: Some(rule.head.terms.len()),
785 }));
786 objects.extend(
787 program
788 .directives
789 .set_pragma_names()
790 .into_iter()
791 .map(|name| ObjectDescriptor {
792 kind: ResolvedSourceObjectKind::Directive,
793 primary_name: Some(name.to_string()),
794 arity: None,
795 }),
796 );
797 objects
798}
799
800fn sha256_prefixed(bytes: &[u8]) -> String {
801 let mut digest = Sha256::new();
802 digest.update(bytes);
803 format!("sha256:{}", hex_digest(digest.finalize()))
804}
805
806fn hex_digest(bytes: impl AsRef<[u8]>) -> String {
807 let bytes = bytes.as_ref();
808 let mut output = String::with_capacity(bytes.len() * 2);
809 for byte in bytes {
810 use std::fmt::Write as _;
811 write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
812 }
813 output
814}