1use std::collections::{BTreeMap, BTreeSet};
2
3use type_bridge_contract::capability::{CapabilityId, CapabilitySet};
4use type_bridge_contract::codec::FormatVersion;
5use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory};
6use type_bridge_contract::id::{Label, RoleId, TypeId, TypeKind};
7use type_bridge_contract::schema::{
8 DeclaredSchema, PlaysFact, PlaysFactId, RelatesFact, RelatesFactId, SchemaDiagnostic,
9 SchemaDiagnostics, SchemaFact, SchemaFactId, SourceSpan, SourcedSchemaFact, StructFact,
10};
11
12use crate::diagnostic::{diagnostic, diagnostic_with_related};
13
14struct PendingRelates {
15 id: RelatesFactId,
16 specializes: Option<(Label, SourceSpan)>,
17 source: SourceSpan,
18}
19
20struct PendingPlays {
21 player: Label,
22 relation: Label,
23 role: Label,
24 source: SourceSpan,
25}
26
27pub struct FactAssembler {
33 format: FormatVersion,
34 facts: Vec<SourcedSchemaFact>,
35 fact_sources: BTreeMap<SchemaFactId, SourceSpan>,
36 capabilities: CapabilitySet,
37 capability_sources: BTreeMap<CapabilityId, SourceSpan>,
38 declaration_labels: BTreeMap<Label, (SchemaFactId, SourceSpan)>,
39 type_labels: BTreeMap<Label, (TypeId, SourceSpan)>,
40 direct_parents: BTreeMap<TypeId, (TypeId, SourceSpan)>,
41 pending_relates: Vec<PendingRelates>,
42 pending_relates_sources: BTreeMap<RelatesFactId, SourceSpan>,
43 pending_plays: Vec<PendingPlays>,
44}
45
46impl FactAssembler {
47 #[must_use]
49 pub fn new(format: FormatVersion) -> Self {
50 Self {
51 format,
52 facts: Vec::new(),
53 fact_sources: BTreeMap::new(),
54 capabilities: CapabilitySet::new(),
55 capability_sources: BTreeMap::new(),
56 declaration_labels: BTreeMap::new(),
57 type_labels: BTreeMap::new(),
58 direct_parents: BTreeMap::new(),
59 pending_relates: Vec::new(),
60 pending_relates_sources: BTreeMap::new(),
61 pending_plays: Vec::new(),
62 }
63 }
64
65 pub fn require_capability(
67 &mut self,
68 capability: CapabilityId,
69 source: SourceSpan,
70 ) -> Result<(), SchemaDiagnostics> {
71 if let Some(previous) = self.capability_sources.get(&capability) {
72 return Err(diagnostic_with_related(
73 DiagnosticCategory::InvalidContract,
74 "duplicate_required_capability",
75 format!("required capability `{capability}` is declared more than once"),
76 source,
77 previous.clone(),
78 "first capability requirement is here",
79 ));
80 }
81 self.capability_sources.insert(capability.clone(), source);
82 self.capabilities.insert(capability);
83 Ok(())
84 }
85
86 pub fn insert_fact(
88 &mut self,
89 fact: SchemaFact,
90 source: SourceSpan,
91 ) -> Result<(), SchemaDiagnostics> {
92 let id = fact.id();
93 if let Some(previous) = self.fact_sources.get(&id) {
94 return Err(diagnostic_with_related(
95 DiagnosticCategory::InvalidContract,
96 "duplicate_schema_fact",
97 "a direct schema fact is declared more than once",
98 source,
99 previous.clone(),
100 "first declaration is here",
101 ));
102 }
103
104 let declaration_label = match &fact {
105 SchemaFact::Type(type_fact) => Some(type_fact.id().label()),
106 SchemaFact::Struct(struct_fact) => Some(struct_fact.id().label()),
107 _ => None,
108 };
109 if let Some(label) = declaration_label {
110 if let Some((previous_id, previous_source)) = self.declaration_labels.get(label) {
111 let (code, message, related_message) = if matches!(
112 (&id, previous_id),
113 (SchemaFactId::Type(_), SchemaFactId::Type(_))
114 ) {
115 (
116 "duplicate_schema_type_label",
117 format!("schema label `{label}` is declared with more than one type kind"),
118 "first type declaration is here",
119 )
120 } else {
121 (
122 "duplicate_schema_label",
123 format!("schema label `{label}` is declared as both a type and a struct"),
124 "first type or struct declaration is here",
125 )
126 };
127 return Err(diagnostic_with_related(
128 DiagnosticCategory::InvalidContract,
129 code,
130 message,
131 source,
132 previous_source.clone(),
133 related_message,
134 ));
135 }
136 self.declaration_labels
137 .insert(label.clone(), (id.clone(), source.clone()));
138 }
139
140 if let SchemaFact::Type(type_fact) = &fact {
141 let type_id = type_fact.id();
142 self.type_labels
143 .insert(type_id.label().clone(), (type_id.clone(), source.clone()));
144 }
145
146 if let SchemaFact::Sub(sub_fact) = &fact {
147 let subtype = sub_fact.id().subtype();
148 let supertype = sub_fact.id().supertype();
149 if let Some((previous_parent, previous_source)) = self.direct_parents.get(subtype) {
150 if previous_parent != supertype {
151 return Err(diagnostic_with_related(
152 DiagnosticCategory::InvalidContract,
153 "multiple_direct_schema_parents",
154 format!(
155 "schema type `{}` has more than one direct parent",
156 subtype.label()
157 ),
158 source,
159 previous_source.clone(),
160 "first direct parent is declared here",
161 ));
162 }
163 } else {
164 self.direct_parents
165 .insert(subtype.clone(), (supertype.clone(), source.clone()));
166 }
167 }
168
169 self.fact_sources.insert(id, source.clone());
170 self.facts.push(SourcedSchemaFact::new(fact, source));
171 Ok(())
172 }
173
174 pub fn insert_struct(
176 &mut self,
177 fact: StructFact,
178 source: SourceSpan,
179 ) -> Result<(), SchemaDiagnostics> {
180 self.insert_fact(SchemaFact::Struct(fact), source)
181 }
182
183 pub fn insert_relates(
185 &mut self,
186 id: RelatesFactId,
187 specializes: Option<(Label, SourceSpan)>,
188 source: SourceSpan,
189 ) -> Result<(), SchemaDiagnostics> {
190 if let Some(previous) = self.pending_relates_sources.get(&id) {
191 return Err(diagnostic_with_related(
192 DiagnosticCategory::InvalidContract,
193 "duplicate_schema_fact",
194 "a direct related-role fact is declared more than once",
195 source,
196 previous.clone(),
197 "first declaration is here",
198 ));
199 }
200 if let Some(previous) = self.fact_sources.get(&SchemaFactId::Relates(id.clone())) {
201 return Err(diagnostic_with_related(
202 DiagnosticCategory::InvalidContract,
203 "duplicate_schema_fact",
204 "a direct related-role fact is declared more than once",
205 source,
206 previous.clone(),
207 "first declaration is here",
208 ));
209 }
210 self.pending_relates_sources
211 .insert(id.clone(), source.clone());
212 self.pending_relates.push(PendingRelates {
213 id,
214 specializes,
215 source,
216 });
217 Ok(())
218 }
219
220 pub fn insert_plays(
222 &mut self,
223 player: Label,
224 relation: Label,
225 role: Label,
226 source: SourceSpan,
227 ) {
228 self.pending_plays.push(PendingPlays {
229 player,
230 relation,
231 role,
232 source,
233 });
234 }
235
236 pub fn finish(mut self) -> Result<DeclaredSchema, SchemaDiagnostics> {
238 self.materialize_relates()?;
239 self.materialize_plays()?;
240 DeclaredSchema::from_facts(self.format, self.capabilities, self.facts)
241 }
242
243 fn materialize_relates(&mut self) -> Result<(), SchemaDiagnostics> {
244 let declarations = std::mem::take(&mut self.pending_relates);
245 for declaration in &declarations {
246 let specializes = declaration
247 .specializes
248 .as_ref()
249 .map(|(label, source)| {
250 self.resolve_inherited_role(
251 declaration.id.relation(),
252 label,
253 source,
254 &declarations,
255 )
256 })
257 .transpose()?;
258 let fact = RelatesFact::new(declaration.id.clone(), specializes)
259 .map_err(|error| contract(error, declaration.source.clone()))?;
260 self.insert_fact(SchemaFact::Relates(fact), declaration.source.clone())?;
261 }
262 Ok(())
263 }
264
265 fn materialize_plays(&mut self) -> Result<(), SchemaDiagnostics> {
266 let declarations = std::mem::take(&mut self.pending_plays);
267 for declaration in declarations {
268 let player = self
269 .type_labels
270 .get(&declaration.player)
271 .map(|(id, _)| id.clone())
272 .ok_or_else(|| {
273 diagnostic(
274 DiagnosticCategory::InvalidContract,
275 "unknown_schema_player",
276 format!("unknown playing type `{}`", declaration.player),
277 Some(declaration.source.clone()),
278 )
279 })?;
280 if !matches!(player.kind(), TypeKind::Entity | TypeKind::Relation) {
281 return Err(diagnostic(
282 DiagnosticCategory::InvalidContract,
283 "invalid_schema_player_kind",
284 "only entity and relation types can play roles",
285 Some(declaration.source),
286 ));
287 }
288 let relation = self
289 .type_labels
290 .get(&declaration.relation)
291 .map(|(id, _)| id.clone())
292 .ok_or_else(|| {
293 diagnostic(
294 DiagnosticCategory::InvalidContract,
295 "unknown_schema_relation",
296 format!("unknown relation type `{}`", declaration.relation),
297 Some(declaration.source.clone()),
298 )
299 })?;
300 if relation.kind() != TypeKind::Relation {
301 return Err(diagnostic(
302 DiagnosticCategory::InvalidContract,
303 "invalid_schema_relation_kind",
304 "a playing relation reference must name a relation type",
305 Some(declaration.source),
306 ));
307 }
308 let role = RoleId::new(relation.label().as_str(), declaration.role.as_str())
309 .map_err(|error| contract(error, declaration.source.clone()))?;
310 let id = PlaysFactId::new(player, role)
311 .map_err(|error| contract(error, declaration.source.clone()))?;
312 self.insert_fact(SchemaFact::Plays(PlaysFact::new(id)), declaration.source)?;
313 }
314 Ok(())
315 }
316
317 fn resolve_inherited_role(
318 &self,
319 relation: &TypeId,
320 role_label: &Label,
321 source: &SourceSpan,
322 declarations: &[PendingRelates],
323 ) -> Result<RoleId, SchemaDiagnostics> {
324 let mut current = relation.clone();
325 let mut visited = BTreeSet::new();
326 loop {
327 if !visited.insert(current.clone()) {
328 return Err(diagnostic(
329 DiagnosticCategory::InvalidContract,
330 "schema_inheritance_cycle",
331 "relation inheritance contains a cycle",
332 Some(source.clone()),
333 ));
334 }
335 let Some((parent, _)) = self.direct_parents.get(¤t) else {
336 return Err(diagnostic(
337 DiagnosticCategory::InvalidContract,
338 "invalid_role_specialization",
339 "specialized role is not declared by any ancestor relation",
340 Some(source.clone()),
341 ));
342 };
343 if let Some(role) = declarations.iter().find_map(|candidate| {
344 (candidate.id.relation() == parent && candidate.id.role().label() == role_label)
345 .then(|| candidate.id.role().clone())
346 }) {
347 return Ok(role);
348 }
349 if let Some(role) = self.facts.iter().find_map(|sourced| {
350 let SchemaFact::Relates(fact) = sourced.fact() else {
351 return None;
352 };
353 (fact.id().relation() == parent && fact.id().role().label() == role_label)
354 .then(|| fact.id().role().clone())
355 }) {
356 return Ok(role);
357 }
358 current = parent.clone();
359 }
360 }
361}
362
363fn contract(error: Diagnostic, source: SourceSpan) -> SchemaDiagnostics {
364 SchemaDiagnostics::one(SchemaDiagnostic::new(error, Some(source)))
365}