Skip to content

Commit b152409

Browse files
authored
Merge pull request #37 from ausimian/dialyzer-cleanup
Wire up dialyzer; address all warnings
2 parents c8fc143 + 570fba7 commit b152409

8 files changed

Lines changed: 104 additions & 11 deletions

File tree

.dialyzer_ignore.exs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Dialyzer warning suppressions. Every entry is a false positive driven
2+
# by a dialyzer limitation documented below, not a correctness issue.
3+
# Use `{file, warning_type[, line]}` tuples; add a comment per cluster.
4+
5+
[
6+
# ---------------------------------------------------------------
7+
# Emily.Backend — Nx.Backend callback type mismatches
8+
# ---------------------------------------------------------------
9+
#
10+
# `wrap/3` (lib/emily/backend.ex:71) returns `%T{out | data: %B{...}}`,
11+
# which dialyzer's map-update analysis infers as a partial map
12+
# (`%Nx.Tensor{data, type, _ => _}`) — the unchanged `:shape`,
13+
# `:names`, `:vectorized_axes` keys drop out of the inferred type
14+
# because dialyzer doesn't propagate struct field types across map
15+
# updates. Nx.Backend's callback specs require the full
16+
# `Nx.Tensor.t()` record, so dialyzer reports a "nothing in common"
17+
# mismatch for every `@impl`'d callback (≈115 warnings).
18+
#
19+
# The code IS correct — we genuinely return complete `%Nx.Tensor{}`
20+
# structs — so suppressing the category wholesale is the right call.
21+
# Attempts to coax dialyzer (guards, explicit struct reconstruction,
22+
# narrower `@type tensor`) fix the shape/names tracking but introduce
23+
# their own quirks. Revisit if dialyzer's map-update analysis improves
24+
# (pre-Erlang 28 this was worse).
25+
{"lib/emily/backend.ex", :callback_type_mismatch},
26+
{"lib/emily/backend.ex", :callback_arg_type_mismatch},
27+
# Same root cause — `wrap/3`'s success typing ends up wider than the
28+
# declared `@spec wrap(ref(), tensor(), reference()) :: tensor()`.
29+
{"lib/emily/backend.ex", :invalid_contract, 71},
30+
31+
# ---------------------------------------------------------------
32+
# Emily.Fast — `Nx.Defn.Expr.optional/3` untyped return
33+
# ---------------------------------------------------------------
34+
#
35+
# `Nx.Defn.Expr.optional/3` has no `@spec` (see
36+
# `deps/nx/lib/nx/defn/expr.ex`). Dialyzer infers its return as
37+
# `tuple() | %{data: %Nx.Defn.Expr{…}, _ => _}` — the tuple branch
38+
# only fires when the fallback fun itself returns a tuple, which ours
39+
# don't. At runtime every one of these returns a proper
40+
# `Nx.Tensor.t()`, matching the declared `@spec`. Suppress the contract
41+
# check until Nx specs `optional/3`.
42+
{"lib/emily/fast.ex", :invalid_contract},
43+
44+
# ---------------------------------------------------------------
45+
# Emily.Quantization / Emily.QuantizedWeight — tensor construction
46+
# ---------------------------------------------------------------
47+
#
48+
# `quantized_matmul/2`, `from_dense/2`, and `to_dense/1` build a
49+
# fresh `%T{data: %B{ref: …}, shape: …, type: …, names: …}` from
50+
# NIF-returned refs. Dialyzer infers the resulting tensor's `:type`
51+
# as `{atom(), non_neg_integer()}` — `Native.dtype/1`'s declared
52+
# return — which doesn't overlap with `Nx.Tensor.t()`'s
53+
# `Nx.Type.t()` specific union. The NIF does return a valid
54+
# `Nx.Type.t()` tuple; the width is a declaration limitation in
55+
# the stub module.
56+
{"lib/emily/quantization.ex", :invalid_contract, 67},
57+
{"lib/emily/quantized_weight.ex", :invalid_contract, 78},
58+
{"lib/emily/quantized_weight.ex", :invalid_contract, 112}
59+
]

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ emily-*.tar
2626
/priv/libemily.*
2727
/priv/mlx/
2828

29+
# Dialyzer PLTs
30+
/priv/plts/
31+
2932
# Editor / tooling
3033
.DS_Store
3134
.elixir_ls/

lib/emily.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ defmodule Emily do
5454
@opaque t :: reference()
5555

5656
@typedoc "An Nx-compatible dtype, e.g. `{:f, 32}` or `{:s, 64}`."
57-
@type dtype :: {atom(), non_neg_integer()}
57+
@type dtype :: Nx.Type.t()
5858

5959
@doc """
6060
Build a lazy MLX tensor from a raw binary.

lib/emily/backend.ex

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ defmodule Emily.Backend do
4545
@typep tensor :: T.t()
4646
@typep ref :: reference()
4747

48+
# The four callbacks below are genuinely unimplementable on MLX and
49+
# exist solely to raise — dialyzer's `:error_handling` flag would
50+
# otherwise flag every one as "only terminates with explicit exception".
51+
@dialyzer {:nowarn_function,
52+
[
53+
from_pointer: 5,
54+
to_pointer: 2,
55+
count_leading_zeros: 2,
56+
population_count: 2
57+
]}
58+
4859
# =================================================================
4960
# Helpers
5061
# =================================================================
@@ -58,8 +69,8 @@ defmodule Emily.Backend do
5869
defp ref(%T{} = t), do: t |> Nx.backend_transfer(Emily.Backend) |> ref()
5970

6071
@spec wrap(ref(), tensor(), reference()) :: tensor()
61-
defp wrap(ref, %T{type: type} = out, w) do
62-
%{out | data: %B{ref: coerce(ref, type, w)}}
72+
defp wrap(ref, %T{} = out, w) do
73+
%{out | data: %B{ref: coerce(ref, out.type, w)}}
6374
end
6475

6576
# Fast path: pred→u8 is the most common mismatch (MLX comparison/logical
@@ -647,6 +658,11 @@ defmodule Emily.Backend do
647658
end
648659

649660
@impl true
661+
# `{:pred, 1}` is Nx's 1-bit boolean dtype, mapped to `mx::bool_` in
662+
# `c_src/emily/dtype.hpp`. Not listed in `Nx.Type.t()` so
663+
# `Native.astype/3`'s spec is too narrow — suppress here rather than
664+
# widen the Native dtype type (that pollutes every wrapped tensor).
665+
@dialyzer {:nowarn_function, select: 4}
650666
def select(%T{} = out, pred, on_true, on_false) do
651667
w = worker()
652668
cond_ref = Native.astype(w, ref(pred), {:pred, 1})

lib/emily/native.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ defmodule Emily.Native do
2020

2121
@type tensor :: reference()
2222
@type worker :: reference()
23-
@type dtype :: {atom(), non_neg_integer()}
23+
@type dtype :: Nx.Type.t()
2424

2525
defp nif, do: :erlang.nif_error(:nif_not_loaded)
2626

lib/emily/telemetry.ex

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,14 @@ defmodule Emily.Telemetry do
115115
def init_dedup_table do
116116
case :ets.whereis(@dedup_table) do
117117
:undefined ->
118-
:ets.new(@dedup_table, [
119-
:set,
120-
:public,
121-
:named_table,
122-
read_concurrency: true,
123-
write_concurrency: true
124-
])
118+
_ =
119+
:ets.new(@dedup_table, [
120+
:set,
121+
:public,
122+
:named_table,
123+
read_concurrency: true,
124+
write_concurrency: true
125+
])
125126

126127
:ok
127128

mix.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ defmodule Emily.MixProject do
2121
compilers: [:emily_mlx, :elixir_make] ++ Mix.compilers(),
2222
make_env: &make_env/0,
2323
test_coverage: test_coverage(),
24+
dialyzer: dialyzer(),
2425
docs: docs(),
2526
package: package()
2627
]
@@ -69,10 +70,21 @@ defmodule Emily.MixProject do
6970
{:scidata, "~> 0.1", only: :test},
7071
{:stream_data, "~> 1.1", only: [:dev, :test]},
7172
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
73+
{:dialyxir, "~> 1.4", only: [:dev], runtime: false},
7274
{:ex_doc, "~> 0.34", only: :docs, runtime: false}
7375
]
7476
end
7577

78+
defp dialyzer do
79+
[
80+
plt_add_apps: [:mix, :ex_unit],
81+
plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
82+
plt_core_path: "priv/plts/core.plt",
83+
flags: [:error_handling, :unknown, :unmatched_returns, :extra_return],
84+
ignore_warnings: ".dialyzer_ignore.exs"
85+
]
86+
end
87+
7688
defp aliases do
7789
[
7890
precommit: [

mix.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
"complex": {:hex, :complex, "0.6.0", "b0130086a7a8c33574d293b2e0e250f4685580418eac52a5658a4bd148f3ccf1", [:mix], [], "hexpm", "0a5fa95580dcaf30fcd60fe1aaf24327c0fe401e98c24d892e172e79498269f9"},
77
"credo": {:hex, :credo, "1.7.18", "5c5596bf7aedf9c8c227f13272ac499fe8eae6237bd326f2f07dfc173786f042", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a189d164685fd945809e862fe76a7420c4398fa288d76257662aecb909d6b3e5"},
88
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
9+
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
910
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
1011
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
12+
"erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"},
1113
"ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"},
1214
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
1315
"fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"},

0 commit comments

Comments
 (0)