Some way to debug syntax errors?

I’ve got a syntax error in my code (in a function in a module) and I can’t seem to figure it out. The expression goes something like this:

ntuple(
  let a = b
    i -> expr(a, i)
  end,
  Val{c}())

According to the error message, it seems that Julia expects the entire let expression should be a function argument name!?

julia> include("HeterogeneousArrays.jl")
Main.HeterogeneousArrays

julia> include("LayoutTrees.jl")
ERROR: LoadError: syntax: "let dim = dim, axs = axs, ta = ta
    # /home/nsajko/src/gitlab.com/nsajko/BenchmarkVisualization/src/LayoutTrees.jl, line 189
    i -> begin
    # /home/nsajko/src/gitlab.com/nsajko/BenchmarkVisualization/src/LayoutTrees.jl, line 189
    ifelse((i == dim), ta, axs[i])
end
end" is not a valid function argument name around /home/nsajko/src/gitlab.com/nsajko/BenchmarkVisualization/src/LayoutTrees.jl:177
Stacktrace:
 [1] top-level scope
   @ ~/src/gitlab.com/nsajko/BenchmarkVisualization/src/LayoutTrees.jl:164

Here’s my code:

HeterogeneousArrays.jl
# Copyright 2022 Neven Sajko. All right reserved.

module HeterogeneousArrays

using LinearAlgebra

export AbstractHeterogeneousArray, HSArray

# An array with the given axes. Heterogeneous in general, in the sense
# that different elements can have different types.
abstract type AbstractHeterogeneousArray{axes} end

Base.axes(::AbstractHeterogeneousArray{a}) where {a} = a

Base.axes(::AbstractHeterogeneousArray{a}, i) where {a} = a[i]

Base.size(::AbstractHeterogeneousArray{a}) where {a} = map(length, a)

Base.size(::AbstractHeterogeneousArray{a}, i) where {a} = length(a[i])

Base.length(::AbstractHeterogeneousArray{a}) where {a} = prod(length, a)

Base.getindex(ar::AbstractHeterogeneousArray{a},
              i::CartesianIndex{n}) where {a, n} = ar[Tuple(i)...]

# Heterogeneous static array.
struct HSArray{
  axes, size, len, dim_count,
  ind_helper_mul,
  ind_helper_diff,
  T <: NTuple{len, Any}} <: AbstractHeterogeneousArray{axes}

  data::T

  function indexing_helper_mul(::Val{axes}) where {axes}
    local t = ntuple(i -> length(axes[i + 1]), Val{max(0, length(axes) - 1)}())
    ntuple(
      let t = t
        i -> prod(t[begin+i-1 : end])
      end,
      Val{length(axes)}())
  end

  function HSArray{a}(t::T) where {a, T <: Tuple}
    isa(a, NTuple{n, AbstractUnitRange{Int}} where {n}) ||
      error("axes not a tuple of Int unit ranges")
    local size = map(length, a)
    local len = prod(length, a)
    local dc = length(a)
    local ind_helper_mul = indexing_helper_mul(Val{a}())
    local ind_helper_diff = ntuple(i -> first(a[i]), Val{dc}())
    new{a, size, len, dc, ind_helper_mul, ind_helper_diff, T}(t)
  end
end

check_bounds(r::NTuple{n, AbstractUnitRange{Int}},
             i::Vararg{Int, n}) where {n} =
  for p in zip(r, i)
    (p[2] in p[1]) || Base.throw_boundserror(p[1], p[2])
  end

function Base.getindex(
  ar::HSArray{a, s, l, dc, ihm, ihd},
  i::Vararg{Int, dc}) where {a, s, l, dc, ihm, ihd}

  @boundscheck check_bounds(a, i...)
  local linind = dot(
    ihm,
    ntuple(
      let t = NTuple{dc, Int}(i)
        i -> t[i] - ihd[i]
      end,
      Val{dc}()))
  ar.data[linind + firstindex(())]
end

module HeterogeneousArraysWithConstAxes

using ..HeterogeneousArrays
using ..HeterogeneousArrays: check_bounds

export HSArrayWithConstAxes

# Merges tuples while preserving intra-tuple order, with indices
# determining where each element goes (which element of the resultant
# tuple comes from which source tuple).
#
# For example, if indices is:
#  (1, 2, 1, 1, 2)
# and tuples is:
#  ((11, 12, 13), (21, 22))
# the result is:
#  (11, 21, 12, 13, 22)
function merged_tuples(::Val{indices}, ::Val{tuples}) where {indices, tuples}
  (length(indices) == sum(length, tuples, init = 0)) || error("mismatched total size")
  all(
    pair -> ==(pair...),
    zip(
      ntuple(
        i -> count(==(i), indices, init = 0),
        Val{length(tuples)}()),
      map(length, tuples))) || error("mismatched size")

  ntuple(
    i ->
      let j = indices[i]
        tuples[j][count(==(j), indices[begin:(i - 1)], init = 0) + 1]
      end,
    Val{length(indices)}())
end

struct HSArrayWithConstAxes{
  parent_axis_bool_indices,
  const_axes,
  axes, size, len, dim_count,
  const_axis_bool_indices,
  parent_axis_indices, const_axis_indices,
  parent_axes,
  Parent <: AbstractHeterogeneousArray{parent_axes}} <:
  AbstractHeterogeneousArray{axes}

  parent::Parent

  function HSArrayWithConstAxes{pabi, ca}(ar::HA) where
  {pabi, ca, pa, HA <: AbstractHeterogeneousArray{pa}}
    local ax = merged_tuples(
      Val{ntuple(i -> Int(pabi[i]) + 1, Val{length(pabi)}())}(),
      Val{(ca, pa)}())
    local cabi = ntuple(i -> !pabi[i], Val{length(pabi)}())
    new{pabi, ca,
        ax,
        map(length, ax),
        prod(length, ax),
        length(ax),
        cabi,
        (findall(pabi)...,),
        (findall(cabi)...,),
        pa,
        HA}(ar)
  end
end

parent_axis_indices(::HSArrayWithConstAxes{
  pabi,
  ca,
  a, s, l, dc,
  cabi,
  pai}) where {pabi, ca, a, s, l, dc, cabi, pai} =
  pai

function Base.getindex(
  ar::HSArrayWithConstAxes{pabi, ca, a, s, l, dc},
  i::Vararg{Int, dc}) where {pabi, ca, a, s, l, dc}
  @boundscheck check_bounds(a, i...)
  ar.parent[i[collect(parent_axis_indices(ar))]...]
end

end  # module HeterogeneousArraysWithConstAxes

end  # module HeterogeneousArrays
LayoutTrees.jl
# Copyright 2022 Neven Sajko. All right reserved.

module LayoutTrees

using ..HeterogeneousArrays
using ..HeterogeneousArrays.HeterogeneousArraysWithConstAxes

export
  LayoutTree,
  LinearEquation,
  SystemSolution

struct Handle
  v::Int
end

mutable struct HandleGenerator{first}
  next::Handle
  HandleGenerator{first}() where {first} = new{first}(Handle(first))
end

function (hg::HandleGenerator)()
  local ret = hg.next
  hg.next = Handle(hg.next.v + 1)
  ret
end

count(hg::HandleGenerator{f}) where {f} =
  hg.next.v - f

struct LinearEquation{F <: Real}
  lin::Vector{Pair{Handle, F}}
  off::F
end

LinearEquation(l::Vector{Pair{Handle, F}}) where {F <: Real} =
  LinearEquation(l, zero(F))

struct SystemSolution{F <: Real}
  s::Vector{F}

  function SystemSolution(eqs::Vector{LinearEquation{F}}) where {F <: Real}
    local min_handle = minimum(eq -> minimum(v -> v.first.v, eq.lin), eqs)
    local max_handle = maximum(eq -> maximum(v -> v.first.v, eq.lin), eqs)

    isone(min_handle) || error("mnbvcfghj")

    local off = map(eq -> eq.off, eqs)
    local ar = zeros(F, length(eqs), max_handle)
    for (i, eq) in enumerate(eqs)
      for v in eq.lin
        ar[i, v.first.v] = v.second
      end
    end
    new{F}(ar \ off)
  end
end

Base.getindex(s::SystemSolution, i::Handle) = s.s[i.v]

# Length is the number of children.
struct LayoutTree{
  length, chil_dim_count, pabi,
  T <: Any,
  Children <: HSArrayWithConstAxes{
    pabi, ca, a, s, length, chil_dim_count, cabi, pai, cai, pa,
    <:HSArray{pa}} where {pabi, ca, a, s, cabi, pai, cai, pa}}

  data::T

  children::Children

  LayoutTree(v::T, chil::Chil) where
  {T <: Any,
   pabi, ca, a, s, l, dc, cabi, pai, cai, pa, ps, pl, pdc, ihm, ihd, cl,
   Chil <: HSArrayWithConstAxes{
    pabi, ca, a, s, l, dc, cabi, pai, cai, pa,
    <:HSArray{
      pa, ps, pl, pdc, ihm, ihd,
      <:NTuple{pl, <:LayoutTree{cl, dc, T}}}}} =
    new{l, dc, pabi, T, Chil}(v, chil)
end

struct Node{n}
  # Width, height, etc.
  sizes::NTuple{n, Handle}

  # If the node has a single child, this equals the top padding, bottom
  # padding, left padding and right padding, in the 2D case.
  #
  # If the node has more than one child, this is the layout padding.
  padding::Handle
end

(g::HandleGenerator)(::Type{Node{n}}) where {n} =
  Node(
    ntuple(
      let g = g
        i -> g()
      end,
      Val{n}()),
    g(),
    g())

(g::HandleGenerator)(chil::Chil) where
{pabi, ca, a, s, l, dc, Chil <: HSArrayWithConstAxes{pabi, ca, a, s, l, dc}} =
  LayoutTree(g(Node{dc}), chil)

const Tree = LayoutTree{len, dc, pabi, Node{dc}} where {len, dc, pabi}

# If root has no children.
root_node_equations(t::Tree{0}, ::Int, ::Type{<:Real}) =
  ()

# If root has a single child.
root_node_equations(t::Tree{1, dc}, dim::Int, ::Type{F}) where
{dc, F <: Real} =
  (LinearEquation([
    t.data.sizes[dim] => F(-1),
    t.data.padding => F(2),
    t.children[ntuple(
      let t = t
        j -> first(axes(t, j))
      end,
      Val{dc}())...].data.sizes[dim] => one(F)]),)

parent_children_padding_equation(
  t::Tree{len, dc, pabi},
  dim::Int,
  ::Type{F}) where {len, dc, pabi, F <: Real} =
  LinearEquation(
    let
      cnt = size(t, dim),
      first_two = (t.data.sizes[dim] => F(-1), t.data.padding => F(cnt - 1))

      ifelse(
        pabi[dim],
        [
          first_two...,
          ntuple(
            let t = t, dim = dim
              i -> (
                t.children[ntuple(
                  let t = t, dim = dim, i = i
                    j ->
                      let a = axes(t, j)
                        ifelse(j == dim, a[begin + i - 1], first(a))
                      end
                  end,
                  Val{dc}())...].data.sizes[dim] =>
                one(F))
            end,
            Val{cnt}())...],
        [
          first_two...,
          t.children[ntuple(
            let t = t
              j -> first(axes(t, j))
            end,
            Val{dc}())].data.sizes[dim] => F(cnt)])
    end)

# If root has more than one child.
root_node_equations(tree::Tree{len, dc}, dim::Int, ::Type{F}) where
{len, dc, F <: Real} =
  (
    parent_children_padding_equation(tree, dim, F),

    # Property of an n-dimensional grid:
    #  1. Choose an element of the grid. Its location is defined by n
    #     coordinates, one for each dimension. Its shape is defined by
    #     n sizes, again one for each dimension.
    #  2. Choose one of the n dimensions. Varying the element's
    #     coordinate associated with the chosen dimension keeps the
    #     sizes associated with the other n-1 dimensions constant.
    let
      har = tree.children,

      axs = axes(har),

      # Chosen axis
      ca = axs[dim],

      fi = first(ca),
      ta = ca[(begin + 1):end],

      naxs = ntuple(
        let dim = dim, axs = axs, ta = ta
          i -> ifelse(i == dim, ta, axs[i])
        end,
        Val{dc}()),

      cinds = CartesianIndices(naxs)

      ntuple(
        let har = har, dim = dim, fi = fi, cinds = cinds
          i ->
            let
              (j, k) = map(x -> x + 1, divrem(i - 1, dc - 1)),
              d = ifelse(k < dim, k, k + 1),
              t = cinds[j],
              r = CartesianIndex(ntuple(
                let dim = dim, fi = fi, t = t
                  i -> ifelse(i == dim, fi, t[i])
                end,
                Val{length(t)}()))
              LinearEquation([
                har[t].data.sizes[d] => one(F),
                har[r].data.sizes[d] => F(-1)])
            end
        end,
        Val{
          length(ta) *
          prod(map(
            a -> prod(length, a, init = 1),
            (axs[begin:(dim - 1)], axs[(dim + 1):end]))) *
          (dc - 1)}())
    end...)

function add_tree_equations!(
  eqs::Vector{LinearEquation{F}},
  tree::Tree{len, dc}) where {F, len, dc}

  for dim in 1:dc
    append!(eqs, root_node_equations(tree, dim, F))
  end

  let
    har = tree.children,
    cartesian_indices = CartesianIndices(axes(har))
    for cind in (cartesian_indices[i] for i in Base.OneTo(len))
      add_tree_equations!(eqs, har[cind])
    end
  end

  eqs
end

end  # module LayoutTrees

Is it perhaps possible to debug this by looking at some intermediate representation?

Bug report:
https://github.com/JuliaLang/julia/issues/46482

1 Like