A micro-manual of the programming language L O G L A N - 82 ================ Basic constructs and facilities Author: Antoni Kreczmar Institute of Informatics, Warsaw University September 1982 LOGLAN-82 is a universal programming language designed at the Institute of Informatics, University of Warsaw. Its syntax is patterned upon Pascal's. Its rich semantics includes the classical constructs and facilities offered by the Algol-family programming languages as well as more modern facilities, such as concurrency and exception handling. The basic constructs and facilities of the LOGLAN-82 programming language include: 1) A convenient set of structured statements, 2) Modularity (with the possibility of module nesting and extending), 3) Procedures and functions (fully recursive; procedures and functions can be used also as formal parameters), 4) Classes (as a generalization of records) which enable to define complex structured types, data structures, packages, etc., 5) Adjustable arrays whose bounds are determined at run-time in such a way that multidimensional arrays may be of various shapes, e.g. triangular, k-diagonal, streaked, etc., 6) Coroutines and semi-coroutines, 7) Prefixing - the facility borrowed from Simula-67, substantially generalized in LOGLAN-82 - which enables to build up hierarchies of types and data structures, problem-oriented languages, etc., 8) Formal types treated as a method of module parametrization, 9) Module protection and encapsulation techniques, 10) Programmed deallocator - a tool for efficient and secure garbage collection, which allows the user to implement the optimal strategy of storage management, 11) Exception handling which provides facilities for dealing with run-time errors and other exceptional situations raised by the user, 12) Separate compilation techniques, 13) Concurrency easily adaptable to any operating system kernel and allowing parallel programming in a natural and efficient way. The language covers system programming, data processing, and numerical computations. Its constructs represent the state-of-art and are efficiently implementable. Large systems consisting of many cooperating modules are easily decomposed and assembled, due to the class concept and prefixing. LOGLAN-82 constructs and facilities have appeared and evolved simultaneously with the experiments on the first pilot compiler (implemented on Mera-400 Polish minicomputer). The research on LOGLAN-82 implementation engendered with new algorithms for static semantics, context analysis, code generation, data structures for storage management etc. The LOGLAN-82 compiler provides a keen analysis of syntactic and semantic errors at compilation as well as at run time. The object code is very efficient with respect to time and space. The completeness of error checking guarantees full security and ease of program debugging. 1. Compound statements ###################### Compound statements in LOGLAN-82 are built up from simple statements (like assignment statement e.g. x:=y+0.5, call statement e.g. callP(7,x+5) e The syntax of conditional statement is as follows: if boolean expression then sequence of statements else sequence of statements fi where "else part" may be omitted: if boolean expression then sequence of statements fi The semantics of conditional statement is standard. The keyword fi allows to nest conditional statements without appearence of "dangling else" ambiguity. Example: -------- if delta>0 if then x2:=sqrt(delta)/a/2; if b=0 then x1:=x2 else x1:=-b/a/2+x2; x2:=x1-2*x2 fi else if delta=0 then x1:=-b/a/2; x2:=x1 else write(" no real roots") fi fi The statements in a sequence of statements are separated with semicolons (semicolon may end a sequence , and then, the last statement in the sequence is the empty statement). The short circuit control forms are realized in LOGLAN-82 by the conditional statements with orif (or andif) list. A conditional statement with orif list has the form: orif if wb1 orif wb2 ... orif wbk then sequence of statements else sequence of statements fi and corresponds somehow to a conditional statement: if wb1 or wb2 ... or wbk then sequence of statements else sequence of statements fi The above conditional statement (without orif list) selects for execution one of two sequences of statements, depending on the truth value of the boolean expression: wb1 or wb2 or ... wbk which is always evaluated till the end. For the execution of the conditional statement with orif list the specified conditons wb1,...,wbk are evaluated in succession, until the first one evaluates to true. Then the rest of the sequence wb1,...,wbk is abandoned and "then part" is executed. If none of the conditions wb1,...,wbk evaluates to true "else part" is executed (if any). Conditional statements with orif list facilitate to program those conditions, which evaluation to the end may raise a run-time error. Example: -------- The execution of the statement: if i>n or A(i)=0 then i:=i-1 else A(i):=1 fi where the value of i is greater than n, and A is an array with upper bound n, will raise the run-time error. Then the user can write: if i>n orif A(i)=0 then i:=i-1 else A(i):=1 fi what allows to avoid this run-time error and probably agrees with his intension. Conditional statement with andif list has the form: if wb1 andif wb2 ... andif wbk then sequence of statements else sequence of statements fi For the execution of this kind of statements, the conditions wb1,...,wbk are evaluated in succession, until the first one evaluates to false; then "else part" (if any) is executed. Otherwise "then part" is executed. Iteration statement in LOGLAN-82 has the form: do sequence of statements od An iteration statement specifies repeated execution of the sequence of statements and terminates with the execution of the simple statement exit Example: -------- s:=1; t:=1; i:=1; do i:=i+1; t:=t*x/i; if abs(t) < 1.0E-10 then exit fi; s:=s+t od; If two iteration statements are nested, then double exit in the inner one terminates both of them. Example: -------- r,x:=0; do s,t:=1; i:=1; x:=x+0.2; do i:=i+1; t:=t*x/i; if i > n then exit exit fi; (* termination of both loops *) if t < 1 then exit fi; (* termination of the inner loop *) s:=s+t od od In the example above simultaneous assignment statements are illustrated (e.g. r,x:=0) and comments, which begin with a left parenthesis immediately followed by an asterisk and end with an asterisk immediately followed by a right parenthesis. Triple exit terminates three nested iteration statements, four exit terminates four nested iteration statements etc. The iteration statement with while condition: w while boolean expression do sequence of statements od is equivalent to: do if not boolean expression then exit fi; sequence of statements od The iteration statements with controlled variables (for statements) have the forms: for j:=wa1 step wa2 to wa3 do sequence of statements od or for j:=wa1 step wa2 downto wa3 do sequence of statements od The type of the controlled variable j must be discrete. The value of this variable in the case of the for statement with to is increased, and in the case of the for statement with downto is decreased. The discrete range begins with the value of wa1 and changes with the step equal to the value of wa2. The execution of the for statement with to terminates when the value of j for the first time becomes greater than the value of wa3 (with downto when the value of j for the first time becomes less than the value of wa3). After the for statement termination the value of its controlled variable is determined and equal to the first value exceeding the specified discrete range. The values of expressions wa1, wa2 and wa3 are evaluated once, upon entry to the iteration statement. Default value of wa2 is equal 1 (when the keyword step and expression wa2 are omitted). for or while statements may be combined with exit statement. Example: -------- for j:=1 to n do if x=A(j) then exit fi; od The above iteration statement terminates either for the least j, 1<=j<=n, such that x=A(j) or for j=n+1 when x=/=A(j), j=1,...,n. To enhance the user's comfort, the simple statement repeat is provided. It may appear in an iteration statement and causes the current iteration to be finished and the next one to be continued (something like jump to CONTINUE in Fortran's DO statements). Example: -------- i:=0; s:=0; do i:=i+1; if A(i) < 0 then repeat fi; (* jump to od, iterations are continued *) if i > m then exit fi; (* iteration statement is terminated *) s:=s+sqrt(A(i)); od; Just as exit, repeat may appear in for statement or while statement. Then the next iteration begins with either the evaluation of a new value of the controlled variable (for statement) or with the evaluation of the condition (while statement). Case statement in LOGLAN-82 has the form: case WA when L1 : I1 when L2 : I2 ... when Lk : Ik otherwise I esac where WA is an expression , L1,...,Lk are constants and I1,..., Ik,I are sequences of statements. A case statement selects for execution a sequence of statements Ij, 1<=j<=k, where the value of WA equals Lj. The choice otherwise covers all values (possibly none) not given in the previous choices. The execution of a case statement chooses one and only one alternative (since the choices are to be exhaustive and mutually exclusive). 2. Modularity ############# Modular structure of the language is gained due to the large set of means for module nesting and extending. Program modules (units) are blocks, procedures, functions, classes, coroutines and processes. Block is the simplest kind of unit. Its syntax is the following: block lists of declarations begin sequence of statements end The sequence of statements commences with the keyword begin (it may be omitted when this sequence is empty). The lists of declarations define the syntactic entities (variables, constants, other units), whose scope is that block. The syntactic entities are identified in the sequence of statements by means of names (identifiers). Example: -------- block const n=250; var x,y:real, i,j,k: integer, b: boolean; const m=n+1; begin read(i,j); (* read two integers *) x,y:=n/(i+j); (* simultaneous assignment *) read(c) ; (* read a character *) b:= c = 'a'; (* 'a' a character *) for k:= 1 to m do write(x+y/k:10:4); (* print the value of x+y/k in the field of 10 characters, 4 digits after the point *) od end In the lists of declarations semicolons terminate the whole lists, not the lists elements. Any declaration list must begin with the pertinent keyword (var for variables, const for constants etc.). The value of an expression defining a constant must be determinable statically (at compilation time). Program in LOGLAN-82 may be a block or alternatively may be of the following form: program name; lists of declarations begin sequence of statements end Then the whole program can be identified by that name (the source as well as the object code). A block can appear in the sequence of statements (of any unit), thus it is a statement. (Main block is assumed to appear as a statement of the given job control language.) For the execution of a block statement the object of block is created in a computer memory, and then, the sequence of statements is performed. The syntactic entities declared in the block are allocated in its object. After a block's termination its object is automatically deallocated (and the corresponding space may be immediately reused). The modular structure of the language works "in full steam" when not only blocks, but the other kinds of units are also used. They will be described closer in the following points. Unit nesting allows to build up hierarchies of units and supports security of programming. It follows from the general visibility rules; namely, a syntactic entity declared in an outer unit is visible in an inner one (unless hidden by an inner declaration). On the other hand, a syntactic entity declared in an inner unit is not visible from an outer one. Example: -------- program test; var a,b,c:real, i,j,k:integer; begin read(a,b,c,i); block var j,k:real; begin j:=a; k:=j+b; write(" this is the inner block ",j,k) end; write(" this is the outer block ",i,a:20) end; In this program, first the main block statement is executed (with variables a,b,c,i,j,k). Next, after the read statement, the inner block statement is executed (with variables j,k). In the inner block the global variables j,k are hidden by the local ones. 3. Procedures and functions ########################### Procedures and functions are well-known kinds of units. Their syntax is modelled on Pascal's, though with some slight modifications. Procedure (function) declaration consists of a specification part and a body. Example: -------- unit Euclid: function(i,j:integer):integer; var k:integer; begin do if j=0 then exit fi; k:=i mod j; i:=j; j:=k od; result:=i end; Procedure or function specification begins with its identifier preceded by the keyword unit. (The same syntax concerns any other module named unit.) Then follows its kind declaration, its formal parameters (if any), and the type of the returned value (only for functions). A body consists of declaration lists for local entities and a sequence of statements. The keyword begin commences the sequence of statements , and is omitted , if this sequence is empty. The value returned by a function equals to the most recent value of the standard variable "result", implicitly declared in any function. This variable can be used as a local auxiliary variable as well. Example: -------- unit Newton: function(n,m:integer):integer; var i:integer; begin if m > n then return fi; result:=n; for i:=2 to m do result:=result*(n-i+1) div i od end Newton; The optional identifier at the end of a unit must repeat the identifier of a unit. It is suggested that the compilers check the order of unit nesting, so these optional occurrences of identifiers would facilitate program debugging. All the local variables of a unit are initialized (real with 0.0, integer with 0, boolean with false etc.). Thus , for instance, the value of function Newton is 0 for m>n, since "result" is also initialized, as any other local variable. The return statement (return) completes the execution of a procedure (function) body,i.e. return is made to the caller. If return does not appear explicitly, return is made with the execution of the final end of a unit. Upon return to the caller the procedure (function) object is deallocated. Functions are invoked in expressions with the corresponding list of actual parameters. Procedures are invoked by call statement (also with the corresponding list of actual parameters). Example: -------- i:=i*Euclid(k,105)-Newton(n,m+1); call P(x,y+3); Formal parameters are of four categories: variable parameters, procedure parameters, function parameters and type parameters (cf p.8). Variable parameters are considered local variables to the unit. A variable parameter has one of three transmission modes: input, output or inout. If no mode is explicitly given the input mode is assumed. For instance in the unit declaration: unit P: procedure(x,y:real,b:boolean;output c:char,i:integer;inout j:integer); x,y,b are input parameters , c,i are output parameters , and j is inout parameter. Input parameter acts as a local variable whose value is initialized by the value of the corresponding actual parameter. Output parameter acts as a local variable initialized in the standard manner (real with 0.0, integer with 0, boolean with false etc.). Upon return its value is assigned to the corresponding actual parameter, in which case it must be a variable. However the address of such an actual parameter is determined upon entry to the body. Inout parameter acts as an input parameter and output parameter together. Example: -------- unit squareeq: procedure(a,b,c:real;output xr,xi,yr,yi:real); (* given a,b,c the procedure solves square equation : ax*x+bx+c=0. xr,xi- real and imaginary part of the first root yr,yi- real and imaginary part of the second root *) var delta: real; begin (*a=/=0*) a:=2*a; c:=2*c; delta:=b*b-a*c; if delta <= 0 then xr,yr:=-b/a; if delta=0 then return fi; (*xi=yi=0 by default*) delta:=sqrt(-delta); xi:=delta/a; yi:=-xi; return fi; delta:=sqrt(delta); if b=0 then xr:=delta/a; yr:=-xr; return fi; if b>0 then b:=b+delta else b:=b-delta fi; xr:=-b/a; yr:=-c/b; end squareeq; A procedure call to the above unit may be the following: call squareeq(3.75*H,b+7,3.14,g,gi,h,hi); where g,h,gi,hi are real variables. No restriction is imposed on the order of declarations. In particular, recursive procedures and functions can be declared without additional announcements (in contrast to Pascal). Example: -------- For two recursive sequences defined as: a(n)=b(n-1)+n+2 n>0 b(n)=a(n-1)+(n-1)*n n>0 a(0)=b(0)=0 one can declare two functions: unit a: function(n:integer):integer; begin if n>0 then result:=b(n-1)+n+2 fi end a; unit b: function(n:integer):integer; begin if n>0 then result:=a(n-1)+(n-1)*n fi end b; and invoke them: k:=a(100)*b(50)+a(15); Functions and procedures can be formal parameters as well. Example: -------- unit Bisec: procedure(a,b,eps:real;output x:real;function f(x:real):real); (*this procedures searches for zero of continous function f in segment (a,b) *) var h:real,s:integer; begin s:=sign(f(a)); if sign(f(b))=s then return fi; (* wrong segment *) h:=b-a; do h:=h/2; x:=a+h; if h < eps then return fi; if sign(f(x))=s then a:=x else b:=x fi od end Bisec; In the above declaration, after the input variable parameters a,b,eps and the output variable parameter x, a function parameter f appears. Note that its specification part is complete. Thus the check of actual-formal parameter compatibility is possible at compilation time. Making use of this syntactic facility is not possible in general, if a formal procedure (function) is again a formal parameter of a formal procedure (function). The second degree of formal procedures (functions) nesting is rather scarce, but LOGLAN-82 admits such a construct. Then formal procedure (function) has no specification part and the full check of actual-formal parameter compatibility is left to be done at run time. Example: -------- unit P: procedure(j:integer;procedure G(i:integer;procedure H)); ... begin ... call G(j,P); end P; Procedure G is a first degree parameter, therefore it occurs with complete specification part. Procedure H is a second degree parameter and has no specification part. In this case a procedure call can be strongly recursive: call P(i+10,P); 4. Classes ########## Class is a facility which covers such programming constructs as structured type, package, access type, data structure etc. To begin with the presentation of this construct, let us consider a structured type assembled from primitive ones: unit bill: class; var dollars :real, not_paid :boolean, year,month,day :integer; end bill; The above class declaration has the attributes : dollars (real), not_paid (boolean), and year,month,day (integer). Wherever class bill is visibile one can declare variables of type bill: var x,y,z:bill; The values of variables x, y, z can be the addresses of objects of class bill. These variables are called reference variables. With reference variable one can create and operate the objects of reference variable type. An object of a class is created by the class generation statement (new), and thereafter, its attributes are accessed through dot notation. x:=new bill; (* a new object of class bill is created *) x.dollars:=500.5; (* define amount *) x.year:=1982; (* define year *) x.month:=3; (* define month *) x.day:=8; (* define day *) y:=new bill; (* create a new object *) y.not_paid:=true; (* bill not_paid *) z:=y; (* variable z points the same object as variable y *) If an object of class bill has been created (new bill) and its address has been assigned to variable x (x:=new bill), then the attributes of that object are accessible through dot notation (remote access). The expression x.dollars gives , for instance, the remote access to attribute dollars of the object referenced by x. All attributes of class objects are initialized as usual. For the above example the object referenced by x, after the execution of the specified sequence of statements, has the following structure: --------------- | 500.5 | dollars --------------- | false | not_paid --------------- | 1982 | year --------------- | 3 | month --------------- | 8 | day --------------- The object referenced by y and z has the following structure: --------------- | 0 | dollars --------------- | true | not_paid --------------- | 0 | year --------------- | 0 | month --------------- | 0 | day --------------- The value none is the default initial value of any reference variable and denotes no object. A remote access to an attribute of none raises a run time error. Class may have also formal parameters (as procedures and functions). Kinds and transmission modes of formal parameters are the same as in the case of procedures. Example: -------- unit node: class (a:integer); var left,right:node; end node; Let , for instance, variables z1, z2, z3 be of type node. Then the sequence of statements: z1:=new node(5); z2:=new node(3); z3:=new node(7); z1.left:=z2; z1.right:=z3; creates the structure: ----------- z1----> | 5 | ----------- <---- | left | | ----------- | | right | -------> | ----------- | | | ------------ ------------ z2---->| 3 | | 7 | <----z3 ------------ ------------ | none | | none | ------------ ------------ | none | | none | ------------ ------------ where arrows denote the values of the reference variables. Class may also have a sequence of statements (as any other unit). That sequence can initialize the attributes of the class objects. Example: -------- unit complex:class(re,im:real); var module:real; begin module:=sqrt(re*re+im*im) end complex; Attribute module is evaluated for any object generation of class complex: z1:=new complex(0,1); (* z1.module equals 1 *) z2:=new complex(2,0); (* z2.module equals 2 *) For the execution of a class generator, first a class object is created, then the input parameters are transmitted , and finally, the sequence of statements (if any) is performed. Return is made with the execution of return statement or the final end of a unit. Upon return the output parameters are transmitted. Procedure object is automatically deallocated when return is made to the caller. Class object is not deallocated , its address can be assigned to a reference variable, and its attributes can be thereafter accessed via this variable. The classes presented so far had only variable attributes. In general, class attributes may be also other syntactic entities, such as constants, procedures, functions, classes etc. Classes with procedure and function attributes provide a good facility to define data structures. Example: -------- A push_down memory of integers may be implemented in the following way: unit push_down :class; unit elem:class(value:integer,next:elem); (* elem - stack element *) end elem; var top:elem; unit pop: function :integer; begin if top=/= none then result:=top.value; top:=top.next fi; end pop; unit push:procedure(x:integer); (* x - pushed integer *) begin top:=new elem(x,top); end push; end push_down; Assume that somewhere in a program reference variables of type push_down are declared (of course, in place where push_down is visibile): var s,t,z:push_down; Three different push_down memories may be now generated: s:=new push_down(100); t:=new push_down(911); z:=new push_down(5); One can use these push_down memories as follows: call s.push(7); (* push 7 to s *) call t.push(1); (* push 1 to t *) i:=z.pop; (* pop an element from z *) etc. 5. Adjustable arrays #################### In LOGLAN-82 arrays are adjustable at run time. They may be treated as objects of specified standard type with index instead of identifier selecting an attribute. An adjustable array should be declare somewhere among the lists of declarations and then may be generated in the sequence of statements. Example: -------- block var n,j:integer; var A:arrayof integer; (* here is the declaration of A *) begin read(n); new_array A dim (1:n); (* here is the generation of A *) for i:=1 to n do read(A(i)); od; (* etc.*) end A variable A is an array variable. Its value should be the reference to an integer array, i.e. a composite object consisting of integer components each one defined by an integer index. Array generation statement: new_array A dim (1:n); allocates a one-dimensional integer array with the index bounds 1,n , and assigns its address to variable A. The figure below illustrates this situation: ---------- ----------- | | | A(1) | | | ----------- | ... | | A(2) | ---------- ----------- | n | | | ---------- ... | j | | | ---------- ----------- | A | ---------> | A(n) | ---------- ----------- Block object Array object A general case of array generation statement has the form: new_array A dim (lower:upper) where lower and upper are arithmetic expressions which define the range of the array index. Example: -------- Two-dimensional array declaration : var A: arrayof arrayof integer; and generation: new_array A dim (1:n) for i:=1 to n do new_array A(i) dim (1:m) od; create the structure: ---------- | A(1,1) | ---------- | | ... | | ------------ ---------- | A(1) | ---------> | A(1,m) | ------------ ---------- | | ... | | ------------ ---------- | A(n) | ---------> | A(n,1) | ------------ ---------- | | ... | | ---------- | A(n,m) | ---------- Example: -------- block var i,j:integer, A,B: arrayof arrayof real, n:integer; begin read(n); new_array A dim (1:n); for i:=1 to n do new_array A(i) dim (1:n) od; (* A is square array *) new_array B dim (1:n); for i:=1 to n do new_array B(i) dim(1:i) od; (* B is lower triangular array *) A(n,n):=B(n,n); B(1):=A(1); B(1):=copy(A(1)); end Array A is the square array n by n. Each element A(i) , 1<=i<=n contains the address of row A(i,j), 1<=j<=n. Array B is the lower-triangular array. Each element B(i), 1<=i<=n, contains the address of row B(i,j), 1<=j<=i. Thus an assignment statement A(n,n):=B(n,n) transmits real value B(n,n) to real variable A(n,n). Assignment B(1):=A(1) transmits the address of the first row of A to variable B(1). Finally assignment B(1):=copy (A(1)) creates a copy of the first row of A and assigns its address to B(1). Upper and lower bounds of an adjustable array A are determined by standard operators lower(A) and upper(A). Example: -------- unit sort: procedure(A:arrayof integer); (* insertion sort *) var n,i,j:integer; var x:integer; begin n:=upper(A); (* assume lower bound is 1 *) for i:=2 to n do x:=A(i); j:=i-1; do if x >= A(j) then exit fi; A(j+1):=A(j); j:=j-1; if j=0 then exit fi; od; A(j+1):=x od; end sort; If an array variable A refers to no array its value is equal none (the standard default value of any array variable). An attempt to access an array element (e.g. A(i)) or a bound (e.g. lower(A)), where A is none, raises a run time error. - 24 - 6. Coroutines and semicoroutines ################################ Coroutine is a generalization of class. A coroutine object is an object such that the execution of its sequence of statements can be suspended and reactivated in a programmed manner. Consider first a simple class with a sequence of statements such that after return some non-executed statements remain. The generation of its object terminates with the execution of return statement, although the object can be later reactivated. If such a class is declared as a coroutine, then its objects may be reactivated. This can be realized by attach statement: attach(X) where X is a reference variable designating the activating coroutine object. In general, since the moment of generation a coroutine object is either active or suspended. Any reactivation of a suspended coroutine object X (by attach(X)) causes the active coroutine object to be suspended and continues the execution of X from the statement following the last executed one. Main program is also a coroutine. It is accessed through the standard variable main and may be reactivated (if suspended) by the statement attach(main). Example: -------- In the example below the cooperation of two coroutines is presented. One reads the real values from an input device, another prints these values in columns on a line-printer, n numbers in a line. The input stream ends with 0. program prodcons; var prod:producer,cons:consumer,n:integer,mag:real,last:bool; unit producer: coroutine; begin return; do read(mag); (* mag- nonlocal variable, common store *) if mag=0 then (* end of data *) last:=true; exit fi; attach(cons); od; attach(cons) end producer; unit consumer: coroutine(n:integer); var Buf:arrayof real; var i,j:integer; begin new_array Buf dim(1:n); return; do for i:=1 to n do Buf(i):=mag; attach(prod); if last then exit exit fi; od; for i:=1 to n do (* print Buf *) write(' ',Buf(i):10:2) od; writeln; od; (* print the rest of Buf *) for j:=1 to i do write(' ',Buf(j):10:2) od; writeln; attach(main); end consumer; begin prod:=new producer; read(n); cons:=new consumer(n); attach(prod); writeln; end prodcons; The above task could be programmed without coroutines at all. The presented solution is, however, strictly modular, i.e. one unit realizes the input process, another realizes the output process, and both are ready to cooperate with each other. LOGLAN-82 provides also a facility for the semi-coroutine operations. This is gained by the simple statement detach. If X is the active coroutine object, then detach reactivates that coroutine object at where the last attach(X) was executed. This statement meets the need for the asymetric coroutine cooperations. (by so it is called semi-coroutine operation). Operation attach requires a reactivated coroutine to be defined explicitly by the user as an actual parameter. Operation detach corresponds in some manner to return in procedures. It gives the control back to a coroutine object where the last attach(X) was executed, and that coroutine object need not be known explicitly in X. This mechanism is, however, not so secure as the normal control transfers during procedure calls and returns. In fact, the user is able to loop two coroutines traces by : attach(Y) in X attach(X) in Y Then detach in X reactivates Y, detach in Y reactivates X. In the example below the application of detach statement is illustrated. Example: -------- program reader_writers; (* In this example a single input stream consisting of blocks of numbers, each ending with 0, is printed on two printers of different width. The choice of the printer is determined by the block header which indicates the desired number of print columns. The input stream ends with a double 0. m1 - the width of printer_1, m2 - the width of printer_2 *) const m1=10,m2=20; var reader:reading,printer_1,printer_2:writing; var n:integer,new_sequence:boolean,mag:real; unit writing:coroutine(n:integer); var Buf: arrayof real, i,j:integer; begin new_array Buf dim (1:n); (* array generation *) return; (* return terminates coroutine initialization *) do attach(reader); (* reactivates coroutine reader *) if new_sequence then (* a new sequence causes buffer Buf to be cleared up *) for j:=1 to i do write(' ',Buf(j):10:2) od; writeln; i:=0; new_sequence:=false; attach(main) else i:=i+1; Buf(i):=mag; if i=n then for j:=1 to n do write(' ',Buf(j):10:2) od; writeln; i:=0; fi fi od end writing; unit reading: coroutine; begin return; do read(mag); if mag=0 then new_sequence:=true; fi; detach; (* detach returns control to printer_1 or od end reading; begin reader:=new reading; printer_1:=new writing(m1); printer_2:=new writing(m2); do read(n); case n when 0: exit when m1: attach(printer_1) when m2: attach(printer_2) otherwise write(" wrong data"); exit esac od end; Coroutines play the substantial role in process simulation. Class Simulation provided in Simula-67 makes use of coroutines at most degree. LOGLAN-82 provides for easy simulation as well. The LOGLAN-82 class Simulation is implemented on a heap what gives lg(n) time cost (in contrast with O(n) cost of the original implementation). It covers also various simulation problems of large size and degree of complexity. 7. Prefixing ############ Classes and prefixing are ingenius inventions of Simula-67 (cf [1]). Unfortunately they are hardly ever known and, perhaps, by this have not been introduced into any other programming language. Moreover, implementation constraints of Simula-67 bind prefixing and classes workableness to such a degree that both facilities cannot be used in all respects. We hope that LOGLAN-82, adopting merits and rooting up deficiencies of these constructs, will smooth their variations and vivify theirs usefulness. What is prefixing ? First of all it is a method for unit extending. Consider the simplest example: unit bill: class; var dollars :real, not_paid :boolean, year,month,day :integer; end bill; Assume the user desires to extend this class with new attributes. Instead of writing a completely new class, he may enlarge the existing one: unit gas_bill:bill class; var cube_meters: real; end gas_bill; Class gas_bill is prefixed by class bill. This new declaration may appear anywhere within the scope of declaration of class bill. (In Simula-67 such a prefixing is forbidden in nested units.) Class gas_bill has all the attributes of class bill and additionally its own attributes (in this case the only one: cube_meters). The generation statement of this class has the form: z:=new gas_bill; where z is a reference variable of type gas_bill. Remote access to the attributes of prefixed class is standard: z.dollars:=500.5; z.year:=1982; z.month:=3; z.day:=8; z.cube_meters:=100000; Consider now the example of a class with parameters. Assume that in a program a class: unit id_card: class(name:string,age:integer); end id_card; and its extension: unit idf_card:id card class(first name:string); end idf_card; are declared. Then for variable z of type id_card and variable t of type idf_card the corresponding generation statement may be the following: z:=new id_card("kreczmar",37); t:=new idf_card("Kreczmar",37,"Qntoni"); Thus the formal parameters of a class are concatenated with the formal parameters of its prefix. One can still extend class idf_card. For instance: unit idr_card:idf_card class; var children_number:integer; var birth_place:string; end idr_card; Prefixing allows to build up hierarchies of classes. Each one hierarchy has a tree structure. A root of such a tree is a class without prefix. One class is a successor of another class iff the first is prefixed by the latter one. Consider the prefix structure: A . . . . . . . . . B. .C .D . . .E . . .F . . . . G. .H Class H has a prefix sequence A, B, E, F, H. Let a, b, ... , h denote the corresponding unique attributes of classes A, B, ... , H, respectively. The objects of these classes have the following forms: ------------ ------------ ------------ ------------ | a | | a | | a | | a | ------------ ------------ ------------ ------------ object A | b | | c | | d | ------------ ------------ ------------ object B object C object D ------------ ------------ ------------ ------------ | a | | a | | a | | a | ------------ ------------ ------------ ------------ | b | | b | | b | | b | ------------ ------------ ------------ ------------ | e | | e | | e | | e | ------------ ------------ ------------ ------------ object E | f | | f | | f | ------------ ------------ ------------ object F | g | | h | ------------ ------------ object G object H Let Ra, Rb,..., Rh denote reference variables of types A, B,..., H, respectively. Then the following expressions are correct: Ra.a, Rb.b, Rb.a, Rg.g, Rg.f, Rh.h, Rh.f, Rh.e, Rh.b, Rh.a etc. Variable Ra may designate the object of class B (or C,..., H), i.e. the statement: Ra:=new B is legal. But then attribute b is not accessible through dot via Ra, i.e. Ra.b is incorrect. This follows from insecurity of such a remote access. In fact, variable Ra may point any object of a class prefixed by A, in particular, Ra may point the object of A itself, which has no attribute b. If Ra.b had been correct, a compiler should have distiguish the cases Ra points to the object of A or not. But this, of course, is undistinguishable at compilation time. To allow, however, the user's access to attribute b (after instruc tion Ra:= n Ra qua B The correctness of this expression is checked at run time. If Ra designates an object of B or prefixed ba B, the type of the expression is B. Otherwise the expression is erroneous. Thus, for instance, the expressions: Ra qua G.b, Ra qua G.e etc. enable remote access to the attributes b, c, ... via Ra. So far the question of attribute concatenation was merely discussed. However the sequences of statements can be also concatenated. Consider class B prefixed with class A. In the sequence of statements of class A the keyword inner may occur anywhere, but only once. The sequence of statements of class B consists of the sequence of statements of class A with inner replaced by the sequence of statements of class B. unit A :class unit B:A class ... ... begin begin ... |---... | | ... |---... end A; end B; In this case inner in class B is equivalent to the empty statement. If class B prefixes another class, say C, then inner in B is replaced by the sequence of statements of class C, and so on. If inner does not occur explicitly, an implicit occurrence of inner before the final end of a class is assumed. Example ------- Let class complex be declared as usual: unit complex: class(re,im:real); end complex; and assume one desires to declare a class mcomplex with the additional attribute module. In order the generation of class mcomplex define the value of attribute module, one can declare a class: unit mcomplex:complex class; var module:real; begin module:=sqrt(re*re+im*im) end mcomplex; Class mcomplex may be still extended: unit pcomplex:mcomplex class; var alfa:real; begin alfa:=arccos(re/module) end pcomplex; For these declarations each generation of class mcomplex defines the value of attribute module, each generation of class pcomplex defines the values of attributes module and alfa. For reference variables z1, z2 z3 of type complex, the following sequence of statements illustrates the presented constructs: z1:=new complex(0,1); z2:=new mcomplex(4,7); z3:=new pcomplex(-10,12); if z2 qua mcomplex.module > 1 then z1:=z2; fi; if z3 qua pcomplex.alfa < 3.14 then z3.re:=-z3.re; z3.alfa:=z3.alfa+3.14; fi; z1 qua mcomplex.module:= 0; z1.re,z1.im:=0; Example: -------- Binary search tree (Bst) is a binary tree where for each node x the nodes in the left subtree are less than x, the nodes in the right subtree are greater than x. It is the well-known exercise to program the algorithms for the following operations on Bst: member(x) = true iff x belongs to Bst insert(x), enlarge Bst with x, if x does not yet belong to Bst We define both these operations in a class: unit Bst: class; unit node: class(value:integer); (* tree node *) var left,right:node; end node; var root:node; unit help: class(x:integer); (* auxiliary class *) var p,q:node; begin q:=root; while q=/= none do if x < q.value then p:=q; q:=q.left; repeat (* jump to the beginning of a loop *) fi; if q.value < x then p:=q; q:=q.right; repeat fi; exit od; inner (* virtual instruction to be end help; unit member:help function:boolean; (* x is a formal parameter derived from the prefix help *) begin result:=q=/=none end member; unit insert:help procedure; (* x is a formal parameter derived from the prefix help *) begin if q=/=none then return fi; q:=new node(x); if p=none then root:=q; return fi; if p.value < x then p.right:=q else p.left:=q fi; end insert; begin inner; end Bst; In the example the common actions of member and insert are programmed in class help. Then it suffices to use class help as a prefix of function member and procedure insert, instead of redundant occurrences of the corresponding sequence of statements in both units. Class Bst may be applied as follows: var X,Y:Bst; begin X:=new Bst; Y:=new Bst; call X.insert(5); if Y.member(-17) then .... end As shown in the declaration of Bst, class may prefix not only other classes but also procedures and functions. Class may prefix blocks as well. Example: -------- Let class push_down (p. 5) prefix a block: pref push_down(1000) block var ... begin ... call push(50); ... i:=pop; ... end In the above block prefixed with class push_down one can use pop and push as local attributes. (They are local since the block is embedded in the prefix push down.) Example: -------- pref push down(1000) block begin ... pref Bst block begin (* in this block both structures push down and Bst are visible *) call push(50); call insert(13); if member(10) then ... i:=pop; ... end end In place where classes push_down and Bst are visible together a block prefixed with Bst may be nested in a block prefixed with push_down (or vice versa). In the inner block both data structures are directly accessible. Note that this construct is illegal in Simula 67. 8. Formal types ############### Formal types serve for unit parametrization with respect to any non-primitive type. Example: -------- unit Gsort:procedure(type T; A:arrayof T; function less(x,y:T):boolean); var n,i,j:integer; var x:T; begin n:=upper(A); for i:=2 to n do x:=A(i); j:=i-1; do if less(A(j),x) then exit fi; exit fi A(j+1):=A(j); j:=j-1; if j=0 then exit fi; od; A(j+1):=x; od end Gsort; Procedure Gsort (the generalization of procedure sort from p.4) has type parameter T. A corresponding actual parameter may be an arbitrary non-primitive type. An actual parameter corresponding to A should be an array of elements of the actual type T. Function less should define the linear ordering on the domain T. For instance, the array A of type bill (cf p.7) may be sorted with respect to attribute dollars , if the function: unit less: function(t,u:bill):boolean; begin result:=t.dollars <= u.dollars end less; is used as an actual parameter: call Gsort(bill,A,less); If the user desires to sort A with respect to date, it is sufficient to declare : unit earlier:function(t,u:bill):boolean; begin if t.year < u.year then result:= true; return fi; if t.year=u.year then if t.month < u.month then result:=true; return fi; if t.month=u.month then result:=t.day<=u.day fi fi; end earlier; and to call: call Gsort(bill,A,earlier); 9. Protection techniques ######################## Protection techniques ease secure programming. If a program is large, uses some system classes, is designed by a team etc., this is important (and non-trivial) to impose some restrictions on access to non-local attributes. Let us consider a data structure declared as a class. Some of its attributes should be accessible for the class users , the others should not. For instance, in class Bst (p.7) the attributes member and insert are to be accessible. On the other hand the attributes root, node and help should not be accessible, even for a meddlesome user. An improper use of them may jeopardize the data structure invariants. To forbid the access to some class attributes the three following protection mechanisms are provided: close, hidden, and taken. The protection close defined in a class forbids remote access to the specified attributes. For example, consider the class declaration: unit A: class; close x,y,z; var x: integer, y,z:real; .... end Remote access to the attributes x,y,z from outside of A is forbidden. The protection hidden (with akin syntax) does not allow to use the specified attributes form outside of A neither by the remote access nor in the units prefixed by A. The only way to use a hidden attribute is to use it within the body of class A. Protection taken defines these attributes derived from prefix, which the user wishes to use in the prefixed unit. Consider a unit B prefixed by a class A. In unit B one may specify the attributes of A which are used in B. This protects the user against an unconscious use of an attribute of class A in unit B (because of identifier conflict). When taken list does not occur , then by default, all non-hidden attributes of class A are accessible in unit B. 10. Programmed deallocation ########################### The classical methods implemented to deallocate class objects are based on reference counters or garbage collection. Sometimes the both methods may be combined. A reference counter is a system attribute holding the number of references pointing to the given object. Hence any change of the value of a reference variable X is followed by a corresponding increase or decrease of the value of its reference counter. When the reference counter becomes equals 0, the object can be deallocated. The deallocation of class objects may also occur during the process of garbage collection. During this process all unreferenced objects are found and removed (while memory may be compactified). In order to keep the garbage collector able to collect all the garbage, the user should clear all reference variables , i.e. set to None, whenever possible. This system has many disadvantages. First of all, the programmer is forced to clear all reference variables, even those which are of auxiliary character. Moreover, garbage collector is a very expensive mechanism and thus it can be used only in emergency cases. In LOGLAN a dual operation to the object generator, the so-called object deallocator is provided. Its syntactic form is as follows: kill(X) where X is a reference expression. If the value of X points to no object (none) then kill(X) is equivalent to an empty statement. If the value of X points to an object O, then after the execution of kill(X), the object O is deallocated. Moreover all reference variables which pointed to O are set to none. This deallocator provides full security, i.e. the attempt to access the deallocated object O is checked and results in a run-time error. For example: Y:=X; kill(X); Y.W:=Z; causes the same run-time error as: X:=none; X.W:=Z; The system of storage management is arranged in such a way that the frames of killed objects may be immediately reused without the necessity of calling the garbage collector, i.e. the relocation is performed automatically. There is nothing for it but to remember not to use remote access to a killed object. (Note that the same problem appears when remote access X.W is used and X=none). Example: -------- Below a practical example of the programmed deallocation is presented. Consider class Bst (p.7). Let us define a procedure that deallocates the whole tree and is called with the termination of the class Bst. unit Bst:class; (* standard declarations list of Bst *) unit kill_all:procedure(p:node); (* procedure kill_all deallocates a tree with root p *) begin if p= none then return fi; call kill_all(p.left); call kill_all(p.right); kill(p) end kill_all; begin inner; call kill_all(root) end Bst; Bst may be applied as a prefix: pref Bst block ... end and automatically will cause the deallocation of the whole tree after return to call kill_all(root) from the prefixed block. To use properly this structure by remote accessing one must call kill_all by himself: unit var X,Y:Bst; ... begin X:=new Bst; Y:=new Bst; ... (* after the structures' application *) call X.kill_all(X.root); kill(X); call Y.kill_all(Y.root); kill(Y); ... end Finally note that deallocator kill enables deallocation of array objects, and suspended coroutines and processes as well (cf p.13). 11. Exception handling ####################### Exceptions are events that cause interruption of normal program execution. One kind of exceptions are those which are raised as a result of some run time errors. For instance, when an attempt is made to access a killed object, when the result of numeric operation does not lie within the range, when the dynamic storage allocated to a program is exceeded etc. Another kind of exceptions are those which are raised explicitly by a user (with the execution of the raise statement). The response to exceptions (one or more) is defined by an exception handler. A handler may appear at the end of declarations of any unit. The corresponding actions are defined as sequences of statements preceded by keyword when and an exception identifier. Example: -------- In procedure squareeq (p.3) we wish to include the case when a=0. It may be treated as an exception (division by zero). unit squareeq(a,b,c:real;output xr,xi,yr,yi:real); var delta:real; handlers when division_by_zero: if b =/= 0 then xi,yr,yi:=0; xr:=-c/b; terminate else raise Wrong_data(" no roots") fi; end begin ... end squareeq; The handler declared in that procedure handles the only one exception (division_by_zero). When an exception is raised, the corresponding handler is searched for, starting from the active object and going through return traces. If there is no object containing the declaration of the handler, then the program (or the corresponding process) is terminated. Otherwise the control is transferred to the first found handler. In our example the handler is declared within the unit itself, so control is passed to a sequence: if b=/=0 ... Therefore, when b=/=0, the unique root of square equation will be determined and the procedure will be normally terminated (terminate). In general, terminate causes that all the objects are terminated, starting from that one where the exception was raised and ending on that one where the handler was found. Then the computation is continued in a normal way. In our example, when b=0, a new exception is raised by the user. For this kind of exceptions , the exception itself should be declared (because it is not predefined as a run time error). Its declaration may have parameters which are transmitted to a handler. The exception declaration need not be visible by the exception handler. However the way the handler is searched for does not differ from the standard one. Consider an example: block signal Wrong_data(t:string); unit squareeq: ... end squareeq; ... begin ... end Exception Wrong_data may be raised wherever its declaration (signal Wrong_data) is visible. When its handler is found the specified sequence of actions is performed. In the example above different handlers may be defined in inner units to the main block where squereeq is called. The case a=0 could be included , of course, in a normal way, i.e. by a corresponding conditional statement occurring in the procedure body. But the case a=0 was assumed to be exceptional (happens scarcely). Thus the evaluation of condition a=0 would be mostly unnecessary. As can be noticed thanks to exceptions the above problem can be solved with the minimal waste of run time. 12. Separate compilation (this section does not apply to PC version) ######################## 13. Processes ############# The implementation of processes is different (May 1988) c.f. user's manual. Process in LOGLAN-82 is a natural generalization of coroutine (cf p.6). Coroutines are units which once generated may operate independently, each one treated as a separate process. For coroutines, however, an essential assumption is established; namely, when one coroutine object is reactivated, the active one must be suspended (i.e. there which is onle one control is switched between coroutine objects). When processes are used, the activation of a new process does not require the active one to be suspended. So many objects may be simultaneously active. The statement that reactivates a suspended process X (without suspention of the active one) has the form: resume(X) The main problem of parallel programming is, of course, synchronization. Elementary synchronization in LOGLAN-82 is achieved by two-valued semaphores and some number of simple statements operating on them. A semaphore variable controls the entry to a critical region, i.e. a sequence of statements that may be executed by the one process only. When a semaphore is open, the corresponding critical region is free. When a semaphore is closed, it means the corresponding critical region is just executed by a certain process. These are the simple indivisible statements that operate on semaphores: lock(S) - If semaphore S is open, the given process enters the critical region guarded by S , and simultaneously, semaphore S becomes closed. If semaphore S is already closed, the given process waits until the critical region is open (by another process). unlock(S)- If semaphore S is closed, then it becomes open. Otherwise the statement is empty. stop(S) - The statement causes semaphore S to be open, and simultaneously, it stops the given process execution. The statement may be used without a parameter, and then, it stops the given process execution. Moreover, only those three above statements may change the values of semaphore variables. In general, several processes may wait for an entry to the same critical region. When the process executing this critical region opens the semaphore (by unlock or stop), one of the waiting processes is reactivated and enters the region. The way such a process is selected is not defined by the language. The user must assume that this selection is performed arbitrarily. Example: -------- In the example an input stream of real numbers is copied. The copying process is parallelized in such a way that when process get reads the next number, the process put writes simultaneously the number read in the preceding step. The input stream ends with 0. block var in_buf,out_buf:real, completed:boolean, sem:semaphore; var counter:integer,get:get_type,put:put_type; unit cobegin:procedure; (* called by the main program *) begin lock(sem); (* critical region *) resume(get); (* activate reading *) resume(put); (* activate writing *) stop(sem); (* exit from critical region *) end cobegin; unit coend: procedure; begin (* called by get and put *) lock(sem); (* entry to critical region *) if counter=0 then (* one process entered *) counter:=1 else (* two processes entered *) counter:=0; resume(main) (* reactivate main program *) fi; stop(sem) (* exit from critical region *) end coend; unit get_type:process; begin return; do read(in_buf); if in_buf=0 then (* end of data *) completed:=true fi; call coend od end get_type; unit put_type:process; begin return; do write(out_buf); call coend; od end put_type; begin (* main process *) read(in_buf); get:=new get_type; put:=new put_type; do out_buf:=in_buf; call cobegin; if completed then exit fi; od; kill(get); kill(put); end; Two procedures cobegin and coend synchronize the execution of the main loop. Procedure cobegin implements fork operator, procedure coend called from processes put and get implements the end of fork operator. Variable count defines the number of processes that called procedure coend. By an easy modification one can generalize these procedures in order to implement the general k-fork and end of k-fork operators (if count can assume the values 0,1,...,k-1). Finally, let us present an example of a class that realizes Hoare's monitors (cf. [2]). Monitor is a structure that synchronizes the access to a common pool of data. The number and kinds of these data are defined by the user. Monitor task is only to give non-conflict access to it. The access to a monitor is realized by the so-called entry procedures. Entry procedure has a prefix entry which guarantees that only one such a procedure may enter the monitor. In order to allow scheduling of processes that entered the monitor, two specialized procedures operating on the inner monitor queues are provided. delay(Q) - stops the execution of the process and puts it into a queue Q, the entry to the monitor is free, continue(Q) - resumes the execution of the first process from a queue Q (if Q is non-empty, otherwise the entry to the monitor is free). As can be easily seen, the correct use of these constructs is achieved when continue is called as the last statement of an entry procedure. The declaration of the class Monitor is as follows: unit Monitor : queue class; hidden sem,queue; (* hidden protects attributes sem and queue *) var sem:semaphore; (* sem is the semaphore guarding the monitor entry *) unit entry: class; (* all entry procedures must have prefix entry which realized non-conflict access to Monitor *) var busy:boolean; (* busy is true iff continue(Q) was executed hidden busy; unit delay: procedure(Q:queue); begin call Q.into(this process); (* put the active process into queue Q *) stop(sem) (* free the monitor access, halt the active process *) end delay; unit continue:procedure(Q:queue); (* continue can be called as the last statement of an entry procedure *) begin if not Q.empty then busy:=true resume(Q.out); (* resume the next process from queue Q *) fi; end continue; begin (* beginning of the prefix entry *) lock(sem); (* entry to the critical region *) inner; (* the virtual body of an entry procedure *) if not busy then unlock(sem) (* free the monitor access, unless continue fi; end entry; end Monitor; The mail-box structure which receives and sends the items of type T may be implemented as the following class prefixed by Monitor: unit Buffering:Monitor class(type T;size:integer); var Pool:arrayof T,count,in_index,out_index:integer; var readers_queue,writers_queue:queue; unit writer:entry procedure(r:T); begin if count=size then call delay(writers_queue) fi; in_index Pool(in_index):=r; call continue(readers_queue); end writer; unit reader:entry procedure(output r:T); begin if count=0 then call delay(readers_queue) fi; out_index:=out_index mod size +1; count:=count-1; r:=Pool(out_index); call continue(writers_queue); end reader; begin new_array Pool dim (1:size); readers_queue:=new queue; writers_queue:=new queue; end Buffering; References. ########### [1] Dahl O-J.,Myhrhaug B,Nygaard K.: Common Base Language . NCC S-22, October 1970 [2] Hoare C.A.R.: Monitors, an operating system structuring concept. CACM,vol.17,N.10,October 1974,pp.549-57 [3] LOGLAN-82 Report , Warsaw, 1982