From 723cddc59d297a0aae28de5d1d47742dac567c00 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 26 Oct 2026 16:11:09 +0400 Subject: [PATCH 1/4] Let defrustmacro functions name the module's types The functions a defrustmacro defines parsed their specs without the module's type aliases, so naming one of its struct types failed. The macros now lower with the same aliases defrust does, passed with the rest of what lowering a module's macros needs as one context. --- CHANGELOG.md | 7 +++ lib/rustq/meta.ex | 9 ++- lib/rustq/meta/rust_macro.ex | 87 +++++++++++++---------------- test/rustq/meta/rust_macro_test.exs | 28 +++++++++- 4 files changed, 82 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09e0577..42c6e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Fixed + +- A function a `defrustmacro` defines can name the module's own types in its + spec, as a `defrust` can. + ## 1.0.0-rc.18 - 2026-10-26 ### Added 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/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/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 From 51e510e85020f976c14f16178db991bceaf9636e Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 26 Oct 2026 16:13:53 +0400 Subject: [PATCH 2/4] Propagate a fallible method receiver's error A Result or NifResult receiving a method Result lacks, as decoder.read_bool().encode(env), was lowered as is and failed to compile. It now propagates with ?, as a fallible operand does. --- CHANGELOG.md | 2 ++ lib/rustq/meta/lower.ex | 16 ++++++++++++++-- test/rustq/meta/lower/call_propagation_test.exs | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42c6e29..6096c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - 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. ## 1.0.0-rc.18 - 2026-10-26 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/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]}} From 6e301728927b2c477da1b3cd484ac12e71fe70e5 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 26 Oct 2026 16:25:44 +0400 Subject: [PATCH 3/4] Read a loop's items by reference over a slice for item <- slice binds each item as a reference, as Rust's for loop does, so a call taking a reference no longer borrows it again. --- CHANGELOG.md | 2 ++ lib/rustq/meta/lower/pattern.ex | 16 +++++++++++++++- test/rustq/meta/lower/enum_test.exs | 14 ++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6096c5a..4a908c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ 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/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/test/rustq/meta/lower/enum_test.exs b/test/rustq/meta/lower/enum_test.exs index 1986633..a74fc6d 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.unit() + defrust(greet(_name), do: nil) end defmodule Queries do @@ -176,6 +186,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) From aaa7c8feb6a5858af615e4835042cde07089448d Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 26 Oct 2026 16:43:18 +0400 Subject: [PATCH 4/4] Give the slice loop fixture a function the corpus check compiles --- test/rustq/meta/lower/enum_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/rustq/meta/lower/enum_test.exs b/test/rustq/meta/lower/enum_test.exs index a74fc6d..7bb2160 100644 --- a/test/rustq/meta/lower/enum_test.exs +++ b/test/rustq/meta/lower/enum_test.exs @@ -63,8 +63,8 @@ defmodule RustQ.Meta.Lower.EnumTest do end end - @spec greet(R.ref(String.t())) :: R.unit() - defrust(greet(_name), do: nil) + @spec greet(R.ref(String.t())) :: R.usize() + defrust(greet(name), do: name.len()) end defmodule Queries do