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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Changelog

## Unreleased

### Added

- A pipeline of `Enum` stages runs as one Rust iterator chain:
`list |> Enum.map(f) |> Enum.find(p)` is `.map(f).find(p)`, with no list
collected between them, and `Enum.count/1` on a stage counts as it goes.
- `mix rustq.std` also derives what std's methods on strings, characters,
lists, maps, sets, and options return where the receiver's type parameters
do not matter, so `name.len()` is a `usize` and `name.trim()` a `&str`.
- A comprehension's type is the list, map, or set it collects its body's
values into, and a block of expressions lowers wherever an expression does.

### Changed

- Whether a value may be taken is decided for each use of it: a pass over the
body records what is still in use after every variable occurrence, as
Elixir scopes it, so a list used again later in the same expression, as
`{Map.put(scores, name, 1), scores}`, is copied. A parameter is `mut` where
the lowered Rust changes it in place.
- `Map.new`, `MapSet.new`, `Enum.into` into an empty collection, `group_by`,
`uniq`, and generators over `Enum.with_index` are rewritten into other
Rusty-Elixir before lowering, by `RustQ.Meta.Desugar`.
- Pattern, comprehension, path, and mutability lowering are their own modules
beside `RustQ.Meta.Lower`.
- Each Elixir module's lowering implements
`RustQ.Meta.Lower.Stdlib.Lowering`, whose `use` imports the helpers they
share, and a call goes to the lowering named after its module. Lowering
builds Rust with `RustQ.Rust.AST.Builder`, whose `expr/1` now accepts every
RustQ AST node.

## 1.0.0-rc.17 - 2026-10-25

### Added
Expand Down
46 changes: 46 additions & 0 deletions dev/rustq/meta/std_methods/source.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ defmodule RustQ.Meta.StdMethods.Source do

@doc "Methods of std collections, their own or of traits they implement."
def collection, do: #{atoms(collection(std, collections))}

@doc \"""
What the inherent methods of std's strings, characters, lists, maps, sets, and options
return, keyed by the type they belong to and their name, where that type does not depend
on the receiver's own type parameters: `str`'s `len` returns `usize`.
\"""
def returns, do: #{inspect(returns(std), limit: :infinity, printable_limit: :infinity)}
end
"""
|> Code.format_string!()
Expand Down Expand Up @@ -247,6 +254,45 @@ defmodule RustQ.Meta.StdMethods.Source do

defp parameter?(impl, name), do: name in impl.type_parameters

# The std types Elixir values lower to, whose methods RustQ types.
@typed_receivers ["str", "String", "char", "[T]", "Vec", "HashMap", "HashSet", "Option"]

defp returns(std) do
for impl <- std.impls,
is_nil(impl.trait),
(target = target_name(impl)) in @typed_receivers,
%{returns_ast: returns} = method when not is_nil(returns) <- impl.methods,
method.visibility == :public and receiver?(method),
stable?(method),
concrete?(returns, impl.type_parameters ++ method.type_parameters),
reduce: %{} do
returns_by_method -> Map.put_new(returns_by_method, {target, method.name}, returns)
end
end

defp stable?(%{docs: docs, name: name, attributes: attributes}),
do: docs != [] and "unstable" not in attributes and not String.starts_with?(name, "_")

# A type naming none of the impl's or method's type parameters, nor `Self` or an opaque
# `impl Trait`, means the same for every receiver.
defp concrete?(%Type.Path{name: name, args: args}, parameters),
do: name not in ["Self" | parameters] and concrete?(args, parameters)

defp concrete?(%Type.Self{}, _parameters), do: false
defp concrete?(%Type.ImplTrait{}, _parameters), do: false
defp concrete?(%Type.Fn{}, _parameters), do: false

defp concrete?(%_{} = type, parameters),
do:
type
|> Map.from_struct()
|> Enum.all?(fn {_field, value} -> concrete?(value, parameters) end)

defp concrete?(values, parameters) when is_list(values),
do: Enum.all?(values, &concrete?(&1, parameters))

defp concrete?(_value, _parameters), do: true

defp inherent_methods(std, targets) do
for impl <- std.impls,
is_nil(impl.trait),
Expand Down
45 changes: 26 additions & 19 deletions lib/rustq/corpus.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ defmodule RustQ.Corpus do
"""

@root Path.expand("../../test/corpus", __DIR__)
@lower_path Path.expand("meta/lower.ex", __DIR__)
# Core lowering, in `lower.ex` and the modules split out of it; the standard-library modules
# under `lower/` report their own diagnostics.
@lower_paths Enum.map(
~w(lower.ex lower/paths.ex lower/mutability.ex lower/pattern.ex lower/comprehension.ex),
&Path.expand("meta/#{&1}", __DIR__)
)

@spec cases(Path.t()) :: [Path.t()]
def cases(root \\ @root) do
Expand Down Expand Up @@ -91,27 +96,29 @@ defmodule RustQ.Corpus do
}
end

@spec unsupported_diagnostics(Path.t()) :: [atom()]
def unsupported_diagnostics(lower_path \\ @lower_path) do
lower_path
@spec unsupported_diagnostics([Path.t()] | Path.t()) :: [atom()]
def unsupported_diagnostics(lower_paths \\ @lower_paths) do
lower_paths
|> List.wrap()
|> Enum.flat_map(&diagnostic_codes/1)
|> Enum.filter(&(Atom.to_string(&1) |> String.starts_with?("unsupported")))
|> Enum.uniq()
|> Enum.sort()
end

defp diagnostic_codes(path) do
path
|> File.read!()
|> Code.string_to_quoted!()
|> then(fn ast ->
{_ast, codes} =
Macro.prewalk(ast, [], fn
{{:., _, [{:__aliases__, _, [:Diagnostic]}, :lower]}, _, [code | _]} = node, acc
when is_atom(code) ->
{node, [code | acc]}

node, acc ->
{node, acc}
end)

codes
|> Enum.filter(&(Atom.to_string(&1) |> String.starts_with?("unsupported")))
|> Enum.uniq()
|> Enum.sort()
|> Macro.prewalk([], fn
{{:., _, [{:__aliases__, _, [:Diagnostic]}, :lower]}, _, [code | _]} = node, acc
when is_atom(code) ->
{node, [code | acc]}

node, acc ->
{node, acc}
end)
|> elem(1)
end

defp corpus_category(root, source_path) do
Expand Down
51 changes: 22 additions & 29 deletions lib/rustq/meta/ast.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule RustQ.Meta.AST do
alias RustQ.Diagnostic
alias RustQ.Meta.Decoder
alias RustQ.Meta.Lower
alias RustQ.Meta.Lower.Mutability
alias RustQ.Meta.RustMacro
alias RustQ.Meta.Type
alias RustQ.Rust
Expand Down Expand Up @@ -543,7 +544,8 @@ defmodule RustQ.Meta.AST do
|> mark_mutable_owned_args(body_ast, arg_names, arg_types, external_callables)
|> maybe_prepend_nif_env(implicit_env?)

vars = Map.merge(external_static_types, Map.new(Enum.zip(arg_names, arg_types)))
arg_types_by_name = Map.new(Enum.zip(arg_names, arg_types))
vars = Map.merge(external_static_types, arg_types_by_name)

paths =
env.module |> Module.get_attribute(:rustq_rust_sources) |> List.wrap() |> List.flatten()
Expand All @@ -561,6 +563,8 @@ defmodule RustQ.Meta.AST do
rust_macros: rust_macros
)

args = mark_updated_args(args, body, arg_types_by_name)

lifetime =
if implicit_env? or Enum.any?(arg_types ++ [return_type], &Type.lifetime?/1), do: :a

Expand All @@ -576,21 +580,6 @@ defmodule RustQ.Meta.AST do
%{ast: ast, rust_module: rust_module, rust_impl: rust_impl}
end

# The collection updates that change a collection in place where it is rebound to them.
@in_place_updates [
{:Map, :put},
{:Map, :delete},
{:Map, :merge},
{:Map, :update},
{:MapSet, :put},
{:MapSet, :delete},
{:MapSet, :union},
{:MapSet, :difference},
{:MapSet, :intersection},
{:Enum, :sort_by},
{:Enum, :dedup}
]

defp mark_mutable_owned_args(args, body, arg_names, arg_types, callables) do
types = Map.new(Enum.zip(arg_names, arg_types))
callables = Index.new(callables)
Expand All @@ -613,22 +602,11 @@ defmodule RustQ.Meta.AST do
# A field changed in place, as in `record.claims.push(claim)`.
{{:., _, [{{:., _, [_, field]}, _, []} = receiver, method]}, _, method_args} = call, acc
when is_atom(field) and is_atom(method) and is_list(method_args) ->
{call, put_owned_root(acc, receiver, types, Lower.mutating_method?(method))}
{call, put_owned_root(acc, receiver, types, Mutability.mutating_method?(method))}

{:mut_ref, _, [target]} = call, acc ->
{call, put_owned_root(acc, target, types, true)}

# `scores = Map.put(scores, ...)` changes `scores` in place.
{:=, _,
[
{name, _, _} = target,
{{:., _, [{:__aliases__, _, [module]}, function]}, _, [{name, _, _} | _]}
]} =
rebinding,
acc
when {module, function} in @in_place_updates ->
{rebinding, put_owned_root(acc, target, types, true)}

{:assign!, _, [target, _value]} = call, acc ->
{call, put_owned_root(acc, target, types, true)}

Expand All @@ -640,6 +618,21 @@ defmodule RustQ.Meta.AST do
Enum.map(args, fn arg -> %{arg | mutable: MapSet.member?(mutable, arg.name)} end)
end

# An owned argument the lowered body changes in place, as rebinding it to its update does.
defp mark_updated_args(args, body, types) do
mutated = Mutability.mutated_variables(body)

Enum.map(args, fn %AST.FunctionArg{name: name} = arg ->
if owned?(Map.get(types, name)) and MapSet.member?(mutated, name),
do: %{arg | mutable: true},
else: arg
end)
end

defp owned?(%Type{rust: "&" <> _borrowed}), do: false
defp owned?(%Type{}), do: true
defp owned?(nil), do: false

# The owned argument a field path or variable starts from, when the access changes it.
defp put_owned_root(acc, _target, _types, false), do: acc

Expand Down Expand Up @@ -682,7 +675,7 @@ defmodule RustQ.Meta.AST do
# An owned value changed in place, such as `values.retain(...)`; a borrowed one stays as is.
defp mutable_standard_receiver?(%Type{rust: "&" <> _borrowed}, _method), do: false

defp mutable_standard_receiver?(%Type{}, method), do: Lower.mutating_method?(method)
defp mutable_standard_receiver?(%Type{}, method), do: Mutability.mutating_method?(method)

defp nif_attrs?(attrs) do
Enum.any?(attrs, &match?(%AST.Attribute{path: [:rustler, :nif]}, &1))
Expand Down
Loading
Loading