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    scalar_identity: Option<&'static str>,
76}
77
78impl ProgramValueMetadata {
79    /// Construct value metadata from a dtype and symbolic shape.
80    pub fn new(dtype: DType, shape: impl IntoIterator<Item = DimExpr>) -> Self {
81        Self {
82            dtype,
83            shape: shape.into_iter().map(ShapeExtent::Exact).collect(),
84            scalar_identity: None,
85        }
86    }
87
88    /// Construct metadata with explicit exact, bounded, or unknown extents.
89    pub fn from_extents(
90        dtype: DType,
91        shape: impl IntoIterator<Item = ShapeExtent<DimExpr>>,
92    ) -> Self {
93        Self {
94            dtype,
95            shape: shape.into_iter().collect(),
96            scalar_identity: None,
97        }
98    }
99
100    /// Declare the canonical identity of an externally defined scalar.
101    ///
102    /// A semantic program's identity must be reproducible across processes, and an
103    /// externally defined tag is a process-local `TypeId`, so a program that carries
104    /// one has to declare the stable name. The name belongs to the contribution that
105    /// owns the scalar, and two scalars must not share one.
106    ///
107    /// # Examples
108    ///
109    /// ```rust
110    /// use tenferro_ops::dim_expr::DimExpr;
111    /// use tenferro_runtime::program::ProgramValueMetadata;
112    /// use tenferro_tensor::DType;
113    ///
114    /// let dtype = DType::External(std::any::TypeId::of::<f64>());
115    /// let declared = ProgramValueMetadata::new(dtype, [DimExpr::Const(3)])
116    ///     .with_scalar_identity("example.scalar.v1");
117    /// assert_eq!(declared.scalar_identity(), Some("example.scalar.v1"));
118    /// # Ok::<(), Box<dyn std::error::Error>>(())
119    /// ```
120    #[must_use]
121    pub fn with_scalar_identity(mut self, identity: &'static str) -> Self {
122        self.scalar_identity = Some(identity);
123        self
124    }
125
126    /// Return the declared identity of an externally defined scalar, if any.
127    #[must_use]
128    pub const fn scalar_identity(&self) -> Option<&'static str> {
129        self.scalar_identity
130    }
131
132    /// Return the value dtype.
133    pub const fn dtype(&self) -> DType {
134        self.dtype
135    }
136
137    /// Borrow the ordered symbolic extents without allocation.
138    pub fn shape(&self) -> &[ShapeExtent<DimExpr>] {
139        &self.shape
140    }
141
142    pub(crate) fn logical_retained_bytes(&self) -> Option<usize> {
143        checked_sum([
144            self.shape
145                .len()
146                .checked_mul(std::mem::size_of::<ShapeExtent<DimExpr>>())?,
147            checked_sum_options(self.shape.iter().map(shape_extent_logical_retained_bytes))?,
148        ])
149    }
150}
151
152/// Metadata for one externally supplied semantic-program input.
153#[derive(Clone, Debug, PartialEq, Eq, Hash)]
154pub struct ProgramInputSpec {
155    metadata: ProgramValueMetadata,
156}
157
158impl ProgramInputSpec {
159    /// Construct an input specification.
160    pub fn new(dtype: DType, shape: impl IntoIterator<Item = DimExpr>) -> Self {
161        Self {
162            metadata: ProgramValueMetadata::new(dtype, shape),
163        }
164    }
165
166    /// Construct an input specification with exact, bounded, or unknown extents.
167    /// Declare the canonical identity of this input's externally defined scalar.
168    ///
169    /// # Examples
170    ///
171    /// ```rust
172    /// use tenferro_ops::dim_expr::DimExpr;
173    /// use tenferro_runtime::program::ProgramInputSpec;
174    /// use tenferro_tensor::DType;
175    ///
176    /// let dtype = DType::External(std::any::TypeId::of::<f64>());
177    /// let spec = ProgramInputSpec::new(dtype, [DimExpr::Const(3)])
178    ///     .with_scalar_identity("example.scalar.v1");
179    /// assert_eq!(spec.metadata().scalar_identity(), Some("example.scalar.v1"));
180    /// # Ok::<(), Box<dyn std::error::Error>>(())
181    /// ```
182    #[must_use]
183    pub fn with_scalar_identity(mut self, identity: &'static str) -> Self {
184        self.metadata = self.metadata.with_scalar_identity(identity);
185        self
186    }
187
188    /// Construct a specification from existing value metadata.
189    pub fn from_metadata(metadata: ProgramValueMetadata) -> Self {
190        Self { metadata }
191    }
192
193    /// Borrow this input's value metadata.
194    pub const fn metadata(&self) -> &ProgramValueMetadata {
195        &self.metadata
196    }
197}
198
199/// Relation enforced between two symbolic dimension expressions.
200#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
201#[non_exhaustive]
202pub enum ProgramShapeRelation {
203    /// Both expressions must evaluate to the same extent.
204    Equal,
205    /// The left expression must not exceed the right expression.
206    LessEqual,
207    /// The left expression must be at least the right expression.
208    GreaterEqual,
209}
210
211/// A backend-neutral symbolic-shape obligation.
212#[derive(Clone, Debug)]
213pub struct ShapeGuard {
214    relation: ProgramShapeRelation,
215    lhs: DimExpr,
216    rhs: DimExpr,
217    source_family: Option<&'static str>,
218}
219
220impl ShapeGuard {
221    /// Construct a shape guard.
222    pub fn new(relation: ProgramShapeRelation, lhs: DimExpr, rhs: DimExpr) -> Self {
223        Self {
224            relation,
225            lhs,
226            rhs,
227            source_family: None,
228        }
229    }
230
231    #[cfg(test)]
232    pub(crate) fn with_source_family(mut self, family: &'static str) -> Self {
233        self.source_family = Some(family);
234        self
235    }
236
237    /// Return the required relation.
238    pub const fn relation(&self) -> ProgramShapeRelation {
239        self.relation
240    }
241
242    /// Borrow the left expression.
243    pub const fn lhs(&self) -> &DimExpr {
244        &self.lhs
245    }
246
247    /// Borrow the right expression.
248    pub const fn rhs(&self) -> &DimExpr {
249        &self.rhs
250    }
251
252    /// Return the diagnostic family that introduced this guard, when known.
253    ///
254    /// Diagnostic provenance does not participate in semantic equality or the
255    /// semantic fingerprint.
256    pub const fn source_family(&self) -> Option<&'static str> {
257        self.source_family
258    }
259
260    pub(crate) fn logical_retained_bytes(&self) -> Option<usize> {
261        checked_sum([
262            dim_expr_logical_retained_bytes(&self.lhs)?,
263            dim_expr_logical_retained_bytes(&self.rhs)?,
264        ])
265    }
266}
267
268impl PartialEq for ShapeGuard {
269    fn eq(&self, other: &Self) -> bool {
270        self.relation == other.relation && self.lhs == other.lhs && self.rhs == other.rhs
271    }
272}
273
274impl Eq for ShapeGuard {}
275
276impl std::hash::Hash for ShapeGuard {
277    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
278        self.relation.hash(state);
279        self.lhs.hash(state);
280        self.rhs.hash(state);
281    }
282}
283
284fn shape_extent_logical_retained_bytes(extent: &ShapeExtent<DimExpr>) -> Option<usize> {
285    match extent {
286        ShapeExtent::Exact(expression) | ShapeExtent::UpperBound(expression) => {
287            dim_expr_logical_retained_bytes(expression)
288        }
289        ShapeExtent::Unknown => Some(0),
290    }
291}
292
293fn dim_expr_logical_retained_bytes(expression: &DimExpr) -> Option<usize> {
294    match expression {
295        DimExpr::Const(_) | DimExpr::InputDim { .. } => Some(0),
296        DimExpr::Add(left, right)
297        | DimExpr::Sub(left, right)
298        | DimExpr::Mul(left, right)
299        | DimExpr::FloorDiv(left, right)
300        | DimExpr::Min(left, right)
301        | DimExpr::Max(left, right) => checked_sum([
302            2usize.checked_mul(std::mem::size_of::<DimExpr>())?,
303            dim_expr_logical_retained_bytes(left)?,
304            dim_expr_logical_retained_bytes(right)?,
305        ]),
306    }
307}
308
309fn checked_sum(values: impl IntoIterator<Item = usize>) -> Option<usize> {
310    values
311        .into_iter()
312        .try_fold(0usize, |sum, value| sum.checked_add(value))
313}
314
315fn checked_sum_options(values: impl IntoIterator<Item = Option<usize>>) -> Option<usize> {
316    values
317        .into_iter()
318        .try_fold(0usize, |sum, value| sum.checked_add(value?))
319}
320
321/// Typed identity of an observable state resource.
322#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
323pub struct EffectResource {
324    family: &'static str,
325    key: u64,
326}
327
328impl EffectResource {
329    /// Construct a versioned resource identity.
330    ///
331    /// # Errors
332    ///
333    /// Returns [`EffectResourceError::InvalidFamily`] when `family` is empty
334    /// or has no `.v<version>` suffix.
335    pub fn new(family: &'static str, key: u64) -> Result<Self, EffectResourceError> {
336        let version = family.rsplit_once(".v").map(|(_, version)| version);
337        if family.is_empty()
338            || !version.is_some_and(|version| {
339                !version.is_empty() && version.bytes().all(|byte| byte.is_ascii_digit())
340            })
341        {
342            return Err(EffectResourceError::InvalidFamily);
343        }
344        Ok(Self { family, key })
345    }
346
347    /// Return the stable resource family.
348    pub const fn family(self) -> &'static str {
349        self.family
350    }
351
352    /// Return the family-local resource key.
353    pub const fn key(self) -> u64 {
354        self.key
355    }
356}
357
358/// Access mode of one semantic effect.
359#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
360pub enum EffectAccess {
361    /// Read-only access.
362    Read,
363    /// Mutating access.
364    Write,
365}
366
367/// One typed observable state access.
368#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
369pub struct Effect {
370    resource: EffectResource,
371    access: EffectAccess,
372}
373
374impl Effect {
375    /// Construct a typed effect.
376    pub const fn new(resource: EffectResource, access: EffectAccess) -> Self {
377        Self { resource, access }
378    }
379
380    /// Return the affected resource.
381    pub const fn resource(self) -> EffectResource {
382        self.resource
383    }
384
385    /// Return the access mode.
386    pub const fn access(self) -> EffectAccess {
387        self.access
388    }
389}
390
391/// Semantic alias class for one operation output.
392#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
393pub enum AliasKind {
394    /// The output has fresh value semantics.
395    Fresh,
396    /// The output is a view of an input.
397    ViewOf,
398    /// The output must alias an input.
399    MustAlias,
400    /// The output aliases an external typed resource.
401    ExternalAlias,
402}
403
404/// Alias contract for one operation output.
405#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
406pub struct Alias {
407    kind: AliasKind,
408    output: usize,
409    input: Option<usize>,
410    resource: Option<EffectResource>,
411}
412
413impl Alias {
414    /// Declare a semantically fresh output.
415    pub const fn fresh(output: usize) -> Self {
416        Self {
417            kind: AliasKind::Fresh,
418            output,
419            input: None,
420            resource: None,
421        }
422    }
423
424    /// Declare an output view of an input.
425    pub const fn view_of(output: usize, input: usize) -> Self {
426        Self {
427            kind: AliasKind::ViewOf,
428            output,
429            input: Some(input),
430            resource: None,
431        }
432    }
433
434    /// Require an output to alias an input.
435    pub const fn must_alias(output: usize, input: usize) -> Self {
436        Self {
437            kind: AliasKind::MustAlias,
438            output,
439            input: Some(input),
440            resource: None,
441        }
442    }
443
444    /// Declare an alias of an external typed resource.
445    pub const fn external(output: usize, resource: EffectResource) -> Self {
446        Self {
447            kind: AliasKind::ExternalAlias,
448            output,
449            input: None,
450            resource: Some(resource),
451        }
452    }
453
454    /// Return the alias class.
455    pub const fn kind(self) -> AliasKind {
456        self.kind
457    }
458
459    /// Return the operation-local output index.
460    pub const fn output(self) -> usize {
461        self.output
462    }
463
464    /// Return the aliased input index, when applicable.
465    pub const fn input(self) -> Option<usize> {
466        self.input
467    }
468
469    /// Return the external resource, when applicable.
470    pub const fn resource(self) -> Option<EffectResource> {
471        self.resource
472    }
473}
474
475/// Kind of unresolved semantic placement requirement.
476#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
477#[non_exhaustive]
478pub enum SemanticPlacementKind {
479    /// No semantic placement restriction.
480    Any,
481    /// Placement must match one operation input.
482    SameAsInput,
483}
484
485/// Backend-neutral placement requirement retained for runtime preparation.
486#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
487pub struct SemanticPlacementConstraint {
488    kind: SemanticPlacementKind,
489    input: Option<usize>,
490}
491
492impl SemanticPlacementConstraint {
493    /// Construct an unrestricted placement constraint.
494    pub const fn any() -> Self {
495        Self {
496            kind: SemanticPlacementKind::Any,
497            input: None,
498        }
499    }
500
501    /// Require placement compatible with one operation input.
502    pub const fn same_as_input(input: usize) -> Self {
503        Self {
504            kind: SemanticPlacementKind::SameAsInput,
505            input: Some(input),
506        }
507    }
508
509    /// Return the placement kind.
510    pub const fn kind(self) -> SemanticPlacementKind {
511        self.kind
512    }
513
514    /// Return the referenced input index, when applicable.
515    pub const fn input(self) -> Option<usize> {
516        self.input
517    }
518}