use crate::op::{BindingOp, ComponentOp, ElementOp, Namespace, Op};
use crate::provenance::{ProvenanceRecord, ProvenanceSink};
use vize_l0::pass::RemarkSink;
use vize_l0::side_table::SideTable;
use vize_l0::{cstr, ensure_sufficient_stack};
use super::consts::{props_all_hoistable, slot_props_static};
use super::{StaticFacts, StaticLevel};
use crate::walk::PageWalk;
pub(super) struct Contribution {
level: ChildLevel,
nested: bool,
native: bool,
}
pub(super) enum ChildLevel {
Static,
DynamicText,
Dynamic,
}
pub(super) struct RegionSummary {
any_dynamic: bool,
any_dynamic_text: bool,
all_nested: bool,
all_native: bool,
child_count: usize,
pub(super) first_dynamic: Option<&'static str>,
pub(super) first_dynamic_text: Option<&'static str>,
}
impl RegionSummary {
fn empty() -> Self {
Self {
any_dynamic: false,
any_dynamic_text: false,
all_nested: true,
all_native: true,
child_count: 0,
first_dynamic: None,
first_dynamic_text: None,
}
}
fn note_blocker(&mut self, contribution: &Contribution, mnemonic: &'static str) {
match contribution.level {
ChildLevel::Static => {}
ChildLevel::DynamicText => {
self.first_dynamic_text.get_or_insert(mnemonic);
}
ChildLevel::Dynamic => {
self.first_dynamic.get_or_insert(mnemonic);
}
}
}
fn absorb(&mut self, contribution: &Contribution) {
match contribution.level {
ChildLevel::Static => {}
ChildLevel::DynamicText => self.any_dynamic_text = true,
ChildLevel::Dynamic => self.any_dynamic = true,
}
self.all_nested &= contribution.nested;
self.all_native &= contribution.native;
self.child_count += 1;
}
}
pub(super) fn visit_region<P: ProvenanceSink, R: RemarkSink>(
walk: &mut PageWalk,
ops: &[Op<'_>],
ns: Namespace,
provenance: &mut P,
facts: &mut SideTable<StaticFacts>,
remarks: &mut R,
) -> RegionSummary {
ensure_sufficient_stack(|| visit_region_guarded(walk, ops, ns, provenance, facts, remarks))
}
fn visit_region_guarded<P: ProvenanceSink, R: RemarkSink>(
walk: &mut PageWalk,
ops: &[Op<'_>],
ns: Namespace,
provenance: &mut P,
facts: &mut SideTable<StaticFacts>,
remarks: &mut R,
) -> RegionSummary {
let mut summary = RegionSummary::empty();
for op in ops {
let id = walk.mint();
let contribution = match op {
Op::Element(element) => {
for _ in element.bindings.iter() {
let _ = walk.mint();
}
let children = visit_region(
walk,
&element.children.ops,
children_ns(element.namespace, element.tag),
provenance,
facts,
remarks,
);
let fact = element_facts(element, &children);
publish(provenance, facts, id, "ui.element", element.span, fact);
if R::ENABLED {
super::remarks::element(remarks, element, fact, &children);
}
element_contribution(element, fact)
}
Op::Component(component) => {
for _ in component.bindings.iter() {
let _ = walk.mint();
}
let children = visit_region(
walk,
&component.children.ops,
ns,
provenance,
facts,
remarks,
);
let fact = component_facts(component, ns, &children);
publish(provenance, facts, id, "ui.component", component.span, fact);
if R::ENABLED {
super::remarks::component(remarks, component, fact);
}
Contribution {
level: ChildLevel::Dynamic,
nested: false,
native: false,
}
}
Op::Text(_) => Contribution {
level: ChildLevel::Static,
nested: true,
native: true,
},
Op::Interpolation(_) => Contribution {
level: ChildLevel::DynamicText,
nested: true,
native: true,
},
Op::JsxChildValue(_) => Contribution {
level: ChildLevel::Dynamic,
nested: false,
native: false,
},
Op::Comment(_) => Contribution {
level: ChildLevel::Dynamic,
nested: false,
native: true,
},
Op::If(if_op) => {
for branch in if_op.branches.iter() {
visit_region(walk, &branch.region.ops, ns, provenance, facts, remarks);
}
Contribution {
level: ChildLevel::Dynamic,
nested: false,
native: false,
}
}
Op::For(for_op) => {
visit_region(walk, &for_op.region.ops, ns, provenance, facts, remarks);
Contribution {
level: ChildLevel::Dynamic,
nested: false,
native: false,
}
}
Op::OriginalFor(for_op) => {
visit_region(walk, &for_op.region.ops, ns, provenance, facts, remarks);
Contribution {
level: ChildLevel::Dynamic,
nested: false,
native: false,
}
}
Op::Slot(slot) => {
for _ in slot.bindings.iter() {
let _ = walk.mint();
}
visit_region(walk, &slot.fallback.ops, ns, provenance, facts, remarks);
Contribution {
level: ChildLevel::Dynamic,
nested: slot_props_static(slot),
native: false,
}
}
};
if R::ENABLED {
summary.note_blocker(&contribution, op.mnemonic());
}
summary.absorb(&contribution);
}
summary
}
fn element_facts(element: &ElementOp<'_>, children: &RegionSummary) -> StaticFacts {
let props_static = props_all_hoistable(&element.attributes, &element.bindings);
let svg_blocked = element.tag == "svg" && !element.bindings.is_empty();
let level = if svg_blocked || !props_static || children.any_dynamic {
StaticLevel::NotStatic
} else if children.any_dynamic_text {
StaticLevel::HasDynamicText
} else {
StaticLevel::FullyStatic
};
StaticFacts {
level,
props_hoistable: props_static
&& !(element.attributes.is_empty() && element.bindings.is_empty()),
nested_static: children.child_count > 0 && children.all_nested,
native_descendants: children.all_native,
foreign: element.namespace != Namespace::Html,
}
}
fn component_facts(
component: &ComponentOp<'_>,
ns: Namespace,
children: &RegionSummary,
) -> StaticFacts {
let props_static = props_all_hoistable(&component.attributes, &component.bindings);
StaticFacts {
level: StaticLevel::NotStatic,
props_hoistable: props_static
&& !(component.attributes.is_empty() && component.bindings.is_empty()),
nested_static: children.child_count > 0 && children.all_nested,
native_descendants: children.all_native,
foreign: ns != Namespace::Html,
}
}
fn children_ns(own: Namespace, tag: &str) -> Namespace {
match own {
Namespace::Svg if matches!(tag, "foreignObject" | "desc" | "title") => Namespace::Html,
Namespace::MathMl if matches!(tag, "mi" | "mo" | "mn" | "ms" | "mtext") => Namespace::Html,
other => other,
}
}
fn element_contribution(element: &ElementOp<'_>, fact: StaticFacts) -> Contribution {
let slot_carrier = element
.bindings
.iter()
.any(|binding| matches!(binding, BindingOp::SlotContent(_)));
Contribution {
level: match fact.level {
StaticLevel::FullyStatic => ChildLevel::Static,
StaticLevel::HasDynamicText | StaticLevel::NotStatic => ChildLevel::Dynamic,
},
nested: !slot_carrier
&& props_all_hoistable(&element.attributes, &element.bindings)
&& (element.children.ops.is_empty() || fact.nested_static),
native: !slot_carrier && fact.native_descendants,
}
}
fn publish(
provenance: &mut impl ProvenanceSink,
facts: &mut SideTable<StaticFacts>,
id: Option<vize_l0::id::NodeId>,
owner: &str,
span: vize_l0::Span,
fact: StaticFacts,
) {
let Some(id) = id else {
return;
};
provenance.record(|| {
let level = match fact.level {
StaticLevel::NotStatic => "not-static",
StaticLevel::HasDynamicText => "dynamic-text",
StaticLevel::FullyStatic => "fully-static",
};
ProvenanceRecord {
rule: vize_l0::String::from("pass.hoist-static.fact"),
node: Some(id),
before: vize_l0::String::from(owner),
after: cstr!(
"level={level} props={} nested={} native={}",
fact.props_hoistable,
fact.nested_static,
fact.native_descendants
),
span,
}
});
facts.insert(id, fact);
}