|
As we saw in the last project, at the point of having to
evaluate expressions it is much nicer if the user's input
has already been transformed into an abstraction of the
underlying expression, rather than having to deal with the
concrete syntax directly.
For example, for the expression (+ (* 3 4)
2), which we saw above is represented by the
sexp value:
Atom (Symbol "+") $
( Atom (Symbol "*") $
Atom (Number (Int 3)) $
Atom (Number (Int 4)) $
Atom Nil ) $
Atom (Number (Int 2)) $
Atom Nil
the following representation is much closer to the
underlying meaning and therefore easier to manipulate:
Cappl (Operator "+",
[Cappl (Operator "*",
[Cdata (Int 3),
Cdata (Int 4)]),
Cdata (Int 2)])
Similarly, the essence of the command (defun (add1
x) (+ x 1)) is captured in the structure:
Cdefun (Operator "add1",
[Vname "x"],
Cappl (Operator "+",
[Cvar (Vname "x"),Cdata (Int 1)]))
Expressions and commands are represented after conversion
by different ML data types: cexpression and
ccommand (for "Calculator Expression" and
"Calculator Command"). Because it is necessary for the
syntax converter to return just a single type, and the user
can type either an expression or a command at the input, the
user input is always converted to a ccommand, but
one of the cases for that type is just a wrapper for an
underlying cexpression, as will be seen below.
The full abstract syntax for the language is given by the
following signature:
signature ABSYN =
sig
datatype vname = Vname of string;
datatype operator = Operator of string
type datavalue
datatype cexpression =
Cdata of datavalue
| Cvar of vname
| Cappl of operator * cexpression list
datatype ccommand =
Cexpression of cexpression
| Cdefine of vname * cexpression
| Cdefun of operator * vname list * cexpression
| Cbindings
| Cexit
val datavalueToString : datavalue -> string
val cexpressionToString : cexpression -> string
end;
Notice that the abstract syntax stops at the point of
specifying the datavalues. We will use different
datavalue definitions as our language grows and
gets richer over the next few projects. For now we will just
be using numbers, which you can assume are defined by the
data type
datatype number = Int of int
| Real of real
The number type is defined as part of a
signature called CONCSYN, which provides other bits
you don't need to worry about in this phase. Your functors
will, however, need to take a structure of signature
CONCSYN in order to get the definition for the type
number.
You'll notice that the ABSYN signature fully
specifies the implementation details of many of the types.
This is necessary as we will need to be able to access
internals of the data structures in many of the routines
making use of structures of this signature. The alternative
would have been to leave the type less specified and instead
to specify a large set of accessor and constructor
functions, but that would have been fairly cumbersome.
This signature has been implemented by the following
functor:
functor AbstractSyntaxFun (structure ConcreteSyntax : CONCSYN) =
struct
type datavalue = ConcreteSyntax.number;
datatype vname = Vname of string;
datatype operator = Operator of string
datatype cexpression =
Cdata of datavalue
| Cvar of vname
| Cappl of operator * cexpression list
datatype ccommand =
Cexpression of cexpression
| Cdefine of vname * cexpression
| Cdefun of operator * vname list * cexpression
| Cbindings
| Cexit
val datavalueToString = ConcreteSyntax.numberToString;
fun cexpressionToString (Cdata data) = datavalueToString data
| cexpressionToString (Cvar (Vname name)) = name
| cexpressionToString (Cappl (Operator opr, cexps)) =
"(" ^ opr ^ (cexpListToString cexps) ^ ")"
and cexpListToString nil = ""
| cexpListToString (ce::ces) =
" " ^ cexpressionToString ce ^ cexpListToString ces
end;
Note that in order to get access to the
number datatype defined in the concrete syntax
implementation, the concrete syntax structure is taken as an
argument to this functor.
|