diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9c045fa..d9a32923 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -87,8 +87,9 @@
the body of a range that runs, and after a raising guard,
`if w < 1, do: raise(ArgumentError, "...")`. docs/language.md now
recommends raising guards for what a kernel needs of its arguments. When
- a check stays, the fast-path report names a small fact that would drop
- it: `nothing proves i - w < length(x) (it would follow from w >= 0)`. An
+ a check stays, the fast-path report of a module with `report: :full`
+ names a small fact that would drop it:
+ `nothing proves i - w < length(x) (it would follow from w >= 0)`. An
`if` value whose arm relies on its condition to drop a check is a branch,
not a select. Results are unchanged.
- Faster: a row-major index with a runtime row length, `m[r * dim + j]`,
diff --git a/docs/adr/0007-symbolic-bounds.md b/docs/adr/0007-symbolic-bounds.md
index d64b7e45..92e87adf 100644
--- a/docs/adr/0007-symbolic-bounds.md
+++ b/docs/adr/0007-symbolic-bounds.md
@@ -87,7 +87,10 @@ Replace the fact model with one representation:
The compiler is at about +29% of its time before step 1, past the
ceiling; the next step cuts first. The copy analysis's one pass per arm
(`defk-o08.3`, 6.39) took 9% off: +17.6% of the work before step 1, still
- past the ceiling.
+ past the ceiling. Each statement's reads walked once (`defk-o08.4`, 6.40)
+ took 8.6% more off: +7.4% of the work before step 1 (-1.0% without
+ `SatEx.Kernels`), under the ceiling again. The report's hint search only
+ under `report: :full` (`defk-o08.5`, 6.41): +3.5% (-2.2%).
- **Raising guards become the way to state facts about arguments** in a
kernel, which the language docs would describe.
- **Out of scope:** degree 3 and nested products, facts across loop
diff --git a/docs/agents/runs.md b/docs/agents/runs.md
index d964836b..bcb5f8f2 100644
--- a/docs/agents/runs.md
+++ b/docs/agents/runs.md
@@ -12,7 +12,12 @@ no more.
`dev/dump_c.exs`): the kernels compile, their C goes to `
`, and no
library is built (`DEFK_NO_CC=1`, set by the alias). Every test file and
example took 68 s on 2026-10-19 with a cold library cache; named files take
- seconds. Diff two dumps to see which modules' C changed.
+ seconds. Diff two dumps to see which modules' C changed. Beside each
+ `.c` the dump writes `.report`, the compiler's report notes
+ (`__defk_report__/0` without `report: :full`'s clang part), so the same
+ diff shows a change to the report; its notes name absolute paths only for
+ a note in another file, so dump both sides from the same directory when a
+ report diff matters.
- **An example's check** (`mix defk.examples --only a,b`): builds the
libraries and runs the example's `:check`, defk against Elixir. No timing.
- **`mix test`, `mix ci`**: before the PR, as `AGENTS.md` says.
diff --git a/docs/fast-path-report.md b/docs/fast-path-report.md
index 0e4361b5..1519ca46 100644
--- a/docs/fast-path-report.md
+++ b/docs/fast-path-report.md
@@ -40,10 +40,11 @@ next_sum/1 test/report_test.exs:16
20 | end
```
-When one small fact about the kernel's values would prove the missing
-half, the note names it. Here a raising guard `if w < 1, do: raise(...)`
-before the loop (docs/language.md, "Preconditions: raising guards") would
-drop both checks:
+With `use Defk, report: :full` (below), when one small fact about the
+kernel's values would prove the missing half, the note names it. Here a
+raising guard `if w < 1, do: raise(...)` before the loop
+(docs/language.md, "Preconditions: raising guards") would drop both
+checks:
```text
6 | n -> if x[i] > x[i - w], do: n + 1, else: n # x[i]: bounds check kept; nothing proves 0 <= i (it would follow from w >= 0)
@@ -52,7 +53,9 @@ drop both checks:
The fact is a hint: the residual with the fewest terms that the proof
search reached, when it has at most two variables or lengths and a small
-constant. Another fact could also do.
+constant. Another fact could also do. The search runs once per kept check,
+so a module asks for it with `report: :full`; without it the note ends at
+what nothing proves.
An `if`, `cond` or `case` value whose arms are pure arithmetic becomes a
select, which computes every arm. An arm that dropped a check only because
@@ -88,7 +91,7 @@ it before the loop and clearing each pass) and `:vectorize`.
defk's own facts are noted while the kernel is lowered. They cost no extra
compiler pass and nothing at run time.
-## Vectorisation: `report: :full`
+## Hints and vectorisation: `report: :full`
```elixir
defmodule Stats do
@@ -97,7 +100,8 @@ defmodule Stats do
end
```
-This adds a `{:vectorize, verdict, text}` fact to each loop, taken from
+This names the fact that would drop a kept bounds check, as shown above,
+and adds a `{:vectorize, verdict, text}` fact to each loop, taken from
clang's `-Rpass=loop-vectorize`, `-Rpass-missed=loop-vectorize` and
`-Rpass-analysis=loop-vectorize` remarks. The verdict is `:vectorized`,
`:missed` (with clang's reasons), `:partial` (some of the loop's C forms,
diff --git a/docs/language.md b/docs/language.md
index e4787eb4..d4487cb2 100644
--- a/docs/language.md
+++ b/docs/language.md
@@ -1229,7 +1229,8 @@ lengths, through:
It does not use products of three variables (`(y * w + x) * ch`), a `!=`
or an `or` that holds, or `case` guards yet: such an index keeps its check, and the
fast-path report says which half (`0 <= i` or `i < length(a)`) nothing
-proves, and, when one small fact would prove it, that fact:
+proves, and, with `use Defk, report: :full`, when one small fact would
+prove it, that fact:
`nothing proves i - w < length(x) (it would follow from w >= 0)`. That
is a hint, not a promise: another fact could also do.
@@ -2156,7 +2157,8 @@ iex> M.rises(<<1::64-native>>, 0)
`x[i]` and `x[i - w]` have no bounds check: `i >= w` from the range and
`w >= 1` from the guard give `0 <= i - w` and `0 <= i`, and
`i <= length(x) - 1` gives the rest. Without the guard both keep their
-checks, and the fast-path report names the fact they lack:
+checks, and the fast-path report (with `use Defk, report: :full`) names
+the fact they lack:
`x[i - w]: bounds check kept; nothing proves i - w < length(x) (it would
follow from w >= 0)`. The guard costs one compare per call; a check in the
loop costs one per element and may keep clang from vectorising it.
diff --git a/docs/performance.md b/docs/performance.md
index f9e449d1..eb1861c8 100644
--- a/docs/performance.md
+++ b/docs/performance.md
@@ -2577,6 +2577,62 @@ else: 0))`; it now says `if first < 0 and n > 0, do: raise(ArgumentError,
- **Source:** `defk-o08.3`; `test/copies_test.exs`.
- **Status:** measured, provisional.
+### 6.40 Each statement's reads walked once: another -9% of the compiler's work
+
+- **Numbers:** entering a statement computed its reads three times: in
+ `Copies.live_after/1` (`stmt_reads/1`), in the compiler's `hoist/3` and
+ in `Copies.enter_stmt/4` (`reads/1`, the same set: `read_counts/1` skips a
+ match's left side as `stmt_reads/1` does). `Copies.block_liveness/1` keeps
+ each statement's reads beside its live set, and `enter/3` hands them on.
+ Over the 108 modules of `mix dump_c`, two rounds each, interleaved with
+ main: 125.9 -> 115.0 M reductions (-8.6%, the same without
+ `SatEx.Kernels`), 987 -> 934 ms (-5.4%); `HashChain.Kernels` and its
+ Elixir-mode twin -2.2 M reductions each, `SatEx.Kernels` -3.6 M. Against
+ the compiler before step 1 (6.38's baseline, by reductions): +7.4%
+ (-1.0% without `SatEx.Kernels`), from +17.6%: under ADR-0007's ceiling.
+- **Same C:** `mix dump_c` byte-identical for all 108 modules; a property
+ test checks `reads/1 == stmt_reads/1` and that `block_liveness/1` agrees
+ with `live_after/1` on generated blocks.
+- **Not done:** a statement nested d blocks deep is still walked once per
+ enclosing statement (its reads are part of theirs); building a
+ statement's reads from its blocks' would need the walk to follow the
+ lowering's order.
+- **Conditions:** M-series Mac on battery, no quiet check; `cc` not run.
+- **Source:** `defk-o08.4`; `test/copies_test.exs`.
+- **Status:** measured, provisional.
+
+### 6.41 The report's hint search runs only under `report: :full`: -3.6% of the compiler's work
+
+- **Numbers:** the search for "it would follow from ..." (`Facts.residual/5`
+ for every kept bounds check) took about 2.4% of the compiler's time, 6% on
+ `SatEx.Kernels`. It now runs only in a module with `use Defk, report:
+ :full` (maintainer's call, 2026-10-20). Over the 108 modules of `mix
+ dump_c`, two rounds each, interleaved with 6.40's state: 114.9 -> 110.8 M
+ reductions (-3.6%; -1.2% without `SatEx.Kernels`, which alone saves
+ 3.2 M), 932 -> 922 ms (-1.1%, within drift). Same C (but for the path-hashed
+ loop markers of the two test modules that took `report: :full`); every
+ report the same less its hints, and two notes of `HashChain.Kernels` that
+ differed only by their hints now kept once (a note made twice is kept
+ once). Against the compiler before step 1 (6.38's baseline, by
+ reductions): +3.5% (-2.2% without `SatEx.Kernels`).
+- **Tried first, not adopted** (both keep every report the same, checked
+ with `mix dump_c`'s `.report` files, two rounds interleaved against
+ main):
+ - A text made only when the note survives (a closure, rendered in
+ `report/0`, so a lowering that is undone and redone drops its notes
+ unrendered): -1.5% of the work (reductions), all on `SatEx.Kernels`
+ (-1.85 M; no other module redoes a lowering), but +9.0% time (+4.3%
+ without `SatEx.Kernels`, both rounds): every kept note's closure holds
+ the env and the facts scope of its moment until the compile ends, and
+ the collector copies them. Not adopted.
+ - A smaller `@hint_budget`: 8 drops the hints of two notes (a kernel of
+ `{:load, 9}`) for -0.5% of the work, 4 drops hints in four modules for
+ -0.9%. Not adopted: 16 stays.
+- **Conditions:** M-series Mac on battery, no quiet check; `cc` not run.
+- **Source:** `defk-o08.5`; `test/condition_facts_test.exs` (a kernel with and
+ without `report: :full`).
+- **Status:** measured, provisional.
+
---
## 7. When a hand-written or wrapper NIF is still the answer
@@ -2884,4 +2940,4 @@ example or close a workload ticket, add its entries and its line here.
`defk-sve`, `defk-sve.1` ([`state-copy-cost.md`](https://github.com/DanielLuning/defk/blob/main/docs/research/state-copy-cost.md)), `defk-ulk`, `defk-ulk.1` ([`simd.md`](https://github.com/DanielLuning/defk/blob/main/docs/research/simd.md)), `defk-33b`,
`defk-ulk.4`, `defk-52v.15` ([`cdcl_sat.exs`](https://github.com/DanielLuning/defk/blob/main/examples/cdcl_sat.exs)), `defk-tif`, `defk-gqk`, `defk-6ag`,
`defk-7dm` ([`bench/port_roundtrip.exs`](https://github.com/DanielLuning/defk/blob/main/bench/port_roundtrip.exs)), `defk-1ub`,
- `defk-o08.2`, `defk-o08.3`.
+ `defk-o08.2`, `defk-o08.3`, `defk-o08.4`, `defk-o08.5`.
diff --git a/lib/defk.ex b/lib/defk.ex
index e32f349d..f6baad61 100644
--- a/lib/defk.ex
+++ b/lib/defk.ex
@@ -47,8 +47,10 @@ defmodule Defk do
`Mix.install` script, for instance). The kernels' own functions still
run natively. Default `false`. See `Defk.ElixirMode`.
* `report: :full` adds the C compiler's vectorisation of each loop to the
- [fast-path report](`Defk.Report`), at the cost of a second C compile.
- What it adds is unstable.
+ [fast-path report](`Defk.Report`), at the cost of a second C compile,
+ and names, for a kept bounds check, a small fact that would drop it,
+ at the cost of a proof search per kept check. What it adds is
+ unstable.
* `adaptive_slice: false` turns off the short first slice on a crowded
run queue. By default a yielding kernel's function reads
`:erlang.statistics(:total_run_queue_lengths)` (about 9 ns) and, when
@@ -444,7 +446,8 @@ defmodule Defk do
nif_name: nif_name,
fault_hooks: fault_hooks?,
adaptive_slice: adaptive?,
- max_owned_bytes: max_owned_bytes!(env.module)
+ max_owned_bytes: max_owned_bytes!(env.module),
+ hints: Module.get_attribute(env.module, :defk_report) == :full
)
end)
@@ -452,7 +455,7 @@ defmodule Defk do
# renamed, and the kernel's function passes the hint
all_sigs = Enum.map(all_sigs, &Map.update!(&1, :yields, fn y -> adaptive? and y end))
- if dump = System.get_env("DEFK_DUMP_C"), do: dump_c!(dump, env.module, c_src)
+ if dump = System.get_env("DEFK_DUMP_C"), do: dump_c!(dump, env.module, c_src, report)
{report, placement} = library(env, c_src, report)
@@ -514,12 +517,19 @@ defmodule Defk do
end
# DEFK_DUMP_C=: every module compiled while it is set writes its
- # generated C to /.c — the byte-identical gate for compiler
- # refactors (dev/dump_c.exs). A pure side channel: the build
- # and the module are unchanged.
- defp dump_c!(dir, module, c_src) do
+ # generated C to /.c, and the compiler's report notes (the
+ # fast-path report before `report: :full` adds clang's) to
+ # /.report — the byte-identical gate for compiler refactors
+ # (dev/dump_c.exs). A pure side channel: the build and the module are
+ # unchanged.
+ defp dump_c!(dir, module, c_src, report) do
File.mkdir_p!(dir)
File.write!(Path.join(dir, "#{inspect(module)}.c"), c_src)
+
+ File.write!(
+ Path.join(dir, "#{inspect(module)}.report"),
+ Enum.map(report, &[inspect(&1, limit: :infinity, printable_limit: :infinity), "\n"])
+ )
end
# DEFK_COMPILE_TIME=: every module compiled while it is set appends
diff --git a/lib/defk/compiler.ex b/lib/defk/compiler.ex
index b5f3a66d..42dd683b 100644
--- a/lib/defk/compiler.ex
+++ b/lib/defk/compiler.ex
@@ -104,9 +104,15 @@ defmodule Defk.Compiler do
# adaptive_slice - a yielding kernel's NIF is `__defk_native___`
# with the run-queue hint as an extra last argument (default false);
# max_owned_bytes - the byte budget of a call's owned buffers (DEFK_MAX_OWNED
- # in priv/defk_runtime.h; default nil: half of physical memory)
+ # in priv/defk_runtime.h; default nil: half of physical memory);
+ # hints - a kept bounds check's note names the fact that would drop
+ # it (note_bounds/4: a proof search per kept check; default false,
+ # `report: :full` turns it on)
def compile(module, raw_kernels, file, opts \\ []) do
- Process.put(@ctx, Ctx.new(file, module, source_lines(file)))
+ Process.put(@ctx, %{
+ Ctx.new(file, module, source_lines(file))
+ | hints: Keyword.get(opts, :hints, false)
+ })
kernels =
raw_kernels
@@ -369,7 +375,8 @@ defmodule Defk.Compiler do
case Code.string_to_quoted(src) do
# the statements are lowered in the block of the statement env is at
{:ok, ast} ->
- probe(fn -> stmt_list(exprs(ast), Copies.live_after(exprs(ast)), env) end) != :error
+ probe(fn -> stmt_list(exprs(ast), Copies.block_liveness(exprs(ast)), env) end) !=
+ :error
_ ->
false
@@ -906,7 +913,7 @@ defmodule Defk.Compiler do
defp lower_block_env(ast, env) do
es = exprs(ast)
{init, [last]} = Enum.split(es, -1)
- lives = Copies.live_after(es)
+ lives = Copies.block_liveness(es)
{s1, env} = stmt_list(init, lives, nested(env, ast))
{s2, v, env} =
@@ -957,10 +964,11 @@ defmodule Defk.Compiler do
# A block evaluated only for effect, nested like lower_block's.
defp lower_stmts(ast, env) do
es = exprs(ast)
- stmt_list(es, Copies.live_after(es), nested(env, ast))
+ stmt_list(es, Copies.block_liveness(es), nested(env, ast))
end
- # statements `es` of one block; `lives` has the names read after each
+ # statements `es` of one block; `lives` has, for each, the names read after
+ # it and the names it reads (Copies.block_liveness/1)
defp stmt_list(es, lives, env) do
Enum.zip(AllocZeroing.mark(es), lives)
|> Enum.reduce({[], env}, fn {e, live}, {acc, env} ->
@@ -1349,12 +1357,13 @@ defmodule Defk.Compiler do
defp array_ids({:arr, _}), do: %{id: make_ref(), buf: make_ref(), fin: false, depth: 1}
defp array_ids(_t), do: %{}
- # Before statement `e` of a block, `live` the names read after it: copy
- # what its nested writes need (hoist/3), then note what is live.
- defp enter(e, live, env) do
+ # Before statement `e` of a block, `live` the names read after it and
+ # `reads` those it reads: copy what its nested writes need (hoist/4), then
+ # note what is live.
+ defp enter(e, {live, reads}, env) do
names = Copies.live_names(e, live)
- {sh, env} = hoist(e, names, env)
- {sh, %{env | scope: Copies.enter_stmt(env.scope, e, names, env.vars)}}
+ {sh, env} = hoist(e, names, reads, env)
+ {sh, %{env | scope: Copies.enter_stmt(env.scope, e, names, reads, env.vars)}}
end
# A variable bound outside the branches and loop bodies of `e` and written
@@ -1363,8 +1372,8 @@ defmodule Defk.Compiler do
# only binding of an owned buffer, so its writes are in place, and a loop
# copies once. An input array written in a branch is copied even when the
# branch is not taken.
- defp hoist(e, names, env) do
- seen = MapSet.union(names, Copies.reads(e))
+ defp hoist(e, names, reads, env) do
+ seen = MapSet.union(names, reads)
Enum.reduce(Copies.nested_writes(e), {[], env}, fn n, {acc, env} ->
case env.vars do
@@ -5099,7 +5108,7 @@ defmodule Defk.Compiler do
defp lower_tail(ast, env, leaf) do
es = exprs(ast)
{init, [last]} = Enum.split(es, -1)
- lives = Copies.live_after(es)
+ lives = Copies.block_liveness(es)
{s, env} = stmt_list(init, lives, nested(env, ast))
[
@@ -6287,9 +6296,10 @@ defmodule Defk.Compiler do
# a[i], or the word load u64_le(bytes, i), of width w: its bounds check
# dropped, or kept with the facts that would drop it. `proof` is the
# access's own (Facts.check/4), which names the half that fails, so what
- # is named is what the proof would accept. A failing half names the fact
- # it would follow from when that is small (Facts.residual/5): a value `a`
- # indexed by `i`, in `env`.
+ # is named is what the proof would accept. Under `report: :full` (ctx
+ # hints), a failing half also names the fact it would follow from when
+ # that is small (Facts.residual/5, a search per kept check, so not for
+ # every compile): a value `a` indexed by `i`, in `env`.
defp note_bounds(node, w, proof, {a, i, env}) do
{text, arr, idx} =
case node do
@@ -6304,10 +6314,14 @@ defmodule Defk.Compiler do
{:unproven, half} ->
{arr, idx} = {src_text(arr), src_text(idx)}
+ hints? = ctx(:hints)
+
named = fn h, what ->
- case Facts.residual(a, i, w, h, env.facts) |> hint_text(env) do
- nil -> what
- fact -> "#{what} (it would follow from #{fact})"
+ with true <- hints?,
+ fact when fact != nil <- Facts.residual(a, i, w, h, env.facts) |> hint_text(env) do
+ "#{what} (it would follow from #{fact})"
+ else
+ _ -> what
end
end
diff --git a/lib/defk/compiler/ctx.ex b/lib/defk/compiler/ctx.ex
index 3487a214..530967ca 100644
--- a/lib/defk/compiler/ctx.ex
+++ b/lib/defk/compiler/ctx.ex
@@ -15,6 +15,7 @@ defmodule Defk.Compiler.Ctx do
broken: MapSet.new(),
statics: %{},
report: [],
+ hints: false,
bits_names: %{},
lowering: nil
diff --git a/lib/defk/copies.ex b/lib/defk/copies.ex
index 8f157f06..8778d1fe 100644
--- a/lib/defk/copies.ex
+++ b/lib/defk/copies.ex
@@ -177,10 +177,17 @@ defmodule Defk.Copies do
def stmt_reads(e), do: reads(e)
# for each statement of a block, the names read after it in the block
- def live_after(es) do
+ def live_after(es), do: es |> block_liveness() |> Enum.map(&elem(&1, 0))
+
+ # for each statement of a block, {the names read after it in the block,
+ # the names it reads}: what entering it needs (enter_stmt/5), from one
+ # walk of it. stmt_reads/1 is reads/1 of the statement: read_counts/1
+ # skips a match's left side as stmt_reads/1 does.
+ def block_liveness(es) do
{lives, _} =
List.foldr(es, {[], MapSet.new()}, fn e, {acc, live} ->
- {[live | acc], live |> MapSet.difference(binds(e)) |> MapSet.union(stmt_reads(e))}
+ reads = stmt_reads(e)
+ {[{live, reads} | acc], live |> MapSet.difference(binds(e)) |> MapSet.union(reads)}
end)
lives
@@ -289,9 +296,12 @@ defmodule Defk.Copies do
# with `vars` the bindings after the copies hoisted before `e`: what is
# live after `e`, and for the blocks nested in it (which also see what `e`
# reads, as a later iteration or a later arm does).
- def enter_stmt(%Scope{} = s, e, names, vars) do
+ # `reads` is reads(e), when the caller has it (block_liveness/1).
+ def enter_stmt(%Scope{} = s, e, names, vars), do: enter_stmt(s, e, names, reads(e), vars)
+
+ def enter_stmt(%Scope{} = s, e, names, reads, vars) do
live = MapSet.union(pairs(vars, names), s.out)
- nest = MapSet.union(live, pairs(vars, reads(e)))
+ nest = MapSet.union(live, pairs(vars, reads))
%{s | stmt: e, live: live, live_names: names, nest: nest}
end
diff --git a/test/condition_facts_test.exs b/test/condition_facts_test.exs
index 236e2adc..a2fb6a79 100644
--- a/test/condition_facts_test.exs
+++ b/test/condition_facts_test.exs
@@ -15,7 +15,9 @@
# every dropped bounds and overflow check where the condition holds.
defmodule Defk.ConditionFactsTest.K do
- use Defk
+ # the report names the fact that would drop a kept check only under
+ # report: :full, and the tests below read those hints
+ use Defk, report: :full
# x[i - w] under i >= w, after a raise when w < 1: 0 <= i - w from the
# arm, i - w < length(x) from i <= length(x) - 1 and w >= 1
@@ -138,6 +140,22 @@ defmodule Defk.ConditionFactsTest.K do
end
end
+# the same unguarded kernel without report: :full: its note names no fact
+defmodule Defk.ConditionFactsTest.Plain do
+ use Defk
+
+ defk rolling_diff_unguarded(x :: [f64], w :: i64) :: [f64] do
+ out = alloc(f64, length(x))
+
+ for i <- 0..(length(x) - 1)//1 do
+ old = if i >= w, do: x[i - w], else: 0.0
+ out[i] = x[i] - old
+ end
+
+ out
+ end
+end
+
defmodule Defk.ConditionFactsTest do
use ExUnit.Case, async: true
use Defk.Properties
@@ -186,6 +204,11 @@ defmodule Defk.ConditionFactsTest do
"x[i - w]: bounds check kept; nothing proves i - w < length(x) (it would follow from w >= 0)"
]
+ # the hint is searched for only under report: :full
+ assert kept_bounds(Defk.ConditionFactsTest.Plain, :rolling_diff_unguarded) == [
+ "x[i - w]: bounds check kept; nothing proves i - w < length(x)"
+ ]
+
for w <- [@i64_min, -3, -1, 0, 1, 2, @i64_max] do
assert {_, _} = agrees(:rolling_diff_unguarded, [f64s([1, 2, 3]), w])
end
diff --git a/test/copies_test.exs b/test/copies_test.exs
index 1a32eb51..6e95fbca 100644
--- a/test/copies_test.exs
+++ b/test/copies_test.exs
@@ -407,4 +407,33 @@ defmodule Defk.CopiesTest do
assert Copies.arm_reads(twice, node) == nil
end
end
+
+ describe "block_liveness/1, each statement's reads walked once" do
+ property "a statement's reads are its stmt_reads, and the lives are live_after's" do
+ check all(es <- StreamData.list_of(stmt_gen(), min_length: 1, max_length: 5), max_runs: 300) do
+ for e <- es, do: assert(Copies.reads(e) == Copies.stmt_reads(e), Macro.to_string(e))
+
+ liveness = Copies.block_liveness(es)
+ assert Enum.map(liveness, &elem(&1, 0)) == Copies.live_after(es)
+ assert Enum.map(liveness, &elem(&1, 1)) == Enum.map(es, &Copies.reads/1)
+ end
+ end
+
+ test "an array write reads its target, a match does not read its left side" do
+ [{_, write}, {_, match}] =
+ Copies.block_liveness(
+ quote(
+ do:
+ (
+ a[i] = v
+ x = f(y)
+ )
+ )
+ |> elem(2)
+ )
+
+ assert write == MapSet.new([:a, :i, :v])
+ assert match == MapSet.new([:y])
+ end
+ end
end
diff --git a/test/product_facts_test.exs b/test/product_facts_test.exs
index d2800393..cb22b504 100644
--- a/test/product_facts_test.exs
+++ b/test/product_facts_test.exs
@@ -16,7 +16,9 @@
# arithmetic.
defmodule Defk.ProductFactsTest.K do
- use Defk
+ # the report names the fact that would drop a kept check only under
+ # report: :full, and a test below reads one
+ use Defk, report: :full
# the rows of m (dim = length(q) values each) scored against q: r * dim + j
# is below n * dim <= length(m), from n = div(length(m), dim)