Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 124 additions & 130 deletions compiler/Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion compiler/crates/react_compiler_ast/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ indexmap = { version = "2", features = ["serde"] }

[dev-dependencies]
walkdir = "2"
similar = "2"
similar = "3"
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,26 @@ pub(crate) fn find_disjoint_mutable_values(
.map(|iid| func.instructions[iid.0 as usize].id)
.unwrap_or(block.terminal.evaluation_order());

if phi_range.start.0 + 1 != phi_range.end.0 && phi_range.end > first_instr_id {
let is_phi_mutated_after_creation = phi_range.start.0 + 1 != phi_range.end.0
&& phi_range.end > first_instr_id;
// A phi operand defined at or after the phi's block is a loop
// back-edge: the variable is reassigned within the loop (eg a
// counter `a++` or `a = a + 1`). The reassignment must count as
// the loop's scope reassigning the variable, so union the phi
// with its operands and declaration. Otherwise the variable's
// pre-loop value would become a dependency of the scope even
// though the scope changes the value as it executes, making the
// scope's dependencies unstable (the cached dependency would be
// the post-loop value, which can never match the pre-loop value
// compared at the top of the scope).
let is_loop_carried_reassignment = !is_phi_mutated_after_creation
&& phi.operands.iter().any(|(_pred_id, operand)| {
env.identifiers[operand.identifier.0 as usize]
.mutable_range
.start
>= first_instr_id
});
if is_phi_mutated_after_creation || is_loop_carried_reassignment {
let mut operands = vec![phi_id];
if let Some(&decl_id) = declarations.get(&phi_decl_id) {
operands.push(decl_id);
Expand Down
3 changes: 1 addition & 2 deletions compiler/crates/react_compiler_reactive_scopes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,4 @@ react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
react_compiler_hir = { path = "../react_compiler_hir" }
indexmap = "2"
serde_json = "1"
sha2 = "0.10"
hmac = "0.12"
hmac-sha256 = "1"
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,17 @@ pub struct OutlinedFunction {
}

/// Top-level entry point: generates code for a reactive function.
/// Computes the Fast Refresh source hash used to bust the memo cache when the
/// source file changes. Matches the TS compiler's
/// `createHmac('sha256', code).digest('hex')`: an HMAC-SHA256 keyed by the
/// source code, hashing empty data.
fn source_file_hash(code: &str) -> String {
hmac_sha256::HMAC::mac(b"", code.as_bytes())
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}

pub fn codegen_function(
func: &ReactiveFunction,
env: &mut Environment,
Expand All @@ -194,15 +205,7 @@ pub fn codegen_function(
let fast_refresh_state: Option<(u32, String)> =
if cx.env.config.enable_reset_cache_on_source_file_changes == Some(true) {
if let Some(ref code) = cx.env.code {
use hmac::Hmac;
use hmac::Mac;
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
// Match TS: createHmac('sha256', code).digest('hex')
// Node's createHmac uses the code as the HMAC key and hashes empty data.
let mac = HmacSha256::new_from_slice(code.as_bytes())
.expect("HMAC can take key of any size");
let hash = format!("{:x}", mac.finalize().into_bytes());
let hash = source_file_hash(code);
let cache_index = cx.alloc_cache_index(); // Reserve slot 0 for the hash check
Some((cache_index, hash))
} else {
Expand Down Expand Up @@ -382,7 +385,9 @@ pub fn codegen_function(
arguments: vec![Expression::StringLiteral(
StringLiteral {
base: BaseNode::typed("StringLiteral"),
value: MEMO_CACHE_SENTINEL.to_string().into(),
value: MEMO_CACHE_SENTINEL
.to_string()
.into(),
},
)],
type_parameters: None,
Expand Down Expand Up @@ -4239,6 +4244,27 @@ mod tests {

use super::{UnsupportedOriginalNode, codegen_unsupported_original_node};

/// The Fast Refresh source hash must match Node's
/// `createHmac('sha256', code).digest('hex')` byte-for-byte, or hot-reload
/// cache invalidation would diverge from the TS compiler. Reference values
/// were computed with Node's `crypto` module.
#[test]
fn source_file_hash_matches_node_create_hmac() {
use super::source_file_hash;
assert_eq!(
source_file_hash("hello world"),
"0de8bee5d7f9c5d209f8c6fabed0ea84cb3fca1244e8ed38079a61b599a84c47"
);
assert_eq!(
source_file_hash(""),
"b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"
);
assert_eq!(
source_file_hash("function App(){}"),
"d637acb4985c789d6622c70197db2b62dda282f16f3276aa810b598d6e6cab7b"
);
}

/// A modeled statement tag parses typed and is emitted directly.
#[test]
fn unsupported_original_node_modeled_statement_tag_emits_statement() {
Expand Down
1 change: 0 additions & 1 deletion compiler/crates/react_compiler_ssa/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@ edition = "2024"
[dependencies]
react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
react_compiler_hir = { path = "../react_compiler_hir" }
react_compiler_lowering = { path = "../react_compiler_lowering" }
indexmap = "2"
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ edition = "2024"
crate-type = ["cdylib"]

[dependencies]
napi = { version = "2", features = ["napi4"] }
napi-derive = "2"
napi = { version = "3", features = ["napi4"] }
napi-derive = "3"
react_compiler = { path = "../../../crates/react_compiler" }
react_compiler_ast = { path = "../../../crates/react_compiler_ast" }
serde = "1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
eachPatternOperand,
} from '../HIR/visitors';
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
import {Iterable_some, assertExhaustive} from '../Utils/utils';

/*
* Note: this is the 1st of 4 passes that determine how to break a function into discrete
Expand Down Expand Up @@ -287,12 +287,31 @@ export function findDisjointMutableValues(
* are assigned to the same scope.
*/
for (const phi of block.phis) {
if (
const firstInstructionIdOfBlock =
block.instructions.at(0)?.id ?? block.terminal.id;
const isPhiMutatedAfterCreation =
phi.place.identifier.mutableRange.start + 1 !==
phi.place.identifier.mutableRange.end &&
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id)
) {
phi.place.identifier.mutableRange.end > firstInstructionIdOfBlock;
/*
* A phi operand defined at or after the phi's block is a loop back-edge:
* the variable is reassigned within the loop (eg a counter `a++` or
* `a = a + 1`). The reassignment must count as the loop's scope
* reassigning the variable, so union the phi with its operands and
* declaration. Otherwise the variable's pre-loop value would become a
* dependency of the scope even though the scope changes the value as it
* executes, making the scope's dependencies unstable (the cached
* dependency would be the post-loop value, which can never match the
* pre-loop value compared at the top of the scope).
*/
const isLoopCarriedReassignment =
!isPhiMutatedAfterCreation &&
Iterable_some(
phi.operands.values(),
operand =>
operand.identifier.mutableRange.start >= firstInstructionIdOfBlock,
);
if (isPhiMutatedAfterCreation || isLoopCarriedReassignment) {
const operands = [phi.place.identifier];
const declaration = declarations.get(
phi.place.identifier.declarationId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,19 @@ import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
let x;
let t0;
if ($[0] !== props.value) {
t0 = { ...props.value };
const object = { ...props.value };
for (const y in object) {
if (y === "break") {
break;
}

x = object[y];
}
$[0] = props.value;
$[1] = t0;
$[1] = x;
} else {
t0 = $[1];
}
const object = t0;
for (const y in object) {
if (y === "break") {
break;
}

x = object[y];
x = $[1];
}

return x;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,19 @@ import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
let x;
let t0;
if ($[0] !== props.value) {
t0 = { ...props.value };
const object = { ...props.value };
for (const y in object) {
if (y === "continue") {
continue;
}

x = object[y];
}
$[0] = props.value;
$[1] = t0;
$[1] = x;
} else {
t0 = $[1];
}
const object = t0;
for (const y in object) {
if (y === "continue") {
continue;
}

x = object[y];
x = $[1];
}

return x;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,21 @@ export const FIXTURE_ENTRYPOINT = {
## Code

```javascript
// @enablePreserveExistingMemoizationGuarantees:false
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
const { identity, mutate } = require("shared-runtime");

function Component(props) {
const $ = _c(2);
let x;
const object = { ...props.value };
for (const y in object) {
x = y;
if ($[0] !== props.value) {
const object = { ...props.value };
for (const y in object) {
x = y;
}
$[0] = props.value;
$[1] = x;
} else {
x = $[1];
}

mutate(x);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

## Input

```javascript
import {Stringify} from 'shared-runtime';

function Component({count}) {
let a = 0;
const items = [];
for (let i = 0; i < count; i++) {
a = a + 1;
items.push(a);
}
return <Stringify items={items} a={a} />;
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{count: 2}],
sequentialRenders: [{count: 2}, {count: 2}, {count: 3}],
};

```

## Code

```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";

function Component(t0) {
const $ = _c(2);
const { count } = t0;
let t1;
if ($[0] !== count) {
let a = 0;
const items = [];
for (let i = 0; i < count; i++) {
a = a + 1;
items.push(a);
}
t1 = <Stringify items={items} a={a} />;
$[0] = count;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ count: 2 }],
sequentialRenders: [{ count: 2 }, { count: 2 }, { count: 3 }],
};

```

### Eval output
(kind: ok) <div>{"items":[1,2],"a":2}</div>
<div>{"items":[1,2],"a":2}</div>
<div>{"items":[1,2,3],"a":3}</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Stringify} from 'shared-runtime';

function Component({count}) {
let a = 0;
const items = [];
for (let i = 0; i < count; i++) {
a = a + 1;
items.push(a);
}
return <Stringify items={items} a={a} />;
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{count: 2}],
sequentialRenders: [{count: 2}, {count: 2}, {count: 3}],
};
Loading
Loading