============================================================================= icalc - a complex-number expression language (C) 1991, 1992 Martin W. Scott. All Rights Reserved. Version 2.1 ============================================================================= Advanced Guide ============================================================================= This document describes the more advanced features of icalc, including programming constructs and array manipulation, as well as some information that was left out of the User Guide for clarity and continuity. The syntax used in icalc is closely related to that of C, with many exceptions. Information/warnings specifically for people who are familiar with C will be indicated by lines prefixed with 'C:'. Parentheses and braces will sometimes be used in examples where they don't strictly need to be, due to operator precedence rules. They are included for clarity. (NB: A table detailing relative precedences is in Appendix 1 of the User Guide.) Numbers ------- Icalc can read and display numbers in any base from 2 to 36. The most common bases used are 10 (almost always), 2 (binary) and 16 (hexadecimal). For bases greater than 10, the letters from A to Z (upper or lower-case) are used for the 'digits' 11 to 35. If you don't know what a number-base is, you may skip this section. When you input a number, icalc assumes it is decimal unless indicated otherwise. To inform icalc that you are entering a binary number, prefix it with a percent sign '%'. Similarly, to denote a hexadecimal number, use a '$' prefix. (Note that there should be no space between the prefix and the number). For any other base, you should precede the number with its base, a letter 'r' (in lower case) followed by the number itself. To input negative numbers in a base other than 10, use a minus sign '-' BEFORE the base-specifier. Examples: 255 - (a) decimal representation of number 255 %11111111 - binary representation of number 255 $FF - hexadecimal representation of number 255 3r100110 - base-3 representation of number 255 36r73 - base-36 representation of number 255 -$A - hexadecimal representation of number -10 Sometimes it is convenient to use scientific notation when representing very large or small numbers. Usually, the letter 'e' denotes that an exponent is given, as for example in 1e10 (a 1 followed by 10 zeros). However, for bases >= 15, 'e' is a digit. I am unaware of any standard convention in such cases, so opted to use the '@' character, which can be used with any base (you may still use 'e' for any base < 15 though). The exponent should be entered in the SAME BASE AS THE GIVEN NUMBER, and icalc displays numbers using this convention also (this seems the logical and consistent way to do things). More Examples: 1e5 - (a) decimal representation of the number 10000 %101e11 - binary representation of the number 40 (%101 = 5, %11 = 3, so %101e11 = 5 * 2^3) 8r7e7 - octal representation of the number 14680064 (8r7 = 7, so 8r7e7 = 7 * 8^7) $1@10 - hexadecimal representation of the number 16^16 When icalc displays a number using scientific notation, it always explicitly shows the sign of its exponent, eg. entering 1e30 would produce 1e+30. Fractions are input in the usual way as for decimal numbers. Thus $FF.FF represents the number 15*16 + 15*1 + 15/16 + 15/(16^2), whilst %101.101 represents the number 1*4 + 0*2 + 1*1 + 1/2 + 0/4 + 1/8. Note that to convert from one base to another, it is not enough just to convert each of the integer fraction and exponent parts separately. But anyway, you can use icalc for all base conversions you might want. To specify which base icalc should display numbers, use the builtin outbase(n) - this affects all numbers output, except array indices output with the display() builtin (see the section on arrays below). The display routine in icalc prints a number to a certain number of significant digits. This is 12 by default, but may be changed by the prec() builtin. Entering prec(n) sets the number of significant digits to n. n must lie in the range 1 to 55. prec() returns the last answer (ans) so that you can see the effect immediately. It allows values up to 55 only for representations in lower bases -- you don't get 55 digits of precision using decimal representations. To save time, the functions bin(), oct(), dec() and hex() are defined in the 'icalc.init' file for your use. They set respectively base 2, 8, 10 and 16, as well as setting a suitable precision for numbers to be displayed at. Generally speaking, icalc doesn't consider leading zeros in a fraction as significant, e.g. in the number 0.0012, the significant part is 12. If a fraction has more than the set number of significant digits, it is displayed in scientific notation. Whilst this gives the desired number of significant digits, it is sometimes aesthetically unpleasing. Therefore, a fixed number of leading zeros are condidered significant, by default 4. This can be altered by the sigzeros(n) builtin. Note that if n is zero, numbers will always be displayed with the set number of significant digits. To demonstrate what I mean, here's an example (assuming default settings): > PI 3.14159265359 > PI/10 0.31415926536 > PI/1000 0.00314159265 > PI/100000 # gives rise to 5 leading zeros 3.14159265359e-5 > sigzeros(5) 0.00003141593 ^ ^^^^ these 5 zeros are considered significant And finally, a small example on rounding errors. Recall from the user guide the example expression sin(2*x) - 2*sin(x)*cos(x) where x had the value 4, and the expression returned 2.22044604925e-16. This looks at first like a rather strange error, but turns out not to be. The following sample session shows why: > x = 4 4 > sin(2*x) - 2*sin(x)*cos(x) 2.22044604925e-16 > bin() # sets prec to 35 %1e-110100 > prec(55) # we can see what's happened, but let's see better %0.0000000000000000000000000000000000000000000000000001 and we see that the apparently strange error is actually caused by one bit in the computer's (binary) representation of the result. Not at all strange... Principal value ranges (PVRs) ----------------------------- The PVRs are defined for the inverse trigonometric functions as follows: asin: if u + iv = asin(z), then -PI/2 <= u <= PI/2 if v >= 0 -PI/2 < u < PI/2 if v < 0 acos: if u + iv = acos(z), then 0 <= u <= PI if v >= 0 0 < u < PI if v < 0 atan: if u + iv = atan(z), then -PI/2 < u < PI/2 Additional assignment operators ------------------------------- As well as the standard = operator, icalc comes with some others both to improve readability and efficiency of functions. They are: a += b equivalent to a = a+b a -= b equivalent to a = a-b a *= b equivalent to a = a*b a /= b equivalent to a = a/b It is faster to use these operators rather than the explicit operations. C: The ++ and -- operators are not included, since the evaluation method C: cannot handle pre- and post- operations rigorously. Relational operators -------------------- These operators allow comparisons between two values. Of course, the set of complex numbers has no logical ordering; all relational comparisons are done with real parts only, but equivalence comparisons are done on both real and imaginary parts. This system allows for people who are writing real-number applications to once again completely ignore complex numbers. To use relational operators on imaginary parts, you must use Im(). Relational operators are used in BOOLEAN expressions; these expressions can take one of two values: TRUE (1.0) and FALSE (0.0). In the table below, a and b represent numerical expressions, and E and F represent boolean expressions. BOOLEAN EXPRESSION TRUE if and only if a == b a has same value as b a != b a has different value to b a > b value of a greater than that of b a >= b value of a greater than or equal to that of b a < b value of a less than that of b a <= b value of a less than or equal to that of b E && F E is TRUE and F is TRUE E || F E is TRUE or F is TRUE Examples: 5 > 3 TRUE 2 != 2 FALSE 2 + 10i >= 20 + i FALSE (imaginary parts are ignored) 3 > 2i TRUE (2i has real part 0.0) -3 > 2i FALSE and as always, consider rounding errors when comparing: PI == atan(1)*4 FALSE The logical operators && and || use short-circuit evaluation. This means that they only evaluate their right-hand argument if the result of the operation is still undetermined. An example will clarify. (5 > 3) && (6 < 8) Needs to evaluate E and F E F (5 > 8) && (6 < 8) E evaluates to FALSE, so expression is false, E F and so F is not evaluated. This has some important implications, for example if (boolean) expression F contained an assignment expression, then it might not be evaluated depending on expression E. I have found that short-circuit evaluation is the most convenient way of handling && and ||. Some languages don't guarantee even order of evaluation (e.g. Pascal) and this can sometimes create harder to read programs. One other operator is present in icalc, as a shorthand for if-then-else statements, the ternary operator, '?:'. This has the form E ? a : b and reads as "if E is true then the value of the ternary operator is a, otherwise it's b". Only one of a or b is evaluated. An example will clarify: (x >= 2) ? (x -= 1) : (x = 100) means "If x is greater than 2, then subtract one from x, otherwise assign the value 100 to x". Numerical expressions can stand in for boolean expressions. A numerical expression has boolean value TRUE if its real part is non-zero, and FALSE if it is zero. So, for example, if x is purely real, y = (x ? 1 : 0) (*) is equivalent to y = (x != 0 ? 1 : 0) (**) and assigns 1 to y if x is non-zero, and 0 to y if x is zero. Do remember that imaginary parts are ignored in comparisons. In x were purely imaginary, (*) and (**) would assign 1 and 0 respectively to y (i.e. they would behave differently). Arrays ------ icalc has simple one-dimensional arrays, which can only be declared outside function definitions. Array elements are referenced using square brackets, and the first element by default has index 1, but this can be changed. C: This is in contrast to C, where indices start at zero. The system used in C: icalc is more suited to many mathematical applications. To create an array a, use the statement array a[dimension] # creates cleared array The dimension should be a positive integer. It may be an expression, which will have its imaginary part discarded, and rounded DOWN to an integer. All elements initially hold the value zero. You may optionally pre-assign the array at creation by using array a[dimension] = { expr1, expr2, ... exprP } This second form allows pre-initialization of elements 1 to P; P must be <= dimension (so can partially initialise array). Arrays are referenced by a[expr], the index being calculated as the real part of rounded to the NEAREST integer. NB: in version 2.0, the index was calculated as the floor of the real NB: value. Range checking is performed, and user will be informed of invalid array references. As already mentioned, arrays must be created at the top level, ie. NOT in function definitions. In this version, there are NO local (to function) arrays. There are a few builtin functions to help you manuipulate arrays: arraybase(n) index n now denotes 1st element of all arrays sizeof(a) returns number of elements in a, resize(a,n) resizes a to n elements, display(a) prints a list of elements in a. The latter two functions return a value 1 by convention. If you resize() an array to make it larger, the new elements created will NOT be set to zero. The arraybase routine lets you change how arrays are indexed. As already stated, the first element by default has index 1. Using arraybase, any non-negative integer may be used (primarily zero, as in C). arraybase(n) returns the old base, so you can write functions which set things up how the like at the start, and restore them when they return. Also, the constant ABASE holds the current setting, so functions can also use that to handle whatever preference is set. arraybase returns the value -1 if given an invalid argument. In the example scripts contained in the distribution of icalc, I have NOT provided for array bases other than 1, except in zroots.ic, where the routines change and restore the arraybase to suit their needs. If you wish to use the other routines with a different base, you will have to modify them using one of the above methods (I didn't want to confuse the examples with excess baggage). Here is a small example session to demonstrate some features: > array a[3] = { 1, 2, 3 } > display(a) a[1] = 1 a[2] = 2 a[3] = 3 1 > a[2] = 12 12 > a[3] < a[2] ? x = 3 : x = 2 3 > sizeof(a) 3 > resize(a,4) # a[4] will contain garbage 1 > a[4] = 4 4 > display(a) a[1] = 1 a[2] = 12 a[3] = 3 a[4] = 4 1 > resize(a,2) 1 > display(a) a[1] = 1 a[2] = 12 Blocks ------ Blocks are a way to group expressions so they are treated as one big expression. They have the form { expr1; expr2; ... exprN; } and the value of a block is the value of its last expression, exprN. The expressions may also be separated by newlines (as many as you like)instead. Blocks can have as little as one expression, which is sometimes useful for easier-to-read source code. Examples: > { a=3;b=2;a*b;} 6 > { > a = 2 > b = 3 > c = 4; d = 5 > a*b*c*d > } 120 Blocks are principally of use with control-flow constructs, and further (more useful) examples are given below. Control-flow constructs ----------------------- A number of control-flow constructs are available, closely modelled on those of the C language. C: C is a free-form language, and spacing can appear anywhere in C source. C: icalc however, is free-form only per-line, due to its interactive nature; C: icalc must be able to determine when an expression stops, so that it knows C: when to print a result. Care should be taken to follow icalc's conventions. Where newlines are shown, they are optional; where they are not shown, they are not permitted, and will either cause a syntax error, or incorrect behaviour. All horizontal spacing is optional, and used for clarity. Below, Bexpr denotes a boolean expression, Texpr is the expression evaluated if Bexpr is TRUE, and Fexpr the expression evaluated if Bexpr is FALSE. if-else ------- A construct common to almost all programming languages is the if-statement. In icalc, it has the form if (Bexpr) Texpr or with an else-clause, if (Bexpr) Texpr else Fexpr Note that 'else' keyword must be on same line as Texpr. If it were allowed on the next line, non-else-clause if-statements would cause problems in interactive use. It sometimes looks ungainly, but blocks may be used to spread it over lines, as shown below. Some examples: if (a == 2) b = a+1 if (a == 2) b = a+1 else b = a-1 if (a == 2) { b = a+1 } else { b = a-1 } while-loop ---------- This is an iteration construct, of the form while (Bexpr) Texpr and reads as "while the boolean expression Bexpr evaluates to TRUE, evaluate Texpr". The following example calculates n!, where n is an integer: f = 1; n = 10; # initialize f, n while (n > 0) # n not yet zero, so still work to do { f *= n # multiply f by n n -= 1 # decrement n } # f now holds value of 10! do-while-loop ------------- This is another iteration construct, closely related to a while-loop, with the distinction that the loop-test (the boolean expression governing the loop's operation) is performed AFTER the body of the loop. It has the form do Texpr while (Bexpr) Texpr is evaluated first; then, if Bexpr is TRUE, Texpr is re-evaluated and Bexpr tested again; and so on... Whereas with a while-loop, the Texpr may never be evaluated (if Bexpr evaluates to FALSE initially), in a do-while-loop, Texpr is always evaluated at least once. An example -- sin() applied five times to a value x: n = 5; s = x # n is number of times to apply sin() do { s = sin(s) # apply sin() n -= 1 # decrement n } while (n >= 1) # more to do? # s has value sin(sin(sin(sin(sin(x))))) (sin 5 times) for-loop -------- The for-loop in icalc is like that of the C language, and has the form for (initexpr; condexpr; incexpr) expr initexpr, condexpr and incexpr are all optional (but semi-colons ; are not). The for loop is a shorthand for initexpr while (condexpr) { expr incexpr } Example: array a[100] for (n = sizeof(a); n > 0; n -= 1) a[n] = 1 # all elements of a are 1. C: icalc's for-loop is not as flexible as C's; there is no comma operator, C: and initexpr, condexpr and incexpr can only be simple (not blocks). Function definitions -------------------- The User Guide explains how to define simple functions. As mentioned there, you can use a block as the body of a function (e.g. rec2pol definition). There are more sophisticated facilities available, which provide a similar functionality to that of C. However, for a number of reasons, the syntax used in icalc differs widely from C's. Function parameters may be one of four types: VALUE: These are the normal parameters that we've been using all along. POINTER: These are like variable parameters in Pascal. Denoted in parameter list by *. ARRAY: These are passed by reference, NOT value, as in C Denoted by @. EXPR: Strange one this. It's like a pointer to a function, but more/less flexible depending on how you look at it. Example below will clarify. Denoted by ~. Note that, although a function cannot have local arrays, the array reference parameter IS local, and completely independent of arrays defined globally. Functions may also have local variables, introduced by a statement local v1,v2,v3,...,vN within the function body. There can be as many local declarations as you like. All local variables behave like VALUE variables - there are no local pointers or arrays as yet. C: local declarations apply to the function they appear in; there are no C: block-local variables. The following small suite of functions demostrate the ideas above. # swaps values in variables a and b. func swap(*a,*b) = { # a and b are pointers local tmp # only expressions in swap() know about tmp tmp = a a = b b = tmp } x = 5; y = 7 swap(x,y) # x now holds 7, y holds 5 # nexpr is an expression in (global) variable n # a is an (arbitrary) array. # fill evaluates Nexpr for every index n of array a, and places # result in a[n]. func fill(@a, ~nexpr) = { for (n = sizeof(a); n > 0; n -= 1) a[n] = nexpr; # just referencing nexpr causes # it to be evaluated. } The return value of a function is the value of the last expression evaluated. Sometimes it's inconvenient for this to be the case, so the return statement is provided. A line of the form return a encountered anywhere during function evaluation will end the function call, returning the value of a. I'm sure you will agree that these mechanisms allow many sophisticated operations to be performed by user-functions. The examples in this document are simple-minded for the most part, to give you the bare facts about what can be done. I have included many sample script-files containing one or more function definitions which are useful to a varying degree, and in some cases add very powerful extensions to icalc. Script files ------------ The script files include: array.ic - utility routines for arrays arraytest.ic - tests routines defined in array.ic bern.ic - computes Bernoulli numbers cheby.ic - computes coefficients for Chebychev polynomials gamma.ic - gamma and related functions gl.ic - integration by 10-pt Gauss-Legendre quadrature loop.ic - demonstrates all looping constructs poly.ic - polynomial support routines root.ic - root-finding of functions sort.ic - routine to perform Shell sort on arrays trig.ic - some rarely used trig-functions zroots.ic - find all roots of a polynomial (real & complex) polint.ic - trapzd.ic | qtrap.ic |- numerical integration routines qsimp.ic | (see Integration.notes in Scripts directory). qromb.ic - Undocumented commands --------------------- There is a 'clear' command, which deletes from memory a variable, array or function. However, care MUST be taken, since icalc does not check that the object being deleted is not referenced elsewhere (in a function). If it is referenced after it has been deleted, the system will more than likely crash. The command was only really added to delete large arrays which take up a lot of memory. You have been warned... clear Caveats ------- Assignment ---------- When assigning variables within another expression, it is safer to enclose the assignment in parentheses, to ensure that the expression is evaluated correctly. Defining functions in terms of other functions ---------------------------------------------- A function is stored internally as a structure, and a reference to a function, f, from another, g, creates a pointer to the structure for f. Thus, changing f changes g also. An example will illuminate this: > func f(x) = x*x > func g(x) = f(2*x) > f(4) 16 > g(4) 64 > func f(x) = sqrt(x) # changes behaviour of g > f(4) 2 > g(4) 2.82842712 Therefore, you cannot use function definitions as a kind of function assignment. Comparisons ----------- Due to the inexactness of computer calculations, you must be careful when using comparison operators. Again, an example will illuminate: > j = 12*PI 37.69911184 > while j != 0 do {print(j); j = j-PI;} 37.69911184 34.55751919 : 3.14159265 -3.5527136e-15 <--- not quite zero -3.14159265 : etc. The arrow points to the position where, because of rounding errors, j is not exactly zero, and the loop never terminates, since the condition j != 0 is always satisfied. There are two ways to get round problems like this. You can use a >= operator, or, if comparison is with (theoretically) integral values, the int() function, which rounds its argument to the NEAREST integer. Questions --------- I've tried to cover everything in a clear and precise way, but almost certainly have failed in some areas. If you have any questions about icalc, write to me and I'll respond as soon as I can. Addresses are in the User Guide. Also, if you write any scripts which you feel other users may find useful (for example, routines applying to physics or astronomy calculations) please send them to me for inclusion with the next release of icalc. Enjoy, Martin Scott.