Skip to main content

src/girard.erl

-module(girard).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/girard.gleam").
-export([disk_resolver/0, default_options/0, with_resolver/2, with_target/2, annotate_module/2, annotate/2, new_cache/0, annotate_with_cache/3, invalidate/2, annotate_package/2, type_to_string/1, describe_error/1, report/1, main/0]).
-export_type([annotation/0, annotated_module/0, target/0, def/0, options/0, cache/0, module_result/0, group_item/0, input_error/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " A type annotator for Gleam, written in Gleam.\n"
    "\n"
    " Reports the inferred type of every expression — keyed by its source span —\n"
    " and the signature of every top-level function and constant, for a single\n"
    " module ([`annotate`](#annotate)) or a whole package\n"
    " ([`annotate_package`](#annotate_package)). Give it source text or a\n"
    " `glance` AST you parsed yourself.\n"
    "\n"
    " Imported modules are resolved through a [`Resolver`](#Resolver) to obtain\n"
    " their public interfaces.\n"
).

-type annotation() :: {annotation, glance:span(), girard@types:type()}.

-type annotated_module() :: {annotated_module,
        list({binary(), girard@types:scheme()}),
        list({binary(), girard@types:scheme()}),
        list(annotation())}.

-type target() :: erlang | java_script.

-type def() :: {function_def, glance:function_()} |
    {constant_def, glance:constant()}.

-opaque options() :: {options,
        fun((binary()) -> {ok, binary()} | {error, nil}),
        target()}.

-opaque cache() :: {cache,
        gleam@dict:dict(binary(), girard@internal@infer:module_interface())}.

-type module_result() :: {module_result,
        annotated_module(),
        list({binary(), girard@types:error()})}.

-type group_item() :: {annotated_def,
        def(),
        glance:function_(),
        list(girard@types:type()),
        girard@types:type()} |
    {placeholder_def, def(), girard@types:type()}.

-type input_error() :: {file_unreadable, binary()} | stdin_unreadable.

-file("src/girard.gleam", 83).
-spec def_name(def()) -> binary().
def_name(Def) ->
    case Def of
        {function_def, F} ->
            erlang:element(3, F);

        {constant_def, C} ->
            erlang:element(3, C)
    end.

-file("src/girard.gleam", 91).
?DOC(" `#(value references, field-access qualifier names)` of a definition.\n").
-spec def_refs(def()) -> {list(binary()), list(binary())}.
def_refs(Def) ->
    case Def of
        {function_def, F} ->
            girard@internal@reference:in_function(F);

        {constant_def, C} ->
            girard@internal@reference:in_constant(C)
    end.

-file("src/girard.gleam", 98).
-spec infer_def(
    girard@internal@infer:env(),
    girard@internal@infer:state(),
    def()
) -> {ok, {girard@types:type(), girard@internal@infer:state()}} |
    {error, girard@types:error()}.
infer_def(Env, St, Def) ->
    case Def of
        {function_def, F} ->
            girard@internal@infer:infer_function(Env, St, F);

        {constant_def, C} ->
            girard@internal@infer:infer_constant(Env, St, C)
    end.

-file("src/girard.gleam", 856).
-spec first_readable(list(binary())) -> {ok, binary()} | {error, nil}.
first_readable(Paths) ->
    case Paths of
        [] ->
            {error, nil};

        [Path | Rest] ->
            case simplifile:read(Path) of
                {ok, Source} ->
                    {ok, Source};

                {error, _} ->
                    first_readable(Rest)
            end
    end.

-file("src/girard.gleam", 842).
?DOC(
    " The default resolver: looks for an imported module's source under `src/` and\n"
    " the `build/packages/*/src` dependency sources, relative to the current\n"
    " working directory. The `build/packages` listing is read once and captured,\n"
    " so resolving many imports does not re-scan the directory each time.\n"
).
-spec disk_resolver() -> fun((binary()) -> {ok, binary()} | {error, nil}).
disk_resolver() ->
    Packages@1 = case simplifile_erl:read_directory(<<"build/packages"/utf8>>) of
        {ok, Packages} ->
            Packages;

        {error, _} ->
            []
    end,
    fun(Path) ->
        Candidates = gleam@list:map(
            Packages@1,
            fun(Pkg) ->
                <<<<<<<<"build/packages/"/utf8, Pkg/binary>>/binary,
                            "/src/"/utf8>>/binary,
                        Path/binary>>/binary,
                    ".gleam"/utf8>>
            end
        ),
        first_readable(
            [<<<<"src/"/utf8, Path/binary>>/binary, ".gleam"/utf8>> |
                Candidates]
        )
    end.

-file("src/girard.gleam", 124).
?DOC(
    " Default options: resolve imports from disk (`disk_resolver()`) and type for\n"
    " the `Erlang` target (matching `gleam build`'s default).\n"
).
-spec default_options() -> options().
default_options() ->
    {options, disk_resolver(), erlang}.

-file("src/girard.gleam", 130).
?DOC(
    " Resolve imported modules with `resolver` — e.g. `fn(_) { Error(Nil) }` to\n"
    " resolve none, or a custom in-memory resolver.\n"
).
-spec with_resolver(options(), fun((binary()) -> {ok, binary()} | {error, nil})) -> options().
with_resolver(Options, Resolver) ->
    {options, Resolver, erlang:element(3, Options)}.

-file("src/girard.gleam", 136).
?DOC(
    " Type for `target`. `@target(...)` definitions that do not match are dropped,\n"
    " exactly as the compiler omits them from the build.\n"
).
-spec with_target(options(), target()) -> options().
with_target(Options, Target) ->
    {options, erlang:element(2, Options), Target}.

-file("src/girard.gleam", 1002).
-spec sort_by_span(list(annotation())) -> list(annotation()).
sort_by_span(Annotations) ->
    gleam@list:sort(
        Annotations,
        fun(A, B) ->
            case gleam@int:compare(
                erlang:element(2, erlang:element(2, A)),
                erlang:element(2, erlang:element(2, B))
            ) of
                eq ->
                    gleam@int:compare(
                        erlang:element(3, erlang:element(2, A)),
                        erlang:element(3, erlang:element(2, B))
                    );

                Other ->
                    Other
            end
        end
    ).

-file("src/girard.gleam", 898).
?DOC(
    " The inferred (generalized) scheme of each definition, in source order.\n"
    " Definitions the environment somehow lacks are skipped.\n"
).
-spec collect_schemes(list(def()), girard@internal@infer:env()) -> list({binary(),
    girard@types:scheme()}).
collect_schemes(Defs, Env) ->
    gleam@list:filter_map(
        Defs,
        fun(Def) ->
            Name = def_name(Def),
            case girard@internal@infer:lookup(Env, Name) of
                {ok, Scheme} ->
                    {ok, {Name, Scheme}};

                {error, _} ->
                    {error, nil}
            end
        end
    ).

-file("src/girard.gleam", 869).
-spec render(
    glance:module_(),
    girard@internal@infer:env(),
    girard@internal@infer:state()
) -> annotated_module().
render(Module, Env, St) ->
    Functions = gleam@list:map(
        erlang:element(6, Module),
        fun(D) -> {function_def, erlang:element(3, D)} end
    ),
    Constants = gleam@list:map(
        erlang:element(5, Module),
        fun(D@1) -> {constant_def, erlang:element(3, D@1)} end
    ),
    Expressions = gleam@list:map(
        lists:reverse(erlang:element(4, St)),
        fun(Entry) ->
            {Span, Type_} = Entry,
            {annotation, Span, girard@internal@infer:zonk(St, Type_)}
        end
    ),
    {annotated_module,
        collect_schemes(Functions, Env),
        collect_schemes(Constants, Env),
        sort_by_span(Expressions)}.

-file("src/girard.gleam", 827).
?DOC(
    " Public types whose field accessors are reachable from other modules: public,\n"
    " non-opaque custom types. An `opaque` type's fields are private to its\n"
    " defining module, so its accessors are not exported — a same-named module\n"
    " function then wins over the (inaccessible) field at an external call site, as\n"
    " the compiler does (kata's opaque `Schema` with a `decode` field and a\n"
    " `decode` function).\n"
).
-spec public_accessor_type_names(glance:module_()) -> list(binary()).
public_accessor_type_names(Module) ->
    gleam@list:filter_map(
        erlang:element(3, Module),
        fun(D) ->
            case {erlang:element(4, erlang:element(3, D)),
                erlang:element(5, erlang:element(3, D))} of
                {public, false} ->
                    {ok, erlang:element(3, erlang:element(3, D))};

                {_, _} ->
                    {error, nil}
            end
        end
    ).

-file("src/girard.gleam", 803).
-spec public_type_names(glance:module_()) -> list(binary()).
public_type_names(Module) ->
    Types = gleam@list:filter_map(
        erlang:element(3, Module),
        fun(D) -> case erlang:element(4, erlang:element(3, D)) of
                public ->
                    {ok, erlang:element(3, erlang:element(3, D))};

                private ->
                    {error, nil}
            end end
    ),
    Aliases = gleam@list:filter_map(
        erlang:element(4, Module),
        fun(D@1) -> case erlang:element(4, erlang:element(3, D@1)) of
                public ->
                    {ok, erlang:element(3, erlang:element(3, D@1))};

                private ->
                    {error, nil}
            end end
    ),
    lists:append(Types, Aliases).

-file("src/girard.gleam", 776).
-spec public_value_names(glance:module_()) -> list(binary()).
public_value_names(Module) ->
    Functions = gleam@list:filter_map(
        erlang:element(6, Module),
        fun(D) -> case erlang:element(4, erlang:element(3, D)) of
                public ->
                    {ok, erlang:element(3, erlang:element(3, D))};

                private ->
                    {error, nil}
            end end
    ),
    Constants = gleam@list:filter_map(
        erlang:element(5, Module),
        fun(D@1) -> case erlang:element(4, erlang:element(3, D@1)) of
                public ->
                    {ok, erlang:element(3, erlang:element(3, D@1))};

                private ->
                    {error, nil}
            end end
    ),
    Constructors = gleam@list:flat_map(
        erlang:element(3, Module),
        fun(D@2) ->
            Ct = erlang:element(3, D@2),
            case {erlang:element(4, Ct), erlang:element(5, Ct)} of
                {public, false} ->
                    gleam@list:map(
                        erlang:element(7, Ct),
                        fun(V) -> erlang:element(2, V) end
                    );

                {_, _} ->
                    []
            end
        end
    ),
    lists:append([Functions, Constants, Constructors]).

-file("src/girard.gleam", 577).
-spec placeholder(
    girard@internal@infer:env(),
    list(group_item()),
    girard@internal@infer:state(),
    def()
) -> {girard@internal@infer:env(),
    list(group_item()),
    girard@internal@infer:state()}.
placeholder(Env, Items, St, Def) ->
    {Var, St@1} = girard@internal@infer:fresh_var(St),
    {girard@internal@infer:define(Env, def_name(Def), {scheme, [], Var}),
        [{placeholder_def, Def, Var} | Items],
        St@1}.

-file("src/girard.gleam", 594).
?DOC(
    " Pre-register one SCC member: a function with signature variables is bound at\n"
    " its declared scheme (`AnnotatedDef`); any other definition gets a fresh\n"
    " monomorphic placeholder.\n"
).
-spec prereg_def(
    girard@internal@infer:env(),
    list(group_item()),
    girard@internal@infer:state(),
    def()
) -> {girard@internal@infer:env(),
    list(group_item()),
    girard@internal@infer:state()}.
prereg_def(Env, Items, St, Def) ->
    Annotated = case Def of
        {function_def, F} ->
            case girard@internal@infer:has_annotation_vars(F) of
                true ->
                    {ok, F};

                false ->
                    {error, nil}
            end;

        {constant_def, _} ->
            {error, nil}
    end,
    case Annotated of
        {error, _} ->
            placeholder(Env, Items, St, Def);

        {ok, F@1} ->
            {Params, Return_type, Rigid_ids, St@1} = girard@internal@infer:signature_skeleton(
                Env,
                St,
                F@1
            ),
            {girard@internal@infer:define(
                    Env,
                    def_name(Def),
                    girard@internal@infer:rigid_scheme(
                        Rigid_ids,
                        Params,
                        Return_type
                    )
                ),
                [{annotated_def, Def, F@1, Params, Return_type} | Items],
                St@1}
    end.

-file("src/girard.gleam", 491).
?DOC(
    " Infer one strongly-connected component of mutually recursive definitions,\n"
    " then generalize each against the surrounding environment and add it back for\n"
    " later components.\n"
    "\n"
    " A function with signature variables is pre-registered at a scheme over those\n"
    " variables so recursion and siblings see it polymorphically; its body is\n"
    " checked against the signature with those variables rigid, and within its own\n"
    " body it sees itself at the rigid monotype (no polymorphic recursion). Every\n"
    " other definition is inferred monomorphically against a fresh placeholder.\n"
    " The members are marked *live* (see `infer.mark_live`): a reference to a\n"
    " sibling resolves its scheme through the current substitution, so once a\n"
    " member's body has settled an unannotated part (absorbing it into a signature\n"
    " variable) a later sibling sees the resolved type — the compiler's shared\n"
    " mutable cells, reproduced through girard's threaded substitution. Because of\n"
    " that, bodies are inferred *provider-first*: a member whose signature has an\n"
    " unannotated part is typed before the fully-annotated members that consume it\n"
    " (a dependency-respecting order within the component, as Tarjan provides).\n"
).
-spec infer_group(
    girard@internal@infer:env(),
    girard@internal@infer:state(),
    list(def())
) -> {ok, {girard@internal@infer:env(), girard@internal@infer:state()}} |
    {error, girard@types:error()}.
infer_group(Env, St, Group) ->
    {Group_env, Rev_items, St@2} = gleam@list:fold(
        Group,
        {Env, [], St},
        fun(Acc, Def) ->
            {Env@1, Items, St@1} = Acc,
            prereg_def(Env@1, Items, St@1, Def)
        end
    ),
    Group_env@1 = girard@internal@infer:mark_live(
        Group_env,
        gleam@list:map(Group, fun def_name/1)
    ),
    Items@1 = lists:reverse(Rev_items),
    {Providers, Consumers} = gleam@list:partition(
        Items@1,
        fun(Item) -> case Item of
                {annotated_def, _, F, _, _} ->
                    (erlang:element(6, F) =:= none) orelse gleam@list:any(
                        erlang:element(5, F),
                        fun(P) -> erlang:element(4, P) =:= none end
                    );

                {placeholder_def, _, _} ->
                    false
            end end
    ),
    gleam@result:'try'(
        gleam@list:try_fold(
            lists:append(Providers, Consumers),
            St@2,
            fun(St@3, Item@1) -> case Item@1 of
                    {annotated_def, Def@1, F@1, Params, Return_type} ->
                        Body_env = girard@internal@infer:bind_params(
                            girard@internal@infer:define(
                                Group_env@1,
                                def_name(Def@1),
                                girard@internal@infer:rigid_self_scheme(
                                    Params,
                                    Return_type
                                )
                            ),
                            F@1,
                            Params
                        ),
                        girard@internal@infer:check_body(
                            Body_env,
                            St@3,
                            F@1,
                            Return_type
                        );

                    {placeholder_def, Def@2, Var} ->
                        gleam@result:'try'(
                            infer_def(Group_env@1, St@3, Def@2),
                            fun(_use0) ->
                                {Inferred, St@4} = _use0,
                                girard@internal@infer:unify(St@4, Var, Inferred)
                            end
                        )
                end end
        ),
        fun(St@5) ->
            gleam@result:'try'(
                girard@internal@infer:resolve_pending(Group_env@1, St@5),
                fun(St@6) ->
                    Env@3 = gleam@list:fold(
                        Items@1,
                        Env,
                        fun(Env@2, Item@2) -> case Item@2 of
                                {annotated_def,
                                    Def@3,
                                    _,
                                    Params@1,
                                    Return_type@1} ->
                                    girard@internal@infer:define(
                                        Env@2,
                                        def_name(Def@3),
                                        girard@internal@infer:function_scheme(
                                            Env@2,
                                            St@6,
                                            Params@1,
                                            Return_type@1
                                        )
                                    );

                                {placeholder_def, Def@4, Var@1} ->
                                    girard@internal@infer:define(
                                        Env@2,
                                        def_name(Def@4),
                                        girard@internal@infer:generalize(
                                            St@6,
                                            Env@2,
                                            Var@1
                                        )
                                    )
                            end end
                    ),
                    {ok, {Env@3, St@6}}
                end
            )
        end
    ).

-file("src/girard.gleam", 460).
?DOC(
    " One best-effort step over a strongly-connected component: on success adopt\n"
    " the new environment; on failure keep the prior one (discarding the\n"
    " component's partial work) and record every definition in it as skipped.\n"
).
-spec best_effort_group(
    {{girard@internal@infer:env(), girard@internal@infer:state()},
        list({binary(), girard@types:error()})},
    list(def())
) -> {{girard@internal@infer:env(), girard@internal@infer:state()},
    list({binary(), girard@types:error()})}.
best_effort_group(Acc, Group) ->
    {{Env, St}, Skipped} = Acc,
    case infer_group(Env, St, Group) of
        {ok, Env_st} ->
            {Env_st, Skipped};

        {error, Error} ->
            Entries = gleam@list:map(Group, fun(D) -> {def_name(D), Error} end),
            {{Env, St}, lists:append(Skipped, Entries)}
    end.

-file("src/girard.gleam", 402).
-spec infer_defs(
    girard@internal@infer:env(),
    girard@internal@infer:state(),
    gleam@set:set(binary()),
    list(def()),
    boolean()
) -> {ok,
        {{girard@internal@infer:env(), girard@internal@infer:state()},
            list({binary(), girard@types:error()})}} |
    {error, girard@types:error()}.
infer_defs(Env, St, Module_aliases, Defs, Best_effort) ->
    By_name = maps:from_list(
        gleam@list:map(Defs, fun(D) -> {def_name(D), D} end)
    ),
    Names = gleam@list:map(Defs, fun def_name/1),
    Name_set = gleam@set:from_list(Names),
    Edges = maps:from_list(
        gleam@list:map(
            Defs,
            fun(D@1) ->
                {Values, Qualifiers} = def_refs(D@1),
                Kept_qualifiers = gleam@list:filter(
                    Qualifiers,
                    fun(Name) ->
                        not gleam@set:contains(Module_aliases, Name)
                    end
                ),
                Refs = gleam@list:filter(
                    lists:append(Values, Kept_qualifiers),
                    fun(_capture) -> gleam@set:contains(Name_set, _capture) end
                ),
                {def_name(D@1), Refs}
            end
        )
    ),
    Groups = gleam@list:map(
        girard@internal@scc:components(Names, Edges),
        fun(Group) ->
            gleam@list:filter_map(
                Group,
                fun(_capture@1) -> gleam_stdlib:map_get(By_name, _capture@1) end
            )
        end
    ),
    case Best_effort of
        false ->
            gleam@result:map(
                gleam@list:try_fold(
                    Groups,
                    {Env, St},
                    fun(Acc, Group@1) ->
                        {Env@1, St@1} = Acc,
                        infer_group(Env@1, St@1, Group@1)
                    end
                ),
                fun(_use0) ->
                    {Env@2, St@2} = _use0,
                    {{Env@2, St@2}, []}
                end
            );

        true ->
            {ok,
                gleam@list:fold(
                    Groups,
                    {{Env, St}, []},
                    fun best_effort_group/2
                )}
    end.

-file("src/girard.gleam", 743).
-spec last_segment(binary()) -> binary().
last_segment(Path) ->
    case gleam@list:last(gleam@string:split(Path, <<"/"/utf8>>)) of
        {ok, Segment} ->
            Segment;

        {error, _} ->
            Path
    end.

-file("src/girard.gleam", 735).
?DOC(
    " The name under which an import is accessible for qualified access, or\n"
    " `Error` when the module is imported with a discarded alias (`as _x`) and so\n"
    " has no qualified name at all.\n"
).
-spec qualified_alias(glance:import()) -> {ok, binary()} | {error, nil}.
qualified_alias(Import_) ->
    case erlang:element(4, Import_) of
        {some, {named, Alias}} ->
            {ok, Alias};

        {some, {discarded, _}} ->
            {error, nil};

        none ->
            {ok, last_segment(erlang:element(3, Import_))}
    end.

-file("src/girard.gleam", 662).
?DOC(" Bring an import's qualified alias and unqualified values/types into scope.\n").
-spec import_items(
    girard@internal@infer:env(),
    glance:import(),
    girard@internal@infer:module_interface()
) -> girard@internal@infer:env().
import_items(Env, Import_, Interface) ->
    Env@1 = case qualified_alias(Import_) of
        {ok, Alias} ->
            girard@internal@infer:import_qualified(Env, Alias, Interface);

        {error, _} ->
            Env
    end,
    Env@3 = gleam@list:fold(
        erlang:element(6, Import_),
        Env@1,
        fun(Env@2, U) ->
            girard@internal@infer:import_value(
                Env@2,
                gleam@option:unwrap(erlang:element(3, U), erlang:element(2, U)),
                Interface,
                erlang:element(2, U)
            )
        end
    ),
    gleam@list:fold(
        erlang:element(5, Import_),
        Env@3,
        fun(Env@4, U@1) ->
            girard@internal@infer:import_type(
                Env@4,
                gleam@option:unwrap(
                    erlang:element(3, U@1),
                    erlang:element(2, U@1)
                ),
                Interface,
                erlang:element(2, U@1)
            )
        end
    ).

-file("src/girard.gleam", 763).
-spec on_target(glance:definition(any()), target()) -> boolean().
on_target(Definition, Target) ->
    Active = case Target of
        erlang ->
            <<"erlang"/utf8>>;

        java_script ->
            <<"javascript"/utf8>>
    end,
    gleam@list:all(
        erlang:element(2, Definition),
        fun(Attr) -> case {erlang:element(2, Attr), erlang:element(3, Attr)} of
                {<<"target"/utf8>>, [{variable, _, T}]} ->
                    T =:= Active;

                {_, _} ->
                    true
            end end
    ).

-file("src/girard.gleam", 753).
?DOC(
    " Keep only the definitions and imports compiled for `target`: those with no\n"
    " `@target` attribute, or one naming the active target. A definition annotated\n"
    " for the other target is dropped, exactly as the compiler omits it.\n"
).
-spec for_target(glance:module_(), target()) -> glance:module_().
for_target(Module, Target) ->
    {module,
        gleam@list:filter(
            erlang:element(2, Module),
            fun(_capture) -> on_target(_capture, Target) end
        ),
        gleam@list:filter(
            erlang:element(3, Module),
            fun(_capture@1) -> on_target(_capture@1, Target) end
        ),
        gleam@list:filter(
            erlang:element(4, Module),
            fun(_capture@2) -> on_target(_capture@2, Target) end
        ),
        gleam@list:filter(
            erlang:element(5, Module),
            fun(_capture@3) -> on_target(_capture@3, Target) end
        ),
        gleam@list:filter(
            erlang:element(6, Module),
            fun(_capture@4) -> on_target(_capture@4, Target) end
        )}.

-file("src/girard.gleam", 703).
-spec resolve_uncached(
    options(),
    gleam@set:set(binary()),
    gleam@dict:dict(binary(), girard@internal@infer:module_interface()),
    binary(),
    boolean()
) -> {ok,
        {gleam@option:option(girard@internal@infer:module_interface()),
            gleam@dict:dict(binary(), girard@internal@infer:module_interface())}} |
    {error, girard@types:error()}.
resolve_uncached(Options, Loading, Cache, Path, Best_effort) ->
    case (erlang:element(2, Options))(Path) of
        {error, _} ->
            {ok, {none, Cache}};

        {ok, Source} ->
            case glance:module(Source) of
                {error, _} ->
                    {ok, {none, Cache}};

                {ok, Module} ->
                    gleam@result:'try'(
                        infer_module(
                            Options,
                            gleam@set:insert(Loading, Path),
                            Cache,
                            Path,
                            Module,
                            Best_effort
                        ),
                        fun(_use0) ->
                            {_, Interface, Cache@1, _} = _use0,
                            {ok,
                                {{some, Interface},
                                    gleam@dict:insert(Cache@1, Path, Interface)}}
                        end
                    )
            end
    end.

-file("src/girard.gleam", 684).
-spec resolve_interface(
    options(),
    gleam@set:set(binary()),
    gleam@dict:dict(binary(), girard@internal@infer:module_interface()),
    binary(),
    boolean()
) -> {ok,
        {gleam@option:option(girard@internal@infer:module_interface()),
            gleam@dict:dict(binary(), girard@internal@infer:module_interface())}} |
    {error, girard@types:error()}.
resolve_interface(Options, Loading, Cache, Path, Best_effort) ->
    gleam@bool:lazy_guard(
        Path =:= <<"gleam"/utf8>>,
        fun() ->
            {ok, {{some, girard@internal@infer:prelude_interface()}, Cache}}
        end,
        fun() -> case gleam_stdlib:map_get(Cache, Path) of
                {ok, Interface} ->
                    {ok, {{some, Interface}, Cache}};

                {error, _} ->
                    resolve_uncached(Options, Loading, Cache, Path, Best_effort)
            end end
    ).

-file("src/girard.gleam", 628).
-spec process_imports(
    options(),
    gleam@set:set(binary()),
    gleam@dict:dict(binary(), girard@internal@infer:module_interface()),
    girard@internal@infer:env(),
    list(glance:definition(glance:import())),
    boolean()
) -> {ok,
        {girard@internal@infer:env(),
            gleam@dict:dict(binary(), girard@internal@infer:module_interface())}} |
    {error, girard@types:error()}.
process_imports(Options, Loading, Cache, Env, Imports, Best_effort) ->
    gleam@list:try_fold(
        Imports,
        {Env, Cache},
        fun(Acc, Definition) ->
            {Env@1, Cache@1} = Acc,
            Import_ = erlang:element(3, Definition),
            Path = erlang:element(3, Import_),
            gleam@bool:guard(
                gleam@set:contains(Loading, Path),
                {ok, {Env@1, Cache@1}},
                fun() ->
                    gleam@result:'try'(
                        resolve_interface(
                            Options,
                            Loading,
                            Cache@1,
                            Path,
                            Best_effort
                        ),
                        fun(_use0) ->
                            {Maybe_interface, Cache@2} = _use0,
                            case Maybe_interface of
                                none ->
                                    {ok, {Env@1, Cache@2}};

                                {some, Interface} ->
                                    {ok,
                                        {import_items(Env@1, Import_, Interface),
                                            Cache@2}}
                            end
                        end
                    )
                end
            )
        end
    ).

-file("src/girard.gleam", 309).
?DOC(
    " Fully infer a module: resolve imports, register types, and infer every\n"
    " definition in dependency order. Returns the final environment and state\n"
    " plus the module's public interface.\n"
).
-spec infer_module(
    options(),
    gleam@set:set(binary()),
    gleam@dict:dict(binary(), girard@internal@infer:module_interface()),
    binary(),
    glance:module_(),
    boolean()
) -> {ok,
        {{girard@internal@infer:env(), girard@internal@infer:state()},
            girard@internal@infer:module_interface(),
            gleam@dict:dict(binary(), girard@internal@infer:module_interface()),
            list({binary(), girard@types:error()})}} |
    {error, girard@types:error()}.
infer_module(Options, Loading, Cache, Module_name, Module, Best_effort) ->
    Module@1 = for_target(Module, erlang:element(3, Options)),
    {Prelude_env, St} = girard@internal@infer:prelude(),
    Env = girard@internal@infer:set_module(Prelude_env, Module_name),
    gleam@result:'try'(
        process_imports(
            Options,
            Loading,
            Cache,
            Env,
            erlang:element(2, Module@1),
            Best_effort
        ),
        fun(_use0) ->
            {Env@1, Cache@1} = _use0,
            Env@3 = gleam@list:fold(
                erlang:element(3, Module@1),
                Env@1,
                fun(Env@2, D) ->
                    Ct = erlang:element(3, D),
                    girard@internal@infer:declare_type(
                        Env@2,
                        erlang:element(3, Ct),
                        erlang:length(erlang:element(6, Ct))
                    )
                end
            ),
            Env@5 = gleam@list:fold(
                erlang:element(4, Module@1),
                Env@3,
                fun(Env@4, D@1) ->
                    girard@internal@infer:register_type_alias(
                        Env@4,
                        erlang:element(3, D@1)
                    )
                end
            ),
            {Env@7, St@2} = gleam@list:fold(
                erlang:element(3, Module@1),
                {Env@5, St},
                fun(Acc, D@2) ->
                    {Env@6, St@1} = Acc,
                    girard@internal@infer:register_custom_type(
                        Env@6,
                        St@1,
                        erlang:element(3, D@2)
                    )
                end
            ),
            Env@9 = gleam@list:fold(
                erlang:element(6, Module@1),
                Env@7,
                fun(Env@8, D@3) ->
                    Function = erlang:element(3, D@3),
                    girard@internal@infer:register_field_map(
                        Env@8,
                        erlang:element(3, Function),
                        gleam@list:map(
                            erlang:element(5, Function),
                            fun(P) -> erlang:element(2, P) end
                        )
                    )
                end
            ),
            Functions = gleam@list:map(
                erlang:element(6, Module@1),
                fun(D@4) -> {function_def, erlang:element(3, D@4)} end
            ),
            Constants = gleam@list:map(
                erlang:element(5, Module@1),
                fun(D@5) -> {constant_def, erlang:element(3, D@5)} end
            ),
            Defs = lists:append(Functions, Constants),
            Module_aliases = gleam@set:from_list(
                gleam@list:filter_map(
                    erlang:element(2, Module@1),
                    fun(D@6) -> qualified_alias(erlang:element(3, D@6)) end
                )
            ),
            gleam@result:'try'(
                infer_defs(Env@9, St@2, Module_aliases, Defs, Best_effort),
                fun(_use0@1) ->
                    {{Final_env, St@3}, Skipped} = _use0@1,
                    Interface = girard@internal@infer:build_interface(
                        Final_env,
                        St@3,
                        Module_name,
                        public_value_names(Module@1),
                        public_type_names(Module@1),
                        public_accessor_type_names(Module@1)
                    ),
                    {ok, {{Final_env, St@3}, Interface, Cache@1, Skipped}}
                end
            )
        end
    ).

-file("src/girard.gleam", 157).
?DOC(
    " Annotate an already-parsed `glance.Module`. Use this when you have parsed the\n"
    " source with `glance` yourself — the returned spans are glance's, so they line\n"
    " up with your AST's node spans and you avoid parsing the same source twice.\n"
    " (Imported modules are still parsed internally, via the resolver.) Returns the\n"
    " inferred error if the module does not type; for partial results on an\n"
    " ill-typed module, use [`annotate_package`](#annotate_package).\n"
).
-spec annotate_module(glance:module_(), options()) -> {ok, annotated_module()} |
    {error, girard@types:error()}.
annotate_module(Module, Options) ->
    gleam@result:'try'(
        infer_module(
            Options,
            gleam@set:new(),
            maps:new(),
            <<""/utf8>>,
            Module,
            false
        ),
        fun(_use0) ->
            {{Env, St}, _, _, _} = _use0,
            {ok, render(Module, Env, St)}
        end
    ).

-file("src/girard.gleam", 998).
-spec parse(binary()) -> {ok, glance:module_()} | {error, girard@types:error()}.
parse(Source) ->
    _pipe = glance:module(Source),
    gleam@result:map_error(_pipe, fun(Field@0) -> {parse_failed, Field@0} end).

-file("src/girard.gleam", 143).
?DOC(
    " Annotate a Gleam source string: parse it with `glance`, then annotate as\n"
    " [`annotate_module`](#annotate_module). Returns the inferred error if the\n"
    " module does not type. The quick path is `annotate(source, default_options())`.\n"
).
-spec annotate(binary(), options()) -> {ok, annotated_module()} |
    {error, girard@types:error()}.
annotate(Source, Options) ->
    gleam@result:'try'(
        parse(Source),
        fun(Module) -> annotate_module(Module, Options) end
    ).

-file("src/girard.gleam", 191).
?DOC(
    " An empty [`Cache`](#Cache) to seed a run of\n"
    " [`annotate_with_cache`](#annotate_with_cache) calls.\n"
).
-spec new_cache() -> cache().
new_cache() ->
    {cache, maps:new()}.

-file("src/girard.gleam", 204).
?DOC(
    " Annotate a source string like [`annotate`](#annotate), but reuse and extend\n"
    " `cache`: imported modules already inferred in it are taken from the cache\n"
    " rather than resolved and inferred again, and any newly inferred ones are\n"
    " added. Returns the result and the updated cache to thread into the next call.\n"
    "\n"
    " `annotate_with_cache(source, options, new_cache())` matches\n"
    " [`annotate`](#annotate)`(source, options)` exactly; the cache only pays off\n"
    " when shared across calls that import overlapping modules — an editor\n"
    " re-checking a file as it changes, or a walk over a package's modules.\n"
).
-spec annotate_with_cache(binary(), options(), cache()) -> {{ok,
            annotated_module()} |
        {error, girard@types:error()},
    cache()}.
annotate_with_cache(Source, Options, Cache) ->
    case parse(Source) of
        {error, Error} ->
            {{error, Error}, Cache};

        {ok, Module} ->
            case infer_module(
                Options,
                gleam@set:new(),
                erlang:element(2, Cache),
                <<""/utf8>>,
                Module,
                false
            ) of
                {error, Error@1} ->
                    {{error, Error@1}, Cache};

                {ok, {{Env, St}, _, Interfaces, _}} ->
                    {{ok, render(Module, Env, St)}, {cache, Interfaces}}
            end
    end.

-file("src/girard.gleam", 239).
?DOC(
    " Drop the cached interface for `path` (the module path, e.g.\n"
    " `\"my_app/router\"`), so the next [`annotate_with_cache`](#annotate_with_cache)\n"
    " that needs it re-infers it from source. Use this when a module changes.\n"
    "\n"
    " Only the named module is dropped. A cached module that *imports* the changed\n"
    " one keeps its own (now possibly stale) interface, so after a change that\n"
    " alters a module's public surface, also invalidate its importers — or start\n"
    " from a [`new_cache`](#new_cache).\n"
).
-spec invalidate(cache(), binary()) -> cache().
invalidate(Cache, Path) ->
    {cache, gleam@dict:delete(erlang:element(2, Cache), Path)}.

-file("src/girard.gleam", 271).
?DOC(
    " Annotate every module in a package in one pass, sharing inference of common\n"
    " imports across modules. `modules` maps each module's path (e.g.\n"
    " `\"my_app/router\"`) to its parsed `glance.Module`; the result maps the same\n"
    " paths to a [`ModuleResult`](#ModuleResult).\n"
    "\n"
    " This is the batch counterpart to [`annotate_module`](#annotate_module): a\n"
    " dependency imported by several modules is inferred once for the whole run\n"
    " rather than once per\n"
    " importing module. Cross-module references *within* the package are resolved\n"
    " through the options' resolver, so it must also resolve the package's own\n"
    " modules (a resolver wrapping the build's module sources does); a module\n"
    " reached only that way is inferred for its interface and again here for its\n"
    " annotations.\n"
    "\n"
    " Best-effort per definition: a top-level function or constant that does not\n"
    " type — along with any that depend on it — is reported in that module's\n"
    " `skipped` list rather than failing the module, while every other definition\n"
    " is still annotated. A module thus always appears in the result; a fully\n"
    " strict check is `result.skipped == []`.\n"
).
-spec annotate_package(list({binary(), glance:module_()}), options()) -> gleam@dict:dict(binary(), module_result()).
annotate_package(Modules, Options) ->
    {Results@1, _} = gleam@list:fold(
        Modules,
        {maps:new(), maps:new()},
        fun(Acc, Entry) ->
            {Results, Cache} = Acc,
            {Path, Module} = Entry,
            case infer_module(
                Options,
                gleam@set:new(),
                Cache,
                Path,
                Module,
                true
            ) of
                {error, _} ->
                    {Results, Cache};

                {ok, {{Env, St}, Interface, Cache@1, Skipped}} ->
                    Cache@2 = gleam@dict:insert(Cache@1, Path, Interface),
                    Result = {module_result, render(Module, Env, St), Skipped},
                    {gleam@dict:insert(Results, Path, Result), Cache@2}
            end
        end
    ),
    Results@1.

-file("src/girard.gleam", 911).
?DOC(
    " Render an inferred `Type` to Gleam syntax (e.g. `fn(Int) -> a`), naming type\n"
    " variables `a, b, c, …`. Each call names variables independently: an `a` in\n"
    " one rendered type is unrelated to an `a` in another.\n"
).
-spec type_to_string(girard@types:type()) -> binary().
type_to_string(Type_) ->
    girard@internal@printer:to_string(Type_).

-file("src/girard.gleam", 969).
?DOC(" A short, human-readable description of an inference error.\n").
-spec describe_error(girard@types:error()) -> binary().
describe_error(Error) ->
    case Error of
        {type_mismatch, A, B} ->
            <<<<<<"type mismatch: "/utf8,
                        (girard@internal@printer:to_string(A))/binary>>/binary,
                    " vs "/utf8>>/binary,
                (girard@internal@printer:to_string(B))/binary>>;

        arity_mismatch ->
            <<"wrong number of arguments"/utf8>>;

        {recursive_type, _, Type_} ->
            <<"recursive type: "/utf8,
                (girard@internal@printer:to_string(Type_))/binary>>;

        {unbound_variable, Name} ->
            <<"unbound variable: "/utf8, Name/binary>>;

        {unknown_constructor, Name@1} ->
            <<"unknown constructor: "/utf8, Name@1/binary>>;

        {unknown_module, Alias} ->
            <<"unknown module: "/utf8, Alias/binary>>;

        {no_such_export, Module, Name@2} ->
            <<<<<<<<"module `"/utf8, Module/binary>>/binary, "` has no `"/utf8>>/binary,
                    Name@2/binary>>/binary,
                "`"/utf8>>;

        {no_such_field, Type_name, Label} ->
            <<<<<<<<"type `"/utf8, Type_name/binary>>/binary,
                        "` has no field `"/utf8>>/binary,
                    Label/binary>>/binary,
                "`"/utf8>>;

        not_a_record ->
            <<"field access or update on a non-record value"/utf8>>;

        not_a_tuple ->
            <<"tuple index on a non-tuple value"/utf8>>;

        {tuple_index_out_of_range, Index} ->
            <<"tuple index out of range: "/utf8,
                (erlang:integer_to_binary(Index))/binary>>;

        {unknown_label, Label@1} ->
            <<"unknown argument label: "/utf8, Label@1/binary>>;

        ambiguous_call ->
            <<"labelled arguments to an unknown callable"/utf8>>;

        missing_argument ->
            <<"missing argument"/utf8>>;

        {unsupported, Feature} ->
            <<"unsupported: "/utf8, Feature/binary>>;

        {parse_failed, _} ->
            <<"could not parse source"/utf8>>
    end.

-file("src/girard.gleam", 931).
?DOC(
    " Annotate a source string and render the result as a human-readable text\n"
    " report (signatures and per-expression types). On failure the report is a\n"
    " single `// error:` line.\n"
    "\n"
    " ## Example\n"
    "\n"
    " ```gleam\n"
    " report(\"pub fn double(x) { x + x }\")\n"
    " ```\n"
    "\n"
    " ```text\n"
    " double: fn(Int) -> Int\n"
    " 19-20: Int\n"
    " 19-24: Int\n"
    " 23-24: Int\n"
    " ```\n"
).
-spec report(binary()) -> binary().
report(Source) ->
    case annotate(Source, default_options()) of
        {error, Error} ->
            <<"// error: "/utf8, (describe_error(Error))/binary>>;

        {ok, Annotated} ->
            Names = girard@internal@printer:new_names(),
            {Rev_sigs, Names@3} = gleam@list:fold(
                lists:append(
                    erlang:element(2, Annotated),
                    erlang:element(3, Annotated)
                ),
                {[], Names},
                fun(Acc, Def) ->
                    {Lines, Names@1} = Acc,
                    {Text, Names@2} = girard@internal@printer:print(
                        Names@1,
                        erlang:element(3, (erlang:element(2, Def)))
                    ),
                    {[<<<<(erlang:element(1, Def))/binary, ": "/utf8>>/binary,
                                Text/binary>> |
                            Lines],
                        Names@2}
                end
            ),
            {Rev_exprs, _} = gleam@list:fold(
                erlang:element(4, Annotated),
                {[], Names@3},
                fun(Acc@1, A) ->
                    {Lines@1, Names@4} = Acc@1,
                    {Text@1, Names@5} = girard@internal@printer:print(
                        Names@4,
                        erlang:element(3, A)
                    ),
                    Line = <<<<<<<<(erlang:integer_to_binary(
                                        erlang:element(2, erlang:element(2, A))
                                    ))/binary,
                                    "-"/utf8>>/binary,
                                (erlang:integer_to_binary(
                                    erlang:element(3, erlang:element(2, A))
                                ))/binary>>/binary,
                            ": "/utf8>>/binary,
                        Text@1/binary>>,
                    {[Line | Lines@1], Names@5}
                end
            ),
            gleam@string:join(
                lists:append(lists:reverse(Rev_sigs), lists:reverse(Rev_exprs)),
                <<"\n"/utf8>>
            )
    end.

-file("src/girard.gleam", 1057).
-spec usage() -> binary().
usage() ->
    <<"girard — a type annotator for Gleam

Usage:
  gleam run -- <file.gleam>     annotate a file
  gleam run -- -                annotate stdin
  cat file.gleam | gleam run    annotate stdin

Output: each top-level definition's inferred signature, then one
`<start>-<end>: <type>` line per expression (by source byte span)."/utf8>>.

-file("src/girard.gleam", 1047).
-spec read_file(binary()) -> {ok, binary()} | {error, input_error()}.
read_file(Path) ->
    _pipe = simplifile:read(Path),
    gleam@result:replace_error(_pipe, {file_unreadable, Path}).

-file("src/girard.gleam", 1040).
-spec input_error_message(input_error()) -> binary().
input_error_message(Error) ->
    case Error of
        {file_unreadable, Path} ->
            <<"could not read file: "/utf8, Path/binary>>;

        stdin_unreadable ->
            <<"could not read stdin"/utf8>>
    end.

-file("src/girard.gleam", 1033).
-spec emit({ok, binary()} | {error, input_error()}) -> nil.
emit(Source) ->
    case Source of
        {ok, Text} ->
            gleam_stdlib:println(report(Text));

        {error, Error} ->
            gleam_stdlib:println_error(
                <<"error: "/utf8, (input_error_message(Error))/binary>>
            )
    end.

-file("src/girard.gleam", 1052).
-spec read_stdin() -> {ok, binary()} | {error, input_error()}.
read_stdin() ->
    _pipe = simplifile:read(<<"/dev/stdin"/utf8>>),
    gleam@result:replace_error(_pipe, stdin_unreadable).

-file("src/girard.gleam", 1015).
?DOC(
    " `gleam run -- <file.gleam>` annotates a file; `gleam run -- -` (or no\n"
    " arguments, or piped input) annotates stdin. Imports are resolved from disk.\n"
).
-spec main() -> nil.
main() ->
    case erlang:element(4, argv:load()) of
        [<<"--help"/utf8>>] ->
            gleam_stdlib:println(usage());

        [<<"-h"/utf8>>] ->
            gleam_stdlib:println(usage());

        [] ->
            emit(read_stdin());

        [<<"-"/utf8>>] ->
            emit(read_stdin());

        [Path] ->
            emit(read_file(Path));

        _ ->
            gleam_stdlib:println_error(
                <<"error: expected a single file path, `-`, or no input"/utf8>>
            ),
            gleam_stdlib:println_error(usage())
    end.