Current section

Files

Jump to
graded src graded.erl
Raw

src/graded.erl

-module(graded).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/graded.gleam").
-export([infer_path_dep/2, run/1, run_format/1, run_format_check/1, run_format_stdin/1, run_infer/1, main/0]).
-export_type([graded_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(
" Effect checker for Gleam via sidecar `.graded` annotation files.\n"
"\n"
" graded verifies that your Gleam functions respect their declared effect\n"
" budgets. Annotations live in `.graded` sidecar files alongside your source\n"
" — your Gleam code stays clean.\n"
"\n"
" ## Usage\n"
"\n"
" ```sh\n"
" gleam run -m graded check [directory] # enforce check annotations (default)\n"
" gleam run -m graded infer [directory] # infer and write effect annotations\n"
" gleam run -m graded format [directory] # normalize .graded file formatting\n"
" ```\n"
"\n"
" ## Programmatic API\n"
"\n"
" Use `run` to check a directory and get back a list of `CheckResult` values,\n"
" each containing any violations found per file. Use `run_infer` to infer\n"
" effects and write `.graded` files.\n"
"\n"
).
-type graded_error() :: {directory_read_error,
binary(),
simplifile:file_error()} |
{file_read_error, binary(), simplifile:file_error()} |
{file_write_error, binary(), simplifile:file_error()} |
{directory_create_error, binary(), simplifile:file_error()} |
{gleam_parse_error, binary(), glance:error()} |
{graded_parse_error, binary(), graded@internal@annotation:parse_error()} |
{invalid_config, binary(), graded@internal@config:config_error()} |
{format_check_failed, list(binary())} |
{cyclic_imports, list(binary())}.
-file("src/graded.gleam", 1261).
-spec target_directory(list(binary())) -> binary().
target_directory(Arguments) ->
case Arguments of
[Directory | _] ->
Directory;
[] ->
<<"src"/utf8>>
end.
-file("src/graded.gleam", 1328).
-spec format_error(graded_error()) -> binary().
format_error(Error) ->
case Error of
{directory_read_error, Path, _} ->
<<"Could not read directory: "/utf8, Path/binary>>;
{file_read_error, Path@1, _} ->
<<"Could not read: "/utf8, Path@1/binary>>;
{file_write_error, Path@2, _} ->
<<"Could not write: "/utf8, Path@2/binary>>;
{directory_create_error, Path@3, _} ->
<<"Could not create directory: "/utf8, Path@3/binary>>;
{gleam_parse_error, Path@4, _} ->
<<"Could not parse: "/utf8, Path@4/binary>>;
{graded_parse_error, Path@5, _} ->
<<"Parse error in .graded file for: "/utf8, Path@5/binary>>;
{invalid_config, Path@6, _} ->
<<"Invalid gleam.toml: "/utf8, Path@6/binary>>;
{format_check_failed, Paths} ->
<<"Unformatted .graded files:\n"/utf8,
(gleam@string:join(
gleam@list:map(
Paths,
fun(Path@7) -> <<" "/utf8, Path@7/binary>> end
),
<<"\n"/utf8>>
))/binary>>;
{cyclic_imports, Modules} ->
<<"Cyclic project imports detected (this should be unreachable — Gleam disallows circular imports):\n"/utf8,
(gleam@string:join(
gleam@list:map(
Modules,
fun(M) -> <<" "/utf8, M/binary>> end
),
<<"\n"/utf8>>
))/binary>>
end.
-file("src/graded.gleam", 1352).
-spec print_violation(binary(), graded@internal@types:violation()) -> nil.
print_violation(File, Violation) ->
Base = <<<<<<<<<<<<<<<<<<<<File/binary, ": "/utf8>>/binary,
(erlang:element(2, Violation))/binary>>/binary,
" calls "/utf8>>/binary,
(erlang:element(2, erlang:element(3, Violation)))/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(3, erlang:element(3, Violation)))/binary>>/binary,
" with effects "/utf8>>/binary,
(graded@internal@effects:format_effect_set(
erlang:element(6, Violation)
))/binary>>/binary,
" but declared "/utf8>>/binary,
(graded@internal@effects:format_effect_set(erlang:element(5, Violation)))/binary>>,
Hint = case graded@internal@types:has_variables(
erlang:element(6, Violation)
) of
true ->
<<<<<<"\n hint: actual effects contain unresolved variables; add a `check "/utf8,
(erlang:element(2, Violation))/binary>>/binary,
"(<param>: [...])` bound, or pass a function reference / constructor"/utf8>>/binary,
" whose effects are known"/utf8>>;
false ->
<<""/utf8>>
end,
gleam_stdlib:println(<<Base/binary, Hint/binary>>).
-file("src/graded.gleam", 1346).
-spec print_violations(graded@internal@types:check_result()) -> nil.
print_violations(Check_result) ->
gleam@list:each(
erlang:element(3, Check_result),
fun(Violation) ->
print_violation(erlang:element(2, Check_result), Violation)
end
).
-file("src/graded.gleam", 1385).
-spec print_warning(binary(), graded@internal@types:warning()) -> nil.
print_warning(File, Warning) ->
case Warning of
{untracked_effect_warning, Function, Reference, _, Effs} ->
gleam_stdlib:println(
<<<<<<<<<<<<<<<<<<File/binary, ": warning: "/utf8>>/binary,
Function/binary>>/binary,
" passes "/utf8>>/binary,
(erlang:element(2, Reference))/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(3, Reference))/binary>>/binary,
" as a value — its effects "/utf8>>/binary,
(graded@internal@effects:format_effect_set(Effs))/binary>>/binary,
" won't be tracked"/utf8>>
);
{unmatched_field_bound_warning,
Function@1,
Field_path,
Receiver_is_param} ->
Cause = case Receiver_is_param of
true ->
<<" matches no field call in its body — check the path"/utf8>>;
false ->
<<" matches no field call in its body — check the path, or the receiver is traced to a construction site and resolved through value provenance (field bounds apply only to untraceable receivers)"/utf8>>
end,
gleam_stdlib:println(
<<<<<<<<<<File/binary, ": warning: field bound "/utf8>>/binary,
Field_path/binary>>/binary,
" on "/utf8>>/binary,
Function@1/binary>>/binary,
Cause/binary>>
);
{unmatched_param_bound_warning, Function@2, Param} ->
gleam_stdlib:println(
<<<<<<<<<<File/binary, ": warning: parameter bound "/utf8>>/binary,
Param/binary>>/binary,
" on "/utf8>>/binary,
Function@2/binary>>/binary,
" names no parameter of the function — check the name"/utf8>>
)
end.
-file("src/graded.gleam", 1379).
-spec print_warnings(graded@internal@types:check_result()) -> nil.
print_warnings(Check_result) ->
gleam@list:each(
erlang:element(4, Check_result),
fun(Warning) ->
print_warning(erlang:element(2, Check_result), Warning)
end
).
-file("src/graded.gleam", 920).
-spec check_one_file(
binary(),
binary(),
glance:module_(),
list(graded@internal@types:effect_annotation()),
graded@internal@effects:knowledge_base(),
graded@internal@signatures:signature_registry(),
gleam@dict:dict({integer(), integer()}, girard@types:type()),
gleam@dict:dict(binary(), gleam@set:set(binary()))
) -> graded@internal@types:check_result().
check_one_file(
Gleam_path,
Module_path,
Module,
Module_checks,
Knowledge_base,
Registry,
Module_types,
Girard_fn_typed
) ->
{Violations, Warnings} = graded@internal@checker:check(
Module,
Module_path,
Module_checks,
Knowledge_base,
Registry,
Module_types,
Girard_fn_typed
),
{check_result, Gleam_path, Violations, Warnings}.
-file("src/graded.gleam", 430).
-spec qualify_by_module(
gleam@dict:dict(binary(), {binary(), glance:module_()}),
fun((glance:module_()) -> gleam@dict:dict(binary(), TPX))
) -> gleam@dict:dict({binary(), binary()}, TPX).
qualify_by_module(Index, Per_module) ->
gleam@dict:fold(
Index,
maps:new(),
fun(Acc, Path, Entry) ->
{_, Module} = Entry,
gleam@dict:fold(
Per_module(Module),
Acc,
fun(Inner, Name, Value) ->
gleam@dict:insert(Inner, {Path, Name}, Value)
end
)
end
).
-file("src/graded.gleam", 531).
-spec merge_field_effect(
graded@internal@types:type_field_effect(),
graded@internal@types:type_field_effect()
) -> graded@internal@types:type_field_effect().
merge_field_effect(Existing, New) ->
{Bounds, Source} = case erlang:element(4, Existing) of
{some, _} ->
{erlang:element(3, Existing), erlang:element(4, Existing)};
none ->
{erlang:element(3, New), erlang:element(4, New)}
end,
{type_field_effect,
graded@internal@effect_term:normalize(
{t_union, [erlang:element(2, Existing), erlang:element(2, New)]}
),
Bounds,
Source}.
-file("src/graded.gleam", 517).
-spec field_value_function(graded@internal@types:argument_value(), binary()) -> gleam@option:option(graded@internal@types:qualified_name()).
field_value_function(Value, Module_path) ->
case Value of
{function_ref, Name} ->
{some, Name};
{local_ref, Name@1} ->
{some, {qualified_name, Module_path, Name@1}};
_ ->
none
end.
-file("src/graded.gleam", 477).
-spec field_effect_of(
graded@internal@effects:knowledge_base(),
graded@internal@types:argument_value(),
binary(),
fun((list(binary()), list(glance:statement())) -> graded@internal@types:effect_term())
) -> graded@internal@types:type_field_effect().
field_effect_of(Knowledge_base, Value, Module_path, Closure_effect) ->
case field_value_function(Value, Module_path) of
{some, Name} ->
Field_effects = graded@internal@effects:lookup_effects(
Knowledge_base,
Name
),
case gleam@set:is_empty(
graded@internal@effect_term:free_vars(Field_effects)
) of
true ->
{type_field_effect, Field_effects, [], none};
false ->
{type_field_effect,
Field_effects,
graded@internal@effects:lookup_param_bounds(
Knowledge_base,
Name
),
{some, Name}}
end;
none ->
case Value of
{closure, Params, _, Body} ->
{type_field_effect, Closure_effect(Params, Body), [], none};
_ ->
{type_field_effect,
graded@internal@effects:argument_value_effects(
Knowledge_base,
Value
),
[],
none}
end
end.
-file("src/graded.gleam", 447).
-spec accumulate_constructor_binding(
gleam@dict:dict({binary(), binary(), binary()}, graded@internal@types:type_field_effect()),
graded@internal@extract:constructor_binding(),
gleam@dict:dict({binary(), binary()}, binary()),
graded@internal@effects:knowledge_base(),
binary(),
fun((list(binary()), list(glance:statement())) -> graded@internal@types:effect_term())
) -> gleam@dict:dict({binary(), binary(), binary()}, graded@internal@types:type_field_effect()).
accumulate_constructor_binding(
Acc,
Binding,
Constructor_types,
Knowledge_base,
Module_path,
Closure_effect
) ->
{constructor_binding, Binding_module, Constructor, Fields} = Binding,
Module = gleam@option:unwrap(Binding_module, Module_path),
case gleam_stdlib:map_get(Constructor_types, {Module, Constructor}) of
{error, nil} ->
Acc;
{ok, Type_name} ->
gleam@dict:fold(
Fields,
Acc,
fun(Inner, Label, Value) ->
Field_effect = field_effect_of(
Knowledge_base,
Value,
Module_path,
Closure_effect
),
Key = {Module, Type_name, Label},
Merged = case gleam_stdlib:map_get(Inner, Key) of
{ok, Existing} ->
merge_field_effect(Existing, Field_effect);
{error, nil} ->
Field_effect
end,
gleam@dict:insert(Inner, Key, Merged)
end
)
end.
-file("src/graded.gleam", 372).
-spec build_constructor_field_index(
gleam@dict:dict(binary(), {binary(), glance:module_()}),
graded@internal@effects:knowledge_base()
) -> list({{binary(), binary(), binary()},
graded@internal@types:type_field_effect()}).
build_constructor_field_index(Index, Knowledge_base) ->
Constructor_types = qualify_by_module(
Index,
fun graded@internal@extract:build_constructor_type_map/1
),
Cross_constructors = qualify_by_module(
Index,
fun graded@internal@extract:constructor_label_map/1
),
_pipe@2 = gleam@dict:fold(
Index,
maps:new(),
fun(Acc, Path, Entry) ->
{_, Module} = Entry,
Context = begin
_pipe = graded@internal@extract:build_import_context(Module),
graded@internal@extract:with_cross_constructors(
_pipe,
Cross_constructors
)
end,
Function_map = graded@internal@checker:build_function_map(Module),
Scc_ids = graded@internal@checker:build_scc_ids(
Module,
Context,
maps:new(),
false
),
Closure_effect = fun(Params, Body) ->
graded@internal@checker:closure_field_operator(
Params,
Body,
Context,
Function_map,
Knowledge_base,
Scc_ids
)
end,
_pipe@1 = graded@internal@extract:collect_constructor_bindings(
Module,
Context
),
gleam@list:fold(
_pipe@1,
Acc,
fun(Inner, Binding) ->
accumulate_constructor_binding(
Inner,
Binding,
Constructor_types,
Knowledge_base,
Path,
Closure_effect
)
end
)
end
),
maps:to_list(_pipe@2).
-file("src/graded.gleam", 1121).
-spec fold_inferred_into_kb(
graded@internal@effects:knowledge_base(),
gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()),
gleam@dict:dict(graded@internal@types:qualified_name(), list(graded@internal@types:param_bound())),
gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term())
) -> graded@internal@effects:knowledge_base().
fold_inferred_into_kb(Knowledge_base, Effs, Params, Returns) ->
_pipe = Knowledge_base,
_pipe@1 = graded@internal@effects:with_inferred(_pipe, Effs),
_pipe@2 = graded@internal@effects:with_inferred_params(_pipe@1, Params),
graded@internal@effects:with_inferred_returned_operators(_pipe@2, Returns).
-file("src/graded.gleam", 833).
-spec qualified_inferred(
list(graded@internal@types:effect_annotation()),
gleam@dict:dict(binary(), graded@internal@types:effect_term()),
binary()
) -> {gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()),
gleam@dict:dict(graded@internal@types:qualified_name(), list(graded@internal@types:param_bound())),
gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term())}.
qualified_inferred(Inferred, Returned_operators, Module_path) ->
Qualify = fun(Function) -> {qualified_name, Module_path, Function} end,
Effects_dict = gleam@list:fold(
Inferred,
maps:new(),
fun(Acc, Ann) ->
gleam@dict:insert(
Acc,
Qualify(erlang:element(3, Ann)),
erlang:element(5, Ann)
)
end
),
Params_dict = gleam@list:fold(
Inferred,
maps:new(),
fun(Acc@1, Ann@1) -> case erlang:element(4, Ann@1) of
[] ->
Acc@1;
Params ->
gleam@dict:insert(
Acc@1,
Qualify(erlang:element(3, Ann@1)),
Params
)
end end
),
Returns_dict = gleam@dict:fold(
Returned_operators,
maps:new(),
fun(Acc@2, Function@1, Op) ->
gleam@dict:insert(Acc@2, Qualify(Function@1), Op)
end
),
{Effects_dict, Params_dict, Returns_dict}.
-file("src/graded.gleam", 817).
-spec thread_inferred_into_kb(
graded@internal@effects:knowledge_base(),
list(graded@internal@types:effect_annotation()),
gleam@dict:dict(binary(), graded@internal@types:effect_term()),
binary()
) -> graded@internal@effects:knowledge_base().
thread_inferred_into_kb(
Knowledge_base,
Inferred,
Returned_operators,
Module_path
) ->
{Effects_dict, Params_dict, Returns_dict} = qualified_inferred(
Inferred,
Returned_operators,
Module_path
),
fold_inferred_into_kb(
Knowledge_base,
Effects_dict,
Params_dict,
Returns_dict
).
-file("src/graded.gleam", 794).
-spec fold_inferred_module(
graded@internal@effects:knowledge_base(),
glance:module_(),
binary(),
graded@internal@signatures:signature_registry(),
graded@internal@typeinfo:type_info()
) -> graded@internal@effects:knowledge_base().
fold_inferred_module(Kb, Module, Module_path, Registry, Type_info) ->
{Inferred, Returned_operators} = graded@internal@checker:infer_with_returns(
Module,
Module_path,
Kb,
[],
Registry,
graded@internal@typeinfo:for_module(Type_info, Module_path),
graded@internal@typeinfo:fn_typed_for_module(Type_info, Module_path)
),
thread_inferred_into_kb(Kb, Inferred, Returned_operators, Module_path).
-file("src/graded.gleam", 674).
-spec build_dependency_graph(
gleam@dict:dict(binary(), {binary(), glance:module_()})
) -> gleam@dict:dict(binary(), gleam@set:set(binary())).
build_dependency_graph(Index) ->
gleam@dict:map_values(
Index,
fun(_, Entry) ->
{_, Module} = Entry,
Context = graded@internal@extract:build_import_context(Module),
_pipe = erlang:element(3, Context),
_pipe@1 = maps:values(_pipe),
_pipe@2 = gleam@list:filter(
_pipe@1,
fun(Imported) -> gleam@dict:has_key(Index, Imported) end
),
gleam@set:from_list(_pipe@2)
end
).
-file("src/graded.gleam", 772).
-spec infer_project_in_memory(
graded@internal@effects:knowledge_base(),
gleam@dict:dict(binary(), {binary(), glance:module_()}),
graded@internal@signatures:signature_registry(),
graded@internal@typeinfo:type_info()
) -> graded@internal@effects:knowledge_base().
infer_project_in_memory(Base_kb, Index, Registry, Type_info) ->
case graded@internal@topo:sort(build_dependency_graph(Index)) of
{error, _} ->
Base_kb;
{ok, Sorted} ->
gleam@list:fold(
Sorted,
Base_kb,
fun(Kb, Module_path) ->
case gleam_stdlib:map_get(Index, Module_path) of
{error, _} ->
Kb;
{ok, {_, Module}} ->
fold_inferred_module(
Kb,
Module,
Module_path,
Registry,
Type_info
)
end
end
)
end.
-file("src/graded.gleam", 1213).
-spec infer_path_dep_module(
{gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()),
gleam@dict:dict(graded@internal@types:qualified_name(), list(graded@internal@types:param_bound())),
gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()),
graded@internal@effects:knowledge_base()},
binary(),
gleam@dict:dict(binary(), {glance:module_(),
list(graded@internal@types:effect_annotation())}),
graded@internal@signatures:signature_registry()
) -> {gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()),
gleam@dict:dict(graded@internal@types:qualified_name(), list(graded@internal@types:param_bound())),
gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()),
graded@internal@effects:knowledge_base()}.
infer_path_dep_module(State, Module_path, Index, Registry) ->
{Eff_acc, Param_acc, Returns_acc, Kb} = State,
case gleam_stdlib:map_get(Index, Module_path) of
{error, _} ->
State;
{ok, {Module, Checks}} ->
{Annotations, Returned_operators} = graded@internal@checker:infer_with_returns(
Module,
Module_path,
Kb,
Checks,
Registry,
maps:new(),
maps:new()
),
{Module_effects, Module_params, Module_returns} = qualified_inferred(
Annotations,
Returned_operators,
Module_path
),
New_kb = fold_inferred_into_kb(
Kb,
Module_effects,
Module_params,
Module_returns
),
{maps:merge(Eff_acc, Module_effects),
maps:merge(Param_acc, Module_params),
maps:merge(Returns_acc, Module_returns),
New_kb}
end.
-file("src/graded.gleam", 1309).
-spec read_and_parse_gleam(binary()) -> {ok, glance:module_()} |
{error, graded_error()}.
read_and_parse_gleam(Gleam_path) ->
gleam@result:'try'(
begin
_pipe = simplifile:read(Gleam_path),
gleam@result:map_error(
_pipe,
fun(_capture) -> {file_read_error, Gleam_path, _capture} end
)
end,
fun(Source) -> _pipe@1 = glance:module(Source),
gleam@result:map_error(
_pipe@1,
fun(_capture@1) ->
{gleam_parse_error, Gleam_path, _capture@1}
end
) end
).
-file("src/graded.gleam", 1145).
?DOC(
" Build the dependency-graph index for a single path dep, topo-sort it,\n"
" then infer every module in dependency order. Returns the union of all\n"
" inferred effects, polymorphic param bounds, and returned-operator\n"
" signatures keyed by `QualifiedName` so the caller can fold them into the\n"
" global knowledge base. Errors are swallowed (returned as `Error(Nil)`) to\n"
" preserve the existing tolerance: a malformed dep shouldn't break the whole\n"
" project.\n"
"\n"
" Exposed (pub) primarily so tests can exercise the topological-order path\n"
" inference on a temporary directory tree without going through\n"
" `gleam.toml` resolution. Production callers go through\n"
" `enrich_with_path_deps` which reads `gleam.toml` to discover dep paths.\n"
).
-spec infer_path_dep(binary(), graded@internal@effects:knowledge_base()) -> {ok,
{gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()),
gleam@dict:dict(graded@internal@types:qualified_name(), list(graded@internal@types:param_bound())),
gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term())}} |
{error, nil}.
infer_path_dep(Dep_path, Base_kb) ->
Source_dir = <<Dep_path/binary, "/src"/utf8>>,
Gleam_files = case simplifile:get_files(Source_dir) of
{ok, Found} ->
gleam@list:filter(
Found,
fun(Path) ->
gleam_stdlib:string_ends_with(Path, <<".gleam"/utf8>>)
end
);
{error, _} ->
[]
end,
Entries = gleam@list:filter_map(
Gleam_files,
fun(Gleam_path) ->
gleam@result:'try'(
begin
_pipe = read_and_parse_gleam(Gleam_path),
gleam@result:map_error(_pipe, fun(_) -> nil end)
end,
fun(Module) ->
Module_path = graded@internal@config:module_path_for_source(
Gleam_path,
Source_dir
),
{ok, {Module_path, Module, []}}
end
)
end
),
Index = gleam@list:fold(
Entries,
maps:new(),
fun(Acc, Entry) ->
{Module_path@1, Module@1, Checks} = Entry,
gleam@dict:insert(Acc, Module_path@1, {Module@1, Checks})
end
),
Graph = gleam@dict:map_values(
Index,
fun(_, Entry@1) ->
{Module@2, _} = Entry@1,
Context = graded@internal@extract:build_import_context(Module@2),
_pipe@1 = erlang:element(3, Context),
_pipe@2 = maps:values(_pipe@1),
_pipe@3 = gleam@list:filter(
_pipe@2,
fun(Imported) -> gleam@dict:has_key(Index, Imported) end
),
gleam@set:from_list(_pipe@3)
end
),
Registry = gleam@dict:fold(
Index,
graded@internal@signatures:empty(),
fun(Acc@1, Module_path@2, Entry@2) ->
{Module@3, _} = Entry@2,
graded@internal@signatures:merge(
Acc@1,
graded@internal@signatures:from_glance_module(
Module_path@2,
Module@3
)
)
end
),
gleam@result:'try'(
begin
_pipe@4 = graded@internal@topo:sort(Graph),
gleam@result:map_error(_pipe@4, fun(_) -> nil end)
end,
fun(Sorted) ->
{Effs, Params, Returns, _} = gleam@list:fold(
Sorted,
{maps:new(), maps:new(), maps:new(), Base_kb},
fun(State, Module_path@3) ->
infer_path_dep_module(State, Module_path@3, Index, Registry)
end
),
{ok, {Effs, Params, Returns}}
end
).
-file("src/graded.gleam", 1031).
-spec resolve_path(binary(), binary()) -> binary().
resolve_path(Root, Path) ->
gleam@bool:guard(
gleam_stdlib:string_starts_with(Path, <<"/"/utf8>>) orelse (Root =:= <<"."/utf8>>),
Path,
fun() -> filepath:join(Root, Path) end
).
-file("src/graded.gleam", 1071).
-spec enrich_with_path_deps(graded@internal@effects:knowledge_base(), binary()) -> graded@internal@effects:knowledge_base().
enrich_with_path_deps(Knowledge_base, Package_root) ->
Path_deps = graded@internal@effects:parse_path_dependencies(
filepath:join(Package_root, <<"gleam.toml"/utf8>>)
),
gleam@list:fold(
Path_deps,
Knowledge_base,
fun(Kb, Dep) ->
{Name, Dep_path} = Dep,
Resolved_dep_path = resolve_path(Package_root, Dep_path),
Spec_path = graded@internal@config:spec_file_for(
Resolved_dep_path,
Name
),
case simplifile_erl:is_file(Spec_path) of
{ok, true} ->
{Effs, Params, Returns} = graded@internal@effects:load_dep_spec(
Resolved_dep_path,
Name
),
fold_inferred_into_kb(Kb, Effs, Params, Returns);
_ ->
case infer_path_dep(Resolved_dep_path, Kb) of
{error, nil} ->
Kb;
{ok, {Effs@1, Params@1, Returns@1}} ->
fold_inferred_into_kb(
Kb,
Effs@1,
Params@1,
Returns@1
)
end
end
end
).
-file("src/graded.gleam", 1001).
-spec manifest_path(binary()) -> binary().
manifest_path(Package_root) ->
filepath:join(Package_root, <<"manifest.toml"/utf8>>).
-file("src/graded.gleam", 995).
-spec packages_dir(binary()) -> binary().
packages_dir(Package_root) ->
filepath:join(Package_root, <<"build/packages"/utf8>>).
-file("src/graded.gleam", 617).
-spec fn_typed_names(glance:function_(), list(girard@types:type())) -> gleam@set:set(binary()).
fn_typed_names(Function, Argument_types) ->
gleam@bool:guard(
erlang:length(erlang:element(5, Function)) /= erlang:length(
Argument_types
),
gleam@set:new(),
fun() ->
_pipe = gleam@list:zip(erlang:element(5, Function), Argument_types),
_pipe@1 = gleam@list:filter_map(
_pipe,
fun(Pair) ->
{Parameter, Argument_type} = Pair,
case {Argument_type, erlang:element(3, Parameter)} of
{{fn, _, _}, {named, Parameter_name}} ->
{ok, Parameter_name};
{_, _} ->
{error, nil}
end
end
),
gleam@set:from_list(_pipe@1)
end
).
-file("src/graded.gleam", 597).
-spec fn_typed_params_from_schemes(girard:module_result(), glance:module_()) -> gleam@dict:dict(binary(), gleam@set:set(binary())).
fn_typed_params_from_schemes(Module_result, Module) ->
Function_map = gleam@list:fold(
erlang:element(6, Module),
maps:new(),
fun(Acc, Definition) ->
gleam@dict:insert(
Acc,
erlang:element(3, erlang:element(3, Definition)),
erlang:element(3, Definition)
)
end
),
gleam@list:fold(
erlang:element(2, erlang:element(2, Module_result)),
maps:new(),
fun(Acc@1, Entry) ->
{Name, Scheme} = Entry,
case {erlang:element(3, Scheme),
gleam_stdlib:map_get(Function_map, Name)} of
{{fn, Argument_types, _}, {ok, Function}} ->
gleam@dict:insert(
Acc@1,
Name,
fn_typed_names(Function, Argument_types)
);
{_, _} ->
Acc@1
end
end
).
-file("src/graded.gleam", 643).
-spec build_girard_resolver(
gleam@dict:dict(binary(), {binary(), glance:module_()})
) -> fun((binary()) -> {ok, binary()} | {error, nil}).
build_girard_resolver(Index) ->
Disk = girard:disk_resolver(),
fun(Module_path) -> case gleam_stdlib:map_get(Index, Module_path) of
{ok, {Gleam_path, _}} ->
_pipe = simplifile:read(Gleam_path),
gleam@result:replace_error(_pipe, nil);
{error, nil} ->
Disk(Module_path)
end end.
-file("src/graded.gleam", 550).
-spec build_type_index(gleam@dict:dict(binary(), {binary(), glance:module_()})) -> graded@internal@typeinfo:type_info().
build_type_index(Index) ->
Options = begin
_pipe = girard:default_options(),
girard:with_resolver(_pipe, build_girard_resolver(Index))
end,
Entries = begin
_pipe@1 = maps:to_list(Index),
gleam@list:map(
_pipe@1,
fun(Pair) ->
{Module_path, {_, Module}} = Pair,
{Module_path, Module}
end
)
end,
Results = begin
_pipe@2 = girard:annotate_package(Entries, Options),
maps:to_list(_pipe@2)
end,
Span_types = gleam@list:map(
Results,
fun(Pair@1) ->
{Module_path@1, Module_result} = Pair@1,
Types = gleam@list:fold(
erlang:element(4, erlang:element(2, Module_result)),
maps:new(),
fun(Acc, Annotation) ->
gleam@dict:insert(
Acc,
{erlang:element(2, erlang:element(2, Annotation)),
erlang:element(3, erlang:element(2, Annotation))},
erlang:element(3, Annotation)
)
end
),
{Module_path@1, Types}
end
),
Fn_typed = gleam@list:filter_map(
Results,
fun(Pair@2) ->
{Module_path@2, Module_result@1} = Pair@2,
case gleam_stdlib:map_get(Index, Module_path@2) of
{ok, {_, Module@1}} ->
{ok,
{Module_path@2,
fn_typed_params_from_schemes(
Module_result@1,
Module@1
)}};
{error, nil} ->
{error, nil}
end
end
),
graded@internal@typeinfo:from_modules(Span_types, Fn_typed).
-file("src/graded.gleam", 357).
-spec build_project_registry(
gleam@dict:dict(binary(), {binary(), glance:module_()})
) -> graded@internal@signatures:signature_registry().
build_project_registry(Index) ->
gleam@dict:fold(
Index,
graded@internal@signatures:empty(),
fun(Acc, Module_path, Entry) ->
{_, Module} = Entry,
graded@internal@signatures:merge(
Acc,
graded@internal@signatures:from_glance_module(
Module_path,
Module
)
)
end
).
-file("src/graded.gleam", 1104).
-spec path_dep_registry(binary()) -> graded@internal@signatures:signature_registry().
path_dep_registry(Package_root) ->
_pipe = graded@internal@effects:parse_path_dependencies(
filepath:join(Package_root, <<"gleam.toml"/utf8>>)
),
gleam@list:fold(
_pipe,
graded@internal@signatures:empty(),
fun(Acc, Dep) ->
{_, Dep_path} = Dep,
Resolved_dep_path = resolve_path(Package_root, Dep_path),
graded@internal@signatures:merge(
Acc,
graded@internal@signatures:load_from_source_dir(
<<Resolved_dep_path/binary, "/src"/utf8>>
)
)
end
).
-file("src/graded.gleam", 659).
-spec build_module_index(list({binary(), glance:module_()}), binary()) -> gleam@dict:dict(binary(), {binary(),
glance:module_()}).
build_module_index(Parsed, Directory) ->
gleam@list:fold(
Parsed,
maps:new(),
fun(Acc, Entry) ->
{Gleam_path, Module} = Entry,
Module_path = graded@internal@config:module_path_for_source(
Gleam_path,
Directory
),
gleam@dict:insert(Acc, Module_path, {Gleam_path, Module})
end
).
-file("src/graded.gleam", 345).
-spec parse_all_files(list(binary())) -> {ok,
list({binary(), glance:module_()})} |
{error, graded_error()}.
parse_all_files(Gleam_files) ->
gleam@list:try_map(
Gleam_files,
fun(Gleam_path) ->
gleam@result:'try'(
read_and_parse_gleam(Gleam_path),
fun(Module) -> {ok, {Gleam_path, Module}} end
)
end
).
-file("src/graded.gleam", 1303).
-spec find_gleam_files(binary()) -> {ok, list(binary())} |
{error, graded_error()}.
find_gleam_files(Directory) ->
_pipe = simplifile:get_files(Directory),
_pipe@1 = gleam@result:map_error(
_pipe,
fun(_capture) -> {directory_read_error, Directory, _capture} end
),
gleam@result:map(
_pipe@1,
fun(_capture@1) ->
gleam@list:filter(
_capture@1,
fun(Path) ->
gleam_stdlib:string_ends_with(Path, <<".gleam"/utf8>>)
end
)
end
).
-file("src/graded.gleam", 900).
-spec checks_grouped_by_module(graded@internal@types:graded_file()) -> gleam@dict:dict(binary(), list(graded@internal@types:effect_annotation())).
checks_grouped_by_module(Spec) ->
gleam@list:fold(
graded@internal@annotation:extract_checks(Spec),
maps:new(),
fun(Acc, Ann) ->
case graded@internal@annotation:split_qualified_name(
erlang:element(3, Ann)
) of
{error, _} ->
Acc;
{ok, {Module, Function}} ->
Bare = {effect_annotation,
erlang:element(2, Ann),
Function,
erlang:element(4, Ann),
erlang:element(5, Ann)},
Existing = case gleam_stdlib:map_get(Acc, Module) of
{ok, List} ->
List;
{error, _} ->
[]
end,
gleam@dict:insert(Acc, Module, [Bare | Existing])
end
end
).
-file("src/graded.gleam", 1048).
-spec read_spec(binary()) -> graded@internal@types:graded_file().
read_spec(Spec_path) ->
case simplifile:read(Spec_path) of
{error, _} ->
{graded_file, []};
{ok, Content} ->
case graded@internal@annotation:parse_file(Content) of
{ok, File} ->
File;
{error, _} ->
{graded_file, []}
end
end.
-file("src/graded.gleam", 1005).
-spec find_gleam_toml_dir(binary(), binary()) -> binary().
find_gleam_toml_dir(Dir, Original) ->
Dir@1 = case Dir of
<<""/utf8>> ->
<<"."/utf8>>;
_ ->
Dir
end,
case simplifile_erl:is_file(filepath:join(Dir@1, <<"gleam.toml"/utf8>>)) of
{ok, true} ->
Dir@1;
_ ->
Parent = case filepath:directory_name(Dir@1) of
<<""/utf8>> ->
<<"."/utf8>>;
Other ->
Other
end,
case Parent =:= Dir@1 of
true ->
Original;
false ->
find_gleam_toml_dir(Parent, Original)
end
end.
-file("src/graded.gleam", 987).
-spec source_root_for(binary()) -> binary().
source_root_for(Directory) ->
case Directory of
<<"src"/utf8>> ->
<<"."/utf8>>;
_ ->
Directory
end.
-file("src/graded.gleam", 978).
-spec resolve_package_root(binary()) -> binary().
resolve_package_root(Directory) ->
Source_root = source_root_for(Directory),
find_gleam_toml_dir(Source_root, Source_root).
-file("src/graded.gleam", 1039).
-spec default_package_name(binary()) -> binary().
default_package_name(Project_root) ->
case filepath:base_name(Project_root) of
<<""/utf8>> ->
<<"graded"/utf8>>;
<<"/"/utf8>> ->
<<"graded"/utf8>>;
<<"."/utf8>> ->
<<"graded"/utf8>>;
Name ->
Name
end.
-file("src/graded.gleam", 954).
-spec read_config(binary()) -> {ok, graded@internal@config:graded_config()} |
{error, graded_error()}.
read_config(Directory) ->
Project_root = source_root_for(Directory),
Toml_path = filepath:join(Project_root, <<"gleam.toml"/utf8>>),
gleam@result:'try'(case graded@internal@config:read(Toml_path) of
{ok, Cfg} ->
{ok, Cfg};
{error, {toml_read_error, _, _}} ->
{ok,
graded@internal@config:defaults_for(
default_package_name(Project_root)
)};
{error, Cause} ->
{error, {invalid_config, Toml_path, Cause}}
end, fun(Raw) ->
{ok,
{graded_config,
erlang:element(2, Raw),
resolve_path(Project_root, erlang:element(3, Raw)),
resolve_path(Project_root, erlang:element(4, Raw))}}
end).
-file("src/graded.gleam", 125).
?DOC(
" Run the checker on all .gleam files in a directory.\n"
"\n"
" Reads the project's single spec file (default `<package_name>.graded`)\n"
" to find inferred public-API effects, `check` invariants, `external`\n"
" hints, and `type` field annotations, then reports violations per source\n"
" file.\n"
).
-spec run(binary()) -> {ok, list(graded@internal@types:check_result())} |
{error, graded_error()}.
run(Directory) ->
gleam@result:'try'(
read_config(Directory),
fun(Cfg) ->
Package_root = resolve_package_root(Directory),
Spec = read_spec(erlang:element(3, Cfg)),
Checks_by_module = checks_grouped_by_module(Spec),
gleam@result:'try'(
find_gleam_files(Directory),
fun(Gleam_files) ->
gleam@result:'try'(
parse_all_files(Gleam_files),
fun(Parsed) ->
Index = build_module_index(Parsed, Directory),
Dep_registry = begin
_pipe = graded@internal@signatures:load_from_packages_dir(
packages_dir(Package_root)
),
graded@internal@signatures:merge(
_pipe,
path_dep_registry(Package_root)
)
end,
Registry = graded@internal@signatures:merge(
Dep_registry,
build_project_registry(Index)
),
Type_info = build_type_index(Index),
Kb_base = begin
_pipe@1 = graded@internal@effects:load_knowledge_base(
packages_dir(Package_root),
manifest_path(Package_root)
),
_pipe@2 = enrich_with_path_deps(
_pipe@1,
Package_root
),
_pipe@3 = graded@internal@effects:with_inferred(
_pipe@2,
graded@internal@effects:load_spec_effects_from_file(
Spec
)
),
_pipe@4 = graded@internal@effects:with_inferred_returned_operators(
_pipe@3,
graded@internal@effects:load_spec_returns_from_file(
Spec
)
),
_pipe@5 = graded@internal@effects:with_externals(
_pipe@4,
graded@internal@annotation:extract_externals(
Spec
)
),
infer_project_in_memory(
_pipe@5,
Index,
Registry,
Type_info
)
end,
Knowledge_base = begin
_pipe@6 = Kb_base,
_pipe@7 = graded@internal@effects:with_inferred_type_fields(
_pipe@6,
build_constructor_field_index(
Index,
Kb_base
)
),
_pipe@8 = graded@internal@effects:with_type_fields(
_pipe@7,
graded@internal@annotation:extract_type_fields(
Spec
)
),
graded@internal@effects:with_factories(
_pipe@8,
qualify_by_module(
Index,
fun graded@internal@extract:factory_map/1
)
)
end,
Results = gleam@list:map(
Parsed,
fun(Entry) ->
{Gleam_path, Module} = Entry,
Module_path = graded@internal@config:module_path_for_source(
Gleam_path,
Directory
),
Module_checks = case gleam_stdlib:map_get(
Checks_by_module,
Module_path
) of
{ok, List} ->
List;
{error, _} ->
[]
end,
check_one_file(
Gleam_path,
Module_path,
Module,
Module_checks,
Knowledge_base,
Registry,
graded@internal@typeinfo:for_module(
Type_info,
Module_path
),
graded@internal@typeinfo:fn_typed_for_module(
Type_info,
Module_path
)
)
end
),
{ok, Results}
end
)
end
)
end
).
-file("src/graded.gleam", 1268).
-spec run_check(binary()) -> nil.
run_check(Directory) ->
case run(Directory) of
{ok, Results} ->
Violations = gleam@list:flat_map(
Results,
fun(Check_result) -> erlang:element(3, Check_result) end
),
Warnings = gleam@list:flat_map(
Results,
fun(Check_result@1) -> erlang:element(4, Check_result@1) end
),
gleam@list:each(Results, fun print_warnings/1),
case Warnings of
[] ->
nil;
_ ->
gleam_stdlib:println(
<<<<"graded: "/utf8,
(erlang:integer_to_binary(
erlang:length(Warnings)
))/binary>>/binary,
" warning(s)"/utf8>>
)
end,
case Violations of
[] ->
gleam_stdlib:println(<<"graded: all checks passed"/utf8>>);
_ ->
gleam@list:each(Results, fun print_violations/1),
gleam_stdlib:println(
<<<<"\ngraded: "/utf8,
(erlang:integer_to_binary(
erlang:length(Violations)
))/binary>>/binary,
" violation(s) found"/utf8>>
),
erlang:halt(1)
end;
{error, Error} ->
gleam_stdlib:println_error(
<<"graded: error: "/utf8, (format_error(Error))/binary>>
),
erlang:halt(1)
end.
-file("src/graded.gleam", 328).
-spec format_one_spec(binary()) -> {ok, gleam@option:option(binary())} |
{error, graded_error()}.
format_one_spec(Spec_path) ->
case simplifile:read(Spec_path) of
{error, _} ->
{ok, none};
{ok, Content} ->
_pipe = graded@internal@annotation:parse_file(Content),
_pipe@1 = gleam@result:map(
_pipe,
fun(File) ->
{some, graded@internal@annotation:format_sorted(File)}
end
),
gleam@result:map_error(
_pipe@1,
fun(_capture) -> {graded_parse_error, Spec_path, _capture} end
)
end.
-file("src/graded.gleam", 286).
?DOC(
" Format the project's spec file in place. The spec file is the single\n"
" source of truth for hand-written `check`/`external`/`type` lines and\n"
" the inferred public-API effects.\n"
).
-spec run_format(binary()) -> {ok, nil} | {error, graded_error()}.
run_format(Directory) ->
gleam@result:'try'(
read_config(Directory),
fun(Cfg) ->
gleam@result:'try'(
format_one_spec(erlang:element(3, Cfg)),
fun(Formatted) -> case Formatted of
none ->
{ok, nil};
{some, Formatted@1} ->
_pipe = simplifile:write(
erlang:element(3, Cfg),
Formatted@1
),
gleam@result:map_error(
_pipe,
fun(_capture) ->
{file_write_error,
erlang:element(3, Cfg),
_capture}
end
)
end end
)
end
).
-file("src/graded.gleam", 309).
?DOC(
" Check that the project's spec file is already formatted. Returns error\n"
" with the file path if it isn't. Used by CI as `format --check`.\n"
).
-spec run_format_check(binary()) -> {ok, nil} | {error, graded_error()}.
run_format_check(Directory) ->
gleam@result:'try'(
read_config(Directory),
fun(Cfg) ->
gleam@result:'try'(
format_one_spec(erlang:element(3, Cfg)),
fun(Formatted) -> case Formatted of
none ->
{ok, nil};
{some, Formatted@1} ->
case simplifile:read(erlang:element(3, Cfg)) of
{error, _} ->
{ok, nil};
{ok, Content} ->
case Content =:= Formatted@1 of
true ->
{ok, nil};
false ->
{error,
{format_check_failed,
[erlang:element(3, Cfg)]}}
end
end
end end
)
end
).
-file("src/graded.gleam", 300).
?DOC(
" Format a `.graded` spec given as a string, as `graded format --stdin` does\n"
" for editor integration: parse the input, then sort and reformat it. Returns\n"
" the input's parse error if it doesn't parse.\n"
).
-spec run_format_stdin(binary()) -> {ok, binary()} |
{error, graded@internal@annotation:parse_error()}.
run_format_stdin(Input) ->
gleam@result:map(
graded@internal@annotation:parse_file(Input),
fun(File) -> graded@internal@annotation:format_sorted(File) end
).
-file("src/graded.gleam", 1320).
-spec write_graded_file(binary(), graded@internal@types:graded_file()) -> {ok,
nil} |
{error, graded_error()}.
write_graded_file(Path, Graded_file) ->
_pipe = simplifile:write(
Path,
graded@internal@annotation:format_file(Graded_file)
),
gleam@result:map_error(
_pipe,
fun(_capture) -> {file_write_error, Path, _capture} end
).
-file("src/graded.gleam", 875).
-spec write_spec_file(
binary(),
graded@internal@types:graded_file(),
list(graded@internal@types:effect_annotation()),
list(graded@internal@types:returns_annotation())
) -> {ok, nil} | {error, graded_error()}.
write_spec_file(Spec_path, Existing, Inferred, Inferred_returns) ->
Merged = graded@internal@annotation:merge_inferred(
Existing,
Inferred,
Inferred_returns
),
Parent = filepath:directory_name(Spec_path),
gleam@result:'try'(
case (Parent =:= <<""/utf8>>) orelse (Parent =:= <<"."/utf8>>) of
true ->
{ok, nil};
false ->
_pipe = simplifile:create_directory_all(Parent),
gleam@result:map_error(
_pipe,
fun(_capture) ->
{directory_create_error, Parent, _capture}
end
)
end,
fun(_use0) ->
nil = _use0,
write_graded_file(Spec_path, Merged)
end
).
-file("src/graded.gleam", 862).
-spec public_function_names(glance:module_()) -> gleam@set:set(binary()).
public_function_names(Module) ->
gleam@list:fold(
erlang:element(6, Module),
gleam@set:new(),
fun(Acc, Def) -> case erlang:element(4, erlang:element(3, Def)) of
public ->
gleam@set:insert(
Acc,
erlang:element(3, erlang:element(3, Def))
);
private ->
Acc
end end
).
-file("src/graded.gleam", 691).
-spec infer_one_module(
glance:module_(),
binary(),
binary(),
graded@internal@effects:knowledge_base(),
graded@internal@signatures:signature_registry(),
gleam@dict:dict({integer(), integer()}, girard@types:type()),
gleam@dict:dict(binary(), gleam@set:set(binary()))
) -> {ok,
{graded@internal@effects:knowledge_base(),
list(graded@internal@types:effect_annotation()),
list(graded@internal@types:returns_annotation())}} |
{error, graded_error()}.
infer_one_module(
Module,
Module_path,
Cache_dir,
Knowledge_base,
Registry,
Module_types,
Girard_fn_typed
) ->
{Inferred, Returned_operators} = graded@internal@checker:infer_with_returns(
Module,
Module_path,
Knowledge_base,
[],
Registry,
Module_types,
Girard_fn_typed
),
Cache_path = filepath:join(
Cache_dir,
<<Module_path/binary, ".graded"/utf8>>
),
gleam@result:'try'(case Inferred of
[] ->
{ok, nil};
_ ->
Parent_directory = filepath:directory_name(Cache_path),
gleam@result:'try'(
begin
_pipe = simplifile:create_directory_all(
Parent_directory
),
gleam@result:map_error(
_pipe,
fun(_capture) ->
{directory_create_error,
Parent_directory,
_capture}
end
)
end,
fun(_use0) ->
nil = _use0,
Cache_file = {graded_file,
gleam@list:map(
Inferred,
fun(Field@0) -> {annotation_line, Field@0} end
)},
write_graded_file(Cache_path, Cache_file)
end
)
end, fun(_use0@1) ->
nil = _use0@1,
New_kb = thread_inferred_into_kb(
Knowledge_base,
Inferred,
Returned_operators,
Module_path
),
Public_names = public_function_names(Module),
Public_annotations = begin
_pipe@1 = Inferred,
_pipe@2 = gleam@list:filter(
_pipe@1,
fun(Ann) ->
gleam@set:contains(Public_names, erlang:element(3, Ann))
end
),
gleam@list:map(
_pipe@2,
fun(Ann@1) ->
{effect_annotation,
erlang:element(2, Ann@1),
<<<<Module_path/binary, "."/utf8>>/binary,
(erlang:element(3, Ann@1))/binary>>,
erlang:element(4, Ann@1),
erlang:element(5, Ann@1)}
end
)
end,
Public_returns = begin
_pipe@3 = Returned_operators,
_pipe@4 = maps:to_list(_pipe@3),
_pipe@5 = gleam@list:filter(
_pipe@4,
fun(Pair) ->
gleam@set:contains(
Public_names,
erlang:element(1, Pair)
)
end
),
gleam@list:map(
_pipe@5,
fun(Pair@1) ->
{returns_annotation,
<<<<Module_path/binary, "."/utf8>>/binary,
(erlang:element(1, Pair@1))/binary>>,
erlang:element(2, Pair@1)}
end
)
end,
{ok, {New_kb, Public_annotations, Public_returns}}
end).
-file("src/graded.gleam", 203).
?DOC(
" Infer effects for all `.gleam` files in `directory`. Writes two outputs:\n"
"\n"
" 1. **Per-module cache files** under `<cache_dir>/<module_path>.graded`,\n"
" containing the inferred effects of every function in the module\n"
" (public + private). Regenerated freely; not shipped.\n"
"\n"
" 2. **One spec file** at `<spec_file>` containing the inferred effects of\n"
" every *public* function across all modules, plus any hand-written\n"
" `check`, `external effects`, or `type` annotations the user already\n"
" had in the spec file (those lines are preserved verbatim).\n"
"\n"
" Walks the project's import graph in topological order so each module is\n"
" analysed after every other project module it imports — a single pass\n"
" resolves transitive chains of any depth.\n"
).
-spec run_infer(binary()) -> {ok, nil} | {error, graded_error()}.
run_infer(Directory) ->
gleam@result:'try'(
read_config(Directory),
fun(Cfg) ->
Package_root = resolve_package_root(Directory),
Spec = read_spec(erlang:element(3, Cfg)),
gleam@result:'try'(
find_gleam_files(Directory),
fun(Gleam_files) ->
gleam@result:'try'(
parse_all_files(Gleam_files),
fun(Parsed) ->
Index = build_module_index(Parsed, Directory),
Kb_base = begin
_pipe = graded@internal@effects:load_knowledge_base(
packages_dir(Package_root),
manifest_path(Package_root)
),
_pipe@1 = enrich_with_path_deps(
_pipe,
Package_root
),
graded@internal@effects:with_externals(
_pipe@1,
graded@internal@annotation:extract_externals(
Spec
)
)
end,
Construction_kb = graded@internal@effects:with_inferred(
Kb_base,
graded@internal@effects:load_spec_effects_from_file(
Spec
)
),
Base_kb = begin
_pipe@2 = Kb_base,
_pipe@3 = graded@internal@effects:with_inferred_type_fields(
_pipe@2,
build_constructor_field_index(
Index,
Construction_kb
)
),
_pipe@4 = graded@internal@effects:with_type_fields(
_pipe@3,
graded@internal@annotation:extract_type_fields(
Spec
)
),
graded@internal@effects:with_factories(
_pipe@4,
qualify_by_module(
Index,
fun graded@internal@extract:factory_map/1
)
)
end,
Graph = build_dependency_graph(Index),
gleam@result:'try'(
begin
_pipe@5 = graded@internal@topo:sort(Graph),
gleam@result:map_error(
_pipe@5,
fun(Error) ->
{cycle, Nodes} = Error,
{cyclic_imports, Nodes}
end
)
end,
fun(Sorted) ->
Dep_registry = begin
_pipe@6 = graded@internal@signatures:load_from_packages_dir(
packages_dir(Package_root)
),
graded@internal@signatures:merge(
_pipe@6,
path_dep_registry(Package_root)
)
end,
Registry = graded@internal@signatures:merge(
Dep_registry,
build_project_registry(Index)
),
Type_info = build_type_index(Index),
gleam@result:'try'(
gleam@list:try_fold(
Sorted,
{Base_kb, [], []},
fun(State, Module_path) ->
{Kb, Acc, Returns_acc} = State,
case gleam_stdlib:map_get(
Index,
Module_path
) of
{error, _} ->
{ok, State};
{ok, {_, Module}} ->
gleam@result:'try'(
infer_one_module(
Module,
Module_path,
erlang:element(
4,
Cfg
),
Kb,
Registry,
graded@internal@typeinfo:for_module(
Type_info,
Module_path
),
graded@internal@typeinfo:fn_typed_for_module(
Type_info,
Module_path
)
),
fun(_use0) ->
{New_kb,
New_public,
New_returns} = _use0,
{ok,
{New_kb,
lists:append(
New_public,
Acc
),
lists:append(
New_returns,
Returns_acc
)}}
end
)
end
end
),
fun(_use0@1) ->
{_,
Public_annotations,
Public_returns} = _use0@1,
write_spec_file(
erlang:element(3, Cfg),
Spec,
Public_annotations,
Public_returns
)
end
)
end
)
end
)
end
)
end
).
-file("src/graded.gleam", 79).
-spec main() -> nil.
main() ->
Arguments = erlang:element(4, argv:load()),
case Arguments of
[<<"infer"/utf8>> | Rest] ->
case run_infer(target_directory(Rest)) of
{ok, nil} ->
gleam_stdlib:println(
<<"graded: inferred effects written"/utf8>>
);
{error, Error} ->
gleam_stdlib:println_error(
<<"graded: error: "/utf8, (format_error(Error))/binary>>
),
erlang:halt(1)
end;
[<<"format"/utf8>>, <<"--stdin"/utf8>> | _] ->
case run_format_stdin(graded_ffi:read_stdin()) of
{ok, Output} ->
gleam_stdlib:print(Output);
{error, _} ->
gleam_stdlib:println_error(
<<"graded: error: could not parse stdin"/utf8>>
),
erlang:halt(1)
end;
[<<"format"/utf8>>, <<"--check"/utf8>> | Rest@1] ->
case run_format_check(target_directory(Rest@1)) of
{ok, nil} ->
nil;
{error, Error@1} ->
gleam_stdlib:println_error(
<<"graded: error: "/utf8,
(format_error(Error@1))/binary>>
),
erlang:halt(1)
end;
[<<"format"/utf8>> | Rest@2] ->
case run_format(target_directory(Rest@2)) of
{ok, nil} ->
nil;
{error, Error@2} ->
gleam_stdlib:println_error(
<<"graded: error: "/utf8,
(format_error(Error@2))/binary>>
),
erlang:halt(1)
end;
[<<"check"/utf8>> | Rest@3] ->
run_check(target_directory(Rest@3));
_ ->
run_check(target_directory(Arguments))
end.