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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion lib/rustq/meta/lower.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion lib/rustq/meta/typing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -558,6 +560,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)
Expand Down
2 changes: 1 addition & 1 deletion lib/rustq/rustler/term.ex
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ defmodule RustQ.Rustler.Term do
@spec cached_struct_keys(
R.raw(:"Env<'_>"),
R.raw(:"&'static OnceLock<Vec<Atom>>"),
R.raw(:"&[&str]")
R.slice(R.str())
) :: R.raw(:"&'static [Atom]")
defrust cached_struct_keys(env, cache, fields) do
cache.get_or_init(fn ->
Expand Down
45 changes: 45 additions & 0 deletions test/rustq/meta/lower/defrust_typing_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -495,9 +505,44 @@ 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();"
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
30 changes: 30 additions & 0 deletions test/rustq/meta/typing_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,36 @@ 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 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")

Expand Down
Loading