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

## Unreleased

### Added

- What std's methods return is known wherever the receiver's own type
parameters decide it: `name.as_ref()` on an `&Option<String>` is an
`Option<&String>`, and `list.first()` an `Option<&T>` of its items.
`Type.substitute/2` binds a type's parameters.
- A list literal whose items share a type is a list of that type.

### Fixed

- `Enum.at` on a pipeline stage is the iterator's `nth`, collecting no list.
- A filter over copied tuples matches each by reference, as `|&(set, field)|`.
- `Enum.count`, `length`, and `byte_size` stay `usize` where one is expected.

## 1.0.0-rc.18 - 2026-10-26

### Added
Expand Down
36 changes: 28 additions & 8 deletions dev/rustq/meta/std_methods/source.ex
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ defmodule RustQ.Meta.StdMethods.Source do

@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`.
return, keyed by the type they belong to and their name, with the type parameters of the
type they belong to, in the order it takes them: `str`'s `len` returns `usize`, and
`Option`'s `as_ref` returns `Option<&T>` for its `T`.
\"""
def returns, do: #{inspect(returns(std), limit: :infinity, printable_limit: :infinity)}
end
Expand Down Expand Up @@ -264,19 +265,38 @@ defmodule RustQ.Meta.StdMethods.Source do
%{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),
concrete?(returns, method.type_parameters -- impl.type_parameters),
reduce: %{} do
returns_by_method -> Map.put_new(returns_by_method, {target, method.name}, returns)
returns_by_method ->
Map.put_new(returns_by_method, {target, method.name}, {parameters(impl), returns})
end
end

# The impl's type parameters, those its type takes first and in its order, as `[:T, :A]` for
# `impl<T, A: Allocator> Vec<T, A>`, so typing binds them to a receiver's type arguments.
defp parameters(%Syn.Impl{target_ast: target, type_parameters: parameters}) do
taken = target |> type_arguments() |> Enum.filter(&(&1 in parameters))
Enum.map(taken ++ (parameters -- taken), &String.to_atom/1)
end

defp type_arguments(%Type.Slice{inner: %Type.Path{name: name}}), do: [name]
defp type_arguments(%Type.Option{inner: %Type.Path{name: name}}), do: [name]

defp type_arguments(%Type.Path{args: args}),
do: for(%Type.Path{name: name} <- args, do: name)

defp type_arguments(_type), do: []

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)
# A type naming none of the method's own type parameters, nor `Self` or an opaque
# `impl Trait`, is known once the receiver's type arguments are: `Option<&T>` on an
# `Option<String>` is `Option<&String>`.
defp concrete?(%Type.Path{name: name, segments: segments, args: args}, parameters),
do:
not Enum.any?([name | segments], &(&1 in ["Self" | parameters])) and
concrete?(args, parameters)

defp concrete?(%Type.Self{}, _parameters), do: false
defp concrete?(%Type.ImplTrait{}, _parameters), do: false
Expand Down
47 changes: 32 additions & 15 deletions lib/rustq/meta/lower/enum.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,21 @@ 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) |> A.method(:count) |> cast_i64()}
:error -> {:ok, collection |> lower_collection(context) |> A.method(:len) |> cast_i64()}
{:ok, stage} ->
{:ok, stage |> continued(context) |> A.method(:count) |> integer_count(context)}

:error ->
{:ok, collection |> lower_collection(context) |> A.method(:len) |> integer_count(context)}
end
end

defp lower_call(%Call{function: :count, args: [collection, predicate]}, %Context{} = context) do
iterator = iterate(collection, :question, context)
{:ok, iterator |> lower_filter(predicate, false, collection, context, :count) |> cast_i64()}

{:ok,
iterator
|> lower_filter(predicate, false, collection, context, :count)
|> integer_count(context)}
end

defp lower_call(%Call{function: :empty?, args: [collection]}, %Context{} = context),
Expand Down Expand Up @@ -301,12 +308,7 @@ defmodule RustQ.Meta.Lower.Enum do
defp lower_call(%Call{function: :at, args: [enumerable, index | default]} = call, context) do
case nonnegative_count(index, context) do
{:ok, index} ->
found =
lowered(enumerable, context)
|> A.method(:get, [index])
|> Collection.read_out(
collection_inner(type_of(enumerable, context)) || %Type{kind: :type}
)
found = item_at(enumerable, index, context)

case default do
[] -> {:ok, found}
Expand Down Expand Up @@ -409,6 +411,22 @@ defmodule RustQ.Meta.Lower.Enum do

defp lower_call(%Call{}, %Context{}), do: :unsupported

# A stage's item is the iterator's `nth`, with no list collected to index into.
defp item_at(enumerable, index, context) do
case stage(enumerable) do
{:ok, stage} ->
stage |> continued(context) |> A.method(:nth, [index])

:error ->
enumerable
|> lowered(context)
|> A.method(:get, [index])
|> Collection.read_out(
collection_inner(type_of(enumerable, context)) || %Type{kind: :type}
)
end
end

@impl true
def synth(%Call{function: :sum, args: [collection]}, %TypeContext{} = context),
do: collection |> type_of(context) |> collection_inner_result()
Expand Down Expand Up @@ -690,14 +708,16 @@ defmodule RustQ.Meta.Lower.Enum do
context,
terminal
) do
name = closure_arg(argument)
pattern = closure_arg(argument)
condition = closure_body(body, nil, context)
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
do: %AST.PatRef{
pattern: if(is_atom(pattern), do: %AST.PatVar{name: pattern}, else: pattern)
},
else: pattern

closure = A.closure([argument], condition)

Expand Down Expand Up @@ -928,9 +948,6 @@ defmodule RustQ.Meta.Lower.Enum do

defp collect(receiver, %Context{}), do: A.method(receiver, :collect)

defp cast_i64(expression),
do: A.cast(expression, A.type(:i64))

defp continuation(body) do
Macro.prewalk(body, fn
{:cont, value} -> quote(do: ok(unquote(value)))
Expand Down
5 changes: 1 addition & 4 deletions lib/rustq/meta/lower/kernel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,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(A.method(lowered(value, context), :len))}
do: {:ok, value |> lowered(context) |> A.method(:len) |> integer_count(context)}

def lower(%Call{function: :map_size, args: [map]} = call, %Context{} = context) do
case struct_fields(shape_type(type_of(map, context))) do
Expand Down Expand Up @@ -178,9 +178,6 @@ defmodule RustQ.Meta.Lower.Kernel do

def synth(%Call{}, %TypeContext{}), do: :unsupported

defp cast_i64(expression),
do: A.cast(expression, A.type(:i64))

defp tuple_elements(%Type{kind: :tuple, meta: %{elements: elements}}), do: elements
defp tuple_elements(_type), do: nil

Expand Down
5 changes: 5 additions & 0 deletions lib/rustq/meta/lower/stdlib/lowering.ex
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ defmodule RustQ.Meta.Lower.Stdlib.Lowering do
])}
end

@doc "A Rust count as Elixir's integer, an `i64`, unless the call is expected to produce a `usize`."
@spec integer_count(AST.expr(), Context.t()) :: AST.expr()
def integer_count(count, %Context{expected: %Type{kind: :usize}}), do: count
def integer_count(count, %Context{}), do: A.cast(count, A.type(:i64))

@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,
Expand Down
Loading
Loading