Easy-C is language that has similar expressive power to the popular ANSI-C language while avoiding many of the issues that make ANSI-C difficult and/or tedious to code in. Easy-C is a strongly typed object oriented language.
{Disclaimer: I have to do another sweep through the compiler adding a few minor features before the code in this tutorial will actually compile. -Wayne}
The simplest program is contained in
Hello.ezc
and when executed prints
out the string "Hello, World!" and exists.
The Easy-C program that does this is shown below:
easy_c 1.0
library Easy_C
routine main@Easy_C
takes arguments Array[String]
returns Integer
call p@("Hello, World!\n\")
return 0i
Hello.ezc
is compiled and executed by
the following commands:
prompt> ezc Hello prompt> ./Hello Hello, World! prompt>Please give it a try before reading the code explanation below.
The first line is:
This line specifies that the program is an Easy-C
program at the 1.0 version of the language. The
version number incremented whenever changes are
made to the Easy-C language. Upward compatible
changes increment the fractional part (e.g. 1.1,
1.2, etc.) and incompatible changes increment the
integer part (2.0, 3.0, etc.) This line allows
the compiler to easily figure out if it can compile
the code that follows. This little statement avoids
the chaos that ensues when incompatible language
changes occur
easy_c 1.0
Note that in Easy-C each declaration and statement occurs on its own line. There are no nasty semi-colons (';') to sprinkle through out the code. The reason for this is to simplify error recovery. If the compiler can not figure what a line means, it will display an error message and continue to the next line. This is in sharp contrast to other programming languages where a single missing semi-colon can cause multiple pages of obscure error messages. That form of compiler ill behavior is a thing of the past with Easy-C.
The next declaration is:
This specifies that a library of types and routines
is to be made available for the program. The
library Easy_C
Easy_C
library defines all of the basic
types such as Logical
, Unsigned
,
Integer
, etc. Pretty much every program
has this declaration, since most programs use the
basic types.
This next few lines specify the name, arguments,
and return values for a procedure:
The name of the routine is
routine main@Easy_C
takes arguments Array[String]
returns Unsigned
main@Easy_C
.
In Easy-C, every routine is associated with a type.
The routine name is main
and the
type is Easy_C
. This routine takes
a single argument called arguments
of type Array[String]
. The
Hello.ezc
program does not use
arguments
. The routine returns a
single value of type Unsigned
.
This is the first instance of using indentation in Easy-C. All routine declarations, statements and comments are indented. By convention, the lines are indented by 4 spaces, but the compiler will accept any consistent indentation amount (i.e. all 3 spaces, 4 spaces, 5 spaces, etc.) The reason for using indentation is to improve compiler error recovery when something goes wrong. If there are errors inside of the code for a routine, the compiler will gracefully recover when it encounters the next routine declaration. In other compilers, a missing open or close brace can cause pages of error messages. This bogus compiler behavior is a thing of the past using Easy-C.
The next line prints out "Hello, World!":
The word
call p@("Hello, World!\n\")
call
is followed by
an expression that is evaluated. Any return
values are ignored. In this particular case,
a routine called p@String
is
invoked. The routine name is p
and the type name is String
.
The p@String
routine takes a
single argument of type String
and returns nothing. This routine prints
its string argument out on the command console.
This routine call could have alternatively
been written as:
The rule in Easy-C is that type name between
the
call p@String("Hello, World!\n\")
@
and (
can be
dropped, if the type of the routine first
argument is the same. Since the type of
"Hello, World!\n\"
is type
String
, there is no need use this
longer and wordier alternative.
A string constant is enclosed in double quotes
("...".) Each character in the string is printed
out on the console. Alternate character encoding
is done by enclosing the alternate character
encoding between two backslash characters (\...\.)
Each symbol between the backslashes corresponds to
a single character. Commonly used characters such
as new-line (line-feed) or horizontal tab are encoded
as "\n\"
and "\t\"
respectively. More than one character can be encoded
by simply separating the symbols by commas. For
example, three tabs in a row are represented as
"\t,t,t\"
. For the string,
"Hello, World!\n\"
we are terminating
the string constant with a new-line character.
The last line in the routine is:
This routine will cause the program to terminate with
a status code of 0. The "i" afterward makes the 0 have
a type of
return 0i
Integer
rather than a type of
Unsigned
. A status code of 0 means "The
program is done now and everything was acceptable",
whereas a non-zero status code means "something bad
happened, but program is all done anyhow." Program
exit status codes are used by people who write so
called shell programs. Easy-C is not a shell programming
language, so no further discussion is shell programming
is required here.
So, here is the Hello.ezc
program all
over again:
Hopefully, after the explanation above, it mostly
makes sense now.
easy_c 1.0
library Easy_C
routine main@Easy_C
takes arguments Array[String]
returns Integer
call p@("Hello, World!\n\")
return 0i
Printing string constants is all well and good, but sometimes we want to use the computer, to well..., compute things. For that, we need a way to print results such as numbers, strings, an the like. The Easy-C formatting system is designed to allow for easy formatting of numbers, string, and such. In addition, the Easy-C formatting system also introduces some additional language features.
The Format.ezc
program is shown below.
Please do not dwell on it too long, it needs some
pretty thorough discussion before it will make much
sense:
As usual, please compile it and run it as follows:
easy_c 1.0
library Easy_C
routine main@Easy_C
takes arguments Array[String]
returns Integer
call p@(form@("Decimal:%d% Hexadecimal:%x%\n\") %
f@(1234) / f@(4321))
call p@(form@("Raw String:%s% Visual_String:%v%\n\") %
f@("Hello") / f@("Hello, World!\n\"))
return 0i
prompt> ezc Format prompt> ./Format Decimal:1234 Hexadecimal:0x10e1 Raw String:Hello Visual String:"Hello, World!\n\" prompt>Now that you have actually run the program, it is now time to figure out what is actually going on. Please, continue reading.
A basic formatting expression is organized as
follows:
There are three basic phases to formatting:
form@("...") % f@(...) % f@(...) % ... / f@(...)
\ / \ / \ / \ /
----1---- ---2--- ---2--- ---3---
form@("...")
, where
"..."
is the template string.
% f@(...)
" fills in one more
format field in the template string.
/ f@(...)
"
fills in the last format field and causes the
final formatted string to be returned.
So what is the percent ('%') and forward slash ('/')
stuff all about? As in many other programming
languages, the percent ('%') is the remainder operator
and the forward slash ('/') is the divide operator.
For numbers, remainder and divide are pretty well defined
concepts. For example,
9 % 4 = 1
and
9 / 4 = 2
. For floating
point numbers 9.9 % 4.4 = 1.1
and 9.9 / 4.4 = 2.25
.
But what does division and remainder mean when applied to
strings? The short answer is string division and remainder
does not make any sense at all. The longer answer
involves understanding a concept called operator overloading.
In older programming languages, the binary and unary arithmetic operators could only be used for built-in types. Over time, the concept of allowing these operators to be used for user defined types occurred. The ability use arithmetic operators on user defined types is called "operator overloading".
So how does operator overloading work? For operator
overloading, the compiler substitutes a routine call
for the operator. The rule is:
is replaced by:
left / right
For example, the expression:
divide@{typeof left}(left, right)
is replaced by:
9 / 4
Similarly, the expression:
divide@Unsigned(9, 4)
is replaced by:
9.9 / 4.4
And finally, when it comes to strings:
remainder@Double(9.9, 4.4)
is replaced by:
"left" % "right"
Hopefully, operator overloading is starting to make
sense.
remainder@String("left", "right")
In Easy-C, the string remainder and divide operators have been allocated to the string formatting subsystem. The reason for this allocation is because:
So, what happens internally to the compiler is that:
gets turned into:
form@("...") % f@(a) / f@(b)
or slightly more compactly:
divide@String(remainder@String(form@("..."), f@(a)), f@(b))
which is a little easier to read, but not nearly as
easy to read as the first line.
divide@(remainder@(form@("..."), f@(a)), f@(b))
Now it is time to talk about the
form@String()
routine. When this
routine is called, it starts the formatting system.
Its only argument is of type String
and
contains the overall formatting string template.
The string format template is a string that
one or more format fields embedded in it.
A format field consists of one or more characters
(usually lower case letters ) bracketed by a pair
of percent characters ('%'). For example,
"...%d%..."
is one and
"...%10Rd%..."
is another. Just
as an aside, "...%%..."
is converted
into a single percent character on output and is
not counted as a formatting field because there
is no character between the percent characters.
The form@String()
routine just happens
to return the string that was passed in as an
argument without any modifications.
Every basic type in Easy-C has a routine of
the form f@
basic_type.
This routine takes a basic_type
as its only argument, and returns a formatted
string as its return value. Each time
f@
basic_type is called,
it will fetch the next format field from the
formatting system and use that to control overall
formatting. For example, the f@Unsigned
routine will treat "...%d%..."
as a directive to format its argument as
a decimal number. Similarly,
"...%x%..."
causes the number to
be formatted as a hexadecimal number.
For example,
replaces the format field of
f@Unsigned(1234)
"...%d%...
with "...1234..."
. Similarly,
replaces the format field of
f@Unsigned(4321)
"...%x%..."
with "...0x10e1..."
.
The amounts and kinds of formatting are defined
for each f@
type routine.
For the f@Unsigned
routine, decimal
and hexadecimal formatting are available. For
f@String
, there are three supported
formats:
"...%s%..."
- Do no formatting at all, just copy each character into the format field. This is a raw format.
"...%v%..."
- This is a visual format where the string is enclosed in double quotes and uses the same formatting rules that Easy-C uses.
"...%a%..."
- This is an ANSI-C format that causes the string to look like an ANSI-C string.
One of the key facets of the Easy-C formatting
system is that it allows people to define and
implement their own f@
user_type
routines to be used in conjunction with the
Easy-C formatting system. This capability is not
discussed here, but it is an important consideration
of the overall Easy-C formatting system design.
The last topic that needs some discussion, is the topic of continuation lines. As in all programming languages, it is possible to have statements and/or declarations that do not conveniently fit on one line. The solution is to have a mechanism for letting the statement/declaration span multiple lines. In Easy-C, the way a statement/expression is continued onto the next line is to:
The following statement from program at the beginning
of this section spans two lines via continuation:
The first line ends in a percent ('%') and the next
line finishes the statement. Note that the second line
is indented from the first line by two additional spaces.
This line could have been entered as:
call p@(form@("Decimal:%d% Hexadecimal:%x%\n\") %
f@(1234) / f@(4321))
or spanning three lines as:
call p@(form@("Decimal:%d% Hexadecimal:%x%\n\") % f@(1234) / f@(1234))
All three are all equivalent.
call p@(form@(
"Decimal:%d% Hexadecimal:%x%\n\") %
f@(1234) / f@(4321))
After all that discussion, we can finally understand
what is going on with the program presented at the
beginning of this section. The two statements:
invoke the formatting system twice. The first statement
prints two numbers (the first in decimal and the second
in hexadecimal). The second statement prints two
strings (the first in raw mode and the second in
visual mode.)
call p@(form@("Decimal:%d% Hexadecimal:%x%\n\") %
f@(1234) / f@(4321))
call p@(form@("Raw String:%s% Visual_String:%v%\n\") %
f@("Hello") / f@("Hello, World!\n\"))
That pretty much wraps up the initial foray into the Easy-C formatting system. With this initial understanding it is now possible to write programs that compute things and print out the results. All-in-all, a pretty handy thing to be able to do.
The next program is Simple_Loop.ezc
and is listed below:
easy_c 1.0
library Easy_C
routine main@Easy_C
takes arguments Array[String]
returns Unsigned
index :@= 0
call p@("index\t\square\n\")
while index <= 10
square :@= index * index
call p@(form@("%d%\t\%d%\n\") % f@(index) / f@(square))
index := index + 1
return 0
Simple_Loop.ezc
is compiled and
executed as follows:
prompt> ezc Simple_Loop prompt> ./Simple_Loop index square 0 0 1 1 2 4 3 9 4 16 5 25 6 36 7 49 8 64 9 81 10 100 prompt>This pretty simple program just prints an index and its associated squares.
There are exactly two ways to specify local variables for a routine in Easy-C:
takes
clause.
takes
clause
and two define assign operators.
The following takes
clause:
defines a variable named
takes arguments Array[String]
arguments
of type Array[String]
. This is
a routine argument that is passed in when the
main@Easy_C
routine is called.
There is one takes
clause for
each routine argument. If a routine takes
no arguments at all, takes_nothing
is required. Thus a routine declaration, must
either specify takes_nothing
or one or more takes
clauses.
The other way to define a local variable is using
the define assign operator :@='
.
The define assign operator is used as follows:
variable :@=
expression
where variable is a new local variable
and expression is an expression to be
evaluated. The type of variable is defined
to be {typeof expression}. For example,
sum :@= 2 + 2
defines a new variable called sum
of
type Unsigned
. The type is
Unsigned
because the value of the
expression 2 + 2
is
of type Unsigned
.
As another example,
hello :@= "Hello"
This expression creates a new variable
hello
of type String
and whose value is "Hello"
.
Once a variable is defined it remains in scope
(i.e. accessible) for the remainder of the code
at the same indentation level. The language
does not allow a variable to be defined in the
same scope more than once. Thus,
has an error on the second line, since the first
line has already defined the
sum :@= 0
sum :@= sum + 1 #Error
sum
variable.
The way that parsing works in Easy-C, is that each
statement and declaration starts with key word
like routine
, call
, etc.
The only exception to this rule is the two assignment
operators ':=" and ":@=". What happens is that the
compiler scans each line looking for ':=" or ":@=".
If it finds either one, it prepends an invisible
{assign}
to the beginning of the line.
Thus,
looks like:
sum :@= 1 + 2
to the compiler.
{assign} sum :@= 1 + 2
The prescan technology in conjunction with every
other statement and declaration starting with a
keyword has an interesting characteristic of
allowing Easy-C to completely dispense with reserved
words. In other languages, a reserved word is
one that can only be used as a statement/declaration
keyword and not as a type name or variable name.
In Easy-C, there are no reserved words, so the
following code is perfectly legal:
because the compiler actually sees:
routine :@= 1
takes :@= 2
call :@= 3
Please note, reusing keywords can make the code
harder read, so you should probably avoid reusing
a keyword. However, if you do manage to use a
keyword as a variable, no harm is done.
{assign} routine :@= 1
{assign} takes :@= 2
{assign} call :@= 3
Enough about variables, prescanning, and keywords.
Now it is time to introduce the while
statement. The while
statement has
the following basic form:
where
while logical_expression
indented_statements
logical_expression
is
an expression that returns a value of type
Logical
. The only two values of
type Logical
are true
and false
. The while
statement evaluates
logical_expression
and if
it returns a value of true
, the
indented statements
are executed. After
indented statements
are executed, logical_expression
is retested and the loop repeats. The first time
logical_expression
returns false
the while
statement terminates and execution moves onto the
statement that follows the while
statement.
In the program above, the while
statement is:
This code repeatably tests the variable
while index <= 10
square :@= index * index
call p@(form@("%d%\t\%d%\n\") % f@(index) / f@(square))
index := index + 1
index
to see if it is less than
or equal to 10. The '<=
' operator
stands for "less than or equal". As long as
index
is less than or equal to 10,
the loop body is executed. In the loop body,
the variable square
is defined and
assigned a value of the square of index
.
Both index
and square
are printed out using the string formatting system
in the next line. Lastly, the variable index
in incremented by one and the loop continues.
That pretty much covers simple while
loops and local variable creation.
The delcaration:
loads the compiler with the Easy-C basic types --
library Easy_C
Integer
, Unsigned
,
Float
, Double
,
Character
, and Logical
.
In addition, this library provides three somewhat
more substantial types -- String
,
Array
, and Hash_Table
.
This section discusses the String
and Array
types.
Both the Array
and String
types implement variable length data structures.
A String
is a sequence of
Character
's. The Array
is more general purpose and implements a variable
length sequence of objects of the same type.
Thus, Array[Unsigned]
implements a
sequence of Unsigned
numbers,
and Array[String]
implements a sequence
of String
. The String
type is discussed first, with a discussion of
A String
is a sequence of zero, one,
or more [...]
operator it is possible
to index individual character from a string:
There are a number of other routines for doing block
inserts and deletes of strings as well. An immutable
copy of a string can be made using
hello :@= "Hello"
h :@= hello[0] # h = 'h'
e :@= hello[1] # e = 'e'
l :@= hello[2] # l = 'l'
o :@= hello[4] # o = 'o'
It turns out that there two kinds of
as follows:
String
-- a mutable string and an
immutable string. A mutable string is one
whose contents can be changed and an immutable string
is one whose contents can not be changed. It turns out
that string constants are immutable. Thus, the constant
"Hello"
above can not be modified, since
it is an immutable string. The easiest way to obtain
a mutable string is by allocating a new mutable string
via the new@String
routine. This routine
will return a new empty mutable String
object as follows:
# text = ""
Another string can be appended to
text := new@String()
text
as follows:
Since
call string_append@(text, "Hello") # Append String
# text = "Hello"
text
is mutable, characters can be
inserted, deleted, and changed in place. Characters
can be trimmed of the end with trim@String
:
Individual characters can be changed via:
Individual character can changed by simple assignment:
call trim@(text, 4) # Trim to 4 characters in length
# text = "Hell"
append@String
text[0] := "Y" # Change 1st character
# text = "Yell"
text[3] := 'p' # Change 4th character
# text = "Yelp"
A single character can be inserted via:
call append@(text, '!") # Append '!' character
# text = "Yelp!"
A single character can be deleted via:
call delete@(text, 2) # Delete 3rd character
# text = "Yeper!"
call insert@(text, 3, 'r') # Insert 'r' after 4th character
# text = "Yelpr!"
call insert@(text, 3, 'e') # Insert 'e' after 4th character
# text = "Yelper!"
read_only_copy@String
:
# immutable = "Yeper!"
Conversely, a mutable string can be made using the
immutable :@= read_only_copy@(text)
read_write_copy@String
routine as follows:
# mutable = "Hello"
Lastly, size of the string is obtained via the
mutable :@= read_write_copy@("Hello")
size_get@String
routine:
It turns out that syntactically easier way of
fetching the string size is:
size :@= size_get@(text)
# text= "Yeper!" and size = 6
size := text.size
# text = "Yeper! and size = 6
There are a whole bunch of additional string routines that can be used to compare strings, convert between upper and lower case, convert them into different types, etc. These additional routines are summarized in the {to be written} Easy-C library reference.
Now it is time to switch over to discussing the
Array
type. The Array
type a special kind of type called a parameterized
type. The parameter is another type that is enclosed
in square brackets ([...]
) after the base
type name. Thus, for Array[Unsigned]
,
the base type is Array
and the parameter
type is Unsigned
. Similarly, the
parameter type for Array[String]
is
String
. Nested, parameterized types
are allowed as in Array[Array[Unsigned]]
,
where the parameter is Array[Unsigned]
.
Lastly, it is possible to have a parameterized type
with more than one parameter; for example, the
Hash_Table
type takes to parameters as
in Hash_Table[String, Unsigned]
. There
is no further discussion about Hash_Table
in this section, it is mentioned just so that you
know that types with multiple parameters are permitted.
Now that you know that Array
is a
parameterized type, what does it mean? For the
Array
type, the parameter type specifies
what type of object can be stored into the specific
Array
. So, an object of type
Array[Unsigned]
contains a sequence
of zero, one or more Unsigned
objects.
Similarly, an Array[String]
object
contains a sequence of String
objects.
So, how does a String
object differ
from an Array[Character]
object?
Other than the fact the types are not interchangeable,
there is little material difference between the two.
The Array
type supports many of the
operations as strings, such as,
insert@Array
, append@Array
,
trim@Array
, delete@Array
,
etc. In general, there more specialized operations
that are only available for String
objects, such as read_only_copy@String
.
In short, you should always use a String
instead of an Array[Character]
object,
but the compiler will not complain if you use
Array[Character]
.
Unlike String
object, the
Array
type does not support mutable
and immutable arrays. All Array
objects
are mutable at all times.
The code below shows some examples of the
Array
type in action.
That should be enough to get the basic idea behind
the
colors :@= new@Array[String]() # Allocate new Array
# colors = []
call append@Array[String](colors, "Red")
# colors = ["Red"]
call append@(colors, "Blue") # A shorter way
# colors = ["Red", "Blue"]
call insert@(colors, 1, "White") # Insert a color
# colors = ["Red", "White", "Blue"]
white :@= colors[1] # Fetch a value
# colors = ["Red", "White", "Blue"]
colors[1] := "Green" # Replace a color
# colors = ["Red", "Green", "Blue"]
call trim@(colors, 2) # Remove last color
# colors = ["Red", "Green"]
call delete@(colors, 0) # Remove first color
# colors = ["Green"]
call trim@(colors, 0) # Empty the array
# colors = []
Array
type.
To briefly summarize, both the String
and Array
represent sequences with
many similar operations between them. The
String
is optimized of a sequence of
Character
's, and the Array
is used for other sequences.
The if
statement in Easy-C is very
similar to the if
statement in many
other languages. The overall form is shown below:
Basically, each of the expressions is evaluated
in sequence until the first expression returns
if expression_1
nested_statements_1
else_if Expression_2
nested_statements_2
...
else_if Expression_N
nested_statements_N
else
nested_statements_last
true
. For the first expression that
evaluates to true
the nested statements
immediately under the expression are executed.
After the nested statements are executed, the
statement is finished and code execution resumes
immediately after the if
statement.
In the case, where none of the expressions evaluates
to true
, the nested statements under
the else
clause are executed instead.
Finally, the else_if
and else
clause are optional. The only required portion of
an if
statement is the first expression
(i.e. expression_1
) and the
nested statements immediately following.
Here are some example code sequences using the
if
statement:
and another:
# Keep {angle} between -pi and +pi:
angle :@= ...
if angle > pi
angle := angle - 2 * pi
else_if angle < -pi
angle := angle + 2 * pi
and yet another:
# Compute absolute value:
value :@= ...
if value < 0
value := -value
This is the first instance in this tutorial of the
conditional and operator ('
# Convert hexadecimal to a number between 0 and 15:
digit :@= 0
if '0' <= character && character <= '9'
digit := unsigned@(character - '0')
else_if 'A' <= character && character <= 'F'
digit := unsigned@(character - 'A') + 10
else_if 'a' <= character && character <= 'f'
digit := unsigned@(character - 'a') + 10
&&
'). It
is probably time to talk about relational operators
and conditional operators.
In Easy-C, there are three logical operators:
The first two operators are binary operators and the last operators is a unary operator. For conditional-AND and conditional OR, the left expression is always evaluated and the right expression may be evaluated depending upon the value of the first expression. This is summarized in the two tables below:
Operator Name a && b Conditional-AND a || b Conditional-OR !a Logical Not
a b a && b false unevaluated false true false false true true true
The logical not operator ('
a b a || b false false false false true true true unevaluated true
!
') just
inverts its value as follows:
a !a false true true false
In Easy-C there are 8 relational operators as summarized in the table below:
The first two columns are pretty self-explanatory. The third column is the routine that is used by the compiler to compute the result. Just like most binary operators, the compiler is actually substituting a routine call for the operator. For:
Operator Name Routine a = b Equal equal@(a, b)
a != b Not Equal !equal@(a, b)
a < b Less Than less_than@(a, b)
a <= b Less Than or Equal !greater_than@(a, b)
a > b Greater Than greater_than@(a, b)
a >= b Greater Than or Equal !less_than@(a, b)
a == b Identical identical@(a, b)
a !== b Not Identical !identical@(a, b)
a = b
the compiler substitutes:
equal@{typeof a}(a, b)
and similarly for:
a != b
The first 6 relational operators do not need any real
discussion, since they are the common relational
operators for comparing numbers. The last two are
kind of unique to Easy-C and are discussed below in
the section on objects.
The precedence of the operators is such that the
expression below:
is grouped as follows:
if '0' <= character && character <= '9'
if ('0' <= character) && (character <= '9')
The table below expresses the precedence of operators in Easy-C:
The operators that are preceded by a letter need a little more discussion. The 't[' and 't]' refer to when a square brackets are used for type parameters. Conversely, 'i[' and 'i]', refer to when square brackets are used as an indexing operator (e.g.
Prec. Operators Assoc. Routines 14 t[ t] left 13 ( @( ) i[ i] @ . left fetch_#(), store_#(), field_set(), field_get() 12 u- u! u+ u~ right negate(), not() 11 * / % left multiply(), divide(), remainder() 10 + - left add, minus 9 << >> left left_shift(), right_shift() 8 & left and() 7 ^ left xor() 6 | left or() 5 < > <= >= != = == !== left equal(), less_than(), greater_than(), identical() 4 && left 3 || left 2 , left 1 := :@= left
Array
fetch and
store.) Finally, the 'u-', 'u!', 'u+', and 'u~' refer
to unary operators. Unary operators are the only ones
that group right to left in their associativity (e.g. ---a
is the same a -(-(-a)).) All other operators are left
to right associativity.
For those of you that are familiar with the operator precedence of ANSI-C, you should be warned that there are a few differences between Easy-C and ANSI-C precedence. In particular, the relational operators in Easy-C have a lower precedence than in ANSI-C. In addition, the precedence of comma and assignment are swapped.
In ANSI-C, the evaluation order for routine arguments undefined. In Easy-C, left to right evaluation is strictly enforced.
So far we have been using types that have been
predefined in the Easy_C
library.
In this section, we start defining our own types.
We start with the simplest type, an enumerated
type. The code below defines a new type called
Color
:
This new type has define 3 global symbols:
define Color
enumeration
red
green
blue
A color variable can exactly one of those three values.
For example,
red@Color
green@Color
blue@Color
color1 :@= red@Color
color2 :@= blue@Color
The only comparison operator that is predefined for
an enumerated type is the identical operator
('
==
'.) The identical operator returns
true if two objects are indistinguishable from one
another. Thus, the following piece of code works
with our new Color
type:
The piece of code above is sufficiently common that
there is a statement, called
if color == red@Color
call p@("Red\n\")
else_if color == green@Color
call p@("Green\n\")
else_if color == blue@Color
call p@("Blue\n\")
switch
statement, that basically implements the code above.
For example, the code above can be replaced with:
switch color
case red
call p@("Red\n\")
case green
call p@("Green\n\")
case blue
call p@("Blue\n\")
It is allowed to put multiple enumeration names on
the same case
clause separated by commas:
switch color case red, green call p@("Red or green\n\") case blue call p@("Blue\n\")A
default
can deal with all cases that
are not explicitly named:
switch color
case red
call p@("Red\n\")
default
call p@("Other\n\")
Lastly, there is all_cases_required
clause.
This clause instructs the compiler that you intend to
explicitly name all possible enumeration values as
case clauses in a switch statement:
switch color
all_cases_required
case red, green
call p@("Red or green\n\")
case blue
call p@("Blue\n\")
If either red
, green
, or
blue
were left out of the switch statement
the compiler would complain. More importantly, if you
add another color to the original type definition, the
compiler will also complain.
The next type that can be defined is called the
record type. A example record type is defined as
follows:
This defines a new type named
define Point2
record
x Double
y Double
Point2
with two fields -- x
and y
.
The type of both fields is Double
.
In addition, the define declaration above defines
a new routine called new@Point2
which
is invoked to create each new Point2
object. Lastly, the declaration above also causes
the an initial instance of Point2
to
be assigned to a global constant named
null@Point2
.
The code below shows some basic operations on
Point2
objects:
The first line:
point :@= new@Point2()
point.x := 1.0
point.y := 1.0
x :@= point.x
y :@= point.y
radius :@= square_root@(x * x + y * y)
angle :@= arc_tangent2@(x, y)
allocates a new
point :@= new@Point2()
Point2
object by
invoking new@Point2()
. The new
Point2
object is stored in the
new local variable point
. The
next two lines:
assign initial values to the
point.x := 1.0
point.y := 1.0
x
and
y
fields of newly created
Point2
object. The next two lines
fetch the values out of the newly created
Point2
object:
The values are stored in local variables named
x :@= point.x
y :@= point.y
x
and y
. Lastly, the
polar representation is computed via the last
two lines:
These two lines invoke the
radius :@= square_root@(x * x + y * y)
angle :@= arc_tangent2@(x, y)
square_root@Double
and
arc_tangent2@Double
routines to
compute a radius and angle for polar coordinates.
In addition to the actual field names defined for
the record, it is possible to create pseudo field
names. This is done by defining get and/or set
routines. For example, assume that we want to be
able to access polar coordinates for a
Point2
object. The following two
routines provide "get" routines for the values:
With these two routines, it is possible to write the
following code:
routine radius_get@Point2
takes point Point2
returns Double
# This routine returns the Polar coordinate radius.
x :@= point.x
y :@= point.y
return square_root@(x * x + y * y)
routine angle_get@Point2
takes point Point2
returns Double
# This routine returns the Polar coordinate angle.
return arc_tangent2@(point.x, point.y)
However, the compile is trained to recognize routines
that end in "_get" as special. If the compile sees
syntax of the form
point :@= new@Point2()
# ...
angle :@= angle_get@(point)
radius :@= radius_get@(point)
expression.name
, it
first checks to see of name
is a valid field name. If not, it looks for a
routine of the form
name_get@{typeof expression}
.
If it find the routine. Thus, the code:
is converted into:
angle :@= point.angle
radius :@= point.radius
Used sparingly, "get" (and "set") routines can really
improve the legibility of code. In general, when you
write a "get" routine, it should be "idempotent". What
this means is that if you call it twice in a row, it
will return the same value both times.
angle :@= angle_get@(point)
radius :@= radius_get@(point)
It is possible to write "set" routines the same way.
The "set" routines for polar coordinates are not nearly
as easy to understand though:
and the following code:
routine angle_set@Point2
takes point Point2
takes angle Double
returns_nothing
# This routine will set the angle of {point} to {angle}.
x :@= point.x
y :@= point.y
radius :@= square_root@(x * x + y * y)
point.x := radius * cosine@(angle)
point.y := radius * sine@(angle)
routine radius_set@Point2
takes point Point2
takes radius Double
returns_nothing
# This routine will set the radius of {point} to {radius}.
angle :@= arc_tangent2@(point.x, point.y)
point.x := radius * cosine@(angle)
point.y := radius * sine@(angle)
is translated into the following code by the compiler:
point.radius := 2.0
point.angle := 0.0
That pretty much covers basic record types.
call radius_set@(point, 2.0)
call angle_set@(point, 0.0)
The last major type is the variant type. A variant
type allows an object to point to objects of different
types. The following type declaration:
integer Integer
unsigned Unsigned
actually defines two types --
define Number
variant kind Number_Kind
double Double
Number
and Number_Kind
. The
Number_Kind
type is an enumeration type
that is equivalent to:
The main type is
define Number_Kind
enumeration
double
integer
unsigned
Number
and it can
contain either a Double
,
Integer
or Unsigned
,
but only one at a time. As with record types,
the declaration above defines the
new@Number
routine and a global
constant called null@Number
.
The print@Number
routine below shows
some code for manipulating a number object:
In the code above, the
routine print@Number
takes number Number
returns_nothing
# This routine will print out the contents of {number}.
switch number.kind
case double
call p@(form@("double:%f%\n\") / f@(number.double))
case integer
call p@(form@("integer:%d%\n\") / f@(number.integer))
case unsigned
call p@(form@("unsigned:%f%\n\") / f@(number.unsigned))
switch
statement accesses the kind
field
of the number
object. The
kind
field contains either the value
double@Number_Kind
,
integer@Number_Kind
, or
unsigned@Number_Kind
, depending
upon whether Number
object
pointed to by number
is
Double
, Integer
,
or Unsigned
type respectively.
Once the switch statement has dispatched to the
correct case clause, the actual variant value is
fetched using the dot ('.
') operator.
In the case of case double
,
returns the double value stored
number.double
number
.
Values are stored into a variant in a similar fashion.
Consider the code below:
In the first two lines: the new variable
number1 :@= new@Number()
number1.double := 3.1415926
number2 :@= new@Number()
number2.unsigned := 17
number1
is assigned a newly
created Number
object:
It is then forced to contain a
number1 :@= new@Number()
Double
by:
The last two lines do the same thing for
number1.double := 3.1415926
number2
, but stuff an
Unsigned
type into it.
What happens if the following chunk of code is
executed:
In this case, when it is attempt to fetch an
unsigned value from
switch number.kind
case double
d :@= number.unsigned # Error
number
when it
actually contains a Double
object,
a fatal run time error occurs.
It turns out that variant types and record types
can be combined in a single define
declaration. This is shown in the next section
on parameterized types.
Easy-C permits routines to be treated like objects.
Two routines are of the same type if they have the
same number of arguments with the same types, and
returns the same number of arguments with the same
types. Thus, the following to routines have the
same type:
They can be assigned to variables just as you would
expect:
routine add@Unsigned
takes left Unsigned
takes right Unsigned
returns Unsigned
return left + right
routine multiply@Unsigned
takes left Unsigned
takes right Unsigned
returns Unsigned
return left * right
after they are assigned to a variable, they can be
invoked like a routine:
operate :@= add@Unsigned
operate := multiply@Unsigned
They can also be passed as arguments to a routine.
A routine argument needs both a name and a routine
type. They syntax for a routine type is:
result :@= operate(2, 2)
Thus, the type for the two routines above is:
[{return type} <= {argument 1}, ..., {argument N}]
An example of a routine that takes a routine variable
is:
[Unsigned <= Unsigned, Unsigned]
If the routine does not return anything, there is
nothing between the open square bracket ('[') and
the '<=". For example,
routine compute@Easy_C
takes left Unsigned
takes right Unsigned
takes operator [Unsigned <= Unsigned, Unsigned]
returns Unsigned
return operator(left, right)
Similarly, if there are no routine arguments, there
is nothing between the '<=" and the ']':
[ <= Unsigned, Unsigned]
Routine variables are particularly useful for
some parameterized types and routines.
[ Unsigned <= ]
Easy-C permits parameterized types. A parameterized
type is a powerful form of code reuse. The first
instance of parameterized type is the Array
several sections earlier. It turns out that you can
define your own parameterized type as well. In this
section, we declare and define a new parameterized
type call Tree
.
The Tree
is actually two types, a top
level object that is the tree root and a lower level
object that contains tree elements. The two declarations
are listed immediately below:
Both the
define Compare
record
less_than
equal
greater_than
define Tree[Key, Value]
record
element_empty Tree_Bind[Key, Value] # Empty Element
key_compare [Compare <= Key, Key] # Key compare routine
key_empty Key # Empty Key
root Tree_Bind[Key, Value] # Root element of the tree
size Unsigned # Number of tree elements
value_empty Value # Empty Value
define Tree_Bind[Key, Value]
record
key Key # Key
value Value # Value
left Tree_Bind[Key, Value] # Left branch
right Tree_Bind[Key, Value] # Right branch
Tree
and the Tree_Bind
types are parameterized with the same type place holder
parameter types Key
and Value
.
In a parameterized type definition, each parameter type
must be a simple type name. Trying to define a type
like:
will not work. The define record (or variant)
can have as many fields as needed. The scope of
the parameter types is until the declaration,
after which that parameter type is no longer
recognized as a legal type. Thus, for the
duration of the type declaration above,
define Bad_Type[Array[Key], Array[Value]] # Bad Type
Key
and Value
are
treated like any other type such as
Unsigned
, Integer
,
or Array
. Lastly, the parameter types
can not be further parameterized themselves as
in:
Now that we have the types defined, it is time to
define a few routines to manipulate the types:
record
illegal1 Key[Unsigned] # Bad type
illagal2 Array[Key[Unsigned]] # Bad type
First, this routine creates an
routine create@Tree[Key, Value]
takes key_compare [Compare <= Key, Key]
takes key_empty Key
takes value_empty Value
returns Tree[Key, Value]
# Create and return a new {Tree} object.
# Create an empty {Tree_Element} object:
element_empty :@= new@Tree_Element[Key, Value]()
element_empty.key := key_empty
element_empty.value := value_empty
element_empty.left := element_empty
element_empty.right := element_empty
# Now create the {Tree} object:
tree :@= new@Tree[Key, Value]()
tree.key_compare := key_compare
tree.key_empty := key_empty
tree.value_empty := value_empty
tree.element_empty := element_empty
tree.size := 0
tree.root := element_empty
return tree
Tree_Element
object and stores that into empty_element
object. Note that the left
and
right
fields are initialized to
empty_element
. Second, the
Tree
object is created, initialized and
returned.
The insert@Tree
is shown below:
This routine searches
routine insert@Tree[Key, Value]
takes tree Tree[Key, Value]
takes key Key
takes value Value
returns Logical
# This routine will insert {value} into {tree} with
# with a key of {key}. {true} is returned if key is
# already in {tree}.
empty_element :@= tree.empty_element
key_compare :@= tree.key_compare
# Search through the tree:
previous :@= empty_element
current :@= tree.root
compare :@= equal@Compare
while current != empty_element
compare := key_compare(key, current.key)
switch compare
case less_than
previous := current
current := current.left
case greater_than
previous := current
current := current.right
case equal
current.value := value
return true@Logical
# Create the {key}/{value} binding:
current := new@Tree_Element[Key, Value]()
current.key := key
current.value := value
current.left := empty_element
current.right := empty_element
# Splice {current} into the binding:
switch compare
case equal
tree.root := current
case less_than
previous.left := current
case greater_than
previous.right := current
return false@Logical
tree
starting at
tree.root
looking for a
Tree_Element
object that matches
key
. The key_compare
routine is used to compare routines. When
key_compare
returns
less_than@Compare
, the left branch of
the tree is traversed; otherwise the right branch
is traversed.
The lookup@Tree
routine is shown below:
This routine simple traverses the tree until
a
routine lookup@Tree[Key, Value]
takes tree tree[Key, Value]
takes key Key
returns Value
# This routine will return the value that is bound to
# {key} in {tree}.
key_compare :@= tree.key_compare
empty_element :@= tree.empty_element
current :@= tree.root
while current != empty_element
switch key_compare@(key, current.key)
case equal
return current.value
case less_than
current := current.left
case greater_than
current := current.right
return tree.value_empty
Tree_Element
object that matches
key
is found (or not.)