Easy-C Tutorial

Table of Contents

  1. Download_and_Install
  2. Hello World!
  3. Formatting
  4. Variables and Simple Loops
  5. Strings and Arrays
  6. If_Statements_and_Relational_Expressions
  7. Expressions
  8. Enumerated Types
  9. Record Types
  10. Variant Types
  11. Routine Variables
  12. Parameterized Types

1. Introduction

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}

2. Hello World!

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:

    easy_c 1.0							
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

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:

    library Easy_C						
This specifies that a library of types and routines is to be made available for the program. The 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:

    routine main@Easy_C
	takes arguments Array[String]
	returns Unsigned					
The name of the routine is 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!":

    call p@("Hello, World!\n\")					
The word 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:

    call p@String("Hello, World!\n\")				
The rule in Easy-C is that type name between the @ 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:

    return 0i							
This routine will cause the program to terminate with a status code of 0. The "i" afterward makes the 0 have a type of 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:

    easy_c 1.0	
    library Easy_C

    routine main@Easy_C
	takes arguments Array[String]
	returns Integer

	call p@("Hello, World!\n\")
	return 0i						
Hopefully, after the explanation above, it mostly makes sense now.

3. Formatting

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:

    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						
As usual, please compile it and run it as follows:
    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:

    form@("...") % f@(...) % f@(...) % ... / f@(...)

    \         /  \       / \       /       \       /
     ----1----    ---2---   ---2---         ---3---		
There are three basic phases to formatting:
  1. The format template string is specified via the call to form@("..."), where "..." is the template string.
  2. Each successive call of the form "% f@(...)" fills in one more format field in the template string.
  3. The last call of the form "/ f@(...)" fills in the last format field and causes the final formatted string to be returned.
The three phases are identified by number in the statement above.

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:

    left / right						
is replaced by:
    divide@{typeof left}(left, right)				
For example, the expression:
    9 / 4							
is replaced by:
    divide@Unsigned(9, 4)					
Similarly, the expression:
    9.9 / 4.4							
is replaced by:
    remainder@Double(9.9, 4.4)					
And finally, when it comes to strings:
    "left" % "right"						
is replaced by:
    remainder@String("left", "right")				
Hopefully, operator overloading is starting to make sense.

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 ultimately, that is the reason why you see the percent '%' and '/' characters splattered in string formatting expressions.

So, what happens internally to the compiler is that:

    form@("...") % f@(a) / f@(b)				
gets turned into:
    divide@String(remainder@String(form@("..."), f@(a)), f@(b))	
or slightly more compactly:
    divide@(remainder@(form@("..."), f@(a)), f@(b))		
which is a little easier to read, but not nearly as easy to read as the first line.

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,

    f@Unsigned(1234)						
replaces the format field of "...%d%... with "...1234...". Similarly,
    f@Unsigned(4321)						
replaces the format field of "...%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:

Following the two rules above are all that is needed to successfully span multiple lines.

The following statement from program at the beginning of this section spans two lines via continuation:

	call p@(form@("Decimal:%d% Hexadecimal:%x%\n\") %
	  f@(1234) / f@(4321))					
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@(1234))
or spanning three lines as:
	call p@(form@(
	  "Decimal:%d% Hexadecimal:%x%\n\") %
	  f@(1234) / f@(4321))					
All three are all equivalent.

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:

	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\"))			
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.)

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.

4. Variables and Simple Loops

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:

This program has one takes clause and two define assign operators.

The following takes clause:

	takes arguments Array[String]				
defines a variable named 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,

    sum :@= 0
    sum :@= sum + 1	#Error					
has an error on the second line, since the first line has already defined the 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,

    sum :@= 1 + 2						
looks like:
    {assign} sum :@= 1 + 2					
to the compiler.

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:

    routine :@= 1
    takes :@= 2
    call :@= 3							
because the compiler actually sees:
    {assign} routine :@= 1
    {assign} takes :@= 2
    {assign} 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.

Enough about variables, prescanning, and keywords. Now it is time to introduce the while statement. The while statement has the following basic form:

    while logical_expression
	indented_statements				
where 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:

	while index <= 10
	    square :@= index * index
	    call p@(form@("%d%\t\%d%\n\") % f@(index) / f@(square))
	    index := index + 1					
This code repeatably tests the variable 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.

5. Strings and Arrays

The delcaration:

    library Easy_C						
loads the compiler with the Easy-C basic types -- 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 's. The first character has an index of 0, the second an index of 1, etc. Using the [...] operator it is possible to index individual character from a string:

    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 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 := new@String()					
# text = "" Another string can be appended to text as follows:
    call string_append@(text, "Hello")	# Append String
    # text = "Hello"						
Since 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:
    call trim@(text, 4)	# Trim to 4 characters in length
    # text = "Hell"						
Individual character can changed by simple assignment:
    text[0] := "Y"	# Change 1st character
    # text = "Yell"
    text[3] := 'p'	# Change 4th character
    # text = "Yelp"						
append@String
as follows:
    call append@(text, '!")	# Append '!' character
    # text = "Yelp!"						
A single character can be inserted via:
    call insert@(text, 3, 'r')	# Insert 'r' after 4th character
    # text = "Yelpr!"
    call insert@(text, 3, 'e')	# Insert 'e' after 4th character
    # text = "Yelper!"						
A single character can be deleted via: call delete@(text, 2) # Delete 3rd character # text = "Yeper!"
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 read_only_copy@String:
    immutable :@= read_only_copy@(text)				
# immutable = "Yeper!" Conversely, a mutable string can be made using the read_write_copy@String routine as follows:
    mutable :@= read_write_copy@("Hello")			
# mutable = "Hello" Lastly, size of the string is obtained via the size_get@String routine:
    size :@= size_get@(text)
    # text= "Yeper!" and size = 6				
It turns out that syntactically easier way of fetching the string size is:
    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.

    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 = []						
That should be enough to get the basic idea behind the 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.

6. If Statements and Relational Expressions

The if statement in Easy-C is very similar to the if statement in many other languages. The overall form is shown below:

    if expression_1
	nested_statements_1
    else_if Expression_2
	nested_statements_2
    ...
    else_if Expression_N
	nested_statements_N
    else
	nested_statements_last				
Basically, each of the expressions is evaluated in sequence until the first expression returns 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:

    # Keep {angle} between -pi and +pi:
    angle :@= ...
    if angle > pi
	angle := angle - 2 * pi
    else_if angle < -pi
	angle := angle + 2 * pi					
and another:
    # Compute absolute value:
    value :@= ...
    if value < 0
	value := -value						
and yet another:
    # 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		
This is the first instance in this tutorial of the conditional and operator ('&&'). It is probably time to talk about relational operators and conditional operators.

In Easy-C, there are three logical operators:

Operator Name
a && b Conditional-AND
a || b Conditional-OR
!a Logical Not
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:
a b a && b
false unevaluated false
true false false
true true true
a b a || b
false false false
false true true
true unevaluated true
The logical not operator ('!') 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:

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)
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:
    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:

    if '0' <= character && character <= '9'			
is grouped as follows:
    if ('0' <= character) && (character <= '9')			

7. Expressions

The table below expresses the precedence of operators in Easy-C:

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
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. 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.

8. Enumerated Types

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:

    define Color
	enumeration
	    red
	    green
	    blue						
This new type has define 3 global symbols:
    red@Color
    green@Color
    blue@Color							
A color variable can exactly one of those three values. For example,
    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:
    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\")					
The piece of code above is sufficiently common that there is a statement, called 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.

9. Record Types

The next type that can be defined is called the record type. A example record type is defined as follows:

    define Point2
	record
	    x Double
	    y Double						
This defines a new type named 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:

    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)				
The first line:
    point :@= new@Point2()					
allocates a new Point2 object by invoking new@Point2(). The new Point2 object is stored in the new local variable point. The next two lines:
    point.x := 1.0
    point.y := 1.0						
assign initial values to the x and y fields of newly created Point2 object. The next two lines fetch the values out of the newly created Point2 object:
    x :@= point.x
    y :@= point.y						
The values are stored in local variables named x and y. Lastly, the polar representation is computed via the last two lines:
    radius :@= square_root@(x * x + y * y)
    angle :@= arc_tangent2@(x, y)				
These two lines invoke the 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:

    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)			
With these two routines, it is possible to write the following code:
    point :@= new@Point2()
    # ...
    angle :@= angle_get@(point)
    radius :@= radius_get@(point)				
However, the compile is trained to recognize routines that end in "_get" as special. If the compile sees syntax of the form 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:
    angle :@= point.angle
    radius :@= point.radius					
is converted into:
    angle :@= angle_get@(point)
    radius :@= radius_get@(point)				
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.

It is possible to write "set" routines the same way. The "set" routines for polar coordinates are not nearly as easy to understand though:

    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)			
and the following code:
    point.radius := 2.0
    point.angle := 0.0						
is translated into the following code by the compiler:
    call radius_set@(point, 2.0)
    call angle_set@(point, 0.0)					
That pretty much covers basic record types.

10. Variant Types

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:

    define Number
	variant kind Number_Kind
	    double Double					
integer Integer unsigned Unsigned actually defines two types -- Number and Number_Kind. The Number_Kind type is an enumeration type that is equivalent to:
    define Number_Kind
	enumeration
	    double
	    integer
	    unsigned						
The main type is 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:
    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))
In the code above, the 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,
    number.double						
returns the double value stored number.

Values are stored into a variant in a similar fashion. Consider the code below:

    number1 :@= new@Number()
    number1.double := 3.1415926
    number2 :@= new@Number()
    number2.unsigned := 17					
In the first two lines: the new variable number1 is assigned a newly created Number object:
    number1 :@= new@Number()					
It is then forced to contain a Double by:
    number1.double := 3.1415926					
The last two lines do the same thing for number2, but stuff an Unsigned type into it.

What happens if the following chunk of code is executed:

    switch number.kind
      case double
	d :@= number.unsigned	# Error				
In this case, when it is attempt to fetch an unsigned value from 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.

11. Routine Variables

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:

    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					
They can be assigned to variables just as you would expect:
    operate :@= add@Unsigned
    operate := multiply@Unsigned				
after they are assigned to a variable, they can be invoked like a routine:
    result :@= operate(2, 2)					
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:
    [{return type} <= {argument 1}, ..., {argument N}]		
Thus, the type for the two routines above is:
    [Unsigned <= Unsigned, Unsigned]				
An example of a routine that takes a routine variable is:
    routine compute@Easy_C
	takes left Unsigned
	takes right Unsigned
	takes operator [Unsigned <= Unsigned, Unsigned]
	returns Unsigned

	return operator(left, right)				
If the routine does not return anything, there is nothing between the open square bracket ('[') and the '<=". For example,
    [ <= Unsigned, Unsigned]					
Similarly, if there are no routine arguments, there is nothing between the '<=" and the ']':
    [ Unsigned <= ]						
Routine variables are particularly useful for some parameterized types and routines.

12. Parameterized Types

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:

    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		
Both the 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:
    define Bad_Type[Array[Key], Array[Value]]	# Bad Type	
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, 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:
	record
	    illegal1 Key[Unsigned]	# Bad type
	    illagal2 Array[Key[Unsigned]] # Bad type		
Now that we have the types defined, it is time to define a few routines to manipulate the types:
    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						
First, this routine creates an 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:

    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					
This routine searches 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:

    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					
This routine simple traverses the tree until a Tree_Element object that matches key is found (or not.)


Copyright © 2007-2010 by Wayne C. Gramlich. All rights reserved.