Skip to main content

macroonz_compiler/recipe/account/
relation.rs

1//! Informed relation construction from authored rows and vocabularies.
2
3use super::settle::{missing_vocabulary, referenced_refusal, settle_relation_requirements};
4use super::{
5    RELATION_ROW_LIMIT, RecipeError, RecipeIssue, RecipeRelation, RecipeRelationParts,
6    RecipeRelationPayload, RecipeRelationPayloadKind, RecipeRelationRequirements,
7    RecipeRelationRow, RecipeTransitionEffect, RecipeVocabulary, RelationLowering,
8    VOCABULARY_LIMIT,
9};
10use crate::bounded::{Bounded, KeyedRoster};
11use crate::relation::{EmptyPosture, KeyedRosterRows, RepetitionPosture};
12use crate::token::{GeneratedTree, SpanHandle};
13
14impl RecipeRelationPayload {
15    /// Reads the one row-payload contract this material follows.
16    #[must_use]
17    pub const fn kind(&self) -> RecipeRelationPayloadKind {
18        match self {
19            Self::Unlabeled => RecipeRelationPayloadKind::Unlabeled,
20            Self::Path(_) => RecipeRelationPayloadKind::Path,
21            Self::ExactRust(_) => RecipeRelationPayloadKind::ExactRust,
22            Self::Transition { .. } => RecipeRelationPayloadKind::Transition,
23        }
24    }
25
26    pub(in crate::recipe) fn transition(
27        target: String,
28        target_name: crate::token::GeneratedToken,
29        effect: GeneratedTree,
30    ) -> Self {
31        Self::Transition {
32            target,
33            target_name,
34            effect: RecipeTransitionEffect::Path(effect),
35        }
36    }
37
38    pub(in crate::recipe) fn transition_exact(
39        target: String,
40        target_name: crate::token::GeneratedToken,
41        target_binding: crate::token::GeneratedToken,
42        body: GeneratedTree,
43    ) -> Self {
44        Self::Transition {
45            target,
46            target_name,
47            effect: RecipeTransitionEffect::ExactRust {
48                target_binding,
49                body,
50            },
51        }
52    }
53
54    pub(in crate::recipe) const fn transition_parts(
55        &self,
56    ) -> Option<(&str, &crate::token::GeneratedToken, &RecipeTransitionEffect)> {
57        match self {
58            Self::Transition {
59                target,
60                target_name,
61                effect,
62            } => Some((target.as_str(), target_name, effect)),
63            Self::Unlabeled | Self::Path(_) | Self::ExactRust(_) => None,
64        }
65    }
66}
67
68impl RecipeRelationRow {
69    pub(in crate::recipe) fn authored(
70        left: (String, crate::token::GeneratedToken, SpanHandle),
71        right: (String, crate::token::GeneratedToken, SpanHandle),
72        payload: RecipeRelationPayload,
73        payload_at: SpanHandle,
74        effect_binding_at: Option<SpanHandle>,
75    ) -> Self {
76        Self {
77            left: left.0,
78            left_name: left.1,
79            left_at: left.2,
80            right: right.0,
81            right_name: right.1,
82            right_at: right.2,
83            payload,
84            payload_at,
85            effect_binding_at,
86        }
87    }
88
89    /// Reads the left endpoint member spelling.
90    #[must_use]
91    pub fn left(&self) -> &str {
92        self.left.as_str()
93    }
94
95    /// Reads the exact ordinary or raw identifier token naming the left endpoint.
96    #[must_use]
97    pub const fn left_name_token(&self) -> &crate::token::GeneratedToken {
98        &self.left_name
99    }
100
101    /// Reads the right endpoint member spelling.
102    #[must_use]
103    pub fn right(&self) -> &str {
104        self.right.as_str()
105    }
106
107    /// Reads the exact ordinary or raw identifier token naming the right endpoint.
108    #[must_use]
109    pub const fn right_name_token(&self) -> &crate::token::GeneratedToken {
110        &self.right_name
111    }
112
113    /// Reads the caller-owned material attached to this relation row.
114    #[must_use]
115    pub const fn payload(&self) -> &RecipeRelationPayload {
116        &self.payload
117    }
118
119    pub(in crate::recipe) const fn effect_binding_at(&self) -> Option<SpanHandle> {
120        self.effect_binding_at
121    }
122}
123
124impl RecipeRelationRequirements {
125    pub(in crate::recipe) const fn unspecified() -> Self {
126        Self {
127            empty: None,
128            repetition: None,
129            membership: None,
130            completeness: None,
131            density: None,
132            absence: None,
133            self_relation: None,
134            cycle: None,
135        }
136    }
137
138    pub(in crate::recipe) const fn transitions(absence: crate::relation::AbsencePosture) -> Self {
139        Self {
140            empty: Some(EmptyPosture::Refusal),
141            repetition: Some(RepetitionPosture::Refusal),
142            membership: None,
143            completeness: None,
144            density: None,
145            absence: Some(absence),
146            self_relation: None,
147            cycle: None,
148        }
149    }
150
151    pub(in crate::recipe) fn with_empty(mut self, posture: EmptyPosture) -> Option<Self> {
152        (self.empty.is_none()).then(|| {
153            self.empty = Some(posture);
154            self
155        })
156    }
157
158    pub(in crate::recipe) fn with_repetition(mut self, posture: RepetitionPosture) -> Option<Self> {
159        (self.repetition.is_none()).then(|| {
160            self.repetition = Some(posture);
161            self
162        })
163    }
164
165    pub(in crate::recipe) fn with_membership(
166        mut self,
167        left: crate::relation::MembershipPosture,
168        right: crate::relation::MembershipPosture,
169    ) -> Option<Self> {
170        self.membership.is_none().then(|| {
171            self.membership = Some([left, right]);
172            self
173        })
174    }
175
176    pub(in crate::recipe) fn with_completeness(
177        mut self,
178        left: crate::relation::CompletenessPosture,
179        right: crate::relation::CompletenessPosture,
180    ) -> Option<Self> {
181        self.completeness.is_none().then(|| {
182            self.completeness = Some([left, right]);
183            self
184        })
185    }
186
187    pub(in crate::recipe) fn with_density(
188        mut self,
189        posture: crate::relation::DensityPosture,
190    ) -> Option<Self> {
191        (self.density.is_none()).then(|| {
192            self.density = Some(posture);
193            self
194        })
195    }
196
197    pub(in crate::recipe) fn with_absence(
198        mut self,
199        posture: crate::relation::AbsencePosture,
200    ) -> Option<Self> {
201        (self.absence.is_none()).then(|| {
202            self.absence = Some(posture);
203            self
204        })
205    }
206
207    pub(in crate::recipe) fn with_self_relation(
208        mut self,
209        posture: crate::relation::SelfRelationPosture,
210    ) -> Option<Self> {
211        (self.self_relation.is_none()).then(|| {
212            self.self_relation = Some(posture);
213            self
214        })
215    }
216
217    pub(in crate::recipe) fn with_cycle(
218        mut self,
219        posture: crate::relation::CyclePosture,
220    ) -> Option<Self> {
221        (self.cycle.is_none()).then(|| {
222            self.cycle = Some(posture);
223            self
224        })
225    }
226
227    /// Reads the declared empty-relation posture when the caller asked that question.
228    #[must_use]
229    pub const fn empty(&self) -> Option<EmptyPosture> {
230        self.empty
231    }
232
233    /// Reads the declared repetition posture when the caller asked that question.
234    #[must_use]
235    pub const fn repetition(&self) -> Option<RepetitionPosture> {
236        self.repetition
237    }
238
239    /// Reads the declared left-membership posture when the caller asked that question.
240    #[must_use]
241    pub const fn left_membership(&self) -> Option<crate::relation::MembershipPosture> {
242        match self.membership {
243            Some([left, _]) => Some(left),
244            None => None,
245        }
246    }
247
248    /// Reads the declared right-membership posture when the caller asked that question.
249    #[must_use]
250    pub const fn right_membership(&self) -> Option<crate::relation::MembershipPosture> {
251        match self.membership {
252            Some([_, right]) => Some(right),
253            None => None,
254        }
255    }
256
257    /// Reads the declared left-completeness posture when the caller asked that question.
258    #[must_use]
259    pub const fn left_completeness(&self) -> Option<crate::relation::CompletenessPosture> {
260        match self.completeness {
261            Some([left, _]) => Some(left),
262            None => None,
263        }
264    }
265
266    /// Reads the declared right-completeness posture when the caller asked that question.
267    #[must_use]
268    pub const fn right_completeness(&self) -> Option<crate::relation::CompletenessPosture> {
269        match self.completeness {
270            Some([_, right]) => Some(right),
271            None => None,
272        }
273    }
274
275    /// Reads the declared density posture when the caller asked that question.
276    #[must_use]
277    pub const fn density(&self) -> Option<crate::relation::DensityPosture> {
278        self.density
279    }
280
281    /// Reads the declared absent-row posture when the caller asked that question.
282    #[must_use]
283    pub const fn absence(&self) -> Option<crate::relation::AbsencePosture> {
284        self.absence
285    }
286
287    /// Reads the declared self-relation posture when the caller asked that question.
288    #[must_use]
289    pub const fn self_relation(&self) -> Option<crate::relation::SelfRelationPosture> {
290        self.self_relation
291    }
292
293    /// Reads the declared cycle posture when the caller asked that question.
294    #[must_use]
295    pub const fn cycle(&self) -> Option<crate::relation::CyclePosture> {
296        self.cycle
297    }
298}
299
300impl RecipeRelation {
301    pub(super) fn informed(
302        parts: RecipeRelationParts,
303        vocabularies: &KeyedRoster<RecipeVocabulary, String, VOCABULARY_LIMIT>,
304        lowering: RelationLowering,
305    ) -> Result<Self, RecipeError> {
306        let RecipeRelationParts {
307            name,
308            name_token,
309            name_at,
310            left_vocabulary,
311            left_vocabulary_at,
312            right_vocabulary,
313            right_vocabulary_at,
314            rows,
315            requirements,
316        } = parts;
317        let at = rows.first().map(|row| row.left_at);
318        let left = vocabularies.get(left_vocabulary.as_str()).ok_or_else(|| {
319            missing_vocabulary(left_vocabulary.as_str(), Some(left_vocabulary_at))
320        })?;
321        let right = vocabularies.get(right_vocabulary.as_str()).ok_or_else(|| {
322            missing_vocabulary(right_vocabulary.as_str(), Some(right_vocabulary_at))
323        })?;
324        let informed = KeyedRosterRows::referenced(
325            left.members(),
326            right.members(),
327            rows.clone(),
328            |row| row.left.clone(),
329            |row| row.right.clone(),
330        )
331        .map_err(|refusal| referenced_refusal(left.name(), right.name(), &rows, refusal))?;
332        let payload_kind = rows
333            .first()
334            .map_or(RecipeRelationPayloadKind::Unlabeled, |row| {
335                row.payload.kind()
336            });
337        if let Some(row) = rows.iter().find(|row| row.payload.kind() != payload_kind) {
338            return Err(RecipeError::at(
339                RecipeIssue::RelationPayloadShapeMismatch {
340                    relation: name,
341                    expected: payload_kind,
342                    observed: row.payload.kind(),
343                },
344                Some(row.payload_at),
345            ));
346        }
347        if requirements.repetition == Some(RepetitionPosture::Refusal)
348            && let Err(repeated) = informed.clone().distinct()
349        {
350            let Some(pair) = repeated.iter().next() else {
351                return Err(RecipeError::at(RecipeIssue::FragmentNotGenerated, at));
352            };
353            let Some(repeated_position) = pair.repeated_positions().iter().next().copied() else {
354                return Err(RecipeError::at(RecipeIssue::FragmentNotGenerated, at));
355            };
356            let Some(row) = rows.get(repeated_position) else {
357                return Err(RecipeError::at(RecipeIssue::FragmentNotGenerated, at));
358            };
359            let issue = if lowering == RelationLowering::Transition {
360                RecipeIssue::DuplicateTransition {
361                    state: row.left.clone(),
362                    event: row.right.clone(),
363                }
364            } else {
365                RecipeIssue::DuplicateRelationRow {
366                    relation: name.clone(),
367                    left: row.left.clone(),
368                    right: row.right.clone(),
369                }
370            };
371            return Err(RecipeError::at(issue, Some(row.left_at)));
372        }
373        settle_relation_requirements(name.as_str(), &informed, requirements, at)?;
374        let overflow_at = rows.get(RELATION_ROW_LIMIT).map(|row| row.left_at);
375        let rows = Bounded::new(rows).map_err(|overflow| {
376            RecipeError::at(
377                RecipeIssue::Grammar(crate::token::CaptureReadIssue::SequenceUnbounded {
378                    limit: overflow.capacity,
379                }),
380                overflow_at.or(at),
381            )
382        })?;
383        Ok(Self {
384            name,
385            name_token,
386            name_at,
387            left_vocabulary,
388            right_vocabulary,
389            rows,
390            payload_kind,
391            requirements,
392        })
393    }
394
395    /// Reads the caller-authored relation name.
396    #[must_use]
397    pub fn name(&self) -> &str {
398        self.name.as_str()
399    }
400
401    /// Reads the exact ordinary or raw identifier token naming the relation.
402    #[must_use]
403    pub const fn name_token(&self) -> &crate::token::GeneratedToken {
404        &self.name_token
405    }
406
407    /// Reads the name of the relation's left endpoint vocabulary.
408    #[must_use]
409    pub fn left_vocabulary(&self) -> &str {
410        self.left_vocabulary.as_str()
411    }
412
413    /// Reads the name of the relation's right endpoint vocabulary.
414    #[must_use]
415    pub fn right_vocabulary(&self) -> &str {
416        self.right_vocabulary.as_str()
417    }
418
419    /// Reads every informed relation row in caller-authored order.
420    pub fn rows(&self) -> impl Iterator<Item = &RecipeRelationRow> {
421        self.rows.iter()
422    }
423
424    /// Reads how many informed rows the relation carries.
425    #[must_use]
426    pub fn row_count(&self) -> usize {
427        self.rows.len()
428    }
429
430    /// Reads the payload contract shared by every row in this relation.
431    #[must_use]
432    pub const fn payload_kind(&self) -> RecipeRelationPayloadKind {
433        self.payload_kind
434    }
435
436    /// Reads the structural questions this relation declaration chose to answer.
437    #[must_use]
438    pub const fn requirements(&self) -> &RecipeRelationRequirements {
439        &self.requirements
440    }
441}