Skip to main content

macroonz_compiler/recipe/account/
informed.rs

1//! Informed recipe construction and structural-account readback.
2
3use super::admit::{informed_codecs, informed_members, informed_relations, informed_vocabularies};
4use super::collisions::ensure_standard_names;
5use super::contracts::{
6    ensure_evidence_targets, ensure_projection_contracts, ensure_transition_account,
7};
8use super::restore::{restore_projection_references, restore_relation_references};
9use super::settle::missing_vocabulary;
10use super::{
11    EffectiveProjection, EvidenceTarget, PROJECTION_LIMIT, ProjectionDisposition,
12    ProjectionStanding, Recipe, RecipeCodec, RecipeError, RecipeEvidence, RecipeIssue,
13    RecipeMember, RecipeParts, RecipeRelation, RecipeRole, VOCABULARY_LIMIT,
14};
15use crate::bounded::KeyedRoster;
16use crate::recipe::evidence_position;
17use crate::support::SupportName;
18use crate::token::{GeneratedTree, SpanHandle};
19
20impl RecipeMember {
21    pub(in crate::recipe) fn authored(
22        spelling: String,
23        name: crate::token::GeneratedToken,
24        at: SpanHandle,
25    ) -> Self {
26        Self { spelling, name, at }
27    }
28
29    /// Reads the authored member spelling.
30    #[must_use]
31    pub fn spelling(&self) -> &str {
32        self.spelling.as_str()
33    }
34
35    /// Reads the exact ordinary or raw identifier token that names this member.
36    #[must_use]
37    pub const fn name_token(&self) -> &crate::token::GeneratedToken {
38        &self.name
39    }
40
41    /// Reads the captured producer span for this member.
42    #[must_use]
43    pub(in crate::recipe) const fn at(&self) -> SpanHandle {
44        self.at
45    }
46}
47
48impl EvidenceTarget {
49    pub(in crate::recipe) const fn named(vocabulary: String) -> Self {
50        Self { vocabulary }
51    }
52
53    /// Reads the caller-authored vocabulary name selected for evidence.
54    #[must_use]
55    pub fn name(&self) -> &str {
56        self.vocabulary.as_str()
57    }
58}
59
60impl RecipeCodec {
61    pub(in crate::recipe) fn informed(
62        name: String,
63        content: crate::codec::CodecContent,
64        at: SpanHandle,
65        refusal_at: SpanHandle,
66        direction_at: SpanHandle,
67    ) -> Self {
68        Self {
69            name,
70            content,
71            at,
72            refusal_at,
73            direction_at,
74        }
75    }
76
77    /// Reads the caller-owned codec declaration name.
78    #[must_use]
79    pub fn name(&self) -> &str {
80        self.name.as_str()
81    }
82
83    /// Reads the existing codec owner's informed content.
84    #[must_use]
85    pub const fn content(&self) -> &crate::codec::CodecContent {
86        &self.content
87    }
88
89    pub(in crate::recipe) const fn at(&self) -> SpanHandle {
90        self.at
91    }
92
93    pub(in crate::recipe) const fn refusal_at(&self) -> SpanHandle {
94        self.refusal_at
95    }
96
97    pub(in crate::recipe) const fn direction_at(&self) -> SpanHandle {
98        self.direction_at
99    }
100}
101
102impl super::RecipeVocabulary {
103    pub(super) fn informed(
104        name: String,
105        name_token: crate::token::GeneratedToken,
106        members: Vec<RecipeMember>,
107        at: SpanHandle,
108    ) -> Result<Self, RecipeError> {
109        let members = informed_members(name.as_str(), members, at)?;
110        Ok(Self {
111            name,
112            name_token,
113            members,
114            at,
115        })
116    }
117
118    /// Reads the caller-authored vocabulary name.
119    #[must_use]
120    pub fn name(&self) -> &str {
121        self.name.as_str()
122    }
123
124    /// Reads the exact ordinary or raw identifier token naming the vocabulary.
125    #[must_use]
126    pub const fn name_token(&self) -> &crate::token::GeneratedToken {
127        &self.name_token
128    }
129
130    /// Reads the informed member roster in caller-authored order.
131    #[must_use]
132    pub const fn members(&self) -> &KeyedRoster<RecipeMember, String, VOCABULARY_LIMIT> {
133        &self.members
134    }
135}
136
137impl RecipeEvidence {
138    pub(in crate::recipe) const fn captured(
139        role: RecipeRole,
140        target: Option<EvidenceTarget>,
141        body: crate::token::CapturedInput,
142        at: SpanHandle,
143    ) -> Self {
144        Self {
145            role,
146            target,
147            body,
148            at,
149        }
150    }
151
152    /// Reads the descriptor-native role this block declares.
153    #[must_use]
154    pub const fn role(&self) -> RecipeRole {
155        self.role
156    }
157
158    /// Reads the explicitly selected vocabulary where this evidence form requires one.
159    #[must_use]
160    pub const fn target(&self) -> Option<&EvidenceTarget> {
161        self.target.as_ref()
162    }
163
164    /// Reads the exact captured descriptor declaration body.
165    #[must_use]
166    pub const fn body(&self) -> &crate::token::CapturedInput {
167        &self.body
168    }
169
170    pub(crate) const fn at(&self) -> SpanHandle {
171        self.at
172    }
173}
174
175impl Recipe {
176    pub(in crate::recipe) fn informed(offered: RecipeParts) -> Result<Self, RecipeError> {
177        let RecipeParts {
178            module_name,
179            module_name_token,
180            module_head,
181            authored_body,
182            authored_declaration,
183            module_body_at,
184            vocabularies,
185            relations,
186            transition_relation,
187            codecs,
188            mut projections,
189            evidence,
190            support,
191        } = offered;
192        let vocabularies = informed_vocabularies(vocabularies)?;
193        let relations = informed_relations(
194            relations,
195            vocabularies.as_ref(),
196            transition_relation.as_deref(),
197        )?;
198        let codecs = informed_codecs(codecs)?;
199        ensure_evidence_targets(&evidence, vocabularies.as_ref())?;
200        ensure_transition_account(
201            transition_relation.as_deref(),
202            relations.as_ref(),
203            vocabularies.as_ref(),
204        )?;
205        resolve_typestate_subject(&mut projections, vocabularies.as_ref())?;
206        ensure_projection_contracts(
207            &projections,
208            codecs.as_ref(),
209            relations.as_ref(),
210            transition_relation.as_deref(),
211        )?;
212        ensure_standard_names(
213            &projections,
214            vocabularies.as_ref(),
215            relations.as_ref(),
216            transition_relation.as_deref(),
217        )?;
218        Ok(Self {
219            module_name,
220            module_name_token,
221            module_head,
222            authored_body,
223            authored_declaration,
224            module_body_at,
225            vocabularies,
226            relations,
227            transition_relation,
228            codecs,
229            projections,
230            evidence,
231            support,
232        })
233    }
234
235    /// Reads the authored module name.
236    #[must_use]
237    pub fn module_name(&self) -> &str {
238        self.module_name.as_str()
239    }
240
241    /// Reads the exact ordinary or raw identifier token that names the recipe module.
242    #[must_use]
243    pub const fn module_name_token(&self) -> &crate::token::GeneratedToken {
244        &self.module_name_token
245    }
246
247    pub(in crate::recipe) const fn module_head(&self) -> &GeneratedTree {
248        &self.module_head
249    }
250
251    pub(in crate::recipe) const fn authored_body(&self) -> &GeneratedTree {
252        &self.authored_body
253    }
254
255    /// Restore authored reference spans onto the generated material that repeats them.
256    pub(in crate::recipe) fn restore_authored_references(
257        &self,
258        tree: &GeneratedTree,
259    ) -> GeneratedTree {
260        let mut restored = tree.clone();
261        for relation in self.relations() {
262            restored = restore_relation_references(&restored, relation);
263        }
264        for role in RecipeRole::ALL.iter().copied() {
265            let Some(effective) = self.effective(role) else {
266                continue;
267            };
268            restored = restore_projection_references(&restored, effective);
269        }
270        restored.restored_references_from(&self.authored_declaration)
271    }
272
273    pub(in crate::recipe) const fn module_body_at(&self) -> Option<SpanHandle> {
274        self.module_body_at
275    }
276
277    /// Reads every informed vocabulary in caller-authored order.
278    pub fn vocabularies(&self) -> impl Iterator<Item = &super::RecipeVocabulary> {
279        self.vocabularies.iter().flat_map(KeyedRoster::members)
280    }
281
282    /// Reads one informed vocabulary by its caller-authored name.
283    #[must_use]
284    pub fn vocabulary(&self, name: &str) -> Option<&super::RecipeVocabulary> {
285        self.vocabularies
286            .as_ref()
287            .and_then(|vocabularies| vocabularies.get(name))
288    }
289
290    /// Reads every informed relation in caller-authored order.
291    pub fn relations(&self) -> impl Iterator<Item = &RecipeRelation> {
292        self.relations.iter().flat_map(KeyedRoster::members)
293    }
294
295    /// Reads one informed relation by its caller-authored name.
296    #[must_use]
297    pub fn relation(&self, name: &str) -> Option<&RecipeRelation> {
298        self.relations
299            .as_ref()
300            .and_then(|relations| relations.get(name))
301    }
302
303    /// Reads the relation selected by the ergonomic transition lowering.
304    #[must_use]
305    pub fn transition_relation(&self) -> Option<&RecipeRelation> {
306        self.transition_relation
307            .as_deref()
308            .and_then(|name| self.relation(name))
309    }
310
311    /// Reads every caller-named codec declaration in authored order.
312    pub fn codecs(&self) -> impl Iterator<Item = &RecipeCodec> {
313        self.codecs.iter().flat_map(KeyedRoster::members)
314    }
315
316    /// Reads one codec declaration by its caller-owned name.
317    #[must_use]
318    pub fn codec(&self, name: &str) -> Option<&RecipeCodec> {
319        self.codecs.as_ref().and_then(|codecs| codecs.get(name))
320    }
321
322    pub(in crate::recipe) fn transition_account(
323        &self,
324    ) -> Option<(
325        &super::RecipeVocabulary,
326        &super::RecipeVocabulary,
327        &RecipeRelation,
328    )> {
329        let relation = self.transition_relation()?;
330        let left = self.vocabulary(relation.left_vocabulary())?;
331        let right = self.vocabulary(relation.right_vocabulary())?;
332        Some((left, right, relation))
333    }
334
335    /// Reads the complete standing for one projection role.
336    #[must_use]
337    pub(in crate::recipe) fn standing(&self, role: RecipeRole) -> &ProjectionStanding {
338        role.standing(&self.projections)
339    }
340
341    /// Reads the complete public disposition of one possible projection.
342    #[must_use]
343    pub fn projection_disposition(&self, role: RecipeRole) -> ProjectionDisposition {
344        match self.standing(role) {
345            ProjectionStanding::Generated(_) => ProjectionDisposition::Generated,
346            ProjectionStanding::NotRequested => ProjectionDisposition::NotRequested,
347            ProjectionStanding::FeatureUnavailable => ProjectionDisposition::FeatureUnavailable,
348            ProjectionStanding::TargetUnavailable => ProjectionDisposition::TargetUnavailable,
349        }
350    }
351
352    /// Reads the effective mechanical configuration for one selected role.
353    #[must_use]
354    pub fn effective(&self, role: RecipeRole) -> Option<&EffectiveProjection> {
355        match self.standing(role) {
356            ProjectionStanding::Generated(effective) => Some(effective),
357            ProjectionStanding::NotRequested
358            | ProjectionStanding::FeatureUnavailable
359            | ProjectionStanding::TargetUnavailable => None,
360        }
361    }
362
363    /// Reads every generated role in declared role order.
364    pub(in crate::recipe) fn selected_roles(&self) -> impl Iterator<Item = RecipeRole> + '_ {
365        RecipeRole::ALL
366            .iter()
367            .copied()
368            .filter(|role| matches!(self.standing(*role), ProjectionStanding::Generated(_)))
369    }
370
371    /// Reads the evidence carrier's explicit public address where one was declared.
372    #[must_use]
373    pub(crate) const fn support(&self) -> Option<&SupportName> {
374        self.support.as_ref()
375    }
376
377    /// Reads the exact descriptor-native evidence block for one generated evidence role.
378    #[must_use]
379    pub fn evidence(&self, role: RecipeRole) -> Option<&RecipeEvidence> {
380        let position = evidence_position(role)?;
381        self.evidence.get(position).and_then(Option::as_ref)
382    }
383
384    pub(crate) fn baked_type_names(&self) -> Vec<String> {
385        let mut names = self
386            .effective(RecipeRole::RelationTables)
387            .into_iter()
388            .flat_map(EffectiveProjection::relation_tables)
389            .map(|table| table.relation().to_owned())
390            .collect::<Vec<_>>();
391        if self.effective(RecipeRole::Codec).is_some() {
392            names.extend(
393                self.codecs()
394                    .filter(|codec| codec.content().direction.reads())
395                    .map(|codec| codec.content().shape.refusal().to_owned()),
396            );
397        }
398        if self.effective(RecipeRole::Dispatch).is_some() {
399            names.push("TransitionRefusal".to_owned());
400        }
401        if self.effective(RecipeRole::Typestate).is_some() {
402            names.push("typestate".to_owned());
403        }
404        names
405    }
406}
407
408fn resolve_typestate_subject(
409    projections: &mut [ProjectionStanding; PROJECTION_LIMIT],
410    vocabularies: Option<&KeyedRoster<super::RecipeVocabulary, String, VOCABULARY_LIMIT>>,
411) -> Result<(), RecipeError> {
412    let ProjectionStanding::Generated(effective) = RecipeRole::Typestate.standing_mut(projections)
413    else {
414        return Ok(());
415    };
416    let subject = effective.subject().map(str::to_owned).or_else(|| {
417        let mut candidates = vocabularies.into_iter().flat_map(KeyedRoster::members);
418        let only = candidates.next()?;
419        candidates.next().is_none().then(|| only.name().to_owned())
420    });
421    let Some(subject) = subject else {
422        return Err(RecipeError::at(
423            RecipeIssue::ProjectionSubjectRequired {
424                role: RecipeRole::Typestate,
425                expected: "one named vocabulary when several are declared",
426            },
427            None,
428        ));
429    };
430    if vocabularies
431        .and_then(|vocabularies| vocabularies.get(subject.as_str()))
432        .is_none()
433    {
434        return Err(missing_vocabulary(subject.as_str(), None));
435    }
436    effective.select_subject(subject);
437    Ok(())
438}