| 1 | # Rust Port: Architecture Guide |
| 2 | |
| 3 | Reference for key data structures, patterns, and constraints in the Rust compiler port. See `rust-port-research.md` for detailed per-pass analysis and `rust-port-notes.md` for the original design decisions. |
| 4 | |
| 5 | ## Arenas and ID Types |
| 6 | |
| 7 | All shared mutable data is stored in arenas on `Environment`, referenced by copyable ID types. This replaces JavaScript's shared object references. |
| 8 | |
| 9 | | Arena | ID Type | Stored On | Replaces | |
| 10 | |-------|---------|-----------|----------| |
| 11 | | `identifiers: Vec<Identifier>` | `IdentifierId` | `Environment` | Shared `Identifier` object references across `Place` values | |
| 12 | | `scopes: Vec<ReactiveScope>` | `ScopeId` | `Environment` | Shared `ReactiveScope` references across identifiers | |
| 13 | | `functions: Vec<HIRFunction>` | `FunctionId` | `Environment` | Inline `HIRFunction` on `FunctionExpression`/`ObjectMethod` | |
| 14 | | `types: Vec<Type>` | `TypeId` | `Environment` | Inline `Type` on `Identifier` | |
| 15 | |
| 16 | All ID types are `Copy + Clone + Hash + Eq + PartialEq` newtypes wrapping `u32`. |
| 17 | |
| 18 | ## Instructions and EvaluationOrder |
| 19 | |
| 20 | - `HirFunction.instructions: Vec<Instruction>` — flat instruction table |
| 21 | - `BasicBlock.instructions: Vec<InstructionId>` — indices into the table above |
| 22 | - The old TypeScript `InstructionId` is renamed to `EvaluationOrder` — it represents evaluation order and appears on both instructions and terminals |
| 23 | - The new `InstructionId` is an index into `HirFunction.instructions`, giving passes a single copyable ID to reference any instruction |
| 24 | |
| 25 | ## Place is Clone, MutableRange is on Identifier/Scope |
| 26 | |
| 27 | `Place` stores an `IdentifierId` (not a shared reference), making it small and cheap to clone. Mutation of `mutable_range` goes through the identifier arena: |
| 28 | |
| 29 | ```rust |
| 30 | env.identifiers[place.identifier].mutable_range.end = new_end; |
| 31 | ``` |
| 32 | |
| 33 | After `InferReactiveScopeVariables`, an identifier's effective mutable range is its scope's range. Downstream passes access this through the scope arena: |
| 34 | |
| 35 | ```rust |
| 36 | let range = match env.identifiers[id].scope { |
| 37 | Some(scope_id) => env.scopes[scope_id].range, |
| 38 | None => env.identifiers[id].mutable_range, |
| 39 | }; |
| 40 | ``` |
| 41 | |
| 42 | ## Function Arena and FunctionId |
| 43 | |
| 44 | `FunctionExpression` and `ObjectMethod` instruction values store a `FunctionId` instead of an inline `HIRFunction`. Inner functions are accessed via the arena: |
| 45 | |
| 46 | ```rust |
| 47 | let inner = &env.functions[function_id]; // read |
| 48 | let inner = &mut env.functions[function_id]; // write |
| 49 | ``` |
| 50 | |
| 51 | This makes `CreateFunction` aliasing effects store `FunctionId`, and function signature caches key by `FunctionId`. |
| 52 | |
| 53 | ## AliasingEffect |
| 54 | |
| 55 | Effects own cloned `Place` values (cheap since `Place` contains `IdentifierId`). Key variants: |
| 56 | |
| 57 | - `Apply` — clones the args `Vec<PlaceOrSpreadOrHole>` from the instruction value |
| 58 | - `CreateFunction` — stores `FunctionId` (not the `FunctionExpression` itself), plus cloned `captures: Vec<Place>` |
| 59 | |
| 60 | Effect interning uses content hashing. The interned `EffectId` serves as both dedup key and allocation-site identity for abstract interpretation in `InferMutationAliasingEffects`. |
| 61 | |
| 62 | ## Environment: Separate from HirFunction |
| 63 | |
| 64 | `HirFunction` does not store `env`. Passes receive `env: &mut Environment` as a separate parameter. Fields are flat (no sub-structs) to allow precise sliced borrows: |
| 65 | |
| 66 | ```rust |
| 67 | // Simultaneous borrow of different fields is fine: |
| 68 | let id = &env.identifiers[some_id]; |
| 69 | let scope = &env.scopes[some_scope_id]; |
| 70 | ``` |
| 71 | |
| 72 | ## Ordered Maps |
| 73 | |
| 74 | Use `IndexMap`/`IndexSet` (from the `indexmap` crate) wherever the TypeScript uses `Map`/`Set` and iteration order matters. The primary case is `HIR.blocks: IndexMap<BlockId, BasicBlock>` which maintains reverse postorder. |
| 75 | |
| 76 | ## Side Maps |
| 77 | |
| 78 | Side maps fall into four categories: |
| 79 | |
| 80 | 1. **ID-only maps** — `HashMap<IdType, T>` / `HashSet<IdType>`. No borrow issues. Most passes use this. |
| 81 | 2. **Reference-identity maps** — TypeScript `Map<Identifier, T>` becomes `HashMap<IdentifierId, T>`. Similarly `DisjointSet<Identifier>` becomes `DisjointSet<IdentifierId>`, `DisjointSet<ReactiveScope>` becomes `DisjointSet<ScopeId>`. |
| 82 | 3. **Instruction/value reference maps** — Store `InstructionId` or `FunctionId` instead of references. Access the actual data through the instruction table or function arena when needed. |
| 83 | 4. **Scope reference sets with mutation** — Store `ScopeId` in sets. Mutate through the arena: `env.scopes[scope_id].range.start = new_start`. |
| 84 | |
| 85 | When a pass needs to both iterate over data and mutate the HIR, use two-phase collect/apply: collect IDs or updates into a `Vec`, then apply mutations in a second loop. |
| 86 | |
| 87 | ## Error Handling |
| 88 | |
| 89 | | TypeScript Pattern | Rust Approach | |
| 90 | |---|---| |
| 91 | | Non-null assertion (`!`) | `.unwrap()` (panic) | |
| 92 | | `CompilerError.invariant()`, `CompilerError.throwTodo()`, `throw ...` | Return `Err(CompilerDiagnostic)` via `Result` | |
| 93 | | `env.recordError()` or `pushDiagnostic()` with an invariant error | Return `Err(CompilerDiagnostic)` | |
| 94 | | `env.recordError()` or `pushDiagnostic()` with a NON invariant error | Keep as-is — accumulate on `Environment` | |
| 95 | |
| 96 | Preserve full error details: reason, description, location, suggestions, category. |
| 97 | |
| 98 | ## JS→Rust Boundary |
| 99 | |
| 100 | The JS side serializes the Babel AST and Babel's scope information (scope tree, bindings, reference-to-binding map) to Rust. Keep this serialization thin: only send the core data structures that Babel already computed during parsing. Any derived analysis — identifier source locations, JSX classification, captured variables, etc. — should be computed on the Rust side by walking the AST. See `scope.ts`. |
| 101 | |
| 102 | ## Pipeline and Pass Structure |
| 103 | |
| 104 | ```rust |
| 105 | fn compile(ast: Ast, scope: Scope, env: &mut Environment) |
| 106 | -> Result<CompileResult, CompilerDiagnostic> |
| 107 | { |
| 108 | let mut hir = lower(ast, scope, env)?; |
| 109 | some_pass(&mut hir, env)?; |
| 110 | // ... |
| 111 | let ast = codegen(...)?; |
| 112 | |
| 113 | if env.has_errors() { |
| 114 | Ok(CompileResult::Failure(env.take_errors())) |
| 115 | } else { |
| 116 | Ok(CompileResult::Success(ast)) |
| 117 | } |
| 118 | } |
| 119 | ``` |
| 120 | |
| 121 | Pass signatures follow these patterns: |
| 122 | |
| 123 | ```rust |
| 124 | // Most passes: mutable HIR + mutable environment |
| 125 | fn pass(func: &mut HirFunction, env: &mut Environment) -> Result<(), CompilerDiagnostic>; |
| 126 | |
| 127 | // Passes that don't need env |
| 128 | fn pass(func: &mut HirFunction); |
| 129 | |
| 130 | // Validation passes: read-only HIR, env for error recording |
| 131 | fn validate(func: &HirFunction, env: &mut Environment) -> Result<(), CompilerDiagnostic>; |
| 132 | ``` |
| 133 | |
| 134 | Use `?` to propagate errors that would have thrown or short-circuited in TypeScript. Non-fatal errors are accumulated on `env` and checked at the end via `env.has_errors()`. |
| 135 | |
| 136 | ## Structural Similarity to TypeScript |
| 137 | |
| 138 | Target ~85-95% structural correspondence. A developer should be able to view TypeScript and Rust side-by-side and trace the logic. The ported code should preserve: |
| 139 | |
| 140 | - **Same high-level data flow** through the code. Only deviate where strictly necessary due to data model differences (arenas, borrow checker workarounds, etc.). |
| 141 | - **Same grouping of types, functions, and "classes" (structs with methods) into files.** A TypeScript file maps to a Rust file with the same logical contents. |
| 142 | - **Similar filenames, type names, and identifier names**, adjusted for Rust naming conventions (`camelCase` -> `snake_case` for functions/variables, `PascalCase` preserved for types). |
| 143 | - **Crate structure**: The monolithic `babel-plugin-react-compiler` package is split into crates, roughly 1:1 by top-level folder (e.g., `src/HIR/` -> a crate, `src/Inference/` -> a crate, etc.). We split the lowering logic (BuildHIR and HIRBuilder) into react_compiler_lowering bc of its complexity. |
| 144 | |
| 145 | Key mechanical translations: |
| 146 | |
| 147 | | TypeScript | Rust | |
| 148 | |---|---| |
| 149 | | `switch (value.kind)` | `match &value` (exhaustive) | |
| 150 | | `Map<Identifier, T>` | `HashMap<IdentifierId, T>` | |
| 151 | | `for...of` with `Set.delete()` | `set.retain(\|x\| ...)` | |
| 152 | | `instr.value = { kind: 'X', ... }` | `std::mem::replace` + reconstruct | |
| 153 | | `{ ...place, effect: Effect.Read }` | `Place { effect: Effect::Read, ..place.clone() }` | |
| 154 | | `array.filter(x => ...)` | `vec.retain(\|x\| ...)` | |
| 155 | | `identifier.mutableRange.end = x` | `env.identifiers[id].mutable_range.end = x` | |
| 156 | | Builder closures setting outer variables | Return values from closures | |