Skip to content

Commit 7a10b74

Browse files
committed
feat(ext-core): is_subtype?/is_supertype? now use Graph.get_path_by/2; fix graph building
1 parent 8dd33ab commit 7a10b74

2 files changed

Lines changed: 190 additions & 66 deletions

File tree

lib/apix.schema.extensions/core/type_graph.ex

Lines changed: 97 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ defmodule Apix.Schema.Extensions.Core.TypeGraph do
2222

2323
@doc """
2424
Runs all the `#{inspect __MODULE__}` functions that are intended to be run
25-
after compilation if compilation is finished to avoid corrupting `#{inspect __MODULE__}` state.
25+
after compilation is finished to avoid corrupting `#{inspect __MODULE__}` state.
2626
2727
- `prune!/0`
2828
- `validate!/0`
@@ -157,14 +157,23 @@ defmodule Apix.Schema.Extensions.Core.TypeGraph do
157157
- Known and unknown types are not subtypes.
158158
- `t:Ast.t/0` and `t:Context.t/0` referencing same schema are subtypes.
159159
"""
160-
def is_subtype?(subtype, supertype) when (is_struct(subtype, Context) or is_struct(subtype, Ast)) and (is_struct(supertype, Context) or is_struct(supertype, Ast)) do
161-
subtype = Apix.Schema.get_schema(subtype) || subtype
162-
supertype = Apix.Schema.get_schema(supertype) || supertype
163-
164-
subtype_vertex = Apix.Schema.hash(subtype)
165-
supertype_vertex = Apix.Schema.hash(supertype)
160+
def is_subtype?(_subtype, %Context{module: Any, schema: :t, params: []}), do: true
161+
def is_subtype?(_subtype, %Ast{module: Any, schema: :t, args: []}), do: true
166162

167-
!!Graph.edge({subtype_vertex, supertype_vertex, :subtype})
163+
def is_subtype?(subtype, supertype) when (is_struct(subtype, Context) or is_struct(subtype, Ast)) and (is_struct(supertype, Context) or is_struct(supertype, Ast)) do
164+
subtype =
165+
subtype
166+
|> Apix.Schema.get_schema()
167+
|> Kernel.||(subtype)
168+
|> Apix.Schema.hash()
169+
170+
supertype =
171+
supertype
172+
|> Apix.Schema.get_schema()
173+
|> Kernel.||(supertype)
174+
|> Apix.Schema.hash()
175+
176+
!!Graph.get_path_by(supertype, subtype, &(&1 == :subtype))
168177
end
169178

170179
@doc """
@@ -174,57 +183,71 @@ defmodule Apix.Schema.Extensions.Core.TypeGraph do
174183
- Known and unknown types are not supertypes.
175184
- `t:Ast.t/0` and `t:Context.t/0` referencing same schema are supertypes.
176185
"""
177-
def is_supertype?(supertype, subtype) when (is_struct(supertype, Context) or is_struct(supertype, Ast)) and (is_struct(subtype, Context) or is_struct(subtype, Ast)) do
178-
supertype = Apix.Schema.get_schema(supertype) || supertype
179-
subtype = Apix.Schema.get_schema(subtype) || subtype
180-
181-
supertype_vertex = Apix.Schema.hash(supertype)
182-
subtype_vertex = Apix.Schema.hash(subtype)
186+
def is_supertype?(%Context{module: Any, schema: :t, params: []}, _subtype), do: true
187+
def is_supertype?(%Ast{module: Any, schema: :t, args: []}, _subtype), do: true
183188

184-
!!Graph.edge({supertype_vertex, subtype_vertex, :supertype})
189+
def is_supertype?(supertype, subtype) when (is_struct(supertype, Context) or is_struct(supertype, Ast)) and (is_struct(subtype, Context) or is_struct(subtype, Ast)) do
190+
supertype =
191+
supertype
192+
|> Apix.Schema.get_schema()
193+
|> Kernel.||(supertype)
194+
|> Apix.Schema.hash()
195+
196+
subtype =
197+
subtype
198+
|> Apix.Schema.get_schema()
199+
|> Kernel.||(subtype)
200+
|> Apix.Schema.hash()
201+
202+
!!Graph.get_path_by(subtype, supertype, &(&1 == :supertype))
185203
end
186204

187-
defp build_type_relations!(%Context{} = context) do
188-
context.ast
189-
|> normalize()
190-
|> Ast.prewalk([{context, context}], fn
191-
%Ast{module: m, schema: :t, args: [_, _]} = ast, [{parent_ast, parent_ast} | _rest] = acc when m in [And, Or] ->
192-
{
193-
ast,
194-
[{m, ast.args} | acc]
195-
}
196-
197-
%Ast{module: m, schema: :t, args: [_, _]} = ast, [{m, args} | _rest] = acc when m in [And, Or] ->
198-
{
199-
ast,
200-
[{m, ast.args ++ args} | acc]
201-
}
205+
def build_type_relations!(context_or_ast) do
206+
context_or_ast
207+
|> case do
208+
%Context{} = context ->
209+
context.ast
202210

203-
%Ast{} = ast, [{parent_ast, parent_ast} | _rest] = acc ->
211+
%Ast{} = ast ->
212+
ast
213+
end
214+
|> normalize()
215+
|> Ast.prewalk({context_or_ast, []}, fn
216+
ast, {%Ast{module: And, schema: :t, args: [_, _]} = last, relations} ->
204217
{
205218
ast,
206-
[{ast, parent_ast} | acc]
219+
{last, [{ast, last} | relations]}
207220
}
208221

209-
ast, [{Or, args}, {parent_ast, parent_ast} | rest] ->
210-
{
211-
ast,
212-
Enum.map(args, &{parent_ast, &1}) ++ [{parent_ast, parent_ast} | rest]
213-
}
222+
ast, {%Ast{module: Or, schema: :t, args: [_, _]} = last, relations} ->
223+
{ast, {last, [{last, ast} | relations]}}
214224

215-
ast, [{And, args}, {parent_ast, parent_ast} | rest] ->
216-
{
217-
ast,
218-
Enum.map(args, &{&1, parent_ast}) ++ [{parent_ast, parent_ast} | rest]
219-
}
225+
ast, {last, []} ->
226+
{ast, {ast, [{last, ast}]}}
220227

221228
ast, acc ->
222229
{ast, acc}
223230
end)
224231
|> elem(1)
232+
|> elem(1)
225233
|> Enum.each(fn {sup, sub} ->
226-
sup = Apix.Schema.get_schema(sup) || sup
227-
sub = Apix.Schema.get_schema(sub) || sub
234+
sup =
235+
case sup do
236+
%{module: m} when m in [And, Or] ->
237+
sup
238+
239+
_ ->
240+
Apix.Schema.get_schema(sup) || sup
241+
end
242+
243+
sub =
244+
case sub do
245+
%{module: m} when m in [And, Or] ->
246+
sub
247+
248+
_ ->
249+
Apix.Schema.get_schema(sub) || sub
250+
end
228251

229252
sup_vertex = Apix.Schema.hash(sup)
230253
sub_vertex = Apix.Schema.hash(sub)
@@ -250,31 +273,38 @@ defmodule Apix.Schema.Extensions.Core.TypeGraph do
250273
defp normalize_not(%Ast{module: Not, schema: :t, args: [%Ast{module: None, schema: :t, args: []} = ast]}), do: struct(ast, module: Any, schema: :t, args: [])
251274

252275
defp normalize_not(%Ast{module: Not, schema: :t, args: [%Ast{module: And, schema: :t, args: args}]} = ast) do
253-
args
254-
|> Enum.sort_by(&Apix.Schema.msa/1)
255-
|> Enum.map(&struct(ast, module: Not, schema: :t, args: [&1]))
276+
args =
277+
args
278+
|> Enum.sort_by(&Apix.Schema.msa/1)
279+
|> Enum.map(&struct(ast, module: Not, schema: :t, args: [&1]))
256280

257281
struct(ast, module: Or, schema: :t, args: args)
258282
end
259283

260-
defp normalize_not(%Ast{module: Not, schema: :t, args: [%Ast{module: Or, schema: :t, args: args}]} = ast) do
261-
args
262-
|> Enum.sort_by(&Apix.Schema.msa/1)
263-
|> Enum.map(&struct(ast, module: Not, schema: :t, args: [&1]))
264-
265-
struct(ast, module: And, schema: :t, args: args)
266-
end
267-
268284
defp normalize_not(%Ast{module: Not, schema: :t, args: [arg]} = ast) do
269-
reject = [
270-
Apix.Schema.msa(arg),
271-
{And, :t, 2},
272-
{Or, :t, 2},
273-
{Not, :t, 1},
274-
{Const, :t, 1},
275-
{Any, :t, 0},
276-
{None, :t, 0}
277-
]
285+
reject =
286+
arg
287+
|> Ast.postwalk([], fn
288+
%Ast{module: Or, schema: :t, args: args} = ast, acc ->
289+
msa =
290+
args
291+
|> Enum.map(&Apix.Schema.msa/1)
292+
|> Enum.reject(&(&1 == {Or, :t, 2}))
293+
294+
{ast, msa ++ acc}
295+
296+
ast, acc ->
297+
{ast, acc}
298+
end)
299+
|> elem(1)
300+
|> Kernel.++([
301+
{And, :t, 2},
302+
{Or, :t, 2},
303+
{Not, :t, 1},
304+
{Const, :t, 1},
305+
{Any, :t, 0},
306+
{None, :t, 0}
307+
])
278308

279309
Graph.vertices()
280310
|> Enum.map(fn hash ->
@@ -284,6 +314,7 @@ defmodule Apix.Schema.Extensions.Core.TypeGraph do
284314
end)
285315
|> Enum.reject(&(Apix.Schema.msa(&1) in reject))
286316
|> Enum.sort_by(&Apix.Schema.msa/1)
317+
|> Enum.uniq()
287318
|> case do
288319
[] ->
289320
struct(ast, module: None, schema: :t, args: [])
@@ -292,8 +323,8 @@ defmodule Apix.Schema.Extensions.Core.TypeGraph do
292323
context.ast
293324

294325
[first | rest] ->
295-
Enum.reduce(rest, first.ast, fn current, acc ->
296-
struct(ast, module: And, schema: :t, args: [current.ast, acc])
326+
Enum.reduce(rest, %Ast{module: first.module, schema: first.schema, args: Enum.map(first.params, fn _ -> false end)}, fn current, acc ->
327+
struct(ast, module: Or, schema: :t, args: [%Ast{module: current.module, schema: current.schema, args: Enum.map(current.params, fn _ -> false end)}, acc])
297328
end)
298329
end
299330
end

lib/apix.schema.extensions/core/type_graph/graph.ex

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,30 @@ defmodule Apix.Schema.Extensions.Core.TypeGraph.Graph do
9595
end
9696
end
9797

98+
@doc """
99+
Like `:digraph.get_path/3`, but only traverses edges for which `predicate` returns `true`.
100+
101+
`predicate` can have arity 1, 3, or 4:
102+
- `fn label -> boolean end`
103+
- `fn from, to` -> boolean end`
104+
- `fn from, to, label -> boolean end`
105+
- `fn edge, from, to, label -> boolean end`
106+
107+
Returns `false` if no path exists, or a list of vertices `[v1, ..., vn]` otherwise.
108+
"""
109+
@spec get_path_by(
110+
:digraph.vertex(),
111+
:digraph.vertex(),
112+
(:digraph.label() -> boolean())
113+
| (:digraph.vertex(), :digraph.vertex() -> boolean())
114+
| (:digraph.vertex(), :digraph.vertex(), :digraph.label() -> boolean())
115+
| (:digraph.edge(), :digraph.vertex(), :digraph.vertex(), :digraph.label() -> boolean())
116+
) :: false | [:digraph.edge()]
117+
def get_path_by(source, target, predicate) when is_function(predicate, 1) or is_function(predicate, 2) or is_function(predicate, 3) or is_function(predicate, 4) do
118+
ensure!()
119+
GenServer.call(__MODULE__, {:get_path_by, source, target, predicate})
120+
end
121+
98122
# GenServer
99123

100124
@impl GenServer
@@ -150,6 +174,11 @@ defmodule Apix.Schema.Extensions.Core.TypeGraph.Graph do
150174
{:reply, reply, state}
151175
end
152176

177+
def handle_call({:get_path_by, source, target, predicate}, _from, state) do
178+
path = do_get_path_by(state.digraph, source, target, predicate)
179+
{:reply, path, state}
180+
end
181+
153182
@impl GenServer
154183
def handle_info({:EXIT, _from, _reason}, state) do
155184
{:ok, state} = do_dump(state)
@@ -213,4 +242,68 @@ defmodule Apix.Schema.Extensions.Core.TypeGraph.Graph do
213242

214243
{:ok, %__MODULE__{digraph: digraph}}
215244
end
245+
246+
defp do_get_path_by(_g, v, v, _predicate), do: [v]
247+
248+
defp do_get_path_by(g, source, target, predicate) do
249+
# Ensure both vertices exist (match :digraph.get_path/3 behavior)
250+
with {^source, _} <- :digraph.vertex(g, source) || :error,
251+
{^target, _} <- :digraph.vertex(g, target) || :error do
252+
bfs_with_edge_filter(g, source, target, predicate)
253+
else
254+
_ -> false
255+
end
256+
end
257+
258+
defp bfs_with_edge_filter(g, source, target, predicate) do
259+
# Standard BFS over vertices, but we only traverse out-edges passing `predicate`
260+
queue = :queue.from_list([source])
261+
visited = MapSet.new([source])
262+
prev = %{source => nil}
263+
264+
do_bfs(g, target, predicate, queue, visited, prev)
265+
end
266+
267+
defp do_bfs(g, target, predicate, queue, visited, prev) do
268+
case :queue.out(queue) do
269+
{:empty, _} ->
270+
false
271+
272+
{{:value, v}, queue1} ->
273+
if v == target do
274+
reconstruct_path(prev, v)
275+
else
276+
{queue2, visited2, prev2} = expand_neighbours(g, v, predicate, queue1, visited, prev)
277+
do_bfs(g, target, predicate, queue2, visited2, prev2)
278+
end
279+
end
280+
end
281+
282+
defp expand_neighbours(g, v, predicate, queue, visited, prev) do
283+
# Filter out-edges by predicate, then enqueue unseen `to` vertices
284+
Enum.reduce(:digraph.out_edges(g, v), {queue, visited, prev}, fn e, {q, vis, pr} ->
285+
{^e, from, to, label} = :digraph.edge(g, e)
286+
287+
if edge_passes?(predicate, e, from, to, label) and not MapSet.member?(vis, to) do
288+
{:queue.in(to, q), MapSet.put(vis, to), Map.put(pr, to, from)}
289+
else
290+
{q, vis, pr}
291+
end
292+
end)
293+
end
294+
295+
defp edge_passes?(predicate, _e, _from, _to, label) when is_function(predicate, 1), do: predicate.(label)
296+
defp edge_passes?(predicate, _e, from, to, _label) when is_function(predicate, 2), do: predicate.(from, to)
297+
defp edge_passes?(predicate, _e, from, to, label) when is_function(predicate, 3), do: predicate.(from, to, label)
298+
defp edge_passes?(predicate, e, from, to, label) when is_function(predicate, 4), do: predicate.(e, from, to, label)
299+
300+
defp reconstruct_path(prev, v), do: do_reconstruct(prev, v, []) |> Enum.reverse()
301+
302+
defp do_reconstruct(prev, v, acc) do
303+
case Map.fetch(prev, v) do
304+
{:ok, nil} -> [v | acc]
305+
{:ok, p} -> do_reconstruct(prev, p, [v | acc])
306+
:error -> false
307+
end
308+
end
216309
end

0 commit comments

Comments
 (0)