home *** CD-ROM | disk | FTP | other *** search
Wrap
Text File | 1993-02-27 | 100.9 KB | 3,347 lines
The GRS programming language and environment. Reference Manual. Steven Inkster. Guy Verbist. Department Of Computer Science. Heriot Watt University. Archimedes Public Domain Release 1.05 September 1991 Acknowledgement. The authors would like to thank Dr. N. W. Paton for his invaluable aid and advice throughout the design and implementation of this language. Contents 1.0 Basics. 1 1.1 Format of input. 1 1.2 Comments. 1 1.3 Statements. 1 1.4 Variables. 1 1.4.1 Types. 2 1.4.2 Declarations. 2 1.4.3 Implicit declarations. 3 1.5 Constants. 4 1.5.1 Integers. 4 1.5.2 Strings. 4 1.5.3. Expressions. 4 1.6. Operators. 5 1.7 Assignment. 7 1.8 Loops. 7 1.9 Blocks. 7 1.10 Iterators. 8 1.11 Functions. 8 1.11.1 Declaration. 9 1.11.2 Return. 10 1.11.3 Calling. 10 1.12 The assume statement. 10 1.13 System defined functions. 11 2.0 Expressions and meta - level programming. 15 2.1 Null expressions. 15 2.1.2 Using null expressions as slots, or reusing outside an assume. 19 2.1.3 Assigning expressions. 20 2.2 Typed expressions. 20 3.0 The Object Store. 22 3.1 Introduction 22 3.2 What is an object? 22 3.3 How to create an object 23 3.4 The object hierarchy 23 3.6 More on methods and slots 25 3.7 Objects as variables 26 3.8 Objects as slots 26 3.9 Summary 26 3.10 Advanced use of objects 27 3.10.1 isa and super 27 3.10.2 Creating new class objects 29 3.10.3 Referring to objects by their literal name 29 3.11 Methods to manipulate objects 30 3.11.1 Other pre-defined methods 31 3.12 Directly manipulating the object store 32 4.0 The GRS programming environment 35 4.1 Getting started 35 4.2 Running programs 35 4.3 A short example 36 4.4 Organising GRS programs 36 4.5 Debugging options 38 4.6 Resources required by GRS 38 4.7 Recovering from errors 39 4.8 Common sources of error 39 5.0 References. 40 A) Some example GRS code 41 Desk calculator. 41 Database. 41 Some short programs to demonstrate some of the features of GRS 45 Sharing objects and using objects as parameters and slots 45 Local "methods" / replacing methods 48 A simple front-end to GRS 49 A tank game 50 B) Compiler / Analyser Error Messages. 56 B1) Syntax errors. 56 B2) Semantic errors. 59 C) Interpreter / Object Store Error Messages. 65 C1) Internal fatal errors 65 C 1.1) Stack errors 65 C 1.2) Errors around system defined functions 66 C 1.3) Miscellaneous errors 67 C 1.4) Object store errors 69 C 1.5) Memory failures 69 C 1.6) User errors 70 C 1.7) User warnings 71 D) BNF grammar for GRS. 72 E) Known Infelcities Of Version 1.05. 77 1.0 Basics. GRS is a strongly typed object - oriented language. Strong typing is unusual in most object - oriented langauges, however this stricture encourages a more disciplined and modular approach. As opposed to most object - oriented languages, GRS supports an array of meta - level programming constructs, allowing the programmer to change the behaviour of objects dynamically. The implementation of GRS incorporates an interactive environment allowing the state of the system to be monitored. This section deals with the normal facilities of GRS which match those of other high level languages closely. Section two explains the meta - level facilities in more detail. Section three deals with the use of the object store, and finally section four serves as a guide to interacting with the environment. This manual assumes a knowledge of a high level language such as C or Pascal. 1.1 Format of input. GRS is a free form language. That is, no restrictions are placed on the typographical appearance of source text. It may be indented, and statements may span over as many or as few lines as the programmer deems necessary. Any character in a source file which is outside the range of those used in GRS constructs is ignored. 1.2 Comments. Anything within (* and *) is a comment and is ignored completely. 1.3 Statements. GRS is a statement based language. A statement is terminated by a semicolon. 1.4 Variables. Variables have a type and an identifier. 1.4.1 Types. Inbuilt types are: integer string null and these are augmented by the ability to create a listof any type, and an expression returning any type. To create a list of a type, the keyword listof is placed before the type e.g. listof integer To create an expression, the keyword expression is placed after the type. To avoid ambiguity when combining listof and expression the type being "expressioned" is placed in round brackets e.g. (integer) expression is an expression which returns an integer listof (string) expression is a list of expressions which return strings (listof string) expression is an expression which returns a list of strings listofs and expressions can be nested to any level. 1.4.2 Declarations. Variables are declared with a name and a type. A valid variable name begins with an alphabetic character, upper or lower case, which is followed by zero or more characters from a-z, A-Z, 0-9 and '_'. A variable declaration begins with a type and then a list of identifiers separated by commas. Valid variable declarations include: integer one; integer two, three; listof string ls; (null) expression ne; If a declaration for a variable is present in the source file, a variable with that name will be declared once and only once, no matter where the declaration occurs e.g. The following are legal and execute as expected: let integer i := 0; if i=1 then string s; (* will ALWAYS be declared *) write("Hello\n"); endif; s:= "Hello again\n"; write(s); let integer j := 6; loop integer i; (* again will always be declared *) i:=4; (* but only once *) exiton(j>10); j := j+i; endloop; 1.4.3 Implicit declarations. The loop control variable of an iterator is implicitly declared within the scope of the iteration, with the type of an element of the list it is iterating across. (see later) The let statement declares and assigns to a variable in one statement e.g. let integer i := 10; Note that when a let statement occurs within a loop the variable will be declared only once, but the assignment in the statement will be executed as many times as the loop runs. A function declaration implicitly declares the function name in the level of scope of the declaration, and the function arguments within the level of scope inside the function. 1.5 Constants. Valid constants (literals) are string constants, integer constants, expression constants, and list constants. 1.5.1 Integers. An integer constant is one or more digits e.g. 0 1234532 96 Note that -96 is not a constant it is the integer constant 96 preceded by the unary minus operator. The minimum value for an integer is -2147483648 and the maximum 2147483647. 1.5.2 Strings. A string constant is zero or more ascii characters between inverted commas "". Within a string constant the backslash character '\' has a special significance. A single backslash on its own is not permitted, but several combinations of the backslash and other characters are allowed. \\ means a single backslash within a string \" means an inverted comma within a string \n is interpreted by the write() function as a new line \t is interpreted by the write() function as a horizontal tab When the string is stored the surrounding inverted commas are removed and backslashes which precede other backslashes or inverted commas are also removed. This is important when using the compile() function with string constants. (see later) The maximum length for a string read from the keyboard is 1023 characters, but a constant string in a program has unlimited length. 1.5.3. Expressions. An expression constant is any expression enclosed in curly brackets (braces) {}. A typed expression has the type of the expression within the brackets. A null expression is one or more program statements within braces, since an expression has type null e.g. integer i; let (integer) expression ie := {1+4}; let (null) expression ne := {write("Hello");}; It should be noted that null expressions have their own local level of scope so a variable declaration within a null expression is local to that expression, but can be brought into the current level of scope by a call to exec() or summon(). Expressions are discussed more fully in section two. 1.6. Operators. GRS supports the following operators with operand types, result types and meanings: <graphic - "operators"> 1.7 Assignment. Assignment is much like assignment in any other high level language. An identifier followed by the ':=' symbol followed by an expression. The expression must be of exactly the same type as the variable; i.e. no coercion is possible. As noted above the let statement performs a declaration and an assignment in one statement. It is not legal to assign to a function name, even though the name may appear as a normal identifier. 1.8 Loops. Only one kind of loop construct is available at the basic level of GRS (but see 'Iterators' section 1.10) as opposed to the normal array of for, while, and repeat constructs in other languages. The construct is governed by one and only one integer expression, and the loop terminates when this expression becomes true (non zero). It takes the form: loop <zero or more statements> exiton(<expr>); <zero or more statements> endloop; There must be one and only one exiton() statement in a loop. 1.9 Blocks. A block is a statement containing zero or more statements enclosed in braces. A block is a single statement so it is useful when more than one statement is needing to be governed by an if statement, an iterator or an assume, so the statement governed can be a block containing many statements. Unlike the C language, variables declared within a block are not local to the block. 1.10 Iterators. An iterator is a type of loop construct which iterates across a list, performing actions for each element in the list. An iterator takes the form: foreach <varname> in <expression yielding a list> do <statement> An iterator has its own level of scope (see later) and the governing variable is declared within this scope with the type of an element of the list expression. The iterator gives <varname> the value of the head of <list expression>, executes <statement>, makes <list expression> equal to the tail of <list expression> and executes <statement> etc etc. It stops when <list expression> yields the empty list. e.g. let integer j := 0; foreach i in [1,2,3,4] do { j := j + i; write(j,"\n"); }; Iterators are of most use when interacting with the self description facilities of the object store. 1.11 Functions. Functions have a type and an identifier, like normal variables. If a function is not intended to return anything then it can be declared as a null function. Functions can have any number of parameters, which are value, not variable parameters. Values for the function are returned via the return mechanism. Local variables and functions are possible, as is recursion. It is even possible to declare a local function with the same name as the parent function and still be able to recurse to the parent function, as will be explained later. 1.11.1 Declaration. A function declaration has the form <type> function <ident> ( <paramlist>) { <statements> }; The <paramlist> is a set of normal variable declarations separated by semi-colons, the last declaration is not followed by a semi-colon. For instance listof string function fred(integer i; integer j,k) { ....... }; null function do_nothing() { }; The name of the function is declared in the level of scope of the declaration, and the parameters are declared local to the function i.e. the level of scope is raised at the opening bracket of the parameter list. Anything declared within the function is local to that function. As usual, a reference to identifier refers to the most local identifier with that name, so the following is possible: null function fred(integer i) { if i = 1 then fred(4); (*recursive*) integer function fred(string s) { write(s); }; fred("Hello"); (*refers to local function*) }; Functions may have objects as parameters, or return objects (see section 3). 1.11.2 Return. The value returned by a function comes from a return statement which takes the form return <expression>; where expression has the same type as the declaration of the function. There may be any number of return statements in a function e.g. integer function bigger_than_ten(integer param) { if param > 10 then return true; else return false; endif; }; 1.11.3 Calling. Function calls are as in any other high level language, the name of the function followed by a list of parameters separated by commas. The parameters must match exactly the number and types of those given in the declaration e.g. integer function add(integer i,j) { return i+j; (* brackets are not essential *) }; let integer k := add(1,1); 1.12 The assume statement. To allow the use of expressions that are evaluated late, it is possible to assume the existence of variables and functions at runtime which do not exist at compile time. The assume statement takes the form: assume <variable declaration> in <statement>; or assume <type> function <ident> (<paramlist>) in <statement>; e.g. assume integer i; string s in { let (integer) expression ne1 := {i+4}; let (string) expression ne2 := {"Hello "+s}; }; let integer i := 4; let string s := "world"; write(eval(ne2),eval(ne1)); It is only valid to assume functions within a method declaration (see later), to enable methods to "see" each other and for mutual recursion. The effect of referring to an assumed variable which does not exist at runtime is undefined. 1.13 System defined functions. The basic system - defined functions are as follows: null write(e1,e2,....) Takes any number of integer or string or lists of integers or strings expressions (not in the GRS sense of an expression) as parameters. Writes their literal value to the console device. null read(v1,v2,....) Takes any number of integer or string variables as parameters and reads their value from the console device. element head(list) Takes any kind of list as parameter and returns the head of the list. list tail(list) Takes any kind of list as a parameter and returns the tail of the list. (null) expression compile(string) The callable compiler which compiles the string to a null expression. It should be noted that, as with other null expressions, the contents of what is returned by compile() has its own level of scope, so any variable declarations within the string given as a parameter can only be accessed from within that string. null consult(string) Compiles and executes a file with the name string. type eval(typed expression) Evaluates a typed expression (with late binding of the value and scope of variables) and returns the value. null exec((null) expression) Executes a null expression (including variable declarations) in the level of scope of the call to exec(). Variables in the expression are redeclared in the current level of scope i.e. the expression is semantically re-analysed. A call exec(ne) is equivalent to summon(ne); run(ne). null summon((null) expression) Semantically analyses a null expression and redeclares the variables in the expression in the current level of scope, but does not run the expression. null run((null) expression) Executes a null expression which must have been previously analysed by either exec() or summon(). integer len(string) Returns the number of characters in a string. string mid(string; integer a; integer b) Returns the string starting at the a'th character with length b, of the string given as parameter. null beep() Writes an ascii bel character to the console. integer rnd(integer) Returns a random integer between 1 and the value provided. null tab(integer x; integer y) Moves the screen cursor to the position x,y. null cls() Clears the console screen. string itos(integer) converts an integer to a string. integer stoi(string) converts a string to an integer. true and false are system defined variables which are initially set to 1 and 0 respectively, but the user may assign to them if he / she feels so inclined. There are also several system defined messages which may be sent to objects. These are explained in section 3. 2.0 Expressions and meta - level programming. Expressions will be unfamiliar to most new users so this section attempts to shed some light on their meaning and use. It will concern itself with * compile() * exec() * summon() * run() * eval() * typed expressions * null expressions * the assume statement 2.1 Null expressions. A null expression is a piece of code which can be passed around a program as a normal variable, and can be used as a slot for classes. Their primary use is for the creation of new classes, so that methods can be passed to the new() method, but they can be used in many other ways to make code far more efficient and intuitive than in normal imperative languages. For instance depending on a variable, different null expressions can be passed to a function so it will take different actions. However for this added convenience, a price is paid in the form of added complexity when considering such things as scope. A null expression can be created in two different ways, through the compile() function or by declaration as a constant in a program. compile() takes a string and compiles it to a null expression, constant null expressions are simply pieces of program code enclosed in curly brackets. However there are subtle differences between these methods, compile() simply compiles the expression but does not analyse the semantics of the expression, whereas constant expressions in a program are automatically analysed and could be run immediately. Null expressions have their own level of scope so variables declared within them are not visible outside the expression. e.g. let (null) expression ne := compile("let string s := \"Hello\n\"; write(s);"); (correct) (note the use of the \" symbol to prevent the inverted commas from being removed by the first pass of the compiler) or let (null) expression ne := {let string s := "Hello\n"; write(s);}; (correct) To execute a null expression either exec() or run() can be called. The difference between these two calls is that exec() runs the semantic analyser over the expression and declares any variables in the expression in the current level of scope, whereas run() only executes the code. To analyse an expression and redeclare the variables without executing it, a call should be made to summon(). exec(ne) is equivalent to summon(ne); run(ne);. e.g. exec({let string s := "Hello\n"; write(s);}); (correct) run({let string s := "Hello\n"; write(s);}); (correct) exec(compile("let string s:= \"Hello\n\"; write(s);")); (correct) are all correct, whereas let string s := "Goodbye\n"; exec({let string s := "Hello\n"; write(s);}); (wrong) is incorrect because it would redeclare string s in the level of scope to the call to exec() where an identifier with that name already exists. The call to exec() should be replaced with a call to run(). Also run(compile("let string s := \"Hello\n\"; write(s);")); (wrong) is incorrect because it is not meaningful to run expressions which have not been analysed. However let string s := "Goodbye\n"; exec({write(s);}); (correct) would be correct because there is no variable declaration for s in the expression. Note the semicolon after the write(s). If there were no semicolon then the expression would be an expression that returns a null because write() returns a null, but it would not be a null expression in the GRS sense, for which it must be a complete statement. Also assume string s in let (null) expression ne := {write(s);}; let (null) expression vardecl := compile("string s;"); exec(vardecl); assume string s in exec({s := "Hello\n";}); exec(ne); (* or run(ne *) (correct) would be correct because variables in expressions are looked up dynamically at run time. The reader may notice extensive use of the assume statement when constant null expressions are being created. It is used to tell the compiler that a variable referred to may not exist in the context of the input file at compile time, but will exist in the context of the expression at run time. It is only valid to assume a variable in an expression. All variable references outside expressions are bound at compile time. For instance assume integer i in let (null) expression ne := {write(i);}; (correct) integer i; i := 6; exec(ne); is meaningful, whereas assume integer i in let (null) expression ne := {write(i);}; (wrong) exec(ne); is not. One final point of note is that the semantic analyser uses a great deal of memory every time it is called, and the analysis process is relatively time consuming. Hence if is at all possible, it is advisable to call the analyser only once and then to use run(). If there are no variable declarations in a constant expression then it need not be called at all because the analyser was run at compile time e.g. let (null) expression ne := {write("Hello\n");}; (correct) run(ne); run(ne); run(ne); or let (null) expression ne := {write("Hello\n");}; (correct) exec(ne); (*still OK but wasting time and memory *) exec(ne); exec(ne); Now use a variable in the expression ...... let (null) expression ne := {let string s := "Hello\n"; write(s);}; run(ne); assume string s in exec({write(s);}); (*s not in scope*) (wrong) run(ne); run(ne); but it must be brought into scope if you are going to use it outside the expression. let (null) expression ne := {let string s := "Hello\n"; write(s);}; exec(ne); assume string s in exec({write(s);}); (*s is now in scope*) (correct) run(ne); run(ne); Also, expressions from the callable compiler must be be analysed first: let (null) expression ne := compile("write(\"Hello\n\");"); summon(ne); (* no variables but need to analyse *) run(ne); (correct) run(ne); run(ne); or let (null) expression ne := compile("write(\"Hello\n\");"); exec(ne); (*no variables but need to analyse, then run*) run(ne); (* or exec *) (correct) run(ne); (* or exec *) 2.1.2 Using null expressions as slots, or reusing outside an assume. Consider the declaration assume integer i in { (null) expression ne; ne := {write(i);}; }; To exec(ne) or run(ne) where i is in scope is perfectly meaningful. However if ne is passed to a method on a class and then exec'ed, with the intention of it finding i as a slot, this will not work. This is because within methods, slots are looked up dynamically at run time, so according to the semantic analyser the variable i will not exist. Note that if the expression did not come from compile() then it could be run() straight away inside the method with no problems. If the expression must be analysed within a method then technique is to put the assume inside the null expression so it is still present when the semantic analyser runs over the expression i.e. (null) expression ne; ne := {assume integer i in write(i);}; 2.1.3 Assigning expressions. If one null expression variable is assigned to another then both refer to the same variable, i.e. the expression is not copied, thus when the expression is referred to via one identifier, this reference will be reflected in the state of what the other identifier refers to. This is the same as the way in which objects can be assigned and referred to. 2.2 Typed expressions. A typed expression is a collection of meaningful symbols (i.e. in scope or assumed) which can normally be evaluated by the interpreter, but whose value is determined at run time depending on the position in the code of the evaluation. Identifiers are looked up dynamically starting at the call to eval() and using the nearest (by scope) match. It is not meaningful to try to compile typed expressions as null expressions can be compiled, this would be equivalent of attempting to compile e.g. 3+4 using a Pascal compiler. A typed expression takes its type automatically from its contents i.e. what is between the curly brackets e.g. {"hello"} (*(string) expression*) string s; {s} (*(string) expression*) {s+"hello"} (*(string) expression*) {123} (*(integer) expression*) integer i; {i/145} (*(integer) expression*) {[1,2,3]} (*(listof integer) expression*) [{1},{2},{3}] (*listof (integer) expression*) When a typed expression is passed to eval() the expression is evaluated and returned. As stated before the values of identifiers are those of the identifiers with that name which are nearest by scope. The type eval() returns is determined by its argument, and is bound at compile time. e.g. let (integer) expression ie := {4}; (correct) write(3+eval(ie)); For assignment types must be correct as per usual let (integer) expression ie := {"Hello"}; (wrong) Finding by scope ... assume string s in let (string) expression se := {s}; let string s := "Hello"; string function rubbish() { let string s := "Goodbye"; return eval(se); }; write(eval(se)); (*Hello*) write(rubbish()); (*Goodbye*) 3.0 The Object Store. 3.1 Introduction So far this manual has dealt with the basic elements of the GRS programming language such as assignment, loops, iterators and so on, and has looked in detail at the complexities of the expression type. With these techniques it is possible to write many impressive programs (see the examples given in the previous chapters and in section 5). This chapter explains how these features can be used to create objects. Firstly, the term object is explained, and through example code, it is shown how to create, access and manipulate objects in GRS. For further reading see "Object-oriented Software Construction" by Betrand Meyer. 3.2 What is an object? Most programming languages provide a means for creating data structures, that is, units of information consisting of several different attributes with unique names. For example, in the programming language C, a person may be described as struct person { char *name; int age; }; Here the person is given attributes name (a string), and age (an integer). The programmer is then free to create as many examples of type person as they wish, each of which will have a different (but not necessarily unique) name and age. Within the same program, functions may be written which manipulate examples of the type person. For example, to calculate the year of birth for a given person, a function may be written (in C) something like: int year_of_birth (struct person p, int this_year ) { return(this_year-p.age); } This simply takes the age of the given person, and subtracts it from the current year. It seems logical that year_of_birth and a person's age should belong together in some way. This is where objects come in. An object is simply a collection of attributes (commonly known as slots) and functions (known as methods). The object can only communicate to the rest of the world via its methods (which is why executing a method is known as sending a message) and its slots are invisible to the rest of the world. Only the object can access its own slots, and can only be commanded by its methods. This gives an object very dynamic characteristics. 3.3 How to create an object Now that we know what an object is, how does GRS allow objects to be created? This is achieved by sending a message to another object! When starting up GRS (see chapter 4) there are two objects already installed. The first is known as meta_class, the other is known as class. Before continuing, the hierarchy which exists between objects is explained. 3.4 The object hierarchy The example C code above explained how a structure describing a person is built. So, we may have a person whose name is Fred and age is 42. Fred is simply an instance of a person, which means that Fred can do all of the things a person can do. Similarly, a structure describing a tank may be built, with its position, and which side it is fighting for, meaning the King Tiger would be an instance of a tank, and could do everything which a tank could do. So, if we asked Fred to move forward, he would step forward. If we asked the King Tiger to move forward, there would be a grinding of gears, and it would move forward. If we asked Fred to sit down, he would do so, but the King Tiger would be a little lost! Similarly, asking the King Tiger to fire a shell a few miles would cause it no problems, Fred would however be struggling! The point here is that instances of a person have the characteristics of people, and all instances of a tank have the characteristics of a tank. "Peopleness" and "tankness" may seem similar (for example both can sensibly respond to the message "go forward"), but they can be as different or as similar as whoever created them would wish. In GRS, phenomena such as person and tank are known as classes. A class describes exactly the properties and behaviour of each of its instances (such as Fred or the King Tiger), and provides a means for creating new instances. Within GRS, every object is treated equally, therefore each has a class. For cases such as person or tank, their class is the object called class. The class of the object class is meta_class, the class of which is itself, ie meta_class. This is made a little clearer in figure O1. To create an instance of a class, the message "new" is sent to the appropriate object. For example, to create a new example of the class class, the code is instanceof class person; person := class.new( ....some parameters... ); The parameters are explained in detail later. Notice the form of sending a message—the object to receive the message precedes the dot, the message to pass follows the dot. The message follows the usual rules for calling a GRS function. In the case of sending the message "new" to the class class, the parameters are * a string, which is the name of the object to be created (which is not the same as the identifier, although it is usually clearer to use the same name) * a list of null expressions, each element of which must be a single function declaration. A warning is produced if this is not the case. * a list of null expressions, each element of which must be a set of one or more variable declarations. A warning is given if this is not the case. The full code to create the class person is therefore instanceof class person; person := class.new( "person", (* name of new object *) [ (* list of methods *) { null function set( string pname; integer page ) { assume integer age; string name in { age := page; name := pname; }; }; }, { null function show() { assume integer age; string name in write(name," is ",age," years old\n"); }; } ], [ (* list of slots *) { integer age; string name; } ] ); There are a few points to note * the use of the assume command to avoid a compile time error, as the slots are only added at run-time, and until then their existence will not be accepted. The types of assumed variables are not checked at run-time, meaning that a wrongly specified type or a non-existent variable which was assumed will produce strange behaviour, most likely crashing the program. * the methods and slots follow the usual syntactic form of a list of GRS null expressions * the slots could equally have been represented using the list [ {integer age;}, {string name;} ] The order of the slots is not important. Then, to create instances of the class person, the message "new" must be sent to the object person, ie instanceof person fred; fred := fred.new("fred"); When creating instances of new classes, the only parameter is the name of the new object. Now that we have an instance of person, we can manipulate the object. For example, fred.set("Fred",42); fred.show(); would produce Fred is 42 years old. Creating more people is easy, say instanceof person guy; guy := person.new("guy"); Using this new object follows the same pattern, ie guy.set("Guy",24); guy.show(); would produce Guy is 24 years old. Notice that there is a shorter way of creating an instance of a new class, using the create keyword. For example, the object guy could equally have been produced using create person guy; or #person guy; which is equivalent to the code shown above. The # form is quicker but less clear, and is intended only for interactive use. 3.6 More on methods and slots Once a class is defined, passing messages causes the methods to be executed. For the most part, the code behaves exactly as any other piece of GRS code, and all of the normal rules of scope apply, but with two exceptions. When executing a method, all identifiers are checked dynamically, that is, every time a variable is encountered, the object store and attribute tables are searched to produce a value. Thus, when responding to a message, the interpreter first checks to see if there is a slot with the same name. Slots are always searched first within a method, and care must be taken to avoid naming local variables which share the same name as slots. Similarly, any function calls cause the interpreter to firstly search through the methods, and only if none of these match, then the normal rules of scope apply. Note that when creating a new class, the methods do not yet exist. So, if methods refer to other methods in the class, their existence must be assumed for now. 3.7 Objects as variables Objects can be assigned to one another, and used as parameters. So, to make two identifiers indicate the same object, use instanceof someclass o1,o2; o1 := someclass.new("o1"); o2 := o1; Notice that this does not make a copy of o1, it simply means that both of the identifiers refer to the same object. This is particularly important when objects are used as slots. 3.8 Objects as slots Often an attribute of a class is another object. For example, a student will be studying a course, which itself could be a class. There are two methods of implementing this in GRS: * declare a slot as instanceof classid slotname. This means that the slot can be used in a very general way, assigned to new objects, used as a parameter, and so on. It does however mean that the object will have to be initialised in one of the class's methods. * declare a slot as create classid slotname. In this case the object is created, without the need to send the message "new" to the class. 3.9 Summary To create a new class instanceof class newclass; newclass := class.new( "newclassname", [ listof methods ], [ listof slots ] ); The methods must be single function declarations in the form of a null expression, the slots can be any number of variable declarations, again in the form of a null expression. A warning is given if any of the elements are not of the correct form. All references to slots and other methods must be assumed. To create instances of the new class use instanceof newclass instance; instance := newclass.new("instancename"); or create newclass instance2; which is equivalent to instanceof newclass instance2; instance2 := newclass.new("instance2"); Now instance and instance2 can respond to the messages defined within newclass, that is they share the properties defined by newclass. 3.10 Advanced use of objects It is possible for the user to create hierarchies, and to manipulate more directly the object store. Details of how this is achieved are given below. 3.10.1 isa and super Often in real life certain groups are subsets of a larger group. For example, humans and dolphins are both mammals, and as such, they share the properties of all mammals, whilst retaining quite unique properties. Thus, both dolphins and humans would share the property of having skin, but only dolphins have fins, and only humans have hands. Similarly, people have certain attributes, and students, whilst having all of the attributes associated with people, also have characteristics such as college, course name, and year of study. In GRS it is possible to represent this relationship quite easily. Assuming that the class person has been set up, student can also be defined thus instanceof class student; student := class.new( "student", [ { null function set( string pname; integer page; string pcrse; integer pyear ) { assume instanceof person super in super.set(pname,page); assume string course; integer year in { course := pcrse; year := pyear; }; }; }, { null function show() { write("\nStudent details :\n"); assume instanceof person super in super.show(); assume string course; integer year in write("Course : ",course, " year : ",year,"\n"); }; } ], [ { string course; integer year; } ] ); Notice how super is used only within assume blocks in order to prevent a compile time error. Whenever the interpreter finds a message is to be sent to the object super, the message is sent to the appropriate superclass. For example, create student guy; guy.set("Guy",24,"Comp Sci",4); Here an instance of student is being created, and a message passed to initialise its slots. As it stands though, the superclass has not been defined. Thus, before executing guy.set, the statement student isa person; must be executed. If this was not done, an error isa of <student> does not exist would result. The isa operator defines the type of object which super will be for the given class. It also means that every instance of student is paired with an instance of person, although this extra object is invisible to the user and can only be accessed through the use of the object super. This is explained in figure O2. It is only possible for a class to have one superclass, but there is no limit to the number of levels of superclasses. For example, person could be a mammal, ie person isa mammal, and mammal could be a living being, ie mammal isa living_being. Equally, there is no limit to the number of times a class can be used as a superclass, so we could have dolphin isa mammal. 3.10.2 Creating new class objects So far this chapter has concentrated on creating new classes, that is, instances of the object class. Just as class defines how each of its instances behave, meta_class defines how class behaves. There is no reason why more instances of meta_class cannot be created, allowing user-defined class types to be created. Examples of this, with regard to the ADAM database system, can be found in [Paton 90]. Details of how to manipulate the object store directly are given in section 3.12. 3.10.3 Referring to objects by their literal name Previously, every time a new object has been created, its name has been the same as the object identifier. This need not be the case however, as the following example shows. instanceof person fred; fred := fred.new("another fred"); instanceof person p; p := person.new("fred"); p := person.new("jim"); "jim".set("Jim",29); "fred".set("Fred",55); (* these last two refer to *) fred.set("Fred",42); (* different objects *) The object name may be used, rather than an object identifier, whenever a message is to be sent. It is much quicker to use the identifier, but if the name is to be used, remember that the match is case-sensitive and all characters are significant. The advantage of using the literal name is that it is insensitive to scope and thus makes the object accessible from any level. However the "object name" form cannot be used if the object is part of an assignment or being passed as a parameter, only when a message is to be sent. 3.11 Methods to manipulate objects It is possible to delete slots, methods and objects, to add new methods and slots, and to replace methods within an object. To delete an object, use sys_delete_object( objectid, flag ); The objectid must be a valid object identifier, and flag is an integer indicating whether or not confirmation is required before the deletion takes place. When set to true, the list of objects, methods and slots which would be deleted as a result is displayed, and the user then types y or n. When deleting an object, the object store also deletes * every instance of the object if the object is a class * every subclass of the object (ie those joined by isa links) * every method in the object, and every method which refers to these methods, and so on * every slot in the object The consequcnces of deleting an object are further reaching than this. All identifiers which refer to the deleted object will no longer be valid, and any code containing such references (for example inside expressions) is no longer valid. Such code must be recompiled before it can be used again. To delete the slot slotname from the class classid, use classid.delete_slot("slotname",flag); flag is an integer, either true or false, and indicates whether or not warning is required before deletion takes place. This is because deleting a slot also deletes all of the methods which refer to that slot, and the methods which refer to those methods, and so on. If true is used, the user then types y or n to delete or ignore the slot and methods listed. If the slot does not exist, a warning is given such as Delete warning-slot 'slotname' does not exist in object 'objectname'. Deletion of a method is similar, ie classid.delete_method("methodname",flag); Again, the flag being true allows inspection of the methods which will be deleted and a choice to be made. If the method does not exist, a warning is given. To add a slot to an object, use classid.add_slot( ne ); where ne is a (null) expression consisting of one or more variable declarations. Similarly, to add a method, use classid.add_method( ne ); where ne is a (null) expression consisting of a single function declaration. If in either case the null expression is not in the correct format an error is produced. 3.11.1 Other pre-defined methods When starting GRS, meta_class and class both have a set of methods. As well as those described above, these are get_instances This returns a listof instances of the object to which the message was passed. Thus, meta_class.get_instances() returns instances of meta_class, and person.get_instances() returns instances of person. The most common use of this method is within foreach loops. get_slots This returns a list of strings, which are the names of the slots attached to the object to which the message was passed. describe_object Provided mainly for debugging purposes, this prints details of the object to which the message was passed. 3.12 Directly manipulating the object store Several system-defined functions are used by class and meta_class, and may be used with care within other methods. However, there is seldom any need to use these, unless the user wishes to set up another instance of meta_class. The current object is mentioned throughout. By default, this is meta_class, but changes whenever a message is passed to the object receiving the message. For example, with person.new("fred") when the method new is being executed, the current object is person. null function sys_create_object( name,methods,slots ); name the name of the object to create methods a list of single function declarations, in the form of null expressions slots a list of one or more variable declarations, in the form of null expressions Although this is a null function, it returns an instance of the current object. To return the value properly, this function must be placed within a method called new, as the methods (but not functions) called new have their types altered to indicate that they return an instance of the current object. null function sys_create_class_object( name ); name a string which is the name of the object to be created Similar to sys_create_object, this returns an instance of the current object, and thus should only be used within a method, which again must have the name new. The method new in class is simply null function new( stringname ) { sys_create_class_object( name ); }; This returns an instance of the object class. null function sys_add_method( ne ) ne a null expression containing a single function declaration The method described is added to the methods of the current object. The definition of add_method attached to meta_class and class is null function add_method( (null) expression ne ) { sys_add_method(ne); }; null function sys_add_slot( ne ) ne a null expression containing one or more variable declarations The slot(s) described are added to the current object. The definition of the method add_slot is null function add_slot( (null) expression ne ) { sys_add_slot( ne ); }; null function sys_describe_object() This displays full information about the current object. The definition of the method describe_object is null function describe_object() { sys_describe_object(); }; null function sys_get_instances() This function returns a list of all instances of the current object. It should be used within a method named get_instances, the type of which is changed to return a list of instances of the current object. For example, the definition of the method get_instances is null function get_instances() { sys_get_instances(); }; and the type is changed so that get_instances returns the desired object type. The most common use of this function is with the foreach statement. null function sys_delete_slot( name,flag ) name the name of the slot to be deleted flag an integer indicating whether or not confirmation is required This function removes the slot with the given name from the current object, and all methods which refer to that slot. It is possible to remove all slots with the method null function delete_all(integer warning) { foreach s in sys_get_slots() do sys_delete_slot(s,warning); }; null function sys_delete_method( name,flag ) name the name of the function to be deleted flag an integer indicating whether or not confirmation is required This function removes the method named from the current object, and all methods which refer to this method. null function sys_replace_method( name,ne ) name the name of the method to be replaced ne a null expression containing a single function declaration, which will directly replace the named method This function simply replaces the method named with that provided within the null expression. The names of the new method must match the old name, and the types must be the same. listof string function sys_get_slots() This function returns a list of the names of all the slots of the current object. Thus, person.get_slots() would return ["name","age"]. Note that this is one of the few "sys" functions to return a value with a fixed type. 4.0 The GRS programming environment In order to start tinkering with GRS, the GRS environment needs to be understood. If the reader has started reading this manual here, they need only read the sections Getting Started and Running Programs. Once the basics have been mastered, the rest of the chapter should be read. 4.1 Getting started To start a GRS session, the command is grs [filename] [options] The filename is optional, and the options are mainly for debugging purposes. If a filename is provided, the file is read and executed as if it were being read by the consult command (explained below). An error in this file returns causes GRS to quit, otherwise from now on all GRS errors will return to the GRS prompt. The possible options are described later. 4.2 Running programs Unlike some other interpreters which execute a command at a time, GRS deals with files. This distinction is important, and must not be forgotten, especially where the assume statement is being used a lot, and in particular when building up an object store. There are two ways of executing files in GRS: * use an editor such as the !Edit application on the Archimedes to create a text file. This file can then be compiled and run using the consult command or by specifying the filename when starting GRS. * typing the program at the GRS prompt, and then pressing the end-of-file key. It is important not to forget this end-of-file, as no action can take place until the end-of-file has been found. On the Acorn Archimedes, this is a simultaneous press of the Ctrl key and the D key, commonly written as Ctrl-D. There are many disadvantages to this method, not least having to re-type many lines of code if a mistake is made. However it is useful for examining the state of certain variables or objects, and is essential when there is more than one GRS application resident in memory at once. For the remainder of this chapter, the term file is taken to mean either a physical file, executed by using the consult command or commands typed at the command line, followed by an end-of-file. 4.3 A short example In this example, it is assumed that the following file has been created and saved as examp. integer function fact( integer n ) { if n <= 0 then return(1); else return( n*fact(n-1) ); endif; }; write("fact (5) = ",fact(5),"\n"); foreach a in [2,4,6,8] do write("fact (",a,") = ",fact(a),"\n"); The following sequence describes how to load and run this program, and then make use of the function it creates. Note that the end-of-file key-press is represented as [EOF]. *grs No file name given - input from keyboard. GRS->consult("g.examp"); [EOF] Consulting file g.examp ... fact (5) = 120 fact (2) = 2 fact (4) = 24 fact (6) = 720 fact (8) = 40320 GRS->let integer z := 9; write(z,"\n"); z := z+fact(z); write(z,"\n"); [EOF] 9 362889 GRS-> To quit from GRS, use Escape on the Archimedes, or Ctrl-C on the Unix machines. 4.4 Organising GRS programs Because of their dynamic nature, GRS programs of any reasonable complexity can rarely be written in a single file. There is a limit to the number of assume statements which can be used whilst retaining readability in the code, and messages cannot be sent to instances until the class has been created at run time. So, before writing an application in GRS, thought must be given to organising the files. Several points should be kept in mind: * in object creation, the first file should set up the classes and their methods and any global functions to be used in the application. It is a good idea to test this file, and then make this the default starting state by using the name of the file in the command line when starting up GRS. * once the initial classes and functions are in place, any relationships should be placed in subsequent files. For example, setting up student isa person requires that person is created in an earlier file, so the object super can be used without error; then student has to be set up and finally the isa operator applied. * finally, the instances can be declared and created and the operations can begin. Here it is good practice to use as few global variables as possible, and to keep the function names as specific as possible, to allow several applications to be resident in memory at the same time. For example, the overly zealous use of integer i will produce many frustrating Duplicate identifier in same scope errors. A possible way to overcome this is to place the final processes inside a large function (or even an object!), so that all "globals" are in fact local, but there are often circumstances where this is not possible. Once the files have been produced, a master file can be created, which would look something like consult("file1"); consult("file2"); (* and so on *) The naming of files should be consistent too. For example, a database application may have four files, and the master file. These could be named: * on an Archimedes, create a directory called "database" and have files file1 file2 file3 file4 master Or these could be given more meaningful names, such as classes and functions. Here, the master file would be (for the Archimedes) consult("database.file1"); consult("database.file2"); consult("database.file3"); consult("database.file4"); and the whole process would be started with either grs database.master from the operating system command line, or consult("database.master"); (* plus end-of-file *) from inside GRS. GRS does not insist on any particular format of file name, so above all try to group related files and keep the names fairly meaningful. 4.5 Debugging options There are several options available to help debug a program, although these should be used only as a last resort. -p This displays the parse tree for every file read in by GRS. Unless the file is small the output from this option soon becomes confusing. -s This displays the symbol table of every file read in by GRS. The following options all follow -S on the command line, but can be combined, eg -Ssat and ÂSsa -St are equivalent. a This displays some information about the operators such as + and ::. d This displays information about the dependencies of slots and methods and the processes of deletion. f This displays information surrounding function and method calls. l This echoes the file being read in by GRS. o This displays information about the object store communicating with the interpreter. s This displays the communication between the parameter stack and the interpreter. t This displays information about each node of the parse tree as it is interpreted and evaluated. It must be stressed again that tracing in the GRS sources will provide much more specific and helpful information than these options, although they do provide an insight into how the GRS system operates. 4.6 Resources required by GRS When GRS is started, it executes a file bootclass which creates the object class, and add some methods to meta_class. In addition, dummy declarations of many of the system defined functions (for example beep and itos) appear in this file. The user can alter this as required, but should bear in mind the consequences. bootclass is expected to be in a directory called "g", so it must be present in the current path as g.bootclass. GRS also requires the shared C library 3.50 or higher, and the floating point emulator module. The user is advised to set up an obey file to set the current directory and load the requisite modules. 4.7 Recovering from errors GRS tries as far as possible to return the system to its initial state after an error has occurred. However, the error may have happened during a critical operation such as the summon command, so there may be some structures which contain incomplete information. Compile time errors mean that no attempt will be made to execute the code which caused the error. 4.8 Common sources of error The most common causes of error are: * no return in a function which should return a value. This is usually accompanied by the GRS system error "Tried to pop from stack size zero." * variables declared within an assume statement not existing at run time, or the type being different from that within the assume statement. Check for typing errors in variable names. This is harder to confirm, as the program usually simply crashes or just behaves in an unexpected manner. * trying to reference incomplete objects. This can occur when sending a message to a method which inculded an assume statement–see above. Perhaps the program does not send the message "new" to fully set up a new object, or a warning from add_method or add_slot has been missed. 5.0 References. Paton, N.W. Diaz, O. 'Metaclasses in Object - Oriented Databases.' Object - Oriented Databases: Analysis, Design and Construction, Proc IFIP TC 2 Working Conference On Database Semantics, R. A. Meersman and W. Kent (eds) North - Holland, 1990. Stroustrup, B. 'An Overview of C++'. Sigplan Notices v21 #10. October 1986. Meyer, B. 'Object-oriented Software Construction'. Prentice-Hall, 1986. Inkster, S. P. A. 'A Virtual Machine And Object Store For An Object - Oriented Programming Language.' Submitted as a final year dissertation for the Department Of Computer Science, Heriot - Watt University. 1991. Verbist, G. 'A Compiler For An Object - Oriented Language.' Submitted as a final year dissertation for the Department Of Computer Science, Heriot - Watt University. 1991. Appendix. A) Some example GRS code Desk calculator. The following is a desk calculator which reads a typed expression from the keyboard, and then compiles and evaluates the expression, thus remaining independent of the type of the expression. (* string / integer expression evaluator *) (* supports integers, strings, lists of. + - / * :: or and not head tail mid len *) (* Guy Verbist 1991 *) let string s := ""; string s_to_eval; loop write("?-> "); read(s); exiton( (s="quit") or (s="end") or (s="exit") or (s = "q") or (s="x")); s_to_eval := "write(\"Answer is : \",eval({" + s + "}),\"\n\");"; exec(compile(s_to_eval)); endloop; Database. This is a database which reads its fields and the types of fields from the keyboard. It creates an instance of class called data and then compiles and executes a message to class creating this new class, with the requisite slots and set methods. It then uses an add_method message to add show methods. The database is then queried and modified via a menu. (* generic database written in GRS *) (* shows off most of the capabilities of the language *) (* Guy Verbist April 1991 *) write("Enter the attributes of object:\n"); write("Enter 'end' to end.\n"); let listof string attribs := []; let listof string attribtypes := []; let string attrib := ""; let string attribtype := ""; loop write("Enter attribute name:\n"); read(attrib); exiton(attrib = "end"); write("Enter attribute type:\n"); read(attribtype); attribs := attrib::attribs; attribtypes := attribtype::attribtypes; endloop; let listof string attribs_copy := attribs; let listof string attribtypes_copy := attribtypes; let listof string main_attribs := attribs; let listof string main_types := attribtypes; (* now create the class *) instanceof class data; let string tempstring := "data := class.new(\"data\",[{null function hello()" + "{write(\"Hello\n\");};} " ; (*add in methods*) let string methodstring := ""; loop exiton(attribs_copy = []); methodstring := methodstring + ",{null function set_" + head(attribs_copy)+ "("+head(attribtypes_copy) + " param){" + "assume " + head(attribtypes_copy) + " "+head(attribs_copy) + " in " + head(attribs_copy)+":= param;};}" ; attribs_copy := tail(attribs_copy); attribtypes_copy := tail(attribtypes_copy); endloop; methodstring := methodstring + "],[{"; (*add in slots*) let string slotstring := ""; loop exiton (attribs = []); slotstring := slotstring + head(attribtypes) + " " + head(attribs)+ ";"; attribtypes := tail(attribtypes); attribs := tail(attribs); endloop; slotstring := slotstring+"}]);"; exec(compile(tempstring+methodstring+slotstring)); (*now add show method for each attribute*) attribs_copy := main_attribs; attribtypes_copy := main_types; loop exiton(attribs_copy = []); methodstring := "null function show_" + head(attribs_copy) + "(){assume " + head(attribtypes_copy) + " " + head(attribs_copy) + " in write(\"" + head(attribs_copy) + " is \"," + head(attribs_copy) + ",\"\n\");};" ; let (null) expression new_method := compile(methodstring); exec(compile("data.add_method(new_method);")); attribs_copy := tail(attribs_copy); attribtypes_copy := tail(attribtypes_copy); endloop; write("Type 'db();' to access the database.\n"); null function db() { integer function menu(listof string menu) { let integer options := 0; write("\n"); loop exiton(menu = []); options := options +1; write(options,") ",head(menu),"\n"); menu := tail(menu); endloop; write("\n"); integer selection; loop write("Select an option between 1 and ",options," :"); read(selection); exiton( (selection > 0) and (selection <= options)); endloop; write("\n"); return selection; }; integer choice; string entry_name, attrib_name, attrib_val; loop choice := menu(["add an entry", "delete an entry", "modify entry", "show an entry", "show all entries", "show all attributes", "quit"]); exiton(choice=7); if (choice=1) then write("Please enter name of entry : "); read(entry_name); exec(compile("#data "+entry_name+";")); endif; (* these are declared locally to the db() function so *) (* cannot be accessed from the command line, and will *) (*not conflict with anything else *) if (choice=2) then write("Please enter name of entry : "); read(entry_name); exec(compile("sys_delete_object("+entry_name+",true);")); endif; if (choice=3) then (*modify*) write("Please enter name of entry : "); read(entry_name); write("Please enter name of attribute to change : "); read(attrib_name); write("and the new value for that attribute : "); read(attrib_val); exec(compile(entry_name+".set_"+attrib_name + "("+attrib_val+");")); endif; if (choice=4) then (*show*) write("Please enter name of entry : "); read(entry_name); attribs_copy := main_attribs; string meth_str; exec(compile("loop exiton(attribs_copy = []);" + "meth_str := \".show_\" + head(attribs_copy)+\"();\";" + "exec(compile(entry_name + meth_str));" + "attribs_copy := tail(attribs_copy);" + "endloop;" )); endif; if (choice=5) then (*show*) exec(compile("foreach inst in data.get_instances() do {" + "attribs_copy := main_attribs;" + "loop exiton(attribs_copy = []);" + "meth_str := \"show_\" + head(attribs_copy)+\"();\";" + "exec(compile(\"inst.\"+ meth_str));" + "attribs_copy := tail(attribs_copy);" + "endloop;};" )); endif; if (choice=6) then listof string slts; let integer sltcount := 0; foreach slt in data.get_slots() do { sltcount := sltcount +1; write("Attribute ",sltcount," is ",slt,".\n"); }; endif; endloop; }; Some short programs to demonstrate some of the features of GRS Sharing objects and using objects as parameters and slots In this example, there are three files. The first creates the class person, the next creates the class book1, and finally some examples are set up and used. (* --------------------------------------------------------------*) (* file 1 - creates the class person *) (* Steven Inkster, April 1991 *) instanceof class person; person := class.new( "person", [ { null function set( integer b,d; string n ) { assume string name; integer born,died in { name := n; born := b; died := d; }; }; }, { null function show() { assume string name; integer born,died in { write("\n",name," was born in ",born); if died >= born then write(" and died in ",died); endif; write("\n\n"); }; }; }, { null function died( integer year ) { assume integer died in died := year; }; } ], [ { string name; integer born, died; } ] ); (* -------------------------------------------------------------*) (* file 2 - creates the class book1 *) instanceof class book1; book1 := class.new( "book1", [ { null function set( string s; integer i; listof integer l; instanceof person p ) { assume string title; listof integer publication_dates; integer number_of_pages; instanceof person author in { title := s; publication_dates := l; number_of_pages := i; author := p; }; }; }, { null function show() { assume string title; listof integer publication_dates; integer number_of_pages; instanceof person author in { write("\nBook : ",title,"\n"); write("published in : "); foreach i in publication_dates do write(i," "); write("\nno of pages : ", number_of_pages,"\n\n"); write("Author :\n"); author.show(); }; }; }, { null function republished( integer date ) { assume listof integer publication_dates in publication_dates := publication_dates + [date]; }; } ], [ { string title; listof integer publication_dates; integer number_of_pages; instanceof person author; } ] ); (* --------------------------------------------------------------*) (* file 3 - uses the classes to demonstrate the sharing of *) (* objects and using objects as parameters *) (* first set up the author *) instanceof person a1; a1 := person.new("Lawrence"); a1.set(1921,-1,"D. H. Lawrence"); (* now set up the books *) instanceof book1 b1,b2; b1 := book1.new( "Lady Chatterly" ); b1.set("Lady Chatterly",566,[1963],a1); b2 := book1.new( "Woman In Love" ); b2.set("Woman In Love",343,[1968],a1); (* now show them. Notice the author is the same OBJECT thus *) (* when the author is declared dead, all references to the *) (* author reflect this *) b1.show(); b2.show(); a1.died(1977); b1.republished(1974); b1.show(); b2.show(); Local "methods" / replacing methods This pair of programs demonstrate how null expressions can be used to give individual behaviour patterns to instances of a class, and then using the method replace_method to return to the normal, uniform, manner of behaviour. (* ------------------------------------------------------------- *) (* Steven Inkster April 1991 *) (* program to demonstrate local "methods" and replacing methods *) (* file 1 - sets up the class *) instanceof class test; test := class.new( "test", [ { null function go() { assume (null) expression ne in run(ne); }; }, { null function set( (null) expression p ) { assume (null) expression ne in ne := p; }; } ], [ { (null) expression ne; } ] ); (* --------------------------------------------------------------- *) (* file 2 - this actually uses the class just set up *) instanceof test t1,t2; t1 := test.new("t1"); t2 := test.new("t2"); t1.set( {write("I am t1\n");} ); t2.set( { write("I am t2\n"); test.replace_method( "go", { null function go() { write("I am an instance of test\n"); }; } ); test.delete_slot("ne",false); (* so we fully tidy up *) (* this deletion should NOT delete "go" as the new go *) (* does not refer to the slot ne at all *) } ); t1.go(); t2.go(); t1.go(); t2.go(); Running these programs would produce : I am t1 I am t2 I am an instance of test I am an instance of test A simple front-end to GRS This program allows single commands to be passed to the GRS interpreter without need for the end-of-file character, effectively replacing end-of-file with the Return key. (* a simple GRS front end *) (* Steven Inkster November 1990 / April 1991 *) null function fe() { string s; loop write("> "); read(s); exiton(s="quit"); exec(compile(s)); endloop; }; fe(); (* so it auto-starts *) A tank game These programs demonstrate how get_instances and sys_delete_object can be used in combination to produce very short, compact code. There are three files. The first sets up the possible tactics to be employed by the tanks and the class tank; the second adds the method move to the class tank and sets up the teams; the third loops, exiting when one side has won the battle. (* ------------------------------------------------------------ *) (* a tank game *) (* Steven Inkster March 1991 *) (* file 1 - sets up the possible tactics and the class tank *) instanceof class tank; let (null) expression default := { assume integer x,y in { x := x + rnd(3) - 2; y := y + rnd(3) - 2; if x<0 then x:=0; endif; if x>25 then x:=25; endif; if y<0 then y:=0; endif; if y>19 then y:=19; endif; }; }; let (null) expression retreat := { assume string side; integer x in { if side="German" and x>0 then x := x-1; else if side="American" and x<25 then x := x+1; endif; endif; }; }; let (null) expression advance := { assume string side; integer x in { if side="American" and x>0 then x := x-1; else if side="German" and x<25 then x := x+1; endif; endif; }; }; let (null) expression panic := { assume integer x,y in { x := x; y := y; }; }; assume string side; integer x,y,skill; (null) expression tactic in { tank := class.new( "tank", [ { null function set( string pside; integer px; integer py; integer pskill ) { side := pside; x := px; y := py; skill := pskill; tactic := default; }; }, { null function set_tactic( (null) expression ptactic ) { tactic := ptactic; }; }, { integer function get_pos( string myside ) { if myside = side then return(-1); else return(x+y*256); endif; }; }, { null function blowup() { beep(); foreach s in ["X","x","X","x","X","x"] do { tab(x,y); write(s); }; }; }, { null function display() { tab(x,y); write(mid(side,1,1)); }; } ], [ { string side; integer x,y,skill; (null) expression tactic; } ] ); }; (* ---------------------------------------------------------------- *) (* file 2 - add the method "move" and set up the teams *) integer function abs( integer i ) { if i<0 then return(-i); else return(i); endif; }; assume string side; integer x,y,skill; (null) expression tactic in { let (null) expression movefunc := { null function move() { integer shot; integer epos; integer ex,ey; foreach i in tank.get_instances() do { shot := false; epos := i.get_pos(side); ey := epos/256; ex := epos - (ey*256); if epos<>-1 and abs(y-ey)<4 and abs(x-ex)<4 and rnd(3)<>1 then shot := true; foreach s in [".","|",".","|",".","|", ".","|",".","|",".","|", ".","|",".","|",".","|", ".","|",".","|",".","|", ".","|",".","|",".","|"] do { tab(x,y); write(s); }; if rnd(100)<skill then i.blowup(); sys_delete_object(i,false); endif; endif; if not shot then run(tactic); if rnd(10)=1 then integer nt; nt := rnd(10); if nt=1 or nt=2 then i.set_tactic(retreat); else if nt=3 or nt=4 or nt=5 then i.set_tactic(advance); else if nt=6 then i.set_tactic(panic); else if nt>6 then i.set_tactic(default); endif; endif; endif; endif; endif; endif; }; }; }; }; tank.add_method( movefunc ); #tank gt1; #tank at1; #tank gt2; #tank at2; #tank gt3; #tank gt4; #tank at3; #tank gt5; gt1.set("German",10,3,30); gt2.set("German",10,6,30); gt3.set("German",10,9,30); gt4.set("German",10,12,30); gt5.set("German",10,15,30); at1.set("American",15,5,60); at2.set("American",15,15,60); at3.set("American",25,10,90); (* ----------------------------------------------------------- *) (* file 3 - the main loop *) integer function somebody_has_won() { integer germans, americans; germans := 0; americans := 0; foreach i in tank.get_instances() do { if i.get_pos("German")=-1 then germans := germans + 1; else americans := americans + 1; endif; }; if americans=0 then write("\n\nThe Germans have won!\n\n"); return(true); else if germans=0 then write("\n\nThe Americans have won!\n\n"); return(true); else return(false); endif; endif; }; null function go() { cls(); foreach i in tank.get_instances() do { i.display(); }; loop foreach i in tank.get_instances() do { i.move(); }; cls(); foreach i in tank.get_instances() do { i.display(); }; exiton( somebody_has_won() ); endloop; }; go(); b) Compiler / Analyser Error Messages. Where possible a line number is printed for the error and that line of the source file is printed. * = An identifier or the like will appear here ! = Internal error, should never appear. b1) Syntax errors. These are generated by the syntax analyser. GRS parser: unrecognised statement (ignored) A whole statement was syntactically incorrect and was ignored. GRS parser: missing 'function' in function declaration (inserted) A function declaration was missing the keyword 'function'. This error is fully recoverable. GRS parser: missing closing ')' in function argument list (inserted) A function argument list was missing a closing parenthesis. This error is fully recoverable. GRS parser: error in function decl arg list (args ignored) An error occurred in the argument list of a function declaration. The arguments were ignored. GRS parser: error in identifier of iterator (iterator ignored) The identifier used in an iterator caused a parse error. The entire iterator was ignored. GRS parser: error in assumed function arg list (args ignored) The arguments of an assumed function were incorrect and so ignored. GRS parser: error in declaration list of assume (assume ignored) A list of assumed variables caused a parse error, the whole assume statement was ignored. GRS parser: error in constant expression (ignored) A constant typed or null expression declaration caused a parse error. The expression was ignored. GRS parser: error in constant list (ignored) A constant list caused a parse error. The list was ignored. GRS parser: missing closing ')' around <type> expression (inserted) A type declaration of an expression needs brackets around the type, of which the closing on was missing but inserted. This error is fully recoverable. GRS parser: missing closing ')' in exiton() (inserted) The closing bracket of an exiton(<expression>) was missing. This error is fully recoverable. GRS parser: error in block (ignored) A parse error occurred in a block statement. The entire block was ignored. GRS parser: error in function parameters (parameters ignored) A parse error occurred in a set of function call parameters. The parameters were ignored. GRS parser: error in bracketed expression (ignored) A parse error occurred in a bracketed expression. The expression was ignored. GRS parser: parse error at symbol * line * A general syntax error has occurred in the current input file. GRS parser: Unexpected end of file within a comment. The compiler encountered an end of file within a comment. This probably means that the programmer has forgotten to close a comment. GRS parser: warning - possible nested comments at line * The compiler has encountered a (* sequence within a comment. This is possibly valid, but probably means that comments have been nested, probably during "commenting out" of sizeable blocks of code. In this case the code will be scanned from the first closing comment delimiter and this will probably result in bizarre syntactic and semantic errors. b2) Semantic errors. These errors are flagged by the semantic analyser: GRS parser: cannot bring a non-null expression into scope The user is attempting to run summon() or exec() on something which is not a null expression. This is commonly caused by a semicolon missing from the end of statement, thus making it an expression which yields a null, not a null expression in the GRS sense. ! GRS parser: not a null expression. A parameter given to a new() method as a slot declaration was not a null expression. GRS parser: not a variable declaration for slots. A null expression given to a new() method as a slot declaration was not a variable declaration. It will be ignored but this may well cause code referring to this slot to crash at run time. A common cause of this error is an assume statement surrounding a variable declaration. GRS parser: Warning, multiple statements in single method declaration. A parameter given to a new() method as a method declaration contained multiple statements. Everything after the first statement is ignored. GRS parser: duplicate identifier in same scope * An identifier has been declared with the same name as one already present in this level of scope. GRS parser: attempt to create instanceof non-class object * The user has attempted to create an instanceof something which is not a class. GRS parser: too few params in function call. Too few parameters were given in a function call compared with the number of parameters in the declaration. GRS parser: type mismatch in fcall (should be a typed expression). A call to eval() was attempted with a parameter which is not a typed expression. GRS parser: type mismatch in fcall (should be a list). A call to head() or tail() was attempted with a parameter which was not a list. GRS parser: type mismatch in fcall. A parameter in a function call had a different type to parameter in declaration. GRS parser: attempt to write null expression. It is not meaningful to attempt to write() an expression which yields a null. GRS parser: attempt to read non variable. It is not meaningful to attempt to read() a constant expression or an object. GRS parser: mismatch in fcall args. A bad mismatch in argument numbers occurred in a function call. Either the call has multiple arguments and the declaration has none or the call has none whereas the declaration expects one or more. GRS parser: too many args in fcall. There were too many arguments given for a function call. ! GRS parser: attribs are null. Bad internal error. GRS parser: Differring types in constant list. Expressions of different types have been placed in a constant list. The expressions must all be of the same type so the list is listof that type. GRS parser: unknown identifier * (class name?). An attempt has probably been made to assume an instanceof a class which does not exist. GRS parser: expression to iterate across does not yield a list. A foreach iterator must iterate across a list of some type. GRS parser: attempt to access non - existent object by name * The user has attempted to send a message to an object by a name which does not exist. e.g. "name".method() where "name" does not exist. GRS parser: attempt to access non - existent method * The user is sending a message to an object trying to use a method which that object does not have. GRS parser: unknown identifier * An attempt to access an identifier which does not exist. GRS parser: cannot return - not in a function. A return statement has been encountered outside a function. GRS parser: must return an expression from a non-null function. A non-null function must return something, it cannot just return;. GRS parser: returned expression must be same type as function. The type of the expression returned from a function must match the type declared for the function. GRS parser: rhs of cons is not a list. The right hand side of a cons "::" operation must a list. GRS parser: type mismatch in cons. The left hand side of a cons "::" operation must have the same type as an element of the list on the right hand side. GRS parser: type mismatch. One or more of the operands for an operator do not match the permitted combinations of types for that operator. GRS parser: type mismatch in assignment. The right hand side of an assignment must have the same type as the variable on the left hand side. GRS parser: cannot assign to a function It is not meaningful to assign to an identifier which represents a function. GRS parser: controlling expression does not yield an integer. The controlling expression of an if or loop statements must yield an integer; true or false. GRS parser: non -existent class name in isa * One or more of the identifiers in an isa statement is not a class name. GRS parser: sys_delete_object takes (object,integer) parameters. The first parameter of sys_delete_object must be an object of some class and the second must be an integer, true or false. ! GRS parser: parameter which should be an object has no attributes. A bad error has occurred in checking the parameters of sys_delete_object. ! GRS parser: first parameter is not an object. A bad error has occurred in checking the parameters of sys_delete_object. ! GRS parser: parameter which should be an integer has no attributes. A bad error has occurred in checking the parameters of sys_delete_object. ! GRS parser: second parameter does not yield an integer. A bad error has occurred in checking the parameters of sys_delete_object. b3) Memory Errors. If one of these errors occurs then GRS has run out of memory, or something more sinister is occurring within the target hardware. The solution is system dependent. GRS parser: no room for new scope child. GRS parser: no room for new attribute. GRS parser: no room for heap attribute. GRS parser: no room for expression stack. GRS parser: out of memory for copying tree. GRS parser: no memory for symbol table. c) Interpreter / Object Store Error Messages. c1) Internal fatal errors All of these errors are preceded by the message GRS fatal system error and followed by information as to where within the GRS system the error was produced, for example Source is 'c.eval' at line 309 c 1.1) Stack errors Stack errors occur when the internal stack has become full, or an attempt has been made to pop a value from an empty stack. The former will happen only in very complex expressions or with long lists, the latter will happen if a non-null function is called, but the function has not executed a return statement. This will produce the error Tried to pop from stack size zero followed by the specific error. Stack error in list cons The interpreter was unable to stack the result of the :: operator. Failed to push result of string + The interpreter was unable to stack the result of the + operator. Failed to push result of unary minus The interpreter was unable to stack the result of the unary minus operator. Failed to push the result of logical NOT The interpreter was unable to stack the result of the NOT operator. EXPRCONST could not stack the address The interpreter was unable to stack the value of an expression. Stack error in XXX NODE This indicates that an operator was unable to pop its operands. The most likely cause is that a function appears as one of the operands and has failed to return a value. The XXX can be PLUS, MINUS, MULT, DIVIDE, CONS, OR, AND, EQUALS, NOT EQUALS, LESS THAN OR EQUAL TO, LESS THAN, GREATER THAN OR EQUAL TO, GREATER THAN, NOT, UMINUS. Error in popping stack to build a list Building a list involves making constant use of the stack. This error should never occur. Could not pop the LVAL at assign node The stack was empty when the address to which to assign the value was required. Could not pop the RVAL at assign node The stack was empty when the value of the expression to assign to the identifier was required. IF node not able to find logical value The stack was empty when the value of the condition in an if statement was required. Stack error at LOOP node The stack was empty after the evaluation of the condition within the exiton command. c 1.2) Errors around system defined functions The errors which may be produced are all to do with collecting the parameters from the stack, and then stacking the result. The former should always be correct because of the semantic checking [Verbist 91], the only time an error will occur is when the internal stack is full. All of the errors are of the form FUNCTION NAME did not have N parameter(s) and Could not stack the result of FUNCTION NAME c 1.3) Miscellaneous errors These errors are caused for a variety of reasons. Failed to compare element in lists_identical The list equality operator has found an element with unknown type in the list. Failed to unstack parameter in call_function The parameter stack had emptied before all of the parameters had been assigned a value. This will happen if a parameter is a function call which has not returned a value. Failed to assign parameter in call_function The value on the stack being assigned to the parameter was of unknown type. Expression type tree error The type tree of a variable is of an unknown form, despite the root node suggesting that the type is some sort of expression. Tried to read <identifier> Cannot take the value of a NULL variable The interpreter had to try and read the value of an identifier whose type was null. Failed to evaluate identifier The identifier was of an unrecognised type. call_node is null for exec/eval The interpreter has not recorded the node from which the call to exec or eval was made. This information is required so that variables can be dealt with in the correct scope. This error should never appear. Add given unknown types The + operator has been asked to add a type which it does not recognise. Found a node not capable of being an LVAL The identifiers on the left side of an assignment or in the parameter list of a call to the read function are of an unknown type. Failed to assign list element in foreach The list element was of an unknown type. Assign node - Failed to assign the node The value on the stack was of an unknown type. Failed to unstack parameter when saving locals A parameter within a function could not be given its value as the stack was empty. Failed to assign parameter in save_locals (unknown type) An unrecognised type has appeared on the stack and therefore the parameter cannot be assigned a value. Could not print the variable type or Failed to print parameter or Could not print variable parameter or Failed to print constant parameter These are when trying to print an identifier (either via debugging diagnostics or directly using write) but an unexpected type has been found. c 1.4) Object store errors These errors should never occur, as the situations should only arise if there are inconsistencies within the object store. <O1> was supposed to be an instance of <O2> Unexpected instance of an object This occurs when the class of the object O1 is O2, but the list of instances of O2 is empty. Failed to find <O1> in instances of <O2> Failed to find an instance of an object This error occurs when deleting object O1, and it cannot be found in the list of instances of its class, the object O2. <O1> was supposed to be an instance of <O2> Unexpected subclass of an object and Failed to find <O1> in subclasses of <O2> Failed to find a subclass of an object These errors are the same as the previous two, except they deal with the isa relationship rather than the usual object-class relationship. c 1.5) Memory failures The GRS system often has to claim more memory. If it is unable to claim any more memory, the error Fatal error : GRS system out of memory is produced. c 1.6) User errors These errors are all preceded by GRS run time error : which is then followed by the messages described below. Error occurred in file described in command line If the file which was given to execute when starting GRS is not correct this error is produced and the system then exits and returns to the operating system. Assumed variable non-existent at run time A variable which had appeared within an assume statement has not been provided. This error is unlikely to appear, as the result of referring to a non-existent variable is undefined. Attempt to evaluate head([]) Attempting to evaluate head of the empty list is illegal and causes a run time error. Parameter to add_method was not a method The null expression passed to add_method was not a single function declaration. Parameter to add_slot was not a slot The null expression passed to add_slot was not a set of one or more variable declarations. Parameter to sys_replace_method was not a method The null expression passed to sys_add_method was not a single function declaration. GRS parser could not consult 'filename' File could not be opened The parameter passed to consult was not a valid filename. Tried to find method <M> in class <C> Failed to find/execute the method An attempt has been made to reference, most likely to execute, a method which has been deleted since compilation. Replace method - method <M> is not attached to object <O> An attempt has been made to replace a non-existent method. This error may be followed by nor its superclass <S> if the object has a superclass. Name error - tried to replace <N1> with <N2> The method offered as a replacement has a different name to the original method. Type of new method <M> is not the same as the previous method The name of the method offered as a replacement matches, but the type it returns is not the same. isa of <O> does not exist A reference to the object super has been made within a class whose superclass does not exist. c 1.7) User warnings A warning given at run time has the form GRS run time warning : followed by the appropriate message. Division by zero If the user attempts a division by zero, this warning is produced, and the result is L where the division is L/R. null expression has not been analysed The user has attempted to run a null expression which has been produced by compile but not analysed by either exec or summon. expression provided is not a legal slot and expression provided is not a legal method The list of slots or methods passed with the message new are not legal. However it is only a warning as the rest of the list will be processed correctly. d) BNF grammar for GRS. String literals are enclosed in single (') quotation marks. Non terminals are enclosed in angle brackets (<,>). { ... } indicates 0 or more. <statements> ::= {<statement> ';'} <statement> ::= <funcdecl> | <loop> | <vardecl> | <assignment> | <ifstmt> | <letstmt> | <message> | <assumest> | <block> | <iterator> | <fcall> | <return> | <isa> <funcdecl> ::= <type> 'function' <identifier> '(' <arglist> ')' <block> <iterator> ::= 'foreach' <identifier> 'in' <expr> 'do' <statement> <assumest> ::= 'assume' <declist> 'in' <statement> | 'assume' <type> 'function' <identifier> '(' <arglist> ')' 'in' <statement> <isa> ::= identifier 'isa' identifier <arglist> ::= {<declist>} <declist> ::= <declist> ';' <vardecl> | <vardecl> <vardecl> ::= <type> <identlist> <paramlist> ::= {<expr>} {',' <expr>} <identlist> ::= <identifier> {',' <identifier>} <identifier> ::= <letter> {id2} <id2> ::= <letter> | <digit> <letter> ::= 'a' | 'b' | 'c' . . . | 'z' | 'A' | 'B' . . . | 'Z' <digit> ::= '0' | '1' | '2' . . . | '9' <constant> ::= <integer> | <string> | '[' <constlist> ']' | '{' <expr> '}' | '{' <statements> '}' <integer> ::= <digit> {<digit>} <string> ::= '"' {<string2>} '"' <string2> ::= <character> | '\"' <constlist> ::= <expr> {,<expr>} <type> ::= <type1> | '(' <type> ')' 'expression' <type1> ::= 'integer' | 'string' | 'null' | 'listof' <type> | 'instanceof' <identifier> | '#' <identifier> | 'create' <identifier> <letstmt> ::= 'let' <type> <assignment> <loop> ::= 'loop' <statements> 'exiton' '(' <expr> ')' ';' <statements> 'endloop' <block> ::= '{' <statements> '}' <assignment> ::= <identifier> ':=' <expr> <fcall> ::= <identifier> '(' <paramlist> ')' <return> ::= 'return' <expr> | 'return' <ifstmt> ::= 'if' <expr> 'then' <statements> <restif> <restif> ::= 'endif' | 'else' <statements> 'endif' <expr> ::= <expr> 'or' <e1> | <e1> <e1> ::= <e1> 'and' <e2> | <e2> <e2> ::= <e2> '=' <e3> | <e2> '<>' <e3> | <e2> '>' <e3> | <e2> '<' <e3> | <e2> '>=' <e3> | <e2> '<=' <e3> | <e3> <e3> ::= <e3> '+' <e4> | <e3> '-' <e4> | <e4> <e4> ::= <e4> '*' <e5> | <e4> '/' <e5> | <e4> '::' <e5> | <e5> <e5> ::= 'not' <e5> | '-' <e5> | <e6> <e6> ::= <fcall> | '(' <expr> ')' | <val> | <message> <val> ::= <identifier> | <constant> <message> ::= <identifier> '.' <fcall> | <string> '.' <fcall> e) Known Infelcities Of Version 1.05. a) References to assumed variables which do not exist at run time cause the interpreter to crash. No method was found to signal the fact that a reference was assumed and may not exist any more. b) A declaration of instanceof person fred; does not create a new object. This object must be filled out by fred := person.new("fred"); or the whole affair can be replaced by #person fred; If an instance is referenced before it has been "filled out" then the object store will crash e.g. instanceof person fred; fred.set(12);