Current section
Files
Jump to
Current section
Files
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_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()} |
{format_check_failed, list(binary())} |
{cyclic_imports, list(binary())}.
-file("src/graded.gleam", 1083).
-spec target_directory(list(binary())) -> binary().
target_directory(Arguments) ->
case Arguments of
[Directory | _] ->
Directory;
[] ->
<<"src"/utf8>>
end.
-file("src/graded.gleam", 1150).
-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>>;
{format_check_failed, Paths} ->
<<"Unformatted .graded files:\n"/utf8,
(gleam@string:join(
gleam@list:map(
Paths,
fun(Path@6) -> <<" "/utf8, Path@6/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", 1173).
-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", 1167).
-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", 1206).
-spec print_warning(binary(), graded@internal@types:warning()) -> nil.
print_warning(File, Warning) ->
gleam_stdlib:println(
<<<<<<<<<<<<<<<<<<File/binary, ": warning: "/utf8>>/binary,
(erlang:element(2, Warning))/binary>>/binary,
" passes "/utf8>>/binary,
(erlang:element(2, erlang:element(3, Warning)))/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(3, erlang:element(3, Warning)))/binary>>/binary,
" as a value — its effects "/utf8>>/binary,
(graded@internal@effects:format_effect_set(
erlang:element(5, Warning)
))/binary>>/binary,
" won't be tracked"/utf8>>
).
-file("src/graded.gleam", 1200).
-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", 884).
?DOC(
" Run the checker against one source file using the slice of `check`\n"
" annotations from the spec file that mention this file's module.\n"
).
-spec check_one_file(
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,
Module_checks,
Knowledge_base,
Registry,
Module_types,
Girard_fn_typed
) ->
{Violations, Warnings} = graded@internal@checker:check(
Module,
Module_checks,
Knowledge_base,
Registry,
Module_types,
Girard_fn_typed
),
{check_result, Gleam_path, Violations, Warnings}.
-file("src/graded.gleam", 402).
?DOC(
" Build a package-wide map keyed by `#(defining module, name)` from a per-module\n"
" `name -> value` map, qualifying each entry with the module it came from.\n"
).
-spec qualify_by_module(
gleam@dict:dict(binary(), {binary(), glance:module_()}),
fun((glance:module_()) -> gleam@dict:dict(binary(), UNF))
) -> gleam@dict:dict({binary(), binary()}, UNF).
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", 501).
?DOC(
" Union two field-effect contributions for the same field across sites. Keeps\n"
" the first polymorphic source — conflicting polymorphism across sites is rare,\n"
" and unbound variables collapse to `[Unknown]` at the call site.\n"
).
-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", 487).
?DOC(
" The qualified function a field value refers to, if any: a `FunctionRef`\n"
" directly, or a `LocalRef` (a bare same-module name) qualified by the current\n"
" module. `None` for constructors and inline expressions.\n"
).
-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", 449).
?DOC(
" The effect a constructor field's value contributes. A function reference (or\n"
" a same-module function, qualified by `module_path`) resolves via the\n"
" knowledge base — capturing its param bounds + identity when it is\n"
" effect-polymorphic. A constructor is pure; anything else is `[Unknown]`.\n"
).
-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 not gleam@set:is_empty(
graded@internal@effect_term:free_vars(Field_effects)
) of
true ->
{type_field_effect,
Field_effects,
graded@internal@effects:lookup_param_bounds(
Knowledge_base,
Name
),
{some, Name}};
false ->
{type_field_effect, Field_effects, [], none}
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", 419).
?DOC(
" Fold one constructor call's field bindings into the (module, type, field) ->\n"
" effect accumulator, unioning with any effect already recorded for that field.\n"
" The defining module is the call's resolved module (qualified) or the current\n"
" module (unqualified) — so same-named constructors in different modules don't\n"
" collide.\n"
).
-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", 344).
?DOC(
" Stage C: derive `type Foo.field : [...]` annotations from constructor call\n"
" sites across the package. `Validator(to_error: io.println)` anywhere makes\n"
" `Validator.to_error` carry io.println's effects (unioned across all sites),\n"
" so a field call resolves without a hand-written annotation. Resolved via\n"
" girard's receiver typing at the use site; hand-written `type` lines still\n"
" win, since they are merged over these.\n"
).
-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", 787).
?DOC(
" Infer one module against `kb` and fold its effects, param bounds, and\n"
" returned operators (qualified by `module_path`) into the knowledge base, with\n"
" existing entries winning. The per-module step of `infer_project_in_memory`.\n"
).
-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,
Kb,
[],
Registry,
graded@internal@typeinfo:for_module(Type_info, Module_path),
graded@internal@typeinfo:fn_typed_for_module(Type_info, 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
),
_pipe = Kb,
_pipe@1 = graded@internal@effects:with_inferred(_pipe, Effects_dict),
_pipe@2 = graded@internal@effects:with_inferred_params(_pipe@1, Params_dict),
graded@internal@effects:with_inferred_returned_operators(
_pipe@2,
Returns_dict
).
-file("src/graded.gleam", 644).
?DOC(
" For every project module, derive its set of project-internal imports.\n"
" Imports of stdlib/dep modules (anything not in `index`) are filtered out\n"
" — those are leaves with effects already resolved via the knowledge base\n"
" and don't belong in the topological sort.\n"
).
-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(2, 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", 765).
?DOC(
" Infer project modules in topological order, in memory, folding their\n"
" effects, param bounds, and returned operators into `base_kb` — with existing\n"
" (spec / dependency) entries taking priority, so committed effects are never\n"
" overridden. This lets `check` resolve calls into project modules that haven't\n"
" been `graded infer`-ed yet, without writing the cache. Falls back to\n"
" `base_kb` unchanged when the import graph has a cycle (the real\n"
" `graded infer` reports that error; `check` just degrades to spec-only).\n"
).
-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", 1051).
-spec infer_path_dep_module(
{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())})
) -> {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) ->
{Acc, Kb} = State,
case gleam_stdlib:map_get(Index, Module_path) of
{error, _} ->
{Acc, Kb};
{ok, {Module, Checks}} ->
Annotations = graded@internal@checker:infer(
Module,
Kb,
Checks,
graded@internal@signatures:empty(),
maps:new(),
maps:new()
),
Module_dict = gleam@list:fold(
Annotations,
maps:new(),
fun(D, Annotation) ->
gleam@dict:insert(
D,
{qualified_name,
Module_path,
erlang:element(3, Annotation)},
erlang:element(5, Annotation)
)
end
),
{maps:merge(Acc, Module_dict),
graded@internal@effects:with_inferred(Kb, Module_dict)}
end.
-file("src/graded.gleam", 1131).
-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", 1004).
?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 keyed by `QualifiedName` so the caller can fold them\n"
" into the global knowledge base. Errors are swallowed (returned as\n"
" `Error(Nil)`) to preserve the existing tolerance: a malformed dep\n"
" shouldn't break the whole 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())} |
{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(2, 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
),
gleam@result:'try'(
begin
_pipe@4 = graded@internal@topo:sort(Graph),
gleam@result:map_error(_pipe@4, fun(_) -> nil end)
end,
fun(Sorted) ->
{Inferred, _} = gleam@list:fold(
Sorted,
{maps:new(), Base_kb},
fun(State, Module_path@2) ->
infer_path_dep_module(State, Module_path@2, Index)
end
),
{ok, Inferred}
end
).
-file("src/graded.gleam", 976).
?DOC(
" For each path dependency declared in `gleam.toml`:\n"
"\n"
" 1. Try to load its spec file (via the dep's own `[tools.graded]`\n"
" config, defaulting to `<package_name>.graded`) and fold its\n"
" annotations into the knowledge base. This is the fast, intended\n"
" path: the dep author already ran `graded infer`, committed the\n"
" spec file, and the consumer just reads it.\n"
"\n"
" 2. If the dep has no spec file, fall back to inferring from source via\n"
" `infer_path_dep` so path deps without graded set up still work.\n"
" Cross-path-dep imports are not currently merged into a single graph\n"
" — each dep is processed sequentially.\n"
).
-spec enrich_with_path_deps(graded@internal@effects:knowledge_base()) -> graded@internal@effects:knowledge_base().
enrich_with_path_deps(Knowledge_base) ->
Path_deps = graded@internal@effects:parse_path_dependencies(
<<"gleam.toml"/utf8>>
),
gleam@list:fold(
Path_deps,
Knowledge_base,
fun(Kb, Dep) ->
{Name, Dep_path} = Dep,
Spec_path = graded@internal@config:spec_file_for(Dep_path, Name),
case simplifile_erl:is_file(Spec_path) of
{ok, true} ->
graded@internal@effects:with_inferred(
Kb,
graded@internal@effects:load_spec_effects(Spec_path)
);
_ ->
case infer_path_dep(Dep_path, Kb) of
{error, nil} ->
Kb;
{ok, Inferred} ->
graded@internal@effects:with_inferred(Kb, Inferred)
end
end
end
).
-file("src/graded.gleam", 587).
?DOC(
" The names of `function`'s parameters whose inferred type (positional in\n"
" `argument_types`) is itself a `Fn`.\n"
).
-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", 567).
?DOC(
" From girard's inferred top-level signatures, the set of function-typed\n"
" parameter names for each function — including parameters with no syntactic\n"
" `fn(...)` annotation, which the glance-only detection misses. A parameter is\n"
" function-typed when its inferred type (positional in the function's `Fn`\n"
" type) is itself a `Fn`.\n"
).
-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", 613).
?DOC(
" A girard `Resolver` that resolves graded's own project modules from `index`\n"
" first (so non-`src` layouts like `test/fixtures` work), then falls through\n"
" to girard's stock disk resolver for dependencies and stdlib under\n"
" `build/packages`.\n"
).
-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", 520).
?DOC(
" Run girard's whole-package type inference once over every project module\n"
" and fold the result into a `TypeInfo` (module path -> span start -> type).\n"
" girard is best-effort: a function it can't type contributes no expressions,\n"
" so the checker silently falls back to syntax-level resolution for it.\n"
).
-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", 329).
?DOC(
" Build a signature registry covering every project module. Used by\n"
" the checker's call-site substitution to resolve effect variables\n"
" when the caller passes positional (unlabeled) arguments.\n"
).
-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", 629).
?DOC(
" Build an index from dotted module name (`app/router`) to the parsed file.\n"
" This is the set of *project* modules — every module name in this dict is\n"
" a candidate dependency-graph node.\n"
).
-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", 317).
?DOC(
" Parse every project source file once, returning `(path, parsed module)`\n"
" pairs. Used by `run_infer` so the topo sort can read each module's\n"
" imports without re-parsing on the inference pass.\n"
).
-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", 1125).
-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", 864).
?DOC(
" Group a parsed spec file's `check` annotations by their module path. Used\n"
" during `run` to hand each source file only the checks that apply to it.\n"
" The checker expects bare function names per module, so we strip the\n"
" module qualifier from the grouped annotations.\n"
).
-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", 953).
-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", 936).
?DOC(
" Join a path against a root, but leave it untouched if it's already\n"
" absolute (starts with `/`) or if the root is `.` (so production paths\n"
" stay short and unprefixed).\n"
).
-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", 944).
-spec default_package_name(binary()) -> binary().
default_package_name(Directory) ->
case filepath:base_name(Directory) of
<<""/utf8>> ->
<<"graded"/utf8>>;
<<"/"/utf8>> ->
<<"graded"/utf8>>;
Name ->
Name
end.
-file("src/graded.gleam", 916).
?DOC(
" Read the project's `[tools.graded]` config and return spec/cache paths\n"
" already resolved relative to the project root. The \"project root\" is\n"
" the directory containing `gleam.toml`:\n"
"\n"
" - When `directory == \"src\"` (the production case), project root is `.`\n"
" and gleam.toml lives at `./gleam.toml`.\n"
" - Otherwise (tests against ad-hoc directories), the source directory\n"
" itself acts as the project root and gleam.toml is looked up there.\n"
"\n"
" Resolved paths are returned in the same `GradedConfig` shape so callers\n"
" can use them as-is for I/O without further joining.\n"
).
-spec read_config(binary()) -> graded@internal@config:graded_config().
read_config(Directory) ->
Project_root = case Directory of
<<"src"/utf8>> ->
<<"."/utf8>>;
_ ->
Directory
end,
Toml_path = filepath:join(Project_root, <<"gleam.toml"/utf8>>),
Raw = case graded@internal@config:read(Toml_path) of
{ok, Cfg} ->
Cfg;
{error, _} ->
graded@internal@config:defaults_for(default_package_name(Directory))
end,
{graded_config,
erlang:element(2, Raw),
resolve_path(Project_root, erlang:element(3, Raw)),
resolve_path(Project_root, erlang:element(4, Raw))}.
-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) ->
Cfg = read_config(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 = graded@internal@signatures:load_from_packages_dir(
<<"build/packages"/utf8>>
),
Registry = graded@internal@signatures:merge(
Dep_registry,
build_project_registry(Index)
),
Type_info = build_type_index(Index),
Kb_base = begin
_pipe = graded@internal@effects:load_knowledge_base(
<<"build/packages"/utf8>>
),
_pipe@1 = enrich_with_path_deps(_pipe),
_pipe@2 = graded@internal@effects:with_inferred(
_pipe@1,
graded@internal@effects:load_spec_effects_from_file(
Spec
)
),
_pipe@3 = graded@internal@effects:with_inferred_returned_operators(
_pipe@2,
graded@internal@effects:load_spec_returns_from_file(
Spec
)
),
_pipe@4 = graded@internal@effects:with_externals(
_pipe@3,
graded@internal@annotation:extract_externals(Spec)
),
infer_project_in_memory(
_pipe@4,
Index,
Registry,
Type_info
)
end,
Knowledge_base = begin
_pipe@5 = Kb_base,
_pipe@6 = graded@internal@effects:with_inferred_type_fields(
_pipe@5,
build_constructor_field_index(Index, Kb_base)
),
_pipe@7 = graded@internal@effects:with_type_fields(
_pipe@6,
graded@internal@annotation:extract_type_fields(Spec)
),
graded@internal@effects:with_factories(
_pipe@7,
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,
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
).
-file("src/graded.gleam", 1090).
-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", 301).
-spec format_one_spec(binary()) -> {ok, binary()} | {error, graded_error()}.
format_one_spec(Spec_path) ->
gleam@result:'try'(
begin
_pipe = simplifile:read(Spec_path),
gleam@result:map_error(
_pipe,
fun(_capture) -> {file_read_error, Spec_path, _capture} end
)
end,
fun(Content) ->
gleam@result:'try'(
begin
_pipe@1 = graded@internal@annotation:parse_file(Content),
gleam@result:map_error(
_pipe@1,
fun(_capture@1) ->
{graded_parse_error, Spec_path, _capture@1}
end
)
end,
fun(File) ->
{ok, graded@internal@annotation:format_sorted(File)}
end
)
end
).
-file("src/graded.gleam", 273).
?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) ->
Cfg = read_config(Directory),
case format_one_spec(erlang:element(3, Cfg)) of
{error, _} ->
{ok, nil};
{ok, Formatted} ->
_pipe = simplifile:write(erlang:element(3, Cfg), Formatted),
gleam@result:map_error(
_pipe,
fun(_capture) ->
{file_write_error, erlang:element(3, Cfg), _capture}
end
)
end.
-file("src/graded.gleam", 285).
?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) ->
Cfg = read_config(Directory),
case format_one_spec(erlang:element(3, Cfg)) of
{error, _} ->
{ok, nil};
{ok, Formatted} ->
case simplifile:read(erlang:element(3, Cfg)) of
{error, _} ->
{ok, nil};
{ok, Content} ->
case Content =:= Formatted of
true ->
{ok, nil};
false ->
{error,
{format_check_failed, [erlang:element(3, Cfg)]}}
end
end
end.
-file("src/graded.gleam", 1142).
-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", 839).
?DOC(
" Write the project's spec file. Reads the existing spec (if any),\n"
" preserves all `check`/`external`/`type` lines plus comments and blank\n"
" lines, replaces the inferred `effects` lines with the freshly inferred\n"
" public-function annotations, and writes the result back.\n"
).
-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", 826).
?DOC(" Build a set of public function names from a parsed Gleam module.\n").
-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", 661).
?DOC(
" Infer effects for a single module, write its cache file (with bare\n"
" names), and return the new knowledge base + the module's *public*\n"
" inferred annotations qualified with the module path. The caller\n"
" accumulates the public annotations for the eventual spec file write.\n"
).
-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,
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,
Inferred_dict = gleam@list:fold(
Inferred,
maps:new(),
fun(Acc, Ann) ->
gleam@dict:insert(
Acc,
{qualified_name, Module_path, 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;
_ ->
gleam@dict:insert(
Acc@1,
{qualified_name,
Module_path,
erlang:element(3, Ann@1)},
erlang:element(4, Ann@1)
)
end end
),
Returned_dict = gleam@dict:fold(
Returned_operators,
maps:new(),
fun(Acc@2, Function, Operator) ->
gleam@dict:insert(
Acc@2,
{qualified_name, Module_path, Function},
Operator
)
end
),
New_kb = begin
_pipe@1 = Knowledge_base,
_pipe@2 = graded@internal@effects:with_inferred(
_pipe@1,
Inferred_dict
),
_pipe@3 = graded@internal@effects:with_inferred_params(
_pipe@2,
Params_dict
),
graded@internal@effects:with_inferred_returned_operators(
_pipe@3,
Returned_dict
)
end,
Public_names = public_function_names(Module),
Public_annotations = begin
_pipe@4 = Inferred,
_pipe@5 = gleam@list:filter(
_pipe@4,
fun(Ann@2) ->
gleam@set:contains(
Public_names,
erlang:element(3, Ann@2)
)
end
),
gleam@list:map(
_pipe@5,
fun(Ann@3) ->
{effect_annotation,
erlang:element(2, Ann@3),
<<<<Module_path/binary, "."/utf8>>/binary,
(erlang:element(3, Ann@3))/binary>>,
erlang:element(4, Ann@3),
erlang:element(5, Ann@3)}
end
)
end,
Public_returns = begin
_pipe@6 = Returned_operators,
_pipe@7 = maps:to_list(_pipe@6),
_pipe@8 = gleam@list:filter(
_pipe@7,
fun(Pair) ->
gleam@set:contains(
Public_names,
erlang:element(1, Pair)
)
end
),
gleam@list:map(
_pipe@8,
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", 196).
?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) ->
Cfg = read_config(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(
<<"build/packages"/utf8>>
),
_pipe@1 = enrich_with_path_deps(_pipe),
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 = graded@internal@signatures:load_from_packages_dir(
<<"build/packages"/utf8>>
),
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
).
-file("src/graded.gleam", 77).
-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>>] ->
Input = begin
_pipe = stdin:read_lines(),
_pipe@1 = gleam@yielder:to_list(_pipe),
gleam@string:join(_pipe@1, <<""/utf8>>)
end,
case graded@internal@annotation:parse_file(Input) of
{ok, File} ->
gleam_stdlib:print(
graded@internal@annotation:format_sorted(File)
);
{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.