From 0ca9d2077245a026f3b1430cede786a69d312434 Mon Sep 17 00:00:00 2001 From: lauren Date: Wed, 17 Jun 2026 15:54:14 -0700 Subject: [PATCH 1/2] [compiler] Count loop reassignments as the enclosing scope reassigning the variable (#36732) A counter initialized before a memo scope and incremented inside it (`a++` or `a = a + 1` in a `for` body) was emitted as a scope *dependency*, compared at its pre-loop value (constant every render) while the cache stored its post-loop value. The memo could never hit, so the scope recomputed on every render. Root cause: the phi-union rule in `InferReactiveScopeVariables.findDisjointMutableValues` only unioned a phi into the scope when the phi value was mutated after creation, which range-extension only does for object mutation. Primitive reassignments around a loop back-edge never extend ranges, so the counter's SSA versions stayed outside the scope and downstream dependency propagation classified the pre-loop read as a dep. The fix unions a phi with its operands and declaration when any operand is defined at or after the phi's block, i.e. the value is reassigned around a loop back-edge. This matches the shape the compiler already produced for non-primitive loop reassignment (`x = [...x, i]`). Implemented identically in the TypeScript compiler and the Rust port. Both `a++` and `a = a + 1` variants are pinned by fixtures; the first commit documents the previously-wrong codegen, the second fixes it (counter becomes a scope output, dep on `count` only). Corpus delta beyond the new fixtures is 4 fixtures, all strict improvements with byte-identical eval output: `for-in-statement-break`, `for-in-statement-continue`, `for-in-statement-type-inference` (loops previously re-ran every render, now memoized), and `sequence-expression` (two memo blocks collapse into one). Known limitation, unchanged from before: conditional reassignment in a loop (`for (...) { if (c) a++; }`) routes through a join phi that this rule cannot see; that shape behaves as it did before this change. Verification: TS snap 1806/1806, Rust snap 1806/1806, cargo workspace green, scoped TS-vs-Rust HIR parity harness green. Closes #34971 --- .../src/infer_reactive_scope_variables.rs | 21 ++++++- .../InferReactiveScopeVariables.ts | 29 +++++++-- .../compiler/for-in-statement-break.expect.md | 22 +++---- .../for-in-statement-continue.expect.md | 22 +++---- .../for-in-statement-type-inference.expect.md | 15 +++-- ...p-counter-memoization-assignment.expect.md | 62 +++++++++++++++++++ ...for-loop-counter-memoization-assignment.js | 17 +++++ .../for-loop-counter-memoization.expect.md | 62 +++++++++++++++++++ .../compiler/for-loop-counter-memoization.js | 17 +++++ .../compiler/sequence-expression.expect.md | 16 ++--- 10 files changed, 238 insertions(+), 45 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization-assignment.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization-assignment.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization.js diff --git a/compiler/crates/react_compiler_inference/src/infer_reactive_scope_variables.rs b/compiler/crates/react_compiler_inference/src/infer_reactive_scope_variables.rs index a64c73e91411..8847f21214b5 100644 --- a/compiler/crates/react_compiler_inference/src/infer_reactive_scope_variables.rs +++ b/compiler/crates/react_compiler_inference/src/infer_reactive_scope_variables.rs @@ -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); diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts index cbc973efa8d1..7585130c40a4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts @@ -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 @@ -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, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-break.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-break.expect.md index a9d20954d00b..7c2d1164ca3c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-break.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-break.expect.md @@ -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; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-continue.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-continue.expect.md index 06f13c05775a..2625b2b2bd1e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-continue.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-continue.expect.md @@ -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; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-type-inference.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-type-inference.expect.md index 686d95a6f504..e9f0f74eac88 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-type-inference.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-in-statement-type-inference.expect.md @@ -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); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization-assignment.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization-assignment.expect.md new file mode 100644 index 000000000000..10a399c0ef34 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization-assignment.expect.md @@ -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 ; +} + +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 = ; + $[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)
{"items":[1,2],"a":2}
+
{"items":[1,2],"a":2}
+
{"items":[1,2,3],"a":3}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization-assignment.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization-assignment.js new file mode 100644 index 000000000000..eed82622088f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization-assignment.js @@ -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 ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{count: 2}], + sequentialRenders: [{count: 2}, {count: 2}, {count: 3}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization.expect.md new file mode 100644 index 000000000000..6bff9c3b18e5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization.expect.md @@ -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++; + items.push(a); + } + return ; +} + +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++; + items.push(a); + } + t1 = ; + $[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)
{"items":[1,2],"a":2}
+
{"items":[1,2],"a":2}
+
{"items":[1,2,3],"a":3}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization.js new file mode 100644 index 000000000000..687448dce693 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-counter-memoization.js @@ -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++; + items.push(a); + } + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{count: 2}], + sequentialRenders: [{count: 2}, {count: 2}, {count: 3}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequence-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequence-expression.expect.md index 3d06e2421d9b..1b85cd7b3dcf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequence-expression.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/sequence-expression.expect.md @@ -19,22 +19,16 @@ function foo() {} ```javascript import { c as _c } from "react/compiler-runtime"; function sequence(props) { - const $ = _c(2); - let t0; + const $ = _c(1); + let x; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = (Math.max(1, 2), foo()); - $[0] = t0; - } else { - t0 = $[0]; - } - let x = t0; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + x = (Math.max(1, 2), foo()); while ((foo(), true)) { x = (foo(), 2); } - $[1] = x; + $[0] = x; } else { - x = $[1]; + x = $[0]; } return x; From 560db51408f4c186016ccdd8062af75a77c22104 Mon Sep 17 00:00:00 2001 From: Boshen Date: Thu, 18 Jun 2026 07:07:24 +0800 Subject: [PATCH 2/2] [rust-compiler] Switch to hmac-sha256 and bump napi/similar (#36809) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Dependency maintenance for the Rust compiler crates. ### Fast Refresh hash: sha2 + hmac → hmac-sha256 The Fast Refresh source hash (`enableResetCacheOnSourceFileChanges`) only needs an HMAC-SHA256 of the source text, matching the TS compiler's `createHmac('sha256', code).digest('hex')`. RustCrypto's `sha2` + `hmac` pulled in **11 crates** (`sha2`, `hmac`, `digest`, `block-buffer`, `typenum`, `crypto-common`, `hybrid-array`, `const-oid`, `cpufeatures`, `cmov`, `ctutils`) — generic-hashing / constant-time machinery that is irrelevant for a non-security content fingerprint. This replaces them with the single **zero-dependency** `hmac-sha256` crate: a net reduction of **10 crates**. HMAC-SHA256 is a standardized deterministic algorithm, so the emitted hash is unchanged. A new unit test `source_file_hash_matches_node_create_hmac` pins the result against Node's `createHmac('sha256', code).digest('hex')` for several inputs. ### Other bumps - `napi` / `napi-derive` 2 → 3 - `similar` 2 → 3 (dev-dependency) - Drop the unused `react_compiler_lowering` dependency from `react_compiler_ssa` `cargo check --workspace --all-targets` passes, including the napi 3 native crate. --- compiler/Cargo.lock | 254 +++++++++--------- compiler/crates/react_compiler_ast/Cargo.toml | 2 +- .../react_compiler_reactive_scopes/Cargo.toml | 3 +- .../src/codegen_reactive_function.rs | 46 +++- compiler/crates/react_compiler_ssa/Cargo.toml | 1 - .../native/Cargo.toml | 4 +- 6 files changed, 164 insertions(+), 146 deletions(-) diff --git a/compiler/Cargo.lock b/compiler/Cargo.lock index cd8be675a22b..9d6fa50b75c6 100644 --- a/compiler/Cargo.lock +++ b/compiler/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - [[package]] name = "bitflags" version = "2.11.0" @@ -18,12 +9,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] -name = "block-buffer" -version = "0.10.4" +name = "bstr" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ - "generic-array", + "memchr", + "serde", ] [[package]] @@ -34,67 +26,117 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "convert_case" -version = "0.6.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" dependencies = [ "unicode-segmentation", ] [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "ctor" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "400a21f1014a968ec518c7ccdf9b4a4ed0cac8c56ccb6d604f8b91f00110501e" + +[[package]] +name = "ctor" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "futures" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ - "libc", + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] -name = "crypto-common" -version = "0.1.7" +name = "futures-channel" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ - "generic-array", - "typenum", + "futures-core", + "futures-sink", ] [[package]] -name = "ctor" -version = "0.2.9" +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ - "quote", - "syn", + "futures-core", + "futures-task", + "futures-util", ] [[package]] -name = "digest" -version = "0.10.7" +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ - "block-buffer", - "crypto-common", - "subtle", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "futures-sink" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] -name = "generic-array" -version = "0.14.7" +name = "futures-task" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "typenum", - "version_check", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", ] [[package]] @@ -104,13 +146,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] -name = "hmac" -version = "0.12.1" +name = "hmac-sha256" +version = "1.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" [[package]] name = "indexmap" @@ -130,17 +169,11 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "libc" -version = "0.2.183" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" - [[package]] name = "libloading" -version = "0.8.9" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" dependencies = [ "cfg-if", "windows-link", @@ -154,15 +187,17 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "napi" -version = "2.16.17" +version = "3.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" +checksum = "8e55037284865448ecf329baa86a4d05401f647ebde99f5747b640d32c2c5226" dependencies = [ "bitflags", - "ctor", - "napi-derive", + "ctor 0.11.1", + "futures", + "napi-build", "napi-sys", - "once_cell", + "nohash-hasher", + "rustc-hash", ] [[package]] @@ -173,12 +208,12 @@ checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" [[package]] name = "napi-derive" -version = "2.16.13" +version = "3.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +checksum = "89b3f766e04667e6da0e181e2da4f85475d5a6513b7cf6a80bea184e224a5b42" dependencies = [ - "cfg-if", "convert_case", + "ctor 1.0.7", "napi-derive-backend", "proc-macro2", "quote", @@ -187,33 +222,37 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "1.0.75" +version = "5.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +checksum = "0d5af30503edf933ce7377cf6d4c877a62b0f1107ea05585f1b5e430e88d5baf" dependencies = [ "convert_case", - "once_cell", "proc-macro2", "quote", - "regex", "semver", "syn", ] [[package]] name = "napi-sys" -version = "2.4.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +checksum = "1f5bcdf71abd3a50d00b49c1c2c75251cb3c913777d6139cd37dabc093a5e400" dependencies = [ "libloading", ] [[package]] -name = "once_cell" -version = "1.21.4" +name = "nohash-hasher" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "proc-macro2" @@ -334,13 +373,12 @@ dependencies = [ name = "react_compiler_reactive_scopes" version = "0.1.0" dependencies = [ - "hmac", + "hmac-sha256", "indexmap", "react_compiler_ast", "react_compiler_diagnostics", "react_compiler_hir", "serde_json", - "sha2", ] [[package]] @@ -350,7 +388,6 @@ dependencies = [ "indexmap", "react_compiler_diagnostics", "react_compiler_hir", - "react_compiler_lowering", ] [[package]] @@ -379,33 +416,10 @@ dependencies = [ ] [[package]] -name = "regex" -version = "1.12.3" +name = "rustc-hash" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "same-file" @@ -475,27 +489,19 @@ dependencies = [ ] [[package]] -name = "sha2" -version = "0.10.9" +name = "similar" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "e6505efef05804732ed8a3f2d4f279429eb485bd69d5b0cc6b19cc02005cda16" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "bstr", ] [[package]] -name = "similar" -version = "2.7.0" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "syn" @@ -508,12 +514,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -526,12 +526,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" diff --git a/compiler/crates/react_compiler_ast/Cargo.toml b/compiler/crates/react_compiler_ast/Cargo.toml index a5b504b0de80..49208522fb5e 100644 --- a/compiler/crates/react_compiler_ast/Cargo.toml +++ b/compiler/crates/react_compiler_ast/Cargo.toml @@ -12,4 +12,4 @@ indexmap = { version = "2", features = ["serde"] } [dev-dependencies] walkdir = "2" -similar = "2" +similar = "3" diff --git a/compiler/crates/react_compiler_reactive_scopes/Cargo.toml b/compiler/crates/react_compiler_reactive_scopes/Cargo.toml index 5ec27f2a5f33..83ce70f37f53 100644 --- a/compiler/crates/react_compiler_reactive_scopes/Cargo.toml +++ b/compiler/crates/react_compiler_reactive_scopes/Cargo.toml @@ -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" diff --git a/compiler/crates/react_compiler_reactive_scopes/src/codegen_reactive_function.rs b/compiler/crates/react_compiler_reactive_scopes/src/codegen_reactive_function.rs index 55264109fe4d..98aa3935868e 100644 --- a/compiler/crates/react_compiler_reactive_scopes/src/codegen_reactive_function.rs +++ b/compiler/crates/react_compiler_reactive_scopes/src/codegen_reactive_function.rs @@ -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, @@ -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; - // 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 { @@ -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, @@ -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() { diff --git a/compiler/crates/react_compiler_ssa/Cargo.toml b/compiler/crates/react_compiler_ssa/Cargo.toml index 3334d93d026d..f0b0f08be0ee 100644 --- a/compiler/crates/react_compiler_ssa/Cargo.toml +++ b/compiler/crates/react_compiler_ssa/Cargo.toml @@ -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" diff --git a/compiler/packages/babel-plugin-react-compiler-rust/native/Cargo.toml b/compiler/packages/babel-plugin-react-compiler-rust/native/Cargo.toml index 8af091ca97ba..568787480ade 100644 --- a/compiler/packages/babel-plugin-react-compiler-rust/native/Cargo.toml +++ b/compiler/packages/babel-plugin-react-compiler-rust/native/Cargo.toml @@ -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"