-module(graded). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/graded.gleam"). -export([infer_path_dep/3, run/1, run_format/1, run_format_check/1, run_format_stdin/1, run_infer/1, main/0]). -export_type([graded_error/0, module_info/0, callable/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())}. -type module_info() :: {module_info, gleam@dict:dict(binary(), glance:type()), gleam@set:set(binary()), gleam@dict:dict(binary(), binary()), gleam@dict:dict(binary(), {binary(), binary()})}. -type callable() :: callable | not_callable | unknown_callable. -file("src/graded.gleam", 1702). -spec target_directory(list(binary())) -> binary(). target_directory(Arguments) -> case Arguments of [Directory | _] -> Directory; [] -> <<"src"/utf8>> end. -file("src/graded.gleam", 1769). -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", 1793). -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", 1787). -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", 1826). -spec print_warning(binary(), graded@internal@types:warning()) -> nil. print_warning(File, Warning) -> case Warning of {untracked_effect_warning, Function, Reference, _, Effs} -> gleam_stdlib:println( <<<<<<<<<<<<<<<<<>/binary, Function/binary>>/binary, " passes "/utf8>>/binary, (erlang:element(2, Reference))/binary>>/binary, "."/utf8>>/binary, (erlang:element(3, Reference))/binary>>/binary, " as a value — its effects "/utf8>>/binary, (graded@internal@effects:format_effect_set(Effs))/binary>>/binary, " won't be tracked"/utf8>> ); {unmatched_field_bound_warning, Function@1, Field_path, Receiver_is_param} -> Cause = case Receiver_is_param of true -> <<" matches no field call in its body — check the path"/utf8>>; false -> <<" matches no field call in its body — check the path, or the receiver is traced to a construction site and resolved through value provenance (field bounds apply only to untraceable receivers)"/utf8>> end, gleam_stdlib:println( <<<<<<<<<>/binary, Field_path/binary>>/binary, " on "/utf8>>/binary, Function@1/binary>>/binary, Cause/binary>> ); {unmatched_param_bound_warning, Function@2, Param} -> gleam_stdlib:println( <<<<<<<<<>/binary, Param/binary>>/binary, " on "/utf8>>/binary, Function@2/binary>>/binary, " names no parameter of the function — check the name"/utf8>> ); {unmatched_check_warning, Function@3} -> gleam_stdlib:println( <<<<<>/binary, Function@3/binary>>/binary, " names no function in any project module — check the module qualifier; the check never runs"/utf8>> ); {unmatched_type_field_warning, Name} -> gleam_stdlib:println( <<<<<>/binary, Name/binary>>/binary, " names no field of any project type — check the module qualifier; the field resolves to [Unknown]"/utf8>> ) end. -file("src/graded.gleam", 1820). -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", 509). -spec classify_in_module( binary(), binary(), module_info(), fun((binary()) -> {ok, module_info()} | {error, nil}), gleam@set:set(binary()) ) -> callable(). classify_in_module(Name, Module_path, Info, Module_info, Seen) -> case gleam_stdlib:map_get(erlang:element(2, Info), Name) of {ok, Aliased} -> classify_field_type(Aliased, Module_path, Module_info, Seen); {error, nil} -> case {gleam@set:contains(erlang:element(3, Info), Name), gleam_stdlib:map_get(erlang:element(5, Info), Name)} of {true, _} -> not_callable; {false, {ok, {Real_module, Original}}} -> classify_named_type( Original, Real_module, Module_info, Seen ); {false, {error, nil}} -> not_callable end end. -file("src/graded.gleam", 485). -spec classify_named_type( binary(), binary(), fun((binary()) -> {ok, module_info()} | {error, nil}), gleam@set:set(binary()) ) -> callable(). classify_named_type(Name, Module_path, Module_info, Seen) -> Key = <<<>/binary, Name/binary>>, case {gleam@set:contains(Seen, Key), Module_info(Module_path)} of {true, _} -> unknown_callable; {_, {error, nil}} -> unknown_callable; {false, {ok, Info}} -> classify_in_module( Name, Module_path, Info, Module_info, gleam@set:insert(Seen, Key) ) end. -file("src/graded.gleam", 454). -spec classify_field_type( glance:type(), binary(), fun((binary()) -> {ok, module_info()} | {error, nil}), gleam@set:set(binary()) ) -> callable(). classify_field_type(Type_, Module_path, Module_info, Seen) -> case Type_ of {function_type, _, _, _} -> callable; {named_type, _, Name, none, _} -> classify_named_type(Name, Module_path, Module_info, Seen); {named_type, _, Name@1, {some, Qualifier}, _} -> case Module_info(Module_path) of {ok, Info} -> case gleam_stdlib:map_get( erlang:element(4, Info), Qualifier ) of {ok, Real_module} -> classify_named_type( Name@1, Real_module, Module_info, Seen ); {error, nil} -> unknown_callable end; {error, nil} -> unknown_callable end; {variable_type, _, _} -> unknown_callable; _ -> not_callable end. -file("src/graded.gleam", 337). -spec lookup_labelled_field(glance:module_(), binary(), binary()) -> {ok, glance:type()} | {error, nil}. lookup_labelled_field(Module, Type_name, Field) -> gleam@result:'try'( gleam@list:find( erlang:element(3, Module), fun(D) -> erlang:element(3, erlang:element(3, D)) =:= Type_name end ), fun(Definition) -> gleam@list:find_map( erlang:element(7, erlang:element(3, Definition)), fun(Variant) -> gleam@list:find_map( erlang:element(3, Variant), fun(F) -> case F of {labelled_variant_field, Item, Label} when Label =:= Field -> {ok, Item}; _ -> {error, nil} end end ) end ) end ). -file("src/graded.gleam", 303). -spec valid_type_field( binary(), graded@internal@types:type_field_annotation(), gleam@dict:dict(binary(), {binary(), glance:module_()}), gleam@set:set(binary()), fun((binary()) -> {ok, module_info()} | {error, nil}) ) -> boolean(). valid_type_field(Module, Tf, Index, Dep_modules, Module_info) -> case gleam_stdlib:map_get(Index, Module) of {ok, {_, Mod}} -> case lookup_labelled_field( Mod, erlang:element(3, Tf), erlang:element(4, Tf) ) of {ok, Field_type} -> classify_field_type( Field_type, Module, Module_info, gleam@set:new() ) /= not_callable; {error, nil} -> false end; {error, nil} -> gleam@set:contains(Dep_modules, Module) end. -file("src/graded.gleam", 280). -spec unmatched_type_field_warning( graded@internal@types:type_field_annotation(), gleam@dict:dict(binary(), {binary(), glance:module_()}), gleam@set:set(binary()), fun((binary()) -> {ok, module_info()} | {error, nil}) ) -> {ok, graded@internal@types:warning()} | {error, nil}. unmatched_type_field_warning(Tf, Index, Dep_modules, Module_info) -> case erlang:element(2, Tf) of none -> {ok, {unmatched_type_field_warning, <<<<(erlang:element(3, Tf))/binary, "."/utf8>>/binary, (erlang:element(4, Tf))/binary>>}}; {some, Module} -> case valid_type_field(Module, Tf, Index, Dep_modules, Module_info) of true -> {error, nil}; false -> {ok, {unmatched_type_field_warning, <<<<<<<>/binary, (erlang:element(3, Tf))/binary>>/binary, "."/utf8>>/binary, (erlang:element(4, Tf))/binary>>}} end end. -file("src/graded.gleam", 420). -spec last_segment(binary()) -> binary(). last_segment(Module_path) -> _pipe = Module_path, _pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>), _pipe@2 = gleam@list:last(_pipe@1), gleam@result:unwrap(_pipe@2, Module_path). -file("src/graded.gleam", 386). -spec module_info_from_glance(glance:module_()) -> module_info(). module_info_from_glance(Module) -> Aliases = gleam@list:fold( erlang:element(4, Module), maps:new(), fun(Acc, Definition) -> gleam@dict:insert( Acc, erlang:element(3, erlang:element(3, Definition)), erlang:element(6, erlang:element(3, Definition)) ) end ), Custom_types = gleam@list:fold( erlang:element(3, Module), gleam@set:new(), fun(Acc@1, Definition@1) -> gleam@set:insert( Acc@1, erlang:element(3, erlang:element(3, Definition@1)) ) end ), {Qualified_imports, Unqualified_types} = gleam@list:fold( erlang:element(2, Module), {maps:new(), maps:new()}, fun(Acc@2, Definition@2) -> {Quals, Unquals} = Acc@2, Import_ = erlang:element(3, Definition@2), Alias = case erlang:element(4, Import_) of {some, {named, Name}} -> Name; _ -> last_segment(erlang:element(3, Import_)) end, Unquals@1 = gleam@list:fold( erlang:element(5, Import_), Unquals, fun(U, Unqualified) -> Local = case erlang:element(3, Unqualified) of {some, A} -> A; none -> erlang:element(2, Unqualified) end, gleam@dict:insert( U, Local, {erlang:element(3, Import_), erlang:element(2, Unqualified)} ) end ), {gleam@dict:insert(Quals, Alias, erlang:element(3, Import_)), Unquals@1} end ), {module_info, Aliases, Custom_types, Qualified_imports, Unqualified_types}. -file("src/graded.gleam", 427). -spec lookup_module_info( binary(), gleam@dict:dict(binary(), module_info()), gleam@dict:dict(binary(), binary()) ) -> {ok, module_info()} | {error, nil}. lookup_module_info(Module_path, Project_infos, Dep_files) -> case gleam_stdlib:map_get(Project_infos, Module_path) of {ok, Info} -> {ok, Info}; {error, nil} -> case gleam_stdlib:map_get(Dep_files, Module_path) of {ok, File} -> gleam@result:'try'( begin _pipe = simplifile:read(File), gleam@result:replace_error(_pipe, nil) end, fun(Source) -> gleam@result:'try'( begin _pipe@1 = glance:module(Source), gleam@result:replace_error(_pipe@1, nil) end, fun(Module) -> {ok, module_info_from_glance(Module)} end ) end ); {error, nil} -> {error, nil} end end. -file("src/graded.gleam", 377). -spec project_module_infos( gleam@dict:dict(binary(), {binary(), glance:module_()}) ) -> gleam@dict:dict(binary(), module_info()). project_module_infos(Index) -> gleam@dict:map_values( Index, fun(_, Entry) -> {_, Module} = Entry, module_info_from_glance(Module) end ). -file("src/graded.gleam", 1428). -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", 1392). -spec packages_dir(binary()) -> binary(). packages_dir(Package_root) -> filepath:join(Package_root, <<"build/packages"/utf8>>). -file("src/graded.gleam", 217). -spec dependency_module_files(binary()) -> gleam@dict:dict(binary(), binary()). dependency_module_files(Package_root) -> Installed = graded@internal@effects:dependency_module_files( packages_dir(Package_root) ), _pipe = graded@internal@effects:parse_path_dependencies( filepath:join(Package_root, <<"gleam.toml"/utf8>>) ), gleam@list:fold( _pipe, Installed, fun(Acc, Dep) -> {_, Dep_path} = Dep, Src_dir = filepath:join( resolve_path(Package_root, Dep_path), <<"src"/utf8>> ), maps:merge( Acc, graded@internal@effects:source_dir_module_files(Src_dir) ) end ). -file("src/graded.gleam", 324). -spec known_function_names( gleam@dict:dict(binary(), {binary(), glance:module_()}) ) -> gleam@set:set(binary()). known_function_names(Index) -> gleam@dict:fold( Index, gleam@set:new(), fun(Acc, Module_path, Entry) -> {_, Module} = Entry, gleam@list:fold( erlang:element(6, Module), Acc, fun(Acc2, Definition) -> gleam@set:insert( Acc2, <<<>/binary, (erlang:element(3, erlang:element(3, Definition)))/binary>> ) end ) end ). -file("src/graded.gleam", 232). -spec validate_spec_annotations( graded@internal@types:graded_file(), gleam@dict:dict(binary(), {binary(), glance:module_()}), binary() ) -> list(graded@internal@types:warning()). validate_spec_annotations(Spec, Index, Package_root) -> Known_functions = known_function_names(Index), Check_warnings = begin _pipe = graded@internal@annotation:extract_checks(Spec), _pipe@1 = gleam@list:filter( _pipe, fun(Ann) -> not gleam@set:contains(Known_functions, erlang:element(3, Ann)) end ), gleam@list:map( _pipe@1, fun(Ann@1) -> {unmatched_check_warning, erlang:element(3, Ann@1)} end ) end, Type_field_warnings = case graded@internal@annotation:extract_type_fields( Spec ) of [] -> []; Type_fields -> Dep_files = dependency_module_files(Package_root), Dep_modules = gleam@set:from_list(maps:keys(Dep_files)), Project_infos = project_module_infos(Index), Module_info = fun(Module_path) -> lookup_module_info(Module_path, Project_infos, Dep_files) end, gleam@list:filter_map( Type_fields, fun(_capture) -> unmatched_type_field_warning( _capture, Index, Dep_modules, Module_info ) end ) end, lists:append(Check_warnings, Type_field_warnings). -file("src/graded.gleam", 1317). -spec check_one_file( binary(), 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_path, Module, Module_checks, Knowledge_base, Registry, Module_types, Girard_fn_typed ) -> {Violations, Warnings} = graded@internal@checker:check( Module, Module_path, Module_checks, Knowledge_base, Registry, Module_types, Girard_fn_typed ), {check_result, Gleam_path, Violations, Warnings}. -file("src/graded.gleam", 786). -spec qualify_by_module( gleam@dict:dict(binary(), {binary(), glance:module_()}), fun((glance:module_()) -> gleam@dict:dict(binary(), TWO)) ) -> gleam@dict:dict({binary(), binary()}, TWO). 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", 887). -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", 873). -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", 833). -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", 803). -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", 728). -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", 1529). -spec fold_inferred_into_kb( graded@internal@effects:knowledge_base(), gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()), gleam@dict:dict(graded@internal@types:qualified_name(), list(graded@internal@types:param_bound())), gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()) ) -> graded@internal@effects:knowledge_base(). fold_inferred_into_kb(Knowledge_base, Effs, Params, Returns) -> _pipe = Knowledge_base, _pipe@1 = graded@internal@effects:with_inferred(_pipe, Effs), _pipe@2 = graded@internal@effects:with_inferred_params(_pipe@1, Params), graded@internal@effects:with_inferred_returned_operators(_pipe@2, Returns). -file("src/graded.gleam", 1694). -spec drop_declared_modules( gleam@dict:dict(graded@internal@types:qualified_name(), UBB), gleam@set:set(binary()) ) -> gleam@dict:dict(graded@internal@types:qualified_name(), UBB). drop_declared_modules(Entries, Modules) -> gleam@bool:guard( gleam@set:is_empty(Modules), Entries, fun() -> gleam@dict:filter( Entries, fun(Name, _) -> not gleam@set:contains(Modules, erlang:element(2, Name)) end ) end ). -file("src/graded.gleam", 1230). -spec qualified_inferred( list(graded@internal@types:effect_annotation()), gleam@dict:dict(binary(), graded@internal@types:effect_term()), binary() ) -> {gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()), gleam@dict:dict(graded@internal@types:qualified_name(), list(graded@internal@types:param_bound())), gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term())}. qualified_inferred(Inferred, Returned_operators, 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 ), {Effects_dict, Params_dict, Returns_dict}. -file("src/graded.gleam", 1212). -spec thread_inferred_into_kb( graded@internal@effects:knowledge_base(), list(graded@internal@types:effect_annotation()), gleam@dict:dict(binary(), graded@internal@types:effect_term()), binary(), gleam@set:set(binary()) ) -> graded@internal@effects:knowledge_base(). thread_inferred_into_kb( Knowledge_base, Inferred, Returned_operators, Module_path, Declared_modules ) -> {Effects_dict, Params_dict, Returns_dict} = qualified_inferred( Inferred, Returned_operators, Module_path ), Effects_dict@1 = drop_declared_modules(Effects_dict, Declared_modules), fold_inferred_into_kb( Knowledge_base, Effects_dict@1, Params_dict, Returns_dict ). -file("src/graded.gleam", 1175). -spec fold_inferred_module( graded@internal@effects:knowledge_base(), glance:module_(), binary(), graded@internal@signatures:signature_registry(), graded@internal@typeinfo:type_info(), gleam@set:set(binary()) ) -> graded@internal@effects:knowledge_base(). fold_inferred_module( Kb, Module, Module_path, Registry, Type_info, Declared_modules ) -> {Inferred, Returned_operators} = graded@internal@checker:infer_with_returns( Module, Module_path, Kb, [], Registry, graded@internal@typeinfo:for_module(Type_info, Module_path), graded@internal@typeinfo:fn_typed_for_module(Type_info, Module_path) ), thread_inferred_into_kb( Kb, Inferred, Returned_operators, Module_path, Declared_modules ). -file("src/graded.gleam", 1043). -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(3, 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", 1145). -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(), gleam@set:set(binary()) ) -> graded@internal@effects:knowledge_base(). infer_project_in_memory(Base_kb, Index, Registry, Type_info, Declared_modules) -> 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, Declared_modules ) end end ) end. -file("src/graded.gleam", 1628). -spec infer_path_dep_module( {gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()), gleam@dict:dict(graded@internal@types:qualified_name(), list(graded@internal@types:param_bound())), 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())}), graded@internal@signatures:signature_registry(), gleam@set:set(binary()) ) -> {gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()), gleam@dict:dict(graded@internal@types:qualified_name(), list(graded@internal@types:param_bound())), 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, Registry, Consumer_modules) -> {Eff_acc, Param_acc, Returns_acc, Kb} = State, case gleam_stdlib:map_get(Index, Module_path) of {error, _} -> State; {ok, {Module, Checks}} -> {Annotations, Returned_operators} = graded@internal@checker:infer_with_returns( Module, Module_path, Kb, Checks, Registry, maps:new(), maps:new() ), {Inferred_effs, Inferred_params, Inferred_returns} = qualified_inferred( Annotations, Returned_operators, Module_path ), Inferred_effs@1 = drop_declared_modules( Inferred_effs, Consumer_modules ), New_kb = fold_inferred_into_kb( Kb, Inferred_effs@1, Inferred_params, Inferred_returns ), {maps:merge(Eff_acc, Inferred_effs@1), maps:merge(Param_acc, Inferred_params), maps:merge(Returns_acc, Inferred_returns), New_kb} end. -file("src/graded.gleam", 1750). -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", 1553). ?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, polymorphic param bounds, and returned-operator\n" " signatures keyed by `QualifiedName` so the caller can fold them into the\n" " global knowledge base. Errors are swallowed (returned as `Error(Nil)`) to\n" " preserve the existing tolerance: a malformed dep shouldn't break the whole\n" " 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(), gleam@set:set(binary()) ) -> {ok, {gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term()), gleam@dict:dict(graded@internal@types:qualified_name(), list(graded@internal@types:param_bound())), gleam@dict:dict(graded@internal@types:qualified_name(), graded@internal@types:effect_term())}} | {error, nil}. infer_path_dep(Dep_path, Base_kb, Consumer_modules) -> 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(3, 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 ), Registry = gleam@dict:fold( Index, graded@internal@signatures:empty(), fun(Acc@1, Module_path@2, Entry@2) -> {Module@3, _} = Entry@2, graded@internal@signatures:merge( Acc@1, graded@internal@signatures:from_glance_module( Module_path@2, Module@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) -> {Effs, Params, Returns, _} = gleam@list:fold( Sorted, {maps:new(), maps:new(), maps:new(), Base_kb}, fun(State, Module_path@3) -> infer_path_dep_module( State, Module_path@3, Index, Registry, Consumer_modules ) end ), {ok, {Effs, Params, Returns}} end ). -file("src/graded.gleam", 1473). -spec enrich_with_path_deps( graded@internal@effects:knowledge_base(), binary(), gleam@set:set(binary()) ) -> graded@internal@effects:knowledge_base(). enrich_with_path_deps(Knowledge_base, Package_root, Consumer_modules) -> Path_deps = graded@internal@effects:parse_path_dependencies( filepath:join(Package_root, <<"gleam.toml"/utf8>>) ), gleam@list:fold( Path_deps, Knowledge_base, fun(Kb, Dep) -> {Name, Dep_path} = Dep, Resolved_dep_path = resolve_path(Package_root, Dep_path), Spec_path = graded@internal@config:spec_file_for( Resolved_dep_path, Name ), case simplifile_erl:is_file(Spec_path) of {ok, true} -> {Effs, Params, Returns, Type_fields} = graded@internal@effects:load_dep_spec( Resolved_dep_path, Name ), _pipe = fold_inferred_into_kb(Kb, Effs, Params, Returns), graded@internal@effects:with_type_fields(_pipe, Type_fields); _ -> case infer_path_dep(Resolved_dep_path, Kb, Consumer_modules) of {error, nil} -> Kb; {ok, {Effs@1, Params@1, Returns@1}} -> fold_inferred_into_kb( Kb, Effs@1, Params@1, Returns@1 ) end end end ). -file("src/graded.gleam", 1398). -spec manifest_path(binary()) -> binary(). manifest_path(Package_root) -> filepath:join(Package_root, <<"manifest.toml"/utf8>>). -file("src/graded.gleam", 977). -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", 957). -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", 1007). -spec build_girard_resolver( gleam@dict:dict(binary(), {binary(), glance:module_()}), gleam@dict:dict(binary(), binary()) ) -> fun((binary()) -> {ok, binary()} | {error, nil}). build_girard_resolver(Index, Dep_files) -> 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} -> case gleam_stdlib:map_get(Dep_files, Module_path) of {ok, File} -> _pipe@1 = simplifile:read(File), gleam@result:replace_error(_pipe@1, nil); {error, nil} -> Disk(Module_path) end end end. -file("src/graded.gleam", 906). -spec build_type_index( gleam@dict:dict(binary(), {binary(), glance:module_()}), binary() ) -> graded@internal@typeinfo:type_info(). build_type_index(Index, Package_root) -> Options = begin _pipe = girard:default_options(), girard:with_resolver( _pipe, build_girard_resolver(Index, dependency_module_files(Package_root)) ) 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", 713). -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", 1512). -spec path_dep_registry(binary()) -> graded@internal@signatures:signature_registry(). path_dep_registry(Package_root) -> _pipe = graded@internal@effects:parse_path_dependencies( filepath:join(Package_root, <<"gleam.toml"/utf8>>) ), gleam@list:fold( _pipe, graded@internal@signatures:empty(), fun(Acc, Dep) -> {_, Dep_path} = Dep, Resolved_dep_path = resolve_path(Package_root, Dep_path), graded@internal@signatures:merge( Acc, graded@internal@signatures:load_from_source_dir( <> ) ) end ). -file("src/graded.gleam", 1028). -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", 701). -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", 1744). -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", 1297). -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", 1445). -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", 1402). -spec find_gleam_toml_dir(binary(), binary()) -> binary(). find_gleam_toml_dir(Dir, Original) -> Dir@1 = case Dir of <<""/utf8>> -> <<"."/utf8>>; _ -> Dir end, case simplifile_erl:is_file(filepath:join(Dir@1, <<"gleam.toml"/utf8>>)) of {ok, true} -> Dir@1; _ -> Parent = case filepath:directory_name(Dir@1) of <<""/utf8>> -> <<"."/utf8>>; Other -> Other end, case Parent =:= Dir@1 of true -> Original; false -> find_gleam_toml_dir(Parent, Original) end end. -file("src/graded.gleam", 1384). -spec source_root_for(binary()) -> binary(). source_root_for(Directory) -> case Directory of <<"src"/utf8>> -> <<"."/utf8>>; _ -> Directory end. -file("src/graded.gleam", 1375). -spec resolve_package_root(binary()) -> binary(). resolve_package_root(Directory) -> Source_root = source_root_for(Directory), find_gleam_toml_dir(Source_root, Source_root). -file("src/graded.gleam", 1436). -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", 1351). -spec read_config(binary()) -> {ok, graded@internal@config:graded_config()} | {error, graded_error()}. read_config(Directory) -> Project_root = source_root_for(Directory), 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", 126). ?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) -> Package_root = resolve_package_root(Directory), Spec = read_spec(erlang:element(3, Cfg)), Checks_by_module = checks_grouped_by_module(Spec), Declared_modules = graded@internal@annotation:module_external_modules( 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 = begin _pipe = graded@internal@signatures:load_from_packages_dir( packages_dir(Package_root) ), graded@internal@signatures:merge( _pipe, path_dep_registry(Package_root) ) end, Registry = graded@internal@signatures:merge( Dep_registry, build_project_registry(Index) ), Type_info = build_type_index(Index, Package_root), Kb_base = begin _pipe@1 = graded@internal@effects:load_knowledge_base( packages_dir(Package_root), manifest_path(Package_root) ), _pipe@2 = graded@internal@effects:with_externals( _pipe@1, graded@internal@annotation:extract_externals( Spec ) ), _pipe@3 = enrich_with_path_deps( _pipe@2, Package_root, Declared_modules ), _pipe@4 = graded@internal@effects:with_inferred( _pipe@3, drop_declared_modules( graded@internal@effects:load_spec_effects_from_file( Spec ), Declared_modules ) ), _pipe@5 = graded@internal@effects:with_inferred_returned_operators( _pipe@4, graded@internal@effects:load_spec_returns_from_file( Spec ) ), infer_project_in_memory( _pipe@5, Index, Registry, Type_info, Declared_modules ) end, Knowledge_base = begin _pipe@6 = Kb_base, _pipe@7 = graded@internal@effects:with_inferred_type_fields( _pipe@6, build_constructor_field_index( Index, Kb_base ) ), _pipe@8 = graded@internal@effects:with_type_fields( _pipe@7, graded@internal@annotation:extract_type_fields( Spec ) ), graded@internal@effects:with_factories( _pipe@8, 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_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 ), Results@1 = case validate_spec_annotations( Spec, Index, Package_root ) of [] -> Results; Spec_warnings -> [{check_result, erlang:element(3, Cfg), [], Spec_warnings} | Results] end, {ok, Results@1} end ) end ) end ). -file("src/graded.gleam", 1709). -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", 684). -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", 642). ?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", 665). ?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", 656). ?DOC( " Format a `.graded` spec given as a string, as `graded format --stdin` does\n" " for editor integration: parse the input, then sort and reformat it. Returns\n" " the input's parse error if it doesn't parse.\n" ). -spec run_format_stdin(binary()) -> {ok, binary()} | {error, graded@internal@annotation:parse_error()}. run_format_stdin(Input) -> gleam@result:map( graded@internal@annotation:parse_file(Input), fun(File) -> graded@internal@annotation:format_sorted(File) end ). -file("src/graded.gleam", 1761). -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", 1272). -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", 1259). -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", 1060). -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())), 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, Declared_modules ) -> {Inferred, Returned_operators} = graded@internal@checker:infer_with_returns( Module, Module_path, 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, New_kb = thread_inferred_into_kb( Knowledge_base, Inferred, Returned_operators, Module_path, Declared_modules ), Public_names = public_function_names(Module), Public_annotations = begin _pipe@1 = Inferred, _pipe@2 = gleam@list:filter( _pipe@1, fun(Ann) -> gleam@set:contains(Public_names, erlang:element(3, Ann)) end ), gleam@list:map( _pipe@2, fun(Ann@1) -> {effect_annotation, erlang:element(2, Ann@1), <<<>/binary, (erlang:element(3, Ann@1))/binary>>, erlang:element(4, Ann@1), erlang:element(5, Ann@1)} end ) end, Public_returns = begin _pipe@3 = Returned_operators, _pipe@4 = maps:to_list(_pipe@3), _pipe@5 = gleam@list:filter( _pipe@4, fun(Pair) -> gleam@set:contains( Public_names, erlang:element(1, Pair) ) end ), gleam@list:map( _pipe@5, 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", 545). ?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) -> Package_root = resolve_package_root(Directory), Spec = read_spec(erlang:element(3, Cfg)), Declared_modules = graded@internal@annotation:module_external_modules( 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), Kb_base = begin _pipe = graded@internal@effects:load_knowledge_base( packages_dir(Package_root), manifest_path(Package_root) ), _pipe@1 = graded@internal@effects:with_externals( _pipe, graded@internal@annotation:extract_externals( Spec ) ), enrich_with_path_deps( _pipe@1, Package_root, Declared_modules ) end, Construction_kb = graded@internal@effects:with_inferred( Kb_base, drop_declared_modules( graded@internal@effects:load_spec_effects_from_file( Spec ), Declared_modules ) ), 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 = begin _pipe@6 = graded@internal@signatures:load_from_packages_dir( packages_dir(Package_root) ), graded@internal@signatures:merge( _pipe@6, path_dep_registry(Package_root) ) end, Registry = graded@internal@signatures:merge( Dep_registry, build_project_registry(Index) ), Type_info = build_type_index( Index, Package_root ), 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 ), Declared_modules ), 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>> | _] -> case run_format_stdin(graded_ffi:read_stdin()) of {ok, Output} -> gleam_stdlib:print(Output); {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.