Skip to main content

tenferro_runtime/program/
metadata.rs

1use tenferro_ops::dim_expr::DimExpr;
2use tenferro_ops::shape_extent::ShapeExtent;
3use tenferro_tensor::DType;
4
5use super::EffectResourceError;
6
7/// Diagnostic origin class of one semantic operation.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9#[non_exhaustive]
10pub enum SemanticProvenanceKind {
11    /// Operation was added directly through a semantic-program builder.
12    Builder,
13    /// Operation was preserved by importing another semantic program.
14    Imported,
15    /// Operation was produced by an internal semantic derivation.
16    Derived,
17}
18
19#[derive(Clone)]
20pub(crate) struct SemanticProvenance {
21    kind: SemanticProvenanceKind,
22    label: Option<std::sync::Arc<str>>,
23}
24
25impl SemanticProvenance {
26    pub(crate) fn builder(label: Option<&str>) -> Self {
27        Self {
28            kind: SemanticProvenanceKind::Builder,
29            label: label.map(std::sync::Arc::from),
30        }
31    }
32
33    pub(crate) fn view(&self) -> SemanticProvenanceView<'_> {
34        SemanticProvenanceView {
35            kind: self.kind,
36            label: self.label.as_deref(),
37        }
38    }
39}
40
41/// Bounded, allocation-free diagnostic provenance view.
42#[derive(Clone, Copy)]
43pub struct SemanticProvenanceView<'a> {
44    kind: SemanticProvenanceKind,
45    label: Option<&'a str>,
46}
47
48impl<'a> SemanticProvenanceView<'a> {
49    /// Return the origin class without exposing source value or operation IDs.
50    pub const fn kind(self) -> SemanticProvenanceKind {
51        self.kind
52    }
53
54    /// Return an optional operation-family or transform label.
55    pub const fn label(self) -> Option<&'a str> {
56        self.label
57    }
58}
59
60impl std::fmt::Debug for SemanticProvenanceView<'_> {
61    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        formatter
63            .debug_struct("SemanticProvenanceView")
64            .field("kind", &self.kind)
65            .field("has_label", &self.label.is_some())
66            .finish()
67    }
68}
69
70/// Dtype and ordered symbolic extents of one semantic SSA value.
71#[derive(Clone, Debug, PartialEq, Eq, Hash)]
72pub struct ProgramValueMetadata {
73    dtype: DType,
74    shape: Box<[ShapeExtent<DimExpr>]>,
75}
76
77impl ProgramValueMetadata {
78    /// Construct value metadata from a dtype and symbolic shape.
79    pub fn new(dtype: DType, shape: impl IntoIterator<Item = DimExpr>) -> Self {
80        Self {
81            dtype,
82            shape: shape.into_iter().map(ShapeExtent::Exact).collect(),
83        }
84    }
85
86    /// Construct metadata with explicit exact, bounded, or unknown extents.
87    pub fn from_extents(
88        dtype: DType,
89        shape: impl IntoIterator<Item = ShapeExtent<DimExpr>>,
90    ) -> Self {
91        Self {
92            dtype,
93            shape: shape.into_iter().collect(),
94        }
95    }
96
97    /// Return the value dtype.
98    pub const fn dtype(&self) -> DType {
99        self.dtype
100    }
101
102    /// Borrow the ordered symbolic extents without allocation.
103    pub fn shape(&self) -> &[ShapeExtent<DimExpr>] {
104        &self.shape
105    }
106
107    pub(crate) fn logical_retained_bytes(&self) -> Option<usize> {
108        checked_sum([
109            self.shape
110                .len()
111                .checked_mul(std::mem::size_of::<ShapeExtent<DimExpr>>())?,
112            checked_sum_options(self.shape.iter().map(shape_extent_logical_retained_bytes))?,
113        ])
114    }
115}
116
117/// Metadata for one externally supplied semantic-program input.
118#[derive(Clone, Debug, PartialEq, Eq, Hash)]
119pub struct ProgramInputSpec {
120    metadata: ProgramValueMetadata,
121}
122
123impl ProgramInputSpec {
124    /// Construct an input specification.
125    pub fn new(dtype: DType, shape: impl IntoIterator<Item = DimExpr>) -> Self {
126        Self {
127            metadata: ProgramValueMetadata::new(dtype, shape),
128        }
129    }
130
131    /// Construct an input specification with exact, bounded, or unknown extents.
132    pub fn from_metadata(metadata: ProgramValueMetadata) -> Self {
133        Self { metadata }
134    }
135
136    /// Borrow this input's value metadata.
137    pub const fn metadata(&self) -> &ProgramValueMetadata {
138        &self.metadata
139    }
140}
141
142/// Relation enforced between two symbolic dimension expressions.
143#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
144#[non_exhaustive]
145pub enum ProgramShapeRelation {
146    /// Both expressions must evaluate to the same extent.
147    Equal,
148    /// The left expression must not exceed the right expression.
149    LessEqual,
150    /// The left expression must be at least the right expression.
151    GreaterEqual,
152}
153
154/// A backend-neutral symbolic-shape obligation.
155#[derive(Clone, Debug)]
156pub struct ShapeGuard {
157    relation: ProgramShapeRelation,
158    lhs: DimExpr,
159    rhs: DimExpr,
160    source_family: Option<&'static str>,
161}
162
163impl ShapeGuard {
164    /// Construct a shape guard.
165    pub fn new(relation: ProgramShapeRelation, lhs: DimExpr, rhs: DimExpr) -> Self {
166        Self {
167            relation,
168            lhs,
169            rhs,
170            source_family: None,
171        }
172    }
173
174    pub(crate) fn with_source_family(mut self, family: &'static str) -> Self {
175        self.source_family = Some(family);
176        self
177    }
178
179    /// Return the required relation.
180    pub const fn relation(&self) -> ProgramShapeRelation {
181        self.relation
182    }
183
184    /// Borrow the left expression.
185    pub const fn lhs(&self) -> &DimExpr {
186        &self.lhs
187    }
188
189    /// Borrow the right expression.
190    pub const fn rhs(&self) -> &DimExpr {
191        &self.rhs
192    }
193
194    /// Return the diagnostic family that introduced this guard, when known.
195    ///
196    /// Diagnostic provenance does not participate in semantic equality or the
197    /// semantic fingerprint.
198    pub const fn source_family(&self) -> Option<&'static str> {
199        self.source_family
200    }
201
202    pub(crate) fn logical_retained_bytes(&self) -> Option<usize> {
203        checked_sum([
204            dim_expr_logical_retained_bytes(&self.lhs)?,
205            dim_expr_logical_retained_bytes(&self.rhs)?,
206        ])
207    }
208}
209
210impl PartialEq for ShapeGuard {
211    fn eq(&self, other: &Self) -> bool {
212        self.relation == other.relation && self.lhs == other.lhs && self.rhs == other.rhs
213    }
214}
215
216impl Eq for ShapeGuard {}
217
218impl std::hash::Hash for ShapeGuard {
219    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
220        self.relation.hash(state);
221        self.lhs.hash(state);
222        self.rhs.hash(state);
223    }
224}
225
226fn shape_extent_logical_retained_bytes(extent: &ShapeExtent<DimExpr>) -> Option<usize> {
227    match extent {
228        ShapeExtent::Exact(expression) | ShapeExtent::UpperBound(expression) => {
229            dim_expr_logical_retained_bytes(expression)
230        }
231        ShapeExtent::Unknown => Some(0),
232    }
233}
234
235fn dim_expr_logical_retained_bytes(expression: &DimExpr) -> Option<usize> {
236    match expression {
237        DimExpr::Const(_) | DimExpr::InputDim { .. } => Some(0),
238        DimExpr::Add(left, right)
239        | DimExpr::Sub(left, right)
240        | DimExpr::Mul(left, right)
241        | DimExpr::FloorDiv(left, right)
242        | DimExpr::Min(left, right)
243        | DimExpr::Max(left, right) => checked_sum([
244            2usize.checked_mul(std::mem::size_of::<DimExpr>())?,
245            dim_expr_logical_retained_bytes(left)?,
246            dim_expr_logical_retained_bytes(right)?,
247        ]),
248    }
249}
250
251fn checked_sum(values: impl IntoIterator<Item = usize>) -> Option<usize> {
252    values
253        .into_iter()
254        .try_fold(0usize, |sum, value| sum.checked_add(value))
255}
256
257fn checked_sum_options(values: impl IntoIterator<Item = Option<usize>>) -> Option<usize> {
258    values
259        .into_iter()
260        .try_fold(0usize, |sum, value| sum.checked_add(value?))
261}
262
263/// Typed identity of an observable state resource.
264#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
265pub struct EffectResource {
266    family: &'static str,
267    key: u64,
268}
269
270impl EffectResource {
271    /// Construct a versioned resource identity.
272    ///
273    /// # Errors
274    ///
275    /// Returns [`EffectResourceError::InvalidFamily`] when `family` is empty
276    /// or has no `.v<version>` suffix.
277    pub fn new(family: &'static str, key: u64) -> Result<Self, EffectResourceError> {
278        let version = family.rsplit_once(".v").map(|(_, version)| version);
279        if family.is_empty()
280            || !version.is_some_and(|version| {
281                !version.is_empty() && version.bytes().all(|byte| byte.is_ascii_digit())
282            })
283        {
284            return Err(EffectResourceError::InvalidFamily);
285        }
286        Ok(Self { family, key })
287    }
288
289    /// Return the stable resource family.
290    pub const fn family(self) -> &'static str {
291        self.family
292    }
293
294    /// Return the family-local resource key.
295    pub const fn key(self) -> u64 {
296        self.key
297    }
298}
299
300/// Access mode of one semantic effect.
301#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
302pub enum EffectAccess {
303    /// Read-only access.
304    Read,
305    /// Mutating access.
306    Write,
307}
308
309/// One typed observable state access.
310#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
311pub struct Effect {
312    resource: EffectResource,
313    access: EffectAccess,
314}
315
316impl Effect {
317    /// Construct a typed effect.
318    pub const fn new(resource: EffectResource, access: EffectAccess) -> Self {
319        Self { resource, access }
320    }
321
322    /// Return the affected resource.
323    pub const fn resource(self) -> EffectResource {
324        self.resource
325    }
326
327    /// Return the access mode.
328    pub const fn access(self) -> EffectAccess {
329        self.access
330    }
331}
332
333/// Semantic alias class for one operation output.
334#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
335pub enum AliasKind {
336    /// The output has fresh value semantics.
337    Fresh,
338    /// The output is a view of an input.
339    ViewOf,
340    /// The output must alias an input.
341    MustAlias,
342    /// The output aliases an external typed resource.
343    ExternalAlias,
344}
345
346/// Alias contract for one operation output.
347#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
348pub struct Alias {
349    kind: AliasKind,
350    output: usize,
351    input: Option<usize>,
352    resource: Option<EffectResource>,
353}
354
355impl Alias {
356    /// Declare a semantically fresh output.
357    pub const fn fresh(output: usize) -> Self {
358        Self {
359            kind: AliasKind::Fresh,
360            output,
361            input: None,
362            resource: None,
363        }
364    }
365
366    /// Declare an output view of an input.
367    pub const fn view_of(output: usize, input: usize) -> Self {
368        Self {
369            kind: AliasKind::ViewOf,
370            output,
371            input: Some(input),
372            resource: None,
373        }
374    }
375
376    /// Require an output to alias an input.
377    pub const fn must_alias(output: usize, input: usize) -> Self {
378        Self {
379            kind: AliasKind::MustAlias,
380            output,
381            input: Some(input),
382            resource: None,
383        }
384    }
385
386    /// Declare an alias of an external typed resource.
387    pub const fn external(output: usize, resource: EffectResource) -> Self {
388        Self {
389            kind: AliasKind::ExternalAlias,
390            output,
391            input: None,
392            resource: Some(resource),
393        }
394    }
395
396    /// Return the alias class.
397    pub const fn kind(self) -> AliasKind {
398        self.kind
399    }
400
401    /// Return the operation-local output index.
402    pub const fn output(self) -> usize {
403        self.output
404    }
405
406    /// Return the aliased input index, when applicable.
407    pub const fn input(self) -> Option<usize> {
408        self.input
409    }
410
411    /// Return the external resource, when applicable.
412    pub const fn resource(self) -> Option<EffectResource> {
413        self.resource
414    }
415}
416
417/// Kind of unresolved semantic placement requirement.
418#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
419#[non_exhaustive]
420pub enum SemanticPlacementKind {
421    /// No semantic placement restriction.
422    Any,
423    /// Placement must match one operation input.
424    SameAsInput,
425}
426
427/// Backend-neutral placement requirement retained for runtime preparation.
428#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
429pub struct SemanticPlacementConstraint {
430    kind: SemanticPlacementKind,
431    input: Option<usize>,
432}
433
434impl SemanticPlacementConstraint {
435    /// Construct an unrestricted placement constraint.
436    pub const fn any() -> Self {
437        Self {
438            kind: SemanticPlacementKind::Any,
439            input: None,
440        }
441    }
442
443    /// Require placement compatible with one operation input.
444    pub const fn same_as_input(input: usize) -> Self {
445        Self {
446            kind: SemanticPlacementKind::SameAsInput,
447            input: Some(input),
448        }
449    }
450
451    /// Return the placement kind.
452    pub const fn kind(self) -> SemanticPlacementKind {
453        self.kind
454    }
455
456    /// Return the referenced input index, when applicable.
457    pub const fn input(self) -> Option<usize> {
458        self.input
459    }
460}