Chapter 3. Expressions

The Rosetta expression language is used to define properties involving Rosetta items. The simplest Rosetta expression is an atomic expression consisting of a literal or an item name and forms the leaves of the expression tree. Function and operator applications define operations over other expressions by instantiating function values with actual parameters. The if and case expressions define selection operations using boolean conditions and set membership, respectively. The let expression defines local variables over expressions. Because all expression types other than atomic expressions are defined recursively over expressions, compound expressions are formed by nesting expressions of all types.

All Rosetta expressions from atomic to compound have associated types. Determining types is instrumental to analyzing specifications for type safety, a critical aspect of system correctness. Due to the richness of the Rosetta type system, typing and type checking are particularly critical to successful design activities. To determine the type of an expression, Rosetta uses a combination of declared and asserted type information with rules for each expression type. The types of atomic expressions can be learned from their declarations. Similarly, the types of function and operator applications can be learned from their declarations and parameter values. Special rules exist for inferring the types of if , case, and let expressions from their sub-expressions.

Atomic Expressions

An atomic expression is the simplest expression consisting of a value or an item label. Such expressions are called atomic because they cannot be reduced to a simpler form and have no identifiable parts. Examples of atomic expressions include:

 1          // Value of 1 in base 10
-16         // Value of -16 in base 10
21111     // Value of 15 in base 2
123e2       // Value of 12300 in base 10
21111e1   // 11110 in base 2

'a'      //Character a
a        //Item named a
"Hello"  //String value
Hello    //Item named Hello
true // Boolean true value

The examples shown demonstrate several mechanisms for specifying atomic items ranging from number values and string literals to item names.

An item label used as an expression refers to the item it names. Thus, the type associated with an item label is the type of its item’s declaration. Because each item must be declared and each declaration includes a type, the type of a declared item is always known.

The type associated with a literal value is discussed extensively in Chapter 4. For now it is sufficient to understand that a literal’s type is its most restrictive legal type. For example, the number 3 is an element of several types, including complex, real, and posreal. When appearing in a specification, its type is posint because no subtype of posint contains 3. However, when performing type checking, 3 can be treated as posint or any of its supertypes.

Function Application

A function application is simply the instantiation of a function with a collection of actual parameters. For example:

cos(x)

is the application of the cosine function to an item, x.

Function applications are specified using the following form:

f(e0,e1,e2,...,en)

where f is the name of an item whose type is a function and e0 through en are expressions representing the function’s actual parameters; ek may be any expression of the same type or a subtype of its associated formal parameter in the definition of f. Thus, actual parameters may be function applications, sequence references, literals, or any other expression satisfying type conditions. Examples of function applications include:

inc(1)           // Increment the value 1
add(1,2)         // Add the values 1 and 1
inc(x)           // Increment the value of x
add(1,inc(x))    // Add the value 1 to inc(x)

Functions of a single argument are frequently referred to as unary functions, and unary functions whose return type is boolean are referred to as predicates. Functions of two arguments are frequently referred to as binary functions, while binary functions whose return type is boolean are referred to as relations.

Rosetta function application is lazy and defined using curried function semantics. Curried function semantics turns multi-parameter functions into a sequence of unary function applications. For example:

add(1,inc(x)) == (add(1))(inc(x))

The application of the two-argument add function to its first argument results in a unary function that adds 1 to its argument. When this result is applied to inc(x), it adds one to the result of the function application. It is perfectly legal to partially specify function arguments. Specifically, the following definition of inc can be perfectly legal:

inc == add(1)

The inc function that increments its argument is equivalent to the add function with one argument instantiated with one. Currying and function evaluation will be explained later. For now one need only understand that function applications are applications of function items to formal parameters and that curried functions are allowed.

The type of a function application whose formal parameters are of the correct type is the return type specified in its declaration. If the formal parameters are not of the correct type, the function application’s type cannot be determined and it is not soundly typed. Consider a simple function declaration for an increment function:

inc(x::natural)::natural is x+1;

Legal applications of inc include:

inc(1)
inc(inc(1))
inc(2+3)

In each case, the formal parameter to inc can be determined to be a subtype of natural. Thus, the type of each application is natural, the type specified by inc’s declaration. In contrast, illegal applications of inc include:

inc('a')
inc(-1)
inc([1,2,3])

In each case, the formal parameter is not a subtype of natural, no type can be inferred, and the application is not well-typed.

Operator Application

An operator application is a special syntax for function application. For example:

x + y // add x to y

%x       // convert x from boolean to bit
1,2,3    // form a set containing 1,2, and 3

are all operator applications and represent the following function applications:

__+__(x,y)
%__(a)
{__}{1,2,3}

where the function name is obtained by replacing arguments with underscores.

Operators come in four forms, infix, prefix, postfix, and mixfix. The most common operator form is the traditional infix notation used for traditional mathematical operators. Infix operators use the syntax:

x o y

where o is the operator being applied and x and y are arguments to the operator. Examples of infix operations include:

1 + sqrt(x)                    // add 1 to square root of x
x or y                        // boolean disjunction of x and y
x & "⊔error"                 // concatenate x with a literal string

Prefix operations are also quite common and include many mathematical operators such as negation and size. The syntax for prefix operations is:

o x

where o is the prefix operation and x is its argument. Examples of prefix operators include:

-x // negate x
#x // size of x
%x // convert x to a boolean or bit

Mixfix operators are special operators, where the arguments are mixed within the operation. There are several forms of mixfix operations, including formers and if expressions. Formers are used to collect items and form new items representing collections such as sequences and sets. The syntax of a former is generally:

< arglist >

where “<” and “>” represent the name of the former and arglist is a comma-separated collection of arguments. Examples of formers include set formers, sequence formers, and function formers:

 {1,2,3}       // set containing 1,2, and 3
 {*1,1,3*}     // multiset containing 1,1, and 3
 [1,2,3]       // sequence containing 1,2, and 3
 <* (x::integer)::integer *>       // function type or signature
 <* (x::integer)::integer is x+1 *> // anonymous function

The syntax of the function former differs from the set and sequence formers.

Operators are special syntactic forms of function calls. In the case of pre- and postfix operations, the definition of the operation is provided by a unary function. In the case of infix operations, the defining function is binary. Mixfix operations differ based on the number of arguments, but still use a traditional function for their semantic definition. Special function names derived from operator symbols allow definition of operator functions. We take the operator and replace parameters with double underscores to form the name. For example, the names for the % and + operators are defined as follows:

%__       // function name for % operation
__+__     // function name for + operation

The following equivalences define mapping from operators to function applications:

%x Operator Application %__(x)
a+b+c Operator Application __+__(__+__(a,b),c)

Now operators and functions share a common semantics for evaluation. Such equivalences, called derived forms, allow sharing of semantics across language constructs.

Because operators are special forms for functions, the types associated with operator application are defined in the same manner. Specifically, if operator arguments are of a type compatible with the operator, then the operator application type is obtained from the function signature. Unfortunately, overloading of operators complicates this process substantially, with the number operations introducing significant complexity. Chapter 4 discusses operators over basic types extensively and describes operator type inference.

If Expressions

An if expression specifies choice between values based on a boolean expression. The if expression:

if x>0.3 then 1 else 0 end if

equals 1 if x>0.3 and 0 if it does not. Thus the term:

X′ = if x>0.3 then 1 else 0 end if

asserts that the next value of x equals the result of evaluating the if expression.

Most traditional programming languages use an if statement rather than an if expression. The distinction is that an if expression, like all expressions, has an associated value. An if statement simply orders the application of other statements. This distinction appears in the previous example. It can be rewritten in a form similar to an if statement by lifting the if outside the equality:

if x>0.3 then x′ = 1 else x′ = 0 end if

The net result is the same — an assertion of a value for x in the next state. The formal syntax of the if expression uses the traditional block style more typical of if statements and includes an elseif option:

if c0 then e0
   [[elseif ck then ek]]*
   else en
 end if

In the if expression, all cj expressions must be of type boolean while all ej expressions must be of the same type. This assures that the if expression will have a single type. The if expression evaluates to the first ej associated with a true condition. If no conditions are true, then evaluation results in en associated with the else keyword. Although the if expression is block-oriented and reminiscent of imperative languages, it is a true expression and not a statement. Like all expressions, if expressions have values and types.

The most common form of the if expression is the simple if-then-else form:

if ec then et else ef end if;

The rule for evaluating this expression is a simplification of the general semantics. When ec is true, the expression is equal to et. When ec is false, the expression is equal to ef. Specifically:

if true then et else ef end if If Expressions et

if true then et else ef end if If Expressions ef

These two equivalences define two-thirds of the if semantics. The final rule states that when et is an expression other than a boolean value, it must be evaluated first.

Understanding the elseif construct is a simple extension of the if expression presented so far. The following expression:

x′ = if x>0 then 1
       elseif x<0 then -1
       else 0
     end if

is equivalent to:

x′ = if x>0 then 1
       else if x<0 then -1
              else 0
            end if;
    end if;

Applying the previous rules to this form results in the expected behavior. If x>0 the expression is equal to 1. If x<0 then the expression is equal to -1. Otherwise the expression is equal to 0.

The if expression must be viewed like all other operators as a function with an associated type and value. The value is determined by evaluating one of the two expressions based on a boolean condition. Again, the if expression does not define paths or sequences of instructions in a Rosetta specification.

Example 3.1. If Expressions

Evaluating the following form:

if x=1 then f(x) else g(x) end if;

results in f(1) when x=1 and g(x) otherwise. Note that the expression is replaced by these results where it occurs in a specification.

Evaluating the following form:

if p(x) then f(x)
  elseif q(x) then g(x)
  else h(x)
end if;

results in f(x) when p(x) is true; g(x) when p(x) is false and q(x) is true; and h(x) when both p(x) and q(x) are false.

Each expression, f(x), g(x), and h(x), must have the same type.

The type of an if expression is derived from the sub-expressions defined in the then and else clauses. The conditional expression must be of type boolean and the then and else expressions must be of the same type. If these conditions hold, the type of the if expression is the type of the then and else expressions. When elseif clauses are present, they must share the type of the then and else clauses. The following if expression defines a signumm, or sign, operation:

if x >= 0 then 1 else -1 end if;

x >= 0 is a boolean expression, thus the type of the if expression is the common type of 1 and -1, or integer. It can be used wherever an integer is allowed. The following forms are legal uses of this expression:

 (if x >= 0 then 1 else -1 end if) + 1;
 sgn(x::integer)::integer is if x>= 0 then 1 else -1 end if;

In contrast, the following forms are not legal uses:

 s(if x >=0 then 1 else -1 end if)
 (if x >=0 then 1 else -1 end if) and 1

In the first case, sequence access must use a natural value. Because the if expression is of type integer, there is no guarantee that when evaluated the result can be used to access a sequence element. In the second case, and expects bit arguments.

Although the if expression can produce a value 1::bit, it is not guaranteed to produce a bit.

Example 3.2. If and Conditional Equivalence

Among the most common uses of if statements in a traditional programming language is to provide a mechanism for conditional assignment. A simple case of such a description from the digital design domain is the definition of a multiplexer. A naive specification of a Rosetta multiplexer has the form:

facet mux(i0,i1,c:: input bit; z:: output bit)::state_based is
begin
  if c=0 then z'=i0 else z' = i1 end if;
end facet mux;

In this case, the if expression evaluates to either z’=i0 or z’=i1, depending on the value of c. To completely understand this specification, it is important to note that z’ refers to the next value of z much like the (VHDL) after clause that specifies values for signals sometime in the future.

A more succinct method of writing the multiplexer if expression in Rosetta takes advantage of the if expression feature that it is in essence a function. By lifting the equivalence out of the if expression, the mux facet can be rewritten as:

facet mux(i0,i1,c:: input bit; z::output
bit)::state_based is begin
  z'=if c=0 then i0 else i1 end if;
end facet mux;

Semantically, the two definitions are identical. However, the second specification more directly defines what the multiplexer does. If c=0 is true, then the if expression simplifies to i0. Otherwise, it simplifies to i1.

It should be noted that if an if expression’s type is determined to be top, it is virtually useless as a specification expression. If a system is to be well-typed, the only place such an expression can be used is where a top is expected. Few functions are general enough to operate on top typed arguments.

Example 3.3. Type Inference for If Expressions

Some examples of if expressions and associated types are shown below. Because we have not defined many of Rosetta’s basic types, some intuition must be used to think about expression types. The negint type is the set of negative integers and is a subtype of integer. The bit type is the set {0,1} and is a subtype of natural, which is in turn a subtype of integer. The imaginary and real types are both subtypes of complex. These types will be defined later; for now, intuition should be sufficient to understand these typing examples.

In the expression:

if true then -1 else 1 end if :: integer

the condition is boolean and the clauses are both elements of integer. Even though the condition is always true and the expression reduces to -1, the inferred type is integer, not negint. Evaluation of the conditional expression is not and cannot be performed during type inference. If a static analysis tool can make this determinations, then a type assertion can be added to inform the type checking system of this fact.

In the following expression, where j is the imaginary root:

if x<1 then j else -1 end if :: complex

the condition is boolean, one expression is imaginary, and the other is negint. The common type for these types is complex and thus the expression type is complex.

In the expression:

if x=y then j else -j end if :: imaginary

the condition is boolean and both expressions are imaginary, so the expression type is imaginary.

In the expression:

if x>0 then 1
  elseif x=0 then 0
  else -1
end if :: integer

the condition is boolean and the expressions are typed bit, bit, and negint, respectively. The common type is integer, thusthe if expression is type integer.

Finally, in the expression:

if x>0 then 1
  elseif x=0 then 'a'
  else -1
end if ::  top

the condition is boolean and the expressions are bit, character, and negint, respectively. The only common type is top, sothe if expression is type top.

Case Expressions

A case expression allows selection from among many alternatives based on set membership. The case expression is much like the if expression, but checks to determine if the result of evaluating an expression is contained in a set of items, rather than checking a boolean value. For example:

case x is
 {0,1,2} -> x+1 |
 {3} -> 0
end case;

represents a state transition function for a modulo four counter. If the value of x is in the set {0,1,2}, then the case expression evaluates to x+1. If the value of x is in the set {3}, then the form evaluates to 0. Like the if expression, the case expression evaluates to a value. Assuming that x is the current state and x’ is the next state, the following form defines a mechanism for calculating the next state in a zero to three counter:

x' = case x is
 {0,1,2} -> x+1 |
 {3} -> 0
end case;

The general syntax for a case expression is:

case e is
  s0 -> e0
  [[ | sn-> en]]*
end case;

The case expression evaluates to ek associated with the first sk containing the result of evaluating e. If e is not contained in any sk, then the case expression is bottom. No default case is provided; however, using the type top will result in a default case. Specifically, the top type is defined as the set containing all possible Rosetta values. Thus, any value of e will be an element of top. Specifically:

case x  is
 {0,1,2} -> x+1 |
 {3} -> 0 |
 top -> bottom
end case;

will evaluate to bottom if x is not contained in either the set {0,1,2} or the set {3}. The value bottom is an element of all sets and will commonly be used to represent an error or default case when choices are specified. This case specifies the same behavior as leaving the top case off. An alternate approach specifies a kind of reset behavior when the selection expression is out of range:

case x is
 {0,1,2} -> x+1 |
 {3} -> 0 |
 top -> 0
end case;

In this example, the expression will evaluate to 0 when the selection expression is out of range. A counter using this definition will always reset to 0 when the current state is illegal.

The previous example illustrates several common Rosetta specification techniques that will be examined throughout the remainder of this text. The terms are declarative constructs that must be simultaneously true. Thus, the output value, the next state value, and the state type are constrained concurrently, not sequentially. Both the case and if expressions have values and are not sequencing statements. Finally, the use of the decorated variable, current', denotes the value of current in the next state.

Determining the type of a case expression is identical to determining the type of an if expression. The most common of the possible result expressions is the type of the case expression. Unlike the if expression, there is no requirement on the selection expression. The presence of the top type again assures that a least common supertype is available.

Example 3.4. Case Expressions and State Machines

State-based specification is one of the most common mechanisms used for abstract system design. In state-based systems, a set of states is specified along with state transition and output functions defining next state and output respectively. The following facet defines requirements for a modulo-4 counter that outputs its state value.

facet counter(clk, reset::input bit;
           value:: output word(2))::state_based is
  current::state;
begin
  state_def: state = word(2);
  next_state: current' = if event(clk) and clk=1
     then if reset=1 then b"00"
             else case current is
               b"00" -> b"01" |
               b"01" -> b"10" |
               b"10" -> b"11" |
               b"11" -> b"00"
             end case
         end if
     else current;
     end if;
  output_ val: value = current;
end facet counter;

In this definition, the case expression defines the next state function by listing all possibilities of state and their associated values. The facet specification defines a model with two binary inputs, clk representing the clock input, and reset representing a synchronous reset operation. The variable current is the value of the current state. The heart of the specification is three terms specifying the state type, state value and output functions. These terms are labeled state_def, next_state, and output_val respectively.

The state_def term defines the state type to be word (2), a 2-bit binary word. This allows the remainder of the specification to use the state value in expressions if necessary. The output_val term defines the values of the value output by equating it with the current state. This term defines an invariant and makes the machine a Moore machine because outputs are associated with state only.

The next_state term defines how state changes using if and case expressions. The outer if determines if an event has occurred on the clock and whether the resulting value is 1. This is a common mechanism for specifying the rising edge of a clock. If this condition fails, the value of the if expression is current and the system maintains its state. The inner if checks the status of the synchronous reset signal. If it is high, the value of the if expression is b"00", the Rosetta representation for 2-bit, binary zero. If the reset value is not high, the case expression specifies the value of the if expression. The value resulting from evaluating the if expressions constrains the value of current' representing the value of current in the next state.

Example 3.5. Type Inference for Case Expressions

Assuming that x is a natural number, the type of the case statement previously defined is natural:

case x is
 0,1,2 -> x+1 |
 3 -> 0 |
 top -> 0
end case :: natural

Let Expressions

The let expression defines local items and their scopes. The expression:

let pi::real be 3.1415 in
   pi*radius*radius
end let;

evaluates to the area of a circle. The value of pi is defined to be the real value 3.1415 in the scope of the let expression.

The syntax of a let expression is:

let v0 :: T 0 be e0 [[; vn:: Tn be en]]* in
  e
end let;

The let keyword is followed by a declarative section where one or more local items may be defined. The syntax of local item declarations is identical to the syntax of a traditional item declaration except that the keyword is is replaced by the keyword be for readability. Semantically, declarations are identical. Like traditional item declarations, let declarations are defined by specifying a name and type using the type membership operator. The be clause is required and specifies a value for the local item. Within the scope of the let expressions, these items behave as traditional items. Like traditional declarations, multiple items of the same type may be specified using a comma-separated collection of item names in the declaration. Within e, any variable defined in the let declaration section is visible. For example, given area::boolean:

let pi::real be 3.14159 in
  if area then pi*r^2 else 2*pi*r end if
end let;

defines a let expression that either calculates the area or radius of a circle. The let expression declares and defines a local value, pi, that is visible in the if expression used to calculate the indicated value.

Like all Rosetta expressions, the let expression simplifies to a value and has a type. In the previous example, the let expression’s value is the if expression in the context of local variables. The let expression’s type is the type of its encapsulated expression with the types of local variables added to the expression’s context.

Let expressions may be nested in the traditional fashion. In the following specification, the variable x of type T1 has the associated expression v1, while y of type T2 has the expression v2. Both may be referenced in the expression e:

let x::T1 be v1 in
  let y::T2 be v2 in
    e
  end let
end let;

This expression may also be written as:

let x::T1 be v1; y::T2 be v2 in e end let;

Semantically, the let expression is actually a letrec expression. Languages such as Scheme and Common Lisp provide separate let and letrec constructs. A traditional let construct is not recursive. The defined variable cannot be referenced in the expression defining its value. A letrec, traditionally defined as a derived form of the traditional let construct, is recursive and allows such references.

Example 3.6. Let Expressions and Abstraction

A key use for the let expression is adding abstraction to definitions. Using the let, new declarations specific to an expression define and name concepts. In the following expression, a let expression defines quantities for use in a definition:

let newx::real be x+dx; newy::real be y+dy in
  update_position(newx,newy)
end let

Here the local variables newx and newy provide names for new values of an x and y position, respectively. Although this is a trivial use of the let expression, it demonstrates an abstraction capability that will prove more useful when recursive let applications and the fixed point operator, fix, are introduced.

The type of a let expression is the same as the type of its encapsulated expression. If the expression is well-typed, so is the associated let expression. For the previously defined let expression, the encapsulated expression is real, thus the let expression is real:

let (pi::real be 3.14159) in
  if area then pi*r^2 else 2*pi*r end if
end let:: real

Compound Expressions

Compound expressions are formed from atomic expressions, function applications, operator applications, and if, case, and let expressions by replacing formal parameters with expressions of an appropriate type. If ek and gk are legal expressions, then the following forms are examples of legal compound expressions:

e1 o e2        // Infix operator application
o e1              // Prefix operator application
(e1)              // Parenthesized expression
f(e1)             // Application of function f to e1
[inc(e1),3]       // Sequence of inc(e1) and 3
if e1             // If expression
 then e2
 else e3
end if
case e1  of       // Case expression with 3 options
 {e2}-> g2 |
 {e3}-> g3 |
 top -> gtop       // Default case
end case
let v::T be e1 in  // Let expression
e2
end let

Each of these compound expressions assumes that expressions are of a type compatible with their associated formal parameter or position in the expression. For example, the if expression mandates that e1 be boolean and e2 and e3 share a common supertype. If type conditions are met, then these represent templates for legal compound expressions. Because ek can be compound expressions, arbitrarily complex expressions can be formed through substitutions.

All Rosetta operations fall into one of several classes that define operator precedence, as illustrated in Table 3.1. Parenthesized expressions are of highest precedence followed by type assertions. Prefix, product, sum, and relational operations follow in that order. Function applications and mixfix value and type formers follow next, with equivalence having the lowest precedence. As other operations are defined, they will attempt to assume canonical positions in the precedence table. Parentheses may always be used to specify precedence explicitly rather than relying on precedence rules.

Table 3.1. Operator precedence table

Operation

Syntax

Parenthesized expression

(e)

Type assertions

e::T

Prefix operations

-e, #e, %e,...

Product operations

e * e, e / e,

 

e^e, e and e

Sum operations

e + e, e - e,

 

e or e, e => e,

 

e <= e

Relational operations

e=e, e/=e,

 

e<e, e=<e,

 

e>e, e>=e

Function applications

f (e,e,e,...e)

Value and Type formers

[e,e,e,...],

 

{e,e,e,...},

 

{*e,e,e,...*},

 

< *(v::T,v::T...)::T is e * >

Equivalence

e == e

When defined, all operation symbols are assigned to a class and remain in that class regardless of their functional definition. For example, e1 + e2 could be redefined to perform a multiplication operation. However, e1 + e2 will always have lower precedence as compared to a multiplication operation defined by e1 * e2. All binary operations are left associative unless otherwise specified.

Currently Rosetta provides no mechanism for defining precedence of new operations. If users introduce new operator syntax using domain structures, they are required to disambiguate precedence explicitly using parentheses. This is rarely an issue, as new operations are typically introduced using function definitions.

 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.135.175