-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()} | {invalid_config, binary(), graded@internal@config:config_error()} | {format_check_failed, list(binary())} | {cyclic_imports, list(binary())}. -file("src/graded.gleam", 1096). -spec target_directory(list(binary())) -> binary(). target_directory(Arguments) -> case Arguments of [Directory | _] -> Directory; [] -> <<"src"/utf8>> end. -file("src/graded.gleam", 1163). -spec format_error(graded_error()) -> binary(). format_error(Error) -> case Error of {directory_read_error, Path, _} -> <<"Could not read directory: "/utf8, Path/binary>>; {file_read_error, Path@1, _} -> <<"Could not read: "/utf8, Path@1/binary>>; {file_write_error, Path@2, _} -> <<"Could not write: "/utf8, Path@2/binary>>; {directory_create_error, Path@3, _} -> <<"Could not create directory: "/utf8, Path@3/binary>>; {gleam_parse_error, Path@4, _} -> <<"Could not parse: "/utf8, Path@4/binary>>; {graded_parse_error, Path@5, _} -> <<"Parse error in .graded file for: "/utf8, Path@5/binary>>; {invalid_config, Path@6, _} -> <<"Invalid gleam.toml: "/utf8, Path@6/binary>>; {format_check_failed, Paths} -> <<"Unformatted .graded files:\n"/utf8, (gleam@string:join( gleam@list:map( Paths, fun(Path@7) -> <<" "/utf8, Path@7/binary>> end ), <<"\n"/utf8>> ))/binary>>; {cyclic_imports, Modules} -> <<"Cyclic project imports detected (this should be unreachable — Gleam disallows circular imports):\n"/utf8, (gleam@string:join( gleam@list:map( Modules, fun(M) -> <<" "/utf8, M/binary>> end ), <<"\n"/utf8>> ))/binary>> end. -file("src/graded.gleam", 1187). -spec print_violation(binary(), graded@internal@types:violation()) -> nil. print_violation(File, Violation) -> Base = <<<<<<<<<<<<<<<<<<<>/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, "(: [...])` bound, or pass a function reference / constructor"/utf8>>/binary, " whose effects are known"/utf8>>; false -> <<""/utf8>> end, gleam_stdlib:println(<>). -file("src/graded.gleam", 1181). -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", 1220). -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", 1214). -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", 894). -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", 410). -spec qualify_by_module( gleam@dict:dict(binary(), {binary(), glance:module_()}), fun((glance:module_()) -> gleam@dict:dict(binary(), UNH)) ) -> gleam@dict:dict({binary(), binary()}, UNH). 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", 511). -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", 497). -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", 457). -spec field_effect_of( graded@internal@effects:knowledge_base(), graded@internal@types:argument_value(), binary(), fun((list(binary()), list(glance:statement())) -> graded@internal@types:effect_term()) ) -> graded@internal@types:type_field_effect(). field_effect_of(Knowledge_base, Value, Module_path, Closure_effect) -> case field_value_function(Value, Module_path) of {some, Name} -> Field_effects = graded@internal@effects:lookup_effects( Knowledge_base, Name ), case gleam@set:is_empty( graded@internal@effect_term:free_vars(Field_effects) ) of true -> {type_field_effect, Field_effects, [], none}; false -> {type_field_effect, Field_effects, graded@internal@effects:lookup_param_bounds( Knowledge_base, Name ), {some, Name}} end; none -> case Value of {closure, Params, Body} -> {type_field_effect, Closure_effect(Params, Body), [], none}; _ -> {type_field_effect, graded@internal@effects:argument_value_effects( Knowledge_base, Value ), [], none} end end. -file("src/graded.gleam", 427). -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", 352). -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", 797). -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", 654). -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", 775). -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", 1064). -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", 1144). -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", 1017). ?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 = <>, 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", 989). -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", 597). -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", 577). -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", 623). -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", 530). -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", 337). -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", 639). -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", 325). -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", 1138). -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", 874). -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", 966). -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", 949). -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", 957). -spec default_package_name(binary()) -> binary(). default_package_name(Project_root) -> case filepath:base_name(Project_root) of <<""/utf8>> -> <<"graded"/utf8>>; <<"/"/utf8>> -> <<"graded"/utf8>>; <<"."/utf8>> -> <<"graded"/utf8>>; Name -> Name end. -file("src/graded.gleam", 926). -spec read_config(binary()) -> {ok, graded@internal@config:graded_config()} | {error, graded_error()}. read_config(Directory) -> Project_root = case Directory of <<"src"/utf8>> -> <<"."/utf8>>; _ -> Directory end, Toml_path = filepath:join(Project_root, <<"gleam.toml"/utf8>>), gleam@result:'try'(case graded@internal@config:read(Toml_path) of {ok, Cfg} -> {ok, Cfg}; {error, {toml_read_error, _, _}} -> {ok, graded@internal@config:defaults_for( default_package_name(Project_root) )}; {error, Cause} -> {error, {invalid_config, Toml_path, Cause}} end, fun(Raw) -> {ok, {graded_config, erlang:element(2, Raw), resolve_path(Project_root, erlang:element(3, Raw)), resolve_path(Project_root, erlang:element(4, Raw))}} end). -file("src/graded.gleam", 128). ?DOC( " Run the checker on all .gleam files in a directory.\n" "\n" " Reads the project's single spec file (default `.graded`)\n" " to find inferred public-API effects, `check` invariants, `external`\n" " hints, and `type` field annotations, then reports violations per source\n" " file.\n" ). -spec run(binary()) -> {ok, list(graded@internal@types:check_result())} | {error, graded_error()}. run(Directory) -> gleam@result:'try'( read_config(Directory), fun(Cfg) -> 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 ) end ). -file("src/graded.gleam", 1103). -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", 308). -spec format_one_spec(binary()) -> {ok, gleam@option:option(binary())} | {error, graded_error()}. format_one_spec(Spec_path) -> case simplifile:read(Spec_path) of {error, _} -> {ok, none}; {ok, Content} -> _pipe = graded@internal@annotation:parse_file(Content), _pipe@1 = gleam@result:map( _pipe, fun(File) -> {some, graded@internal@annotation:format_sorted(File)} end ), gleam@result:map_error( _pipe@1, fun(_capture) -> {graded_parse_error, Spec_path, _capture} end ) end. -file("src/graded.gleam", 276). ?DOC( " Format the project's spec file in place. The spec file is the single\n" " source of truth for hand-written `check`/`external`/`type` lines and\n" " the inferred public-API effects.\n" ). -spec run_format(binary()) -> {ok, nil} | {error, graded_error()}. run_format(Directory) -> gleam@result:'try'( read_config(Directory), fun(Cfg) -> gleam@result:'try'( format_one_spec(erlang:element(3, Cfg)), fun(Formatted) -> case Formatted of none -> {ok, nil}; {some, Formatted@1} -> _pipe = simplifile:write( erlang:element(3, Cfg), Formatted@1 ), gleam@result:map_error( _pipe, fun(_capture) -> {file_write_error, erlang:element(3, Cfg), _capture} end ) end end ) end ). -file("src/graded.gleam", 289). ?DOC( " Check that the project's spec file is already formatted. Returns error\n" " with the file path if it isn't. Used by CI as `format --check`.\n" ). -spec run_format_check(binary()) -> {ok, nil} | {error, graded_error()}. run_format_check(Directory) -> gleam@result:'try'( read_config(Directory), fun(Cfg) -> gleam@result:'try'( format_one_spec(erlang:element(3, Cfg)), fun(Formatted) -> case Formatted of none -> {ok, nil}; {some, Formatted@1} -> case simplifile:read(erlang:element(3, Cfg)) of {error, _} -> {ok, nil}; {ok, Content} -> case Content =:= Formatted@1 of true -> {ok, nil}; false -> {error, {format_check_failed, [erlang:element(3, Cfg)]}} end end end end ) end ). -file("src/graded.gleam", 1155). -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", 849). -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", 836). -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", 671). -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, <> ), 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), <<<>/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, <<<>/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", 199). ?DOC( " Infer effects for all `.gleam` files in `directory`. Writes two outputs:\n" "\n" " 1. **Per-module cache files** under `/.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 `` containing the inferred effects of\n" " every *public* function across all modules, plus any hand-written\n" " `check`, `external effects`, or `type` annotations the user already\n" " had in the spec file (those lines are preserved verbatim).\n" "\n" " Walks the project's import graph in topological order so each module is\n" " analysed after every other project module it imports — a single pass\n" " resolves transitive chains of any depth.\n" ). -spec run_infer(binary()) -> {ok, nil} | {error, graded_error()}. run_infer(Directory) -> gleam@result:'try'( read_config(Directory), fun(Cfg) -> 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 ) end ). -file("src/graded.gleam", 80). -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.