lib/sparql/algebra/translation.ex

defmodule SPARQL.Algebra.Translation do
  @moduledoc """
  Translation of ASTs of the `SPARQL.Language.Decoder` into a `SPARQL.Algebra.Expression`.

  <https://www.w3.org/TR/sparql11-query/#sparqlQuery>
  """

  alias RDF.{IRI, XSD}

  defmodule GroupGraphPattern do
    @enforce_keys [:expr]
    defstruct fs: [], expr: nil
  end

  defmodule OptionalGraphPattern do
    @enforce_keys [:expr]
    defstruct fs: [], expr: nil
  end

  @no_mapping nil
  @zero_bgp SPARQL.Algebra.BGP.zero()


  def translate(ast, prefixes, base, options \\ %{}) do
    with state = %{expr: ast},
         {:ok, state} <-
           # 18.2.2 Converting Graph Patterns
           convert_graph_patterns(state, prefixes, base, options),

#         {:ok, state} <-
#           # 18.2.4.1 Grouping and Aggregation - Step: GROUP BY
#           group(...) # https://www.w3.org/TR/sparql11-query/#defn_algGroup

         {:ok, state} <-
           # 18.2.4.1 Grouping and Aggregation - Step: Aggregates
           convert_aggregates(state),

         {:ok, state} <-
           # 18.2.4.2 HAVING
           convert_having(state),

         {:ok, state} <-
           # 18.2.4.3 VALUES
           convert_final_values_clause(state),

         {:ok, state} <-
           # 18.2.4.4 SELECT Expressions
           convert_select(state),

         {:ok, state} <-
           # 18.2.5 Converting Solution Modifiers
           convert_solution_modifiers(state)
    do
      {:ok, state.expr}
    end
  end


  ############################################################################
  # 18.2.2 Converting Graph Patterns

  @doc !"""
  Translates a SPARQL graph pattern into a `SPARQL.Algebra.Expression`

  This process is applied to the group graph pattern (the unit between `{...}` delimiters)
  forming the `WHERE` clause of a query, and recursively to each syntactic element
  within the group graph pattern.

  The result of the translation is a `SPARQL.Algebra.Expression`.

  <https://www.w3.org/TR/sparql11-query/#convertGraphPattern>
  """
  defp convert_graph_patterns(state, prefixes, base, _options) do
    with {:ok, ast} <-
           # 18.2.2.1 Expand Syntax Forms
           expand_syntax_forms(state.expr, prefixes, base),

         {:ok, ast} <-
           # 18.2.2.2 Collect FILTER Elements
           collect_filter_elements(ast),

         {:ok, ast} <-
           # 18.2.2.3 Translate Property Path Expressions
           translate_property_path_expressions(ast),

         {:ok, ast} <-
           # 18.2.2.4 Translate Property Path Patterns
           translate_property_path_patterns(ast),

         {:ok, ast} <-
           # 18.2.2.5 Translate Basic Graph Patterns
           translate_basic_graph_patterns(ast),

         {:ok, ast} <-
           # 18.2.2.6 Translate Graph Patterns
           translate_graph_patterns(ast),

         {:ok, ast} <-
           # 18.2.2.7 Filters of Group
           add_filters_of_group(ast),

         {:ok, ast} <-
           # 18.2.2.8 Simplification step
           simplification(ast)
    do
      {:ok, %{state | expr: ast}}
    end
  end


  @doc !"""
  Expand Syntax Forms

  Expand abbreviations for IRIs and triple patterns.

  We also do various other basic AST transformations here.

  <https://www.w3.org/TR/sparql11-query/#sparqlExpandForms>
  """
  defp expand_syntax_forms(ast, prefixes, base) do
    {:ok, map(ast, %{prefixes: prefixes, base: base}, &expand_syntax_form/2)}
  end

  defp expand_syntax_form({:prefix_ln, line, {ns, locale}}, %{prefixes: prefixes, base: base}) do
    case prefixes[ns] do
      nil ->
        raise "unknown prefix in '#{ns <> ":" <> locale}' on line #{line}"
      prefix ->
        IRI.absolute(prefix.value <> locale, base)
    end
  end

  defp expand_syntax_form({:prefix_ns, line, ns}, prologue) do
    expand_syntax_form({:prefix_ln, line, {ns, ""}}, prologue)
  end

  defp expand_syntax_form({:relative_iri, relative_iri}, %{base: base}) do
    IRI.absolute(relative_iri, base)
  end

  # Typed literals with a prefixed name datatype aren't expanded during parsing, so we do this here
  defp expand_syntax_form({{:string_literal_quote, _, _} = literal_ast,
                           {:datatype, {_, _, _} = prefixed_name_ast}}, prologue) do
    RDF.Serialization.ParseHelper.to_literal(literal_ast,
      {:datatype, expand_syntax_form(prefixed_name_ast, prologue)})
  end

  defp expand_syntax_form({:var, _, var}, _), do: var
  defp expand_syntax_form({:*, _}, _),        do: "*"
  defp expand_syntax_form({:nil, _}, _),      do: RDF.nil()

  defp expand_syntax_form({:builtin_function_call, function_name, args}, prologue) do
    %SPARQL.Algebra.FunctionCall.Builtin{
      name: function_name,
      arguments: args |> map(prologue, &expand_syntax_form/2)
    }
  end

  defp expand_syntax_form({:function_call, function_name, args}, prologue) do
    {distinct, args} =
      case args do
        [{:distinct, _}, rest] -> {true, rest}
        _                      -> {false, args}
      end

    %SPARQL.Algebra.FunctionCall.Extension{
      name: function_name |> expand_syntax_form(prologue),
      arguments: args |> map(prologue, &expand_syntax_form/2),
      distinct: distinct
    }
  end

  # TODO: optimize performance by providing function clauses for AST patterns which don't need further traversal

  defp expand_syntax_form(_, _), do: @no_mapping


  @doc !"""
  Collect FILTER Elements

  `FILTER` expressions apply to the whole group graph pattern in which they appear.
  The algebra operators to perform filtering are added to the group after translation
  of each group element. We collect the filters together here and remove them from group,
  then apply them to the whole translated group graph pattern.

  In this step, we also translate graph patterns within `FILTER` expressions `EXISTS` and `NOT EXISTS`.

      Let FS := empty set

      For each form FILTER(expr) in the group graph pattern:
          In expr, replace NOT EXISTS{P} with fn:not(exists(translate(P)))
          In expr, replace EXISTS{P} with exists(translate(P))
          FS := FS ∪ {expr}
          End

  The set of filter expressions `FS` is used later.

  <https://www.w3.org/TR/sparql11-query/#sparqlCollectFilters>
  """
  defp collect_filter_elements(ast) do
    {:ok, map(ast, &do_collect_filter_elements/2)}
  end

  defp do_collect_filter_elements({:optional, group_graph_pattern}, state) do
    %OptionalGraphPattern{expr: do_collect_filter_elements(group_graph_pattern, state)}
  end

  defp do_collect_filter_elements({:group_graph_pattern, graph_patterns}, _)
      when is_list(graph_patterns) do
    {filters, group} =
      Enum.split_with(graph_patterns, fn
        {:filter, _} -> true
        _            -> false
      end)

    # TODO: Do we have to call do_collect_filter_elements recursively on the group? Can it recursively contain other group_graph_patterns, eg. in subqueries
    fs = Enum.map(filters, fn {_, expr} -> translate_exists_patterns(expr) end)

    %GroupGraphPattern{fs: fs, expr: map(group, &do_collect_filter_elements/2)}
  end

  # TODO: optimize performance by providing function clauses for AST patterns which don't need further traversal

  defp do_collect_filter_elements(_, _), do: @no_mapping

  def translate_exists_patterns(expr) do
  # TODO: In expr, replace NOT EXISTS{P} with fn:not(exists(translate(P)))
  # TODO: In expr, replace EXISTS{P} with exists(translate(P))
    expr
  end


  @doc !"""
  Translate Property Path Expressions

  The following table gives the translation of property paths expressions from
  SPARQL syntax to terms in the SPARQL algebra. This applies to all elements of
  a property path expression recursively.

  The next step after this one - - translates certain forms to triple patterns, and
  these are converted later to basic graph patterns by adjacency (without intervening
  group pattern delimiters { and }) or other syntax forms. Overall, SPARQL syntax
  property paths of just an IRI become triple patterns and these are aggregated
  into basic graph patterns.

  Notes: The order of forms IRI and ^IRI in negated property sets is not relevant.

  We introduce the following symbols:

  - `link`
  - `inv`
  - `alt`
  - `seq`
  - `ZeroOrMorePath`
  - `OneOrMorePath`
  - `ZeroOrOnePath`
  - `NPS (for NegatedPropertySet)`


  |! Syntax Form (path)                      |!	Algebra (path)                         |
  |:======================================== |:========================================|
  | `iri`	                                   | `link(iri)`                             |
  | `^path`	                                 | `inv(path)`                             |
  | `!(:iri1|...|:irin)`	                   | `NPS({:iri1 ... :irin})`                |
  | `!(^:iri1|...|^:irin)`                   | `inv(NPS({:iri1 ... :irin}))`           |
  | `!(:iri1|...|:irii|^:irii+1|...|^:irim)` | `alt(NPS({:iri1 ...:irii}),`            |
  |                                          | `    inv(NPS({:irii+1, ..., :irim})) )` |
  | `path1 / path2`                          |	`seq(path1, path2)`                    |
  | `path1 | path2`                          |	`alt(path1, path2)`                    |
  | `path*`                                  | `ZeroOrMorePath(path)`                  |
  | `path+`                                  | `OneOrMorePath(path)`                   |
  | `path?`                                  | `ZeroOrOnePath(path)`                   |

  <https://www.w3.org/TR/sparql11-query/#sparqlTranslatePathExpressions>
  """
  defp translate_property_path_expressions(ast) do
    {:ok, ast}
  end


  @doc !"""
  Translate Property Path Patterns

  The previous step translated property path expressions. This step translates
  property path patterns, which are a subject end point, property path expression
  and object end point, into triple patterns or wraps in a general algebra operation
  for path evaluation.

  Notes:

  - `X` and `Y` are `RDF.Term`s or `SPARQL.Query.Variable`s.
  - `?V` is a fresh `SPARQL.Query.Variable`.
  - `P` and `Q` are path expressions.
  - These are only applied to property path patterns, not within
    property path expressions.
  - Translations earlier in the table are applied in preference to the last translation.
  - The final translation simply wraps any remaining property path expression to
    use a common form `Path(...)`.

  |! Algebra (path)	|! Translation      |
  |:=============== |:================= |
  | `X link(iri) Y`	| `X iri Y`         |
  | `X inv(iri) Y`	| `Y iri X`         |
  | `X seq(P, Q) Y` |	`X P ?V . ?V Q P` |
  | `X P Y`	        | `Path(X, P, Y)`   |

  <https://www.w3.org/TR/sparql11-query/#sparqlTranslatePathPatterns>
  """
  defp translate_property_path_patterns(ast) do
    {:ok, ast}
  end


  @doc !"""
  Translate Basic Graph Patterns

  After translating property paths, any adjacent triple patterns are collected
  together to form a basic graph pattern `BGP(triples)`.

  <https://www.w3.org/TR/sparql11-query/#sparqlTranslateBasicGraphPatterns>
  """
  defp translate_basic_graph_patterns(ast) do
    {:ok, map(ast, &do_translate_basic_graph_patterns/2)}
  end

  defp do_translate_basic_graph_patterns(%GroupGraphPattern{expr: []} = group_pattern, _) do
    %GroupGraphPattern{group_pattern | expr: [@zero_bgp]}
  end

  defp do_translate_basic_graph_patterns({:triples_block, triples_block}, _) do
    %SPARQL.Algebra.BGP{triples: translate_triples_block(triples_block)}
  end

   # TODO: optimize performance by providing function clauses for AST patterns which don't need further traversal

  defp do_translate_basic_graph_patterns(_, _), do: @no_mapping


  def translate_triples_block(triples_block) do
    Enum.flat_map triples_block, fn same_subject_triples ->
      with [{:subject, subject} | property_list] = same_subject_triples,
           {subject, triples}                    = node_with_triples(subject)
      do
        triples ++ property_list_to_triples(subject, property_list)
      end
    end
  end

  defp node_with_triples({:blank_node_property_list, property_list}) do
    with bnode = RDF.bnode() do
      {bnode, property_list_to_triples(bnode, property_list)}
    end
  end

  defp node_with_triples({:collection, list}) do
    list_to_triples(list, RDF.bnode())
  end

  defp node_with_triples(node) do
    {node, []}
  end

  defp property_list_to_triples(subject, property_list) do
    predications = chunk_property_list(property_list)
    Enum.flat_map predications, fn predication ->
      [predicate | objects] = predication
      Enum.flat_map objects, fn object ->
        {object, triples} = node_with_triples(object)
        [{subject, predicate, object} | triples]
      end
    end
  end

  defp chunk_property_list(property_list, acc \\ [])

  defp chunk_property_list([], acc) do
    acc
    |> Stream.map(&Enum.reverse/1)
    |> Enum.reverse()
  end

  defp chunk_property_list([{:predicate, predicate} | rest], acc) do
    chunk_property_list(rest, [[predicate] | acc])
  end

  defp chunk_property_list([{:object, object} | rest], [current | done]) do
    chunk_property_list(rest, [[object | current] | done])
  end


  defp list_to_triples(list, head, triples \\ [])
  defp list_to_triples([], _, triples), do: {RDF.nil, triples}
  defp list_to_triples([first | rest], head, triples) do
    with {node, node_triples} = node_with_triples(first),
         {next, triples} = list_to_triples(rest, RDF.bnode(), triples) do
      {
        head,
        [{head, RDF.first, node}] ++ node_triples ++
        [{head, RDF.rest,  next}] ++ triples
      }
    end
  end


  @doc !"""
  Translate Graph Patterns

  Next, we translate each remaining graph pattern form, recursively applying the
  translation process.

  If the form is GroupOrUnionGraphPattern

      Let A := undefined

      For each element G in the GroupOrUnionGraphPattern
          If A is undefined
              A := Translate(G)
          Else
              A := Union(A, Translate(G))
          End

      The result is A

  If the form is GraphGraphPattern

      If the form is GRAPH IRI GroupGraphPattern
          The result is Graph(IRI, Translate(GroupGraphPattern))
      If the form is GRAPH Var GroupGraphPattern
          The result is Graph(Var, Translate(GroupGraphPattern))

  If the form is GroupGraphPattern:

      Let FS := the empty set
      Let G := the empty pattern, a basic graph pattern which is the empty set.

      For each element E in the GroupGraphPattern

          If E is of the form OPTIONAL{P}
              Let A := Translate(P)
              If A is of the form Filter(F, A2)
                  G := LeftJoin(G, A2, F)
              Else
                  G := LeftJoin(G, A, true)
                  End
              End

          If E is of the form MINUS{P}
              G := Minus(G, Translate(P))
              End

          If E is of the form BIND(expr AS var)
              G := Extend(G, var, expr)
              End

          If E is any other form
              Let A := Translate(E)
              G := Join(G, A)
              End

         End

      The result is G.

  If the form is InlineData

      The result is a multiset of solution mappings 'data'.
      data is formed by forming a solution mapping from the variable in the
      corresponding position in list of variables (or single variable), omitting
      a binding if the BindingValue is the word UNDEF.

  If the form is SubSelect

      The result is ToMultiset(Translate(SubSelect))


  <https://www.w3.org/TR/sparql11-query/#sparqlTranslateGraphPatterns>
  """
  defp translate_graph_patterns(ast) do
    {:ok, map(ast, &translate_graph_pattern/2)}
  end

  # TODO: remove this when the implementation is complete; we currently need this to make the W3C syntax tests pass on non-select queries
  defp translate_graph_pattern({:group_graph_pattern, :"$undefined"}, _), do: @zero_bgp
  defp translate_graph_pattern(:"$undefined", _), do: @zero_bgp

  defp translate_graph_pattern(%GroupGraphPattern{expr: patterns} = group_pattern, state) do
    %GroupGraphPattern{group_pattern | expr:
      Enum.reduce(patterns, @zero_bgp, fn
        %OptionalGraphPattern{} = optional_graph_pattern, g ->
          %SPARQL.Algebra.LeftJoin{
            translate_graph_pattern(optional_graph_pattern, state) | expr1: g}

        %SPARQL.Algebra.BGP{} = e, g ->
          %SPARQL.Algebra.Join{expr1: g, expr2: e}

        %GroupGraphPattern{} = e, g ->
          %SPARQL.Algebra.Join{expr1: g, expr2: translate_graph_pattern(e, state)}

        {:bind, expr, var}, g ->
          %SPARQL.Algebra.Extend{child_expr: g, var: var, expr: expr}

        {:union, _, _} = union, g ->
          %SPARQL.Algebra.Join{expr1: g,
            expr2: translate_union_graph_pattern(union, nil, state)}

        {:minus, group_graph_pattern_expr}, g ->
          %SPARQL.Algebra.Minus{expr1: g,
            expr2: translate_graph_pattern(group_graph_pattern_expr, state)}


        # TODO: Handle subSelect
        {:group_graph_pattern, :"$undefined"}, _ -> @zero_bgp

        # TODO: remove this when the implementation is complete; we currently need this to make the W3C syntax tests pass on non-select queries
        :"$undefined", _ -> @zero_bgp
      end)
    }
  end

  defp translate_graph_pattern(%OptionalGraphPattern{expr: expr}, state) do
    case translate_graph_pattern(expr, state) do
      %GroupGraphPattern{expr: expr, fs: []} ->
        %SPARQL.Algebra.LeftJoin{expr2: expr, filters: [XSD.true]}

      %GroupGraphPattern{expr: expr, fs: fs} ->
        %SPARQL.Algebra.LeftJoin{expr2: expr, filters: fs}

      # TODO: Handle subSelect

      # TODO: remove this when the implementation is complete; we currently need this to make the W3C syntax tests pass on non-select queries
      nil ->
       %SPARQL.Algebra.LeftJoin{expr2: @zero_bgp, filters: [XSD.true]}
    end
  end

  # TODO: optimize performance by providing function clauses for AST patterns which don't need further traversal

  defp translate_graph_pattern(_, _), do: @no_mapping


  defp translate_union_graph_pattern({:union, left, right}, nil, state) do
    translate_union_graph_pattern(right,
      translate_graph_pattern(left, state), state)
  end

  defp translate_union_graph_pattern({:union, left, right}, a, state) do
    translate_union_graph_pattern(right,
      %SPARQL.Algebra.Union{
        expr1: a,
        expr2: translate_graph_pattern(left, state)
      }, state)
  end

  defp translate_union_graph_pattern(group_graph_pattern, a, state) do
    %SPARQL.Algebra.Union{
      expr1: a,
      expr2: translate_graph_pattern(group_graph_pattern, state)
    }
  end


  @doc !"""
  Add filters of Group

  After the group has been translated, the filter expressions are added so they
  will apply to the whole of the rest of the group:

      If FS is not empty
          Let G := output of preceding step
          Let X := Conjunction of expressions in FS
          G := Filter(X, G)
          End

  <https://www.w3.org/TR/sparql11-query/#sparqlAddFilters>
  """
  defp add_filters_of_group(ast) do
    {:ok, map(ast, &do_add_filters_of_group/2)}
  end

  defp do_add_filters_of_group(%GroupGraphPattern{fs: [], expr: g}, _) do
    map(g, &do_add_filters_of_group/2)
  end

  defp do_add_filters_of_group(%GroupGraphPattern{fs: fs, expr: g}, _) do
    %SPARQL.Algebra.Filter{filters: fs, expr: map(g, &do_add_filters_of_group/2)}
  end

  # TODO: optimize performance by providing function clauses for AST patterns which don't need further traversal

  defp do_add_filters_of_group(_, _), do: @no_mapping


  @doc !"""
  Simplification step

  Some groups of one graph pattern become `join(Z, A)`, where `Z` is the empty
  basic graph pattern (which is the empty set). These can be replaced by `A`.
  The empty graph pattern `Z` is the identity for join:

      Replace join(Z, A) by A
      Replace join(A, Z) by A

  <https://www.w3.org/TR/sparql11-query/#sparqlSimplification>
  """
  defp simplification(ast) do
    simplified = map(ast, &do_simplification/2)
    if ast == simplified do
      {:ok, simplified}
    else
      simplification(simplified)
    end
  end

  defp do_simplification(%SPARQL.Algebra.Join{expr1: @zero_bgp, expr2: a}, _), do: a
  defp do_simplification(%SPARQL.Algebra.Join{expr1: a, expr2: @zero_bgp}, _), do: a
  defp do_simplification(_, _), do: @no_mapping

  ############################################################################
  # 18.2.4 Converting Groups, Aggregates, HAVING, final VALUES clause and SELECT Expressions

  # https://www.w3.org/TR/sparql11-query/#sparqlGroupAggregate
  defp convert_aggregates(state) do
    {:ok, state}
  end


  # https://www.w3.org/TR/sparql11-query/#sparqlAlgebraFinalValues
  defp convert_final_values_clause(%{expr: {:query, p, nil}} = state) do
    {:ok, %{state | expr: p}}
  end

  defp convert_final_values_clause(%{expr: {:query, p, _data}} = state) do
    {:ok, %{state | expr: p}}
  end

  # https://www.w3.org/TR/sparql11-query/#sparqlHavingClause
  defp convert_having(state) do
    {:ok, state}
  end


  # https://www.w3.org/TR/sparql11-query/#sparqlSelectExpressions
  defp convert_select(%{expr:
        {:select, {var_decls, modifier}, _dataset_clause, where_clause, _solution_modifier}} = state) do
    with vs            = visible_variables(where_clause),
         {:ok, pv, e} <- select_items(var_decls, vs)
    do
      x =
        Enum.reduce e, where_clause, fn {var, expr}, x ->
          %SPARQL.Algebra.Extend{
            child_expr: x,
            var: var,
            expr: expr
          }
        end

      {:ok,
        state
        |> Map.put(:expr, {x, modifier})
        |> Map.put(:pv, pv)
      }
    end
  end

  defp convert_select(%{expr:
        {:ask, _dataset_clauses, where_clause, _solution_modifier}} = state) do
    {:ok,
      state
      |> Map.put(:expr, where_clause)
      |> Map.put(:pv, visible_variables(where_clause))
    }
  end

  defp convert_select(%{expr:
        {:describe, _subject, _dataset_clauses, where_clause, _solution_modifier}} = state) do
    {:ok,
      state
      |> Map.put(:expr, where_clause)
      |> Map.put(:pv, visible_variables(where_clause))
    }
  end

  defp convert_select(%{expr:
        {:construct, nil, _dataset_clauses, triple_template, _solution_modifier}} = state) do
    {:ok,
      state
      |> Map.put(:expr,
           %SPARQL.Algebra.Construct{
             template: translate_triples_block(triple_template),
             query: do_translate_basic_graph_patterns({:triples_block, triple_template}, state)
           }
         )
    }
  end

  defp convert_select(%{expr:
        {:construct, construct_template, _dataset_clauses, where_clause, _solution_modifier}} = state) do
    {:ok,
      state
      |> Map.put(:expr,
           %SPARQL.Algebra.Construct{
             template: translate_triples_block(construct_template),
             query: where_clause
           }
         )
    }
  end


  defp select_items(["*"], _vs) do
#    {:ok, vs, []} # TODO: The spec says PV := VS; but this would produce project() expression when they aren't needed (and not generated in other implementations)
    {:ok, [], []}
  end

  defp select_items(sel_items, vs) do
    sel_items
    |> Enum.reduce_while({[], []}, fn
         {variable, nil},  {pv, e} ->
           {:cont, {[variable | pv], e}}

         {variable, expr}, {pv, e} ->
           cond do
             variable in vs -> {:halt, "variable ?#{variable} already used"}
             variable in pv -> {:halt, "variable ?#{variable} used for multiple expressions"}
             true           -> {:cont, {[variable | pv], [{variable, expr} | e]}}
           end
       end)
    |> case do
         {pv, e} -> {:ok, Enum.reverse(pv), Enum.reverse(e)}
         error   -> {:error, error}
       end
  end

  ############################################################################
  # 18.2.5 Converting Solution Modifiers

  @doc !"""
  Converting Solution Modifiers

  Solutions modifiers apply to the processing of a SPARQL query after pattern matching.
  The solution modifiers are applied to a query in the following order:

  - Order by
  - Projection
  - Distinct
  - Reduced
  - Offset
  - Limit

  <https://www.w3.org/TR/sparql11-query/#convertSolMod>
  """
  defp convert_solution_modifiers(state) do
    {pattern, modifier} = case state.expr do
      {pattern, modifier} -> {pattern, modifier}
      pattern             -> {pattern, nil}
    end

    m = pattern # TODO: to_list(pattern)

    # TODO: 18.2.5.1 ORDER BY

    # 18.2.5.2 Projection
    m =
      if Map.get(state, :pv) && not Enum.empty?(state.pv) do
        %SPARQL.Algebra.Project{vars: state.pv, expr: m}
      else
        m
      end

    # 18.2.5.3 DISTINCT and 18.2.5.4 REDUCED
    m = case modifier do
      :distinct -> %SPARQL.Algebra.Distinct{expr: m}
      :reduced  -> %SPARQL.Algebra.Reduced{expr: m}
      nil       -> m
      _         -> m # TODO: remove this when the implementation is complete; we currently need this to make the W3C syntax tests pass on non-select queries
    end

    # TODO: 18.2.5.5 OFFSET and LIMIT

    {:ok, %{state | expr: m}}
  end


  ############################################################################
  # Helpers

  defp visible_variables(pattern) do
# TODO: remove this conditional which is currently needed to work with the unfinished algebra expression
#       there are similar conditionals in Join.variables/1 which should be removed together
    if is_map(pattern) do
      SPARQL.Algebra.Expression.variables(pattern)
    else
      []
    end
  end

  defp map(ast, state \\ %{}, fun)

  defp map(%{__struct__: _, child_expr: e} = s, state, fun) do
    Map.put(s, :child_expr, fun.(e, state) || map(e, state, fun))
  end

  defp map(%{__struct__: _, expr: e} = s, state, fun) do
    Map.put(s, :expr, fun.(e, state) || map(e, state, fun))
  end

  defp map(%{__struct__: _, expr1: e1, expr2: e2} = s, state, fun) do
    s
    |> Map.put(:expr1, fun.(e1, state) || map(e1, state, fun))
    |> Map.put(:expr2, fun.(e2, state) || map(e2, state, fun))
  end

  defp map(ast, state, fun) when is_list(ast) do
    Enum.map ast, fn element ->
      fun.(element, state) || map(element, state, fun)
    end
  end

  defp map({e0, e1, e2, e3, e4, e5}, state, fun) do
    {
      fun.(e0, state) || map(e0, state, fun),
      fun.(e1, state) || map(e1, state, fun),
      fun.(e2, state) || map(e2, state, fun),
      fun.(e3, state) || map(e3, state, fun),
      fun.(e4, state) || map(e4, state, fun),
      fun.(e5, state) || map(e5, state, fun)
    }
  end

  defp map({e0, e1, e2, e3, e4}, state, fun) do
    {
      fun.(e0, state) || map(e0, state, fun),
      fun.(e1, state) || map(e1, state, fun),
      fun.(e2, state) || map(e2, state, fun),
      fun.(e3, state) || map(e3, state, fun),
      fun.(e4, state) || map(e4, state, fun)
    }
  end

  defp map({e0, e1, e2, e3}, state, fun) do
    {
      fun.(e0, state) || map(e0, state, fun),
      fun.(e1, state) || map(e1, state, fun),
      fun.(e2, state) || map(e2, state, fun),
      fun.(e3, state) || map(e3, state, fun)
    }
  end

  defp map({e0, e1, e2}, state, fun) do
    {
      fun.(e0, state) || map(e0, state, fun),
      fun.(e1, state) || map(e1, state, fun),
      fun.(e2, state) || map(e2, state, fun)
    }
  end

  defp map({e0, e1}, state, fun) do
    {
      fun.(e0, state) || map(e0, state, fun),
      fun.(e1, state) || map(e1, state, fun)
    }
  end

  defp map({e}, state, fun) do
    {fun.(e, state) || map(e, state, fun)}
  end

  defp map(other, _, _), do: other

end