Skip to content

Commit e56def0

Browse files
Merge pull request #215 from IntersectMBO/feat/blueprint-codegen-recursive
feat(blueprint): recursive type schema codegen
2 parents 2b66b46 + 2d07804 commit e56def0

8 files changed

Lines changed: 1163 additions & 195 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@evolution-sdk/evolution": patch
3+
---
4+
5+
Blueprint codegen now supports recursive type schemas — Plutus types that reference themselves
6+
directly or through an intermediate type (e.g. `MultiSig` containing a `List<MultiSig>` field).
7+
Cyclic references are emitted as typed `Schema.suspend` thunks where the encoded type `I` is
8+
inferred by recursively walking the blueprint definition graph rather than hardcoded to `Data.Constr`:
9+
`list``readonly ItemEncoded[]`, `map``globalThis.Map<Data.Data, Data.Data>`,
10+
`bytes``Uint8Array`, `integer``bigint`, `constructor` and union → `Data.Constr`,
11+
`$ref` followed transitively up to depth 10. The previous hardcoded `Data.Constr` caused a
12+
TypeScript invariance error for any recursive field referencing a list type.
13+
14+
Several other codegen correctness and API improvements ship in the same release:
15+
16+
- **Namespace emission ordering** — the group-by-namespace emitter is replaced by a streaming emitter
17+
that walks a global topological sort and opens/closes namespace blocks on demand. TypeScript namespace
18+
merging handles split declarations transparently. This fixes cases where a type was emitted before
19+
its cross-namespace dependency (e.g. `Option.OfStakeCredential` appearing before `Cardano.Address.StakeCredential`).
20+
21+
- **Cyclic type emit pattern** — cyclic types now emit a `export type X = ...` / `export const X = ...`
22+
pair with no outer `Schema.suspend` wrapper and no `as` cast. Only the inner field references that
23+
close the cycle use typed thunks: `Schema.suspend((): Schema.Schema<T, I> => T)`.
24+
25+
- **`unionStyle` config**`CodegenConfig` gains `unionStyle: "Variant" | "Struct" | "TaggedStruct"`
26+
in place of the removed `forceVariant` and `useSuspend` fields. `Struct` emits
27+
`TSchema.Struct({ Tag: TSchema.Struct({...}, { flatFields: true }) }, { flatInUnion: true })`,
28+
`TaggedStruct` emits `TSchema.TaggedStruct("Tag", {...}, { flatInUnion: true })`,
29+
and `Variant` emits `TSchema.Variant({ Tag: {...} })`.
30+
31+
- **Import hygiene** — generated files emit `import { Schema } from "@evolution-sdk/evolution"`
32+
only when cyclic types are present, rather than always importing from `effect` directly.
33+
`CodegenConfig.imports.effect` is replaced by `imports.schema`.
34+

docs/app/tools/blueprint-codegen/blueprint-codegen.tsx

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function BlueprintCodegen() {
1212
const [error, setError] = useState<string | null>(null)
1313
const [optionStyle, setOptionStyle] = useState<"NullOr" | "UndefinedOr" | "Union">("UndefinedOr")
1414
const [moduleStrategy, setModuleStrategy] = useState<"flat" | "namespaced">("namespaced")
15-
const [forceVariant, setForceVariant] = useState(true)
15+
const [unionStyle, setUnionStyle] = useState<"Variant" | "Struct" | "TaggedStruct">("Variant")
1616

1717
const generateTypes = async () => {
1818
setError(null)
@@ -33,8 +33,7 @@ export function BlueprintCodegen() {
3333
const config = createCodegenConfig({
3434
optionStyle,
3535
moduleStrategy,
36-
forceVariant,
37-
useSuspend: false,
36+
unionStyle,
3837
useRelativeRefs: true,
3938
emptyConstructorStyle: "Literal"
4039
})
@@ -75,7 +74,8 @@ export function BlueprintCodegen() {
7574

7675
const loadSample = async () => {
7776
try {
78-
const response = await fetch("/evolution-sdk/sample-blueprint.json")
77+
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""
78+
const response = await fetch(`${basePath}/sample-blueprint.json`)
7979
if (!response.ok) {
8080
throw new Error("Failed to load sample blueprint")
8181
}
@@ -139,21 +139,19 @@ export function BlueprintCodegen() {
139139
</div>
140140

141141
<div className="space-y-2">
142-
<label htmlFor="force-variant" className="text-sm font-medium leading-none">
143-
Force Variant
142+
<label htmlFor="union-style" className="text-sm font-medium leading-none">
143+
Union Style
144144
</label>
145-
<div className="flex items-center h-10">
146-
<input
147-
id="force-variant"
148-
type="checkbox"
149-
checked={forceVariant}
150-
onChange={(e) => setForceVariant(e.target.checked)}
151-
className="h-4 w-4 rounded border-input"
152-
/>
153-
<label htmlFor="force-variant" className="ml-2 text-sm text-muted-foreground">
154-
Use Variant for all unions
155-
</label>
156-
</div>
145+
<select
146+
id="union-style"
147+
value={unionStyle}
148+
onChange={(e) => setUnionStyle(e.target.value as any)}
149+
className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
150+
>
151+
<option value="Variant">Variant</option>
152+
<option value="Struct">Struct (verbose)</option>
153+
<option value="TaggedStruct">TaggedStruct</option>
154+
</select>
157155
</div>
158156
</div>
159157

0 commit comments

Comments
 (0)