This paper was converted on www.awesomepapers.org from LaTeX by an anonymous user.
Want to know more? Visit the Converter page.

Cons-free Programs and Complexity Classes between logspace and ptime

ABCDEFGHIJKLMNOP Computer Science Department
 University of CopenhagenComputer Science Department
 University of CopenhagenDepartment of Software Science
Radboud University. NijmegenComputer Science Department
 University of Copenhagen
   Neil D. Jones Computer Science Department
 University of Copenhagen [email protected] Computer Science Department
 University of CopenhagenDepartment of Software Science
Radboud University. NijmegenComputer Science Department
 University of Copenhagen
   ABCDEFGHIJKLMNOP Computer Science Department
 University of CopenhagenDepartment of Software Science
Radboud University. NijmegenComputer Science Department
 University of Copenhagen
   Siddharth Bhaskar Computer Science Department
 University of Copenhagen [email protected] Department of Software Science
Radboud University. NijmegenComputer Science Department
 University of Copenhagen
   Cynthia Kop Department of Software Science
Radboud University. Nijmegen [email protected] Computer Science Department
 University of Copenhagen
   Jakob Grue Simonsen Computer Science Department
 University of Copenhagen [email protected]
Abstract

Programming language concepts are used to give some new perspectives on a long-standing open problem: is logspace = ptime ?

Introduction

“P =? NP” is an archetypical question in computational complexity theory, unanswered since its formulation in the 1970s. The question: Is the computional power of polynomially time-bounded programs increased by adding the ability to “guess” (i.e., nondeterminism) ? This is interesting because “polynomial time” is a plausible candidate for “feasibly solvable”.

Perhaps the second most important question is “L =? P”: whether logspace=ptime\mbox{\sc logspace}=\mbox{\sc ptime}. Here L is the set of problems solvable by cursor programs. These also run in polynomial time, but have no rewritable storage111One take: a cursor program is a multihead two-way read-only finite automaton. A more classical but equivalent version: a 2-tape Turing machine with nn-bit read-only input tape 1, that uses at most O(logn)O(\log n) bits of storage space on read-write tape 2.. Both questions remain open since Cook and Savitch’s pathbreaking papers in the 1970s [4, 16].

We investigate the question “L =? P” from the viewpoint of functional programming languages: a different viewpoint than Turing machines. The link is earlier characterisations of L and P by “cons-free” programs [7, 8, 9]. The net result: a deeper and finer-grained analysis, illuminated by perspectives both from programming languages and complexity theory.

Some new definitions and theorems give fresh perspectives on the question L =? P. We use programs to define and study complexity classes between the two. By [7, 8, 9] cursor programs exactly capture the problem class L; and cursor programs with recursive function definitions exactly capture the problem class P. A drawback though is that recursive cursor programs can run for exponential time, even though they exactly capture the decision problems that can be solved in polynomial time by Turing machines.

The goal of this paper is to better understand the problems in the interval between classes L and P. Problem class NL is already-studied in this interval, and it is the logspace analog of similar long-standing open problems. Kuroda’s two “LBA problems” posed in 1964 [12]: (1) Is dspace(nn) =? nspace(nn) and (2) Is nspace(nn) closed under complementation? After both stood unresolved for 23 years, (2) was finally answered ”yes” (independently in 1987) by Immerman and by Szelepcsényi [6, 17]: NL and larger nondeterministic space classes (with constructive bounds) are closed under complementation.222Kuroda’s other LBA problem dspace(nn) =? nspace(nn) is still open, as well as the question L =? NL.

We study the problems solvable by an in-between class CFpoly: recursive cursor programs that run in polynomial time. Recursion is in some sense orthogonal to the ability to make nondeterministic choices, i.e., to “guess”. The class CFpoly seems more natural than NL from a programming perspective.

1 Complexity by Turing machines and by programming languages

1.1 Overview

Let X{0,1}X\subseteq\{0,1\}^{*} be a set of bit strings. The decision problem for XX: given x{0,1}x\in\{0,1\}^{*}, to decide whether or not xXx\in X. We say that XX is in ptime iff it is decidable by a 1-tape deterministic Turing machine that runs within polynomial time O(nk)O(n^{k}) [here n=|x|n=|x| is the length of its input xx, and kk is a constant independent of xx]. Further, XX is in logspace iff it is decidable by a 2-tape Turing machine that uses at most O(logn)O(\log n) bits of storage space on tape 2, assuming its nn-bit input is given on read-only tape 1. Both problem classes are of practical interest as they are decidable by programs with running times bounded by polynomial functions of their input lengths. The essential difference is the amount of allowed storage space. These classes are invariant across a wide range of variations among computation models, and it is easy to see that logspaceptime\mbox{\sc logspace}\subseteq\mbox{\sc ptime}.

However, the question: is logspaceptime?\mbox{\sc logspace}\subsetneq\mbox{\sc ptime}? has stood open for many years.

Programs and problem decision. Semantics: a program computes a (partial!) function from bit strings to bits:

[[𝚙]]:{0,1}{0,1}{}\mbox{$[[$}{\tt p}\mbox{$]]$}:\{0,1\}^{*}\to\{0,1\}\cup\{\bot\}

Program semantics is call-by-value; and [[𝚙]](x)=\mbox{$[[$}{\tt p}\mbox{$]]$}(x)=\bot means: p does not terminate on input xx.

A set X{0,1}X\subseteq\{0,1\}^{*} is decided by a program p if p terminates on all inputs x{0,1}x\in\{0,1\}^{*}, and for any xx,

[[𝚙]](x)={1if xX0if xX\mbox{$[[$}{\tt p}\mbox{$]]$}(x)=\left\{\begin{array}[]{ll}1&\mbox{if $x\in X$}\\ 0&\mbox{if $x\notin X$}\end{array}\right.

Complexity by cons-free programs. We use programming languages (several in this paper) to explore the interesting boundary zone between the problem classes logspace and ptime. Strong links were established in [8, 9]: Each class was characterised by a small general recursive functional programming language. The one language (called CF for “cons-free”) is limited to programs without data constructors.333“Cons-free” is not to be confused with “context-free”. TR stands for “tail recursive”. Data access is read-only in both of our languages, so neither CF nor CFTR is Turing-complete. The other, named CFTR, is identical to CF but has restricted control, allowing only tail recursive calls to defined functions.444These ideas stem from S. A. Cook’s seminal work on complexity classes and pushdown machines [4]. Papers [8, 9] re-express and adapt Cook’s ideas to a cons-free programming context. Paper [9] extends [4], characterising decision powers of higher-order cons-free programs. CFTR is (yet another) version of the “cursor programs” mentioned in the Introduction. From [9] we know:

Theorem 1

logspace={X{0,1}|some CFTR program decides X}\mbox{\sc logspace}=\{X\subseteq\{0,1\}^{*}\ |\ \mbox{some CFTR program decides }X\}

Theorem 2

ptime={X{0,1}|some CF program decides X}\mbox{\sc ptime}=\{X\subseteq\{0,1\}^{*}\ |\ \mbox{some CF program decides }X\}

A compact notation relating programs and problems

Definition 1

{{\mathbf{\{\{}L}}\mathbf{\}\}} is the set of all problems (i.e., sets X{0,1}X\subseteq\{0,1\}^{*}) that are decidable by L-programs:

{{L}}=def{X{0,1}| some L-program p decides X}\mbox{\mbox{$\mathbf{\{\{}$}\rm{L}\mbox{$\mathbf{\}\}}$}}\ \ \stackrel{{\scriptstyle def}}{{=}}\ \ \{X\subseteq\{0,1\}^{*}\ |\ \mbox{\ some {\rm L}-program {\tt p} decides\ }X\}

Theorems 1 and 2 can thus be restated as: logspace={{CFTR}}{{CF}}=ptime\mbox{\sc logspace}=\mbox{\mbox{$\mathbf{\{\{}$}\rm{CFTR}\mbox{$\mathbf{\}\}}$}}\subseteq\mbox{\mbox{$\mathbf{\{\{}$}\rm{CF}\mbox{$\mathbf{\}\}}$}}=\mbox{\sc ptime}

1.2 The cons-free programming language CF

All programs are first-order in this paper,555The larger language described in [9] encompasses both first and higher-order programs. and deterministic to begin with (until nondeterminism is expressly added in Section 5). First, a brief overview (details in [7, 8, 9]):

Definition 2

Program syntax: a CF-program is a finite sequence of mutually recursive function definitions. Write CF programs and fragments in teletype, e.g., program p or expression tail(f x y). The first definition, of form 𝚏𝟷𝚡=𝚎𝚏𝟷{\tt f_{1}\ x=e^{f_{1}}}, defines the program entry function (with one argument, always named x). A context-free grammar for programs, definitions and expressions e:


  • p ::= def | p def -- Program = sequence of function definitions
    def ::= f x1...xm = e -- (where m >= 0)
    e ::= True | False | [] | xi | base | if e then e else e | f e1...em
    base ::= not e | null e | head e | tail e -- Base function call

Data types: bits and bit lists. Variable and expression values are elements of one of the value sets {0,1}\{0,1\} or list {0,1}\{0,1\}^{*}. A string x{0,1}x\in\{0,1\}^{*} is a bit list, and list 10111011 can be written [1,0,1,1] as data value. [] is the empty list, and b:bs is the result of prepending bit b to list bs. We sometimes identify 0, 1 with False and True, resp. e.g., in the test position e0 of an expression if e0 then e1 else e2.

Expressions: Expression e may be a constant; a variable; a base function call (not, null, head, or tail); a conditional if e0 then e1 else e2; or a call f e1...er to some program-defined function f. A program contains no CONS function or other constructors (hence CF for cons-free). Thus CF does not have successor, +,+,* or explicit storage allocators such as malloc in CC, :: in ML, or : in Haskell.

Function definition: A program-defined definition has form f x1 x2...xm = e with m0m\geq 0. No function may be defined more than once, left-side variables must be distinct, and any variable appearing on the right side (in e) must be one of x1,…,xm.

Semantics: The semantics of CF is given by the inference rules in Figure 1. Given a program p and input xx, these rules define a (unique) computation tree that we call 𝒯𝚙,x{\cal T}^{{\tt p},x}. The inference rules define statements [[𝚙]](x)=v\mbox{$[[$}{\tt p}\mbox{$]]$}(x)=v (at the root of the computation tree) or 𝚙,ρ𝚎v{\tt p},\rho\vdash{\tt e}\to v. The p in 𝚙,ρ𝚎v{\tt p},\rho\vdash{\tt e}\to v is a CF program. The e is a subexpression of the right side of some function definition. Further, ρ\rho is an environment that binds the current program variables to respective argument values v1,,vmv_{1},\ldots,v_{m}; and vv is the result value of e. Base or defined function calls are evaluated using call-by-value.

Axioms:

                𝚙,ρ𝚡ρ(𝚡){\tt p},\rho\vdash{\tt x}\to\rho({\tt x})

                  𝚙,ρ𝚃𝚛𝚞𝚎𝚃𝚛𝚞𝚎{\tt p},\rho\vdash{\tt True}\to{\tt True}

                  𝚙,ρ𝙵𝚊𝚕𝚜𝚎𝙵𝚊𝚕𝚜𝚎{\tt p},\rho\vdash{\tt False}\to{\tt False}

              𝚙,ρ[][]{\tt p},\rho\vdash{\tt[]}\to[]

Base functions:

     𝚙,ρ𝚎𝙵𝚊𝚕𝚜𝚎{\tt p},\rho\vdash{\tt e}\to{\tt False}     𝚙,ρ𝚗𝚘𝚝𝚎𝚃𝚛𝚞𝚎{\tt p},\rho\vdash{\tt not}\ {\tt e}\to{\tt True}

      𝚙,ρ𝚎𝚃𝚛𝚞𝚎{\tt p},\rho\vdash{\tt e}\to{\tt True}     𝚙,ρ𝚗𝚘𝚝𝚎𝙵𝚊𝚕𝚜𝚎{\tt p},\rho\vdash{\tt not}\ {\tt e}\to{\tt False}

        𝚙,ρ𝚎[]{\tt p},\rho\vdash{\tt e}\to{\tt[]}     𝚙,ρ𝚗𝚞𝚕𝚕𝚎𝚃𝚛𝚞𝚎{\tt p},\rho\vdash{\tt null}\ {\tt e}\to{\tt True}

      𝚙,ρ𝚎v1:v2{\tt p},\rho\vdash{\tt e}\to v_{1}{\tt:}v_{2}     𝚙,ρ𝚗𝚞𝚕𝚕𝚎𝙵𝚊𝚕𝚜𝚎{\tt p},\rho\vdash{\tt null}\ {\tt e}\to{\tt False}

     𝚙,ρ𝚎v1:v2{\tt p},\rho\vdash{\tt e}\to v_{1}{\tt:}v_{2}     𝚙,ρ𝚑𝚎𝚊𝚍𝚎v1{\tt p},\rho\vdash{\tt head}\ {\tt e}\to v_{1}

    𝚙,ρ𝚎v1:v2{\tt p},\rho\vdash{\tt e}\to v_{1}{\tt:}v_{2}     𝚙,ρ𝚝𝚊𝚒𝚕𝚎v2{\tt p},\rho\vdash{\tt tail}\ {\tt e}\to v_{2}

Condition:

  𝚙,ρ𝚎𝟶𝚃𝚛𝚞𝚎{\tt p},\rho\vdash{\tt e_{0}}\to{\tt True}         𝚙,ρ𝚎𝟷v{\tt p},\rho\vdash{\tt e_{1}}\to v     𝚙,ρ𝚒𝚏𝚎𝟶𝚝𝚑𝚎𝚗𝚎𝟷𝚎𝚕𝚜𝚎𝚎𝟸v{\tt p},\rho\vdash{\tt if\ e_{0}\ then\ e_{1}\ else\ e_{2}}\to v

  𝚙,ρ𝚎𝟶𝙵𝚊𝚕𝚜𝚎{\tt p},\rho\vdash{\tt e_{0}}\to{\tt False}         𝚙,ρ𝚎𝟸v{\tt p},\rho\vdash{\tt e_{2}}\to v     𝚙,ρ𝚒𝚏𝚎𝟶𝚝𝚑𝚎𝚗𝚎𝟷𝚎𝚕𝚜𝚎𝚎𝟸v{\tt p},\rho\vdash{\tt if\ e_{0}\ then\ e_{1}\ else\ e_{2}}\to v

Function call:

  𝚙,ρ𝚎1w1{\tt p},\rho\vdash{\tt e}_{1}\to w_{1}  … 𝚙,ρ𝚎mwm{\tt p},\rho\vdash{\tt e}_{m}\to w_{m}         𝚙,[𝚡𝟷w1,,𝚡𝚖wm]𝚎𝚏v{\tt p},[{\tt x1}\mapsto w_{1},\dots,\,{\tt xm}\ \mapsto w_{m}]\vdash{\tt e}^{\tt f}\to v    if 𝚏𝚡𝟷𝚡𝚖=𝚎𝚏𝚙{\tt f\,x1\dots xm=e^{f}}\in{\tt p}                                   𝚙,ρ𝚏𝚎𝟷𝚎𝚖v{\tt p},\rho\vdash{\tt fe_{1}\dots e_{m}}\to v

Program running:

  𝚙,[𝚡𝑖𝑛𝑝𝑢𝑡]𝚎𝚏𝟷𝚟{\tt p},[\tt x\mapsto\mathit{input}]\vdash{\tt e}^{{\tt f}_{1}}\to v    If 𝚏1x=𝚎𝚏1{\tt f}_{1}\ x={\tt e}^{{\tt f}_{1}} is the entry function definition of 𝚙{\tt p}     [[𝚙]](𝑖𝑛𝑝𝑢𝑡)=v\mbox{$[[$}{\tt p}\mbox{$]]$}(\mathit{input})=v

Figure 1: Inference rules for CF

The full inference rule set is given in Figure 1. It is essentially the first-order part of Fig. 1 in [9].

Example 1

The CF program parity decides membership in the set X={x{0,1}||x| is even}X=\{x\in\{0,1\}^{*}\ \ |\ \ |x|\mbox{\rm\ is even}\}.


  • entry x = even x
    even z = if (null z) then True else not(even(tail z))

This satisfies [[parity]](x)=𝚃𝚛𝚞𝚎\mbox{$[[$}\mbox{\tt parity}\mbox{$]]$}(x)={\tt True} if |x||x| is even, else False. For example, [[parity]]([𝟷,𝟶,𝟷])=𝙵𝚊𝚕𝚜𝚎\mbox{$[[$}\mbox{\tt parity}\mbox{$]]$}({\tt[1,0,1]})={\tt False}.

1.3 The computation tree 𝒯𝚙,x{\cal T}^{{\tt p},x} and an evaluation order

Evaluation steps: The computation tree 𝒯𝚙,x{\cal T}^{{\tt p},x} can be built systematically, applying the rules of Figure 1 bottom-up and left-to-right. Initially we are given a known program p and 𝑖𝑛𝑝𝑢𝑡{0,1}\mathit{input}\in\{0,1\}^{*}, and the computation goal is [[𝚙]](input)=?\mbox{$[[$}{\tt p}\mbox{$]]$}(input)=?, where ?? is to be filled in with the appropriate value vv (if it exists). Intermediate goals are of form 𝚙,ρ𝚎?{\tt p},\rho\vdash{\tt e}\to?, where p, ρ\rho and e are known, and ? is again to be filled in.

Axioms have no premises at all, and base function calls have exactly one premise, making their tree construction straightforward. The inference rules for conditional expressions have two premises, and a call to an mm-ary function has m+1m+1 premises. We choose to evaluate the premises left-to-right. For the case if e0 then e1 else e2 ?\to?, there are two possibly applicable inference rules. However both possibilities begin by evaluating e0: finding vv such that e0 v\to v. Once vv is known, only one of the two if rules can be applied.

Claim. the evaluation order above for given p and 𝑖𝑛𝑝𝑢𝑡\mathit{input} is general: if [[𝚙]](input)=v\mbox{$[[$}{\tt p}\mbox{$]]$}(input)=v is deducible by any finite proof at all, then the evaluation order will terminate with result [[𝚙]](input)=v\mbox{$[[$}{\tt p}\mbox{$]]$}(input)=v at the root.

A consequence: tree 𝒯𝚙,x{\cal T}^{{\tt p},x} is unique if it exists, so CF is a deterministic language. (Nondeterministic variants of CF will considered later.)

1.4 The suffix property

CF programs have no constructors. This implies that all values occurring in the tree 𝒯𝚙,x{\cal T}^{{\tt p},x} must be boolean values, or suffixes of the input. Expressed more formally:

Definition 3

For program p and input x{0,1}x\in\{0,1\}^{*}, define its range of variation and its reachable calls by

Vx={0,1}𝑠𝑢𝑓𝑓𝑖𝑥𝑒𝑠(x) and𝑅𝑒𝑎𝑐ℎ𝚙(x)={(𝚏,ρ)|𝚎𝚏 is called at least once with environment ρ while computing [[𝚙]](x)}\begin{array}[]{lclc}V_{x}&=&\{0,1\}\cup\mathit{suffixes}(x)\mbox{\rm\ and}\\ \vspace{1mm}\par\mathit{Reach}_{\tt p}(x)&=&\{({\tt f},\rho)\ |\ {\tt e}^{\tt f}\mbox{\rm\ is called at least once with environment $\rho$ while computing $\mbox{$[[$}{\tt p}\mbox{$]]$}(x)$}\}\end{array}

Lemma 1

The computation tree 𝒯𝚙,x{\cal T}^{{\tt p},x} is finite iff p terminates on input xx.

Lemma 2

If 𝒯𝚙,x{\cal T}^{{\tt p},x} contains 𝚙,ρ𝚎v{\tt p},\rho\vdash{\tt e}\to v then vVxv\in V_{x} and ρ(𝚣)Vx\rho({\tt z})\in V_{x} for every 𝚣𝑑𝑜𝑚𝑎𝑖𝑛(ρ){\tt z}\in\mathit{domain}(\rho).

Proof: a straightforward induction on the depth of computation trees. If the entry function definition is f x = ef, then the root [[𝚙]](x)=v\mbox{$[[$}{\tt p}\mbox{$]]$}(x)=v has a parent of the form 𝚙,ρ0𝚎𝚏v{\tt p},\rho_{0}\vdash{\tt e}^{\tt f}\to v where the initial environment is ρ0=[𝚡x]\rho_{0}=[{\tt x}\mapsto x]. Of course xx is a suffix of xx. At any non-root node in the computation tree, any new value must either be a Boolean constant or [], or constructed by a base function not, head, tail, or null from a value already proven to be in VxV_{x}.

Consequence: a CF computation has at most polynomially many different function call arguments.

Lemma 3

For any CF program p there is a polynomial π(|x|)\pi(|x|) that bounds #𝑅𝑒𝑎𝑐ℎ𝚙(x)\#\mathit{Reach}_{\tt p}(x) for all x{0,1}x\in\{0,1\}^{*}.

Proof: By Lemma 2, any argument of any mm-ary p function f has at most #Vx=3+|x|\#V_{x}=3+|x| possible values, so the number of reachable argument tuples for f can at most be (3+|x|)m(3+|x|)^{m}.

Remark: this does not necessarily imply that p’s running time is polynomial in |x||x|. We say more on this in Section 2.

1.5 The tail recursive programming language CFTR

A CFTR program is a CF program p such that every function call occurring on the right side of any defined function is in tail form. This is a syntactic condition; the semantic purpose is to enable a no-stack CFTR implementation, as outlined in Section 2.1. The operational intention is that if a call f e1...em is in tail form then, after the call has been performed, control is immediately returned from the current function: there is “nothing more to do” after the call.

Definition 4

Define function α:Exp{X,T,N}\alpha:Exp\to\{X,T,N\} as follows, where the set of descriptions is ordered by X<T<NX<T<N. The intention: if expression e contains no function calls then α(𝚎)=X\alpha({\tt e})=X, else if all function calls in e are in tail form then α(𝚎)=T\alpha({\tt e})=T, else if e contains a function call in one or more non-tail-call positions then α(𝚎)=N\alpha({\tt e})=N.

α(𝚎)=Xif e is a constantα(𝚋𝚊𝚜𝚎𝚎)=Xif α(𝚎)=Xα(𝚋𝚊𝚜𝚎𝚎)=Nif α(𝚎)=T or α(𝚎)=Nα(𝚏𝚎𝟷𝚎𝚖)=Tif α(𝚎𝟷)==α(𝚎𝚖)=Xα(𝚏𝚎𝟷𝚎𝚖)=Notherwiseα(𝚒𝚏𝚎𝟶𝚝𝚑𝚎𝚗𝚎𝟷𝚎𝚕𝚜𝚎𝚎𝟸)=max(α(𝚎𝟷),α(𝚎𝟸))if α(𝚎𝟶)=Xα(𝚒𝚏𝚎𝟶𝚝𝚑𝚎𝚗𝚎𝟷𝚎𝚕𝚜𝚎𝚎𝟸)=Notherwise\begin{array}[]{lcll}\alpha({\tt e})&=&X&\mbox{if {\tt e} is a constant}\\ \alpha({\tt base\ e})&=&X&\mbox{if $\alpha({\tt e})=X$}\\ \alpha({\tt base\ e})&=&N&\mbox{if $\alpha({\tt e})=T$ or $\alpha({\tt e})=N$}\\ \alpha({\tt f\ e1...em})&=&T&\mbox{if\ }\alpha({\tt e1})=\ldots=\alpha({\tt em})=X\\ \alpha({\tt f\ e1...em})&=&N&\mbox{otherwise}\\ \alpha({\tt if\,e0\ then\ e1\ else\ e2})&=&\max(\alpha({\tt e1}),\alpha({\tt e2}))&\mbox{if\ }\alpha({\tt e0})=X\\ \alpha({\tt if\,e0\ then\ e1\ else\ e2})&=&N&\mbox{otherwise}\\ \end{array}

Definition 5

CF program p is in CFTR iff α(𝚎){X,T}\alpha({\tt e})\in\{X,T\} for all function definitions f x1...xm = e.

Some instances in the program parity, to clarify the definition:

  1. 1.

    α(𝚗𝚞𝚕𝚕𝚣)=α(𝚝𝚊𝚒𝚕𝚣)=X\alpha({\tt null\ z})=\alpha({\tt tail\ z})=X. Neither calls any program-defined functions.

  2. 2.

    Expression even x is a call, and is in tail form: α(𝚎𝚟𝚎𝚗𝚡)=T\alpha({\tt even\ x})=T.

  3. 3.

    The expression even(tail z) is in tail form: α(𝚎𝚟𝚎𝚗(𝚝𝚊𝚒𝚕𝚣))=T\alpha({\tt even(tail\ z)})=T.

  4. 4.

    The expression not(even(tail z)) is not in tail form: α(𝚗𝚘𝚝(𝚎𝚟𝚎𝚗(𝚝𝚊𝚒𝚕𝚣)))=N\alpha({\tt not(even(tail\ z)}))=N.

By point 4, parity is not a CFTR program. However the set XX it decides can be decided by the following alternative program whose recursive calls are in tail form. Thus XX is CFTR decidable.

Example 2

Program parity


  • entry x  = f x True
    f x y = if (null x) then y else f (tail x) (not y)

A very useful result from [2] is the following: we can assume, without loss of generality, that a CF program contains no nested function calls such as f x1...xm = ...g(h(e))....

Lemma 4

For any CF program p there is a CF program p such that [[[[p]]]] = [[[[p]]]], and for each function definition 𝚏𝚡𝟷𝚡𝚖=𝚎𝚏\tt f\ x1\ ...\ xm=e^{f} in p, either α(𝚎𝚏)T\alpha({\tt e^{f}})\leq T, or 𝚎𝚏=𝚒𝚏𝚎𝟶𝚝𝚑𝚎𝚗𝚎𝟷𝚎𝚕𝚜𝚎𝚎𝟸{\tt e^{f}}={\tt if\ e_{0}\ then\ e_{1}\ else\ e_{2}} and α(𝚎𝚒)\alpha({\tt e_{i}})\leq T for i{0,1,2}i\in\{0,1,2\}.

An interesting step in the proof of Lemma 4 is to show that a function f:XYf:X\to Y is CF-computable if and only if its graph Gf={(x,y)X×Y|f(x)=y}G^{f}=\{(x,y)\in X\times Y\ |\ f(x)=y\} is CF-decidable (see [2] for details). In a CFTR program, no function calls are allowed to occur in the test part of a tail-recursive conditional expression (by the last line of the α\alpha definition). Thus Lemma 4 does not rule out a call nested in a conditional’s test part, and so does not imply that p is tail-recursive.

An example that suggests that some call nesting is essential: the MCVMCV program of Section 2.3 has function calls inside if tests. It is well-known that the function that MCVMCV computes can be computed tail-recursively if and only if logspace=ptime [15].

2 About time and space measures

Theorems 1 and 2 relate computations by two rather different computation models: cons-free programs and Turing machines. We now focus on time and space relations between the two. We define the time and space used to run given CF program p on input x{0,1}x\in\{0,1\}^{*}:

Definition 6
  • time𝚙(x){\mathit{t}ime}_{\tt p}(x) is the number of evaluation steps to run program p on xx (as in Section 1.3).

  • space𝚙(x){\mathit{s}pace}_{\tt p}(x) is the number of bits required to run p on input xx.

Both time𝚙(x){\mathit{t}ime}_{\tt p}(x) and space𝚙(x){\mathit{s}pace}_{\tt p}(x) are non-negative integers (or \bot for nontermination). We call time𝚙(x){\mathit{t}ime}_{\tt p}(x) the “native time” for CF or CFTR (in contrast to Turing machine time). For the example, time𝚙𝚊𝚛𝚒𝚝𝚢(x)=O(|x|){\mathit{t}ime}_{\tt parity}(x)=O(|x|). It is reasonably simple to define running time as time𝚙(x)=|𝒯𝚙,x|{\mathit{t}ime}_{\tt p}(x)=|{\cal T}^{{\tt p},x}|, i.e., the number of nodes in the computation tree 𝒯𝚙,x{\cal T}^{{\tt p},x}.

A full definition of space𝚙(x){\mathit{s}pace}_{\tt p}(x) requires a lower-level execution model than the abstract semantics above. (Section 2.1 will show a bit more detail.) Consider the parity example. The length of the call stack needed to implement the program is linear in the depth of the proof tree, and so O(|x|)O(|x|). Moreover, each non-root node has an environment ρ\rho that binds x or z to its value. By Lemma 2 any such value is a suffix of the input. Any suffix can be represented in O(log|x|)O(\log|x|) bits, so the total space that parity uses is

space𝚙𝚊𝚛𝚒𝚝𝚢(x)=O(|x|log|x|){\mathit{s}pace}_{\tt parity}(x)=O(|x|\log|x|)

2.1 Space usage: a lower-level operational semantics, and the tail call optimisation

To more clearly define space usage and present the tail call optimisation, we use a finer, more operational level of detail based on traditional implementations666Readers may think of machine-code run-time states, e.g., value stacks, stack frames,…; or of tapes and state transition relations. Such implementation paraphernalia are well-known both to compiler writers and programming language theorists.. Programs are executed sequentially as in the proof-tree-building algorithm seen earlier. The expression on the right side of a function definition is evaluated one step at a time as in Section 1.3; but with an extra data structure, a stack σ\sigma used to record some of the environments seen before. The stack’s topmost activation record corresponds closely to the current environment ρ\rho.

Each defined function f x1...xm = e has an activation record format that contains the values v1,,vmv_{1},\ldots,v_{m} of the arguments with which f has been called (plus shorter-lived “temporary results,” i.e., as-yet-unused subexpression values). On any call to f, a new activation record is pushed onto the stack σ\sigma. This activation record will be popped when control returns from the call.

Definition 7

The call history for CF program p and input x{0,1}x\in\{0,1\}^{*} is a sequence (finite or infinite) of configurations, each of form (𝚏,ρ)𝑅𝑒𝑎𝑐ℎ𝚙(x)({\tt f},\rho)\in\mathit{Reach}_{\tt p}(x) where f is a defined function name and ρ\rho is an environment. The first configuration always has form (𝚙𝚎𝚗𝚝𝚛𝚢,[𝚣x[)({\tt p-entry},[{\tt z}\mapsto x[).

How the syntactic CFTR condition in Definition 5 allows more efficient implementation:

The tail call optimisation

Claim: every CFTR program can be implemented using at most one stack frame at a time.

Suppose CF program p has a definition f x1...xr = ...(g e1...es)... where the g call is a tail call. If so, then after the call (g e1...es) has been started there can be no future references to the current values of x1,...,xr. Informally, there is “nothing more to do” after a tail call has been completed. (One can review the parity’ and parity examples in this light.)

The tail call optimisation: The new activation record for g overwrites f’s current activation record. In a CFTR program every call will be a tail call. Thus there is never more than one frame at a time in the runtime stack. One stack frame requires O(log|x|)O(\log|x|) bits. For example, this gives parity a major improvement over the space used by parity: space𝚙𝚊𝚛𝚒𝚝𝚢(x)=O(log|x|){\mathit{s}pace}_{{\tt parity}^{\prime}}(x)=O(\log|x|) bits.

A relaxation of the tail call restriction. Sketch (details in [2]): It is sufficient that the functions defined in p can be given a partial order << such that no call is to another that is earlier in the order, and recursive calls must be tail calls. Under this lighter restriction there may be more than one frame at a time in the runtime stack; but these frames are properly ordered by <<. Consequently every restricted program can be implemented within a constant depth of stack frames.

2.2 CFTR programs run in polynomial time; but CF can take exponential time

Theorem 3

Any terminating CFTR program runs in (native) time polynomial in |x||x|.

Proof: in the call history of p on input xx, all runtime configurations must be distinct (else the program is in an infinite loop). There is a constant bound on the number of stack frames. Thus, by Lemma 3 there are only polynomially many distinct runtime configurations.

Theorem 4

A terminating run of a CF program can take time exponential in its input length.

First, a trivial example with non-polynomial running time (from [9]):

Example 3

  • f x = if (null x) then True else
    if f(tail x) then f(tail x) else False

It is easy to see that 𝑡𝑖𝑚𝑒𝚚(x)=2O(|x|)\mathit{time}_{\tt q}(x)=2^{O(|x|)} due to the repeated calls to tail x. Exponential time is not necessary, though, since the computed function satisfies f(x) = True.

2.3 A more fundamental example: a CF-decidable problem that is ptime-complete

MCVMCV is the Monotone Circuit Value Problem. Technically it is the set of all straight-line Boolean programs whose last computed value is True, e.g.,

x0:=𝙵𝚊𝚕𝚜𝚎;x1:=𝚃𝚛𝚞𝚎;x2:=x1x0;x3:=x2x0;x4:=x3x2;x5:=x4x3x_{0}:={\tt False};\ x_{1}:={\tt True};\ x_{2}:=x_{1}\lor x_{0};\ x_{3}:=x_{2}\land x_{0};\ x_{4}:=x_{3}\lor x_{2};\ x_{5}:=x_{4}\lor x_{3}

MCVMCV is a well-known “hardest” problem for ptime [15]. In complexity theory, completeness means two things: (1) MCVptimeMCV\in\mbox{\sc ptime}; and (2) if X{0,1}X\subseteq\{0,1\}^{*} is any problem in ptime, then XX is logspace-reducible777Details may be found in [7], Chapters 25, 26. to MCVMCV. By transitivity of logspace-reduction, MCVlogspaceMCV\in\mbox{\sc logspace} if and only if logspace=ptime\mbox{\sc logspace}=\mbox{\sc ptime}.

Following is the core of a Haskell program to decide membership in MCVMCV, together with a sample run. We chose a compact Boolean program representation, so the program above becomes a list:


  • program = [ OR 5 4 3, OR 4 3 2, AND 3 2 0, OR 2 1 0 ]

Haskell encoding: Assignment xi:=xjxkx_{i}:=x_{j}\land x_{k} is represented by omitting :=:=, and coding \land as AND in prefix position (and similarly for \lor). The program is presented in reverse order; and variables X0, X1 always have predefined values False, True resp., so their assignments are omitted in the representation.


  • type Program = [Instruction]
    data Instruction = AND Int Int Int | OR Int Int Int

    mcv :: Program -> Bool
    vv :: (Int,Program) -> Bool

    mcv ((OR lhs x y):s) = if vv(x,s) then True else vv(y,s)
    mcv ((AND lhs x y):s) = if vv(x,s) then vv(y,s) else False

    vv(0,s) = False -- vv(v,s) = value of variable v at program suffix s
    vv(1,s) = True --
    vv(v,s) = case s of
    ((AND lhs x y):s’) -> if v==lhs then mcv(s) else vv(v,s’)
    ((OR lhs x y):s’) -> if v==lhs then mcv(s) else vv(v,s’)

This works by recursive descent, beginning at the end of the program; and uses no storage at all (beyond the implicit implementation stack). Following is a trace of nontrivial calls to vv, ended by the final result of running p. Note that variable X2 is evaluated repeatedly.


  • program = [X5:=X4 OR X3, X4:=X3 OR X2, X3:=X2 AND X0, X2:=X1 OR X0]

    vv(X4,[ X4 := X3 OR X2, X3 := X2 AND X0, X2 := X1 OR X0])
    vv(X3,[ X3 := X2 AND X0, X2 := X1 OR X0])
    vv(X2,[ X2 := X1 OR X0])
    vv(X2,[ X3 := X2 AND X0, X2 := X1 OR X0])
    vv(X2,[ X2 := X1 OR X0])

    True

Re-expressing the Boolean program as a bit string for CF input

Of course CF programs do not have data values of type Int. However, any straight-line Boolean program can be coded by a CF bit string. Example: the Boolean program above can be coded as a 44-bit string888The -- parts delimit comments, and are not part of the Boolean program’s bit-string encoding.:


  • [1,1,1,0, 1,0,1, 0, 1,0,0, 0,1,1, -- x5 := OR x4 x3
    1,0,0, 0, 0,1,1, 0,1,0, -- x4 := OR x3 x2
    0,1,1, 1, 0,1,0, 0,0,0, -- x3 := AND x2 x0
    0,1,0, 0, 0,0,1, 0,0,0] -- x2 := OR x1 x0

Bag of tricks: The program has 5 variables. The index i of any variable xi can be coded by a 33-bit binary sequence we denote by 𝚒^\hat{\tt i}, e.g., x3 is coded by 𝟹^\hat{\tt 3} = 0,1,1. This has length 33 since 3=log253=\lceil\log_{2}5\rceil.

The beginning 1,1,1,0 of the program code gives (in unary) the code block length, 33 for this program. A Boolean assignment xi := xj op xk is coded as 𝚒^𝚘𝚙^𝚓^𝚔^\hat{\tt i}\ \hat{\tt op}\ \hat{\tt j}\ \hat{\tt k}, where the Boolean operators ,\lor,\land are coded as 0, 1 respectively.

Using this encoding, it is not hard to transform the Haskell-like program to use if-then-else statements rather than case; and then implement it in the true CF language.

A dramatic contrast in running times

  • The CF program just sketched has repeated (and nested) function calls with the same argument values. For example, Boolean variable X2 in program p is evaluated 3 times in the trace above. More generally, the running time bound is exponential:

    𝑡𝑖𝑚𝑒𝚖𝚌𝚟(x)=2O(|x|)\mathit{time}_{\tt mcv}(x)=2^{O(|x|)}
  • On the other hand, one can show that MCVMCV can be decided by a Turing machine in polynomial time: Execute the Boolean instructions from the beginning, store the values of left side variables when computed, and refer to stored variable values as needed.

This major difference is due to data storage: the Turing machine tape can save already-computed values, to be looked up as needed. A CF program has no such storage, and must resort to recomputation999Duplicating CF variables does not suffice, since the number of variables is independent of the length of the input data..

Is this contrast necessary? It seems strange that the cost of strengthening tail recursive programs (in CFTR) by adding recursion (to get CF) is to raise run times from polynomial to exponential. The next sections, abbreviated from [9], show that the problem is a general one of the relation between logspace and ptime. Thus it is not peculiar to the MCV problem, nor to the way we have programmed its solution.

3 How logspace={{CFTR}}\mbox{\sc logspace}=\mbox{\mbox{$\mathbf{\{\{}$}\rm{CFTR}\mbox{$\mathbf{\}\}}$}} and ptime={{CF}}\mbox{\sc ptime}=\mbox{\mbox{$\mathbf{\{\{}$}\rm{CF}\mbox{$\mathbf{\}\}}$}} are proven

3.1 Proof sketch of Theorem 1, that logspace={{CFTR}}\mbox{\sc logspace}=\mbox{\mbox{$\mathbf{\{\{}$}\rm{CFTR}\mbox{$\mathbf{\}\}}$}}

  • For \supseteq, we simulate a CFTR program p by a logspace Turing machine (informal). Given an input x=a1an{0,1}x=a_{1}\ldots a_{n}\in\{0,1\}^{*}, by Lemma 2 any reachable configuration (𝚏,ρ)({\tt f},\rho) satisfies 0|ρ(𝚡𝚒)|max(n,1)0\leq|\rho({\tt xi})|\leq\max(n,1) for i=1,,mi=1,\ldots,m. Each viv_{i} can be coded in O(logn)O(\log n) bits. Now f and the number of f’s arguments are independent of xx, so an entire configuration (𝚏,ρ)({\tt f},\rho) can be coded into O(logn)O(\log n) bits.

    The remaining task is to show that the operational semantics of running p on xx can be simulated by a logspace Turing machine. The key: because p is tail recursive, there is no nesting of calls to program-defined functions. The construction can be described by cases:

    1. 1.

      Expressions without calls to any defined function: Suppose p’s current environment ρ\rho is represented on the Turing machine work tape. Simulating this evaluation is straighforward.

    2. 2.

      Evaluation of an expression f e1…em: Since p is tail recursive, none of e1,...,em contains a call, and there is “nothing more to do” after the call f e1…em has been simulated. Assume inductively that e1…em have all been evaluated. Given the definition f x1…xm = e of f, the remaining steps101010These steps are done using Turing machine representations of the environments as coded on Tape 2. are to collect the values v1,,vmv_{1},\ldots,v_{m} construct a new environment ρ=[𝚡𝟷v1,,𝚡𝚖vm]\rho^{\prime}=[{\tt x1}\mapsto v_{1},\ldots,{\tt xm}\mapsto v_{m}], and replace the current ρ\rho by ρ\rho^{\prime}.

  • For \subseteq, we simulate a logspace Turing machine by a CFTR program. This can be done using Lemma 2: represent a Tape 2 value by one or more suffixes of xx. (A more general result is shown in [9] by “counting modules”.)

Remark: given an input xx, the number of times that a call (𝚏,ρ)({\tt f},\rho) appears in the call history of [[𝚙]](x)\mbox{$[[$}{\tt p}\mbox{$]]$}(x) may be much larger than the number of such calls with distinct argument values, even exponentially larger.

3.2 Proof sketch of Theorem 2, that ptime={{CF}}\mbox{\sc ptime}=\mbox{\mbox{$\mathbf{\{\{}$}\rm{CF}\mbox{$\mathbf{\}\}}$}}

  • For \subseteq, we simulate a ptime Turing machine ZZ by a CF program. (this is the core of Proposition 6.5 from [9]). Assume Turing machine Z=(Q,{0,1},δ,q0)Z=(Q,\{0,1\},\delta,q_{0}) is given111111As usual QQ is a finite set with initial state q0q_{0}, and transition function of type δ:Q×{0,1,B}Q×{0,1,B}×{0,1,1}\delta:Q\times\{0,1,B\}\to Q\times\{0,1,B\}\times\{0,1,-1\}. A total state is a triple (q,i,τ)(q,i,\tau) where qQ,i0q\in Q,i\geq 0 and the tape is τ:{0,1,B}\tau:{\mathbb{N}}\to\{0,1,B\}. Scan positions are counted i=0,1,i=0,1,\ldots from the tape left end. Transition δ(q,a)=(q,a,d)\delta(q,a)=(q^{\prime},a^{\prime},d) means: if ZZ is in state qq and a=τ(i)a=\tau(i) is the scanned tape symbol, then change the state to qq^{\prime}, write aa^{\prime} in place of the scanned symbol, and move the read head from its current position ii to position i+di+d. and that it runs in time at most nkn^{k} for inputs xx of length nn. Consider an input x=𝚊1𝚊2𝚊n{𝟶,𝟷}x={\tt a}_{1}{\tt a}_{2}\ldots{\tt a}_{n}\in\{{\tt 0},{\tt 1}\}^{*}.

    Idea: in the space-time diagram of ZZ’s computation on xx (e.g., Fig. 3 in [9]), data propagation is local; the information (control state qq, symbol aa, whether or not it is scanned) at time tt and tape position ii is a mathematical function of the same information at time t1t-1 and tape positions i1,i,i+1i-1,i,i+1. This connection can be used to compute the contents of any tape cell at any time. Repeating for t=0,1,,nkt=0,1,\ldots,n^{k} steps gives the final result of the computation (xx is accepted or not accepted).

    The simulating CF program computes functions

    𝚜𝚝𝚊𝚝𝚎(t){\tt state}(t) = the state qq that ZZ is in at time tt
    𝚙𝚘𝚜𝚒𝚝𝚒𝚘𝚗(t){\tt position}(t) = the scanning position i{0,1,2,,nk}i\in\{0,1,2,\ldots,n^{k}\} at time tt
    𝚜𝚢𝚖𝚋𝚘𝚕(t,i){\tt symbol}(t,i) = the tape symbol aa found at time tt and scanning position ii

    Initially, at time t=0t=0 we have total state (q0,0,τ0)(q_{0},0,\tau_{0}) where τ0(i)=𝚊i\tau_{0}(i)={\tt a}_{i} for 1in1\leq i\leq n, else τ0(i)=B\tau_{0}(i)=B. Now suppose t>0t>0 and the total state at time t1t-1 is (q,i,τ)(q,i,\tau), and that δ(q,a)=(q,a,d)\delta(q,a)=(q^{\prime},a^{\prime},d). Then the total state at time tt will be (q,i+d,τ)(q^{\prime},i+d,\tau^{\prime}) where τ(i)=a\tau^{\prime}(i)=a^{\prime} and τ(j)=τ(j)\tau^{\prime}(j)=\tau(j) if jij\neq i.

    Construction of a CF program z with definitions of state, position and symbol is straightforward. Arguments t,i{0,1,2,,nk}t,i\in\{0,1,2,\ldots,n^{k}\} can be uniquely encoded as tuples of suffixes of x=𝚊1𝚊2𝚊n{𝟶,𝟷}x={\tt a}_{1}{\tt a}_{2}\ldots{\tt a}_{n}\in\{{\tt 0},{\tt 1}\}^{*}. It is not hard to see that such an encoding is possible; [9] works out details for an imperative version of the Turing machine (cf. Fig. 5).

  • For \supseteq, we simulate an arbitrary CF program p by a ptime Turing machine (call it Z𝚙Z_{\tt p}). We describe the Z𝚙Z_{\tt p} computation informally. Given an input x{0,1}x\in\{0,1\}^{*}, Z𝚙Z_{\tt p} will systematically build a cache containing 𝑅𝑒𝑎𝑐ℎ𝚙(x)\mathit{Reach}_{\tt p}(x). By Lemma 3 its size is polynomially bounded.

    The cache (call it cc) at any time contains a set of triples (𝚏,ρ,v)({\tt f},\rho,v^{\prime}) where the simulator has completed evaluation of function f on argument values in environment ρ\rho, and this f call has returned value vv.

    Concretely, cc is built “breadth-first”: when a p call f e1…em is encountered (initially cc is empty):

    • First, arguments e1,…,em are evaluated to values v1,,vmv_{1},\ldots,v_{m}; and these are collected into an environment ρ\rho.

    • Second, cache cc is searched. If it contains (𝚏,ρ,v)({\tt f},\rho,v), then simulation continues using the value vv as the value of f e1…em.

    • If cc contains no such triple, then p must contain a function definition f x1...xm = ef. Then expression ef is evaluated in environment ρ\rho to yield some value vv. After finishing, add the triple (𝚏,ρ,v)({\tt f},\rho,v) to cc, and return the value vv.

An observation: The CF program of Section 3.2 (simulating a polynomial time Turing machine) has exponential runtime.

Paradoxically, as observed in Section 2.2: even though CF exactly characterises ptime, its programs do not run in polynomial time. The polynomial versus exponential contrast between the running times of CFTR and CF programs is interesting since both program classes are natural; and the decision powers of CFTR and CF are exactly the complexity classes logspace and ptime. Alas, we have found no CF algorithm to simulate polynomial-time Turing machines. We explain how this happens in more detail.

Definition 8

Call overlap occurs if a CF program can call the same function more than once with the same tuple of argument values.

How can this happen? By Lemma 3, only polynomially many argument tuples are distinct. Consequently, superpolynomial running time implies that some simulation functions may be called repeatedly with the same argument value tuples, even though only polynomially many of them are distinct.

This can be seen in the proof that ptime{{CF}}\mbox{\sc ptime}\supseteq\mbox{\mbox{$\mathbf{\{\{}$}\rm{CF}\mbox{$\mathbf{\}\}}$}}: the value of 𝚜𝚢𝚖𝚋𝚘𝚕(t+1,i){\tt symbol}(t+1,i) may depend on the values of 𝚜𝚢𝚖𝚋𝚘𝚕(t,i1){\tt symbol}(t,i-1), and 𝚜𝚢𝚖𝚋𝚘𝚕(t,i){\tt symbol}(t,i), and 𝚜𝚢𝚖𝚋𝚘𝚕(t,i+1){\tt symbol}(t,i+1). In turn, the value of 𝚜𝚢𝚖𝚋𝚘𝚕(t,i){\tt symbol}(t,i) may depend on the values of 𝚜𝚢𝚖𝚋𝚘𝚕(t1,i1){\tt symbol}(t-1,i-1), and 𝚜𝚢𝚖𝚋𝚘𝚕(t1,i){\tt symbol}(t-1,i), and 𝚜𝚢𝚖𝚋𝚘𝚕(t1,i+1){\tt symbol}(t-1,i+1).

Net effect: a symbol ai on the final tape (with t=nkt=n^{k}) may depend many distinct function call sequences that “bottom out” at t=0t=0. The number of call sequences may be exponential in nn.

Lemma 5

Call overlap will occur if the length of the call history for [[p]](x)\mbox{\mbox{$[[$}{\tt p}\mbox{$]]$}}(x) is greater than #Reach𝚙(x)\#Reach_{\tt p}(x).

Unfortunately, it is is hard to see which calls will overlap (it seems impossible without storage). Furthermore, the “caching” technique used to prove Theorem 2 cannot be used because, in contrast with Turing machines, CF programs do not have a memory that can accumulate previously computed values.

4 Closer to the boundary between CFTR and CF

Viewed extensionally, the difference (if any) between CFTR and CF corresponds to the difference (if any) between logspace and ptime. Viewed intensionally, there seem to be significant differences, e.g., in algorithm expressivity as well as in running time. A relevant early result:

A program transformation by Greibach shows that programs whose calls (to defined functions) are all linear121212A call is linear if it is not contained in a call to any defined function. An example with a linear call that is not a tail call: not(even(tail z)) in Section 1.5. can be made tail recursive [5]. Using this transformation, a CF program p whose calls are tail calls or linear calls may be transformed into one containing only tail calls (and no CONS or other constructors), so it is in CFTR. Thus p decides a problem in logspace.

There is a price to be paid, though: in general, the transformed program may run polynomially slower than the original, due to re-computation. For instance, the Greibach method can transform our first “parity” program into CFTR form. The cost: to raise time complexity from linear to quadratic.

Following is a new tool to investigate the problem. It is analogous to the set of programs for polynomial-time Turing machines, but adapted to the CF world. By Theorem 4, CFpoly \subsetneq CF.

Definition 9

The programming language CFpoly has the same semantics as CF; but CFpoly’s programs are restricted to be those CF programs that terminate in polynomial time.

Immediate:

logspace={{CFTR}}{{CFpoly}}{{CF}}=ptime\mbox{\sc logspace}=\mbox{\mbox{$\mathbf{\{\{}$}\rm{CFTR}\mbox{$\mathbf{\}\}}$}}\subseteq\mbox{\mbox{$\mathbf{\{\{}$}\rm{CFpoly}\mbox{$\mathbf{\}\}}$}}\subseteq\mbox{\mbox{$\mathbf{\{\{}$}\rm{CF}\mbox{$\mathbf{\}\}}$}}\ =\mbox{\sc ptime}

By Theorem 3, every terminating CFTR program is in CFpoly. One can see Greibach’s result as transforming a subset of CFpoly into CFTR.

Lemma 6

If a terminating CF program does not have call overlap, then it is in CFpoly.

The reason: by Lemma 3, such a CF program must run in polynomial time.

Remark: On the other hand, it may have non-tail calls, and so not be in CFTR.

Lemma 7

Problem class {{\mathbf{\{\{}CFpoly}}\mathbf{\}\}} is closed under ,\cup,\cap and complement.

5 Nondeterminism and cons-free programs

Nondeterministic programs allow expression evaluation and program runs to be relations (\leadsto) rather than functions. A nondeterministic version of Figure 1 would use [[𝚙]](x)v\mbox{$[[$}{\tt p}\mbox{$]]$}(x)\leadsto v and 𝚙,ρ𝚎v{\tt p},\rho\vdash{\tt e}\leadsto v in place of [[𝚙]](x)v\mbox{$[[$}{\tt p}\mbox{$]]$}(x)\to v and 𝚙,ρ𝚎v{\tt p},\rho\vdash{\tt e}\to v. Alas, the algorithm of Section 1.3 cannot be used if p is nondeterministic.

Definition 10

A set X{0,1}X\subseteq\{0,1\}^{*} is decided by an NCF program p if for all inputs x{0,1}x\in\{0,1\}^{*},

xX if and only if p has a computation [[𝚙]](x)𝚃𝚛𝚞𝚎x\in X\mbox{\ if and only if {\tt p} has a computation\ }\mbox{$[[$}{\tt p}\mbox{$]]$}(x)\leadsto{\tt True}

5.1 Nondeterministic versions of the CF program classes

Remark: the reasoning used in Theorem 3 clearly extends to show that nlogspace={{NCFTR}}\mbox{\sc nlogspace}=\mbox{\mbox{$\mathbf{\{\{}$}\rm{NCFTR}\mbox{$\mathbf{\}\}}$}}. The following is particularly interesting since the question logspace =? nlogspace is still open.

Theorem 5

{{CF}}={{NCF}}\mbox{\mbox{$\mathbf{\{\{}$}\rm{CF}\mbox{$\mathbf{\}\}}$}}=\mbox{\mbox{$\mathbf{\{\{}$}\rm{NCF}\mbox{$\mathbf{\}\}}$}}

This was proven by Bonfante [3] by a technique that stems back to in Cook [4]. The implication is that {{\mathbf{\{\{}CF}}\mathbf{\}\}} is closed under nondeterminism, since both problem classes are equal to ptime. (This does not imply ptime = nptime, since it is not hard to see that any NCF program can be simulated by a deterministic polynomial-time Turing machine: Lemma 3 holds for NCF as well as for CF.)

However it is not known whether {{\mathbf{\{\{}CFpoly}}\mathbf{\}\}} is closed under nondeterminism, since Bonfante and Cook’s reasoning does not seem to apply. Why? The memoisation used in [3] yields a Turing machine polynomial time algorithm. Consequently, the problem is decidable by some CF program; but we know no way to reduce its time usage from exponential to polynomial.

A “devil’s advocate” remark: by Theorem 2 the classes {{\mathbf{\{\{}CFpoly}}\mathbf{\}\}} and nlogspace are both between logspace and ptime:

logspace={{CFTR}}nlogspace{{CF}}=ptime\mbox{\sc logspace}=\mbox{\mbox{$\mathbf{\{\{}$}\rm{CFTR}\mbox{$\mathbf{\}\}}$}}\subseteq\mbox{\sc nlogspace}\subseteq\mbox{\mbox{$\mathbf{\{\{}$}\rm{CF}\mbox{$\mathbf{\}\}}$}}=\mbox{\sc ptime}

So why bother with yet another class? One answer: CFpoly seems more natural than NCFTR from a programming viewpoint; and intuitively {{\mathbf{\{\{}CFpoly}}\mathbf{\}\}} seems to be a larger extension of logspace than nlogspace or the program class NCFTR. However we have no proof that nlogspace{{CFpoly}}\mbox{\sc nlogspace}\neq\mbox{\mbox{$\mathbf{\{\{}$}\rm{CFpoly}\mbox{$\mathbf{\}\}}$}}, and no solid reason to think that either class contains the other.

5.2 Simulating CFpoly by a nondeterministic algorithm

Theorem 6

{{CFPoly}}\mbox{\mbox{$\mathbf{\{\{}$}\rm{CFPoly}\mbox{$\mathbf{\}\}}$}}\subseteq NSPACE(log2 n)).

Proof: If p \in CFPoly, there is a polynomial π\pi such that for any input x{0,1}x\in\{0,1\}^{*} one can decide, in time π(|x|)\pi(|x|), whether or not [[𝚙]](x)v\mbox{$[[$}{\tt p}\mbox{$]]$}(x)\to v. The question: can this be done in significantly less space? We answer “yes”. The proof uses a space-economical nondeterministic algorithm to find vv at the root [[𝚙]](x)=v\mbox{$[[$}{\tt p}\mbox{$]]$}(x)=v of tree 𝒯𝚙,x{\cal T}^{{\tt p},x}. First, observe that any reachable statement 𝚙,ρ𝚎w\mathtt{p},\rho\vdash{\tt e}\rightarrow w can be represented in O(logn)O(\log n) bits.

  • To evaluate p on input xx, we nondeterministically guess a value vv such that [[𝚙]](x)=v\mbox{$[[$}{\tt p}\mbox{$]]$}(x)=v. The remaining task is to confirm that this statement is true. If we cannot do that, the whole algorithm has failed.

  • To confirm a statement such as 𝚙,ρ𝚏𝚎v\mathtt{p},\rho\vdash{\tt f\ e}\to v, we must confirm the existence of an evaluation tree of the form:

                 𝚙,ρ𝚎w\mathtt{p},\rho\vdash{\tt e}\rightarrow w                       𝚙,[x1w]𝚎𝚏v\mathtt{p},[x_{1}\mapsto w]\vdash{\tt e^{f}}\rightarrow v                 𝚙,ρ𝚏𝚎v\mathtt{p},\rho\vdash{\tt f\ e}\rightarrow v

    To do this, we now guess wVxw\in V_{x}, and now need to confirm two statements. We also guess which of these two statements has the shortest evaluation tree.

    For example, suppose we guess that this is the case for 𝚙,[x1w]𝚎𝚏v\mathtt{p},[x_{1}\mapsto w]\vdash{\tt e^{f}}\rightarrow v. Then we do a recursive call to confirm this statement (which temporarily stores the current state on the algorithm’s stack). Afterwards, we tail-recursively confirm the other statement, 𝚙,ρ𝚎w\mathtt{p},\rho\vdash{\tt e}\rightarrow w. Since this is a tail call, the algorithm’s stack size does not increase.

  • This extends naturally to multi-argument function calls.

  • To confirm a statement 𝚙,ρ𝚒𝚏e1𝚝𝚑𝚎𝚗e2𝚎𝚕𝚜𝚎e3v\mathtt{p},\rho\vdash\mathtt{if}\ e_{1}\ \mathtt{then}\ e_{2}\ \mathtt{else}\ e_{3}\to v, we guess whether e1e_{1} reduces to True or False; for example, we guess that it reduces to False. Then it suffices to confirm that an evaluation tree of the following form exists:

                   𝚙,ρe1\mathtt{p},\rho\vdash e_{1}\rightarrowFalse                    𝚙,ρe3v\mathtt{p},\rho\vdash e_{3}\rightarrow v     𝚙,ρ𝚒𝚏e1𝚝𝚑𝚎𝚗e2𝚎𝚕𝚜𝚎e3v\mathtt{p},\rho\vdash\mathtt{if}\ e_{1}\ \mathtt{then}\ e_{2}\ \mathtt{else}\ e_{3}\rightarrow v

    For this, we again have to confirm two statements. As before, we guess which of the two statements has the shorter evaluation tree, evaluate that statement first, and then evaluate the other statement tail-recursively.

If all guessed values are correct, then this algorithm returns the correct result. Furthermore, if we always guessed correctly which statement has the shortest evaluation tree, it does so with an overall stack depth that is never more than log(|𝒯𝚙,x||\mathcal{T}^{\mathtt{p},x}|). The reason is that every time we do a non-tail recursive call, this shortest subtree has size at most half of the previous subtree’s size. Since a statement 𝚙,ρ𝚎w\mathtt{p},\rho\vdash{\tt e}\rightarrow w can be represented in O(logn)O(\log n) bits we have the desired result.

The idea of applying tail-recursion to the largest subtree to save space was also used to implement Quicksort [10]. However our problem is much more general; and we must use nondeterminism because it cannot be known in advance which subtree will be the largest.

We expect that this construction can be extended to show {{NCFpoly}}nspace(log2n)\mbox{\mbox{$\mathbf{\{\{}$}\rm{NCFpoly}\mbox{$\mathbf{\}\}}$}}\subseteq\mbox{\sc nspace}(\log^{2}n) as well.

5.3 Closure under complementation

The following was shown by a subtle nondeterministic state-enumeration and state-counting algorithm. It was devised independently by Immerman and Szelepcsényi, and solved Kuroda’s second and long-standing open question [12, 6, 17]. It is still open whether logspacenlogspace\mbox{\sc logspace}\subsetneq\mbox{\sc nlogspace}.

Theorem 7

nlogspace is closed under complement.

6 Conclusions, future work

We have probed the question logspace =? ptime from a programming language viewpoint. A starting point was that the “cons-free” programming language CF exactly captures the Turing complexity class ptime; while its cons-free tail recursive subset CFTR exactly captures logspace. Section 3 recapitulates the reasoning used in [9].

In more detail: all CFTR programs run in polynomial time; but on the other hand, some CF programs run for exponentially many steps. Further the sets decided by the two program classes have seemingly different closure properties: the questions logspace =? nlogspace and ptime =? nptime and even logspace =? ptime have been open for decades. Given this, it seems almost paradoxical that {{CF}}={{NCF}}\mbox{\mbox{$\mathbf{\{\{}$}\rm{CF}\mbox{$\mathbf{\}\}}$}}=\mbox{\mbox{$\mathbf{\{\{}$}\rm{NCF}\mbox{$\mathbf{\}\}}$}} (from [3]).

Trying to understand these differences made it natural to consider CFpoly - the class of polynomially time-bounded CF programs since they have feasible running times (even though some CF programs may have superpolynomial behavior=. One test of CFpoly was to see whether it contained any ptime-complete problems. As a case study we wrote (Section 2.3) a CF-program for MCV (the monotone circuit value) problem to clarify where non-tail-recursion was necessary, and where superpolynomial runtimes came into the picture. (One key was nested recursion in function calls, clearly visible in function mcv in the MCV code.) Another test was to see whether {{\mathbf{\{\{}CFpoly}}\mathbf{\}\}} is perhaps a smaller complexity class than ptime. Theorem 6 leads in this direction, with an interesting proof construction and the surprising upper bound NSPACE(log2n)NSPACE(\mathrm{log}^{2}n).

Many questions for CFpoly are still to be investigated. One is to see whether the Immerman-Szelepcsenyi algorithm can be adapted to NCFpoly.

References

  • [1]
  • [2] Siddharth Bhaskar & Jakob G. Simonsen (2020): Implicit Complexity via Controlled Construction and Destruction. Technical Report, Department of Computer Science, University of Copenhagen.
  • [3] Guillaume Bonfante (2006): Some Programming Languages for Logspace and Ptime. In Michael Johnson & Varmo Vene, editors: Algebraic Methodology and Software Technology, 11th International Conference, AMAST 2006, Kuressaare, Estonia, July 5-8, 2006, Proceedings, Lecture Notes in Computer Science 4019, Springer, pp. 66–80, 10.1007/11784180_8.
  • [4] Stephen A. Cook (1971): Characterizations of Pushdown Machines in Terms of Time-Bounded Computers. J. ACM 18(1), pp. 4–18, 10.1145/321623.321625.
  • [5] Sheila A. Greibach (1975): Theory of Program Structures: Schemes, Semantics, Verification. Lecture Notes in Computer Science 36, Springer, 10.1007/BFb0023017.
  • [6] Neil Immerman (1988): Nondeterministic Space is Closed Under Complementation. SIAM J. Comput. 17(5), pp. 935–938, 10.1137/0217058.
  • [7] Neil D. Jones (1997): Computability and complexity - from a programming perspective. Foundations of computing series, MIT Press, 10.7551/mitpress/2003.001.0001.
  • [8] Neil D. Jones (1999): LOGSPACE and PTIME Characterized by Programming Languages. Theor. Comput. Sci. 228(1-2), pp. 151–174, 10.1016/S0304-3975(98)00357-0.
  • [9] Neil D. Jones (2001): The expressive power of higher-order types or, life without CONS. J. Funct. Program. 11(1), pp. 55–94, 10.1017/S0956796800003889. Available at http://journals.cambridge.org/action/displayAbstract?aid=68581.
  • [10] Donald E. Knuth (1973): The Art of Computer Programming, Volume III: Sorting and Searching. Addison-Wesley.
  • [11] Cynthia Kop & Jakob Grue Simonsen (2017): The Power of Non-determinism in Higher-Order Implicit Complexity - Characterising Complexity Classes Using Non-deterministic Cons-Free Programming. In Hongseok Yang, editor: Programming Languages and Systems - 26th European Symposium on Programming, ESOP 2017, Held as Part of the European Joint Conferences on Theory and Practice of Software, ETAPS 2017, Uppsala, Sweden, April 22-29, 2017, Proceedings, Lecture Notes in Computer Science 10201, Springer, pp. 668–695, 10.1007/978-3-662-54434-1_25.
  • [12] Sige-Yuki Kuroda (1964): Classes of languages and linear-bounded automata. Information and Control 7(2), pp. 207–223, 10.1016/S0019-9958(64)90120-2.
  • [13] John McCarthy (1960): Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I. Commun. ACM 3(4), pp. 184–195, 10.1145/367177.367199.
  • [14] Yiannis Moschovakis (2019): Abstract Recursion and Intrinsic Complexity. Cambridge University Press (Lecture Notes in Logic), 10.1017/9781108234238.
  • [15] Christos H. Papadimitriou (1994): Computational complexity. Addison-Wesley.
  • [16] Walter J. Savitch (1970): Relationships Between Nondeterministic and Deterministic Tape Complexities. J. Comput. Syst. Sci. 4(2), pp. 177–192, 10.1016/S0022-0000(70)80006-X.
  • [17] Róbert Szelepcsényi (1988): The Method of Forced Enumeration for Nondeterministic Automata. Acta Inf. 26(3), pp. 279–284, 10.1007/BF00299636.
  • [18] Alan M. Turing (1936-7): On Computable Numbers with an Application to the Entscheidungsproblem. Proceedings of the London Mathematical Society 42(2), pp. 230–265, 10.1112/plms/s2-42.1.230.