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([gleam_to_graded_path/2, run_format/1, run_format_check/1, 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())}.
-file("src/graded.gleam", 214).
?DOC(" Convert a .gleam source path to its .graded path in priv/graded/.\n").
-spec gleam_to_graded_path(binary(), binary()) -> binary().
gleam_to_graded_path(Gleam_path, Source_directory) ->
Prefix = <<Source_directory/binary, "/"/utf8>>,
Relative = case gleam_stdlib:string_starts_with(Gleam_path, Prefix) of
true ->
gleam@string:drop_start(Gleam_path, string:length(Prefix));
false ->
Gleam_path
end,
Graded_relative = <<(filepath:strip_extension(Relative))/binary,
".graded"/utf8>>,
Priv_directory = case Source_directory of
<<"src"/utf8>> ->
<<"priv/graded"/utf8>>;
_ ->
<<Source_directory/binary, "/priv/graded"/utf8>>
end,
<<<<Priv_directory/binary, "/"/utf8>>/binary, Graded_relative/binary>>.
-file("src/graded.gleam", 271).
-spec infer_from_parsed_deps(
list(list({binary(),
glance:module_(),
list(graded@internal@types:effect_annotation())})),
graded@internal@effects:knowledge_base()
) -> gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_set()).
infer_from_parsed_deps(Parsed_deps, Base_kb) ->
Result = gleam@list:fold(
Parsed_deps,
{maps:new(), Base_kb},
fun(State, Dep_files) ->
{All_inferred, Kb} = State,
Dep_inferred = gleam@list:fold(
Dep_files,
maps:new(),
fun(Acc, File) ->
{Module_path, Module, Checks} = File,
Annotations = graded@internal@checker:infer(
Module,
Kb,
Checks
),
gleam@list:fold(
Annotations,
Acc,
fun(Effect_acc, Annotation) ->
gleam@dict:insert(
Effect_acc,
{qualified_name,
Module_path,
erlang:element(3, Annotation)},
erlang:element(5, Annotation)
)
end
)
end
),
{maps:merge(All_inferred, Dep_inferred),
graded@internal@effects:with_inferred(Kb, Dep_inferred)}
end
),
erlang:element(1, Result).
-file("src/graded.gleam", 300).
-spec source_relative_module(binary(), binary()) -> binary().
source_relative_module(Gleam_path, Source_dir) ->
Prefix = <<Source_dir/binary, "/"/utf8>>,
Relative = case gleam_stdlib:string_starts_with(Gleam_path, Prefix) of
true ->
gleam@string:drop_start(Gleam_path, string:length(Prefix));
false ->
Gleam_path
end,
gleam@string:replace(Relative, <<".gleam"/utf8>>, <<""/utf8>>).
-file("src/graded.gleam", 309).
-spec load_path_dep_checks(binary(), binary()) -> list(graded@internal@types:effect_annotation()).
load_path_dep_checks(Dep_path, Module_path) ->
Graded_path = <<<<<<Dep_path/binary, "/priv/graded/"/utf8>>/binary,
Module_path/binary>>/binary,
".graded"/utf8>>,
case simplifile:read(Graded_path) of
{error, _} ->
[];
{ok, Content} ->
case graded@internal@annotation:parse_file(Content) of
{error, _} ->
[];
{ok, Graded_file} ->
graded@internal@annotation:extract_checks(Graded_file)
end
end.
-file("src/graded.gleam", 324).
-spec enrich_knowledge_base(
graded@internal@types:graded_file(),
graded@internal@effects:knowledge_base()
) -> {graded@internal@effects:knowledge_base(),
list(graded@internal@types:effect_annotation())}.
enrich_knowledge_base(Graded_file, Knowledge_base) ->
Checks = graded@internal@annotation:extract_checks(Graded_file),
Type_fields = graded@internal@annotation:extract_type_fields(Graded_file),
Externs = graded@internal@annotation:extract_externals(Graded_file),
Knowledge_base@1 = begin
_pipe = graded@internal@effects:with_type_fields(
Knowledge_base,
Type_fields
),
graded@internal@effects:with_externals(_pipe, Externs)
end,
{Knowledge_base@1, Checks}.
-file("src/graded.gleam", 337).
-spec find_graded_files(binary()) -> {ok, list(binary())} |
{error, graded_error()}.
find_graded_files(Directory) ->
Priv_directory = case Directory of
<<"src"/utf8>> ->
<<"priv/graded"/utf8>>;
_ ->
<<Directory/binary, "/priv/graded"/utf8>>
end,
Files = case simplifile:get_files(Priv_directory) of
{ok, Found} ->
Found;
{error, _} ->
[]
end,
{ok,
gleam@list:filter(
Files,
fun(Path) ->
gleam_stdlib:string_ends_with(Path, <<".graded"/utf8>>)
end
)}.
-file("src/graded.gleam", 351).
-spec read_and_format(binary()) -> {ok, binary()} | {error, graded_error()}.
read_and_format(Graded_path) ->
gleam@result:'try'(
begin
_pipe = simplifile:read(Graded_path),
gleam@result:map_error(
_pipe,
fun(_capture) -> {file_read_error, Graded_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, Graded_path, _capture@1}
end
)
end,
fun(Graded_file) ->
{ok, graded@internal@annotation:format_sorted(Graded_file)}
end
)
end
).
-file("src/graded.gleam", 179).
?DOC(" Format all .graded files in priv/graded/ for a given source directory.\n").
-spec run_format(binary()) -> {ok, nil} | {error, graded_error()}.
run_format(Directory) ->
gleam@result:'try'(
find_graded_files(Directory),
fun(Graded_files) ->
gleam@list:try_each(
Graded_files,
fun(Graded_path) ->
gleam@result:'try'(
read_and_format(Graded_path),
fun(Formatted) ->
_pipe = simplifile:write(Graded_path, Formatted),
gleam@result:map_error(
_pipe,
fun(_capture) ->
{file_write_error, Graded_path, _capture}
end
)
end
)
end
)
end
).
-file("src/graded.gleam", 190).
?DOC(
" Check that all .graded files are already formatted. Returns error with\n"
" the list of unformatted file paths. Exit code 1 in CI.\n"
).
-spec run_format_check(binary()) -> {ok, nil} | {error, graded_error()}.
run_format_check(Directory) ->
gleam@result:'try'(
find_graded_files(Directory),
fun(Graded_files) ->
Unformatted = gleam@list:filter_map(
Graded_files,
fun(Graded_path) -> case read_and_format(Graded_path) of
{error, _} ->
{error, nil};
{ok, Formatted} ->
case simplifile:read(Graded_path) of
{error, _} ->
{error, nil};
{ok, Content} ->
case Content =:= Formatted of
true ->
{error, nil};
false ->
{ok, Graded_path}
end
end
end end
),
case Unformatted of
[] ->
{ok, nil};
Paths ->
{error, {format_check_failed, Paths}}
end
end
).
-file("src/graded.gleam", 363).
-spec target_directory(list(binary())) -> binary().
target_directory(Arguments) ->
case Arguments of
[Directory | _] ->
Directory;
[] ->
<<"src"/utf8>>
end.
-file("src/graded.gleam", 405).
-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", 411).
-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", 249).
-spec parse_path_dep_sources(list({binary(), binary()})) -> list(list({binary(),
glance:module_(),
list(graded@internal@types:effect_annotation())})).
parse_path_dep_sources(Path_deps) ->
gleam@list:map(
Path_deps,
fun(Dep) ->
{_, Dep_path} = Dep,
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,
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 = source_relative_module(
Gleam_path,
Source_dir
),
Checks = load_path_dep_checks(Dep_path, Module_path),
{ok, {Module_path, Module, Checks}}
end
)
end
)
end
).
-file("src/graded.gleam", 233).
-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>>
),
case Path_deps of
[] ->
Knowledge_base;
_ ->
Parsed = parse_path_dep_sources(Path_deps),
Pass1 = infer_from_parsed_deps(Parsed, Knowledge_base),
Enriched = graded@internal@effects:with_inferred(
Knowledge_base,
Pass1
),
Pass2 = infer_from_parsed_deps(Parsed, Enriched),
graded@internal@effects:with_inferred(Knowledge_base, Pass2)
end.
-file("src/graded.gleam", 422).
-spec check_file(binary(), binary(), graded@internal@effects:knowledge_base()) -> {ok,
graded@internal@types:check_result()} |
{error, graded_error()}.
check_file(Gleam_path, Graded_content, Knowledge_base) ->
gleam@result:'try'(
begin
_pipe = graded@internal@annotation:parse_file(Graded_content),
gleam@result:map_error(
_pipe,
fun(_capture) -> {graded_parse_error, Gleam_path, _capture} end
)
end,
fun(Graded_file) ->
{Knowledge_base@1, Check_annotations} = enrich_knowledge_base(
Graded_file,
Knowledge_base
),
gleam@result:'try'(
read_and_parse_gleam(Gleam_path),
fun(Module) ->
{Violations, Warnings} = graded@internal@checker:check(
Module,
Check_annotations,
Knowledge_base@1
),
{ok, {check_result, Gleam_path, Violations, Warnings}}
end
)
end
).
-file("src/graded.gleam", 104).
?DOC(
" Run the checker on all .gleam files in a directory.\n"
" Only enforces `check` annotations.\n"
).
-spec run(binary()) -> {ok, list(graded@internal@types:check_result())} |
{error, graded_error()}.
run(Directory) ->
Knowledge_base = 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) ->
Results = gleam@list:filter_map(
Gleam_files,
fun(Gleam_path) ->
Graded_path = gleam_to_graded_path(Gleam_path, Directory),
case simplifile:read(Graded_path) of
{error, _} ->
{error, nil};
{ok, Graded_content} ->
case check_file(
Gleam_path,
Graded_content,
Knowledge_base
) of
{ok, Check_result} ->
{ok, Check_result};
{error, _} ->
{error, nil}
end
end
end
),
{ok, Results}
end
).
-file("src/graded.gleam", 441).
-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", 130).
?DOC(" Infer effects for all .gleam files and write/merge .graded files.\n").
-spec run_infer(binary()) -> {ok, nil} | {error, graded_error()}.
run_infer(Directory) ->
Knowledge_base = 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@list:try_each(
Gleam_files,
fun(Gleam_path) ->
Graded_path = gleam_to_graded_path(Gleam_path, Directory),
gleam@result:'try'(
read_and_parse_gleam(Gleam_path),
fun(Module) ->
Existing_file = begin
_pipe@1 = simplifile:read(Graded_path),
_pipe@2 = gleam@result:map_error(
_pipe@1,
fun(_) -> nil end
),
gleam@result:'try'(
_pipe@2,
fun(Content) ->
_pipe@3 = graded@internal@annotation:parse_file(
Content
),
gleam@result:map_error(
_pipe@3,
fun(_) -> nil end
)
end
)
end,
{Knowledge_base@1, Existing_checks} = case Existing_file of
{ok, File} ->
enrich_knowledge_base(File, Knowledge_base);
{error, nil} ->
{Knowledge_base, []}
end,
Inferred = graded@internal@checker:infer(
Module,
Knowledge_base@1,
Existing_checks
),
Parent_directory = filepath:directory_name(
Graded_path
),
gleam@result:'try'(
begin
_pipe@4 = simplifile:create_directory_all(
Parent_directory
),
gleam@result:map_error(
_pipe@4,
fun(_capture) ->
{directory_create_error,
Parent_directory,
_capture}
end
)
end,
fun(_use0) ->
nil = _use0,
case {Inferred, Existing_file} of
{[], {error, nil}} ->
{ok, nil};
{_, {ok, File@1}} ->
Merged = graded@internal@annotation:merge_inferred(
File@1,
Inferred
),
write_graded_file(
Graded_path,
Merged
);
{_, {error, nil}} ->
Graded_file = {graded_file,
gleam@list:map(
Inferred,
fun(Field@0) -> {annotation_line, Field@0} end
)},
write_graded_file(
Graded_path,
Graded_file
)
end
end
)
end
)
end
)
end
).
-file("src/graded.gleam", 449).
-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>>
end.
-file("src/graded.gleam", 469).
-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", 463).
-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", 491).
-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", 485).
-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", 370).
-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", 60).
-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.