Help with macro to construct truth tables

I’m trying to write a macro to generate truth tables. So far I’ve had success with finding the variables and evaluating the expression. However, I haven’t been able to display the truth value for individual variables.

The code I have so far is the following:

macro truth_table(expr)
    # Walk the expression and push the variables found to a vector `vars`:
    vars = Vector{Symbol}([])
    find_vars!(var::Symbol) = !(var in vars) && push!(vars, var)
    find_vars!(expr::Expr) = find_vars!.(expr.args)
    find_vars!(::Bool) = nothing

    find_vars!(expr)

    # Generate nested for loops where each variable is given a truth value:
    ex = :(@show $vars, $expr)         # <- the problematic line (I think?)
    for i in reverse(vars)             # Take a variable (in the order they were found).
        ex = quote
            for $i in [true, false]    # Iterate over truth values.
                $ex                    # Nest the previous expression.
            end
        end
    end
    return ex
end # truth_table

This will correctly evaluate truth values for arbitrary boolean expressions, but in the REPL it’ll print as:

julia> @truth_table x && y
([:x, :y], x && y) = ([:x, :y], true)
([:x, :y], x && y) = ([:x, :y], false)
([:x, :y], x && y) = ([:x, :y], false)
([:x, :y], x && y) = ([:x, :y], false)

I would expect the RHS to be ([true, true], true), ([true, false], false) and so on.

My question is: how can I show the evaluated value for each variable? I know vars is an array of symbols, but I’d like to get whatever the symbol evaluates to. What should I write in the intial ex instead?

Ideally I would like to save the table in a NamedArray where the column names are the names of each variable, but for now I’m just trying to get it to print correctly.

Hello @savq and welcome.

You’re much more likely to get help if you show us a minimal working example of the proble, you’re facing. PSA: make it easier to help you

In this case, if you could make a simple example of the code that produces the problem that we can actually run on our machines, it’s much easier to help.

That said, I think in this case I can guess what the problem is even though the code you posted doesn’t actually have the error. The problem is essentially a difference between this macro:

julia> macro foo(args...)
           esc(:(($(args...),)))
       end
@foo (macro with 1 method)

and this macro:

julia> macro bar(args...)
           esc(:(($(args...,))))
       end
@bar (macro with 1 method)

Here’s the difference:

julia> x, y = 1, 2
(1, 2)

julia> @foo x y
(1, 2)

julia> @bar x y
(:x, :y)

Now, where this mistake may have been made in your code is anyone’s guess.

(The snippets I posted were the whole code. I edited it to make it clearer)

I’m not sure it’s an escaping problem. To be clear, the variables don’t have to be defined before a call to @truth_table is made.

The macro can do this:

julia> isdefined(Main, :p)
false

julia> isdefined(Main, :q)
false

julia> isdefined(Main, :r)
false

julia> @truth_table p || (q && r)
([:p, :q, :r], p || q && r) = ([:p, :q, :r], true)
([:p, :q, :r], p || q && r) = ([:p, :q, :r], true)
([:p, :q, :r], p || q && r) = ([:p, :q, :r], true)
([:p, :q, :r], p || q && r) = ([:p, :q, :r], true)
([:p, :q, :r], p || q && r) = ([:p, :q, :r], true)
([:p, :q, :r], p || q && r) = ([:p, :q, :r], false)
([:p, :q, :r], p || q && r) = ([:p, :q, :r], false)
([:p, :q, :r], p || q && r) = ([:p, :q, :r], false)

So the context of the caller doesn’t really matter. The problem is mainly that I’m creating the loops from the inside out so escaping in ex would result in an outerref that doesn’t exist yet. I’m trying to find an alternative to that.

Just some idea, the truth table for and is just a Boolean multiplication table

V=[false, true ]
V.*V'

2×2 BitMatrix:
 0  0
 0  1

and for or

⊕(x,y) = max(x,y)
V.⊕V'

2×2 BitMatrix:
 0  1
 1  1

Not sure if this is of any help.

p =  [fill(false,4)...,fill(true,4)...]
q = repeat([false,true],4)
r = repeat([false,false,true,true],2)

hcat(p,q,r,p.⊕(q.*r))

8×4 Matrix{Bool}:
 0  0  0  0
 0  1  0  0
 0  0  1  0
 0  1  1  1
 1  0  0  1
 1  1  0  1
 1  0  1  1
 1  1  1  1

:disappointed_relieved: sorry that I’m not using macros, just some idea.

Sorry, for the misunderstanding, I thought there was a missing chunk.

I wasn’t talking about escaping, I was talking about the difference between

:(($(args...),))

and

:(($(args...,)))

To in your code, the problem is that when you write

:(@show $vars, $expr)

this doesn’t do what you want:

julia> let vars = [:x, :y], expr=:(x && y)
           :(@show $vars, $expr)
       end
:(#= REPL[91]:2 =# @show ([:x, :y], x && y))

here we see there’s a vector of Symbols in here instead of what you wanted. This can be re-written as

julia> let vars = [:x, :y], expr=:(x && y)
           :(@show [$(vars...)], $expr)
       end
:(#= REPL[92]:2 =# @show ([x, y], x && y))

to now get a vector of x and y. Though, for efficiency, you likely are better off writing

julia> let vars = [:x, :y], expr=:(x && y)
           :(@show ($(vars...),), $expr)
       end
:(#= REPL[93]:2 =# @show ((x, y), x && y))

to get a Tuple of values instead.

Here’s the revised macro:

julia> macro truth_table(expr)
           # Walk the expression and push the variables found to a vector `vars`:
           vars = Vector{Symbol}() # the `[]` wasn't necessary here.
           find_vars!(var::Symbol) = !(var in vars) && push!(vars, var)
           find_vars!(expr::Expr) = find_vars!.(expr.args)
           find_vars!(::Bool) = nothing

           find_vars!(expr)

           # Generate nested for loops where each variable is given a truth value:
           ex = :(@show ($(vars...),), $expr)  
           for i in reverse(vars)             # Take a variable (in the order they were found).
               ex = quote
                   for $i in (true, false)    # Iterate over truth values.
                       $ex                    # Nest the previous expression.
                   end
               end
           end
           return ex
       end # truth_table
@truth_table (macro with 1 method)

Now, we have

julia> @truth_table x && y
((x, y), x && y) = ((true, true), true)
((x, y), x && y) = ((true, false), false)
((x, y), x && y) = ((false, true), false)
((x, y), x && y) = ((false, false), false)

and

julia> @truth_table p || (q && r)
((p, q, r), p || q && r) = ((true, true, true), true)
((p, q, r), p || q && r) = ((true, true, false), true)
((p, q, r), p || q && r) = ((true, false, true), true)
((p, q, r), p || q && r) = ((true, false, false), true)
((p, q, r), p || q && r) = ((false, true, true), true)
((p, q, r), p || q && r) = ((false, true, false), false)
((p, q, r), p || q && r) = ((false, false, true), false)
((p, q, r), p || q && r) = ((false, false, false), false)
2 Likes

I wasn’t talking about escaping…

Ah, then I misunderstood. I missed the syntactic difference there. But I get know.

Thanks a lot!

1 Like

Happy to help

Just some idea, the truth table for and is just a Boolean multiplication table

Yes, I was mostly interested in the meta-programming aspect of the problem tho :slight_smile:

1 Like

By the way, you can just use & and | directly.

julia> lift(v, N=1) = reshape(v, ntuple(_ -> 1, N)..., size(v)...)
lift (generic function with 2 methods)

julia> p, q, r = [true, false], lift([true, false]), lift([true, false], 2);

julia> p .& q
2×2 BitMatrix:
 1  0
 0  0

julia> p .| q
2×2 BitMatrix:
 1  1
 1  0

julia> p .| (q .& r)
2×2×2 BitArray{3}:
[:, :, 1] =
 1  1
 1  0

[:, :, 2] =
 1  1
 0  0
1 Like

:joy: this is definitely better

1 Like