Skip to content

feat(desugar): data types that refer to each other, as nested recursive types - #440

Merged
hyperpolymath merged 1 commit into
mainfrom
claude/e2e-language-pass-ow72tj
Nov 10, 2026
Merged

hyperpolymath merged 1 commit into
mainfrom
claude/e2e-language-pass-ow72tj

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Summary

data types that refer to each other were refused by name (#438). They are now accepted, behind the flag --unstable-core as each recursive type is.

data Tree = Leaf | Node(I32, Forest)

data Forest = FNil | FCons(Tree, Forest)

The core has no new former, and the checker and the 2 backends do not change. Only the type expansion of the desugarer changes.

How

The types of a cycle become nested mu types (Bekic's construction), in the order of their names. A data type that is entered hides, for its body, each data type in progress that comes after it: in that body, such a type is not a variable, and it is expanded again. For Forest before Tree:

Forest = mu F. () + ((() + (I32 * F)) * F)
Tree   = () + (I32 * Forest)        (not folded)

The reason for this order rule: with iso-recursive types, a type is not equal to its unfolding. So the unfolding of each type must have the other types in it as they are. With the rule it does, for a cycle of each form and each number of types. A field of a constructor then has the type that its declaration names, and no fold or unfold is missing or in excess.

2 more changes follow from the nesting:

  • A data type is folded when its own expansion refers to it. A type whose type is the recursive type of another data type (data B = MkB(A)) is not folded.
  • Equality of 2 recursive types gives the 2 bound variables a new name when their names differ: a body can have a recursive type in it that binds the name of the other variable.

Limit, with a named error

A dense cycle makes the nested types grow fast.

Cycle Bodies in the largest type
2 types, each refers to the 2 7
3 types, each refers to the 3 64
4 types, each refers to the 4 3613
5 types, each refers to the 5 8 million
a ring of 32 types 127
5 types as in a syntax tree (Expr, Stmt, Decl, Pat, Type) fewer than 400

A type of more than 10000 bodies, or of more than 256 nested named types, is refused: "the type T4 is too large ...". Without the limit, the dense cycle of 5 does not end, and a ring of 700 types overflows the stack. The depth limit also applies to data types that do not refer to each other: 700 nested data types overflowed the stack before this change, and are now the named error.

A recursive type of n variables (as the rec groups of WasmGC) removes the size limit. It is a change of the core language, so it is a decision for the owner and not part of this pull request. The plan document records this next to the D7 ratification.

Verification

  • cargo test --workspace --no-fail-fast: 638 passed, 0 failed.
  • cargo fmt --all --check and cargo clippy --workspace --all-targets: clean.
  • scripts/status-gate.sh: 635 all-target tests (1 more than before).
  • scripts/probe-battery.sh: 51 SAME, 7 SKIP (the SKIP rows need the wasmtime CLI).
  • New test data_types_that_refer_to_each_other_run, each program as wasm and in the interpreter:
    • Tree and Forest, Even and Odd, a cycle where no type refers to itself, type parameters at I64, a linear component;
    • a cycle of 3 types where each one refers to the 2 others, in each of the 6 orders of its names (each order is another nesting);
    • refused: a value of a linear type of a cycle used 2 times (2 programs), a Forest as a Tree, a Tree as a Forest.
  • a_recursive_form_with_no_lowering_is_refused_by_name: the 2 limits (the dense cycle of 5, a ring of 300).
  • a_recursive_data_type_crosses_a_module: 2 types that refer to each other, used from another module.

Generated by Claude Code

…ve types

`data Tree = Leaf | Node(I32, Forest)` and `data Forest = FNil |
FCons(Tree, Forest)` were refused by name. They are now accepted, behind
the flag `--unstable-core` as each recursive type is.

The core has no new former: the types of a cycle become nested `mu`
types (Bekic's construction), in the order of their names. A `data`
type that is entered hides, for its body, each `data` type in progress
that comes after it: in that body, such a type is not a variable, and
it is expanded again. For Forest before Tree:

    Forest = mu F. () + ((() + (I32 * F)) * F)
    Tree   = () + (I32 * Forest)        (not folded)

So the unfolding of each type has the other types in it as they are,
and not types that are only equal to them after more unfolds. A field
of a constructor has the type that its declaration names. The checker
and the 2 backends do not change.

Equality of 2 recursive types now gives the 2 bound variables a new
name when their names differ: a body can have a recursive type in it
that binds the name of the other variable.

Limit, with a named error: a dense cycle makes the nested types grow
fast (4 types that each refer to each of the 4 need 3613 bodies, 5 need
8 million). A type of more than 10000 bodies, or of more than 256
nested named types, is refused: "the type ... is too large". A ring of
32 types needs 127 bodies, and a cycle of 5 types as in a syntax tree
needs fewer than 400. The same limit turns a stack overflow for 700
nested `data` types into the named error.

Tests: data_types_that_refer_to_each_other_run (11 programs as wasm and
in the interpreter, with a cycle of 3 types in each of the 6 orders of
its names, and 4 refused programs), the 2 limits, and a case across
modules. 638 tests pass; the all-target count is 635.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136P7ZmKrYLDC4WHi2yt11u
@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Caution

Review failed

The pull request is closed.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

Clippy (1.99.1)

Clippy execution failed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@sonarqubecloud

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath marked this pull request as ready for review November 10, 2026 20:41
@hyperpolymath
hyperpolymath merged commit 4c29a0a into main Nov 10, 2026
46 of 47 checks passed
@chatgpt-codex-connector

Copy link
Copy Markdown

This security review couldn’t start because the paying account has exhausted its Codex allowance and has insufficient available credits. The payer can check their balance and add credits in the Codex usage dashboard, then try again. If you do not manage the paying account, contact this repository's admins.

@hyperpolymath
hyperpolymath deleted the claude/e2e-language-pass-ow72tj branch November 10, 2026 20:41
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T20:44:18.708335Z 85cbca6 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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.

3 participants