use std::collections::{HashMap, HashSet};
use react_compiler_diagnostics::{
CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, SourceLocation,
};
use react_compiler_hir::{
DeclarationId, DependencyPathEntry, IdentifierId, InstructionKind, InstructionValue,
ManualMemoDependency, ManualMemoDependencyRoot, Place, ReactiveBlock, ReactiveFunction,
ReactiveInstruction, ReactiveScopeBlock, ReactiveStatement, ReactiveValue, ScopeId,
IdentifierName, Identifier,
};
use react_compiler_hir::environment::Environment;
struct ManualMemoBlockState {
reassignments: HashMap<DeclarationId, HashSet<IdentifierId>>,
loc: Option<SourceLocation>,
decls: HashSet<DeclarationId>,
deps_from_source: Option<Vec<ManualMemoDependency>>,
manual_memo_id: u32,
}
struct VisitorState<'a> {
env: &'a mut Environment,
manual_memo_state: Option<ManualMemoBlockState>,
scopes: HashSet<ScopeId>,
pruned_scopes: HashSet<ScopeId>,
temporaries: HashMap<IdentifierId, ManualMemoDependency>,
}
pub fn validate_preserved_manual_memoization(
func: &ReactiveFunction,
env: &mut Environment,
) {
let mut state = VisitorState {
env,
manual_memo_state: None,
scopes: HashSet::new(),
pruned_scopes: HashSet::new(),
temporaries: HashMap::new(),
};
visit_block(&func.body, &mut state);
}
fn is_named(ident: &Identifier) -> bool {
matches!(ident.name, Some(IdentifierName::Named(_)))
}
fn visit_block(block: &ReactiveBlock, state: &mut VisitorState) {
for stmt in block {
visit_statement(stmt, state);
}
}
fn visit_statement(stmt: &ReactiveStatement, state: &mut VisitorState) {
match stmt {
ReactiveStatement::Instruction(instr) => {
visit_instruction(instr, state);
}
ReactiveStatement::Terminal(terminal) => {
visit_terminal(terminal, state);
}
ReactiveStatement::Scope(scope_block) => {
visit_scope(scope_block, state);
}
ReactiveStatement::PrunedScope(pruned) => {
visit_pruned_scope(pruned, state);
}
}
}
fn visit_terminal(
terminal: &react_compiler_hir::ReactiveTerminalStatement,
state: &mut VisitorState,
) {
use react_compiler_hir::ReactiveTerminal;
match &terminal.terminal {
ReactiveTerminal::If {
consequent,
alternate,
..
} => {
visit_block(consequent, state);
if let Some(alt) = alternate {
visit_block(alt, state);
}
}
ReactiveTerminal::Switch { cases, .. } => {
for case in cases {
if let Some(ref block) = case.block {
visit_block(block, state);
}
}
}
ReactiveTerminal::For { loop_block, .. }
| ReactiveTerminal::ForOf { loop_block, .. }
| ReactiveTerminal::ForIn { loop_block, .. }
| ReactiveTerminal::While { loop_block, .. }
| ReactiveTerminal::DoWhile { loop_block, .. } => {
visit_block(loop_block, state);
}
ReactiveTerminal::Label { block, .. } => {
visit_block(block, state);
}
ReactiveTerminal::Try {
block, handler, ..
} => {
visit_block(block, state);
visit_block(handler, state);
}
_ => {}
}
}
fn visit_scope(scope_block: &ReactiveScopeBlock, state: &mut VisitorState) {
visit_block(&scope_block.instructions, state);
if let Some(ref memo_state) = state.manual_memo_state {
if let Some(ref deps_from_source) = memo_state.deps_from_source {
let scope = &state.env.scopes[scope_block.scope.0 as usize];
let deps = scope.dependencies.clone();
let memo_loc = memo_state.loc;
let decls = memo_state.decls.clone();
let deps_from_source = deps_from_source.clone();
let temporaries = state.temporaries.clone();
for dep in &deps {
validate_inferred_dep(
dep.identifier,
&dep.path,
&temporaries,
&decls,
&deps_from_source,
state.env,
memo_loc,
);
}
}
}
let scope = &state.env.scopes[scope_block.scope.0 as usize];
let merged = scope.merged.clone();
state.scopes.insert(scope_block.scope);
for merged_id in merged {
state.scopes.insert(merged_id);
}
}
fn visit_pruned_scope(
pruned: &react_compiler_hir::PrunedReactiveScopeBlock,
state: &mut VisitorState,
) {
visit_block(&pruned.instructions, state);
state.pruned_scopes.insert(pruned.scope);
}
fn visit_instruction(instr: &ReactiveInstruction, state: &mut VisitorState) {
record_temporaries(instr, state);
match &instr.value {
ReactiveValue::Instruction(InstructionValue::StartMemoize {
manual_memo_id,
deps,
has_invalid_deps,
..
}) => {
if state.manual_memo_state.is_some() {
return;
}
if *has_invalid_deps {
return;
}
let deps_from_source = deps.clone();
state.manual_memo_state = Some(ManualMemoBlockState {
loc: instr.loc,
decls: HashSet::new(),
deps_from_source,
manual_memo_id: *manual_memo_id,
reassignments: HashMap::new(),
});
let operand_places = start_memoize_operands(deps);
for place in &operand_places {
let ident = &state.env.identifiers[place.identifier.0 as usize];
if let Some(scope_id) = ident.scope {
if !state.scopes.contains(&scope_id)
&& !state.pruned_scopes.contains(&scope_id)
{
let diag = CompilerDiagnostic::new(
ErrorCategory::PreserveManualMemo,
"Existing memoization could not be preserved",
Some(
"React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. \
This dependency may be mutated later, which could cause the value to change unexpectedly".to_string(),
),
)
.with_detail(CompilerDiagnosticDetail::Error {
loc: place.loc,
message: Some(
"This dependency may be modified later".to_string(),
),
identifier_name: None,
});
state.env.record_diagnostic(diag);
}
}
}
}
ReactiveValue::Instruction(InstructionValue::FinishMemoize {
decl,
pruned,
manual_memo_id,
..
}) => {
if state.manual_memo_state.is_none() {
return;
}
if state.manual_memo_state.as_ref().map_or(true, |s| s.manual_memo_id != *manual_memo_id) {
state.manual_memo_state = None;
return;
}
let memo_state = state.manual_memo_state.take().unwrap();
if !pruned {
let decl_ident = &state.env.identifiers[decl.identifier.0 as usize];
if decl_ident.scope.is_none() {
let decls_to_check = memo_state
.reassignments
.get(&decl_ident.declaration_id)
.map(|ids| ids.iter().copied().collect::<Vec<_>>())
.unwrap_or_else(|| vec![decl.identifier]);
for id in decls_to_check {
if is_unmemoized(id, &state.scopes, &state.env.identifiers) {
record_unmemoized_error(decl.loc, state.env);
}
}
} else {
if is_unmemoized(decl.identifier, &state.scopes, &state.env.identifiers) {
record_unmemoized_error(decl.loc, state.env);
}
}
}
}
ReactiveValue::Instruction(InstructionValue::StoreLocal {
lvalue,
value,
..
}) => {
if state.manual_memo_state.is_some() && lvalue.kind == InstructionKind::Reassign {
let decl_id =
state.env.identifiers[lvalue.place.identifier.0 as usize].declaration_id;
state
.manual_memo_state
.as_mut()
.unwrap()
.reassignments
.entry(decl_id)
.or_default()
.insert(value.identifier);
}
}
ReactiveValue::Instruction(InstructionValue::LoadLocal { place, .. }) => {
if state.manual_memo_state.is_some() {
let place_ident = &state.env.identifiers[place.identifier.0 as usize];
if let Some(ref lvalue) = instr.lvalue {
let lvalue_ident = &state.env.identifiers[lvalue.identifier.0 as usize];
if place_ident.scope.is_some() && lvalue_ident.scope.is_none() {
state
.manual_memo_state
.as_mut()
.unwrap()
.reassignments
.entry(lvalue_ident.declaration_id)
.or_default()
.insert(place.identifier);
}
}
}
}
_ => {}
}
}
fn record_unmemoized_error(loc: Option<SourceLocation>, env: &mut Environment) {
let diag = CompilerDiagnostic::new(
ErrorCategory::PreserveManualMemo,
"Existing memoization could not be preserved",
Some(
"React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output".to_string(),
),
)
.with_detail(CompilerDiagnosticDetail::Error {
loc,
message: Some("Could not preserve existing memoization".to_string()),
identifier_name: None,
});
env.record_diagnostic(diag);
}
fn record_temporaries(instr: &ReactiveInstruction, state: &mut VisitorState) {
let lvalue = &instr.lvalue;
let lv_id = lvalue.as_ref().map(|lv| lv.identifier);
if let Some(id) = lv_id {
if state.temporaries.contains_key(&id) {
return;
}
}
if let Some(ref lvalue) = instr.lvalue {
let lv_ident = &state.env.identifiers[lvalue.identifier.0 as usize];
if is_named(lv_ident) && state.manual_memo_state.is_some() {
state
.manual_memo_state
.as_mut()
.unwrap()
.decls
.insert(lv_ident.declaration_id);
}
}
record_deps_in_value(&instr.value, state);
if let Some(ref lvalue) = instr.lvalue {
state.temporaries.insert(
lvalue.identifier,
ManualMemoDependency {
root: ManualMemoDependencyRoot::NamedLocal {
value: lvalue.clone(),
constant: false,
},
path: Vec::new(),
loc: lvalue.loc,
},
);
}
}
fn record_deps_in_value(value: &ReactiveValue, state: &mut VisitorState) {
match value {
ReactiveValue::SequenceExpression {
instructions,
value,
..
} => {
for instr in instructions {
visit_instruction(instr, state);
}
record_deps_in_value(value, state);
}
ReactiveValue::OptionalExpression { value: inner, .. } => {
record_deps_in_value(inner, state);
}
ReactiveValue::ConditionalExpression {
test,
consequent,
alternate,
..
} => {
record_deps_in_value(test, state);
record_deps_in_value(consequent, state);
record_deps_in_value(alternate, state);
}
ReactiveValue::LogicalExpression { left, right, .. } => {
record_deps_in_value(left, state);
record_deps_in_value(right, state);
}
ReactiveValue::Instruction(iv) => {
match iv {
InstructionValue::StoreLocal { lvalue, .. }
| InstructionValue::StoreContext { lvalue, .. } => {
if let Some(ref mut memo_state) = state.manual_memo_state {
let ident =
&state.env.identifiers[lvalue.place.identifier.0 as usize];
memo_state.decls.insert(ident.declaration_id);
if is_named(ident) {
state.temporaries.insert(
lvalue.place.identifier,
ManualMemoDependency {
root: ManualMemoDependencyRoot::NamedLocal {
value: lvalue.place.clone(),
constant: false,
},
path: Vec::new(),
loc: lvalue.place.loc,
},
);
}
}
}
InstructionValue::Destructure { lvalue, .. } => {
if let Some(ref mut memo_state) = state.manual_memo_state {
for place in destructure_lvalue_places(&lvalue.pattern) {
let ident =
&state.env.identifiers[place.identifier.0 as usize];
memo_state.decls.insert(ident.declaration_id);
if is_named(ident) {
state.temporaries.insert(
place.identifier,
ManualMemoDependency {
root: ManualMemoDependencyRoot::NamedLocal {
value: place.clone(),
constant: false,
},
path: Vec::new(),
loc: place.loc,
},
);
}
}
}
}
_ => {}
}
}
}
}
fn start_memoize_operands(deps: &Option<Vec<ManualMemoDependency>>) -> Vec<Place> {
let mut result = Vec::new();
if let Some(deps) = deps {
for dep in deps {
if let ManualMemoDependencyRoot::NamedLocal { value, .. } = &dep.root {
result.push(value.clone());
}
}
}
result
}
fn destructure_lvalue_places(pattern: &react_compiler_hir::Pattern) -> Vec<&Place> {
let mut result = Vec::new();
match pattern {
react_compiler_hir::Pattern::Array(arr) => {
for item in &arr.items {
match item {
react_compiler_hir::ArrayPatternElement::Place(place) => {
result.push(place);
}
react_compiler_hir::ArrayPatternElement::Spread(spread) => {
result.push(&spread.place);
}
react_compiler_hir::ArrayPatternElement::Hole => {}
}
}
}
react_compiler_hir::Pattern::Object(obj) => {
for entry in &obj.properties {
match entry {
react_compiler_hir::ObjectPropertyOrSpread::Property(prop) => {
result.push(&prop.place);
}
react_compiler_hir::ObjectPropertyOrSpread::Spread(spread) => {
result.push(&spread.place);
}
}
}
}
}
result
}
fn is_unmemoized(
id: IdentifierId,
completed_scopes: &HashSet<ScopeId>,
identifiers: &[Identifier],
) -> bool {
let ident = &identifiers[id.0 as usize];
if let Some(scope_id) = ident.scope {
!completed_scopes.contains(&scope_id)
} else {
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum CompareDependencyResult {
Ok = 0,
RootDifference = 1,
PathDifference = 2,
Subpath = 3,
RefAccessDifference = 4,
}
fn compare_deps(
inferred: &ManualMemoDependency,
source: &ManualMemoDependency,
) -> CompareDependencyResult {
let roots_equal = match (&inferred.root, &source.root) {
(
ManualMemoDependencyRoot::Global {
identifier_name: a,
},
ManualMemoDependencyRoot::Global {
identifier_name: b,
},
) => a == b,
(
ManualMemoDependencyRoot::NamedLocal { value: a, .. },
ManualMemoDependencyRoot::NamedLocal { value: b, .. },
) => a.identifier == b.identifier,
_ => false,
};
if !roots_equal {
return CompareDependencyResult::RootDifference;
}
let min_len = inferred.path.len().min(source.path.len());
let mut is_subpath = true;
for i in 0..min_len {
if inferred.path[i].property != source.path[i].property {
is_subpath = false;
break;
} else if inferred.path[i].optional != source.path[i].optional {
return CompareDependencyResult::PathDifference;
}
}
if is_subpath
&& (source.path.len() == inferred.path.len()
|| (inferred.path.len() >= source.path.len()
&& !inferred.path.iter().any(|t| t.property == react_compiler_hir::PropertyLiteral::String("current".to_string()))))
{
CompareDependencyResult::Ok
} else if is_subpath {
if source.path.iter().any(|t| t.property == react_compiler_hir::PropertyLiteral::String("current".to_string()))
|| inferred.path.iter().any(|t| t.property == react_compiler_hir::PropertyLiteral::String("current".to_string()))
{
CompareDependencyResult::RefAccessDifference
} else {
CompareDependencyResult::Subpath
}
} else {
CompareDependencyResult::PathDifference
}
}
fn pretty_print_scope_dependency(
dep_id: IdentifierId,
dep_path: &[DependencyPathEntry],
identifiers: &[react_compiler_hir::Identifier],
) -> String {
let ident = &identifiers[dep_id.0 as usize];
let root_str = match &ident.name {
Some(react_compiler_hir::IdentifierName::Named(n)) => n.clone(),
Some(react_compiler_hir::IdentifierName::Promoted(n)) => n.clone(),
None => "[unnamed]".to_string(),
};
let path_str: String = dep_path.iter().map(|entry| {
let prop = match &entry.property {
react_compiler_hir::PropertyLiteral::String(s) => s.clone(),
react_compiler_hir::PropertyLiteral::Number(n) => format!("{}", n),
};
if entry.optional {
format!("?.{}", prop)
} else {
format!(".{}", prop)
}
}).collect();
format!("{}{}", root_str, path_str)
}
fn print_manual_memo_dependency(
dep: &ManualMemoDependency,
identifiers: &[react_compiler_hir::Identifier],
with_optional: bool,
) -> String {
let root_str = match &dep.root {
ManualMemoDependencyRoot::NamedLocal { value, .. } => {
let ident = &identifiers[value.identifier.0 as usize];
match &ident.name {
Some(react_compiler_hir::IdentifierName::Named(n)) => n.clone(),
Some(react_compiler_hir::IdentifierName::Promoted(n)) => n.clone(),
None => "[unnamed]".to_string(),
}
}
ManualMemoDependencyRoot::Global { identifier_name } => identifier_name.clone(),
};
let path_str: String = dep.path.iter().map(|entry| {
let prop = match &entry.property {
react_compiler_hir::PropertyLiteral::String(s) => s.clone(),
react_compiler_hir::PropertyLiteral::Number(n) => format!("{}", n),
};
if with_optional && entry.optional {
format!("?.{}", prop)
} else {
format!(".{}", prop)
}
}).collect();
format!("{}{}", root_str, path_str)
}
fn get_compare_dependency_result_description(
result: CompareDependencyResult,
) -> &'static str {
match result {
CompareDependencyResult::Ok => "Dependencies equal",
CompareDependencyResult::RootDifference | CompareDependencyResult::PathDifference => {
"Inferred different dependency than source"
}
CompareDependencyResult::RefAccessDifference => "Differences in ref.current access",
CompareDependencyResult::Subpath => "Inferred less specific property than source",
}
}
fn validate_inferred_dep(
dep_id: IdentifierId,
dep_path: &[DependencyPathEntry],
temporaries: &HashMap<IdentifierId, ManualMemoDependency>,
decls_within_memo_block: &HashSet<DeclarationId>,
valid_deps_in_memo_block: &[ManualMemoDependency],
env: &mut Environment,
memo_location: Option<SourceLocation>,
) {
let normalized_dep = if let Some(temp) = temporaries.get(&dep_id) {
let mut path = temp.path.clone();
path.extend_from_slice(dep_path);
ManualMemoDependency {
root: temp.root.clone(),
path,
loc: temp.loc,
}
} else {
let ident = &env.identifiers[dep_id.0 as usize];
if !is_named(ident) {
return;
}
ManualMemoDependency {
root: ManualMemoDependencyRoot::NamedLocal {
value: Place {
identifier: dep_id,
effect: react_compiler_hir::Effect::Read,
reactive: false,
loc: ident.loc,
},
constant: false,
},
path: dep_path.to_vec(),
loc: ident.loc,
}
};
if let ManualMemoDependencyRoot::NamedLocal { value, .. } = &normalized_dep.root {
let ident = &env.identifiers[value.identifier.0 as usize];
if decls_within_memo_block.contains(&ident.declaration_id) {
return;
}
}
let mut error_diagnostic: Option<CompareDependencyResult> = None;
for source_dep in valid_deps_in_memo_block {
let result = compare_deps(&normalized_dep, source_dep);
if result == CompareDependencyResult::Ok {
return;
}
error_diagnostic = Some(match error_diagnostic {
Some(prev) => prev.max(result),
None => result,
});
}
let ident = &env.identifiers[dep_id.0 as usize];
let extra = if is_named(ident) {
let dep_str = pretty_print_scope_dependency(
dep_id,
dep_path,
&env.identifiers,
);
let source_deps_str: String = valid_deps_in_memo_block
.iter()
.map(|d| print_manual_memo_dependency(d, &env.identifiers, true))
.collect::<Vec<_>>()
.join(", ");
let result_desc = error_diagnostic
.map(|d| get_compare_dependency_result_description(d).to_string())
.unwrap_or_else(|| "Inferred dependency not present in source".to_string());
format!(
"The inferred dependency was `{}`, but the source dependencies were [{}]. {}",
dep_str, source_deps_str, result_desc
)
} else {
String::new()
};
let description = format!(
"React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. \
The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. {}",
extra
);
let diag = CompilerDiagnostic::new(
ErrorCategory::PreserveManualMemo,
"Existing memoization could not be preserved",
Some(description.trim().to_string()),
)
.with_detail(CompilerDiagnosticDetail::Error {
loc: memo_location,
message: Some("Could not preserve existing manual memoization".to_string()),
identifier_name: None,
});
env.record_diagnostic(diag);
}