From 17e585f75c826e2846496919f730bc00a6c2ca86 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 25 Oct 2026 21:14:03 +0400 Subject: [PATCH 01/10] Decide ownership from each use's liveness, and parameter mut from the lowered updates --- lib/rustq/meta/ast.ex | 46 ++-- lib/rustq/meta/lower.ex | 203 ++++----------- lib/rustq/meta/lower/collection.ex | 12 +- lib/rustq/meta/lower/enum.ex | 97 ++++---- lib/rustq/meta/lower/map.ex | 8 +- lib/rustq/meta/lower/map_set.ex | 8 +- lib/rustq/meta/lower/stdlib.ex | 2 +- lib/rustq/meta/ownership.ex | 385 +++++++++++++++++++++++++++++ test/rustq/meta/lower/map_test.exs | 8 + test/rustq/meta/ownership_test.exs | 100 ++++++++ 10 files changed, 622 insertions(+), 247 deletions(-) create mode 100644 lib/rustq/meta/ownership.ex create mode 100644 test/rustq/meta/ownership_test.exs diff --git a/lib/rustq/meta/ast.ex b/lib/rustq/meta/ast.ex index a3890b9..046dff9 100644 --- a/lib/rustq/meta/ast.ex +++ b/lib/rustq/meta/ast.ex @@ -543,7 +543,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() @@ -561,6 +562,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 @@ -576,21 +579,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) @@ -618,17 +606,6 @@ defmodule RustQ.Meta.AST do {: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)} @@ -640,6 +617,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 = Lower.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 diff --git a/lib/rustq/meta/lower.ex b/lib/rustq/meta/lower.ex index f29dd41..1f066bb 100644 --- a/lib/rustq/meta/lower.ex +++ b/lib/rustq/meta/lower.ex @@ -11,6 +11,7 @@ defmodule RustQ.Meta.Lower do alias RustQ.Meta.Core.Call alias RustQ.Meta.Lower.Stdlib alias RustQ.Meta.Lower.Stdlib.Context, as: StdlibContext + alias RustQ.Meta.Ownership alias RustQ.Meta.Pattern alias RustQ.Meta.RustMacro alias RustQ.Meta.Semantics @@ -45,7 +46,6 @@ defmodule RustQ.Meta.Lower do callables: %BindingIndex{}, macro_vars: %{}, rust_macros: %{}, - in_use: MapSet.new(), built_from: %{} ] end @@ -54,7 +54,10 @@ defmodule RustQ.Meta.Lower do @spec quoted_body(Macro.t(), Type.t() | nil, map(), keyword()) :: [struct()] def quoted_body(body_ast, return_type, vars \\ %{}, opts \\ []) do + {body_ast, built_from} = Ownership.annotate(body_ast) + context = %Context{ + built_from: built_from, return_type: return_type, scope_return_type: return_type, type_aliases: Keyword.get(opts, :type_aliases, %{}), @@ -97,126 +100,15 @@ defmodule RustQ.Meta.Lower do defp lower_block(expressions, %Context{} = context) do context = context_with_downstream_let_types(expressions, context) - {statements, final} = split_final(expressions) - {statement_contexts, final_context} = expression_contexts(statements ++ [final], context) - - Enum.zip_with( - statements, - statement_contexts, - &lower_statement(&1, %{&2 | position: :statement}) - ) ++ - [ - lower_final( - final, - %{final_context | position: :statement}, - %{final_context | position: :return} - ) - ] - end - - # Each expression of a block lowers knowing the variables still in use after it, with the - # variables they were built from, which they may borrow from: Elixir never changes a value, - # so lowering takes one only where nothing uses it afterwards. - defp expression_contexts(expressions, %Context{} = context) do - built_from = Enum.reduce(expressions, context.built_from, &record_built_from/2) - - contexts = - expressions - |> Enum.with_index(1) - |> Enum.map(fn {expression, index} -> - later = - expressions - |> Enum.drop(index) - |> used_after(bound_by(expression)) - |> MapSet.union(context.in_use) - - current = used_variables([expression]) - sources = sources(MapSet.union(later, current), built_from, context.vars) - - %{context | in_use: MapSet.union(later, sources), built_from: built_from} - end) - - {statement_contexts, [final_context]} = Enum.split(contexts, -1) - {statement_contexts, final_context} - end - - # A name rebound in a block means the new value from there on, as Elixir scopes it, so a use - # after the rebinding is not a use of the value before it. - defp used_after(expressions, shadowed) do - expressions - |> Enum.reduce({MapSet.new(), shadowed}, fn expression, {used, shadowed} -> - used = MapSet.union(used, MapSet.difference(used_variables([expression]), shadowed)) - {used, MapSet.union(shadowed, bound_by(expression))} - end) - |> elem(0) - end - - defp bound_by({:=, _, [pattern, _expression]}), do: variables_in(pattern) - defp bound_by(_expression), do: MapSet.new() - defp record_built_from({:=, _, [pattern, expression]}, built_from) do - from = used_variables([expression]) - pattern |> variables_in() |> Enum.reduce(built_from, &Map.put(&2, &1, from)) - end - - defp record_built_from(_expression, built_from), do: built_from - - # A binding's own name is not in use because its value is being built. - defp used_variables(expressions) do - Enum.reduce(expressions, MapSet.new(), fn - {:=, _, [_pattern, expression]}, used -> MapSet.union(used, used_variables([expression])) - expression, used -> MapSet.union(used, variables_in(expression)) - end) - end - - defp variables_in(ast) do - ast - |> Macro.prewalk(MapSet.new(), fn - {name, _meta, context} = node, variables - when is_atom(name) and is_atom(context) and name not in [:_, :__MODULE__] -> - {node, MapSet.put(variables, name)} - - node, variables -> - {node, variables} - end) - |> elem(1) - end - - # Only a value that may hold a reference can borrow from what it was built from; one whose - # type is unknown may. - defp sources(names, built_from, vars) do - Enum.reduce(names, MapSet.new(), fn name, found -> - add_sources(name, built_from, vars, found) - end) - end + {statements, final} = split_final(expressions) + statement_context = %{context | position: :statement} + return_context = %{context | position: :return} - defp add_sources(name, built_from, vars, found) do - if may_borrow?(Map.get(vars, name)) do - built_from - |> Map.get(name, MapSet.new()) - |> Enum.reject(&MapSet.member?(found, &1)) - |> Enum.reduce(found, fn source, found -> - add_sources(source, built_from, vars, MapSet.put(found, source)) - end) - else - found - end + Enum.map(statements, &lower_statement(&1, statement_context)) ++ + [lower_final(final, statement_context, return_context)] end - defp may_borrow?(%Type{kind: kind}) when kind in [:ref, :mut_ref, :slice], do: true - defp may_borrow?(%Type{ast: ast}), do: borrowing_type?(ast) - defp may_borrow?(_unknown), do: true - - defp borrowing_type?(%AST.TypeRef{}), do: true - defp borrowing_type?(%AST.TypePath{lifetimes: [_ | _]}), do: true - defp borrowing_type?(%AST.TypeRaw{}), do: true - - defp borrowing_type?(%_{} = type), - do: type |> Map.from_struct() |> Enum.any?(fn {_field, value} -> borrowing_type?(value) end) - - defp borrowing_type?(types) when is_list(types), do: Enum.any?(types, &borrowing_type?/1) - defp borrowing_type?(_other), do: false - # A body returning nothing runs its last expression for its effects, as a statement. defp lower_final(final, statement_context, %Context{return_type: %Type{kind: :unit}}) when final not in [:ok, nil], @@ -227,19 +119,13 @@ defmodule RustQ.Meta.Lower do defp lower_expected_block(expressions, %Context{} = context, %Type{} = expected_type) do context = context_with_downstream_let_types(expressions, context) + {statements, final} = split_final(expressions) - {statement_contexts, final_context} = expression_contexts(statements ++ [final], context) + statement_context = %{context | position: :statement} + return_context = %{context | position: :return} - Enum.zip_with( - statements, - statement_contexts, - &lower_statement(&1, %{&2 | position: :statement}) - ) ++ - [ - %AST.Return{ - expr: lower_expr(final, expected_type, %{final_context | position: :return}) - } - ] + Enum.map(statements, &lower_statement(&1, statement_context)) ++ + [%AST.Return{expr: lower_expr(final, expected_type, return_context)}] end @keyed_collections [:HashMap, :HashSet, :BTreeMap, :BTreeSet] @@ -2119,10 +2005,9 @@ defmodule RustQ.Meta.Lower do # loop is its last use. defp indexed_generators(qualifiers, %Context{} = context) do Enum.map(qualifiers, fn - {:<-, meta, [pattern, {{:., _, [{:__aliases__, _, [:Enum]}, :with_index]}, _, [list]}]} = - generator -> + {:<-, meta, [pattern, {{:., _, [{:__aliases__, _, [:Enum]}, :with_index]}, _, [list]}]} -> method = - if borrowed_list?(list, List.delete(qualifiers, generator), context), + if borrowed_list?(list, context), do: :iter, else: :into_iter @@ -2139,10 +2024,10 @@ defmodule RustQ.Meta.Lower do defp indexed_generator(pattern, enumerated), do: [pattern, quote(do: unquote(enumerated) |> map(fn {index, item} -> {item, index} end))] - defp borrowed_list?(list, other_qualifiers, context) do + defp borrowed_list?(list, context) do case Typing.synth(list, typing_env(context)) do %Type{kind: kind} when kind in [:slice, :ref, :mut_ref] -> true - _owned -> in_use?(list, other_qualifiers, context) + _owned -> in_use?(list, context) end end @@ -2167,7 +2052,7 @@ defmodule RustQ.Meta.Lower do root != nil and ((nested? and root not in generated) or (item_type != nil and root not in generated and - in_use?(collection, rest ++ [body], context))) + in_use?(collection, context))) {stage, binder} = generator_stage(pattern, collection, filters, item_type, reused?) bound = context_with_match_pattern(pattern, item_type, context) @@ -2343,6 +2228,15 @@ defmodule RustQ.Meta.Lower do defp root_variable(_collection), do: nil + defp root_variable_node({name, _, ast_context} = variable) + when is_atom(name) and is_atom(ast_context), + do: variable + + defp root_variable_node({{:., _, [receiver, _field]}, meta, []}), + do: if(meta[:no_parens], do: root_variable_node(receiver)) + + defp root_variable_node(_collection), do: nil + # A collection already given as an iterator, as in `values.iter()`, is iterated as it is. defp iterator_call?({{:., _, [_receiver, method]}, _, args}) when is_list(args), do: method in @iterator_methods @@ -3580,27 +3474,21 @@ defmodule RustQ.Meta.Lower do lower_capture: &lower_function_capture(&1, context), closure_arg: &closure_arg!/1, type_of: &Typing.synth(&1, typing_env(context)), - in_use?: &in_use?(&1, &2, context), + in_use?: &in_use?(&1, context), expected: expected } end - # A collection is in use when the expressions after this one use it, a value they use was - # built from it, or the call's other arguments use it. A field reached through a reference - # is never taken, since the reference does not own it. - defp in_use?(collection, other_args, %Context{} = context) do - case root_variable(collection) do + # A collection is in use unless this is its value's last use; see `RustQ.Meta.Ownership`. A + # field reached through a reference is never taken, since the reference does not own it. + defp in_use?(collection, %Context{} = context) do + case root_variable_node(collection) do + {name, _meta, _context} = variable -> + behind_reference?(collection, name, context) or + Ownership.in_use?(variable, context.built_from, context.vars) + nil -> false - - name -> - behind_reference?(collection, name, context) or - MapSet.member?(context.in_use, name) or - MapSet.member?( - sources(used_variables(other_args), context.built_from, context.vars), - name - ) or - MapSet.member?(used_variables(other_args), name) end end @@ -4234,6 +4122,23 @@ defmodule RustQ.Meta.Lower do defp mutated_chain_root(%AST.MethodCall{receiver: receiver}), do: mutated_chain_root(receiver) defp mutated_chain_root(_expression), do: nil + @doc """ + The variables a lowered body changes in place through a statement calling a mutating + method on them, as `scores.insert(name, 1);` does where `scores` was rebound to its update. + """ + @spec mutated_variables([struct()]) :: MapSet.t(atom()) + def mutated_variables(body) do + body + |> Walk.reduce([], fn + %AST.ExprStmt{expr: %AST.MethodCall{receiver: %AST.Var{name: name}, method: method}}, acc -> + if mutating_method?(method), do: [name | acc], else: acc + + _node, acc -> + acc + end) + |> MapSet.new() + end + @doc "Whether a method changes its receiver, as std collections' `&mut self` methods do." @spec mutating_method?(atom()) :: boolean() def mutating_method?(method), diff --git a/lib/rustq/meta/lower/collection.ex b/lib/rustq/meta/lower/collection.ex index 93a5acf..9fe45b9 100644 --- a/lib/rustq/meta/lower/collection.ex +++ b/lib/rustq/meta/lower/collection.ex @@ -56,8 +56,8 @@ defmodule RustQ.Meta.Lower.Collection do An update of `collection`: `statements` receives the variable holding the collection, or a copy of it when `collection` is still in use, and the block returns that variable. """ - @spec updated(Macro.t(), [Macro.t()], (AST.expr() -> [AST.stmt()]), Context.t()) :: AST.expr() - def updated(collection, others, statements, %Context{} = context) do + @spec updated(Macro.t(), (AST.expr() -> [AST.stmt()]), Context.t()) :: AST.expr() + def updated(collection, statements, %Context{} = context) do name = :rustq_updated variable = %AST.Var{name: name} @@ -66,7 +66,7 @@ defmodule RustQ.Meta.Lower.Collection do [ %AST.Let{ pattern: %AST.PatVar{name: name}, - expr: owned(collection, others, context), + expr: owned(collection, context), mutable: true } ] ++ @@ -75,11 +75,11 @@ defmodule RustQ.Meta.Lower.Collection do end @doc "`collection` to take: itself at its last use, otherwise a copy." - @spec owned(Macro.t(), [Macro.t()], Context.t()) :: AST.expr() - def owned(collection, others, %Context{} = context) do + @spec owned(Macro.t(), Context.t()) :: AST.expr() + def owned(collection, %Context{} = context) do lowered = context.lower.(collection) - if context.in_use?.(collection, others), + if context.in_use?.(collection), do: %AST.MethodCall{receiver: lowered, method: :clone, args: []}, else: lowered end diff --git a/lib/rustq/meta/lower/enum.ex b/lib/rustq/meta/lower/enum.ex index 0062ad1..d9a6aa1 100644 --- a/lib/rustq/meta/lower/enum.ex +++ b/lib/rustq/meta/lower/enum.ex @@ -23,7 +23,7 @@ defmodule RustQ.Meta.Lower.Enum do do: {:ok, lower_map(collection, mapper, context)} defp lower_call(%Call{function: :sum, args: [collection]}, %Context{} = context), - do: {:ok, collection |> iterate(:question, [], context) |> method_chain(:sum)} + do: {:ok, collection |> iterate(:question, context) |> method_chain(:sum)} defp lower_call( %Call{function: :reduce, args: [collection, initial, reducer]}, @@ -42,7 +42,7 @@ defmodule RustQ.Meta.Lower.Enum do do: {:ok, collection |> lower_collection(context) |> method_chain(:len) |> cast_i64()} defp lower_call(%Call{function: :count, args: [collection, predicate]}, %Context{} = context) do - iterator = iterate(collection, :question, [predicate], context) + iterator = iterate(collection, :question, context) {:ok, iterator |> lower_filter(predicate, false, collection, context, :count) |> cast_i64()} end @@ -50,7 +50,7 @@ defmodule RustQ.Meta.Lower.Enum do do: {:ok, collection - |> iterate(:question, [], context) + |> iterate(:question, context) |> method_chain(:next) |> method_chain(:is_none)} @@ -64,7 +64,7 @@ defmodule RustQ.Meta.Lower.Enum do }} defp lower_call(%Call{function: :find, args: [collection, predicate]}, %Context{} = context) do - iterator = iterate(collection, :keeps_items, [predicate], context) + iterator = iterate(collection, :keeps_items, context) {:ok, lower_filter(iterator, predicate, false, collection, context, :find)} end @@ -72,7 +72,7 @@ defmodule RustQ.Meta.Lower.Enum do %Call{function: :find, args: [collection, default, predicate]}, %Context{} = context ) do - iterator = iterate(collection, :keeps_items, [default, predicate], context) + iterator = iterate(collection, :keeps_items, context) {:ok, iterator @@ -119,7 +119,7 @@ defmodule RustQ.Meta.Lower.Enum do do: {:ok, collection - |> iterate(:keeps_items, [], context) + |> iterate(:keeps_items, context) |> method_chain(:rev) |> method_chain(:collect)} @@ -160,7 +160,7 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, collection - |> iterate(:keeps_items, [count], context) + |> iterate(:keeps_items, context) |> method_chain(method, [lowered_count]) |> method_chain(:collect)} @@ -180,11 +180,10 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, Collection.updated( collectable, - [enumerable], fn collection -> [ Collection.statement(collection, :extend, [ - Collection.owned(enumerable, [], context) + Collection.owned(enumerable, context) ]) ] end, @@ -226,7 +225,7 @@ defmodule RustQ.Meta.Lower.Enum do defp lower_call(%Call{function: :with_index, args: [enumerable]}, context) do {:ok, enumerable - |> iterate(:keeps_items, [], context) + |> iterate(:keeps_items, context) |> method_chain(:enumerate) |> method_chain(:map, [ %AST.Closure{ @@ -238,23 +237,23 @@ defmodule RustQ.Meta.Lower.Enum do end defp lower_call(%Call{function: :find_index, args: [enumerable, predicate]}, context) do - iteration = iteration(enumerable, :question, [predicate], context) + iteration = iteration(enumerable, :question, context) predicate = typed_mapper(predicate, enumerable, iteration, context) {:ok, enumerable - |> iterate(iteration, context) + |> iterate_as(iteration, context) |> method_chain(:position, [mapper(predicate, context)])} end # The function gives `some(value)` for the value to find and `nil` to go on. defp lower_call(%Call{function: :find_value, args: [enumerable, function]}, context) do - iteration = iteration(enumerable, :maps_items, [function], context) + iteration = iteration(enumerable, :maps_items, context) function = typed_mapper(function, enumerable, iteration, context) {:ok, enumerable - |> iterate(iteration, context) + |> iterate_as(iteration, context) |> method_chain(:find_map, [mapper(function, context)])} end @@ -265,18 +264,18 @@ defmodule RustQ.Meta.Lower.Enum do args: [ enumerable, initial, - {:fn, _, [{:->, _, [[item, accumulator], body]}]} = reducer + {:fn, _, [{:->, _, [[item, accumulator], body]}]} ] }, context ) do - iteration = iteration(enumerable, :maps_items, [initial, reducer], context) + iteration = iteration(enumerable, :maps_items, context) [item] = typed_items([item], enumerable, iteration, context) closure = context.lower_closure.([accumulator, item], continuation(body)) {:ok, enumerable - |> iterate(iteration, context) + |> iterate_as(iteration, context) |> method_chain(:try_fold, [context.lower.(initial), closure]) |> method_chain(:unwrap_or_else, [ %AST.Closure{args: [:rustq_halted], body: %AST.Var{name: :rustq_halted}} @@ -298,7 +297,7 @@ defmodule RustQ.Meta.Lower.Enum do order = if function == :max_by, do: :desc, else: :asc enumerable - |> iterate(:keeps_items, [key, fallback], context) + |> iterate(:keeps_items, context) |> method_chain(:min_by_key, [key_function(enumerable, key, order, context)]) |> fallback.() |> then(&{:ok, &1}) @@ -309,7 +308,7 @@ defmodule RustQ.Meta.Lower.Enum do when function in [:max, :min] do with {:ok, fallback} <- empty_fallback(fallback, context) do enumerable - |> iterate(:keeps_items, [fallback], context) + |> iterate(:keeps_items, context) |> method_chain(function) |> fallback.() |> then(&{:ok, &1}) @@ -326,7 +325,6 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, Collection.updated( enumerable, - [], fn list -> [Collection.statement(list, :dedup, [])] end, context )} @@ -335,7 +333,7 @@ defmodule RustQ.Meta.Lower.Enum do defp lower_call(%Call{function: :split_with, args: [enumerable, predicate]}, context) do case context.type_of.(enumerable) |> collection_inner() do %Type{} = inner -> - iterator = iterate(enumerable, :keeps_items, [predicate], context) + iterator = iterate(enumerable, :keeps_items, context) partitioned = lower_filter(iterator, predicate, false, enumerable, context, :partition) {:ok, %{partitioned | generics: [Type.vec(inner).ast, %AST.TypeRaw{source: "_"}]}} @@ -369,7 +367,7 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, amount} <- Stdlib.nonnegative_count(amount, context) do {:ok, enumerable - |> iterate(:keeps_items, [], context) + |> iterate(:keeps_items, context) |> method_chain(:skip, [start]) |> method_chain(:take, [amount]) |> collect(context)} @@ -380,7 +378,7 @@ defmodule RustQ.Meta.Lower.Enum do defp lower_call(%Call{function: function, args: [enumerable, predicate]}, context) when function in [:take_while, :drop_while] do - iterator = iterate(enumerable, :keeps_items, [predicate], context) + iterator = iterate(enumerable, :keeps_items, context) {:ok, lower_filter(iterator, predicate, false, enumerable, context, function)} end @@ -388,13 +386,13 @@ defmodule RustQ.Meta.Lower.Enum do %Call{function: :zip_with, args: [left, right, {:fn, _, [{:->, _, [[x, y], body]}]}]}, context ) do - lefts = iterate(left, :maps_items, [right, body], context) + lefts = iterate(left, :maps_items, context) # `zip` takes anything iterable, so an owned list goes in as it is. rights = - case iteration(right, :maps_items, [body], context) do + case iteration(right, :maps_items, context) do :owned -> context.lower.(right) - iteration -> iterate(right, iteration, context) + iteration -> iterate_as(right, iteration, context) end {:ok, @@ -435,12 +433,12 @@ defmodule RustQ.Meta.Lower.Enum do [separator, function] -> {[separator], function} end - iteration = iteration(enumerable, :maps_items, [function], context) + iteration = iteration(enumerable, :maps_items, context) function = typed_mapper(function, enumerable, iteration, context) strings = enumerable - |> iterate(iteration, context) + |> iterate_as(iteration, context) |> method_chain(:map, [mapper(function, context)]) |> then( &%AST.MethodCall{ @@ -638,8 +636,7 @@ defmodule RustQ.Meta.Lower.Enum do defp lower_map(collection, {:fn, _, [{:->, _, [args, body]}]}, context) do mapped_type = collection_inner(context.expected) - mapper = {:fn, [], [{:->, [], [args, body]}]} - iteration = iteration(collection, :maps_items, [mapper], context) + iteration = iteration(collection, :maps_items, context) args = typed_items(args, collection, iteration, context) closure = @@ -650,14 +647,14 @@ defmodule RustQ.Meta.Lower.Enum do end collection - |> iterate(iteration, context) + |> iterate_as(iteration, context) |> method_chain(:map, [closure]) |> collect(context) end - defp lower_map(collection, {:&, _, [{:/, _, [capture, 1]}]} = mapper, context) do + defp lower_map(collection, {:&, _, [{:/, _, [capture, 1]}]}, context) do collection - |> iterate(:maps_items, [mapper], context) + |> iterate(:maps_items, context) |> method_chain(:map, [context.lower_capture.(capture)]) |> collect(context) end @@ -677,13 +674,12 @@ defmodule RustQ.Meta.Lower.Enum do {:fn, _, [{:->, _, [[item, accumulator], body]}]}, context ) do - reducer = {:fn, [], [{:->, [], [[item, accumulator], body]}]} - iteration = iteration(collection, :maps_items, [initial, reducer], context) + iteration = iteration(collection, :maps_items, context) [item] = typed_items([item], collection, iteration, context) closure = context.lower_closure.([accumulator, item], body) collection - |> iterate(iteration, context) + |> iterate_as(iteration, context) |> method_chain(:fold, [context.lower.(initial), closure]) end @@ -697,14 +693,14 @@ defmodule RustQ.Meta.Lower.Enum do end defp lower_operation(operation, collection, mapper, context) do - iteration = iteration(collection, operation_use(operation), [mapper], context) + iteration = iteration(collection, operation_use(operation), context) mapper = if operation in [:flat_map, :each, :any?, :all?], do: typed_mapper(mapper, collection, iteration, context), else: mapper - iterator = iterate(collection, iteration, context) + iterator = iterate_as(collection, iteration, context) case operation do :filter -> @@ -863,10 +859,10 @@ defmodule RustQ.Meta.Lower.Enum do # Elixir never changes a list, so an operation takes an owned list only where nothing uses it # afterwards. A question about a list borrows it; so does any other operation on a list still # in use, copying the items it keeps. - defp iterate(collection, use, others, context), - do: iterate(collection, iteration(collection, use, others, context), context) + defp iterate(collection, use, context) when use in [:question, :keeps_items, :maps_items], + do: iterate_as(collection, iteration(collection, use, context), context) - defp iterate(collection, iteration, context) do + defp iterate_as(collection, iteration, context) do receiver = lower_collection(collection, context) case iteration do @@ -889,20 +885,18 @@ defmodule RustQ.Meta.Lower.Enum do # A question needs no item it could take, so it borrows other items even from a list nothing # uses afterwards. Copying out a number or atom costs what borrowing it does, and keeps the # item's own type. - defp iteration(collection, use, others, context), - do: iteration(context.type_of.(collection), collection, use, others, context) + defp iteration(collection, use, context), + do: iteration(context.type_of.(collection), collection, use, context) - defp iteration(%Type{kind: :vec} = type, collection, use, others, context), - do: list_iteration(collection, type, use, others, context) + defp iteration(%Type{kind: :vec} = type, collection, use, context), + do: list_iteration(collection, type, use, context) - defp iteration(%Type{} = type, _collection, :keeps_items, _others, _context), - do: lent_iteration(type) + defp iteration(%Type{} = type, _collection, :keeps_items, _context), do: lent_iteration(type) + defp iteration(_type, _collection, _use, _context), do: :owned - defp iteration(_type, _collection, _use, _others, _context), do: :owned - - defp list_iteration(collection, type, use, others, context) do + defp list_iteration(collection, type, use, context) do copy? = Collection.copy?(Type.vec_inner(type)) - in_use? = (use == :question and not copy?) or context.in_use?.(collection, others) + in_use? = (use == :question and not copy?) or context.in_use?.(collection) cond do not in_use? -> :owned @@ -1017,7 +1011,6 @@ defmodule RustQ.Meta.Lower.Enum do defp sort_by(enumerable, key, order, context) do Collection.updated( enumerable, - [key], fn list -> [ Collection.statement(list, :sort_by_key, [key_function(enumerable, key, order, context)]) diff --git a/lib/rustq/meta/lower/map.ex b/lib/rustq/meta/lower/map.ex index 3dee738..7897dc1 100644 --- a/lib/rustq/meta/lower/map.ex +++ b/lib/rustq/meta/lower/map.ex @@ -105,7 +105,6 @@ defmodule RustQ.Meta.Lower.Map do {:ok, Collection.updated( map, - [key, value], fn map -> [ Collection.statement(map, :insert, [ @@ -122,7 +121,6 @@ defmodule RustQ.Meta.Lower.Map do {:ok, Collection.updated( map, - [key], fn map -> [Collection.statement(map, :remove, [Collection.borrowed(key, key_type, context)])] end, @@ -134,9 +132,8 @@ defmodule RustQ.Meta.Lower.Map do {:ok, Collection.updated( map, - [other], fn map -> - [Collection.statement(map, :extend, [Collection.owned(other, [], context)])] + [Collection.statement(map, :extend, [Collection.owned(other, context)])] end, context )} @@ -155,7 +152,6 @@ defmodule RustQ.Meta.Lower.Map do {:ok, Collection.updated( map, - [key, default, fun], fn map -> updated = map @@ -191,7 +187,7 @@ defmodule RustQ.Meta.Lower.Map do defp lower_keyed(%Call{function: :to_list, args: [map]}, key_type, value_type, context) do {:ok, map - |> Collection.owned([], context) + |> Collection.owned(context) |> Collection.call(:into_iter) |> Collection.call(:collect) |> collected(Type.vec(Type.tuple([key_type, value_type])))} diff --git a/lib/rustq/meta/lower/map_set.ex b/lib/rustq/meta/lower/map_set.ex index a7469b6..1ea0d8a 100644 --- a/lib/rustq/meta/lower/map_set.ex +++ b/lib/rustq/meta/lower/map_set.ex @@ -38,7 +38,6 @@ defmodule RustQ.Meta.Lower.MapSet do {:ok, Collection.updated( set, - [value], fn set -> [Collection.statement(set, :insert, [context.lower_expected.(value, item)])] end, @@ -50,7 +49,6 @@ defmodule RustQ.Meta.Lower.MapSet do {:ok, Collection.updated( set, - [value], fn set -> [Collection.statement(set, :remove, [Collection.borrowed(value, item, context)])] end, @@ -67,9 +65,8 @@ defmodule RustQ.Meta.Lower.MapSet do {:ok, Collection.updated( set, - [other], fn set -> - [Collection.statement(set, :extend, [Collection.owned(other, [], context)])] + [Collection.statement(set, :extend, [Collection.owned(other, context)])] end, context )} @@ -88,7 +85,6 @@ defmodule RustQ.Meta.Lower.MapSet do {:ok, Collection.updated( set, - [other], fn set -> [Collection.statement(set, :retain, [%AST.Closure{args: [:rustq_item], body: kept}])] end, @@ -114,7 +110,7 @@ defmodule RustQ.Meta.Lower.MapSet do defp lower_set(%Call{function: :to_list, args: [set]}, item, context) do collected = set - |> Collection.owned([], context) + |> Collection.owned(context) |> Collection.call(:into_iter) |> Collection.call(:collect) diff --git a/lib/rustq/meta/lower/stdlib.ex b/lib/rustq/meta/lower/stdlib.ex index bfe51a3..75e0f37 100644 --- a/lib/rustq/meta/lower/stdlib.ex +++ b/lib/rustq/meta/lower/stdlib.ex @@ -40,7 +40,7 @@ defmodule RustQ.Meta.Lower.Stdlib do lower_capture: (Macro.t() -> term()), closure_arg: (Macro.t() -> atom()), type_of: (Macro.t() -> term()), - in_use?: (Macro.t(), [Macro.t()] -> boolean()), + in_use?: (Macro.t() -> boolean()), expected: Type.t() | nil } end diff --git a/lib/rustq/meta/ownership.ex b/lib/rustq/meta/ownership.ex new file mode 100644 index 0000000..99dc4b6 --- /dev/null +++ b/lib/rustq/meta/ownership.ex @@ -0,0 +1,385 @@ +defmodule RustQ.Meta.Ownership do + @moduledoc """ + Which uses of a variable in a Rusty-Elixir body are its last, so lowering knows where Rust + may take a value and where it must borrow or copy one. + + Elixir never changes a value, so a list, map, or set passed to a function is still there + for every later use. Rust code taking the value ends it, so lowering takes one only at its + last use. `annotate/1` walks a body backwards, as liveness is computed, and records on each + variable occurrence the variables still in use after it: + + * expressions, arguments, and pipes run in order; + * the branches of `case`, `if`, `cond`, and `with` are alternatives; + * a closure or comprehension body may run many times, so no use inside one is the last; + * rebinding a name ends the value it had. + + A value built from another, as `by_id = index(records)`, may borrow from it, so the other + stays in use while the built value does; `in_use?/3` asks. + """ + + alias RustQ.Meta.Type + alias RustQ.Rust.AST + + @live :rustq_live_after + + @typedoc "A set of variable names." + @type names :: %{optional(atom()) => true} + + @typedoc "The variables each binding was built from, by name." + @type built_from :: %{optional(atom()) => names()} + + @doc """ + Annotates every variable occurrence in `body` with the variables in use after it, and + returns what each binding was built from. + """ + @spec annotate(Macro.t()) :: {Macro.t(), built_from()} + def annotate(body) do + {annotated, _live_in} = live(body, %{}) + {annotated, built_from(body)} + end + + @doc """ + Whether the variable occurrence `variable` is not the last use of its value: a later use + follows, it is inside a body that may run again, or a value still in use was built from it + and, by its type in `vars`, may borrow from it. An occurrence lowering made itself, without + an annotation, is a fresh value's only use. + """ + @spec in_use?(Macro.t(), built_from(), %{optional(atom()) => Type.t()}) :: boolean() + def in_use?({name, meta, _context}, built_from, vars) when is_atom(name) do + case Keyword.get(meta, @live) do + nil -> + false + + live_after -> + member?(live_after, name) or + Enum.any?(Map.keys(live_after), &member?(sources(&1, built_from, vars), name)) + end + end + + def in_use?(_variable, _built_from, _vars), do: false + + # Backwards liveness: `live(ast, live_out)` annotates `ast` and returns the variables in + # use before it. + defp live({name, meta, context}, live_out) when is_atom(name) and is_atom(context) do + if variable?(name), + do: {{name, Keyword.put(meta, @live, live_out), context}, put(live_out, name)}, + else: {{name, meta, context}, live_out} + end + + defp live({:=, meta, [pattern, expression]}, live_out) do + after_binding = difference(live_out, bound(pattern)) + {pattern, pinned} = live_pins(pattern, after_binding) + {expression, live_in} = live(expression, pinned) + {{:=, meta, [pattern, expression]}, live_in} + end + + defp live({:__block__, meta, expressions}, live_out) do + {expressions, live_in} = live_sequence(expressions, live_out) + {{:__block__, meta, expressions}, live_in} + end + + # A closure may run many times, so what it captures stays in use throughout its body, while + # its own variables follow the body. What is live at the start of its body, with nothing in + # use after it, is what it captures. + defp live({:fn, meta, clauses}, live_out) do + {_clauses, captured} = live_clauses(clauses, %{}) + {clauses, _clauses_live} = live_clauses(clauses, captured) + {{:fn, meta, clauses}, union(live_out, captured)} + end + + defp live({:&, meta, [body]}, live_out) when not is_integer(body) do + {_body, captured} = live(body, %{}) + {body, _body_live} = live(body, captured) + {{:&, meta, [body]}, union(live_out, captured)} + end + + defp live({:case, meta, [scrutinee, [do: clauses]]}, live_out) when is_list(clauses) do + {clauses, branches} = live_clauses(clauses, live_out) + {scrutinee, live_in} = live(scrutinee, branches) + {{:case, meta, [scrutinee, [do: clauses]]}, live_in} + end + + defp live({:if, meta, [condition, branches]}, live_out) when is_list(branches) do + {branches, branch_live} = + Enum.map_reduce(branches, %{}, fn {key, branch}, union -> + {branch, branch_live} = live(branch, live_out) + {{key, branch}, union(union, branch_live)} + end) + + # A missing `else` does nothing, so what is in use after the `if` stays in use. + branch_live = + if Keyword.has_key?(branches, :else), + do: branch_live, + else: union(branch_live, live_out) + + {condition, live_in} = live(condition, branch_live) + {{:if, meta, [condition, branches]}, live_in} + end + + defp live({:with, meta, args}, live_out) do + {clauses, [options]} = Enum.split(args, -1) + {body, body_live} = live(Keyword.fetch!(options, :do), live_out) + + {else_clauses, else_live} = + case Keyword.get(options, :else) do + nil -> {nil, live_out} + clauses -> live_clauses(clauses, live_out) + end + + {clauses, live_in} = live_with_clauses(clauses, union(body_live, else_live)) + options = Keyword.replace!(options, :do, body) + options = if else_clauses, do: Keyword.replace!(options, :else, else_clauses), else: options + {{:with, meta, clauses ++ [options]}, live_in} + end + + # A comprehension evaluates its first collection once; the rest may run for every item, so + # what it captures stays in use throughout. + defp live({:for, meta, [{:<-, generator_meta, [pattern, collection]} | rest]}, live_out) do + {_rest, region_live} = live_comprehension(rest, %{}) + captured = difference(region_live, bound(pattern)) + {rest, _rest_live} = live_comprehension(rest, captured) + {collection, live_in} = live(collection, union(live_out, captured)) + {{:for, meta, [{:<-, generator_meta, [pattern, collection]} | rest]}, live_in} + end + + # A `cond` tests its conditions in order until one holds, and runs that clause's body. + defp live({:cond, meta, [[do: clauses]]}, live_out) do + {reversed, live_in} = + clauses + |> Enum.reverse() + |> Enum.map_reduce(%{}, fn {:->, clause_meta, [[condition], body]}, later -> + {body, body_live} = live(body, live_out) + {condition, live_in} = live(condition, union(body_live, later)) + {{:->, clause_meta, [[condition], body]}, live_in} + end) + + {{:cond, meta, [[do: Enum.reverse(reversed)]]}, live_in} + end + + defp live({:^, meta, [variable]}, live_out) do + {variable, live_in} = live(variable, live_out) + {{:^, meta, [variable]}, live_in} + end + + defp live({:__aliases__, _meta, _parts} = alias, live_out), do: {alias, live_out} + + # A call runs its receiver, then its arguments, in order; a pipe runs its left side first. + defp live({{:., dot_meta, [receiver, function]}, meta, args}, live_out) when is_list(args) do + {[receiver | args], live_in} = live_sequence([receiver | args], live_out) + {{{:., dot_meta, [receiver, function]}, meta, args}, live_in} + end + + defp live({form, meta, args}, live_out) when is_list(args) do + {args, live_in} = live_sequence(args, live_out) + {{form, meta, args}, live_in} + end + + defp live({left, right}, live_out) do + {[left, right], live_in} = live_sequence([left, right], live_out) + {{left, right}, live_in} + end + + defp live(list, live_out) when is_list(list), do: live_sequence(list, live_out) + defp live(other, live_out), do: {other, live_out} + + defp live_sequence(expressions, live_out) do + expressions + |> Enum.reverse() + |> Enum.map_reduce(live_out, &live/2) + |> then(fn {reversed, live_in} -> {Enum.reverse(reversed), live_in} end) + end + + # Each clause binds its pattern's variables for its body; the clauses are alternatives. + defp live_clauses(clauses, live_out) when is_list(clauses) do + Enum.map_reduce(clauses, %{}, fn {:->, meta, [heads, body]}, union -> + heads = List.wrap(heads) + {body, body_live} = live(body, live_out) + {heads, head_live} = live_heads(heads, body_live) + {{:->, meta, [heads, body]}, union(union, head_live)} + end) + end + + # `fn a, b when guard ->` puts its patterns and guard in one `when`. + defp live_heads([{:when, meta, patterns_and_guard}], body_live) do + {patterns, [guard]} = Enum.split(patterns_and_guard, -1) + {guard, guard_live} = live(guard, body_live) + {patterns, live_in} = live_patterns(patterns, guard_live) + {[{:when, meta, patterns ++ [guard]}], live_in} + end + + defp live_heads(patterns, body_live), do: live_patterns(patterns, body_live) + + defp live_patterns(patterns, live_out) do + bound = Enum.reduce(patterns, %{}, &union(&2, bound(&1))) + live_pins(patterns, difference(live_out, bound)) + end + + # A comprehension's qualifiers and option lists after its first generator: each generator + # binds its pattern for the qualifiers and body after it. The option lists keep their shape, + # as `for x <- xs, reduce: acc do ... end` writes two. + defp live_comprehension(rest, live_out) do + {qualifiers, option_lists} = Enum.split_while(rest, &(not Keyword.keyword?(&1) or &1 == [])) + {option_lists, body_live} = live_options(option_lists, live_out, &live_body_option/2) + {qualifiers, qualifiers_live} = live_qualifiers(qualifiers, body_live) + {option_lists, live_in} = live_options(option_lists, qualifiers_live, &live_other_option/2) + {qualifiers ++ option_lists, live_in} + end + + defp live_options(option_lists, live_out, live_option) do + option_lists + |> Enum.reverse() + |> Enum.map_reduce(live_out, fn options, live -> + {reversed, live} = options |> Enum.reverse() |> Enum.map_reduce(live, live_option) + {Enum.reverse(reversed), live} + end) + |> then(fn {reversed, live} -> {Enum.reverse(reversed), live} end) + end + + defp live_body_option({:do, [{:->, _meta, _clause} | _] = clauses}, live) do + {clauses, live} = live_clauses(clauses, live) + {{:do, clauses}, live} + end + + defp live_body_option({:do, body}, live) do + {body, live} = live(body, live) + {{:do, body}, live} + end + + defp live_body_option(option, live), do: {option, live} + + defp live_other_option({:do, _body} = option, live), do: {option, live} + + defp live_other_option({key, value}, live) do + {value, live} = live(value, live) + {{key, value}, live} + end + + defp live_qualifiers(qualifiers, live_out) do + qualifiers + |> Enum.reverse() + |> Enum.map_reduce(live_out, fn + {:<-, meta, [pattern, collection]}, live -> + live_generator(meta, pattern, collection, live) + + filter, live -> + live(filter, live) + end) + |> then(fn {reversed, live} -> {Enum.reverse(reversed), live} end) + end + + defp live_with_clauses(clauses, live_out) do + clauses + |> Enum.reverse() + |> Enum.map_reduce(live_out, fn + {:<-, meta, [pattern, expression]}, live -> + live_generator(meta, pattern, expression, live) + + expression, live -> + live(expression, live) + end) + |> then(fn {reversed, live_in} -> {Enum.reverse(reversed), live_in} end) + end + + # `pattern <- expression` binds the pattern's variables for what follows it. + defp live_generator(meta, pattern, expression, live) do + {pattern, pinned} = live_pins(pattern, difference(live, bound(pattern))) + {expression, live_in} = live(expression, pinned) + {{:<-, meta, [pattern, expression]}, live_in} + end + + # A pinned variable in a pattern is a use of it. + defp live_pins(pattern, live_out) do + Macro.prewalk(pattern, live_out, fn + {:^, meta, [{name, variable_meta, context}]}, live + when is_atom(name) and is_atom(context) -> + {{:^, meta, [{name, Keyword.put(variable_meta, @live, live), context}]}, put(live, name)} + + node, live -> + {node, live} + end) + end + + defp bound(pattern) do + pattern + |> Macro.prewalk(%{}, fn + {:^, _meta, [_pinned]}, names -> + {:pinned, names} + + {name, _meta, context} = node, names when is_atom(name) and is_atom(context) -> + {node, if(variable?(name), do: put(names, name), else: names)} + + node, names -> + {node, names} + end) + |> elem(1) + end + + defp free_variables(ast) do + ast + |> Macro.prewalk(%{}, fn + {name, _meta, context} = node, names when is_atom(name) and is_atom(context) -> + {node, if(variable?(name), do: put(names, name), else: names)} + + node, names -> + {node, names} + end) + |> elem(1) + end + + defp variable?(name), do: name not in [:_, :__MODULE__, :__CALLER__, :__ENV__, :__DIR__] + + # Each binding's name and the variables its expression uses; a later binding of a name + # replaces an earlier one. + defp built_from(body) do + body + |> Macro.prewalk(%{}, fn + {:=, _meta, [pattern, expression]} = node, built_from -> + from = free_variables(expression) + + {node, + pattern |> bound() |> Map.keys() |> Enum.reduce(built_from, &Map.put(&2, &1, from))} + + node, built_from -> + {node, built_from} + end) + |> elem(1) + end + + # The variables a value may borrow from: those it was built from, and theirs, through values + # whose types may hold a reference. A value of unknown type may. + defp sources(name, built_from, vars), do: add_sources(name, built_from, vars, %{}) + + defp add_sources(name, built_from, vars, found) do + if may_borrow?(Map.get(vars, name)) do + built_from + |> Map.get(name, %{}) + |> Map.keys() + |> Enum.reject(&member?(found, &1)) + |> Enum.reduce(found, fn source, found -> + add_sources(source, built_from, vars, put(found, source)) + end) + else + found + end + end + + defp may_borrow?(%Type{kind: kind}) when kind in [:ref, :mut_ref, :slice], do: true + defp may_borrow?(%Type{ast: ast}), do: borrowing_type?(ast) + defp may_borrow?(_unknown), do: true + + defp borrowing_type?(%AST.TypeRef{}), do: true + defp borrowing_type?(%AST.TypePath{lifetimes: [_ | _]}), do: true + defp borrowing_type?(%AST.TypeRaw{}), do: true + + defp borrowing_type?(%_{} = type), + do: type |> Map.from_struct() |> Enum.any?(fn {_field, value} -> borrowing_type?(value) end) + + defp borrowing_type?(types) when is_list(types), do: Enum.any?(types, &borrowing_type?/1) + defp borrowing_type?(_other), do: false + + # Sets of names are maps to `true`, as `MapSet` is, without its opaque type. + defp put(names, name), do: Map.put(names, name, true) + defp member?(names, name), do: Map.has_key?(names, name) + defp union(left, right), do: Map.merge(left, right) + defp difference(left, right), do: Map.drop(left, Map.keys(right)) +end diff --git a/test/rustq/meta/lower/map_test.exs b/test/rustq/meta/lower/map_test.exs index 174463a..751ef21 100644 --- a/test/rustq/meta/lower/map_test.exs +++ b/test/rustq/meta/lower/map_test.exs @@ -37,6 +37,10 @@ defmodule RustQ.Meta.Lower.MapTest do @spec names(R.ref(%{String.t() => integer()})) :: [String.t()] defrust(names(scores), do: Map.keys(scores)) + @spec beside(%{String.t() => integer()}, String.t()) :: + {%{String.t() => integer()}, %{String.t() => integer()}} + defrust(beside(scores, name), do: {Map.put(scores, name, 1), scores}) + @spec tallied(%{String.t() => integer()}, String.t()) :: %{String.t() => integer()} defrust tallied(scores, name) do scores = Map.put(scores, name, 1) @@ -66,6 +70,10 @@ defmodule RustQ.Meta.Lower.MapTest do assert rust_source!(Scores, :kept) =~ "let mut rustq_updated = scores.clone();" end + test "an update copies a map the same expression uses after it" do + assert rust_source!(Scores, :beside) =~ "let mut rustq_updated = scores.clone();" + end + test "rebinding a map to its update changes it in place" do source = rust_source!(Scores, :tallied) diff --git a/test/rustq/meta/ownership_test.exs b/test/rustq/meta/ownership_test.exs new file mode 100644 index 0000000..f7fc5b5 --- /dev/null +++ b/test/rustq/meta/ownership_test.exs @@ -0,0 +1,100 @@ +defmodule RustQ.Meta.OwnershipTest do + use ExUnit.Case, async: true + + alias RustQ.Meta.Ownership + + # Whether each use of `name`, in order, is still in use after it; a pattern binding the name + # is not a use. + defp uses(body, name, vars \\ %{}) do + {annotated, built_from} = Ownership.annotate(body) + + annotated + |> Macro.prewalk([], fn + {^name, meta, context} = node, uses when is_atom(context) -> + if Keyword.has_key?(meta, :rustq_live_after), + do: {node, [Ownership.in_use?(node, built_from, vars) | uses]}, + else: {node, uses} + + node, uses -> + {node, uses} + end) + |> elem(1) + |> Enum.reverse() + end + + test "the last use in order is the last" do + assert uses(quote(do: {f(list), g(list)}), :list) == [true, false] + end + + test "branches are alternatives" do + body = quote(do: if(flag, do: f(list), else: g(list))) + assert uses(body, :list) == [false, false] + end + + test "a use after the branches keeps the branches' uses from being last" do + body = + quote do + if flag, do: f(list) + g(list) + end + + assert uses(body, :list) == [true, false] + end + + test "a closure's captures stay in use throughout it, its own variables do not" do + body = quote(do: Enum.map(items, fn item -> f(list, item) end)) + + assert uses(body, :list) == [true] + assert uses(body, :item) == [false] + assert uses(quote(do: fn item -> {f(item), g(item)} end), :item) == [true, false] + end + + test "a shadowing inner closure does not hide what the outer one captures" do + body = quote(do: Enum.map(list, fn item -> f(item, first.map(fn first -> first end)) end)) + assert uses(body, :first) == [true, false] + end + + test "rebinding ends the value it replaces" do + body = + quote do + list = sort(list) + list + end + + assert uses(body, :list) == [false, false] + end + + test "a comprehension's collection is evaluated once, what its body captures is not" do + body = quote(do: for(item <- list, do: f(item, other))) + + assert uses(body, :list) == [false] + assert uses(body, :other) == [true] + end + + test "a value built from another keeps it in use while it may borrow from it" do + body = + quote do + index = by_id(records) + Enum.map(records, fn record -> lookup(index, record) end) + end + + assert uses(body, :records) == [true, true] + + owned = RustQ.Spec.type(quote(do: %{integer() => integer()})) + assert uses(body, :records, %{index: owned}) == [true, false] + end + + test "annotating keeps the shape lowering reads, as a with's do before its else" do + body = + quote do + with {:some, value} <- lookup(key) do + use(value) + else + _ -> :ok + end + end + + {{:with, _meta, [_clause, options]}, _built_from} = Ownership.annotate(body) + assert Keyword.keys(options) == [:do, :else] + end +end From 34d4b2552a5a5b0909caec25aed89303aef9f754 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 25 Oct 2026 21:17:38 +0400 Subject: [PATCH 02/10] Give the with-order test a second clause --- test/rustq/meta/ownership_test.exs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/rustq/meta/ownership_test.exs b/test/rustq/meta/ownership_test.exs index f7fc5b5..6274ce2 100644 --- a/test/rustq/meta/ownership_test.exs +++ b/test/rustq/meta/ownership_test.exs @@ -87,14 +87,15 @@ defmodule RustQ.Meta.OwnershipTest do test "annotating keeps the shape lowering reads, as a with's do before its else" do body = quote do - with {:some, value} <- lookup(key) do - use(value) + with {:some, value} <- lookup(key), + {:some, other} <- lookup(value) do + use(other) else _ -> :ok end end - {{:with, _meta, [_clause, options]}, _built_from} = Ownership.annotate(body) + {{:with, _meta, [_first, _second, options]}, _built_from} = Ownership.annotate(body) assert Keyword.keys(options) == [:do, :else] end end From 04f88dfe3c281a7b30ce85034f7cc8fb3e1d3b01 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 25 Oct 2026 21:39:03 +0400 Subject: [PATCH 03/10] Fuse Enum pipelines into one iterator chain, and type std methods from std's source --- dev/rustq/meta/std_methods/source.ex | 46 + lib/rustq/meta/lower/enum.ex | 65 +- lib/rustq/meta/std_methods.ex | 2341 ++++++++++++++++++++++++ lib/rustq/meta/typing.ex | 46 +- mix.exs | 2 +- test/rustq/meta/lower/defrust_test.exs | 11 +- test/rustq/meta/lower/enum_test.exs | 34 + test/rustq/meta/std_methods_test.exs | 12 + test/rustq/meta/typing_test.exs | 10 + 9 files changed, 2555 insertions(+), 12 deletions(-) diff --git a/dev/rustq/meta/std_methods/source.ex b/dev/rustq/meta/std_methods/source.ex index 9553da7..6df748a 100644 --- a/dev/rustq/meta/std_methods/source.ex +++ b/dev/rustq/meta/std_methods/source.ex @@ -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!() @@ -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), diff --git a/lib/rustq/meta/lower/enum.ex b/lib/rustq/meta/lower/enum.ex index d9a6aa1..9e14468 100644 --- a/lib/rustq/meta/lower/enum.ex +++ b/lib/rustq/meta/lower/enum.ex @@ -38,8 +38,13 @@ defmodule RustQ.Meta.Lower.Enum do when operation in [:filter, :reject, :flat_map, :each, :any?, :all?], do: {:ok, lower_operation(operation, collection, mapper, context)} - defp lower_call(%Call{function: :count, args: [collection]}, %Context{} = context), - do: {:ok, collection |> lower_collection(context) |> method_chain(:len) |> cast_i64()} + # A stage's items are counted as they come; a list knows its length. + defp lower_call(%Call{function: :count, args: [collection]}, %Context{} = context) do + case stage(collection) do + {:ok, stage} -> {:ok, stage |> continued(context) |> method_chain(:count) |> cast_i64()} + :error -> {:ok, collection |> lower_collection(context) |> method_chain(:len) |> cast_i64()} + end + end defp lower_call(%Call{function: :count, args: [collection, predicate]}, %Context{} = context) do iterator = iterate(collection, :question, context) @@ -863,6 +868,53 @@ defmodule RustQ.Meta.Lower.Enum do do: iterate_as(collection, iteration(collection, use, context), context) defp iterate_as(collection, iteration, context) do + case stage(collection) do + {:ok, stage} -> continued(stage, context) + :error -> iterate_receiver(collection, iteration, context) + end + end + + # A stage's iterator, without the list it would collect: `list |> Enum.map(f) |> Enum.find(p)` + # is one Rust chain, `.map(f).find(p)`, since no list between them is ever seen. + defp continued(stage, context) do + case context.lower.(stage) do + %AST.MethodCall{method: :collect, receiver: iterator} -> iterator + collected -> method_chain(collected, :into_iter) + end + end + + @stages [ + :map, + :filter, + :reject, + :flat_map, + :with_index, + :take_while, + :drop_while, + :zip_with, + :slice, + :take, + :drop, + :reverse, + :chunk_every + ] + + defp stage({:|>, _meta, [left, right]} = pipe) do + case Call.pipe_remote(left, right) do + {:ok, call} -> stage(call) + :unsupported -> stage_call(pipe) + end + end + + defp stage(collection), do: stage_call(collection) + + defp stage_call({{:., _, [{:__aliases__, _, [:Enum]}, function]}, _, _args} = call) + when function in @stages, + do: {:ok, call} + + defp stage_call(_collection), do: :error + + defp iterate_receiver(collection, iteration, context) do receiver = lower_collection(collection, context) case iteration do @@ -885,8 +937,13 @@ defmodule RustQ.Meta.Lower.Enum do # A question needs no item it could take, so it borrows other items even from a list nothing # uses afterwards. Copying out a number or atom costs what borrowing it does, and keeps the # item's own type. - defp iteration(collection, use, context), - do: iteration(context.type_of.(collection), collection, use, context) + # A stage's items are its own, so a call continuing it takes them. + defp iteration(collection, use, context) do + case stage(collection) do + {:ok, _stage} -> :owned + :error -> iteration(context.type_of.(collection), collection, use, context) + end + end defp iteration(%Type{kind: :vec} = type, collection, use, context), do: list_iteration(collection, type, use, context) diff --git a/lib/rustq/meta/std_methods.ex b/lib/rustq/meta/std_methods.ex index 6c117ae..22e3fd8 100644 --- a/lib/rustq/meta/std_methods.ex +++ b/lib/rustq/meta/std_methods.ex @@ -536,4 +536,2345 @@ defmodule RustQ.Meta.StdMethods do :write_clone_of_slice, :write_copy_of_slice ] + + @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: %{ + {"str", "slice_mut_unchecked"} => %RustQ.Syn.Type.Ref{ + code: "& mut str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + }, + {"str", "trim_right"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"char", "is_ascii_control"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "trim_start"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"char", "is_lowercase"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "find"} => %RustQ.Syn.Type.Option{ + code: "Option < usize >", + inner: %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"str", "strip_circumfix"} => %RustQ.Syn.Type.Option{ + code: "Option < & str >", + inner: %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + } + }, + {"Vec", "as_ptr"} => %RustQ.Syn.Type.Raw{code: "* const T"}, + {"String", "as_str"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"str", "as_bytes"} => %RustQ.Syn.Type.Ref{ + code: "& [u8]", + inner: %RustQ.Syn.Type.Slice{ + code: "[u8]", + inner: %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + lifetime: nil, + mutable: false + }, + {"Vec", "capacity"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "get_unchecked_mut"} => %RustQ.Syn.Type.Ref{ + code: "& mut I :: Output", + inner: %RustQ.Syn.Type.Path{ + code: "I :: Output", + name: "Output", + callable: nil, + segments: ["I", "Output"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + }, + {"str", "strip_suffix"} => %RustQ.Syn.Type.Option{ + code: "Option < & str >", + inner: %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + } + }, + {"char", "is_ascii_alphanumeric"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "binary_search_by_key"} => %RustQ.Syn.Type.Result{ + code: "Result < usize , usize >", + ok: %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + error: %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"str", "ends_with"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"HashSet", "capacity"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"Vec", "try_reserve"} => %RustQ.Syn.Type.Result{ + code: "Result < () , TryReserveError >", + ok: %RustQ.Syn.Type.Tuple{code: "()", elems: []}, + error: %RustQ.Syn.Type.Path{ + code: "TryReserveError", + name: "TryReserveError", + callable: nil, + segments: ["TryReserveError"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"[T]", "get_mut"} => %RustQ.Syn.Type.Option{ + code: "Option < & mut I :: Output >", + inner: %RustQ.Syn.Type.Ref{ + code: "& mut I :: Output", + inner: %RustQ.Syn.Type.Path{ + code: "I :: Output", + name: "Output", + callable: nil, + segments: ["I", "Output"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + } + }, + {"str", "escape_debug"} => %RustQ.Syn.Type.Path{ + code: "EscapeDebug < '_ >", + name: "EscapeDebug", + callable: nil, + segments: ["EscapeDebug"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"str", "into_boxed_bytes"} => %RustQ.Syn.Type.Path{ + code: "Box < [u8] >", + name: "Box", + callable: nil, + segments: ["Box"], + args: [ + %RustQ.Syn.Type.Slice{ + code: "[u8]", + inner: %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + } + } + ], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{ + kind: :type, + name: nil, + type: %RustQ.Syn.Type.Slice{ + code: "[u8]", + inner: %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + source: nil + } + ] + }, + {"str", "len"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"String", "is_empty"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "len_utf8"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "is_sorted"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "encode_utf8"} => %RustQ.Syn.Type.Ref{ + code: "& mut str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + }, + {"str", "slice_unchecked"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"str", "to_uppercase"} => %RustQ.Syn.Type.Path{ + code: "String", + name: "String", + callable: nil, + segments: ["String"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "join"} => %RustQ.Syn.Type.Raw{code: "< Self as Join < Separator > > :: Output"}, + {"str", "replacen"} => %RustQ.Syn.Type.Path{ + code: "String", + name: "String", + callable: nil, + segments: ["String"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "connect"} => %RustQ.Syn.Type.Raw{code: "< Self as Join < Separator > > :: Output"}, + {"Vec", "as_mut_ptr"} => %RustQ.Syn.Type.Raw{code: "* mut T"}, + {"HashSet", "is_empty"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "len_utf16"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "split_ascii_whitespace"} => %RustQ.Syn.Type.Path{ + code: "SplitAsciiWhitespace < '_ >", + name: "SplitAsciiWhitespace", + callable: nil, + segments: ["SplitAsciiWhitespace"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"HashSet", "is_disjoint"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "to_lowercase"} => %RustQ.Syn.Type.Path{ + code: "String", + name: "String", + callable: nil, + segments: ["String"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "trim_ascii_end"} => %RustQ.Syn.Type.Ref{ + code: "& [u8]", + inner: %RustQ.Syn.Type.Slice{ + code: "[u8]", + inner: %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + lifetime: nil, + mutable: false + }, + {"HashSet", "insert"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "trim_left_matches"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"str", "trim"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"Vec", "into_raw_parts"} => %RustQ.Syn.Type.Tuple{ + code: "(* mut T , usize , usize)", + elems: [ + %RustQ.Syn.Type.Raw{code: "* mut T"}, + %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + } + ] + }, + {"String", "len"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "escape_unicode"} => %RustQ.Syn.Type.Path{ + code: "EscapeUnicode", + name: "EscapeUnicode", + callable: nil, + segments: ["EscapeUnicode"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "to_ascii_lowercase"} => %RustQ.Syn.Type.Path{ + code: "Vec < u8 >", + name: "Vec", + callable: nil, + segments: ["Vec"], + args: [ + %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + } + ], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{ + kind: :type, + name: nil, + type: %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + }, + source: nil + } + ] + }, + {"str", "trim_left"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"str", "contains"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "as_bytes_mut"} => %RustQ.Syn.Type.Ref{ + code: "& mut [u8]", + inner: %RustQ.Syn.Type.Slice{ + code: "[u8]", + inner: %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + lifetime: nil, + mutable: true + }, + {"[T]", "concat"} => %RustQ.Syn.Type.Raw{code: "< Self as Concat < Item > > :: Output"}, + {"[T]", "as_ptr"} => %RustQ.Syn.Type.Raw{code: "* const T"}, + {"String", "drain"} => %RustQ.Syn.Type.Path{ + code: "Drain < '_ >", + name: "Drain", + callable: nil, + segments: ["Drain"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"String", "pop"} => %RustQ.Syn.Type.Option{ + code: "Option < char >", + inner: %RustQ.Syn.Type.Path{ + code: "char", + name: "char", + callable: nil, + segments: ["char"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"[T]", "get"} => %RustQ.Syn.Type.Option{ + code: "Option < & I :: Output >", + inner: %RustQ.Syn.Type.Ref{ + code: "& I :: Output", + inner: %RustQ.Syn.Type.Path{ + code: "I :: Output", + name: "Output", + callable: nil, + segments: ["I", "Output"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + } + }, + {"char", "is_digit"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "binary_search"} => %RustQ.Syn.Type.Result{ + code: "Result < usize , usize >", + ok: %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + error: %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"[T]", "subslice_range"} => %RustQ.Syn.Type.Option{ + code: "Option < core :: range :: Range < usize > >", + inner: %RustQ.Syn.Type.Path{ + code: "core :: range :: Range < usize >", + name: "Range", + callable: nil, + segments: ["core", "range", "Range"], + args: [ + %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + } + ], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{ + kind: :type, + name: nil, + type: %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + source: nil + } + ] + } + }, + {"char", "eq_ignore_ascii_case"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"HashMap", "try_reserve"} => %RustQ.Syn.Type.Result{ + code: "Result < () , TryReserveError >", + ok: %RustQ.Syn.Type.Tuple{code: "()", elems: []}, + error: %RustQ.Syn.Type.Path{ + code: "TryReserveError", + name: "TryReserveError", + callable: nil, + segments: ["TryReserveError"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"Option", "is_some"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "is_uppercase"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "partition_point"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"String", "as_mut_vec"} => %RustQ.Syn.Type.Ref{ + code: "& mut Vec < u8 >", + inner: %RustQ.Syn.Type.Path{ + code: "Vec < u8 >", + name: "Vec", + callable: nil, + segments: ["Vec"], + args: [ + %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + } + ], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{ + kind: :type, + name: nil, + type: %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + }, + source: nil + } + ] + }, + lifetime: nil, + mutable: true + }, + {"Option", "is_none"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "get"} => %RustQ.Syn.Type.Option{ + code: "Option < & I :: Output >", + inner: %RustQ.Syn.Type.Ref{ + code: "& I :: Output", + inner: %RustQ.Syn.Type.Path{ + code: "I :: Output", + name: "Output", + callable: nil, + segments: ["I", "Output"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + } + }, + {"str", "chars"} => %RustQ.Syn.Type.Path{ + code: "Chars < '_ >", + name: "Chars", + callable: nil, + segments: ["Chars"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"HashSet", "remove"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "split_whitespace"} => %RustQ.Syn.Type.Path{ + code: "SplitWhitespace < '_ >", + name: "SplitWhitespace", + callable: nil, + segments: ["SplitWhitespace"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"str", "repeat"} => %RustQ.Syn.Type.Path{ + code: "String", + name: "String", + callable: nil, + segments: ["String"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"String", "try_reserve_exact"} => %RustQ.Syn.Type.Result{ + code: "Result < () , TryReserveError >", + ok: %RustQ.Syn.Type.Tuple{code: "()", elems: []}, + error: %RustQ.Syn.Type.Path{ + code: "TryReserveError", + name: "TryReserveError", + callable: nil, + segments: ["TryReserveError"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"str", "get_mut"} => %RustQ.Syn.Type.Option{ + code: "Option < & mut I :: Output >", + inner: %RustQ.Syn.Type.Ref{ + code: "& mut I :: Output", + inner: %RustQ.Syn.Type.Path{ + code: "I :: Output", + name: "Output", + callable: nil, + segments: ["I", "Output"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + } + }, + {"[T]", "contains"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "escape_debug"} => %RustQ.Syn.Type.Path{ + code: "EscapeDebug", + name: "EscapeDebug", + callable: nil, + segments: ["EscapeDebug"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "is_ascii"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "is_empty"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "as_mut_ptr"} => %RustQ.Syn.Type.Raw{code: "* mut u8"}, + {"str", "trim_end_matches"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"Option", "is_some_and"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "lines"} => %RustQ.Syn.Type.Path{ + code: "Lines < '_ >", + name: "Lines", + callable: nil, + segments: ["Lines"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"HashMap", "capacity"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "as_ptr_range"} => %RustQ.Syn.Type.Path{ + code: "Range < * const T >", + name: "Range", + callable: nil, + segments: ["Range"], + args: [%RustQ.Syn.Type.Raw{code: "* const T"}], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{ + kind: :type, + name: nil, + type: %RustQ.Syn.Type.Raw{code: "* const T"}, + source: nil + } + ] + }, + {"String", "as_mut_str"} => %RustQ.Syn.Type.Ref{ + code: "& mut str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + }, + {"str", "split_at_mut_checked"} => %RustQ.Syn.Type.Option{ + code: "Option < (& mut str , & mut str) >", + inner: %RustQ.Syn.Type.Tuple{ + code: "(& mut str , & mut str)", + elems: [ + %RustQ.Syn.Type.Ref{ + code: "& mut str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + }, + %RustQ.Syn.Type.Ref{ + code: "& mut str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + } + ] + } + }, + {"str", "trim_ascii_end"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"str", "to_ascii_uppercase"} => %RustQ.Syn.Type.Path{ + code: "String", + name: "String", + callable: nil, + segments: ["String"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "split_at_mut"} => %RustQ.Syn.Type.Tuple{ + code: "(& mut str , & mut str)", + elems: [ + %RustQ.Syn.Type.Ref{ + code: "& mut str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + }, + %RustQ.Syn.Type.Ref{ + code: "& mut str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + } + ] + }, + {"str", "char_indices"} => %RustQ.Syn.Type.Path{ + code: "CharIndices < '_ >", + name: "CharIndices", + callable: nil, + segments: ["CharIndices"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"char", "to_digit"} => %RustQ.Syn.Type.Option{ + code: "Option < u32 >", + inner: %RustQ.Syn.Type.Path{ + code: "u32", + name: "u32", + callable: nil, + segments: ["u32"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"str", "escape_unicode"} => %RustQ.Syn.Type.Path{ + code: "EscapeUnicode < '_ >", + name: "EscapeUnicode", + callable: nil, + segments: ["EscapeUnicode"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"[T]", "starts_with"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"Option", "is_none_or"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "get_unchecked"} => %RustQ.Syn.Type.Ref{ + code: "& I :: Output", + inner: %RustQ.Syn.Type.Path{ + code: "I :: Output", + name: "Output", + callable: nil, + segments: ["I", "Output"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"[T]", "as_mut_ptr_range"} => %RustQ.Syn.Type.Path{ + code: "Range < * mut T >", + name: "Range", + callable: nil, + segments: ["Range"], + args: [%RustQ.Syn.Type.Raw{code: "* mut T"}], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{ + kind: :type, + name: nil, + type: %RustQ.Syn.Type.Raw{code: "* mut T"}, + source: nil + } + ] + }, + {"char", "to_ascii_lowercase"} => %RustQ.Syn.Type.Path{ + code: "char", + name: "char", + callable: nil, + segments: ["char"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"String", "remove"} => %RustQ.Syn.Type.Path{ + code: "char", + name: "char", + callable: nil, + segments: ["char"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "ceil_char_boundary"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"HashMap", "is_empty"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "ends_with"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"HashMap", "contains_key"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "split_at_checked"} => %RustQ.Syn.Type.Option{ + code: "Option < (& str , & str) >", + inner: %RustQ.Syn.Type.Tuple{ + code: "(& str , & str)", + elems: [ + %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + } + ] + } + }, + {"[T]", "is_ascii"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "get_unchecked"} => %RustQ.Syn.Type.Ref{ + code: "& I :: Output", + inner: %RustQ.Syn.Type.Path{ + code: "I :: Output", + name: "Output", + callable: nil, + segments: ["I", "Output"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"Vec", "is_empty"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "trim_ascii_start"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"char", "is_ascii_lowercase"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "escape_ascii"} => %RustQ.Syn.Type.Path{ + code: "EscapeAscii < '_ >", + name: "EscapeAscii", + callable: nil, + segments: ["EscapeAscii"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"str", "replace"} => %RustQ.Syn.Type.Path{ + code: "String", + name: "String", + callable: nil, + segments: ["String"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "split_at"} => %RustQ.Syn.Type.Tuple{ + code: "(& str , & str)", + elems: [ + %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + } + ] + }, + {"[T]", "trim_ascii"} => %RustQ.Syn.Type.Ref{ + code: "& [u8]", + inner: %RustQ.Syn.Type.Slice{ + code: "[u8]", + inner: %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + lifetime: nil, + mutable: false + }, + {"char", "is_ascii_whitespace"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "is_sorted_by_key"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "binary_search_by"} => %RustQ.Syn.Type.Result{ + code: "Result < usize , usize >", + ok: %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + error: %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"char", "is_numeric"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "is_ascii_punctuation"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "trim_ascii_start"} => %RustQ.Syn.Type.Ref{ + code: "& [u8]", + inner: %RustQ.Syn.Type.Slice{ + code: "[u8]", + inner: %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + lifetime: nil, + mutable: false + }, + {"HashSet", "is_subset"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "substr_range"} => %RustQ.Syn.Type.Option{ + code: "Option < Range < usize > >", + inner: %RustQ.Syn.Type.Path{ + code: "Range < usize >", + name: "Range", + callable: nil, + segments: ["Range"], + args: [ + %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + } + ], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{ + kind: :type, + name: nil, + type: %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + source: nil + } + ] + } + }, + {"[T]", "get_disjoint_mut"} => %RustQ.Syn.Type.Result{ + code: "Result < [& mut I :: Output ; N] , GetDisjointMutError >", + ok: %RustQ.Syn.Type.Array{ + code: "[& mut I :: Output ; N]", + inner: %RustQ.Syn.Type.Ref{ + code: "& mut I :: Output", + inner: %RustQ.Syn.Type.Path{ + code: "I :: Output", + name: "Output", + callable: nil, + segments: ["I", "Output"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + }, + length: "N" + }, + error: %RustQ.Syn.Type.Path{ + code: "GetDisjointMutError", + name: "GetDisjointMutError", + callable: nil, + segments: ["GetDisjointMutError"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"[T]", "element_offset"} => %RustQ.Syn.Type.Option{ + code: "Option < usize >", + inner: %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"str", "strip_prefix"} => %RustQ.Syn.Type.Option{ + code: "Option < & str >", + inner: %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + } + }, + {"char", "encode_utf16"} => %RustQ.Syn.Type.Ref{ + code: "& mut [u16]", + inner: %RustQ.Syn.Type.Slice{ + code: "[u16]", + inner: %RustQ.Syn.Type.Path{ + code: "u16", + name: "u16", + callable: nil, + segments: ["u16"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + lifetime: nil, + mutable: true + }, + {"HashSet", "contains"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "to_uppercase"} => %RustQ.Syn.Type.Path{ + code: "ToUppercase", + name: "ToUppercase", + callable: nil, + segments: ["ToUppercase"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "is_ascii_digit"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "is_alphabetic"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "trim_end"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"char", "is_whitespace"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "len"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "trim_start_matches"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"[T]", "get_disjoint_unchecked_mut"} => %RustQ.Syn.Type.Array{ + code: "[& mut I :: Output ; N]", + inner: %RustQ.Syn.Type.Ref{ + code: "& mut I :: Output", + inner: %RustQ.Syn.Type.Path{ + code: "I :: Output", + name: "Output", + callable: nil, + segments: ["I", "Output"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + }, + length: "N" + }, + {"char", "is_ascii"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"String", "into_boxed_str"} => %RustQ.Syn.Type.Path{ + code: "Box < str >", + name: "Box", + callable: nil, + segments: ["Box"], + args: [ + %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + } + ], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{ + kind: :type, + name: nil, + type: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + source: nil + } + ] + }, + {"char", "is_control"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "is_empty"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "is_char_boundary"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"HashSet", "len"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"String", "into_bytes"} => %RustQ.Syn.Type.Path{ + code: "Vec < u8 >", + name: "Vec", + callable: nil, + segments: ["Vec"], + args: [ + %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + } + ], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{ + kind: :type, + name: nil, + type: %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + }, + source: nil + } + ] + }, + {"str", "trim_right_matches"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"[T]", "as_mut_ptr"} => %RustQ.Syn.Type.Raw{code: "* mut T"}, + {"[T]", "is_sorted_by"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "to_ascii_uppercase"} => %RustQ.Syn.Type.Path{ + code: "char", + name: "char", + callable: nil, + segments: ["char"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"String", "into_raw_parts"} => %RustQ.Syn.Type.Tuple{ + code: "(* mut u8 , usize , usize)", + elems: [ + %RustQ.Syn.Type.Raw{code: "* mut u8"}, + %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + } + ] + }, + {"str", "to_ascii_lowercase"} => %RustQ.Syn.Type.Path{ + code: "String", + name: "String", + callable: nil, + segments: ["String"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "eq_ignore_ascii_case"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "is_alphanumeric"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "to_ascii_uppercase"} => %RustQ.Syn.Type.Path{ + code: "Vec < u8 >", + name: "Vec", + callable: nil, + segments: ["Vec"], + args: [ + %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + } + ], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{ + kind: :type, + name: nil, + type: %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + }, + source: nil + } + ] + }, + {"str", "trim_matches"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"HashMap", "len"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "split_once"} => %RustQ.Syn.Type.Option{ + code: "Option < (& '_ str , & '_ str) >", + inner: %RustQ.Syn.Type.Tuple{ + code: "(& '_ str , & '_ str)", + elems: [ + %RustQ.Syn.Type.Ref{ + code: "& '_ str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: "'_", + mutable: false + }, + %RustQ.Syn.Type.Ref{ + code: "& '_ str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: "'_", + mutable: false + } + ] + } + }, + {"str", "bytes"} => %RustQ.Syn.Type.Path{ + code: "Bytes < '_ >", + name: "Bytes", + callable: nil, + segments: ["Bytes"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"Vec", "try_reserve_exact"} => %RustQ.Syn.Type.Result{ + code: "Result < () , TryReserveError >", + ok: %RustQ.Syn.Type.Tuple{code: "()", elems: []}, + error: %RustQ.Syn.Type.Path{ + code: "TryReserveError", + name: "TryReserveError", + callable: nil, + segments: ["TryReserveError"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"HashSet", "is_superset"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"String", "split_off"} => %RustQ.Syn.Type.Path{ + code: "String", + name: "String", + callable: nil, + segments: ["String"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "is_ascii_hexdigit"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"String", "as_bytes"} => %RustQ.Syn.Type.Ref{ + code: "& [u8]", + inner: %RustQ.Syn.Type.Slice{ + code: "[u8]", + inner: %RustQ.Syn.Type.Path{ + code: "u8", + name: "u8", + callable: nil, + segments: ["u8"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + lifetime: nil, + mutable: false + }, + {"Option", "as_deref_mut"} => %RustQ.Syn.Type.Option{ + code: "Option < & mut T :: Target >", + inner: %RustQ.Syn.Type.Ref{ + code: "& mut T :: Target", + inner: %RustQ.Syn.Type.Path{ + code: "T :: Target", + name: "Target", + callable: nil, + segments: ["T", "Target"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + } + }, + {"str", "floor_char_boundary"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"char", "is_ascii_graphic"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "utf8_chunks"} => %RustQ.Syn.Type.Path{ + code: "Utf8Chunks < '_ >", + name: "Utf8Chunks", + callable: nil, + segments: ["Utf8Chunks"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"str", "lines_any"} => %RustQ.Syn.Type.Path{ + code: "LinesAny < '_ >", + name: "LinesAny", + callable: nil, + segments: ["LinesAny"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"String", "leak"} => %RustQ.Syn.Type.Ref{ + code: "& 'a mut str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: "'a", + mutable: true + }, + {"Vec", "len"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"HashSet", "try_reserve"} => %RustQ.Syn.Type.Result{ + code: "Result < () , TryReserveError >", + ok: %RustQ.Syn.Type.Tuple{code: "()", elems: []}, + error: %RustQ.Syn.Type.Path{ + code: "TryReserveError", + name: "TryReserveError", + callable: nil, + segments: ["TryReserveError"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"char", "is_ascii_uppercase"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "encode_utf16"} => %RustQ.Syn.Type.Path{ + code: "EncodeUtf16 < '_ >", + name: "EncodeUtf16", + callable: nil, + segments: ["EncodeUtf16"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"char", "escape_default"} => %RustQ.Syn.Type.Path{ + code: "EscapeDefault", + name: "EscapeDefault", + callable: nil, + segments: ["EscapeDefault"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "trim_ascii"} => %RustQ.Syn.Type.Ref{ + code: "& str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + }, + {"str", "as_ptr"} => %RustQ.Syn.Type.Raw{code: "* const u8"}, + {"char", "to_lowercase"} => %RustQ.Syn.Type.Path{ + code: "ToLowercase", + name: "ToLowercase", + callable: nil, + segments: ["ToLowercase"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"Option", "as_deref"} => %RustQ.Syn.Type.Option{ + code: "Option < & T :: Target >", + inner: %RustQ.Syn.Type.Ref{ + code: "& T :: Target", + inner: %RustQ.Syn.Type.Path{ + code: "T :: Target", + name: "Target", + callable: nil, + segments: ["T", "Target"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: false + } + }, + {"String", "try_reserve"} => %RustQ.Syn.Type.Result{ + code: "Result < () , TryReserveError >", + ok: %RustQ.Syn.Type.Tuple{code: "()", elems: []}, + error: %RustQ.Syn.Type.Path{ + code: "TryReserveError", + name: "TryReserveError", + callable: nil, + segments: ["TryReserveError"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"str", "rsplit_once"} => %RustQ.Syn.Type.Option{ + code: "Option < (& '_ str , & '_ str) >", + inner: %RustQ.Syn.Type.Tuple{ + code: "(& '_ str , & '_ str)", + elems: [ + %RustQ.Syn.Type.Ref{ + code: "& '_ str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: "'_", + mutable: false + }, + %RustQ.Syn.Type.Ref{ + code: "& '_ str", + inner: %RustQ.Syn.Type.Path{ + code: "str", + name: "str", + callable: nil, + segments: ["str"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: "'_", + mutable: false + } + ] + } + }, + {"str", "starts_with"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "into_string"} => %RustQ.Syn.Type.Path{ + code: "String", + name: "String", + callable: nil, + segments: ["String"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"str", "escape_default"} => %RustQ.Syn.Type.Path{ + code: "EscapeDefault < '_ >", + name: "EscapeDefault", + callable: nil, + segments: ["EscapeDefault"], + args: [], + assoc: %{}, + generic_args: [ + %RustQ.Syn.Type.GenericArgument{kind: :lifetime, name: nil, type: nil, source: "'_"} + ] + }, + {"char", "is_ascii_alphabetic"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"String", "capacity"} => %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + }, + {"[T]", "get_unchecked_mut"} => %RustQ.Syn.Type.Ref{ + code: "& mut I :: Output", + inner: %RustQ.Syn.Type.Path{ + code: "I :: Output", + name: "Output", + callable: nil, + segments: ["I", "Output"], + args: [], + assoc: %{}, + generic_args: [] + }, + lifetime: nil, + mutable: true + }, + {"str", "rfind"} => %RustQ.Syn.Type.Option{ + code: "Option < usize >", + inner: %RustQ.Syn.Type.Path{ + code: "usize", + name: "usize", + callable: nil, + segments: ["usize"], + args: [], + assoc: %{}, + generic_args: [] + } + }, + {"[T]", "eq_ignore_ascii_case"} => %RustQ.Syn.Type.Path{ + code: "bool", + name: "bool", + callable: nil, + segments: ["bool"], + args: [], + assoc: %{}, + generic_args: [] + } + } end diff --git a/lib/rustq/meta/typing.ex b/lib/rustq/meta/typing.ex index ce86c84..f576799 100644 --- a/lib/rustq/meta/typing.ex +++ b/lib/rustq/meta/typing.ex @@ -13,8 +13,10 @@ defmodule RustQ.Meta.Typing do alias RustQ.Meta.Lower.Stdlib.TypeContext, as: StdlibTypeContext alias RustQ.Meta.Pattern alias RustQ.Meta.StandardPointer + alias RustQ.Meta.StdMethods alias RustQ.Meta.Type alias RustQ.Rust.AST + alias RustQ.Spec defmodule Env do @moduledoc """ @@ -482,12 +484,50 @@ defmodule RustQ.Meta.Typing do defp synth_method_return({{:., _, [receiver, function]}, _meta, args}, %Env{} = env) when is_atom(function) and is_list(args) do - receiver - |> synth(env) + receiver_type = synth(receiver, env) + + receiver_type |> callable_target_from_type() - |> then(&BindingIndex.return_type(env.callables, &1, function, length(args))) + |> then(&BindingIndex.return_type(env.callables, &1, function, length(args))) || + std_return(receiver_type, function) + end + + # What a std method returns on a string, character, list, map, set, or option, as std's + # source says; see `RustQ.Meta.StdMethods.returns/0`. + defp std_return(%Type{} = receiver, function) do + name = Atom.to_string(function) + returns = StdMethods.returns() + + receiver + |> std_receivers() + |> Enum.find_value(&Map.get(returns, {&1, name})) + |> then(&(&1 && Spec.from_syn(&1))) + end + + defp std_return(_receiver, _function), do: nil + + defp std_receivers(%Type{kind: kind} = type) when kind in [:ref, :mut_ref], + do: std_receivers(Type.ref_inner(type)) + + defp std_receivers(%Type{kind: :vec}), do: ["Vec", "[T]"] + defp std_receivers(%Type{kind: :slice}), do: ["[T]"] + defp std_receivers(%Type{kind: :option}), do: ["Option"] + + defp std_receivers(%Type{ast: %AST.TypePath{parts: [:String], generics: []}}), + do: ["String", "str"] + + defp std_receivers(%Type{rust: rust}) when rust in ["str", "&str"], do: ["str"] + defp std_receivers(%Type{rust: "char"}), do: ["char"] + + defp std_receivers(%Type{ast: %AST.TypePath{parts: parts}}) do + case List.last(parts) do + collection when collection in [:HashMap, :HashSet] -> [Atom.to_string(collection)] + _other -> [] + end end + defp std_receivers(_type), do: [] + defp field_type(%Type{} = type, field) when is_atom(field) do type |> field_receiver_type() diff --git a/mix.exs b/mix.exs index a8d51c3..040f432 100644 --- a/mix.exs +++ b/mix.exs @@ -68,7 +68,7 @@ defmodule RustQ.MixProject do "test", "credo --strict", "dialyzer", - "ex_dna --max-clones 0", + "ex_dna --max-clones 0 --ignore lib/rustq/meta/std_methods.ex", "reach.check --arch --smells" ], "rust.fmt": &rust_fmt/1, diff --git a/test/rustq/meta/lower/defrust_test.exs b/test/rustq/meta/lower/defrust_test.exs index d913421..c5f218d 100644 --- a/test/rustq/meta/lower/defrust_test.exs +++ b/test/rustq/meta/lower/defrust_test.exs @@ -733,11 +733,14 @@ defmodule RustQ.Meta.Lower.DefrustTest do count_positive = rust_source!(RemotePipelineCase, :count_positive) has_square = rust_source!(RemotePipelineCase, :has_square) - assert positive_square_sum =~ - ~r/filter\(.*collect::>.*\.map.*collect::>.*\.fold/s + assert positive_square_sum =~ ~r/\.filter\(\|&value\| value > 0\)\s*\.map\(.*\)\s*\.fold\(/s + refute positive_square_sum =~ "collect" + + assert count_positive =~ ~r/\.filter\(\|&value\| value > 0\)\s*\.count\(\) as i64/s + + assert has_square =~ + "values.into_iter().map(|value| value * value).any(|value| value == expected)" - assert count_positive =~ ~r/filter\(.*collect::>.*\.len\(\) as i64/s - assert has_square =~ ~r/\.map.*collect::>.*\.any/s assert rust_source!(RemotePipelineCase, :first_or) =~ ".into_iter().next().unwrap_or(default)" assert rust_source!(RemotePipelineCase, :trimmed) =~ ".trim().to_string()" assert RustQ.valid?(rust_source!(RemotePipelineCase), "remote_pipeline_case.rs") diff --git a/test/rustq/meta/lower/enum_test.exs b/test/rustq/meta/lower/enum_test.exs index 65f3444..1986633 100644 --- a/test/rustq/meta/lower/enum_test.exs +++ b/test/rustq/meta/lower/enum_test.exs @@ -127,6 +127,29 @@ defmodule RustQ.Meta.Lower.EnumTest do defrust(named(names), do: Enum.into(names, MapSet.new())) end + defmodule Pipelines do + @moduledoc "`Enum` pipelines, which run as one Rust iterator chain." + + use RustQ.Meta + + alias RustQ.Type, as: R + + @spec first_long([String.t()]) :: R.option(R.usize()) + defrust first_long(names) do + names |> Enum.map(fn name -> name.len() end) |> Enum.find(fn length -> length > 3 end) + end + + @spec positive_product([integer()]) :: integer() + defrust positive_product(values) do + values + |> Enum.filter(fn value -> value > 0 end) + |> Enum.reduce(1, fn value, product -> value * product end) + end + + @spec doubled_sum([integer()]) :: integer() + defrust(doubled_sum(values), do: Enum.sum(Enum.map(values, fn value -> value * 2 end))) + end + test "a question borrows the list it asks about" do assert rust_source!(Lists, :any_long) =~ "names.iter().any(|name| name.len() > 3)" end @@ -198,4 +221,15 @@ defmodule RustQ.Meta.Lower.EnumTest do assert rust_source!(Queries, :described) =~ ~s|.collect::>().join("-")| assert rust_source!(Queries, :named) =~ "names.into_iter().collect()" end + + test "a pipeline of Enum stages collects no list between them" do + assert rust_source!(Pipelines, :first_long) =~ + "names.into_iter().map(|name| name.len()).find(|&length| length > 3)" + + assert rust_source!(Pipelines, :positive_product) =~ + ~r/values\s*\.into_iter\(\)\s*\.filter\(\|&value\| value > 0\)\s*\.fold\(/ + + assert rust_source!(Pipelines, :doubled_sum) =~ + "values.into_iter().map(|value| value * 2).sum()" + end end diff --git a/test/rustq/meta/std_methods_test.exs b/test/rustq/meta/std_methods_test.exs index d648559..f777525 100644 --- a/test/rustq/meta/std_methods_test.exs +++ b/test/rustq/meta/std_methods_test.exs @@ -43,4 +43,16 @@ defmodule RustQ.Meta.StdMethodsTest do refute Enum.any?(all, &String.starts_with?(Atom.to_string(&1), "_")) end + + test "derives what std's methods on its values return, where the receiver's types do not matter" do + returns = StdMethods.returns() + + assert %{code: "usize"} = returns[{"str", "len"}] + assert %{code: "bool"} = returns[{"String", "is_empty"}] + assert %{code: "& str"} = returns[{"str", "trim"}] + assert %{code: "usize"} = returns[{"Vec", "len"}] + + # `Vec::first` returns `Option<&T>`, which depends on the list's item type. + refute Map.has_key?(returns, {"[T]", "first"}) + end end diff --git a/test/rustq/meta/typing_test.exs b/test/rustq/meta/typing_test.exs index 0c5088a..ffe4fa8 100644 --- a/test/rustq/meta/typing_test.exs +++ b/test/rustq/meta/typing_test.exs @@ -441,4 +441,14 @@ defmodule RustQ.Meta.TypingTest do assert %Typing.Check{type: ^count, coercion: :none} = Typing.check(quote(do: count), count, env) end + + test "types std methods on strings, lists, and maps from std's source" do + string = Type.parse(quote(do: String.t()), %{}) + names = Type.parse(quote(do: [String.t()]), %{}) + env = Typing.env(vars: %{name: Type.ref(string), names: names}) + + assert %Type{rust: "usize"} = Typing.synth(quote(do: name.len()), env) + assert %Type{rust: "bool"} = Typing.synth(quote(do: names.is_empty()), env) + assert %Type{rust: "&str"} = Typing.synth(quote(do: name.trim()), env) + end end From b94a9454b6351ce365d29c26c397dfca9e93017e Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 25 Oct 2026 21:57:57 +0400 Subject: [PATCH 04/10] Desugar standard-library calls that are other Rusty-Elixir before lowering --- lib/rustq/meta/desugar.ex | 166 +++++++++++++++++++++++++++++++ lib/rustq/meta/lower.ex | 64 +++++++----- lib/rustq/meta/lower/enum.ex | 144 +++------------------------ lib/rustq/meta/lower/map.ex | 46 +-------- lib/rustq/meta/lower/map_set.ex | 20 +--- lib/rustq/meta/typing.ex | 71 +++++++++++++ test/rustq/meta/desugar_test.exs | 38 +++++++ 7 files changed, 330 insertions(+), 219 deletions(-) create mode 100644 lib/rustq/meta/desugar.ex create mode 100644 test/rustq/meta/desugar_test.exs diff --git a/lib/rustq/meta/desugar.ex b/lib/rustq/meta/desugar.ex new file mode 100644 index 0000000..30f0996 --- /dev/null +++ b/lib/rustq/meta/desugar.ex @@ -0,0 +1,166 @@ +defmodule RustQ.Meta.Desugar do + @moduledoc """ + Rewrites standard-library calls that are other Rusty-Elixir into that Rusty-Elixir, before + `RustQ.Meta.Ownership` and lowering see a body, so lowering keeps to forms Rust has: + + * `Map.new/1,2`, `MapSet.new/1,2`, and `Enum.into/2,3` into an empty collection are a + comprehension collecting into it; + * `Enum.group_by/2,3` is a map filled by a loop, and `Enum.uniq/1` and `Enum.uniq_by/2` + keep the items a set has not seen; + * a generator over `Enum.with_index(list)` enumerates the list in place. + + Where a rewrite needs what only lowering knows, it leaves an internal form for lowering to + resolve: `__rustq_items__(list)` iterates a list as its ownership allows, and + `__rustq_owned__(item, list)` is an item of `list`, copied where that costs nothing and + cloned otherwise. + """ + + @doc "Rewrites `body`'s standard-library calls that are other Rusty-Elixir." + @spec desugar(Macro.t()) :: Macro.t() + def desugar(body), do: Macro.prewalk(body, &rewrite/1) + + @doc """ + A one-argument function applied to `value`, inlined: `fn name -> name.len() end` applied to + `item` is `item.len()`, and `&key/1` applied to it is `key(item)`. + """ + @spec apply_function(Macro.t(), Macro.t()) :: Macro.t() + def apply_function({:fn, _, [{:->, _, [[{name, _, ast_context}], body]}]}, value) + when is_atom(name) and is_atom(ast_context) do + Macro.prewalk(body, fn + {^name, _, ^ast_context} -> value + node -> node + end) + end + + def apply_function({:fn, _, [{:->, _, [[pattern], body]}]}, value), + do: quote(do: case(unquote(value), do: (unquote(pattern) -> unquote(body)))) + + def apply_function({:&, _, [{:/, _, [{{:., _, _} = remote, _, _}, 1]}]}, value), + do: {remote, [], [value]} + + def apply_function({:&, _, [{:/, _, [{name, _, _}, 1]}]}, value) when is_atom(name), + do: {name, [], [value]} + + def apply_function(other, _value) do + RustQ.Diagnostic.lower( + :unsupported_enum_function, + other, + "unsupported function argument in defrust", + suggestion: "Use a one-argument anonymous function or a named one-argument capture." + ) + end + + # Names a rewrite introduces, which shadow nothing a body writes. + defp fresh(name), do: Macro.var(:"rustq_#{name}", __MODULE__) + + defp rewrite({{:., _, [{:__aliases__, _, [:Map]}, :new]}, _, [enumerable | transform]}), + do: comprehension(enumerable, transform, quote(do: HashMap.new())) + + defp rewrite({{:., _, [{:__aliases__, _, [:MapSet]}, :new]}, _, [enumerable | transform]}), + do: comprehension(enumerable, transform, quote(do: HashSet.new())) + + defp rewrite( + {{:., _, [{:__aliases__, _, [:Enum]}, :into]}, _, [enumerable, collectable | transform]} = + call + ) do + case empty_collection(collectable) do + nil -> call + into -> comprehension(enumerable, transform, into) + end + end + + # Groups keep their items in order, as Elixir's do. + defp rewrite({{:., _, [{:__aliases__, _, [:Enum]}, :group_by]}, _, [enumerable, key | value]}) do + item = fresh(:item) + groups = fresh(:groups) + value = if value == [], do: item, else: apply_function(hd(value), item) + + quote do + unquote(groups) = HashMap.new() + + for unquote(item) <- unquote(enumerable) do + unquote(groups).entry(unquote(apply_function(key, item))).or_default().push( + unquote(value) + ) + end + + unquote(groups) + end + end + + # Keeps each item whose key is new, in order, as Elixir does. + defp rewrite({{:., _, [{:__aliases__, _, [:Enum]}, function]}, _, [enumerable | key]} = call) + when function in [:uniq, :uniq_by] do + if length(key) == if(function == :uniq, do: 0, else: 1) do + item = fresh(:item) + seen = fresh(:seen) + + seen_key = + case key do + [] -> quote(do: __rustq_owned__(unquote(item), unquote(enumerable))) + [key] -> apply_function(key, item) + end + + quote do + unquote(seen) = HashSet.new() + + Enum.filter(unquote(enumerable), fn unquote(item) -> + unquote(seen).insert(unquote(seen_key)) + end) + end + else + call + end + end + + defp rewrite({:for, meta, args}), do: {:for, meta, Enum.map(args, &indexed_generator/1)} + defp rewrite(ast), do: ast + + # `{item, index} <- Enum.with_index(list)` enumerates the list, binding `{index, item}`, + # rather than collecting the pairs first. + defp indexed_generator( + {:<-, meta, [pattern, {{:., _, [{:__aliases__, _, [:Enum]}, :with_index]}, _, [list]}]} + ) do + enumerated = quote(do: __rustq_items__(unquote(list)) |> enumerate()) + + case pattern do + {item, index} -> + {:<-, meta, [{index, item}, enumerated]} + + pattern -> + {:<-, meta, + [pattern, quote(do: unquote(enumerated) |> map(fn {index, item} -> {item, index} end))]} + end + end + + defp indexed_generator(qualifier), do: qualifier + + defp comprehension(enumerable, [], into), + do: quote(do: for(item <- unquote(enumerable), into: unquote(into), do: item)) + + defp comprehension(enumerable, [{:fn, _, [{:->, _, [[pattern], body]}]}], into), + do: + quote( + do: for(unquote(pattern) <- unquote(enumerable), into: unquote(into), do: unquote(body)) + ) + + defp comprehension(enumerable, [transform], into) do + item = fresh(:item) + + quote do + for unquote(item) <- unquote(enumerable), + into: unquote(into), + do: unquote(apply_function(transform, item)) + end + end + + # The collection an empty collectable names, or nil for one with items. + defp empty_collection({:%{}, _, []}), do: quote(do: HashMap.new()) + defp empty_collection([]), do: quote(do: Vec.new()) + + defp empty_collection({{:., _, [{:__aliases__, _, [module]}, :new]}, _, []} = constructor) + when module in [:Map, :MapSet, :HashMap, :HashSet, :BTreeMap, :BTreeSet], + do: constructor + + defp empty_collection(_collectable), do: nil +end diff --git a/lib/rustq/meta/lower.ex b/lib/rustq/meta/lower.ex index 1f066bb..bb4333b 100644 --- a/lib/rustq/meta/lower.ex +++ b/lib/rustq/meta/lower.ex @@ -9,6 +9,8 @@ defmodule RustQ.Meta.Lower do alias RustQ.Diagnostic alias RustQ.Meta.Capture alias RustQ.Meta.Core.Call + alias RustQ.Meta.Desugar + alias RustQ.Meta.Lower.Collection alias RustQ.Meta.Lower.Stdlib alias RustQ.Meta.Lower.Stdlib.Context, as: StdlibContext alias RustQ.Meta.Ownership @@ -54,7 +56,7 @@ defmodule RustQ.Meta.Lower do @spec quoted_body(Macro.t(), Type.t() | nil, map(), keyword()) :: [struct()] def quoted_body(body_ast, return_type, vars \\ %{}, opts \\ []) do - {body_ast, built_from} = Ownership.annotate(body_ast) + {body_ast, built_from} = body_ast |> Desugar.desugar() |> Ownership.annotate() context = %Context{ built_from: built_from, @@ -208,7 +210,7 @@ defmodule RustQ.Meta.Lower do defp lower_statement({:for, _, args} = expression, %Context{} = context) when is_list(args) do case comprehension_parts(expression) do {qualifiers, [do: body]} -> - lower_for_loop(indexed_generators(qualifiers, context), body, context) + lower_for_loop(qualifiers, body, context) _other -> %AST.ExprStmt{expr: lower_comprehension(expression, nil, context)} @@ -569,6 +571,13 @@ defmodule RustQ.Meta.Lower do ), do: lower_expected_expr_context(expression, expected_type, context) + defp lower_expected_expr_context( + {:__block__, _, [_first, _second | _rest] = expressions}, + %Type{} = expected_type, + %Context{} = context + ), + do: %AST.BlockExpr{body: lower_expected_block(expressions, context, expected_type)} + defp lower_expected_expr_context( {tag, _value} = expression, %Type{kind: kind} = expected_type, @@ -1032,6 +1041,31 @@ defmodule RustQ.Meta.Lower do end # `match?(pattern, value)` is the `case` that answers whether the value matches. + # `__rustq_items__(list)`, which `RustQ.Meta.Desugar` leaves, iterates a list by reference + # where it is borrowed or still in use, and takes it otherwise. + defp lower_expr_context({:__rustq_items__, _, [list]}, %Context{} = context) do + method = if borrowed_list?(list, context), do: :iter, else: :into_iter + %AST.MethodCall{receiver: lower_expr(list, context), method: method, args: []} + end + + # `__rustq_owned__(item, list)` is a copy of an item of `list`, as `.clone()` makes one of an + # item that does not copy freely. + defp lower_expr_context({:__rustq_owned__, _, [item, list]}, %Context{} = context) do + inner = + case Typing.synth(list, typing_env(context)) do + %Type{} = type -> Type.vec_inner(type) || Type.slice_inner(type) + nil -> nil + end + + if Collection.copy?(inner), + do: lower_expr(item, context), + else: %AST.MethodCall{receiver: lower_expr(item, context), method: :clone, args: []} + end + + # A block of expressions is a Rust block whose value is its last expression. + defp lower_expr_context({:__block__, _, [_first, _second | _rest] = expressions}, context), + do: %AST.BlockExpr{body: lower_value_block(expressions, context)} + defp lower_expr_context({:match?, meta, [pattern, expression]}, %Context{} = context) do clauses = [{:->, meta, [[pattern], true]}, {:->, meta, [[{:_, meta, nil}], false]}] lower_expr_context({:case, meta, [expression, [do: clauses]]}, context) @@ -1930,7 +1964,6 @@ defmodule RustQ.Meta.Lower do # its closures never outlive the outer item they borrow. defp lower_comprehension(expression, expected_type, %Context{} = context) do {qualifiers, opts} = comprehension_parts(expression) - qualifiers = indexed_generators(qualifiers, context) if Keyword.has_key?(opts, :reduce) do Diagnostic.lower( @@ -2000,30 +2033,7 @@ defmodule RustQ.Meta.Lower do defp collect_target(%Type{ast: ast} = type), do: if(slice_type?(type), do: @any_vec, else: ast) defp collect_target(nil), do: @any_vec - # `{item, index} <- Enum.with_index(list)` iterates `list.iter().enumerate()`, binding - # `{index, item}`, rather than collecting the pairs first. The list is borrowed unless the - # loop is its last use. - defp indexed_generators(qualifiers, %Context{} = context) do - Enum.map(qualifiers, fn - {:<-, meta, [pattern, {{:., _, [{:__aliases__, _, [:Enum]}, :with_index]}, _, [list]}]} -> - method = - if borrowed_list?(list, context), - do: :iter, - else: :into_iter - - enumerated = quote(do: unquote(list) |> unquote(method)() |> enumerate()) - {:<-, meta, indexed_generator(pattern, enumerated)} - - qualifier -> - qualifier - end) - end - - defp indexed_generator({item, index}, enumerated), do: [{index, item}, enumerated] - - defp indexed_generator(pattern, enumerated), - do: [pattern, quote(do: unquote(enumerated) |> map(fn {index, item} -> {item, index} end))] - + # A list borrowed, or owned but still in use, is iterated by reference. defp borrowed_list?(list, context) do case Typing.synth(list, typing_env(context)) do %Type{kind: kind} when kind in [:slice, :ref, :mut_ref] -> true diff --git a/lib/rustq/meta/lower/enum.ex b/lib/rustq/meta/lower/enum.ex index 9e14468..fd5610e 100644 --- a/lib/rustq/meta/lower/enum.ex +++ b/lib/rustq/meta/lower/enum.ex @@ -7,8 +7,8 @@ defmodule RustQ.Meta.Lower.Enum do alias RustQ.Diagnostic alias RustQ.Meta.Capture alias RustQ.Meta.Core.Call + alias RustQ.Meta.Desugar alias RustQ.Meta.Lower.Collection - alias RustQ.Meta.Lower.Map, as: MapLowering alias RustQ.Meta.Lower.Stdlib alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type @@ -179,54 +179,19 @@ defmodule RustQ.Meta.Lower.Enum do end end - defp lower_call(%Call{function: :into, args: [enumerable, collectable | transform]}, context) do - case empty_collectable(collectable) do - nil when transform == [] -> - {:ok, - Collection.updated( - collectable, - fn collection -> - [ - Collection.statement(collection, :extend, [ - Collection.owned(enumerable, context) - ]) - ] - end, - context - )} - - nil -> - :unsupported - - into -> - {:ok, - MapLowering.collect(MapLowering.comprehension(enumerable, transform, into), context)} - end - end - - # Groups keep their items in order, as Elixir's do. - defp lower_call(%Call{function: :group_by, args: [enumerable, key | value]}, context) do - item = Macro.var(:rustq_item, __MODULE__) - groups = Macro.var(:rustq_groups, __MODULE__) - value = if value == [], do: item, else: apply_function(hd(value), item) - - block = - quote do - unquote(groups) = HashMap.new() - - for unquote(item) <- unquote(enumerable) do - unquote(groups).entry(unquote(apply_function(key, item))).or_default().push( - unquote(value) - ) - end - - unquote(groups) - end - - {:ok, context.lower_closure_body.(block, context.expected)} + # `Enum.into/2` into an empty collection is a comprehension; see `RustQ.Meta.Desugar`. Into one + # with items, it extends a copy of it, or it at its last use. + defp lower_call(%Call{function: :into, args: [enumerable, collectable]}, context) do + {:ok, + Collection.updated( + collectable, + fn collection -> + [Collection.statement(collection, :extend, [Collection.owned(enumerable, context)])] + end, + context + )} end - # An index counts as Rust indexes do, in `usize`. defp lower_call(%Call{function: :with_index, args: [enumerable]}, context) do {:ok, enumerable @@ -320,12 +285,6 @@ defmodule RustQ.Meta.Lower.Enum do end end - defp lower_call(%Call{function: :uniq, args: [enumerable]}, context), - do: lower_uniq(enumerable, [], context) - - defp lower_call(%Call{function: :uniq_by, args: [enumerable, key]}, context), - do: lower_uniq(enumerable, [key], context) - defp lower_call(%Call{function: :dedup, args: [enumerable]}, context) do {:ok, Collection.updated( @@ -626,17 +585,6 @@ defmodule RustQ.Meta.Lower.Enum do def synth(%Call{function: function}, %TypeContext{}) when function in [:join, :map_join], do: {:ok, string_type()} - def synth(%Call{function: :group_by, args: [collection, key | value]}, context) do - with %Type{} = inner <- collection_inner(context.type_of.(collection)), - %Type{} = key <- mapper_type(key, inner, context), - %Type{} = value <- - if(value == [], do: inner, else: mapper_type(hd(value), inner, context)) do - {:ok, Collection.map(key, Type.vec(value))} - else - _unknown -> :unsupported - end - end - def synth(%Call{}, %TypeContext{}), do: :unsupported defp lower_map(collection, {:fn, _, [{:->, _, [args, body]}]}, context) do @@ -992,71 +940,6 @@ defmodule RustQ.Meta.Lower.Enum do defp cast_i64(expression), do: %AST.Cast{expr: expression, type: %AST.TypePath{parts: [:i64]}} - # Keeps each item whose key is new, in order, as Elixir does. - defp lower_uniq(enumerable, key, context) do - item = Macro.var(:rustq_item, __MODULE__) - seen = Macro.var(:rustq_seen, __MODULE__) - - seen_key = - case key do - [] -> - if Collection.copy?(collection_inner(context.type_of.(enumerable))), - do: item, - else: quote(do: unquote(item).clone()) - - [key] -> - apply_function(key, item) - end - - block = - quote do - unquote(seen) = HashSet.new() - - Enum.filter(unquote(enumerable), fn unquote(item) -> - unquote(seen).insert(unquote(seen_key)) - end) - end - - {:ok, context.lower_closure_body.(block, context.expected || context.type_of.(enumerable))} - end - - # The comprehension target an empty collectable names, or nil for one with items. - defp empty_collectable({:%{}, _, []}), do: quote(do: HashMap.new()) - defp empty_collectable([]), do: quote(do: Vec.new()) - - defp empty_collectable({{:., _, [{:__aliases__, _, [module]}, :new]}, _, []} = constructor) - when module in [:Map, :MapSet, :HashMap, :HashSet, :BTreeMap, :BTreeSet], - do: constructor - - defp empty_collectable(_collectable), do: nil - - # A one-argument function applied to `value`, inlined. - defp apply_function({:fn, _, [{:->, _, [[{name, _, ast_context}], body]}]}, value) - when is_atom(name) and is_atom(ast_context) do - Macro.prewalk(body, fn - {^name, _, ^ast_context} -> value - node -> node - end) - end - - defp apply_function({:fn, _, [{:->, _, [[pattern], body]}]}, value), - do: quote(do: case(unquote(value), do: (unquote(pattern) -> unquote(body)))) - - defp apply_function({:&, _, [{:/, _, [{{:., _, _} = remote, _, _}, 1]}]}, value), - do: {remote, [], [value]} - - defp apply_function({:&, _, [{:/, _, [{name, _, _}, 1]}]}, value) when is_atom(name), - do: {name, [], [value]} - - defp apply_function(other, _value) do - Diagnostic.lower( - :unsupported_enum_function, - other, - "unsupported Enum function argument in defrust", - suggestion: "Use a one-argument anonymous function or a named one-argument capture." - ) - end - defp continuation(body) do Macro.prewalk(body, fn {:cont, value} -> quote(do: ok(unquote(value))) @@ -1088,7 +971,8 @@ defmodule RustQ.Meta.Lower.Enum do nil -> item end - %AST.Closure{body: body} = closure = context.lower_closure.([arg], apply_function(key, item)) + %AST.Closure{body: body} = + closure = context.lower_closure.([arg], Desugar.apply_function(key, item)) if order == :desc, do: %{ diff --git a/lib/rustq/meta/lower/map.ex b/lib/rustq/meta/lower/map.ex index 7897dc1..50e3200 100644 --- a/lib/rustq/meta/lower/map.ex +++ b/lib/rustq/meta/lower/map.ex @@ -18,8 +18,8 @@ defmodule RustQ.Meta.Lower.Map do alias RustQ.Rust.AST @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported - def lower(%Call{function: :new, args: args}, %Context{} = context), - do: lower_new(args, context) + def lower(%Call{function: :new, args: []}, %Context{} = context), + do: lower_new([], context) def lower(%Call{args: [map | _]} = call, %Context{} = context) do case Collection.keyed_of(map, context) do @@ -28,40 +28,10 @@ defmodule RustQ.Meta.Lower.Map do end end - # `Map.new/0,1,2` builds what the comprehension `for item <- enumerable, into: %{}` would. + # `Map.new/1,2` is a comprehension; see `RustQ.Meta.Desugar`. defp lower_new([], _context), do: {:ok, %AST.PathCall{path: %AST.Path{parts: [:HashMap, :new]}, args: []}} - defp lower_new([enumerable | transform], context), - do: {:ok, collect(comprehension(enumerable, transform, quote(do: HashMap.new())), context)} - - @doc "Lowers a comprehension collecting into the type the call is expected to give, if known." - @spec collect(Macro.t(), Context.t()) :: AST.expr() - def collect(comprehension, %Context{expected: %Type{} = expected} = context), - do: context.lower_expected.(comprehension, expected) - - def collect(comprehension, %Context{} = context), do: context.lower.(comprehension) - - @doc """ - The comprehension collecting `enumerable` into `into`, through the one-argument function - `transform` gives, as `Map.new/1,2`, `MapSet.new/1,2`, and `Enum.into/2` build. - """ - @spec comprehension(Macro.t(), [Macro.t()], Macro.t()) :: Macro.t() - def comprehension(enumerable, [], into), - do: quote(do: for(item <- unquote(enumerable), into: unquote(into), do: item)) - - def comprehension(enumerable, [{:fn, _, [{:->, _, [[pattern], body]}]}], into), - do: - quote( - do: for(unquote(pattern) <- unquote(enumerable), into: unquote(into), do: unquote(body)) - ) - - def comprehension(enumerable, [{:&, _, [{:/, _, [function, 1]}]}], into) do - quote do - for item <- unquote(enumerable), into: unquote(into), do: unquote(function)(item) - end - end - defp lower_keyed(%Call{function: :get, args: [map, key]}, key_type, value_type, context) do lookup = Collection.call(context.lower.(map), :get, [Collection.borrowed(key, key_type, context)]) @@ -277,16 +247,6 @@ defmodule RustQ.Meta.Lower.Map do defp lower_fields(%Call{}, %Context{}), do: :unsupported @spec synth(Call.t(), TypeContext.t()) :: {:ok, Type.t()} | :unsupported - def synth(%Call{function: :new, args: [enumerable]}, %TypeContext{} = context) do - with %Type{} = type <- context.type_of.(enumerable), - %Type{kind: :tuple, meta: %{elements: [key, value]}} <- - Type.vec_inner(type) || Type.slice_inner(type) do - {:ok, Collection.map(key, value)} - else - _unknown -> :unsupported - end - end - def synth(%Call{function: :new}, %TypeContext{}), do: :unsupported def synth(%Call{args: [map | _]} = call, %TypeContext{} = context) do diff --git a/lib/rustq/meta/lower/map_set.ex b/lib/rustq/meta/lower/map_set.ex index 1ea0d8a..aef32f7 100644 --- a/lib/rustq/meta/lower/map_set.ex +++ b/lib/rustq/meta/lower/map_set.ex @@ -1,14 +1,13 @@ defmodule RustQ.Meta.Lower.MapSet do @moduledoc """ Lowers `MapSet` functions on a `HashSet` or `BTreeSet` for `RustQ.Meta.Lower.Stdlib`, and - types such calls: `new`, `put`, `delete`, `member?`, `union`, `difference`, `intersection`, + types such calls: `new/0` (`new/1,2` are comprehensions; see `RustQ.Meta.Desugar`), `put`, `delete`, `member?`, `union`, `difference`, `intersection`, `subset?`, `disjoint?`, `size`, and `to_list`. An update takes the set only at its last use; see `RustQ.Meta.Lower.Collection`. """ alias RustQ.Meta.Core.Call alias RustQ.Meta.Lower.Collection - alias RustQ.Meta.Lower.Map, as: MapLowering alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type alias RustQ.Rust.AST @@ -17,14 +16,6 @@ defmodule RustQ.Meta.Lower.MapSet do def lower(%Call{function: :new, args: []}, %Context{}), do: {:ok, %AST.PathCall{path: %AST.Path{parts: [:HashSet, :new]}, args: []}} - def lower(%Call{function: :new, args: [enumerable | transform]}, %Context{} = context) do - {:ok, - MapLowering.collect( - MapLowering.comprehension(enumerable, transform, quote(do: HashSet.new())), - context - )} - end - def lower(%Call{args: [set | _]} = call, %Context{} = context) do case Collection.keyed_of(set, context) do {:set, item} -> lower_set(call, item, context) @@ -120,15 +111,6 @@ defmodule RustQ.Meta.Lower.MapSet do defp lower_set(%Call{}, _item, _context), do: :unsupported @spec synth(Call.t(), TypeContext.t()) :: {:ok, Type.t()} | :unsupported - def synth(%Call{function: :new, args: [enumerable]}, %TypeContext{} = context) do - with %Type{} = type <- context.type_of.(enumerable), - %Type{} = item <- Type.vec_inner(type) || Type.slice_inner(type) do - {:ok, Collection.set(item)} - else - _unknown -> :unsupported - end - end - def synth(%Call{function: :new}, %TypeContext{}), do: :unsupported def synth(%Call{function: function, args: [set | _]}, %TypeContext{} = context) do diff --git a/lib/rustq/meta/typing.ex b/lib/rustq/meta/typing.ex index f576799..7919d3f 100644 --- a/lib/rustq/meta/typing.ex +++ b/lib/rustq/meta/typing.ex @@ -9,6 +9,7 @@ defmodule RustQ.Meta.Typing do alias RustQ.Meta.Core.Call alias RustQ.Meta.Inference alias RustQ.Meta.Lower + alias RustQ.Meta.Lower.Collection alias RustQ.Meta.Lower.Stdlib alias RustQ.Meta.Lower.Stdlib.TypeContext, as: StdlibTypeContext alias RustQ.Meta.Pattern @@ -162,6 +163,22 @@ defmodule RustQ.Meta.Typing do def synth({:{}, _meta, [first | _rest] = elements}, %Env{} = env) when not is_atom(first), do: synth_tuple(elements, env) + # A comprehension collects its body's values into a list, or into the map or set `into:` + # names, as `RustQ.Meta.Desugar` writes `Map.new/1,2` and `MapSet.new/1,2`. + def synth({:for, _meta, [{:<-, _, [pattern, collection]} | rest]}, %Env{} = env) do + {_qualifiers, options} = Enum.split_while(rest, &(not Keyword.keyword?(&1) or &1 == [])) + options = Enum.concat(options) + + with %Type{} = list <- synth(collection, env), + %Type{} = item <- Type.vec_inner(list) || slice_item(list), + {:ok, body} <- Keyword.fetch(options, :do), + %Type{} = value <- synth(body, bind_pattern(pattern, item, env)) do + collected(Keyword.get(options, :into), value) + else + _unknown -> nil + end + end + def synth(call_ast, %Env{} = env) do case synth_stdlib(call_ast, env) do {:ok, %Type{} = type} -> @@ -179,6 +196,60 @@ defmodule RustQ.Meta.Typing do def synth(ast, opts) when is_list(opts), do: synth(ast, env(opts)) + defp slice_item(%Type{} = list) do + case Type.slice_inner(list) do + %Type{} = item -> Type.ref(item) + nil -> nil + end + end + + defp bind_pattern({name, _meta, context}, %Type{} = item, %Env{} = env) + when is_atom(name) and is_atom(context), + do: %{env | vars: Map.put(env.vars, name, item)} + + defp bind_pattern( + {left, right}, + %Type{kind: :tuple, meta: %{elements: [left_type, right_type]}}, + %Env{} = env + ), + do: + env + |> then(&bind_pattern(left, left_type, &1)) + |> then(&bind_pattern(right, right_type, &1)) + + defp bind_pattern(_pattern, _item, %Env{} = env), do: env + + defp collected(into, %Type{} = value) do + case {Collection.keyed(constructed(into)), value} do + {{:map, _, _}, %Type{kind: :tuple, meta: %{elements: [key, item]}}} -> + Collection.map(key, item) + + {{:set, _}, value} -> + Collection.set(value) + + {nil, value} -> + Type.vec(value) + + _other -> + nil + end + end + + # The collection type an `into:` constructor names, with types to be filled in. + defp constructed({:%{}, _, []}), do: Collection.map(unknown(), unknown()) + + defp constructed({{:., _, [{:__aliases__, _, [module]}, :new]}, _, []}) + when module in [:Map, :HashMap, :BTreeMap], + do: Collection.map(unknown(), unknown()) + + defp constructed({{:., _, [{:__aliases__, _, [module]}, :new]}, _, []}) + when module in [:MapSet, :HashSet, :BTreeSet], + do: Collection.set(unknown()) + + defp constructed(_into), do: nil + + defp unknown, do: %Type{kind: :type, rust: "_", ast: %AST.TypeRaw{source: "_"}, meta: %{}} + defp indexed_type(%Type{} = type), do: Type.vec_inner(type) || Type.slice_inner(type) defp synth_stdlib({name, _meta, args} = call_ast, %Env{} = env) diff --git a/test/rustq/meta/desugar_test.exs b/test/rustq/meta/desugar_test.exs new file mode 100644 index 0000000..f4d9274 --- /dev/null +++ b/test/rustq/meta/desugar_test.exs @@ -0,0 +1,38 @@ +defmodule RustQ.Meta.DesugarTest do + use ExUnit.Case, async: true + + alias RustQ.Meta.Desugar + + defp desugared(ast), do: ast |> Desugar.desugar() |> Macro.to_string() + + test "Map.new, MapSet.new, and Enum.into an empty collection are comprehensions" do + assert desugared(quote(do: Map.new(pairs))) == + "for item <- pairs, into: HashMap.new(), do: item" + + assert desugared(quote(do: MapSet.new(names, fn name -> name.len() end))) == + "for name <- names, into: HashSet.new(), do: name.len()" + + assert desugared(quote(do: Enum.into(names, %{}, &pair/1))) == + "for rustq_item <- names, into: HashMap.new(), do: pair(rustq_item)" + + assert desugared(quote(do: Enum.into(names, scores))) == "Enum.into(names, scores)" + end + + test "group_by fills a map in a loop, keeping each group's order" do + assert desugared(quote(do: Enum.group_by(names, fn name -> name.len() end))) =~ + "rustq_groups.entry(rustq_item.len()).or_default().push(rustq_item)" + end + + test "uniq keeps the items a set has not seen, copied or cloned as lowering decides" do + assert desugared(quote(do: Enum.uniq(names))) =~ + "rustq_seen.insert(__rustq_owned__(rustq_item, names))" + + assert desugared(quote(do: Enum.uniq_by(names, &key/1))) =~ + "rustq_seen.insert(key(rustq_item))" + end + + test "a generator over Enum.with_index enumerates the list in place" do + assert desugared(quote(do: for({name, index} <- Enum.with_index(names), do: f(name, index)))) == + "for {index, name} <- __rustq_items__(names) |> enumerate() do\n f(name, index)\nend" + end +end From eec7275ea6e365698c536d7e176e6b5ed3c4e80a Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 25 Oct 2026 23:37:38 +0400 Subject: [PATCH 05/10] Split pattern, comprehension, path, and mutability lowering out of Lower --- lib/rustq/corpus.ex | 45 +- lib/rustq/meta/ast.ex | 7 +- lib/rustq/meta/lower.ex | 1892 ++--------------- lib/rustq/meta/lower/comprehension.ex | 468 ++++ lib/rustq/meta/lower/mutability.ex | 213 ++ lib/rustq/meta/lower/paths.ex | 355 ++++ lib/rustq/meta/lower/pattern.ex | 705 ++++++ lib/rustq/meta/typing.ex | 6 +- .../meta/lower/call_propagation_test.exs | 12 +- 9 files changed, 1961 insertions(+), 1742 deletions(-) create mode 100644 lib/rustq/meta/lower/comprehension.ex create mode 100644 lib/rustq/meta/lower/mutability.ex create mode 100644 lib/rustq/meta/lower/paths.ex create mode 100644 lib/rustq/meta/lower/pattern.ex diff --git a/lib/rustq/corpus.ex b/lib/rustq/corpus.ex index b8a83ed..8d603cf 100644 --- a/lib/rustq/corpus.ex +++ b/lib/rustq/corpus.ex @@ -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 @@ -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 diff --git a/lib/rustq/meta/ast.ex b/lib/rustq/meta/ast.ex index 046dff9..b2f39f4 100644 --- a/lib/rustq/meta/ast.ex +++ b/lib/rustq/meta/ast.ex @@ -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 @@ -601,7 +602,7 @@ 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)} @@ -619,7 +620,7 @@ defmodule RustQ.Meta.AST do # 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 = Lower.mutated_variables(body) + 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), @@ -674,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)) diff --git a/lib/rustq/meta/lower.ex b/lib/rustq/meta/lower.ex index bb4333b..9390030 100644 --- a/lib/rustq/meta/lower.ex +++ b/lib/rustq/meta/lower.ex @@ -1,8 +1,16 @@ defmodule RustQ.Meta.Lower do @moduledoc """ - Lowers Rusty-Elixir to RustQ AST: expressions, patterns, control flow, comprehensions, and - closures, with the borrows, `?` propagation, and wrapping their expected types call for. - Calls into Elixir's standard library go through `RustQ.Meta.Lower.Stdlib`. + Lowers Rusty-Elixir to RustQ AST: expressions, statements, control flow, calls, and closures, + with the borrows, `?` propagation, and wrapping their expected types call for. + + A body is first desugared by `RustQ.Meta.Desugar` and annotated by `RustQ.Meta.Ownership`. + Lowering then hands parts to the modules beside it, which call back into `lower_expr/2`: + + * `RustQ.Meta.Lower.Pattern` lowers patterns and binds their variables; + * `RustQ.Meta.Lower.Comprehension` lowers comprehensions and `for` loops; + * `RustQ.Meta.Lower.Paths` resolves aliases to Rust paths and calls to callables; + * `RustQ.Meta.Lower.Mutability` marks what lowered Rust changes `mut`; + * `RustQ.Meta.Lower.Stdlib` lowers calls into Elixir's standard library. """ alias RustQ.Binding.Index, as: BindingIndex @@ -11,19 +19,20 @@ defmodule RustQ.Meta.Lower do alias RustQ.Meta.Core.Call alias RustQ.Meta.Desugar alias RustQ.Meta.Lower.Collection + alias RustQ.Meta.Lower.Comprehension + alias RustQ.Meta.Lower.Mutability + alias RustQ.Meta.Lower.Paths + alias RustQ.Meta.Lower.Pattern, as: LowerPattern alias RustQ.Meta.Lower.Stdlib alias RustQ.Meta.Lower.Stdlib.Context, as: StdlibContext alias RustQ.Meta.Ownership - alias RustQ.Meta.Pattern alias RustQ.Meta.RustMacro alias RustQ.Meta.Semantics - alias RustQ.Meta.StandardPointer alias RustQ.Meta.StdMethods alias RustQ.Meta.Type alias RustQ.Meta.Typing alias RustQ.Rust alias RustQ.Rust.AST - alias RustQ.Rust.AST.Walk alias RustQ.Rust.Identifier # A vector whose item type Rust infers, for collecting where none is known. @@ -73,7 +82,7 @@ defmodule RustQ.Meta.Lower do body_ast |> block_expressions() |> lower_block(context) - |> infer_mutability() + |> Mutability.infer_mutability() end @spec function_body(Macro.t(), Type.t(), map(), keyword()) :: String.t() @@ -83,23 +92,6 @@ defmodule RustQ.Meta.Lower do |> Enum.map_join("\n", &Rust.render/1) end - @doc """ - Looks up the known return type for a local or remote call AST. - - This is a lowering-time query over the callable metadata supplied through the - `:callables` option. It is intentionally side-effect-free and does not alter - lowering yet; type-driven propagation inference will use this lookup to decide - when a call returning `Result`/`Option`/`NifResult` should lower with Rust `?`. - """ - @spec callable_return_type(Macro.t(), keyword()) :: Type.t() | nil - def callable_return_type(call_ast, opts \\ []) do - callables = opts |> Keyword.get(:callables, []) |> BindingIndex.new() - rust_modules = Keyword.get(opts, :rust_modules, %{}) - vars = Keyword.get(opts, :vars, %{}) - - callable_return_type_from_index(call_ast, callables, rust_modules, vars) - end - defp lower_block(expressions, %Context{} = context) do context = context_with_downstream_let_types(expressions, context) @@ -132,9 +124,7 @@ defmodule RustQ.Meta.Lower do @keyed_collections [:HashMap, :HashSet, :BTreeMap, :BTreeSet] @stdlib_modules [:Kernel, :Enum, :List, :Map, :MapSet, :String, :Tuple, :Range] - @iterator_methods StdMethods.iterator() @iterator_option_methods StdMethods.iterator_option() - @mutating_methods StdMethods.mutating() # 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. @@ -164,17 +154,17 @@ defmodule RustQ.Meta.Lower do defp lower_statement({:=, _, [pattern, expression]}, %Context{} = context) do expected_type = infer_let_expected_type(pattern, expression, context) - expression_context = context_with_lexical_pattern_vars(pattern, context) + expression_context = LowerPattern.context_with_lexical_pattern_vars(pattern, context) %AST.Let{ - pattern: lower_binding_pattern(pattern), + pattern: LowerPattern.lower_binding_pattern(pattern), expr: expression |> lower_expr(expected_type, expression_context) - |> collect_as(collect_target(expected_type)), + |> Comprehension.collect_as(Comprehension.collect_target(expected_type)), type: empty_collection_type(expression, expected_type) } - |> updated_in_place() + |> Mutability.updated_in_place() end defp lower_statement({:case, _, [expression, [do: clauses]]}, %Context{} = context) do @@ -208,12 +198,12 @@ defmodule RustQ.Meta.Lower do end defp lower_statement({:for, _, args} = expression, %Context{} = context) when is_list(args) do - case comprehension_parts(expression) do + case Comprehension.comprehension_parts(expression) do {qualifiers, [do: body]} -> - lower_for_loop(qualifiers, body, context) + Comprehension.lower_for_loop(qualifiers, body, context) _other -> - %AST.ExprStmt{expr: lower_comprehension(expression, nil, context)} + %AST.ExprStmt{expr: Comprehension.lower_comprehension(expression, nil, context)} end end @@ -228,49 +218,6 @@ defmodule RustQ.Meta.Lower do %AST.ExprStmt{expr: lower_statement_expr(expression, context)} end - # `map = Map.put(map, key, value)` updates the map it rebinds, which nothing uses again, so it - # changes that map in place: `map.insert(key, value);`. - defp updated_in_place( - %AST.Let{ - pattern: %AST.PatVar{name: name}, - expr: %AST.BlockExpr{ - body: [ - %AST.Let{pattern: %AST.PatVar{name: :rustq_updated}, expr: %AST.Var{name: name}} - | rest - ] - } - } = let - ) do - case Enum.split(rest, -1) do - {[_ | _] = updates, [%AST.Return{expr: %AST.Var{name: :rustq_updated}}]} -> - case rename_var(updates, :rustq_updated, name) do - [update] -> update - updates -> %AST.ExprStmt{expr: %AST.BlockExpr{body: updates}} - end - - _other -> - let - end - end - - defp updated_in_place(statement), do: statement - - defp rename_var(%AST.Var{name: from}, from, to), do: %AST.Var{name: to} - - defp rename_var(%module{} = node, from, to) do - fields = - node - |> Map.from_struct() - |> Map.new(fn {key, value} -> {key, rename_var(value, from, to)} end) - - struct(module, fields) - end - - defp rename_var(values, from, to) when is_list(values), - do: Enum.map(values, &rename_var(&1, from, to)) - - defp rename_var(value, _from, _to), do: value - defp cond_statement([{:->, _, [[true], body]} | _rest]), do: body defp cond_statement([{:->, meta, [[condition], body]} | rest]) do @@ -340,13 +287,14 @@ defmodule RustQ.Meta.Lower do expression, %Context{} = context ) do - %AST.Return{expr: lower_for_reduce_expr(expression, context)} + %AST.Return{expr: Comprehension.lower_for_reduce_expr(expression, context)} end defp lower_return(expression, %Context{return_type: return_type} = context), do: %AST.Return{expr: lower_return_expr(expression, return_type, context)} - defp lower_return_expr(expression, return_type, %Context{} = context), + @doc "Lowers an expression in return position." + def lower_return_expr(expression, return_type, %Context{} = context), do: lower_return_expr_context(expression, return_type, context) defp lower_return_expr_context(:ok, %Type{kind: kind} = return_type, %Context{} = context) @@ -543,9 +491,10 @@ defmodule RustQ.Meta.Lower do defp option_chain?(_expression, _context), do: false - defp lower_expr(expression, %Context{} = context), do: lower_expr_context(expression, context) + @doc "Lowers an expression, as the `RustQ.Meta.Lower.*` modules lowering its parts call back." + def lower_expr(expression, %Context{} = context), do: lower_expr_context(expression, context) - defp lower_expr(expression, expected_type, %Context{} = context), + def lower_expr(expression, expected_type, %Context{} = context), do: lower_expected_expr_context(expression, declared_type(expected_type, context), context) # A type a recursive declaration names inside itself, resolved to the whole declaration. @@ -644,7 +593,7 @@ defmodule RustQ.Meta.Lower do %Type{} = expected_type, %Context{} = context ) do - lower_for_reduce_expr(expression, expected_type, context) + Comprehension.lower_for_reduce_expr(expression, expected_type, context) end defp lower_expected_expr_context( @@ -653,7 +602,7 @@ defmodule RustQ.Meta.Lower do %Context{} = context ) when is_list(args), - do: lower_comprehension(expression, expected_type, context) + do: Comprehension.lower_comprehension(expression, expected_type, context) defp lower_expected_expr_context( {:struct_literal, _, [path, fields]}, @@ -929,10 +878,10 @@ defmodule RustQ.Meta.Lower do ) do cond do slice_type?(Type.expected_value(expected_type)) -> - %AST.Ref{expr: expression |> lower_expr(context) |> collect_as(@any_vec)} + %AST.Ref{expr: expression |> lower_expr(context) |> Comprehension.collect_as(@any_vec)} expected_type.kind in [:option, :result] -> - expression |> lower_expr(context) |> collect_as(expected_type.ast) + expression |> lower_expr(context) |> Comprehension.collect_as(expected_type.ast) true -> lower_checked_expr(expression, expected_type, context) @@ -954,7 +903,7 @@ defmodule RustQ.Meta.Lower do closure_context = context |> then(&closure_context([arg], &1)) - |> then(&context_with_match_pattern(arg, Type.inner(receiver_type), &1)) + |> then(&LowerPattern.context_with_match_pattern(arg, Type.inner(receiver_type), &1)) |> Map.merge(%{ return_type: closure_return, scope_return_type: closure_return, @@ -1007,7 +956,8 @@ defmodule RustQ.Meta.Lower do end end - defp collection_item_type(type) do + @doc "The item type of a list a comprehension builds." + def collection_item_type(type) do Type.vec_inner(Type.expected_value(type)) || Type.slice_inner(type) end @@ -1162,7 +1112,7 @@ defmodule RustQ.Meta.Lower do do: lower_expr(expression, context) defp lower_expr_context({:pat!, _, [pattern]}, %Context{} = context), - do: lower_semantic_pat(pattern, context) + do: LowerPattern.lower_semantic_pat(pattern, context) defp lower_expr_context({:stmt!, _, [expression]}, %Context{} = context), do: %AST.ExprStmt{expr: lower_expr(expression, context)} @@ -1174,7 +1124,7 @@ defmodule RustQ.Meta.Lower do defp lower_expr_context({:arm!, _, [pattern, block]}, %Context{} = context), do: %AST.Arm{ - pattern: lower_semantic_pat(pattern, context), + pattern: LowerPattern.lower_semantic_pat(pattern, context), body: lower_semantic_arm_body(block, context) } @@ -1314,11 +1264,11 @@ defmodule RustQ.Meta.Lower do expression, %Context{} = context ), - do: lower_for_reduce_expr(expression, context) + do: Comprehension.lower_for_reduce_expr(expression, context) defp lower_expr_context({:for, _, args} = expression, %Context{} = context) when is_list(args), - do: lower_comprehension(expression, nil, context) + do: Comprehension.lower_comprehension(expression, nil, context) defp lower_expr_context( {{:., _meta, [receiver, field_or_function]}, call_meta, []}, @@ -1334,7 +1284,7 @@ defmodule RustQ.Meta.Lower do defp lower_expr_context({:__rustq_path__, _, parts}, %Context{}), do: %AST.Path{parts: parts} defp lower_expr_context({:__aliases__, _, parts}, %Context{} = context), - do: %AST.Path{parts: mapped_alias_parts(parts, context.rust_modules)} + do: %AST.Path{parts: Paths.mapped_alias_parts(parts, context.rust_modules)} defp lower_expr_context({:{}, _, values}, %Context{} = context), do: %AST.Tuple{values: Enum.map(values, &lower_expr(&1, context))} @@ -1380,7 +1330,7 @@ defmodule RustQ.Meta.Lower do expression, %Context{return_type: %Type{}} = context ) do - %AST.Try{expr: lower_for_reduce_expr(expression, context)} + %AST.Try{expr: Comprehension.lower_for_reduce_expr(expression, context)} end defp lower_statement_expr(expression, %Context{return_type: %Type{} = return_type} = context) do @@ -1443,7 +1393,7 @@ defmodule RustQ.Meta.Lower do end defp same_wrapper_propagation?(expression, %Type{} = return_type, %Context{} = context) do - case callable_return_type(expression, + case Paths.callable_return_type(expression, callables: context.callables, rust_modules: context.rust_modules, vars: context.vars @@ -1553,16 +1503,18 @@ defmodule RustQ.Meta.Lower do arms = Enum.map(clauses, fn {:->, _, [[pattern], body]} -> - {pattern, guard} = split_guarded_pattern(pattern) - body_context = context_with_match_pattern(pattern, case_type, context) + {pattern, guard} = LowerPattern.split_guarded_pattern(pattern) + body_context = LowerPattern.context_with_match_pattern(pattern, case_type, context) body = lower_clause_body(body, body_context, expected_type) - mutable_vars = body |> collect_pattern_mutations(body_context) |> MapSet.new() + + mutable_vars = + body |> LowerPattern.collect_pattern_mutations(body_context) |> MapSet.new() %AST.Arm{ pattern: pattern - |> lower_match_pattern(case_type, context) - |> mark_mutable_pattern_vars(mutable_vars), + |> LowerPattern.lower_match_pattern(case_type, context) + |> Mutability.mark_mutable_pattern_vars(mutable_vars), guard: lower_guard_expr(guard, body_context), body: body } @@ -1596,32 +1548,19 @@ defmodule RustQ.Meta.Lower do end defp case_scrutinee(expression, _expression_type, clauses, %Context{} = context) do - {infer_case_type_from_patterns(clauses), lower_expr(expression, context)} + {LowerPattern.infer_case_type_from_patterns(clauses), lower_expr(expression, context)} end defp list_case_clause?({:->, _, [[pattern], _body]}) do - {pattern, _guard} = split_guarded_pattern(pattern) - list_pattern?(pattern) + {pattern, _guard} = LowerPattern.split_guarded_pattern(pattern) + LowerPattern.list_pattern?(pattern) end defp propagate_case_scrutinee?(%Type{} = expression_type, clauses) do - Type.propagates?(expression_type) and not wrapper_case_patterns?(expression_type, clauses) - end - - defp wrapper_case_patterns?(%Type{kind: kind}, clauses) when kind in [:result, :nif_result] do - Enum.any?(clauses, fn {:->, _, [[pattern], _body]} -> - pattern |> split_guarded_pattern() |> elem(0) |> result_pattern?() - end) - end - - defp wrapper_case_patterns?(%Type{kind: :option}, clauses) do - Enum.any?(clauses, fn {:->, _, [[pattern], _body]} -> - pattern |> split_guarded_pattern() |> elem(0) |> option_pattern?() - end) + Type.propagates?(expression_type) and + not LowerPattern.wrapper_case_patterns?(expression_type, clauses) end - defp wrapper_case_patterns?(%Type{}, _clauses), do: false - defp normalize_case_clauses({:__block__, _meta, clauses}), do: clauses defp normalize_case_clauses(clauses), do: clauses @@ -1713,7 +1652,7 @@ defmodule RustQ.Meta.Lower do with_value = :__rustq_with_value match_type = - callable_return_type(expression, + Paths.callable_return_type(expression, callables: context.callables, rust_modules: context.rust_modules, vars: context.vars @@ -1721,13 +1660,13 @@ defmodule RustQ.Meta.Lower do Typing.synth(expression, typing_env(context)) match_type = declared_case_type(match_type, context) - body_context = context_with_match_pattern(pattern, match_type, context) + body_context = LowerPattern.context_with_match_pattern(pattern, match_type, context) %AST.Match{ expr: lower_expr(expression, context), arms: [ %AST.Arm{ - pattern: lower_with_pattern(pattern, match_type, context), + pattern: LowerPattern.lower_with_pattern(pattern, match_type, context), body: [ %AST.Return{ expr: lower_with_matches(rest, body, else_clauses, body_context, expected_type) @@ -1739,14 +1678,6 @@ defmodule RustQ.Meta.Lower do } end - # A `with` pattern matches what its expression gives; a bare variable binds all of it. - defp lower_with_pattern({name, _, ast_context} = pattern, _type, %Context{} = context) - when is_atom(name) and is_atom(ast_context), - do: lower_match_pattern(pattern, nil, context) - - defp lower_with_pattern(pattern, type, %Context{} = context), - do: lower_match_pattern(pattern, type, context) - # Without `else`, an unmatched result or option passes its error or `None` on, as in Elixir. defp with_fallback_arm(_value_name, {:ok, _value}, [], _context, _expected_type), do: @@ -1791,7 +1722,7 @@ defmodule RustQ.Meta.Lower do arms: Enum.map(else_clauses, fn {:->, _, [[pattern], body]} -> %AST.Arm{ - pattern: lower_match_pattern(pattern, nil, context), + pattern: LowerPattern.lower_match_pattern(pattern, nil, context), body: lower_clause_body(body, %{context | position: :return}, expected_type) } end) @@ -1856,15 +1787,17 @@ defmodule RustQ.Meta.Lower do {case_type, match_expr} = case_scrutinee(expression, expression_type, clauses, context) case_type = declared_case_type(case_type, context) - then_context = context_with_match_pattern(pattern, case_type, context) + then_context = LowerPattern.context_with_match_pattern(pattern, case_type, context) then_body = lower_clause_body(body, then_context) - mutable_vars = then_body |> collect_pattern_mutations(then_context) |> MapSet.new() + + mutable_vars = + then_body |> LowerPattern.collect_pattern_mutations(then_context) |> MapSet.new() %AST.IfLet{ pattern: pattern - |> lower_match_pattern(case_type, context) - |> mark_mutable_pattern_vars(mutable_vars), + |> LowerPattern.lower_match_pattern(case_type, context) + |> Mutability.mark_mutable_pattern_vars(mutable_vars), expr: match_expr, then: then_body } @@ -1880,15 +1813,15 @@ defmodule RustQ.Meta.Lower do ) do if unit_body?(fallback) do type = Typing.synth(expression, typing_env(context)) - context = context_with_match_pattern(pattern, type, context) + context = LowerPattern.context_with_match_pattern(pattern, type, context) body = lower_clause_body(body, context) - mutable = body |> collect_pattern_mutations(context) |> MapSet.new() + mutable = body |> LowerPattern.collect_pattern_mutations(context) |> MapSet.new() %AST.IfLet{ pattern: pattern - |> lower_match_pattern(%Type{kind: :result}, context) - |> mark_mutable_pattern_vars(mutable), + |> LowerPattern.lower_match_pattern(%Type{kind: :result}, context) + |> Mutability.mark_mutable_pattern_vars(mutable), expr: lower_expr(expression, context), then: body } @@ -1902,15 +1835,15 @@ defmodule RustQ.Meta.Lower do true <- unit_body?(none_body) do expression_type = Typing.synth(expression, typing_env(context)) || %Type{kind: :option} option_type = propagated_option_type(expression_type) || expression_type - then_context = context_with_match_pattern(some_pattern, option_type, context) + then_context = LowerPattern.context_with_match_pattern(some_pattern, option_type, context) then_body = lower_clause_body(some_body, then_context) - mutable_vars = then_body |> collect_mut_refs() |> MapSet.new() + mutable_vars = then_body |> LowerPattern.collect_mut_refs() |> MapSet.new() %AST.IfLet{ pattern: some_pattern - |> lower_match_pattern(%Type{kind: :option}, context) - |> mark_mutable_pattern_vars(mutable_vars), + |> LowerPattern.lower_match_pattern(%Type{kind: :option}, context) + |> Mutability.mark_mutable_pattern_vars(mutable_vars), expr: lower_option_case_scrutinee(expression, expression_type, option_type, context), then: then_body } @@ -1958,81 +1891,6 @@ defmodule RustQ.Meta.Lower do |> Enum.all?(&(&1 in [:ok, nil])) end - # A comprehension with any generators, filters, and refutable patterns, lowered through an - # iterator pipeline: each generator filters and binds in one `filter_map`, its filters as the - # match guard; later generators nest through `flat_map`, collecting each inner pipeline so - # its closures never outlive the outer item they borrow. - defp lower_comprehension(expression, expected_type, %Context{} = context) do - {qualifiers, opts} = comprehension_parts(expression) - - if Keyword.has_key?(opts, :reduce) do - Diagnostic.lower( - :unsupported_for_reduce, - expression, - "unsupported defrust for/reduce expression", - suggestion: "Use `for pattern <- enumerable, reduce: initial do acc_pattern -> body end`." - ) - end - - body = - typed_body(Keyword.fetch!(opts, :do), expected_type && collection_item_type(expected_type)) - - pipeline = - quote( - do: - unquote(comprehension_pipeline(qualifiers, body, context, {false, []})) - |> collect() - ) - - case expected_type do - %Type{} = type -> - lowered = lower_expr(pipeline, type, context) - - cond do - slice_type?(type) -> collect_as(lowered, @any_vec) - context.position == :statement -> collect_as(lowered, type.ast) - true -> lowered - end - - nil -> - pipeline |> lower_expr(context) |> collect_as(into_target(opts[:into])) - end - end - - # Without an expected type, `into:` names the collection: `HashMap.new()`, `%{}`, - # `Map.new()`, `HashSet.new()`, or `MapSet.new()`; otherwise a list, as in Elixir. - defp into_target({:%{}, _, []}), do: inferred_collection(:HashMap, 2) - - defp into_target({{:., _, [{:__aliases__, _, [module]}, :new]}, _, []}) - when module in [:Map, :HashMap, :BTreeMap], - do: inferred_collection(if(module == :BTreeMap, do: :BTreeMap, else: :HashMap), 2) - - defp into_target({{:., _, [{:__aliases__, _, [module]}, :new]}, _, []}) - when module in [:MapSet, :HashSet, :BTreeSet], - do: inferred_collection(if(module == :BTreeSet, do: :BTreeSet, else: :HashSet), 1) - - defp into_target(_into), do: @any_vec - - defp inferred_collection(collection, arity) do - %AST.TypePath{ - parts: [collection], - generics: List.duplicate(%AST.TypeRaw{source: "_"}, arity) - } - end - - # A comprehension bound in a statement states what it collects into, since Rust may only - # learn it from a later borrow; without a collection, it makes a list, as in Elixir. - defp collect_as(lowered, nil), do: lowered - - defp collect_as(%AST.MethodCall{method: :collect, generics: []} = call, type), - do: %{call | generics: [type]} - - defp collect_as(lowered, _type), do: lowered - - # Without a known type, a binding collects a list, as Elixir's enumerables do. - defp collect_target(%Type{ast: ast} = type), do: if(slice_type?(type), do: @any_vec, else: ast) - defp collect_target(nil), do: @any_vec - # A list borrowed, or owned but still in use, is iterated by reference. defp borrowed_list?(list, context) do case Typing.synth(list, typing_env(context)) do @@ -2041,203 +1899,12 @@ defmodule RustQ.Meta.Lower do end end - defp comprehension_parts({:for, _, args}) do - {opts, qualifiers} = args |> Enum.reverse() |> Enum.split_while(&Keyword.keyword?/1) - {Enum.reverse(qualifiers), opts |> Enum.reverse() |> Enum.concat()} - end - - # `nested?` says whether earlier generators enclose this one, and `generated` names the - # variables they bound. - defp comprehension_pipeline( - [{:<-, _, [pattern, collection]} | rest], - body, - context, - {nested?, generated} - ) do - {filters, rest} = Enum.split_while(rest, &(not generator?(&1))) - item_type = comprehension_item_type(collection, context) - root = root_variable(collection) - - reused? = - root != nil and - ((nested? and root not in generated) or - (item_type != nil and root not in generated and - in_use?(collection, context))) - - {stage, binder} = generator_stage(pattern, collection, filters, item_type, reused?) - bound = context_with_match_pattern(pattern, item_type, context) - - generated = binding_pattern_names(pattern) ++ generated - - binder_type = binder_type(binder, bound) - - case rest do - [] -> - comprehension_map(stage, binder, binder_type, body) - - # A generator over one value, as `{:some, id} <- [record_id(record)]`, matches that value - # beside the item instead of iterating a list made for it. - [{:<-, meta, [value_pattern, [value]]} | later] -> - paired = - quote( - do: - unquote(stage) - |> map(fn unquote(typed_arg(binder, binder_type)) -> - {unquote(binder), unquote(value)} - end) - ) - - comprehension_pipeline( - [{:<-, meta, [{binder, value_pattern}, paired]} | later], - body, - bound, - {nested?, generated} - ) - - _ -> - quote( - do: - unquote(stage) - |> flat_map(fn unquote(typed_arg(binder, binder_type)) -> - Vec.from_iter(unquote(comprehension_pipeline(rest, body, bound, {true, generated}))) - end) - ) - end - end - - defp comprehension_pipeline(qualifiers, _body, _context, _generated) do - Diagnostic.lower( - :unsupported_comprehension, - qualifiers, - "unsupported defrust comprehension", - suggestion: "Start the comprehension with a `pattern <- enumerable` generator." - ) - end - - # A comprehension body lowers as the item type of the collection it builds, when known. - defp typed_body(body, %Type{} = item_type), do: {:__rustq_typed__, [], [body, item_type]} - defp typed_body(body, _item_type), do: body - - defp untyped({:__rustq_typed__, _, [body, _type]}), do: body - defp untyped(body), do: body - - # A generator over a list gives its closures the list's items, which they may then use as - # that type; other collections leave their items untyped. - defp comprehension_item_type(collection, %Context{} = context) do - unless iterator_call?(collection) do - case infer_expr_type(collection, context.vars) || - Typing.synth(collection, typing_env(context)) do - %Type{kind: :vec} = type -> Type.vec_inner(type) - _unknown -> nil - end - end - end - - # The type of the variables a generator binds, as one variable or a tuple of them. - defp binder_type({name, _, ast_context}, %Context{vars: vars}) when is_atom(ast_context), - do: Map.get(vars, name) - - defp binder_type(binder, %Context{} = context) do - with [_ | _] = elements <- Pattern.tuple_elements(binder), - types = Enum.map(elements, &binder_type(&1, context)), - true <- Enum.all?(types, &match?(%Type{}, &1)) do - Type.tuple(types) - else - _unknown -> nil - end - end - - defp typed_arg(arg, %Type{} = type), do: {:__rustq_typed_arg__, [], [arg, type]} - defp typed_arg(arg, nil), do: arg - - # An item the body returns as it is needs no mapping, and one it clones is `cloned()`. - defp comprehension_map(stage, binder, binder_type, body) do - plain = untyped(body) - - cond do - same_variables?(binder, plain) -> - stage - - match?({{:., _, [_, :clone]}, _, []}, plain) and - same_variables?(binder, plain |> elem(0) |> elem(2) |> hd()) -> - quote(do: unquote(stage) |> cloned()) - - true -> - binder = binder |> unused_as_underscored(body) |> typed_arg(binder_type) - quote(do: unquote(stage) |> map(fn unquote(binder) -> unquote(body) end)) - end - end - - # A binder variable the body does not use is ignored. - defp unused_as_underscored(binder, body) do - {_body, used} = - Macro.prewalk(body, MapSet.new(), fn - {name, _, context} = node, used when is_atom(name) and is_atom(context) -> - {node, MapSet.put(used, name)} - - node, used -> - {node, used} - end) - - Macro.prewalk(binder, fn - {name, meta, context} = node when is_atom(name) and is_atom(context) -> - if MapSet.member?(used, name), do: node, else: {:_, meta, context} - - node -> - node - end) - end - - defp same_variables?({name, _, context}, {name, _, context}) when is_atom(context), do: true - - defp same_variables?(left, right) do - case {Pattern.tuple_elements(left), Pattern.tuple_elements(right)} do - {lefts, rights} - when is_list(lefts) and is_list(rights) and length(lefts) == length(rights) -> - lefts |> Enum.zip_with(rights, &same_variables?/2) |> Enum.all?() - - _different -> - false - end - end - - defp generator?({:<-, _, [_pattern, _collection]}), do: true - defp generator?(_qualifier), do: false - - # A generator that cannot fail and has no filters iterates as it is; otherwise its items are - # matched, filtered, and reduced to the variables the pattern binds. - # A later generator over a collection from outside the comprehension iterates it once per - # item of the earlier ones, so it copies its items rather than consuming it, as does a - # generator over a collection still in use or reached through a reference. - defp generator_stage(pattern, collection, filters, item_type, reused?) do - items = - cond do - iterator_call?(collection) -> collection - reused? -> quote(do: unquote(collection) |> iter() |> cloned()) - true -> quote(do: unquote(collection) |> into_iter()) - end - - if filters == [] and binding_pattern?(pattern) do - {items, pattern} - else - binder = pattern_binder(pattern) - {filter_bind(items, guarded_pattern(pattern, filters), binder, item_type), binder} - end - end - defp behind_reference?({name, _, ast_context}, name, _context) when is_atom(ast_context), do: false defp behind_reference?(_field, name, %Context{vars: vars}), do: match?(%Type{kind: kind} when kind in [:ref, :mut_ref], Map.get(vars, name)) - defp root_variable({name, _, ast_context}) when is_atom(name) and is_atom(ast_context), do: name - # The variable a collection is, or is a field of; a call or literal makes a fresh one. - defp root_variable({{:., _, [receiver, _field]}, meta, []}), - do: if(meta[:no_parens], do: root_variable(receiver)) - - defp root_variable(_collection), do: nil - defp root_variable_node({name, _, ast_context} = variable) when is_atom(name) and is_atom(ast_context), do: variable @@ -2247,214 +1914,14 @@ defmodule RustQ.Meta.Lower do defp root_variable_node(_collection), do: nil - # A collection already given as an iterator, as in `values.iter()`, is iterated as it is. - defp iterator_call?({{:., _, [_receiver, method]}, _, args}) when is_list(args), - do: method in @iterator_methods - - defp iterator_call?({:|>, _, [_left, {method, _, args}]}) when is_list(args), - do: method in @iterator_methods - - defp iterator_call?(_collection), do: false - - defp filter_bind(items, guarded, binder, item_type) do - item = quote(do: item) - - quote do - unquote(items) - |> filter_map(fn unquote(typed_arg(item, item_type)) -> - case unquote(item) do - unquote(guarded) -> some(unquote(binder)) - _ -> nil - end - end) - end - end - - defp guarded_pattern(pattern, []), do: pattern - - defp guarded_pattern(pattern, filters) do - guard = - Enum.reduce(filters, fn filter, guard -> quote(do: unquote(guard) and unquote(filter)) end) - - quote(do: unquote(pattern) when unquote(guard)) - end - - defp binding_pattern?({name, _meta, context}) when is_atom(name) and is_atom(context), do: true - - defp binding_pattern?(pattern) do - case Pattern.tuple_elements(pattern) do - elements when is_list(elements) -> Enum.all?(elements, &binding_pattern?/1) - _not_tuple -> false - end - end - - # The variables a pattern binds, as one variable or a tuple of them. - defp pattern_binder(pattern) do - case pattern_variables(pattern) do - [] -> quote(do: {}) - [variable] -> variable - variables -> {:{}, [], variables} - end - end - - defp pattern_variables(pattern) do - {_pattern, variables} = - Macro.prewalk(pattern, [], fn - {:^, _meta, _args}, variables -> - {:pinned, variables} - - {name, meta, context} = variable, variables when is_atom(name) and is_atom(context) -> - if name |> Atom.to_string() |> String.starts_with?("_"), - do: {variable, variables}, - else: {variable, [{name, meta, context} | variables]} - - other, variables -> - {other, variables} - end) - - variables |> Enum.reverse() |> Enum.uniq_by(fn {name, _meta, context} -> {name, context} end) - end - - # A comprehension evaluated for its effects: nested loops, `if let` for patterns that can fail, - # and `if` for filters, so the body may return early. - defp lower_for_loop([{:<-, _, [pattern, collection]} | rest], body, %Context{} = context) do - body_context = context_with_for_pattern(pattern, collection, context) - - if binding_pattern?(pattern) do - %AST.For{ - pattern: lower_binding_pattern(pattern), - expr: lower_expr(collection, context), - body: lower_for_loop_body(rest, body, body_context) - } - else - %AST.For{ - pattern: %AST.PatVar{name: :__rustq_item}, - expr: lower_expr(collection, context), - body: [ - %AST.IfLet{ - pattern: lower_match_pattern(pattern, nil, context), - expr: %AST.Var{name: :__rustq_item}, - then: - lower_for_loop_body(rest, body, context_with_match_pattern(pattern, nil, context)) - } - ] - } - end - end - - defp lower_for_loop_body([], body, %Context{} = context), do: lower_clause_body(body, context) - - defp lower_for_loop_body([{:<-, _, _} | _] = qualifiers, body, %Context{} = context), - do: [lower_for_loop(qualifiers, body, context)] - - defp lower_for_loop_body([filter | rest], body, %Context{} = context) do - [ - %AST.ExprStmt{ - expr: %AST.If{ - condition: lower_expr(filter, context), - then: lower_for_loop_body(rest, body, context) - } - } - ] - end - - defp lower_for_reduce_expr(expression, %Context{return_type: %Type{} = return_type} = context), - do: lower_for_reduce_expr(expression, return_type, context) - - defp lower_for_reduce_expr( - {:for, _, [{:<-, _, [pattern, expression]}, [reduce: initial], [do: clauses]]}, - %Type{} = return_type, - %Context{} = context - ) do - acc = :__rustq_reduce - acc_type = reduce_acc_type(initial, return_type) - - %AST.BlockExpr{ - body: [ - %AST.Let{ - pattern: %AST.PatVar{name: acc}, - mutable: true, - expr: lower_reduce_initial(initial, acc_type, context) - }, - %AST.For{ - pattern: lower_binding_pattern(pattern), - expr: lower_expr(expression, context), - body: [ - %AST.Assign{ - target: %AST.Var{name: acc}, - expr: %AST.Match{ - expr: %AST.Var{name: acc}, - arms: lower_for_reduce_arms(clauses, acc_type, context) - } - } - ] - }, - %AST.Return{expr: %AST.Var{name: acc}} - ] - } - end - - defp lower_for_reduce_expr(other, _return_type, _context) do - Diagnostic.lower( - :unsupported_for_reduce, - other, - "unsupported defrust for/reduce expression", - suggestion: "Use `for pattern <- enumerable, reduce: initial do acc_pattern -> body end`." - ) - end - - defp lower_reduce_initial(initial, %Type{} = acc_type, %Context{} = context) do - if Type.propagates?(acc_type) do - lower_return_expr(initial, acc_type, context) - else - lower_expr(initial, acc_type, context) - end - end - - defp reduce_acc_type(:ok, %Type{kind: :nif_result}) do - unit = %Type{kind: :unit, rust: "()", ast: %AST.TypeUnit{}} - - %Type{ - kind: :nif_result, - rust: "NifResult<()>", - ast: %AST.TypeNifResult{inner: unit.ast}, - meta: %{inner: unit} - } - end - - defp reduce_acc_type(_initial, %Type{} = return_type), do: return_type - - defp lower_for_reduce_arms(clauses, %Type{} = return_type, %Context{} = context) do - carry = :__rustq_reduce_value - - Enum.map(clauses, fn {:->, _, [[pattern], body]} -> - body_context = context_with_match_pattern(pattern, return_type, context) - - %AST.Arm{ - pattern: lower_match_pattern(pattern, return_type, context), - body: - lower_clause_body( - body, - %{body_context | position: :return, return_type: return_type}, - return_type - ) - } - end) ++ - [ - %AST.Arm{ - pattern: %AST.PatVar{name: carry}, - body: [%AST.Return{expr: %AST.Var{name: carry}}] - } - ] - end - defp block_expr([%AST.Return{expr: expr}]), do: expr defp block_expr(statements) do %AST.BlockExpr{body: statements} end - defp lower_clause_body(body, %Context{position: :statement} = context) do + @doc "Lowers the body of a clause, expected to give `type` when known." + def lower_clause_body(body, %Context{position: :statement} = context) do expressions = block_expressions(body) context = context_with_downstream_let_types(expressions, context) @@ -2463,25 +1930,25 @@ defmodule RustQ.Meta.Lower do |> reject_unit_statements() end - defp lower_clause_body(body, %Context{position: :return} = context) do + def lower_clause_body(body, %Context{position: :return} = context) do body |> block_expressions() |> lower_block(context) end - defp lower_clause_body(body, %Context{position: :expr} = context) do + def lower_clause_body(body, %Context{position: :expr} = context) do body |> block_expressions() |> lower_block(%{context | return_type: nil}) end - defp lower_clause_body(body, %Context{position: :expr} = context, %Type{} = expected_type) do + def lower_clause_body(body, %Context{position: :expr} = context, %Type{} = expected_type) do body |> block_expressions() |> lower_expected_block(%{context | return_type: expected_type}, expected_type) end - defp lower_clause_body(body, %Context{position: :return} = context, %Type{} = expected_type) do + def lower_clause_body(body, %Context{position: :return} = context, %Type{} = expected_type) do if Type.propagates?(expected_type) do lower_clause_body(body, context) else @@ -2491,46 +1958,9 @@ defmodule RustQ.Meta.Lower do end end - defp lower_clause_body(body, %Context{} = context, _expected_type), + def lower_clause_body(body, %Context{} = context, _expected_type), do: lower_clause_body(body, context) - defp infer_case_type_from_patterns(clauses) do - if Enum.any?(clauses, fn {:->, _, [[pattern], _body]} -> option_pattern?(pattern) end) do - %Type{kind: :option} - end - end - - defp option_pattern?(nil), do: true - defp option_pattern?(:none), do: true - defp option_pattern?({:some, _pattern}), do: true - defp option_pattern?({:{}, _, [:some, _pattern]}), do: true - defp option_pattern?(_pattern), do: false - - defp list_pattern?([]), do: true - defp list_pattern?([_head | _tail]), do: true - defp list_pattern?(_pattern), do: false - - defp split_list_pattern(pattern), do: do_split_list_pattern(pattern, []) - - defp do_split_list_pattern([], elements), do: {Enum.reverse(elements), nil} - - defp do_split_list_pattern([{:|, _, [head, rest]}], elements), - do: {Enum.reverse([head | elements]), rest} - - defp do_split_list_pattern([head | tail], elements) when is_list(tail), - do: do_split_list_pattern(tail, [head | elements]) - - defp do_split_list_pattern([head | tail], elements), - do: {Enum.reverse([head | elements]), tail} - - defp result_pattern?({:ok, _pattern}), do: true - defp result_pattern?({:error, _pattern}), do: true - defp result_pattern?({:{}, _, [:ok, _pattern]}), do: true - defp result_pattern?({:{}, _, [:error, _pattern]}), do: true - defp result_pattern?(ok: _pattern), do: true - defp result_pattern?(error: _pattern), do: true - defp result_pattern?(_pattern), do: false - defp reject_unit_statements(statements) do Enum.reject(statements, fn %AST.ExprStmt{expr: %AST.Tuple{values: []}} -> true @@ -2538,491 +1968,57 @@ defmodule RustQ.Meta.Lower do end) end - defp lower_binding_pattern({name, _, context}) when is_atom(name) and is_atom(context), - do: %AST.PatVar{name: name} + defp lower_guard_expr(nil, %Context{}), do: nil + defp lower_guard_expr(guard, %Context{} = context), do: lower_expr(guard, context) - defp lower_binding_pattern({:{}, _, values}), - do: %AST.PatTuple{patterns: Enum.map(values, &lower_binding_pattern/1)} + defp lower_dot_expr(receiver, field_or_function, call_meta, %Context{} = context) do + no_parens? = Keyword.get(call_meta, :no_parens, false) - defp lower_binding_pattern({left, right}), - do: %AST.PatTuple{patterns: Enum.map([left, right], &lower_binding_pattern/1)} + cond do + no_parens? and Paths.alias_path_ast?(receiver) -> + %AST.Path{ + parts: Paths.alias_path_parts(receiver, context.rust_modules) ++ [field_or_function] + } - defp lower_binding_pattern(other) do - Diagnostic.lower( - :unsupported_binding_pattern, - other, - "unsupported defrust binding pattern", - suggestion: "Use a variable or tuple pattern." - ) - end + no_parens? -> + %AST.Field{receiver: lower_expr(receiver, context), field: field_or_function} - defp split_guarded_pattern({:when, _, [pattern, guard]}), do: {pattern, guard} - defp split_guarded_pattern(pattern), do: {pattern, nil} + Paths.super_alias_ast?(receiver) -> + %AST.PathCall{path: %AST.Path{parts: [:super, field_or_function]}, args: []} - defp context_with_match_pattern(pattern, %Type{kind: :option} = type, %Context{} = context) do - context_with_inner_pattern(pattern, Type.inner(type), [:some], context) - end + Paths.alias_ast?(receiver) -> + %AST.PathCall{ + path: %AST.Path{ + parts: Paths.alias_parts(receiver, context.rust_modules) ++ [field_or_function] + }, + args: [] + } - defp context_with_match_pattern(pattern, %Type{kind: kind} = type, %Context{} = context) - when kind in [:result, :nif_result] do - context_with_inner_pattern(pattern, Type.inner(type), [:ok], context) - end + field_or_function == :into_iter -> + receiver_type = + infer_expr_type(receiver, context.vars) || Typing.synth(receiver, typing_env(context)) + + %AST.MethodCall{ + receiver: lower_dot_receiver(receiver, :into_iter, context), + method: borrowed_iteration(:into_iter, [], receiver_type) + } - defp context_with_match_pattern([], %Type{kind: :vec} = type, %Context{} = context), - do: context_with_list_pattern([], type, context) + true -> + %AST.MethodCall{ + receiver: lower_dot_receiver(receiver, field_or_function, context), + method: field_or_function + } + end + end - defp context_with_match_pattern(pattern, %Type{} = type, %Context{} = context) - when is_list(pattern) and type.kind == :slice, - do: context_with_list_pattern(pattern, type, context) + defp lower_dot_receiver(receiver, :ok, %Context{} = context), do: lower_expr(receiver, context) - defp context_with_match_pattern( - [_head | _tail] = pattern, - %Type{kind: :vec} = type, + defp lower_dot_receiver( + {{:., _, [_term, :map_get]}, _, [_key]} = receiver, + _method, %Context{} = context ), - do: context_with_list_pattern(pattern, type, context) - - defp context_with_match_pattern( - {:%{}, _, fields}, - %Type{kind: :struct} = type, - %Context{} = context - ) - when is_list(fields), - do: context_with_struct_pattern(fields, type, context) - - defp context_with_match_pattern( - {:%, _, [_module, {:%{}, _, fields}]}, - %Type{kind: :struct} = type, - %Context{} = context - ) - when is_list(fields), - do: context_with_struct_pattern(fields, type, context) - - defp context_with_match_pattern( - {name, _meta, ast_context}, - %Type{} = type, - %Context{} = context - ) - when is_atom(name) and is_atom(ast_context) do - %{context | vars: Map.put(context.vars, name, type)} - end - - defp context_with_match_pattern(pattern, %Type{kind: :rust_enum} = type, %Context{} = context), - do: context_with_variant_pattern(pattern, type, & &1, context) - - defp context_with_match_pattern( - pattern, - %Type{kind: kind, meta: %{inner: %Type{kind: :rust_enum} = inner}}, - %Context{} = context - ) - when kind in [:ref, :mut_ref], - do: context_with_variant_pattern(pattern, inner, &Type.ref/1, context) - - defp context_with_match_pattern(pattern, %Type{kind: :tuple, meta: %{elements: types}}, context) do - case Pattern.tuple_elements(pattern) do - elements when is_list(elements) and length(elements) == length(types) -> - elements - |> Enum.zip(types) - |> Enum.reduce(context, fn {element, type}, acc -> - context_with_match_pattern(element, type, acc) - end) - - _not_tuple -> - context - end - end - - # A pattern of unknown type still shadows the names it binds; they lose their old types. - defp context_with_match_pattern(pattern, _type, %Context{} = context) do - names = pattern |> pattern_variables() |> Enum.map(&elem(&1, 0)) - %{context | vars: Map.drop(context.vars, names)} - end - - defp context_with_variant_pattern(pattern, type, payload_type, %Context{} = context) do - case rust_enum_variant(pattern, type) do - {_variant, payloads, types} when length(payloads) == length(types) -> - payloads - |> Enum.zip(types) - |> Enum.reduce(context, fn {payload, type}, acc -> - context_with_match_pattern(payload, payload_type.(type), acc) - end) - - _other -> - context - end - end - - defp context_with_struct_pattern(fields, %Type{} = type, %Context{} = context) do - Enum.reduce(fields, context, fn {field, pattern}, acc -> - case Type.field_type(type, field) do - %Type{} = field_type -> context_with_match_pattern(pattern, field_type, acc) - nil -> acc - end - end) - end - - # A slice binds its elements by reference; a vector matched as a slice does too. - defp context_with_list_pattern(pattern, %Type{} = type, %Context{} = context) do - {elements, rest} = split_list_pattern(pattern) - inner = list_item_type(type) - inner = if inner && type.kind == :slice, do: Type.ref(inner), else: inner - - context = - Enum.reduce(elements, context, fn element, acc -> - context_with_match_pattern(element, inner, acc) - end) - - if rest, - do: context_with_match_pattern(rest, Type.slice_ref(inner), context), - else: context - end - - defp context_with_for_pattern(pattern, expression, %Context{} = context) do - expression - |> infer_expr_type(context.vars) - |> Kernel.||(Typing.synth(expression, typing_env(context))) - |> for_item_type() - |> case do - %Type{} = item_type -> context_with_match_pattern(pattern, item_type, context) - nil -> context - end - end - - defp for_item_type(%Type{} = type), do: Type.vec_inner(type) || Type.slice_inner(type) - defp for_item_type(_type), do: nil - - defp context_with_inner_pattern(pattern, %Type{} = inner, wrappers, %Context{} = context) do - case unwrap_match_pattern(pattern, wrappers) do - nil -> context - inner_pattern -> context_with_match_pattern(inner_pattern, inner, context) - end - end - - defp context_with_inner_pattern(_pattern, _inner, _wrappers, %Context{} = context), do: context - - defp unwrap_match_pattern({name, _meta, ast_context} = pattern, _wrappers) - when is_atom(name) and is_atom(ast_context), - do: pattern - - defp unwrap_match_pattern({wrapper, inner}, wrappers) do - if wrapper in wrappers, do: inner - end - - defp unwrap_match_pattern({:{}, _, [wrapper, inner]}, wrappers) do - if wrapper in wrappers, do: inner - end - - defp unwrap_match_pattern(_pattern, _wrappers), do: nil - - defp lower_guard_expr(nil, %Context{}), do: nil - defp lower_guard_expr(guard, %Context{} = context), do: lower_expr(guard, context) - - # Patterns name constants and enum variants through the module's Rust module aliases. - defp lower_match_pattern(pattern, case_type, %Context{} = context), - do: pattern |> resolve_pattern_paths(context) |> lower_match_pattern(case_type) - - defp resolve_pattern_paths(pattern, %Context{} = context) do - Macro.prewalk(pattern, fn - {:enum_variant, meta, [{:__aliases__, _, parts} | rest]} -> - {:enum_variant, meta, [rust_path_marker(parts, context) | rest]} - - {:__aliases__, _, parts} = alias -> - if constant_alias?(parts), do: rust_path_marker(parts, context), else: alias - - other -> - other - end) - end - - defp rust_path_marker(parts, %Context{} = context), - do: {:__rustq_path__, [], mapped_alias_parts(parts, context.rust_modules)} - - # A constant ends in an upper-case segment such as `Kind.PLAIN`. - defp constant_alias?(parts) do - name = parts |> List.last() |> Atom.to_string() - name == String.upcase(name) and String.length(name) > 1 - end - - defp lower_match_pattern({:__rustq_path__, _, parts}, _case_type), - do: %AST.PatPath{path: %AST.Path{parts: parts}} - - defp lower_match_pattern(:ok, %Type{kind: kind}) when kind in [:result, :nif_result], - do: %AST.PatOk{pattern: %AST.PatTuple{patterns: []}} - - defp lower_match_pattern(nil, %Type{kind: :option}), do: %AST.PatNone{} - defp lower_match_pattern(:none, %Type{kind: :option}), do: %AST.PatNone{} - defp lower_match_pattern(nil, _case_type), do: %AST.PatNone{} - defp lower_match_pattern(:none, _case_type), do: %AST.PatNone{} - defp lower_match_pattern({:_, _, _}, _case_type), do: %AST.PatWildcard{} - - defp lower_match_pattern(value, _case_type) - when is_binary(value) or is_integer(value) or is_boolean(value), - do: %AST.PatLiteral{value: value} - - defp lower_match_pattern([ok: pattern], _case_type), - do: %AST.PatOk{pattern: lower_match_pattern(pattern, nil)} - - defp lower_match_pattern([error: pattern], _case_type), - do: %AST.PatErr{pattern: lower_match_pattern(pattern, nil)} - - defp lower_match_pattern({:ok, pattern}, _case_type), - do: %AST.PatOk{pattern: lower_match_pattern(pattern, nil)} - - defp lower_match_pattern({:error, pattern}, _case_type), - do: %AST.PatErr{pattern: lower_match_pattern(pattern, nil)} - - defp lower_match_pattern({:some, pattern}, %Type{kind: :option} = type), - do: %AST.PatSome{pattern: lower_tuple_element_pattern(pattern, Type.inner(type))} - - defp lower_match_pattern({:some, pattern}, _case_type), - do: %AST.PatSome{pattern: lower_match_pattern(pattern, nil)} - - defp lower_match_pattern({:{}, _, [:some, pattern]}, %Type{kind: :option} = type), - do: %AST.PatSome{pattern: lower_tuple_element_pattern(pattern, Type.inner(type))} - - defp lower_match_pattern({:{}, _, [:some, pattern]}, _case_type), - do: %AST.PatSome{pattern: lower_match_pattern(pattern, nil)} - - defp lower_match_pattern({:enum_variant, _, [path, variant]}, _case_type) do - %AST.PatPath{path: enum_variant_path(path, variant)} - end - - defp lower_match_pattern({:enum_variant, _, [path, variant | patterns]}, _case_type) do - %AST.PatPathTuple{ - path: enum_variant_path(path, variant), - patterns: Enum.map(patterns, &lower_match_pattern(&1, nil)) - } - end - - defp lower_match_pattern([], %Type{kind: :vec} = type), do: lower_list_pattern([], type) - - defp lower_match_pattern(pattern, %Type{kind: :slice} = type) when is_list(pattern), - do: lower_list_pattern(pattern, type) - - # A list pattern on a value of unknown type matches it as a slice. - defp lower_match_pattern(pattern, nil) - when is_list(pattern) and pattern != [] and not is_tuple(hd(pattern)), - do: lower_list_pattern(pattern, nil) - - defp lower_match_pattern([_head | _tail] = pattern, %Type{kind: :vec} = type), - do: lower_list_pattern(pattern, type) - - defp lower_match_pattern( - {:%{}, _, fields}, - %Type{kind: :struct, rust: rust_name, meta: %{fields: type_fields}} - ) - when is_list(fields) do - %AST.PatStruct{ - path: %AST.Path{parts: [rust_name]}, - fields: lower_struct_pattern_fields(fields, type_fields) - } - end - - defp lower_match_pattern( - {:%, _, [_module, {:%{}, _, fields}]}, - %Type{kind: :struct, rust: rust_name, meta: %{fields: type_fields}} - ) - when is_list(fields) do - %AST.PatStruct{ - path: %AST.Path{parts: [rust_name]}, - fields: lower_struct_pattern_fields(fields, type_fields) - } - end - - defp lower_match_pattern({:%, _, [{:__aliases__, _, modules}, {:%{}, _, fields}]}, %Type{ - kind: :tuple_enum, - rust: rust_name - }) do - module = List.last(modules) - - %AST.PatPathTuple{ - path: %AST.Path{parts: [rust_name, module]}, - patterns: [ - %AST.PatStruct{ - path: %AST.Path{parts: [module]}, - fields: - Enum.map(fields, fn {name, pattern} -> {name, lower_match_pattern(pattern, nil)} end) - } - ] - } - end - - defp lower_match_pattern({name, _, context}, %Type{kind: :option}) - when is_atom(name) and is_atom(context), - do: %AST.PatSome{pattern: %AST.PatVar{name: name}} - - defp lower_match_pattern({name, _, context}, _case_type) - when is_atom(name) and is_atom(context), - do: %AST.PatVar{name: name} - - # A borrowed enum matches its variants as they are, binding payloads by reference. - defp lower_match_pattern( - pattern, - %Type{kind: kind, meta: %{inner: %Type{kind: :rust_enum} = inner}} - ) - when kind in [:ref, :mut_ref] and (is_atom(pattern) or is_tuple(pattern)), - do: lower_match_pattern(pattern, inner) - - defp lower_match_pattern(pattern, %Type{kind: :rust_enum, rust: rust_name} = type) - when is_atom(pattern) or is_tuple(pattern) do - case rust_enum_variant(pattern, type) do - {variant, [], _types} -> - %AST.PatPath{path: %AST.Path{parts: [rust_name, rust_variant(variant)]}} - - {variant, payloads, types} when length(payloads) == length(types) -> - %AST.PatPathTuple{ - path: %AST.Path{parts: [rust_name, rust_variant(variant)]}, - patterns: Enum.zip_with(payloads, types, &lower_tuple_element_pattern/2) - } - - _other -> - lower_match_pattern(pattern, nil) - end - end - - defp lower_match_pattern(atom, %Type{kind: :enum, rust: rust_name}) when is_atom(atom), - do: %AST.PatPath{path: %AST.Path{parts: [rust_name, rust_variant(atom)]}} - - defp lower_match_pattern(atom, %Type{kind: :atom}) when is_atom(atom), - do: %AST.PatAtomGuard{name: atom} - - defp lower_match_pattern(atom, _case_type) when is_atom(atom), do: %AST.PatAtomGuard{name: atom} - - defp lower_match_pattern({:{}, _, values}, case_type), - do: lower_tuple_match_pattern(values, case_type) - - defp lower_match_pattern({left, right}, case_type), - do: lower_tuple_match_pattern([left, right], case_type) - - defp lower_match_pattern(other, _case_type) do - Diagnostic.lower( - :unsupported_match_pattern, - other, - "unsupported defrust match pattern", - suggestion: - "Use a variable, tuple, list, option/result, atom, literal, or supported struct pattern." - ) - end - - defp lower_struct_pattern_fields(fields, type_fields) do - patterns = Map.new(fields) - - Enum.map(type_fields, fn {name, _type, _presence} -> - pattern = Map.get(patterns, name, {:_, [], nil}) - {name, lower_match_pattern(pattern, nil)} - end) - end - - defp lower_list_pattern(pattern, type) do - {elements, rest} = split_list_pattern(pattern) - inner = type && list_item_type(type) - - %AST.PatSlice{ - patterns: Enum.map(elements, &lower_tuple_element_pattern(&1, inner)), - rest: if(rest, do: lower_match_pattern(rest, inner && Type.slice_ref(inner))) - } - end - - defp list_item_type(%Type{} = type), do: Type.vec_inner(type) || Type.slice_inner(type) - - # Each element is a pattern of its own, such as `{:some, Kind.PLAIN}`. - defp lower_tuple_match_pattern(values, case_type) do - types = - case case_type && Type.expected_value(case_type) do - %Type{kind: :tuple, meta: %{elements: elements}} - when length(elements) == length(values) -> - elements - - _unknown -> - List.duplicate(nil, length(values)) - end - - %AST.PatTuple{patterns: Enum.zip_with(values, types, &lower_tuple_element_pattern/2)} - end - - # A variable binds the whole element, whatever its type. - defp lower_tuple_element_pattern({name, _, context} = pattern, _type) - when is_atom(name) and is_atom(context), - do: lower_match_pattern(pattern, nil) - - defp lower_tuple_element_pattern(pattern, type), do: lower_match_pattern(pattern, type) - - defp mark_mutable_pattern_vars(%AST.PatVar{name: name} = pattern, mutable_vars) do - if MapSet.member?(mutable_vars, name), do: %{pattern | mutable: true}, else: pattern - end - - defp mark_mutable_pattern_vars(%AST.PatSome{pattern: pattern} = some, mutable_vars), - do: %{some | pattern: mark_mutable_pattern_vars(pattern, mutable_vars)} - - defp mark_mutable_pattern_vars(%AST.PatOk{pattern: pattern} = ok, mutable_vars), - do: %{ok | pattern: mark_mutable_pattern_vars(pattern, mutable_vars)} - - defp mark_mutable_pattern_vars(%AST.PatErr{pattern: pattern} = err, mutable_vars), - do: %{err | pattern: mark_mutable_pattern_vars(pattern, mutable_vars)} - - defp mark_mutable_pattern_vars(%AST.PatTuple{patterns: patterns} = tuple, mutable_vars), - do: %{tuple | patterns: Enum.map(patterns, &mark_mutable_pattern_vars(&1, mutable_vars))} - - defp mark_mutable_pattern_vars(%AST.PatPathTuple{patterns: patterns} = tuple, mutable_vars), - do: %{tuple | patterns: Enum.map(patterns, &mark_mutable_pattern_vars(&1, mutable_vars))} - - defp mark_mutable_pattern_vars(%AST.PatStruct{fields: fields} = struct, mutable_vars) do - fields = - Enum.map(fields, fn {name, pattern} -> - {name, mark_mutable_pattern_vars(pattern, mutable_vars)} - end) - - %{struct | fields: fields} - end - - defp mark_mutable_pattern_vars(pattern, _mutable_vars), do: pattern - - defp lower_dot_expr(receiver, field_or_function, call_meta, %Context{} = context) do - no_parens? = Keyword.get(call_meta, :no_parens, false) - - cond do - no_parens? and alias_path_ast?(receiver) -> - %AST.Path{parts: alias_path_parts(receiver, context.rust_modules) ++ [field_or_function]} - - no_parens? -> - %AST.Field{receiver: lower_expr(receiver, context), field: field_or_function} - - super_alias_ast?(receiver) -> - %AST.PathCall{path: %AST.Path{parts: [:super, field_or_function]}, args: []} - - alias_ast?(receiver) -> - %AST.PathCall{ - path: %AST.Path{ - parts: alias_parts(receiver, context.rust_modules) ++ [field_or_function] - }, - args: [] - } - - field_or_function == :into_iter -> - receiver_type = - infer_expr_type(receiver, context.vars) || Typing.synth(receiver, typing_env(context)) - - %AST.MethodCall{ - receiver: lower_dot_receiver(receiver, :into_iter, context), - method: borrowed_iteration(:into_iter, [], receiver_type) - } - - true -> - %AST.MethodCall{ - receiver: lower_dot_receiver(receiver, field_or_function, context), - method: field_or_function - } - end - end - - defp lower_dot_receiver(receiver, :ok, %Context{} = context), do: lower_expr(receiver, context) - - defp lower_dot_receiver( - {{:., _, [_term, :map_get]}, _, [_key]} = receiver, - _method, - %Context{} = context - ), - do: %AST.Try{expr: lower_expr(receiver, context)} + do: %AST.Try{expr: lower_expr(receiver, context)} defp lower_dot_receiver(receiver, _method, %Context{} = context), do: lower_expr(receiver, context) @@ -3032,22 +2028,22 @@ defmodule RustQ.Meta.Lower do macro_call_name?(function) -> lower_remote_macro_call(receiver, function, args, context) - super_alias_ast?(receiver) -> + Paths.super_alias_ast?(receiver) -> %AST.PathCall{ path: %AST.Path{parts: [:super, function]}, args: lower_parent_call_args(function, args, context) } - rust_constructor_alias?(receiver) -> - path = alias_parts(receiver, context.rust_modules) ++ [rust_variant(function)] + Paths.rust_constructor_alias?(receiver) -> + path = Paths.alias_parts(receiver, context.rust_modules) ++ [rust_variant(function)] %AST.PathCall{ path: %AST.Path{parts: path}, args: lower_path_call_args(path, function, args, context) } - alias_ast?(receiver) -> - path = alias_parts(receiver, context.rust_modules) ++ [function] + Paths.alias_ast?(receiver) -> + path = Paths.alias_parts(receiver, context.rust_modules) ++ [function] %AST.PathCall{ path: %AST.Path{parts: path}, @@ -3058,7 +2054,7 @@ defmodule RustQ.Meta.Lower do receiver_type = infer_expr_type(receiver, context.vars) || Typing.synth(receiver, typing_env(context)) - target = callable_target_from_type(receiver_type) + target = Paths.callable_target_from_type(receiver_type) %AST.MethodCall{ receiver: lower_method_receiver(receiver, receiver_type, function, context), @@ -3127,18 +2123,23 @@ defmodule RustQ.Meta.Lower do defp lower_path_call_args(path, function, args, %Context{} = context) do path - |> path_callable_argument_types(function, length(args), context) + |> Paths.path_callable_argument_types(function, length(args), context) |> lower_args_with_expected(args, context) end defp lower_call_args(target, function, args, %Context{} = context) do target - |> callable_argument_types(function, length(args), context) + |> Paths.callable_argument_types(function, length(args), context) |> lower_args_with_expected(args, context) end defp lower_parent_call_args(function, args, %Context{} = context) do - parent_module_callable_argument_types([:Super], function, length(args), context.callables) + Paths.parent_module_callable_argument_types( + [:Super], + function, + length(args), + context.callables + ) |> lower_args_with_expected(args, context) end @@ -3190,7 +2191,7 @@ defmodule RustQ.Meta.Lower do defp lower_method_call_args(receiver_type, target, function, args, %Context{} = context) do arity = length(args) - expected = callable_argument_types(target, function, arity, context) + expected = Paths.callable_argument_types(target, function, arity, context) callable = BindingIndex.get(context.callables, target, function, arity) expected = specialize_method_args(callable, receiver_type, args, expected, context) lower_args_with_expected(expected, args, context) @@ -3266,94 +2267,12 @@ defmodule RustQ.Meta.Lower do defp array_expr?({:array, _, [_values]}), do: true defp array_expr?(_expression), do: false - defp slice_type?(%Type{kind: :slice}), do: true - defp slice_type?(%Type{ast: %AST.TypeSlice{}}), do: true - defp slice_type?(%Type{ast: %AST.TypeRef{inner: %AST.TypeSlice{}}}), do: true - defp slice_type?(%Type{}), do: false - defp slice_type?(_type), do: false - - defp callable_argument_types(target, function, arity, %Context{callables: callables}) do - callable_argument_types(callables, target, function, arity) - end - - defp callable_argument_types(%BindingIndex{} = callables, target, function, arity) do - BindingIndex.argument_types(callables, target, function, arity) - end - - defp path_callable_argument_types(path, function, arity, %Context{} = context) do - path_callable_argument_types(path, function, arity, context.callables, context.rust_modules) - end - - defp path_callable_argument_types( - path, - function, - arity, - %BindingIndex{} = callables, - rust_modules - ) do - target_parts = Enum.drop(path, -1) - - parent_module_callable_argument_types(target_parts, function, arity, callables) || - target_parts - |> exact_callable_target_candidates(rust_modules) - |> Enum.find_value(&callable_argument_types(callables, &1, function, arity)) || - path_module_fallback_argument_types(target_parts, function, arity, callables) || - target_parts - |> callable_target_candidates(rust_modules) - |> Enum.find_value(&callable_argument_types(callables, &1, function, arity)) - end - - defp parent_module_callable_argument_types( - [:Super], - function, - arity, - %BindingIndex{} = callables - ), - do: - callable_argument_types(callables, nil, function, arity) || - BindingIndex.unqualified_argument_types(callables, function, arity) - - defp parent_module_callable_argument_types(_target_parts, _function, _arity, %BindingIndex{}), - do: nil - - defp path_module_fallback_argument_types( - target_parts, - function, - arity, - %BindingIndex{} = callables - ) do - if rust_module_path?(target_parts) do - callable_argument_types(callables, nil, function, arity) - end - end - - defp parent_module_callable_return_type([:Super], function, arity, %BindingIndex{} = callables), - do: - BindingIndex.return_type(callables, nil, function, arity) || - BindingIndex.unqualified_return_type(callables, function, arity) - - defp parent_module_callable_return_type(_target_parts, _function, _arity, %BindingIndex{}), - do: nil - - defp path_module_fallback_return_type( - target_parts, - function, - arity, - %BindingIndex{} = callables - ) do - if rust_module_path?(target_parts) do - BindingIndex.return_type(callables, nil, function, arity) - end - end - - defp rust_module_path?([_ | _] = parts), - do: rust_module_name?(parts |> List.last() |> to_string()) - - defp rust_module_path?(_parts), do: false - - defp rust_module_name?(<>) when first in ?a..?z, do: true - defp rust_module_name?(<<"_", _rest::binary>>), do: true - defp rust_module_name?(_name), do: false + @doc "Whether `type` is a slice or a reference to one." + def slice_type?(%Type{kind: :slice}), do: true + def slice_type?(%Type{ast: %AST.TypeSlice{}}), do: true + def slice_type?(%Type{ast: %AST.TypeRef{inner: %AST.TypeSlice{}}}), do: true + def slice_type?(%Type{}), do: false + def slice_type?(_type), do: false # Iterating a borrowed collection with `into_iter` is `iter`. defp borrowed_iteration(:into_iter, [], nil), do: :into_iter @@ -3366,8 +2285,9 @@ defmodule RustQ.Meta.Lower do defp borrowed_iteration(function, _args, _receiver_type), do: function + @doc "The Rust enum variant path an atom of an enum type names." # The variant a tagged tuple or atom names, with its payloads and their declared types. - defp rust_enum_variant(pattern, %Type{meta: %{variants: variants}}) do + def rust_enum_variant(pattern, %Type{meta: %{variants: variants}}) do {tag, payloads} = case pattern do tag when is_atom(tag) -> {tag, []} @@ -3382,9 +2302,6 @@ defmodule RustQ.Meta.Lower do end end - defp callable_target_from_type(%Type{} = type), do: Type.callable_target(type) - defp callable_target_from_type(_type), do: nil - defp decode_as_expr(expression, type_ast, %Context{} = context) do %AST.MethodCall{ receiver: lower_decode_receiver(expression, context), @@ -3489,9 +2406,10 @@ defmodule RustQ.Meta.Lower do } end + @doc "Whether a collection is still in use after this use of it; see `RustQ.Meta.Ownership`." # A collection is in use unless this is its value's last use; see `RustQ.Meta.Ownership`. A # field reached through a reference is never taken, since the reference does not own it. - defp in_use?(collection, %Context{} = context) do + def in_use?(collection, %Context{} = context) do case root_variable_node(collection) do {name, _meta, _context} = variable -> behind_reference?(collection, name, context) or @@ -3530,7 +2448,7 @@ defmodule RustQ.Meta.Lower do context = Enum.zip(args, expected_args) |> Enum.reduce(context, fn {pattern, type}, acc -> - context_with_match_pattern(pattern, type, acc) + LowerPattern.context_with_match_pattern(pattern, type, acc) end) %AST.Closure{ @@ -3540,7 +2458,7 @@ defmodule RustQ.Meta.Lower do end defp closure_context(args, context) do - names = Enum.flat_map(args, &binding_pattern_names/1) + names = Enum.flat_map(args, &LowerPattern.binding_pattern_names/1) %{ context @@ -3558,11 +2476,11 @@ defmodule RustQ.Meta.Lower do |> Map.keys() |> MapSet.new() |> MapSet.union(context.captured) - |> MapSet.difference(MapSet.new(Enum.flat_map(args, &binding_pattern_names/1))) + |> MapSet.difference(MapSet.new(Enum.flat_map(args, &LowerPattern.binding_pattern_names/1))) context = Enum.reduce(typed, closure_context(args, context), fn {arg, type}, context -> - context_with_match_pattern(arg, type, context) + LowerPattern.context_with_match_pattern(arg, type, context) end) context = %{context | captured: captured} @@ -3634,7 +2552,7 @@ defmodule RustQ.Meta.Lower do {{:., _, [{:__aliases__, _, parts}, name]}, _, []}, %Context{} = context ), - do: %AST.Path{parts: mapped_alias_parts(parts, context.rust_modules) ++ [name]} + do: %AST.Path{parts: Paths.mapped_alias_parts(parts, context.rust_modules) ++ [name]} defp lower_function_capture(other, %Context{}) do Diagnostic.lower( @@ -3647,7 +2565,7 @@ defmodule RustQ.Meta.Lower do defp closure_arg!({name, _, context}) when is_atom(name) and is_atom(context), do: name - defp closure_arg!(other), do: lower_binding_pattern(other) + defp closure_arg!(other), do: LowerPattern.lower_binding_pattern(other) defp closure_body_expr([expression]), do: expression defp closure_body_expr(expression), do: expression @@ -3728,9 +2646,14 @@ defmodule RustQ.Meta.Lower do defp lower_remote_macro_call(receiver, function, args, rust_modules, %Context{} = context) do path_parts = cond do - super_alias_ast?(receiver) -> [:super, macro_call_part(function)] - alias_ast?(receiver) -> alias_parts(receiver, rust_modules) ++ [macro_call_part(function)] - true -> nil + Paths.super_alias_ast?(receiver) -> + [:super, macro_call_part(function)] + + Paths.alias_ast?(receiver) -> + Paths.alias_parts(receiver, rust_modules) ++ [macro_call_part(function)] + + true -> + nil end if path_parts do @@ -3770,82 +2693,9 @@ defmodule RustQ.Meta.Lower do defp lower_nif_error(atom, %Context{}) when is_atom(atom), do: %AST.NifRaiseAtom{name: atom} defp lower_nif_error(other, %Context{} = context), do: lower_expr(other, context) - defp lower_semantic_pat({:ident, _, [name]}, %Context{}), - do: %AST.PatVar{name: semantic_atom!(name)} - - defp lower_semantic_pat({:mut_ident, _, [name]}, %Context{}), - do: %AST.PatVar{name: semantic_atom!(name), mutable: true} - - defp lower_semantic_pat({:path, _, [path]}, %Context{} = context), - do: %AST.PatPath{path: lower_expr_path(path, context)} - - defp lower_semantic_pat({:some, _, [pattern]}, %Context{} = context), - do: %AST.PatSome{pattern: lower_semantic_pat(pattern, context)} - - defp lower_semantic_pat({:ok, pattern}, %Context{} = context), - do: %AST.PatOk{pattern: lower_semantic_pat(pattern, context)} - - defp lower_semantic_pat({:error, pattern}, %Context{} = context), - do: %AST.PatErr{pattern: lower_semantic_pat(pattern, context)} - - defp lower_semantic_pat({:{}, _, [:ok, pattern]}, %Context{} = context), - do: %AST.PatOk{pattern: lower_semantic_pat(pattern, context)} - - defp lower_semantic_pat({:{}, _, [:error, pattern]}, %Context{} = context), - do: %AST.PatErr{pattern: lower_semantic_pat(pattern, context)} - - defp lower_semantic_pat({:tuple, _, [patterns]}, %Context{} = context), - do: %AST.PatTuple{patterns: Enum.map(patterns, &lower_semantic_pat(&1, context))} - - defp lower_semantic_pat({:path_tuple, _, [path, patterns]}, %Context{} = context), - do: %AST.PatPathTuple{ - path: lower_expr_path(path, context), - patterns: Enum.map(patterns, &lower_semantic_pat(&1, context)) - } - - defp lower_semantic_pat({:struct, _, [path, fields]}, %Context{} = context), - do: %AST.PatStruct{ - path: lower_expr_path(path, context), - fields: lower_semantic_pat_fields(fields, context) - } - - defp lower_semantic_pat(nil, %Context{}), do: %AST.PatNone{} - defp lower_semantic_pat(:_, %Context{}), do: %AST.PatWildcard{} - defp lower_semantic_pat({:_, _, _}, %Context{}), do: %AST.PatWildcard{} - - defp lower_semantic_pat(other, %Context{} = context), - do: lower_match_pattern(other, nil, context) - - defp lower_semantic_pat_fields(fields, %Context{} = context) when is_list(fields) do - Enum.map(fields, fn {name, pattern} -> {name, lower_semantic_pat(pattern, context)} end) - end - defp lower_semantic_arm_body(body, %Context{} = context), do: lower_clause_body(body, %{context | position: :return}) - defp lower_expr_path(%AST.Path{} = path, %Context{}), do: path - - defp lower_expr_path(expression, %Context{} = context) do - case lower_expr(expression, context) do - %AST.Path{} = path -> - path - - other -> - Diagnostic.lower( - :expected_rust_path, - expression, - "expected Rust path, got: #{inspect(other)}" - ) - end - end - - defp semantic_atom!(atom) when is_atom(atom), do: atom - defp semantic_atom!({name, _, context}) when is_atom(name) and is_atom(context), do: name - - defp semantic_atom!(other) do - Diagnostic.lower(:expected_atom_identifier, other, "expected atom identifier") - end - defp lower_type_arg({name, _, ast_context}, %Context{} = context) when is_atom(name) and is_atom(ast_context) do case macro_var_fragment(name, context.macro_vars) do @@ -3891,7 +2741,7 @@ defmodule RustQ.Meta.Lower do defp lower_struct_literal_path({:__aliases__, meta, parts} = alias_ast, %Context{} = context) do explicit = - Enum.find_value(alias_prefixes(parts), fn {prefix, suffix} -> + Enum.find_value(Paths.alias_prefixes(parts), fn {prefix, suffix} -> case Map.fetch(context.rust_modules, prefix) do {:ok, mapped} -> mapped ++ suffix :error -> nil @@ -3909,11 +2759,12 @@ defmodule RustQ.Meta.Lower do defp lower_struct_literal_path(path, %Context{} = context), do: lower_expr(path, context) - defp enum_variant_path(path, variant), do: enum_variant_path(path, variant, %Context{}) + @doc "The Rust path of an enum variant written as `enum_variant(Enum, :variant)`." + def enum_variant_path(path, variant), do: enum_variant_path(path, variant, %Context{}) - defp enum_variant_path(path, variant, %Context{} = context) do + def enum_variant_path(path, variant, %Context{} = context) do %AST.Path{parts: parts} = lower_expr(path, context) - %AST.Path{parts: parts ++ [rust_variant(semantic_atom!(variant))]} + %AST.Path{parts: parts ++ [rust_variant(LowerPattern.semantic_atom!(variant))]} end # The struct a type declaration of this module, or of a callable module, names `name`. @@ -3988,10 +2839,11 @@ defmodule RustQ.Meta.Lower do Typing.expected_for_let(pattern, expression, typing_env(context)) end - defp infer_expr_type({name, _, context}, vars) when is_atom(name) and is_atom(context), + @doc "The type of a variable in `vars`, or nil for another expression." + def infer_expr_type({name, _, context}, vars) when is_atom(name) and is_atom(context), do: Map.get(vars, name) - defp infer_expr_type(_expression, _vars), do: nil + def infer_expr_type(_expression, _vars), do: nil defp context_with_downstream_let_types(expressions, %Context{} = context) do inferred = @@ -4004,47 +2856,22 @@ defmodule RustQ.Meta.Lower do %{context | vars: Map.merge(context.vars, inferred), lexical_vars: context.vars} end - defp context_with_lexical_pattern_vars(pattern, %Context{} = context) do - vars = - pattern - |> binding_pattern_names() - |> Enum.reduce(context.vars, fn name, vars -> - case Map.fetch(context.lexical_vars, name) do - {:ok, type} -> Map.put(vars, name, type) - :error -> vars - end - end) - - %{context | vars: vars} - end - - defp binding_pattern_names({name, _meta, ast_context}) - when is_atom(name) and is_atom(ast_context) and name != :_, - do: [name] - - defp binding_pattern_names(pattern) do - case Pattern.tuple_elements(pattern) do - elements when is_list(elements) -> Enum.flat_map(elements, &binding_pattern_names/1) - _not_tuple -> [] - end - end - defp inference_callbacks(%Context{} = context) do callables = context.callables %{ - return_type: &callable_return_type(&1, callables: callables, vars: context.vars), + return_type: &Paths.callable_return_type(&1, callables: callables, vars: context.vars), local_argument_types: fn name, arity -> - callable_argument_types(callables, nil, name, arity) + Paths.callable_argument_types(callables, nil, name, arity) end, path_argument_types: fn parts, function, arity -> - path = alias_parts({:__aliases__, [], parts}, context.rust_modules) ++ [function] - path_callable_argument_types(path, function, arity, callables, context.rust_modules) + path = Paths.alias_parts({:__aliases__, [], parts}, context.rust_modules) ++ [function] + Paths.path_callable_argument_types(path, function, arity, callables, context.rust_modules) end, method_argument_types: fn target, function, arity -> - callable_argument_types(callables, target, function, arity) + Paths.callable_argument_types(callables, target, function, arity) end, - target_type: &callable_target_from_type/1, + target_type: &Paths.callable_target_from_type/1, method_receiver_type: &method_receiver_type(&1, &2, callables), struct_type: &declared_struct_type(&1, context) } @@ -4052,203 +2879,13 @@ defmodule RustQ.Meta.Lower do defp method_receiver_type(function, arity, %BindingIndex{} = callables) do case BindingIndex.method_targets(callables, function, arity) do - [target] -> type_for_callable_target(target) + [target] -> Paths.type_for_callable_target(target) _ambiguous_or_missing -> nil end end - defp type_for_callable_target(target) when is_binary(target) do - ast = - case callable_target_parts(target) do - [_ | _] = parts -> %AST.TypePath{parts: parts} - nil -> %AST.TypeRaw{source: target} - end - - %Type{kind: :type, rust: target, ast: ast} - end - - defp callable_target_parts(target) do - parts = String.split(target, "::") - - if Enum.all?(parts, &simple_rust_identifier?/1) do - Enum.map(parts, &Identifier.atom!/1) - end - end - - defp simple_rust_identifier?(<>) when first in ?A..?Z or first in ?a..?z, - do: simple_rust_identifier_tail?(rest) - - defp simple_rust_identifier?(<<"_", rest::binary>>), do: simple_rust_identifier_tail?(rest) - defp simple_rust_identifier?(""), do: false - - defp simple_rust_identifier_tail?(<<>>), do: true - - defp simple_rust_identifier_tail?(<>) - when char in ?A..?Z or char in ?a..?z or char in ?0..?9, - do: simple_rust_identifier_tail?(rest) - - defp simple_rust_identifier_tail?(<<"_", rest::binary>>), do: simple_rust_identifier_tail?(rest) - defp simple_rust_identifier_tail?(_invalid), do: false - - defp infer_mutability(body) do - mutable_vars = body |> collect_mutable_let_refs() |> MapSet.new() - - Walk.postwalk(body, fn - %AST.Let{pattern: %AST.PatVar{name: name}, expr: %AST.Ref{}} = let -> - %{let | mutable: rebound_or_borrowed?(body, name)} - - %AST.Let{pattern: %AST.PatVar{name: name}} = let -> - %{let | mutable: MapSet.member?(mutable_vars, name)} - - %AST.Let{pattern: %AST.PatTuple{} = pattern} = let -> - %{let | pattern: mark_mutable_pattern_vars(pattern, mutable_vars)} - - %AST.Closure{args: args, body: closure_body} = closure -> - %{closure | args: mutable_closure_args(args, closure_body)} - - other -> - other - end) - end - - # A closure argument its body changes, such as a fold accumulator, is bound `mut`. - defp mutable_closure_args(args, body) do - mutated = body |> collect_mutable_let_refs() |> MapSet.new() - - Enum.map(args, fn - name when is_atom(name) -> - if MapSet.member?(mutated, name), do: %AST.PatVar{name: name, mutable: true}, else: name - - pattern -> - pattern - end) - end - - # The variable a statement-level method chain starts from, when its first method changes - # its receiver, as in `map.entry(key).or_default().push(value)`. - defp mutated_chain_root(%AST.MethodCall{receiver: %AST.Var{name: name}, method: method}), - do: if(mutating_method?(method), do: name) - - defp mutated_chain_root(%AST.MethodCall{receiver: receiver}), do: mutated_chain_root(receiver) - defp mutated_chain_root(_expression), do: nil - - @doc """ - The variables a lowered body changes in place through a statement calling a mutating - method on them, as `scores.insert(name, 1);` does where `scores` was rebound to its update. - """ - @spec mutated_variables([struct()]) :: MapSet.t(atom()) - def mutated_variables(body) do - body - |> Walk.reduce([], fn - %AST.ExprStmt{expr: %AST.MethodCall{receiver: %AST.Var{name: name}, method: method}}, acc -> - if mutating_method?(method), do: [name | acc], else: acc - - _node, acc -> - acc - end) - |> MapSet.new() - end - - @doc "Whether a method changes its receiver, as std collections' `&mut self` methods do." - @spec mutating_method?(atom()) :: boolean() - def mutating_method?(method), - do: method in @mutating_methods or String.ends_with?(Atom.to_string(method), "_mut") - - defp rebound_or_borrowed?(body, name) do - Walk.reduce(body, false, fn - %AST.Assign{target: %AST.Var{name: ^name}}, _ -> true - %AST.Ref{mutable: true, expr: %AST.Var{name: ^name}}, _ -> true - _, found -> found - end) - end - - defp collect_pattern_mutations(term, context) do - Walk.reduce(term, collect_mut_refs(term), fn - %AST.ExprStmt{ - expr: %AST.MethodCall{receiver: %AST.Var{name: name}, method: method, args: args} - }, - acc -> - if mutable_method?(context, name, method, length(args)), do: [name | acc], else: acc - - _, acc -> - acc - end) - end - - defp mutable_method?(context, name, method, arity) do - targets = - case Map.get(context.vars, name) do - %Type{} = type -> - case type |> StandardPointer.receiver() |> Type.callable_target() do - nil -> [] - target -> [target] - end - - nil -> - [] - end - - targets != [] and - Enum.all?(targets, fn target -> - match?( - %{args: [%{type: %Type{kind: :mut_ref}} | _]}, - BindingIndex.get(context.callables, target, method, arity) - ) - end) - end - - defp collect_mut_refs(term) do - Walk.reduce(term, [], fn - %AST.Ref{mutable: true, expr: %AST.Var{name: name}}, acc -> [name | acc] - _other, acc -> acc - end) - end - - defp collect_mutable_let_refs(term) do - Walk.reduce(term, [], fn - %AST.ExprStmt{expr: %AST.MethodCall{receiver: %AST.Var{name: name}}}, acc -> - [name | acc] - - %AST.ExprStmt{expr: %AST.MethodCall{} = call}, acc -> - case mutated_chain_root(call) do - nil -> acc - name -> [name | acc] - end - - %AST.Assign{target: %AST.Var{name: name}}, acc -> - [name | acc] - - %AST.AssignOp{target: %AST.Var{name: name}}, acc -> - [name | acc] - - %AST.Assign{target: %AST.Index{receiver: %AST.Var{name: name}}}, acc -> - [name | acc] - - %AST.AssignOp{target: %AST.Index{receiver: %AST.Var{name: name}}}, acc -> - [name | acc] - - %AST.Assign{target: %AST.Field{receiver: %AST.Var{name: name}}}, acc -> - [name | acc] - - %AST.AssignOp{target: %AST.Field{receiver: %AST.Var{name: name}}}, acc -> - [name | acc] - - %AST.Ref{mutable: true, expr: %AST.Var{name: name}}, acc -> - [name | acc] - - # A changing method used as a value, as in `if seen.insert(key)`. - %AST.MethodCall{} = call, acc -> - case mutated_chain_root(call) do - nil -> acc - name -> [name | acc] - end - - _other, acc -> - acc - end) - end - - defp typing_env(%Context{} = context) do + @doc "The `RustQ.Meta.Typing` environment of a lowering context." + def typing_env(%Context{} = context) do Typing.env( type_aliases: context.type_aliases, vars: context.vars, @@ -4257,181 +2894,8 @@ defmodule RustQ.Meta.Lower do ) end - defp alias_ast?({:__aliases__, _, _parts}), do: true - defp alias_ast?(_other), do: false - - defp alias_path_ast?({:__aliases__, _, _parts}), do: true - - defp alias_path_ast?({{:., _, [receiver, _field]}, meta, []}), - do: Keyword.get(meta, :no_parens, false) and alias_path_ast?(receiver) - - defp alias_path_ast?(_other), do: false - - defp super_alias_ast?({:__aliases__, _, [:Super]}), do: true - defp super_alias_ast?(_other), do: false - - defp rust_constructor_alias?({:__aliases__, _, [module]}) when module in [:Stmt], do: true - defp rust_constructor_alias?(_other), do: false - - defp rust_variant(name), do: Identifier.atom!(Macro.camelize(Atom.to_string(name))) - - defp alias_parts({:__aliases__, _, parts}, rust_modules), - do: mapped_alias_parts(parts, rust_modules) - - defp alias_path_parts(ast, rust_modules), - do: ast |> raw_alias_path_parts() |> mapped_alias_parts(rust_modules) - - defp raw_alias_path_parts(ast), do: raw_alias_path_parts(ast, []) - - defp raw_alias_path_parts({:__aliases__, _, parts}, acc) do - parts - |> Enum.reverse() - |> Enum.reduce(acc, &[&1 | &2]) - end - - defp raw_alias_path_parts({{:., _, [receiver, field]}, meta, []} = ast, acc) do - if Keyword.get(meta, :no_parens, false) do - raw_alias_path_parts(receiver, [field | acc]) - else - Diagnostic.lower(:unsupported_alias_path, ast, "unsupported alias path") - end - end - - defp mapped_alias_parts(parts, rust_modules) do - parts - |> alias_prefixes() - |> Enum.find_value(fn {prefix, suffix} -> - mapped = Map.get(rust_modules, prefix) - if mapped, do: mapped ++ suffix - end) || automatic_rust_alias_parts(parts) - end - - defp alias_prefixes(parts) do - parts - |> length() - |> Range.new(1, -1) - |> Enum.map(fn count -> Enum.split(parts, count) end) - end - - defp automatic_rust_alias_parts(parts) do - if rust_module_alias?(parts) do - Enum.map(parts, &rust_module_part/1) - else - parts - end - end - - defp rust_module_alias?(parts) do - parts - |> List.last() - |> Atom.to_string() - |> String.ends_with?("s") - end - - defp rust_module_part(part), - do: Identifier.atom!(Macro.underscore(Atom.to_string(part))) - - defp callable_return_type_from_index( - {name, _meta, args}, - %BindingIndex{} = callables, - _rust_modules, - _vars - ) - when is_atom(name) and is_list(args) do - BindingIndex.return_type(callables, nil, name, length(args)) - end - - defp callable_return_type_from_index( - {{:., _, [{:__aliases__, _, parts}, function]}, _meta, args}, - %BindingIndex{} = callables, - rust_modules, - _vars - ) - when is_atom(function) and is_list(args) do - arity = length(args) - - callable_return_type_for_path(parts, function, arity, callables, rust_modules) - end - - defp callable_return_type_from_index( - {{:., _, [receiver, function]}, _meta, args}, - %BindingIndex{} = callables, - _rust_modules, - vars - ) - when is_atom(function) and is_list(args) do - target = - receiver - |> infer_expr_type(vars) - |> callable_target_from_type() - - BindingIndex.return_type(callables, target, function, length(args)) - end - - defp callable_return_type_from_index(_call_ast, %BindingIndex{}, _rust_modules, _vars), do: nil - - defp callable_return_type_for_path( - parts, - function, - arity, - callables, - rust_modules - ) do - parent_module_callable_return_type(parts, function, arity, callables) || - parts - |> callable_target_candidates(rust_modules) - |> Enum.find_value(&BindingIndex.return_type(callables, &1, function, arity)) || - path_module_fallback_return_type(parts, function, arity, callables) - end - - defp exact_callable_target_candidates(parts, rust_modules) do - mapped = mapped_alias_parts(parts, rust_modules) - - [ - Enum.map_join(mapped, "::", &to_string/1), - mapped |> List.last() |> to_string(), - Enum.map_join(parts, "::", &to_string/1), - parts |> List.last() |> to_string() - ] - |> Enum.uniq() - end - - defp callable_target_candidates(parts, rust_modules) do - mapped = mapped_alias_parts(parts, rust_modules) - mapped_last = List.last(mapped) - parts_last = List.last(parts) - - [ - Enum.map_join(mapped, "::", &to_string/1), - to_string(mapped_last), - singular_candidate(mapped_last), - singular_module_candidate(mapped_last), - Enum.map_join(parts, "::", &to_string/1), - to_string(parts_last), - singular_candidate(parts_last), - singular_module_candidate(parts_last) - ] - |> Enum.reject(&is_nil/1) - |> Enum.uniq() - end - - defp singular_candidate(part) when is_atom(part), - do: part |> to_string() |> singular_candidate() - - defp singular_candidate(part) when is_binary(part) do - if String.ends_with?(part, "s") and String.length(part) > 1 do - String.trim_trailing(part, "s") - end - end - - defp singular_candidate(_part), do: nil - - defp singular_module_candidate(part) do - case singular_candidate(part) do - nil -> nil - singular -> Macro.camelize(singular) - end - end + @doc "The Rust variant name an Elixir atom names, in Rust case." + def rust_variant(name), do: Identifier.atom!(Macro.camelize(Atom.to_string(name))) defp lower_array_value( {:repeat, _, [{group, _, ast_context}, [do: expression]]}, diff --git a/lib/rustq/meta/lower/comprehension.ex b/lib/rustq/meta/lower/comprehension.ex new file mode 100644 index 0000000..3512aa4 --- /dev/null +++ b/lib/rustq/meta/lower/comprehension.ex @@ -0,0 +1,468 @@ +defmodule RustQ.Meta.Lower.Comprehension do + @moduledoc """ + Comprehensions, as values and as loops. + + A comprehension used as a value is an iterator pipeline: each generator filters and binds + its items in one `filter_map`, later generators nest through `flat_map`, and the result is + collected into the type expected, or the collection `into:` names. Run for its effects, a + comprehension is nested `for` loops, and `for ..., reduce: acc` is a loop updating `acc`. + """ + + alias RustQ.Diagnostic + alias RustQ.Meta.Lower + alias RustQ.Meta.Lower.Env, as: Context + alias RustQ.Meta.Lower.Pattern, as: LowerPattern + alias RustQ.Meta.Pattern + alias RustQ.Meta.StdMethods + alias RustQ.Meta.Type + alias RustQ.Meta.Typing + alias RustQ.Rust.AST + + @any_vec %AST.TypeVec{inner: %AST.TypeRaw{source: "_"}} + @iterator_methods StdMethods.iterator() + + @doc "Lowers a comprehension used as a value, collecting into `expected_type` when known." + # A comprehension with any generators, filters, and refutable patterns, lowered through an + # iterator pipeline: each generator filters and binds in one `filter_map`, its filters as the + # match guard; later generators nest through `flat_map`, collecting each inner pipeline so + # its closures never outlive the outer item they borrow. + def lower_comprehension(expression, expected_type, %Context{} = context) do + {qualifiers, opts} = comprehension_parts(expression) + + if Keyword.has_key?(opts, :reduce) do + Diagnostic.lower( + :unsupported_for_reduce, + expression, + "unsupported defrust for/reduce expression", + suggestion: "Use `for pattern <- enumerable, reduce: initial do acc_pattern -> body end`." + ) + end + + body = + typed_body( + Keyword.fetch!(opts, :do), + expected_type && Lower.collection_item_type(expected_type) + ) + + pipeline = + quote( + do: + unquote(comprehension_pipeline(qualifiers, body, context, {false, []})) + |> collect() + ) + + case expected_type do + %Type{} = type -> + lowered = Lower.lower_expr(pipeline, type, context) + + cond do + Lower.slice_type?(type) -> collect_as(lowered, @any_vec) + context.position == :statement -> collect_as(lowered, type.ast) + true -> lowered + end + + nil -> + pipeline |> Lower.lower_expr(context) |> collect_as(into_target(opts[:into])) + end + end + + # Without an expected type, `into:` names the collection: `HashMap.new()`, `%{}`, + # `Map.new()`, `HashSet.new()`, or `MapSet.new()`; otherwise a list, as in Elixir. + defp into_target({:%{}, _, []}), do: inferred_collection(:HashMap, 2) + + defp into_target({{:., _, [{:__aliases__, _, [module]}, :new]}, _, []}) + when module in [:Map, :HashMap, :BTreeMap], + do: inferred_collection(if(module == :BTreeMap, do: :BTreeMap, else: :HashMap), 2) + + defp into_target({{:., _, [{:__aliases__, _, [module]}, :new]}, _, []}) + when module in [:MapSet, :HashSet, :BTreeSet], + do: inferred_collection(if(module == :BTreeSet, do: :BTreeSet, else: :HashSet), 1) + + defp into_target(_into), do: @any_vec + + defp inferred_collection(collection, arity) do + %AST.TypePath{ + parts: [collection], + generics: List.duplicate(%AST.TypeRaw{source: "_"}, arity) + } + end + + @doc "A `collect()` given the type it collects into, when Rust cannot infer it." + # A comprehension bound in a statement states what it collects into, since Rust may only + # learn it from a later borrow; without a collection, it makes a list, as in Elixir. + def collect_as(lowered, nil), do: lowered + + def collect_as(%AST.MethodCall{method: :collect, generics: []} = call, type), + do: %{call | generics: [type]} + + def collect_as(lowered, _type), do: lowered + + @doc "The type a binding collects into: its own, or a list of what Rust infers for a slice." + # Without a known type, a binding collects a list, as Elixir's enumerables do. + def collect_target(%Type{ast: ast} = type), + do: if(Lower.slice_type?(type), do: @any_vec, else: ast) + + def collect_target(nil), do: @any_vec + + @doc "A comprehension split into its qualifiers and its options, `into:`, `reduce:`, and `do:`." + def comprehension_parts({:for, _, args}) do + {opts, qualifiers} = args |> Enum.reverse() |> Enum.split_while(&Keyword.keyword?/1) + {Enum.reverse(qualifiers), opts |> Enum.reverse() |> Enum.concat()} + end + + # `nested?` says whether earlier generators enclose this one, and `generated` names the + # variables they bound. + defp comprehension_pipeline( + [{:<-, _, [pattern, collection]} | rest], + body, + context, + {nested?, generated} + ) do + {filters, rest} = Enum.split_while(rest, &(not generator?(&1))) + item_type = comprehension_item_type(collection, context) + root = root_variable(collection) + + reused? = + root != nil and + ((nested? and root not in generated) or + (item_type != nil and root not in generated and + Lower.in_use?(collection, context))) + + {stage, binder} = generator_stage(pattern, collection, filters, item_type, reused?) + bound = LowerPattern.context_with_match_pattern(pattern, item_type, context) + + generated = LowerPattern.binding_pattern_names(pattern) ++ generated + + binder_type = binder_type(binder, bound) + + case rest do + [] -> + comprehension_map(stage, binder, binder_type, body) + + # A generator over one value, as `{:some, id} <- [record_id(record)]`, matches that value + # beside the item instead of iterating a list made for it. + [{:<-, meta, [value_pattern, [value]]} | later] -> + paired = + quote( + do: + unquote(stage) + |> map(fn unquote(typed_arg(binder, binder_type)) -> + {unquote(binder), unquote(value)} + end) + ) + + comprehension_pipeline( + [{:<-, meta, [{binder, value_pattern}, paired]} | later], + body, + bound, + {nested?, generated} + ) + + _ -> + quote( + do: + unquote(stage) + |> flat_map(fn unquote(typed_arg(binder, binder_type)) -> + Vec.from_iter(unquote(comprehension_pipeline(rest, body, bound, {true, generated}))) + end) + ) + end + end + + defp comprehension_pipeline(qualifiers, _body, _context, _generated) do + Diagnostic.lower( + :unsupported_comprehension, + qualifiers, + "unsupported defrust comprehension", + suggestion: "Start the comprehension with a `pattern <- enumerable` generator." + ) + end + + # A comprehension body lowers as the item type of the collection it builds, when known. + defp typed_body(body, %Type{} = item_type), do: {:__rustq_typed__, [], [body, item_type]} + + defp typed_body(body, _item_type), do: body + + defp untyped({:__rustq_typed__, _, [body, _type]}), do: body + + defp untyped(body), do: body + + # A generator over a list gives its closures the list's items, which they may then use as + # that type; other collections leave their items untyped. + defp comprehension_item_type(collection, %Context{} = context) do + unless iterator_call?(collection) do + case Lower.infer_expr_type(collection, context.vars) || + Typing.synth(collection, Lower.typing_env(context)) do + %Type{kind: :vec} = type -> Type.vec_inner(type) + _unknown -> nil + end + end + end + + # The type of the variables a generator binds, as one variable or a tuple of them. + defp binder_type({name, _, ast_context}, %Context{vars: vars}) when is_atom(ast_context), + do: Map.get(vars, name) + + defp binder_type(binder, %Context{} = context) do + with [_ | _] = elements <- Pattern.tuple_elements(binder), + types = Enum.map(elements, &binder_type(&1, context)), + true <- Enum.all?(types, &match?(%Type{}, &1)) do + Type.tuple(types) + else + _unknown -> nil + end + end + + defp typed_arg(arg, %Type{} = type), do: {:__rustq_typed_arg__, [], [arg, type]} + + defp typed_arg(arg, nil), do: arg + + # An item the body returns as it is needs no mapping, and one it clones is `cloned()`. + defp comprehension_map(stage, binder, binder_type, body) do + plain = untyped(body) + + cond do + same_variables?(binder, plain) -> + stage + + match?({{:., _, [_, :clone]}, _, []}, plain) and + same_variables?(binder, plain |> elem(0) |> elem(2) |> hd()) -> + quote(do: unquote(stage) |> cloned()) + + true -> + binder = binder |> unused_as_underscored(body) |> typed_arg(binder_type) + quote(do: unquote(stage) |> map(fn unquote(binder) -> unquote(body) end)) + end + end + + # A binder variable the body does not use is ignored. + defp unused_as_underscored(binder, body) do + {_body, used} = + Macro.prewalk(body, MapSet.new(), fn + {name, _, context} = node, used when is_atom(name) and is_atom(context) -> + {node, MapSet.put(used, name)} + + node, used -> + {node, used} + end) + + Macro.prewalk(binder, fn + {name, meta, context} = node when is_atom(name) and is_atom(context) -> + if MapSet.member?(used, name), do: node, else: {:_, meta, context} + + node -> + node + end) + end + + defp same_variables?({name, _, context}, {name, _, context}) when is_atom(context), do: true + + defp same_variables?(left, right) do + case {Pattern.tuple_elements(left), Pattern.tuple_elements(right)} do + {lefts, rights} + when is_list(lefts) and is_list(rights) and length(lefts) == length(rights) -> + lefts |> Enum.zip_with(rights, &same_variables?/2) |> Enum.all?() + + _different -> + false + end + end + + defp generator?({:<-, _, [_pattern, _collection]}), do: true + + defp generator?(_qualifier), do: false + + # A generator that cannot fail and has no filters iterates as it is; otherwise its items are + # matched, filtered, and reduced to the variables the pattern binds. + # A later generator over a collection from outside the comprehension iterates it once per + # item of the earlier ones, so it copies its items rather than consuming it, as does a + # generator over a collection still in use or reached through a reference. + defp generator_stage(pattern, collection, filters, item_type, reused?) do + items = + cond do + iterator_call?(collection) -> collection + reused? -> quote(do: unquote(collection) |> iter() |> cloned()) + true -> quote(do: unquote(collection) |> into_iter()) + end + + if filters == [] and LowerPattern.binding_pattern?(pattern) do + {items, pattern} + else + binder = LowerPattern.pattern_binder(pattern) + + {filter_bind(items, LowerPattern.guarded_pattern(pattern, filters), binder, item_type), + binder} + end + end + + defp root_variable({name, _, ast_context}) when is_atom(name) and is_atom(ast_context), do: name + + # The variable a collection is, or is a field of; a call or literal makes a fresh one. + defp root_variable({{:., _, [receiver, _field]}, meta, []}), + do: if(meta[:no_parens], do: root_variable(receiver)) + + defp root_variable(_collection), do: nil + + # A collection already given as an iterator, as in `values.iter()`, is iterated as it is. + defp iterator_call?({{:., _, [_receiver, method]}, _, args}) when is_list(args), + do: method in @iterator_methods + + defp iterator_call?({:|>, _, [_left, {method, _, args}]}) when is_list(args), + do: method in @iterator_methods + + defp iterator_call?(_collection), do: false + + defp filter_bind(items, guarded, binder, item_type) do + item = quote(do: item) + + quote do + unquote(items) + |> filter_map(fn unquote(typed_arg(item, item_type)) -> + case unquote(item) do + unquote(guarded) -> some(unquote(binder)) + _ -> nil + end + end) + end + end + + @doc "Lowers a comprehension run for its effects, as nested `for` loops." + # A comprehension evaluated for its effects: nested loops, `if let` for patterns that can fail, + # and `if` for filters, so the body may return early. + def lower_for_loop([{:<-, _, [pattern, collection]} | rest], body, %Context{} = context) do + body_context = LowerPattern.context_with_for_pattern(pattern, collection, context) + + if LowerPattern.binding_pattern?(pattern) do + %AST.For{ + pattern: LowerPattern.lower_binding_pattern(pattern), + expr: Lower.lower_expr(collection, context), + body: lower_for_loop_body(rest, body, body_context) + } + else + %AST.For{ + pattern: %AST.PatVar{name: :__rustq_item}, + expr: Lower.lower_expr(collection, context), + body: [ + %AST.IfLet{ + pattern: LowerPattern.lower_match_pattern(pattern, nil, context), + expr: %AST.Var{name: :__rustq_item}, + then: + lower_for_loop_body( + rest, + body, + LowerPattern.context_with_match_pattern(pattern, nil, context) + ) + } + ] + } + end + end + + defp lower_for_loop_body([], body, %Context{} = context), + do: Lower.lower_clause_body(body, context) + + defp lower_for_loop_body([{:<-, _, _} | _] = qualifiers, body, %Context{} = context), + do: [lower_for_loop(qualifiers, body, context)] + + defp lower_for_loop_body([filter | rest], body, %Context{} = context) do + [ + %AST.ExprStmt{ + expr: %AST.If{ + condition: Lower.lower_expr(filter, context), + then: lower_for_loop_body(rest, body, context) + } + } + ] + end + + @doc "Lowers `for ..., reduce: acc` as a loop updating `acc`." + def lower_for_reduce_expr(expression, %Context{return_type: %Type{} = return_type} = context), + do: lower_for_reduce_expr(expression, return_type, context) + + def lower_for_reduce_expr( + {:for, _, [{:<-, _, [pattern, expression]}, [reduce: initial], [do: clauses]]}, + %Type{} = return_type, + %Context{} = context + ) do + acc = :__rustq_reduce + acc_type = reduce_acc_type(initial, return_type) + + %AST.BlockExpr{ + body: [ + %AST.Let{ + pattern: %AST.PatVar{name: acc}, + mutable: true, + expr: lower_reduce_initial(initial, acc_type, context) + }, + %AST.For{ + pattern: LowerPattern.lower_binding_pattern(pattern), + expr: Lower.lower_expr(expression, context), + body: [ + %AST.Assign{ + target: %AST.Var{name: acc}, + expr: %AST.Match{ + expr: %AST.Var{name: acc}, + arms: lower_for_reduce_arms(clauses, acc_type, context) + } + } + ] + }, + %AST.Return{expr: %AST.Var{name: acc}} + ] + } + end + + def lower_for_reduce_expr(other, _return_type, _context) do + Diagnostic.lower( + :unsupported_for_reduce, + other, + "unsupported defrust for/reduce expression", + suggestion: "Use `for pattern <- enumerable, reduce: initial do acc_pattern -> body end`." + ) + end + + defp lower_reduce_initial(initial, %Type{} = acc_type, %Context{} = context) do + if Type.propagates?(acc_type) do + Lower.lower_return_expr(initial, acc_type, context) + else + Lower.lower_expr(initial, acc_type, context) + end + end + + defp reduce_acc_type(:ok, %Type{kind: :nif_result}) do + unit = %Type{kind: :unit, rust: "()", ast: %AST.TypeUnit{}} + + %Type{ + kind: :nif_result, + rust: "NifResult<()>", + ast: %AST.TypeNifResult{inner: unit.ast}, + meta: %{inner: unit} + } + end + + defp reduce_acc_type(_initial, %Type{} = return_type), do: return_type + + defp lower_for_reduce_arms(clauses, %Type{} = return_type, %Context{} = context) do + carry = :__rustq_reduce_value + + Enum.map(clauses, fn {:->, _, [[pattern], body]} -> + body_context = LowerPattern.context_with_match_pattern(pattern, return_type, context) + + %AST.Arm{ + pattern: LowerPattern.lower_match_pattern(pattern, return_type, context), + body: + Lower.lower_clause_body( + body, + %{body_context | position: :return, return_type: return_type}, + return_type + ) + } + end) ++ + [ + %AST.Arm{ + pattern: %AST.PatVar{name: carry}, + body: [%AST.Return{expr: %AST.Var{name: carry}}] + } + ] + end +end diff --git a/lib/rustq/meta/lower/mutability.ex b/lib/rustq/meta/lower/mutability.ex new file mode 100644 index 0000000..7b5294d --- /dev/null +++ b/lib/rustq/meta/lower/mutability.ex @@ -0,0 +1,213 @@ +defmodule RustQ.Meta.Lower.Mutability do + @moduledoc """ + Which variables lowered Rust changes, so their bindings are `mut`: a `let` whose value a later + statement changes in place, a closure argument its body changes, and an owned argument a + rebinding updates in place, as `scores = Map.put(scores, name, 1)` becomes + `scores.insert(name, 1);`. + """ + + alias RustQ.Meta.StdMethods + alias RustQ.Rust.AST + alias RustQ.Rust.AST.Walk + + @mutating_methods StdMethods.mutating() + + @doc """ + A binding of a collection to its update, `map = Map.put(map, key, value)`, as the in-place + change of that collection, `map.insert(key, value);`, where the update takes the collection + at its last use; any other statement unchanged. + """ + @spec updated_in_place(AST.stmt()) :: AST.stmt() + def updated_in_place( + %AST.Let{ + pattern: %AST.PatVar{name: name}, + expr: %AST.BlockExpr{ + body: [ + %AST.Let{pattern: %AST.PatVar{name: :rustq_updated}, expr: %AST.Var{name: name}} + | rest + ] + } + } = let + ) do + case Enum.split(rest, -1) do + {[_ | _] = updates, [%AST.Return{expr: %AST.Var{name: :rustq_updated}}]} -> + case rename_var(updates, :rustq_updated, name) do + [update] -> update + updates -> %AST.ExprStmt{expr: %AST.BlockExpr{body: updates}} + end + + _other -> + let + end + end + + def updated_in_place(statement), do: statement + + defp rename_var(%AST.Var{name: from}, from, to), do: %AST.Var{name: to} + + defp rename_var(%module{} = node, from, to) do + fields = + node + |> Map.from_struct() + |> Map.new(fn {key, value} -> {key, rename_var(value, from, to)} end) + + struct(module, fields) + end + + defp rename_var(values, from, to) when is_list(values), + do: Enum.map(values, &rename_var(&1, from, to)) + + defp rename_var(value, _from, _to), do: value + + @doc "Marks the variables of `pattern` in `mutable_vars` `mut`." + @spec mark_mutable_pattern_vars(AST.pat(), MapSet.t(atom())) :: AST.pat() + def mark_mutable_pattern_vars(%AST.PatVar{name: name} = pattern, mutable_vars) do + if MapSet.member?(mutable_vars, name), do: %{pattern | mutable: true}, else: pattern + end + + def mark_mutable_pattern_vars(%AST.PatSome{pattern: pattern} = some, mutable_vars), + do: %{some | pattern: mark_mutable_pattern_vars(pattern, mutable_vars)} + + def mark_mutable_pattern_vars(%AST.PatOk{pattern: pattern} = ok, mutable_vars), + do: %{ok | pattern: mark_mutable_pattern_vars(pattern, mutable_vars)} + + def mark_mutable_pattern_vars(%AST.PatErr{pattern: pattern} = err, mutable_vars), + do: %{err | pattern: mark_mutable_pattern_vars(pattern, mutable_vars)} + + def mark_mutable_pattern_vars(%AST.PatTuple{patterns: patterns} = tuple, mutable_vars), + do: %{tuple | patterns: Enum.map(patterns, &mark_mutable_pattern_vars(&1, mutable_vars))} + + def mark_mutable_pattern_vars(%AST.PatPathTuple{patterns: patterns} = tuple, mutable_vars), + do: %{tuple | patterns: Enum.map(patterns, &mark_mutable_pattern_vars(&1, mutable_vars))} + + def mark_mutable_pattern_vars(%AST.PatStruct{fields: fields} = struct, mutable_vars) do + fields = + Enum.map(fields, fn {name, pattern} -> + {name, mark_mutable_pattern_vars(pattern, mutable_vars)} + end) + + %{struct | fields: fields} + end + + def mark_mutable_pattern_vars(pattern, _mutable_vars), do: pattern + + @doc "Marks the `let`s and closure arguments of a lowered body that it changes `mut`." + @spec infer_mutability([AST.stmt()]) :: [AST.stmt()] + def infer_mutability(body) do + mutable_vars = body |> collect_mutable_let_refs() |> MapSet.new() + + Walk.postwalk(body, fn + %AST.Let{pattern: %AST.PatVar{name: name}, expr: %AST.Ref{}} = let -> + %{let | mutable: rebound_or_borrowed?(body, name)} + + %AST.Let{pattern: %AST.PatVar{name: name}} = let -> + %{let | mutable: MapSet.member?(mutable_vars, name)} + + %AST.Let{pattern: %AST.PatTuple{} = pattern} = let -> + %{let | pattern: mark_mutable_pattern_vars(pattern, mutable_vars)} + + %AST.Closure{args: args, body: closure_body} = closure -> + %{closure | args: mutable_closure_args(args, closure_body)} + + other -> + other + end) + end + + # A closure argument its body changes, such as a fold accumulator, is bound `mut`. + defp mutable_closure_args(args, body) do + mutated = body |> collect_mutable_let_refs() |> MapSet.new() + + Enum.map(args, fn + name when is_atom(name) -> + if MapSet.member?(mutated, name), do: %AST.PatVar{name: name, mutable: true}, else: name + + pattern -> + pattern + end) + end + + # The variable a statement-level method chain starts from, when its first method changes + # its receiver, as in `map.entry(key).or_default().push(value)`. + defp mutated_chain_root(%AST.MethodCall{receiver: %AST.Var{name: name}, method: method}), + do: if(mutating_method?(method), do: name) + + defp mutated_chain_root(%AST.MethodCall{receiver: receiver}), do: mutated_chain_root(receiver) + + defp mutated_chain_root(_expression), do: nil + + @doc """ + The variables a lowered body changes in place through a statement calling a mutating + method on them, as `scores.insert(name, 1);` does where `scores` was rebound to its update. + """ + + @spec mutated_variables([struct()]) :: MapSet.t(atom()) + def mutated_variables(body) do + body + |> Walk.reduce([], fn + %AST.ExprStmt{expr: %AST.MethodCall{receiver: %AST.Var{name: name}, method: method}}, acc -> + if mutating_method?(method), do: [name | acc], else: acc + + _node, acc -> + acc + end) + |> MapSet.new() + end + + @doc "Whether a method changes its receiver, as std collections' `&mut self` methods do." + @spec mutating_method?(atom()) :: boolean() + def mutating_method?(method), + do: method in @mutating_methods or String.ends_with?(Atom.to_string(method), "_mut") + + defp rebound_or_borrowed?(body, name) do + Walk.reduce(body, false, fn + %AST.Assign{target: %AST.Var{name: ^name}}, _ -> true + %AST.Ref{mutable: true, expr: %AST.Var{name: ^name}}, _ -> true + _, found -> found + end) + end + + defp collect_mutable_let_refs(term) do + Walk.reduce(term, [], fn + %AST.ExprStmt{expr: %AST.MethodCall{receiver: %AST.Var{name: name}}}, acc -> + [name | acc] + + %AST.ExprStmt{expr: %AST.MethodCall{} = call}, acc -> + case mutated_chain_root(call) do + nil -> acc + name -> [name | acc] + end + + %AST.Assign{target: %AST.Var{name: name}}, acc -> + [name | acc] + + %AST.AssignOp{target: %AST.Var{name: name}}, acc -> + [name | acc] + + %AST.Assign{target: %AST.Index{receiver: %AST.Var{name: name}}}, acc -> + [name | acc] + + %AST.AssignOp{target: %AST.Index{receiver: %AST.Var{name: name}}}, acc -> + [name | acc] + + %AST.Assign{target: %AST.Field{receiver: %AST.Var{name: name}}}, acc -> + [name | acc] + + %AST.AssignOp{target: %AST.Field{receiver: %AST.Var{name: name}}}, acc -> + [name | acc] + + %AST.Ref{mutable: true, expr: %AST.Var{name: name}}, acc -> + [name | acc] + + # A changing method used as a value, as in `if seen.insert(key)`. + %AST.MethodCall{} = call, acc -> + case mutated_chain_root(call) do + nil -> acc + name -> [name | acc] + end + + _other, acc -> + acc + end) + end +end diff --git a/lib/rustq/meta/lower/paths.ex b/lib/rustq/meta/lower/paths.ex new file mode 100644 index 0000000..26384a7 --- /dev/null +++ b/lib/rustq/meta/lower/paths.ex @@ -0,0 +1,355 @@ +defmodule RustQ.Meta.Lower.Paths do + @moduledoc """ + The Rust paths Elixir aliases name, and the callables a path or a receiver's type reaches. + + An alias a `defrustmod` maps is that Rust module, with what follows it; an alias ending in a + plural, as `Atoms`, is a Rust module in snake case; any other is a Rust type or module as + written. A call through a path or on a receiver finds its signature among the callables + RustQ knows, trying the targets the path may name. + """ + + alias RustQ.Binding.Index, as: BindingIndex + alias RustQ.Diagnostic + alias RustQ.Meta.Lower.Env, as: Context + alias RustQ.Meta.Type + alias RustQ.Rust.AST + alias RustQ.Rust.Identifier + + @doc """ + The return type of a local or remote call, from the callable metadata the `:callables` option + supplies, which tells lowering when a call returning `Result`, `Option`, or `NifResult` + lowers with Rust `?`. + """ + @spec callable_return_type(Macro.t(), keyword()) :: Type.t() | nil + def callable_return_type(call_ast, opts \\ []) do + callables = opts |> Keyword.get(:callables, []) |> BindingIndex.new() + rust_modules = Keyword.get(opts, :rust_modules, %{}) + vars = Keyword.get(opts, :vars, %{}) + + callable_return_type_from_index(call_ast, callables, rust_modules, vars) + end + + @doc "Whether alias `parts` end in an upper-case segment, as a constant such as `Kind.PLAIN`." + # A constant ends in an upper-case segment such as `Kind.PLAIN`. + def constant_alias?(parts) do + name = parts |> List.last() |> Atom.to_string() + name == String.upcase(name) and String.length(name) > 1 + end + + @doc "The argument types of the callable `function/arity` on `target` (nil for a function), from the callables RustQ knows." + def callable_argument_types(target, function, arity, %Context{callables: callables}) do + callable_argument_types(callables, target, function, arity) + end + + def callable_argument_types(%BindingIndex{} = callables, target, function, arity) do + BindingIndex.argument_types(callables, target, function, arity) + end + + @doc "The argument types of a function a Rust path names, trying each target the path may mean." + def path_callable_argument_types(path, function, arity, %Context{} = context) do + path_callable_argument_types(path, function, arity, context.callables, context.rust_modules) + end + + def path_callable_argument_types( + path, + function, + arity, + %BindingIndex{} = callables, + rust_modules + ) do + target_parts = Enum.drop(path, -1) + + parent_module_callable_argument_types(target_parts, function, arity, callables) || + target_parts + |> exact_callable_target_candidates(rust_modules) + |> Enum.find_value(&callable_argument_types(callables, &1, function, arity)) || + path_module_fallback_argument_types(target_parts, function, arity, callables) || + target_parts + |> callable_target_candidates(rust_modules) + |> Enum.find_value(&callable_argument_types(callables, &1, function, arity)) + end + + @doc "The argument types of a function `Super.function` names, in the module's parent." + def parent_module_callable_argument_types( + [:Super], + function, + arity, + %BindingIndex{} = callables + ), + do: + callable_argument_types(callables, nil, function, arity) || + BindingIndex.unqualified_argument_types(callables, function, arity) + + def parent_module_callable_argument_types(_target_parts, _function, _arity, %BindingIndex{}), + do: nil + + defp path_module_fallback_argument_types( + target_parts, + function, + arity, + %BindingIndex{} = callables + ) do + if rust_module_path?(target_parts) do + callable_argument_types(callables, nil, function, arity) + end + end + + defp parent_module_callable_return_type([:Super], function, arity, %BindingIndex{} = callables), + do: + BindingIndex.return_type(callables, nil, function, arity) || + BindingIndex.unqualified_return_type(callables, function, arity) + + defp parent_module_callable_return_type(_target_parts, _function, _arity, %BindingIndex{}), + do: nil + + defp path_module_fallback_return_type( + target_parts, + function, + arity, + %BindingIndex{} = callables + ) do + if rust_module_path?(target_parts) do + BindingIndex.return_type(callables, nil, function, arity) + end + end + + defp rust_module_path?([_ | _] = parts), + do: rust_module_name?(parts |> List.last() |> to_string()) + + defp rust_module_path?(_parts), do: false + + defp rust_module_name?(<>) when first in ?a..?z, do: true + + defp rust_module_name?(<<"_", _rest::binary>>), do: true + + defp rust_module_name?(_name), do: false + + @doc "The callable target, as a Rust type name, a receiver of `type` calls methods on." + def callable_target_from_type(%Type{} = type), do: Type.callable_target(type) + + def callable_target_from_type(_type), do: nil + + @doc "The type a callable target names." + def type_for_callable_target(target) when is_binary(target) do + ast = + case callable_target_parts(target) do + [_ | _] = parts -> %AST.TypePath{parts: parts} + nil -> %AST.TypeRaw{source: target} + end + + %Type{kind: :type, rust: target, ast: ast} + end + + defp callable_target_parts(target) do + parts = String.split(target, "::") + + if Enum.all?(parts, &simple_rust_identifier?/1) do + Enum.map(parts, &Identifier.atom!/1) + end + end + + @doc "Whether `ast` is an alias." + def alias_ast?({:__aliases__, _, _parts}), do: true + + def alias_ast?(_other), do: false + + @doc "Whether `ast` is an alias or a field path through one, as `Kinds.FANCY`." + def alias_path_ast?({:__aliases__, _, _parts}), do: true + + def alias_path_ast?({{:., _, [receiver, _field]}, meta, []}), + do: Keyword.get(meta, :no_parens, false) and alias_path_ast?(receiver) + + def alias_path_ast?(_other), do: false + + @doc "Whether `ast` is `Super`, the Rust parent module." + def super_alias_ast?({:__aliases__, _, [:Super]}), do: true + + def super_alias_ast?(_other), do: false + + @doc "Whether `ast` is an alias RustQ reserves for building Rust AST, as `Stmt`." + def rust_constructor_alias?({:__aliases__, _, [module]}) when module in [:Stmt], do: true + + def rust_constructor_alias?(_other), do: false + + @doc "The Rust path an alias names; see the module docs." + def alias_parts({:__aliases__, _, parts}, rust_modules), + do: mapped_alias_parts(parts, rust_modules) + + @doc "The Rust path an alias or field path through one names." + def alias_path_parts(ast, rust_modules), + do: ast |> raw_alias_path_parts() |> mapped_alias_parts(rust_modules) + + defp raw_alias_path_parts(ast), do: raw_alias_path_parts(ast, []) + + defp raw_alias_path_parts({:__aliases__, _, parts}, acc) do + parts + |> Enum.reverse() + |> Enum.reduce(acc, &[&1 | &2]) + end + + defp raw_alias_path_parts({{:., _, [receiver, field]}, meta, []} = ast, acc) do + if Keyword.get(meta, :no_parens, false) do + raw_alias_path_parts(receiver, [field | acc]) + else + Diagnostic.lower(:unsupported_alias_path, ast, "unsupported alias path") + end + end + + @doc "The Rust path alias `parts` name, through the longest `defrustmod` mapping of a prefix." + def mapped_alias_parts(parts, rust_modules) do + parts + |> alias_prefixes() + |> Enum.find_value(fn {prefix, suffix} -> + mapped = Map.get(rust_modules, prefix) + if mapped, do: mapped ++ suffix + end) || automatic_rust_alias_parts(parts) + end + + @doc "The ways to split alias `parts` into a prefix and the rest, longest prefix first." + def alias_prefixes(parts) do + parts + |> length() + |> Range.new(1, -1) + |> Enum.map(fn count -> Enum.split(parts, count) end) + end + + defp automatic_rust_alias_parts(parts) do + if rust_module_alias?(parts) do + Enum.map(parts, &rust_module_part/1) + else + parts + end + end + + defp rust_module_alias?(parts) do + parts + |> List.last() + |> Atom.to_string() + |> String.ends_with?("s") + end + + defp rust_module_part(part), + do: Identifier.atom!(Macro.underscore(Atom.to_string(part))) + + defp callable_return_type_from_index( + {name, _meta, args}, + %BindingIndex{} = callables, + _rust_modules, + _vars + ) + when is_atom(name) and is_list(args) do + BindingIndex.return_type(callables, nil, name, length(args)) + end + + defp callable_return_type_from_index( + {{:., _, [{:__aliases__, _, parts}, function]}, _meta, args}, + %BindingIndex{} = callables, + rust_modules, + _vars + ) + when is_atom(function) and is_list(args) do + arity = length(args) + + callable_return_type_for_path(parts, function, arity, callables, rust_modules) + end + + defp callable_return_type_from_index( + {{:., _, [receiver, function]}, _meta, args}, + %BindingIndex{} = callables, + _rust_modules, + vars + ) + when is_atom(function) and is_list(args) do + target = + receiver + |> variable_type(vars) + |> callable_target_from_type() + + BindingIndex.return_type(callables, target, function, length(args)) + end + + defp callable_return_type_from_index(_call_ast, %BindingIndex{}, _rust_modules, _vars), do: nil + + defp callable_return_type_for_path( + parts, + function, + arity, + callables, + rust_modules + ) do + parent_module_callable_return_type(parts, function, arity, callables) || + parts + |> callable_target_candidates(rust_modules) + |> Enum.find_value(&BindingIndex.return_type(callables, &1, function, arity)) || + path_module_fallback_return_type(parts, function, arity, callables) + end + + defp exact_callable_target_candidates(parts, rust_modules) do + mapped = mapped_alias_parts(parts, rust_modules) + + [ + Enum.map_join(mapped, "::", &to_string/1), + mapped |> List.last() |> to_string(), + Enum.map_join(parts, "::", &to_string/1), + parts |> List.last() |> to_string() + ] + |> Enum.uniq() + end + + defp callable_target_candidates(parts, rust_modules) do + mapped = mapped_alias_parts(parts, rust_modules) + mapped_last = List.last(mapped) + parts_last = List.last(parts) + + [ + Enum.map_join(mapped, "::", &to_string/1), + to_string(mapped_last), + singular_candidate(mapped_last), + singular_module_candidate(mapped_last), + Enum.map_join(parts, "::", &to_string/1), + to_string(parts_last), + singular_candidate(parts_last), + singular_module_candidate(parts_last) + ] + |> Enum.reject(&is_nil/1) + |> Enum.uniq() + end + + defp singular_candidate(part) when is_atom(part), + do: part |> to_string() |> singular_candidate() + + defp singular_candidate(part) when is_binary(part) do + if String.ends_with?(part, "s") and String.length(part) > 1 do + String.trim_trailing(part, "s") + end + end + + defp singular_candidate(_part), do: nil + + defp singular_module_candidate(part) do + case singular_candidate(part) do + nil -> nil + singular -> Macro.camelize(singular) + end + end + + defp variable_type({name, _, context}, vars) when is_atom(name) and is_atom(context), + do: Map.get(vars, name) + + defp variable_type(_expression, _vars), do: nil + + defp simple_rust_identifier?(<>) when first in ?A..?Z or first in ?a..?z, + do: simple_rust_identifier_tail?(rest) + + defp simple_rust_identifier?(<<"_", rest::binary>>), do: simple_rust_identifier_tail?(rest) + defp simple_rust_identifier?(""), do: false + + defp simple_rust_identifier_tail?(<<>>), do: true + + defp simple_rust_identifier_tail?(<>) + when char in ?A..?Z or char in ?a..?z or char in ?0..?9, + do: simple_rust_identifier_tail?(rest) + + defp simple_rust_identifier_tail?(<<"_", rest::binary>>), do: simple_rust_identifier_tail?(rest) + defp simple_rust_identifier_tail?(_invalid), do: false +end diff --git a/lib/rustq/meta/lower/pattern.ex b/lib/rustq/meta/lower/pattern.ex new file mode 100644 index 0000000..46f3d59 --- /dev/null +++ b/lib/rustq/meta/lower/pattern.ex @@ -0,0 +1,705 @@ +defmodule RustQ.Meta.Lower.Pattern do + @moduledoc """ + Elixir patterns as Rust patterns: in `case` clauses, `with` clauses, bindings, closure + arguments, and comprehension generators. + + Tuples, lists, structs, `{:some, value}`, `{:ok, value}`, `{:error, reason}`, atoms, and enum + variants lower to the Rust patterns matching the same values, by the type matched. Matching + binds the pattern's variables with the types it reveals, which the clause body lowers with. + """ + + alias RustQ.Binding.Index, as: BindingIndex + alias RustQ.Diagnostic + alias RustQ.Meta.Lower + alias RustQ.Meta.Lower.Env, as: Context + alias RustQ.Meta.Lower.Paths + alias RustQ.Meta.Pattern, as: ElixirPattern + alias RustQ.Meta.StandardPointer + alias RustQ.Meta.Type + alias RustQ.Meta.Typing + alias RustQ.Rust.AST + alias RustQ.Rust.AST.Walk + + @doc "Whether `case` clauses match `{:ok, _}`/`{:error, _}` or `{:some, _}`/`nil`." + def wrapper_case_patterns?(%Type{kind: kind}, clauses) when kind in [:result, :nif_result] do + Enum.any?(clauses, fn {:->, _, [[pattern], _body]} -> + pattern |> split_guarded_pattern() |> elem(0) |> result_pattern?() + end) + end + + def wrapper_case_patterns?(%Type{kind: :option}, clauses) do + Enum.any?(clauses, fn {:->, _, [[pattern], _body]} -> + pattern |> split_guarded_pattern() |> elem(0) |> option_pattern?() + end) + end + + def wrapper_case_patterns?(%Type{}, _clauses), do: false + + @doc "The Rust pattern a `with` clause pattern is." + # A `with` pattern matches what its expression gives; a bare variable binds all of it. + def lower_with_pattern({name, _, ast_context} = pattern, _type, %Context{} = context) + when is_atom(name) and is_atom(ast_context), + do: lower_match_pattern(pattern, nil, context) + + def lower_with_pattern(pattern, type, %Context{} = context), + do: lower_match_pattern(pattern, type, context) + + @doc "A pattern with its comprehension filters as its guard." + def guarded_pattern(pattern, []), do: pattern + + def guarded_pattern(pattern, filters) do + guard = + Enum.reduce(filters, fn filter, guard -> quote(do: unquote(guard) and unquote(filter)) end) + + quote(do: unquote(pattern) when unquote(guard)) + end + + @doc "Whether `pattern` only binds, so it cannot fail to match." + def binding_pattern?({name, _meta, context}) when is_atom(name) and is_atom(context), do: true + + def binding_pattern?(pattern) do + case ElixirPattern.tuple_elements(pattern) do + elements when is_list(elements) -> Enum.all?(elements, &binding_pattern?/1) + _not_tuple -> false + end + end + + @doc "The variables a pattern binds, as one variable or a tuple of them." + # The variables a pattern binds, as one variable or a tuple of them. + def pattern_binder(pattern) do + case pattern_variables(pattern) do + [] -> quote(do: {}) + [variable] -> variable + variables -> {:{}, [], variables} + end + end + + defp pattern_variables(pattern) do + {_pattern, variables} = + Macro.prewalk(pattern, [], fn + {:^, _meta, _args}, variables -> + {:pinned, variables} + + {name, meta, context} = variable, variables when is_atom(name) and is_atom(context) -> + if name |> Atom.to_string() |> String.starts_with?("_"), + do: {variable, variables}, + else: {variable, [{name, meta, context} | variables]} + + other, variables -> + {other, variables} + end) + + variables |> Enum.reverse() |> Enum.uniq_by(fn {name, _meta, context} -> {name, context} end) + end + + @doc "The type a `case` scrutinee has, as its clauses' patterns reveal it." + def infer_case_type_from_patterns(clauses) do + if Enum.any?(clauses, fn {:->, _, [[pattern], _body]} -> option_pattern?(pattern) end) do + %Type{kind: :option} + end + end + + defp option_pattern?(nil), do: true + + defp option_pattern?(:none), do: true + + defp option_pattern?({:some, _pattern}), do: true + + defp option_pattern?({:{}, _, [:some, _pattern]}), do: true + + defp option_pattern?(_pattern), do: false + + @doc "Whether `pattern` matches a list." + def list_pattern?([]), do: true + + def list_pattern?([_head | _tail]), do: true + + def list_pattern?(_pattern), do: false + + defp split_list_pattern(pattern), do: do_split_list_pattern(pattern, []) + + defp do_split_list_pattern([], elements), do: {Enum.reverse(elements), nil} + + defp do_split_list_pattern([{:|, _, [head, rest]}], elements), + do: {Enum.reverse([head | elements]), rest} + + defp do_split_list_pattern([head | tail], elements) when is_list(tail), + do: do_split_list_pattern(tail, [head | elements]) + + defp do_split_list_pattern([head | tail], elements), + do: {Enum.reverse([head | elements]), tail} + + defp result_pattern?({:ok, _pattern}), do: true + + defp result_pattern?({:error, _pattern}), do: true + + defp result_pattern?({:{}, _, [:ok, _pattern]}), do: true + + defp result_pattern?({:{}, _, [:error, _pattern]}), do: true + + defp result_pattern?(ok: _pattern), do: true + + defp result_pattern?(error: _pattern), do: true + + defp result_pattern?(_pattern), do: false + + @doc "The Rust pattern a binding pattern is." + def lower_binding_pattern({name, _, context}) when is_atom(name) and is_atom(context), + do: %AST.PatVar{name: name} + + def lower_binding_pattern({:{}, _, values}), + do: %AST.PatTuple{patterns: Enum.map(values, &lower_binding_pattern/1)} + + def lower_binding_pattern({left, right}), + do: %AST.PatTuple{patterns: Enum.map([left, right], &lower_binding_pattern/1)} + + def lower_binding_pattern(other) do + Diagnostic.lower( + :unsupported_binding_pattern, + other, + "unsupported defrust binding pattern", + suggestion: "Use a variable or tuple pattern." + ) + end + + @doc "A clause head split into its pattern and guard." + def split_guarded_pattern({:when, _, [pattern, guard]}), do: {pattern, guard} + + def split_guarded_pattern(pattern), do: {pattern, nil} + + @doc "`context` with the variables `pattern` binds, typed by the value of `type` it matches." + def context_with_match_pattern(pattern, %Type{kind: :option} = type, %Context{} = context) do + context_with_inner_pattern(pattern, Type.inner(type), [:some], context) + end + + def context_with_match_pattern(pattern, %Type{kind: kind} = type, %Context{} = context) + when kind in [:result, :nif_result] do + context_with_inner_pattern(pattern, Type.inner(type), [:ok], context) + end + + def context_with_match_pattern([], %Type{kind: :vec} = type, %Context{} = context), + do: context_with_list_pattern([], type, context) + + def context_with_match_pattern(pattern, %Type{} = type, %Context{} = context) + when is_list(pattern) and type.kind == :slice, + do: context_with_list_pattern(pattern, type, context) + + def context_with_match_pattern( + [_head | _tail] = pattern, + %Type{kind: :vec} = type, + %Context{} = context + ), + do: context_with_list_pattern(pattern, type, context) + + def context_with_match_pattern( + {:%{}, _, fields}, + %Type{kind: :struct} = type, + %Context{} = context + ) + when is_list(fields), + do: context_with_struct_pattern(fields, type, context) + + def context_with_match_pattern( + {:%, _, [_module, {:%{}, _, fields}]}, + %Type{kind: :struct} = type, + %Context{} = context + ) + when is_list(fields), + do: context_with_struct_pattern(fields, type, context) + + def context_with_match_pattern( + {name, _meta, ast_context}, + %Type{} = type, + %Context{} = context + ) + when is_atom(name) and is_atom(ast_context) do + %{context | vars: Map.put(context.vars, name, type)} + end + + def context_with_match_pattern(pattern, %Type{kind: :rust_enum} = type, %Context{} = context), + do: context_with_variant_pattern(pattern, type, & &1, context) + + def context_with_match_pattern( + pattern, + %Type{kind: kind, meta: %{inner: %Type{kind: :rust_enum} = inner}}, + %Context{} = context + ) + when kind in [:ref, :mut_ref], + do: context_with_variant_pattern(pattern, inner, &Type.ref/1, context) + + def context_with_match_pattern(pattern, %Type{kind: :tuple, meta: %{elements: types}}, context) do + case ElixirPattern.tuple_elements(pattern) do + elements when is_list(elements) and length(elements) == length(types) -> + elements + |> Enum.zip(types) + |> Enum.reduce(context, fn {element, type}, acc -> + context_with_match_pattern(element, type, acc) + end) + + _not_tuple -> + context + end + end + + # A pattern of unknown type still shadows the names it binds; they lose their old types. + def context_with_match_pattern(pattern, _type, %Context{} = context) do + names = pattern |> pattern_variables() |> Enum.map(&elem(&1, 0)) + %{context | vars: Map.drop(context.vars, names)} + end + + defp context_with_variant_pattern(pattern, type, payload_type, %Context{} = context) do + case Lower.rust_enum_variant(pattern, type) do + {_variant, payloads, types} when length(payloads) == length(types) -> + payloads + |> Enum.zip(types) + |> Enum.reduce(context, fn {payload, type}, acc -> + context_with_match_pattern(payload, payload_type.(type), acc) + end) + + _other -> + context + end + end + + defp context_with_struct_pattern(fields, %Type{} = type, %Context{} = context) do + Enum.reduce(fields, context, fn {field, pattern}, acc -> + case Type.field_type(type, field) do + %Type{} = field_type -> context_with_match_pattern(pattern, field_type, acc) + nil -> acc + end + end) + end + + # A slice binds its elements by reference; a vector matched as a slice does too. + defp context_with_list_pattern(pattern, %Type{} = type, %Context{} = context) do + {elements, rest} = split_list_pattern(pattern) + inner = list_item_type(type) + inner = if inner && type.kind == :slice, do: Type.ref(inner), else: inner + + context = + Enum.reduce(elements, context, fn element, acc -> + context_with_match_pattern(element, inner, acc) + end) + + if rest, + do: context_with_match_pattern(rest, Type.slice_ref(inner), context), + else: context + end + + @doc "`context` with the variables a generator pattern binds, typed by the items it matches." + def context_with_for_pattern(pattern, expression, %Context{} = context) do + expression + |> Lower.infer_expr_type(context.vars) + |> Kernel.||(Typing.synth(expression, Lower.typing_env(context))) + |> for_item_type() + |> case do + %Type{} = item_type -> context_with_match_pattern(pattern, item_type, context) + nil -> context + end + end + + defp for_item_type(%Type{} = type), do: Type.vec_inner(type) || Type.slice_inner(type) + + defp for_item_type(_type), do: nil + + defp context_with_inner_pattern(pattern, %Type{} = inner, wrappers, %Context{} = context) do + case unwrap_match_pattern(pattern, wrappers) do + nil -> context + inner_pattern -> context_with_match_pattern(inner_pattern, inner, context) + end + end + + defp context_with_inner_pattern(_pattern, _inner, _wrappers, %Context{} = context), do: context + + defp unwrap_match_pattern({name, _meta, ast_context} = pattern, _wrappers) + when is_atom(name) and is_atom(ast_context), + do: pattern + + defp unwrap_match_pattern({wrapper, inner}, wrappers) do + if wrapper in wrappers, do: inner + end + + defp unwrap_match_pattern({:{}, _, [wrapper, inner]}, wrappers) do + if wrapper in wrappers, do: inner + end + + defp unwrap_match_pattern(_pattern, _wrappers), do: nil + + @doc "The Rust pattern a `case` pattern is, matching a value of `type`." + # Patterns name constants and enum variants through the module's Rust module aliases. + def lower_match_pattern(pattern, case_type, %Context{} = context), + do: pattern |> resolve_pattern_paths(context) |> lower_match_pattern(case_type) + + defp resolve_pattern_paths(pattern, %Context{} = context) do + Macro.prewalk(pattern, fn + {:enum_variant, meta, [{:__aliases__, _, parts} | rest]} -> + {:enum_variant, meta, [rust_path_marker(parts, context) | rest]} + + {:__aliases__, _, parts} = alias -> + if Paths.constant_alias?(parts), do: rust_path_marker(parts, context), else: alias + + other -> + other + end) + end + + defp rust_path_marker(parts, %Context{} = context), + do: {:__rustq_path__, [], Paths.mapped_alias_parts(parts, context.rust_modules)} + + def lower_match_pattern({:__rustq_path__, _, parts}, _case_type), + do: %AST.PatPath{path: %AST.Path{parts: parts}} + + def lower_match_pattern(:ok, %Type{kind: kind}) when kind in [:result, :nif_result], + do: %AST.PatOk{pattern: %AST.PatTuple{patterns: []}} + + def lower_match_pattern(nil, %Type{kind: :option}), do: %AST.PatNone{} + + def lower_match_pattern(:none, %Type{kind: :option}), do: %AST.PatNone{} + + def lower_match_pattern(nil, _case_type), do: %AST.PatNone{} + + def lower_match_pattern(:none, _case_type), do: %AST.PatNone{} + + def lower_match_pattern({:_, _, _}, _case_type), do: %AST.PatWildcard{} + + def lower_match_pattern(value, _case_type) + when is_binary(value) or is_integer(value) or is_boolean(value), + do: %AST.PatLiteral{value: value} + + def lower_match_pattern([ok: pattern], _case_type), + do: %AST.PatOk{pattern: lower_match_pattern(pattern, nil)} + + def lower_match_pattern([error: pattern], _case_type), + do: %AST.PatErr{pattern: lower_match_pattern(pattern, nil)} + + def lower_match_pattern({:ok, pattern}, _case_type), + do: %AST.PatOk{pattern: lower_match_pattern(pattern, nil)} + + def lower_match_pattern({:error, pattern}, _case_type), + do: %AST.PatErr{pattern: lower_match_pattern(pattern, nil)} + + def lower_match_pattern({:some, pattern}, %Type{kind: :option} = type), + do: %AST.PatSome{pattern: lower_tuple_element_pattern(pattern, Type.inner(type))} + + def lower_match_pattern({:some, pattern}, _case_type), + do: %AST.PatSome{pattern: lower_match_pattern(pattern, nil)} + + def lower_match_pattern({:{}, _, [:some, pattern]}, %Type{kind: :option} = type), + do: %AST.PatSome{pattern: lower_tuple_element_pattern(pattern, Type.inner(type))} + + def lower_match_pattern({:{}, _, [:some, pattern]}, _case_type), + do: %AST.PatSome{pattern: lower_match_pattern(pattern, nil)} + + def lower_match_pattern({:enum_variant, _, [path, variant]}, _case_type) do + %AST.PatPath{path: Lower.enum_variant_path(path, variant)} + end + + def lower_match_pattern({:enum_variant, _, [path, variant | patterns]}, _case_type) do + %AST.PatPathTuple{ + path: Lower.enum_variant_path(path, variant), + patterns: Enum.map(patterns, &lower_match_pattern(&1, nil)) + } + end + + def lower_match_pattern([], %Type{kind: :vec} = type), do: lower_list_pattern([], type) + + def lower_match_pattern(pattern, %Type{kind: :slice} = type) when is_list(pattern), + do: lower_list_pattern(pattern, type) + + # A list pattern on a value of unknown type matches it as a slice. + def lower_match_pattern(pattern, nil) + when is_list(pattern) and pattern != [] and not is_tuple(hd(pattern)), + do: lower_list_pattern(pattern, nil) + + def lower_match_pattern([_head | _tail] = pattern, %Type{kind: :vec} = type), + do: lower_list_pattern(pattern, type) + + def lower_match_pattern( + {:%{}, _, fields}, + %Type{kind: :struct, rust: rust_name, meta: %{fields: type_fields}} + ) + when is_list(fields) do + %AST.PatStruct{ + path: %AST.Path{parts: [rust_name]}, + fields: lower_struct_pattern_fields(fields, type_fields) + } + end + + def lower_match_pattern( + {:%, _, [_module, {:%{}, _, fields}]}, + %Type{kind: :struct, rust: rust_name, meta: %{fields: type_fields}} + ) + when is_list(fields) do + %AST.PatStruct{ + path: %AST.Path{parts: [rust_name]}, + fields: lower_struct_pattern_fields(fields, type_fields) + } + end + + def lower_match_pattern({:%, _, [{:__aliases__, _, modules}, {:%{}, _, fields}]}, %Type{ + kind: :tuple_enum, + rust: rust_name + }) do + module = List.last(modules) + + %AST.PatPathTuple{ + path: %AST.Path{parts: [rust_name, module]}, + patterns: [ + %AST.PatStruct{ + path: %AST.Path{parts: [module]}, + fields: + Enum.map(fields, fn {name, pattern} -> {name, lower_match_pattern(pattern, nil)} end) + } + ] + } + end + + def lower_match_pattern({name, _, context}, %Type{kind: :option}) + when is_atom(name) and is_atom(context), + do: %AST.PatSome{pattern: %AST.PatVar{name: name}} + + def lower_match_pattern({name, _, context}, _case_type) + when is_atom(name) and is_atom(context), + do: %AST.PatVar{name: name} + + # A borrowed enum matches its variants as they are, binding payloads by reference. + def lower_match_pattern( + pattern, + %Type{kind: kind, meta: %{inner: %Type{kind: :rust_enum} = inner}} + ) + when kind in [:ref, :mut_ref] and (is_atom(pattern) or is_tuple(pattern)), + do: lower_match_pattern(pattern, inner) + + def lower_match_pattern(pattern, %Type{kind: :rust_enum, rust: rust_name} = type) + when is_atom(pattern) or is_tuple(pattern) do + case Lower.rust_enum_variant(pattern, type) do + {variant, [], _types} -> + %AST.PatPath{path: %AST.Path{parts: [rust_name, Lower.rust_variant(variant)]}} + + {variant, payloads, types} when length(payloads) == length(types) -> + %AST.PatPathTuple{ + path: %AST.Path{parts: [rust_name, Lower.rust_variant(variant)]}, + patterns: Enum.zip_with(payloads, types, &lower_tuple_element_pattern/2) + } + + _other -> + lower_match_pattern(pattern, nil) + end + end + + def lower_match_pattern(atom, %Type{kind: :enum, rust: rust_name}) when is_atom(atom), + do: %AST.PatPath{path: %AST.Path{parts: [rust_name, Lower.rust_variant(atom)]}} + + def lower_match_pattern(atom, %Type{kind: :atom}) when is_atom(atom), + do: %AST.PatAtomGuard{name: atom} + + def lower_match_pattern(atom, _case_type) when is_atom(atom), do: %AST.PatAtomGuard{name: atom} + + def lower_match_pattern({:{}, _, values}, case_type), + do: lower_tuple_match_pattern(values, case_type) + + def lower_match_pattern({left, right}, case_type), + do: lower_tuple_match_pattern([left, right], case_type) + + def lower_match_pattern(other, _case_type) do + Diagnostic.lower( + :unsupported_match_pattern, + other, + "unsupported defrust match pattern", + suggestion: + "Use a variable, tuple, list, option/result, atom, literal, or supported struct pattern." + ) + end + + defp lower_struct_pattern_fields(fields, type_fields) do + patterns = Map.new(fields) + + Enum.map(type_fields, fn {name, _type, _presence} -> + pattern = Map.get(patterns, name, {:_, [], nil}) + {name, lower_match_pattern(pattern, nil)} + end) + end + + defp lower_list_pattern(pattern, type) do + {elements, rest} = split_list_pattern(pattern) + inner = type && list_item_type(type) + + %AST.PatSlice{ + patterns: Enum.map(elements, &lower_tuple_element_pattern(&1, inner)), + rest: if(rest, do: lower_match_pattern(rest, inner && Type.slice_ref(inner))) + } + end + + defp list_item_type(%Type{} = type), do: Type.vec_inner(type) || Type.slice_inner(type) + + # Each element is a pattern of its own, such as `{:some, Kind.PLAIN}`. + defp lower_tuple_match_pattern(values, case_type) do + types = + case case_type && Type.expected_value(case_type) do + %Type{kind: :tuple, meta: %{elements: elements}} + when length(elements) == length(values) -> + elements + + _unknown -> + List.duplicate(nil, length(values)) + end + + %AST.PatTuple{patterns: Enum.zip_with(values, types, &lower_tuple_element_pattern/2)} + end + + # A variable binds the whole element, whatever its type. + defp lower_tuple_element_pattern({name, _, context} = pattern, _type) + when is_atom(name) and is_atom(context), + do: lower_match_pattern(pattern, nil) + + defp lower_tuple_element_pattern(pattern, type), do: lower_match_pattern(pattern, type) + + @doc "The Rust pattern a pattern over atoms and enum variants is." + def lower_semantic_pat({:ident, _, [name]}, %Context{}), + do: %AST.PatVar{name: semantic_atom!(name)} + + def lower_semantic_pat({:mut_ident, _, [name]}, %Context{}), + do: %AST.PatVar{name: semantic_atom!(name), mutable: true} + + def lower_semantic_pat({:path, _, [path]}, %Context{} = context), + do: %AST.PatPath{path: lower_expr_path(path, context)} + + def lower_semantic_pat({:some, _, [pattern]}, %Context{} = context), + do: %AST.PatSome{pattern: lower_semantic_pat(pattern, context)} + + def lower_semantic_pat({:ok, pattern}, %Context{} = context), + do: %AST.PatOk{pattern: lower_semantic_pat(pattern, context)} + + def lower_semantic_pat({:error, pattern}, %Context{} = context), + do: %AST.PatErr{pattern: lower_semantic_pat(pattern, context)} + + def lower_semantic_pat({:{}, _, [:ok, pattern]}, %Context{} = context), + do: %AST.PatOk{pattern: lower_semantic_pat(pattern, context)} + + def lower_semantic_pat({:{}, _, [:error, pattern]}, %Context{} = context), + do: %AST.PatErr{pattern: lower_semantic_pat(pattern, context)} + + def lower_semantic_pat({:tuple, _, [patterns]}, %Context{} = context), + do: %AST.PatTuple{patterns: Enum.map(patterns, &lower_semantic_pat(&1, context))} + + def lower_semantic_pat({:path_tuple, _, [path, patterns]}, %Context{} = context), + do: %AST.PatPathTuple{ + path: lower_expr_path(path, context), + patterns: Enum.map(patterns, &lower_semantic_pat(&1, context)) + } + + def lower_semantic_pat({:struct, _, [path, fields]}, %Context{} = context), + do: %AST.PatStruct{ + path: lower_expr_path(path, context), + fields: lower_semantic_pat_fields(fields, context) + } + + def lower_semantic_pat(nil, %Context{}), do: %AST.PatNone{} + + def lower_semantic_pat(:_, %Context{}), do: %AST.PatWildcard{} + + def lower_semantic_pat({:_, _, _}, %Context{}), do: %AST.PatWildcard{} + + def lower_semantic_pat(other, %Context{} = context), + do: lower_match_pattern(other, nil, context) + + defp lower_semantic_pat_fields(fields, %Context{} = context) when is_list(fields) do + Enum.map(fields, fn {name, pattern} -> {name, lower_semantic_pat(pattern, context)} end) + end + + defp lower_expr_path(%AST.Path{} = path, %Context{}), do: path + + defp lower_expr_path(expression, %Context{} = context) do + case Lower.lower_expr(expression, context) do + %AST.Path{} = path -> + path + + other -> + Diagnostic.lower( + :expected_rust_path, + expression, + "expected Rust path, got: #{inspect(other)}" + ) + end + end + + @doc "The atom a pattern names, or a diagnostic for one it cannot." + def semantic_atom!(atom) when is_atom(atom), do: atom + + def semantic_atom!({name, _, context}) when is_atom(name) and is_atom(context), do: name + + def semantic_atom!(other) do + Diagnostic.lower(:expected_atom_identifier, other, "expected atom identifier") + end + + @doc "`context` with the variables a binding pattern binds shadowing any of the same name." + def context_with_lexical_pattern_vars(pattern, %Context{} = context) do + vars = + pattern + |> binding_pattern_names() + |> Enum.reduce(context.vars, fn name, vars -> + case Map.fetch(context.lexical_vars, name) do + {:ok, type} -> Map.put(vars, name, type) + :error -> vars + end + end) + + %{context | vars: vars} + end + + @doc "The variables `pattern` binds." + def binding_pattern_names({name, _meta, ast_context}) + when is_atom(name) and is_atom(ast_context) and name != :_, + do: [name] + + def binding_pattern_names(pattern) do + case ElixirPattern.tuple_elements(pattern) do + elements when is_list(elements) -> Enum.flat_map(elements, &binding_pattern_names/1) + _not_tuple -> [] + end + end + + @doc "The variables a clause body changes among those its pattern binds." + def collect_pattern_mutations(term, context) do + Walk.reduce(term, collect_mut_refs(term), fn + %AST.ExprStmt{ + expr: %AST.MethodCall{receiver: %AST.Var{name: name}, method: method, args: args} + }, + acc -> + if mutable_method?(context, name, method, length(args)), do: [name | acc], else: acc + + _, acc -> + acc + end) + end + + defp mutable_method?(context, name, method, arity) do + targets = + case Map.get(context.vars, name) do + %Type{} = type -> + case type |> StandardPointer.receiver() |> Type.callable_target() do + nil -> [] + target -> [target] + end + + nil -> + [] + end + + targets != [] and + Enum.all?(targets, fn target -> + match?( + %{args: [%{type: %Type{kind: :mut_ref}} | _]}, + BindingIndex.get(context.callables, target, method, arity) + ) + end) + end + + @doc "The variables `ast` borrows `mut`." + def collect_mut_refs(term) do + Walk.reduce(term, [], fn + %AST.Ref{mutable: true, expr: %AST.Var{name: name}}, acc -> [name | acc] + _other, acc -> acc + end) + end +end diff --git a/lib/rustq/meta/typing.ex b/lib/rustq/meta/typing.ex index 7919d3f..7c39b5c 100644 --- a/lib/rustq/meta/typing.ex +++ b/lib/rustq/meta/typing.ex @@ -8,8 +8,8 @@ defmodule RustQ.Meta.Typing do alias RustQ.Binding.Index, as: BindingIndex alias RustQ.Meta.Core.Call alias RustQ.Meta.Inference - alias RustQ.Meta.Lower alias RustQ.Meta.Lower.Collection + alias RustQ.Meta.Lower.Paths alias RustQ.Meta.Lower.Stdlib alias RustQ.Meta.Lower.Stdlib.TypeContext, as: StdlibTypeContext alias RustQ.Meta.Pattern @@ -185,7 +185,7 @@ defmodule RustQ.Meta.Typing do type :unsupported -> - Lower.callable_return_type( + Paths.callable_return_type( call_ast, callables: env.callables, rust_modules: env.rust_modules, @@ -613,7 +613,7 @@ defmodule RustQ.Meta.Typing do # lowering does. defp synth_piped_call(left, {name, meta, args}, %Env{} = env) when is_atom(name) and is_list(args) do - Lower.callable_return_type( + Paths.callable_return_type( {name, meta, [left | args]}, callables: env.callables, rust_modules: env.rust_modules, diff --git a/test/rustq/meta/lower/call_propagation_test.exs b/test/rustq/meta/lower/call_propagation_test.exs index 16e034c..57eda6c 100644 --- a/test/rustq/meta/lower/call_propagation_test.exs +++ b/test/rustq/meta/lower/call_propagation_test.exs @@ -4,6 +4,7 @@ defmodule RustQ.Meta.Lower.CallPropagationTest do alias RustQ.Binding.Callable alias RustQ.Meta.GeneratedCase, as: Generated alias RustQ.Meta.Lower + alias RustQ.Meta.Lower.Paths alias RustQ.Meta.Type alias RustQ.Rust.AST @@ -409,12 +410,17 @@ defmodule RustQ.Meta.Lower.CallPropagationTest do } ] - assert Lower.callable_return_type(quote(do: maybe_path()), callables: callables) == + assert Paths.callable_return_type(quote(do: maybe_path()), + callables: callables + ) == return_type - assert Lower.callable_return_type(quote(do: Canvas.draw_rect(rect)), callables: callables) == + assert Paths.callable_return_type(quote(do: Canvas.draw_rect(rect)), + callables: callables + ) == return_type - assert Lower.callable_return_type(quote(do: missing()), callables: callables) == nil + assert Paths.callable_return_type(quote(do: missing()), callables: callables) == + nil end end From 834d1ae9bf4427ab4af61f97c9e5a91301250eb2 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 25 Oct 2026 23:41:19 +0400 Subject: [PATCH 06/10] Note the ownership pass, fusion, std method types, and desugaring in the changelog --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9196d8..6bcb01a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # 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`. + ## 1.0.0-rc.17 - 2026-10-25 ### Added From 91e210b50e313bfb6c687d77f549fb0d2f17d7be Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 26 Oct 2026 13:46:16 +0400 Subject: [PATCH 07/10] Build lowered Rust with RustQ.Rust.AST.Builder Lowering wrote Rust nodes as struct literals and kept five copies of a method-call constructor. It now builds them with the Builder the Rustler code generation already uses, and Builder.expr/1 accepts every node in the AST registry instead of a hand-kept list. --- lib/rustq/meta/lower.ex | 242 ++++++++++++-------------- lib/rustq/meta/lower/collection.ex | 39 ++--- lib/rustq/meta/lower/comprehension.ex | 49 ++---- lib/rustq/meta/lower/enum.ex | 162 ++++++++--------- lib/rustq/meta/lower/kernel.ex | 26 ++- lib/rustq/meta/lower/list.ex | 26 ++- lib/rustq/meta/lower/map.ex | 97 +++++------ lib/rustq/meta/lower/map_set.ex | 32 ++-- lib/rustq/meta/lower/mutability.ex | 5 +- lib/rustq/meta/lower/pattern.ex | 17 +- lib/rustq/meta/lower/range.ex | 10 +- lib/rustq/meta/lower/stdlib.ex | 13 +- lib/rustq/meta/lower/string.ex | 28 ++- lib/rustq/meta/lower/tuple.ex | 13 +- lib/rustq/rust/ast/builder.ex | 52 +----- 15 files changed, 344 insertions(+), 467 deletions(-) diff --git a/lib/rustq/meta/lower.ex b/lib/rustq/meta/lower.ex index 9390030..8575fef 100644 --- a/lib/rustq/meta/lower.ex +++ b/lib/rustq/meta/lower.ex @@ -33,6 +33,7 @@ defmodule RustQ.Meta.Lower do alias RustQ.Meta.Typing alias RustQ.Rust alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A alias RustQ.Rust.Identifier # A vector whose item type Rust infers, for collecting where none is known. @@ -119,7 +120,7 @@ defmodule RustQ.Meta.Lower do return_context = %{context | position: :return} Enum.map(statements, &lower_statement(&1, statement_context)) ++ - [%AST.Return{expr: lower_expr(final, expected_type, return_context)}] + [A.return_stmt(lower_expr(final, expected_type, return_context))] end @keyed_collections [:HashMap, :HashSet, :BTreeMap, :BTreeSet] @@ -170,12 +171,12 @@ defmodule RustQ.Meta.Lower do defp lower_statement({:case, _, [expression, [do: clauses]]}, %Context{} = context) do case lower_option_if_let_statement(expression, normalize_case_clauses(clauses), context) do %AST.IfLet{} = if_let -> if_let - nil -> %AST.ExprStmt{expr: lower_case(expression, clauses, context)} + nil -> A.stmt(lower_case(expression, clauses, context)) end end defp lower_statement({:if, _, [condition, branches]}, %Context{} = context) do - %AST.ExprStmt{expr: lower_if(condition, branches, context)} + A.stmt(lower_if(condition, branches, context)) end # Run for its effects, a `cond` is a chain of `if`s; a final branch doing nothing is left out. @@ -185,7 +186,7 @@ defmodule RustQ.Meta.Lower do defp lower_statement({:with, _, clauses} = with, %Context{} = context) do case with_if_lets(clauses) do {:ok, matches, body} -> lower_statement(nested_if_lets(matches, body), context) - :error -> %AST.ExprStmt{expr: lower_with(with |> elem(2), context)} + :error -> A.stmt(lower_with(with |> elem(2), context)) end end @@ -194,7 +195,7 @@ defmodule RustQ.Meta.Lower do expression, %Context{} = context ) do - %AST.ExprStmt{expr: lower_statement_expr(expression, context)} + A.stmt(lower_statement_expr(expression, context)) end defp lower_statement({:for, _, args} = expression, %Context{} = context) when is_list(args) do @@ -203,19 +204,19 @@ defmodule RustQ.Meta.Lower do Comprehension.lower_for_loop(qualifiers, body, context) _other -> - %AST.ExprStmt{expr: Comprehension.lower_comprehension(expression, nil, context)} + A.stmt(Comprehension.lower_comprehension(expression, nil, context)) end end - defp lower_statement(:ok, %Context{}), do: %AST.ExprStmt{expr: %AST.Tuple{values: []}} + defp lower_statement(:ok, %Context{}), do: A.stmt(%AST.Tuple{values: []}) defp lower_statement({:__block__, _, [expression]}, context), do: lower_statement(expression, context) - defp lower_statement(nil, %Context{}), do: %AST.ExprStmt{expr: %AST.Tuple{values: []}} + defp lower_statement(nil, %Context{}), do: A.stmt(%AST.Tuple{values: []}) defp lower_statement(expression, %Context{} = context) do - %AST.ExprStmt{expr: lower_statement_expr(expression, context)} + A.stmt(lower_statement_expr(expression, context)) end defp cond_statement([{:->, _, [[true], body]} | _rest]), do: body @@ -267,19 +268,19 @@ defmodule RustQ.Meta.Lower do do: {:__block__, [], [expression, nested_if_lets(rest, body)]} defp lower_return({:case, _, [expression, [do: clauses]]}, %Context{} = context) do - %AST.Return{expr: lower_case(expression, clauses, context, context.return_type)} + A.return_stmt(lower_case(expression, clauses, context, context.return_type)) end defp lower_return({:if, _, [condition, branches]}, %Context{} = context) do - %AST.Return{expr: lower_if(condition, branches, context)} + A.return_stmt(lower_if(condition, branches, context)) end defp lower_return({:cond, _, [[do: clauses]]}, %Context{} = context) do - %AST.Return{expr: lower_cond(clauses, context, context.return_type)} + A.return_stmt(lower_cond(clauses, context, context.return_type)) end defp lower_return({:with, _, clauses}, %Context{} = context) do - %AST.Return{expr: lower_with(clauses, context)} + A.return_stmt(lower_with(clauses, context)) end defp lower_return( @@ -287,11 +288,11 @@ defmodule RustQ.Meta.Lower do expression, %Context{} = context ) do - %AST.Return{expr: Comprehension.lower_for_reduce_expr(expression, context)} + A.return_stmt(Comprehension.lower_for_reduce_expr(expression, context)) end defp lower_return(expression, %Context{return_type: return_type} = context), - do: %AST.Return{expr: lower_return_expr(expression, return_type, context)} + do: A.return_stmt(lower_return_expr(expression, return_type, context)) @doc "Lowers an expression in return position." def lower_return_expr(expression, return_type, %Context{} = context), @@ -525,7 +526,7 @@ defmodule RustQ.Meta.Lower do %Type{} = expected_type, %Context{} = context ), - do: %AST.BlockExpr{body: lower_expected_block(expressions, context, expected_type)} + do: A.block_expr(lower_expected_block(expressions, context, expected_type)) defp lower_expected_expr_context( {tag, _value} = expression, @@ -549,23 +550,21 @@ defmodule RustQ.Meta.Lower do %Context{} ) when is_binary(value), - do: %AST.MethodCall{receiver: %AST.Literal{value: value}, method: :to_string, args: []} + do: A.method(A.lit(value), :to_string) # A borrowed list literal is a borrowed array, which coerces to the slice expected. defp lower_expected_expr_context({:ref, _, [values]}, %Type{} = expected_type, context) when is_list(values) do item_type = Type.slice_inner(Type.expected_value(expected_type)) - %AST.Ref{ - expr: %AST.ArrayLiteral{ - values: - Enum.map(values, fn value -> - if item_type, - do: lower_expr(value, item_type, context), - else: lower_expr(value, context) - end) - } - } + A.ref(%AST.ArrayLiteral{ + values: + Enum.map(values, fn value -> + if item_type, + do: lower_expr(value, item_type, context), + else: lower_expr(value, context) + end) + }) end defp lower_expected_expr_context( @@ -584,7 +583,7 @@ defmodule RustQ.Meta.Lower do _none -> lower_expr(expression, context) end - %AST.Ref{expr: expr} + A.ref(expr) end defp lower_expected_expr_context( @@ -624,7 +623,7 @@ defmodule RustQ.Meta.Lower do inner = expected_type |> Type.inner() |> Type.expected_value() %AST.PathCall{ - path: %AST.Path{parts: [:ResourceArc, :new]}, + path: A.path([:ResourceArc, :new]), args: [lower_expr(expression, inner, context)] } end @@ -638,7 +637,7 @@ defmodule RustQ.Meta.Lower do inner = expected_type |> Type.inner() |> Type.expected_value() %AST.PathCall{ - path: %AST.Path{parts: [:ResourceArc, :new]}, + path: A.path([:ResourceArc, :new]), args: [lower_expr(expression, inner, context)] } end @@ -654,11 +653,11 @@ defmodule RustQ.Meta.Lower do (is_tuple(expression) and elem(expression, 0) == :{}) do case rust_enum_variant(expression, type) do {variant, [], _types} -> - %AST.Path{parts: [rust_name, rust_variant(variant)]} + A.path([rust_name, rust_variant(variant)]) {variant, payloads, types} when length(payloads) == length(types) -> %AST.PathCall{ - path: %AST.Path{parts: [rust_name, rust_variant(variant)]}, + path: A.path([rust_name, rust_variant(variant)]), args: Enum.zip_with(payloads, types, &lower_expr(&1, &2, context)) } @@ -692,7 +691,7 @@ defmodule RustQ.Meta.Lower do ) when is_atom(atom) and atom not in [nil, true, false] and is_list(variants) do if atom in variants, - do: %AST.Path{parts: [rust_name, rust_variant(atom)]}, + do: A.path([rust_name, rust_variant(atom)]), else: lower_expr(atom, context) end @@ -711,7 +710,7 @@ defmodule RustQ.Meta.Lower do ) when is_list(fields) do %AST.StructLiteral{ - path: %AST.Path{parts: parts}, + path: A.path(parts), fields: lower_named_fields(fields, expected_type, context) } end @@ -723,7 +722,7 @@ defmodule RustQ.Meta.Lower do ) when is_list(fields) do %AST.StructLiteral{ - path: %AST.Path{parts: parts}, + path: A.path(parts), fields: lower_named_fields(fields, expected_type, context) } end @@ -802,7 +801,7 @@ defmodule RustQ.Meta.Lower do values: lower_expected_collection_values(values, expected_type, context) } - if slice_type?(expected_type), do: %AST.Ref{expr: array}, else: array + if slice_type?(expected_type), do: A.ref(array), else: array end defp lower_expected_expr_context(values, %Type{} = expected_type, %Context{} = context) @@ -811,7 +810,7 @@ defmodule RustQ.Meta.Lower do values: lower_expected_collection_values(values, expected_type, context) } - if slice_type?(expected_type), do: %AST.Ref{expr: vec}, else: vec + if slice_type?(expected_type), do: A.ref(vec), else: vec end defp lower_expected_expr_context( @@ -878,7 +877,7 @@ defmodule RustQ.Meta.Lower do ) do cond do slice_type?(Type.expected_value(expected_type)) -> - %AST.Ref{expr: expression |> lower_expr(context) |> Comprehension.collect_as(@any_vec)} + A.ref(expression |> lower_expr(context) |> Comprehension.collect_as(@any_vec)) expected_type.kind in [:option, :result] -> expression |> lower_expr(context) |> Comprehension.collect_as(expected_type.ast) @@ -910,16 +909,12 @@ defmodule RustQ.Meta.Lower do position: :expr }) - %AST.MethodCall{ - receiver: lower_expr(receiver, context), - method: method, - args: [ - %AST.Closure{ - args: [closure_arg!(arg)], - body: lower_closure_body(body, closure_return, closure_context) - } - ] - } + A.method(lower_expr(receiver, context), method, [ + %AST.Closure{ + args: [closure_arg!(arg)], + body: lower_closure_body(body, closure_return, closure_context) + } + ]) _other -> lower_checked_expr(expression, expected_type, context) @@ -935,7 +930,7 @@ defmodule RustQ.Meta.Lower do when is_atom(name) and is_atom(ast_context) do case Map.get(context.vars, name) do %Type{ast: %AST.TypePath{parts: [:Binary]}} -> - %AST.MethodCall{receiver: lower_expr(variable, context), method: :to_vec, args: []} + A.method(lower_expr(variable, context), :to_vec) _other -> lower_checked_expr(variable, expected_type, context) @@ -995,7 +990,7 @@ defmodule RustQ.Meta.Lower do # where it is borrowed or still in use, and takes it otherwise. defp lower_expr_context({:__rustq_items__, _, [list]}, %Context{} = context) do method = if borrowed_list?(list, context), do: :iter, else: :into_iter - %AST.MethodCall{receiver: lower_expr(list, context), method: method, args: []} + A.method(lower_expr(list, context), method) end # `__rustq_owned__(item, list)` is a copy of an item of `list`, as `.clone()` makes one of an @@ -1009,12 +1004,12 @@ defmodule RustQ.Meta.Lower do if Collection.copy?(inner), do: lower_expr(item, context), - else: %AST.MethodCall{receiver: lower_expr(item, context), method: :clone, args: []} + else: A.method(lower_expr(item, context), :clone) end # A block of expressions is a Rust block whose value is its last expression. defp lower_expr_context({:__block__, _, [_first, _second | _rest] = expressions}, context), - do: %AST.BlockExpr{body: lower_value_block(expressions, context)} + do: A.block_expr(lower_value_block(expressions, context)) defp lower_expr_context({:match?, meta, [pattern, expression]}, %Context{} = context) do clauses = [{:->, meta, [[pattern], true]}, {:->, meta, [[{:_, meta, nil}], false]}] @@ -1026,11 +1021,7 @@ defmodule RustQ.Meta.Lower do defp lower_expr_context({:ok_or!, _, [option, error]}, %Context{} = context) do %AST.Try{ - expr: %AST.MethodCall{ - receiver: lower_expr(option, context), - method: :ok_or, - args: [lower_nif_error(error, context)] - } + expr: A.method(lower_expr(option, context), :ok_or, [lower_nif_error(error, context)]) } end @@ -1038,10 +1029,7 @@ defmodule RustQ.Meta.Lower do do: lower_pipe(left, right, context) defp lower_expr_context({:cast, _, [expression, type]}, %Context{} = context), - do: %AST.Cast{ - expr: lower_cast_operand(expression, context), - type: lower_type_arg(type, context) - } + do: A.cast(lower_cast_operand(expression, context), lower_type_arg(type, context)) defp lower_expr_context({:decode_as!, _, [expression, type_ast]}, %Context{} = context), do: %AST.Try{expr: decode_as_expr(expression, type_ast, context)} @@ -1051,13 +1039,13 @@ defmodule RustQ.Meta.Lower do # A borrowed list literal is a slice of an array, not of a vector built for the borrow. defp lower_expr_context({:ref, _, [values]}, %Context{} = context) when is_list(values), - do: %AST.Ref{expr: %AST.ArrayLiteral{values: Enum.map(values, &lower_expr(&1, context))}} + do: A.ref(%AST.ArrayLiteral{values: Enum.map(values, &lower_expr(&1, context))}) defp lower_expr_context({:ref, _, [expression]}, %Context{} = context), - do: %AST.Ref{expr: lower_expr(expression, context)} + do: A.ref(lower_expr(expression, context)) defp lower_expr_context({:mut_ref, _, [expression]}, %Context{} = context), - do: %AST.Ref{expr: lower_expr(expression, context), mutable: true} + do: A.mut_ref(lower_expr(expression, context)) defp lower_expr_context({:deref, _, [expression]}, %Context{} = context) do expr = @@ -1115,7 +1103,7 @@ defmodule RustQ.Meta.Lower do do: LowerPattern.lower_semantic_pat(pattern, context) defp lower_expr_context({:stmt!, _, [expression]}, %Context{} = context), - do: %AST.ExprStmt{expr: lower_expr(expression, context)} + do: A.stmt(lower_expr(expression, context)) defp lower_expr_context({:raw_expr!, _, [tokens]}, %Context{}), do: parse_syn(:Expr, tokens) defp lower_expr_context({:raw_pat!, _, [tokens]}, %Context{}), do: parse_syn(:Pat, tokens) @@ -1128,10 +1116,10 @@ defmodule RustQ.Meta.Lower do body: lower_semantic_arm_body(block, context) } - defp lower_expr_context({:nif_env, _, []}, %Context{}), do: %AST.Path{parts: [:env]} + defp lower_expr_context({:nif_env, _, []}, %Context{}), do: A.path([:env]) defp lower_expr_context({:badarg, _, []}, %Context{}), - do: %AST.Path{parts: [:rustler, :Error, :BadArg]} + do: A.path([:rustler, :Error, :BadArg]) defp lower_expr_context({:%{}, _, [{:|, _, [base, fields]}]}, %Context{} = context) when is_list(fields), @@ -1185,7 +1173,7 @@ defmodule RustQ.Meta.Lower do index = if map_type?(infer_expr_type(receiver, context.vars)), - do: %AST.Ref{expr: index}, + do: A.ref(index), else: index %AST.Index{receiver: lower_expr(receiver, context), index: index} @@ -1281,10 +1269,10 @@ defmodule RustQ.Meta.Lower do lower_remote_or_method_call(receiver, function, args, context) end - defp lower_expr_context({:__rustq_path__, _, parts}, %Context{}), do: %AST.Path{parts: parts} + defp lower_expr_context({:__rustq_path__, _, parts}, %Context{}), do: A.path(parts) defp lower_expr_context({:__aliases__, _, parts}, %Context{} = context), - do: %AST.Path{parts: Paths.mapped_alias_parts(parts, context.rust_modules)} + do: A.path(Paths.mapped_alias_parts(parts, context.rust_modules)) defp lower_expr_context({:{}, _, values}, %Context{} = context), do: %AST.Tuple{values: Enum.map(values, &lower_expr(&1, context))} @@ -1305,13 +1293,13 @@ defmodule RustQ.Meta.Lower do defp lower_expr_context(values, %Context{} = context) when is_list(values), do: %AST.VecLiteral{values: Enum.map(values, &lower_array_value(&1, context))} - defp lower_expr_context(value, %Context{}) when is_binary(value), do: %AST.Literal{value: value} + defp lower_expr_context(value, %Context{}) when is_binary(value), do: A.lit(value) defp lower_expr_context(value, %Context{}) when is_integer(value) or is_float(value), - do: %AST.Literal{value: value} + do: A.lit(value) - defp lower_expr_context(true, %Context{}), do: %AST.Literal{value: true} - defp lower_expr_context(false, %Context{}), do: %AST.Literal{value: false} + defp lower_expr_context(true, %Context{}), do: A.lit(true) + defp lower_expr_context(false, %Context{}), do: A.lit(false) defp lower_expr_context(nil, %Context{}), do: %AST.None{} defp lower_expr_context(atom, %Context{}) when is_atom(atom), do: %AST.AtomValue{name: atom} @@ -1442,20 +1430,20 @@ defmodule RustQ.Meta.Lower do do: lower_expr(expression, context) defp lower_checked_coercion(%Typing.Check{coercion: :borrow}, expression, context), - do: %AST.Ref{expr: lower_expr(expression, context)} + do: A.ref(lower_expr(expression, context)) defp lower_checked_coercion(%Typing.Check{coercion: :mut_borrow}, expression, context), - do: %AST.Ref{expr: lower_expr(expression, context), mutable: true} + do: A.mut_ref(lower_expr(expression, context)) defp lower_checked_coercion(%Typing.Check{coercion: :propagate_borrow}, expression, context), - do: %AST.Ref{expr: %AST.Try{expr: lower_expr(expression, context)}} + do: A.ref(%AST.Try{expr: lower_expr(expression, context)}) defp lower_checked_coercion( %Typing.Check{coercion: :propagate_mut_borrow}, expression, context ), - do: %AST.Ref{expr: %AST.Try{expr: lower_expr(expression, context)}, mutable: true} + do: A.mut_ref(%AST.Try{expr: lower_expr(expression, context)}) defp lower_checked_coercion(%Typing.Check{coercion: :as_ref}, expression, context), do: option_as_ref_expr(lower_expr(expression, context)) @@ -1470,13 +1458,13 @@ defmodule RustQ.Meta.Lower do do: %AST.Some{expr: lower_expr(expression, context)} defp lower_checked_coercion(%Typing.Check{coercion: :to_vec}, expression, context), - do: %AST.MethodCall{receiver: lower_expr(expression, context), method: :to_vec, args: []} + do: A.method(lower_expr(expression, context), :to_vec) defp lower_checked_coercion(%Typing.Check{}, expression, context), do: lower_expr(expression, context) defp option_as_ref_expr(expr) do - %AST.MethodCall{receiver: expr, method: :as_ref, args: []} + A.method(expr, :as_ref) end defp option_propagation_in_result_context?( @@ -1532,7 +1520,7 @@ defmodule RustQ.Meta.Lower do match_expr = lower_expr(expression, context) if Enum.any?(clauses, &list_case_clause?/1) do - {expression_type, %AST.MethodCall{receiver: match_expr, method: :as_slice, args: []}} + {expression_type, A.method(match_expr, :as_slice)} else {expression_type, match_expr} end @@ -1588,7 +1576,7 @@ defmodule RustQ.Meta.Lower do %AST.If{ condition: lower_condition(condition, context), then: lower_clause_body(body, context, expected_type), - else: [%AST.Return{expr: lower_cond_clauses(rest, context, expected_type)}] + else: [A.return_stmt(lower_cond_clauses(rest, context, expected_type))] } end @@ -1610,14 +1598,14 @@ defmodule RustQ.Meta.Lower do defp clause_expr(body, context, _expected_type) do case body |> block_expressions() |> lower_value_block(context) do [%AST.Return{expr: expr}] -> expr - statements -> %AST.BlockExpr{body: statements} + statements -> A.block_expr(statements) end end defp lower_expected_value_block(expressions, %Context{} = context, %Type{} = expected_type) do case lower_expected_block(expressions, context, expected_type) do [%AST.Return{expr: expr}] -> expr - statements -> %AST.BlockExpr{body: statements} + statements -> A.block_expr(statements) end end @@ -1668,9 +1656,9 @@ defmodule RustQ.Meta.Lower do %AST.Arm{ pattern: LowerPattern.lower_with_pattern(pattern, match_type, context), body: [ - %AST.Return{ - expr: lower_with_matches(rest, body, else_clauses, body_context, expected_type) - } + A.return_stmt( + lower_with_matches(rest, body, else_clauses, body_context, expected_type) + ) ] }, with_fallback_arm(with_value, pattern, else_clauses, context, expected_type) @@ -1682,7 +1670,7 @@ defmodule RustQ.Meta.Lower do defp with_fallback_arm(_value_name, {:ok, _value}, [], _context, _expected_type), do: passthrough_arm(%AST.PatErr{pattern: %AST.PatVar{name: :error}}, %AST.Err{ - expr: %AST.Var{name: :error} + expr: A.var(:error) }) defp with_fallback_arm(_value_name, {:some, _value}, [], _context, _expected_type), @@ -1706,19 +1694,19 @@ defmodule RustQ.Meta.Lower do %AST.Arm{ pattern: %AST.PatVar{name: value_name}, body: [ - %AST.Return{expr: lower_with_else(value_name, else_clauses, context, expected_type)} + A.return_stmt(lower_with_else(value_name, else_clauses, context, expected_type)) ] } end defp passthrough_arm(pattern, expr), - do: %AST.Arm{pattern: pattern, body: [%AST.Return{expr: expr}]} + do: %AST.Arm{pattern: pattern, body: [A.return_stmt(expr)]} - defp lower_with_else(value_name, [], _context, _expected_type), do: %AST.Var{name: value_name} + defp lower_with_else(value_name, [], _context, _expected_type), do: A.var(value_name) defp lower_with_else(value_name, else_clauses, %Context{} = context, expected_type) do %AST.Match{ - expr: %AST.Var{name: value_name}, + expr: A.var(value_name), arms: Enum.map(else_clauses, fn {:->, _, [[pattern], body]} -> %AST.Arm{ @@ -1917,7 +1905,7 @@ defmodule RustQ.Meta.Lower do defp block_expr([%AST.Return{expr: expr}]), do: expr defp block_expr(statements) do - %AST.BlockExpr{body: statements} + A.block_expr(statements) end @doc "Lowers the body of a clause, expected to give `type` when known." @@ -1976,21 +1964,17 @@ defmodule RustQ.Meta.Lower do cond do no_parens? and Paths.alias_path_ast?(receiver) -> - %AST.Path{ - parts: Paths.alias_path_parts(receiver, context.rust_modules) ++ [field_or_function] - } + A.path(Paths.alias_path_parts(receiver, context.rust_modules) ++ [field_or_function]) no_parens? -> %AST.Field{receiver: lower_expr(receiver, context), field: field_or_function} Paths.super_alias_ast?(receiver) -> - %AST.PathCall{path: %AST.Path{parts: [:super, field_or_function]}, args: []} + %AST.PathCall{path: A.path([:super, field_or_function]), args: []} Paths.alias_ast?(receiver) -> %AST.PathCall{ - path: %AST.Path{ - parts: Paths.alias_parts(receiver, context.rust_modules) ++ [field_or_function] - }, + path: A.path(Paths.alias_parts(receiver, context.rust_modules) ++ [field_or_function]), args: [] } @@ -2030,7 +2014,7 @@ defmodule RustQ.Meta.Lower do Paths.super_alias_ast?(receiver) -> %AST.PathCall{ - path: %AST.Path{parts: [:super, function]}, + path: A.path([:super, function]), args: lower_parent_call_args(function, args, context) } @@ -2038,7 +2022,7 @@ defmodule RustQ.Meta.Lower do path = Paths.alias_parts(receiver, context.rust_modules) ++ [rust_variant(function)] %AST.PathCall{ - path: %AST.Path{parts: path}, + path: A.path(path), args: lower_path_call_args(path, function, args, context) } @@ -2046,7 +2030,7 @@ defmodule RustQ.Meta.Lower do path = Paths.alias_parts(receiver, context.rust_modules) ++ [function] %AST.PathCall{ - path: %AST.Path{parts: path}, + path: A.path(path), args: lower_path_call_args(path, function, args, context) } @@ -2056,11 +2040,11 @@ defmodule RustQ.Meta.Lower do target = Paths.callable_target_from_type(receiver_type) - %AST.MethodCall{ - receiver: lower_method_receiver(receiver, receiver_type, function, context), - method: borrowed_iteration(function, args, receiver_type), - args: lower_method_call_args(receiver_type, target, function, args, context) - } + A.method( + lower_method_receiver(receiver, receiver_type, function, context), + borrowed_iteration(function, args, receiver_type), + lower_method_call_args(receiver_type, target, function, args, context) + ) end end @@ -2106,7 +2090,7 @@ defmodule RustQ.Meta.Lower do defp lower_var_or_macro_capture(name, ast_context, %Context{} = context) do case macro_var_fragment(name, context.macro_vars) do nil -> - %AST.Var{name: name} + A.var(name) :ty -> Diagnostic.lower( @@ -2353,7 +2337,7 @@ defmodule RustQ.Meta.Lower do end defp lower_pipe_call(receiver, {:cast, _, [type]}, %Context{}), - do: %AST.Cast{expr: receiver, type: RustQ.Spec.type(type).ast} + do: A.cast(receiver, RustQ.Spec.type(type).ast) defp lower_pipe_call( receiver, @@ -2366,11 +2350,7 @@ defmodule RustQ.Meta.Lower do defp lower_pipe_call(receiver, {name, _, args}, %Context{} = context) when is_atom(name) and is_list(args) do - %AST.MethodCall{ - receiver: receiver, - method: name, - args: Enum.map(args, &lower_expr(&1, context)) - } + A.method(receiver, name, Enum.map(args, &lower_expr(&1, context))) end defp lower_pipe_call(_receiver, other, %Context{}) do @@ -2516,7 +2496,7 @@ defmodule RustQ.Meta.Lower do _none -> lower_value_block(expressions, context) end - %AST.BlockExpr{body: body} + A.block_expr(body) end defp lower_closure_body(body, expected_return_type, %Context{} = context), @@ -2527,7 +2507,7 @@ defmodule RustQ.Meta.Lower do {statements, final} = split_final(expressions) Enum.map(statements, &lower_statement(&1, %{context | position: :statement})) ++ - [%AST.Return{expr: lower_expr(final, %{context | position: :return})}] + [A.return_stmt(lower_expr(final, %{context | position: :return}))] end defp map_type?(%Type{kind: kind} = type) when kind in [:ref, :mut_ref], @@ -2540,19 +2520,19 @@ defmodule RustQ.Meta.Lower do defp lower_function_capture({name, _, capture_context}, %Context{}) when is_atom(name) and is_atom(capture_context), - do: %AST.Path{parts: [name]} + do: A.path([name]) defp lower_function_capture( {{:., _, [{:__aliases__, _, [:Super]}, name]}, _, []}, %Context{} ), - do: %AST.Path{parts: [:super, name]} + do: A.path([:super, name]) defp lower_function_capture( {{:., _, [{:__aliases__, _, parts}, name]}, _, []}, %Context{} = context ), - do: %AST.Path{parts: Paths.mapped_alias_parts(parts, context.rust_modules) ++ [name]} + do: A.path(Paths.mapped_alias_parts(parts, context.rust_modules) ++ [name]) defp lower_function_capture(other, %Context{}) do Diagnostic.lower( @@ -2610,7 +2590,7 @@ defmodule RustQ.Meta.Lower do defp lower_macro_call(name, args, rust_macros, %Context{} = context) do part = macro_call_part(name) - path = %AST.Path{parts: [part]} + path = A.path([part]) case Map.get(rust_macros, part) do nil -> @@ -2658,7 +2638,7 @@ defmodule RustQ.Meta.Lower do if path_parts do %AST.MacroCall{ - path: %AST.Path{parts: path_parts}, + path: A.path(path_parts), args: Enum.map(args, &lower_expr(&1, context)) } else @@ -2722,13 +2702,13 @@ defmodule RustQ.Meta.Lower do defp parse_syn(type, tokens) do %AST.PathCall{ - path: %AST.Path{parts: [:super, "parse_syn::<#{type}>"]}, + path: A.path([:super, "parse_syn::<#{type}>"]), args: [quote_tokens(tokens)] } end defp quote_tokens(tokens) when is_binary(tokens), - do: %AST.TokenMacro{path: %AST.Path{parts: [:quote]}, tokens: tokens} + do: %AST.TokenMacro{path: A.path([:quote]), tokens: tokens} defp quote_tokens(other) do Diagnostic.lower( @@ -2749,11 +2729,11 @@ defmodule RustQ.Meta.Lower do end) if explicit do - %AST.Path{parts: explicit} + A.path(explicit) else type_ast = {{:., meta, [alias_ast, :t]}, meta, []} %AST.TypePath{parts: resolved} = RustQ.Spec.type(type_ast, context.type_aliases).ast - %AST.Path{parts: resolved} + A.path(resolved) end end @@ -2764,7 +2744,7 @@ defmodule RustQ.Meta.Lower do def enum_variant_path(path, variant, %Context{} = context) do %AST.Path{parts: parts} = lower_expr(path, context) - %AST.Path{parts: parts ++ [rust_variant(LowerPattern.semantic_atom!(variant))]} + A.path(parts ++ [rust_variant(LowerPattern.semantic_atom!(variant))]) end # The struct a type declaration of this module, or of a callable module, names `name`. @@ -2786,7 +2766,7 @@ defmodule RustQ.Meta.Lower do defp clone_captured(lowered, {name, _, ast_context}, %Context{captured: captured}) when is_atom(name) and is_atom(ast_context) do if MapSet.member?(captured, name), - do: %AST.MethodCall{receiver: lowered, method: :clone, args: []}, + do: A.method(lowered, :clone), else: lowered end @@ -2797,7 +2777,7 @@ defmodule RustQ.Meta.Lower do case infer_expr_type(base, context.vars) || Typing.synth(base, typing_env(context)) do %Type{kind: :struct, ast: %AST.TypePath{parts: parts}} = type -> %AST.StructLiteral{ - path: %AST.Path{parts: parts}, + path: A.path(parts), fields: lower_named_fields(fields, type, context), base: base |> lower_expr(type, context) |> clone_captured(base, context) } @@ -2823,8 +2803,8 @@ defmodule RustQ.Meta.Lower do end) end - defp lower_token_macro_path(atom) when is_atom(atom), do: %AST.Path{parts: [atom]} - defp lower_token_macro_path({:__aliases__, _, parts}), do: %AST.Path{parts: parts} + defp lower_token_macro_path(atom) when is_atom(atom), do: A.path([atom]) + defp lower_token_macro_path({:__aliases__, _, parts}), do: A.path(parts) defp lower_token_macro_path(other) do Diagnostic.lower( diff --git a/lib/rustq/meta/lower/collection.ex b/lib/rustq/meta/lower/collection.ex index 9fe45b9..7d8166e 100644 --- a/lib/rustq/meta/lower/collection.ex +++ b/lib/rustq/meta/lower/collection.ex @@ -12,6 +12,7 @@ defmodule RustQ.Meta.Lower.Collection do alias RustQ.Meta.Lower.Stdlib.Context alias RustQ.Meta.Type alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A @maps [:HashMap, :BTreeMap] @sets [:HashSet, :BTreeSet] @@ -59,19 +60,18 @@ defmodule RustQ.Meta.Lower.Collection do @spec updated(Macro.t(), (AST.expr() -> [AST.stmt()]), Context.t()) :: AST.expr() def updated(collection, statements, %Context{} = context) do name = :rustq_updated - variable = %AST.Var{name: name} - - %AST.BlockExpr{ - body: - [ - %AST.Let{ - pattern: %AST.PatVar{name: name}, - expr: owned(collection, context), - mutable: true - } - ] ++ - statements.(variable) ++ [%AST.Return{expr: variable}] - } + variable = A.var(name) + + A.block_expr( + [ + %AST.Let{ + pattern: %AST.PatVar{name: name}, + expr: owned(collection, context), + mutable: true + } + ] ++ + statements.(variable) ++ [A.return_stmt(variable)] + ) end @doc "`collection` to take: itself at its last use, otherwise a copy." @@ -80,7 +80,7 @@ defmodule RustQ.Meta.Lower.Collection do lowered = context.lower.(collection) if context.in_use?.(collection), - do: %AST.MethodCall{receiver: lowered, method: :clone, args: []}, + do: A.method(lowered, :clone), else: lowered end @@ -93,7 +93,7 @@ defmodule RustQ.Meta.Lower.Collection do @spec read_out(AST.expr(), Type.t()) :: AST.expr() def read_out(option, %Type{} = type) do method = if copy?(type), do: :copied, else: :cloned - %AST.MethodCall{receiver: option, method: method, args: []} + A.method(option, method) end @doc """ @@ -112,13 +112,4 @@ defmodule RustQ.Meta.Lower.Collection do end def copy?(_type), do: false - - @doc "A call on `receiver`." - @spec call(AST.expr(), atom(), [AST.expr()]) :: AST.expr() - def call(receiver, method, args \\ []), - do: %AST.MethodCall{receiver: receiver, method: method, args: args} - - @doc "A statement calling `method` on `receiver`." - @spec statement(AST.expr(), atom(), [AST.expr()]) :: AST.stmt() - def statement(receiver, method, args), do: %AST.ExprStmt{expr: call(receiver, method, args)} end diff --git a/lib/rustq/meta/lower/comprehension.ex b/lib/rustq/meta/lower/comprehension.ex index 3512aa4..ee419fe 100644 --- a/lib/rustq/meta/lower/comprehension.ex +++ b/lib/rustq/meta/lower/comprehension.ex @@ -17,6 +17,7 @@ defmodule RustQ.Meta.Lower.Comprehension do alias RustQ.Meta.Type alias RustQ.Meta.Typing alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A @any_vec %AST.TypeVec{inner: %AST.TypeRaw{source: "_"}} @iterator_methods StdMethods.iterator() @@ -345,7 +346,7 @@ defmodule RustQ.Meta.Lower.Comprehension do body: [ %AST.IfLet{ pattern: LowerPattern.lower_match_pattern(pattern, nil, context), - expr: %AST.Var{name: :__rustq_item}, + expr: A.var(:__rustq_item), then: lower_for_loop_body( rest, @@ -366,12 +367,10 @@ defmodule RustQ.Meta.Lower.Comprehension do defp lower_for_loop_body([filter | rest], body, %Context{} = context) do [ - %AST.ExprStmt{ - expr: %AST.If{ - condition: Lower.lower_expr(filter, context), - then: lower_for_loop_body(rest, body, context) - } - } + A.stmt(%AST.If{ + condition: Lower.lower_expr(filter, context), + then: lower_for_loop_body(rest, body, context) + }) ] end @@ -384,32 +383,18 @@ defmodule RustQ.Meta.Lower.Comprehension do %Type{} = return_type, %Context{} = context ) do - acc = :__rustq_reduce + acc = A.var(:__rustq_reduce) acc_type = reduce_acc_type(initial, return_type) - %AST.BlockExpr{ - body: [ - %AST.Let{ - pattern: %AST.PatVar{name: acc}, - mutable: true, - expr: lower_reduce_initial(initial, acc_type, context) - }, - %AST.For{ - pattern: LowerPattern.lower_binding_pattern(pattern), - expr: Lower.lower_expr(expression, context), - body: [ - %AST.Assign{ - target: %AST.Var{name: acc}, - expr: %AST.Match{ - expr: %AST.Var{name: acc}, - arms: lower_for_reduce_arms(clauses, acc_type, context) - } - } - ] - }, - %AST.Return{expr: %AST.Var{name: acc}} - ] - } + A.block_expr([ + A.let_mut(acc.name, lower_reduce_initial(initial, acc_type, context)), + A.for_( + LowerPattern.lower_binding_pattern(pattern), + Lower.lower_expr(expression, context), + [A.assign(acc, A.match_expr(acc, lower_for_reduce_arms(clauses, acc_type, context)))] + ), + A.return_stmt(acc) + ]) end def lower_for_reduce_expr(other, _return_type, _context) do @@ -461,7 +446,7 @@ defmodule RustQ.Meta.Lower.Comprehension do [ %AST.Arm{ pattern: %AST.PatVar{name: carry}, - body: [%AST.Return{expr: %AST.Var{name: carry}}] + body: [A.return_stmt(A.var(carry))] } ] end diff --git a/lib/rustq/meta/lower/enum.ex b/lib/rustq/meta/lower/enum.ex index fd5610e..aa97657 100644 --- a/lib/rustq/meta/lower/enum.ex +++ b/lib/rustq/meta/lower/enum.ex @@ -13,6 +13,7 @@ defmodule RustQ.Meta.Lower.Enum do alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A alias RustQ.Spec @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported @@ -23,7 +24,7 @@ defmodule RustQ.Meta.Lower.Enum do do: {:ok, lower_map(collection, mapper, context)} defp lower_call(%Call{function: :sum, args: [collection]}, %Context{} = context), - do: {:ok, collection |> iterate(:question, context) |> method_chain(:sum)} + do: {:ok, collection |> iterate(:question, context) |> A.method(:sum)} defp lower_call( %Call{function: :reduce, args: [collection, initial, reducer]}, @@ -41,8 +42,8 @@ defmodule RustQ.Meta.Lower.Enum do # A stage's items are counted as they come; a list knows its length. defp lower_call(%Call{function: :count, args: [collection]}, %Context{} = context) do case stage(collection) do - {:ok, stage} -> {:ok, stage |> continued(context) |> method_chain(:count) |> cast_i64()} - :error -> {:ok, collection |> lower_collection(context) |> method_chain(:len) |> cast_i64()} + {:ok, stage} -> {:ok, stage |> continued(context) |> A.method(:count) |> cast_i64()} + :error -> {:ok, collection |> lower_collection(context) |> A.method(:len) |> cast_i64()} end end @@ -56,17 +57,13 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, collection |> iterate(:question, context) - |> method_chain(:next) - |> method_chain(:is_none)} + |> A.method(:next) + |> A.method(:is_none)} defp lower_call(%Call{function: :member?, args: [collection, value]}, %Context{} = context), do: {:ok, - %AST.MethodCall{ - receiver: lower_collection(collection, context), - method: :contains, - args: [%AST.Ref{expr: context.lower.(value)}] - }} + A.method(lower_collection(collection, context), :contains, [A.ref(context.lower.(value))])} defp lower_call(%Call{function: :find, args: [collection, predicate]}, %Context{} = context) do iterator = iterate(collection, :keeps_items, context) @@ -82,7 +79,7 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, iterator |> lower_filter(predicate, false, collection, context, :find) - |> method_chain(:unwrap_or, [context.lower.(default)])} + |> A.method(:unwrap_or, [context.lower.(default)])} end defp lower_call(%Call{function: :concat, args: [collections]}, %Context{} = context), @@ -90,59 +87,57 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, collections |> context.lower.() - |> method_chain(:into_iter) - |> method_chain(:flatten) - |> method_chain(:collect)} + |> A.method(:into_iter) + |> A.method(:flatten) + |> A.method(:collect)} defp lower_call(%Call{function: :concat, args: [left, right]}, %Context{} = context), do: {:ok, left |> context.lower.() - |> method_chain(:into_iter) - |> method_chain(:chain, [context.lower.(right)]) - |> method_chain(:collect)} + |> A.method(:into_iter) + |> A.method(:chain, [context.lower.(right)]) + |> A.method(:collect)} defp lower_call(%Call{function: :zip, args: [left, right]}, %Context{} = context), do: {:ok, left |> context.lower.() - |> method_chain(:into_iter) - |> method_chain(:zip, [context.lower.(right)]) - |> method_chain(:collect)} + |> A.method(:into_iter) + |> A.method(:zip, [context.lower.(right)]) + |> A.method(:collect)} defp lower_call(%Call{function: :unzip, args: [collection]}, %Context{} = context), do: {:ok, collection |> context.lower.() - |> method_chain(:into_iter) - |> method_chain(:unzip)} + |> A.method(:into_iter) + |> A.method(:unzip)} defp lower_call(%Call{function: :reverse, args: [collection]}, %Context{} = context), do: {:ok, collection |> iterate(:keeps_items, context) - |> method_chain(:rev) - |> method_chain(:collect)} + |> A.method(:rev) + |> A.method(:collect)} defp lower_call(%Call{function: :sort, args: [collection]} = call, %Context{} = context) do case collection_inner(context.type_of.(collection)) do %Type{} = inner when inner.kind in [:i8, :i16, :i32, :i64, :isize, :u8, :u16, :u32, :u64, :usize] -> name = :rustq_sorted_values - variable = %AST.Var{name: name} + variable = A.var(name) {:ok, - %AST.BlockExpr{ - body: [ - %AST.Let{pattern: %AST.PatVar{name: name}, expr: context.lower.(collection)}, - %AST.ExprStmt{expr: method_chain(variable, :sort)}, - %AST.Return{expr: variable} - ] - }} + A.block_expr([ + %AST.Let{pattern: %AST.PatVar{name: name}, expr: context.lower.(collection)}, + A.stmt(A.method(variable, :sort)), + A.return_stmt(variable) + ])} _type -> Diagnostic.lower( @@ -166,8 +161,8 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, collection |> iterate(:keeps_items, context) - |> method_chain(method, [lowered_count]) - |> method_chain(:collect)} + |> A.method(method, [lowered_count]) + |> A.method(:collect)} :unsupported -> Diagnostic.lower( @@ -186,7 +181,7 @@ defmodule RustQ.Meta.Lower.Enum do Collection.updated( collectable, fn collection -> - [Collection.statement(collection, :extend, [Collection.owned(enumerable, context)])] + [A.stmt(A.method(collection, :extend, [Collection.owned(enumerable, context)]))] end, context )} @@ -196,11 +191,11 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, enumerable |> iterate(:keeps_items, context) - |> method_chain(:enumerate) - |> method_chain(:map, [ + |> A.method(:enumerate) + |> A.method(:map, [ %AST.Closure{ args: [%AST.PatTuple{patterns: [%AST.PatVar{name: :index}, %AST.PatVar{name: :item}]}], - body: %AST.Tuple{values: [%AST.Var{name: :item}, %AST.Var{name: :index}]} + body: %AST.Tuple{values: [A.var(:item), A.var(:index)]} } ]) |> collect(context)} @@ -213,7 +208,7 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, enumerable |> iterate_as(iteration, context) - |> method_chain(:position, [mapper(predicate, context)])} + |> A.method(:position, [mapper(predicate, context)])} end # The function gives `some(value)` for the value to find and `nil` to go on. @@ -224,7 +219,7 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, enumerable |> iterate_as(iteration, context) - |> method_chain(:find_map, [mapper(function, context)])} + |> A.method(:find_map, [mapper(function, context)])} end # `{:cont, acc}` goes on and `{:halt, acc}` stops, as `Ok` and `Err` do for `try_fold`. @@ -246,9 +241,9 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, enumerable |> iterate_as(iteration, context) - |> method_chain(:try_fold, [context.lower.(initial), closure]) - |> method_chain(:unwrap_or_else, [ - %AST.Closure{args: [:rustq_halted], body: %AST.Var{name: :rustq_halted}} + |> A.method(:try_fold, [context.lower.(initial), closure]) + |> A.method(:unwrap_or_else, [ + %AST.Closure{args: [:rustq_halted], body: A.var(:rustq_halted)} ])} end @@ -268,7 +263,7 @@ defmodule RustQ.Meta.Lower.Enum do enumerable |> iterate(:keeps_items, context) - |> method_chain(:min_by_key, [key_function(enumerable, key, order, context)]) + |> A.method(:min_by_key, [key_function(enumerable, key, order, context)]) |> fallback.() |> then(&{:ok, &1}) end @@ -279,7 +274,7 @@ defmodule RustQ.Meta.Lower.Enum do with {:ok, fallback} <- empty_fallback(fallback, context) do enumerable |> iterate(:keeps_items, context) - |> method_chain(function) + |> A.method(function) |> fallback.() |> then(&{:ok, &1}) end @@ -289,7 +284,7 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, Collection.updated( enumerable, - fn list -> [Collection.statement(list, :dedup, [])] end, + fn list -> [A.stmt(A.method(list, :dedup, []))] end, context )} end @@ -311,14 +306,14 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, index} -> found = context.lower.(enumerable) - |> method_chain(:get, [index]) + |> A.method(:get, [index]) |> Collection.read_out( collection_inner(context.type_of.(enumerable)) || %Type{kind: :type} ) case default do [] -> {:ok, found} - [default] -> {:ok, method_chain(found, :unwrap_or, [context.lower.(default)])} + [default] -> {:ok, A.method(found, :unwrap_or, [context.lower.(default)])} end :unsupported -> @@ -332,8 +327,8 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, enumerable |> iterate(:keeps_items, context) - |> method_chain(:skip, [start]) - |> method_chain(:take, [amount]) + |> A.method(:skip, [start]) + |> A.method(:take, [amount]) |> collect(context)} else :unsupported -> unsupported_count!(call) @@ -361,8 +356,8 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, lefts - |> method_chain(:zip, [rights]) - |> method_chain(:map, [context.lower_closure.([{x, y}], body)]) + |> A.method(:zip, [rights]) + |> A.method(:map, [context.lower_closure.([{x, y}], body)]) |> collect(context)} end @@ -371,9 +366,9 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, count} -> {:ok, context.lower.(enumerable) - |> method_chain(:chunks, [count]) - |> method_chain(:map, [ - %AST.Closure{args: [:chunk], body: method_chain(%AST.Var{name: :chunk}, :to_vec)} + |> A.method(:chunks, [count]) + |> A.method(:map, [ + %AST.Closure{args: [:chunk], body: A.method(A.var(:chunk), :to_vec)} ]) |> collect(context)} @@ -385,8 +380,7 @@ defmodule RustQ.Meta.Lower.Enum do defp lower_call(%Call{function: :join, args: [enumerable | separator]}, context) do if strings?(context.type_of.(enumerable)), do: - {:ok, - method_chain(context.lower.(enumerable), :join, [join_separator(separator, context)])}, + {:ok, A.method(context.lower.(enumerable), :join, [join_separator(separator, context)])}, else: :unsupported end @@ -403,7 +397,7 @@ defmodule RustQ.Meta.Lower.Enum do strings = enumerable |> iterate_as(iteration, context) - |> method_chain(:map, [mapper(function, context)]) + |> A.method(:map, [mapper(function, context)]) |> then( &%AST.MethodCall{ receiver: &1, @@ -413,7 +407,7 @@ defmodule RustQ.Meta.Lower.Enum do } ) - {:ok, method_chain(strings, :join, [join_separator(separator, context)])} + {:ok, A.method(strings, :join, [join_separator(separator, context)])} end defp lower_call(%Call{}, %Context{}), do: :unsupported @@ -601,14 +595,14 @@ defmodule RustQ.Meta.Lower.Enum do collection |> iterate_as(iteration, context) - |> method_chain(:map, [closure]) + |> A.method(:map, [closure]) |> collect(context) end defp lower_map(collection, {:&, _, [{:/, _, [capture, 1]}]}, context) do collection |> iterate(:maps_items, context) - |> method_chain(:map, [context.lower_capture.(capture)]) + |> A.method(:map, [context.lower_capture.(capture)]) |> collect(context) end @@ -633,7 +627,7 @@ defmodule RustQ.Meta.Lower.Enum do collection |> iterate_as(iteration, context) - |> method_chain(:fold, [context.lower.(initial), closure]) + |> A.method(:fold, [context.lower.(initial), closure]) end defp lower_reduce(_collection, _initial, other, _context) do @@ -663,16 +657,16 @@ defmodule RustQ.Meta.Lower.Enum do lower_filter(iterator, mapper, true, collection, context, :collect) :flat_map -> - iterator |> method_chain(:flat_map, [mapper(mapper, context)]) |> collect(context) + iterator |> A.method(:flat_map, [mapper(mapper, context)]) |> collect(context) :each -> - method_chain(iterator, :for_each, [mapper(mapper, context)]) + A.method(iterator, :for_each, [mapper(mapper, context)]) :any? -> - method_chain(iterator, :any, [mapper(mapper, context)]) + A.method(iterator, :any, [mapper(mapper, context)]) :all? -> - method_chain(iterator, :all, [mapper(mapper, context)]) + A.method(iterator, :all, [mapper(mapper, context)]) end end @@ -712,11 +706,11 @@ defmodule RustQ.Meta.Lower.Enum do {method, finish} = Map.fetch!(@filter_adapters, terminal) {iterator, kept} = kept_items(iterator, finish) - adapted = iterator |> method_chain(method, [closure]) |> kept.() + adapted = iterator |> A.method(method, [closure]) |> kept.() case finish do :collect -> collect(adapted, context) - :count -> method_chain(adapted, :count) + :count -> A.method(adapted, :count) _kept -> adapted end end @@ -734,7 +728,7 @@ defmodule RustQ.Meta.Lower.Enum do # not at all when only counted. `partition` keeps every item, so it copies them first. defp kept_items(%AST.MethodCall{method: :cloned, receiver: borrowed}, finish) when finish != :partition, - do: {borrowed, if(finish == :count, do: & &1, else: &method_chain(&1, :cloned))} + do: {borrowed, if(finish == :count, do: & &1, else: &A.method(&1, :cloned))} defp kept_items(iterator, _finish), do: {iterator, & &1} @@ -827,7 +821,7 @@ defmodule RustQ.Meta.Lower.Enum do defp continued(stage, context) do case context.lower.(stage) do %AST.MethodCall{method: :collect, receiver: iterator} -> iterator - collected -> method_chain(collected, :into_iter) + collected -> A.method(collected, :into_iter) end end @@ -868,17 +862,17 @@ defmodule RustQ.Meta.Lower.Enum do case iteration do :owned -> if borrowed_collection?(context.type_of.(collection)), - do: method_chain(receiver, :iter), - else: method_chain(receiver, :into_iter) + do: A.method(receiver, :iter), + else: A.method(receiver, :into_iter) :borrowed -> - method_chain(receiver, :iter) + A.method(receiver, :iter) :copied -> - receiver |> method_chain(:iter) |> method_chain(:copied) + receiver |> A.method(:iter) |> A.method(:copied) :cloned -> - receiver |> method_chain(:iter) |> method_chain(:cloned) + receiver |> A.method(:iter) |> A.method(:cloned) end end @@ -935,10 +929,10 @@ defmodule RustQ.Meta.Lower.Enum do %AST.MethodCall{receiver: receiver, method: :collect, args: [], generics: [type]} end - defp collect(receiver, %Context{}), do: method_chain(receiver, :collect) + defp collect(receiver, %Context{}), do: A.method(receiver, :collect) defp cast_i64(expression), - do: %AST.Cast{expr: expression, type: %AST.TypePath{parts: [:i64]}} + do: A.cast(expression, %AST.TypePath{parts: [:i64]}) defp continuation(body) do Macro.prewalk(body, fn @@ -953,7 +947,7 @@ defmodule RustQ.Meta.Lower.Enum do enumerable, fn list -> [ - Collection.statement(list, :sort_by_key, [key_function(enumerable, key, order, context)]) + A.stmt(A.method(list, :sort_by_key, [key_function(enumerable, key, order, context)])) ] end, context @@ -977,7 +971,7 @@ defmodule RustQ.Meta.Lower.Enum do if order == :desc, do: %{ closure - | body: %AST.PathCall{path: %AST.Path{parts: [:std, :cmp, :Reverse]}, args: [body]} + | body: %AST.PathCall{path: A.path([:std, :cmp, :Reverse]), args: [body]} }, else: closure end @@ -989,10 +983,10 @@ defmodule RustQ.Meta.Lower.Enum do defp empty_fallback({:fn, _, [{:->, _, [[], default]}]}, context) do if Macro.quoted_literal?(default) or match?({name, _, ctx} when is_atom(name) and is_atom(ctx), default) do - {:ok, &method_chain(&1, :unwrap_or, [context.lower.(default)])} + {:ok, &A.method(&1, :unwrap_or, [context.lower.(default)])} else {:ok, - &method_chain(&1, :unwrap_or_else, [%AST.Closure{args: [], body: context.lower.(default)}])} + &A.method(&1, :unwrap_or_else, [%AST.Closure{args: [], body: context.lower.(default)}])} end end @@ -1013,7 +1007,7 @@ defmodule RustQ.Meta.Lower.Enum do defp string_type, do: RustQ.Spec.type(quote(do: String.t())) defp usize, do: RustQ.Spec.type(quote(do: RustQ.Type.usize())) - defp join_separator([], _context), do: %AST.Literal{value: ""} + defp join_separator([], _context), do: A.lit("") defp join_separator([separator], context), do: context.lower.(separator) @spec unsupported_count!(Call.t()) :: no_return() @@ -1025,8 +1019,4 @@ defmodule RustQ.Meta.Lower.Enum do suggestion: "Use R.usize() for dynamic non-negative native indexes." ) end - - defp method_chain(receiver, method, args \\ []) do - %AST.MethodCall{receiver: receiver, method: method, args: args} - end end diff --git a/lib/rustq/meta/lower/kernel.ex b/lib/rustq/meta/lower/kernel.ex index 05c39ec..d4c2709 100644 --- a/lib/rustq/meta/lower/kernel.ex +++ b/lib/rustq/meta/lower/kernel.ex @@ -11,6 +11,7 @@ defmodule RustQ.Meta.Lower.Kernel do alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A alias RustQ.Spec @binary_ops %{ @@ -85,12 +86,12 @@ defmodule RustQ.Meta.Lower.Kernel do do: {:ok, %AST.UnaryOp{op: :not, expr: context.lower.(expression)}} def lower(%Call{function: :abs, args: [expression]}, %Context{} = context), - do: {:ok, method(context.lower.(expression), :abs)} + do: {:ok, A.method(context.lower.(expression), :abs)} def lower(%Call{function: function, args: [left, right]} = call, %Context{} = context) when function in [:min, :max] do if integer_type?(context.type_of.(left)) and integer_type?(context.type_of.(right)) do - {:ok, method(context.lower.(left), function, [context.lower.(right)])} + {:ok, A.method(context.lower.(left), function, [context.lower.(right)])} else unsupported_semantics!( call, @@ -101,7 +102,7 @@ defmodule RustQ.Meta.Lower.Kernel do def lower(%Call{function: function, args: [value]}, %Context{} = context) when function in [:byte_size, :length], - do: {:ok, cast_i64(method(context.lower.(value), :len))} + do: {:ok, cast_i64(A.method(context.lower.(value), :len))} def lower(%Call{function: :map_size, args: [map]} = call, %Context{} = context) do case struct_fields(shape_type(context.type_of.(map))) do @@ -122,7 +123,7 @@ defmodule RustQ.Meta.Lower.Kernel do case tuple_elements(context.type_of.(tuple)) do elements when index < length(elements) -> tuple_name = :rustq_tuple_value - tuple_var = %AST.Var{name: tuple_name} + tuple_var = A.var(tuple_name) values = elements @@ -133,12 +134,10 @@ defmodule RustQ.Meta.Lower.Kernel do end) {:ok, - %AST.BlockExpr{ - body: [ - %AST.Let{pattern: %AST.PatVar{name: tuple_name}, expr: context.lower.(tuple)}, - %AST.Return{expr: %AST.Tuple{values: values}} - ] - }} + A.block_expr([ + %AST.Let{pattern: %AST.PatVar{name: tuple_name}, expr: context.lower.(tuple)}, + A.return_stmt(%AST.Tuple{values: values}) + ])} _unknown -> unsupported_semantics!( @@ -159,7 +158,7 @@ defmodule RustQ.Meta.Lower.Kernel do end def lower(%Call{function: :is_nil, args: [value]}, %Context{} = context), - do: {:ok, method(context.lower.(value), :is_none)} + do: {:ok, A.method(context.lower.(value), :is_none)} def lower(%Call{}, %Context{}), do: :unsupported @@ -189,11 +188,8 @@ defmodule RustQ.Meta.Lower.Kernel do def synth(%Call{}, %TypeContext{}), do: :unsupported - defp method(receiver, name, args \\ []), - do: %AST.MethodCall{receiver: receiver, method: name, args: args} - defp cast_i64(expression), - do: %AST.Cast{expr: expression, type: %AST.TypePath{parts: [:i64]}} + do: A.cast(expression, %AST.TypePath{parts: [:i64]}) defp tuple_elements(%Type{kind: :tuple, meta: %{elements: elements}}), do: elements defp tuple_elements(_type), do: nil diff --git a/lib/rustq/meta/lower/list.ex b/lib/rustq/meta/lower/list.ex index dbb03a4..cb3226b 100644 --- a/lib/rustq/meta/lower/list.ex +++ b/lib/rustq/meta/lower/list.ex @@ -9,6 +9,7 @@ defmodule RustQ.Meta.Lower.List do alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported def lower(%Call{function: function, args: [list]}, %Context{} = context) @@ -18,8 +19,8 @@ defmodule RustQ.Meta.Lower.List do {:ok, list |> context.lower.() - |> method(:into_iter) - |> method(terminal)} + |> A.method(:into_iter) + |> A.method(terminal)} end def lower(%Call{function: function, args: [list, default]}, %Context{} = context) @@ -29,9 +30,9 @@ defmodule RustQ.Meta.Lower.List do {:ok, list |> context.lower.() - |> method(:into_iter) - |> method(terminal) - |> method(:unwrap_or, [context.lower.(default)])} + |> A.method(:into_iter) + |> A.method(terminal) + |> A.method(:unwrap_or, [context.lower.(default)])} end def lower(%Call{function: :wrap, args: [nil]}, %Context{}), @@ -54,11 +55,11 @@ defmodule RustQ.Meta.Lower.List do {:ok, lowered_count} -> {:ok, %AST.PathCall{ - path: %AST.Path{parts: [:std, :iter, :repeat]}, + path: A.path([:std, :iter, :repeat]), args: [context.lower.(value)] } - |> method(:take, [lowered_count]) - |> method(:collect)} + |> A.method(:take, [lowered_count]) + |> A.method(:collect)} :unsupported -> :unsupported @@ -69,9 +70,9 @@ defmodule RustQ.Meta.Lower.List do depth = flatten_depth(context.type_of.(list)) if is_integer(depth) and depth > 0 do - iterator = list |> context.lower.() |> method(:into_iter) + iterator = list |> context.lower.() |> A.method(:into_iter) - {:ok, iterator |> flatten_iterator(depth) |> method(:collect)} + {:ok, iterator |> flatten_iterator(depth) |> A.method(:collect)} else :unsupported end @@ -122,7 +123,7 @@ defmodule RustQ.Meta.Lower.List do defp flatten_iterator(iterator, 1), do: iterator defp flatten_iterator(iterator, depth) do - Elixir.Enum.reduce(1..(depth - 1), iterator, fn _, acc -> method(acc, :flatten) end) + Elixir.Enum.reduce(1..(depth - 1), iterator, fn _, acc -> A.method(acc, :flatten) end) end defp flatten_depth(type), do: do_flatten_depth(type, 0) @@ -150,7 +151,4 @@ defmodule RustQ.Meta.Lower.List do defp type_result(%Type{} = type), do: {:ok, type} defp type_result(_type), do: :unsupported - - defp method(receiver, name, args \\ []), - do: %AST.MethodCall{receiver: receiver, method: name, args: args} end diff --git a/lib/rustq/meta/lower/map.ex b/lib/rustq/meta/lower/map.ex index 50e3200..f3dfecc 100644 --- a/lib/rustq/meta/lower/map.ex +++ b/lib/rustq/meta/lower/map.ex @@ -16,6 +16,7 @@ defmodule RustQ.Meta.Lower.Map do alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported def lower(%Call{function: :new, args: []}, %Context{} = context), @@ -30,11 +31,11 @@ defmodule RustQ.Meta.Lower.Map do # `Map.new/1,2` is a comprehension; see `RustQ.Meta.Desugar`. defp lower_new([], _context), - do: {:ok, %AST.PathCall{path: %AST.Path{parts: [:HashMap, :new]}, args: []}} + do: {:ok, %AST.PathCall{path: A.path([:HashMap, :new]), args: []}} defp lower_keyed(%Call{function: :get, args: [map, key]}, key_type, value_type, context) do lookup = - Collection.call(context.lower.(map), :get, [Collection.borrowed(key, key_type, context)]) + A.method(context.lower.(map), :get, [Collection.borrowed(key, key_type, context)]) {:ok, Collection.read_out(lookup, value_type)} end @@ -46,12 +47,12 @@ defmodule RustQ.Meta.Lower.Map do context ) do lookup = - Collection.call(context.lower.(map), :get, [Collection.borrowed(key, key_type, context)]) + A.method(context.lower.(map), :get, [Collection.borrowed(key, key_type, context)]) {:ok, lookup |> Collection.read_out(value_type) - |> Collection.call(:unwrap_or, [context.lower_expected.(default, value_type)])} + |> A.method(:unwrap_or, [context.lower_expected.(default, value_type)])} end # A missing key raises in Elixir; indexing panics, which Rustler raises. @@ -61,12 +62,12 @@ defmodule RustQ.Meta.Lower.Map do index: Collection.borrowed(key, key_type, context) } - {:ok, if(Collection.copy?(value_type), do: value, else: Collection.call(value, :clone))} + {:ok, if(Collection.copy?(value_type), do: value, else: A.method(value, :clone))} end defp lower_keyed(%Call{function: :has_key?, args: [map, key]}, key_type, _value_type, context) do {:ok, - Collection.call(context.lower.(map), :contains_key, [ + A.method(context.lower.(map), :contains_key, [ Collection.borrowed(key, key_type, context) ])} end @@ -77,10 +78,12 @@ defmodule RustQ.Meta.Lower.Map do map, fn map -> [ - Collection.statement(map, :insert, [ - context.lower_expected.(key, key_type), - context.lower_expected.(value, value_type) - ]) + A.stmt( + A.method(map, :insert, [ + context.lower_expected.(key, key_type), + context.lower_expected.(value, value_type) + ]) + ) ] end, context @@ -92,7 +95,7 @@ defmodule RustQ.Meta.Lower.Map do Collection.updated( map, fn map -> - [Collection.statement(map, :remove, [Collection.borrowed(key, key_type, context)])] + [A.stmt(A.method(map, :remove, [Collection.borrowed(key, key_type, context)]))] end, context )} @@ -103,7 +106,7 @@ defmodule RustQ.Meta.Lower.Map do Collection.updated( map, fn map -> - [Collection.statement(map, :extend, [Collection.owned(other, context)])] + [A.stmt(A.method(map, :extend, [Collection.owned(other, context)]))] end, context )} @@ -116,8 +119,8 @@ defmodule RustQ.Meta.Lower.Map do value_type, context ) do - key_name = %AST.Var{name: :rustq_key} - value_name = %AST.Var{name: :rustq_value} + key_name = A.var(:rustq_key) + value_name = A.var(:rustq_value) {:ok, Collection.updated( @@ -125,9 +128,9 @@ defmodule RustQ.Meta.Lower.Map do fn map -> updated = map - |> Collection.call(:remove, [%AST.Ref{expr: key_name}]) - |> Collection.call(:map, [update_function(fun, value_type, context)]) - |> Collection.call(:unwrap_or, [context.lower_expected.(default, value_type)]) + |> A.method(:remove, [A.ref(key_name)]) + |> A.method(:map, [update_function(fun, value_type, context)]) + |> A.method(:unwrap_or, [context.lower_expected.(default, value_type)]) [ %AST.Let{ @@ -135,7 +138,7 @@ defmodule RustQ.Meta.Lower.Map do expr: context.lower_expected.(key, key_type) }, %AST.Let{pattern: %AST.PatVar{name: :rustq_value}, expr: updated}, - Collection.statement(map, :insert, [key_name, value_name]) + A.stmt(A.method(map, :insert, [key_name, value_name])) ] end, context @@ -148,9 +151,9 @@ defmodule RustQ.Meta.Lower.Map do {:ok, context.lower.(map) - |> Collection.call(method) + |> A.method(method) |> Collection.read_out(type) - |> Collection.call(:collect) + |> A.method(:collect) |> collected(Type.vec(type))} end @@ -158,8 +161,8 @@ defmodule RustQ.Meta.Lower.Map do {:ok, map |> Collection.owned(context) - |> Collection.call(:into_iter) - |> Collection.call(:collect) + |> A.method(:into_iter) + |> A.method(:collect) |> collected(Type.vec(Type.tuple([key_type, value_type])))} end @@ -222,23 +225,21 @@ defmodule RustQ.Meta.Lower.Map do with %Type{} = map_type <- map_type(context.type_of.(map)), %Type{} = field_type <- Type.field_type(map_type, key) do name = :rustq_map_value - variable = %AST.Var{name: name} + variable = A.var(name) {:ok, - %AST.BlockExpr{ - body: [ - %AST.Let{ - pattern: %AST.PatVar{name: name}, - expr: context.lower.(map), - mutable: true - }, - %AST.Assign{ - target: %AST.Field{receiver: variable, field: key}, - expr: context.lower_expected.(value, field_type) - }, - %AST.Return{expr: variable} - ] - }} + A.block_expr([ + %AST.Let{ + pattern: %AST.PatVar{name: name}, + expr: context.lower.(map), + mutable: true + }, + %AST.Assign{ + target: %AST.Field{receiver: variable, field: key}, + expr: context.lower_expected.(value, field_type) + }, + A.return_stmt(variable) + ])} else _unknown -> unsupported_field!(call) end @@ -326,13 +327,11 @@ defmodule RustQ.Meta.Lower.Map do name = :rustq_map_get_value {:ok, - %AST.BlockExpr{ - body: [ - %AST.Let{pattern: %AST.PatVar{name: name}, expr: context.lower.(map)}, - %AST.Let{pattern: %AST.PatWildcard{}, expr: default}, - %AST.Return{expr: %AST.Field{receiver: %AST.Var{name: name}, field: key}} - ] - }} + A.block_expr([ + %AST.Let{pattern: %AST.PatVar{name: name}, expr: context.lower.(map)}, + %AST.Let{pattern: %AST.PatWildcard{}, expr: default}, + A.return_stmt(%AST.Field{receiver: A.var(name), field: key}) + ])} end defp lower_present_get(map, key, _default, false, context), @@ -340,12 +339,10 @@ defmodule RustQ.Meta.Lower.Map do defp lower_missing_get(map, default, context) do {:ok, - %AST.BlockExpr{ - body: [ - %AST.Let{pattern: %AST.PatWildcard{}, expr: context.lower.(map)}, - %AST.Return{expr: default} - ] - }} + A.block_expr([ + %AST.Let{pattern: %AST.PatWildcard{}, expr: context.lower.(map)}, + A.return_stmt(default) + ])} end defp map_type(%Type{kind: :alias, meta: %{target: %Type{} = target}}), do: map_type(target) diff --git a/lib/rustq/meta/lower/map_set.ex b/lib/rustq/meta/lower/map_set.ex index aef32f7..1d26495 100644 --- a/lib/rustq/meta/lower/map_set.ex +++ b/lib/rustq/meta/lower/map_set.ex @@ -11,10 +11,11 @@ defmodule RustQ.Meta.Lower.MapSet do alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported def lower(%Call{function: :new, args: []}, %Context{}), - do: {:ok, %AST.PathCall{path: %AST.Path{parts: [:HashSet, :new]}, args: []}} + do: {:ok, %AST.PathCall{path: A.path([:HashSet, :new]), args: []}} def lower(%Call{args: [set | _]} = call, %Context{} = context) do case Collection.keyed_of(set, context) do @@ -30,7 +31,7 @@ defmodule RustQ.Meta.Lower.MapSet do Collection.updated( set, fn set -> - [Collection.statement(set, :insert, [context.lower_expected.(value, item)])] + [A.stmt(A.method(set, :insert, [context.lower_expected.(value, item)]))] end, context )} @@ -41,15 +42,14 @@ defmodule RustQ.Meta.Lower.MapSet do Collection.updated( set, fn set -> - [Collection.statement(set, :remove, [Collection.borrowed(value, item, context)])] + [A.stmt(A.method(set, :remove, [Collection.borrowed(value, item, context)]))] end, context )} end defp lower_set(%Call{function: :member?, args: [set, value]}, item, context) do - {:ok, - Collection.call(context.lower.(set), :contains, [Collection.borrowed(value, item, context)])} + {:ok, A.method(context.lower.(set), :contains, [Collection.borrowed(value, item, context)])} end defp lower_set(%Call{function: :union, args: [set, other]}, _item, context) do @@ -57,7 +57,7 @@ defmodule RustQ.Meta.Lower.MapSet do Collection.updated( set, fn set -> - [Collection.statement(set, :extend, [Collection.owned(other, context)])] + [A.stmt(A.method(set, :extend, [Collection.owned(other, context)]))] end, context )} @@ -65,11 +65,7 @@ defmodule RustQ.Meta.Lower.MapSet do defp lower_set(%Call{function: function, args: [set, other]}, _item, context) when function in [:difference, :intersection] do - kept = %AST.MethodCall{ - receiver: context.lower.(other), - method: :contains, - args: [%AST.Var{name: :rustq_item}] - } + kept = A.method(context.lower.(other), :contains, [A.var(:rustq_item)]) kept = if function == :difference, do: %AST.UnaryOp{op: :not, expr: kept}, else: kept @@ -77,7 +73,7 @@ defmodule RustQ.Meta.Lower.MapSet do Collection.updated( set, fn set -> - [Collection.statement(set, :retain, [%AST.Closure{args: [:rustq_item], body: kept}])] + [A.stmt(A.method(set, :retain, [%AST.Closure{args: [:rustq_item], body: kept}]))] end, context )} @@ -87,23 +83,19 @@ defmodule RustQ.Meta.Lower.MapSet do when function in [:subset?, :disjoint?] do method = if function == :subset?, do: :is_subset, else: :is_disjoint - {:ok, Collection.call(context.lower.(set), method, [%AST.Ref{expr: context.lower.(other)}])} + {:ok, A.method(context.lower.(set), method, [A.ref(context.lower.(other))])} end defp lower_set(%Call{function: :size, args: [set]}, _item, context) do - {:ok, - %AST.Cast{ - expr: Collection.call(context.lower.(set), :len), - type: %AST.TypePath{parts: [:i64]} - }} + {:ok, A.cast(A.method(context.lower.(set), :len), %AST.TypePath{parts: [:i64]})} end defp lower_set(%Call{function: :to_list, args: [set]}, item, context) do collected = set |> Collection.owned(context) - |> Collection.call(:into_iter) - |> Collection.call(:collect) + |> A.method(:into_iter) + |> A.method(:collect) {:ok, %{collected | generics: [Type.vec(item).ast]}} end diff --git a/lib/rustq/meta/lower/mutability.ex b/lib/rustq/meta/lower/mutability.ex index 7b5294d..62488e5 100644 --- a/lib/rustq/meta/lower/mutability.ex +++ b/lib/rustq/meta/lower/mutability.ex @@ -8,6 +8,7 @@ defmodule RustQ.Meta.Lower.Mutability do alias RustQ.Meta.StdMethods alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A alias RustQ.Rust.AST.Walk @mutating_methods StdMethods.mutating() @@ -33,7 +34,7 @@ defmodule RustQ.Meta.Lower.Mutability do {[_ | _] = updates, [%AST.Return{expr: %AST.Var{name: :rustq_updated}}]} -> case rename_var(updates, :rustq_updated, name) do [update] -> update - updates -> %AST.ExprStmt{expr: %AST.BlockExpr{body: updates}} + updates -> A.stmt(A.block_expr(updates)) end _other -> @@ -43,7 +44,7 @@ defmodule RustQ.Meta.Lower.Mutability do def updated_in_place(statement), do: statement - defp rename_var(%AST.Var{name: from}, from, to), do: %AST.Var{name: to} + defp rename_var(%AST.Var{name: from}, from, to), do: A.var(to) defp rename_var(%module{} = node, from, to) do fields = diff --git a/lib/rustq/meta/lower/pattern.ex b/lib/rustq/meta/lower/pattern.ex index 46f3d59..3d88b63 100644 --- a/lib/rustq/meta/lower/pattern.ex +++ b/lib/rustq/meta/lower/pattern.ex @@ -18,6 +18,7 @@ defmodule RustQ.Meta.Lower.Pattern do alias RustQ.Meta.Type alias RustQ.Meta.Typing alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A alias RustQ.Rust.AST.Walk @doc "Whether `case` clauses match `{:ok, _}`/`{:error, _}` or `{:some, _}`/`nil`." @@ -347,7 +348,7 @@ defmodule RustQ.Meta.Lower.Pattern do do: {:__rustq_path__, [], Paths.mapped_alias_parts(parts, context.rust_modules)} def lower_match_pattern({:__rustq_path__, _, parts}, _case_type), - do: %AST.PatPath{path: %AST.Path{parts: parts}} + do: %AST.PatPath{path: A.path(parts)} def lower_match_pattern(:ok, %Type{kind: kind}) when kind in [:result, :nif_result], do: %AST.PatOk{pattern: %AST.PatTuple{patterns: []}} @@ -420,7 +421,7 @@ defmodule RustQ.Meta.Lower.Pattern do ) when is_list(fields) do %AST.PatStruct{ - path: %AST.Path{parts: [rust_name]}, + path: A.path([rust_name]), fields: lower_struct_pattern_fields(fields, type_fields) } end @@ -431,7 +432,7 @@ defmodule RustQ.Meta.Lower.Pattern do ) when is_list(fields) do %AST.PatStruct{ - path: %AST.Path{parts: [rust_name]}, + path: A.path([rust_name]), fields: lower_struct_pattern_fields(fields, type_fields) } end @@ -443,10 +444,10 @@ defmodule RustQ.Meta.Lower.Pattern do module = List.last(modules) %AST.PatPathTuple{ - path: %AST.Path{parts: [rust_name, module]}, + path: A.path([rust_name, module]), patterns: [ %AST.PatStruct{ - path: %AST.Path{parts: [module]}, + path: A.path([module]), fields: Enum.map(fields, fn {name, pattern} -> {name, lower_match_pattern(pattern, nil)} end) } @@ -474,11 +475,11 @@ defmodule RustQ.Meta.Lower.Pattern do when is_atom(pattern) or is_tuple(pattern) do case Lower.rust_enum_variant(pattern, type) do {variant, [], _types} -> - %AST.PatPath{path: %AST.Path{parts: [rust_name, Lower.rust_variant(variant)]}} + %AST.PatPath{path: A.path([rust_name, Lower.rust_variant(variant)])} {variant, payloads, types} when length(payloads) == length(types) -> %AST.PatPathTuple{ - path: %AST.Path{parts: [rust_name, Lower.rust_variant(variant)]}, + path: A.path([rust_name, Lower.rust_variant(variant)]), patterns: Enum.zip_with(payloads, types, &lower_tuple_element_pattern/2) } @@ -488,7 +489,7 @@ defmodule RustQ.Meta.Lower.Pattern do end def lower_match_pattern(atom, %Type{kind: :enum, rust: rust_name}) when is_atom(atom), - do: %AST.PatPath{path: %AST.Path{parts: [rust_name, Lower.rust_variant(atom)]}} + do: %AST.PatPath{path: A.path([rust_name, Lower.rust_variant(atom)])} def lower_match_pattern(atom, %Type{kind: :atom}) when is_atom(atom), do: %AST.PatAtomGuard{name: atom} diff --git a/lib/rustq/meta/lower/range.ex b/lib/rustq/meta/lower/range.ex index 22e43e7..61a7983 100644 --- a/lib/rustq/meta/lower/range.ex +++ b/lib/rustq/meta/lower/range.ex @@ -7,6 +7,7 @@ defmodule RustQ.Meta.Lower.Range do alias RustQ.Meta.Core.Call alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A alias RustQ.Spec @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported @@ -31,16 +32,11 @@ defmodule RustQ.Meta.Lower.Range do def lower(%Call{function: :in, args: [value, collection]}, %Context{} = context) do receiver = case context.lower.(collection) do - %AST.Range{} = range -> %AST.BlockExpr{body: [%AST.Return{expr: range}]} + %AST.Range{} = range -> A.block_expr([A.return_stmt(range)]) expression -> expression end - {:ok, - %AST.MethodCall{ - receiver: receiver, - method: :contains, - args: [%AST.Ref{expr: context.lower.(value)}] - }} + {:ok, A.method(receiver, :contains, [A.ref(context.lower.(value))])} end def lower(%Call{}, %Context{}), do: :unsupported diff --git a/lib/rustq/meta/lower/stdlib.ex b/lib/rustq/meta/lower/stdlib.ex index 75e0f37..15acdfd 100644 --- a/lib/rustq/meta/lower/stdlib.ex +++ b/lib/rustq/meta/lower/stdlib.ex @@ -9,6 +9,7 @@ defmodule RustQ.Meta.Lower.Stdlib do alias RustQ.Meta.Core.Call alias RustQ.Meta.Type alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A defmodule Context do @moduledoc """ @@ -63,12 +64,10 @@ defmodule RustQ.Meta.Lower.Stdlib do end {:ok, - %AST.BlockExpr{ - body: [ - %AST.Let{pattern: %AST.PatWildcard{}, expr: evaluated}, - %AST.Return{expr: %AST.Literal{value: value}} - ] - }} + A.block_expr([ + %AST.Let{pattern: %AST.PatWildcard{}, expr: evaluated}, + A.return_stmt(A.lit(value)) + ])} end @spec lower(Macro.t(), Context.t()) :: {:ok, term()} | :unsupported @@ -123,7 +122,7 @@ defmodule RustQ.Meta.Lower.Stdlib do {:ok, context.lower.(count)} %Type{kind: kind} when kind in [:u8, :u16, :u32, :u64] -> - {:ok, %AST.Cast{expr: context.lower.(count), type: %AST.TypePath{parts: [:usize]}}} + {:ok, A.cast(context.lower.(count), %AST.TypePath{parts: [:usize]})} _type -> :unsupported diff --git a/lib/rustq/meta/lower/string.ex b/lib/rustq/meta/lower/string.ex index c294770..c320c3c 100644 --- a/lib/rustq/meta/lower/string.ex +++ b/lib/rustq/meta/lower/string.ex @@ -9,6 +9,7 @@ defmodule RustQ.Meta.Lower.String do alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A alias RustQ.Spec @predicate_methods %{ @@ -21,11 +22,9 @@ defmodule RustQ.Meta.Lower.String do def lower(%Call{function: function, args: [string, pattern]}, %Context{} = context) when is_map_key(@predicate_methods, function) do {:ok, - %AST.MethodCall{ - receiver: context.lower.(string), - method: Map.fetch!(@predicate_methods, function), - args: [%AST.Ref{expr: context.lower.(pattern)}] - }} + A.method(context.lower.(string), Map.fetch!(@predicate_methods, function), [ + A.ref(context.lower.(pattern)) + ])} end def lower(%Call{function: :trim, args: [string]}, %Context{} = context), @@ -33,8 +32,8 @@ defmodule RustQ.Meta.Lower.String do {:ok, string |> context.lower.() - |> method(:trim) - |> method(:to_string)} + |> A.method(:trim) + |> A.method(:to_string)} def lower( %Call{function: :replace, args: [string, pattern, replacement]}, @@ -42,14 +41,14 @@ defmodule RustQ.Meta.Lower.String do ), do: {:ok, - method(context.lower.(string), :replace, [ + A.method(context.lower.(string), :replace, [ string_slice(pattern, context), string_slice(replacement, context) ])} def lower(%Call{function: :duplicate, args: [string, count]} = call, %Context{} = context) do case Stdlib.nonnegative_count(count, context) do - {:ok, lowered_count} -> {:ok, method(context.lower.(string), :repeat, [lowered_count])} + {:ok, lowered_count} -> {:ok, A.method(context.lower.(string), :repeat, [lowered_count])} :unsupported -> unsupported_count!(call) end end @@ -57,15 +56,15 @@ defmodule RustQ.Meta.Lower.String do def lower(%Call{function: :valid?, args: [string]}, %Context{} = context) do case context.type_of.(string) do %Type{} = type when type.kind == :binary -> - bytes = %AST.MethodCall{receiver: context.lower.(string), method: :as_slice, args: []} + bytes = A.method(context.lower.(string), :as_slice) {:ok, - %AST.PathCall{path: %AST.Path{parts: [:std, :str, :from_utf8]}, args: [bytes]} + %AST.PathCall{path: A.path([:std, :str, :from_utf8]), args: [bytes]} |> then(&%AST.MethodCall{receiver: &1, method: :is_ok, args: []})} %Type{} = type -> if Type.category(type) == :string, - do: {:ok, %AST.Literal{value: true}}, + do: {:ok, A.lit(true)}, else: :unsupported _unknown -> @@ -87,10 +86,7 @@ defmodule RustQ.Meta.Lower.String do def synth(%Call{}, %TypeContext{}), do: :unsupported defp string_slice(value, context) when is_binary(value), do: context.lower.(value) - defp string_slice(value, context), do: method(context.lower.(value), :as_str) - - defp method(receiver, name, args \\ []), - do: %AST.MethodCall{receiver: receiver, method: name, args: args} + defp string_slice(value, context), do: A.method(context.lower.(value), :as_str) @spec unsupported_count!(Call.t()) :: no_return() defp unsupported_count!(%Call{source: source}) do diff --git a/lib/rustq/meta/lower/tuple.ex b/lib/rustq/meta/lower/tuple.ex index 3d6646e..4b62079 100644 --- a/lib/rustq/meta/lower/tuple.ex +++ b/lib/rustq/meta/lower/tuple.ex @@ -8,13 +8,14 @@ defmodule RustQ.Meta.Lower.Tuple do alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported def lower(%Call{function: :to_list, args: [tuple]} = call, %Context{} = context) do with elements when is_list(elements) <- tuple_elements(context.type_of.(tuple)), true <- homogeneous?(elements) do name = :rustq_tuple_value - variable = %AST.Var{name: name} + variable = A.var(name) values = elements @@ -24,12 +25,10 @@ defmodule RustQ.Meta.Lower.Tuple do end) {:ok, - %AST.BlockExpr{ - body: [ - %AST.Let{pattern: %AST.PatVar{name: name}, expr: context.lower.(tuple)}, - %AST.Return{expr: %AST.VecLiteral{values: values}} - ] - }} + A.block_expr([ + %AST.Let{pattern: %AST.PatVar{name: name}, expr: context.lower.(tuple)}, + A.return_stmt(%AST.VecLiteral{values: values}) + ])} else _unknown -> unsupported_tuple!(call) end diff --git a/lib/rustq/rust/ast/builder.ex b/lib/rustq/rust/ast/builder.ex index 8889c3c..de8f964 100644 --- a/lib/rustq/rust/ast/builder.ex +++ b/lib/rustq/rust/ast/builder.ex @@ -66,7 +66,6 @@ defmodule RustQ.Rust.AST.Builder do MacroItem, MacroItemCall, MacroRepeat, - MacroRepeatExpr, MacroRule, MacroRules, MacroVar, @@ -95,6 +94,8 @@ defmodule RustQ.Rust.AST.Builder do VecLiteral } + @ast_modules AST.__rustq_ast_modules__() + defmacro block(do: body) do quote do Builder.flatten(unquote(block_values(body))) @@ -406,53 +407,8 @@ defmodule RustQ.Rust.AST.Builder do def pat(name) when is_atom(name), do: PatternBuilder.var(name) def wildcard, do: PatternBuilder.wildcard() - def expr(%{__struct__: module} = value) - when module in [ - Use, - AST.Attribute, - AST.Module, - AST.Const, - AST.Static, - AST.TypeAlias, - AST.MacroItem, - AST.MacroItemCall, - AST.Impl, - AST.Var, - AST.Path, - AST.Field, - AST.Index, - AST.Range, - AST.Cast, - AST.UnaryOp, - AST.PathCall, - AST.MethodCall, - AST.LocalCall, - AST.StructLiteral, - AST.Ref, - AST.Try, - AST.Tuple, - AST.VecLiteral, - AST.ArrayLiteral, - MacroRepeatExpr, - AST.Closure, - AST.Literal, - AST.ByteString, - AST.EscapeExpr, - AST.TokenMacro, - AST.MacroCall, - AST.AtomValue, - AST.None, - AST.Some, - AST.Ok, - AST.Err, - AST.NifRaiseAtom, - AST.BlockExpr, - AST.UnsafeBlock, - AST.Match, - AST.If, - AST.BinaryOp - ], - do: value + # Any RustQ AST node is already an expression, statement, or pattern in its own right. + def expr(%{__struct__: module} = value) when module in @ast_modules, do: value def expr(value) when is_boolean(value), do: lit(value) def expr(name) when is_atom(name), do: var(name) From 1d46f465e757841003ff9fda3105bd4d6da93ac1 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 26 Oct 2026 13:55:13 +0400 Subject: [PATCH 08/10] Give standard-library lowerings a behaviour and shared helpers Each Elixir module's lowering implements RustQ.Meta.Lower.Stdlib.Lowering. Using it imports the helpers they share (lowered/2, type_of/2, closure/3, in_use?/2, and the rest), which call Lower and Typing directly instead of through a context of closures. Stdlib dispatches a call to the module named after its Elixir module when that module implements the behaviour, in place of a clause per module for lowering and another for typing. --- lib/rustq/meta/lower.ex | 76 +++++----- lib/rustq/meta/lower/collection.ex | 12 +- lib/rustq/meta/lower/enum.ex | 153 ++++++++++---------- lib/rustq/meta/lower/kernel.ex | 69 ++++----- lib/rustq/meta/lower/list.ex | 41 +++--- lib/rustq/meta/lower/map.ex | 67 +++++---- lib/rustq/meta/lower/map_set.ex | 24 ++-- lib/rustq/meta/lower/range.ex | 18 ++- lib/rustq/meta/lower/stdlib.ex | 181 +++++------------------- lib/rustq/meta/lower/stdlib/lowering.ex | 168 ++++++++++++++++++++++ lib/rustq/meta/lower/string.ex | 31 ++-- lib/rustq/meta/lower/tuple.ex | 16 +-- lib/rustq/meta/typing.ex | 9 +- 13 files changed, 449 insertions(+), 416 deletions(-) create mode 100644 lib/rustq/meta/lower/stdlib/lowering.ex diff --git a/lib/rustq/meta/lower.ex b/lib/rustq/meta/lower.ex index 8575fef..09dfeb5 100644 --- a/lib/rustq/meta/lower.ex +++ b/lib/rustq/meta/lower.ex @@ -24,7 +24,7 @@ defmodule RustQ.Meta.Lower do alias RustQ.Meta.Lower.Paths alias RustQ.Meta.Lower.Pattern, as: LowerPattern alias RustQ.Meta.Lower.Stdlib - alias RustQ.Meta.Lower.Stdlib.Context, as: StdlibContext + alias RustQ.Meta.Lower.Stdlib.Lowering.Context, as: StdlibContext alias RustQ.Meta.Ownership alias RustQ.Meta.RustMacro alias RustQ.Meta.Semantics @@ -60,6 +60,8 @@ defmodule RustQ.Meta.Lower do rust_macros: %{}, built_from: %{} ] + + @type t :: %__MODULE__{} end alias __MODULE__.Env, as: Context @@ -2370,21 +2372,8 @@ defmodule RustQ.Meta.Lower do } end - defp stdlib_context(%Context{} = context, expected \\ nil) do - %StdlibContext{ - lower: &lower_expr(&1, context), - lower_expected: &lower_expr(&1, &2, context), - lower_binary_operand: &lower_binary_operand(&1, context), - lower_closure: &lower_closure_args(&1, &2, context), - lower_typed_closure: &lower_closure_args(&1, &2, context, &3), - lower_closure_body: &lower_closure_body(&1, &2, context), - lower_capture: &lower_function_capture(&1, context), - closure_arg: &closure_arg!/1, - type_of: &Typing.synth(&1, typing_env(context)), - in_use?: &in_use?(&1, context), - expected: expected - } - end + defp stdlib_context(%Context{} = context, expected \\ nil), + do: %StdlibContext{env: context, expected: expected} @doc "Whether a collection is still in use after this use of it; see `RustQ.Meta.Ownership`." # A collection is in use unless this is its value's last use; see `RustQ.Meta.Ownership`. A @@ -2400,7 +2389,8 @@ defmodule RustQ.Meta.Lower do end end - defp lower_binary_operand(expression, %Context{} = context) do + @doc "`expression`, lowered as an operand: a fallible one propagates its error with `?`." + def lower_binary_operand(expression, %Context{} = context) do if fallible_expression?(expression, context) and propagation_allowed?(context.return_type) do %AST.Try{expr: lower_expr(expression, context)} else @@ -2447,8 +2437,9 @@ defmodule RustQ.Meta.Lower do } end - defp lower_closure_args(args, body, %Context{} = context, expected_return_type \\ nil) - when is_list(args) do + @doc "A closure taking `args` and returning `body`, as a value of `expected_return_type` when given." + def lower_closure_args(args, body, %Context{} = context, expected_return_type \\ nil) + when is_list(args) do {args, typed} = Enum.map_reduce(args, [], &untyped_closure_arg/2) captured = @@ -2485,11 +2476,12 @@ defmodule RustQ.Meta.Lower do defp untyped_closure_arg(arg, typed), do: {arg, typed} - defp lower_closure_body( - {:__block__, _, [_first, _second | _rest] = expressions}, - expected_return_type, - %Context{} = context - ) do + @doc "A closure's `body`, lowered to return a value of `expected_return_type` when given." + def lower_closure_body( + {:__block__, _, [_first, _second | _rest] = expressions}, + expected_return_type, + %Context{} = context + ) do body = case expected_return_type do %Type{} = type -> lower_expected_block(expressions, context, type) @@ -2499,7 +2491,7 @@ defmodule RustQ.Meta.Lower do A.block_expr(body) end - defp lower_closure_body(body, expected_return_type, %Context{} = context), + def lower_closure_body(body, expected_return_type, %Context{} = context), do: lower_expr(closure_body_expr(body), expected_return_type, context) defp lower_value_block(expressions, %Context{} = context) do @@ -2518,23 +2510,24 @@ defmodule RustQ.Meta.Lower do defp map_type?(_type), do: false - defp lower_function_capture({name, _, capture_context}, %Context{}) - when is_atom(name) and is_atom(capture_context), - do: A.path([name]) + @doc "A captured named function, as the Rust path of that function." + def lower_function_capture({name, _, capture_context}, %Context{}) + when is_atom(name) and is_atom(capture_context), + do: A.path([name]) - defp lower_function_capture( - {{:., _, [{:__aliases__, _, [:Super]}, name]}, _, []}, - %Context{} - ), - do: A.path([:super, name]) + def lower_function_capture( + {{:., _, [{:__aliases__, _, [:Super]}, name]}, _, []}, + %Context{} + ), + do: A.path([:super, name]) - defp lower_function_capture( - {{:., _, [{:__aliases__, _, parts}, name]}, _, []}, - %Context{} = context - ), - do: A.path(Paths.mapped_alias_parts(parts, context.rust_modules) ++ [name]) + def lower_function_capture( + {{:., _, [{:__aliases__, _, parts}, name]}, _, []}, + %Context{} = context + ), + do: A.path(Paths.mapped_alias_parts(parts, context.rust_modules) ++ [name]) - defp lower_function_capture(other, %Context{}) do + def lower_function_capture(other, %Context{}) do Diagnostic.lower( :unsupported_function_capture, other, @@ -2543,9 +2536,10 @@ defmodule RustQ.Meta.Lower do ) end - defp closure_arg!({name, _, context}) when is_atom(name) and is_atom(context), do: name + @doc "A closure argument, as a Rust closure argument: a name, or a binding pattern." + def closure_arg!({name, _, context}) when is_atom(name) and is_atom(context), do: name - defp closure_arg!(other), do: LowerPattern.lower_binding_pattern(other) + def closure_arg!(other), do: LowerPattern.lower_binding_pattern(other) defp closure_body_expr([expression]), do: expression defp closure_body_expr(expression), do: expression diff --git a/lib/rustq/meta/lower/collection.ex b/lib/rustq/meta/lower/collection.ex index 7d8166e..6b90d83 100644 --- a/lib/rustq/meta/lower/collection.ex +++ b/lib/rustq/meta/lower/collection.ex @@ -9,7 +9,9 @@ defmodule RustQ.Meta.Lower.Collection do { let mut rustq_updated = scores; rustq_updated.insert(name, 1); rustq_updated } """ - alias RustQ.Meta.Lower.Stdlib.Context + import RustQ.Meta.Lower.Stdlib.Lowering, only: [lowered: 2, lowered: 3, type_of: 2, in_use?: 2] + + alias RustQ.Meta.Lower.Stdlib.Lowering.Context alias RustQ.Meta.Type alias RustQ.Rust.AST alias RustQ.Rust.AST.Builder, as: A @@ -51,7 +53,7 @@ defmodule RustQ.Meta.Lower.Collection do @doc "The keyed collection `collection` is, by its type." @spec keyed_of(Macro.t(), Context.t()) :: keyed() | nil - def keyed_of(collection, %Context{} = context), do: keyed(context.type_of.(collection)) + def keyed_of(collection, %Context{} = context), do: keyed(type_of(collection, context)) @doc """ An update of `collection`: `statements` receives the variable holding the collection, or a @@ -77,9 +79,9 @@ defmodule RustQ.Meta.Lower.Collection do @doc "`collection` to take: itself at its last use, otherwise a copy." @spec owned(Macro.t(), Context.t()) :: AST.expr() def owned(collection, %Context{} = context) do - lowered = context.lower.(collection) + lowered = lowered(collection, context) - if context.in_use?.(collection), + if in_use?(collection, context), do: A.method(lowered, :clone), else: lowered end @@ -87,7 +89,7 @@ defmodule RustQ.Meta.Lower.Collection do @doc "A key or item passed by reference, borrowed unless it already is one." @spec borrowed(Macro.t(), Type.t(), Context.t()) :: AST.expr() def borrowed(expression, %Type{} = type, %Context{} = context), - do: context.lower_expected.(expression, Type.ref(type)) + do: lowered(expression, Type.ref(type), context) @doc "A value read out of a collection: copied when that costs nothing, otherwise cloned." @spec read_out(AST.expr(), Type.t()) :: AST.expr() diff --git a/lib/rustq/meta/lower/enum.ex b/lib/rustq/meta/lower/enum.ex index aa97657..0a528bf 100644 --- a/lib/rustq/meta/lower/enum.ex +++ b/lib/rustq/meta/lower/enum.ex @@ -4,19 +4,16 @@ defmodule RustQ.Meta.Lower.Enum do searching, sorting, and zipping for `RustQ.Meta.Lower.Stdlib`, and types such calls. """ + use RustQ.Meta.Lower.Stdlib.Lowering + alias RustQ.Diagnostic alias RustQ.Meta.Capture - alias RustQ.Meta.Core.Call alias RustQ.Meta.Desugar alias RustQ.Meta.Lower.Collection - alias RustQ.Meta.Lower.Stdlib - alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type - alias RustQ.Rust.AST - alias RustQ.Rust.AST.Builder, as: A alias RustQ.Spec - @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported + @impl true def lower(%Call{args: args} = call, %Context{} = context), do: lower_call(%{call | args: Enum.map(args, &Capture.expand/1)}, context) @@ -63,7 +60,7 @@ defmodule RustQ.Meta.Lower.Enum do defp lower_call(%Call{function: :member?, args: [collection, value]}, %Context{} = context), do: {:ok, - A.method(lower_collection(collection, context), :contains, [A.ref(context.lower.(value))])} + A.method(lower_collection(collection, context), :contains, [A.ref(lowered(value, context))])} defp lower_call(%Call{function: :find, args: [collection, predicate]}, %Context{} = context) do iterator = iterate(collection, :keeps_items, context) @@ -79,14 +76,14 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, iterator |> lower_filter(predicate, false, collection, context, :find) - |> A.method(:unwrap_or, [context.lower.(default)])} + |> A.method(:unwrap_or, [lowered(default, context)])} end defp lower_call(%Call{function: :concat, args: [collections]}, %Context{} = context), do: {:ok, collections - |> context.lower.() + |> lowered(context) |> A.method(:into_iter) |> A.method(:flatten) |> A.method(:collect)} @@ -95,25 +92,25 @@ defmodule RustQ.Meta.Lower.Enum do do: {:ok, left - |> context.lower.() + |> lowered(context) |> A.method(:into_iter) - |> A.method(:chain, [context.lower.(right)]) + |> A.method(:chain, [lowered(right, context)]) |> A.method(:collect)} defp lower_call(%Call{function: :zip, args: [left, right]}, %Context{} = context), do: {:ok, left - |> context.lower.() + |> lowered(context) |> A.method(:into_iter) - |> A.method(:zip, [context.lower.(right)]) + |> A.method(:zip, [lowered(right, context)]) |> A.method(:collect)} defp lower_call(%Call{function: :unzip, args: [collection]}, %Context{} = context), do: {:ok, collection - |> context.lower.() + |> lowered(context) |> A.method(:into_iter) |> A.method(:unzip)} @@ -126,7 +123,7 @@ defmodule RustQ.Meta.Lower.Enum do |> A.method(:collect)} defp lower_call(%Call{function: :sort, args: [collection]} = call, %Context{} = context) do - case collection_inner(context.type_of.(collection)) do + case collection_inner(type_of(collection, context)) do %Type{} = inner when inner.kind in [:i8, :i16, :i32, :i64, :isize, :u8, :u16, :u32, :u64, :usize] -> name = :rustq_sorted_values @@ -134,7 +131,7 @@ defmodule RustQ.Meta.Lower.Enum do {:ok, A.block_expr([ - %AST.Let{pattern: %AST.PatVar{name: name}, expr: context.lower.(collection)}, + %AST.Let{pattern: %AST.PatVar{name: name}, expr: lowered(collection, context)}, A.stmt(A.method(variable, :sort)), A.return_stmt(variable) ])} @@ -154,7 +151,7 @@ defmodule RustQ.Meta.Lower.Enum do %Context{} = context ) when operation in [:take, :drop] do - case Stdlib.nonnegative_count(count, context) do + case nonnegative_count(count, context) do {:ok, lowered_count} -> method = if operation == :drop, do: :skip, else: :take @@ -236,12 +233,12 @@ defmodule RustQ.Meta.Lower.Enum do ) do iteration = iteration(enumerable, :maps_items, context) [item] = typed_items([item], enumerable, iteration, context) - closure = context.lower_closure.([accumulator, item], continuation(body)) + closure = closure([accumulator, item], continuation(body), context) {:ok, enumerable |> iterate_as(iteration, context) - |> A.method(:try_fold, [context.lower.(initial), closure]) + |> A.method(:try_fold, [lowered(initial, context), closure]) |> A.method(:unwrap_or_else, [ %AST.Closure{args: [:rustq_halted], body: A.var(:rustq_halted)} ])} @@ -290,7 +287,7 @@ defmodule RustQ.Meta.Lower.Enum do end defp lower_call(%Call{function: :split_with, args: [enumerable, predicate]}, context) do - case context.type_of.(enumerable) |> collection_inner() do + case type_of(enumerable, context) |> collection_inner() do %Type{} = inner -> iterator = iterate(enumerable, :keeps_items, context) partitioned = lower_filter(iterator, predicate, false, enumerable, context, :partition) @@ -302,18 +299,18 @@ defmodule RustQ.Meta.Lower.Enum do end defp lower_call(%Call{function: :at, args: [enumerable, index | default]} = call, context) do - case Stdlib.nonnegative_count(index, context) do + case nonnegative_count(index, context) do {:ok, index} -> found = - context.lower.(enumerable) + lowered(enumerable, context) |> A.method(:get, [index]) |> Collection.read_out( - collection_inner(context.type_of.(enumerable)) || %Type{kind: :type} + collection_inner(type_of(enumerable, context)) || %Type{kind: :type} ) case default do [] -> {:ok, found} - [default] -> {:ok, A.method(found, :unwrap_or, [context.lower.(default)])} + [default] -> {:ok, A.method(found, :unwrap_or, [lowered(default, context)])} end :unsupported -> @@ -322,8 +319,8 @@ defmodule RustQ.Meta.Lower.Enum do end defp lower_call(%Call{function: :slice, args: [enumerable, start, amount]} = call, context) do - with {:ok, start} <- Stdlib.nonnegative_count(start, context), - {:ok, amount} <- Stdlib.nonnegative_count(amount, context) do + with {:ok, start} <- nonnegative_count(start, context), + {:ok, amount} <- nonnegative_count(amount, context) do {:ok, enumerable |> iterate(:keeps_items, context) @@ -350,22 +347,22 @@ defmodule RustQ.Meta.Lower.Enum do # `zip` takes anything iterable, so an owned list goes in as it is. rights = case iteration(right, :maps_items, context) do - :owned -> context.lower.(right) + :owned -> lowered(right, context) iteration -> iterate_as(right, iteration, context) end {:ok, lefts |> A.method(:zip, [rights]) - |> A.method(:map, [context.lower_closure.([{x, y}], body)]) + |> A.method(:map, [closure([{x, y}], body, context)]) |> collect(context)} end defp lower_call(%Call{function: :chunk_every, args: [enumerable, count]} = call, context) do - case Stdlib.nonnegative_count(count, context) do + case nonnegative_count(count, context) do {:ok, count} -> {:ok, - context.lower.(enumerable) + lowered(enumerable, context) |> A.method(:chunks, [count]) |> A.method(:map, [ %AST.Closure{args: [:chunk], body: A.method(A.var(:chunk), :to_vec)} @@ -378,9 +375,9 @@ defmodule RustQ.Meta.Lower.Enum do end defp lower_call(%Call{function: :join, args: [enumerable | separator]}, context) do - if strings?(context.type_of.(enumerable)), + if strings?(type_of(enumerable, context)), do: - {:ok, A.method(context.lower.(enumerable), :join, [join_separator(separator, context)])}, + {:ok, A.method(lowered(enumerable, context), :join, [join_separator(separator, context)])}, else: :unsupported end @@ -412,15 +409,15 @@ defmodule RustQ.Meta.Lower.Enum do defp lower_call(%Call{}, %Context{}), do: :unsupported - @spec synth(Call.t(), TypeContext.t()) :: {:ok, Type.t()} | :unsupported + @impl true def synth(%Call{function: :sum, args: [collection]}, %TypeContext{} = context), - do: collection |> context.type_of.() |> collection_inner_result() + do: collection |> type_of(context) |> collection_inner_result() def synth( %Call{function: :reduce, args: [_collection, initial, _reducer]}, %TypeContext{} = context ), - do: type_result(context.type_of.(initial)) + do: type_result(type_of(initial, context)) def synth(%Call{function: operation}, %TypeContext{}) when operation in [:any?, :all?], do: {:ok, Spec.type(quote(do: boolean()))} @@ -430,7 +427,7 @@ defmodule RustQ.Meta.Lower.Enum do def synth(%Call{function: operation, args: [collection, _mapper]}, %TypeContext{} = context) when operation in [:filter, :reject] do - case context.type_of.(collection) do + case type_of(collection, context) do %Type{} = type -> case Type.vec_inner(type) || Type.slice_inner(type) do %Type{} = inner -> {:ok, Type.vec(inner)} @@ -443,7 +440,7 @@ defmodule RustQ.Meta.Lower.Enum do end def synth(%Call{function: :map, args: [collection, mapper]}, %TypeContext{} = context) do - with %Type{} = inner <- collection_inner(context.type_of.(collection)), + with %Type{} = inner <- collection_inner(type_of(collection, context)), %Type{} = mapped <- mapper_type(mapper, inner, context) do {:ok, Type.vec(mapped)} else @@ -452,7 +449,7 @@ defmodule RustQ.Meta.Lower.Enum do end def synth(%Call{function: :flat_map, args: [collection, mapper]}, %TypeContext{} = context) do - with %Type{} = inner <- collection_inner(context.type_of.(collection)), + with %Type{} = inner <- collection_inner(type_of(collection, context)), %Type{} = mapped_collection <- mapper_type(mapper, inner, context), %Type{} = mapped <- collection_inner(mapped_collection) do {:ok, Type.vec(mapped)} @@ -472,7 +469,7 @@ defmodule RustQ.Meta.Lower.Enum do )} def synth(%Call{function: :find, args: [collection, _predicate]}, %TypeContext{} = context) do - case collection_inner(context.type_of.(collection)) do + case collection_inner(type_of(collection, context)) do %Type{} = inner -> {:ok, Type.option(inner)} nil -> :unsupported end @@ -482,10 +479,10 @@ defmodule RustQ.Meta.Lower.Enum do %Call{function: :find, args: [_collection, default, _predicate]}, %TypeContext{} = context ), - do: type_result(context.type_of.(default)) + do: type_result(type_of(default, context)) def synth(%Call{function: :concat, args: [collections]}, %TypeContext{} = context) do - with %Type{} = nested <- collection_inner(context.type_of.(collections)), + with %Type{} = nested <- collection_inner(type_of(collections, context)), %Type{} = inner <- collection_inner(nested) do {:ok, Type.vec(inner)} else @@ -494,15 +491,15 @@ defmodule RustQ.Meta.Lower.Enum do end def synth(%Call{function: :concat, args: [left, _right]}, %TypeContext{} = context), - do: collection_vec_result(context.type_of.(left)) + do: collection_vec_result(type_of(left, context)) def synth(%Call{function: operation, args: [collection | _rest]}, %TypeContext{} = context) when operation in [:reverse, :sort, :take, :drop], - do: collection_vec_result(context.type_of.(collection)) + do: collection_vec_result(type_of(collection, context)) def synth(%Call{function: :zip, args: [left, right]}, %TypeContext{} = context) do - with %Type{} = left_inner <- collection_inner(context.type_of.(left)), - %Type{} = right_inner <- collection_inner(context.type_of.(right)) do + with %Type{} = left_inner <- collection_inner(type_of(left, context)), + %Type{} = right_inner <- collection_inner(type_of(right, context)) do {:ok, Type.vec(Type.tuple([left_inner, right_inner]))} else _unknown -> :unsupported @@ -510,7 +507,7 @@ defmodule RustQ.Meta.Lower.Enum do end def synth(%Call{function: :unzip, args: [collection]}, %TypeContext{} = context) do - with %Type{} = pair <- collection_inner(context.type_of.(collection)), + with %Type{} = pair <- collection_inner(type_of(collection, context)), %Type{kind: :tuple, meta: %{elements: [left, right]}} <- pair do {:ok, Type.tuple([Type.vec(left), Type.vec(right)])} else @@ -520,10 +517,10 @@ defmodule RustQ.Meta.Lower.Enum do def synth(%Call{function: function, args: [collection | _]}, %TypeContext{} = context) when function in [:sort_by, :uniq, :uniq_by, :dedup, :slice, :take_while, :drop_while], - do: collection_vec_result(context.type_of.(collection)) + do: collection_vec_result(type_of(collection, context)) def synth(%Call{function: :with_index, args: [collection]}, %TypeContext{} = context) do - case collection_inner(context.type_of.(collection)) do + case collection_inner(type_of(collection, context)) do %Type{} = inner -> {:ok, Type.vec(Type.tuple([inner, usize()]))} nil -> :unsupported end @@ -534,7 +531,7 @@ defmodule RustQ.Meta.Lower.Enum do def synth(%Call{function: :find_value, args: [collection, function]}, %TypeContext{} = context) do # It gives an `Option` even when what it finds has no known type. found = - with %Type{} = inner <- collection_inner(context.type_of.(collection)), + with %Type{} = inner <- collection_inner(type_of(collection, context)), %Type{} = found <- mapper_type(function, inner, context) do found else @@ -545,11 +542,11 @@ defmodule RustQ.Meta.Lower.Enum do end def synth(%Call{function: :reduce_while, args: [_collection, initial, _reducer]}, context), - do: type_result(context.type_of.(initial)) + do: type_result(type_of(initial, context)) def synth(%Call{function: function, args: [collection | rest]}, %TypeContext{} = context) when function in [:max_by, :min_by, :max, :min] do - case {collection_inner(context.type_of.(collection)), List.last(rest)} do + case {collection_inner(type_of(collection, context)), List.last(rest)} do {nil, _fallback} -> :unsupported {inner, {:fn, _, [{:->, _, [[], nil]}]}} -> {:ok, Type.option(inner)} {inner, _default} -> {:ok, inner} @@ -557,13 +554,13 @@ defmodule RustQ.Meta.Lower.Enum do end def synth(%Call{function: :split_with, args: [collection, _predicate]}, context) do - with {:ok, list} <- collection_vec_result(context.type_of.(collection)) do + with {:ok, list} <- collection_vec_result(type_of(collection, context)) do {:ok, Type.tuple([list, list])} end end def synth(%Call{function: :at, args: [collection | rest]}, %TypeContext{} = context) do - case {collection_inner(context.type_of.(collection)), rest} do + case {collection_inner(type_of(collection, context)), rest} do {%Type{} = inner, [_index]} -> {:ok, Type.option(inner)} {%Type{} = inner, [_index, _default]} -> {:ok, inner} _unknown -> :unsupported @@ -571,7 +568,7 @@ defmodule RustQ.Meta.Lower.Enum do end def synth(%Call{function: :chunk_every, args: [collection, _count]}, context) do - with {:ok, list} <- collection_vec_result(context.type_of.(collection)) do + with {:ok, list} <- collection_vec_result(type_of(collection, context)) do {:ok, Type.vec(list)} end end @@ -588,9 +585,9 @@ defmodule RustQ.Meta.Lower.Enum do closure = if mapped_type do - context.lower_typed_closure.(args, body, mapped_type) + closure(args, body, mapped_type, context) else - context.lower_closure.(args, body) + closure(args, body, context) end collection @@ -602,7 +599,7 @@ defmodule RustQ.Meta.Lower.Enum do defp lower_map(collection, {:&, _, [{:/, _, [capture, 1]}]}, context) do collection |> iterate(:maps_items, context) - |> A.method(:map, [context.lower_capture.(capture)]) + |> A.method(:map, [capture(capture, context)]) |> collect(context) end @@ -623,11 +620,11 @@ defmodule RustQ.Meta.Lower.Enum do ) do iteration = iteration(collection, :maps_items, context) [item] = typed_items([item], collection, iteration, context) - closure = context.lower_closure.([accumulator, item], body) + closure = closure([accumulator, item], body, context) collection |> iterate_as(iteration, context) - |> A.method(:fold, [context.lower.(initial), closure]) + |> A.method(:fold, [lowered(initial, context), closure]) end defp lower_reduce(_collection, _initial, other, _context) do @@ -693,12 +690,12 @@ defmodule RustQ.Meta.Lower.Enum do context, terminal ) do - name = context.closure_arg.(argument) - condition = context.lower_closure_body.(body, nil) + name = closure_arg(argument) + condition = closure_body(body, nil, context) condition = if negate?, do: %AST.UnaryOp{op: :not, expr: condition}, else: condition argument = - if Collection.copy?(collection_inner(context.type_of.(collection))), + if Collection.copy?(collection_inner(type_of(collection, context))), do: %AST.PatRef{pattern: %AST.PatVar{name: name}}, else: name @@ -733,10 +730,10 @@ defmodule RustQ.Meta.Lower.Enum do defp kept_items(iterator, _finish), do: {iterator, & &1} defp mapper({:fn, _, [{:->, _, [args, body]}]}, context), - do: context.lower_closure.(args, body) + do: closure(args, body, context) defp mapper({:&, _, [{:/, _, [capture, 1]}]}, context), - do: context.lower_capture.(capture) + do: capture(capture, context) defp mapper(other, _context) do Diagnostic.lower( @@ -753,7 +750,7 @@ defmodule RustQ.Meta.Lower.Enum do context ) when is_atom(name) and is_atom(ast_context), - do: context.type_with_vars.(body, %{name => inner}) + do: type_with_vars(body, %{name => inner}, context) defp mapper_type(_mapper, _inner, _context), do: nil @@ -778,7 +775,7 @@ defmodule RustQ.Meta.Lower.Enum do # A list is iterated by value, so a closure given its items may treat each as the list's # element type, as a struct update needs; filters see references and keep untyped arguments. defp typed_items([item | rest], collection, iteration, context) do - case context.type_of.(collection) do + case type_of(collection, context) do %Type{kind: :vec} = type -> inner = Type.vec_inner(type) inner = if iteration == :borrowed, do: Type.ref(inner), else: inner @@ -819,7 +816,7 @@ defmodule RustQ.Meta.Lower.Enum do # A stage's iterator, without the list it would collect: `list |> Enum.map(f) |> Enum.find(p)` # is one Rust chain, `.map(f).find(p)`, since no list between them is ever seen. defp continued(stage, context) do - case context.lower.(stage) do + case lowered(stage, context) do %AST.MethodCall{method: :collect, receiver: iterator} -> iterator collected -> A.method(collected, :into_iter) end @@ -861,7 +858,7 @@ defmodule RustQ.Meta.Lower.Enum do case iteration do :owned -> - if borrowed_collection?(context.type_of.(collection)), + if borrowed_collection?(type_of(collection, context)), do: A.method(receiver, :iter), else: A.method(receiver, :into_iter) @@ -883,7 +880,7 @@ defmodule RustQ.Meta.Lower.Enum do defp iteration(collection, use, context) do case stage(collection) do {:ok, _stage} -> :owned - :error -> iteration(context.type_of.(collection), collection, use, context) + :error -> iteration(type_of(collection, context), collection, use, context) end end @@ -895,7 +892,7 @@ defmodule RustQ.Meta.Lower.Enum do defp list_iteration(collection, type, use, context) do copy? = Collection.copy?(Type.vec_inner(type)) - in_use? = (use == :question and not copy?) or context.in_use?.(collection) + in_use? = (use == :question and not copy?) or in_use?(collection, context) cond do not in_use? -> :owned @@ -919,9 +916,9 @@ defmodule RustQ.Meta.Lower.Enum do defp borrowed_collection?(_type), do: false defp lower_collection(collection, context) do - case context.type_of.(collection) do - %Type{kind: :vec} = type -> context.lower_expected.(collection, type) - _unknown -> context.lower.(collection) + case type_of(collection, context) do + %Type{kind: :vec} = type -> lowered(collection, type, context) + _unknown -> lowered(collection, context) end end @@ -960,13 +957,13 @@ defmodule RustQ.Meta.Lower.Enum do item = Macro.var(:rustq_item, __MODULE__) arg = - case collection_inner(context.type_of.(enumerable)) do + case collection_inner(type_of(enumerable, context)) do %Type{} = inner -> {:__rustq_typed_arg__, [], [item, Type.ref(inner)]} nil -> item end %AST.Closure{body: body} = - closure = context.lower_closure.([arg], Desugar.apply_function(key, item)) + closure = closure([arg], Desugar.apply_function(key, item), context) if order == :desc, do: %{ @@ -983,10 +980,10 @@ defmodule RustQ.Meta.Lower.Enum do defp empty_fallback({:fn, _, [{:->, _, [[], default]}]}, context) do if Macro.quoted_literal?(default) or match?({name, _, ctx} when is_atom(name) and is_atom(ctx), default) do - {:ok, &A.method(&1, :unwrap_or, [context.lower.(default)])} + {:ok, &A.method(&1, :unwrap_or, [lowered(default, context)])} else {:ok, - &A.method(&1, :unwrap_or_else, [%AST.Closure{args: [], body: context.lower.(default)}])} + &A.method(&1, :unwrap_or_else, [%AST.Closure{args: [], body: lowered(default, context)}])} end end @@ -1008,7 +1005,7 @@ defmodule RustQ.Meta.Lower.Enum do defp usize, do: RustQ.Spec.type(quote(do: RustQ.Type.usize())) defp join_separator([], _context), do: A.lit("") - defp join_separator([separator], context), do: context.lower.(separator) + defp join_separator([separator], context), do: lowered(separator, context) @spec unsupported_count!(Call.t()) :: no_return() defp unsupported_count!(%Call{function: function, source: source}) do diff --git a/lib/rustq/meta/lower/kernel.ex b/lib/rustq/meta/lower/kernel.ex index d4c2709..d16cb22 100644 --- a/lib/rustq/meta/lower/kernel.ex +++ b/lib/rustq/meta/lower/kernel.ex @@ -4,14 +4,10 @@ defmodule RustQ.Meta.Lower.Kernel do `is_nil/1` for `RustQ.Meta.Lower.Stdlib`, and types such calls. """ - import RustQ.Meta.Lower.Stdlib, only: [evaluated_constant: 3, shape_type: 1] + use RustQ.Meta.Lower.Stdlib.Lowering alias RustQ.Diagnostic - alias RustQ.Meta.Core.Call - alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type - alias RustQ.Rust.AST - alias RustQ.Rust.AST.Builder, as: A alias RustQ.Spec @binary_ops %{ @@ -33,9 +29,12 @@ defmodule RustQ.Meta.Lower.Kernel do @comparison_functions [:==, :!=, :<, :<=, :>, :>=, :and, :or, :not, :is_nil] - @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported + @impl true + def lower(%Call{function: function} = call, %Context{} = context) when function in [:.., :in], + do: RustQ.Meta.Lower.Range.lower(call, context) + def lower(%Call{function: :/, args: [left, right]} = call, %Context{} = context) do - if integer_type?(context.type_of.(left)) or integer_type?(context.type_of.(right)) do + if integer_type?(type_of(left, context)) or integer_type?(type_of(right, context)) do unsupported_semantics!( call, "Kernel.//2 with integer operands requires an explicit floating-point conversion" @@ -43,9 +42,9 @@ defmodule RustQ.Meta.Lower.Kernel do else {:ok, %AST.BinaryOp{ - left: context.lower_binary_operand.(left), + left: operand(left, context), op: :div, - right: context.lower_binary_operand.(right) + right: operand(right, context) }} end end @@ -55,8 +54,8 @@ defmodule RustQ.Meta.Lower.Kernel do when function in [:==, :!=] do lower_operand = if wrapper_comparison?(left, right, context), - do: context.lower, - else: context.lower_binary_operand + do: &lowered(&1, context), + else: &operand(&1, context) {:ok, %AST.BinaryOp{ @@ -70,28 +69,28 @@ defmodule RustQ.Meta.Lower.Kernel do when is_map_key(@binary_ops, function) do {:ok, %AST.BinaryOp{ - left: context.lower_binary_operand.(left), + left: operand(left, context), op: Map.fetch!(@binary_ops, function), - right: context.lower_binary_operand.(right) + right: operand(right, context) }} end def lower(%Call{function: :-, args: [expression]}, %Context{} = context), - do: {:ok, %AST.UnaryOp{op: :neg, expr: context.lower.(expression)}} + do: {:ok, %AST.UnaryOp{op: :neg, expr: lowered(expression, context)}} def lower(%Call{function: :+, args: [expression]}, %Context{} = context), - do: {:ok, context.lower.(expression)} + do: {:ok, lowered(expression, context)} def lower(%Call{function: :not, args: [expression]}, %Context{} = context), - do: {:ok, %AST.UnaryOp{op: :not, expr: context.lower.(expression)}} + do: {:ok, %AST.UnaryOp{op: :not, expr: lowered(expression, context)}} def lower(%Call{function: :abs, args: [expression]}, %Context{} = context), - do: {:ok, A.method(context.lower.(expression), :abs)} + do: {:ok, A.method(lowered(expression, context), :abs)} def lower(%Call{function: function, args: [left, right]} = call, %Context{} = context) when function in [:min, :max] do - if integer_type?(context.type_of.(left)) and integer_type?(context.type_of.(right)) do - {:ok, A.method(context.lower.(left), function, [context.lower.(right)])} + if integer_type?(type_of(left, context)) and integer_type?(type_of(right, context)) do + {:ok, A.method(lowered(left, context), function, [lowered(right, context)])} else unsupported_semantics!( call, @@ -102,10 +101,10 @@ defmodule RustQ.Meta.Lower.Kernel do def lower(%Call{function: function, args: [value]}, %Context{} = context) when function in [:byte_size, :length], - do: {:ok, cast_i64(A.method(context.lower.(value), :len))} + do: {:ok, cast_i64(A.method(lowered(value, context), :len))} def lower(%Call{function: :map_size, args: [map]} = call, %Context{} = context) do - case struct_fields(shape_type(context.type_of.(map))) do + case struct_fields(shape_type(type_of(map, context))) do fields when is_list(fields) -> evaluated_constant(map, length(fields), context) @@ -116,11 +115,11 @@ defmodule RustQ.Meta.Lower.Kernel do def lower(%Call{function: :elem, args: [tuple, index]}, %Context{} = context) when is_integer(index) and index >= 0, - do: {:ok, %AST.Field{receiver: context.lower.(tuple), field: index}} + do: {:ok, %AST.Field{receiver: lowered(tuple, context), field: index}} def lower(%Call{function: :put_elem, args: [tuple, index, value]} = call, %Context{} = context) when is_integer(index) and index >= 0 do - case tuple_elements(context.type_of.(tuple)) do + case tuple_elements(type_of(tuple, context)) do elements when index < length(elements) -> tuple_name = :rustq_tuple_value tuple_var = A.var(tuple_name) @@ -129,13 +128,13 @@ defmodule RustQ.Meta.Lower.Kernel do elements |> Elixir.Enum.with_index() |> Elixir.Enum.map(fn - {type, ^index} -> context.lower_expected.(value, type) + {type, ^index} -> lowered(value, type, context) {_type, field} -> %AST.Field{receiver: tuple_var, field: field} end) {:ok, A.block_expr([ - %AST.Let{pattern: %AST.PatVar{name: tuple_name}, expr: context.lower.(tuple)}, + %AST.Let{pattern: %AST.PatVar{name: tuple_name}, expr: lowered(tuple, context)}, A.return_stmt(%AST.Tuple{values: values}) ])} @@ -148,7 +147,7 @@ defmodule RustQ.Meta.Lower.Kernel do end def lower(%Call{function: :tuple_size, args: [tuple]} = call, %Context{} = context) do - case tuple_elements(shape_type(context.type_of.(tuple))) do + case tuple_elements(shape_type(type_of(tuple, context))) do elements when is_list(elements) -> evaluated_constant(tuple, length(elements), context) @@ -158,18 +157,22 @@ defmodule RustQ.Meta.Lower.Kernel do end def lower(%Call{function: :is_nil, args: [value]}, %Context{} = context), - do: {:ok, A.method(context.lower.(value), :is_none)} + do: {:ok, A.method(lowered(value, context), :is_none)} def lower(%Call{}, %Context{}), do: :unsupported - @spec synth(Call.t(), TypeContext.t()) :: {:ok, Type.t()} | :unsupported + @impl true + def synth(%Call{function: function} = call, %TypeContext{} = context) + when function in [:.., :in], + do: RustQ.Meta.Lower.Range.synth(call, context) + def synth(%Call{function: function}, %TypeContext{}) when function in @comparison_functions, do: {:ok, Spec.type(quote(do: boolean()))} # Arithmetic on an option or result works on the value its operand propagates. def synth(%Call{function: function, args: [left | _rest]}, %TypeContext{} = context) when function in [:+, :-, :*, :/, :div, :rem, :abs, :min, :max], - do: left |> context.type_of.() |> propagated_value() |> type_result() + do: left |> type_of(context) |> propagated_value() |> type_result() def synth(%Call{function: function}, %TypeContext{}) when function in [:byte_size, :length, :map_size, :tuple_size], @@ -177,14 +180,14 @@ defmodule RustQ.Meta.Lower.Kernel do def synth(%Call{function: :elem, args: [tuple, index]}, %TypeContext{} = context) when is_integer(index) and index >= 0 do - case tuple_elements(context.type_of.(tuple)) do + case tuple_elements(type_of(tuple, context)) do elements when index < length(elements) -> type_result(Elixir.Enum.at(elements, index)) _unknown -> :unsupported end end def synth(%Call{function: :put_elem, args: [tuple, _index, _value]}, %TypeContext{} = context), - do: type_result(context.type_of.(tuple)) + do: type_result(type_of(tuple, context)) def synth(%Call{}, %TypeContext{}), do: :unsupported @@ -219,8 +222,8 @@ defmodule RustQ.Meta.Lower.Kernel do defp wrapper_comparison?(left, right, %Context{} = context) do wrapper_literal?(left) or wrapper_literal?(right) or - (wrapper_kind(context.type_of.(left)) != nil and - wrapper_kind(context.type_of.(left)) == wrapper_kind(context.type_of.(right))) + (wrapper_kind(type_of(left, context)) != nil and + wrapper_kind(type_of(left, context)) == wrapper_kind(type_of(right, context))) end defp wrapper_literal?({:some, _, [_value]}), do: true diff --git a/lib/rustq/meta/lower/list.ex b/lib/rustq/meta/lower/list.ex index cb3226b..51f7417 100644 --- a/lib/rustq/meta/lower/list.ex +++ b/lib/rustq/meta/lower/list.ex @@ -4,21 +4,18 @@ defmodule RustQ.Meta.Lower.List do and types such calls. """ - alias RustQ.Meta.Core.Call - alias RustQ.Meta.Lower.Stdlib - alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} + use RustQ.Meta.Lower.Stdlib.Lowering + alias RustQ.Meta.Type - alias RustQ.Rust.AST - alias RustQ.Rust.AST.Builder, as: A - @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported + @impl true def lower(%Call{function: function, args: [list]}, %Context{} = context) when function in [:first, :last] do terminal = if function == :first, do: :next, else: :last {:ok, list - |> context.lower.() + |> lowered(context) |> A.method(:into_iter) |> A.method(terminal)} end @@ -29,21 +26,21 @@ defmodule RustQ.Meta.Lower.List do {:ok, list - |> context.lower.() + |> lowered(context) |> A.method(:into_iter) |> A.method(terminal) - |> A.method(:unwrap_or, [context.lower.(default)])} + |> A.method(:unwrap_or, [lowered(default, context)])} end def lower(%Call{function: :wrap, args: [nil]}, %Context{}), do: {:ok, %AST.VecLiteral{values: []}} def lower(%Call{function: :wrap, args: [value]}, %Context{} = context) do - case context.type_of.(value) do + case type_of(value, context) do %Type{} = type -> if collection_inner(type), - do: {:ok, context.lower.(value)}, - else: {:ok, %AST.VecLiteral{values: [context.lower.(value)]}} + do: {:ok, lowered(value, context)}, + else: {:ok, %AST.VecLiteral{values: [lowered(value, context)]}} _unknown -> :unsupported @@ -51,12 +48,12 @@ defmodule RustQ.Meta.Lower.List do end def lower(%Call{function: :duplicate, args: [value, count]}, %Context{} = context) do - case Stdlib.nonnegative_count(count, context) do + case nonnegative_count(count, context) do {:ok, lowered_count} -> {:ok, %AST.PathCall{ path: A.path([:std, :iter, :repeat]), - args: [context.lower.(value)] + args: [lowered(value, context)] } |> A.method(:take, [lowered_count]) |> A.method(:collect)} @@ -67,10 +64,10 @@ defmodule RustQ.Meta.Lower.List do end def lower(%Call{function: :flatten, args: [list]}, %Context{} = context) do - depth = flatten_depth(context.type_of.(list)) + depth = flatten_depth(type_of(list, context)) if is_integer(depth) and depth > 0 do - iterator = list |> context.lower.() |> A.method(:into_iter) + iterator = list |> lowered(context) |> A.method(:into_iter) {:ok, iterator |> flatten_iterator(depth) |> A.method(:collect)} else @@ -80,10 +77,10 @@ defmodule RustQ.Meta.Lower.List do def lower(%Call{}, %Context{}), do: :unsupported - @spec synth(Call.t(), TypeContext.t()) :: {:ok, Type.t()} | :unsupported + @impl true def synth(%Call{function: function, args: [list]}, %TypeContext{} = context) when function in [:first, :last] do - case collection_inner(context.type_of.(list)) do + case collection_inner(type_of(list, context)) do %Type{} = inner -> {:ok, Type.option(inner)} nil -> :unsupported end @@ -91,10 +88,10 @@ defmodule RustQ.Meta.Lower.List do def synth(%Call{function: function, args: [_list, default]}, %TypeContext{} = context) when function in [:first, :last], - do: type_result(context.type_of.(default)) + do: type_result(type_of(default, context)) def synth(%Call{function: :wrap, args: [value]}, %TypeContext{} = context) do - case context.type_of.(value) do + case type_of(value, context) do %Type{} = type -> {:ok, if(collection_inner(type), do: Type.vec(collection_inner(type)), else: Type.vec(type))} @@ -105,14 +102,14 @@ defmodule RustQ.Meta.Lower.List do end def synth(%Call{function: :duplicate, args: [value, _count]}, %TypeContext{} = context) do - case context.type_of.(value) do + case type_of(value, context) do %Type{} = type -> {:ok, Type.vec(type)} nil -> :unsupported end end def synth(%Call{function: :flatten, args: [list]}, %TypeContext{} = context) do - case innermost_type(context.type_of.(list)) do + case innermost_type(type_of(list, context)) do %Type{} = inner -> {:ok, Type.vec(inner)} nil -> :unsupported end diff --git a/lib/rustq/meta/lower/map.ex b/lib/rustq/meta/lower/map.ex index f3dfecc..85d7436 100644 --- a/lib/rustq/meta/lower/map.ex +++ b/lib/rustq/meta/lower/map.ex @@ -9,16 +9,13 @@ defmodule RustQ.Meta.Lower.Map do `fetch!`, `has_key?`, `put`, and `replace!` read and write its fields. """ + use RustQ.Meta.Lower.Stdlib.Lowering + alias RustQ.Diagnostic - alias RustQ.Meta.Core.Call alias RustQ.Meta.Lower.Collection - alias RustQ.Meta.Lower.Stdlib - alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type - alias RustQ.Rust.AST - alias RustQ.Rust.AST.Builder, as: A - @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported + @impl true def lower(%Call{function: :new, args: []}, %Context{} = context), do: lower_new([], context) @@ -35,7 +32,7 @@ defmodule RustQ.Meta.Lower.Map do defp lower_keyed(%Call{function: :get, args: [map, key]}, key_type, value_type, context) do lookup = - A.method(context.lower.(map), :get, [Collection.borrowed(key, key_type, context)]) + A.method(lowered(map, context), :get, [Collection.borrowed(key, key_type, context)]) {:ok, Collection.read_out(lookup, value_type)} end @@ -47,18 +44,18 @@ defmodule RustQ.Meta.Lower.Map do context ) do lookup = - A.method(context.lower.(map), :get, [Collection.borrowed(key, key_type, context)]) + A.method(lowered(map, context), :get, [Collection.borrowed(key, key_type, context)]) {:ok, lookup |> Collection.read_out(value_type) - |> A.method(:unwrap_or, [context.lower_expected.(default, value_type)])} + |> A.method(:unwrap_or, [lowered(default, value_type, context)])} end # A missing key raises in Elixir; indexing panics, which Rustler raises. defp lower_keyed(%Call{function: :fetch!, args: [map, key]}, key_type, value_type, context) do value = %AST.Index{ - receiver: context.lower.(map), + receiver: lowered(map, context), index: Collection.borrowed(key, key_type, context) } @@ -67,7 +64,7 @@ defmodule RustQ.Meta.Lower.Map do defp lower_keyed(%Call{function: :has_key?, args: [map, key]}, key_type, _value_type, context) do {:ok, - A.method(context.lower.(map), :contains_key, [ + A.method(lowered(map, context), :contains_key, [ Collection.borrowed(key, key_type, context) ])} end @@ -80,8 +77,8 @@ defmodule RustQ.Meta.Lower.Map do [ A.stmt( A.method(map, :insert, [ - context.lower_expected.(key, key_type), - context.lower_expected.(value, value_type) + lowered(key, key_type, context), + lowered(value, value_type, context) ]) ) ] @@ -130,12 +127,12 @@ defmodule RustQ.Meta.Lower.Map do map |> A.method(:remove, [A.ref(key_name)]) |> A.method(:map, [update_function(fun, value_type, context)]) - |> A.method(:unwrap_or, [context.lower_expected.(default, value_type)]) + |> A.method(:unwrap_or, [lowered(default, value_type, context)]) [ %AST.Let{ pattern: %AST.PatVar{name: :rustq_key}, - expr: context.lower_expected.(key, key_type) + expr: lowered(key, key_type, context) }, %AST.Let{pattern: %AST.PatVar{name: :rustq_value}, expr: updated}, A.stmt(A.method(map, :insert, [key_name, value_name])) @@ -150,7 +147,7 @@ defmodule RustQ.Meta.Lower.Map do {method, type} = if function == :keys, do: {:keys, key_type}, else: {:values, value_type} {:ok, - context.lower.(map) + lowered(map, context) |> A.method(method) |> Collection.read_out(type) |> A.method(:collect) @@ -169,10 +166,10 @@ defmodule RustQ.Meta.Lower.Map do defp lower_keyed(%Call{}, _key_type, _value_type, _context), do: :unsupported defp update_function({:fn, _, [{:->, _, [[argument], body]}]}, value_type, context), - do: context.lower_closure.([{:__rustq_typed_arg__, [], [argument, value_type]}], body) + do: closure([{:__rustq_typed_arg__, [], [argument, value_type]}], body, context) defp update_function({:&, _, [{:/, _, [function, 1]}]}, _value_type, context), - do: context.lower_capture.(function) + do: capture(function, context) defp update_function(other, _value_type, _context) do Diagnostic.lower( @@ -192,13 +189,13 @@ defmodule RustQ.Meta.Lower.Map do defp lower_fields(%Call{function: :get, args: [map, key, default]} = call, %Context{} = context) when is_atom(key), - do: lower_get(call, map, key, context.lower.(default), true, context) + do: lower_get(call, map, key, lowered(default, context), true, context) defp lower_fields(%Call{function: :fetch!, args: [map, key]} = call, %Context{} = context) when is_atom(key) do - with %Type{} = map_type <- map_type(context.type_of.(map)), + with %Type{} = map_type <- map_type(type_of(map, context)), %Type{} <- Type.field_type(map_type, key) do - {:ok, %AST.Field{receiver: context.lower.(map), field: key}} + {:ok, %AST.Field{receiver: lowered(map, context), field: key}} else _unknown -> unsupported_field!(call) end @@ -206,11 +203,11 @@ defmodule RustQ.Meta.Lower.Map do defp lower_fields(%Call{function: :has_key?, args: [map, key]} = call, %Context{} = context) when is_atom(key) do - case map_type(Stdlib.shape_type(context.type_of.(map))) do + case map_type(shape_type(type_of(map, context))) do %Type{} = type -> present? = match?(%Type{}, Type.field_type(type, key)) - Stdlib.evaluated_constant(map, present?, context) + evaluated_constant(map, present?, context) nil -> unsupported_field!(call) @@ -222,7 +219,7 @@ defmodule RustQ.Meta.Lower.Map do %Context{} = context ) when function in [:put, :replace!] and is_atom(key) do - with %Type{} = map_type <- map_type(context.type_of.(map)), + with %Type{} = map_type <- map_type(type_of(map, context)), %Type{} = field_type <- Type.field_type(map_type, key) do name = :rustq_map_value variable = A.var(name) @@ -231,12 +228,12 @@ defmodule RustQ.Meta.Lower.Map do A.block_expr([ %AST.Let{ pattern: %AST.PatVar{name: name}, - expr: context.lower.(map), + expr: lowered(map, context), mutable: true }, %AST.Assign{ target: %AST.Field{receiver: variable, field: key}, - expr: context.lower_expected.(value, field_type) + expr: lowered(value, field_type, context) }, A.return_stmt(variable) ])} @@ -247,11 +244,11 @@ defmodule RustQ.Meta.Lower.Map do defp lower_fields(%Call{}, %Context{}), do: :unsupported - @spec synth(Call.t(), TypeContext.t()) :: {:ok, Type.t()} | :unsupported + @impl true def synth(%Call{function: :new}, %TypeContext{}), do: :unsupported def synth(%Call{args: [map | _]} = call, %TypeContext{} = context) do - case Collection.keyed(context.type_of.(map)) do + case Collection.keyed(type_of(map, context)) do {:map, key, value} -> synth_keyed(call, map, key, value, context) _fields -> synth_fields(call, context) end @@ -271,7 +268,7 @@ defmodule RustQ.Meta.Lower.Map do defp synth_keyed(%Call{function: function}, map, _key_type, _value, context) when function in [:put, :delete, :merge, :update] do - case context.type_of.(map) do + case type_of(map, context) do %Type{kind: kind} = type when kind in [:ref, :mut_ref] -> type_result(Type.ref_inner(type)) type -> type_result(type) end @@ -289,7 +286,7 @@ defmodule RustQ.Meta.Lower.Map do defp synth_fields(%Call{function: function, args: [map, key | _rest]}, %TypeContext{} = context) when function in [:get, :fetch!] and is_atom(key) do - with %Type{} = map_type <- map_type(context.type_of.(map)), + with %Type{} = map_type <- map_type(type_of(map, context)), %Type{} = field_type <- Type.field_type(map_type, key) do {:ok, field_type} else @@ -305,12 +302,12 @@ defmodule RustQ.Meta.Lower.Map do %TypeContext{} = context ) when function in [:put, :replace!], - do: type_result(context.type_of.(map)) + do: type_result(type_of(map, context)) defp synth_fields(%Call{}, %TypeContext{}), do: :unsupported defp lower_get(call, map, key, default, evaluate_default?, context) do - case map_type(context.type_of.(map)) do + case map_type(type_of(map, context)) do %Type{} = type -> lower_typed_get(type, map, key, default, evaluate_default?, context) nil -> unsupported_field!(call) end @@ -328,19 +325,19 @@ defmodule RustQ.Meta.Lower.Map do {:ok, A.block_expr([ - %AST.Let{pattern: %AST.PatVar{name: name}, expr: context.lower.(map)}, + %AST.Let{pattern: %AST.PatVar{name: name}, expr: lowered(map, context)}, %AST.Let{pattern: %AST.PatWildcard{}, expr: default}, A.return_stmt(%AST.Field{receiver: A.var(name), field: key}) ])} end defp lower_present_get(map, key, _default, false, context), - do: {:ok, %AST.Field{receiver: context.lower.(map), field: key}} + do: {:ok, %AST.Field{receiver: lowered(map, context), field: key}} defp lower_missing_get(map, default, context) do {:ok, A.block_expr([ - %AST.Let{pattern: %AST.PatWildcard{}, expr: context.lower.(map)}, + %AST.Let{pattern: %AST.PatWildcard{}, expr: lowered(map, context)}, A.return_stmt(default) ])} end diff --git a/lib/rustq/meta/lower/map_set.ex b/lib/rustq/meta/lower/map_set.ex index 1d26495..7d70250 100644 --- a/lib/rustq/meta/lower/map_set.ex +++ b/lib/rustq/meta/lower/map_set.ex @@ -6,14 +6,12 @@ defmodule RustQ.Meta.Lower.MapSet do see `RustQ.Meta.Lower.Collection`. """ - alias RustQ.Meta.Core.Call + use RustQ.Meta.Lower.Stdlib.Lowering + alias RustQ.Meta.Lower.Collection - alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type - alias RustQ.Rust.AST - alias RustQ.Rust.AST.Builder, as: A - @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported + @impl true def lower(%Call{function: :new, args: []}, %Context{}), do: {:ok, %AST.PathCall{path: A.path([:HashSet, :new]), args: []}} @@ -31,7 +29,7 @@ defmodule RustQ.Meta.Lower.MapSet do Collection.updated( set, fn set -> - [A.stmt(A.method(set, :insert, [context.lower_expected.(value, item)]))] + [A.stmt(A.method(set, :insert, [lowered(value, item, context)]))] end, context )} @@ -49,7 +47,7 @@ defmodule RustQ.Meta.Lower.MapSet do end defp lower_set(%Call{function: :member?, args: [set, value]}, item, context) do - {:ok, A.method(context.lower.(set), :contains, [Collection.borrowed(value, item, context)])} + {:ok, A.method(lowered(set, context), :contains, [Collection.borrowed(value, item, context)])} end defp lower_set(%Call{function: :union, args: [set, other]}, _item, context) do @@ -65,7 +63,7 @@ defmodule RustQ.Meta.Lower.MapSet do defp lower_set(%Call{function: function, args: [set, other]}, _item, context) when function in [:difference, :intersection] do - kept = A.method(context.lower.(other), :contains, [A.var(:rustq_item)]) + kept = A.method(lowered(other, context), :contains, [A.var(:rustq_item)]) kept = if function == :difference, do: %AST.UnaryOp{op: :not, expr: kept}, else: kept @@ -83,11 +81,11 @@ defmodule RustQ.Meta.Lower.MapSet do when function in [:subset?, :disjoint?] do method = if function == :subset?, do: :is_subset, else: :is_disjoint - {:ok, A.method(context.lower.(set), method, [A.ref(context.lower.(other))])} + {:ok, A.method(lowered(set, context), method, [A.ref(lowered(other, context))])} end defp lower_set(%Call{function: :size, args: [set]}, _item, context) do - {:ok, A.cast(A.method(context.lower.(set), :len), %AST.TypePath{parts: [:i64]})} + {:ok, A.cast(A.method(lowered(set, context), :len), %AST.TypePath{parts: [:i64]})} end defp lower_set(%Call{function: :to_list, args: [set]}, item, context) do @@ -102,11 +100,11 @@ defmodule RustQ.Meta.Lower.MapSet do defp lower_set(%Call{}, _item, _context), do: :unsupported - @spec synth(Call.t(), TypeContext.t()) :: {:ok, Type.t()} | :unsupported + @impl true def synth(%Call{function: :new}, %TypeContext{}), do: :unsupported def synth(%Call{function: function, args: [set | _]}, %TypeContext{} = context) do - case {function, Collection.keyed(context.type_of.(set))} do + case {function, Collection.keyed(type_of(set, context))} do {_function, nil} -> :unsupported @@ -120,7 +118,7 @@ defmodule RustQ.Meta.Lower.MapSet do {:ok, Type.vec(item)} {function, _set} when function in [:put, :delete, :union, :difference, :intersection] -> - owned(context.type_of.(set)) + owned(type_of(set, context)) _other -> :unsupported diff --git a/lib/rustq/meta/lower/range.ex b/lib/rustq/meta/lower/range.ex index 61a7983..867f306 100644 --- a/lib/rustq/meta/lower/range.ex +++ b/lib/rustq/meta/lower/range.ex @@ -3,20 +3,18 @@ defmodule RustQ.Meta.Lower.Range do Lowers ranges and `in` over them for `RustQ.Meta.Lower.Stdlib`, and types such calls. """ + use RustQ.Meta.Lower.Stdlib.Lowering + alias RustQ.Diagnostic - alias RustQ.Meta.Core.Call - alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} - alias RustQ.Rust.AST - alias RustQ.Rust.AST.Builder, as: A alias RustQ.Spec - @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported + @impl true def lower(%Call{function: :.., args: [start, stop]} = call, %Context{} = context) do if ascending_literal_range?(start, stop) do {:ok, %AST.Range{ - start: context.lower.(start), - stop: context.lower.(stop), + start: lowered(start, context), + stop: lowered(stop, context), inclusive: true }} else @@ -31,17 +29,17 @@ defmodule RustQ.Meta.Lower.Range do def lower(%Call{function: :in, args: [value, collection]}, %Context{} = context) do receiver = - case context.lower.(collection) do + case lowered(collection, context) do %AST.Range{} = range -> A.block_expr([A.return_stmt(range)]) expression -> expression end - {:ok, A.method(receiver, :contains, [A.ref(context.lower.(value))])} + {:ok, A.method(receiver, :contains, [A.ref(lowered(value, context))])} end def lower(%Call{}, %Context{}), do: :unsupported - @spec synth(Call.t(), TypeContext.t()) :: {:ok, term()} | :unsupported + @impl true def synth(%Call{function: :in}, %TypeContext{}), do: {:ok, Spec.type(quote(do: boolean()))} diff --git a/lib/rustq/meta/lower/stdlib.ex b/lib/rustq/meta/lower/stdlib.ex index 15acdfd..ee4aea6 100644 --- a/lib/rustq/meta/lower/stdlib.ex +++ b/lib/rustq/meta/lower/stdlib.ex @@ -1,74 +1,15 @@ defmodule RustQ.Meta.Lower.Stdlib do @moduledoc """ - Lowers calls to Elixir's standard library (`Kernel`, `Enum`, `List`, `Map`, `MapSet`, - `String`, `Tuple`, and `Range`) to Rust that keeps their semantics, dispatching to one lowering module - per Elixir module. Calls whose semantics Rust cannot preserve are rejected. + Lowers calls to Elixir's standard library to Rust that keeps their semantics: a call to an + Elixir module goes to the module lowering it, named after it (`RustQ.Meta.Lower.Enum` for + `Enum`), which implements `RustQ.Meta.Lower.Stdlib.Lowering`. Calls whose semantics Rust + cannot preserve are rejected. """ alias RustQ.Diagnostic alias RustQ.Meta.Core.Call - alias RustQ.Meta.Type - alias RustQ.Rust.AST - alias RustQ.Rust.AST.Builder, as: A - - defmodule Context do - @moduledoc """ - The callbacks a standard-library lowering uses to lower its arguments and closures, and - the type the call is expected to produce. - """ - - defstruct [ - :lower, - :lower_expected, - :lower_binary_operand, - :lower_closure, - :lower_typed_closure, - :lower_closure_body, - :lower_capture, - :closure_arg, - :type_of, - :in_use?, - :expected - ] - - @type t :: %__MODULE__{ - lower: (Macro.t() -> term()), - lower_expected: (Macro.t(), term() -> term()), - lower_binary_operand: (Macro.t() -> term()), - lower_closure: ([Macro.t()], Macro.t() -> term()), - lower_typed_closure: ([Macro.t()], Macro.t(), Type.t() | nil -> term()), - lower_closure_body: (Macro.t(), term() -> term()), - lower_capture: (Macro.t() -> term()), - closure_arg: (Macro.t() -> atom()), - type_of: (Macro.t() -> term()), - in_use?: (Macro.t() -> boolean()), - expected: Type.t() | nil - } - end - - @spec shape_type(Type.t() | nil) :: Type.t() | nil - def shape_type(%Type{kind: kind} = type) when kind in [:nif_result, :result], - do: Type.inner(type) - - def shape_type(type), do: type - - @spec evaluated_constant(Macro.t(), term(), Context.t()) :: {:ok, AST.expr()} - def evaluated_constant(argument, value, context) do - evaluated = - case context.type_of.(argument) do - %Type{kind: kind} = type when kind in [:nif_result, :result] -> - context.lower_expected.(argument, Type.inner(type)) - - _type -> - context.lower.(argument) - end - - {:ok, - A.block_expr([ - %AST.Let{pattern: %AST.PatWildcard{}, expr: evaluated}, - A.return_stmt(A.lit(value)) - ])} - end + alias RustQ.Meta.Lower.Stdlib.Lowering + alias RustQ.Meta.Lower.Stdlib.Lowering.{Context, TypeContext} @spec lower(Macro.t(), Context.t()) :: {:ok, term()} | :unsupported def lower(ast, %Context{} = context) do @@ -94,17 +35,6 @@ defmodule RustQ.Meta.Lower.Stdlib do end end - defmodule TypeContext do - @moduledoc "The callbacks a standard-library lowering uses to type its arguments." - - defstruct [:type_of, :type_with_vars] - - @type t :: %__MODULE__{ - type_of: (Macro.t() -> term()), - type_with_vars: (Macro.t(), map() -> term()) - } - end - @spec synth(Macro.t(), TypeContext.t()) :: {:ok, term()} | :unsupported def synth(ast, %TypeContext{} = context) do with {:ok, %Call{} = call} <- Call.normalize(ast) do @@ -112,80 +42,39 @@ defmodule RustQ.Meta.Lower.Stdlib do end end - @spec nonnegative_count(Macro.t(), Context.t()) :: {:ok, AST.expr()} | :unsupported - def nonnegative_count(count, %Context{} = context) when is_integer(count) and count >= 0, - do: {:ok, context.lower.(count)} - - def nonnegative_count(count, %Context{} = context) do - case context.type_of.(count) do - %Type{kind: :usize} -> - {:ok, context.lower.(count)} - - %Type{kind: kind} when kind in [:u8, :u16, :u32, :u64] -> - {:ok, A.cast(context.lower.(count), %AST.TypePath{parts: [:usize]})} - - _type -> - :unsupported + defp lower_call(%Call{} = call, context) do + case lowering(call.module) do + nil -> :unsupported + lowering -> lowering.lower(call, context) end end - defp lower_call(%Call{module: Kernel, function: function} = call, context) - when function in [:.., :in], - do: RustQ.Meta.Lower.Range.lower(call, context) - - defp lower_call(%Call{module: Kernel} = call, context), - do: RustQ.Meta.Lower.Kernel.lower(call, context) - - defp lower_call(%Call{module: Enum} = call, context), - do: RustQ.Meta.Lower.Enum.lower(call, context) - - defp lower_call(%Call{module: List} = call, context), - do: RustQ.Meta.Lower.List.lower(call, context) - - defp lower_call(%Call{module: Map} = call, context), - do: RustQ.Meta.Lower.Map.lower(call, context) - - defp lower_call(%Call{module: MapSet} = call, context), - do: RustQ.Meta.Lower.MapSet.lower(call, context) - - defp lower_call(%Call{module: String} = call, context), - do: RustQ.Meta.Lower.String.lower(call, context) - - defp lower_call(%Call{module: Tuple} = call, context), - do: RustQ.Meta.Lower.Tuple.lower(call, context) - - defp lower_call(%Call{module: Range} = call, context), - do: RustQ.Meta.Lower.Range.lower(call, context) - - defp lower_call(%Call{}, _context), do: :unsupported - - defp synth_call(%Call{module: Kernel, function: function} = call, context) - when function in [:.., :in], - do: RustQ.Meta.Lower.Range.synth(call, context) - - defp synth_call(%Call{module: Kernel} = call, context), - do: RustQ.Meta.Lower.Kernel.synth(call, context) - - defp synth_call(%Call{module: Enum} = call, context), - do: RustQ.Meta.Lower.Enum.synth(call, context) - - defp synth_call(%Call{module: List} = call, context), - do: RustQ.Meta.Lower.List.synth(call, context) - - defp synth_call(%Call{module: Map} = call, context), - do: RustQ.Meta.Lower.Map.synth(call, context) - - defp synth_call(%Call{module: MapSet} = call, context), - do: RustQ.Meta.Lower.MapSet.synth(call, context) - - defp synth_call(%Call{module: String} = call, context), - do: RustQ.Meta.Lower.String.synth(call, context) + defp synth_call(%Call{} = call, context) do + case lowering(call.module) do + nil -> :unsupported + lowering -> lowering.synth(call, context) + end + end - defp synth_call(%Call{module: Tuple} = call, context), - do: RustQ.Meta.Lower.Tuple.synth(call, context) + # The module named after an Elixir module that lowers it, such as `RustQ.Meta.Lower.Enum` + # for `Enum`. Only Elixir's own modules are looked up, so an alias a body names never is. + # Each module's answer is kept, since a body names the same few modules many times. + defp lowering(module) do + case :persistent_term.get({__MODULE__, module}, :unknown) do + :unknown -> + lowering = find_lowering(module) + :persistent_term.put({__MODULE__, module}, lowering) + lowering + + lowering -> + lowering + end + end - defp synth_call(%Call{module: Range} = call, context), - do: RustQ.Meta.Lower.Range.synth(call, context) + defp find_lowering(module) do + lowering = Module.concat(RustQ.Meta.Lower, module) - defp synth_call(%Call{}, _context), do: :unsupported + if Application.get_application(module) == :elixir and Lowering.lowering?(lowering), + do: lowering + end end diff --git a/lib/rustq/meta/lower/stdlib/lowering.ex b/lib/rustq/meta/lower/stdlib/lowering.ex new file mode 100644 index 0000000..fcc7a53 --- /dev/null +++ b/lib/rustq/meta/lower/stdlib/lowering.ex @@ -0,0 +1,168 @@ +defmodule RustQ.Meta.Lower.Stdlib.Lowering do + @moduledoc """ + The behaviour of a module lowering one Elixir standard-library module, and the helpers such + a module lowers and types its arguments with. + + `RustQ.Meta.Lower.Stdlib` dispatches a call to `Enum` to `RustQ.Meta.Lower.Enum`, a call to + `Map` to `RustQ.Meta.Lower.Map`, and so on: the module named after the Elixir module, when + it implements this behaviour. `use` this module to implement it and import the helpers: + + defmodule RustQ.Meta.Lower.List do + use RustQ.Meta.Lower.Stdlib.Lowering + + @impl true + def lower(%Call{function: :first, args: [list]}, %Context{} = context), + do: {:ok, list |> lowered(context) |> A.method(:into_iter) |> A.method(:next)} + end + + A lowering returns `:unsupported` for a call it does not lower, and `RustQ.Meta.Lower.Stdlib` + reports it. + """ + + alias RustQ.Meta.Core.Call + alias RustQ.Meta.Lower + alias RustQ.Meta.Type + alias RustQ.Meta.Typing + alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A + + defmodule Context do + @moduledoc """ + What a standard-library lowering knows: the lowering environment of the call, and the + type the call is expected to produce. + """ + + defstruct [:env, :expected] + + @type t :: %__MODULE__{env: Lower.Env.t(), expected: Type.t() | nil} + end + + defmodule TypeContext do + @moduledoc "What a standard-library lowering knows when it types a call." + + defstruct [:env] + + @type t :: %__MODULE__{env: Typing.Env.t()} + end + + @doc "Lowers `call`, or returns `:unsupported` for a call this module does not lower." + @callback lower(Call.t(), Context.t()) :: {:ok, AST.expr()} | :unsupported + + @doc "Types `call`, or returns `:unsupported` for a call this module does not type." + @callback synth(Call.t(), TypeContext.t()) :: {:ok, Type.t()} | :unsupported + + defmacro __using__(_opts) do + quote do + @behaviour unquote(__MODULE__) + + import unquote(__MODULE__), except: [lowering?: 1] + + alias RustQ.Meta.Core.Call + alias unquote(__MODULE__).{Context, TypeContext} + alias RustQ.Rust.AST + alias RustQ.Rust.AST.Builder, as: A + end + end + + @doc "Whether `module` lowers an Elixir standard-library module." + @spec lowering?(module()) :: boolean() + def lowering?(module) do + # `ensure_compiled` waits for a lowering RustQ itself is still compiling. + match?({:module, _}, Code.ensure_compiled(module)) and + __MODULE__ in List.flatten(Keyword.get_values(module.module_info(:attributes), :behaviour)) + end + + @doc "`expression`, lowered." + @spec lowered(Macro.t(), Context.t()) :: AST.expr() + def lowered(expression, %Context{env: env}), do: Lower.lower_expr(expression, env) + + @doc "`expression`, lowered to produce a value of `expected`." + @spec lowered(Macro.t(), Type.t() | nil, Context.t()) :: AST.expr() + def lowered(expression, expected, %Context{env: env}), + do: Lower.lower_expr(expression, expected, env) + + @doc "`expression`, lowered as an operand: a fallible one propagates its error with `?`." + @spec operand(Macro.t(), Context.t()) :: AST.expr() + def operand(expression, %Context{env: env}), do: Lower.lower_binary_operand(expression, env) + + @doc "A closure taking `args` and returning `body`, lowered." + @spec closure([Macro.t()], Macro.t(), Context.t()) :: AST.expr() + def closure(args, body, %Context{env: env}), do: Lower.lower_closure_args(args, body, env) + + @doc "A closure taking `args` and returning `body` as a value of `returns`, lowered." + @spec closure([Macro.t()], Macro.t(), Type.t() | nil, Context.t()) :: AST.expr() + def closure(args, body, returns, %Context{env: env}), + do: Lower.lower_closure_args(args, body, env, returns) + + @doc "A closure's `body`, lowered to return a value of `returns`." + @spec closure_body(Macro.t(), Type.t() | nil, Context.t()) :: AST.expr() + def closure_body(body, returns, %Context{env: env}), + do: Lower.lower_closure_body(body, returns, env) + + @doc "A closure argument, as a Rust closure argument." + @spec closure_arg(Macro.t()) :: atom() | AST.pat() + defdelegate closure_arg(arg), to: Lower, as: :closure_arg! + + @doc "A captured named function, as the Rust path of that function." + @spec capture(Macro.t(), Context.t()) :: AST.expr() + def capture(function, %Context{env: env}), do: Lower.lower_function_capture(function, env) + + @doc "The type of `expression`, or nil when it is not known." + @spec type_of(Macro.t(), Context.t() | TypeContext.t()) :: Type.t() | nil + def type_of(expression, %Context{env: env}), do: Typing.synth(expression, Lower.typing_env(env)) + def type_of(expression, %TypeContext{env: env}), do: Typing.synth(expression, env) + + @doc "The type of `expression` where `vars` are bound to the given types." + @spec type_with_vars(Macro.t(), %{optional(atom()) => Type.t()}, TypeContext.t()) :: + Type.t() | nil + def type_with_vars(expression, vars, %TypeContext{env: env}), + do: Typing.synth(expression, %{env | vars: Map.merge(env.vars, vars)}) + + @doc "Whether `collection` is still in use after this use of it; see `RustQ.Meta.Ownership`." + @spec in_use?(Macro.t(), Context.t()) :: boolean() + def in_use?(collection, %Context{env: env}), do: Lower.in_use?(collection, env) + + @doc "The type a call shapes its value as: a fallible call's success type, and others' own." + @spec shape_type(Type.t() | nil) :: Type.t() | nil + def shape_type(%Type{kind: kind} = type) when kind in [:nif_result, :result], + do: Type.inner(type) + + def shape_type(type), do: type + + @doc "`argument` evaluated for its effects, then the constant `value`." + @spec evaluated_constant(Macro.t(), term(), Context.t()) :: {:ok, AST.expr()} + def evaluated_constant(argument, value, %Context{} = context) do + evaluated = + case type_of(argument, context) do + %Type{kind: kind} = type when kind in [:nif_result, :result] -> + lowered(argument, Type.inner(type), context) + + _type -> + lowered(argument, context) + end + + {:ok, + A.block_expr([ + %AST.Let{pattern: %AST.PatWildcard{}, expr: evaluated}, + A.return_stmt(A.lit(value)) + ])} + end + + @doc "`count` as a `usize`, when it is a literal or unsigned count that cannot be negative." + @spec nonnegative_count(Macro.t(), Context.t()) :: {:ok, AST.expr()} | :unsupported + def nonnegative_count(count, %Context{} = context) when is_integer(count) and count >= 0, + do: {:ok, lowered(count, context)} + + def nonnegative_count(count, %Context{} = context) do + case type_of(count, context) do + %Type{kind: :usize} -> + {:ok, lowered(count, context)} + + %Type{kind: kind} when kind in [:u8, :u16, :u32, :u64] -> + {:ok, A.cast(lowered(count, context), %AST.TypePath{parts: [:usize]})} + + _type -> + :unsupported + end + end +end diff --git a/lib/rustq/meta/lower/string.ex b/lib/rustq/meta/lower/string.ex index c320c3c..89c5840 100644 --- a/lib/rustq/meta/lower/string.ex +++ b/lib/rustq/meta/lower/string.ex @@ -4,12 +4,9 @@ defmodule RustQ.Meta.Lower.String do `String.replace/3` and `String.trim/1` for `RustQ.Meta.Lower.Stdlib`, and types such calls. """ - alias RustQ.Meta.Core.Call - alias RustQ.Meta.Lower.Stdlib - alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} + use RustQ.Meta.Lower.Stdlib.Lowering + alias RustQ.Meta.Type - alias RustQ.Rust.AST - alias RustQ.Rust.AST.Builder, as: A alias RustQ.Spec @predicate_methods %{ @@ -18,12 +15,12 @@ defmodule RustQ.Meta.Lower.String do contains?: :contains } - @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported + @impl true def lower(%Call{function: function, args: [string, pattern]}, %Context{} = context) when is_map_key(@predicate_methods, function) do {:ok, - A.method(context.lower.(string), Map.fetch!(@predicate_methods, function), [ - A.ref(context.lower.(pattern)) + A.method(lowered(string, context), Map.fetch!(@predicate_methods, function), [ + A.ref(lowered(pattern, context)) ])} end @@ -31,7 +28,7 @@ defmodule RustQ.Meta.Lower.String do do: {:ok, string - |> context.lower.() + |> lowered(context) |> A.method(:trim) |> A.method(:to_string)} @@ -41,22 +38,22 @@ defmodule RustQ.Meta.Lower.String do ), do: {:ok, - A.method(context.lower.(string), :replace, [ + A.method(lowered(string, context), :replace, [ string_slice(pattern, context), string_slice(replacement, context) ])} def lower(%Call{function: :duplicate, args: [string, count]} = call, %Context{} = context) do - case Stdlib.nonnegative_count(count, context) do - {:ok, lowered_count} -> {:ok, A.method(context.lower.(string), :repeat, [lowered_count])} + case nonnegative_count(count, context) do + {:ok, lowered_count} -> {:ok, A.method(lowered(string, context), :repeat, [lowered_count])} :unsupported -> unsupported_count!(call) end end def lower(%Call{function: :valid?, args: [string]}, %Context{} = context) do - case context.type_of.(string) do + case type_of(string, context) do %Type{} = type when type.kind == :binary -> - bytes = A.method(context.lower.(string), :as_slice) + bytes = A.method(lowered(string, context), :as_slice) {:ok, %AST.PathCall{path: A.path([:std, :str, :from_utf8]), args: [bytes]} @@ -74,7 +71,7 @@ defmodule RustQ.Meta.Lower.String do def lower(%Call{}, %Context{}), do: :unsupported - @spec synth(Call.t(), TypeContext.t()) :: {:ok, term()} | :unsupported + @impl true def synth(%Call{function: function}, %TypeContext{}) when function in [:starts_with?, :ends_with?, :contains?, :valid?], do: {:ok, Spec.type(quote(do: boolean()))} @@ -85,8 +82,8 @@ defmodule RustQ.Meta.Lower.String do def synth(%Call{}, %TypeContext{}), do: :unsupported - defp string_slice(value, context) when is_binary(value), do: context.lower.(value) - defp string_slice(value, context), do: A.method(context.lower.(value), :as_str) + defp string_slice(value, context) when is_binary(value), do: lowered(value, context) + defp string_slice(value, context), do: A.method(lowered(value, context), :as_str) @spec unsupported_count!(Call.t()) :: no_return() defp unsupported_count!(%Call{source: source}) do diff --git a/lib/rustq/meta/lower/tuple.ex b/lib/rustq/meta/lower/tuple.ex index 4b62079..47ab3c4 100644 --- a/lib/rustq/meta/lower/tuple.ex +++ b/lib/rustq/meta/lower/tuple.ex @@ -3,16 +3,14 @@ defmodule RustQ.Meta.Lower.Tuple do Lowers `Tuple.to_list/1` for `RustQ.Meta.Lower.Stdlib`, and types such calls. """ + use RustQ.Meta.Lower.Stdlib.Lowering + alias RustQ.Diagnostic - alias RustQ.Meta.Core.Call - alias RustQ.Meta.Lower.Stdlib.{Context, TypeContext} alias RustQ.Meta.Type - alias RustQ.Rust.AST - alias RustQ.Rust.AST.Builder, as: A - @spec lower(Call.t(), Context.t()) :: {:ok, term()} | :unsupported + @impl true def lower(%Call{function: :to_list, args: [tuple]} = call, %Context{} = context) do - with elements when is_list(elements) <- tuple_elements(context.type_of.(tuple)), + with elements when is_list(elements) <- tuple_elements(type_of(tuple, context)), true <- homogeneous?(elements) do name = :rustq_tuple_value variable = A.var(name) @@ -26,7 +24,7 @@ defmodule RustQ.Meta.Lower.Tuple do {:ok, A.block_expr([ - %AST.Let{pattern: %AST.PatVar{name: name}, expr: context.lower.(tuple)}, + %AST.Let{pattern: %AST.PatVar{name: name}, expr: lowered(tuple, context)}, A.return_stmt(%AST.VecLiteral{values: values}) ])} else @@ -36,9 +34,9 @@ defmodule RustQ.Meta.Lower.Tuple do def lower(%Call{}, %Context{}), do: :unsupported - @spec synth(Call.t(), TypeContext.t()) :: {:ok, Type.t()} | :unsupported + @impl true def synth(%Call{function: :to_list, args: [tuple]}, %TypeContext{} = context) do - with [first | _rest] = elements <- tuple_elements(context.type_of.(tuple)), + with [first | _rest] = elements <- tuple_elements(type_of(tuple, context)), true <- homogeneous?(elements) do {:ok, Type.vec(first)} else diff --git a/lib/rustq/meta/typing.ex b/lib/rustq/meta/typing.ex index 7c39b5c..6615e41 100644 --- a/lib/rustq/meta/typing.ex +++ b/lib/rustq/meta/typing.ex @@ -11,7 +11,7 @@ defmodule RustQ.Meta.Typing do alias RustQ.Meta.Lower.Collection alias RustQ.Meta.Lower.Paths alias RustQ.Meta.Lower.Stdlib - alias RustQ.Meta.Lower.Stdlib.TypeContext, as: StdlibTypeContext + alias RustQ.Meta.Lower.Stdlib.Lowering.TypeContext, as: StdlibTypeContext alias RustQ.Meta.Pattern alias RustQ.Meta.StandardPointer alias RustQ.Meta.StdMethods @@ -344,12 +344,7 @@ defmodule RustQ.Meta.Typing do if Enum.all?(types, &match?(%Type{}, &1)), do: Type.tuple(types) end - defp stdlib_type_context(%Env{} = env) do - %StdlibTypeContext{ - type_of: &synth(&1, env), - type_with_vars: fn ast, vars -> synth(ast, %{env | vars: Map.merge(env.vars, vars)}) end - } - end + defp stdlib_type_context(%Env{} = env), do: %StdlibTypeContext{env: env} defp default_inference_callbacks do %{ From 3a154ebb5727b6da53c3eb0eb1a755fa8228d5d5 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 26 Oct 2026 13:58:45 +0400 Subject: [PATCH 09/10] Build the remaining lowered node kinds with Builder Path calls, unary and binary operations, closures, tuples, Some/Ok/Err, matches, assignments, and named types now use Builder constructors too. --- lib/rustq/meta/lower.ex | 247 ++++++++++-------------- lib/rustq/meta/lower/enum.ex | 23 ++- lib/rustq/meta/lower/kernel.ex | 31 +-- lib/rustq/meta/lower/list.ex | 9 +- lib/rustq/meta/lower/map.ex | 18 +- lib/rustq/meta/lower/map_set.ex | 8 +- lib/rustq/meta/lower/stdlib/lowering.ex | 2 +- lib/rustq/meta/lower/string.ex | 2 +- lib/rustq/meta/lower/tuple.ex | 4 +- 9 files changed, 138 insertions(+), 206 deletions(-) diff --git a/lib/rustq/meta/lower.ex b/lib/rustq/meta/lower.ex index 09dfeb5..5c0e3f9 100644 --- a/lib/rustq/meta/lower.ex +++ b/lib/rustq/meta/lower.ex @@ -210,12 +210,12 @@ defmodule RustQ.Meta.Lower do end end - defp lower_statement(:ok, %Context{}), do: A.stmt(%AST.Tuple{values: []}) + defp lower_statement(:ok, %Context{}), do: A.stmt(A.tuple([])) defp lower_statement({:__block__, _, [expression]}, context), do: lower_statement(expression, context) - defp lower_statement(nil, %Context{}), do: A.stmt(%AST.Tuple{values: []}) + defp lower_statement(nil, %Context{}), do: A.stmt(A.tuple([])) defp lower_statement(expression, %Context{} = context) do A.stmt(lower_statement_expr(expression, context)) @@ -311,7 +311,7 @@ defmodule RustQ.Meta.Lower do defp lower_return_expr_context(:ok, %Type{kind: :atom} = return_type, %Context{} = context), do: lower_expr(:ok, return_type, context) - defp lower_return_expr_context(:ok, _return_type, %Context{}), do: %AST.Tuple{values: []} + defp lower_return_expr_context(:ok, _return_type, %Context{}), do: A.tuple([]) # A tail `decode_as!` in a NIF returning what it decodes already is the `NifResult`. defp lower_return_expr_context( @@ -338,7 +338,7 @@ defmodule RustQ.Meta.Lower do %Type{kind: :option} = return_type, %Context{} = context ) do - %AST.Some{expr: lower_expr(expression, Type.inner(return_type), context)} + A.some(lower_expr(expression, Type.inner(return_type), context)) end defp lower_return_expr_context( @@ -347,17 +347,17 @@ defmodule RustQ.Meta.Lower do %Context{} = context ) when kind in [:result, :nif_result], - do: %AST.Ok{expr: lower_expr(value, Type.inner(return_type) || return_type, context)} + do: A.ok(lower_expr(value, Type.inner(return_type) || return_type, context)) defp lower_return_expr_context({:error, value}, %Type{kind: :nif_result}, %Context{} = context), - do: %AST.Err{expr: lower_nif_error(value, context)} + do: A.err(lower_nif_error(value, context)) defp lower_return_expr_context( {:error, value}, %Type{kind: :result} = return_type, %Context{} = context ), - do: %AST.Err{expr: lower_expr(value, Type.error(return_type), context)} + do: A.err(lower_expr(value, Type.error(return_type), context)) defp lower_return_expr_context( expression, @@ -371,7 +371,7 @@ defmodule RustQ.Meta.Lower do %Typing.Check{} -> if option_chain?(expression, context), do: lower_expr(expression, return_type, context), - else: %AST.Some{expr: lower_expr(expression, context)} + else: A.some(lower_expr(expression, context)) end end @@ -385,7 +385,7 @@ defmodule RustQ.Meta.Lower do defp lower_return_expr_context(expression, %Type{} = return_type, %Context{} = context) do if infer_propagation?(expression, return_type, context) do - %AST.Try{expr: lower_expr(expression, context)} + A.try(lower_expr(expression, context)) else lower_expr(expression, return_type, context) end @@ -401,7 +401,7 @@ defmodule RustQ.Meta.Lower do defp lower_result_return(expression, return_type, %Context{} = context) do cond do infer_propagation?(expression, return_type, context) -> - %AST.Try{expr: lower_expr(expression, context)} + A.try(lower_expr(expression, context)) plain_value?(expression, context) -> lower_plain_result(expression, return_type, context) @@ -417,10 +417,10 @@ defmodule RustQ.Meta.Lower do %AST.PathCall{path: %AST.Path{parts: [:atoms, _name]}, args: []} = atom, %Type{kind: :nif_result} ), - do: %AST.Ok{expr: atom} + do: A.ok(atom) defp wrap_atom_result(%AST.AtomValue{} = atom, %Type{kind: :nif_result}), - do: %AST.Ok{expr: atom} + do: A.ok(atom) defp wrap_atom_result(lowered, _return_type), do: lowered @@ -447,7 +447,7 @@ defmodule RustQ.Meta.Lower do collected _lowered when return_type.kind == :nif_result -> - %AST.Ok{expr: lower_expr(expression, Type.inner(return_type), context)} + A.ok(lower_expr(expression, Type.inner(return_type), context)) _lowered -> plain_result_value(expression, return_type) @@ -542,7 +542,7 @@ defmodule RustQ.Meta.Lower do # `{:some, value}` and `:none` where an option is expected are the option, as patterns read # them and as `{:ok, value}` is a result. defp lower_expected_expr_context({:some, value}, %Type{kind: :option} = expected_type, context), - do: %AST.Some{expr: lower_expr(value, Type.inner(expected_type), context)} + do: A.some(lower_expr(value, Type.inner(expected_type), context)) defp lower_expected_expr_context(:none, %Type{kind: :option}, %Context{}), do: %AST.None{} @@ -624,10 +624,7 @@ defmodule RustQ.Meta.Lower do when is_list(fields) do inner = expected_type |> Type.inner() |> Type.expected_value() - %AST.PathCall{ - path: A.path([:ResourceArc, :new]), - args: [lower_expr(expression, inner, context)] - } + A.path_call([:ResourceArc, :new], [lower_expr(expression, inner, context)]) end defp lower_expected_expr_context( @@ -638,10 +635,7 @@ defmodule RustQ.Meta.Lower do when is_list(fields) do inner = expected_type |> Type.inner() |> Type.expected_value() - %AST.PathCall{ - path: A.path([:ResourceArc, :new]), - args: [lower_expr(expression, inner, context)] - } + A.path_call([:ResourceArc, :new], [lower_expr(expression, inner, context)]) end # A tagged tuple or atom where a Rust enum is expected names its variant, as in @@ -658,10 +652,10 @@ defmodule RustQ.Meta.Lower do A.path([rust_name, rust_variant(variant)]) {variant, payloads, types} when length(payloads) == length(types) -> - %AST.PathCall{ - path: A.path([rust_name, rust_variant(variant)]), - args: Enum.zip_with(payloads, types, &lower_expr(&1, &2, context)) - } + A.path_call( + [rust_name, rust_variant(variant)], + Enum.zip_with(payloads, types, &lower_expr(&1, &2, context)) + ) _other -> lower_checked_expr(expression, type, context) @@ -676,14 +670,14 @@ defmodule RustQ.Meta.Lower do ) when kind in [:result, :nif_result] do case Type.inner(expected_type) do - %Type{} = inner -> %AST.Ok{expr: lower_expr(value, inner, context)} - nil -> %AST.Ok{expr: lower_expr(value, context)} + %Type{} = inner -> A.ok(lower_expr(value, inner, context)) + nil -> A.ok(lower_expr(value, context)) end end # `:ok` where nothing is expected is the unit value, as in `ok(:ok)`. defp lower_expected_expr_context(:ok, %Type{kind: :unit}, %Context{}), - do: %AST.Tuple{values: []} + do: A.tuple([]) # An atom where an atom-union enum is expected names its variant. defp lower_expected_expr_context( @@ -790,7 +784,7 @@ defmodule RustQ.Meta.Lower do generics: [target.ast] } - if expected_type.kind == :nif_result, do: decoded, else: %AST.Try{expr: decoded} + if expected_type.kind == :nif_result, do: decoded, else: A.try(decoded) end end @@ -808,9 +802,7 @@ defmodule RustQ.Meta.Lower do defp lower_expected_expr_context(values, %Type{} = expected_type, %Context{} = context) when is_list(values) do - vec = %AST.VecLiteral{ - values: lower_expected_collection_values(values, expected_type, context) - } + vec = A.vec(lower_expected_collection_values(values, expected_type, context)) if slice_type?(expected_type), do: A.ref(vec), else: vec end @@ -822,7 +814,7 @@ defmodule RustQ.Meta.Lower do ) do case expected_option_inner(expected_type) do %Type{} = inner_expected -> - %AST.Some{expr: lower_option_some_expr(expression, inner_expected, context)} + A.some(lower_option_some_expr(expression, inner_expected, context)) nil -> lower_checked_expr({:some, [], [expression]}, expected_type, context) @@ -912,10 +904,10 @@ defmodule RustQ.Meta.Lower do }) A.method(lower_expr(receiver, context), method, [ - %AST.Closure{ - args: [closure_arg!(arg)], - body: lower_closure_body(body, closure_return, closure_context) - } + A.closure( + [closure_arg!(arg)], + lower_closure_body(body, closure_return, closure_context) + ) ]) _other -> @@ -979,12 +971,11 @@ defmodule RustQ.Meta.Lower do do: lower_expr(expression, inner_expected, context) defp lower_expected_tuple(values, types, %Context{} = context) do - %AST.Tuple{ - values: - values - |> Enum.zip(types) - |> Enum.map(fn {value, type} -> lower_expr(value, type, context) end) - } + A.tuple( + values + |> Enum.zip(types) + |> Enum.map(fn {value, type} -> lower_expr(value, type, context) end) + ) end # `match?(pattern, value)` is the `case` that answers whether the value matches. @@ -1019,12 +1010,10 @@ defmodule RustQ.Meta.Lower do end defp lower_expr_context({:unwrap!, _, [expression]}, %Context{} = context), - do: %AST.Try{expr: lower_expr(expression, context)} + do: A.try(lower_expr(expression, context)) defp lower_expr_context({:ok_or!, _, [option, error]}, %Context{} = context) do - %AST.Try{ - expr: A.method(lower_expr(option, context), :ok_or, [lower_nif_error(error, context)]) - } + A.try(A.method(lower_expr(option, context), :ok_or, [lower_nif_error(error, context)])) end defp lower_expr_context({:|>, _, [left, right]}, %Context{} = context), @@ -1034,7 +1023,7 @@ defmodule RustQ.Meta.Lower do do: A.cast(lower_cast_operand(expression, context), lower_type_arg(type, context)) defp lower_expr_context({:decode_as!, _, [expression, type_ast]}, %Context{} = context), - do: %AST.Try{expr: decode_as_expr(expression, type_ast, context)} + do: A.try(decode_as_expr(expression, type_ast, context)) defp lower_expr_context({:decode_as, _, [expression, type_ast]}, %Context{} = context), do: decode_as_expr(expression, type_ast, context) @@ -1059,24 +1048,24 @@ defmodule RustQ.Meta.Lower do nil -> lower_expr(expression, context) end - %AST.UnaryOp{op: :deref, expr: expr} + A.deref(expr) end defp lower_expr_context({:tuple_field, _, [expression, index]}, %Context{} = context) when is_integer(index), - do: %AST.Field{receiver: lower_expr(expression, context), field: index} + do: A.field(lower_expr(expression, context), index) defp lower_expr_context({:some, _, [expression]}, %Context{} = context), - do: %AST.Some{expr: lower_some_arg_expr(expression, context)} + do: A.some(lower_some_arg_expr(expression, context)) defp lower_expr_context({:none, _, []}, %Context{}), do: %AST.None{} defp lower_expr_context({:ok, _, []}, %Context{}), do: %AST.Ok{} defp lower_expr_context({:ok, _, [expression]}, %Context{} = context), - do: %AST.Ok{expr: lower_expr(expression, context)} + do: A.ok(lower_expr(expression, context)) defp lower_expr_context({:err, _, [expression]}, %Context{} = context), - do: %AST.Err{expr: lower_expr(expression, context)} + do: A.err(lower_expr(expression, context)) defp lower_expr_context({:token_macro, _, [path, tokens]}, %Context{}), do: %AST.TokenMacro{path: lower_token_macro_path(path), tokens: tokens} @@ -1167,7 +1156,7 @@ defmodule RustQ.Meta.Lower do do: repeat_expression(group, expression, context) defp lower_expr_context({:index, _, [receiver, index]}, %Context{} = context), - do: %AST.Index{receiver: lower_expr(receiver, context), index: lower_expr(index, context)} + do: A.index(lower_expr(receiver, context), lower_expr(index, context)) # `container[key]` indexes; a map is indexed by a reference to its key. defp lower_expr_context({{:., _, [Access, :get]}, _, [receiver, key]}, %Context{} = context) do @@ -1178,7 +1167,7 @@ defmodule RustQ.Meta.Lower do do: A.ref(index), else: index - %AST.Index{receiver: lower_expr(receiver, context), index: index} + A.index(lower_expr(receiver, context), index) end defp lower_expr_context({:&, _, [{:/, _, [function, arity]}]}, %Context{} = context) @@ -1277,10 +1266,10 @@ defmodule RustQ.Meta.Lower do do: A.path(Paths.mapped_alias_parts(parts, context.rust_modules)) defp lower_expr_context({:{}, _, values}, %Context{} = context), - do: %AST.Tuple{values: Enum.map(values, &lower_expr(&1, context))} + do: A.tuple(Enum.map(values, &lower_expr(&1, context))) defp lower_expr_context({left, right}, %Context{} = context), - do: %AST.Tuple{values: [lower_expr(left, context), lower_expr(right, context)]} + do: A.tuple([lower_expr(left, context), lower_expr(right, context)]) defp lower_expr_context({name, _, args}, %Context{} = context) when is_atom(name) and is_list(args) do @@ -1293,7 +1282,7 @@ defmodule RustQ.Meta.Lower do end defp lower_expr_context(values, %Context{} = context) when is_list(values), - do: %AST.VecLiteral{values: Enum.map(values, &lower_array_value(&1, context))} + do: A.vec(Enum.map(values, &lower_array_value(&1, context))) defp lower_expr_context(value, %Context{}) when is_binary(value), do: A.lit(value) @@ -1320,12 +1309,12 @@ defmodule RustQ.Meta.Lower do expression, %Context{return_type: %Type{}} = context ) do - %AST.Try{expr: Comprehension.lower_for_reduce_expr(expression, context)} + A.try(Comprehension.lower_for_reduce_expr(expression, context)) end defp lower_statement_expr(expression, %Context{return_type: %Type{} = return_type} = context) do if infer_statement_propagation?(expression, return_type, context) do - %AST.Try{expr: lower_expr(expression, context)} + A.try(lower_expr(expression, context)) else lower_expr(expression, context) end @@ -1339,7 +1328,7 @@ defmodule RustQ.Meta.Lower do defp lower_wrapper_arg_expr(expression, %Context{} = context) do if same_wrapper_propagation?(expression, context.return_type, context) do - %AST.Try{expr: lower_expr(expression, context)} + A.try(lower_expr(expression, context)) else lower_expr(expression, context) end @@ -1347,7 +1336,7 @@ defmodule RustQ.Meta.Lower do defp lower_some_arg_expr(expression, %Context{} = context) do if fallible_expression?(expression, context) and propagation_allowed?(context.return_type) do - %AST.Try{expr: lower_expr(expression, context)} + A.try(lower_expr(expression, context)) else lower_wrapper_arg_expr(expression, context) end @@ -1376,7 +1365,7 @@ defmodule RustQ.Meta.Lower do defp lower_cast_operand(expression, %Context{} = context) do if same_wrapper_propagation?(expression, context.return_type, context) do - %AST.Try{expr: lower_expr(expression, context)} + A.try(lower_expr(expression, context)) else lower_expr(expression, context) end @@ -1418,7 +1407,7 @@ defmodule RustQ.Meta.Lower do if option_propagation_in_result_context?(check, context) do lower_expr(expression, context) else - %AST.Try{expr: lower_expr(expression, context)} + A.try(lower_expr(expression, context)) end end @@ -1438,26 +1427,26 @@ defmodule RustQ.Meta.Lower do do: A.mut_ref(lower_expr(expression, context)) defp lower_checked_coercion(%Typing.Check{coercion: :propagate_borrow}, expression, context), - do: A.ref(%AST.Try{expr: lower_expr(expression, context)}) + do: A.ref(A.try(lower_expr(expression, context))) defp lower_checked_coercion( %Typing.Check{coercion: :propagate_mut_borrow}, expression, context ), - do: A.mut_ref(%AST.Try{expr: lower_expr(expression, context)}) + do: A.mut_ref(A.try(lower_expr(expression, context))) defp lower_checked_coercion(%Typing.Check{coercion: :as_ref}, expression, context), do: option_as_ref_expr(lower_expr(expression, context)) defp lower_checked_coercion(%Typing.Check{coercion: :propagate_as_ref}, expression, context), - do: option_as_ref_expr(%AST.Try{expr: lower_expr(expression, context)}) + do: option_as_ref_expr(A.try(lower_expr(expression, context))) defp lower_checked_coercion(%Typing.Check{coercion: :deref}, expression, context), - do: %AST.UnaryOp{op: :deref, expr: lower_expr(expression, context)} + do: A.deref(lower_expr(expression, context)) defp lower_checked_coercion(%Typing.Check{coercion: :some}, expression, context), - do: %AST.Some{expr: lower_expr(expression, context)} + do: A.some(lower_expr(expression, context)) defp lower_checked_coercion(%Typing.Check{coercion: :to_vec}, expression, context), do: A.method(lower_expr(expression, context), :to_vec) @@ -1510,7 +1499,7 @@ defmodule RustQ.Meta.Lower do } end) - %AST.Match{expr: match_expr, arms: arms} + A.match_expr(match_expr, arms) end defp case_scrutinee( @@ -1652,28 +1641,20 @@ defmodule RustQ.Meta.Lower do match_type = declared_case_type(match_type, context) body_context = LowerPattern.context_with_match_pattern(pattern, match_type, context) - %AST.Match{ - expr: lower_expr(expression, context), - arms: [ - %AST.Arm{ - pattern: LowerPattern.lower_with_pattern(pattern, match_type, context), - body: [ - A.return_stmt( - lower_with_matches(rest, body, else_clauses, body_context, expected_type) - ) - ] - }, - with_fallback_arm(with_value, pattern, else_clauses, context, expected_type) - ] - } + A.match_expr(lower_expr(expression, context), [ + %AST.Arm{ + pattern: LowerPattern.lower_with_pattern(pattern, match_type, context), + body: [ + A.return_stmt(lower_with_matches(rest, body, else_clauses, body_context, expected_type)) + ] + }, + with_fallback_arm(with_value, pattern, else_clauses, context, expected_type) + ]) end # Without `else`, an unmatched result or option passes its error or `None` on, as in Elixir. defp with_fallback_arm(_value_name, {:ok, _value}, [], _context, _expected_type), - do: - passthrough_arm(%AST.PatErr{pattern: %AST.PatVar{name: :error}}, %AST.Err{ - expr: A.var(:error) - }) + do: passthrough_arm(%AST.PatErr{pattern: %AST.PatVar{name: :error}}, A.err(A.var(:error))) defp with_fallback_arm(_value_name, {:some, _value}, [], _context, _expected_type), do: passthrough_arm(%AST.PatNone{}, %AST.None{}) @@ -1707,16 +1688,15 @@ defmodule RustQ.Meta.Lower do defp lower_with_else(value_name, [], _context, _expected_type), do: A.var(value_name) defp lower_with_else(value_name, else_clauses, %Context{} = context, expected_type) do - %AST.Match{ - expr: A.var(value_name), - arms: - Enum.map(else_clauses, fn {:->, _, [[pattern], body]} -> - %AST.Arm{ - pattern: LowerPattern.lower_match_pattern(pattern, nil, context), - body: lower_clause_body(body, %{context | position: :return}, expected_type) - } - end) - } + A.match_expr( + A.var(value_name), + Enum.map(else_clauses, fn {:->, _, [[pattern], body]} -> + %AST.Arm{ + pattern: LowerPattern.lower_match_pattern(pattern, nil, context), + body: lower_clause_body(body, %{context | position: :return}, expected_type) + } + end) + ) end defp lower_assignment(target, {op, _meta, [left, right]}, expected_type, %Context{} = context) @@ -1735,10 +1715,7 @@ defmodule RustQ.Meta.Lower do end defp lower_assignment(target, expression, expected_type, %Context{} = context) do - %AST.Assign{ - target: lower_expr(target, context), - expr: lower_expr(expression, expected_type, context) - } + A.assign(lower_expr(target, context), lower_expr(expression, expected_type, context)) end defp lower_assignment_op(target, op, left, right, expected_type, %Context{} = context) do @@ -1752,10 +1729,7 @@ defmodule RustQ.Meta.Lower do expr: lower_expr(right, expected_type, context) } else - %AST.Assign{ - target: lowered_target, - expr: lower_expr({op, [], [left, right]}, expected_type, context) - } + A.assign(lowered_target, lower_expr({op, [], [left, right]}, expected_type, context)) end end @@ -1969,16 +1943,13 @@ defmodule RustQ.Meta.Lower do A.path(Paths.alias_path_parts(receiver, context.rust_modules) ++ [field_or_function]) no_parens? -> - %AST.Field{receiver: lower_expr(receiver, context), field: field_or_function} + A.field(lower_expr(receiver, context), field_or_function) Paths.super_alias_ast?(receiver) -> - %AST.PathCall{path: A.path([:super, field_or_function]), args: []} + A.path_call([:super, field_or_function]) Paths.alias_ast?(receiver) -> - %AST.PathCall{ - path: A.path(Paths.alias_parts(receiver, context.rust_modules) ++ [field_or_function]), - args: [] - } + A.path_call(Paths.alias_parts(receiver, context.rust_modules) ++ [field_or_function]) field_or_function == :into_iter -> receiver_type = @@ -2004,7 +1975,7 @@ defmodule RustQ.Meta.Lower do _method, %Context{} = context ), - do: %AST.Try{expr: lower_expr(receiver, context)} + do: A.try(lower_expr(receiver, context)) defp lower_dot_receiver(receiver, _method, %Context{} = context), do: lower_expr(receiver, context) @@ -2015,26 +1986,17 @@ defmodule RustQ.Meta.Lower do lower_remote_macro_call(receiver, function, args, context) Paths.super_alias_ast?(receiver) -> - %AST.PathCall{ - path: A.path([:super, function]), - args: lower_parent_call_args(function, args, context) - } + A.path_call([:super, function], lower_parent_call_args(function, args, context)) Paths.rust_constructor_alias?(receiver) -> path = Paths.alias_parts(receiver, context.rust_modules) ++ [rust_variant(function)] - %AST.PathCall{ - path: A.path(path), - args: lower_path_call_args(path, function, args, context) - } + A.path_call(path, lower_path_call_args(path, function, args, context)) Paths.alias_ast?(receiver) -> path = Paths.alias_parts(receiver, context.rust_modules) ++ [function] - %AST.PathCall{ - path: A.path(path), - args: lower_path_call_args(path, function, args, context) - } + A.path_call(path, lower_path_call_args(path, function, args, context)) true -> receiver_type = @@ -2056,7 +2018,7 @@ defmodule RustQ.Meta.Lower do _function, %Context{} = context ) do - %AST.Try{expr: lower_expr(receiver, context)} + A.try(lower_expr(receiver, context)) end defp lower_method_receiver(receiver, %Type{} = receiver_type, :unwrap_or, %Context{} = context) do @@ -2347,7 +2309,7 @@ defmodule RustQ.Meta.Lower do %Context{} = context ) when operator in [:+, :-, :*, :/] do - %AST.BinaryOp{left: receiver, op: operator_op(operator), right: lower_expr(right, context)} + A.binary(receiver, operator_op(operator), lower_expr(right, context)) end defp lower_pipe_call(receiver, {name, _, args}, %Context{} = context) @@ -2365,11 +2327,7 @@ defmodule RustQ.Meta.Lower do end defp lower_binary_op(left, op, right, %Context{} = context) do - %AST.BinaryOp{ - left: lower_binary_operand(left, context), - op: op, - right: lower_binary_operand(right, context) - } + A.binary(lower_binary_operand(left, context), op, lower_binary_operand(right, context)) end defp stdlib_context(%Context{} = context, expected \\ nil), @@ -2392,7 +2350,7 @@ defmodule RustQ.Meta.Lower do @doc "`expression`, lowered as an operand: a fallible one propagates its error with `?`." def lower_binary_operand(expression, %Context{} = context) do if fallible_expression?(expression, context) and propagation_allowed?(context.return_type) do - %AST.Try{expr: lower_expr(expression, context)} + A.try(lower_expr(expression, context)) else lower_expr(expression, context) end @@ -2421,10 +2379,10 @@ defmodule RustQ.Meta.Lower do LowerPattern.context_with_match_pattern(pattern, type, acc) end) - %AST.Closure{ - args: Enum.map(args, &closure_arg!/1), - body: lower_closure_body(body, closure_return_type(expected_type), context) - } + A.closure( + Enum.map(args, &closure_arg!/1), + lower_closure_body(body, closure_return_type(expected_type), context) + ) end defp closure_context(args, context) do @@ -2463,10 +2421,10 @@ defmodule RustQ.Meta.Lower do position: :expr } - %AST.Closure{ - args: Enum.map(args, &closure_arg!/1), - body: lower_closure_body(body, expected_return_type, context) - } + A.closure( + Enum.map(args, &closure_arg!/1), + lower_closure_body(body, expected_return_type, context) + ) end # A closure argument may carry the type of the items it is given, as an element of the list @@ -2695,10 +2653,7 @@ defmodule RustQ.Meta.Lower do defp rustler_atom_type, do: RustQ.Spec.type(quote(do: RustQ.Type.atom())) defp parse_syn(type, tokens) do - %AST.PathCall{ - path: A.path([:super, "parse_syn::<#{type}>"]), - args: [quote_tokens(tokens)] - } + A.path_call([:super, "parse_syn::<#{type}>"], [quote_tokens(tokens)]) end defp quote_tokens(tokens) when is_binary(tokens), diff --git a/lib/rustq/meta/lower/enum.ex b/lib/rustq/meta/lower/enum.ex index 0a528bf..d21a0e1 100644 --- a/lib/rustq/meta/lower/enum.ex +++ b/lib/rustq/meta/lower/enum.ex @@ -190,10 +190,10 @@ defmodule RustQ.Meta.Lower.Enum do |> iterate(:keeps_items, context) |> A.method(:enumerate) |> A.method(:map, [ - %AST.Closure{ - args: [%AST.PatTuple{patterns: [%AST.PatVar{name: :index}, %AST.PatVar{name: :item}]}], - body: %AST.Tuple{values: [A.var(:item), A.var(:index)]} - } + A.closure( + [%AST.PatTuple{patterns: [%AST.PatVar{name: :index}, %AST.PatVar{name: :item}]}], + A.tuple([A.var(:item), A.var(:index)]) + ) ]) |> collect(context)} end @@ -240,7 +240,7 @@ defmodule RustQ.Meta.Lower.Enum do |> iterate_as(iteration, context) |> A.method(:try_fold, [lowered(initial, context), closure]) |> A.method(:unwrap_or_else, [ - %AST.Closure{args: [:rustq_halted], body: A.var(:rustq_halted)} + A.closure([:rustq_halted], A.var(:rustq_halted)) ])} end @@ -365,7 +365,7 @@ defmodule RustQ.Meta.Lower.Enum do lowered(enumerable, context) |> A.method(:chunks, [count]) |> A.method(:map, [ - %AST.Closure{args: [:chunk], body: A.method(A.var(:chunk), :to_vec)} + A.closure([:chunk], A.method(A.var(:chunk), :to_vec)) ]) |> collect(context)} @@ -692,14 +692,14 @@ defmodule RustQ.Meta.Lower.Enum do ) do name = closure_arg(argument) condition = closure_body(body, nil, context) - condition = if negate?, do: %AST.UnaryOp{op: :not, expr: condition}, else: condition + condition = if negate?, do: A.not_(condition), else: condition argument = if Collection.copy?(collection_inner(type_of(collection, context))), do: %AST.PatRef{pattern: %AST.PatVar{name: name}}, else: name - closure = %AST.Closure{args: [argument], body: condition} + closure = A.closure([argument], condition) {method, finish} = Map.fetch!(@filter_adapters, terminal) {iterator, kept} = kept_items(iterator, finish) @@ -929,7 +929,7 @@ defmodule RustQ.Meta.Lower.Enum do defp collect(receiver, %Context{}), do: A.method(receiver, :collect) defp cast_i64(expression), - do: A.cast(expression, %AST.TypePath{parts: [:i64]}) + do: A.cast(expression, A.type(:i64)) defp continuation(body) do Macro.prewalk(body, fn @@ -968,7 +968,7 @@ defmodule RustQ.Meta.Lower.Enum do if order == :desc, do: %{ closure - | body: %AST.PathCall{path: A.path([:std, :cmp, :Reverse]), args: [body]} + | body: A.path_call([:std, :cmp, :Reverse], [body]) }, else: closure end @@ -982,8 +982,7 @@ defmodule RustQ.Meta.Lower.Enum do match?({name, _, ctx} when is_atom(name) and is_atom(ctx), default) do {:ok, &A.method(&1, :unwrap_or, [lowered(default, context)])} else - {:ok, - &A.method(&1, :unwrap_or_else, [%AST.Closure{args: [], body: lowered(default, context)}])} + {:ok, &A.method(&1, :unwrap_or_else, [A.closure([], lowered(default, context))])} end end diff --git a/lib/rustq/meta/lower/kernel.ex b/lib/rustq/meta/lower/kernel.ex index d16cb22..721c141 100644 --- a/lib/rustq/meta/lower/kernel.ex +++ b/lib/rustq/meta/lower/kernel.ex @@ -40,12 +40,7 @@ defmodule RustQ.Meta.Lower.Kernel do "Kernel.//2 with integer operands requires an explicit floating-point conversion" ) else - {:ok, - %AST.BinaryOp{ - left: operand(left, context), - op: :div, - right: operand(right, context) - }} + {:ok, A.binary(operand(left, context), :div, operand(right, context))} end end @@ -58,31 +53,23 @@ defmodule RustQ.Meta.Lower.Kernel do else: &operand(&1, context) {:ok, - %AST.BinaryOp{ - left: lower_operand.(left), - op: Map.fetch!(@binary_ops, function), - right: lower_operand.(right) - }} + A.binary(lower_operand.(left), Map.fetch!(@binary_ops, function), lower_operand.(right))} end def lower(%Call{function: function, args: [left, right]}, %Context{} = context) when is_map_key(@binary_ops, function) do {:ok, - %AST.BinaryOp{ - left: operand(left, context), - op: Map.fetch!(@binary_ops, function), - right: operand(right, context) - }} + A.binary(operand(left, context), Map.fetch!(@binary_ops, function), operand(right, context))} end def lower(%Call{function: :-, args: [expression]}, %Context{} = context), - do: {:ok, %AST.UnaryOp{op: :neg, expr: lowered(expression, context)}} + do: {:ok, A.neg(lowered(expression, context))} def lower(%Call{function: :+, args: [expression]}, %Context{} = context), do: {:ok, lowered(expression, context)} def lower(%Call{function: :not, args: [expression]}, %Context{} = context), - do: {:ok, %AST.UnaryOp{op: :not, expr: lowered(expression, context)}} + do: {:ok, A.not_(lowered(expression, context))} def lower(%Call{function: :abs, args: [expression]}, %Context{} = context), do: {:ok, A.method(lowered(expression, context), :abs)} @@ -115,7 +102,7 @@ defmodule RustQ.Meta.Lower.Kernel do def lower(%Call{function: :elem, args: [tuple, index]}, %Context{} = context) when is_integer(index) and index >= 0, - do: {:ok, %AST.Field{receiver: lowered(tuple, context), field: index}} + do: {:ok, A.field(lowered(tuple, context), index)} def lower(%Call{function: :put_elem, args: [tuple, index, value]} = call, %Context{} = context) when is_integer(index) and index >= 0 do @@ -129,13 +116,13 @@ defmodule RustQ.Meta.Lower.Kernel do |> Elixir.Enum.with_index() |> Elixir.Enum.map(fn {type, ^index} -> lowered(value, type, context) - {_type, field} -> %AST.Field{receiver: tuple_var, field: field} + {_type, field} -> A.field(tuple_var, field) end) {:ok, A.block_expr([ %AST.Let{pattern: %AST.PatVar{name: tuple_name}, expr: lowered(tuple, context)}, - A.return_stmt(%AST.Tuple{values: values}) + A.return_stmt(A.tuple(values)) ])} _unknown -> @@ -192,7 +179,7 @@ defmodule RustQ.Meta.Lower.Kernel do def synth(%Call{}, %TypeContext{}), do: :unsupported defp cast_i64(expression), - do: A.cast(expression, %AST.TypePath{parts: [:i64]}) + do: A.cast(expression, A.type(:i64)) defp tuple_elements(%Type{kind: :tuple, meta: %{elements: elements}}), do: elements defp tuple_elements(_type), do: nil diff --git a/lib/rustq/meta/lower/list.ex b/lib/rustq/meta/lower/list.ex index 51f7417..6d2b6c0 100644 --- a/lib/rustq/meta/lower/list.ex +++ b/lib/rustq/meta/lower/list.ex @@ -33,14 +33,14 @@ defmodule RustQ.Meta.Lower.List do end def lower(%Call{function: :wrap, args: [nil]}, %Context{}), - do: {:ok, %AST.VecLiteral{values: []}} + do: {:ok, A.vec([])} def lower(%Call{function: :wrap, args: [value]}, %Context{} = context) do case type_of(value, context) do %Type{} = type -> if collection_inner(type), do: {:ok, lowered(value, context)}, - else: {:ok, %AST.VecLiteral{values: [lowered(value, context)]}} + else: {:ok, A.vec([lowered(value, context)])} _unknown -> :unsupported @@ -51,10 +51,7 @@ defmodule RustQ.Meta.Lower.List do case nonnegative_count(count, context) do {:ok, lowered_count} -> {:ok, - %AST.PathCall{ - path: A.path([:std, :iter, :repeat]), - args: [lowered(value, context)] - } + A.path_call([:std, :iter, :repeat], [lowered(value, context)]) |> A.method(:take, [lowered_count]) |> A.method(:collect)} diff --git a/lib/rustq/meta/lower/map.ex b/lib/rustq/meta/lower/map.ex index 85d7436..eb5820e 100644 --- a/lib/rustq/meta/lower/map.ex +++ b/lib/rustq/meta/lower/map.ex @@ -28,7 +28,7 @@ defmodule RustQ.Meta.Lower.Map do # `Map.new/1,2` is a comprehension; see `RustQ.Meta.Desugar`. defp lower_new([], _context), - do: {:ok, %AST.PathCall{path: A.path([:HashMap, :new]), args: []}} + do: {:ok, A.path_call([:HashMap, :new])} defp lower_keyed(%Call{function: :get, args: [map, key]}, key_type, value_type, context) do lookup = @@ -54,10 +54,7 @@ defmodule RustQ.Meta.Lower.Map do # A missing key raises in Elixir; indexing panics, which Rustler raises. defp lower_keyed(%Call{function: :fetch!, args: [map, key]}, key_type, value_type, context) do - value = %AST.Index{ - receiver: lowered(map, context), - index: Collection.borrowed(key, key_type, context) - } + value = A.index(lowered(map, context), Collection.borrowed(key, key_type, context)) {:ok, if(Collection.copy?(value_type), do: value, else: A.method(value, :clone))} end @@ -195,7 +192,7 @@ defmodule RustQ.Meta.Lower.Map do when is_atom(key) do with %Type{} = map_type <- map_type(type_of(map, context)), %Type{} <- Type.field_type(map_type, key) do - {:ok, %AST.Field{receiver: lowered(map, context), field: key}} + {:ok, A.field(lowered(map, context), key)} else _unknown -> unsupported_field!(call) end @@ -231,10 +228,7 @@ defmodule RustQ.Meta.Lower.Map do expr: lowered(map, context), mutable: true }, - %AST.Assign{ - target: %AST.Field{receiver: variable, field: key}, - expr: lowered(value, field_type, context) - }, + A.assign(A.field(variable, key), lowered(value, field_type, context)), A.return_stmt(variable) ])} else @@ -327,12 +321,12 @@ defmodule RustQ.Meta.Lower.Map do A.block_expr([ %AST.Let{pattern: %AST.PatVar{name: name}, expr: lowered(map, context)}, %AST.Let{pattern: %AST.PatWildcard{}, expr: default}, - A.return_stmt(%AST.Field{receiver: A.var(name), field: key}) + A.return_stmt(A.field(A.var(name), key)) ])} end defp lower_present_get(map, key, _default, false, context), - do: {:ok, %AST.Field{receiver: lowered(map, context), field: key}} + do: {:ok, A.field(lowered(map, context), key)} defp lower_missing_get(map, default, context) do {:ok, diff --git a/lib/rustq/meta/lower/map_set.ex b/lib/rustq/meta/lower/map_set.ex index 7d70250..b32ac02 100644 --- a/lib/rustq/meta/lower/map_set.ex +++ b/lib/rustq/meta/lower/map_set.ex @@ -13,7 +13,7 @@ defmodule RustQ.Meta.Lower.MapSet do @impl true def lower(%Call{function: :new, args: []}, %Context{}), - do: {:ok, %AST.PathCall{path: A.path([:HashSet, :new]), args: []}} + do: {:ok, A.path_call([:HashSet, :new])} def lower(%Call{args: [set | _]} = call, %Context{} = context) do case Collection.keyed_of(set, context) do @@ -65,13 +65,13 @@ defmodule RustQ.Meta.Lower.MapSet do when function in [:difference, :intersection] do kept = A.method(lowered(other, context), :contains, [A.var(:rustq_item)]) - kept = if function == :difference, do: %AST.UnaryOp{op: :not, expr: kept}, else: kept + kept = if function == :difference, do: A.not_(kept), else: kept {:ok, Collection.updated( set, fn set -> - [A.stmt(A.method(set, :retain, [%AST.Closure{args: [:rustq_item], body: kept}]))] + [A.stmt(A.method(set, :retain, [A.closure([:rustq_item], kept)]))] end, context )} @@ -85,7 +85,7 @@ defmodule RustQ.Meta.Lower.MapSet do end defp lower_set(%Call{function: :size, args: [set]}, _item, context) do - {:ok, A.cast(A.method(lowered(set, context), :len), %AST.TypePath{parts: [:i64]})} + {:ok, A.cast(A.method(lowered(set, context), :len), A.type(:i64))} end defp lower_set(%Call{function: :to_list, args: [set]}, item, context) do diff --git a/lib/rustq/meta/lower/stdlib/lowering.ex b/lib/rustq/meta/lower/stdlib/lowering.ex index fcc7a53..4df0cd2 100644 --- a/lib/rustq/meta/lower/stdlib/lowering.ex +++ b/lib/rustq/meta/lower/stdlib/lowering.ex @@ -159,7 +159,7 @@ defmodule RustQ.Meta.Lower.Stdlib.Lowering do {:ok, lowered(count, context)} %Type{kind: kind} when kind in [:u8, :u16, :u32, :u64] -> - {:ok, A.cast(lowered(count, context), %AST.TypePath{parts: [:usize]})} + {:ok, A.cast(lowered(count, context), A.type(:usize))} _type -> :unsupported diff --git a/lib/rustq/meta/lower/string.ex b/lib/rustq/meta/lower/string.ex index 89c5840..6725fb0 100644 --- a/lib/rustq/meta/lower/string.ex +++ b/lib/rustq/meta/lower/string.ex @@ -56,7 +56,7 @@ defmodule RustQ.Meta.Lower.String do bytes = A.method(lowered(string, context), :as_slice) {:ok, - %AST.PathCall{path: A.path([:std, :str, :from_utf8]), args: [bytes]} + A.path_call([:std, :str, :from_utf8], [bytes]) |> then(&%AST.MethodCall{receiver: &1, method: :is_ok, args: []})} %Type{} = type -> diff --git a/lib/rustq/meta/lower/tuple.ex b/lib/rustq/meta/lower/tuple.ex index 47ab3c4..1e34da7 100644 --- a/lib/rustq/meta/lower/tuple.ex +++ b/lib/rustq/meta/lower/tuple.ex @@ -19,13 +19,13 @@ defmodule RustQ.Meta.Lower.Tuple do elements |> Elixir.Enum.with_index() |> Elixir.Enum.map(fn {_type, index} -> - %AST.Field{receiver: variable, field: index} + A.field(variable, index) end) {:ok, A.block_expr([ %AST.Let{pattern: %AST.PatVar{name: name}, expr: lowered(tuple, context)}, - A.return_stmt(%AST.VecLiteral{values: values}) + A.return_stmt(A.vec(values)) ])} else _unknown -> unsupported_tuple!(call) From b2b2a4604bb4699bd34718e643ff5849d4ccd09d Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 26 Oct 2026 13:59:07 +0400 Subject: [PATCH 10/10] Note the lowering behaviour and Builder in the changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bcb01a..4bfdedc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,11 @@ 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