# Type escape error in mutually recursive functions on GADT and phantom types

**URL:** <https://discuss.ocaml.org/t/type-escape-error-in-mutually-recursive-functions-on-gadt-and-phantom-types/17625>\
**Category:** Learning\
**Created:** [December 22, 2025, 9:13pm UTC](https://discuss.ocaml.org/t/type-escape-error-in-mutually-recursive-functions-on-gadt-and-phantom-types/17625 "2025-12-22T21:13:58Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![far\_us](https://avatars.discourse-cdn.com/v4/letter/f/cc9497/32.png) [@far\_us](https://discuss.ocaml.org/u/far_us)\
**Post date:** [December 22, 2025, 9:13pm UTC](https://discuss.ocaml.org/t/type-escape-error-in-mutually-recursive-functions-on-gadt-and-phantom-types/17625/1 "2025-12-22T21:13:58Z")

</div>

Hello,

I’m trying to write an interpreter using **GADTs and phantom types** to encode AST variants with state and exceptions. I’m now combining the state and exception variants and I have two mutually recursive functions.

Here’s a **minimal example** that reproduces the type error:

```ocaml
type 'a has_state_m = 'a constraint 'a = [< `HasState]
type 'a has_exception_m = 'a constraint 'a = [< `HasException]

type _ t =
  | HasState : 'a t -> 'a has_state_m t
  | HasException : 'a t -> 'a has_exception_m t
  | Core : 'a t -> 'a t
  | End : 'a t

let rec print : type a. a t -> unit =
  fun mode ->
    match mode with
    | HasState inner ->
        print_endline "Has State";
        do_something_for_state (HasState inner)
    | HasException inner ->
        print_endline "Has Exception";
        print inner
    | Core inner ->
        print_endline "Core";
        print inner
    | End -> print_endline "End of modes"; ()

and do_something_for_state : 'a -> unit = function
  | HasState inner ->
      print_endline "doing something for Has State";
      print inner
  | _ -> failwith "Not applicable"

```

```bash
Error: The value inner has type [< `HasState] t
       but an expression was expected of type [< `HasState] t
       The type constructor $0 would escape its scope

```

I **cannot remove the mutual recursion** because it reflects the semantics of my interpreter. Does anyone know a way to structure this code so that it typechecks?

---

<div class="post-metadata">

**Author:** ![octachron](https://avatars.discourse-cdn.com/v4/letter/o/49beb7/32.png) [@octachron](https://discuss.ocaml.org/u/octachron)\
**Post date:** [December 22, 2025, 10:11pm UTC](https://discuss.ocaml.org/t/type-escape-error-in-mutually-recursive-functions-on-gadt-and-phantom-types/17625/2 "2025-12-22T22:11:13Z")

</div>

You should essentially never mix unification variables (an `'a` appearing unbound in an annotation) with GADTs (and recursion). Using the correct annotation fixes the surface issue:

```ocaml
let rec print : type a. a t -> unit = ...
and do_something_for_state : type a. a t -> unit = function
  | HasState inner ->
      print_endline "doing something for Has State";
      print inner
  | _ -> failwith "Not applicable"

```

However, the next issue is probably that your phantom type represents the wrong semantics: currently you can only have state _xor_ exceptions. Moreover, the interaction of polymorphic variants and GADTs is complex and brittle. Thus I tend to advise to start with the simpler object type encoding:

```ocaml
type yes = Yes
type no = No

type _ t =
  | HasState : <has_exception:'e; .. > -> <has_state:yes; has_exception:'e> t
  | HasException : <has_state:'s; .. > t -> <has_exception:yes; has_state:'s> t
  | Core : 'a t -> 'a t
  | End : 'a t

```

---

<div class="post-metadata">

**Author:** ![far\_us](https://avatars.discourse-cdn.com/v4/letter/f/cc9497/32.png) [@far\_us](https://discuss.ocaml.org/u/far_us)\
**Post date:** [January 6, 2026, 2:31am UTC](https://discuss.ocaml.org/t/type-escape-error-in-mutually-recursive-functions-on-gadt-and-phantom-types/17625/3 "2026-01-06T02:31:55Z")

</div>

> [@octachron](#):
>
> ```auto
> type yes = Yes
> type no = No
> 
> type _ t =
> | HasState : <has_exception:'e; .. > -> <has_state:yes; has_exception:'e> t
> | HasException : <has_state:'s; .. > t -> <has_exception:yes; has_state:'s> t
> | Core : 'a t -> 'a t
> | End : 'a t
> 
> ```

Thank you so much! This solved my problem.
