Skip to content

Point at ->{e} when an argument's effect meets a closed callback arrow in an extern #180

Closed

Description

@simontreanor

Found while testing Pyfun main (the 0.10.0 cold Async, #177) against
super-scrabble's network code.

An extern whose callback parameter is written with a plain arrow rejects
every Pyfun function that does anything, and the error gives no hint of the
fix. The fix is an effect variable on that arrow:

extern import asyncio
extern type Reader
extern type Writer
extern type Server
extern serve: (Reader -> Writer -> Async unit) -> string -> int -> Async Server = asyncio.start_server
extern closeConn: Writer -> unit = .close()

let handle r w =
  async {
    closeConn w
  }

let listen = serve handle "127.0.0.1" 7777
error: effect mismatch: cannot unify Writer -> Async unit with Writer ->{io, async} Async unit
  --> 13:14
   |
13 | let listen = serve handle "127.0.0.1" 7777
   |              ^^^^^^^^^^^^

Same message on 0.9.0 and on main (18a4a31). Writing the callback arrow as
(Reader -> Writer ->{e} Async unit) checks clean, works for any handler
body, and keeps the _pf_run wrapping #177 gives an Async-returning
callback.

The tell

The mismatch is real and the checker is right to report it: an async { }
block performs async where it is written (DESIGN.md §4), so handle is
Reader -> Writer ->{io, async} Async unit, and declared effects are exact.
The message has three problems, though:

  • It says what failed but not where the closed set came from. A user who
    wrote Reader -> Writer -> Async unit reads it as "my handler isn't
    async", which it plainly is.
  • The obvious fix, copying the inferred set into the extern
    (->{io, async}), type-checks, but it is wrong: it only accepts handlers
    whose effect is exactly {io, async}. A handler that only awaits
    ({async}) is then rejected the same way. I made exactly this mistake
    before finding ->{e}.
  • The caret covers serve handle instead of handle, the argument whose
    effect didn't fit.

->{e} is the documented answer (DESIGN.md §4 "effect variable,
extern-only"; lesson 12; examples/interop/README.md), but nothing points a
user from this error to it.

Why it happens

  • src/types/mod.rs:9439: the (Ty::Fun, Ty::Fun) arm of unify builds
    the message once unify_eff fails. It sees only two types and knows
    nothing of an extern or an application.
  • src/types/mod.rs:8774: infer_apply_plain unifies the whole function
    type against arg_ty -> result at the application's span. A bad argument
    is therefore reported on func arg, and nothing at this level adds
    context. operator_hint (src/types/mod.rs:6673) is the existing pattern
    for wrapping a generic error with guidance, through map_err.
  • src/types/mod.rs:3380-3395: the extern's scheme carries its effect
    variables in eff_vars. A closed set on a callback arrow can only come
    from a declaration, because a Pyfun-defined higher-order function infers
    an effect variable for its parameter's arrow. So a closed callback effect
    that fails to unify is a strong sign of a declared signature that wants
    ->{e} (in an extern) or the exact set spelled out (in a type, where
    effect variables are an error).

What I would like

When an effect mismatch comes from applying an argument whose arrow meets
a closed, declared arrow, say so and name the effect variable, pointing at
the argument:

error: `handle` performs {io, async}, but `serve` declares this callback as `Writer -> Async unit`, which performs nothing
  --> 13:20
   |
13 | let listen = serve handle "127.0.0.1" 7777
   |                    ^^^^^^
   = help: to accept a callback whatever it does, give the arrow an effect variable in the extern: `(Reader -> Writer ->{e} Async unit)`

The wording is only a sketch. The two parts that matter are the pointer to
->{e} and the warning, implied or stated, against pasting in the inferred
set.

Activity

  1. added a commit that references this issue on Oct 17, 2026
    262e368
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions