How to change operator symbols in truth table

The undesired spacing around \sim comes from the fact that \sim is defined as a relational symbol using \mathrel; from the kernel:

\DeclareMathSymbol{\sim}{\mathrel}{symbols}{"18}

You can supress this space treating \sim as an ordinary symbol using \mathord{\sim}:

\cs_new_protected:Npn \__tt_build_header:
    {
        \seq_set_from_clist:NN \l__tt_header_seq \l__tt_vars_clist
        \seq_concat:NNN \l__tt_header_seq \l__tt_header_seq \l__tt_exprs_seq
        \tl_set:Nx \l_tmpa_tl {\seq_use:Nnnn \l__tt_header_seq {&}{&}{&}}
        \tl_replace_all:Nnn \l_tmpa_tl {*} {\cdot}
        \tl_replace_all:Nnn \l_tmpa_tl {+} {+}
        \tl_replace_all:Nnn \l_tmpa_tl {->} {\to}
        \tl_replace_all:Nnn \l_tmpa_tl {-} {\mathord{\sim}}
        \tl_use:N \l_tmpa_tl
    }

the compete code:

\documentclass{article}
\usepackage{xparse}

\begingroup
  \catcode`\%=12\relax
  \gdef\patmatch{"(%b())->(%b())","!%1||%2"}
\endgroup

\def\setimpaux#1{%
  \directlua{
    local s, _ = string.gsub("\luatexluaescapestring{#1}",\patmatch)
    tex.sprint(s)
  }
}

\ExplSyntaxOn
\int_new:N \l__tt_num_rows_int
\int_new:N \l__tt_num_cols_int
\int_new:N \l__tt_num_vars_int
\clist_new:N \l__tt_vars_clist
\seq_new:N \l__tt_exprs_seq
\seq_new:N \l__tt_header_seq

\NewDocumentCommand {\truthtable}{ m m }
    {
        \truth_table:nn {#1}{#2}
    }

\cs_new_protected:Npn \truth_table:nn #1#2
    {
        \clist_set:Nn \l__tt_vars_clist {#1}
        \seq_set_split:Nnn \l__tt_exprs_seq {;} {#2}
        \int_set:Nn \l__tt_num_vars_int {\clist_count:N \l__tt_vars_clist}
        \int_set:Nn \l__tt_num_rows_int {\fp_to_int:n {2^{\l__tt_num_vars_int}-1}}
        \int_set:Nn \l__tt_num_cols_int {\clist_count:N \l__tt_vars_clist +\seq_count:N \l__tt_exprs_seq}
        \__tt_gen_bins:
        \seq_map_function:NN \l__tt_exprs_seq \__tt_eval_bools:n
        \__tt_build_table:
    }

\cs_new_protected:Npn \__tt_build_header:
    {
        \seq_set_from_clist:NN \l__tt_header_seq \l__tt_vars_clist
        \seq_concat:NNN \l__tt_header_seq \l__tt_header_seq \l__tt_exprs_seq
        \tl_set:Nx \l_tmpa_tl {\seq_use:Nnnn \l__tt_header_seq {&}{&}{&}}
        \tl_replace_all:Nnn \l_tmpa_tl {*} {\cdot}
        \tl_replace_all:Nnn \l_tmpa_tl {+} {+}
        \tl_replace_all:Nnn \l_tmpa_tl {->} {\to}
        \tl_replace_all:Nnn \l_tmpa_tl {-} {\mathord{\sim}}
        \tl_use:N \l_tmpa_tl
    }

\cs_generate_variant:Nn \seq_use:Nnnn {cnnn}
\cs_new_protected:Npn \__tt_build_table:
    {
        \begin{array}{*{\int_use:N \l__tt_num_cols_int}{c}}
            \__tt_build_header:\\\hline
            \int_step_inline:nnnn {0}{1}{\l__tt_num_rows_int}
                {
                    \seq_use:cnnn {l__tt_row_{##1}_seq}{&}{&}{&}\\
                }
        \end{array}
    }

\cs_new_protected:Npn \__tt_set_imp:n #1
    {
        \tl_if_in:nnT {#1} {->}
            {
                \tl_set:Nx \l_tmpb_tl {\setimpaux{#1}}
                \exp_args:NV \__tt_set_imp:n \l_tmpb_tl
            }
    }
\cs_generate_variant:Nn \__tt_set_imp:n {V}

\cs_generate_variant:Nn \tl_replace_all:Nnn {Nnx}
\cs_new_protected:Npn \__tt_eval_bools:n #1
    {
        \tl_set:Nn \l_tmpa_tl {#1}
        \int_step_inline:nnnn {0}{1}{\l__tt_num_rows_int}
            {
                \int_set:Nn \l_tmpa_int {1}
                \tl_set_eq:NN \l_tmpb_tl \l_tmpa_tl
                \__tt_set_imp:V \l_tmpb_tl
                \tl_replace_all:Nnn \l_tmpb_tl {*}{&&}
                \tl_replace_all:Nnn \l_tmpb_tl {+}{||}
                \tl_replace_all:Nnn \l_tmpb_tl {-}{!}
                \clist_map_inline:Nn \l__tt_vars_clist
                    {
                        \tl_replace_all:Nnx \l_tmpb_tl {####1} {\seq_item:cn {l__tt_row_{##1}_seq} {\l_tmpa_int}}
                        \int_incr:N \l_tmpa_int
                    }
                \seq_put_right:cx {l__tt_row_{##1}_seq} {\fp_eval:n \l_tmpb_tl}
            }
    }

\cs_generate_variant:Nn \seq_set_split:Nnn {cnx}
\cs_new_protected:Npn \__tt_gen_bins:
    {
        \int_step_inline:nnnn {0}{1}{\l__tt_num_rows_int}
            {
                \seq_clear_new:c {l__tt_row_{##1}_seq}
                \seq_set_split:cnx {l__tt_row_{##1}_seq} {} {\int_to_binary:n {##1}}
                \int_while_do:nn {\seq_count:c {l__tt_row_{##1}_seq} < \l__tt_num_vars_int}
                    {
                        \seq_put_left:cn {l__tt_row_{##1}_seq} {0}
                    }
            }
    }

\ExplSyntaxOff
\begin{document}

\[
\truthtable{a,b,c}{a+b;b*(-c);-(a+b)+(b*(-c))}
\]

\end{document}

enter image description here

An explanation of \mathord, \mathrel, \mathbin and similars can be found in TeX by Topic or in the TeXbook.


Compatibility updates

  • release 1.2l (2017/07/26) removed an internal macro (\xint_gob_til_xint_relax) which I had used here. So I added the definition to make the code compile again.

  • (2015/03/07) since release 1.1 (2014/10/28) of xint the original code of this answer was broken. The overhaul of \xintNewExpr was mentioned in the user manual, but it should have been included among the list of "breaking changes" in CHANGES.html. Actually it is only a very esoteric feature of \xintNew(Bool)Expr whose change impacted the code here, I had forgotten that there existed some use of this in the world !

For people with xint < 1.1 the old code is there, but commented out.

In passing I mention that the logic operator && and || are now preferred by xint rather than & and |, but the latter are still accepted although perhaps they will be used in the future for other things: bitwise operations perhaps.


This is a (modest) alternative to the LaTeX3+lualatex approach. It uses xintexpr. It requires the user to employ \AND, \OR, \NOT in the logical expressions. Other keywords are \XOR, \ALL, \ANY.


latest update: the number of variables is anyhow limited to 9, because each expression will be converted into a macro with that many parameters. Hence the first answer with nested \xintFor, which was spelled out up to 5 variables had the potential of full generality in this context. But in my second answer (bottom of this answer) I generated the rows in a way analogous to the method I see in the LaTeX3+lualatex code, via binary expansion of the integers 0, 1 ... up to 2^n-1, with n the number of variables. This makes for a more compact coding (also less efficient, but that doesn't matter much).

This second answer additionally adds an optional parameter allowing the user to specify directly how the expressions will appear in the header row.

Also, it allows the final expression too to be ended by a ;.

And, a starred variant uses \halign rather than an array environment in order to allow pagebreaks. With 9 variables, there are 512 generated rows, hence this is necessary. The image is only the first page of a document having 12 of them.


update: use of ; (as in OP) rather than , to separate expressions.

The boolean expressions are typed in using \AND, \OR, \NOT and also there is \XOR, \ALL, \ANY (which have multiple arguments separated by ,). One can then customize in the macro how they are supposed to be typeset.

The boolean expressions are separated (as in the OP) by semi-colons: my initial answer used commas, because the \xintexpr parser handles this natively, also when commas are used elsewhere to separate arguments to function. However, for typesetting the first row, one had to hide such commas with braces for example \XOR({a,b,c}), or \XOR(a\SEP b\SEP c) with \def\SEP{,}, not very convenient. Thus, expressions in the user input are now separated using ;.

As one can see, the code here is quite short. I ran a bit out of steam at the end and possibly there could be a way to shorten it somewhat more and avoid the big \ifcase. As it stands, the code allow to construct a table with up to 5 indeterminates represented as letters (enlarging the \ifcase one may go up to 9; with 5 letters we already have 32 rows) and arbitrarily many boolean expressions separated by commas (actually as it stands the nb of expressions plus the nb of indeterminates should be kept at most 10, see the *{10}c array preamble) [I didn't have time to check if one can replace 10 by something dynamical, in that case there is no problem computing it from the two arguments to \truthtable]

truthtables

Two more truth tables:

more truth tables

\documentclass{article}
\usepackage{xintexpr}

\catcode`_ 11

\newcount \tt_varcnt

% this internal macro was suppressed in xint 1.2l (2017/07/26)
% but is used in this answer
\long\def\xint_gob_til_xint_relax #1\xint_relax {}%
\let\xint_relax\relax

% auxiliary macro to handle things separated by semi-colons
\def\truthtable_scanexprs #1;{%
    \xint_gob_til_xint_relax #1\truthtable_scan_done\xint_relax
    \odef\tt_exprlist {\tt_exprlist{#1}}%
    \truthtable_scanexprs
}%
\def\truthtable_scan_done #1\truthtable_scanexprs {}                           

\newcommand\truthtable [2]{%
% example of use :
% \truthtable{a,b,c}{a\OR b; b\AND\NOT(c)}
%
% The first argument lists the used indeterminates, they must be lowercase or
% uppercase Latin letters.
% 
% The second argument is a ; separated list of logical expressions.
  \begingroup
    \endlinechar-1 \everyeof{\noexpand }% for use of \scantokens
    % initializing
    \tt_varcnt 0
    \def\tt_exprlist {}% will be {exprA}{exprB}{exprC}...
    % scan #2 for ; separated expressions and fill \tt_exprlist:
    \truthtable_scanexprs #2;\xint_relax;%
    %
    % PREPARATION FOR DEFINING THE BOOLEAN EXPRESSIONS
% MODIFIED FOR COMPATIBILITY WITH xint 1.1 OR LATER
%%%% | and & are still accepted but the manual recommend || and &&
%%%%  (2015/03/07)
    \def\OR{||}\def\AND{&&}\def\NOT{!}% needed for \xintNewBoolExpr
% original was
    % \def\OR{|}\def\AND{&}\def\NOT{!}% needed for \xintNewBoolExpr
% END
    % the negation ! must be applied to a parenthesized expression
    % 
    % extra strange set-up before \edef and then \scantokens
    % the idea is too allow, X, O, R, A, L, L, A, N, Y as indeterminates...
    % ... and for this as we will need to make them active, thus
    %     we must first replace \XOR for example by \1.
    \def\XOR{\1}\def\ALL{\2}\def\ANY{\3}%
    \let\1\relax\let\2\relax\let\3\relax
    %
    \edef\tt_x {\tt_exprlist}% replaces \XOR by \1, \ALL by \2, \ANY by \3
    %
% ORIGINAL VERSION:
    % % we now make active the indeterminates to replace them by {$1}, {$2}, ..
    % % place holders (cf.  xintexpr manual)
    % \xintFor ##1 in {#1} \do 
    %   {\catcode`##1\active
    %    \advance\tt_varcnt 1
    %    \lccode`~=`##1\relax
    %    \lowercase{\edef~}{{$\the\tt_varcnt}}%$1, $2, $3, as in xintexpr manual
    % }%
% NEW VERSION FOR COMPATIBILITY WITH xint 1.1 OR LATER  (2015/03/07)
    % we now make active the indeterminates to replace them by {#1}, {#2}, ..
    % place holders (with a catcode 12 # as it simplifies things here, and
    % the \xintNewExpr scanner does not mind)
    \xintFor ##1 in {#1} \do% 
    % I should have defined a sub-macro for this, code would be clearer
        {\catcode`##1\active
         \advance\tt_varcnt 1
         \lccode`~=`##1\relax
         \lowercase{\edef~}{{\string####\the\tt_varcnt}}}%
% END OF MODIFICATION
    %
    % Now \scantokens to replace the now active letters by the braced
    % macro parameters (I don't recall exactly why I wanted them braced,
    % these braces will get removed during \xintNewExpr scan)
    %
    \edef\tt_x {\scantokens\expandafter{\tt_x}}%
    %
    % For use of \xintNewBoolExpr (which again does some \scantokens), 
    % the letters *must* be reset to their standard catcodes.
    %
    \xintFor ##1 in {#1} \do {\catcode`##1 11 }% 
    %
    % Definition of the boolean expressions. Using ; as separator for
    % user input was only in order to ease up
% NOTE: (2015/03/07)
% for simplicity I stick here to the originally used ; as expression separator
% on input, but xint 1.1 has macros which can be used, out of \xintexpr
% context to identify (expandably) comma separated things, even themselves
% containing commas if they are in parentheses
    % finding the correct
    % expressions for typesetting the head-row of the table, we need to
    % re-install here the , as separator, this allows to do only one
    % \xintNewBoolExpr, as it knows how to identify the various 
    % comma separated sub-expressions.
    %
    \def\1{xor}\def\2{all}\def\3{any}%
    \xintNewBoolExpr\tt_y[\tt_varcnt]{\xintListWithSep{,}{\tt_x}}%
    % 
    % CUSTOMIZE HERE:
    % For typesetting the head row, customize as desired:
    \def\OR{+}\def\AND{\cdot}\def\NOT{\mathord{\sim}}%
    \def\XOR{\mbox{\texttt{xor}}}%
    \def\ALL{\mbox{\texttt{all}}}%
    \def\ANY{\mbox{\texttt{any}}}%
    %
    % the table. Up to five variables, extensible up to use of nine variables
    \begin{array}{*{10}c}
       \xintListWithSep{&}{\xintCSVtoList{#1}}&%
                                 \xintListWithSep{&}{\tt_exprlist}\\
       \hline
       \ifcase\tt_varcnt
       \or
       \xintFor* ##1 in {01}\do
       {##1 & \xintListWithSep{&}{\xintCSVtoList{\tt_y {##1}}}\\ }
       \or
       \xintFor* ##1 in {01}\do
       {\xintFor* ##2 in {01}\do
        {##1 & ##2& 
           \xintListWithSep{&}{\xintCSVtoList{\tt_y {##1}{##2}}}\\ }}
       \or
       \xintFor* ##1 in {01}\do
       {\xintFor* ##2 in {01}\do
        {\xintFor* ##3 in {01}\do
         {##1 & ##2 & ##3& \xintListWithSep{&}%
                           {\xintCSVtoList{\tt_y {##1}{##2}{##3}}}\\ }}}
       \or
       \xintFor* ##1 in {01}\do
       {\xintFor* ##2 in {01}\do
        {\xintFor* ##3 in {01}\do
         {\xintFor* ##4 in {01}\do
          {##1 & ##2 & ##3& ##4&\xintListWithSep{&}%
                           {\xintCSVtoList{\tt_y {##1}{##2}{##3}{##4}}}\\ }}}}
       \or
       \xintFor* ##1 in {01}\do
       {\xintFor* ##2 in {01}\do
        {\xintFor* ##3 in {01}\do
         {\xintFor* ##4 in {01}\do
          {\xintFor* ##5 in {01}\do
          {##1 & ##2 & ##3& ##4& ##5&\xintListWithSep{&}%
                      {\xintCSVtoList{\tt_y {##1}{##2}{##3}{##4}{##5}}}\\ }}}}}
       \fi
       \hline 
    \end{array}
    \endgroup
}
\catcode`_ 8   

\begin{document}

\[
\truthtable{a,b,c}{a\OR b; b\AND\NOT(c); \NOT (a\OR b)\OR (b\AND\NOT(c))}
\]

\[
\truthtable{X,Y,Z}{Z\AND X \OR Y \AND X}
\]

\[
\truthtable{X,Y,Z,T}{Z\AND X \OR Y \AND X \AND T}
\]

\[
\truthtable{a, b}{\XOR (a,b)}
\]

\[
\truthtable {p, q, r, s, t}{\ANY(p,q,r,s,t); \XOR(p,q,r,s,t); \ALL(p,q,r,s,t)}
\]

\[
\truthtable{a, b, c, d}{\XOR (a,b,c,d); \XOR(a,\XOR(b,c,d)); \XOR(\XOR(a,b),\XOR(c,d))}
\]

\[
\truthtable{A,D,N}{A\AND D \OR N \AND D; A\OR D \AND N \OR D}
\]

\end{document}

Here is the code of the second answer:

\documentclass[a4paper]{article}
\usepackage{geometry}
\usepackage{xintexpr}
% in order to convert from decimal to binary using \xintDecBin
\usepackage{xintbinhex}

\catcode`_ 11
\makeatletter

% to count the number of variables:
\newcount \tt_varcnt
% The number of variables is at most 9 (leading to 512 rows...)

% to count the number of expressions (in order to choose dynamically
% a large enough number of columns for the array environment)
\newcount \tt_exprcnt

% to handle things separated by semi-colons

% we add some extra to allow empty expressions to be skipped, and also to allow
% the user to terminate the last expression by a ; 

% [if the optional parameter is used, use \space to get something empty in the
% header row cell]

% this internal macro was suppressed in xint 1.2l (2017/07/26)
% but is used in this answer
\long\def\xint_gob_til_xint_relax #1\xint_relax {}%
\let\xint_relax\relax

\def\truthtable_scanexprs #1;{%
    \if\relax\detokenize{#1}\relax\expandafter\truthtable_scan_skip\fi
    \xint_gob_til_xint_relax #1\truthtable_scan_done\xint_relax
    \odef\tt_exprlist {\tt_exprlist{#1}}%
    \advance \tt_exprcnt 1
    \truthtable_scanexprs
}%
\def\truthtable_scan_done #1\truthtable_scanexprs {}%                     
\def\truthtable_scan_skip #1\truthtable_scanexprs {\truthtable_scanexprs}%

% we now have an optional parameter to provide a custom typesetting
% of the expressions in the head row

% and we have furthermore a star variant to use \halign rather than array

\newcommand*\truthtable {%
   \begingroup
     \endlinechar-1 \everyeof{\noexpand }% for use of \scantokens
     \tt_varcnt 0
     \tt_exprcnt 0
     \def\tt_exprlist {}%
     \def\tt_headexprlist {\tt_exprlist}%
     \def\tt_usearray {1}%
     \@ifstar{\def\tt_usearray {0}\truthtable@chkopt}\truthtable@chkopt
}%

\def\truthtable@chkopt {\@ifnextchar[{\truthtable@opt}{\truthtable@}%]
                       }

\def\truthtable@opt [#1]{%
      % we first scan the optional argument for the header row.
      % it will be printed as is, only need to transform the ;'s into &'s
      \truthtable_scanexprs #1;\xint_relax;%
      \let\tt_headexprlist\tt_exprlist
      \tt_exprcnt 0
      \def\tt_exprlist {}%
      \truthtable@
      % *NO* check is done that #1 defines the same number of expressions
      % as the last mandatory argument to \truthtable !
}

\def\truthtable@ #1#2{%
    % convert the comma separated indeterminates into a list
    \oodef\tt_varlist{\xintCSVtoListNoExpand {#1}}%
    % scan #2 for ; separated expressions and fill \tt_exprlist:
    \truthtable_scanexprs #2;\xint_relax;%
    % PREPARATION FOR DEFINING THE BOOLEAN EXPRESSIONS
% MODIFIED FOR COMPATIBILITY WITH xint 1.1 OR LATER
%%%% | and & are still accepted but the manual recommend || and &&
%%%%  (2015/03/07)
    \def\OR{||}\def\AND{&&}\def\NOT{!}% needed for \xintNewBoolExpr
% original was
    % \def\OR{|}\def\AND{&}\def\NOT{!}% needed for \xintNewBoolExpr
% END
    % the negation ! must be applied to a parenthesized expression
    % 
    % extra strange set-up before \edef and then \scantokens
    % the idea is too allow, X, O, R, A, L, L, A, N, Y as indeterminates...
    % ... and for this as we will need to make them active, thus
    %     we must first replace \XOR for example by \1.
    \def\XOR{\1}\def\ALL{\2}\def\ANY{\3}%
    \let\1\relax\let\2\relax\let\3\relax
    %
    \edef\tt_x {\tt_exprlist}%  replaces \XOR by \1, \ALL by \2, \ANY by \3
    %
% ORIGINAL VERSION:
    % % we now make active the indeterminates to replace them by {$1}, {$2}, ..
    % % place holders (cf.  xintexpr manual)
    % \xintFor ##1 in {#1} \do 
    %   {\catcode`##1\active
    %    \advance\tt_varcnt 1
    %    \lccode`~=`##1\relax
    %    \lowercase{\edef~}{{$\the\tt_varcnt}}%$1, $2, $3, as in xintexpr manual
    % }%
% NEW VERSION FOR COMPATIBILITY WITH xint 1.1 OR LATER  (2015/03/07)
    % we now make active the indeterminates to replace them by {#1}, {#2}, ..
    % place holders (with a catcode 12 # as it simplifies things here, and
    % the \xintNewExpr scanner does not mind)
    \xintFor ##1 in {#1} \do% 
    % I should have defined a sub-macro for this, code would be clearer
        {\catcode`##1\active
         \advance\tt_varcnt 1
         \lccode`~=`##1\relax
         \lowercase{\edef~}{{\string####\the\tt_varcnt}}}%
% END OF MODIFICATION
    % Now the \scantokens with active letters. 
    \edef\tt_x {\scantokens\expandafter{\tt_x}}%
    %
    % For use of \xintNewBoolExpr (which again does some \scantokens), 
    % the letters *must* recover their standard catcodes
    %
    \xintFor* ##1 in \tt_varlist \do {\catcode`##1 11 }% 
    %
    % Definition of the boolean expressions. Using ; as separator for
    % user input was only in order to ease up finding the correct
    % expressions for typesetting the head-row of the table, we need to
    % re-install here the , as separator, this allows to do only one
    % \xintNewBoolExpr, it knows how to identify the various sub-expressions.
    \def\1{xor}\def\2{all}\def\3{any}%
    \xintNewBoolExpr\tt_y[\tt_varcnt]{\xintListWithSep{,}{\tt_x}}%
    % 
    % CUSTOMIZE HERE:
    % For typesetting the head row, customize as desired:
    % (*not* relevant in case of use of the optional parameter)
    \def\OR{+}\def\AND{\cdot}\def\NOT{\mathord{\sim}}%
    \def\XOR{\mbox{\texttt{xor}}}%
    \def\ALL{\mbox{\texttt{all}}}%
    \def\ANY{\mbox{\texttt{any}}}%
    %
    \if\tt_usearray 1\expandafter\truthtable_array\else
                     \expandafter\truthtable_halign
    \fi
    \endgroup
}

\def\truthtable_array {%
    % (we have the count of variables in \tt_varcnt and the count of 
    %  expressions in \tt_exprcnt)
    \begin{array}{*{\numexpr\tt_varcnt+\tt_exprcnt\relax}c}
       % generate the header-row, 
         \xintListWithSep{&}{\tt_varlist}
       &
         \xintListWithSep{&}{\tt_headexprlist}\\
       \hline
       % calculate 2^n, n is nb of variables
       \tt_varcnt \xintiiPow {2}{\tt_varcnt} 
       % generate integers from 2^n  to 2*2^n -1, then binary notation
       % will always start by a 1 which we can gobble
       \xintFor* ##1 in {\xintSeq {\tt_varcnt}{2*\tt_varcnt-1}}
       \do
       %    (the problem is that \xintDecToBin always trims leading zeros...
       %     hence we resort to this to always have a leading 1)
       {\edef\tt_temp{\expandafter\expandafter\expandafter
                      \xint_gobble_i\xintDecToBin {##1}}%
        \edef\tt_temp{\tt_temp\xintCSVtoList{\expandafter\tt_y\tt_temp}}%
        \xintListWithSep {&}{\tt_temp}\\}
       \hline 
    \end{array}
}

\newtoks\tt_toks

% The \halign variant (should *not* be put inside \[..\])
\def\truthtable_halign {%
    % This is variant using \halign to allow break accross pages.
    % I use some tricks to center it (whow! incredibly it works)
    % (I initially used a repeatable preamble, but following TeX by Topic
    %  25.5, to center the alignment, I need to insert some \tabskip
    %  at the LAST column, hence here I follow a more complicated approach
    %  constructing the preamble first.)
    %  
    % but then I don't know how to have horizontal rules limited to
    % the width covered by the actual contents... 
    %
    \tt_toks {\hfil$\mathstrut##$\hfil\tabskip 2\arraycolsep}%
    \xintiloop [{\tt_varcnt+\tt_exprcnt-1}+-1]
    \ifnum\xintiloopindex>0
      \tt_toks \expandafter{\the\tt_toks &\hfil$##$\hfil}%
    \repeat
    %\showthe\tt_toks
    \tabskip 0pt plus 1000pt minus 1000pt 
    \halign to \hsize
    {\span\the\tt_toks\tabskip 0pt plus 1000pt minus 1000pt \cr
     % first row
     \xintListWithSep{&}{\tt_varlist}&\xintListWithSep{&}{\tt_headexprlist}\cr
     % however the rule extends across the full page
     %     \hline
     \tt_varcnt \xintiiPow {2}{\tt_varcnt} 
     \xintFor* ##1 in {\xintSeq {\tt_varcnt}{2*\tt_varcnt-1}}
       \do
       {\edef\tt_temp{\expandafter\expandafter\expandafter
                      \xint_gobble_i\xintDecToBin {##1}}%
        \edef\tt_temp{\tt_temp\xintCSVtoList{\expandafter\tt_y\tt_temp}}%
        \xintListWithSep {&}{\tt_temp}\cr }%
     %      \hline 
    }%
}

\catcode`_ 8   
\makeatother

\pagestyle{empty}
\begin{document}\thispagestyle{empty}

\[
\truthtable{a,b,c}{a\OR b; b\AND\NOT(c); \NOT (a\OR b)\OR (b\AND\NOT(c));}
\]


\textbf{Exercise:~} fill in the header row:

\[% in the optional argument, \AND, \NOT, are not mandatory but optional
  % on can use whatever one wants.
\truthtable [a?b; ?\cdot\NOT(c); \NOT(a+?)?(b\AND\NOT(?));]% trailing ; allowed
            {a, b, c}
            {a\OR b; b\AND\NOT(c); \NOT (a\OR b)\OR (b\AND\NOT(c))}
\]


% maximal number of variables is 9

% \truthtable* {a, b, c}{\XOR(a, b, c)}
\bigskip
\hrule
\medskip
An example with the maximal number of variables (9, hence 512 rows) using
\verb|\halign| to allow breaking accross pages:\medskip

\truthtable* [\XOR(a,\cdots,i); ?(a,\cdots,d)\AND\NOT(?(e,\cdots,i));]
             {a, b , c, d, e, f, g, h, i}
             {\XOR(a, b , c, d, e, f, g, h, i); 
              \ALL(a,b,c,d)\AND\NOT(\ANY(e,f,g,h,i))
            }

\end{document}

truth table ...


I am providing another answer for two reasons:

  1. since fall 2015 relase of xint a simpler approach is possible; no need to make letters active to scan for variables.

  2. This simpler approach is also more powerful: it is not needed to format the input with \AND, \OR, etc.. macros; it can be given in the syntax which understands \xintexpr. It will then be possible to both evaluate and format the original expressions with suitable symbols.

  3. questions were asked in comments to the first answer how to add more operators. It is not possible to handle binary infix logic operators which are not already known to the xintexpr syntax. But some operators can be simulated by the comparison operators. One does use \IFF etc.. notations, not because it is mandatory, but because the notation with comparison operators (on input) is non-intuitive.

See the code comments for more explanations.

(update is only about some changes of detail in implementation and code comments)

\documentclass[a4paper]{article}

\usepackage{geometry}
\usepackage{xintexpr}

\usepackage{newtxtext,newtxmath}
\usepackage[straightquotes]{newtxtt}

\usepackage{longtable, array, color, bm}% Thanks to D.C.
\setlength{\LTcapwidth}{\textwidth}

\makeatletter
% Utility macro to use directly the underlying macro foo with variables bar
% as defined earlier  by \xintdeffunc foo(bar):=...;
\newcommand*\UseUserFuncMacro [2]{\csname XINT_expr_userfunc_#1\endcsname #2,}

% Define logical values:
\xintdefvar T:= 1;
\xintdefvar F:= 0;

% We will use input with control sequences \AND, etc.. 


% The mechanism of the Truth Table necessitates that the xintexpr syntax
% has some infix binary operator pre-assigned to the given operation

% This is currently the case for AND, OR, XOR (and NOT as function).

% Currently there is no available operator for things such as 
% NOR, NAND, XNOR.

% **This will have to wait for a future package release.**

% Some operations can be simulated by comparison operators

% IFF             can be simulated by =
% IMPLIES         can be simulated by <=
% IS IMPLIED BY   can be simulated by >=
% DOES NOT IMPLY  can be simulated by >
% IS NOT IMPLIED  can be simulated by <
% IS NOT EQUIV.   can be simulated by != but same as XOR

% BUT THE PRECEDENCE LEVELS MUST BE MODIFIED. 

% This is easier than adding a new infix operator to the xintexpr syntax. 

% Changing precedences could have been a one-liner but unfortunately xintexpr
% (1.2e) has some unnecessarily hard-coded things. Thus we have to do some
% redefinitions. Adding genuine new operators rather than abusing already
% existing ones would require a few more definitions, but this is best left to
% a package upgrade.

% ----

% The approach here is to code the formulas with macros \IFF, \IMP, \ISIMP,
% \NIMP, \NISIMP. For coherence also \AND, \OR, \XOR,  will be defined, but
% direct use of the &&, 'and, ||, 'or', 'xor' is possible.

% The input expressions will be used for two things :

% 1. format the expression with suitable math mode symbols
% 2. evaluate logic expressions.

% **** Control sequences \IFF etc... in the input are only needed due to the
% fact that direct use of the ersatz <=, <, >, >= gives counterintuitive
% looking formulas on input. ****

% The \NOT has a **mandatory** parenthesized argument and \NOT(P), not(P),
% !(P) are equivalent.


% In the following it would have been more efficient to use "iiexpr",
% "\xintdefiifunc", "\xintiiLtorEq", etc ...but there is a bug in
% current xint (1.2e): the macros associated to == and <= by
% \xintiiexpr were forgotten for inclusion in the
% \xintdefiifunc/xintNewIIExpr mechanism. Hence we must use \xintexpr.

% Because they may be used in captions etc...
% ************ Although here I have used a \detokenize 
% ************ to allow for example also &&, hence it is
% ************ not so much needed to have the \AND etc robust
\DeclareRobustCommand*\AND{\string\AND\space}
\DeclareRobustCommand*\OR{\string\OR\space}
\DeclareRobustCommand*\NOT{\string\NOT\space}
\DeclareRobustCommand*\XOR{\string\XOR\space}
\DeclareRobustCommand*\IMP{\string\IMP\space}% <=
\DeclareRobustCommand*\ISIMP{\string\IMP\space}% >=
\DeclareRobustCommand*\NIMP{\string\NIMP\space}% >
\DeclareRobustCommand*\NISIMP{\string\NISIMP\space}% <
\DeclareRobustCommand*\IFF{\string\IFF\space}% =
%\DeclareRobustCommand*\NIFF{\string\NIFF\space}% =
\def\T{T}

% Note that use of \AND, \OR, \NOT, \XOR, is optional
% one can use 'and', 'or', 'xor' (quotes mandatory) and not or ! (no quotes,
% necessarily with parentheses not(P), !(Q) for example)
\newcommand{\TruthTableParseSyntaxOn}
% to be used in a group or environment
{\def\AND{&}% would prefer && and ||, but & diminishes the needed work in
            % AdjustPrecedences (which should not have been needed, but well).
 \def\OR{|}%
 \def\NOT{!}% used as function \NOT(P), parentheses mandatory
 \def\XOR{'xor'}% infix binary, there is also multi-variable function xor(..)
 \def\IMP{<=}\def\ISIMP{>=}\def\NIMP{>}\def\NISIMP{<}%
 \def\IFF{=}%
}
\newcommand{\TruthTableParseSyntaxOff}
{\edef\AND  {\expandafter\noexpand\csname AND \endcsname}%
\edef\OR    {\expandafter\noexpand\csname OR \endcsname}%
\edef\NOT   {\expandafter\noexpand\csname NOT \endcsname}%
\edef\XOR   {\expandafter\noexpand\csname XOR \endcsname}%
\edef\IMP   {\expandafter\noexpand\csname IMP \endcsname}%
\edef\ISIMP {\expandafter\noexpand\csname ISIMP \endcsname}%
\edef\NIMP  {\expandafter\noexpand\csname NIMP \endcsname}%
\edef\NISIMP{\expandafter\noexpand\csname NISIMP \endcsname}%
\edef\IFF   {\expandafter\noexpand\csname IFF \endcsname}%
}

% Let's now do the adjustment to infix operator precedences.

\catcode`_ 11
\catcode`& 11
\catcode`| 11
\catcode`< 11
\catcode`> 11
\catcode`= 11
\newcommand*\TruthTableAdjustPrecedences
{% to be used preferably in a group or environment as it impacts other
 % xintexpr contexts.
% The ! is always used with function syntax !(P), and as such has 
% automatically highest precedecence. Hence no change needed here for !.
 \let\XINT_expr_precedence_&  \xint_c_vi  % 6 : a high precedence
 \let\XINT_expr_precedence_|  \xint_c_v   % 5
 \let\XINT_expr_precedence_xor\xint_c_vi  % same as AND
 \let\XINT_expr_precedence_<= \xint_c_iv  % implies
 \let\XINT_expr_precedence_>= \xint_c_iv  % is implied by
 \let\XINT_expr_precedence_>  \xint_c_iv   % does not imply
 \let\XINT_expr_precedence_<  \xint_c_iv   % is not implied by
 \let\XINT_expr_precedence_=  \xint_c_iii  % iff : lowest precedence
 % Unfortunately the above is not enough because xintexpr currently
 % stupidly uses at some spot \xint_c_... constants rather than the
 % precedence control sequences above. Using etoolbox's \patchcmd is
 % not easily done due to special catcodes (in particular & must be
 % used with two distinct catcodes...) Must do brute force
 % redefinitions.
 \XINT_Redefine {&}{\xintAND}%
 \XINT_Redefine {|}{\xintOR}%
 \XINT_Redefine {xor}{\xintXOR}%
 \XINT_Redefine {<=}{\xintLtorEq}%
 \XINT_Redefine {>=}{\xintGtorEq}%
 \XINT_Redefine {<}{\xintLt}%
 \XINT_Redefine {>}{\xintGt}%
 \XINT_Redefine {=}{\xintEq}%
}%
\catcode`& 4
\catcode`| 12
\catcode`< 12
\catcode`> 12
\catcode`= 12

\def\XINT_Redefine #1{%
    \expandafter\XINT_Redefine_a
    \csname XINT_expr_until_#1_b\expandafter\endcsname
    \csname XINT_expr_until_#1_a\expandafter\endcsname
    \csname XINT_expr_precedence_#1\endcsname
}

\def\XINT_Redefine_a #1#2#3#4{%
  \def#1##1##2##3##4{\ifnum ##2>#3%
    \xint_afterfi {\expandafter #2\expandafter ##1%
     \romannumeral`\^^@\csname XINT_expr_op_##3\endcsname {##4}}%
   \else 
    \xint_afterfi {\expandafter ##2\expandafter ##3%
      \csname .=#4{\XINT_expr_unlock ##1}{\XINT_expr_unlock ##4}\endcsname }%
   \fi }%
}
\catcode`_ 8


% This section takes care of how the logic operators will be formatted
% to display the evaluated formula.
\catcode`: 11
% \newcommand{\TruthTableFormattingOn}
% % to be used in a group
% {%
%  \renewcommand*\xintAND[2]{##1\land ##2}%
%  \renewcommand*\xintOR[2]{##1\lor ##2}%
%  \renewcommand*\xintiiIsZero[1]{\neg ##1}%
%  \renewcommand*\xintXOR[2]{##1\mathbin{\mathrm{xor}}##2}%
%  \renewcommand*\xintLtorEq[2]{##1 \Rightarrow ##2}%
%  \renewcommand*\xintGtorEq[2]{##1 \Leftarrow ##2}%
%  \renewcommand*\xintLt[2]{##1 \nLeftarrow ##2}%
%  \renewcommand*\xintGt[2]{##1 \nRightarrow ##2}%
%  \renewcommand*\xintEq[2]{##1 \Leftrightarrow ##2}%
%  \renewcommand*\xintANDof:csv[1]{{\mathrm{all}(##1)}}% hide sub commas from \xintFor's sight
%  \renewcommand*\xintORof:csv[1] {{\mathrm{any}(##1)}}%
%  \renewcommand*\xintXORof:csv[1]{{\mathrm{xor}(##1)}}%
% }%

\newcommand{\TruthTableFormattingOnWithParens}
% to be used in a group
{% parentheses added to display precedence levels
% But we want to remove outermost parentheses.
% This will use a trick. The definition of \EncloseInParen below
% is done to 1) survive \edef, 2) compatible with full first expansion
% done by \UseUserFuncMacro, 3) allow easy removal of outermost ones if
% present.
% 
% ADDED NOTE: code in \TruthTable does not use an \edef anymore, hence it is
% not anymore needed for the stuff here to be \edef compatible.
% 
 \renewcommand*\xintAND[2]{\EncloseInParen{##1\land ##2}}%
 \renewcommand*\xintOR[2]{\EncloseInParen{##1\lor ##2}}%
 \renewcommand*\xintiiIsZero[1]{\neg ##1}%
 \renewcommand*\xintXOR[2]{\EncloseInParen{##1\mathbin{\mathrm{xor}}##2}}%
 \renewcommand*\xintLtorEq[2]{\EncloseInParen{##1 \Rightarrow ##2}}%
 \renewcommand*\xintGtorEq[2]{\EncloseInParen{##1 \Leftarrow ##2}}%
 \renewcommand*\xintLt[2]{\EncloseInParen{##1 \nLeftarrow ##2}}%
 \renewcommand*\xintGt[2]{\EncloseInParen{##1 \nRightarrow ##2}}%
 \renewcommand*\xintEq[2]{\EncloseInParen{##1 \Leftrightarrow ##2}}%
% There are braces here to hide commas from \xintFor on one occasion below
 \renewcommand*\xintANDof:csv[1]{{\mathrm{all}(##1)}}%
 \renewcommand*\xintORof:csv[1] {{\mathrm{any}(##1)}}%
 \renewcommand*\xintXORof:csv[1]{{\mathrm{xor}(##1)}}%
}%
\catcode`: 12

\newcommand*\EncloseInParen  {\relax\noexpand\EncloseInParen@i}
\newcommand*\FixParen [1]{\ifx\relax#1\expandafter\@gobbletwo\fi #1}
\newcommand*\EncloseInParen@i [1]
  {\mathopen{{\color{blue}\bm(}}#1\mathclose{{\color{blue}\bm)}}}

% unfortunately can't bolden delimiters directly with \bm
% \newcommand*\EncloseInParen@i [1]
%   {\mathopen{{\color{blue}\bm{\big(}}}#1\mathclose{{\color{blue}\bm{\big)}}}}
% unfortunately can't bolden delimiters with \bm
% \newcommand*\EncloseInParen@i [1]
%   {\mathopen{{\color{blue}\big(}}#1\mathclose{{\color{blue}\big)}}}


\newcommand*{\TruthTable@ZeroAndOneAsFandT}
{%
      \lccode`~`0 \lccode`F `F \lowercase{\def~{F}}%
      \lccode`~`1 \lccode`T `T \lowercase{\def~{T}}%
%      \catcode`0\active\catcode`1\active
% switch to math active, as anyhow in math mode, 
% and this spares always dangerous \scantokens
      \mathcode`0 "8000 \mathcode`1 "8000
}

% NOW TO OUR \TruthTable MACRO

\newcommand\TruthTable [2]{%
\begingroup\xintverbosetrue % for checking in the log what "stuff" is.
%
% #1 = variables, must be lowercase/uppercase single letters, comma
% separated. Use of T and F forbidden, must be "dummy" letters.
% #2 = comma separated list of expressions. Each expression may have
% (balanced) parentheses and commas, no need to hide the commas in braces.
%
% \fdef is defined in xint, it does less than \edef and is enough here.
%
  \fdef\TruthTable@Vars     {\xintCSVtoList{#1}}%
  \fdef\TruthTable@nbofvars {\xintNthElt{0}{\TruthTable@Vars}}%
  % We are not going to count expressions now, because they are comma
  % separated and possibly the comma is also in use inside as separator
  % for the arguments f the all, any, or xor functions. We could ask
  % the user to brace them, but let's be smarter.
%
   \TruthTableAdjustPrecedences
   \TruthTableParseSyntaxOn
% All the hard work will be done right now
       \xintdeffunc stuff(#1):=nuple(#2);%
   \TruthTableParseSyntaxOff
% Let's now count how many expressions we have:
  \fdef\TruthTable@nbofexprs 
     {\xintNthElt{0}{\xinttheexpr stuff(1..\TruthTable@nbofvars)\relax}}%
% We are now ready for the Table.
  \begin{longtable}
% LONGTABLE PREAMBLE
    {*{\TruthTable@nbofvars}{>{$}c<{$}}|*{\TruthTable@nbofexprs}{>{$}c<{$}}}
% LONGTABLE CAPTION
   \caption {\texttt{\protect\detokenize{#2}}}\\
%
% HEADER ROW WITH FORMATTED EXPRESSIONS (showing precedences with parentheses)
% 
   \xintListWithSep{&}{\TruthTable@Vars}% list of variables
   &%
      \TruthTableFormattingOnWithParens
% The #1 here isn't trimmed of spaces. But we will display in math mode.
      \edef\TruthTable@Formatted {\UseUserFuncMacro{stuff}{#1}}%
      \xintFor ##1 in \TruthTable@Formatted \do 
       {%
   %
   % a complication arise if we want to allow use of T and F in the input
   % syntax, because they have been converted to 1's and 0's. We could demand
   % use of \T and \F, but let's do our (dangerous) \scantokens tricks.
   % Update: I use rather math active characters, as we are in math mode.
   %
   % The "stuff" function will have simplified all things initially like (T
   % \AND T) etc... The only way for this to not happen would be to use \T and
   % \F with some suitable definitions in \TruthTableParseSyntaxOn. But these
   % definitions would depend on inner knowledge of \xintdeffunc/\xintNewExpr
   % functioning.
   %
          \TruthTable@ZeroAndOneAsFandT
          \FixParen ##1\xintifForLast{}{&}%
       }%
   \\
   \hline
%
% THE 2^N EVALUATION ROWS
% We first generate recursively the input truth values
% Later we will use \scantokens tricks to get the commas 
% to act as tabulations. This is faster than parsing them.
% It is not allowed to have ZERO variables ...
  \def\TruthTable@varlist {\do T\do F}%
  \def\do ##1{\noexpand\do {##1,T}\noexpand\do {##1,F}}
  \count@\numexpr\TruthTable@nbofvars\relax
    \xintloop
    \ifnum\count@>\@ne
        \edef\TruthTable@varlist{\TruthTable@varlist}%
        \advance\count@\m@ne
    \repeat
  \let\do\empty
  \edef\TruthTable@varlist{\TruthTable@varlist}%
  \xintFor* ##1 in \TruthTable@varlist \do 
  {%
   % first, the variables for this row. They are a comma separated list.
   % We could use an \xintFor to inserte tabulations, but let's play
   % with catcodes.
      \catcode`, 4 \makeatletter
      \scantokens {\csname @firstofone\endcsname {##1}}% 
       % \scantokens{##1} would not work (exercise...)
      &%
   % second the evaluated expressions.
   % We will obtain a comma separated list of 0's and 1's.
   % We must use \xinttheexpr rather than \UseUserFuncMacro
   % as only inside \xinttheexpr will T and F be interpreted as truth values.
      \fdef\TruthTable@B{\xinttheexpr stuff(##1)\relax}%
   % We now need to convert commas into tabs, could use \xintFor, but well.
      \catcode`, 4 \makeatletter
      \scantokens \expandafter{\expandafter
          \def\expandafter\TruthTable@B\expandafter{\TruthTable@B}%
          }%
% We played some tricks to display the computed 0's and 1's as T's and F's.
% We used at some pointactive characters,
% but it is also possible to do \lowercase.
      \lccode`0 `F \lccode`1 `T
      \lowercase\expandafter{\TruthTable@B}%
   \\%
  }%
  \end{longtable}
\endgroup
}

\makeatother

\begin{document}

% All used variables in second argument must be declared in first argument.

\TruthTable {P, Q}{T, F, P && Q, P||Q, P \XOR T, Q \XOR F}

\TruthTable {P, Q}{P \IMP Q, P \ISIMP Q, P \NIMP Q, P\NISIMP Q, P\IFF Q}

\TruthTable {P,Q}{P 'and' Q, P \AND Q, P\OR Q, P 'or' Q, P\XOR Q, P
  'xor' Q, !(P), not(P)}

\TruthTable {P,Q}{\NOT(P)\OR\NOT(Q), P\IFF Q, P\IMP Q, P\IFF T, Q\IFF F}%

\TruthTable {P,Q}{P\XOR Q, P\AND\NOT(Q)\OR\NOT(P)\AND Q,
                  P\XOR Q \IFF P\AND\NOT(Q)\OR\NOT(P)\AND Q}

\TruthTable {P, Q}{\NOT(P\AND Q)\IFF \NOT(P)\OR\NOT(Q)}

\TruthTable {P,Q}{P\IMP Q, \NOT(Q)\IMP\NOT(P), \NOT(P)\OR Q, \NOT(P\IMP Q), P \AND \NOT(Q)}

\TruthTable {P, Q}{P\IMP Q\IFF \NOT(Q)\IMP \NOT(P)}

\typeout{ICI}\TruthTable {P, Q}{P\IFF Q\IFF (P\IMP Q)\AND(Q\IMP P)}

\TruthTable {P,Q}{P\XOR \NOT(Q)\IFF (P\IFF Q), 
                  P\IMP Q\IFF \NOT (Q)\IMP \NOT(P)}


\TruthTable {P,Q,R}{P\AND Q\AND R, P\OR Q\AND R}

\TruthTable {P,Q,R,S}{xor(P,Q,R,S), (P \XOR Q) \XOR (R \XOR S)}

\TruthTable {P,Q,R}{(P \XOR Q) \XOR R \IFF P \XOR (Q \XOR R)}

\TruthTable {P, Q, R, S}{(P \IMP Q) \AND (Q \IMP R) \AND (R\IMP S) \AND (S \IMP P)\IMP (Q \IFF R)} 

\TruthTable {P, Q, R, S}{(P \OR Q\IMP R)&& (R\IMP S)\IMP (\NOT(S)\IMP\NOT(P))}

\listoftables
\end{document}

Output:

Blockquote

Blockquote

Blockquote

Blockquote