RpcElixir.Types.FromInferred (elixir_ts_rpc v0.0.2)

Copy Markdown View Source

EXPERIMENTAL backend. It reads signatures inferred by Elixir's set-theoretic type system. The source is the ExCk BEAM chunk, not user-written @spec.

Not the recommended path. Use RpcElixir.Types.FromSpec for real work. This module tracks the type system as it evolves toward a public API.

Hard caveats:

  • Requires Elixir 1.19 or later. Set-theoretic signatures do not exist before that, so every lookup returns {:error, :no_signature} on older versions. The library itself supports Elixir 1.17+, but this backend does not. Its own tests carry the :requires_inference tag and are excluded below 1.19 (see test/test_helper.exs).

  • Private API. The ExCk chunk format and Module.Types.Descr shape are undocumented compiler internals. The chunk version is :elixir_checker_v3 on Elixir 1.19. Both have changed every minor release, so expect breakage on upgrade.

  • Requires Code.compiler_options(infer_signatures: true) when the target module compiles. Without it the chunk carries only function names. Enable it in your own mix.exs:

    defmodule MyApp.MixProject do
      use Mix.Project
    
      Code.compiler_options(infer_signatures: true)
    
      def project, do: [...]
    end
  • Inference is lossy. Most argument types come back as dynamic. You get more only when the function pattern-matches or guards on input. Returns fare better.

  • Anything untranslatable becomes %{kind: "dynamic"}, not a raise. So callers can fall back to FromSpec.

Gaps versus FromSpec

AreaBehaviour
Argumentsusually dynamic, unless the function pattern-matches or guards on them
Listscollapse to dynamic; no list kind
T | nilnullability is dropped
Optional map keysmap openness is ignored, so all fields look required
Module identitylost: no :struct tag, no built-in date or decimal resolution, no Ecto schema derivation, and wire_spec/0 is never consulted
any() / term()become dynamic instead of raising
integer() | float()widened to primitive / float
Multi-clause functionsonly the first inferred clause survives
{:error, E} branchesunprovable unless the body returns one, so error comes back as nil

What it does recover: atom-literal enums (:a | :b) become enum. An RPC return is decomposed when it contains an {:ok, T} tuple.

Summary

Functions

Convenience for the RPC convention call(input, context) :: {:ok, output}.

Returns the inferred argument and return type maps for an MFA. Gives {:error, :no_signature} when unavailable.

Functions

fetch_rpc(module, function)

@spec fetch_rpc(module(), atom()) ::
  {:ok,
   %{
     input: RpcElixir.Types.internal_spec(),
     output: RpcElixir.Types.internal_spec(),
     error: RpcElixir.Types.internal_spec() | nil
   }}
  | {:error, :no_signature}
  | {:error, {:invalid_return, RpcElixir.Types.internal_spec()}}

Convenience for the RPC convention call(input, context) :: {:ok, output}.

Inference cannot prove an {:error, _} branch unless the body returns one. So the recovered shape is usually a single {:ok, T} tuple. Returns {:ok, %{input: t, output: t, error: t | nil}} when that pattern is recognized, else {:error, {:invalid_return, t}}.

fetch_signature(module, function, arity)

@spec fetch_signature(module(), atom(), non_neg_integer()) ::
  {:ok,
   %{
     args: [RpcElixir.Types.internal_spec()],
     return: RpcElixir.Types.internal_spec()
   }}
  | {:error, :no_signature}

Returns the inferred argument and return type maps for an MFA. Gives {:error, :no_signature} when unavailable.