diff --git a/CHANGELOG.md b/CHANGELOG.md index 1922f7a..a88aea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ - `Enum.at` on a pipeline stage is the iterator's `nth`, collecting no list. - A filter over copied tuples matches each by reference, as `|&(set, field)|`. - `Enum.count`, `length`, and `byte_size` stay `usize` where one is expected. +- A function a `defrustmacro` defines can name the module's own types in its + spec, as a `defrust` can. +- A fallible call used as the receiver of a method its result lacks, as + `decoder.read_bool().encode(env)`, propagates its error first. +- A loop over a slice, or a list behind a reference, reads each item by + reference, so passing one on where a reference is expected borrows it once. ## 1.0.0-rc.18 - 2026-10-26 diff --git a/lib/rustq/meta.ex b/lib/rustq/meta.ex index 5303761..e4b37c2 100644 --- a/lib/rustq/meta.ex +++ b/lib/rustq/meta.ex @@ -519,7 +519,14 @@ defmodule RustQ.Meta do rust_macro_index ) ), - built_macros: RustMacro.items(rust_macros, rust_modules, env, callables, rust_macro_index), + built_macros: + RustMacro.items(rust_macros, %{ + rust_modules: rust_modules, + env: env, + callables: callables, + index: rust_macro_index, + type_aliases: type_aliases + }), local_callables: local_callables, rust_macros: rust_macros, type_aliases: own_type_aliases, diff --git a/lib/rustq/meta/lower.ex b/lib/rustq/meta/lower.ex index 5c0e3f9..10d55a5 100644 --- a/lib/rustq/meta/lower.ex +++ b/lib/rustq/meta/lower.ex @@ -128,6 +128,7 @@ defmodule RustQ.Meta.Lower do @keyed_collections [:HashMap, :HashSet, :BTreeMap, :BTreeSet] @stdlib_modules [:Kernel, :Enum, :List, :Map, :MapSet, :String, :Tuple, :Range] @iterator_option_methods StdMethods.iterator_option() + @wrapper_methods StdMethods.wrapper() # An empty map or set filled through `entry(key).or_default()` leaves Rust without its value # type; when RustQ knows it, the binding states it. @@ -2035,8 +2036,19 @@ defmodule RustQ.Meta.Lower do end end - defp lower_method_receiver(receiver, _receiver_type, _function, %Context{} = context), - do: lower_expr(receiver, context) + # A fallible receiver of a method its result lacks, as `decoder.read_bool().encode(env)`, + # propagates its error first, as a fallible operand does. + defp lower_method_receiver(receiver, receiver_type, function, %Context{} = context) do + if fallible_receiver?(receiver_type, function, context), + do: A.try(lower_expr(receiver, context)), + else: lower_expr(receiver, context) + end + + defp fallible_receiver?(%Type{kind: kind}, function, %Context{} = context) + when kind in [:result, :nif_result], + do: function not in @wrapper_methods and propagation_allowed?(context.return_type) + + defp fallible_receiver?(_receiver_type, _function, %Context{}), do: false defp lower_local_or_macro_call(name, args, %Context{} = context) do cond do diff --git a/lib/rustq/meta/lower/pattern.ex b/lib/rustq/meta/lower/pattern.ex index 3d88b63..1f9cc0e 100644 --- a/lib/rustq/meta/lower/pattern.ex +++ b/lib/rustq/meta/lower/pattern.ex @@ -299,8 +299,22 @@ defmodule RustQ.Meta.Lower.Pattern do end end - defp for_item_type(%Type{} = type), do: Type.vec_inner(type) || Type.slice_inner(type) + # A loop over a slice, or over a list behind a reference, reads each item by reference. + defp for_item_type(%Type{kind: kind} = type) when kind in [:ref, :mut_ref] do + case type |> Type.ref_inner() |> for_item_type() do + %Type{kind: kind} = item when kind not in [:ref, :mut_ref] -> Type.ref(item) + item -> item + end + end + + defp for_item_type(%Type{kind: :slice} = type) do + case Type.slice_inner(type) do + %Type{} = item -> Type.ref(item) + nil -> nil + end + end + defp for_item_type(%Type{} = type), do: Type.vec_inner(type) defp for_item_type(_type), do: nil defp context_with_inner_pattern(pattern, %Type{} = inner, wrappers, %Context{} = context) do diff --git a/lib/rustq/meta/rust_macro.ex b/lib/rustq/meta/rust_macro.ex index 7fc4d45..eca680c 100644 --- a/lib/rustq/meta/rust_macro.ex +++ b/lib/rustq/meta/rust_macro.ex @@ -80,9 +80,21 @@ defmodule RustQ.Meta.RustMacro do end @doc "Lowers each definition to its `macro_rules!` item." - @spec items([Definition.t()], map(), Macro.Env.t(), [term()], index()) :: [Item.t()] - def items(definitions, rust_modules, env, callables, index) do - Enum.map(definitions, &item(&1, rust_modules, env, callables, index)) + @typedoc """ + What lowering a module's definitions needs: its Rust module mappings, environment, callables, + `defrustmacro` index, and type aliases, which the macros' specs and bodies name. + """ + @type context :: %{ + rust_modules: map(), + env: Macro.Env.t(), + callables: [term()], + index: index(), + type_aliases: map() + } + + @spec items([Definition.t()], context()) :: [Item.t()] + def items(definitions, context) do + Enum.map(definitions, &item(&1, context)) end @doc "The fragment kinds of a definition's non-repeated arguments, in order." @@ -177,22 +189,15 @@ defmodule RustQ.Meta.RustMacro do defp render_call_value(value, _fragment) when is_list(value), do: IO.iodata_to_binary(value) defp render_call_value(value, _fragment), do: to_string(value) - defp item(%Definition{} = definition, rust_modules, env, callables, index) do + defp item(%Definition{} = definition, context) do ast = - case item_macro_body(definition.body_ast, definition, rust_modules, callables, index) do + case item_macro_body(definition.body_ast, definition, context) do {:ok, source} -> %AST.MacroItem{source: source} :error -> - body_ast = MetaAST.expand_body_macros(definition.body_ast, env) - - body = - Lower.quoted_body(body_ast, nil, %{}, - rust_modules: rust_modules, - callables: callables, - macro_vars: macro_var_map(definition.args), - rust_macros: index - ) + body_ast = MetaAST.expand_body_macros(definition.body_ast, context.env) + body = Lower.quoted_body(body_ast, nil, %{}, lower_opts(definition, context)) %AST.MacroItem{source: source(definition.name, definition.args, body)} end @@ -335,25 +340,18 @@ defmodule RustQ.Meta.RustMacro do ) end - defp item_macro_body( - {:__block__, _meta, expressions}, - definition, - rust_modules, - callables, - index - ) do + defp item_macro_body({:__block__, _meta, expressions}, definition, context) do pairs = item_defrust_pairs(expressions) if pairs == [] do :error else - {:ok, item_macro_source(definition, pairs, rust_modules, callables, index)} + {:ok, item_macro_source(definition, pairs, context)} end end - defp item_macro_body(expression, definition, rust_modules, callables, index), - do: - item_macro_body({:__block__, [], [expression]}, definition, rust_modules, callables, index) + defp item_macro_body(expression, definition, context), + do: item_macro_body({:__block__, [], [expression]}, definition, context) defp item_defrust_pairs(expressions), do: item_defrust_pairs(expressions, nil, []) @@ -377,44 +375,39 @@ defmodule RustQ.Meta.RustMacro do defp spec_ast(_expression), do: nil + defp lower_opts(definition, context) do + [ + rust_modules: context.rust_modules, + callables: context.callables, + type_aliases: context.type_aliases, + macro_vars: macro_var_map(definition.args), + rust_macros: context.index + ] + end + defp defrust_ast({:defrust, _meta, [call_ast, [do: body_ast]]}), do: {call_ast, body_ast} defp defrust_ast(_expression), do: nil - defp item_macro_source(definition, pairs, rust_modules, callables, index) do + defp item_macro_source(definition, pairs, context) do pattern = item_pattern(definition.args) expansion = - pairs - |> Enum.map_join("\n\n", fn {spec, defrust} -> - item_function_expansion(definition, spec, defrust, rust_modules, callables, index) + Enum.map_join(pairs, "\n\n", fn {spec, defrust} -> + item_function_expansion(definition, spec, defrust, context) end) "macro_rules! #{definition.name} {\n#{indent("(#{pattern}) => {\n#{indent(expansion)}\n};")}\n}" end - defp item_function_expansion( - definition, - {spec_call, return_ast}, - {call_ast, body_ast}, - rust_modules, - callables, - index - ) do + defp item_function_expansion(definition, {spec_call, return_ast}, {call_ast, body_ast}, context) do {_spec_name, _spec_meta, arg_type_asts} = spec_call {name_ast, _call_meta, arg_asts} = call_ast arg_names = Enum.map(arg_asts, &capture_var_name!/1) - arg_types = Enum.map(arg_type_asts, &Type.parse(&1, %{})) - return_type = Type.parse(return_ast, %{}) + arg_types = Enum.map(arg_type_asts, &Type.parse(&1, context.type_aliases)) + return_type = Type.parse(return_ast, context.type_aliases) vars = Map.new(Enum.zip(arg_names, arg_types)) - - body = - Lower.quoted_body(body_ast, return_type, vars, - rust_modules: rust_modules, - callables: callables, - macro_vars: macro_var_map(definition.args), - rust_macros: index - ) + body = Lower.quoted_body(body_ast, return_type, vars, lower_opts(definition, context)) function_expansion( name_ast, diff --git a/test/rustq/meta/lower/call_propagation_test.exs b/test/rustq/meta/lower/call_propagation_test.exs index 57eda6c..709220d 100644 --- a/test/rustq/meta/lower/call_propagation_test.exs +++ b/test/rustq/meta/lower/call_propagation_test.exs @@ -176,6 +176,22 @@ defmodule RustQ.Meta.Lower.CallPropagationTest do ] = statements end + test "a fallible receiver of a method its result lacks propagates first" do + nif_count = RustQ.Spec.type(quote(do: RustQ.Type.nif_result(RustQ.Type.u32()))) + + statements = + Lower.quoted_body( + quote do + {:ok, read_count().max(1)} + end, + nif_count, + %{}, + callables: [%Callable{name: "read_count", kind: :function, args: [], returns: nif_count}] + ) + + assert RustQ.Rust.render_all(statements) =~ "Ok(read_count()?.max(1))" + end + test "infers remote call argument propagation from free-function callable metadata" do mode_type = %Type{kind: :type, rust: "Mode", ast: %AST.TypePath{parts: [:Mode]}} diff --git a/test/rustq/meta/lower/enum_test.exs b/test/rustq/meta/lower/enum_test.exs index f5f1a79..23d5a3c 100644 --- a/test/rustq/meta/lower/enum_test.exs +++ b/test/rustq/meta/lower/enum_test.exs @@ -55,6 +55,16 @@ defmodule RustQ.Meta.Lower.EnumTest do @spec nonzero(R.ref(String.t())) :: R.option(R.usize()) defrust(nonzero(name), do: if(name.is_empty(), do: nil, else: some(name.len()))) + + @spec greet_all(R.slice(String.t())) :: R.unit() + defrust greet_all(names) do + for name <- names do + greet(name) + end + end + + @spec greet(R.ref(String.t())) :: R.usize() + defrust(greet(name), do: name.len()) end defmodule Queries do @@ -203,6 +213,10 @@ defmodule RustQ.Meta.Lower.EnumTest do assert rust_source!(Lists, :checked) =~ "unseen(&name, &HashSet::new())" end + test "a loop over a slice reads each item by reference" do + assert rust_source!(Lists, :greet_all) =~ "greet(name);" + end + test "a generator over one value matches it beside the item" do source = rust_source!(Lists, :by_length) diff --git a/test/rustq/meta/rust_macro_test.exs b/test/rustq/meta/rust_macro_test.exs index ffe73d9..2ace20c 100644 --- a/test/rustq/meta/rust_macro_test.exs +++ b/test/rustq/meta/rust_macro_test.exs @@ -40,7 +40,14 @@ defmodule RustQ.Meta.RustMacroTest do {quote(do: identity(value)), quote(do: value), [:helpers]} ]) - [item] = RustMacro.items([definition], %{}, __ENV__, [], RustMacro.index!([definition])) + [item] = + RustMacro.items([definition], %{ + rust_modules: %{}, + env: __ENV__, + callables: [], + index: RustMacro.index!([definition]), + type_aliases: %{} + }) assert item.rust_module == [:helpers] assert item.ast.source =~ "macro_rules! identity" @@ -53,6 +60,25 @@ defmodule RustQ.Meta.RustMacroTest do alias RustQ.Diagnostic alias RustQ.Meta.AST, as: MetaAST + test "a function a defrustmacro defines names the module's types" do + defmodule DefrustMacroTypeCase do + use RustQ.Meta + alias RustQ.Type, as: R + + @type variant :: %{required(:value) => R.u32()} + + defrustmacro first_value(fn: name(:ident)) do + @spec name(R.slice(variant())) :: R.u32() + defrust name(variants) do + index(variants, 0).value + end + end + end + + [item] = MetaAST.macro_items!(DefrustMacroTypeCase, [:first_value]) + assert item.source =~ "fn $name(variants: &[Variant]) -> u32" + end + test "defrustmacro defines Rust macros from Rusty-Elixir bodies" do defmodule DefrustMacroFieldCase do use RustQ.Meta