✨ Read this must-read post from Hacker News 📖
📂 **Category**:
📌 **What You’ll Learn**:
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 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. Route them through the
same Statement::Unknown carrier the Babel path uses: the forward
converter builds the Babel-compatible raw node (field names and nesting
match @babel/parser; importKind/isExport carried; qualified
TSQualifiedName refs supported), and the reverse converter rebuilds
real swc module declarations at the ModuleItem layer, deserializing
sub-fields through the typed AST and reusing the existing expression
conversion. Malformed raw shapes, including invalid importKind or
isExport types, return None and hit the loud Stmt-level tripwire
rather than degrading.
swc_ecma_codegen v24 misprints TsNamespaceExportDecl as the
TsExportAssignment shape ("export = Foo"); the bug is also on swc
master and a genuine export assignment prints byte-identical text, so
the affected lines cannot be identified from output text alone. The
ts_namespace_export_fixup module anchors the rewrite on the source map
the emitter records: candidates are positions within the declaration's
span (the v24 emitter records only the identifier's span.lo, pinned by
a test), filtered by content verification because compiler-generated
imports carry synthetic spans that collide with a first-statement
declaration's span.lo. Unlocatable declarations panic; the silent
alternative emits a semantically different statement. A guard test
asserts raw swc still misprints the node, so an swc upgrade that fixes
the bug fails the test and prompts deleting the module.
The fixtures drop their todo- prefix (ts-import-equals-declaration,
ts-export-assignment, ts-namespace-export-declaration) now that both
Babel and SWC are green; the SproutTodoFilter entry follows the rename
(sprout's TS->CJS transform still cannot process export-as-namespace).
OXC remains deferred and documented.
Verified: react_compiler_swc 51 tests green including round trips for
import type / export import forms, UMD pairs, decoy template-literal
and block-comment lines, and the synthetic-span collision; workspace 78
green; all three fixtures pass scoped e2e on both variants including
events parity; full swc 1783/1798 and babel 1791/1798 with failure
lists identical to the documented baselines.
Port note: re-measured on this branch (lauren/port-rust-research,
fork corpus 1799 -> 1802 with the three fixtures): cargo workspace 84
green; yarn snap 1803/1803; full e2e babel 1792/1802 and swc 1786/1802
with failure sets byte-identical to the pre-stack baseline at
2aa3f0c (10 babel / 16 swc, none involving the ts-* fixtures).
TODO.md's status snapshot is updated to these measured numbers and the
inherited SWC triage section is marked historical like its siblings.
CompileError logger events carry plain-object details (normalized for
Rust/TS logger parity), but the playground pushed event.detail straight
into CompilerError.details. Printing the error then crashed with
"detail.printErrorMessage is not a function", leaving the Next.js error
overlay up so Monaco never loaded and the source-syntax-error e2e test
timed out on every retry. Reconstruct CompilerDiagnostic /
CompilerErrorDetail instances at the logEvent boundary so downstream
consumers keep their method-based API.
JSON.stringify maps NaN/Infinity/-Infinity to "null" in the debug HIR
printer, so the TS side of the rust-port comparison harness printed
Primitive 💬 for folded 0/0 while the Rust printer emits
the faithful NaN/Infinity spellings (format_js_number). The lossy form
also can't be told apart from a genuine null primitive. Print non-finite
numbers via String(); fixes codegen-nan-infinity-as-identifiers at the
ConstantPropagation frontier in the e2e comparison (final codegen
already matched).
…cope
TS resolves a function declaration's id via Babel's getBinding starting
at the function's OWN scope, so a body-level local that shadows the
function's name receives the store while outer references resolve to the
hoisted binding. The resulting split store/load chain is a known TS
quirk these fixtures memorialize (uninitialized-value invariant). The
port had switched to node-id resolution (30f1ba7), which stored into
the outer binding and made the fixtures compile successfully, diverging
from TS in names, identifier numbering, and locs.
Restore the Babel-faithful scope walk as the primary resolution, with
rename-awareness (Babel scope.rename re-keys bindings, which is how
function-decl-shadowed-by-inner-const still resolves outward) and the
previous node-based path as fallback for backends with split
function-body scopes. The StoreContext/StoreLocal decision now derives
from the same resolved binding.
With parity restored, both compilers error identically on the three
fixtures, so they return to their pre-30f1ba7fd9 error.-prefixed names
(reverting the 4245fe2 renames) with snapshots regenerated from the
now-converged output.
…lerates context places
Two halves of one parity fix:
The rust babel plugin's scope serialization registered lowercase JSX tag
names matching a local binding only in the deprecated position-keyed
referenceToBinding map; route them through mapRef so they also land in
refNodeIdToBinding, the map the Rust side actually consumes. The Rust
capture analysis now sees e.g. resolving to a local const
colgroup, matching TS gatherCapturedContext.
That capture surfaces a latent bug shared by BOTH compilers: a
function's context places capture a binding, not a value, but EnterSSA
treated an entry-reaching context place as use-before-define and threw
the [hoisting] todo when the variable was declared later in the block
(const colgroup = useMemo(() => ...) self-capture). Unmark
context-place identifiers from the unknown set in both EnterSSA
implementations; genuine reads-before-define inside the function body
re-mark via LoadLocal and still bail (error.dont-hoist-inline-reference
unchanged). The spurious context entry is pruned by AnalyseFunctions +
DCE, so final output is unchanged.
Fixes todo-jsx-intrinsic-tag-matches-local-binding on the e2e comparison
(both pass-by-pass and codegen), where Rust previously missed the
capture entirely.
Four TS_SKIP_FIXTURES entries are now vacuous: the three shadowed-own-
name fixtures error identically in both compilers (and were renamed back
to error.-prefixed names), and todo-jsx-intrinsic-tag-matches-local-
binding now compiles identically in both. The remaining entries are
genuinely divergent fixtures.
…semantics
Four root causes, all in how the port approximated Babel/TS traversal:
1. Hoisting guard over-applied. The is_binding_in_block_direct_statements
guard compensates for scope_bindings_with_children pulling in child
block scopes, but it also rejected the block's OWN scope bindings.
Babel attributes catch params and for-in/for-of head vars to the
block's scope without any direct declaring statement (probe: the
catch body's path.scope IS the CatchClause scope), and TS hoists
them into DeclareContext. Guard now applies only to child-scope
bindings. Fixes error.bug-context-variable-catch-in-lambda,
error.bug-invariant-local-or-context-references (both now converge
on TS's consistently-local-or-context invariant) and round2_loc_diff
(a 10-file round-2 pattern).
2. Babel's scope crawl misses references its own isReferencedIdentifier
classifies as referenced (observed: Flow FunctionTypeParam names
resolving to value bindings are absent from binding.referencePaths
under @babel/core's traverse, present under a bare re-traverse). TS's
FindContextIdentifiers and hoisting re-traverse and so DO see them.
scope.ts now maps crawl-missed referenced identifiers; the identifier
loc index tracks in_type_annotation for them; gather_captured_context
excludes annotation refs, matching TS's gatherCapturedContext which
skips TypeAnnotation subtrees while FindContextIdentifiers does not.
Fixes error.todo-update-expression-context-variable-via-type-annotation
(StoreContext parity + the UpdateExpression-on-context todo) and
todo-hir_identifier_diff (a 20-file pattern: React.Node annotation
refs no longer captured into jest.mock factory contexts).
3. record_unsupported_lval recorded the TSAsExpression assignment-target
todo and continued, so Rust logged HIR for functions TS never lowered
(TS's handleAssignment default case throws immediately). It now
returns Err. Fixes error.todo-rust-as-expression-assignment-target.
4. Hermes component-syntax desugar reuses source offsets, so a sibling
reference (the forwardRef argument naming the desugared inner
function) positionally aliases the function name it refers to and
fell inside the function's capture range. Skip references whose
offset equals their binding's declaration offset; impossible in real
source, exact for desugared aliases. Fixes
error.todo-round2_id_numbering (a 12-file round-2 pattern).
e2e comparison: Results 1801/1803, Code 1803/1803 (remaining two are
the parked fbt local-require and WTF-8 lone-surrogate items). Both snap
channels 1804/1804 with the companion fixture-rename commit.
…hots, skip list
With the hoisting parity fix, Rust errors identically to TS on the two
catch-param-captured-by-lambda fixtures, so they return to their
pre-4245fe23b9 error.bug- names with snapshots regenerated from the
now-converged output, and their TS_SKIP_FIXTURES entries are dropped
(three genuinely-divergent entries remain). Depends on the preceding
parity commit; snap --rust is 1804/1804 only with both applied.
…ed_names
has_local_binding() checked used_names, which is only populated as
identifiers are resolved during HIR lowering. JSX tag names bypass
normal identifier resolution, so when lowering , the fbt binding
from `const fbt = require('fbt')` might not be in used_names yet.
Switch to scope_info.find_binding_in_descendants(), which searches
Babel's complete scope data for any binding with the given name in the
compiled function's scope tree. This matches TS behavior where
resolveIdentifier uses scope.getBinding().
…shots
- Bump snap's hermes-parser dependency from ^0.28.0 to ^0.32.0 to get
enableExperimentalFlowMatchSyntax support for Flow match fixtures.
Update yarn.lock to resolve ^0.32.0 to 0.32.0 with correct integrity.
Yarn workspaces nests 0.32.0 in packages/snap/node_modules/ since
babel-plugin-syntax-hermes-parser pins 0.25.1 at the workspace root.
- Regenerate 6 match-expr/match-stmt fixture snapshots (now parse and compile)
- Update method-call-scope-merge-mutable-range-sync snapshot
- ts-namespace-export-declaration was already in SproutTodoFilter
Both yarn snap --rust and yarn snap: 1804/1804, 0 failures.
Verified: rm -rf node_modules && yarn install resolves hermes-parser
0.32.0 for snap, tests pass from clean state.
… version
Update eval output from `(kind: exception) licensedGeos.toSorted is not a
function` to the actual rendered HTML. The exception was an artifact of
system Node 16 which lacks Array.prototype.toSorted(); CI uses Node 20+
where toSorted() works and the component renders successfully.
…l fixture
toSorted() is unavailable on Node 16 (system default), causing the eval
to throw instead of rendering. Replace with [...licensedGeos].sort()
which works on all Node versions. The test exercises scope merging and
mutable range sync, not Array.prototype.toSorted specifically.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⚡ **What’s your take?**
Share your thoughts in the comments below!
#️⃣ **#compiler #Port #React #Compiler #Rust #josephsavona #Pull #Request #reactreact #GitHub**
🕒 **Posted on**: 1781087048
🌟 **Want more?** Click here for more info! 🌟
