Academia.eduAcademia.edu

Outline

Interpreters from functions and grammars

1986

https://doi.org/10.1016/0096-0551(86)90003-2

Abstract

Abstract The addition of context free grammar rules to a functional language simplifies the construction of interpreters from denotational semantic language definitions. Functional abstraction over grammar rules enables the specification and processing of context sensitive language syntax aspects in a functional style.

Comput Lang Vol ll. No 2, pp 85--104, 1986 0096-0551 86 5300+000 Pnnted m Great Britain Pergamon Journal~ Ltd INTERPRETERS FROM FUNCTIONS A N D G R A M M A R S GREG MICHAELSON Department of Computer Science, Heriot-Watt Um~ersity, 79 Grassmarket, Edinburgh EHI 2HJ. U K, (Received 23 September 1985) Abstract--The addition of context free grammar rules to a functional language simplifies the construction of interpreters from denotationaI semantic language definitions. Functional abstraction over grammar rules enables the specificationand processingof context sensitive language syntax aspects in a functional style. DenotatLonaI semantics Language design Language implementation Functional language Grammar rules Interpreters Contextsensitive processing 1. I N T R O D U C T I O N 1.1 Denotational semantics Denotational semantics [1] is a formalism for defining programming languages. A denotational semantic definition consists of semantic domains, abstract syntax and semantic functions. The semantic domains define the universe of objects in which the language is given meaning, the abstract syntax defines meaningful syntactic constructs and the meaning of each abstract syntax construct is defined by a semantic function over the serriantic domains. Abstract syntax is distinguished from concrete syntax which describes the physical representation of programs and contains details which are not significant from a semantic point of view [2]. Approaches based on denotational semantics are used to provide succinct and theoretically rigorous programming language standards. These then inform language implementations and program manipulation. Such approaches are also used for programming language design [3] but there are few software tools which are appropriate for experimenting with denotational semantic based designs. 1.2 Compiler generators Compiler generators [4], which are used to automate the construction of production compilers, are based on very different methodologies to denotational semantics and place a far greater emphasis on concrete syntax. A typical compiler generator, like YACC [5], inputs a formalised definition of the context free [6] concrete syntax of a language along with semantic actions written in an imperative language. These implement context sensitive concrete syntax checking and the generation of the target language. When compiler generators are used with denotational semantic designs a basic problem is the lack of correspondence between the functional meta-language used to define semantic equations and the imperative semantic action language. Translating semantic equations into semantic actions loses clarity and is a potential source of errors. When the design changes so must the translation for the compiler generator. One alternative is to use an existing compiler generator with a new semantic action language which has a well defined correspondence with the functional meta-language [7]. Another is to develop a system which is oriented specifically for use with denotational semantics. 1.3 SIS SIS [8] is a language implementation system which enables direct experimentation with denotational semantic language designs. It takes in a full denotational semantic definition and uses it to generate what is in effect the code for an abstract machine. The code is then interpreted. The strict adherence to denotational semantics, however, with the formal separation of concrete syntax, abstract syntax and semantics, appears to restrict the ease with which SIS may be used. CL H :~: 85 86 GREGMICHAZLSOY In particular, the denotational semantic methodology treats concrete syntax considerations as of secondary importance with context sensitive syntax aspects often bring ignored or pushed into the semantics. Concrete syntax is a major part of a language design as it determines the physical appearance of programs and influences the ease with which they are understood and manipulated. SIS provides grammar rules for specifying a context free concrete syntax and associating it with the abstract syntax but there is no provision for context sensitivity other than incorporation in semantic equations. SIS is essentially a batch system and definitions are frozen by compiler-compilation. Thus, there is no facility for moving easily from definition to test without using the host filing system and associated tools, and re-compiling the whole definition for each change. There is, of course, no reason why SIS should not be implemented within an interactive environment but the need for separate but related sub-definitions may restrict flexibility and integration. 1.4 Navel Navel*, which is described in the rest of this paper, was conceived of as an interpreter-interpreter language which would enable the interactive testing and modification of language designs. It is a functional language, heavily influenced by the original 'let... i n . . . ' SASL [9], and has grammar rules fully integrated as functional objects. Navel interpreters are written in a denotational semantic style using functional semantic equations but, unlike SIS, denotational semantics is not directly implemented. The main differences are the loss of the traditional distinction between the syntactic and semantic meta-languages, the lack of need for abstract syntax to mediate concrete syntax and semantics, and the absence of semantic domain specifications. These differences lose much of the rigour of SIS but simplify interpretive implementation from a formal definition. Context sensitive aspects of concrete syntax may be specified in a functional style through higher order grammar rules. Subsequent sections describe the functional language, the construction of an interpreter for a small language from a denotational semantic definition, rule generalisation through functional abstraction and the use of higher order rules for checking assignment before use, declarations and type consistency. 2. NAVEL 2.1 Introduction Navel [10] is a weakly typed functional language. It is implemented within an interactive environment which provides file access to the host system and local editing facilities. The main features of the language are described through examples. Each example may pre-suppose preceding examples. In examples, the prompt 'ok' heralds the input and execution of an expression or command ending with a ';'. 2.2 Objects and definitions The Navel objects are integers, characters which coerce to integers, booleans, lists, fields, functions and rules. Global definition commands are used to establish name/object associations which are retained for use by subsequent definitions and expressions: ok def Value_Added_Tax = 15; ok Value__Added_Tax; 15 ok def pie_beans_and_chips = 135; ok pie-beans_and_chips.Value_Added_Tax/! 00; 2O *Not A Very Exciting Language. Interpreters from functions and grammars 87 2.3 Ltsts and strmgs The concatenation operator ':' is used to construct lists. The empty list is '()'. The operators "hd' and "tl" are used to extract the head and tail from lists: ok def squares = 1:4:9:16:25:( ); ok hd tl squares; 4 List elements may also be accessed through indexing. The empty list is normally omitted from the end of lists: ok squares; 1:4:9:16:25: Strings are lists of characters. They are usually denoted by sequences of characters within ....s: ok def orange = 'o':'r':'a':'n':'g':'e':; ok orange; "'orange" Lists are lazy evaluated [l l], i.e. function calls in lists are not evaluated until they are selected. Lazy infinite lists may be constructed using recursiye functions. 2.4 Fields A field is an object tagged with a name and is denoted by the name followed by the object within '[' and ']'. The object is selected from a field with the o p e r a t o r ' ' " ok def plant = [tree "larch"]; ok p l a n t " tree; "larch" The field selection operator is overloaded to enable field object selection from lists: ok def plants = [tree "larch"]:[bush "privet" ]:[tree "fir"]:; ok plants ^ bush; "'privet" When there are several fields in a list with the same name, the index operator '@' is used to identify the requisite instance: ok plants A tree @ 2; "'fir" 2.5 Functions Functions are based on lambda calculus [1]. They may be defined with a single formal parameter: ok def cube = lam x.x,x,x; ok cube 3; 27 88 GREG MICHAEL.SON or a list of formal parameters: ok def sum_squares = lam x:y.x,x + y,y; ok sum_squares 3:4; 25 For function definitions, the 'lam' may be dropped and the formal parameters moved to the left of the ' = 's: ok def volume length:depth;height = length,depth,height; Function calls are evaluated in applicative order. Formal parameter lists may be arbitrarily complicated and used for structure match selection with actual parameter lists: ok def tl_hd (hh:ht):t = ht; ok tl_hd ("bangers":"mash"):("fish":"chips"):; ~'mash" 2.6 Control expressions Navel provides 'if" and 'case' control expressions which enable the construction of recursive functions: ok def power x:n = if n = 0 then 1 else x,(power x:n-l); ok def fib n = case n of o-) ~, 1-) 1, (fib n - l ) + (fib n-2); The 'case' expression ends with a default. 2.7 Local definitions Local definitions are used to introduce temporary name/object associations within expressions: ok def sum_squares x:y-- let sq n = n , n in (sq x ) + (sq y); They are evaluated in applicative order and are directly equivalent to function calls. Local definitions may be mutually recursive. For global definitions, as in POP2 [12], a forward reference to an undefined name in a function definition results in the creation of a dummy definition for the name which may be fully defined later, 2.8 Partial application Functions may return functions through partial application: ok def add x = lain y.x + y ; Interpreters from functions and grammars 89 ok def increment = add 1; ok increment 99; 100 Partial application is used in subsequent sections to construct update functions which, in effect, extend functions. For example, an array may be modelled as a function from addresses to the values held at those addresses. Initially, the array is empty and returns "fail" for any address: ok def array_0 addr = "fail"; When a value is assigned to an array address, a new function is constructed which returns that value if passed the corresponding address or calls the old function to find the value: ok def new_array old_array:address:value = lain addr. (if addr = address then value else old_array addr); Now, for example, '33' may be assigned to address '3': ok def array_l = new_array array_0:3:33; and "55' may be assigned to address '5': ok def array_2 = new_array array_l:5:55; ok a r r a y ~ 3; 33 ok array__2 6; "fail" 2.9 Input and output Output is effected with the 'write' expression which returns the value of its argument and also sends it to the standard output. Characters and strings are output without quotes: ok def writeln 1-~ let line = write 1 in let nl = write '\n' in ( ); ok writeln "festive greetings!"; festive greetings! The 'readln' object returns the next line from the standard input as a string: ok readln; testing testing 1 2 3 "testing testing 1 2 3" Amusing effects result from its use in function calls in lazy evaluated lists! Other facilities include character input, lazy file input and file output. 90 GROG MICHAELSON 2.10 Grammar rules Navel grammar rules are based on context free productions. The notation used is yet another variant of B.N.F. [13]. As in B.N.F., productions with the same left hand side name are combined into a single rule with a number of options. Unlike B.N.F., right hand sides of productions are not necessarily bound to particular left hand side non-terminals but are objects in their own right. For Navel rules, the terminal symbols are strings and the non-terminal symbols are names associated with strings or rules. A rule consists of one or more options separated by Ts within '{' and '}'. At simplest, an option is a sequence of one or more strings and names. For example, the B.N.F. for binary numbers: (binary) :: = (digit) (binary) I (digit) (digit) :: = 0 [ 1 might be rendered as: ok def binary = {digit binary I digit}; ok def digit = {"0" I " i " } ; 2.11 Parsing with rules A rule is used like a function to parse a string to return a list containing the list representation of the parse tree, or the empty list if the parse fails, and the rest of the string. Parsing is left to right. For a rule, each option is tried in turrr until one succeeds. If an option begins with a string and the argument string starts with that string then the rest of the option is applied to the rest of the argument string and the resulting parse tree list starts with the string starting the option. Otherwise the option fails: ok {"l"} "101"; ("1":):"01" ok {"i .... 0"} "lOl"; ("l":"O":):"l" Similarly, if an option begins with a name then its associated object is found and used and a field consisting of the sub-tree tagged with the name forms the start of the parse tree list: ok {digit} "10"; ([digit "1":]:):"0" ok {binary} "10"; ([binary [digit "l":]:[binary [digit "0":]:]:]:): ok {binary} "0123"; ([binary [digit "O":]:[binary [digit "l":]:]:]:):"2Y' No attempt is made to automatically factor rules or to remove left recursion because, as is discussed in Section 4, rules may be abstracted over and may contain any expressions that return strings or rules. Thus, it is not possible in general to check statically a rule for left recursion or options with common initial symbol sequences. With some rules the parser may fail to recognise valid strings because there is no backtracking within options. Option order may be important. If two options have a common initial symbol sequence and the shorter option precedes the longer then the shorter may lead to successful Interpreters from funct,ons and grammars 91 recognition of a sub-string: ok def binary = {digit ] digit binary}: ok {binary} "111"; ([binary [digit "'1":]:]:):"I1" 2.12 Finding a rule that produced a tree The 'rule' expression is similar to a 'case' expression and is used to identify a rule (not necessarily unique) that produced a parse tree. For example, to interpret binary numbers: ok def mdigit d = rule d of {"0"}-) 0, {"1"}-) 1, "fail"; ok def mbinary v b = rule b of {digit binary}-) mbinary 2,v + (mdigit b A digit) b ~ binary, {digit}-) 2,v + (mdigit b ^ digit), "fail"; ok def m string = let tree:rest = binary string in if tree = () I r e s t ( ) ( ) then "syntax error" else mbinary 0 tree; ok m "101"; 5 2.13 Simplifying rules Rules may contain sub-rules. A successful sub-rule option is treated as if it were in-line. The empty rule '{ }' matches anything and returns the empty list '( )'. This may be used to factor rules: ok def binary = {digit {binary I { }}}; The Navel rule 'number' matches a sequence of decimal digits and the rule "word' matches a sequence of letters: ok word "praxis"; "praxis": ok {number} "2001"; ([number "2001"]:): The rule 'fail' always fails. This is used in subsequent sections to initialise rules which are to be extended. Rules may be used for the generation of strings but this is not discussed here. 92 GREG MICHAELSON 2.14 Implementation The present Navel implementation is written in C and runs under UNIX*. Navel objects are represented by linked lists of tagged two-field cells held in a heap. All name/object associations are made and accessed via a stack. Programs are compiled to parse trees which contain stack frame relative addresses for names. These are found by symbolic evaluation at compile time. At run time, when a function is encountered the positions of objects for free variables are known from the compile time symbolic evaluation and the objects are picked up directly to make a closure. When a function is entere d, the actual parameter and free variable objects are pushed onto the stack. When the object associated with a name is required it is accessed directly through the stack. No text is held for programs. A pretty printer is used to reconstruct text from parse trees for function editing and saving global name/object associations as definitions in the hostess filing system. Preliminary timing tests suggest that for worst case insertion sort and for Ackermann's function. Navel runs at more than twice the speed of the 1979 SASL [14], the same speed as interpreted F R A N Z LISP [15] and half the speed of CProlog [16]. 3. FROM D E N O T A T I O N A L S E M A N T I C S TO A N A V E L I N T E R P R E T E R 3.1 Introduction The following sections describe a denotational semantics based on that for T I N Y [2] and discuss the construction of a Navel interpreter from it. TINY is an imperative language with assignment, integer arithmetic and I/O. Here. a subset of TINY is used without conditional and iterative commands to simplify the presentation. For example, to output double-the value of an input: a: = read b:=a+a output b 3.2 Denotational semantics 3.2.1 Semantic domains and abstract syntax. The semantic domains for T I N Y are: State = M e m o r y , I n p u t , O u t p u t Memory = I d e - ) [ N u m + {unbound}] Input = N u m , Output = N u m , Ide = identifiers Num = numbers A memory is a function mapping names onto values and returns 'unbound' for a name without an associated value. Inputs and outputs are lists of numbers. The abstract syntax is: E ::=011 [ read ] I I E l + E 2 where "E' ranges over the domain 'Exp' of expressions, and C ::=I:=El output E I C1 C2 where 'C' ranges over the domain 'Corn' of commands. 3.2.2 Semantic notation and auxiliary definitions. In the semantic notation, a list is denoted as a sequence of elements separated by ",'s within '(' and ")'. Alternatively, '.' is used as the concatenation operator. 'hd' and 'tl' select the head and tail of lists respectively. *UNIX is a trademark of Bell Laboratones. Interpreters from functions and grammars 93 The conditional e x p r e s s i o n - - ' a - ) b , c ' - - m e a n s that if "a" is true then "b" is returned: otherwise "c" is returned. This m a y also be used to match a list of values with a list of variables and bind the ,,ariables, as, for example, in the second definition for function sequencing below. The o p e r a t o r "," is used for function sequencing. There are cases for sequencing a monadic function with m o n a d m and dyadic functions: a) f: D 1 - ) [ D 2 + {errorl] g: D 2 - ) [ D 3 + {error}] f , ,,--c"D I - ) [ D 3 + {error's] f • g = larnbda x.(f x = e r r o r ) - ) e r r o r , g ( f x) b) f: D I - ) [ [ D 2 • D3] + {error's] g: D 2 - ) D 3 - ) [ D 4 + {error~j] f • g: D I - ) [ D 4 + {error~j] f • g = lambda x.(f x = e r r o r ) - ) e r r o r , ( f x = (d2,d3))-)g d2 d3 It also traps errors. For function updating. "f[rv/lv]" means that a new function is constructed which when called returns 'rv' for an a r g u m e n t with value "Iv'; otherwise "f" is called with the argument: f[rv/lv] 1 = 1 = l v - ) r v , f t This is used to extend the m e m o r y function with a name and value from an assignment. The following auxiliary function is used in semantic definitions: result: N u m - ) S t a t e - ) [ [ N u m • State] + [errorl] result = lambda v s.(~.s) 3.2.3 Semantic functions. The semantic function for expressions is: E: E x p - ) S t a t e - ) [ [ N u m • State] + {error}] E[0] = result 0 E[I] = result 1 E[read] = lambda (ma.o).null i - ) e r r o r , (hd i,(m,tl i,o)) E[I] = l a m b d a (m,i,o).m I = u n b o u n d - ) e r r o r , (m I,(m,i,o)) E[EI + E2] = E [ E 1 ] , l a m b d a v l . E [ E 2 ] , l a m b d a v2.result (vl + v2) Note that for an identifier T in an expression, the m e m o r y function ' m ' returns "unbound" if the identifier has not been assigned to. Thus the context sensitive syntax requirement that names be assigned before use has been dealt with in the semantics. In Section 5, a syntactic specification for this using higher order g r a m m a r rules is discussed. The semantic function for c o m m a n d s is: C: C o m - ) S t a t e - ) [ S t a t e + {error}] C[I: = E] = E[E] • l a m b d a v (m,i,o).(m[v/I),i,o) C[output E] = E[E] • lambda v (m,i,o).(m,i,v.o) c [ c 1 c2] = C [ C 1 ] • C[C2] 3.3 Navel interpreter 3.3.1 Concrete syntax. To construct a Navel interpreter from this definition, it is first necessary to contrive a concrete syntax for the language. Left recursion is removed through the introduction of auxiliary rules: ok def b a s e = { " 0 " I "1'" I "'read'" [ word}; ok def exp = {base " ' + ' " exp [ base}: ok def c o m m = {word "': = ' " exp I "'output" exp}; ok 94 GREG MICHAELSON def comms = {comm comms [ comm}; ok {comms} "a: = 0 b: = a"; ([comms [comm [word "a"]:": = ":[exp [base "0":]:]: ]: [comms [comm [word "b"]:": = ":[exp [base [word "a"]:]:]: ]: ]: ]: ): 3.3.2 Semantic notation and auxiliary definitions in Navel. Many aspects of the semantic notation have direct Navel equivalents but function sequencing must be modelled explicitly: ok def stara f g x = i f (f x ) = e r r o r then error else g (f x); ok def starb f g x = i f (f x ) = error " then error else let d2:d3 = f x in g d2 d3; Function updating is modelled by the functional: ok defu frv lv= lain arg. (if arg = lv then rv else f arg); 'unbound' and 'error' are modelled as strings: ok def unbound = "unbound"; ok def error = "error"; The auxiliary function 'result' translates to: ok def result v s = v:s; 3.3.3 Semantic functions. The semantic functions in Navel are: ok def mbase b = rule b of {"0"}-)result 0, { " l " } - ) r e s u i t 1, {"read"}-)lam m:i:o.(if i = () then error else (hd i):(m:(tl i):o)), Interpreters from functions and grammars 95 { w o r d } - ) l a m m:i:o.(if (m b " word) = u n b o u n d then error else ( m b word):(m:i:o)), "error-base":b; ok def mexp e = rule e of {base}-)mbase e ^ base, {base " + " e x p } - ) s t a r b (mbase e " base) lam vl.(starb (mexp e exp) lam v2.(result vl + v2)), "error-exp":e ok def m c o m m c = rule c o f {word ": = " e x p } - ) s t a r b (mexp c ^ exp) lam v m:i:o.(u m v c " word):i:o, { " o u t p u t " e x p } - ) s t a r b (mexp c ^ exp) lam v m:i:o.m:i:(v:o), "error--comm":c; ok def m c o m m s c = rule c o f {comm}-)mcomm c ^ comm, ^ ^ {comm c o m m s } - ) s t a r a ( m c o m m c c o m m ) ( m c o m m s c comms), "error-comms":c; - 3.3.4 Interpreter. To run the semantic functions as an interpreter, a function is constructed which takes the string for a T I N Y p r o g r a m with the input list, parses the p r o g r a m and calls the "mcomms' semantic function with the tree, an initial m e m o r y which returns ' u n b o u n d ' for all names, the input and an empty output list: ok def interpret text input = let tree:rest = c o m m s text in if tree = () [ r e s t ( ) ( ) then "syntax error":rest else let s = m c o m m s tree (lain name.unbound):input: in if s = error then error else let m:i:o = s in o; ok interpret "a: = read output a + a" 1:; 2: 3.3.5 Interactive testing. F o r interactive testing, 'interpret' m a y be called repeatedly by a recursive function that p r o m p t s for and inputs the p r o g r a m text and input lists: ok def p r o m p t _ r e a d p r o m p t = let p = write p r o m p t in readln; 96 GREG MICHAELSON ok def run n = let text = p r o m p L r e a d "'program)" in let data = convert (prompt_read "'data)") in let output = writeln (interpret text data) in run n; ok run ( ); program) output read + read data) 1 0 1: program) . .. The function 'convert" should transform a string into a list of numbers. One way is to parse the string and strip out the fields from the tree: ok def convert string = let numb = {"0" I "1"} and n u m b s = (numb numbs I numb} and value n = if n = "0": then 0 else 1 . . . . and strip n = rule n of {numb}-) (value n ^ numb):, {numb numbs}-) (value n ^ numb):(strip n " numbs), "'input error":n in strip (hd (numbs string)); Thus, rules may also be used to define and process the inputs for a programming language. Full checks that the inputs are correct are not included here. 3.4 Denotational semantics and Navel A denotational semantics consists of three separate sections with distinct description languages. In Navel, there are no semantic domain specifications and syntax and semantics are described in a unitary language. The domain specifications are used to describe the functionality of the semantic equations. This would correspond to specifying explicitly the types of formal parameters and results of functions in a strongly typed language and would enable checks for type consistency at compile time. Navel is weakly typed with run-time type checking. It is thus less rigorous than denotational semantics and has a relatively inefficient implementation. In denotational semantics, abstract syntax mediates concrete syntax and semantic equations. In Navel, there is no necessity to introduce abstract syntax. In principle, its absence will make the interpreter less general: the semantic functions must be changed if the concrete syntax is changed whereas for a denotational semantics it would only be necessary to change the concrete/abstract syntax association specification. In practise, however, it is not clear that language design and development proceed cleanly with changes in one area isolated from other areas. The omission of abstract syntax may lead to extra semantic functions, as in this example, but reduces the number of different stages in a design. There is one denotational semantic equation for each abstract syntax construct with sub- equations for each sub-construct. Sub-constructs and sub-equations are associated through implicit pattern matching on the sub-construct abstracted implicitly from a sub-tree. Sub-trees are then selected implicitly by mention of the associated sub-construct name. Interpreters from functions and grammars 97 Part of the motivation for Navel's development was to investigate the association of syntax and semantics in language definitions. This is highlighted through explicit operations on tree represent- ations for rule identification and sub-tree selection. Here, Navel interpreters are more akin to operational semantics [17]. 4. G E N E R A L I S I N G RULES Rules have the same civil rights as other Navel objects. In particular they may be abstracted over which enables the specification of rule schemas. For example, consider the following rules for sequences of numbers and words: ok def numbers = {number {"," numbers l[ }}}: ok def w o r d s = {word ['1:" words I {}}}; These have a common structure which may be expressed by: ok def sequence one separator m a n y = {one {separator many I~ ~fi,~'~" and they may now be defined by: ok def numbers = sequence number "," {numbers}; ok -- def words = sequence word ":" {words}; When rules are formed in this way, the parse tree list fields will contain the names from the schema: ok numbers "11,22"; ([one [number "1 I"]]: [separator ",":]: [many [numbers [one [number "22"]]:]:]:): Unlike LISP[18] literals are not objects and so here there is no literal replacement. As with functions, the values of the arguments are used. Navel allows rules to contain bracketed expressions returning rules or strings. These are then treated as if they were in-line. Bracketing a name results in its value being used without a field being constructed for the resulting sub-tree. This may be used to make rule schemas which give appropriate names to fields: ok def sequence one separator many = {(one) {(separator) (many) [ {)}I: ok def numbers = sequence number "," {numbers}; ok numbers "11,22"; ([number "'11"]: [numbers [number "'22"]:]:): Such abstraction over rules enables the equivalent of consistent substitution in two-level grammars [19]. Two-level grammars are based on the generation of rules from rule schemas through what is in effect the macro expansion of rule names. This contrasts with Navel where names are fixed and rules are generated from schemas through parameter substitution. 98 GILEG MtCrtAELSON For example: ok def match n symb = ifn=l then {(symb)} else {(symb) (match n-1 symb)}; will generate a rule to recognise a specified length sequence for a given symbol. This may be used to recognise: a.b.c ~ for any given 'n'; ok def anbncn a b c n = {(match n a) (match n b) (match n c)}; ok def xnynzn-- let x = "x" and y = " y " and z = "z" in anbncn {x} {y} {z}; ok xnynzn 2 "xxyyzz"; ([x "'x"]:[x "'x"]:[y "y"]:[y "y"]:[z "z"]:[z "z"]:): Recognising: anbnc n for an unknown 'n' is more tricky. It might appear that: ok def anbncnstar a b c n = {(anbncn a b c n) I (anbncnstar a b c n + 1)}; would do the trick. This is, alas, only a partial solution. If the string being checked is not of the form: anbnc n then there is no value of 'n' for which the first rule option can succeed and the rule will recurse indefinitely. Such context sensitive checking requires the passing of information about the parse from one part of a rule to another. Context free rules have no mechanism for information passing other than that an earlier rule part must have succeeded for a later rule part to be reached. This may be overcome within a functional framework by making the application of each rule part explicit and then generating and passing information between them through intermediate processing. In effect, information from the start of the parse is used to select or construct appropriate rules from the rest of the parse. For example, to return to: a,bOc. where 'n' is unknown until the sequence has been examined, processing the 'a's may be used to guide the construction of a new rule to recognise the corresponding number of 'b's and ~c's. For each 'a', a new 'b' is added to the rule for 'b's and a new 'c' is added to the rule for "c's. At the same time, the tree for the 'a's is accumulated. When all the 'a's have been found, a rule Interpreters from functions and grammars 99 is built to recognise the same number of "b's and "c's. The trees for the 'a's and for the "b's and 'c's are joined to form the final tree. ok def an a b c atree bn cn text = let tl:sl = a text in if t l ( )( ) then an a b c (hd tl):atree {(b) (bn)} {(c) (cn)} sl else let t2:s2 = {(bn) (cn)} sl in (append atree t2):s2; ok def anbncn a b c = a n a b c () {} {}; These approaches are used in the next two sections for parsing context sensitive aspects of programming languages. 5. C H E C K I N G V A R I A B L E A S S I G N M E N T B E F O R E USE 5.1 Assignment checking Considering the language T I N Y from section 4 with the additional requirement that an identifier in an expression must have appeared on the left had side of a preceding assignment. This may be specified in Navel in a manner similar to dynamic syntax [20, 21] by constructing rules from assignments for use in checking subsequent commands. Initially, a base may only be a digit ~3r a 'read': ok def b a s e = {"0" I "1" [ " r e a d " } ; ok defexp = {base " + " e x p l base}; ok def comm = {word ": = " exp I " o u t p u t " exp}; and there are no assigned variables: ok def assigned = fail; The rule for assigned variables is extended by the function: ok def new id old = {(id) [ (old)}; which when passed an identifier and the previous rule for assigned variables returns a rule which tries to recognise that identifier before using the previous assigned variable rule. When an assignment has been recognised, the identifier is checked to see if it is recognised by the rule for assigned identifiers. If it is then it has been assigned already and the current rules for assigned identifiers and commands are returned. Otherwise, the identifier is added to the rule for assigned identifiers which is then used to build a new rule for recognising commands: ok def check id:assigned:comm = let t:r = assigned id in if t( )( ) then assigned:comm else let newassigned = new id assigned in newassigned:(makecomm newassigned); 100 GREG MICHAELSON ok def m a k e c o m m assigned = let b a s e = {assigned I '0"" I "1"" I "'read"] and exp = {base " ' + "" exp I basel and c o m m - - [ w o r d "': = "" exp I "'output" exp l in {comm}; 5.2 Command sequence and program checking C o m m a n d sequences are recognized by a rule that picks up the first command. If it is the last c o m m a n d in the sequence then the tree for it is returned. Otherwise, if it is an assignment then new c o m m a n d and assigned identifier rules are constructed. The commands rule is called recursively to recognise the rest of the sequence and a final tree is constructed and returned: ok def comms assigned:comm text = let tl:rl = c o m m text in if tl = ( ) then ( ):text else if r l - - ( ) then ([comms tl]:): else let t'~:r2 = rule t l " comm of {word "': = " e x p } - ) comms (check (id tl):assigned:comm) rl, comms assigned:comm rl in if t 2 = ( ) then ( ):text else ([comms (hd tl):t2]:):r2; ok def id c = c ^ c o m m ^ word; The rule for a program is constructed from the rule for commands with the initial assigned identifier and c o m m a n d rules: ok def program = comms assigned:{comm}; ok program "'a: = 0 b: = a"; ([comms [comm [word "a"l:":---":[exp [base "0":]:]: 1: [comms [comm [word "b"]:": = ":[exp [base [assigned "a":]:]:]: ]: ]: 1: ): The interpreter can now be simplified with the removal of checks for errors from unassigned identifiers. Interpreters from functions and grammars I01 5.3 Checking in syntax and semantics In the TINY semantics in Section 4, the update function: def u f r v I v = lam arg.(if arg = lv then rv else f arg); was used to extend the memory function. Here: def new id old = {(id) I (old)}; is used in the same way to extend rules. "Iv' and 'f' in the update function correspond to "id' and 'old" in the rule extender, The return o f ' r v ' for 'Iv' in the update function corresponds to the return of a tree for the successful recognition of 'id'. There is no equivalent for "arg' because rules have implicit formal parameters. Here it may be made explicit by: def new id old = lain arg.({(id) L (old)} arg); The use of the update and rule extender functions are further analogous. In TINY, assignments both introduce and update name/value associations For a name in an expression, the memory function is used to find the corresponding value. If the required name had not been introduced by a previous assignment then the memory function would return 'unbound'. Assignment before use is a syntactic requirement and is irrelevant for semantics [2] but the use of a memory function implicitly encompasses the checking of this requirement. 6. D E C L A R A T I O N AND TYPE C H E C K I N G 6.1 Syntax for declared and typed variables The following rules describe partially the syntax of a language fragTnent with declarations. assignment and integer arithmetic: ok def program = {declarations statements}; ok def declarations = {declaration {declarations I }}}; ok def declaration= {{"long" I "short"} word}; ok def statements = {statement {statements I {I}}: ok def statement = {word ": = " expression}: ok def expression = {base { " + " base I { }}}; ok def base = {word [ number [ "(" expression ")"}; It is required that no identifier may appear in more than one declaration and that an identifier must appear in a declaration before it appears in a statement. It is intended that the language should enable 'long' and 'short' precision arithmetic, that variables be typed according to the precision of the values they may hold and that 'long' values may not be coerced to 'short'. Thus if the identifier on the left of a statement appears in a declaration preceded by "short" then all identifiers in the expression on the right of the statement must also appear in declarations preceded by "short". 6.2 Denotational semantics with declarations For a denotational semantic definition of an imperative language with declarations it is sometimes convenient to model name/value associations in two stages, mediated by addresses, CL II 2--D 102 GREG MICHAELSON through an environment function which records name/address associations and a store function which records address/value associations. When a declaration is encountered, a new name/address association is added to the environment. When a name is encountered in a command, the environment is used to find the associated address. If the name is in an expression then the address is used to find the corresponding value from the store. If the name is that of a variable which is being assigned to then a new address/value association is added to the store. As with the memory function for TINY, environment use may encompass name checking. If a name in a command has not appeared in a preceding declaration then the environment will not have an address for it. Similarly, if a name in a declaration has appeared in a preceding declaration then the environment will already have an associated address for it. The environment function may also be extended to enable the recording and checking of type information for names. 6.3 Declaration checking Name checking may be transferred to the syntax with the construction of appropriate rules from declarations. For the above language, separate rules will be used to check whether or not identifiers have been declared as "'short" or "'long". Initially, nothing has been declared and so these rules should not match anything: ok def init_short = fail; ok def init_long = fail; As before, rules are extended with the function: ok def new old name =-{(name) ( o l d ) } ;[- When a declaration is found, the rules are used to check the identifier for re-declaration and the appropriate rule is then extended with the identifier: ok def decls string:long:short = let tree:rest = {{"short" I "long"} word} string in if tree = ( ) then long:short:string else if hd(long tree " w o r d ) ( ) ( ) I hd(short tree ^ w o r d ) ( ) ( ) then long:short:string else rule tree of {"long" word}-) decls rest:(new long tree ^ word):short, {"short" word}-) decls rest:long:(new short tree ^ word), "never going to reach h e r e . . . " ; 6.4 Assignment and program checking Separate rules are required to recognise assignment to variables declared as "long' and 'short'. They are built from a schema which defines local rules for recognising expressions: ok def makeassign nametype basetype = let base = {(basetype) J number f "(" expression ")"} and expression = {base { " + " base I { }}} in {(nametype) "': = " expression}; Programs are parsed by a function which uses 'decls' to parse the declarations and construct the rules for 'long's and "short's. They are then used with the assignment schema to construct the Interpreters from functions and grammars 103 assignment rules which are incorporated into local rules for recognising statements: ok def program string = let long:short:rest = decls string:init_long:init_short in let lassign = makeassign {long} {long ! short} and sassign = makeassign {short} {short} and statement = {lassign I sassign} and statements = {statement {statements [ { }}} in statements rest; ok program "'short a long b short c c: = a + a b: = c + b"; ([statement [sassign [short "c":]: [exp [base [short "a":]:]:" +":[base [short "a":]:]:]: ]: ]: [statements [statement [lassign [long "b":]: [exp [base [short "c":]:]:" +":[base [long "b":]:]:]: ]~ ]: 1: ): To simplify the example there are no diagnostics and no tree is built for declarations: these could easily be added. 7. C O N C L U S I O N S The approaches described here could be used with any language with general pattern recognition facilities. For example, string scanning in Icon [22] is ideal for context free parsing and may be combined naturally with list construction for tree production. Prolog [23] provides context free grammar rules as syntactic sugaring of the underlying pattern recognition aspects of unification. Navel's novelty lies in the integration of grammar rules as first class objects within a functional language. Work continues on the use of higher order grammar rules in interpretive language imple- mentations, for example for handling forward references and to associate abstract and concrete syntax. Another area of interest is the more general use of grammar rules for the direct construction of data structures from data specifications. 8. S U M M A R Y This paper discusses Navel, a functional language which is intended for use as an interpreter- interpreter to simplify the construction of interactive tests intepreters from formal language definitions. Denotational semantics provides a rigorous and versatile methodology for defining pro- gramming languages. However, current production compiler generators are not suitable for direct experimentation with such language definitions because their semantic definition facilities are based on imperative languages which do not correspond closely to denotational semantics semantic [04 GREG M[CHAELSON equations. The compiler generator SIS is designed for use with denotational semantic definitions but is based on the separation of lexicaL syntactic and semantic formalisms which may restrict its flexibility. Pure functional languages are very similar to the semantic equations of denotational semantics but lack facilities for syntactic specification. Navel is a functional language with fully integrated grammar rules, implemented in an interactive environment. Its use enables the unitary expression of the syntactic as well as the semantic aspects of a denotational language definition. Although there is some loss of rigour, interpreter construction is simplified greatly. Here, Navel is illustrated through examples and used to construct an interpreter from the denotational semantic definition of a simple assignment language. Denotational semantics does not address the context sensitive aspects of programming language syntax. Navel provides a means to handle context sensitivity through functional abstraction over grammar rules to construct rule schemas, which are analogous to higher order functions. Here, this approach is presented through discussion of the classic 'a"b~c"' problem and is used to process assignment and declaration before used, and simple type matching. Acknowledgements--This work was started in the Department of Computing Science, University of Glasgow and is continued in the Department of Computer Science, Heriot-Watt University. The author wishes to thank Bdl Findlay (Glasgow). David Watt (Glasgov,). Paul Chisholm (Heriot-Watt) and Stuart Anderson (Heriot-Watt) for much fruitful d~sputation, Zdrav Podolski (Glasgow,,) and Chris Miller (Heriot-Watt) for making UNIX intelligible and David Watt, Paul Ch,sholm, Stuart Anderson and Hov~ard Williams (Heriot-Watt) for chewing through previous drafts of this paper. REFERENCES I. Stoy J Denotational Semantics: The Scott-Strachey Approach to Programming Language Theoo'. MIT Press. Cambridge. Mass. (1977). 2. Gordon M. The Denotational Description of Programm#tg Languages. Springer, New York (1979). 3. Tennent R. Language design methods based on semantic principles. Acta Informatica 8, 97-112 (1977). 4. Gries D. Compiler Constructton for Digttal Computers. Wdey, New York (1971). 5 Johnson S, Yacc: yet another compiler compiler No 32, Computing Science Technical Report, Bell Laboratories. Murray Hill, NJ 07974 (1975). 6. Hopcroft J. and Ullman J. Formal Languages and their Relation to Automata. Addison-Wesley, Reading, Mass. (1969). 7. Sethi R. Control flow aspects of semantic-directed compiling. ACM Trans. Programming Lang. Systems 5(4), 55'4-595 (1983). 8. Mosses P. SIS--Semantics lmplementatton System Reference Manual and User Guide. DAIMI MD-30, Computer Science Department, Aarhus University, Denmark (1979). 9. Turner D. SASL Language Manual. Department of Computational Science, University of St Andrews, St Andrews, Scotland (1975). 10. M,chaelson G. User Gutde to NAVEL. Department of Computer Science, Heriot-Watt University, Endmburgh, Scotland (1984) 11. Friedman D. and Wise D. CONS should not evaluate its arguments. In Automata, Languages and Programming (Edited by Milner R. and Michaelson S.), pp. 257-284. Endinburgh University Press, Edinburgh, Scotland (1976). 12. Burstall R., Collins J. and Popplestone R. Programming in POP2. Edinburgh University Press, Edinburgh, Scotland (1977). 13. Naur P. (Ed.), Revised report on the algorithmic language ALGOL 60. Commun. ACM 6(1). 1-17 (1963) 14. Turner D. SASL Language Manual. CS/79/3, Department of Computational Science, University of St Andrews, St Andrews, Scotland ([979). 15. Foderaro J. and Sklowar K. The FRANZ LISP Manual. University of California at Berkeley (1983). 16. Pereira F. CProlog User's Manual Verston 1.1 EdCAAD, Department of Architecture, University of Edinburgh, Edinburgh. Scotland (1982). 17. Wegner P. The Vienna defimtion language. ACM Comput. Surv. 4(I), 5-63 (1972). 18. McCarthy J. The LISP 1.5 Programmers" Manual. MIT Press, Cambridge, Mass. (1969). 19. Cleaveland J. and Uzgahs R. Grammars for Programming Languages, 4. Elsevier Computer Science Library (Programming Languages Series), Amsterdam (1977). 20. Hanford K. and Jones C. Dynamic syntax: A concept for the definition of the syntax of programming languages. TR 12.090, IBM Tech. Rep. (1971). 21. Ginsburg S. and Rounds E. Dynamic syntax specification using grammar forms. IEEE Trans. Software Engng SE..4I 1). 44-55 (1978). 22. Griswold R. E. and Griswold M. T. The Icon Programming Language. Prentice-Hall, Englewood Chffs, N.J. (1983). 23. Clocksin W. F. and Mellish C. S. Programming in Prolog. Springer, Berlin (1981). About the Author--GgEGOR'~ JoH'q MICHAELSONhas a B A. in Computer Science from the Umvers~ty of Essex (1973) and an M.Sc. in Computational Science from the University of St Andrews (1982). He has taught Computer Studies at Napier College, Edinburgh. Computing Science at the University of Glasgow, and is at present a Lecturer in Computer Science at the Heriot-Watt University, Edinburgh. His current research is into the use of formal definmons in programming language implementations.

References (23)

  1. I. Stoy J Denotational Semantics: The Scott-Strachey Approach to Programming Language Theoo'. MIT Press. Cambridge. Mass. (1977).
  2. Gordon M. The Denotational Description of Programm#tg Languages. Springer, New York (1979).
  3. Tennent R. Language design methods based on semantic principles. Acta Informatica 8, 97-112 (1977).
  4. Gries D. Compiler Constructton for Digttal Computers. Wdey, New York (1971).
  5. Johnson S, Yacc: yet another compiler compiler No 32, Computing Science Technical Report, Bell Laboratories. Murray Hill, NJ 07974 (1975).
  6. Hopcroft J. and Ullman J. Formal Languages and their Relation to Automata. Addison-Wesley, Reading, Mass. (1969).
  7. Sethi R. Control flow aspects of semantic-directed compiling. ACM Trans. Programming Lang. Systems 5(4), 55'4-595 (1983).
  8. Mosses P. SIS--Semantics lmplementatton System Reference Manual and User Guide. DAIMI MD-30, Computer Science Department, Aarhus University, Denmark (1979).
  9. Turner D. SASL Language Manual. Department of Computational Science, University of St Andrews, St Andrews, Scotland (1975).
  10. M,chaelson G. User Gutde to NAVEL. Department of Computer Science, Heriot-Watt University, Endmburgh, Scotland (1984)
  11. Friedman D. and Wise D. CONS should not evaluate its arguments. In Automata, Languages and Programming (Edited by Milner R. and Michaelson S.), pp. 257-284. Endinburgh University Press, Edinburgh, Scotland (1976).
  12. Burstall R., Collins J. and Popplestone R. Programming in POP2. Edinburgh University Press, Edinburgh, Scotland (1977).
  13. Naur P. (Ed.), Revised report on the algorithmic language ALGOL 60. Commun. ACM 6(1). 1-17 (1963)
  14. Turner D. SASL Language Manual. CS/79/3, Department of Computational Science, University of St Andrews, St Andrews, Scotland ([979).
  15. Foderaro J. and Sklowar K. The FRANZ LISP Manual. University of California at Berkeley (1983).
  16. Pereira F. CProlog User's Manual Verston 1.1 EdCAAD, Department of Architecture, University of Edinburgh, Edinburgh. Scotland (1982).
  17. Wegner P. The Vienna defimtion language. ACM Comput. Surv. 4(I), 5-63 (1972).
  18. McCarthy J. The LISP 1.5 Programmers" Manual. MIT Press, Cambridge, Mass. (1969).
  19. Cleaveland J. and Uzgahs R. Grammars for Programming Languages, 4. Elsevier Computer Science Library (Programming Languages Series), Amsterdam (1977).
  20. Hanford K. and Jones C. Dynamic syntax: A concept for the definition of the syntax of programming languages. TR 12.090, IBM Tech. Rep. (1971).
  21. Ginsburg S. and Rounds E. Dynamic syntax specification using grammar forms. IEEE Trans. Software Engng SE..4I 1 ). 44-55 (1978).
  22. Griswold R. E. and Griswold M. T. The Icon Programming Language. Prentice-Hall, Englewood Chffs, N.J. (1983).
  23. Clocksin W. F. and Mellish C. S. Programming in Prolog. Springer, Berlin (1981). About the Author--GgEGOR'~ JoH'q MICHAELSON has a B A. in Computer Science from the Umvers~ty of Essex (1973) and an M.Sc. in Computational Science from the University of St Andrews (1982). He has taught Computer Studies at Napier College, Edinburgh. Computing Science at the University of Glasgow, and is at present a Lecturer in Computer Science at the Heriot-Watt University, Edinburgh. His current research is into the use of formal definmons in programming language implementations.
About the author
Heriot-Watt University, Department Member
Papers
210
Followers
136
View all papers from Greg Michaelsonarrow_forward