11
//! creation, aliasing, mutation, freezing, and error conditions for each
12
//! instruction and terminal in the HIR.
13
14
-use indexmap::{IndexMap, IndexSet};
14
+use indexmap::IndexMap;
15
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
16
17
use react_compiler_diagnostics::CompilerDiagnostic;
69
value_id,
70
AbstractValue {
71
kind: ValueKind::Context,
72
- reason: hashset_of(ValueReason::Other),
72
+ reason: ValueReasonSet::single(ValueReason::Other),
73
},
74
);
75
initial_state.define(ctx_place.identifier, value_id);
78
let param_kind: AbstractValue = if is_function_expression {
79
AbstractValue {
80
kind: ValueKind::Mutable,
81
- reason: hashset_of(ValueReason::Other),
81
+ reason: ValueReasonSet::single(ValueReason::Other),
82
}
83
} else {
84
AbstractValue {
85
kind: ValueKind::Frozen,
86
- reason: hashset_of(ValueReason::ReactiveFunctionArgument),
86
+ reason: ValueReasonSet::single(ValueReason::ReactiveFunctionArgument),
87
}
88
};
89
103
value_id,
104
AbstractValue {
105
kind: ValueKind::Mutable,
106
- reason: hashset_of(ValueReason::Other),
106
+ reason: ValueReasonSet::single(ValueReason::Other),
107
},
108
);
109
initial_state.define(ref_place.identifier, value_id);
258
// AbstractValue
259
// =============================================================================
260
261
-#[derive(Debug, Clone)]
261
+#[derive(Debug, Clone, Copy)]
262
struct AbstractValue {
263
kind: ValueKind,
264
- reason: IndexSet<ValueReason, FxBuildHasher>,
264
+ reason: ValueReasonSet,
265
+}
266
+
267
+/// Capacity of [`ValueReasonSet`]. A set holds at most one of each `ValueReason`
268
+/// variant, of which there are currently 12; the extra slots are headroom so
269
+/// that adding variants upstream cannot overflow the set.
270
+const VALUE_REASON_CAPACITY: usize = 16;
271
+
272
+/// An insertion-ordered set of [`ValueReason`]s, stored inline.
273
+///
274
+/// This is a deliberate replacement for `IndexSet`, enabling insertion-order
275
+/// memory while avoiding any heap allocation. At `AbstractValue`'s scale, this
276
+/// has a dramatic impact on heap memory and wall time.
277
+/// This takes advantage of the format of the data it's actually storing. A set
278
+/// can hold at most one of each variant, so the members fit into a fixed inline
279
+/// array. `ValueReason` is implemented as a single byte, so this struct is
280
+/// ~18 bytes on the stack.
281
+///
282
+/// Insertion order is preserved deliberately: [`primary_reason`] returns the
283
+/// first non-`Other` member, matching the iteration order of the `Set` used by
284
+/// the TypeScript implementation this is ported from.
285
+#[derive(Debug, Clone, Copy)]
286
+struct ValueReasonSet {
287
+ /// Members in insertion order. Only the first `len` entries are meaningful.
288
+ members: [ValueReason; VALUE_REASON_CAPACITY],
289
+ len: u8,
290
}
291
267
-fn hashset_of(r: ValueReason) -> IndexSet<ValueReason, FxBuildHasher> {
268
- let mut s = IndexSet::default();
269
- s.insert(r);
270
- s
292
+impl Default for ValueReasonSet {
293
+ fn default() -> Self {
294
+ ValueReasonSet {
295
+ members: [ValueReason::Other; VALUE_REASON_CAPACITY],
296
+ len: 0,
297
+ }
298
+ }
299
+}
300
+
301
+impl ValueReasonSet {
302
+ fn single(reason: ValueReason) -> Self {
303
+ let mut set = Self::default();
304
+ set.insert(reason);
305
+ set
306
+ }
307
+
308
+ fn contains(&self, reason: ValueReason) -> bool {
309
+ self.members[..self.len as usize].contains(&reason)
310
+ }
311
+
312
+ fn iter(&self) -> impl Iterator<Item = ValueReason> + '_ {
313
+ self.members[..self.len as usize].iter().copied()
314
+ }
315
+
316
+ /// Appends `reason` if not already present, preserving insertion order.
317
+ fn insert(&mut self, reason: ValueReason) {
318
+ if self.contains(reason) {
319
+ return;
320
+ }
321
+ debug_assert!(
322
+ (self.len as usize) < VALUE_REASON_CAPACITY,
323
+ "ValueReasonSet capacity must cover every ValueReason variant"
324
+ );
325
+ if (self.len as usize) < VALUE_REASON_CAPACITY {
326
+ self.members[self.len as usize] = reason;
327
+ self.len += 1;
328
+ }
329
+ }
330
+
331
+ /// True when every member of `other` is also a member of `self`.
332
+ fn is_superset_of(&self, other: &ValueReasonSet) -> bool {
333
+ other.iter().all(|reason| self.contains(reason))
334
+ }
335
+
336
+ /// Adds every member of `other`, keeping `self`'s existing order and
337
+ /// appending newcomers in `other`'s order — matching `IndexSet::insert`.
338
+ fn union_with(&mut self, other: &ValueReasonSet) {
339
+ for reason in other.iter() {
340
+ self.insert(reason);
341
+ }
342
+ }
343
}
344
345
// =============================================================================
387
}
388
return AbstractValue {
389
kind: ValueKind::Mutable,
318
- reason: hashset_of(ValueReason::Other),
390
+ reason: ValueReasonSet::single(ValueReason::Other),
391
};
392
}
393
};
404
}
405
merged_kind.unwrap_or_else(|| AbstractValue {
406
kind: ValueKind::Mutable,
335
- reason: hashset_of(ValueReason::Other),
407
+ reason: ValueReasonSet::single(ValueReason::Other),
408
})
409
}
410
432
vid,
433
AbstractValue {
434
kind: ValueKind::Mutable,
363
- reason: hashset_of(ValueReason::Other),
435
+ reason: ValueReasonSet::single(ValueReason::Other),
436
},
437
);
438
}
510
value_id,
511
AbstractValue {
512
kind: ValueKind::Frozen,
441
- reason: hashset_of(reason),
513
+ reason: ValueReasonSet::single(reason),
514
},
515
);
516
// Note: In TS, this also transitively freezes FunctionExpression captures
565
if let Some(other_value) = other.values.get(id) {
566
let merged = merge_abstract_values(this_value, other_value);
567
if merged.kind != this_value.kind
496
- || !is_superset(&this_value.reason, &merged.reason)
568
+ || !this_value.reason.is_superset_of(&merged.reason)
569
{
570
let nv = next_values.get_or_insert_with(|| self.values.clone());
571
nv.insert(*id, merged);
638
}
639
}
640
569
-fn is_superset(
570
- a: &IndexSet<ValueReason, FxBuildHasher>,
571
- b: &IndexSet<ValueReason, FxBuildHasher>,
572
-) -> bool {
573
- b.iter().all(|x| a.contains(x))
574
-}
575
-
641
#[derive(Debug, Clone, Copy)]
642
enum MutateVariant {
643
Mutate,
803
804
fn merge_abstract_values(a: &AbstractValue, b: &AbstractValue) -> AbstractValue {
805
let kind = merge_value_kinds(a.kind, b.kind);
741
- if kind == a.kind && kind == b.kind && is_superset(&a.reason, &b.reason) {
806
+ if kind == a.kind && kind == b.kind && a.reason.is_superset_of(&b.reason) {
807
return a.clone();
808
}
744
- let mut reason = a.reason.clone();
745
- for r in &b.reason {
746
- reason.insert(*r);
747
- }
809
+ let mut reason = a.reason;
810
+ reason.union_with(&b.reason);
811
AbstractValue { kind, reason }
812
}
813
1296
vid,
1297
AbstractValue {
1298
kind: ValueKind::Mutable,
1236
- reason: hashset_of(ValueReason::Other),
1299
+ reason: ValueReasonSet::single(ValueReason::Other),
1300
},
1301
);
1302
state.define(instr.lvalue.identifier, vid);
1404
value_id,
1405
AbstractValue {
1406
kind,
1344
- reason: hashset_of(reason),
1407
+ reason: ValueReasonSet::single(reason),
1408
},
1409
);
1410
state.define(into.identifier, value_id);
1433
value_id,
1434
AbstractValue {
1435
kind: from_value.kind,
1373
- reason: from_value.reason.clone(),
1436
+ reason: from_value.reason,
1437
},
1438
);
1439
state.define(into.identifier, value_id);
1550
} else {
1551
ValueKind::Frozen
1552
},
1490
- reason: IndexSet::default(),
1553
+ reason: ValueReasonSet::default(),
1554
},
1555
);
1556
state.define(into.identifier, value_id);
1662
value_id,
1663
AbstractValue {
1664
kind: from_value.kind,
1602
- reason: from_value.reason.clone(),
1665
+ reason: from_value.reason,
1666
},
1667
);
1668
state.define(into.identifier, value_id);
1678
value_id,
1679
AbstractValue {
1680
kind: from_value.kind,
1618
- reason: from_value.reason.clone(),
1681
+ reason: from_value.reason,
1682
},
1683
);
1684
state.define(into.identifier, value_id);
3473
/// since the primary reason is always inserted first, this effectively
3474
/// picks the most specific non-Other reason. We replicate this by
3475
/// preferring any non-Other reason over Other.
3413
-fn primary_reason(reasons: &IndexSet<ValueReason, FxBuildHasher>) -> ValueReason {
3414
- for &r in reasons {
3476
+fn primary_reason(reasons: &ValueReasonSet) -> ValueReason {
3477
+ for r in reasons.iter() {
3478
if r != ValueReason::Other {
3479
return r;
3480
}
3483
}
3484
3485
fn get_write_error_reason(abstract_value: &AbstractValue) -> String {
3423
- if abstract_value.reason.contains(&ValueReason::Global) {
3486
+ if abstract_value.reason.contains(ValueReason::Global) {
3487
"Modifying a variable defined outside a component or hook is not allowed. Consider using an effect".to_string()
3425
- } else if abstract_value.reason.contains(&ValueReason::JsxCaptured) {
3488
+ } else if abstract_value.reason.contains(ValueReason::JsxCaptured) {
3489
"Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX".to_string()
3427
- } else if abstract_value.reason.contains(&ValueReason::Context) {
3490
+ } else if abstract_value.reason.contains(ValueReason::Context) {
3491
"Modifying a value returned from 'useContext()' is not allowed.".to_string()
3492
} else if abstract_value
3493
.reason
3431
- .contains(&ValueReason::KnownReturnSignature)
3494
+ .contains(ValueReason::KnownReturnSignature)
3495
{
3496
"Modifying a value returned from a function whose return value should not be mutated"
3497
.to_string()
3498
} else if abstract_value
3499
.reason
3437
- .contains(&ValueReason::ReactiveFunctionArgument)
3500
+ .contains(ValueReason::ReactiveFunctionArgument)
3501
{
3502
"Modifying component props or hook arguments is not allowed. Consider using a local variable instead".to_string()
3440
- } else if abstract_value.reason.contains(&ValueReason::State) {
3503
+ } else if abstract_value.reason.contains(ValueReason::State) {
3504
"Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead".to_string()
3442
- } else if abstract_value.reason.contains(&ValueReason::ReducerState) {
3505
+ } else if abstract_value.reason.contains(ValueReason::ReducerState) {
3506
"Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead".to_string()
3444
- } else if abstract_value.reason.contains(&ValueReason::Effect) {
3507
+ } else if abstract_value.reason.contains(ValueReason::Effect) {
3508
"Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()".to_string()
3446
- } else if abstract_value.reason.contains(&ValueReason::HookCaptured) {
3509
+ } else if abstract_value.reason.contains(ValueReason::HookCaptured) {
3510
"Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook".to_string()
3448
- } else if abstract_value.reason.contains(&ValueReason::HookReturn) {
3511
+ } else if abstract_value.reason.contains(ValueReason::HookReturn) {
3512
"Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed".to_string()
3513
} else {
3514
"This modifies a variable that React considers immutable".to_string()