-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.