Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 8 additions & 1 deletion lib/rustq/meta.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 14 additions & 2 deletions lib/rustq/meta/lower.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion lib/rustq/meta/lower/pattern.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
87 changes: 40 additions & 47 deletions lib/rustq/meta/rust_macro.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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, [])

Expand All @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions test/rustq/meta/lower/call_propagation_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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]}}

Expand Down
14 changes: 14 additions & 0 deletions test/rustq/meta/lower/enum_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
28 changes: 27 additions & 1 deletion test/rustq/meta/rust_macro_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
Loading