Skip to content

fix: Option<T> generates Optional<T> fields instead of PlutusData (#573)#573

Open
matiwinnetou wants to merge 1 commit intomasterfrom
feat/blueprint-batch-9
Open

fix: Option<T> generates Optional<T> fields instead of PlutusData (#573)#573
matiwinnetou wants to merge 1 commit intomasterfrom
feat/blueprint-batch-9

Conversation

@matiwinnetou
Copy link
Contributor

@matiwinnetou matiwinnetou commented Feb 11, 2026

Problem:
Blueprint fields referencing Option definitions generated untyped PlutusData
instead of type-safe Optional, requiring manual casting and losing compile-time
type safety. This affected both Aiken v1.0.26 (Option$T) and v1.1.21+ (Option)
syntax, causing SundaeSwap V2/V3 blueprints to have opaque PlutusData fields where
typed Optional was expected.

Root Cause:
PlutusBlueprintLoader only detected Option$ prefix (line 119), missing Option<
syntax from newer Aiken versions. When Option definitions weren't marked with
dataType=option, they bypassed OptionDataTypeProcessor and fell back to PlutusData.

Solution:

  1. PlutusBlueprintLoader.java - Extended Option detection to both syntaxes:

    • Added definitions resolution loop (lines 59-70) to set dataType=option
    • Extended check from ref.startsWith("Option$") to include "Option<"
    • Added null check for blueprints without definitions
    • Now routes all Option types through OptionDataTypeProcessor → Optional
  2. BlueprintAnnotationProcessor.java - Smart generic type handling:

    • Added resolveDefinitionKeyForClassGeneration() to distinguish built-in
      containers (List, Option, Tuple) from domain types (Interval, ValidityRange)
    • Built-in containers return null (skip generation), domain types return base
      type for typed class generation
    • Supports both $ and <> syntax across Aiken v1.0.x and v1.1.x
    • Changed methods to package-private for testing (no reflection needed)
  3. FieldSpecProcessor.java - Removed buggy workaround:

    • Reverted resolveBuiltInContainerTypeName() added by previous attempt
    • Simplified PlutusData fallback for truly unresolvable types
    • Now relies on PlutusBlueprintLoader fix for proper type routing

Test Coverage:

  1. PlutusBlueprintLoaderTest.java - New OptionTypeResolution nested class:

    • Tests Option$ and Option< syntax both set dataType=option
    • Verifies Some/None anyOf variants preserved after resolution
    • Tests null definitions handling (no NPE)
    • Validates SundaeSwap V2 and V3 blueprint loading
  2. BlueprintAnnotationProcessorTest.java - Consolidated comprehensive tests:

    • 56 tests organized in @nested classes (no reflection, uses package-private)
    • IsBuiltInGenericContainerTests (9 built-ins, 7 domain types, 3 edge cases)
    • ResolveDefinitionKeyForClassGenerationTests (30+ tests covering $ and <>)
    • GenericTypeSkipTests (integration tests for compilation success)
    • Deleted old reflection-based test files
  3. FieldSpecProcessorTest.java - Fixed misleading comments:

    • Clarified resolveClassNameFromRef() tests class generation, not field typing
    • Corrected "PlutusData fallback" comments to explain Option → Optional
    • Added notes distinguishing skip generation from field type resolution
  4. SundaeSwapV2Test.java & SundaeSwapV3Test.java - PlutusData assertions:

    • Added verifyNoOpaqueTypes() to detect illegitimate PlutusData fields
    • Filters out legitimate uses (extension/data fields per CIP-57)
    • Ensures Option generates Optional, not PlutusData

Impact:

Before (❌):

private PlutusData stakeCredential;  // Option<Credential> → untyped
private PlutusData validityRange;    // Interval$Int → untyped

After (✅):

private Optional<StakeCredential> stakeCredential;  // Option<Credential> → typed
private Interval<Integer> validityRange;                     // Interval$Int → typed

Real-World Validation:

  • SundaeSwap V2 (Aiken v1.0.26): Interval$Int generates typed Interval class
  • SundaeSwap V3 (Aiken v1.1.21): Option generates Optional
  • All tests pass: ./gradlew :annotation-processor:test :plutus:test

Breaking Changes: None - existing blueprints gain better type safety

@matiwinnetou matiwinnetou changed the base branch from master to feat/blueprint-batch-8 February 11, 2026 16:35
@matiwinnetou matiwinnetou force-pushed the feat/blueprint-batch-9 branch from 65cacb5 to 10d451c Compare February 11, 2026 16:36
@matiwinnetou matiwinnetou changed the title feat: unified type-safe blueprint processing for generic instantiations fix: unified type-safe blueprint processing for generic instantiations Feb 11, 2026
@matiwinnetou matiwinnetou force-pushed the feat/blueprint-batch-9 branch 2 times, most recently from 93e31c5 to 9db45a6 Compare February 12, 2026 08:37
@matiwinnetou matiwinnetou changed the base branch from feat/blueprint-batch-8 to master February 12, 2026 08:38
@matiwinnetou
Copy link
Contributor Author

Q: Why is @plutus/src/main/java/com/bloxbean/cardano/client/plutus/blueprint/PlutusBlueprintLoader.java handling only Option$ and Option< type, what about other container types like: List<, Pair<, etc etc

A:

  Answer: Why Only Option Needs Special Handling

  Option IS actually special - and for a very good reason!

  The Key Difference

  Option definitions in CIP-57 JSON:
  "Option$Int": {
    "title": "Option",
    // ❌ NO root-level "dataType" field
    "anyOf": [
      {"title": "Some", "dataType": "constructor", ...},
      {"title": "None", "dataType": "constructor", ...}
    ]
  }

  List/Map/Pair/Tuple definitions in CIP-57 JSON:
  "List<Int>": {
    "dataType": "list",  // ✅ HAS root-level dataType
    "items": [...]
  }

  "Pair$Int_String": {
    "dataType": "#pair",  // ✅ HAS root-level dataType
    "left": {...}, "right": {...}
  }

  Why This Matters

  Without dataType=option being explicitly set:
  1. Option fields fall through to PlutusData fallback (type-unsafe)
  2. Can't route to OptionDataTypeProcessor → SchemaTypeResolver.resolveOptionType()
  3. Won't generate Optional → stays as opaque PlutusData

  Other containers don't need this because they already have dataType in the JSON:
  - Jackson deserializes them with the dataType already set
  - They route directly to their respective processors
  - No manual intervention needed

  Real-World Verification

  From SundaeSwap V3 blueprint (sundaeswap_aiken_v1_1_21_42babe5.json):

  Option types - NO root dataType:
  - Option<List<aiken/crypto/VerificationKey>>
  - Option<cardano/address/StakeCredential>
  - Option<sundae/multisig/MultisigScript>

  Container types - HAS root dataType:
  - List<Int> → "dataType": "list"
  - Tuple<<Int,Option<...>,Int>> → "dataType": "list"
  - Map definitions → "dataType": "map"

  Conclusion

  Your code is correct as-is! Only Option needs this special handling because:

  1. ✅ CIP-57 spec: Aiken compiler doesn't put dataType at root for Option
  2. ✅ Other containers: List, Pair, Map, Tuple all have explicit dataType in JSON
  3. ✅ Type safety: Without this, Option → PlutusData (the bug we fixed)
  4. ✅ Well-tested: 6 tests validate both Option$ and Option< syntax

@matiwinnetou matiwinnetou force-pushed the feat/blueprint-batch-9 branch 3 times, most recently from e1f0b42 to 1e9cf24 Compare February 12, 2026 13:12
matiwinnetou pushed a commit that referenced this pull request Feb 12, 2026
**Problem:**
Blueprint fields referencing Option<T> definitions generated untyped PlutusData
instead of type-safe Optional<T>, requiring manual casting and losing compile-time
type safety. This affected both Aiken v1.0.26 (Option$T) and v1.1.21+ (Option<T>)
syntax, causing SundaeSwap V2/V3 blueprints to have opaque PlutusData fields where
typed Optional<T> was expected.

**Root Cause:**
PlutusBlueprintLoader only detected Option$ prefix (line 119), missing Option<
syntax from newer Aiken versions. When Option<T> definitions weren't marked with
dataType=option, they bypassed OptionDataTypeProcessor and fell back to PlutusData.

**Solution:**

1. **PlutusBlueprintLoader.java** - Extended Option detection to both syntaxes:
   - Added definitions resolution loop (lines 59-70) to set dataType=option
   - Extended check from `ref.startsWith("Option$")` to include `"Option<"`
   - Added null check for blueprints without definitions
   - Now routes all Option types through OptionDataTypeProcessor → Optional<T>

2. **BlueprintAnnotationProcessor.java** - Smart generic type handling:
   - Added resolveDefinitionKeyForClassGeneration() to distinguish built-in
     containers (List, Option, Tuple) from domain types (Interval, ValidityRange)
   - Built-in containers return null (skip generation), domain types return base
     type for typed class generation
   - Supports both $ and <> syntax across Aiken v1.0.x and v1.1.x
   - Changed methods to package-private for testing (no reflection needed)

3. **FieldSpecProcessor.java** - Removed buggy workaround:
   - Reverted resolveBuiltInContainerTypeName() added by previous attempt
   - Simplified PlutusData fallback for truly unresolvable types
   - Now relies on PlutusBlueprintLoader fix for proper type routing

**Test Coverage:**

1. **PlutusBlueprintLoaderTest.java** - New OptionTypeResolution nested class:
   - Tests Option$ and Option< syntax both set dataType=option
   - Verifies Some/None anyOf variants preserved after resolution
   - Tests null definitions handling (no NPE)
   - Validates SundaeSwap V2 and V3 blueprint loading

2. **BlueprintAnnotationProcessorTest.java** - Consolidated comprehensive tests:
   - 56 tests organized in @nested classes (no reflection, uses package-private)
   - IsBuiltInGenericContainerTests (9 built-ins, 7 domain types, 3 edge cases)
   - ResolveDefinitionKeyForClassGenerationTests (30+ tests covering $ and <>)
   - GenericTypeSkipTests (integration tests for compilation success)
   - Deleted old reflection-based test files

3. **FieldSpecProcessorTest.java** - Fixed misleading comments:
   - Clarified resolveClassNameFromRef() tests class generation, not field typing
   - Corrected "PlutusData fallback" comments to explain Option<T> → Optional<T>
   - Added notes distinguishing skip generation from field type resolution

4. **SundaeSwapV2Test.java & SundaeSwapV3Test.java** - PlutusData assertions:
   - Added verifyNoOpaqueTypes() to detect illegitimate PlutusData fields
   - Filters out legitimate uses (extension/data fields per CIP-57)
   - Ensures Option<T> generates Optional<T>, not PlutusData

**Impact:**

Before (❌):
```java
private PlutusData stakeCredential;  // Option<Credential> → untyped
private PlutusData validityRange;    // Interval$Int → untyped
```

After (✅):
```java
private Optional<StakeCredential> stakeCredential;  // Option<Credential> → typed
private Interval validityRange;                     // Interval$Int → typed
```

**Real-World Validation:**
- SundaeSwap V2 (Aiken v1.0.26): Interval$Int generates typed Interval class
- SundaeSwap V3 (Aiken v1.1.21): Option<Credential> generates Optional<Credential>
- All tests pass: ./gradlew :annotation-processor:test :plutus:test

**Breaking Changes:** None - existing blueprints gain better type safety
@matiwinnetou matiwinnetou force-pushed the feat/blueprint-batch-9 branch from 1e9cf24 to 32f02a5 Compare February 12, 2026 13:22
@matiwinnetou matiwinnetou changed the title fix: unified type-safe blueprint processing for generic instantiations fix: Option<T> generates Optional<T> fields instead of PlutusData (#573) Feb 12, 2026
@matiwinnetou matiwinnetou marked this pull request as ready for review February 12, 2026 13:47
**Problem:**
Blueprint fields referencing Option<T> definitions generated untyped PlutusData
instead of type-safe Optional<T>, requiring manual casting and losing compile-time
type safety. This affected both Aiken v1.0.26 (Option$T) and v1.1.21+ (Option<T>)
syntax, causing SundaeSwap V2/V3 blueprints to have opaque PlutusData fields where
typed Optional<T> was expected.

**Root Cause:**
PlutusBlueprintLoader only detected Option$ prefix (line 119), missing Option<
syntax from newer Aiken versions. When Option<T> definitions weren't marked with
dataType=option, they bypassed OptionDataTypeProcessor and fell back to PlutusData.

**Solution:**

1. **PlutusBlueprintLoader.java** - Extended Option detection to both syntaxes:
   - Added definitions resolution loop (lines 59-70) to set dataType=option
   - Extended check from `ref.startsWith("Option$")` to include `"Option<"`
   - Added null check for blueprints without definitions
   - Now routes all Option types through OptionDataTypeProcessor → Optional<T>

2. **BlueprintAnnotationProcessor.java** - Smart generic type handling:
   - Added resolveDefinitionKeyForClassGeneration() to distinguish built-in
     containers (List, Option, Tuple) from domain types (Interval, ValidityRange)
   - Built-in containers return null (skip generation), domain types return base
     type for typed class generation
   - Supports both $ and <> syntax across Aiken v1.0.x and v1.1.x
   - Changed methods to package-private for testing (no reflection needed)

3. **FieldSpecProcessor.java** - Removed buggy workaround:
   - Reverted resolveBuiltInContainerTypeName() added by previous attempt
   - Simplified PlutusData fallback for truly unresolvable types
   - Now relies on PlutusBlueprintLoader fix for proper type routing

**Test Coverage:**

1. **PlutusBlueprintLoaderTest.java** - New OptionTypeResolution nested class:
   - Tests Option$ and Option< syntax both set dataType=option
   - Verifies Some/None anyOf variants preserved after resolution
   - Tests null definitions handling (no NPE)
   - Validates SundaeSwap V2 and V3 blueprint loading

2. **BlueprintAnnotationProcessorTest.java** - Consolidated comprehensive tests:
   - 56 tests organized in @nested classes (no reflection, uses package-private)
   - IsBuiltInGenericContainerTests (9 built-ins, 7 domain types, 3 edge cases)
   - ResolveDefinitionKeyForClassGenerationTests (30+ tests covering $ and <>)
   - GenericTypeSkipTests (integration tests for compilation success)
   - Deleted old reflection-based test files

3. **FieldSpecProcessorTest.java** - Fixed misleading comments:
   - Clarified resolveClassNameFromRef() tests class generation, not field typing
   - Corrected "PlutusData fallback" comments to explain Option<T> → Optional<T>
   - Added notes distinguishing skip generation from field type resolution

4. **SundaeSwapV2Test.java & SundaeSwapV3Test.java** - PlutusData assertions:
   - Added verifyNoOpaqueTypes() to detect illegitimate PlutusData fields
   - Filters out legitimate uses (extension/data fields per CIP-57)
   - Ensures Option<T> generates Optional<T>, not PlutusData

**Impact:**

Before (❌):
```java
private PlutusData stakeCredential;  // Option<Credential> → untyped
private PlutusData validityRange;    // Interval$Int → untyped
```

After (✅):
```java
private Optional<StakeCredential> stakeCredential;  // Option<Credential> → typed
private Interval validityRange;                     // Interval$Int → typed
```

**Real-World Validation:**
- SundaeSwap V2 (Aiken v1.0.26): Interval$Int generates typed Interval class
- SundaeSwap V3 (Aiken v1.1.21): Option<Credential> generates Optional<Credential>
- All tests pass: ./gradlew :annotation-processor:test :plutus:test

**Breaking Changes:** None - existing blueprints gain better type safety
@matiwinnetou matiwinnetou force-pushed the feat/blueprint-batch-9 branch from 32f02a5 to 9bd405b Compare February 13, 2026 10:56
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant