Current section

Files

Jump to
caffeine_lang src caffeine_lang@frontend@lowering.erl
Raw

src/caffeine_lang@frontend@lowering.erl

-module(caffeine_lang@frontend@lowering).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/caffeine_lang/frontend/lowering.gleam").
-export([literal_to_value/1, lower_measurements/1, lower_expectations/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-file("src/caffeine_lang/frontend/lowering.gleam", 444).
-spec transform_template_vars_loop(binary(), list(binary())) -> binary().
transform_template_vars_loop(Remaining, Acc) ->
case gleam@string:split_once(Remaining, <<"$"/utf8>>) of
{ok, {Before, After}} ->
case gleam_stdlib:string_starts_with(After, <<"$"/utf8>>) of
true ->
transform_template_vars_loop(
gleam@string:drop_start(After, 1),
[<<"$$"/utf8>>, Before | Acc]
);
false ->
case gleam@string:split_once(After, <<"$"/utf8>>) of
{ok, {Var_content, Rest}} ->
Transformed = gleam@string:replace(
Var_content,
<<".not"/utf8>>,
<<":not"/utf8>>
),
transform_template_vars_loop(
Rest,
[<<"$$"/utf8>>,
Transformed,
<<"$$"/utf8>>,
Before |
Acc]
);
{error, nil} ->
erlang:list_to_binary(
lists:reverse(
[After, <<"$"/utf8>>, Before | Acc]
)
)
end
end;
{error, nil} ->
erlang:list_to_binary(lists:reverse([Remaining | Acc]))
end.
-file("src/caffeine_lang/frontend/lowering.gleam", 440).
?DOC(
" Transforms template variables from $var->attr$ to $$var->attr$$ format.\n"
" Also handles $var->attr.not$ to $$var->attr:not$$ format.\n"
).
-spec transform_template_vars(binary()) -> binary().
transform_template_vars(S) ->
transform_template_vars_loop(S, []).
-file("src/caffeine_lang/frontend/lowering.gleam", 408).
?DOC(false).
-spec literal_to_value(caffeine_lang@frontend@ast:literal()) -> caffeine_lang@value:value().
literal_to_value(Lit) ->
case Lit of
{literal_string, S} ->
{string_value, transform_template_vars(S)};
{literal_integer, I} ->
{int_value, I};
{literal_float, F} ->
{float_value, F};
{literal_percentage, F@1} ->
{percentage_value, F@1};
{literal_duration, Amount, Unit} ->
case caffeine_lang@value:duration_unit_from_string(Unit) of
{ok, Parsed_unit} ->
{duration_value, Amount, Parsed_unit};
{error, nil} ->
nil_value
end;
literal_true ->
{bool_value, true};
literal_false ->
{bool_value, false};
{literal_list, Elements} ->
{list_value, gleam@list:map(Elements, fun literal_to_value/1)};
{literal_struct, Fields, _} ->
_pipe = Fields,
_pipe@1 = gleam@list:filter_map(
_pipe,
fun(Field) -> case erlang:element(3, Field) of
{literal_value, Inner} ->
{ok,
{erlang:element(2, Field),
literal_to_value(Inner)}};
{type_value, _} ->
{error, nil}
end end
),
_pipe@2 = maps:from_list(_pipe@1),
{dict_value, _pipe@2}
end.
-file("src/caffeine_lang/frontend/lowering.gleam", 395).
?DOC(" Converts a struct's literal-valued fields to an inputs dict.\n").
-spec struct_to_inputs(caffeine_lang@frontend@ast:struct()) -> gleam@dict:dict(binary(), caffeine_lang@value:value()).
struct_to_inputs(S) ->
_pipe = erlang:element(2, S),
_pipe@1 = gleam@list:filter_map(
_pipe,
fun(Field) -> case erlang:element(3, Field) of
{literal_value, Lit} ->
{ok, {erlang:element(2, Field), literal_to_value(Lit)}};
{type_value, _} ->
{error, nil}
end end
),
maps:from_list(_pipe@1).
-file("src/caffeine_lang/frontend/lowering.gleam", 383).
?DOC(" Resolves inner types of a refinement.\n").
-spec resolve_refinement(
caffeine_lang@types:refinement_types(caffeine_lang@types:parsed_type()),
gleam@dict:dict(binary(), caffeine_lang@types:parsed_type())
) -> caffeine_lang@types:refinement_types(caffeine_lang@types:accepted_types()).
resolve_refinement(Refinement, Aliases) ->
case Refinement of
{one_of, Inner, Values} ->
{one_of, resolve_type_aliases(Inner, Aliases), Values};
{inclusive_range, Inner@1, Low, High} ->
{inclusive_range, resolve_type_aliases(Inner@1, Aliases), Low, High}
end.
-file("src/caffeine_lang/frontend/lowering.gleam", 371).
?DOC(" Resolves inner types of a modifier.\n").
-spec resolve_modifier(
caffeine_lang@types:modifier_types(caffeine_lang@types:parsed_type()),
gleam@dict:dict(binary(), caffeine_lang@types:parsed_type())
) -> caffeine_lang@types:modifier_types(caffeine_lang@types:accepted_types()).
resolve_modifier(Modifier, Aliases) ->
case Modifier of
{optional, Inner} ->
{optional, resolve_type_aliases(Inner, Aliases)};
{defaulted, Inner@1, Default} ->
{defaulted, resolve_type_aliases(Inner@1, Aliases), Default}
end.
-file("src/caffeine_lang/frontend/lowering.gleam", 356).
?DOC(" Resolves inner types of a collection.\n").
-spec resolve_collection(
caffeine_lang@types:collection_types(caffeine_lang@types:parsed_type()),
gleam@dict:dict(binary(), caffeine_lang@types:parsed_type())
) -> caffeine_lang@types:collection_types(caffeine_lang@types:accepted_types()).
resolve_collection(Collection, Aliases) ->
case Collection of
{list, Inner} ->
{list, resolve_type_aliases(Inner, Aliases)};
{dict, Key, Value} ->
{dict,
resolve_type_aliases(Key, Aliases),
resolve_type_aliases(Value, Aliases)}
end.
-file("src/caffeine_lang/frontend/lowering.gleam", 330).
?DOC(
" Resolves all ParsedTypeAliasRef instances, converting ParsedType to AcceptedTypes.\n"
" This is the resolution boundary where parsed types become fully resolved types.\n"
).
-spec resolve_type_aliases(
caffeine_lang@types:parsed_type(),
gleam@dict:dict(binary(), caffeine_lang@types:parsed_type())
) -> caffeine_lang@types:accepted_types().
resolve_type_aliases(T, Aliases) ->
case T of
{parsed_primitive, P} ->
{primitive_type, P};
{parsed_type_alias_ref, Name} ->
case gleam_stdlib:map_get(Aliases, Name) of
{ok, Resolved} ->
resolve_type_aliases(Resolved, Aliases);
{error, _} ->
{primitive_type, string}
end;
{parsed_collection, Collection} ->
{collection_type, resolve_collection(Collection, Aliases)};
{parsed_modifier, Modifier} ->
{modifier_type, resolve_modifier(Modifier, Aliases)};
{parsed_refinement, Refinement} ->
{refinement_type, resolve_refinement(Refinement, Aliases)};
{parsed_record, Fields} ->
{record_type,
gleam@dict:map_values(
Fields,
fun(_, V) -> resolve_type_aliases(V, Aliases) end
)}
end.
-file("src/caffeine_lang/frontend/lowering.gleam", 311).
?DOC(
" Converts a struct's type-valued fields to a params dict.\n"
" Resolves type alias references before storing.\n"
).
-spec struct_to_params(
caffeine_lang@frontend@ast:struct(),
gleam@dict:dict(binary(), caffeine_lang@types:parsed_type())
) -> gleam@dict:dict(binary(), caffeine_lang@types:accepted_types()).
struct_to_params(S, Type_aliases) ->
_pipe = erlang:element(2, S),
_pipe@1 = gleam@list:filter_map(
_pipe,
fun(Field) -> case erlang:element(3, Field) of
{type_value, T} ->
Resolved = resolve_type_aliases(T, Type_aliases),
{ok, {erlang:element(2, Field), Resolved}};
{literal_value, _} ->
{error, nil}
end end
),
maps:from_list(_pipe@1).
-file("src/caffeine_lang/frontend/lowering.gleam", 299).
?DOC(
" Removes duplicate field names, keeping the last occurrence (allows overrides).\n"
" Returns fields sorted by name for consistent output.\n"
).
-spec dedupe_fields(list(caffeine_lang@frontend@ast:field())) -> list(caffeine_lang@frontend@ast:field()).
dedupe_fields(Fields) ->
_pipe = Fields,
_pipe@1 = gleam@list:fold(
_pipe,
maps:new(),
fun(Acc, Field) ->
gleam@dict:insert(Acc, erlang:element(2, Field), Field)
end
),
_pipe@2 = maps:to_list(_pipe@1),
_pipe@3 = gleam@list:sort(
_pipe@2,
fun(A, B) ->
gleam@string:compare(erlang:element(1, A), erlang:element(1, B))
end
),
gleam@list:map(_pipe@3, fun(Pair) -> erlang:element(2, Pair) end).
-file("src/caffeine_lang/frontend/lowering.gleam", 241).
?DOC(" Collects fields from extended extendables matching a given kind.\n").
-spec collect_extended_fields(
list(binary()),
gleam@dict:dict(binary(), caffeine_lang@frontend@ast:extendable()),
caffeine_lang@frontend@ast:extendable_kind()
) -> list(caffeine_lang@frontend@ast:field()).
collect_extended_fields(Extends, Extendables, Kind) ->
_pipe = Extends,
gleam@list:flat_map(
_pipe,
fun(Name) -> case gleam_stdlib:map_get(Extendables, Name) of
{ok, Ext} when erlang:element(3, Ext) =:= Kind ->
erlang:element(2, erlang:element(4, Ext));
_ ->
[]
end end
).
-file("src/caffeine_lang/frontend/lowering.gleam", 257).
?DOC(
" Merges extended fields into a measurement item's requires and provides.\n"
" Order: extended extendables left-to-right, then item's own fields (can override).\n"
).
-spec merge_measurement_extends(
caffeine_lang@frontend@ast:measurement_item(),
gleam@dict:dict(binary(), caffeine_lang@frontend@ast:extendable())
) -> {caffeine_lang@frontend@ast:struct(), caffeine_lang@frontend@ast:struct()}.
merge_measurement_extends(Item, Extendables) ->
Requires_fields = begin
_pipe = collect_extended_fields(
erlang:element(4, Item),
Extendables,
extendable_requires
),
_pipe@1 = lists:append(
_pipe,
erlang:element(2, erlang:element(5, Item))
),
dedupe_fields(_pipe@1)
end,
Provides_fields = begin
_pipe@2 = collect_extended_fields(
erlang:element(4, Item),
Extendables,
extendable_provides
),
_pipe@3 = lists:append(
_pipe@2,
erlang:element(2, erlang:element(6, Item))
),
dedupe_fields(_pipe@3)
end,
{{struct, Requires_fields, []}, {struct, Provides_fields, []}}.
-file("src/caffeine_lang/frontend/lowering.gleam", 66).
?DOC(" Generates a single measurement from an AST item.\n").
-spec generate_measurement_item(
caffeine_lang@frontend@ast:measurement_item(),
gleam@dict:dict(binary(), caffeine_lang@frontend@ast:extendable()),
gleam@dict:dict(binary(), caffeine_lang@types:parsed_type())
) -> caffeine_lang@linker@measurements:measurement(caffeine_lang@linker@measurements:raw()).
generate_measurement_item(Item, Extendables, Type_aliases) ->
{Merged_requires, Merged_provides} = merge_measurement_extends(
Item,
Extendables
),
Params = struct_to_params(Merged_requires, Type_aliases),
Inputs = struct_to_inputs(Merged_provides),
{measurement,
erlang:element(2, Item),
Params,
Inputs,
erlang:element(3, Item)}.
-file("src/caffeine_lang/frontend/lowering.gleam", 49).
?DOC(" Builds a map of extendable name to extendable for quick lookup.\n").
-spec build_extendable_map(list(caffeine_lang@frontend@ast:extendable())) -> gleam@dict:dict(binary(), caffeine_lang@frontend@ast:extendable()).
build_extendable_map(Extendables) ->
_pipe = Extendables,
_pipe@1 = gleam@list:map(_pipe, fun(E) -> {erlang:element(2, E), E} end),
maps:from_list(_pipe@1).
-file("src/caffeine_lang/frontend/lowering.gleam", 58).
?DOC(" Builds a map of type alias name to its parsed type for quick lookup.\n").
-spec build_type_alias_map(list(caffeine_lang@frontend@ast:type_alias())) -> gleam@dict:dict(binary(), caffeine_lang@types:parsed_type()).
build_type_alias_map(Type_aliases) ->
_pipe = caffeine_lang@frontend@ast:build_type_alias_pairs(Type_aliases),
maps:from_list(_pipe).
-file("src/caffeine_lang/frontend/lowering.gleam", 27).
?DOC(false).
-spec lower_measurements(
caffeine_lang@frontend@ast:measurements_file(caffeine_lang@frontend@ast:validated())
) -> list(caffeine_lang@linker@measurements:measurement(caffeine_lang@linker@measurements:raw())).
lower_measurements(File) ->
Type_aliases = build_type_alias_map(erlang:element(2, File)),
Extendables = build_extendable_map(erlang:element(3, File)),
_pipe = erlang:element(4, File),
gleam@list:map(
_pipe,
fun(Item) ->
generate_measurement_item(Item, Extendables, Type_aliases)
end
).
-file("src/caffeine_lang/frontend/lowering.gleam", 232).
-spec strip_doc_comment_text(binary()) -> binary().
strip_doc_comment_text(Text) ->
Stripped = case gleam_stdlib:string_starts_with(Text, <<" "/utf8>>) of
true ->
gleam@string:drop_start(Text, 1);
false ->
Text
end,
gleam@string:trim_end(Stripped).
-file("src/caffeine_lang/frontend/lowering.gleam", 217).
?DOC(
" Extracts `###` doc-comment text from a node's leading comments and joins\n"
" the lines with `\\n`. Returns `None` when no doc comments are present.\n"
" Each line drops a single leading space (the universal `### ` form) and\n"
" rstrips trailing whitespace. `#` and `##` comments are ignored — they're\n"
" treated as section headers / inline notes, not SLO descriptions.\n"
).
-spec extract_doc_description(list(caffeine_lang@frontend@ast:comment())) -> gleam@option:option(binary()).
extract_doc_description(Comments) ->
Lines = begin
_pipe = Comments,
gleam@list:filter_map(_pipe, fun(C) -> case C of
{doc_comment, Text} ->
{ok, strip_doc_comment_text(Text)};
_ ->
{error, nil}
end end)
end,
case Lines of
[] ->
none;
_ ->
{some, gleam@string:join(Lines, <<"\n"/utf8>>)}
end.
-file("src/caffeine_lang/frontend/lowering.gleam", 190).
-spec deps_to_value(list(caffeine_lang@frontend@ast:dependency())) -> caffeine_lang@value:value().
deps_to_value(Deps) ->
{Hard, Soft} = gleam@list:partition(
Deps,
fun(D) -> case erlang:element(2, D) of
hard_dep ->
true;
soft_dep ->
false
end end
),
Hard_list = begin
_pipe = Hard,
_pipe@1 = gleam@list:map(
_pipe,
fun(D@1) -> {string_value, erlang:element(3, D@1)} end
),
{list_value, _pipe@1}
end,
Soft_list = begin
_pipe@2 = Soft,
_pipe@3 = gleam@list:map(
_pipe@2,
fun(D@2) -> {string_value, erlang:element(3, D@2)} end
),
{list_value, _pipe@3}
end,
_pipe@4 = maps:new(),
_pipe@5 = gleam@dict:insert(_pipe@4, <<"hard"/utf8>>, Hard_list),
_pipe@6 = gleam@dict:insert(_pipe@5, <<"soft"/utf8>>, Soft_list),
{dict_value, _pipe@6}.
-file("src/caffeine_lang/frontend/lowering.gleam", 176).
-spec maybe_insert_depends_on(
gleam@dict:dict(binary(), caffeine_lang@value:value()),
gleam@option:option(caffeine_lang@frontend@ast:assumes())
) -> gleam@dict:dict(binary(), caffeine_lang@value:value()).
maybe_insert_depends_on(Inputs, Assumes) ->
case Assumes of
none ->
Inputs;
{some, A} ->
case erlang:element(2, A) of
[] ->
Inputs;
_ ->
gleam@dict:insert(
Inputs,
<<"depends_on"/utf8>>,
deps_to_value(erlang:element(2, A))
)
end
end.
-file("src/caffeine_lang/frontend/lowering.gleam", 156).
-spec maybe_insert_below_ms(
gleam@dict:dict(binary(), caffeine_lang@value:value()),
gleam@option:option(caffeine_lang@frontend@ast:duration_literal())
) -> gleam@dict:dict(binary(), caffeine_lang@value:value()).
maybe_insert_below_ms(Inputs, Below) ->
case Below of
none ->
Inputs;
{some, D} ->
case caffeine_lang@value:duration_unit_from_string(
erlang:element(3, D)
) of
{ok, Unit} ->
gleam@dict:insert(
Inputs,
<<"below_ms"/utf8>>,
{float_value,
caffeine_lang@value:duration_to_milliseconds(
erlang:element(2, D),
Unit
)}
);
{error, nil} ->
Inputs
end
end.
-file("src/caffeine_lang/frontend/lowering.gleam", 148).
-spec float_floor_to_int(float()) -> integer().
float_floor_to_int(F) ->
Rounded = erlang:round(F - 0.5),
case Rounded < 0 of
true ->
0;
false ->
Rounded
end.
-file("src/caffeine_lang/frontend/lowering.gleam", 133).
?DOC(
" Converts a duration literal to whole days. Non-day units are normalized\n"
" through `value.duration_to_milliseconds` and divided down. Floors fractional\n"
" days (e.g. `1h` -> 0 days, `25h` -> 1 day) since the IR currently carries\n"
" `window_in_days` as an integer. Negative values clamp to 0.\n"
).
-spec duration_to_days(caffeine_lang@frontend@ast:duration_literal()) -> integer().
duration_to_days(D) ->
case caffeine_lang@value:duration_unit_from_string(erlang:element(3, D)) of
{ok, Unit} ->
Ms = caffeine_lang@value:duration_to_milliseconds(
erlang:element(2, D),
Unit
),
Days_float = Ms / 86400000.0,
case Days_float < +0.0 of
true ->
0;
false ->
float_floor_to_int(Days_float)
end;
{error, nil} ->
0
end.
-file("src/caffeine_lang/frontend/lowering.gleam", 281).
?DOC(
" Merges extended fields into an expect item's `with: {...}` args.\n"
" Order: extended extendables left-to-right, then the item's own with-fields\n"
" (can override). Unmeasured expectations have no `with:`; for them only the\n"
" extendable fields contribute.\n"
).
-spec merge_expect_with_args(
caffeine_lang@frontend@ast:expect_item(),
gleam@dict:dict(binary(), caffeine_lang@frontend@ast:extendable())
) -> caffeine_lang@frontend@ast:struct().
merge_expect_with_args(Item, Extendables) ->
Item_fields = case erlang:element(5, erlang:element(5, Item)) of
{some, Mb} ->
erlang:element(2, erlang:element(3, Mb));
none ->
[]
end,
Merged = begin
_pipe = collect_extended_fields(
erlang:element(3, Item),
Extendables,
extendable_provides
),
_pipe@1 = lists:append(_pipe, Item_fields),
dedupe_fields(_pipe@1)
end,
{struct, Merged, []}.
-file("src/caffeine_lang/frontend/lowering.gleam", 94).
?DOC(
" Generates a single expectation from a new-envelope AST item.\n"
"\n"
" Flattens the structured envelope (Assumes section, Guarantees clause,\n"
" optional `as measured by ... with: {...}`) into the flat `inputs` dict the\n"
" linker IR builder expects. Keys produced:\n"
" - \"threshold\": PercentageValue from `Guarantees N%`\n"
" - \"window_in_days\": IntValue from `over <dur> window` (any unit normalized to days)\n"
" - \"depends_on\": DictValue with \"hard\"/\"soft\" -> ListValue(StringValue) from `Assumes:`\n"
" - <each `with:` field>: literal value (after extendable merge)\n"
).
-spec generate_expect_item(
caffeine_lang@frontend@ast:expect_item(),
gleam@dict:dict(binary(), caffeine_lang@frontend@ast:extendable())
) -> caffeine_lang@linker@expectations:expectation().
generate_expect_item(Item, Extendables) ->
With_args = merge_expect_with_args(Item, Extendables),
Base_inputs = struct_to_inputs(With_args),
Inputs = begin
_pipe = Base_inputs,
_pipe@1 = gleam@dict:insert(
_pipe,
<<"threshold"/utf8>>,
{percentage_value, erlang:element(2, erlang:element(5, Item))}
),
_pipe@2 = gleam@dict:insert(
_pipe@1,
<<"window_in_days"/utf8>>,
{int_value,
duration_to_days(erlang:element(4, erlang:element(5, Item)))}
),
_pipe@3 = maybe_insert_below_ms(
_pipe@2,
erlang:element(3, erlang:element(5, Item))
),
maybe_insert_depends_on(_pipe@3, erlang:element(4, Item))
end,
Measurement_ref = case erlang:element(5, erlang:element(5, Item)) of
{some, Mb} ->
{some, erlang:element(2, Mb)};
none ->
none
end,
Description = extract_doc_description(erlang:element(6, Item)),
{expectation, erlang:element(2, Item), Measurement_ref, Inputs, Description}.
-file("src/caffeine_lang/frontend/lowering.gleam", 41).
?DOC(false).
-spec lower_expectations(
caffeine_lang@frontend@ast:expects_file(caffeine_lang@frontend@ast:validated())
) -> list(caffeine_lang@linker@expectations:expectation()).
lower_expectations(File) ->
Extendables = build_extendable_map(erlang:element(2, File)),
_pipe = erlang:element(3, File),
gleam@list:map(
_pipe,
fun(Item) -> generate_expect_item(Item, Extendables) end
).