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([run_format/1, run_format_check/1, infer_path_dep/2, run/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", 235).
-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", 263).
?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@extract:module_path_for_source(
Gleam_path,
Directory
),
gleam@dict:insert(Acc, Module_path, {Gleam_path, Module})
end
).
-file("src/graded.gleam", 278).
?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", 342).
?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", 378).
?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", 442).
?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", 450).
-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", 422).
?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", 207).
?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", 219).
?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", 459).
-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", 561).
-spec infer_path_dep_module(
{gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_set()),
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_set()),
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),
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", 584).
-spec target_directory(list(binary())) -> binary().
target_directory(Arguments) ->
case Arguments of
[Directory | _] ->
Directory;
[] ->
<<"src"/utf8>>
end.
-file("src/graded.gleam", 626).
-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", 632).
-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", 251).
?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", 398).
?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(),
list(graded@internal@types:effect_annotation()),
graded@internal@effects:knowledge_base()
) -> {ok, graded@internal@types:check_result()} | {error, nil}.
check_one_file(Gleam_path, Module_checks, Knowledge_base) ->
gleam@result:'try'(
begin
_pipe = read_and_parse_gleam(Gleam_path),
gleam@result:replace_error(_pipe, nil)
end,
fun(Module) ->
{Violations, Warnings} = graded@internal@checker:check(
Module,
Module_checks,
Knowledge_base
),
{ok, {check_result, Gleam_path, Violations, Warnings}}
end
).
-file("src/graded.gleam", 514).
?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_set())} |
{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@extract: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", 482).
?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_file = case graded@internal@config:read(
filepath:join(Dep_path, <<"gleam.toml"/utf8>>)
) of
{ok, Cfg} ->
erlang:element(3, Cfg);
{error, _} ->
graded@internal@config:default_spec_file(Name)
end,
Spec_path = filepath:join(Dep_path, Spec_file),
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", 119).
?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)),
Knowledge_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_externals(
_pipe@2,
graded@internal@annotation:extract_externals(Spec)
),
graded@internal@effects:with_type_fields(
_pipe@3,
graded@internal@annotation:extract_type_fields(Spec)
)
end,
Checks_by_module = checks_grouped_by_module(Spec),
gleam@result:'try'(
find_gleam_files(Directory),
fun(Gleam_files) ->
Results = begin
_pipe@4 = gleam@list:map(
Gleam_files,
fun(Gleam_path) ->
Module_path = graded@internal@extract: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_checks,
Knowledge_base
)
end
),
gleam@list:filter_map(_pipe@4, fun(Result) -> Result end)
end,
{ok, Results}
end
).
-file("src/graded.gleam", 643).
-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", 295).
?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()
) -> {ok,
{graded@internal@effects:knowledge_base(),
list(graded@internal@types:effect_annotation())}} |
{error, graded_error()}.
infer_one_module(Module, Module_path, Cache_dir, Knowledge_base) ->
Inferred = graded@internal@checker:infer(Module, Knowledge_base, []),
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
),
New_kb = graded@internal@effects:with_inferred(
Knowledge_base,
Inferred_dict
),
Public_names = public_function_names(Module),
Public_annotations = begin
_pipe@1 = Inferred,
_pipe@2 = gleam@list:filter(
_pipe@1,
fun(Ann@1) ->
gleam@set:contains(
Public_names,
erlang:element(3, Ann@1)
)
end
),
gleam@list:map(
_pipe@2,
fun(Ann@2) ->
{effect_annotation,
erlang:element(2, Ann@2),
<<<<Module_path/binary, "."/utf8>>/binary,
(erlang:element(3, Ann@2))/binary>>,
erlang:element(4, Ann@2),
erlang:element(5, Ann@2)}
end
)
end,
{ok, {New_kb, Public_annotations}}
end).
-file("src/graded.gleam", 355).
?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(), list(graded@internal@types:effect_annotation())) -> {ok,
nil} |
{error, graded_error()}.
write_spec_file(Spec_path, Inferred) ->
Merged = graded@internal@annotation:merge_inferred(
read_spec(Spec_path),
Inferred
),
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", 162).
?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),
Base_kb = begin
_pipe = graded@internal@effects:load_knowledge_base(
<<"build/packages"/utf8>>
),
enrich_with_path_deps(_pipe)
end,
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),
Graph = build_dependency_graph(Index),
gleam@result:'try'(
begin
_pipe@1 = graded@internal@topo:sort(Graph),
gleam@result:map_error(
_pipe@1,
fun(Error) ->
{cycle, Nodes} = Error,
{cyclic_imports, Nodes}
end
)
end,
fun(Sorted) ->
gleam@result:'try'(
gleam@list:try_fold(
Sorted,
{Base_kb, []},
fun(State, Module_path) ->
{Kb, 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
),
fun(_use0) ->
{New_kb, New_public} = _use0,
{ok,
{New_kb,
lists:append(
New_public,
Acc
)}}
end
)
end
end
),
fun(_use0@1) ->
{_, Public_annotations} = _use0@1,
write_spec_file(
erlang:element(3, Cfg),
Public_annotations
)
end
)
end
)
end
)
end
).
-file("src/graded.gleam", 651).
-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", 674).
-spec print_violation(binary(), graded@internal@types:violation()) -> nil.
print_violation(File, Violation) ->
gleam_stdlib:println(
<<<<<<<<<<<<<<<<<<<<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>>
).
-file("src/graded.gleam", 668).
-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", 696).
-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", 690).
-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", 591).
-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", 71).
-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.