| name | translate-from-shared-core |
| 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. |
| argument-hint | <commit-sha or range e.g. abc123 or abc123..def456> |
Translate sdk-shared-core (Rust) to sdk-java
You are translating Rust code changes from restatedev/sdk-shared-core into equivalent Java code in this repository.
Input
$ARGUMENTS contains one or more commit references from sdk-shared-core. These can be:
- A single commit SHA (e.g.,
abc123)
- Multiple commit SHAs separated by spaces (e.g.,
abc123 def456)
- A commit range (e.g.,
abc123..def456)
Step 1: Fetch the Rust diffs
For each commit or range, fetch the diff from GitHub:
- Single commit:
gh api repos/restatedev/sdk-shared-core/commits/<sha> --header "Accept: application/vnd.github.diff"
- Range:
gh api repos/restatedev/sdk-shared-core/compare/<base>...<head> --header "Accept: application/vnd.github.diff"
Also fetch the commit message(s) for context:
gh api repos/restatedev/sdk-shared-core/commits/<sha> --jq '.commit.message'
Step 2: Understand the changes
Before translating, analyze:
- What the Rust change does semantically (not just syntactically)
- Which Rust files/modules are affected
- What the commit message says about intent and motivation
Step 3: Architectural mapping between the two codebases
The primary mapping target is:
sdk-core/src/main/java/dev/restate/sdk/core/statemachine/
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.
Public interface: VM trait vs StateMachine interface
- Rust:
VM trait in src/lib.rs with sys_* methods (e.g., sys_state_get, sys_call, sys_run)
- Java:
StateMachine interface in StateMachine.java with plain method names (e.g., stateGet, call, run)
- Both return integer handles (
NotificationHandle in Rust, int in Java) for async operations
- Both have
doProgress() / do_progress() as the core async driver
- Both have
takeNotification() / take_notification() for retrieving results
State representation: enum variants vs class implementations
This is the biggest architectural difference.
Rust: States are variants of a single State enum in src/vm/mod.rs. Each variant carries its own data inline:
enum State {
WaitingStart,
WaitingReplayEntries { received_entries: u32, commands: VecDeque<RawMessage>, async_results: AsyncResultsState },
Replaying { commands: VecDeque<RawMessage>, run_state: RunState, async_results: AsyncResultsState },
Processing { processing_first_entry: bool, run_state: RunState, async_results: AsyncResultsState },
Closed,
}
Java: States are classes implementing a sealed State interface. Each state class contains its own data as fields:
WaitingStartState → WaitingReplayEntriesState → ReplayingState → ProcessingState → ClosedState
- The
State interface declares default methods that throw ProtocolException.badState() for unsupported operations
- Concrete state classes override the methods they support
Transitions: structs with pattern matching vs methods on state classes
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:
impl TransitionAndReturn<Context, PopJournalEntry<M>> for State {
fn transition_and_return(self, context: &mut Context, event) -> Result<(Self, Output), Error> {
match self {
State::Replaying { mut commands, run_state, async_results } => { ... }
State::Processing { ... } => { ... }
s => Err(s.as_unexpected_state(...))
}
}
}
Transitions live in separate modules: transitions/input.rs, transitions/journal.rs, transitions/async_results.rs, transitions/terminal.rs.
Java: Transitions are methods on the state classes themselves. Each state class implements the transitions it supports:
int processStateGetCommand(String key, StateContext ctx) { ... }
int processStateGetCommand(String key, StateContext ctx) { ... }
The StateMachineImpl delegates to the current state: this.stateContext.getCurrentState().processStateGetCommand(key, this.stateContext).
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).
Transition dispatch: do_transition vs StateHolder
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.
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.
Context: Context struct vs StateContext class
- Rust:
Context in src/vm/context.rs — holds start_info, journal, output, eager_state, input_is_closed
- Java:
StateContext in StateContext.java — holds StateHolder, Journal, EagerState, StartInfo, inputClosed, outputSubscriber
- Both serve the same purpose: mutable state that persists across transitions
Journal and async results
These are structurally very similar between both codebases:
Journal tracks commandIndex, notificationIndex, completionIndex, signalIndex
AsyncResultsState maps handles to NotificationIds, with toProcess queue and ready map
RunState tracks pending/executing run blocks
EagerState caches state values from StartMessage
File mapping
| Rust file | Java file |
|---|
src/lib.rs (VM trait) | StateMachine.java |
src/vm/mod.rs (CoreVM) | StateMachineImpl.java |
src/vm/context.rs (Context) | StateContext.java, Journal.java, EagerState.java, StartInfo.java |
src/vm/context.rs (AsyncResultsState) | AsyncResultsState.java |
src/vm/context.rs (RunState) | RunState.java |
src/vm/transitions/input.rs | Logic in WaitingStartState.java, WaitingReplayEntriesState.java |
src/vm/transitions/journal.rs | Methods across ReplayingState.java and ProcessingState.java |
src/vm/transitions/async_results.rs | Methods in ReplayingState.java, ProcessingState.java, AsyncResultsState.java |
src/vm/transitions/terminal.rs | hitError()/hitSuspended() methods on State interface |
src/vm/errors.rs | ProtocolException.java (factory methods, not separate error classes) |
src/error.rs (CommandMetadata, NotificationMetadata) | CommandMetadata.java (record); notification metadata is built as strings inline |
src/service_protocol/ | MessageDecoder.java, MessageEncoder.java, MessageType.java, ServiceProtocol.java |
Command processing patterns
Both codebases distinguish two kinds of commands:
- Non-completable (fire-and-forget): e.g.,
stateSet, stateClear — no handle returned
- Completable (returns a handle): e.g.,
stateGet, call, run — returns an int handle mapped to a NotificationId
In Rust, these are generic transitions like SysNonCompletableEntry<M> and SysCompletableEntry<M>.
In Java, these are processNonCompletableCommand() and processCompletableCommand() methods on the state classes.
Tests: builder VMTestCase vs 3-layer handler tests
Rust tests are low-level, directly exercising the VM:
VMTestCase::new()
.input(StartMessage { known_entries: 1, .. })
.input(input_entry_message(b"my-data"))
.run(|vm| {
let input = vm.sys_input().unwrap();
vm.sys_write_output(NonEmptyValue::Success(input), ...).unwrap();
vm.sys_end().unwrap();
});
Java tests use a 3-layer architecture:
- Base test suites (abstract classes like
StateTestSuite, CallTestSuite) define test scenarios as TestDefinition streams
- Concrete implementations (e.g.,
StateTest) extend suites with actual handler code using the SDK's context API
- Test executors (
MockRequestResponse, MockBidiStream) run each test in both buffered and streaming modes
Java test inputs are built with ProtoUtils helpers (startMessage(), inputCmd(), getLazyStateCmd(), etc.) and assertions use AssertUtils.
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.
Test translation details
When translating Rust VM tests to Java:
-
Identify the right test suite: Match the Rust test module to the Java abstract test suite:
src/tests/failures.rs (journal_mismatch) → StateMachineFailuresTestSuite
src/tests/async_result.rs → AsyncResultTestSuite
src/tests/run.rs → SideEffectTestSuite
src/tests/state.rs → StateTestSuite / EagerStateTestSuite
-
Add abstract method + test definition: Add the abstract handler method to the suite, then add test definitions using withInput(...) and assertion patterns like assertingOutput(containsOnly(errorMessage(...))) or expectingOutput(...).
-
Implement in both Java and Kotlin: The suite is extended in both:
sdk-core/src/test/java/dev/restate/sdk/core/javaapi/<TestName>.java
sdk-core/src/test/kotlin/dev/restate/sdk/core/kotlinapi/<TestName>.kt
-
Kotlin API differences:
- Must
import dev.restate.sdk.kotlin.* for reified extension functions (runAsync, runBlock, etc.)
ctx.runAsync<String>(name) { ... } (reified, not ctx.runAsync(name, String.class, ...))
ctx.awakeable(TestSerdes.STRING) (needs a serde, not String::class.java)
ctx.timer(0.milliseconds) (uses Kotlin Duration)
- Handler factories:
testDefinitionForService<Unit, String?>("Name") { ctx, _: Unit -> ... }
-
Cancel signal is always included: The HandlerContextImpl automatically appends CANCEL_HANDLE (handle=1, mapping to SignalId(1)) to every doProgress call. This matches Rust's CoreVM.do_progress which appends cancel_signal_handle. So in test assertions, the cancel signal notification ID will always be part of the awaited notifications.
-
ProtoUtils helpers: Use startMessage(n), inputCmd(), runCmd(completionId, name), suspensionMessage(completionIds...), etc. For messages without helpers (e.g., SleepCommandMessage, SleepCompletionNotificationMessage), build them directly with the protobuf builders.
Shared utilities
Util.awakeableIdStr(invocationId, signalId) — computes the awakeable ID string from invocation ID and signal ID. Used in both StateMachineImpl (for creating awakeables) and ReplayingState (for error messages).
StateMachineImpl.CANCEL_SIGNAL_ID — the signal ID for the built-in cancel signal (value: 1). Package-private, available via static import.
- Java 17 target — do NOT use switch pattern matching (
case Type t ->) in Java source; use instanceof chains instead.
Step 4: Apply the translation
- Read the affected Java files first before making changes
- Adapt the Rust change to Java's architecture (methods on state classes, not transition structs)
- If a Rust change doesn't apply to Java (borrow checker workarounds, Rust-specific memory management), skip it and note why
- Run a build check after translating:
./gradlew :sdk-core:compileJava
Step 5: Summary
After translating, provide:
- A summary of what was translated
- Any Rust changes that were skipped and why
- Any areas that need manual review or testing