polydat-core 0.6.1

Polydat runtime: value model, graph compiler, execution engines, kernels
Documentation
// Copyright 2024-2026 Jonathan Shook
// SPDX-License-Identifier: Apache-2.0

//! Diagnostic types: [`SourceContext`] and [`ContractViolation`].

use super::name::ChildName;

/// Diagnostic context attached to a [`super::ScopeModule`] —
/// where the module's source came from. Used in error messages
/// when a contract violation surfaces at spawn or finalize.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SourceContext {
    /// Logical label — workload phase / op-template name.
    /// Free-form; appears verbatim in diagnostics.
    pub label: String,
    /// Source file path, if applicable.
    pub file: Option<String>,
    /// Line range `(start, end)` if known.
    pub line_range: Option<(usize, usize)>,
}

impl SourceContext {
    /// A context with a label and no file or lines.
    pub fn new(label: impl Into<String>) -> Self {
        Self {
            label: label.into(),
            file: None,
            line_range: None,
        }
    }

    /// The context of a phase, labelled `phase:<name>`.
    pub fn for_phase(name: &str) -> Self {
        Self::new(format!("phase:{name}"))
    }

    /// The context of an op, labelled `op:<name>`.
    pub fn for_op(name: &str) -> Self {
        Self::new(format!("op:{name}"))
    }

    /// The same context with its source file.
    pub fn with_file(mut self, file: impl Into<String>) -> Self {
        self.file = Some(file.into());
        self
    }

    /// The same context with its line range.
    pub fn with_lines(mut self, start: usize, end: usize) -> Self {
        self.line_range = Some((start, end));
        self
    }

    /// Render as a single line for error messages.
    pub fn display(&self) -> String {
        let mut s = self.label.clone();
        if let Some(f) = &self.file {
            s.push_str(&format!(" ({f}"));
            if let Some((a, b)) = self.line_range {
                s.push_str(&format!(":{a}-{b}"));
            }
            s.push(')');
        } else if let Some((a, b)) = self.line_range {
            s.push_str(&format!(" ({a}-{b})"));
        }
        s
    }
}

/// Contract violation surfaced at finalize or spawn.
///
/// Variants per the cross-binding rules plus the umbrella
/// [`Self::Compile`] for errors raised by the Polydat compiler when
/// the body fragment is converted into a program (typically an
/// unbound identifier in the body, which the compiler catches
/// after `finalize`'s name-closure check on declared imports).
///
/// The set is subcontext_construction.md §7's error contract:
/// [`Self::UnboundImport`], [`Self::FinalShadow`],
/// [`Self::DuplicateChild`], [`Self::Compile`],
/// [`Self::StrictNonePropagation`], and [`Self::Bind`] for a child the
/// binder could not build under its parent. An import's type and modifier are
/// checked by the compiler and the kernel's slot types, not here
/// (subcontext_construction.md §2.2).
#[derive(Debug, Clone)]
pub enum ContractViolation {
    /// Rule 1 — Import resolution: an artifact import has no
    /// matching parent export.
    UnboundImport {
        /// The import's name.
        import: String,
        /// Where the import is declared.
        site: SourceContext,
    },
    /// Rule 2 — Final-shadow on export: a child can't redefine
    /// a parent `const` output.
    FinalShadow {
        /// The export shadowed.
        export: String,
        /// Where the child redefines it.
        site: SourceContext,
    },
    /// Named-child registry: a duplicate spawn under the same
    /// name (subcontext_construction.md §4.1). Reports both spawn
    /// sites.
    DuplicateChild {
        /// The child's name.
        name: ChildName,
        /// Boxed: this is the only variant carrying two
        /// `SourceContext`s — boxing one keeps the whole enum (and
        /// every `Result<_, ContractViolation>`) small.
        prior_site: Box<SourceContext>,
        /// The second spawn site.
        this_site: SourceContext,
    },
    /// Polydat compile-time error — the body failed to compile (most
    /// commonly an unbound identifier: a free identifier in the body
    /// that no import, parent name, or local binding supplies).
    Compile(String),
    /// L2.f strict-mode hardening: an intermediate-layer
    /// `const` binding's initialization yielded
    /// `Value::None`, and the build was running with strict
    /// mode enabled. Per composition_substrate.md L2.f's
    /// strict-mode hardening clause, silent fall-through to
    /// the outer scope's binding is rejected in strict mode —
    /// the author must either ensure the const yields a
    /// defined value or remove the binding and declare an
    /// explicit `extern <name>` if fall-through to outer was
    /// intended. The `bindings` field carries every const
    /// output that materialised to None.
    StrictNonePropagation {
        /// Every const output that materialised to `None`.
        bindings: Vec<String>,
        /// Where the bindings are declared.
        site: SourceContext,
    },
    /// Binding the compiled child under its parent failed: a value
    /// copied from the parent was refused, a const failed when the child
    /// was initialized, or the parent's engine refused the child's
    /// program.
    Bind(std::sync::Arc<crate::KernelError>),
}

impl std::fmt::Display for ContractViolation {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::UnboundImport { import, site } => write!(
                f,
                "unbound import `{import}` (parent does not export it) at {}",
                site.display()
            ),
            Self::FinalShadow { export, site } => write!(
                f,
                "child export `{export}` shadows parent's `final` export at {}",
                site.display()
            ),
            Self::DuplicateChild {
                name,
                prior_site,
                this_site,
            } => write!(
                f,
                "duplicate spawn of child `{name}`: prior at {}, this at {}",
                prior_site.display(),
                this_site.display()
            ),
            Self::Compile(msg) => write!(f, "compile error: {msg}"),
            Self::StrictNonePropagation { bindings, site } => {
                let names = bindings.join(", ");
                write!(
                    f,
                    "L2.f strict-mode violation: intermediate-layer const \
                    binding(s) [{names}] yielded `Value::None` at scope-init \
                    at {}; strict mode rejects silent fall-through to the \
                    outer scope. Either ensure the binding yields a defined \
                    value, or remove the binding and declare \
                    `extern <name>` explicitly if fall-through to outer was \
                    intended.",
                    site.display()
                )
            }
            Self::Bind(e) => write!(f, "binding the child under its parent failed: {e}"),
        }
    }
}

impl From<crate::KernelError> for ContractViolation {
    fn from(e: crate::KernelError) -> Self {
        Self::Bind(std::sync::Arc::new(e))
    }
}

impl std::error::Error for ContractViolation {}