[compiler] Port React Compiler to Rust by josephsavona · Pull Request #36173 · react/react
Pangram verdict · v3.3
We believe that this document is primarily human-written, with some AI-generated content detected
AI likelihood · overall
MixedArticle text · 1,408 words · 6 segments analyzed
…nodes
The Babel parser emits `"predicate": null` on function-like nodes (FunctionDeclaration, ArrowFunctionExpression, FunctionExpression, ObjectMethod, DeclareFunction) to signal "no Flow `%checks` predicate". Plain `Option` deserialization collapses both "absent" and "explicit null" into `None`, and `skip_serializing_if = "Option::is_none"` then drops the field on the way back out, so round-tripped JSON loses the field and byte-equivalent comparison fails.
Apply the existing `crate::common::nullable_value` deserializer to all five predicate fields (ported from pr-36173 commit 8b880f2, adapted: this branch already had plain `predicate` fields on all function-like nodes, so only the attribute form changes). The helper distinguishes:
- absent -> None (skip on serialize) - null -> Some(Value::Null) (round-trips as `"predicate": null`) - object -> Some(Value::Object(_)) (round-trips a populated predicate)
This makes the predicate-related round_trip known_failures entries unnecessary; remove them and keep only lone-surrogate-string-values (serde_json rejects lone surrogates at parse time, unrelated to predicate). Each removal verified against the full fixtures dir:
- component-in-object-method-body.flow: passes - error.todo-round2_severity_diff: passes - error.todo-update-expression-context-variable-via-type-annotation: passes - error.todo-hoist-type-alias-before-declaration: entry was already dead (fixture renamed to todo-hoist-type-alias-before-declaration, the contains() check no longer matched) and the renamed fixture was failing both round_trip and scope_resolution_rename at baseline; passes now.
Test plan: - bash compiler/scripts/test-babel-ast.sh: round_trip: 1782/1782 (baseline: 1778/1779, 1 failure) scope info round-trip: 1783/1783; rename: 1767/1767 (12 skipped) (baseline rename: 1 of 1767 failed on the same fixture) - cargo test --workspace: all green (33 suites, 0 failures)
Replace the "rename to error.todo-*" approach for the six Flow `match` fixtures with actual support, ported from pr-36173 commits 0dc7f2e and d8aae6b. npm hermes-parser CAN parse match syntax: it requires hermes-parser >= 0.28 plus the `enableExperimentalFlowMatchSyntax` parser option (snap pinned 0.25.1 and never passed the flag; 0.26 carries an incompatible draft grammar).
- Un-rename the six match fixtures from error.todo-* back to their original names, restoring the pre-rename inputs (hermes-canonical formatting) and their real compiled snapshots: match-expr-captured-var.flow.js match-expr-jsx-spread.flow.js match-expr-multi-gen-bindings.flow.js match-expr-outlined-jsx.flow.js match-expression-with-tuple-and-early-return.js match-stmt-self-ref-const.flow.js All six pass against the checked-in snapshots with no regeneration (yarn snap -p 'match-*': 6 Tests, 6 Passed, 0 Failed). - snap: hermes-parser ^0.28.0 in snap/package.json (+compiler yarn.lock; babel-plugin-syntax-hermes-parser stays 0.25.1 since nothing in snap's pipeline re-parses match syntax), pass enableExperimentalFlowMatchSyntax in parseInput, add the option to the hermes-parser module type in types.d.ts.
compiler/yarn.lock also gains the previously-missing typescript entry for the babel-plugin-react-compiler-rust workspace (pre-existing drift that any yarn install regenerates). - method-call-scope-merge-mutable-range-sync: rename tr/td to div/span (valid DOM nesting). The bare <tr> in sprout's container triggered a validateDOMNesting warning in exactly one of the two evaluations (warning dedup shares process state), so logs differed while rendered output was identical; this was the 1 failing test at baseline. Compiled shape unchanged; snapshot diff is tag literals only. - prettier: format the match fixtures for real instead of ignoring them. Remove the .prettierignore match-* globs (stale since the error.todo-* rename: they no longer matched any file, which is why the prettier check failed at baseline with 6 parse errors). Add a .prettierrc.js override scoped to the match-* fixture paths using prettier-plugin-hermes-parser (root devDep at ^0.32.0 + root yarn.lock), whose parser handles the experimental syntax. - TS_SKIP_FIXTURES: no entries removed; the current list (9 entries) contains no match-related fixtures. The match fixtures were handled entirely by the error.todo-* rename, which this commit reverts. The snapshots-reflect-Rust-output semantics and skip machinery are kept as-is.
Test plan: - yarn snap: 1800 Tests, 1800 Passed, 0 Failed (baseline: 1800 Tests, 1799 Passed, 1 Failed) - yarn snap -p 'match-*' -v: 6 Tests, 6 Passed, 0 Failed - node scripts/prettier/index.js: exit 0 (baseline: exit 1 with parse errors on the six match fixtures) - bash compiler/scripts/test-babel-ast.sh: parse 1771/1799 (28 parse errors, unchanged: @babel/parser cannot parse match syntax so those fixtures
remain excluded from the round-trip corpus, exactly as before the un-rename); round_trip 1782/1782; scope info 1783/1783; rename 1767/1767 (12 skipped); exit 0
Pin how TSImportEqualsDeclaration, TSExportAssignment, and TSNamespaceExportDeclaration must behave: the statement is preserved in output and the file's functions still compile, as the TS reference already does. The three frontends share the broken symptom today via three different root causes: the Babel/NAPI path throws "Failed to parse AST JSON: unknown variant ..." (the typed AST's tagged enums have no catch-all) and fails the whole file; the SWC converter explicitly rewrites the statements to EmptyStatement, erasing them from output with no error and no event; OXC todo!()-panics in its converter (deferred).
The fixtures use the bare todo- prefix rather than error.*: snap asserts error.* fixtures throw on the TS side, and these compile cleanly there. All three function bodies allocate so the compiled snapshots visibly memoize; combined with the e2e events comparison, a degenerate whole-file bailout cannot pass them.
Known-red until the fix slices land: Babel and SWC e2e on these three fixtures, and test-babel-ast.sh (both round_trip and scope_resolution_rename deserialize the same fixture JSON). TS-side snap is green. SproutTodoFilter skips only the namespace fixture: export as namespace is .d.ts-shaped and sprout's evaluator transform cannot process it; the other two transform to CJS and evaluate fine.
Babel can emit statement kinds the typed AST does not model (the todo-ts-* fixtures pin three TS module-interop forms). Deserialization previously failed the whole file on the first such node, while the TS reference compiles the file and leaves the statement alone.
Statement gains a final #[serde(untagged)] Unknown(UnknownStatement) variant carrying the complete raw node.
Deserialization is hand-written and dispatches modeled `type` tags through a KnownStatement helper so a malformed modeled node still errors with its precise field-level message instead of degrading to Unknown; only genuinely unmodeled tags take the catch-all. The TS reference reaches its equivalent default case only via assertExhaustive (Babel's closed types), so it crashes; here unmodeled syntax is reachable by construction and degrades instead: top-level statements are preserved verbatim through re-serialization, and function-body occurrences record the standard UnsupportedSyntax bailout with an UnsupportedNode instruction carrying the raw node. A known_statements! macro is the single source for the dispatch enum, its From mapping, and the tag list, so those three cannot drift; a variant added to Statement but not the macro is the one remaining silent gap, documented on the variant.
UnknownStatement caches BaseNode for position helpers; the scoped with_raw_mut mutator refreshes the cache and rejects mutations that strip `type`, so the two views cannot desync. Program-level analyses treat Unknown explicitly: the gating reference-before-declaration scan walks the raw node for identifier references (an `export = X` does reference X), and the prefilter and return-analysis arms are deliberately inert. SWC/OXC reverse converters emit a deliberate runtime tripwire (a throw in generated code) for the arms that are unreachable until the SWC forward conversion stops rewriting these statements to EmptyStatement in the next slice.
Deserialization now materializes a serde_json::Value per statement before typed parsing. The cost is one move-based tree rebuild per nesting level at a one-time boundary; the previous derive also buffered every node through serde's internal Content to read the tag, so the delta is allocation shape, not asymptotics.
Verified: ast unit tests including malformed/edge cases, a lowering integration test pinning the function-body bailout, round_trip green on the three fixtures, scoped and full Babel e2e green on all three with events parity, cargo test --workspace green.
The scope-resolution half of test-babel-ast.sh is green on this stack's base and remains red corpus-wide on the pr-36173 tip, whose node-ID migration removed position-based keying while babel-ast-to-json.mjs still emits offset-based scope JSON; that generator gap needs its own fix before this stack rebases onto the tip. rust-port-0001-babel-ast.md's no-catch-all policy is amended to document Statement as the deliberate exception.
Port adaptation for this branch's UnsupportedNode codegen fix (0957b55), which discriminated statement-vs-expression original_node by attempting a Statement deserialization. With the tolerant deserializer that attempt succeeds for every tagged object, which would silently emit expression nodes as raw statements and orphan their lvalue temporaries — regressing the ~10 fixtures that commit fixed. The codegen site now discriminates explicitly (codegen_unsupported_original_node): modeled statement tags parse typed and a parse failure is an invariant, not a degrade; tags that parse as Expression or PatternLike (both strict enums, no catch-all) flow through expression codegen unchanged, preserving the lvalue binding and the pattern placeholder fallback; only genuinely unmodeled tags — producible solely by the unknown-statement lowering bailout, i.e. from statement position — degrade to Statement::Unknown and are emitted verbatim, matching TS codegen's 'return node'. is_known_statement_type is now exposed (pub) from the known_statements! macro for this, and unit tests pin the dispatch (modeled statement tag, malformed modeled tag, expression tag, pattern tag, unknown tag).
…tend
The SWC converter rewrote TSImportEqualsDeclaration, TSExportAssignment, and TSNamespaceExportDeclaration to EmptyStatement, silently deleting them from output with no error and no event.