Skip to content

Commit 5d01fac

Browse files
willemnealclaude
andcommitted
Resolve Error to user-defined UDT in client generation
Replace the spec-side `apply_error_udt_fallback` (which rewrote `Udt { name: "Error" }` → `ScSpecTypeDef::Error` when no error enum named `Error` was defined) with the inverse `apply_error_udt_override` (which rewrites `ScSpecTypeDef::Error` → `Udt { name: "Error" }` when the spec *does* define an error enum named `Error`). This keeps the on-the-wire spec format identical to what every release has produced: a contract function returning `Result<_, Error>` still serializes its error type as the built-in `ScSpecTypeDef::Error`. The override fires only at client generation, so already-deployed contracts that define an `Error` error enum get a generated client referencing that UDT instead of `soroban_sdk::Error` — without any redeploy. Tests updated accordingly: - `test_add_u64_spec_entries` now guards that the wasm spec still emits `ScSpecTypeDef::Error` (and `Udt { name: "MyError" }` for the differently-named enum). - `test_missing_error_udt_falls_back_to_sdk_error` and `test_error_udt_overrides_sdk_error` (renamed) build hand-crafted specs whose function returns `Result<_, ScSpecTypeDef::Error>`, asserting the override only fires when a matching UDT is present. Per review feedback on #1709: #1709 (comment) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 265e0ba commit 5d01fac

1 file changed

Lines changed: 50 additions & 47 deletions

File tree

soroban-spec-rust/src/lib.rs

Lines changed: 50 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use proc_macro2::TokenStream;
88
use quote::quote;
99
use sha2::{Digest, Sha256};
1010
use stellar_xdr::curr as stellar_xdr;
11-
use stellar_xdr::{ScSpecEntry, ScSpecTypeDef, ScSpecUdtUnionCaseV0};
11+
use stellar_xdr::{ScSpecEntry, ScSpecTypeDef, ScSpecTypeUdt, ScSpecUdtUnionCaseV0};
1212
use syn::Error;
1313

1414
use soroban_spec::read::{from_wasm, FromWasmError};
@@ -100,7 +100,7 @@ pub fn generate_without_file_with_options(
100100
specs: &[ScSpecEntry],
101101
opts: &GenerateOptions,
102102
) -> TokenStream {
103-
let specs = apply_error_udt_fallback(specs);
103+
let specs = apply_error_udt_override(specs);
104104
let specs: &[ScSpecEntry] = &specs;
105105

106106
let mut spec_fns = Vec::new();
@@ -152,40 +152,48 @@ pub fn generate_without_file_with_options(
152152
}
153153
}
154154

155-
/// The `#[contractimpl]` macro emits any type named `Error` as a UDT reference
156-
/// in the spec, so a contract that uses `soroban_sdk::Error` directly (rather
157-
/// than defining its own error enum) produces a spec with
158-
/// `Udt { name: "Error" }` but no matching error-enum entry. When that
159-
/// happens, rewrite those references to the built-in `Error` type so the
160-
/// generated client code references `soroban_sdk::Error`.
155+
/// The `#[contractimpl]` macro emits any type named `Error` in a contract's
156+
/// function signatures as the built-in `ScSpecTypeDef::Error` in the spec,
157+
/// regardless of whether the contract defined its own error enum named `Error`
158+
/// or used `soroban_sdk::Error` directly. To let clients of contracts that
159+
/// define their own `Error` enum see the user-defined type instead of
160+
/// `soroban_sdk::Error`, this pass rewrites every `ScSpecTypeDef::Error`
161+
/// reference in the spec to `Udt { name: "Error" }` whenever the spec also
162+
/// contains a `UdtErrorEnumV0` named `Error`.
163+
///
164+
/// This keeps the on-the-wire spec format unchanged (so already-deployed
165+
/// contracts benefit without redeployment) and shifts the resolution to the
166+
/// client generator.
161167
///
162168
/// Returns a borrowed slice when no rewrite is needed, otherwise a
163169
/// freshly-owned `Vec` with the rewrite applied.
164-
fn apply_error_udt_fallback(specs: &[ScSpecEntry]) -> Cow<'_, [ScSpecEntry]> {
170+
fn apply_error_udt_override(specs: &[ScSpecEntry]) -> Cow<'_, [ScSpecEntry]> {
165171
let has_error_udt = specs.iter().any(|e| {
166172
matches!(
167173
e,
168174
ScSpecEntry::UdtErrorEnumV0(err) if err.name.to_utf8_string_lossy() == "Error"
169175
)
170176
});
171177
if has_error_udt {
172-
Cow::Borrowed(specs)
173-
} else {
174178
let mut v = specs.to_vec();
175-
rewrite_missing_error_udt(&mut v);
179+
rewrite_error_to_udt(&mut v);
176180
Cow::Owned(v)
181+
} else {
182+
Cow::Borrowed(specs)
177183
}
178184
}
179185

180-
/// Rewrites every `ScSpecTypeDef::Udt { name: "Error" }` reference in the
181-
/// given entries to `ScSpecTypeDef::Error`. Called only when the spec has no
182-
/// user-defined error enum named `Error`, so any such reference must refer to
183-
/// the built-in `soroban_sdk::Error` type.
184-
fn rewrite_missing_error_udt(entries: &mut [ScSpecEntry]) {
186+
/// Rewrites every `ScSpecTypeDef::Error` reference in the given entries to
187+
/// `ScSpecTypeDef::Udt { name: "Error" }`. Called only when the spec contains
188+
/// a user-defined error enum named `Error`, so the UDT reference resolves to
189+
/// that enum during code generation.
190+
fn rewrite_error_to_udt(entries: &mut [ScSpecEntry]) {
185191
fn rewrite_ty(t: &mut ScSpecTypeDef) {
186192
match t {
187-
ScSpecTypeDef::Udt(u) if u.name.to_utf8_string_lossy() == "Error" => {
188-
*t = ScSpecTypeDef::Error;
193+
ScSpecTypeDef::Error => {
194+
*t = ScSpecTypeDef::Udt(ScSpecTypeUdt {
195+
name: "Error".try_into().unwrap(),
196+
});
189197
}
190198
ScSpecTypeDef::Option(o) => rewrite_ty(&mut o.value_type),
191199
ScSpecTypeDef::Result(r) => {
@@ -344,7 +352,11 @@ pub enum MyError {
344352
}
345353

346354
/// Test that shows the raw spec entries from the wasm.
347-
/// Verifies that both Error and MyError are correctly represented as UDTs.
355+
/// Verifies that the on-the-wire spec format is unchanged: a contract
356+
/// error enum named `Error` is still emitted as the built-in
357+
/// `ScSpecTypeDef::Error` in function signatures (the user-defined-vs-SDK
358+
/// disambiguation happens at client generation time, not here). A
359+
/// differently-named error enum (`MyError`) is emitted as a UDT reference.
348360
#[test]
349361
fn test_add_u64_spec_entries() {
350362
use super::ScSpecEntry;
@@ -371,16 +383,10 @@ pub enum MyError {
371383
matches!(r.ok_type.as_ref(), ScSpecTypeDef::U64),
372384
"ok_type should be U64"
373385
);
374-
let ScSpecTypeDef::Udt(u) = r.error_type.as_ref() else {
375-
panic!(
376-
"error_type should be a UDT for Error, got {:?}",
377-
r.error_type
378-
);
379-
};
380-
assert_eq!(
381-
u.name.to_utf8_string().unwrap(),
382-
"Error",
383-
"error_type should be Error UDT"
386+
assert!(
387+
matches!(r.error_type.as_ref(), ScSpecTypeDef::Error),
388+
"error_type should be the built-in Error in the wasm spec, got {:?}",
389+
r.error_type
384390
);
385391

386392
// Find the safe_add_two function spec
@@ -420,25 +426,23 @@ pub enum MyError {
420426
);
421427
}
422428

423-
/// If the spec references `Udt { name: "Error" }` but no error enum
424-
/// named `Error` is defined, the generator must fall back to
425-
/// `soroban_sdk::Error` so the generated client code still compiles.
426-
/// This covers the (rare) case where a contract uses `soroban_sdk::Error`
427-
/// directly as its Result error type instead of defining its own enum.
429+
/// When the spec references `ScSpecTypeDef::Error` and contains no error
430+
/// enum named `Error`, the generator must leave it as `soroban_sdk::Error`.
431+
/// This covers contracts that use `soroban_sdk::Error` directly as their
432+
/// Result error type, including every contract compiled before the
433+
/// error-enum override was introduced.
428434
#[test]
429435
fn test_missing_error_udt_falls_back_to_sdk_error() {
430436
use super::ScSpecEntry;
431-
use stellar_xdr::curr::{ScSpecFunctionV0, ScSpecTypeDef, ScSpecTypeResult, ScSpecTypeUdt};
437+
use stellar_xdr::curr::{ScSpecFunctionV0, ScSpecTypeDef, ScSpecTypeResult};
432438

433439
let func = ScSpecFunctionV0 {
434440
doc: "".try_into().unwrap(),
435441
name: "safe_add".try_into().unwrap(),
436442
inputs: [].try_into().unwrap(),
437443
outputs: [ScSpecTypeDef::Result(Box::new(ScSpecTypeResult {
438444
ok_type: Box::new(ScSpecTypeDef::U64),
439-
error_type: Box::new(ScSpecTypeDef::Udt(ScSpecTypeUdt {
440-
name: "Error".try_into().unwrap(),
441-
})),
445+
error_type: Box::new(ScSpecTypeDef::Error),
442446
}))]
443447
.try_into()
444448
.unwrap(),
@@ -459,14 +463,15 @@ pub trait Contract {
459463
);
460464
}
461465

462-
/// When the spec *does* contain a user-defined `Error` error enum, the
463-
/// UDT reference must be preserved and not rewritten to `soroban_sdk::Error`.
466+
/// When the spec contains a user-defined `Error` error enum, every
467+
/// `ScSpecTypeDef::Error` reference in the spec must be rewritten to
468+
/// reference that UDT instead of `soroban_sdk::Error`.
464469
#[test]
465-
fn test_error_udt_preserved_when_defined() {
470+
fn test_error_udt_overrides_sdk_error() {
466471
use super::ScSpecEntry;
467472
use stellar_xdr::curr::{
468-
ScSpecFunctionV0, ScSpecTypeDef, ScSpecTypeResult, ScSpecTypeUdt,
469-
ScSpecUdtErrorEnumCaseV0, ScSpecUdtErrorEnumV0,
473+
ScSpecFunctionV0, ScSpecTypeDef, ScSpecTypeResult, ScSpecUdtErrorEnumCaseV0,
474+
ScSpecUdtErrorEnumV0,
470475
};
471476

472477
let func = ScSpecFunctionV0 {
@@ -475,9 +480,7 @@ pub trait Contract {
475480
inputs: [].try_into().unwrap(),
476481
outputs: [ScSpecTypeDef::Result(Box::new(ScSpecTypeResult {
477482
ok_type: Box::new(ScSpecTypeDef::U64),
478-
error_type: Box::new(ScSpecTypeDef::Udt(ScSpecTypeUdt {
479-
name: "Error".try_into().unwrap(),
480-
})),
483+
error_type: Box::new(ScSpecTypeDef::Error),
481484
}))]
482485
.try_into()
483486
.unwrap(),

0 commit comments

Comments
 (0)