-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", 183). ?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 = <>, 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>>; _ -> <> end, <<<>/binary, Graded_relative/binary>>. -file("src/graded.gleam", 283). -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", 312). -spec source_relative_module(binary(), binary()) -> binary(). source_relative_module(Gleam_path, Source_dir) -> Prefix = <>, 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", 321). -spec load_path_dep_checks(binary(), binary()) -> list(graded@internal@types:effect_annotation()). load_path_dep_checks(Dep_path, Module_path) -> Graded_path = <<<<<>/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", 336). -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", 349). -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>>; _ -> <> 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", 363). -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", 148). ?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", 159). ?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", 375). -spec target_directory(list(binary())) -> binary(). target_directory(Arguments) -> case Arguments of [Directory | _] -> Directory; [] -> <<"src"/utf8>> end. -file("src/graded.gleam", 417). -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", 423). -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", 261). -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 = <>, 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", 245). -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", 434). -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) -> Project_effects = graded@internal@effects:load_project_effects(Directory), Knowledge_base = begin _pipe = graded@internal@effects:load_knowledge_base( <<"build/packages"/utf8>> ), _pipe@1 = enrich_with_path_deps(_pipe), graded@internal@effects:with_inferred(_pipe@1, Project_effects) 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", 453). -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", 202). -spec infer_files( list(binary()), binary(), graded@internal@effects:knowledge_base() ) -> {ok, nil} | {error, graded_error()}. infer_files(Gleam_files, Directory, Knowledge_base) -> 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 = simplifile:read(Graded_path), _pipe@1 = gleam@result:map_error( _pipe, fun(_) -> nil end ), gleam@result:'try'( _pipe@1, fun(Content) -> _pipe@2 = graded@internal@annotation:parse_file( Content ), gleam@result:map_error( _pipe@2, 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@3 = simplifile:create_directory_all( Parent_directory ), gleam@result:map_error( _pipe@3, 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 ). -file("src/graded.gleam", 132). ?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) -> 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'( infer_files(Gleam_files, Directory, Base_kb), fun(_use0) -> nil = _use0, Project_effects = graded@internal@effects:load_project_effects( Directory ), Enriched_kb = graded@internal@effects:with_inferred( Base_kb, Project_effects ), infer_files(Gleam_files, Directory, Enriched_kb) end ) end ). -file("src/graded.gleam", 461). -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", 481). -spec print_violation(binary(), graded@internal@types:violation()) -> nil. print_violation(File, Violation) -> gleam_stdlib:println( <<<<<<<<<<<<<<<<<<<>/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", 475). -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", 503). -spec print_warning(binary(), graded@internal@types:warning()) -> nil. print_warning(File, Warning) -> gleam_stdlib:println( <<<<<<<<<<<<<<<<<>/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", 497). -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", 382). -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.