|
| 1 | +--- |
| 2 | +name: translate-from-shared-core |
| 3 | +description: Translate Rust changes from restatedev/sdk-shared-core into equivalent Java code in this repo. Use when the user mentions translating, porting, or syncing commits from sdk-shared-core. |
| 4 | +argument-hint: <commit-sha or range e.g. abc123 or abc123..def456> |
| 5 | +--- |
| 6 | + |
| 7 | +# Translate sdk-shared-core (Rust) to sdk-java |
| 8 | + |
| 9 | +You are translating Rust code changes from [restatedev/sdk-shared-core](https://github.com/restatedev/sdk-shared-core) into equivalent Java code in this repository. |
| 10 | + |
| 11 | +## Input |
| 12 | + |
| 13 | +`$ARGUMENTS` contains one or more commit references from sdk-shared-core. These can be: |
| 14 | +- A single commit SHA (e.g., `abc123`) |
| 15 | +- Multiple commit SHAs separated by spaces (e.g., `abc123 def456`) |
| 16 | +- A commit range (e.g., `abc123..def456`) |
| 17 | + |
| 18 | +## Step 1: Fetch the Rust diffs |
| 19 | + |
| 20 | +For each commit or range, fetch the diff from GitHub: |
| 21 | + |
| 22 | +- Single commit: `gh api repos/restatedev/sdk-shared-core/commits/<sha> --header "Accept: application/vnd.github.diff"` |
| 23 | +- Range: `gh api repos/restatedev/sdk-shared-core/compare/<base>...<head> --header "Accept: application/vnd.github.diff"` |
| 24 | + |
| 25 | +Also fetch the commit message(s) for context: |
| 26 | +- `gh api repos/restatedev/sdk-shared-core/commits/<sha> --jq '.commit.message'` |
| 27 | + |
| 28 | +## Step 2: Understand the changes |
| 29 | + |
| 30 | +Before translating, analyze: |
| 31 | +1. What the Rust change does semantically (not just syntactically) |
| 32 | +2. Which Rust files/modules are affected |
| 33 | +3. What the commit message says about intent and motivation |
| 34 | + |
| 35 | +## Step 3: Architectural mapping between the two codebases |
| 36 | + |
| 37 | +The primary mapping target is: |
| 38 | +``` |
| 39 | +sdk-core/src/main/java/dev/restate/sdk/core/statemachine/ |
| 40 | +``` |
| 41 | + |
| 42 | +The two codebases implement the same state machine but with fundamentally different architectural patterns. Read the existing Java code before making changes. Don't blindly transliterate — adapt to the Java architecture while preserving semantics. |
| 43 | + |
| 44 | +### Public interface: VM trait vs StateMachine interface |
| 45 | + |
| 46 | +- **Rust**: `VM` trait in `src/lib.rs` with `sys_*` methods (e.g., `sys_state_get`, `sys_call`, `sys_run`) |
| 47 | +- **Java**: `StateMachine` interface in `StateMachine.java` with plain method names (e.g., `stateGet`, `call`, `run`) |
| 48 | +- Both return integer handles (`NotificationHandle` in Rust, `int` in Java) for async operations |
| 49 | +- Both have `doProgress()` / `do_progress()` as the core async driver |
| 50 | +- Both have `takeNotification()` / `take_notification()` for retrieving results |
| 51 | + |
| 52 | +### State representation: enum variants vs class implementations |
| 53 | + |
| 54 | +This is the **biggest architectural difference**. |
| 55 | + |
| 56 | +**Rust**: States are variants of a single `State` enum in `src/vm/mod.rs`. Each variant carries its own data inline: |
| 57 | +```rust |
| 58 | +enum State { |
| 59 | + WaitingStart, |
| 60 | + WaitingReplayEntries { received_entries: u32, commands: VecDeque<RawMessage>, async_results: AsyncResultsState }, |
| 61 | + Replaying { commands: VecDeque<RawMessage>, run_state: RunState, async_results: AsyncResultsState }, |
| 62 | + Processing { processing_first_entry: bool, run_state: RunState, async_results: AsyncResultsState }, |
| 63 | + Closed, |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +**Java**: States are classes implementing a sealed `State` interface. Each state class contains its own data as fields: |
| 68 | +- `WaitingStartState` → `WaitingReplayEntriesState` → `ReplayingState` → `ProcessingState` → `ClosedState` |
| 69 | +- The `State` interface declares default methods that throw `ProtocolException.badState()` for unsupported operations |
| 70 | +- Concrete state classes override the methods they support |
| 71 | + |
| 72 | +### Transitions: structs with pattern matching vs methods on state classes |
| 73 | + |
| 74 | +**Rust**: Transitions are **structs** (e.g., `NewMessage`, `SysGetState`, `DoProgress`) that implement `Transition<Context, Event>` or `TransitionAndReturn<Context, Event>` for `State`. Inside each impl, you **pattern match on the current state**: |
| 75 | +```rust |
| 76 | +impl TransitionAndReturn<Context, PopJournalEntry<M>> for State { |
| 77 | + fn transition_and_return(self, context: &mut Context, event) -> Result<(Self, Output), Error> { |
| 78 | + match self { |
| 79 | + State::Replaying { mut commands, run_state, async_results } => { ... } |
| 80 | + State::Processing { ... } => { ... } |
| 81 | + s => Err(s.as_unexpected_state(...)) |
| 82 | + } |
| 83 | + } |
| 84 | +} |
| 85 | +``` |
| 86 | +Transitions live in separate modules: `transitions/input.rs`, `transitions/journal.rs`, `transitions/async_results.rs`, `transitions/terminal.rs`. |
| 87 | + |
| 88 | +**Java**: Transitions are **methods on the state classes themselves**. Each state class implements the transitions it supports: |
| 89 | +```java |
| 90 | +// In ReplayingState.java |
| 91 | +int processStateGetCommand(String key, StateContext ctx) { ... } |
| 92 | + |
| 93 | +// In ProcessingState.java |
| 94 | +int processStateGetCommand(String key, StateContext ctx) { ... } |
| 95 | +``` |
| 96 | +The `StateMachineImpl` delegates to the current state: `this.stateContext.getCurrentState().processStateGetCommand(key, this.stateContext)`. |
| 97 | + |
| 98 | +**Key implication**: When a Rust commit adds or modifies a transition struct, in Java you need to add or modify the corresponding method across multiple state classes (typically `ReplayingState` and `ProcessingState`). |
| 99 | + |
| 100 | +### Transition dispatch: do_transition vs StateHolder |
| 101 | + |
| 102 | +**Rust**: `CoreVM.do_transition(event)` uses `mem::replace` to extract the current state, calls the transition, and stores the new state. Errors automatically send an error message and close output. |
| 103 | + |
| 104 | +**Java**: `StateHolder.transition(newState)` simply swaps the current state reference. Error handling is done explicitly in state methods. `StateContext` acts as the central hub holding `StateHolder`, `Journal`, `EagerState`, etc. |
| 105 | + |
| 106 | +### Context: Context struct vs StateContext class |
| 107 | + |
| 108 | +- **Rust**: `Context` in `src/vm/context.rs` — holds `start_info`, `journal`, `output`, `eager_state`, `input_is_closed` |
| 109 | +- **Java**: `StateContext` in `StateContext.java` — holds `StateHolder`, `Journal`, `EagerState`, `StartInfo`, `inputClosed`, `outputSubscriber` |
| 110 | +- Both serve the same purpose: mutable state that persists across transitions |
| 111 | + |
| 112 | +### Journal and async results |
| 113 | + |
| 114 | +These are structurally very similar between both codebases: |
| 115 | +- `Journal` tracks `commandIndex`, `notificationIndex`, `completionIndex`, `signalIndex` |
| 116 | +- `AsyncResultsState` maps handles to `NotificationId`s, with `toProcess` queue and `ready` map |
| 117 | +- `RunState` tracks pending/executing run blocks |
| 118 | +- `EagerState` caches state values from StartMessage |
| 119 | + |
| 120 | +### File mapping |
| 121 | + |
| 122 | +| Rust file | Java file | |
| 123 | +|-----------|-----------| |
| 124 | +| `src/lib.rs` (VM trait) | `StateMachine.java` | |
| 125 | +| `src/vm/mod.rs` (CoreVM) | `StateMachineImpl.java` | |
| 126 | +| `src/vm/context.rs` (Context) | `StateContext.java`, `Journal.java`, `EagerState.java`, `StartInfo.java` | |
| 127 | +| `src/vm/context.rs` (AsyncResultsState) | `AsyncResultsState.java` | |
| 128 | +| `src/vm/context.rs` (RunState) | `RunState.java` | |
| 129 | +| `src/vm/transitions/input.rs` | Logic in `WaitingStartState.java`, `WaitingReplayEntriesState.java` | |
| 130 | +| `src/vm/transitions/journal.rs` | Methods across `ReplayingState.java` and `ProcessingState.java` | |
| 131 | +| `src/vm/transitions/async_results.rs` | Methods in `ReplayingState.java`, `ProcessingState.java`, `AsyncResultsState.java` | |
| 132 | +| `src/vm/transitions/terminal.rs` | `hitError()`/`hitSuspended()` methods on `State` interface | |
| 133 | +| `src/vm/errors.rs` | `ProtocolException.java` | |
| 134 | +| `src/service_protocol/` | `MessageDecoder.java`, `MessageEncoder.java`, `MessageType.java`, `ServiceProtocol.java` | |
| 135 | + |
| 136 | +### Command processing patterns |
| 137 | + |
| 138 | +Both codebases distinguish two kinds of commands: |
| 139 | +- **Non-completable** (fire-and-forget): e.g., `stateSet`, `stateClear` — no handle returned |
| 140 | +- **Completable** (returns a handle): e.g., `stateGet`, `call`, `run` — returns an int handle mapped to a NotificationId |
| 141 | + |
| 142 | +In **Rust**, these are generic transitions like `SysNonCompletableEntry<M>` and `SysCompletableEntry<M>`. |
| 143 | +In **Java**, these are `processNonCompletableCommand()` and `processCompletableCommand()` methods on the state classes. |
| 144 | + |
| 145 | +### Tests: builder VMTestCase vs 3-layer handler tests |
| 146 | + |
| 147 | +**Rust** tests are low-level, directly exercising the VM: |
| 148 | +```rust |
| 149 | +VMTestCase::new() |
| 150 | + .input(StartMessage { known_entries: 1, .. }) |
| 151 | + .input(input_entry_message(b"my-data")) |
| 152 | + .run(|vm| { |
| 153 | + let input = vm.sys_input().unwrap(); |
| 154 | + vm.sys_write_output(NonEmptyValue::Success(input), ...).unwrap(); |
| 155 | + vm.sys_end().unwrap(); |
| 156 | + }); |
| 157 | +``` |
| 158 | + |
| 159 | +**Java** tests use a 3-layer architecture: |
| 160 | +1. **Base test suites** (abstract classes like `StateTestSuite`, `CallTestSuite`) define test scenarios as `TestDefinition` streams |
| 161 | +2. **Concrete implementations** (e.g., `StateTest`) extend suites with actual handler code using the SDK's context API |
| 162 | +3. **Test executors** (`MockRequestResponse`, `MockBidiStream`) run each test in both buffered and streaming modes |
| 163 | + |
| 164 | +Java test inputs are built with `ProtoUtils` helpers (`startMessage()`, `inputCmd()`, `getLazyStateCmd()`, etc.) and assertions use `AssertUtils`. |
| 165 | + |
| 166 | +**Key implication**: When a Rust commit adds a new VM-level test, in Java you typically need to add a handler-level test in the appropriate test suite, not a direct state machine test. |
| 167 | + |
| 168 | +## Step 4: Apply the translation |
| 169 | + |
| 170 | +1. **Read the affected Java files first** before making changes |
| 171 | +2. Adapt the Rust change to Java's architecture (methods on state classes, not transition structs) |
| 172 | +3. If a Rust change doesn't apply to Java (borrow checker workarounds, Rust-specific memory management), skip it and note why |
| 173 | +4. **Run a build check** after translating: `./gradlew :sdk-core:compileJava` |
| 174 | + |
| 175 | +## Step 5: Summary |
| 176 | + |
| 177 | +After translating, provide: |
| 178 | +1. A summary of what was translated |
| 179 | +2. Any Rust changes that were skipped and why |
| 180 | +3. Any areas that need manual review or testing |
0 commit comments