|
1.1 Compiling
Download the tar file ee-version.tar.gz, and extract the source tree, run
make, and copy the executable ee into a directory in your path:
tar -zxvf ee-version.tar.gz
make
cp ee /usr/local/bin
or on a Windows system (with a gcc toolchain installed, such as MinGW), use
make -f Makefile.win32
1.2 The Infamous Greeting
ee -c 'print("Hello, World\n")'
Hello, World
Ok, now that we got that out of the way...
2e programs are composed of expressions (actually, one large expression
consisting of one or more sub-expressions).
White space is mostly ignored, including line breaks (so they can appear
pretty much anywhere to improve readability). Also, anything on a line
following a "#" character is treated as a comment and ignored.
An expression is something like this:
2 + 3 * 5
where "3 * 5" is a sub-expression of the full expression, "2 + 3 * 5".
An expression can be evaluated via ee in a couple ways; either on the command
line:
ee [ -p | -c] 'expression'
-- or --
ee filename
In the first form, if "-p" is used, the expression is evaluated and the result
printed. On the other hand, "-c" will just evaluate the expression. Use this
if your expression contains output functions such as print(). The second
form will execute the program in the given filename. This is useful for longer
programs, or for placing the line #!/usr/local/bin/ee at the top of
the script to automatically run it under ee when called, similar to other
interpreters on Unix type environments.
For the first several examples, we'll use the first form:
$ ee -p '(12 + 144 + 20 + 3 * 4 ** .5) / 7 + 5 * 11'
81
1.3 More MeatSpecial operators, variables, assignments
Values can be assigned to variables using the "=" operator. Variable names
can contain alpha, numeric, or the underscore "_" characters, and are case
sensitive. They also must not begin with a numeric. The assignment returns
the value of the right side of the operator. This works just like C.
$ ee -p 'a = 7; b = 11; x = a * b'
77
The semicolon ";" is used to join multiple sub expressions. It is a binary
operator, similar to the standard math operators, but has a lower precedence.
It evaluates the expression on the left (including side effects, such as
assignment and function calls), then it returns the result of what is on the
right. Since it is a normal operator, it can be used wherever needed,
including within parentheses, so the expression
foo = (expr1 ; expr2) will
assign the result of expr2 to the variable foo .
Without the parentheses, foo will be set to expr1 ,
since "=" has a higher precedence then ";" does (i.e., a higher precedence
operator binds "tighter" than a lower precedence, so it is evaluated first).
Another special purpose operator is actually a pair of operators, "?" and ":",
called the conditional operator. Similar to the C conditional, it is used
like this:
exprc ? exprt : exprf
The way it works is exprc is evaluated (which is typically a
comparison expression), and if true (i.e., returns a non-zero result) then
exprt is evaluated and it's result returned as the value of the
whole expression. Otherwise, if false, then exprf is evaluated
and returned.
$ ee -c 'a = 1; a < 10 ? print("True\n") : print("False\n")'
True
Either exprt or exprf may contain
several sub-expressions spread over multiple lines, each sub-expression
separated by a semicolon operator. That way, the conditional expression can be
used similar to an "if / else" clause. However, if a semicolon is used
within either the true or false targets, then the target expression must be
enclosed in parentheses, since a semicolon is of lower precedence than the
conditional. Which leads to a shortcut implemented in 2e:
$ ee -c 'x = 8; x % 2 == 0 ? print(x, " is even\n"); print("End of program\n")'
8 is even
End of program
The rule is, that if an operator of lower precedence than "?" is reached
prior to the ":" operator, then the conditional is terminated automatically
with a ": 0 " as the false condition (unless, of course, the
operator in question is part of a parentheses enclosed expression). This
also occurs if an unmatched closing parenthesis is encountered, or the end
of the program is reached prior to an ":" (else) operator. Therefore,
the following three are all equivalent:
x = 2 > 3 ? 4 : 0; ...
x = 2 > 3 ? 4; ...
x = (2 > 3 ? 4); ...
There is also an iterative (looping) form of the conditional operator, "?? :",
which functions as follows:
$ ee -c 'a = 1; a <= 5 ?? (print(a, "\n"); a++) : print("Error...\n")'
1
2
3
4
5
$ ee -c 'a = 6; a <= 5 ?? (print(a, "\n"); a++) : print("Error...\n")'
Error...
In the first example the true target was evaluated, which printed out the value
and incremented the variable "a". This was repeated until the condition,
"a <= 5 " was false, in which case the whole conditional exited.
The second example, however, set "a" to be a value which cause the condition
"a <= 5 " to start off false. In this case, and this case only,
the false target (after the ":") is evaluated. Normally, this would be a
good place to put some type of error handling expression if you would typically
expect the condition to be true at least once. But usually you don't need
the false target, so you can use the above mention shortcut and terminate
the iterative conditional expression with an operator of lower precedence
than "??", such as ";" or a closing parenthesis if appropriate.
Also, these examples are starting to get harder to read, so we'll switch to
a more appropriate coding style and start putting the larger scripts into
an executable file that would look like this:
#!/usr/local/bin/ee
a = 6;
a <= 5 ?? (
print(a, "\n");
a++
) : (
print("Error...\n")
)
Of course, once you start doing this, you will have to remember that the ";"
operator isn't a statement terminator but a normal binary operator. This
means that in the above example, you don't have a ";" after the
a++ line or the print("Error...\n") line, since
they are immediately
followed by a closing parenthesis.
1.4 Variable types, Arrays
Variables can have one of the following types: integer, floating point,
character, and string . The type is set automatically whenever a value
is assigned to it. If a different value type is later assigned to the
variable, the variable type is automatically adjusted. So,
foo = 3
foo is an integer in this case. And
foo = 7.3
foo is then changed to a floating point after it is reassigned. Also:
x = 7.4;
y = 2;
result = x + y
Since the value of 7.4 + 2 ends up being a
floating point, then result is a floating point type. Same as
if you had "result = 7.4 + 2.6 ", result would still
be a float even though it's value, 10, is a whole number.
Single character values are created with single quotes, such as:
c = 'A';
t = '\t'
Special characters are referenced when prefixed with a backslash. The one
above, '\t', is a tab. Other special characters are '\b' (backspace), '\n'
(newline), '\r' (return), '\\' (backslash), and '\'' (single quote).
Character strings are enclosed in double quotes, and can contain the same
escape sequences mentioned above...
str = "This is a test\nSecond line of the test";
Array variables are automatically created when either it is referenced, or
another array is assigned to a variable. If a variable previously contained
another data type, then as soon as it is referenced as an array the variable
is destroyed and re-created as an array. Arrays are indexed, starting at 0,
by following a variable with the index enclosed in brackets:
my_array[0] = 2;
my_array[1] = 3;
my_array[2] = 5;
Multi-dimensional arrays are also supported: my_array[3][4] = x;
By default, arrays are set up as generic arrays. Any given value can be
any data type, floating point, integer, string, or even another array.
In fact, that's how multi-dimensional arrays are implemented -- in the
previous example, my_array[3] is an array element with a data type of generic
array. So in reality, a 2-dimensional array is actually an array of arrays,
which means that the dimensions are infinite in all directions (up to memory
limits, of course).
In addition to the general purpose generic arrays, single data type arrays
are also supported. For example, a string is actually an array of characters:
$ ee -c ' bla = "This is a test"; print(bla[8], "\n")'
a
The main thing to note about typed arrays is they can only be one dimensional,
and you can't assign a value of a different data type to one of it's elements.
You can, however, have a general purpose array set up as an array of character
arrays, which will give you the same effect as a two dimensional array.
If you have a number of items you want to initialize in an array, you can
either do them one at a time via a series of assignments:
ary[0] = 1.3;
ary[1] = 3.7;
ary[2] = 19;
ary[3] = 4;
...
Or, you can enclose the values in curly braces "{ ... }", which is an array
initializer:
ary = { 1.3, 3.7, 19, 4 };
The array initializer returns a generic array which can be used in place, or
assigned to a variable, like above. So it is technically legal to code:
print({ 1.3, 3.7, 19, 4}[2]) which would return the number 19
as it's value. Not that this is actually useful, but it is legal.
Multi dimensional arrays can also be initialized:
#!/usr/local/bin/ee
ary = {
{ 1; 2; 3 };
{ 4; 5; 6 };
99
};
print(ary[0][1], " ", ary[1][2], " ", ary[2], "\n")
Which returns:
2 6 99
as it's output.
You can also initialize typed arrays. That is, you can create an array of
integers, floating point, or character values, by prefixing the opening brace
with an i, f, or c,
such as
intary = i{ 1, 2, 3};
floatary = f{ 2.7, 3.4, 7.9};
chararray = c{ 'a', 'b', 'c' } # same thing as chararray = "abc"
Note that typed arrays can only hold values of the given type, and type
conversions are not automatic at this time. So you may have to use the totype()
functions, as in myarray = c{ tochar(65), tochar(66) }
1.5 Functions
Both internal (built-in) and user-defined functions are available. User
defined functions are ones that are declared within the 2e code. Internal
functions are pre-defined and built into the into the interpreter (such
as the print() function used in the previous examples). These internal
functions can be supplemented with external function libraries by using the
function call loadlib() , as documented in the reference
manual.
Here's a sample user defined function, which raises a number to a given power.
Of course, this simple pow() function only works with integer exponents
greater than 1. There is a built-in exponent operator, "**" which is part
of the base language, which works on floating point exponents also.
#!/usr/local/bin/ee
print(pow(2, 3));
@pow(
b = $1; #base
e = $2; #exponent
r = 1; #result
i = 1;
result = (
i <= e ?? ( # while "i" is less than or equal to "e"
i++;
r = r * b # once "i <= e" returns false, this will be the expression value
) : ( # otherwise if "i <= e" starts off false,
r = 0 # then this becomes the return value
)
);
result;; # ;; is a unary postfix operator, and causes the
# function to return the value on its left
)
Function definitions begin with the "@" symbol followed by the function name,
then followed by an expression (composed of one or more sub expressions)
enclosed in parentheses. When the function flow encounters a ";;", the
function is terminated and the value of the expression to the left of ";;"
is returned as a result. The ";;" (return) operator is a unary operator with
left/right association (i.e., a postfix unary operator), with a precedence
equal to ";". It is legal to use it anywhere a unary postfix operator is
usable, and if it is missing from the end of the function, then it will be
automatically assumed. Finally, function parameters are passed in the function
call by separating them with "," operators and parameters are referenced
by "$x", where "x" is an integer starting at 1 (and can also be an integer
variable). $0 will return the number of parameters passed to the function.
Note that this is the same syntax used to retrieve parameters that have been
passed from the command line.
In the above example, "i <= e ?? ( ... ) : ( ...) "
is an iterative conditional expression. As you recall, the first and middle
expressions are evaluated repeatedly until the first expression is no longer
true, then the final result of the middle expression (i++ ; r = r * b
returns r = r * b as it's result) is the value of the
conditional. It is this value then that gets assigned to "result", which is
returned with the ";;" (return) unary operator.
The third expression of the conditional is ignored, unless the condition
in the first expression started out false. In that case only, this third
expression is evaluated and it becomes the value of the iterative conditional
expression, and assigned to "result".
A number of built-in functions are provided. Some are necessary as they
provide functions that aren't part of the base language (such as file i/o and
certain variable manipulation). Others are general utility type functions
which could be implimented within 2e, however their usage is common enough
that they have been also provided. See the reference section of this manual
for a complete list.
Function name assignments and Anonymous functions
Function names can be assigned to other variable names, or array values,
which then act as an alias for that function. For example, you can code
mypow = pow
and then use mypow(3, 2) to call the pow() function. Not
very useful in itself, however this works with array values too. So lets
say you have a process that returns an integer value from 0 thru 2, and
you wanted to call a function based on that value. You code write the
following:
result == 0 ? # if result is equal to 0,
foo(arguments) # call function foo()
: result == 1 ? # else if result is 1, then
bar(arguments) # call function bar(), etc...
: result == 2 ?
xyzzy(arguments);
but that can be fairly inefficient, especially if you end up with a fairly
long list of functions. A better way is to build a "state machine", which is
a programming construct that immediately calls a particular section of code
based on the state of a given variable, without going throug a whole
if/else if/... comparison table. You can do that like this:
handler = { foo, bar, xyzzy }; # these are functions defined elsewhere
...
# sometime later after "result" is set ...
handler[result](arguments);
A couple of real-world examples of where you'd use this syntax is perhaps
writing a terminal emulator, in which you have to jump to a handler routine
based on encountering specific screen control sequences, or perhapse when
writing an interpreter where you have a list of handler functions -- one for
each operator in an expression.
But what if you only have a short example, and you didn't want to set up the
handler functions in a different part of the code? Well, you can also code
up handler functions at the time of assignment by using anonymous functions.
An anonymous function is a function that doesn't have a name, but the definition
of it returns a value that can be assigned to a variable and used later.
myfunc = @@(expression );
- or -
handler = {
@@(expression_foo);
@@(expression_bar);
@@(expression_xyzzy)
};
The syntax looks very much like a normal function definition, but there is
no function name after the "@" symbol. But, the contents are like any
other function - what appears between the parentheses can be as simple or
complex as needed. If a single "@" symbol is used, then variables within
share their scope with the parent function. If two are used "@@", then
variable scope is local to that function.
|