From 70cba49404fef4aa866de92ce935e078c7cc66b2 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Tue, 27 Oct 2026 02:34:04 +0400 Subject: [PATCH 1/2] Type Option.as_ref as an option of a reference --- lib/rustq/meta/typing.ex | 12 ++++++++++ test/rustq/meta/lower/defrust_typing_test.exs | 11 ++++++++++ test/rustq/meta/typing_test.exs | 22 +++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/lib/rustq/meta/typing.ex b/lib/rustq/meta/typing.ex index 0650ba8..f27dfa4 100644 --- a/lib/rustq/meta/typing.ex +++ b/lib/rustq/meta/typing.ex @@ -558,6 +558,18 @@ defmodule RustQ.Meta.Typing do end end + # Borrowing an option, owned or behind a reference, gives an option of a reference; an option + # of a reference stays as it is. + defp synth_method_call({{:., _, [receiver, :as_ref]}, _meta, []} = ast, %Env{} = env) do + with %Type{} = type <- synth(receiver, env), + %Type{kind: :option} = option <- Type.ref_inner(type) || type, + %Type{} = inner <- Type.inner(option) do + if Type.ref_inner(inner), do: option, else: Type.option(Type.ref(inner)) + else + _unknown -> synth_method_return(ast, env) + end + end + defp synth_method_call({{:., _, [receiver, :ok_or]}, _meta, [_error]}, %Env{} = env) do receiver |> synth(env) diff --git a/test/rustq/meta/lower/defrust_typing_test.exs b/test/rustq/meta/lower/defrust_typing_test.exs index 644ac9a..600242e 100644 --- a/test/rustq/meta/lower/defrust_typing_test.exs +++ b/test/rustq/meta/lower/defrust_typing_test.exs @@ -469,6 +469,16 @@ defmodule RustQ.Meta.Lower.DefrustTypingTest do ok(named(base, count)) end + @spec present(R.option(R.ref(item()))) :: R.bool() + defrustp(present(base), do: base != nil) + + @spec reused(holder()) :: R.bool() + defrustp reused(holder) do + base = holder.base.as_ref() + found = present(base) + found and base != nil + end + @spec count(R.ref(holder())) :: R.usize() defrustp count(holder) do case holder.base do @@ -495,6 +505,7 @@ defmodule RustQ.Meta.Lower.DefrustTypingTest do source = BorrowedOptionCase.__rustq_source__() assert source =~ "Ok(named(base, count))" + assert source =~ "let found = present(base);" assert source =~ "match &holder.base {" assert source =~ "Some(item) => item.names.len()" assert source =~ "let mut tally = HashMap::new();" diff --git a/test/rustq/meta/typing_test.exs b/test/rustq/meta/typing_test.exs index 1070c43..4c26366 100644 --- a/test/rustq/meta/typing_test.exs +++ b/test/rustq/meta/typing_test.exs @@ -132,6 +132,28 @@ defmodule RustQ.Meta.TypingTest do assert %Typing.Check{coercion: :none} = Typing.check(expression, option, env) end + test "synthesizes Option.as_ref as an option of a reference" do + term = type(:term, "Term") + borrowed = Type.option(Type.ref(term)) + + env = + Typing.env( + vars: %{ + owned: Type.option(term), + lent: Type.ref(Type.option(term)), + borrowed: borrowed + }, + callables: [] + ) + + assert Typing.synth(quote(do: owned.as_ref()), env) == borrowed + assert Typing.synth(quote(do: lent.as_ref()), env) == borrowed + assert Typing.synth(quote(do: borrowed.as_ref()), env) == borrowed + + assert %Typing.Check{coercion: :none} = + Typing.check(quote(do: owned.as_ref()), borrowed, env) + end + test "synthesizes unambiguous parent-module free-function returns" do string = type(:type, "String") From f7a92362fc6fa8990a4277dbf5bf925be165bea6 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Tue, 27 Oct 2026 02:49:15 +0400 Subject: [PATCH 2/2] Borrow items indexed out of slices and type enum_variant payloads --- lib/rustq/meta/lower.ex | 15 +++++++- lib/rustq/meta/typing.ex | 4 ++- lib/rustq/rustler/term.ex | 2 +- test/rustq/meta/lower/defrust_typing_test.exs | 34 +++++++++++++++++++ test/rustq/meta/typing_test.exs | 8 +++++ 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/lib/rustq/meta/lower.ex b/lib/rustq/meta/lower.ex index 5d4f55b..9eee4f8 100644 --- a/lib/rustq/meta/lower.ex +++ b/lib/rustq/meta/lower.ex @@ -165,7 +165,8 @@ defmodule RustQ.Meta.Lower do expr: expression |> lower_expr(expected_type, expression_context) - |> Comprehension.collect_as(Comprehension.collect_target(expected_type)), + |> Comprehension.collect_as(Comprehension.collect_target(expected_type)) + |> lent_item(expression, context), type: empty_collection_type(expression, expected_type) } |> Mutability.updated_in_place() @@ -222,6 +223,17 @@ defmodule RustQ.Meta.Lower do A.stmt(lower_statement_expr(expression, context)) end + # An item cannot be moved out of the collection it is indexed in, so one that cannot be copied + # is borrowed. + defp lent_item(%AST.Index{} = expr, expression, %Context{} = context) do + case Typing.synth(expression, typing_env(context)) do + %Type{} = type -> if Collection.copy?(type), do: expr, else: A.ref(expr) + nil -> expr + end + end + + defp lent_item(expr, _expression, _context), do: expr + defp cond_statement([{:->, _, [[true], body]} | _rest]), do: body defp cond_statement([{:->, meta, [[condition], body]} | rest]) do @@ -2386,6 +2398,7 @@ defmodule RustQ.Meta.Lower do case pattern do tag when is_atom(tag) -> {tag, []} {:{}, _, [tag | payloads]} -> {tag, payloads} + {:enum_variant, _, [_path, tag | payloads]} -> {tag, payloads} {tag, payload} -> {tag, [payload]} _other -> {nil, []} end diff --git a/lib/rustq/meta/typing.ex b/lib/rustq/meta/typing.ex index f27dfa4..e4046ab 100644 --- a/lib/rustq/meta/typing.ex +++ b/lib/rustq/meta/typing.ex @@ -135,9 +135,10 @@ defmodule RustQ.Meta.Typing do end end + # A slice is indexed before any reference is looked through, which keeps its items' metadata. def synth({:index, _meta, [collection, _index]}, %Env{} = env) do case synth(collection, env) do - %Type{} = type -> indexed_type(Type.ref_inner(type) || type) + %Type{} = type -> indexed_type(type) || indexed_type(Type.ref_inner(type)) nil -> nil end end @@ -355,6 +356,7 @@ defmodule RustQ.Meta.Typing do 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 indexed_type(nil), do: nil defp synth_stdlib({name, _meta, args} = call_ast, %Env{} = env) when is_atom(name) and is_list(args) do diff --git a/lib/rustq/rustler/term.ex b/lib/rustq/rustler/term.ex index 7622b43..f93d43e 100644 --- a/lib/rustq/rustler/term.ex +++ b/lib/rustq/rustler/term.ex @@ -67,7 +67,7 @@ defmodule RustQ.Rustler.Term do @spec cached_struct_keys( R.raw(:"Env<'_>"), R.raw(:"&'static OnceLock>"), - R.raw(:"&[&str]") + R.slice(R.str()) ) :: R.raw(:"&'static [Atom]") defrust cached_struct_keys(env, cache, fields) do cache.get_or_init(fn -> diff --git a/test/rustq/meta/lower/defrust_typing_test.exs b/test/rustq/meta/lower/defrust_typing_test.exs index 600242e..4c85ba8 100644 --- a/test/rustq/meta/lower/defrust_typing_test.exs +++ b/test/rustq/meta/lower/defrust_typing_test.exs @@ -511,4 +511,38 @@ defmodule RustQ.Meta.Lower.DefrustTypingTest do assert source =~ "let mut tally = HashMap::new();" assert RustQ.valid?(source, "borrowed_option.rs") end + + test "items read out of a borrowed slice keep their types" do + defmodule IndexedItemCase do + use RustQ.Meta + alias RustQ.Type, as: R + + @typep kind :: R.enum(one: [R.u32()], none: []) + @typep field :: %{required(:id) => R.u32(), required(:kind) => kind()} + + @spec named(R.ref(kind())) :: R.u32() + defrustp named(kind) do + case kind do + enum_variant(Kind, :one, value) -> taken(value) + enum_variant(Kind, :none) -> 0 + end + end + + @spec taken(R.u32()) :: R.u32() + defrustp(taken(value), do: value) + + @spec at(R.slice(field()), R.usize()) :: R.u32() + defrustp at(fields, index) do + field = index(fields, index) + field.id + named(index(fields, index).kind) + end + end + + source = IndexedItemCase.__rustq_source__() + + assert source =~ "Kind::One(value) => taken(*value)," + assert source =~ "let field = &fields[index];" + assert source =~ "field.id + named(&fields[index].kind)" + assert RustQ.valid?(source, "indexed_item.rs") + end end diff --git a/test/rustq/meta/typing_test.exs b/test/rustq/meta/typing_test.exs index 4c26366..2821291 100644 --- a/test/rustq/meta/typing_test.exs +++ b/test/rustq/meta/typing_test.exs @@ -154,6 +154,14 @@ defmodule RustQ.Meta.TypingTest do Typing.check(quote(do: owned.as_ref()), borrowed, env) end + test "synthesizes an item indexed out of a borrowed slice with its metadata" do + point = %{type(:struct, "Point") | meta: %{fields: [{:x, type(:f32, "f32")}]}} + env = Typing.env(vars: %{points: Type.slice_ref(point)}, callables: []) + + assert Typing.synth(quote(do: index(points, 0)), env) == point + assert Typing.synth(quote(do: index(points, 0).x), env) == type(:f32, "f32") + end + test "synthesizes unambiguous parent-module free-function returns" do string = type(:type, "String")