main
md 156 lines 7.96 KB
Rendered Raw
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 |