LOOP is a simple register language designed to precisely capture the primitive recursive functions. The language is derived from the counter-machine model. Like the Counter machines the LOOP language comprises a set of one or more unbounded registers, each of which can hold a single non-negative integer. A few arithmetic instructions operate on the registers: inc x (increment), dec x (decrement: ), x = 0 (clear), and x = y (copy). The only control flow instruction is LOOP x: .... It causes the instructions within its scope to be repeated times.Changes of the content of register x during the execution of the loop do not affect the number of passes.
In contrast to GOTO programs and WHILE programs, LOOP programs always terminate. The running time is known in advance. Therefore, the set of functions computable by LOOP-programs is a proper subset of computable functions (and thus a subset of the computable by WHILE and GOTO program functions).
The LOOP language admits several variants, distinguished by their set of basic instructions. Four such variants, L0 – L3, are distinguished in this article. Despite differences in syntactic convenience and loop-nesting depth, all variants compute exactly the primitive recursive functions.
At shallow nesting depths, the choice of basic instructions affects which functions are reachable. The simple functions are exactly the functions computable at depth 1 in L1; the Presburger-definable functions are exactly the functions computable at depth 1 in L3; the Kalmár elementary functions are exactly the functions computable at depth 2 in L1, L2, and a fortiori L3. From depth 3 onward all variants agree.
An example of a total computable function that is not LOOP-computable is the Ackermann function.
History The LOOP language was formulated in a 1967 paper by Albert R. Meyer and Dennis M. Ritchie. They showed the correspondence between the LOOP language and primitive recursive functions, proving that each primitive recursive function is LOOP-computable and vice versa.
The language also was the topic of the unpublished PhD thesis of Ritchie.
It was presented by Uwe Schöning, along with GOTO and WHILE.
Definition
Several variants of the LOOP programming language have been defined, based on different sets of basic instructions. Despite these variations, they agree on the class of functions they compute: exactly the primitive recursive functions. However, when considering loop nesting depth that is, the number of nested LOOP constructs allowed the choice of basic instructions affects the expressive power at shallow depths.
Four variants of the LOOP language are distinguished here: L0: Minsky (1967); L1: Meyer & Ritchie (1967), Tsichritzis (1970), Machtey (1972), Beck (1975), Fachini & Maggiolo-Schettini (1979), Goetze & Nehrlich (1980), Calude (1988), Schöning & Pruim (1998), Tourlakis (2012), Matos (2015); L2: Tsichritzis (1971), Beck (1975), Kfoury et al. (1982), Odifreddi (1989), Matos (2014); L3: Cherniavsky (1976), Cherniavsky & Kamin (1979), Kfoury (1980), Ibarra & Rosier (1983), Handley & Wainer (1999).To L3 added as basic the conditional if x = y then ... else ... fi.
In this presentation, the term "LOOP program" refers collectively to any of L0 – L3.
Syntax
A LOOP program consists of a sequence of instructions, which modify a finite number of registers, any of which may contain a non-negative integer.
If x and y are registers, define the following sets Bi of basic instructions:
For we define the loop language Li as the smallest set of programs generated by the following rules:
1. Every instruction is a program in Li.
2. If P and Q are in Li, then the sequential composition P Q is in Li.
3. If , then LOOP x: P is in Li.
Indentation determines block structure in a Python-like notation. For brevity, a short body may be written on the same line as its LOOP header, and consecutive instructions on one line are separated by semicolons.
A LOOP program may be associated with a signature specifying input and output registers: signature ::= 'def' program_name '(' input ')' '->' '(' output ')' ':' input ::= [ register (',' register)* ] output ::= register (',' register)*
Notes The signature is not part of the program. As and can be any lists of registers, in general P can compute several functions, depending on the chosen mapping. So, multiple signatures can be associated to one single LOOP program. In this presentation an intended signature is placed before the program.
Semantics Registers range over . Execution proceeds sequentially. A loop of the form LOOP x: ... executes its body exactly times, where is the value of the register at loop entry.
A LOOP program P is said to compute a function when: , an m-tuple, specifies registers which contain the arguments of ; the remaining registers contain ; , an n-tuple, specifies registers; after the execution of P the output registers contain .
Note When specifies a single register, P defines a scalar-valued function; otherwise, P defines a vector-valued function.
Definition
This definition agrees with the following:
A (vector-valued) function is primitive recursive if it can be written as
where each component is a (scalar-valued) primitive recursive function.
Completing the set of basic instructions As all four systems compute the primitive recursive functions: x = y is definable in L0 and in L2, dec x is definable in L0 and in L1.
Bootstrapping x = y In L0 or L2 x = y can be implemented by the program def copy(y) -> (x): LOOP y: inc x
We will write x = y in all languages, understanding that this instruction denotes x = copy(y) in L0 and in L2.Wherever x = y appears in L0 or in L2 code, one has to add to the local loop depth.The overall nesting depths are """ Loop nesting: 1 (L0), 0 (L1) """
Bootstrapping dec x The predecessor functionImplementation in Python: x = 0 if x == 0 else x - 1
can be implemented by the programs
| style="vertical-align: top; border: none; padding: 0 20px 0 0;" | in L0 def dec_L0(x) -> (x): LOOP x: x = 0 LOOP a: inc x inc a
| style="vertical-align: top; border: none; padding: 0 20px 0 0;" | in L1 def dec_L1(x) -> (x): LOOP x: x = a inc a
We will write dec x in all languages, understanding that this instruction denotes dec_L0(x) in L0 and dec_L1(x) in L1.Wherever dec x appears in L0 or in L1 code, one has to add respectively to the local loop depth.The overall nesting depths are """ Loop nesting: 2 (L0), 1 (L1), 0 (L2) """
Loop nesting The central significance of the LOOP language lies in its stratification by maximum loop nesting depth. Writing Loopk(L) for the class of functions computable by programs in variant L with at most k-nested LOOP constructs, the four variants are related by the following inclusion diagram:
where an upward edge denotes that the upper variant has strictly more primitive instructions than the lower. L1 and L2 are incomparable: L1 has x = y but not dec x; L2 has dec x but not x = y; L3 has both; L0 has neither.
For nesting depth k < 2 this induces strict inclusions along the edges:
with Loopk(L1) and Loopk(L2) incomparable. The strict inclusions reflect the cost of simulating missing base instructions by extra loop nesting.At depth 2 the variants L1, L2 and L3 collapse; from depth 3 onward the choice of basic instructions ceases to matter and all four variants agree. In particular:
At depth 1, Loop1(L1) = simple functions,Loop1(L2) falls short of simple functions: requires depth 2 in L2. Loop1(L3) = Presburger functions:Loop1(L1) falls short of Presburger: monus requires depth 2 in L1.Loop1(L2) falls short of Presburger: requires depth 2 in L2. At depth 2, Loop2(L1) = Loop2(L2) = Kalmár elementary functions:All three basis functions are Loop2 computable in both L1 and L2, so each Kalmár elementary function is reachable by loop-free concatenation of Loop2 programs in L1 or L2.
The example programs below track loop nesting depth explicitly for each variant.The """ docstrings """ mention depths that improve upon all lower variants in the inclusion diagram; omitted variants inherit the minimum depth of their reachable predecessors.
Macros as syntactic sugar Let P and Q be LOOP programs.
Assume that Q is a program with input registers and output registers, computing a primitive recursive function
.
Suppose input registers of P supply the inputs , and we wish to use Q as a subroutine inside P.
Then the code of Q can be embedded ("macro-expanded") into P as follows:
rename all registers of Q to avoid collisions with registers of P; initialize all registers of Q to ;When Q is expanded inside a loop, its registers may still hold values from a previous pass. copy the input values from P into the input registers of Q; execute the program Q; copy the output values from the output registers of Q into the designated registers of P.
This construction allows Q to be used as a subprogram of P, simulating an activation record in the sense required here.
Notes In P only the output registers are updated. This macro expansion does not alter the LOOP language. The notation is meant to improve the readability of the software and to facilitate the separate discussion of the inserted code. goes further. His function calls enhance the language to augmented loop programs. Machtey allows calls to general recursive functions.
Example programs
Integer division by a constant Integer division by a constant is a Presburger function that requires loop depth 1 in L1 and loop depth 2 in L2.
By convention, . can be computed by the LOOP program def divk(x) -> (c1): """ Loop nesting: 2 (L0), 1 (L1), 2 (L2) """ LOOP x: c0 = c1 # add to loop depth: 1 (L0), - (L1), 1 (L2) c1 = c2 # add to loop depth: 1 (L0), - (L1), 1 (L2) ... # ... ... ck = c0 # add to loop depth: 1 (L0), - (L1), 1 (L2) inc ck def div4(x) -> (c1): """ Loop nesting: 2 (L0), 1 (L1), 2 (L2) """ LOOP x: c0 = c1 # add to loop depth: 1 (L0), - (L1), 1 (L2) c1 = c2 # add to loop depth: 1 (L0), - (L1), 1 (L2) c2 = c3 # add to loop depth: 1 (L0), - (L1), 1 (L2) c3 = c4 # add to loop depth: 1 (L0), - (L1), 1 (L2) c4 = c0 # add to loop depth: 1 (L0), - (L1), 1 (L2) inc c4
Note The variables c0, c1, ..., ck form a cyclic shift chain, rotated one link to the left each iteration; ck is then incremented. Every iterations, the incremented value returns to c1, so c1 counts complete groups of iterations.
x modulo k This is the (integer) remainder after division by a constant.
By convention, . For :
def modk(x) -> (r): """ Loop nesting: 2 (L0), 1 (L1) """ # Step 1: Initialize the state ring inc c0
# Step 2: Cycle the states x times LOOP x: # Shift the ring forward: # (c0, c1, c2, ..., c{k-1}) = (c{k-1}, c0, c1, ..., c{k-2}) t0 = c0 t1 = c1 # add to loop depth: 1 (L0), - (L1), 1 (L2) t2 = c2 ... t{k-1} = c{k-1}
c0 = t{k-1} c1 = t0 # add to loop depth: 1 (L0), - (L1), 1 (L2) c2 = t1 ... c{k-1} = t{k-2}
# Step 3: Extract the final answer. LOOP c1: r = 1 LOOP c2: r = 2 ... LOOP c{k-1}: r = {k-1} def mod5(x) -> (r): """ Loop nesting: 2 (L0), 1 (L1) """ # Step 1: Initialize the state ring for k=5 inc c0
# Step 2: Cycle the states x times LOOP x: # Shift the ring forward: # (c0, c1, c2, c3, c4) = (c4, c0, c1, c2, c3) t0 = c0 t1 = c1 # add to loop depth: 1 (L0), - (L1), 1 (L2) t2 = c2 t3 = c3 t4 = c4
c0 = t4 c1 = t0 # add to loop depth: 1 (L0), - (L1), 1 (L2) c2 = t1 c3 = t2 c4 = t3
# Step 3: Extract the final answer. LOOP c1: r = 1 LOOP c2: r = 2 LOOP c3: r = 3 LOOP c4: r = 4
Monus This is the monus function (cutoff-subtraction).
def monus(x, y) -> (x): """ Loop nesting: 3 (L0), 2 (L1), 1 (L2) """ LOOP y: dec x # add to loop depth: 2 (L0), 1 (L1)
The expanded L1 code is listed by Meyer & Ritchie and by Matos: def monus(x, y) -> (x): LOOP y: a = 0 LOOP x: x = a inc a
Conditional selection w
def w(x, y) -> (x): """ Loop nesting: 1 (L0) """ LOOP y: x = 0