Current section
Files
Jump to
Current section
Files
src/libero@walker.erl
-module(libero@walker).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/libero/walker.gleam").
-export([to_snake_case/1, walk_shared_types/1]).
-export_type([discovered_type/0, discovered_variant/0, type_resolver/0, walker_state/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.
-type discovered_type() :: {discovered_type,
binary(),
binary(),
list(binary()),
list(discovered_variant())}.
-type discovered_variant() :: {discovered_variant,
binary(),
binary(),
binary(),
list(integer()),
list(libero@field_type:field_type())}.
-type type_resolver() :: {type_resolver,
gleam@dict:dict(binary(), binary()),
gleam@dict:dict(binary(), binary()),
gleam@dict:dict(binary(), binary())}.
-type walker_state() :: {walker_state,
list({binary(), binary()}),
gleam@set:set({binary(), binary()}),
list(discovered_type()),
gleam@dict:dict(binary(), binary()),
gleam@dict:dict(binary(), glance:module_()),
list(libero@gen_error:gen_error())}.
-file("src/libero/walker.gleam", 79).
?DOC(
" Primitive/builtin type names: not custom types, never walked.\n"
" Sourced from `field_type.builtin_type_names` so scanner and walker\n"
" agree on what counts as a builtin.\n"
" True if a module path should not be walked by the type graph walker.\n"
).
-spec is_skipped_module(binary()) -> boolean().
is_skipped_module(Module_path) ->
gleam@list:any(
[<<"libero/"/utf8>>, <<"gleam/"/utf8>>],
fun(Prefix) -> gleam_stdlib:string_starts_with(Module_path, Prefix) end
).
-file("src/libero/walker.gleam", 86).
?DOC(" True if a type name is a primitive/builtin that needs no registration.\n").
-spec is_primitive_type(binary()) -> boolean().
is_primitive_type(Name) ->
libero@field_type:is_builtin(Name).
-file("src/libero/walker.gleam", 130).
?DOC(
" True when a module path is a Gleam stdlib (or stdlib-extension) module,\n"
" e.g. `gleam`, `gleam/option`, `gleam/dict`. Used to decide whether an\n"
" unqualified primitive type name imported from that module is still the\n"
" stdlib type (e.g. `import gleam/option.{type Option}`) versus a user\n"
" type that happens to share a name with a stdlib primitive.\n"
).
-spec is_stdlib_module_path(binary()) -> boolean().
is_stdlib_module_path(Path) ->
(Path =:= <<"gleam"/utf8>>) orelse gleam_stdlib:string_starts_with(
Path,
<<"gleam/"/utf8>>
).
-file("src/libero/walker.gleam", 98).
?DOC(
" True when a NamedType reference points to a Gleam stdlib type, not a\n"
" user-defined type that happens to share the name. Without this check,\n"
" `pub type Result` defined in shared/ would be silently dropped from\n"
" codegen because its name matches `registry_primitives`.\n"
"\n"
" Distinguishes by module qualifier and by whether the user imported a\n"
" same-named type. A bare `Result` with no shadowing import is stdlib;\n"
" `import shared/myresult.{type Result}` makes bare `Result` user-defined.\n"
).
-spec is_stdlib_reference(
binary(),
gleam@option:option(binary()),
type_resolver()
) -> boolean().
is_stdlib_reference(Name, Module, Resolver) ->
case {is_primitive_type(Name), Module} of
{false, _} ->
false;
{true, {some, <<"gleam"/utf8>>}} ->
true;
{true, {some, <<"option"/utf8>>}} ->
Name =:= <<"Option"/utf8>>;
{true, {some, <<"result"/utf8>>}} ->
Name =:= <<"Result"/utf8>>;
{true, {some, <<"dict"/utf8>>}} ->
Name =:= <<"Dict"/utf8>>;
{true, {some, <<"list"/utf8>>}} ->
Name =:= <<"List"/utf8>>;
{true, {some, <<"bool"/utf8>>}} ->
Name =:= <<"Bool"/utf8>>;
{true, {some, <<"bit_array"/utf8>>}} ->
Name =:= <<"BitArray"/utf8>>;
{true, {some, _}} ->
false;
{true, none} ->
case gleam_stdlib:map_get(erlang:element(2, Resolver), Name) of
{error, nil} ->
true;
{ok, Module_path} ->
is_stdlib_module_path(Module_path)
end
end.
-file("src/libero/walker.gleam", 668).
?DOC(" Extract the type from a variant field, whether labelled or unlabelled.\n").
-spec variant_field_type(glance:variant_field()) -> glance:type().
variant_field_type(Field) ->
case Field of
{labelled_variant_field, Item, _} ->
Item;
{unlabelled_variant_field, Item@1} ->
Item@1
end.
-file("src/libero/walker.gleam", 427).
?DOC(
" Resolve a type name (with optional module qualifier) to its full\n"
" module path. Falls back to current_module when the name is unqualified\n"
" and not in the resolver - meaning it's defined in the current module.\n"
).
-spec resolve_type_module(
binary(),
gleam@option:option(binary()),
type_resolver(),
binary()
) -> {ok, binary()} | {error, nil}.
resolve_type_module(Name, Module, Resolver, Current_module) ->
case Module of
{some, Alias} ->
gleam_stdlib:map_get(erlang:element(3, Resolver), Alias);
none ->
case gleam_stdlib:map_get(erlang:element(2, Resolver), Name) of
{ok, Mp} ->
{ok, Mp};
{error, nil} ->
{ok, Current_module}
end
end.
-file("src/libero/walker.gleam", 448).
?DOC(
" Walk a glance.Type and return (module_path, type_name) refs for any\n"
" named custom types found. Uses resolver to map alias/unqualified names\n"
" to their full module paths. `current_module` is the module path of the\n"
" file being walked - used to resolve unqualified names that are defined\n"
" in the same file (not in any import).\n"
).
-spec collect_type_refs(glance:type(), type_resolver(), binary()) -> list({binary(),
binary()}).
collect_type_refs(T, Resolver, Current_module) ->
case T of
{named_type, _, Name, Module, Parameters} ->
Param_refs = gleam@list:flat_map(
Parameters,
fun(P) -> collect_type_refs(P, Resolver, Current_module) end
),
gleam@bool:guard(
is_stdlib_reference(Name, Module, Resolver),
Param_refs,
fun() ->
case resolve_type_module(
Name,
Module,
Resolver,
Current_module
) of
{error, nil} ->
Param_refs;
{ok, Mp} ->
gleam@bool:guard(
is_skipped_module(Mp),
Param_refs,
fun() ->
Original_name = gleam@result:unwrap(
gleam_stdlib:map_get(
erlang:element(4, Resolver),
Name
),
Name
),
lists:append(
[{Mp, Original_name}],
Param_refs
)
end
)
end
end
);
{tuple_type, _, Elements} ->
gleam@list:flat_map(
Elements,
fun(E) -> collect_type_refs(E, Resolver, Current_module) end
);
{function_type, _, _, _} ->
[];
{variable_type, _, _} ->
[];
{hole_type, _, _} ->
[]
end.
-file("src/libero/walker.gleam", 402).
?DOC(
" Collect (module_path, type_name) refs from a variant's fields,\n"
" filtering out visited, skipped, and primitive refs.\n"
).
-spec collect_variant_field_refs(
glance:variant(),
type_resolver(),
binary(),
gleam@set:set({binary(), binary()})
) -> list({binary(), binary()}).
collect_variant_field_refs(Variant, Resolver, Current_module, Visited) ->
Field_refs = gleam@list:flat_map(
erlang:element(3, Variant),
fun(Field) ->
collect_type_refs(
variant_field_type(Field),
Resolver,
Current_module
)
end
),
gleam@list:filter(
Field_refs,
fun(Ref) ->
{Ref_module, Ref_type} = Ref,
(not gleam@set:contains(Visited, Ref) andalso not is_skipped_module(
Ref_module
))
andalso not is_primitive_type(Ref_type)
end
).
-file("src/libero/walker.gleam", 663).
-spec is_upper_grapheme(binary()) -> boolean().
is_upper_grapheme(G) ->
G /= string:lowercase(G).
-file("src/libero/walker.gleam", 649).
-spec build_triples(list(binary()), binary()) -> list({binary(),
binary(),
binary()}).
build_triples(Remaining, Prev) ->
case Remaining of
[] ->
[];
[G] ->
[{Prev, G, <<""/utf8>>}];
[G@1, Next | Rest] ->
[{Prev, G@1, Next} | build_triples([Next | Rest], G@1)]
end.
-file("src/libero/walker.gleam", 623).
?DOC(
" Convert a PascalCase variant name to snake_case for the wire atom.\n"
" \"AdminData\" → \"admin_data\", \"One\" → \"one\", \"TwoOrMore\" → \"two_or_more\".\n"
" Handles consecutive uppercase: \"XMLParser\" → \"xml_parser\".\n"
" Must stay aligned with `snakeCase()` in rpc_ffi.mjs.\n"
).
-spec to_snake_case(binary()) -> binary().
to_snake_case(Name) ->
Graphemes = gleam@string:to_graphemes(Name),
Triples = build_triples(Graphemes, <<""/utf8>>),
gleam@list:index_fold(
Triples,
<<""/utf8>>,
fun(Acc, Triple, I) ->
{Prev, G, Next} = Triple,
case {I =:= 0, is_upper_grapheme(G)} of
{true, _} ->
<<Acc/binary, (string:lowercase(G))/binary>>;
{false, true} ->
Prev_upper = is_upper_grapheme(Prev),
Next_lower = (Next /= <<""/utf8>>) andalso not is_upper_grapheme(
Next
),
case {Prev_upper, Next_lower} of
{true, true} ->
<<<<Acc/binary, "_"/utf8>>/binary,
(string:lowercase(G))/binary>>;
{true, false} ->
<<Acc/binary, (string:lowercase(G))/binary>>;
{_, _} ->
<<<<Acc/binary, "_"/utf8>>/binary,
(string:lowercase(G))/binary>>
end;
{false, false} ->
<<Acc/binary, G/binary>>
end
end
).
-file("src/libero/walker.gleam", 572).
?DOC(
" Resolve a named type: if it's a local type alias, recurse on the aliased\n"
" type; otherwise produce a UserType.\n"
).
-spec resolve_field_type(
binary(),
gleam@option:option(binary()),
list(glance:type()),
type_resolver(),
gleam@dict:dict(binary(), glance:type()),
binary()
) -> libero@field_type:field_type().
resolve_field_type(Name, Module, Parameters, Resolver, Aliases, Current_module) ->
case {Module, gleam_stdlib:map_get(Aliases, Name)} of
{none, {ok, Aliased_type}} ->
field_type_of(Aliased_type, Resolver, Aliases, Current_module);
{_, _} ->
Args = gleam@list:map(
Parameters,
fun(P) ->
field_type_of(P, Resolver, Aliases, Current_module)
end
),
Resolved_module = resolve_type_module(
Name,
Module,
Resolver,
Current_module
),
Mp = gleam@result:unwrap(Resolved_module, Current_module),
Original_name = gleam@result:unwrap(
gleam_stdlib:map_get(erlang:element(4, Resolver), Name),
Name
),
{user_type, Mp, Original_name, Args}
end.
-file("src/libero/walker.gleam", 546).
?DOC(
" Map a stdlib NamedType reference to its FieldType. Caller must have\n"
" already verified via `is_stdlib_reference` that this isn't a\n"
" user-defined type shadowing a primitive name.\n"
).
-spec stdlib_field_type(
binary(),
list(glance:type()),
type_resolver(),
gleam@dict:dict(binary(), glance:type()),
binary()
) -> libero@field_type:field_type().
stdlib_field_type(Name, Parameters, Resolver, Aliases, Current_module) ->
Recurse = fun(T) -> field_type_of(T, Resolver, Aliases, Current_module) end,
case libero@field_type:builtin_field_type(Name, Parameters, Recurse) of
{ok, Ft} ->
Ft;
{error, nil} ->
{user_type,
Current_module,
Name,
gleam@list:map(Parameters, Recurse)}
end.
-file("src/libero/walker.gleam", 499).
?DOC(
" Convert a glance.Type into a FieldType, resolving named types via the resolver.\n"
" Type aliases in `aliases` are resolved transparently to their underlying type.\n"
).
-spec field_type_of(
glance:type(),
type_resolver(),
gleam@dict:dict(binary(), glance:type()),
binary()
) -> libero@field_type:field_type().
field_type_of(T, Resolver, Aliases, Current_module) ->
case T of
{variable_type, _, Name} ->
{type_var, Name};
{tuple_type, _, Elements} ->
{tuple_of,
gleam@list:map(
Elements,
fun(E) ->
field_type_of(E, Resolver, Aliases, Current_module)
end
)};
{function_type, _, _, _} ->
{type_var, <<"_fn"/utf8>>};
{hole_type, _, _} ->
{type_var, <<"_"/utf8>>};
{named_type, _, Name@1, Module, Parameters} ->
case is_stdlib_reference(Name@1, Module, Resolver) of
true ->
stdlib_field_type(
Name@1,
Parameters,
Resolver,
Aliases,
Current_module
);
false ->
resolve_field_type(
Name@1,
Module,
Parameters,
Resolver,
Aliases,
Current_module
)
end
end.
-file("src/libero/walker.gleam", 392).
?DOC(" Check if a glance type is `Float` (unqualified or gleam-qualified).\n").
-spec is_float_type(glance:type()) -> boolean().
is_float_type(T) ->
case T of
{named_type, _, <<"Float"/utf8>>, none, _} ->
true;
{named_type, _, <<"Float"/utf8>>, {some, <<"gleam"/utf8>>}, _} ->
true;
_ ->
false
end.
-file("src/libero/walker.gleam", 381).
?DOC(
" Return 0-based indices of fields whose outermost type is Float.\n"
" Used by the JS ETF encoder to distinguish Int from Float\n"
" (JS erases this distinction at runtime, but ETF and BEAM need it).\n"
).
-spec detect_float_fields(list(glance:variant_field())) -> list(integer()).
detect_float_fields(Fields) ->
_pipe = gleam@list:index_fold(
Fields,
[],
fun(Acc, Field, Index) ->
case is_float_type(variant_field_type(Field)) of
true ->
[Index | Acc];
false ->
Acc
end
end
),
lists:reverse(_pipe).
-file("src/libero/walker.gleam", 601).
?DOC(
" Build a map from type alias names to their underlying glance.Type.\n"
" Used to resolve aliases transparently in field_type_of.\n"
).
-spec build_alias_map(list(glance:definition(glance:type_alias()))) -> gleam@dict:dict(binary(), glance:type()).
build_alias_map(Type_aliases) ->
gleam@list:fold(
Type_aliases,
maps:new(),
fun(Acc, Def) ->
gleam@dict:insert(
Acc,
erlang:element(3, erlang:element(3, Def)),
erlang:element(6, erlang:element(3, Def))
)
end
).
-file("src/libero/walker.gleam", 609).
-spec build_type_resolver(list(glance:definition(glance:import()))) -> type_resolver().
build_type_resolver(Imports) ->
{type_resolver,
libero@scanner:build_type_import_map(Imports),
libero@scanner:build_alias_resolution_map(Imports),
libero@scanner:build_type_alias_originals(Imports)}.
-file("src/libero/walker.gleam", 364).
?DOC(" Parse a module, returning the cached version if available.\n").
-spec load_ast(binary(), binary(), gleam@dict:dict(binary(), glance:module_())) -> {ok,
{glance:module_(), gleam@dict:dict(binary(), glance:module_())}} |
{error, libero@gen_error:gen_error()}.
load_ast(Module_path, File_path, Parsed_cache) ->
case gleam_stdlib:map_get(Parsed_cache, Module_path) of
{ok, Ast} ->
{ok, {Ast, Parsed_cache}};
{error, nil} ->
gleam@result:map(
libero@scanner:parse_module(File_path),
fun(Ast@1) ->
{Ast@1, gleam@dict:insert(Parsed_cache, Module_path, Ast@1)}
end
)
end.
-file("src/libero/walker.gleam", 295).
-spec process_type_ast_custom(
binary(),
binary(),
glance:module_(),
walker_state()
) -> {ok, list(discovered_type())} | {error, list(libero@gen_error:gen_error())}.
process_type_ast_custom(Module_path, Type_name, Ast, State) ->
case gleam@list:find(
erlang:element(3, Ast),
fun(D) -> erlang:element(3, erlang:element(3, D)) =:= Type_name end
) of
{error, nil} ->
do_walk(
{walker_state,
erlang:element(2, State),
erlang:element(3, State),
erlang:element(4, State),
erlang:element(5, State),
erlang:element(6, State),
[{type_not_found, Module_path, Type_name} |
erlang:element(7, State)]}
);
{ok, Ct_def} ->
Custom_type = erlang:element(3, Ct_def),
Resolver = build_type_resolver(erlang:element(2, Ast)),
Aliases = build_alias_map(erlang:element(4, Ast)),
{Variants_rev, New_queue_items_rev} = gleam@list:fold(
erlang:element(7, Custom_type),
{[], []},
fun(Acc, Variant) ->
{Disc_acc, Queue_acc} = Acc,
Float_indices = detect_float_fields(
erlang:element(3, Variant)
),
Fields = gleam@list:map(
erlang:element(3, Variant),
fun(Field) ->
field_type_of(
variant_field_type(Field),
Resolver,
Aliases,
Module_path
)
end
),
Disc_item = {discovered_variant,
Module_path,
erlang:element(2, Variant),
to_snake_case(erlang:element(2, Variant)),
Float_indices,
Fields},
Field_refs = collect_variant_field_refs(
Variant,
Resolver,
Module_path,
erlang:element(3, State)
),
{[Disc_item | Disc_acc],
lists:append(Field_refs, Queue_acc)}
end
),
Discovered_type = {discovered_type,
Module_path,
Type_name,
erlang:element(6, Custom_type),
lists:reverse(Variants_rev)},
New_queue_items = lists:reverse(New_queue_items_rev),
do_walk(
{walker_state,
lists:append(New_queue_items, erlang:element(2, State)),
erlang:element(3, State),
[Discovered_type | erlang:element(4, State)],
erlang:element(5, State),
erlang:element(6, State),
erlang:element(7, State)}
)
end.
-file("src/libero/walker.gleam", 262).
-spec process_type_ast(binary(), binary(), glance:module_(), walker_state()) -> {ok,
list(discovered_type())} |
{error, list(libero@gen_error:gen_error())}.
process_type_ast(Module_path, Type_name, Ast, State) ->
case gleam@list:find(
erlang:element(4, Ast),
fun(D) -> erlang:element(3, erlang:element(3, D)) =:= Type_name end
) of
{ok, Alias_def} ->
Resolver = build_type_resolver(erlang:element(2, Ast)),
Target_refs = collect_type_refs(
erlang:element(6, erlang:element(3, Alias_def)),
Resolver,
Module_path
),
New_refs = gleam@list:filter(
Target_refs,
fun(Ref) ->
{Ref_module, Ref_type} = Ref,
(not gleam@set:contains(erlang:element(3, State), Ref)
andalso not is_skipped_module(Ref_module))
andalso not is_primitive_type(Ref_type)
end
),
do_walk(
{walker_state,
lists:append(New_refs, erlang:element(2, State)),
erlang:element(3, State),
erlang:element(4, State),
erlang:element(5, State),
erlang:element(6, State),
erlang:element(7, State)}
);
{error, nil} ->
process_type_ast_custom(Module_path, Type_name, Ast, State)
end.
-file("src/libero/walker.gleam", 243).
-spec process_type_file(binary(), binary(), binary(), walker_state()) -> {ok,
list(discovered_type())} |
{error, list(libero@gen_error:gen_error())}.
process_type_file(Module_path, Type_name, File_path, State) ->
case load_ast(Module_path, File_path, erlang:element(6, State)) of
{error, E} ->
do_walk(
{walker_state,
erlang:element(2, State),
erlang:element(3, State),
erlang:element(4, State),
erlang:element(5, State),
erlang:element(6, State),
[E | erlang:element(7, State)]}
);
{ok, {Ast, New_cache}} ->
process_type_ast(
Module_path,
Type_name,
Ast,
{walker_state,
erlang:element(2, State),
erlang:element(3, State),
erlang:element(4, State),
erlang:element(5, State),
New_cache,
erlang:element(7, State)}
)
end.
-file("src/libero/walker.gleam", 224).
-spec process_type(binary(), binary(), walker_state()) -> {ok,
list(discovered_type())} |
{error, list(libero@gen_error:gen_error())}.
process_type(Module_path, Type_name, State) ->
case gleam_stdlib:map_get(erlang:element(5, State), Module_path) of
{error, nil} ->
do_walk(
{walker_state,
erlang:element(2, State),
erlang:element(3, State),
erlang:element(4, State),
erlang:element(5, State),
erlang:element(6, State),
[{unresolved_type_module, Module_path, Type_name} |
erlang:element(7, State)]}
);
{ok, File_path} ->
process_type_file(Module_path, Type_name, File_path, State)
end.
-file("src/libero/walker.gleam", 199).
-spec do_walk(walker_state()) -> {ok, list(discovered_type())} |
{error, list(libero@gen_error:gen_error())}.
do_walk(State) ->
case erlang:element(2, State) of
[] ->
case erlang:element(7, State) of
[] ->
{ok, lists:reverse(erlang:element(4, State))};
_ ->
{error, lists:reverse(erlang:element(7, State))}
end;
[{Module_path, Type_name} | Rest_queue] ->
Key = {Module_path, Type_name},
gleam@bool:lazy_guard(
gleam@set:contains(erlang:element(3, State), Key),
fun() ->
do_walk(
{walker_state,
Rest_queue,
erlang:element(3, State),
erlang:element(4, State),
erlang:element(5, State),
erlang:element(6, State),
erlang:element(7, State)}
)
end,
fun() ->
State@1 = {walker_state,
Rest_queue,
gleam@set:insert(erlang:element(3, State), Key),
erlang:element(4, State),
erlang:element(5, State),
erlang:element(6, State),
erlang:element(7, State)},
process_type(Module_path, Type_name, State@1)
end
)
end.
-file("src/libero/walker.gleam", 185).
-spec read_public_type_pairs(binary(), binary()) -> {ok,
list({binary(), binary()})} |
{error, libero@gen_error:gen_error()}.
read_public_type_pairs(Module_path, File_path) ->
gleam@result:map(
libero@scanner:parse_module(File_path),
fun(Parsed) ->
gleam@list:fold(
erlang:element(3, Parsed),
[],
fun(Acc, Ct) ->
{definition, _, T} = Ct,
case erlang:element(4, T) =:= public of
true ->
[{Module_path, erlang:element(3, T)} | Acc];
false ->
Acc
end
end
)
end
).
-file("src/libero/walker.gleam", 138).
?DOC(
" Walk all exported custom types from a list of shared source files.\n"
" Seeds from every public type in shared/, since the codegen pipeline\n"
" needs decoders for any type that may appear in a handler's params or\n"
" return type.\n"
).
-spec walk_shared_types(binary()) -> {ok, list(discovered_type())} |
{error, list(libero@gen_error:gen_error())}.
walk_shared_types(Shared_src) ->
gleam@result:'try'(
begin
_pipe = libero@scanner:walk_directory(Shared_src),
gleam@result:map_error(_pipe, fun(Err) -> [Err] end)
end,
fun(Files) ->
Module_files = gleam@list:fold(
Files,
maps:new(),
fun(Acc, File_path) ->
Module_path = libero@scanner:derive_module_path(File_path),
gleam@dict:insert(Acc, Module_path, File_path)
end
),
{Seed_set, Errors_rev} = gleam@list:fold(
Files,
{gleam@set:new(), []},
fun(Acc@1, File_path@1) ->
{Set_acc, Errs_acc} = Acc@1,
Module_path@1 = libero@scanner:derive_module_path(
File_path@1
),
case read_public_type_pairs(Module_path@1, File_path@1) of
{ok, Pairs} ->
{gleam@list:fold(
Pairs,
Set_acc,
fun gleam@set:insert/2
),
Errs_acc};
{error, Err@1} ->
{Set_acc, [Err@1 | Errs_acc]}
end
end
),
case Errors_rev of
[] ->
do_walk(
{walker_state,
gleam@set:to_list(Seed_set),
gleam@set:new(),
[],
Module_files,
maps:new(),
[]}
);
_ ->
{error, lists:reverse(Errors_rev)}
end
end
).