use crate::hir::ExprKind;
use crate::ir::lower::LoweredPlotField;
use crate::registry::error::GraphcalError;
use crate::syntax::ast::{CompositionProperty, MarkProperty, PlotProperty, PlotPropertyType};
use super::{DimCheckContext, InferredType, helpers::format_inferred_type, infer};
pub(super) fn check_plot_properties_dag(ctx: &DimCheckContext<'_>) -> Result<(), GraphcalError> {
let Some(dag) = ctx.dag else {
return Ok(());
};
check_plot_references(ctx, dag)?;
for entry in &dag.plots {
let Some(body) = &entry.body else { continue };
for field in &body.mark_properties {
let Some(prop) = MarkProperty::from_name(field.name.as_str()) else {
return Err(invalid_property(
ctx,
field,
"a mark block",
&valid_names(MarkProperty::ALL.iter().map(|p| p.name())),
));
};
check_property_value(ctx, prop.name(), prop.value_type(), field)?;
}
for field in &body.properties {
let Some(prop) = PlotProperty::from_name(field.name.as_str()) else {
return Err(invalid_property(
ctx,
field,
"a plot declaration",
&valid_names(PlotProperty::ALL.iter().map(|p| p.name())),
));
};
check_property_value(ctx, prop.name(), prop.value_type(), field)?;
}
}
for entry in &dag.figures {
for field in &entry.fields {
let prop = CompositionProperty::from_name(field.name.as_str())
.filter(|p| p.applies_to_figure());
let Some(prop) = prop else {
return Err(invalid_property(
ctx,
field,
"a figure declaration",
&format!(
"{}; figures render as side-by-side concatenation, so sizes belong on \
the constituent plots or layers",
valid_names(
CompositionProperty::ALL
.iter()
.filter(|p| p.applies_to_figure())
.map(|p| p.name()),
)
),
));
};
check_property_value(ctx, prop.name(), prop.value_type(), field)?;
}
}
for entry in &dag.layers {
for field in &entry.fields {
let Some(prop) = CompositionProperty::from_name(field.name.as_str()) else {
return Err(invalid_property(
ctx,
field,
"a layer declaration",
&valid_names(CompositionProperty::ALL.iter().map(|p| p.name())),
));
};
check_property_value(ctx, prop.name(), prop.value_type(), field)?;
}
}
Ok(())
}
fn check_plot_references(
ctx: &DimCheckContext<'_>,
dag: &crate::tir::typed::DagTIR,
) -> Result<(), GraphcalError> {
let owners = dag
.figures
.iter()
.map(|f| ("figure", &f.name, &f.plot_names))
.chain(dag.layers.iter().map(|l| ("layer", &l.name, &l.plot_names)));
for (owner_kind, owner, plot_names) in owners {
for (i, reference) in plot_names.iter().enumerate() {
let is_known_plot = dag.plots.iter().any(|p| p.name == reference.value)
|| dag.included_plots.iter().any(|p| p.name == reference.value);
if !is_known_plot {
let actual_kind = if dag.figures.iter().any(|f| f.name == reference.value) {
Some("figure")
} else if dag.layers.iter().any(|l| l.name == reference.value) {
Some("layer")
} else {
None
};
return Err(actual_kind.map_or_else(
|| GraphcalError::UnknownPlotReference {
owner_kind,
owner: owner.clone(),
name: reference.value.clone(),
src: ctx.src.clone(),
span: reference.span.into(),
},
|actual_kind| GraphcalError::CompositionReferencesNonPlot {
owner_kind,
actual_kind,
name: reference.value.clone(),
src: ctx.src.clone(),
span: reference.span.into(),
},
));
}
if plot_names[..i].iter().any(|p| p.value == reference.value) {
return Err(GraphcalError::DuplicatePlotReference {
owner_kind,
owner: owner.clone(),
name: reference.value.clone(),
src: ctx.src.clone(),
span: reference.span.into(),
});
}
}
}
Ok(())
}
fn valid_names<'a>(names: impl Iterator<Item = &'a str>) -> String {
format!(
"valid properties are: {}",
names.collect::<Vec<_>>().join(", ")
)
}
fn invalid_property(
ctx: &DimCheckContext<'_>,
field: &LoweredPlotField,
context: &'static str,
valid: &str,
) -> GraphcalError {
GraphcalError::InvalidPlotProperty {
property: field.name.as_str().to_string(),
context,
valid: valid.to_string(),
src: ctx.src.clone(),
span: field.name_span.into(),
}
}
fn check_property_value(
ctx: &DimCheckContext<'_>,
property: &'static str,
expected: PlotPropertyType,
field: &LoweredPlotField,
) -> Result<(), GraphcalError> {
let is_string_literal = matches!(&field.value.kind, ExprKind::StringLiteral(_));
let mismatch = |found: String| GraphcalError::PlotPropertyTypeMismatch {
property,
expected: expected.describe(),
found,
src: ctx.src.clone(),
span: field.value.span.into(),
};
match expected {
PlotPropertyType::String => {
if is_string_literal {
Ok(())
} else {
Err(mismatch("not a string literal".to_string()))
}
}
PlotPropertyType::Number | PlotPropertyType::PositiveNumber => {
if is_string_literal {
return Err(mismatch("a string literal".to_string()));
}
match infer_property_type(ctx, field)? {
InferredType::Int => Ok(()),
InferredType::Scalar(d) if d.is_dimensionless() => Ok(()),
InferredType::Scalar(d) => Err(GraphcalError::PlotPropertyDimensioned {
property,
dimension: ctx.registry.dimensions.format_dimension(&d),
src: ctx.src.clone(),
span: field.value.span.into(),
}),
other => Err(mismatch(format_inferred_type(&other, ctx.registry))),
}
}
PlotPropertyType::Bool => {
if is_string_literal {
return Err(mismatch("a string literal".to_string()));
}
match infer_property_type(ctx, field)? {
InferredType::Bool => Ok(()),
other => Err(mismatch(format_inferred_type(&other, ctx.registry))),
}
}
}
}
fn infer_property_type(
ctx: &DimCheckContext<'_>,
field: &LoweredPlotField,
) -> Result<InferredType, GraphcalError> {
let Some(dag) = ctx.dag else {
return Err(GraphcalError::InternalError {
message: "plot property inference requires DAG context".to_string(),
src: ctx.src.clone(),
span: field.value.span.into(),
});
};
infer::hir::infer_hir_type_with_owner(
&field.value,
None,
ctx.declared_types,
dag,
ctx.tir,
ctx.registry,
ctx.builtin_fns,
ctx.src,
)
}